FeaturesLibrary (라이브러리 큐레이팅)Library (라이브러리 — 음원 큐레이팅 컬렉션)

Library — 음원→라이브러리 2계층의 중간 층 · 운영사 큐레이팅 (#053)

SPEC #053 정합. BE 계약 + /sync-api 타입 생성. 운영사 백오피스(apps/admin) /libraries 화면(목록·생성·상세 곡 목록·이름수정·삭제)과 /music 카탈로그 [라이브러리에 추가] 가 라이브.

Overview

라이브러리 = 타입(AI/TRUST) 묶음의 음원 컬렉션(OPERATOR 소유). 음악 2계층(음원→라이브러리→ 플레이리스트)의 중간 층으로, 운영사(OPERATOR)가 음원을 라이브러리에 큐레이팅한다. 라이브러리 CRUD + 음원 할당/해제(M:N)를 제공한다. 플레이리스트(라이브러리를 담음)·플랜 gating 은 후속(F2).

  • 라이브러리 = 단일 타입 묶음libraryType AI(AI 생성)·TRUST(신탁). 기획서 §4 “라이브러리 단일 타입·혼합 불가”. 타입은 생성 시 고정(이후 불변 — 수정은 이름만).
  • library_music = 음원 할당(M:N) — 운영사가 음원을 라이브러리에 추가/제거. 추가는 멱등 (중복 추가 무시 — unique(library,music)), 제거도 멱등(담겨 있지 않아도 성공). 음원 자체는 유지.
  • 소프트삭제 — 라이브러리 삭제(deleteLibrary → 204)는 deleted_at 만 채우고 담긴 음원 할당을 유지한다(복구 가능, #042 패턴).
  • 음원 타입 enforcement (#059, F1 해소) — 음원에 musicSource(AI/TRUST)가 추가돼 “라이브러리 타입(libraryType) = 음원 타입(musicSource)” 을 강제한다. 불일치 음원을 담으려 하면 400 LIBRARY_TYPE_MISMATCH(위반 musicId 를 fields.violatingMusicIds 에 노출·전체 reject).

BE 계약

operationId메서드 · 경로요청성공
createLibraryPOST /api/v1/admin/libraries{name(1..255), libraryType(AI/TRUST)}201 LibraryResponse(곡 수 0)
listLibrariesGET /api/v1/admin/librariesquery q?(max 100) · libraryType? · page(=0) · size(=20, 1..100 clamp)200 LibraryListResponse
getLibraryGET /api/v1/admin/libraries/:idpath id200 LibraryResponse(곡 수 포함)
updateLibraryPATCH /api/v1/admin/libraries/:id{name} (libraryType 불변)200 LibraryResponse
deleteLibraryDELETE /api/v1/admin/libraries/:idpath id204 No Content (소프트삭제)
listLibraryMusicGET /api/v1/admin/libraries/:id/musicquery page(=0) · size(=20, 1..100 clamp)200 LibraryMusicListResponse
addLibraryMusicPOST /api/v1/admin/libraries/:id/music{musicIds[](0..200)} (멱등)204 No Content (타입 불일치 시 400 LIBRARY_TYPE_MISMATCH, #059)
removeLibraryMusicDELETE /api/v1/admin/libraries/:id/music/:musicIdpath 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건이라도 미존재 → 404 MUSIC_NOT_FOUND(all-or-nothing).
  • (#059) addLibraryMusic 음원 musicSource ≠ 라이브러리 libraryType → 400 LIBRARY_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) — generated useListLibraries(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 — 행에 다중 선택 체크박스 컬럼.
  • 음원 담기(상세 직접, 일괄) — 헤더/빈상태 [음원 담기] → LibraryAddMusicDialog. generated useListMusic(검색 + musicSource = libraryType 로 일치 음원만, #059)로 후보를 조회하고 체크박스 다중 선택(1..200) → addLibraryMusic({musicIds:[...]}, 멱등). 성공 시 현재 page 의 getListLibraryMusicQueryKey invalidate + router.refresh()(헤더 곡 수 최신화). picker 가 후보를 타입 일치로 좁혀 사전 차단하며, 그래도 발생하는 400 LIBRARY_TYPE_MISMATCH 는 사용자 메시지로 매핑.
  • 음원 제거(단건·일괄) — 행 [제거]는 LibraryMusicRemoveDialog(1-step, “음원 자체는 삭제되지 않음” info 안내) → removeLibraryMusic. 체크박스 선택 시 하단 일괄 제거 바(N곡 선택됨 · 일괄 제거 · 선택 해제)가 뜨고, [선택 음원 일괄 제거] → LibraryMusicBulkRemoveDialog 가 선택 id 마다 removeLibraryMusic 를 fan-out(Promise.allSettled, 부분 성공 안내) 한다. 제거 성공 시 getListLibraryMusicQueryKey invalidate + 선택 해제.
  • 이름수정/삭제 — 헤더 [이름 수정] → LibraryRenameDialog(이름만 수정 + 타입은 잠금 표시: 읽기 전용 배지 + 🔒 “변경할 수 없습니다”) → updateLibrary(성공 시 router.refresh()). [삭제] → LibraryDeleteDialog(2-step: warn 안내 → danger 확정, 이름·타입 병기) → deleteLibrary(소프트삭제, 성공 시 /libraries 복귀).
  • 카탈로그 [라이브러리에 추가]/music 카탈로그 행 액션. MusicAddToLibraryDialoguseListLibraries(검색 포함, libraryType = 음원 musicSource 로 일치 후보만, #059) 로 대상 라이브러리를 고르게 하고 addLibraryMusic({musicIds:[id]}) 로 추가한다(멱등 — 중복 추가도 성공). picker 필터로 타입 불일치 선택을 사전 차단하며, 그래도 발생하는 400 LIBRARY_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 일치를 강제(불일치 400 LIBRARY_TYPE_MISMATCH). 무드·BPM 등 나머지 음원 enrich 는 별도.
  • F2 플레이리스트 — 라이브러리를 담는 플레이리스트(다음 도메인). 플랜 gating 포함.
  • F3 본사 조회 — 본사가 라이브러리/플레이리스트를 보는 read(플레이리스트 경유).
  • 다중선택 일괄 추가/제거ops-libraries 시안 정합으로 해소. 상세 [음원 담기]에서 체크박스 다중 선택 → 일괄 담기(addLibraryMusic musicIds[]), 담긴 음원은 체크박스 선택 → 일괄 제거(단건 removeLibraryMusic fan-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