qcast-front/docs/superpowers/specs/2026-06-21-pdf-page-split-design.md
sangwook yoo ce295dd8ff feat(pdf-import): 지정 페이지만 분할·인라인 전송으로 분석 속도 개선
- '읽기 페이지 설정'의 평면도/입면도 페이지 번호가 프롬프트 힌트로만 쓰이고
  PDF 전체가 Gemini 로 전송되던 문제 해결 — pdf-lib 로 지정 페이지만 잘라 전송
- 분할본이 12MB 이하면 inlineData(base64) 즉시 전송 → Files API 업로드·폴링 제거,
  초과 시에만 Files API 로 fallback
- 분할 시 buildPrompt 를 페이지번호 힌트 대신 평면도/입면도 순서 매핑으로 전환
- 유효 페이지 0개 → 422 INVALID_PAGE_RANGE, 분할 실패 → 422 PDF_SPLIT_FAILED
- 설계 문서 추가(docs/superpowers/specs/2026-06-21-pdf-page-split-design.md)

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-21 20:49:11 +09:00

6.1 KiB

PDF 도면 임포트 — 지정 페이지 분할 + 인라인 전송 설계

  • 날짜: 2026-06-21
  • 브랜치: feature/pv-btob
  • 상태: 승인됨 (구현 대기)
  • 관련 파일: src/app/api/gemini/floor-plan/route.js, src/components/floor-plan/modal/placementShape/PlacementShapeSetting.jsx

배경 / 문제

배치면 초기설정의 "읽기 페이지 설정"에서 평면도/입면도 페이지를 지정할 수 있으나, 현재 페이지 번호는 프롬프트 텍스트 힌트로만 쓰이고 PDF 는 전체가 Gemini 에 전송된다(buildPrompt 에만 사용, file 버퍼 전체를 uploadFile). 따라서:

  • Gemini 가 전 페이지를 비전 토큰화 → 입력 토큰·추론 시간·비용이 페이지 수에 비례(mediaResolution: HIGH 로 페이지당 토큰이 큼).
  • 페이지 지정은 정확도엔 도움이 되나 속도엔 거의 효과가 없다.

목표

지정한 페이지만 실제로 잘라서 전송하여 분석 속도·비용을 절감한다. 추가로 Files API 업로드 + 처리 폴링(최대 60초 대기) 단계를 인라인 전송으로 제거한다.

범위

In scope

  • specify 모드에서 지정한 평면도 + 입면도 페이지만 pdf-lib 로 추출한 새 PDF 생성.
  • 분할본을 inlineData(base64) 로 Gemini 에 전송 — Files API 업로드·폴링·deleteFile 제거.
  • 페이지 번호 파싱("1,3-5", 1-based) 및 PDF 범위 검증.
  • 분할 시 프롬프트를 "페이지 번호 힌트" → "평면도/입면도 순서 매핑"으로 전환.

Out of scope (후속 작업)

  • 입면도를 지붕 형상 판별에 실제 활용 — 응답 스키마에 높이/지붕형태 필드 추가, 프롬프트 재설계, 결과를 캔버스 지붕형상에 연결하는 별도 작업. 이번엔 입면도 페이지를 전송하는 데까지만(토대 마련).
  • 서버 에러 메시지 i18n (기존 잔여 과제, 기존 한국어 하드코딩 패턴 유지).
  • 클라이언트 측 PDF 가공/미리보기.

설계

데이터 흐름

클라이언트(PlacementShapeSetting.jsx) — 변경 없음: 기존대로 pageMode/floorPages/facadePages 텍스트 + 원본 PDF 전체를 multipart 로 전송.

서버(route.js):

formData 수신 (file, pageMode, floorPages, facadePages)
  │
  ├─ pageMode === 'specify' AND (floorPages 또는 facadePages 비어있지 않음)
  │     ├─ pdf-lib 로 원본 로드, getPageCount()
  │     ├─ floorPages/facadePages 파싱 → 1-based → 0-based 인덱스
  │     ├─ PDF 범위로 검증, 유효 인덱스만 (평면도 먼저, 입면도 뒤, 그룹 내 오름차순·중복제거)
  │     ├─ 유효 페이지 0개 → 422 INVALID_PAGE_RANGE
  │     ├─ 분할 중 예외(손상 PDF 등) → 422 PDF_SPLIT_FAILED
  │     └─ 유효 페이지 ≥1 → copyPages 로 새 PDF 생성 → splitBuffer
  │
  └─ else (all 모드 또는 입력 둘 다 빈값) → 원본 buffer 사용

