보고서 도구 확장 — 리포트 프롬프트 & JSON→Notion 테스트

마지막 업데이트 2026-07-22

1d3172a0 모카 · 2026-07-22 Feature 14 files +3,253 −238

PR #911/main/report 페이지를 회기별 편집 테이블 하나에서 탭 3개(회기 보고서 세팅 · 프롬프트 테스트 · JSON 노션 테스트)로 재구성한다. 실제 AI 보고서 생성은 외부 리포터 서버(Gemini)가 맡고, web은 인증·키 은닉 프록시 / JSON→Notion 문서 변환·발행 / 학습플랜 프롬프트 저장 / 테스트 UI·폴링을 담당하는 오케스트레이션 계층이다.

리뷰 시 먼저 볼 지점 3가지: ① 두 curriculum 라우트에 parseReport* 검증기가 통째로 복붙됨, ② 테스트 탭이 dev에서 프록시/인증을 우회해 127.0.0.1:8787을 직접 호출(prod만 /api 경유), ③ updateCurriculum이 리포트 필드를 항상 덮어쓰는데 데이터 소실은 API 머지 로직으로만 방어된다.

무엇이, 왜 바뀌었나

PR #889에서 만든 회기 보고서 전사 파이프라인의 후속 작업으로, 그때 생긴 "보고서 탭"을 프롬프트 실험·검증 도구로 확장한다. 순변경은 +3,253/−238이고 대부분 신규 파일 추가이며, 기존 회기 편집 로직은 페이지에서 세팅 탭으로 위치만 이동했다.

흐름 ① 프롬프트 테스트 — 브라우저에서 외부 Gemini 서버까지

1

폼 입력 + 로그 CSV 변환

report-test.tsx

아동명·MP3(≤8)·수업로그·프롬프트 입력. 붙여넣은 탭 표를 Speaker,Text,Source CSV로 변환.

2

인증 프록시 (prod)

POST /api/reports/test

admin/dev 인증 후 X-Service-API-Key를 붙여 외부 서버로 formData 전달.

3

외부 생성 + 상태 폴링

GET /api/reports/test/[jobId]

Gemini 서버가 생성, 3초 간격 폴링으로 queued→completed 추적.

흐름 ② JSON→Notion — 변환·발행은 web이 직접 수행

1

결과 JSON 붙여넣기

report-json-test.tsx

2

JSON 파싱 + 블록 변환

report-document.ts

구조화 JSON → Notion 블록(표/콜아웃/progress/metrics), 2,000자 분할.

3

Notion 페이지 생성

report-json-notion.ts

NOTION_API_KEY로 직접 호출, 100블록 단위 페이지네이션.

코드로 보는 핵심 지점

1. 페이지가 3-탭 셸로 축소되고 로직은 섹션으로 분리

apps/web/components/pages/report-page.tsx - // 학습플랜 선택 → 회기별 SessionReport 편집 테이블 (단일 화면, ~200줄) +type ReportTab = "session" | "test" | "json-test"; + {activeTab === "session" && <ReportSessionSettings />} + {activeTab === "test" && <ReportTest />} + {activeTab === "json-test" && <ReportJsonTest />}

2. curriculum에 optional 속성 2개 + update는 항상 SET(|| null)

apps/web/types/db/curriculum.types.ts +export type ReportSessionPrompts = Record<string, string>; // 회차(1부터) → 프롬프트 + reportCommonPrompt?: string; + reportSessionPrompts?: ReportSessionPrompts; apps/web/lib/db-queries.ts — updateCurriculum + ":reportCommonPrompt": curriculum.reportCommonPrompt || null, + ":reportSessionPrompts": curriculum.reportSessionPrompts || null,

UpdateExpression이 두 필드를 부분 업데이트가 아니라 매번 SET한다. 필드를 뺀 채 호출하면 null로 지워진다.

3. 데이터 소실은 API PUT 핸들러의 "기존 값 머지"로만 방어

apps/web/app/api/curriculums/[id]/route.ts — putHandler + reportCommonPrompt: + reportCommonPrompt === undefined + ? existingCurriculum.reportCommonPrompt // body에 없으면 기존 값 유지 + : reportCommonPrompt || undefined,

curriculum-page.tsx의 카테고리/순서 변경 등 20여 곳이 updateCurriculum을 호출하지만, 이 머지 덕분에 리포트 프롬프트가 날아가지 않는다. 방어는 API 계층에만 존재한다.

4. 테스트 탭의 dev 우회 — prod만 프록시를 탄다

