# AI 캐릭터 관리자 웹 PRD ## 문서 정보 | 항목 | 내용 | |---|---| | 문서 상태 | OpenAPI 반영 구현 기준 | | 작성일 | 2026-07-25 | | 최종 수정일 | 2026-07-30 | | 대상 제품 | AI 캐릭터 전용 독립 관리자 웹 | | 구현 대상 | React + TypeScript + Vite SPA | | UI 기반 | Tailwind CSS + shadcn/ui | | 관련 계획 | [plan-task.md](./plan-task.md) | | 정규화 계약 | [api-contract.openapi.json](./api-contract.openapi.json) | ### 상태 표기 - **확정**: 이번 인터뷰에서 합의되어 구현 기준으로 사용할 사항 - **미결**: 프론트엔드 제품·UI 또는 운영 정책이 결정되지 않아 후속 인터뷰나 UI 검토가 필요한 사항 - **외부 의존**: 프론트엔드가 결정할 사항이 아니며, 해당 기능의 network integration 전에 백엔드가 제공해야 하는 계약 - **권고**: 미결 사항에 대한 현재 추천안이며, 확정 전에는 계약으로 간주하지 않음 - **제외**: 현재 OpenAPI 또는 릴리스 범위에 포함하지 않으며, 다시 포함할 조건을 별도로 기록한 사항 ### 문서 유지보수 원칙 1. 요구사항이 변경되면 `prd.md`의 결정 기록, 기능 요구사항, OpenAPI 소비 주의사항, 미결·외부 의존 사항을 함께 갱신한다. 2. 구현 범위나 순서가 바뀌면 같은 디렉터리의 `plan-task.md`도 같은 변경에서 갱신한다. 3. endpoint, query, multipart part, request/response field, required 여부, status와 오류 응답은 OpenAPI 계약을 단일 진실 원천으로 사용한다. OpenAPI에 표현되지 않는 제품·UI·운영 정책은 이 문서가 소유한다. 4. 미결 사항과 외부 의존 계약은 추측으로 구현하지 않는다. **미결**에는 추천안을, **외부 의존**에는 제공 주체와 영향을 함께 기록한다. 5. 완료된 미결 사항과 제공 완료된 외부 의존 계약은 결정일과 결정 내용을 “결정 기록”에 추가한 뒤 관련 수용 기준까지 갱신한다. 6. goal 실행의 objective·순서·완료 증거·범위는 `plan-task.md`의 `Phase Goal`과 `Goal 실행`을 기준으로 한다. goal 수행 중 제품 결정이 바뀌면 이 문서의 결정 기록과 요구사항을 먼저 갱신한다. --- ## 1. Overview AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘텐츠·시리즈·커뮤니티·FanTalk·댓글 활동을 수행하도록 관리하는 독립 관리자 웹을 만든다. 관리자는 ADMIN 권한으로 로그인한 뒤 AI 캐릭터를 선택한다. 이후의 모든 생성·수정·비활성화 작업은 선택한 `characterId`와 연결된 AI 캐릭터 크리에이터의 활동으로 저장된다. 관리자가 AI 캐릭터 계정으로 직접 로그인하거나 토큰을 교환하는 방식은 사용하지 않는다. 이번 문서는 요구사항, 구현 계획, 완료된 후속 계약 반영 상태를 정의한다. ## 2. Problem Statement 현재 AI 캐릭터는 연결된 `creator(memberKind = AI_CHARACTER)`를 가지지만, 운영자가 캐릭터의 전체 활동을 한곳에서 관리할 독립 UI가 없다. 운영자는 다음 문제를 해결해야 한다. - 캐릭터와 연결된 creator의 관계를 이해하지 않아도 안전하게 캐릭터를 관리해야 한다. - 여러 캐릭터의 리소스가 섞이지 않도록 선택한 캐릭터 문맥 안에서만 작업해야 한다. - 오디오 콘텐츠를 관리자 화면에서 즉시 재생해 검수해야 한다. - 예약 공개, 시리즈 연결과 순서, 게시글 고정, FanTalk 단일 답변 같은 도메인 규칙을 UI에서 명확히 안내해야 한다. - 데스크톱에서는 전체 운영을 수행하고 모바일에서는 조회와 긴급 응대가 가능해야 한다. - 정식 OpenAPI 계약과 기존 PRD·구현 계획의 잘못되었거나 누락된 API 전제를 구현 전에 바로잡아야 한다. ## 3. Goals ### 3.1 제품 목표 - AI 캐릭터 목록 검색, 상세 조회, 생성, 수정, 비활성화를 제공한다. - 선택한 캐릭터 문맥에서 오디오 콘텐츠, 시리즈, 커뮤니티 게시글을 관리한다. - 선택한 캐릭터로 FanTalk에 한 번 답변하고 기존 답변을 수정할 수 있게 한다. - 오디오 콘텐츠와 커뮤니티 첨부 오디오를 관리자 화면에서 재생할 수 있게 한다. - 오디오 콘텐츠와 커뮤니티 게시글의 댓글·답글을 캐릭터 명의로 관리한다. - 비활성 리소스와 권한 오류를 안전하게 처리하고 의도하지 않은 변경을 방지한다. ### 3.2 UX 목표 - 캐릭터 선택 이후 모든 화면에서 현재 대상 캐릭터를 분명히 표시한다. - 데이터가 많은 운영 화면을 조밀하지만 빠르게 탐색할 수 있게 한다. - 업로드, 저장, 비활성화, 연결 해제 등 비동기 작업의 상태와 결과를 즉시 피드백한다. - 데스크톱·태블릿에서는 전체 기능을, 모바일에서는 합의된 조회·응대 기능을 제공한다. - 기본적인 키보드 탐색, 포커스 표시, 입력 레이블, 오류 연결을 보장한다. ## 4. Non-Goals - 일반 사용자 또는 사람 크리에이터를 관리하는 기능 - 관리자 계정을 AI 캐릭터 계정으로 전환하거나 AI 캐릭터 JWT를 발급하는 기능 - refresh token 또는 자동 access token 갱신 - 비활성 리소스 복원 - hard delete - FanTalk 답변 삭제 또는 두 번째 답변 추가 - WAV 업로드 - 오디오 최대 재생 길이 제한 - 모바일에서 캐릭터·오디오·시리즈·커뮤니티 리소스 생성/수정/비활성화, 파일 업로드, 시리즈 연결/순서 변경 - 다국어 UI - 초기 릴리스의 다크 모드, 테마 전환 버튼과 시스템 색상 테마 연동 - 정식 WCAG 2.2 AA 인증 또는 외부 접근성 감사 - 백엔드가 담당할 creator 생성·프로필 동기화의 조건부 정책 변경 - 비활성 ID에 대한 상세 GET 반환 여부와 오류 status 등 백엔드 조회 정책 결정 - 감사 로그 조회 UI - 실제 API의 404를 감지해 mock 응답으로 자동 전환하는 production fallback ## 5. Target Users ### 5.1 주 사용자 - AI 캐릭터와 해당 캐릭터의 콘텐츠를 운영하는 ADMIN - 콘텐츠 공개 상태와 오디오 품질을 확인하는 운영 담당자 - 모바일에서 댓글 또는 FanTalk에 긴급 응대하는 운영 담당자 ### 5.2 권한 - JWT claim의 role과 현재 DB role이 모두 ADMIN이어야 한다. - 비ADMIN JWT는 403으로 차단한다. - JWT claim은 ADMIN이지만 현재 DB role이 비ADMIN인 stale claim도 403으로 차단한다. - 관리자는 선택한 캐릭터의 creator 권한으로 리소스를 작성하지만 인증 주체는 계속 ADMIN이다. ## 6. 핵심 사용자 흐름 1. 관리자가 이메일과 비밀번호로 로그인한다. 2. AI 캐릭터 목록에서 이름으로 검색하고 캐릭터를 선택한다. 3. 캐릭터 워크스페이스의 기본 정보, 오디오, 시리즈, 커뮤니티, FanTalk 탭을 이동한다. 4. 활성 캐릭터라면 데스크톱·태블릿에서 리소스를 생성·수정·비활성화한다. 5. 오디오 플레이어로 캐릭터가 올린 오디오를 검수한다. 6. FanTalk 목록 item에서 답변이 없을 때 한 번 답변하고, 답변이 있으면 기존 답변을 수정한다. 7. 오디오 또는 커뮤니티 댓글에 캐릭터 명의로 댓글·답글을 작성하거나 운영 정책에 따라 삭제한다. 8. 모바일에서는 리소스를 조회하고 오디오를 재생하며 댓글과 FanTalk 답변을 관리한다. 9. JWT가 만료되거나 폐기되면 인증 정보를 지우고 로그인 화면으로 이동한다. ## 7. 정보 구조와 라우팅 ### 7.1 화면 구조 ```text /login /ai-characters /ai-characters/new /ai-characters/:characterId /profile /audio-contents /audio-contents/new /audio-contents/:contentId /audio-contents/:contentId/edit /series /series/new /series/:seriesId /series/:seriesId/edit /series/orders /community-posts /community-posts/new /fan-talks ``` 라우트 문자열은 구현 시 확정하되 다음 원칙은 고정한다. - 캐릭터를 선택하지 않은 전역 화면은 로그인과 캐릭터 목록·생성뿐이다. - 캐릭터 리소스 화면은 모두 URL에 `characterId`를 포함한다. - 계약이 제공하는 `searchTerm`, `search_word`, `page`, `size`와 제품 filter 상태는 URL query에 보존한다. 계약에 없는 server filter를 client 전체 결과 filter처럼 가장하지 않는다. - 상세 GET이 제공되는 주요 리소스의 목록과 상세 화면은 새로고침과 직접 링크 진입이 가능해야 한다. - 커뮤니티 게시글은 별도 상세·수정 route 없이 목록 행/카드에서 여는 Sheet를 사용한다. 페이지 새로고침은 목록을 다시 조회한다. - FanTalk도 별도 상세 GET이 없으므로 목록 item을 source로 Sheet 또는 panel을 열고 답변을 작성한다. - 존재하지 않거나 다른 캐릭터 소유인 하위 리소스는 서버 결과에 따라 오류 화면으로 처리한다. ### 7.2 캐릭터 워크스페이스 - 상단에 캐릭터 이미지, 이름, 활성 상태, `characterId`를 항상 표시한다. - 1차 탭은 기본 정보, 오디오, 시리즈, 커뮤니티, FanTalk로 구성한다. - 워크스페이스 진입·복원용 상세 성공 응답으로 `isActive=false`인 캐릭터 데이터를 받은 경우에는 읽기 전용 배너를 표시하고 모든 변경 진입점을 숨기거나 비활성화한다. soft delete mutation 성공 직후에는 이 규칙보다 `CHAR-014`의 목록 이동을 우선한다. 비활성 ID의 상세 GET 반환 여부는 프론트엔드가 규정하지 않는다. - 브레드크럼으로 캐릭터 목록과 현재 리소스 위치를 표시한다. ## 8. 기능 요구사항 ### 8.1 인증과 세션 | ID | 상태 | 요구사항 | |---|---|---| | AUTH-001 | 확정 | 독립 관리자 웹에 자체 로그인 화면을 제공한다. | | AUTH-002 | 확정 | 로그인 입력은 이메일과 비밀번호다. | | AUTH-003 | 확정 | ADMIN만 보호 라우트에 접근할 수 있다. | | AUTH-004 | 확정 | refresh token과 자동 갱신을 사용하지 않는다. | | AUTH-005 | 확정 | 401 수신 시 보관 중인 인증 상태를 제거하고 로그인으로 이동한다. | | AUTH-006 | 확정 | 403 수신 시 접근 거부 화면을 표시하며 권한이 필요한 작업을 실행하지 않는다. | | AUTH-007 | 확정 | 모든 API 요청에 `Accept-Language: ko`를 보낸다. | | AUTH-008 | 확정 | 로그인은 `POST /admin/member/login`에 email/password JSON body를 보낸다. | | AUTH-009 | 확정 | 로그인 성공 시 응답 `data.token`과 `data.role`을 받고 role은 `ADMIN`이어야 한다. | | AUTH-010 | 확정 | 보호 API와 로그아웃에는 `Authorization: Bearer {jwt-token}` header를 사용한다. | | AUTH-011 | 확정 | 관리자 전용 로그아웃 endpoint는 없으며 공통 `POST /member/logout`을 body 없이 호출한다. | | AUTH-012 | 확정 | 로그인 성공 시 JWT와 ADMIN role을 `sessionStorage`에만 저장한다. 같은 탭의 새로고침에서는 session을 복원하고 탭 종료 시 브라우저 동작에 따라 제거한다. `localStorage`, IndexedDB, cookie에는 저장하지 않는다. | | AUTH-013 | 확정 | `POST /member/logout`이 성공하거나 네트워크·비2xx 오류로 실패해도 프론트엔드는 `sessionStorage` 인증 정보를 제거하고 로그인 화면으로 이동한다. 실패 시 서버 로그아웃 확인 실패 경고를 표시하며 session을 복원하지 않는다. | ### 8.2 AI 캐릭터 | ID | 상태 | 요구사항 | |---|---|---| | CHAR-001 | 확정 | 이름 검색, 페이지네이션이 있는 캐릭터 목록을 제공한다. | | CHAR-002 | 확정 | 캐릭터 상세, 생성, 수정, 비활성화를 제공한다. | | CHAR-003 | 확정 | 생성 multipart는 필수 `image`와 필수 `request` part를 사용한다. `request`의 필수 입력은 `name`, `systemPrompt`, `description`이고 나머지 필드는 OpenAPI의 optional/nullable 정의를 따른다. | | CHAR-004 | 확정 | 생성 요청에는 `isActive`를 보내지 않는다. 최초 활성 상태는 백엔드가 결정한다. | | CHAR-005 | 확정 | `externalCharacterId`는 존재하지 않는 값이므로 모든 요청·응답·UI에서 제거한다. | | CHAR-006 | 확정 | 일반 수정 요청에서는 `isActive`를 생략하고 soft delete 요청에만 `isActive=false`를 보낸다. `isActive=true`는 전송하지 않는다. | | CHAR-007 | 확정 | 복원·hard delete는 제공하지 않는다. 워크스페이스 진입·복원용 상세 성공 응답으로 `isActive=false`인 캐릭터를 받은 경우 해당 workspace는 read-only로 처리한다. soft delete mutation 성공 직후에는 `CHAR-014`를 우선하며, 비활성 ID 상세 조회 정책은 백엔드 범위다. | | CHAR-008 | 확정 | 캐릭터 생성 시 연결된 `creator(memberKind = AI_CHARACTER)` 생성은 백엔드가 함께 수행한다. | | CHAR-009 | 확정 | 캐릭터 이름·설명·이미지 변경 시 creator의 nickname·introduce·profile image 동기화는 현재 백엔드가 수행한다. | | CHAR-010 | 확정 | creator 상황에 따른 조건부 생성·동기화는 다음 백엔드 범위이며 현재 UI 범위가 아니다. | | CHAR-011 | 제외 | 현 OpenAPI의 Character 목록·상세 응답에는 `creatorMemberId`, `creatorNickname`이 없다. 계약에 추가되기 전에는 creator 정보 UI와 DTO를 만들지 않는다. | | CHAR-012 | 확정 | 이름 검색 여부와 관계없이 캐릭터 목록 API는 `isActive=true`인 항목만 반환한다. 프론트엔드는 활성 상태 query나 client-side filter를 추가하지 않고 서버 반환값을 사용한다. | | CHAR-013 | 확정 | 원작 검색은 필수 `searchTerm` query를 사용하는 `GET /api/v2/admin/ai-characters/original-works/search`를 호출하고 `data[]`의 `id`, `title`, `contentType`, `category`, `isAdult`, `description`, `originalWork`, `originalLink`, `writer`, `studio`, `originalLinks`, `tags`, `imageUrl`을 사용한다. `originalWorkId` 미선택은 serializer의 canonical 생략으로 고정한다. | | CHAR-014 | 확정 | 캐릭터 soft delete 성공 시 캐릭터 목록으로 이동해 재조회하고 성공 알림을 표시한다. 현재 상세 화면에 머물지 않으며 서버의 active-only 목록에서 비활성 항목이 제외돼야 한다. | | CHAR-015 | 확정 | 목록은 `searchTerm`, `page`, `size`를 사용하고 `data.totalCount`, `data.content[]`를 소비한다. 목록 ID field는 `id`다. | | CHAR-016 | 확정 | 생성·수정 성공은 `data=null`이므로 생성 후 목록을 무효화해 이동하고, 수정 후 기존 `characterId` 상세와 목록을 다시 조회한다. 생성 응답에서 새 ID나 상세 DTO를 추정하지 않는다. | | CHAR-017 | 확정 | 상세의 `characterUUID`는 OpenAPI에 존재하는 읽기 전용 값이며, 제거된 `externalCharacterId`와 다른 field다. | | CHAR-018 | 확정 | 생성 request에는 `region`이 있지만 수정 request에는 없다. 수정 화면에서 region을 읽기 전용으로 표시하고 update payload에 보내지 않는다. | #### 캐릭터 생성·수정 폼 - 이름, system prompt와 설명은 명시적인 레이블과 필드 오류 영역을 가진다. - OpenAPI의 optional scalar(`age`, `gender`, `mbti`, `speechPattern`, `speechStyle`, `appearance`, `region`, `originalTitle`, `originalLink`, `characterType`)와 배열(`tags`, `hobbies`, `values`, `goals`, `relationships`, `personalities`, `backgrounds`, `memories`)을 생성 form에서 편집할 수 있게 한다. `originalWorkId`는 원작 검색 선택기로 편집하고, 수정 request에 없는 `region`은 수정 화면에서 읽기 전용이다. - 이미지는 미리보기, 교체, 새로 선택한 파일의 선택 취소를 제공한다. 기존 이미지 자체를 제거하는 기능은 contract가 없어 현재 범위가 아니다. - 생성 이미지는 필수다. 수정 이미지는 선택이며 미전송하면 기존 이미지를 유지한다. - 원작은 제목·콘텐츠 타입·카테고리 부분 검색을 지원하는 Combobox로 선택한다. 미선택은 허용하고 serializer는 `originalWorkId` key 생략을 canonical form으로 사용한다. - 비활성화는 폼 Switch가 아니라 영향 범위를 설명하는 확인 Dialog로 실행한다. - 저장 중 중복 제출을 막고 성공 후 상세 데이터를 다시 동기화한다. ### 8.3 오디오 콘텐츠 | ID | 상태 | 요구사항 | |---|---|---| | AUDIO-001 | 확정 | 선택한 캐릭터의 오디오 목록·제목 검색·상세·생성·수정·비활성화를 제공한다. | | AUDIO-002 | 제외 | 현 OpenAPI 목록·상세에는 `OPEN`, `SCHEDULED` status field가 없고 목록 status query도 없다. 상태 badge와 server status filter는 계약에 추가되기 전에는 제공하지 않는다. | | AUDIO-003 | 확정 | 공개 예약은 생성 request의 nullable `releaseDate`로 표현한다. 예약 값은 클라이언트가 UTC로 변환한 ISO-8601 `Z` 문자열이며 `timezone` field는 보내지 않는다. 목록·상세의 `releaseDate`도 UTC `Z` 문자열 또는 `null`로 소비하고 별도 status enum을 만들지 않는다. | | AUDIO-004 | 확정 | 프론트엔드는 `releaseDate`로 `OPEN`·`SCHEDULED` 같은 API status를 재계산하거나 DTO에 추가하지 않는다. | | AUDIO-005 | 제외 | 현 OpenAPI에 status filter가 없으므로 status query를 보내거나 현재 page를 client에서 status별로 거르지 않는다. | | AUDIO-006 | 확정 | 생성 요청에는 `isActive`를 보내지 않는다. | | AUDIO-007 | 확정 | 일반 수정 요청에서는 `isActive`를 생략하고 soft delete 요청에만 `isActive=false`를 보낸다. `isActive=true`는 전송하지 않는다. | | AUDIO-008 | 확정 | 공개 방식은 “지금 즉시 공개”와 “예약 공개” 두 선택 버튼으로 제공한다. | | AUDIO-009 | 확정 | 생성 시 즉시 공개가 기본값이며 `releaseDate=null`을 보내고 `timezone`은 보내지 않는다. 예약 날짜 입력은 비활성화하고 기존 값을 지운다. | | AUDIO-010 | 확정 | 예약 공개를 선택한 경우에만 날짜·시간을 입력할 수 있고 미래 시각이 필수다. | | AUDIO-011 | 확정 | 예약 시각은 Asia/Seoul로 입력·표시하되, 생성 API에는 해당 시각을 클라이언트에서 UTC로 변환한 ISO-8601 `Z` 형식의 `releaseDate`를 보낸다. 예를 들어 `2026-07-29 18:00` Asia/Seoul은 `2026-07-29T09:00:00Z`로 전송한다. | | AUDIO-012 | 확정 | 생성 multipart의 `contentFile`, `coverImage`, `request`는 필수다. 수정은 `coverImage`와 `request`만 허용하므로 오디오 원본 파일 교체 UI를 제공하지 않는다. | | AUDIO-013 | 확정 | 오디오 확장자는 `.mp3`, `.aac`, `.m4a`를 허용한다. WAV는 허용하지 않는다. | | AUDIO-014 | 확정 | canonical MIME은 `audio/mpeg`, `audio/aac`, `audio/mp4`다. | | AUDIO-015 | 확정 | 오디오 파일의 운영 기준 최대 크기는 1,024MB이고 최대 재생 길이는 제한하지 않는다. | | AUDIO-016 | 확정 | 확장자와 MIME만 신뢰하지 않고 실제 컨테이너·코덱 검증은 백엔드가 수행해야 한다. | | AUDIO-017 | 확정 | 업로드 진행률, 취소, 전체 재시도를 제공하고 resumable upload는 제공하지 않는다. | | AUDIO-018 | 확정 | 업로드 실패 후 입력한 폼 값과 선택 가능한 파일 상태를 최대한 유지한다. | | AUDIO-019 | 확정 | 가격 단위는 “캔”이고 `0..99999` 정수다. 0은 무료이며 UI는 예: `1,000캔`으로 표시한다. | | AUDIO-020 | 확정 | Audio 생성·수정 request에는 `seriesIds`가 없다. 시리즈 연결은 Audio form이 아니라 Series 콘텐츠 연결 endpoint와 Phase 5 UI에서 관리한다. | | AUDIO-021 | 확정 | 목록과 상세에서 오디오를 재생할 수 있다. | | AUDIO-022 | 확정 | 오디오 목록 API는 `isActive=true`인 항목만 반환한다. request에는 활성 상태 query를 추가하지 않고 응답에도 client-side 활성 filter를 적용하지 않는다. status query는 여전히 제공하지 않는다. | | AUDIO-023 | 확정 | 최대 크기는 decimal 1,024MB인 `1,024,000,000 bytes` 이하이며 `1,024,000,001 bytes`부터 거부한다. 오디오 콘텐츠와 커뮤니티 첨부 audio에 동일하게 적용한다. | | AUDIO-024 | 확정 | `audio/x-m4a`는 `.m4a` 파일에 한해 호환 MIME으로 허용한다. 실제 MP4/M4A 컨테이너·코덱 검증을 통과해야 하며 다른 확장자와의 조합은 거부한다. | | AUDIO-025 | 확정 | 오디오 soft delete 성공 시 선택 캐릭터의 오디오 목록으로 이동해 재조회하고 성공 알림을 표시한다. 현재 상세 화면에 머물지 않으며 서버의 active-only 목록에서 비활성 항목이 제외돼야 한다. | | AUDIO-026 | 확정 | 재생 오류를 signed URL 만료로 구분하거나 추정하지 않는다. media error만으로 상세·목록을 자동 재조회하거나 자동 재생하지 않고, 일반 오류와 수동 재시도·페이지 새로고침 안내를 제공한다. | | AUDIO-027 | 확정 | 오디오 콘텐츠 생성 시 유효한 `themeId`가 필수다. OpenAPI의 기본값 `0`은 binding 기본값일 뿐 domain에서 유효하지 않으므로 프론트엔드는 테마 선택을 강제한다. | | AUDIO-028 | 확정 | 오디오 콘텐츠 테마 목록 조회는 query/body 없이 호출하며 응답 `data[]`의 `id`, `theme`, `image`를 사용한다. | | AUDIO-029 | 확정 | 목록 검색 query는 `search_word`이며 검색어가 2자 이상일 때만 보낸다. 목록 응답은 `data.totalCount`, `data.items[]`를 사용한다. | | AUDIO-030 | 확정 | 상세 GET에는 `timezone` query를 보내지 않는다. 응답의 nullable `releaseDate`는 ISO-8601 UTC `Z` 값으로 소비하고 화면 표시 시 Asia/Seoul로 변환한다. | | AUDIO-031 | 확정 | 생성 성공은 `data.contentId`를 사용해 상세로 이동할 수 있다. 수정·soft delete 성공은 `data=null`이므로 기존 ID 기준 cache를 무효화한다. | | AUDIO-032 | 확정 | 생성 `request`는 필수 `title`, `detail`, `tags`, `price`와 OpenAPI의 optional field만 보낸다. 수정은 `title`, `detail`, `tags`, `price`, `isAdult`, `isActive`, `isPointAvailable`, `isCommentAvailable`만 변경할 수 있다. | | AUDIO-033 | 확정 | 생성 form은 OpenAPI의 `purchaseOption`, `limited`, `isAdult`, `isGeneratePreview`, `isOnlyRental`, `isPointAvailable`, `isCommentAvailable`, `isFullDetailVisible`, `previewStartTime`, `previewEndTime`, `languageCode`를 계약 enum·type과 default에 맞춰 제공한다. 계약에 없는 추가 상관관계 validation은 만들지 않는다. | | AUDIO-034 | 확정 | 공통 관리자 오디오 플레이어는 오디오 콘텐츠 목록·상세와 커뮤니티 목록·Sheet에 동일한 compact audio-only control bar를 사용한다. Plyr audio player를 1차 시각 기준, Media Chrome audio player를 control anatomy 기준으로 삼아 표준 viewport에서는 재생·진행·현재/전체 시간·배속·음량을 상시 텍스트 label 없이 한 줄에 표시한다. 별도 image·video 영역을 만들지 않고 기존 화면의 cover·게시물 media는 그대로 유지한다. | 현 수정 계약에는 `releaseDate`, `themeId`, `contentFile`이 없다. 따라서 공개 예약·테마·오디오 원본 변경은 생성 화면에서만 제공하고 수정 화면에서는 읽기 전용으로 표시한다. #### 관리자 오디오 플레이어 - 재생/일시정지, 탐색, 현재/전체 시간, 볼륨, 배속을 제공한다. - native `