diff --git a/src/app/api/gemini/floor-plan/route.js b/src/app/api/gemini/floor-plan/route.js index 0d34a90e..8299b07f 100644 --- a/src/app/api/gemini/floor-plan/route.js +++ b/src/app/api/gemini/floor-plan/route.js @@ -26,37 +26,58 @@ const FLOOR_PLAN_SCHEMA = { scale: { type: SchemaType.NUMBER }, confidence: { type: SchemaType.NUMBER }, notes: { type: SchemaType.STRING }, + selectedDrawingType: { type: SchemaType.STRING }, }, required: ['outerline', 'unit'], } -const BASE_PROMPT = `당신은 건축 평면도(주택 도면)의 외곽선 추출 전문가입니다. -첨부된 PDF에서 건물의 외곽(외벽선) 폴리곤 좌표를 추출하여 JSON 으로만 응답하세요. +const BASE_PROMPT = `당신은 일본 주택 건축 도면에서 "지붕 투영면(屋根伏図 footprint)" 외곽선을 추출하는 전문가입니다. +첨부된 PDF 에서 지붕을 위에서 내려다본 외곽(처마 끝선 포함) 폴리곤 좌표를 추출하여 JSON 으로만 응답하세요. -규칙: -1. 모든 좌표는 밀리미터(mm) 단위의 실측 치수로 환산해야 합니다. 도면의 축척(scale)을 식별하여 적용하세요. +[도면 종류 식별 — 매우 중요] +- 추출 대상: 屋根伏図(지붕평면도)가 있으면 그 지붕 외곽선(처마/軒先·케라바/ケラバ 끝선 포함)을 사용합니다. +- 屋根伏図 가 없으면 平面図(1층/1階 평면도)의 외벽 외주에 처마 출폭(軒の出)을 더한 지붕 투영 외곽을 추정합니다. +- 절대 사용 금지: 立面図(입면도)·配置図(배치도)·求積図(면적도)·断面図(단면도). 이 도면들에서는 윤곽을 추출하지 마세요. +- 도머(ドーマー)·천창·개구부(開口部) 등 지붕 위 돌출/구멍은 외곽선에 포함하지 마세요(외곽 윤곽이 아닙니다). +- 평면도가 여러 층이면 1층(1F) 기준으로 지붕 투영을 판단합니다. + +[축척·치수 결정 — 우선순위] +1. 도면에 적힌 寸法線(치수선)의 실제 mm 숫자를 최우선 근거로 좌표를 환산합니다. +2. 그 다음 縮尺 표기(예: 縮尺 1/100, S=1:100)를 사용합니다. +3. 마지막으로 通り芯(grid axis) 간격으로 보정합니다. +4. 치수선 숫자와 축척이 충돌하면 치수선 숫자를 신뢰하세요. +5. 일본 주택은 尺貫法 모듈(1間=1820mm, 半間=910mm, 455mm, 303mm)에 정렬되는 경향이 있습니다 — 픽셀비례로 추정한 길이를 가장 가까운 모듈값과 대조해 검토하세요(강제 아님, 참고용). + +[형상 규칙] +1. 모든 좌표는 밀리미터(mm) 단위의 실측 치수로 환산합니다. 2. 좌표계는 도면의 좌상단을 원점으로 하고, x 는 우측 양수, y 는 하단 양수로 합니다. 3. 외곽선은 시계 반대 방향(CCW) 으로 정렬된 정점 배열로 반환합니다. 4. 자기교차가 없는 단순 폴리곤이어야 하며, 정점은 3개 이상이어야 합니다. -5. PDF 가 평면도가 아니거나 외곽을 추출할 수 없으면 outerline 을 빈 배열로 반환하고 notes 에 사유를 적습니다. -6. 응답은 반드시 단일 JSON 객체만 포함하며, 다른 텍스트, 마크다운, 코드펜스를 포함하지 마세요. -7. 좌표는 정수 또는 소수 첫째 자리까지 허용합니다. +5. L자·凹凸(요철) 평면의 모든 꺾임을 정점으로 보존하고, 직각(直角)에 가까운 코너는 직각으로 간주합니다(비스듬히 근사 금지). +6. 분리된 복수 동(棟)이 있으면 가장 큰 주 건물 하나만 반환하고, 별동은 notes 에 기재합니다. +7. 지붕 투영을 추출할 수 없으면 outerline 을 빈 배열로 반환하고 notes 에 사유를 적습니다. + +[출력] +- 응답은 반드시 단일 JSON 객체만 포함하며, 다른 텍스트, 마크다운, 코드펜스를 포함하지 마세요. +- 좌표는 정수 또는 소수 첫째 자리까지 허용합니다. +- selectedDrawingType 에 사용한 도면 종류('roof_plan' | 'floor_plan_1f' | 'other')를 기재합니다. 스키마: { "outerline": [{"x": number, "y": number}, ...], "unit": "mm", - "scale": number (선택, 도면 1mm 당 실제 mm), + "scale": number (선택, 축척 분모. 1/100 이면 100), "confidence": number (0~1), - "notes": string (선택) + "notes": string (선택), + "selectedDrawingType": string (선택) }` const buildPrompt = ({ pageMode, facadePages, floorPages } = {}) => { if (pageMode === 'specify' && (facadePages || floorPages)) { const hints = [] - if (floorPages) hints.push(`평면도(平面図) 페이지: ${floorPages}`) + if (floorPages) hints.push(`평면도(平面図)·지붕평면도(屋根伏図) 페이지: ${floorPages}`) if (facadePages) hints.push(`입면도(立面図) 페이지: ${facadePages}`) - return `${BASE_PROMPT}\n\n페이지 힌트: ${hints.join(' / ')}. 외곽선은 위에서 지정한 평면도 페이지 기준으로 추출하세요.` + return `${BASE_PROMPT}\n\n페이지 힌트: ${hints.join(' / ')}. 입면도 페이지는 외곽선 추출에 쓰지 말고 높이·층수 참고로만 사용하세요. 지붕 투영 외곽선은 지정된 평면도/지붕평면도 페이지 기준으로 추출하세요.` } return BASE_PROMPT } @@ -157,6 +178,8 @@ export async function POST(req) { responseMimeType: 'application/json', responseSchema: FLOOR_PLAN_SCHEMA, temperature: 0.1, + // Gemini 3 고fidelity 경로 — 얇은 외벽선/작은 寸法 텍스트 가독성 확보(미지원 시 무시됨). 적용 여부는 usageMetadata 토큰 증가로 검증. + mediaResolution: 'MEDIA_RESOLUTION_HIGH', }, }) @@ -174,6 +197,7 @@ export async function POST(req) { const raw = result.response.text() logger.debug('[gemini/floor-plan] raw response', raw) + logger.debug('[gemini/floor-plan] usageMetadata', result.response.usageMetadata) let parsed try { @@ -193,6 +217,7 @@ export async function POST(req) { scale: parsed.scale ?? null, confidence: parsed.confidence ?? null, notes: parsed.notes ?? null, + selectedDrawingType: parsed.selectedDrawingType ?? null, empty: validation.empty, }) } catch (error) {