qcast-front/docs/PDF Gemini 평면도 분석 기능 구현 계획.md
2026-05-19 16:20:48 +09:00

167 lines
7.1 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# PDF 평면도 → Gemini 분석 → 캔버스 반영 구현 계획
> 작성일: 2026-05-18
> 대상 브랜치: `claude/lucid-shaw-91e51f` (dev `f32f9187` 분기)
> 모델: `gemini-3.1-pro-preview` · 좌표 단위: `mm` · 진입 위치: floor-plan 상단 툴바
---
## 1. 기능 요구 사항
1. 기본 프로세스 상에서 특정 버튼 클릭으로 사용자가 PDF 문서를 업로드한다.
2. Gemini API 로 해당 문서의 내용 중 평면도를 분석한다.
3. 분석 내용을 응답으로 전달받고 현재 프로세스(캔버스)로 전달한다.
4. 분석이 성공적으로 마무리되면 업로드한 파일은 폐기 처리한다.
---
## 2. 결정사항
| 항목 | 선택 |
|---|---|
| 워크트리 | 현재 워크트리(`lucid-shaw-91e51f`)에서 진행 |
| 캔버스 반영 형태 | 외곽선 폴리곤 좌표(JSON) |
| 버튼 위치 | floor-plan 상단 툴바 |
| API 호출 위치 | 서버 라우트 (`/api/gemini/floor-plan`) |
| Gemini 모델 | `gemini-3.1-pro-preview` |
| 좌표 단위 | `mm` |
| 툴바 아이콘 | 신규 PDF 아이콘 추가 |
---
## 3. 전체 흐름
```
[툴바 버튼] → [PdfImportModal] → POST /api/gemini/floor-plan (multipart)
Gemini File API 업로드
외곽선 좌표 JSON 응답
Gemini 파일 삭제 (finally)
응답 좌표 → 좌표 검증/정규화 → outerLinePointsState + QLine/QPolygon 생성
```
---
## 4. 핵심 파일 (수정/신규)
| 영역 | 경로 | 변경 |
|---|---|---|
| 서버 라우트 | `src/app/api/gemini/floor-plan/route.js` | **신규** — multipart 수신 + Gemini 호출 + finally 폐기 |
| 환경설정 | `src/config/config.*.js` | `GEMINI_API_KEY`, `GEMINI_MODEL` 추가 |
| 의존성 | `package.json` | `@google/generative-ai` 추가 |
| 메뉴 등록 | `src/store/menuAtom.js` (`menusState`) | `{ type: 'pdf-import', name, icon }` 항목 |
| 툴바 분기 | `src/components/floor-plan/CanvasMenu.jsx` (`onClickNav`) | `case 'pdf-import'` 추가 |
| 업로드 모달 | `src/components/floor-plan/modal/pdfImport/PdfImportModal.jsx` | **신규** — 파일 선택/진행/에러 |
| 캔버스 반영 hook | `src/hooks/pdf-import/usePdfImport.js` | **신규** — 좌표 검증 + 캔버스 반영 |
| 다국어 메시지 | 메시지 키 추가 | `pdf.import.*` (ja/ko) |
| 아이콘 | `public/` 또는 기존 아이콘 디렉토리 | PDF 임포트용 SVG 추가 |
---
## 5. 서버 라우트 설계 (`/api/gemini/floor-plan`)
### 5-1. 입력
- `multipart/form-data`
- 필드:
- `file`: PDF (≤ 20MB, MIME `application/pdf`)
- `unitHint` (옵션): `mm` | `auto`
### 5-2. 처리 단계
1. `formData.get('file')` 로 PDF 수신 → 사이즈/MIME 검증
2. `GoogleGenerativeAI(GEMINI_API_KEY)` 클라이언트 생성
3. **Gemini File API 업로드**`fileUri` 획득
4. 모델 호출 (`gemini-3.1-pro-preview`)
- 프롬프트 핵심:
- 역할: "건축 평면도 외곽선 추출 전문가"
- 출력: JSON only, 단위 mm
- 스키마: `{ outerline: [{x,y}, ...], unit: 'mm', scale?: number, confidence: 0..1, notes?: string }`
- "평면도가 아니면 `{ outerline: [], notes: '...' }` 반환"
- `response_mime_type=application/json`, `response_schema` 강제
5. JSON 파싱 → 좌표 검증
- vertex ≥ 3
- 자기교차 검출 (turf 활용)
- 0 면적 거부
6. **finally** 블록에서 `genai.files.delete(uploadedFile.name)` — 성공/실패 무관 삭제
### 5-3. 출력
- 성공: `{ outerline: [{x,y}, ...], unit: 'mm', confidence }`
- 실패: `{ error: { code, message } }`
### 5-4. 보안 / 운영
- `GEMINI_API_KEY` 는 서버 env 만 사용, 클라이언트 노출 금지
- S3 등 영구 저장소에 절대 저장하지 않음 (요구 4번: 폐기)
- 로깅: `src/util/logger.js` 만 사용, raw 응답은 `NEXT_PUBLIC_ENABLE_LOGGING=true` 일 때만 출력
---
## 6. 클라이언트 흐름
### 6-1. 모달 (`PdfImportModal`)
- 상태머신: `idle → uploading → analyzing → success | error`
- 호출: `fetch('/api/gemini/floor-plan', { method: 'POST', body: formData })`
- 성공: `usePdfImport.applyOuterline(points, unit)` 호출 후 모달 닫기
- 실패: `useSwal` 로 에러 메시지 표시
### 6-2. 캔버스 반영 hook (`usePdfImport`)
- **단위 변환**: `unit === 'mm'` 응답 → canvas px/mm 비율로 변환
- **정규화**: 시계방향 정렬, bounding box 중앙 정렬(translate)
- **기존 외곽선 분기**:
- 존재 시 `useSwal` confirm: "기존 외곽선을 덮어쓰시겠습니까?"
- 빈 캔버스 시 즉시 적용
- **적용**:
- 점들을 `name === 'outerLine'` QLine 객체들로 생성하여 캔버스에 add
- `setOuterLinePoints(points)` (recoil `outerLinePointsState`)
- `addPolygonByLines(lines)` 흐름으로 QPolygon 생성 (기존 `useRoofShapeSetting` 자산 재사용)
- `canvas.renderAll()`
---
## 7. 검증 / 엣지 케이스
| 케이스 | 대응 |
|---|---|
| PDF 가 아닌 파일 | 라우트에서 MIME 거부 |
| 평면도가 아닌 PDF | 모델이 빈 outerline 반환 → 친화적 메시지 |
| 좌표 ≤ 2개 / 자기교차 / 0면적 | 라우트에서 거부 (`SizeSetting` 가드와 일관) |
| 모델 JSON 파싱 실패 | retry 1회 후 실패 메시지 |
| Gemini 호출 실패 | finally 에서 File API 삭제 보장 |
| 캔버스 기존 외곽선 존재 | confirm 후 덮어쓰기 |
| 응답이 거대한 폴리곤(>500 vertex) | DouglasPeucker 단순화 또는 거부 |
| 업로드 파일 크기 초과 | 클라이언트/서버 양쪽 가드 |
| 네트워크 실패 | 모달에 재시도 버튼 + 파일 폐기 보장 |
---
## 8. 컨벤션 / 운영
- Prettier: single quote, **no semicolons**, tabWidth 2, printWidth 150
- 메시지/모달: `useSwal` + `useMessage` (raw `alert()` 금지)
- 다국어: `ja/ko` 양쪽 키 추가 (`pdf.import.*`)
- 경로 alias: `@/...` 우선
- 로깅: `src/util/logger.js` 만 사용
### 커밋 단위
1. `[xxxx] feat(api) — Gemini 평면도 분석 라우트 신설`
2. `[xxxx] feat(floor-plan) — PDF 임포트 모달/툴바 진입`
3. `[xxxx] feat(floor-plan) — 분석 결과 외곽선 캔버스 반영`
4. `[xxxx] chore — 다국어 메시지/lint 정리`
마지막에 lint/type/build subagent 점검.
---
## 9. 작업 순서 (To-Do)
1. `@google/generative-ai` 의존성 추가, `GEMINI_API_KEY`/`GEMINI_MODEL` env·config 매핑
2. 서버 라우트 `src/app/api/gemini/floor-plan/route.js` 작성 (Gemini 호출 + 파일 폐기)
3. `menusState``pdf-import` 항목 + PDF 아이콘 SVG 추가
4. `CanvasMenu.jsx` `onClickNav` switch 에 케이스 추가, `modalAtom` 으로 모달 오픈
5. `PdfImportModal.jsx` 작성 (idle→uploading→analyzing→success/error 상태머신)
6. `usePdfImport.js` 작성 (단위/방향 정규화, 외곽선 덮어쓰기 분기, QLine/QPolygon 반영)
7. 다국어 메시지 키 추가 (ja/ko)
8. `yarn lint` + dev에서 실제 PDF 한두 건 manual test
9. 의미 단위로 커밋