Library — 음원→라이브러리 2계층의 중간 층 · 운영사 큐레이팅 (#053)
SPEC #053 정합. BE 계약 + /sync-api 타입 생성. 운영사 백오피스(apps/admin) /libraries
화면(목록·생성·상세 곡 목록·이름수정·삭제)과 /music 카탈로그 [라이브러리에 추가] 가 라이브.
Overview
라이브러리 = 타입(AI/TRUST) 묶음의 음원 컬렉션(OPERATOR 소유). 음악 2계층(음원→라이브러리→ 플레이리스트)의 중간 층으로, 운영사(OPERATOR)가 음원을 라이브러리에 큐레이팅한다. 라이브러리 CRUD + 음원 할당/해제(M:N)를 제공한다. 플레이리스트(라이브러리를 담음)·플랜 gating 은 후속(F2).
- 라이브러리 = 단일 타입 묶음 —
libraryTypeAI(AI 생성)·TRUST(신탁). 기획서 §4 “라이브러리 단일 타입·혼합 불가”. 타입은 생성 시 고정(이후 불변 — 수정은 이름만). - library_music = 음원 할당(M:N) — 운영사가 음원을 라이브러리에 추가/제거. 추가는 멱등 (중복 추가 무시 — unique(library,music)), 제거도 멱등(담겨 있지 않아도 성공). 음원 자체는 유지.
- 소프트삭제 — 라이브러리 삭제(
deleteLibrary→ 204)는deleted_at만 채우고 담긴 음원 할당을 유지한다(복구 가능, #042 패턴). - 음원 타입 enforcement (#059, F1 해소) — 음원에
musicSource(AI/TRUST)가 추가돼 “라이브러리 타입(libraryType) = 음원 타입(musicSource)” 을 강제한다. 불일치 음원을 담으려 하면 400LIBRARY_TYPE_MISMATCH(위반 musicId 를fields.violatingMusicIds에 노출·전체 reject).
BE 계약
| operationId | 메서드 · 경로 | 요청 | 성공 |
|---|---|---|---|
createLibrary | POST /api/v1/admin/libraries | {name(1..255), libraryType(AI/TRUST)} | 201 LibraryResponse(곡 수 0) |
listLibraries | GET /api/v1/admin/libraries | query q?(max 100) · libraryType? · page(=0) · size(=20, 1..100 clamp) | 200 LibraryListResponse |
getLibrary | GET /api/v1/admin/libraries/:id | path id | 200 LibraryResponse(곡 수 포함) |
updateLibrary | PATCH /api/v1/admin/libraries/:id | {name} (libraryType 불변) | 200 LibraryResponse |
deleteLibrary | DELETE /api/v1/admin/libraries/:id | path id | 204 No Content (소프트삭제) |
listLibraryMusic | GET /api/v1/admin/libraries/:id/music | query page(=0) · size(=20, 1..100 clamp) | 200 LibraryMusicListResponse |
addLibraryMusic | POST /api/v1/admin/libraries/:id/music | {musicIds[](0..200)} (멱등) | 204 No Content (타입 불일치 시 400 LIBRARY_TYPE_MISMATCH, #059) |
removeLibraryMusic | DELETE /api/v1/admin/libraries/:id/music/:musicId | path id·musicId (멱등) | 204 No Content |
- 인가: OPERATOR-only(
/api/v1/admin/**=hasRole("OPERATOR")). - 정렬 고정 — 라이브러리 목록
created_at DESC, 라이브러리 곡 목록 할당 시각 DESC. 클라이언트 정렬 없음. - 활성만 —
deleted_at IS NULL라이브러리/할당만 노출.total은 활성 전체 건수. - 목록 항목(
LibraryListItem)은 곡 수 미포함 — 곡 수(musicCount)는 상세(getLibrary) 응답에만 포함. - DTO 상세는 DTOs · Library · endpoint 표는 Endpoints · Admin Library 참조.
에러 / 제한
- 라이브러리 미존재·soft-deleted → 404
LIBRARY_NOT_FOUND(존재 은닉). addLibraryMusic음원 1건이라도 미존재 → 404MUSIC_NOT_FOUND(all-or-nothing).- (#059)
addLibraryMusic음원musicSource≠ 라이브러리libraryType→ 400LIBRARY_TYPE_MISMATCH(위반 musicId 를fields.violatingMusicIds에 노출·전체 reject). 운영사 picker 는 음원 타입과 일치하는 라이브러리만 후보로 좁혀 사전 차단한다(Music 라이브러리에 추가 참조). - 이름 1..255 위반·타입 누락 → 400
LIBRARY_INVALID_FIELD/VALIDATION_ERROR. - 에러 카탈로그: Error Codes.
운영사 라이브러리 UI (apps/admin)
목록/생성/상세/다이얼로그는 ops-libraries 시안 정합이다(handoff design/screens/ops-libraries.jsx).
시안이 전제하나 BE 데이터에 없는 요소(곡 미리듣기·커버·아티스트)는 생략한다([[feedback_design_only_from_handoff]]).
- 목록(
/libraries) — generateduseListLibraries(react-query, client query) fetch. 컬럼: 이름(아이콘 칩 + 상세 링크) · 타입 배지(AI/TRUST) · 생성일(KST) · 행 액션(상세·이름수정·삭제). 이름 검색(q, max 100)· 타입 필터(AI/TRUST)·페이지네이션을 client state 로 보유하고(applied/draft 분리), 생성/수정/삭제 mutation 성공 시 현재 params 의getListLibrariesQueryKey를 invalidate 해 즉시 갱신한다. 공용ListToolbar·ListPagination·ListNoResults재사용. - 타입 배지는 색만으로 구분하지 않는다 —
AI/TRUST배지 옆에 한국어 부가 라벨(AI 생성·신탁)을 항상 병기한다(가드 — 색맹/대비 접근성,library-meta.ts단일 소스). - 생성 — 헤더 [라이브러리 생성] → 다이얼로그(이름 입력 + 타입 카드 선택: 배지 + 부가라벨 +
설명) →
createLibrary. 타입은 생성 시 고정(이후 불변). 빈 라이브러리가 만들어지고 음원은 상세에서 담는다. - 상세(
/libraries/[id]) — server component 가getLibrary(개요+곡 수)를 fetch 해 client 에 내려준다 (refresh-aware, 404 → notFound). 시안 정합 헤더: 뒤로가기 링크 → 아이콘 칩 + 제목 + 타입 배지(라벨 병기)- 담긴 음원 수, 액션 [음원 담기]·[이름 수정]·[삭제]. 그 아래 타입 enforcement 안내 노트(라이브러리
타입 = 음원 타입, 같은 타입 음원만 담긴다는 #059 설명). 담긴 음원 목록은 generated
useListLibraryMusic(페이지네이션) client query 로 fetch — 행에 다중 선택 체크박스 컬럼.
- 담긴 음원 수, 액션 [음원 담기]·[이름 수정]·[삭제]. 그 아래 타입 enforcement 안내 노트(라이브러리
타입 = 음원 타입, 같은 타입 음원만 담긴다는 #059 설명). 담긴 음원 목록은 generated
- 음원 담기(상세 직접, 일괄) — 헤더/빈상태 [음원 담기] →
LibraryAddMusicDialog. generateduseListMusic(검색 +musicSource = libraryType로 일치 음원만, #059)로 후보를 조회하고 체크박스 다중 선택(1..200) →addLibraryMusic({musicIds:[...]}, 멱등). 성공 시 현재 page 의getListLibraryMusicQueryKeyinvalidate +router.refresh()(헤더 곡 수 최신화). picker 가 후보를 타입 일치로 좁혀 사전 차단하며, 그래도 발생하는 400LIBRARY_TYPE_MISMATCH는 사용자 메시지로 매핑. - 음원 제거(단건·일괄) — 행 [제거]는
LibraryMusicRemoveDialog(1-step, “음원 자체는 삭제되지 않음” info 안내) →removeLibraryMusic. 체크박스 선택 시 하단 일괄 제거 바(N곡 선택됨 · 일괄 제거 · 선택 해제)가 뜨고, [선택 음원 일괄 제거] →LibraryMusicBulkRemoveDialog가 선택 id 마다removeLibraryMusic를 fan-out(Promise.allSettled, 부분 성공 안내) 한다. 제거 성공 시getListLibraryMusicQueryKeyinvalidate + 선택 해제. - 이름수정/삭제 — 헤더 [이름 수정] →
LibraryRenameDialog(이름만 수정 + 타입은 잠금 표시: 읽기 전용 배지 + 🔒 “변경할 수 없습니다”) →updateLibrary(성공 시router.refresh()). [삭제] →LibraryDeleteDialog(2-step: warn 안내 → danger 확정, 이름·타입 병기) →deleteLibrary(소프트삭제, 성공 시/libraries복귀). - 카탈로그 [라이브러리에 추가] —
/music카탈로그 행 액션.MusicAddToLibraryDialog가useListLibraries(검색 포함,libraryType = 음원 musicSource로 일치 후보만, #059) 로 대상 라이브러리를 고르게 하고addLibraryMusic({musicIds:[id]}) 로 추가한다(멱등 — 중복 추가도 성공). picker 필터로 타입 불일치 선택을 사전 차단하며, 그래도 발생하는 400LIBRARY_TYPE_MISMATCH는 사용자 메시지로 매핑한다. 추가는 라이브러리 측 할당이라 카탈로그 목록은 invalidate 하지 않는다.
영향 파일: apps/admin/src/app/(protected)/libraries/{page,library-list-client,library-create-dialog, library-rename-dialog,library-delete-dialog,library-music-remove-dialog,library-add-music-dialog, library-music-bulk-remove-dialog,library-meta}.tsx · libraries/[id]/{page,library-detail-client}.tsx ·
music/music-add-to-library-dialog.tsx(카탈로그 [라이브러리에 추가]) · apps/admin/src/lib/backend.ts
(backendListLibraries·backendCreateLibrary·backendGetLibrary·backendUpdateLibrary·
backendDeleteLibrary·backendListLibraryMusic·backendAddLibraryMusic·backendRemoveLibraryMusic
server-side helper + 타입 alias).
Not in scope / Followups
F1 음원 타입 enforcement— #059 로 해소. 음원musicSource(AI/TRUST) ↔ 라이브러리libraryType일치를 강제(불일치 400LIBRARY_TYPE_MISMATCH). 무드·BPM 등 나머지 음원 enrich 는 별도.- F2 플레이리스트 — 라이브러리를 담는 플레이리스트(다음 도메인). 플랜 gating 포함.
- F3 본사 조회 — 본사가 라이브러리/플레이리스트를 보는 read(플레이리스트 경유).
다중선택 일괄 추가/제거—ops-libraries시안 정합으로 해소. 상세 [음원 담기]에서 체크박스 다중 선택 → 일괄 담기(addLibraryMusicmusicIds[]), 담긴 음원은 체크박스 선택 → 일괄 제거(단건removeLibraryMusicfan-out). 카탈로그 측 행 단위 [라이브러리에 추가]는 그대로 유지.- AI 추천·diff·중복 힌트 (v1 스코프 외) — 시안/기획이 전제하는 “이 라이브러리에 어울리는 음원 AI 추천”, “다른 라이브러리와의 곡 구성 diff”, “이미 담긴 곡/유사곡 중복 힌트” 는 음원 리치 메타 (무드/BPM/태그)·유사도 분석(Cyanite 등)이 선행해야 한다. 현재 라이브러리는 운영사 수동 큐레이팅 (행 단위 추가/제거)만 제공하며, 위 추천류는 v1 범위 밖이다([[feedback_design_only_from_handoff]]).
References
- SPEC #053 — 라이브러리 도메인(음원→라이브러리, 운영사 큐레이팅)
- SPEC #059 — 음원 타입(
musicSource) enforcement(라이브러리 타입 일치 강제·F1 해소) - 기획서
21-policy-music §4(라이브러리 단일 타입·AI/TRUST) · 핸드오프design/screens/ops-libraries.jsx(목록·상세·다이얼로그 시안) ·ops-music-library(카탈로그 [라이브러리에 추가]) linkmusic-frontend-space/packages/api-client/src/generated/endpoints/library/·schemas/libraryResponse.ts등- Endpoints · DTOs · Music · Data Schema