diff --git a/.gitignore b/.gitignore index 7f720b6a..faa32996 100644 --- a/.gitignore +++ b/.gitignore @@ -57,11 +57,12 @@ sl graphify-out/ # [local-only 2026-05-08] 모든 markdown 문서는 git 추적 제외 (로컬 전용) -# 단, 루트 README.md / CLAUDE.md / AGENTS.md, .agents 및 .claude 하위의 md 는 추적 유지 +# 단, 루트 README.md / CLAUDE.md / AGENTS.md, docs, .agents 및 .claude 하위의 md 는 추적 유지 *.md !/README.md !/CLAUDE.md !/AGENTS.md +!/docs/**/*.md !.agents/**/*.md !.claude/**/*.md diff --git a/docs/PDF Gemini 평면도 분석 기능 구현 계획.md b/docs/PDF Gemini 평면도 분석 기능 구현 계획.md new file mode 100644 index 00000000..7a5db65a --- /dev/null +++ b/docs/PDF Gemini 평면도 분석 기능 구현 계획.md @@ -0,0 +1,166 @@ +# 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. 의미 단위로 커밋