docs(ai-character): OpenAPI 계약 반영

This commit is contained in:
Yu Sung
2026-07-28 03:33:16 +09:00
parent 2a5efb0f3e
commit dd30e36323
13 changed files with 1896 additions and 1164 deletions

View File

@@ -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에서 보완한다.