941 lines
99 KiB
Markdown
941 lines
99 KiB
Markdown
# AI 캐릭터 관리자 웹 PRD
|
||
|
||
## 문서 정보
|
||
|
||
| 항목 | 내용 |
|
||
|---|---|
|
||
| 문서 상태 | OpenAPI 반영 구현 기준 |
|
||
| 작성일 | 2026-07-25 |
|
||
| 최종 수정일 | 2026-08-04 |
|
||
| 대상 제품 | 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 | 확정 | 공개 방식은 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는 허용하지 않는다. |
|
||
| 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이며 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는 여전히 제공하지 않는다. |
|
||
| 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 | 확정 | 태그는 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`이 없다. 따라서 공개 예약·테마·오디오 원본 변경은 생성 화면에서만 제공하고 수정 화면에서는 읽기 전용으로 표시한다.
|
||
|
||
#### 관리자 오디오 플레이어
|
||
|
||
- 재생/일시정지, 탐색, 현재/전체 시간, 볼륨, 배속을 제공한다.
|
||
- native `<audio>`와 현재 재생 상태·오류 처리 로직은 유지하고, [Plyr audio](https://plyr.io/#audio)의 밝은 compact bar를 1차 시각 기준, [Media Chrome audio](https://www.media-chrome.org/docs/en/audio-player)의 명시적 시간·배속 anatomy를 보조 기준으로 사용한다.
|
||
- 표준 viewport의 기본 배치는 원형 재생 버튼, 유동형 재생 위치 slider, 현재/전체 시간, compact 배속 select, 음량 icon과 slider 순서의 단일 control bar다. `볼륨`, `재생 속도` 같은 설명 label은 화면에 상시 노출하지 않고 accessible name으로 제공한다.
|
||
- 200% zoom처럼 실제 player 폭이 control 최소 폭보다 작을 때만 secondary control을 다음 줄로 보내며, control이 잘리거나 가로 overflow가 생기지 않아야 한다.
|
||
- 플레이어 내부에는 cover image, poster, video viewport를 표시하지 않는다. 오디오 콘텐츠 cover와 게시물 media는 기존 화면 영역에서만 표시한다.
|
||
- 새 audio player library, waveform, playlist와 download control은 추가하지 않는다.
|
||
- 명시적인 다운로드 버튼은 제공하지 않는다.
|
||
- 여러 행의 플레이어가 동시에 재생되지 않게 현재 재생 항목을 단일화한다.
|
||
- 재생 오류는 원인을 signed URL 만료로 구분하지 않고 “오디오를 재생할 수 없습니다”와 수동 재시도·페이지 새로고침 안내를 표시한다.
|
||
- media error 자체로 상세·목록 API를 자동 재조회하거나 `play()`를 자동 재호출하지 않는다.
|
||
- signed URL은 로그, 분석 이벤트, 영구 저장소에 기록하지 않는다.
|
||
- 커뮤니티 첨부 오디오는 URL 갱신만을 목적으로 요청하지 않는다. 사용자가 페이지를 새로고침하거나 mutation 후 cache 무효화 등 일반 목록 lifecycle로 재조회된 경우 새 응답의 URL을 사용한다. 별도 상세 조회나 URL 재발급 호출을 추가하지 않는다.
|
||
|
||
### 8.4 시리즈
|
||
|
||
| ID | 상태 | 요구사항 |
|
||
|---|---|---|
|
||
| SERIES-001 | 확정 | 목록·상세·생성·수정·비활성화, 콘텐츠 연결·해제, 시리즈 순서 변경을 제공한다. |
|
||
| SERIES-002 | 확정 | 상태 enum은 `PROCEEDING`(연재중), `SUSPEND`(휴재중), `COMPLETE`(완결)이다. `OPEN`은 유효하지 않다. |
|
||
| SERIES-003 | 확정 | 생성 시 state를 선택하거나 보내지 않는다. 성공 응답은 `data=null`이므로 생성 후 목록으로 이동해 서버가 결정한 state를 다시 조회한다. |
|
||
| SERIES-004 | 확정 | 수정 시 state를 선택할 수 있으며 선택하지 않으면 필드를 생략해 이전 상태를 유지한다. |
|
||
| SERIES-005 | 확정 | 연재 요일 enum은 `SUN`, `MON`, `TUE`, `WED`, `THU`, `FRI`, `SAT`, `RANDOM`이다. |
|
||
| SERIES-006 | 확정 | `RANDOM`은 다른 요일과 함께 보낼 수 없다. 값은 RANDOM 단독 또는 하나 이상의 실제 요일 목록이어야 한다. |
|
||
| SERIES-007 | 확정 | 생성 시 유효한 `genreId`가 필요하고 OpenAPI 기본값 `0`은 domain에서 유효하지 않다. 장르 선택지는 query/body 없는 `GET /api/v2/admin/ai-characters/series-genres`의 `data[]`에서 `id`, `genre`, `isAdult`를 사용한다. |
|
||
| SERIES-008 | 확정 | `data.totalCount`, `data.items[]`를 page별로 읽어 서버가 반환한 시리즈 전체를 별도 순서 변경 모드에 표시하고 최종 순서의 모든 ID를 `{ "ids": [...] }`로 전송한다. active-only 여부는 `SERIES-012` 계약 제공 후 검증한다. |
|
||
| SERIES-009 | 확정 | drag-and-drop 외에 키보드와 위/아래 버튼으로 순서를 바꿀 수 있어야 한다. |
|
||
| SERIES-010 | 확정 | 일반 수정 요청에서는 `isActive`를 생략하고 soft delete 요청에만 `isActive=false`를 보낸다. `isActive=true`, 복원과 hard delete는 제공하지 않는다. |
|
||
| SERIES-011 | 확정 | 장르 목록 endpoint는 검색·페이지 query 없이 활성 장르 전체를 반환한다. 생성 초기 state의 정확한 기본값은 프론트엔드 의존사항이 아니다. |
|
||
| SERIES-012 | 확정 | 시리즈 목록 API는 `isActive=true`인 항목만 반환한다. 프론트엔드는 활성 상태 query나 client-side filter를 추가하지 않는다. |
|
||
| SERIES-013 | 확정 | 시리즈 soft delete 성공 시 선택 캐릭터의 시리즈 목록으로 이동해 재조회하고 성공 알림을 표시한다. 현재 상세 화면에 머물지 않으며 서버의 active-only 목록에서 비활성 항목이 제외돼야 한다. |
|
||
| SERIES-014 | 확정 | 생성 multipart의 `image`와 `request`는 필수다. 생성 화면은 image 입력을 폼 최상단에 두고 keyword를 Enter/comma로 확정·삭제하는 chip UI로 제공한다. 생성 request는 chip 값을 comma-separated `keyword` 단일 문자열로 보내며 `keywords` 배열을 보내지 않는다. |
|
||
| SERIES-015 | 확정 | 목록과 상세는 동일한 `SeriesListItem` schema를 사용하고 `genreId`, enum 배열 `publishedDaysOfWeek`, enum `state`를 반환한다. 화면 label은 클라이언트에서 표시용으로 변환하되 원본 enum과 ID를 수정 payload에 사용한다. |
|
||
| SERIES-016 | 확정 | 연결 후보는 `GET .../contents/search?search_word=...`, 연결은 `{ "contentIdList": [...] }`, 해제는 body 없는 DELETE를 사용한다. |
|
||
| SERIES-017 | 확정 | 시리즈 상세 응답은 목록 item과 동일한 수정용 원본값 `genreId`, enum `publishedDaysOfWeek`, enum `state`를 제공하므로 별도 edit DTO가 필요 없다. 수정 화면은 상세 응답으로 기존 선택값을 초기화하고, 장르 API는 option 목록 표시용으로 호출한다. |
|
||
| SERIES-018 | 확정 | 생성 request의 `keyword`는 수정 request와 상세 응답에 없다. 수정 화면에서 keyword 편집·표시를 추가하거나 update payload에 보내지 않는다. |
|
||
|
||
#### 시리즈 콘텐츠 연결
|
||
|
||
- 현재 연결 콘텐츠는 `page`, `size`로 조회한다. 현 계약에는 연결 목록 검색 query가 없다.
|
||
- 연결 후보는 `.../contents/search?search_word=...`의 반환값을 사용하며 선택 캐릭터·해당 시리즈 문맥을 벗어난 별도 Audio 후보 endpoint를 만들지 않는다.
|
||
- 이미 연결된 콘텐츠를 중복 연결하지 않는다.
|
||
- 연결 해제 전 대상 제목과 영향을 확인한다.
|
||
- 연결/해제 성공 후 시리즈 상세와 콘텐츠 목록을 함께 갱신한다.
|
||
|
||
### 8.5 커뮤니티 게시글
|
||
|
||
| ID | 상태 | 요구사항 |
|
||
|---|---|---|
|
||
| COMMUNITY-001 | 확정 | 선택 캐릭터의 게시글 목록 기반 조회·등록·수정·고정·비활성화를 제공한다. |
|
||
| COMMUNITY-002 | 확정 | 생성 요청에는 `isActive`를 보내지 않는다. |
|
||
| COMMUNITY-003 | 확정 | 이미지와 오디오 파일은 선택 첨부다. |
|
||
| COMMUNITY-004 | 확정 | 첨부 오디오가 있으면 목록 행/카드와 게시글 Sheet에서 재생할 수 있다. |
|
||
| COMMUNITY-005 | 확정 | 일반 수정 요청에서는 `isActive`를 생략하고 soft delete 요청에만 `isActive=false`를 보낸다. `isActive=true`는 전송하지 않는다. |
|
||
| COMMUNITY-006 | 확정 | soft delete request에는 `isActive=false`와 `isFixed=false`를 함께 보낸다. 현 목록 응답에는 `fixedAtUtc`가 없으므로 해당 field를 DTO·UI에 만들지 않는다. |
|
||
| COMMUNITY-007 | 확정 | 가격은 오디오와 동일하게 `0..99999` 정수 “캔” 단위를 사용한다. |
|
||
| COMMUNITY-008 | 확정 | 커뮤니티 목록 API는 `isActive=true`인 게시글만 반환한다. request에는 활성 상태 query를 추가하지 않고 item에도 client-side 활성 filter를 적용하지 않는다. |
|
||
| COMMUNITY-009 | 확정 | 커뮤니티 전용 상세 GET과 상세·수정 직접 route를 추가하지 않는다. 목록 응답으로 행/카드의 Sheet를 열어 조회·수정·고정·비활성화·댓글 진입을 제공한다. |
|
||
| COMMUNITY-010 | 확정 | 커뮤니티 게시글 soft delete 성공 시 열린 Sheet를 닫고 목록을 무효화·재조회하며 성공 알림을 표시한다. 서버의 active-only 목록에서 해당 게시글이 제외돼야 한다. |
|
||
| COMMUNITY-011 | 확정 | 첨부 audio URL 갱신만을 위한 자동 요청은 하지 않으며 media error도 refetch trigger로 사용하지 않는다. 사용자 페이지 새로고침이나 mutation 후 cache 무효화 등 일반 목록 재조회가 발생하면 새 응답의 `audioUrl`을 사용한다. |
|
||
| COMMUNITY-012 | 확정 | 목록 GET은 `timezone` 없이 `page`, `size`를 사용하고 `data.totalCount`, `data.page`, `data.size`, `data.hasNext`, `data.items[]`를 소비한다. 전체 건수와 다음 page 여부는 서버 metadata를 그대로 사용한다. |
|
||
| COMMUNITY-013 | 확정 | 생성 multipart는 optional `audioFile`, optional `postImage`, 필수 `request`를 사용하고 request에 필수 `content`, `isCommentAvailable`, `isAdult`와 optional `price`만 보낸다. |
|
||
| COMMUNITY-014 | 확정 | 수정 multipart는 optional `postImage`와 필수 `request`만 허용한다. 수정에서 가격·첨부 audio 교체는 제공하지 않고, 고정은 `isFixed`, soft delete는 `isActive=false`로 처리한다. |
|
||
| COMMUNITY-015 | 확정 | 생성·수정·고정·soft delete 성공은 `data=null`이므로 목록을 무효화·재조회하고 mutation 응답에 게시글 DTO가 있다고 가정하지 않는다. |
|
||
|
||
### 8.6 FanTalk
|
||
|
||
| ID | 상태 | 요구사항 |
|
||
|---|---|---|
|
||
| FANTALK-001 | 확정 | 기본 목록은 backend가 반환한 순서를 유지한다. 현 계약에 sort query가 없으므로 client가 page 사이의 최신순을 재정렬하지 않는다. |
|
||
| FANTALK-002 | 제외 | 현재 UI에는 전체·미답변·답변 완료 filter control을 제공하지 않는다. 현재 page만 client에서 거르는 불완전한 filter도 만들지 않는다. 후속 제품 범위에서 전체 결과 filter가 필요해지면 server query 계약과 함께 별도 요구사항으로 다시 포함한다. |
|
||
| FANTALK-003 | 확정 | 하나의 FanTalk에는 답변을 한 번만 작성할 수 있다. |
|
||
| FANTALK-004 | 확정 | `creatorReplies`가 비어 있으면 답변 작성 UI를, 비어 있지 않으면 기존 답변 수정 UI를 제공한다. 수정은 `PUT /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies/{replyId}`에 `{ "content": string }`만 보내며 path의 `replyId`에는 `creatorReplies[].fanTalkId`를 사용한다. 백엔드 구현 완료 확인에 따라 OpenAPI operation도 `implemented`로 정정했으므로 mock/client와 실제 server integration을 모두 구현·검증한다. |
|
||
| FANTALK-005 | 확정 | 답변 삭제는 현재 범위가 아니다. |
|
||
| FANTALK-006 | 확정 | 데스크톱·태블릿·모바일 모두 목록 조회, 답변 작성과 기존 답변 수정을 지원한다. |
|
||
| FANTALK-007 | 확정 | 별도 상세 GET·직접 route, 답변 상태 filter, sort control은 현재 UI 범위가 아니다. 목록 item을 source로 Sheet/panel을 열고 backend 반환 순서를 유지하므로 추가 network 계약 없이 목록·작성·수정·팬 원글 삭제를 구현한다. |
|
||
| FANTALK-008 | 제외 | 중복 생성의 정확한 비2xx status/message key를 사용하는 도메인별 오류 분기는 만들지 않는다. 답변 1개 불변식 자체는 `FANTALK-003`의 backend 수용 기준이며, client는 한 화면의 중복 submit을 막고 일반 오류 후 현재 목록을 재조회한다. 동시 POST 후에도 답변이 하나인지 실제 server integration에서 검증하며 위반 시 backend 결함으로 기록한다. |
|
||
| FANTALK-009 | 확정 | 목록은 `page`, `size`를 사용하고 `data.fanTalkCount`, `data.fanTalks`, `data.page`, `data.size`, `data.hasNext`를 소비한다. `creatorReplies.length === 0`이면 미답변, 하나 이상이면 답변 완료로 판단하며 기존 답변 수정 시 첫 답변의 `fanTalkId`를 `replyId`로 사용한다. |
|
||
| FANTALK-010 | 확정 | 답변 작성은 `{ "content": string }`을 보내고 성공 응답의 `fanTalkId`, `replyId`, `creatorMemberId`, `content`, `createdAtUtc`를 사용한다. |
|
||
| FANTALK-011 | 확정 | 별도 상세 endpoint가 없으므로 목록 item을 source로 collection Sheet 또는 panel을 열며 `/fan-talks/:fanTalkId` 직접 route를 만들지 않는다. |
|
||
| FANTALK-012 | 확정 | 관리자는 팬 작성 FanTalk 원글을 `DELETE /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}`로 soft delete할 수 있다. 성공 후 현재 목록을 재조회하고 해당 원글을 닫으며, 연결된 creator reply를 별도 삭제했다고 가정하지 않는다. |
|
||
|
||
### 8.7 댓글과 답글
|
||
|
||
| ID | 상태 | 요구사항 |
|
||
|---|---|---|
|
||
| COMMENT-001 | 확정 | 오디오 콘텐츠와 커뮤니티 게시글에 댓글 영역을 제공한다. |
|
||
| COMMENT-002 | 확정 | 구조는 루트 댓글과 그 댓글의 직접 답글까지 정확히 2단계다. 답글의 답글은 허용하지 않는다. |
|
||
| COMMENT-003 | 확정 | AI 캐릭터는 루트 댓글과 답글을 작성하고 자신이 작성한 내용을 수정할 수 있다. 댓글의 `writerId`와 대상 오디오·커뮤니티 게시글의 `creatorId`가 같으면 AI 캐릭터 작성으로 판단한다. |
|
||
| COMMENT-004 | 확정 | `writerId !== creatorId`인 팬 작성 루트 댓글과 답글에는 수정 UI를 제공하지 않는다. 관리자는 작성자와 관계없이 운영 목적으로 해당 row만 soft delete할 수 있으며 하위 답글을 함께 삭제했다고 가정하지 않는다. |
|
||
| COMMENT-005 | 확정 | 모바일에서도 조회·작성·수정·soft delete를 모두 지원한다. |
|
||
| COMMENT-006 | 확정 | 오디오 콘텐츠와 커뮤니티 게시글은 각각 루트 댓글 목록 GET, 댓글·답글 POST, AI 캐릭터 작성 댓글 PUT, 작성자 무관 DELETE, 루트별 답글 목록 GET을 제공한다. 모든 mutation 성공 `data=null`을 처리하고 목록·답글 cache를 재조회한다. |
|
||
| COMMENT-007 | 확정 | 루트 댓글은 `.../comments`, 직접 답글은 `.../comments/{commentId}/replies`에서 조회한다. 작성 POST의 `parentId`를 생략하거나 `null`로 보내면 루트, 같은 target의 활성 루트 ID를 보내면 직접 답글이며 답글의 답글은 UI에서 허용하지 않는다. |
|
||
| COMMENT-008 | 확정 | 댓글 날짜는 ISO-8601 UTC `Z` 값으로 소비해 Asia/Seoul로 표시한다. 오디오 댓글 작성 request는 `comment`, optional `parentId`, `isSecret`, `languageCode`를 사용하고 커뮤니티 댓글 작성 request는 `comment`, optional `parentId`, `isSecret`을 사용한다. 수정 request는 공통 `{ "comment": string }`이다. |
|
||
|
||
### 8.8 공통 파일 정책
|
||
|
||
| ID | 상태 | 요구사항 |
|
||
|---|---|---|
|
||
| FILE-001 | 확정 | 캐릭터·오디오 cover·시리즈·커뮤니티 image의 최대 크기는 `10,485,760 bytes` 이하다. `10,485,761 bytes`부터 거부한다. |
|
||
| FILE-002 | 확정 | 기본 image 형식은 JPEG(`.jpg`/`.jpeg`, `image/jpeg`)와 PNG(`.png`, `image/png`)다. WebP 등 다른 형식은 허용하지 않는다. |
|
||
| FILE-003 | 확정 | GIF(`.gif`, `image/gif`)는 커뮤니티 image에서만 허용한다. 캐릭터·시리즈·오디오 cover에서는 거부한다. |
|
||
| FILE-004 | 확정 | 커뮤니티 JPEG/PNG image는 자유 aspect ratio로 크롭하며 결과의 최대 가로 폭은 800px, 세로는 선택한 crop ratio에 따라 결정한다. |
|
||
| FILE-005 | 확정 | 시리즈 image는 `210:297` 세로형 고정 aspect ratio로 크롭하며 결과의 최대 가로 폭은 1,000px다. |
|
||
| FILE-006 | 확정 | 오디오 콘텐츠 cover는 `1:1` 고정 aspect ratio로 크롭하며 결과의 최대 가로 폭은 800px다. |
|
||
| FILE-007 | 확정 | 커뮤니티 JPEG/PNG·시리즈·오디오 콘텐츠에서 새 image를 선택하면 업로드 전에 crop UI를 반드시 거친다. 커뮤니티 GIF는 예외다. |
|
||
| FILE-008 | 확정 | crop UI는 이동, 확대/축소, 초기화, 결과 미리보기, 취소, 적용을 제공한다. drag/pinch만 강제하지 않고 키보드와 버튼 대안을 제공한다. |
|
||
| FILE-009 | 확정 | optional 교체 파일 미전송은 기존 media 유지다. crop 취소도 기존 media를 변경하지 않는다. 기존 media 자체 제거는 별도 remove contract가 없어 범위 밖이다. |
|
||
| FILE-010 | 확정 | 캐릭터 image는 JPEG/PNG만 허용하고 `1:1` 고정 aspect ratio로 크롭하며 결과의 최대 가로·세로는 800px다. |
|
||
| FILE-011 | 확정 | 커뮤니티 GIF는 crop하지 않는다. crop Dialog를 열지 않고 원본 비율과 animation을 유지한 File을 등록한다. |
|
||
| FILE-012 | 확정 | JPEG/PNG crop 결과는 선택된 원본 crop 영역의 pixel 크기보다 확대하지 않는다. resource별 800px/1,000px 값은 최대 출력 폭이며 작은 원본은 가능한 원본 크기로 출력한다. |
|
||
| FILE-013 | 확정 | 커뮤니티 첨부 audio는 오디오 콘텐츠와 동일하게 MP3(`.mp3`, `audio/mpeg`), AAC(`.aac`, `audio/aac`), M4A(`.m4a`, `audio/mp4` 또는 `audio/x-m4a`), 최대 `1,024,000,000 bytes`, 재생 길이 제한 없음 정책을 사용하고 WAV는 거부한다. `audio/x-m4a`는 `.m4a`와 실제 container/codec 검증이 일치할 때만 허용한다. |
|
||
| FILE-014 | 확정 | 커뮤니티 GIF의 원본 가로가 800px을 초과하면 등록을 거부한다. client에서 제출 전에 차단하고 server도 같은 제한을 검증한다. GIF를 축소·crop·재인코딩하지 않는다. |
|
||
| FILE-015 | 확정 | Series crop 결과의 세로 pixel은 `round(width × 297 ÷ 210)`으로 계산한다. 최대 폭에서는 1,000×1,414px이며 비율 검증은 계산된 세로값 기준 1px 이내 오차를 허용한다. |
|
||
|
||
### 8.9 개발 전용 Mock Preview
|
||
|
||
| ID | 상태 | 요구사항 |
|
||
|---|---|---|
|
||
| MOCK-001 | 확정 | 개발 환경은 명시적인 `server`와 `mock` API mode를 제공한다. 기본 `npm run dev`는 실제 개발 API를 사용하고 `npm run dev:mock`만 browser MSW를 활성화한다. |
|
||
| MOCK-002 | 확정 | mock mode도 production과 같은 Page, Query, API client, endpoint path, request serializer와 response DTO를 사용한다. 별도 화면이나 mock 전용 API adapter를 만들지 않는다. |
|
||
| MOCK-003 | 확정 | 실제 API의 404·network error를 감지해 mock으로 자동 fallback하지 않는다. `server` mode의 오류는 실제 오류 UI로 처리한다. |
|
||
| MOCK-004 | 확정 | production build에서는 mock mode를 거부하고 browser worker·fixture가 활성화되지 않는다. |
|
||
| MOCK-005 | 확정 | 계약이 제공됐지만 backend endpoint가 아직 구현되지 않은 기능은 정규화 API Contract 기반 fixture와 browser handler로 최종 UI의 happy path를 확인할 수 있다. |
|
||
| MOCK-006 | 확정 | endpoint·DTO·오류 계약 자체가 미제공인 기능은 fixture를 추정하지 않는다. 계약과 무관한 shell·상태 inventory만 구현하고 최종 network UI 완료를 주장하지 않는다. |
|
||
| MOCK-007 | 확정 | 각 도메인 mock은 deterministic seed와 새로고침 시 초기화되는 in-memory store를 사용해 목록·상세·생성·수정·soft delete의 연결된 흐름을 재현한다. |
|
||
| MOCK-008 | 확정 | mock mode 화면에는 실제 서버가 아니라는 지속적으로 보이는 안내를 제공하고, JWT·password·signed URL·업로드 파일 본문을 log나 영구 저장소에 기록하지 않는다. |
|
||
| MOCK-009 | 확정 | 도메인 완료 상태는 `UI 확인 완료(mock)`와 `실제 서버 연동 완료(server)`를 분리한다. mock Gate만 통과한 경우 Phase 전체와 network integration을 완료로 표시하지 않는다. |
|
||
|
||
“가로 800/1,000”은 이 문서에서 등록 결과의 **최대 출력 폭**으로 해석한다. JPEG/PNG crop 결과에는 이 제한을 적용하되 선택한 원본 crop 영역이 더 작으면 확대하지 않는다. 커뮤니티 GIF는 원본 가로가 800px 이하일 때만 등록할 수 있다.
|
||
|
||
#### Image crop UI 흐름
|
||
|
||
1. 새 image 선택 직후 resource별 MIME과 `10,485,760 bytes` 이하 제한을 먼저 검증한다.
|
||
2. 커뮤니티 GIF이면 원본 가로를 검사한다. 800px 초과는 inline 오류로 차단하고, 800px 이하는 crop Dialog 없이 원본 비율과 animation을 유지한다.
|
||
3. 캐릭터 image, 커뮤니티 JPEG/PNG, 시리즈 image, 오디오 콘텐츠 cover이면 crop Dialog를 열고 resource별 자유/고정 aspect ratio를 적용한다.
|
||
4. crop Dialog에는 현재 crop 영역과 예상 결과 크기를 표시한다.
|
||
5. “적용” 시 crop 결과 File과 미리보기를 form에 반영하고, “취소” 시 새 선택과 crop 결과를 버려 기존 image 상태를 유지한다.
|
||
6. form 제출 시 crop 대상은 적용된 결과 File을, 커뮤니티 GIF는 crop하지 않은 File을 기존 multipart image part로 보낸다.
|
||
|
||
커뮤니티 GIF는 canvas crop이나 resize 대상이 아니다. client 검증을 우회한 요청도 server가 원본 가로 800px 제한을 다시 검증하고 거부한다.
|
||
|
||
## 9. 반응형 기능 범위
|
||
|
||
| 기능 | 데스크톱 | 태블릿 | 모바일 |
|
||
|---|---:|---:|---:|
|
||
| 로그인·로그아웃 | 전체 | 전체 | 전체 |
|
||
| 캐릭터 목록·검색·상세 조회 | 전체 | 전체 | 조회 |
|
||
| 캐릭터 생성·수정·비활성화 | 전체 | 전체 | 미지원 |
|
||
| 오디오 목록·상세·재생 | 전체 | 전체 | 전체 |
|
||
| 오디오 생성·수정·비활성화·업로드 | 전체 | 전체 | 미지원 |
|
||
| 시리즈 목록·상세·연결 콘텐츠 조회 | 전체 | 전체 | 조회 |
|
||
| 시리즈 생성·수정·비활성화·연결·순서 | 전체 | 전체 | 미지원 |
|
||
| 커뮤니티 목록·게시글 Sheet·오디오 재생 | 전체 | 전체 | 전체 |
|
||
| 커뮤니티 등록·수정·고정·비활성화 | 전체 | 전체 | 미지원 |
|
||
| 댓글·답글 관리 | 전체 | 전체 | 전체 |
|
||
| FanTalk 조회·답변 작성 | 전체 | 전체 | 전체 |
|
||
| FanTalk 답변 수정·팬 원글 soft delete | 전체 | 전체 | 전체 |
|
||
|
||
모바일에서 미지원인 기능은 좁은 화면에 데스크톱 폼을 억지로 노출하지 않는다. 화면에는 읽기 전용임을 알리고 전체 관리가 필요하면 데스크톱·태블릿 사용을 안내한다.
|
||
|
||
## 10. UI/UX Expectations
|
||
|
||
### 10.1 디자인 방향
|
||
|
||
`ui-ux-pro-max` 검색과 shadcn/ui 패턴을 바탕으로 다음을 초기 설계 기준으로 사용한다. `#00BDF7` main color는 고정하고, 보조 치수와 supporting semantic color는 접근성·실화면 검증에서 같은 역할과 대비를 유지하는 범위 안에서 조정할 수 있다.
|
||
|
||
| ID | 상태 | 요구사항 |
|
||
|---|---|---|
|
||
| UX-001 | 확정 | 초기 릴리스는 밝은 테마만 제공한다. 다크 모드, 테마 전환 버튼과 `prefers-color-scheme: dark` 연동은 후속 범위다. |
|
||
| UX-002 | 확정 | 브랜드 main color와 primary 기준색은 `#00BDF7`이다. 전체 밝은 테마 palette는 이 cyan 계열과 조화되는 semantic token으로 구성하며, raw hex를 feature component에서 직접 사용하지 않는다. |
|
||
|
||
| 항목 | 초기 권고 기준 |
|
||
|---|---|
|
||
| 스타일 | Data-Dense Dashboard + Trust & Authority |
|
||
| 정보 밀도 | 9/10, 조밀하지만 행·필드 그룹은 명확히 구분 |
|
||
| 시각적 변주 | 2/10, 장식보다 상태와 계층 중심 |
|
||
| 모션 | 2/10, 150–200ms의 짧은 상태 전환만 사용 |
|
||
| 기본 모드 | 밝은 테마만 제공 |
|
||
| 기본 간격 | 8px 배수 |
|
||
| 데스크톱 내비게이션 | 약 240px sidebar + 56px header |
|
||
| 표 행 높이 | 최소 44px, 헤더 고정 가능 |
|
||
| 아이콘 | shadcn/ui와 일관된 Lucide 아이콘, emoji 아이콘 금지 |
|
||
|
||
검색 결과에 포함된 landing page 중심의 과장된 대형 타이포그래피, glassmorphism, 지속 애니메이션은 운영 관리자 화면과 맞지 않아 적용하지 않는다.
|
||
|
||
### 10.2 색상과 토큰
|
||
|
||
기능 코드에서 `cyan-600`이나 raw hex처럼 용도를 알 수 없는 값을 직접 반복하지 않는다. primitive → semantic → component의 3단계 token을 사용하고 shadcn/ui CSS variable과 Tailwind semantic color에 연결한다.
|
||
|
||
#### Primary primitive scale
|
||
|
||
`brand-500`은 사용자가 지정한 값을 변경하지 않는다. 옅은 단계는 배경·선택 상태, 짙은 단계는 링크·focus·정보 상태에 사용한다.
|
||
|
||
| Token | 값 | 대표 용도 |
|
||
|---|---|---|
|
||
| `brand-50` | `#F0FBFF` | 아주 옅은 강조 배경 |
|
||
| `brand-100` | `#D9F6FF` | 선택 행, accent 배경 |
|
||
| `brand-200` | `#B5EEFF` | 강조 border |
|
||
| `brand-300` | `#7CE2FF` | 비활성 장식 강조 |
|
||
| `brand-400` | `#36D1FF` | 보조 시각 강조 |
|
||
| `brand-500` | `#00BDF7` | main color, primary 기본 배경 |
|
||
| `brand-600` | `#00A9DE` | primary hover |
|
||
| `brand-700` | `#009DCE` | primary active |
|
||
| `brand-800` | `#007EA8` | 링크, focus ring, info |
|
||
| `brand-900` | `#086789` | 링크 hover |
|
||
| `brand-950` | `#063747` | 가장 짙은 브랜드 강조 |
|
||
|
||
#### 밝은 테마 semantic palette
|
||
|
||
| Semantic token | 기준색 | 사용 |
|
||
|---|---|---|
|
||
| `background` | `#F6FBFD` | 페이지 배경 |
|
||
| `card` / `popover` | `#FFFFFF` | 카드, 표, 폼, overlay surface |
|
||
| `foreground` | `#102A33` | 본문과 제목 |
|
||
| `muted` | `#E9F4F7` | 비강조 surface |
|
||
| `muted-foreground` | `#425F69` | 보조 정보 |
|
||
| `secondary` | `#E1F5FA` | 보조 버튼과 선택 전 control |
|
||
| `secondary-foreground` | `#123E4B` | secondary 위 텍스트 |
|
||
| `accent` | `#D9F6FF` | 선택 행, hover surface |
|
||
| `accent-foreground` | `#0C566F` | accent 위 텍스트 |
|
||
| `border` | `#D5E8EE` | 표 구분선 등 장식 경계 |
|
||
| `input` | `#577581` | 입력과 필수 조작 경계 |
|
||
| `primary` | `#00BDF7` | 주요 CTA와 브랜드 강조 |
|
||
| `primary-hover` | `#00A9DE` | 주요 CTA hover |
|
||
| `primary-active` | `#009DCE` | 주요 CTA pressed/active |
|
||
| `primary-foreground` | `#062B36` | primary 위 텍스트·아이콘 |
|
||
| `link` / `ring` / `info` | `#007EA8` | 흰 배경 링크, focus indicator, 정보 상태 |
|
||
| `link-hover` | `#086789` | 링크 hover |
|
||
| `success` / `OPEN` | `#167347` | 현재 공개, 성공 |
|
||
| `success-surface` | `#EAF8F0` | 성공 Badge·Alert 배경 |
|
||
| `warning` / `SCHEDULED` | `#9A5B00` | 예약 공개, 주의 |
|
||
| `warning-surface` | `#FFF7E6` | 경고 Badge·Alert 배경 |
|
||
| `destructive` | `#B42318` | 비활성화, 실패 |
|
||
| `destructive-surface` | `#FEF0EE` | 오류 Badge·Alert 배경 |
|
||
| `inactive` | `#52636A` | 비활성 상태 |
|
||
| `inactive-surface` | `#EEF3F5` | 비활성 Badge 배경 |
|
||
|
||
- 상태는 색상만으로 전달하지 않고 Badge 텍스트와 아이콘 또는 보조 문구를 함께 사용한다.
|
||
- 일반 텍스트 대비는 4.5:1, 정보를 이해하는 데 필요한 control boundary와 focus indicator는 3:1을 목표로 검증한다. 장식용 divider는 정보 전달 수단으로 사용하지 않는다.
|
||
- `#00BDF7` 위에는 흰색을 사용하지 않고 `primary-foreground=#062B36`을 사용한다. 이 조합은 약 6.84:1이며, `#00BDF7`과 흰색의 약 2.18:1 조합은 텍스트용으로 금지한다.
|
||
- 흰 배경 위 텍스트 링크와 focus ring에는 `brand-500`이 아니라 `brand-800=#007EA8`을 사용한다. `#007EA8`과 흰색은 약 4.62:1이다.
|
||
- 반복되는 상태색은 `success`, `warning`, `destructive`, `inactive` token으로 정의한다.
|
||
- 초기 릴리스에서는 밝은 테마용 semantic token만 구현한다. 다크 token override, ThemeProvider와 테마 전환 control을 만들지 않으며 시스템이 dark mode여도 밝은 테마를 유지한다.
|
||
|
||
### 10.3 타이포그래피
|
||
|
||
- 한국어 가독성을 위해 `Pretendard`, `Noto Sans KR`, `Apple SD Gothic Neo`, `system-ui` 순의 sans-serif stack을 사용한다.
|
||
- 원격 font가 초기 렌더링을 막지 않게 system fallback을 항상 둔다.
|
||
- 페이지 제목 24px, 섹션 제목 18–20px, 본문·표 14px, 보조 정보 12–13px를 기본으로 한다.
|
||
- iOS 입력 확대를 막기 위해 모바일 입력 요소의 실제 font-size는 16px 이상으로 한다.
|
||
- heading에 mono font나 landing page용 48px 이상 크기를 사용하지 않는다.
|
||
|
||
### 10.4 shadcn/ui 구성요소 매핑
|
||
|
||
| UI 목적 | 기본 구성요소 |
|
||
|---|---|
|
||
| 앱 구조 | Sidebar, Sheet, Breadcrumb, Tabs, Separator, ScrollArea |
|
||
| 목록 | Table/Data Table, Card, Badge, Pagination, DropdownMenu |
|
||
| 검색·선택 | Input, Select, Command + Popover Combobox |
|
||
| 폼 | Form, Label, Input, Textarea, Checkbox, RadioGroup, Calendar, Popover |
|
||
| 상태·피드백 | Alert, Skeleton, Progress, Sonner/Toast |
|
||
| 확인·편집 | Dialog, AlertDialog, Drawer |
|
||
| 파일 | Input 기반 공통 FileField + Dialog 기반 ImageCropDialog |
|
||
| 정렬 | 접근 가능한 SortableList와 위/아래 Button |
|
||
| 미디어 | native audio를 감싼 공통 AdminAudioPlayer |
|
||
|
||
- 비활성화처럼 영향이 큰 동작은 Switch가 아니라 AlertDialog를 사용한다.
|
||
- icon-only 버튼에는 `aria-label`과 Tooltip을 제공한다.
|
||
- 모바일의 보조 작업은 DropdownMenu 또는 Drawer에 배치하되 핵심 답변·댓글 동작은 한 번에 찾을 수 있어야 한다.
|
||
- crop Dialog는 pointer drag와 pinch/zoom을 지원하되 이동·확대·축소·초기화를 실행하는 명시적 버튼과 keyboard 조작도 제공한다.
|
||
- crop frame, preview, 적용/취소 control은 tablet touch target 44×44px 이상과 보이는 label 또는 accessible name을 가진다.
|
||
|
||
### 10.5 화면 상태
|
||
|
||
모든 목록과 상세 화면은 다음 상태를 명시적으로 가진다.
|
||
|
||
- 첫 로딩: 레이아웃과 유사한 Skeleton
|
||
- 백그라운드 갱신: 기존 데이터를 유지하고 작은 진행 표시
|
||
- 빈 결과: 현재 검색·필터를 설명하고 초기화 동작 제공
|
||
- 오류: 서버의 한국어 message, 재시도, 필요한 경우 목록으로 이동
|
||
- 저장 중: 제출 버튼 비활성화와 진행 표시
|
||
- 저장 성공: toast와 최신 서버 응답 반영
|
||
- soft delete 성공: 해당 resource 목록을 무효화·재조회하고 필요한 화면 이동과 성공 toast 표시. 서버 active-only 목록에서 비활성 항목 제외를 확인하며 client filter로 보정하지 않음
|
||
- 필드 오류: 로컬 validation은 해당 필드 아래에 표시하고 `aria-invalid`, 오류와 입력 연결, 첫 오류 focus를 제공한다. 서버 오류는 `errorProperty`가 실제 필드명을 제공하는 계약일 때만 inline으로 연결한다.
|
||
- 업로드: 파일별 진행률, 취소, 재시도
|
||
|
||
300ms 이상 걸릴 수 있는 작업에는 시각적 피드백을 제공하며, 연속 장식 애니메이션은 사용하지 않는다.
|
||
|
||
### 10.6 반응형 원칙
|
||
|
||
- Tailwind의 mobile-first breakpoints를 사용하고 한 요소에 2–3개를 넘는 불필요한 breakpoint를 피한다.
|
||
- 320, 640, 768, 1024, 1280px와 가로 방향을 검증한다.
|
||
- 데스크톱 표는 모바일에서 핵심 필드 중심 Card 목록으로 전환한다. 단순 horizontal scroll만으로 핵심 동작을 숨기지 않는다.
|
||
- 모바일 터치 target은 최소 44×44px이다.
|
||
- 좁은 화면에서 가로 넘침, 잘린 dialog, keyboard에 가려진 답변 입력이 없어야 한다.
|
||
- 데스크톱 sidebar는 모바일에서 Sheet 내비게이션으로 바뀐다.
|
||
|
||
### 10.7 접근성 최소 기준
|
||
|
||
- semantic HTML과 올바른 button/link를 사용한다.
|
||
- “본문으로 건너뛰기” 링크와 `main` landmark를 제공한다.
|
||
- 모든 입력에 보이는 Label을 제공하고 placeholder를 Label 대신 사용하지 않는다.
|
||
- 키보드만으로 메뉴, 탭, 표 행 동작, dialog, 정렬, 답변 작성이 가능해야 한다.
|
||
- focus indicator를 제거하지 않는다.
|
||
- dialog focus trap, 닫힌 후 trigger로 focus 복귀를 검증한다.
|
||
- 비동기 결과와 오류는 적절한 live region 또는 shadcn toast로 전달한다.
|
||
- `prefers-reduced-motion`을 존중한다.
|
||
- 200% browser zoom에서도 핵심 기능을 사용할 수 있어야 한다.
|
||
|
||
### 10.8 z-index와 overlay
|
||
|
||
임의의 `z-[9999]`를 사용하지 않고 semantic scale을 둔다.
|
||
|
||
| 단계 | 값 | 예 |
|
||
|---|---:|---|
|
||
| sticky | 10 | table header |
|
||
| navigation | 20 | header, sidebar |
|
||
| popover | 30 | select, dropdown, date picker |
|
||
| overlay | 40 | sheet/dialog backdrop |
|
||
| modal/notification | 50 | dialog content, toast |
|
||
|
||
Radix Portal과 stacking context를 함께 검증해 Popover가 Dialog 뒤로 숨지 않게 한다.
|
||
|
||
### 10.9 ui-ux-pro-max 사용 의무
|
||
|
||
UI 구현자는 화면 생성 전에 저장소의 `ui-ux-pro-max` design-system 검색을 실행하고 결과를 이 문서의 디자인 방향과 대조한다.
|
||
|
||
```bash
|
||
python3 .codex/skills/ui-ux-pro-max/scripts/search.py \
|
||
"enterprise internal admin console data tables forms file upload operational dashboard neutral compact" \
|
||
--design-system --variance 2 --motion 2 --density 9 \
|
||
-p "AI Character Admin" -f markdown
|
||
```
|
||
|
||
구현 중에는 필요한 domain 검색을 수행하고, 화면 검증 전에 다음 UX 검색을 다시 실행한다.
|
||
|
||
```bash
|
||
python3 .codex/skills/ui-ux-pro-max/scripts/search.py \
|
||
"animation accessibility z-index loading" --domain ux -n 12
|
||
```
|
||
|
||
검색 결과가 관리자 제품의 목적과 충돌하면 그대로 적용하지 않고, 채택·제외 이유를 PR 또는 작업 기록에 남긴다.
|
||
|
||
## 11. API 계약
|
||
|
||
현재 기준은 OpenAPI `3.1.0`, 문서 version `2.3.0`인
|
||
`api-contract.openapi.json`의 25개 path·37개 operation이다.
|
||
OpenAPI에 아직 포함되지 않은 기존 로그인·로그아웃은 `EXT-006 현재 구현
|
||
기준 계약`에 별도로 기록하며, 정식 OpenAPI가 제공될 때까지 구현과 회귀
|
||
검증의 임시 기준으로 사용한다.
|
||
|
||
### 11.1 공통 응답
|
||
|
||
`/api/v2/admin/ai-characters` 아래 OpenAPI operation의 성공 응답은
|
||
`success`, `message`, `data`, `errorProperty`를 사용한다.
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": null,
|
||
"data": {},
|
||
"errorProperty": null
|
||
}
|
||
```
|
||
|
||
mutation에 따라 `data`는 상세 DTO, ID DTO 또는 `null`이다. 각 operation의
|
||
response schema를 따르며 공통 client가 임의의 상세 응답으로 정규화하지
|
||
않는다.
|
||
|
||
오류는 의미에 맞는 비2xx status와 다음 envelope를 사용한다.
|
||
|
||
```json
|
||
{
|
||
"success": false,
|
||
"message": "잘못된 요청입니다.",
|
||
"data": null,
|
||
"errorProperty": null
|
||
}
|
||
```
|
||
|
||
- 오류를 2xx로 normalize하지 않는다.
|
||
- UI는 서버가 반환한 현지화된 한국어 `message`를 우선 사용한다.
|
||
- OpenAPI의 `Accept-Language`는 optional이고 기본값은 `ko`다. `ko|en|ja` 이외 값과 header 누락은 KO로 fallback하며, 프론트엔드는 일관되게 `ko`를 보낸다.
|
||
- OpenAPI operation은 전역 `bearerAuth`를 사용한다. 로그인 이외의 관리자 API에는 `Authorization: Bearer {jwt-token}`을 보낸다.
|
||
- 공통 `page`는 기본 `0`, 최소 `0`이고 공통 `size`는 기본 `20`, 최소 `1`이다. 전역 최대 `50`은 없다. FanTalk `size`만 설명에 따라 `20..50`으로 보정된다.
|
||
- AI 캐릭터 관리자 API의 날짜·시간 request/response는 OpenAPI가 별도로 명시한 nullable 조건을 제외하고 ISO-8601 UTC `Z`를 사용하며 `timezone` query/body field를 보내지 않는다.
|
||
- `characterId`는 선택된 대상 캐릭터가 필요한 하위 resource endpoint에만 사용한다.
|
||
- 캐릭터 목록·검색과 캐릭터 생성에는 path `characterId`가 없다.
|
||
- `/admin/member/login`, `/member/logout`은 현 OpenAPI 범위 밖의 기존 인증 계약이다. 두 endpoint의 현재 구현 기준은 `11.5 EXT-006`에 기록하고 Phase 1 구현과 회귀 검증을 유지하되, 정식 OpenAPI operation으로 표기하지 않는다.
|
||
|
||
### 11.2 오류 처리 매핑
|
||
|
||
| HTTP | 의미 | UI 처리 |
|
||
|---:|---|---|
|
||
| 400 | binding, target 미존재, creator 불변식 등 invalid request | 로컬 검증은 inline, 서버 `errorProperty`가 필드명을 제공할 때만 inline, 그 외 화면 Alert |
|
||
| 401 | JWT 없음·잘못됨·만료·폐기 | 인증 제거 후 로그인 이동 |
|
||
| 403 | 비ADMIN 또는 stale ADMIN claim | 접근 거부 화면 |
|
||
| 404 | path 또는 target 미존재 | 찾을 수 없음과 가능한 목록 이동 |
|
||
| 405 | 지원하지 않는 method | 서버 message와 재시도 불가 안내 |
|
||
| 406 | 허용되지 않는 표현 또는 응답 조건 | 서버 message와 요청 조건 확인 안내 |
|
||
| 415 | 지원하지 않는 media type | 파일 필드 오류와 허용 형식 안내 |
|
||
| 500 | 예상하지 못한 오류 | 일반 오류, request ID가 있으면 함께 표시, 재시도 |
|
||
|
||
OpenAPI는 공통 status와 `ApiErrorResponse` shape만 제공하고 도메인별 정확한
|
||
message key를 열거하지 않는다. 특정 message key 분기가 필요한 기능은
|
||
구현 전에 backend 계약을 추가로 받아야 하며, 그전에는 status와
|
||
`errorProperty`가 제공된 경우만 공통 처리한다.
|
||
|
||
### 11.3 제공된 endpoint 목록
|
||
|
||
AI 캐릭터 관리자 domain의 query, multipart part, request/response field와
|
||
오류 응답은 [api-contract.openapi.json](./api-contract.openapi.json)을
|
||
기준으로 한다. 인증 두 건은 현재 구현·test 기준을 백엔드 정식화 입력으로
|
||
함께 표시한 것이며 현 OpenAPI operation에는 포함되지 않는다.
|
||
|
||
| 영역 | Method | Path | 계약 상태 |
|
||
|---|---|---|---|
|
||
| 인증 | POST | `/admin/member/login` | 현재 구현·test 기준, OpenAPI 미포함(`EXT-006`) |
|
||
| 인증 | POST | `/member/logout` | 현재 구현·test 기준, OpenAPI 미포함(`EXT-006`) |
|
||
| 캐릭터 | GET, POST | `/api/v2/admin/ai-characters` | 제공됨 |
|
||
| 원작 검색 | GET | `/api/v2/admin/ai-characters/original-works/search` | 제공됨, 필수 `searchTerm` |
|
||
| 캐릭터 | GET, PUT | `/api/v2/admin/ai-characters/{characterId}` | 제공됨 |
|
||
| 오디오 테마 | GET | `/api/v2/admin/ai-characters/audio-content-themes` | 제공됨, query/body 없음 |
|
||
| 오디오 | GET, POST | `/api/v2/admin/ai-characters/{characterId}/audio-contents` | 제공됨 |
|
||
| 오디오 | GET, PUT | `.../audio-contents/{contentId}` | 제공됨 |
|
||
| 오디오 댓글 | GET, POST | `.../audio-contents/{contentId}/comments` | 제공됨 |
|
||
| 오디오 댓글 | PUT, DELETE | `.../audio-contents/{contentId}/comments/{commentId}` | 제공됨 |
|
||
| 오디오 댓글 답글 | GET | `.../audio-contents/{contentId}/comments/{commentId}/replies` | 제공됨 |
|
||
| 시리즈 장르 | GET | `/api/v2/admin/ai-characters/series-genres` | 제공됨, query/body 없음 |
|
||
| 시리즈 | GET, POST | `/api/v2/admin/ai-characters/{characterId}/series` | 제공됨 |
|
||
| 시리즈 순서 | PUT | `.../series/orders` | 제공됨 |
|
||
| 시리즈 | GET, PUT | `.../series/{seriesId}` | 제공됨 |
|
||
| 시리즈 콘텐츠 | GET, POST | `.../series/{seriesId}/contents` | 제공됨 |
|
||
| 시리즈 연결 후보 | GET | `.../series/{seriesId}/contents/search` | 제공됨 |
|
||
| 시리즈 콘텐츠 | DELETE | `.../series/{seriesId}/contents/{contentId}` | 제공됨 |
|
||
| 커뮤니티 | GET, POST | `.../{characterId}/community-posts` | 제공됨 |
|
||
| 커뮤니티 | PUT | `.../community-posts/{postId}` | 제공됨 |
|
||
| 커뮤니티 댓글 | GET, POST | `.../community-posts/{postId}/comments` | 제공됨 |
|
||
| 커뮤니티 댓글 | PUT, DELETE | `.../community-posts/{postId}/comments/{commentId}` | 제공됨 |
|
||
| 커뮤니티 댓글 답글 | GET | `.../community-posts/{postId}/comments/{commentId}/replies` | 제공됨 |
|
||
| FanTalk 목록 | GET | `.../{characterId}/fan-talks` | 제공됨 |
|
||
| FanTalk 팬 원글 | DELETE | `.../{characterId}/fan-talks/{fanTalkId}` | 제공됨 |
|
||
| FanTalk 답변 | POST | `.../{characterId}/fan-talks/{fanTalkId}/replies` | 제공됨 |
|
||
| FanTalk 답변 | PUT | `.../{characterId}/fan-talks/{fanTalkId}/replies/{replyId}` | 제공됨, backend 구현 완료 |
|
||
|
||
### 11.4 OpenAPI 소비 시 주의사항
|
||
|
||
아래 표는 삭제된 Markdown 계약을 기준으로 작성된 기존 문서·구현 계획을
|
||
OpenAPI로 전환할 때 반드시 반영할 차이다. 제품 정책이 OpenAPI payload에
|
||
없는 field를 추가하는 근거로 사용돼서는 안 된다.
|
||
|
||
| 영역 | OpenAPI 계약 | 프론트엔드 처리 |
|
||
|---|---|---|
|
||
| Character 목록 | query `searchTerm`; `data={totalCount,content}`; item ID `id` | `search`·`items`·`hasNext`로 바꾸지 않는다. |
|
||
| Character 원작 검색 | 필수 `searchTerm`; `data`는 `OriginalWorkSearchItem[]` | legacy lookup을 호출하지 않고 반환 `id`를 `originalWorkId`로 사용한다. |
|
||
| Character 생성 | 필수 multipart `image`, `request`; request의 필수 `name`, `systemPrompt`, `description`; 성공 `data=null` | 생성 후 목록을 재조회하고 새 ID를 응답에서 추정하지 않는다. |
|
||
| Character 상세 | `characterUUID`, `originalWork`를 포함하고 creator field는 없음 | `characterUUID`를 `externalCharacterId`로 취급하지 않고 creator UI는 만들지 않는다. |
|
||
| Audio 목록 | query `search_word`; `data={totalCount,items}`; status query/field 없음 | 2자 이상 제목 검색만 보내고 status filter·badge를 만들지 않는다. |
|
||
| Audio 테마 | `data=[{id,theme,image}]` | `themeId`·`themeName`·`imageUrl`로 역직렬화하지 않는다. |
|
||
| Audio 생성 | multipart `contentFile`, `coverImage`, `request`; `releaseDate`는 nullable ISO-8601 UTC `Z`; `timezone` field 없음; 성공 `data.contentId` | 예약 입력을 client에서 UTC로 변환하고 `audioFile`, `releaseDateUtc`, `timezone`, `seriesIds`를 보내지 않는다. |
|
||
| Audio 상세 | `timezone` query 없음; nullable `releaseDate`는 ISO-8601 UTC `Z` | UTC 값을 Asia/Seoul 표시로 변환하되 API status를 재계산하지 않는다. |
|
||
| Audio 수정 | optional `coverImage`와 제한된 request field만 제공 | content file, 공개 예약, theme, series 연결 수정 UI를 제공하지 않는다. |
|
||
| 가격 | Audio 생성·수정과 Community 생성 request의 `price`는 `0..99999` 정수 | `-1`, `100000`, 소수는 제출 전에 거부하고 `0`, `99999`는 허용한다. |
|
||
| Series 생성 | 필수 `image`; request의 `keyword`는 문자열; 성공 `data=null` | `keywords` 배열을 보내지 않고 목록으로 이동해 재조회한다. |
|
||
| Series 장르 | query/body 없는 활성 장르 목록; `data=[{id,genre,isAdult}]` | `id`를 `genreId` 선택값으로 사용하고 `0`을 유효값으로 허용하지 않는다. |
|
||
| Series 상세 | 목록과 같은 `SeriesListItem`; `genreId`, enum `publishedDaysOfWeek`, enum `state` | 직접 수정 form을 원본값으로 초기화하고 create-only `keyword`를 상세·수정 field로 만들지 않는다. |
|
||
| Series 연결·순서 | 후보 `contents/search`; 연결 `{contentIdList}`; 순서 `{ids}` | `{contentIds}`, `{seriesIds}`를 보내지 않는다. |
|
||
| Community 목록 | `timezone` query 없음; `data={totalCount,page,size,hasNext,items}` | `audioUrl`과 서버 pagination metadata를 그대로 사용한다. |
|
||
| Community 생성·수정 | 생성 part `postImage`/`audioFile`; 수정은 `postImage`만 교체 가능; mutation `data=null` | `image`, `audioSignedUrl`, 수정 audio/price field를 만들지 않는다. |
|
||
| FanTalk | 목록 GET, 답변 POST, 팬 원글 DELETE, 답변 PUT이 모두 구현됨 | `creatorReplies`의 빈 배열 여부로 작성/수정을 나누고 `creatorReplies[].fanTalkId`를 PUT의 `replyId`로 사용한다. 별도 상세·filter/sort와 중복 오류 key 분기는 현재 UI 범위에서 제외한다. |
|
||
| 댓글 | Audio·Community 각각 루트 목록/작성, AI 작성 댓글 수정, 작성자 무관 soft delete, 루트별 답글 목록 제공 | `writerId === creatorId`일 때만 수정 UI를 노출하고 삭제는 작성자와 관계없이 제공한다. |
|
||
|
||
### 11.5 백엔드 계약 제공·대기 상태
|
||
|
||
아래 항목은 프론트엔드 인터뷰로 결정할 Open Question이 아니라 backend
|
||
계약의 제공·해결 상태를 추적한다. `해결`된 항목은 완료된 Phase를
|
||
묵시적으로 다시 열지 않고 `plan-task.md`의 완료된 Phase 10 후속
|
||
vertical slice와 남은 수동 QA에서 검증한다. `EXT-006`은 현재 확정 기능 구현을 차단하지
|
||
않으며 정식 OpenAPI 추적성만 후속으로 관리한다.
|
||
|
||
| ID | 우선순위 | 백엔드 제공 필요 계약 | 사용하는 화면 | 관련 API 흐름 | 현재 프론트엔드 영향 |
|
||
|---|---:|---|---|---|---|
|
||
| EXT-001 | 해결 | `GET /api/v2/admin/ai-characters/original-works/search?searchTerm=...`와 `OriginalWorkSearchItem[]`가 OpenAPI에 `implemented`로 추가됐다. | 캐릭터 생성 `/ai-characters/new`, 캐릭터 수정 `/ai-characters/:characterId/profile` | 원작 검색 GET과 Character create/update의 `originalWorkId` | legacy 후보 대신 v2 lookup을 사용하는 선택기·contract test를 Phase 10에서 구현 완료했다. |
|
||
| EXT-002 | 해결 | query/body 없는 `GET /api/v2/admin/ai-characters/series-genres`와 `{id,genre,isAdult}[]`가 OpenAPI에 `implemented`로 추가됐다. | 시리즈 생성 `/ai-characters/:characterId/series/new`, 수정 `/ai-characters/:characterId/series/:seriesId/edit` | 장르 목록 GET과 Series create/update의 `genreId` | 장르 선택과 Series 생성·수정 후속 구현을 Phase 10에서 완료했다. |
|
||
| EXT-003 | 필요 없음 | 별도 Series 수정 DTO 또는 표시 문자열 mapping 계약 | 시리즈 수정 `/ai-characters/:characterId/series/:seriesId/edit`, 시리즈 상세 직접 진입 | `GET /api/v2/admin/ai-characters/{characterId}/series/{seriesId}`가 `SeriesListItem`과 같은 `genreId`, enum `publishedDaysOfWeek`, enum `state`를 내려주면 충분하다. | 별도 외부 의존으로 추적하지 않는다. 수정 화면은 상세 응답으로 기존 값을 초기화하고 장르 API는 option 목록 표시용으로만 사용한다. |
|
||
| EXT-004 | 해결 | 답변 유무는 `creatorReplies`의 빈 배열 여부로 판정한다. 답변 PUT과 팬 원글 DELETE가 `implemented`이며 PUT path의 `replyId`는 `creatorReplies[].fanTalkId`를 사용한다. 별도 상세·filter·sort와 중복 오류 key는 현재 UI에 필요하지 않아 `FANTALK-002`, `FANTALK-008`에서 제외했다. | FanTalk 목록 `/ai-characters/:characterId/fan-talks`, FanTalk item Sheet/panel | 목록 GET, 답변 POST, 팬 원글 DELETE, 답변 PUT을 사용한다. 목록 item이 Sheet source이고 backend 반환 순서를 유지하므로 별도 상세 GET·filter·sort query를 요청하지 않는다. | Phase 10에서 답변 수정·팬 원글 삭제의 mock/client integration을 구현 완료했다. 실제 개발 API 수동 QA에서 동시 POST 후 활성 답변 1개 유지 등 server 수용 기준을 분리 확인한다. |
|
||
| EXT-005 | 해결 | Audio·Community 각각 루트 목록/작성, AI 작성 댓글 수정, 작성자 무관 soft delete, 루트별 답글 목록 operation과 DTO가 OpenAPI에 `implemented`로 추가됐다. `writerId === creatorId`이면 AI 작성 댓글로 판정한다. | 오디오 상세 `/ai-characters/:characterId/audio-contents/:contentId`, 커뮤니티 Sheet `/ai-characters/:characterId/community-posts` | 두 target의 `comments`, `comments/{commentId}`, `comments/{commentId}/replies` | Phase 10에서 2단계 thread, 작성·수정·삭제, Comments mock/client integration을 구현 완료했다. 팬 작성 댓글은 수정하지 않고 관리자 DELETE만 제공한다. |
|
||
| EXT-006 | P0 비차단 | 현재 구현된 `POST /admin/member/login`, `POST /member/logout`의 정식 OpenAPI 포함 또는 별도 버전 고정 계약 | 로그인 `/login`, 보호 shell 로그아웃 | `POST /admin/member/login`, `POST /member/logout` | 현재 구현과 Phase 1 회귀는 유지한다. 정식 OpenAPI 추적성만 대기하며 Phase 3~9를 차단하지 않는다. |
|
||
| EXT-007 | 해결 | Character 검색 결과와 Audio·Series·Community 목록 API는 `isActive=true`인 항목만 반환한다. | 캐릭터 목록, 오디오 목록, 시리즈 목록, 커뮤니티 목록 | 각 목록 GET: `/ai-characters`, `/audio-contents`, `/series`, `/community-posts` | 프론트엔드 변경 없음. 활성 query·client-side filter를 만들지 않고 soft delete 후 목록 재조회 결과를 server integration에서 확인한다. |
|
||
| EXT-008 | 해결 | Community 목록 응답이 `data={totalCount,page,size,hasNext,items}`로 보완됐다. | 커뮤니티 목록 `/ai-characters/:characterId/community-posts` | `GET /api/v2/admin/ai-characters/{characterId}/community-posts` | Phase 10에서 기존 배열 parser와 `timezone` query 제거, 서버 pagination metadata 기반 UI·test 갱신을 완료했다. |
|
||
| EXT-009 | 해결 | price 최대값은 `99999` 캔이며 관련 OpenAPI request schema에도 `minimum=0`, `maximum=99999`를 명시했다. | 오디오 생성·수정, 커뮤니티 생성 | Audio `POST/PUT .../audio-contents`, Community `POST .../community-posts` | Phase 10에서 schema·form과 경계값 test를 `0..99999` 정수로 갱신 완료했다. |
|
||
| EXT-010 | 해결 | 파일 정책은 `FILE-001~015`로 확정됐다. 이미지 비율·crop 결과는 client가 보장하고 backend는 검증하지 않는다. 파일 용량, MIME 등 서버에서 확인 가능한 항목은 backend가 동일하게 검증한다. | 캐릭터 생성·수정, 오디오 생성·수정, 시리즈 생성, 커뮤니티 생성·수정 | 각 multipart 생성·수정 API의 image/audio file part | 외부 의존에서 제외한다. client는 기존 사전 검증을 유지하고, server integration에서는 용량·MIME reject만 확인한다. |
|
||
| EXT-011 | 해결 | OpenAPI에 명시된 공통 오류 envelope/status만 도메인 화면에서 사용한다. OpenAPI 밖의 도메인별 오류 key는 추정하지 않고, 미정의 오류는 `알 수 없는 오류가 발생했습니다.`로 표시한다. | Character, Audio, Series, Community, FanTalk의 form·list·detail·mutation 화면 | 각 도메인 API의 비2xx 응답 `status`, `message`, `errorProperty` | 기능별 정확한 message key 분기가 필요한 inline 오류·충돌·중복·missing-id 처리는 후속 계약 전까지 만들지 않는다. |
|
||
|
||
#### EXT-006 현재 구현 기준 계약
|
||
|
||
다음 내용은 현재 프론트엔드 adapter·contract test·E2E가 사용하는 기존
|
||
인증 계약이다. 백엔드는 이를 정식 OpenAPI operation으로 옮기거나 별도
|
||
버전 고정 계약으로 제공할 수 있다. 정식 계약이 제공되기 전에도 현재
|
||
로그인·로그아웃 구현은 유지하며 Phase 3~9를 진행한다.
|
||
|
||
| 항목 | Admin Login | Member Logout |
|
||
|---|---|---|
|
||
| Method·Path | `POST /admin/member/login` | `POST /member/logout` |
|
||
| `Accept-Language` | `ko` | `ko` |
|
||
| `Content-Type` | `application/json` | body 없음 |
|
||
| Authorization | 보내지 않음 | `Bearer {jwt-token}` 필수 |
|
||
| Request body | `{ "email": string, "password": string }` | 없음 |
|
||
| 성공 `data` | `{ "token": string, "role": "ADMIN" }` | `{}` |
|
||
| 현재 client 처리 | 유효한 token·ADMIN role만 session으로 저장 | 성공·비2xx·network 오류 모두 local session 제거 후 `/login` 이동 |
|
||
|
||
성공 응답은 현재 다음 envelope를 사용한다.
|
||
|
||
```json
|
||
{
|
||
"success": true,
|
||
"message": null,
|
||
"data": {},
|
||
"errorProperty": null
|
||
}
|
||
```
|
||
|
||
- 로그인 성공에서는 `data`가 `{ "token": "jwt-token", "role": "ADMIN" }`이다.
|
||
- 로그아웃 성공에서는 `data`가 `{}`다.
|
||
- 현재 contract fixture는 로그인 JSON binding 실패 `400`, 지원하지 않는 media type `415`, 로그아웃의 Bearer 없음·잘못됨·폐기 `401`, 비ADMIN `403`, body 전송 `400`을 검증한다.
|
||
- 정확한 backend 오류 message key와 추가 status는 정식 계약 제공 시 확정한다. 제공 전에는 기존 공통 envelope와 서버 `message` 우선 표시를 유지한다.
|
||
- 구현 근거는 `src/features/auth/api/auth-api.ts`, `src/features/auth/model/auth-session.tsx`, `src/features/auth/tests/auth-api.test.ts`, `src/features/auth/tests/auth-session.test.tsx`, `src/shared/mocks/__tests__/auth-handlers.test.ts`, `tests/e2e/auth.spec.ts`다.
|
||
|
||
## 12. 보안과 데이터 취급
|
||
|
||
- JWT, 비밀번호, signed URL, 업로드 파일 본문을 console·분석 이벤트·오류 리포트에 남기지 않는다.
|
||
- 로그인 요청은 인증 header 없이 `POST /admin/member/login`을 호출한다.
|
||
- 로그인 응답의 `token`은 보호 API와 `POST /member/logout`에 `Authorization: Bearer {jwt-token}` 형식으로 적용한다.
|
||
- 로그인 응답의 JWT와 ADMIN role은 `sessionStorage`에만 저장하고 `localStorage`, IndexedDB 또는 cookie로 복제하지 않는다. 같은 탭의 새로고침에서는 복원하며 로그아웃과 401 처리 시 제거한다.
|
||
- 로그아웃 요청은 현재 Bearer token으로 한 번 호출한다. 성공 여부와 관계없이 로컬 인증 정보를 제거하고 로그인 화면으로 이동하며, 네트워크·비2xx 실패 시 서버 로그아웃 확인 실패 경고를 표시한다.
|
||
- API client는 Bearer header와 `Accept-Language: ko`를 중앙에서 일관되게 적용한다.
|
||
- 401 처리 중 여러 요청이 동시에 실패해도 로그인 이동과 알림을 한 번만 수행한다.
|
||
- 파일 이름은 화면 표시에만 사용하고 경로나 MIME을 신뢰하지 않는다.
|
||
- 캐릭터 하위 mutation은 URL의 `characterId`와 서버 ownership 검증을 모두 통과해야 한다.
|
||
- 워크스페이스 진입·복원용 상세 성공 응답으로 `isActive=false`인 캐릭터를 받은 경우 해당 character workspace의 mutation 진입점을 차단한다. soft delete mutation 성공 직후에는 목록 이동을 우선한다. 비활성 ID 조회 허용 여부와 서버의 mutation 검증 정책은 백엔드 책임이다.
|
||
|
||
### 감사 로그 미결 사항과 권고
|
||
|
||
감사 로그 조회 UI는 현재 릴리스에서 **제외**한다.
|
||
|
||
**처리:** 이번 범위에서는 백엔드 감사 기록을 우선한다. 조회 화면이 필요해지면 조회 endpoint, 권한, 필터 계약을 포함한 별도 Phase로 다시 계획한다. JWT, 비밀번호, signed URL, 파일 본문은 기록하지 않는다.
|
||
|
||
## 13. 성능과 품질 요구사항
|
||
|
||
- 목록은 계약에 있는 서버 page/size를 사용하고 무제한 전체 로드를 피한다. 시리즈 순서 변경 모드는 `totalCount`를 기준으로 모든 page를 읽는다. Community는 서버가 제공하는 `totalCount`, `page`, `size`, `hasNext` metadata로 전체 건수와 다음 page 여부를 표시한다.
|
||
- 이미지에는 고정 aspect ratio와 크기를 예약해 layout shift를 줄이고 목록 이미지는 lazy load한다.
|
||
- 필터 변경 중 기존 데이터를 유지해 화면 깜빡임을 줄인다.
|
||
- 파일 업로드 외의 일반 mutation은 중복 제출을 막는다.
|
||
- 목록 검색 debounce 시간은 구현 시 300ms 전후로 일관되게 적용한다.
|
||
- 날짜, 가격, 상태 label은 중앙 formatter로 일관되게 표시한다.
|
||
- 브라우저 지원 범위는 데스크톱 Chrome과 모바일 Chrome이다. 로컬 자동 Gate는 Chromium/mobile Chrome project로 검증한다.
|
||
- mock/server mode는 build-time 환경 설정으로 명시적으로 선택하며 runtime 404 fallback을 사용하지 않는다.
|
||
- domain fixture와 browser handler는 `api-contract.openapi.json`의 제공 계약에서 파생하고 contract test와 함께 변경한다.
|
||
|
||
## 14. 성공 기준
|
||
|
||
### 14.1 기능 수용 기준
|
||
|
||
- ADMIN이 로그인해 캐릭터를 검색하고 선택할 수 있다.
|
||
- 로그인 요청이 `POST /admin/member/login`에 email/password만 보내고 ADMIN role과 token을 처리한다.
|
||
- 로그인 성공 후 JWT와 ADMIN role이 `sessionStorage`에만 저장되어 같은 탭의 새로고침에서 복원되고, `localStorage`, IndexedDB, cookie에는 기록되지 않으며 로그아웃과 401에서 제거된다.
|
||
- 로그아웃 요청이 관리자 전용 경로가 아닌 `POST /member/logout`에 Bearer header와 body 없이 전송된다.
|
||
- 로그아웃 API가 성공하거나 네트워크·비2xx 오류로 실패해도 로컬 session이 제거되고 로그인 화면으로 이동하며, 실패한 경우 경고가 표시되고 session이 복원되지 않는다.
|
||
- 캐릭터 생성 multipart가 필수 `image`와 `request`를 보내고 request에 `name`, `systemPrompt`, `description`이 포함되며 `isActive`와 `externalCharacterId`는 포함되지 않는다.
|
||
- Character, Audio, Series, Community의 일반 수정은 `isActive`를 생략하고 soft delete에만 `isActive=false`를 보내며 `true`는 전송하지 않는다.
|
||
- Character, Audio, Series의 soft delete가 성공하면 목록 cache를 무효화·재조회하고 목록으로 이동하며 성공 알림을 표시한다. Community는 열린 Sheet를 닫고 목록을 재조회한다. 재조회된 각 서버 목록에는 `isActive=false`인 항목이 없어야 하며 클라이언트 필터로 이 결과를 만들지 않는다.
|
||
- 캐릭터 수정이 creator profile 동기화 결과와 함께 다시 조회된다.
|
||
- 워크스페이스 진입·복원용 상세 성공 응답으로 `isActive=false`인 캐릭터를 받은 경우 프론트엔드가 모든 하위 mutation 진입점을 차단한다. soft delete 성공 직후에는 목록 이동·재조회를 우선하고, 상세 요청의 비2xx 응답은 공통 오류 처리로 표시한다.
|
||
- 오디오 생성에서 즉시 공개는 `releaseDate=null`을 보내고, 예약 공개는 미래 Asia/Seoul 입력을 클라이언트에서 ISO-8601 UTC `Z`로 변환해 보낸다. 생성·상세·커뮤니티 목록 요청에는 `timezone` field/query가 없어야 한다.
|
||
- MP3, AAC, M4A 업로드의 진행률·취소·재시도와 `1,024,000,000 bytes` 허용·`1,024,000,001 bytes` 거부 경계 검증이 동작한다.
|
||
- 오디오 콘텐츠와 커뮤니티 첨부 audio가 동일한 확장자·MIME·최대 크기·재생 길이 정책을 사용한다.
|
||
- `.m4a`는 `audio/mp4`와 `audio/x-m4a`를 허용하되 호환 MIME도 실제 MP4/M4A container·codec 검증을 통과해야 한다.
|
||
- 모든 image upload가 `10,485,760 bytes` 이하 제한과 resource별 JPEG/PNG/GIF 허용 범위를 적용한다.
|
||
- 캐릭터 image는 `1:1`로 크롭하고 최대 800×800px 결과를 업로드한다.
|
||
- 커뮤니티 JPEG/PNG는 자유 비율·최대 800px, Series는 `210:297`·최대 1,000px, Audio cover는 `1:1`·최대 800px crop 결과를 업로드한다.
|
||
- JPEG/PNG crop 영역이 resource별 최대 출력 폭보다 작으면 확대하지 않고 가능한 원본 pixel 크기로 업로드한다.
|
||
- Series crop 결과는 `height = round(width × 297 ÷ 210)`을 사용하며 최대 출력은 1,000×1,414px이고 검증 시 1px 이내 오차를 허용한다.
|
||
- 커뮤니티 GIF는 crop Dialog를 열지 않고 원본 비율과 animation을 유지해 등록하며, 원본 가로가 800px을 초과하면 제출 전에 거부한다.
|
||
- crop Dialog에서 이동·확대/축소·초기화·미리보기·취소/적용을 keyboard와 pointer로 완료할 수 있다.
|
||
- 오디오 목록·상세와 커뮤니티 목록 항목의 첨부 오디오가 재생된다.
|
||
- 오디오 콘텐츠 생성 시 테마 목록 `data[]`의 `id`, `theme`, `image`를 query/body 없이 조회하고 선택한 `id`를 request의 `themeId`에 포함한다.
|
||
- 캐릭터 원작 선택기는 필수 `searchTerm`으로 v2 원작 검색 API를 호출하고 선택한 `OriginalWorkSearchItem.id`를 create/update의 `originalWorkId`로 보낸다.
|
||
- media error를 signed URL 만료로 구분하지 않고 일반 재생 오류를 표시한다. 오류만으로 API를 자동 재조회하거나 자동 재생하지 않으며 사용자가 수동 재시도하거나 페이지를 새로고침할 수 있다.
|
||
- 커뮤니티는 전용 상세 GET이나 상세·수정 route 없이 목록 응답 기반 Sheet에서 조회·수정·고정·비활성화·댓글 진입을 제공한다.
|
||
- 오디오 생성·수정과 커뮤니티 생성의 가격은 `0`, `99999`를 허용하고 `-1`, `100000`, 소수를 제출 전에 거부한다.
|
||
- Series에 `OPEN`이나 `MONDAY` 같은 잘못된 값을 보내지 않는다.
|
||
- Series 생성에는 state를 보내지 않고 `keyword` 문자열을 사용하며, 수정에서 state를 바꾸지 않으면 field를 생략한다.
|
||
- RANDOM과 실제 요일을 동시에 선택할 수 없다.
|
||
- 시리즈 장르 목록을 query/body 없이 조회하고 선택한 `SeriesGenreItem.id`를 create/update의 `genreId`로 보내며, 상세의 `genreId`·요일 enum·state enum으로 수정 form을 초기화한다.
|
||
- 커뮤니티 목록은 `timezone` 없이 `page`, `size`를 보내고 서버의 `totalCount`, `page`, `size`, `hasNext`, `items`를 사용한다.
|
||
- FanTalk 목록 item의 `creatorReplies`가 비어 있으면 답변 작성, 비어 있지 않으면 답변 수정 UI를 제공한다. 수정 path의 `replyId`에는 첫 답변의 `fanTalkId`를 사용하고 client의 두 번째 POST를 차단한다. 실제 server에서는 동일 root에 대한 동시 POST 후 재조회해도 활성 creator reply가 하나만 존재해야 하며, 정확한 충돌 status/message key를 client에서 분기하지 않는다.
|
||
- 팬 작성 FanTalk 원글 soft delete가 성공하면 목록을 재조회하고 열린 Sheet를 닫는다. 연결된 creator reply가 함께 삭제됐다고 가정하지 않는다.
|
||
- 댓글은 루트와 직접 답글의 2단계를 넘지 않는다. `writerId === creatorId`인 댓글에만 수정 UI를 제공하고, soft delete는 작성자와 관계없이 해당 row에 제공한다.
|
||
- 모바일에서 조회·오디오 재생, 댓글 전체 관리, FanTalk 답변 작성·수정과 팬 원글 soft delete가 가능하다.
|
||
- `npm run dev:mock`에서 실제 backend 요청 없이 제공 계약 범위의 최종 UI happy path를 확인할 수 있고 mock mode 안내가 표시된다.
|
||
- 기본 `npm run dev`에서는 실제 개발 API를 사용하며 404·network error가 mock 응답으로 바뀌지 않는다.
|
||
- production build에는 browser mock이 활성화되지 않고 mock mode 설정을 허용하지 않는다.
|
||
- 각 도메인의 Progress와 Phase Gate는 `UI 확인 완료(mock)`와 `실제 서버 연동 완료(server)` 증거를 별도로 기록한다.
|
||
|
||
### 14.2 UI/UX 수용 기준
|
||
|
||
- 모든 화면에 로딩, 빈 결과, 오류, 성공 상태가 있다.
|
||
- 모든 폼 입력은 보이는 label과 연결된 오류를 가진다.
|
||
- keyboard-only로 로그인, 캐릭터 선택, 탭 이동, FanTalk 답변, 댓글 관리가 가능하다.
|
||
- 320px에서 가로 넘침 없이 합의된 모바일 기능을 사용할 수 있다.
|
||
- 주요 touch target이 44×44px 이상이다.
|
||
- 상태 정보가 색상에만 의존하지 않는다.
|
||
- primary 기준색은 `#00BDF7`이고 그 위 텍스트·아이콘은 `#062B36`을 사용한다. 흰 배경의 링크와 focus ring은 `#007EA8`을 사용하며 핵심 foreground/background 조합이 WCAG 대비 기준을 충족한다.
|
||
- 초기 릴리스에는 다크 모드와 테마 전환 control이 없고, 시스템 색상 테마와 관계없이 접근성 검증을 통과한 밝은 token을 사용한다.
|
||
- `prefers-reduced-motion`에서 불필요한 transition이 제거된다.
|
||
- 모든 핵심 route의 axe 기반 자동 검사에서 critical·serious 접근성 위반이 0건이다.
|
||
- `ui-ux-pro-max`의 loading, reduced motion, z-index, touch 검증 항목을 확인한다.
|
||
- 공통 오디오 플레이어가 오디오 콘텐츠 목록·상세와 커뮤니티 목록·Sheet에서 동일한 compact audio-only UI로 표시되고, 320px와 200% zoom에서도 핵심 control이 가려지거나 가로 overflow를 만들지 않는다.
|
||
|
||
## 15. Open Questions와 결정 절차
|
||
|
||
| ID | 상태 | 결정 필요 사항 | 현재 권고 |
|
||
|---|---|---|---|
|
||
| OQ-009 | 확정 | 각 페이지의 문자열 최대 길이와 배열 최대 개수 | 초기 UI를 구현한 뒤 실제 페이지에서 문자열 입력 공간과 반복 항목 사용성을 검토해 최대 길이·최대 개수의 권고값을 정한다. backend 호환 확인 후 PRD·OpenAPI 계약·schema·경계값 test를 같은 변경에서 갱신한다. 그전에는 계약에 없는 임의의 최대값을 추가하지 않는다. |
|
||
| OQ-010 | 제외 | 감사 로그 UI 제공 여부 | 현재 릴리스에서는 조회 UI를 만들지 않고 backend 기록을 우선한다. 포함 시 backend 조회 계약을 포함한 별도 Phase로 계획한다. |
|
||
|
||
## 16. 결정 기록
|
||
|
||
| 날짜 | 결정 |
|
||
|---|---|
|
||
| 2026-07-25 | 독립 관리자 웹, 자체 이메일/비밀번호 인증, ADMIN JWT만 사용한다. |
|
||
| 2026-07-25 | refresh token과 자동 갱신 없이 401에서 로그아웃 처리한다. |
|
||
| 2026-07-25 | 로그인은 `POST /admin/member/login`, 로그아웃은 공통 `POST /member/logout`을 사용하고 JWT는 Bearer header로 전달한다. |
|
||
| 2026-07-25 | JWT와 ADMIN role은 `sessionStorage`에만 저장해 같은 탭의 새로고침에서 복원하고 탭 종료 시 제거한다. `localStorage`, IndexedDB, cookie에는 저장하지 않는다. |
|
||
| 2026-07-25 | 로그아웃 API가 실패해도 로컬 session을 제거하고 로그인 화면으로 이동하며 서버 로그아웃 확인 실패 경고를 표시한다. |
|
||
| 2026-07-25 | 관리자는 선택한 캐릭터 문맥에서 작업하며 캐릭터 계정으로 전환하지 않는다. |
|
||
| 2026-07-25 | 생성과 일반 수정 요청에는 `isActive`를 보내지 않고 soft delete 요청에만 `isActive=false`를 보낸다. `isActive=true`는 전송하지 않는다. |
|
||
| 2026-07-25 | Character, Audio, Series, Community 목록은 활성 resource만 반환하고 활성 상태 filter/query를 제공하지 않는다. |
|
||
| 2026-07-25 | Character, Audio, Series soft delete 성공 후 해당 active-only 목록으로 이동하고, Community는 열린 Sheet를 닫고 현재 active-only 목록을 갱신하며 모두 성공 알림을 표시한다. |
|
||
| 2026-07-25 | 비활성 ID 상세 GET의 반환·거부 정책은 백엔드 책임이므로 프론트엔드 요구사항·P0 Gate에서 제외한다. 프론트엔드는 실제 성공 또는 비2xx 응답을 공통 규칙대로 처리한다. |
|
||
| 2026-07-25 | `externalCharacterId`를 모든 계약에서 제거한다. |
|
||
| 2026-07-25 | 오디오 상태는 `OPEN`과 `SCHEDULED`이며 서버가 계산한다. |
|
||
| 2026-07-25 | 즉시 공개는 null, 예약 공개만 날짜를 입력하고 UTC로 전송한다. |
|
||
| 2026-07-25 | MP3, AAC, M4A, 최대 decimal 1,024MB(`1,024,000,000 bytes`), 재생 길이 무제한을 사용한다. |
|
||
| 2026-07-25 | `audio/x-m4a`는 `.m4a` 파일에만 허용하고 실제 MP4/M4A container·codec을 검증한다. |
|
||
| 2026-07-25 | 커뮤니티 첨부 audio도 오디오 콘텐츠와 같은 MP3/AAC/M4A, 최대 1,024MB, 재생 길이 무제한 정책을 사용한다. |
|
||
| 2026-07-25 | 공통 image upload 최대 크기는 10MB로 한다. exact byte 값은 백엔드 확인 후 PRD·API 계약·구현 상수·경계 test를 같은 값으로 정렬한다. |
|
||
| 2026-07-27 | 공통 image upload 10MB의 exact byte를 `10,485,760 bytes`로 확정하고 `10,485,761 bytes`부터 거부한다. |
|
||
| 2026-07-25 | image는 JPEG/PNG를 허용하고 GIF는 커뮤니티에만 허용한다. Character는 1:1·800px, Community JPEG/PNG는 자유 비율·800px, Series는 210:297·1,000px, Audio cover는 1:1·800px crop UI를 제공한다. Community GIF는 crop·resize하지 않고 원본 가로 800px 초과 시 등록을 거부한다. |
|
||
| 2026-07-25 | JPEG/PNG crop 결과는 선택한 원본 crop 영역보다 확대하지 않고 resource별 800px/1,000px을 최대 출력 폭으로만 사용한다. |
|
||
| 2026-07-25 | Series crop 세로는 `round(width × 297 ÷ 210)`으로 계산하고 최대 1,000×1,414px, 검증 오차 1px을 적용한다. |
|
||
| 2026-07-25 | Series state와 요일 enum을 실제 backend enum에 맞게 정정한다. |
|
||
| 2026-07-25 | FanTalk는 한 번만 답변할 수 있고 기존 답변 수정은 허용한다. |
|
||
| 2026-07-25 | 모바일은 조회, 오디오 재생, 댓글 전체 관리, FanTalk 답변 작성·수정을 지원한다. |
|
||
| 2026-07-25 | Tailwind CSS와 shadcn/ui를 사용하고 `ui-ux-pro-max`로 UI를 생성·검증한다. |
|
||
| 2026-07-26 | 초기 릴리스는 밝은 테마만 제공하고 다크 모드, 테마 전환 버튼과 시스템 색상 테마 연동은 후속 범위로 둔다. |
|
||
| 2026-07-26 | main color와 primary 기준색을 `#00BDF7`로 정하고 이에 맞춘 cyan 계열 primitive·semantic palette를 사용한다. primary 위에는 접근 가능한 `#062B36`을, 흰 배경의 링크·focus에는 `#007EA8`을 사용한다. |
|
||
| 2026-07-26 | Series 생성 request에는 state를 보내지 않으며 프론트엔드는 서버의 정확한 초기 기본값을 알 필요 없이 생성 응답의 state를 표시한다. |
|
||
| 2026-07-26 | 재생 오류를 signed URL 만료로 구분하지 않고, media error만으로 API 자동 재조회나 자동 재생을 수행하지 않는다. 일반 오류와 수동 재시도·페이지 새로고침 안내를 제공한다. |
|
||
| 2026-07-26 | 커뮤니티 전용 상세 GET과 상세·수정 route를 추가하지 않는다. 목록 응답 기반 Sheet를 사용하고 첨부 audio URL 갱신 전용 요청은 만들지 않는다. 사용자 새로고침이나 mutation 후 cache 무효화 등 일반 목록 재조회가 있으면 새 응답 값을 사용한다. |
|
||
| 2026-07-26 | 문자열 최대 길이와 배열 최대 개수는 초기 UI 작성 후 각 페이지에서 권고값을 정하고 백엔드 호환 확인 후 확정한다. 그 전에는 제공 계약에 없는 최대값을 추정하지 않는다. |
|
||
| 2026-07-26 | 댓글 API·팬 댓글 삭제 권한 오류와 FanTalk 목록·상세·답변 수정·중복 답변 계약은 백엔드 제공 대기 사항으로 분류하고 프론트엔드 Open Questions에서 제외한다. |
|
||
| 2026-07-27 | 오디오 콘텐츠 생성에는 `themeId`가 필수이며, 테마 선택지는 `GET /api/v2/admin/ai-characters/audio-content-themes`에서 query/body 없이 조회한다. |
|
||
| 2026-07-27 | backend endpoint 구현 전에도 제공된 API Contract 범위의 최종 UI를 확인할 수 있도록 명시적 개발 전용 browser MSW mode를 제공한다. 실제 404 자동 fallback은 금지하고 mock UI 완료와 실제 server 연동 완료를 분리한다. |
|
||
| 2026-07-28 | 삭제된 `api-contract.md`를 `api-contract.openapi.json`으로 대체하고, endpoint·query·multipart·request/response·오류는 OpenAPI를 단일 진실 원천으로 사용한다. OpenAPI에 없는 제품·UI 정책만 PRD가 소유한다. |
|
||
| 2026-07-28 | 새 OpenAPI에 맞춰 Character 생성 image/systemPrompt 필수, Audio의 `search_word`·`contentFile`·`releaseDate`·테마 field, Series의 `keyword`·`contentIdList`·`ids`, Community의 `timezone`·`postImage`·`audioUrl`, FanTalk 목록 GET을 구현 기준으로 정정한다. |
|
||
| 2026-07-28 | OpenAPI에 없는 인증 정식 명세, 원작·장르 lookup, 목록 active-only 보장, FanTalk 상세·답변 수정·filter/sort·유일성 오류, 댓글 CRUD와 도메인별 오류 key는 외부 의존으로 관리하며 추정 구현하지 않는다. Series 수정 form은 상세 응답이 list item과 같은 `genreId`, enum 요일 배열, enum state를 제공하면 별도 edit DTO 없이 초기화한다. |
|
||
| 2026-07-28 | `EXT-003`은 별도 backend 계약 항목에서 제외한다. 시리즈 상세 API가 `SeriesListItem`과 같은 수정용 원본값을 내려주고, 장르 API는 선택 option 목록 표시용으로 호출하는 방식으로 처리한다. |
|
||
| 2026-07-28 | OQ-009는 초기 UI를 먼저 구현하고 실제 페이지를 보며 문자열 최대 길이와 배열 최대 개수를 제안한 뒤 backend 호환을 확인해 확정하는 절차로 종결한다. 실제 최대값 확정 전에는 임의 상한을 추가하지 않는다. |
|
||
| 2026-07-28 | `EXT-006`에 현재 구현된 `POST /admin/member/login`과 `POST /member/logout`의 request·response·인증·client 처리 기준을 기록한다. 정식 OpenAPI 포함은 비차단 외부 의존이며 기존 인증 구현과 Phase 3~9 진행을 유지한다. |
|
||
| 2026-07-28 | Phase 3부터는 OpenAPI 제공 범위를 먼저 구현해 Phase 9 활성 범위 Gate까지 진행한다. 미제공 계약은 영향을 받는 기능만 후속 범위로 남기고, 계약 도착 후 별도 vertical slice와 관련 Phase Gate·Phase 9를 다시 실행한다. |
|
||
| 2026-07-28 | 감사 로그 조회 UI는 현재 릴리스에서 제외한다. backend 감사 기록을 우선하고, 조회 UI가 필요해지면 조회 endpoint·권한·필터 계약을 포함한 후속 Phase로 다시 계획한다. |
|
||
| 2026-07-29 | `EXT-010`은 해결로 정정한다. 이미지 비율·crop 결과는 backend가 검증하지 않고 client가 보장하며, 파일 용량·MIME 등 서버에서 확인 가능한 항목은 backend가 `FILE-001~015`와 동일하게 검증한다. |
|
||
| 2026-07-29 | OpenAPI 2.3.0에 추가된 원작 검색, 활성 장르 목록, Audio·Community 댓글, Community pagination, FanTalk 팬 원글 DELETE와 답변 PUT을 후속 구현 계약으로 채택한다. 목록 API는 서버가 `isActive=true` 항목만 반환하므로 client 활성 filter를 추가하지 않는다. |
|
||
| 2026-07-29 | 오디오 예약 공개는 Asia/Seoul 입력을 client에서 ISO-8601 UTC `Z`로 변환해 `releaseDate`에 보내고 `timezone` field/query를 제거한다. 즉시 공개는 `releaseDate=null`을 유지한다. |
|
||
| 2026-07-29 | FanTalk 답변 여부는 `creatorReplies`의 빈 배열 여부로 판단하고 수정 path의 `replyId`에는 `creatorReplies[].fanTalkId`를 사용한다. 댓글은 `writerId === creatorId`일 때 AI 캐릭터 작성으로 판단하며 팬 작성 FanTalk 원글 soft delete를 이번 후속 구현 범위에 포함한다. |
|
||
| 2026-07-29 | 백엔드에서 FanTalk 답변 수정 API가 이미 구현됐고 OpenAPI status만 누락됐음을 확인했다. 해당 operation을 `implemented`로 정정하고 Phase 10에서 mock/client뿐 아니라 실제 server integration까지 구현·검증한다. |
|
||
| 2026-07-29 | `EXT-009`의 가격 범위 `0..99999`를 Audio 생성·수정과 Community 생성 OpenAPI request schema, 요구사항, Phase 10 경계 test에 동일하게 적용한다. |
|
||
| 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는 하나의 선택 카드 시각 문법을 사용한다. |
|
||
| 2026-08-04 | Series 생성 form은 image 입력을 최상단에 배치하고 keyword를 Audio tags와 같은 공용 `TagInput` chip UI로 입력한다. Enter/comma로 확정하고 remove button으로 삭제하며 server request는 기존 comma-separated `keyword` 문자열 계약을 유지한다. |
|