docs(ai-character): OpenAPI 계약 반영
This commit is contained in:
@@ -1,900 +0,0 @@
|
||||
# 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을 구현하지 않는다.
|
||||
|
||||
goal 실행 시 계약 입력은 해당 Task의 `시작 조건`이 가리키는 이 문서 section을 사용한다. 제공 대기 계약을 추정해 goal을 완료하지 않으며, 계약이 새로 제공되거나 제외 결정이 나면 PRD 결정 기록 → 이 문서 → `plan-task.md` 순서로 갱신한다.
|
||||
|
||||
## 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 재발급 호출을 추가하지 않는다.
|
||||
|
||||
### 1.5 개발 전용 Mock Preview 계약 경계
|
||||
|
||||
- browser MSW handler와 fixture는 이 문서에 **제공됨**으로 기록된 endpoint, request/response DTO와 오류 규칙에서만 파생한다.
|
||||
- mock mode도 production과 같은 URL, method, header, serializer, envelope와 API client를 사용하며 별도 mock 전용 DTO·adapter를 만들지 않는다.
|
||||
- 계약은 제공됐지만 backend endpoint가 아직 404인 경우 mock mode에서 최종 UI를 확인할 수 있다. server mode의 404·network error를 감지해 mock으로 자동 fallback하지 않는다.
|
||||
- “백엔드 제공 대기” 항목은 fixture로 추정하지 않는다. 계약이 제공되기 전에는 관련 최종 network UI와 integration 완료를 주장하지 않는다.
|
||||
- mock mutation은 deterministic in-memory store를 갱신하고 새로고침 때 seed로 초기화한다. domain fixture를 browser 영구 저장소에 기록하지 않는다.
|
||||
- mock mode는 개발 환경에서만 명시적으로 활성화하고 production build에서는 거부한다.
|
||||
- mock UI Gate 통과는 실제 backend 연동 완료 증거가 아니다. 각 도메인은 `UI 확인 완료(mock)`와 `실제 서버 연동 완료(server)` 결과를 별도로 기록한다.
|
||||
|
||||
## 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의 원본 파일 크기는 최대 `10,485,760 bytes`다. `10,485,760 bytes`는 허용하고 `10,485,761 bytes`부터 거부한다. 확장자 문자열만 신뢰하지 않고 실제 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.0 오디오 콘텐츠 테마 목록
|
||||
|
||||
`GET /api/v2/admin/ai-characters/audio-content-themes`
|
||||
|
||||
- Query 없음
|
||||
- Request body 없음
|
||||
|
||||
Response `data`:
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"themeId": 11,
|
||||
"themeName": "ASMR",
|
||||
"imageUrl": "https://cdn.example.com/audio-content-theme/asmr.png"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
- 오디오 콘텐츠 생성 UI는 이 목록에서 테마를 선택하게 하고, 선택한 `themeId`를 생성 request JSON에 포함한다.
|
||||
- 테마 목록은 선택 캐릭터 path를 포함하지 않는 공통 관리자 조회다.
|
||||
|
||||
### 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
|
||||
themeId: number
|
||||
price: number
|
||||
isAdult: boolean
|
||||
releaseDateUtc: string | null
|
||||
seriesIds: number[]
|
||||
}
|
||||
```
|
||||
|
||||
- `isActive`와 `status`를 보내지 않는다.
|
||||
- `themeId`는 필수이며 `GET /api/v2/admin/ai-characters/audio-content-themes` 응답에서 선택한 값이다.
|
||||
- 즉시 공개는 `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를 같은 변경에서 갱신한다.
|
||||
1260
docs/20260725_AI캐릭터관리자웹/api-contract.openapi.json
Normal file
1260
docs/20260725_AI캐릭터관리자웹/api-contract.openapi.json
Normal file
File diff suppressed because it is too large
Load Diff
@@ -14,25 +14,26 @@
|
||||
| 최초 작성일 | 2026-07-25 |
|
||||
| 재작성일 | 2026-07-26 |
|
||||
| 요구사항 기준 | [prd.md](./prd.md) |
|
||||
| API 기준 | [api-contract.md](./api-contract.md) |
|
||||
| API 기준 | [api-contract.openapi.json](./api-contract.openapi.json) |
|
||||
|
||||
## 1. 전역 제약
|
||||
|
||||
- 계획 보완 단계에서는 PRD·API Contract·plan과 연결 가이드만 동기화하고, 애플리케이션 코드와 프로젝트 설정은 해당 Task goal 실행 때 변경한다.
|
||||
- PRD와 최초 API Contract가 충돌하면 PRD `11.4 API 계약 보정사항`을 우선한다.
|
||||
- endpoint, query, multipart part, request/response field, required 여부, status와 오류 응답은 `api-contract.openapi.json`을 우선한다. OpenAPI에 표현되지 않는 제품·UI·운영 정책은 PRD를 따른다.
|
||||
- OpenAPI에 없는 기존 인증 `POST /admin/member/login`, `POST /member/logout`은 PRD `11.5 EXT-006 현재 구현 기준 계약`과 완료된 Phase 1 contract test를 임시 기준으로 유지한다. 정식 계약 제공 전에도 기존 인증을 다시 구현하거나 제거하지 않으며 Phase 3~9 진행을 차단하지 않는다.
|
||||
- 로그인은 `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·Community 목록은 활성 상태 query와 client-side 활성 필터를 추가하지 않는다. active-only 반환 보장은 외부 의존으로 추적하고 서버 반환값을 그대로 사용한다.
|
||||
- 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와 기존 화면 데이터를 유지한다.
|
||||
- 목록은 operation별 OpenAPI pagination을 사용한다. 공통 page는 기본 0·최소 0, size는 기본 20·최소 1이고 FanTalk size만 20..50으로 보정된다. 검색을 제공하는 목록은 약 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`이다.
|
||||
@@ -94,25 +95,44 @@ import 오류, test 환경 오류, 임시 mock 누락 같은 우발적 실패는
|
||||
|
||||
| 분류 | 처리 규칙 |
|
||||
|---|---|
|
||||
| 계약이 제공됨 | `api-contract.md`에 request/response/error 예시를 반영하고 contract test를 만든 뒤 구현한다. |
|
||||
| 계약이 제공됨 | `api-contract.openapi.json`의 operation/schema를 contract test로 고정한 뒤 구현한다. 계약 자체의 변경은 backend가 제공한 새 버전을 받은 경우에만 반영한다. |
|
||||
| 구현됐지만 OpenAPI에서 누락됨 | PRD에 현재 endpoint·request/response·검증 근거를 기록하고 기존 회귀 test를 유지한다. 정식 계약 전에는 동작을 확장하지 않지만 독립 Phase 진행은 차단하지 않는다. 현재 해당 항목은 `EXT-006` 인증뿐이다. |
|
||||
| 안전한 확정 기본값이 있음 | 문서에 적힌 최소 규칙만 구현한다. 예: price 상한 미제공 시 `0 이상 정수`만 검증한다. |
|
||||
| 계약 없이 안전하게 구현할 수 없음 | endpoint·DTO·오류를 추측하지 않는다. 해당 최소 기능 또는 Phase를 현재 릴리스에서 제외하기 전에 PRD 결정 기록, API Contract, 이 계획을 함께 갱신한다. |
|
||||
| 구현 중 불필요하다고 판단 | 활성 체크 항목을 제거하되 PRD 결정 기록에 삭제 이유와 날짜를 남긴다. 과거 결정 기록은 지우지 않는다. |
|
||||
| 계약이 후속 도착 | 완료한 Phase를 묵시적으로 다시 열지 않고 별도 후속 vertical slice를 계획한다. |
|
||||
|
||||
- `OQ-009`는 각 도메인의 실제 폼을 만든 시점에 한 번만 판단한다. 최대값이 필요하면 backend 호환 확인 후 PRD·API Contract·schema·경계 test를 같은 변경에서 갱신한다. 필요 없으면 “상한 추가 없음”으로 종결하고 관련 구현 항목을 삭제한다.
|
||||
- `OQ-009`의 결정 절차는 확정됐다. 각 도메인의 초기 UI를 만든 뒤 실제 페이지에서 문자열 입력 공간과 반복 항목 사용성을 검토하고 최대값 권고안을 정한다. backend 호환 확인 후 PRD·OpenAPI 계약·schema·경계 test를 같은 변경에서 갱신하며, 그전에는 임의 상한을 추가하지 않는다.
|
||||
- `OQ-010` 감사 로그 조회 UI는 현재 릴리스 구현 항목을 만들지 않는다. 포함하기로 바뀌면 backend 조회 계약을 포함한 별도 Phase로 다시 계획한다.
|
||||
- P0 외부 의존이 남아 있으면 영향을 받는 network flow를 완료로 표시하지 않는다. 다른 독립 Phase는 계속 진행할 수 있다.
|
||||
- 이미지 최대 `10MB`의 정확한 byte 경계처럼 표현만으로 단일 값이 정해지지 않는 항목은 첫 파일 Phase에서 결정 기록과 contract를 먼저 보정한다.
|
||||
|
||||
### 2.4 Goal 기능 운영 규칙
|
||||
### 2.4 제공 범위 우선 실행·후속 보완 전략
|
||||
|
||||
전체 일정을 미제공 backend 계약 하나에 직렬화하지 않는다. Phase 3부터
|
||||
각 Phase의 OpenAPI 제공 범위를 먼저 구현하고, 독립적으로 진행 가능한
|
||||
후속 Phase를 계속 수행해 Phase 9의 활성 릴리스 범위 Gate까지 완료한다.
|
||||
|
||||
1. 각 Phase의 계약 확인 Task에서 제공 operation과 외부 의존을 먼저 분리한다.
|
||||
2. 제공된 endpoint·DTO로 안전하게 구현할 수 있는 Task는 mock/server 상태를 구분해 구현·검증한다.
|
||||
3. 계약이 없는 기능은 endpoint·DTO·fixture·UI 완료 상태를 추정하지 않고 해당 Task 또는 network 범위만 `외부 계약 대기` 후속으로 기록한다.
|
||||
4. 외부 의존 때문에 실행하지 않은 범위가 있더라도 완료된 독립 Task를 되돌리지 않고 다음 Phase를 진행한다.
|
||||
5. Phase 8 Comments 계약이 없으면 `P8-T1`의 계약 부재·재개 조건과 `P8-GATE` 제외/대기 증거를 남긴 뒤 Phase 9로 진행한다.
|
||||
6. Phase 9는 제공 계약과 명시적 제외 범위만 대상으로 실행하고 결과를 `활성 범위 완료`로 기록한다. 미제공 계약을 포함한 `전체 기능 완료`로 표현하지 않는다.
|
||||
7. backend 계약이 후속 도착하면 완료 Phase를 묵시적으로 다시 열지 않고 별도 vertical slice를 계획해 구현한 뒤 영향받는 Phase Gate와 `P9-GATE`를 다시 실행한다.
|
||||
|
||||
이 전략은 추가 사용자 결정을 기다리기 위한 임시 우회가 아니라, 제공
|
||||
범위의 UI와 공통 품질을 먼저 완성해 전체 lead time을 줄이는 확정 실행
|
||||
방식이다.
|
||||
|
||||
### 2.5 Goal 기능 운영 규칙
|
||||
|
||||
- `create_goal`에는 동시에 하나의 미완료 goal만 등록한다. Phase 전체가 아니라 아래에 `Goal 실행`으로 표시한 Task 하나를 기본 단위로 사용한다.
|
||||
- goal objective는 해당 Task의 `Goal 실행`, `시작 조건`, `완료 증거`, `범위 밖`을 함께 복사해 등록한다. 사용자가 명시적으로 요청하지 않으면 token budget을 설정하지 않는다.
|
||||
- 활성 goal이 있으면 새 goal을 만들지 않고 같은 Task를 이어서 수행한다. 체크박스 일부만 끝났거나 검증·기록이 남았으면 goal을 완료 처리하지 않는다.
|
||||
- `완료 증거`와 해당 Task의 체크박스를 모두 충족하고 `plan-task.md` 검증 기록까지 누적한 뒤에만 goal을 `complete`로 갱신한다.
|
||||
- 외부 계약이나 권한 같은 동일 차단 사유가 최초 시도와 자동 후속을 포함해 3회 연속 반복되고, 문서화·독립 작업 등 의미 있는 진전도 불가능할 때만 goal을 `blocked`로 갱신한다. 그 전에는 가능한 범위를 계속 수행한다.
|
||||
- 계약 미제공으로 기능을 제외할 때는 PRD 결정 기록 → `api-contract.md` → 이 계획의 활성 checklist 순서로 갱신해야 한다. 이 문서화가 끝나기 전에는 goal을 완료 처리하지 않는다.
|
||||
- 계약 미제공으로 기능을 제외할 때는 PRD 결정 기록 → 제공된 OpenAPI 또는 외부 의존 상태 → 이 계획의 활성 checklist 순서로 갱신해야 한다. 이 문서화가 끝나기 전에는 goal을 완료 처리하지 않는다.
|
||||
- 각 Phase는 자신의 Task goal을 번호 순서로 완료한 뒤 Phase Gate를 마지막 goal로 실행한다. Gate goal은 실제 명령 결과와 수동 검증 결과를 기록한 뒤 완료한다.
|
||||
- Phase 0~1처럼 이미 완료 체크된 범위는 새 구현 goal로 다시 만들지 않는다. 회귀나 기록 정합성 보정이 필요하면 별도 수정 goal을 만들고 기존 검증 기록을 덮어쓰지 않는다.
|
||||
|
||||
@@ -133,15 +153,31 @@ Goal objective 권장 형식:
|
||||
| 1 | 공통 플랫폼·인증/인가·컴포넌트 기반 | Phase 0 | shared component contract + login → protected shell → refresh restore → logout/401/403 |
|
||||
| 2 | 개발 전용 Mock Preview 기반 | Phase 1 | explicit mock/server mode, browser MSW, mock banner, no production fallback |
|
||||
| 3 | Character workspace | Phase 2 | list/search → create/select → detail/edit → deactivate |
|
||||
| 4 | Audio vertical slice | Phase 3의 workspace core | list/filter/detail/play → create/edit/upload → deactivate |
|
||||
| 5 | Series vertical slice | Phase 4의 Audio 조회 API | CRUD → content link/unlink → full reorder |
|
||||
| 4 | Audio vertical slice | Phase 3의 workspace core | list/search/detail/play → create/edit/upload → deactivate |
|
||||
| 5 | Series vertical slice | Phase 4의 Audio 조회 API | list/detail → 계약 제공 후 CRUD; content link/unlink → full reorder |
|
||||
| 6 | Community vertical slice | Phase 4의 media/file primitive | list → collection Sheet edit/pin → media play → deactivate |
|
||||
| 7 | FanTalk vertical slice | Phase 3 | list/filter/detail → one reply → edit |
|
||||
| 7 | FanTalk vertical slice | Phase 3 | list → one reply; detail/edit/filter는 계약 대기 |
|
||||
| 8 | Comments vertical slice | Phase 4 + Phase 6 | Audio/Community thread → permission별 CRUD |
|
||||
| 9 | 교차 회귀·인수인계 | 활성 범위의 Phase 0~8 | 전체 journey, viewport, axe, security, mock/server build |
|
||||
|
||||
기본 진행 순서는 Phase 번호를 따른다. 다만 Phase 5·6·7은 자신의 선행조건과 계약이 충족되면 병행할 수 있고, 외부 계약으로 막힌 Phase가 다른 독립 Phase를 막지 않는다.
|
||||
|
||||
### 3.1 Phase 3~9 OpenAPI 준비 상태
|
||||
|
||||
| Phase | 현재 구현 가능 범위 | 외부 의존 또는 제외 범위 | 판정 |
|
||||
|---:|---|---|---|
|
||||
| 3 Character | 활성 기본 목록·레거시 검색·상세·필수 image 생성·허용 field 수정·soft delete request와 workspace UI | original work lookup, 검색 결과 active-only 보장, 도메인 오류 key | 핵심 UI 가능, 일부 server 수용 기준 대기 |
|
||||
| 4 Audio | 테마·제목 검색·상세·생성·허용 field 수정·재생·upload | active-only 반환, backend file/container 검증, 오류 key; status filter와 수정 audio/schedule/theme/series는 계약상 제외 | 제공 operation 범위 구현 가능 |
|
||||
| 5 Series | 목록·상세 조회, 연결 후보 검색·연결·해제·전체 순서 | genre lookup이 생성 차단, edit DTO가 수정 차단, active-only·오류 key 대기 | CRUD 전체는 차단, 조회·연결·순서 부분 가능 |
|
||||
| 6 Community | timezone 목록·Sheet·생성·허용 field 수정·고정·soft delete request·media | active-only, pagination 종료 metadata, backend file 검증과 오류 key | 핵심 UI 가능, 목록 종료·server 수용 기준 대기 |
|
||||
| 7 FanTalk | page 목록과 답변 1회 생성 | 상세·답변 수정·전체 결과 filter/sort·유일성 오류 | 제공 operation 범위 구현 가능 |
|
||||
| 8 Comments | 계약 독립적인 shell·상태 inventory | 두 target의 댓글 CRUD·2단계·권한 오류 전체 | network slice 차단 |
|
||||
| 9 Final | 제공 계약과 명시적 제외 범위의 교차 회귀 | 미제공 P0 범위를 포함한 전체 릴리스 완료 주장 | 활성 범위 Gate 후 가능 |
|
||||
|
||||
OQ-009를 포함한 프론트엔드 제품 결정 절차는 확정됐다. 위 표의 대기
|
||||
사항은 추가 사용자 결정이 아니라 backend OpenAPI 보완 또는 명시적
|
||||
후속/제외 기록이 필요한 외부 의존이다.
|
||||
|
||||
```text
|
||||
Phase 0 Setup
|
||||
└─ Phase 1 Platform + Auth/Authz + Shared Components
|
||||
@@ -635,7 +671,7 @@ npm run build
|
||||
|
||||
**Goal 실행 `P2-T2`:** production API 경계를 그대로 사용하는 deterministic auth fixture, in-memory store와 mock mode 안내를 완성한다.
|
||||
|
||||
- **시작 조건:** `P2-T1` 완료, API Contract §1·§3 확인.
|
||||
- **시작 조건:** `P2-T1` 완료, PRD `AUTH-001~013`과 Phase 1에서 검증한 기존 인증 계약 확인. 인증 operation은 현 OpenAPI 범위 밖이며 `EXT-006`으로 유지한다.
|
||||
- **완료 증거:** auth handler/store/banner focused test와 login → protected shell mock preview E2E 기록.
|
||||
- **범위 밖:** 도메인별 endpoint handler와 계약 미제공 fixture.
|
||||
|
||||
@@ -741,7 +777,7 @@ npm run build:prod
|
||||
- 잘못된 origin의 login이 200을 반환하고 invalid·revoked JWT logout이 200을 반환하는 현재 동작을 각각 실패 test로 재현한다.
|
||||
- 완료 증거:
|
||||
- 설정된 `VITE_API_BASE_URL`의 정확한 URL·method만 handler가 처리하고 다른 origin은 `onUnhandledRequest: "error"` 경계에 남음
|
||||
- invalid·revoked JWT logout은 API Contract §1.2의 401 오류 envelope를 반환
|
||||
- invalid·revoked JWT logout은 Phase 1에서 검증한 기존 인증 계약의 401 오류 envelope를 반환
|
||||
- 정상 login → logout → login 흐름과 403 fixture 회귀 없음
|
||||
- focused handler test, mock preview E2E와 P2-GATE 실행 기록
|
||||
- 범위 밖:
|
||||
@@ -1331,7 +1367,7 @@ npm run build:prod
|
||||
|
||||
## Phase 3. Character workspace vertical slice
|
||||
|
||||
**목표:** ADMIN이 active Character를 검색·생성·선택하고 workspace에서 상세·수정·soft delete까지 완료한다.
|
||||
**목표:** ADMIN이 Character를 검색·생성·선택하고 workspace에서 상세·수정·soft delete 요청과 후속 목록 재조회를 완료한다.
|
||||
|
||||
**Phase Goal `P3`:** Task 3.1 → 3.4와 Phase 3 Gate로 Character workspace vertical slice를 완성한다.
|
||||
|
||||
@@ -1339,9 +1375,9 @@ npm run build:prod
|
||||
- **완료 조건:** `P3-T1`~`P3-T4`, `P3-GATE` 완료. 외부 의존은 구현 또는 명시적 제외 결정으로 종결.
|
||||
- **실행 순서:** 계약 확인 → 목록/workspace → mutation → 반응형·접근성.
|
||||
|
||||
**요구사항:** `CHAR-001~014`, `FILE-001~002`, `FILE-008~010`, `FILE-012`, PRD `7`, `9`의 Character 범위.
|
||||
**요구사항:** `CHAR-001~018`, `FILE-001~002`, `FILE-008~010`, `FILE-012`, PRD `7`, `9`의 Character 범위.
|
||||
|
||||
**외부 의존:** `CHAR-013` original work lookup·미선택 직렬화, 신규 Character 오류 계약.
|
||||
**외부 의존:** `EXT-007`/`CHAR-012`의 `searchTerm` 지정 결과에 대한 active-only 보장, `EXT-001`/`CHAR-013` original work lookup, `EXT-011` 신규 Character 오류 message key. `searchTerm` 생략 시 활성 목록은 OpenAPI에 명시돼 있다. `originalWorkId` 미선택은 key 생략과 `null`이 모두 가능하며 frontend canonical serializer만 하나로 고정한다.
|
||||
|
||||
**주요 Files:**
|
||||
|
||||
@@ -1359,31 +1395,45 @@ npm run build:prod
|
||||
- Create: `tests/e2e/character-workspace.spec.ts`
|
||||
- Modify: `src/app/router.tsx`, `src/app/route-paths.ts`
|
||||
|
||||
#### Phase 3 Task 실행 계약
|
||||
|
||||
| Goal | Files | Interfaces | TDD·검증 기준 |
|
||||
|---|---|---|---|
|
||||
| `P3-T1` | Modify: `docs/20260725_AI캐릭터관리자웹/prd.md`, `docs/20260725_AI캐릭터관리자웹/plan-task.md`; Read: `docs/20260725_AI캐릭터관리자웹/api-contract.openapi.json`; Test: 없음 | Consumes: OpenAPI `Character*` schema·4 operation. Produces: `CharacterListResponse`, `CharacterDetailResponse`, create/update multipart와 screen inventory | **TDD 예외:** 외부 계약 조사 Task다. `node -e "JSON.parse(require('fs').readFileSync('docs/20260725_AI캐릭터관리자웹/api-contract.openapi.json','utf8'))"`와 `rg -n 'CHAR-0(1[2-8]|0[1-9])' docs/20260725_AI캐릭터관리자웹/prd.md docs/20260725_AI캐릭터관리자웹/plan-task.md`가 exit 0인지 확인한다. 수동 확인: 제공·미제공 범위가 PRD와 일치한다. |
|
||||
| `P3-T2` | Create: `src/features/characters/api/character-api.ts`, `src/features/characters/model/types.ts`, `src/features/characters/pages/{CharacterListPage,CharacterDetailPage}.tsx`, `src/features/characters/components/{CharacterList,CharacterListItem,CharacterProfile}.tsx`, `src/features/characters/tests/character-list.test.tsx`, `src/layouts/CharacterWorkspaceLayout.tsx`, `src/layouts/CharacterWorkspaceLayout.test.tsx`; Modify: `src/app/router.tsx`, `src/app/route-paths.ts` | Consumes: `CharacterListResponse`, `CharacterDetailResponse`. Produces: `getCharacters({searchTerm,page,size})`, `getCharacter(characterId)`, list/workspace route | **TDD 적용:** `npm run test:run -- src/features/characters/tests/character-list.test.tsx src/layouts/CharacterWorkspaceLayout.test.tsx`; 기대 `exit 0`. 수동 확인: searchTerm request와 deep link/read-only 상태. |
|
||||
| `P3-T3` | Create: `src/features/characters/schemas/character-schema.ts`, `src/features/characters/validation/character-image-policy.ts`, `src/features/characters/pages/CharacterFormPage.tsx`, `src/features/characters/components/{CharacterForm,CharacterImageField}.tsx`, `src/features/characters/tests/{character-api.test.ts,character-form.test.tsx}`; Modify: `src/features/characters/api/character-api.ts` | Consumes: Character create/update multipart. Produces: `createCharacter`, `updateCharacter`, `deactivateCharacter`, form serializer | **TDD 적용:** `npm run test:run -- src/features/characters/tests/character-api.test.ts src/features/characters/tests/character-form.test.tsx`; 기대 `exit 0`. 수동 확인: 필수 image/systemPrompt, crop, 저장·목록 이동. |
|
||||
| `P3-T4` | Modify: `src/features/characters/pages/{CharacterListPage,CharacterDetailPage,CharacterFormPage}.tsx`, `src/features/characters/components/{CharacterList,CharacterListItem,CharacterProfile,CharacterForm,CharacterImageField}.tsx`; Test: `tests/e2e/character-workspace.spec.ts` | Consumes: P3-T2/T3 UI. Produces: viewport·keyboard capability evidence | **TDD 적용:** `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts`; 기대 지원 project 전부 통과. 수동 확인: 320/768/1280px, 200% zoom, keyboard, axe. |
|
||||
|
||||
`P3-T2`~`P3-T4`는 각 row의 test에 가장 작은 실패 assertion을 먼저
|
||||
추가해 RED를 확인하고, 최소 구현으로 같은 명령을 통과시킨 뒤 관련
|
||||
feature test·typecheck·lint를 실행한다. 각 Task 마지막에는 RED, GREEN,
|
||||
REFACTOR와 수동 확인의 실제 결과를 `§7 검증 기록`에 누적한다.
|
||||
|
||||
### Task 3.1 Phase 계약 확인
|
||||
|
||||
**Goal 실행 `P3-T1`:** Character 구현 계약, mock scenario와 화면 component map을 확정한다.
|
||||
|
||||
- **시작 조건:** `P2-GATE` 완료, PRD `CHAR-001~014`, `MOCK-001~009`와 API Contract §4 확인.
|
||||
- **시작 조건:** `P2-GATE` 완료, PRD `CHAR-001~018`, `MOCK-001~009`와 OpenAPI Character 4개 operation/schema 확인.
|
||||
- **완료 증거:** 체크박스 전체, 제공 계약 또는 제외 결정의 세 문서 일치, 상태/action inventory.
|
||||
- **범위 밖:** 계약을 추정한 production adapter와 Character 화면 구현.
|
||||
|
||||
- [ ] 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에 기록한다.
|
||||
- [ ] OpenAPI에서 목록 `searchTerm/page/size`, `data.totalCount/content`, item `id`, 상세 `characterUUID/originalWork`, mutation `data=null`을 contract fixture로 고정한다.
|
||||
- [ ] original work lookup endpoint, DTO, search/page를 확인하고, 미제공이면 network control을 제외한다. `originalWorkId` 미선택 serializer는 허용된 omit 또는 `null` 중 하나를 contract test로 고정한다.
|
||||
- [ ] OpenAPI 공통 400/401/403/404/405/406/415/500과 `ApiErrorResponse`를 fixture에 기록한다. Character 전용 message key는 미제공으로 표시하고 분기하지 않는다.
|
||||
- [ ] 목록·상세·form·workspace의 상태/action inventory를 작성하고 Page는 route/query/policy 조합, feature component는 Character 표시·입력 규칙, shared component는 Phase 1 contract를 사용하도록 component map을 확정한다.
|
||||
- [ ] 제공 계약 범위의 Character seed, loading·empty·error·success와 CRUD/deactivate browser handler 시나리오를 확정한다. 계약 미제공 original work network fixture는 만들지 않는다.
|
||||
|
||||
### Task 3.2 목록·선택·workspace
|
||||
|
||||
**Goal 실행 `P3-T2`:** active Character 목록·검색·선택과 URL 기반 workspace 복원을 완성한다.
|
||||
**Goal 실행 `P3-T2`:** Character 목록·검색·선택과 URL 기반 workspace 복원을 완성한다.
|
||||
|
||||
- **시작 조건:** `P3-T1` 완료.
|
||||
- **완료 증거:** 체크박스 전체, route/list/workspace test와 read-only/error 상태 검증 기록.
|
||||
- **범위 밖:** Character 생성·수정·비활성화 form.
|
||||
|
||||
- [ ] 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를 작성한다.
|
||||
- [ ] 목록 UI의 `search` 상태를 API `searchTerm`으로 직렬화하고 `page`, `size` URL query 보존, `data.content` 역직렬화와 loading·empty·error·retry test를 작성한다.
|
||||
- [ ] 목록 request에 `isActive`·`activeStatus`가 없고 client-side 활성 filter도 없는 contract test를 작성한다. active-only 보장은 외부 의존으로 남긴다.
|
||||
- [ ] Character 선택 시 URL의 `characterId`로 workspace에 진입하고 새로고침·deep link가 동작하는 test를 작성한다.
|
||||
- [ ] workspace header에 image, name, active 상태, `characterId`와 탭·breadcrumb를 표시한다.
|
||||
- [ ] 상세 성공 응답이 `isActive=false`이면 read-only 배너와 중앙 write policy로 모든 mutation 진입점을 차단한다.
|
||||
@@ -1398,17 +1448,19 @@ npm run build:prod
|
||||
- **완료 증거:** 체크박스 전체, serializer/form/image/deactivate test, `OQ-009` 결정과 검증 기록.
|
||||
- **범위 밖:** 계약 미제공 original-work integration과 하위 도메인 mutation.
|
||||
|
||||
- [ ] create multipart가 `request` JSON part와 optional image만 보내며 `isActive`, `externalCharacterId`를 포함하지 않는 test를 작성한다.
|
||||
- [ ] create multipart가 필수 `image`와 필수 `request` JSON part를 보내고 request에 `name`, `systemPrompt`, `description`을 포함하며 `isActive`, `externalCharacterId`를 포함하지 않는 test를 작성한다.
|
||||
- [ ] 일반 update는 `isActive`를 생략하고 soft delete만 `isActive=false`를 보내며 `true`를 보내지 않는 test를 작성한다.
|
||||
- [ ] name·description visible label, field error, 중복 제출 방지, dirty-form 이탈 확인을 test한다.
|
||||
- [ ] create-only `region`을 수정 화면에서 읽기 전용으로 표시하고 update payload에 보내지 않는 test를 작성한다.
|
||||
- [ ] name·systemPrompt·description visible label, field error, 중복 제출 방지, dirty-form 이탈 확인을 test한다.
|
||||
- [ ] OpenAPI optional scalar와 tags·hobbies·values·goals·relationships·personalities·backgrounds·memories 반복 입력을 create/update schema에 맞게 직렬화하는 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를 갱신한다.
|
||||
- [ ] 상세의 `characterUUID`는 읽기 전용으로 표시할 수 있지만 `externalCharacterId`로 이름을 바꾸지 않는다. 계약에 없는 creator member ID·nickname DTO/UI는 만들지 않고 creator 생성·동기화도 client가 수행하지 않는다.
|
||||
- [ ] create의 `data=null` 성공 후 목록을 무효화해 이동하고, update는 기존 `characterId`의 list/detail cache를 무효화한다.
|
||||
- [ ] 비활성화 AlertDialog가 영향·복원 미지원·hard delete 미지원을 설명하는 test를 작성한다.
|
||||
- [ ] soft delete 성공 후 active-only 목록 재조회, 목록 이동, 성공 toast를 확인하고 상세에 머물지 않는다.
|
||||
- [ ] Character form을 실제로 작성한 뒤 `OQ-009`의 `name`·`description` 상한 필요성을 판단하고, 구현 전 결정 문서를 갱신하거나 “상한 추가 없음”으로 종결한다.
|
||||
- [ ] soft delete 성공 후 목록 cache 무효화·재조회, 목록 이동과 성공 toast를 확인하고 상세에 머물지 않는다. 비활성 항목이 서버 결과에서 제외되는지는 active-only 계약 제공 후 server mode에서 검증한다.
|
||||
- [ ] 초기 Character form을 실제 페이지에서 확인한 뒤 `name`, `systemPrompt`, `description`과 tags·hobbies·values·goals·relationships·personalities·backgrounds·memories의 최대 길이·개수 권고안을 작성한다. backend 호환 확인 전에는 상한을 구현하지 않는다.
|
||||
- [ ] mock store가 create/update/deactivate 후 목록·상세를 같은 server response contract로 갱신하는 E2E를 작성한다.
|
||||
|
||||
### Task 3.4 Character 반응형·접근성
|
||||
@@ -1442,13 +1494,17 @@ npm run lint
|
||||
npm run build
|
||||
```
|
||||
|
||||
**Expected:** mock mode에서 login → Character 검색/생성 → 선택/workspace → 수정 → soft delete → active-only 목록 복귀의 최종 UI가 통과한다. server mode 결과는 별도로 기록하며 backend 미구현 404이면 `UI 확인 완료(mock) / 실제 서버 연동 대기`로 남긴다. original work 계약이 없으면 fixture를 추정하지 않는다.
|
||||
**Expected:** mock mode에서 login → Character 검색/생성 → 선택/workspace → 수정 → soft delete 요청 → 목록 재조회 UI가 OpenAPI request/response shape로 통과한다. server mode 결과는 별도로 기록하며 active-only·original work 계약이 없으면 fixture로 보장을 추정하지 않고 연동 대기로 남긴다.
|
||||
|
||||
**수동 확인:** 320/768/1280px와 200% zoom에서 검색·workspace·필수 image
|
||||
생성·수정·비활성화 Dialog를 keyboard-only로 확인하고, `searchTerm`,
|
||||
multipart part와 mutation 후 network 요청이 OpenAPI와 일치하는지 본다.
|
||||
|
||||
---
|
||||
|
||||
## Phase 4. Audio vertical slice
|
||||
|
||||
**목표:** 선택 Character의 Audio를 검색·검수·발행·수정·비활성화하고 대용량 upload를 안전하게 제어한다.
|
||||
**목표:** 선택 Character의 Audio를 제목 검색·검수·발행·수정·비활성화하고 대용량 upload를 안전하게 제어한다.
|
||||
|
||||
**Phase Goal `P4`:** Task 4.1 → 4.4와 Phase 4 Gate로 Audio 조회·재생·발행·upload slice를 완성한다.
|
||||
|
||||
@@ -1456,13 +1512,13 @@ npm run build
|
||||
- **완료 조건:** `P4-T1`~`P4-T4`, `P4-GATE` 완료. 오류·price 계약은 제공값 또는 명시된 최소 규칙으로 종결.
|
||||
- **실행 순서:** 계약 확인 → 목록/상세/player → form/upload → 반응형·접근성.
|
||||
|
||||
**요구사항:** `AUDIO-001~028`, `FILE-001~002`, `FILE-006~009`, `FILE-012~013`, PRD `9`의 Audio 범위.
|
||||
**요구사항:** `AUDIO-001~033`, `FILE-001~002`, `FILE-006~009`, `FILE-012~013`, PRD `9`의 Audio 범위.
|
||||
|
||||
**외부 의존:** Audio 도메인 오류 계약, optional P1 price 상한. price 상한이 없으면 `0 이상 정수`만 적용한다.
|
||||
**외부 의존:** `EXT-007` active-only 반환 보장, `EXT-011` Audio 도메인 오류 message key, `EXT-010` backend 파일/container/codec 검증 계약, `EXT-009` optional P1 price 상한. status filter·답변 없는 status badge는 현재 범위에서 제외한다.
|
||||
|
||||
**주요 Files:**
|
||||
|
||||
- Create: `src/features/audio-contents/api/{audio-content-api,audio-content-theme-api,series-options-api,upload-audio-content}.ts`
|
||||
- Create: `src/features/audio-contents/api/{audio-content-api,audio-content-theme-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`
|
||||
@@ -1472,32 +1528,46 @@ npm run build
|
||||
- Create: `src/features/audio-contents/tests/{audio-list,audio-player,audio-form}.test.tsx`
|
||||
- Create: `tests/e2e/audio-content.spec.ts`
|
||||
|
||||
#### Phase 4 Task 실행 계약
|
||||
|
||||
| Goal | Files | Interfaces | TDD·검증 기준 |
|
||||
|---|---|---|---|
|
||||
| `P4-T1` | Modify: `docs/20260725_AI캐릭터관리자웹/prd.md`, `docs/20260725_AI캐릭터관리자웹/plan-task.md`; Read: `docs/20260725_AI캐릭터관리자웹/api-contract.openapi.json`; Test: 없음 | Consumes: `AudioContent*`, `AudioTheme*` schema·5 Audio operation. Produces: list/detail/create/update/theme contract map과 screen inventory | **TDD 예외:** 외부 계약 조사 Task다. OpenAPI JSON parse와 `rg -n 'AUDIO-0(2[7-9]|3[0-3])'` 문서 추적 검사를 실행한다. 기대 `exit 0`. 수동 확인: status·seriesIds·수정 file 제외가 명시된다. |
|
||||
| `P4-T2` | Create: `src/features/audio-contents/api/audio-content-api.ts`, `src/features/audio-contents/model/types.ts`, `src/features/audio-contents/pages/{AudioContentListPage,AudioContentDetailPage}.tsx`, `src/features/audio-contents/components/{AudioContentList,AudioContentListItem,AudioContentSummary}.tsx`, `src/features/audio-contents/tests/{audio-list,audio-player}.test.tsx` | Consumes: `AudioContentListResponse`, `AudioContentDetailResponse`. Produces: `getAudioContents({characterId,search_word,page,size})`, `getAudioContent({characterId,contentId,timezone})`와 player UI | **TDD 적용:** `npm run test:run -- src/features/audio-contents/tests/audio-list.test.tsx src/features/audio-contents/tests/audio-player.test.tsx`; 기대 `exit 0`. 수동 확인: 2자 검색, timezone, 단일 재생·no-auto-refetch. |
|
||||
| `P4-T3` | Create: `src/features/audio-contents/api/{audio-content-theme-api,upload-audio-content}.ts`, `src/features/audio-contents/schemas/audio-content-schema.ts`, `src/features/audio-contents/validation/audio-cover-policy.ts`, `src/features/audio-contents/pages/AudioContentFormPage.tsx`, `src/features/audio-contents/components/{AudioContentForm,AudioContentThemeSelect,ReleaseScheduleField}.tsx`, `src/features/audio-contents/tests/{audio-contract.test.ts,audio-upload.test.ts,audio-form.test.tsx}`; Modify: `src/features/audio-contents/api/audio-content-api.ts` | Consumes: `AudioContentCreateRequest`, `AudioContentUpdateRequest`, `AudioContentTheme`. Produces: `createAudioContent`, `updateAudioContent`, `deactivateAudioContent`, upload adapter | **TDD 적용:** `npm run test:run -- src/features/audio-contents/tests/audio-contract.test.ts src/features/audio-contents/tests/audio-upload.test.ts src/features/audio-contents/tests/audio-form.test.tsx`; 기대 `exit 0`. 수동 확인: contentFile, local releaseDate, theme, 진행률·취소·재시도. |
|
||||
| `P4-T4` | Modify: `src/features/audio-contents/pages/{AudioContentListPage,AudioContentDetailPage,AudioContentFormPage}.tsx`, `src/features/audio-contents/components/{AudioContentList,AudioContentListItem,AudioContentSummary,AudioContentForm,AudioContentThemeSelect,ReleaseScheduleField}.tsx`; Test: `tests/e2e/audio-content.spec.ts` | Consumes: P4-T2/T3 UI. Produces: viewport·keyboard capability evidence | **TDD 적용:** `npm run e2e:mock -- tests/e2e/audio-content.spec.ts`; 기대 지원 project 전부 통과. 수동 확인: 320px player, 200% zoom, keyboard, axe. |
|
||||
|
||||
`P4-T2`~`P4-T4`는 각 row의 focused test로 RED → GREEN →
|
||||
REFACTOR를 실행하고, 관련 feature test·typecheck·lint 결과와 수동 확인을
|
||||
`§7 검증 기록`에 누적한다.
|
||||
|
||||
### Task 4.1 Phase 계약 확인
|
||||
|
||||
**Goal 실행 `P4-T1`:** Audio 오류·price·theme·상태 계약, mock scenario와 component map을 확정한다.
|
||||
|
||||
- **시작 조건:** `P3-T2`, `P2-GATE` 완료, PRD `AUDIO-001~028`, `MOCK-001~009`와 API Contract §5 확인.
|
||||
- **시작 조건:** `P3-T2`, `P2-GATE` 완료, PRD `AUDIO-001~033`, `MOCK-001~009`와 OpenAPI Audio·theme operation/schema 확인.
|
||||
- **완료 증거:** 체크박스 전체, contract fixture와 상태/action inventory의 세 문서 일치.
|
||||
- **범위 밖:** 오류 status/key 또는 price 상한 추정과 Audio UI 구현.
|
||||
|
||||
- [ ] Audio 신규 오류 status/message key와 backend container·codec 오류 fixture를 기록한다.
|
||||
- [ ] 오디오 테마 목록 `GET /api/v2/admin/ai-characters/audio-content-themes`가 query/body 없이 호출되고 `themeId`, `themeName`, `imageUrl` 배열을 반환하는 contract fixture를 기록한다.
|
||||
- [ ] OpenAPI 공통 오류 status·shape를 fixture에 기록하고 Audio 전용 message key와 backend container·codec 오류 계약은 미제공으로 표시한다. 정확한 fixture를 추정하지 않는다.
|
||||
- [ ] 오디오 테마 목록 `GET /api/v2/admin/ai-characters/audio-content-themes`가 query/body 없이 호출되고 `data[]`의 `id`, `theme`, `image`를 반환하는 contract fixture를 기록한다.
|
||||
- [ ] price 최대값이 제공되면 schema와 경계 test를 추가하고, 없으면 상한을 만들지 않는다.
|
||||
- [ ] status query 미전송 시 server가 결과 집합을 결정한다는 계약을 유지하고 client fixture에서 임의 집합을 강제하지 않는다.
|
||||
- [ ] 목록 `search_word/page/size`, `data.totalCount/items`, 상세 필수 `timezone`, 생성 `contentFile/coverImage/request`, 생성 `data.contentId`, 수정 `data=null`을 contract fixture로 고정한다.
|
||||
- [ ] status query·status field가 없음을 고정하고 client status filter·status enum을 만들지 않는다.
|
||||
- [ ] 목록·상세·player·form/upload의 상태/action inventory를 작성하고 Page는 route/query/policy 조합, feature component는 Audio 규칙, shared component는 Phase 1 contract를 사용하도록 component map을 확정한다.
|
||||
- [ ] 제공 계약 범위의 Audio seed와 목록·상세·player·theme·upload CRUD browser handler 시나리오를 확정한다.
|
||||
|
||||
### Task 4.2 목록·상세·player
|
||||
|
||||
**Goal 실행 `P4-T2`:** Audio active-only 목록·상세 복원과 안전한 단일 재생 흐름을 완성한다.
|
||||
**Goal 실행 `P4-T2`:** Audio 목록·상세 복원과 안전한 단일 재생 흐름을 완성한다.
|
||||
|
||||
- **시작 조건:** `P4-T1` 완료.
|
||||
- **완료 증거:** 체크박스 전체, list/detail/player test와 signed URL 비기록·no-auto-refetch 기록.
|
||||
- **범위 밖:** 생성·수정·upload form.
|
||||
|
||||
- [ ] status type과 filter가 `OPEN | SCHEDULED`만 허용하고 서버 값을 client가 재계산하지 않는 test를 작성한다.
|
||||
- [ ] 검색·status·page URL 보존, active-only request, loading·empty·error·retry를 test한다.
|
||||
- [ ] Audio detail route의 직접 진입과 새로고침에서 같은 resource를 복원하는 test를 작성한다.
|
||||
- [ ] UI 검색어가 2자 이상일 때만 API `search_word`로 직렬화되고 `page`, `size` URL 상태와 `data.items` 역직렬화, loading·empty·error·retry가 동작하는 test를 작성한다.
|
||||
- [ ] status query·활성 query·client-side status/active filter request가 0회임을 test한다.
|
||||
- [ ] Audio detail route의 직접 진입과 새로고침에서 `timezone=Asia/Seoul`을 보내 같은 resource를 복원하는 test를 작성한다.
|
||||
- [ ] 목록과 상세가 Phase 1 `AdminAudioPlayer`를 조합하고 play/pause, seek, current/duration, volume, speed, keyboard를 지원하는 integration test를 작성한다.
|
||||
- [ ] 한 player 재생 시 기존 player가 정지되고 명시적 download button이 없음을 test한다.
|
||||
- [ ] media error를 signed URL 만료로 추정하지 않고 일반 오류·수동 재시도·페이지 새로고침 안내를 표시한다.
|
||||
@@ -1513,22 +1583,23 @@ npm run build
|
||||
- **완료 증거:** 체크박스 전체, contract/form/upload/file-boundary test, `OQ-009` 결정과 검증 기록.
|
||||
- **범위 밖:** resumable upload, client codec 판정, 계약 없는 price 상한.
|
||||
|
||||
- [ ] 생성은 cover image와 audio file 필수, 수정 교체 파일은 optional이며 미전송 시 기존 media 유지임을 test한다.
|
||||
- [ ] 생성 multipart의 `contentFile`, `coverImage`, `request`가 필수이고 수정에는 optional `coverImage`와 `request`만 있으며 content file 교체 part·UI가 없음을 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한다.
|
||||
- [ ] 생성 form은 오디오 테마 목록을 불러와 visible label이 있는 선택 UI를 제공하고, 미선택 제출을 차단하며 선택한 `themeId`를 create payload에 포함하는 test를 작성한다.
|
||||
- [ ] 즉시 공개 기본값은 날짜 입력을 비활성화·초기화하고 `releaseDateUtc=null`을 보낸다.
|
||||
- [ ] 예약 공개는 미래 Asia/Seoul 시각만 받고 UTC ISO-8601 `Z`로 변환하는 test를 작성한다.
|
||||
- [ ] 수정 form은 server `releaseDateUtc/status`로 초기화하고 사용자가 바꾸지 않으면 기존 값을 유지한다.
|
||||
- [ ] 제공된 active Series 목록 endpoint를 사용하는 options request와 `seriesIds` 다중 선택·keyboard 제거를 test한다.
|
||||
- [ ] create payload에 필수 `themeId`가 있고 `status`, `isActive`가 없으며 update/soft delete의 `isActive` 규칙이 지켜지는 contract test를 작성한다.
|
||||
- [ ] 즉시 공개 기본값은 날짜 입력을 비활성화·초기화하고 `releaseDate=null`, `timezone="Asia/Seoul"`을 보낸다.
|
||||
- [ ] 예약 공개는 미래 Asia/Seoul 시각만 받고 `yyyy-MM-dd HH:mm` 문자열과 `timezone="Asia/Seoul"`을 보내며 UTC `Z`로 변환하지 않는 test를 작성한다.
|
||||
- [ ] 수정 form은 계약에 없는 release schedule·theme·series·content file 변경 control을 만들지 않고 기존 값을 읽기 전용으로 표시한다.
|
||||
- [ ] create request에 필수 `title`, `detail`, `tags`, `price`, 유효한 `themeId`가 있고 `status`, `isActive`, `seriesIds`가 없음을 contract test로 고정한다.
|
||||
- [ ] create optional purchase/limited/adult/preview/point/comment/detail/language field의 enum·type·OpenAPI default와 serializer를 test한다.
|
||||
- [ ] update request는 `title`, `detail`, `tags`, `price`, `isAdult`, `isActive`, `isPointAvailable`, `isCommentAvailable` 이외 field를 보내지 않고 soft delete에만 `isActive=false`를 보낸다.
|
||||
- [ ] 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` 상한 필요성을 판단하고, 구현 전 결정 문서를 갱신하거나 “상한 추가 없음”으로 종결한다.
|
||||
- [ ] create 성공의 `data.contentId`로 상세에 이동하고, update/soft delete의 `data=null` 성공은 기존 ID cache를 무효화한다. soft delete 후 목록 이동과 toast를 제공하며 active-only 제거는 외부 계약 제공 후 server mode에서 검증한다.
|
||||
- [ ] 초기 Audio form을 실제 페이지에서 확인한 뒤 `title`, `detail`, `tags`의 최대 길이 권고안을 작성한다. backend 호환 확인 전에는 상한을 구현하지 않는다.
|
||||
- [ ] mock handler가 multipart request contract를 검증하고 create/update/deactivate 후 같은 store의 list/detail을 갱신하는 E2E를 작성한다.
|
||||
|
||||
### Task 4.4 Audio 반응형·접근성
|
||||
@@ -1561,23 +1632,27 @@ npm run lint
|
||||
npm run build
|
||||
```
|
||||
|
||||
**Expected:** mock mode에서 Audio 즉시/예약 생성 → 진행률/취소/재시도 → 목록·상세 재생 → 수정 → soft delete의 최종 UI가 통과하고 media error가 자동 refetch·자동 재생을 0회 발생시킨다. server mode는 별도 결과를 기록한다.
|
||||
**Expected:** mock mode에서 OpenAPI `contentFile`·`releaseDate` 계약으로 Audio 즉시/예약 생성 → 진행률/취소/재시도 → 검색·상세 재생 → 허용 field 수정 → soft delete 요청의 UI가 통과하고 media error가 자동 refetch·자동 재생을 0회 발생시킨다. status filter·seriesIds·content file 교체 request는 0건이며 server mode와 active-only 결과는 별도로 기록한다.
|
||||
|
||||
**수동 확인:** 320px player와 desktop/tablet form을 keyboard-only로
|
||||
확인하고, `search_word`, `timezone`, `contentFile`, `releaseDate`, theme
|
||||
field와 수정 금지 control이 실제 network 요청·화면에 일치하는지 본다.
|
||||
|
||||
---
|
||||
|
||||
## Phase 5. Series vertical slice
|
||||
|
||||
**목표:** 선택 Character의 Series를 생성·수정·비활성화하고 Audio 연결·해제와 활성 Series 전체 순서를 관리한다.
|
||||
**목표:** 선택 Character의 Series를 조회하고, 계약 제공 후 생성·수정·비활성화하며 Audio 연결·해제와 서버가 반환한 Series 전체 순서를 관리한다.
|
||||
|
||||
**Phase Goal `P5`:** Task 5.1 → 5.4와 Phase 5 Gate로 Series CRUD·연결·전체 순서 slice를 완성한다.
|
||||
**Phase Goal `P5`:** Task 5.1 → 5.5와 Phase 5 Gate로 Series 조회, 계약 제공 후 CRUD, 연결·전체 순서 slice를 완성한다.
|
||||
|
||||
- **시작 조건:** `P4-T2`의 Audio 조회 API 완료.
|
||||
- **완료 조건:** `P5-T1`~`P5-T4`, `P5-GATE` 완료. 계약 없는 genre·연결·순서는 구현 또는 명시적 제외 결정으로 종결.
|
||||
- **실행 순서:** 계약 확인 → CRUD → 연결/순서 → 반응형·접근성.
|
||||
- **완료 조건:** `P5-T1`~`P5-T5`, `P5-GATE` 완료. genre·edit DTO 외부 의존은 제공 또는 명시적 후속/제외 상태로 종결.
|
||||
- **실행 순서:** 계약 확인 → 목록/상세 → CRUD → 연결/순서 → 반응형·접근성.
|
||||
|
||||
**요구사항:** `SERIES-001~013`, `FILE-001~002`, `FILE-005`, `FILE-007~009`, `FILE-012`, `FILE-015`, PRD `9`의 Series 범위.
|
||||
**요구사항:** `SERIES-001~018`, `FILE-001~002`, `FILE-005`, `FILE-007~009`, `FILE-012`, `FILE-015`, PRD `9`의 Series 범위.
|
||||
|
||||
**외부 의존:** genre lookup(`SERIES-011`), 연결 후보, 50개 초과 전체 로딩, 누락 ID, 동시 충돌, 신규 오류 계약.
|
||||
**외부 의존:** `EXT-002` genre lookup(`SERIES-007`, `SERIES-011`)은 유효한 `genreId`가 필요한 생성 flow를 차단한다. 상세의 표시 문자열을 update enum/ID로 안전하게 복원할 `EXT-003` edit DTO(`SERIES-017`), `EXT-007` active-only 반환 보장과 `EXT-011` 도메인별 오류 message key도 외부 의존이다. 연결 후보·page 기반 전체 로딩 endpoint는 OpenAPI에 제공됐다.
|
||||
|
||||
**주요 Files:**
|
||||
|
||||
@@ -1591,63 +1666,92 @@ npm run build
|
||||
- Create: `src/features/series/tests/{series-form,series-contents,series-order}.test.tsx`
|
||||
- Create: `tests/e2e/series.spec.ts`
|
||||
|
||||
#### Phase 5 Task 실행 계약
|
||||
|
||||
| Goal | Files | Interfaces | TDD·검증 기준 |
|
||||
|---|---|---|---|
|
||||
| `P5-T1` | Modify: `docs/20260725_AI캐릭터관리자웹/prd.md`, `docs/20260725_AI캐릭터관리자웹/plan-task.md`; Read: `docs/20260725_AI캐릭터관리자웹/api-contract.openapi.json`; Test: 없음 | Consumes: `Series*` schema·9 operation. Produces: CRUD·contents/search·order contract map, 외부 의존과 screen inventory | **TDD 예외:** 외부 계약 조사 Task다. OpenAPI JSON parse와 `rg -n 'SERIES-01[4-8]'` 문서 추적 검사를 실행한다. 기대 `exit 0`. 수동 확인: genre/edit DTO 차단과 제공된 link/order 범위가 분리된다. |
|
||||
| `P5-T2` | Create: `src/features/series/api/series-api.ts`, `src/features/series/model/types.ts`, `src/features/series/pages/{SeriesListPage,SeriesDetailPage}.tsx`, `src/features/series/components/{SeriesList,SeriesListItem,SeriesSummary}.tsx`, `src/features/series/tests/series-contract.test.ts` | Consumes: `SeriesListResponse`, `SeriesDetailResponse`. Produces: `getSeries({characterId,page,size})`, `getSeriesDetail({characterId,seriesId})`와 read-only 조회 UI | **TDD 적용:** `npm run test:run -- src/features/series/tests/series-contract.test.ts`; 기대 `exit 0`. 수동 확인: 목록 enum과 상세 표시 문자열, 직접 링크 조회. |
|
||||
| `P5-T3` | genre/edit 계약 제공 후 Create: `src/features/series/schemas/series-schema.ts`, `src/features/series/validation/series-image-policy.ts`, `src/features/series/pages/SeriesFormPage.tsx`, `src/features/series/components/{SeriesForm,PublishedDaysField,GenreCombobox}.tsx`, `src/features/series/tests/series-form.test.tsx`; Modify: `src/features/series/api/series-api.ts`, `src/features/series/tests/series-contract.test.ts` | Consumes: create/update multipart와 genre/edit DTO. Produces: `createSeries`, `updateSeries`, `deactivateSeries` | **TDD 적용:** genre lookup·edit DTO 제공 후 `npm run test:run -- src/features/series/tests/series-contract.test.ts src/features/series/tests/series-form.test.tsx`; 기대 `exit 0`. 수동 확인: keyword/image/state와 직접 edit 초기화. |
|
||||
| `P5-T4` | Create: `src/features/series/pages/SeriesOrderPage.tsx`, `src/features/series/components/{SeriesContents,SeriesOrderList}.tsx`, `src/features/series/tests/{series-contents,series-order}.test.tsx`; Modify: `src/features/series/api/series-api.ts`, `src/features/series/pages/SeriesDetailPage.tsx` | Consumes: `SeriesContentListResponse`, `SeriesContentSearchItem`, `SeriesContentAddRequest`, `SeriesOrderUpdateRequest`. Produces: `searchUnlinkedContents`, `addSeriesContents`, `removeSeriesContent`, `updateSeriesOrder` | **TDD 적용:** `npm run test:run -- src/features/series/tests/series-contents.test.tsx src/features/series/tests/series-order.test.tsx`; 기대 `exit 0`. 수동 확인: contentIdList·ids request와 keyboard reorder. |
|
||||
| `P5-T5` | Modify: `src/features/series/pages/{SeriesListPage,SeriesDetailPage,SeriesFormPage,SeriesOrderPage}.tsx`, `src/features/series/components/{SeriesList,SeriesListItem,SeriesSummary,SeriesForm,PublishedDaysField,GenreCombobox,SeriesContents,SeriesOrderList}.tsx`; Test: `tests/e2e/series.spec.ts` | Consumes: P5-T2~T4 활성 UI. Produces: viewport·keyboard capability evidence | **TDD 적용:** `npm run e2e:mock -- tests/e2e/series.spec.ts`; 기대 활성 계약 범위 통과. 수동 확인: 320px 조회, 200% zoom, keyboard, axe. |
|
||||
|
||||
`P5-T2`~`P5-T5`는 각 row의 focused test로 RED → GREEN →
|
||||
REFACTOR를 실행한다. 외부 계약 때문에 RED test의 기대 동작 자체를 정할
|
||||
수 없으면 test를 skip하지 않고 해당 network 범위를 시작하지 않으며,
|
||||
대체 검증과 남은 조건을 `§7 검증 기록`에 남긴다.
|
||||
|
||||
### Task 5.1 Phase 계약 확인
|
||||
|
||||
**Goal 실행 `P5-T1`:** Series genre·연결 후보·전체 순서·오류 계약, mock scenario와 component map을 확정한다.
|
||||
|
||||
- **시작 조건:** `P4-T2` 완료, PRD `SERIES-001~013`, `MOCK-001~009`와 API Contract §6 확인.
|
||||
- **시작 조건:** `P4-T2` 완료, PRD `SERIES-001~018`, `MOCK-001~009`와 OpenAPI Series 9개 operation/schema 확인.
|
||||
- **완료 증거:** 체크박스 전체, 제공 계약 또는 제외 결정의 세 문서 일치, 상태/action inventory.
|
||||
- **범위 밖:** 계약 없는 genre/연결/순서 network 구현.
|
||||
- **범위 밖:** 계약 없는 genre lookup·edit DTO·active-only·도메인별 오류 동작의 추정 구현.
|
||||
|
||||
- [ ] genre lookup endpoint·DTO·search/page 계약을 기록한다.
|
||||
- [ ] 선택 Character의 연결 가능한 active Audio 후보 계약을 기록한다.
|
||||
- [ ] 활성 Series가 50개를 넘을 때 전체를 누락 없이 읽는 방식과 누락 ID·동시 변경 충돌 오류를 기록한다.
|
||||
- [ ] 계약이 없는 연결·순서·genre 기능은 추측 구현하지 않고 제외/후속 여부를 문서에서 먼저 결정한다.
|
||||
- [ ] 연결 후보 `GET .../contents/search?search_word=...`, 연결 `{contentIdList}`, 해제 body 없는 DELETE, 전체 순서 `{ids}` 계약을 기록한다.
|
||||
- [ ] 목록 `data.totalCount/items`와 `page/size`로 전체 Series page를 누락 없이 읽는 방식을 기록한다. 도메인별 누락 ID·동시 변경 오류 key는 제공되지 않았음을 외부 의존으로 남긴다.
|
||||
- [ ] 계약이 없는 genre lookup과 active-only 보장은 추측 구현하지 않고 영향 범위를 문서에서 먼저 확인한다.
|
||||
- [ ] 목록·상세·form·연결·순서 화면의 상태/action inventory를 작성하고 Page는 route/query/policy 조합, feature component는 Series 규칙, shared component는 Phase 1 contract를 사용하도록 component map을 확정한다.
|
||||
- [ ] 제공 계약 범위만 Series CRUD·연결·순서 browser fixture로 만들고 genre·후보·충돌 계약 미제공 부분은 mock에서도 추정하지 않는다.
|
||||
- [ ] 제공 계약 범위만 Series 목록·상세·연결·순서 browser fixture로 만들고 genre·edit DTO·active-only·충돌 계약 미제공 부분은 mock에서도 추정하지 않는다.
|
||||
|
||||
### Task 5.2 Series CRUD
|
||||
### Task 5.2 Series 목록·상세 조회
|
||||
|
||||
**Goal 실행 `P5-T2`:** Series CRUD, enum·요일·image와 soft delete 규칙을 완성한다.
|
||||
**Goal 실행 `P5-T2`:** OpenAPI가 제공한 Series 목록·상세 조회와 서로 다른 응답 DTO 표시 규칙을 완성한다.
|
||||
|
||||
- **시작 조건:** `P5-T1` 완료.
|
||||
- **완료 증거:** 체크박스 전체, contract/form/image/list/detail/deactivate test와 검증 기록.
|
||||
- **완료 증거:** 체크박스 전체, list/detail contract·route·state test와 검증 기록.
|
||||
- **범위 밖:** Series 생성·수정·비활성화와 Audio 연결·해제·전체 순서 저장.
|
||||
|
||||
- [ ] list가 `page`, `size`만 보내고 `data.totalCount/items`를 소비하며 활성 query·client 활성 filter 없이 loading·empty·error·retry를 제공하는 test를 작성한다.
|
||||
- [ ] Series detail route의 직접 진입과 새로고침에서 같은 resource를 복원하는 test를 작성한다.
|
||||
- [ ] 목록의 enum field와 상세의 표시용 `publishedDaysOfWeek`, `genre`, `keywords`, 한국어 `state` 문자열을 각 응답 DTO 그대로 표시하고 서로 역변환하지 않는 test를 작성한다.
|
||||
- [ ] 제공된 목록·상세 계약만 mock handler로 만들고 loading·empty·error·retry와 직접 링크 조회를 확인한다.
|
||||
|
||||
### Task 5.3 Series 생성·수정·비활성화
|
||||
|
||||
**Goal 실행 `P5-T3`:** 계약 제공 후 Series form, enum·요일·image와 soft delete 규칙을 완성한다.
|
||||
|
||||
- **시작 조건:** `P5-T2` 완료. genre lookup과 직접 링크 수정 form의 `genreId`·요일 enum·state enum을 제공하는 edit DTO/mapping 계약이 제공됨.
|
||||
- **완료 증거:** 체크박스 전체, contract/form/image/deactivate test와 검증 기록.
|
||||
- **범위 밖:** Audio 연결·해제와 전체 순서 저장.
|
||||
|
||||
- [ ] 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한다.
|
||||
- [ ] 생성 multipart에 필수 `image`와 `request`가 있고 request의 필수 `title`, `introduction`, `publishedDaysOfWeek`, `keyword`를 보내며 `state`, `isActive`, `keywords`가 없음을 test한다.
|
||||
- [ ] 수정에서 state 미선택은 key 생략, 선택은 유효 enum만 전송하고 `null`은 보내지 않는다.
|
||||
- [ ] `RANDOM`은 단독, 실제 요일은 하나 이상이어야 하는 schema·UI test를 작성한다.
|
||||
- [ ] genre 계약이 제공됐다면 이름 검색 후 `genreId`만 전송하는 Combobox를 test한다.
|
||||
- [ ] genre 이름 검색 후 유효한 `genreId`만 전송하는 Combobox를 test한다. OpenAPI binding 기본값 `0`은 선택값으로 허용하지 않는다.
|
||||
- [ ] 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한다.
|
||||
- [ ] mock store로 Series CRUD 후 list/detail과 server state enum이 일관되게 갱신되는 최종 UI를 확인한다.
|
||||
- [ ] edit DTO의 `genreId`·요일 enum·state enum으로 직접 링크 form을 초기화하고 상세의 표시용 문자열을 update enum/ID로 역변환하지 않는 test를 작성한다.
|
||||
- [ ] create-only `keyword`를 수정 화면에서 읽기 전용으로 표시하고 update payload에 보내지 않는 test를 작성한다.
|
||||
- [ ] create/update/soft delete의 `data=null`을 처리하고 일반 update와 soft delete의 `isActive` 규칙, 목록 재조회·이동·toast를 test한다.
|
||||
- [ ] mock store로 Series CRUD 후 list enum과 detail 표시 문자열의 서로 다른 DTO가 일관되게 갱신되는 UI를 확인한다.
|
||||
- [ ] 초기 Series form을 실제 페이지에서 확인한 뒤 `title`, `introduction`, `keyword`, `writer`, `studio`, `publishedDaysOfWeek`의 최대 길이·개수 권고안을 작성한다. backend 호환 확인 전에는 상한을 구현하지 않는다.
|
||||
|
||||
### Task 5.3 Audio 연결·해제·전체 순서
|
||||
### Task 5.4 Audio 연결·해제·전체 순서
|
||||
|
||||
**Goal 실행 `P5-T3`:** Series Audio 연결·해제와 active Series 전체 순서를 안전하게 관리한다.
|
||||
**Goal 실행 `P5-T4`:** Series Audio 연결·해제와 서버가 반환한 Series 전체 순서를 안전하게 관리한다.
|
||||
|
||||
- **시작 조건:** `P5-T1`, `P5-T2` 완료 및 관련 P0 계약 제공.
|
||||
- **시작 조건:** `P5-T1`, `P5-T2` 완료.
|
||||
- **완료 증거:** 체크박스 전체, link/unlink/reorder contract·interaction test, 충돌 보존, `OQ-009` 결정 기록.
|
||||
- **범위 밖:** 계약 없는 후보/전체 로딩/충돌 동작의 추정 구현.
|
||||
- **범위 밖:** 계약 없는 active-only·도메인별 충돌 동작의 추정 구현.
|
||||
|
||||
- [ ] 현재 연결 Audio 목록의 search/page와 상세 cache 동기화를 test한다.
|
||||
- [ ] 후보는 선택 Character의 active Audio로 제한하고 이미 연결된 항목을 중복 선택하지 않는다.
|
||||
- [ ] 연결 POST는 `{ contentIds }`, 해제 DELETE는 body 없음임을 contract test로 고정한다.
|
||||
- [ ] 현재 연결 Audio 목록은 `page/size`와 `data.totalCount/items`를 사용하고 제공되지 않은 search query를 보내지 않으며 상세 cache를 동기화한다.
|
||||
- [ ] 후보는 `GET .../contents/search?search_word=...` 결과만 사용하고 이미 연결된 항목을 중복 선택하지 않는다.
|
||||
- [ ] 연결 POST는 `{ contentIdList }`, 해제 DELETE는 body 없음임을 contract test로 고정한다.
|
||||
- [ ] 연결 해제 전 대상 title과 영향을 AlertDialog로 확인한다.
|
||||
- [ ] 순서 mode는 active Series 전체를 읽고 최종 순서의 모든 `seriesIds`를 한 번에 보낸다.
|
||||
- [ ] 순서 mode는 `totalCount`와 page/size로 Series 전체를 읽고 최종 순서의 모든 ID를 `{ ids }`로 한 번에 보낸다.
|
||||
- [ ] drag-and-drop과 동일한 결과를 keyboard·위/아래 button으로 만들 수 있는 test를 작성한다.
|
||||
- [ ] server의 누락 ID·동시 충돌 오류에서 기존 화면 순서를 보존하고 재조회/재시도 안내를 제공한다.
|
||||
- [ ] Series form·연결·순서 UI를 실제로 작성한 뒤 `OQ-009`의 title·introduction·keywords·writer·studio·days·contentIds·seriesIds 상한 필요성을 판단하고, 구현 전 결정 문서를 갱신하거나 “상한 추가 없음”으로 종결한다.
|
||||
- [ ] 계약 제공 후 mock handler가 연결·해제와 전체 순서 payload를 검증하고 store 결과를 반영하는 E2E를 작성한다.
|
||||
- [ ] 초기 연결·순서 UI를 실제 페이지에서 확인한 뒤 `contentIdList`, `ids`의 최대 개수 권고안을 작성한다. backend 호환 확인 전에는 상한을 구현하지 않는다.
|
||||
- [ ] mock handler가 연결·해제와 전체 순서 payload를 검증하고 store 결과를 반영하는 E2E를 작성한다.
|
||||
|
||||
### Task 5.4 Series 반응형·접근성
|
||||
### Task 5.5 Series 반응형·접근성
|
||||
|
||||
**Goal 실행 `P5-T4`:** Series viewport capability와 form·연결·정렬 접근성을 검증한다.
|
||||
**Goal 실행 `P5-T5`:** Series viewport capability와 활성 범위의 form·연결·정렬 접근성을 검증한다.
|
||||
|
||||
- **시작 조건:** `P5-T2`, `P5-T3`의 활성 범위 완료.
|
||||
- **시작 조건:** `P5-T2`와 `P5-T3`~`P5-T4` 중 계약이 제공된 활성 범위 완료.
|
||||
- **완료 증거:** 체크박스 전체, 320px·keyboard·200% zoom·axe E2E 기록.
|
||||
- **범위 밖:** 모바일 CRUD·연결·순서 mutation.
|
||||
|
||||
@@ -1659,7 +1763,7 @@ npm run build
|
||||
|
||||
**Goal 실행 `P5-GATE`:** Series mock UI journey와 실제 server integration 상태를 분리해 판정한다.
|
||||
|
||||
- **시작 조건:** `P5-T1`~`P5-T4` 완료.
|
||||
- **시작 조건:** `P5-T1`~`P5-T5` 완료. 외부 계약 때문에 실행하지 않은 범위는 대기/제외 상태와 재개 조건이 기록됨.
|
||||
- **완료 증거:** 아래 명령과 Expected 통과, 외부 의존 상태와 Phase 검증 기록.
|
||||
- **범위 밖:** 실패와 무관한 Community/FanTalk 구현.
|
||||
|
||||
@@ -1672,13 +1776,17 @@ npm run lint
|
||||
npm run build
|
||||
```
|
||||
|
||||
**Expected:** 제공 계약 범위의 mock mode에서 Series 생성 → 수정 → Audio 연결/해제 → active 전체 reorder → soft delete 최종 UI가 통과하고 잘못된 enum·부분 순서 payload가 생성되지 않는다. server mode는 별도 결과를 기록한다.
|
||||
**Expected:** 현재 제공된 계약으로 목록·상세 조회와 `contentIdList` 연결/해제, `ids` 전체 reorder UI가 통과한다. genre lookup·edit DTO 제공 후에는 필수 image와 `keyword`로 Series 생성 → 수정 → soft delete UI까지 통과하고 잘못된 enum·`keywords/contentIds/seriesIds` payload가 생성되지 않는다. 미제공 계약 범위는 대기로 유지하되 완료된 조회·연결·순서 상태를 되돌리지 않는다.
|
||||
|
||||
**수동 확인:** 현재 계약으로 320px 조회와 desktop/tablet 연결·keyboard
|
||||
reorder를 확인한다. genre/edit DTO 제공 후 생성·수정 form도 확인하고,
|
||||
표시용 상세 문자열이 update payload로 역변환되지 않는지 network에서 본다.
|
||||
|
||||
---
|
||||
|
||||
## Phase 6. Community vertical slice
|
||||
|
||||
**목표:** 별도 상세 route/GET 없이 active Community 목록과 Sheet만으로 게시글 등록·조회·수정·고정·비활성화·첨부 재생을 완료한다.
|
||||
**목표:** 별도 상세 route/GET 없이 Community 목록과 Sheet만으로 게시글 등록·조회·수정·고정·비활성화·첨부 재생을 완료한다.
|
||||
|
||||
**Phase Goal `P6`:** Task 6.1 → 6.4와 Phase 6 Gate로 목록 기반 Community Sheet·media slice를 완성한다.
|
||||
|
||||
@@ -1686,9 +1794,9 @@ npm run build
|
||||
- **완료 조건:** `P6-T1`~`P6-T4`, `P6-GATE` 완료. 오류·price는 제공 계약 또는 최소 규칙으로 종결.
|
||||
- **실행 순서:** 계약 확인 → 목록/Sheet → form/media → 반응형·접근성.
|
||||
|
||||
**요구사항:** `COMMUNITY-001~011`, `FILE-001~004`, `FILE-007~009`, `FILE-011~014`, PRD `9`의 Community 범위.
|
||||
**요구사항:** `COMMUNITY-001~015`, `FILE-001~004`, `FILE-007~009`, `FILE-011~014`, PRD `9`의 Community 범위.
|
||||
|
||||
**외부 의존:** Community 신규 오류 계약, optional P1 price 상한. Comments는 Phase 8에서 연결한다.
|
||||
**외부 의존:** `EXT-007` active-only 반환 보장, `EXT-008` pagination의 total/hasNext 또는 종료 규칙, `EXT-011` Community 오류 message key, `EXT-010` backend file 검증 계약, `EXT-009` optional P1 price 상한. Comments는 Phase 8에서 연결한다.
|
||||
|
||||
**주요 Files:**
|
||||
|
||||
@@ -1702,34 +1810,49 @@ npm run build
|
||||
- Create: `src/features/community-posts/tests/{community-list,community-sheet}.test.tsx`
|
||||
- Create: `tests/e2e/community-post.spec.ts`
|
||||
|
||||
#### Phase 6 Task 실행 계약
|
||||
|
||||
| Goal | Files | Interfaces | TDD·검증 기준 |
|
||||
|---|---|---|---|
|
||||
| `P6-T1` | Modify: `docs/20260725_AI캐릭터관리자웹/prd.md`, `docs/20260725_AI캐릭터관리자웹/plan-task.md`; Read: `docs/20260725_AI캐릭터관리자웹/api-contract.openapi.json`; Test: 없음 | Consumes: `CommunityPost*` schema·3 operation. Produces: list/create/update contract map, 외부 의존과 Sheet inventory | **TDD 예외:** 외부 계약 조사 Task다. OpenAPI JSON parse와 `rg -n 'COMMUNITY-01[2-5]'` 문서 추적 검사를 실행한다. 기대 `exit 0`. 수동 확인: timezone·배열 data·null mutation·미제공 pagination이 분리된다. |
|
||||
| `P6-T2` | Create: `src/features/community-posts/api/community-post-api.ts`, `src/features/community-posts/model/types.ts`, `src/features/community-posts/pages/CommunityPostListPage.tsx`, `src/features/community-posts/components/{CommunityPostList,CommunityPostListItem,CommunityPostSheet}.tsx`, `src/features/community-posts/tests/{community-contract.test.ts,community-list.test.tsx,community-sheet.test.tsx}` | Consumes: `CommunityPostListApiResponse`, update multipart. Produces: `getCommunityPosts({characterId,timezone,page,size})`, `updateCommunityPost`, collection Sheet cache policy | **TDD 적용:** `npm run test:run -- src/features/community-posts/tests/community-contract.test.ts src/features/community-posts/tests/community-list.test.tsx src/features/community-posts/tests/community-sheet.test.tsx`; 기대 `exit 0`. 수동 확인: detail GET 0회, pin/deactivate 후 refetch. |
|
||||
| `P6-T3` | Create: `src/features/community-posts/schemas/community-post-schema.ts`, `src/features/community-posts/validation/community-media-policy.ts`, `src/features/community-posts/components/CommunityPostForm.tsx`; Modify: `src/features/community-posts/api/community-post-api.ts`, `src/features/community-posts/components/CommunityPostSheet.tsx`, `src/features/community-posts/tests/{community-contract.test.ts,community-sheet.test.tsx}` | Consumes: `CommunityPostCreateRequest`, `CommunityPostUpdateRequest`, multipart part names. Produces: `createCommunityPost`, form serializer와 media policy | **TDD 적용:** `npm run test:run -- src/features/community-posts/tests/community-contract.test.ts src/features/community-posts/tests/community-sheet.test.tsx src/shared/validation`; 기대 `exit 0`. 수동 확인: postImage/audioFile, update audio/price 없음, audioUrl 재생. |
|
||||
| `P6-T4` | Modify: `src/features/community-posts/pages/CommunityPostListPage.tsx`, `src/features/community-posts/components/{CommunityPostList,CommunityPostListItem,CommunityPostForm,CommunityPostSheet}.tsx`; Test: `tests/e2e/community-post.spec.ts` | Consumes: P6-T2/T3 UI. Produces: viewport·Sheet focus capability evidence | **TDD 적용:** `npm run e2e:mock -- tests/e2e/community-post.spec.ts`; 기대 지원 project 전부 통과. 수동 확인: 320px, focus trap/복귀, 200% zoom, axe. |
|
||||
|
||||
`P6-T2`~`P6-T4`는 각 row의 focused test로 RED → GREEN →
|
||||
REFACTOR를 실행하고 관련 feature test·typecheck·lint 결과와 수동 확인을
|
||||
`§7 검증 기록`에 누적한다.
|
||||
|
||||
### Task 6.1 Phase 계약 확인
|
||||
|
||||
**Goal 실행 `P6-T1`:** Community 오류·media·price 계약, mock scenario와 목록/Sheet component map을 확정한다.
|
||||
|
||||
- **시작 조건:** `P2-GATE`, `P4-T2` 완료, PRD `COMMUNITY-001~011`, `MOCK-001~009`와 API Contract §7 확인.
|
||||
- **시작 조건:** `P2-GATE`, `P4-T2` 완료, PRD `COMMUNITY-001~015`, `MOCK-001~009`와 OpenAPI Community 3개 operation/schema 확인.
|
||||
- **완료 증거:** 체크박스 전체, contract fixture와 상태/action inventory의 세 문서 일치.
|
||||
- **범위 밖:** price 상한·오류 key 추정과 Comments 구현.
|
||||
|
||||
- [ ] Community 오류 status/message key와 media upload 오류 fixture를 기록한다.
|
||||
- [ ] OpenAPI 공통 오류 status·shape를 fixture에 기록하고 Community 전용 message key와 media upload 오류 계약은 미제공으로 표시한다. 정확한 fixture를 추정하지 않는다.
|
||||
- [ ] price 최대값이 제공되면 Audio와 같은 정책으로 갱신하고, 없으면 0 이상 정수만 유지한다.
|
||||
- [ ] 목록 필수 `timezone`, `page/size`, 배열 `data`, 생성 `audioFile/postImage/request`, 수정 `postImage/request`, mutation `data=null`을 contract fixture로 고정한다.
|
||||
- [ ] 목록에 total/hasNext와 `isActive/fixedAtUtc`가 없음을 기록하고 해당 값을 fixture에서 추가하지 않는다.
|
||||
- [ ] 목록·Sheet·form/media의 상태/action inventory를 작성하고 Page는 collection query/policy 조합, feature component는 Community 규칙, shared component는 Phase 1 contract를 사용하도록 component map을 확정한다.
|
||||
- [ ] Community active list, Sheet, pin, media와 CRUD의 deterministic browser fixture 시나리오를 확정한다.
|
||||
|
||||
### Task 6.2 목록·collection Sheet
|
||||
|
||||
**Goal 실행 `P6-T2`:** 전용 detail route/GET 없는 active 목록과 collection Sheet mutation 흐름을 완성한다.
|
||||
**Goal 실행 `P6-T2`:** 전용 detail route/GET 없는 목록과 collection Sheet mutation 흐름을 완성한다.
|
||||
|
||||
- **시작 조건:** `P6-T1` 완료.
|
||||
- **완료 증거:** 체크박스 전체, list/Sheet/router/cache/pin/deactivate test와 detail GET 0회 기록.
|
||||
- **범위 밖:** 댓글과 제공 계약에 없는 Community 검색.
|
||||
|
||||
- [ ] active-only 목록의 `page/size`, loading·empty·error·retry와 URL query 보존을 test한다. 제공 계약에 없는 Community `search` query나 현재 page 한정 client 검색은 만들지 않는다.
|
||||
- [ ] 목록이 필수 `timezone=Asia/Seoul`과 `page/size`를 보내고 배열 `data`를 소비하며 loading·empty·error·retry와 URL query 보존을 제공하는지 test한다. 제공 계약에 없는 Community `search`, total, hasNext를 만들지 않는다.
|
||||
- [ ] 목록 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를 확인한다.
|
||||
- [ ] 고정/해제 후 mutation `data=null`을 처리하고 목록을 재조회해 `isFixed`를 갱신한다. 계약에 없는 `fixedAtUtc`는 표시하지 않는다.
|
||||
- [ ] soft delete request가 `isActive=false`, `isFixed=false`를 보내고 성공 `data=null`을 처리하는 contract test를 작성한다.
|
||||
- [ ] soft delete 성공 시 Sheet 종료, 목록 재조회와 성공 toast를 확인한다. 비활성 항목 제거는 active-only 계약 제공 후 server mode에서 검증한다.
|
||||
- [ ] mock mode도 전용 detail GET 없이 list store만으로 Sheet와 pin/deactivate 최종 UI를 갱신한다.
|
||||
|
||||
### Task 6.3 게시글 form·첨부 media
|
||||
@@ -1741,15 +1864,16 @@ npm run build
|
||||
- **범위 밖:** GIF 재인코딩, URL 갱신 전용 요청, Comments.
|
||||
|
||||
- [ ] 생성 payload에 `isActive`가 없고 일반 update/soft delete가 공통 `isActive` 규칙을 지키는 test를 작성한다.
|
||||
- [ ] content, price 0 이상 정수, isAdult, isFixed와 optional image/audio를 test한다.
|
||||
- [ ] create의 필수 content·isCommentAvailable·isAdult, optional price와 multipart `postImage/audioFile`을 test하고 생성 request에 `isFixed`가 없음을 고정한다.
|
||||
- [ ] update는 optional `postImage`와 content·isCommentAvailable·isAdult·isActive·isFixed만 보내며 price·audioFile 교체 UI/request가 없음을 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 4와 동일하게 조합해 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` 상한 필요성을 판단하고, 구현 전 결정 문서를 갱신하거나 “상한 추가 없음”으로 종결한다.
|
||||
- [ ] 사용자 새로고침이나 mutation cache invalidation으로 목록이 정상 재조회된 때만 새 `audioUrl`을 사용한다.
|
||||
- [ ] 초기 Community Sheet/form을 실제 페이지에서 확인한 뒤 `content`의 최대 길이 권고안을 작성한다. backend 호환 확인 전에는 상한을 구현하지 않는다.
|
||||
- [ ] mock handler가 multipart contract를 검증하고 local preview media asset으로 create/update/play 최종 UI를 재현한다.
|
||||
|
||||
### Task 6.4 Community 반응형·접근성
|
||||
@@ -1781,67 +1905,84 @@ npm run lint
|
||||
npm run build
|
||||
```
|
||||
|
||||
**Expected:** mock mode에서 create → 목록 item Sheet 조회/수정 → pin/unpin → 첨부 재생 → soft delete 최종 UI가 통과하고 Community detail GET·detail route 호출은 0건이다. server mode는 별도 결과를 기록한다.
|
||||
**Expected:** mock mode에서 `timezone` 목록 → `postImage/audioFile/request` create → 목록 item Sheet 조회/허용 field 수정 → pin/unpin → `audioUrl` 재생 → soft delete 요청 UI가 통과하고 Community detail GET·detail route·수정 audio/price request는 0건이다. pagination 종료와 active-only 결과는 계약 제공 전 완료로 주장하지 않는다.
|
||||
|
||||
**수동 확인:** 320px와 desktop/tablet에서 목록·Sheet·첨부 재생·form을
|
||||
keyboard-only로 확인하고, detail GET·수정 audio/price 요청이 없으며
|
||||
focus가 Sheet trigger로 복귀하는지 본다.
|
||||
|
||||
---
|
||||
|
||||
## Phase 7. FanTalk vertical slice
|
||||
|
||||
**목표:** 모든 viewport에서 FanTalk를 최신순·답변 상태로 조회하고 답변을 한 번 작성한 뒤 기존 답변만 수정한다.
|
||||
**목표:** 모든 viewport에서 FanTalk 목록을 조회하고 답변이 없는 item에 한 번 답변한다. 상세·답변 수정·전체 결과 filter/sort는 계약 제공 후 후속 slice로 추가한다.
|
||||
|
||||
**Phase Goal `P7`:** Task 7.1 → 7.3과 Phase 7 Gate로 FanTalk 조회·단일 답변·수정 slice를 완성한다.
|
||||
**Phase Goal `P7`:** Task 7.1 → 7.3과 Phase 7 Gate로 OpenAPI가 제공한 FanTalk 목록·단일 답변 생성 slice를 완성한다.
|
||||
|
||||
- **시작 조건:** `P3-T2` workspace core와 `P2-GATE` 완료.
|
||||
- **완료 조건:** 핵심 계약이 제공되면 `P7-T1`~`P7-T3`, `P7-GATE` 완료. 미제공이면 Phase 제외/후속 결정 문서화로 종결.
|
||||
- **완료 조건:** 제공된 목록 GET·답변 POST 범위의 `P7-T1`~`P7-T3`, `P7-GATE` 완료. 미제공 상세·수정·filter/sort·유일성 오류는 외부 의존 상태와 후속 재개 조건 기록.
|
||||
- **실행 순서:** 계약 확인 → 목록/답변 → 반응형·접근성.
|
||||
|
||||
**요구사항:** `FANTALK-001~008`, PRD `9`의 FanTalk 범위.
|
||||
**요구사항:** `FANTALK-001~011`, PRD `9`의 FanTalk 범위.
|
||||
|
||||
**외부 의존:** 목록·상세·답변 수정 endpoint/DTO, filter/sort, reply uniqueness의 원자적 강제와 중복 오류 계약. 핵심 계약이 없으면 이 Phase 전체를 추측 구현하지 않는다.
|
||||
**외부 의존:** `EXT-004` 별도 상세·답변 수정 endpoint/DTO, 전체 결과 답변 상태 filter, sort, reply uniqueness의 원자적 강제와 중복 오류 계약. 목록 GET과 답변 POST는 제공됐다.
|
||||
|
||||
**주요 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/pages/FanTalkListPage.tsx`
|
||||
- Create: `src/features/fan-talks/components/{FanTalkList,FanTalkListItem,FanTalkReplySheet,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`
|
||||
|
||||
#### Phase 7 Task 실행 계약
|
||||
|
||||
| Goal | Files | Interfaces | TDD·검증 기준 |
|
||||
|---|---|---|---|
|
||||
| `P7-T1` | Modify: `docs/20260725_AI캐릭터관리자웹/prd.md`, `docs/20260725_AI캐릭터관리자웹/plan-task.md`; Read: `docs/20260725_AI캐릭터관리자웹/api-contract.openapi.json`; Test: 없음 | Consumes: `FanTalkList*`, `FanTalkReply*` schema·2 operation. Produces: 제공 목록/reply map, 미제공 범위와 Sheet inventory | **TDD 예외:** 외부 계약 조사 Task다. OpenAPI JSON parse와 `rg -n 'FANTALK-0(0[7-9]|1[0-1])'` 문서 추적 검사를 실행한다. 기대 `exit 0`. 수동 확인: 제공 list/reply와 상세·edit·filter/sort 의존성이 분리된다. |
|
||||
| `P7-T2` | Create: `src/features/fan-talks/api/fan-talk-api.ts`, `src/features/fan-talks/model/types.ts`, `src/features/fan-talks/schemas/fan-talk-reply-schema.ts`, `src/features/fan-talks/pages/FanTalkListPage.tsx`, `src/features/fan-talks/components/{FanTalkList,FanTalkListItem,FanTalkReplySheet,FanTalkReplyForm}.tsx`, `src/features/fan-talks/tests/{fan-talk-contract.test.ts,fan-talk-list.test.tsx,fan-talk-reply.test.tsx}`; Modify: `src/app/router.tsx` | Consumes: `FanTalkListResponse`, `FanTalkReplyCreateRequest`, `FanTalkReplyResponse`. Produces: `getFanTalks({characterId,page,size})`, `createFanTalkReply`, list-item Sheet flow | **TDD 적용:** `npm run test:run -- src/features/fan-talks/tests/fan-talk-contract.test.ts src/features/fan-talks/tests/fan-talk-list.test.tsx src/features/fan-talks/tests/fan-talk-reply.test.tsx`; 기대 `exit 0`. 수동 확인: detail/filter/edit request 0회와 한 번 reply. |
|
||||
| `P7-T3` | Modify: `src/features/fan-talks/pages/FanTalkListPage.tsx`, `src/features/fan-talks/components/{FanTalkList,FanTalkListItem,FanTalkReplySheet,FanTalkReplyForm}.tsx`; Test: `tests/e2e/fan-talk.spec.ts` | Consumes: P7-T2 UI. Produces: 전 viewport reply capability evidence | **TDD 적용:** `npm run e2e:mock -- tests/e2e/fan-talk.spec.ts`; 기대 지원 project 전부 통과. 수동 확인: 320px keyboard, 200% zoom, keyboard-only, axe. |
|
||||
|
||||
`P7-T2`~`P7-T3`는 각 row의 focused test로 RED → GREEN →
|
||||
REFACTOR를 실행하고 관련 feature test·typecheck·lint 결과와 수동 확인을
|
||||
`§7 검증 기록`에 누적한다.
|
||||
|
||||
### Task 7.1 Phase 계약 확인
|
||||
|
||||
**Goal 실행 `P7-T1`:** FanTalk 목록·상세·수정·유일성·오류 계약, mock 가능 범위와 component map을 확정한다.
|
||||
**Goal 실행 `P7-T1`:** FanTalk 목록·답변 생성 계약과 미제공 상세·수정·filter/sort·유일성 범위, mock 가능 범위와 component map을 확정한다.
|
||||
|
||||
- **시작 조건:** `P3-T2`, `P2-GATE` 완료, PRD `FANTALK-001~008`, `MOCK-001~009`와 API Contract §8~9 확인.
|
||||
- **완료 증거:** 체크박스 전체, 제공 계약 또는 Phase 제외/후속 결정의 세 문서 일치.
|
||||
- **시작 조건:** `P3-T2`, `P2-GATE` 완료, PRD `FANTALK-001~011`, `MOCK-001~009`와 OpenAPI FanTalk 2개 operation/schema 확인.
|
||||
- **완료 증거:** 체크박스 전체, 제공 범위와 외부 의존의 PRD·OpenAPI·plan 일치.
|
||||
- **범위 밖:** 임시 endpoint·placeholder DTO·production mock adapter.
|
||||
|
||||
- [ ] 목록·상세·답변 수정 endpoint, request/response DTO, page/filter/latest sort, ownership error를 기록한다.
|
||||
- [ ] 목록 `page/size`, `data.fanTalkCount/fanTalks/page/size/hasNext`, item `creatorReplies`와 답변 POST `{content}`·성공 DTO를 contract fixture로 고정한다.
|
||||
- [ ] 별도 상세·답변 수정 endpoint, answer filter와 sort query가 없음을 기록하고 임시 route·query를 만들지 않는다.
|
||||
- [ ] 답변 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을 확정한다.
|
||||
- [ ] 목록·상세·수정·유일성 계약이 모두 제공된 뒤에만 browser fixture를 만들고, 현재 제공된 POST만으로 최종 FanTalk mock UI를 추정하지 않는다.
|
||||
- [ ] 미제공 범위에는 임시 endpoint·DTO·production adapter를 만들지 않고 후속 재개 조건을 문서화한다.
|
||||
- [ ] 목록·reply Sheet/form의 상태/action inventory를 작성하고 Page는 route/query/policy 조합, feature component는 FanTalk 규칙, shared component는 Phase 1 contract를 사용하도록 component map을 확정한다.
|
||||
- [ ] 제공된 목록·답변 생성 범위만 browser fixture로 만들고 상세·수정·filter/sort·유일성 오류는 mock에서도 추정하지 않는다.
|
||||
|
||||
### Task 7.2 목록·답변 생성·수정
|
||||
### Task 7.2 목록·답변 생성
|
||||
|
||||
**Goal 실행 `P7-T2`:** 최신순/filter 목록과 답변 1회 생성·기존 답변 수정 흐름을 완성한다.
|
||||
**Goal 실행 `P7-T2`:** backend 순서를 유지하는 목록과 답변 1회 생성 흐름을 완성한다.
|
||||
|
||||
- **시작 조건:** `P7-T1`에서 핵심 계약 제공 확인.
|
||||
- **완료 증거:** 체크박스 전체, list/detail/reply contract·UI test, 중복 제출/오류 복구, `OQ-009` 결정 기록.
|
||||
- **범위 밖:** 답변 삭제·두 번째 답변과 계약 없는 network 동작.
|
||||
- **시작 조건:** `P7-T1` 완료.
|
||||
- **완료 증거:** 체크박스 전체, list/reply contract·UI test, 중복 제출 차단·오류 복구, OQ-009 후속 검토 기록.
|
||||
- **범위 밖:** 별도 상세, 답변 수정·삭제·두 번째 답변, 전체 결과 filter/sort와 계약 없는 network 동작.
|
||||
|
||||
- [ ] 기본 목록은 최신순 전체이며 전체/미답변/답변 완료 filter와 page를 URL에 보존한다.
|
||||
- [ ] loading·empty·error·retry와 direct detail/refresh를 test한다.
|
||||
- [ ] 답변이 없을 때만 POST form을, 있으면 edit form만 표시하고 delete UI는 만들지 않는다.
|
||||
- [ ] 목록은 `page`, `size`를 URL에 보존하고 backend 반환 순서와 `hasNext`를 사용한다. 전체/미답변/답변 완료 filter와 client 재정렬은 만들지 않는다.
|
||||
- [ ] loading·empty·error·retry와 목록 새로고침을 test하고 `/fan-talks/:fanTalkId` route·상세 GET이 0건임을 검증한다.
|
||||
- [ ] 목록 item의 `creatorReplies`가 비어 있을 때만 POST form을 표시하고 답변이 있으면 읽기 전용으로 표시하며 edit/delete UI는 만들지 않는다.
|
||||
- [ ] 빠른 두 번 제출에도 POST가 한 번만 호출되는 test를 작성한다.
|
||||
- [ ] 답변 수정은 제공된 endpoint/reply identity만 사용한다.
|
||||
- [ ] server 중복 오류를 받으면 최신 detail을 재조회해 edit 상태로 전환하고 status/key를 추정 분기하지 않는다.
|
||||
- [ ] 답변 POST 성공 DTO의 `fanTalkId`, `replyId`, `creatorMemberId`, `content`, `createdAtUtc`를 반영하고 해당 목록 page를 재조회한다.
|
||||
- [ ] server 중복 오류를 받으면 현재 목록 page를 재조회해 `creatorReplies`를 갱신하고 status/key를 추정 분기하지 않는다.
|
||||
- [ ] 저장 중 중복 제출 차단, visible label, 오류 연결, 성공 live feedback을 test한다.
|
||||
- [ ] FanTalk reply form을 실제로 작성한 뒤 `OQ-009`의 `content` 상한 필요성을 판단하고, 구현 전 결정 문서를 갱신하거나 “상한 추가 없음”으로 종결한다.
|
||||
- [ ] 계약 제공 후 mock store로 미답변 → 답변 생성 → 수정과 중복 오류의 최종 UI E2E를 작성한다.
|
||||
- [ ] 초기 FanTalk reply form을 실제 페이지에서 확인한 뒤 `content` 최대 길이 권고안을 작성한다. backend 호환 확인 전에는 상한을 구현하지 않는다.
|
||||
- [ ] 제공 계약 범위의 mock store로 미답변 → 답변 생성 → 읽기 전용 답변 표시의 최종 UI E2E를 작성한다.
|
||||
|
||||
### Task 7.3 FanTalk 반응형·접근성
|
||||
|
||||
@@ -1851,9 +1992,9 @@ npm run build
|
||||
- **완료 증거:** 체크박스 전체, 320px keyboard viewport·keyboard-only·200% zoom·axe E2E 기록.
|
||||
- **범위 밖:** viewport별 기능 축소와 답변 삭제.
|
||||
|
||||
- [ ] desktop/tablet/mobile 모두 조회·답변 작성·수정을 제공한다.
|
||||
- [ ] desktop/tablet/mobile 모두 목록 조회·답변 작성을 제공하고 수정 action은 제공하지 않는다.
|
||||
- [ ] 320px에서 keyboard가 reply input/submit을 가리지 않는 E2E를 작성한다.
|
||||
- [ ] keyboard-only filter/detail/create/edit, 200% zoom, axe critical·serious 0건을 확인한다.
|
||||
- [ ] keyboard-only 목록 탐색·reply Sheet·create, 200% zoom, axe critical·serious 0건을 확인한다.
|
||||
|
||||
### Phase 7 Gate
|
||||
|
||||
@@ -1872,7 +2013,11 @@ npm run lint
|
||||
npm run build
|
||||
```
|
||||
|
||||
**Expected:** 핵심 계약 제공 후 mock mode에서 미답변 조회 → 답변 1회 생성 → 기존 답변 수정 최종 UI가 모든 viewport에서 통과하며 두 번째 reply 생성과 delete UI가 없다. 계약 미제공이면 mock fixture도 만들지 않고 연동 대기로 기록한다.
|
||||
**Expected:** mock mode에서 FanTalk page 조회 → 목록 item Sheet → 답변 1회 생성 → 읽기 전용 답변 표시 UI가 모든 viewport에서 통과하며 상세 GET, filter/sort query, 두 번째 reply, edit/delete UI가 없다. 미제공 기능은 외부 의존으로 남고 목록·답변 생성 범위와 섞여 완료 표시되지 않는다.
|
||||
|
||||
**수동 확인:** desktop/tablet/mobile에서 목록·reply Sheet를 keyboard-only로
|
||||
확인하고, `creatorReplies`가 있는 item의 POST가 차단되며 상세·수정·filter
|
||||
request가 발생하지 않는지 본다.
|
||||
|
||||
---
|
||||
|
||||
@@ -1888,7 +2033,7 @@ npm run build
|
||||
|
||||
**요구사항:** `COMMENT-001~006`, PRD `9`의 Comments 범위.
|
||||
|
||||
**외부 의존:** Audio·Community 댓글 목록/작성/수정/soft delete endpoint·DTO, 2단계 강제, fan 댓글 삭제 권한 오류. 핵심 계약이 없으면 이 Phase 전체를 추측 구현하지 않는다.
|
||||
**외부 의존:** `EXT-005`. OpenAPI의 Audio 상세 `commentList`와 Community 목록 `firstComment`는 읽기용 요약일 뿐 CRUD 계약이 아니다. Audio·Community 댓글 목록/작성/수정/soft delete endpoint·DTO, 2단계 강제와 fan 댓글 삭제 권한 오류가 제공되기 전에는 이 Phase 전체를 추측 구현하지 않는다.
|
||||
|
||||
**주요 Files:**
|
||||
|
||||
@@ -1901,11 +2046,24 @@ npm run build
|
||||
- Create: `tests/e2e/comments.spec.ts`
|
||||
- Modify: `AudioContentDetailPage.tsx`, `CommunityPostSheet.tsx`
|
||||
|
||||
#### Phase 8 Task 실행 계약
|
||||
|
||||
| Goal | Files | Interfaces | TDD·검증 기준 |
|
||||
|---|---|---|---|
|
||||
| `P8-T1` | Modify: `docs/20260725_AI캐릭터관리자웹/prd.md`, `docs/20260725_AI캐릭터관리자웹/plan-task.md`; Read: `docs/20260725_AI캐릭터관리자웹/api-contract.openapi.json`; Test: 없음 | Consumes: Audio `commentList`, Community `firstComment` summary와 CRUD operation 부재. Produces: target별 외부 의존·재개 조건과 screen inventory | **TDD 예외:** 계약 부재 조사 Task다. OpenAPI JSON parse와 `rg -n 'COMMENT-00[1-6]|댓글 CRUD'` 문서 추적 검사를 실행한다. 기대 `exit 0`. 수동 확인: summary DTO를 CRUD 계약으로 사용하지 않는다. |
|
||||
| `P8-T2` | 계약 제공 후 Create: `src/features/comments/api/comment-api.ts`, `src/features/comments/model/{types,comment-target}.ts`, `src/features/comments/schemas/comment-schema.ts`, `src/features/comments/components/{CommentThread,CommentForm,CommunityPostCommentsSheet}.tsx`, `src/features/comments/tests/{comment-contract.test.ts,comment-thread.test.tsx}`; Modify: `src/features/audio-contents/pages/AudioContentDetailPage.tsx`, `src/features/community-posts/components/CommunityPostSheet.tsx` | Consumes: backend가 제공할 target별 list/create/update/delete DTO. Produces: `CommentTarget`, target adapter와 2단계 thread | **TDD 적용:** 계약 제공 후 `npm run test:run -- src/features/comments/tests/comment-contract.test.ts src/features/comments/tests/comment-thread.test.tsx`; 기대 `exit 0`. 수동 확인: 두 target과 root/direct reply만 표시. |
|
||||
| `P8-T3` | 계약 제공 후 Create: `src/features/comments/components/CommentActions.tsx`, `src/features/comments/tests/comment-permissions.test.tsx`; Modify: `src/features/comments/api/comment-api.ts`, `src/features/comments/components/CommentThread.tsx` | Consumes: P8-T2 adapter와 ownership/error contract. Produces: create/update/soft-delete action policy | **TDD 적용:** 계약 제공 후 `npm run test:run -- src/features/comments/tests/comment-contract.test.ts src/features/comments/tests/comment-permissions.test.tsx`; 기대 `exit 0`. 수동 확인: author별 action과 fan edit 0회. |
|
||||
| `P8-T4` | 계약 제공 후 Modify: `src/features/comments/components/{CommentThread,CommentForm,CommentActions,CommunityPostCommentsSheet}.tsx`, `src/features/audio-contents/pages/AudioContentDetailPage.tsx`, `src/features/community-posts/components/CommunityPostSheet.tsx`; Test: `tests/e2e/comments.spec.ts` | Consumes: P8-T2/T3 UI. Produces: 전 viewport Comments capability evidence | **TDD 적용:** 계약 제공 후 `npm run e2e:mock -- tests/e2e/comments.spec.ts`; 기대 지원 project 전부 통과. 수동 확인: 320px, keyboard, focus 복귀, 200% zoom, axe. |
|
||||
|
||||
`P8-T2`~`P8-T4`는 CRUD 계약이 제공된 뒤 각 row의 focused test로 RED →
|
||||
GREEN → REFACTOR를 실행한다. 계약 전에는 test용 endpoint·DTO를 만들지
|
||||
않고 `P8-T1`의 대체 검증 결과만 `§7 검증 기록`에 누적한다.
|
||||
|
||||
### Task 8.1 Phase 계약 확인
|
||||
|
||||
**Goal 실행 `P8-T1`:** 두 댓글 target의 CRUD·2단계·권한 오류 계약, mock 가능 범위와 component map을 확정한다.
|
||||
|
||||
- **시작 조건:** `P4-T2`, `P6-T2`, `P2-GATE` 완료, PRD `COMMENT-001~006`, `MOCK-001~009`와 API Contract §9 확인.
|
||||
- **시작 조건:** `P4-T2`, `P6-T2`, `P2-GATE` 완료, PRD `COMMENT-001~006`, `MOCK-001~009`와 OpenAPI의 comment summary schema 및 CRUD operation 부재 확인.
|
||||
- **완료 증거:** 체크박스 전체, 제공 계약 또는 Phase 제외/후속 결정의 세 문서 일치.
|
||||
- **범위 밖:** endpoint 이름 추정과 client-only permission 완료 주장.
|
||||
|
||||
@@ -1943,7 +2101,7 @@ npm run build
|
||||
- [ ] delete 전 대상과 영향을 확인하고 server 계약에 따라 tombstone 또는 목록 갱신을 적용한다.
|
||||
- [ ] Character workspace read-only 정책이 모든 comment mutation도 차단하는 test를 작성한다.
|
||||
- [ ] 중복 제출, server permission 오류, session 401/403이 공통 정책을 따르는지 test한다.
|
||||
- [ ] Comment thread/form을 실제로 작성한 뒤 `OQ-009`의 `content` 상한 필요성을 판단하고, 구현 전 결정 문서를 갱신하거나 “상한 추가 없음”으로 종결한다.
|
||||
- [ ] 댓글 CRUD 계약 제공 후 초기 Comment thread/form을 실제 페이지에서 확인하고 `content` 최대 길이 권고안을 작성한다. backend 호환 확인 전에는 상한을 구현하지 않는다.
|
||||
- [ ] 계약 제공 후 mock store로 양 target의 root/reply CRUD와 작성자별 권한 오류 최종 UI E2E를 작성한다.
|
||||
|
||||
### Task 8.4 Comments 반응형·접근성
|
||||
@@ -1977,6 +2135,10 @@ npm run build
|
||||
|
||||
**Expected:** 핵심 계약 제공 후 mock mode에서 Audio와 Community 두 진입점의 2단계 댓글 CRUD·권한·모바일 최종 UI가 통과하고 reply의 reply 및 fan edit request는 생성되지 않는다. 계약 미제공이면 mock fixture도 만들지 않고 연동 대기로 기록한다.
|
||||
|
||||
**수동 확인:** 댓글 CRUD 계약 제공 후 두 target에서 root/direct reply,
|
||||
작성자별 action, 320px keyboard와 focus 복귀를 확인한다. 계약 미제공이면
|
||||
Comments network request와 browser fixture가 0건인지 확인한다.
|
||||
|
||||
---
|
||||
|
||||
## Phase 9. 교차 회귀·인수인계
|
||||
@@ -1993,7 +2155,20 @@ npm run build
|
||||
|
||||
- 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.
|
||||
- Modify: `docs/20260725_AI캐릭터관리자웹/{prd.md,plan-task.md}` when an actual product or implementation decision differs.
|
||||
- Replace from backend: `docs/20260725_AI캐릭터관리자웹/api-contract.openapi.json` only when a new formal contract version is provided.
|
||||
|
||||
#### Phase 9 Task 실행 계약
|
||||
|
||||
| Goal | Files | Interfaces | TDD·검증 기준 |
|
||||
|---|---|---|---|
|
||||
| `P9-T1` | Create: `tests/e2e/resource-workflows.spec.ts`, `tests/e2e/error-mapping.spec.ts` | Consumes: 활성 Phase API/UI와 OpenAPI common responses. Produces: 교차 journey·serializer·error regression evidence | **TDD 적용:** 기존 회귀가 놓치는 불변식의 실패 E2E를 먼저 추가하고 `npm run e2e:mock -- tests/e2e/resource-workflows.spec.ts tests/e2e/error-mapping.spec.ts`; 기대 `exit 0`. 수동 확인: 406 포함 오류, 금지 request 0회. |
|
||||
| `P9-T2` | Create: `tests/e2e/responsive-capabilities.spec.ts`, `tests/e2e/accessibility.spec.ts` | Consumes: PRD viewport matrix와 P9-T1 journey. Produces: responsive·a11y·security evidence와 별도 회귀 수정 Task 입력 | **TDD 적용:** 실패 viewport/a11y 회귀를 먼저 재현하고 `npm run e2e:mock -- tests/e2e/responsive-capabilities.spec.ts tests/e2e/accessibility.spec.ts`; 기대 `exit 0`. 수동 확인: browser matrix, keyboard, zoom, 민감정보 비기록. |
|
||||
| `P9-T3` | Modify: `README.md`, `docs/20260725_AI캐릭터관리자웹/prd.md`, `docs/20260725_AI캐릭터관리자웹/plan-task.md`; Test: 없음 | Consumes: P9-T1/T2 실제 결과와 외부 의존 상태. Produces: 요구사항 추적·Progress·인수 문서 | **TDD 예외:** 문서 정합성 Task다. `rg` 추적 검사, Markdown link 확인과 `git diff --check`를 실행한다. 기대 `exit 0`. 수동 확인: 모든 확정·외부 의존·OQ-009 후속 값이 증거와 연결된다. |
|
||||
|
||||
`P9-T1`~`P9-T2`는 row의 E2E로 RED → GREEN → REFACTOR를 실행하고
|
||||
server mode 회귀·typecheck·lint·build를 이어서 수행한다. `P9-T3`는 실행한
|
||||
대체 검증과 수동 대조 결과를 `§7 검증 기록`에 누적한다.
|
||||
|
||||
### Task 9.1 교차 journey·오류 회귀
|
||||
|
||||
@@ -2003,8 +2178,8 @@ npm run build
|
||||
- **완료 증거:** 체크박스 전체, resource-workflows/error-mapping E2E와 request 0회·serializer fixture 기록.
|
||||
- **범위 밖:** 새 기능과 계약 미제공 제외 범위의 가짜 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 처리를 하는지 검증한다.
|
||||
- [ ] login → Character select → Audio immediate/scheduled create/play → Series link/order → Community Sheet → FanTalk list/reply → Comments 중 계약이 제공된 활성 범위 journey를 검증한다.
|
||||
- [ ] 400/401/403/404/405/406/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()`가 발생하지 않는지 검증한다.
|
||||
@@ -2040,9 +2215,9 @@ npm run build
|
||||
- **범위 밖:** 결정되지 않은 계약을 문서상 확정하는 행위.
|
||||
|
||||
- [ ] 활성 범위의 P0 외부 의존이 0건인지, 아니면 구현 전에 명시적으로 후속/제외 결정됐는지 확인한다.
|
||||
- [ ] `OQ-009`를 각 도메인별 확정 또는 “상한 추가 없음”으로 종결하고 중복 checklist를 남기지 않는다.
|
||||
- [ ] OQ-009의 확정 절차에 따라 각 초기 UI의 최대 길이·배열 개수 권고안, backend 호환 결과와 실제 값 또는 “상한 추가 없음”을 기록하고 중복 checklist를 남기지 않는다.
|
||||
- [ ] `OQ-010` 감사 로그 UI가 현재 릴리스 non-goal임을 결정 기록과 맞춘다.
|
||||
- [ ] 실제 구현과 다른 결정은 PRD 결정 기록 → API Contract → plan 순으로 갱신한다.
|
||||
- [ ] 실제 구현과 다른 제품 결정은 PRD 결정 기록 → plan 순으로 갱신한다. API 사실이 달라졌다면 backend가 제공한 새 OpenAPI 계약을 먼저 반영한 뒤 두 문서를 맞춘다.
|
||||
- [ ] PRD 수용 기준마다 자동 test 또는 수동 검증 증거를 연결한다.
|
||||
- [ ] README에 install, env, run, test, build, 지원 브라우저, 알려진 backend 제약을 기록한다.
|
||||
- [ ] plan 하단 검증 기록에 무엇을/왜/어떻게와 실제 명령·성공/실패/불가 사유를 누적한다.
|
||||
@@ -2083,6 +2258,10 @@ assert_no_match "externalCharacterId|SUNDAY|MONDAY|TUESDAY|WEDNESDAY|THURSDAY|FR
|
||||
|
||||
**Expected:** mock UI 전체 journey와 실제 server integration 결과가 분리 기록되고 production mock 활성화·404 자동 fallback이 0건이다. 전체 자동 Gate는 0 failure/0 error이며 production source의 금지 값과 미완료 placeholder는 0건이다.
|
||||
|
||||
**수동 확인:** 지원 browser·viewport에서 활성 릴리스 journey, keyboard,
|
||||
200% zoom, 민감정보 비기록, mock banner와 server mode를 확인하고 외부
|
||||
의존 범위를 완료로 표시하지 않았는지 PRD·README·Progress를 대조한다.
|
||||
|
||||
## 5. 요구사항 추적표
|
||||
|
||||
| Phase | PRD 범위 | 집중 test |
|
||||
@@ -2090,11 +2269,11 @@ assert_no_match "externalCharacterId|SUNDAY|MONDAY|TUESDAY|WEDNESDAY|THURSDAY|FR
|
||||
| 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 | `MOCK-001~009`, §11.1~11.2, §12~13 | `src/shared/mocks`, `tests/e2e/mock-preview-shell.spec.ts` |
|
||||
| 3 | `CHAR-001~014`, Character 관련 `FILE`, `MOCK`, §7, §9 | `src/features/characters`, `tests/e2e/character-workspace.spec.ts` |
|
||||
| 4 | `AUDIO-001~028`, Audio 관련 `FILE`, `MOCK`, §9 | `src/features/audio-contents`, `tests/e2e/audio-content.spec.ts` |
|
||||
| 5 | `SERIES-001~013`, Series 관련 `FILE`, `MOCK`, §9 | `src/features/series`, `tests/e2e/series.spec.ts` |
|
||||
| 6 | `COMMUNITY-001~011`, Community 관련 `FILE`, `MOCK`, §9 | `src/features/community-posts`, `tests/e2e/community-post.spec.ts` |
|
||||
| 7 | `FANTALK-001~008`, `MOCK`, §9 | `src/features/fan-talks`, `tests/e2e/fan-talk.spec.ts` |
|
||||
| 3 | `CHAR-001~018`, Character 관련 `FILE`, `MOCK`, §7, §9 | `src/features/characters`, `tests/e2e/character-workspace.spec.ts` |
|
||||
| 4 | `AUDIO-001~033`, Audio 관련 `FILE`, `MOCK`, §9 | `src/features/audio-contents`, `tests/e2e/audio-content.spec.ts` |
|
||||
| 5 | `SERIES-001~018`, Series 관련 `FILE`, `MOCK`, §9 | `src/features/series`, `tests/e2e/series.spec.ts` |
|
||||
| 6 | `COMMUNITY-001~015`, Community 관련 `FILE`, `MOCK`, §9 | `src/features/community-posts`, `tests/e2e/community-post.spec.ts` |
|
||||
| 7 | `FANTALK-001~011`, `MOCK`, §9 | `src/features/fan-talks`, `tests/e2e/fan-talk.spec.ts` |
|
||||
| 8 | `COMMENT-001~006`, `MOCK`, §9 | `src/features/comments`, `tests/e2e/comments.spec.ts` |
|
||||
| 9 | §9~10, §12~14, 활성 범위 전체 | 전체 unit/integration/mock·server E2E/build |
|
||||
|
||||
@@ -2121,6 +2300,12 @@ assert_no_match "externalCharacterId|SUNDAY|MONDAY|TUESDAY|WEDNESDAY|THURSDAY|FR
|
||||
|
||||
구현 단계마다 아래 형식으로 누적하고 기존 기록을 삭제하거나 덮어쓰지 않는다.
|
||||
|
||||
> **계약 이력 안내 (2026-07-28):** 아래 2026-07-27 기록의
|
||||
> `API Contract §...` 표기는 당시 사용한 삭제 전 `api-contract.md`의
|
||||
> section을 가리킨다. 현재 Phase 3 이후 구현 기준은
|
||||
> [api-contract.openapi.json](./api-contract.openapi.json)이며 과거 section
|
||||
> 표기는 실행 당시 근거를 보존하기 위해 수정하지 않는다.
|
||||
|
||||
```markdown
|
||||
### N차 구현 또는 수정 — YYYY-MM-DD
|
||||
|
||||
@@ -2463,3 +2648,77 @@ assert_no_match "externalCharacterId|SUNDAY|MONDAY|TUESDAY|WEDNESDAY|THURSDAY|FR
|
||||
- E2E: `npm run e2e` — 4 projects / 36 tests 통과. `npm run e2e:mock` — 4 projects / 28 tests 통과.
|
||||
- Diff: `git diff --check -- docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md` — exit 0.
|
||||
- 남은 항목: Phase 2 review의 열린 확정 항목 없음. `P2-GATE`와 모든 회귀 수정이 완료돼 Phase 3 진행 가능. mock 통과는 Phase 3의 실제 server integration 완료로 간주하지 않는다.
|
||||
|
||||
### OpenAPI 계약 교체 및 Phase 3~9 문서 정합화 — 2026-07-28
|
||||
|
||||
- 무엇을: 삭제된 Markdown 계약을 `api-contract.openapi.json`으로 교체한
|
||||
사실을 PRD와 계획에 반영하고, Phase 3~9의 query·multipart
|
||||
part·request/response DTO·오류·외부 의존·TDD/검증 계약을 새 OpenAPI에
|
||||
맞췄다. 가이드·샘플은 실제 API Contract 형식을 사용하도록
|
||||
일반화하고 과거 review에는 계약 이력 안내만 추가했다.
|
||||
- 왜: 삭제된 계약 링크와 기존 DTO 가정을 그대로 두면 Phase 3 이후
|
||||
구현자가 `releaseDateUtc`, `seriesIds`, `{contentIds}`,
|
||||
`{seriesIds}`, Community 상세, FanTalk 수정과 댓글 CRUD처럼 OpenAPI에
|
||||
없는 계약을 추정하게 되기 때문이다.
|
||||
- 어떻게:
|
||||
- OpenAPI parse·구조 검사 — `openapi=3.1.0 version=2.0.0 paths=15
|
||||
operations=23`.
|
||||
- schema assertion — Character 필수 image/systemPrompt, Audio
|
||||
contentFile/releaseDate/themeId, Series keyword/contentIdList/ids,
|
||||
Community postImage/audioFile, FanTalk pagination field를 확인해
|
||||
`schema assertions=passed`.
|
||||
- 요구사항 ID 검사 — AUTH 13, CHAR 18, AUDIO 33, SERIES 18,
|
||||
COMMUNITY 15, FANTALK 11, COMMENT 6, FILE 15, MOCK 9개이며 중복 0건.
|
||||
백엔드 외부 의존은 `EXT-001~011`로 추적한다.
|
||||
- Phase 계획 검사 — Phase 3~9의 Task 실행 계약 7개와 Gate 수동 확인
|
||||
7개를 확인했다.
|
||||
- Markdown link 검사 — 대상 11개 문서의 상대 링크가 모두 존재했다.
|
||||
- stale 활성 링크 검사 — PRD·plan·가이드·샘플의 삭제된
|
||||
`api-contract.md` 링크 0건. 과거 review와 검증 기록의 표기는 계약
|
||||
이력 안내 아래 역사 근거로 보존했다.
|
||||
- 회귀 검사 — `npm run test:run` 34 files / 147 tests 통과,
|
||||
`npm run typecheck`와 `npm run lint` 모두 출력 없이 exit 0.
|
||||
- 변경 범위 검사 — 애플리케이션 source·설정 diff 0건이며
|
||||
`git diff --check`는 출력 없이 exit 0.
|
||||
- 문서 위치 정정: 기능별 설계·실행 문서를 `docs/superpowers/`에 따로
|
||||
두지 않고 설계 결정은 이 디렉터리의 `prd.md`, 실행 계약과 검증 기록은
|
||||
`plan-task.md`에 통합했다. `docs/agent-guide/documentation.md`에도 같은
|
||||
배치 규칙을 명시했다.
|
||||
- 남은 항목: 추가 사용자 결정은 없다. backend가 제공해야 하는
|
||||
original work·genre lookup, Series edit DTO, active-only 보장,
|
||||
Community pagination 종료 metadata, FanTalk 상세·수정·filter/sort·
|
||||
유일성 오류, Comments CRUD·권한 오류와 도메인별 오류 key는 Phase별
|
||||
외부 의존으로 남는다.
|
||||
|
||||
### EXT-006 인증 계약 기록·제공 범위 우선 실행 결정 — 2026-07-28
|
||||
|
||||
- 무엇을: `EXT-006`에 현재 구현된 로그인·로그아웃 endpoint, header,
|
||||
body, 성공 data, client 처리와 검증 근거를 기록했다. Phase 3부터
|
||||
제공된 OpenAPI 범위를 먼저 구현해 Phase 9 활성 범위 Gate까지 진행하고
|
||||
미제공 계약은 후속 vertical slice로 보완하는 실행 전략을 확정했다.
|
||||
- 왜: 이미 동작하는 인증을 구현 대기로 오해하지 않게 하고, backend가
|
||||
정식 OpenAPI를 작성할 때 현재 프론트엔드 계약을 바로 대조할 수 있게
|
||||
하며, 독립 기능을 계약 대기 때문에 직렬로 지연하지 않기 위해서다.
|
||||
- 어떻게:
|
||||
- 구현 대조 — `src/features/auth/api/auth-api.ts`,
|
||||
`src/features/auth/model/auth-session.tsx`, auth contract/session/mock
|
||||
test와 `tests/e2e/auth.spec.ts`에서 두 endpoint와 client 동작을 확인했다.
|
||||
- 문서 구조 검사 — `EXT-006` endpoint·비차단 상태, Phase 3~9 진행
|
||||
문구, `P8-GATE`·`P9-GATE` 후속 규칙, `EXT-001~011`, Markdown
|
||||
link·fence를 검사해 모두 통과했다.
|
||||
- focused unit — `npm run test:run --
|
||||
src/features/auth/tests/auth-api.test.ts
|
||||
src/features/auth/tests/auth-session.test.tsx
|
||||
src/shared/mocks/__tests__/auth-handlers.test.ts`는 3 files / 26 tests
|
||||
통과.
|
||||
- 인증 E2E — `npm run e2e -- tests/e2e/auth.spec.ts`는 최초 sandbox
|
||||
local listen `EPERM`으로 실패했고, 포트 권한을 허용한 동일 명령
|
||||
재실행에서 4 browser projects / 4 tests 통과.
|
||||
- `git diff --check` — 출력 없이 exit 0.
|
||||
- 변경 범위: `prd.md`, `plan-task.md`만 보완했으며
|
||||
`api-contract.openapi.json`과 애플리케이션 코드·test·설정은 변경하지
|
||||
않았다.
|
||||
- 남은 항목: backend가 `EXT-006` 두 operation을 정식 OpenAPI에
|
||||
포함하거나 별도 version 계약으로 고정하면 현재 기록과 대조해
|
||||
정규화한다. 다른 외부 의존은 제공 범위 구현을 차단하지 않고 각 후속
|
||||
vertical slice에서 보완한다.
|
||||
|
||||
@@ -4,13 +4,14 @@
|
||||
|
||||
| 항목 | 내용 |
|
||||
|---|---|
|
||||
| 문서 상태 | 인터뷰 반영 초안 |
|
||||
| 문서 상태 | OpenAPI 반영 구현 기준 |
|
||||
| 작성일 | 2026-07-25 |
|
||||
| 최종 수정일 | 2026-07-28 |
|
||||
| 대상 제품 | AI 캐릭터 전용 독립 관리자 웹 |
|
||||
| 구현 대상 | React + TypeScript + Vite SPA |
|
||||
| UI 기반 | Tailwind CSS + shadcn/ui |
|
||||
| 관련 계획 | [plan-task.md](./plan-task.md) |
|
||||
| 정규화 계약 | [api-contract.md](./api-contract.md) |
|
||||
| 정규화 계약 | [api-contract.openapi.json](./api-contract.openapi.json) |
|
||||
|
||||
### 상태 표기
|
||||
|
||||
@@ -18,12 +19,13 @@
|
||||
- **미결**: 프론트엔드 제품·UI 또는 운영 정책이 결정되지 않아 후속 인터뷰나 UI 검토가 필요한 사항
|
||||
- **외부 의존**: 프론트엔드가 결정할 사항이 아니며, 해당 기능의 network integration 전에 백엔드가 제공해야 하는 계약
|
||||
- **권고**: 미결 사항에 대한 현재 추천안이며, 확정 전에는 계약으로 간주하지 않음
|
||||
- **제외**: 현재 OpenAPI 또는 릴리스 범위에 포함하지 않으며, 다시 포함할 조건을 별도로 기록한 사항
|
||||
|
||||
### 문서 유지보수 원칙
|
||||
|
||||
1. 요구사항이 변경되면 `prd.md`의 결정 기록, 기능 요구사항, API 계약 보정표, 미결 사항을 함께 갱신한다.
|
||||
1. 요구사항이 변경되면 `prd.md`의 결정 기록, 기능 요구사항, OpenAPI 소비 주의사항, 미결·외부 의존 사항을 함께 갱신한다.
|
||||
2. 구현 범위나 순서가 바뀌면 같은 디렉터리의 `plan-task.md`도 같은 변경에서 갱신한다.
|
||||
3. 사용자 인터뷰 결정과 최초 API Contract가 충돌하면 이 문서의 “API 계약 보정사항”을 우선한다.
|
||||
3. endpoint, query, multipart part, request/response field, required 여부, status와 오류 응답은 OpenAPI 계약을 단일 진실 원천으로 사용한다. OpenAPI에 표현되지 않는 제품·UI·운영 정책은 이 문서가 소유한다.
|
||||
4. 미결 사항과 외부 의존 계약은 추측으로 구현하지 않는다. **미결**에는 추천안을, **외부 의존**에는 제공 주체와 영향을 함께 기록한다.
|
||||
5. 완료된 미결 사항과 제공 완료된 외부 의존 계약은 결정일과 결정 내용을 “결정 기록”에 추가한 뒤 관련 수용 기준까지 갱신한다.
|
||||
6. goal 실행의 objective·순서·완료 증거·범위는 `plan-task.md`의 `Phase Goal`과 `Goal 실행`을 기준으로 한다. goal 수행 중 제품 결정이 바뀌면 이 문서의 결정 기록과 요구사항을 먼저 갱신한다.
|
||||
@@ -49,7 +51,7 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
|
||||
- 오디오 콘텐츠를 관리자 화면에서 즉시 재생해 검수해야 한다.
|
||||
- 예약 공개, 시리즈 연결과 순서, 게시글 고정, FanTalk 단일 답변 같은 도메인 규칙을 UI에서 명확히 안내해야 한다.
|
||||
- 데스크톱에서는 전체 운영을 수행하고 모바일에서는 조회와 긴급 응대가 가능해야 한다.
|
||||
- 최초 API Contract의 잘못되었거나 누락된 필드를 구현 전에 바로잡아야 한다.
|
||||
- 정식 OpenAPI 계약과 기존 PRD·구현 계획의 잘못되었거나 누락된 API 전제를 구현 전에 바로잡아야 한다.
|
||||
|
||||
## 3. Goals
|
||||
|
||||
@@ -57,7 +59,7 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
|
||||
|
||||
- AI 캐릭터 목록 검색, 상세 조회, 생성, 수정, 비활성화를 제공한다.
|
||||
- 선택한 캐릭터 문맥에서 오디오 콘텐츠, 시리즈, 커뮤니티 게시글을 관리한다.
|
||||
- 선택한 캐릭터로 FanTalk에 한 번 답변하고 기존 답변을 수정할 수 있게 한다.
|
||||
- 선택한 캐릭터로 FanTalk에 한 번 답변할 수 있게 한다. 기존 답변 수정은 OpenAPI에 수정 endpoint가 추가된 뒤 활성화한다.
|
||||
- 오디오 콘텐츠와 커뮤니티 첨부 오디오를 관리자 화면에서 재생할 수 있게 한다.
|
||||
- 오디오 콘텐츠와 커뮤니티 게시글의 댓글·답글을 캐릭터 명의로 관리한다.
|
||||
- 비활성 리소스와 권한 오류를 안전하게 처리하고 의도하지 않은 변경을 방지한다.
|
||||
@@ -110,7 +112,7 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
|
||||
3. 캐릭터 워크스페이스의 기본 정보, 오디오, 시리즈, 커뮤니티, FanTalk 탭을 이동한다.
|
||||
4. 활성 캐릭터라면 데스크톱·태블릿에서 리소스를 생성·수정·비활성화한다.
|
||||
5. 오디오 플레이어로 캐릭터가 올린 오디오를 검수한다.
|
||||
6. FanTalk에는 한 번만 답변하고 필요하면 기존 답변을 수정한다.
|
||||
6. FanTalk 목록 item에서 답변이 없을 때 한 번 답변한다. 기존 답변 수정은 수정 계약이 제공된 뒤 추가한다.
|
||||
7. 오디오 또는 커뮤니티 댓글에 캐릭터 명의로 댓글·답글을 작성하거나 운영 정책에 따라 삭제한다.
|
||||
8. 모바일에서는 리소스를 조회하고 오디오를 재생하며 댓글과 FanTalk 답변을 관리한다.
|
||||
9. JWT가 만료되거나 폐기되면 인증 정보를 지우고 로그인 화면으로 이동한다.
|
||||
@@ -137,16 +139,16 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
|
||||
/community-posts
|
||||
/community-posts/new
|
||||
/fan-talks
|
||||
/fan-talks/:fanTalkId
|
||||
```
|
||||
|
||||
라우트 문자열은 구현 시 확정하되 다음 원칙은 고정한다.
|
||||
|
||||
- 캐릭터를 선택하지 않은 전역 화면은 로그인과 캐릭터 목록·생성뿐이다.
|
||||
- 캐릭터 리소스 화면은 모두 URL에 `characterId`를 포함한다.
|
||||
- 목록의 `search`, `status`, 답변 상태, `page`, `size`는 가능한 범위에서 URL query에 보존한다.
|
||||
- 계약이 제공하는 `searchTerm`, `search_word`, `page`, `size`와 제품 filter 상태는 URL query에 보존한다. 계약에 없는 server filter를 client 전체 결과 filter처럼 가장하지 않는다.
|
||||
- 상세 GET이 제공되는 주요 리소스의 목록과 상세 화면은 새로고침과 직접 링크 진입이 가능해야 한다.
|
||||
- 커뮤니티 게시글은 별도 상세·수정 route 없이 목록 행/카드에서 여는 Sheet를 사용한다. 페이지 새로고침은 목록을 다시 조회한다.
|
||||
- FanTalk도 별도 상세 GET이 없으므로 목록 item을 source로 Sheet 또는 panel을 열고 답변을 작성한다.
|
||||
- 존재하지 않거나 다른 캐릭터 소유인 하위 리소스는 서버 결과에 따라 오류 화면으로 처리한다.
|
||||
|
||||
### 7.2 캐릭터 워크스페이스
|
||||
@@ -182,7 +184,7 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
|
||||
|---|---|---|
|
||||
| CHAR-001 | 확정 | 이름 검색, 페이지네이션이 있는 캐릭터 목록을 제공한다. |
|
||||
| CHAR-002 | 확정 | 캐릭터 상세, 생성, 수정, 비활성화를 제공한다. |
|
||||
| CHAR-003 | 확정 | 생성 입력은 `name`, `description`, 선택 이미지, 선택 `originalWorkId`다. |
|
||||
| CHAR-003 | 확정 | 생성 multipart는 필수 `image`와 필수 `request` part를 사용한다. `request`의 필수 입력은 `name`, `systemPrompt`, `description`이고 나머지 필드는 OpenAPI의 optional/nullable 정의를 따른다. |
|
||||
| CHAR-004 | 확정 | 생성 요청에는 `isActive`를 보내지 않는다. 최초 활성 상태는 백엔드가 결정한다. |
|
||||
| CHAR-005 | 확정 | `externalCharacterId`는 존재하지 않는 값이므로 모든 요청·응답·UI에서 제거한다. |
|
||||
| CHAR-006 | 확정 | 일반 수정 요청에서는 `isActive`를 생략하고 soft delete 요청에만 `isActive=false`를 보낸다. `isActive=true`는 전송하지 않는다. |
|
||||
@@ -190,16 +192,22 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
|
||||
| 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 목록으로 이동하고 성공 알림을 표시한다. 현재 상세 화면에 머물지 않는다. |
|
||||
| CHAR-011 | 제외 | 현 OpenAPI의 Character 목록·상세 응답에는 `creatorMemberId`, `creatorNickname`이 없다. 계약에 추가되기 전에는 creator 정보 UI와 DTO를 만들지 않는다. |
|
||||
| CHAR-012 | 외부 의존 | OpenAPI는 `searchTerm`을 생략하면 활성 목록을 반환한다고 명시하지만, `searchTerm` 지정 시에는 “레거시 검색”만 명시해 active-only 여부가 불명확하다. 검색 결과 보장이 추가되기 전에도 client 활성 filter는 만들지 않고 서버 반환값을 표시한다. |
|
||||
| CHAR-013 | 외부 의존 | `originalWorkId`는 생성·수정 request에서 optional nullable이므로 미선택 시 key 생략과 `null`이 모두 계약상 가능하다. 원작 검색 선택기에 필요한 lookup API는 OpenAPI에 없으므로 제공 전에는 원작 선택 network integration을 구현하지 않는다. |
|
||||
| CHAR-014 | 확정 | 캐릭터 soft delete 성공 시 캐릭터 목록으로 이동해 재조회하고 성공 알림을 표시한다. 현재 상세 화면에 머물지 않는다. 비활성 항목 제외는 active-only 계약 제공 후 검증한다. |
|
||||
| CHAR-015 | 확정 | 목록은 `searchTerm`, `page`, `size`를 사용하고 `data.totalCount`, `data.content[]`를 소비한다. 목록 ID field는 `id`다. |
|
||||
| CHAR-016 | 확정 | 생성·수정 성공은 `data=null`이므로 생성 후 목록을 무효화해 이동하고, 수정 후 기존 `characterId` 상세와 목록을 다시 조회한다. 생성 응답에서 새 ID나 상세 DTO를 추정하지 않는다. |
|
||||
| CHAR-017 | 확정 | 상세의 `characterUUID`는 OpenAPI에 존재하는 읽기 전용 값이며, 제거된 `externalCharacterId`와 다른 field다. |
|
||||
| CHAR-018 | 확정 | 생성 request에는 `region`이 있지만 수정 request에는 없다. 수정 화면에서 region을 읽기 전용으로 표시하고 update payload에 보내지 않는다. |
|
||||
|
||||
#### 캐릭터 생성·수정 폼
|
||||
|
||||
- 이름과 설명은 명시적인 레이블과 필드 오류 영역을 가진다.
|
||||
- 이름, system prompt와 설명은 명시적인 레이블과 필드 오류 영역을 가진다.
|
||||
- OpenAPI의 optional scalar(`age`, `gender`, `mbti`, `speechPattern`, `speechStyle`, `appearance`, `region`, `originalTitle`, `originalLink`, `characterType`)와 배열(`tags`, `hobbies`, `values`, `goals`, `relationships`, `personalities`, `backgrounds`, `memories`)을 생성 form에서 편집할 수 있게 한다. `originalWorkId`는 lookup 계약 제공 후 선택기로 편집하고, 수정 request에 없는 `region`은 수정 화면에서 읽기 전용이다.
|
||||
- 이미지는 미리보기, 교체, 새로 선택한 파일의 선택 취소를 제공한다. 기존 이미지 자체를 제거하는 기능은 contract가 없어 현재 범위가 아니다.
|
||||
- 원작은 이름 검색형 Combobox로 선택한다. 미선택은 허용하되 multipart JSON에서 `null`을 보낼지 key를 생략할지는 API 계약으로 확정한다.
|
||||
- 생성 이미지는 필수다. 수정 이미지는 선택이며 미전송하면 기존 이미지를 유지한다.
|
||||
- 원작 lookup 계약이 제공되면 이름 검색형 Combobox로 선택한다. 미선택은 허용하고 serializer는 key 생략 또는 `null` 중 한 가지 canonical form을 contract test로 고정한다.
|
||||
- 비활성화는 폼 Switch가 아니라 영향 범위를 설명하는 확인 Dialog로 실행한다.
|
||||
- 저장 중 중복 제출을 막고 성공 후 상세 데이터를 다시 동기화한다.
|
||||
|
||||
@@ -207,18 +215,18 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
|
||||
|
||||
| ID | 상태 | 요구사항 |
|
||||
|---|---|---|
|
||||
| AUDIO-001 | 확정 | 선택한 캐릭터의 오디오 목록·검색·상태 필터·상세·생성·수정·비활성화를 제공한다. |
|
||||
| AUDIO-002 | 확정 | 상태 값은 `OPEN`과 `SCHEDULED` 두 개뿐이다. |
|
||||
| AUDIO-003 | 확정 | `OPEN`은 현재 출시된 콘텐츠, `SCHEDULED`는 미래 출시 예약 콘텐츠다. |
|
||||
| AUDIO-004 | 확정 | 상태는 백엔드가 공개 시각을 기준으로 계산해 반환하고 프론트엔드는 재계산하지 않는다. |
|
||||
| AUDIO-005 | 확정 | 상태 필터를 보내지 않은 경우의 결과 집합도 백엔드가 결정해 반환한다. |
|
||||
| AUDIO-001 | 확정 | 선택한 캐릭터의 오디오 목록·제목 검색·상세·생성·수정·비활성화를 제공한다. |
|
||||
| AUDIO-002 | 제외 | 현 OpenAPI 목록·상세에는 `OPEN`, `SCHEDULED` status field가 없고 목록 status query도 없다. 상태 badge와 server status filter는 계약에 추가되기 전에는 제공하지 않는다. |
|
||||
| AUDIO-003 | 확정 | 공개 예약은 생성 request의 nullable `releaseDate`와 `timezone`으로 표현한다. 목록·상세에서는 OpenAPI가 반환한 `releaseDate` 문자열을 그대로 표시하고 별도 status enum을 만들지 않는다. |
|
||||
| AUDIO-004 | 확정 | 프론트엔드는 `releaseDate`로 `OPEN`·`SCHEDULED` 같은 API status를 재계산하거나 DTO에 추가하지 않는다. |
|
||||
| AUDIO-005 | 제외 | 현 OpenAPI에 status filter가 없으므로 status query를 보내거나 현재 page를 client에서 status별로 거르지 않는다. |
|
||||
| AUDIO-006 | 확정 | 생성 요청에는 `isActive`를 보내지 않는다. |
|
||||
| AUDIO-007 | 확정 | 일반 수정 요청에서는 `isActive`를 생략하고 soft delete 요청에만 `isActive=false`를 보낸다. `isActive=true`는 전송하지 않는다. |
|
||||
| AUDIO-008 | 확정 | 공개 방식은 “지금 즉시 공개”와 “예약 공개” 두 선택 버튼으로 제공한다. |
|
||||
| AUDIO-009 | 확정 | 생성 시 즉시 공개가 기본값이며 `releaseDateUtc=null`을 보낸다. 예약 날짜 입력은 비활성화하고 기존 값을 지운다. |
|
||||
| AUDIO-009 | 확정 | 생성 시 즉시 공개가 기본값이며 `releaseDate=null`과 `timezone="Asia/Seoul"`을 보낸다. 예약 날짜 입력은 비활성화하고 기존 값을 지운다. |
|
||||
| AUDIO-010 | 확정 | 예약 공개를 선택한 경우에만 날짜·시간을 입력할 수 있고 미래 시각이 필수다. |
|
||||
| AUDIO-011 | 확정 | 예약 시각은 Asia/Seoul로 입력·표시하고 API에는 UTC ISO-8601 `Z` 값으로 변환해 보낸다. |
|
||||
| AUDIO-012 | 확정 | 생성 시 cover image와 audio file은 필수이며 수정 시 교체 파일은 선택이다. |
|
||||
| AUDIO-011 | 확정 | 예약 시각은 Asia/Seoul로 입력·표시하고 생성 API에는 `yyyy-MM-dd HH:mm` 형식의 `releaseDate`와 `timezone="Asia/Seoul"`을 보낸다. UTC `Z` 값으로 변환하지 않는다. |
|
||||
| AUDIO-012 | 확정 | 생성 multipart의 `contentFile`, `coverImage`, `request`는 필수다. 수정은 `coverImage`와 `request`만 허용하므로 오디오 원본 파일 교체 UI를 제공하지 않는다. |
|
||||
| AUDIO-013 | 확정 | 오디오 확장자는 `.mp3`, `.aac`, `.m4a`를 허용한다. WAV는 허용하지 않는다. |
|
||||
| AUDIO-014 | 확정 | canonical MIME은 `audio/mpeg`, `audio/aac`, `audio/mp4`다. |
|
||||
| AUDIO-015 | 확정 | 오디오 파일의 운영 기준 최대 크기는 1,024MB이고 최대 재생 길이는 제한하지 않는다. |
|
||||
@@ -226,17 +234,22 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
|
||||
| AUDIO-017 | 확정 | 업로드 진행률, 취소, 전체 재시도를 제공하고 resumable upload는 제공하지 않는다. |
|
||||
| AUDIO-018 | 확정 | 업로드 실패 후 입력한 폼 값과 선택 가능한 파일 상태를 최대한 유지한다. |
|
||||
| AUDIO-019 | 확정 | 가격 단위는 “캔”이고 0 이상의 정수다. 0은 무료이며 UI는 예: `1,000캔`으로 표시한다. |
|
||||
| AUDIO-020 | 확정 | 오디오를 여러 시리즈에 연결할 수 있도록 `seriesIds` 다중 선택을 제공한다. |
|
||||
| AUDIO-020 | 확정 | Audio 생성·수정 request에는 `seriesIds`가 없다. 시리즈 연결은 Audio form이 아니라 Series 콘텐츠 연결 endpoint와 Phase 5 UI에서 관리한다. |
|
||||
| AUDIO-021 | 확정 | 목록과 상세에서 오디오를 재생할 수 있다. |
|
||||
| AUDIO-022 | 확정 | 오디오 목록은 활성 오디오만 반환하며 활성 상태 filter/query를 제공하지 않는다. 기존 `status=OPEN|SCHEDULED` 공개 상태 필터는 유지한다. |
|
||||
| AUDIO-022 | 외부 의존 | 오디오 목록 request에는 활성 상태 query와 status query가 없고 응답에도 `isActive`가 없다. active-only 반환 보장은 OpenAPI에 명시돼야 하며 client-side 활성 filter는 만들지 않는다. |
|
||||
| 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-025 | 확정 | 오디오 soft delete 성공 시 선택 캐릭터의 오디오 목록으로 이동해 재조회하고 성공 알림을 표시한다. 현재 상세 화면에 머물지 않는다. 비활성 항목 제외는 active-only 계약 제공 후 검증한다. |
|
||||
| AUDIO-026 | 확정 | 재생 오류를 signed URL 만료로 구분하거나 추정하지 않는다. media error만으로 상세·목록을 자동 재조회하거나 자동 재생하지 않고, 일반 오류와 수동 재시도·페이지 새로고침 안내를 제공한다. |
|
||||
| AUDIO-027 | 확정 | 오디오 콘텐츠 생성 시 `themeId`는 필수이며, 프론트엔드는 `GET /api/v2/admin/ai-characters/audio-content-themes`로 테마 목록을 불러와 선택 UI를 제공한다. |
|
||||
| AUDIO-028 | 확정 | 오디오 콘텐츠 테마 목록 조회는 query/body 없이 호출하며 응답의 `themeId`, `themeName`, `imageUrl`만 사용한다. |
|
||||
| AUDIO-027 | 확정 | 오디오 콘텐츠 생성 시 유효한 `themeId`가 필수다. OpenAPI의 기본값 `0`은 binding 기본값일 뿐 domain에서 유효하지 않으므로 프론트엔드는 테마 선택을 강제한다. |
|
||||
| AUDIO-028 | 확정 | 오디오 콘텐츠 테마 목록 조회는 query/body 없이 호출하며 응답 `data[]`의 `id`, `theme`, `image`를 사용한다. |
|
||||
| AUDIO-029 | 확정 | 목록 검색 query는 `search_word`이며 검색어가 2자 이상일 때만 보낸다. 목록 응답은 `data.totalCount`, `data.items[]`를 사용한다. |
|
||||
| AUDIO-030 | 확정 | 상세 GET은 필수 `timezone=Asia/Seoul` query를 보낸다. |
|
||||
| AUDIO-031 | 확정 | 생성 성공은 `data.contentId`를 사용해 상세로 이동할 수 있다. 수정·soft delete 성공은 `data=null`이므로 기존 ID 기준 cache를 무효화한다. |
|
||||
| AUDIO-032 | 확정 | 생성 `request`는 필수 `title`, `detail`, `tags`, `price`와 OpenAPI의 optional field만 보낸다. 수정은 `title`, `detail`, `tags`, `price`, `isAdult`, `isActive`, `isPointAvailable`, `isCommentAvailable`만 변경할 수 있다. |
|
||||
| AUDIO-033 | 확정 | 생성 form은 OpenAPI의 `purchaseOption`, `limited`, `isAdult`, `isGeneratePreview`, `isOnlyRental`, `isPointAvailable`, `isCommentAvailable`, `isFullDetailVisible`, `previewStartTime`, `previewEndTime`, `languageCode`를 계약 enum·type과 default에 맞춰 제공한다. 계약에 없는 추가 상관관계 validation은 만들지 않는다. |
|
||||
|
||||
수정 화면은 즉시 공개로 재초기화하지 않는다. 서버의 기존 `releaseDateUtc`와 `status`로 공개 방식과 날짜를 초기화하고, 관리자가 바꾸지 않으면 기존 값을 유지한다.
|
||||
현 수정 계약에는 `releaseDate`, `timezone`, `themeId`, `contentFile`이 없다. 따라서 공개 예약·테마·오디오 원본 변경은 생성 화면에서만 제공하고 수정 화면에서는 읽기 전용으로 표시한다.
|
||||
|
||||
#### 관리자 오디오 플레이어
|
||||
|
||||
@@ -254,22 +267,27 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
|
||||
|---|---|---|
|
||||
| SERIES-001 | 확정 | 목록·상세·생성·수정·비활성화, 콘텐츠 연결·해제, 시리즈 순서 변경을 제공한다. |
|
||||
| SERIES-002 | 확정 | 상태 enum은 `PROCEEDING`(연재중), `SUSPEND`(휴재중), `COMPLETE`(완결)이다. `OPEN`은 유효하지 않다. |
|
||||
| SERIES-003 | 확정 | 생성 시 state를 선택하거나 보내지 않는다. 초기 state는 백엔드가 결정하며 프론트엔드는 정확한 기본값을 알 필요 없이 생성 응답의 state를 그대로 표시한다. |
|
||||
| SERIES-003 | 확정 | 생성 시 state를 선택하거나 보내지 않는다. 성공 응답은 `data=null`이므로 생성 후 목록으로 이동해 서버가 결정한 state를 다시 조회한다. |
|
||||
| SERIES-004 | 확정 | 수정 시 state를 선택할 수 있으며 선택하지 않으면 필드를 생략해 이전 상태를 유지한다. |
|
||||
| SERIES-005 | 확정 | 연재 요일 enum은 `SUN`, `MON`, `TUE`, `WED`, `THU`, `FRI`, `SAT`, `RANDOM`이다. |
|
||||
| SERIES-006 | 확정 | `RANDOM`은 다른 요일과 함께 보낼 수 없다. 값은 RANDOM 단독 또는 하나 이상의 실제 요일 목록이어야 한다. |
|
||||
| SERIES-007 | 확정 | 장르는 이름 검색형 선택기로 고르고 API에는 `genreId`를 보낸다. |
|
||||
| SERIES-008 | 확정 | 활성 시리즈 전체를 별도 순서 변경 모드에서 불러와 최종 순서의 모든 `seriesIds`를 전송한다. |
|
||||
| SERIES-007 | 외부 의존 | 생성 시 유효한 `genreId`가 필요하고 OpenAPI 기본값 `0`은 domain에서 유효하지 않다. 장르 이름 검색 API는 OpenAPI에 없으므로 제공 전에는 장르 선택 network integration과 Series 생성을 완료할 수 없다. |
|
||||
| SERIES-008 | 확정 | `data.totalCount`, `data.items[]`를 page별로 읽어 서버가 반환한 시리즈 전체를 별도 순서 변경 모드에 표시하고 최종 순서의 모든 ID를 `{ "ids": [...] }`로 전송한다. active-only 여부는 `SERIES-012` 계약 제공 후 검증한다. |
|
||||
| SERIES-009 | 확정 | drag-and-drop 외에 키보드와 위/아래 버튼으로 순서를 바꿀 수 있어야 한다. |
|
||||
| SERIES-010 | 확정 | 일반 수정 요청에서는 `isActive`를 생략하고 soft delete 요청에만 `isActive=false`를 보낸다. `isActive=true`, 복원과 hard delete는 제공하지 않는다. |
|
||||
| SERIES-011 | 외부 의존 | 장르 이름 검색 API는 백엔드가 제공해야 한다. 제공 전에는 장르 선택 network integration을 구현하지 않는다. 생성 초기 state의 정확한 기본값은 프론트엔드 의존사항이 아니다. |
|
||||
| SERIES-012 | 확정 | 시리즈 목록은 활성 시리즈만 반환하며 활성 상태 filter/query를 제공하지 않는다. |
|
||||
| SERIES-013 | 확정 | 시리즈 soft delete 성공 시 선택 캐릭터의 시리즈 active-only 목록으로 이동하고 성공 알림을 표시한다. 현재 상세 화면에 머물지 않는다. |
|
||||
| SERIES-011 | 외부 의존 | 장르 이름 검색 endpoint·DTO는 백엔드가 제공해야 한다. 제공 전에는 장르 선택 network integration을 구현하지 않는다. 생성 초기 state의 정확한 기본값은 프론트엔드 의존사항이 아니다. |
|
||||
| SERIES-012 | 외부 의존 | 시리즈 목록 request에는 활성 상태 query가 없다. active-only 반환 보장은 OpenAPI에 명시돼야 하며 list item의 `isActive`를 client에서 숨기는 방식으로 대체하지 않는다. |
|
||||
| SERIES-013 | 확정 | 시리즈 soft delete 성공 시 선택 캐릭터의 시리즈 목록으로 이동해 재조회하고 성공 알림을 표시한다. 현재 상세 화면에 머물지 않는다. 비활성 항목 제외는 active-only 계약 제공 후 검증한다. |
|
||||
| SERIES-014 | 확정 | 생성 multipart의 `image`와 `request`는 필수다. 생성 request는 `keyword` 단일 문자열을 사용하며 `keywords` 배열을 보내지 않는다. |
|
||||
| SERIES-015 | 확정 | 목록은 enum `publishedDaysOfWeek`, `genreId`, enum `state`를 사용하지만 상세는 표시용 문자열 `publishedDaysOfWeek`, `genre`, `keywords`, 한국어 `state`를 사용한다. 상세 표시 문자열을 update enum으로 재사용하지 않는다. |
|
||||
| SERIES-016 | 확정 | 연결 후보는 `GET .../contents/search?search_word=...`, 연결은 `{ "contentIdList": [...] }`, 해제는 body 없는 DELETE를 사용한다. |
|
||||
| SERIES-017 | 외부 의존 | 상세 응답에는 update에 필요한 `genreId`, enum `publishedDaysOfWeek`, enum `state`가 없다. 직접 링크에서도 안전하게 수정 form을 초기화할 edit DTO 또는 별도 mapping 계약이 제공되기 전에는 상세 표시 문자열을 역변환하지 않고 수정 network integration을 완료하지 않는다. |
|
||||
| SERIES-018 | 확정 | 생성 request의 `keyword`는 수정 request에 없다. 수정 화면에서 상세의 `keywords`를 읽기 전용으로 표시하고 update payload에 보내지 않는다. |
|
||||
|
||||
#### 시리즈 콘텐츠 연결
|
||||
|
||||
- 현재 연결 콘텐츠를 검색·페이지네이션해 보여준다.
|
||||
- 연결 후보는 선택 캐릭터의 활성 오디오로 제한한다.
|
||||
- 현재 연결 콘텐츠는 `page`, `size`로 조회한다. 현 계약에는 연결 목록 검색 query가 없다.
|
||||
- 연결 후보는 `.../contents/search?search_word=...`의 반환값을 사용하며 선택 캐릭터·해당 시리즈 문맥을 벗어난 별도 Audio 후보 endpoint를 만들지 않는다.
|
||||
- 이미 연결된 콘텐츠를 중복 연결하지 않는다.
|
||||
- 연결 해제 전 대상 제목과 영향을 확인한다.
|
||||
- 연결/해제 성공 후 시리즈 상세와 콘텐츠 목록을 함께 갱신한다.
|
||||
@@ -283,25 +301,32 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
|
||||
| COMMUNITY-003 | 확정 | 이미지와 오디오 파일은 선택 첨부다. |
|
||||
| COMMUNITY-004 | 확정 | 첨부 오디오가 있으면 목록 행/카드와 게시글 Sheet에서 재생할 수 있다. |
|
||||
| COMMUNITY-005 | 확정 | 일반 수정 요청에서는 `isActive`를 생략하고 soft delete 요청에만 `isActive=false`를 보낸다. `isActive=true`는 전송하지 않는다. |
|
||||
| COMMUNITY-006 | 확정 | 비활성 게시글은 반드시 `isFixed=false`, `fixedAtUtc=null` 상태여야 한다. |
|
||||
| COMMUNITY-006 | 확정 | soft delete request에는 `isActive=false`와 `isFixed=false`를 함께 보낸다. 현 목록 응답에는 `fixedAtUtc`가 없으므로 해당 field를 DTO·UI에 만들지 않는다. |
|
||||
| COMMUNITY-007 | 확정 | 가격은 오디오와 동일하게 0 이상의 정수 “캔” 단위를 사용한다. |
|
||||
| COMMUNITY-008 | 확정 | 커뮤니티 게시글 목록은 활성 게시글만 반환하며 활성 상태 filter/query를 제공하지 않는다. |
|
||||
| COMMUNITY-008 | 외부 의존 | 커뮤니티 목록 request에는 활성 상태 query가 없고 item에도 `isActive`가 없다. active-only 반환 보장은 OpenAPI에 명시돼야 하며 client-side filter는 만들지 않는다. |
|
||||
| COMMUNITY-009 | 확정 | 커뮤니티 전용 상세 GET과 상세·수정 직접 route를 추가하지 않는다. 목록 응답으로 행/카드의 Sheet를 열어 조회·수정·고정·비활성화·댓글 진입을 제공한다. |
|
||||
| COMMUNITY-010 | 확정 | 커뮤니티 게시글 soft delete 성공 시 열린 Sheet를 닫고 게시글 active-only 목록을 무효화·재조회해 해당 항목을 제거하며 성공 알림을 표시한다. |
|
||||
| COMMUNITY-011 | 확정 | 첨부 audio URL 갱신만을 위한 자동 요청은 하지 않으며 media error도 refetch trigger로 사용하지 않는다. 사용자 페이지 새로고침이나 mutation 후 cache 무효화 등 일반 목록 재조회가 발생하면 새 응답의 `audioSignedUrl`을 사용한다. |
|
||||
| COMMUNITY-010 | 확정 | 커뮤니티 게시글 soft delete 성공 시 열린 Sheet를 닫고 목록을 무효화·재조회하며 성공 알림을 표시한다. 해당 항목 제외는 active-only 계약 제공 후 검증한다. |
|
||||
| COMMUNITY-011 | 확정 | 첨부 audio URL 갱신만을 위한 자동 요청은 하지 않으며 media error도 refetch trigger로 사용하지 않는다. 사용자 페이지 새로고침이나 mutation 후 cache 무효화 등 일반 목록 재조회가 발생하면 새 응답의 `audioUrl`을 사용한다. |
|
||||
| COMMUNITY-012 | 확정 | 목록 GET은 필수 `timezone=Asia/Seoul`, `page`, `size`를 사용하고 `data`의 게시글 배열을 소비한다. 응답에 `totalCount`, `page`, `hasNext`가 없으므로 전체 건수·마지막 page를 추정하지 않는다. |
|
||||
| COMMUNITY-013 | 확정 | 생성 multipart는 optional `audioFile`, optional `postImage`, 필수 `request`를 사용하고 request에 필수 `content`, `isCommentAvailable`, `isAdult`와 optional `price`만 보낸다. |
|
||||
| COMMUNITY-014 | 확정 | 수정 multipart는 optional `postImage`와 필수 `request`만 허용한다. 수정에서 가격·첨부 audio 교체는 제공하지 않고, 고정은 `isFixed`, soft delete는 `isActive=false`로 처리한다. |
|
||||
| COMMUNITY-015 | 확정 | 생성·수정·고정·soft delete 성공은 `data=null`이므로 목록을 무효화·재조회하고 mutation 응답에 게시글 DTO가 있다고 가정하지 않는다. |
|
||||
|
||||
### 8.6 FanTalk
|
||||
|
||||
| ID | 상태 | 요구사항 |
|
||||
|---|---|---|
|
||||
| FANTALK-001 | 확정 | 기본 목록은 전체 FanTalk를 최신순으로 표시한다. |
|
||||
| FANTALK-002 | 확정 | 필터는 전체, 미답변, 답변 완료 세 가지다. |
|
||||
| FANTALK-001 | 확정 | 기본 목록은 backend가 반환한 순서를 유지한다. 현 계약에 sort query가 없으므로 client가 page 사이의 최신순을 재정렬하지 않는다. |
|
||||
| FANTALK-002 | 외부 의존 | 전체·미답변·답변 완료 server filter query가 OpenAPI에 없다. 전체 결과 filter 계약이 제공되기 전에는 현재 page만 거르는 filter를 완성 기능으로 제공하지 않는다. |
|
||||
| FANTALK-003 | 확정 | 하나의 FanTalk에는 답변을 한 번만 작성할 수 있다. |
|
||||
| FANTALK-004 | 확정 | 답변이 있으면 추가 작성은 차단하고 기존 답변 수정만 허용한다. |
|
||||
| FANTALK-004 | 외부 의존 | 답변이 있으면 추가 작성은 UI에서 차단한다. 기존 답변 수정 endpoint는 OpenAPI에 없으므로 계약 제공 전에는 수정 network integration을 구현하지 않는다. |
|
||||
| FANTALK-005 | 확정 | 답변 삭제는 현재 범위가 아니다. |
|
||||
| FANTALK-006 | 확정 | 데스크톱·태블릿·모바일 모두 답변 작성과 수정을 지원한다. |
|
||||
| FANTALK-007 | 외부 의존 | 제공된 계약에는 답변 POST만 있다. 목록·상세·답변 수정 endpoint와 DTO는 백엔드가 제공해야 하며, 제공 전에는 해당 network integration을 구현하지 않는다. |
|
||||
| FANTALK-006 | 확정 | 데스크톱·태블릿·모바일 모두 목록 조회와 답변 작성을 지원한다. 답변 수정은 수정 계약이 제공된 뒤 같은 viewport 범위에 추가한다. |
|
||||
| FANTALK-007 | 외부 의존 | 목록 GET과 답변 POST는 제공됐다. 별도 상세 GET, 답변 수정 endpoint·DTO, 답변 상태 filter와 sort 계약은 백엔드가 제공해야 하며 제공 전에는 해당 network integration을 구현하지 않는다. |
|
||||
| FANTALK-008 | 외부 의존 | 답변 1개 불변식의 원자적 강제와 중복 생성의 정확한 비2xx status/message key는 백엔드가 결정·제공해야 한다. |
|
||||
| FANTALK-009 | 확정 | 목록은 `page`, `size`를 사용하고 `data.fanTalkCount`, `data.fanTalks`, `data.page`, `data.size`, `data.hasNext`를 소비한다. 각 item의 `creatorReplies[]`로 답변 유무를 판단한다. |
|
||||
| FANTALK-010 | 확정 | 답변 작성은 `{ "content": string }`을 보내고 성공 응답의 `fanTalkId`, `replyId`, `creatorMemberId`, `content`, `createdAtUtc`를 사용한다. |
|
||||
| FANTALK-011 | 확정 | 별도 상세 endpoint가 없으므로 목록 item을 source로 collection Sheet 또는 panel을 열며 `/fan-talks/:fanTalkId` 직접 route를 만들지 않는다. |
|
||||
|
||||
### 8.7 댓글과 답글
|
||||
|
||||
@@ -375,7 +400,8 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
|
||||
| 커뮤니티 목록·게시글 Sheet·오디오 재생 | 전체 | 전체 | 전체 |
|
||||
| 커뮤니티 등록·수정·고정·비활성화 | 전체 | 전체 | 미지원 |
|
||||
| 댓글·답글 관리 | 전체 | 전체 | 전체 |
|
||||
| FanTalk 조회·답변 작성·답변 수정 | 전체 | 전체 | 전체 |
|
||||
| FanTalk 조회·답변 작성 | 전체 | 전체 | 전체 |
|
||||
| FanTalk 답변 수정 | 계약 제공 후 전체 | 계약 제공 후 전체 | 계약 제공 후 전체 |
|
||||
|
||||
모바일에서 미지원인 기능은 좁은 화면에 데스크톱 폼을 억지로 노출하지 않는다. 화면에는 읽기 전용임을 알리고 전체 관리가 필요하면 데스크톱·태블릿 사용을 안내한다.
|
||||
|
||||
@@ -501,7 +527,7 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘
|
||||
- 오류: 서버의 한국어 message, 재시도, 필요한 경우 목록으로 이동
|
||||
- 저장 중: 제출 버튼 비활성화와 진행 표시
|
||||
- 저장 성공: toast와 최신 서버 응답 반영
|
||||
- soft delete 성공: 해당 resource의 active-only 목록으로 이동하고 성공 toast 표시
|
||||
- soft delete 성공: 해당 resource 목록을 무효화·재조회하고 필요한 화면 이동과 성공 toast 표시. 비활성 항목 제외는 active-only 계약 제공 후 검증
|
||||
- 필드 오류: 로컬 validation은 해당 필드 아래에 표시하고 `aria-invalid`, 오류와 입력 연결, 첫 오류 focus를 제공한다. 서버 오류는 `errorProperty`가 실제 필드명을 제공하는 계약일 때만 inline으로 연결한다.
|
||||
- 업로드: 파일별 진행률, 취소, 재시도
|
||||
|
||||
@@ -564,21 +590,31 @@ python3 .codex/skills/ui-ux-pro-max/scripts/search.py \
|
||||
|
||||
## 11. API 계약
|
||||
|
||||
현재 기준은 OpenAPI `3.1.0`, 문서 version `2.0.0`인
|
||||
`api-contract.openapi.json`의 15개 path·23개 operation이다.
|
||||
OpenAPI에 아직 포함되지 않은 기존 로그인·로그아웃은 `EXT-006 현재 구현
|
||||
기준 계약`에 별도로 기록하며, 정식 OpenAPI가 제공될 때까지 구현과 회귀
|
||||
검증의 임시 기준으로 사용한다.
|
||||
|
||||
### 11.1 공통 응답
|
||||
|
||||
모든 성공 응답은 `ApiResponse.ok(...)` wrapper를 사용한다.
|
||||
`/api/v2/admin/ai-characters` 아래 OpenAPI operation의 성공 응답은
|
||||
`success`, `message`, `data`, `errorProperty`를 사용한다.
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": null,
|
||||
"data": {}
|
||||
"data": {},
|
||||
"errorProperty": null
|
||||
}
|
||||
```
|
||||
|
||||
`/admin/member/login`과 `/member/logout` 성공 예시는 최상위 `errorProperty=null`도 포함한다. 공통 API client는 성공 응답에서 `errorProperty`가 없거나 `null`인 두 형태를 모두 수용한다.
|
||||
mutation에 따라 `data`는 상세 DTO, ID DTO 또는 `null`이다. 각 operation의
|
||||
response schema를 따르며 공통 client가 임의의 상세 응답으로 정규화하지
|
||||
않는다.
|
||||
|
||||
모든 오류는 의미에 맞는 비2xx status와 `ApiResponse.error(...)` wrapper를 사용한다.
|
||||
오류는 의미에 맞는 비2xx status와 다음 envelope를 사용한다.
|
||||
|
||||
```json
|
||||
{
|
||||
@@ -591,10 +627,13 @@ python3 .codex/skills/ui-ux-pro-max/scripts/search.py \
|
||||
|
||||
- 오류를 2xx로 normalize하지 않는다.
|
||||
- UI는 서버가 반환한 현지화된 한국어 `message`를 우선 사용한다.
|
||||
- 백엔드는 `Accept-Language: ko|en|ja`에 따라 번역하고 누락·미지원 언어는 KO로 fallback한다. security filter도 header를 직접 해석한다.
|
||||
- 모든 목록·검색 endpoint는 `page=0`, `size=20` 기본값과 size 최소 20, 최대 50 보정을 적용한다.
|
||||
- OpenAPI의 `Accept-Language`는 optional이고 기본값은 `ko`다. `ko|en|ja` 이외 값과 header 누락은 KO로 fallback하며, 프론트엔드는 일관되게 `ko`를 보낸다.
|
||||
- OpenAPI operation은 전역 `bearerAuth`를 사용한다. 로그인 이외의 관리자 API에는 `Authorization: Bearer {jwt-token}`을 보낸다.
|
||||
- 공통 `page`는 기본 `0`, 최소 `0`이고 공통 `size`는 기본 `20`, 최소 `1`이다. 전역 최대 `50`은 없다. FanTalk `size`만 설명에 따라 `20..50`으로 보정된다.
|
||||
- Audio 상세와 Community 목록은 필수 `timezone=Asia/Seoul` query를 보낸다.
|
||||
- `characterId`는 선택된 대상 캐릭터가 필요한 하위 resource endpoint에만 사용한다.
|
||||
- 캐릭터 목록·검색과 캐릭터 생성에는 path `characterId`가 없다.
|
||||
- `/admin/member/login`, `/member/logout`은 현 OpenAPI 범위 밖의 기존 인증 계약이다. 두 endpoint의 현재 구현 기준은 `11.5 EXT-006`에 기록하고 Phase 1 구현과 회귀 검증을 유지하되, 정식 OpenAPI operation으로 표기하지 않는다.
|
||||
|
||||
### 11.2 오류 처리 매핑
|
||||
|
||||
@@ -603,67 +642,121 @@ python3 .codex/skills/ui-ux-pro-max/scripts/search.py \
|
||||
| 400 | binding, target 미존재, creator 불변식 등 invalid request | 로컬 검증은 inline, 서버 `errorProperty`가 필드명을 제공할 때만 inline, 그 외 화면 Alert |
|
||||
| 401 | JWT 없음·잘못됨·만료·폐기 | 인증 제거 후 로그인 이동 |
|
||||
| 403 | 비ADMIN 또는 stale ADMIN claim | 접근 거부 화면 |
|
||||
| 404 | 신규 prefix 미매핑 경로 | 찾을 수 없음과 목록 이동 |
|
||||
| 404 | path 또는 target 미존재 | 찾을 수 없음과 가능한 목록 이동 |
|
||||
| 405 | 지원하지 않는 method | 서버 message와 재시도 불가 안내 |
|
||||
| 406 | 허용되지 않는 표현 또는 응답 조건 | 서버 message와 요청 조건 확인 안내 |
|
||||
| 415 | 지원하지 않는 media type | 파일 필드 오류와 허용 형식 안내 |
|
||||
| 500 | 예상하지 못한 오류 | 일반 오류, request ID가 있으면 함께 표시, 재시도 |
|
||||
|
||||
Phase 2~6에서 추가되는 오류는 구현 전에 정확한 비2xx status와 KO/EN/JA message key를 확정해야 한다. 신규 prefix 전용 처리를 legacy/public endpoint로 확장하지 않는다.
|
||||
OpenAPI는 공통 status와 `ApiErrorResponse` shape만 제공하고 도메인별 정확한
|
||||
message key를 열거하지 않는다. 특정 message key 분기가 필요한 기능은
|
||||
구현 전에 backend 계약을 추가로 받아야 하며, 그전에는 status와
|
||||
`errorProperty`가 제공된 경우만 공통 처리한다.
|
||||
|
||||
### 11.3 제공된 endpoint 목록
|
||||
|
||||
모든 query, multipart part, request/response field를 보존한 보정 후 계약은 [api-contract.md](./api-contract.md)를 기준으로 한다.
|
||||
AI 캐릭터 관리자 domain의 query, multipart part, request/response field와
|
||||
오류 응답은 [api-contract.openapi.json](./api-contract.openapi.json)을
|
||||
기준으로 한다. 인증 두 건은 현재 구현·test 기준을 백엔드 정식화 입력으로
|
||||
함께 표시한 것이며 현 OpenAPI operation에는 포함되지 않는다.
|
||||
|
||||
| 영역 | Method | Path | 계약 상태 |
|
||||
|---|---|---|---|
|
||||
| 인증 | POST | `/admin/member/login` | 제공됨, 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}` | 제공됨, 필드 보정 필요 |
|
||||
| 인증 | POST | `/admin/member/login` | 현재 구현·test 기준, OpenAPI 미포함(`EXT-006`) |
|
||||
| 인증 | POST | `/member/logout` | 현재 구현·test 기준, OpenAPI 미포함(`EXT-006`) |
|
||||
| 캐릭터 | GET, POST | `/api/v2/admin/ai-characters` | 제공됨 |
|
||||
| 캐릭터 | GET, PUT | `/api/v2/admin/ai-characters/{characterId}` | 제공됨 |
|
||||
| 오디오 테마 | GET | `/api/v2/admin/ai-characters/audio-content-themes` | 제공됨, query/body 없음 |
|
||||
| 오디오 | GET, POST | `/api/v2/admin/ai-characters/{characterId}/audio-contents` | 제공됨, 생성 필드 보정 필요 |
|
||||
| 오디오 | GET, 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}` | 제공됨 |
|
||||
| 시리즈 | GET, POST | `/api/v2/admin/ai-characters/{characterId}/series` | 제공됨 |
|
||||
| 시리즈 순서 | PUT | `.../series/orders` | 제공됨 |
|
||||
| 커뮤니티 | GET, POST | `.../{characterId}/community-posts` | 제공됨, 생성 필드 보정 필요 |
|
||||
| 시리즈 | GET, PUT | `.../series/{seriesId}` | 제공됨 |
|
||||
| 시리즈 콘텐츠 | GET, POST | `.../series/{seriesId}/contents` | 제공됨 |
|
||||
| 시리즈 연결 후보 | GET | `.../series/{seriesId}/contents/search` | 제공됨 |
|
||||
| 시리즈 콘텐츠 | DELETE | `.../series/{seriesId}/contents/{contentId}` | 제공됨 |
|
||||
| 커뮤니티 | GET, POST | `.../{characterId}/community-posts` | 제공됨 |
|
||||
| 커뮤니티 | PUT | `.../community-posts/{postId}` | 제공됨 |
|
||||
| FanTalk 목록 | GET | `.../{characterId}/fan-talks` | 제공됨 |
|
||||
| FanTalk 답변 | POST | `.../{characterId}/fan-talks/{fanTalkId}/replies` | 제공됨 |
|
||||
|
||||
### 11.4 API 계약 보정사항
|
||||
### 11.4 OpenAPI 소비 시 주의사항
|
||||
|
||||
이 표는 최초 API Contract보다 우선한다.
|
||||
아래 표는 삭제된 Markdown 계약을 기준으로 작성된 기존 문서·구현 계획을
|
||||
OpenAPI로 전환할 때 반드시 반영할 차이다. 제품 정책이 OpenAPI payload에
|
||||
없는 field를 추가하는 근거로 사용돼서는 안 된다.
|
||||
|
||||
| 항목 | 최초 계약 | 확정 보정 |
|
||||
| 영역 | OpenAPI 계약 | 프론트엔드 처리 |
|
||||
|---|---|---|
|
||||
| Character `externalCharacterId` | 요청·응답에 존재 | 존재하지 않는 필드이므로 전부 제거 |
|
||||
| 생성 `isActive` | Character, Audio, Community 예시에 존재 | 모든 생성 요청에서 제거, 서버가 초기값 결정 |
|
||||
| 수정 `isActive` | 수정 예시에 존재 | 일반 수정에서는 key를 생략하고 soft delete에만 `false`를 보낸다. `true`는 전송하지 않는다. |
|
||||
| Audio status | 예시에 `OPEN` | 허용값은 `OPEN`, `SCHEDULED`이며 서버 계산 |
|
||||
| Audio create `themeId` | 누락 | 생성 시 필수이며 오디오 테마 목록 endpoint에서 선택한 `themeId`를 보낸다. |
|
||||
| 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만 표현 | 한 번만 생성, 기존 답변 수정 가능, 삭제 불가 |
|
||||
| Character 목록 | query `searchTerm`; `data={totalCount,content}`; item ID `id` | `search`·`items`·`hasNext`로 바꾸지 않는다. |
|
||||
| Character 생성 | 필수 multipart `image`, `request`; request의 필수 `name`, `systemPrompt`, `description`; 성공 `data=null` | 생성 후 목록을 재조회하고 새 ID를 응답에서 추정하지 않는다. |
|
||||
| Character 상세 | `characterUUID`, `originalWork`를 포함하고 creator field는 없음 | `characterUUID`를 `externalCharacterId`로 취급하지 않고 creator UI는 만들지 않는다. |
|
||||
| Audio 목록 | query `search_word`; `data={totalCount,items}`; status query/field 없음 | 2자 이상 제목 검색만 보내고 status filter·badge를 만들지 않는다. |
|
||||
| Audio 테마 | `data=[{id,theme,image}]` | `themeId`·`themeName`·`imageUrl`로 역직렬화하지 않는다. |
|
||||
| Audio 생성 | multipart `contentFile`, `coverImage`, `request`; `releaseDate`는 `yyyy-MM-dd HH:mm`; 성공 `data.contentId` | `audioFile`, `releaseDateUtc`, `seriesIds`를 보내지 않는다. |
|
||||
| Audio 수정 | optional `coverImage`와 제한된 request field만 제공 | content file, 공개 예약, theme, series 연결 수정 UI를 제공하지 않는다. |
|
||||
| Series 생성 | 필수 `image`; request의 `keyword`는 문자열; 성공 `data=null` | `keywords` 배열을 보내지 않고 목록으로 이동해 재조회한다. |
|
||||
| Series 상세 | 요일·장르·keywords·state가 표시용 문자열 | 목록 enum 또는 update payload 값으로 재사용하지 않는다. |
|
||||
| Series 연결·순서 | 후보 `contents/search`; 연결 `{contentIdList}`; 순서 `{ids}` | `{contentIds}`, `{seriesIds}`를 보내지 않는다. |
|
||||
| Community 목록 | 필수 `timezone`; `data`는 배열이고 pagination metadata 없음 | `audioUrl`을 사용하고 total/hasNext를 추정하지 않는다. |
|
||||
| Community 생성·수정 | 생성 part `postImage`/`audioFile`; 수정은 `postImage`만 교체 가능; mutation `data=null` | `image`, `audioSignedUrl`, 수정 audio/price field를 만들지 않는다. |
|
||||
| FanTalk | 목록 GET과 답변 POST만 제공 | 목록 item 기반 UI와 답변 생성만 구현하고 상세·수정·filter/sort는 외부 의존으로 둔다. |
|
||||
|
||||
### 11.5 백엔드 제공 대기 계약
|
||||
|
||||
아래 항목은 프론트엔드 인터뷰로 결정할 Open Question이 아니라 **외부 의존**이다. P0의 request/response/error 계약을 제공받기 전에는 해당 기능의 network integration을 구현하지 않는다. P1은 추정하지 않고 출시 전 계약과 검증을 맞춘다. UI shell과 계약에 의존하지 않는 표현 작업은 병행할 수 있다.
|
||||
아래 항목은 프론트엔드 인터뷰로 결정할 Open Question이 아니라 **외부
|
||||
의존**이다. P0 계약을 제공받기 전에는 영향을 받는 network integration을
|
||||
구현하지 않는다. UI shell과 계약에 의존하지 않는 표현 작업은 병행할 수
|
||||
있다. 단, `EXT-006`은 이미 구현·검증된 기존 인증 endpoint의 정식 문서화
|
||||
의존이므로 Phase 3~9 진행을 차단하지 않는다.
|
||||
|
||||
| 우선순위 | 백엔드 제공 필요 계약 | 프론트엔드 영향 |
|
||||
|---:|---|---|
|
||||
| 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 제공 대기 |
|
||||
| ID | 우선순위 | 백엔드 제공 필요 계약 | 프론트엔드 영향 |
|
||||
|---|---:|---|---|
|
||||
| EXT-001 | P0 | 원작 검색 lookup | Character 원작 선택기 network integration 대기 |
|
||||
| EXT-002 | P0 | 장르 검색 lookup | 유효한 `genreId` 선택이 필요한 Series 생성 integration 대기 |
|
||||
| EXT-003 | P0 | Series 수정 form 초기화용 edit DTO 또는 표시 문자열 mapping | 직접 링크에서 genreId·요일 enum·state enum을 안전하게 복원하는 수정 integration 대기 |
|
||||
| EXT-004 | P0 | FanTalk 상세·답변 수정·답변 상태 filter·sort·유일성 오류 | 목록 item 밖의 상세·수정, 전체 결과 filter와 동시 중복 답변 처리 대기 |
|
||||
| EXT-005 | P0 | 오디오·커뮤니티 댓글 CRUD와 팬 댓글 삭제 권한 오류 | 댓글·답글 연동과 권한별 오류 처리 대기 |
|
||||
| EXT-006 | P0 비차단 | 현재 구현된 `POST /admin/member/login`, `POST /member/logout`의 정식 OpenAPI 포함 또는 별도 버전 고정 계약 | 현재 구현과 Phase 1 회귀는 유지하며 Phase 3~9를 차단하지 않는다. 신규 인증 변경과 전체 계약의 단일 추적성만 정식 계약 제공 대기다. |
|
||||
| EXT-007 | P0 | Character 검색 결과와 Audio·Series·Community 목록의 active-only 반환 보장 | soft delete 뒤 비활성 항목이 서버 목록에서 제외된다는 수용 기준 검증 대기 |
|
||||
| EXT-008 | P1 | Community pagination의 total/hasNext 또는 종료 규칙 | 신뢰할 수 있는 전체 건수와 마지막 page UI 대기 |
|
||||
| EXT-009 | P1 | price 최대값 | 현재 0 이상 정수 규칙만 적용하며 상한 계약 제공 시 Audio·Community schema와 경계값 test 갱신 |
|
||||
| EXT-010 | P1 | 파일 크기·MIME·crop·container/codec의 backend 검증 계약 | PRD의 client 사전 검증은 유지하되 server와 동일 경계라는 완료 주장은 계약 제공 후 검증 |
|
||||
| EXT-011 | P0 | 도메인별 오류 | 기능별 정확한 비2xx status와 message key 분기가 필요한 흐름 대기 |
|
||||
|
||||
#### EXT-006 현재 구현 기준 계약
|
||||
|
||||
다음 내용은 현재 프론트엔드 adapter·contract test·E2E가 사용하는 기존
|
||||
인증 계약이다. 백엔드는 이를 정식 OpenAPI operation으로 옮기거나 별도
|
||||
버전 고정 계약으로 제공할 수 있다. 정식 계약이 제공되기 전에도 현재
|
||||
로그인·로그아웃 구현은 유지하며 Phase 3~9를 진행한다.
|
||||
|
||||
| 항목 | Admin Login | Member Logout |
|
||||
|---|---|---|
|
||||
| Method·Path | `POST /admin/member/login` | `POST /member/logout` |
|
||||
| `Accept-Language` | `ko` | `ko` |
|
||||
| `Content-Type` | `application/json` | body 없음 |
|
||||
| Authorization | 보내지 않음 | `Bearer {jwt-token}` 필수 |
|
||||
| Request body | `{ "email": string, "password": string }` | 없음 |
|
||||
| 성공 `data` | `{ "token": string, "role": "ADMIN" }` | `{}` |
|
||||
| 현재 client 처리 | 유효한 token·ADMIN role만 session으로 저장 | 성공·비2xx·network 오류 모두 local session 제거 후 `/login` 이동 |
|
||||
|
||||
성공 응답은 현재 다음 envelope를 사용한다.
|
||||
|
||||
```json
|
||||
{
|
||||
"success": true,
|
||||
"message": null,
|
||||
"data": {},
|
||||
"errorProperty": null
|
||||
}
|
||||
```
|
||||
|
||||
- 로그인 성공에서는 `data`가 `{ "token": "jwt-token", "role": "ADMIN" }`이다.
|
||||
- 로그아웃 성공에서는 `data`가 `{}`다.
|
||||
- 현재 contract fixture는 로그인 JSON binding 실패 `400`, 지원하지 않는 media type `415`, 로그아웃의 Bearer 없음·잘못됨·폐기 `401`, 비ADMIN `403`, body 전송 `400`을 검증한다.
|
||||
- 정확한 backend 오류 message key와 추가 status는 정식 계약 제공 시 확정한다. 제공 전에는 기존 공통 envelope와 서버 `message` 우선 표시를 유지한다.
|
||||
- 구현 근거는 `src/features/auth/api/auth-api.ts`, `src/features/auth/model/auth-session.tsx`, `src/features/auth/tests/auth-api.test.ts`, `src/features/auth/tests/auth-session.test.tsx`, `src/shared/mocks/__tests__/auth-handlers.test.ts`, `tests/e2e/auth.spec.ts`다.
|
||||
|
||||
## 12. 보안과 데이터 취급
|
||||
|
||||
@@ -686,7 +779,7 @@ Phase 2~6에서 추가되는 오류는 구현 전에 정확한 비2xx status와
|
||||
|
||||
## 13. 성능과 품질 요구사항
|
||||
|
||||
- 목록은 서버 페이지네이션을 사용하고 무제한 전체 로드를 피한다. 단, 시리즈 순서 변경 모드는 계약상 활성 시리즈 전체를 명시적으로 로드한다.
|
||||
- 목록은 계약에 있는 서버 page/size를 사용하고 무제한 전체 로드를 피한다. 시리즈 순서 변경 모드는 `totalCount`를 기준으로 모든 page를 읽는다. Community는 종료 metadata 계약이 제공되기 전까지 전체 건수·마지막 page를 표시하지 않는다.
|
||||
- 이미지에는 고정 aspect ratio와 크기를 예약해 layout shift를 줄이고 목록 이미지는 lazy load한다.
|
||||
- 필터 변경 중 기존 데이터를 유지해 화면 깜빡임을 줄인다.
|
||||
- 파일 업로드 외의 일반 mutation은 중복 제출을 막는다.
|
||||
@@ -694,7 +787,7 @@ Phase 2~6에서 추가되는 오류는 구현 전에 정확한 비2xx status와
|
||||
- 날짜, 가격, 상태 label은 중앙 formatter로 일관되게 표시한다.
|
||||
- 브라우저 지원 범위는 데스크톱 Chrome/Edge/Safari 최신 2개 주요 버전과 모바일 Chrome/Safari 최신 2개 주요 버전이다.
|
||||
- mock/server mode는 build-time 환경 설정으로 명시적으로 선택하며 runtime 404 fallback을 사용하지 않는다.
|
||||
- domain fixture와 browser handler는 `api-contract.md`의 제공 계약에서 파생하고 contract test와 함께 변경한다.
|
||||
- domain fixture와 browser handler는 `api-contract.openapi.json`의 제공 계약에서 파생하고 contract test와 함께 변경한다.
|
||||
|
||||
## 14. 성공 기준
|
||||
|
||||
@@ -705,12 +798,12 @@ Phase 2~6에서 추가되는 오류는 구현 전에 정확한 비2xx status와
|
||||
- 로그인 성공 후 JWT와 ADMIN role이 `sessionStorage`에만 저장되어 같은 탭의 새로고침에서 복원되고, `localStorage`, IndexedDB, cookie에는 기록되지 않으며 로그아웃과 401에서 제거된다.
|
||||
- 로그아웃 요청이 관리자 전용 경로가 아닌 `POST /member/logout`에 Bearer header와 body 없이 전송된다.
|
||||
- 로그아웃 API가 성공하거나 네트워크·비2xx 오류로 실패해도 로컬 session이 제거되고 로그인 화면으로 이동하며, 실패한 경우 경고가 표시되고 session이 복원되지 않는다.
|
||||
- 캐릭터 생성 요청에 `isActive`와 `externalCharacterId`가 포함되지 않는다.
|
||||
- 캐릭터 생성 multipart가 필수 `image`와 `request`를 보내고 request에 `name`, `systemPrompt`, `description`이 포함되며 `isActive`와 `externalCharacterId`는 포함되지 않는다.
|
||||
- Character, Audio, Series, Community의 일반 수정은 `isActive`를 생략하고 soft delete에만 `isActive=false`를 보내며 `true`는 전송하지 않는다.
|
||||
- Character, Audio, Series의 soft delete가 성공하면 해당 active-only 목록 cache를 갱신해 비활성화한 항목을 표시하지 않고 목록으로 이동하며 성공 알림을 표시한다. Community는 열린 Sheet를 닫고 현재 active-only 목록을 갱신해 해당 항목을 제거한다.
|
||||
- Character, Audio, Series의 soft delete가 성공하면 목록 cache를 무효화·재조회하고 목록으로 이동하며 성공 알림을 표시한다. Community는 열린 Sheet를 닫고 목록을 재조회한다. 서버 목록에서 비활성 항목이 제외되는지는 active-only 계약 제공 후 검증한다.
|
||||
- 캐릭터 수정이 creator profile 동기화 결과와 함께 다시 조회된다.
|
||||
- 워크스페이스 진입·복원용 상세 성공 응답으로 `isActive=false`인 캐릭터를 받은 경우 프론트엔드가 모든 하위 mutation 진입점을 차단한다. soft delete 성공 직후에는 active-only 목록 이동을 우선하고, 상세 요청의 비2xx 응답은 공통 오류 처리로 표시한다.
|
||||
- 오디오 생성에서 즉시 공개는 `releaseDateUtc=null`, 예약 공개는 미래 UTC 시각을 보낸다.
|
||||
- 워크스페이스 진입·복원용 상세 성공 응답으로 `isActive=false`인 캐릭터를 받은 경우 프론트엔드가 모든 하위 mutation 진입점을 차단한다. soft delete 성공 직후에는 목록 이동·재조회를 우선하고, 상세 요청의 비2xx 응답은 공통 오류 처리로 표시한다.
|
||||
- 오디오 생성에서 즉시 공개는 `releaseDate=null`, 예약 공개는 미래 Asia/Seoul 시각을 `yyyy-MM-dd HH:mm`로 보내고 `timezone="Asia/Seoul"`을 포함한다.
|
||||
- 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 검증을 통과해야 한다.
|
||||
@@ -722,15 +815,15 @@ Phase 2~6에서 추가되는 오류는 구현 전에 정확한 비2xx status와
|
||||
- 커뮤니티 GIF는 crop Dialog를 열지 않고 원본 비율과 animation을 유지해 등록하며, 원본 가로가 800px을 초과하면 제출 전에 거부한다.
|
||||
- crop Dialog에서 이동·확대/축소·초기화·미리보기·취소/적용을 keyboard와 pointer로 완료할 수 있다.
|
||||
- 오디오 목록·상세와 커뮤니티 목록 항목의 첨부 오디오가 재생된다.
|
||||
- 오디오 콘텐츠 생성 시 테마 목록을 query/body 없이 조회하고 선택한 `themeId`를 request JSON에 포함한다.
|
||||
- 오디오 콘텐츠 생성 시 테마 목록 `data[]`의 `id`, `theme`, `image`를 query/body 없이 조회하고 선택한 `id`를 request의 `themeId`에 포함한다.
|
||||
- media error를 signed URL 만료로 구분하지 않고 일반 재생 오류를 표시한다. 오류만으로 API를 자동 재조회하거나 자동 재생하지 않으며 사용자가 수동 재시도하거나 페이지를 새로고침할 수 있다.
|
||||
- 커뮤니티는 전용 상세 GET이나 상세·수정 route 없이 목록 응답 기반 Sheet에서 조회·수정·고정·비활성화·댓글 진입을 제공한다.
|
||||
- Series에 `OPEN`이나 `MONDAY` 같은 잘못된 값을 보내지 않는다.
|
||||
- Series 생성에는 state를 보내지 않고 수정 미선택 시 state를 생략한다.
|
||||
- Series 생성에는 state를 보내지 않고 `keyword` 문자열을 사용하며, 수정에서 state를 바꾸지 않으면 field를 생략한다.
|
||||
- RANDOM과 실제 요일을 동시에 선택할 수 없다.
|
||||
- FanTalk 답변이 있으면 두 번째 POST가 UI에서 차단되고 수정 동작만 제공되며, 직접·동시 요청도 백엔드가 원자적으로 거부한다.
|
||||
- 댓글은 2단계를 넘지 않고 작성자에 따른 수정·삭제 권한이 구분된다.
|
||||
- 모바일에서 조회·오디오 재생·댓글 관리·FanTalk 답변 작성/수정이 가능하다.
|
||||
- FanTalk 목록 item의 `creatorReplies`에 답변이 있으면 두 번째 POST를 UI에서 차단한다. 답변 수정·전체 결과 filter·직접 또는 동시 중복 요청 거부는 외부 계약이 제공된 뒤 수용 기준을 활성화한다.
|
||||
- 댓글 계약이 제공된 뒤 댓글은 2단계를 넘지 않고 작성자에 따른 수정·삭제 권한이 구분된다.
|
||||
- 모바일에서 조회·오디오 재생과 FanTalk 답변 작성이 가능하다. 댓글 관리와 FanTalk 답변 수정은 각 외부 계약 제공 후 같은 capability로 활성화한다.
|
||||
- `npm run dev:mock`에서 실제 backend 요청 없이 제공 계약 범위의 최종 UI happy path를 확인할 수 있고 mock mode 안내가 표시된다.
|
||||
- 기본 `npm run dev`에서는 실제 개발 API를 사용하며 404·network error가 mock 응답으로 바뀌지 않는다.
|
||||
- production build에는 browser mock이 활성화되지 않고 mock mode 설정을 허용하지 않는다.
|
||||
@@ -750,11 +843,11 @@ Phase 2~6에서 추가되는 오류는 구현 전에 정확한 비2xx status와
|
||||
- 모든 핵심 route의 axe 기반 자동 검사에서 critical·serious 접근성 위반이 0건이다.
|
||||
- `ui-ux-pro-max`의 loading, reduced motion, z-index, touch 검증 항목을 확인한다.
|
||||
|
||||
## 15. Open Questions
|
||||
## 15. Open Questions와 결정 절차
|
||||
|
||||
| ID | 상태 | 결정 필요 사항 | 현재 권고 |
|
||||
|---|---|---|---|
|
||||
| OQ-009 | 미결 | 각 페이지의 문자열 최대 길이와 배열 최대 개수 | 초기 UI를 만든 뒤 Character의 이름·설명, Audio의 제목·설명·`seriesIds`, Series의 제목·소개·요일·keywords·writer·studio·연결 `contentIds`·순서 `seriesIds`, Community 본문, FanTalk 답변과 댓글을 페이지별로 검토해 권고값을 작성한다. 백엔드 호환 확인 후 확정하며, 그 전에는 제공 계약에 없는 임의의 최대값을 추가하지 않는다. 확정 시 PRD·API Contract·schema·경계값 test를 함께 갱신한다. |
|
||||
| OQ-009 | 확정 | 각 페이지의 문자열 최대 길이와 배열 최대 개수 | 초기 UI를 구현한 뒤 실제 페이지에서 문자열 입력 공간과 반복 항목 사용성을 검토해 최대 길이·최대 개수의 권고값을 정한다. backend 호환 확인 후 PRD·OpenAPI 계약·schema·경계값 test를 같은 변경에서 갱신한다. 그전에는 계약에 없는 임의의 최대값을 추가하지 않는다. |
|
||||
| OQ-010 | 미결 | 감사 로그 UI 제공 여부 | backend 기록 우선, 조회 UI는 후속 범위 |
|
||||
|
||||
## 16. 결정 기록
|
||||
@@ -795,3 +888,9 @@ Phase 2~6에서 추가되는 오류는 구현 전에 정확한 비2xx status와
|
||||
| 2026-07-26 | 댓글 API·팬 댓글 삭제 권한 오류와 FanTalk 목록·상세·답변 수정·중복 답변 계약은 백엔드 제공 대기 사항으로 분류하고 프론트엔드 Open Questions에서 제외한다. |
|
||||
| 2026-07-27 | 오디오 콘텐츠 생성에는 `themeId`가 필수이며, 테마 선택지는 `GET /api/v2/admin/ai-characters/audio-content-themes`에서 query/body 없이 조회한다. |
|
||||
| 2026-07-27 | backend endpoint 구현 전에도 제공된 API Contract 범위의 최종 UI를 확인할 수 있도록 명시적 개발 전용 browser MSW mode를 제공한다. 실제 404 자동 fallback은 금지하고 mock UI 완료와 실제 server 연동 완료를 분리한다. |
|
||||
| 2026-07-28 | 삭제된 `api-contract.md`를 `api-contract.openapi.json`으로 대체하고, endpoint·query·multipart·request/response·오류는 OpenAPI를 단일 진실 원천으로 사용한다. OpenAPI에 없는 제품·UI 정책만 PRD가 소유한다. |
|
||||
| 2026-07-28 | 새 OpenAPI에 맞춰 Character 생성 image/systemPrompt 필수, Audio의 `search_word`·`contentFile`·`releaseDate`·테마 field, Series의 `keyword`·`contentIdList`·`ids`, Community의 `timezone`·`postImage`·`audioUrl`, FanTalk 목록 GET을 구현 기준으로 정정한다. |
|
||||
| 2026-07-28 | OpenAPI에 없는 인증 정식 명세, 원작·장르 lookup, Series 수정 form의 edit DTO, 목록 active-only 보장, FanTalk 상세·답변 수정·filter/sort·유일성 오류, 댓글 CRUD와 도메인별 오류 key는 외부 의존으로 관리하며 추정 구현하지 않는다. |
|
||||
| 2026-07-28 | OQ-009는 초기 UI를 먼저 구현하고 실제 페이지를 보며 문자열 최대 길이와 배열 최대 개수를 제안한 뒤 backend 호환을 확인해 확정하는 절차로 종결한다. 실제 최대값 확정 전에는 임의 상한을 추가하지 않는다. |
|
||||
| 2026-07-28 | `EXT-006`에 현재 구현된 `POST /admin/member/login`과 `POST /member/logout`의 request·response·인증·client 처리 기준을 기록한다. 정식 OpenAPI 포함은 비차단 외부 의존이며 기존 인증 구현과 Phase 3~9 진행을 유지한다. |
|
||||
| 2026-07-28 | Phase 3부터는 OpenAPI 제공 범위를 먼저 구현해 Phase 9 활성 범위 Gate까지 진행한다. 미제공 계약은 영향을 받는 기능만 후속 범위로 남기고, 계약 도착 후 별도 vertical slice와 관련 Phase Gate·Phase 9를 다시 실행한다. |
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# AI 캐릭터 관리자 웹 Phase 0·1 코드 리뷰·QA 리포트
|
||||
|
||||
> **계약 이력 안내 (2026-07-28):** 이 리뷰의 `api-contract.md`
|
||||
> section·line reference는 2026-07-27 당시 사용한 계약을 가리키는 역사
|
||||
> 기록이다. 현재 구현 기준 계약은
|
||||
> [api-contract.openapi.json](../api-contract.openapi.json)이며, 기존 판정과
|
||||
> line reference는 당시 검증 근거를 보존하기 위해 수정하지 않는다.
|
||||
|
||||
## 1. 리뷰 정보
|
||||
|
||||
| 항목 | 내용 |
|
||||
|
||||
@@ -1,5 +1,11 @@
|
||||
# AI 캐릭터 관리자 웹 Phase 2 코드 리뷰·QA 리포트
|
||||
|
||||
> **계약 이력 안내 (2026-07-28):** 이 리뷰의 `api-contract.md`
|
||||
> section reference는 2026-07-27 당시 사용한 계약을 가리키는 역사
|
||||
> 기록이다. 현재 구현 기준 계약은
|
||||
> [api-contract.openapi.json](../api-contract.openapi.json)이며, 기존 판정과
|
||||
> section reference는 당시 검증 근거를 보존하기 위해 수정하지 않는다.
|
||||
|
||||
## 1. 리뷰 정보
|
||||
|
||||
| 항목 | 내용 |
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
- PRD 문서와 구현 계획/TASK 문서는 `docs/[날짜]_구현할내용한글/` 아래에 함께 둔다.
|
||||
- 날짜는 `YYYYMMDD` 8자리 숫자를 사용한다.
|
||||
- PRD 문서 파일명은 `prd.md`, 구현 계획/TASK 문서 파일명은 `plan-task.md`를 사용한다.
|
||||
- 기능별 API Contract도 같은 디렉터리에 두고 PRD·계획에서 실제 파일명을 링크한다. 계약 형식은 Markdown에 한정하지 않으며 OpenAPI JSON이면 `<이름>.openapi.json`처럼 형식이 드러나는 파일명을 사용한다.
|
||||
- 기능별 설계 결정은 `prd.md`의 요구사항·결정 기록에, 실행 절차는 `plan-task.md`의 Task·검증 기록에 통합한다. 같은 기능을 위한 별도 spec/plan 디렉터리를 `docs/` 아래에 병렬로 만들지 않는다.
|
||||
- 공통 샘플 문서는 `docs/sample/`에 두며 기능별 문서 디렉터리에 복제하지 않는다.
|
||||
- PRD를 작성·변경할 때는 [PRD 작성 및 유지보수 규칙](./prd.md)과 [PRD 샘플](../sample/sample-prd.md)을 따른다.
|
||||
- 구현 항목은 기능/작업 단위로 분리해 체크박스(`- [ ]`) 목록으로 작성한다.
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
## 1. 적용 시점
|
||||
|
||||
- 사용자가 goal 기능으로 구현을 진행하거나, goal에 적합한 `plan-task.md` 작성·보완을 요청하면 이 문서를 따른다.
|
||||
- 구현 전에 [PRD 작성 및 유지보수 규칙](./prd.md), 대상 기능의 `prd.md`, `api-contract.md`, 기존 `plan-task.md`, 관련 코드·test와 저장소 가이드를 읽는다.
|
||||
- 구현 전에 [PRD 작성 및 유지보수 규칙](./prd.md), 대상 기능의 `prd.md`, 실제 API Contract 파일, 기존 `plan-task.md`, 관련 코드·test와 저장소 가이드를 읽는다.
|
||||
- 계획 작성 요청은 문서 변경 범위다. 사용자가 구현까지 요청하지 않았다면 애플리케이션 코드와 설정을 변경하지 않는다.
|
||||
|
||||
## 2. 기준 템플릿과 위치
|
||||
|
||||
@@ -27,7 +27,7 @@
|
||||
## 4. 문서 간 추적
|
||||
|
||||
- 제품 결정과 사용자 요구는 PRD가 소유한다.
|
||||
- request/response/error와 endpoint 세부 계약은 `api-contract.md`가 소유한다.
|
||||
- request/response/error와 endpoint 세부 계약은 대상 기능의 실제 API Contract 파일(Markdown 또는 OpenAPI)이 소유한다.
|
||||
- 구현 순서, Files, Interfaces, Task/Goal과 완료 증거는 `plan-task.md`가 소유한다.
|
||||
- 모든 확정 요구사항을 API Contract section 또는 명시적인 contract 불필요 판정, 하나 이상의 Phase/Goal, 자동·수동 검증으로 연결한다.
|
||||
- Open Question과 외부 의존을 혼합하지 않는다. 제품이 결정할 수 없는 backend 계약은 별도 외부 의존 ID로 관리한다.
|
||||
@@ -47,6 +47,6 @@
|
||||
- 사용자·권한·핵심 흐름과 라우팅 경계가 명확하다.
|
||||
- 모든 요구사항 ID가 고유하고 상태·수용 기준을 가진다.
|
||||
- 미결·외부 의존·제외 항목에 다음 행동과 담당 주체가 있다.
|
||||
- API endpoint와 payload 규칙이 `api-contract.md`로 추적된다.
|
||||
- API endpoint와 payload 규칙이 대상 API Contract 파일로 추적된다.
|
||||
- 기능·UX·보안·성능 성공 기준이 자동 또는 수동 검증 가능한 표현이다.
|
||||
- placeholder, 근거 없는 최대값과 추정 계약이 없다.
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
## 2. 기준 문서와 템플릿
|
||||
|
||||
- 리뷰 전에 대상 기능 디렉터리의 `prd.md`, `api-contract.md`, `plan-task.md`와 관련 구현·test를 읽는다.
|
||||
- 리뷰 전에 대상 기능 디렉터리의 `prd.md`, 실제 API Contract 파일, `plan-task.md`와 관련 구현·test를 읽는다.
|
||||
- 대상 `prd.md`와 `plan-task.md`가 있는 기능 문서 디렉터리 아래 `reviews/`를 만들고 모든 리뷰 문서를 그 안에 둔다.
|
||||
- 리뷰 문서를 기능 문서 디렉터리 바로 아래나 단수형 `review/`에 두지 않는다. 여러 Phase·Task 리뷰가 생겨도 같은 `reviews/`에 누적한다.
|
||||
- [코드 리뷰 보고서 샘플](../sample/sample-review.md)을 원본 템플릿으로 사용하고, section·필드·상태 의미를 임의로 축소하지 않는다.
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
| 상태 | 계획 작성 중 / 구현 중 / 외부 조건 대기 / 구현 완료 |
|
||||
| 작성일 | `YYYY-MM-DD` |
|
||||
| 요구사항 기준 | `<대상 prd.md 경로>` |
|
||||
| API 기준 | `<대상 api-contract.md 경로>` |
|
||||
| API 기준 | `<대상 API Contract 파일 경로>` |
|
||||
| 현재 Phase | `<Phase 번호와 이름>` |
|
||||
| 현재 활성 Goal | `<없음 또는 Goal ID>` |
|
||||
|
||||
@@ -182,7 +182,7 @@
|
||||
**Files:**
|
||||
|
||||
- Modify: `<대상 prd.md 경로>`
|
||||
- Modify: `<대상 api-contract.md 경로>`
|
||||
- Modify: `<대상 API Contract 파일 경로>`
|
||||
- Modify: `<대상 plan-task.md 경로>`
|
||||
- Test: 없음 — 이 Task의 산출물은 실행 코드가 아니라 확정된 계약과 구현 map이다.
|
||||
|
||||
@@ -205,8 +205,8 @@
|
||||
- **실행 명령:**
|
||||
|
||||
```bash
|
||||
! rg --pcre2 -n '^(?!\s*!?\s*rg\b).*(?:TBD|TODO|적절히 처리|나중에 구현|위와 동일)' <대상 prd.md 경로> <대상 api-contract.md 경로> <대상 plan-task.md 경로>
|
||||
git diff --check -- <대상 prd.md 경로> <대상 api-contract.md 경로> <대상 plan-task.md 경로>
|
||||
! rg --pcre2 -n '^(?!\s*!?\s*rg\b).*(?:TBD|TODO|적절히 처리|나중에 구현|위와 동일)' <대상 prd.md 경로> <대상 API Contract 파일 경로> <대상 plan-task.md 경로>
|
||||
git diff --check -- <대상 prd.md 경로> <대상 API Contract 파일 경로> <대상 plan-task.md 경로>
|
||||
```
|
||||
|
||||
- **기대 결과:** 두 명령 모두 출력 없이 `exit 0`; 모든 요구사항 ID와 계약 이름이 세 문서에서 일치한다.
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
| 최종 수정일 | `YYYY-MM-DD` |
|
||||
| 대상 제품 | `<제품 또는 기능 이름>` |
|
||||
| 작성자·결정권자 | `<이름 또는 역할>` |
|
||||
| 관련 API Contract | `<대상 api-contract.md 경로>` |
|
||||
| 관련 API Contract | `<대상 API Contract 파일 경로>` |
|
||||
| 관련 구현 계획 | `<대상 plan-task.md 경로>` |
|
||||
| 관련 review | `<없음 또는 review 문서 링크>` |
|
||||
|
||||
@@ -28,7 +28,7 @@
|
||||
### 문서 우선순위와 갱신 순서
|
||||
|
||||
1. 사용자·제품 결정은 이 PRD에 기록한다.
|
||||
2. request/response/error 계약은 `api-contract.md`에 정규화한다.
|
||||
2. request/response/error 계약은 프로젝트의 실제 API Contract 파일에 정규화한다.
|
||||
3. 구현 범위·순서·완료 증거는 `plan-task.md`에 반영한다.
|
||||
4. 요구사항이 바뀌면 Decision Log → 관련 요구사항·수용 기준 → API Contract → plan 순서로 갱신한다.
|
||||
5. 기존 결정과 검증 기록은 삭제하거나 덮어쓰지 않고 정정 기록을 누적한다.
|
||||
@@ -117,7 +117,7 @@ Non-Goal을 변경하려면 Decision Log와 `plan-task.md` 범위를 먼저 갱
|
||||
|
||||
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|
||||
|---|---|---|---|---|
|
||||
| `DOMAINA-001` | 확정 | `<사용자가 할 수 있어야 하는 동작>` | `<관찰 가능한 성공·오류 결과>` | `api-contract.md §<번호>`, `P1-T1` |
|
||||
| `DOMAINA-001` | 확정 | `<사용자가 할 수 있어야 하는 동작>` | `<관찰 가능한 성공·오류 결과>` | `<API Contract operation/section>`, `P1-T1` |
|
||||
| `DOMAINA-002` | 확정 | `<payload·상태·권한 불변식>` | `<보내야/보내지 말아야 할 값과 test>` | `P1-T2` |
|
||||
| `DOMAINA-003` | 외부 의존 | `<외부 제공이 필요한 계약>` | `<제공 전 network integration 0건>` | `EXT-001`, `P1-T1` |
|
||||
|
||||
@@ -125,14 +125,14 @@ Non-Goal을 변경하려면 Decision Log와 `plan-task.md` 범위를 먼저 갱
|
||||
|
||||
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|
||||
|---|---|---|---|---|
|
||||
| `DOMAINB-001` | 확정 | `<목록·상세·mutation 흐름>` | `<loading·empty·error·success 포함>` | `api-contract.md §<번호>`, `P2-T2` |
|
||||
| `DOMAINB-001` | 확정 | `<목록·상세·mutation 흐름>` | `<loading·empty·error·success 포함>` | `<API Contract operation/section>`, `P2-T2` |
|
||||
| `DOMAINB-002` | 미결 | `<제품 결정이 필요한 항목>` | `<확정 전 최대값·동작 추정 금지>` | `OQ-001` |
|
||||
|
||||
### 8.3 공통 파일·데이터 정책
|
||||
|
||||
| ID | 상태 | 요구사항 | 수용 기준 | 계약/Goal 연결 |
|
||||
|---|---|---|---|---|
|
||||
| `FILE-001` | 확정 | `<허용 확장자·MIME·크기>` | `<정확한 byte 경계 test>` | `api-contract.md §<번호>`, `<Goal ID>` |
|
||||
| `FILE-001` | 확정 | `<허용 확장자·MIME·크기>` | `<정확한 byte 경계 test>` | `<API Contract operation/section>`, `<Goal ID>` |
|
||||
| `DATA-001` | 확정 | `<날짜·가격·enum·pagination 규칙>` | `<formatter/schema/contract test>` | `<Goal ID>` |
|
||||
|
||||
## 9. 반응형 기능 범위
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
| 기준 commit 또는 working tree | `<commit SHA 또는 변경 상태>` |
|
||||
| 리뷰 일자 | `YYYY-MM-DD` |
|
||||
| 리뷰어 | `<이름 또는 agent>` |
|
||||
| 기준 문서 | `<대상 prd.md, api-contract.md, plan-task.md 경로>` |
|
||||
| 기준 문서 | `<대상 prd.md, API Contract 파일, plan-task.md 경로>` |
|
||||
| 리뷰 상태 | 진행 중 / 판정 완료 / 수정 검증 완료 |
|
||||
|
||||
## 2. 리뷰 목적과 범위
|
||||
@@ -94,7 +94,7 @@ Browser/viewport: <값>
|
||||
- **심각도:** `<Blocker | High | Medium | Low>`
|
||||
- **상태:** `<후보 | 확정 | 오탐 | 보류 | 수정 완료>`
|
||||
- **관련 요구사항:** `<요구사항 ID 또는 없음>`
|
||||
- **관련 계약:** `<api-contract.md section 또는 없음>`
|
||||
- **관련 계약:** `<API Contract operation/section 또는 없음>`
|
||||
- **소유 Task:** `<기존 Goal ID 또는 신규 회귀 Task>`
|
||||
|
||||
**관찰 내용**
|
||||
|
||||
Reference in New Issue
Block a user