# 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) | Douglas–Peucker 단순화 또는 거부 | | 업로드 파일 크기 초과 | 클라이언트/서버 양쪽 가드 | | 네트워크 실패 | 모달에 재시도 버튼 + 파일 폐기 보장 | --- ## 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. 의미 단위로 커밋