1) 공공데이터 활용 — 국립국악원 API 500건
💡 쉽게 말하면: 국립국악원이 공개한 국악 악구 데이터를 API로 500건 받아 왔습니다. 그런데 원본에는 감성 태그도, 숫자 BPM도, 루프 길이도 없습니다. 그래서 있는 필드(장단·박자·템포 텍스트)에서 규칙에 따라 이 값들을 만들어 냅니다. 아래는 “어느 원본 필드가 우리 DB의 어느 칸에 들어가는가”를 전부 적어 둔 표입니다.
3-1. 데이터 소스
| 항목 | 내용 |
|---|---|
| 제공기관 | 국립국악원 (공공데이터포털 경유) |
| 엔드포인트 | https://apis.data.go.kr/1371034/phrasedataview2/view |
| 응답 형식 | XML (header.resultCode + body.items.item[]) |
| 페이지 크기 | 20~100건 / 요청, SLEEP_SEC = 0.4 레이트 제한 준수 |
| 수집 목표 | TARGET = 500건 |
| 산출물 | backend/scripts/gugak_500.csv — 500행 확정 |
| 라이선스 | KOGL(공공누리) 제1유형 / 제2유형 |
수집 스크립트는 두 개입니다.
backend/scripts/collect_gugak_500.py— API → CSV (수집 전용)backend/해커톤 대비용/Collect_tracks.py— CSV/API → PostgreSQL 적재 (--dry-run지원)
3-2. 원본 XML 필드 목록
CSV 헤더가 곧 원본 API가 내려주는 필드입니다.
| 원본 필드 | 의미 | 값 예시 |
|---|---|---|
phrs_nm_kor / phrs_nm_eng | 악구명 (한/영) | 상사별곡050 / sangsabyeolgok_5bit |
phrs_desc_kor / phrs_desc_eng | 악구 설명 (한/영) | 상사별곡 장구 |
singer / player | 가창자 / 연주자 | 홍창남 |
instr_cd | 악기 코드 | PHINST0022 |
genre_cd | 장르 코드 | GENR0030 |
rhythm | 장단·리듬 | 5박, 도드리, 5박·8박, (빈 값) |
beat | 박자 기호 | 5/4 |
traditional_key / western_key | 전통 조 / 서양 조성 | 계면조 / NULL |
area | 시대 구분 | 조선, 고려, 신라, NULL |
wav_file_path | 음원 URL | https://apis.gugak.go.kr/phrase/...wav |
phrase_cd | 악구 코드 (고유키) | P1-001-050 |
lyrics | 가사 (긴 텍스트, 개행 포함) | … |
⚠️
lyrics필드에는 줄바꿈이 포함돼 있어 CSV 파일의 줄 수와 레코드 수가 다릅니다. 파일은 10,252줄이지만 정식 CSV 파서로 읽으면 500행입니다. 적재 스크립트는csv모듈로 읽으므로 문제가 없지만,wc -l로 건수를 세면 안 됩니다.
3-3. 원본 필드 ↔ DB 컬럼 매핑
gugak_tracks 테이블 기준입니다.
| DB 컬럼 | 원본 필드 | 변환 |
|---|---|---|
id | — | uuid4() 생성 |
title | phrs_nm_kor (폴백 sorc_nm, title) | 공백 제거 |
artist | player / singer | 그대로 |
instrument | instr_cd | 악기 코드 → 악기명 매핑 |
jangdan_name | rhythm, beat 등 | 파생 — §3-4 |
bpm | flctn_tempo / tempo / bpm | 파생 — §3-5 |
audio_url | wav_file_path | 그대로 |
public_license_type | pblcte_se_nm 등 | 파생 — §3-6 |
description_ko / description_en | phrs_desc_kor / phrs_desc_eng | 그대로 |
cue_points | — | MVP는 빈 배열 [] (JSONB) |
embedding | — | 별도 임베딩 파이프라인이 채움 (Vector(1536)) |
created_at | — | 적재 시각 (UTC) |
| 중복 판정 키 | phrase_cd + title | 재실행 시 기존 키 스킵 |
track_emotion_tags 테이블은 §3-7에서 별도로 채웁니다.
3-4. 장단 파생 규칙 (infer_jangdan)
원본의 rhythm은 5박, 도드리처럼 6종 표준 장단과 어긋난 값이 많고 결측도 있습니다. 후보 필드를 모두 이어 붙인 문자열에서 표준 장단명을 부분 문자열로 탐색합니다.
VALID_JANGDAN = {"자진모리", "중모리", "굿거리", "휘모리", "세마치", "엇모리"}
JANGDAN_FALLBACK = "중모리"
def infer_jangdan(item) -> str:
blob = " ".join([item.get("jangdan_nm"), item.get("jangdanNm"),
item.get("jangdan"), item.get("rhythm"), item.get("beat")])
for jangdan in VALID_JANGDAN:
if jangdan in blob:
return jangdan
return JANGDAN_FALLBACK # 중모리
투명한 한계 선언: 매칭에 실패하면 전부
중모리로 폴백합니다. 수집한 500건의rhythm값이5박·도드리·5박·8박·빈 값 네 종류뿐이라, 실제로는 대부분이 폴백을 탄다는 뜻입니다. 이는 원본 데이터에 표준 장단 필드가 없기 때문이며, 장단 정확도를 높이려면 (a) 악구 코드→장단 대응표를 별도로 확보하거나 (b) 오디오 신호에서 박 주기를 추출하는 후처리가 필요합니다. v1.2 과제로 둡니다.
3-5. BPM 파생 규칙 (parse_bpm)
def parse_bpm(item) -> int:
raw = item.get("flctn_tempo") or item.get("tempo") or item.get("bpm") or ""
m = re.search(r"\d+", str(raw))
return int(m.group()) if m else 90 # 기본값 90
자유 텍스트에서 첫 숫자를 뽑고, 없으면 중간 템포인 90으로 둡니다. CREATE의 BPM 필터 기본 범위는 60~200이므로 폴백 값도 필터에 걸립니다.
3-6. 라이선스 판정 규칙 (normalize_license)
def normalize_license(item) -> str:
raw = item.get("pblcte_se_nm") or item.get("pblctSeNm") or item.get("public_license_type") or ""
return "KOGL_2" if "2" in raw else "KOGL_1"
| 값 | 의미 | 상업적 이용 | 프론트 배지 |
|---|---|---|---|
KOGL_1 | 공공누리 제1유형 — 출처표시 | ✅ 가능 | CC-BY (Commercial OK) |
KOGL_2 | 공공누리 제2유형 — 출처표시 + 상업금지 | ❌ 불가 | CC-BY-NC (Attribution Only) |
도메인 레이어의 LICENSE_IS_COMMERCIAL 딕셔너리가 이 판정을 갖고 있고, GugakTrack.is_commercial 프로퍼티로 노출됩니다. 프론트엔드 LicenseBadge가 이 값으로 색을 바꿉니다.
판정 근거가 되는 원본 필드가 비어 있으면
KOGL_1(상업 가능)으로 낙관 처리됩니다. 상업 이용 전에는 원본 음원 페이지의 이용 조건을 반드시 재확인해야 하며, v1.1에서 원본 필드 결측 시KOGL_2로 보수 처리하도록 규칙을 뒤집을 예정입니다.
3-7. 감성 태그 파생 규칙 (infer_emotions)
원본에 감성 축이 없으므로 장단에서 감성을 유도합니다.
table = {
"자진모리": ["신남", "경쾌"],
"중모리": ["서정", "차분"],
"굿거리": ["흥겨움", "신명"],
"휘모리": ["긴박", "빠름"],
"세마치": ["슬픔", "차분"],
"엇모리": ["신비", "독특"],
}
파생된 태그는 track_emotion_tags 테이블에 행 단위로 저장되며, sort_order로 순서를 보존합니다. 첫 번째 태그(sort_order = 0)가 primary_emotion이 되고, 프리셋 URL의 emotion 파라미터로 쓰입니다.
⚠️ 도메인 enum과 수집 규칙의 어휘가 다릅니다. 도메인
EmotionTag는 6종(신남·서정·웅장·슬픔·신비·차분)인데, 수집 규칙은경쾌·흥겨움·신명·긴박·빠름·독특같은 enum 밖 값도 만듭니다. 현재는 두 번째 태그가 enum에 없는 값일 수 있으므로, 적재 시 enum 화이트리스트로 걸러 내거나EmotionTag를 확장하는 정리가 필요합니다. MVP 잔여 과제로 표시합니다.
3-8. 재실행 안전성
수집·적재는 중단 후 재개할 수 있어야 합니다.
| 단계 | 안전장치 |
|---|---|
| API 수집 | 페이지 단위 진행, SLEEP_SEC = 0.4로 레이트 제한 회피 |
| DB 적재 | get_existing_keys()로 (phrase_cd, title) 기존 키를 미리 읽어 중복 스킵 |
| 임베딩 | LEFT JOIN으로 임베딩 없는 트랙만 조회 — 중단 후 이어받기 가능 |
| 검증 | --dry-run 모드로 DB 쓰기 없이 매핑 결과만 확인 |
3-9. 데이터 품질 요약
| 항목 | 상태 |
|---|---|
| 수집 건수 | 500 / 500 ✅ |
audio_url 유효성 | 전 건 apis.gugak.go.kr 도메인 ✅ |
| 장단 정확도 | ⚠️ 대부분 폴백(중모리) — v1.2 과제 |
| 감성 태그 | ⚠️ 장단 파생, enum 밖 어휘 혼입 — 정리 필요 |
| BPM | ⚠️ 원본 결측 시 90 고정 |
| 라이선스 | ⚠️ 결측 시 KOGL_1 낙관 처리 — v1.1에서 보수 처리로 변경 |
cue_points | MVP는 빈 배열, v1.1에서 자동 추출 |
이 표를 감추지 않고 문서 전면에 두는 이유는, 심사와 재현 검증에서 “어디까지가 실데이터이고 어디부터가 파생값인가”가 가장 먼저 물어지는 질문이기 때문입니다.