Add new HTML report for PDF 도면 → 외벽선 자동작성 기능

This commit introduces a new HTML document detailing the functionality of the PDF 도면 to 외벽선 자동작성 feature, including styles and layout for improved presentation.
This commit is contained in:
sangwook yoo 2026-05-29 18:24:04 +09:00
parent 0187b234fb
commit 3e37b7448c

View File

@ -0,0 +1,408 @@
<!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
⑧ 중복점 필터 인접거리 &lt; 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|&lt;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>DouglasPeucker 단순화 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 &lt; 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>