docs(ai-character): 관리자 웹 기획 문서 추가
This commit is contained in:
864
docs/20260725_AI캐릭터관리자웹/api-contract.md
Normal file
864
docs/20260725_AI캐릭터관리자웹/api-contract.md
Normal file
@@ -0,0 +1,864 @@
|
||||
# AI 캐릭터 관리자 웹 정규화 API Contract
|
||||
|
||||
## 문서 정보
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 상태 | 인터뷰 보정 반영, 백엔드 제공 대기 계약 분리 |
|
||||
| 작성일 | 2026-07-25 |
|
||||
| 기준 | 사용자 제공 API Contract + 인터뷰 확정사항 |
|
||||
| 관련 요구사항 | [prd.md](./prd.md) |
|
||||
| 관련 구현 계획 | [plan-task.md](./plan-task.md) |
|
||||
|
||||
이 문서는 대화로 제공된 API Contract를 저장소에 영속적으로 보존하고 인터뷰에서 확정된 보정사항을 적용한 프론트엔드 기준 계약이다. 실제 백엔드 구현을 다른 저장소에서 추정하지 않는다. “백엔드 제공 대기”로 표시한 endpoint와 세부 규칙은 프론트엔드 Open Question이 아니며, 백엔드 계약이 제공되기 전에는 해당 network integration을 구현하지 않는다.
|
||||
|
||||
## 1. 공통 규칙
|
||||
|
||||
### 1.1 성공 응답
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": null,
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
- 모든 성공 응답은 `ApiResponse.ok(...)` wrapper를 사용한다.
|
||||
- 인증 endpoint의 실제 성공 응답은 최상위 `errorProperty=null`도 포함한다. 프론트엔드 공통 type은 성공 응답의 `errorProperty`가 없거나 `null`인 두 형태를 수용한다.
|
||||
|
||||
### 1.2 오류 응답
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "잘못된 요청입니다.",
|
||||
"data": null,
|
||||
"errorProperty": null
|
||||
}
|
||||
```
|
||||
|
||||
- 모든 오류는 의미에 맞는 비2xx HTTP status와 `ApiResponse.error(...)` wrapper를 사용한다.
|
||||
- 오류를 2xx로 normalize하지 않는다.
|
||||
- `Accept-Language: ko|en|ja`에 따라 KO/EN/JA message를 반환한다.
|
||||
- header가 없거나 지원하지 않는 언어면 KO로 fallback한다.
|
||||
- security filter 단계도 MVC interceptor에 의존하지 않고 `Accept-Language` header를 직접 해석한다.
|
||||
- 이 관리자 웹은 모든 요청에 `Accept-Language: ko`를 보낸다.
|
||||
|
||||
| 오류 | HTTP status | message key |
|
||||
|---|---:|---|
|
||||
| JWT 없음·잘못됨·만료·폐기 | 401 | `common.error.bad_credentials` |
|
||||
| JWT role 비ADMIN | 403 | `common.error.access_denied` |
|
||||
| JWT ADMIN + 현재 DB role 비ADMIN stale claim | 403 | `common.error.access_denied` |
|
||||
| request binding·target 미존재·creatorMember 누락·role/memberKind 불변식 위반 | 400 | `common.error.invalid_request` |
|
||||
| 신규 prefix 미매핑 경로 | 404 | `common.error.invalid_request` |
|
||||
| 지원하지 않는 HTTP method | 405 | `common.error.invalid_request` |
|
||||
| 지원하지 않는 media type | 415 | `common.error.invalid_request` |
|
||||
| 예상하지 못한 서버 오류 | 500 | `common.error.unknown` |
|
||||
|
||||
Phase 2~6에서 추가되는 domain/client/server 오류는 구현 전에 정확한 비2xx status와 KO/EN/JA message key를 확정한다. 신규 prefix 전용 오류 처리는 legacy/public endpoint에 적용하지 않는다.
|
||||
|
||||
### 1.3 페이지 응답
|
||||
|
||||
모든 목록·검색 endpoint는 `page` 기본 0, `size` 기본 20, 최소 20, 최대 50 보정을 적용한다.
|
||||
|
||||
```ts
|
||||
type PageData<T> = {
|
||||
totalCount: number
|
||||
page: number
|
||||
size: number
|
||||
hasNext: boolean
|
||||
items: T[]
|
||||
}
|
||||
```
|
||||
|
||||
### 1.4 인터뷰 공통 보정
|
||||
|
||||
- `characterId`는 선택된 target resource endpoint의 외부 대상 식별자다.
|
||||
- 캐릭터 목록·검색과 생성에는 path `characterId`가 없다.
|
||||
- `externalCharacterId`는 존재하지 않으므로 모든 request, response, UI에서 제거한다.
|
||||
- 생성 request에는 `isActive`를 보내지 않는다. 최초 상태는 백엔드가 결정한다.
|
||||
- 일반 수정 request에는 `isActive` key를 보내지 않는다. soft delete request에만 `isActive=false`를 보내며 `isActive=true`는 전송하지 않는다.
|
||||
- 비활성 resource의 복원과 hard delete는 현재 범위가 아니다.
|
||||
- multipart의 JSON part 이름은 `request`다.
|
||||
- optional 교체 파일을 보내지 않으면 기존 파일을 유지한다. 기존 media 자체를 제거하는 contract는 없다.
|
||||
- Character, Audio, Series, Community 목록 endpoint는 활성 resource만 반환하며 `isActive`, `activeStatus` 등 활성 상태 query를 받지 않는다.
|
||||
- Character, Audio, Series의 soft delete 성공 응답을 받으면 프론트엔드는 관련 active-only 목록 query를 무효화·재조회해 비활성화한 항목을 표시하지 않고 목록으로 이동해 성공 알림을 표시한다. Community는 열린 Sheet를 닫고 현재 active-only 목록을 갱신해 해당 항목을 제거한다.
|
||||
- 비활성 ID 상세 GET의 반환·거부 정책은 백엔드 책임이며 이 프론트엔드 계약에서 규정하거나 구현 Gate로 관리하지 않는다. 클라이언트는 성공 envelope가 오면 반환 데이터를 사용하고 비2xx이면 공통 오류 처리를 적용한다.
|
||||
- 오디오 media error는 signed URL 만료로 구분하거나 추정하지 않는다. 재생 오류만으로 목록·상세 API를 자동 재조회하거나 `play()`를 자동 재호출하지 않으며 일반 오류와 수동 재시도·페이지 새로고침 안내를 제공한다.
|
||||
- 커뮤니티 첨부 audio URL만을 갱신하기 위한 요청은 하지 않고 media error를 refetch trigger로 사용하지 않는다. 사용자 페이지 새로고침이나 mutation 후 cache 무효화 등 일반 목록 재조회에서는 새 응답의 URL을 사용한다. 전용 상세 조회나 URL 재발급 호출을 추가하지 않는다.
|
||||
|
||||
## 2. 공통 enum과 형식
|
||||
|
||||
```ts
|
||||
type AudioContentStatus = "OPEN" | "SCHEDULED"
|
||||
type SeriesState = "PROCEEDING" | "SUSPEND" | "COMPLETE"
|
||||
type SeriesPublishedDay =
|
||||
| "SUN"
|
||||
| "MON"
|
||||
| "TUE"
|
||||
| "WED"
|
||||
| "THU"
|
||||
| "FRI"
|
||||
| "SAT"
|
||||
| "RANDOM"
|
||||
```
|
||||
|
||||
- `OPEN`은 현재 출시된 오디오 콘텐츠다.
|
||||
- `SCHEDULED`는 미래 공개가 예약된 오디오 콘텐츠다.
|
||||
- Audio status는 백엔드가 선택해 반환하며 프론트엔드가 계산하지 않는다.
|
||||
- `RANDOM`은 다른 요일과 함께 보낼 수 없다.
|
||||
- published days는 RANDOM 단독 또는 하나 이상의 실제 요일 목록이다.
|
||||
- 모든 `*AtUtc`, `releaseDateUtc`는 ISO-8601 UTC `Z` 문자열이다.
|
||||
- 가격은 0 이상의 정수이며 단위는 “캔”이다.
|
||||
|
||||
### 2.1 이미지 업로드와 crop
|
||||
|
||||
모든 image part의 원본 파일 크기는 최대 10MB다. 확장자 문자열만 신뢰하지 않고 실제 MIME을 함께 검증한다.
|
||||
|
||||
| resource | 허용 확장자 | 허용 MIME | crop aspect ratio | 등록 결과 최대 가로 폭 | 결과 세로 |
|
||||
|---|---|---|---:|---:|---|
|
||||
| 캐릭터 | `.jpg`, `.jpeg`, `.png` | `image/jpeg`, `image/png` | `1:1` | 800px | 가로와 동일 |
|
||||
| 커뮤니티 JPEG/PNG | `.jpg`, `.jpeg`, `.png` | `image/jpeg`, `image/png` | 자유 | 800px | 선택한 crop ratio에 따라 결정 |
|
||||
| 커뮤니티 GIF | `.gif` | `image/gif` | crop 없음 | 800px | 원본 비율 유지 |
|
||||
| 시리즈 | `.jpg`, `.jpeg`, `.png` | `image/jpeg`, `image/png` | `210:297` | 1,000px | 비율에 따라 결정 |
|
||||
| 오디오 콘텐츠 cover | `.jpg`, `.jpeg`, `.png` | `image/jpeg`, `image/png` | `1:1` | 800px | 가로와 동일 |
|
||||
|
||||
- WebP와 표에 없는 image 형식은 허용하지 않는다.
|
||||
- GIF는 커뮤니티 image에서만 허용하며 캐릭터, 시리즈, 오디오 콘텐츠 cover에서는 거부한다.
|
||||
- 캐릭터, 커뮤니티 JPEG/PNG, 시리즈, 오디오 콘텐츠에서 새 image를 선택하면 resource별 비율로 crop한 결과 File을 기존 multipart file part에 보낸다. crop 좌표나 비율을 별도 request field로 추가하지 않는다.
|
||||
- 커뮤니티 GIF는 crop Dialog를 열거나 canvas로 재처리하지 않는다. 원본 가로가 800px 이하인 경우에만 원본 비율·animation 유지 File을 기존 multipart `image` part에 보낸다.
|
||||
- 수정 화면에서 새 image를 선택하지 않거나 crop을 취소하면 해당 file part를 보내지 않으며 기존 image를 유지한다.
|
||||
- 서버는 파일 크기, 실제 MIME, 결과 pixel 크기와 고정 비율을 다시 검증해야 한다.
|
||||
- “가로 800/1,000”은 등록 결과의 최대 출력 폭이다. JPEG/PNG crop 결과에는 이 제한을 적용하되 선택된 원본 crop 영역보다 확대하지 않는다.
|
||||
- Series crop 결과의 세로 pixel은 `round(width × 297 ÷ 210)`으로 계산한다. 최대 폭에서는 1,000×1,414px이며 server 비율 검증은 계산된 세로값 기준 1px 이내 오차를 허용한다.
|
||||
- 커뮤니티 GIF의 원본 가로가 800px을 초과하면 client가 제출을 차단하고 server도 `400`과 `common.error.invalid_request` envelope로 거부한다. GIF를 축소·crop·재인코딩하지 않는다.
|
||||
|
||||
## 3. 인증
|
||||
|
||||
인증 endpoint는 AI 캐릭터 관리자 신규 prefix가 아니라 기존 admin/common member 경로를 사용한다.
|
||||
|
||||
### 3.1 Admin Login
|
||||
|
||||
`POST /admin/member/login`
|
||||
|
||||
Content-Type: `application/json`
|
||||
|
||||
Authorization header: 없음
|
||||
|
||||
Request body:
|
||||
|
||||
```json
|
||||
{
|
||||
"email": "admin@test.com",
|
||||
"password": "password"
|
||||
}
|
||||
```
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": null,
|
||||
"data": {
|
||||
"token": "jwt-token",
|
||||
"role": "ADMIN"
|
||||
},
|
||||
"errorProperty": null
|
||||
}
|
||||
```
|
||||
|
||||
- 보호 route에 진입하려면 `data.role`이 `ADMIN`이어야 한다.
|
||||
- `data.token`은 보호 API의 Bearer token으로 사용한다.
|
||||
|
||||
### 3.2 Admin Logout
|
||||
|
||||
관리자 전용 로그아웃 endpoint는 없다. 공통 로그아웃 endpoint를 사용한다.
|
||||
|
||||
`POST /member/logout`
|
||||
|
||||
Headers:
|
||||
|
||||
```http
|
||||
Authorization: Bearer {jwt-token}
|
||||
```
|
||||
|
||||
- Request body 없음
|
||||
|
||||
Response:
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": null,
|
||||
"data": {},
|
||||
"errorProperty": null
|
||||
}
|
||||
```
|
||||
|
||||
### 3.3 클라이언트 인증 규칙
|
||||
|
||||
- 로그인 요청에는 Authorization header를 보내지 않는다.
|
||||
- 로그인 이후 보호 API와 로그아웃에는 `Authorization: Bearer {jwt-token}`을 보낸다.
|
||||
- refresh token과 자동 갱신은 사용하지 않는다.
|
||||
- 401이면 인증 상태를 제거하고 로그인으로 이동한다.
|
||||
- 로그인 성공 시 `data.token`과 `data.role="ADMIN"`을 `sessionStorage`에 저장한다.
|
||||
- 앱 시작과 같은 탭의 새로고침에서는 `sessionStorage`의 token과 role을 읽어 session을 복원한다. 값이 없거나 role이 `ADMIN`이 아니면 보호 route에 진입시키지 않는다.
|
||||
- 로그아웃 요청은 현재 Bearer token으로 한 번 호출한다. 성공 여부와 관계없이 `sessionStorage`의 인증 정보를 제거하고 로그인 화면으로 이동한다.
|
||||
- 로그아웃이 네트워크 오류 또는 비2xx로 실패하면 서버 로그아웃 확인 실패 경고를 표시하고 제거한 session을 복원하지 않는다.
|
||||
- 401 처리 시에도 `sessionStorage`의 인증 정보를 제거하고 로그인 화면으로 이동한다.
|
||||
- 인증 정보를 `localStorage`, IndexedDB 또는 cookie로 복제하지 않는다. 탭 종료 시에는 `sessionStorage`의 브라우저 lifecycle을 따른다.
|
||||
|
||||
## 4. 캐릭터
|
||||
|
||||
### 4.1 목록·검색
|
||||
|
||||
`GET /api/v2/admin/ai-characters?search=루나&page=0&size=20`
|
||||
|
||||
- 활성 캐릭터만 반환한다. 활성 상태 filter/query는 없다.
|
||||
|
||||
Query:
|
||||
|
||||
```ts
|
||||
{
|
||||
search?: string
|
||||
page?: number
|
||||
size?: number
|
||||
}
|
||||
```
|
||||
|
||||
Response `data`:
|
||||
|
||||
```json
|
||||
{
|
||||
"totalCount": 1,
|
||||
"page": 0,
|
||||
"size": 20,
|
||||
"hasNext": false,
|
||||
"items": [
|
||||
{
|
||||
"characterId": 101,
|
||||
"name": "루나",
|
||||
"description": "달빛을 좋아하는 AI 캐릭터",
|
||||
"imageUrl": "https://cdn.example.com/characters/luna.png",
|
||||
"creatorMemberId": 9001,
|
||||
"creatorNickname": "루나",
|
||||
"originalWorkId": 31,
|
||||
"isActive": true,
|
||||
"createdAtUtc": "2026-07-24T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 4.2 상세
|
||||
|
||||
`GET /api/v2/admin/ai-characters/{characterId}`
|
||||
|
||||
Response `data`:
|
||||
|
||||
```json
|
||||
{
|
||||
"characterId": 101,
|
||||
"name": "루나",
|
||||
"description": "달빛을 좋아하는 AI 캐릭터",
|
||||
"imageUrl": "https://cdn.example.com/characters/luna.png",
|
||||
"creatorMemberId": 9001,
|
||||
"creatorNickname": "루나",
|
||||
"creatorProfileImageUrl": "https://cdn.example.com/characters/luna.png",
|
||||
"creatorIntroduce": "달빛을 좋아하는 AI 캐릭터",
|
||||
"originalWorkId": 31,
|
||||
"isActive": true,
|
||||
"createdAtUtc": "2026-07-24T00:00:00Z",
|
||||
"updatedAtUtc": "2026-07-24T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 4.3 생성
|
||||
|
||||
`POST /api/v2/admin/ai-characters`
|
||||
|
||||
Content-Type: `multipart/form-data`
|
||||
|
||||
| Part | Content | 필수 |
|
||||
|---|---|---:|
|
||||
| `image` | File | 아니요 |
|
||||
| `request` | `application/json` | 예 |
|
||||
|
||||
`request`:
|
||||
|
||||
```ts
|
||||
{
|
||||
name: string
|
||||
description: string
|
||||
originalWorkId?: number | null
|
||||
}
|
||||
```
|
||||
|
||||
- `isActive`와 `externalCharacterId`를 보내지 않는다.
|
||||
- `originalWorkId`는 UI에서 선택 사항이다. 미선택 값을 `null`로 보낼지 key를 생략할지는 백엔드 canonical form 확정이 필요하다.
|
||||
- 백엔드는 새 `ChatCharacter`와 연결된 `creator(memberKind = AI_CHARACTER)`를 함께 생성한다.
|
||||
- Response `data`는 캐릭터 상세와 같다.
|
||||
|
||||
### 4.4 수정·비활성화
|
||||
|
||||
`PUT /api/v2/admin/ai-characters/{characterId}`
|
||||
|
||||
Content-Type: `multipart/form-data`
|
||||
|
||||
| Part | Content | 필수 |
|
||||
|---|---|---:|
|
||||
| `image` | 교체 File | 아니요 |
|
||||
| `request` | `application/json` | 예 |
|
||||
|
||||
`request`:
|
||||
|
||||
```ts
|
||||
{
|
||||
name: string
|
||||
description: string
|
||||
originalWorkId?: number | null
|
||||
isActive?: false
|
||||
}
|
||||
```
|
||||
|
||||
- `image` 미전송은 기존 이미지 유지다.
|
||||
- 기존 이미지 자체를 제거하는 flag는 제공되지 않았다.
|
||||
- 일반 수정에서는 `isActive`를 생략한다. `isActive=false`는 soft delete 전용이며 `true`는 보내지 않는다.
|
||||
- 현재 백엔드는 name/description/image 변경을 creator nickname/introduce/profile image에 동기화한다.
|
||||
- creator 상황에 따른 조건부 동기화는 다음 백엔드 범위이며 현재 프론트엔드가 분기하지 않는다.
|
||||
- Response `data`는 캐릭터 상세와 같다.
|
||||
- soft delete 성공 후 캐릭터 active-only 목록으로 이동하고 성공 알림을 표시한다.
|
||||
|
||||
## 5. 오디오 콘텐츠
|
||||
|
||||
### 5.1 목록·검색
|
||||
|
||||
`GET /api/v2/admin/ai-characters/{characterId}/audio-contents?search=밤&status=OPEN&page=0&size=20`
|
||||
|
||||
- 활성 오디오만 반환한다. 활성 상태 filter/query는 없으며 `status`는 공개 상태 필터다.
|
||||
|
||||
Query:
|
||||
|
||||
```ts
|
||||
{
|
||||
search?: string
|
||||
status?: "OPEN" | "SCHEDULED"
|
||||
page?: number
|
||||
size?: number
|
||||
}
|
||||
```
|
||||
|
||||
Response `data`:
|
||||
|
||||
```json
|
||||
{
|
||||
"totalCount": 1,
|
||||
"page": 0,
|
||||
"size": 20,
|
||||
"hasNext": false,
|
||||
"items": [
|
||||
{
|
||||
"contentId": 501,
|
||||
"title": "밤 산책",
|
||||
"coverImageUrl": "https://cdn.example.com/audio/501-cover.png",
|
||||
"audioSignedUrl": "https://cdn.example.com/signed/audio/501.m4a?Expires=...",
|
||||
"price": 1000,
|
||||
"isAdult": false,
|
||||
"isActive": true,
|
||||
"releaseDateUtc": "2026-07-25T00:00:00Z",
|
||||
"status": "OPEN"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
- status 미전송 시 어떤 집합을 반환할지는 백엔드가 결정한다.
|
||||
|
||||
### 5.2 상세
|
||||
|
||||
`GET /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}`
|
||||
|
||||
Response `data`:
|
||||
|
||||
```json
|
||||
{
|
||||
"contentId": 501,
|
||||
"title": "밤 산책",
|
||||
"description": "조용한 밤 산책 오디오",
|
||||
"coverImageUrl": "https://cdn.example.com/audio/501-cover.png",
|
||||
"audioSignedUrl": "https://cdn.example.com/signed/audio/501.m4a?Expires=...",
|
||||
"price": 1000,
|
||||
"isAdult": false,
|
||||
"isActive": true,
|
||||
"releaseDateUtc": "2026-07-25T00:00:00Z",
|
||||
"status": "OPEN",
|
||||
"seriesIds": [701],
|
||||
"createdAtUtc": "2026-07-24T00:00:00Z",
|
||||
"updatedAtUtc": "2026-07-24T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 5.3 생성
|
||||
|
||||
`POST /api/v2/admin/ai-characters/{characterId}/audio-contents`
|
||||
|
||||
Content-Type: `multipart/form-data`
|
||||
|
||||
| Part | Content | 필수 |
|
||||
|---|---|---:|
|
||||
| `coverImage` | File | 예 |
|
||||
| `audioFile` | File | 예 |
|
||||
| `request` | `application/json` | 예 |
|
||||
|
||||
`request`:
|
||||
|
||||
```ts
|
||||
{
|
||||
title: string
|
||||
description: string
|
||||
price: number
|
||||
isAdult: boolean
|
||||
releaseDateUtc: string | null
|
||||
seriesIds: number[]
|
||||
}
|
||||
```
|
||||
|
||||
- `isActive`와 `status`를 보내지 않는다.
|
||||
- 즉시 공개는 `releaseDateUtc=null`이다.
|
||||
- 예약 공개는 Asia/Seoul 미래 시각을 UTC `Z` 문자열로 변환해 보낸다.
|
||||
- Response `data`는 오디오 상세와 같다.
|
||||
|
||||
### 5.4 수정·비활성화
|
||||
|
||||
`PUT /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}`
|
||||
|
||||
Content-Type: `multipart/form-data`
|
||||
|
||||
| Part | Content | 필수 |
|
||||
|---|---|---:|
|
||||
| `coverImage` | 교체 File | 아니요 |
|
||||
| `audioFile` | 교체 File | 아니요 |
|
||||
| `request` | `application/json` | 예 |
|
||||
|
||||
`request`:
|
||||
|
||||
```ts
|
||||
{
|
||||
title: string
|
||||
description: string
|
||||
price: number
|
||||
isAdult: boolean
|
||||
isActive?: false
|
||||
releaseDateUtc: string | null
|
||||
seriesIds: number[]
|
||||
}
|
||||
```
|
||||
|
||||
- 교체 File 미전송은 기존 media 유지다.
|
||||
- 일반 수정에서는 `isActive`를 생략한다. `isActive=false`는 soft delete 전용이며 `true`는 보내지 않는다.
|
||||
- Response `data`는 오디오 상세와 같다.
|
||||
- soft delete 성공 후 선택 캐릭터의 오디오 active-only 목록으로 이동하고 성공 알림을 표시한다.
|
||||
|
||||
### 5.5 업로드 규칙
|
||||
|
||||
- 오디오 콘텐츠 확장자: `.mp3`, `.aac`, `.m4a`
|
||||
- canonical MIME: `audio/mpeg`, `audio/aac`, `audio/mp4`
|
||||
- compatibility MIME: `audio/x-m4a`는 `.m4a` 파일에 한해 허용
|
||||
- 최대 파일 크기: decimal 1,024MB, `1,024,000,000 bytes` 이하
|
||||
- byte 경계: `1,024,000,000` 허용, `1,024,000,001` 거부
|
||||
- 최대 재생 길이: 제한 없음
|
||||
- WAV: 미지원
|
||||
- 서버는 확장자와 MIME 외에 실제 container/codec을 검증해야 한다.
|
||||
- `audio/x-m4a`가 `.m4a`가 아닌 확장자와 조합되거나 실제 MP4/M4A container·codec 검증에 실패하면 `415`와 `common.error.invalid_request` envelope로 거부한다.
|
||||
- cover image의 형식, `1:1` crop과 최대 800px 출력 규칙은 `2.1 이미지 업로드와 crop`을 따른다.
|
||||
|
||||
## 6. 시리즈
|
||||
|
||||
### 6.1 목록
|
||||
|
||||
`GET /api/v2/admin/ai-characters/{characterId}/series?page=0&size=20`
|
||||
|
||||
- 활성 시리즈만 반환한다. 활성 상태 filter/query는 없다.
|
||||
|
||||
Query:
|
||||
|
||||
```ts
|
||||
{
|
||||
page?: number
|
||||
size?: number
|
||||
}
|
||||
```
|
||||
|
||||
Response `data`:
|
||||
|
||||
```json
|
||||
{
|
||||
"totalCount": 1,
|
||||
"page": 0,
|
||||
"size": 20,
|
||||
"hasNext": false,
|
||||
"items": [
|
||||
{
|
||||
"seriesId": 701,
|
||||
"title": "루나의 밤",
|
||||
"introduction": "밤을 주제로 한 시리즈",
|
||||
"coverImageUrl": "https://cdn.example.com/series/701.png",
|
||||
"genreId": 3,
|
||||
"isAdult": false,
|
||||
"state": "PROCEEDING",
|
||||
"isActive": true,
|
||||
"orders": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 6.2 상세
|
||||
|
||||
`GET /api/v2/admin/ai-characters/{characterId}/series/{seriesId}`
|
||||
|
||||
Response `data`:
|
||||
|
||||
```json
|
||||
{
|
||||
"seriesId": 701,
|
||||
"title": "루나의 밤",
|
||||
"introduction": "밤을 주제로 한 시리즈",
|
||||
"coverImageUrl": "https://cdn.example.com/series/701.png",
|
||||
"publishedDaysOfWeek": ["MON", "WED"],
|
||||
"genreId": 3,
|
||||
"keywords": ["밤", "산책"],
|
||||
"isAdult": false,
|
||||
"state": "PROCEEDING",
|
||||
"isActive": true,
|
||||
"writer": "루나",
|
||||
"studio": "소다라이브",
|
||||
"orders": 1
|
||||
}
|
||||
```
|
||||
|
||||
### 6.3 생성
|
||||
|
||||
`POST /api/v2/admin/ai-characters/{characterId}/series`
|
||||
|
||||
Content-Type: `multipart/form-data`
|
||||
|
||||
| Part | Content | 필수 |
|
||||
|---|---|---:|
|
||||
| `image` | File | 아니요 |
|
||||
| `request` | `application/json` | 예 |
|
||||
|
||||
`request`:
|
||||
|
||||
```ts
|
||||
{
|
||||
title: string
|
||||
introduction: string
|
||||
publishedDaysOfWeek: SeriesPublishedDay[]
|
||||
genreId: number
|
||||
keywords: string[]
|
||||
isAdult: boolean
|
||||
writer: string
|
||||
studio: string
|
||||
}
|
||||
```
|
||||
|
||||
- `state`와 `isActive`를 보내지 않는다.
|
||||
- 초기 state는 백엔드가 결정한다. 프론트엔드는 정확한 기본값에 의존하지 않고 생성 응답의 `state`를 그대로 표시한다.
|
||||
- Response `data`는 시리즈 상세와 같다.
|
||||
|
||||
### 6.4 수정·비활성화
|
||||
|
||||
`PUT /api/v2/admin/ai-characters/{characterId}/series/{seriesId}`
|
||||
|
||||
Content-Type: `multipart/form-data`
|
||||
|
||||
| Part | Content | 필수 |
|
||||
|---|---|---:|
|
||||
| `image` | 교체 File | 아니요 |
|
||||
| `request` | `application/json` | 예 |
|
||||
|
||||
`request`:
|
||||
|
||||
```ts
|
||||
{
|
||||
title: string
|
||||
introduction: string
|
||||
publishedDaysOfWeek: SeriesPublishedDay[]
|
||||
genreId: number
|
||||
keywords: string[]
|
||||
isAdult: boolean
|
||||
state?: "PROCEEDING" | "SUSPEND" | "COMPLETE"
|
||||
isActive?: false
|
||||
writer: string
|
||||
studio: string
|
||||
}
|
||||
```
|
||||
|
||||
- state를 변경하지 않을 때는 key를 생략한다. `null`을 보내지 않는다.
|
||||
- image 미전송은 기존 이미지 유지다.
|
||||
- 일반 수정에서는 `isActive`를 생략한다. `isActive=false`는 soft delete 전용이며 `true`는 보내지 않는다.
|
||||
- Response `data`는 시리즈 상세와 같다.
|
||||
- soft delete 성공 후 선택 캐릭터의 시리즈 active-only 목록으로 이동하고 성공 알림을 표시한다.
|
||||
|
||||
### 6.5 시리즈 콘텐츠 조회
|
||||
|
||||
`GET /api/v2/admin/ai-characters/{characterId}/series/{seriesId}/contents?search=밤&page=0&size=20`
|
||||
|
||||
Query:
|
||||
|
||||
```ts
|
||||
{
|
||||
search?: string
|
||||
page?: number
|
||||
size?: number
|
||||
}
|
||||
```
|
||||
|
||||
Response `data`:
|
||||
|
||||
```json
|
||||
{
|
||||
"totalCount": 1,
|
||||
"page": 0,
|
||||
"size": 20,
|
||||
"hasNext": false,
|
||||
"items": [
|
||||
{
|
||||
"contentId": 501,
|
||||
"title": "밤 산책",
|
||||
"coverImageUrl": "https://cdn.example.com/audio/501-cover.png",
|
||||
"isAdult": false,
|
||||
"orders": 1
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 6.6 시리즈 콘텐츠 연결
|
||||
|
||||
`POST /api/v2/admin/ai-characters/{characterId}/series/{seriesId}/contents`
|
||||
|
||||
Request body:
|
||||
|
||||
```json
|
||||
{
|
||||
"contentIds": [501, 502]
|
||||
}
|
||||
```
|
||||
|
||||
Response `data`는 시리즈 상세와 같다.
|
||||
|
||||
### 6.7 시리즈 콘텐츠 연결 해제
|
||||
|
||||
`DELETE /api/v2/admin/ai-characters/{characterId}/series/{seriesId}/contents/{contentId}`
|
||||
|
||||
- Request body 없음
|
||||
- Response `data`는 시리즈 상세와 같다.
|
||||
|
||||
### 6.8 시리즈 순서 변경
|
||||
|
||||
`PUT /api/v2/admin/ai-characters/{characterId}/series/orders`
|
||||
|
||||
Request body:
|
||||
|
||||
```json
|
||||
{
|
||||
"seriesIds": [701, 702, 703]
|
||||
}
|
||||
```
|
||||
|
||||
- 활성 시리즈 전체의 ID를 최종 순서대로 보낸다.
|
||||
- 50개를 초과할 때 전체를 읽는 방식, 누락 ID 오류, 동시 변경 충돌 처리는 백엔드 제공 대기 계약이다.
|
||||
- Response `data`는 시리즈 목록과 같은 page envelope다.
|
||||
|
||||
## 7. 커뮤니티 게시글
|
||||
|
||||
### 7.1 목록
|
||||
|
||||
`GET /api/v2/admin/ai-characters/{characterId}/community-posts?page=0&size=20`
|
||||
|
||||
- 활성 게시글만 반환한다. 활성 상태 filter/query는 없다.
|
||||
|
||||
Query:
|
||||
|
||||
```ts
|
||||
{
|
||||
page?: number
|
||||
size?: number
|
||||
}
|
||||
```
|
||||
|
||||
Response `data`:
|
||||
|
||||
```json
|
||||
{
|
||||
"totalCount": 1,
|
||||
"page": 0,
|
||||
"size": 20,
|
||||
"hasNext": false,
|
||||
"items": [
|
||||
{
|
||||
"postId": 801,
|
||||
"content": "오늘의 소식입니다.",
|
||||
"imageUrl": "https://cdn.example.com/community/801.png",
|
||||
"audioSignedUrl": null,
|
||||
"price": 0,
|
||||
"isAdult": false,
|
||||
"isFixed": true,
|
||||
"fixedAtUtc": "2026-07-24T00:00:00Z",
|
||||
"isActive": true,
|
||||
"createdAtUtc": "2026-07-24T00:00:00Z"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### 7.2 등록
|
||||
|
||||
`POST /api/v2/admin/ai-characters/{characterId}/community-posts`
|
||||
|
||||
Content-Type: `multipart/form-data`
|
||||
|
||||
| Part | Content | 필수 |
|
||||
|---|---|---:|
|
||||
| `image` | File | 아니요 |
|
||||
| `audioFile` | File | 아니요 |
|
||||
| `request` | `application/json` | 예 |
|
||||
|
||||
`request`:
|
||||
|
||||
```ts
|
||||
{
|
||||
content: string
|
||||
price: number
|
||||
isAdult: boolean
|
||||
isFixed: boolean
|
||||
}
|
||||
```
|
||||
|
||||
- `isActive`를 보내지 않는다.
|
||||
|
||||
Response `data`:
|
||||
|
||||
```json
|
||||
{
|
||||
"postId": 801,
|
||||
"content": "오늘의 소식입니다.",
|
||||
"imageUrl": "https://cdn.example.com/community/801.png",
|
||||
"audioSignedUrl": null,
|
||||
"price": 0,
|
||||
"isAdult": false,
|
||||
"isFixed": false,
|
||||
"fixedAtUtc": null,
|
||||
"isActive": true,
|
||||
"createdAtUtc": "2026-07-24T00:00:00Z",
|
||||
"updatedAtUtc": "2026-07-24T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
### 7.3 수정·고정·비활성화
|
||||
|
||||
`PUT /api/v2/admin/ai-characters/{characterId}/community-posts/{postId}`
|
||||
|
||||
Content-Type: `multipart/form-data`
|
||||
|
||||
| Part | Content | 필수 |
|
||||
|---|---|---:|
|
||||
| `image` | 교체 File | 아니요 |
|
||||
| `audioFile` | 교체 File | 아니요 |
|
||||
| `request` | `application/json` | 예 |
|
||||
|
||||
`request`:
|
||||
|
||||
```ts
|
||||
{
|
||||
content: string
|
||||
price: number
|
||||
isAdult: boolean
|
||||
isFixed: boolean
|
||||
isActive?: false
|
||||
}
|
||||
```
|
||||
|
||||
- 교체 File 미전송은 기존 media 유지다.
|
||||
- 일반 수정에서는 `isActive`를 생략한다. `isActive=false`는 soft delete 전용이며 이때 서버 응답은 `isFixed=false`, `fixedAtUtc=null`이어야 한다. `true`는 보내지 않는다.
|
||||
- Response `data`는 등록 응답과 같다.
|
||||
- soft delete 성공 후 열린 게시글 Sheet를 닫고 선택 캐릭터의 커뮤니티 게시글 active-only 목록을 무효화·재조회해 해당 항목을 제거하며 성공 알림을 표시한다.
|
||||
|
||||
### 7.4 프론트엔드 조회·갱신 규칙
|
||||
|
||||
- `GET /api/v2/admin/ai-characters/{characterId}/community-posts/{postId}`는 추가하거나 소비하지 않는다.
|
||||
- 커뮤니티 상세·수정 직접 route를 제공하지 않고 목록 응답의 item으로 목록 행/카드 Sheet를 초기화한다.
|
||||
- 조회·audio 재생·수정·고정·비활성화·댓글 진입에 목록 item의 `postId`와 필드를 사용한다.
|
||||
- 생성·수정 응답은 목록 cache를 갱신하는 데 사용한다.
|
||||
- 사용자 페이지 새로고침이나 mutation 후 cache 무효화 등 일반 목록 재조회에서는 새 `audioSignedUrl`을 사용한다. media error는 목록·상세 조회, URL 재발급 또는 자동 재생의 trigger가 아니다.
|
||||
|
||||
### 7.5 첨부 audio upload 규칙
|
||||
|
||||
커뮤니티의 optional `audioFile`은 오디오 콘텐츠의 `5.5 업로드 규칙`을 그대로 사용한다.
|
||||
|
||||
- 확장자: `.mp3`, `.aac`, `.m4a`
|
||||
- canonical MIME: `audio/mpeg`, `audio/aac`, `audio/mp4`
|
||||
- compatibility MIME: `audio/x-m4a`는 `.m4a` 파일에 한해 허용
|
||||
- 최대 파일 크기: decimal 1,024MB, `1,024,000,000 bytes` 이하
|
||||
- byte 경계: `1,024,000,000` 허용, `1,024,000,001` 거부
|
||||
- 최대 재생 길이: 제한 없음
|
||||
- WAV: 미지원
|
||||
- 확장자와 MIME 외에 실제 container/codec을 server가 검증한다.
|
||||
- `audio/x-m4a`도 실제 MP4/M4A container·codec 검증을 통과해야 하며 불일치는 `415`와 `common.error.invalid_request` envelope로 거부한다.
|
||||
|
||||
## 8. FanTalk 답변
|
||||
|
||||
### 8.1 답변 작성
|
||||
|
||||
`POST /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies`
|
||||
|
||||
Request body:
|
||||
|
||||
```json
|
||||
{
|
||||
"content": "응원해줘서 고마워요!"
|
||||
}
|
||||
```
|
||||
|
||||
Response `data`:
|
||||
|
||||
```json
|
||||
{
|
||||
"fanTalkId": 901,
|
||||
"replyId": 902,
|
||||
"creatorMemberId": 9001,
|
||||
"content": "응원해줘서 고마워요!",
|
||||
"createdAtUtc": "2026-07-24T00:00:00Z"
|
||||
}
|
||||
```
|
||||
|
||||
- 하나의 FanTalk에는 답변을 한 번만 생성할 수 있다.
|
||||
- 답변이 이미 있으면 수정만 가능하고 삭제는 범위 밖이다.
|
||||
- 백엔드가 유일성을 원자적으로 강제하는 방식과 중복 생성의 비2xx status/message key는 백엔드 제공 대기 계약이다. 프론트엔드에서 status를 추정하지 않는다.
|
||||
|
||||
## 9. 백엔드 제공 대기 계약
|
||||
|
||||
아래 항목은 프론트엔드 인터뷰의 Open Question이 아니라 백엔드 소유 계약이다. P0 계약을 제공받기 전에는 관련 network integration을 구현하지 않는다. P1은 제공 계약 없이 값을 추정하지 않고 출시 전 검증을 맞춘다.
|
||||
|
||||
| 우선순위 | 영역 | 필요한 계약 |
|
||||
|---:|---|---|
|
||||
| P0 | FanTalk | 목록·상세·답변 수정, 전체/미답변/답변 완료 filter, 최신순, reply uniqueness 오류 |
|
||||
| P0 | 댓글 | 오디오·커뮤니티의 2단계 목록·작성·수정·soft delete, 팬 댓글 삭제를 포함한 작성자별 권한 오류와 status/message key |
|
||||
| P0 | lookup | original work 이름 검색, genre 이름 검색, originalWork 미선택 `null`/omit |
|
||||
| P0 | Series 연결 후보 | 선택 캐릭터의 연결 가능한 활성 오디오 조회 |
|
||||
| P0 | Series | 전체 순서의 50개 초과 로딩·누락 ID·동시 변경 충돌 |
|
||||
| P1 | validation | price 최대값 |
|
||||
| P0 | 오류 | 신규 domain 오류별 비2xx status와 KO/EN/JA message key |
|
||||
| P2 | 감사 로그 | backend event schema. 관리자 조회 UI 여부는 PRD OQ-010에서 별도로 결정 |
|
||||
|
||||
## 10. UI 작성 후 확정할 validation
|
||||
|
||||
문자열 최대 길이와 배열 최대 개수는 초기 UI를 만든 뒤 페이지별로 검토해 결정한다. 제공된 계약에 명시되지 않은 최대값을 프론트엔드가 먼저 추정해 schema에 추가하지 않는다.
|
||||
|
||||
- 문자열 대상: Character `name`·`description`, Audio `title`·`description`, Series `title`·`introduction`·`keywords[]`·`writer`·`studio`, Community `content`, FanTalk reply `content`, 댓글 `content`
|
||||
- 배열 대상: Audio `seriesIds`, Series `publishedDaysOfWeek`·`keywords`·연결 `contentIds`·순서 `seriesIds`
|
||||
- UI 검토에서 권고값을 작성하고 백엔드 validation과 호환되는지 확인한 뒤 확정한다. 확정 시 이 문서, PRD, form schema와 최소/최대/초과 경계값 test를 같은 변경에서 갱신한다.
|
||||
894
docs/20260725_AI캐릭터관리자웹/plan-task.md
Normal file
894
docs/20260725_AI캐릭터관리자웹/plan-task.md
Normal file
@@ -0,0 +1,894 @@
|
||||
# AI 캐릭터 관리자 웹 구현 계획
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` (recommended) or `superpowers:executing-plans` to implement this plan Phase-by-Phase. 모든 구현 항목은 체크박스(`- [ ]`)로 추적한다.
|
||||
|
||||
**Goal:** ADMIN이 로그인한 뒤 AI 캐릭터를 선택하고, 선택한 캐릭터 문맥에서 Character·Audio·Series·Community·FanTalk·Comments를 안전하게 관리하는 독립 React SPA를 구현한다.
|
||||
|
||||
**Architecture:** 프로젝트 세팅과 공통 플랫폼·인증/인가를 먼저 완결한 뒤, 각 도메인을 API·상태·화면·오류·반응형·접근성·E2E까지 포함한 vertical slice로 구현한다. 모든 하위 리소스는 URL의 `characterId`를 기준으로 격리하고, 공통 API client가 envelope parsing, 인증 header, `Accept-Language: ko`, 401/403을 담당한다. 두 개 이상의 Phase에서 동일한 의미로 반복될 것이 확정된 UI·파일·미디어 컴포넌트는 Phase 1에서 먼저 만들고, 그 밖의 UI는 도메인 Phase 안에서 작게 나눈 뒤 실제 재사용 근거가 생길 때 shared로 올린다.
|
||||
|
||||
**Tech Stack:** React, TypeScript, Vite, Tailwind CSS, shadcn/ui, React Router, TanStack Query, React Hook Form, Zod, Axios/XHR upload adapter, date-fns/date-fns-tz, dnd-kit, Lucide React, Vitest, React Testing Library, MSW, Playwright, axe-core.
|
||||
|
||||
| 문서 항목 | 내용 |
|
||||
|---|---|
|
||||
| 상태 | 구현 전 재계획 완료 |
|
||||
| 최초 작성일 | 2026-07-25 |
|
||||
| 재작성일 | 2026-07-26 |
|
||||
| 요구사항 기준 | [prd.md](./prd.md) |
|
||||
| API 기준 | [api-contract.md](./api-contract.md) |
|
||||
|
||||
## 1. 전역 제약
|
||||
|
||||
- 이번 단계에서는 이 계획 문서만 수정한다. 애플리케이션 코드와 프로젝트 설정은 후속 구현 단계에서 변경한다.
|
||||
- PRD와 최초 API Contract가 충돌하면 PRD `11.4 API 계약 보정사항`을 우선한다.
|
||||
- 로그인은 `POST /admin/member/login`, 로그아웃은 body 없는 `POST /member/logout`을 사용한다.
|
||||
- JWT와 ADMIN role은 `sessionStorage`에만 저장한다. refresh token과 자동 갱신은 구현하지 않는다.
|
||||
- 모든 요청에 `Accept-Language: ko`를 보내고, 로그인 이외의 보호 요청과 로그아웃에 Bearer token을 보낸다.
|
||||
- `externalCharacterId`는 type, DTO, payload, fixture, UI에 만들지 않는다.
|
||||
- 모든 생성 payload에는 `isActive`를 넣지 않는다.
|
||||
- Character·Audio·Series·Community 일반 수정에는 `isActive`를 넣지 않고 soft delete에만 `isActive=false`를 보낸다. `isActive=true`, 복원, hard delete는 구현하지 않는다.
|
||||
- Character·Audio·Series·Community 목록은 active-only 서버 응답을 사용한다. 활성 상태 query나 client-side 활성 필터를 추가하지 않는다.
|
||||
- Character·Audio·Series soft delete 성공 후 해당 목록으로 이동한다. Community는 열린 Sheet를 닫고 현재 목록에서 제거한다. 모두 성공 알림을 표시한다.
|
||||
- 워크스페이스 상세 성공 응답의 Character가 `isActive=false`이면 모든 하위 mutation 진입점을 차단한다. soft delete 직후에는 목록 이동을 우선한다.
|
||||
- Series 생성 payload에는 `state`를 넣지 않는다. 수정에서 state를 바꾸지 않으면 key를 생략한다.
|
||||
- Series state에 `OPEN`, 요일에 `MONDAY` 같은 보정 전 enum을 사용하지 않는다.
|
||||
- multipart의 JSON part 이름은 `request`로 고정하고, optional 교체 파일을 보내지 않으면 기존 media를 유지한다.
|
||||
- 모든 일반 목록은 server pagination을 사용한다. 검색을 제공하는 목록은 약 300ms debounce를 일관되게 적용하고 URL query와 기존 화면 데이터를 유지한다.
|
||||
- image 영역은 비율과 크기를 예약하고 목록 image는 lazy load한다. 날짜·가격 공통 formatter는 Phase 1에서 만들고 상태 label은 각 도메인이 `StatusBadge`에 주입한다.
|
||||
- 모바일 기능 범위는 PRD `9`를 각 도메인 Phase에서 함께 구현한다. 반응형 정책을 마지막에 덧붙이지 않는다.
|
||||
- 초기 릴리스는 밝은 테마만 제공한다. main/primary는 `#00BDF7`, primary foreground는 `#062B36`, 흰 배경의 link/focus ring은 `#007EA8`이다.
|
||||
- JWT, password, signed URL, 업로드 파일 본문을 console, 분석 이벤트, 오류 리포트, 영구 저장소에 기록하지 않는다.
|
||||
- 제공 계약에 없는 문자열·배열 최대값, price 상한, 오류 status/key를 추정하지 않는다.
|
||||
- 새 dependency는 해당 Phase에서 실제로 필요한 최소 항목만 추가한다. shadcn component와 폴더도 소비 시점에 생성한다.
|
||||
- Page는 routing·query·permission·component 조합만 담당한다. 도메인 표시와 상호작용은 feature component로, 두 Phase 이상에서 의미와 동작이 같은 것은 shared component로 분리한다.
|
||||
- 단순 markup 한 조각, 한 화면 전용 UI, 서로 다른 도메인 규칙을 하나의 범용 prop API로 합치기 위한 component는 만들지 않는다.
|
||||
- 모든 기능은 실패하는 test를 먼저 만들고 최소 구현으로 통과시킨다.
|
||||
|
||||
## 2. Phase 운영 규칙
|
||||
|
||||
### 2.1 독립 검증 규칙
|
||||
|
||||
각 Phase는 다음 결과를 모두 가진다.
|
||||
|
||||
1. 사용자가 직접 확인할 수 있는 하나 이상의 완결된 흐름
|
||||
2. 해당 Phase가 소유하는 API contract test
|
||||
3. loading·empty·error·success 상태
|
||||
4. 해당 viewport 범위와 keyboard·접근성 검증
|
||||
5. Phase 전용 E2E와 공통 typecheck·lint·build 결과
|
||||
|
||||
각 Task는 다음 Red/Green loop를 따른다.
|
||||
|
||||
1. Task에 적힌 test file에 가장 작은 실패 test를 추가한다.
|
||||
2. 해당 test만 실행해 의도한 assertion 실패인지 확인한다.
|
||||
3. 그 test를 통과시키는 최소 구현을 작성한다.
|
||||
4. 관련 feature test 전체를 실행한다.
|
||||
5. refactor 후 typecheck·lint와 Phase E2E를 다시 실행한다.
|
||||
|
||||
import 오류, test 환경 오류, 임시 mock 누락 같은 우발적 실패는 Red 증거로 인정하지 않는다.
|
||||
|
||||
### 2.2 컴포넌트 조합 규칙
|
||||
|
||||
각 도메인 Phase는 코드를 작성하기 전에 다음 component map을 해당 Phase의 `주요 Files`와 checklist에 반영한다.
|
||||
|
||||
| 분류 | 책임 | 예시 |
|
||||
|---|---|---|
|
||||
| Page | route param/query, data loading, permission, navigation, component 조합 | `CharacterListPage`, `AudioContentDetailPage` |
|
||||
| Feature component | 한 도메인의 표시·입력·상호작용 규칙 | `CharacterForm`, `PublishedDaysField` |
|
||||
| Shared component | 두 개 이상 Phase에서 같은 의미·동작으로 재사용 | `PageState`, `ConfirmDeactivateDialog`, `ImageCropDialog` |
|
||||
| shadcn primitive | 접근 가능한 저수준 control | `Button`, `Dialog`, `Table`, `Form` |
|
||||
|
||||
- 화면별 loading·empty·error·success·read-only·mobile 상태와 주요 action을 먼저 inventory한다.
|
||||
- Page에 큰 JSX와 form/media 로직을 직접 쌓지 않고, 독립 test가 가능한 feature/shared component를 조합한다.
|
||||
- 기존 shadcn/shared component로 표현할 수 있으면 새 wrapper를 만들지 않는다.
|
||||
- shared component는 domain DTO나 endpoint를 import하지 않고 controlled value, slot, callback으로 조합한다.
|
||||
- 재사용을 위해 boolean prop를 계속 늘리기보다 작은 component와 composition을 사용한다.
|
||||
- component map과 실제 화면 구성이 달라지면 구현 전에 이 계획의 `주요 Files`를 먼저 갱신한다.
|
||||
|
||||
### 2.3 미결·외부 의존 처리
|
||||
|
||||
전역 Backend Contract Phase를 만들지 않는다. 각 도메인 Phase의 첫 Task에서 그 도메인에 필요한 계약만 확인한다.
|
||||
|
||||
| 분류 | 처리 규칙 |
|
||||
|---|---|
|
||||
| 계약이 제공됨 | `api-contract.md`에 request/response/error 예시를 반영하고 contract test를 만든 뒤 구현한다. |
|
||||
| 안전한 확정 기본값이 있음 | 문서에 적힌 최소 규칙만 구현한다. 예: price 상한 미제공 시 `0 이상 정수`만 검증한다. |
|
||||
| 계약 없이 안전하게 구현할 수 없음 | endpoint·DTO·오류를 추측하지 않는다. 해당 최소 기능 또는 Phase를 현재 릴리스에서 제외하기 전에 PRD 결정 기록, API Contract, 이 계획을 함께 갱신한다. |
|
||||
| 구현 중 불필요하다고 판단 | 활성 체크 항목을 제거하되 PRD 결정 기록에 삭제 이유와 날짜를 남긴다. 과거 결정 기록은 지우지 않는다. |
|
||||
| 계약이 후속 도착 | 완료한 Phase를 묵시적으로 다시 열지 않고 별도 후속 vertical slice를 계획한다. |
|
||||
|
||||
- `OQ-009`는 각 도메인의 실제 폼을 만든 시점에 한 번만 판단한다. 최대값이 필요하면 backend 호환 확인 후 PRD·API Contract·schema·경계 test를 같은 변경에서 갱신한다. 필요 없으면 “상한 추가 없음”으로 종결하고 관련 구현 항목을 삭제한다.
|
||||
- `OQ-010` 감사 로그 조회 UI는 현재 릴리스 구현 항목을 만들지 않는다. 포함하기로 바뀌면 backend 조회 계약을 포함한 별도 Phase로 다시 계획한다.
|
||||
- P0 외부 의존이 남아 있으면 영향을 받는 network flow를 완료로 표시하지 않는다. 다른 독립 Phase는 계속 진행할 수 있다.
|
||||
- 이미지 최대 `10MB`의 정확한 byte 경계처럼 표현만으로 단일 값이 정해지지 않는 항목은 첫 파일 Phase에서 결정 기록과 contract를 먼저 보정한다.
|
||||
|
||||
## 3. Phase 지도
|
||||
|
||||
| Phase | 결과 | 선행조건 | 독립 검증 핵심 |
|
||||
|---:|---|---|---|
|
||||
| 0 | 프로젝트 세팅 | 없음 | fresh install, root smoke, unit/E2E/build |
|
||||
| 1 | 공통 플랫폼·인증/인가·컴포넌트 기반 | Phase 0 | shared component contract + login → protected shell → refresh restore → logout/401/403 |
|
||||
| 2 | Character workspace | Phase 1 | list/search → create/select → detail/edit → deactivate |
|
||||
| 3 | Audio vertical slice | Phase 2의 workspace core | list/filter/detail/play → create/edit/upload → deactivate |
|
||||
| 4 | Series vertical slice | Phase 3의 Audio 조회 API | CRUD → content link/unlink → full reorder |
|
||||
| 5 | Community vertical slice | Phase 3의 media/file primitive | list → collection Sheet edit/pin → media play → deactivate |
|
||||
| 6 | FanTalk vertical slice | Phase 2 | list/filter/detail → one reply → edit |
|
||||
| 7 | Comments vertical slice | Phase 3 + Phase 5 | Audio/Community thread → permission별 CRUD |
|
||||
| 8 | 교차 회귀·인수인계 | 활성 범위의 Phase 0~7 | 전체 journey, viewport, axe, security, build |
|
||||
|
||||
기본 진행 순서는 Phase 번호를 따른다. 다만 Phase 4·5·6은 자신의 선행조건과 계약이 충족되면 병행할 수 있고, 외부 계약으로 막힌 Phase가 다른 독립 Phase를 막지 않는다.
|
||||
|
||||
```text
|
||||
Phase 0 Setup
|
||||
└─ Phase 1 Platform + Auth/Authz + Shared Components
|
||||
└─ Phase 2 Character Workspace
|
||||
├─ Phase 3 Audio ──┬─ Phase 4 Series
|
||||
│ └─ Phase 5 Community ──┐
|
||||
└─ Phase 6 FanTalk ├─ Phase 8 Final
|
||||
Phase 3 + Phase 5 ── Phase 7 Comments ┘
|
||||
```
|
||||
|
||||
## 4. 파일 책임 지도
|
||||
|
||||
```text
|
||||
src/
|
||||
app/ # entry, providers, router, route constants
|
||||
components/ui/ # 실제로 추가한 shadcn primitive만 보관
|
||||
features/
|
||||
auth/
|
||||
characters/
|
||||
audio-contents/
|
||||
series/
|
||||
community-posts/
|
||||
fan-talks/
|
||||
comments/
|
||||
layouts/ # Admin shell, Character workspace
|
||||
shared/
|
||||
api/ # envelope, client, query, pagination, errors
|
||||
config/ # runtime env
|
||||
hooks/ # 공통 query/form interaction hook
|
||||
lib/ # 공통 formatter/media helper
|
||||
test/ # MSW, render helper, fixtures
|
||||
ui/ # Phase 1에서 확정한 공통 UI와 후속 검증된 추출물
|
||||
validation/ # 공통 file/media policy
|
||||
styles/
|
||||
tests/e2e/
|
||||
```
|
||||
|
||||
feature 내부의 `api/`, `components/`, `model/`, `pages/`, `schemas/`, `tests/`는 실제 파일이 생길 때만 만든다. 한 구현만을 위한 interface, factory, registry는 만들지 않는다. 모든 Page는 해당 feature의 component와 shared component를 조합해 구성한다.
|
||||
|
||||
---
|
||||
|
||||
## Phase 0. 프로젝트 세팅
|
||||
|
||||
**목표:** 비즈니스 기능 없이도 동일한 명령으로 개발·test·build할 수 있는 React SPA 기반을 만든다.
|
||||
|
||||
**주요 Files:**
|
||||
|
||||
- Create: `package.json`, `package-lock.json`
|
||||
- Create: `index.html`, `vite.config.ts`
|
||||
- Create: `tsconfig.json`, `tsconfig.app.json`, `tsconfig.node.json`
|
||||
- Create: `eslint.config.js`
|
||||
- Create: `.env.example`, `README.md`
|
||||
- Create: `playwright.config.ts`
|
||||
- Create: `src/main.tsx`, `src/app/App.tsx`, `src/app/App.test.tsx`
|
||||
- Create: `src/shared/config/env.ts`, `src/shared/config/env.test.ts`
|
||||
- Create: `src/shared/test/setup.ts`
|
||||
- Create: `tests/e2e/smoke.spec.ts`
|
||||
|
||||
### Task 0.1 런타임·패키지 기반
|
||||
|
||||
- [ ] `mise.toml`의 Node `24.12.0`을 기준으로 npm package와 lockfile을 생성한다.
|
||||
- [ ] React + TypeScript + Vite 진입점과 `@` path alias를 구성한다.
|
||||
- [ ] `VITE_API_BASE_URL`만 `.env.example`에 문서화하고 token·password 같은 비밀값을 넣지 않는다.
|
||||
- [ ] runtime env 누락·잘못된 URL을 앱 시작 전에 설명 가능한 오류로 차단하는 test를 작성한다.
|
||||
- [ ] unit test는 `vi.stubEnv`, Playwright webServer는 명시적 test URL로 `VITE_API_BASE_URL`을 주입해 `.env.example` 자동 로드를 전제하지 않는다.
|
||||
- [ ] `dev`, `build`, `typecheck`, `lint`, `test`, `test:run`, `e2e` script를 정의한다.
|
||||
- [ ] 이 Phase에 필요하지 않은 router, server-state, form, drag-and-drop dependency는 아직 설치하지 않는다.
|
||||
|
||||
### Task 0.2 test 기반
|
||||
|
||||
- [ ] Vitest, jsdom, React Testing Library, jest-dom을 구성한다.
|
||||
- [ ] 각 test 뒤 DOM·mock·storage가 정리되는 공통 setup을 만든다.
|
||||
- [ ] Playwright에 desktop Chromium/WebKit과 mobile Chrome/Safari viewport project, Vite webServer를 구성한다.
|
||||
- [ ] fresh environment에서 Chromium/WebKit browser binary를 설치하는 명령을 README와 Gate에 포함한다.
|
||||
- [ ] `<html lang="ko">`, `main` landmark, root content를 확인하는 unit test를 먼저 실패시킨 뒤 최소 App shell을 만든다.
|
||||
- [ ] 동일 shell이 각 Playwright project에서 열리는 smoke E2E를 만든다.
|
||||
|
||||
### Phase 0 Gate
|
||||
|
||||
```bash
|
||||
npm ci
|
||||
npx playwright install chromium webkit
|
||||
export VITE_API_BASE_URL=http://127.0.0.1:4010
|
||||
npm run typecheck
|
||||
npm run lint
|
||||
npm run test:run -- src/app/App.test.tsx src/shared/config/env.test.ts
|
||||
npm run e2e -- tests/e2e/smoke.spec.ts
|
||||
npm run build
|
||||
```
|
||||
|
||||
**Expected:** fresh install 후 typecheck·lint·unit·E2E·production build가 모두 0 exit code이며 환경 변수 오류가 test로 고정된다.
|
||||
|
||||
---
|
||||
|
||||
## Phase 1. 공통 플랫폼·인증/인가·컴포넌트 기반
|
||||
|
||||
**목표:** ADMIN 인증 흐름과 보호된 Admin shell을 완결하고, Phase 2 이후 화면이 조합해 사용할 공통 UI·form·file·media component contract를 제공한다.
|
||||
|
||||
**요구사항:** `AUTH-001~013`, `UX-001~002`, PRD `7`, `10.1~10.8`, `11.1~11.2`, `12`의 공통 항목.
|
||||
|
||||
**주요 Files:**
|
||||
|
||||
- Create: `components.json`, `src/styles/globals.css`
|
||||
- Create: `src/app/providers.tsx`, `src/app/router.tsx`, `src/app/route-paths.ts`
|
||||
- Create: `src/shared/api/{types,api-error,client,pagination,query-client}.ts`
|
||||
- Create: `src/shared/api/__tests__/{client,pagination}.test.ts`
|
||||
- Create: `src/shared/test/{server,handlers,render}.ts`
|
||||
- Create: `src/shared/hooks/use-search-params-state.ts`
|
||||
- Create: `src/shared/ui/{page-state,status-badge,page-header,search-toolbar,resource-pagination,responsive-resource-list}.tsx`
|
||||
- Create: `src/shared/ui/{confirm-deactivate-dialog,unsaved-changes-guard,file-field,image-crop-dialog,upload-progress}.tsx`
|
||||
- Create: `src/shared/ui/{admin-audio-player,audio-playback-provider}.tsx`
|
||||
- Create: `src/shared/validation/{file-validation,image-policy,audio-file-policy}.ts`
|
||||
- Create: `src/shared/validation/{file-validation,image-policy,audio-file-policy}.test.ts`
|
||||
- Create: `src/shared/lib/{crop-image,format-date,format-price}.ts`
|
||||
- Create: `src/shared/lib/{crop-image,format-date,format-price}.test.ts`
|
||||
- Create: `src/shared/ui/__tests__/{page-state,resource-list,confirm-deactivate-dialog,unsaved-changes-guard}.test.tsx`
|
||||
- Create: `src/shared/ui/__tests__/{file-field,image-crop-dialog,upload-progress,admin-audio-player}.test.tsx`
|
||||
- Create: `src/features/auth/api/auth-api.ts`
|
||||
- Create: `src/features/auth/model/auth-session.tsx`
|
||||
- Create: `src/features/auth/model/auth-session-storage.ts`
|
||||
- Create: `src/features/auth/schemas/login-schema.ts`
|
||||
- Create: `src/features/auth/pages/{LoginPage,AccessDeniedPage}.tsx`
|
||||
- Create: `src/features/auth/components/ProtectedRoute.tsx`
|
||||
- Create: `src/features/auth/tests/auth-api.test.ts`
|
||||
- Create: `src/features/auth/tests/{login-page,protected-route}.test.tsx`
|
||||
- Create: `src/layouts/AdminLayout.tsx`
|
||||
- Create: `tests/e2e/auth.spec.ts`, `tests/e2e/accessibility-shell.spec.ts`
|
||||
|
||||
**Phase 1 공통 컴포넌트 소비처 Matrix:**
|
||||
|
||||
| Shared component | 확정 소비 Phase | 공통 책임 |
|
||||
|---|---|---|
|
||||
| `PageHeader`, `PageState` | 1~7 | 제목·action slot, loading·empty·error·retry |
|
||||
| `StatusBadge` | 2~6 | semantic tone과 label/icon slot |
|
||||
| `SearchToolbar` | 2, 3, 4 | controlled search, filter slot, debounce callback |
|
||||
| `ResourcePagination` | 2~7 | `PageData` 기반 page/size control |
|
||||
| `ResponsiveResourceList` | 2~6 | desktop/mobile rendering slot |
|
||||
| `ConfirmDeactivateDialog` | 2~5 | 대상명·영향·확인 callback |
|
||||
| `UnsavedChangesGuard` | 2~7 | dirty form route 이탈 확인 |
|
||||
| `FileField`, `ImageCropDialog` | 2~5 | file input과 주입된 crop policy 실행 |
|
||||
| `UploadProgress` | 3, 5 | 진행률·취소·재시도 callback |
|
||||
| `AdminAudioPlayer`, `AudioPlaybackProvider` | 3, 5 | native audio control과 단일 재생 상태 |
|
||||
|
||||
이 표는 PRD에 이미 확정된 반복 소비만 포함한다. 각 도메인 Phase의 component map은 이 배치를 검증하고 domain component 구성을 추가하며, shared component의 존재 근거를 새로 만드는 선행조건이 아니다.
|
||||
|
||||
### Task 1.1 최소 디자인 시스템
|
||||
|
||||
- [ ] PRD `10.9`의 `ui-ux-pro-max` design-system 검색을 실행하고 관리자 제품에 맞는 채택·제외 결과를 작업 기록에 남긴다.
|
||||
- [ ] Tailwind CSS와 shadcn/ui CSS variable mode를 구성한다.
|
||||
- [ ] `brand-500=#00BDF7`, hover `#00A9DE`, active `#009DCE`, primary foreground `#062B36`, link/ring `#007EA8`을 primitive → semantic → component token으로 연결한다.
|
||||
- [ ] 핵심 foreground 대비, control boundary 3:1, primary 위 흰색 금지를 token test로 고정한다.
|
||||
- [ ] 밝은 `:root` token만 만들고 `.dark`, ThemeProvider, theme toggle, system dark 연동이 없음을 test한다.
|
||||
- [ ] Korean system font stack, mobile input 16px, 44px target, focus ring, reduced motion, semantic z-index를 base style에 둔다.
|
||||
- [ ] 상태 Badge가 text label을 포함하고 색상만으로 상태를 전달하지 않는 test를 작성한다.
|
||||
- [ ] icon-only action에는 accessible name과 Tooltip이 있고 필수 control boundary·focus indicator가 인접 배경 대비 3:1 이상인지 test한다.
|
||||
- [ ] Auth와 공통 상태에 실제 필요한 shadcn component만 추가한다. 전체 component를 선행 scaffold하지 않는다.
|
||||
|
||||
### Task 1.2 공통 API·server state
|
||||
|
||||
- [ ] `AuthSessionRecord = { token: string; role: "ADMIN" }`의 읽기·저장·제거 adapter를 먼저 만들고, API client는 React provider가 아니라 이 adapter의 token reader/clear callback에만 의존한다.
|
||||
- [ ] `ApiResponse<T>` 성공형이 `errorProperty` 생략과 `null`을 모두 수용하고 오류형은 비2xx status·`message`·`errorProperty`를 보존하는 test를 작성한다.
|
||||
- [ ] `PageData<T>`와 `page=0`, `size=20`, size 최소 20·최대 50 보정을 test한다. 문서에 없는 음수 page 동작은 추정하지 않는다.
|
||||
- [ ] 모든 요청에 `Accept-Language: ko`를 붙이고 로그인 요청에는 Authorization을 제외하는 test를 작성한다.
|
||||
- [ ] 보호 요청과 logout에만 현재 session의 Bearer token을 붙이는 test를 작성한다.
|
||||
- [ ] 400/404/405/415/500의 서버 한국어 message를 공통 `ApiError`가 보존하는 MSW test를 작성한다.
|
||||
- [ ] 동시에 여러 401이 와도 session clear·알림·login redirect가 한 번만 발생하는 test를 작성한다.
|
||||
- [ ] 403은 session을 지우지 않고 AccessDenied 상태로 전달하는 test를 작성한다.
|
||||
- [ ] logger가 JWT, password, signed URL, multipart body를 받지 않는 test를 작성한다.
|
||||
- [ ] TanStack Query provider와 공통 retry 정책을 구성하되 401/403과 mutation을 무조건 재시도하지 않는다.
|
||||
|
||||
### Task 1.3 로그인·session·logout
|
||||
|
||||
- [ ] email 형식, password 필수, visible label, 오류 연결, first-invalid-focus test를 작성한다.
|
||||
- [ ] login이 `POST /admin/member/login`에 `{ email, password }` JSON만 보내는 contract test를 작성한다.
|
||||
- [ ] 응답의 `data.token`과 `data.role="ADMIN"`만 유효 session으로 인정한다.
|
||||
- [ ] 성공 session을 `sessionStorage`에만 저장하고 같은 탭 새로고침에서 복원하는 test를 작성한다.
|
||||
- [ ] token 누락, role 누락·비ADMIN이면 보호 route를 렌더링하지 않고 저장도 하지 않는 test를 작성한다.
|
||||
- [ ] `localStorage`, IndexedDB, cookie에 인증 정보가 기록되지 않는 test를 작성한다.
|
||||
- [ ] refresh endpoint 호출이 0건임을 확인한다.
|
||||
- [ ] logout이 Bearer header와 body 없이 `POST /member/logout`을 한 번 호출하는 test를 작성한다.
|
||||
- [ ] logout 성공·비2xx·network error 모두 local session을 제거하고 `/login`으로 이동하며, 실패 때만 서버 확인 실패 경고를 표시하고 session을 복원하지 않는 test를 작성한다.
|
||||
|
||||
### Task 1.4 보호 route·Admin shell
|
||||
|
||||
- [ ] 미인증 사용자가 보호 content를 한 프레임도 보지 않고 `/login`으로 이동하는 test를 작성한다.
|
||||
- [ ] 401은 session 제거 후 login, 403은 AccessDeniedPage로 가는 route test를 작성한다.
|
||||
- [ ] desktop sidebar, mobile Sheet, header, logout, breadcrumb, skip link, `main` landmark를 구현한다.
|
||||
- [ ] keyboard로 login, navigation, logout을 완료하고 dialog/menu focus가 trigger로 복귀하는 test를 작성한다.
|
||||
- [ ] `/ai-characters`에는 Phase 2가 교체할 명시적 빈 route state만 두고 가짜 도메인 데이터를 만들지 않는다.
|
||||
- [ ] 320px과 200% zoom에서 shell overflow와 가려진 control이 없는지 E2E로 확인한다.
|
||||
- [ ] shell route의 axe critical·serious 위반 0건을 확인한다.
|
||||
|
||||
### Task 1.5 공통 화면·form component
|
||||
|
||||
- [ ] 위 소비처 Matrix를 contract test와 component API에 대조하고 모든 shared component가 두 Phase 이상에서 같은 의미로 사용되는지 확인한다.
|
||||
- [ ] `PageState`가 loading·empty·error·retry를 접근 가능한 status/alert와 keyboard action으로 표현하는 test를 작성한다.
|
||||
- [ ] `StatusBadge`가 domain label·icon/보조 문구를 slot으로 받고 색상만으로 상태를 전달하지 않는 test를 작성한다.
|
||||
- [ ] `SearchToolbar`는 controlled search/filter slot과 약 300ms debounce·URL query callback만 제공하고 특정 endpoint query를 알지 않게 한다.
|
||||
- [ ] `ResourcePagination`은 공통 `PageData`로 page/size를 제어하고 disabled·accessible name·keyboard 동작을 제공한다.
|
||||
- [ ] `ResponsiveResourceList`는 desktop/mobile rendering slot만 제공하고 domain column·DTO·action을 prop union으로 내장하지 않는다.
|
||||
- [ ] `ConfirmDeactivateDialog`는 대상명·영향 설명·확인 callback을 조합하고 Switch로 대체되지 않게 test한다.
|
||||
- [ ] `UnsavedChangesGuard`는 dirty 상태에서만 route 이탈을 확인하고 저장 성공 후 해제되며 focus를 trigger로 복귀한다.
|
||||
- [ ] UTC 시각의 Asia/Seoul 표시와 0 이상 정수 “캔” 표시를 공통 formatter로 고정하고 domain status label은 formatter에 넣지 않는다.
|
||||
- [ ] 각 component는 독립 RTL test를 먼저 통과시킨 뒤 Admin shell에서 최소 한 번 실제 조합해 integration test를 작성한다.
|
||||
|
||||
### Task 1.6 공통 file·media component
|
||||
|
||||
- [ ] image `10MB`의 정확한 byte 기준을 backend와 맞춰 PRD·API Contract·경계 test에 기록한다. 미확정이면 `FileField`는 주입된 `maxBytes`만 검증하고 도메인 정책 완료를 주장하지 않는다.
|
||||
- [ ] `FileField`는 visible label, 설명·오류 연결, accept 안내, keyboard activation, 선택 취소와 controlled `File | null` contract만 제공한다.
|
||||
- [ ] 공통 file validation은 주입된 allowed extension·MIME·maxBytes를 함께 확인한다. JPEG/PNG·GIF 같은 resource별 allowed set은 각 도메인 policy가 소유한다.
|
||||
- [ ] 공통 audio policy는 MP3/AAC/M4A, `.m4a + audio/x-m4a`, `1,024,000,000 bytes` 경계, WAV 거부를 표현하되 실제 container·codec을 client에서 판정하지 않는다.
|
||||
- [ ] `ImageCropDialog`는 주입된 aspect/max width 정책으로 이동·zoom·reset·preview·취소·적용·keyboard/button 대안·no-upscale 결과를 제공한다.
|
||||
- [ ] crop interaction은 검증된 단일 dependency가 native pointer/Canvas 직접 구현보다 코드·접근성 위험을 줄이는지 확인해 하나만 선택하고, Canvas는 결과 File 생성에만 사용한다. 선택 근거는 작업 기록에 남긴다.
|
||||
- [ ] 공통 image policy는 `aspect`, `maxWidth`, `noUpscale`, crop 적용 여부를 받는 domain-neutral contract만 정의한다. Character·Audio·Series·Community profile과 GIF 예외는 각 feature Phase가 소유한다.
|
||||
- [ ] `UploadProgress`는 진행률·취소·재시도 callback과 상태 표시만 담당하고 Axios request나 domain form을 직접 소유하지 않는다.
|
||||
- [ ] `AdminAudioPlayer`는 native audio를 감싸 play/pause·seek·time·volume·speed·keyboard·일반 오류·수동 재시도를 제공하고 download와 자동 refetch/자동 play를 만들지 않는다.
|
||||
- [ ] `AudioPlaybackProvider`가 동시에 하나의 player만 재생되게 하며 signed URL을 log·storage에 전달하지 않는 test를 작성한다.
|
||||
- [ ] 공통 file/media component는 endpoint·query cache·domain DTO를 import하지 않는 dependency test 또는 review checklist를 통과한다.
|
||||
|
||||
### Phase 1 Gate
|
||||
|
||||
```bash
|
||||
npm run test:run -- src/shared src/features/auth src/layouts
|
||||
npm run e2e -- tests/e2e/auth.spec.ts tests/e2e/accessibility-shell.spec.ts
|
||||
npm run typecheck
|
||||
npm run lint
|
||||
npm run build
|
||||
```
|
||||
|
||||
**Expected:** 공통 component contract가 독립 test로 고정되고, ADMIN login → 보호 shell → 새로고침 session 복원 → logout이 동작하며 비ADMIN·stale claim·401·logout 실패 경로가 독립적으로 검증된다.
|
||||
|
||||
---
|
||||
|
||||
## Phase 2. Character workspace vertical slice
|
||||
|
||||
**목표:** ADMIN이 active Character를 검색·생성·선택하고 workspace에서 상세·수정·soft delete까지 완료한다.
|
||||
|
||||
**요구사항:** `CHAR-001~014`, `FILE-001~002`, `FILE-008~010`, `FILE-012`, PRD `7`, `9`의 Character 범위.
|
||||
|
||||
**외부 의존:** `CHAR-013` original work lookup·미선택 직렬화, 신규 Character 오류 계약.
|
||||
|
||||
**주요 Files:**
|
||||
|
||||
- Create: `src/features/characters/api/character-api.ts`
|
||||
- Create: `src/features/characters/model/types.ts`
|
||||
- Create: `src/features/characters/schemas/character-schema.ts`
|
||||
- Create: `src/features/characters/validation/character-image-policy.ts`
|
||||
- Create: `src/features/characters/pages/{CharacterListPage,CharacterDetailPage,CharacterFormPage}.tsx`
|
||||
- Create: `src/features/characters/components/{CharacterList,CharacterListItem,CharacterProfile,CharacterForm,CharacterImageField}.tsx`
|
||||
- Create when `CHAR-013` contract is available: `src/features/characters/components/OriginalWorkCombobox.tsx`
|
||||
- Create: `src/features/characters/tests/character-api.test.ts`
|
||||
- Create: `src/features/characters/tests/{character-list,character-form}.test.tsx`
|
||||
- Create: `src/layouts/CharacterWorkspaceLayout.tsx`
|
||||
- Create: `src/layouts/CharacterWorkspaceLayout.test.tsx`
|
||||
- Create: `tests/e2e/character-workspace.spec.ts`
|
||||
- Modify: `src/app/router.tsx`, `src/app/route-paths.ts`
|
||||
|
||||
### Task 2.1 Phase 계약 확인
|
||||
|
||||
- [ ] original work lookup endpoint, DTO, search/page, 미선택 `null`/omit canonical form을 확인한다.
|
||||
- [ ] 계약이 없으면 original work network control과 serializer를 추측하지 않고, 현재 slice에서 제외할 범위를 PRD·API Contract·plan에 먼저 기록한다.
|
||||
- [ ] Character 도메인 오류의 비2xx status와 message key를 contract fixture에 기록한다.
|
||||
- [ ] 목록·상세·form·workspace의 상태/action inventory를 작성하고 Page는 route/query/policy 조합, feature component는 Character 표시·입력 규칙, shared component는 Phase 1 contract를 사용하도록 component map을 확정한다.
|
||||
|
||||
### Task 2.2 목록·선택·workspace
|
||||
|
||||
- [ ] Character 목록·생성 path에는 `characterId`가 없고 하위 resource route에만 선택한 `characterId`가 들어가는 contract test를 작성한다.
|
||||
- [ ] active-only 목록의 `search`, `page`, `size` URL query 보존과 loading·empty·error·retry test를 작성한다.
|
||||
- [ ] 목록 request에 `isActive`·`activeStatus`가 없고 client-side 활성 filter도 없는 contract test를 작성한다.
|
||||
- [ ] Character 선택 시 URL의 `characterId`로 workspace에 진입하고 새로고침·deep link가 동작하는 test를 작성한다.
|
||||
- [ ] workspace header에 image, name, active 상태, `characterId`와 탭·breadcrumb를 표시한다.
|
||||
- [ ] 상세 성공 응답이 `isActive=false`이면 read-only 배너와 중앙 write policy로 모든 mutation 진입점을 차단한다.
|
||||
- [ ] 상세 400/404/500은 공통 오류 화면을 사용하고 비활성 ID 응답 정책을 client가 추정하지 않는다.
|
||||
|
||||
### Task 2.3 생성·수정·soft delete
|
||||
|
||||
- [ ] create multipart가 `request` JSON part와 optional image만 보내며 `isActive`, `externalCharacterId`를 포함하지 않는 test를 작성한다.
|
||||
- [ ] 일반 update는 `isActive`를 생략하고 soft delete만 `isActive=false`를 보내며 `true`를 보내지 않는 test를 작성한다.
|
||||
- [ ] name·description visible label, field error, 중복 제출 방지, dirty-form 이탈 확인을 test한다.
|
||||
- [ ] Character image의 JPEG/PNG·10MB, `1:1`, 최대 800×800, no-upscale, crop 이동·zoom·reset·preview·취소·적용·keyboard 대안을 test한다.
|
||||
- [ ] crop 취소·교체 파일 미선택이 기존 image를 유지하고 기존 image 제거 UI는 없음을 test한다.
|
||||
- [ ] original work 계약이 제공됐다면 이름 검색 Combobox와 canonical 미선택 payload를 contract test로 고정한다.
|
||||
- [ ] creator member ID·nickname 등 응답 정보는 read-only로 표시하고 creator 생성·동기화를 client가 수행하지 않는다.
|
||||
- [ ] 저장 성공 후 server response로 list/detail cache를 갱신한다.
|
||||
- [ ] 비활성화 AlertDialog가 영향·복원 미지원·hard delete 미지원을 설명하는 test를 작성한다.
|
||||
- [ ] soft delete 성공 후 active-only 목록 재조회, 목록 이동, 성공 toast를 확인하고 상세에 머물지 않는다.
|
||||
- [ ] Character form을 실제로 작성한 뒤 `OQ-009`의 `name`·`description` 상한 필요성을 판단하고, 구현 전 결정 문서를 갱신하거나 “상한 추가 없음”으로 종결한다.
|
||||
|
||||
### Task 2.4 Character 반응형·접근성
|
||||
|
||||
- [ ] desktop/tablet에서는 전체 관리 action을 제공한다.
|
||||
- [ ] mobile에서는 목록·검색·상세만 제공하고 create/edit/deactivate route 직접 진입도 desktop 안내로 종료한다.
|
||||
- [ ] Table이 mobile Card로 바뀌어도 동일한 accessible name과 핵심 상태를 유지한다.
|
||||
- [ ] keyboard-only로 search → select → tabs → form → dialog를 완료한다.
|
||||
- [ ] 320/768/1280px, 200% zoom, axe critical·serious 0건을 Phase E2E에서 확인한다.
|
||||
|
||||
### Phase 2 Gate
|
||||
|
||||
```bash
|
||||
npm run test:run -- src/features/characters src/layouts/CharacterWorkspaceLayout.test.tsx
|
||||
npm run e2e -- tests/e2e/character-workspace.spec.ts
|
||||
npm run typecheck
|
||||
npm run lint
|
||||
npm run build
|
||||
```
|
||||
|
||||
**Expected:** login → Character 검색/생성 → 선택/workspace → 수정 → soft delete → active-only 목록 복귀가 한 slice로 통과한다. original work 계약이 없으면 그 기능의 제외 결정과 문서가 명시돼야 하며 완료로 가장하지 않는다.
|
||||
|
||||
---
|
||||
|
||||
## Phase 3. Audio vertical slice
|
||||
|
||||
**목표:** 선택 Character의 Audio를 검색·검수·발행·수정·비활성화하고 대용량 upload를 안전하게 제어한다.
|
||||
|
||||
**요구사항:** `AUDIO-001~026`, `FILE-001~002`, `FILE-006~009`, `FILE-012~013`, PRD `9`의 Audio 범위.
|
||||
|
||||
**외부 의존:** Audio 도메인 오류 계약, optional P1 price 상한. price 상한이 없으면 `0 이상 정수`만 적용한다.
|
||||
|
||||
**주요 Files:**
|
||||
|
||||
- Create: `src/features/audio-contents/api/{audio-content-api,series-options-api,upload-audio-content}.ts`
|
||||
- Create: `src/features/audio-contents/model/types.ts`
|
||||
- Create: `src/features/audio-contents/schemas/audio-content-schema.ts`
|
||||
- Create: `src/features/audio-contents/validation/audio-cover-policy.ts`
|
||||
- Create: `src/features/audio-contents/pages/{AudioContentListPage,AudioContentDetailPage,AudioContentFormPage}.tsx`
|
||||
- Create: `src/features/audio-contents/components/{AudioContentList,AudioContentListItem,AudioContentSummary,AudioContentForm,ReleaseScheduleField,SeriesMultiCombobox}.tsx`
|
||||
- Create: `src/features/audio-contents/tests/{audio-contract,audio-upload}.test.ts`
|
||||
- Create: `src/features/audio-contents/tests/{audio-list,audio-player,audio-form}.test.tsx`
|
||||
- Create: `tests/e2e/audio-content.spec.ts`
|
||||
|
||||
### Task 3.1 Phase 계약 확인
|
||||
|
||||
- [ ] Audio 신규 오류 status/message key와 backend container·codec 오류 fixture를 기록한다.
|
||||
- [ ] price 최대값이 제공되면 schema와 경계 test를 추가하고, 없으면 상한을 만들지 않는다.
|
||||
- [ ] status query 미전송 시 server가 결과 집합을 결정한다는 계약을 유지하고 client fixture에서 임의 집합을 강제하지 않는다.
|
||||
- [ ] 목록·상세·player·form/upload의 상태/action inventory를 작성하고 Page는 route/query/policy 조합, feature component는 Audio 규칙, shared component는 Phase 1 contract를 사용하도록 component map을 확정한다.
|
||||
|
||||
### Task 3.2 목록·상세·player
|
||||
|
||||
- [ ] status type과 filter가 `OPEN | SCHEDULED`만 허용하고 서버 값을 client가 재계산하지 않는 test를 작성한다.
|
||||
- [ ] 검색·status·page URL 보존, active-only request, loading·empty·error·retry를 test한다.
|
||||
- [ ] Audio detail route의 직접 진입과 새로고침에서 같은 resource를 복원하는 test를 작성한다.
|
||||
- [ ] 목록과 상세가 Phase 1 `AdminAudioPlayer`를 조합하고 play/pause, seek, current/duration, volume, speed, keyboard를 지원하는 integration test를 작성한다.
|
||||
- [ ] 한 player 재생 시 기존 player가 정지되고 명시적 download button이 없음을 test한다.
|
||||
- [ ] media error를 signed URL 만료로 추정하지 않고 일반 오류·수동 재시도·페이지 새로고침 안내를 표시한다.
|
||||
- [ ] media error만으로 목록/detail GET과 `play()`가 자동 재호출되지 않는 test를 작성한다.
|
||||
- [ ] signed URL이 log·storage·분석 event로 전달되지 않는 test를 작성한다.
|
||||
|
||||
### Task 3.3 발행 form·upload
|
||||
|
||||
- [ ] 생성은 cover image와 audio file 필수, 수정 교체 파일은 optional이며 미전송 시 기존 media 유지임을 test한다.
|
||||
- [ ] Audio cover가 Phase 1 `FileField`·`ImageCropDialog`의 JPEG/PNG·10MB, `1:1`, 최대 800px, no-upscale profile을 조합하는 test를 작성한다.
|
||||
- [ ] MP3/AAC/M4A 허용, WAV 거부, extension/MIME 조합을 test한다.
|
||||
- [ ] `.m4a + audio/x-m4a`만 호환 조합으로 허용하고 실제 container·codec 판정은 server 책임으로 둔다.
|
||||
- [ ] `1,024,000,000 bytes` 허용, `1,024,000,001 bytes` 거부 경계 test를 작성한다.
|
||||
- [ ] price는 0 이상 정수 “캔”으로 입력·format한다.
|
||||
- [ ] 즉시 공개 기본값은 날짜 입력을 비활성화·초기화하고 `releaseDateUtc=null`을 보낸다.
|
||||
- [ ] 예약 공개는 미래 Asia/Seoul 시각만 받고 UTC ISO-8601 `Z`로 변환하는 test를 작성한다.
|
||||
- [ ] 수정 form은 server `releaseDateUtc/status`로 초기화하고 사용자가 바꾸지 않으면 기존 값을 유지한다.
|
||||
- [ ] 제공된 active Series 목록 endpoint를 사용하는 options request와 `seriesIds` 다중 선택·keyboard 제거를 test한다.
|
||||
- [ ] create payload에 `status`, `isActive`가 없고 update/soft delete의 `isActive` 규칙이 지켜지는 contract test를 작성한다.
|
||||
- [ ] upload 진행률, AbortController 취소, 전체 재시도, 실패 후 form/file 상태 보존을 test한다.
|
||||
- [ ] 415 server 오류를 field 안내로 보존하고 resumable upload는 만들지 않는다.
|
||||
- [ ] 저장 성공은 detail/list cache를 갱신하고 soft delete 성공은 active-only 목록 이동과 toast로 끝낸다.
|
||||
- [ ] Audio form을 실제로 작성한 뒤 `OQ-009`의 `title`·`description`·`seriesIds` 상한 필요성을 판단하고, 구현 전 결정 문서를 갱신하거나 “상한 추가 없음”으로 종결한다.
|
||||
|
||||
### Task 3.4 Audio 반응형·접근성
|
||||
|
||||
- [ ] mobile에서는 목록·상세·player만 제공하고 create/edit/deactivate/upload는 숨김이 아닌 route capability로 차단한다.
|
||||
- [ ] desktop/tablet에서는 모든 form·upload action을 제공한다.
|
||||
- [ ] 320px에서 player control, error text, 긴 title이 overflow하지 않는 E2E를 작성한다.
|
||||
- [ ] keyboard-only player/form, 200% zoom, axe critical·serious 0건을 확인한다.
|
||||
|
||||
### Phase 3 Gate
|
||||
|
||||
```bash
|
||||
npm run test:run -- src/features/audio-contents src/shared/ui src/shared/validation
|
||||
npm run e2e -- tests/e2e/audio-content.spec.ts
|
||||
npm run typecheck
|
||||
npm run lint
|
||||
npm run build
|
||||
```
|
||||
|
||||
**Expected:** Audio 즉시/예약 생성 → 진행률/취소/재시도 → 목록·상세 재생 → 수정 → soft delete가 통과하고 media error가 자동 refetch·자동 재생을 0회 발생시킨다.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4. Series vertical slice
|
||||
|
||||
**목표:** 선택 Character의 Series를 생성·수정·비활성화하고 Audio 연결·해제와 활성 Series 전체 순서를 관리한다.
|
||||
|
||||
**요구사항:** `SERIES-001~013`, `FILE-001~002`, `FILE-005`, `FILE-007~009`, `FILE-012`, `FILE-015`, PRD `9`의 Series 범위.
|
||||
|
||||
**외부 의존:** genre lookup(`SERIES-011`), 연결 후보, 50개 초과 전체 로딩, 누락 ID, 동시 충돌, 신규 오류 계약.
|
||||
|
||||
**주요 Files:**
|
||||
|
||||
- Create: `src/features/series/api/series-api.ts`
|
||||
- Create: `src/features/series/model/types.ts`
|
||||
- Create: `src/features/series/schemas/series-schema.ts`
|
||||
- Create: `src/features/series/validation/series-image-policy.ts`
|
||||
- Create: `src/features/series/pages/{SeriesListPage,SeriesDetailPage,SeriesFormPage,SeriesOrderPage}.tsx`
|
||||
- Create: `src/features/series/components/{SeriesList,SeriesListItem,SeriesSummary,SeriesForm,PublishedDaysField,GenreCombobox,SeriesContents,SeriesOrderList}.tsx`
|
||||
- Create: `src/features/series/tests/series-contract.test.ts`
|
||||
- Create: `src/features/series/tests/{series-form,series-contents,series-order}.test.tsx`
|
||||
- Create: `tests/e2e/series.spec.ts`
|
||||
|
||||
### Task 4.1 Phase 계약 확인
|
||||
|
||||
- [ ] genre lookup endpoint·DTO·search/page 계약을 기록한다.
|
||||
- [ ] 선택 Character의 연결 가능한 active Audio 후보 계약을 기록한다.
|
||||
- [ ] 활성 Series가 50개를 넘을 때 전체를 누락 없이 읽는 방식과 누락 ID·동시 변경 충돌 오류를 기록한다.
|
||||
- [ ] 계약이 없는 연결·순서·genre 기능은 추측 구현하지 않고 제외/후속 여부를 문서에서 먼저 결정한다.
|
||||
- [ ] 목록·상세·form·연결·순서 화면의 상태/action inventory를 작성하고 Page는 route/query/policy 조합, feature component는 Series 규칙, shared component는 Phase 1 contract를 사용하도록 component map을 확정한다.
|
||||
|
||||
### Task 4.2 Series CRUD
|
||||
|
||||
- [ ] list가 active-only이며 활성 query를 보내지 않고 loading·empty·error·retry를 제공하는 test를 작성한다.
|
||||
- [ ] Series detail route의 직접 진입과 새로고침에서 같은 resource를 복원하는 test를 작성한다.
|
||||
- [ ] enum은 `PROCEEDING | SUSPEND | COMPLETE`, 요일은 `SUN~SAT | RANDOM`만 허용한다.
|
||||
- [ ] 생성 form에 state 입력이 없고 payload에도 `state`, `isActive`가 없음을 test한다.
|
||||
- [ ] 수정에서 state 미선택은 key 생략, 선택은 유효 enum만 전송하고 `null`은 보내지 않는다.
|
||||
- [ ] `RANDOM`은 단독, 실제 요일은 하나 이상이어야 하는 schema·UI test를 작성한다.
|
||||
- [ ] genre 계약이 제공됐다면 이름 검색 후 `genreId`만 전송하는 Combobox를 test한다.
|
||||
- [ ] Series image JPEG/PNG·10MB, `210:297`, `height=round(width×297÷210)`, 최대 1000×1414, 1px 오차, no-upscale을 test한다.
|
||||
- [ ] 일반 update와 soft delete의 `isActive` 규칙, soft delete 후 목록 이동·toast를 test한다.
|
||||
|
||||
### Task 4.3 Audio 연결·해제·전체 순서
|
||||
|
||||
- [ ] 현재 연결 Audio 목록의 search/page와 상세 cache 동기화를 test한다.
|
||||
- [ ] 후보는 선택 Character의 active Audio로 제한하고 이미 연결된 항목을 중복 선택하지 않는다.
|
||||
- [ ] 연결 POST는 `{ contentIds }`, 해제 DELETE는 body 없음임을 contract test로 고정한다.
|
||||
- [ ] 연결 해제 전 대상 title과 영향을 AlertDialog로 확인한다.
|
||||
- [ ] 순서 mode는 active Series 전체를 읽고 최종 순서의 모든 `seriesIds`를 한 번에 보낸다.
|
||||
- [ ] drag-and-drop과 동일한 결과를 keyboard·위/아래 button으로 만들 수 있는 test를 작성한다.
|
||||
- [ ] server의 누락 ID·동시 충돌 오류에서 기존 화면 순서를 보존하고 재조회/재시도 안내를 제공한다.
|
||||
- [ ] Series form·연결·순서 UI를 실제로 작성한 뒤 `OQ-009`의 title·introduction·keywords·writer·studio·days·contentIds·seriesIds 상한 필요성을 판단하고, 구현 전 결정 문서를 갱신하거나 “상한 추가 없음”으로 종결한다.
|
||||
|
||||
### Task 4.4 Series 반응형·접근성
|
||||
|
||||
- [ ] mobile은 목록·상세·연결 콘텐츠 조회만 제공하고 CRUD·연결·순서 action을 route capability로 차단한다.
|
||||
- [ ] desktop/tablet에서 전체 관리 흐름을 제공한다.
|
||||
- [ ] keyboard-only 요일·genre·연결·정렬, 320px 조회, 200% zoom, axe critical·serious 0건을 확인한다.
|
||||
|
||||
### Phase 4 Gate
|
||||
|
||||
```bash
|
||||
npm run test:run -- src/features/series
|
||||
npm run e2e -- tests/e2e/series.spec.ts
|
||||
npm run typecheck
|
||||
npm run lint
|
||||
npm run build
|
||||
```
|
||||
|
||||
**Expected:** Series 생성 → 수정 → Audio 연결/해제 → active 전체 reorder → soft delete가 통과하고 잘못된 enum·부분 순서 payload가 생성되지 않는다.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5. Community vertical slice
|
||||
|
||||
**목표:** 별도 상세 route/GET 없이 active Community 목록과 Sheet만으로 게시글 등록·조회·수정·고정·비활성화·첨부 재생을 완료한다.
|
||||
|
||||
**요구사항:** `COMMUNITY-001~011`, `FILE-001~004`, `FILE-007~009`, `FILE-011~014`, PRD `9`의 Community 범위.
|
||||
|
||||
**외부 의존:** Community 신규 오류 계약, optional P1 price 상한. Comments는 Phase 7에서 연결한다.
|
||||
|
||||
**주요 Files:**
|
||||
|
||||
- Create: `src/features/community-posts/api/community-post-api.ts`
|
||||
- Create: `src/features/community-posts/model/types.ts`
|
||||
- Create: `src/features/community-posts/schemas/community-post-schema.ts`
|
||||
- Create: `src/features/community-posts/validation/community-media-policy.ts`
|
||||
- Create: `src/features/community-posts/pages/CommunityPostListPage.tsx`
|
||||
- Create: `src/features/community-posts/components/{CommunityPostList,CommunityPostListItem,CommunityPostForm,CommunityPostSheet}.tsx`
|
||||
- Create: `src/features/community-posts/tests/community-contract.test.ts`
|
||||
- Create: `src/features/community-posts/tests/{community-list,community-sheet}.test.tsx`
|
||||
- Create: `tests/e2e/community-post.spec.ts`
|
||||
|
||||
### Task 5.1 Phase 계약 확인
|
||||
|
||||
- [ ] Community 오류 status/message key와 media upload 오류 fixture를 기록한다.
|
||||
- [ ] price 최대값이 제공되면 Audio와 같은 정책으로 갱신하고, 없으면 0 이상 정수만 유지한다.
|
||||
- [ ] 목록·Sheet·form/media의 상태/action inventory를 작성하고 Page는 collection query/policy 조합, feature component는 Community 규칙, shared component는 Phase 1 contract를 사용하도록 component map을 확정한다.
|
||||
|
||||
### Task 5.2 목록·collection Sheet
|
||||
|
||||
- [ ] active-only 목록의 `page/size`, loading·empty·error·retry와 URL query 보존을 test한다. 제공 계약에 없는 Community `search` query나 현재 page 한정 client 검색은 만들지 않는다.
|
||||
- [ ] 목록 item을 source로 Sheet를 열고 전용 detail GET을 0회 호출하는 test를 작성한다.
|
||||
- [ ] `/community-posts/:postId`, `/edit` route가 존재하지 않는 router test를 작성한다.
|
||||
- [ ] Sheet의 조회·수정·고정/해제·비활성화가 목록 cache와 같은 server response를 사용한다.
|
||||
- [ ] 고정/해제 후 `isFixed/fixedAtUtc`를 server 값으로 표시한다.
|
||||
- [ ] soft delete 응답의 `isFixed=false`, `fixedAtUtc=null`을 contract test로 고정한다.
|
||||
- [ ] soft delete 성공 시 Sheet 종료, active-only 목록 재조회·항목 제거, 성공 toast를 확인한다.
|
||||
|
||||
### Task 5.3 게시글 form·첨부 media
|
||||
|
||||
- [ ] 생성 payload에 `isActive`가 없고 일반 update/soft delete가 공통 `isActive` 규칙을 지키는 test를 작성한다.
|
||||
- [ ] content, price 0 이상 정수, isAdult, isFixed와 optional image/audio를 test한다.
|
||||
- [ ] JPEG/PNG는 자유 ratio crop·최대 800px·no-upscale을 적용한다.
|
||||
- [ ] GIF는 Community에서만 허용하고 crop Dialog/canvas/re-encode 없이 원본 ratio·animation을 유지한다.
|
||||
- [ ] GIF 원본 width 800px은 허용하고 801px은 제출 전에 거부한다.
|
||||
- [ ] 첨부 Audio는 Phase 1의 공통 audio file policy와 `FileField`를 Phase 3과 동일하게 조합해 MP3/AAC/M4A, x-m4a, `1,024,000,000 bytes`, WAV 거부 규칙을 재사용한다.
|
||||
- [ ] 첨부 Audio가 있으면 목록 Card/row와 Sheet에 공통 player를 렌더링한다.
|
||||
- [ ] media error가 detail/list refetch·URL 재발급·자동 play를 발생시키지 않는다.
|
||||
- [ ] 사용자 새로고침이나 mutation cache invalidation으로 목록이 정상 재조회된 때만 새 `audioSignedUrl`을 사용한다.
|
||||
- [ ] Community Sheet/form을 실제로 작성한 뒤 `OQ-009`의 `content` 상한 필요성을 판단하고, 구현 전 결정 문서를 갱신하거나 “상한 추가 없음”으로 종결한다.
|
||||
|
||||
### Task 5.4 Community 반응형·접근성
|
||||
|
||||
- [ ] mobile은 목록·Sheet 조회·첨부 재생만 제공하고 등록·수정·고정·비활성화를 route/action policy로 차단한다.
|
||||
- [ ] desktop/tablet에는 전체 관리 흐름을 제공한다.
|
||||
- [ ] Sheet focus trap/복귀, keyboard media/form, 320px overflow, 200% zoom, axe critical·serious 0건을 확인한다.
|
||||
|
||||
### Phase 5 Gate
|
||||
|
||||
```bash
|
||||
npm run test:run -- src/features/community-posts src/shared/validation
|
||||
npm run e2e -- tests/e2e/community-post.spec.ts
|
||||
npm run typecheck
|
||||
npm run lint
|
||||
npm run build
|
||||
```
|
||||
|
||||
**Expected:** create → 목록 item Sheet 조회/수정 → pin/unpin → 첨부 재생 → soft delete가 통과하고 Community detail GET·detail route 호출은 0건이다.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6. FanTalk vertical slice
|
||||
|
||||
**목표:** 모든 viewport에서 FanTalk를 최신순·답변 상태로 조회하고 답변을 한 번 작성한 뒤 기존 답변만 수정한다.
|
||||
|
||||
**요구사항:** `FANTALK-001~008`, PRD `9`의 FanTalk 범위.
|
||||
|
||||
**외부 의존:** 목록·상세·답변 수정 endpoint/DTO, filter/sort, reply uniqueness의 원자적 강제와 중복 오류 계약. 핵심 계약이 없으면 이 Phase 전체를 추측 구현하지 않는다.
|
||||
|
||||
**주요 Files:**
|
||||
|
||||
- Create: `src/features/fan-talks/api/fan-talk-api.ts`
|
||||
- Create: `src/features/fan-talks/model/types.ts`
|
||||
- Create: `src/features/fan-talks/schemas/fan-talk-reply-schema.ts`
|
||||
- Create: `src/features/fan-talks/pages/{FanTalkListPage,FanTalkDetailPage}.tsx`
|
||||
- Create: `src/features/fan-talks/components/{FanTalkList,FanTalkListItem,FanTalkDetail,FanTalkReplyForm}.tsx`
|
||||
- Create: `src/features/fan-talks/tests/fan-talk-contract.test.ts`
|
||||
- Create: `src/features/fan-talks/tests/{fan-talk-list,fan-talk-reply}.test.tsx`
|
||||
- Create: `tests/e2e/fan-talk.spec.ts`
|
||||
- Modify: `src/app/router.tsx`
|
||||
|
||||
### Task 6.1 Phase 계약 확인
|
||||
|
||||
- [ ] 목록·상세·답변 수정 endpoint, request/response DTO, page/filter/latest sort, ownership error를 기록한다.
|
||||
- [ ] 답변 1개를 server가 원자적으로 강제하는 방식과 중복 생성 비2xx status/message key를 기록한다.
|
||||
- [ ] 계약이 없으면 임시 endpoint·placeholder DTO·mock production adapter를 만들지 않고 Phase 제외/후속 결정을 문서화한다.
|
||||
- [ ] 목록·상세·reply form의 상태/action inventory를 작성하고 Page는 route/query/policy 조합, feature component는 FanTalk 규칙, shared component는 Phase 1 contract를 사용하도록 component map을 확정한다.
|
||||
|
||||
### Task 6.2 목록·답변 생성·수정
|
||||
|
||||
- [ ] 기본 목록은 최신순 전체이며 전체/미답변/답변 완료 filter와 page를 URL에 보존한다.
|
||||
- [ ] loading·empty·error·retry와 direct detail/refresh를 test한다.
|
||||
- [ ] 답변이 없을 때만 POST form을, 있으면 edit form만 표시하고 delete UI는 만들지 않는다.
|
||||
- [ ] 빠른 두 번 제출에도 POST가 한 번만 호출되는 test를 작성한다.
|
||||
- [ ] 답변 수정은 제공된 endpoint/reply identity만 사용한다.
|
||||
- [ ] server 중복 오류를 받으면 최신 detail을 재조회해 edit 상태로 전환하고 status/key를 추정 분기하지 않는다.
|
||||
- [ ] 저장 중 중복 제출 차단, visible label, 오류 연결, 성공 live feedback을 test한다.
|
||||
- [ ] FanTalk reply form을 실제로 작성한 뒤 `OQ-009`의 `content` 상한 필요성을 판단하고, 구현 전 결정 문서를 갱신하거나 “상한 추가 없음”으로 종결한다.
|
||||
|
||||
### Task 6.3 FanTalk 반응형·접근성
|
||||
|
||||
- [ ] desktop/tablet/mobile 모두 조회·답변 작성·수정을 제공한다.
|
||||
- [ ] 320px에서 keyboard가 reply input/submit을 가리지 않는 E2E를 작성한다.
|
||||
- [ ] keyboard-only filter/detail/create/edit, 200% zoom, axe critical·serious 0건을 확인한다.
|
||||
|
||||
### Phase 6 Gate
|
||||
|
||||
```bash
|
||||
npm run test:run -- src/features/fan-talks
|
||||
npm run e2e -- tests/e2e/fan-talk.spec.ts
|
||||
npm run typecheck
|
||||
npm run lint
|
||||
npm run build
|
||||
```
|
||||
|
||||
**Expected:** 미답변 조회 → 답변 1회 생성 → 기존 답변 수정이 모든 viewport에서 통과하며 두 번째 reply 생성과 delete UI가 없다.
|
||||
|
||||
---
|
||||
|
||||
## Phase 7. Comments vertical slice
|
||||
|
||||
**목표:** Audio detail과 Community Sheet 양쪽에서 같은 2단계 댓글 UX를 제공하고 작성자별 수정·soft delete 권한을 일관되게 적용한다.
|
||||
|
||||
**요구사항:** `COMMENT-001~006`, PRD `9`의 Comments 범위.
|
||||
|
||||
**외부 의존:** Audio·Community 댓글 목록/작성/수정/soft delete endpoint·DTO, 2단계 강제, fan 댓글 삭제 권한 오류. 핵심 계약이 없으면 이 Phase 전체를 추측 구현하지 않는다.
|
||||
|
||||
**주요 Files:**
|
||||
|
||||
- Create: `src/features/comments/api/comment-api.ts`
|
||||
- Create: `src/features/comments/model/{types,comment-target}.ts`
|
||||
- Create: `src/features/comments/schemas/comment-schema.ts`
|
||||
- Create: `src/features/comments/components/{CommentThread,CommentForm,CommentActions,CommunityPostCommentsSheet}.tsx`
|
||||
- Create: `src/features/comments/tests/comment-contract.test.ts`
|
||||
- Create: `src/features/comments/tests/{comment-thread,comment-permissions}.test.tsx`
|
||||
- Create: `tests/e2e/comments.spec.ts`
|
||||
- Modify: `AudioContentDetailPage.tsx`, `CommunityPostSheet.tsx`
|
||||
|
||||
### Task 7.1 Phase 계약 확인
|
||||
|
||||
- [ ] Audio·Community target별 endpoint, query, DTO, page, 작성/수정/soft delete 응답을 기록한다.
|
||||
- [ ] root/direct reply 정확히 2단계인 server rule과 fan content 삭제 권한 오류 status/message key를 기록한다.
|
||||
- [ ] 계약이 없으면 target endpoint를 이름만 보고 추정하거나 client-only permission을 완료로 간주하지 않는다.
|
||||
- [ ] Audio detail·Community Sheet 진입별 thread/form/action inventory를 작성하고 host Page/Sheet는 target·query 조합, Comments component는 thread·permission 규칙, shared component는 Phase 1 contract를 사용하도록 component map을 확정한다.
|
||||
|
||||
### Task 7.2 target adapter·2단계 thread
|
||||
|
||||
- [ ] UI target은 Audio와 Community를 명시적으로 구분하고 각 제공 endpoint로만 요청하는 contract test를 작성한다.
|
||||
- [ ] Community 상세 GET 없이 목록 item의 `characterId/postId`로 Comments Sheet를 연다.
|
||||
- [ ] Comments Sheet 종료 후 Community page·scroll 상태를 보존한다.
|
||||
- [ ] root와 direct reply만 렌더링하고 reply에는 reply action이 없음을 test한다.
|
||||
- [ ] long content, loading·empty·error·retry, page 갱신을 양 target에서 test한다.
|
||||
|
||||
### Task 7.3 작성자별 action
|
||||
|
||||
- [ ] AI Character 작성 root/reply에는 edit·soft delete를 제공한다.
|
||||
- [ ] fan 작성 root/reply에는 edit를 제공하지 않고 운영 soft delete만 제공한다.
|
||||
- [ ] fan edit request는 type과 UI 양쪽에서 생성할 수 없음을 test한다.
|
||||
- [ ] delete 전 대상과 영향을 확인하고 server 계약에 따라 tombstone 또는 목록 갱신을 적용한다.
|
||||
- [ ] Character workspace read-only 정책이 모든 comment mutation도 차단하는 test를 작성한다.
|
||||
- [ ] 중복 제출, server permission 오류, session 401/403이 공통 정책을 따르는지 test한다.
|
||||
- [ ] Comment thread/form을 실제로 작성한 뒤 `OQ-009`의 `content` 상한 필요성을 판단하고, 구현 전 결정 문서를 갱신하거나 “상한 추가 없음”으로 종결한다.
|
||||
|
||||
### Task 7.4 Comments 반응형·접근성
|
||||
|
||||
- [ ] desktop/tablet/mobile 모두 조회·작성·수정·soft delete를 제공한다.
|
||||
- [ ] 320px에서 긴 댓글, reply indentation, action menu, keyboard 입력이 overflow하지 않는 E2E를 작성한다.
|
||||
- [ ] keyboard-only root/reply 작성·수정·delete dialog, focus 복귀, 200% zoom, axe critical·serious 0건을 확인한다.
|
||||
|
||||
### Phase 7 Gate
|
||||
|
||||
```bash
|
||||
npm run test:run -- src/features/comments
|
||||
npm run e2e -- tests/e2e/comments.spec.ts
|
||||
npm run typecheck
|
||||
npm run lint
|
||||
npm run build
|
||||
```
|
||||
|
||||
**Expected:** Audio와 Community 두 진입점에서 2단계 댓글 CRUD·권한·모바일 흐름이 통과하고 reply의 reply 및 fan edit request는 생성되지 않는다.
|
||||
|
||||
---
|
||||
|
||||
## Phase 8. 교차 회귀·인수인계
|
||||
|
||||
**목표:** 새 기능을 추가하지 않고 활성 릴리스 범위 전체가 PRD, API Contract, 보안, 반응형, 접근성 기준을 만족한다는 최신 증거를 남긴다.
|
||||
|
||||
**주요 Files:**
|
||||
|
||||
- Create: `tests/e2e/{resource-workflows,error-mapping,responsive-capabilities,accessibility}.spec.ts`
|
||||
- Modify: `README.md`
|
||||
- Modify: `docs/20260725_AI캐릭터관리자웹/{prd.md,api-contract.md,plan-task.md}` only when actual implementation decision differs.
|
||||
|
||||
### Task 8.1 교차 journey·오류 회귀
|
||||
|
||||
- [ ] login → Character select → Audio immediate/scheduled create/play → Series link/order → Community Sheet → FanTalk → Comments의 활성 범위 journey를 검증한다.
|
||||
- [ ] 400/401/403/404/405/415/500 fixture가 공통 한국어 message와 올바른 route/session 처리를 하는지 검증한다.
|
||||
- [ ] Character·Audio·Series soft delete는 목록 이동, Community soft delete는 Sheet 종료·목록 제거로 끝나는지 검증한다.
|
||||
- [ ] inactive Character workspace에서 모든 하위 mutation request가 0건인지 검증한다.
|
||||
- [ ] media error로 Audio/Community GET·URL 재발급·자동 `play()`가 발생하지 않는지 검증한다.
|
||||
- [ ] 생성·일반 수정·soft delete serializer 불변식을 모든 도메인 fixture에서 다시 검증한다.
|
||||
|
||||
### Task 8.2 반응형·접근성·보안 회귀
|
||||
|
||||
- [ ] PRD `9` 기능 matrix를 table-driven E2E data로 검증한다.
|
||||
- [ ] 320, 640, 768, 1024, 1280px와 landscape에서 overflow·가려진 keyboard·44px target을 확인한다.
|
||||
- [ ] desktop Chrome/Edge/Safari와 mobile Chrome/Safari 최신 2개 주요 버전 범위를 실제 지원 환경에서 확인한다.
|
||||
- [ ] 모든 핵심 route에서 axe critical·serious 위반 0건을 확인한다.
|
||||
- [ ] keyboard-only, first-error-focus, dialog focus 복귀, skip link, live region, reduced motion, 200% zoom을 수동 검증한다.
|
||||
- [ ] system dark mode에서도 밝은 token을 유지하고 theme toggle이 없음을 검증한다.
|
||||
- [ ] JWT, password, signed URL, file body가 log·storage·분석 event에 남지 않는지 검증한다.
|
||||
- [ ] 각 Page가 승인된 component map대로 route/query/permission과 component 조합만 담당하고, domain 상호작용이 feature/shared component test로 분리됐는지 review한다.
|
||||
- [ ] PRD `10.9`의 UX 검증 검색을 다시 실행하고 채택·제외 결과를 기록한다.
|
||||
|
||||
### Task 8.3 문서·품질 Gate
|
||||
|
||||
- [ ] 활성 범위의 P0 외부 의존이 0건인지, 아니면 구현 전에 명시적으로 후속/제외 결정됐는지 확인한다.
|
||||
- [ ] `OQ-009`를 각 도메인별 확정 또는 “상한 추가 없음”으로 종결하고 중복 checklist를 남기지 않는다.
|
||||
- [ ] `OQ-010` 감사 로그 UI가 현재 릴리스 non-goal임을 결정 기록과 맞춘다.
|
||||
- [ ] 실제 구현과 다른 결정은 PRD 결정 기록 → API Contract → plan 순으로 갱신한다.
|
||||
- [ ] PRD 수용 기준마다 자동 test 또는 수동 검증 증거를 연결한다.
|
||||
- [ ] README에 install, env, run, test, build, 지원 브라우저, 알려진 backend 제약을 기록한다.
|
||||
- [ ] plan 하단 검증 기록에 무엇을/왜/어떻게와 실제 명령·성공/실패/불가 사유를 누적한다.
|
||||
- [ ] 별도 code review를 받고 지적사항 수정 후 관련 Phase Gate와 전체 Gate를 다시 실행한다.
|
||||
|
||||
### Phase 8 Gate
|
||||
|
||||
```bash
|
||||
set -e
|
||||
|
||||
assert_no_match() {
|
||||
if rg -n "$@"; then
|
||||
return 1
|
||||
else
|
||||
rg_status=$?
|
||||
[ "$rg_status" -eq 1 ]
|
||||
fi
|
||||
}
|
||||
|
||||
npm ci
|
||||
npm run typecheck
|
||||
npm run lint
|
||||
npm run test:run
|
||||
npm run e2e
|
||||
npm run build
|
||||
assert_no_match 'TODO|TBD|FIXME' src tests
|
||||
assert_no_match "externalCharacterId|SUNDAY|MONDAY|TUESDAY|WEDNESDAY|THURSDAY|FRIDAY|SATURDAY|state.?[:=].?['\\\"]OPEN" src \
|
||||
--glob '!**/*.test.*' --glob '!**/*.spec.*' --glob '!**/__tests__/**'
|
||||
```
|
||||
|
||||
**Expected:** 전체 자동 Gate가 0 failure/0 error이고, `assert_no_match`는 no-match인 `rg` exit 1만 성공으로 바꾸며 `rg` 실행 오류는 실패로 전파한다. production source의 금지 값과 미완료 placeholder는 0건이어야 한다. 부정 test fixture의 금지 문자열은 허용하며 production 결과와 구분한다.
|
||||
|
||||
## 5. 요구사항 추적표
|
||||
|
||||
| Phase | PRD 범위 | 집중 test |
|
||||
|---:|---|---|
|
||||
| 0 | React+TypeScript+Vite, 지원 브라우저 기반 | `src/app/App.test.tsx`, `tests/e2e/smoke.spec.ts` |
|
||||
| 1 | `AUTH-001~013`, `UX-001~002`, `FILE`의 domain-neutral component mechanics, §7, §10 공통, §11.1~11.2, §12 | `src/shared`, `src/features/auth`, `tests/e2e/auth.spec.ts` |
|
||||
| 2 | `CHAR-001~014`, Character 관련 `FILE`, §7, §9 | `src/features/characters`, `tests/e2e/character-workspace.spec.ts` |
|
||||
| 3 | `AUDIO-001~026`, Audio 관련 `FILE`, §9 | `src/features/audio-contents`, `tests/e2e/audio-content.spec.ts` |
|
||||
| 4 | `SERIES-001~013`, Series 관련 `FILE`, §9 | `src/features/series`, `tests/e2e/series.spec.ts` |
|
||||
| 5 | `COMMUNITY-001~011`, Community 관련 `FILE`, §9 | `src/features/community-posts`, `tests/e2e/community-post.spec.ts` |
|
||||
| 6 | `FANTALK-001~008`, §9 | `src/features/fan-talks`, `tests/e2e/fan-talk.spec.ts` |
|
||||
| 7 | `COMMENT-001~006`, §9 | `src/features/comments`, `tests/e2e/comments.spec.ts` |
|
||||
| 8 | §9~10, §12~14, 활성 범위 전체 | 전체 unit/integration/E2E/build |
|
||||
|
||||
`FILE-001~015`의 domain-neutral component mechanics는 Phase 1에서 먼저 만든다. Character·Audio·Series·Community Phase는 자신의 allowed type·crop profile·GIF 예외와 multipart 흐름을 소유하면서 공통 mechanics를 조합·검증하고, Phase 8에서 전체 matrix를 회귀 검증한다.
|
||||
|
||||
## 6. 구현 완료 정의
|
||||
|
||||
- [ ] Phase 0~7의 활성 범위 Gate와 Phase 8 전체 Gate가 최신 실행에서 통과한다.
|
||||
- [ ] 모든 **확정** 요구사항이 구현, 명시적 non-goal, 또는 결정 기록이 있는 후속 범위 중 하나로 추적된다.
|
||||
- [ ] 외부 의존을 추정 endpoint·placeholder DTO·임시 production mock으로 우회하지 않았다.
|
||||
- [ ] 생성·일반 수정·soft delete payload와 enum 보정 contract test가 통과한다.
|
||||
- [ ] desktop/tablet/mobile 기능 matrix가 route와 action policy 양쪽에서 일치한다.
|
||||
- [ ] 파일 MIME·크기·crop·no-upscale·GIF·오디오 경계 test가 통과한다.
|
||||
- [ ] 모든 화면이 Page → feature component → shared/shadcn component 조합 규칙을 따르고 component별 독립 test를 가진다.
|
||||
- [ ] media error, upload 취소·재시도, 중복 제출, session 오류가 검증된다.
|
||||
- [ ] keyboard, label, focus, contrast, reduced motion, 200% zoom, axe 기준이 충족된다.
|
||||
- [ ] 문서와 실제 구현의 알려진 차이가 0건이다.
|
||||
- [ ] 검증 기록이 실제 명령과 결과를 포함해 누적돼 있다.
|
||||
|
||||
## 7. 검증 기록
|
||||
|
||||
구현 단계마다 아래 형식으로 누적하고 기존 기록을 삭제하거나 덮어쓰지 않는다.
|
||||
|
||||
```markdown
|
||||
### N차 구현 또는 수정 — YYYY-MM-DD
|
||||
|
||||
- 무엇을: 완료한 Phase와 주요 결과
|
||||
- 왜: 해당 범위와 결정 근거
|
||||
- 어떻게:
|
||||
- `실행 명령` — 성공/실패와 핵심 수치
|
||||
- 수동 검증 항목 — 성공/실패/불가 사유
|
||||
- 남은 항목: 외부 의존, 후속 범위, 없음 중 하나
|
||||
```
|
||||
|
||||
### 계획 재작성 — 2026-07-26
|
||||
|
||||
- 무엇을: 기존 18개 선형 Task를 프로젝트 세팅, 공통 플랫폼·인증/인가·컴포넌트 기반, 6개 도메인 vertical slice, 교차 회귀의 9개 Phase로 재구성했다. Phase 1 공통 컴포넌트 소비처 Matrix와 각 도메인 component map을 추가했다.
|
||||
- 왜: 프로젝트 세팅을 분리하고 공통 기반을 도메인보다 먼저 완결하며, 각 Phase를 기능·오류·반응형·접근성까지 독립 검증하기 위해서다. 미결·외부 의존은 전역 blocker 대신 소유 도메인에서 결정·제외하도록 했다.
|
||||
- 어떻게:
|
||||
- `rg -c '^## Phase [0-8]\.' plan-task.md`와 `rg -c '^### Phase [0-8] Gate$' plan-task.md` — Phase 9개, Gate 9개 확인.
|
||||
- `rg -c '^- \[ \].*component map을 확정한다' plan-task.md` — 도메인 component map 6개 확인.
|
||||
- 요구사항 range와 Phase 1 shared component Matrix `rg` 검사 — 모든 range와 12개 shared component 이름 확인.
|
||||
- Markdown fence `awk` 검사 — 24개, 짝수로 균형 확인.
|
||||
- `git diff --check` — 성공, whitespace 오류 0건.
|
||||
- `git -c core.quotePath=false diff --name-only` — `docs/20260725_AI캐릭터관리자웹/plan-task.md` 1개만 확인, 애플리케이션 코드 변경 0건.
|
||||
- 애플리케이션 test/build — 이번 단계는 계획 전용이고 아직 `package.json`이 없어 실행하지 않음.
|
||||
- 남은 항목: 후속 구현 전체와 각 도메인에 기록한 backend 외부 의존 계약.
|
||||
767
docs/20260725_AI캐릭터관리자웹/prd.md
Normal file
767
docs/20260725_AI캐릭터관리자웹/prd.md
Normal file
@@ -0,0 +1,767 @@
|
||||
# AI 캐릭터 관리자 웹 PRD
|
||||
|
||||
## 문서 정보
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 문서 상태 | 인터뷰 반영 초안 |
|
||||
| 작성일 | 2026-07-25 |
|
||||
| 대상 제품 | AI 캐릭터 전용 독립 관리자 웹 |
|
||||
| 구현 대상 | React + TypeScript + Vite SPA |
|
||||
| UI 기반 | Tailwind CSS + shadcn/ui |
|
||||
| 관련 계획 | [plan-task.md](./plan-task.md) |
|
||||
| 정규화 계약 | [api-contract.md](./api-contract.md) |
|
||||
|
||||
### 상태 표기
|
||||
|
||||
- **확정**: 이번 인터뷰에서 합의되어 구현 기준으로 사용할 사항
|
||||
- **미결**: 프론트엔드 제품·UI 또는 운영 정책이 결정되지 않아 후속 인터뷰나 UI 검토가 필요한 사항
|
||||
- **외부 의존**: 프론트엔드가 결정할 사항이 아니며, 해당 기능의 network integration 전에 백엔드가 제공해야 하는 계약
|
||||
- **권고**: 미결 사항에 대한 현재 추천안이며, 확정 전에는 계약으로 간주하지 않음
|
||||
|
||||
### 문서 유지보수 원칙
|
||||
|
||||
1. 요구사항이 변경되면 `prd.md`의 결정 기록, 기능 요구사항, API 계약 보정표, 미결 사항을 함께 갱신한다.
|
||||
2. 구현 범위나 순서가 바뀌면 같은 디렉터리의 `plan-task.md`도 같은 변경에서 갱신한다.
|
||||
3. 사용자 인터뷰 결정과 최초 API Contract가 충돌하면 이 문서의 “API 계약 보정사항”을 우선한다.
|
||||
4. 미결 사항과 외부 의존 계약은 추측으로 구현하지 않는다. **미결**에는 추천안을, **외부 의존**에는 제공 주체와 영향을 함께 기록한다.
|
||||
5. 완료된 미결 사항과 제공 완료된 외부 의존 계약은 결정일과 결정 내용을 “결정 기록”에 추가한 뒤 관련 수용 기준까지 갱신한다.
|
||||
|
||||
---
|
||||
|
||||
## 1. Overview
|
||||
|
||||
AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘텐츠·시리즈·커뮤니티·FanTalk·댓글 활동을 수행하도록 관리하는 독립 관리자 웹을 만든다.
|
||||
|
||||
관리자는 ADMIN 권한으로 로그인한 뒤 AI 캐릭터를 선택한다. 이후의 모든 생성·수정·비활성화 작업은 선택한 `characterId`와 연결된 AI 캐릭터 크리에이터의 활동으로 저장된다. 관리자가 AI 캐릭터 계정으로 직접 로그인하거나 토큰을 교환하는 방식은 사용하지 않는다.
|
||||
|
||||
이번 문서는 요구사항과 구현 계획만 정의한다. 애플리케이션 코드는 이번 단계에서 수정하지 않는다.
|
||||
|
||||
## 2. Problem Statement
|
||||
|
||||
현재 AI 캐릭터는 연결된 `creator(memberKind = AI_CHARACTER)`를 가지지만, 운영자가 캐릭터의 전체 활동을 한곳에서 관리할 독립 UI가 없다.
|
||||
|
||||
운영자는 다음 문제를 해결해야 한다.
|
||||
|
||||
- 캐릭터와 연결된 creator의 관계를 이해하지 않아도 안전하게 캐릭터를 관리해야 한다.
|
||||
- 여러 캐릭터의 리소스가 섞이지 않도록 선택한 캐릭터 문맥 안에서만 작업해야 한다.
|
||||
- 오디오 콘텐츠를 관리자 화면에서 즉시 재생해 검수해야 한다.
|
||||
- 예약 공개, 시리즈 연결과 순서, 게시글 고정, FanTalk 단일 답변 같은 도메인 규칙을 UI에서 명확히 안내해야 한다.
|
||||
- 데스크톱에서는 전체 운영을 수행하고 모바일에서는 조회와 긴급 응대가 가능해야 한다.
|
||||
- 최초 API Contract의 잘못되었거나 누락된 필드를 구현 전에 바로잡아야 한다.
|
||||
|
||||
## 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 등 백엔드 조회 정책 결정
|
||||
|
||||
## 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에는 한 번만 답변하고 필요하면 기존 답변을 수정한다.
|
||||
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
|
||||
/fan-talks/:fanTalkId
|
||||
```
|
||||
|
||||
라우트 문자열은 구현 시 확정하되 다음 원칙은 고정한다.
|
||||
|
||||
- 캐릭터를 선택하지 않은 전역 화면은 로그인과 캐릭터 목록·생성뿐이다.
|
||||
- 캐릭터 리소스 화면은 모두 URL에 `characterId`를 포함한다.
|
||||
- 목록의 `search`, `status`, 답변 상태, `page`, `size`는 가능한 범위에서 URL query에 보존한다.
|
||||
- 상세 GET이 제공되는 주요 리소스의 목록과 상세 화면은 새로고침과 직접 링크 진입이 가능해야 한다.
|
||||
- 커뮤니티 게시글은 별도 상세·수정 route 없이 목록 행/카드에서 여는 Sheet를 사용한다. 페이지 새로고침은 목록을 다시 조회한다.
|
||||
- 존재하지 않거나 다른 캐릭터 소유인 하위 리소스는 서버 결과에 따라 오류 화면으로 처리한다.
|
||||
|
||||
### 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 | 확정 | 생성 입력은 `name`, `description`, 선택 이미지, 선택 `originalWorkId`다. |
|
||||
| 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 | 확정 | `creatorMemberId`, `creatorNickname` 등 응답으로 제공되는 creator 정보는 읽기 전용으로 표시한다. |
|
||||
| CHAR-012 | 확정 | 캐릭터 목록은 활성 캐릭터만 반환하며 활성 상태 filter/query를 제공하지 않는다. |
|
||||
| CHAR-013 | 외부 의존 | 원작 검색 선택기에 필요한 lookup API와 원작 미선택 직렬화 계약은 백엔드가 제공해야 한다. 제공 전에는 원작 선택 network integration을 구현하지 않는다. |
|
||||
| CHAR-014 | 확정 | 캐릭터 soft delete 성공 시 캐릭터 active-only 목록으로 이동하고 성공 알림을 표시한다. 현재 상세 화면에 머물지 않는다. |
|
||||
|
||||
#### 캐릭터 생성·수정 폼
|
||||
|
||||
- 이름과 설명은 명시적인 레이블과 필드 오류 영역을 가진다.
|
||||
- 이미지는 미리보기, 교체, 새로 선택한 파일의 선택 취소를 제공한다. 기존 이미지 자체를 제거하는 기능은 contract가 없어 현재 범위가 아니다.
|
||||
- 원작은 이름 검색형 Combobox로 선택한다. 미선택은 허용하되 multipart JSON에서 `null`을 보낼지 key를 생략할지는 API 계약으로 확정한다.
|
||||
- 비활성화는 폼 Switch가 아니라 영향 범위를 설명하는 확인 Dialog로 실행한다.
|
||||
- 저장 중 중복 제출을 막고 성공 후 상세 데이터를 다시 동기화한다.
|
||||
|
||||
### 8.3 오디오 콘텐츠
|
||||
|
||||
| ID | 상태 | 요구사항 |
|
||||
|---|---|---|
|
||||
| AUDIO-001 | 확정 | 선택한 캐릭터의 오디오 목록·검색·상태 필터·상세·생성·수정·비활성화를 제공한다. |
|
||||
| AUDIO-002 | 확정 | 상태 값은 `OPEN`과 `SCHEDULED` 두 개뿐이다. |
|
||||
| AUDIO-003 | 확정 | `OPEN`은 현재 출시된 콘텐츠, `SCHEDULED`는 미래 출시 예약 콘텐츠다. |
|
||||
| AUDIO-004 | 확정 | 상태는 백엔드가 공개 시각을 기준으로 계산해 반환하고 프론트엔드는 재계산하지 않는다. |
|
||||
| AUDIO-005 | 확정 | 상태 필터를 보내지 않은 경우의 결과 집합도 백엔드가 결정해 반환한다. |
|
||||
| AUDIO-006 | 확정 | 생성 요청에는 `isActive`를 보내지 않는다. |
|
||||
| AUDIO-007 | 확정 | 일반 수정 요청에서는 `isActive`를 생략하고 soft delete 요청에만 `isActive=false`를 보낸다. `isActive=true`는 전송하지 않는다. |
|
||||
| AUDIO-008 | 확정 | 공개 방식은 “지금 즉시 공개”와 “예약 공개” 두 선택 버튼으로 제공한다. |
|
||||
| AUDIO-009 | 확정 | 생성 시 즉시 공개가 기본값이며 `releaseDateUtc=null`을 보낸다. 예약 날짜 입력은 비활성화하고 기존 값을 지운다. |
|
||||
| AUDIO-010 | 확정 | 예약 공개를 선택한 경우에만 날짜·시간을 입력할 수 있고 미래 시각이 필수다. |
|
||||
| AUDIO-011 | 확정 | 예약 시각은 Asia/Seoul로 입력·표시하고 API에는 UTC ISO-8601 `Z` 값으로 변환해 보낸다. |
|
||||
| AUDIO-012 | 확정 | 생성 시 cover image와 audio file은 필수이며 수정 시 교체 파일은 선택이다. |
|
||||
| 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 이상의 정수다. 0은 무료이며 UI는 예: `1,000캔`으로 표시한다. |
|
||||
| AUDIO-020 | 확정 | 오디오를 여러 시리즈에 연결할 수 있도록 `seriesIds` 다중 선택을 제공한다. |
|
||||
| AUDIO-021 | 확정 | 목록과 상세에서 오디오를 재생할 수 있다. |
|
||||
| AUDIO-022 | 확정 | 오디오 목록은 활성 오디오만 반환하며 활성 상태 filter/query를 제공하지 않는다. 기존 `status=OPEN|SCHEDULED` 공개 상태 필터는 유지한다. |
|
||||
| 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만으로 상세·목록을 자동 재조회하거나 자동 재생하지 않고, 일반 오류와 수동 재시도·페이지 새로고침 안내를 제공한다. |
|
||||
|
||||
수정 화면은 즉시 공개로 재초기화하지 않는다. 서버의 기존 `releaseDateUtc`와 `status`로 공개 방식과 날짜를 초기화하고, 관리자가 바꾸지 않으면 기존 값을 유지한다.
|
||||
|
||||
#### 관리자 오디오 플레이어
|
||||
|
||||
- 재생/일시정지, 탐색, 현재/전체 시간, 볼륨, 배속을 제공한다.
|
||||
- 명시적인 다운로드 버튼은 제공하지 않는다.
|
||||
- 여러 행의 플레이어가 동시에 재생되지 않게 현재 재생 항목을 단일화한다.
|
||||
- 재생 오류는 원인을 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를 선택하거나 보내지 않는다. 초기 state는 백엔드가 결정하며 프론트엔드는 정확한 기본값을 알 필요 없이 생성 응답의 state를 그대로 표시한다. |
|
||||
| SERIES-004 | 확정 | 수정 시 state를 선택할 수 있으며 선택하지 않으면 필드를 생략해 이전 상태를 유지한다. |
|
||||
| SERIES-005 | 확정 | 연재 요일 enum은 `SUN`, `MON`, `TUE`, `WED`, `THU`, `FRI`, `SAT`, `RANDOM`이다. |
|
||||
| SERIES-006 | 확정 | `RANDOM`은 다른 요일과 함께 보낼 수 없다. 값은 RANDOM 단독 또는 하나 이상의 실제 요일 목록이어야 한다. |
|
||||
| SERIES-007 | 확정 | 장르는 이름 검색형 선택기로 고르고 API에는 `genreId`를 보낸다. |
|
||||
| SERIES-008 | 확정 | 활성 시리즈 전체를 별도 순서 변경 모드에서 불러와 최종 순서의 모든 `seriesIds`를 전송한다. |
|
||||
| SERIES-009 | 확정 | drag-and-drop 외에 키보드와 위/아래 버튼으로 순서를 바꿀 수 있어야 한다. |
|
||||
| SERIES-010 | 확정 | 일반 수정 요청에서는 `isActive`를 생략하고 soft delete 요청에만 `isActive=false`를 보낸다. `isActive=true`, 복원과 hard delete는 제공하지 않는다. |
|
||||
| SERIES-011 | 외부 의존 | 장르 이름 검색 API는 백엔드가 제공해야 한다. 제공 전에는 장르 선택 network integration을 구현하지 않는다. 생성 초기 state의 정확한 기본값은 프론트엔드 의존사항이 아니다. |
|
||||
| SERIES-012 | 확정 | 시리즈 목록은 활성 시리즈만 반환하며 활성 상태 filter/query를 제공하지 않는다. |
|
||||
| SERIES-013 | 확정 | 시리즈 soft delete 성공 시 선택 캐릭터의 시리즈 active-only 목록으로 이동하고 성공 알림을 표시한다. 현재 상세 화면에 머물지 않는다. |
|
||||
|
||||
#### 시리즈 콘텐츠 연결
|
||||
|
||||
- 현재 연결 콘텐츠를 검색·페이지네이션해 보여준다.
|
||||
- 연결 후보는 선택 캐릭터의 활성 오디오로 제한한다.
|
||||
- 이미 연결된 콘텐츠를 중복 연결하지 않는다.
|
||||
- 연결 해제 전 대상 제목과 영향을 확인한다.
|
||||
- 연결/해제 성공 후 시리즈 상세와 콘텐츠 목록을 함께 갱신한다.
|
||||
|
||||
### 8.5 커뮤니티 게시글
|
||||
|
||||
| ID | 상태 | 요구사항 |
|
||||
|---|---|---|
|
||||
| COMMUNITY-001 | 확정 | 선택 캐릭터의 게시글 목록 기반 조회·등록·수정·고정·비활성화를 제공한다. |
|
||||
| COMMUNITY-002 | 확정 | 생성 요청에는 `isActive`를 보내지 않는다. |
|
||||
| COMMUNITY-003 | 확정 | 이미지와 오디오 파일은 선택 첨부다. |
|
||||
| COMMUNITY-004 | 확정 | 첨부 오디오가 있으면 목록 행/카드와 게시글 Sheet에서 재생할 수 있다. |
|
||||
| COMMUNITY-005 | 확정 | 일반 수정 요청에서는 `isActive`를 생략하고 soft delete 요청에만 `isActive=false`를 보낸다. `isActive=true`는 전송하지 않는다. |
|
||||
| COMMUNITY-006 | 확정 | 비활성 게시글은 반드시 `isFixed=false`, `fixedAtUtc=null` 상태여야 한다. |
|
||||
| COMMUNITY-007 | 확정 | 가격은 오디오와 동일하게 0 이상의 정수 “캔” 단위를 사용한다. |
|
||||
| COMMUNITY-008 | 확정 | 커뮤니티 게시글 목록은 활성 게시글만 반환하며 활성 상태 filter/query를 제공하지 않는다. |
|
||||
| COMMUNITY-009 | 확정 | 커뮤니티 전용 상세 GET과 상세·수정 직접 route를 추가하지 않는다. 목록 응답으로 행/카드의 Sheet를 열어 조회·수정·고정·비활성화·댓글 진입을 제공한다. |
|
||||
| COMMUNITY-010 | 확정 | 커뮤니티 게시글 soft delete 성공 시 열린 Sheet를 닫고 게시글 active-only 목록을 무효화·재조회해 해당 항목을 제거하며 성공 알림을 표시한다. |
|
||||
| COMMUNITY-011 | 확정 | 첨부 audio URL 갱신만을 위한 자동 요청은 하지 않으며 media error도 refetch trigger로 사용하지 않는다. 사용자 페이지 새로고침이나 mutation 후 cache 무효화 등 일반 목록 재조회가 발생하면 새 응답의 `audioSignedUrl`을 사용한다. |
|
||||
|
||||
### 8.6 FanTalk
|
||||
|
||||
| ID | 상태 | 요구사항 |
|
||||
|---|---|---|
|
||||
| FANTALK-001 | 확정 | 기본 목록은 전체 FanTalk를 최신순으로 표시한다. |
|
||||
| FANTALK-002 | 확정 | 필터는 전체, 미답변, 답변 완료 세 가지다. |
|
||||
| FANTALK-003 | 확정 | 하나의 FanTalk에는 답변을 한 번만 작성할 수 있다. |
|
||||
| FANTALK-004 | 확정 | 답변이 있으면 추가 작성은 차단하고 기존 답변 수정만 허용한다. |
|
||||
| FANTALK-005 | 확정 | 답변 삭제는 현재 범위가 아니다. |
|
||||
| FANTALK-006 | 확정 | 데스크톱·태블릿·모바일 모두 답변 작성과 수정을 지원한다. |
|
||||
| FANTALK-007 | 외부 의존 | 제공된 계약에는 답변 POST만 있다. 목록·상세·답변 수정 endpoint와 DTO는 백엔드가 제공해야 하며, 제공 전에는 해당 network integration을 구현하지 않는다. |
|
||||
| FANTALK-008 | 외부 의존 | 답변 1개 불변식의 원자적 강제와 중복 생성의 정확한 비2xx status/message key는 백엔드가 결정·제공해야 한다. |
|
||||
|
||||
### 8.7 댓글과 답글
|
||||
|
||||
| ID | 상태 | 요구사항 |
|
||||
|---|---|---|
|
||||
| COMMENT-001 | 확정 | 오디오 콘텐츠와 커뮤니티 게시글에 댓글 영역을 제공한다. |
|
||||
| COMMENT-002 | 확정 | 구조는 루트 댓글과 그 댓글의 직접 답글까지 정확히 2단계다. 답글의 답글은 허용하지 않는다. |
|
||||
| COMMENT-003 | 확정 | AI 캐릭터는 루트 댓글과 답글을 작성하고 자신이 작성한 내용을 수정·soft delete할 수 있다. |
|
||||
| COMMENT-004 | 확정 | 팬이 작성한 루트 댓글과 답글은 수정할 수 없고 운영 목적의 soft delete만 가능하다. |
|
||||
| COMMENT-005 | 확정 | 모바일에서도 조회·작성·수정·soft delete를 모두 지원한다. |
|
||||
| COMMENT-006 | 외부 의존 | 댓글 목록·작성·수정·soft delete API와 팬 댓글 삭제 권한 오류 계약은 백엔드가 결정·제공해야 한다. 제공 전에는 댓글 network integration을 구현하지 않는다. |
|
||||
|
||||
### 8.8 공통 파일 정책
|
||||
|
||||
| ID | 상태 | 요구사항 |
|
||||
|---|---|---|
|
||||
| FILE-001 | 확정 | 캐릭터·오디오 cover·시리즈·커뮤니티 image의 최대 크기는 10MB다. |
|
||||
| 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 이내 오차를 허용한다. |
|
||||
|
||||
“가로 800/1,000”은 이 문서에서 등록 결과의 **최대 출력 폭**으로 해석한다. JPEG/PNG crop 결과에는 이 제한을 적용하되 선택한 원본 crop 영역이 더 작으면 확대하지 않는다. 커뮤니티 GIF는 원본 가로가 800px 이하일 때만 등록할 수 있다.
|
||||
|
||||
#### Image crop UI 흐름
|
||||
|
||||
1. 새 image 선택 직후 resource별 MIME과 10MB 제한을 먼저 검증한다.
|
||||
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 조회·답변 작성·답변 수정 | 전체 | 전체 | 전체 |
|
||||
|
||||
모바일에서 미지원인 기능은 좁은 화면에 데스크톱 폼을 억지로 노출하지 않는다. 화면에는 읽기 전용임을 알리고 전체 관리가 필요하면 데스크톱·태블릿 사용을 안내한다.
|
||||
|
||||
## 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의 active-only 목록으로 이동하고 성공 toast 표시
|
||||
- 필드 오류: 로컬 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 계약
|
||||
|
||||
### 11.1 공통 응답
|
||||
|
||||
모든 성공 응답은 `ApiResponse.ok(...)` wrapper를 사용한다.
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": null,
|
||||
"data": {}
|
||||
}
|
||||
```
|
||||
|
||||
`/admin/member/login`과 `/member/logout` 성공 예시는 최상위 `errorProperty=null`도 포함한다. 공통 API client는 성공 응답에서 `errorProperty`가 없거나 `null`인 두 형태를 모두 수용한다.
|
||||
|
||||
모든 오류는 의미에 맞는 비2xx status와 `ApiResponse.error(...)` wrapper를 사용한다.
|
||||
|
||||
```json
|
||||
{
|
||||
"success": false,
|
||||
"message": "잘못된 요청입니다.",
|
||||
"data": null,
|
||||
"errorProperty": null
|
||||
}
|
||||
```
|
||||
|
||||
- 오류를 2xx로 normalize하지 않는다.
|
||||
- UI는 서버가 반환한 현지화된 한국어 `message`를 우선 사용한다.
|
||||
- 백엔드는 `Accept-Language: ko|en|ja`에 따라 번역하고 누락·미지원 언어는 KO로 fallback한다. security filter도 header를 직접 해석한다.
|
||||
- 모든 목록·검색 endpoint는 `page=0`, `size=20` 기본값과 size 최소 20, 최대 50 보정을 적용한다.
|
||||
- `characterId`는 선택된 대상 캐릭터가 필요한 하위 resource endpoint에만 사용한다.
|
||||
- 캐릭터 목록·검색과 캐릭터 생성에는 path `characterId`가 없다.
|
||||
|
||||
### 11.2 오류 처리 매핑
|
||||
|
||||
| HTTP | 의미 | UI 처리 |
|
||||
|---:|---|---|
|
||||
| 400 | binding, target 미존재, creator 불변식 등 invalid request | 로컬 검증은 inline, 서버 `errorProperty`가 필드명을 제공할 때만 inline, 그 외 화면 Alert |
|
||||
| 401 | JWT 없음·잘못됨·만료·폐기 | 인증 제거 후 로그인 이동 |
|
||||
| 403 | 비ADMIN 또는 stale ADMIN claim | 접근 거부 화면 |
|
||||
| 404 | 신규 prefix 미매핑 경로 | 찾을 수 없음과 목록 이동 |
|
||||
| 405 | 지원하지 않는 method | 서버 message와 재시도 불가 안내 |
|
||||
| 415 | 지원하지 않는 media type | 파일 필드 오류와 허용 형식 안내 |
|
||||
| 500 | 예상하지 못한 오류 | 일반 오류, request ID가 있으면 함께 표시, 재시도 |
|
||||
|
||||
Phase 2~6에서 추가되는 오류는 구현 전에 정확한 비2xx status와 KO/EN/JA message key를 확정해야 한다. 신규 prefix 전용 처리를 legacy/public endpoint로 확장하지 않는다.
|
||||
|
||||
### 11.3 제공된 endpoint 목록
|
||||
|
||||
모든 query, multipart part, request/response field를 보존한 보정 후 계약은 [api-contract.md](./api-contract.md)를 기준으로 한다.
|
||||
|
||||
| 영역 | Method | Path | 계약 상태 |
|
||||
|---|---|---|---|
|
||||
| 인증 | POST | `/admin/member/login` | 제공됨, body는 email/password, 성공 시 token/ADMIN role |
|
||||
| 인증 | POST | `/member/logout` | 제공됨, 공통 endpoint, Bearer header, body 없음 |
|
||||
| 캐릭터 | GET, POST | `/api/v2/admin/ai-characters` | 제공됨, 필드 보정 필요 |
|
||||
| 캐릭터 | GET, PUT | `/api/v2/admin/ai-characters/{characterId}` | 제공됨, 필드 보정 필요 |
|
||||
| 오디오 | GET, POST | `/api/v2/admin/ai-characters/{characterId}/audio-contents` | 제공됨, 생성 필드 보정 필요 |
|
||||
| 오디오 | GET, PUT | `.../audio-contents/{contentId}` | 제공됨 |
|
||||
| 시리즈 | GET, POST | `/api/v2/admin/ai-characters/{characterId}/series` | 제공됨, enum 보정 필요 |
|
||||
| 시리즈 | GET, PUT | `.../series/{seriesId}` | 제공됨, enum 보정 필요 |
|
||||
| 시리즈 콘텐츠 | GET, POST | `.../series/{seriesId}/contents` | 제공됨 |
|
||||
| 시리즈 콘텐츠 | DELETE | `.../series/{seriesId}/contents/{contentId}` | 제공됨 |
|
||||
| 시리즈 순서 | PUT | `.../series/orders` | 제공됨 |
|
||||
| 커뮤니티 | GET, POST | `.../{characterId}/community-posts` | 제공됨, 생성 필드 보정 필요 |
|
||||
| 커뮤니티 | PUT | `.../community-posts/{postId}` | 제공됨 |
|
||||
| FanTalk 답변 | POST | `.../{characterId}/fan-talks/{fanTalkId}/replies` | 제공됨 |
|
||||
|
||||
### 11.4 API 계약 보정사항
|
||||
|
||||
이 표는 최초 API Contract보다 우선한다.
|
||||
|
||||
| 항목 | 최초 계약 | 확정 보정 |
|
||||
|---|---|---|
|
||||
| Character `externalCharacterId` | 요청·응답에 존재 | 존재하지 않는 필드이므로 전부 제거 |
|
||||
| 생성 `isActive` | Character, Audio, Community 예시에 존재 | 모든 생성 요청에서 제거, 서버가 초기값 결정 |
|
||||
| 수정 `isActive` | 수정 예시에 존재 | 일반 수정에서는 key를 생략하고 soft delete에만 `false`를 보낸다. `true`는 전송하지 않는다. |
|
||||
| Audio status | 예시에 `OPEN` | 허용값은 `OPEN`, `SCHEDULED`이며 서버 계산 |
|
||||
| Series state | 예시에 `OPEN` | `PROCEEDING`, `SUSPEND`, `COMPLETE`만 허용 |
|
||||
| Series 생성 state | 요청 예시에 `OPEN` | 생성 요청에서 state 제거 |
|
||||
| Series 수정 state | 필수처럼 표현 | 선택 필드, 미선택 시 생략하여 기존 값 유지 |
|
||||
| Series 요일 | `MONDAY` 등 장문 값 | `SUN`~`SAT`와 `RANDOM` 사용 |
|
||||
| RANDOM | 규칙 없음 | 단독만 허용, 다른 요일과 조합 금지 |
|
||||
| Character creator | 응답 연결만 표현 | 생성 시 AI_CHARACTER creator 동시 생성, 프로필 동기화는 백엔드 담당 |
|
||||
| FanTalk 답변 | POST만 표현 | 한 번만 생성, 기존 답변 수정 가능, 삭제 불가 |
|
||||
|
||||
### 11.5 백엔드 제공 대기 계약
|
||||
|
||||
아래 항목은 프론트엔드 인터뷰로 결정할 Open Question이 아니라 **외부 의존**이다. P0의 request/response/error 계약을 제공받기 전에는 해당 기능의 network integration을 구현하지 않는다. P1은 추정하지 않고 출시 전 계약과 검증을 맞춘다. UI shell과 계약에 의존하지 않는 표현 작업은 병행할 수 있다.
|
||||
|
||||
| 우선순위 | 백엔드 제공 필요 계약 | 프론트엔드 영향 |
|
||||
|---:|---|---|
|
||||
| P0 | FanTalk 목록·상세·답변 수정·유일성 | 목록·상세·수정 연동과 동시 중복 답변 처리 대기 |
|
||||
| P0 | 오디오·커뮤니티 댓글 CRUD와 팬 댓글 삭제 권한 오류 | 댓글·답글 연동과 권한별 오류 처리 대기 |
|
||||
| P0 | 원작·장르 검색과 originalWork 미선택 직렬화 | Character·Series 선택기 연동 대기 |
|
||||
| P0 | 시리즈 연결 후보 | 연결 가능한 활성 오디오 선택기 연동 대기 |
|
||||
| P0 | 시리즈 전체 순서의 50개 초과 로딩·누락 ID·동시 충돌 | 전체 순서 저장 연동 대기 |
|
||||
| P1 | price 최대값 | 현재 0 이상 정수 규칙만 적용하며 상한 계약 제공 시 Audio·Community schema와 경계값 test 갱신 |
|
||||
| P0 | 신규 도메인 오류 | 기능별 정확한 비2xx status와 KO/EN/JA message key 제공 대기 |
|
||||
|
||||
## 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 검증 정책은 백엔드 책임이다.
|
||||
|
||||
### 감사 로그 미결 사항과 권고
|
||||
|
||||
현재 감사 로그 정책은 **미결**이다.
|
||||
|
||||
**권고:** 이번 범위에서는 백엔드가 관리자 ID, 대상 캐릭터 ID, resource 종류와 ID, action, 성공/실패, 서버 시각, request ID, 민감정보를 제거한 변경 요약을 기록한다. 관리자 UI의 감사 로그 조회 화면은 후속 범위로 둔다. JWT, 비밀번호, signed URL, 파일 본문은 기록하지 않는다.
|
||||
|
||||
## 13. 성능과 품질 요구사항
|
||||
|
||||
- 목록은 서버 페이지네이션을 사용하고 무제한 전체 로드를 피한다. 단, 시리즈 순서 변경 모드는 계약상 활성 시리즈 전체를 명시적으로 로드한다.
|
||||
- 이미지에는 고정 aspect ratio와 크기를 예약해 layout shift를 줄이고 목록 이미지는 lazy load한다.
|
||||
- 필터 변경 중 기존 데이터를 유지해 화면 깜빡임을 줄인다.
|
||||
- 파일 업로드 외의 일반 mutation은 중복 제출을 막는다.
|
||||
- 목록 검색 debounce 시간은 구현 시 300ms 전후로 일관되게 적용한다.
|
||||
- 날짜, 가격, 상태 label은 중앙 formatter로 일관되게 표시한다.
|
||||
- 브라우저 지원 범위는 데스크톱 Chrome/Edge/Safari 최신 2개 주요 버전과 모바일 Chrome/Safari 최신 2개 주요 버전이다.
|
||||
|
||||
## 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이 복원되지 않는다.
|
||||
- 캐릭터 생성 요청에 `isActive`와 `externalCharacterId`가 포함되지 않는다.
|
||||
- Character, Audio, Series, Community의 일반 수정은 `isActive`를 생략하고 soft delete에만 `isActive=false`를 보내며 `true`는 전송하지 않는다.
|
||||
- Character, Audio, Series의 soft delete가 성공하면 해당 active-only 목록 cache를 갱신해 비활성화한 항목을 표시하지 않고 목록으로 이동하며 성공 알림을 표시한다. Community는 열린 Sheet를 닫고 현재 active-only 목록을 갱신해 해당 항목을 제거한다.
|
||||
- 캐릭터 수정이 creator profile 동기화 결과와 함께 다시 조회된다.
|
||||
- 워크스페이스 진입·복원용 상세 성공 응답으로 `isActive=false`인 캐릭터를 받은 경우 프론트엔드가 모든 하위 mutation 진입점을 차단한다. soft delete 성공 직후에는 active-only 목록 이동을 우선하고, 상세 요청의 비2xx 응답은 공통 오류 처리로 표시한다.
|
||||
- 오디오 생성에서 즉시 공개는 `releaseDateUtc=null`, 예약 공개는 미래 UTC 시각을 보낸다.
|
||||
- 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가 10MB와 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로 완료할 수 있다.
|
||||
- 오디오 목록·상세와 커뮤니티 목록 항목의 첨부 오디오가 재생된다.
|
||||
- media error를 signed URL 만료로 구분하지 않고 일반 재생 오류를 표시한다. 오류만으로 API를 자동 재조회하거나 자동 재생하지 않으며 사용자가 수동 재시도하거나 페이지를 새로고침할 수 있다.
|
||||
- 커뮤니티는 전용 상세 GET이나 상세·수정 route 없이 목록 응답 기반 Sheet에서 조회·수정·고정·비활성화·댓글 진입을 제공한다.
|
||||
- Series에 `OPEN`이나 `MONDAY` 같은 잘못된 값을 보내지 않는다.
|
||||
- Series 생성에는 state를 보내지 않고 수정 미선택 시 state를 생략한다.
|
||||
- RANDOM과 실제 요일을 동시에 선택할 수 없다.
|
||||
- FanTalk 답변이 있으면 두 번째 POST가 UI에서 차단되고 수정 동작만 제공되며, 직접·동시 요청도 백엔드가 원자적으로 거부한다.
|
||||
- 댓글은 2단계를 넘지 않고 작성자에 따른 수정·삭제 권한이 구분된다.
|
||||
- 모바일에서 조회·오디오 재생·댓글 관리·FanTalk 답변 작성/수정이 가능하다.
|
||||
|
||||
### 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 검증 항목을 확인한다.
|
||||
|
||||
## 15. Open Questions
|
||||
|
||||
| ID | 상태 | 결정 필요 사항 | 현재 권고 |
|
||||
|---|---|---|---|
|
||||
| OQ-009 | 미결 | 각 페이지의 문자열 최대 길이와 배열 최대 개수 | 초기 UI를 만든 뒤 Character의 이름·설명, Audio의 제목·설명·`seriesIds`, Series의 제목·소개·요일·keywords·writer·studio·연결 `contentIds`·순서 `seriesIds`, Community 본문, FanTalk 답변과 댓글을 페이지별로 검토해 권고값을 작성한다. 백엔드 호환 확인 후 확정하며, 그 전에는 제공 계약에 없는 임의의 최대값을 추가하지 않는다. 확정 시 PRD·API Contract·schema·경계값 test를 함께 갱신한다. |
|
||||
| OQ-010 | 미결 | 감사 로그 UI 제공 여부 | backend 기록 우선, 조회 UI는 후속 범위 |
|
||||
|
||||
## 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로 한다. |
|
||||
| 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에서 제외한다. |
|
||||
Reference in New Issue
Block a user