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에서 자동 추출

이 표를 감추지 않고 문서 전면에 두는 이유는, 심사와 재현 검증에서 “어디까지가 실데이터이고 어디부터가 파생값인가”가 가장 먼저 물어지는 질문이기 때문입니다.


SoundBridge — 당신의 음악 언어로 국악을 만나세요. 본 사이트는 개발 문서 자료입니다.

This site uses Just the Docs, a documentation theme for Jekyll.