전송 PDF(splitBuffer 또는 원본) 크기 판정:
  ├─ raw 크기 ≤ INLINE_MAX_BYTES → inlineData(base64) 경로
  └─ 초과 → Files API 경로 (기존: uploadFile → waitForFileActive → fileData)

generateContent([ <pdf part>, { text: prompt } ])

페이지 번호 파싱

  • 형식: 쉼표 구분 + 하이픈 범위. 예) "1,3-5"[1,3,4,5]. 1-based.
  • 공백/빈 토큰/숫자 아닌 토큰은 무시.
  • floorPages, facadePages 각각 파싱 → PDF 총 페이지 수로 필터 → 0-based 변환.
  • 분할본 페이지 순서: [...floorIdx, ...facadeIdx](평면도 먼저). 각 그룹 내 오름차순·중복 제거, 그룹 간 동일 페이지가 겹치면 평면도 우선.

인라인 / Files API 분기

  • INLINE_MAX_BYTES 상수: 요청 총 20MB 한도 + base64 약 +33% 를 감안해 보수적으로 raw 기준 약 12MB.
  • 분할본은 거의 항상 한도 이하 → 대부분 인라인. 초과(고해상도 스캔 등) 시 Files API 로 자동 fallback(기존 경로 재사용).
  • 인라인 경로: waitForFileActive/deleteFile 불필요(임시 파일 없음).

프롬프트 보정

  • buildPrompt 에 분할 컨텍스트 전달: { splitMode: true, floorCount, facadeCount }.
  • 분할 시: 페이지 번호 힌트 대신 순서 매핑 안내. 예)

    "첨부 PDF 는 평면도/지붕평면도 {floorCount}장과 입면도 {facadeCount}장으로 구성됩니다(평면도가 앞). 외곽선은 평면도/지붕평면도에서만 추출하고 입면도는 사용하지 마세요."

    • facadeCount === 0 이면 입면도 문구 생략.
  • 비분할(all/fallback) 경로는 기존 buildPrompt 동작 유지.

에러 처리 (정책 A — 명시적 에러)

  • 유효 페이지 0개(범위 밖 번호만 입력) → 422 { code: 'INVALID_PAGE_RANGE' }. 클라이언트는 기존 payload.error.message 분기로 표시.
  • 분할 단계 예외 → 422 { code: 'PDF_SPLIT_FAILED' }. 조용한 전체 fallback 하지 않음(A 정책 일관).
  • 입력 둘 다 빈값 + specifyall 처럼 전체 전송(자연스러운 기본값, 에러 아님).

의존성

  • pdf-lib 추가(yarn add pdf-lib). 서버 라우트에서만 import. 순수 JS → next.config.mjs externals 수정 불필요.

변경 파일

  • src/app/api/gemini/floor-plan/route.js — 분할·인라인·프롬프트 분기(핵심).
  • package.jsonpdf-lib 의존성.
  • 클라이언트 변경 없음.

검증 (테스트 러너 없음 → 수동)

  • NEXT_PUBLIC_ENABLE_LOGGING=truetiming(fileReady/generate) 로그 확인 — 인라인 경로에서 fileReady 단계 소멸 확인.
  • 시나리오: (a) 평면도 1p 지정 (b) 평면도+입면도 지정 (c) 범위 밖 페이지 → 422 (d) all 모드 회귀 (e) 대용량 → Files API fallback.
  • yarn lint, yarn build 통과(eslint 미설치 시 build 가 lint 를 스킵하는 점 주의).

위험 / 완화

  • 인라인 한도 초과 → Files API fallback 으로 흡수.
  • 분할본 순서와 프롬프트 매핑 불일치 → 순서를 [평면도, 입면도] 로 고정하고 프롬프트도 동일 순서로 명시.
  • 입면도가 외곽선 추출을 오염 → 프롬프트에서 "입면도 미사용" 명시(기존 BASE_PROMPT 에도 존재).