k-curator / docs /04-api-reference.md
wangihong's picture
docs: 종합 문서화 + LICENSE + CHANGELOG + CONTRIBUTING
9c1b3d2
# 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`만 찾던 파서가 묵묵히 무한 대기 → 정규식으로 모두 수용.