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

GET /api/health
{
  "ok": true,
  "collection_size": 1265,
  "modes": ["adult", "kid", "foreign"]
}

GET /api/today

날짜 기반 결정적 큐레이션. 같은 날엔 같은 응답, 다음 날 자동 갱신.

GET /api/today
{
  "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

GET /api/works?limit=24&offset=0&q=백자
파라미터 타입 기본값 설명
limit int 0 (전체) 최대 반환 개수
offset int 0 시작 인덱스
q string "" 작품명 부분 일치 검색
{
  "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}

GET /api/works/2351292

전체 작품 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 이미지 유사도로 닮은 작품 추천.

GET /api/works/2351292/similar?k=6

내부 동작:

  1. 해당 작품의 첫 이미지 임베딩을 query로 사용
  2. kcurator_images 컬렉션에서 cosine top-N 검색
  3. 같은 작품 자기 자신 제외, 작품 단위 dedupe
{
  "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

GET /api/exhibitions
{
  "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}

GET /api/halls/서화관

해당 관의 모든 실 + 작품 리스트 전체.

{
  "hall": "서화관",
  "floor": "2F",
  "rooms": [
    {
      "hall": "서화관",
      "floor": "2F",
      "room_name": "외규장각 의궤",
      "showroom_code": "DM0028",
      "intro": "...",
      "works": ["장렬왕후존숭도감의궤", "..."],
      "url": "..."
    }
  ]
}

POST /api/chat (SSE)

3-mode RAG 챗.

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 페이로드 예시:

[
  {
    "relic_id": 2351292,
    "title": "〈기영회도〉",
    "subtitle": "세 가지 복을 누린 원로 관료들의 잔치",
    "curator": "오다연",
    "section": "",
    "period": "조선 1584년",
    "score": 0.883,
    "thumbnail_url": "https://...",
    "detail_url": "https://..."
  }
]

POST /api/plan (SSE)

관람 코스 빌더.

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 페이로드:

[
  {
    "relic_id": 16847,
    "title": "단원풍속도첩, 김홍도",
    "subtitle": "",
    "category": "recommend",
    "hall": "",
    "floor": "",
    "location": "",
    "thumbnail_url": "https://..."
  }
]

LLM 출력 예시 (foreign mode):

## 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)

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만 찾던 파서가 묵묵히 무한 대기 → 정규식으로 모두 수용.