This commit introduces a new HTML document detailing the functionality of the PDF 도면 to 외벽선 자동작성 feature, including styles and layout for improved presentation.
409 lines
28 KiB
HTML
409 lines
28 KiB
HTML
<!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>
|