@ -1,408 +0,0 @@
<!DOCTYPE html>
< html lang = "ko" >
< head >
< meta charset = "UTF-8" / >
< meta name = "viewport" content = "width=device-width, initial-scale=1.0" / >
< title > PDF 도면 → 외벽선 자동작성 · 코드베이스 현황 분석< / title >
< style >
:root {
--bg: #0e1117;
--panel: #161b22;
--panel-2: #1c232d;
--border: #2a323d;
--text: #e6edf3;
--muted: #8b949e;
--accent: #4493f8;
--green: #3fb950;
--amber: #d29922;
--red: #f85149;
--purple: #bc8cff;
--code-bg: #0b0f14;
--mono: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace;
}
* { box-sizing: border-box; }
body {
margin: 0;
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", "Apple SD Gothic Neo", "Noto Sans KR", sans-serif;
background: var(--bg);
color: var(--text);
line-height: 1.7;
font-size: 15px;
}
.wrap { max-width: 1080px; margin: 0 auto; padding: 48px 24px 96px; }
header.hero {
border: 1px solid var(--border);
border-radius: 16px;
padding: 36px 36px 32px;
background: linear-gradient(135deg, #161b22 0%, #1a2330 100%);
margin-bottom: 36px;
}
.eyebrow { color: var(--accent); font-weight: 600; font-size: 13px; letter-spacing: .08em; text-transform: uppercase; }
h1 { font-size: 30px; margin: 10px 0 6px; line-height: 1.25; }
.sub { color: var(--muted); font-size: 15px; margin: 0; }
.meta-row { display: flex; flex-wrap: wrap; gap: 8px; margin-top: 22px; }
.pill {
display: inline-flex; align-items: center; gap: 6px;
font-size: 12.5px; padding: 5px 12px; border-radius: 999px;
border: 1px solid var(--border); background: var(--panel-2); color: var(--muted);
}
.pill.ok { color: var(--green); border-color: rgba(63,185,80,.4); background: rgba(63,185,80,.08); }
.pill.warn { color: var(--amber); border-color: rgba(210,153,34,.4); background: rgba(210,153,34,.08); }
h2 { font-size: 21px; margin: 48px 0 16px; padding-bottom: 10px; border-bottom: 1px solid var(--border); }
h2 .num { color: var(--accent); font-variant-numeric: tabular-nums; margin-right: 10px; }
h3 { font-size: 16.5px; margin: 28px 0 10px; color: #c9d4e0; }
p { margin: 12px 0; }
a { color: var(--accent); text-decoration: none; }
a:hover { text-decoration: underline; }
code {
font-family: var(--mono); font-size: 12.8px;
background: var(--code-bg); border: 1px solid var(--border);
padding: 1.5px 6px; border-radius: 5px; color: #9fd2ff;
white-space: nowrap;
}
.path { color: var(--purple); }
/* TL;DR callout */
.tldr {
border: 1px solid rgba(63,185,80,.35);
background: rgba(63,185,80,.07);
border-radius: 14px; padding: 22px 26px; margin: 8px 0 12px;
}
.tldr h3 { margin-top: 0; color: var(--green); font-size: 15px; letter-spacing:.04em; text-transform: uppercase; }
.tldr p { margin: 8px 0; }
.tldr strong { color: var(--text); }
/* cards grid */
.grid { display: grid; grid-template-columns: repeat(auto-fit, minmax(240px,1fr)); gap: 14px; margin: 18px 0; }
.card {
border: 1px solid var(--border); background: var(--panel);
border-radius: 12px; padding: 18px 20px;
}
.card .k { font-size: 12px; color: var(--muted); text-transform: uppercase; letter-spacing: .05em; }
.card .v { font-size: 17px; font-weight: 650; margin-top: 4px; }
.card .d { font-size: 13px; color: var(--muted); margin-top: 6px; }
/* timeline */
.timeline { list-style: none; padding: 0; margin: 18px 0; }
.timeline li {
position: relative; padding: 0 0 22px 32px; border-left: 2px solid var(--border); margin-left: 8px;
}
.timeline li:last-child { border-left-color: transparent; }
.timeline .dot {
position: absolute; left: -9px; top: 2px; width: 16px; height: 16px; border-radius: 50%;
background: var(--bg); border: 3px solid var(--muted);
}
.timeline li.now .dot { border-color: var(--green); box-shadow: 0 0 0 4px rgba(63,185,80,.15); }
.timeline li.off .dot { border-color: var(--amber); }
.timeline .when { font-family: var(--mono); font-size: 12px; color: var(--muted); }
.timeline .what { font-weight: 600; margin: 2px 0; }
.timeline .desc { font-size: 13.5px; color: var(--muted); }
.hash { font-family: var(--mono); color: var(--accent); font-size: 12.5px; }
/* flow diagram */
.flow {
background: var(--code-bg); border: 1px solid var(--border); border-radius: 12px;
padding: 24px; margin: 18px 0; overflow-x: auto;
}
.flow pre { margin: 0; font-family: var(--mono); font-size: 12.5px; line-height: 1.75; color: #c9d4e0; white-space: pre; }
.flow .c-cli { color: var(--accent); }
.flow .c-srv { color: var(--green); }
.flow .c-ext { color: var(--purple); }
.flow .c-cmt { color: var(--muted); }
/* table */
table { width: 100%; border-collapse: collapse; margin: 16px 0; font-size: 13.8px; }
th, td { text-align: left; padding: 11px 13px; border-bottom: 1px solid var(--border); vertical-align: top; }
th { color: var(--muted); font-weight: 600; font-size: 12.5px; text-transform: uppercase; letter-spacing: .04em; background: var(--panel-2); }
tr:hover td { background: rgba(255,255,255,.015); }
td.center { text-align: center; }
.tag { font-size: 11.5px; padding: 2px 8px; border-radius: 6px; font-weight: 600; white-space: nowrap; }
.tag.done { color: var(--green); background: rgba(63,185,80,.12); }
.tag.diff { color: var(--amber); background: rgba(210,153,34,.12); }
.tag.miss { color: var(--red); background: rgba(248,81,73,.12); }
.tag.add { color: var(--purple); background: rgba(188,140,255,.12); }
/* risk blocks */
.risk { border-left: 3px solid var(--border); padding: 4px 0 4px 18px; margin: 18px 0; }
.risk.high { border-color: var(--red); }
.risk.mid { border-color: var(--amber); }
.risk.low { border-color: var(--accent); }
.risk h4 { margin: 0 0 6px; font-size: 15px; }
.risk h4 .sev { font-size: 11px; padding: 2px 8px; border-radius: 6px; margin-left: 8px; vertical-align: middle; font-weight: 700; }
.sev.high { color: var(--red); background: rgba(248,81,73,.14); }
.sev.mid { color: var(--amber); background: rgba(210,153,34,.14); }
.sev.low { color: var(--accent); background: rgba(68,147,248,.14); }
.risk p { margin: 6px 0; font-size: 14px; }
.risk .fix { color: var(--muted); font-size: 13.5px; }
.risk .fix b { color: #c9d4e0; }
ul.clean { padding-left: 20px; }
ul.clean li { margin: 7px 0; }
.file-list { font-family: var(--mono); font-size: 13px; }
.file-list td:first-child { color: var(--purple); }
footer { margin-top: 64px; padding-top: 20px; border-top: 1px solid var(--border); color: var(--muted); font-size: 12.5px; }
.legend { display:flex; flex-wrap:wrap; gap:14px; margin: 6px 0 0; font-size:12px; color:var(--muted);}
.legend span { display:inline-flex; align-items:center; gap:6px;}
.sw { width:11px;height:11px;border-radius:3px;display:inline-block;}
< / style >
< / head >
< body >
< div class = "wrap" >
< header class = "hero" >
< div class = "eyebrow" > 코드베이스 현황 분석 리포트< / div >
< h1 > PDF 도면 → 외벽선 자동작성 기능< / h1 >
< p class = "sub" > 배치면 초기설정 단계에서 PDF 도면을 업로드하면 AI(Gemini)가 외벽선을 추출해 캔버스에 그려주는 기능의 구현 현황< / p >
< div class = "meta-row" >
< span class = "pill" > Q.CAST Front · Next.js 14 · Fabric.js 5.5.2< / span >
< span class = "pill" > branch < code style = "background:none;border:none;padding:0;color:inherit" > feature/pv-btob< / code > < / span >
< span class = "pill ok" > ● 이미 구현됨 · 현재 활성< / span >
< span class = "pill" > 분석일 2026-05-29< / span >
< / div >
< / header >
<!-- ================= TL;DR ================= -->
< div class = "tldr" >
< h3 > 핵심 결론 (TL;DR)< / h3 >
< p > 요청하신 < strong > “PDF 도면 업로드 → AI 분석 → 외벽선 자동 그리기”< / strong > 기능은 < strong > 새로 만들 필요가 없습니다.< / strong > 이미 < strong > 완전히 구현되어 동작 중< / strong > 입니다.< / p >
< p > 커밋 < span class = "hash" > 849f7a1d< / span > (2026-05-20)에서 Google < strong > Gemini< / strong > 기반으로 전체 구현 → < span class = "hash" > f8641079< / span > 에서 일시 비활성화 → 현재 HEAD < span class = "hash" > 0187b234< / span > 에서 < strong > 다시 활성화< / strong > 되어 있습니다.< / p >
< p > 따라서 이번 작업의 실제 성격은 < strong > “신규 개발”이 아니라 “기존 구현의 검증 · 정합성 보완 · 품질 개선”< / strong > 입니다. 아래에서 구현 위치, 데이터 흐름, 그리고 보강이 필요한 갭/리스크를 정리합니다.< / p >
< / div >
<!-- ================= 1. 한눈에 ================= -->
< h2 > < span class = "num" > 01< / span > 한눈에 보기< / h2 >
< div class = "grid" >
< div class = "card" >
< div class = "k" > AI 엔진< / div >
< div class = "v" > Gemini< / div >
< div class = "d" > < code > gemini-3.1-pro-preview< / code > · < code > @google/generative-ai ^0.24.1< / code > < / div >
< / div >
< div class = "card" >
< div class = "k" > 진입점 (UI)< / div >
< div class = "v" > 배치면 초기설정 모달< / div >
< div class = "d" > 치수 입력방법 라디오에 < b > “도면 파일 업로드”< / b > 옵션 추가< / div >
< / div >
< div class = "card" >
< div class = "k" > 서버 처리< / div >
< div class = "v" > Next.js API Route< / div >
< div class = "d" > < code > POST /api/gemini/floor-plan< / code > · 분석 후 파일 즉시 폐기< / div >
< / div >
< div class = "card" >
< div class = "k" > 캔버스 반영< / div >
< div class = "v" > 외벽선 + WALL 폴리곤< / div >
< div class = "d" > < code > QLine< / code > (outerLine) + < code > QPolygon< / code > (WALL) 생성< / div >
< / div >
< div class = "card" >
< div class = "k" > 좌표 단위< / div >
< div class = "v" > mm → canvas< / div >
< div class = "d" > 10mm = 1 canvas unit, 이후 화면 맞춤 스케일< / div >
< / div >
< div class = "card" >
< div class = "k" > 다국어< / div >
< div class = "v" > ko / ja 완비< / div >
< div class = "d" > 각 30개 < code > pdf.import.*< / code > / < code > ...size.pdf.*< / code > 키< / div >
< / div >
< / div >
<!-- ================= 2. 타임라인 ================= -->
< h2 > < span class = "num" > 02< / span > 구현 타임라인 (git 이력)< / h2 >
< ul class = "timeline" >
< li >
< span class = "dot" > < / span >
< div class = "when" > 2026-05-18< / div >
< div class = "what" > 구현 계획 수립< / div >
< div class = "desc" > < span class = "path" > docs/PDF Gemini 평면도 분석 기능 구현 계획.md< / span > 작성 — 요구사항·흐름·검증 정의< / div >
< / li >
< li >
< span class = "dot" > < / span >
< div class = "when" > 2026-05-20 · < span class = "hash" > 849f7a1d< / span > < / div >
< div class = "what" > feat(pdf-gemini): 전체 기능 구현< / div >
< div class = "desc" > 서버 라우트 + < code > usePdfImport< / code > 훅 + 배치면 초기설정 통합 + env/의존성/로케일. 총 12개 파일, +2137 라인.< / div >
< / li >
< li class = "off" >
< span class = "dot" > < / span >
< div class = "when" > 2026-05-27 · < span class = "hash" > f8641079< / span > < / div >
< div class = "what" > Disable PDF input mode option< / div >
< div class = "desc" > 라디오 옵션 일시 비활성화 (운영 노출 차단 목적으로 추정)< / div >
< / li >
< li class = "now" >
< span class = "dot" > < / span >
< div class = "when" > 2026-05-27 · < span class = "hash" > 0187b234< / span > · < b > HEAD< / b > < / div >
< div class = "what" > Revert: PDF 도면 파일 업로드 라디오 옵션 재표시< / div >
< div class = "desc" > < b > 현재 상태 — 기능 활성.< / b > 사용자에게 “도면 파일 업로드” 옵션이 다시 노출됨.< / div >
< / li >
< / ul >
<!-- ================= 3. 데이터 흐름 ================= -->
< h2 > < span class = "num" > 03< / span > 전체 데이터 흐름 (실제 구현)< / h2 >
< div class = "flow" >
< pre > < span class = "c-cli" > [클라이언트 · PlacementShapeSetting.jsx]< / span >
배치면 초기설정 모달 열기
│
▼ "도면 파일 업로드" 라디오 선택 (INPUT_MODE.PDF)
파일 선택 handlePdfFileChange() < span class = "c-cmt" > # MIME=application/pdf, ≤20MB 검증< / span >
│
▼ [저장] 버튼 → handleSaveBtn() → analyzePdfAndApply()
FormData(file, unitHint=mm, pageMode, facadePages?, floorPages?)
│
▼ POST
< span class = "c-srv" > [서버 · /api/gemini/floor-plan/route.js]< / span >
① MIME / 20MB 재검증
② < span class = "c-ext" > GoogleAIFileManager.uploadFile()< / span > ──→ Gemini File API 업로드
③ waitForFileActive() < span class = "c-cmt" > # PROCESSING→ACTIVE, 60s 타임아웃< / span >
④ < span class = "c-ext" > model.generateContent()< / span > < span class = "c-cmt" > # responseSchema 강제, temperature 0.1< / span >
⑤ validateOuterline() < span class = "c-cmt" > # 정점 3~500개, 면적≠0 검증< / span >
⑥ < span class = "c-ext" > finally: deleteFile()< / span > < span class = "c-cmt" > # 성공/실패 무관 Gemini 파일 폐기< / span >
│
▼ { outerline:[{x,y}…], unit:'mm', scale, confidence, notes }
< span class = "c-cli" > [클라이언트 · usePdfImport.js]< / span > applyOuterline()
⑦ toCanvasUnits() mm ÷ 10 → canvas unit
⑧ 중복점 필터 인접거리 < 0.5 제거
⑨ fitAndCenter() 캔버스 60% / 최대 1500 내로 스케일 + 중앙정렬
⑩ ensureCounterClockwise() CCW 정규화
⑪ [기존 외벽선 존재 시] useSwal confirm "덮어쓰시겠습니까?"
⑫ clearExistingCanvasState() → setOuterLinePoints() (Recoil)
⑬ drawOuterLines() QLine(name='outerLine') 생성
⑭ addPolygonByLines() QPolygon(name=WALL) 생성
⑮ canvas.renderAll()
│
▼ roofSizeSet='1'(복시도)로 저장 → setSelectedMenu('outline')
외벽선 메뉴로 자동 전환 (수동 입력 흐름과 동일 지점에 합류)< / pre >
< / div >
<!-- ================= 4. 구성요소 상세 ================= -->
< h2 > < span class = "num" > 04< / span > 구성요소별 상세< / h2 >
< h3 > ① 서버 라우트 — < span class = "path" > src/app/api/gemini/floor-plan/route.js< / span > < / h3 >
< ul class = "clean" >
< li > < b > 역할< / b > : multipart 수신 → Gemini File API 업로드 → 외곽선 JSON 추출 → 파일 폐기. (210 라인, < code > POST< / code > 단일 핸들러)< / li >
< li > < b > 모델/한도< / b > : < code > gemini-3.1-pro-preview< / code > (L9), 20MB (L7), 최대 500 정점 (L8).< / li >
< li > < b > 구조화 출력 강제< / b > : < code > responseMimeType: 'application/json'< / code > + < code > responseSchema< / code > (FLOOR_PLAN_SCHEMA, L11-31) + < code > temperature: 0.1< / code > (L156-160).< / li >
< li > < b > 프롬프트< / b > : “건축 평면도 외곽선 추출 전문가” — mm 환산·CCW·자기교차 없는 단순 폴리곤·JSON only를 명시 (BASE_PROMPT, L33-52). 페이지 지정 시 입면도/평면도 힌트 주입(buildPrompt, L54-62).< / li >
< li > < b > 서버측 검증< / b > : < code > validateOuterline()< / code > (L64-93) — 배열/정점수/숫자형/< b > 면적≠0< / b > (|area|< 1 거부, L89). 빈 배열은 “평면도 아님”으로 정상 처리.< / li >
< li > < b > 리소스 정리< / b > : < code > finally< / code > 에서 < code > deleteFile()< / code > 로 업로드 PDF 폐기 (L202-208) — 계획서 요구사항 “분석 후 폐기” 충족.< / li >
< li > < b > 키 관리< / b > : < code > process.env.GEMINI_API_KEY< / code > 직접 사용 (서버 전용, 클라이언트 미노출).< / li >
< / ul >
< h3 > ② 캔버스 반영 훅 — < span class = "path" > src/hooks/pdf-import/usePdfImport.js< / span > < / h3 >
< ul class = "clean" >
< li > < b > 핵심 상수< / b > : < code > MM_PER_CANVAS_UNIT = 10< / code > (L11), < code > MAX_DIM = 1500< / code > (L12), < code > MIN_CANVAS_UNIT_LENGTH = 0.5< / code > (L13).< / li >
< li > < b > 좌표 파이프라인< / b > : < code > toCanvasUnits< / code > (mm/10) → 중복점 필터 → < code > fitAndCenter< / code > (화면 60% 맞춤·중앙정렬, L32-58) → < code > ensureCounterClockwise< / code > (L27-30).< / li >
< li > < b > 그리기< / b > : < code > drawOuterLines< / code > (L89-102)가 점 배열을 순회하며 < code > QLine< / code > (stroke #000, width 3, < code > name:'outerLine'< / code > , idx) 생성 → canvas.add.< / li >
< li > < b > 폴리곤화< / b > : 외벽선 ≥3개면 < code > addPolygonByLines(lines, { name: POLYGON_TYPE.WALL })< / code > 로 WALL 폴리곤 생성(L148).< / li >
< li > < b > 덮어쓰기 가드< / b > : 기존 outerLine/WALL/ROOF 존재 시 confirm 후 진행(L157-169), 진행 시 < code > clearExistingCanvasState< / code > 로 관련 객체 정리.< / li >
< li > < b > 외부 API< / b > : < code > applyOuterline(mmPoints, { unit })< / code > , < code > hasExistingOuterLine()< / code > 노출.< / li >
< / ul >
< h3 > ③ UI 통합 — < span class = "path" > src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx< / span > < / h3 >
< ul class = "clean" >
< li > < b > 입력 모드< / b > : < code > INPUT_MODE = { SIZE_ROOF:'1', SIZE_ACTUAL:'2', PDF:'pdf' }< / code > (L34-38). 라디오 배열에 < code > ra-pdf< / code > 항목 추가(L107-111).< / li >
< li > < b > 모드 매핑< / b > : PDF 선택 시 내부 < code > roofSizeSet< / code > 은 < b > ‘ 1’ (복시도)< / b > 로 저장(L113-121) → 저장 후 외벽선 메뉴로 자동 합류(L427-429).< / li >
< li > < b > 파일 검증< / b > : < code > handlePdfFileChange< / code > (L128-142) — MIME/20MB 클라이언트 가드.< / li >
< li > < b > 분석 트리거< / b > : < code > analyzePdfAndApply< / code > (L144-198) — fetch → 에러/빈응답 분기 → < code > applyOuterline< / code > 호출.< / li >
< li > < b > 페이지 지정< / b > : 전체/지정 라디오 + 입면도·평면도 페이지 입력(L534-589) — < span class = "tag add" > 계획서에 없던 추가 기능< / span > .< / li >
< li > < b > 로딩 UX< / b > : 분석 중 버튼 비활성화 + “분석 중…” 텍스트(L867-872). 별도 진행률 없음.< / li >
< / ul >
< h3 > ④ 의존성 · 환경변수 · 로케일< / h3 >
< ul class = "clean" >
< li > < b > 의존성< / b > : < code > package.json< / code > L23 — < code > @google/generative-ai: ^0.24.1< / code > .< / li >
< li > < b > 환경변수< / b > : < code > GEMINI_API_KEY< / code > , < code > GEMINI_MODEL< / code > — < code > .env.development / .env.local.dev / .env.localhost / .env.production< / code > 4개 파일 모두 존재. < span style = "color:var(--muted)" > (값은 본 리포트에 미기재)< / span > < / li >
< li > < b > 로케일< / b > : < code > ko.json< / code > (L19-45) / < code > ja.json< / code > 각 30개 메시지 — < code > pdf.import.*< / code > (동작/에러) + < code > modal.placement.initial.setting.size.pdf.*< / code > (UI 라벨/안내).< / li >
< / ul >
<!-- ================= 5. 계획 대비 실제 ================= -->
< h2 > < span class = "num" > 05< / span > 계획서 대비 실제 구현 차이< / h2 >
< p > 구현 과정에서 초기 계획(< span class = "path" > docs/…구현 계획.md< / span > )과 달라진 설계 결정들입니다. 대부분 < b > 더 나은 통합 방향< / b > 으로 수렴했습니다.< / p >
< table >
< thead > < tr > < th style = "width:18%" > 항목< / th > < th style = "width:30%" > 계획서< / th > < th style = "width:34%" > 실제 구현 (HEAD)< / th > < th style = "width:18%" > 평가< / th > < / tr > < / thead >
< tbody >
< tr > < td > UI 진입점< / td > < td > floor-plan 상단 툴바 + 신규 PDF 아이콘< / td > < td > 배치면 초기설정 모달의 치수 입력방법 라디오 옵션< / td > < td > < span class = "tag diff" > 변경< / span > 기존 흐름과 자연스럽게 통합< / td > < / tr >
< tr > < td > 업로드 모달< / td > < td > 별도 < code > PdfImportModal.jsx< / code > (상태머신)< / td > < td > 없음 — PlacementShapeSetting에 통합< / td > < td > < span class = "tag diff" > 변경< / span > 모달 중첩 회피< / td > < / tr >
< tr > < td > 메뉴 등록< / td > < td > < code > menuAtom< / code > 에 < code > pdf-import< / code > 항목< / td > < td > 미등록 (툴바 방식 폐기로 불필요)< / td > < td > < span class = "tag done" > 정합< / span > < / td > < / tr >
< tr > < td > 페이지 지정< / td > < td > (없음)< / td > < td > 전체/지정 + 입면도·평면도 페이지 입력< / td > < td > < span class = "tag add" > 추가< / span > 분석 속도·정확도 개선< / td > < / tr >
< tr > < td > config 매핑< / td > < td > < code > config.*.js< / code > 에 GEMINI 추가< / td > < td > 미적용 — route가 < code > process.env< / code > 직접 사용< / td > < td > < span class = "tag diff" > 상이< / span > 아래 리스크 R3 참조< / td > < / tr >
< tr > < td > 자기교차 검증< / td > < td > turf 로 self-intersection 검출< / td > < td > 미구현 — 정점수·면적만 검증, 나머지는 프롬프트+CCW 의존< / td > < td > < span class = "tag miss" > 미구현< / span > 리스크 R2< / td > < / tr >
< tr > < td > 거대 폴리곤< / td > < td > Douglas– Peucker 단순화 or 거부< / td > < td > 500 정점 초과 시 거부만 (단순화 없음)< / td > < td > < span class = "tag miss" > 미구현< / span > < / td > < / tr >
< tr > < td > 파일 폐기< / td > < td > < code > finally< / code > 에서 삭제 보장< / td > < td > 동일 구현< / td > < td > < span class = "tag done" > 정합< / span > < / td > < / tr >
< / tbody >
< / table >
<!-- ================= 6. 갭 / 리스크 ================= -->
< h2 > < span class = "num" > 06< / span > 갭 · 리스크 · 보완 권장< / h2 >
< p > 기능은 동작하지만, “외벽선을 도면 그대로 정확히” 재현하려는 목표 관점에서 다음 항목들의 검증/보완을 권장합니다.< / p >
< div class = "risk high" >
< h4 > R1 · 실측 치수가 캔버스에 보존되지 않을 수 있음< span class = "sev high" > HIGH< / span > < / h4 >
< p > < code > fitAndCenter()< / code > 가 외벽선을 < b > 캔버스 60%(≈840 canvas unit, 약 8.4m)에 맞춰 추가 스케일링< / b > 합니다. 도면이 그보다 크면 < code > scale < 1< / code > 이 곱해져 < b > 실제 치수 ↔ 캔버스 길이 정합이 깨집니다.< / b > 수동 입력 흐름은 < code > 실측/10< / code > 을 그대로 좌표로 쓰므로, PDF 경로만 길이가 달라질 위험이 있습니다.< / p >
< p class = "fix" > < b > 권장:< / b > 일반 주택(외곽 8.4m 초과 흔함) 기준으로 < b > 실측 비율을 보존하는 변환< / b > 으로 재검토. 화면 맞춤이 필요하면 좌표 자체가 아니라 canvas < b > viewport zoom/pan< / b > 으로 처리하거나, 그리드 축척과 동일한 mm/unit 비율을 강제할 것.< / p >
< / div >
< div class = "risk mid" >
< h4 > R2 · 자기교차(self-intersection) 폴리곤 통과 가능< span class = "sev mid" > MID< / span > < / h4 >
< p > 서버 < code > validateOuterline()< / code > 은 정점수·면적만 본다. 자기교차는 < b > Gemini 프롬프트 지침과 CCW 정규화에만 의존< / b > 한다. 꼬인 폴리곤이 들어오면 < code > QPolygon< / code > 의 < code > sortedPointLessEightPoint< / code > 정렬과 충돌해 형상이 깨질 수 있다.< / p >
< p class = "fix" > < b > 권장:< / b > 계획서대로 turf(또는 경량 segment-intersection)로 서버/클라이언트에서 자기교차 거부. < code > SizeSetting< / code > 의 0-area 가드 정신과 일관되게 처리.< / p >
< / div >
< div class = "risk mid" >
< h4 > R3 · 지붕면(ROOF) 자동 생성 부재 — 안내 문구와 동작 갭< span class = "sev mid" > MID< / span > < / h4 >
< p > 로케일 < code > …pdf.file.info< / code > 는 “외벽선과 < b > 지붕형상< / b > 이 함께 생성”된다고 안내하지만, < code > usePdfImport< / code > 는 < b > 외벽선(QLine) + WALL 폴리곤< / b > 까지만 만든다. 수동 흐름의 < code > ccwCheck → RoofShapeSetting 자동 오픈 → drawRoofPolygon< / code > 단계가 PDF 경로에선 호출되지 않아 < b > 지붕면은 수동 작성< / b > 이 필요하다.< / p >
< p class = "fix" > < b > 권장:< / b > ①요청 목표(외벽선 그리기)만 보면 충족이므로 문구를 동작에 맞게 정정하거나, ②PDF 적용 후 기존 < code > useRoofShapeSetting< / code > 자산을 재사용해 지붕 자동화를 확장. (요청 범위 명확화 필요)< / p >
< / div >
< div class = "risk low" >
< h4 > R4 · GEMINI 키의 config 우회 — 컨벤션 관점< span class = "sev low" > LOW< / span > < / h4 >
< p > CLAUDE.md는 “feature code에서 env 직접 읽지 말고 < code > src/config< / code > 에서 import”를 규정한다. route는 < code > process.env< / code > 를 직접 읽는다. < b > 다만 서버 시크릿은 클라이언트 config로 노출하면 안 되므로< / b > 서버 라우트에서의 직접 사용은 정당화 가능한 회색지대다.< / p >
< p class = "fix" > < b > 권장:< / b > 현행 유지 가능. 일관성을 원하면 서버 전용 config 헬퍼로 묶되 클라이언트 번들 유입이 없도록 주의.< / p >
< / div >
< div class = "risk low" >
< h4 > R5 · 비결정적 분석 · 검증 자동화 부재< span class = "sev low" > LOW< / span > < / h4 >
< p > 프로젝트에 테스트 러너가 없고(CLAUDE.md), Gemini 출력은 비결정적이다. 회귀 검증은 수동 의존. 분석 소요(최대 60s 타임아웃)에 비해 진행률 UX가 약하다.< / p >
< p class = "fix" > < b > 권장:< / b > 좌표 변환부(< code > toCanvasUnits< / code > /< code > fitAndCenter< / code > /< code > ensureCounterClockwise< / code > )는 순수 함수이므로 < b > 단위 테스트로 분리 검증< / b > 가능. 분석 중 스피너/단계 표시 추가 검토.< / p >
< / div >
<!-- ================= 7. 파일 인벤토리 ================= -->
< h2 > < span class = "num" > 07< / span > 파일 인벤토리< / h2 >
< table class = "file-list" >
< thead > < tr > < th style = "width:46%" > 경로< / th > < th style = "width:14%" > 구분< / th > < th > 역할 / 핵심 라인< / th > < / tr > < / thead >
< tbody >
< tr > < td > src/app/api/gemini/floor-plan/route.js< / td > < td > < span class = "tag done" > 신규< / span > < / td > < td > Gemini 분석 서버 라우트 · POST(L111) · validate(L64) · finally 폐기(L202)< / td > < / tr >
< tr > < td > src/hooks/pdf-import/usePdfImport.js< / td > < td > < span class = "tag done" > 신규< / span > < / td > < td > 좌표 변환·캔버스 반영 · applyOuterline(L104) · drawOuterLines(L89)< / td > < / tr >
< tr > < td > src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx< / td > < td > < span class = "tag diff" > 수정< / span > < / td > < td > UI 통합 · INPUT_MODE(L34) · analyzePdfAndApply(L144) · UI(L486-591)< / td > < / tr >
< tr > < td > package.json< / td > < td > < span class = "tag diff" > 수정< / span > < / td > < td > @google/generative-ai ^0.24.1 (L23)< / td > < / tr >
< tr > < td > .env.{development,local.dev,localhost,production}< / td > < td > < span class = "tag diff" > 수정< / span > < / td > < td > GEMINI_API_KEY · GEMINI_MODEL< / td > < / tr >
< tr > < td > src/locales/ko.json · ja.json< / td > < td > < span class = "tag diff" > 수정< / span > < / td > < td > pdf.import.* / …size.pdf.* (각 30개)< / td > < / tr >
< tr > < td > docs/PDF Gemini 평면도 분석 기능 구현 계획.md / .html< / td > < td > < span class = "tag add" > 문서< / span > < / td > < td > 초기 설계 문서 (2026-05-18)< / td > < / tr >
< / tbody >
< / table >
< div class = "legend" >
< span > < span class = "sw" style = "background:var(--green)" > < / span > 신규 파일< / span >
< span > < span class = "sw" style = "background:var(--amber)" > < / span > 수정 파일< / span >
< span > < span class = "sw" style = "background:var(--purple)" > < / span > 문서/참고< / span >
< / div >
<!-- ================= 8. 다음 단계 제안 ================= -->
< h2 > < span class = "num" > 08< / span > 다음 단계 제안< / h2 >
< p > “새 기능 개발”이 아니라 < b > 기존 구현의 검증·보완< / b > 으로 방향을 잡는 것을 권합니다. 우선순위 순:< / p >
< ul class = "clean" >
< li > < b > 1. 실측 정합성 검증 (R1)< / b > — 실제 PDF 한두 건으로 “도면 치수 vs 캔버스 외벽선 길이”를 측정. 어긋나면 < code > fitAndCenter< / code > 스케일 전략부터 재설계.< / li >
< li > < b > 2. 요구 범위 확정 (R3)< / b > — “외벽선만”인지 “지붕면까지”인지 결정. 결정에 따라 안내 문구 정정 또는 지붕 자동화 확장.< / li >
< li > < b > 3. 자기교차/단순화 가드 (R2)< / b > — 꼬인 폴리곤·과다 정점에 대한 방어 추가.< / li >
< li > < b > 4. 좌표 변환 단위 테스트< / b > — 순수 함수부를 분리해 회귀 안전망 마련.< / li >
< li > < b > 5. lint/build 점검< / b > — 보완 작업 시 CLAUDE.md 규약(no-semicolon, single-quote, < code > yarn lint< / code > )에 맞춰 마무리.< / li >
< / ul >
< p style = "color:var(--muted);font-size:13.5px;" > 실제 코드 보완에 착수하기 전, 위 R1·R3는 제품 의사결정이 필요하므로 먼저 합의하는 것을 권합니다. 합의되면 brainstorming → 구현 → 검증 순으로 진행하겠습니다.< / p >
< footer >
생성: Claude Code · 코드베이스 정적 분석 기반 (Gemini 실호출/런타임 검증은 미포함) · 라인 번호는 분석 시점(HEAD < span class = "hash" > 0187b234< / span > ) 기준< br / >
저장 위치: < span class = "path" > docs/PDF 도면 외벽선 자동작성 - 코드베이스 현황 분석.html< / span >
< / footer >
< / div >
< / body >
< / html >