chore: GitHub-readiness 재검토 정리
Browse files코드 안전성:
- explore_data.py · test_api.py: 모듈 레벨 코드를 main() 함수로 감싸고
if __name__ == '__main__' 가드 추가. 이전엔 import만으로 .env 로드 +
API 호출 시도 + UnicodeEncodeError(cp949) 발생.
- 이모지 제거 (Windows 콘솔 인코딩 안전성 가이드라인 준수)
- 14개 모듈 모두 부작용 없이 import 됨 검증.
브랜드 정합성:
- src/ 12개 .py 모듈의 docstring 첫 줄 'K-Curator: ...' → '사이 (SAI): ...'
저장소 정돈:
- 루트 CLAUDE.md 삭제 (v0 stale, 옛 K-Curator 이름·e뮤지엄 한정 진행상황)
- src/CLAUDE.md 추적 해제 (어시스턴트 internal handoff. 정식 회고는
docs/07-development-journey.md 참고)
- .gitignore에 CLAUDE.md 패턴 명시
- .gitignore +5 -0
- CLAUDE.md +0 -105
- src/CLAUDE.md +0 -155
- src/api.py +1 -1
- src/build_index.py +1 -1
- src/daily_pick.py +1 -1
- src/embed_images.py +1 -1
- src/explore_data.py +59 -57
- src/match_locations.py +1 -1
- src/rag.py +1 -1
- src/scrape_all.py +1 -1
- src/scrape_list.py +1 -1
- src/scrape_one.py +1 -1
- src/scrape_permanent.py +1 -1
- src/scrape_special.py +1 -1
- src/search.py +1 -1
- src/test_api.py +54 -44
.gitignore
CHANGED
|
@@ -32,3 +32,8 @@ data/processed/chunks.jsonl
|
|
| 32 |
.claude/
|
| 33 |
.DS_Store
|
| 34 |
Thumbs.db
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 32 |
.claude/
|
| 33 |
.DS_Store
|
| 34 |
Thumbs.db
|
| 35 |
+
|
| 36 |
+
# AI 어시스턴트 internal handoff 문서 (CLAUDE.md)
|
| 37 |
+
# — 일반 독자에겐 노이즈. 정식 인수인계 정보는 docs/07-development-journey.md에 정리됨.
|
| 38 |
+
CLAUDE.md
|
| 39 |
+
src/CLAUDE.md
|
CLAUDE.md
DELETED
|
@@ -1,105 +0,0 @@
|
|
| 1 |
-
# K-Curator 프로젝트 — 진행 상황 인수인계
|
| 2 |
-
|
| 3 |
-
## 프로젝트 개요
|
| 4 |
-
국립중앙박물관 소장품 기반 AI 도슨트 챗봇 (포트폴리오용 사이드 프로젝트).
|
| 5 |
-
RAG + 멀티모달 + 사용자 적응형 톤 조절(어린이/성인/외국인)이 차별점.
|
| 6 |
-
|
| 7 |
-
상세 기획은 `docs/K-Curator_프로젝트_정리.md` 참고.
|
| 8 |
-
|
| 9 |
-
## 환경
|
| 10 |
-
- OS: Windows 11
|
| 11 |
-
- Python 3.13.12 (시스템 PATH 등록됨)
|
| 12 |
-
- 가상환경: `C:\K-Curator\.venv`
|
| 13 |
-
- 활성화 명령: `.\.venv\Scripts\Activate.ps1`
|
| 14 |
-
- 설치된 라이브러리: requests, python-dotenv, beautifulsoup4, lxml
|
| 15 |
-
- API 키: `.env` 파일의 `EMUSEUM_API_KEY` (36자, KCISA 발급)
|
| 16 |
-
|
| 17 |
-
## 현재까지 진행한 것
|
| 18 |
-
|
| 19 |
-
### 1. e뮤지엄 OpenAPI 발급 완료
|
| 20 |
-
- 문화포털(KCISA)에서 발급
|
| 21 |
-
- 일 1,000건 한도 (개발 계정)
|
| 22 |
-
|
| 23 |
-
### 2. 첫 API 호출 성공 (`src/test_api.py`)
|
| 24 |
-
- 엔드포인트: `https://api.kcisa.kr/openapi/service/rest/meta/MPKreli`
|
| 25 |
-
- 파라미터: serviceKey, numOfRows, pageNo
|
| 26 |
-
- 전체 데이터: 334,187개
|
| 27 |
-
|
| 28 |
-
### 3. 데이터 품질 조사 완료 (`src/explore_data.py`)
|
| 29 |
-
- description 채워진 비율: 페이지마다 60~100% 들쭉날쭉
|
| 30 |
-
- subjectKeyword/subjectCategory: 거의 0% (사용 불가)
|
| 31 |
-
- 발견: API의 description은 "보존상태 기록" 위주
|
| 32 |
-
→ RAG 자료로는 부적합
|
| 33 |
-
|
| 34 |
-
### 4. 큐레이터 추천 페이지 분석 완료
|
| 35 |
-
- URL: https://www.museum.go.kr/MUSEUM/contents/M0501000000.do
|
| 36 |
-
- 총 321건 (33페이지)
|
| 37 |
-
- 6개 카테고리: 선사·고대, 중·근세, 조각·공예, 서화, 아시아, 보존과학
|
| 38 |
-
- 작품 1개당 약 3,000자 깊이 있는 큐레이터 해설
|
| 39 |
-
- 큐레이터 실명 명시
|
| 40 |
-
- 라이선스: 공공누리 3유형 (출처표시+변경금지)
|
| 41 |
-
- **이게 K-Curator의 진짜 RAG 자료원**
|
| 42 |
-
|
| 43 |
-
## 데이터 소스 전략 (확정)
|
| 44 |
-
|
| 45 |
-
### 메인 RAG 자료
|
| 46 |
-
**큐레이터 추천 페이지 321건 (스크래핑)**
|
| 47 |
-
- URL 패턴 (리스트): `?cp={페이지}&searchId=recommend&relicRecommendUse=Y`
|
| 48 |
-
- URL 패턴 (상세): `?schM=view&relicRecommendId={ID}`
|
| 49 |
-
- 이미지 패턴: `https://www.museum.go.kr/files/zin/curator_{번호}_{n}.jpg`
|
| 50 |
-
|
| 51 |
-
### 메타데이터 보강
|
| 52 |
-
**e뮤지엄 API**
|
| 53 |
-
- 시대(temporal), 재질(medium) 등 안정적 메타데이터
|
| 54 |
-
|
| 55 |
-
### Phase 1 목표
|
| 56 |
-
**200점 → 321건 다 가져가도 무방**
|
| 57 |
-
|
| 58 |
-
## 지금 작업 중인 것
|
| 59 |
-
|
| 60 |
-
**파일: `src/scrape_one.py`**
|
| 61 |
-
|
| 62 |
-
목표: 작품 1개(`<기영회도>` ID 2351292)를 자동 스크래핑해서 JSON으로 저장.
|
| 63 |
-
|
| 64 |
-
추출 항목:
|
| 65 |
-
1. 제목(메인+부제) + 큐레이터명
|
| 66 |
-
2. 메타데이터 캡션 (작가, 시대, 재질, 크기, 소장번호, 등급)
|
| 67 |
-
3. 본문 해설 (h5, p 태그 위주)
|
| 68 |
-
4. 이미지 URL 5장
|
| 69 |
-
5. 라이선스 정보
|
| 70 |
-
|
| 71 |
-
저장 위치: `data/raw/relic_{ID}.json`
|
| 72 |
-
|
| 73 |
-
테스트 작품 정보:
|
| 74 |
-
- 제목: 〈기영회도〉- 세 가지 복을 누린 원로 관료들의 잔치
|
| 75 |
-
- 큐레이터: 오다연
|
| 76 |
-
- 시대: 조선 1584년
|
| 77 |
-
- 재질: 비단에 색
|
| 78 |
-
- 등급: 보물 제1328호
|
| 79 |
-
|
| 80 |
-
## 다음 단계 (우선순위)
|
| 81 |
-
|
| 82 |
-
1. ✅ `scrape_one.py` 완성 + 테스트 (1개 작품 자동 수집)
|
| 83 |
-
2. ⏳ `scrape_list.py` (321개 작품 ID 리스트 수집)
|
| 84 |
-
3. ⏳ `scrape_all.py` (321개 전체 자동 스크래핑 → `data/raw/`)
|
| 85 |
-
4. ⏳ 텍스트 임베딩 + 벡터 DB 구축 (Chroma 또는 Qdrant)
|
| 86 |
-
5. ⏳ RAG 파이프라인 (어린이/성인 모드 시스템 프롬프트)
|
| 87 |
-
6. ⏳ Streamlit UI
|
| 88 |
-
|
| 89 |
-
## 주의사항
|
| 90 |
-
|
| 91 |
-
- 스크래핑 시 서버 부담 줄이기: 요청 간 1~2초 sleep 권장
|
| 92 |
-
- 라이선스 표시 필수 (출처: 국립중앙박물관 / 공공누리 3유형)
|
| 93 |
-
- API 키 / 서비스키는 절대 GitHub에 커밋 X (`.env`는 `.gitignore` 등록 완료)
|
| 94 |
-
- 한국 정부 API/사이트는 가끔 한글 인코딩 이슈가 있을 수 있음
|
| 95 |
-
- 이미지는 멀티모달용으로 저작권 주의 (Phase 2)
|
| 96 |
-
|
| 97 |
-
## 코드 작성 가이드라인
|
| 98 |
-
|
| 99 |
-
- Python 3.13 기준
|
| 100 |
-
- 가상환경(.venv) 활성화 상태에서 작업
|
| 101 |
-
- 한글 주석/메시지 OK
|
| 102 |
-
- 함수는 짧고 명확하게
|
| 103 |
-
- 에러 처리는 명시적으로
|
| 104 |
-
- 출력 메시지에 이모지 자제 (Windows 콘솔 인코딩 이슈 방지)
|
| 105 |
-
- 데이터 저장은 항상 `data/raw/` (raw) 또는 `data/processed/` (정제)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/CLAUDE.md
DELETED
|
@@ -1,155 +0,0 @@
|
|
| 1 |
-
# K-Curator 프로젝트 — 진행 상황 인수인계
|
| 2 |
-
|
| 3 |
-
## 프로젝트 개요
|
| 4 |
-
국립중앙박물관 소장품 기반 AI 도슨트 챗봇 (포트폴리오용 사이드 프로젝트).
|
| 5 |
-
RAG + 멀티모달 + 사용자 적응형 톤 조절(어린이/성인/외국인)이 차별점.
|
| 6 |
-
|
| 7 |
-
상세 기획은 `docs/K-Curator_프로젝트_정리.md` 참고.
|
| 8 |
-
|
| 9 |
-
## 환경
|
| 10 |
-
- OS: Windows 11
|
| 11 |
-
- Python 3.13.12 (시스템 PATH 등록됨)
|
| 12 |
-
- 가상환경: `C:\K-Curator\.venv`
|
| 13 |
-
- 활성화 명령: `.\.venv\Scripts\Activate.ps1`
|
| 14 |
-
- 설치된 라이브러리: requests, python-dotenv, beautifulsoup4, lxml,
|
| 15 |
-
chromadb, sentence-transformers (torch CPU 포함), openai
|
| 16 |
-
- API 키 (.env):
|
| 17 |
-
- `EMUSEUM_API_KEY` — 36자, KCISA 발급
|
| 18 |
-
- `OPENAI_API_KEY` — sk-proj-... 약 160자 (RAG LLM 생성용)
|
| 19 |
-
- LLM: OpenAI gpt-4o-mini (Anthropic API 미사용, 사용자 결제 환경 고려)
|
| 20 |
-
|
| 21 |
-
## 현재까지 진행한 것
|
| 22 |
-
|
| 23 |
-
### 1. e뮤지엄 OpenAPI 발급 완료
|
| 24 |
-
- 문화포털(KCISA)에서 발급
|
| 25 |
-
- 일 1,000건 한도 (개발 계정)
|
| 26 |
-
|
| 27 |
-
### 2. 첫 API 호출 성공 (`src/test_api.py`)
|
| 28 |
-
- 엔드포인트: `https://api.kcisa.kr/openapi/service/rest/meta/MPKreli`
|
| 29 |
-
- 파라미터: serviceKey, numOfRows, pageNo
|
| 30 |
-
- 전체 데이터: 334,187개
|
| 31 |
-
|
| 32 |
-
### 3. 데이터 품질 조사 완료 (`src/explore_data.py`)
|
| 33 |
-
- description 채워진 비율: 페이지마다 60~100% 들쭉날쭉
|
| 34 |
-
- subjectKeyword/subjectCategory: 거의 0% (사용 불가)
|
| 35 |
-
- 발견: API의 description은 "보존상태 기록" 위주
|
| 36 |
-
→ RAG 자료로는 부적합
|
| 37 |
-
|
| 38 |
-
### 4. 큐레이터 추천 페이지 분석 완료
|
| 39 |
-
- URL: https://www.museum.go.kr/MUSEUM/contents/M0501000000.do
|
| 40 |
-
- 총 321건 (33페이지)
|
| 41 |
-
- 6개 카테고리: 선사·고대, 중·근세, 조각·공예, 서화, 아시아, 보존과학
|
| 42 |
-
- 작품 1개당 약 3,000자 깊이 있는 큐레이터 해설
|
| 43 |
-
- 큐레이터 실명 명시
|
| 44 |
-
- 라이선스: 공공누리 3유형 (출처표시+변경금지)
|
| 45 |
-
- **이게 K-Curator의 진짜 RAG 자료원**
|
| 46 |
-
|
| 47 |
-
## 데이터 소스 전략 (확정)
|
| 48 |
-
|
| 49 |
-
### 메인 RAG 자료
|
| 50 |
-
**큐레이터 추천 페이지 321건 (스크래핑)**
|
| 51 |
-
- URL 패턴 (리스트): `?cp={페이지}&searchId=recommend&relicRecommendUse=Y`
|
| 52 |
-
- URL 패턴 (상세): `?schM=view&relicRecommendId={ID}`
|
| 53 |
-
- 이미지 패턴: `https://www.museum.go.kr/files/zin/curator_{번호}_{n}.jpg`
|
| 54 |
-
|
| 55 |
-
### 메타데이터 보강
|
| 56 |
-
**e뮤지엄 API**
|
| 57 |
-
- 시대(temporal), 재질(medium) 등 안정적 메타데이터
|
| 58 |
-
|
| 59 |
-
### Phase 1 목표
|
| 60 |
-
**200점 → 321건 다 가져가도 무방**
|
| 61 |
-
|
| 62 |
-
## 지금까지 만든 스크립트
|
| 63 |
-
|
| 64 |
-
**`src/scrape_one.py`** — 작품 1건 스크래퍼. `scrape(relic_id)` / `save(data)` 함수 export.
|
| 65 |
-
**`src/scrape_list.py`** — 321건 ID/제목/썸네일 리스트 수집. 단일 요청(pageSize=500).
|
| 66 |
-
**`src/scrape_all.py`** — list 결과를 읽어 전수 수집. `--force` 옵션으로 재수집 가능.
|
| 67 |
-
|
| 68 |
-
추출 항목:
|
| 69 |
-
1. 제목 + 부제 + 큐레이터명 (`curator_NNN_tit.gif`의 alt 파싱; 콜론/대시 변형 처리)
|
| 70 |
-
2. 메타데이터 (작가, 시대, 재질, 크기, 소장번호, 등급) + `raw_caption` 원본 보존
|
| 71 |
-
3. 본문 (heading/paragraph/quote/caption 4종 블록, 순서 보존)
|
| 72 |
-
4. 이미지 URL + alt 캡션
|
| 73 |
-
5. 라이선스 텍스트
|
| 74 |
-
|
| 75 |
-
## Phase 1 데이터 수집 완료 상태 (2026-04-29)
|
| 76 |
-
|
| 77 |
-
전체 321건 → `data/raw/relic_{ID}.json` 저장 완료.
|
| 78 |
-
|
| 79 |
-
| 지표 | 값 |
|
| 80 |
-
|---|---|
|
| 81 |
-
| 총 작품 수 | 321 |
|
| 82 |
-
| 총 본문 글자 | 약 108만 자 |
|
| 83 |
-
| 평균 본문 길이 | 3,366자 |
|
| 84 |
-
| 총 본문 블록 | 4,497개 |
|
| 85 |
-
| 총 이미지 | 1,423장 |
|
| 86 |
-
| 큐레이터명 추출 성공 | 251/321 (78%) |
|
| 87 |
-
|
| 88 |
-
남은 빈 필드는 대부분 캡션에 원래 없는 정보(선사 유물엔 등급 없음 등).
|
| 89 |
-
필요 시 `raw_caption`에서 후처리 가능.
|
| 90 |
-
|
| 91 |
-
## 임베딩 / 벡터 DB (2026-04-29 완료)
|
| 92 |
-
|
| 93 |
-
**`src/build_index.py`** — 청킹 → 임베딩 → Chroma 인덱스 빌드
|
| 94 |
-
**`src/search.py`** — top-k 검색 (RAG 검증/디버그용)
|
| 95 |
-
|
| 96 |
-
- 청킹: heading 단위 섹션, 1500자 초과 시 paragraph 경계로 분할, 100자 미만 제거
|
| 97 |
-
- 모델: `intfloat/multilingual-e5-small` (passage:/query: 프리픽스, normalize_embeddings)
|
| 98 |
-
- 저장: `data/processed/chunks.jsonl` (사람용 덤프) + `data/chroma/` (kcurator_relics)
|
| 99 |
-
|
| 100 |
-
| 지표 | 값 |
|
| 101 |
-
|---|---|
|
| 102 |
-
| 청크 수 | 1,171 |
|
| 103 |
-
| 평균 길이 | 951자 (max 2,486 / min 110) |
|
| 104 |
-
| 임베딩 차원 | 384 (cosine) |
|
| 105 |
-
| Chroma 디스크 | ~24 MB |
|
| 106 |
-
| 빌드 시간 | 약 4분 (CPU) |
|
| 107 |
-
|
| 108 |
-
검색 스모크 테스트 결과 top-1 cosine 유사도 0.86~0.91 — 의미적 retrieval 작동 확인.
|
| 109 |
-
재빌드: `python src/build_index.py` (기존 컬렉션 자동 삭제 후 재생성)
|
| 110 |
-
|
| 111 |
-
## RAG 파이프라인 (2026-04-29 완료)
|
| 112 |
-
|
| 113 |
-
**`src/rag.py`** — 검색 → 컨텍스트 구성 → OpenAI 호출 → 스트리밍 출력
|
| 114 |
-
- 검색: build_index의 Chroma 컬렉션 재사용 (top-k 기본 5)
|
| 115 |
-
- LLM: `gpt-4o-mini`, temperature=0.7, system 프롬프트로 톤 분리
|
| 116 |
-
- 톤 모드 3종: `adult` (성인 존댓말) / `kid` (어린이 ~예요체) / `foreign` (영어, 한자 병기)
|
| 117 |
-
- 출처 자동 표기: `— 참고: <작품명> (큐레이터: 이름)` 등 모드별 포맷
|
| 118 |
-
- "자료에 없으면 모른다고 답하라" 가드레일 시스템 프롬프트에 포함
|
| 119 |
-
|
| 120 |
-
사용 예:
|
| 121 |
-
```
|
| 122 |
-
python src/rag.py "기영회도가 뭐야?" --mode kid
|
| 123 |
-
python src/rag.py "Tell me about the moon jar" --mode foreign
|
| 124 |
-
python src/rag.py "..." --no-stream --k 5
|
| 125 |
-
```
|
| 126 |
-
|
| 127 |
-
알려진 자잘한 이슈:
|
| 128 |
-
- foreign 모드에서 LLM이 출처를 가끔 `<자료 1>`로 카피해 적음 → 시스템 프롬프트 보강 필요
|
| 129 |
-
|
| 130 |
-
## 다음 단계 (우선순위)
|
| 131 |
-
|
| 132 |
-
1. ✅ `scrape_one.py` (1건 자동 수집)
|
| 133 |
-
2. ✅ `scrape_list.py` (321개 ID 리스트)
|
| 134 |
-
3. ✅ `scrape_all.py` (전체 수집 — 321/321 성공)
|
| 135 |
-
4. ✅ `build_index.py` + `search.py` (청킹/임베딩/Chroma)
|
| 136 |
-
5. ✅ `rag.py` (RAG, 어린이/성인/외국인 3-mode)
|
| 137 |
-
6. ⏳ Streamlit UI (작품 이미지 카드 + 톤 토글 + 멀티턴)
|
| 138 |
-
|
| 139 |
-
## 주의사항
|
| 140 |
-
|
| 141 |
-
- 스크래핑 시 서버 부담 줄이기: 요청 간 1~2초 sleep 권장
|
| 142 |
-
- 라이선스 표시 필수 (출처: 국립중앙박물관 / 공공누리 3유형)
|
| 143 |
-
- API 키 / 서비스키는 절대 GitHub에 커밋 X (`.env`는 `.gitignore` 등록 완료)
|
| 144 |
-
- 한국 정부 API/사이트는 가끔 한글 인코딩 이슈가 있을 수 있음
|
| 145 |
-
- 이미지는 멀티모달용으로 저작권 주의 (Phase 2)
|
| 146 |
-
|
| 147 |
-
## 코드 작성 가이드라인
|
| 148 |
-
|
| 149 |
-
- Python 3.13 기준
|
| 150 |
-
- 가상환경(.venv) 활성화 상태에서 작업
|
| 151 |
-
- 한글 주석/메시지 OK
|
| 152 |
-
- 함수는 짧고 명확하게
|
| 153 |
-
- 에러 처리는 명시적으로
|
| 154 |
-
- 출력 메시지에 이모지 자제 (Windows 콘솔 인코딩 이슈 방지)
|
| 155 |
-
- 데이터 저장은 항상 `data/raw/` (raw) 또는 `data/processed/` (정제)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
src/api.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
- POST /api/chat — 검색 + LLM 스트리밍 (Server-Sent Events)
|
| 4 |
- GET /api/works/{id} — 작품 상세 JSON (출처 카드 클릭 시 사용)
|
| 5 |
- GET /api/health — 헬스체크
|
|
|
|
| 1 |
"""
|
| 2 |
+
사이 (SAI): FastAPI 백엔드
|
| 3 |
- POST /api/chat — 검색 + LLM 스트리밍 (Server-Sent Events)
|
| 4 |
- GET /api/works/{id} — 작품 상세 JSON (출처 카드 클릭 시 사용)
|
| 5 |
- GET /api/health — 헬스체크
|
src/build_index.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
- 입력: data/raw/relic_*.json (321건)
|
| 4 |
- 청킹: heading 단위 섹션. heading 등장 전의 도입부는 'intro' 섹션.
|
| 5 |
- 임베딩: intfloat/multilingual-e5-small (passage: 프리픽스 사용)
|
|
|
|
| 1 |
"""
|
| 2 |
+
사이 (SAI): 본문 청킹 + 임베딩 + Chroma 인덱스 구축
|
| 3 |
- 입력: data/raw/relic_*.json (321건)
|
| 4 |
- 청킹: heading 단위 섹션. heading 등장 전의 도입부는 'intro' 섹션.
|
| 5 |
- 임베딩: intfloat/multilingual-e5-small (passage: 프리픽스 사용)
|
src/daily_pick.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
- 날짜 기반 결정적 테마 (day-of-year) → 임베딩 검색으로 추천 작품 6점
|
| 4 |
- 별도 cron 불필요 — 같은 날엔 같은 결과, 다음 날엔 자동 갱신
|
| 5 |
|
|
|
|
| 1 |
"""
|
| 2 |
+
사이 (SAI): '오늘의 큐레이션' 생성기
|
| 3 |
- 날짜 기반 결정적 테마 (day-of-year) → 임베딩 검색으로 추천 작품 6점
|
| 4 |
- 별도 cron 불필요 — 같은 날엔 같은 결과, 다음 날엔 자동 갱신
|
| 5 |
|
src/embed_images.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
- 입력: data/raw/relic_*.json 의 images[] 필드 (1,400+장)
|
| 4 |
- 모델: sentence-transformers/clip-ViT-B-32-multilingual-v1 (한국어 텍스트도 같은 공간)
|
| 5 |
- 출력: data/chroma/ 에 'kcurator_images' 컬렉션
|
|
|
|
| 1 |
"""
|
| 2 |
+
사이 (SAI): 작품 이미지 CLIP 임베딩 → Chroma 인덱스
|
| 3 |
- 입력: data/raw/relic_*.json 의 images[] 필드 (1,400+장)
|
| 4 |
- 모델: sentence-transformers/clip-ViT-B-32-multilingual-v1 (한국어 텍스트도 같은 공간)
|
| 5 |
- 출력: data/chroma/ 에 'kcurator_images' 컬렉션
|
src/explore_data.py
CHANGED
|
@@ -1,101 +1,103 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 4 |
"""
|
| 5 |
|
| 6 |
import os
|
|
|
|
|
|
|
| 7 |
import requests
|
| 8 |
from dotenv import load_dotenv
|
| 9 |
|
| 10 |
-
load_dotenv()
|
| 11 |
-
API_KEY = os.getenv("EMUSEUM_API_KEY")
|
| 12 |
URL = "https://api.kcisa.kr/openapi/service/rest/meta/MPKreli"
|
| 13 |
|
| 14 |
|
| 15 |
-
def fetch_page(page_no: int, num_rows: int = 10):
|
| 16 |
-
"""특정 페이지의 데이터를 가져옴"""
|
| 17 |
-
params = {
|
| 18 |
-
"serviceKey": API_KEY,
|
| 19 |
-
"numOfRows": num_rows,
|
| 20 |
-
"pageNo": page_no,
|
| 21 |
-
}
|
| 22 |
response = requests.get(URL, params=params, headers={"Accept": "application/json"})
|
| 23 |
data = response.json()
|
| 24 |
-
|
| 25 |
if data["response"]["header"]["resultCode"] != "0000":
|
| 26 |
return None, 0
|
| 27 |
-
|
| 28 |
items = data["response"]["body"]["items"]["item"]
|
| 29 |
total = int(data["response"]["body"]["totalCount"])
|
| 30 |
return items, total
|
| 31 |
|
| 32 |
|
| 33 |
-
def analyze_quality(items, label: str):
|
| 34 |
-
"""데이터 품질 분석 + 출력"""
|
| 35 |
print(f"\n{'='*60}")
|
| 36 |
-
print(f"
|
| 37 |
print(f"{'='*60}")
|
| 38 |
-
|
| 39 |
if not items:
|
| 40 |
print(" 데이터 없음")
|
| 41 |
return
|
| 42 |
-
|
| 43 |
total = len(items)
|
| 44 |
-
|
| 45 |
-
# 필드별 채워진 개수 카운트
|
| 46 |
fields_to_check = [
|
| 47 |
-
(
|
| 48 |
-
(
|
| 49 |
-
(
|
| 50 |
-
(
|
| 51 |
-
(
|
| 52 |
]
|
| 53 |
-
|
| 54 |
-
print(f"\n
|
| 55 |
for field, label_kr in fields_to_check:
|
| 56 |
filled = sum(1 for item in items if item.get(field))
|
| 57 |
pct = (filled / total) * 100
|
| 58 |
-
bar =
|
| 59 |
print(f" {label_kr:6s} [{bar}] {filled}/{total} ({pct:.0f}%)")
|
| 60 |
-
|
| 61 |
-
|
| 62 |
-
print(f"\n [작품 제목 샘플]")
|
| 63 |
for i, item in enumerate(items[:5], 1):
|
| 64 |
-
title = item.get(
|
| 65 |
-
temporal = item.get(
|
| 66 |
print(f" {i}. {title} ({temporal})")
|
| 67 |
-
|
| 68 |
-
|
| 69 |
-
items_with_desc = [item for item in items if item.get('description')]
|
| 70 |
if items_with_desc:
|
| 71 |
sample = items_with_desc[0]
|
| 72 |
-
desc = sample.get(
|
| 73 |
-
print(
|
| 74 |
print(f" 제목: {sample.get('title') or '(없음)'}")
|
| 75 |
print(f" 해설: {desc}...")
|
| 76 |
|
| 77 |
|
| 78 |
-
|
| 79 |
-
|
| 80 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
|
| 82 |
-
|
| 83 |
-
|
| 84 |
-
|
| 85 |
|
| 86 |
-
|
|
|
|
|
|
|
| 87 |
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
|
| 91 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 92 |
|
| 93 |
-
# 3. 중간 페이지 조회
|
| 94 |
-
middle_page = last_page // 2
|
| 95 |
-
items_middle, _ = fetch_page(middle_page, 10)
|
| 96 |
-
analyze_quality(items_middle, f"중간 페이지 #{middle_page}")
|
| 97 |
|
| 98 |
-
|
| 99 |
-
|
| 100 |
-
print(f"{'='*60}")
|
| 101 |
-
print("\n💰 사용한 API 호출 수: 3회 / 일일 한도 1000회")
|
|
|
|
| 1 |
"""
|
| 2 |
+
사이 (SAI): e뮤지엄 API 데이터 품질 조사 (v0 데이터 탐색 단계 산물).
|
| 3 |
+
|
| 4 |
+
1페이지(오래된 데이터) vs 마지막 페이지(최신) 데이터 품질 비교.
|
| 5 |
+
|
| 6 |
+
NOTE: v0 시절 의사결정의 근거가 된 스크립트 — e뮤지엄 API의 description이
|
| 7 |
+
"보존상태 기록" 위주임을 확인하고 큐레이터 추천 페이지 스크래핑으로
|
| 8 |
+
방향을 전환하게 만든 도구. 학습 여정의 흔적으로 보존됩니다.
|
| 9 |
"""
|
| 10 |
|
| 11 |
import os
|
| 12 |
+
import sys
|
| 13 |
+
|
| 14 |
import requests
|
| 15 |
from dotenv import load_dotenv
|
| 16 |
|
|
|
|
|
|
|
| 17 |
URL = "https://api.kcisa.kr/openapi/service/rest/meta/MPKreli"
|
| 18 |
|
| 19 |
|
| 20 |
+
def fetch_page(api_key: str, page_no: int, num_rows: int = 10):
|
| 21 |
+
"""특정 페이지의 데이터를 가져옴."""
|
| 22 |
+
params = {"serviceKey": api_key, "numOfRows": num_rows, "pageNo": page_no}
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
response = requests.get(URL, params=params, headers={"Accept": "application/json"})
|
| 24 |
data = response.json()
|
|
|
|
| 25 |
if data["response"]["header"]["resultCode"] != "0000":
|
| 26 |
return None, 0
|
|
|
|
| 27 |
items = data["response"]["body"]["items"]["item"]
|
| 28 |
total = int(data["response"]["body"]["totalCount"])
|
| 29 |
return items, total
|
| 30 |
|
| 31 |
|
| 32 |
+
def analyze_quality(items, label: str) -> None:
|
| 33 |
+
"""데이터 품질 분석 + 출력."""
|
| 34 |
print(f"\n{'='*60}")
|
| 35 |
+
print(f" [{label}]")
|
| 36 |
print(f"{'='*60}")
|
| 37 |
+
|
| 38 |
if not items:
|
| 39 |
print(" 데이터 없음")
|
| 40 |
return
|
| 41 |
+
|
| 42 |
total = len(items)
|
|
|
|
|
|
|
| 43 |
fields_to_check = [
|
| 44 |
+
("description", "해설"),
|
| 45 |
+
("subjectKeyword", "키워드"),
|
| 46 |
+
("subjectCategory", "분류"),
|
| 47 |
+
("temporal", "시대"),
|
| 48 |
+
("medium", "재질"),
|
| 49 |
]
|
| 50 |
+
|
| 51 |
+
print(f"\n 필드 채워짐 비율 (총 {total}개 중)")
|
| 52 |
for field, label_kr in fields_to_check:
|
| 53 |
filled = sum(1 for item in items if item.get(field))
|
| 54 |
pct = (filled / total) * 100
|
| 55 |
+
bar = "#" * int(pct / 10) + "-" * (10 - int(pct / 10))
|
| 56 |
print(f" {label_kr:6s} [{bar}] {filled}/{total} ({pct:.0f}%)")
|
| 57 |
+
|
| 58 |
+
print("\n 작품 제목 샘플")
|
|
|
|
| 59 |
for i, item in enumerate(items[:5], 1):
|
| 60 |
+
title = item.get("title") or "(제목없음)"
|
| 61 |
+
temporal = item.get("temporal") or "(시대불명)"
|
| 62 |
print(f" {i}. {title} ({temporal})")
|
| 63 |
+
|
| 64 |
+
items_with_desc = [item for item in items if item.get("description")]
|
|
|
|
| 65 |
if items_with_desc:
|
| 66 |
sample = items_with_desc[0]
|
| 67 |
+
desc = sample.get("description", "")[:200]
|
| 68 |
+
print("\n 해설 있는 작품 예시")
|
| 69 |
print(f" 제목: {sample.get('title') or '(없음)'}")
|
| 70 |
print(f" 해설: {desc}...")
|
| 71 |
|
| 72 |
|
| 73 |
+
def main() -> int:
|
| 74 |
+
load_dotenv()
|
| 75 |
+
api_key = os.getenv("EMUSEUM_API_KEY")
|
| 76 |
+
if not api_key:
|
| 77 |
+
print("EMUSEUM_API_KEY 가 .env 에 없습니다.", file=sys.stderr)
|
| 78 |
+
return 1
|
| 79 |
+
|
| 80 |
+
print("사이 — 데이터 품질 조사 시작")
|
| 81 |
+
print(f" API 키 길이: {len(api_key)}자")
|
| 82 |
|
| 83 |
+
items_first, total = fetch_page(api_key, 1, 10)
|
| 84 |
+
print(f"\n 전체 데이터 수: {total:,}개")
|
| 85 |
+
analyze_quality(items_first, "1페이지 (가장 오래된 데이터 추정)")
|
| 86 |
|
| 87 |
+
last_page = (total // 10) + (1 if total % 10 else 0)
|
| 88 |
+
items_last, _ = fetch_page(api_key, last_page, 10)
|
| 89 |
+
analyze_quality(items_last, f"마지막 페이지 #{last_page} (최신 데이터 추정)")
|
| 90 |
|
| 91 |
+
middle_page = last_page // 2
|
| 92 |
+
items_middle, _ = fetch_page(api_key, middle_page, 10)
|
| 93 |
+
analyze_quality(items_middle, f"중간 페이지 #{middle_page}")
|
| 94 |
+
|
| 95 |
+
print(f"\n{'='*60}")
|
| 96 |
+
print(" 조사 완료")
|
| 97 |
+
print(f"{'='*60}")
|
| 98 |
+
print("\n 사용한 API 호출 수: 3회 / 일일 한도 1000회")
|
| 99 |
+
return 0
|
| 100 |
|
|
|
|
|
|
|
|
|
|
|
|
|
| 101 |
|
| 102 |
+
if __name__ == "__main__":
|
| 103 |
+
sys.exit(main())
|
|
|
|
|
|
src/match_locations.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
- 추천 321점의 작품 제목과 상설 643점의 작품명을 비교
|
| 4 |
- 매칭되면 추천 작품에도 hall/floor/room_name 위치 메타를 부착
|
| 5 |
- 출력: data/raw/relic_locations.json (relic_id → location 매핑)
|
|
|
|
| 1 |
"""
|
| 2 |
+
사이 (SAI): 추천 작품 ↔ 상설 작품 매칭
|
| 3 |
- 추천 321점의 작품 제목과 상설 643점의 작품명을 비교
|
| 4 |
- 매칭되면 추천 작품에도 hall/floor/room_name 위치 메타를 부착
|
| 5 |
- 출력: data/raw/relic_locations.json (relic_id → location 매핑)
|
src/rag.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
- 검색: Chroma + e5-small (build_index.py와 동일 모델)
|
| 4 |
- 생성: OpenAI gpt-4o-mini
|
| 5 |
- 톤 모드: adult(성인) / kid(어린이) / foreign(외국인용 영어)
|
|
|
|
| 1 |
"""
|
| 2 |
+
사이 (SAI): RAG 챗 파이프라인
|
| 3 |
- 검색: Chroma + e5-small (build_index.py와 동일 모델)
|
| 4 |
- 생성: OpenAI gpt-4o-mini
|
| 5 |
- 톤 모드: adult(성인) / kid(어린이) / foreign(외국인용 영어)
|
src/scrape_all.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
- 입력: data/raw/relic_list.json (scrape_list.py 결과)
|
| 4 |
- 동작: 각 ID마다 scrape_one.scrape() 호출 → data/raw/relic_{ID}.json 저장
|
| 5 |
- 이미 저장된 파일은 스킵하므로 중간 재시작 가능
|
|
|
|
| 1 |
"""
|
| 2 |
+
사이 (SAI): 전체 큐레이터 추천 작품 자동 스크래핑
|
| 3 |
- 입력: data/raw/relic_list.json (scrape_list.py 결과)
|
| 4 |
- 동작: 각 ID마다 scrape_one.scrape() 호출 → data/raw/relic_{ID}.json 저장
|
| 5 |
- 이미 저장된 파일은 스킵하므로 중간 재시작 가능
|
src/scrape_list.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
- 출처: https://www.museum.go.kr/MUSEUM/contents/M0501000000.do?searchId=recommend&relicRecommendUse=Y
|
| 4 |
- pageSize=500 으로 한 번에 321건을 받아온다.
|
| 5 |
- 출력: data/raw/relic_list.json
|
|
|
|
| 1 |
"""
|
| 2 |
+
사이 (SAI): 큐레이터 추천 작품 ID 리스트 수집
|
| 3 |
- 출처: https://www.museum.go.kr/MUSEUM/contents/M0501000000.do?searchId=recommend&relicRecommendUse=Y
|
| 4 |
- pageSize=500 으로 한 번에 321건을 받아온다.
|
| 5 |
- 출력: data/raw/relic_list.json
|
src/scrape_one.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
- 입력: relicRecommendId (예: 2351292 = 〈기영회도〉)
|
| 4 |
- 출력: data/raw/relic_{ID}.json
|
| 5 |
"""
|
|
|
|
| 1 |
"""
|
| 2 |
+
사이 (SAI): 큐레이터 추천 작품 1개 스크래핑
|
| 3 |
- 입력: relicRecommendId (예: 2351292 = 〈기영회도〉)
|
| 4 |
- 출력: data/raw/relic_{ID}.json
|
| 5 |
"""
|
src/scrape_permanent.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
- 7관 39실을 순회하며 각 실의 소개 텍스트 + 현재 전시중인 작품 리스트 수집
|
| 4 |
- 출력: data/raw/permanent.json (단일 파일)
|
| 5 |
|
|
|
|
| 1 |
"""
|
| 2 |
+
사이 (SAI): 국립중앙박물관 상설전시 스크래퍼
|
| 3 |
- 7관 39실을 순회하며 각 실의 소개 텍스트 + 현재 전시중인 작품 리스트 수집
|
| 4 |
- 출력: data/raw/permanent.json (단일 파일)
|
| 5 |
|
src/scrape_special.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
- 현재 진행중인 특별·테마 전시 + 상세 페이지 본문 수집
|
| 4 |
- 출력: data/raw/special.json
|
| 5 |
"""
|
|
|
|
| 1 |
"""
|
| 2 |
+
사이 (SAI): 국립중앙박물관 특별전/테마전 스크래퍼
|
| 3 |
- 현재 진행중인 특별·테마 전시 + 상세 페이지 본문 수집
|
| 4 |
- 출력: data/raw/special.json
|
| 5 |
"""
|
src/search.py
CHANGED
|
@@ -1,5 +1,5 @@
|
|
| 1 |
"""
|
| 2 |
-
|
| 3 |
사용법:
|
| 4 |
python src/search.py "조선시대 잔치 그림"
|
| 5 |
python src/search.py "조선시대 잔치 그림" --k 10
|
|
|
|
| 1 |
"""
|
| 2 |
+
사이 (SAI): Chroma 인덱스 검색 (RAG 검증용)
|
| 3 |
사용법:
|
| 4 |
python src/search.py "조선시대 잔치 그림"
|
| 5 |
python src/search.py "조선시대 잔치 그림" --k 10
|
src/test_api.py
CHANGED
|
@@ -1,51 +1,61 @@
|
|
| 1 |
"""
|
| 2 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 3 |
"""
|
| 4 |
|
| 5 |
import os
|
|
|
|
|
|
|
| 6 |
import requests
|
| 7 |
from dotenv import load_dotenv
|
| 8 |
|
| 9 |
-
|
| 10 |
-
|
| 11 |
-
|
| 12 |
-
|
| 13 |
-
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
| 17 |
-
|
| 18 |
-
|
| 19 |
-
|
| 20 |
-
|
| 21 |
-
"
|
| 22 |
-
|
| 23 |
-
|
| 24 |
-
|
| 25 |
-
|
| 26 |
-
print(
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
| 31 |
-
|
| 32 |
-
|
| 33 |
-
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
| 38 |
-
|
| 39 |
-
|
| 40 |
-
print(f"가져온 작품: {len(items)}개")
|
| 41 |
-
|
| 42 |
-
|
| 43 |
-
|
| 44 |
-
|
| 45 |
-
|
| 46 |
-
|
| 47 |
-
|
| 48 |
-
|
| 49 |
-
|
| 50 |
-
|
| 51 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
"""
|
| 2 |
+
사이 (SAI): e뮤지엄 API 첫 호출 테스트 (v0 데이터 탐색 단계 산물).
|
| 3 |
+
|
| 4 |
+
NOTE: 이 스크립트는 v0 시절 데이터 품질 조사 도구입니다.
|
| 5 |
+
사이의 본 운영(상세 작품 본문은 큐레이터 추천 페이지 스크래핑으로 확보)에는
|
| 6 |
+
사용되지 않으며, 학습 여정의 흔적으로 보존됩니다.
|
| 7 |
"""
|
| 8 |
|
| 9 |
import os
|
| 10 |
+
import sys
|
| 11 |
+
|
| 12 |
import requests
|
| 13 |
from dotenv import load_dotenv
|
| 14 |
|
| 15 |
+
|
| 16 |
+
def main() -> int:
|
| 17 |
+
load_dotenv()
|
| 18 |
+
api_key = os.getenv("EMUSEUM_API_KEY")
|
| 19 |
+
if not api_key:
|
| 20 |
+
print(
|
| 21 |
+
".env 파일에서 EMUSEUM_API_KEY를 못 찾았어요. "
|
| 22 |
+
"KCISA 발급 키를 .env에 추가하세요.",
|
| 23 |
+
file=sys.stderr,
|
| 24 |
+
)
|
| 25 |
+
return 1
|
| 26 |
+
|
| 27 |
+
print(f"API 키 로딩 성공 (길이: {len(api_key)}자)")
|
| 28 |
+
|
| 29 |
+
url = "https://api.kcisa.kr/openapi/service/rest/meta/MPKreli"
|
| 30 |
+
params = {"serviceKey": api_key, "numOfRows": 5, "pageNo": 1}
|
| 31 |
+
|
| 32 |
+
print("API 호출 중...")
|
| 33 |
+
response = requests.get(url, params=params, headers={"Accept": "application/json"})
|
| 34 |
+
print(f"응답 코드: {response.status_code}")
|
| 35 |
+
|
| 36 |
+
data = response.json()
|
| 37 |
+
result_code = data["response"]["header"]["resultCode"]
|
| 38 |
+
result_msg = data["response"]["header"]["resultMsg"]
|
| 39 |
+
print(f"결과: [{result_code}] {result_msg}")
|
| 40 |
+
if result_code != "0000":
|
| 41 |
+
print("정상 응답이 아닙니다.")
|
| 42 |
+
return 2
|
| 43 |
+
|
| 44 |
+
items = data["response"]["body"]["items"]["item"]
|
| 45 |
+
total = data["response"]["body"]["totalCount"]
|
| 46 |
+
print(f"전체 작품 수: {total}개 / 가져온 작품: {len(items)}개\n")
|
| 47 |
+
|
| 48 |
+
for idx, item in enumerate(items, 1):
|
| 49 |
+
print(f"--- [{idx}] ---")
|
| 50 |
+
print(f"제목: {item.get('title') or '(없음)'}")
|
| 51 |
+
print(f"시대: {item.get('temporal') or '(없음)'}")
|
| 52 |
+
print(f"재질: {item.get('medium') or '(없음)'}")
|
| 53 |
+
print(f"분류: {item.get('subjectCategory') or '(없음)'}")
|
| 54 |
+
desc = item.get("description") or "(없음)"
|
| 55 |
+
print(f"해설: {desc[:100]}\n")
|
| 56 |
+
|
| 57 |
+
return 0
|
| 58 |
+
|
| 59 |
+
|
| 60 |
+
if __name__ == "__main__":
|
| 61 |
+
sys.exit(main())
|