wangihong commited on
Commit
433f312
·
1 Parent(s): 9c1b3d2

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 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
- K-Curator: FastAPI 백엔드
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
- K-Curator: 본문 청킹 + 임베딩 + Chroma 인덱스 구축
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
- K-Curator: '오늘의 큐레이션' 생성기
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
- K-Curator: 작품 이미지 CLIP 임베딩 → Chroma 인덱스
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
- K-Curator: 데이터 품질 조사
3
- 1페이지(오래된 데이터) vs 마지막 페이지(최신 데이터) 비교
 
 
 
 
 
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" 📊 {label}")
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
- ('description', '해설'),
48
- ('subjectKeyword', '키워드'),
49
- ('subjectCategory', '분류'),
50
- ('temporal', '시대'),
51
- ('medium', '재질'),
52
  ]
53
-
54
- print(f"\n [필드 채워짐 비율] (총 {total}개 중)")
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 = '█' * int(pct / 10) + '░' * (10 - int(pct / 10))
59
  print(f" {label_kr:6s} [{bar}] {filled}/{total} ({pct:.0f}%)")
60
-
61
- # 작품 제목 샘플 5개
62
- print(f"\n [작품 제목 샘플]")
63
  for i, item in enumerate(items[:5], 1):
64
- title = item.get('title') or '(제목없음)'
65
- temporal = item.get('temporal') or '(시대불명)'
66
  print(f" {i}. {title} ({temporal})")
67
-
68
- # 해설 있는 작품 1개 보여주기
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('description', '')[:200]
73
- print(f"\n [해설 있는 작품 예시]")
74
  print(f" 제목: {sample.get('title') or '(없음)'}")
75
  print(f" 해설: {desc}...")
76
 
77
 
78
- # ===== 메인 실행 =====
79
- print("🔍 K-Curator 데이터 품질 조사 시작")
80
- print(f" API 길이: {len(API_KEY)}자")
 
 
 
 
 
 
81
 
82
- # 1. 1페이지 조회
83
- items_first, total = fetch_page(1, 10)
84
- print(f"\n📦 전체 데이터 수: {total:,}개")
85
 
86
- analyze_quality(items_first, "1페이지 (가장 오래된 데이터 추정)")
 
 
87
 
88
- # 2. 마지막 페이지 조회
89
- last_page = (total // 10) + (1 if total % 10 else 0)
90
- items_last, _ = fetch_page(last_page, 10)
91
- analyze_quality(items_last, f"마지막 페이지 #{last_page} (최신 데이터 추정)")
 
 
 
 
 
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
- print(f"\n{'='*60}")
99
- print(" ✅ 조사 완료!")
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
- K-Curator: 추천 작품 ↔ 상설 작품 매칭
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
- K-Curator: RAG 챗 파이프라인
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
- K-Curator: 전체 큐레이터 추천 작품 자동 스크래핑
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
- K-Curator: 큐레이터 추천 작품 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
 
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
- K-Curator: 큐레이터 추천 작품 1개 스크래핑
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
- K-Curator: 국립중앙박물관 상설전시 스크래퍼
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
- K-Curator: 국립중앙박물관 특별전/테마전 스크래퍼
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
- K-Curator: Chroma 인덱스 검색 (RAG 검증용)
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
- K-Curator: e뮤지엄 API 첫 호출 테스트
 
 
 
 
3
  """
4
 
5
  import os
 
 
6
  import requests
7
  from dotenv import load_dotenv
8
 
9
- load_dotenv()
10
- API_KEY = os.getenv("EMUSEUM_API_KEY")
11
-
12
- if not API_KEY:
13
- raise ValueError(".env 파일에서 EMUSEUM_API_KEY를 못 찾았어요!")
14
-
15
- print(f"API 로딩 성공 (길이: {len(API_KEY)}자)")
16
-
17
- URL = "https://api.kcisa.kr/openapi/service/rest/meta/MPKreli"
18
- params = {
19
- "serviceKey": API_KEY,
20
- "numOfRows": 5,
21
- "pageNo": 1,
22
- }
23
-
24
- print("API 호출 중...")
25
- response = requests.get(URL, params=params, headers={"Accept": "application/json"})
26
- print(f"응답 코드: {response.status_code}")
27
-
28
- data = response.json()
29
- result_code = data["response"]["header"]["resultCode"]
30
- result_msg = data["response"]["header"]["resultMsg"]
31
- print(f"결과: [{result_code}] {result_msg}")
32
-
33
- if result_code != "0000":
34
- print("정상 응답이 아닙니다.")
35
- exit()
36
-
37
- items = data["response"]["body"]["items"]["item"]
38
- total = data["response"]["body"]["totalCount"]
39
- print(f"전체 작품 수: {total}개")
40
- print(f"가져온 작품: {len(items)}개")
41
- print("")
42
-
43
- for idx, item in enumerate(items, 1):
44
- print(f"--- [{idx}] ---")
45
- print(f"제목: {item.get('title') or '(없음)'}")
46
- print(f"시대: {item.get('temporal') or '(없음)'}")
47
- print(f"재질: {item.get('medium') or '(없음)'}")
48
- print(f"분류: {item.get('subjectCategory') or '(없음)'}")
49
- desc = item.get('description') or '(없음)'
50
- print(f"해설: {desc[:100]}")
51
- print("")
 
 
 
 
 
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())