diff --git a/src/app/api/gemini/floor-plan/route.js b/src/app/api/gemini/floor-plan/route.js index 877629c3..6e132d9e 100644 --- a/src/app/api/gemini/floor-plan/route.js +++ b/src/app/api/gemini/floor-plan/route.js @@ -7,6 +7,7 @@ import { PDFDocument } from 'pdf-lib' import { sessionOptions } from '@/lib/session' import { logger } from '@/util/logger' +import { normalizeRoofShape } from '@/common/roofShapePattern' const MAX_FILE_BYTES = 20 * 1024 * 1024 // 20MB const MAX_VERTICES = 500 @@ -51,6 +52,16 @@ const FLOOR_PLAN_SCHEMA = { confidence: { type: SchemaType.NUMBER }, notes: { type: SchemaType.STRING }, selectedDrawingType: { type: SchemaType.STRING }, + roofShape: { + type: SchemaType.OBJECT, + properties: { + type: { type: SchemaType.STRING }, + ridge: { type: SchemaType.STRING }, + flows: { type: SchemaType.ARRAY, items: { type: SchemaType.STRING } }, + confidence: { type: SchemaType.NUMBER }, + }, + required: ['type', 'ridge', 'flows'], + }, }, required: ['outerline', 'unit'], } @@ -81,6 +92,19 @@ const BASE_PROMPT = `당신은 일본 주택 건축 도면에서 "지붕 투영 6. 분리된 복수 동(棟)이 있으면 가장 큰 주 건물 하나만 반환하고, 별동은 notes 에 기재합니다. 7. 지붕 투영을 추출할 수 없으면 outerline 을 빈 배열로 반환하고 notes 에 사유를 적습니다. +[지붕형상 판독 — 屋根伏図 棟·軒先 + 立面図 보조] +- 屋根伏図의 棟(용마루) 위치·개수와 軒先(처마)가 흘러내리는 방향으로 지붕 유형을 판별합니다. +- type: 寄棟→'hip', 切妻→'gable', 片流れ(단일경사)→'shed', 판별 불가/복합형→'manual'. +- ridge(용마루 방향): 寄棟→'hip', 切妻 가로 용마루→'horizontal', 切妻 세로 용마루→'vertical', 片流れ→'shed', 없음/복합→'none'. +- flows: 처마가 흘러내리는 방향을 화면축 집합으로 기재합니다. 좌표계는 외곽선과 동일(좌상단 원점, 위=top·아래=bottom·왼쪽=left·오른쪽=right). + · 寄棟: ['top','bottom','left','right'] (사방 흐름) + · 切妻 가로 용마루(용마루가 좌우로 길게): ['top','bottom'] + · 切妻 세로 용마루(용마루가 상하로 길게): ['left','right'] + · 片流れ: 처마가 향하는 단일 방향 1개만, 예: ['bottom'] + · 복합형/판별 불가: [] (이 경우 type='manual', ridge='none') +- 立面図(입면도)가 있으면 棟방향·경사 방향 판별의 보조로만 활용하세요(외곽선/정점 추출에는 절대 사용 금지). +- 확신이 없으면 roofShape.confidence 를 낮게 주거나 roofShape 자체를 생략하세요(틀린 추정보다 누락이 낫습니다). + [출력] - 응답은 반드시 단일 JSON 객체만 포함하며, 다른 텍스트, 마크다운, 코드펜스를 포함하지 마세요. - 좌표는 정수 또는 소수 첫째 자리까지 허용합니다. @@ -93,7 +117,8 @@ const BASE_PROMPT = `당신은 일본 주택 건축 도면에서 "지붕 투영 "scale": number (선택, 축척 분모. 1/100 이면 100), "confidence": number (0~1), "notes": string (선택), - "selectedDrawingType": string (선택) + "selectedDrawingType": string (선택), + "roofShape": { "type": "hip|gable|shed|manual", "ridge": "hip|horizontal|vertical|shed|none", "flows": ["top|bottom|left|right", ...], "confidence": number } (선택) }` const buildPrompt = ({ pageMode, facadePages, floorPages, splitInfo } = {}) => { @@ -103,13 +128,13 @@ const buildPrompt = ({ pageMode, facadePages, floorPages, splitInfo } = {}) => { if (splitInfo.floorCount > 0) segs.push(`평면도/지붕평면도 ${splitInfo.floorCount}장`) if (splitInfo.facadeCount > 0) segs.push(`입면도 ${splitInfo.facadeCount}장`) const compo = `첨부 PDF 는 ${segs.join('과 ')}으로 구성됩니다(평면도가 앞).` - return `${BASE_PROMPT}\n\n${compo} 외곽선은 평면도/지붕평면도 페이지에서만 추출하고, 입면도 페이지는 외곽선 추출에 쓰지 말고 높이·층수 참고로만 사용하세요.` + return `${BASE_PROMPT}\n\n${compo} 외곽선은 평면도/지붕평면도 페이지에서만 추출하세요. 입면도 페이지는 외곽선 추출에 쓰지 말고, 높이·층수와 지붕 棟방향·경사 방향(지붕형상 판별) 참고로만 사용하세요.` } if (pageMode === 'specify' && (facadePages || floorPages)) { const hints = [] 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 } @@ -202,10 +227,7 @@ export async function POST(req) { return NextResponse.json({ error: { code: 'UNAUTHORIZED', message: '로그인이 필요합니다.' } }, { status: 401 }) } if (isRateLimited(session.userId || 'unknown')) { - return NextResponse.json( - { error: { code: 'RATE_LIMITED', message: '분석 요청이 너무 잦습니다. 잠시 후 다시 시도해주세요.' } }, - { status: 429 }, - ) + return NextResponse.json({ error: { code: 'RATE_LIMITED', message: '분석 요청이 너무 잦습니다. 잠시 후 다시 시도해주세요.' } }, { status: 429 }) } if (!apiKey) { @@ -328,6 +350,7 @@ export async function POST(req) { confidence: parsed.confidence ?? null, notes: parsed.notes ?? null, selectedDrawingType: parsed.selectedDrawingType ?? null, + roofShape: normalizeRoofShape(parsed.roofShape), empty: validation.empty, }) } catch (error) {