apps/web/components/sections/report-test.tsx +function getReportTestEndpoint(path = "") { + if (process.env.NODE_ENV !== "production") { + return `http://127.0.0.1:8787/report-tests${path}`; // 브라우저가 외부 서버 직접 호출 + } + return `/api/reports/test${path}`; // prod만 인증 프록시 경유 +}

dev에서는 인증·X-Service-API-Key 프록시(test/route.ts)가 실행되지 않는다. 반면 JSON 노션 탭은 dev에서도 항상 /api/reports/json-test를 탄다 — 두 탭의 경로 전략이 다르다.

5. Notion 페이지 생성 — 100블록 초과분 append

apps/web/lib/report-json-notion.ts + children: blocks.slice(0, NOTION_BLOCK_LIMIT), // 첫 페이지에 100개 + for (let index = 100; index < blocks.length; index += 100) { + await requestNotion(apiKey, `/blocks/${page.id}/children`, { ... });

레이어별 변경 요약

레이어파일핵심 변경
Pagecomponents/pages/report-page.tsx단일 테이블 → 3탭 셸 (−205줄)
FE 섹션sections/report-session-settings.tsx신규 527줄. 회기 편집 + 공통/개별 프롬프트 UI, 편집 모달
FE 섹션sections/report-test.tsx신규 575줄. MP3+로그 폼, 로그→CSV 변환, 상태 폴링
FE 섹션sections/report-json-test.tsx신규 169줄. 결과 JSON→Notion 발행 UI
APIapi/reports/test/route.ts신규. 외부 서버 프록시(GET 프롬프트, POST 폼)
APIapi/reports/test/[jobId]/route.ts신규. 작업 상태 폴링 프록시
APIapi/reports/json-test/route.ts신규. JSON→Notion 생성 엔드포인트
APIapi/curriculums/route.ts · [id]/route.ts리포트 프롬프트 파싱·검증, 기존 값 머지
Liblib/report-document.ts신규 741줄. JSON→Notion 블록 변환기 + 스키마 + 폴백
Liblib/report-json-notion.ts신규 91줄. Notion API 클라이언트
Liblib/report-evaluation-design-sample.ts신규 764줄. 테스트용 샘플 JSON(데이터)
DBlib/db-queries.ts · types/db/curriculum.types.tscurriculum에 리포트 프롬프트 필드 2개

리뷰 관전 포인트

구조

검증기 복붙. parseReportCommonPrompt/parseReportSessionPromptsapi/curriculums/route.ts[id]/route.ts에 완전히 동일하게 중복 정의됐다. 회차 키 정규식(^[1-9]\d*$) 등 규칙이 갈라질 위험이 있으니 공용 모듈로 추출 권장.

동작 확인

dev 우회 비대칭. 프롬프트 테스트 탭은 dev에서 127.0.0.1:8787을 직접 호출해 인증·프록시가 검증되지 않는다. JSON 노션 탭은 dev에서도 /api 경유라 로컬에도 NOTION_* 키가 필요하다. 두 탭의 경로 전략 차이가 의도된 것인지 확인 필요.

데이터 소실

update가 항상 덮어씀. updateCurriculum(|| null)은 부분 업데이트가 아니다. API PUT의 기존 값 머지로만 방어되므로, 배치/스크립트가 db 함수를 필드 없이 직접 호출하면 프롬프트가 초기화된다.

하드코딩

워크스페이스 slug 기본값. NOTION_APP_WORKSPACE_SLUG 기본값이 "succulent-glitter-109"로 코드에 박혀 있다. 다른 워크스페이스에 배포하면 생성된 문서 링크가 잘못된 slug를 가리킨다.

엄격도

파서 두 갈래. json-test는 parseGeneratedReportDocument(hasOnlyKeys 엄격 검증)만 사용 — Gemini가 규격 외 키를 하나라도 넣으면 전체가 REPORT_GENERATION_INVALID_JSON(400)으로 실패한다. 같은 파일의 관대한 parseReportDocument(dynamic 폴백)는 이 경로에서 안 쓰인다.

영향 범위

롤아웃 안전. DB는 optional 속성 추가라 마이그레이션·백필 불필요, 기존 curriculum 무변경. 신규 API는 admin/dev 전용. env 미설정 시 각각 *_NOT_CONFIGURED/503으로 안전하게 실패한다.

관련 문서

관련 문서: 회기 보고서 전사 파이프라인 — 기획과 구현 (75282732) — PR #889에서 만든 보고서 탭·전사→보고서→Notion 파이프라인을 이 PR이 프롬프트 실험/JSON 테스트 도구로 확장한다.