# 04. API 레퍼런스 베이스 URL (production): `https://wangihong-k-curator.hf.space` 베이스 URL (local dev): `http://127.0.0.1:8000` ## 엔드포인트 요약 | 메서드 | 경로 | 역할 | 응답 형식 | |---|---|---|---| | GET | `/api/health` | 헬스체크 | JSON | | GET | `/api/today` | 오늘의 테마 + 추천 6점 | JSON | | GET | `/api/works` | 321점 카탈로그 (검색·페이지) | JSON | | GET | `/api/works/{id}` | 작품 1점 상세 | JSON | | GET | `/api/works/{id}/similar` | CLIP 닮은 작품 | JSON | | GET | `/api/exhibitions` | 7관 36실 + 진행중 특별전 | JSON | | GET | `/api/halls/{name}` | 특정 관 상세 | JSON | | POST | `/api/chat` | RAG 챗 (3-mode) | SSE 스트림 | | POST | `/api/plan` | 관람 코스 빌더 | SSE 스트림 | 자동 OpenAPI 문서: `/docs` (Swagger UI), `/redoc` --- ## GET /api/health ```http GET /api/health ``` ```json { "ok": true, "collection_size": 1265, "modes": ["adult", "kid", "foreign"] } ``` --- ## GET /api/today 날짜 기반 결정적 큐레이션. 같은 날엔 같은 응답, 다음 날 자동 갱신. ```http GET /api/today ``` ```json { "date": "2026-04-30", "theme": { "id": "letters", "ko": "한자와 한글", "en": "Hanja and Hangeul", "seed": "한글 훈민정음 한자 글씨" }, "picks": [ { "relic_id": 1234567, "title": "한글 금속활자", "subtitle": "", "curator": "이재정", "period": "조선", "thumbnail_url": "https://www.museum.go.kr/...", "score": 0.872 } // ... 5 more ] } ``` 테마 풀은 `src/daily_pick.py`에 30개 정의. day-of-year(0~365) 기준 회전. --- ## GET /api/works ```http GET /api/works?limit=24&offset=0&q=백자 ``` | 파라미터 | 타입 | 기본값 | 설명 | |---|---|---|---| | `limit` | int | 0 (전체) | 최대 반환 개수 | | `offset` | int | 0 | 시작 인덱스 | | `q` | string | "" | 작품명 부분 일치 검색 | ```json { "total": 321, "offset": 0, "items": [ { "relic_recommend_id": 2351292, "title_full": "<기영회도> - 세 가지 복을 누린 원로 관료들의 잔치", "thumbnail_url": "https://...", "detail_url": "https://www.museum.go.kr/..." } ] } ``` --- ## GET /api/works/{relic_id} ```http GET /api/works/2351292 ``` 전체 작품 JSON 반환 (제목·부제·큐레이터·메타·본문 블록·이미지·라이선스). ```json { "relic_recommend_id": 2351292, "source_url": "...", "scraped_at": "2026-04-29T14:10:33", "title": "〈기영회도〉", "subtitle": "세 가지 복을 누린 원로 관료들의 잔치", "curator": "오다연", "title_image_url": "https://...", "metadata": { "raw_caption": "작가 모름, <기영회도>, 조선 1584년, 비단에 색, ...", "artist": "작가 모름", "period": "조선 1584년", "medium": "비단에 색", "size": "163x128.5cm", "collection_no": "국립중앙박물관(신수14888)", "grade": "보물 제1328호" }, "images": [ { "url": "https://...", "caption": "작가 모름, <기영회도>, ..." } ], "body": [ { "type": "paragraph", "text": "16세기 후반..." }, { "type": "heading", "text": "1584년에 열린 성대한 잔치" }, { "type": "quote", "text": "..." }, { "type": "caption", "text": "...", "image_url": "https://..." } ], "license": "국립중앙박물관이(가) 창작한 ..." } ``` --- ## GET /api/works/{relic_id}/similar CLIP 이미지 유사도로 닮은 작품 추천. ```http GET /api/works/2351292/similar?k=6 ``` 내부 동작: 1. 해당 작품의 첫 이미지 임베딩을 query로 사용 2. `kcurator_images` 컬렉션에서 cosine top-N 검색 3. 같은 작품 자기 자신 제외, 작품 단위 dedupe ```json { "source_relic_id": 2351292, "similar": [ { "relic_id": 16858, "title": "<호조낭관계회도>", "subtitle": "", "curator": "...", "period": "조선", "image_url": "https://...", "thumbnail_url": "https://...", "score": 0.913 } // ... up to k items ] } ``` 이미지 컬렉션이 없으면 503 반환. --- ## GET /api/exhibitions ```http GET /api/exhibitions ``` ```json { "halls": [ { "name": "서화관", "floor": "2F", "rooms": [ { "name": "외규장각 의궤", "showroom_code": "DM0028", "url": "...", "works_count": 8 } ] } ], "special": [ { "exhi_id": "3056031", "title": "각角진 백자 이야기", "labels": ["현재전시", "테마전"], "period": "2025-08-26~2026-06-21", "location": "분청사기·백자실", "thumbnail_url": "https://...", "detail_url": "https://...", "intro": "조선 17세기부터 등장하여..." } ] } ``` --- ## GET /api/halls/{hall_name} ```http GET /api/halls/서화관 ``` 해당 관의 모든 실 + 작품 리스트 전체. ```json { "hall": "서화관", "floor": "2F", "rooms": [ { "hall": "서화관", "floor": "2F", "room_name": "외규장각 의궤", "showroom_code": "DM0028", "intro": "...", "works": ["장렬왕후존숭도감의궤", "..."], "url": "..." } ] } ``` --- ## POST /api/chat (SSE) 3-mode RAG 챗. ```http POST /api/chat Content-Type: application/json { "query": "기영회도가 뭐야?", "mode": "adult", // adult | kid | foreign "k": 5 } ``` 응답: `text/event-stream` (Server-Sent Events). 이벤트 타입: | event | data | 시점 | |---|---|---| | `sources` | JSON 배열 | 첫 번째 — 검색 결과 출처 카드 | | `token` | 문자열 | LLM 토큰 단위로 N회 | | `error` | 문자열 | 실패 시 1회 | | `done` | (빈 문자열) | 마지막 | `sources` 페이로드 예시: ```json [ { "relic_id": 2351292, "title": "〈기영회도〉", "subtitle": "세 가지 복을 누린 원로 관료들의 잔치", "curator": "오다연", "section": "", "period": "조선 1584년", "score": 0.883, "thumbnail_url": "https://...", "detail_url": "https://..." } ] ``` --- ## POST /api/plan (SSE) 관람 코스 빌더. ```http POST /api/plan Content-Type: application/json { "duration_min": 60, // 30 | 60 | 90 | 120 | 180 "companion": "self", // self | kid | foreign "interests": "조선 풍속화", "k": 18 } ``` 내부: 1. `interests` 임베딩 → 후보 18점 (작품 단위 dedupe) 2. LLM에게 후보 + 시간/동반자 가이드 + 출력 포맷을 system+user prompt로 전달 3. 마크다운 형식 코스 응답 스트리밍 이벤트: - `candidates` (JSON 배열) — 1회, 후보 작품 미리보기 - `token` (문자열) — N회, LLM 마크다운 토큰 - `done` — 1회 `candidates` 페이로드: ```json [ { "relic_id": 16847, "title": "단원풍속도첩, 김홍도", "subtitle": "", "category": "recommend", "hall": "", "floor": "", "location": "", "thumbnail_url": "https://..." } ] ``` LLM 출력 예시 (foreign mode): ```markdown ## Today's Course — about 60 min A walk through Joseon-era folk paintings... ### 1. Danwon Pungsokdo Cheop, Kim Hong-do — 큐레이터 추천 (10 min) Look at the rhythmic compositions... → → 1F → 2F (about 3 min walk) ### 2. ... ``` --- ## 클라이언트 SSE 파싱 (frontend/src/lib/api.js) ```js const SEP = /\r\n\r\n|\n\n|\r\r/ // 모든 EOL 변형 수용 while (true) { const { done, value } = await reader.read() if (done) break buffer += decoder.decode(value, { stream: true }) let m while ((m = SEP.exec(buffer))) { const ev = parseSSE(buffer.slice(0, m.index)) buffer = buffer.slice(m.index + m[0].length) handleEvent(ev) // event/data 분기 } } ``` 서버는 `sse-starlette`로 응답 — 이벤트 라인 끝이 `\r\n` 이라 처음에 `\n\n`만 찾던 파서가 묵묵히 무한 대기 → 정규식으로 모두 수용.