feat(ai-character): 오디오 콘텐츠 폼 입력 UX 정렬

This commit is contained in:
Yu Sung
2026-08-04 12:20:04 +09:00
parent a213479e8e
commit b62c416fa1
13 changed files with 608 additions and 173 deletions

View File

@@ -6,7 +6,7 @@
|---|---|
| 문서 상태 | OpenAPI 반영 구현 기준 |
| 작성일 | 2026-07-25 |
| 최종 수정일 | 2026-07-30 |
| 최종 수정일 | 2026-08-04 |
| 대상 제품 | AI 캐릭터 전용 독립 관리자 웹 |
| 구현 대상 | React + TypeScript + Vite SPA |
| UI 기반 | Tailwind CSS + shadcn/ui |
@@ -223,9 +223,9 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
| 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-008 | 확정 | 공개 방식은 native radio를 사용하는 “지금 즉시 공개”와 “예약 공개” 선택 카드로 제공한다. radio와 checkbox는 같은 선택 카드 시각 문법을 사용한다. |
| AUDIO-009 | 확정 | 생성 시 즉시 공개가 기본값이며 `releaseDate=null`을 보내고 `timezone`은 보내지 않는다. 즉시 공개에서는 예약 일시 입력을 렌더링하지 않고 예약 값을 지운다. |
| AUDIO-010 | 확정 | 예약 공개를 선택한 경우에만 native `datetime-local` 입력을 렌더링하고 미래 시각 필수로 받는다. `showPicker()` 같은 custom picker 호출은 사용하지 않는다. |
| 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는 허용하지 않는다. |
@@ -234,7 +234,7 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
| AUDIO-016 | 확정 | 확장자와 MIME만 신뢰하지 않고 실제 컨테이너·코덱 검증은 백엔드가 수행해야 한다. |
| AUDIO-017 | 확정 | 업로드 진행률, 취소, 전체 재시도를 제공하고 resumable upload는 제공하지 않는다. |
| AUDIO-018 | 확정 | 업로드 실패 후 입력한 폼 값과 선택 가능한 파일 상태를 최대한 유지한다. |
| AUDIO-019 | 확정 | 가격 단위는 “캔”이고 `0..99999` 정수다. 0은 무료이며 UI는 예: `1,000캔`으로 표시한다. |
| AUDIO-019 | 확정 | 가격 단위는 “캔”이고 입력·저장 값은 숫자만 사용한다. 생성 기본값은 0이며 native number input은 `min=0`, `step=1`로 방향키 위/아래 증감을 제공한다. 유효 범위는 `0..99999` 정수이며 0은 무료다. payload에는 정수 price를 보낸다. |
| 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는 여전히 제공하지 않는다. |
@@ -248,7 +248,7 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
| 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-033 | 확정 | 태그는 chip으로 표시하며 Enter 또는 comma로 추가하고 각 chip의 remove button으로 삭제한다. request `tags` payload는 comma-separated string을 유지한다. 가격이 0보다 클 때만 `purchaseOption`, preview 생성 여부·시간, point 사용 가능 여부를 표시한다. 가격을 0으로 바꾸면 `purchaseOption=BOTH`, `isGeneratePreview=false`, `isPointAvailable=false`, `previewStartTime=null`, `previewEndTime=null`으로 즉시 초기화한다. preview 시작·종료는 시각이 아니라 오디오 duration 내 offset이며 preview 생성이 켜진 경우에만 text control로 완전한 `HH:MM:SS`를 입력한다. request는 입력한 `HH:mm:ss` 값을 그대로 보낸다. 문서화되지 않은 nullable `limited` UI, `languageCode` UI, 독립 `isOnlyRental` UI는 제공하지 않으며 각각 `limited=null`, `languageCode=null`, `isOnlyRental=false`를 계속 전송한다. `isAdult`, `isCommentAvailable`, `isFullDetailVisible`과 preview 생성 여부는 native checkbox 선택 카드로 제공한다. |
| 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`이 없다. 따라서 공개 예약·테마·오디오 원본 변경은 생성 화면에서만 제공하고 수정 화면에서는 읽기 전용으로 표시한다.
@@ -936,3 +936,4 @@ vertical slice와 남은 수동 QA에서 검증한다. `EXT-006`은 현재 확
| 2026-07-29 | 현재 FanTalk UI는 목록 item 기반 Sheet와 backend 반환 순서만 사용하므로 별도 상세 GET·답변 상태 filter·sort query가 필요하지 않다고 확정했다. 중복 생성 전용 오류 key 분기도 제외하고 일반 오류 후 목록을 재조회한다. 답변 1개 불변식은 `FANTALK-003`의 backend 수용 기준으로 server integration에서 검증하므로 `EXT-004`를 해결로 종결한다. |
| 2026-07-31 | 사용자 직접 지시에 따라 로컬 자동 Gate와 지원 browser 범위는 데스크톱 Chrome·모바일 Chrome의 Chrome 2종으로 한정한다. Playwright는 Chromium/mobile Chrome project만 유지하고, 현재 제품 지원 대상이 아닌 WebKit·Mobile Safari 자동 실행과 수동 QA는 테스트 시간을 크게 늘리므로 현재 릴리스 범위에서 제외한다. |
| 2026-08-03 | 공통 `AdminAudioPlayer`의 native media 동작과 단일 재생·오류 계약은 유지하고, Plyr·Media Chrome의 audio-only control 배치를 참고한 compact 가로형 UI로 개선한다. 플레이어 내부 image·video 영역과 새 외부 라이브러리·waveform은 추가하지 않으며 오디오 콘텐츠 목록·상세와 커뮤니티 목록·Sheet에 동일하게 적용한다. |
| 2026-08-04 | Audio form은 tags를 chip으로 입력하고 Enter/comma 추가와 remove button 삭제를 제공하되 payload는 comma-separated string으로 유지한다. 가격은 숫자만 표시·저장하는 `0..99999` 정수이며 0은 무료다. 가격이 0보다 클 때만 purchase option, preview 생성·시간, point 사용 가능 여부를 보이고, 0 전환 시 `purchaseOption=BOTH`, `isGeneratePreview=false`, `isPointAvailable=false`, preview times=`null`로 초기화한다. `limited`는 문서화되지 않은 NullableInt32이므로 UI를 제거하고 `null`을 보내며, `languageCode` UI는 제거하고 `null`, 독립 `isOnlyRental` UI는 제거하고 `false`를 보낸다. preview 시작·종료는 오디오 duration offset으로 text 입력하며 완전한 `HH:MM:SS`만 안내하고 request에는 입력한 `HH:mm:ss` 값을 그대로 보낸다. 예약 일시는 예약 공개에서만 native `datetime-local`로 렌더링하고 custom `showPicker()`는 사용하지 않는다. native radio/checkbox는 하나의 선택 카드 시각 문법을 사용한다. |