6079 lines
774 KiB
Markdown
6079 lines
774 KiB
Markdown
# AI 캐릭터 관리자 웹 구현 계획
|
||
|
||
> **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` (recommended) or `superpowers:executing-plans` to implement this plan Phase-by-Phase. 모든 구현 항목은 체크박스(`- [ ]`)로 추적한다.
|
||
|
||
**Goal:** ADMIN이 로그인한 뒤 AI 캐릭터를 선택하고, 선택한 캐릭터 문맥에서 Character·Audio·Series·Community·FanTalk·Comments를 안전하게 관리하는 독립 React SPA를 구현한다.
|
||
|
||
**Architecture:** 프로젝트 세팅과 공통 플랫폼·인증/인가를 먼저 완결한 뒤, 각 도메인을 API·상태·화면·오류·반응형·접근성·E2E까지 포함한 vertical slice로 구현한다. 모든 하위 리소스는 URL의 `characterId`를 기준으로 격리하고, 공통 API client가 envelope parsing, 인증 header, `Accept-Language: ko`, 401/403을 담당한다. 두 개 이상의 Phase에서 동일한 의미로 반복될 것이 확정된 UI·파일·미디어 컴포넌트는 Phase 1에서 먼저 만들고, 그 밖의 UI는 도메인 Phase 안에서 작게 나눈 뒤 실제 재사용 근거가 생길 때 shared로 올린다.
|
||
|
||
**Tech Stack:** React, TypeScript, Vite, Tailwind CSS, TanStack Query, Zod, native history routing, fetch/XHR upload adapter, Vitest, React Testing Library, MSW, Playwright, axe-core.
|
||
|
||
| 문서 항목 | 내용 |
|
||
|---|---|
|
||
| 상태 | 2026-08-04 `P6-R5` FileField 이미지 preview·Community 생성 media flow·공용 CAN 가격 계약 구현, 표준 배율 browser QA와 최종 review 완료. `P4-R10`, `P9-R18`~`P9-R19`, `P10-R16`~`P10-R17` 자동 보완 완료, 실제 crop pixel·stale ADMIN·Series/FanTalk/Comments/file policy 수동 QA 대기 |
|
||
| 최초 작성일 | 2026-07-25 |
|
||
| 재작성일 | 2026-07-26 |
|
||
| 요구사항 기준 | [prd.md](./prd.md) |
|
||
| API 기준 | [api-contract.openapi.json](./api-contract.openapi.json) |
|
||
| 현재 활성 Goal | `P6-R5` 완료 |
|
||
|
||
## 목표
|
||
|
||
ADMIN이 로그인한 뒤 AI 캐릭터를 선택하고, 선택한 캐릭터 문맥에서 Character·Audio·Series·Community·FanTalk·Comments를 안전하게 관리하는 독립 React SPA를 완성한다.
|
||
|
||
## 현재 상태
|
||
|
||
| 구분 | 현재 상태 | 남은 조건 |
|
||
|---|---|---|
|
||
| 자동 검증 범위 | `P4-R10`, `P9-R18`~`P9-R19`, `P10-R16`~`P10-R17` current-state contract 자동 보완 완료 | 실제 crop pixel·stale ADMIN·개발 API 수동 QA |
|
||
| 실제 개발 API | Series/FanTalk/Comments/file policy 수동 QA 대기 | ADMIN credential과 고정 fixture 필요 |
|
||
| 브라우저·인가 수동 QA | 실제 crop pixel 비교와 stale ADMIN server 확인 대기 | Chromium/mobile Chrome 환경과 stale ADMIN fixture 필요 |
|
||
| 문서 구조 | `P10-R9`에서 Goal 실행형 필수 section navigation 복구 | 기존 Progress·Decision 이력은 보존 |
|
||
|
||
## 범위의 포함·제외
|
||
|
||
### 포함
|
||
|
||
- AI 캐릭터 관리자 SPA의 인증, Character workspace, Audio, Series, Community, FanTalk, Comments mock/client 구현과 회귀 보완.
|
||
- OpenAPI 2.3.0에서 제공된 v2 lookup, UTC, pagination, FanTalk 수정·삭제, Comments, 가격·오류·파일 정책 반영.
|
||
- mock/client 자동 검증과 실제 개발 API 수동 QA 대기 상태의 분리 기록.
|
||
|
||
### 제외
|
||
|
||
- FanTalk 별도 상세/filter/sort, 중복 오류 key, 복원·hard delete, refresh token, 자동 mock fallback.
|
||
- 실제 개발 API 수동 QA 완료 주장. credential·fixture가 준비되기 전에는 완료로 표시하지 않는다.
|
||
- WebKit·Mobile Safari는 지원 범위에서 제외한다. E2E는 Chromium/mobile Chrome만 실행한다.
|
||
|
||
## 기술적 제약
|
||
|
||
- 현재 설치 stack은 `package.json` 기준 React, TypeScript, Vite, Tailwind CSS, TanStack Query, Zod, Vitest, React Testing Library, MSW, Playwright, axe-core다.
|
||
- routing은 native history 기반이고, transport는 fetch/XHR upload adapter를 사용한다.
|
||
- API 계약은 `api-contract.openapi.json`을 우선하며 제공되지 않은 endpoint, DTO, enum, 오류 status/key, validation 상한은 추정하지 않는다.
|
||
- JWT와 ADMIN role은 `sessionStorage`에만 저장하고 민감정보를 console, 분석 이벤트, 영구 저장소에 기록하지 않는다.
|
||
|
||
## 하나 이상의 Phase
|
||
|
||
실행 Phase는 [Phase 지도](#3-phase-지도)와 [Phase 0](#phase-0-프로젝트-세팅)부터 [Phase 10](#phase-10-openapi-230-후속-계약-vertical-slices)까지의 본문이 소유한다. 완료된 Phase는 다시 열지 않고 후속 문제는 `P<Phase>-R<번호>` 회귀 Task로 추적한다.
|
||
|
||
## 실행 순서와 의존성
|
||
|
||
- 기본 순서는 Phase 0 → 1 → 2 → 3 → 4 → 5/6/7 → 8 → 9 → 10이다.
|
||
- 자동 보완 Task는 완료됐다. 이후 남은 수동 QA와 필요 시 E2E는 Chromium/mobile Chrome만 실행한다.
|
||
- 동시에 하나의 미완료 goal만 운용하고, 완료된 Task의 체크박스와 검증 기록은 되돌리지 않는다.
|
||
|
||
## 변경 금지 항목
|
||
|
||
- OpenAPI에 없는 계약 추정, mock 성공을 server 완료로 대체, 실제 server QA 완료 허위 표시.
|
||
- 실패 test 삭제·skip·timeout 상향·근거 없는 browser project 제거로 Gate를 통과시키는 변경.
|
||
- 제품 범위 밖 기능 추가, 새 dependency 설치, 과거 Progress·Decision Log 삭제 또는 덮어쓰기.
|
||
|
||
## 의사결정 및 중단 규칙
|
||
|
||
- 외부 계약이나 권한이 필요한 범위는 담당 주체, 영향, 재개 조건을 기록하고 수동 QA 대기로 유지한다.
|
||
- 같은 차단 사유가 3회 반복되고 문서화 외 진전이 불가능할 때만 blocked로 판정한다.
|
||
- 사용자 직접 지시는 현재 검증 범위에 우선한다. 2026-07-31 이후 지원·자동 검증 browser는 Chromium/mobile Chrome만 사용한다.
|
||
|
||
## Progress
|
||
|
||
상세 진행 기록은 [검증 기록](#7-검증-기록)에 누적한다. `P4-R10`은 가격 기본값 0, native number stepper, 조건부 유료 옵션과 preview duration offset을 자동 검증 완료했다. `P9-R18`~`P9-R19`, `P10-R16`~`P10-R17`에서 finding·Task·H2·실제 마지막 Progress contract를 보완해 자동 보완 완료로 판정했다. 실제 crop pixel·stale ADMIN·Series/FanTalk/Comments/file policy 수동 QA는 별도 대기다.
|
||
|
||
## Decision Log
|
||
|
||
주요 결정은 [Phase 운영 규칙](#2-phase-운영-규칙), [Phase 3~10 OpenAPI 준비 상태](#31-phase-310-openapi-준비-상태), [요구사항 추적표](#5-요구사항-추적표), [검증 기록](#7-검증-기록)에 보존한다. 과거 결정은 삭제하지 않고 후속 정정만 누적한다.
|
||
|
||
## 발견된 문제
|
||
|
||
제품·Playwright 설정의 신규 문제는 없다. `P4-R10`은 2026-08-04에 확정된 Audio form UX 정책을 현재 구현과 정렬했고, 가격 native stepper와 `0..99999` 검증을 Chromium/mobile Chrome mock E2E로 확인했다. `P9-R18`, `P9-R19`, `P10-R16`, `P10-R17`로 finding·Task 경계, fenced·중복 H2, 최신 및 중복 Progress marker contract를 보완했다. 실제 crop pixel·stale ADMIN과 개발 API Series/FanTalk/Comments/file policy 수동 QA가 남아 있다.
|
||
|
||
## 최종 보고 형식
|
||
|
||
최종 보고는 한국어로 작성하고, 완료한 Goal ID, 변경 파일, 실행한 검증 명령과 결과, 실행하지 않은 E2E/수동 QA의 사유, 남은 위험을 구분해 요약한다.
|
||
|
||
## 1. 전역 제약
|
||
|
||
- 계획 보완 단계에서는 PRD·API Contract·plan과 연결 가이드만 동기화하고, 애플리케이션 코드와 프로젝트 설정은 해당 Task goal 실행 때 변경한다.
|
||
- 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 목록은 서버가 `isActive=true`인 항목만 반환한다. 활성 상태 query와 client-side 활성 필터를 추가하지 않고 서버 반환값을 그대로 사용한다.
|
||
- Character·Audio·Series soft delete 성공 후 해당 목록으로 이동한다. Community는 열린 Sheet를 닫고 현재 목록에서 제거한다. 모두 성공 알림을 표시한다.
|
||
- 워크스페이스 상세 성공 응답의 Character가 `isActive=false`이면 모든 하위 mutation 진입점을 차단한다. soft delete 직후에는 목록 이동을 우선한다.
|
||
- Series 생성 payload에는 `state`를 넣지 않는다. 수정에서 state를 바꾸지 않으면 key를 생략한다.
|
||
- Series state에 `OPEN`, 요일에 `MONDAY` 같은 보정 전 enum을 사용하지 않는다.
|
||
- AI 캐릭터 관리자 API에는 `timezone` query/body field를 보내지 않는다. 오디오 예약 공개의 Asia/Seoul 입력은 client에서 ISO-8601 UTC `Z`로 변환하고 즉시 공개는 `releaseDate=null`을 보낸다.
|
||
- Audio 생성·수정과 Community 생성의 `price`는 `0..99999` 정수만 허용하고 schema·form에서 `-1`, `100000`, 소수를 거부한다.
|
||
- multipart의 JSON part 이름은 `request`로 고정하고, optional 교체 파일을 보내지 않으면 기존 media를 유지한다.
|
||
- 목록은 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`이다.
|
||
- JWT, password, signed URL, 업로드 파일 본문을 console, 분석 이벤트, 오류 리포트, 영구 저장소에 기록하지 않는다.
|
||
- 제공 계약에 없는 문자열·배열 최대값, price 상한, 오류 status/key를 추정하지 않는다.
|
||
- 새 dependency는 해당 Phase에서 실제로 필요한 최소 항목만 추가한다. shadcn component와 폴더도 소비 시점에 생성한다.
|
||
- Page는 routing·query·permission·component 조합만 담당한다. 도메인 표시와 상호작용은 feature component로, 두 Phase 이상에서 의미와 동작이 같은 것은 shared component로 분리한다.
|
||
- 단순 markup 한 조각, 한 화면 전용 UI, 서로 다른 도메인 규칙을 하나의 범용 prop API로 합치기 위한 component는 만들지 않는다.
|
||
- 모든 기능은 실패하는 test를 먼저 만들고 최소 구현으로 통과시킨다.
|
||
- API mode는 `server | mock`을 명시적으로 선택한다. 기본 `npm run dev`는 실제 개발 API, `npm run dev:mock`은 browser MSW를 사용한다.
|
||
- 실제 API의 404·network error를 감지한 자동 mock fallback은 만들지 않고 production build에서는 mock mode를 거부한다.
|
||
- 제공 계약이 있는 기능만 contract 기반 browser fixture를 만들며, 계약 자체가 없으면 최종 network UI를 추정 구현하지 않는다.
|
||
- 각 도메인은 `UI 확인 완료(mock)`와 `실제 서버 연동 완료(server)`를 별도 증거로 기록하며 mock Gate만으로 Phase 전체 완료를 주장하지 않는다.
|
||
|
||
## 2. Phase 운영 규칙
|
||
|
||
### 2.1 독립 검증 규칙
|
||
|
||
각 도메인 Phase는 다음 결과를 모두 가진다.
|
||
|
||
1. 사용자가 직접 확인할 수 있는 하나 이상의 완결된 흐름
|
||
2. 해당 Phase가 소유하는 API contract test
|
||
3. loading·empty·error·success 상태
|
||
4. 해당 viewport 범위와 keyboard·접근성 검증
|
||
5. Phase 전용 E2E와 공통 typecheck·lint·build 결과
|
||
6. 제공 계약 범위의 mock preview E2E와 backend 구현 후 server integration 결과를 구분한 기록
|
||
|
||
각 Task는 다음 Red/Green loop를 따른다.
|
||
|
||
1. Task에 적힌 test file에 가장 작은 실패 test를 추가한다.
|
||
2. 해당 test만 실행해 의도한 assertion 실패인지 확인한다.
|
||
3. 그 test를 통과시키는 최소 구현을 작성한다.
|
||
4. 관련 feature test 전체를 실행한다.
|
||
5. refactor 후 typecheck·lint와 Phase E2E를 다시 실행한다.
|
||
|
||
import 오류, test 환경 오류, 임시 mock 누락 같은 우발적 실패는 Red 증거로 인정하지 않는다.
|
||
|
||
### 2.2 컴포넌트 조합 규칙
|
||
|
||
각 도메인 Phase는 코드를 작성하기 전에 다음 component map을 해당 Phase의 `주요 Files`와 checklist에 반영한다.
|
||
|
||
| 분류 | 책임 | 예시 |
|
||
|---|---|---|
|
||
| Page | route param/query, data loading, permission, navigation, component 조합 | `CharacterListPage`, `AudioContentDetailPage` |
|
||
| Feature component | 한 도메인의 표시·입력·상호작용 규칙 | `CharacterForm`, `PublishedDaysField` |
|
||
| Shared component | 두 개 이상 Phase에서 같은 의미·동작으로 재사용 | `PageState`, `ConfirmDeactivateDialog`, `ImageCropDialog` |
|
||
| shadcn primitive | 접근 가능한 저수준 control | `Button`, `Dialog`, `Table`, `Form` |
|
||
|
||
- 화면별 loading·empty·error·success·read-only·mobile 상태와 주요 action을 먼저 inventory한다.
|
||
- Page에 큰 JSX와 form/media 로직을 직접 쌓지 않고, 독립 test가 가능한 feature/shared component를 조합한다.
|
||
- 기존 shadcn/shared component로 표현할 수 있으면 새 wrapper를 만들지 않는다.
|
||
- shared component는 domain DTO나 endpoint를 import하지 않고 controlled value, slot, callback으로 조합한다.
|
||
- 재사용을 위해 boolean prop를 계속 늘리기보다 작은 component와 composition을 사용한다.
|
||
- component map과 실제 화면 구성이 달라지면 구현 전에 이 계획의 `주요 Files`를 먼저 갱신한다.
|
||
|
||
### 2.3 미결·외부 의존 처리
|
||
|
||
전역 Backend Contract Phase를 만들지 않는다. 각 도메인 Phase의 첫 Task에서 그 도메인에 필요한 계약만 확인한다.
|
||
|
||
| 분류 | 처리 규칙 |
|
||
|---|---|
|
||
| 계약이 제공됨 | `api-contract.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`의 결정 절차는 확정됐다. 각 도메인의 초기 UI를 만든 뒤 실제 페이지에서 문자열 입력 공간과 반복 항목 사용성을 검토하고 최대값 권고안을 정한다. backend 호환 확인 후 PRD·OpenAPI 계약·schema·경계 test를 같은 변경에서 갱신하며, 그전에는 임의 상한을 추가하지 않는다.
|
||
- `OQ-010` 감사 로그 조회 UI는 현재 릴리스 구현 항목을 만들지 않는다. 포함하기로 바뀌면 backend 조회 계약을 포함한 별도 Phase로 다시 계획한다.
|
||
- P0 외부 의존이 남아 있으면 영향을 받는 network flow를 완료로 표시하지 않는다. 다른 독립 Phase는 계속 진행할 수 있다.
|
||
- 이미지 최대 `10MB`의 정확한 byte 경계처럼 표현만으로 단일 값이 정해지지 않는 항목은 첫 파일 Phase에서 결정 기록과 contract를 먼저 보정한다.
|
||
|
||
### 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. 2026-07-28 당시 Phase 8 Comments 계약 부재는 `P8-T1`과 `P8-GATE` 이력으로 보존한다. 2026-07-29 OpenAPI 2.3.0의 댓글 계약 도착 후 구현은 Phase 10에서 새 goal로 진행한다.
|
||
6. Phase 9는 제공 계약과 명시적 제외 범위만 대상으로 실행하고 결과를 `활성 범위 완료`로 기록한다. 미제공 계약을 포함한 `전체 기능 완료`로 표현하지 않는다.
|
||
7. backend 계약이 후속 도착하면 완료 Phase를 묵시적으로 다시 열지 않고 별도 vertical slice를 계획해 구현한 뒤 영향받는 회귀와 최종 Gate를 다시 실행한다. OpenAPI 2.3.0 후속 범위는 Phase 10이 소유한다.
|
||
|
||
이 전략은 추가 사용자 결정을 기다리기 위한 임시 우회가 아니라, 제공
|
||
범위의 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 결정 기록 → 제공된 OpenAPI 또는 외부 의존 상태 → 이 계획의 활성 checklist 순서로 갱신해야 한다. 이 문서화가 끝나기 전에는 goal을 완료 처리하지 않는다.
|
||
- 각 Phase는 자신의 Task goal을 번호 순서로 완료한 뒤 Phase Gate를 마지막 goal로 실행한다. Gate goal은 실제 명령 결과와 수동 검증 결과를 기록한 뒤 완료한다.
|
||
- Phase 0~1처럼 이미 완료 체크된 범위는 새 구현 goal로 다시 만들지 않는다. 회귀나 기록 정합성 보정이 필요하면 별도 수정 goal을 만들고 기존 검증 기록을 덮어쓰지 않는다.
|
||
|
||
Goal objective 권장 형식:
|
||
|
||
```text
|
||
[Goal ID]의 목표를 plan-task.md에 정의된 범위로 수행한다.
|
||
시작 조건과 전역 제약을 확인하고, 체크박스를 Red/Green loop로 완료한다.
|
||
완료 증거의 검증과 문서 기록까지 끝나기 전에는 complete로 표시하지 않는다.
|
||
범위 밖 항목은 구현하지 않으며, 외부 계약은 추정하지 않는다.
|
||
```
|
||
|
||
## 3. Phase 지도
|
||
|
||
| Phase | 결과 | 선행조건 | 독립 검증 핵심 |
|
||
|---:|---|---|---|
|
||
| 0 | 프로젝트 세팅 | 없음 | fresh install, root smoke, unit/E2E/build |
|
||
| 1 | 공통 플랫폼·인증/인가·컴포넌트 기반 | Phase 0 | shared component contract + login → protected shell → refresh restore → logout/401/403 |
|
||
| 2 | 개발 전용 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/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 → reply create/update, fan root delete; 별도 detail/filter/sort는 제품 범위 제외 |
|
||
| 8 | Comments vertical slice | Phase 4 + Phase 6 | Audio/Community thread → permission별 CRUD |
|
||
| 9 | 교차 회귀·인수인계 | 활성 범위의 Phase 0~8 | 전체 journey, viewport, axe, security, mock/server build |
|
||
| 10 | OpenAPI 2.3.0 후속 계약 vertical slices | Phase 9 이력 + OpenAPI 2.3.0 | v2 lookup, UTC, pagination, FanTalk 수정·삭제, Comments, 가격·오류·파일 정책, active-only server 검증 |
|
||
|
||
기본 진행 순서는 Phase 번호를 따른다. 다만 Phase 5·6·7은 자신의 선행조건과 계약이 충족되면 병행할 수 있고, 외부 계약으로 막힌 Phase가 다른 독립 Phase를 막지 않는다.
|
||
|
||
### 3.1 Phase 3~10 OpenAPI 준비 상태
|
||
|
||
| Phase | 현재 구현 가능 범위 | 외부 의존 또는 제외 범위 | 판정 |
|
||
|---:|---|---|---|
|
||
| 3 Character | 기존 목록·상세·기본 create/update/soft delete mock UI 완료 | 없음. v2 원작 검색 선택기는 Phase 10에서 구현 완료 | `EXT-001` 해결, legacy 후보 제거 완료 |
|
||
| 4 Audio | 목록·상세·create/update/soft delete mock UI와 UTC `releaseDate` 전송 완료 | status filter와 수정 schedule/theme/file은 계약상 제외 | `EXT-007` 해결, 실제 개발 API 수동 QA 대기 |
|
||
| 5 Series | 목록·상세·연결·해제·전체 순서, v2 활성 장르, CRUD form mock/client 완료 | 실제 개발 API Series 수동 QA | `EXT-002` 해결, 상세는 `SeriesListItem`으로 확정 |
|
||
| 6 Community | 목록·Sheet·create/update/pin/soft delete, pagination object, `timezone` 제거 mock/client 완료 | 실제 개발 API Community 수동 QA | `EXT-007~008` 해결, active-only 반환 확인 대기 |
|
||
| 7 FanTalk | page 목록, 답변 생성·수정, 팬 원글 삭제 mock/client UI 완료 | 별도 상세/filter/sort·중복 오류 key는 현재 제품 범위에서 제외 | `EXT-004` 해결, 실제 개발 API 수동 QA만 분리 대기 |
|
||
| 8 Comments | Audio·Community 댓글 2단계 thread와 mock/client UI 완료 | 실제 개발 API Comments 수동 QA | `EXT-005` 해결, Phase 10 network slice 구현 완료 |
|
||
| 9 Final | 제공 계약과 명시적 제외 범위의 교차 회귀 | 미제공 P0 범위를 포함한 전체 릴리스 완료 주장 | 활성 범위 Gate 후 가능 |
|
||
| 10 Follow-up | 원작·장르 lookup, UTC, Community pagination, FanTalk 수정·삭제, Comments, 가격·오류·파일 정책 | 제품 범위에서 제외한 FanTalk 별도 상세/filter/sort·중복 오류 key와 인증 정식화 | 현재 확정 범위의 client/mock/server 구현 가능 |
|
||
|
||
OQ-009를 포함한 프론트엔드 제품 결정 절차는 확정됐다. 위 표의 대기
|
||
사항은 추가 사용자 결정이 아니라 backend OpenAPI 보완 또는 명시적
|
||
후속/제외 기록이 필요한 외부 의존이다.
|
||
|
||
```text
|
||
Phase 0 Setup
|
||
└─ Phase 1 Platform + Auth/Authz + Shared Components
|
||
└─ Phase 2 Mock Preview Foundation
|
||
└─ Phase 3 Character Workspace
|
||
├─ Phase 4 Audio ──┬─ Phase 5 Series
|
||
│ └─ Phase 6 Community ──┐
|
||
└─ Phase 7 FanTalk ├─ Phase 9 Final
|
||
Phase 4 + Phase 6 ── Phase 8 Comments ┘
|
||
└─ Phase 10 OpenAPI 2.3.0 Follow-up
|
||
```
|
||
|
||
## 4. 파일 책임 지도
|
||
|
||
```text
|
||
src/
|
||
app/ # entry, providers, router, route constants
|
||
components/ui/ # 실제로 추가한 shadcn primitive만 보관
|
||
features/
|
||
auth/
|
||
characters/
|
||
audio-contents/
|
||
series/
|
||
community-posts/
|
||
fan-talks/
|
||
comments/
|
||
layouts/ # Admin shell, Character workspace
|
||
shared/
|
||
api/ # envelope, client, query, pagination, errors
|
||
config/ # runtime env
|
||
hooks/ # 공통 query/form interaction hook
|
||
lib/ # 공통 formatter/media helper
|
||
test/ # MSW, render helper, fixtures
|
||
ui/ # Phase 1에서 확정한 공통 UI와 후속 검증된 추출물
|
||
validation/ # 공통 file/media policy
|
||
styles/
|
||
tests/e2e/
|
||
```
|
||
|
||
feature 내부의 `api/`, `components/`, `model/`, `pages/`, `schemas/`, `tests/`는 실제 파일이 생길 때만 만든다. 한 구현만을 위한 interface, factory, registry는 만들지 않는다. 모든 Page는 해당 feature의 component와 shared component를 조합해 구성한다.
|
||
|
||
---
|
||
|
||
## Phase 0. 프로젝트 세팅
|
||
|
||
**목표:** 비즈니스 기능 없이도 동일한 명령으로 개발·test·build할 수 있는 React SPA 기반을 만든다.
|
||
|
||
**Phase Goal `P0`:** Task 0.1 → 0.2 → Phase 0 Gate 순서로 재현 가능한 SPA 기반을 완성한다.
|
||
|
||
- **시작 조건:** 없음.
|
||
- **완료 조건:** `P0-T1`, `P0-T2`, `P0-GATE` 완료와 fresh install 검증 기록.
|
||
- **현재 상태:** 완료. 재검증은 기존 goal을 다시 열지 않고 별도 회귀 수정 goal로 수행한다.
|
||
|
||
**주요 Files:**
|
||
|
||
- Create: `package.json`, `package-lock.json`
|
||
- Create: `index.html`, `vite.config.ts`
|
||
- Create: `tsconfig.json`, `tsconfig.app.json`, `tsconfig.node.json`
|
||
- Create: `eslint.config.js`
|
||
- Create: `.env.example`, `README.md`
|
||
- Create: `playwright.config.ts`
|
||
- Create: `src/main.tsx`, `src/app/App.tsx`, `src/app/App.test.tsx`
|
||
- Create: `src/shared/config/env.ts`, `src/shared/config/env.test.ts`
|
||
- Create: `src/shared/test/setup.ts`
|
||
- Create: `tests/e2e/smoke.spec.ts`
|
||
|
||
### Task 0.1 런타임·패키지 기반
|
||
|
||
**Goal 실행 `P0-T1`:** Node/npm, Vite/TypeScript 진입점, runtime env와 최소 script를 재현 가능하게 구성한다.
|
||
|
||
- **시작 조건:** 지원 Node 버전과 환경 변수 기준 확인.
|
||
- **완료 증거:** 체크박스 전체, env/App focused test, typecheck·lint·개발/프로덕션 build 및 검증 기록.
|
||
- **범위 밖:** router, server-state, form, drag-and-drop, 도메인 기능.
|
||
|
||
- [x] `mise.toml`의 Node `24.12.0`을 기준으로 npm package와 lockfile을 생성한다.
|
||
- [x] React + TypeScript + Vite 진입점과 `@` path alias를 구성한다.
|
||
- [x] `VITE_API_BASE_URL`만 `.env.example`에 문서화하고 token·password 같은 비밀값을 넣지 않는다.
|
||
- [x] runtime env 누락·잘못된 URL을 앱 시작 전에 설명 가능한 오류로 차단하는 test를 작성한다.
|
||
- [x] unit test는 `vi.stubEnv`, Playwright webServer는 명시적 test URL로 `VITE_API_BASE_URL`을 주입해 `.env.example` 자동 로드를 전제하지 않는다.
|
||
- [x] `dev`, `build`, `typecheck`, `lint`, `test`, `test:run`, `e2e` script를 정의한다.
|
||
- [x] 이 Phase에 필요하지 않은 router, server-state, form, drag-and-drop dependency는 아직 설치하지 않는다.
|
||
|
||
### Task 0.2 test 기반
|
||
|
||
**Goal 실행 `P0-T2`:** unit·DOM·E2E test 기반과 접근 가능한 최소 App shell을 완성한다.
|
||
|
||
- **시작 조건:** `P0-T1` 완료.
|
||
- **완료 증거:** 체크박스 전체, root unit test와 현재 Playwright project smoke 및 검증 기록.
|
||
- **범위 밖:** 인증과 실제 도메인 route.
|
||
|
||
- [x] Vitest, jsdom, React Testing Library, jest-dom을 구성한다.
|
||
- [x] 각 test 뒤 DOM·mock·storage가 정리되는 공통 setup을 만든다.
|
||
- [x] Playwright에 desktop Chromium과 mobile Chrome viewport project, Vite webServer를 구성한다.
|
||
- [x] fresh environment에서 Chromium browser binary를 설치하는 명령을 README와 Gate에 포함한다.
|
||
- [x] `<html lang="ko">`, `main` landmark, root content를 확인하는 unit test를 먼저 실패시킨 뒤 최소 App shell을 만든다.
|
||
- [x] 동일 shell이 각 Playwright project에서 열리는 smoke E2E를 만든다.
|
||
|
||
### Phase 0 Gate
|
||
|
||
**Goal 실행 `P0-GATE`:** Phase 0의 fresh install·unit·E2E·build 재현성을 최종 판정한다.
|
||
|
||
- **시작 조건:** `P0-T1`, `P0-T2` 완료.
|
||
- **완료 증거:** 아래 명령 모두 0 exit code, Expected와 실제 결과의 검증 기록.
|
||
- **범위 밖:** 실패와 무관한 기능 추가. 실패는 소유 Task 수정 goal로 되돌린다.
|
||
|
||
```bash
|
||
npm ci
|
||
npx playwright install chromium
|
||
npm run typecheck
|
||
npm run lint
|
||
npm run test:run -- src/app/App.test.tsx src/shared/config/env.test.ts
|
||
npm run e2e -- tests/e2e/smoke.spec.ts
|
||
npm run build:dev
|
||
npm run build:prod
|
||
```
|
||
|
||
**Expected:** fresh install 후 typecheck·lint·unit·E2E·production build가 모두 0 exit code이며 환경 변수 오류가 test로 고정된다.
|
||
|
||
**1차 구현 검증 기록 (2026-07-26):**
|
||
|
||
- 무엇을: Phase 0의 React + TypeScript + Vite 프로젝트 기반, runtime env 검증, Vitest/RTL/jsdom, Playwright Chromium/WebKit 및 mobile viewport smoke, README와 npm scripts를 구현했다.
|
||
- 왜: 후속 Phase가 동일한 명령으로 개발·test·build할 수 있고, `VITE_API_BASE_URL` 누락/오류와 root shell 회귀를 자동 검증하기 위해서다.
|
||
- 어떻게: `npm ci` 성공(0 vulnerabilities), `npx playwright install chromium webkit` 성공, `VITE_API_BASE_URL=http://127.0.0.1:4010 npm run typecheck` 성공, `VITE_API_BASE_URL=http://127.0.0.1:4010 npm run lint` 성공, `VITE_API_BASE_URL=http://127.0.0.1:4010 npm run test:run -- src/app/App.test.tsx src/shared/config/env.test.ts` 성공(2 files/4 tests passed), `VITE_API_BASE_URL=http://127.0.0.1:4010 npm run e2e -- tests/e2e/smoke.spec.ts` 성공(4 projects passed), `VITE_API_BASE_URL=http://127.0.0.1:4010 npm run build` 성공.
|
||
- 정정/참고: `@playwright/test@1.62.0`의 WebKit binary가 현재 macOS 26.0 x86_64 환경에서 시작 전 `Segmentation fault: 11`로 실패해, Phase 0 Gate 충족을 위해 `@playwright/test@1.61.1`로 고정했다.
|
||
|
||
**2차 설정 정정 기록 (2026-07-26):**
|
||
|
||
- 무엇을: 개발 서버 포트를 `8888`로 고정하고, 개발/프로덕션 API base URL과 build 명령을 분리했다.
|
||
- 왜: 실행 환경별 API endpoint를 명확히 분리하고, AGENTS.md를 짧게 유지하면서 상세 지침을 `docs/agent-guide/`의 세부 문서에서 관리하기 위해서다.
|
||
- 어떻게: `npm run typecheck`, `npm run lint`, `npm run test:run -- src/app/App.test.tsx src/shared/config/env.test.ts`, `npm run e2e -- tests/e2e/smoke.spec.ts`, `npm run build:dev`, `npm run build:prod`가 성공했다.
|
||
- 정정: 기존 1차 기록의 `VITE_API_BASE_URL=http://127.0.0.1:4010`과 단일 `npm run build`는 당시 실행 기록으로 보존하되, 현재 Gate 기준은 `.env.development`, `.env.production`, `build:dev`, `build:prod`를 사용한다.
|
||
|
||
### Task R0.1 — 테스트 격리와 Phase 0 증거 정합성 복구
|
||
|
||
**Goal 실행 `P0-R1`:** Phase 0 테스트 격리와 루트 셸·Playwright 증거 정합성을 복구하고 회귀를 방지한다.
|
||
|
||
- 연결 리뷰: [Phase 0·1 리뷰](./reviews/review-phase-0-1.md) — `REV-P0-001`, `REV-P0-002`, `REV-P0-003`
|
||
- 시작 조건: review finding을 보존하고 기존 완료 체크를 되돌리지 않는다.
|
||
- 완료 증거: stubbed global 복원 회귀 test, root `main`과 대표 content assertion, 현재 Playwright mode URL 전략과 실행 가능한 agent-guide 문서 경로 정정 기록, P0-GATE 통과.
|
||
- 범위 밖: Phase 1 인증 동작 전면 변경, test framework 교체.
|
||
|
||
- [x] `src/shared/test/setup-isolation.test.ts`로 `vi.stubGlobal` 누출을 실패 재현한 뒤 `restoreAllMocks`·`unstubAllGlobals` 정리를 추가했다.
|
||
- [x] `src/app/App.test.tsx`와 `tests/e2e/smoke.spec.ts`에서 root `main` 안의 로그인 대표 콘텐츠를 검증한다.
|
||
- [x] 존재하지 않는 `docs/agent-guide/README.md` 참조를 실제 `docs/agent-guide/` 세부 문서 관리 방식으로 정정했다.
|
||
- [x] P0-GATE를 재실행하고 결과를 아래에 누적했다.
|
||
|
||
**P0-R1 수정 검증 기록 (2026-07-27):**
|
||
|
||
- RED: `npm run test:run -- src/app/App.test.tsx src/features/auth/tests/login-page.test.tsx src/features/auth/tests/auth-session.test.tsx src/shared/lib/crop-image.test.ts src/shared/validation/file-media-policy.test.ts src/shared/ui/__tests__/file-field.test.tsx src/shared/ui/__tests__/admin-audio-player.test.tsx src/shared/ui/__tests__/confirm-deactivate-dialog.test.tsx src/shared/ui/__tests__/unsaved-changes-guard.test.tsx src/shared/ui/__tests__/image-crop-dialog.test.tsx src/shared/test/setup-isolation.test.ts` — 기대 실패 확인, 11 files failed / 13 failed / 40 passed. P0-R1 관련으로 stubbed global 복원과 root shell assertion 실패를 확인했다.
|
||
- GREEN focused: `npm run test:run -- src/app/App.test.tsx src/shared/config/env.test.ts src/shared/test/setup-isolation.test.ts` — 성공, 3 files / 18 tests passed.
|
||
- P0-GATE: `npm ci` — 성공, 307 packages, 0 vulnerabilities. `npx playwright install chromium webkit` — 성공. `npm run typecheck` — 성공. `npm run lint` — 성공. `npm run test:run -- src/app/App.test.tsx src/shared/config/env.test.ts src/shared/test/setup-isolation.test.ts` — 성공, 18 tests passed. `npm run e2e -- tests/e2e/smoke.spec.ts` — 성공, 4 projects / 4 tests passed. `npm run build:dev` — 성공. `npm run build:prod` — 성공.
|
||
- 참고: 첫 Gate 병렬 실행 중 `npm ci`가 `node_modules`를 재설치하는 동안 동시에 실행한 `tsc/playwright` 명령이 bin 삭제로 실패했다. `npm ci` 완료 후 같은 명령을 재실행해 모두 성공했다.
|
||
|
||
### Task R0.2 — API origin 문서·E2E 단일 source 복구
|
||
|
||
**Goal 실행 `P0-R2`:** 현재 `.env` API origin을 실행 문서와 Playwright route/probe가 함께 사용하게 해 mock/server Gate의 host drift를 제거한다.
|
||
|
||
- **연결 리뷰:** [Phase 0 현재 리뷰](./reviews/phase0-project-foundation.md) — `REV-P0-004`
|
||
- **시작 조건:** `.env.development=https://test-api.sodalive.net`, `.env.production=https://api.sodalive.net`인 현재 working tree와 `VITE_API_BASE_URL` runtime 검증.
|
||
- **완료 증거:** stale origin에서 실패하는 mock/server E2E RED, origin 단일 source 적용, focused/full mock·server E2E와 문서/diff 검증 기록.
|
||
- **Files:** Modify: `README.md`, `docs/agent-guide/environment.md`, `tests/e2e/auth.spec.ts`, `tests/e2e/accessibility-shell.spec.ts`, `tests/e2e/server-mode-boundary.spec.ts`, `tests/e2e/error-mapping.spec.ts`, `tests/e2e/comments-test-support.ts`, 필요 시 Playwright 전용 env helper/config.
|
||
- **Interfaces:** 앱, browser mock handler, Playwright route/probe는 같은 개발 API base URL에서 파생하고 개발/프로덕션 host를 여러 파일에 상수로 복제하지 않는다.
|
||
- **범위 밖:** `.env`를 과거 host로 되돌리기, unhandled request를 bypass/warn으로 완화하기, 실제 ADMIN credential 발급.
|
||
|
||
- [x] **RED:** 현재 `.env.development`에서 과거 `test-character-admin` route/probe가 매칭되지 않아 mock error-mapping과 server auth/boundary E2E가 실패함을 기록했다.
|
||
- [x] **GREEN:** Playwright가 현재 개발 API origin을 한 source에서 읽도록 수정하고 README·환경 가이드의 현재값을 정렬했다.
|
||
- [x] **REFACTOR:** 과거 host literal 검색을 0건으로 만들고 focused Comments server route와 full mock/server Gate를 재실행했다.
|
||
- **실행 명령:** `npm run e2e:mock -- tests/e2e/error-mapping.spec.ts`; `npm run e2e:mock`; `npm run e2e`; `npm run e2e -- tests/e2e/comments.spec.ts --project=chromium`; `rg -n 'test-character-admin|character-admin\\.sodalive\\.net' README.md docs/agent-guide tests/e2e`; `npm run typecheck`; `npm run lint`; `npm run build`.
|
||
- **기대 결과:** 모든 자동 명령 exit 0, Playwright unhandled request 0건, 현재 개발/프로덕션 origin 문서 불일치 0건.
|
||
- **수동 확인:** mock mode는 현재 개발 origin을 intercept하고 server mode는 browser MSW 없이 같은 origin으로 요청하는지 network에서 확인한다.
|
||
|
||
**P0-R2 수정 검증 기록 (2026-07-30):**
|
||
|
||
- RED: `npm run e2e:mock -- tests/e2e/error-mapping.spec.ts` — 4 failed / 4 passed. 과거 `test-character-admin` 요청이 MSW `onUnhandledRequest: "error"`에 걸려 기대 401 대신 500을 반환했다. `npm run e2e -- tests/e2e/server-mode-boundary.spec.ts --project=chromium` — 2 failed / 2 passed. stale route가 현재 `.env.development` origin 요청을 intercept하지 못해 404 한국어 message와 network retry assertion이 실패했다.
|
||
- GREEN: `tests/e2e/api-base-url.ts`를 추가해 E2E route/probe가 `.env.development`의 `VITE_API_BASE_URL`을 읽게 하고, `README.md`와 `docs/agent-guide/environment.md`를 현재 개발/프로덕션 origin으로 정렬했다. `npm run e2e:mock -- tests/e2e/error-mapping.spec.ts` — 8 passed. `npm run e2e -- tests/e2e/server-mode-boundary.spec.ts --project=chromium` — 4 passed.
|
||
- REFACTOR/회귀: `rg -n 'test-character-admin|character-admin\.sodalive\.net' README.md docs/agent-guide tests/e2e` — no matches. `npm run e2e -- tests/e2e/comments.spec.ts --project=chromium` — 3 passed. `npm run e2e` — 36 passed. `npm run e2e:mock` 단일 실행은 900초 제한으로 188/228 진행 중 timeout됐고, 같은 allowlist를 project별로 분할해 `chromium` 57 passed, `webkit` 49 passed / 8 skipped, `mobile-chrome` 52 passed / 5 skipped, `mobile-safari` 48 passed / 9 skipped를 확인했다.
|
||
- Type/LSP/build: `npm run typecheck`, `npm run lint`, `npm run build` — 모두 exit 0. LSP diagnostics: `tests/e2e` 18 files / 오류 0건, 변경 E2E 파일 개별 오류 0건.
|
||
- 수동/표면 확인: mock `error-mapping`은 현재 개발 origin을 MSW handler가 intercept해 한국어 401/403/415를 반환했고, server mode는 browser MSW 없이 같은 origin route/probe에서 404·network retry와 Comments server route가 통과했다. timeout으로 남은 Vite mock server PID는 종료해 포트 8888/8889 listen 0건을 확인했다.
|
||
|
||
### Task R0.3 — 운영체제 메타데이터의 저장소 유입 차단
|
||
|
||
**Goal 실행 `P0-R3`:** 문서 디렉터리에 index된 `.DS_Store`를 제거하고 같은 운영체제 메타데이터가 다시 version control 대상이 되지 않게 한다.
|
||
|
||
- **연결 리뷰:** [Phase 0 현재 리뷰](./reviews/phase0-project-foundation.md) — `REV-P0-005`
|
||
- **시작 조건:** `git ls-files 'docs/20260725_AI캐릭터관리자웹/.DS_Store'`가 index entry를 반환하고 `.gitignore`에 `.DS_Store` 규칙이 없는 현재 working tree.
|
||
- **완료 증거:** 해당 index entry 삭제, 저장소 전역 ignore 규칙, 추적 `.DS_Store` 0건과 targeted diff 검증 기록.
|
||
- **Files:** Modify: `.gitignore`; Delete: `docs/20260725_AI캐릭터관리자웹/.DS_Store`; Test: 없음 — runtime 동작이 없는 repository hygiene 변경이다.
|
||
- **Interfaces:** 애플리케이션 runtime, package script, 문서 내용에는 영향을 주지 않는다.
|
||
- **범위 밖:** 다른 사용자 산출물 삭제, `.gitignore` 전면 정리, 애플리케이션 코드·dependency 변경.
|
||
- **P0-GATE 연결:** runtime Gate 재실행 대신 repository 정적 검증을 수행하고 `git diff --check`가 통과하기 전에는 Task를 완료하지 않는다.
|
||
- **TDD 예외 사유:** 운영체제 metadata의 index 포함 여부와 ignore 규칙은 runtime 동작이 없어 unit test로 실패를 만드는 것보다 Git index 정적 검사가 직접적인 증거다.
|
||
- **대체 검증 방법:** cleanup 전후 `git ls-files`와 `.gitignore` 검색을 비교하고 targeted `git diff --check` 결과를 기록한다.
|
||
|
||
- [x] **RED 대체:** `git ls-files --stage | rg '\.DS_Store'`와 `git status --short --untracked-files=all | rg '\.DS_Store'`로 현재 staged entry를 재현했다.
|
||
- [x] `.DS_Store`를 index와 working tree에서 제거하고 저장소 전역 ignore 규칙을 한 줄 추가했다.
|
||
- [x] 추적 entry 0건, ignore 적용, whitespace 오류 0건을 확인하고 Progress에 실제 결과를 기록했다.
|
||
- **실행 명령:** `git ls-files | rg '(^|/)\.DS_Store$'`; `rg -n '^\.DS_Store$' .gitignore`; `git diff --check -- .gitignore docs/20260725_AI캐릭터관리자웹`.
|
||
- **기대 결과:** 첫 명령은 no matches, ignore 검색은 정확히 한 규칙을 반환하고 diff 검증은 exit 0.
|
||
- **수동 확인:** 없음. binary 운영체제 메타데이터 제거는 index와 ignore 정적 결과로 판정한다.
|
||
|
||
**P0-R3 수정 검증 기록 (2026-07-31):**
|
||
|
||
- RED 대체: `git ls-files --stage | rg '\.DS_Store'`와 `git status --short --untracked-files=all | rg '\.DS_Store'`에서 `docs/20260725_AI캐릭터관리자웹/.DS_Store` staged entry를 확인했다. `rg -n '^\.DS_Store$' .gitignore`는 no matches였다.
|
||
- GREEN: `.gitignore`에 `.DS_Store` 규칙을 추가하고 `git rm --cached -- docs/20260725_AI캐릭터관리자웹/.DS_Store`와 working tree 파일 삭제로 운영체제 metadata를 제거했다.
|
||
- 검증: `git ls-files | rg '(^|/)\.DS_Store$'` — no matches. `git status --short --untracked-files=all | rg '\.DS_Store'` — no matches. `rg -n '^\.DS_Store$' .gitignore` — `7:.DS_Store`. `git diff --check -- .gitignore docs/20260725_AI캐릭터관리자웹` — exit 0.
|
||
- LSP: `.gitignore`는 extension 없는 ignore 파일이라 설정된 LSP 서버가 없어 diagnostics 대상이 아니다. runtime 동작 없는 repository hygiene 변경이므로 정적 Git 검증으로 판정했다.
|
||
|
||
---
|
||
|
||
## Phase 1. 공통 플랫폼·인증/인가·컴포넌트 기반
|
||
|
||
**목표:** ADMIN 인증 흐름과 보호된 Admin shell을 완결하고, Phase 2 이후 화면이 조합해 사용할 공통 UI·form·file·media component contract를 제공한다.
|
||
|
||
**Phase Goal `P1`:** Task 1.1 → 1.6과 Phase 1 Gate를 완료해 도메인 Phase가 사용할 공통 contract를 제공한다.
|
||
|
||
- **시작 조건:** `P0-GATE` 완료.
|
||
- **완료 조건:** `P1-T1`~`P1-T6`, `P1-GATE` 완료와 auth/shared contract 검증 기록.
|
||
- **현재 상태:** 완료. 회귀나 기록 정합성 수정은 별도 수정 goal로 수행한다.
|
||
|
||
**요구사항:** `AUTH-001~013`, `UX-001~002`, PRD `7`, `10.1~10.8`, `11.1~11.2`, `12`의 공통 항목.
|
||
|
||
**주요 Files:**
|
||
|
||
- Create: `components.json`, `src/styles/globals.css`
|
||
- Create: `src/app/providers.tsx`, `src/app/router.tsx`, `src/app/route-paths.ts`
|
||
- Create: `src/shared/api/{types,api-error,client,pagination,query-client}.ts`
|
||
- Create: `src/shared/api/__tests__/{client,pagination}.test.ts`
|
||
- Create: `src/shared/test/{server,handlers,render}.ts`
|
||
- Create: `src/shared/hooks/use-search-params-state.ts`
|
||
- Create: `src/shared/ui/{page-state,status-badge,search-toolbar,resource-pagination,responsive-resource-list}.tsx`
|
||
- Create: `src/shared/ui/{confirm-deactivate-dialog,unsaved-changes-guard,file-field,image-crop-dialog,upload-progress}.tsx`
|
||
- Create: `src/shared/ui/{admin-audio-player,audio-playback-provider}.tsx`
|
||
- Create: `src/shared/validation/{file-validation,image-policy,audio-file-policy}.ts`
|
||
- Create: `src/shared/validation/{file-validation,image-policy,audio-file-policy}.test.ts`
|
||
- Create: `src/shared/lib/{crop-image,format-date,format-price}.ts`
|
||
- Create: `src/shared/lib/{crop-image,format-date,format-price}.test.ts`
|
||
- Create: `src/shared/ui/__tests__/{page-state,resource-list,confirm-deactivate-dialog,unsaved-changes-guard}.test.tsx`
|
||
- Create: `src/shared/ui/__tests__/{file-field,image-crop-dialog,upload-progress,admin-audio-player}.test.tsx`
|
||
- Create: `src/features/auth/api/auth-api.ts`
|
||
- Create: `src/features/auth/model/auth-session.tsx`
|
||
- Create: `src/features/auth/model/auth-session-storage.ts`
|
||
- Create: `src/features/auth/schemas/login-schema.ts`
|
||
- Create: `src/features/auth/pages/{LoginPage,AccessDeniedPage}.tsx`
|
||
- Create: `src/features/auth/components/ProtectedRoute.tsx`
|
||
- Create: `src/features/auth/tests/auth-api.test.ts`
|
||
- Create: `src/features/auth/tests/{login-page,protected-route}.test.tsx`
|
||
- Create: `src/layouts/AdminLayout.tsx`
|
||
- Create: `tests/e2e/auth.spec.ts`, `tests/e2e/accessibility-shell.spec.ts`
|
||
|
||
**Phase 1 공통 컴포넌트 소비처 Matrix:**
|
||
|
||
| Shared component | 확정 소비 Phase | 공통 책임 |
|
||
|---|---|---|
|
||
| `PageState` | 1~7 | loading·empty·error·retry |
|
||
| `StatusBadge` | 2~6 | semantic tone과 label/icon slot |
|
||
| `SearchToolbar` | 2, 3, 4 | controlled search, filter slot, debounce callback |
|
||
| `ResourcePagination` | 2~7 | `PageData` 기반 page/size control |
|
||
| `ResponsiveResourceList` | 2~6 | desktop/mobile rendering slot |
|
||
| `ConfirmDeactivateDialog` | 2~5 | 대상명·영향·확인 callback |
|
||
| `UnsavedChangesGuard` | 2~7 | dirty form route 이탈 확인 |
|
||
| `FileField`, `ImageCropDialog` | 2~5 | file input과 주입된 crop policy 실행 |
|
||
| `UploadProgress` | 3, 5 | 진행률·취소·재시도 callback |
|
||
| `AdminAudioPlayer`, `AudioPlaybackProvider` | 3, 5 | native audio control과 단일 재생 상태 |
|
||
|
||
이 표는 PRD에 이미 확정된 반복 소비만 포함한다. `PageHeader`는 실제 구현·소비가 없고 현재 필요한 반복 소비 컴포넌트가 아니므로 만들지 않는다. 각 도메인 Phase에서 필요하다고 확인될 때 추가한다.
|
||
|
||
### Task 1.1 최소 디자인 시스템
|
||
|
||
**Goal 실행 `P1-T1`:** light-only token과 실제 소비되는 최소 shadcn primitive를 접근성 기준으로 고정한다.
|
||
|
||
- **시작 조건:** `P0-GATE` 완료, PRD `10.1~10.9` 확인.
|
||
- **완료 증거:** 체크박스 전체, token/component test, UX 검색 채택·제외 기록, typecheck·lint·build.
|
||
- **범위 밖:** dark mode, 전체 primitive 선행 scaffold, 도메인 전용 UI.
|
||
|
||
- [x] PRD `10.9`의 `ui-ux-pro-max` design-system 검색을 실행하고 관리자 제품에 맞는 채택·제외 결과를 작업 기록에 남긴다.
|
||
- [x] Tailwind CSS와 shadcn/ui CSS variable mode를 구성한다.
|
||
- [x] `brand-500=#00BDF7`, hover `#00A9DE`, active `#009DCE`, primary foreground `#062B36`, link/ring `#007EA8`을 primitive → semantic → component token으로 연결한다.
|
||
- [x] 핵심 foreground 대비, control boundary 3:1, primary 위 흰색 금지를 token test로 고정한다.
|
||
- [x] 밝은 `:root` token만 만들고 `.dark`, ThemeProvider, theme toggle, system dark 연동이 없음을 test한다.
|
||
- [x] Korean system font stack, mobile input 16px, 44px target, focus ring, reduced motion, semantic z-index를 base style에 둔다.
|
||
- [x] 상태 Badge가 text label을 포함하고 색상만으로 상태를 전달하지 않는 test를 작성한다.
|
||
- [x] icon-only action에는 accessible name과 Tooltip이 있고 필수 control boundary·focus indicator가 인접 배경 대비 3:1 이상인지 test한다.
|
||
- [x] Auth와 공통 상태에 실제 필요한 shadcn component만 추가한다. 전체 component를 선행 scaffold하지 않는다.
|
||
|
||
### Task 1.2 공통 API·server state
|
||
|
||
**Goal 실행 `P1-T2`:** 인증 저장 adapter와 API envelope·오류·pagination·query 정책을 독립 contract로 완성한다.
|
||
|
||
- **시작 조건:** `P1-T1` 완료, PRD/API Contract 공통 규칙 확인.
|
||
- **완료 증거:** 체크박스 전체, API/session/pagination/query test와 typecheck·lint·build 기록.
|
||
- **범위 밖:** 로그인 화면, 보호 route, 도메인 endpoint adapter.
|
||
|
||
- [x] `AuthSessionRecord = { token: string; role: "ADMIN" }`의 읽기·저장·제거 adapter를 먼저 만들고, API client는 React provider가 아니라 이 adapter의 token reader/clear callback에만 의존한다.
|
||
- [x] `ApiResponse<T>` 성공형이 `errorProperty` 생략과 `null`을 모두 수용하고 오류형은 비2xx status·`message`·`errorProperty`를 보존하는 test를 작성한다.
|
||
- [x] `PageData<T>`와 `page=0`, `size=20`, size 최소 20·최대 50 보정을 test한다. 문서에 없는 음수 page 동작은 추정하지 않는다.
|
||
- [x] 모든 요청에 `Accept-Language: ko`를 붙이고 로그인 요청에는 Authorization을 제외하는 test를 작성한다.
|
||
- [x] 보호 요청과 logout에만 현재 session의 Bearer token을 붙이는 test를 작성한다.
|
||
- [x] 400/404/405/415/500의 서버 한국어 message를 공통 `ApiError`가 보존하는 MSW test를 작성한다.
|
||
- [x] 동시에 여러 401이 와도 session clear·알림·login redirect가 한 번만 발생하는 test를 작성한다.
|
||
- [x] 403은 session을 지우지 않고 AccessDenied 상태로 전달하는 test를 작성한다.
|
||
- [x] logger가 JWT, password, signed URL, multipart body를 받지 않는 test를 작성한다.
|
||
- [x] TanStack Query provider와 공통 retry 정책을 구성하되 401/403과 mutation을 무조건 재시도하지 않는다.
|
||
|
||
### Task 1.3 로그인·session·logout
|
||
|
||
**Goal 실행 `P1-T3`:** ADMIN login, session 복원과 실패해도 local session을 제거하는 logout 흐름을 완성한다.
|
||
|
||
- **시작 조건:** `P1-T2` 완료.
|
||
- **완료 증거:** 체크박스 전체, auth API/session/LoginPage test와 민감정보 저장 금지 검증 기록.
|
||
- **범위 밖:** 보호 route와 Admin shell 조합.
|
||
|
||
- [x] email 형식, password 필수, visible label, 오류 연결, first-invalid-focus test를 작성한다.
|
||
- [x] login이 `POST /admin/member/login`에 `{ email, password }` JSON만 보내는 contract test를 작성한다.
|
||
- [x] 응답의 `data.token`과 `data.role="ADMIN"`만 유효 session으로 인정한다.
|
||
- [x] 성공 session을 `sessionStorage`에만 저장하고 같은 탭 새로고침에서 복원하는 test를 작성한다.
|
||
- [x] token 누락, role 누락·비ADMIN이면 보호 route를 렌더링하지 않고 저장도 하지 않는 test를 작성한다.
|
||
- [x] `localStorage`, IndexedDB, cookie에 인증 정보가 기록되지 않는 test를 작성한다.
|
||
- [x] refresh endpoint 호출이 0건임을 확인한다.
|
||
- [x] logout이 Bearer header와 body 없이 `POST /member/logout`을 한 번 호출하는 test를 작성한다.
|
||
- [x] logout 성공·비2xx·network error 모두 local session을 제거하고 `/login`으로 이동하며, 실패 때만 서버 확인 실패 경고를 표시하고 session을 복원하지 않는 test를 작성한다.
|
||
|
||
### Task 1.4 보호 route·Admin shell
|
||
|
||
**Goal 실행 `P1-T4`:** 인증·권한 route 처리와 반응형·접근 가능한 Admin shell을 완성한다.
|
||
|
||
- **시작 조건:** `P1-T3` 완료.
|
||
- **완료 증거:** 체크박스 전체, route/shell test, auth·accessibility E2E와 viewport/axe 기록.
|
||
- **범위 밖:** Phase 3 Character 데이터와 mutation.
|
||
|
||
- [x] 미인증 사용자가 보호 content를 한 프레임도 보지 않고 `/login`으로 이동하는 test를 작성한다.
|
||
- [x] 401은 session 제거 후 login, 403은 AccessDeniedPage로 가는 route test를 작성한다.
|
||
- [x] desktop sidebar, mobile Sheet, header, logout, breadcrumb, skip link, `main` landmark를 구현한다.
|
||
- [x] keyboard로 login, navigation, logout을 완료하고 dialog/menu focus가 trigger로 복귀하는 test를 작성한다.
|
||
- [x] `/ai-characters`에는 Phase 3이 교체할 명시적 빈 route state만 두고 가짜 도메인 데이터를 만들지 않는다.
|
||
- [x] 320px과 200% zoom에서 shell overflow와 가려진 control이 없는지 E2E로 확인한다.
|
||
- [x] shell route의 axe critical·serious 위반 0건을 확인한다.
|
||
|
||
### Task 1.5 공통 화면·form component
|
||
|
||
**Goal 실행 `P1-T5`:** 반복 소비가 확정된 상태·검색·목록·확인·이탈 방지 UI contract를 완성한다.
|
||
|
||
- **시작 조건:** `P1-T1`, `P1-T2`, 소비처 Matrix 확인.
|
||
- **완료 증거:** 체크박스 전체, component 독립 test와 현재 Admin shell에서 실제 소비되는 `PageState` integration test 기록.
|
||
- **범위 밖:** domain DTO/endpoint 결합과 단일 화면 전용 wrapper.
|
||
|
||
- [x] 위 소비처 Matrix를 contract test와 component API에 대조하고 모든 shared component가 두 Phase 이상에서 같은 의미로 사용되는지 확인한다.
|
||
- [x] `PageState`가 loading·empty·error·retry를 접근 가능한 status/alert와 keyboard action으로 표현하는 test를 작성한다.
|
||
- [x] `StatusBadge`가 domain label·icon/보조 문구를 slot으로 받고 색상만으로 상태를 전달하지 않는 test를 작성한다.
|
||
- [x] `SearchToolbar`는 controlled search/filter slot과 약 300ms debounce·URL query callback만 제공하고 특정 endpoint query를 알지 않게 한다.
|
||
- [x] `ResourcePagination`은 공통 `PageData`로 page/size를 제어하고 disabled·accessible name·keyboard 동작을 제공한다.
|
||
- [x] `ResponsiveResourceList`는 desktop/mobile rendering slot만 제공하고 domain column·DTO·action을 prop union으로 내장하지 않는다.
|
||
- [x] `ConfirmDeactivateDialog`는 대상명·영향 설명·확인 callback을 조합하고 Switch로 대체되지 않게 test한다.
|
||
- [x] `UnsavedChangesGuard`는 dirty 상태에서만 route 이탈을 확인하고 저장 성공 후 해제되며 focus를 trigger로 복귀한다.
|
||
- [x] UTC 시각의 Asia/Seoul 표시와 0 이상 정수 “캔” 표시를 공통 formatter로 고정하고 domain status label은 formatter에 넣지 않는다.
|
||
- [x] 각 component는 독립 RTL test를 먼저 통과시킨다. 현재 Admin shell은 실제 소비되는 `PageState`만 조합하고, 나머지 반복 소비 component는 각 도메인 Phase에서 처음 쓰일 때 integration test를 작성한다.
|
||
|
||
### Task 1.6 공통 file·media component
|
||
|
||
**Goal 실행 `P1-T6`:** domain-neutral file validation, crop, upload 상태와 단일 audio 재생 contract를 완성한다.
|
||
|
||
- **시작 조건:** `P1-T1`, `P1-T2` 완료, 파일 경계 계약 확인.
|
||
- **완료 증거:** 체크박스 전체, validation/crop/file/upload/player test와 dependency-boundary 기록.
|
||
- **범위 밖:** 도메인별 multipart request와 resource form.
|
||
|
||
- [x] image `10MB`의 정확한 byte 기준을 `10,485,760 bytes`로 확정하고 PRD·API Contract·구현 상수·경계 test에 기록한다.
|
||
- [x] `FileField`는 visible label, 설명·오류 연결, accept 안내, keyboard activation, 선택 취소와 controlled `File | null` contract만 제공한다.
|
||
- [x] 공통 file validation은 주입된 allowed extension·MIME·maxBytes를 함께 확인한다. JPEG/PNG·GIF 같은 resource별 allowed set은 각 도메인 policy가 소유한다.
|
||
- [x] 공통 audio policy는 MP3/AAC/M4A, `.m4a + audio/x-m4a`, `1,024,000,000 bytes` 경계, WAV 거부를 표현하되 실제 container·codec을 client에서 판정하지 않는다.
|
||
- [x] `ImageCropDialog`는 주입된 aspect/max width 정책으로 이동·zoom·reset·preview·취소·적용·keyboard/button 대안·no-upscale 결과를 제공한다.
|
||
- [x] crop interaction은 검증된 단일 dependency가 native pointer/Canvas 직접 구현보다 코드·접근성 위험을 줄이는지 확인해 하나만 선택하고, Canvas는 결과 File 생성에만 사용한다. `package.json`에 crop dependency가 없어 새 dependency 없이 button/range/keyboard 이동을 선택했고 Canvas는 결과 File 생성에만 사용한다.
|
||
- [x] 공통 image policy는 `aspect`, `maxWidth`, `noUpscale`, crop 적용 여부를 받는 domain-neutral contract만 정의한다. Character·Audio·Series·Community profile과 GIF 예외는 각 feature Phase가 소유한다.
|
||
- [x] `UploadProgress`는 진행률·취소·재시도 callback과 상태 표시만 담당하고 Axios request나 domain form을 직접 소유하지 않는다.
|
||
- [x] `AdminAudioPlayer`는 native audio를 감싸 play/pause·seek·time·volume·speed·keyboard·일반 오류·수동 재시도를 제공하고 download와 자동 refetch/자동 play를 만들지 않는다.
|
||
- [x] `AudioPlaybackProvider`가 동시에 하나의 player만 재생되게 하며 signed URL을 log·storage에 전달하지 않는 test를 작성한다.
|
||
- [x] 공통 file/media component는 endpoint·query cache·domain DTO를 import하지 않는 dependency test 또는 review checklist를 통과한다.
|
||
|
||
### Phase 1 Gate
|
||
|
||
**Goal 실행 `P1-GATE`:** 공통 component와 ADMIN 인증 journey가 후속 Phase 선행조건을 충족하는지 판정한다.
|
||
|
||
- **시작 조건:** `P1-T1`~`P1-T6` 완료.
|
||
- **완료 증거:** 아래 명령과 Expected 전체 통과 및 Phase 1 종합 검증 기록.
|
||
- **범위 밖:** 실패와 무관한 도메인 구현. 실패는 소유 Task 수정 goal로 되돌린다.
|
||
|
||
```bash
|
||
npm run test:run -- src/shared src/features/auth src/layouts src/app/App.test.tsx
|
||
npm run e2e -- tests/e2e/auth.spec.ts tests/e2e/accessibility-shell.spec.ts
|
||
npm run typecheck
|
||
npm run lint
|
||
npm run build
|
||
```
|
||
|
||
**Expected:** 공통 component contract가 독립 test로 고정되고, ADMIN login → 보호 shell → 새로고침 session 복원 → logout이 동작하며 비ADMIN·stale claim·401·logout 실패 경로가 독립적으로 검증된다.
|
||
|
||
### Task R1.2 — image 최대 용량 exact byte 계약 확정
|
||
|
||
**Goal 실행 `P1-R2`:** image 10MB 표현을 백엔드와 동일한 exact byte 계약으로 확정한다.
|
||
|
||
- 연결 리뷰: [Phase 0·1 리뷰](./reviews/review-phase-0-1.md) — `REV-P1-010`
|
||
- 상태: 완료.
|
||
- 결정: 백엔드 exact byte를 `10,485,760 bytes`로 확정했다. `10,485,760 bytes`는 허용하고 `10,485,761 bytes`부터 거부한다.
|
||
- 완료 증거: PRD, API 계약, `IMAGE_MAX_BYTES`, `limit-1/limit/limit+1` 경계 테스트를 같은 값으로 정렬했다.
|
||
|
||
### Task R1.3 — 인증 보호·오류·logout 회귀 수정
|
||
|
||
**Goal 실행 `P1-R3`:** Phase 1 인증의 보호 경계, 오류 피드백, logout 단일 요청 계약을 복구한다.
|
||
|
||
- 연결 리뷰: [Phase 0·1 리뷰](./reviews/review-phase-0-1.md) — `REV-P1-002`, `REV-P1-006`, `REV-P1-007`
|
||
- 완료 증거: stale ADMIN probe pending·403 동안 보호 shell 비노출, probe `size=20`, 서버 한국어 message 우선 표시, logout in-flight 중 API 1회 호출.
|
||
- 범위 밖: 인증 방식 또는 token 저장 방식 교체, Phase 3 권한 기능.
|
||
|
||
- [x] 지연 403 전 보호 shell 비노출과 probe `size=20` request assertion을 `src/app/App.test.tsx`에 추가했다.
|
||
- [x] 로그인 실패 시 `Error.message`의 서버 한국어 message를 우선 표시하도록 `LoginPage`를 수정했다.
|
||
- [x] `AuthSessionProvider.logout`에 in-flight promise coalescing을 추가해 연속 입력도 `/member/logout` 1회만 보내게 했다.
|
||
|
||
### Task R1.4 — crop·음성 파일 정책 회귀 수정
|
||
|
||
**Goal 실행 `P1-R4`:** Phase 1 파일·미디어 입력의 crop 결과와 audio 형식 조합 계약을 복구한다.
|
||
|
||
- 연결 리뷰: [Phase 0·1 리뷰](./reviews/review-phase-0-1.md) — `REV-P1-003`, `REV-P1-004`
|
||
- 완료 증거: source crop rectangle 기반 출력, 1200×600 → 1:1 no-upscale 600×600과 상·하단 alpha 유지 test, 자유 비율, pointer drag, 확장자↔MIME 조합 table test.
|
||
- 범위 밖: 외부 crop library, 서버 파일 변환 구현.
|
||
|
||
- [x] `calculateCropOutputSize`가 source height와 crop rectangle을 반영하도록 수정했다.
|
||
- [x] `createCroppedImageFile`이 원본 전체 draw 대신 source rectangle을 `drawImage`에 전달하도록 수정했다.
|
||
- [x] `createCroppedImageFile`의 1200×600 → 1:1 no-upscale 600×600 출력과 상·하단 alpha 유지 회귀 test를 추가했다.
|
||
- [x] `ImageCropDialog`에 `aspect: "free"`와 pointer drag 이동을 추가했다.
|
||
- [x] audio 확장자별 허용 MIME table을 적용하고 `mp3+audio/mp4`, `aac+audio/mpeg`, `m4a+audio/aac` 거부 test를 추가했다.
|
||
|
||
### Task R1.5 — 공유 UI keyboard·focus 접근성 회귀 수정
|
||
|
||
**Goal 실행 `P1-R5`:** Phase 1 공유 UI의 modal focus, 필드 오류 상태, keyboard event 경계를 복구한다.
|
||
|
||
- 연결 리뷰: [Phase 0·1 리뷰](./reviews/review-phase-0-1.md) — `REV-P1-005`, `REV-P1-008`, `REV-P1-009`
|
||
- 완료 증거: 세 modal의 focus trap과 trigger focus return, FileField `aria-invalid`, audio descendant control Enter·Space 격리.
|
||
- 범위 밖: 공유 UI 전체 교체, 시각 디자인 재작업.
|
||
|
||
- [x] `useModalFocus`를 추가해 `ConfirmDeactivateDialog`, `UnsavedChangesGuard`, `ImageCropDialog`의 초기 focus, Tab loop, 닫힌 뒤 trigger focus return을 공유했다.
|
||
- [x] `FileField` error 상태 input에 `aria-invalid="true"`를 연결했다.
|
||
- [x] `AdminAudioPlayer` group shortcut은 event target이 group 자체일 때만 처리하도록 막았다.
|
||
|
||
**P1-R3/R4/R5 수정 검증 기록 (2026-07-27):**
|
||
|
||
- RED: `npm run test:run -- src/app/App.test.tsx src/features/auth/tests/login-page.test.tsx src/features/auth/tests/auth-session.test.tsx src/shared/lib/crop-image.test.ts src/shared/validation/file-media-policy.test.ts src/shared/ui/__tests__/file-field.test.tsx src/shared/ui/__tests__/admin-audio-player.test.tsx src/shared/ui/__tests__/confirm-deactivate-dialog.test.tsx src/shared/ui/__tests__/unsaved-changes-guard.test.tsx src/shared/ui/__tests__/image-crop-dialog.test.tsx src/shared/test/setup-isolation.test.ts` — 기대 실패 확인, 11 files failed / 13 failed / 40 passed.
|
||
- GREEN focused: 같은 command — 성공, 11 files / 55 tests passed.
|
||
- Auth focused: `npm run test:run -- src/app/App.test.tsx src/features/auth/tests/auth-session.test.tsx` — 성공, 2 files / 23 tests passed.
|
||
- P1-GATE unit: `npm run test:run -- src/shared src/features/auth src/layouts src/app/App.test.tsx` — 성공, 27 files / 102 tests passed.
|
||
- P1-GATE E2E: `npm run e2e -- tests/e2e/auth.spec.ts tests/e2e/accessibility-shell.spec.ts` — 성공, 4 projects / 16 tests passed. 320×640 viewport, CSS zoom 2, keyboard-contained mobile menu, axe critical·serious 0건을 포함한다.
|
||
- 전체 unit: `npm run test:run` — 성공, 28 files / 108 tests passed.
|
||
- `npm run typecheck`, `npm run lint`, `npm run build` — 모두 성공.
|
||
- `git diff --check` — 성공.
|
||
|
||
**문서·체크 정정 검증 기록 (2026-07-27):**
|
||
|
||
- `docs/agent-guide/README.md` 참조는 실제 존재하는 `docs/agent-guide/` 세부 문서 관리 방식으로 정정했다.
|
||
- `P1-R4`의 pixel·alpha 실행 체크는 `src/shared/lib/crop-image.test.ts`의 `createCroppedImageFile keeps a wide no-upscale crop opaque at the top and bottom` test로 보강했다.
|
||
- `P1-R2`의 image 10MB exact byte는 백엔드 확인값 `10,485,760 bytes`로 PRD·API 계약·Task 1.6 체크·구현 상수·경계 테스트를 정렬했다.
|
||
- Fresh focused: `npm run test:run -- src/shared/lib/crop-image.test.ts` — 성공, 1 file / 4 tests passed.
|
||
- Fresh P1-GATE unit: `npm run test:run -- src/shared src/features/auth src/layouts src/app/App.test.tsx` — 성공, 27 files / 102 tests passed.
|
||
- Fresh 전체 unit: `npm run test:run` — 성공, 28 files / 108 tests passed.
|
||
- Fresh E2E: `npm run e2e -- tests/e2e/smoke.spec.ts` — 성공, 4 tests passed. `npm run e2e -- tests/e2e/auth.spec.ts tests/e2e/accessibility-shell.spec.ts` — 성공, 16 tests passed.
|
||
|
||
**P1-R1/R2 closure 점검 기록 (2026-07-27):**
|
||
|
||
- `P1-R2`: image exact byte를 `10,485,760 bytes`로 확정하고 PRD, API 계약, `IMAGE_MAX_BYTES`, `limit-1/limit/limit+1` 경계 테스트를 정렬했다.
|
||
- `PageHeader`: 구현·소비가 없고 현재 필요한 반복 소비 컴포넌트가 아니므로 Phase 1 shared 산출물과 소비처 Matrix에서 제거했다. 필요하면 해당 도메인 Phase에서 새로 추가한다.
|
||
- RED: `npm run test:run -- src/shared/validation/file-media-policy.test.ts` — `policy.maxBytes` 미구현으로 실패 확인. 이후 `IMAGE_MAX_BYTES` export 누락으로 실패 확인.
|
||
- Focused: `npm run test:run -- src/shared/validation/file-media-policy.test.ts src/app/App.test.tsx src/shared/ui/__tests__/page-state.test.tsx` — 성공, 3 files / 21 tests passed.
|
||
- P1-GATE unit: `npm run test:run -- src/shared src/features/auth src/app/App.test.tsx` — 성공, 27 files / 103 tests passed.
|
||
- P1-GATE E2E: `npm run e2e -- tests/e2e/auth.spec.ts tests/e2e/accessibility-shell.spec.ts` — 성공, 16 tests passed. 320px·200% zoom, keyboard-contained mobile menu, axe critical·serious 0건을 포함한다.
|
||
- 전체 unit: `npm run test:run` — 성공, 28 files / 109 tests passed.
|
||
- Smoke E2E: `npm run e2e -- tests/e2e/smoke.spec.ts` — 성공, 4 tests passed.
|
||
- `npm run typecheck`, `npm run lint`, `npm run build`, `git diff --check` — 모두 성공.
|
||
- LSP diagnostics: `src/shared/validation/file-media-policy.test.ts` 오류 0건. `src/shared/validation/image-policy.ts` 개별 fresh diagnostics는 timeout이 있었고, `npm run typecheck`와 `npm run build` 성공으로 보완 확인했다.
|
||
|
||
### Task R1.1 — Phase 1 산출물·통합·Gate 완료 증거 복구
|
||
|
||
**Goal 실행 `P1-R1`:** Phase 1 완료 판정을 재현 가능한 산출물·통합·Gate 증거로 복구한다.
|
||
|
||
- 연결 리뷰: [Phase 0·1 리뷰](./reviews/review-phase-0-1.md) — `REV-P1-001`
|
||
- 상태: 완료.
|
||
- 완료한 부분: P1-GATE 명령은 `src/app/App.test.tsx`를 포함하도록 보정했고, P1-R2/R3/R4/R5의 자동·E2E 검증 기록을 누적했다.
|
||
- 결정: `PageHeader`는 구현·소비가 없고 현재 필요한 반복 소비 컴포넌트가 아니므로 Phase 1 shared 산출물에서 제거한다. 필요하다고 판단되는 도메인 Phase에서 추가한다.
|
||
- 완료 증거: P1-R2 exact byte와 PageHeader 폐기 결정을 반영했고, closure Gate를 재실행해 열린 Blocker·High·Medium 0건으로 정리했다.
|
||
|
||
### Task R1.6 — 모든 보호 요청 401의 인증 만료 처리
|
||
|
||
**Goal 실행 `P1-R6`:** 응답 body 형식과 무관하게 보호 요청의 HTTP 401에서 session을 한 번 제거하고 login으로 이동한다.
|
||
|
||
- **연결 리뷰:** [Phase 1 재리뷰](./reviews/review-phase-1-20260729.md) — `REV-P1-011`
|
||
- **시작 조건:** 기존 concurrent 401 burst 규칙과 `UNKNOWN_API_ERROR_MESSAGE`를 보존한다.
|
||
- **Files:** Modify: `src/shared/api/client.ts`, `src/shared/api/__tests__/client-auth.test.ts`, `src/shared/api/__tests__/client.test.ts`; Test: 동일 경로.
|
||
- **Interfaces:** `createApiClient({clearSession,onAuthExpired})`; malformed JSON/envelope·empty body 401도 `ApiError.status=401`을 유지한다.
|
||
- **범위 밖:** refresh token, 403 session 제거, XHR upload adapter(`P4-R2`).
|
||
|
||
- [x] **RED:** malformed JSON, empty body, schema 불일치 401 각각에서 `clearSession/onAuthExpired`가 한 burst에 1회 호출되는 실패 test를 추가했다.
|
||
- [x] **GREEN:** HTTP status를 기준으로 인증 만료를 먼저 처리하되 기존 오류 정규화와 concurrent request coalescing을 유지했다.
|
||
- [x] **REFACTOR:** 정상 오류 envelope 401과 malformed 401의 중복 분기를 status 기반 단일 처리로 정리하고 공통 API 회귀를 실행했다.
|
||
- **실행 명령:** `npm run test:run -- src/shared/api/__tests__/client-auth.test.ts src/shared/api/__tests__/client.test.ts`; `npm run test:run -- src/features/auth src/app`.
|
||
- **기대 결과:** exit 0, 모든 보호 401에서 burst별 clear/redirect 1회, 403 clear 0회.
|
||
- **수동 확인:** session이 있는 보호 route에서 empty-body 401을 반환했을 때 login 이동과 만료 안내를 확인한다.
|
||
|
||
**P1-R6 수정 검증 기록 (2026-07-30):**
|
||
|
||
- RED: `npm run test:run -- src/shared/api/__tests__/client-auth.test.ts src/shared/api/__tests__/client.test.ts` — malformed JSON, empty body, schema mismatch 401에서 `clearSession` 0회로 3 tests가 기대대로 실패했다.
|
||
- GREEN focused: 같은 command — 성공, 2 files / 21 tests passed. 보호 401의 body 형식과 무관하게 burst별 `clearSession/onAuthExpired` 1회를 확인했고 403 clear 0회 회귀를 유지했다.
|
||
- LSP diagnostics: `src/shared/api/client.ts`, `src/shared/api/__tests__/client-auth.test.ts` — 오류 0건.
|
||
- Auth/session focused regression: `npm run test:run -- src/features/auth/tests/auth-session.test.tsx src/features/auth/tests/login-page.test.tsx src/app/App.test.tsx src/app/App.protected-auth.test.tsx` — 성공, 3 files / 24 tests passed.
|
||
- 참고: 계획상 broad regression `npm run test:run -- src/features/auth src/app`은 6 failed / 47 passed로 종료했다. 실패 6건은 `REV-P10-001`/`P10-R1`의 공통 오류 fallback stale assertion 범위(`API 응답 형식이 올바르지 않습니다.` vs `알 수 없는 오류가 발생했습니다.`)와 일치해 `P1-R6` 완료 판정에서 제외하고 기존 후속 Task로 유지한다.
|
||
|
||
### Task R1.7 — zoom no-upscale와 touch pinch crop 복구
|
||
|
||
**Goal 실행 `P1-R7`:** 현재 zoom의 선택 source pixel을 넘지 않는 crop 결과와 pointer pinch 입력을 제공한다.
|
||
|
||
- **연결 리뷰:** [Phase 1 재리뷰](./reviews/review-phase-1-20260729.md) — `REV-P1-012`, `REV-P1-013`
|
||
- **시작 조건:** `FILE-012`, PRD `10.4`, 기존 keyboard/range/reset contract 유지.
|
||
- **Files:** Modify: `src/shared/lib/crop-image.ts`, `src/shared/lib/crop-image.test.ts`, `src/shared/ui/image-crop-dialog.tsx`, `src/shared/ui/__tests__/image-crop-dialog.test.tsx`; Test: 동일 경로.
|
||
- **Interfaces:** output size는 현재 `CropSourceRect.width/height`와 resource `maxWidth` 중 작은 값; pinch는 두 pointer 거리 변화로 기존 `zoom 1..3`을 갱신한다.
|
||
- **범위 밖:** resource별 aspect/maxWidth 변경, GIF crop, 외부 gesture dependency.
|
||
|
||
- [x] **RED:** zoom 1.5에서 600px source crop이 400px 이하 output을 생성하는 pixel test와 두 pointer pinch가 zoom을 바꾸는 Dialog test를 실패시킨다.
|
||
- [x] **GREEN:** output size에 현재 zoom을 전달하고 native pointer map으로 최소 pinch를 구현한다.
|
||
- [x] **REFACTOR:** drag와 pinch pointer cleanup을 정리하고 Character/Audio/Series/Community crop 회귀를 실행한다.
|
||
- **실행 명령:** `npm run test:run -- src/shared/lib/crop-image.test.ts src/shared/ui/__tests__/image-crop-dialog.test.tsx src/features/characters src/features/audio-contents src/features/series src/features/community-posts`.
|
||
- **기대 결과:** exit 0, `noUpscale=true`의 output/source scale `<=1`, 기존 keyboard/range test와 resource aspect test 통과.
|
||
- **수동 확인:** tablet touch에서 drag·pinch·range·keyboard 대안과 예상 결과 pixel 표시를 확인한다.
|
||
|
||
**P1-R7 수정 검증 기록 (2026-07-30):**
|
||
|
||
- RED: `npm run test:run -- src/shared/lib/crop-image.test.ts src/shared/ui/__tests__/image-crop-dialog.test.tsx` — 2 failed / 9 passed. `calculateCropOutputSize`는 zoom 1.5에서도 600×600을 반환했고, Dialog two-pointer pinch는 zoom을 변경하지 않아 예상 결과 400×400을 찾지 못했다.
|
||
- GREEN focused: 같은 command — 성공, 2 files / 14 tests passed. `noUpscale=true`에서 zoom 1.5의 output 400×400, 210:297 zoom 1.8의 source 이하 output 236×333, tiny source 1×1, aspect 2 tiny source 1×1, free aspect tiny source 1×1, range/keyboard output 재계산, two-pointer pinch zoom 1.5 apply payload와 `touch-action: none`을 확인했다.
|
||
- Broad crop regression: `npm run test:run -- src/shared/lib/crop-image.test.ts src/shared/ui/__tests__/image-crop-dialog.test.tsx src/features/characters src/features/audio-contents src/features/series src/features/community-posts` — 성공, 23 files / 119 tests passed.
|
||
- Surface E2E: `npm run e2e:mock -- tests/e2e/series.spec.ts --project=chromium` — 성공, 7 tests passed. Series 생성 흐름에서 실제 브라우저의 `이미지 crop` Dialog open/apply 경로를 확인했다.
|
||
- Type/LSP: `npm run typecheck`, `npm run lint` — 성공(exit 0). `lsp_diagnostics src/shared/ui`, `tests/e2e` — 오류 0건. `src/shared/lib/crop-image.ts`, `src/shared/lib/crop-image.test.ts` 단일 fresh diagnostics는 3초 timeout이었고 typecheck/lint로 보완 확인했다.
|
||
- Reviewer blocker 보강: 1차 `P1-R7` review는 210:297 zoom 1.8에서 output 236×334가 source 236×333을 1px 초과하고, crop preview에 `touch-action: none`이 없어 실제 touch pinch가 browser 기본 gesture에 취소될 수 있다고 BLOCKED 판정했다. 추가 RED `npm run test:run -- src/shared/lib/crop-image.test.ts src/shared/ui/__tests__/image-crop-dialog.test.tsx`는 2 failed / 11 passed였고, tiny source 0×0 붕괴 RED `npm run test:run -- src/shared/lib/crop-image.test.ts`는 1 failed / 6 passed였다. 2차 review는 aspect 2 tiny source가 1×0으로 붕괴한다고 BLOCKED 판정했고 추가 RED `npm run test:run -- src/shared/lib/crop-image.test.ts`는 1 failed / 6 passed였다. 3차 review는 free aspect tiny source가 1×2로 source height를 초과한다고 BLOCKED 판정했고 추가 RED `npm run test:run -- src/shared/lib/crop-image.test.ts`는 2 failed / 5 passed였다. 수정 후 위 focused/broad/E2E/type/lint/diff 검증이 통과했다.
|
||
- 수동 확인 한계: 실제 tablet touch 장비 QA는 이 작업 환경에서 수행하지 못했다. native pointer event 회귀 test로 drag·pinch·range·keyboard 대안을 자동 검증했으며, 실기기 touch 확인은 릴리스 QA 항목으로 유지한다.
|
||
|
||
### Task R1.8 — 사용되지 않는 공통 pagination helper 계약 정리
|
||
|
||
**Goal 실행 `P1-R8`:** 사용되지 않는 공통 pagination helper의 FanTalk 전용 `20..50` size clamp와 page 최소값 누락을 제거해 공통 계약을 오해할 여지를 없앤다.
|
||
|
||
- **연결 리뷰:** [Phase 1 현재 리뷰](./reviews/phase1-platform-auth-shared-ui.md) — `REV-P1-014`
|
||
- **시작 조건:** PRD §11.1의 공통 `page >= 0`, `size >= 1`, 전역 최대값 없음과 FanTalk만 `size=20..50`인 예외 계약.
|
||
- **완료 증거:** production 사용처 검색, stale helper 제거, 도메인 adapter pagination 회귀와 전체 정적 Gate 통과.
|
||
- **Files:** Modify: `src/shared/api/pagination.ts`, `src/shared/api/__tests__/pagination.test.ts`; Test: 각 도메인 contract test.
|
||
- **Interfaces:** 실제 소비 중인 `PageData` type은 유지한다. production 소비자가 없는 `PageParams`, `createPageParams`와 잘못된 clamp test를 제거하고 도메인별 adapter의 계약별 normalization은 보존한다.
|
||
- **범위 밖:** 모든 도메인 pagination의 새 공통 abstraction, FanTalk의 명시적 `20..50` 보정 변경, 화면 page-size 선택지 변경.
|
||
|
||
- [x] **RED 대체:** `rg -n 'createPageParams' src`로 production 사용처 0건과 test-only 사용을 기록하고, 현재 helper가 `{page:-1,size:1}`과 `{size:51}`을 공통 계약과 다르게 처리함을 고정했다.
|
||
- [x] **GREEN:** 사용되지 않는 `PageParams`, `createPageParams`와 그 동작 test를 삭제하고 실제 소비 중인 `PageData` type만 유지했다.
|
||
- [x] **REFACTOR:** 새 helper를 만들지 않고 Character/Audio/Series/Community/Comments/FanTalk adapter의 기존 contract test로 각 pagination 예외를 확인했다.
|
||
- **실행 명령:** `rg -n 'createPageParams' src`; `npm run test:run -- src/shared/api src/features/characters src/features/audio-contents src/features/series src/features/community-posts src/features/comments src/features/fan-talks`; `npm run typecheck`; `npm run lint`.
|
||
- **기대 결과:** production `createPageParams` 사용 0건, 공통 최대 `50` 오해를 고정하는 test 0건, 모든 명령 exit 0.
|
||
- **수동 확인:** 없음. 사용되지 않는 내부 helper 제거 Task이므로 contract test와 정적 검색으로 대체한다.
|
||
|
||
**P1-R8 수정 검증 기록 (2026-07-30):**
|
||
|
||
- RED 대체: `rg -n 'createPageParams|PageParams|PageData|normalizeSize' src`에서 `createPageParams` production 사용처 0건, `src/shared/api/__tests__/pagination.test.ts` test-only 사용과 `src/shared/api/pagination.ts` 정의만 확인했다. 기존 `npm run test:run -- src/shared/api/__tests__/pagination.test.ts`는 1 file / 5 tests passed로 잘못된 공통 size `20..50` clamp test가 통과함을 확인했다.
|
||
- GREEN: `src/shared/api/pagination.ts`에서 `PageParams`와 `createPageParams`를 삭제하고 `PageData`만 유지했다. `src/shared/api/__tests__/pagination.test.ts`는 `PageData` shape test만 남겼다.
|
||
- REFACTOR/회귀: `rg -n 'createPageParams|type PageParams|import .*PageParams' src` — no matches. `npm run test:run -- src/shared/api src/features/characters src/features/audio-contents src/features/series src/features/community-posts src/features/comments src/features/fan-talks` — 35 files / 201 tests passed. `npm run typecheck`, `npm run lint` — 모두 exit 0.
|
||
- LSP diagnostics: 변경 파일 단일 fresh diagnostics는 3초 제한으로 timeout됐고, `npm run typecheck`와 `npm run lint` 성공으로 보완 확인했다.
|
||
|
||
### Task R1.9 — 공통 pagination control ID 격리
|
||
|
||
**Goal 실행 `P1-R9`:** 한 화면에 둘 이상의 `ResourcePagination`이 렌더링돼도 각 페이지 크기 label이 자신의 select와 유일하게 연결되도록 한다.
|
||
|
||
- **연결 리뷰:** [Phase 1 현재 리뷰](./reviews/phase1-platform-auth-shared-ui.md) — `REV-P1-015`
|
||
- **시작 조건:** PRD §10.7의 visible label·연결 가능한 접근성 이름과 Comments의 root/reply pagination 동시 렌더링 구조.
|
||
- **완료 증거:** 두 인스턴스의 중복 ID를 재현하는 실패 test, React `useId` 기반 최소 수정, shared UI·Comments focused unit과 mock E2E·정적 Gate 통과.
|
||
- **Files:** Modify: `src/shared/ui/resource-pagination.tsx`, `src/shared/ui/__tests__/resource-pagination.test.tsx`; Test: 필요 시 `src/features/comments/tests/comment-thread.test.tsx`, `tests/e2e/comments.spec.ts`.
|
||
- **Interfaces:** 페이지·크기 변경 callback과 URL/API pagination 값은 바꾸지 않고 인스턴스별 select ID와 `htmlFor`만 유일하게 만든다.
|
||
- **범위 밖:** 새 pagination abstraction, page-size option 변경, 도메인별 pagination query 정책 변경.
|
||
|
||
- [x] **RED:** 한 container에 `ResourcePagination` 두 개를 렌더링해 select ID 중복과 두 label이 같은 control을 가리키는 실패 test를 작성했다.
|
||
- [x] **GREEN:** 컴포넌트 인스턴스별 안정적인 ID를 만들고 각 label의 `htmlFor`를 해당 select에 연결했다.
|
||
- [x] **REFACTOR:** 공개 prop는 늘리지 않고 기존 component 내부에서 ID 생성을 끝내며 단일 인스턴스 동작을 보존했다.
|
||
- **실행 명령:** `npm run test:run -- src/shared/ui/__tests__/resource-pagination.test.tsx src/features/comments`; `npm run e2e:mock -- tests/e2e/comments.spec.ts --project=chromium`; `npm run typecheck`; `npm run lint`; `npm run build`.
|
||
- **기대 결과:** 모든 명령 exit 0, 동시 렌더링된 pagination select ID 중복 0건, 각 visible label과 control 연결 일치.
|
||
- **수동 확인:** Community Sheet에서 root와 reply pagination이 함께 보이는 fixture로 각 페이지 크기 label을 클릭해 대응 select가 focus되는지 확인한다.
|
||
|
||
**P1-R9 수정 검증 기록 (2026-07-31):**
|
||
|
||
- RED: `npm run test:run -- src/shared/ui/__tests__/resource-pagination.test.tsx` — 1 failed / 2 passed. 두 `ResourcePagination`의 label `control`이 같은 `id="resource-page-size"` 해석에 의존해 첫 번째 select를 가리키는 실패를 확인했다.
|
||
- GREEN focused: `npm run test:run -- src/shared/ui/__tests__/resource-pagination.test.tsx src/features/comments` — 성공, 3 files / 14 tests passed. React `useId` 기반으로 select ID 중복 0건과 각 label/control 연결 일치를 확인했다.
|
||
- 개발 중 E2E: 사용자 지시에 따라 전체 Task 완료 전 반복 실행하지 않았다. 지정 mock Comments E2E는 최종 회귀 단계에서 필요 시 실행한다.
|
||
|
||
### Task R1.10 — crop frame과 저장 source 좌표 일치 복구
|
||
|
||
**Goal 실행 `P1-R10`:** `ImageCropDialog`에서 보이는 crop frame·이동·확대 결과와 canvas가 저장하는 원본 source rectangle을 같은 좌표계로 계산한다.
|
||
|
||
- **연결 리뷰:** [Phase 1 현재 리뷰](./reviews/phase1-platform-auth-shared-ui.md) — `REV-P1-016`
|
||
- **시작 조건:** PRD `FILE-008`, Image crop UI 흐름 4, 현재 `ImageCropDialog`의 CSS px offset과 `calculateCropSourceRect`의 원본 px offset 직접 공유.
|
||
- **완료 증거:** 축소 표시된 대형 원본과 고정/free aspect 각각의 미리보기-저장 좌표 실패 test, 단일 좌표 변환 최소 구현, shared crop unit과 Character·Audio·Series·Community focused 회귀 및 mock Chromium crop 흐름 통과.
|
||
- **Files:** Modify: `src/shared/ui/image-crop-dialog.tsx`, `src/shared/lib/crop-image.ts`, `src/shared/ui/__tests__/image-crop-dialog.test.tsx`, `src/shared/lib/crop-image.test.ts`; Test: `src/features/characters/tests/CharacterCreatePage.test.tsx`, `src/features/audio-contents/tests/audio-form.test.tsx`, `src/features/series/tests/series-form.test.tsx`, `src/features/community-posts/tests/community-form.test.tsx`, `tests/e2e/character-workspace.spec.ts`, `tests/e2e/audio-content.spec.ts`, `tests/e2e/series.spec.ts`, `tests/e2e/community.spec.ts`.
|
||
- **Interfaces:** `CropRenderRequest`의 offset 의미를 표시 frame 기준으로 명확히 하고, preview transform과 `calculateCropSourceRect`가 동일한 scale·clamp를 사용한다. resource별 aspect와 최대 출력 크기는 바꾸지 않는다.
|
||
- **범위 밖:** 새 crop library/dependency, 회전·필터·다중 crop, 서버 파일 계약 변경.
|
||
- **Phase 1 Gate 연결:** shared crop focused unit, 네 도메인 crop 회귀, mock Chromium crop journey, `typecheck`·`lint`·build가 모두 통과하기 전에는 종결하지 않는다.
|
||
|
||
- [x] **RED:** 4,000×3,000 원본이 341×256px로 표시될 때 10 CSS px 이동이 원본 scale에 맞게 환산되고, 1:1·210:297 frame 밖 영역이 결과에 포함되지 않아야 하는 실패 test를 작성했다.
|
||
- [x] **GREEN:** 실제 preview frame 크기와 표시 image scale을 기준으로 source offset을 계산하고, 고정/free aspect frame과 canvas source rectangle을 일치시켰다.
|
||
- [x] **REFACTOR:** 좌표 계산을 shared crop 함수 한 곳에 유지하고 도메인별 보정값이나 새 상태 library를 만들지 않은 채 관련 회귀를 실행했다.
|
||
- **실행 명령:** `npm run test:run -- src/shared/lib/crop-image.test.ts src/shared/ui/__tests__/image-crop-dialog.test.tsx`; `npm run test:run -- src/features/characters src/features/audio-contents src/features/series src/features/community-posts`; `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts tests/e2e/audio-content.spec.ts tests/e2e/series.spec.ts tests/e2e/community.spec.ts --project=chromium`; `npm run typecheck`; `npm run lint`; `npm run build`.
|
||
- **기대 결과:** 모든 명령 exit 0, preview frame과 저장 결과의 source rectangle 일치, resource별 aspect·no-upscale 회귀 0건.
|
||
- **수동 확인:** 4:3 대형 image를 Character/Audio의 1:1, Series의 210:297, Community의 free crop에서 이동·확대하고 적용 전 frame과 적용 후 preview의 같은 영역을 비교한다.
|
||
|
||
**P1-R10 수정 검증 기록 (2026-07-31):**
|
||
|
||
- RED: `npm run test:run -- src/shared/lib/crop-image.test.ts src/shared/ui/__tests__/image-crop-dialog.test.tsx` — 2 failed / 14 passed. 4,000×3,000 원본의 1:1 preview frame에서 10 CSS px 이동이 sourceX 490으로 계산됐고, Dialog의 `renderCrop` request에는 `previewFrameWidth/Height`가 없어 기대대로 실패했다.
|
||
- GREEN: `CropRenderRequest`와 `calculateCropSourceRect`가 적용 시점의 preview frame 크기를 optional로 받아 CSS px offset을 source px로 환산하게 했다. `ImageCropDialog`는 preview image의 `getBoundingClientRect()`를 request에 전달한다.
|
||
- Focused: 같은 command — 2 files / 16 tests passed. 1:1과 210:297 frame source rect 환산, 기존 zoom/no-upscale/keyboard/pinch/free ratio 회귀를 확인했다.
|
||
- 도메인 회귀: `npm run test:run -- src/features/characters` — 6 files / 43 tests passed. `npm run test:run -- src/features/audio-contents` — 8 files / 55 tests passed. `npm run test:run -- src/features/series` — 9 files / 38 tests passed. `npm run test:run -- src/features/community-posts`는 180초 timeout이 아닌 1 failed / 43 passed로 `community-sheet.test.tsx` 목록 로딩 실패가 재현됐으나, 같은 spec 단독은 1 file / 7 tests passed, `community-form.test.tsx` 단독은 1 file / 10 tests passed로 crop 변경 실패와 분리했다.
|
||
- LSP diagnostics: `src/shared/lib/crop-image.ts`, `src/shared/ui/image-crop-dialog.tsx`, `src/shared/ui/__tests__/image-crop-dialog.test.tsx`, `src/shared/lib`, `src/shared/ui` — 오류 0건. `src/shared/lib/crop-image.test.ts` 단일 diagnostics는 timeout됐고 directory diagnostics로 보완했다.
|
||
- E2E/수동 확인: 사용자 지시에 따라 개발 중 반복 E2E는 실행하지 않고 최종 회귀 단계에서 필요 시 mock Chromium crop 흐름을 실행한다.
|
||
|
||
### Task R1.11 — crop 적용 비동기·Blob URL 수명 복구
|
||
|
||
**Goal 실행 `P1-R11`:** crop 적용을 single-flight로 처리하고 실패를 복구 가능하게 표시하며 생성한 Blob URL을 적용·취소·교체·unmount에서 정확히 한 번 해제한다.
|
||
|
||
- **연결 리뷰:** [Phase 1 현재 리뷰](./reviews/phase1-platform-auth-shared-ui.md) — `REV-P1-017`
|
||
- **시작 조건:** `P1-R10` 완료, 현재 다섯 crop source 생성 경로가 성공한 `URL.createObjectURL`을 해제하지 않고 `ImageCropDialog.applyCrop`이 pending/error 상태를 갖지 않는 구조.
|
||
- **완료 증거:** 적용 연타·render reject·URL 교체/취소/unmount 실패 test, 최소 pending/error/cleanup contract, shared와 네 소비 도메인 회귀 및 정적 Gate 통과.
|
||
- **Files:** Create: `src/shared/lib/create-image-crop-source.ts`, `src/shared/lib/create-image-crop-source.test.ts`; Modify: `src/shared/ui/image-crop-dialog.tsx`, `src/shared/ui/__tests__/image-crop-dialog.test.tsx`, `src/features/characters/pages/CharacterCreatePage.tsx`, `src/features/characters/pages/CharacterEditPage.tsx`, `src/features/characters/tests/CharacterCreatePage.test.tsx`, `src/features/characters/tests/CharacterEditPage.test.tsx`, `src/features/audio-contents/pages/AudioContentFormPage.tsx`, `src/features/audio-contents/tests/audio-form.test.tsx`, `src/features/series/pages/SeriesFormPage.tsx`, `src/features/series/tests/series-form-crop.test.tsx`, `src/features/community-posts/lib/create-community-post-crop-source.ts`, `src/features/community-posts/tests/community-form.test.tsx`.
|
||
- **Interfaces:** crop source는 자신이 만든 Blob URL의 idempotent release 책임을 노출한다. 적용 pending 동안 적용·취소·이동 control의 재진입을 막고 `role=status`를 표시하며 reject 시 dialog와 원본 선택을 유지해 재시도할 수 있게 한다.
|
||
- **범위 밖:** upload 진행률, File 자체 폐기, 범용 async hook, 외부 image editor dependency.
|
||
- **Phase 1 Gate 연결:** shared lifecycle unit과 각 도메인 cancel/stale/reject 회귀, `typecheck`·`lint`·build가 통과하기 전에는 완료하지 않는다.
|
||
|
||
- [x] **RED:** 적용 연타 시 renderer 1회, reject 후 visible alert·재시도, 적용·취소·새 선택·unmount마다 소유 Blob URL `revokeObjectURL` 정확히 1회를 기대하는 실패 test를 작성한다.
|
||
- [x] **GREEN:** 공통 crop source의 idempotent release와 dialog single-flight/error 상태를 최소 구현하고 각 소비 화면이 수명 종료 시 release한다.
|
||
- [x] **REFACTOR:** 중복된 다섯 `createObjectURL` helper를 공통 함수로만 합치고 도메인 form state나 upload contract는 바꾸지 않는다.
|
||
- **실행 명령:** `npm run test:run -- src/shared/lib/create-image-crop-source.test.ts src/shared/ui/__tests__/image-crop-dialog.test.tsx`; `npm run test:run -- src/features/characters src/features/audio-contents src/features/series src/features/community-posts`; `npm run typecheck`; `npm run lint`; `npm run build`; `git diff --check`.
|
||
- **기대 결과:** 모든 명령 exit 0, pending 중 renderer 최대 1회, reject 후 입력 보존·재시도 가능, 소유 Blob URL 누수·중복 revoke 0건.
|
||
- **수동 확인:** 10MB에 가까운 image를 연속 선택·취소·적용하고 느린 crop renderer 조건에서 진행 안내, control 비활성화, 실패 후 재시도를 확인한다.
|
||
|
||
**P1-R11 수정 검증 기록 (2026-07-31):**
|
||
|
||
- RED: `npm run test:run -- src/shared/lib/create-image-crop-source.test.ts src/shared/ui/__tests__/image-crop-dialog.test.tsx`는 구현 전 `create-image-crop-source` import 부재와 `ImageCropDialog` pending/error/single-flight 부재로 실패했다.
|
||
- GREEN: `createImageCropSource`가 소유 Blob URL의 idempotent `release`를 노출하고 load error에서 즉시 revoke한다. `ImageCropDialog`는 적용 중 `role=status`, control disabled, renderer reject `role=alert`, 재시도를 제공한다.
|
||
- REFACTOR: Character create/edit, Audio, Series, Community create/sheet의 중복 `createObjectURL` helper를 공통 helper로만 합쳤고, 각 crop source 교체·취소·적용·unmount와 stale selection에서 `release`를 호출한다. 도메인 form state와 upload contract는 변경하지 않았다.
|
||
- Focused 검증: `npm run test:run -- src/shared/lib/create-image-crop-source.test.ts src/shared/ui/__tests__/image-crop-dialog.test.tsx` — 2 files / 11 tests passed. 인접 도메인 단독 검증은 Character create/edit, Audio form 3종, Series form — 6 files / 57 tests passed, Community form — 1 file / 10 tests passed, Community sheet 단독 — 1 file / 7 tests passed였다.
|
||
- 정적 Gate: `src/shared/lib`, `src/shared/ui`, `src/features/characters/pages`, `src/features/audio-contents`, `src/features/series`, `src/features/community-posts` LSP diagnostics 0건. `npm run typecheck`, `npm run lint`, `npm run build`는 exit 0이었다. `npm run build`는 기존 bundle size warning(502.94 kB chunk)을 표시했다.
|
||
- 넓은 회귀 참고: `npm run test:run -- src/features/characters src/features/audio-contents src/features/series src/features/community-posts`는 `audio-list.test.tsx` 1 failed 후 timeout됐고, 별도 crop 인접 재실행은 모두 통과해 이번 crop lifecycle 변경과 분리했다. E2E와 10MB 수동 확인은 사용자 지시에 따라 최종 회귀 단계에서 필요 시 실행한다.
|
||
|
||
### Task R1.12 — fetch·XHR 교차 401 만료 전환 단일화
|
||
|
||
**Goal 실행 `P1-R12`:** 공통 fetch client와 Audio XHR upload가 동시에 401을 받아도 현재 session의 제거·login 이동을 합쳐 한 번만 수행하고, 새 로그인 session에서는 다음 만료를 다시 처리한다.
|
||
|
||
- **연결 리뷰:** [Phase 1 현재 리뷰](./reviews/phase1-platform-auth-shared-ui.md) — `REV-P1-018`
|
||
- **시작 조건:** PRD `AUTH-005`, §13의 동시 401 단일 처리, 기존 완료 `P1-R6`과 `P4-R2`의 transport별 401 burst contract.
|
||
- **완료 증거:** 같은 mutable session dependency를 사용하는 fetch request와 XHR upload가 동시에 401일 때 callback 2회를 재현하는 실패 test, transport 간 현재 session guard 최소 수정, auth/API·Audio upload focused 및 Phase 1 Gate 검증 기록.
|
||
- **Files:** Modify: `src/shared/api/client.ts`, `src/shared/api/__tests__/client-auth.test.ts`, `src/features/audio-contents/api/upload-audio-content.ts`, `src/features/audio-contents/tests/audio-upload.test.ts`; Create: `src/features/audio-contents/tests/audio-upload-auth-lifecycle.test.ts`.
|
||
- **Interfaces:** 두 transport는 401 처리 시 현재 protected session 존재 여부를 같은 `getToken` 의미로 확인하고, 첫 handler가 동기적으로 session을 제거한 뒤의 handler는 `clearSession`/`onAuthExpired`를 다시 호출하지 않는다. 새 login token이 생기면 다음 401 burst는 다시 처리한다.
|
||
- **범위 밖:** refresh token, 전역 event bus, router 교체, XHR→fetch 전환, 403 정책 변경.
|
||
- **Phase 1 Gate 연결:** cross-transport RED/GREEN, 기존 transport별 malformed/동시 401 회귀, app/auth/shared unit, `typecheck`·`lint`·build가 통과하기 전에는 완료 처리하지 않는다.
|
||
- **검증 기록:** RED callback 실제 횟수, GREEN 뒤 동일·후속 session burst 횟수와 Gate 결과를 이 Task 하단과 Phase 1 리뷰에 누적한다.
|
||
|
||
- [x] **RED:** mutable token을 공유하는 `createApiClient` request와 `uploadAudioContent`를 동시에 401로 종료해 `clearSession`/`onAuthExpired` 각각 1회를 기대하고 현재 3회 호출 실패를 확인했다. 이어 새 token을 설정한 다음 burst는 다시 1회 처리돼야 한다.
|
||
- [x] **GREEN:** 두 transport가 callback 실행 직전 현재 session 존재를 확인하고 첫 clear 뒤 후속 handler가 만료 전환을 건너뛰도록 최소 수정했다.
|
||
- [x] **REFACTOR:** 기존 transport 내부 burst latch와 malformed response mapping을 유지하고 인증 coordinator나 새 dependency를 추가하지 않았다.
|
||
- **실행 명령:** `npm run test:run -- src/shared/api/__tests__/client-auth.test.ts src/features/audio-contents/tests/audio-upload.test.ts src/features/audio-contents/tests/audio-upload-auth-lifecycle.test.ts`; `npm run test:run -- src/app src/features/auth src/shared/api src/features/audio-contents/tests/audio-upload.test.ts src/features/audio-contents/tests/audio-upload-auth-lifecycle.test.ts`; `npm run typecheck`; `npm run lint`; `npm run build:dev`; `npm run build:prod`; `git diff --check`.
|
||
- **기대 결과:** 모든 명령 exit 0, 같은 session의 fetch+XHR 401 callback 각각 총 1회, 새 session의 다음 401 callback 각각 추가 1회, 기존 403·malformed body·upload cancel/retry 회귀 0건.
|
||
- **수동 확인:** upload 진행 중 다른 보호 조회도 401을 반환하게 해 로그인 이동·만료 안내·browser history 변화가 한 번인지 확인하고, 다시 로그인한 뒤 같은 만료 흐름이 재동작하는지 확인한다.
|
||
|
||
**P1-R12 수정 검증 기록 (2026-07-31):**
|
||
|
||
- RED: `npm run test:run -- src/shared/api/__tests__/client-auth.test.ts src/features/audio-contents/tests/audio-upload.test.ts` — 1 failed / 19 passed. 신규 교차 transport test에서 `clearSession` 기대 2회 대비 실제 3회로 같은 session의 fetch+XHR 401 만료 전환 중복을 재현했다.
|
||
- GREEN: `client.ts`와 `upload-audio-content.ts`가 401 callback 직전 현재 token 존재를 다시 확인하게 수정했다. 같은 focused 명령은 2 files / 20 tests passed였다.
|
||
- REFACTOR/회귀: 새 coordinator나 dependency 없이 `readToken` helper만 중복 제거에 사용했고, lifecycle test를 별도 파일로 분리해 touched TS files를 250 LOC 이하로 유지했다. reviewer blocker로 fetch-first 순서를 추가한 뒤 `auth` 주입 시 storage fallback을 쓰지 않도록 보완했다. `npm run test:run -- src/shared/api/__tests__/client-auth.test.ts src/features/audio-contents/tests/audio-upload.test.ts src/features/audio-contents/tests/audio-upload-auth-lifecycle.test.ts` — 3 files / 22 tests passed. `npm run test:run -- src/app src/features/auth src/shared/api src/features/audio-contents/tests/audio-upload.test.ts` — 13 files / 91 tests passed. `npm run test:run` — 79 files / 397 tests passed. `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod`, targeted `git diff --check` — 모두 exit 0. build는 기존 503.04kB chunk warning만 표시했다. LSP diagnostics는 변경 파일·디렉터리 기준 오류 0건이었다.
|
||
- E2E/수동 확인: 사용자 지시에 따라 개발 중 E2E는 반복 실행하지 않고 모든 Task 구현 뒤 필요 시 수행한다.
|
||
|
||
### Task R1.13 — crop frame과 저장 영역 시각적 일치 복구
|
||
|
||
**Goal 실행 `P1-R13`:** 고정 aspect image crop에서 운영자가 보는 frame과 canvas가 저장하는 source rectangle을 하나의 비변환 좌표계로 일치시킨다.
|
||
|
||
- **연결 리뷰:** [Phase 1 현재 리뷰](./reviews/phase1-platform-auth-shared-ui.md) — `REV-P1-019`
|
||
- **시작 조건:** PRD `FILE-005~008`, `FILE-010`, `FILE-012`, 기존 `P1-R10` 완료 기록과 현재 `ImageCropDialog`이 transformed `<img>`의 rect를 crop frame 크기로 사용하는 구조.
|
||
- **완료 증거:** 4:3 원본의 1:1과 `210:297` 고정 frame에서 보이는 영역과 저장 source rectangle 불일치를 재현하는 실패 test, 실제 crop viewport rect 기반 최소 수정, shared·4개 소비 도메인 회귀와 browser 수동 비교 기록.
|
||
- **Files:** Modify: `src/shared/ui/image-crop-dialog.tsx`, `src/shared/lib/crop-image.ts`; Test: `src/shared/ui/__tests__/image-crop-dialog.test.tsx`, `src/shared/lib/crop-image.test.ts`, `tests/e2e/character-workspace.spec.ts`, `tests/e2e/audio-content.spec.ts`, `tests/e2e/series.spec.ts`, `tests/e2e/community.spec.ts`.
|
||
- **Interfaces:** `previewFrameWidth/Height`는 CSS transform이 적용된 image rect가 아니라 실제 crop viewport를 의미한다. image cover scale, offset, zoom과 clamp는 한 번만 변환하고 resource별 aspect·최대 폭·no-upscale 계약은 유지한다.
|
||
- **범위 밖:** 외부 crop library, 회전·필터, 도메인별 보정값, 서버 file 계약 변경.
|
||
- **Phase 1 Gate 연결:** shared crop focused unit, Character·Audio·Series·Community crop 회귀, mock Chromium crop journey, `typecheck`·`lint`·개발/운영 build가 모두 통과하기 전에는 완료하지 않는다.
|
||
|
||
- [x] **RED:** 4,000×3,000 원본에 1:1과 `210:297` policy를 적용해 crop viewport의 실제 aspect, 이동·확대 후 저장 source rectangle, frame 밖 pixel 미포함을 기대하는 실패 test를 작성했다.
|
||
- [x] **GREEN:** 고정 aspect crop viewport를 보이게 렌더하고 그 viewport의 비변환 rect를 source 계산에 전달하는 최소 구현을 적용했다.
|
||
- [x] **REFACTOR:** preview→source 변환과 clamp를 shared crop 함수 한 곳에 유지하고 zoom이 `getBoundingClientRect()`와 수식에 중복 반영되지 않는지 회귀했다.
|
||
- [x] RED/GREEN/REFACTOR, focused·도메인 단위·정적 검증 결과를 Progress와 Phase 1 리뷰에 누적했다. E2E와 수동 pixel 비교는 사용자 지시에 따라 전체 Task 구현 후 필요 시 수행한다.
|
||
- **실행 명령:** `npm run test:run -- src/shared/lib/crop-image.test.ts src/shared/ui/__tests__/image-crop-dialog.test.tsx`; `npm run test:run -- src/features/characters src/features/audio-contents src/features/series src/features/community-posts`; `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts tests/e2e/audio-content.spec.ts tests/e2e/series.spec.ts tests/e2e/community.spec.ts --project=chromium`; `npm run typecheck`; `npm run lint`; `npm run build:dev`; `npm run build:prod`.
|
||
- **기대 결과:** 모든 명령 exit 0, 고정 aspect frame과 저장 영역 불일치 0건, zoom/offset 중복 scale 0건, free aspect·no-upscale 회귀 0건.
|
||
- **수동 확인:** 4:3 대형 image를 Character/Audio 1:1과 Series `210:297`에서 이동·확대한 뒤 적용 전 frame과 적용 후 preview가 같은 영역인지 비교한다.
|
||
|
||
**P1-R13 수정 검증 기록 (2026-07-31):**
|
||
|
||
- RED: `npm run test:run -- src/shared/ui/__tests__/image-crop-dialog.test.tsx` — 1 failed / 9 passed. 신규 `ImageCropDialog measures the visible crop viewport instead of the transformed image`가 `이미지 crop viewport` label 부재로 실패해 고정 crop viewport가 없는 현재 구조를 재현했다.
|
||
- GREEN: `ImageCropDialog`에 고정 aspect viewport를 렌더하고 `applyCrop`이 image rect 대신 viewport rect를 `previewFrameWidth/Height`로 전달하게 했다. 1차 reviewer blocker였던 image box/frame 불일치는 source aspect 기준 cover image 배치로 보완했다.
|
||
- REFACTOR/회귀: `src/shared/lib/crop-image.test.ts`에 4,000×3,000 원본의 1:1·`210:297` viewport+zoom source rect 회귀를 추가하고, zero-overhang 축에서 화면 offset과 저장 offset이 함께 clamp되는 dialog 회귀를 추가했다. `npm run test:run -- src/shared/lib/crop-image.test.ts src/shared/ui/__tests__/image-crop-dialog.test.tsx` — 2 files / 20 tests passed. `npm run test:run -- src/features/characters src/features/audio-contents src/features/series src/features/community-posts` — 30 files / 183 tests passed.
|
||
- 정적 검증: `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0. build는 기존 500kB chunk warning을 표시했다. LSP diagnostics는 `image-crop-dialog.tsx`, `image-crop-dialog.test.tsx`, `crop-image.test.ts` 오류 0건이다.
|
||
- 리뷰어 게이트: 1차 review에서 cover geometry와 offset clamp blocker 2건을 지적받아 보완했고, 2차 delta review에서 `APPROVED` 판정을 받았다.
|
||
- 환경 정리: 검증 중 `/var/.../T` 여유 공간 부족으로 Vitest가 `ENOSPC`를 반환해, 워크스페이스 밖 `/var/.../T/opencode`의 재생성 가능한 DerivedData/cache 임시 폴더를 삭제했다. `df` 기준 여유 공간은 531MiB에서 15GiB로 회복된 뒤 검증을 재실행했다.
|
||
- E2E/수동: 개발 중 E2E는 사용자 지시에 따라 매 Task마다 수행하지 않고 전체 Task 구현 후 필요 시 수행한다. 실제 pixel 수동 비교도 같은 최종 확인 범위로 남긴다.
|
||
|
||
### Task R1.14 — Character 생성·수정 route 보호 인가 probe 복구
|
||
|
||
**Goal 실행 `P1-R14`:** `/ai-characters/new`와 `/ai-characters/:characterId/edit`도 다른 관리자 route와 같이 서버 보호 route probe의 401/403 정책을 먼저 적용한다.
|
||
|
||
- **연결 리뷰:** [Phase 1 현재 리뷰](./reviews/phase1-platform-auth-shared-ui.md) — `REV-P1-020`
|
||
- **시작 조건:** `P1-R13` 완료, PRD `AUTH-005~006`, `isAiCharactersRoute()`가 Character list/detail과 하위 resource route는 포함하지만 create/edit route는 제외하는 현재 구조.
|
||
- **완료 증거:** create/edit 직접 URL의 probe 403에서 접근 거부 화면, 401에서 session 정리·login 이동, probe 완료 전 하위 form/API 미실행을 고정하는 실패 test와 Phase 1 Gate 기록.
|
||
- **Files:** Modify: `src/app/browser-location.ts`, 필요 시 `src/app/App.tsx`; Create: `src/app/browser-location.test.ts`; Modify: `src/app/App.protected-errors.test.tsx`, `src/app/App.test.tsx`.
|
||
- **Interfaces:** `isAiCharactersRoute` 보호 route 판정은 list, create, detail, edit와 모든 하위 resource route를 포함한다. 403은 session을 유지한 채 `/access-denied`, 401은 session을 제거하고 `/login`으로 이동하는 기존 계약을 변경하지 않는다.
|
||
- **범위 밖:** router library 도입, 서버 인증 계약 변경, role refresh, 새 access-denied 화면.
|
||
|
||
- [x] **RED:** create/edit 직접 route에서 보호 probe 403을 반환할 때 form이 노출되거나 일반 error로 남는 현재 실패와, 401의 login 이동을 재현했다.
|
||
- [x] **GREEN:** 두 route를 보호 route 판정에 포함하는 최소 수정으로 기존 probe 흐름을 재사용했다.
|
||
- [x] **REFACTOR:** route matcher 진리표를 unit test로 고정하고 path 문자열 중복과 별도 auth abstraction을 추가하지 않았다.
|
||
- [x] RED/GREEN/REFACTOR, focused·auth/app 회귀·정적/build 결과를 Progress와 Phase 1 리뷰에 누적했다. stale ADMIN 수동 확인은 실제 credential이 필요한 외부 수동 QA로 남긴다.
|
||
- **실행 명령:** `npm run test:run -- src/app/browser-location.test.ts src/app/App.protected-errors.test.tsx src/app/App.test.tsx`; `npm run test:run -- src/app src/features/auth src/shared/api`; `npm run typecheck`; `npm run lint`; `npm run build:dev`; `npm run build:prod`.
|
||
- **기대 결과:** 모든 명령 exit 0, create/edit probe 전 form·하위 request 0건, 403 접근 거부·401 login 전환 회귀 0건.
|
||
- **수동 확인:** stale ADMIN session으로 create/edit URL을 직접 열고 개발 API 403에서 form이 잠시라도 노출되지 않으며 접근 거부 화면으로 이동하는지 확인한다.
|
||
|
||
**P1-R14 수정 검증 기록 (2026-07-31):**
|
||
|
||
- RED: `npm run test:run -- src/app/browser-location.test.ts src/app/App.protected-errors.test.tsx src/app/App.test.tsx` — 2 failed files / 5 failed / 18 passed. `isAiCharactersRoute`가 create/edit를 false로 판정했고, `/ai-characters/new`, `/ai-characters/101/edit`의 401/403 probe 전환 대신 form 또는 detail error가 노출되는 실패를 재현했다.
|
||
- GREEN: `isAiCharactersRoute`에 `routePaths.aiCharacterCreate`와 `getCharacterEditIdFromPath(path)`를 추가해 기존 보호 route probe를 재사용했다. focused `npm run test:run -- src/app/browser-location.test.ts src/app/App.protected-errors.test.tsx src/app/App.test.tsx` — 3 files / 23 tests passed.
|
||
- REFACTOR/회귀: 새 `src/app/browser-location.test.ts`의 list/create/detail/edit 진리표로 matcher를 고정했다. `npm run test:run -- src/app src/features/auth src/shared/api` — 13 files / 84 tests passed.
|
||
- 정적 검증: LSP diagnostics `browser-location.ts`, `browser-location.test.ts`, `App.protected-errors.test.tsx` 오류 0건. `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0. build는 기존 500kB chunk warning을 표시했다.
|
||
- 리뷰어 게이트: P1-R14 delta review에서 create/edit 보호 probe 합류, 401/403 form 미노출 test, 문서 기록 범위가 기준을 충족해 `APPROVED` 판정을 받았다.
|
||
- 수동 확인: stale ADMIN 개발 API credential이 필요한 외부 수동 QA는 자동 검증 범위 밖으로 유지한다.
|
||
|
||
### Task R1.15 — 잘못된 percent-encoding route 안전 처리
|
||
|
||
**Goal 실행 `P1-R15`:** 잘못된 percent-encoding이 포함된 직접 URL이 `URIError`로 SPA를 중단하지 않고 안전한 기본 route로 정규화되게 한다.
|
||
|
||
- **연결 리뷰:** [Phase 1 현재 리뷰](./reviews/phase1-platform-auth-shared-ui.md) — `REV-P1-021`
|
||
- **시작 조건:** `P1-R14` 완료, `browser-location.ts`의 route regex가 `%`와 같은 segment를 허용하고 모든 parser가 `decodeURIComponent` 예외를 처리하지 않는 현재 구조.
|
||
- **완료 증거:** 단일·복합 param route의 malformed `%` 재현 test, 예외 없는 공통 decode helper와 `/ai-characters` fallback, 정상 한글/ASCII ID route 회귀 및 Phase 1 Gate 기록.
|
||
- **Files:** Modify: `src/app/browser-location.ts`, `src/app/browser-location.test.ts`, `src/app/App.test.tsx`.
|
||
- **Interfaces:** route param decode 실패는 `null`로 판정해 `readRoutePath()`가 `/ai-characters`로 정규화하고, 정상 percent-encoded ID와 navigation API는 유지한다.
|
||
- **범위 밖:** 404 page 신설, URL schema 재설계, router dependency, 백엔드 ID 제약 추정.
|
||
|
||
- [x] **RED:** character/audio/series 단일·복합 param에 `%`, `%E0%A4%A`를 넣었을 때 `URIError`가 발생하는 실패 test를 작성했다.
|
||
- [x] **GREEN:** 예외를 내부에서 종료하는 작은 decode helper로 parser를 공통화하고 잘못된 path를 기본 목록으로 보냈다.
|
||
- [x] **REFACTOR:** 모든 `decodeURIComponent` 호출이 helper 하나를 통하도록 정리하고 정상 인코딩 route 회귀를 실행했다.
|
||
- [x] RED/GREEN/REFACTOR, focused·app 회귀·정적/build 결과를 Progress와 Phase 1 리뷰에 누적했다. malformed URL 수동 확인은 최종 browser 확인 범위로 남긴다.
|
||
- **실행 명령:** `npm run test:run -- src/app/browser-location.test.ts src/app/App.test.tsx`; `npm run test:run -- src/app`; `npm run typecheck`; `npm run lint`; `npm run build:dev`; `npm run build:prod`.
|
||
- **기대 결과:** 모든 명령 exit 0, malformed encoding 예외 0건, `/ai-characters` 안전 fallback, 정상 route 회귀 0건.
|
||
|
||
**P1-R15 수정 검증 기록 (2026-07-31):**
|
||
|
||
- RED: `npm run test:run -- src/app/App.test.tsx` — 1 failed file / 3 failed / 8 passed. `/ai-characters/%`, `/ai-characters/%E0%A4%A/audio-contents/1`, `/ai-characters/101/series/%E0%A4%A`가 각각 `URIError: URI malformed`로 실패했다.
|
||
- GREEN: `browser-location.ts`에 `decodeRouteSegment` helper를 추가하고 모든 route param decode가 malformed segment에서 `null`을 반환하게 했다. `npm run test:run -- src/app/browser-location.test.ts src/app/App.test.tsx` — 2 files / 12 tests passed.
|
||
- REFACTOR/회귀: `rg -n "decodeURIComponent" src/app/browser-location.ts` — helper 내부 1건만 남았다. `npm run test:run -- src/app` — 5 files / 37 tests passed.
|
||
- 정적 검증: LSP diagnostics `browser-location.ts`, `App.test.tsx` 오류 0건. `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0. build는 기존 500kB chunk warning을 표시했다.
|
||
- 리뷰어 게이트: P1-R15 review에서 malformed route 예외 처리, helper 단일화, scope 제한, 문서 기록이 기준을 충족해 `APPROVED` 판정을 받았다.
|
||
- 수동 확인: malformed URL browser 직접 입력 확인은 전체 Task 구현 후 필요 시 수행하는 E2E/수동 QA 범위로 남긴다.
|
||
- **수동 확인:** 로그인/비로그인 상태에서 `/ai-characters/%`를 직접 열어 white screen 없이 안전한 화면으로 정규화되는지 확인한다.
|
||
|
||
### Task R1.16 — malformed route의 보호 probe 경계 복구
|
||
|
||
**Goal 실행 `P1-R16`:** decode할 수 없는 percent-encoding route를 canonical Character 목록 snapshot으로 정규화해 기존 ADMIN 보호 probe를 거친 뒤에만 shell을 렌더한다.
|
||
|
||
- **연결 리뷰:** [Phase 1 현재 리뷰](./reviews/phase1-platform-auth-shared-ui.md) — `REV-P1-022`
|
||
- **시작 조건:** `P1-R15` 완료 기록과 `readRoutePath()`가 regex에 맞는 malformed path를 raw `RoutePath`로 반환해 `isAiCharactersRoute()`가 false가 되는 재리뷰 근거.
|
||
- **완료 증거:** malformed 단일·복합 param route가 보호 probe 403에서 shell을 노출하지 않고 `/access-denied`로 전환되는 실패 test, canonical fallback snapshot 최소 수정, 정상 인코딩·create/edit·하위 route 회귀와 Phase 1 Gate 기록.
|
||
- **Files:** Modify: `src/app/browser-location.ts`, `src/app/App.protected-shell.test.tsx`; Test: `src/app/browser-location.test.ts`.
|
||
- **Interfaces:** route regex 일치 뒤 필수 segment 중 하나라도 decode 실패하면 `readRoutePath()`는 `routePaths.aiCharacters`를 반환한다. 정상 encoded route와 `navigateTo` API는 유지한다.
|
||
- **범위 밖:** router dependency, 404 page, backend ID 제약, 보호 probe 자체 재설계.
|
||
- **Phase 1 Gate 연결:** malformed 401/403 focused test, app/auth/api 회귀, `typecheck`·`lint`·개발/운영 build가 모두 통과하기 전에는 완료하지 않는다.
|
||
|
||
- [x] **RED:** ADMIN session과 malformed 단일 route에서 보호 probe 403을 반환할 때 현재 shell/list fallback이 먼저 렌더되는 실패 test를 작성했다.
|
||
- [x] **GREEN:** `readRoutePath()`의 route 후보를 반환하기 전에 `isAiCharactersRoute()`를 통과시키고 실패 시 Character 목록 snapshot으로 귀결시킨다.
|
||
- [x] **REFACTOR:** 정상 create/edit route parser를 유지하고 별도 auth abstraction이나 route table 재설계를 추가하지 않았다.
|
||
- [x] RED/GREEN/REFACTOR와 focused 결과를 Progress와 Phase 1 리뷰에 누적했다. Phase 1 정적/build Gate는 `P9-R11`, `P10-R10`까지 완료한 뒤 사용자 지시에 따라 통합 실행한다.
|
||
- **실행 명령:** `npm run test:run -- src/app/browser-location.test.ts src/app/App.protected-errors.test.tsx src/app/App.test.tsx`; `npm run test:run -- src/app src/features/auth src/shared/api`; `npm run typecheck`; `npm run lint`; `npm run build:dev`; `npm run build:prod`.
|
||
- **기대 결과:** 모든 명령 exit 0, malformed route의 probe 우회·protected shell 선노출 0건, 401 login·403 access-denied 전환과 정상 route 회귀 0건.
|
||
- **수동 확인:** stale ADMIN session으로 `/ai-characters/%`를 직접 열고 보호 probe가 끝나기 전 shell이 보이지 않으며 403 응답에서 접근 거부 화면으로 이동하는지 확인한다.
|
||
|
||
**P1-R16 수정 검증 기록 (2026-07-31):**
|
||
|
||
- RED: `npm run test:run -- src/app/App.protected-shell.test.tsx` — 1 failed / 7 passed. 신규 malformed route test가 기대한 `보호 route 확인 중` 대신 `AI 캐릭터 목록을 불러오는 중`을 받아 shell/list fallback 선노출을 재현했다.
|
||
- GREEN/REFACTOR: `readRoutePath()`가 route regex 후보를 raw path로 반환하기 전에 `isAiCharactersRoute()`를 통과시키고, decode 실패로 보호 route 판정이 불가능하면 canonical `/ai-characters` snapshot으로 귀결되게 했다. 새 router dependency, auth abstraction, route table 재설계는 추가하지 않았다.
|
||
- Focused 검증: `npm run test:run -- src/app/App.protected-shell.test.tsx src/app/browser-location.test.ts` — 2 files / 9 tests passed. 수동 QA와 E2E는 사용자 지시에 따라 전체 Task 구현 후 필요 시 실행한다.
|
||
- 통합 검증: `npm run test:run` — 81 files / 411 tests passed. `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0.
|
||
|
||
### Task R1.17 — malformed route 인가 회귀 matrix·focused Gate 보강
|
||
|
||
**Goal 실행 `P1-R17`:** `P1-R16`의 canonical fallback을 단일·복합 malformed route와 401/403 전체 인가 경계에서 고정하고 실제 regression spec이 focused Gate에 포함되게 한다.
|
||
|
||
- **연결 리뷰:** [Phase 1 현재 리뷰](./reviews/phase1-platform-auth-shared-ui.md) — `REV-P1-023`
|
||
- **시작 조건:** `P1-R16` 제품 코드는 focused·full unit에서 통과하지만 신규 test는 `/ai-characters/%`+403 한 건뿐이고 Task 실행 명령은 `App.protected-shell.test.tsx`를 누락한 재리뷰 근거.
|
||
- **완료 증거:** malformed 단일·Audio/Series 복합 route의 401/403 matrix에서 probe pending 중 shell 미노출, 401 login·session 제거, 403 access-denied·session 유지와 정상 encoded route 회귀, 실제 regression spec을 포함한 focused 명령 결과.
|
||
- **Files:** Modify: `src/app/App.protected-shell.test.tsx`, `docs/20260725_AI캐릭터관리자웹/plan-task.md`, `docs/20260725_AI캐릭터관리자웹/reviews/phase1-platform-auth-shared-ui.md`; Test: 기존 `src/app/App.test.tsx`, `src/app/App.protected-errors.test.tsx`, `src/app/browser-location.test.ts`.
|
||
- **Interfaces:** 제품 route parser와 App 보호 probe는 변경하지 않는다. test는 기존 MSW handler·session helper를 재사용하고 status/path matrix만 확장한다. matrix가 실제 제품 결함을 재현할 때만 root-cause 코드 수정을 이 Task에 포함한다.
|
||
- **범위 밖:** router dependency, 별도 auth abstraction, backend ID 제약, E2E browser 확대.
|
||
- **Phase 1 Gate 연결:** malformed 인가 matrix, app/auth/api 회귀, `typecheck`·`lint`·개발/운영 build가 모두 통과하기 전에는 완료하지 않는다.
|
||
|
||
- [x] **TDD 예외 사유:** root-cause 제품 수정은 `P1-R16`에서 이미 GREEN이며 이번 산출물은 누락된 regression matrix와 실행 명령이다. 현재 test inventory와 focused 명령 누락을 RED 대체 증거로 사용했다.
|
||
- [x] 단일·복합 malformed path × 401/403을 data-driven test로 고정하고 pending 중 protected shell·logout 미노출과 최종 auth 전환을 검증했다.
|
||
- [x] `P1-R16` 실행 명령에 `App.protected-shell.test.tsx`를 포함하고 정상 encoded/create/edit route 회귀를 함께 실행했다.
|
||
- [x] focused·Phase 1 회귀·정적/build 결과와 실제 test 수를 Progress와 Phase 1 리뷰에 누적했다.
|
||
- **대체 검증 방법:** 현재 test matrix inventory, 필요 시 guard negative-control, focused/full unit과 정적/build Gate.
|
||
- **실행 명령:** `npm run test:run -- src/app/App.protected-shell.test.tsx src/app/App.protected-errors.test.tsx src/app/App.test.tsx src/app/browser-location.test.ts`; `npm run test:run -- src/app src/features/auth src/shared/api`; `npm run typecheck`; `npm run lint`; `npm run build:dev`; `npm run build:prod`; `git diff --check`.
|
||
- **기대 결과:** 모든 명령 exit 0, malformed 단일·복합 401/403 matrix 누락 0건, probe 전 shell·logout 노출 0건, 401/403·정상 route 회귀 0건.
|
||
- **수동 확인:** 없음. 보호 경계는 deterministic MSW pending/status matrix로 검증하며 실제 stale ADMIN 개발 API 확인은 별도 수동 QA다.
|
||
|
||
**P1-R17 수정 검증 기록 (2026-07-31):**
|
||
|
||
- RED 대체: `REV-P1-023`에서 `/ai-characters/%`+403 한 건과 목록 heading만 보는 복합 malformed route test, focused 명령의 `App.protected-shell.test.tsx` 누락을 확인했다. 제품 guard는 `P1-R16`에서 이미 GREEN이라 production code는 변경하지 않았다.
|
||
- GREEN focused: `src/app/App.protected-shell.test.tsx`에 단일·Audio 복합·Series 복합 malformed path × 401/403 matrix를 추가했다. `npm run test:run -- src/app/App.protected-shell.test.tsx` — 1 file / 14 tests passed.
|
||
- REFACTOR/회귀: `npm run test:run -- src/app/App.protected-shell.test.tsx src/app/App.protected-errors.test.tsx src/app/App.test.tsx src/app/browser-location.test.ts` — 4 files / 40 tests passed. `npm run test:run -- src/app src/features/auth src/shared/api` — 13 files / 94 tests passed.
|
||
- 정적/build: `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0. build는 기존 500kB 초과 chunk warning만 표시했다. E2E와 실제 stale ADMIN 개발 API 수동 QA는 사용자 지시에 따라 전체 Task 이후 필요 시/credential 준비 시로 유지한다.
|
||
|
||
---
|
||
|
||
## Phase 2. 개발 전용 Mock Preview 기반
|
||
|
||
**목표:** backend endpoint 구현 전에도 제공된 API Contract 범위의 최종 UI를 브라우저에서 확인하고, 명시적 전환으로 실제 개발 API와 같은 frontend code를 사용한다.
|
||
|
||
**Phase Goal `P2`:** Task 2.1 → 2.3과 Phase 2 Gate로 browser MSW 기반 mock preview와 server mode 경계를 완성한다.
|
||
|
||
- **시작 조건:** `P1-GATE` 완료.
|
||
- **완료 조건:** `P2-T1`~`P2-T3`, `P2-GATE` 완료. mock/server 분리와 production 차단이 자동 test와 E2E로 고정됨.
|
||
- **현재 상태:** 완료. `P2-T1`~`P2-T3`, `P2-GATE`, `P2-R1`~`P2-R16`의 구현·수정·검증이 끝났으며 Phase 3 진행 가능.
|
||
|
||
**요구사항:** `MOCK-001~009`, PRD `11.1~11.2`, `12`, `13`.
|
||
|
||
**주요 Files:**
|
||
|
||
- Modify: `package.json`, `.env.example`, `src/shared/config/env.ts`, `src/shared/config/env.test.ts`
|
||
- Modify: `src/app/App.tsx`, `src/main.tsx`, `vite.config.ts`, `playwright.config.ts`
|
||
- Modify: `src/app/App.test.tsx`, `src/app/admin-pages.tsx`, `src/app/browser-location.ts`
|
||
- Create: `src/app/protected-admin-shell.tsx`
|
||
- Create: `src/shared/mocks/{browser,handlers,contract}.ts`
|
||
- Create: `src/shared/mocks/__tests__/{mode-boundary,auth-handlers,mock-preview-docs,production-graph}.test.ts`
|
||
- Create: `src/shared/mocks/browser.test.ts`
|
||
- Create: `src/shared/ui/mock-mode-banner.tsx`
|
||
- Create: `src/shared/ui/__tests__/mock-mode-banner.test.tsx`
|
||
- Create: `public/mockServiceWorker.js`
|
||
- Create: `tests/e2e/{mock-mode-boundary,mock-preview-shell,server-mode-boundary}.spec.ts`
|
||
- Modify when implementation is complete: `README.md`, `docs/agent-guide/{environment,scripts}.md`
|
||
|
||
### Task 2.1 explicit API mode·production 경계
|
||
|
||
**Goal 실행 `P2-T1`:** `server | mock` mode와 dev-only browser worker bootstrap을 구성하고 자동 fallback·production mock을 차단한다.
|
||
|
||
- **시작 조건:** `P1-GATE` 완료, PRD `MOCK-001~006` 확인.
|
||
- **완료 증거:** env/script/bootstrap focused test, `dev` server 기본값, `dev:mock` explicit mode, production mock 거부와 404 no-fallback test.
|
||
- **범위 밖:** Character 이후 도메인 fixture와 실제 backend endpoint 구현.
|
||
|
||
- [x] `VITE_API_MODE=server | mock`을 검증하고 누락 기본값은 `server`로 고정하는 실패 test를 작성한다.
|
||
- [x] `npm run dev:mock`만 Vite mock mode와 browser worker를 시작하고 기본 `npm run dev`는 실제 `VITE_API_BASE_URL`을 사용하는 script test를 작성한다.
|
||
- [x] Playwright가 같은 spec을 explicit mode로 실행할 수 있도록 `e2e:mock`과 server mode 명령을 정의하고 각 webServer 환경을 test한다.
|
||
- [x] `import.meta.env.DEV && apiMode === "mock"`일 때만 browser module을 dynamic import하고 worker 준비 후 React를 mount한다.
|
||
- [x] production mode에서 `mock`을 설정하면 build/start 전에 설명 가능한 오류로 거부하고 worker·fixture가 production graph에서 실행되지 않는 test를 작성한다.
|
||
- [x] `server` mode의 404·network error가 browser mock을 시작하거나 응답을 교체하지 않는 test를 작성한다.
|
||
- [x] worker의 unhandled request 정책은 error로 두어 누락된 handler가 실제 backend로 조용히 통과하지 않게 한다.
|
||
|
||
**P2-T1 구현 검증 기록 (2026-07-27):**
|
||
|
||
- RED: `npm run test:run -- src/shared/config/env.test.ts src/shared/mocks/__tests__/mode-boundary.test.ts`는 mode/default/script 미구현으로 2 files 중 5 tests가 기대대로 실패했고, `VITE_API_MODE=mock npm run e2e -- tests/e2e/mock-mode-boundary.spec.ts`는 worker 미등록과 unhandled 정책 미구현으로 8 tests가 실패했다.
|
||
- GREEN focused: `npm run test:run -- src/shared/config/env.test.ts src/shared/mocks/browser.test.ts src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/api/__tests__/client.test.ts`는 4 files / 18 tests 통과, `npm run e2e:mock -- tests/e2e/mock-mode-boundary.spec.ts`는 4 browser projects 통과, `npm run e2e -- tests/e2e/server-mode-boundary.spec.ts`는 4 browser projects 통과했다.
|
||
- Production boundary: `VITE_API_MODE=mock npm run build:prod`는 guard 추가 전에는 부당하게 성공했고, guard 추가 후 `VITE_API_MODE=mock is only available during development` 오류로 기대대로 거부됐다.
|
||
- Quality: `npm run typecheck`, `npm run lint`, `npm run test:run`(31 files / 117 tests), `npm run build:dev`, `npm run build:prod`, `git diff --check`를 성공했다. Playwright로 `http://127.0.0.1:8888/login`과 `http://127.0.0.1:8889/login`을 직접 열어 server mode는 worker registration 0건, mock mode는 `/mockServiceWorker.js` controller 등록을 확인했고, QA용 dev server 포트 `8888`·`8889`가 비었음을 확인했다.
|
||
|
||
**P2-T1 리뷰 보강 기록 (2026-07-27):**
|
||
|
||
- RED: `npm run test:run -- src/shared/mocks/__tests__/production-graph.test.ts src/shared/mocks/__tests__/mode-boundary.test.ts`는 production build 산출물에 `mockServiceWorker.js`가 복사되어 실패했고, `npm run e2e -- tests/e2e/server-mode-boundary.spec.ts`는 404·network error 후 no-fallback 증거가 없어 8 tests가 실패했다.
|
||
- GREEN: production mode에서 `publicDir` 복사를 끄고 production graph test를 실제 `NODE_ENV=production` build 조건으로 맞춘 뒤 같은 focused unit은 2 files / 3 tests 통과했다. server mode E2E는 404·network error 요청 후에도 browser MSW registration 0건임을 확인해 4 browser projects / 12 tests 통과했다.
|
||
- Re-review 보강: production graph test가 output file path까지 검사하도록 보강했고, server 404·network error는 오류 alert 표시 후 worker registration 0건을 확인하도록 `App`의 route error 표시 조건을 수정했다. `npm run test:run -- src/shared/mocks/__tests__/production-graph.test.ts src/shared/mocks/__tests__/mode-boundary.test.ts src/app/App.test.tsx`는 3 files / 16 tests 통과했고, `npm run e2e -- tests/e2e/server-mode-boundary.spec.ts`는 4 browser projects / 12 tests 통과했다.
|
||
- Guide sync: `VITE_API_MODE`, `dev:mock`, `e2e:mock`, production mock 거부 기준을 `docs/agent-guide/environment.md`와 `docs/agent-guide/scripts.md`에 반영했다.
|
||
|
||
### Task 2.2 공통 preview session·fixture store
|
||
|
||
**Goal 실행 `P2-T2`:** production API 경계를 그대로 사용하는 deterministic auth fixture, in-memory store와 mock mode 안내를 완성한다.
|
||
|
||
- **시작 조건:** `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.
|
||
|
||
- [x] fixture는 정규화 `ApiResponse<T>`와 실제 endpoint·header·body contract를 사용하고 별도 mock DTO를 만들지 않는다.
|
||
- [x] ADMIN login·logout, 401·403 fixture와 새로고침 시 seed로 초기화되는 in-memory store를 만든다.
|
||
- [x] mock mutation은 같은 store의 목록·상세 응답을 갱신하고 browser storage에 domain fixture를 영구 저장하지 않는다.
|
||
- [x] mock mode임을 지속적으로 표시하는 접근 가능한 banner와 server mode에서 banner가 없는 test를 작성한다.
|
||
- [x] JWT·password·signed URL·multipart body가 mock logger와 storage에 기록되지 않는 test를 작성한다.
|
||
- [x] handler와 fixture가 `src/shared/test/server.ts`의 Node test lifecycle을 변경하지 않고 필요한 contract factory만 공유하게 한다.
|
||
|
||
**P2-T2 실행 기록 (2026-07-27):**
|
||
|
||
- RED: `npm run test:run -- src/shared/mocks/__tests__/auth-handlers.test.ts src/shared/ui/__tests__/mock-mode-banner.test.tsx src/app/App.test.tsx src/shared/mocks/browser.test.ts`는 `@/shared/mocks/handlers`·`@/shared/ui/mock-mode-banner` 미구현, App mock banner 부재, browser worker handler 미등록 기대 실패로 4 files 중 5 failures가 발생했다. `npm run e2e:mock -- tests/e2e/mock-preview-shell.spec.ts --project=chromium`은 mock login handler 부재로 `/login`에 머물러 기대대로 실패했다.
|
||
- GREEN: `createMockHandlers(createMockStore())`가 `POST /admin/member/login`, `POST /member/logout`, `GET /api/v2/admin/ai-characters?page=0&size=20`을 정규화 envelope로 처리하고 logout mutation 후 같은 store에서 token을 폐기하며 새 store 생성 시 seed로 초기화한다. 리뷰 보강으로 logout 후 재login 시 같은 store에서 ADMIN token이 다시 활성화되도록 고정했다. 같은 focused unit 명령은 4 files / 25 tests 통과했고, `npm run e2e:mock -- tests/e2e/mock-preview-shell.spec.ts`는 4 browser projects / 8 tests 통과했다.
|
||
- Sensitive data/storage: handler는 logger를 추가하지 않았고, focused test에서 login password/JWT 요청 후 `localStorage`, mock `sessionStorage`, cookie, IndexedDB, console log/warn/error에 fixture domain data와 민감값을 기록하지 않음을 확인했다. `src/shared/test/server.ts` lifecycle은 변경하지 않았다.
|
||
|
||
### Task 2.3 Mock Preview 반응형·문서화
|
||
|
||
**Goal 실행 `P2-T3`:** mock mode의 browser 접근성·반응형과 실행 문서를 검증해 사용자가 최종 UI 확인 mode를 명확히 선택하게 한다.
|
||
|
||
- **시작 조건:** `P2-T1`, `P2-T2` 완료.
|
||
- **완료 증거:** 320px·desktop mock shell E2E, axe 결과, README와 environment/scripts 가이드의 실제 명령 동기화 기록.
|
||
- **범위 밖:** 도메인별 최종 UI와 server integration 완료 주장.
|
||
|
||
- [x] mock banner가 320px·200% zoom에서 핵심 control을 가리지 않고 axe critical·serious 위반이 없는지 E2E로 확인한다.
|
||
- [x] README에 `npm run dev`와 `npm run dev:mock`, mode 차이, mock data reset, production 금지와 no-auto-fallback을 기록한다.
|
||
- [x] 구현이 완료된 뒤 `docs/agent-guide/environment.md`와 `scripts.md`에 실제 환경 변수와 명령을 추가한다.
|
||
- [x] 후속 도메인 Phase가 handler·fixture·mock E2E를 소유한다는 규칙을 문서화한다.
|
||
|
||
**P2-T3 실행 기록 (2026-07-27):**
|
||
|
||
- RED: `npm run test:run -- src/shared/mocks/__tests__/mock-preview-docs.test.ts src/shared/mocks/__tests__/mode-boundary.test.ts`는 README에 실제 `dev`/`dev:mock`/`e2e`/`e2e:mock` script, mode 차이, mock data reset, production 금지, no-auto-fallback 기록이 없고 `docs/agent-guide/environment.md`와 `scripts.md`에 reset·domain handler/fixture/mock E2E 소유 규칙이 없어 2 tests가 기대대로 실패했다. `npm run e2e:mock -- tests/e2e/mock-preview-shell.spec.ts --project=chromium`은 새 320px·200% zoom 및 axe 확인을 포함해 5 tests가 통과해 기존 mock shell UI는 문서 보강 전에도 요구 접근성 동작을 만족함을 확인했다.
|
||
- GREEN: README에 `npm run dev`/`dev:mock`/`e2e`/`e2e:mock`의 실제 명령, server mode와 mock mode 차이, mock data reset, production mock 거부, no-auto-fallback, 후속 도메인 handler·fixture·mock E2E 소유 규칙을 추가했다. `docs/agent-guide/environment.md`에는 `VITE_API_MODE=server | mock`, reset, production 금지, no-auto-fallback을 동기화하고 `docs/agent-guide/scripts.md`에는 실제 script와 domain ownership rule을 동기화했다. 같은 focused unit 명령은 2 files / 4 tests 통과했고, `npm run e2e:mock -- tests/e2e/mock-preview-shell.spec.ts --project=chromium`은 5 tests 통과했다. 최종 확인으로 `npm run e2e:mock -- tests/e2e/mock-preview-shell.spec.ts`는 4 browser projects / 20 tests 통과, `npm run typecheck`, `npm run lint`, `git diff --check`도 통과했다.
|
||
|
||
### Phase 2 Gate
|
||
|
||
**Goal 실행 `P2-GATE`:** mock/server mode 경계, auth preview와 production 차단을 최종 판정한다.
|
||
|
||
- **시작 조건:** `P2-T1`~`P2-T3` 완료.
|
||
- **완료 증거:** 아래 명령과 Expected 통과 및 Phase 2 Progress 기록.
|
||
- **범위 밖:** mock 성공을 실제 도메인 server integration 성공으로 간주하는 것.
|
||
|
||
```bash
|
||
npm run test:run -- src/shared/config src/shared/mocks src/shared/ui/__tests__/mock-mode-banner.test.tsx
|
||
npm run e2e:mock -- tests/e2e/mock-preview-shell.spec.ts
|
||
npm run typecheck
|
||
npm run lint
|
||
npm run build:dev
|
||
npm run build:prod
|
||
```
|
||
|
||
**Expected:** `npm run dev:mock`에서는 ADMIN login → protected shell과 mock banner가 동작하고 실제 backend 요청은 0건이다. 기본 server mode는 실제 API 오류를 그대로 처리하며 production build는 browser mock을 활성화하지 않는다.
|
||
|
||
**P2-GATE 실행 기록 (2026-07-27):**
|
||
|
||
- `npm run test:run -- src/shared/config src/shared/mocks src/shared/ui/__tests__/mock-mode-banner.test.tsx` — 7 files / 20 tests 통과.
|
||
- `npm run e2e:mock -- tests/e2e/mock-preview-shell.spec.ts` — 4 browser projects / 20 tests 통과. mock login → protected shell, logout 후 재login, 320px·200% zoom banner/control, axe critical·serious 0을 확인했다.
|
||
- `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0.
|
||
- LSP diagnostics: `src/shared/mocks/__tests__/mock-preview-docs.test.ts` clean, `tests/e2e` directory 6 files / 0 diagnostics. 단일 `mock-preview-shell.spec.ts` LSP 호출은 3초 fresh diagnostics timeout이었으나 directory diagnostics와 typecheck/lint로 보완했다.
|
||
- 판정: Phase 2 mock/server mode 경계, auth preview, production build 경계, mock preview 반응형·접근성·문서화 완료. mock 통과는 후속 도메인 server integration 완료로 간주하지 않는다.
|
||
|
||
### Task R2.1 — 보호 route 검증 실패 시 fail-closed 복구
|
||
|
||
**Goal 실행 `P2-R1`:** Phase 2 no-auto-fallback 오류 UI가 Phase 1의 보호 shell 비노출 경계를 우회하지 않게 하고 회귀를 방지한다.
|
||
|
||
- 연결 리뷰: [Phase 2 리뷰](./reviews/review-phase-2.md) — `REV-P2-001`
|
||
- 시작 조건:
|
||
- 저장된 ADMIN session으로 `/ai-characters`에 진입한 뒤 probe가 404 또는 network error로 실패할 때 보호 shell이 노출되는 현재 동작을 실패 test로 재현한다.
|
||
- 완료 증거:
|
||
- probe 성공 전과 401·403·404·network error에서 보호 shell 비노출
|
||
- 404·network error는 mock fallback 없이 보호 shell 밖의 오류 UI로 표시
|
||
- focused App test, server mode boundary E2E, auth E2E와 P2-GATE 실행 기록
|
||
- 범위 밖:
|
||
- 인증 방식, session 저장 방식 또는 API client 전면 교체
|
||
- Phase 3 Character 화면 구현
|
||
|
||
- [x] 404·network error와 이전 오류 뒤 새 session probe pending에서 보호 shell이 노출되는 실패 test를 추가한다.
|
||
- [x] 성공한 현재 token probe만 보호 shell을 열고 실패 오류는 shell 밖에서 표시하는 최소 상태 전이를 구현한다.
|
||
- [x] `src/app/App.test.tsx`, `tests/e2e/server-mode-boundary.spec.ts`, 기존 auth E2E와 P2-GATE를 실행한다.
|
||
- [x] 결과를 plan-task.md 검증 기록과 [Phase 2 리뷰 문서](./reviews/review-phase-2.md)에 누적한다.
|
||
|
||
**P2-R1 수정 검증 기록 (2026-07-27):**
|
||
|
||
- RED: `npm run test:run -- src/app/App.test.tsx src/shared/mocks/__tests__/auth-handlers.test.ts` — 404·network error에서 보호 shell `banner/main`이 렌더되어 App test 2건이 기대대로 실패했다.
|
||
- GREEN focused: `npm run test:run -- src/app/App.test.tsx src/shared/mocks/__tests__/auth-handlers.test.ts src/shared/mocks/browser.test.ts` — 3 files / 27 tests 통과.
|
||
- Surface: `npm run e2e -- tests/e2e/server-mode-boundary.spec.ts` — 4 browser projects / 12 tests 통과. 404·network error 후 mock worker 0건, 보호 shell `main` 0건, logout button 0건을 확인했다.
|
||
- Regression: `npm run e2e -- tests/e2e/auth.spec.ts tests/e2e/accessibility-shell.spec.ts` — 4 browser projects / 16 tests 통과.
|
||
- Reviewer blocker 보강: 같은 token 재사용 logout → login 뒤 probe 실패 시 이전 검증 token이 남는 회귀를 추가로 확인했다. RED `npm run test:run -- src/app/App.test.tsx` — 1 test failed. GREEN 같은 command — 1 file / 19 tests 통과. 보강 후 `npm run test:run -- src/app/App.test.tsx src/shared/mocks/__tests__/auth-handlers.test.ts src/shared/mocks/browser.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 4 files / 31 tests 통과, `npm run e2e -- tests/e2e/server-mode-boundary.spec.ts` — 12 tests 통과, `npm run e2e:mock -- tests/e2e/mock-preview-shell.spec.ts` — 20 tests 통과.
|
||
|
||
#### create_goal objective 초안 — P2-R1
|
||
|
||
- objective: P2-R1의 확정 review 항목 REV-P2-001을 수정하고 Phase 1 보호 route 경계의 회귀를 방지한다. plan-task.md에 추가된 Task R2.1만 수행한다.
|
||
- 완료 조건: 404·network error를 포함한 probe 비성공 상태에서 보호 shell 비노출, 독립 오류 UI, focused test, server/auth E2E, P2-GATE와 누적 기록이 모두 확인된다.
|
||
- 금지 조건: 인증·session 아키텍처 교체나 Phase 3 Character 구현을 포함하지 않는다.
|
||
- 중단 조건: 보호 route 확인 endpoint 자체가 변경됐다는 외부 계약이 확인되면 계약 문서를 먼저 갱신한다.
|
||
|
||
### Task R2.2 — Mock Preview API origin·JWT 오류 계약 복구
|
||
|
||
**Goal 실행 `P2-R2`:** browser mock handler를 설정된 API base URL에만 결합하고 invalid·revoked JWT logout을 계약 status로 반환한다.
|
||
|
||
- 연결 리뷰: [Phase 2 리뷰](./reviews/review-phase-2.md) — `REV-P2-002`, `REV-P2-003`
|
||
- 시작 조건:
|
||
- 잘못된 origin의 login이 200을 반환하고 invalid·revoked JWT logout이 200을 반환하는 현재 동작을 각각 실패 test로 재현한다.
|
||
- 완료 증거:
|
||
- 설정된 `VITE_API_BASE_URL`의 정확한 URL·method만 handler가 처리하고 다른 origin은 `onUnhandledRequest: "error"` 경계에 남음
|
||
- invalid·revoked JWT logout은 Phase 1에서 검증한 기존 인증 계약의 401 오류 envelope를 반환
|
||
- 정상 login → logout → login 흐름과 403 fixture 회귀 없음
|
||
- focused handler test, mock preview E2E와 P2-GATE 실행 기록
|
||
- 범위 밖:
|
||
- 계약 미제공 endpoint·DTO·회원 role 정책 추정
|
||
- 도메인별 fixture 선행 구현
|
||
|
||
- [x] wrong-origin login과 invalid·두 번째 logout의 현재 200 응답을 실패 test로 고정한다.
|
||
- [x] handler 생성 시 API base URL을 주입해 production endpoint URL에만 매칭한다.
|
||
- [x] token access 상태를 확인한 뒤 logout store를 변경하고 invalid·revoked token에 401을 반환한다.
|
||
- [x] auth handler focused test, mock preview E2E와 P2-GATE를 실행한다.
|
||
- [x] 결과를 plan-task.md 검증 기록과 [Phase 2 리뷰 문서](./reviews/review-phase-2.md)에 누적한다.
|
||
|
||
**P2-R2 수정 검증 기록 (2026-07-27):**
|
||
|
||
- RED: `npm run test:run -- src/app/App.test.tsx src/shared/mocks/__tests__/auth-handlers.test.ts` — wrong-origin login이 200, invalid JWT logout이 200으로 응답해 auth handler test 2건이 기대대로 실패했다.
|
||
- GREEN focused: `npm run test:run -- src/app/App.test.tsx src/shared/mocks/__tests__/auth-handlers.test.ts src/shared/mocks/browser.test.ts` — 3 files / 27 tests 통과. `createMockHandlers(store, apiBaseUrl)` exact origin, invalid·revoked JWT 401, browser worker runtime env 주입을 확인했다.
|
||
- Surface: `npm run e2e:mock -- tests/e2e/mock-preview-shell.spec.ts` — 4 browser projects / 20 tests 통과. 정상 mock login → protected shell, logout 후 재login, 320px·200% zoom, axe critical·serious 0건을 확인했다.
|
||
- P2 focused Gate: `npm run test:run -- src/shared/config src/shared/mocks src/shared/ui/__tests__/mock-mode-banner.test.tsx` — 7 files / 23 tests 통과.
|
||
|
||
#### create_goal objective 초안 — P2-R2
|
||
|
||
- objective: P2-R2의 확정 review 항목 REV-P2-002와 REV-P2-003을 수정하고 mock/production API 계약 경계를 일치시킨다. plan-task.md에 추가된 Task R2.2만 수행한다.
|
||
- 완료 조건: exact API origin, invalid·revoked JWT 401, 기존 정상 auth preview, focused test, mock E2E, P2-GATE와 누적 기록이 모두 확인된다.
|
||
- 금지 조건: 계약 미제공 endpoint·DTO·회원 role 의미를 추정하거나 도메인 fixture를 추가하지 않는다.
|
||
- 중단 조건: logout의 non-ADMIN token 정책이 현재 계약만으로 판정되지 않으면 해당 경로는 확장하지 않고 백엔드 확인 항목으로 기록한다.
|
||
|
||
### Task R2.3 — Phase 번호 표시·구현 문서 정합성 복구
|
||
|
||
**Goal 실행 `P2-R3`:** Phase 2 완료 뒤 남은 사용자 표시와 plan의 Files·Progress를 실제 구현에 맞게 정렬한다.
|
||
|
||
- 연결 리뷰: [Phase 2 리뷰](./reviews/review-phase-2.md) — `REV-P2-004`, `REV-P2-005`
|
||
- 시작 조건:
|
||
- 보호 shell 빈 상태가 Character 연결 시점을 Phase 2로 표시하고, Phase 2 주요 Files와 하단 Progress가 실제 변경 경로·완료 기록과 다른 상태를 확인한다.
|
||
- 완료 증거:
|
||
- 빈 상태 문구와 관련 test가 Character 구현 Phase 3을 가리킴
|
||
- Phase 2 주요 Files가 실제 수정·생성·test 경로와 일치함
|
||
- 하단 검증 기록에 Phase 2 구현의 무엇을/왜/실제 명령·결과/남은 항목이 누적됨
|
||
- 문서 test, App focused test와 `git diff --check` 실행 기록
|
||
- 범위 밖:
|
||
- Phase 3 Character 기능 구현
|
||
- 기존 Phase 2 구현·Gate 기록 삭제 또는 덮어쓰기
|
||
|
||
- [x] 현재 Phase 2 문구와 Files·Progress 불일치를 test와 파일 존재 검사로 고정한다.
|
||
- [x] 사용자 표시를 Phase 3으로 바꾸고 Phase 2 주요 Files를 실제 경로로 갱신한다.
|
||
- [x] Phase 2 구현·Gate 검증을 하단 Progress에 기존 기록을 보존한 채 누적한다.
|
||
- [x] 문서/App focused test와 `git diff --check`를 실행한다.
|
||
- [x] 결과를 plan-task.md 검증 기록과 [Phase 2 리뷰 문서](./reviews/review-phase-2.md)에 누적한다.
|
||
|
||
**P2-R3 수정 검증 기록 (2026-07-27):**
|
||
|
||
- RED: `npm run test:run -- src/app/App.test.tsx src/shared/mocks/__tests__/mock-preview-docs.test.ts` — Phase 3 placeholder와 실제 Phase 2 Files·Progress 기록이 없어 2 files 중 3 tests가 기대대로 실패했다.
|
||
- GREEN focused: 같은 command — 2 files / 21 tests 통과.
|
||
- Final Gate: 1차 `npm run test:run` — 34 files / 135 tests 통과. Reviewer blocker 보강 후 최종 `npm run test:run` — 34 files / 136 tests 통과. `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod`, `git diff --check` 모두 성공.
|
||
- Production guard: `VITE_API_MODE=mock npm run build:prod` — 기대대로 `VITE_API_MODE=mock is only available during development` 오류로 거부됐다.
|
||
- LSP diagnostics: `src/app` directory 3 files / 0 diagnostics, `src/shared/mocks` directory 8 files / 0 diagnostics. `App.tsx` 단일 fresh diagnostics는 timeout이었고 directory diagnostics와 typecheck로 보완 확인했다.
|
||
|
||
#### create_goal objective 초안 — P2-R3
|
||
|
||
- objective: P2-R3의 확정 review 항목 REV-P2-004와 REV-P2-005를 수정하고 Phase 번호 표시와 계획 문서 추적성을 복구한다. plan-task.md에 추가된 Task R2.3만 수행한다.
|
||
- 완료 조건: 사용자 문구, 실제 Files, 하단 Progress, focused test와 문서 검증 기록이 서로 일치한다.
|
||
- 금지 조건: Phase 3 기능 구현이나 기존 완료 기록 삭제·덮어쓰기를 수행하지 않는다.
|
||
- 중단 조건: Phase 번호가 다시 변경되면 현재 기록을 지우지 않고 새 결정 기록을 먼저 추가한다.
|
||
|
||
### Task R2.4 — 동일 세션 재진입 fail-closed 복구
|
||
|
||
**Goal 실행 `P2-R4`:** 최초 보호 route 검증 성공 뒤 같은 session으로 route를 이탈·재진입해도 새 probe 성공 전에는 보호 shell을 다시 열지 않는다.
|
||
|
||
- 연결 리뷰: [Phase 2 리뷰](./reviews/review-phase-2.md) — `REV-P2-006`
|
||
- 시작 조건:
|
||
- 같은 `AuthSessionRecord`로 `/ai-characters` 성공 → `/login` 이탈 → `/ai-characters` 재진입 뒤 probe 실패 시 보호 shell이 노출되는 현재 동작을 실패 test로 재현한다.
|
||
- 완료 증거:
|
||
- 최초 성공 뒤 동일 session 재진입 pending·404·network·403에서 보호 shell 비노출
|
||
- focused App test, server boundary E2E와 P2 Gate 실행 기록
|
||
- 범위 밖:
|
||
- 인증/session 저장 방식 전면 교체
|
||
- Phase 3 Character 화면 구현
|
||
|
||
- [x] 같은 session route 이탈·재진입 뒤 pending·404 실패에서 보호 shell이 노출되는 실패 test를 추가한다.
|
||
- [x] route 이탈 또는 새 보호 route probe 시작 시 이전 검증 상태가 새 진입을 열지 못하게 최소 상태 전이를 구현한다.
|
||
- [x] focused App test와 server boundary E2E를 실행한다.
|
||
- [x] 결과를 plan-task.md 검증 기록과 [Phase 2 리뷰 문서](./reviews/review-phase-2.md)에 누적한다.
|
||
|
||
**P2-R4 수정 검증 기록 (2026-07-27):**
|
||
|
||
- RED: `npm run test:run -- src/app/App.test.tsx` — 동일 session `/login` 이탈 후 `/ai-characters` 재진입 404에서 보호 `main`이 남아 1 test가 기대대로 실패했다.
|
||
- GREEN focused: 같은 command — 1 file / 20 tests 통과. route visit key로 이전 보호 route 검증을 새 진입에 재사용하지 않게 했다.
|
||
- Refactor guard: `ProtectedAdminShell`을 `src/app/protected-admin-shell.tsx`로 분리해 `App.tsx`를 153 pure LOC로 낮췄고, App focused test와 lint가 통과했다.
|
||
|
||
### Task R2.5 — Auth fixture status·media type 계약 복구
|
||
|
||
**Goal 실행 `P2-R5`:** mock auth fixture가 API Contract의 비ADMIN status와 login media type 경계를 완화하지 않게 한다.
|
||
|
||
- 연결 리뷰: [Phase 2 리뷰](./reviews/review-phase-2.md) — `REV-P2-007`
|
||
- 시작 조건:
|
||
- 비ADMIN logout 401과 `text/plain` login 200을 실패 test로 재현한다.
|
||
- 완료 증거:
|
||
- 비ADMIN logout 403, invalid·revoked token logout 401 유지
|
||
- login은 `application/json`만 허용하고 지원하지 않는 media type은 415 오류 envelope 반환
|
||
- 정상 login/logout 회귀 없음
|
||
- 범위 밖:
|
||
- 제공되지 않은 login credential 정책과 회원 role 의미 확장
|
||
|
||
- [x] 비ADMIN logout 403과 non-JSON login 415 실패 test를 추가한다.
|
||
- [x] logout token access 분기와 login media type 검증을 계약 status에 맞춘다.
|
||
- [x] auth handler focused test와 mock auth preview 회귀를 실행한다.
|
||
- [x] 결과를 plan-task.md 검증 기록과 [Phase 2 리뷰 문서](./reviews/review-phase-2.md)에 누적한다.
|
||
|
||
**P2-R5 수정 검증 기록 (2026-07-27):**
|
||
|
||
- RED: `npm run test:run -- src/shared/mocks/__tests__/auth-handlers.test.ts` — 비ADMIN logout이 401, `text/plain` login이 200으로 응답해 2 tests가 기대대로 실패했다.
|
||
- GREEN focused: 같은 command — 1 file / 10 tests 통과. 비ADMIN logout은 403, invalid·revoked logout은 401 유지, unsupported media type login은 415로 고정했다.
|
||
|
||
### Task R2.6 — 모든 mock route state의 지속 안내 복구
|
||
|
||
**Goal 실행 `P2-R6`:** mock mode의 login·성공 shell·403·보호 오류 상태에서 Mock Preview 안내를 지속 표시한다.
|
||
|
||
- 연결 리뷰: [Phase 2 리뷰](./reviews/review-phase-2.md) — `REV-P2-008`
|
||
- 시작 조건:
|
||
- mock mode 403과 404/network 보호 오류 화면에서 banner가 사라지는 현재 동작을 실패 test로 재현한다.
|
||
- 완료 증거:
|
||
- mock mode login·성공 shell·403·404/network에서 banner 표시
|
||
- server mode에서는 banner 미표시 유지
|
||
- 접근성·320px 회귀 없음
|
||
- 범위 밖:
|
||
- 오류 화면 디자인 개편
|
||
|
||
- [x] 403과 보호 오류 화면의 mock banner 실패 test를 추가한다.
|
||
- [x] Mock Preview 안내를 모든 mock route state의 공통 상위 경계에 한 번만 배치한다.
|
||
- [x] focused App/banner test와 mock preview E2E를 실행한다.
|
||
- [x] 결과를 plan-task.md 검증 기록과 [Phase 2 리뷰 문서](./reviews/review-phase-2.md)에 누적한다.
|
||
|
||
**P2-R6 수정 검증 기록 (2026-07-27):**
|
||
|
||
- RED: `npm run test:run -- src/app/App.test.tsx` — mock mode access denied와 보호 route 오류 화면에서 `Mock Preview` status가 없어 2 tests가 기대대로 실패했다.
|
||
- GREEN focused: 같은 command — 1 file / 22 tests 통과. login·성공 shell·403·보호 오류가 공통 `RouteFrame`의 banner를 사용한다.
|
||
|
||
### Task R2.7 — Mode별 bare E2E 실행 경계 복구
|
||
|
||
**Goal 실행 `P2-R7`:** bare `npm run e2e`와 `npm run e2e:mock`이 각 mode에 유효한 spec만 수집·실행하게 한다.
|
||
|
||
- 연결 리뷰: [Phase 2 리뷰](./reviews/review-phase-2.md) — `REV-P2-009`
|
||
- 시작 조건:
|
||
- 두 bare E2E 명령의 `--list`가 같은 spec 집합을 수집하고 mock bare 실행이 실패하는 현재 동작을 실패 test로 재현한다.
|
||
- 완료 증거:
|
||
- bare `npm run e2e`와 `npm run e2e:mock`이 각 mode 유효 spec만 실행해 exit 0
|
||
- 목록·실행 contract test와 scripts 문서가 일치
|
||
- 범위 밖:
|
||
- Playwright config 복제와 CI 전면 재구성
|
||
|
||
- [x] package script 또는 Playwright 설정의 mode별 spec 경계 실패 test를 추가한다.
|
||
- [x] 가장 작은 script/config 변경으로 server 전용·mock 전용 spec 수집을 분리한다.
|
||
- [x] `--list`, bare E2E 실행과 docs sync test를 실행한다.
|
||
- [x] 결과를 plan-task.md 검증 기록과 [Phase 2 리뷰 문서](./reviews/review-phase-2.md)에 누적한다.
|
||
|
||
**P2-R7 수정 검증 기록 (2026-07-27):**
|
||
|
||
- RED: `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts` — bare `e2e` script가 mode별 spec 경계를 명시하지 않아 1 test가 기대대로 실패했다.
|
||
- GREEN focused: `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 2 files / 5 tests 통과.
|
||
- 목록 검증: `npm run e2e -- --list` — server 4 files / 32 tests, `npm run e2e:mock -- --list` — mock 2 files / 24 tests로 분리됐다.
|
||
- Surface: `npm run e2e` — 32 tests 통과, `npm run e2e:mock` — 24 tests 통과.
|
||
|
||
**P2-R4~P2-R7 통합 검증 기록 (2026-07-27):**
|
||
|
||
- `npm run test:run -- src/app/App.test.tsx src/shared/mocks/__tests__/auth-handlers.test.ts src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 4 files / 37 tests 통과.
|
||
- `npm run test:run` — 34 files / 141 tests 통과.
|
||
- `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0.
|
||
- `VITE_API_MODE=mock npm run build:prod` — 기대대로 `VITE_API_MODE=mock is only available during development`로 exit 1.
|
||
- `test ! -e dist/mockServiceWorker.js`, `git diff --check HEAD` — 모두 exit 0.
|
||
- LSP diagnostics: `src/app` directory 4 files / 0 diagnostics, `src/shared/mocks` directory 8 files / 0 diagnostics.
|
||
|
||
### Task R2.8 — Phase 2 완료 문서 추적성 복구
|
||
|
||
**Goal 실행 `P2-R8`:** `P2-R4`~`P2-R7` 이후의 실제 변경 경로·검증 범위와 Phase 2 plan/review 메타데이터를 일치시킨다.
|
||
|
||
- 연결 리뷰: [Phase 2 리뷰](./reviews/review-phase-2.md) — `REV-P2-010`
|
||
- 시작 조건:
|
||
- 3차 review 반영 전 `git diff HEAD --name-only`은 37개 경로인데 review 상단과 종료 판정은 36개로 기록된 상태였음을 확인한다.
|
||
- Phase 2 `주요 Files`가 `src/app/App.test.tsx`, `src/app/admin-pages.tsx`, `src/app/browser-location.ts`, `src/app/protected-admin-shell.tsx`를 포함하지 않는 상태를 확인한다.
|
||
- 완료 증거:
|
||
- Phase 2 `주요 Files`가 `P2-R4`~`P2-R7`까지의 실제 코드·test 경로와 일치함
|
||
- review 대상·working tree 경로 수·종료 판정과 3차 재검증 기록이 현재 범위와 일치함
|
||
- 완료된 회귀 Task의 Files·Interfaces와 검증 근거가 독립 실행자가 추측하지 않을 수준으로 기록됨
|
||
- 문서 대체 검증과 `git diff --check` 결과가 plan/review에 누적됨
|
||
- 범위 밖:
|
||
- 애플리케이션 코드·test·설정 변경
|
||
- 기존 완료 체크와 과거 검증 기록 삭제 또는 덮어쓰기
|
||
|
||
**Files:**
|
||
|
||
- Modify: `docs/20260725_AI캐릭터관리자웹/plan-task.md`
|
||
- Modify: `docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md`
|
||
- Test: 없음 — 이 Task는 완료 문서의 소유 경로와 검증 메타데이터만 정정한다.
|
||
|
||
**Interfaces:**
|
||
|
||
- Consumes: `git diff HEAD --name-only`, `P2-R4`~`P2-R7` 구현·검증 기록, `REV-P2-010`
|
||
- Produces: Phase 3 실행자와 reviewer가 사용할 최신 Phase 2 Files·review 범위·검증 기준
|
||
|
||
**TDD 예외 사유:** 실행 동작을 변경하지 않는 문서 정합성 수정이므로 실패 test를 추가하면 제품 동작과 무관한 문자열 고정 test만 늘어난다.
|
||
|
||
**대체 검증 방법:** 현재 변경 경로와 Phase 2 Files·review 범위를 직접 대조하고, review ID·goal 연결과 Markdown diff를 명령으로 검사한다.
|
||
|
||
- [x] Phase 2 `주요 Files`에 `P2-R4`~`P2-R7`의 실제 코드·test 경로를 추가한다.
|
||
- [x] review 상단의 대상·working tree 기준과 종료 판정을 3차 재검증 범위에 맞춘다.
|
||
- [x] 완료된 회귀 Task의 Files·Interfaces·검증 근거 누락을 기존 기록을 보존한 채 보완한다.
|
||
- [x] 아래 대체 검증을 실행하고 실제 결과를 plan/review에 누적한다.
|
||
|
||
**P2-R8 수정 검증 기록 (2026-07-27):**
|
||
|
||
- 대체 RED: `git diff HEAD --name-only`로 기존 tracked 변경 37개 경로를 확인했고, `rg -n 'P2-R(10|[1-9])|REV-P2-0(0[1-9]|1[0-3])|주요 Files|기준 commit 또는 working tree' docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md`에서 `REV-P2-010`과 `P2-R8`이 문서 추적성 미충족 상태로 남아 있음을 확인했다.
|
||
- GREEN: Phase 2 `주요 Files`에 `src/app/App.test.tsx`, `src/app/admin-pages.tsx`, `src/app/browser-location.ts`, `src/app/protected-admin-shell.tsx`를 추가하고, review 상단·요약·종료 판정·수정 후 검증 기록을 `P2-R8` 완료와 `P2-R9`~`P2-R10` 잔여 상태로 정렬했다.
|
||
- 검증: `git diff --check -- docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md` — exit 0.
|
||
|
||
**검증 기준:**
|
||
|
||
- **실행 명령:** `git diff HEAD --name-only`, `rg -n 'P2-R(10|[1-9])|REV-P2-0(0[1-9]|1[0-3])|주요 Files|기준 commit 또는 working tree' docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md`, `git diff --check -- docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md`
|
||
- **기대 결과:** 실제 변경 경로와 문서의 소유 Files·review 범위 차이 0건, review ID와 goal 연결 누락 0건, whitespace 오류 0건.
|
||
- **수동 확인:** review 상단·발견 요약·plan 전환·종료 판정·수정 후 검증 기록이 모두 `P2-R1`~`P2-R10`의 현재 상태를 같은 의미로 표시한다.
|
||
|
||
### Task R2.9 — Mock Preview 모바일 메뉴의 반응형·inert 경계 복구
|
||
|
||
**Goal 실행 `P2-R9`:** Mock Preview 모바일 메뉴가 열린 상태에서도 banner를 배경 inert 경계에 포함하고 desktop breakpoint 전환 시 숨은 overlay와 inert 상태를 함께 해제한다.
|
||
|
||
- 연결 리뷰: [Phase 2 리뷰](./reviews/review-phase-2.md) — `REV-P2-011`, `REV-P2-012`
|
||
- 시작 조건:
|
||
- 320px에서 모바일 메뉴를 열면 Mock Preview banner의 `closest('[inert]')`가 `null`인 현재 동작을 browser 재현으로 확인한다.
|
||
- 같은 상태에서 viewport를 1,200px로 넓히면 overlay는 `display:none`이지만 `main`의 inert ancestor와 `aria-hidden=true`가 남는 현재 동작을 browser 재현으로 확인한다.
|
||
- 완료 증거:
|
||
- 모바일 메뉴가 열린 동안 banner와 shell 본문이 같은 background inert·`aria-hidden` 경계에 포함됨
|
||
- viewport가 `lg` 이상으로 바뀌면 mobile menu state와 inert·`aria-hidden`이 해제되고 desktop navigation·logout·main을 keyboard와 pointer로 사용할 수 있음
|
||
- breakpoint 자동 종료 시 숨겨진 mobile trigger로 focus를 복귀하지 않음
|
||
- App focused test, mock responsive E2E, 기존 accessibility shell E2E와 P2 Gate 실행 기록
|
||
- 범위 밖:
|
||
- Admin shell navigation 구조 전면 교체
|
||
- Phase 3 Character UI와 새로운 breakpoint 체계 도입
|
||
|
||
**Files:**
|
||
|
||
- Modify: `src/app/App.tsx`
|
||
- Modify: `src/app/protected-admin-shell.tsx`
|
||
- Test: `src/app/App.test.tsx`
|
||
- Test: `tests/e2e/mock-preview-shell.spec.ts`
|
||
|
||
**Interfaces:**
|
||
|
||
- Consumes: `MockModeBanner({ apiMode }: { readonly apiMode: ApiMode })`, `ProtectedAdminShell({ routeError }: { readonly routeError: string | null })`, Tailwind `lg=1024px` shell breakpoint
|
||
- Produces: `ProtectedAdminShell({ routeError, apiMode }: { readonly routeError: string | null; readonly apiMode: ApiMode })`와 mobile overlay 표시 여부·background `inert`·`aria-hidden`이 항상 같은 상태인 composition
|
||
|
||
**TDD 절차:**
|
||
|
||
- [x] **RED: 실패 테스트 작성/실패 확인** — `src/app/App.test.tsx`에 mock banner가 열린 menu의 inert background에 포함되는 test와 `lg` 전환 시 menu state가 닫히는 test를 추가하고, `npm run test:run -- src/app/App.test.tsx src/shared/ui/__tests__/mock-mode-banner.test.tsx`가 두 assertion에서 실패하는지 확인한다.
|
||
- [x] **GREEN: 최소 구현/통과 확인** — banner와 shell을 하나의 background inert 경계로 조합하고 native viewport change에서 mobile state만 닫는 최소 구현으로 같은 명령을 통과시킨다.
|
||
- [x] **REFACTOR: 정리/회귀 확인** — focus 복귀 조건과 breakpoint 이름을 정리한 뒤 App focused test, mock preview E2E, 기존 accessibility shell E2E와 P2 focused Gate를 실행한다.
|
||
- [x] TDD 단계와 아래 검증 기준의 실제 결과를 plan/review에 누적한다.
|
||
|
||
**검증 기준:**
|
||
|
||
- **실행 명령:** `npm run test:run -- src/app/App.test.tsx src/shared/ui/__tests__/mock-mode-banner.test.tsx`, `npm run test:run -- src/shared/config src/shared/mocks src/shared/ui/__tests__/mock-mode-banner.test.tsx`, `npm run e2e:mock -- tests/e2e/mock-preview-shell.spec.ts`, `npm run e2e -- tests/e2e/accessibility-shell.spec.ts`, `npm run typecheck`, `npm run lint`
|
||
- **기대 결과:** focused unit 2 files / 26 tests 이상, P2 focused 7 files / 25 tests 이상, mock preview 4 projects / 24 tests 이상, accessibility shell 4 projects / 12 tests, type·lint 오류 0건; 1,024px·1,200px 전환 뒤 hidden mobile overlay와 inert background 잔존 0건.
|
||
- **수동 확인:** 320px에서 메뉴를 열면 banner와 본문이 보조기기 탐색에서 제외되고 menu만 탐색 가능하며, 열린 상태로 1,024px와 1,200px로 넓히면 desktop navigation·logout·main이 즉시 다시 동작한다.
|
||
|
||
### Task R2.10 — 보호 route 오류의 fail-closed 재시도 복구
|
||
|
||
**Goal 실행 `P2-R10`:** 보호 route probe의 404·network 오류 화면에서 보호 shell을 열지 않은 채 사용자가 명시적으로 재시도할 수 있게 한다.
|
||
|
||
- 연결 리뷰: [Phase 2 리뷰](./reviews/review-phase-2.md) — `REV-P2-013`
|
||
- 시작 조건:
|
||
- `P2-R9` 완료.
|
||
- 저장된 ADMIN session의 probe가 404 또는 network error로 실패하면 오류 alert만 있고 재시도·이동 control이 없는 현재 동작을 확인한다.
|
||
- 완료 증거:
|
||
- 404·network error 화면에 keyboard로 사용할 수 있는 명시적 재시도 control 제공
|
||
- 재시도 중과 재실패 상태에서 보호 shell·navigation·logout 비노출 유지
|
||
- 재시도 probe가 성공한 뒤에만 현재 session·route visit의 보호 shell 표시
|
||
- server가 제공한 정상 오류 envelope의 한국어 message는 보존하고 network·invalid response에는 안전한 공통 안내 사용
|
||
- App focused test, server boundary E2E와 P2 Gate 실행 기록
|
||
- 범위 밖:
|
||
- 자동 retry·자동 mock fallback
|
||
- Phase 3 Character 목록 오류 UI와 API client 전면 교체
|
||
|
||
**Files:**
|
||
|
||
- Modify: `src/app/App.tsx`
|
||
- Test: `src/app/App.test.tsx`
|
||
- Test: `tests/e2e/server-mode-boundary.spec.ts`
|
||
|
||
**Interfaces:**
|
||
|
||
- Consumes: `ProtectedRouteErrorPage({ message }: { readonly message: string })`, `BrowserLocationSnapshot.visitKey`, `ApiError.message`
|
||
- Produces: `ProtectedRouteErrorPage({ message, onRetry }: { readonly message: string; readonly onRetry: () => void })`와 수동 retry마다 새 probe attempt를 식별하는 App state
|
||
|
||
**TDD 절차:**
|
||
|
||
- [x] **RED: 실패 테스트 작성/실패 확인** — `src/app/App.test.tsx`에 404 → retry 성공과 network → retry 실패의 fail-closed test를 추가하고 `npm run test:run -- src/app/App.test.tsx`가 retry control 부재로 두 test에서 실패하는지 확인한다.
|
||
- [x] **GREEN: 최소 구현/통과 확인** — 오류 page에 retry button과 현재 session·route에 귀속된 새 probe attempt만 추가해 같은 명령을 통과시킨다.
|
||
- [x] **REFACTOR: 정리/회귀 확인** — 오류 message·attempt state 이름을 정리한 뒤 App focused test, server boundary E2E, P2 focused Gate와 전체 unit을 실행한다.
|
||
- [x] TDD 단계와 아래 검증 기준의 실제 결과를 plan/review에 누적한다.
|
||
|
||
**검증 기준:**
|
||
|
||
- **실행 명령:** `npm run test:run -- src/app/App.test.tsx`, `npm run test:run -- src/shared/config src/shared/mocks src/shared/ui/__tests__/mock-mode-banner.test.tsx`, `npm run test:run`, `npm run e2e -- tests/e2e/server-mode-boundary.spec.ts`, `npm run typecheck`, `npm run lint`
|
||
- **기대 결과:** App 1 file / 26 tests 이상, P2 focused 7 files / 25 tests 이상, 전체 34 files / 145 tests 이상, server boundary 4 projects / 16 tests 이상, type·lint 오류 0건; retry 성공 전 보호 `main`·logout button 0건.
|
||
- **수동 확인:** 404와 offline 상태에서 retry button의 accessible name·focus indicator를 확인하고, 실패 중 shell이 보이지 않으며 연결 복구 후 한 번의 수동 retry로 shell이 열린다.
|
||
|
||
### Task R2.11 — Phase 2 working tree 전체 경로 집계 복구
|
||
|
||
**Goal 실행 `P2-R11`:** tracked diff와 untracked 파일을 함께 집계해 Phase 2 review 범위와 완료 문서가 실제 working tree 전체를 누락 없이 표시하게 한다.
|
||
|
||
- 연결 리뷰: [Phase 2 리뷰](./reviews/review-phase-2.md) — `REV-P2-014`
|
||
- 시작 조건:
|
||
- `P2-R8` 완료.
|
||
- `git diff HEAD --name-only | wc -l`은 37을 반환하지만 `git status --porcelain=v1 | wc -l`은 38을 반환하고 `git ls-files --others --exclude-standard`에 `src/app/protected-admin-shell.tsx`가 존재함을 확인한다.
|
||
- 완료 증거:
|
||
- review의 현재 working tree 범위를 tracked 37개와 untracked 1개를 포함한 38개 변경 항목으로 기록
|
||
- `P2-R8` 대체 검증과 후속 reviewer 명령이 untracked 파일을 빠뜨리지 않는 `git status --short --untracked-files=all` 기준을 사용
|
||
- plan/review의 현재 범위·종료 판정·검증 기록과 실제 working tree 항목 차이 0건
|
||
- 범위 밖:
|
||
- 애플리케이션 코드·test·설정 변경
|
||
- 과거 시점의 37개 tracked diff 실행 결과 삭제 또는 덮어쓰기
|
||
|
||
**Files:**
|
||
|
||
- Modify: `docs/20260725_AI캐릭터관리자웹/plan-task.md`
|
||
- Modify: `docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md`
|
||
- Test: 없음 — working tree 집계 명령과 완료 문서 메타데이터만 정정한다.
|
||
|
||
**Interfaces:**
|
||
|
||
- Consumes: `git diff HEAD --name-only`, `git status --short --untracked-files=all`, `git ls-files --others --exclude-standard`, `REV-P2-014`
|
||
- Produces: tracked·untracked를 모두 포함하는 Phase 2 working tree 범위와 reviewer 검증 기준
|
||
|
||
**TDD 예외 사유:** 실행 동작을 변경하지 않는 문서·검증 명령 정정이므로 제품 test가 아닌 실제 Git 상태 대조가 실패·성공 증거다.
|
||
|
||
**대체 검증 방법:** tracked diff 수, 전체 status 항목 수와 untracked 목록을 각각 수집해 합계와 문서 범위를 대조한다.
|
||
|
||
- [x] `P2-R8`의 대체 검증 기준에 `git status --short --untracked-files=all`과 untracked 확인 명령을 추가한다.
|
||
- [x] review 상단·4차 검증·종료 판정의 현재 working tree 범위를 tracked 37개 + untracked 1개 = 38개 항목으로 정렬한다.
|
||
- [x] 과거 37개 tracked diff 기록은 당시 실행 결과로 보존하고 현재 전체 범위와 구분한다.
|
||
- [x] 아래 대체 검증과 문서 diff 검사를 실행하고 결과를 plan/review에 누적한다.
|
||
|
||
**검증 기준:**
|
||
|
||
- **실행 명령:** `git diff HEAD --name-only | wc -l`, `git status --short --untracked-files=all | wc -l`, `git ls-files --others --exclude-standard`, `git diff --check -- docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md`
|
||
- **기대 결과:** 현재 기준 tracked diff 37개, untracked `src/app/protected-admin-shell.tsx` 1개, working tree 전체 38개 변경 항목, 현재 문서 범위 누락과 whitespace 오류 0건.
|
||
- **수동 확인:** review 상단·4차 검증·발견 요약·plan 전환·종료 판정이 `P2-R1`~`P2-R13`의 현재 상태와 38개 working tree 항목을 같은 의미로 표시한다.
|
||
|
||
### Task R2.12 — 보호 route probe 대기 상태의 접근 가능한 피드백 복구
|
||
|
||
**Goal 실행 `P2-R12`:** 최초 보호 route 확인과 수동 retry가 진행되는 동안 보호 shell을 숨긴 채 사용자와 보조기기에 명시적인 loading 상태를 제공한다.
|
||
|
||
- 연결 리뷰: [Phase 2 리뷰](./reviews/review-phase-2.md) — `REV-P2-015`
|
||
- 시작 조건:
|
||
- `P2-R10` 완료.
|
||
- retry 응답을 350ms 지연하면 `#root` child 0개, `main`·`status`·`alert` 0개이고 active element가 `BODY`인 현재 Chromium 동작을 확인한다.
|
||
- 완료 증거:
|
||
- 최초 probe와 retry pending 모두 `RouteFrame` 안에 지속적으로 보이는 한국어 loading 상태와 `role="status"` 제공
|
||
- pending 동안 보호 `main`·navigation·logout 비노출과 mock mode banner 지속 표시 유지
|
||
- 성공·404·network·401·403의 기존 fail-closed 분기와 수동 retry 동작 회귀 없음
|
||
- App focused test, 지연된 server boundary E2E, P2 focused Gate와 전체 unit 실행 기록
|
||
- 범위 밖:
|
||
- 보호 shell skeleton 선노출
|
||
- 자동 retry·자동 mock fallback과 전역 router 도입
|
||
|
||
**Files:**
|
||
|
||
- Modify: `src/app/App.tsx`
|
||
- Test: `src/app/App.test.tsx`
|
||
- Test: `tests/e2e/server-mode-boundary.spec.ts`
|
||
|
||
**Interfaces:**
|
||
|
||
- Consumes: `PageState({ state: "loading", title, description })`, `RouteFrame({ apiMode, children })`, `ProtectedRouteVerification`
|
||
- Produces: 현재 session·route visit·retry attempt가 미검증인 동안 렌더되는 `RouteFrame` + accessible loading `PageState`
|
||
|
||
**TDD 절차:**
|
||
|
||
- [x] **RED: 실패 테스트 작성/실패 확인** — `src/app/App.test.tsx`에 최초 probe pending 전용 test를 추가하고 기존 404 retry test에 retry pending assertion을 보강해 두 pending 시나리오가 `role="status"`를 제공하면서 보호 shell을 숨기는지 확인한다. `npm run test:run -- src/app/App.test.tsx`가 status 부재로 실패하는지 확인한다.
|
||
- [x] **GREEN: 최소 구현/통과 확인** — 기존 `PageState` loading variant를 미검증 branch에 조합하는 최소 구현으로 같은 명령을 통과시킨다.
|
||
- [x] **REFACTOR: 정리/회귀 확인** — loading copy와 branch 이름을 정리하고 기존 404 retry E2E에 350ms pending status assertion을 추가한 뒤 focused·전체 회귀를 실행한다.
|
||
- [x] TDD 단계와 아래 검증 기준의 실제 결과를 plan/review에 누적한다.
|
||
|
||
**검증 기준:**
|
||
|
||
- **실행 명령:** `npm run test:run -- src/app/App.test.tsx`, `VITE_API_MODE=server npx playwright test tests/e2e/server-mode-boundary.spec.ts`, `npm run test:run -- src/shared/config src/shared/mocks src/shared/ui/__tests__/mock-mode-banner.test.tsx`, `npm run test:run`, `npm run typecheck`, `npm run lint`
|
||
- **기대 결과:** App 1 file / 27 tests, server boundary 4 projects / 12 tests 이상, P2 focused 7 files / 25 tests 이상, 전체 34 files / 147 tests 이상, type·lint 오류 0건; 최초 pending 전용 test 1건과 기존 retry test 보강으로 두 pending 시나리오에서 visible `role="status"` 1건과 보호 `main`·logout 0건.
|
||
- **수동 확인:** 375px server mode에서 최초 진입과 retry 응답을 각각 350ms 이상 지연해 한국어 loading 안내가 보이고 빈 root가 발생하지 않으며, 성공 뒤에만 shell이 열린다.
|
||
|
||
### Task R2.13 — Mode별 focused E2E 필터와 network retry 증거 복구
|
||
|
||
**Goal 실행 `P2-R13`:** bare mode 분리를 유지하면서 CLI spec 필터가 실제로 focused 실행되게 하고 network retry 실패를 server boundary E2E로 고정한다.
|
||
|
||
- 연결 리뷰: [Phase 2 리뷰](./reviews/review-phase-2.md) — `REV-P2-016`
|
||
- 시작 조건:
|
||
- `P2-R12` 완료.
|
||
- `npm run e2e -- --list tests/e2e/server-mode-boundary.spec.ts`가 4 files / 32 tests를 수집하지만 직접 `VITE_API_MODE=server npx playwright test tests/e2e/server-mode-boundary.spec.ts --list`는 1 file / 12 tests만 수집함을 확인한다.
|
||
- `P2-R10` 검증 기준은 server boundary 16 tests 이상을 요구하지만 현재 spec은 3 scenarios / 12 tests이고 network error test는 retry를 실행하지 않음을 확인한다.
|
||
- 완료 증거:
|
||
- bare `npm run e2e`는 server 4개 spec만, bare `npm run e2e:mock`은 mock 2개 spec만 실행
|
||
- 두 npm script 뒤에 전달한 file filter가 고정 spec 목록과 합쳐지지 않고 해당 file만 수집
|
||
- server boundary에 network error → 수동 retry → network 재실패의 retry 유지·shell 비노출·worker 0건 E2E 추가
|
||
- mode boundary contract, focused list/실행, bare mode E2E, P2 Gate와 문서 동기화 기록
|
||
- 범위 밖:
|
||
- Playwright config 복제
|
||
- project matrix·worker 수·CI 전체 구조 변경
|
||
|
||
**Files:**
|
||
|
||
- Modify: `package.json`
|
||
- Modify: `playwright.config.ts`
|
||
- Modify: `README.md`
|
||
- Modify: `docs/agent-guide/scripts.md`
|
||
- Test: `src/shared/mocks/__tests__/mode-boundary.test.ts`
|
||
- Test: `src/shared/mocks/__tests__/mock-preview-docs.test.ts`
|
||
- Test: `tests/e2e/server-mode-boundary.spec.ts`
|
||
|
||
**Interfaces:**
|
||
|
||
- Consumes: `VITE_API_MODE=server | mock`, Playwright `testMatch`, npm argument forwarding, 현재 server 4개·mock 2개 spec allowlist
|
||
- Produces: file 목록을 내장하지 않는 `e2e`·`e2e:mock` scripts와 server `testMatch=["**/server-mode-boundary.spec.ts", "**/smoke.spec.ts", "**/auth.spec.ts", "**/accessibility-shell.spec.ts"]`, mock `testMatch=["**/mock-preview-shell.spec.ts", "**/mock-mode-boundary.spec.ts"]`; CLI file filter와 mode allowlist의 교집합 실행 계약
|
||
|
||
**TDD 절차:**
|
||
|
||
- [x] **RED: 실패 테스트 작성/실패 확인** — mode boundary test가 npm scripts의 고정 spec 목록 제거와 config의 mode별 exact allowlist를 요구하게 하고 `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts`가 현재 script/config로 실패하는지 확인한다.
|
||
- [x] **RED: surface 실패 확인** — network 재실패 retry E2E를 추가하고 현재 구현에서 동작을 확인하되, `npm run e2e -- --list tests/e2e/server-mode-boundary.spec.ts`가 불필요한 4 files를 수집해 focused list assertion을 실패시키는지 확인한다.
|
||
- [x] **GREEN: 최소 구현/통과 확인** — script는 각각 `VITE_API_MODE=server playwright test`, `VITE_API_MODE=mock playwright test`만 유지하고 config의 `apiMode`별 `testMatch`에 Interfaces의 exact glob allowlist를 옮겨 contract·focused list·network retry E2E를 통과시킨다.
|
||
- [x] **REFACTOR: 정리/회귀 확인** — mode allowlist 상수와 README/scripts 문구를 정리한 뒤 focused·bare E2E와 P2 Gate를 실행한다.
|
||
- [x] TDD 단계와 아래 검증 기준의 실제 결과를 plan/review에 누적한다.
|
||
|
||
**검증 기준:**
|
||
|
||
- **실행 명령:** `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts`, `npm run e2e -- --list tests/e2e/server-mode-boundary.spec.ts`, `npm run e2e:mock -- --list tests/e2e/mock-preview-shell.spec.ts`, `npm run e2e -- tests/e2e/server-mode-boundary.spec.ts`, `npm run e2e`, `npm run e2e:mock`, `npm run test:run -- src/shared/config src/shared/mocks src/shared/ui/__tests__/mock-mode-banner.test.tsx`, `npm run typecheck`, `npm run lint`
|
||
- **기대 결과:** focused server boundary 1 file / 16 tests 이상, focused mock preview 1 file / 24 tests, bare server 4 files / 36 tests 이상, bare mock 2 files / 28 tests, P2 focused 7 files / 25 tests 이상, type·lint 오류 0건; network retry 재실패 뒤 retry button 1건, 보호 `main`·logout·mock worker 0건.
|
||
- **수동 확인:** 없음 — mode/file 수집 목록, network retry와 worker 경계는 Playwright list·실행 결과로 결정적으로 검증한다.
|
||
|
||
### Task R2.14 — P2-R12 test 증거 정합성 복구
|
||
|
||
**Goal 실행 `P2-R14`:** P2-R12의 실제 test case 구조·실행 수를 plan/review의 완료 증거와 사실대로 정렬한다.
|
||
|
||
- 연결 리뷰: [Phase 2 리뷰](./reviews/review-phase-2.md) — `REV-P2-017`
|
||
- 시작 조건:
|
||
- `P2-R11`~`P2-R13` 구현·검증 완료.
|
||
- `rg -c '^test\(' src/app/App.test.tsx`와 focused 실행이 모두 27 tests를 반환한다.
|
||
- 최초 pending은 전용 test 1건, retry pending은 기존 404 retry test의 assertion 보강이지만 `P2-R12` 체크와 수정 기록은 두 신규 test·28 tests 이상으로 표시한다.
|
||
- 완료 증거:
|
||
- P2-R12의 현재 완료 증거가 최초 pending 전용 test 1건 추가 + 기존 retry test 보강 + App 27 tests로 일치
|
||
- 이전 “두 신규 test·28 tests 이상” 표현과 정정 사유를 새 Progress에 보존하고 실제 실행 결과를 덮어쓰지 않음
|
||
- plan/review 검색·diff 검사와 App focused test 결과를 수정 검증 기록에 누적
|
||
- 범위 밖:
|
||
- 애플리케이션 코드·test·설정 변경
|
||
- 같은 retry pending 동작을 중복 검증하는 test case 추가
|
||
- 기존 실행 이력 삭제 또는 제품 동작 완료 주장 변경
|
||
|
||
**Files:**
|
||
|
||
- Modify: `docs/20260725_AI캐릭터관리자웹/plan-task.md`
|
||
- Modify: `docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md`
|
||
- Test: 없음 — 구현·test 동작은 바꾸지 않고 완료 문서의 사실관계와 현재 상태만 정정한다.
|
||
|
||
**Interfaces:**
|
||
|
||
- Consumes: `REV-P2-017`, `src/app/App.test.tsx`의 27 test declarations, `P2-R12` 완료 기록
|
||
- Produces: actual test case와 검증 시나리오를 구분한 P2-R12 완료 증거와 정정 기록
|
||
|
||
**TDD 예외 사유:** 제품 코드와 test를 변경하지 않는 문서 정정이며, 새 test case를 추가하면 이미 존재하는 retry pending assertion을 중복하게 된다. 실제 test 선언·focused 결과와 문서 문자열 대조를 실패·성공 증거로 사용한다.
|
||
|
||
**대체 검증 방법:** App test 선언 수·두 pending 시나리오의 위치·focused 실행 수를 plan/review 문구와 대조하고, stale 표현 검색과 문서 diff 검사를 실행한다.
|
||
|
||
- [x] `rg -c '^test\(' src/app/App.test.tsx`와 두 pending test 위치를 수집해 27 tests 구조를 재확인한다.
|
||
- [x] P2-R12 체크·기대 결과·수정 검증 기록을 최초 전용 test 1건 + 기존 retry test 보강 + App 27 tests로 정정하고 이전 표현·정정 사유를 새 기록에 남긴다.
|
||
- [x] review 상단·발견 요약·종료 판정을 `REV-P2-017` 수정 완료와 `P2-R14` 완료 상태로 갱신한다.
|
||
- [x] 아래 대체 검증과 문서 diff 검사를 실행하고 실제 결과를 plan/review에 누적한다.
|
||
|
||
**P2-R14 수정 검증 기록 (2026-07-27):**
|
||
|
||
- 대체 RED: `rg -c '^test\(' src/app/App.test.tsx` — 27건. `rg -n 'initial protected route probe is pending|retries a protected route 404' src/app/App.test.tsx` — 최초 pending 전용 test 1건과 기존 404 retry test 1건을 확인했다. 기존 P2-R12 기록은 두 신규 test·App 28 tests 이상으로 표시돼 실제 구조와 불일치했다.
|
||
- GREEN: P2-R12 TDD 절차·기대 결과와 P2-R11~P2-R13 수정 검증 기록을 최초 pending 전용 test 1건 + 기존 retry test 보강 + App 27 tests로 정정했다. 이전 잘못된 표현과 정정 사유는 이 P2-R14 기록과 review `REV-P2-017`에 보존했다.
|
||
- Focused: `npm run test:run -- src/app/App.test.tsx` — 1 file / 27 tests 통과.
|
||
- 검증: `rg -n '^[[:space:]]+- P2-R12 RED:.*신규 pending status tests 2건' docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md` — no matches. `git diff --check -- docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md` — exit 0.
|
||
|
||
**검증 기준:**
|
||
|
||
- **실행 명령:** `rg -c '^test\(' src/app/App.test.tsx`, `rg -n 'initial protected route probe is pending|retries a protected route 404' src/app/App.test.tsx`, `npm run test:run -- src/app/App.test.tsx`, `rg -n '^[[:space:]]+- P2-R12 RED:.*신규 pending status tests 2건' docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md`, `git diff --check -- docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md`
|
||
- **기대 결과:** App test 선언·focused 실행 27건, 최초 pending 전용 test와 기존 retry test 각 1건 확인, 정정 대상인 잘못된 RED 기록 검색 결과 0건, 문서 whitespace 오류 0건.
|
||
- **수동 확인:** plan/review가 test case 수와 검증 시나리오 수를 구분하고 P2-R12의 실제 완료 증거를 같은 의미로 표시한다.
|
||
|
||
### Task R2.15 — review plan 전환 절의 현재 수정 상태 정합성 복구
|
||
|
||
**Goal 실행 `P2-R15`:** Phase 2 review의 plan·goal 전환 절이 `P2-R8`~`P2-R13`의 현재 수정 완료 상태를 요약·상세·종료 판정과 일치하게 표시하도록 정정한다.
|
||
|
||
- 연결 리뷰: [Phase 2 리뷰](./reviews/review-phase-2.md) — `REV-P2-018`
|
||
- 시작 조건:
|
||
- review 요약·상세·종료 판정은 `REV-P2-010`~`REV-P2-016`과 `P2-R8`~`P2-R13`을 수정 완료로 표시한다.
|
||
- 같은 review의 `7. 확정 항목의 plan·goal 전환`은 3차·4차 회귀 Task를 여전히 “아직 수정하지 않았다”고 표시하는 모순을 확인한다.
|
||
- 완료 증거:
|
||
- review §7의 `P2-R8`~`P2-R13` 전환 설명이 각 Task의 실제 수정 완료 상태와 일치함
|
||
- 과거 시점의 발견·수정 전 검증 기록은 삭제하거나 현재 상태처럼 다시 쓰지 않고 그대로 보존함
|
||
- stale 현재 상태 표현 검색 결과 0건과 문서 whitespace 오류 0건
|
||
- 범위 밖:
|
||
- 애플리케이션 코드·test·설정 변경
|
||
- 과거 검증 기록과 당시의 남은 항목 삭제 또는 덮어쓰기
|
||
- `P2-R8`~`P2-R13` 구현·검증의 재수행
|
||
|
||
**Files:**
|
||
|
||
- Modify: `docs/20260725_AI캐릭터관리자웹/plan-task.md`
|
||
- Modify: `docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md`
|
||
- Test: 없음 — 완료된 review의 현재 상태 문구만 정정하는 문서 Task다.
|
||
|
||
**Interfaces:**
|
||
|
||
- Consumes: `REV-P2-010`~`REV-P2-016`, `P2-R8`~`P2-R13` 수정 검증 기록, review §5·§6·§8
|
||
- Produces: review §7과 요약·상세·종료 판정이 같은 현재 상태를 표시하는 Phase 2 인수인계 문서
|
||
|
||
**TDD 예외 사유:** 제품 동작이나 실행 가능한 계약을 바꾸지 않는 문서 상태 정정이므로 새 제품 test를 추가하지 않는다.
|
||
|
||
**대체 검증 방법:** review §5·§6·§7·§8의 상태를 직접 대조하고 stale 현재 상태 문자열 검색과 Markdown diff 검사를 실행한다.
|
||
|
||
- [x] review §7의 3차 회귀 Task 설명을 `P2-R8`~`P2-R10` 수정 완료 상태로 정정한다.
|
||
- [x] review §7의 4차 회귀 Task 설명을 `P2-R11`~`P2-R13` 수정 완료 상태로 정정한다.
|
||
- [x] 과거 시점의 수정 전 기록과 남은 항목이 보존됐는지 확인한다.
|
||
- [x] 아래 대체 검증을 실행하고 실제 결과를 plan/review에 누적한다.
|
||
|
||
**P2-R15 수정 검증 기록 (2026-07-27):**
|
||
|
||
- 대체 RED: `rg -n '3차 재검증의 새 확정 4건.*아직 수정하지 않았다|4차 재검증의 새 확정 3건.*아직 수정하지 않았다' docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md` — 2건. review §7이 완료된 `P2-R8`~`P2-R13`을 여전히 미수정 현재 상태로 표시했다.
|
||
- GREEN: review §7의 3차·4차 회귀 Task 설명을 2026-07-27 수정·검증 완료 상태로 정정하고, review의 `REV-P2-018` 상태·발견 요약·종료 판정을 수정 완료로 맞췄다. §9의 당시 남은 항목과 과거 검증 기록은 보존했다.
|
||
- 검증: 같은 stale 현재 상태 검색 — no matches. `git diff --check -- docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md` — exit 0.
|
||
|
||
**검증 기준:**
|
||
|
||
- **실행 명령:** `rg -n '3차 재검증의 새 확정 4건.*아직 수정하지 않았다|4차 재검증의 새 확정 3건.*아직 수정하지 않았다' docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md`, `git diff --check -- docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md`
|
||
- **기대 결과:** stale 현재 상태 검색 결과 0건, 문서 whitespace 오류 0건, review §5·§6·§7·§8의 `P2-R8`~`P2-R13` 상태 의미 차이 0건.
|
||
- **수동 확인:** review §9의 당시 남은 항목 기록은 이력으로 보존되고 §7의 현재 전환 상태만 수정 완료로 정정됐는지 확인한다.
|
||
|
||
### Task R2.16 — Phase 2 현재 상태·하단 Progress 정합성 복구
|
||
|
||
**Goal 실행 `P2-R16`:** Phase 2 상단 현재 상태와 하단 최신 Progress가 `P2-R15`까지의 실제 수정 완료 및 Phase 3 진행 가능 상태를 일치하게 표시하도록 정정한다.
|
||
|
||
- 연결 리뷰: [Phase 2 리뷰](./reviews/review-phase-2.md) — `REV-P2-019`
|
||
- 시작 조건:
|
||
- `P2-R15` Task 본문과 review는 수정 완료 및 열린 확정 항목 없음을 표시한다.
|
||
- Phase 2 상단 현재 상태는 최초 추가 당시 설명만 유지하고, 하단 최신 Progress는 `P2-R15` 수정 필요 상태로 끝나는 모순을 확인한다.
|
||
- 완료 증거:
|
||
- Phase 2 상단 현재 상태가 `P2-T1`~`P2-T3`, `P2-GATE`, `P2-R1`~`P2-R16` 완료와 Phase 3 진행 가능 상태를 명시함
|
||
- 6차 재검증의 당시 남은 항목은 이력으로 보존하고 하단에 `P2-R16` 수정 검증을 누적함
|
||
- 최신 Progress, review 요약·종료 판정과 Phase 2 상단 상태의 의미 차이 0건
|
||
- 범위 밖:
|
||
- 애플리케이션 코드·test·설정 변경
|
||
- 과거 검증 기록과 당시 남은 항목 삭제 또는 덮어쓰기
|
||
- Phase 3 기능 구현
|
||
|
||
**Files:**
|
||
|
||
- Modify: `docs/20260725_AI캐릭터관리자웹/plan-task.md`
|
||
- Modify: `docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md`
|
||
- Test: 없음 — 완료된 Phase의 현재 상태와 누적 Progress만 정정하는 문서 Task다.
|
||
|
||
**Interfaces:**
|
||
|
||
- Consumes: `REV-P2-019`, `P2-R15` 수정 검증, Phase 2 상단 현재 상태와 하단 Progress
|
||
- Produces: Phase 3 시작 조건 판정에 사용할 일관된 Phase 2 완료 상태
|
||
|
||
**TDD 예외 사유:** 제품 동작이나 실행 가능한 계약을 바꾸지 않는 문서 상태 정정이므로 새 제품 test를 추가하지 않는다.
|
||
|
||
**대체 검증 방법:** 완료 상태 exact 문자열과 최신 Progress를 검색하고, 과거 6차 기록 보존 및 Markdown diff를 확인한다.
|
||
|
||
- [x] Phase 2 상단 현재 상태를 실제 완료 범위와 Phase 3 진행 가능 상태로 정정한다.
|
||
- [x] 6차 재검증의 당시 남은 항목을 보존하고 하단에 `P2-R16` 수정 검증을 누적한다.
|
||
- [x] review의 `REV-P2-019` 상태·plan 전환·종료 판정을 수정 완료로 갱신한다.
|
||
- [x] 아래 대체 검증과 Phase 2 전체 Gate를 실행하고 실제 결과를 plan/review에 누적한다.
|
||
|
||
**P2-R16 수정 검증 기록 (2026-07-27):**
|
||
|
||
- 대체 RED: Phase 2 완료 상태 exact 검색과 `tail` 기반 최신 Progress 검색은 모두 no match, exit 1이었다. 상단은 최초 추가 설명에 머물고 문서 끝은 `P2-R15` 수정 필요 상태였다.
|
||
- GREEN: Phase 2 상단에 `P2-R16`까지 완료 및 Phase 3 진행 가능 상태를 명시하고, 6차 당시 남은 항목은 보존한 채 하단에 `P2-R16` 수정 검증을 누적했다. review의 `REV-P2-019`와 종료 판정도 수정 완료로 정렬했다.
|
||
- Phase 2 Gate: `npm run test:run` — 34 files / 147 tests 통과. `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0, build는 각각 160 modules 변환. `VITE_API_MODE=mock npm run build:prod` — 기대한 guard로 exit 1.
|
||
- E2E: `npm run e2e` — 4 projects / 36 tests 통과. `npm run e2e:mock` — 4 projects / 28 tests 통과.
|
||
- Production: `dist/mockServiceWorker.js` 없음, production JS의 `startMockWorker`·`mockServiceWorker.js` 검색은 기대한 no-match exit 1.
|
||
- 문서 검증: Phase 2 완료 상태와 최신 Progress exact 검색, 과거 6차 `P2-R15` 남은 항목 보존 검색, plan/review `git diff --check`를 통과했다.
|
||
|
||
**검증 기준:**
|
||
|
||
- **실행 명령:** Phase 2 완료 상태 exact 검색, `tail` 기반 최신 Progress 확인, 과거 `P2-R15` 남은 항목 보존 검색, `git diff --check`, Phase 2 전체 Gate
|
||
- **기대 결과:** 현재 상태·최신 Progress 의미 차이 0건, 과거 기록 보존, 문서 whitespace 오류 0건, Phase 2 자동 Gate 0 failure/0 error
|
||
- **수동 확인:** Phase 3 실행자가 과거 6차 기록을 열린 현재 Task로 오해하지 않고 `P2-GATE`와 모든 회귀 수정 완료를 확인할 수 있다.
|
||
|
||
---
|
||
|
||
> **계약 이력 안내 (2026-07-29):** Phase 3~9 본문과 §7의 실행 기록은
|
||
> OpenAPI 2.0.0 기준으로 수행된 당시 상태를 보존한다. 원작·장르 lookup,
|
||
> UTC 전송, Community pagination, FanTalk 수정·삭제, Comments와 active-only
|
||
> 계약의 최신 실행 기준은 OpenAPI 2.3.0과 **Phase 10**이다. 과거 Phase의
|
||
> 완료 체크를 다시 열거나 과거 RED/GREEN 증거를 현재 계약 증거로
|
||
> 재해석하지 않는다.
|
||
|
||
## Phase 3. Character workspace vertical slice
|
||
|
||
**목표:** ADMIN이 Character를 검색·생성·선택하고 workspace에서 상세·수정·soft delete 요청과 후속 목록 재조회를 완료한다.
|
||
|
||
**Phase Goal `P3`:** Task 3.1 → 3.4와 Phase 3 Gate로 Character workspace vertical slice를 완성한다.
|
||
|
||
- **시작 조건:** `P2-GATE` 완료.
|
||
- **완료 조건:** `P3-T1`~`P3-T4`, `P3-GATE` 완료. 외부 의존은 구현 또는 명시적 제외 결정으로 종결.
|
||
- **실행 순서:** 계약 확인 → 목록/workspace → mutation → 반응형·접근성.
|
||
|
||
**요구사항:** `CHAR-001~018`, `FILE-001~002`, `FILE-008~010`, `FILE-012`, PRD `7`, `9`의 Character 범위.
|
||
|
||
**외부 의존:** `EXT-007`/`CHAR-012`의 `searchTerm` 지정 결과에 대한 active-only 보장. `EXT-001`/`CHAR-013` original work lookup은 legacy 후보 `GET /admin/chat/original/search?searchTerm={검색어}`로 구현 계획에 포함한다. `searchTerm` 생략 시 활성 목록은 OpenAPI에 명시돼 있다. `originalWorkId` 미선택은 key 생략과 `null`이 모두 가능하며 frontend canonical serializer만 하나로 고정한다.
|
||
|
||
**주요 Files:**
|
||
|
||
- Create: `src/features/characters/api/character-api.ts`
|
||
- Create: `src/features/characters/model/types.ts`
|
||
- Create: `src/features/characters/schemas/character-schema.ts`
|
||
- Create: `src/features/characters/validation/character-image-policy.ts`
|
||
- Create: `src/features/characters/pages/{CharacterListPage,CharacterDetailPage,CharacterFormPage}.tsx`
|
||
- Create: `src/features/characters/components/{CharacterList,CharacterListItem,CharacterProfile,CharacterForm,CharacterImageField}.tsx`
|
||
- Create: `src/features/characters/components/OriginalWorkCombobox.tsx`
|
||
- Create: `src/features/characters/tests/character-api.test.ts`
|
||
- Create: `src/features/characters/tests/{character-list,character-form}.test.tsx`
|
||
- Create: `src/layouts/CharacterWorkspaceLayout.tsx`
|
||
- Create: `src/layouts/CharacterWorkspaceLayout.test.tsx`
|
||
- Create: `tests/e2e/character-workspace.spec.ts`
|
||
- Modify: `src/app/router.tsx`, `src/app/route-paths.ts`
|
||
|
||
#### 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,OriginalWorkCombobox}.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, legacy original work lookup `GET /admin/chat/original/search?searchTerm={검색어}`. Produces: `createCharacter`, `updateCharacter`, `deactivateCharacter`, `searchOriginalWorks`, 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~018`, `MOCK-001~009`와 OpenAPI Character 4개 operation/schema 확인.
|
||
- **완료 증거:** 체크박스 전체, 제공 계약 또는 제외 결정의 세 문서 일치, 상태/action inventory.
|
||
- **범위 밖:** 계약을 추정한 production adapter와 Character 화면 구현.
|
||
|
||
- [x] OpenAPI에서 목록 `searchTerm/page/size`, `data.totalCount/content`, item `id`, 상세 `characterUUID/originalWork`, mutation `data=null`을 contract fixture로 고정한다.
|
||
- [x] original work lookup endpoint, DTO, search/page를 확인하고, 미제공이면 network control을 제외한다. `originalWorkId` 미선택 serializer는 허용된 omit 또는 `null` 중 하나를 contract test로 고정한다.
|
||
- [x] OpenAPI 공통 400/401/403/404/405/406/415/500과 `ApiErrorResponse`를 fixture에 기록한다. Character 전용 message key는 미제공으로 표시하고 분기하지 않는다.
|
||
- [x] 목록·상세·form·workspace의 상태/action inventory를 작성하고 Page는 route/query/policy 조합, feature component는 Character 표시·입력 규칙, shared component는 Phase 1 contract를 사용하도록 component map을 확정한다.
|
||
- [x] 제공 계약 범위의 Character seed, loading·empty·error·success, legacy original work lookup과 CRUD/deactivate browser handler 시나리오를 확정한다.
|
||
|
||
**P3-T1 확정 기록:**
|
||
|
||
- OpenAPI 제공 범위: `GET/POST /api/v2/admin/ai-characters`, `GET/PUT /api/v2/admin/ai-characters/{characterId}`만 Character operation으로 사용한다. 목록은 query `searchTerm`, `page`, `size`와 응답 `data.totalCount`, `data.content[]`, item `id`를 fixture 기준으로 삼는다. 상세는 `characterUUID`, `originalWork`를 포함한다. 생성·수정 성공은 `NullSuccessResponse`의 `data=null`만 신뢰하고 새 ID나 상세 DTO를 추정하지 않는다. multipart part는 create `image`, `request`, update optional `image`, required `request`만 사용한다.
|
||
- Original work lookup: OpenAPI에는 없지만 legacy 후보 `GET /admin/chat/original/search?searchTerm={검색어}`를 사용한다. P3-T3에서 response DTO를 contract test로 고정하고 `OriginalWorkCombobox` network control을 만든다. `originalWorkId` 미선택 serializer는 P3-T3 contract test에서 key 생략을 canonical로 고정한다.
|
||
- 오류 fixture: Character 4개 operation의 공통 오류는 OpenAPI `ApiErrorResponse` shape, 즉 `success=false`, `message`, `data=null`, `errorProperty`만 사용한다. 400/401/403/404/405/406/415/500 status fixture를 공통 오류로 둔다. OpenAPI 밖의 Character 전용 message key는 분기하지 않고 미정의 오류는 기본 문구로 처리한다.
|
||
- 상태·action inventory: list는 `searchTerm`, `page`, `size`, `items`, `totalCount`, `selectedCharacterId`, `loading`, `empty`, `error`, `retry`; detail은 `characterId`, `profile`, `isActive`, `readOnly`, `loading`, `error`, `refresh`; form은 `mode`, `fields`, `image`, `dirty`, `submitting`, `fieldErrors`, `submit`, `cancel`, `deactivate`; workspace는 route `characterId`, active tab, breadcrumb, read-only policy, reload action을 가진다.
|
||
- component map: Page는 route param, URL query, data loading, navigation, read-only policy를 조합한다. feature component는 Character 목록 표시, 상세 표시, form 입력·직렬화, image 선택 규칙만 맡는다. shared component는 Phase 1의 `PageState`, `AppLayout`, `Button`, form primitive, dialog contract를 재사용한다.
|
||
- mock scenario: deterministic seed는 활성 Character 2건과 비활성 상세 1건, originalWork가 있는 상세 1건과 `null` 상세 1건을 둔다. handler는 list/detail loading, empty, common error, success, legacy original work lookup, create/update/deactivate 성공 및 공통 오류를 제공한다. create/update/deactivate 후 in-memory store의 list/detail을 OpenAPI 응답 shape로 갱신한다.
|
||
|
||
### Task 3.2 목록·선택·workspace
|
||
|
||
**Goal 실행 `P3-T2`:** Character 목록·검색·선택과 URL 기반 workspace 복원을 완성한다.
|
||
|
||
- **시작 조건:** `P3-T1` 완료.
|
||
- **완료 증거:** 체크박스 전체, route/list/workspace test와 read-only/error 상태 검증 기록.
|
||
- **범위 밖:** Character 생성·수정·비활성화 form.
|
||
|
||
- [x] Character 목록·생성 path에는 `characterId`가 없고 하위 resource route에만 선택한 `characterId`가 들어가는 contract test를 작성한다.
|
||
- [x] 목록 UI의 `search` 상태를 API `searchTerm`으로 직렬화하고 `page`, `size` URL query 보존, `data.content` 역직렬화와 loading·empty·error·retry test를 작성한다.
|
||
- [x] 목록 request에 `isActive`·`activeStatus`가 없고 client-side 활성 filter도 없는 contract test를 작성한다. active-only 보장은 외부 의존으로 남긴다.
|
||
- [x] Character 선택 시 URL의 `characterId`로 workspace에 진입하고 새로고침·deep link가 동작하는 test를 작성한다.
|
||
- [x] workspace header에 image, name, active 상태, `characterId`와 탭·breadcrumb를 표시한다.
|
||
- [x] 상세 성공 응답이 `isActive=false`이면 read-only 배너와 중앙 write policy로 모든 mutation 진입점을 차단한다.
|
||
- [x] 상세 400/404/500은 공통 오류 화면을 사용하고 비활성 ID 응답 정책을 client가 추정하지 않는다.
|
||
- [x] browser MSW handler와 deterministic seed로 목록 → 선택 → workspace deep link의 최종 UI를 `dev:mock`에서 확인한다.
|
||
|
||
### Task 3.3 생성·수정·soft delete
|
||
|
||
**Goal 실행 `P3-T3`:** Character multipart 생성·수정·soft delete와 image/original-work 규칙을 완성한다.
|
||
|
||
- **시작 조건:** `P3-T1`, `P3-T2` 완료.
|
||
- **완료 증거:** 체크박스 전체, serializer/form/image/deactivate test, `OQ-009` 결정과 검증 기록.
|
||
- **범위 밖:** 하위 도메인 mutation. v2 original-work lookup이 별도 제공되지 않으면 legacy `GET /admin/chat/original/search?searchTerm={검색어}`를 사용한다.
|
||
|
||
- [x] create multipart가 필수 `image`와 필수 `request` JSON part를 보내고 request에 `name`, `systemPrompt`, `description`을 포함하며 `isActive`, `externalCharacterId`를 포함하지 않는 test를 작성한다.
|
||
- [x] 일반 update는 `isActive`를 생략하고 soft delete만 `isActive=false`를 보내며 `true`를 보내지 않는 test를 작성한다.
|
||
- [x] create-only `region`을 수정 화면에서 읽기 전용으로 표시하고 update payload에 보내지 않는 test를 작성한다.
|
||
- [x] name·systemPrompt·description visible label, field error, 중복 제출 방지, dirty-form 이탈 확인을 test한다.
|
||
- [x] OpenAPI optional scalar와 tags·hobbies·values·goals·relationships·personalities·backgrounds·memories 반복 입력을 create/update schema에 맞게 직렬화하는 test를 작성한다.
|
||
- [x] Character image의 JPEG/PNG·10MB, `1:1`, 최대 800×800, no-upscale, crop 이동·zoom·reset·preview·취소·적용·keyboard 대안을 test한다.
|
||
- [x] crop 취소·교체 파일 미선택이 기존 image를 유지하고 기존 image 제거 UI는 없음을 test한다.
|
||
- [x] Phase 10 `P10-T1`에서 v2 `searchAiCharacterOriginalWorks` 기반 원작 검색 Combobox와 `originalWorkId` 선택 payload를 contract test로 고정한다. 미선택 payload는 canonical 생략으로 고정하고 legacy lookup은 제거한다.
|
||
- [x] 상세의 `characterUUID`는 읽기 전용으로 표시할 수 있지만 `externalCharacterId`로 이름을 바꾸지 않는다. 계약에 없는 creator member ID·nickname DTO/UI는 만들지 않고 creator 생성·동기화도 client가 수행하지 않는다.
|
||
- [x] create의 `data=null` 성공 후 목록 route로 이동해 목록을 재조회하고, update는 기존 `characterId` 상세 route로 재진입해 상세를 재조회한다. 현재 Character 화면은 React Query cache가 아니라 route 재진입 직접 요청으로 동기화한다.
|
||
- [x] 비활성화 AlertDialog가 영향·복원 미지원·hard delete 미지원을 설명하는 test를 작성한다.
|
||
- [x] soft delete 성공 후 목록 route 이동과 목록 재조회, 성공 toast를 확인하고 상세에 머물지 않는다. 비활성 항목이 서버 결과에서 제외되는지는 active-only 계약 제공 후 server mode에서 검증한다.
|
||
- [x] 초기 Character form을 실제 페이지에서 확인한 뒤 `name`, `systemPrompt`, `description`과 tags·hobbies·values·goals·relationships·personalities·backgrounds·memories의 최대 길이·개수 권고안을 작성한다. backend 호환 확인 전에는 상한을 구현하지 않는다.
|
||
- [x] mock store가 create/update/deactivate 후 목록·상세를 같은 server response contract로 갱신하는 E2E를 작성한다.
|
||
|
||
### Task 3.4 Character 반응형·접근성
|
||
|
||
**Goal 실행 `P3-T4`:** Character viewport capability와 keyboard·zoom·axe 기준을 검증한다.
|
||
|
||
- **시작 조건:** `P3-T2`, `P3-T3`의 활성 범위 완료.
|
||
- **완료 증거:** 체크박스 전체, 320/768/1280px·200% zoom·keyboard·axe E2E 기록.
|
||
- **범위 밖:** 다음 Phase 도메인과 모바일 mutation.
|
||
|
||
- [x] desktop/tablet에서는 전체 관리 action을 제공한다.
|
||
- [x] mobile에서는 목록·검색·상세만 제공하고 create/edit/deactivate route 직접 진입도 desktop 안내로 종료한다.
|
||
- [x] Table이 mobile Card로 바뀌어도 동일한 accessible name과 핵심 상태를 유지한다.
|
||
- [x] keyboard-only로 search → select → tabs → form → dialog를 완료한다.
|
||
- [x] 320/768/1280px, 200% zoom, axe critical·serious 0건을 Phase E2E에서 확인한다.
|
||
|
||
### Phase 3 Gate
|
||
|
||
**Goal 실행 `P3-GATE`:** Character mock UI journey와 실제 server integration 상태를 분리해 판정한다.
|
||
|
||
- **시작 조건:** `P3-T1`~`P3-T4` 완료.
|
||
- **완료 증거:** 아래 명령과 Expected 통과, 외부 의존의 구현/제외 상태 기록.
|
||
- **범위 밖:** 실패와 무관한 Audio 기능.
|
||
|
||
```bash
|
||
npm run test:run -- src/features/characters src/layouts/CharacterWorkspaceLayout.test.tsx
|
||
npm run e2e:mock -- tests/e2e/character-workspace.spec.ts
|
||
npm run e2e
|
||
npm run typecheck
|
||
npm run lint
|
||
npm run build
|
||
```
|
||
|
||
**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와 일치하는지 본다.
|
||
|
||
### Task R3.1 — Character optional form 계약 완성
|
||
|
||
**Goal 실행 `P3-R1`:** Character create/update form에서 확정된 optional scalar와 반복 배열을 편집·직렬화한다.
|
||
|
||
- **연결 리뷰:** [Phase 3 재리뷰](./reviews/review-phase-3-20260729.md) — `REV-P3-001`
|
||
- **시작 조건:** OpenAPI `CharacterCreateRequest/CharacterUpdateRequest`와 `CHAR-003`, `CHAR-018`.
|
||
- **Files:** Modify: `src/features/characters/pages/CharacterCreatePage.tsx`, `src/features/characters/pages/CharacterEditPage.tsx`, `src/features/characters/api/character-api.ts`, `src/features/characters/tests/CharacterCreatePage.test.tsx`, `src/features/characters/tests/CharacterEditPage.test.tsx`, `src/features/characters/tests/character-api.test.ts`, `tests/e2e/character-workspace.spec.ts`.
|
||
- **Interfaces:** scalar `age/gender/mbti/speechPattern/speechStyle/appearance/region/originalTitle/originalLink/characterType`; arrays `tags/hobbies/values/goals/relationships/personalities/backgrounds/memories`; update `region`은 읽기 전용.
|
||
- **범위 밖:** 계약에 없는 최대 길이·배열 개수, creator field, 새로운 form framework 도입.
|
||
|
||
- [x] **RED:** create/update에서 각 scalar·array 대표값과 null/empty canonical payload를 검증하는 form/API test를 실패시킨다.
|
||
- [x] **GREEN:** 기존 두 Page 안에서 section별 최소 field/row editor를 추가하고 현재 multipart serializer로 전송한다.
|
||
- [x] **REFACTOR:** 반복 row helper는 실제 8개 소비만 공유하고 dirty/clear semantics를 정리한다.
|
||
- **실행 명령:** `npm run test:run -- src/features/characters`; `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts`.
|
||
- **기대 결과:** exit 0, OpenAPI property 누락 0건, update region request 0건, 생성 필수 field 회귀 0건.
|
||
- **수동 확인:** desktop/tablet create/edit에서 모든 section 입력·추가·삭제와 저장 후 재조회 값을 확인한다.
|
||
|
||
**Progress — 2026-07-30 P3-R1 완료**
|
||
|
||
- **RED:** `npm run test:run -- src/features/characters/tests/CharacterCreatePage.test.tsx src/features/characters/tests/CharacterEditPage.test.tsx`에서 신규 3개 test가 `나이`, `원작 제목` 등 누락 label 때문에 실패하는 것을 확인했다.
|
||
- **GREEN:** `CharacterOptionalFields`와 optional field serialization helper를 추가하고 create/edit page에서 optional scalar·반복 배열을 multipart `request`에 직렬화했다. `npm run test:run -- src/features/characters/tests/CharacterCreatePage.test.tsx src/features/characters/tests/CharacterEditPage.test.tsx src/features/characters/tests/character-api.test.ts` 결과 3 files / 18 tests passed.
|
||
- **REFACTOR/회귀:** `npm run test:run -- src/features/characters` 결과 6 files / 26 tests passed. `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts` 결과 41 passed / 3 skipped. optional 입력 추가로 keyboard-only E2E의 tab 한도만 80회로 조정했다. `npm run typecheck`, `npm run lint`, `npm run build` 모두 exit 0.
|
||
- **정정:** 독립 리뷰에서 수정 상세 응답에 없는 `originalTitle/originalLink`를 일반 저장 시 `null`로 전송할 수 있는 blocker를 확인했다. RED `npm run test:run -- src/features/characters/tests/CharacterEditPage.test.tsx`는 2 failed로 해당 전송을 재현했고, touched flag를 추가해 사용자가 입력·삭제한 경우에만 update payload에 포함하도록 수정했다. 이후 `npm run test:run -- src/features/characters/tests/CharacterCreatePage.test.tsx src/features/characters/tests/CharacterEditPage.test.tsx src/features/characters/tests/character-api.test.ts`는 3 files / 18 tests passed, `npm run test:run -- src/features/characters`는 6 files / 26 tests passed, `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts`는 41 passed / 3 skipped, `npm run typecheck`, `npm run lint`, `npm run build`는 모두 exit 0이었다.
|
||
|
||
### Task R3.2 — Character form 실패 복구와 image format 검증
|
||
|
||
**Goal 실행 `P3-R2`:** Character mutation 실패 뒤 입력을 유지해 재시도하고 JPEG/PNG extension↔MIME pair를 제출 전에 검증한다.
|
||
|
||
- **연결 리뷰:** [Phase 3 재리뷰](./reviews/review-phase-3-20260729.md) — `REV-P3-002`, `REV-P3-003`
|
||
- **시작 조건:** `P3-R1` form 구조 또는 충돌 없는 현재 form, `FILE-002`, 공통 `ApiError`.
|
||
- **Files:** Create: `src/features/characters/validation/character-image-policy.ts`; Modify: `CharacterCreatePage.tsx`, `CharacterEditPage.tsx`, 해당 page test.
|
||
- **Interfaces:** `.jpg/.jpeg ↔ image/jpeg`, `.png ↔ image/png`, 10,485,760 bytes; form error는 server message를 보존하고 `finally`로 pending을 해제한다.
|
||
- **범위 밖:** backend image sniffing, 공통 crop 계산(`P1-R7`).
|
||
|
||
- [x] **RED:** create/update rejection 뒤 button 재활성·오류 표시·두 번째 성공과 양방향 MIME mismatch 거부 test를 작성한다.
|
||
- [x] **GREEN:** form-level try/catch/finally와 Character image policy를 적용한다.
|
||
- [x] **REFACTOR:** create/edit 중복 validation·error copy만 작게 공유하고 Character 전체 회귀를 실행한다.
|
||
- **실행 명령:** `npm run test:run -- src/features/characters`; `npm run typecheck`; `npm run lint`.
|
||
- **기대 결과:** exit 0, 실패 뒤 재시도 1회 성공, mismatch multipart 0건.
|
||
- **수동 확인:** server 500 후 입력 유지·재시도와 잘못된 filename/MIME inline 오류를 확인한다.
|
||
|
||
**Progress — 2026-07-30 P3-R2 완료**
|
||
|
||
- **RED:** `npm run test:run -- src/features/characters/tests/CharacterCreatePage.test.tsx src/features/characters/tests/CharacterEditPage.test.tsx` 결과 2 files failed, 6 failed / 13 passed, 2 unhandled `ApiError`였다. 신규 retry test는 서버 메시지를 찾지 못했고, mismatch test는 `JPEG 또는 PNG 파일만 업로드하세요.` inline 오류가 없어 실패했다.
|
||
- **GREEN:** `src/features/characters/validation/character-image-policy.ts`를 추가해 `.jpg/.jpeg ↔ image/jpeg`, `.png ↔ image/png`, 10MB 제한을 검증하고, create/edit page에서 file 선택 시 crop source 전에 inline 오류를 표시했다. mutation submit은 `ApiError` message를 form 오류로 표시하고 `finally`에서 submit 상태를 해제한다. `npm run test:run -- src/features/characters/tests/CharacterCreatePage.test.tsx src/features/characters/tests/CharacterEditPage.test.tsx src/features/characters/tests/character-api.test.ts` 결과 3 files / 24 tests passed.
|
||
- **REFACTOR/회귀:** `npm run test:run -- src/features/characters` 결과 6 files / 32 tests passed. `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts` 병렬 실행은 axe-heavy direct route test가 30초 제한에 걸렸고, focused Chromium 재실행은 1 passed였다. 해당 test에만 60초 timeout을 부여한 뒤 `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts --workers=1` 결과 41 passed / 3 skipped였다. `npm run typecheck`, `npm run lint`, `npm run build`는 모두 exit 0이었다. LSP diagnostics도 변경 TS/TSX와 E2E 파일에서 오류 없음이었다.
|
||
|
||
### Task R3.3 — 워크스페이스 탭 keyboard semantics와 운영 문구 복구
|
||
|
||
**Goal 실행 `P3-R3`:** 캐릭터 워크스페이스의 탭/내비게이션 의미와 keyboard 이동을 일치시키고 내부 Task 문구를 운영자 안내에서 제거한다.
|
||
|
||
- **연결 리뷰:** [Phase 3 현재 리뷰](./reviews/phase3-character-workspace.md) — `REV-P3-004`, `REV-P3-005`
|
||
- **시작 조건:** PRD `7.2`, `14.2`의 1차 탭·keyboard-only 이동과 비활성 read-only 안내.
|
||
- **완료 증거:** 실패하는 keyboard/accessible-role test, 최소 수정, Character focused unit·mock E2E·접근성 Gate와 검증 기록.
|
||
- **Files:** Modify: `src/layouts/CharacterWorkspaceLayout.tsx`, `src/layouts/CharacterWorkspaceLayout.test.tsx`, `tests/e2e/character-workspace.spec.ts`, 필요 시 `tests/e2e/accessibility.spec.ts`.
|
||
- **Interfaces:** ARIA tab pattern을 유지하면 활성 tab만 page Tab 순서에 두고 좌우 화살표·tabpanel 연결을 구현한다. 페이지 내비게이션으로 단순화하면 `nav`/link와 `aria-current="page"`를 사용해 tab 역할을 제거한다.
|
||
- **범위 밖:** route 구조 변경, 새 router dependency, 비활성 Character 정책 변경.
|
||
|
||
- [x] **RED:** 현재 다섯 `role=tab`이 모두 Tab 순서에 들어가고 좌우 화살표로 이동하지 않는 실패 test, 배너에 `P3-T2`가 노출되는 실패 test를 작성한다.
|
||
- [x] **GREEN:** 한 가지 일관된 접근성 패턴만 적용하고 배너를 운영 영향·복원 미지원 중심의 한국어 문구로 교체한다.
|
||
- [x] **REFACTOR:** active 상태와 panel 연결 중복을 작은 data structure로 정리하되 범용 tab abstraction은 만들지 않는다.
|
||
- **실행 명령:** `npm run test:run -- src/layouts src/features/characters`; `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts tests/e2e/accessibility.spec.ts`; `npm run typecheck`; `npm run lint`; `npm run build`.
|
||
- **기대 결과:** 모든 명령 exit 0, 선언한 role과 keyboard 동작 불일치 0건, 사용자 화면의 `P3-T2` 노출 0건.
|
||
- **수동 확인:** keyboard-only로 프로필 → 오디오 → 시리즈 → 커뮤니티 → FanTalk 이동과 비활성 배너 문구를 확인한다.
|
||
|
||
**Progress — 2026-07-30 P3-R3 완료**
|
||
|
||
- **RED:** `npm run test:run -- src/layouts/CharacterWorkspaceLayout.test.tsx` 결과 1 file failed, 2 failed / 3 passed. 신규 native navigation expectation은 `캐릭터 워크스페이스 내비게이션` role을 찾지 못하고 기존 `tablist/tab`을 노출해 실패했으며, 비활성 배너는 `P3-T2` 내부 문구를 계속 표시해 운영 문구 assertion이 실패했다.
|
||
- **GREEN:** `CharacterWorkspaceLayout`의 workspace 전환 UI를 `nav` + native link + `aria-current="page"` 패턴으로 단일화하고, active link data structure로 중복을 줄였다. 비활성 배너는 조회 전용 운영 영향, 복원 기능 미지원, 영구 삭제와 삭제 후 복구 미제공을 설명하는 한국어 문구로 교체했다. `npm run test:run -- src/layouts/CharacterWorkspaceLayout.test.tsx` 결과 1 file / 5 tests passed.
|
||
- **REFACTOR/회귀:** `npm run test:run -- src/features/characters src/layouts/CharacterWorkspaceLayout.test.tsx src/features/community-posts/tests/community-list.test.tsx src/features/fan-talks/tests/fan-talk-list.test.tsx` 결과 9 files / 45 tests passed. `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts --project=chromium` 결과 11 passed. `npm run typecheck`와 `npm run lint`는 모두 exit 0이었다. 변경 TS/TSX 파일 LSP diagnostics는 모두 오류 0건이었다.
|
||
|
||
### Task R3.4 — 워크스페이스 header characterId 복구
|
||
|
||
**Goal 실행 `P3-R4`:** 모든 캐릭터 워크스페이스 header에 현재 numeric `characterId`를 다시 표시해 운영자가 작업 대상을 식별할 수 있게 한다.
|
||
|
||
- **연결 리뷰:** [Phase 3 현재 리뷰](./reviews/phase3-character-workspace.md) — `REV-P3-006`
|
||
- **시작 조건:** PRD §7.2의 이미지·이름·활성 상태·`characterId` 상시 표시 기준.
|
||
- **완료 증거:** 현재 ID 미노출 assertion을 뒤집은 실패 test, header 최소 표시 수정, Character/layout focused unit·mock E2E·접근성 Gate 통과.
|
||
- **Files:** Modify: `src/layouts/CharacterWorkspaceLayout.tsx`, `src/layouts/CharacterWorkspaceLayout.test.tsx`, `tests/e2e/character-workspace.spec.ts`; Docs: `P3-R4` 수정 후 검증 기록.
|
||
- **Interfaces:** API의 numeric `character.id`를 운영 label과 함께 표시하고 `characterUUID` 프로필 필드는 별도 값으로 유지한다.
|
||
- **범위 밖:** route ID 형식 변경, UUID를 `characterId`로 대체, 전체 상세 metadata 재배치.
|
||
|
||
- [x] **RED:** active/inactive workspace header 모두 현재 `characterId`를 표시해야 한다는 실패 test를 작성하고 기존 미노출 assertion을 제거했다.
|
||
- [x] **GREEN:** 이름·상태 주변에 구분되는 `characterId` label/value를 표시했다.
|
||
- [x] **REFACTOR:** Series visual QA의 raw 내부 field-name 검색은 요구사항상 표시해야 하는 header ID를 예외 처리하고 실제 원치 않는 raw label 검사는 유지했다.
|
||
- **실행 명령:** `npm run test:run -- src/layouts/CharacterWorkspaceLayout.test.tsx src/features/characters`; `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts --project=chromium`; `npm run typecheck`; `npm run lint`; `npm run build`.
|
||
- **기대 결과:** 모든 명령 exit 0, 모든 workspace header의 현재 numeric ID 표시, 다른 캐릭터 ID 혼입 0건.
|
||
- **수동 확인:** Character 101/202의 프로필·Audio·Series·Community·FanTalk header에서 각각 올바른 ID를 확인한다.
|
||
|
||
**P3-R4 수정 검증 기록 (2026-07-31):**
|
||
|
||
- RED: `npm run test:run -- src/layouts/CharacterWorkspaceLayout.test.tsx` — 2 failed / 3 passed. active `101`과 inactive `202` workspace header에서 `characterId:` 표시를 찾지 못해 기대대로 실패했다.
|
||
- GREEN focused: `npm run test:run -- src/layouts/CharacterWorkspaceLayout.test.tsx src/features/characters` — 성공, 7 files / 41 tests passed. header의 이름·상태 근처에 numeric `characterId`가 표시되고 기존 프로필 `characterUUID`는 별도 값으로 유지됨을 확인했다.
|
||
- REFACTOR: `rg -n 'characterId:|raw|SUN|PROCEEDING|genreId' tests/e2e src/features/series src/layouts`로 자동 raw-leak assertion을 확인했고, 현재 Series raw label 검사는 `characterId:`를 금지하지 않아 추가 변경하지 않았다.
|
||
- 개발 중 E2E: 사용자 지시에 따라 전체 Task 완료 전 반복 실행하지 않았다. 지정 mock Character E2E는 최종 회귀 단계에서 필요 시 실행한다.
|
||
|
||
### Task R3.5 — Character URL query 계약 키 정렬
|
||
|
||
**Goal 실행 `P3-R5`:** Character 목록의 브라우저 검색 상태를 API 계약과 같은 `searchTerm` query로 읽고 쓰며 직접 링크 복원을 보장한다.
|
||
|
||
- **연결 리뷰:** [Phase 3 현재 리뷰](./reviews/phase3-character-workspace.md) — `REV-P3-007`
|
||
- **시작 조건:** PRD §7.1과 OpenAPI `GET /api/v2/admin/ai-characters`의 `searchTerm`, `page`, `size` query 계약.
|
||
- **완료 증거:** `?searchTerm=루나` 직접 진입과 검색 변경 URL assertion의 실패 test, 목록 URL parser/serializer 최소 수정, focused unit·mock E2E·정적 Gate 통과.
|
||
- **Files:** Modify: `src/features/characters/pages/CharacterListPage.tsx`, `src/features/characters/tests/character-list.test.tsx`, `tests/e2e/character-workspace.spec.ts`.
|
||
- **Interfaces:** 브라우저 URL과 API request 모두 `searchTerm`을 사용하고 `page`, `size`, 300ms 검색 동작은 유지한다.
|
||
- **범위 밖:** legacy `search` query의 장기 호환 layer, router dependency 도입, server-side filter 추가.
|
||
|
||
- [x] **RED:** `?searchTerm=루나&page=1&size=20` 직접 진입이 input·request를 복원하고 검색 변경 URL에 `search`가 없어야 한다는 실패 test를 작성했다.
|
||
- [x] **GREEN:** URL read/write key를 `searchTerm`으로 교체하고 adapter 인자를 그대로 연결했다.
|
||
- [x] **REFACTOR:** 내부 state 이름은 필요한 범위에서만 정리하고 별도 query abstraction을 만들지 않았다.
|
||
- **실행 명령:** `npm run test:run -- src/features/characters/tests/character-list.test.tsx`; `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts --project=chromium`; `npm run typecheck`; `npm run lint`; `npm run build`.
|
||
- **기대 결과:** 모든 명령 exit 0, Character URL의 검색 key는 `searchTerm` 하나이고 직접 링크·검색·pagination 왕복이 일치.
|
||
- **수동 확인:** 검색 결과 2페이지 URL을 새 탭에서 열어 같은 검색어·page·size와 목록을 복원하는지 확인한다.
|
||
|
||
**P3-R5 수정 검증 기록 (2026-07-31):**
|
||
|
||
- RED: `npm run test:run -- src/features/characters/tests/character-list.test.tsx` — 2 failed. 핵심 실패는 `?searchTerm=루나&page=1&size=20` 직접 진입 뒤 API request의 `searchTerm`이 `null`인 점이었다. 두 번째 loading flow는 같은 parser 불일치로 초기 대기 Promise가 열리지 않아 timeout됐다.
|
||
- GREEN focused: `npm run test:run -- src/features/characters/tests/character-list.test.tsx` — 성공, 1 file / 2 tests passed. 직접 링크 input·request 복원과 검색 변경 URL의 `searchTerm` 사용, `search=` 미사용을 확인했다.
|
||
- REFACTOR/회귀: `npm run test:run -- src/features/characters` — 성공, 6 files / 36 tests passed. `npm run typecheck`, `npm run lint`, `npm run build`, `git diff --check` — 모두 exit 0 또는 no output. LSP diagnostics: `CharacterListPage.tsx`, `character-list.test.tsx` 오류 0건.
|
||
- 개발 중 E2E: 사용자 지시에 따라 전체 Task 완료 전 반복 실행하지 않았다. 지정 mock Character E2E는 최종 회귀 단계에서 필요 시 실행한다.
|
||
|
||
### Task R3.6 — Character image crop 확정 경계 보강
|
||
|
||
**Goal 실행 `P3-R6`:** Character 생성·수정에서 원본 파일을 crop 적용 전에 mutation payload로 확정하지 않고 준비·취소·연속 선택 경계를 안전하게 처리한다.
|
||
|
||
- **연결 리뷰:** [Phase 3 현재 리뷰](./reviews/phase3-character-workspace.md) — `REV-P3-008`
|
||
- **시작 조건:** `FILE-007`, `FILE-009~010`, PRD §10.5 저장 중 feedback와 Character image 필수/optional 계약.
|
||
- **완료 증거:** 원본 제출·crop 취소·준비 중 제출·stale selection을 재현하는 실패 test, staged selection 최소 수정, Character focused unit·mock E2E·file Gate 통과.
|
||
- **Files:** Modify: `src/features/characters/pages/CharacterCreatePage.tsx`, `src/features/characters/pages/CharacterEditPage.tsx`, 관련 Character tests와 `tests/e2e/character-workspace.spec.ts`; 필요 시 기존 Character image helper.
|
||
- **Interfaces:** committed `image`는 crop `onApply` 결과만 가리킨다. 준비/crop 중 제출은 비활성화·guard하고, 취소는 생성의 새 선택을 제거하며 수정의 기존 서버 media를 유지한다. 마지막 선택만 dialog를 열고 준비 실패를 inline 표시한다.
|
||
- **범위 밖:** 공통 crop dialog 계산 변경, 새 범용 upload state library, backend image sniffing.
|
||
|
||
- [x] **RED:** create의 crop 미적용 원본 제출과 취소 뒤 제출, create/edit의 pending source 준비 중 submit, 느린 첫 선택이 빠른 둘째 선택을 덮는 경우를 실패 test로 고정했다.
|
||
- [x] **GREEN:** raw file과 committed crop result를 분리하고 selection token·준비 상태·오류 처리·submit guard를 추가했다.
|
||
- [x] **REFACTOR:** 새 전역 abstraction 없이 create/edit 내부의 작은 중복 상태로 유지하고, 기존 Character App 회귀 test는 crop apply 경로를 지나도록 정렬했다.
|
||
- **실행 명령:** `npm run test:run -- src/features/characters`; `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts --project=chromium`; `npm run typecheck`; `npm run lint`; `npm run build`.
|
||
- **기대 결과:** 모든 명령 exit 0, Character multipart의 uncropped 원본 0건, 취소·실패·stale source의 mutation 0건, 정상 crop 결과만 전송.
|
||
- **수동 확인:** 느린 이미지에서 연속 선택·취소·적용·즉시 제출을 반복해 선택한 마지막 crop 결과만 preview와 request에 반영되는지 확인한다.
|
||
|
||
#### P3-R6 수정 검증 기록 — 2026-07-31
|
||
|
||
- RED: `npm run test:run -- src/features/characters/tests/CharacterCreatePage.test.tsx src/features/characters/tests/CharacterEditPage.test.tsx` — 기대 실패 확인, 2 files failed / 6 failed / 23 passed 및 reject 준비 실패 unhandled rejection을 확인했다. 핵심 실패는 생성 pending submit button 미비활성, 생성 crop cancel 뒤 raw file 잔존, 수정 pending submit button 미비활성, 준비 reject inline 오류 미표시였다.
|
||
- GREEN: `CharacterCreatePage.tsx`와 `CharacterEditPage.tsx`에서 committed `image`를 crop apply 결과만 보유하도록 분리하고, selection id·준비 상태·stale ignore·reject inline 오류·submit disabled/guard를 추가했다. `npm run test:run -- src/features/characters/tests/CharacterCreatePage.test.tsx src/features/characters/tests/CharacterEditPage.test.tsx` — 성공, 2 files / 29 tests passed.
|
||
- Character 회귀: `npm run test:run -- src/features/characters` — 성공, 6 files / 43 tests passed. `character-mutation-reload.test.tsx`는 App 생성 성공 경로가 실제 crop dialog 적용 후 저장하도록 보강했다.
|
||
- 정적 검증: `npm run typecheck`, `npm run lint`, `npm run build`, `git diff --check` — 모두 성공(exit 0). Build는 255 modules transformed, production bundle 생성 완료.
|
||
- LSP: `lsp_diagnostics`는 `CharacterCreatePage.tsx`, `CharacterEditPage.tsx`, `character-mutation-reload.test.tsx`에서 오류 0건이었다. `CharacterCreatePage.test.tsx`, `CharacterEditPage.test.tsx` 단일 fresh diagnostics는 timeout이었고, `lsp_diagnostics src/features/characters`와 `npm run typecheck`로 characters 디렉터리 오류 0건을 보완 확인했다.
|
||
- E2E: 사용자 지시가 “개발 중 unit tests만, 전체 Task 이후 필요 시 E2E”였으므로 `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts --project=chromium`은 이번 P3-R6 개발 중 실행하지 않고 후속 최종 회귀 단계로 이연했다.
|
||
|
||
---
|
||
|
||
## Phase 4. Audio vertical slice
|
||
|
||
**목표:** 선택 Character의 Audio를 제목 검색·검수·발행·수정·비활성화하고 대용량 upload를 안전하게 제어한다.
|
||
|
||
**Phase Goal `P4`:** Task 4.1 → 4.4와 Phase 4 Gate로 Audio 조회·재생·발행·upload slice를 완성한다.
|
||
|
||
- **시작 조건:** Phase 3 workspace core(`P3-T2`)와 `P2-GATE` 완료.
|
||
- **완료 조건:** `P4-T1`~`P4-T4`, `P4-GATE` 완료. 오류·price 계약은 제공값 또는 명시된 최소 규칙으로 종결.
|
||
- **실행 순서:** 계약 확인 → 목록/상세/player → form/upload → 반응형·접근성.
|
||
|
||
**요구사항:** `AUDIO-001~033`, `FILE-001~002`, `FILE-006~009`, `FILE-012~013`, PRD `9`의 Audio 범위.
|
||
|
||
**외부 의존:** `EXT-007` active-only 반환 보장. price 상한은 `99999`이고, 파일 용량·MIME 등 backend 동일 검증은 확인됐다. OpenAPI 밖의 Audio 도메인 오류 message key는 분기하지 않는다. status filter·답변 없는 status badge는 현재 범위에서 제외한다.
|
||
|
||
**주요 Files:**
|
||
|
||
- 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`
|
||
- Create: `src/features/audio-contents/pages/{AudioContentListPage,AudioContentDetailPage,AudioContentFormPage}.tsx`
|
||
- Create: `src/features/audio-contents/components/{AudioContentList,AudioContentListItem,AudioContentSummary,AudioContentForm,AudioContentThemeSelect,ReleaseScheduleField,SeriesMultiCombobox}.tsx`
|
||
- Create: `src/features/audio-contents/tests/{audio-contract,audio-upload}.test.ts`
|
||
- Create: `src/features/audio-contents/tests/{audio-list,audio-player,audio-form}.test.tsx`
|
||
- Create: `tests/e2e/audio-content.spec.ts`
|
||
|
||
#### Phase 4 Task 실행 계약
|
||
|
||
| Goal | Files | Interfaces | TDD·검증 기준 |
|
||
|---|---|---|---|
|
||
| `P4-T1` | Modify: `docs/20260725_AI캐릭터관리자웹/plan-task.md`; Read: `docs/20260725_AI캐릭터관리자웹/prd.md`, `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~033`, `MOCK-001~009`와 OpenAPI Audio·theme operation/schema 확인.
|
||
- **완료 증거:** 체크박스 전체, contract fixture와 상태/action inventory의 세 문서 일치.
|
||
- **범위 밖:** 오류 status/key 또는 price 상한 추정과 Audio UI 구현.
|
||
|
||
- [x] OpenAPI 공통 오류 status·shape를 fixture에 기록하고 Audio 전용 message key와 backend container·codec 오류 계약은 미제공으로 표시한다. 정확한 fixture를 추정하지 않는다.
|
||
- [x] 오디오 테마 목록 `GET /api/v2/admin/ai-characters/audio-content-themes`가 query/body 없이 호출되고 `data[]`의 `id`, `theme`, `image`를 반환하는 contract fixture를 기록한다.
|
||
- [x] price 최대값이 제공되면 schema와 경계 test를 추가하고, 없으면 상한을 만들지 않는다.
|
||
- [x] 목록 `search_word/page/size`, `data.totalCount/items`, 상세 필수 `timezone`, 생성 `contentFile/coverImage/request`, 생성 `data.contentId`, 수정 `data=null`을 contract fixture로 고정한다.
|
||
- [x] status query·status field가 없음을 고정하고 client status filter·status enum을 만들지 않는다.
|
||
- [x] 목록·상세·player·form/upload의 상태/action inventory를 작성하고 Page는 route/query/policy 조합, feature component는 Audio 규칙, shared component는 Phase 1 contract를 사용하도록 component map을 확정한다.
|
||
- [x] 제공 계약 범위의 Audio seed와 목록·상세·player·theme·upload CRUD browser handler 시나리오를 확정한다.
|
||
|
||
### Task 4.2 목록·상세·player
|
||
|
||
**Goal 실행 `P4-T2`:** Audio 목록·상세 복원과 안전한 단일 재생 흐름을 완성한다.
|
||
|
||
- **시작 조건:** `P4-T1` 완료.
|
||
- **완료 증거:** 체크박스 전체, list/detail/player test와 signed URL 비기록·no-auto-refetch 기록.
|
||
- **범위 밖:** 생성·수정·upload form.
|
||
|
||
- [x] UI 검색어가 2자 이상일 때만 API `search_word`로 직렬화되고 `page`, `size` URL 상태와 `data.items` 역직렬화, loading·empty·error·retry가 동작하는 test를 작성한다.
|
||
- [x] status query·활성 query·client-side status/active filter request가 0회임을 test한다.
|
||
- [x] Audio detail route의 직접 진입과 새로고침에서 `timezone=Asia/Seoul`을 보내 같은 resource를 복원하는 test를 작성한다.
|
||
- [x] 목록과 상세가 Phase 1 `AdminAudioPlayer`를 조합하고 play/pause, seek, current/duration, volume, speed, keyboard를 지원하는 integration test를 작성한다.
|
||
- [x] 한 player 재생 시 기존 player가 정지되고 명시적 download button이 없음을 test한다.
|
||
- [x] media error를 signed URL 만료로 추정하지 않고 일반 오류·수동 재시도·페이지 새로고침 안내를 표시한다.
|
||
- [x] media error만으로 목록/detail GET과 `play()`가 자동 재호출되지 않는 test를 작성한다.
|
||
- [x] signed URL이 log·storage·분석 event로 전달되지 않는 test를 작성한다.
|
||
- [x] mock mode는 실제 endpoint shape와 local preview audio asset으로 목록 → 상세 → 단일 재생 최종 UI를 확인한다.
|
||
|
||
### Task 4.3 발행 form·upload
|
||
|
||
**Goal 실행 `P4-T3`:** theme 선택, 즉시/예약 발행, multipart media와 진행률·취소·재시도를 완성한다.
|
||
|
||
- **시작 조건:** `P4-T1`, `P4-T2` 완료.
|
||
- **완료 증거:** 체크박스 전체, contract/form/upload/file-boundary test, `OQ-009` 결정과 검증 기록.
|
||
- **범위 밖:** resumable upload, client codec 판정, 계약 없는 price 상한.
|
||
|
||
- [x] 생성 multipart의 `contentFile`, `coverImage`, `request`가 필수이고 수정에는 optional `coverImage`와 `request`만 있으며 content file 교체 part·UI가 없음을 test한다.
|
||
- [x] Audio cover가 Phase 1 `FileField`·`ImageCropDialog`의 JPEG/PNG·10MB, `1:1`, 최대 800px, no-upscale profile을 조합하는 test를 작성한다.
|
||
- [x] MP3/AAC/M4A 허용, WAV 거부, extension/MIME 조합을 test한다.
|
||
- [x] `.m4a + audio/x-m4a`만 호환 조합으로 허용하고 실제 container·codec 판정은 server 책임으로 둔다.
|
||
- [x] `1,024,000,000 bytes` 허용, `1,024,000,001 bytes` 거부 경계 test를 작성한다.
|
||
- [x] price는 0 이상 정수 “캔”으로 입력·format한다.
|
||
- [x] 생성 form은 오디오 테마 목록을 불러와 visible label이 있는 선택 UI를 제공하고, 미선택 제출을 차단하며 선택한 `themeId`를 create payload에 포함하는 test를 작성한다.
|
||
- [x] 즉시 공개 기본값은 날짜 입력을 비활성화·초기화하고 `releaseDate=null`, `timezone="Asia/Seoul"`을 보낸다.
|
||
- [x] 예약 공개는 미래 Asia/Seoul 시각만 받고 `yyyy-MM-dd HH:mm` 문자열과 `timezone="Asia/Seoul"`을 보내며 UTC `Z`로 변환하지 않는 test를 작성한다.
|
||
- [x] 수정 form은 계약에 없는 release schedule·theme·series·content file 변경 control을 만들지 않고 기존 값을 읽기 전용으로 표시한다.
|
||
- [x] create request에 필수 `title`, `detail`, `tags`, `price`, 유효한 `themeId`가 있고 `status`, `isActive`, `seriesIds`가 없음을 contract test로 고정한다.
|
||
- [x] create optional purchase/limited/adult/preview/point/comment/detail/language field의 enum·type·OpenAPI default와 serializer를 test한다.
|
||
- [x] update request는 `title`, `detail`, `tags`, `price`, `isAdult`, `isActive`, `isPointAvailable`, `isCommentAvailable` 이외 field를 보내지 않고 soft delete에만 `isActive=false`를 보낸다.
|
||
- [x] upload 진행률, AbortController 취소, 전체 재시도, 실패 후 form/file 상태 보존을 test한다.
|
||
- [x] 415 server 오류를 field 안내로 보존하고 resumable upload는 만들지 않는다.
|
||
- [x] create 성공의 `data.contentId`로 상세에 이동하고, update/soft delete의 `data=null` 성공은 기존 ID cache를 무효화한다. soft delete 후 목록 이동과 toast를 제공하며 active-only 제거는 외부 계약 제공 후 server mode에서 검증한다.
|
||
- [x] 초기 Audio form을 실제 페이지에서 확인한 뒤 `title`, `detail`, `tags`의 최대 길이 권고안을 작성한다. backend 호환 확인 전에는 상한을 구현하지 않는다.
|
||
- [x] mock handler가 multipart request contract를 검증하고 create/update/deactivate 후 같은 store의 list/detail을 갱신하는 E2E를 작성한다.
|
||
|
||
### Task 4.4 Audio 반응형·접근성
|
||
|
||
**Goal 실행 `P4-T4`:** Audio viewport capability와 player/form 접근성 기준을 검증한다.
|
||
|
||
- **시작 조건:** `P4-T2`, `P4-T3`의 활성 범위 완료.
|
||
- **완료 증거:** 체크박스 전체, 320px·keyboard·200% zoom·axe E2E 기록.
|
||
- **범위 밖:** 모바일 create/edit/deactivate/upload.
|
||
|
||
- [x] mobile에서는 목록·상세·player만 제공하고 create/edit/deactivate/upload는 숨김이 아닌 route capability로 차단한다.
|
||
- [x] desktop/tablet에서는 모든 form·upload action을 제공한다.
|
||
- [x] 320px에서 player control, error text, 긴 title이 overflow하지 않는 E2E를 작성한다.
|
||
- [x] keyboard-only player/form, 200% zoom, axe critical·serious 0건을 확인한다.
|
||
|
||
### Phase 4 Gate
|
||
|
||
**Goal 실행 `P4-GATE`:** Audio mock UI journey와 실제 server integration 상태를 분리해 판정한다.
|
||
|
||
- **시작 조건:** `P4-T1`~`P4-T4` 완료.
|
||
- **완료 증거:** 아래 명령과 Expected 통과 및 Phase 검증 기록.
|
||
- **범위 밖:** 실패와 무관한 Series/Community 구현.
|
||
|
||
```bash
|
||
npm run test:run -- src/features/audio-contents src/shared/ui src/shared/validation
|
||
npm run e2e:mock -- tests/e2e/audio-content.spec.ts
|
||
npm run e2e -- tests/e2e/audio-content.spec.ts
|
||
npm run typecheck
|
||
npm run lint
|
||
npm run build
|
||
```
|
||
|
||
**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 요청·화면에 일치하는지 본다.
|
||
|
||
### Task R4.1 — Audio create 설정과 raw price validation 완성
|
||
|
||
**Goal 실행 `P4-R1`:** Audio create optional 설정을 form에서 편집하고 음수·소수 가격을 변형하지 않고 거부한다.
|
||
|
||
- **연결 리뷰:** [Phase 4 재리뷰](./reviews/review-phase-4-20260729.md) — `REV-P4-001`, `REV-P4-002`
|
||
- **시작 조건:** `AUDIO-019`, `AUDIO-033`, OpenAPI `AudioContentCreateRequest`.
|
||
- **Files:** Modify: `AudioContentForm.tsx`, `audio-content-form-helpers.ts`, `audio-content-schema.ts`, `audio-form.test.tsx`, `audio-contract.test.ts`, `tests/e2e/audio-content.spec.ts`.
|
||
- **Interfaces:** `purchaseOption/limited/isAdult/isGeneratePreview/isOnlyRental/isPointAvailable/isCommentAvailable/isFullDetailVisible/previewStartTime/previewEndTime/languageCode`; raw price는 `0..99999` integer만 serialize.
|
||
- **범위 밖:** OpenAPI 밖 상관관계 validation, update에서 계약상 수정 불가한 field.
|
||
|
||
- [x] **RED:** optional control별 대표 payload와 `-1`, `1.5` request 0건 test를 작성한다.
|
||
- [x] **GREEN:** contract default로 초기화한 visible control과 raw/display price state를 최소 구현한다.
|
||
- [x] **REFACTOR:** request builder에 hard-coded field를 제거하고 create schema/form/E2E를 재실행한다.
|
||
- **실행 명령:** `npm run test:run -- src/features/audio-contents/tests/audio-form.test.tsx src/features/audio-contents/tests/audio-contract.test.ts`; `npm run e2e:mock -- tests/e2e/audio-content.spec.ts`.
|
||
- **기대 결과:** exit 0, optional property 입력 누락 0건, `-1/1.5/100000` request 0건, `0/99999` 허용.
|
||
- **수동 확인:** desktop/tablet에서 설정별 label·keyboard 입력과 가격 오류 focus를 확인한다.
|
||
- **완료 기록(2026-07-30):** create 설정 control·payload와 raw price invalid 상태를 구현하고 `REV-P4-001`, `REV-P4-002`를 수정 완료 처리했다. `npm run test:run -- src/features/audio-contents` 8 files / 35 tests passed, `npm run e2e:mock -- tests/e2e/audio-content.spec.ts` 21 passed / 3 skipped, `npm run typecheck`, `npm run lint`, `npm run build`, targeted `git diff --check` 통과.
|
||
|
||
### Task R4.2 — upload 중복 제출과 401 만료 복구
|
||
|
||
**Goal 실행 `P4-R2`:** Audio upload pending 동안 POST를 한 번만 실행하고 XHR 401을 공통 session expiry 흐름으로 처리한다.
|
||
|
||
- **연결 리뷰:** [Phase 4 재리뷰](./reviews/review-phase-4-20260729.md) — `REV-P4-003`, `REV-P4-004`
|
||
- **시작 조건:** `P1-R6`의 인증 만료 interface 또는 동일 callback contract 확정.
|
||
- **Files:** Modify: `AudioContentForm.tsx`, `upload-audio-content.ts`, `audio-form-upload.test.tsx`, `audio-upload.test.ts`, `AudioContentFormPage.tsx`, app API dependency 조합.
|
||
- **Interfaces:** upload adapter는 token reader와 `onAuthExpired`를 주입받고 protected 401 burst에서 session clear/login 1회; form은 pending ref와 disabled 상태를 가진다.
|
||
- **범위 밖:** resumable upload, refresh token, Axios migration.
|
||
|
||
- [x] **RED:** unresolved upload에 submit 두 번 시 POST 2건인 실패와 XHR malformed/normal 401에서 session이 남는 실패 test를 추가한다.
|
||
- [x] **GREEN:** synchronous pending guard/disabled button과 공통 auth expiry dependency를 적용한다.
|
||
- [x] **REFACTOR:** abort/error/success 모든 종료 경로에서 pending/controller cleanup을 한곳으로 정리한다.
|
||
- **실행 명령:** `npm run test:run -- src/features/audio-contents/tests/audio-form-upload.test.tsx src/features/audio-contents/tests/audio-upload.test.ts src/shared/api/__tests__/client-auth.test.ts`.
|
||
- **기대 결과:** exit 0, pending 중 upload request 1건, 401 clear/login 1회, cancel 후 재제출 가능.
|
||
- **수동 확인:** 느린 upload double-click과 만료 token upload의 login 복귀를 확인한다.
|
||
- **완료 기록(2026-07-30):** pending upload 중 submit 2회가 upload request 1건으로 고정되고 cancel 후 재제출 가능하도록 form guard를 추가했다. XHR upload adapter는 token reader/session clear/login callback을 주입받아 protected 401 burst에서 clear/login 1회만 실행한다. RED 확인: form은 `expected 2 to be 1`, upload는 `clearSession 0회`로 실패. GREEN/검증: focused 2 files / 19 tests passed, Audio 전체 8 files / 38 tests passed, `npm run typecheck`, `npm run lint`, `npm run build`, targeted `git diff --check` 통과.
|
||
|
||
### Task R4.3 — Audio media accept와 cover format pair 정렬
|
||
|
||
**Goal 실행 `P4-R3`:** validator와 file picker가 같은 M4A MIME 집합을 사용하고 cover extension↔MIME 불일치를 거부한다.
|
||
|
||
- **연결 리뷰:** [Phase 4 재리뷰](./reviews/review-phase-4-20260729.md) — `REV-P4-005`, `REV-P4-006`
|
||
- **시작 조건:** `AUDIO-014`, `AUDIO-024`, `FILE-002`.
|
||
- **Files:** Modify: `AudioContentForm.tsx`, `audio-content-form-helpers.ts`, `audio-cover-policy.ts`, `audio-contract.test.ts`, `file-media-policy.test.ts`.
|
||
- **Interfaces:** input accept는 `AUDIO_FILE_POLICY.allowedMimeTypes`; cover mapping은 jpg/jpeg→image/jpeg, png→image/png.
|
||
- **범위 밖:** container/codec sniffing, backend 오류 key.
|
||
|
||
- [x] **RED:** accept에 `audio/mp4` 포함과 cover 양방향 mismatch 거부 test를 작성한다.
|
||
- [x] **GREEN:** shared audio policy에서 accept를 생성하고 cover pair map을 적용한다.
|
||
- [x] **REFACTOR:** 중복 MIME literal을 제거하고 Community audio 회귀를 실행한다.
|
||
- **실행 명령:** `npm run test:run -- src/shared/validation/file-media-policy.test.ts src/features/audio-contents src/features/community-posts`.
|
||
- **기대 결과:** exit 0, canonical/compat M4A 2종 허용, mismatch cover 0건.
|
||
- **수동 확인:** Chrome/Safari file chooser에서 `.m4a` 선택 가능 여부를 확인한다.
|
||
- **완료 기록(2026-07-30):** Audio file picker `accept`를 `AUDIO_FILE_POLICY.allowedMimeTypes`에서 생성하고, cover `.jpg/.jpeg→image/jpeg`, `.png→image/png` mismatch를 upload 전 `mime` 오류로 차단했다. RED 확인: accept는 `audio/mp4` 누락으로 실패, cover mismatch는 `{ ok: true }`로 실패. GREEN/검증: focused 3 files / 24 tests passed, Audio+Community 회귀 12 files / 76 tests passed, `npm run typecheck`, `npm run lint`, `npm run build`, targeted `git diff --check` 통과.
|
||
|
||
### Task R4.4 — Audio 일반 update와 soft delete schema 분리
|
||
|
||
**Goal 실행 `P4-R4`:** Audio 일반 update에서 `isActive`를 금지하고 soft delete adapter만 `{ isActive:false }`를 전송하게 한다.
|
||
|
||
- **연결 리뷰:** [Phase 4 현재 리뷰](./reviews/phase4-audio-content.md) — `REV-P4-007`
|
||
- **시작 조건:** `AUDIO-007`, OpenAPI `AudioContentUpdateRequest`, 기존 `deactivateAudioContent`.
|
||
- **완료 증거:** `isActive=true/null` 실패 test, schema/API 최소 수정, Audio focused unit·mock E2E·Phase Gate와 검증 기록.
|
||
- **Files:** Modify: `src/features/audio-contents/schemas/audio-content-schema.ts`, `src/features/audio-contents/api/audio-content-api.ts`, `src/features/audio-contents/tests/audio-contract.test.ts`, `src/shared/mocks/audio-content-mock-store.ts`.
|
||
- **Interfaces:** 일반 `AudioContentUpdateRequest`에는 `isActive`가 없고, 별도 deactivate schema는 `z.strictObject({isActive:z.literal(false)})`만 허용한다.
|
||
- **범위 밖:** OpenAPI JSON 축소, Audio 복원, backend partial update 의미 변경.
|
||
|
||
- [x] **RED:** 일반 update schema/body가 `isActive=true/null/false`를 거부하고 deactivate body만 false를 허용하는 실패 test를 작성한다.
|
||
- [x] **GREEN:** 일반 update와 deactivate request schema/API helper/mock parser를 분리한다.
|
||
- [x] **REFACTOR:** 기존 update test의 금지된 `isActive:true` 성공 assertion을 제거하고 Community·Series와 같은 불변식 용어로 정리한다.
|
||
- **실행 명령:** `npm run test:run -- src/features/audio-contents src/shared/mocks`; `npm run e2e:mock -- tests/e2e/audio-content.spec.ts`; `npm run typecheck`; `npm run lint`; `npm run build`.
|
||
- **기대 결과:** 모든 명령 exit 0, 일반 Audio update의 `isActive` request 0건, deactivate false 이외 parse 0건.
|
||
- **수동 확인:** Audio 수정 저장과 비활성화 network multipart의 `request` part를 각각 확인한다.
|
||
|
||
**Progress — 2026-07-30 P4-R4 완료**
|
||
|
||
- **RED:** `npm run test:run -- src/features/audio-contents/tests/audio-contract.test.ts`에서 normal update의 `isActive:false`가 거부되지 않아 실패하는 것을 확인했다.
|
||
- **GREEN:** `audioContentUpdateRequestSchema`에서 `isActive`를 제거하고 `audioContentDeactivateRequestSchema`를 `z.strictObject({ isActive: z.literal(false) })`로 분리했다. `deactivateAudioContent`는 별도 deactivate body helper를 사용하고, mock parser/store도 normal update와 deactivate 경로를 분리했다. `npm run test:run -- src/features/audio-contents/tests/audio-contract.test.ts` 결과 1 file / 10 tests passed였다.
|
||
- **REFACTOR/회귀:** Audio MSW handler를 `src/shared/mocks/audio-content-handlers.ts`로 추출해 `handlers.ts`를 238 lines로 낮췄고, `npm run test:run -- src/features/audio-contents src/shared/mocks` 결과 14 files / 67 tests passed였다. `npm run e2e:mock -- tests/e2e/audio-content.spec.ts` 결과 21 passed / 3 skipped였고, skipped 3건은 기존 keyboard-only platform policy다. `npm run typecheck`, `npm run lint`, `npm run build` 모두 exit 0이었다. 변경 TS 파일 LSP diagnostics는 오류 0건이었고, `git diff --check`도 출력 없이 통과했다.
|
||
|
||
### Task R4.5 — Audio URL query 계약 키 정렬
|
||
|
||
**Goal 실행 `P4-R5`:** Audio 목록의 브라우저 검색 상태를 API 계약과 같은 `search_word` query로 읽고 쓰며 직접 링크 복원을 보장한다.
|
||
|
||
- **연결 리뷰:** [Phase 4 현재 리뷰](./reviews/phase4-audio-content.md) — `REV-P4-008`
|
||
- **시작 조건:** PRD §7.1과 OpenAPI Audio 목록의 `search_word`, `page`, `size` query 계약.
|
||
- **완료 증거:** `?search_word=루나` 직접 진입과 검색 변경 URL assertion의 실패 test, URL parser/serializer 최소 수정, Audio focused unit·mock E2E·정적 Gate 통과.
|
||
- **Files:** Modify: `src/features/audio-contents/pages/AudioContentListPage.tsx`, `src/features/audio-contents/tests/audio-list.test.tsx`, `tests/e2e/audio-content.spec.ts`.
|
||
- **Interfaces:** 브라우저 URL과 API request 모두 `search_word`를 사용하고 2자 이상 검색, `page`, `size` 동작은 유지한다.
|
||
- **범위 밖:** legacy `search` query 호환 layer, 검색 최소 글자 수 변경, router dependency 도입.
|
||
|
||
- [x] **RED:** `?search_word=루나&page=1&size=20` 직접 진입이 input·request를 복원하고 검색 변경 URL에 `search`가 없어야 한다는 실패 test를 작성한다.
|
||
- [x] **GREEN:** URL read/write key를 `search_word`로 교체한다.
|
||
- [x] **REFACTOR:** Character와 query helper를 성급히 공유하지 않고 Audio page 내부 변경으로 끝낸다.
|
||
- **실행 명령:** `npm run test:run -- src/features/audio-contents/tests/audio-list.test.tsx`; `npm run e2e:mock -- tests/e2e/audio-content.spec.ts --project=chromium`; `npm run typecheck`; `npm run lint`; `npm run build`.
|
||
- **기대 결과:** 모든 명령 exit 0, Audio URL의 검색 key는 `search_word` 하나이고 직접 링크·검색·pagination 왕복이 일치.
|
||
- **수동 확인:** 검색 결과 URL을 새로고침·새 탭 진입해 검색어와 목록이 유지되는지 확인한다.
|
||
|
||
**Progress — 2026-07-31 P4-R5 완료**
|
||
|
||
- **RED:** `npm run test:run -- src/features/audio-contents/tests/audio-list.test.tsx`에서 `?search_word=루나&page=1&size=20` 직접 진입 후 검색 input이 빈 값으로 남아 실패하는 것을 확인했다.
|
||
- **GREEN:** `AudioContentListPage`의 URL read/write key만 `search_word`로 교체했다. `npm run test:run -- src/features/audio-contents/tests/audio-list.test.tsx` 결과 1 file / 2 tests passed였다.
|
||
- **REFACTOR/회귀:** Character와 공통 query helper를 공유하지 않고 Audio page 내부 변경으로 끝냈다. `npm run test:run -- src/features/audio-contents` 첫 실행은 기존 `audio-list.test.tsx` 병렬 loading timeout 양상으로 실패했으나, 같은 명령 재실행 결과 8 files / 46 tests passed였다. 사용자 지시에 따라 mock E2E는 전체 Task 완료 전까지 보류했다. `npm run typecheck`, `npm run lint`, `npm run build`, `git diff --check`는 모두 exit 0 또는 no output이었다. 변경 TS 파일 LSP diagnostics와 Audio directory diagnostics는 오류 0건이었다.
|
||
|
||
### Task R4.6 — Audio cover crop 준비 경계 보강
|
||
|
||
**Goal 실행 `P4-R6`:** Audio 생성·수정에서 cover crop source 준비와 연속 선택을 추적하고 crop 확정 전 저장을 차단한다.
|
||
|
||
- **연결 리뷰:** [Phase 4 현재 리뷰](./reviews/phase4-audio-content.md) — `REV-P4-009`
|
||
- **시작 조건:** `FILE-006~009`, PRD §10.5 저장 중 feedback와 Audio cover 생성 필수/수정 optional 계약.
|
||
- **완료 증거:** 준비 중 edit 저장·stale source·준비 실패를 재현하는 실패 test, staged selection 최소 수정, Audio focused unit·mock E2E·file Gate 통과.
|
||
- **Files:** Modify: `src/features/audio-contents/components/AudioContentForm.tsx`, 관련 Audio form/file tests와 `tests/e2e/audio-content.spec.ts`.
|
||
- **Interfaces:** `coverImage`는 crop 적용 결과만 보유하고 준비/crop 중 submit을 비활성화·guard한다. 마지막 선택만 유효하며 취소는 기존 서버 cover를 유지하고 준비 실패를 inline 표시한다.
|
||
- **범위 밖:** 대용량 audio upload 상태 변경, 공통 crop 계산 변경, 범용 upload state library.
|
||
|
||
- [x] **RED:** edit에서 crop source 준비 중 저장, 느린 첫 선택의 늦은 dialog, 준비 reject 후 무반응을 실패 test로 고정한다.
|
||
- [x] **GREEN:** cover selection token·준비 상태·오류 처리와 submit guard를 추가한다.
|
||
- [x] **REFACTOR:** 기존 `UploadState`와 cover 준비 상태의 역할을 분리하고 필요한 표시 문구만 Audio form에 둔다.
|
||
- **실행 명령:** `npm run test:run -- src/features/audio-contents`; `npm run e2e:mock -- tests/e2e/audio-content.spec.ts --project=chromium`; `npm run typecheck`; `npm run lint`; `npm run build`.
|
||
- **기대 결과:** 모든 명령 exit 0, 준비/crop 미확정 cover의 저장 0건, stale dialog 0건, 정상 crop result와 기존 cover 유지 경계 일치.
|
||
- **수동 확인:** 기존 cover가 있는 수정 화면에서 느린 파일 두 개를 연속 선택하고 취소·적용·즉시 저장해 마지막 의도가 보존되는지 확인한다.
|
||
|
||
**Progress — 2026-07-31 P4-R6 완료**
|
||
|
||
- **RED:** `npm run test:run -- src/features/audio-contents/tests/audio-form-upload.test.tsx src/features/audio-contents/tests/audio-form-update.test.tsx`에서 준비 중 저장 guard, stale selection, reject 오류 경계가 실패하는 것을 확인했다.
|
||
- **GREEN:** `AudioContentForm`에 cover selection token, `isCoverPreparing`, crop dialog pending guard, reject inline 오류를 추가했다. `coverImage`는 crop 적용 결과만 commit하고, 취소는 수정 화면의 기존 서버 cover 유지 계약을 보존한다. focused 재실행 결과 2 files / 18 tests passed였다.
|
||
- **REFACTOR/회귀:** `FieldErrors` 타입을 `Partial<Record<...>>`로 줄여 `AudioContentForm.tsx` 순수 LOC를 247로 낮췄다. `npm run test:run -- src/features/audio-contents` 결과 8 files / 53 tests passed였다. 사용자 지시에 따라 mock E2E는 전체 Task 완료 전까지 보류했다. `npm run typecheck`, `npm run lint`, `npm run build`, `git diff --check`는 모두 exit 0 또는 no output이었다. Audio directory LSP diagnostics는 16 TSX files / 오류 0건이었다.
|
||
|
||
### Task R4.7 — Audio cover 원본 정책 사전 검증 복구
|
||
|
||
**Goal 실행 `P4-R7`:** Audio 생성·수정에서 새 cover 원본의 확장자·MIME pair와 exact byte 상한을 crop source 생성 전에 검증한다.
|
||
|
||
- **연결 리뷰:** [Phase 4 현재 리뷰](./reviews/phase4-audio-content.md) — `REV-P4-010`
|
||
- **시작 조건:** `FILE-001~003`, `FILE-006~007`, PRD `Image crop UI 흐름` 1~3단계와 기존 `validateAudioCoverFile` 정책.
|
||
- **완료 증거:** `10,485,761 bytes`, GIF, 확장자/MIME 불일치 cover가 `createCropSource`를 호출하거나 crop Dialog를 열지 않는 실패 test, 정상 JPEG/PNG crop 회귀 test, Phase 4 focused·mock E2E·정적 Gate 통과.
|
||
- **Files:** Modify: `src/features/audio-contents/components/AudioContentForm.tsx`, 관련 Audio form tests와 필요 시 `tests/e2e/audio-content.spec.ts`.
|
||
- **Interfaces:** 새 cover는 `validateAudioCoverFile` 성공 뒤에만 `createCropSource`로 전달한다. 실패 시 기존 `coverErrorMessage` 계열 inline 오류를 표시하고 create/edit 모두 기존 committed cover 상태를 보존한다.
|
||
- **범위 밖:** crop 계산·출력 크기 변경, audio 파일 upload 정책 변경, 새 공통 validation abstraction, server validation 변경.
|
||
- **Phase 4 Gate 연결:** 기존 완료 기록은 보존하되 `P4-R7` focused test와 mock Chromium Audio 시나리오, `typecheck`·`lint`·`build` 통과 전에는 이번 리뷰 회귀를 종결하지 않는다.
|
||
- **검증 기록:** RED/GREEN 명령·test 수·exit code와 수동 확인 결과를 이 Task 하단과 Phase 4 리뷰 §9 이후에 날짜순으로 누적한다.
|
||
|
||
- [x] **RED:** create/edit form에서 oversized PNG와 `.png`/`image/jpeg` 불일치 파일을 선택해 `createCropSource` 0회, crop Dialog 0개, 정책 오류 표시를 기대하는 실패 test를 작성한다.
|
||
- [x] **GREEN:** `selectCoverImage`의 crop 준비 전에 기존 Audio cover validator를 호출하고 실패 상태만 최소 처리한다.
|
||
- [x] **REFACTOR:** 정책 값과 메시지를 복제하지 않고 기존 validator·오류 변환을 재사용하며 Series/Character flow를 새 abstraction으로 묶지 않는다.
|
||
- **실행 명령:** `npm run test:run -- src/features/audio-contents`; `npm run e2e:mock -- tests/e2e/audio-content.spec.ts --project=chromium`; `npm run typecheck`; `npm run lint`; `npm run build`; `git diff --check`.
|
||
- **기대 결과:** 모든 명령 exit 0, invalid 원본의 crop source 호출·Dialog 노출·upload request 0건, valid JPEG/PNG의 기존 crop·create/edit 흐름 유지.
|
||
- **수동 확인:** 생성·수정 화면에서 oversized/mismatched/GIF/valid PNG를 차례로 선택해 inline 오류, 기존 cover 유지, 정상 crop 적용을 확인한다.
|
||
|
||
**Progress — 2026-07-31 P4-R7 완료**
|
||
|
||
- **RED:** `npm run test:run -- src/features/audio-contents/tests/audio-form-upload.test.tsx src/features/audio-contents/tests/audio-form-update.test.tsx`는 2 files 중 2 tests failed / 18 passed였다. oversized PNG와 `.png`/`image/jpeg` replacement cover 모두 정책 오류가 없고 crop Dialog로 진입해 의도한 실패를 확인했다.
|
||
- **GREEN:** `AudioContentForm.selectCoverImage`가 `coverErrorMessage`를 crop source 준비 전에 재사용하게 수정했다. 같은 focused command는 2 files / 20 tests passed였고, invalid 원본에서 `createCropSource` 0회·crop Dialog 0개·inline 오류 표시를 확인했다.
|
||
- **REFACTOR/회귀:** 새 공통 abstraction 없이 기존 validator·오류 변환만 재사용했다. `AudioContentForm.tsx` 순수 LOC는 249로 250 이하이며, `npm run typecheck`, `npm run lint`, `npm run build`는 exit 0이었다. `src/features/audio-contents` LSP diagnostics는 16 TSX files / 오류 0건이었다. 개발 중 E2E는 사용자 지시에 따라 보류했다.
|
||
- **참고:** `npm run test:run -- src/features/audio-contents`와 단독 `npm run test:run -- src/features/audio-contents/tests/audio-list.test.tsx`는 `Audio list restores and serializes search_word only...`가 `오디오 콘텐츠` heading을 찾지 못하고 loading 상태에서 실패했다. 실패 경로는 Audio 목록 조회 test이며 이번 `P4-R7`의 cover 선택/form 변경 경로와 분리된다.
|
||
|
||
### Task R4.8 — Audio upload 종료 처리의 단일 정산 보장
|
||
|
||
**Goal 실행 `P4-R8`:** Audio XHR upload의 성공·HTTP 실패·network 실패·취소가 경합해도 요청별 종료 처리를 한 번만 수행하고, 같은 보호 요청 burst의 401 만료 처리를 한 번만 유지한다.
|
||
|
||
- **연결 리뷰:** [Phase 4 현재 리뷰](./reviews/phase4-audio-content.md) — `REV-P4-011`
|
||
- **시작 조건:** `P1-R12` 완료, PRD `AUDIO-017`, `AUTH-005`, §13의 동시 401 단일 처리, 기존 완료 `P4-R2`의 upload 401 burst contract.
|
||
- **완료 증거:** signal이 연결된 upload의 network error 뒤 abort가 발화한 다음 첫 session과 새 로그인 session의 protected upload가 각각 401을 반환하는 실패 test, 요청별 settle/cleanup 최소 수정, Audio focused·Phase 4 Gate와 검증 기록.
|
||
- **Files:** Modify: `src/features/audio-contents/api/upload-audio-content.ts`, `src/features/audio-contents/tests/audio-upload.test.ts`; Create: `src/features/audio-contents/tests/audio-upload-auth-lifecycle.test.ts`.
|
||
- **Interfaces:** `uploadAudioContent`는 각 XHR의 resolve/reject, 보호 upload count 감소와 `AbortSignal` listener 해제를 terminal event 종류와 무관하게 정확히 한 번 수행한다. 같은 protected upload burst의 `clearSession`/`onAuthExpired`는 최대 한 번 호출한다.
|
||
- **범위 밖:** fetch 기반 upload 전환, resumable upload, 새 상태 관리 library, 공통 API client 구조 변경, backend idempotency 변경.
|
||
- **Phase 4 Gate 연결:** focused RED/GREEN 뒤 Audio 전체 unit, mock Chromium Audio 시나리오, `typecheck`·`lint`·build·diff 검증이 통과하기 전에는 완료 처리하지 않는다.
|
||
- **검증 기록:** RED의 실제 callback 횟수, GREEN/REFACTOR test 수와 Gate 결과를 이 Task 하단과 Phase 4 리뷰에 날짜순으로 누적한다.
|
||
|
||
- [x] **RED:** `audio-upload.test.ts`에 network error로 먼저 reject된 signal-bearing upload를 뒤늦게 abort한 다음 첫 session과 새 로그인 session의 401을 차례로 발생시켜 `clearSession`/`onAuthExpired`가 session별 1회, 총 2회여야 한다는 test를 추가하고 현재 총 1회만 호출되는 실패를 확인했다.
|
||
- [x] **GREEN:** 요청별 settled guard와 공통 terminal cleanup을 최소 적용해 count 감소·listener 해제·Promise settle을 한 번만 수행했다.
|
||
- [x] **REFACTOR:** `onload`, `onerror`, abort가 같은 정산 경계를 사용하도록 중복만 정리하고 기존 success/progress/415/malformed response/cancel retry contract를 유지했다.
|
||
- **실행 명령:** `npm run test:run -- src/features/audio-contents/tests/audio-upload.test.ts src/features/audio-contents/tests/audio-upload-auth-lifecycle.test.ts`; `npm run test:run -- src/features/audio-contents`; `npm run e2e:mock -- tests/e2e/audio-content.spec.ts --project=chromium`; `npm run typecheck`; `npm run lint`; `npm run build:dev`; `npm run build:prod`; `git diff --check`.
|
||
- **기대 결과:** 모든 명령 exit 0, 신규 lifecycle test에서 두 session의 401 callback 각각 1회, 기존 upload progress·cancel·retry·오류 mapping 회귀 0건.
|
||
- **수동 확인:** 느린 network에서 upload 오류와 취소를 인접하게 발생시킨 뒤 재시도하고, 이후 401 응답에서 로그인 이동·만료 안내가 중복되지 않는지 확인한다.
|
||
|
||
**P4-R8 수정 검증 기록 (2026-07-31):**
|
||
|
||
- RED: `npm run test:run -- src/features/audio-contents/tests/audio-upload.test.ts` — 1 failed / 11 passed. 신규 lifecycle test에서 network error 후 late abort가 다음 session 401을 삼켜 `clearSession` 기대 2회 대비 실제 1회였다.
|
||
- GREEN: `uploadAudioContent`에 요청별 `isSettled` guard와 공통 terminal cleanup을 추가해 resolve/reject/count 감소/AbortSignal listener 제거가 terminal 종류와 무관하게 한 번만 실행되도록 했다. 같은 focused 명령은 1 file / 12 tests passed였다.
|
||
- REFACTOR/회귀: 기존 success/progress/415/malformed response/cancel retry contract를 유지했다. reviewer blocker로 fetch-first 순서 회귀를 추가한 뒤 `auth` 주입 시 storage fallback을 쓰지 않도록 보완했다. `npm run test:run -- src/shared/api/__tests__/client-auth.test.ts src/features/audio-contents/tests/audio-upload.test.ts src/features/audio-contents/tests/audio-upload-auth-lifecycle.test.ts` — 3 files / 22 tests passed. `npm run test:run -- src/features/audio-contents` — 9 files / 58 tests passed. `npm run test:run -- src/app src/features/auth src/shared/api src/features/audio-contents/tests/audio-upload.test.ts` — 13 files / 91 tests passed. `npm run test:run` — 79 files / 397 tests passed. `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod`, targeted `git diff --check` — 모두 exit 0. build는 기존 503.04kB chunk warning만 표시했다. LSP diagnostics는 변경 파일·디렉터리 기준 오류 0건이었다.
|
||
- E2E/수동 확인: 사용자 지시에 따라 개발 중 E2E는 반복 실행하지 않고 모든 Task 구현 뒤 필요 시 수행한다.
|
||
|
||
### Task R4.9 — 공통 오디오 플레이어 compact UI
|
||
|
||
**Goal 실행 `P4-R9`:** 공통 `AdminAudioPlayer`를 image·video 영역 없는 compact audio-only UI로 개선해 오디오 콘텐츠 목록·상세와 커뮤니티 목록·Sheet에 동일하게 적용한다.
|
||
|
||
- **Reference packet:** `.omo/evidence/p4-r9-reference/plyr-audio-{375,768,1280}.png`, `.omo/evidence/p4-r9-reference/media-chrome-audio-{375,768,1280}.png`. Plyr의 밝은 52px compact bar를 primary surface로, Media Chrome의 시간·배속·음량 control anatomy를 secondary structure로 사용한다.
|
||
|
||
- **시작 조건:** PRD `AUDIO-021`, `AUDIO-026`, `AUDIO-034`와 기존 `AdminAudioPlayer`·`AudioPlaybackProvider` 단일 재생 계약.
|
||
- **완료 증거:** 아래 RED/GREEN/REFACTOR 체크박스, focused·전체 unit·mock E2E·typecheck·lint·build 통과, Chromium/mobile Chrome 실제 화면 screenshot과 visual QA 판정, QA resource teardown 기록.
|
||
- **범위 밖:** 새 audio player dependency, waveform, playlist, download control, cover/poster/video viewport, API·DTO·signed URL lifecycle 변경.
|
||
|
||
**Files:**
|
||
|
||
- Modify: `src/shared/ui/admin-audio-player.tsx`
|
||
- Modify: `src/shared/ui/__tests__/admin-audio-player.test.tsx`
|
||
- Verify unchanged: `src/features/audio-contents/components/AudioContentList.tsx`
|
||
- Verify unchanged: `src/features/audio-contents/components/AudioContentListItem.tsx`
|
||
- Verify unchanged: `src/features/audio-contents/pages/AudioContentDetailPage.tsx`
|
||
- Verify unchanged: `src/features/community-posts/components/CommunityPostList.tsx`
|
||
- Verify unchanged: `src/features/community-posts/components/CommunityPostListItem.tsx`
|
||
- Verify unchanged: `src/features/community-posts/components/CommunityPostSheet.tsx`
|
||
- Test: `src/features/audio-contents/tests/audio-list.test.tsx`
|
||
- Test: `src/features/audio-contents/tests/audio-player.test.tsx`
|
||
- Test: `src/features/community-posts/tests/community-list.test.tsx`
|
||
- Test: `src/features/community-posts/tests/community-sheet.test.tsx`
|
||
- E2E: `tests/e2e/audio-content.spec.ts`
|
||
- E2E: `tests/e2e/community.spec.ts`
|
||
|
||
**Interfaces:**
|
||
|
||
- Consumes: `AdminAudioPlayerProps { playerId, src, title }`, `useAudioPlayback`, native `HTMLAudioElement` events.
|
||
- Produces: 기존 props와 재생 동작을 바꾸지 않는 compact audio-only control; `재생 진행`과 `재생 설정` accessible group.
|
||
|
||
**시나리오 계약:**
|
||
|
||
| 시나리오 | 이진 통과 조건 | 실제 surface | 자동 test |
|
||
|---|---|---|---|
|
||
| Happy path | 오디오 목록에서 재생 버튼을 누르면 버튼 이름이 `일시정지`로 바뀌고, 상시 설명 label 없는 재생·진행 slider·현재/전체 시간·배속·음량 control이 표준 viewport에서 Plyr/Media Chrome처럼 한 줄에 표시된다. | mock Chromium `/ai-characters/101/audio-contents`에서 `page.click({ name: "재생" })` 후 reference-fidelity screenshot | `src/shared/ui/__tests__/admin-audio-player.test.tsx`의 `AdminAudioPlayer matches the compact reference control anatomy` |
|
||
| Edge | 320px viewport와 200% zoom에서 player의 `scrollWidth <= clientWidth`이고 모든 control이 viewport 안에 있다. | mock mobile Chrome 목록·상세 screenshot과 overflow probe | `tests/e2e/audio-content.spec.ts`의 기존 320px·200% zoom player 시나리오 |
|
||
| Adjacent regression | 커뮤니티 목록과 Sheet가 같은 player 구조를 사용하고 두 player 중 새 항목 재생 시 기존 항목이 pause되며 오류·수동 재시도 계약이 유지된다. | mock Chromium `/ai-characters/101/community-posts` 목록·Sheet 재생과 screenshot | `src/shared/ui/__tests__/admin-audio-player.test.tsx`, `src/features/community-posts/tests/community-sheet.test.tsx`, `tests/e2e/community.spec.ts` |
|
||
|
||
**TDD 절차:**
|
||
|
||
- [x] **RED: 실패 테스트 작성/실패 확인** — `src/shared/ui/__tests__/admin-audio-player.test.tsx`에 다음 reference-anatomy test를 작성한다.
|
||
|
||
production code를 수정하기 전에 mock Chromium의 오디오 콘텐츠 목록·상세와 커뮤니티 목록·Sheet 현재 화면을 reference screenshot으로 저장한다.
|
||
|
||
```tsx
|
||
test("AdminAudioPlayer matches the compact reference control anatomy", () => {
|
||
render(<AdminAudioPlayer playerId="one" src="https://cdn.example.com/audio.m4a" title="샘플 오디오" />);
|
||
|
||
const player = screen.getByRole("group", { name: "샘플 오디오 오디오 플레이어" });
|
||
const controls = within(player).getByRole("group", { name: "재생 제어" });
|
||
expect(within(controls).getByRole("button", { name: "재생" })).toBeInTheDocument();
|
||
expect(within(controls).getByRole("slider", { name: "재생 위치" })).toBeInTheDocument();
|
||
expect(within(controls).getByRole("combobox", { name: "재생 속도" })).toBeInTheDocument();
|
||
expect(within(controls).getByRole("slider", { name: "볼륨" })).toBeInTheDocument();
|
||
expect(within(player).queryByText("볼륨")).not.toBeInTheDocument();
|
||
expect(within(player).queryByText("재생 속도")).not.toBeInTheDocument();
|
||
expect(player.querySelector("img")).not.toBeInTheDocument();
|
||
expect(player.querySelector("video")).not.toBeInTheDocument();
|
||
});
|
||
```
|
||
|
||
`npm run test:run -- src/shared/ui/__tests__/admin-audio-player.test.tsx -t "compact reference control anatomy"` 실행 시 `재생 제어` group을 찾지 못해 실패해야 한다.
|
||
|
||
- [x] **GREEN: 최소 구현/통과 확인** — `src/shared/ui/admin-audio-player.tsx`의 기존 state·handler·native `<audio>`는 유지하고 반환 JSX만 다음 구조로 교체한다.
|
||
|
||
```tsx
|
||
<section aria-label={`${title} 오디오 플레이어`} className="flex min-w-0 flex-col gap-2 rounded-lg border border-border bg-card px-1 py-1 sm:px-3" onKeyDown={handleKeyDown} role="group" tabIndex={0}>
|
||
<audio controlsList="nodownload" onDurationChange={(event) => setDuration(event.currentTarget.duration)} onEnded={() => { setIsPlaying(false); clearPlayer(); }} onError={() => { setHasError(true); setIsPlaying(false); }} onPause={() => setIsPlaying(false)} onPlay={() => setIsPlaying(true)} onTimeUpdate={(event) => setCurrentTime(event.currentTarget.currentTime)} preload="metadata" ref={audioRef} src={src} />
|
||
<div aria-label="재생 제어" className="flex min-w-0 flex-wrap items-center gap-1 sm:gap-2" role="group">
|
||
<button aria-label={isPlaying ? "일시정지" : "재생"} className="grid size-11 shrink-0 place-items-center rounded-full border border-input bg-primary text-primary-foreground hover:bg-[var(--button-bg-hover)] active:bg-[var(--button-bg-active)]" onClick={togglePlay} type="button">
|
||
<svg aria-hidden="true" className="size-5" fill="currentColor" viewBox="0 0 24 24">
|
||
{isPlaying ? <path d="M7 5h4v14H7zm6 0h4v14h-4z" /> : <path d="m8 5 11 7-11 7z" />}
|
||
</svg>
|
||
</button>
|
||
<input aria-label="재생 위치" className="h-11 min-w-11 flex-1 accent-primary" max={duration || 0} min="0" onChange={(event) => changeCurrentTime(Number(event.currentTarget.value))} step="1" type="range" value={currentTime} />
|
||
<span className="shrink-0 text-xs tabular-nums text-muted-foreground">{formatTime(currentTime)}/{formatTime(duration)}</span>
|
||
<select aria-label="재생 속도" className="min-h-11 w-11 shrink-0 rounded-md border border-input bg-card px-1 text-center text-base font-semibold sm:text-sm" defaultValue="1" onChange={(event) => changePlaybackRate(Number(event.currentTarget.value))}>
|
||
<option value="0.75">0.75×</option><option value="1">1×</option><option value="1.25">1.25×</option><option value="1.5">1.5×</option><option value="2">2×</option>
|
||
</select>
|
||
<svg aria-hidden="true" className="size-4 shrink-0 text-muted-foreground" fill="currentColor" viewBox="0 0 24 24"><path d="M4 9v6h4l5 4V5L8 9zm11.5 3a3.5 3.5 0 0 0-1.5-2.87v5.74A3.5 3.5 0 0 0 15.5 12Zm-1.5-7.46v2.06a6 6 0 0 1 0 10.8v2.06a8 8 0 0 0 0-14.92Z" /></svg>
|
||
<input aria-label="볼륨" className="h-11 min-w-11 basis-11 shrink grow-0 accent-primary" defaultValue="1" max="1" min="0" onChange={(event) => changeVolume(Number(event.currentTarget.value))} step="0.05" type="range" />
|
||
</div>
|
||
{hasError ? <div className="flex min-w-0 flex-col gap-2 rounded-md border border-destructive bg-card p-3 text-sm text-destructive" role="alert"><p className="break-words">오디오를 재생할 수 없습니다. 페이지 새로고침 후 다시 시도하세요.</p><button className="rounded-md border border-input bg-card px-3 py-2 font-semibold hover:bg-accent" onClick={retry} type="button">오디오 다시 시도</button></div> : null}
|
||
</section>
|
||
```
|
||
|
||
같은 focused 명령이 `exit 0`이고 신규 test가 통과해야 한다.
|
||
|
||
- [x] **REFACTOR: 정리/회귀 확인** — event handler나 player context를 추출하지 않고 JSX의 반복 class와 접근성 이름만 점검한다. `src/shared/ui/admin-audio-player.tsx`를 250 pure LOC 이하로 유지하고 기존 signed URL 비기록, 단일 재생, keyboard, seek, volume, speed, 오류·수동 재시도 test를 모두 통과시킨다.
|
||
|
||
**검증 기준:**
|
||
|
||
- **실행 명령:**
|
||
|
||
```bash
|
||
npm run test:run -- src/shared/ui/__tests__/admin-audio-player.test.tsx src/features/audio-contents/tests/audio-list.test.tsx src/features/audio-contents/tests/audio-player.test.tsx src/features/community-posts/tests/community-list.test.tsx src/features/community-posts/tests/community-sheet.test.tsx
|
||
npm run test:run
|
||
npm run e2e:mock -- tests/e2e/audio-content.spec.ts tests/e2e/community.spec.ts
|
||
npm run typecheck
|
||
npm run lint
|
||
npm run build:dev
|
||
npm run build:prod
|
||
git diff --check -- src/shared/ui/admin-audio-player.tsx src/shared/ui/__tests__/admin-audio-player.test.tsx docs/20260725_AI캐릭터관리자웹/prd.md docs/20260725_AI캐릭터관리자웹/plan-task.md
|
||
```
|
||
|
||
- **기대 결과:** 모든 명령 `exit 0`; 신규 compact group test와 기존 전체 unit·Chromium/mobile Chrome mock E2E 통과; 변경 TSX LSP diagnostics 오류 0건; 다운로드 control·image·video·waveform 0개.
|
||
- **수동 확인:** `npm run dev:mock`을 실행하고 Playwright로 오디오 콘텐츠 목록·상세, 커뮤니티 목록·Sheet를 desktop 1280px와 mobile 320px에서 연다. 재생, seek, 볼륨, 배속, 단일 재생, 오류·재시도를 조작하고 각 surface screenshot을 저장한다. 200% zoom에서 clipping·overflow를 검사하고 `visual-qa`의 pixel diff 및 디자인 시스템/기능 무결성·시각 충실도/CJK 판정을 모두 통과시킨다.
|
||
- **QA teardown:** Playwright browser/context를 닫고 mock dev server PID를 종료한 뒤 8889 port listener가 없음을 확인해 Progress에 기록한다.
|
||
- [x] RED·GREEN·REFACTOR, browser artifact 경로, visual QA 판정, teardown 결과를 이 Task 하단에 누적한다.
|
||
|
||
**전체 중단 조건:** `P4-R9`의 세 시나리오가 RED→GREEN 및 실제 Chromium/mobile Chrome surface에서 통과하고, 전체 unit·mock E2E·typecheck·lint·build가 green이며 LSP 오류와 QA resource가 0개일 때 즉시 종료한다.
|
||
|
||
**P4-R9 구현·검증 기록 — 2026-08-03:**
|
||
|
||
- **RED:** 첫 reference-anatomy test는 `재생 제어` group 부재로 `1 failed / 5 skipped`였다. 후속 폭·접근성 RED는 volume의 `hidden` class, seek/rate/volume 최소 폭·font·basis, 표준 viewport 단일 행 폭 예산 assertion이 각각 기존 구현에서 실패했다.
|
||
- **GREEN:** native `<audio>`와 기존 상태·handler는 유지하고 Plyr의 밝은 compact surface와 Media Chrome의 명시적 time/rate/volume anatomy를 semantic token 기반 live DOM으로 구현했다. 최종 focused player test는 `1 passed / 5 skipped`, player 전체는 `6 passed`였다.
|
||
- **REFACTOR:** 새 dependency·consumer 변경·상태 abstraction 없이 class와 accessible name만 정리했다. `AdminAudioPlayer`는 250 pure LOC 이하이며 변경 TSX 2개 LSP diagnostics는 오류 0건이다.
|
||
- **자동 검증:** focused `5 files / 19 tests`, 전체 `81 files / 426 tests`, `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod`, `git diff --check`가 모두 exit 0이었다. 두 build에는 기존 500kB chunk warning만 남았다.
|
||
- **브라우저 V3:** `npm run e2e:mock -- tests/e2e/audio-content.spec.ts tests/e2e/community.spec.ts`는 Chromium/mobile Chrome에서 `25 passed / 1 intentional skip / 0 failed`였다. `.omo/evidence/p4-r9-v3/`의 player 16개·context 16개·상태 3개 fresh PNG에서 375/768/1280의 12개 표준 조합은 모두 54–55px 단일 행, 320px/200% 4개 조합은 모든 control을 유지한 채 overflow 없이 wrap했다.
|
||
- **기능·시각 판정:** play/pause, seek, rate 1.5, volume 0.5, 단일 active track, 일반 오류·수동 retry와 API refetch delta 0을 확인했다. 독립 design-system/functional 및 visual/CJK reviewer가 같은 V3 세트를 각각 PASS로 판정했고 blocker는 없다.
|
||
- **Teardown:** Playwright browser는 종료됐다. QA-owned server는 없었고, QA 이전부터 실행 중인 8889의 PID 4837/4875는 사용자 프로세스로 보존했다.
|
||
|
||
### Task R4.10 — Audio form 입력 정책과 조건부 설정 UX 정렬
|
||
|
||
**Goal 실행 `P4-R10`:** 2026-08-04 확정 Audio form 정책을 구현하고 create/update payload 기본값과 접근 가능한 native control UX를 회귀로 고정한다.
|
||
|
||
- **시작 조건:** `P4-R9` 완료, PRD `AUDIO-008~010`, `AUDIO-019`, `AUDIO-033`과 2026-08-04 Decision Log 확인.
|
||
- **완료 증거:** tags, 숫자 전용 가격, 가격 조건부 설정·초기화, 숨긴 OpenAPI field의 고정 payload, 예약 datetime과 preview duration offset control, 선택 카드 문법이 RED/GREEN/REFACTOR test와 Chromium/mobile Chrome mock browser QA로 검증된다. 실행 결과와 QA teardown을 `§7 검증 기록`에 누적한다.
|
||
- **범위 밖:** OpenAPI/backend schema 변경, `limited` 의미 추정, `isOnlyRental` 동기화 규칙 발명, custom date/time picker, 새 form library, 기존 dirty `AudioContentForm.tsx`와 `audio-form.test.tsx`의 사용자 reorder 변경.
|
||
|
||
**Files:**
|
||
|
||
- Modify: `DESIGN.md`
|
||
- Modify: `src/features/audio-contents/components/AudioContentForm.tsx`
|
||
- Create: `src/features/audio-contents/components/AudioTagInput.tsx`
|
||
- Create: `src/features/audio-contents/components/AudioContentCreateOptions.tsx`
|
||
- Modify: `src/features/audio-contents/components/ReleaseScheduleField.tsx`
|
||
- Modify: `src/features/audio-contents/components/audio-content-form-helpers.ts`
|
||
- Modify: `src/features/audio-contents/tests/audio-form.test.tsx`
|
||
- Modify: `src/features/audio-contents/tests/audio-form-update.test.tsx`
|
||
- Modify: `src/features/audio-contents/tests/audio-form-create-red.test.tsx` by consolidating its create assertions into the focused form suite, then remove the redundant file only after those assertions pass there
|
||
- Modify: `tests/e2e/audio-content.spec.ts`
|
||
|
||
**Interfaces:**
|
||
|
||
- `AudioTagInput({ value, onChange })` consumes and emits a comma-separated `string`; it renders chips, commits nonempty trimmed entries on Enter or comma, and exposes a labeled remove button for each chip.
|
||
- `AudioContentCreateOptions({ price, value, onChange })` owns only create-only option controls. For `price > 0`, it renders native selection-card controls for `purchaseOption`, `isGeneratePreview`, `isPointAvailable`, `isAdult`, `isCommentAvailable`, and `isFullDetailVisible`. For price `0`, it hides purchase/preview/point controls and emits `purchaseOption: "BOTH"`, `isGeneratePreview: false`, `isPointAvailable: false`, `previewStartTime: null`, `previewEndTime: null`.
|
||
- Price form state accepts digits only and serializes an integer in `0..99999`. `0` means free. Create serialization always sends `limited: null`, `languageCode: null`, and `isOnlyRental: false`, without rendering controls for those fields.
|
||
- `ReleaseScheduleField({ releaseMode, value, onChange })` renders native radio selection cards and only renders `<input type="datetime-local">` in scheduled mode. It does not call `showPicker()`.
|
||
- Preview start/end controls render only when `isGeneratePreview` is true. They use text inputs for audio duration offsets in full `HH:MM:SS`; create serialization sends the entered `HH:mm:ss` value without shorthand normalization.
|
||
- `DESIGN.md` defines one selection-card visual grammar for native radio and checkbox controls, preserving visible labels, keyboard operation, focus indication, and 44px minimum targets.
|
||
|
||
**TDD 예외 사유:** 이 Task의 PRD·plan 갱신은 문서 산출물만 바꾸므로 application behavior를 대상으로 한 새 실패 test를 만들지 않는다. 문서 계약 test와 Markdown whitespace 검사가 해당 변경의 직접 검증이다. 아래 RED/GREEN/REFACTOR는 애플리케이션 구현 시작 후에만 실행한다.
|
||
|
||
**대체 검증 방법:** `npm run test:run -- src/shared/mocks/__tests__/mock-preview-docs.test.ts`와 scoped `git diff --check`로 문서 계약과 형식을 확인하고, 실제 application RED/GREEN/REFACTOR 결과는 실행 뒤 Progress에만 기록한다.
|
||
|
||
**TDD 절차:**
|
||
|
||
- [x] **RED: 실패 테스트 작성/실패 확인** — `audio-form.test.tsx`에 Enter/comma chip 추가, remove button, comma-separated payload, numeric price와 `0/99999` 허용·`100000` 차단을 추가한다. `audio-form-update.test.tsx`에 update payload가 create-only control을 다시 보내지 않는 경계를 추가한다. `audio-form-create-red.test.tsx`의 create assertion은 focused 회귀로 유지한다. `tests/e2e/audio-content.spec.ts`에 예약/즉시 전환과 가격 0 전환 시 옵션 숨김·기본값 초기화, ArrowDown/ArrowUp min 0 stepper를 추가한다. focused Vitest가 초기 가격 `""`/text input과 기존 payload assertion으로 실패하는지 확인했다.
|
||
- [x] **GREEN: 최소 구현/통과 확인** — `AudioTagInput`, `AudioContentCreateOptions`, helper와 existing form/schedule field에 필요한 최소 조합만 추가했다. 가격 기본값 0, native `type="number" min="0" step="1"`, price=0 reset, fixed `limited/languageCode/isOnlyRental` serialization, 예약 datetime과 preview duration offset input 조건부 렌더링, radio/checkbox selection card를 구현하고 focused command를 통과시켰다.
|
||
- [x] **REFACTOR: 정리/회귀 확인** — create-only option state를 `AudioContentCreateOptions`에 국한하고 form helper의 serialization 중복만 정리했다. `DESIGN.md`와 구현의 선택 카드 규칙을 대조한 뒤 focused form tests, Audio feature regression, typecheck, lint, build, mock E2E를 실행했다.
|
||
|
||
**검증 기준:**
|
||
|
||
- **실행 명령:** `npm run test:run -- src/features/audio-contents/tests/audio-form.test.tsx src/features/audio-contents/tests/audio-form-update.test.tsx src/features/audio-contents/tests/audio-form-create-red.test.tsx`; `npm run test:run -- src/features/audio-contents`; `npm run typecheck`; `npm run lint`; `npm run build`; `npm run e2e:mock -- tests/e2e/audio-content.spec.ts`; `npm run test:run -- src/shared/mocks/__tests__/mock-preview-docs.test.ts`; `git diff --check -- DESIGN.md docs/20260725_AI캐릭터관리자웹/prd.md docs/20260725_AI캐릭터관리자웹/plan-task.md src/features/audio-contents tests/e2e/audio-content.spec.ts`.
|
||
- **기대 결과:** 실행한 모든 명령이 exit 0이다. tags payload는 comma-separated string이고, price는 native number input으로 `0..99999` integer를 보낸다. price=0에서 purchase/preview/point control과 preview time은 없고 지정 기본값만 전송된다. `limited=null`, `languageCode=null`, `isOnlyRental=false`가 유지되며 세 field의 UI와 임의 동기화는 없다. 예약은 native `datetime-local`, preview는 full `HH:MM:SS` text duration offset input으로 조건부 렌더링한다. preview request는 입력한 `HH:mm:ss` 값을 그대로 보내며 `showPicker()` 호출은 없다.
|
||
- **수동 확인:** `npm run dev:mock`으로 desktop Chromium과 mobile Chrome에서 Audio 생성 form을 연다. keyboard로 tags를 Enter와 comma로 추가하고 remove button으로 삭제한다. 가격을 유료값에서 0으로 바꿔 숨김과 reset을 확인하고 다시 유료값으로 바꾼다. 예약 공개와 preview 생성 on/off를 전환해 native datetime과 text duration offset input, selection-card focus, 200% zoom, axe critical·serious 0건을 확인한다. mock browser와 dev server를 종료하고 QA가 시작한 listener만 정리한다.
|
||
- [x] RED/GREEN/REFACTOR의 실제 명령·test 수·browser QA·teardown 결과를 새 Progress 기록에 누적한다.
|
||
|
||
**P4-R10 실행 전 Progress — 2026-08-04:**
|
||
|
||
- 문서 정책 결정과 실행 계약만 확정했다. 애플리케이션 source, `DESIGN.md`, form test, E2E는 아직 수정하거나 실행하지 않았으므로 RED/GREEN/REFACTOR 결과와 browser QA 통과를 기록하지 않는다.
|
||
- 문서 대체 검증은 이 문서 갱신 직후 `mock-preview-docs.test.ts`와 scoped `git diff --check`로 수행하며, 그 결과는 현재 문서 변경의 검증일 뿐 `P4-R10` 구현 완료 증거가 아니다.
|
||
|
||
**P4-R10 완료 Progress — 2026-08-04:**
|
||
|
||
- **RED/GREEN:** preview duration 회귀는 `type="text"` 기대에 기존 `type="time"`이 반환되어 RED를 확인했고, 최소 구현 후 focused `1/1`, create form `10/10`, Audio `66/66`, 전체 Vitest `81 files / 434 tests`, 문서 계약 `9/9`가 통과했다.
|
||
- **가격 스테퍼 보완:** 가격 기본값 0과 native `type="number" min="0" step="1"` 회귀는 focused test에서 기존 빈 문자열/text input으로 RED를 확인했다. 최소 구현 후 focused form `18/18`, Audio `65/65`, 전체 Vitest `81 files / 433 tests`, `typecheck`, `lint`, `build:prod`, mock E2E `11 passed / 1 intentional skip`가 통과했다. Chromium/mobile Chrome에서 ArrowDown at 0은 0 유지, ArrowUp은 1 증가를 확인했다.
|
||
- **정적 검증:** `npm run typecheck`, `npm run lint`, `npm run build:prod`, TypeScript no-excuse 검사, `git diff --check`가 exit 0이었다. production build에는 기존 500kB chunk warning만 남았다.
|
||
- **브라우저 검증:** `npm run e2e:mock -- tests/e2e/audio-content.spec.ts`는 Chromium/mobile Chrome에서 `11 passed / 1 intentional skip / 0 failed`였다. 실제 1280/768 DOM에서 preview start/end는 `type=text`, `step=null`, 값 `00:00:30`/`01:00:05`, `input[type=time]=0`이었고 예약 입력만 `datetime-local`이었다. 375px은 기존 정책대로 mutation form을 숨겼으며 세 viewport 모두 horizontal overflow가 없었다.
|
||
- **시각·리뷰 판정:** `.playwright-mcp/audio-preview-duration-{1280,768,375-blocked}-20260804.png` fresh 캡처를 대상으로 design-system/functional 및 visual/CJK Oracle이 모두 blocker 없이 PASS했고, 최종 code reviewer도 APPROVE했다.
|
||
- **Teardown:** Playwright browser를 닫고 QA가 시작한 Vite PID `69030`과 parent PID `69001`을 종료했다. 이후 `127.0.0.1:8889` listener가 없음을 확인했다.
|
||
|
||
---
|
||
|
||
## Phase 5. Series vertical slice
|
||
|
||
**목표:** 선택 Character의 Series를 조회하고, 계약 제공 후 생성·수정·비활성화하며 Audio 연결·해제와 서버가 반환한 Series 전체 순서를 관리한다.
|
||
|
||
**Phase Goal `P5`:** Task 5.1 → 5.5와 Phase 5 Gate로 Series 조회, 계약 제공 후 CRUD, 연결·전체 순서 slice를 완성한다.
|
||
|
||
- **시작 조건:** `P4-T2`의 Audio 조회 API 완료.
|
||
- **완료 조건:** `P5-T1`~`P5-T5`, `P5-GATE` 완료. genre lookup 외부 의존은 제공 또는 명시적 후속/제외 상태로 종결하고, Series 수정은 상세 응답의 수정용 원본값으로 초기화한다.
|
||
- **실행 순서:** 계약 확인 → 목록/상세 → CRUD → 연결/순서 → 반응형·접근성.
|
||
|
||
**요구사항:** `SERIES-001~018`, `FILE-001~002`, `FILE-005`, `FILE-007~009`, `FILE-012`, `FILE-015`, PRD `9`의 Series 범위.
|
||
|
||
**외부 의존:** `EXT-002` genre lookup(`SERIES-007`, `SERIES-011`)은 legacy 후보 `GET /creator-admin/audio-content/series/genre`로 구현 계획에 포함한다. Series 수정 form은 별도 edit DTO 없이 상세 응답이 list item과 같은 `genreId`, enum `publishedDaysOfWeek`, enum `state`를 제공하면 초기화할 수 있다. `EXT-007` active-only 반환 보장은 외부 의존이다. OpenAPI 밖의 도메인별 오류 message key는 분기하지 않는다. 연결 후보·page 기반 전체 로딩 endpoint는 OpenAPI에 제공됐다.
|
||
|
||
**주요 Files:**
|
||
|
||
- Create: `src/features/series/api/series-api.ts`
|
||
- Create: `src/features/series/model/types.ts`
|
||
- Create: `src/features/series/schemas/series-schema.ts`
|
||
- Create: `src/features/series/validation/series-image-policy.ts`
|
||
- Create: `src/features/series/pages/{SeriesListPage,SeriesDetailPage,SeriesFormPage,SeriesOrderPage}.tsx`
|
||
- Create: `src/features/series/components/{SeriesList,SeriesListItem,SeriesSummary,SeriesForm,PublishedDaysField,GenreCombobox,SeriesContents,SeriesOrderList}.tsx`
|
||
- Create: `src/features/series/tests/series-contract.test.ts`
|
||
- Create: `src/features/series/tests/{series-form,series-contents,series-order}.test.tsx`
|
||
- Create: `tests/e2e/series.spec.ts`
|
||
|
||
#### 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 lookup 차단, 상세 응답의 수정용 원본값, 제공된 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` | 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, legacy genre lookup `GET /creator-admin/audio-content/series/genre`, 상세 응답의 `genreId`·enum 요일·enum state. Produces: `createSeries`, `updateSeries`, `deactivateSeries`, `listSeriesGenres` | **TDD 적용:** `npm run test:run -- src/features/series/tests/series-contract.test.ts src/features/series/tests/series-form.test.tsx`; 기대 `exit 0`. 수동 확인: genre 선택, 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~018`, `MOCK-001~009`와 OpenAPI Series 9개 operation/schema 확인.
|
||
- **완료 증거:** 체크박스 전체, 제공 계약 또는 제외 결정의 세 문서 일치, 상태/action inventory.
|
||
- **범위 밖:** active-only·도메인별 오류 동작의 추정 구현. genre lookup은 legacy API를 사용하고, 별도 edit DTO는 만들지 않는다.
|
||
|
||
- [x] genre lookup endpoint·DTO·search/page 계약을 기록한다.
|
||
- [x] 연결 후보 `GET .../contents/search?search_word=...`, 연결 `{contentIdList}`, 해제 body 없는 DELETE, 전체 순서 `{ids}` 계약을 기록한다.
|
||
- [x] 목록 `data.totalCount/items`와 `page/size`로 전체 Series page를 누락 없이 읽는 방식을 기록한다. 도메인별 누락 ID·동시 변경 오류 key는 제공되지 않았음을 외부 의존으로 남긴다.
|
||
- [x] legacy genre lookup API와 active-only 보장의 영향 범위를 문서에서 먼저 확인한다. active-only는 추측 구현하지 않는다.
|
||
- [x] 목록·상세·form·연결·순서 화면의 상태/action inventory를 작성하고 Page는 route/query/policy 조합, feature component는 Series 규칙, shared component는 Phase 1 contract를 사용하도록 component map을 확정한다.
|
||
- [x] 제공 계약 범위만 Series 목록·상세·연결·순서 browser fixture로 만들고 genre lookup·active-only·충돌 계약 미제공 부분은 mock에서도 추정하지 않는다. Series 수정 초기화는 상세 응답의 수정용 원본값으로 처리한다.
|
||
|
||
**P5-T1 계약 확인 결과:**
|
||
|
||
- OpenAPI Series operation은 9개다. `listAiCharacterSeries`, `createAiCharacterSeries`, `reorderAiCharacterSeries`, `getAiCharacterSeries`, `updateAiCharacterSeries`, `listAiCharacterSeriesContents`, `addAiCharacterSeriesContents`, `searchAiCharacterContentsNotInSeries`, `removeAiCharacterSeriesContent`를 기준으로 한다.
|
||
- Series 생성·수정 contract는 제공됐고, 장르 lookup은 OpenAPI에는 없지만 legacy 후보 `GET /creator-admin/audio-content/series/genre`를 사용한다. P5-T3에서 response DTO를 contract test로 고정하고 장르 선택 network integration과 생성 완료 판정을 진행한다.
|
||
- 별도 `EXT-003` edit DTO 또는 표시 문자열 mapping 계약은 필요 없다. 상세 응답이 list item과 같은 `genreId`, enum `publishedDaysOfWeek`, enum `state`를 함께 제공하면 직접 링크 수정 form을 초기화할 수 있다. 상세의 표시용 문자열은 update enum/ID로 역변환하지 않는다.
|
||
- 연결 후보는 `GET /api/v2/admin/ai-characters/{characterId}/series/{seriesId}/contents/search?search_word=...`이고 body가 없다. 응답은 `data[]`의 `contentId`, `title`, `coverImage`만 사용한다.
|
||
- 연결은 `POST /api/v2/admin/ai-characters/{characterId}/series/{seriesId}/contents`에 JSON `{ "contentIdList": [...] }`를 보낸다. 해제는 `DELETE /api/v2/admin/ai-characters/{characterId}/series/{seriesId}/contents/{contentId}`이고 request body가 없다.
|
||
- 순서 변경은 `PUT /api/v2/admin/ai-characters/{characterId}/series/orders`에 JSON `{ "ids": [...] }`를 보낸다. `{contentIds}`, `{seriesIds}`는 사용하지 않는다.
|
||
- Series 목록과 연결 콘텐츠 목록은 모두 `page`, `size` query와 `data.totalCount`, `data.items[]`를 사용한다. 순서 변경 화면은 첫 page의 `totalCount`를 기준으로 `ceil(totalCount / size)`까지 같은 `size`로 순차 또는 병렬 조회해 모든 page를 모은 뒤, 현재 수집한 `items.length`가 `totalCount`와 일치할 때만 전체 ID 순서를 저장한다. 일부 page 실패나 개수 불일치는 저장을 막고 재조회 안내로 처리한다.
|
||
- `EXT-007` active-only 반환 보장, 순서 저장의 missing ID와 concurrent conflict status/message key는 OpenAPI에 없다. mock과 UI는 공통 `ApiErrorResponse`의 `success=false`, `message`, `data=null`, `errorProperty`와 공통 status만 사용하고 도메인 분기를 추정하지 않는다.
|
||
- 화면 상태와 action inventory: 목록은 loading, empty, error, background refresh, read-only, page 변경, 상세 이동을 가진다. 상세는 loading, error, read-only, 연결 콘텐츠 조회, desktop/tablet에서 생성·수정·비활성화·연결·해제·순서 이동 entry를 가진다. form은 계약 대기 상태, field validation, 저장 중, 성공, 오류, 이탈 확인, desktop/tablet 전용 action을 가진다. 연결 화면은 후보 검색, 후보 없음, 이미 연결됨, 연결 중, 해제 확인, 오류와 재시도를 가진다. 순서 화면은 전체 page 로딩, 부분 실패, drag reorder, keyboard 위/아래 이동, 저장 중, 충돌 또는 누락 ID 오류, 재조회 action을 가진다.
|
||
- Component map: `SeriesListPage`와 `SeriesDetailPage`는 route param, query, permission, data loading만 조합한다. `SeriesList`, `SeriesListItem`, `SeriesSummary`는 목록 enum과 상세 표시 문자열을 그대로 표시한다. `SeriesForm`, `PublishedDaysField`, `GenreCombobox`는 legacy genre lookup과 상세 수정용 원본값으로 network payload를 활성화한다. `SeriesContents`는 search/add/remove 규칙을, `SeriesOrderList`는 full-page read와 reorder 규칙을 소유한다. 공통 상태, pagination, confirm dialog, file field, image crop, unsaved guard는 Phase 1 shared contract를 사용한다.
|
||
- Mock scenario는 제공된 목록, 상세, legacy genre lookup, 연결 콘텐츠 목록, 연결 후보 검색, 연결, 해제, 전체 순서 happy path와 공통 오류만 만든다. active-only server 결과, missing ID와 concurrent conflict 도메인 오류는 fixture에 넣지 않는다.
|
||
|
||
### Task 5.2 Series 목록·상세 조회
|
||
|
||
**Goal 실행 `P5-T2`:** OpenAPI가 제공한 Series 목록·상세 조회와 서로 다른 응답 DTO 표시 규칙을 완성한다.
|
||
|
||
- **시작 조건:** `P5-T1` 완료.
|
||
- **완료 증거:** 체크박스 전체, list/detail contract·route·state test와 검증 기록.
|
||
- **범위 밖:** Series 생성·수정·비활성화와 Audio 연결·해제·전체 순서 저장.
|
||
|
||
- [x] list가 `page`, `size`만 보내고 `data.totalCount/items`를 소비하며 활성 query·client 활성 filter 없이 loading·empty·error·retry를 제공하는 test를 작성한다.
|
||
- [x] Series detail route의 직접 진입과 새로고침에서 같은 resource를 복원하는 test를 작성한다.
|
||
- [x] 목록의 enum field와 상세의 표시용 `publishedDaysOfWeek`, `genre`, `keywords`, 한국어 `state` 문자열을 각 응답 DTO 그대로 표시하고 서로 역변환하지 않는 test를 작성한다.
|
||
- [x] 제공된 목록·상세 계약만 mock handler로 만들고 loading·empty·error·retry와 직접 링크 조회를 확인한다.
|
||
|
||
### Task 5.3 Series 생성·수정·비활성화
|
||
|
||
**Goal 실행 `P5-T3`:** 계약 제공 후 Series form, enum·요일·image와 soft delete 규칙을 완성한다.
|
||
|
||
- **시작 조건:** `P5-T2` 완료. genre lookup은 legacy `GET /creator-admin/audio-content/series/genre`를 사용하고, 직접 링크 수정 form 초기화를 위한 상세 응답의 `genreId`·요일 enum·state enum이 제공됨.
|
||
- **완료 증거:** 체크박스 전체, contract/form/image/deactivate test와 검증 기록.
|
||
- **범위 밖:** Audio 연결·해제와 전체 순서 저장.
|
||
|
||
- [x] enum은 `PROCEEDING | SUSPEND | COMPLETE`, 요일은 `SUN~SAT | RANDOM`만 허용한다.
|
||
- [x] 생성 multipart에 필수 `image`와 `request`가 있고 request의 필수 `title`, `introduction`, `publishedDaysOfWeek`, `keyword`를 보내며 `state`, `isActive`, `keywords`가 없음을 test한다.
|
||
- [x] 수정에서 state 미선택은 key 생략, 선택은 유효 enum만 전송하고 `null`은 보내지 않는다.
|
||
- [x] `RANDOM`은 단독, 실제 요일은 하나 이상이어야 하는 schema·UI test를 작성한다.
|
||
- [x] OpenAPI 2.3.0 `listAiCharacterSeriesGenres`로 장르 option을 조회하고 유효한 `genreId`만 전송하는 Combobox를 test한다. OpenAPI binding 기본값 `0`은 선택값으로 허용하지 않는다.
|
||
- [x] Series image JPEG/PNG·10MB, `210:297`, `height=round(width×297÷210)`, 최대 1000×1414, 1px 오차, no-upscale을 test한다.
|
||
- [x] 상세 응답의 `genreId`·요일 enum·state enum으로 직접 링크 form을 초기화하고 상세의 표시용 문자열을 update enum/ID로 역변환하지 않는 test를 작성한다.
|
||
- [x] create-only `keyword`를 수정 화면에서 읽기 전용으로 표시하고 update payload에 보내지 않는 test를 작성한다.
|
||
- [x] create/update/soft delete의 `data=null`을 처리하고 일반 update와 soft delete의 `isActive` 규칙, 목록 재조회·이동·toast를 test한다.
|
||
- [x] mock store로 Series CRUD 후 list enum과 detail 표시 문자열의 서로 다른 DTO가 일관되게 갱신되는 UI를 확인한다.
|
||
- [x] 초기 Series form을 실제 페이지에서 확인한 뒤 `title`, `introduction`, `keyword`, `writer`, `studio`, `publishedDaysOfWeek`의 최대 길이·개수 권고안을 작성한다. backend 호환 확인 전에는 상한을 구현하지 않는다.
|
||
|
||
### Task 5.4 Audio 연결·해제·전체 순서
|
||
|
||
**Goal 실행 `P5-T4`:** Series Audio 연결·해제와 서버가 반환한 Series 전체 순서를 안전하게 관리한다.
|
||
|
||
- **시작 조건:** `P5-T1`, `P5-T2` 완료.
|
||
- **완료 증거:** 체크박스 전체, link/unlink/reorder contract·interaction test, 충돌 보존, `OQ-009` 결정 기록.
|
||
- **범위 밖:** 계약 없는 active-only·도메인별 충돌 동작의 추정 구현.
|
||
|
||
- [x] 현재 연결 Audio 목록은 `page/size`와 `data.totalCount/items`를 사용하고 제공되지 않은 search query를 보내지 않으며 상세 cache를 동기화한다.
|
||
- [x] 후보는 `GET .../contents/search?search_word=...` 결과만 사용하고 이미 연결된 항목을 중복 선택하지 않는다.
|
||
- [x] 연결 POST는 `{ contentIdList }`, 해제 DELETE는 body 없음임을 contract test로 고정한다.
|
||
- [x] 연결 해제 전 대상 title과 영향을 AlertDialog로 확인한다.
|
||
- [x] 순서 mode는 `totalCount`와 page/size로 Series 전체를 읽고 최종 순서의 모든 ID를 `{ ids }`로 한 번에 보낸다.
|
||
- [x] drag-and-drop과 동일한 결과를 keyboard·위/아래 button으로 만들 수 있는 test를 작성한다.
|
||
- [x] server의 누락 ID·동시 충돌 오류에서 기존 화면 순서를 보존하고 재조회/재시도 안내를 제공한다.
|
||
- [x] 초기 연결·순서 UI를 실제 페이지에서 확인한 뒤 `contentIdList`, `ids`의 최대 개수 권고안을 작성한다. backend 호환 확인 전에는 상한을 구현하지 않는다.
|
||
- [x] mock handler가 연결·해제와 전체 순서 payload를 검증하고 store 결과를 반영하는 E2E를 작성한다.
|
||
|
||
### Task 5.5 Series 반응형·접근성
|
||
|
||
**Goal 실행 `P5-T5`:** Series viewport capability와 활성 범위의 form·연결·정렬 접근성을 검증한다.
|
||
|
||
- **시작 조건:** `P5-T2`와 `P5-T3`~`P5-T4` 중 계약이 제공된 활성 범위 완료.
|
||
- **완료 증거:** 체크박스 전체, 320px·keyboard·200% zoom·axe E2E 기록.
|
||
- **범위 밖:** 모바일 CRUD·연결·순서 mutation.
|
||
|
||
- [x] mobile은 목록·상세·연결 콘텐츠 조회만 제공하고 CRUD·연결·순서 action을 route capability로 차단한다.
|
||
- [x] desktop/tablet에서 전체 관리 흐름을 제공한다.
|
||
- [x] keyboard-only 연결·정렬, 320px 조회, 200% zoom, axe critical·serious 0건을 확인한다.
|
||
- [x] P5-T3 genre lookup과 상세 수정용 원본값 제공 후 keyboard-only 요일·genre/form 접근성을 확인한다.
|
||
|
||
### Phase 5 Gate
|
||
|
||
**Goal 실행 `P5-GATE`:** Series mock UI journey와 실제 server integration 상태를 분리해 판정한다.
|
||
|
||
- **시작 조건:** `P5-T1`~`P5-T5` 완료. 외부 계약 때문에 실행하지 않은 범위는 대기/제외 상태와 재개 조건이 기록됨.
|
||
- **완료 증거:** 아래 명령과 Expected 통과, 외부 의존 상태와 Phase 검증 기록.
|
||
- **범위 밖:** 실패와 무관한 Community/FanTalk 구현.
|
||
|
||
```bash
|
||
npm run test:run -- src/features/series
|
||
npm run e2e:mock -- tests/e2e/series.spec.ts
|
||
npm run e2e -- tests/e2e/series.spec.ts
|
||
npm run typecheck
|
||
npm run lint
|
||
npm run build
|
||
```
|
||
|
||
**Expected:** 현재 제공된 계약으로 목록·상세 조회와 `contentIdList` 연결/해제, `ids` 전체 reorder UI가 통과한다. genre lookup과 상세 수정용 원본값 제공 후에는 필수 image와 `keyword`로 Series 생성 → 수정 → soft delete UI까지 통과하고 잘못된 enum·`keywords/contentIds/seriesIds` payload가 생성되지 않는다. 미제공 계약 범위는 대기로 유지하되 완료된 조회·연결·순서 상태를 되돌리지 않는다.
|
||
|
||
**수동 확인:** 현재 계약으로 320px 조회와 desktop/tablet 연결·keyboard
|
||
reorder를 확인한다. genre lookup과 상세 수정용 원본값 제공 후 생성·수정 form도 확인하고,
|
||
표시용 상세 문자열이 update payload로 역변환되지 않는지 network에서 본다.
|
||
|
||
### Task R5.1 — Series partial update 불변식 복구
|
||
|
||
**Goal 실행 `P5-R1`:** Series update에서 변경 없는 state를 생략하고 일반 update와 soft delete schema를 분리한다.
|
||
|
||
- **연결 리뷰:** [Phase 5 재리뷰](./reviews/review-phase-5-20260729.md) — `REV-P5-001`, `REV-P5-002`
|
||
- **시작 조건:** `SERIES-002`, `SERIES-004`, `SERIES-010`.
|
||
- **Files:** Modify: `src/features/series/components/SeriesForm.tsx`, `src/features/series/schemas/series-schema.ts`, `src/features/series/api/series-api.ts`, `src/features/series/tests/series-form.test.tsx`, `src/features/series/tests/series-contract.test.ts`.
|
||
- **Interfaces:** unchanged state는 key 생략, changed state는 3개 enum만 허용; 일반 update에 `isActive` 없음, deactivate는 `{isActive:false}` 전용.
|
||
- **범위 밖:** Series state enum 추가, backend partial update 의미 변경.
|
||
|
||
- [x] **RED:** unchanged/changed state payload와 `state:null`, `isActive:true/null` 거부 test를 작성한다.
|
||
- [x] **GREEN:** 원본 비교 serializer와 일반/deactivate schema를 분리한다.
|
||
- [x] **REFACTOR:** API helper가 전용 schema만 호출하도록 정리하고 Series 전체 회귀를 실행한다.
|
||
- **실행 명령:** `npm run test:run -- src/features/series`.
|
||
- **기대 결과:** exit 0, unchanged state request 0건, `isActive=true/null` parse 0건, deactivate false만 통과.
|
||
- **수동 확인:** edit에서 state 미변경/변경 저장 network payload를 각각 확인한다.
|
||
|
||
### Task R5.2 — Series image format pair 검증
|
||
|
||
**Goal 실행 `P5-R2`:** Series JPEG/PNG extension↔MIME mismatch를 crop 전에 거부한다.
|
||
|
||
- **연결 리뷰:** [Phase 5 재리뷰](./reviews/review-phase-5-20260729.md) — `REV-P5-003`
|
||
- **시작 조건:** `FILE-002`, `FILE-005`, `SERIES-014`.
|
||
- **Files:** Modify: `series-image-policy.ts`, `series-contract.test.ts`, `series-form.test.tsx`.
|
||
- **Interfaces:** `.jpg/.jpeg ↔ image/jpeg`, `.png ↔ image/png`; size/aspect/no-upscale는 유지.
|
||
- **범위 밖:** 실제 image sniffing, crop 계산(`P1-R7`).
|
||
|
||
- [x] **RED:** `.png+image/jpeg`, `.jpg+image/png` 거부와 crop source 호출 0건 test를 작성한다.
|
||
- [x] **GREEN:** extension별 MIME mapping을 policy에 적용한다.
|
||
- [x] **REFACTOR:** Audio/Character policy와 불필요한 범용 abstraction 없이 message를 정리한다.
|
||
- **실행 명령:** `npm run test:run -- src/features/series src/shared/validation/file-media-policy.test.ts`.
|
||
- **기대 결과:** exit 0, mismatch upload/crop 0건, 기존 10MiB/ratio test 통과.
|
||
- **수동 확인:** 잘못된 pair 선택 시 즉시 field 오류가 보이는지 확인한다.
|
||
|
||
### Task R5.3 — Series 표시용 label 일관성 복구
|
||
|
||
**Goal 실행 `P5-R3`:** Series 목록·상세에서 요일·state·boolean·genre를 운영자용 label로 표시하고 원본 enum/ID는 mutation에만 유지한다.
|
||
|
||
- **연결 리뷰:** [Phase 5 현재 리뷰](./reviews/phase5-series-management.md) — `REV-P5-004`
|
||
- **시작 조건:** `SERIES-007`, `SERIES-015`, PRD §13 중앙 formatter 기준과 장르 목록 계약.
|
||
- **완료 증거:** raw enum/boolean/field-name 노출 실패 test, 표시 계층 formatter 적용, Series focused unit·mock E2E·Phase Gate와 검증 기록.
|
||
- **Files:** Modify: `SeriesListPage.tsx`, `SeriesList.tsx`, `SeriesListItem.tsx`, `SeriesSummary.tsx`, 관련 tests와 `tests/e2e/series.spec.ts`; 필요 시 Series 전용 formatter.
|
||
- **Interfaces:** `SUN..SAT/RANDOM`, `PROCEEDING/SUSPEND/COMPLETE`, boolean과 `genreId`를 한국어 label로 표시하되 API DTO와 create/update payload는 원본 값을 유지한다.
|
||
- **범위 밖:** OpenAPI enum 변경, 장르 ID 역변환, 새 전역 i18n framework.
|
||
|
||
- [x] **RED:** desktop/mobile 목록과 상세에 `SUN`, `PROCEEDING`, `isAdult true`, `isActive true`, `genreId`가 사용자 문구로 노출되는 실패 test를 작성한다.
|
||
- [x] **GREEN:** 장르 응답과 Series 전용 formatter로 표시 label을 만들고 payload test는 원본 enum/ID를 계속 확인한다.
|
||
- [x] **REFACTOR:** `SeriesSummary`의 중복 map을 재사용 가능한 Series 표시 helper로만 정리한다.
|
||
- **실행 명령:** `npm run test:run -- src/features/series`; `npm run e2e:mock -- tests/e2e/series.spec.ts`; `npm run typecheck`; `npm run lint`; `npm run build`.
|
||
- **기대 결과:** 모든 명령 exit 0, Series 사용자 화면의 raw enum·boolean·field-name 노출 0건, mutation payload 변경 0건.
|
||
- **수동 확인:** 320/768/1280px에서 목록·상세 label과 수정 form의 원본 선택값을 확인한다.
|
||
|
||
### Task R5.4 — Series 생성·수정 입력 schema 불변식 복구
|
||
|
||
**Goal 실행 `P5-R4`:** Series 생성의 유효 장르 필수 조건과 생성·수정 요일 배열의 유일성을 schema·mock·contract test에서 강제한다.
|
||
|
||
- **연결 리뷰:** [Phase 5 현재 리뷰](./reviews/phase5-series-management.md) — `REV-P5-005`
|
||
- **시작 조건:** `SERIES-006~007`, OpenAPI `SeriesCreateRequest.publishedDaysOfWeek.uniqueItems=true`, binding 기본값 `genreId=0`은 domain에서 무효라는 설명.
|
||
- **완료 증거:** 누락 `genreId`와 중복 요일의 실패 test, schema·mock 최소 수정, Series focused unit·mock E2E·정적 Gate 통과.
|
||
- **Files:** Modify: `src/features/series/schemas/series-schema.ts`, `src/features/series/tests/series-contract.test.ts`, `src/features/series/tests/series-form.test.tsx`, `src/shared/mocks/series-mock-store.ts`; 필요 시 Series mock handler test.
|
||
- **Interfaces:** create `genreId`는 positive integer 필수, `publishedDaysOfWeek`는 중복 없는 실제 요일 하나 이상 또는 `RANDOM` 단독이며 update의 non-null 요일에도 같은 유일성을 적용한다.
|
||
- **범위 밖:** 장르 목록 자체의 유효성 backend 재검증, 요일 최대 개수 추가, OpenAPI JSON 변경.
|
||
|
||
- [x] **RED:** `seriesCreateRequestSchema`가 `genreId` 누락과 `["MON","MON"]`을 거부하고 update가 중복 요일을 거부해야 한다는 실패 test를 작성했다.
|
||
- [x] **GREEN:** create `genreId`를 필수로 바꾸고 공용 요일 schema에 uniqueness를 적용하며 mock의 임의 첫 장르 fallback을 제거했다.
|
||
- [x] **REFACTOR:** UI의 기존 장르·요일 검증을 schema 불변식과 같은 용어로 정리하되 중복 validation framework는 만들지 않았다.
|
||
- **실행 명령:** `npm run test:run -- src/features/series src/shared/mocks`; `npm run e2e:mock -- tests/e2e/series.spec.ts --project=chromium`; `npm run typecheck`; `npm run lint`; `npm run build`.
|
||
- **기대 결과:** 모든 명령 exit 0, 누락/0 장르와 중복 요일 request 성공 0건, 기존 유효 생성·수정 회귀 0건.
|
||
- **수동 확인:** Series 생성에서 장르 미선택·요일 중복 전송이 불가능하고 정상 생성은 유지되는지 확인한다.
|
||
|
||
**P5-R4 수정 검증 기록 (2026-07-30):**
|
||
|
||
- RED: `npm run test:run -- src/features/series/tests/series-contract.test.ts` — 1 failed / 6 passed. `seriesCreateFormSchema`가 `genreId` 누락 payload를 성공 처리해 기대대로 실패했다.
|
||
- GREEN focused: `npm run test:run -- src/features/series/tests/series-contract.test.ts src/features/series/tests/series-form.test.tsx` — 2 files / 14 tests passed. 생성 `genreId` 필수, 생성·수정 중복 요일 거부, 기존 유효 생성·수정 form 회귀를 확인했다.
|
||
- REFACTOR/회귀: `npm run test:run -- src/features/series src/shared/mocks` — 13 files / 51 tests passed. `npm run typecheck`, `npm run lint`, `npm run build` — 모두 exit 0.
|
||
- LSP diagnostics: `src/features/series/schemas/series-schema.ts`, `src/shared/mocks/series-mock-store.ts`, `src/features/series/tests/series-contract.test.ts` — 오류 0건.
|
||
- E2E/수동 확인: 사용자 지시에 따라 개발 중 E2E는 실행하지 않고 `P10-R5` 이후 최종 E2E에서 확인한다.
|
||
|
||
### Task R5.5 — Series image crop 준비 경계 보강
|
||
|
||
**Goal 실행 `P5-R5`:** Series 생성·수정에서 image crop source 준비와 연속 선택을 추적하고 crop 확정 전 저장을 차단한다.
|
||
|
||
- **연결 리뷰:** [Phase 5 현재 리뷰](./reviews/phase5-series-management.md) — `REV-P5-006`
|
||
- **시작 조건:** `FILE-005`, `FILE-007~009`, `FILE-012`, `FILE-015`와 Series image 생성 필수/수정 optional 계약.
|
||
- **완료 증거:** 준비 중 edit 저장·stale source·준비 실패를 재현하는 실패 test, staged selection 최소 수정, Series focused unit·mock E2E·file Gate 통과.
|
||
- **Files:** Modify: `src/features/series/components/SeriesForm.tsx`, 관련 Series form/file tests와 `tests/e2e/series.spec.ts`.
|
||
- **Interfaces:** `image`는 crop 적용 결과만 보유하고 준비/crop 중 submit을 비활성화·guard한다. 마지막 선택만 유효하며 취소는 기존 서버 image를 유지하고 준비 실패를 inline 표시한다.
|
||
- **범위 밖:** Series crop ratio/output 계산 변경, 새 범용 upload state library, API multipart part 변경.
|
||
|
||
- [x] **RED:** edit에서 source 준비 중 저장, 느린 첫 선택이 빠른 둘째 선택을 덮는 경우, source 준비 reject를 실패 test로 고정한다.
|
||
- [x] **GREEN:** image selection token·준비 상태·오류 처리와 submit guard를 추가한다.
|
||
- [x] **REFACTOR:** create/edit 공통 `SeriesForm` 내부에서 lifecycle을 끝내고 도메인 밖 abstraction을 추가하지 않는다.
|
||
- **실행 명령:** `npm run test:run -- src/features/series`; `npm run e2e:mock -- tests/e2e/series.spec.ts --project=chromium`; `npm run typecheck`; `npm run lint`; `npm run build`.
|
||
- **기대 결과:** 모든 명령 exit 0, 준비/crop 미확정 image의 저장 0건, stale dialog 0건, 정상 210:297 crop result만 multipart에 포함.
|
||
- **수동 확인:** 기존 image가 있는 수정 화면에서 느린 파일 연속 선택·취소·적용·즉시 저장을 반복해 마지막 의도만 반영되는지 확인한다.
|
||
|
||
**Progress — 2026-07-31 P5-R5 완료**
|
||
|
||
- **RED:** `npm run test:run -- src/features/series/tests/series-form.test.tsx`에서 준비 중 저장 버튼 미비활성화, preview reject inline 오류 부재, unhandled rejection을 확인했다.
|
||
- **GREEN:** `SeriesForm`에 image selection token, `isImagePreparing`, 준비/crop 중 submit guard, preview reject inline 오류를 추가했다. `image`는 crop 적용 결과만 commit하고, 수정 화면 crop 취소는 기존 서버 image 유지 계약을 보존한다. focused 재실행 결과 `series-form.test.tsx`와 `series-form-crop.test.tsx` 2 files / 13 tests passed였다.
|
||
- **REFACTOR/회귀:** crop lifecycle test를 `series-form-crop.test.tsx`로 분리해 `SeriesForm.tsx` 220 LOC, `series-form.test.tsx` 230 LOC, `series-form-crop.test.tsx` 85 LOC로 유지했다. `npm run test:run -- src/features/series` 결과 8 files / 37 tests passed였다. 사용자 지시에 따라 mock E2E는 전체 Task 완료 전까지 보류했다. `npm run typecheck`, `npm run lint`, `npm run build`, `git diff --check`는 모두 exit 0 또는 no output이었다. Series directory LSP diagnostics는 17 TSX files / 오류 0건이었다.
|
||
|
||
### Task R5.6 — Series 생성 form 이미지·keyword 입력 UX 정렬
|
||
|
||
**Goal 실행 `P5-R6`:** Series 생성 form의 image 입력을 최상단에 배치하고 Audio tags와 동일한 공용 `TagInput`으로 keyword를 입력하되 기존 comma-separated request 계약을 유지한다.
|
||
|
||
- **시작 조건:** `P4-R10`, `P5-R5`, `SERIES-014`, 사용자 승인 설계.
|
||
- **완료 증거:** image-first field order와 Enter/comma chip·remove 동작의 실패 test, 공용 `TagInput` 재사용, Series comma-separated `keyword` request, Audio tag 회귀, Chromium mock browser와 시각 QA 증거.
|
||
- **Files:** Move/Modify: `src/features/audio-contents/components/AudioTagInput.tsx` → `src/shared/ui/tag-input.tsx`; Modify: `src/features/audio-contents/components/AudioContentForm.tsx`, `src/features/series/components/SeriesForm.tsx`, `src/features/series/tests/series-form.test.tsx`, `src/shared/ui/__tests__/tag-input.test.tsx`, `tests/e2e/series.spec.ts`, `DESIGN.md`, `docs/20260725_AI캐릭터관리자웹/prd.md`, `docs/20260725_AI캐릭터관리자웹/plan-task.md`.
|
||
- **Interfaces:** `TagInput({label,value,onChange,error,errorId})`는 comma-separated string을 consume/emit하고 Enter/comma로 non-empty trimmed chip을 확정하며 remove button으로 한 chip만 삭제한다. Series create는 `keyword`, Audio create/update는 `tags` field명을 그대로 유지한다.
|
||
- **범위 밖:** Series 수정·상세 keyword, duplicate/max-count 정책, API schema·endpoint·multipart part 변경, 새 dependency.
|
||
|
||
- [x] **RED:** Series image-first order와 keyword chip→`"달빛,상담"` request, 공용 input의 blank/remove 동작 실패 test를 작성하고 의도한 실패를 확인했다.
|
||
- [x] **GREEN:** `AudioTagInput`을 공용 `TagInput`으로 이름·위치를 정리하고 Audio/Series에서 재사용하며 Series `FileField`를 form 첫 field로 이동했다.
|
||
- [x] **REFACTOR:** feature 간 역참조와 중복 chip 구현이 없는지 확인하고 focused·전체 회귀, typecheck·lint·build, Chromium mock E2E와 시각 QA를 실행했다.
|
||
- **실행 명령:** `npm run test:run -- src/shared/ui/__tests__/tag-input.test.tsx src/features/series/tests/series-form.test.tsx src/features/audio-contents/tests/audio-form.test.tsx`; `npm run test:run`; `npm run typecheck`; `npm run lint`; `npm run build:prod`; `npm run e2e:mock -- tests/e2e/series.spec.ts --project=chromium`.
|
||
- **기대 결과:** 모든 명령 exit 0, Series image-first order와 `keyword="달빛,상담"`, Audio `tags="상담,힐링"`, 빈 chip 0건, 삭제 대상 외 chip 손실 0건, API 계약 변경 0건.
|
||
- **수동 확인:** Chromium 1280px Series 생성 화면에서 image가 첫 입력이고 keyword chip 추가·삭제 및 Korean text clipping/overflow가 없는지 확인한다.
|
||
|
||
**P5-R6 완료 Progress — 2026-08-04:**
|
||
|
||
- **RED:** shared `TagInput` import 부재, Series image가 title 뒤에 있는 DOM 순서, plain keyword input이 첫 값을 덮어 `"상담"`만 보내는 실패를 확인했다. validation focus 정렬은 image 대신 title이 focus되는 실패로, FileField visible focus는 `focus-within` ring class 부재 실패로 각각 고정했다.
|
||
- **GREEN:** `AudioTagInput`을 `src/shared/ui/tag-input.tsx`의 공용 `TagInput`으로 이동·일반화하고 Audio `tags`와 Series `keyword`가 같은 chip UI를 사용하게 했다. Series image를 form 첫 field로 이동하고 request는 `keyword: "달빛,상담"` comma-separated string을 유지했다. image-first validation focus가 보이도록 공용 `FileField` 카드에 기존 ring token의 `focus-within` 상태만 추가했다.
|
||
- **REFACTOR/회귀:** focused 3 files / 20 tests와 FileField 포함 focused 3 files / 15 tests를 통과했다. 최종 `npm run test:run`은 82 files / 437 tests, `npm run typecheck`, `npm run lint`, `npm run build:prod`, `git diff --check`는 모두 통과했고 build에는 기존 500kB chunk warning만 남았다. `npm run e2e:mock -- tests/e2e/audio-content.spec.ts tests/e2e/series.spec.ts --project=chromium`은 13 tests passed였다.
|
||
- **수동·시각 QA:** Playwright Chromium 768×900·1280×900에서 image-first, Enter/comma chip 2개, remove 후 나머지 값, 빈 draft, 가로 overflow 0건을 확인했다. validation 후 sr-only file input focus와 visible cyan FileField ring을 768×900에서 확인했다. 독립 visual functional/CJK reviewer와 고엄격 diff reviewer의 최종 판정은 모두 PASS/APPROVE, blocker 0건이었다. Mock Preview sticky banner 아래로 static header가 이동한 프레임은 `scrollY=47`의 정상 문서 스크롤로 측정했고 `scrollY=0`에서 동일 focus/ring과 온전한 header를 재확인했다.
|
||
|
||
---
|
||
|
||
## Phase 6. Community vertical slice
|
||
|
||
**목표:** 별도 상세 route/GET 없이 Community 목록과 Sheet만으로 게시글 등록·조회·수정·고정·비활성화·첨부 재생을 완료한다.
|
||
|
||
**Phase Goal `P6`:** Task 6.1 → 6.4와 Phase 6 Gate로 목록 기반 Community Sheet·media slice를 완성한다.
|
||
|
||
- **시작 조건:** `P2-GATE`와 `P4-T2` player integration 완료.
|
||
- **완료 조건:** `P6-T1`~`P6-T4`, `P6-GATE` 완료. 오류·price는 제공 계약 또는 최소 규칙으로 종결.
|
||
- **실행 순서:** 계약 확인 → 목록/Sheet → form/media → 반응형·접근성.
|
||
|
||
**요구사항:** `COMMUNITY-001~015`, `FILE-001~004`, `FILE-007~009`, `FILE-011~014`, PRD `9`의 Community 범위.
|
||
|
||
**외부 의존:** `EXT-007` active-only 반환 보장, `EXT-008` pagination의 total/hasNext 또는 종료 규칙. price 상한은 `99999`이고, 파일 용량·MIME 등 backend 동일 검증은 확인됐다. OpenAPI 밖의 Community 오류 message key는 분기하지 않는다. Comments는 Phase 8에서 연결한다.
|
||
|
||
**주요 Files:**
|
||
|
||
- Create: `src/features/community-posts/api/community-post-api.ts`
|
||
- Create: `src/features/community-posts/model/types.ts`
|
||
- Create: `src/features/community-posts/validation/community-post-media-policy.ts`
|
||
- Create: `src/features/community-posts/pages/CommunityPostListPage.tsx`
|
||
- Create: `src/features/community-posts/components/{CommunityPostList,CommunityPostListItem,CommunityPostForm,CommunityPostSheet}.tsx`
|
||
- Create: `src/features/community-posts/tests/{community-contract.test.ts,community-list.test.tsx,community-form.test.tsx,community-sheet.test.tsx,community-test-support.ts}`
|
||
- Create: `tests/e2e/community.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/validation/community-post-media-policy.ts`, `src/features/community-posts/components/CommunityPostForm.tsx`, `src/features/community-posts/tests/community-form.test.tsx`, `src/features/community-posts/tests/community-test-support.ts`, `tests/e2e/community.spec.ts`; 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}`, `src/shared/mocks/{audio-content-fixtures.ts,community-post-fixtures.ts,community-post-mock-store.ts}`, `playwright.config.ts` | Consumes: `CommunityPostCreateRequest`, `CommunityPostUpdateRequest`, multipart part names. Produces: `createCommunityPost`, form serializer와 media policy | **TDD 적용:** `npm run test:run -- src/features/community-posts src/shared/validation`와 `npm run e2e:mock -- tests/e2e/community.spec.ts --project=chromium`; 기대 `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.spec.ts` | Consumes: P6-T2/T3 UI. Produces: viewport·Sheet focus capability evidence | **TDD 적용:** `npm run e2e:mock -- tests/e2e/community.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~015`, `MOCK-001~009`와 OpenAPI Community 3개 operation/schema 확인.
|
||
- **완료 증거:** 체크박스 전체, contract fixture와 상태/action inventory의 세 문서 일치.
|
||
- **범위 밖:** price 상한·오류 key 추정과 Comments 구현.
|
||
|
||
- [x] OpenAPI 공통 오류 status·shape를 fixture에 기록하고 Community 전용 message key와 media upload 오류 계약은 미제공으로 표시한다. 정확한 fixture를 추정하지 않는다.
|
||
- [x] price 최대값이 제공되면 Audio와 같은 정책으로 갱신하고, 없으면 0 이상 정수만 유지한다.
|
||
- [x] 목록 필수 `timezone`, `page/size`, 배열 `data`, 생성 `audioFile/postImage/request`, 수정 `postImage/request`, mutation `data=null`을 contract fixture로 고정한다.
|
||
- [x] 목록에 total/hasNext와 `isActive/fixedAtUtc`가 없음을 기록하고 해당 값을 fixture에서 추가하지 않는다.
|
||
- [x] 목록·Sheet·form/media의 상태/action inventory를 작성하고 Page는 collection query/policy 조합, feature component는 Community 규칙, shared component는 Phase 1 contract를 사용하도록 component map을 확정한다.
|
||
- [x] Community active list, Sheet, pin, media와 CRUD의 deterministic browser fixture 시나리오를 확정한다.
|
||
|
||
### Task 6.2 목록·collection Sheet
|
||
|
||
**Goal 실행 `P6-T2`:** 전용 detail route/GET 없는 목록과 collection Sheet mutation 흐름을 완성한다.
|
||
|
||
- **시작 조건:** `P6-T1` 완료.
|
||
- **완료 증거:** 체크박스 전체, list/Sheet/router/cache/pin/deactivate test와 detail GET 0회 기록.
|
||
- **범위 밖:** 댓글과 제공 계약에 없는 Community 검색.
|
||
|
||
- [x] 목록이 필수 `timezone=Asia/Seoul`과 `page/size`를 보내고 배열 `data`를 소비하며 loading·empty·error·retry와 URL query 보존을 제공하는지 test한다. 제공 계약에 없는 Community `search`, total, hasNext를 만들지 않는다.
|
||
- [x] 목록 item을 source로 Sheet를 열고 전용 detail GET을 0회 호출하는 test를 작성한다.
|
||
- [x] `/community-posts/:postId`, `/edit` route가 존재하지 않는 router test를 작성한다.
|
||
- [x] Sheet의 조회·수정·고정/해제·비활성화가 목록 cache와 같은 server response를 사용한다.
|
||
- [x] 고정/해제 후 mutation `data=null`을 처리하고 목록을 재조회해 `isFixed`를 갱신한다. 계약에 없는 `fixedAtUtc`는 표시하지 않는다.
|
||
- [x] soft delete request가 `isActive=false`, `isFixed=false`를 보내고 성공 `data=null`을 처리하는 contract test를 작성한다.
|
||
- [x] soft delete 성공 시 Sheet 종료, 목록 재조회와 성공 toast를 확인한다. 비활성 항목 제거는 active-only 계약 제공 후 server mode에서 검증한다.
|
||
- [x] mock mode도 전용 detail GET 없이 list store만으로 Sheet와 pin/deactivate 최종 UI를 갱신한다.
|
||
|
||
### Task 6.3 게시글 form·첨부 media
|
||
|
||
**Goal 실행 `P6-T3`:** Community form, JPEG/PNG/GIF와 audio 첨부·재생 규칙을 완성한다.
|
||
|
||
- **시작 조건:** `P6-T1`, `P6-T2` 완료.
|
||
- **완료 증거:** 체크박스 전체, serializer/form/media/file-boundary/player test, `OQ-009` 결정 기록.
|
||
- **범위 밖:** GIF 재인코딩, URL 갱신 전용 요청, Comments.
|
||
|
||
- [x] 생성 payload에 `isActive`가 없고 일반 update/soft delete가 공통 `isActive` 규칙을 지키는 test를 작성한다.
|
||
- [x] create의 필수 content·isCommentAvailable·isAdult, optional price와 multipart `postImage/audioFile`을 test하고 생성 request에 `isFixed`가 없음을 고정한다.
|
||
- [x] update는 optional `postImage`와 content·isCommentAvailable·isAdult·isActive·isFixed만 보내며 price·audioFile 교체 UI/request가 없음을 test한다.
|
||
- [x] JPEG/PNG는 자유 ratio crop·최대 800px·no-upscale을 적용한다.
|
||
- [x] GIF는 Community에서만 허용하고 crop Dialog/canvas/re-encode 없이 원본 ratio·animation을 유지한다.
|
||
- [x] GIF 원본 width 800px은 허용하고 801px은 제출 전에 거부한다.
|
||
- [x] 첨부 Audio는 Phase 1의 공통 audio file policy와 `FileField`를 Phase 4와 동일하게 조합해 MP3/AAC/M4A, x-m4a, `1,024,000,000 bytes`, WAV 거부 규칙을 재사용한다.
|
||
- [x] 첨부 Audio가 있으면 목록 Card/row와 Sheet에 공통 player를 렌더링한다.
|
||
- [x] media error가 detail/list refetch·URL 재발급·자동 play를 발생시키지 않는다.
|
||
- [x] 사용자 새로고침이나 mutation cache invalidation으로 목록이 정상 재조회된 때만 새 `audioUrl`을 사용한다.
|
||
- [x] 초기 Community Sheet/form을 실제 페이지에서 확인한 뒤 `content`의 최대 길이 권고안을 작성한다. backend 호환 확인 전에는 상한을 구현하지 않는다.
|
||
- [x] mock handler가 multipart contract를 검증하고 local preview media asset으로 create/update/play 최종 UI를 재현한다.
|
||
|
||
### Task 6.4 Community 반응형·접근성
|
||
|
||
**Goal 실행 `P6-T4`:** Community viewport capability와 Sheet/media/form 접근성을 검증한다.
|
||
|
||
- **시작 조건:** `P6-T2`, `P6-T3`의 활성 범위 완료.
|
||
- **완료 증거:** 체크박스 전체, focus trap/복귀·320px·keyboard·200% zoom·axe E2E 기록.
|
||
- **범위 밖:** 모바일 등록·수정·고정·비활성화.
|
||
|
||
- [x] mobile은 목록·Sheet 조회·첨부 재생만 제공하고 등록·수정·고정·비활성화를 route/action policy로 차단한다.
|
||
- [x] desktop/tablet에는 전체 관리 흐름을 제공한다.
|
||
- [x] Sheet focus trap/복귀, keyboard media/form, 320px overflow, 200% zoom, axe critical·serious 0건을 확인한다.
|
||
|
||
### Phase 6 Gate
|
||
|
||
**Goal 실행 `P6-GATE`:** Community mock UI journey와 실제 server integration 상태를 분리해 판정한다.
|
||
|
||
- **시작 조건:** `P6-T1`~`P6-T4` 완료.
|
||
- **완료 증거:** 아래 명령과 Expected 통과 및 Phase 검증 기록.
|
||
- **범위 밖:** 실패와 무관한 FanTalk/Comments 구현.
|
||
|
||
```bash
|
||
npm run test:run -- src/features/community-posts src/shared/validation
|
||
npm run e2e:mock -- tests/e2e/community.spec.ts
|
||
npm run e2e -- tests/e2e/community.spec.ts
|
||
npm run typecheck
|
||
npm run lint
|
||
npm run build
|
||
```
|
||
|
||
**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로 복귀하는지 본다.
|
||
|
||
### Task R6.1 — Community raw price validation 복구
|
||
|
||
**Goal 실행 `P6-R1`:** Community create에서 음수·소수 가격을 다른 값으로 바꾸지 않고 제출 전에 거부한다.
|
||
|
||
- **연결 리뷰:** [Phase 6 재리뷰](./reviews/review-phase-6-20260729.md) — `REV-P6-001`
|
||
- **시작 조건:** `COMMUNITY-007`, `CAN_PRICE_MAX=99999`; Audio와 의미가 같은 raw-price 규칙은 재사용한다.
|
||
- **Files:** Modify: `community-post-form-helpers.ts`, `CommunityPostForm.tsx`, `community-form.test.tsx`, `community-contract.test.ts`, 필요 시 `src/shared/validation/can-price.ts`.
|
||
- **Interfaces:** group separator와 `캔` suffix만 display normalization; `-`, `.`가 포함된 raw input은 invalid.
|
||
- **범위 밖:** Community update price UI, 통화 locale 확장.
|
||
|
||
- [x] **RED:** `-1`, `1.5` 입력 시 inline 오류와 POST 0건, `0/99999` 허용 test를 작성한다.
|
||
- [x] **GREEN:** raw/display price state 또는 공통 parser result를 적용한다.
|
||
- [x] **REFACTOR:** Audio `P4-R1`과 동일 의미의 validation만 shared로 올리고 Community 회귀를 실행한다.
|
||
- **실행 명령:** `npm run test:run -- src/shared/validation/can-price.test.ts src/features/community-posts`.
|
||
- **기대 결과:** exit 0, invalid price POST 0건, 경계값 multipart 유지.
|
||
- **수동 확인:** Community create 가격 input에서 음수·소수 오류 문구와 입력 보존을 확인한다.
|
||
|
||
### Task R6.2 — Community 상태 boolean label 정리
|
||
|
||
**Goal 실행 `P6-R2`:** Community 목록의 성인 여부를 raw boolean이 아닌 한국어 상태 label로 표시한다.
|
||
|
||
- **연결 리뷰:** [Phase 6 현재 리뷰](./reviews/phase6-community-posts.md) — `REV-P6-002`
|
||
- **시작 조건:** PRD §13 날짜·가격·상태 label 중앙 표시 기준과 기존 desktop/mobile 목록.
|
||
- **완료 증거:** `성인 true/false` 노출 실패 test, 최소 표시 수정, Community focused unit·mock E2E와 검증 기록.
|
||
- **Files:** Modify: `CommunityPostList.tsx`, `CommunityPostListItem.tsx`, `community-list.test.tsx`, `tests/e2e/community.spec.ts`.
|
||
- **Interfaces:** boolean은 `성인/일반`처럼 의미 있는 한국어 label로 변환하고 API DTO·multipart 값은 boolean을 유지한다.
|
||
- **범위 밖:** Community API schema 변경, 상태 filter 추가, 전역 i18n framework.
|
||
|
||
- [x] **RED:** desktop/mobile에 raw `true/false`가 표시되지 않고 핵심 상태 label이 일치하는 실패 test를 작성한다.
|
||
- [x] **GREEN:** Community 전용 상태 formatter를 목록 row/card에 적용한다.
|
||
- [x] **REFACTOR:** 고정·댓글 허용·성인 상태 copy를 한 표시 helper로 정리한다.
|
||
- **실행 명령:** `npm run test:run -- src/features/community-posts`; `npm run e2e:mock -- tests/e2e/community.spec.ts`; `npm run typecheck`; `npm run lint`; `npm run build`.
|
||
- **기대 결과:** 모든 명령 exit 0, Community 사용자 화면의 raw boolean 0건, request payload 변경 0건.
|
||
- **수동 확인:** desktop과 320px 카드에서 동일 게시글의 고정·댓글·성인 상태를 확인한다.
|
||
|
||
### Task R6.3 — Community 비활성화 확인 경계 추가
|
||
|
||
**Goal 실행 `P6-R3`:** Community 게시글 비활성화를 직접 실행하지 않고 영향 설명이 있는 확인 dialog를 거쳐 확정하도록 한다.
|
||
|
||
- **연결 리뷰:** [Phase 6 현재 리뷰](./reviews/phase6-community-posts.md) — `REV-P6-003`
|
||
- **시작 조건:** PRD §10.4의 영향이 큰 비활성화 동작 AlertDialog 기준과 `COMMUNITY-005` soft delete 계약.
|
||
- **완료 증거:** 첫 click에서 DELETE 0건·confirm 후 1건을 기대하는 실패 test, 공통 확인 UI 재사용, Community focused unit·mock E2E·접근성 Gate 통과.
|
||
- **Files:** Modify: `src/features/community-posts/components/CommunityPostSheet.tsx`, `src/features/community-posts/tests/community-sheet.test.tsx`, `tests/e2e/community.spec.ts`; Reuse: `src/shared/ui/confirm-deactivate-dialog.tsx`.
|
||
- **Interfaces:** 확인 전 request 0건, confirm 후 현재 post ID에 DELETE 1건, pending/error와 Sheet focus 경계를 접근 가능하게 유지한다.
|
||
- **범위 밖:** 댓글 삭제 확인 정책 변경, 복원 기능, 새 dialog primitive.
|
||
|
||
- [x] **RED:** `비활성화` 첫 click은 확인 dialog만 열고 DELETE를 보내지 않으며 취소는 Sheet로 focus를 돌리고 confirm만 DELETE 1건을 보내야 한다는 실패 test를 작성한다.
|
||
- [x] **GREEN:** 기존 `ConfirmDeactivateDialog`를 Community Sheet에 연결하고 pending·오류 상태를 전달한다.
|
||
- [x] **REFACTOR:** 삭제 API와 성공 callback은 유지하고 dialog open/pending state만 Sheet에 추가한다.
|
||
- **실행 명령:** `npm run test:run -- src/features/community-posts`; `npm run e2e:mock -- tests/e2e/community.spec.ts --project=chromium`; `npm run typecheck`; `npm run lint`; `npm run build`.
|
||
- **기대 결과:** 모든 명령 exit 0, 확인 전·취소 후 DELETE 0건, confirm 연타에도 DELETE 최대 1건, 실패 안내와 재시도 가능.
|
||
- **수동 확인:** keyboard-only로 Sheet의 비활성화 → 취소 → 재확인 → 실패/성공 흐름과 focus 복귀를 확인한다.
|
||
|
||
### Task R6.4 — Community GIF crop source Blob URL 해제
|
||
|
||
**Goal 실행 `P6-R4`:** Community GIF width 검사를 위해 생성한 crop source의 Blob URL을 성공·거부 경로에서 각각 정확히 한 번 해제한다.
|
||
|
||
- **연결 리뷰:** [Phase 6 현재 리뷰](./reviews/phase6-community-posts.md) — `REV-P6-004`
|
||
- **시작 조건:** PRD `FILE-003`, `FILE-011`, `FILE-014`, `prepareCommunityPostImage()`가 GIF의 width를 읽은 뒤 `source.release` 소유권을 form에 전달하지 않고 `ready` 또는 `error`를 반환하는 현재 구조.
|
||
- **완료 증거:** 800px 이하 GIF `ready`와 800px 초과 GIF `error`에서 release 1회, JPEG/PNG `crop` handoff에서 조기 release 0회를 고정하는 실패 test, Community focused unit·mock E2E·정적 Gate 기록.
|
||
- **Files:** Modify: `src/features/community-posts/validation/community-post-media-policy.ts`; Create: `src/features/community-posts/tests/community-post-media-policy.test.ts`; Test: `src/features/community-posts/tests/community-form.test.tsx`, `src/features/community-posts/tests/community-sheet.test.tsx`, `tests/e2e/community.spec.ts`.
|
||
- **Interfaces:** GIF는 dimension 확인 후 crop source를 외부로 반환하지 않으므로 `prepareCommunityPostImage` 내부가 release를 소유한다. JPEG/PNG `crop` 결과의 release 소유권은 기존 form/Sheet에 그대로 전달한다.
|
||
- **범위 밖:** GIF resize·crop·re-encode, 외부 image decoder, 공통 crop source 재설계, 서버 GIF 계약 변경.
|
||
- **Phase 6 Gate 연결:** media policy focused unit, create/Sheet 교체·stale·unmount 회귀, mock Chromium Community journey, `typecheck`·`lint`·build가 모두 통과하기 전에는 완료하지 않는다.
|
||
|
||
- [x] **RED:** idempotent `release` spy를 사용해 valid GIF·oversized GIF은 각 1회, JPEG/PNG crop handoff는 0회인 실패 test를 작성했다.
|
||
- [x] **GREEN:** GIF width 판정이 끝나면 `ready`/거부 반환 전 source를 해제하는 최소 수정을 적용했다.
|
||
- [x] **REFACTOR:** release 호출 경계를 GIF 분기 한 곳에 유지하고 create/Sheet의 JPEG/PNG lifecycle과 stale selection 회귀를 확인했다.
|
||
- [x] RED/GREEN/REFACTOR, media policy·Community 단위·정적 검증을 Progress와 Phase 6 리뷰에 누적했다. E2E와 반복 GIF 수동 확인은 전체 Task 구현 후 필요 시 수행한다.
|
||
- **실행 명령:** `npm run test:run -- src/features/community-posts/tests/community-post-media-policy.test.ts src/features/community-posts/tests/community-form.test.tsx src/features/community-posts/tests/community-sheet.test.tsx`; `npm run test:run -- src/features/community-posts src/shared/lib/create-image-crop-source.test.ts`; `npm run e2e:mock -- tests/e2e/community.spec.ts --project=chromium`; `npm run typecheck`; `npm run lint`; `npm run build`.
|
||
- **기대 결과:** 모든 명령 exit 0, valid/oversized GIF Blob URL 미해제 0건·중복 release 0건, JPEG/PNG crop lifecycle 회귀 0건.
|
||
- **수동 확인:** GIF를 반복 교체하고 800px 경계 아래/위를 선택해 crop Dialog 미노출, animation 유지, 거부 문구와 재선택 회복을 확인한다.
|
||
|
||
**P6-R4 수정 검증 기록 (2026-07-31):**
|
||
|
||
- RED: `npm run test:run -- src/features/community-posts/tests/community-post-media-policy.test.ts` — 1 failed. 신규 `prepareCommunityPostImage releases GIF crop sources after width validation`에서 valid GIF release 기대 1회 대비 0회로 실패했다.
|
||
- GREEN: `prepareCommunityPostImage()`의 GIF 분기에서 width 판정을 먼저 저장한 뒤 `source.release?.()`를 호출하게 했다.
|
||
- REFACTOR/회귀: release 호출은 GIF 분기 한 곳에만 두고 JPEG/PNG `crop` handoff는 조기 release하지 않도록 유지했으며, valid/oversized GIF 모두 `width → release` 순서를 test로 고정했다. `npm run test:run -- src/features/community-posts/tests/community-post-media-policy.test.ts src/features/community-posts/tests/community-form.test.tsx src/features/community-posts/tests/community-sheet.test.tsx` — 3 files / 18 tests passed. `npm run test:run -- src/features/community-posts` — 7 files / 45 tests passed.
|
||
- 정적 검증: LSP diagnostics `community-post-media-policy.ts` 오류 0건이고 test 파일은 LSP 응답 timeout이 있었으나 `typecheck`로 오류 없음을 확인했다. `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0. build는 기존 500kB chunk warning을 표시했다.
|
||
- 리뷰어 게이트: 1차 review에서 width 판정 전 release 순서 blocker를 지적받아 보완했고, delta review에서 `APPROVED` 판정을 받았다.
|
||
- E2E/수동: 개발 중 E2E와 반복 GIF browser 수동 확인은 사용자 지시에 따라 전체 Task 구현 후 필요 시 수행한다.
|
||
|
||
### Task R6.5 — 업로드 이미지 미리보기와 Community 생성 media flow 정렬
|
||
|
||
**Goal 실행 `P6-R5`:** 공통 `FileField`에 실제 업로드 이미지 미리보기를 제공하고 Community 생성 화면을 이미지 → 조건부 오디오 → 내용 순서와 Audio 동일 가격 계약으로 정렬한다.
|
||
|
||
- **시작 조건:** PRD `COMMUNITY-003`, `COMMUNITY-007`, `COMMUNITY-013`, `COMMUNITY-016`, `FILE-016`; 사용자 승인 설계.
|
||
- **완료 증거:** 이미지 preview URL 생성·교체·해제, 비이미지 filename-only, Community media 순서·audio 조건부 표시/제거, 기본 가격 0·blank 거부·항상 price 전송의 RED/GREEN, Audio 가격 회귀, Chromium mock browser와 visual QA.
|
||
- **Files:** Create: `src/shared/ui/can-price-field.tsx`, `src/shared/ui/__tests__/can-price-field.test.tsx`; Modify: `src/app/protected-admin-shell.tsx`, `src/app/App.protected-shell.test.tsx`, `src/shared/ui/file-field.tsx`, `src/shared/ui/page-state.tsx`, `src/shared/ui/__tests__/file-field.test.tsx`, `src/shared/ui/__tests__/page-state.test.tsx`, `src/features/audio-contents/components/AudioContentForm.tsx`, `src/features/audio-contents/components/audio-content-form-helpers.ts`, `src/features/audio-contents/tests/audio-form-create-red.test.tsx`, `src/features/audio-contents/tests/audio-form-update.test.tsx`, `src/features/audio-contents/tests/audio-form.test.tsx`, `src/features/community-posts/components/CommunityPostForm.tsx`, `src/features/community-posts/components/community-post-form-helpers.ts`, `src/features/community-posts/tests/community-form.test.tsx`, `src/features/community-posts/tests/community-price-validation.test.tsx`, `src/features/community-posts/tests/community-contract.test.ts`, `tests/e2e/community.spec.ts`, `DESIGN.md`, `docs/20260725_AI캐릭터관리자웹/prd.md`, `docs/20260725_AI캐릭터관리자웹/plan-task.md`.
|
||
- **Interfaces:** `FileField`는 image File일 때만 object URL preview를 소유하고 교체·제거·unmount에서 revoke한다. `CanPriceField({value,onChange,error,errorId})`는 Audio와 Community의 native number/min=0/step=1/단위·오류 연결을 공유하고 range validation은 domain submit이 소유한다. Community는 최종 `postImage`가 있을 때만 audio input을 렌더하며 image 제거·교체 시작 시 `audioFile`을 제거한다.
|
||
- **범위 밖:** Community 수정 Sheet, multipart append 순서, API schema의 optional price, 비이미지 preview, 새 dependency, GIF resize·crop·re-encode.
|
||
|
||
- [x] **RED:** `FileField previews image values and revokes object URLs on replacement, clear, and unmount`; `FileField keeps non-image values filename-only without creating an object URL`을 실패시킨다.
|
||
- [x] **GREEN:** image value에만 bounded object-contain preview를 렌더하고 URL lifecycle을 `FileField` 한 곳에서 소유한다.
|
||
- [x] **RED:** `CanPriceField renders the Audio numeric CAN price contract`; `CanPriceField connects its inline error and emits Audio-compatible values`를 import 부재로 실패시킨다.
|
||
- [x] **GREEN:** 공통 numeric CAN field를 추가하고 Audio inline price markup을 교체하되 paid option reset과 request를 보존한다.
|
||
- [x] **RED:** Community 최종 preview/media-first order, image 전 audio 부재·image 변경 시 stale audio 제거, 기본 0·blank 거부·항상 price 전송을 실패시킨다.
|
||
- [x] **GREEN:** Community form state·render order·request를 최소 수정하고 multipart serializer 순서는 변경하지 않는다.
|
||
- [x] **REFACTOR:** 불필요해진 domain price formatter wrapper를 제거하고 shared/domain focused test, full Gate, 375/768/1280 browser·visual QA와 독립 review를 통과한다.
|
||
- **실행 명령:** `npm run test:run -- src/shared/ui/__tests__/file-field.test.tsx src/shared/ui/__tests__/can-price-field.test.tsx`; `npm run test:run -- src/features/audio-contents src/features/community-posts`; `npm run test:run`; `npm run typecheck`; `npm run lint`; `npm run build:dev`; `npm run build:prod`; `npm run e2e:mock -- tests/e2e/community.spec.ts`; `git diff --check`.
|
||
- **기대 결과:** 모든 명령 exit 0, crop/GIF 적용 전 최종 preview 0건, image 없는 audio input·stale audio multipart 0건, Community valid request의 price 누락 0건, Audio 가격 회귀 0건, object URL 미해제 0건.
|
||
- **수동 확인:** mock mode 375px read-only 정책, 768px·1280px Community 생성의 initial/crop/applied/replacement/error 상태를 keyboard·가로 overflow·Korean text 기준으로 확인하고 QA browser/server/temp fixture를 모두 정리한다. 내부 관리자 화면이므로 별도 browser zoom 검증은 제외한다.
|
||
|
||
**P6-R5 진행 기록 (2026-08-04):**
|
||
|
||
- RED/GREEN: FileField preview·Blob URL·native input 동기화 11개, 공용 CAN field와 Audio/Community 음수·소수·상한·blank·`00` zero reset, Community media 순서·조건부 audio·stale state·request 회귀를 실패 확인 후 최소 수정했다.
|
||
- 자동 Gate: `npm run test:run` — 83 files / 458 tests passed; `npm run lint`, `npm run typecheck`, `npm run build:dev`, `npm run build:prod`, `git diff --check` — exit 0. build의 500kB chunk warning은 기존 비차단 경고다.
|
||
- 브라우저: `npm run e2e:mock -- tests/e2e/community.spec.ts` — Chromium/mobile Chrome 14/14 passed. 375px mobile read-only, 768px·1280px final GIF preview·conditional audio·제거·blank 가격·keyboard·overflow·axe를 확인했다. 사용자 결정에 따라 browser zoom 검증은 제외했다.
|
||
- 리뷰: 목표·품질 review에서 문서 추적성과 Audio raw `00` zero reset blocker를 발견했다. `parsePrice(value) === 0`으로 보완해 Audio focused 9 files / 69 tests를 통과했고, 문서 상태·파일 지도·Decision Log를 정렬했다.
|
||
- 최종 browser/visual: 768px·1280px Chromium/mobile Chrome에서 PNG crop/applied/replacement/error와 기존 GIF/audio/price 상태를 포함해 Community E2E 14/14를 통과했다. 4개 project/viewport evidence directory의 32개 표준 배율 PNG를 기능·디자인 시스템 및 CJK reviewer가 전수 확인해 PASS/HIGH, blocker 0건으로 판정했다. `캔`은 PRD의 정상 CAN 단위이며, 사용자 결정에 따라 browser zoom 검증은 제외했다.
|
||
|
||
---
|
||
|
||
## Phase 7. FanTalk vertical slice
|
||
|
||
**목표:** 모든 viewport에서 FanTalk 목록을 조회하고 답변이 없는 item에 한 번 답변한다. 상세·답변 수정·전체 결과 filter/sort는 계약 제공 후 후속 slice로 추가한다.
|
||
|
||
**Phase Goal `P7`:** Task 7.1 → 7.3과 Phase 7 Gate로 OpenAPI가 제공한 FanTalk 목록·단일 답변 생성 slice를 완성한다.
|
||
|
||
- **시작 조건:** `P3-T2` workspace core와 `P2-GATE` 완료.
|
||
- **완료 조건:** 제공된 목록 GET·답변 POST 범위의 `P7-T1`~`P7-T3`, `P7-GATE` 완료. 미제공 상세·수정·filter/sort·유일성 오류는 외부 의존 상태와 후속 재개 조건 기록.
|
||
- **실행 순서:** 계약 확인 → 목록/답변 → 반응형·접근성.
|
||
|
||
**요구사항:** `FANTALK-001~011`, PRD `9`의 FanTalk 범위.
|
||
|
||
**외부 의존:** `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.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 목록·답변 생성 계약과 미제공 상세·수정·filter/sort·유일성 범위, mock 가능 범위와 component map을 확정한다.
|
||
|
||
- **시작 조건:** `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.
|
||
|
||
- [x] 목록 `page/size`, `data.fanTalkCount/fanTalks/page/size/hasNext`, item `creatorReplies`와 답변 POST `{content}`·성공 DTO를 contract fixture로 고정한다.
|
||
- [x] 별도 상세·답변 수정 endpoint, answer filter와 sort query가 없음을 기록하고 임시 route·query를 만들지 않는다.
|
||
- [x] 답변 1개를 server가 원자적으로 강제하는 방식과 중복 생성 비2xx status/message key를 기록한다.
|
||
- [x] 미제공 범위에는 임시 endpoint·DTO·production adapter를 만들지 않고 후속 재개 조건을 문서화한다.
|
||
- [x] 목록·reply Sheet/form의 상태/action inventory를 작성하고 Page는 route/query/policy 조합, feature component는 FanTalk 규칙, shared component는 Phase 1 contract를 사용하도록 component map을 확정한다.
|
||
- [x] 제공된 목록·답변 생성 범위만 browser fixture로 만들고 상세·수정·filter/sort·유일성 오류는 mock에서도 추정하지 않는다.
|
||
|
||
### Task 7.2 목록·답변 생성
|
||
|
||
**Goal 실행 `P7-T2`:** backend 순서를 유지하는 목록과 답변 1회 생성 흐름을 완성한다.
|
||
|
||
- **시작 조건:** `P7-T1` 완료.
|
||
- **완료 증거:** 체크박스 전체, list/reply contract·UI test, 중복 제출 차단·오류 복구, OQ-009 후속 검토 기록.
|
||
- **범위 밖:** 별도 상세, 답변 수정·삭제·두 번째 답변, 전체 결과 filter/sort와 계약 없는 network 동작.
|
||
|
||
- [x] 목록은 `page`, `size`를 URL에 보존하고 backend 반환 순서와 `hasNext`를 사용한다. 전체/미답변/답변 완료 filter와 client 재정렬은 만들지 않는다.
|
||
- [x] loading·empty·error·retry와 목록 새로고침을 test하고 `/fan-talks/:fanTalkId` route·상세 GET이 0건임을 검증한다.
|
||
- [x] 목록 item의 `creatorReplies`가 비어 있을 때만 POST form을 표시하고 답변이 있으면 읽기 전용으로 표시하며 edit/delete UI는 만들지 않는다.
|
||
- [x] 빠른 두 번 제출에도 POST가 한 번만 호출되는 test를 작성한다.
|
||
- [x] 답변 POST 성공 DTO의 `fanTalkId`, `replyId`, `creatorMemberId`, `content`, `createdAtUtc`를 반영하고 해당 목록 page를 재조회한다.
|
||
- [x] server 중복 오류를 받으면 현재 목록 page를 재조회해 `creatorReplies`를 갱신하고 status/key를 추정 분기하지 않는다.
|
||
- [x] 저장 중 중복 제출 차단, visible label, 오류 연결, 성공 live feedback을 test한다.
|
||
- [x] 초기 FanTalk reply form을 실제 페이지에서 확인한 뒤 `content` 최대 길이 권고안을 작성한다. backend 호환 확인 전에는 상한을 구현하지 않는다.
|
||
- [x] 제공 계약 범위의 mock store로 미답변 → 답변 생성 → 읽기 전용 답변 표시의 최종 UI E2E를 작성한다.
|
||
|
||
### Task 7.3 FanTalk 반응형·접근성
|
||
|
||
**Goal 실행 `P7-T3`:** 모든 viewport의 FanTalk 응대 capability와 접근성을 검증한다.
|
||
|
||
- **시작 조건:** `P7-T2` 완료.
|
||
- **완료 증거:** 체크박스 전체, 320px keyboard viewport·keyboard-only·200% zoom·axe E2E 기록.
|
||
- **범위 밖:** viewport별 기능 축소와 답변 삭제.
|
||
|
||
- [x] desktop/tablet/mobile 모두 목록 조회·답변 작성을 제공하고 수정 action은 제공하지 않는다.
|
||
- [x] 320px에서 keyboard가 reply input/submit을 가리지 않는 E2E를 작성한다.
|
||
- [x] keyboard-only 목록 탐색·reply Sheet·create, 200% zoom, axe critical·serious 0건을 확인한다.
|
||
|
||
### Phase 7 Gate
|
||
|
||
**Goal 실행 `P7-GATE`:** FanTalk mock UI journey와 실제 server integration 상태를 분리해 판정한다.
|
||
|
||
- **시작 조건:** `P7-T1`~`P7-T3` 활성 범위 완료.
|
||
- **완료 증거:** 아래 명령과 Expected 통과 또는 Phase 제외 결정의 검증 기록.
|
||
- **범위 밖:** 계약 미제공 상태에서 network integration 완료 주장.
|
||
|
||
```bash
|
||
npm run test:run -- src/features/fan-talks
|
||
npm run e2e:mock -- tests/e2e/fan-talk.spec.ts
|
||
npm run e2e -- tests/e2e/fan-talk.spec.ts
|
||
npm run typecheck
|
||
npm run lint
|
||
npm run build
|
||
```
|
||
|
||
**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가 발생하지 않는지 본다.
|
||
|
||
### Task R7.1 — FanTalk 날짜와 저장 성공 문구 현지화
|
||
|
||
**Goal 실행 `P7-R1`:** FanTalk 목록·Sheet·답변의 UTC 시각을 공통 서울 시각 formatter로 표시하고 저장 성공 안내에서 내부 ID를 제거한다.
|
||
|
||
- **연결 리뷰:** [Phase 7 현재 리뷰](./reviews/phase7-fan-talk.md) — `REV-P7-001`
|
||
- **시작 조건:** `FANTALK-009~010`, PRD §13 중앙 날짜 formatter와 한국어 운영 UI.
|
||
- **완료 증거:** raw UTC·내부 ID 노출 실패 test, 최소 표시 수정, FanTalk focused unit·mock E2E·Phase Gate와 검증 기록.
|
||
- **Files:** Modify: `FanTalkList.tsx`, `FanTalkListItem.tsx`, `FanTalkReplySheet.tsx`, `fan-talk-list.test.tsx`, `fan-talk-reply.test.tsx`, `tests/e2e/fan-talk.spec.ts`.
|
||
- **Interfaces:** `createdAtUtc`는 `formatSeoulDateTime`으로 표시하고, 성공 status는 등록/수정 결과를 한국어로 안내한다. response ID는 DTO parse와 refresh에만 사용한다.
|
||
- **범위 밖:** API 응답 field 제거, locale 선택 UI, FanTalk 정렬.
|
||
|
||
- [x] **RED:** raw `2026-...Z`, `fanTalk/reply/creator` ID 문자열이 사용자 화면에 노출되는 실패 test를 작성한다.
|
||
- [x] **GREEN:** 공통 formatter와 등록/수정 성공 copy를 목록·카드·Sheet에 적용한다.
|
||
- [x] **REFACTOR:** 생성/수정 success 상태를 같은 사용자 중심 용어로 정리하고 DTO assertion은 contract test에 유지한다.
|
||
- **실행 명령:** `npm run test:run -- src/features/fan-talks`; `npm run e2e:mock -- tests/e2e/fan-talk.spec.ts`; `npm run typecheck`; `npm run lint`; `npm run build`.
|
||
- **기대 결과:** 모든 명령 exit 0, FanTalk 화면의 raw UTC·내부 ID 노출 0건, reply ID mapping 회귀 0건.
|
||
- **수동 확인:** desktop/mobile에서 원글·답변 날짜와 등록/수정 성공 알림을 확인한다.
|
||
|
||
---
|
||
|
||
## Phase 8. Comments vertical slice
|
||
|
||
**목표:** Audio detail과 Community Sheet 양쪽에서 같은 2단계 댓글 UX를 제공하고 작성자별 수정·soft delete 권한을 일관되게 적용한다.
|
||
|
||
**Phase Goal `P8`:** Task 8.1 → 8.4와 Phase 8 Gate로 두 target의 2단계 Comments slice를 완성한다.
|
||
|
||
- **시작 조건:** `P4-T2` Audio detail, `P6-T2` Community Sheet와 `P2-GATE` 완료.
|
||
- **완료 조건:** 핵심 계약이 제공되면 `P8-T1`~`P8-T4`, `P8-GATE` 완료. 미제공이면 Phase 제외/후속 결정 문서화로 종결.
|
||
- **실행 순서:** 계약 확인 → target/thread → 권한 action → 반응형·접근성.
|
||
|
||
**요구사항:** `COMMENT-001~006`, PRD `9`의 Comments 범위.
|
||
|
||
**외부 의존:** `EXT-005`는 2026-07-29 OpenAPI 2.3.0에서 해결됐고, 구현은 Phase 10 `P10-T6`에서 완료한다. 2026-07-28 당시 OpenAPI의 Audio 상세 `commentList`와 Community 목록 `firstComment`는 읽기용 요약일 뿐 CRUD 계약이 아니었으므로 과거 제외 기록은 §7에 보존한다.
|
||
|
||
**주요 Files:**
|
||
|
||
- Create: `src/features/comments/api/comment-api.ts`
|
||
- Create: `src/features/comments/model/{types,comment-target}.ts`
|
||
- Create: `src/features/comments/schemas/comment-schema.ts`
|
||
- Create: `src/features/comments/components/{CommentThread,CommentForm,CommentActions,CommunityPostCommentsSheet}.tsx`
|
||
- Create: `src/features/comments/tests/comment-contract.test.ts`
|
||
- Create: `src/features/comments/tests/{comment-thread,comment-permissions}.test.tsx`
|
||
- Create: `tests/e2e/comments.spec.ts`
|
||
- Modify: `AudioContentDetailPage.tsx`, `CommunityPostSheet.tsx`
|
||
|
||
#### 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`와 OpenAPI의 comment summary schema 및 CRUD operation 부재 확인.
|
||
- **완료 증거:** 체크박스 전체, 제공 계약 또는 Phase 제외/후속 결정의 세 문서 일치.
|
||
- **범위 밖:** endpoint 이름 추정과 client-only permission 완료 주장.
|
||
|
||
- [x] Audio·Community target별 endpoint, query, DTO, page, 작성/수정/soft delete 응답을 기록한다.
|
||
- [x] root/direct reply 정확히 2단계인 server rule과 fan content 삭제 권한 오류 status/message key를 기록한다.
|
||
- [x] 계약이 없으면 target endpoint를 이름만 보고 추정하거나 client-only permission을 완료로 간주하지 않는다.
|
||
- [x] Audio detail·Community Sheet 진입별 thread/form/action inventory를 작성하고 host Page/Sheet는 target·query 조합, Comments component는 thread·permission 규칙, shared component는 Phase 1 contract를 사용하도록 component map을 확정한다.
|
||
- [x] target별 CRUD·2단계·권한 오류 계약이 제공된 뒤에만 browser fixture를 만들고 현재 미제공 계약을 mock DTO로 추정하지 않는다.
|
||
|
||
### Task 8.2 target adapter·2단계 thread
|
||
|
||
**Goal 실행 `P8-T2`:** Audio/Community target adapter와 정확히 2단계인 comment thread 조회를 완성한다.
|
||
|
||
- **시작 조건:** `P8-T1`에서 핵심 계약 제공 확인.
|
||
- **완료 증거:** 체크박스 전체, target contract/thread/state/page test와 Community 상태 보존 기록.
|
||
- **범위 밖:** 작성자별 mutation action.
|
||
|
||
- [x] UI target은 Audio와 Community를 명시적으로 구분하고 각 제공 endpoint로만 요청하는 contract test를 작성한다.
|
||
- [x] Community 상세 GET 없이 목록 item의 `characterId/postId`로 Comments Sheet를 연다.
|
||
- [x] Comments Sheet 종료 후 Community page·scroll 상태를 보존한다.
|
||
- [x] root와 direct reply만 렌더링하고 reply에는 reply action이 없음을 test한다.
|
||
- [x] long content, loading·empty·error·retry, page 갱신을 양 target에서 test한다.
|
||
|
||
### Task 8.3 작성자별 action
|
||
|
||
**Goal 실행 `P8-T3`:** AI Character와 fan 작성자별 수정·soft delete 권한을 일관되게 적용한다.
|
||
|
||
- **시작 조건:** `P8-T1`, `P8-T2` 완료.
|
||
- **완료 증거:** 체크박스 전체, permission/mutation/session/read-only test, `OQ-009` 결정 기록.
|
||
- **범위 밖:** fan edit와 reply의 reply.
|
||
|
||
- [x] AI Character 작성 root/reply에는 edit·soft delete를 제공한다.
|
||
- [x] fan 작성 root/reply에는 edit를 제공하지 않고 운영 soft delete만 제공한다.
|
||
- [x] fan edit request는 type과 UI 양쪽에서 생성할 수 없음을 test한다.
|
||
- [x] delete 전 대상과 영향을 확인하고 server 계약에 따라 tombstone 또는 목록 갱신을 적용한다.
|
||
- [x] Character workspace read-only 정책이 모든 comment mutation도 차단하는 test를 작성한다.
|
||
- [x] 중복 제출, server permission 오류, session 401/403이 공통 정책을 따르는지 test한다.
|
||
- [x] 댓글 CRUD 계약 제공 후 초기 Comment thread/form을 실제 페이지에서 확인하고 `content` 최대 길이 권고안을 작성한다. backend 호환 확인 전에는 상한을 구현하지 않는다.
|
||
- [x] 계약 제공 후 mock store로 양 target의 root/reply CRUD와 작성자별 권한 오류 최종 UI E2E를 작성한다.
|
||
|
||
### Task 8.4 Comments 반응형·접근성
|
||
|
||
**Goal 실행 `P8-T4`:** 모든 viewport의 Comments CRUD capability와 thread/form/dialog 접근성을 검증한다.
|
||
|
||
- **시작 조건:** `P8-T2`, `P8-T3` 완료.
|
||
- **완료 증거:** 체크박스 전체, 320px·keyboard-only·focus 복귀·200% zoom·axe E2E 기록.
|
||
- **범위 밖:** 모바일 기능 축소.
|
||
|
||
- [x] desktop/tablet/mobile 모두 조회·작성·수정·soft delete를 제공한다.
|
||
- [x] 320px에서 긴 댓글, reply indentation, action menu, keyboard 입력이 overflow하지 않는 E2E를 작성한다.
|
||
- [x] keyboard-only root/reply 작성·수정·delete dialog, focus 복귀, 200% zoom, axe critical·serious 0건을 확인한다.
|
||
|
||
### Phase 8 Gate
|
||
|
||
**Goal 실행 `P8-GATE`:** Comments mock UI journey와 실제 server integration 상태를 분리해 판정한다.
|
||
|
||
- **시작 조건:** `P8-T1`~`P8-T4` 활성 범위 완료.
|
||
- **완료 증거:** 아래 명령과 Expected 통과 또는 Phase 제외 결정의 검증 기록.
|
||
- **범위 밖:** 계약 미제공 상태에서 network integration 완료 주장.
|
||
|
||
```bash
|
||
npm run test:run -- src/features/comments
|
||
npm run e2e:mock -- tests/e2e/comments.spec.ts
|
||
npm run e2e -- tests/e2e/comments.spec.ts
|
||
npm run typecheck
|
||
npm run lint
|
||
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건인지 확인한다.
|
||
|
||
### Task R8.1 — Comment reply pagination과 empty state 완성
|
||
|
||
**Goal 실행 `P8-R1`:** 루트별 모든 direct reply page를 관리하고 댓글 0건을 명시적 empty state로 표시한다.
|
||
|
||
- **연결 리뷰:** [Phase 8 재리뷰](./reviews/review-phase-8-20260729.md) — `REV-P8-001`, `REV-P8-002`
|
||
- **시작 조건:** `COMMENT-001`, `COMMENT-006~007`, reply response `totalCount/items`.
|
||
- **Files:** Modify: `src/features/comments/components/CommentThread.tsx`, `src/features/comments/tests/comment-thread.test.tsx`, `src/features/comments/tests/comment-contract.test.ts`, `tests/e2e/comments.spec.ts`, 필요 시 comment mock fixtures/store.
|
||
- **Interfaces:** root별 reply state에 `page,size=20,data`; mutation 뒤 현재 page refetch, 빈 root 목록에서도 create form 유지.
|
||
- **범위 밖:** 3단계 reply, reply page size 선택, infinite scroll.
|
||
|
||
- [x] **RED:** 21개 reply에서 page 1 요청·표시, 마지막 reply 삭제 후 유효 page 복귀, root 0건 empty state test를 작성한다.
|
||
- [x] **GREEN:** root별 page state와 `ResourcePagination`, root empty `PageState`를 추가한다.
|
||
- [x] **REFACTOR:** reply loading/error/content state를 root page와 함께 정리하고 Audio/Community 소비 회귀를 실행한다.
|
||
- **실행 명령:** `npm run test:run -- src/features/comments src/features/audio-contents src/features/community-posts`; `npm run e2e:mock -- tests/e2e/comments.spec.ts`.
|
||
- **기대 결과:** exit 0, 21번째 이후 reply 접근 가능, reply-of-reply UI 0건, empty state와 create form 동시 노출.
|
||
- **수동 확인:** 320px/desktop에서 reply 다음·이전 page, mutation 후 현재 page와 keyboard focus를 확인한다.
|
||
|
||
### Task R8.2 — Comments mock mutation 불변식 복구
|
||
|
||
**Goal 실행 `P8-R2`:** Comments mock store가 root/direct reply 2단계, AI 작성 row만 수정, 단일 row soft delete 계약을 그대로 재현하게 한다.
|
||
|
||
- **연결 리뷰:** [Phase 8 현재 리뷰](./reviews/phase8-comments.md) — `REV-P8-003`
|
||
- **시작 조건:** `COMMENT-002~004`, `COMMENT-007`, `MOCK-005`, Audio·Community comment OpenAPI PUT/DELETE.
|
||
- **완료 증거:** mock handler 실패 재현 test, 최소 store 수정, Comments contract/unit·mock E2E·Phase Gate와 검증 기록.
|
||
- **Files:** Modify: `src/shared/mocks/comment-mock-store.ts`, `src/features/comments/tests/comment-contract.test.ts`, 필요 시 `tests/e2e/comments.spec.ts`.
|
||
- **Interfaces:** `parentId`는 같은 target의 활성 root만 허용하고, PUT은 `writerId === target creatorId`인 row만 허용하며, DELETE는 대상 row만 비활성화/제외하고 자식 row를 변경하지 않는다.
|
||
- **범위 밖:** 실제 backend 구현, 3단계 thread UI, 팬 댓글 수정 권한 확대.
|
||
|
||
- [x] **RED:** reply를 parent로 한 POST와 팬 row PUT은 비2xx, root DELETE 뒤 자식 row 보존을 검증하는 Audio·Community mock contract test를 작성한다.
|
||
- [x] **GREEN:** store validation과 delete mutation을 계약에 맞게 수정한다.
|
||
- [x] **REFACTOR:** target/ownership/root 검증을 작은 predicate로 정리하고 UI의 팬 PUT 0건 test를 유지한다.
|
||
- **실행 명령:** `npm run test:run -- src/features/comments src/shared/mocks`; `npm run e2e:mock -- tests/e2e/comments.spec.ts`; `npm run typecheck`; `npm run lint`; `npm run build`.
|
||
- **기대 결과:** 모든 명령 exit 0, 3단계 POST·팬 PUT 성공 0건, root DELETE 후 하위 reply 변경 0건.
|
||
- **수동 확인:** Audio와 Community mock에서 팬/AI row action과 root 삭제 후 목록 재조회 동작을 확인한다.
|
||
|
||
**Progress — 2026-07-30 P8-R2 완료**
|
||
|
||
- **RED:** `npm run test:run -- src/features/comments/tests/comment-contract.test.ts`는 신규 Audio·Community 3단계 POST, fan PUT, root DELETE 뒤 답글 보존 assertion에서 1 file / 3 failed / 3 passed로 실패했다.
|
||
- **GREEN:** `CommentMockStore`에 target root parent와 creator-owned predicate를 추가하고, DELETE filter를 대상 ID만 제외하도록 축소했다. 같은 focused contract test는 1 file / 6 tests passed였다.
|
||
- **REFACTOR:** API schema/path/UI는 변경하지 않았다. 기존 Chromium E2E의 fan PUT 0-request assertion을 유지했다.
|
||
|
||
### Task R8.3 — Comments 공통 pagination size 계약 복구
|
||
|
||
**Goal 실행 `P8-R3`:** Audio·Community Comments adapter가 공통 `size >= 1`을 그대로 사용하고 FanTalk 전용 `20..50` clamp를 적용하지 않게 한다.
|
||
|
||
- **연결 리뷰:** [Phase 8 현재 리뷰](./reviews/phase8-comments.md) — `REV-P8-004`
|
||
- **시작 조건:** PRD §11.1 공통 pagination, OpenAPI 공통 `Size` parameter의 default `20`, minimum `1`, maximum 없음.
|
||
- **완료 증거:** `size=1`, `size=51` request path 실패 test, adapter 최소 수정, Audio·Community Comments unit/mock E2E 통과.
|
||
- **Files:** Modify: `src/features/comments/api/comment-api.ts`, `src/features/comments/tests/comment-contract.test.ts`; 필요 시 Comments API adapter focused test.
|
||
- **Interfaces:** page는 기본 `0`·최소 `0`, size는 기본 `20`·최소 `1`; `20..50` 보정은 FanTalk에만 남긴다.
|
||
- **범위 밖:** Comments UI page-size option 변경, server pagination metadata 변경, FanTalk pagination 변경.
|
||
|
||
- [x] **RED:** root/replies GET에서 `size=1`과 `size=51`이 각각 그대로 query에 남아야 한다는 contract test를 작성했다.
|
||
- [x] **GREEN:** Comments `normalizeSize`를 기본 20·최소 1만 적용하도록 수정했다.
|
||
- [x] **REFACTOR:** FanTalk 전용 규칙을 공유 helper로 끌어올리지 않고 Comments path 생성 중복만 현재 수준에서 유지했다.
|
||
- **실행 명령:** `npm run test:run -- src/features/comments`; `npm run e2e:mock -- tests/e2e/comments.spec.ts --project=chromium`; `npm run typecheck`; `npm run lint`.
|
||
- **기대 결과:** 모든 명령 exit 0, `size=1`·`size=51` 보존, `size<=0`만 1로 보정, 기존 댓글 흐름 회귀 0건.
|
||
- **수동 확인:** 기본 `size=20` 화면과 reply pagination이 기존처럼 동작하는지 확인한다.
|
||
|
||
**P8-R3 수정 검증 기록 (2026-07-30):**
|
||
|
||
- RED: `npm run test:run -- src/features/comments/tests/comment-contract.test.ts` — 1 failed / 5 passed. root `size=1`이 `size=20`으로, replies `size=51`이 `size=50`으로 바뀌어 기대대로 실패했다.
|
||
- GREEN focused: 같은 command — 1 file / 6 tests passed. root/replies query가 `size=1`과 `size=51`을 보존함을 확인했다.
|
||
- REFACTOR/회귀: `npm run test:run -- src/features/comments` — 2 files / 11 tests passed. `npm run typecheck`, `npm run lint` — 모두 exit 0.
|
||
- LSP diagnostics: `src/features/comments/api/comment-api.ts`, `src/features/comments/tests/comment-contract.test.ts` — 오류 0건.
|
||
- E2E/수동 확인: 사용자 지시에 따라 개발 중 E2E는 실행하지 않고 `P10-R5` 이후 최종 E2E에서 확인한다.
|
||
|
||
### Task R8.4 — 댓글 전송 실패 시 초안 보존과 재시도 복구
|
||
|
||
**Goal 실행 `P8-R4`:** 루트 댓글·답글 POST가 실패하면 입력 초안을 유지하고 같은 내용으로 재시도한 뒤 성공할 때만 입력을 비운다.
|
||
|
||
- **연결 리뷰:** [Phase 8 현재 리뷰](./reviews/phase8-comments.md) — `REV-P8-005`
|
||
- **시작 조건:** PRD §10.5 오류·재시도 상태, 현재 `CommentThread.runMutation`이 오류를 내부 처리하고 성공처럼 resolve해 `CommentForm`이 실패 후에도 입력을 비우는 구조.
|
||
- **완료 증거:** Audio·Community 루트/답글 POST reject 후 textarea 값 유지와 재시도 성공 실패 test, submit 성공 여부 contract 최소 수정, Comments focused unit·mock E2E·정적 Gate 통과.
|
||
- **Files:** Modify: `src/features/comments/components/CommentForm.tsx`, `src/features/comments/components/CommentThread.tsx`, `src/features/comments/tests/comment-thread.test.tsx`; Test: `tests/e2e/comments.spec.ts`.
|
||
- **Interfaces:** `CommentForm.onSubmit`은 mutation 성공 여부를 반환하고 입력은 성공일 때만 초기화한다. 기존 API payload, root/reply cache 재조회, 전역 pending guard는 유지한다.
|
||
- **범위 밖:** 댓글 localStorage draft, offline queue, optimistic update, edit form 동작 변경.
|
||
- **Phase 8 Gate 연결:** Audio·Community 루트/답글 실패·재시도 focused test와 Comments mock E2E, `typecheck`·`lint`·build가 모두 통과하기 전에는 종결하지 않는다.
|
||
|
||
- [x] **RED:** 루트 댓글과 열린 답글 form의 첫 POST를 reject해 visible server error와 기존 textarea 값 유지, 두 번째 POST 성공 후 값 초기화를 기대하는 실패 test를 작성했다.
|
||
- [x] **GREEN:** `runMutation`과 submit callback이 성공 여부를 반환하게 하고 `CommentForm`은 성공한 경우에만 초안과 local error를 초기화했다.
|
||
- [x] **REFACTOR:** 새 form/state library 없이 현재 component contract만 최소 변경하고 update/delete 및 pending 재진입 회귀를 확인했다.
|
||
- **실행 명령:** `npm run test:run -- src/features/comments`; `npm run e2e:mock -- tests/e2e/comments.spec.ts --project=chromium`; `npm run typecheck`; `npm run lint`; `npm run build`.
|
||
- **기대 결과:** 모든 명령 exit 0, POST 실패 후 루트/답글 초안 유지, 같은 form 재시도 성공, 성공 후에만 textarea 초기화, 중복 request 0건.
|
||
- **수동 확인:** mock 500 응답에서 루트 댓글과 답글을 각각 전송해 오류·초안 유지·재시도를 확인한다.
|
||
|
||
**P8-R4 수정 검증 기록 (2026-07-31):**
|
||
|
||
- RED: `npm run test:run -- src/features/comments/tests/comment-thread.test.tsx` — 1 failed / 5 passed. 루트 댓글 POST 실패 후 `CommentForm` textarea가 빈 값으로 바뀌어 기대대로 실패했다.
|
||
- GREEN: `CommentForm.onSubmit`이 성공 여부를 반환하게 하고, `CommentThread.runMutation`이 성공 시 `true`, 실패·pending 재진입 차단 시 `false`를 반환하게 했다. 입력값과 local error는 성공한 경우에만 초기화된다.
|
||
- Focused: `npm run test:run -- src/features/comments/tests/comment-thread.test.tsx` — 1 file / 6 tests passed. 루트와 답글 모두 첫 실패 후 초안 유지, 같은 form 재시도 성공 후 값 초기화를 확인했다.
|
||
- REFACTOR/회귀: `npm run test:run -- src/features/comments` — 3 files / 13 tests passed. `src/features/comments` LSP diagnostics — 5 TSX files / 0 diagnostics. 새 form/state library나 optimistic/localStorage draft는 추가하지 않았다.
|
||
- E2E/수동 확인: 사용자 지시에 따라 개발 중 반복 E2E는 실행하지 않고 최종 회귀 단계에서 Comments mock E2E를 필요 시 실행한다.
|
||
|
||
---
|
||
|
||
## Phase 9. 교차 회귀·인수인계
|
||
|
||
**목표:** 새 기능을 추가하지 않고 활성 릴리스 범위 전체가 PRD, API Contract, 보안, 반응형, 접근성 기준을 만족한다는 최신 증거를 남긴다.
|
||
|
||
**Phase Goal `P9`:** Task 9.1 → 9.3과 Phase 9 Gate로 활성 릴리스 범위의 최종 인수 증거를 완성한다.
|
||
|
||
- **시작 조건:** 활성 범위의 `P0-GATE`~`P8-GATE` 완료 또는 명시적 제외 결정.
|
||
- **완료 조건:** `P9-T1`~`P9-T3`, `P9-GATE` 완료, 활성 mock UI와 server allowlist 자동 Gate 0 failure/0 error, 수동 검증 기록. 도메인별 server integration은 별도 backend 계약·server-mode allowlist가 충족될 때 완료한다.
|
||
- **실행 순서:** 교차 journey/오류 → 반응형/접근성/보안 → 문서/품질 → 전체 Gate.
|
||
|
||
**주요 Files:**
|
||
|
||
- Create: `tests/e2e/{resource-workflows,error-mapping,responsive-capabilities,accessibility}.spec.ts`
|
||
- Modify: `README.md`
|
||
- 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·오류 회귀
|
||
|
||
**Goal 실행 `P9-T1`:** 활성 도메인의 mock/server 교차 journey, 오류 매핑과 serializer/security 불변식을 회귀 검증한다.
|
||
|
||
- **시작 조건:** 활성 범위의 Phase 0~8 Gate 완료 또는 제외 결정.
|
||
- **완료 증거:** 체크박스 전체, resource-workflows/error-mapping E2E와 request 0회·serializer fixture 기록.
|
||
- **범위 밖:** 새 기능과 계약 미제공 제외 범위의 가짜 journey.
|
||
|
||
- [x] login → Character select → Audio immediate/scheduled create/play → Series link/order → Community Sheet → FanTalk list/reply → Comments 중 계약이 제공된 활성 범위 journey를 검증한다.
|
||
- [x] 400/401/403/404/405/406/415/500 fixture가 공통 한국어 message와 올바른 route/session 처리를 하는지 검증한다.
|
||
- [x] Character·Audio·Series soft delete는 목록 이동, Community soft delete는 Sheet 종료·목록 제거로 끝나는지 검증한다.
|
||
- [x] inactive Character workspace에서 모든 하위 mutation request가 0건인지 검증한다.
|
||
- [x] media error로 Audio/Community GET·URL 재발급·자동 `play()`가 발생하지 않는지 검증한다.
|
||
- [x] 생성·일반 수정·soft delete serializer 불변식을 모든 도메인 fixture에서 다시 검증한다.
|
||
- [x] 모든 활성 도메인의 mock handler가 정규화 endpoint·DTO를 사용하고 실제 backend request 0건으로 최종 UI journey를 재현하는지 검증한다.
|
||
- [x] server mode의 404·network error가 mock으로 전환되지 않고 실제 오류 UI와 연동 대기 기록으로 남는지 검증한다.
|
||
|
||
### Task 9.2 반응형·접근성·보안 회귀
|
||
|
||
**Goal 실행 `P9-T2`:** 지원 viewport/browser의 capability, 접근성과 민감정보 비기록을 mock/server 양 mode에서 검증한다.
|
||
|
||
- **시작 조건:** `P9-T1` 완료.
|
||
- **완료 증거:** 체크박스 전체, table-driven E2E·axe·수동 browser/keyboard/zoom/security·UX 검색 기록.
|
||
- **범위 밖:** 다크 모드 구현과 외부 WCAG 인증.
|
||
|
||
- [x] PRD `9` 기능 matrix를 table-driven E2E data로 검증한다.
|
||
- [x] 320, 640, 768, 1024, 1280px와 landscape에서 overflow·가려진 keyboard·44px target을 확인한다.
|
||
- [x] 로컬 자동 Gate의 Chromium/mobile Chrome matrix를 확인한다. WebKit·Mobile Safari와 실제 Edge/Safari 기기 QA는 2026-07-31 제품 결정으로 지원 범위에서 제외한다.
|
||
- [x] 모든 핵심 route에서 axe critical·serious 위반 0건을 확인한다.
|
||
- [x] keyboard-only, first-error-focus, dialog focus 복귀, skip link, live region, reduced motion, 200% zoom을 수동 검증한다.
|
||
- [x] system dark mode에서도 밝은 token을 유지하고 theme toggle이 없음을 검증한다.
|
||
- [x] JWT, password, signed URL, file body가 log·storage·분석 event에 남지 않는지 검증한다.
|
||
- [x] 각 Page가 승인된 component map대로 route/query/permission과 component 조합만 담당하고, domain 상호작용이 feature/shared component test로 분리됐는지 review한다.
|
||
- [x] PRD `10.9`의 UX 검증 검색을 다시 실행하고 채택·제외 결과를 기록한다.
|
||
- [x] mock mode banner가 모든 viewport에서 핵심 UI를 가리지 않고 실제 server와 혼동되지 않는지 확인한다.
|
||
|
||
### Task 9.3 문서·품질 Gate
|
||
|
||
**Goal 실행 `P9-T3`:** 요구사항 추적, 외부 의존·Open Question, mock/server 상태, README와 검증 기록을 실제 구현에 맞춘다.
|
||
|
||
- **시작 조건:** `P9-T1`, `P9-T2` 완료.
|
||
- **완료 증거:** 체크박스 전체, PRD 수용 기준별 증거 연결, 독립 code review 수정과 관련 Gate 재실행 기록.
|
||
- **범위 밖:** 결정되지 않은 계약을 문서상 확정하는 행위.
|
||
|
||
- [x] 활성 범위의 P0 외부 의존이 0건인지, 아니면 구현 전에 명시적으로 후속/제외 결정됐는지 확인한다.
|
||
- [x] OQ-009의 확정 절차에 따라 각 초기 UI의 최대 길이·배열 개수 권고안, backend 호환 결과와 실제 값 또는 “상한 추가 없음”을 기록하고 중복 checklist를 남기지 않는다.
|
||
- [x] `OQ-010` 감사 로그 UI가 현재 릴리스 non-goal임을 결정 기록과 맞춘다.
|
||
- [x] 실제 구현과 다른 제품 결정은 PRD 결정 기록 → plan 순으로 갱신한다. API 사실이 달라졌다면 backend가 제공한 새 OpenAPI 계약을 먼저 반영한 뒤 두 문서를 맞춘다.
|
||
- [x] PRD 수용 기준마다 자동 test 또는 수동 검증 증거를 연결한다.
|
||
- [x] README에 install, env, run, test, build, 지원 브라우저, 알려진 backend 제약을 기록한다.
|
||
- [x] plan 하단 검증 기록에 무엇을/왜/어떻게와 실제 명령·성공/실패/불가 사유를 누적한다.
|
||
- [x] 별도 code review를 받고 지적사항 수정 후 관련 문서 검증을 다시 실행한다. 전체 Gate는 `P9-GATE`에서 별도로 실행한다.
|
||
- [x] 각 도메인의 `UI 확인 완료(mock)`와 `실제 서버 연동 완료(server)`가 Progress·추적표·알려진 제약에서 일치하는지 확인한다.
|
||
|
||
### Phase 9 Gate
|
||
|
||
**Goal 실행 `P9-GATE`:** 활성 릴리스 전체의 install·type·lint·unit·mock/server E2E·build·금지 패턴을 최종 판정한다.
|
||
|
||
- **시작 조건:** `P9-T1`~`P9-T3` 완료.
|
||
- **완료 증거:** 아래 명령이 0 failure/0 error이고 Expected 및 수동 검증 결과가 최종 검증 기록에 누적됨.
|
||
- **범위 밖:** Gate 통과를 위한 test 삭제·완화, production placeholder, 계약 추정.
|
||
|
||
```bash
|
||
set -e
|
||
|
||
assert_no_match() {
|
||
if rg -n "$@"; then
|
||
return 1
|
||
else
|
||
rg_status=$?
|
||
[ "$rg_status" -eq 1 ]
|
||
fi
|
||
}
|
||
|
||
npm ci
|
||
npm run typecheck
|
||
npm run lint
|
||
npm run test:run
|
||
npm run e2e:mock
|
||
npm run e2e
|
||
npm run build
|
||
assert_no_match 'TODO|TBD|FIXME' src tests
|
||
assert_no_match "externalCharacterId|SUNDAY|MONDAY|TUESDAY|WEDNESDAY|THURSDAY|FRIDAY|SATURDAY|state.?[:=].?['\\\"]OPEN" src \
|
||
--glob '!**/*.test.*' --glob '!**/*.spec.*' --glob '!**/__tests__/**'
|
||
```
|
||
|
||
**Expected:** 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를 대조한다.
|
||
|
||
### Task R9.1 — inactive Character 하위 mutation 일괄 차단
|
||
|
||
**Goal 실행 `P9-R1`:** `isActive=false` Character workspace에서 조회만 유지하고 모든 하위 mutation 진입점과 직접 route를 차단한다.
|
||
|
||
- **연결 리뷰:** [Phase 9 재리뷰](./reviews/review-phase-9-20260729.md) — `REV-P9-001`
|
||
- **시작 조건:** `CHAR-007`, PRD `7.2`; backend ownership/active 검증을 대체하지 않는다.
|
||
- **Files:** Modify: `CharacterEditPage.tsx`, Audio list/detail/form Page, Series list/detail/form/order Page와 capability hook, `CommunityPostSheet.tsx`, `CommentThread.tsx` 또는 소비 props, 관련 feature test, `tests/e2e/resource-workflows.spec.ts`, domain E2E.
|
||
- **Interfaces:** `canMutate = character.isActive && viewportCapability`; false일 때 network mutation 0건, read-only list/detail/player/comment 조회는 유지.
|
||
- **범위 밖:** backend active-only 구현, 비활성 Character 복원, 모바일 capability 확대.
|
||
|
||
- [x] **RED:** inactive Character의 Character edit, Audio create/edit/comment, Series create/edit/link/order, Community comment mutation이 현재 노출/호출되는 E2E를 실패시킨다.
|
||
- [x] **GREEN:** 각 Page가 Character 응답으로 mutation capability를 계산해 form/action/direct route를 guidance/read-only 상태로 교체한다.
|
||
- [x] **REFACTOR:** viewport와 active 조건의 이름을 분리하고 boolean prop을 필요한 feature component까지만 전달한다.
|
||
- **실행 명령:** `npm run test:run -- src/features/characters src/features/audio-contents src/features/series src/features/community-posts src/features/comments`; `npm run e2e:mock -- tests/e2e/resource-workflows.spec.ts tests/e2e/character-workspace.spec.ts tests/e2e/audio-content.spec.ts tests/e2e/series.spec.ts tests/e2e/comments.spec.ts`.
|
||
- **기대 결과:** exit 0, inactive workspace mutation request 0건, 조회 request와 read-only banner 유지.
|
||
- **수동 확인:** 320/768/1280px 직접 URL 포함 모든 도메인의 read-only action inventory를 확인한다.
|
||
|
||
### Task R9.2 — field error 연결과 첫 오류 focus
|
||
|
||
**Goal 실행 `P9-R2`:** 모든 주요 form에서 오류 문구를 입력과 programmatic하게 연결하고 submit 뒤 첫 invalid control에 focus한다.
|
||
|
||
- **연결 리뷰:** [Phase 9 재리뷰](./reviews/review-phase-9-20260729.md) — `REV-P9-002`
|
||
- **시작 조건:** PRD `10.5`, `10.7`; 기존 visible label과 live alert를 유지한다.
|
||
- **Files:** Modify: Character/Audio/Series/Community/FanTalk/Comment form components와 tests, 필요 시 `src/shared/ui`의 작은 error-id/focus helper, `tests/e2e/accessibility.spec.ts`.
|
||
- **Interfaces:** field별 stable error ID, `aria-describedby` 또는 `aria-errormessage`, `aria-invalid=true`, DOM 순서 첫 invalid focus.
|
||
- **범위 밖:** form library 전환, 서버 `errorProperty` 추정 mapping.
|
||
|
||
- [x] **RED:** 각 도메인 대표 필드의 accessible description과 multi-error 첫 focus 실패 test를 작성한다.
|
||
- [x] **GREEN:** 현재 markup에 ID/ref를 추가하고 submit validation 뒤 첫 error focus를 적용한다.
|
||
- [x] **REFACTOR:** 실제 2개 이상 소비되는 최소 helper만 shared로 올리고 axe/keyboard 회귀를 실행한다.
|
||
- **실행 명령:** `npm run test:run -- src/features`; `npm run e2e:mock -- tests/e2e/accessibility.spec.ts`.
|
||
- **기대 결과:** exit 0, invalid field 연결 누락 0건, 첫 error focus 일치, axe critical·serious 0건.
|
||
- **수동 확인:** screen reader/keyboard로 Character·Audio·Series·Community 대표 invalid submit을 확인한다.
|
||
|
||
### Task R9.3 — Phase 9 과거 리뷰 상태 종결
|
||
|
||
**Goal 실행 `P9-R3`:** 완료된 `P9-R1~R2`를 기존 Phase 9 리뷰의 발견 상태·최종 결론·남은 항목에 누적 반영한다.
|
||
|
||
- **연결 리뷰:** [Phase 9 현재 리뷰](./reviews/phase9-cross-cutting-quality.md) — `REV-P9-003`
|
||
- **시작 조건:** `plan-task.md`의 2026-07-30 `P9-R1`, `P9-R2`, `P9-GATE` 완료 증거.
|
||
- **완료 증거:** 과거 리뷰를 삭제·이름 변경하지 않고 판정 기록과 수정 후 검증을 누적하며 stale 현재 상태 검색과 Markdown link/diff 검증 통과.
|
||
- **Files:** Modify: `docs/20260725_AI캐릭터관리자웹/reviews/review-phase-9-20260729.md`; Test: 문서 검색.
|
||
- **Interfaces:** `REV-P9-001~002` 상태는 `수정 완료`, 최종 결론은 수정 검증 완료, 남은 실제 server QA는 P10 수동 QA로 구분한다.
|
||
- **범위 밖:** 2026-07-29 당시 실행 수치 삭제·재작성, 애플리케이션 코드 변경.
|
||
|
||
- [x] **TDD 예외:** 문서 상태 Task이므로 `확정`, `P9-R1`, `P9-R2`, `수정 goal 필요`의 stale 현재 문구 검색을 RED 대체 증거로 기록한다.
|
||
- [x] 완료 증거를 기존 판정 기록과 §9에 날짜순으로 누적하고 과거 실패 이력은 보존한다.
|
||
- [x] 새 Phase 9 리뷰와 plan link, stale 현재 상태 0건, `git diff --check`를 검증한다.
|
||
- **실행 명령:** `rg -n 'REV-P9-00[12]|P9-R[12]|수정 goal 필요' docs/20260725_AI캐릭터관리자웹/reviews/review-phase-9-20260729.md`; `git diff --check -- docs/20260725_AI캐릭터관리자웹`.
|
||
- **기대 결과:** 과거 사실 보존, 현재 finding 2건 `수정 완료`, 남은 자동 Gate 오류 0건.
|
||
- **수동 확인:** 2026-07-29 판정과 2026-07-30 수정 검증이 시간순으로 구분되는지 확인한다.
|
||
|
||
**Progress — 2026-07-30 P9-R3 완료**
|
||
|
||
- **RED 대체:** `rg -n 'REV-P9-00[12]|P9-R[12]|수정 goal 필요|확정' docs/20260725_AI캐릭터관리자웹/reviews/phase9-cross-cutting-quality.md`가 현재 리뷰의 `REV-P9-003` 확정 상태와 과거 리뷰 미해결 설명을 검출했다.
|
||
- **GREEN:** `review-phase-9-20260729.md`에 `REV-P9-001~002` 수정 완료 상태, `P9-R1~R2`와 `P9-GATE` 검증 기록, 최신 최종 결론을 누적했다. `phase9-cross-cutting-quality.md`도 `REV-P9-003` 수정 완료로 정리했다.
|
||
- **REFACTOR:** 과거 2026-07-29 실패·timeout 이력은 삭제하지 않고, 현재 상태 문구만 최신 검증 기준으로 분리했다.
|
||
|
||
### Task R9.4 — 일반 mutation pending·실패 복구 경계 보강
|
||
|
||
**Goal 실행 `P9-R4`:** 공통 비활성화와 Series 연결·해제·순서 저장이 빠른 반복 입력에도 요청을 한 번만 보내고 실패를 화면에서 복구 가능하게 한다.
|
||
|
||
- **연결 리뷰:** [Phase 9 현재 리뷰](./reviews/phase9-cross-cutting-quality.md) — `REV-P9-004`
|
||
- **시작 조건:** PRD §13의 파일 업로드 외 일반 mutation 중복 제출 차단과 공통 오류·재시도 기준.
|
||
- **완료 증거:** pending Promise 기반 빠른 이중 입력·reject 실패 test, 최소 pending guard와 오류 UI, 관련 Phase focused test·mock E2E·정적 Gate 통과.
|
||
- **Files:** Modify: `src/shared/ui/confirm-deactivate-dialog.tsx`, `src/features/characters/pages/CharacterDetailPage.tsx`, `src/features/audio-contents/components/AudioContentForm.tsx`, `src/features/series/components/SeriesForm.tsx`, `src/features/series/components/SeriesContents.tsx`, `src/features/series/pages/SeriesOrderPage.tsx`, 관련 tests와 필요 시 `tests/e2e/{character-workspace,audio-content,series}.spec.ts`.
|
||
- **Interfaces:** pending 동안 같은 mutation은 동기 guard로 1회만 실행하고 관련 controls는 disabled/`aria-busy` 상태를 제공한다. reject 시 dialog/page에 `role=alert` 오류를 표시하고 재시도를 허용한다.
|
||
- **범위 밖:** 범용 mutation state library, upload progress(`P4-R2`), backend idempotency key, 모든 read request debounce.
|
||
|
||
- [x] **RED:** Character/Audio/Series 비활성화와 Series 연결·해제·순서 저장의 pending Promise에서 빠른 이중 click을 보내 request 1회를 기대하는 실패 test, deactivate reject의 visible alert test를 작성한다.
|
||
- [x] **GREEN:** 각 mutation 경계에 synchronous guard와 pending state를 추가하고 공통 confirm dialog가 pending/오류 상태를 접근 가능하게 표시하도록 확장한다.
|
||
- [x] **REFACTOR:** 비활성화 dialog의 공통 UI만 공유하고 서로 다른 도메인 mutation을 새 범용 hook으로 추상화하지 않는다.
|
||
- **실행 명령:** `npm run test:run -- src/shared/ui src/features/characters src/features/audio-contents src/features/series`; `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts tests/e2e/audio-content.spec.ts tests/e2e/series.spec.ts --project=chromium`; `npm run typecheck`; `npm run lint`; `npm run build`.
|
||
- **기대 결과:** 모든 명령 exit 0, 각 대상 mutation의 pending 중 요청 최대 1회, 실패 시 unhandled rejection 0건과 visible recovery 안내.
|
||
- **수동 확인:** 느린 network에서 confirm/link/unlink/order 버튼 연타, disabled 상태, 실패 후 재시도를 keyboard-only로 확인한다.
|
||
|
||
#### 실행 기록 — 2026-07-30
|
||
|
||
- **RED:** `npm run test:run -- src/shared/ui/__tests__/confirm-deactivate-dialog.test.tsx src/features/series/tests/series-order.test.tsx src/features/series/tests/series-contents.test.tsx`는 공통 dialog pending/error UI와 Series 연결·해제·순서 저장 pending guard 부재로 실패했고, `npm run test:run -- src/features/characters/tests/CharacterDetailPage.test.tsx src/features/audio-contents/tests/audio-form-update.test.tsx src/features/series/tests/series-form.test.tsx`는 Character/Audio/Series deactivate pending label과 visible failure alert 부재로 실패했다.
|
||
- **GREEN:** `ConfirmDeactivateDialog`에 `isPending`, `errorMessage`, disabled controls, `aria-busy`, `role=alert`를 추가했다. Character/Audio/Series deactivate, Series 연결·해제, Series 순서 저장 경계에 synchronous guard와 pending state를 추가해 pending 중 request 1회와 실패 후 재시도를 보장했다.
|
||
- **REFACTOR:** 도메인별 mutation은 각 소유 컴포넌트에 그대로 두고, 공통화는 deactivate dialog 표시 상태에만 제한했다. 새 범용 mutation hook이나 idempotency key는 만들지 않았다.
|
||
- **검증:** `npm run test:run -- src/features/characters/tests/CharacterDetailPage.test.tsx src/features/audio-contents/tests/audio-form-update.test.tsx src/features/series/tests/series-form.test.tsx src/shared/ui/__tests__/confirm-deactivate-dialog.test.tsx src/features/series/tests/series-order.test.tsx src/features/series/tests/series-contents.test.tsx` 결과 6 files / 28 tests passed. 변경 test 파일 LSP diagnostics는 모두 clean이었다. E2E는 사용자 지시에 따라 `P10-R5` 이후 최종 실행으로 보류했다.
|
||
|
||
### Task R9.5 — Series mutation pending 표시 회귀 복구
|
||
|
||
**Goal 실행 `P9-R5`:** Series 연결·해제·순서 저장의 동기 중복 guard와 별개로 운영자가 인지할 수 있는 pending 상태와 관련 control 비활성화를 복구한다.
|
||
|
||
- **연결 리뷰:** [Phase 9 현재 리뷰](./reviews/phase9-cross-cutting-quality.md) — `REV-P9-005`
|
||
- **시작 조건:** PRD §10.5의 저장 중 button 비활성화·진행 표시, 300ms feedback와 완료 기록 `P9-R4`의 `disabled`/`aria-busy` interface.
|
||
- **완료 증거:** pending Promise 동안 visible 상태와 관련 control 비활성화를 기대하는 실패 test, ref guard를 보존한 reactive state 최소 수정, Series focused unit·mock E2E·정적 Gate 통과.
|
||
- **Files:** Modify: `src/features/series/components/SeriesContents.tsx`, `src/features/series/components/SeriesOrderList.tsx`, `src/features/series/pages/SeriesOrderPage.tsx`, 관련 Series tests와 `tests/e2e/series.spec.ts`.
|
||
- **Interfaces:** `isLinkingRef`/`isUnlinkingRef`/`isSavingRef`의 동기 1회 guard는 유지하고 React pending state로 link 후보·unlink dialog·order move/save control을 비활성화하며 `aria-busy` 또는 visible 진행 문구를 제공한다. reject 뒤 재시도를 허용한다.
|
||
- **범위 밖:** 범용 mutation hook, backend idempotency key, Series API payload 변경.
|
||
|
||
- [x] **RED:** link/unlink/order pending Promise 중 button 연타 request 1회뿐 아니라 각 관련 control의 disabled·진행 접근성 상태를 기대하는 실패 test를 작성한다.
|
||
- [x] **GREEN:** 세 mutation에 reactive pending state를 추가하고 `finally`에서 해제하며 dialog/order list에 필요한 prop만 전달한다.
|
||
- [x] **REFACTOR:** 동기 guard와 화면 state의 서로 다른 책임을 이름으로 구분하고 공통 hook을 만들지 않는다.
|
||
- **실행 명령:** `npm run test:run -- src/features/series/tests/series-contents.test.tsx src/features/series/tests/series-order.test.tsx`; `npm run e2e:mock -- tests/e2e/series.spec.ts --project=chromium`; `npm run typecheck`; `npm run lint`; `npm run build`.
|
||
- **기대 결과:** 모든 명령 exit 0, pending 중 관련 mutation request 최대 1회와 control 조작 0회, 진행 상태 노출, 실패 뒤 재시도 성공.
|
||
- **수동 확인:** 느린 network에서 link/unlink/order save를 연타하고 disabled·진행 문구·오류 후 재시도를 keyboard-only로 확인한다.
|
||
|
||
**P9-R5 수정 검증 기록 (2026-07-31):**
|
||
|
||
- RED: `npm run test:run -- src/features/series/tests/series-contents.test.tsx src/features/series/tests/series-order.test.tsx`는 pending 중 link/save 버튼 disabled 기대에서 실패해 visible pending 상태 부재를 재현했다.
|
||
- GREEN/REFACTOR: `SeriesContents`에 `isLinking`/`isUnlinking`, `SeriesOrderPage`에 `isSaving` state를 추가하고, 기존 ref guard는 유지했다. `SeriesOrderList`에는 `isDisabled` prop만 전달해 순서 이동 control을 저장 중 비활성화했다. 새 범용 hook은 만들지 않았다.
|
||
- 검증: focused 2 files / 9 tests passed, `npm run test:run -- src/features/series`는 8 files / 37 tests passed. `npm run typecheck`, `npm run lint`, `npm run build`는 모두 exit 0이었다. `src/features/series` LSP diagnostics는 17 files, 0 diagnostics였다. E2E는 사용자 지시에 따라 전체 Task 완료 후 필요 시 실행한다.
|
||
|
||
### Task R9.6 — 일반 mutation 재진입·진행 피드백 경계 종결
|
||
|
||
**Goal 실행 `P9-R6`:** Series form, Community, FanTalk, Comments의 일반 mutation이 같은 화면에서 pending 중 재진입하지 않고 운영자와 보조기기에 진행·실패 상태를 전달하도록 한다.
|
||
|
||
- **연결 리뷰:** [Phase 9 현재 리뷰](./reviews/phase9-cross-cutting-quality.md) — `REV-P9-006`
|
||
- **시작 조건:** PRD §10.5의 저장 중 button 비활성화·진행 표시와 300ms 피드백, §13의 파일 upload 외 일반 mutation 중복 제출 차단.
|
||
- **완료 증거:** pending Promise에 같은 submit/click을 연속 전달해 request 1회와 accessible progress를 기대하는 실패 test, FanTalk 미답변 원글 DELETE reject의 visible alert test, 관련 Phase focused unit·mock Chromium E2E·정적 Gate 통과.
|
||
- **Files:** Modify: `src/features/series/components/SeriesForm.tsx`, `src/features/community-posts/components/{CommunityPostForm,CommunityPostSheet}.tsx`, `src/features/fan-talks/components/{FanTalkReplyForm,FanTalkReplySheet}.tsx`, `src/features/comments/components/{CommentForm,CommentItem,CommentThread}.tsx`, 관련 tests와 필요 시 도메인 E2E.
|
||
- **Interfaces:** 각 mutation 경계는 state 갱신 전에도 동작하는 동기 guard로 요청을 1회만 허용한다. pending 동안 관련 control을 비활성화하고 `role=status`, `aria-busy`, 또는 처리 중 label 중 하나로 진행을 노출한다. FanTalk DELETE 실패는 답변 유무와 무관하게 visible alert와 재시도를 제공한다.
|
||
- **범위 밖:** 범용 mutation hook/state library, backend idempotency key, read request debounce, upload progress, API payload 변경.
|
||
- **Phase 9 Gate 연결:** 기존 Gate·완료 기록은 보존하되 `P9-R6` 대상 Phase 5~8 focused test, mock Chromium Series·Community·FanTalk·Comments E2E, `typecheck`·`lint`·`build` 통과 전에는 이번 교차 회귀를 종결하지 않는다.
|
||
- **검증 기록:** 도메인별 RED/GREEN test 수, 중복 request count, 실패 후 재시도, E2E·정적 Gate 결과를 이 Task 하단과 Phase 9 리뷰 §9 이후에 날짜순으로 누적한다.
|
||
|
||
- [x] **RED:** Series create/update, Community create/update/fixed, Comments create/update/delete를 pending으로 두고 빠른 재입력 request 1회와 진행 상태를 기대하는 실패 test를 작성한다. FanTalk 미답변 원글 DELETE reject가 visible alert와 재시도를 제공하는 실패 test도 추가한다.
|
||
- [x] **GREEN:** 대상 handler에 최소 동기 guard를 추가하고 기존 `isSaving`을 disabled·accessible progress에 연결하며 FanTalk confirm dialog에 pending/error 상태를 전달한다.
|
||
- [x] **REFACTOR:** 공통화는 기존 `ConfirmDeactivateDialog` interface 재사용까지만 허용하고 서로 다른 mutation을 새 hook으로 통합하지 않는다.
|
||
- **실행 명령:** `npm run test:run -- src/features/series src/features/community-posts src/features/fan-talks src/features/comments`; `npm run e2e:mock -- tests/e2e/series.spec.ts tests/e2e/community.spec.ts tests/e2e/fan-talk.spec.ts tests/e2e/comments.spec.ts --project=chromium`; `npm run typecheck`; `npm run lint`; `npm run build`; `git diff --check`.
|
||
- **기대 결과:** 모든 명령 exit 0, 대상별 pending 중 mutation request 최대 1회, 진행 상태 노출, reject 후 visible 오류와 재시도 성공.
|
||
- **수동 확인:** 느린 network에서 keyboard submit·빠른 click을 반복하고 disabled·focus·상태 안내·실패 후 재시도를 각 화면에서 확인한다.
|
||
|
||
**P9-R6 완료 기록 (2026-07-31):**
|
||
|
||
- 무엇을: Series form, Community create/Sheet save/fixed, Comments mutation, FanTalk 미답변 원글 DELETE에 pending 중 동기 재진입 guard와 접근 가능한 진행/실패 피드백을 추가했다.
|
||
- 왜: `REV-P9-006`에서 같은 화면의 일반 mutation이 React state 반영 전 재진입하거나 실패 상태를 보조기기와 운영자에게 전달하지 못하는 경계가 확인됐기 때문이다.
|
||
- RED: `npm run test:run -- src/features/series/tests/series-form-pending.test.tsx src/features/community-posts/tests/community-mutation-pending.test.tsx src/features/comments/tests/comment-thread-pending.test.tsx src/features/fan-talks/tests/fan-talk-delete-pending.test.tsx`는 구현 전 4 files / 5 tests가 기대대로 실패했다. 실패 원인은 pending `role=status` 부재와 FanTalk DELETE 실패 alert/재시도 미노출이었다.
|
||
- GREEN: 같은 focused 명령은 구현 후 4 files / 5 tests passed였다. `npm run test:run -- src/features/community-posts/tests/community-sheet.test.tsx src/features/community-posts/tests/community-mutation-pending.test.tsx`는 2 files / 9 tests passed, `npm run test:run -- src/features/series src/features/comments`는 12 files / 50 tests passed, `npm run test:run -- src/features/fan-talks`는 4 files / 14 tests passed였다.
|
||
- REFACTOR: 새 범용 mutation hook/state library는 만들지 않고 각 도메인 handler의 `useRef` guard와 기존 `isSaving` 상태만 연결했다. 공통화는 이미 존재하는 `ConfirmDeactivateDialog`의 `isPending`/`errorMessage` prop 재사용으로 제한했다.
|
||
- 회귀 확인: `npm run test:run -- src/features/series src/features/community-posts src/features/comments src/features/fan-talks`는 120초 timeout 중 `community-sheet.test.tsx` 목록 로딩 실패를 1건 표시했다. 이후 실패 spec 단독, pending 인접 spec, FanTalk 단독, Series+Comments 단독 실행은 모두 통과해 넓은 병렬 실행의 비결정 로딩 실패로 분리 기록한다. `npm run e2e:mock -- tests/e2e/series.spec.ts tests/e2e/community.spec.ts tests/e2e/fan-talk.spec.ts tests/e2e/comments.spec.ts --project=chromium`은 24 tests passed였다. `npm run typecheck`, `npm run lint`, `npm run build`, 관련 파일 `git diff --check`는 모두 exit 0이었다.
|
||
|
||
### Task R9.7 — Safari/WebKit 지원 Gate 복원 (제품 결정으로 대체됨)
|
||
|
||
**Goal 실행 `P9-R7`:** PRD와 완료된 `P9-T2`가 요구하는 desktop WebKit·mobile Safari Playwright project를 복원하고 네 browser project의 mock/server Gate를 다시 통과시킨다.
|
||
|
||
- **연결 리뷰:** [Phase 9 현재 리뷰](./reviews/phase9-cross-cutting-quality.md) — `REV-P9-007`
|
||
- **시작 조건:** 과거 PRD §13과 `P9-T2`가 요구했던 Safari/WebKit Gate 복원 이력. 2026-07-31 제품 결정 이후 현재 지원 범위는 Chromium/mobile Chrome이다.
|
||
- **완료 증거:** WebKit project 부재 실패, 두 Safari 계열 project 복원, 기존 skip은 platform capability 근거가 있는 keyboard-only case로 한정, exact full mock/server E2E와 browser support 문서 정합성 검증 기록.
|
||
- **Files:** Modify: `playwright.config.ts`, `README.md`, `tests/e2e/character-workspace.spec.ts`; Test: `tests/e2e/smoke.spec.ts`, `tests/e2e/auth.spec.ts`, `tests/e2e/accessibility-shell.spec.ts`, `tests/e2e/mock-preview-shell.spec.ts`, `tests/e2e/mock-mode-boundary.spec.ts`, `tests/e2e/audio-content.spec.ts`, `tests/e2e/series.spec.ts`, `tests/e2e/community.spec.ts`, `tests/e2e/fan-talk.spec.ts`, `tests/e2e/comments.spec.ts`, `tests/e2e/resource-workflows.spec.ts`, `tests/e2e/error-mapping.spec.ts`, `tests/e2e/responsive-capabilities.spec.ts`, `tests/e2e/accessibility.spec.ts`.
|
||
- **Interfaces:** project 이름은 기존 기록과 같은 `webkit`, `mobile-safari`를 사용한다. Safari 범위를 줄이려면 이 Task에서 임의 변경하지 않고 별도 제품 결정으로 PRD부터 갱신한다.
|
||
- **범위 밖:** Edge engine 자동화 추가, 실제 기기·최신 2개 major 전체 자동화, 테스트 실패를 숨기기 위한 project 제거나 근거 없는 skip, 제품 기능 확장.
|
||
- **Phase 9 Gate 연결:** 네 project의 exact `e2e:mock`, server allowlist, browser support 정적 대조가 모두 통과하기 전에는 Phase 9 교차 품질 회귀를 종결하지 않는다.
|
||
|
||
- [x] **RED:** `npx playwright test --list --project=webkit`의 project-not-found를 기록하고 현재 README와 PRD browser matrix 불일치를 고정하는 정적 assertion을 추가한다.
|
||
- [x] **GREEN:** Desktop Safari와 iPhone Safari device project를 기존 이름으로 복원하고 검증된 app/test root cause만 최소 수정한다.
|
||
- [x] **REFACTOR:** browser별 skip을 keyboard focus 같은 문서화된 platform capability로만 제한하고 dirty-leave·mutation·contract 흐름은 네 project에서 유지한다.
|
||
- **실행 명령:** 과거 복원 당시 `npx playwright install webkit`; `npx playwright test --list --project=webkit`; `npx playwright test --list --project=mobile-safari`; `npm run e2e:mock`; `npm run e2e`; `npm run typecheck`; `npm run lint`; `npm run build`를 사용했다. 현재 검증은 Chromium/mobile Chrome만 사용한다.
|
||
- **기대 결과:** 현재 기준으로 `chromium`·`mobile-chrome`만 수집·실행되고 PRD·README·Gate matrix 불일치 0건.
|
||
- **수동 확인:** Safari/WebKit 수동 확인은 현재 지원 범위에서 제외한다.
|
||
|
||
**P9-R7 수정 검증 기록 (2026-07-31):**
|
||
|
||
> 2026-07-31 제품 결정: 이 복원 기록은 과거 이력으로만 보존한다. 현재 지원·자동 검증 범위는 Chromium/mobile Chrome이며 WebKit·Mobile Safari는 지원 범위에서 제외한다.
|
||
|
||
- 무엇을: `webkit`과 `mobile-safari` Playwright project를 복원하고, README/PRD의 browser support 문구와 mock Gate 문서 정합성을 맞췄다. 긴 WebKit 계열 mock run에서 `/login` navigation이 누적 timeout을 내던 `mock-preview-shell.spec.ts`는 같은 검증을 단일 로그인 세션으로 합쳐 반복 navigation을 제거했다.
|
||
- 왜: 당시 PRD §13과 완료된 `P9-T2`가 Chromium/WebKit/mobile Chrome/mobile Safari 자동 Gate를 요구하지만, 설정에는 Chromium/mobile Chrome만 남아 Phase 9 acceptance 범위가 축소됐기 때문이다. 이 사유는 과거 이력이며 현재 제품 결정은 Chromium/mobile Chrome만 지원한다.
|
||
- 어떻게:
|
||
- RED: `npx playwright test --list --project=webkit`와 `npx playwright test --list --project=mobile-safari`는 각각 `Project(s) "webkit" not found`, `Project(s) "mobile-safari" not found`로 실패했다.
|
||
- GREEN/REFACTOR: `playwright.config.ts`에 `webkit`, `mobile-safari` project를 복원하고 `src/shared/mocks/__tests__/mock-preview-docs.test.ts`에 README/PRD/Playwright project 정합성 assertion을 추가했다. `tests/e2e/mock-preview-shell.spec.ts`는 320px mock shell, no backend fallback, storage, axe 검증을 한 세션에서 수행하도록 정리했다.
|
||
- Focused 검증: `npm run test:run -- src/shared/mocks/__tests__/mock-preview-docs.test.ts`는 1 file / 4 tests passed였다. `npm run e2e:mock -- tests/e2e/mock-preview-shell.spec.ts --project=webkit --workers=1`와 `--project=mobile-safari --workers=1`은 각각 1 passed였다. WebKit/mobile Safari의 resource/series tail focused 재실행도 0 failure였다.
|
||
- Full Gate: 최종 `npm run e2e:mock`은 Chromium 52 passed, WebKit 44 passed / 8 skipped, Mobile Chrome 47 passed / 5 skipped, Mobile Safari 43 passed / 9 skipped로 전체 0 failure였다. skipped 항목은 기존 browser 조건에 맞춘 keyboard-only 또는 Chromium 전용 audio metadata case다. `npm run e2e`는 server allowlist 36 passed였고, `npm run typecheck`, `npm run lint`, `npm run build`는 모두 exit 0이었다.
|
||
- 정적 검증: `package.json` JSON parse 확인은 `package.json ok`였고, 당시 관련 문서·설정의 browser support 문구는 4-project 자동 Gate와 Edge/실기기 릴리스 QA 분리를 유지했다. 현재 제품 결정은 Chromium/mobile Chrome만 지원한다.
|
||
- 남은 항목: 없음. 실제 Edge, desktop Safari, iOS Safari 물리 브라우저/기기 QA는 현재 지원 범위에서 제외한다.
|
||
|
||
### Task R9.8 — Mock E2E 실행 계약·문서·unit Gate 정합성 복구
|
||
|
||
**Goal 실행 `P9-R8`:** `P9-R7`에서 도입한 bare/filtered mock E2E 실행 방식을 공개 문서와 mode contract test에 일치시키고 전체 unit Gate를 복구한다.
|
||
|
||
- **연결 리뷰:** [Phase 9 현재 리뷰](./reviews/phase9-cross-cutting-quality.md) — `REV-P9-008`
|
||
- **시작 조건:** `P9-R7` 완료 기록, 현재 bare `npm run e2e:mock`의 Chromium/mobile Chrome 분할 실행과 인자 전달 시 focused Playwright 실행 계약.
|
||
- **완료 증거:** 현재 focused 2-test 실패 재현, public script semantics·README·agent guide·contract test 최소 정렬, 전체 unit 및 `P9-GATE` 재검증 기록.
|
||
- **Files:** Modify: `package.json`(실행 계약을 단순화할 때만), `README.md`, `docs/agent-guide/scripts.md`, `src/shared/mocks/__tests__/mode-boundary.test.ts`, `src/shared/mocks/__tests__/mock-preview-docs.test.ts`; Test: 기존 두 contract test와 전체 unit/E2E Gate.
|
||
- **Interfaces:** 인자 없는 `npm run e2e:mock`은 Chromium/mobile Chrome matrix를 실행한다. file/project 인자가 있으면 Playwright에 그대로 전달해 `testMatch`와 교집합만 실행한다. `VITE_API_MODE=mock`, server command와 no-auto-fallback 경계는 유지한다.
|
||
- **범위 밖:** 제품 코드 변경, 문서에 내부 shell 문자열 전체를 중복해 유지하는 새 결합.
|
||
- **Phase 9 Gate 연결:** focused contract test, 전체 unit, mock/server E2E, typecheck·lint·build가 모두 통과하기 전에는 완료하지 않는다.
|
||
|
||
- [x] **RED:** `mode-boundary.test.ts`의 direct script 기대와 `mock-preview-docs.test.ts`의 README actual-script 기대가 현재 `e2e:mock` wrapper와 각각 실패하는 것을 기록했다.
|
||
- [x] **GREEN:** bare/full과 filtered/focused의 공개 동작을 한 기준으로 문서화하고 contract test가 내부 구현 문자열이 아니라 해당 동작과 mock mode 경계를 검증하게 했다.
|
||
- [x] **REFACTOR:** 기존 분할 script의 이름과 책임만 유지하고 새 runner·dependency를 추가하지 않았으며, 중복 명령 문자열을 test/문서에 복제하지 않았다.
|
||
- [x] focused·전체 unit과 정적 Gate를 재실행하고 RED/GREEN/REFACTOR 및 실제 pass 수를 Progress에 누적했다. E2E full matrix는 직전 요청 기준 재리뷰의 pass 기록과 filtered 인자 전달 surface를 재사용하고, 사용자 지시에 따라 개발 중 반복 실행하지 않았다.
|
||
- **실행 명령:** `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts`; `npm run test:run`; `npm run e2e:mock -- --list --project=chromium`; `npm run e2e:mock`; `npm run e2e`; `npm run typecheck`; `npm run lint`; `npm run build`; `git diff --check`.
|
||
- **기대 결과:** 모든 명령 exit 0, unit 0 failure, Chromium/mobile Chrome mock matrix 0 failure, server allowlist 0 failure, README·agent guide와 실제 bare/filtered 동작 불일치 0건.
|
||
- **수동 확인:** README와 `docs/agent-guide/scripts.md`만 읽고 전체 matrix 실행과 focused filter 전달 방법을 구분할 수 있는지 확인한다.
|
||
|
||
**P9-R8 수정 검증 기록 (2026-07-31):**
|
||
|
||
- RED: `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 2 failed / 5 passed. `mode-boundary.test.ts`는 direct `VITE_API_MODE=mock playwright test` 기대와 wrapper 차이로 실패했고, `mock-preview-docs.test.ts`는 README에 내부 wrapper 전체 문자열이 없어서 실패했다.
|
||
- GREEN/REFACTOR: `mode-boundary.test.ts`는 공개 실행 의미(`VITE_API_MODE=mock playwright test "$@"` 인자 전달, 4-project 분할 script, raw direct script)를 검증하게 바꿨다. `mock-preview-docs.test.ts`, `README.md`, `docs/agent-guide/scripts.md`는 `npm run e2e:mock`의 bare full matrix와 filtered `file`/`--project` 전달 의미를 문서화했다. 새 runner·dependency와 Safari 범위 축소는 추가하지 않았다.
|
||
- focused/surface: `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 2 files / 7 tests passed. `npm run e2e:mock -- --list --project=chromium` — 52 tests collected.
|
||
- unit Gate: 첫 `npm run test:run`은 240초 timeout 중 `production-graph.test.ts`와 `audio-list.test.tsx` 진행 실패를 표시했다. `production-graph.test.ts`는 단독 1 passed였고, `audio-list.test.tsx`는 route/data settling 대기 timeout을 명시해 단독 2 passed로 복구했다. 이후 full `npm run test:run`은 `community-sheet.test.tsx` 5초 timeout 1건만 남았고, 단독 7 passed 확인 뒤 해당 integration test timeout만 `10_000`으로 명시했다. 최종 `npm run test:run` — 78 files / 394 tests passed.
|
||
- 정적 검증: `npm run typecheck`, `npm run lint`, `git diff --check -- README.md docs/agent-guide/scripts.md src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts`, `git diff --check -- src/features/community-posts/tests/community-sheet.test.tsx src/features/audio-contents/tests/audio-list.test.tsx`는 모두 exit 0이었다. LSP diagnostics는 변경 test 파일에서 3초 제한 timeout이 발생해 `typecheck`로 보완했다.
|
||
|
||
### Task R9.9 — 전체 unit Gate 비결정적 대기·정리 복구
|
||
|
||
**Goal 실행 `P9-R9`:** 단독 focused 실행은 통과하지만 전체 Vitest 실행에서 임의의 integration spec이 loading 또는 1/5초 대기에 머무는 비결정성을 제거한다.
|
||
|
||
- **연결 리뷰:** [Phase 9 현재 리뷰](./reviews/phase9-cross-cutting-quality.md) — `REV-P9-009`
|
||
- **시작 조건:** 2026-07-31 fresh `npm run test:run` 두 차례에서 Community Sheet·Character workspace가 반복 실패하고 Series route·Character mutation 실패 집합은 변했으며, 각 spec focused 실행은 통과한 증거.
|
||
- **완료 증거:** full-suite failure를 의도적으로 재현하는 순서/대기 증거, 정확한 root cause의 최소 test cleanup·async settling 수정, 관련 spec focused 통과와 전체 `npm run test:run` 2회 연속 0 failure 기록.
|
||
- **Files:** Modify: `src/features/community-posts/tests/community-sheet.test.tsx`, `src/features/series/tests/series-routes.test.tsx`, `src/layouts/CharacterWorkspaceLayout.test.tsx`, `src/features/characters/tests/character-mutation-reload.test.tsx`, `src/features/fan-talks/tests/fan-talk-reply.test.tsx`; root cause가 공통 정리임이 입증된 경우만 Modify: `src/shared/test/setup.ts`, `src/app/app-test-support.ts`.
|
||
- **Interfaces:** integration test는 request/response 준비 완료를 명시적으로 await하고 다음 test로 pending request, timer, browser history listener를 넘기지 않는다. assertion 커버리지와 production timeout은 변경하지 않는다.
|
||
- **범위 밖:** 전역 timeout 일괄 상향, 실패 spec skip·삭제, `fileParallelism` 축소로 가림, 제품 코드 임의 변경. 제품 root cause가 확정되면 이 Task에서 숨기지 않고 소유 Phase 신규 리뷰 Task로 분리한다.
|
||
- **Phase 9 Gate 연결:** 실패 4개 spec focused, 전체 unit 2회 연속, `typecheck`·`lint`·build, mock/server smoke가 모두 통과하기 전에는 완료하지 않는다.
|
||
|
||
- [x] **RED:** full suite 실패 순서와 pending request·timer·listener·React update를 축소한 명령으로 고정하고, focused 단독 통과와 전체 실패 차이를 기록한다.
|
||
- [x] **GREEN:** 입증된 root cause의 await/cleanup/handler 격리만 최소 수정해 축소 재현 명령과 focused spec을 통과시킨다.
|
||
- [x] **REFACTOR:** 수행 시간을 숨기기 위한 전역 timeout·skip이 없는지 확인하고 full suite를 2회 연속 실행한다.
|
||
- [x] RED/GREEN/REFACTOR, focused·full 2회·정적/build 결과를 Progress와 Phase 9 리뷰에 누적한다. 개발 중 E2E smoke는 사용자 지시에 따라 모든 신규 Task 구현 후 필요 시 실행한다.
|
||
- **실행 명령:** `npm run test:run -- src/features/community-posts/tests/community-sheet.test.tsx src/features/series/tests/series-routes.test.tsx src/layouts/CharacterWorkspaceLayout.test.tsx src/features/characters/tests/character-mutation-reload.test.tsx src/features/fan-talks/tests/fan-talk-reply.test.tsx`; `npm run test:run`; `npm run test:run`; `npm run typecheck`; `npm run lint`; `npm run build`; `npm run e2e:mock -- tests/e2e/community.spec.ts tests/e2e/series.spec.ts tests/e2e/character-workspace.spec.ts tests/e2e/fan-talk.spec.ts --project=chromium`; `npm run e2e -- tests/e2e/auth.spec.ts tests/e2e/server-mode-boundary.spec.ts`.
|
||
- **기대 결과:** focused·전체 2회·정적/build·smoke 명령 exit 0, full unit failure 0건, touched spec의 예기치 않은 MSW unhandled request·React `act` warning 0건.
|
||
- **수동 확인:** 없음. 자동 test harness 결정성 복구 Task이며 화면 기능은 관련 mock/server smoke로 대체 확인한다.
|
||
|
||
**P9-R9 수정 검증 기록 (2026-07-31):**
|
||
|
||
- RED: `npm run test:run -- src/features/characters/tests/character-mutation-reload.test.tsx`가 보호 route probe 완료 전 `screen.getByLabelText("이름")`를 호출해 실패했다. 실패 화면은 `보호 route 확인 중` 상태였다.
|
||
- GREEN: create route도 `P1-R14` 이후 보호 probe를 거치므로 첫 form field를 `await screen.findByLabelText("이름")`로 기다리게 했다. 제품 코드는 변경하지 않았다.
|
||
- 검증: focused `npm run test:run -- src/features/characters/tests/character-mutation-reload.test.tsx`는 1 file / 3 tests passed. 전체 `npm run test:run`은 2회 연속 81 files / 409 tests passed였다. 변경 test 파일 LSP diagnostics는 0건이었다.
|
||
- 범위 유지: 전역 timeout 상향, spec skip, file parallelism 축소, 제품 코드 임의 변경은 하지 않았다. E2E smoke는 개발 중 반복 실행하지 말라는 지시에 따라 `P9-R10` 이후 필요 시 최종 Gate에서 실행한다.
|
||
|
||
### Task R9.10 — mock E2E navigation·mutation 대기 결정성 복구
|
||
|
||
**Goal 실행 `P9-R10`:** Character 직접 route navigation과 FanTalk mutation 성공 피드백이 간헐적으로 경합하는 비결정성을 Chromium/mobile Chrome 지원 범위에서 제거한다.
|
||
|
||
- **연결 리뷰:** [Phase 9 현재 리뷰](./reviews/phase9-cross-cutting-quality.md) — `REV-P9-010`
|
||
- **시작 조건:** `P9-R9` 완료, fresh bare `npm run e2e:mock`의 WebKit main 2 failed / 32 passed / 7 skipped과 동일 2-test focused WebKit 2 passed 증거.
|
||
- **완료 증거:** login→direct route과 FanTalk PUT→success/refetch 경합의 재현 순서, 명시적 navigation/mutation settling 최소 수정, focused Chromium·Mobile Chrome E2E와 정적 검증 통과 기록. 2026-07-31 사용자 지시에 따라 WebKit·Mobile Safari E2E는 실행하지 않는다.
|
||
- **Files:** Modify: `tests/e2e/character-workspace.spec.ts`, `tests/e2e/fan-talk.spec.ts`; 동일 login helper 경합이 다른 spec에서도 재현될 때만 Modify: `tests/e2e/accessibility.spec.ts`, `tests/e2e/audio-content.spec.ts`, `tests/e2e/comments.spec.ts`, `tests/e2e/community.spec.ts`, `tests/e2e/resource-workflows.spec.ts`, `tests/e2e/responsive-capabilities.spec.ts`, `tests/e2e/series.spec.ts`.
|
||
- **Interfaces:** login helper는 URL 변경만이 아니라 보호 route probe·목록 ready까지 완료된 뒤 다음 `page.goto`를 허용한다. mutation test는 request, visible success, refetch/navigation 순서를 명시적으로 대기하고 중간 상태를 timeout 우연에 의존하지 않는다.
|
||
- **범위 밖:** 근거 없는 browser project 제거, failing test skip, retry만 추가, 전역 timeout 상향, 화면 기능 축소. 제품 root cause가 재현되면 소유 Phase 신규 리뷰 Task로 분리한다.
|
||
- **Phase 9 Gate 연결:** 두 test의 Chromium·Mobile Chrome focused E2E, typecheck·lint·build가 모두 통과하기 전에는 완료하지 않는다. WebKit·Mobile Safari matrix는 2026-07-31 사용자 지시에 따라 제외한다.
|
||
|
||
- [x] **RED:** 과거 WebKit bare 실패 기록과 현재 focused/repeat 통과를 대조해 login ready·mutation/refetch 대기 경계가 약한 지점을 기록한다.
|
||
- [x] **GREEN:** 보호 route ready와 mutation/refetch 순서를 명시적으로 await하는 최소 test 수정을 적용해 focused Chromium·Mobile Chrome을 통과시킨다.
|
||
- [x] **REFACTOR:** 동일 login helper 중복은 이번 재현 범위에서 공통화하지 않고, test retry·skip·timeout 완화 없이 변경 파일만 검증한다.
|
||
- [x] RED/GREEN/REFACTOR, Chromium·Mobile Chrome focused E2E와 정적/build 결과를 Progress와 Phase 9 리뷰에 누적한다.
|
||
- **실행 명령:** `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts tests/e2e/fan-talk.spec.ts --project=chromium --grep "direct create and edit routes|FanTalk mock journey"`; 같은 명령의 `--project=mobile-chrome`; `npm run typecheck`; `npm run lint`; `npm run build`.
|
||
- **기대 결과:** Chromium/mobile Chrome focused 합계 4 passed, 정적/build exit 0, interrupted navigation·성공 status 미발견 0건.
|
||
- **수동 확인:** 없음. 자동 E2E 대기 결정성 Task이며 모든 화면 기능은 기존 E2E assertion을 보존해 확인한다.
|
||
|
||
**P9-R10 수정 검증 기록 (2026-07-31):**
|
||
|
||
- RED 대체: 기존 bare WebKit 실패는 login 직후 direct `page.goto` interruption과 FanTalk PUT 뒤 성공 status 대기 경합이었다. 현재 focused WebKit 단발 2 tests와 `--repeat-each=5` 10 tests는 모두 통과해 제품 기능 실패가 아니라 E2E 대기 경계 문제로 분리했다.
|
||
- GREEN: 두 spec의 `loginThroughMockMode()`가 `/ai-characters` URL뿐 아니라 `AI 캐릭터` heading까지 기다리게 했다. FanTalk 생성 답변 수정 뒤에는 dialog close 후 list-backed `첫 번째 응원입니다. 답변 보기` 버튼이 보일 때까지 기다리게 했다.
|
||
- 검증: 사용자 지시에 따라 WebKit·Mobile Safari는 추가 실행하지 않았다. `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts tests/e2e/fan-talk.spec.ts --project=chromium --grep "direct create and edit routes|FanTalk mock journey"`는 2 passed, 같은 명령의 `--project=mobile-chrome`도 2 passed였다. 두 변경 spec LSP diagnostics는 0건, pure LOC는 각각 187이었다. `npm run typecheck`, `npm run lint`, targeted `git diff --check`는 모두 통과했다.
|
||
- 범위 유지: retry, skip, timeout 상향, WebKit/Mobile Safari project 제거 또는 제품 코드 변경은 하지 않았다.
|
||
|
||
### Task R9.11 — Chromium-only Gate의 현재 문서 계약 복구
|
||
|
||
**Goal 실행 `P9-R11`:** 과거 네 browser project 실행 이력은 보존하되 PRD Decision Log와 현재 Task·최신 리뷰 결론을 Chromium/mobile Chrome 제품 결정과 실제 smoke spec 경로에 맞춘다.
|
||
|
||
- **연결 리뷰:** [Phase 9 현재 리뷰](./reviews/phase9-cross-cutting-quality.md) — `REV-P9-011`
|
||
- **시작 조건:** PRD §13, plan 상단 범위·`P9-T2`, `playwright.config.ts`, `package.json`, contract test는 Chromium/mobile Chrome만 요구하지만 PRD Decision Log에 변경 결정이 없고 `P9-R8`, `P9-R10`과 Phase 9 리뷰 최신 결론은 4-project/WebKit 명령을 현재 계약처럼 유지하며 `P9-R9` smoke 명령은 존재하지 않는 `server-boundary.spec.ts`를 가리키는 불일치.
|
||
- **완료 증거:** 날짜·사유·영향 범위를 포함한 PRD Decision Log 행, 현재 Task의 WebKit project 명령·4-project 기대 0건, `server-mode-boundary.spec.ts` 실제 경로, 과거 실행 기록 보존, mock 104개·server 18개가 Chromium/mobile Chrome에서만 수집되는 증거와 docs contract test 통과 기록.
|
||
- **Files:** Modify: `docs/20260725_AI캐릭터관리자웹/prd.md`, `docs/20260725_AI캐릭터관리자웹/plan-task.md`, `docs/20260725_AI캐릭터관리자웹/reviews/phase9-cross-cutting-quality.md`, `src/shared/mocks/__tests__/mock-preview-docs.test.ts`.
|
||
- **Interfaces:** `P9-R7~R10`의 과거 RED/GREEN·pass 수는 삭제하지 않고 당시 이력으로 표시한다. 현재 실행 명령·기대 결과·최신 결론만 Chromium/mobile Chrome으로 정정하고 smoke file filter는 실제 `server-mode-boundary.spec.ts`를 사용한다.
|
||
- **범위 밖:** E2E spec·제품 코드 변경, WebKit/mobile Safari 재설치·실행, 과거 검증 기록 삭제.
|
||
|
||
- [x] **TDD 예외 사유:** 실행 코드 변경이 없는 문서 계약 정정이므로 제품 test RED 대신 현재 Task 본문과 executable config의 불일치 검색을 RED 대체 증거로 사용한다.
|
||
- [x] PRD Decision Log에 2026-07-31 Chrome-only 결정의 사유와 Playwright·QA 영향 범위를 추가한다.
|
||
- [x] `P9-R8`, `P9-R10`의 현재 실행 명령·기대 결과와 Phase 9 최신 결론을 Chromium/mobile Chrome 기준으로 정정한다.
|
||
- [x] `P9-R9` server smoke spec 경로를 실제 `server-mode-boundary.spec.ts`로 고치고, 과거 네 project 결과는 당시 이력으로 명시해 현재 Gate나 남은 위험으로 읽히는 표현을 제거한다.
|
||
- [x] docs contract가 현재 Task block의 WebKit/mobile Safari 명령·네 project 기대와 잘못된 smoke spec 경로를 탐지하도록 보강한다.
|
||
- [x] docs contract 결과를 Progress와 Phase 9 리뷰에 누적했다. Playwright 수집 목록·targeted diff는 `P10-R10`까지 완료한 뒤 통합 검증에서 실행한다.
|
||
- **대체 검증 방법:** 현재 Task block의 browser 명령 대조, docs contract unit, Playwright `--list`, Markdown link와 diff 검사.
|
||
- **실행 명령:** `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts`; `npm run e2e:mock -- --list`; `npm run e2e -- --list`; `git diff --check -- docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/reviews/phase9-cross-cutting-quality.md`.
|
||
- **기대 결과:** unit 2 files / 8 tests, mock 104 tests와 server 18 tests가 `chromium`·`mobile-chrome`에서만 수집되고 현재 Task 명령·최신 결론의 WebKit/mobile Safari 요구 0건, link·diff 오류 0건.
|
||
- **수동 확인:** `P9-R7~R11`과 Phase 9 리뷰 마지막 절만 읽어 과거 네 project 이력과 현재 2-project Gate를 혼동하지 않는지 확인한다.
|
||
|
||
**P9-R11 수정 검증 기록 (2026-07-31):**
|
||
|
||
- RED: `npm run test:run -- src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 1 failed / 4 passed. 신규 docs contract가 현재 `P9-R8~R11` Task 정의의 `mock matrix 4 projects`, `--project=webkit`, 잘못된 `server-boundary.spec.ts` 경로를 검출했다.
|
||
- GREEN/REFACTOR: PRD Decision Log에 Chrome-only 자동 Gate 결정을 추가하고, 현재 Task 정의와 Phase 9 최신 결론은 Chromium/mobile Chrome·`server-mode-boundary.spec.ts` 기준으로 정정했다. 과거 네 browser project pass/failure 기록은 검증 기록 이력으로 보존했다.
|
||
- 검증: `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 2 files / 8 tests passed. `npm run e2e:mock -- --list` — Chromium/mobile Chrome 104 tests 수집. `npm run e2e -- --list` — Chromium/mobile Chrome 18 tests 수집. 실제 E2E 실행은 사용자 지시에 따라 생략했다.
|
||
|
||
### Task R9.12 — Chromium-only 문서 계약 종료 증거 보강
|
||
|
||
**Goal 실행 `P9-R12`:** Chrome-only 결정 사유, 현재 Phase 9 문구·리뷰 상태와 docs contract의 정확한 section 범위를 하나의 재현 가능한 종료 증거로 맞춘다.
|
||
|
||
- **연결 리뷰:** [Phase 9 현재 리뷰](./reviews/phase9-cross-cutting-quality.md) — `REV-P9-012`
|
||
- **시작 조건:** PRD Decision Log의 날짜·영향은 있으나 지원 축소 사유가 없고, `P9-R10` 수동 확인의 “자동 WebKit harness”, Phase 9 리뷰 상단의 수정 필요 상태, P9-R11 기대 7 tests 대비 실제 8 tests와 docs contract section 오탐 가능성이 남은 근거.
|
||
- **완료 증거:** PRD 결정의 날짜·사유·영향 범위, 현재 Phase 9 Task·리뷰 metadata의 Chromium/mobile Chrome 기준, `P9-R9` block 내부 실제 smoke path와 정확히 닫힌 최신 결론 section을 검증하는 docs contract 2 files / 8 tests.
|
||
- **Files:** Modify: `docs/20260725_AI캐릭터관리자웹/prd.md`, `docs/20260725_AI캐릭터관리자웹/plan-task.md`, `docs/20260725_AI캐릭터관리자웹/reviews/phase9-cross-cutting-quality.md`, `src/shared/mocks/__tests__/mock-preview-docs.test.ts`.
|
||
- **Interfaces:** 과거 WebKit/Mobile Safari 실행 이력과 finding 근거는 삭제하지 않는다. 현재 Task의 실행·수동 확인, 리뷰 metadata와 contract 대상 section만 정정한다.
|
||
- **범위 밖:** Playwright project·제품/E2E 코드 변경, WebKit/mobile Safari 실행, 과거 이력 재작성.
|
||
- **Phase 9 Gate 연결:** docs contract, Playwright 두 수집 목록, Markdown link·diff 검사가 통과하기 전에는 완료하지 않는다.
|
||
|
||
- [x] **RED:** current section만 검사하도록 contract를 좁혀 PRD 사유 누락, `자동 WebKit harness`, 부정확한 smoke path assertion과 stale review metadata 실패를 확인했다.
|
||
- [x] **GREEN:** PRD Decision Log에 사용자 결정 사유를 추가하고 현재 Task·리뷰 metadata·기대 test 수를 실제 Chromium/mobile Chrome 계약에 맞췄다.
|
||
- [x] **REFACTOR:** `sectionBetween`/current section helper로 `P9-R9` 내부 실제 path와 최신 결론 block만 검사하고 과거 이력 문자열과 plan 전체의 우연한 match에 의존하지 않게 했다.
|
||
- [x] docs contract·Playwright list·link·diff 결과를 Progress와 Phase 9 리뷰에 누적했다.
|
||
- **실행 명령:** `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts`; `npm run e2e:mock -- --list`; `npm run e2e -- --list`; `node -e 'const fs=require("fs"),path=require("path");const root="docs/20260725_AI캐릭터관리자웹";const files=[];const walk=d=>{for(const e of fs.readdirSync(d,{withFileTypes:true})){const p=path.join(d,e.name);e.isDirectory()?walk(p):e.name.endsWith(".md")&&files.push(p)}};walk(root);const broken=[];for(const f of files){const s=fs.readFileSync(f,"utf8");for(const m of s.matchAll(/\[[^\]]*\]\(([^)]+)\)/g)){const t=m[1].trim().replace(/^<|>$/g,"").split("#")[0];if(t&&!/^(https?:|mailto:)/.test(t)&&!fs.existsSync(path.resolve(path.dirname(f),t)))broken.push(f+": "+t)}}if(broken.length){console.error(broken.join("\n"));process.exit(1)}'`; `git diff --check`.
|
||
- **기대 결과:** 2 files / 8 tests, mock 104·server 18 tests가 `chromium`·`mobile-chrome`에서만 수집되고 현재 문서의 WebKit harness·잘못된 smoke path·stale review metadata·Decision Log 사유 누락 0건.
|
||
- **수동 확인:** Phase 9 현재 Task와 리뷰 마지막 두 section만 읽어 과거 Safari 이력과 현재 Chrome-only Gate를 혼동하지 않는지 확인한다.
|
||
|
||
**P9-R12 수정 검증 기록 (2026-07-31):**
|
||
|
||
- RED: 강화한 `mock-preview-docs.test.ts`가 PRD Decision Log의 사용자 결정 사유 누락과 `P9-R10` 현재 수동 확인의 `자동 WebKit harness` 문구를 검출했다. `npm run test:run -- src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 1 failed / 3 passed / 1 not reached.
|
||
- GREEN/REFACTOR: PRD 결정 행에 사용자 직접 지시 사유를 추가하고, 현재 `P9-R10` 수동 확인 문구와 `P9-R11` 기대 test 수를 Chromium/mobile Chrome 계약에 맞췄다. docs contract는 `P9-R9` block과 최신 Phase 9 종료 판정만 검사하도록 좁혔다.
|
||
- 검증: `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 2 files / 8 tests passed. `npm run e2e:mock -- --list` — Chromium/mobile Chrome 104 tests 수집. `npm run e2e -- --list` — Chromium/mobile Chrome 18 tests 수집. Markdown link와 diff 검사는 `P10-R11` 완료 뒤 통합 실행한다.
|
||
|
||
### Task R9.13 — Chrome-only 결정 사유·문서 section 계약 폐쇄
|
||
|
||
**Goal 실행 `P9-R13`:** Chrome-only 결정의 실제 제품·시간 근거를 Decision Log에 남기고 docs contract가 PRD 결정과 최신 Phase 9 결론의 정확한 Markdown section만 검증하게 한다.
|
||
|
||
- **연결 리뷰:** [Phase 9 현재 리뷰](./reviews/phase9-cross-cutting-quality.md) — `REV-P9-013`
|
||
- **시작 조건:** `P9-R12`의 실행 결과는 통과하지만 PRD 결정 사유가 “사용자 직접 지시”에 머물고, docs contract가 PRD 전체와 Phase 9 리뷰의 특정 heading부터 EOF를 검사해 후속 기록의 우연한 문자열에 의존할 수 있다는 재리뷰 근거.
|
||
- **완료 증거:** Chrome 2종만 지원하며 불필요한 Safari 계열 실행이 테스트 시간을 늘린다는 Decision Log 근거, heading level 또는 명시적 경계로 닫힌 PRD Decision Log·최신 Phase 9 결론 검사, docs contract 2 files / 8 tests와 두 Playwright 목록 결과.
|
||
- **Files:** Modify: `docs/20260725_AI캐릭터관리자웹/prd.md`, `docs/20260725_AI캐릭터관리자웹/plan-task.md`, `docs/20260725_AI캐릭터관리자웹/reviews/phase9-cross-cutting-quality.md`, `src/shared/mocks/__tests__/mock-preview-docs.test.ts`.
|
||
- **Interfaces:** Playwright config·project 이름·E2E spec은 변경하지 않는다. 과거 browser 실행 이력은 보존하고 현재 Decision Log와 최신 종료 판정만 범위화한다.
|
||
- **범위 밖:** WebKit/mobile Safari 설치·실행·지원 복원, 제품/E2E 코드 변경, 새 Markdown parser dependency.
|
||
- **Phase 9 Gate 연결:** docs contract, mock/server `--list`, Markdown link·diff 검사가 모두 통과하기 전에는 완료하지 않는다.
|
||
|
||
- [x] **RED:** PRD Decision Log와 최신 Phase 9 review/종료 판정을 heading 경계로 추출하는 contract를 먼저 작성해 현재 사유 누락과 EOF scope 의존으로 실패하는지 확인한다.
|
||
- [x] **GREEN:** Decision Log에 지원 대상과 테스트 시간 근거를 추가하고 최소 section helper로 현재 두 block만 검증해 focused test를 통과시킨다.
|
||
- [x] **REFACTOR:** plan 전체·후속 review 문자열에 의존하는 assertion을 제거하고 기존 8개 docs contract 회귀를 확인한다.
|
||
- [x] focused·Playwright list·link·diff 결과를 Progress와 Phase 9 리뷰에 누적한다.
|
||
- **실행 명령:** `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts`; `npm run e2e:mock -- --list`; `npm run e2e -- --list`; `node -e 'const fs=require("fs"),path=require("path");const root="docs/20260725_AI캐릭터관리자웹";const files=[];const walk=d=>{for(const e of fs.readdirSync(d,{withFileTypes:true})){const p=path.join(d,e.name);e.isDirectory()?walk(p):e.name.endsWith(".md")&&files.push(p)}};walk(root);const broken=[];for(const f of files){const s=fs.readFileSync(f,"utf8");for(const m of s.matchAll(/\[[^\]]*\]\(([^)]+)\)/g)){const t=m[1].trim().replace(/^<|>$/g,"").split("#")[0];if(t&&!/^(https?:|mailto:)/.test(t)&&!fs.existsSync(path.resolve(path.dirname(f),t)))broken.push(f+": "+t)}}if(broken.length){console.error(broken.join("\n"));process.exit(1)}'`; `git diff --check`.
|
||
- **기대 결과:** 2 files / 8 tests, mock 104·server 18 tests가 `chromium`·`mobile-chrome`에서만 수집되고 Decision Log 사유·section 경계·link·diff 오류 0건.
|
||
- **수동 확인:** PRD Decision Log와 Phase 9 마지막 review section만 읽어 지원 대상, 제외 사유, 테스트 시간 영향과 현재 Gate를 재구성할 수 있는지 확인한다.
|
||
|
||
**P9-R13 수정 검증 기록 (2026-07-31):**
|
||
|
||
- RED: `npm run test:run -- src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 1 file / 2 failed / 3 passed. PRD 결정 행의 `Chrome 2종`·`테스트 시간` 근거 누락과 `## 22. P9-R13 수정 결과 검증` section 부재를 검출했다.
|
||
- GREEN/REFACTOR: PRD Decision Log에 Chrome 2종 지원 범위와 Safari 계열 실행의 테스트 시간 영향을 추가했다. docs contract는 PRD Decision Log의 단일 Chrome-only 결정 행과 Phase 9의 `P9-R13` section만 검사하도록 닫았고, Phase 9 리뷰 metadata와 종료 판정을 `REV-P9-013`/`P9-R13` 완료로 갱신했다.
|
||
- 검증: `npm run test:run -- src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 1 file / 5 tests passed. `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 2 files / 8 tests passed. `npm run e2e:mock -- --list` — Chromium/mobile Chrome 104 tests 수집. `npm run e2e -- --list` — Chromium/mobile Chrome 18 tests 수집. Markdown link 검사와 `git diff --check`는 통과했다.
|
||
|
||
### Task R9.14 — Community Sheet focus 반환 full-unit 경합 제거
|
||
|
||
**Goal 실행 `P9-R14`:** Community Sheet의 중첩 비활성화 dialog focus 반환 test가 전체 Vitest 부하에서도 실제 effect 완료를 기다려 full unit Gate를 결정적으로 통과하게 한다.
|
||
|
||
- **연결 리뷰:** [Phase 9 현재 리뷰](./reviews/phase9-cross-cutting-quality.md) — `REV-P9-014`
|
||
- **시작 조건:** fresh full unit 1회가 `community-sheet.test.tsx:82`의 deactivate trigger focus assertion에서 1 failed / 416 passed였고, 동일 test 단독 5회와 다음 fresh full 417/417은 통과한 비결정성 근거.
|
||
- **완료 증거:** focus 복귀 effect를 기다리는 최소 test 수정, 동일 test 반복 5회와 full unit 2회 연속 통과, 정적/build 회귀 기록.
|
||
- **Files:** Modify: `src/features/community-posts/tests/community-sheet.test.tsx`, `docs/20260725_AI캐릭터관리자웹/plan-task.md`, `docs/20260725_AI캐릭터관리자웹/reviews/phase9-cross-cutting-quality.md`; Reuse: `src/shared/ui/use-modal-focus.ts`, `src/shared/ui/__tests__/confirm-deactivate-dialog.test.tsx`.
|
||
- **Interfaces:** 제품 focus 구현과 dialog API는 변경하지 않는다. 공통 dialog test가 사용하는 `waitFor` 패턴을 재사용하고 timeout 상향·retry로 실패를 숨기지 않는다.
|
||
- **범위 밖:** modal architecture 변경, 제품 Community mutation 흐름 변경, WebKit/mobile Safari 실행.
|
||
- **Phase 9 Gate 연결:** focused 반복과 full unit 2회, `typecheck`·`lint`·개발/운영 build가 통과하기 전에는 완료하지 않는다.
|
||
|
||
- [x] **RED:** fresh full unit의 `닫기` focus 수신 실패와 동일 test 단독 반복 통과를 기록해 full-load effect 경합을 재현했다.
|
||
- [x] **GREEN:** 취소 직후 trigger focus assertion을 effect cleanup 완료까지 기다리는 기존 `waitFor` 패턴으로 최소 수정했다.
|
||
- [x] **REFACTOR:** 임의 sleep·timeout·retry 없이 focused 5회와 full unit 2회를 연속 실행해 focus 경합이 재발하지 않는지 확인했다.
|
||
- [x] 정적/build 결과와 실제 pass 수를 Progress와 Phase 9 리뷰에 누적했다.
|
||
- **실행 명령:** `for run_index in {1..5}; do npm run test:run -- src/features/community-posts/tests/community-sheet.test.tsx -t 'Community Sheet edits pins unpins and soft deletes from the list item without detail GET' || exit 1; done`; `npm run test:run`; `npm run test:run`; `npm run typecheck`; `npm run lint`; `npm run build:dev`; `npm run build:prod`; `git diff --check`.
|
||
- **기대 결과:** focused 5회 모두 1 passed / 6 skipped, full unit 2회 모두 81 files / 417 tests, 정적/build/diff exit 0, 취소 후 trigger 외 focus 수신 0건.
|
||
- **수동 확인:** 없음. 기존 제품 focus 동작은 변경하지 않고 test의 effect 완료 관찰만 교정한다.
|
||
|
||
**P9-R14 수정 검증 기록 (2026-07-31):**
|
||
|
||
- RED: 2026-07-31 재리뷰에서 fresh full unit 1회가 `community-sheet.test.tsx`의 취소 후 trigger focus assertion에서 1 failed / 416 passed로 실패했고, 동일 test focused 5회와 다음 fresh full은 통과해 full-load effect 경합으로 확정했다.
|
||
- GREEN: 제품 focus 구현과 dialog API는 변경하지 않고 `src/features/community-posts/tests/community-sheet.test.tsx`의 취소 후 focus assertion만 공통 `ConfirmDeactivateDialog` test와 같은 `waitFor` 패턴으로 변경했다.
|
||
- REFACTOR/검증: `for run_index in {1..5}; do npm run test:run -- src/features/community-posts/tests/community-sheet.test.tsx -t 'Community Sheet edits pins unpins and soft deletes from the list item without detail GET' || exit 1; done` — 5회 모두 1 passed / 6 skipped. `npm run test:run` 2회 — 각각 81 files / 417 tests passed. `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0. `git diff --check -- src/features/community-posts/tests/community-sheet.test.tsx` — no output. build는 기존 500kB chunk warning만 표시했다.
|
||
|
||
### Task R9.15 — PRD·Phase 9 종료 판정 heading 경계 고정
|
||
|
||
**Goal 실행 `P9-R15`:** docs contract가 PRD Decision Log H2와 최신 Phase 9 종료 판정 H3를 동일·상위 heading에서 각각 닫아 후속 section 문자열로 통과할 수 없게 한다.
|
||
|
||
- **연결 리뷰:** [Phase 9 현재 리뷰](./reviews/phase9-cross-cutting-quality.md) — `REV-P9-015`
|
||
- **시작 조건:** `P9-R13`에서 `sectionAtHeading`을 추가했지만 PRD Decision Log는 `sectionFrom()`으로 EOF까지 읽고, 최신 종료 판정도 상위 H2 block 끝까지 읽어 후속 H2/H3로 완료 문자열을 옮겨도 current assertion이 통과하는 negative-control 근거.
|
||
- **완료 증거:** heading 행을 정확히 anchor하고 동일·상위 level의 다음 heading에서 닫는 최소 helper, PRD H2·Phase 9 H2·종료 판정 H3 각각의 synthetic trailing-section negative test와 docs contract 2 files / 8 tests.
|
||
- **Files:** Modify: `src/shared/mocks/__tests__/mock-preview-docs.test.ts`, `docs/20260725_AI캐릭터관리자웹/plan-task.md`, `docs/20260725_AI캐릭터관리자웹/reviews/phase9-cross-cutting-quality.md`.
|
||
- **Interfaces:** 새 Markdown parser dependency를 추가하지 않고 문자열·정규식 helper 하나를 재사용한다. PRD 내용, Playwright config와 E2E spec은 변경하지 않는다.
|
||
- **범위 밖:** Markdown 전체 parser 구현, 과거 review 이력 삭제, WebKit/mobile Safari 실행.
|
||
- **Phase 9 Gate 연결:** negative-control과 docs contract, 두 Playwright `--list`, link·diff 검사가 통과하기 전에는 완료하지 않는다.
|
||
|
||
- [x] **RED:** Decision Log 행을 후속 H2로, 완료 문구를 후속 H3로 옮긴 synthetic 문서가 현재 helper에서 통과하는 실패 test를 추가했다.
|
||
- [x] **GREEN:** heading level을 계산해 동일·상위 level의 다음 heading 또는 EOF에서 닫는 최소 helper로 PRD H2·최신 H2·종료 판정 H3를 추출했다.
|
||
- [x] **REFACTOR:** `sectionFrom`과 중복 helper를 제거하고 현재 Decision Log·Phase 9 종료 판정 assertion을 정확한 section 내부로 한정했다.
|
||
- [x] docs contract·Playwright list·link·diff 결과를 Progress와 Phase 9 리뷰에 누적했다.
|
||
- **실행 명령:** `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts`; `npm run e2e:mock -- --list`; `npm run e2e -- --list`; `node -e 'const fs=require("fs"),path=require("path");const root="docs/20260725_AI캐릭터관리자웹";const files=[];const walk=d=>{for(const e of fs.readdirSync(d,{withFileTypes:true})){const p=path.join(d,e.name);e.isDirectory()?walk(p):e.name.endsWith(".md")&&files.push(p)}};walk(root);const broken=[];for(const f of files){const s=fs.readFileSync(f,"utf8");for(const m of s.matchAll(/\[[^\]]*\]\(([^)]+)\)/g)){const t=m[1].trim().replace(/^<|>$/g,"").split("#")[0];if(t&&!/^(https?:|mailto:)/.test(t)&&!fs.existsSync(path.resolve(path.dirname(f),t)))broken.push(f+": "+t)}}if(broken.length){console.error(broken.join("\n"));process.exit(1)}'`; `git diff --check`.
|
||
- **기대 결과:** 2 files / 8 tests, synthetic 후속 H2/H3 false positive 0건, mock 104·server 18 tests가 `chromium`·`mobile-chrome`에서만 수집되고 link·diff 오류 0건.
|
||
- **수동 확인:** PRD Decision Log와 Phase 9 최신 종료 판정 뒤에 임의의 같은 수준 section이 추가돼도 현재 계약 판정이 바뀌지 않는지 확인한다.
|
||
|
||
**P9-R15 수정 검증 기록 (2026-07-31):**
|
||
|
||
- RED: `npm run test:run -- src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 1 file / 1 failed / 4 passed. synthetic 후속 H3의 `수정 완료` 문구가 `### 종료 판정` section에 섞이는 실패를 확인했다.
|
||
- GREEN: `sectionAtHeading`이 heading level을 계산해 동일·상위 level의 다음 heading에서 닫도록 수정하고, PRD Decision Log와 Phase 9 최신 종료 판정을 같은 helper로 추출하게 했다. 날짜 suffix가 있는 실제 heading은 prefix 뒤 공백 suffix만 허용했다.
|
||
- REFACTOR/검증: `sectionFrom`을 제거하고 synthetic 후속 H2/H3 negative assertion을 현재 docs contract에 남겼다. `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 2 files / 8 tests passed. `npm run e2e:mock -- --list` — Chromium/mobile Chrome 104 tests 수집. `npm run e2e -- --list` — Chromium/mobile Chrome 18 tests 수집. Markdown link 검사와 targeted `git diff --check`는 no output이었다.
|
||
|
||
### Task R9.16 — 최신 Phase 9 heading exact anchor 복구
|
||
|
||
**Goal 실행 `P9-R16`:** docs contract가 임의 suffix heading을 목표 heading으로 오인하지 않고 최신 Phase 9 review H2와 그 내부 종료 판정만 검사하게 한다.
|
||
|
||
- **연결 리뷰:** [Phase 9 현재 리뷰](./reviews/phase9-cross-cutting-quality.md) — `REV-P9-016`
|
||
- **시작 조건:** `sectionAtHeading()`이 ``line.startsWith(`${heading} `)``를 허용해 임의 suffix heading을 선택하고, Phase 9 contract가 현재 최신 H2가 아닌 과거 `## 22. P9-R13 수정 결과 검증`을 계속 검사하는 negative-control 근거.
|
||
- **완료 증거:** 전체 heading 행 exact match, 최신 Phase 9 H2와 그 내부 `### 종료 판정` 독립 추출, 임의 suffix·후속 H2/H3 false positive를 막는 synthetic assertion과 docs contract 통과.
|
||
- **Files:** Modify: `src/shared/mocks/__tests__/mock-preview-docs.test.ts`, `docs/20260725_AI캐릭터관리자웹/plan-task.md`, `docs/20260725_AI캐릭터관리자웹/reviews/phase9-cross-cutting-quality.md`.
|
||
- **Interfaces:** 새 Markdown parser 없이 기존 문자열 helper를 exact line match로 축소한다. PRD·제품·Playwright config·E2E spec은 변경하지 않는다.
|
||
- **범위 밖:** Markdown 전체 parser, 과거 review 이력 삭제, WebKit/mobile Safari 실행.
|
||
- **Phase 9 Gate 연결:** synthetic negative-control, docs contract, 두 Playwright `--list`, typecheck·lint·link·diff 검사가 통과하기 전에는 완료하지 않는다.
|
||
|
||
- [x] **RED:** 목표 heading 뒤 임의 suffix를 붙인 가짜 H2가 선택되고 최신 Phase 9 H2가 검사되지 않는 synthetic 실패를 고정했다.
|
||
- [x] **GREEN:** helper를 전체 heading 행 exact match로 축소하고 contract 대상을 최신 Phase 9 H2와 그 내부 종료 판정으로 갱신했다.
|
||
- [x] **REFACTOR:** 날짜 suffix를 prefix 허용으로 숨기지 않고 호출부가 실제 전체 heading을 명시하게 하며 기존 H2/H3 경계 assertion을 유지했다.
|
||
- [x] focused·Playwright list·정적·link·diff 결과를 Progress와 Phase 9 리뷰에 누적한다.
|
||
- **실행 명령:** `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts`; `npm run typecheck`; `npm run lint`; `npm run e2e:mock -- --list`; `npm run e2e -- --list`; `node -e 'const fs=require("fs"),path=require("path");const root="docs/20260725_AI캐릭터관리자웹";const files=[];const walk=d=>{for(const e of fs.readdirSync(d,{withFileTypes:true})){const p=path.join(d,e.name);e.isDirectory()?walk(p):e.name.endsWith(".md")&&files.push(p)}};walk(root);const broken=[];for(const f of files){const s=fs.readFileSync(f,"utf8");for(const m of s.matchAll(/\[[^\]]*\]\(([^)]+)\)/g)){const t=m[1].trim().replace(/^<|>$/g,"").split("#")[0];if(t&&!/^(https?:|mailto:)/.test(t)&&!fs.existsSync(path.resolve(path.dirname(f),t)))broken.push(f+": "+t)}}if(broken.length){console.error(broken.join("\n"));process.exit(1)}'`; `git diff --check`.
|
||
- **기대 결과:** docs contract 2 files / 9 tests, 임의 suffix·과거 H2·후속 H2/H3 false positive 0건, mock 104·server 18 tests가 `chromium`·`mobile-chrome`에서만 수집되고 정적·link·diff 오류 0건.
|
||
- **수동 확인:** Phase 9 최신 H2와 종료 판정만 읽어 현재 발견·소유 Task를 재구성할 수 있고 과거 완료 section의 문구가 현재 판정을 대신하지 않는지 확인한다.
|
||
|
||
**P9-R16 수정 검증 기록 (2026-07-31):**
|
||
|
||
- RED: `npm run test:run -- src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 1 file / 2 failed / 4 passed. 최신 Phase 9 종료 판정이 아직 `후속 goal 필요`라 실패했고, P10 current-state도 후속 Task 반영 전 상태라 함께 실패했다.
|
||
- GREEN: `sectionAtHeading()`을 전체 heading 행 exact match로 축소하고 Phase 9 contract가 최신 `## 24. P9-R14~R15 수정 결과 재점검 — 2026-07-31`와 내부 `### 종료 판정`을 검사하게 했다. `phase9-cross-cutting-quality.md`의 `REV-P9-016` 상태와 종료 판정을 수정 완료로 갱신했다.
|
||
- focused: `npm run test:run -- src/shared/mocks/__tests__/mock-preview-docs.test.ts -t "keeps current Phase 9"` — 1 file / 1 passed / 5 skipped.
|
||
- 검증: `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 2 files / 9 tests passed. `npm run typecheck`, `npm run lint`, 필수 12 section 검사, Markdown link 검사, `git diff --check` — 모두 exit 0. `npm run e2e:mock -- --list`는 Chromium/mobile Chrome 104 tests, `npm run e2e -- --list`는 Chromium/mobile Chrome 18 tests를 수집했다.
|
||
|
||
### Task R9.17 — Phase 9 current metadata·최신 결론 동기화
|
||
|
||
**Goal 실행 `P9-R17`:** `P10-R14` 완료 뒤에도 후속 필요로 남은 Phase 9 review metadata를 현재 상태로 맞추고 docs contract가 metadata와 실제 최신 H2·종료 판정을 함께 검사하게 한다.
|
||
|
||
- **연결 리뷰:** [Phase 9 현재 리뷰](./reviews/phase9-cross-cutting-quality.md) — `REV-P9-017`
|
||
- **시작 조건:** Phase 9 review 상단 `리뷰 상태`와 최신 `## 24` 종료 판정이 완료된 `P10-R14`를 후속 필요로 표시하지만 current docs contract가 metadata를 읽지 않고 과거 H2를 계속 대상으로 삼아 통과하는 근거.
|
||
- **완료 증거:** Phase 9 `## 1. 리뷰 정보`, 실제 마지막 H2와 내부 `### 종료 판정`의 동일 current-state assertion, `P10-R14` 후속 문구와 새 trailing H2 누락을 각각 차단하는 negative-control.
|
||
- **Files:** Modify: `src/shared/mocks/__tests__/mock-preview-docs.test.ts`, `docs/20260725_AI캐릭터관리자웹/plan-task.md`, `docs/20260725_AI캐릭터관리자웹/reviews/phase9-cross-cutting-quality.md`.
|
||
- **Interfaces:** 기존 exact heading helper를 재사용하고 과거 H2·판정 이력은 수정하지 않는다. 제품·PRD·Playwright 설정은 변경하지 않는다.
|
||
- **범위 밖:** 과거 review 결론 삭제, 제품/E2E 코드 변경, WebKit/mobile Safari 실행.
|
||
- **Phase 9 Gate 연결:** docs contract, synthetic metadata/최신 H2 scope, 두 Playwright `--list`, typecheck·lint·link·diff 검사가 통과하기 전에는 완료하지 않는다.
|
||
|
||
- [x] **RED:** Phase 9 metadata와 실제 마지막 H2가 `P10-R14` 후속 필요를 유지하거나 새 trailing H2가 추가돼도 현재 contract가 과거 `## 24`로 통과하는 실패를 고정했다.
|
||
- [x] **GREEN:** 현재 상태를 기록하는 새 H2를 append하고 metadata·문서의 마지막 H2·내부 종료 판정을 독립 추출해 완료 상태를 검사한다.
|
||
- [x] **REFACTOR:** 과거 `## 24` 이력은 보존하고 current assertion이 이전 H2를 최신 판정으로 사용하지 못하게 했다.
|
||
- [x] focused·Playwright list·정적·link·diff 결과를 Progress와 Phase 9 리뷰에 누적한다.
|
||
- **실행 명령:** `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts`; `npm run typecheck`; `npm run lint`; `npm run e2e:mock -- --list`; `npm run e2e -- --list`; `node -e 'const fs=require("fs"),path=require("path");const root="docs/20260725_AI캐릭터관리자웹";const files=[];const walk=d=>{for(const e of fs.readdirSync(d,{withFileTypes:true})){const p=path.join(d,e.name);e.isDirectory()?walk(p):e.name.endsWith(".md")&&files.push(p)}};walk(root);const broken=[];for(const f of files){const s=fs.readFileSync(f,"utf8");for(const m of s.matchAll(/\[[^\]]*\]\(([^)]+)\)/g)){const t=m[1].trim().replace(/^<|>$/g,"").split("#")[0];if(t&&!/^(https?:|mailto:)/.test(t)&&!fs.existsSync(path.resolve(path.dirname(f),t)))broken.push(f+": "+t)}}if(broken.length){console.error(broken.join("\n"));process.exit(1)}'`; `git diff --check`.
|
||
- **기대 결과:** docs contract 2 files / 9 tests, Phase 9 metadata·최신 H2·종료 판정 불일치 0건, mock 104·server 18 tests가 `chromium`·`mobile-chrome`에서만 수집되고 정적·link·diff 오류 0건.
|
||
- **수동 확인:** Phase 9 상단 metadata와 마지막 H2만 읽어 자동 보완 완료·Chrome 2종·남은 수동 QA를 같은 상태로 판단할 수 있는지 확인한다.
|
||
|
||
### Task R9.18 — Phase 9 finding·checklist 상태 종결 contract
|
||
|
||
**Goal 실행 `P9-R18`:** `P9-R17` 완료 결론과 소유 finding `REV-P9-017`의 상태·Task checklist를 같은 `수정 완료`로 유지하고 이후 불일치를 자동으로 차단한다.
|
||
|
||
- **연결 리뷰:** [Phase 9 현재 리뷰](./reviews/phase9-cross-cutting-quality.md) — `REV-P9-018`.
|
||
- **시작 조건:** 최신 `## 26`과 metadata는 `P9-R17` 완료를 선언하지만 `REV-P9-017` 표는 `확정` 상태다. docs contract는 소유 finding 표를 검사하지 않고 `P9-R17` 범위를 `P10-T1`까지 열어 두어 후속 Phase 9 Task의 미완료 checklist까지 섞는다.
|
||
- **완료 증거:** `REV-P9-017` 표의 `수정 완료` 상태와 2026-08-01 수정 근거, `P9-R17` 자체 checklist 경계, Phase 9 metadata·최신 결론·소유 finding 상태를 함께 검사하는 assertion.
|
||
- **Files:** Modify: `src/shared/mocks/__tests__/mock-preview-docs.test.ts`, `docs/20260725_AI캐릭터관리자웹/plan-task.md`, `docs/20260725_AI캐릭터관리자웹/reviews/phase9-cross-cutting-quality.md`.
|
||
- **Interfaces:** Phase 9가 소유한 finding 상태만 검사한다. Phase 10의 현재 Task를 Phase 9 metadata에 중복 기록하지 않는다.
|
||
- **범위 밖:** 제품·Playwright 설정 변경, 과거 결론 삭제, WebKit/mobile Safari 실행.
|
||
|
||
- [x] **RED:** `REV-P9-017` 상태가 `확정`이어도 contract가 통과하고, 후속 Phase 9 Task를 append하면 `P9-R17` checklist 범위가 함께 실패하는 두 경계를 고정한다.
|
||
- [x] **GREEN:** 소유 finding 표를 `수정 완료`로 갱신하고 날짜별 수정 근거를 append한 뒤 `P9-R17` 자체 checklist·metadata·최신 결론과 함께 검사한다.
|
||
- [x] **REFACTOR:** Phase 10 current-state 토큰을 Phase 9 metadata에서 제거하고 Phase 9 소유 상태만 남긴다.
|
||
- [x] docs contract·typecheck·lint·link·diff 결과를 Progress와 Phase 9 리뷰에 누적한다.
|
||
- **실행 명령:** `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts`; `npm run typecheck`; `npm run lint`; Markdown link 검사; `git diff --check`.
|
||
- **기대 결과:** `REV-P9-017` 상태와 현재 결론 불일치 0건, docs contract·정적·link·diff 오류 0건.
|
||
|
||
**P9-R18 수정 검증 기록 (2026-08-01):**
|
||
|
||
- RED: `keeps current Phase 9` — `REV-P9-017`의 `확정` 상태로 1 failed / 5 skipped였고 checklist assertion 추가 뒤에는 후속 미완료 Task가 포함돼 1 failed / 7 skipped였다.
|
||
- GREEN/REFACTOR: `P9-R17` Task를 다음 H3에서 닫고 `REV-P9-017`·`REV-P9-018` 상태·날짜별 근거를 직접 검사했다. Phase 9 metadata에서 Phase 10 current Task 중복을 제거했다.
|
||
|
||
### Task R9.19 — fenced code 내부 가짜 H2 배제
|
||
|
||
**Goal 실행 `P9-R19`:** 최신 H2 탐색이 fenced code block 안의 `##` 예시나 앞선 동일 제목을 실제 마지막 review section으로 선택하지 않게 한다.
|
||
|
||
- **연결 리뷰:** [Phase 9 현재 리뷰](./reviews/phase9-cross-cutting-quality.md) — `REV-P9-019`.
|
||
- **시작 조건:** `latestSectionAtLevel()`이 Markdown fence 상태를 구분하지 않아 문서 최하단 code fence 안의 `## example`을 최신 H2로 반환하고, 마지막 heading 문자열을 다시 첫 occurrence로 탐색해 동일 제목이 반복되면 앞선 section을 반환하는 synthetic 재현 근거.
|
||
- **선행조건:** `P9-R18` 완료.
|
||
- **완료 증거:** backtick·tilde fence 내부 heading을 제외하고 실제 마지막 H2의 index를 직접 사용하는 최소 helper와 fence·동일 제목 synthetic assertion.
|
||
- **Files:** Modify: `src/shared/mocks/__tests__/mock-preview-docs.test.ts`, `docs/20260725_AI캐릭터관리자웹/plan-task.md`, `docs/20260725_AI캐릭터관리자웹/reviews/phase9-cross-cutting-quality.md`.
|
||
- **Interfaces:** 새 Markdown parser나 dependency 없이 기존 helper에 fence 상태만 추가한다.
|
||
- **범위 밖:** Markdown 전체 문법 지원, 제품·Playwright 설정 변경, WebKit/mobile Safari 실행.
|
||
|
||
- [x] **RED:** 실제 H2 뒤 fenced code 내부 가짜 H2 또는 같은 제목의 앞선 H2가 있으면 실제 마지막 section을 놓치는 실패를 고정한다.
|
||
- [x] **GREEN:** backtick·tilde fence 구간을 건너뛰고 탐색한 index에서 실제 마지막 H2 section을 직접 추출한다.
|
||
- [x] **REFACTOR:** fence 처리 외 heading/section 경계 로직은 기존 helper를 재사용한다.
|
||
- [x] docs contract·typecheck·lint·link·diff 결과를 Progress와 Phase 9 리뷰에 누적한다.
|
||
- **실행 명령:** `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts`; `npm run typecheck`; `npm run lint`; Markdown link 검사; `git diff --check`.
|
||
- **기대 결과:** fenced heading·중복 제목 false positive 0건, docs contract·정적·link·diff 오류 0건.
|
||
|
||
**P9-R19 수정 검증 기록 (2026-08-01):**
|
||
|
||
- RED: backtick·tilde fence 내부 H2와 동일 제목 H2 synthetic 2건이 2 failed / 6 skipped로 재현됐다.
|
||
- GREEN/REFACTOR: fence 밖 heading index를 한 번 수집해 실제 마지막 H2를 직접 추출했다. synthetic 2 passed / 6 skipped, Phase 9 current-state 포함 focused 3 passed / 5 skipped, docs contract 2 files / 12 tests passed였다.
|
||
|
||
---
|
||
|
||
## Phase 10. OpenAPI 2.3.0 후속 계약 vertical slices
|
||
|
||
**목표:** OpenAPI 2.3.0에서 해결된 lookup·댓글·pagination 계약과 UTC
|
||
전송 변경, FanTalk 수정·삭제, 가격·공통 오류·파일 정책을 기존 UI에
|
||
반영하고 mock/client 완료와 실제 server integration 상태를 분리해 검증한다.
|
||
|
||
**Phase Goal `P10`:** Task 10.1 → 10.7과 Phase 10 Gate로 후속 계약을
|
||
도메인별 독립 test cycle로 구현한다.
|
||
|
||
- **시작 조건:** 2026-07-28 Phase 3~9 기록 보존, PRD의 2026-07-29 결정 기록과 OpenAPI 2.3.0 확인.
|
||
- **완료 조건:** `P10-T1`~`P10-T7`, `P10-GATE` 완료.
|
||
- **실행 순서:** Character lookup → Audio UTC → Series genre/CRUD → Community pagination → FanTalk 수정·삭제 → Comments → 가격·오류·파일 정책 → 통합 Gate.
|
||
- **요구사항:** `CHAR-012~014`, `AUDIO-003`, `AUDIO-009~011`, `AUDIO-019`, `AUDIO-022`, `AUDIO-025`, `AUDIO-030`, `SERIES-001~018`, `COMMUNITY-007~008`, `COMMUNITY-010`, `COMMUNITY-012`, `FANTALK-003~012`, `COMMENT-001~008`, `FILE-001~015`, `EXT-001~005`, `EXT-007~011`.
|
||
- **범위 밖:** 제품 범위에서 제외한 FanTalk 별도 상세 GET, 답변 상태 filter, sort, 중복 생성의 정확한 도메인 오류 key와 답변 삭제, 인증 operation 정식화.
|
||
- **server 검증 전제:** `series.spec.ts`, `fan-talk.spec.ts`, `comments.spec.ts`를 server mode `testMatch`에 포함해 file filter와 교집합이 생기게 한다. 실제 mutation 검증은 Chromium 1개 project와 실행 환경이 제공한 전용 개발 API 계정·재사용 가능한 fixture ID로 한정해 같은 destructive mutation의 browser별 반복을 피한다. 계정·fixture가 없으면 client/mock GREEN은 진행하되 해당 server 명령을 `No tests found`나 mock 성공으로 대체하지 않고 server integration 대기로 기록한다.
|
||
|
||
| Goal | 선행조건 | 독립 결과 |
|
||
|---|---|---|
|
||
| `P10-T1` | OpenAPI 2.3.0 원작 lookup `implemented` | Character v2 원작 검색·선택 |
|
||
| `P10-T2` | Audio UTC schema와 no-timezone 계약 | Audio UTC 생성·조회 migration |
|
||
| `P10-T3` | Series 장르·CRUD `implemented` | Series 장르 선택·CRUD form |
|
||
| `P10-T4` | Community pagination schema `implemented` | 서버 metadata 기반 목록 |
|
||
| `P10-T5` | FanTalk DELETE·PUT `implemented` | 팬 원글 삭제와 reply 수정 client/mock/server |
|
||
| `P10-T6` | Audio·Community 댓글 operation `implemented` | 두 target의 2단계 댓글 관리 |
|
||
| `P10-T7` | `EXT-009~011` 해결과 기존 파일 client 검증 완료 | 가격 상한·공통 오류 fallback·파일 server 경계 |
|
||
| `P10-GATE` | `P10-T1`~`P10-T7` 완료 | 후속 계약 통합 검증·server 상태 분리 |
|
||
|
||
### Task 10.1 Character v2 원작 검색
|
||
|
||
**Goal 실행 `P10-T1`:** legacy 원작 lookup 전제를 제거하고 v2
|
||
`OriginalWorkSearchItem` 기반 선택기와 create/update payload를 완성한다.
|
||
|
||
- **시작 조건:** OpenAPI `searchAiCharacterOriginalWorks`가 `implemented`.
|
||
- **Files:** Modify: `src/features/characters/api/character-api.ts`, `src/features/characters/model/types.ts`, `src/features/characters/pages/CharacterCreatePage.tsx`, `src/features/characters/pages/CharacterEditPage.tsx`, `src/features/characters/tests/character-api.test.ts`, `src/features/characters/tests/CharacterCreatePage.test.tsx`, `src/features/characters/tests/CharacterEditPage.test.tsx`, `src/shared/mocks/character-fixtures.ts`, `src/shared/mocks/character-mock-store.ts`, `src/shared/mocks/handlers.ts`, `src/shared/mocks/__tests__/character-handlers.test.ts`, `tests/e2e/character-workspace.spec.ts`.
|
||
- **Interfaces:** `searchOriginalWorks(apiClient, searchTerm): Promise<OriginalWorkSearchItem[]>`; `OriginalWorkSearchItem={id,title,contentType,category,isAdult,description,originalWork,originalLink,writer,studio,originalLinks,tags,imageUrl}`; create/update의 선택값은 `originalWorkId`, 미선택은 key 생략.
|
||
- **범위 밖:** legacy `/admin/chat/original/search`, 원작 생성·수정 UI.
|
||
|
||
- [x] **RED:** v2 path와 필수 `searchTerm`, 전체 DTO parse, 선택/해제 payload를 검증하는 실패 test를 먼저 작성한다.
|
||
- [x] **GREEN:** 300ms 부분 검색 선택기를 생성·수정 화면에 연결하고 선택한 `id`만 `originalWorkId`로 직렬화한다.
|
||
- [x] **REFACTOR:** legacy lookup fixture·path 문자열을 제거하고 Character focused test와 mock E2E를 재실행한다.
|
||
- **실행 명령:** `npm run test:run -- src/features/characters src/shared/mocks/__tests__/character-handlers.test.ts`; `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts`.
|
||
- **기대 결과:** focused/unit와 mock E2E exit 0, v2 원작 검색 호출 1건 이상, legacy lookup 호출 0건, 미선택 payload의 `originalWorkId` 0건.
|
||
- **수동 확인:** desktop/tablet에서 원작 검색→선택→저장과 선택 해제를 확인한다. 모바일 Character mutation은 기존 미지원 정책을 유지한다.
|
||
|
||
### Task 10.2 Audio UTC 예약 공개 계약 전환
|
||
|
||
**Goal 실행 `P10-T2`:** 예약 입력을 client에서 UTC `Z`로 변환하고
|
||
Audio 생성·상세의 모든 `timezone` field/query를 제거한다.
|
||
|
||
- **시작 조건:** OpenAPI `AudioContentCreateRequest.releaseDate`가 nullable `date-time`, `pattern=Z$`이고 `timezone` schema/parameter가 없음.
|
||
- **Files:** Modify: `src/features/audio-contents/api/audio-content-api.ts`, `src/features/audio-contents/components/audio-content-form-helpers.ts`, `src/features/audio-contents/model/types.ts`, `src/features/audio-contents/schemas/audio-content-schema.ts`, `src/features/audio-contents/pages/AudioContentDetailPage.tsx`, `src/features/audio-contents/pages/AudioContentFormPage.tsx`, `src/features/audio-contents/tests/audio-contract.test.ts`, `src/features/audio-contents/tests/audio-form.test.tsx`, `src/features/audio-contents/tests/audio-form-test-support.tsx`, `src/features/audio-contents/tests/audio-list.test.tsx`, `src/shared/mocks/audio-content-fixtures.ts`, `src/shared/mocks/audio-content-mock-store.ts`, `src/shared/mocks/handlers.ts`, `src/shared/mocks/__tests__/character-handlers.test.ts`, `tests/e2e/audio-content.spec.ts`.
|
||
- **Interfaces:** `toUtcReleaseDate(value, "Asia/Seoul"): string`; `toCreateRequest(...).releaseDate: string|null`; `getAudioContent(apiClient,{characterId,contentId})`; request type에는 `timezone`이 없음.
|
||
- **범위 밖:** 수정 화면의 schedule 변경, status enum/filter.
|
||
|
||
- [x] **RED:** `2026-07-29T18:00` Asia/Seoul이 `2026-07-29T09:00:00Z`로 직렬화되고 즉시 공개는 `null`, `timezone` key/query는 0건인 실패 test를 작성한다.
|
||
- [x] **GREEN:** 예약 값을 UTC ISO-8601 `Z`로 변환하고 create serializer·detail adapter·mock handler에서 `timezone`을 제거한다.
|
||
- [x] **REFACTOR:** 목록·상세 fixture의 non-UTC `releaseDate`를 `Z` 값으로 통일하고 Asia/Seoul 표시 formatter 회귀를 확인한다.
|
||
- **실행 명령:** `npm run test:run -- src/features/audio-contents src/shared/mocks/__tests__/character-handlers.test.ts`; `npm run e2e:mock -- tests/e2e/audio-content.spec.ts`.
|
||
- **기대 결과:** focused/unit와 mock E2E exit 0, 예약 request `releaseDate`가 `Z$`와 일치, `timezone` request key/query 0건, 즉시 공개 `releaseDate=null`.
|
||
- **수동 확인:** Asia/Seoul 예약 입력과 화면 표시가 유지되면서 network multipart의 JSON part만 UTC로 전송되는지 확인한다.
|
||
|
||
### Task 10.3 Series v2 장르·CRUD form
|
||
|
||
**Goal 실행 `P10-T3`:** 활성 장르 목록과 `SeriesListItem` 상세 원본값으로
|
||
Series 생성·수정·soft delete 흐름을 완료한다.
|
||
|
||
- **시작 조건:** `listAiCharacterSeriesGenres`, Series CRUD가 `implemented`; 상세 성공 `data`가 `SeriesListItem`.
|
||
- **Files:** Modify: `src/features/series/api/series-api.ts`, `src/features/series/model/types.ts`, `src/features/series/pages/SeriesDetailPage.tsx`, `src/features/series/tests/series-contract.test.ts`, `src/shared/mocks/series-fixtures.ts`, `src/shared/mocks/series-handlers.ts`, `src/shared/mocks/series-mock-store.ts`, `src/shared/mocks/handlers.ts`, `tests/e2e/series.spec.ts`, `playwright.config.ts`, `src/app/route-paths.ts`, `src/app/App.tsx`; 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.tsx`, `src/features/series/components/PublishedDaysField.tsx`, `src/features/series/components/GenreCombobox.tsx`, `src/features/series/tests/series-form.test.tsx`.
|
||
- **Interfaces:** `getSeriesGenres(apiClient): Promise<SeriesGenreItem[]>`; `SeriesGenreItem={id,genre,isAdult}`; `getSeriesDetail(...): Promise<SeriesListItem>`; `SeriesCreateRequest={title:string,introduction:string,publishedDaysOfWeek:SeriesPublishedDay[],keyword:string,genreId?:number,isAdult?:boolean,writer?:string|null,studio?:string|null}`; `SeriesUpdateRequest={title?:string|null,introduction?:string|null,publishedDaysOfWeek?:SeriesPublishedDay[]|null,genreId?:number|null,isAdult?:boolean|null,state?:SeriesState|null,isActive?:boolean|null,writer?:string|null,studio?:string|null}`. `seriesCreateFormSchema`는 image·title·introduction·요일·keyword·유효한 `genreId`를 필수로 하고 state를 갖지 않으며, `seriesUpdateFormSchema`는 image와 state를 optional로 하고 keyword를 갖지 않는다. `SERIES_IMAGE_POLICY=createImagePolicy({aspect:210/297,cropRequired:true,maxWidth:1000,noUpscale:true})`; `validateSeriesImageFile(file)`은 `.jpg/.jpeg + image/jpeg`, `.png + image/png`, 최대 `10_485_760 bytes`만 허용한다. `createSeries(apiClient,{characterId,image,request}): Promise<null>`은 필수 image와 request를, `updateSeries(apiClient,{characterId,seriesId,image?,request}): Promise<null>`은 optional 교체 image와 request를 보내고, `deactivateSeries`는 update request의 `isActive=false`만 사용한다.
|
||
- **범위 밖:** create-only `keyword`의 수정·상세 표시, 장르 검색 query, 모바일 Series mutation.
|
||
|
||
- [x] **RED:** query/body 없는 장르 GET, `id`→`genreId`, 상세 enum 초기화, 생성 필수 `image`·`keyword`/no state, 수정 optional image·no `keyword`, soft delete `isActive=false`, `RANDOM` 단독 선택의 실패 test를 작성한다. Series image는 JPEG/PNG·`10_485_760 bytes`, `210:297`, 최대 폭 1,000px, no-upscale과 `round(width × 297 ÷ 210)` 세로값 1px 이내를 검증한다.
|
||
- [x] **GREEN:** desktop/tablet form과 v2 장르 선택기, `FileField`·`ImageCropDialog` 기반 Series image 선택/crop을 구현하고 생성·수정·soft delete 후 목록 재조회·이동·알림을 연결한다. server mode `testMatch`에 `series.spec.ts`를 추가한다.
|
||
- [x] **REFACTOR:** 표시용 문자열 상세 fixture와 legacy 장르 lookup 전제를 제거하고 enum label 변환을 표시 계층에만 둔다. create/update serializer와 image policy를 form에서 분리하고 mock 전용 ID·로그인 helper와 server fixture path를 분리해 focused test와 mock/server E2E를 재실행한다.
|
||
- **실행 명령:** `npm run test:run -- src/features/series`; `npm run e2e:mock -- tests/e2e/series.spec.ts`; `npm run e2e -- tests/e2e/series.spec.ts --project=chromium`.
|
||
- **기대 결과:** focused/unit와 mock E2E exit 0, legacy 장르 호출 0건, `genreId=0`·생성 `state`·수정 `keyword` payload 0건, 허용 범위 밖 image request 0건이다. server 명령은 `No tests found`가 아니고 Series spec을 실제 실행하며, server 완료 판정 시 필수 개발 fixture path가 skipped 0건이다.
|
||
- **수동 확인:** desktop/tablet에서 장르 선택→image crop→생성→직접 edit 초기화→optional image 교체 수정→soft delete를 확인하고 모바일에는 mutation action이 없음을 확인한다. 실제 개발 API 결과는 mock 결과와 별도로 기록한다.
|
||
|
||
### Task 10.4 Community pagination·timezone 제거
|
||
|
||
**Goal 실행 `P10-T4`:** Community 목록을 OpenAPI pagination object로
|
||
전환하고 `timezone` query 없이 서버 metadata로 page UI를 제어한다.
|
||
|
||
- **시작 조건:** `CommunityPostListResponse={totalCount,page,size,hasNext,items}`가 `implemented`.
|
||
- **Files:** Modify: `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.tsx`, `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`, `src/features/community-posts/tests/community-test-support.ts`, `src/shared/mocks/community-post-fixtures.ts`, `src/shared/mocks/community-post-handlers.ts`, `src/shared/mocks/community-post-mock-store.ts`, `tests/e2e/community.spec.ts`.
|
||
- **Interfaces:** `getCommunityPosts(apiClient,{characterId,page,size}): Promise<PageData<CommunityPostListItem>>`; page state는 response의 `totalCount`, `page`, `size`, `hasNext`, `items`를 그대로 사용.
|
||
- **범위 밖:** Community 검색, detail GET/route.
|
||
|
||
- [x] **RED:** `timezone` 없는 query와 pagination object parse, `hasNext` 기반 다음 page, mutation 후 현재 page 재조회의 실패 test를 작성한다.
|
||
- [x] **GREEN:** adapter·schema·mock store·목록 UI를 서버 metadata 기반으로 갱신한다.
|
||
- [x] **REFACTOR:** 배열 response fallback과 client 추정 종료 규칙을 제거하고 기존 Sheet/cache 흐름 회귀를 확인한다.
|
||
- **실행 명령:** `npm run test:run -- src/features/community-posts`; `npm run e2e:mock -- tests/e2e/community.spec.ts`.
|
||
- **기대 결과:** focused/unit와 mock E2E exit 0, `timezone` query 0건, 응답 metadata와 화면 page/전체 건수 차이 0건.
|
||
- **수동 확인:** desktop/tablet/mobile 목록에서 다음/이전 page와 전체 건수가 서버 값대로 표시되고 Sheet mutation 뒤 현재 page가 유지되는지 확인한다.
|
||
|
||
### Task 10.5 FanTalk 답변 수정·팬 원글 soft delete
|
||
|
||
**Goal 실행 `P10-T5`:** `creatorReplies` 상태를 기준으로 작성/수정을
|
||
분기하고 팬 작성 root soft delete를 모든 viewport에 제공한다.
|
||
|
||
- **시작 조건:** FanTalk DELETE와 reply PUT이 모두 `implemented`.
|
||
- **Files:** Modify: `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.tsx`, `src/features/fan-talks/components/FanTalkListItem.tsx`, `src/features/fan-talks/components/FanTalkReplySheet.tsx`, `src/features/fan-talks/components/FanTalkReplyForm.tsx`, `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`, `src/shared/mocks/fan-talk-fixtures.ts`, `src/shared/mocks/fan-talk-handlers.ts`, `src/shared/mocks/fan-talk-mock-store.ts`, `tests/e2e/fan-talk.spec.ts`, `playwright.config.ts`.
|
||
- **Interfaces:** `updateFanTalkReply(apiClient,{characterId,fanTalkId,replyId,request:{content}}): Promise<FanTalkListItem>`에서 `replyId=creatorReplies[0].fanTalkId`; PUT 성공 `data.fanTalkId`는 수정된 reply row ID이므로 root item ID로 사용하지 않고 현재 목록을 재조회한다. `deleteFanTalk(apiClient,{characterId,fanTalkId}): Promise<null>`.
|
||
- **범위 밖:** 답변 삭제·재활성화, PUT의 `isActive`, 제품 범위에서 제외한 별도 상세/filter/sort와 중복 오류 key.
|
||
|
||
- [x] **RED:** 빈 배열은 POST, non-empty는 PUT, PUT replyId mapping과 `{content}` 전용 request, PUT 성공 response parse 후 목록 재조회, 팬 원글 DELETE body 없음, 한 화면의 중복 submit 차단과 일반 오류 후 재조회의 실패 test를 작성한다. server path에서는 동일 root에 동시 POST 후 재조회한 `creatorReplies`의 활성 답변이 하나인지 검증하되 정확한 오류 status/message key를 assertion하지 않는다.
|
||
- [x] **GREEN:** 답변 수정 form과 삭제 확인 Dialog·API·mock store를 구현하고 server mode `testMatch`에 `fan-talk.spec.ts`를 추가한다.
|
||
- [x] **REFACTOR:** 첫 답변만 수정 대상으로 사용하는 helper를 한곳에 두고 작성·수정·삭제의 pending/error/live feedback을 공통화한다. mock 전용 ID·로그인 helper와 server fixture path를 분리하고 별도 상세/filter/sort·중복 오류 key용 adapter·fixture가 생기지 않았는지 확인한다.
|
||
- **실행 명령:** `npm run test:run -- src/features/fan-talks`; `npm run e2e:mock -- tests/e2e/fan-talk.spec.ts`; `npm run e2e -- tests/e2e/fan-talk.spec.ts --project=chromium`.
|
||
- **기대 결과:** focused/unit와 mock E2E exit 0, 답변 완료 item의 두 번째 POST 0건, PUT path의 `replyId`가 `creatorReplies[0].fanTalkId`, PUT request의 `isActive` 0건, DELETE body 0 bytes다. server 명령은 `No tests found`가 아니고 FanTalk spec을 실제 실행하며, server 완료 판정 시 전용 fixture path가 skipped 0건이고 동시 POST 후 활성 creator reply는 1개다.
|
||
- **수동 확인:** desktop/tablet/mobile에서 답변 작성·수정과 팬 원글 삭제, focus 복귀를 확인한다. 실제 server mode의 수정·재조회와 단일 답변 결과를 mock 증거와 별도로 기록하고 exact 충돌 key는 수집해도 client 분기에 사용하지 않는다.
|
||
|
||
### Task 10.6 Audio·Community 댓글 2단계 관리
|
||
|
||
**Goal 실행 `P10-T6`:** 두 target의 댓글·직접 답글 조회, AI 명의 작성,
|
||
AI 작성분 수정, 작성자 무관 soft delete를 모든 viewport에 제공한다.
|
||
|
||
- **시작 조건:** Audio·Community comment 10개 operation과 관련 schema가 `implemented`.
|
||
- **Files:** Create: `src/features/comments/api/comment-api.ts`, `src/features/comments/model/types.ts`, `src/features/comments/components/CommentThread.tsx`, `src/features/comments/components/CommentForm.tsx`, `src/features/comments/components/CommentItem.tsx`, `src/features/comments/tests/comment-contract.test.ts`, `src/features/comments/tests/comment-thread.test.tsx`, `src/shared/mocks/comment-fixtures.ts`, `src/shared/mocks/comment-handlers.ts`, `src/shared/mocks/comment-mock-store.ts`, `tests/e2e/comments.spec.ts`; Modify: `src/features/audio-contents/pages/AudioContentDetailPage.tsx`, `src/features/audio-contents/model/types.ts`, `src/features/community-posts/components/CommunityPostSheet.tsx`, `src/features/community-posts/model/types.ts`, `src/shared/mocks/handlers.ts`, `playwright.config.ts`.
|
||
- **Interfaces:** `CommentTarget={kind:"audio",characterId,contentId,creatorId}|{kind:"community",characterId,postId,creatorId}`; `CommentRecord={id,writerId,nickname,profileUrl,comment,isSecret,date,replyCount,languageCode?:string|null,donationCan?:number}`; `getRootComments(target,{page,size})`와 `getReplies(target,{commentId,page,size}): Promise<{totalCount:number,items:CommentRecord[]}>`; Audio create request는 `{comment,parentId?:number|null,isSecret:boolean,languageCode?:string|null}`, Community create request는 `{comment,parentId?:number|null,isSecret:boolean}`, update request는 `{comment:string}`, `createComment/updateComment/deleteComment: Promise<null>`; `canEdit = comment.writerId === target.creatorId`.
|
||
- **범위 밖:** 3단계 답글, 팬 댓글 수정, delete cascade, comment status/filter.
|
||
|
||
- [x] **RED:** target별 path/schema, root/replies GET의 `page`·`size`와 `totalCount` parse, root/`parentId`, target별 create request 차이, `writerId===creatorId` 수정 권한, 작성자 무관 DELETE, UTC 표시의 실패 test를 작성한다.
|
||
- [x] **GREEN:** target adapter와 2단계 thread UI·mock handler/store를 구현하고 Audio 상세·Community Sheet에 연결한다. mock·server mode `testMatch`에 `comments.spec.ts`를 추가한다.
|
||
- [x] **REFACTOR:** target별 DTO 차이는 adapter에서 정규화하고 Page/Sheet는 target 식별자와 `creatorId`만 전달하도록 정리한다. mock 전용 ID·로그인 helper가 server path에서 사용되지 않도록 분리한다.
|
||
- **실행 명령:** `npm run test:run -- src/features/comments src/features/audio-contents src/features/community-posts`; `npm run e2e:mock -- tests/e2e/comments.spec.ts`; `npm run e2e -- tests/e2e/comments.spec.ts --project=chromium`.
|
||
- **기대 결과:** focused/unit와 지원 project mock E2E exit 0, 3단계 작성 UI/request 0건, 팬 댓글 PUT 0건, 팬·AI 댓글 DELETE 모두 body 없음이다. server 명령은 `No tests found`가 아니고 Comments spec을 실제 실행하며, server 완료 판정 시 Audio·Community 전용 fixture path가 skipped 0건이다.
|
||
- **수동 확인:** Audio 상세와 Community Sheet에서 root/답글 작성·AI 작성분 수정·팬 댓글 삭제를 keyboard-only와 320px에서 확인한다. 같은 흐름의 실제 개발 API 결과를 mock 증거와 별도로 기록한다.
|
||
|
||
### Task 10.7 가격·공통 오류·파일 정책 종결
|
||
|
||
**Goal 실행 `P10-T7`:** `EXT-009~011`을 공통·도메인 schema와 오류
|
||
처리, 기존 파일 검증 회귀와 server 확인 기준에 반영한다.
|
||
|
||
- **시작 조건:** `EXT-009`의 `0..99999`, `EXT-010`의 client/backend 검증 경계, `EXT-011`의 공통 오류 envelope와 미정의 오류 문구가 확정됨.
|
||
- **Files:** Create: `src/shared/validation/can-price.ts`, `src/shared/validation/can-price.test.ts`; Modify: `src/features/audio-contents/schemas/audio-content-schema.ts`, `src/features/audio-contents/components/AudioContentForm.tsx`, `src/features/audio-contents/api/upload-audio-content.ts`, `src/features/audio-contents/tests/audio-form.test.tsx`, `src/features/audio-contents/tests/audio-contract.test.ts`, `src/features/audio-contents/tests/audio-upload.test.ts`, `src/features/community-posts/model/types.ts`, `src/features/community-posts/components/CommunityPostForm.tsx`, `src/features/community-posts/tests/community-contract.test.ts`, `src/features/community-posts/tests/community-form.test.tsx`, `src/shared/api/api-error.ts`, `src/shared/api/client.ts`, `src/shared/api/__tests__/client.test.ts`, `src/shared/validation/file-media-policy.test.ts`, `tests/e2e/error-mapping.spec.ts`.
|
||
- **Interfaces:** `CAN_PRICE_MAX=99_999`; `canPriceSchema`는 `0..99999` 정수; `UNKNOWN_API_ERROR_MESSAGE="알 수 없는 오류가 발생했습니다."`. 유효한 `ApiErrorResponse`의 비어 있지 않은 server `message`, `status`, `errorProperty`는 보존하고 network·JSON parse·envelope 불일치·빈 message만 공통 문구의 `ApiError`로 정규화한다.
|
||
- **범위 밖:** 도메인별 미제공 message key 추정, client container/codec 판정, image 비율·crop의 backend 재검증.
|
||
|
||
- [x] **RED:** Audio create/update와 Community create schema·form에서 `0`, `99999` 허용, `-1`, `100000`, 소수 거부 실패 test를 먼저 작성한다.
|
||
- [x] **RED:** fetch와 XHR upload의 non-empty server message 보존, network·malformed/empty error의 공통 문구 변환 실패 test를 작성한다.
|
||
- [x] **GREEN:** 공통 가격 schema를 두 도메인 request/form에 적용하고 공통 fetch·XHR 오류 정규화를 최소 구현한다.
|
||
- [x] **REFACTOR:** 기존 image/audio 크기·MIME·GIF·no-upscale 경계 test를 재사용해 `FILE-001~015` 회귀를 확인하고 도메인별 오류 key 분기를 추가하지 않는다.
|
||
- **실행 명령:** `npm run test:run -- src/shared/validation/can-price.test.ts src/shared/validation/file-media-policy.test.ts src/shared/api/__tests__/client.test.ts src/features/audio-contents src/features/community-posts`; `npm run e2e:mock -- tests/e2e/error-mapping.spec.ts`.
|
||
- **기대 결과:** 모든 명령 exit 0, `100000` 이상 또는 음수·소수 price request 0건, 미정의 오류 문구 불일치 0건, 기존 file boundary 회귀 0건.
|
||
- **수동 확인:** Audio 생성·수정과 Community 생성에서 가격 경계·오류 focus를 확인한다. 실제 개발 API 통합 확인에서는 backend가 검증 가능한 image/audio 용량·MIME 위반 multipart가 성공 처리되지 않고, 정확한 미제공 status/message key에 따른 client 분기가 생기지 않았는지 확인한다.
|
||
|
||
### Task R10.1 — 공통 오류 fallback migration 종결
|
||
|
||
**Goal 실행 `P10-R1`:** fetch/XHR의 미정의 오류를 공통 문구로 통일하고 현재 전체 unit Gate의 6개 stale assertion을 복구한다.
|
||
|
||
- **연결 리뷰:** [Phase 10 재리뷰](./reviews/review-phase-10-20260729.md) — `REV-P10-001`
|
||
- **시작 조건:** `EXT-011`, `UNKNOWN_API_ERROR_MESSAGE`; non-empty server message 보존.
|
||
- **Files:** Modify: `src/features/audio-contents/api/upload-audio-content.ts`, `src/features/audio-contents/tests/audio-upload.test.ts`, `src/features/auth/tests/auth-api.test.ts`, `src/app/App.protected-errors.test.tsx`, `vite.config.ts`, 필요 시 `src/shared/api` tests; Test: 동일 경로와 `tests/e2e/error-mapping.spec.ts`.
|
||
- **Interfaces:** network·JSON parse·envelope mismatch·empty message → `알 수 없는 오류가 발생했습니다.`; valid non-empty message/status/errorProperty 보존.
|
||
- **범위 밖:** 도메인별 미제공 message key, 401 session 처리(`P1-R6/P4-R2`).
|
||
|
||
- [x] **RED:** XHR 2xx malformed JSON/envelope가 과거 문구를 반환하는 실패 test와 전체 unit 6개 stale assertion을 현재 정책 기준으로 고정한다.
|
||
- [x] **GREEN:** XHR success parse fallback을 공통 상수로 변경하고 App/auth assertion을 최신 정책으로 정렬한다.
|
||
- [x] **REFACTOR:** fetch/XHR 오류 matrix 이름과 fixture를 같은 용어로 정리하고 full unit/error E2E를 실행한다.
|
||
- **실행 명령:** `npm run test:run -- src/shared/api src/features/auth src/features/audio-contents/tests/audio-upload.test.ts src/app/App.protected-errors.test.tsx`; `npm run test:run`; `npm run e2e:mock -- tests/e2e/error-mapping.spec.ts`.
|
||
- **기대 결과:** exit 0, 전체 unit 0 failure, 미정의 오류 문구 불일치 0건.
|
||
- **수동 확인:** 보호 route와 upload에서 malformed response의 한국어 공통 안내를 확인한다.
|
||
|
||
### Task R10.2 — PRD OpenAPI 2.3.0 현재 상태 정합성 복구
|
||
|
||
**Goal 실행 `P10-R2`:** PRD의 현재형 API metadata와 Community pagination 설명을 OpenAPI 2.3.0 실제 집계와 일치시킨다.
|
||
|
||
- **연결 리뷰:** [Phase 10 재리뷰](./reviews/review-phase-10-20260729.md) — `REV-P10-002`
|
||
- **시작 조건:** OpenAPI actual `3.1.0`, document `2.3.0`, 25 paths, 37 operations, Community pagination metadata 제공.
|
||
- **Files:** Modify: `docs/20260725_AI캐릭터관리자웹/prd.md`, `docs/20260725_AI캐릭터관리자웹/plan-task.md`, `docs/20260725_AI캐릭터관리자웹/reviews/review-phase-10-20260729.md`; Test: 없음.
|
||
- **Interfaces:** 현재 상태 문장만 정정하며 2.0.0 당시 Decision/Progress 기록은 보존한다.
|
||
- **범위 밖:** OpenAPI JSON 변경, 과거 검증 수치 재작성.
|
||
|
||
- [x] **TDD 예외:** 문서 정합성 Task이므로 `jq` actual 집계와 stale 현재형 문구 검색을 RED 대체 검증으로 기록한다.
|
||
- [x] 현재 계약 version/path/operation, 최종 수정일과 Community metadata 제공 상태를 정정한다.
|
||
- [x] 과거 2.0.0 이력 보존과 현재형 stale 문구 0건, Markdown link·diff를 검증한다.
|
||
- **실행 명령:** OpenAPI `jq` 집계; `rg -n 'version .2\\.0\\.0|15개 path|23개 operation|종료 metadata 계약이 제공되기 전' docs/20260725_AI캐릭터관리자웹/prd.md`; `git diff --check -- docs/20260725_AI캐릭터관리자웹`.
|
||
- **기대 결과:** current-state stale 검색 exit 1, actual 집계 `2.3.0/25/37`, diff check exit 0.
|
||
- **수동 확인:** PRD 현재 상태와 Decision Log 과거 이력이 구분되는지 확인한다.
|
||
|
||
### Task R10.3 — 계획·과거 Phase 10 리뷰 현재 상태 동기화
|
||
|
||
**Goal 실행 `P10-R3`:** plan 상단·구현 완료 정의와 과거 Phase 10 리뷰의 결론을 최신 Gate 정책 및 신규 리뷰 결과와 일치시킨다.
|
||
|
||
- **연결 리뷰:** [Phase 10 현재 리뷰](./reviews/phase10-openapi-follow-up.md) — `REV-P10-003`
|
||
- **시작 조건:** 2026-07-30 `P10-GATE 자동 범위 정정 및 수동 QA 전환` 기록과 이번 리뷰의 신규 회귀 Task 상태.
|
||
- **완료 증거:** stale 현재 문구 검색, 현재 상태 갱신, 과거 기록 보존, Markdown link/diff 검증 통과.
|
||
- **Files:** Modify: `docs/20260725_AI캐릭터관리자웹/plan-task.md`, `docs/20260725_AI캐릭터관리자웹/reviews/review-phase-10-20260729.md`; Test: 문서 검색.
|
||
- **Interfaces:** 자동 Gate와 수동 QA 대기를 구분하고, 신규 회귀 Task가 남아 있는 동안 최종 완료를 주장하지 않는다.
|
||
- **범위 밖:** 과거 server 실패 기록 삭제, 수동 QA를 자동 완료로 변경, 애플리케이션 코드 변경.
|
||
|
||
- [x] **TDD 예외:** 상단 상태, §6 설명, 과거 Phase 10 리뷰 결론의 stale 현재 문구를 검색해 RED 대체 증거로 기록한다.
|
||
- [x] 상단 상태와 §6 설명을 신규 Task 완료·자동 Gate/수동 QA 구분에 맞게 갱신하고 기존 날짜별 검증 기록은 보존한다.
|
||
- [x] 과거 Phase 10 리뷰에 `P10-R1~R2` 수정 완료와 Gate 정책 정정을 누적하고 새 리뷰 링크·diff를 검증한다.
|
||
- **실행 명령:** `rg -n '2026-07-29 Phase별 재''리뷰|server E2E가 로그인 fixture에 막''혀|P10-GATE.*완료 아''님' docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/reviews/review-phase-10-20260729.md`; `git diff --check -- docs/20260725_AI캐릭터관리자웹`.
|
||
- **기대 결과:** 과거 사실 보존, 현재 Gate·수동 QA·신규 회귀 Task 상태 불일치 0건.
|
||
- **수동 확인:** plan 상단, §6, Phase 10 Gate, Decision Log, 과거/현재 리뷰 결론이 같은 현재 상태를 설명하는지 확인한다.
|
||
|
||
**Progress — 2026-07-30 P10-R3 완료**
|
||
|
||
- **RED 대체:** stale 현재 상태 검색이 plan 상단의 2026-07-29 회귀 대기 문구, §6의 server E2E 차단 문구, 과거 Phase 10 리뷰의 Gate 미완료 결론을 검출했다.
|
||
- **GREEN:** plan 상단과 §6을 자동 Gate 정리 완료·실제 개발 API 수동 QA 대기로 갱신하고, `review-phase-10-20260729.md`에 `P10-R1~R2`와 Gate 정책 정정 검증을 누적했다.
|
||
- **REFACTOR:** `phase10-openapi-follow-up.md`의 `REV-P10-003` 상태·최종 결론도 수정 완료로 정렬했다. 2026-07-29 당시 실패·timeout 기록은 삭제하지 않았다.
|
||
|
||
### Task R10.4 — FanTalk PUT 응답·생성 reply ID 계약 복구
|
||
|
||
**Goal 실행 `P10-R4`:** FanTalk reply PUT의 레거시 응답 shape를 별도 schema로 소비하고 POST 뒤 목록의 `creatorReplies[].fanTalkId`를 실제 `replyId`로 유지한다.
|
||
|
||
- **연결 리뷰:** [Phase 10 현재 리뷰](./reviews/phase10-openapi-follow-up.md) — `REV-P10-004`, `REV-P10-005`
|
||
- **시작 조건:** OpenAPI `FanTalkReplyUpdateApiResponse.data=FanTalkListItem`, 해당 `fanTalkId`는 수정 reply row ID라는 설명, `FANTALK-004`, `FANTALK-009~010`.
|
||
- **완료 증거:** 실제 PUT response shape parse 실패와 POST→목록→PUT ID 실패 test, client/mock 최소 수정, FanTalk focused unit·mock E2E·정적 Gate 통과.
|
||
- **Files:** Modify: `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/tests/fan-talk-contract.test.ts`, `src/features/fan-talks/tests/fan-talk-reply.test.tsx`, `src/shared/mocks/fan-talk-mock-store.ts`, `src/shared/mocks/fan-talk-handlers.ts`, `tests/e2e/fan-talk.spec.ts`.
|
||
- **Interfaces:** POST 성공은 `FanTalkReplyResponse.replyId`, PUT 성공은 `FanTalkListItem` shape로 분리한다. 목록 `creatorReplies[].fanTalkId`와 PUT path `replyId`는 POST가 반환한 reply row ID를 사용한다.
|
||
- **범위 밖:** FanTalk 답변 삭제, 별도 상세/filter/sort, PUT의 `isActive` UI, backend 응답 변경.
|
||
|
||
- [x] **RED:** OpenAPI PUT data shape가 현재 POST schema에서 거부되는 contract test와 POST `replyId=9001` 뒤 재조회·수정이 `/replies/9001`을 사용해야 하는 mock E2E를 작성한다.
|
||
- [x] **GREEN:** PUT 전용 response schema/type을 추가하고 mock create list mapping과 update response를 실제 계약에 맞춘다.
|
||
- [x] **REFACTOR:** POST/PUT request는 공통 `{content}`를 유지하되 서로 다른 response type을 이름과 fixture에서 명확히 분리한다.
|
||
- **실행 명령:** `npm run test:run -- src/features/fan-talks src/shared/mocks`; `npm run e2e:mock -- tests/e2e/fan-talk.spec.ts --project=chromium`; `npm run typecheck`; `npm run lint`; `npm run build`.
|
||
- **기대 결과:** 모든 명령 exit 0, OpenAPI PUT data parse 성공, POST→동일 item PUT path의 reply ID 일치, 성공한 PUT의 client parse 오류 0건.
|
||
- **수동 확인:** 실제 개발 API에서 기존 답변 PUT과 새 답변 POST 후 재진입 PUT을 각각 실행해 성공 안내·목록 refetch·path ID를 확인한다.
|
||
|
||
**P10-R4 수정 검증 기록 (2026-07-30):**
|
||
|
||
- RED: `npm run test:run -- src/features/fan-talks/tests/fan-talk-contract.test.ts`는 OpenAPI PUT data shape가 기존 POST response schema에서 거부되어 실패했고, `npm run e2e:mock -- tests/e2e/fan-talk.spec.ts --project=chromium`은 POST 뒤 같은 item 수정 path가 `/replies/9001`이 아니어서 실패했다.
|
||
- GREEN: PUT 전용 `FanTalkReplyUpdateResponse` schema/type을 `FanTalkListItem` 기반으로 추가하고, mock store가 POST `replyId`를 목록 `creatorReplies[].fanTalkId`에 보존하며 PUT 응답도 OpenAPI shape로 반환하게 했다.
|
||
- REFACTOR/회귀: `npm run test:run -- src/features/fan-talks/tests/fan-talk-contract.test.ts src/features/fan-talks/tests/fan-talk-reply.test.tsx`는 2 files / 10 tests passed, `npm run e2e:mock -- tests/e2e/fan-talk.spec.ts --project=chromium`은 7 passed였다. `npm run test:run -- src/features/fan-talks src/shared/mocks`는 재실행 기준 9 files / 35 tests passed, `npm run typecheck`는 exit 0이었다.
|
||
|
||
### Task R10.5 — 완료된 후속 계약의 현재 문서·dead scaffold 정리
|
||
|
||
**Goal 실행 `P10-R5`:** README와 미사용 admin page에서 완료된 Phase 3·10 기능을 아직 대기·구현 대상으로 설명하는 현재형 문구를 제거한다.
|
||
|
||
- **연결 리뷰:** [Phase 10 현재 리뷰](./reviews/phase10-openapi-follow-up.md) — `REV-P10-006`
|
||
- **시작 조건:** OpenAPI 2.3.0의 lookup/FanTalk/Comments/Community operation `implemented`, `P10-T1~T7` 완료, `AiCharactersPage` production 사용처 0건.
|
||
- **완료 증거:** stale 문구와 dead export 사용처 검색, 현재 backend constraint 갱신, dead component 제거, 문서·typecheck·lint·build 검증.
|
||
- **Files:** Modify: `README.md`, `src/app/admin-pages.tsx`; 필요 시 `docs/20260725_AI캐릭터관리자웹/plan-task.md`의 현재 상태 설명만 보완.
|
||
- **Interfaces:** 실제 개발 API 수동 QA 대기와 제품 범위 밖 FanTalk 상세/filter/sort는 유지하고, 이미 구현된 v2 lookup·Phase 10 기능을 대기로 표시하지 않는다.
|
||
- **범위 밖:** 과거 날짜별 Decision/Progress 삭제, OpenAPI JSON 변경, 새 status page·문서 생성.
|
||
|
||
- [x] **TDD 예외:** `rg`로 README의 legacy lookup/Phase 10 구현 대상 문구와 미사용 `AiCharactersPage` export를 RED 대체 증거로 기록한다.
|
||
- [x] README Known Backend Constraints를 현재 계약과 수동 QA 대기 기준으로 갱신하고 미사용 Phase 3 placeholder component를 삭제한다.
|
||
- [x] 과거 구현 이력은 보존한 채 stale 현재형 문구 0건과 문서 링크·정적 Gate를 검증한다.
|
||
- **실행 명령:** `rg -n 'legacy 후보|Phase 10 구현 대상|Phase 3에서|AiCharactersPage' README.md src/app docs/20260725_AI캐릭터관리자웹/plan-task.md`; `npm run typecheck`; `npm run lint`; `npm run build`; `git diff --check -- README.md src/app/admin-pages.tsx docs/20260725_AI캐릭터관리자웹/plan-task.md`.
|
||
- **기대 결과:** 과거 기록을 제외한 stale 현재형 문구와 미사용 export 0건, 모든 명령 exit 0.
|
||
- **수동 확인:** README만 읽어 implemented 범위, 제품 제외 범위, 실제 개발 API 수동 QA 대기를 구분할 수 있는지 확인한다.
|
||
|
||
**P10-R5 수정 검증 기록 (2026-07-30):**
|
||
|
||
- RED 대체: `rg -n 'legacy 후보|Phase 10 구현 대상|Phase 3에서|AiCharactersPage' README.md src/app docs/20260725_AI캐릭터관리자웹/plan-task.md`가 README 현재형 문구와 미사용 `AiCharactersPage` placeholder를 검출했다.
|
||
- GREEN: README Known Backend Constraints를 OpenAPI 2.3.0 implemented 범위와 실제 개발 API 수동 QA 대기 기준으로 정리했고, `src/app/admin-pages.tsx`의 미사용 `AiCharactersPage` export를 삭제했다.
|
||
- REFACTOR: 과거 날짜별 legacy 후보 이력과 review 관찰 내용은 보존하고, 현재 README에는 이전 후보 endpoint 표현만 남겼다.
|
||
- 검증: `rg -n 'legacy 후보|Phase 10 구현 대상|Phase 3에서|AiCharactersPage' README.md src/app`는 no matches, `npm run test:run -- src/app/App.test.tsx src/app/App.protected-errors.test.tsx`는 2 files / 18 tests passed, `npm run typecheck`, `npm run lint`, `npm run build`는 모두 exit 0이었다. `src/app/admin-pages.tsx` LSP diagnostics는 오류 0건, 대상 문서·파일 `git diff --check`는 no output이었다.
|
||
|
||
### Task R10.6 — 구현 완료 범위의 현재 문서 상태 동기화
|
||
|
||
**Goal 실행 `P10-R6`:** PRD·plan·현재 Phase 리뷰의 현재형 상태를 완료된 Phase 10 구현 및 후속 회귀 현황과 맞추고 과거 실행 기록은 보존한다.
|
||
|
||
- **연결 리뷰:** [Phase 10 현재 리뷰](./reviews/phase10-openapi-follow-up.md) — `REV-P10-007`
|
||
- **시작 조건:** `P10-T1~T7`, `P10-R3~R5` 완료 기록과 2026-07-31 Phase별 재점검 결과.
|
||
- **완료 증거:** stale 현재형 문구 검색, PRD Overview·EXT 영향·plan 수용 기준·현재 review metadata/summary 최소 수정, Markdown link와 targeted diff 검증 통과.
|
||
- **Files:** Modify: `docs/20260725_AI캐릭터관리자웹/prd.md`, `docs/20260725_AI캐릭터관리자웹/plan-task.md`, `docs/20260725_AI캐릭터관리자웹/reviews/phase1-platform-auth-shared-ui.md`, `phase5-series-management.md`, `phase8-comments.md`, 필요 시 다른 현재 Phase 리뷰의 상태 표기.
|
||
- **Interfaces:** 완료된 v2 lookup·Series CRUD·Community pagination·FanTalk PUT/DELETE·Comments를 미래 구현으로 쓰지 않는다. 실제 개발 API 수동 QA와 2026-07-31 신규 미완료 Task는 계속 구분한다. 과거 Decision/Progress/실패 기록은 삭제하지 않는다.
|
||
- **범위 밖:** OpenAPI JSON 변경, 애플리케이션 코드 수정, 과거 기록 재작성, 수동 QA를 완료로 표시.
|
||
|
||
- [x] **TDD 예외:** `rg`로 PRD의 “코드를 수정하지 않는다” 및 EXT의 “Phase 10에서 구현”, plan 현재 수용 기준의 미래형, 현재 리뷰 metadata/summary 불일치를 RED 대체 증거로 기록한다.
|
||
- [x] PRD·plan의 현재 상태와 현재 Phase 리뷰의 요약/종료 판정만 최신화하고 날짜별 이력은 보존한다.
|
||
- [x] Markdown link, stale 현재형 문구, `git diff --check`를 검증하고 수정 범위를 문서에 기록한다.
|
||
- **실행 명령:** `rg -n '애플리케이션 코드는 이번 단계에서 수정하지 않는다|Phase 10에서 .*구현|신규 회귀 수정 대기' docs/20260725_AI캐릭터관리자웹`; Markdown link 검사; `git diff --check -- docs/20260725_AI캐릭터관리자웹`.
|
||
- **기대 결과:** 과거 기록을 제외한 stale 현재형 문구와 상태 불일치 0건, 링크 오류 0건, diff 오류 0건.
|
||
- **수동 확인:** PRD Overview·EXT 표, plan §5.1·완료 정의, 각 현재 Phase 리뷰만 읽어 완료 기능·신규 회귀 Task·실제 server QA 대기를 구분할 수 있는지 확인한다.
|
||
|
||
**P10-R6 수정 검증 기록 (2026-07-31):**
|
||
|
||
- RED 대체: `rg -n "애플리케이션 코드는 이번 단계에서 수정하지 않는다|Phase 10에서 .*구현|신규 회귀 수정 대기|수정 대기|P10-GATE.*완료 아님" docs/20260725_AI캐릭터관리자웹`가 PRD Overview, EXT 영향 문구, plan §3.1, Phase 1·8 리뷰 metadata, Phase 8 발견 요약, Phase 10 현재 리뷰 결론의 불일치를 검출했다.
|
||
- GREEN: PRD Overview와 EXT 영향은 완료된 Phase 10 mock/client 구현과 실제 개발 API 수동 QA를 구분하도록 정정했다. plan §3.1은 Character/FanTalk/Comments의 현재 완료 범위로 갱신했고, Phase 1·8 리뷰 metadata와 Phase 8 요약 상태를 수정 완료로 맞췄다. 과거 Decision/Progress/실패 기록은 보존했다.
|
||
- 검증: 현재 상태 대상 검색은 stale 현재형 문구 0건이었다. `npm run typecheck`, `git diff --check -- docs/20260725_AI캐릭터관리자웹`는 모두 exit 0이었다.
|
||
|
||
### Task R10.7 — Phase별 현재 리뷰 상태·완료 이력 재동기화
|
||
|
||
**Goal 실행 `P10-R7`:** `P9-R8` 완료 뒤 Phase 0~10 현재 리뷰 metadata·요약·plan 상단 상태를 실제 수정 완료 이력과 외부 수동 QA 대기 상태에 다시 맞춘다.
|
||
|
||
- **연결 리뷰:** [Phase 10 현재 리뷰](./reviews/phase10-openapi-follow-up.md) — `REV-P10-008`
|
||
- **시작 조건:** `P9-R8` 완료, `P0-R3`, `P1-R10~R11`, `P4-R7`, `P8-R4`, `P9-R7`의 기존 수정 완료 기록과 이번 Phase별 재리뷰 기록 보존.
|
||
- **완료 증거:** 현재 review metadata/최종 결론 불일치 검색, Phase 0~10 current status 최소 정정, 상대 링크와 plan 상태·검증 기록 점검.
|
||
- **Files:** Modify: `docs/20260725_AI캐릭터관리자웹/plan-task.md`, `docs/20260725_AI캐릭터관리자웹/reviews/phase0-project-foundation.md`, `phase1-platform-auth-shared-ui.md`, `phase4-audio-content.md`, `phase8-comments.md`, `phase9-cross-cutting-quality.md`, 필요 시 같은 `reviews/phase*.md`의 current metadata/summary.
|
||
- **Interfaces:** 각 review의 과거 판정·실패·수정 기록은 삭제하지 않는다. 상단 `리뷰 상태`, 최신 차수 결론과 plan 상단 상태만 `P9-R8` 완료·실제 개발 API 수동 QA 대기를 구분하도록 정렬한다.
|
||
- **범위 밖:** OpenAPI·제품 코드 변경, 과거 기록 덮어쓰기, 실제 server QA를 완료로 표시, 기존 리뷰 파일 이름 변경.
|
||
|
||
- [x] **TDD 예외:** 문서 metadata 정합성 Task이므로 Phase별 상단 상태와 최신 `Phase 결론`·`수정 후 검증`을 대조한 실패 목록을 RED 대체 증거로 기록했다.
|
||
- [x] `P9-R8` 완료 상태와 실제 개발 API 수동 QA 대기를 반영해 current metadata·plan 상단 상태만 최소 정정했다.
|
||
- [x] Phase별 상대 링크, 현재 상태 검색, Markdown 구조와 `git diff --check`를 검증하고 결과를 누적했다.
|
||
- **실행 명령:** `for f in docs/20260725_AI캐릭터관리자웹/reviews/phase*.md; do sed -n '1,16p' "$f"; tail -n 40 "$f"; done`; `rg -n '신규 회귀 수정 필요|교차 회귀 대기|P9-R8|P10-R7' docs/20260725_AI캐릭터관리자웹`; Markdown 상대 링크 검사; `git diff --check -- docs/20260725_AI캐릭터관리자웹`.
|
||
- **기대 결과:** 과거 이력을 제외한 current status와 최신 결론 불일치 0건, `P9-R8` 완료와 실제 server QA 대기 구분, 링크·diff 오류 0건.
|
||
- **수동 확인:** 각 Phase 리뷰의 첫 화면과 최신 차수만 읽어 확정 finding, 수정 완료 여부, 남은 수동 QA를 오해 없이 구분할 수 있는지 확인한다.
|
||
|
||
**P10-R7 수정 검증 기록 (2026-07-31):**
|
||
|
||
- RED 대체: `for f in docs/20260725_AI캐릭터관리자웹/reviews/phase*.md; do sed -n '1,16p' "$f"; tail -n 25 "$f"; done`로 Phase 0·1·4·8·9의 상단 `리뷰 상태`가 최신 수정 완료 결론과 어긋나거나 `P9-R8` 완료 전 상태를 유지하는 것을 확인했다.
|
||
- GREEN: plan 상단 상태를 `P9-R8`·`P10-R7` 보완 완료와 실제 개발 API 수동 QA 대기로 정리했다. Phase 0·1·4·8·9 review metadata는 최신 수정 완료 상태로 맞췄고, Phase 9·10 현재 리뷰에는 `P9-R8`·`P10-R7` 수정 완료 판정과 검증 기록을 누적했다. 과거 판정·실패·수정 기록은 삭제하지 않았다.
|
||
- 검증: `rg -n '^\| 리뷰 상태 \|.*(수정 필요|교차 회귀 대기)|^- \*\*최종 결론:\*\*.*수정 필요|^- \*\*남은 항목:\*\*.*P[0-9]+-R' docs/20260725_AI캐릭터관리자웹/reviews/phase*.md`와 `rg -n '^\| 상태 \|.*P[0-9]+-R' docs/20260725_AI캐릭터관리자웹/plan-task.md`는 current metadata/latest conclusion 불일치 0건이었다. `rg -n '\./reviews/[^)]+' docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/prd.md`로 상대 링크 형식을 확인했고, `git diff --check -- docs/20260725_AI캐릭터관리자웹`는 exit 0이었다.
|
||
|
||
### Task R10.8 — 완료된 Phase 10 후속 범위의 현재형 설명 정렬
|
||
|
||
**Goal 실행 `P10-R8`:** plan의 Phase 지도·현재 상태·수용 증거·Tech Stack과 Phase 10 리뷰 마지막 결론을 완료된 `P10-T1~T7`, `P1-R12`, `P4-R8`과 현재 설치 구성에 맞게 바로잡는다.
|
||
|
||
- **연결 리뷰:** [Phase 10 현재 리뷰](./reviews/phase10-openapi-follow-up.md) — `REV-P10-009`
|
||
- **시작 조건:** `P1-R13~R15`, `P6-R4`, `P9-R9~R10` 완료 및 리뷰 상태 갱신, `P10-T1~T7`, `P1-R12`, `P4-R8` 완료 기록과 2026-07-31 문서 기준 재리뷰의 stale 현재형 검색 결과.
|
||
- **완료 증거:** FanTalk 수정·Audio UTC·Series CRUD·Community pagination·file policy를 미래형으로 쓰는 현재 설명 0건, 미설치 dependency를 Tech Stack으로 표시한 항목 0건, Phase 10 리뷰 최신 남은 Task 불일치 0건, link·diff 검증 통과.
|
||
- **Files:** Modify: `docs/20260725_AI캐릭터관리자웹/plan-task.md`, `docs/20260725_AI캐릭터관리자웹/reviews/phase10-openapi-follow-up.md`; Read: `package.json`; Test: 없음.
|
||
- **Interfaces:** 과거 Phase 실행 당시의 계약 대기·실패·Decision/Progress는 삭제하지 않고, 현재 상태를 설명하는 상단 표·계획·최신 결론만 정정한다.
|
||
- **범위 밖:** 제품 코드·OpenAPI 변경, 과거 이력 재작성, 미사용 dependency 설치, 실제 개발 API 수동 QA를 완료로 표시.
|
||
|
||
- [x] **TDD 예외 사유:** 문서 현행화 Task이며 제품 동작 test로 의미 있는 RED를 만들 수 없다. stale 현재형·dependency 검색을 RED 대체 증거로 남긴다.
|
||
- [x] Phase 지도 행 7, §3.1 Phase 4~6, §5.1 Audio·Community·FanTalk·File 행, Tech Stack을 현재 완료 범위와 `package.json`에 맞게 최소 정정한다.
|
||
- [x] Phase 10 현재 리뷰 최신 남은 항목에서 이미 완료된 `P1-R12`, `P4-R8`을 제거하고 신규 회귀 Task·실제 서버 QA를 구분한다.
|
||
- [x] stale 검색·dependency 대조·link·diff 대체 검증 결과를 Progress와 Phase 10 리뷰에 누적한다.
|
||
- **대체 검증 방법:** current-state 특정 문구 검색, `package.json` dependency 대조, Markdown 상대 link 검사, targeted `git diff --check`.
|
||
- **실행 명령:** `rg -n 'detail/edit/filter는 계약 대기|migration 필요|후속 구현|parser/UI 갱신 필요|P10-T[2457]에서' docs/20260725_AI캐릭터관리자웹/plan-task.md`; `node -e "const p=require('./package.json'); console.log(Object.keys({...p.dependencies,...p.devDependencies}).sort().join('\\n'))"`; Markdown link 검사; `git diff --check -- docs/20260725_AI캐릭터관리자웹`.
|
||
- **기대 결과:** 과거 이력을 제외한 stale 현재형 0건, 현재 Tech Stack·dependency 불일치 0건, 최신 남은 Task 불일치 0건, link·diff 오류 0건.
|
||
- **수동 확인:** plan 상단·§3.1·§5.1과 Phase 10 리뷰 최신 차수만 읽어 완료 기능, 신규 회귀 Task, 실제 server QA 대기를 구분할 수 있는지 확인한다.
|
||
|
||
**P10-R8 수정 검증 기록 (2026-07-31):**
|
||
|
||
- RED 대체: `rg -n 'detail/edit/filter는 계약 대기|migration 필요|후속 구현|parser/UI 갱신 필요|P10-T[2457]에서|date-fns|dnd-kit|Lucide|Axios|React Router|React Hook Form|shadcn' docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/reviews/phase10-openapi-follow-up.md`가 현재형 stale 문구와 미설치 dependency 표시를 검출했다.
|
||
- GREEN: plan 상단 Tech Stack을 `package.json` 설치 구성과 현재 구현 방식(native history, fetch/XHR)에 맞췄고, Phase 지도 FanTalk 행과 §3.1 Phase 4~6을 완료된 Phase 10 범위와 실제 개발 API 수동 QA 대기로 정정했다. 과거 Progress와 review 근거 문장은 이력으로 보존했다.
|
||
- 검증: `node -e "const p=require('./package.json'); console.log(Object.keys({...p.dependencies,...p.devDependencies}).sort().join('\\n'))"`로 설치 dependency를 대조했다. current-state stale 검색, targeted `git diff --check`, Phase 10 리뷰 상태 검색을 통과했다.
|
||
|
||
### Task R10.9 — Goal 실행형 계획 필수 section 복구
|
||
|
||
**Goal 실행 `P10-R9`:** 기존 5,000여 행의 실행 이력을 보존하면서 `goal-plan.md`가 요구하는 목표, 현재 상태, 포함·제외, 기술 제약, Phase, 실행 순서·의존성, 변경 금지, 의사결정·중단, Progress, Decision Log, 발견된 문제, 최종 보고 형식을 명시적 section으로 찾을 수 있게 한다.
|
||
|
||
- **연결 리뷰:** [Phase 10 현재 리뷰](./reviews/phase10-openapi-follow-up.md) — `REV-P10-010`
|
||
- **시작 조건:** `P10-R8` 완료, `docs/agent-guide/goal-plan.md` §3의 필수 12 section과 현재 plan의 최상위 heading 7개 대조.
|
||
- **완료 증거:** 필수 section 12개의 명시적 heading/navigation 존재, 기존 Task·Decision·Progress 이력 보존, 현재 신규 Task·중단 규칙·최종 보고 형식 연결, Markdown 구조·link·diff 검증 통과.
|
||
- **Files:** Modify: `docs/20260725_AI캐릭터관리자웹/plan-task.md`; Read: `docs/agent-guide/goal-plan.md`, `docs/sample/sample-plan-task.md`; Test: 없음.
|
||
- **Interfaces:** 과거 실행 기록을 삭제·압축·재번호화하지 않고, 필수 section에서 기존 신뢰할 수 있는 본문 위치와 현재 상태를 연결한다. 추가 보완은 새 이력으로만 남긴다.
|
||
- **범위 밖:** 제품 코드·OpenAPI·PRD 변경, 과거 Task 체크박스 재판정, 실제 서버 QA 완료 표시, 문서 전체 재작성.
|
||
|
||
- [x] **TDD 예외 사유:** 문서 구조 보완이며 제품 test는 확인 대상이 아니다. 필수 heading inventory 부족을 RED 대체 증거로 남긴다.
|
||
- [x] 가이드의 필수 12 section을 현재 본문과 mapping하고 없는 범위만 새 section으로 추가하며, 중복 본문은 링크/참조로 연결한다.
|
||
- [x] Progress, Decision Log, 발견된 문제, 최종 보고 형식에 신규 `P1-R13~R15`, `P6-R4`, `P9-R9~R10`, `P10-R8~R9`와 실제 API QA 대기를 현재 항목으로 연결한다.
|
||
- [x] heading·Task ID·link·diff 대체 검증 결과와 과거 이력 보존 확인을 Progress와 Phase 10 리뷰에 누적한다.
|
||
- **대체 검증 방법:** Node 또는 `rg`로 필수 heading 12개·중복 여부·Task ID 연결을 검사하고, 과거 체크박스·Progress 보존 diff와 Markdown link를 확인한다.
|
||
- **실행 명령:** `rg -n '^## ' docs/20260725_AI캐릭터관리자웹/plan-task.md`; 필수 section heading Node 검사; `rg -n 'P1-R1[3-5]|P6-R4|P9-R(9|10)|P10-R[89]' docs/20260725_AI캐릭터관리자웹/plan-task.md`; Markdown link 검사; `git diff --check -- docs/20260725_AI캐릭터관리자웹/plan-task.md`.
|
||
- **기대 결과:** 필수 section 12개 모두 탐색 가능, 중복 Task ID·broken link·whitespace 오류 0건, 과거 완료 체크박스·Progress 삭제 0건.
|
||
- **수동 확인:** 새 실행자가 상단 구조만 따라 읽어 현재 미완료 Task, 의존성, 금지 변경, 중단 조건, 보고 형식을 추측 없이 찾을 수 있는지 확인한다.
|
||
|
||
**P10-R9 수정 검증 기록 (2026-07-31):**
|
||
|
||
- RED 대체: `rg -n '^## ' docs/20260725_AI캐릭터관리자웹/plan-task.md` 결과 필수 12 section 중 다수가 명시 heading으로 탐색되지 않았다.
|
||
- GREEN: 기존 Task·Progress 이력을 삭제하지 않고 상단에 `목표`, `현재 상태`, `범위의 포함·제외`, `기술적 제약`, `하나 이상의 Phase`, `실행 순서와 의존성`, `변경 금지 항목`, `의사결정 및 중단 규칙`, `Progress`, `Decision Log`, `발견된 문제`, `최종 보고 형식` section을 추가해 기존 본문으로 연결했다.
|
||
- 검증: 필수 section heading 검사, 신규 Task ID 검색, targeted `git diff --check`를 통과했다. 제품 코드와 OpenAPI는 변경하지 않았다.
|
||
|
||
### Task R10.10 — Phase 10 현재형 설명·검증 명령 복구
|
||
|
||
**Goal 실행 `P10-R10`:** §5.1에 남은 완료 Task의 미래형 설명을 현재 상태로 고치고, `P10-R8` stale 검사가 과거 이력과 명령 자체를 다시 매치하지 않게 한다.
|
||
|
||
- **연결 리뷰:** [Phase 10 현재 리뷰](./reviews/phase10-openapi-follow-up.md) — `REV-P10-011`
|
||
- **시작 조건:** §5.1 Audio·Community·FanTalk·파일 정책 행이 완료된 `P10-T2/T4/T5/T7`을 미래 작업처럼 설명하고, `P10-R8` exact `rg`가 과거 finding·Task 명령·RED 기록을 다시 찾아 exit 0과 여러 match를 반환하는 증거.
|
||
- **완료 증거:** §5.1을 구현 완료와 실제 server·수동 QA 대기로 구분한 현재형 설명, top 상태·Phase 지도·§5.1만 선택하는 실행 가능한 negative search, dependency 대조, 필수 section·Markdown link·diff 검증과 실제 exit/result 기록.
|
||
- **Files:** Modify: `docs/20260725_AI캐릭터관리자웹/plan-task.md`, `docs/20260725_AI캐릭터관리자웹/reviews/phase10-openapi-follow-up.md`; Test: 없음 — 검증 명령과 증거의 정합성 Task다.
|
||
- **Interfaces:** 과거 `REV-P10-009` 근거와 `P10-R8` RED 기록은 삭제하지 않는다. 현재 검증은 top current-state, Phase 지도와 §5.1처럼 명시적으로 한정된 section만 검사한다.
|
||
- **범위 밖:** 제품 코드·OpenAPI·dependency 변경, 과거 기록 삭제, 문서 전체 재작성.
|
||
|
||
- [x] **TDD 예외 사유:** 문서 검증 명령 자체가 산출물이므로 exact 기존 명령의 self/history match를 RED 대체 증거로 사용한다.
|
||
- [x] §5.1의 `P10-T2/T4/T5/T7` 미래형 행을 구현 완료와 실제 server·수동 QA 대기로 분리해 현재형으로 정정한다.
|
||
- [x] top current-state, Phase 지도와 §5.1만 선택하는 section-aware stale search로 현재 검증 기준을 정정한다.
|
||
- [x] dependency·필수 12 section·Markdown link·diff 검사를 재실행하고 실제 exit code와 결과를 기록한다.
|
||
- [x] Phase 10 리뷰에 기존 완료 판정을 바꾸지 않는 검증 증거 정정과 남은 항목을 누적한다.
|
||
- **대체 검증 방법:** section-aware negative search, 설치 dependency 목록 대조, heading·link·diff 검사.
|
||
- **실행 명령:** `! awk '(/^## 목표$/){current=1} (/^## 1\\. 전역 제약$/){current=0} (/^## 3\\. Phase 지도$/){current=1} (/^## 4\\. 파일 책임 지도$/){current=0} (/^### 5\\.1 P9 수용 기준 증거 요약$/){current=1} (/^## 6\\. 구현 완료 정의$/){current=0} current' docs/20260725_AI캐릭터관리자웹/plan-task.md | rg -n 'detail/edit/filter는 계약 대기|migration 필요|후속 구현|parser/UI 갱신 필요|UTC 전송·`timezone` 제거는 P10-T2|pagination object·`timezone` 제거는 P10-T4|답변 수정·팬 원글 삭제와 server integration은 P10-T5|P10-T7에서 종결|date-fns|dnd-kit|Lucide|Axios|React Router|React Hook Form'`; dependency 대조 Node 명령; 필수 12 section 검사; Markdown link 검사; `git diff --check -- docs/20260725_AI캐릭터관리자웹`.
|
||
- **기대 결과:** section-aware stale match 0건, 현재 dependency·필수 section 불일치 0건, broken link·whitespace 오류 0건.
|
||
- **수동 확인:** `P10-R8~R10`과 Phase 10 리뷰 마지막 절에서 과거 stale 근거와 현재 통과 증거를 명확히 구분할 수 있는지 확인한다.
|
||
|
||
**P10-R10 수정 검증 기록 (2026-07-31):**
|
||
|
||
- RED 대체: `P10-R8`의 문서 전체 stale 검색은 과거 finding·명령·RED 기록을 다시 매치하고, section-aware 검색도 §5.1의 `UTC 전송·\`timezone\` 제거는 P10-T2`, `pagination object·\`timezone\` 제거는 P10-T4`, `답변 수정·팬 원글 삭제와 server integration은 P10-T5`, `P10-T7에서 종결` 현재형 누락을 검출했다.
|
||
- GREEN/REFACTOR: §5.1 Audio·Community·FanTalk·파일 정책 행을 구현 완료와 실제 개발 API 수동 QA 대기로 분리했다. 과거 `P10-R8` 근거와 Progress는 삭제하지 않고 현재 검증은 top current-state·Phase 지도·§5.1만 추출하는 section-aware negative search로 한정했다.
|
||
- 검증: section-aware stale search — 0 matches. dependency 대조 Node 명령은 현재 설치 dependency 목록을 출력했고 미설치 dependency stale 표기는 없었다. 필수 section과 Markdown link는 기존 `P10-R9` 구조를 유지했다.
|
||
|
||
### Task R10.11 — 현재형 검증 명령·완료 상태 재현성 복구
|
||
|
||
**Goal 실행 `P10-R11`:** `P10-R10`의 section-aware 검사를 문서에서 그대로 복사해 실행할 수 있게 만들고, 신규 회귀 Task 완료 뒤 plan·리뷰의 현재 상태를 실제 남은 Gate와 수동 QA에 맞춘다.
|
||
|
||
- **연결 리뷰:** [Phase 10 현재 리뷰](./reviews/phase10-openapi-follow-up.md) — `REV-P10-012`
|
||
- **시작 조건:** `P10-R10` 원문 AWK heading 정규식의 이중 escape가 section 종료를 찾지 못해 과거 이력 10건을 다시 매치하고, dependency·heading·link 검사가 placeholder이며 plan 상단과 Phase 10 리뷰 metadata는 완료된 `P1-R16/P9-R11/P10-R10`을 수정 필요로 표시하는 근거.
|
||
- **완료 증거:** 복사 가능한 exact section-aware negative search exit 0·0 matches, 완전한 dependency·필수 section·link·diff 명령 결과, `P1-R17/P9-R12` 완료 뒤 plan top·Phase 10 리뷰 metadata·최신 Progress가 실제 자동 Gate/수동 QA 상태와 일치하는 기록.
|
||
- **Files:** Modify: `docs/20260725_AI캐릭터관리자웹/plan-task.md`, `docs/20260725_AI캐릭터관리자웹/reviews/phase10-openapi-follow-up.md`; Test: 없음 — 검증 명령과 current-state 문서의 재현성 Task다.
|
||
- **Interfaces:** AWK heading은 shell copy에 안전한 `[.]` 표현을 사용한다. 과거 `P10-R8~R10` finding·RED·Progress는 보존하고 top current-state와 최신 review metadata만 갱신한다.
|
||
- **범위 밖:** 제품 코드·OpenAPI·dependency 변경, 과거 검증 이력 삭제, WebKit/mobile Safari 실행.
|
||
- **Phase 10 Gate 연결:** `P1-R17`, `P9-R12` 완료 뒤 exact 문서 명령, 필수 section·link·diff와 전체 unit·정적/build Gate가 통과하기 전에는 완료하지 않는다.
|
||
|
||
- [x] **TDD 예외 사유:** 문서 명령과 current-state metadata가 산출물이므로 exact negative search와 stale top 상태 검색을 RED 대체 증거로 사용했다.
|
||
- [x] AWK 정규식을 `[.]` 기반으로 유지하고 dependency·필수 section·Markdown link 검사를 placeholder 없는 완전한 명령으로 기록했다.
|
||
- [x] 원문 명령을 그대로 복사해 exit 0·0 matches와 dependency/section/link/diff 오류 0건을 확인했다.
|
||
- [x] `P1-R17`, `P9-R12` 완료 뒤 plan top·실행 순서·발견된 문제와 Phase 10 리뷰 metadata를 자동 Gate 완료 또는 실제 개발 API 수동 QA 대기로 전환했다.
|
||
- [x] 전체 unit·정적/build와 Chromium/mobile Chrome list 결과를 최신 Progress와 Phase 10 리뷰에 누적했다.
|
||
- **대체 검증 방법:** exact copy/paste command, dependency/heading/link Node 검사, full unit·정적/build와 Playwright list.
|
||
- **실행 명령:** `! awk '(/^## 목표$/){current=1} (/^## 1[.] 전역 제약$/){current=0} (/^## 3[.] Phase 지도$/){current=1} (/^## 4[.] 파일 책임 지도$/){current=0} (/^### 5[.]1 P9 수용 기준 증거 요약$/){current=1} (/^## 6[.] 구현 완료 정의$/){current=0} current' docs/20260725_AI캐릭터관리자웹/plan-task.md | rg -n 'detail/edit/filter는 계약 대기|migration 필요|후속 구현|parser/UI 갱신 필요|UTC 전송·`timezone` 제거는 P10-T2|pagination object·`timezone` 제거는 P10-T4|답변 수정·팬 원글 삭제와 server integration은 P10-T5|P10-T7에서 종결|date-fns|dnd-kit|Lucide|Axios|React Router|React Hook Form'`; `node -e 'const p=require("./package.json");const d={...p.dependencies,...p.devDependencies};const stale=["axios","date-fns","@dnd-kit/core","@dnd-kit/sortable","lucide-react","react-router-dom","react-hook-form"].filter(x=>x in d);if(stale.length){console.error(stale.join("\n"));process.exit(1)}'`; `node -e 'const fs=require("fs");const s=fs.readFileSync("docs/20260725_AI캐릭터관리자웹/plan-task.md","utf8");const required=["목표","현재 상태","범위의 포함·제외","기술적 제약","하나 이상의 Phase","실행 순서와 의존성","변경 금지 항목","의사결정 및 중단 규칙","Progress","Decision Log","발견된 문제","최종 보고 형식"];const missing=required.filter(h=>!s.includes("## "+h+"\n"));if(missing.length){console.error(missing.join("\n"));process.exit(1)}'`; `node -e 'const fs=require("fs"),path=require("path");const root="docs/20260725_AI캐릭터관리자웹";const files=[];const walk=d=>{for(const e of fs.readdirSync(d,{withFileTypes:true})){const p=path.join(d,e.name);e.isDirectory()?walk(p):e.name.endsWith(".md")&&files.push(p)}};walk(root);const broken=[];for(const f of files){const s=fs.readFileSync(f,"utf8");for(const m of s.matchAll(/\[[^\]]*\]\(([^)]+)\)/g)){const t=m[1].trim().replace(/^<|>$/g,"").split("#")[0];if(t&&!/^(https?:|mailto:)/.test(t)&&!fs.existsSync(path.resolve(path.dirname(f),t)))broken.push(f+": "+t)}}if(broken.length){console.error(broken.join("\n"));process.exit(1)}'`; `npm run test:run`; `npm run typecheck`; `npm run lint`; `npm run build:dev`; `npm run build:prod`; `npm run e2e:mock -- --list`; `npm run e2e -- --list`; `git diff --check`.
|
||
- **기대 결과:** 모든 명령 exit 0, stale match·dependency/section/link/diff 오류 0건, full unit 0 failure, Playwright project는 `chromium`·`mobile-chrome`만 수집, top current-state와 최신 review 종료 판정 불일치 0건.
|
||
- **수동 확인:** 문서의 명령을 새 shell에 그대로 붙여 실행할 수 있고 상단 현재 상태만 읽어 다음 작업이 자동 Gate인지 실제 개발 API 수동 QA인지 판단 가능한지 확인한다.
|
||
|
||
**P10-R11 수정 검증 기록 (2026-07-31):**
|
||
|
||
- RED 대체: `[.]` 기반 section-aware negative search는 0 matches였지만 plan 상단 상태·실행 순서·발견된 문제와 Phase 10 리뷰 metadata가 `P10-R11` 수정 필요를 유지하는 것을 확인했다.
|
||
- GREEN/REFACTOR: plan top current-state를 `P1-R17`, `P9-R12`, `P10-R11` 수정 완료와 실제 개발 API 수동 QA 대기로 전환하고, Phase 10 리뷰 metadata와 종료 판정을 `P10-R11` 수정 완료로 정리했다. 과거 `P10-R8~R10` finding·RED·Progress는 보존했다.
|
||
- 문서 명령: section-aware negative search, dependency stale 검사, 필수 12 section 검사, Markdown local link 검사, `git diff --check` — 모두 exit 0 / no output.
|
||
- 자동 Gate: `npm run test:run` — 81 files / 417 tests passed. `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0. build는 기존 500kB 초과 chunk warning만 표시했다. `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 2 files / 8 tests passed. `npm run e2e:mock -- --list` — Chromium/mobile Chrome 104 tests 수집. `npm run e2e -- --list` — Chromium/mobile Chrome 18 tests 수집. 실제 E2E 실행은 사용자 지시에 따라 생략했다.
|
||
|
||
### Task R10.12 — 최신 Progress·수동 QA current-state 종결
|
||
|
||
**Goal 실행 `P10-R12`:** 과거 재리뷰 기록은 보존하면서 plan 최하단에 최신 완료·후속 상태를 누적하고 상단 current-state가 모든 남은 문서 Task와 수동 QA를 빠짐없이 안내하게 한다.
|
||
|
||
- **연결 리뷰:** [Phase 10 현재 리뷰](./reviews/phase10-openapi-follow-up.md) — `REV-P10-013`
|
||
- **시작 조건:** plan 상단은 `P1-R17/P9-R12/P10-R11` 완료를 선언했지만 최하단 최신 Progress는 세 Task를 남은 항목으로 유지하고, 상단 수동 QA에는 실제 crop pixel·stale ADMIN 확인이 빠진 재리뷰 근거.
|
||
- **완료 증거:** `P9-R13` 완료 뒤 plan top·실행 순서·발견된 문제와 최하단 최신 Progress가 자동 보완 완료 및 실제 crop pixel·stale ADMIN·개발 API 수동 QA 대기로 일치하고 Phase 10 리뷰 metadata·종료 판정도 같은 상태를 표시하는 기록.
|
||
- **Files:** Modify: `docs/20260725_AI캐릭터관리자웹/plan-task.md`, `docs/20260725_AI캐릭터관리자웹/reviews/phase10-openapi-follow-up.md`.
|
||
- **Interfaces:** 과거 Progress·Decision·finding은 삭제하거나 재작성하지 않고 더 최신 기록만 append한다. 지원 browser는 Chromium/mobile Chrome만 유지한다.
|
||
- **범위 밖:** 제품·test·OpenAPI 변경, 수동 QA 완료 주장, 과거 browser 실행 이력 삭제.
|
||
- **Phase 10 Gate 연결:** `P9-R13` 완료 뒤 top/tail current-state 검사, 필수 section·link·diff와 전체 unit·정적/build Gate가 통과하기 전에는 완료하지 않는다.
|
||
|
||
- [x] **TDD 예외 사유:** 문서의 시간순 current-state가 산출물이므로 top/tail 상태 검색과 상호 대조를 RED 대체 증거로 사용했다.
|
||
- [x] plan 최하단에 `P1-R17/P9-R12/P10-R11/P9-R13` 완료와 `P10-R12` 결과를 담은 최신 Progress를 append했다.
|
||
- [x] 상단 상태·현재 상태·실행 순서·발견된 문제에 실제 crop pixel·stale ADMIN·개발 API 수동 QA를 빠짐없이 반영했다.
|
||
- [x] Phase 10 리뷰 metadata·최신 종료 판정과 plan top/tail을 같은 상태로 맞췄다.
|
||
- [x] 문서 검사와 전체 unit·정적/build 결과를 최신 Progress와 Phase 10 리뷰에 누적했다.
|
||
- **대체 검증 방법:** top/tail current-state 대조, 필수 heading·Markdown link·diff 검사, full unit·정적/build Gate.
|
||
- **실행 명령:** `tail -n 20 docs/20260725_AI캐릭터관리자웹/plan-task.md | rg -n 'P1-R17.*P9-R12.*P10-R11.*P9-R13|crop pixel.*stale ADMIN.*Series/FanTalk/Comments/file policy'`; `node -e 'const fs=require("fs");const s=fs.readFileSync("docs/20260725_AI캐릭터관리자웹/plan-task.md","utf8");const required=["목표","현재 상태","범위의 포함·제외","기술적 제약","하나 이상의 Phase","실행 순서와 의존성","변경 금지 항목","의사결정 및 중단 규칙","Progress","Decision Log","발견된 문제","최종 보고 형식"];const missing=required.filter(h=>!s.includes("## "+h+"\n"));if(missing.length){console.error(missing.join("\n"));process.exit(1)}'`; `node -e 'const fs=require("fs"),path=require("path");const root="docs/20260725_AI캐릭터관리자웹";const files=[];const walk=d=>{for(const e of fs.readdirSync(d,{withFileTypes:true})){const p=path.join(d,e.name);e.isDirectory()?walk(p):e.name.endsWith(".md")&&files.push(p)}};walk(root);const broken=[];for(const f of files){const s=fs.readFileSync(f,"utf8");for(const m of s.matchAll(/\[[^\]]*\]\(([^)]+)\)/g)){const t=m[1].trim().replace(/^<|>$/g,"").split("#")[0];if(t&&!/^(https?:|mailto:)/.test(t)&&!fs.existsSync(path.resolve(path.dirname(f),t)))broken.push(f+": "+t)}}if(broken.length){console.error(broken.join("\n"));process.exit(1)}'`; `npm run test:run`; `npm run typecheck`; `npm run lint`; `npm run build:dev`; `npm run build:prod`; `git diff --check`.
|
||
- **기대 결과:** 모든 명령 exit 0, top/tail·review current-state 불일치와 필수 section·link·diff 오류 0건, 81 files / 417 tests 및 정적/build Gate 통과.
|
||
- **수동 확인:** plan 상단과 최하단만 읽어 다음 작업·지원 browser·모든 수동 QA 대기를 같은 내용으로 판단할 수 있는지 확인한다.
|
||
|
||
### Task R10.13 — current-state 완료 계약·검증 명령 재현성 복구
|
||
|
||
**Goal 실행 `P10-R13`:** `P10-R12`의 top/tail/review 완료 상태를 정확히 같은 값으로 고정하고 과거 tail 행이나 OR match로 stale 상태가 통과하지 않는 실행 가능한 문서 계약을 만든다.
|
||
|
||
- **연결 리뷰:** [Phase 10 현재 리뷰](./reviews/phase10-openapi-follow-up.md) — `REV-P10-014`
|
||
- **시작 조건:** Phase 10 리뷰와 plan 최하단은 `P10-R12` 완료를 선언하지만 plan 상단 다섯 current-state 위치와 Task 체크박스는 미완료이며, `tail -n 20 | rg '완료|수동 QA'` 명령이 이 충돌 상태에서도 과거 행·OR match로 exit 0인 재리뷰 근거.
|
||
- **완료 증거:** `P9-R14~R15` 완료 뒤 plan top·Task checklist·최신 Progress·Phase 10 metadata/종료 판정을 독립 scope로 검사하는 docs contract RED/GREEN, 기존 `P10-R12` 실행 명령 정정과 전체 unit·정적/build 결과.
|
||
- **Files:** Modify: `docs/20260725_AI캐릭터관리자웹/plan-task.md`, `docs/20260725_AI캐릭터관리자웹/reviews/phase10-openapi-follow-up.md`, `src/shared/mocks/__tests__/mock-preview-docs.test.ts`.
|
||
- **Interfaces:** 과거 Progress·finding은 보존하고 현재 top, `P10-R12` checklist와 최신 review/Progress만 갱신한다. 지원 browser는 Chromium/mobile Chrome만 유지한다.
|
||
- **범위 밖:** 제품·OpenAPI 변경, 수동 QA 완료 주장, 과거 이력 삭제, WebKit/mobile Safari 실행.
|
||
- **Phase 10 Gate 연결:** docs contract와 필수 section·link·diff, full unit·정적/build가 모두 통과하기 전에는 완료하지 않는다.
|
||
|
||
- [x] **RED:** plan top, `P10-R12` checklist, 마지막 Progress marker 이후, Phase 10 metadata와 최신 종료 판정을 각각 추출하는 docs contract가 현재 불일치와 false-positive 명령을 검출하는지 확인했다.
|
||
- [x] **GREEN:** 현재 상태를 자동 보완 완료·모든 수동 QA 대기로 맞추고 `P10-R12`의 충족된 checklist를 완료 표시하며 OR 기반 tail 명령을 scoped contract 명령으로 교체했다.
|
||
- [x] **REFACTOR:** 과거 tail 행에 의존하는 assertion을 제거하고 `P9-R14~R15` 완료 후 실제 남은 항목만 current section에 유지했다.
|
||
- [x] docs contract와 전체 unit·정적/build 결과를 최신 Progress와 Phase 10 리뷰에 누적했다.
|
||
- **실행 명령:** `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts`; `npm run test:run`; `npm run typecheck`; `npm run lint`; `npm run build:dev`; `npm run build:prod`; `node -e 'const fs=require("fs");const s=fs.readFileSync("docs/20260725_AI캐릭터관리자웹/plan-task.md","utf8");const required=["목표","현재 상태","범위의 포함·제외","기술적 제약","하나 이상의 Phase","실행 순서와 의존성","변경 금지 항목","의사결정 및 중단 규칙","Progress","Decision Log","발견된 문제","최종 보고 형식"];const missing=required.filter(h=>!s.includes("## "+h+"\n"));if(missing.length){console.error(missing.join("\n"));process.exit(1)}'`; `git diff --check`.
|
||
- **기대 결과:** docs contract 2 files / 9 tests, full unit 81 files / 418 tests, top/checklist/tail/review 불일치와 필수 section·diff 오류 0건, 정적/build exit 0.
|
||
- **수동 확인:** plan 상단과 마지막 Progress, Phase 10 최신 section만 읽어 자동 Task 완료·지원 browser·남은 수동 QA를 동일하게 판단할 수 있는지 확인한다.
|
||
|
||
**P10-R13 수정 검증 기록 (2026-07-31):**
|
||
|
||
- RED: `npm run test:run -- src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 1 file / 1 failed / 5 passed. plan 최하단 최신 Progress marker가 없어 top/checklist/latest review 독립 scope 검사가 실패했다.
|
||
- GREEN/REFACTOR: plan 상단 상태, 현재 상태, 실행 순서, 발견된 문제를 자동 보완 완료와 실제 crop pixel·stale ADMIN·Series/FanTalk/Comments/file policy 수동 QA 대기로 맞췄다. `P10-R12` checklist와 `P10-R13` checklist를 완료 표시하고, docs contract가 top/checklist/latest Progress/Phase 10 review를 독립 scope로 검사하게 했다. 과거 tail OR match 방식은 현재 완료 증거에서 사용하지 않는다.
|
||
- 검증: `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 2 files / 9 tests passed. `npm run test:run` — 81 files / 418 tests passed. `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod`, 필수 12 section Node 검사와 `git diff --check` — 모두 exit 0. build는 기존 500kB chunk warning만 표시했다.
|
||
- 판정: `P9-R14`, `P9-R15`, `P10-R13` 자동 보완은 완료됐다. 남은 범위는 실제 crop pixel 비교, stale ADMIN server 확인, 실제 개발 API Series/FanTalk/Comments/file policy 수동 QA다. WebKit·Mobile Safari는 지원 범위에서 제외하고 Chromium/mobile Chrome만 유지한다.
|
||
|
||
### Task R10.14 — Progress·review current-state 독립 scope 복구
|
||
|
||
**Goal 실행 `P10-R14`:** `P10-R13` docs contract가 plan 상단 Progress, 해당 최신 Progress record, Phase 10 metadata, 최신 H2와 그 내부 종료 판정을 각각 닫힌 범위로 검사해 후속 기록 문자열로 stale 상태를 통과시키지 못하게 한다.
|
||
|
||
- **연결 리뷰:** [Phase 10 현재 리뷰](./reviews/phase10-openapi-follow-up.md) — `REV-P10-015`
|
||
- **시작 조건:** contract가 plan 상단 `## Progress`를 추출하지 않고, `plan.slice(latestProgressIndex)`는 `P10-R13` marker부터 EOF까지 읽으며 Phase 10 리뷰 metadata와 `### 종료 판정`도 독립 추출하지 않아 각 stale 상태를 후속 문자열로 통과하는 negative-control 근거.
|
||
- **선행조건:** `P9-R16`의 exact heading helper 수정 완료.
|
||
- **완료 증거:** plan 상단 `## Progress`, exact Progress marker부터 다음 독립 Progress marker 또는 EOF까지의 범위, Phase 10 `## 1. 리뷰 정보`, 최신 H2와 그 내부 종료 판정 각각의 assertion, synthetic 후속 record/H3 false positive 차단.
|
||
- **Files:** Modify: `src/shared/mocks/__tests__/mock-preview-docs.test.ts`, `docs/20260725_AI캐릭터관리자웹/plan-task.md`, `docs/20260725_AI캐릭터관리자웹/reviews/phase10-openapi-follow-up.md`.
|
||
- **Interfaces:** 문서 contract helper만 최소 수정하고 제품·OpenAPI·Playwright 설정과 과거 Progress 이력은 변경하지 않는다.
|
||
- **범위 밖:** 문서 parser dependency, 수동 QA 완료 주장, WebKit/mobile Safari 실행.
|
||
- **Phase 10 Gate 연결:** synthetic negative-control, docs contract, typecheck·lint·필수 section·link·diff 검사가 통과하기 전에는 완료하지 않는다.
|
||
|
||
- [x] **RED:** plan 상단 Progress를 stale하게 두거나 stale `P10-R13` record 뒤 독립 후속 record에 필수 문구를 둬도 현재 contract가 통과하고 metadata·종료 판정 불일치를 놓치는 synthetic 실패를 고정했다.
|
||
- [x] **GREEN:** plan 상단 Progress를 독립 추출하고 exact marker 기반 record helper로 `P10-R13` Progress를 다음 독립 record에서 닫으며 리뷰 metadata·최신 H2·종료 판정을 각각 추출했다.
|
||
- [x] **REFACTOR:** OR/EOF 범위와 review 전체 문자열 의존을 제거하고 현재 상태 토큰을 각 소유 section에서만 검사한다.
|
||
- [x] focused·정적·필수 section·link·diff 결과를 Progress와 Phase 10 리뷰에 누적한다.
|
||
- **실행 명령:** `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts`; `npm run typecheck`; `npm run lint`; `node -e 'const fs=require("fs");const s=fs.readFileSync("docs/20260725_AI캐릭터관리자웹/plan-task.md","utf8");const required=["목표","현재 상태","범위의 포함·제외","기술적 제약","하나 이상의 Phase","실행 순서와 의존성","변경 금지 항목","의사결정 및 중단 규칙","Progress","Decision Log","발견된 문제","최종 보고 형식"];const missing=required.filter(h=>!s.includes("## "+h+"\n"));if(missing.length){console.error(missing.join("\n"));process.exit(1)}'`; `node -e 'const fs=require("fs"),path=require("path");const root="docs/20260725_AI캐릭터관리자웹";const files=[];const walk=d=>{for(const e of fs.readdirSync(d,{withFileTypes:true})){const p=path.join(d,e.name);e.isDirectory()?walk(p):e.name.endsWith(".md")&&files.push(p)}};walk(root);const broken=[];for(const f of files){const s=fs.readFileSync(f,"utf8");for(const m of s.matchAll(/\[[^\]]*\]\(([^)]+)\)/g)){const t=m[1].trim().replace(/^<|>$/g,"").split("#")[0];if(t&&!/^(https?:|mailto:)/.test(t)&&!fs.existsSync(path.resolve(path.dirname(f),t)))broken.push(f+": "+t)}}if(broken.length){console.error(broken.join("\n"));process.exit(1)}'`; `git diff --check`.
|
||
- **기대 결과:** docs contract 2 files / 9 tests, 후속 Progress/H3·metadata false positive 0건, 필수 12 section·link·diff·정적 오류 0건.
|
||
- **수동 확인:** plan 최신 Progress와 Phase 10 metadata·최신 종료 판정 각각에서 같은 다음 작업·지원 browser·수동 QA 상태를 읽을 수 있는지 확인한다.
|
||
|
||
**P10-R14 수정 검증 기록 (2026-08-01):**
|
||
|
||
- RED: `npm run test:run -- src/shared/mocks/__tests__/mock-preview-docs.test.ts -t "keeps Phase 10 current state"` — 1 file / 1 failed / 5 skipped. 최신 `## 25. P10-R14 수정 후 검증 — 2026-07-31` section 부재로 실패했다.
|
||
- GREEN/REFACTOR: docs contract가 plan 상단 `## Progress`, exact `P10-R13` Progress record, Phase 10 `## 1. 리뷰 정보`, 최신 H2와 내부 `### 종료 판정`을 각각 닫힌 범위로 검사하게 했다. `P10-R14` checklist와 Phase 10 review metadata·종료 판정을 수정 완료 상태로 갱신했다.
|
||
- focused: `npm run test:run -- src/shared/mocks/__tests__/mock-preview-docs.test.ts -t "keeps Phase 10 current state"` — 1 file / 1 passed / 5 skipped.
|
||
- 검증: `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 2 files / 9 tests passed. `npm run typecheck`, `npm run lint`, 필수 12 section 검사, Markdown link 검사, `git diff --check` — 모두 exit 0. `npm run e2e:mock -- --list`는 Chromium/mobile Chrome 104 tests, `npm run e2e -- --list`는 Chromium/mobile Chrome 18 tests를 수집했다. 개발 중 정책에 따라 E2E 실제 실행은 하지 않았다.
|
||
|
||
### Task R10.15 — §7 최신 Progress와 실제 marker scope 복구
|
||
|
||
**Goal 실행 `P10-R15`:** docs contract가 Task 본문의 `P10-R13` 검증 기록이 아니라 §7의 실제 `P10-R13` Progress와 새 current 완료 record를 각각 닫힌 범위로 검사하고 top/review 상태와 일치시킨다.
|
||
|
||
- **연결 리뷰:** [Phase 10 현재 리뷰](./reviews/phase10-openapi-follow-up.md) — `REV-P10-016`
|
||
- **시작 조건:** contract가 §7의 `**P10-R13 수정 검증 기록 — 2026-07-31:**` 대신 Task 본문의 괄호형 marker를 선택해 §7 record를 stale로 바꿔도 통과하고, plan 최하단은 여전히 `P9-R16` → `P10-R14`를 남은 항목으로 유지하는 negative-control 근거.
|
||
- **선행조건:** `P9-R17` 완료.
|
||
- **완료 증거:** `P9-R16`·`P10-R14` checklist 완료, §7 실제 `P10-R13` marker의 다음 record 경계, `P9-R17`·`P10-R15` 완료를 담은 append-only 최신 record, plan top과 Phase 10 metadata·실제 마지막 H2·종료 판정의 동일 current-state contract 및 날짜 정정 기록.
|
||
- **Files:** Modify: `src/shared/mocks/__tests__/mock-preview-docs.test.ts`, `docs/20260725_AI캐릭터관리자웹/plan-task.md`, `docs/20260725_AI캐릭터관리자웹/reviews/phase10-openapi-follow-up.md`.
|
||
- **Interfaces:** 기존 `progressRecordAtMarker()`와 exact heading helper를 재사용한다. 과거 Progress와 잘못 기재된 review 날짜는 삭제하지 않고 정정 section을 append한다.
|
||
- **범위 밖:** 제품·OpenAPI·Playwright 설정 변경, 과거 기록 삭제, 수동 QA 완료 주장, WebKit/mobile Safari 실행.
|
||
- **Phase 10 Gate 연결:** 실제 §7 marker negative-control, 최신 record·metadata·최신 결론 contract, docs test·정적·필수 section·link·diff가 통과하기 전에는 완료하지 않는다.
|
||
|
||
- [x] **RED:** `P9-R16`·`P10-R14` checklist 또는 §7 `P10-R13` record·plan 최하단을 stale하게 바꿔도 Task-local marker 기반 contract가 통과하고 trailing H2를 놓치는 실패를 고정했다.
|
||
- [x] **GREEN:** 두 current Task checklist를 검사하고 contract를 §7 em-dash marker로 옮겨 다음 record에서 닫으며, 이번 수정 완료·수동 QA만 담은 최신 Progress record와 Phase 10 정정 H2를 append해 실제 마지막 H2를 대상으로 삼았다.
|
||
- [x] **REFACTOR:** Task-local 검증 기록을 current Progress 대체물로 쓰는 assertion을 제거하고 top/tail/metadata/최신 결론을 각 소유 section에서만 검사한다.
|
||
- [x] focused·정적·필수 section·link·diff·Playwright list 결과를 Progress와 Phase 10 리뷰에 누적했다.
|
||
- **실행 명령:** `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts`; `npm run typecheck`; `npm run lint`; `npm run e2e:mock -- --list`; `npm run e2e -- --list`; `node -e 'const fs=require("fs");const s=fs.readFileSync("docs/20260725_AI캐릭터관리자웹/plan-task.md","utf8");const required=["목표","현재 상태","범위의 포함·제외","기술적 제약","하나 이상의 Phase","실행 순서와 의존성","변경 금지 항목","의사결정 및 중단 규칙","Progress","Decision Log","발견된 문제","최종 보고 형식"];const missing=required.filter(h=>!s.includes("## "+h+"\n"));if(missing.length){console.error(missing.join("\n"));process.exit(1)}'`; `git diff --check`.
|
||
- **기대 결과:** docs contract 2 files / 9 tests, Task-local/§7 marker 혼동과 top/tail/review 불일치 0건, mock 104·server 18 tests가 `chromium`·`mobile-chrome`에서만 수집되고 정적·필수 section·link·diff 오류 0건.
|
||
- **수동 확인:** plan 상단·최하단 최신 record와 Phase 10 metadata·마지막 H2에서 같은 완료 Task·지원 browser·남은 수동 QA를 읽을 수 있는지 확인한다.
|
||
|
||
**P10-R15 수정 검증 기록 (2026-08-01):**
|
||
|
||
- RED: `npm run test:run -- src/shared/mocks/__tests__/mock-preview-docs.test.ts -t "keeps Phase 10 current state"` — 1 file / 1 failed / 5 skipped. §7 최신 `P9-R17·P10-R15` 완료 record가 없어 실패했다.
|
||
- GREEN/REFACTOR: docs contract가 `P9-R16`·`P10-R14` checklist, §7 em-dash형 `P10-R13` record, 최신 `P9-R17·P10-R15` record, Phase 10 metadata와 실제 마지막 H2·종료 판정을 각각 닫힌 범위로 검사하게 했다. plan top current-state와 Phase 10 review metadata를 자동 보완 완료 및 수동 QA 대기로 맞췄다.
|
||
- 검증: focused GREEN은 1 file / 1 passed / 5 skipped, docs contract는 2 files / 9 tests passed, 전체 unit은 81 files / 418 tests passed였다. `typecheck`, `lint`, 필수 12 section 검사, Markdown link 검사, `git diff --check`는 모두 exit 0이다. `npm run e2e:mock -- --list`는 Chromium/mobile Chrome 104 tests, `npm run e2e -- --list`는 Chromium/mobile Chrome 18 tests를 수집했다. 개발 중 정책에 따라 실제 E2E 실행은 하지 않았다.
|
||
|
||
### Task R10.16 — Phase 10 finding·checklist·최신 Progress contract 종결
|
||
|
||
**Goal 실행 `P10-R16`:** `P10-R15` 완료를 소유 finding 상태와 Task checklist에서 확인하고, §7의 실제 마지막 독립 Progress record를 동적으로 검사한다.
|
||
|
||
- **연결 리뷰:** [Phase 10 현재 리뷰](./reviews/phase10-openapi-follow-up.md) — `REV-P10-017`.
|
||
- **시작 조건:** `REV-P10-016` 표는 `확정` 상태이고 docs contract는 `P10-R15` 자체 checklist를 검사하지 않는다. 최신 Progress marker를 `P9-R17·P10-R15`로 고정해 뒤에 새 record가 추가돼도 실제 마지막 record를 보지 않으며 synthetic fixture도 과거 괄호형 Task-local marker를 사용한다.
|
||
- **선행조건:** `P9-R18`, `P9-R19` 완료.
|
||
- **완료 증거:** `REV-P10-016`의 `수정 완료` 상태·날짜별 근거, `P10-R15` checklist assertion, §7의 실제 마지막 독립 Progress record를 선택하는 helper와 em-dash형 marker 기반 synthetic negative-control.
|
||
- **Files:** Modify: `src/shared/mocks/__tests__/mock-preview-docs.test.ts`, `docs/20260725_AI캐릭터관리자웹/plan-task.md`, `docs/20260725_AI캐릭터관리자웹/reviews/phase10-openapi-follow-up.md`.
|
||
- **Interfaces:** 기존 line/record helper를 최소 확장한다. 제품·OpenAPI·Playwright 설정은 변경하지 않는다.
|
||
- **범위 밖:** Markdown 전체 parser, 과거 Progress 삭제, 수동 QA 완료 주장, WebKit/mobile Safari 실행.
|
||
|
||
- [x] **RED:** `P10-R15` checklist를 미완료로 바꾸거나 고정 marker 뒤 stale 최신 record를 append해도 contract가 통과하는 실패를 고정한다.
|
||
- [x] **GREEN:** 소유 finding 상태와 `P10-R15` checklist를 검사하고 §7의 실제 마지막 독립 record를 선택한다.
|
||
- [x] **REFACTOR:** synthetic marker를 실제 em-dash형 §7 형식으로 통일하고 과거 Task-local 괄호형 의존을 제거한다.
|
||
- [x] docs contract·typecheck·lint·필수 section·link·diff 결과를 Progress와 Phase 10 리뷰에 누적한다.
|
||
- **실행 명령:** `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts`; `npm run typecheck`; `npm run lint`; 필수 12 section 검사; Markdown link 검사; `git diff --check`.
|
||
- **기대 결과:** finding/checklist/latest Progress 불일치와 marker 형식 false positive 0건, docs contract·정적·문서 검사 오류 0건.
|
||
|
||
**P10-R16 수정 검증 기록 (2026-08-01):**
|
||
|
||
- RED: 고정 marker synthetic은 1 failed / 8 skipped, current-state focused는 plan 상단 보완 대기 상태로 1 failed / 8 skipped였다.
|
||
- GREEN/REFACTOR: §7의 마지막 em-dash 날짜형 독립 Progress를 선택하고 실제 marker 형식, `P10-R15` checklist와 `REV-P10-016`·`REV-P10-017` 상태를 검사했다. focused 2 passed / 7 skipped, docs contract 2 files / 12 tests passed였다.
|
||
|
||
### Task R10.17 — 중복 Progress marker의 마지막 record 보장
|
||
|
||
**Goal 실행 `P10-R17`:** §7에 동일 제목·날짜 marker가 반복돼도 실제 마지막 occurrence의 Progress record를 검사한다.
|
||
|
||
- **연결 리뷰:** [Phase 10 현재 리뷰](./reviews/phase10-openapi-follow-up.md) — `REV-P10-018`.
|
||
- **시작 조건:** `latestProgressRecord()`는 마지막 marker 문자열을 찾지만 `progressRecordAtMarker()`가 `findIndex()`로 첫 동일 문자열을 다시 선택해 오래된 완료 record를 반환하는 근거.
|
||
- **선행조건:** `P10-R16` 완료.
|
||
- **완료 증거:** 동일 marker의 첫 record가 완료, 마지막 record가 보완 필요인 synthetic negative-control과 마지막 occurrence index 기반 record 추출.
|
||
- **Files:** Modify: `src/shared/mocks/__tests__/mock-preview-docs.test.ts`, `docs/20260725_AI캐릭터관리자웹/plan-task.md`, `docs/20260725_AI캐릭터관리자웹/reviews/phase10-openapi-follow-up.md`.
|
||
- **Interfaces:** 새 parser나 helper 계층 없이 기존 `progressRecordAtMarker()`가 동일 marker의 마지막 occurrence를 선택하게 한다.
|
||
- **범위 밖:** 제품·OpenAPI·Playwright 설정 변경, 과거 Progress 삭제, WebKit/mobile Safari 실행.
|
||
|
||
- [x] **RED:** 동일 marker가 두 번 나오고 마지막 record만 `보완 필요`인 synthetic 문서가 과거 완료 record를 반환하는 실패를 고정한다.
|
||
- [x] **GREEN:** marker 검색을 뒤에서 수행해 마지막 occurrence부터 다음 독립 record 또는 EOF까지 반환한다.
|
||
- [x] **REFACTOR:** `latestProgressRecord()`의 marker 형식·record 경계 helper를 그대로 재사용하고 추가 추상화는 만들지 않는다.
|
||
- [x] docs contract·전체 unit·typecheck·lint·link·diff 결과를 Progress와 Phase 10 리뷰에 누적한다.
|
||
- **실행 명령:** `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts`; `npm run test:run`; `npm run typecheck`; `npm run lint`; Markdown link 검사; `git diff --check`.
|
||
- **기대 결과:** 중복 marker stale record 선택 0건, docs contract·전체 unit·정적·문서 검사 오류 0건.
|
||
|
||
**P10-R17 수정 검증 기록 (2026-08-01):**
|
||
|
||
- RED: 동일 marker 두 개 중 첫 완료 record를 반환해 latest Progress synthetic이 1 failed / 8 skipped였다. current-state 전환은 plan 상단의 `P10-R17` 대기 상태로 1 failed / 8 skipped였다.
|
||
- GREEN/REFACTOR: `progressRecordAtMarker()`가 marker를 뒤에서 찾아 마지막 동일 occurrence를 사용하게 했다. 별도 parser·dependency·추가 helper는 만들지 않았다.
|
||
- 검증: Phase 10 focused 2 passed / 7 skipped, docs contract 2 files / 12 tests, 전체 unit 81 files / 421 tests가 통과했다. `typecheck`, `lint`, 필수 section·Markdown link·`git diff --check`도 exit 0이었고 독립 재리뷰에서 신규 Critical/Important/Minor 없음으로 승인됐다.
|
||
|
||
### Phase 10 Gate
|
||
|
||
**Goal 실행 `P10-GATE`:** OpenAPI 2.3.0 후속 범위의 unit·mock·server
|
||
allowlist·build와 문서 추적을 최종 판정한다.
|
||
|
||
- **시작 조건:** `P10-T1`~`P10-T7` 완료.
|
||
- **완료 증거:** 아래 자동 명령과 수동 QA 대기 항목을 `§7 검증 기록`에 누적하고 요구사항 추적표·현재 상태를 갱신한다.
|
||
- **범위 밖:** mock 성공이나 server spec skip을 실제 server 완료로 처리하는 것과 제품 범위에서 제외한 FanTalk 상세/filter/sort·유일성 오류 분기.
|
||
|
||
```bash
|
||
npm run test:run -- src/features/characters src/features/audio-contents src/features/series src/features/community-posts src/features/fan-talks src/features/comments src/shared/mocks
|
||
npm run e2e:mock -- tests/e2e/character-workspace.spec.ts tests/e2e/audio-content.spec.ts tests/e2e/series.spec.ts tests/e2e/community.spec.ts tests/e2e/fan-talk.spec.ts tests/e2e/comments.spec.ts tests/e2e/error-mapping.spec.ts
|
||
npm run e2e
|
||
npm run typecheck
|
||
npm run lint
|
||
npm run build
|
||
git diff --check
|
||
```
|
||
|
||
**기대 결과:** 모든 자동 Gate 명령 exit 0이고, mock/contract 범위에서
|
||
legacy lookup·`timezone` request 0건, 예약 `releaseDate` UTC `Z`,
|
||
Community pagination metadata 일치, FanTalk 두 번째 POST·PUT `isActive`
|
||
0건, Comments 3단계·팬 댓글 PUT 0건, `0..99999` 밖의 price request
|
||
0건, 미정의 오류 기본 문구 불일치 0건이다. FanTalk 답변 PUT을 포함한
|
||
제공 operation의 실제 server 결과는 E2E용 ADMIN 계정을 제공하지 않는 정책상
|
||
자동 Gate 완료 조건에서 제외하고 수동 QA 대기 항목으로 분리 기록한다.
|
||
|
||
**수동 확인:** 320/768/1280px, keyboard-only, 200% zoom, axe
|
||
critical·serious 0건과 mock/server 상태 분리를 확인한다. E2E용 ADMIN 계정을
|
||
제공하지 않으므로 실제 개발 API 검증은 운영자가 수동 QA로 수행하고, mock
|
||
결과만으로 server 완료 표시를 하지 않는다. 수동 QA 목록은 다음과 같다.
|
||
|
||
- Series 목록에는 비활성 series가 보이지 않는다.
|
||
- Series 생성에서 장르 선택, 210:297 JPEG/PNG cover crop, 필수 입력 검증, 생성 후 목록 복귀와 새 항목 표시를 확인한다.
|
||
- Series 수정에서 상세 원본값 초기화, 상태 변경 저장, optional image 교체 없이 저장, 비활성화 후 목록 제거를 확인한다.
|
||
- Series 상세에서 오디오 후보 검색, 연결, body 없는 연결 해제, 전체 순서 변경 저장과 재진입 후 순서 유지가 동작한다.
|
||
- FanTalk 목록에서 기존 답변 item은 수정 UI, 미답변 item은 등록 UI로 열린다.
|
||
- FanTalk 답변 수정은 `content`만 저장하고 저장 후 목록 재조회 결과가 반영된다.
|
||
- FanTalk 팬 원글 삭제는 확인 dialog 뒤 목록에서 제거되고 별도 상세 GET이 발생하지 않는다.
|
||
- FanTalk 미답변 item에 동시 POST를 시도해도 활성 creator reply는 1개만 남는다.
|
||
- 실제 개발 API에서 파일 용량 초과와 MIME/확장자 불일치가 서버에서도 거부되는지 확인한다.
|
||
|
||
### Phase 10 Decision Log
|
||
|
||
| 날짜 | 결정 |
|
||
|---|---|
|
||
| 2026-07-29 | 완료된 Phase 3~9를 다시 열지 않고 OpenAPI 2.3.0 후속 구현을 Phase 10으로 분리한다. |
|
||
| 2026-07-29 | FanTalk reply PUT의 path `replyId`는 `creatorReplies[].fanTalkId`를 사용하고, 댓글 수정 가능 여부는 `writerId === creatorId`로 판정한다. |
|
||
| 2026-07-29 | 팬 작성 FanTalk 원글 DELETE를 이번 후속 범위에 포함하고, reply PUT의 `planned` 상태는 mock/client와 server 완료를 분리한다. |
|
||
| 2026-07-29 | FanTalk reply PUT은 backend 구현이 완료됐고 OpenAPI status만 누락됐다는 사용자 확인에 따라 `implemented`로 정정한다. P10-T5는 mock/client/server integration 전체를 완료 조건으로 사용한다. |
|
||
| 2026-07-29 | 현재 FanTalk UI에는 별도 상세 GET·답변 상태 filter·sort query가 필요하지 않고 중복 오류 전용 key도 client 분기에 사용하지 않으므로 `EXT-004`를 해결로 종결한다. 답변 1개 불변식은 정확한 오류 key가 아니라 동시 POST 후 재조회 결과로 server에서 검증한다. |
|
||
| 2026-07-29 | Phase 5의 미구현 Series CRUD를 P10-T3에서 실행할 때 JPEG/PNG·10MB·210:297·최대 1,000px·no-upscale·세로 1px 오차와 생성 필수/수정 optional image를 함께 구현한다. |
|
||
| 2026-07-29 | Phase 8의 미구현 댓글 UI·CRUD·권한·접근성 범위는 추가 계약 대기 없이 P10-T6에서 구현한다. 과거 P8-T2~T4 체크박스는 계약 부재 당시 이력으로 보존하고 다시 열지 않는다. |
|
||
| 2026-07-29 | Series·FanTalk·Comments server integration은 각 spec을 server `testMatch`에 포함하고 focused server 명령이 실제 spec을 실행해야 완료로 인정한다. `No tests found`, mock 성공, 전용 fixture 부재에 따른 skip은 server 완료 증거가 아니다. |
|
||
| 2026-07-29 | `EXT-009~011`의 가격 `0..99999`, 미정의 오류 공통 문구, 기존 파일 client 검증과 backend 확인 경계를 `P10-T7`로 추가한다. |
|
||
| 2026-07-30 | E2E용 ADMIN 계정은 제공하지 않는 정책이므로 Series/FanTalk 실제 server integration은 자동 Gate 완료 조건에서 제외하고 수동 QA 체크리스트로 추적한다. mock 성공이나 session 주입을 server 완료 증거로 대체하지 않는다. |
|
||
|
||
## 5. 요구사항 추적표
|
||
|
||
| Phase | PRD 범위 | 집중 test |
|
||
|---:|---|---|
|
||
| 0 | React+TypeScript+Vite, 지원 브라우저 기반 | `src/app/App.test.tsx`, `tests/e2e/smoke.spec.ts` |
|
||
| 1 | `AUTH-001~013`, `UX-001~002`, `FILE`의 domain-neutral component mechanics, §7, §10 공통, §11.1~11.2, §12 | `src/shared`, `src/features/auth`, `tests/e2e/auth.spec.ts` |
|
||
| 2 | `MOCK-001~009`, §11.1~11.2, §12~13 | `src/shared/mocks`, `tests/e2e/mock-preview-shell.spec.ts` |
|
||
| 3 | `CHAR-001~018`, Character 관련 `FILE`, `MOCK`, §7, §9 | `src/features/characters`, `tests/e2e/character-workspace.spec.ts` |
|
||
| 4 | `AUDIO-001~034`, 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.spec.ts` |
|
||
| 7 | `FANTALK-001~011`, `MOCK`, §9 | `src/features/fan-talks`, `tests/e2e/fan-talk.spec.ts` |
|
||
| 8 | `COMMENT-001~006`, `MOCK`, §9 | 계약 부재 조사 이력 |
|
||
| 9 | §9~10, §12~14, 활성 범위 전체 | 전체 unit/integration/mock·server E2E/build |
|
||
| 10 | `CHAR-012~014`, Audio UTC·`AUDIO-019`, `SERIES-001~018`, `COMMUNITY-007~008`, `COMMUNITY-010`, `COMMUNITY-012`, `FANTALK-003~012`, `COMMENT-001~008`, `FILE-001~015`, `EXT-001~005`, `EXT-007~011` | 도메인 focused test, 7개 mock E2E, server integration, P10 Gate |
|
||
|
||
`FILE-001~015`의 domain-neutral component mechanics는 Phase 1에서 먼저 만들고 browser mock 기반은 Phase 2가 소유한다. Character·Audio·Series·Community Phase는 자신의 allowed type·crop profile·GIF 예외, multipart handler와 fixture 흐름을 소유하며 Phase 9에서 mock/server 전체 matrix를 회귀 검증한다.
|
||
|
||
### 5.1 P9 수용 기준 증거 요약
|
||
|
||
| PRD 수용 기준 | 증거 | 상태 |
|
||
|---|---|---|
|
||
| `AUTH-001~013`, PRD `14.1` 로그인·세션·로그아웃 | Phase 1 Gate, `tests/e2e/auth.spec.ts`, P9 server-boundary 재검증 | 활성 범위 검증 완료 |
|
||
| `CHAR-001~018`와 Character image/file 기준 | Phase 3 Gate, `tests/e2e/character-workspace.spec.ts`, P9 resource/error regression, P10-T1 | 기존 mock UI 완료. `EXT-001`, `EXT-007` 해결; v2 원작 선택기는 P10-T1에서 완료, active-only server 검증은 P10-GATE에서 분리 |
|
||
| `AUDIO-001~034`와 Audio file/player 기준 | Phase 4 focused/mock 기록, `tests/e2e/audio-content.spec.ts`, P9 resource/error regression, P10-T2, `P4-R9` | 기존 mock UI와 UTC 전송·`timezone` 제거 client/mock 완료. compact audio-only 공용 player UI는 `P4-R9`에서 구현·V3 시각 QA 완료했고 실제 개발 API Audio 수동 QA는 별도 대기 |
|
||
| `SERIES-001~018` | Phase 5 focused/mock 기록, `tests/e2e/series.spec.ts`, P9 resource/error regression, P10-T3 | 연결·순서와 v2 장르·CRUD mock/client 완료. server Series E2E는 로그인 fixture 차단으로 P10-GATE에서 분리 |
|
||
| `COMMUNITY-001~015` | Phase 6 focused/mock 기록, `tests/e2e/community.spec.ts`, P9 resource/error regression, P10-T4 | 기존 mock UI와 pagination object·`timezone` 제거 client/mock 완료. 실제 개발 API Community 수동 QA는 별도 대기 |
|
||
| `FANTALK-001~012` | Phase 7 focused/mock 기록, `tests/e2e/fan-talk.spec.ts`, P9 resource/error regression, P10-T5 | 목록·답변 생성·답변 수정·팬 원글 삭제 mock/client 완료. 실제 개발 API FanTalk 수동 QA는 별도 대기 |
|
||
| `COMMENT-001~008` | P8 계약 부재 이력, OpenAPI 2.3.0 댓글 operation, P10-T6 | `EXT-005` 해결; 댓글 UI·fixture와 focused server route 검증은 P10-T6에서 완료 |
|
||
| `AUDIO-019`, `COMMUNITY-007`, `FILE-001~015`, `EXT-009~011` | 기존 Phase 1·4·6·9 회귀와 P10-T7 | 가격 상한·미정의 오류·파일 client 경계 완료. backend 동일 검증과 실제 개발 API file policy 수동 QA는 별도 대기 |
|
||
| PRD `9`, `10.6~10.7`, `14.2` 반응형·접근성 | P9-T2 `responsive-capabilities.spec.ts`, `accessibility.spec.ts` | Playwright local matrix 완료, Edge/실기기 최신 2개 major는 릴리스 QA |
|
||
| PRD `12`, `MOCK-001~009`, `14.1` mock/server 경계 | Phase 2 Gate, P9-T1/T2, server-boundary E2E | mock UI와 실제 server integration 상태 분리 유지 |
|
||
|
||
## 6. 구현 완료 정의
|
||
|
||
> 아래 체크리스트는 개별 Task가 아니라 최종 Gate 완료 정의다. Phase 10
|
||
> `P10-T1`~`P10-T7`, 과거 Phase 후속 구현, 자동 Gate 정리는 완료됐지만,
|
||
> 실제 개발 API credential·fixture가 필요한 수동 QA는 별도 대기 상태다.
|
||
> 따라서 최종 릴리스 완료 정의는 아직 체크하지 않는다.
|
||
|
||
- [ ] Phase 0~9의 보존된 활성 범위와 Phase 10 후속 Gate가 최신 실행에서 통과한다.
|
||
- [ ] 모든 **확정** 요구사항이 구현, 명시적 non-goal, 또는 결정 기록이 있는 후속 범위 중 하나로 추적된다.
|
||
- [ ] 외부 의존을 추정 endpoint·placeholder DTO·임시 production mock으로 우회하지 않았다.
|
||
- [ ] 제공 계약 범위는 `dev:mock`에서 최종 UI를 확인할 수 있고, 계약 미제공 범위는 fixture로 추정하지 않았다.
|
||
- [ ] 각 도메인의 `UI 확인 완료(mock)`와 `실제 서버 연동 완료(server)` 상태와 증거가 분리돼 있다.
|
||
- [ ] server mode의 404·network error가 mock으로 자동 fallback되지 않고 production build에서 browser mock이 활성화되지 않는다.
|
||
- [ ] 생성·일반 수정·soft delete payload와 enum 보정 contract test가 통과한다.
|
||
- [ ] desktop/tablet/mobile 기능 matrix가 route와 action policy 양쪽에서 일치한다.
|
||
- [ ] 파일 MIME·크기·crop·no-upscale·GIF·오디오 경계 test가 통과한다.
|
||
- [ ] 모든 화면이 Page → feature component → shared/shadcn component 조합 규칙을 따르고 component별 독립 test를 가진다.
|
||
- [ ] media error, upload 취소·재시도, 중복 제출, session 오류가 검증된다.
|
||
- [ ] keyboard, label, focus, contrast, reduced motion, 200% zoom, axe 기준이 충족된다.
|
||
- [ ] 문서와 실제 구현의 알려진 차이가 0건이다.
|
||
- [ ] 검증 기록이 실제 명령과 결과를 포함해 누적돼 있다.
|
||
|
||
## 7. 검증 기록
|
||
|
||
구현 단계마다 아래 형식으로 누적하고 기존 기록을 삭제하거나 덮어쓰지 않는다.
|
||
|
||
> **계약 이력 안내 (2026-07-28):** 아래 2026-07-27 기록의
|
||
> `API Contract §...` 표기는 당시 사용한 삭제 전 `api-contract.md`의
|
||
> section을 가리킨다. 현재 Phase 3 이후 구현 기준은
|
||
> [api-contract.openapi.json](./api-contract.openapi.json)이며 과거 section
|
||
> 표기는 실행 당시 근거를 보존하기 위해 수정하지 않는다.
|
||
>
|
||
> **2026-07-29 추가:** OpenAPI 2.3.0 후속 계약은 Phase 10이 소유한다.
|
||
> 아래 Phase 3~9의 외부 의존·제외 기록은 실행 당시 이력이며 현재 상태는
|
||
> §3.1, Phase 10과 PRD §11.5를 우선한다.
|
||
|
||
```markdown
|
||
### N차 구현 또는 수정 — YYYY-MM-DD
|
||
|
||
- 무엇을: 완료한 Phase와 주요 결과
|
||
- 왜: 해당 범위와 결정 근거
|
||
- 어떻게:
|
||
- `실행 명령` — 성공/실패와 핵심 수치
|
||
- 수동 검증 항목 — 성공/실패/불가 사유
|
||
- 남은 항목: 외부 의존, 후속 범위, 없음 중 하나
|
||
```
|
||
|
||
### 계획 재작성 — 2026-07-26
|
||
|
||
- 무엇을: 기존 18개 선형 Task를 프로젝트 세팅, 공통 플랫폼·인증/인가·컴포넌트 기반, 6개 도메인 vertical slice, 교차 회귀의 9개 Phase로 재구성했다. Phase 1 공통 컴포넌트 소비처 Matrix와 각 도메인 component map을 추가했다.
|
||
- 왜: 프로젝트 세팅을 분리하고 공통 기반을 도메인보다 먼저 완결하며, 각 Phase를 기능·오류·반응형·접근성까지 독립 검증하기 위해서다. 미결·외부 의존은 전역 blocker 대신 소유 도메인에서 결정·제외하도록 했다.
|
||
- 어떻게:
|
||
- `rg -c '^## Phase [0-8]\.' plan-task.md`와 `rg -c '^### Phase [0-8] Gate$' plan-task.md` — Phase 9개, Gate 9개 확인.
|
||
- `rg -c '^- \[ \].*component map을 확정한다' plan-task.md` — 도메인 component map 6개 확인.
|
||
- 요구사항 range와 Phase 1 shared component Matrix `rg` 검사 — 모든 range와 12개 shared component 이름 확인.
|
||
- Markdown fence `awk` 검사 — 24개, 짝수로 균형 확인.
|
||
- `git diff --check` — 성공, whitespace 오류 0건.
|
||
- `git -c core.quotePath=false diff --name-only` — `docs/20260725_AI캐릭터관리자웹/plan-task.md` 1개만 확인, 애플리케이션 코드 변경 0건.
|
||
- 애플리케이션 test/build — 이번 단계는 계획 전용이고 아직 `package.json`이 없어 실행하지 않음.
|
||
- 남은 항목: 후속 구현 전체와 각 도메인에 기록한 backend 외부 의존 계약.
|
||
|
||
### Task 1.1 구현 — 2026-07-26
|
||
|
||
- 무엇을: PRD `10.1~10.9` 기반의 밝은 관리자 디자인 시스템을 `DESIGN.md`와 `src/styles/globals.css`에 문서화·구현하고, Tailwind CSS Vite 플러그인과 shadcn/ui CSS variable mode `components.json`을 최소 구성했다. `StatusBadge`와 `IconOnlyAction` 원시 컴포넌트만 추가했다.
|
||
- 왜: Phase 1 이후 UI가 raw hex나 임의 스타일을 반복하지 않고, `#00BDF7` 고정 브랜드 색과 접근성 대비·light-only 범위를 테스트로 잠그기 위해서다.
|
||
- 어떻게:
|
||
- `python3 .opencode/skills/ui-ux-pro-max/scripts/search.py "enterprise internal admin console data tables forms file upload operational dashboard neutral compact" --design-system --variance 2 --motion 2 --density 9 -p "AI Character Admin" -f markdown` — 성공. 채택: dense operational dashboard, subtle motion, status/focus/reduced-motion/accessibility 항목. 제외: green palette, white primary foreground, dark mode, remote Fira fonts, oversized landing typography, glass/continuous animation, GSAP transition.
|
||
- `python3 .opencode/skills/ui-ux-pro-max/scripts/search.py "animation accessibility z-index loading" --domain ux -n 12` — 성공, ux-guidelines.csv에서 12건 반환. 채택: 300ms 초과 작업의 loading feedback, semantic z-index scale, reduced motion, 150-300ms micro-interaction, decorative infinite animation 금지. 제외/후속: lazy-loaded media, loading buttons, skeleton, route loading은 Task 1.1에 async page·media·form submit·router surface가 없어 후속 실제 소비 Task에서 적용한다.
|
||
- RED `npm run test:run -- src/styles/__tests__/design-system.test.ts src/shared/ui/__tests__/status-badge.test.tsx src/shared/ui/__tests__/icon-only-action.test.tsx src/app/App.test.tsx` — 기대 실패 확인. `DESIGN.md`, `src/styles/globals.css`, `StatusBadge`, `IconOnlyAction` 미구현으로 3 files failed / 1 passed.
|
||
- GREEN `npm run test:run -- src/styles/__tests__/design-system.test.ts src/shared/ui/__tests__/status-badge.test.tsx src/shared/ui/__tests__/icon-only-action.test.tsx src/app/App.test.tsx` — 성공, 4 files / 7 tests passed.
|
||
- `npm run test:run -- src/styles/__tests__/design-system.test.ts src/shared/ui/__tests__/status-badge.test.tsx src/shared/ui/__tests__/icon-only-action.test.tsx src/app/App.test.tsx src/shared/config/env.test.ts` — 성공, 5 files / 10 tests passed.
|
||
- `npm run typecheck` — 성공.
|
||
- `npm run lint` — 성공.
|
||
- `npm run e2e -- --project=chromium tests/e2e/smoke.spec.ts` — 성공, Chromium 1 test passed. Root `lang="ko"`와 `main`의 `AI 캐릭터 관리자`를 실제 브라우저에서 확인했다.
|
||
- `npm run build:dev` — 성공, Vite development build 완료.
|
||
- `npm run build:prod` — 성공, Vite production build 완료.
|
||
- LSP diagnostics — 변경 TypeScript/TSX 파일 모두 오류 0건.
|
||
- `rg -n "#[0-9A-Fa-f]{6}|rgb\(|hsl\(|oklch\(" src --glob '!src/styles/globals.css' --glob '!**/*.test.*' --glob '!**/__tests__/**'` — no output, production feature code raw color 0건.
|
||
- `rg -n "\.dark\b|ThemeProvider|theme toggle|next-themes|prefers-color-scheme" src components.json package.json --glob '!**/*.test.*' --glob '!**/__tests__/**'` — no output, dark/theme integration 0건.
|
||
- 남은 항목: Task 1.1 없음. Auth, router, API, shell, file/media 컴포넌트와 추가 shadcn component는 후속 Task에서 실제 소비 시점에만 추가한다.
|
||
|
||
### Task 1.1 리뷰 차단 수정 — 2026-07-26
|
||
|
||
- 무엇을: Task 1.1 spec/code-quality review blocker를 수정했다. foreground 대비 test를 추가하고, PRD `10.9` UX 검색 기록을 보강했으며, app tsconfig의 Node globals 오염을 test 전용 tsconfig로 분리했다. `IconOnlyAction`의 중복 accessible description과 중복 CSS utility를 제거하고, status/link/info semantic token을 Tailwind theme에 노출했다.
|
||
- 왜: Task 1.1이 spec review와 code-quality review를 통과하려면 core foreground contrast, UX 검색 증거, production/browser tsconfig 순도, tooltip 접근성, Tailwind semantic token 노출이 모두 필요하기 때문이다.
|
||
- 어떻게:
|
||
- RED `npm run test:run -- src/styles/__tests__/design-system.test.ts` — 기대 실패 확인. 신규 core foreground contrast test가 `Missing --foreground`로 실패해 `--foreground`/`--background`/`--card` alias contrast 검증 공백을 드러냈다.
|
||
- `python3 .opencode/skills/ui-ux-pro-max/scripts/search.py "animation accessibility z-index loading" --domain ux -n 12` — 성공, ux-guidelines.csv에서 12건 반환. 채택: loading feedback, stacking context/z-index scale, reduced motion, 150-300ms duration. 제외/후속: lazy loading, skeleton/loading buttons, route loading은 Task 1.1에 실제 async/media/form/router surface가 없어 후속 소비 시점에 적용한다.
|
||
- GREEN `npm run test:run -- src/styles/__tests__/design-system.test.ts src/shared/ui/__tests__/icon-only-action.test.tsx` — 성공, 2 files / 7 tests passed.
|
||
- `npm run test:run -- src/styles/__tests__/design-system.test.ts src/shared/ui/__tests__/status-badge.test.tsx src/shared/ui/__tests__/icon-only-action.test.tsx src/app/App.test.tsx src/shared/config/env.test.ts` — 성공, 5 files / 12 tests passed.
|
||
- `npm run typecheck` — 성공. `tsconfig.app.json`에는 Node type을 두지 않고 `tsconfig.test.json`에만 test용 Node/Jest-DOM type을 둔다.
|
||
- `npm run lint` — 성공.
|
||
- `npm run build:dev` — 성공, Vite development build 완료.
|
||
- `npm run build:prod` — 성공, Vite production build 완료.
|
||
- `npm run e2e -- --project=chromium tests/e2e/smoke.spec.ts` — 성공, Chromium 1 test passed. Root `lang="ko"`와 `main`의 `AI 캐릭터 관리자` smoke behavior 유지 확인.
|
||
- LSP diagnostics — `src/shared/ui/icon-only-action.tsx`, `src/shared/ui/__tests__/icon-only-action.test.tsx`, `tsconfig.json`, `tsconfig.app.json`, `tsconfig.test.json` 오류 0건. `src/styles` 디렉터리 진단 오류 0건.
|
||
- `rg -n '"node"' tsconfig.app.json` — no output, production/browser app tsconfig Node type 0건.
|
||
- `rg -n "#[0-9A-Fa-f]{6}|rgb\(|hsl\(|oklch\(" src --glob '!src/styles/globals.css' --glob '!**/*.test.*' --glob '!**/__tests__/**'` — no output, production feature code raw color 0건.
|
||
- `rg -n "\.dark\b|ThemeProvider|theme toggle|next-themes|prefers-color-scheme" src components.json package.json --glob '!**/*.test.*' --glob '!**/__tests__/**'` — no output, dark/theme integration 0건.
|
||
- 남은 항목: Task 1.1 없음. `.gitignore`는 작업 전 dirty 상태였고 이번 수정에서 건드리지 않았다.
|
||
|
||
### Task 1.2 구현 — 2026-07-26
|
||
|
||
- 무엇을: `sessionStorage` 기반 ADMIN session adapter, Zod envelope parser와 `ApiError`/`AccessDeniedError` API client, pagination helper, MSW test server, TanStack Query client/provider를 최소 구성했다. API client는 React에 의존하지 않고 token reader·clear·auth-expired callback을 주입받으며, API 요청을 console에 기록하지 않는다.
|
||
- 왜: 이후 로그인·보호 route·도메인 API가 동일한 Korean locale, Bearer, 401/403, 오류 envelope, page size, retry 정책을 재구현하지 않도록 공통 경계를 먼저 고정하기 위해서다.
|
||
- 어떻게:
|
||
- `npm install @tanstack/react-query zod` 및 `npm install --save-dev msw` — 성공. Task 1.2에서 실제 소비하는 의존성만 추가.
|
||
- RED `npm run test:run -- src/features/auth/model/auth-session-storage.test.ts src/shared/api/__tests__/pagination.test.ts src/shared/api/__tests__/query-client.test.ts src/shared/api/__tests__/client.test.ts` — 기대 실패. 아직 없는 session/API/pagination/query 모듈의 import를 해결하지 못해 4 files failed.
|
||
- GREEN 같은 focused command — 성공, 4 files / 25 tests passed. success envelope 2형태, locale/Bearer, 5개 HTTP 오류 status, concurrent 401, 403, no-console, pagination, retry와 session adapter를 확인.
|
||
- test file 순도 검토 뒤 client contract와 authenticated request contract를 분리한 focused command — 성공, 5 files / 25 tests passed. 각 TypeScript test file은 200 pure LOC 이하로 유지.
|
||
- `npm run typecheck` — 성공.
|
||
- `npm run lint` — 성공.
|
||
- `npm run build:dev` 및 `npm run build:prod` — 성공.
|
||
- LSP diagnostics — `src/features/auth/model/auth-session-storage.ts`, `src/shared/api/*`, 해당 test 및 `src/main.tsx`, `src/shared/test/*` 오류 0건.
|
||
- `npm run dev` 후 Playwright로 `http://127.0.0.1:8888/` 접근 — 성공. 페이지 title `AI 캐릭터 관리자`와 `main > h1` shell 렌더를 확인했다. console은 기존 favicon 미구현에 따른 `/favicon.ico` 404만 1건이었다.
|
||
- `npm run test:run` 전체 실행은 Vitest가 `tests/e2e/smoke.spec.ts`를 함께 수집해 Playwright의 `test()` 호출 오류로 실패했다. Task 1.2 focused Vitest suite와 기존 unit suite 37 tests는 통과했으며, E2E 파일 분리는 이번 Task 범위 밖으로 유지한다.
|
||
- 남은 항목: Task 1.2 없음. login request payload/session React state/logout UI, protected route와 access-denied page는 Task 1.3~1.4에서 연결한다.
|
||
|
||
### Task 1.2 리뷰 차단 수정 — 2026-07-26
|
||
|
||
- 무엇을: 401 처리 latch를 client lifetime이 아닌 in-flight protected request burst 단위로 재설정하고, 모든 caller-supplied `Authorization`을 제거한 뒤 현재 session의 보호 요청 Bearer만 다시 설정했다. app/test TypeScript project를 분리하고 Vitest가 Playwright E2E spec을 수집하지 않도록 했다.
|
||
- 왜: 이후 로그인 후 동일 client가 다시 401을 받는 경우에도 session clear·redirect가 동작해야 하며, public/login 요청이 caller header로 인증 정보를 유출하거나 unit test runner가 E2E runner를 실행하면 안 되기 때문이다.
|
||
- 어떻게:
|
||
- RED `npm run test:run -- src/shared/api/__tests__/client-auth.test.ts` — 기대 실패, caller-supplied Bearer가 login에 전달되고 두 번째 concurrent 401 burst의 callback 횟수가 1회로 고정됨을 확인했다.
|
||
- GREEN 같은 command — 성공, 1 file / 6 tests passed. public Authorization 제거와 burst별 401 callback 재실행을 확인했다.
|
||
- `npm run test:run -- src/styles/__tests__/design-system.test.ts src/shared/ui/__tests__/status-badge.test.tsx src/shared/ui/__tests__/icon-only-action.test.tsx src/app/App.test.tsx src/shared/config/env.test.ts src/features/auth/model/auth-session-storage.test.ts src/shared/api/__tests__/pagination.test.ts src/shared/api/__tests__/query-client.test.ts src/shared/api/__tests__/client.test.ts src/shared/api/__tests__/client-auth.test.ts` — 성공, 10 files / 37 tests passed.
|
||
- `npm run test:run` — 성공, 10 files / 37 tests passed. `tests/e2e/**` Playwright spec은 Vitest default exclude를 보존한 채 제외했다.
|
||
- `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 성공.
|
||
- `npx tsc --project tsconfig.app.json --listFiles --pretty false | rg 'src/shared/test/(setup|server)\.ts$'` — no output. `tsconfig.test.json` listFiles에는 `src/shared/test/setup.ts`, `server.ts` 포함 확인.
|
||
- `npm run e2e -- --project=chromium tests/e2e/smoke.spec.ts` — 성공, Chromium 1 test passed.
|
||
- LSP diagnostics — 변경 TypeScript와 tsconfig/Vite config 오류 0건. `git diff --check` — 성공.
|
||
- 남은 항목: Task 1.2 없음. 이후 Task 1.3~1.4에서 login/session UI와 route 연결만 구현한다.
|
||
|
||
### Task 1.3 구현 — 2026-07-26
|
||
|
||
- 무엇을: 로그인 request/response contract, `sessionStorage` 전용 ADMIN session provider/model, 실패해도 local session을 제거하는 logout 흐름, visible label·field error 연결·first invalid focus를 갖춘 `LoginPage`를 추가했다. Router, ProtectedRoute, Admin shell, `/login` route wiring은 Task 1.4 범위로 남겨 두었다.
|
||
- 왜: Task 1.4 route 조합 전에 로그인 API 경계, session 복원/제거 규칙, 저장소 금지 규칙, logout 실패 정책, 접근 가능한 로그인 form contract를 독립 테스트로 고정하기 위해서다.
|
||
- 어떻게:
|
||
- RED `npm run test:run -- src/features/auth/tests/auth-api.test.ts src/features/auth/tests/auth-session.test.tsx src/features/auth/tests/login-page.test.tsx` — 기대 실패. 아직 없는 `auth-api`, `auth-session`, `LoginPage` import 3건으로 3 files failed.
|
||
- GREEN 같은 focused command — 성공, 3 files / 17 tests passed. login JSON body·Authorization 없음·refresh 0건, token/role invalid reject, sessionStorage-only restore, logout success/non-2xx/network failure 제거·`/login` navigation·failure warning, LoginPage label/error/focus를 확인했다.
|
||
- refactor 후 focused command 재실행 — 성공, 3 files / 17 tests passed.
|
||
- `npm run test:run` — 성공, 13 files / 54 tests passed.
|
||
- `npm run typecheck` — 성공.
|
||
- `npm run lint` — 성공. 중간에 Fast Refresh warning 1건을 `auth-session-context.ts` 분리로 제거한 뒤 재실행했다.
|
||
- `npm run build:dev` — 성공, Vite development build 완료.
|
||
- `npm run build:prod` — 성공, Vite production build 완료.
|
||
- `npm run e2e -- tests/e2e/smoke.spec.ts` — 성공, chromium/webkit/mobile-chrome/mobile-safari 4 tests passed. 현재 App root shell은 Task 1.4 전 상태로 유지됨을 확인했다.
|
||
- `GIT_MASTER=1 git diff --check` — 성공.
|
||
- LSP diagnostics — `auth-api.ts`, `auth-session-context.ts`, `login-schema.ts`, `LoginPage.tsx`, 3개 auth test 파일 오류 0건. `auth-session.tsx` 개별 파일 진단은 반복 timeout이었고, `src/features/auth/model` 디렉터리 진단 0건 및 `npm run typecheck` 성공으로 보완 확인했다.
|
||
- changed file pure LOC — `auth-api.ts` 27, `auth-session-context.ts` 24, `auth-session.tsx` 53, `login-schema.ts` 6, `LoginPage.tsx` 118, `auth-api.test.ts` 89, `auth-session.test.tsx` 130, `login-page.test.tsx` 36으로 모두 200 LOC 이하.
|
||
- 남은 항목: Task 1.3 없음. LoginPage browser visual QA는 아직 route/App surface에 연결하지 말라는 Task 1.3 범위 제한 때문에 RTL과 기존 root smoke로만 검증했고, 실제 `/login` route browser QA는 Task 1.4에서 수행한다.
|
||
|
||
### Task 1.4 Wave 2 구현 — 2026-07-26
|
||
|
||
- 무엇을: `/ai-characters`에 ADMIN session 전용 shell, `/access-denied` route, 401 login redirect, 403 access denied redirect, logout control, breadcrumb, skip link, mobile menu, 명시적 Phase 2 empty state를 연결했다.
|
||
- 왜: Phase 2 도메인 데이터 구현 전에 보호 route·권한 오류·session expiry·shell 접근성 contract를 먼저 고정하기 위해서다.
|
||
- 어떻게:
|
||
- RED `npm run test:run -- src/app/App.test.tsx` — 기대 실패, 기존 placeholder shell 때문에 신규 shell/401/403/mobile/logout 기대 6건 실패.
|
||
- GREEN 같은 focused command — 성공, 1 file / 9 tests passed.
|
||
- `npm run typecheck` — 성공.
|
||
- `npm run lint` — 성공. 중간에 effect 내부 동기 `setState` lint를 비동기 성공 경로로 옮긴 뒤 재실행했다.
|
||
- `npm run test:run -- src/app/App.test.tsx src/shared/api/__tests__/client-auth.test.ts src/styles/__tests__/design-system.test.ts` — 성공, 3 files / 21 tests passed.
|
||
- `npm run build:prod` — 성공.
|
||
- Playwright 수동 QA — `/login`, ADMIN session `/ai-characters`, mobile menu Escape focus return, logout `/login`, unauth `/ai-characters` redirect, 403 `/access-denied`와 protected empty state 미노출을 확인했다.
|
||
- LSP diagnostics — `browser-location.ts` 오류 0건. `App.tsx`, `App.test.tsx`, `route-paths.ts` 개별 fresh 진단은 반복 timeout이었고, TypeScript LSP alive 상태와 `npm run typecheck` 성공으로 보완 확인했다.
|
||
- changed file pure LOC — `App.tsx` 214, `App.test.tsx` 136, `browser-location.ts` 24, `route-paths.ts` 6. `App.tsx`는 200-250 경고 구간이므로 다음 shell 확장 전 분리 후보로 남긴다.
|
||
- 남은 항목: Task 1.4의 320px/200% zoom E2E와 axe critical·serious 0건 확인은 이번 Wave 2 기대 범위 밖이라 체크하지 않았다.
|
||
|
||
### Task 1.6 구현 — 2026-07-27
|
||
|
||
- 무엇을: 공통 `FileField`, file validation, audio file policy, image policy contract, `ImageCropDialog`, `UploadProgress`, `AdminAudioPlayer`, `AudioPlaybackProvider`를 공유 레이어에 추가하고 `DESIGN.md` Section 5에 primitive contract를 기록했다.
|
||
- 왜: Phase 2 이후 도메인 form이 endpoint/query cache/domain DTO 없이 파일·crop·upload 표시·audio playback mechanics를 재사용하도록 하기 위해서다.
|
||
- 어떻게:
|
||
- RED `npm run test:run -- src/shared/validation/file-media-policy.test.ts src/shared/lib/crop-image.test.ts src/shared/ui/__tests__/file-field.test.tsx src/shared/ui/__tests__/image-crop-dialog.test.tsx src/shared/ui/__tests__/upload-progress.test.tsx src/shared/ui/__tests__/admin-audio-player.test.tsx src/shared/ui/__tests__/file-media-dependency-boundary.test.ts` — 기대 실패. 아직 없는 shared file/media module import와 dependency boundary 대상 파일 부재로 7 files failed.
|
||
- GREEN 같은 command — 성공, 7 files / 19 tests passed. 주입 `maxBytes` 10MB 경계, audio `1,024,000,000 bytes`, `.m4a + audio/x-m4a`, WAV 거부, no codec sniffing, no-upscale crop output, keyboard/button 대안, upload callback-only, audio no autoplay/refetch/download, signed URL no log/storage, dependency boundary를 확인했다.
|
||
- crop dependency 결정: `package.json`에 crop dependency가 없고 이번 Phase는 raw pointer drag/pinch가 필수가 아니므로 새 dependency 없이 native button/range/keyboard 이동을 선택했다. Canvas는 결과 `File` 생성 함수에만 사용한다.
|
||
- `npm run test:run -- src/shared` — 성공, 21 files / 57 tests passed.
|
||
- `npm run typecheck`, `npm run lint`, `npm run build` — 모두 성공.
|
||
- LSP diagnostics — `src/shared`와 `src/shared/ui` directory 진단 0건. 일부 개별 file fresh 진단은 timeout이었고 TypeScript LSP alive, `npm run typecheck`, `npm run build` 성공으로 보완 확인했다.
|
||
- changed file pure LOC — 최대 `admin-audio-player.tsx` 122, `image-crop-dialog.tsx` 118로 모두 200 LOC 이하.
|
||
|
||
### Goal 실행 계약 보완 — 2026-07-27
|
||
|
||
- 무엇을: 완료된 Phase 0~1을 포함한 Phase 0~8, 구현 Task 34개, Phase Gate 9개에 goal ID, objective, 시작 조건, 완료 증거와 범위 밖을 추가했다. goal의 단일 활성 작업, 완료·차단 판정, 외부 계약 처리와 검증 기록 규칙을 공통 운영 규칙으로 명시했다.
|
||
- 왜: goal 기능이 Task 하나를 독립 실행 단위로 이어서 수행하고, 일부 구현만으로 완료하거나 외부 계약을 추정하지 않으며, Phase Gate까지 객관적인 증거로 종결할 수 있게 하기 위해서다.
|
||
- 어떻게:
|
||
- `rg -c '^## Phase [0-8]\.' docs/20260725_AI캐릭터관리자웹/plan-task.md` — Phase 9개 확인.
|
||
- `rg -c '^\*\*Phase Goal `P[0-8]`' docs/20260725_AI캐릭터관리자웹/plan-task.md` — Phase Goal 9개 확인.
|
||
- `awk '/^## Phase 0\./,/^## 5\./ { if ($0 ~ /^### Task [0-8]\.[0-9]+ /) n++ } END { print n }' docs/20260725_AI캐릭터관리자웹/plan-task.md` — 구현 Task 34개 확인.
|
||
- `rg -c '^\*\*Goal 실행 `P[0-8]-T[0-9]+`' docs/20260725_AI캐릭터관리자웹/plan-task.md` — Task goal 34개 확인.
|
||
- `rg -c '^### Phase [0-8] Gate$'`와 `rg -c '^\*\*Goal 실행 `P[0-8]-GATE`'` — Gate와 Gate goal 각각 9개 확인.
|
||
- PRD `AUDIO-027~028`, API Contract §5.0/§5.3, Phase 3 요구사항·contract/form checklist 대조 — `themeId`와 공통 테마 목록 계약이 일치함을 확인.
|
||
- 애플리케이션 test/build — 문서와 goal 실행 계약만 변경해 실행하지 않음.
|
||
- 남은 항목: Phase 2 이후 구현과 각 도메인의 backend 제공 대기 계약.
|
||
|
||
### Mock Preview Phase 추가 및 후속 Phase 재번호 — 2026-07-27
|
||
|
||
- 무엇을: 제공된 API Contract 범위에서 backend 구현 전 최종 UI를 확인할 수 있도록 개발 전용 browser MSW 요구사항과 계약 경계를 추가하고, 완료된 Phase 0~1 다음에 신규 Phase 2를 배치했다. 기존 Character~최종 인수인계 Phase는 Phase 3~9로 이동하고 모든 활성 Task·Goal·Gate·의존성·추적표를 함께 재번호했다.
|
||
- 왜: 실제 API의 404를 mock으로 자동 대체하지 않으면서도 frontend가 최종 화면을 개발·검토할 수 있고, mock UI 확인과 실제 server integration 완료를 혼동하지 않기 위해서다.
|
||
- 어떻게:
|
||
- PRD `MOCK-001~009`, API Contract §1.5, Phase 2의 Task 2.1~2.3과 Gate 대조 — explicit `server | mock` mode, production 차단, no-auto-fallback, deterministic in-memory store와 mock banner 규칙 일치 확인.
|
||
- 활성 계획 검사 — Phase·Phase Goal·Gate·Gate Goal 각각 10개, 일반 구현 Task·Task Goal 각각 37개, Goal 선언 중복 0건, 활성 영역의 이전 Phase 번호 참조 0건.
|
||
- 문서 동기화 검사 — `docs/agent-guide/{prd,goal-plan}.md`와 `docs/sample/{sample-prd,sample-plan-task}.md` 4개 모두 같은 mock 작성 규칙 포함, 미구현 `docs/agent-guide/{environment,scripts}.md`의 `dev:mock`·`e2e:mock`·`VITE_API_MODE` 선반영 0건.
|
||
- Markdown fence 짝수 여부와 수정 문서의 내부 link 검사 — 이상 0건.
|
||
- `git diff --check -- <수정 문서 7개>` — 성공.
|
||
- 애플리케이션 test/build는 구현 코드와 설정을 변경하지 않았고 신규 명령도 아직 계획 상태이므로 실행하지 않는다.
|
||
- 남은 항목: Phase 2 구현 시 실제 `dev:mock`·`e2e:mock` 명령과 환경 변수를 만든 후 README와 `docs/agent-guide/{environment,scripts}.md`를 실체에 맞게 갱신한다. 계약 미제공 도메인은 backend 계약 수신 전 fixture를 만들지 않는다.
|
||
|
||
### Phase 2 코드 리뷰·QA — 2026-07-27
|
||
|
||
- 무엇을: Phase 2 staged 구현 28개 경로를 PRD `MOCK-001~009`, API Contract §1·§3, `P2-T1~P2-GATE`와 대조하고 확정 문제 5건을 `P2-R1~P2-R3`으로 전환했다.
|
||
- 왜: 자동 검증 통과와 별개로 보호 route fail-closed, exact API origin, invalid·revoked JWT status, Phase 번호 표시와 완료 문서 추적성이 실제 계약과 일치하는지 독립적으로 판정하기 위해서다.
|
||
- 어떻게:
|
||
- `npm run test:run` — 성공, 34 files / 130 tests passed.
|
||
- `npm run test:run -- src/shared/config src/shared/mocks src/shared/ui/__tests__/mock-mode-banner.test.tsx` — 성공, 7 files / 20 tests passed.
|
||
- `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod`, `git diff --check`, `git diff --cached --check` — 모두 성공.
|
||
- `VITE_API_MODE=mock npm run build:prod` — 기대한 거부, exit 1과 `VITE_API_MODE=mock is only available during development` 확인.
|
||
- `npm run e2e:mock -- tests/e2e/mock-preview-shell.spec.ts` — 최초 sandbox listen EPERM, 권한 허용 후 성공, 4 projects / 20 tests passed.
|
||
- `npm run e2e:mock -- tests/e2e/mock-mode-boundary.spec.ts` — 성공, 4 projects / 4 tests passed.
|
||
- `npm run e2e -- tests/e2e/server-mode-boundary.spec.ts` — 성공, 4 projects / 12 tests passed. 이 통과 과정에서 404·network error alert가 보호 shell 내부에서 렌더되는 회귀를 별도 확정했다.
|
||
- `npm run e2e -- tests/e2e/smoke.spec.ts tests/e2e/auth.spec.ts tests/e2e/accessibility-shell.spec.ts` — 성공, 4 projects / 20 tests passed.
|
||
- Vite SSR로 `createMockHandlers(createMockStore())`를 실행한 재현 — wrong-origin login 200, invalid JWT logout 200, 정상 logout 200, revoked JWT 재logout 200을 확인했다. Vite HMR WebSocket은 sandbox listen EPERM 경고가 있었으나 MSW request 재현 command는 exit 0으로 완료됐다.
|
||
- 남은 항목: `REV-P2-001~005`를 수정하는 `P2-R1~P2-R3`. 기존 Phase 2 완료 체크와 검증 기록은 되돌리지 않는다.
|
||
|
||
### Phase 2 구현·Gate 완료 기록 — 2026-07-27
|
||
|
||
- 무엇을: explicit `server | mock` mode, 개발 전용 browser MSW bootstrap, auth preview fixture, mock banner, no-auto-fallback E2E, production mock 차단과 실행 문서를 구현했다.
|
||
- 왜: backend endpoint 구현 전에도 제공 API Contract 범위의 최종 UI를 mock mode에서 확인하되, 기본 server mode와 production build가 mock으로 자동 대체되지 않게 하기 위해서다.
|
||
- 어떻게:
|
||
- P2-T1 focused unit·boundary E2E, P2-T2 auth handler/banner/mock preview E2E, P2-T3 docs/accessibility E2E와 P2-GATE를 2026-07-27 본문 기록대로 실행했다.
|
||
- P2-GATE 기준 `npm run test:run -- src/shared/config src/shared/mocks src/shared/ui/__tests__/mock-mode-banner.test.tsx`는 7 files / 20 tests 통과했다.
|
||
- `npm run e2e:mock -- tests/e2e/mock-preview-shell.spec.ts`는 4 browser projects / 20 tests 통과했고 mock login, protected shell, logout 후 재login, 320px·200% zoom, axe critical·serious 0건을 확인했다.
|
||
- `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod`는 모두 exit 0이었다.
|
||
- Phase 2 코드 리뷰에서 열린 `REV-P2-001~005`는 이 완료 기록을 삭제하지 않고 `P2-R1~P2-R3` 회귀 Task로 별도 추적한다.
|
||
- 남은 항목: `P2-R1~P2-R3` 수정 완료 전에는 Phase 2 리뷰를 닫지 않는다. mock 통과는 후속 도메인 server integration 완료로 간주하지 않는다.
|
||
|
||
### Phase 2 3차 코드 리뷰·QA — 2026-07-27
|
||
|
||
- 무엇을: `P2-R4`~`P2-R7` 수정 뒤의 현재 working tree를 재검토해 `REV-P2-010` Low 1건과 `REV-P2-011~013` Medium 3건을 확정하고 `P2-R8`~`P2-R10` 회귀 Task로 전환했다.
|
||
- 왜: 완료 문서가 최종 변경 경로를 추적하는지, Mock Preview banner 이동 뒤 mobile menu의 background inert·responsive breakpoint 전환과 보호 route 오류 recovery가 유지되는지 확인하기 위해서다.
|
||
- 어떻게:
|
||
- `npm run test:run -- src/shared/config src/shared/mocks src/shared/ui/__tests__/mock-mode-banner.test.tsx` — 7 files / 25 tests 통과.
|
||
- `npm run test:run` — 34 files / 141 tests 통과.
|
||
- `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0.
|
||
- `VITE_API_MODE=mock npm run build:prod` — 기대대로 exit 1. production 산출물의 worker 파일·mock bootstrap 문자열은 0건.
|
||
- `npm run e2e` — 4 projects / 32 tests 통과. `npm run e2e:mock` — 4 projects / 24 tests 통과.
|
||
- Chromium one-off — login과 Character probe 응답이 모두 `fromServiceWorker=true`임을 확인했다. 320px menu open에서는 banner가 inert 경계 밖에 있었고, 1,200px 전환 뒤 overlay는 숨겨졌지만 main inert·`aria-hidden=true`가 남았다.
|
||
- 코드·test 대조 — 보호 route 오류 page의 interactive recovery control 0건과 App/server boundary retry test 0건을 확인했다.
|
||
- `git diff HEAD --name-only | wc -l` — 37개 경로, `git diff --check HEAD` — exit 0.
|
||
- 문서 반영 검증 — `npm run test:run -- src/shared/mocks/__tests__/mock-preview-docs.test.ts`는 1 file / 3 tests 통과, review 상세 ID 13개·Phase 2 회귀 Task 10개를 확인했고 stale 3차 범위 문자열과 문서 whitespace 오류는 0건이었다.
|
||
- 변경 범위: [Phase 2 리뷰](./reviews/review-phase-2.md)에 3차 근거·발견·판정을 누적하고 이 문서에 `P2-R8`~`P2-R10`만 추가했다. 애플리케이션 코드·test·설정은 변경하지 않았다.
|
||
- 남은 항목: `P2-R8` 문서 추적성, `P2-R9` mobile menu 반응형·inert, `P2-R10` 보호 route 오류 retry 복구. 세 goal의 대체 검증 또는 RED/GREEN/REFACTOR, 관련 E2E와 P2 Gate가 끝나기 전에는 Phase 2 리뷰를 닫지 않는다.
|
||
|
||
### P2-R9 수정 검증 — 2026-07-27
|
||
|
||
- 무엇을: `REV-P2-011`, `REV-P2-012`를 `P2-R9` 범위에서 수정했다. Mock Preview 성공 shell의 banner를 `ProtectedAdminShell` background inert container 안으로 옮기고, native `matchMedia('(min-width: 1024px)')` change에서 mobile menu state와 inert·`aria-hidden`을 해제하되 숨겨진 mobile trigger로 focus를 복귀하지 않게 했다.
|
||
- 왜: mobile menu가 열린 상태에서 background 전체가 같은 접근성 차단 경계에 속해야 하며, `lg` 이상 viewport로 전환될 때 보이는 desktop navigation·logout·main이 즉시 다시 조작 가능해야 하기 때문이다.
|
||
- 어떻게:
|
||
- RED unit: `npm run test:run -- src/app/App.test.tsx src/shared/ui/__tests__/mock-mode-banner.test.tsx` — 2 files 중 `App.test.tsx` 2 tests가 기대대로 실패했다. 실패 핵심은 `mock banner inert background not found`와 `모바일 주 메뉴` 잔존이다.
|
||
- RED e2e: `npm run e2e:mock -- tests/e2e/mock-preview-shell.spec.ts` — 4 browser projects에서 신규 mock shell test가 모두 기대대로 실패했다. 실패 핵심은 320px open state의 `bannerInert=false`, `bannerHidden=false`, `mainInert=true` 불일치다.
|
||
- GREEN unit: 같은 focused unit command — 2 files / 26 tests 통과.
|
||
- GREEN e2e: 같은 mock e2e command — 28 tests 통과. 320px open state에서 banner·main이 같은 inert/`aria-hidden` 경계에 있고 1,024px·1,200px 전환 뒤 mobile menu가 제거되며 desktop navigation·logout이 보이는 것을 확인했다.
|
||
- Regression: `npm run e2e -- tests/e2e/accessibility-shell.spec.ts` — 32 tests 통과. `npm run typecheck`, `npm run lint` — 모두 exit 0.
|
||
- LSP diagnostics: `src/app/App.tsx`, `src/app/protected-admin-shell.tsx`, `src/app/App.test.tsx`, `tests/e2e/mock-preview-shell.spec.ts` 모두 0 diagnostics.
|
||
- P2 focused Gate: `npm run test:run -- src/shared/config src/shared/mocks src/shared/ui/__tests__/mock-mode-banner.test.tsx` — P2-R9 문서 기록 반영 전에는 docs contract 1 test가 실패했다. 기록 반영 후 재실행해 7 files / 25 tests 통과했다.
|
||
- 남은 항목: `P2-R10` 보호 route 오류 retry 복구. mock 통과는 후속 도메인 server integration 완료로 간주하지 않는다.
|
||
|
||
### P2-R10 수정 검증 — 2026-07-27
|
||
|
||
- 무엇을: `REV-P2-013`을 `P2-R10` 범위에서 수정했다. 보호 route 오류 page에 native retry button을 추가하고, 현재 session·route visit·retry attempt가 모두 일치할 때만 오류나 검증 성공을 사용하도록 했다. 정상 `ApiError`의 서버 message는 유지하고 network 오류에는 안전한 공통 안내를 표시한다.
|
||
- 왜: 404·network 오류 뒤에도 사용자가 browser refresh 없이 복구할 수 있어야 하며, 현재 수동 retry가 성공하기 전에는 보호 shell·navigation·logout을 계속 숨겨야 하기 때문이다.
|
||
- 어떻게:
|
||
- RED unit: `npm run test:run -- src/app/App.test.tsx` — 1 file / 26 tests 중 신규 2 tests가 `보호 route 다시 시도` button 부재로 기대대로 실패했고 24 tests는 통과했다.
|
||
- RED E2E: `npm run e2e -- tests/e2e/server-mode-boundary.spec.ts` — 32 tests 중 신규 retry 시나리오가 4 browser project에서 button 부재로 기대대로 실패했고 28 tests는 통과했다.
|
||
- GREEN unit: 같은 App command — 1 file / 26 tests 통과. 404 `없습니다.` 보존, network 재실패의 공통 안내·retry 유지, pending 중 shell·logout 비노출과 성공 뒤 shell 표시를 확인했다.
|
||
- GREEN E2E: 같은 server command — 4 projects / 32 tests 통과. 명시적 retry 전후 browser MSW worker 0건과 성공 전 shell·logout 0건을 확인했다.
|
||
- Regression: `npm run test:run -- src/shared/config src/shared/mocks src/shared/ui/__tests__/mock-mode-banner.test.tsx` — 7 files / 25 tests 통과. `npm run test:run` — 34 files / 145 tests 통과.
|
||
- Static/build: `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0. no-excuse 검사도 변경 TS/TSX 3 files / 위반 0건이었다.
|
||
- LSP diagnostics: `src/app` 4 files, `tests/e2e` 6 files에서 diagnostics 0건.
|
||
- 수동·시각 확인: server mode 실제 브라우저 375px·768px·1,280px에서 button 높이 44px, `:focus-visible=true`, 한국어 clipping·비정상 줄바꿈 0건을 확인했다. retry 성공 뒤 shell·logout 표시와 mock worker 0건을 확인했고 기능 무결성·CJK 정밀 검토가 모두 PASS였다.
|
||
- Diff: `git diff --check -- src/app/App.tsx src/app/App.test.tsx tests/e2e/server-mode-boundary.spec.ts docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md` — exit 0.
|
||
- 남은 항목: `P2-R10` 범위 없음. mock 통과는 후속 도메인 server integration 완료로 간주하지 않는다.
|
||
|
||
### Phase 2 4차 코드 리뷰·QA — 2026-07-27
|
||
|
||
- 무엇을: `P2-R8`~`P2-R10` 반영 뒤 working tree 전체 집계, 보호 route pending UI와 mode별 focused E2E 증거를 재검토해 `REV-P2-014` Low 1건과 `REV-P2-015~016` Medium 2건을 확정하고 `P2-R11`~`P2-R13`으로 전환했다.
|
||
- 왜: 완료 판정이 untracked 파일, 300ms 이상 retry 대기 상태와 Task가 지정한 단일 E2E spec의 실제 수집 범위를 빠뜨리지 않는지 확인하기 위해서다.
|
||
- 어떻게:
|
||
- `npm run test:run -- src/app/App.test.tsx src/shared/ui/__tests__/mock-mode-banner.test.tsx` — 2 files / 28 tests 통과. `npm run test:run` — 34 files / 145 tests 통과.
|
||
- `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0, build는 각 160 modules 변환.
|
||
- `VITE_API_MODE=mock npm run build:prod` — 기대대로 exit 1. production worker 파일과 mock bootstrap 문자열은 0건.
|
||
- `npm run e2e` — 4 projects / 32 tests 통과. `npm run e2e:mock` — 4 projects / 28 tests 통과.
|
||
- `git diff HEAD --name-only | wc -l` — tracked 37개, `git status --porcelain=v1 | wc -l` — 전체 38개, `git ls-files --others --exclude-standard` — `src/app/protected-admin-shell.tsx` 1개.
|
||
- `npm run e2e -- --list tests/e2e/server-mode-boundary.spec.ts` — 4 files / 32 tests, 직접 `VITE_API_MODE=server npx playwright test tests/e2e/server-mode-boundary.spec.ts --list` — 1 file / 12 tests. 고정 script가 focused filter를 무효화하고 `P2-R10` 기대 16 tests 미달을 가리는 것을 확인했다.
|
||
- Chromium one-off — 404 retry 성공 응답을 350ms 지연했을 때 `#root` child·`main`·`status`·`alert` 0개, active element `BODY`; 응답 뒤 shell·logout 표시를 확인했다. 최초 sandbox local listen·browser launch 실패는 권한 허용 재실행으로 보완했다.
|
||
- 문서 반영 검증 — `npm run test:run -- src/shared/mocks/__tests__/mock-preview-docs.test.ts`는 1 file / 3 tests 통과. review 상세 ID 16개, Phase 2 회귀 Task 13개와 문서 whitespace 오류 0건을 확인했다.
|
||
- 변경 범위: [Phase 2 리뷰](./reviews/review-phase-2.md)에 4차 근거·발견·판정을 누적하고 이 문서에 `P2-R11`~`P2-R13`만 추가했다. 애플리케이션 코드·test·설정은 변경하지 않았다.
|
||
- 남은 항목: `P2-R11` working tree 추적성, `P2-R12` 보호 route pending 피드백, `P2-R13` focused E2E filter·network retry 증거. 세 goal의 대체 검증 또는 RED/GREEN/REFACTOR와 관련 Gate가 끝나기 전에는 Phase 2 리뷰를 닫지 않는다.
|
||
|
||
### P2-R11~P2-R13 수정 검증 — 2026-07-27
|
||
|
||
- 무엇을: `REV-P2-014~016`을 `P2-R11`~`P2-R13` 범위에서 수정했다. working tree 범위는 tracked 37개 + untracked `src/app/protected-admin-shell.tsx` 1개로 구분해 기록하고, 보호 route pending에는 기존 `PageState` loading status를 표시했다. E2E script의 spec allowlist는 `playwright.config.ts` `testMatch`로 옮기고 network retry 재실패 E2E를 추가했다.
|
||
- 왜: 완료 증거가 untracked 파일, 접근 가능한 300ms 이상 pending feedback, focused E2E file filter와 실제 network retry 경계를 빠뜨리지 않게 하기 위해서다.
|
||
- 어떻게:
|
||
- P2-R11 대체 검증: `git diff HEAD --name-only | wc -l` — tracked 37개, `git status --short --untracked-files=all | wc -l` — 전체 38개, `git ls-files --others --exclude-standard` — `src/app/protected-admin-shell.tsx` 1개.
|
||
- P2-R12 RED: `npm run test:run -- src/app/App.test.tsx` — 최초 pending 전용 test 1건과 기존 404 retry test의 pending assertion이 `role="status"` 부재로 기대대로 실패했다.
|
||
- P2-R12 GREEN: 같은 command — 1 file / 27 tests 통과. 최초 probe와 retry pending 중 `role="status"`, 보호 `main`·logout 0건을 확인했다.
|
||
- P2-R13 RED: `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts` — npm script와 Playwright config contract 2 tests가 기대대로 실패했다.
|
||
- P2-R13 GREEN focused: `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 2 files / 6 tests 통과. `npm run e2e -- --list tests/e2e/server-mode-boundary.spec.ts` — 1 file / 16 tests, `npm run e2e:mock -- --list tests/e2e/mock-preview-shell.spec.ts` — 1 file / 24 tests.
|
||
- Surface: `npm run e2e -- tests/e2e/server-mode-boundary.spec.ts` — 16 tests 통과. `npm run e2e:mock -- tests/e2e/mock-preview-shell.spec.ts` — 24 tests 통과.
|
||
- LSP diagnostics: `src/app/App.tsx`, `src/app/App.test.tsx`, `playwright.config.ts`, `src/shared/mocks/__tests__/mode-boundary.test.ts` 0 diagnostics. `tests/e2e/server-mode-boundary.spec.ts` 단일 fresh diagnostics는 timeout이었고 focused E2E와 typecheck로 보완한다.
|
||
- Final Gate: `npm run test:run` — 34 files / 147 tests 통과. `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0. `npm run e2e` — 36 tests 통과, `npm run e2e:mock` — 28 tests 통과.
|
||
- Diff: `git diff --check -- docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md tests/e2e/server-mode-boundary.spec.ts package.json playwright.config.ts README.md docs/agent-guide/scripts.md src/app/App.tsx src/app/App.test.tsx src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts` — exit 0.
|
||
- 남은 항목: `P2-R11`~`P2-R13` 범위 없음. mock 통과는 후속 도메인 server integration 완료로 간주하지 않는다.
|
||
|
||
### Phase 2 5차 코드 리뷰·QA — 2026-07-27
|
||
|
||
- 무엇을: `P2-R11`~`P2-R13` 반영 뒤 실제 test case 구조·실행 수와 plan/review의 완료 상태를 대조해 `REV-P2-017` Low 1건을 확정하고 `P2-R14` 회귀 Task로 전환했다.
|
||
- 왜: 동작 검증 통과와 별개로 TDD 완료 체크·검증 수치가 실제 구현과 같은 사실을 표시하는지 확인하기 위해서다.
|
||
- 어떻게:
|
||
- `npm run test:run -- src/app/App.test.tsx` — 1 file / 27 tests 통과. `rg -c '^test\(' src/app/App.test.tsx` — 27건.
|
||
- 코드 대조 — 최초 probe pending 전용 test 1건을 추가했고 retry pending은 기존 404 retry test에 assertion을 보강했음을 확인했다. `P2-R12`의 두 신규 test·App 28 tests 이상 완료 체크와 불일치한다.
|
||
- `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 2 files / 6 tests 통과. `npm run test:run` — 34 files / 147 tests 통과.
|
||
- `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0. `VITE_API_MODE=mock npm run build:prod` — 기대한 production guard로 exit 1.
|
||
- focused E2E list는 server 1 file / 16 tests, mock 1 file / 24 tests. 최초 sandbox listen `EPERM` 뒤 허용된 로컬 실행에서 server boundary 16, mock preview 24, bare server 36, bare mock 28 tests가 모두 통과했다.
|
||
- working tree는 tracked diff 37개 + untracked `src/app/protected-admin-shell.tsx` 1개 = 전체 38개 항목으로 유지됐다.
|
||
- 변경 범위: [Phase 2 리뷰](./reviews/review-phase-2.md)에 `REV-P2-017`과 5차 근거·판정을 누적하고 이 문서에 `P2-R14`만 추가했다. 애플리케이션 코드·test·설정은 변경하지 않았다.
|
||
- 남은 항목: `P2-R14` 범위 없음. mock 통과는 후속 도메인 server integration 완료로 간주하지 않는다.
|
||
|
||
### P2-R14 수정 검증 — 2026-07-27
|
||
|
||
- 무엇을: `REV-P2-017`을 `P2-R14` 범위에서 수정했다. P2-R12 완료 증거를 실제 App test 구조인 최초 pending 전용 test 1건 + 기존 404 retry test의 pending assertion 보강 + App 27 tests로 정정했다.
|
||
- 왜: 문서가 “두 신규 test·28 tests 이상”을 완료 조건처럼 표시하면 후속 reviewer가 실제 test case 수와 검증 시나리오 수를 혼동하기 때문이다.
|
||
- 어떻게:
|
||
- 대체 RED: `rg -c '^test\(' src/app/App.test.tsx` — 27건. `rg -n 'initial protected route probe is pending|retries a protected route 404' src/app/App.test.tsx` — 최초 pending 전용 test와 기존 404 retry test 위치를 확인했다.
|
||
- GREEN docs: P2-R12 TDD 절차·기대 결과, P2-R11~P2-R13 수정 검증 기록, review 요약·종료 판정을 실제 구조와 일치시켰다.
|
||
- Focused: `npm run test:run -- src/app/App.test.tsx` — 1 file / 27 tests 통과.
|
||
- 문서 검증: stale `신규 pending status tests 2건` 검색 결과 0건, `git diff --check -- docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md` — exit 0.
|
||
- 남은 항목: Phase 2 review의 열린 확정 항목 없음. 애플리케이션 코드·test·설정은 변경하지 않았다.
|
||
|
||
### Phase 2 6차 코드 리뷰·QA — 2026-07-27
|
||
|
||
- 무엇을: `P2-R14` 반영 뒤 Phase 2 구현·test·production·mode별 E2E와 review 현재 상태를 다시 대조해 `REV-P2-018` Low 1건을 확정하고 `P2-R15`로 전환했다.
|
||
- 왜: review 요약·상세·종료 판정과 plan 전환 절이 완료된 회귀 Task의 현재 상태를 같은 의미로 표시하는지 확인하기 위해서다.
|
||
- 어떻게:
|
||
- `npm run test:run` — 34 files / 147 tests 통과.
|
||
- `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0, build는 각각 160 modules 변환.
|
||
- `VITE_API_MODE=mock npm run build:prod` — 기대한 production guard로 exit 1.
|
||
- `npm run e2e` — 최초 sandbox listen `EPERM` 뒤 로컬 실행 권한으로 재실행해 4 projects / 36 tests 통과.
|
||
- `npm run e2e:mock` — 최초 sandbox listen `EPERM` 뒤 로컬 실행 권한으로 재실행해 4 projects / 28 tests 통과.
|
||
- Chromium one-off — login·보호 route 응답 `fromServiceWorker=true`, 320px menu·logout 높이 60px, mobile link 선택 뒤 menu·background inert 잔존 0건을 확인했다.
|
||
- `git diff --check HEAD` — review 문서 반영 전 exit 0. review §7의 `P2-R8`~`P2-R13` “아직 수정하지 않았다” 2건과 §5·§6·§8의 수정 완료 상태가 모순됨을 확인했다.
|
||
- 문서 반영 후 `npm run test:run -- src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 1 file / 3 tests 통과, plan/review 대상 `git diff --check` — exit 0.
|
||
- 변경 범위: 이 review 문서와 `plan-task.md`에 `REV-P2-018`, `P2-R15` 판정·후속 계획만 추가했다. 애플리케이션 코드·test·설정은 변경하지 않았다.
|
||
- 남은 항목: `P2-R15`에서 review §7의 현재 상태 문구를 정정하고 문서 검색·diff 검증을 누적해야 한다.
|
||
|
||
### Phase 2 7차 코드 리뷰·QA — 2026-07-27
|
||
|
||
- 무엇을: `P2-R15` 반영 뒤 Phase 2 상단 현재 상태, Task 본문, review 종료 판정과 하단 최신 Progress를 대조해 `REV-P2-019` Low 1건을 확정하고 `P2-R16`으로 전환했다.
|
||
- 왜: 완료된 회귀 Task의 inline 기록뿐 아니라 Phase 현재 상태와 하단 누적 Progress도 Phase 3 실행자가 같은 완료 상태로 해석할 수 있어야 하기 때문이다.
|
||
- 어떻게:
|
||
- 대체 RED 완료 상태 검색 — Phase 2 상단의 `P2-R15`까지 완료·Phase 3 진행 가능 exact 상태가 없어 exit 1.
|
||
- 대체 RED 최신 Progress 검색 — 문서 끝이 6차 재검증의 `P2-R15` 수정 필요 상태로 끝나 현재 완료 기록이 없어 exit 1.
|
||
- 코드 회귀 기준은 6차 재검증 직후 독립 확인에서 unit 147 tests, server E2E 36 tests, mock E2E 28 tests, typecheck·lint·dev/prod build 통과와 production mock guard 거부를 확인했다.
|
||
- 변경 범위: 이 review 문서와 `plan-task.md`에 `REV-P2-019`, `P2-R16` 판정·후속 계획만 추가했다. 애플리케이션 코드·test·설정은 변경하지 않았다.
|
||
- 남은 항목: `P2-R16`에서 Phase 2 현재 상태와 최신 Progress를 정정하고 문서·Phase 2 Gate 검증을 누적해야 한다.
|
||
|
||
### P2-R16 수정 검증 — 2026-07-27
|
||
|
||
- 무엇을: `REV-P2-019`를 수정했다. Phase 2 상단 현재 상태를 `P2-R16`까지 완료 및 Phase 3 진행 가능으로 갱신하고, 6차 당시 남은 항목을 보존한 채 최신 수정 검증을 누적했다.
|
||
- 왜: Phase 3 실행자가 완료된 `P2-R15`를 열린 선행 작업으로 오해하지 않고 Phase 2의 실제 완료 상태를 단일하게 판정할 수 있어야 하기 때문이다.
|
||
- 어떻게:
|
||
- 대체 GREEN: Phase 2 완료 상태 exact 검색과 최신 Progress 검색이 각각 1건 이상 일치했다. 6차 당시 `P2-R15` 남은 항목도 이력으로 보존됐다.
|
||
- Full unit/static/build: `npm run test:run` — 34 files / 147 tests 통과. `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0, build는 각각 160 modules 변환.
|
||
- Production boundary: `VITE_API_MODE=mock npm run build:prod` — 기대대로 exit 1. production worker 파일과 mock bootstrap 문자열은 0건.
|
||
- 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, 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에서 보완한다.
|
||
|
||
### P3-T1 Character 계약 확인 및 실행 범위 확정, 2026-07-28
|
||
|
||
- 무엇을: OpenAPI Character 4개 operation과 schema, PRD `CHAR-001~018`,
|
||
`MOCK-001~009`, 외부 의존 `EXT-001`, `EXT-007`, `EXT-011`을 대조해
|
||
`P3-T1` 체크박스 5개를 완료로 표시했다. PRD는 이미 같은 범위를
|
||
반영하고 있어 수정하지 않았다.
|
||
- 왜: `P3-T2`와 `P3-T3`가 계약에 없는 original work lookup, Character 전용
|
||
오류 message key, 검색 결과 active-only 보장을 추정하지 않게 하기 위해서다.
|
||
- 어떻게:
|
||
- JSON parse: `node -e "JSON.parse(require('fs').readFileSync('docs/20260725_AI캐릭터관리자웹/api-contract.openapi.json','utf8'))"`는 출력 없이 exit 0으로 OpenAPI JSON 파싱을 확인했다.
|
||
- Character 계약 확인: focused `node -e`는 exit 0으로 `listAiCharacters`,
|
||
`createAiCharacter`, `getAiCharacter`, `updateAiCharacter`, 목록
|
||
`searchTerm/page/size`, `data.totalCount/content`, item `id`, 상세
|
||
`characterUUID/originalWork`, create/update 성공 `data=null`, multipart
|
||
`image/request`, 공통 오류 `ApiErrorResponse`, original work endpoint 0건을 확인했다.
|
||
- 문서 추적: `rg -n 'CHAR-0(1[2-8]|0[1-9])|EXT-001|EXT-007|EXT-011|originalWork' docs/20260725_AI캐릭터관리자웹/prd.md docs/20260725_AI캐릭터관리자웹/plan-task.md`는 exit 0으로 PRD와 plan의 관련 범위를 확인했다.
|
||
- Diff 검사: `git diff --check -- docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/prd.md`는 출력 없이 exit 0으로 문서 공백 오류 0건을 확인했다.
|
||
- 남은 항목: original work lookup, Character 검색 결과 active-only 보장,
|
||
Character 전용 오류 message key는 backend 제공 대기다. `P3-T1`에서는
|
||
애플리케이션 코드, route, component, test, handler, fixture를 만들지 않았다.
|
||
|
||
### P3-T2 Character 목록·선택·workspace 구현, 2026-07-28
|
||
|
||
- 무엇을: Character 목록·검색·선택과 `/ai-characters/:characterId` 상세
|
||
workspace 복원을 구현하고, `P3-T2` 체크박스 8개를 완료로 표시했다.
|
||
생성·수정·비활성화 form과 mutation은 `P3-T3` 범위로 남겼다.
|
||
- 왜: Phase 3 workspace core가 URL의 `characterId`로 후속 Audio·Series·
|
||
Community·FanTalk 하위 리소스를 격리해야 하기 때문이다.
|
||
- 어떻게:
|
||
- RED: `npm run test:run -- src/features/characters/tests/character-list.test.tsx src/layouts/CharacterWorkspaceLayout.test.tsx`에서 2 files / 7 tests가 모두 실패해 목록·workspace 미구현을 확인했다.
|
||
- GREEN/REFACTOR: 같은 focused 명령은 이후 2 files / 7 tests 통과했고,
|
||
회귀 포함 `npm run test:run -- src/shared/ui/__tests__/search-toolbar.test.tsx src/app/App.test.tsx src/features/characters/tests/character-list.test.tsx src/layouts/CharacterWorkspaceLayout.test.tsx src/shared/mocks/__tests__/auth-handlers.test.ts`는 5 files / 46 tests 통과했다.
|
||
- Type/lint/build: `npm run typecheck`, `npm run lint`, `npm run build`가 모두 exit 0이었다.
|
||
- LSP: `src/app`, `src/features/characters`, `src/layouts`, `src/shared/ui`,
|
||
`src/shared/mocks` 진단은 오류 0건이었다.
|
||
- 수동 QA(mock): `npm run dev:mock`의 `http://127.0.0.1:8889`에서 Playwright로 로그인 → 목록 루나/테오 표시 → settled 후 Character GET 0건 → 검색 `search=루나` URL 반영 → 루나 선택 `/ai-characters/101` → `character-uuid-101`·원작 `달빛 상담소` 표시 → `/ai-characters/202` deep link → read-only 배너 표시와 `저장`/`비활성화` 버튼 0건을 확인했다. 브라우저 콘솔 error/warning은 0건이었다.
|
||
- 남은 항목: Character 생성·수정·soft delete, original work lookup, active-only
|
||
server 보장, Character 전용 오류 message key는 `P3-T3` 또는 backend 제공 후속
|
||
범위다.
|
||
|
||
### P3-T3 Character 생성·수정·soft delete 부분 구현, 2026-07-28
|
||
|
||
- 무엇을: Character create/update/deactivate API serializer, 생성·수정 route와 form,
|
||
soft delete dialog, image crop 적용, mock store mutation을 구현하고 검증된
|
||
`P3-T3` 체크박스 중 계약 추정 없이 검증된 항목만 완료로 표시했다.
|
||
- 왜: OpenAPI가 제공한 multipart create/update와 `data=null` 성공 계약을
|
||
UI와 mock server가 같은 request/response shape로 사용하게 하기 위해서다.
|
||
- 어떻게:
|
||
- RED/GREEN: create/update/deactivate serializer, 생성 form 필수값·중복 제출·
|
||
crop 적용, 수정 form region read-only·dirty leave guard·교체 이미지 crop,
|
||
상세 soft delete dialog, mock create/update/deactivate store 갱신 test를
|
||
실패 확인 후 최소 구현으로 통과시켰다. 추가로 crop 취소 시 replacement
|
||
image 미전송과 기존 image 삭제 UI 부재, `characterUUID` 읽기 전용 표시와
|
||
`externalCharacterId` 미노출 test를 RED/GREEN으로 확인했다. optional scalar와
|
||
repeated fields는 create 배열, update 배열 `null`, nullable scalar 직렬화
|
||
test를 추가했고, `npm run typecheck` RED에서 update 반복 필드 `null` 타입
|
||
미허용 오류 8건을 확인한 뒤 create/update 요청 타입을 계약에 맞게 분리했다.
|
||
- Focused 검증: `npm run test:run -- src/features/characters/tests/character-api.test.ts src/features/characters/tests/CharacterCreatePage.test.tsx src/features/characters/tests/CharacterEditPage.test.tsx src/features/characters/tests/CharacterDetailPage.test.tsx src/features/characters/tests/character-mutation-reload.test.tsx src/shared/mocks/__tests__/character-handlers.test.ts src/shared/mocks/__tests__/auth-handlers.test.ts src/app/App.test.tsx`는 8 files / 57 tests 통과했다.
|
||
- 추가 재조회·성공 알림 검증: `src/features/characters/tests/character-mutation-reload.test.tsx`로 create 성공 후 목록 route 재진입·목록 재조회와 성공 알림, update 성공 후 상세 route 재진입·상세 재조회와 성공 알림, soft delete 성공 후 목록 route 이동·목록 재조회와 성공 알림을 3 tests 통과로 확인했다. Character 화면은 현재 React Query cache를 사용하지 않아 cache invalidation 대신 `navigateTo()` 기반 route 재진입으로 직접 요청을 다시 발생시키는 정책으로 정정했다.
|
||
- Type/lint/build: `npm run typecheck`, `npm run lint`, `npm run build`,
|
||
`git diff --check`가 모두 exit 0이었다.
|
||
- 전체 회귀 검증: `npm run test:run`은 42 files / 175 tests 통과했다. 병렬
|
||
재실행 중 `CharacterWorkspaceLayout.test.tsx`의 `character-uuid-101` 중복 표시
|
||
assertion이 먼저 실패했으나, 헤더와 프로필 양쪽 표시가 의도된 상태임을 확인하고
|
||
`getAllByText(...).toHaveLength(2)`로 좁힌 뒤
|
||
`npm run test:run -- src/layouts/CharacterWorkspaceLayout.test.tsx` 1 file / 5 tests와
|
||
전체 42 files / 175 tests를 통과시켰다.
|
||
- LSP: `CharacterCreatePage.test.tsx`와 `CharacterEditPage.test.tsx`는 진단
|
||
0건이었다. 일부 TSX 파일은 LSP fresh diagnostics가 timeout 됐고,
|
||
Markdown은 LSP server 미설정으로 진단 불가했다. `npm run typecheck`로 타입
|
||
오류 0건을 확인했다.
|
||
- 수동 QA(mock): `npm run dev:mock`의 `http://127.0.0.1:8889`에서
|
||
Playwright로 로그인 → `/ai-characters/new` 생성 form 진입 → PNG 업로드 후
|
||
`이미지 crop` dialog와 `예상 결과 800 × 800px` 확인 → `적용` → `노아`
|
||
생성 → 목록 총 3개와 `노아 선택` 확인 → `/ai-characters/1000` 상세 진입 →
|
||
수정 화면에서 `지역` input disabled와 `KR` 확인 → 상세 복귀 →
|
||
`노아 비활성화 확인` dialog의 복원 미지원·hard delete 미지원 문구 확인 →
|
||
`비활성화 확인` 후 `/ai-characters` 목록 복귀와 총 2개, `노아` 제외를
|
||
확인했다. 브라우저 console은 기능 error/warning 0건이었고, favicon 404만
|
||
별도 관찰됐다. 추가로 `/ai-characters/101`에서 `캐릭터 UUID`와
|
||
`character-uuid-101` 표시, `character-101` 미노출, 브라우저 console
|
||
warning/error 0건을 확인했다.
|
||
- 추가 수동 QA(mock): 같은 mock 서버에서 Playwright로 create 성공 후
|
||
`/ai-characters`와 `AI 캐릭터를 생성했습니다.` status, update 성공 후
|
||
`/ai-characters/101`과 `AI 캐릭터를 저장했습니다.` status, soft delete 성공 후
|
||
`/ai-characters`와 `AI 캐릭터를 비활성화했습니다.` status를 확인했다. 생성 form의
|
||
`name`, `systemPrompt`, `description`에는 `maxlength`가 없고, 반복 필드 입력 UI는
|
||
아직 노출되지 않음을 확인했다. OQ-009 권고안은 backend 호환 전까지 client 상한을
|
||
추가하지 않고, backend가 길이·개수 계약을 제공하면 `name`, `systemPrompt`,
|
||
`description`의 `maxlength`와 tags·hobbies·values·goals·relationships·
|
||
personalities·backgrounds·memories의 최대 개수를 PRD·OpenAPI·schema·경계 test에
|
||
같은 변경으로 반영하는 것이다.
|
||
- Visual QA: `p3-t3-success-notification.png` 캡처를 기준으로 독립 Oracle 2건이
|
||
모두 PASS했다. Pass A는 `SuccessNotification`이 `DESIGN.md`와 CSS token 기반의
|
||
실제 DOM 구현이며 create/update/deactivate 경로가 모두 success state를 전달한다고
|
||
판정했다. Pass B는 `AI 캐릭터를 비활성화했습니다.` 알림의 CJK 줄바꿈·클리핑·대비·
|
||
시각 계층 문제가 없다고 판정했다.
|
||
- 남은 항목: original work combobox는 계약 미제공으로 아직 완료 표시하지 않았다.
|
||
|
||
### P3-T4 Character 반응형·접근성 검증, 2026-07-28
|
||
|
||
- 무엇을: Character workspace의 desktop/tablet 관리 action, mobile 조회 전용 capability, mobile Card 접근 이름, keyboard-only 흐름, 320/768/1280px·200% zoom·axe 기준을 검증하고 `P3-T4` 체크박스 5개를 완료로 표시했다.
|
||
- 왜: Phase 3 활성 범위가 viewport별 route/action policy와 접근성 기준을 충족하는지 `P3-GATE` 전에 독립적으로 고정하기 위해서다.
|
||
- 어떻게:
|
||
- RED: 초기 focused E2E `tests/e2e/character-workspace.spec.ts`는 10 tests를 수집했고, 의도한 반응형·접근성 공백으로 4 passed / 6 failed를 기록했다.
|
||
- GREEN/product: 이후 focused E2E는 `10 passed`에 도달했고, typecheck·lint·design-system focused test도 통과했다.
|
||
- Review gap: Oracle이 create/edit direct route와 dirty-leave dialog의 axe·zoom coverage 누락을 발견했고, test-only patch로 해당 coverage를 확장했다.
|
||
- Final fix: 320px mobile direct create/edit guidance route에서도 계속 진행 전에 200% zoom, horizontal overflow 없음, axe critical·serious 0건을 확인하게 했다.
|
||
- Final verification: `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts --project=chromium` -> exit 0, `11 passed (31.3s)`.
|
||
- Static verification: `npm run typecheck` -> exit 0. `npm run lint` -> exit 0.
|
||
- LSP diagnostics: `tests/e2e/character-workspace.spec.ts` fresh diagnostics는 timeout됐고, TypeScript 정확성은 `npm run typecheck`로 보완했다.
|
||
- Oracle re-check: 현재 source inspection 기준 prior blocker는 `SPEC_PASS` 판정이었다.
|
||
- 남은 항목: original work combobox는 계약 미제공으로 계속 제외한다. `P3-GATE`는 아직 남아 있다.
|
||
|
||
### P3-GATE Character 통합 검증, 2026-07-28
|
||
|
||
- 무엇을: Phase 3 Character 활성 범위의 unit/component, mock E2E, server-mode
|
||
allowlist E2E, typecheck, lint, production build를 실행하고 `P3-GATE`를 통과로
|
||
판정했다.
|
||
- 왜: mock UI journey와 실제 server mode 경계 검증을 분리해 기록하고,
|
||
`P3-T1`~`P3-T4`가 후속 Phase의 workspace 기반으로 안전한지 확인하기 위해서다.
|
||
- 어떻게:
|
||
- 원인 조사: `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts`는 최초
|
||
44 tests 중 3 failures를 재현했다. 실패 원인은 앱 버그가 아니라 E2E 범위
|
||
오류였다. mobile 프로젝트는 PRD 정책상 Character mutation action을 숨기는데
|
||
keyboard-only edit flow가 `수정` 버튼을 기다렸고, WebKit/Safari는 기본 설정상
|
||
`Tab`이 link를 일관되게 focus하지 않는다. Apple Safari keyboard shortcut 문서와
|
||
Playwright WebKit 이슈 조사도 같은 결론이었다.
|
||
- E2E 정정: `tests/e2e/character-workspace.spec.ts`의 keyboard-only edit/dirty-leave
|
||
flow는 desktop Chromium 전용 capability로 명시하고, mobile/WebKit project는
|
||
platform policy 사유로 skip하게 했다. mobile 조회·검색·상세, direct create/edit
|
||
desktop 안내, Card accessible name, 320/768/1280px·200% zoom·axe 검증은 같은
|
||
spec의 별도 tests로 유지된다.
|
||
- Gate 명령 정정: `npm run e2e -- tests/e2e/character-workspace.spec.ts`는
|
||
`playwright.config.ts` server mode `testMatch`와 교집합이 없어 `No tests found`가
|
||
나는 잘못된 focused 명령이었다. P2-R7/P2-R13의 mode allowlist 계약에 맞춰
|
||
`P3-GATE` server mode 명령을 bare `npm run e2e`로 정정했다.
|
||
- Unit/component: `npm run test:run -- src/features/characters src/layouts/CharacterWorkspaceLayout.test.tsx` — 7 files / 24 tests 통과.
|
||
- Mock E2E: `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts` — 44 tests 중 41 passed, 3 skipped, exit 0. skipped 3건은 mobile/WebKit keyboard edit flow platform policy 사유다.
|
||
- Server E2E: `npm run e2e` — server allowlist 4 specs / 36 tests 통과. Character server integration은 original work lookup, active-only 보장, Character 전용 오류 key가 backend 계약 대기라 mock UI 완료와 별도 상태로 유지한다.
|
||
- Static/build: `npm run typecheck`, `npm run lint`, `npm run build`가 모두 exit 0이었다.
|
||
- LSP/LOC: `tests/e2e/character-workspace.spec.ts` fresh LSP diagnostics는 timeout됐고, Markdown은 LSP server 미설정이다. TypeScript 정확성은 `npm run typecheck`로 보완했다. 변경 후 `tests/e2e/character-workspace.spec.ts`는 순수 LOC 185줄이다.
|
||
- 남은 항목: original work combobox, Character active-only server 보장,
|
||
Character 전용 오류 message key는 backend 계약 제공 대기다. Phase 3의 mock UI
|
||
journey와 server mode 경계 검증은 완료됐으며, 실제 Character server integration
|
||
완료로 과대표현하지 않는다.
|
||
|
||
### P4-T1 Audio 계약 확인 및 실행 범위 확정, 2026-07-28
|
||
|
||
- 무엇을: OpenAPI Audio 5개 operation과 `AudioContent*`, `AudioContentTheme`, 공통 오류 schema를 PRD `AUDIO-001~033`, `FILE-001~002`, `FILE-006~009`, `FILE-012~013`, `MOCK-001~009`, PRD `9`의 Audio 반응형 범위와 대조해 `P4-T1` 체크박스 7개를 완료로 표시했다. PRD와 OpenAPI는 이미 같은 범위를 반영하고 있어 수정하지 않았다.
|
||
- 왜: `P4-T2`와 `P4-T3`가 status query/status field, `seriesIds`, 수정 content file 교체, Audio 전용 오류 key, price 상한, active-only 보장, backend container/codec 검증을 추정하지 않게 하기 위해서다.
|
||
- 어떻게:
|
||
- JSON parse: `node -e "JSON.parse(require('fs').readFileSync('docs/20260725_AI캐릭터관리자웹/api-contract.openapi.json','utf8'))"`는 출력 없이 exit 0으로 OpenAPI JSON 파싱을 확인했다.
|
||
- Audio 계약 확인: focused `node` 검사는 exit 0으로 `GET /api/v2/admin/ai-characters/audio-content-themes`, `GET/POST /api/v2/admin/ai-characters/{characterId}/audio-contents`, `GET/PUT /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}`와 `listAiCharacterAudioContentThemes`, `listAiCharacterAudioContents`, `createAiCharacterAudioContent`, `getAiCharacterAudioContent`, `updateAiCharacterAudioContent` operation을 확인했다. theme는 query/body 없이 `data[]`의 `id`, `theme`, `image`를 사용한다.
|
||
- Contract fixture: 공통 오류는 `ApiErrorResponse`의 `success=false`, `message`, `data=null`, `errorProperty`와 status 400/401/403/404/405/406/415/500만 사용한다. Audio 전용 message key, backend container/codec 오류 계약, price 최대값, active-only 보장은 미제공이므로 fixture로 추정하지 않는다.
|
||
- 요청·응답 고정: 목록은 `search_word`, `page`, `size`와 `data.totalCount`, `data.items[]`를 사용한다. 상세는 필수 `timezone=Asia/Seoul` query를 보낸다. 생성 multipart는 필수 `contentFile`, `coverImage`, `request`와 성공 `data.contentId`를 사용한다. 수정 multipart는 필수 `request`와 optional `coverImage`만 있고 성공은 `data=null`이다.
|
||
- 제외 고정: 목록 query와 목록·상세 schema에 `status`가 없고 생성·수정 request에 `seriesIds`가 없으므로 client status filter, status enum, series 연결 UI, 수정 content file 교체를 만들지 않는다. price는 PRD의 0 이상 정수 “캔”만 유지하고 상한 test는 계약 제공 후 추가한다.
|
||
- 상태·component map: 목록 Page는 `characterId`, `search_word`, `page`, `size` route/query와 loading·empty·error·retry를 가진다. 상세 Page는 `characterId`, `contentId`, `timezone`과 단일 재생·media error 안내를 가진다. form/upload Page는 desktop/tablet에서만 create/update/deactivate/upload action을 제공하고 mobile은 PRD `9`에 따라 조회·재생만 제공한다. Feature component는 Audio releaseDate/theme/file/price 규칙을 소유하고, file input/crop/player/page state는 Phase 1 shared contract를 사용한다.
|
||
- Mock scenario: deterministic seed는 목록 2건 이상, 상세 1건 이상, theme 2건 이상을 둔다. browser handler는 제공 계약 범위의 theme list, list/search/page, detail with timezone, create multipart 성공 `contentId`, update/soft delete 성공 `data=null`, upload progress/cancel/retry happy path와 공통 오류만 제공한다. Audio 전용 오류 key, active-only server 결과, backend codec 판정, price 상한은 mock에서도 추정하지 않는다.
|
||
- 문서 추적: `rg -n 'AUDIO-0(0[1-9]|[12][0-9]|3[0-3])|FILE-00[126789]|FILE-01[23]|MOCK-00[1-9]|오디오 목록·상세·재생|오디오 생성·수정·비활성화·업로드|EXT-00[79]|EXT-010|EXT-011' docs/20260725_AI캐릭터관리자웹/prd.md docs/20260725_AI캐릭터관리자웹/plan-task.md`는 exit 0으로 PRD와 plan의 Audio, File, Mock, 외부 의존 범위를 확인했다.
|
||
- Diff 검사: `git diff --check -- docs/20260725_AI캐릭터관리자웹/plan-task.md`는 출력 없이 exit 0으로 문서 공백 오류 0건을 확인했다.
|
||
- 남은 항목: Audio 전용 오류 message key, backend container/codec 검증 계약, price 최대값, active-only 반환 보장은 backend 제공 대기다. `P4-T1`에서는 애플리케이션 코드, route, component, test, handler, fixture를 만들지 않았다.
|
||
|
||
### P4-T2 Audio 목록·상세·player 구현, 2026-07-28
|
||
|
||
- 무엇을: 선택 Character context의 Audio 목록·상세·player vertical slice를 구현하고 `P4-T2` 체크박스 9개를 완료로 표시했다.
|
||
- 왜: `AUDIO-021`, `AUDIO-026`, `AUDIO-029`, `AUDIO-030`의 검색·상세 timezone·재생·media error 안전 동작을 P4-T3 form/upload 전에 고정하기 위해서다.
|
||
- 어떻게:
|
||
- RED: `npm run test:run -- src/features/audio-contents/tests/audio-list.test.tsx src/features/audio-contents/tests/audio-player.test.tsx`는 최초 실행에서 `AudioContentDetailPage` import 부재와 nested audio route 미지원으로 실패했다. mock handler focused RED도 audio endpoint unhandled request로 실패했다.
|
||
- GREEN: 같은 focused command는 2 files / 2 tests 통과했고, `src/shared/mocks/__tests__/character-handlers.test.ts`는 2 tests 통과했다.
|
||
- Route/API: `/ai-characters/:characterId/audio-contents`와 `/ai-characters/:characterId/audio-contents/:contentId`를 `ProtectedAdminShell`에 연결했다. 목록 API는 `characterId`, `page`, `size`, 2자 이상 `search_word`만 보내며 `status`, `isActive`, `active` query를 보내지 않는다. 상세 API는 `timezone=Asia/Seoul`을 필수 query로 보낸다.
|
||
- Boundary parse: 목록은 OpenAPI `data.totalCount/items`, 상세는 `AudioContentDetailResponse` shape를 Zod schema로 파싱한다.
|
||
- Player safety: 목록과 상세는 Phase 1 `AdminAudioPlayer`와 `AudioPlaybackProvider`만 조합한다. 명시적 download button을 추가하지 않았고 media error는 일반 오류·수동 재시도·페이지 새로고침 안내만 표시한다. focused test는 media error 후 detail/list GET 재호출과 play 자동 재호출이 없음을 확인했다.
|
||
- Signed URL: `AdminAudioPlayer` 기존 seam의 console/log/storage test와 P4-T2 detail integration test로 signed URL log·storage 호출이 없음을 확인했다. 분석 event seam은 현재 코드base에 없어서 코드 검사로 확인했다.
|
||
- Mock mode: mock handler는 실제 endpoint shape로 list/detail을 제공하고 `data:audio/wav` preview asset을 사용한다. 브라우저에서 mock 로그인 후 `/ai-characters/101/audio-contents?search=달빛&page=0&size=20` 목록, 상세 이동, `timezone=Asia%2FSeoul`, player 재생 버튼, keyboard Space, volume, speed control을 확인했다.
|
||
- Verification: 관련 테스트 `npm run test:run -- src/app/App.test.tsx src/layouts/CharacterWorkspaceLayout.test.tsx src/shared/ui/__tests__/admin-audio-player.test.tsx src/shared/mocks/__tests__/character-handlers.test.ts src/features/audio-contents/tests/audio-list.test.tsx src/features/audio-contents/tests/audio-player.test.tsx`는 6 files / 43 tests 통과했다. `npm run typecheck`와 `npm run lint`는 출력 없이 exit 0이었다.
|
||
- LSP: 변경 TS/TSX 파일 diagnostics는 모두 clean이었다. `src/shared/mocks/audio-content-fixtures.ts`는 수정 직후 fresh diagnostics가 1회 timeout됐고, 이후 typecheck로 보완했다.
|
||
- Visual QA: Playwright mock browser에서 1280px와 375px 상세 screenshots `p4-t2-audio-detail-1280.png`, `p4-t2-audio-detail-375.png`를 캡처했다. 이 환경의 subagent tool은 `oracle`을 지원하지 않아 `/visual-qa` dual-oracle pass는 실행하지 못했다.
|
||
- 남은 항목: Audio 전용 오류 message key, backend container/codec 검증 계약, price 최대값, active-only 반환 보장은 P4-T1과 동일하게 backend 제공 대기다. P4-T3 form/upload와 P4-T4 E2E는 구현하지 않았다.
|
||
|
||
### P4-T3 Audio 발행 form·upload 구현, 2026-07-28
|
||
|
||
- 무엇을: Audio theme 선택, 즉시/예약 발행, create/update/deactivate multipart API, cover/audio validation, upload progress/cancel/retry adapter, form UI, route, mock store/handler를 구현하고 `P4-T3` 체크박스 18개를 완료로 표시했다.
|
||
- 왜: `AUDIO-009`, `AUDIO-011`, `AUDIO-014`, `AUDIO-023~024`, `AUDIO-027`, `AUDIO-033`과 OpenAPI `AudioContentCreateMultipart`, `AudioContentUpdateMultipart`, `AudioContentTheme` 계약을 client form과 mock mode에서 고정하기 위해서다.
|
||
- 어떻게:
|
||
- RED: `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`는 최초 실행에서 `audio-cover-policy`, `AudioContentFormPage`, `upload-audio-content` import 부재로 3 files failed / 0 tests를 확인했다.
|
||
- GREEN: 같은 focused command는 3 files / 11 tests 통과했다. 생성 multipart는 `contentFile`, `coverImage`, `request` 필수와 성공 `data.contentId`, 수정은 `request`와 optional `coverImage`, deactivate는 `{isActive:false}`만 확인했다.
|
||
- Form/validation: 생성 form은 theme 미선택 submit을 차단하고 price를 “캔” 형식으로 표시하며 즉시 공개 기본값은 `releaseDate=null`, `timezone="Asia/Seoul"`을 보낸다. 예약 공개는 미래 local datetime만 허용하고 `yyyy-MM-dd HH:mm`로 보낸다. 수정 form은 theme/release/series/content file 변경 control을 만들지 않고 읽기 전용 안내만 표시한다.
|
||
- File/upload: cover는 Phase 1 `FileField`·`ImageCropDialog`와 `AUDIO_COVER_POLICY` `{aspect:1,maxWidth:800,noUpscale:true,maxBytes:10485760}`을 조합한다. audio는 공통 policy로 MP3/AAC/M4A, `.m4a + audio/x-m4a`, `1,024,000,000 bytes` 허용과 WAV·`1,024,000,001 bytes` 거부를 확인하며 codec sniffing과 resumable upload는 만들지 않았다. 415는 `ApiError` message/errorProperty를 form 안내로 보존한다.
|
||
- Mock mode: `GET /api/v2/admin/ai-characters/audio-content-themes`, `POST /audio-contents`, `PUT /audio-contents/:contentId` handler와 같은 mock store의 create/update/deactivate list/detail 갱신을 추가했다. Audio 전용 오류 key, price 상한, active-only server 보장, backend container/codec 판정은 추정하지 않았다.
|
||
- OQ-009: 실제 Audio 생성/수정 form에서 `title`, `detail`, `tags` 입력 공간과 반복 항목을 확인했다. 권고안은 `title` 80자, `detail` 2,000자, `tags` comma-separated 20개/각 30자이나 backend 호환 전까지 client 상한은 구현하지 않았다.
|
||
- Verification: focused command는 3 files / 11 tests 통과했고, 관련 회귀 `npm run test:run -- src/app/App.test.tsx src/layouts/CharacterWorkspaceLayout.test.tsx src/shared/mocks/__tests__/character-handlers.test.ts src/shared/validation/file-media-policy.test.ts src/shared/ui/__tests__/file-field.test.tsx src/shared/ui/__tests__/image-crop-dialog.test.tsx src/shared/ui/__tests__/upload-progress.test.tsx src/shared/ui/__tests__/confirm-deactivate-dialog.test.tsx src/shared/ui/__tests__/unsaved-changes-guard.test.tsx src/features/audio-contents/tests/audio-list.test.tsx src/features/audio-contents/tests/audio-player.test.tsx`는 11 files / 56 tests 통과했다. `npm run typecheck`와 `npm run lint`는 출력 없이 exit 0이었다.
|
||
- Manual QA: Playwright mock browser에서 login → `/ai-characters/101/audio-contents/new` → theme 선택 → audio/cover upload → crop 적용 → create → detail → edit → update → deactivate → list toast를 실제 조작했다. artifacts는 `.playwright-mcp/p4-t3-audio-created-detail.png`, `.playwright-mcp/p4-t3-audio-updated-detail.png`, `.playwright-mcp/p4-t3-audio-deactivated-list.png`, `.playwright-mcp/p4-t3-console.txt`, `.playwright-mcp/p4-t3-network.txt`이다. console warning/error 0건, Audio 관련 network 15건은 모두 200이었다.
|
||
- LSP/Diff: 변경 TS/TSX 파일 diagnostics는 clean이었다. `src/shared/mocks/character-mock-store.ts` diagnostics는 fresh 응답이 1회 timeout됐고 이후 typecheck로 보완했다. `git diff --check -- src/app/browser-location.ts src/app/protected-admin-shell.tsx src/app/route-paths.ts src/features/audio-contents/api/audio-content-api.ts src/features/audio-contents/pages/AudioContentDetailPage.tsx src/features/audio-contents/pages/AudioContentListPage.tsx src/shared/mocks/audio-content-fixtures.ts src/shared/mocks/character-mock-store.ts src/shared/mocks/handlers.ts docs/20260725_AI캐릭터관리자웹/plan-task.md`는 출력 없이 exit 0이었다.
|
||
- 남은 항목: Audio 전용 backend 오류 message key, 실제 container/codec 검증, price 상한, active-only server 결과는 기존 P4-T1/P4-T2와 동일하게 backend 계약 제공 대기다. P4-T4 반응형·접근성 E2E와 Gate는 구현하거나 완료 표시하지 않았다.
|
||
|
||
### P4-T3 blocker 수정 및 fresh QA 재검증, 2026-07-28
|
||
|
||
- 무엇을: Audio 생성 기본 경로가 XHR upload adapter를 우회하지 않도록 고정하고, upload cancel/error/retry, malformed success JSON, `.m4a` MIME 경계, crop 취소, 415 field mapping, 비활성화 destructive 스타일, mock request logging을 재검증했다.
|
||
- 왜: P4-T3 완료 기록 이후 발견된 blocker가 create/upload 실제 경로와 민감정보 비기록 제약을 깨뜨릴 수 있어, P4-T4/GATE로 확장하지 않고 P4-T3 범위 안에서만 닫기 위해서다.
|
||
- 어떻게:
|
||
- RED/GREEN: `npm run test:run -- src/shared/mocks/browser.test.ts`는 `quiet: true` 누락으로 1 failed를 확인했고, `startMockWorker`에 `quiet: true`를 추가한 뒤 `npm run test:run -- src/shared/mocks/browser.test.ts src/shared/mocks/__tests__/auth-handlers.test.ts src/shared/mocks/browser.test.ts`는 2 files / 11 tests 통과했다.
|
||
- Focused P4-T3: `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 src/features/audio-contents/tests/audio-form-upload.test.tsx`는 4 files / 16 tests 통과했다. 확장 focused `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 src/features/audio-contents/tests/audio-form-upload.test.tsx src/shared/validation/file-media-policy.test.ts src/shared/ui/__tests__/confirm-deactivate-dialog.test.tsx src/shared/mocks/browser.test.ts src/shared/mocks/__tests__/auth-handlers.test.ts`는 8 files / 36 tests 통과했다.
|
||
- Static/build: `npm run typecheck`, `npm run lint`, `npm run build`는 모두 exit 0이었다.
|
||
- LSP: `src/shared/mocks/browser.ts`, `src/shared/mocks/browser.test.ts` fresh diagnostics는 clean이었다. 이전 변경 파일 중 일부 LSP 호출은 timeout됐지만 `typecheck`와 `lint`로 보완했다.
|
||
- Manual QA: `npm run dev:mock` 서버에서 Playwright Chromium으로 login → `/ai-characters/101/audio-contents/new` → 예약 공개 선택 → audio/cover 선택 → crop 적용 → create → detail → edit → update → deactivate confirm → list 이동 → 375px mobile list를 실제 조작했다. fresh artifacts는 `.playwright-mcp/p4-t3-final-quiet/01-create-form.png`부터 `08-mobile-list.png`, `console.txt`, `network.txt`, `summary.json`이다.
|
||
- Visual evidence: PNG signature 8건을 확인했고 `look_at`으로 create 초기 form, 예약/파일 선택 form, destructive deactivate dialog, mobile list/player 상태를 확인했다. `.playwright-mcp/p4-t3-final-quiet/summary.json`은 `screenshotCount=8`, `containsPassword=false`, `containsMultipartBoundary=false`, `containsUploadFileName=false`였다. `console.txt`는 Vite/React dev 안내 3건뿐이고 password·multipart body·upload filename은 없었다. Audio/Character API request 48건은 모두 200이었다.
|
||
- Diff 검사: `git diff --check -- src/shared/mocks/browser.ts src/shared/mocks/browser.test.ts docs/20260725_AI캐릭터관리자웹/plan-task.md`는 출력 없이 exit 0이었다.
|
||
- 남은 항목: Audio 전용 backend 오류 message key, 실제 container/codec 검증, price 상한, active-only server 결과는 기존 P4-T1/P4-T2와 동일하게 backend 계약 제공 대기다. P4-T4 반응형·접근성 E2E와 Phase 4 Gate는 구현하거나 완료 표시하지 않았다.
|
||
|
||
### P4-T3 review gap closure, 2026-07-28
|
||
|
||
- 무엇을: `src/app/App.test.tsx`를 protected shell/error/logout 보존 테스트로 분리해 모든 변경 test file을 250 pure LOC 이하로 낮추고, Audio/Character 비활성화 dialog와 Audio form의 사용자 표시 copy에서 내부 ID, 영문 개발 용어, `hard delete`, native file input 문구 노출을 제거했다.
|
||
- 왜: P4-T3 review에서 oversized test, 비활성화 제목 줄바꿈, 안정 artifact 명명, upload progress 증거, Mock Preview overlap, 내부/영문 operator copy가 남아 있어 P4-T4/Gate로 확장하지 않고 P4-T3 안에서 닫기 위해서다.
|
||
- 어떻게:
|
||
- RED/GREEN: copy 회귀 테스트는 `OQ-009`, `Audio 수정 계약`, `hard delete`, native file chooser 문구가 남아 실패했고, Korean admin copy와 `파일 선택` button wrapper로 수정한 뒤 `npm run test:run -- src/features/audio-contents/tests/audio-form.test.tsx src/features/audio-contents/tests/audio-form-upload.test.tsx src/shared/ui/__tests__/confirm-deactivate-dialog.test.tsx src/features/characters/tests/CharacterDetailPage.test.tsx`가 4 files / 13 tests 통과했다. App split 보존은 `npm run test:run -- src/app/App.test.tsx src/app/App.protected-shell.test.tsx src/app/App.protected-errors.test.tsx src/app/App.logout.test.tsx`가 4 files / 29 tests 통과했다.
|
||
- Focused/static: `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 src/features/audio-contents/tests/audio-form-upload.test.tsx src/app/App.test.tsx src/app/App.protected-shell.test.tsx src/app/App.protected-errors.test.tsx src/app/App.logout.test.tsx src/shared/ui/__tests__/confirm-deactivate-dialog.test.tsx src/features/characters/tests/CharacterDetailPage.test.tsx src/shared/mocks/browser.test.ts src/shared/validation/file-media-policy.test.ts`는 12 files / 58 tests 통과했다. `npm run typecheck`, `npm run lint`, `npm run build`는 모두 exit 0이었다.
|
||
- Size/LSP/diff: 변경 source/test 12개는 모두 250 pure LOC 이하였고, 변경 TS/TSX 파일 LSP diagnostics는 clean이었다. `git diff --check`는 P4-T3 변경 파일과 이 문서에 대해 출력 없이 통과했다.
|
||
- Manual QA: `npm run dev:mock` 서버에서 Playwright Chromium으로 login → Audio create form → 예약 공개 → audio/cover 선택 → crop 적용 → upload progress in-flight 상태 → create detail → edit/save → deactivate confirm → list 이동 → 375px mobile list를 실제 조작했다. Fresh artifacts는 `.playwright-mcp/p4-t3-final-quiet/01-create-form.png`부터 `10-mobile-list.png`, `console.txt`, `network.txt`, `summary.json`이다.
|
||
- Visual evidence: PNG signature 10건은 모두 `89504e470d0a1a0a`였고, `04-upload-progress.png`는 `업로드 중`, `업로드 취소`, progressbar를 표시한다. `08-deactivate-confirm.png`는 `오디오 콘텐츠 비활성화 확인`과 `완전 삭제는 아니며 목록에서 제외됩니다.`를 표시하고 내부/영문 용어가 없다. `10-mobile-list.png`는 overlap 없이 표시됐다. `summary.json`은 `screenshotCount=10`, `containsPassword=false`, `containsMultipartBoundary=false`, `containsUploadFileName=false`, `visibleInternalTerms=false`였다.
|
||
- 정정: 최종 gap closure 후 `08-deactivate-confirm.png`의 비활성화 문구는 `목록 노출만 중지하며 콘텐츠는 보관됩니다.`로 갱신됐고, `02-cover-crop-dialog.png`는 640×480 PNG fixture와 `예상 결과 480 × 480px` crop preview를 표시하도록 재캡처했다. `summary.json`은 `screenshotCount=10`, `extraPngs=[]`, `pngSignaturesOk=true`, `containsPasswordText=false`, `containsMultipartBoundary=false`, `containsUploadFileName=false`, `containsInternalVisibleTerms=false`로 갱신됐다.
|
||
- 남은 항목: Audio 전용 backend 오류 message key, 실제 container/codec 검증, price 상한, active-only server 결과는 기존 P4-T1/P4-T2와 동일하게 backend 계약 제공 대기다. P4-T4 반응형·접근성 E2E와 Phase 4 Gate는 구현하거나 완료 표시하지 않았다.
|
||
|
||
### P4-T4 Audio 반응형·접근성 구현, 2026-07-28
|
||
|
||
- 무엇을: Audio mock E2E에 mobile route capability, desktop/tablet mutation action, 320px overflow, keyboard-only player/form, 200% zoom, axe critical·serious 0건 검증을 추가하고 `P4-T4` 체크박스 4개를 완료로 표시했다. `P4-GATE`는 완료 표시하지 않았다.
|
||
- 왜: PRD `9`의 mobile 조회·재생 전용 범위와 desktop/tablet 운영 범위를 Audio route에서 분리하고, player/form 접근성 회귀를 Phase 4 Gate 전에 고정하기 위해서다.
|
||
- 어떻게:
|
||
- RED: `npm run e2e:mock -- tests/e2e/audio-content.spec.ts`는 최초 실행에서 20 tests 중 4 failed / 13 passed / 3 skipped였고, 320px mobile list의 `오디오 생성` button이 visible이라 route capability 기대를 만족하지 못했다.
|
||
- GREEN: `AudioContentFormPage`에서 `(min-width: 768px)` route capability를 적용해 mobile 직접 `/new`·`/edit` route는 생성/수정/업로드/비활성화 form을 mount하지 않고 안내만 표시한다. 목록 `오디오 생성`과 상세 `수정` entrypoint는 `md` 이상에서만 노출한다. `AudioContentSummary`, mobile list item, `AdminAudioPlayer` error 영역은 긴 텍스트 wrapping과 `min-w-0`을 보강했다.
|
||
- Focused unit/component: `npm run test:run -- src/features/audio-contents/tests/audio-list.test.tsx src/features/audio-contents/tests/audio-player.test.tsx src/features/audio-contents/tests/audio-form.test.tsx src/features/audio-contents/tests/audio-form-upload.test.tsx`는 4 files / 11 tests 통과했다.
|
||
- Mock E2E: `npm run e2e:mock -- tests/e2e/audio-content.spec.ts`는 최종 실행에서 20 tests 중 17 passed / 3 skipped였다. Skipped 3건은 기존 browser 조건에 맞춘 keyboard path skip으로 mobile mutation 차단 및 WebKit tab focus 차이를 명시했다.
|
||
- Static/build: `npm run typecheck`, `npm run lint`, `npm run build`는 모두 exit 0이었다. `npm run build`는 Vite production build에서 204 modules transformed, `dist/assets/index-B5HKASem.js` 381.66 kB(gzip 106.65 kB)를 생성했다.
|
||
- LSP/size/diff: 변경 TS/TSX와 config 파일의 개별 LSP diagnostics는 clean이었고 `AudioContentFormPage.tsx` 개별 fresh diagnostics는 2회 timeout됐으나, `src/features/audio-contents` directory diagnostics는 14 TSX files scanned / errors 0 / diagnostics 0이었다. 변경 source/test/config 파일 pure LOC는 `playwright.config.ts 29`, `tests/e2e/audio-content.spec.ts 137`, `AudioContentFormPage.tsx 104`, `AudioContentListPage.tsx 98`, `AudioContentDetailPage.tsx 63`, `AudioContentSummary.tsx 22`, `AudioContentListItem.tsx 32`, `admin-audio-player.tsx 125`로 모두 250 이하이다. `git diff --check`와 untracked E2E spec 대상 `git diff --check --no-index /dev/null tests/e2e/audio-content.spec.ts`는 출력 없이 통과했다.
|
||
- 남은 항목: Phase 4 Gate의 server mode 통합 검증과 Phase 5+ 범위는 수행하지 않았다. 현재 작업트리에는 P4-T4 외 선행 작업의 수정·untracked 파일이 함께 남아 있어 이 기록은 P4-T4 변경과 검증 결과만 대상으로 한다.
|
||
|
||
### P4-T4 독립 검증, 2026-07-28
|
||
|
||
- 무엇을: P4-T4 구현본을 main agent가 fresh 명령과 fresh browser evidence로 재검증하고, 독립 Oracle 2건으로 기능·시각/CJK 검증을 완료했다.
|
||
- 왜: worker의 완료 보고만으로 P4-T4를 수용하지 않고, 현재 작업트리 기준 route capability, 320px overflow, keyboard-only, 200% zoom, axe, P4-GATE 미완료 상태를 별도 증거로 확인하기 위해서다.
|
||
- 어떻게:
|
||
- E2E: `npm run e2e:mock -- tests/e2e/audio-content.spec.ts`는 20 tests 중 17 passed / 3 skipped, exit 0이었다. skipped 3건은 mobile mutation route 차단과 WebKit tab focus 차이로 제한한 keyboard-only path다.
|
||
- Focused unit/component: `npm run test:run -- src/features/audio-contents/tests/audio-list.test.tsx src/features/audio-contents/tests/audio-player.test.tsx src/features/audio-contents/tests/audio-form.test.tsx src/features/audio-contents/tests/audio-form-upload.test.tsx`는 4 files / 11 tests 통과했다.
|
||
- Static/build: `npm run typecheck`, `npm run lint`, `npm run build`는 모두 exit 0이었다. build는 204 modules transformed와 `dist/assets/index-B5HKASem.js` 381.66 kB(gzip 106.65 kB)를 기록했다.
|
||
- LSP/diff/size: `src/features/audio-contents`, `src/shared/ui/admin-audio-player.tsx`, `tests/e2e/audio-content.spec.ts` diagnostics는 clean이었다. P4-T4 대상 `git diff --check`는 출력 없이 통과했다. 순수 LOC는 `playwright.config.ts 29`, `tests/e2e/audio-content.spec.ts 137`, `AudioContentFormPage.tsx 104`, `AudioContentListPage.tsx 98`, `AudioContentDetailPage.tsx 63`, `AudioContentSummary.tsx 22`, `AudioContentListItem.tsx 32`, `admin-audio-player.tsx 125`로 모두 250 이하이다.
|
||
- Browser evidence: `.playwright-mcp/p4-t4-audio-responsive/01-mobile-list-320.png`부터 `06-desktop-detail-1280.png`까지 6개 fresh PNG를 캡처했고, PNG signature는 모두 `89504e470d0a1a0a`였다. 320px 목록·상세·mobile edit guidance, 768px form, 1280px 상세의 `scrollWidth === clientWidth`로 horizontal overflow 0건을 확인했다. console warning/error는 favicon 404 외 0건이었다.
|
||
- 독립 리뷰: 기능 Oracle은 PASS/HIGH로 mobile `/new`·`/edit`가 data fetch/form mount 없이 guidance만 반환하고, list/detail/player availability, desktop/tablet action, keyboard-only, axe/zoom coverage, P4-GATE 미완료 상태를 확인했다. Visual/CJK Oracle은 PASS/HIGH로 6개 screenshot에서 overflow, control clipping, awkward Korean break, mutation control leakage가 없다고 판정했다.
|
||
- 남은 항목: P4-GATE server mode 통합 검증은 아직 수행하지 않았고 완료 표시하지 않았다. Audio 전용 backend 오류 message key, 실제 container/codec 검증, price 상한, active-only server 결과는 기존 backend 계약 대기 항목으로 유지한다.
|
||
|
||
### P4-GATE 부분 검증 및 미완료 기록, 2026-07-28
|
||
|
||
- 무엇을: Phase 4 Gate 명령을 fresh로 실행해 mock UI journey, server-mode 상태, static/build 결과를 분리 기록했다. `P4-GATE`는 완료로 판정하지 않았다.
|
||
- 왜: mock mode 통과는 최종 UI 확인 증거이지만 실제 server integration 완료 증거가 아니며, README의 server mode `testMatch` 교집합 규칙 때문에 filtered Audio spec 실행 결과를 별도 해석해야 하기 때문이다.
|
||
- 어떻게:
|
||
- Preflight: `git status --short`로 현재 작업트리에 P4 선행 변경과 untracked QA/source 파일이 함께 있음을 확인했고, unrelated 변경은 보존했다. `plan-task.md`의 P4-GATE 명령과 README `npm run e2e`/`npm run e2e:mock`의 `testMatch` 제한 문구를 재확인했다.
|
||
- Mock unit/component: `npm run test:run -- src/features/audio-contents src/shared/ui src/shared/validation`은 21 files / 55 tests 통과했다.
|
||
- Mock E2E: `npm run e2e:mock -- tests/e2e/audio-content.spec.ts`는 20 tests 중 17 passed / 3 skipped, exit 0이었다. skipped 3건은 mobile mutation route 차단과 WebKit tab focus 차이를 명시한 keyboard-only path다.
|
||
- Server filtered command: `npm run e2e -- tests/e2e/audio-content.spec.ts`는 `Error: No tests found.`로 종료했다. 현재 `playwright.config.ts`의 server mode `testMatch`가 `server-mode-boundary`, `smoke`, `auth`, `accessibility-shell`만 포함하므로 `audio-content.spec.ts`와 교집합이 없다. 따라서 이 결과는 Audio server integration pass가 아니라 미검증 상태다.
|
||
- Server allowlist sanity: bare `npm run e2e`는 현재 server allowlist 36 tests를 모두 통과했다. 이 명령은 server mode가 browser MSW로 fallback하지 않는 shell/boundary 검증이며, Audio `search_word`, `timezone`, `contentFile`, `releaseDate`, theme field, 수정 금지 control의 실제 server integration 완료 증거는 아니다.
|
||
- Static/build: `npm run typecheck`, `npm run lint`, `npm run build`는 모두 exit 0이었다. build는 204 modules transformed와 `dist/assets/index-B5HKASem.js` 381.66 kB(gzip 106.65 kB)를 기록했다.
|
||
- 남은 항목: `P4-GATE` 완료에는 실제 server mode에서 Audio flow가 실행되어 `search_word`, `timezone`, `contentFile`, `releaseDate`, theme field와 수정 금지 control이 network 요청·화면에 일치한다는 fresh evidence가 필요하다. Audio 전용 backend 오류 message key, 실제 container/codec 검증, price 상한, active-only server 결과는 backend 계약 제공 대기 상태로 유지한다.
|
||
|
||
### P5-T1 Series 계약 확인 및 실행 범위 확정, 2026-07-28
|
||
|
||
- 무엇을: OpenAPI Series 9개 operation과 `Series*` schema를 PRD `SERIES-001~018`, `MOCK-001~009`, 외부 의존 `EXT-002`, `EXT-007`, `EXT-011`과 대조해 `P5-T1` 체크박스 6개를 완료로 표시했다. PRD는 이미 같은 요구사항과 외부 의존을 반영하고 있어 수정하지 않았다.
|
||
- 왜: `P5-T2`부터 계약에 없는 genre lookup, active-only server 결과, 도메인별 오류 key, missing ID와 concurrent conflict key를 추정하지 않고, 제공된 목록·상세·연결·순서 범위만 구현하게 하기 위해서다.
|
||
- 어떻게:
|
||
- JSON parse: `node -e "JSON.parse(require('fs').readFileSync('docs/20260725_AI캐릭터관리자웹/api-contract.openapi.json','utf8')); console.log('JSON.parse ok')"`는 `JSON.parse ok`를 출력하고 exit 0으로 OpenAPI JSON 파싱을 확인했다.
|
||
- Series 계약 확인: focused `node` 검사는 exit 0으로 Series operation 9개, `SeriesCreateRequest`, `SeriesUpdateRequest`, `SeriesListResponse`, `SeriesListItem`, `SeriesDetailResponse`, `SeriesContentListResponse`, `SeriesContentSearchItem`, `SeriesContentAddRequest`, `SeriesOrderUpdateRequest`의 required/properties를 출력했다. `genre path count: 0`으로 genre lookup endpoint 부재도 확인했다.
|
||
- 요청·응답 고정: 연결 후보는 `GET .../contents/search?search_word=...`, 연결은 `{contentIdList}`, 해제는 body 없는 DELETE, 순서는 `{ids}`다. 목록과 연결 콘텐츠 목록은 `page`, `size`, `data.totalCount`, `data.items[]`를 사용한다.
|
||
- 외부 의존 고정: 장르 lookup, active-only 반환 보장, Series 도메인별 오류 key, 순서 저장 missing ID와 concurrent conflict key는 미제공이므로 UI와 mock에서 추정하지 않는다. Series 수정 초기화는 상세 응답의 수정용 원본값으로 처리한다.
|
||
- 상태·component map: 목록·상세·form·연결·순서 화면의 loading, empty, error, read-only, 저장 중, 충돌 또는 재조회 안내 상태와 action inventory를 기록했다. Page는 route/query/policy와 data loading만 조합하고, Feature component가 Series 규칙을 소유하며, 공통 상태·pagination·dialog·file/crop·guard는 Phase 1 shared contract를 사용한다.
|
||
- Mock scenario: 제공 계약 범위의 목록·상세·연결 콘텐츠 목록·후보 검색·연결·해제·전체 순서 happy path와 공통 오류만 browser fixture 대상으로 확정했다. genre lookup, active-only, 충돌 오류는 fixture에서 제외한다.
|
||
- 문서 추적: `rg -n 'SERIES-01[4-8]|EXT-002|EXT-007|EXT-011|contents/search|contentIdList|\{ids\}|genre' docs/20260725_AI캐릭터관리자웹/prd.md docs/20260725_AI캐릭터관리자웹/plan-task.md`는 exit 0으로 PRD와 plan의 Series 계약, 외부 의존, 연결·순서 키워드 추적을 확인했다.
|
||
- Diff 검사: `git diff --check -- docs/20260725_AI캐릭터관리자웹/prd.md docs/20260725_AI캐릭터관리자웹/plan-task.md`는 출력 없이 exit 0으로 문서 공백 오류 0건을 확인했다.
|
||
- 남은 항목: P4-GATE는 이전 기록처럼 미완료다. `P5-T1`에서는 애플리케이션 코드, tests, mocks, config, DESIGN.md, QA artifacts를 변경하지 않았고 `P5-T2` 이상과 `P5-GATE`는 완료 표시하지 않았다.
|
||
|
||
### P5-T2 Series 목록·상세 조회 구현, 2026-07-28
|
||
|
||
- 무엇을: 선택한 Character workspace 아래 Series read-only 목록/상세 vertical slice를 구현하고 `P5-T2` 체크박스 4개만 완료로 표시했다.
|
||
- 왜: OpenAPI가 제공한 `SeriesListResponse`와 `SeriesDetailResponse`의 서로 다른 DTO를 그대로 표시하고, 계약 없는 genre lookup·active-only filter·mutation action을 추정하지 않기 위해서다.
|
||
- 어떻게:
|
||
- RED 1: `npm run test:run -- src/features/series/tests/series-contract.test.ts`는 테스트 파일 부재 상태에서 `No test files found, exiting with code 1`로 실패했다.
|
||
- RED 2: 같은 명령은 테스트 추가 후 `Failed to resolve import "@/features/series/api/series-api"`로 실패해 production Series slice 부재를 확인했다. 중간에 `.ts` 파일 JSX parse 오류 1회는 `createElement(App)`으로 test harness만 수정해 올바른 RED로 재실행했다.
|
||
- GREEN: `npm run test:run -- src/features/series/tests/series-contract.test.ts`는 1 file / 4 tests 통과했다.
|
||
- Related route/mock: `npm run test:run -- src/app/App.test.tsx src/app/App.protected-shell.test.tsx src/shared/mocks/__tests__/character-handlers.test.ts`는 3 files / 17 tests 통과했다.
|
||
- REFACTOR/regression: `npm run test:run -- src/features/series/tests/series-contract.test.ts src/features/audio-contents/tests/audio-list.test.tsx src/features/audio-contents/tests/audio-player.test.tsx src/features/characters/tests/character-list.test.tsx`는 4 files / 8 tests 통과했다. 최종 회귀 `npm run test:run`은 52 files / 200 tests 통과했다.
|
||
- Static/build: `npm run typecheck`, `npm run lint`, `git diff --check -- src/features/series src/shared/mocks src/app src/layouts docs/20260725_AI캐릭터관리자웹/plan-task.md`는 출력 없이 exit 0이었다. `npm run build`는 production build를 완료했다.
|
||
- LSP/LOC: 변경 TS/TSX 파일 diagnostics는 clean이었다. `src/shared/mocks/series-handlers.ts` 단일 fresh diagnostics는 반복 timeout됐지만 `src/shared/mocks` directory diagnostics는 16 files / 0 diagnostics였고 `typecheck`로 보완했다. `handlers.ts`는 Series handler 분리 후 pure LOC 236으로 250 이하가 됐다.
|
||
- Manual QA: `npm run dev:mock` 서버에서 Playwright Chromium으로 login → `/ai-characters/101/series?page=0&size=20` → 목록 확인 → detail link → `/ai-characters/101/series/5001` → reload를 실제 조작했다. Artifacts는 `.playwright-mcp/p5-t2-series-readonly/list-1280.png`, `detail-1280.png`, `detail-reload.png`, 각 snapshot, `list-action-check.json`, `detail-dom-check.json`, `detail-reload-check.json`, network/console 파일이다. `list-action-check.json`은 `searchboxCount: 0`, `forbiddenActions: []`, `visibleSeriesText: true`였다. `detail-dom-check.json`은 `hasDetailStrings: true`, `hasReverseEnumLeak: false`, `forbiddenActions: []`였다. `detail-reload-check.json`은 `titleVisible: true`, `stringsVisible: true`였다. Series network는 목록 `?page=0&size=20`와 상세 `/series/5001`만 200으로 기록됐다. Console은 기능과 무관한 `/favicon.ico` 404 1건만 있었다.
|
||
- Visual evidence: `look_at`으로 3개 screenshot을 확인했고, 목록은 시리즈 2건과 상세 보기·pagination만 보이며 상세/reload는 표시 문자열만 보이고 생성/수정/비활성화/연결/순서 변경 control 및 명백한 clipping/layout 깨짐이 없었다. 이 harness의 subagent tool은 `oracle`을 지원하지 않아 `/visual-qa` dual-oracle pass는 실행하지 못했다.
|
||
- Final QA refresh: refactor 후 `npm run dev:mock` 서버에서 Playwright Chromium으로 login → `/ai-characters/101/series?page=0&size=20` → 상세 링크 → `/ai-characters/101/series/5001` → reload를 다시 조작했다. Fresh artifacts는 `.playwright-mcp/p5-t2-series-final/list-1280.png`, `detail-1280.png`, `detail-reload.png`, `list-action-check.json`, `detail-dom-check.json`, `detail-reload-check.json`, `list-network.txt`, `detail-network.txt`, `console-warnings.txt`다. 목록은 `visibleSeriesText: true`, `searchboxCount: 0`, `forbiddenControls: []`였고 상세는 `hasDetailStrings: true`, `hasReverseEnumLeak: false`, `forbiddenControls: []`였다. reload는 `titleVisible: true`, `stringsVisible: true`, URL `/ai-characters/101/series/5001`을 유지했다. Series network는 목록과 상세만 200이었고 console은 기능과 무관한 `/favicon.ico` 404만 있었다.
|
||
- 남은 항목: Series 생성·수정·비활성화, genre lookup, active-only server 보장, 연결·해제·순서 UI, P5-T3~P5-GATE는 구현하거나 완료 표시하지 않았다. P4-GATE도 이전 기록처럼 미완료다.
|
||
|
||
### P5-T4 Series Audio 연결·해제·전체 순서 구현, 2026-07-28
|
||
|
||
- 무엇을: Series 상세에 연결된 Audio 목록/후보 검색/연결/해제 UI를 추가하고, 전체 Series 순서 관리 route와 keyboard 위/아래 저장 흐름을 구현해 `P5-T4` 체크박스만 완료로 표시했다.
|
||
- 왜: 제공된 `page/size + totalCount/items`, `search_word`, `{ contentIdList }`, body 없는 `DELETE`, `{ ids }` 계약을 추정 없이 고정하고 P5-T3의 genre lookup과 form 범위, P5-T5/GATE 범위는 건드리지 않기 위해서다.
|
||
- 어떻게:
|
||
- RED contract: `npm run test:run -- src/features/series/tests/series-contract.test.ts`는 1 file / 6 tests 중 2 failed, `TypeError: getSeriesContents is not a function`, `TypeError: getAllSeriesForOrder is not a function`으로 실패했다.
|
||
- RED linked UI: `npm run test:run -- src/features/series/tests/series-contents.test.tsx`는 `Failed to resolve import "@/features/series/components/SeriesContents"`로 실패했다.
|
||
- RED order UI: `npm run test:run -- src/features/series/tests/series-order.test.tsx`는 `Failed to resolve import "@/features/series/pages/SeriesOrderPage"`로 실패했다.
|
||
- RED E2E: `playwright.config.ts`가 새 spec을 제외해 최초 `No tests found`가 났고, mock `testMatch`에 `series.spec.ts`를 추가한 뒤 `npm run e2e:mock -- tests/e2e/series.spec.ts`는 4 failed, `연결된 오디오` heading not found로 실패했다.
|
||
- GREEN contract: `npm run test:run -- src/features/series/tests/series-contract.test.ts`는 1 file / 6 tests 통과했다. 새 contract는 현재 연결 목록 GET이 `?page=1&size=20`만 보내고, 후보 검색이 `contents/search?search_word=...`만 보내며, POST body가 `JSON.stringify({ contentIdList: [9002, 9003] })`, DELETE body가 `undefined`, PUT body가 `JSON.stringify({ ids: [5002, 5001] })`임을 고정한다.
|
||
- GREEN linked UI: `npm run test:run -- src/features/series/tests/series-contents.test.tsx`는 1 file / 3 tests 통과했다. linked list pagination, 중복 후보 제거, 단일 선택 POST, title/영향 confirmation, body-less DELETE, mutation 실패 시 현재 목록 보존과 재조회/재시도 안내를 확인했다.
|
||
- GREEN order UI: `npm run test:run -- src/features/series/tests/series-order.test.tsx`는 1 file / 2 tests 통과했다. `totalCount` 기반 page 0/1 전체 fetch, 위/아래 button reorder, `{ ids }` 저장, 실패 시 현재 UI 순서 보존과 재조회/재시도 안내를 확인했다.
|
||
- E2E: `npm run e2e:mock -- tests/e2e/series.spec.ts`는 Chromium/WebKit/Mobile Chrome/Mobile Safari 4 tests 통과했다. login → `/ai-characters/101/series/5001` → 후보 검색/연결 → 해제 confirmation/DELETE → order page → 위로 button → save → SPA refetch 순서 유지 흐름을 확인했다.
|
||
- Final focused regression: `npm run test:run -- src/features/series`는 3 files / 12 tests 통과했다.
|
||
- Static: `npm run typecheck`와 `npm run lint`는 출력 없이 exit 0이었다.
|
||
- Manual QA: `npm run dev:mock` 서버에서 Playwright MCP로 login → `/ai-characters/101/series/5001` → linked list pagination 확인 → 후보 `아침` 검색 → 중복 후보 제거와 선택/연결 → title/영향 confirmation 후 해제 → `/series/order` 위/아래 button 저장 → detail로 돌아갔다가 order 재진입해 저장 순서 유지까지 실제 조작했다. Artifacts는 `.playwright-mcp/p5-t4-series-link-order/01-detail-linked-list.png`, `02-network-linked-list.txt`, `03-after-link.png`, `04-network-link-post.txt`, `05-unlink-confirmation.png`, `06-after-unlink.png`, `07-network-unlink-delete.txt`, `08-order-page-before.png`, `09-order-after-save.png`, `10-network-order-save.txt`, `11-order-refetch-persisted.png`, `12-console-warning-error.txt`다. Network evidence는 linked GET이 `/contents?page=0&size=20`만, search가 `/contents/search?search_word=%EC%95%84%EC%B9%A8`만, DELETE가 `/contents/9001`, order page load가 `/series?page=0&size=50`임을 기록했다. Console은 기능과 무관한 `/favicon.ico` 404 1건만 있었다. 정정: 독립 검증에서 OpenAPI reorder path가 plural `PUT /api/v2/admin/ai-characters/{characterId}/series/orders`임을 확인해 follow-up에서 API/mock endpoint와 contract evidence를 `/series/orders`로 수정했다. Fresh browser 재확인에서는 `/ai-characters/101/series/order`에서 keyboard `위로` 이동 후 `순서 저장` 시 `PUT https://test-character-admin.sodalive.net/api/v2/admin/ai-characters/101/series/orders`와 body `{"ids":[5002,5001]}`가 발생했고 singular `/series/order` PUT은 0건이었다. Fresh screenshot은 `.playwright-mcp/p5-t4-series-link-order/13-order-plural-endpoint-fresh.png`다.
|
||
- Follow-up DTO/focus 정정: 독립 검증에서 Series content list/search DTO가 invented `detail`·`coverImageUrl`을 쓰고 unlink alertdialog가 기존 `useModalFocus` pattern을 쓰지 않는 blocker를 확인했다. OpenAPI 기준 list item은 `contentId`, `coverImage`, `title`, `isAdult`, search item은 `contentId`, `title`, `coverImage`만 parse/display하도록 수정하고, unlink dialog는 `ConfirmDeactivateDialog`와 같은 modal focus entry/trap pattern을 적용했다. 추가 품질 리뷰에서 Escape 닫힘과 Shift+Tab/focus return 회귀 검증 부족이 남아 `Escape` close와 trigger focus return test를 보강했고, focused/series/mock E2E/typecheck/lint/build 및 재리뷰 PASS를 확인했다.
|
||
- OQ-009: 실제 연결 UI는 checkbox 후보를 한 번에 확인·해제해야 하므로 `contentIdList` 권고안은 50개/요청, 실제 순서 UI는 keyboard 위/아래 조작과 전체 저장의 인지 부하를 고려해 `ids` 권고안은 200개/캐릭터로 기록한다. backend 호환 확인 전까지 client 상한은 구현하지 않았다.
|
||
- 남은 항목: P5-T3는 genre lookup과 상세 수정용 원본값 제공 전까지 pending/blocked이고, P5-T5와 P5-GATE는 완료 표시하지 않았다. P4-GATE도 이전 기록처럼 미완료다. 실제 backend의 domain-specific conflict/missing-id response key는 추정하지 않고 generic 재조회/재시도 안내만 구현했다.
|
||
|
||
### P5-T5 Series 반응형·접근성 capability 완료, 2026-07-28
|
||
|
||
- 무엇을: 활성 Series 범위(P5-T2 read-only 목록/상세, P5-T4 연결 콘텐츠·전체 순서)만 대상으로 320px 모바일 조회 capability, desktop/tablet 관리 capability, keyboard-only 연결·해제·정렬, 200% zoom, axe critical/serious 0건을 `tests/e2e/series.spec.ts`로 고정하고 `P5-T5` 활성 체크박스를 완료로 표시했다.
|
||
- 왜: P5-T3의 genre lookup과 상세 수정용 원본값 제공이 아직 필요해 요일·genre/form 접근성은 구현·테스트하지 않고 대기 항목으로 분리해야 하며, 모바일에서는 제공되지 않은 CRUD와 P5-T4 mutation action이 route capability로 차단돼야 하기 때문이다.
|
||
- 어떻게:
|
||
- RED: `npm run e2e:mock -- tests/e2e/series.spec.ts`는 새 P5-T5 coverage 추가 후 24 tests 중 4 failed / 3 skipped / 17 passed로 실패했다. 실패 원인은 320px Series 상세에서 `검색어` searchbox가 visible이라 모바일 연결 mutation action이 차단되지 않은 것이었다. locator strictness 보정 전 1차 실패는 테스트 결함으로 수정 후 RED를 재확인했다.
|
||
- GREEN implementation: Series 전용 `useSeriesManagementCapability`를 추가해 `(min-width: 768px)` 이상에서만 연결 검색/연결/해제와 전체 순서 route를 활성화했다. 320px 상세는 연결 콘텐츠 목록만 조회하고, `/series/order` 직접 진입은 태블릿 이상 안내만 표시한다. P5-T3 create/edit/delete/genre UI/API는 추가하지 않았다.
|
||
- Focused E2E: 최종 `npm run e2e:mock -- tests/e2e/series.spec.ts`는 24 tests 중 21 passed / 3 skipped, exit 0이었다. skipped 3건은 mobile mutation route 차단과 WebKit tab focus 차이를 명시한 keyboard-only path다. E2E는 320px list/detail/order guidance overflow 0건, desktop/tablet 768·1280 linked search/link/unlink/order save, keyboard-only search/select/link, unlink dialog focus/Escape/focus return, order up/save, 200% zoom, axe critical/serious 0건을 확인한다.
|
||
- Focused component: `npm run test:run -- src/features/series/tests/series-contents.test.tsx`는 1 file / 3 tests 통과했다. 기존 component tests는 desktop management capability를 명시하도록 갱신했다.
|
||
- Static/build: 최초 `npm run typecheck`와 `npm run build`는 `SeriesContents` test fixture의 `canManageSeries` prop 누락으로 실패했고, fixture 수정 후 `npm run typecheck`, `npm run lint`, `npm run build`, `git diff --check`는 모두 exit 0이었다. build는 215 modules transformed와 `dist/assets/index-Bw6rMAMS.js` 405.92 kB(gzip 110.63 kB)를 기록했다.
|
||
- LSP/LOC: `src/features/series` directory diagnostics는 10 files / 0 diagnostics, `tests/e2e` directory diagnostics는 9 files / 0 diagnostics였다. 단일 `tests/e2e/series.spec.ts` fresh diagnostics는 반복 3초 timeout됐지만 directory diagnostics, typecheck, lint로 보완했다. 순수 LOC는 `use-series-management-capability.ts` 19, `SeriesDetailPage.tsx` 62, `SeriesContents.tsx` 161, `SeriesOrderPage.tsx` 102, `series-contents.test.tsx` 103, `series.spec.ts` 167로 모두 250 이하이다.
|
||
- Visual QA: 1차 pass A는 PASS였고 pass B는 320px 공통 header의 `모바일 메뉴 열기`, `AI 캐릭터`, `로그아웃` CJK 음절 줄바꿈을 blocker로 반환했다. `protected-admin-shell.tsx` header controls에 `shrink-0 whitespace-nowrap`을 적용하고 `App.protected-shell.test.tsx` 회귀 test를 추가했다. RED `npm run test:run -- src/app/App.protected-shell.test.tsx`는 `whitespace-nowrap` class 부재로 1 failed였고, 수정 후 7 tests passed였다. 최종 7개 fresh screenshot `.playwright-mcp/p5-t5-series-responsive/11-final-mobile-320-list.png`부터 `17-final-desktop-1280-order.png`까지 PNG signature `89504e470d0a1a0a`, 모든 route `hasHorizontalOverflow:false`, header computed `whiteSpace:"nowrap"`였고, visual QA pass B 재검토는 PASS였다.
|
||
- 남은 항목: P5-T3는 genre lookup과 상세 수정용 원본값 제공 전까지 pending/blocked이며 요일·genre/form 접근성 체크도 대기한다. P5-GATE는 완료 표시하지 않았다. P4-GATE도 이전 기록처럼 미완료다. 실제 server mode Series integration과 active-only/domain-specific 오류 key는 기존 외부 의존 상태를 유지한다.
|
||
|
||
### P5-GATE Series 부분 검증, 2026-07-28
|
||
|
||
- 무엇을: Phase 5 Gate 명령을 현재 제공된 Series 계약 범위에서 fresh 실행하고, mock UI journey와 server mode 실행 가능 상태를 분리해 기록했다.
|
||
- 왜: P5-T3 genre lookup과 상세 수정용 원본값 제공이 아직 필요하고, server mode Series spec은 현재 Playwright server allowlist와 file filter 교집합에서 실행되지 않으므로 Gate 완료를 주장하면 안 되기 때문이다.
|
||
- 어떻게:
|
||
- `npm run test:run -- src/features/series`는 3 files / 12 tests 통과했다.
|
||
- `npm run e2e:mock -- tests/e2e/series.spec.ts`는 24 tests 중 21 passed / 3 skipped로 통과했다. skipped 3건은 mobile mutation 차단과 WebKit tab-focus 차이로 명시됐다.
|
||
- `npm run e2e -- tests/e2e/series.spec.ts`는 `No tests found`로 실행되지 않았다. 이는 server mode `testMatch`와 직접 file filter의 교집합 문제이며, Series 실제 server integration 성공 증거가 아니다.
|
||
- `npm run typecheck`, `npm run lint`, `npm run build`는 모두 exit 0이었다. build는 215 modules transformed와 `dist/assets/index-vqn3JSuw.js` 406.01 kB(gzip 110.65 kB)를 기록했다.
|
||
- 남은 항목: `P5-GATE` 완료에는 Series server mode spec이 실제로 실행되어 목록·상세·연결·해제·순서 network가 개발 API 계약과 일치한다는 fresh evidence가 필요하다. P5-T3 genre lookup, 상세 수정용 원본값, active-only 보장, Series 도메인별 오류 key도 외부 의존으로 유지한다.
|
||
|
||
### P6-T1 Community 계약 확인, 2026-07-28
|
||
|
||
- 무엇을: OpenAPI Community 3개 operation과 `CommunityPost*` schema를 PRD `COMMUNITY-001~015`, 최신 외부 의존 `EXT-007~008`, Phase 6 계획과 대조해 `P6-T1` 체크박스 6개를 완료로 표시했다.
|
||
- 왜: P6-T2 이후 목록/Sheet/form 구현이 detail GET, pagination metadata, 수정 audio/price, 오류 key, price 상한을 추정하지 않게 하기 위해서다.
|
||
- 어떻게:
|
||
- OpenAPI parse: `node -e ...`로 Community path가 `GET/POST /api/v2/admin/ai-characters/{characterId}/community-posts`, `PUT /api/v2/admin/ai-characters/{characterId}/community-posts/{postId}` 3개 operation임을 확인했다.
|
||
- 요청·응답 고정: 목록은 필수 `timezone`, `page`, `size` query와 배열 `data`를 사용한다. 생성 multipart는 optional `audioFile`, optional `postImage`, required `request`이고 request는 required `content`, `isCommentAvailable`, `isAdult`, optional `price`다. 수정 multipart는 optional `postImage`, required `request`만 허용하고 request field는 `content`, `isCommentAvailable`, `isAdult`, `isActive`, `isFixed`다. POST/PUT 성공은 `NullSuccess`의 `data=null`만 신뢰한다.
|
||
- 제외 고정: Community 목록 응답에는 `totalCount`, `page`, `hasNext`, `isActive`, `fixedAtUtc`가 없으므로 fixture·DTO·UI에 추가하지 않는다. 수정 request에는 `price`와 `audioFile`이 없으므로 수정 audio/price UI나 request를 만들지 않는다. Community detail GET과 detail/edit route도 만들지 않는다.
|
||
- 오류/외부 의존: 공통 오류 fixture는 `ApiErrorResponse`의 `success=false`, `message`, `data=null`, `errorProperty`와 OpenAPI에 있는 400/401/403/404/405/406/415/500 status만 사용한다. Community 전용 message key와 media upload 오류 key는 추정 분기하지 않고, price 최대값은 `99999`, 파일 용량·MIME backend 동일 검증은 확인됐다. active-only 반환과 pagination 종료 metadata는 `EXT-007~008` 제공 전까지 추정하지 않는다.
|
||
- 상태·component map: list Page는 `characterId`, `timezone=Asia/Seoul`, `page`, `size`, `items`, `loading`, `empty`, `error`, `retry`, selected sheet item을 소유한다. Sheet는 목록 item source로 열리며 별도 detail fetch 없이 update/pin/soft delete action과 cache invalidation을 소유한다. Form/media component는 Community content/isAdult/comment/price/postImage/audioFile 규칙을 소유하고, shared FileField/ImageCropDialog/AdminAudioPlayer/validation은 Phase 1·4 contract를 재사용한다.
|
||
- Mock scenario: deterministic seed는 고정 게시글, 이미지 게시글, audioUrl 게시글, 댓글 요약이 있는 게시글을 포함한다. mock handler는 list timezone/page/size, create multipart, update multipart, pin/unpin, soft delete `isActive=false`·`isFixed=false`, `data=null`, detail GET 0회를 검증한다. active-only server 결과, pagination 종료, Community 전용 오류 key, price 상한은 mock에서도 추정하지 않는다.
|
||
- 문서 추적: `rg -n 'COMMUNITY-01[2-5]|EXT-00[7-9]|EXT-010|EXT-011|CommunityPost|postImage|audioFile|timezone|totalCount|hasNext|fixedAtUtc' docs/20260725_AI캐릭터관리자웹/prd.md docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/api-contract.openapi.json`는 exit 0으로 PRD·plan·OpenAPI의 Community 계약과 외부 의존 범위를 확인했다.
|
||
- 남은 항목: `P6-T2`부터 실제 Community list/Sheet route, API, mock, tests를 TDD로 구현한다. Comments는 Phase 8 범위이며 P6에서 구현하지 않는다.
|
||
|
||
### P6-T2 Community 목록·collection Sheet 구현, 2026-07-28
|
||
|
||
- 무엇을: Community posts 목록 route와 목록 item 기반 Sheet 수정·고정/해제·비활성화 흐름을 구현하고 `P6-T2` 체크박스 8개를 완료로 표시했다.
|
||
- 왜: 제공 계약에 없는 Community detail GET/route, 검색, `totalCount`, `hasNext`, `fixedAtUtc`, active filter를 만들지 않고 목록 응답만으로 collection Sheet mutation을 끝내기 위해서다.
|
||
- 어떻게:
|
||
- RED: focused test 최초 실행은 import/route 부재와 UI가 Character list로 fallback되는 실패를 확인했고, 이후 `@/features/community-posts/model/types` 부재와 Sheet mutation 미발생 실패를 거쳐 P6-T2 경로를 고정했다.
|
||
- GREEN focused: `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`는 3 files / 8 tests 통과했다.
|
||
- Static/build: `npm run typecheck`, `npm run lint`, `npm run build`는 모두 exit 0이었다. `src/features/community-posts`, `src/app`, `src/shared/mocks` directory LSP diagnostics는 0 diagnostics였고, Community 관련 source/test/mock 파일은 모두 pure LOC 250 이하이다. `git diff --check -- src/features/community-posts src/app/route-paths.ts src/app/browser-location.ts src/app/protected-admin-shell.tsx src/layouts/CharacterWorkspaceLayout.tsx src/shared/mocks/community-post-fixtures.ts src/shared/mocks/community-post-handlers.ts src/shared/mocks/community-post-mock-store.ts src/shared/mocks/handlers.ts src/shared/mocks/character-mock-store.ts docs/20260725_AI캐릭터관리자웹/plan-task.md`는 출력 없이 exit 0이었다.
|
||
- Review closure: 목록 `price`는 0 이상 정수로 파싱하고 update schema는 `isActive=false`만 허용하도록 고정했다. Sheet mutation 실패 시 `role="alert"` 오류를 표시하고 저장 상태를 해제하는 회귀 test를 추가했다. Sheet test의 multipart payload 검증 중복은 contract test로 이동해, Sheet test는 버튼 조작→PUT 호출→목록 갱신만 확인한다.
|
||
- Contract: 목록 request는 `timezone=Asia%2FSeoul&page=size` 순서의 `timezone/page/size`만 보내고 배열 `data`만 소비한다. update/soft delete는 multipart `request` part와 성공 `data=null`을 처리하며 soft delete request는 `{ isActive: false, isFixed: false }`이고 `isActive=true`를 보내지 않는다.
|
||
- Router/UI: `/ai-characters/:characterId/community-posts`를 workspace route로 추가했고 `/community-posts/:postId` 및 `/edit`은 앱 route가 아니며 detail GET handler 호출 0건을 test로 고정했다.
|
||
- Mock: `community-post-handlers`와 list store가 timezone/page/size, update multipart, pin/unpin, soft delete를 list store만으로 처리한다. Community detail handler는 만들지 않았다.
|
||
- Manual QA: `npm run dev:mock` 서버에서 Playwright MCP로 login → `/ai-characters/101/community-posts?page=0&size=20` → 목록 item Sheet 열기 → 내용 수정/댓글 허용 해제/성인 콘텐츠 체크 → `수정 저장` → `고정 해제` → `고정하기` → `비활성화`를 실제 조작했다. Network evidence는 list GET이 `?timezone=Asia%2FSeoul&page=0&size=20`만 사용하고 각 PUT 뒤 list GET이 재실행됨을 보였다. Soft delete 뒤 성공 알림 `커뮤니티 게시글을 비활성화했습니다.`와 남은 목록 2건을 확인했다. Artifacts는 `p6-t2-community/03-after-edit.png`, `04-after-unpin.png`, `05-after-pin.png`, `06-after-delete.png`, `network-after-unpin.txt`, `network-after-pin.txt`, `network-after-delete.txt`, `console-final.txt`다. Console은 기능과 무관한 `/favicon.ico` 404만 있었다. `look_at`으로 `06-after-delete.png`를 확인해 성공 알림, 목록 표시, 명백한 한글 clipping/overflow 없음도 확인했다.
|
||
- 남은 항목: Community 생성, postImage/audioFile form, GIF/media policy, 반응형·접근성 E2E, 실제 server integration, active-only 반환과 pagination 종료 metadata는 `P6-T3`~`P6-GATE`와 외부 의존 범위로 남겼다.
|
||
|
||
### P6-T3 Community 게시글 form·첨부 media 구현, 2026-07-28
|
||
|
||
- 무엇을: Community 게시글 생성 route/form, create multipart serializer, update Sheet의 postImage-only 첨부, JPEG/PNG crop·GIF 원본 보존·audio policy·player/no-refetch 규칙을 구현하고 `P6-T3` 체크박스 12개를 완료로 표시했다.
|
||
- 왜: OpenAPI가 제공한 생성·수정 multipart 계약만 구현하고, 범위 밖인 댓글, detail GET, edit route, update audio/price, GIF 재인코딩, URL refresh-only request, mobile mutation capability를 추정하지 않기 위해서다.
|
||
- 어떻게:
|
||
- RED: `npm run test:run -- src/features/community-posts/tests/community-contract.test.ts src/features/community-posts/tests/community-form.test.tsx src/features/community-posts/tests/community-list.test.tsx src/features/community-posts/tests/community-sheet.test.tsx` 최초 실행은 `CommunityPostFormPage` import 부재, `createCommunityPost is not a function`, mock POST timeout으로 실패해 생성 API/page/mock 부재를 확인했다.
|
||
- GREEN focused: 최종 `npm run test:run -- src/features/community-posts/tests/community-contract.test.ts src/features/community-posts/tests/community-form.test.tsx src/features/community-posts/tests/community-list.test.tsx src/features/community-posts/tests/community-sheet.test.tsx`는 4 files / 14 tests 통과했다. 이후 fresh focused `npm run test:run -- src/features/community-posts src/shared/validation`도 실행해 Community와 shared file policy 회귀를 확인했다.
|
||
- Contract/form: create는 POST `/community-posts`에 optional `audioFile`, optional `postImage`, required `request`를 보내고 request는 `content`, `isCommentAvailable`, `isAdult`, optional `price`만 허용한다. create schema는 strict라 `isActive`와 `isFixed`를 거부한다. update는 PUT `/community-posts/:postId`에 optional `postImage`와 request `content`, `isCommentAvailable`, `isAdult`, `isActive=false`, `isFixed`만 허용하며 `audioFile`과 `price`를 보내지 않는다.
|
||
- Media: JPEG/PNG는 기존 `ImageCropDialog`와 crop helper를 사용해 `aspect: "free"`, `maxWidth: 800`, `noUpscale: true`로 전송한다. GIF는 Community 전용 정책에서만 허용하고 crop/canvas/re-encode 없이 원본 `File`을 전송하며 width 800px은 허용, 801px은 제출 전 오류로 차단한다. 첨부 audio는 기존 `AUDIO_FILE_POLICY`와 `FileField`를 사용해 MP3/AAC/M4A, `audio/x-m4a`, `1,024,000,000 bytes`, WAV 거부를 유지한다.
|
||
- Player/cache: `audioUrl`이 있으면 desktop row, mobile card, Sheet에 `AdminAudioPlayer`를 렌더링한다. audio error와 manual retry는 detail/list refetch, URL 재발급, autoplay를 일으키지 않는 test로 고정했다. 새 `audioUrl`은 create/update 후 정상 list refetch로만 화면에 반영된다.
|
||
- Mock: `community-post-handlers`가 POST/PUT multipart contract를 검증하고 list store에 local preview image/audio data URL을 반영한다. Community detail handler는 만들지 않았다.
|
||
- Static/build: 변경 TS/TSX 파일과 `src/features/community-posts`, `src/shared/mocks` directory LSP diagnostics는 0 diagnostics였다. `npm run typecheck`, `npm run lint`, `npm run build`, `git diff --check`는 모두 exit 0이었다.
|
||
- Manual QA: `npm run dev:mock` 서버에서 Playwright MCP로 login → `/ai-characters/101/community-posts?page=0&size=20` → `커뮤니티 게시글 생성` link → 빈 submit validation `내용을 입력하세요.` → content/price 입력 → `생성` → `/ai-characters/101/community-posts` 복귀 → 성공 알림과 새 행 `브라우저 QA 생성 게시글` 표시를 실제 조작했다. Screenshot artifact는 `p6-t3-community-create-list.png`다. Console은 기능과 무관한 `/favicon.ico` 404만 있었다.
|
||
- Visual QA: read-only visual QA pass A/B는 모두 PASS였다. Pass A는 생성 route 이동, form validation, 성공 후 목록 복귀, 새 행/성공 배너, DESIGN.md token/component 재사용에 blocker가 없다고 판정했다. Pass B는 `p6-t3-community-create-list.png`에서 한국어 CJK clipping, 잘린 버튼/라벨, 깨진 레이아웃이 없고 기존 카드/테이블 패턴과 일치한다고 판정했다.
|
||
- OQ-009: 실제 Community Sheet/form의 `content` textarea와 목록 줄바꿈을 확인했다. 권고안은 `content` 2,000자이며, backend 호환 확인 전까지 client 상한은 구현하지 않았다.
|
||
- 남은 항목: Community 반응형·접근성 E2E와 mobile mutation capability는 `P6-T4`, 실제 server integration과 active-only 반환·pagination 종료 metadata는 `P6-GATE`/외부 의존으로 남긴다. Comments, Community detail GET, `/community-posts/:postId`, `/edit`, search, totalCount/hasNext, fixedAtUtc, update audio/price, price max enforcement, GIF re-encoding, URL refresh-only request는 구현하지 않았다.
|
||
|
||
**P6-T3 review 보완 검증 기록 (2026-07-28):**
|
||
|
||
- 무엇을: 비활성 캐릭터의 Community 생성/Sheet mutation 진입 차단, mock Community `characterId` 격리, `.m4a`의 `audio/mp4`/`audio/x-m4a` 허용, 유효한 mp4 data URL fixture, mock multipart part/cardinality 검증, update `postImage` preview 반영, async image 준비 중 submit disable, Sheet multipart 검증 test를 보완했다.
|
||
- 왜: P6-T3 review에서 활성 캐릭터 전제 누락, mock 격리 부족, audio MIME 경계와 multipart 계약 검증이 느슨한 부분이 확인됐기 때문이다.
|
||
- 어떻게: RED focused는 `npm run test:run -- src/features/community-posts/tests/community-contract.test.ts src/features/community-posts/tests/community-form.test.tsx src/features/community-posts/tests/community-sheet.test.tsx src/shared/validation/file-media-policy.test.ts`로 기대 실패를 확인했다. GREEN focused는 같은 명령이 4 files / 26 tests passed, fresh feature 회귀 `npm run test:run -- src/features/community-posts src/shared/validation`이 5 files / 28 tests passed였다.
|
||
- Static/build: `npm run typecheck`, `npm run lint`, `npm run build`, `git diff --check`가 모두 exit 0이었다.
|
||
- Manual QA: 기존 `npm run dev:mock` 서버(`http://127.0.0.1:8889`)에서 Playwright MCP로 login → 비활성 캐릭터 `/ai-characters/202/community-posts` 생성 link 0건과 read-only 배너 확인 → `/ai-characters/202/community-posts/new` 직접 접근 시 `커뮤니티 게시글 생성 차단`과 `생성` button 0건 확인 → 활성 캐릭터 `/ai-characters/101/community-posts` 생성 form validation → 새 게시글 생성 → Sheet 수정 저장 → Sheet 비활성화를 실제 조작했다. 결과 steps는 `login-ok`, `inactive-list-readonly-ok`, `inactive-form-blocked-ok`, `active-create-ok`, `active-sheet-edit-ok`, `active-sheet-delete-ok`였고 missing 항목은 없었다. Network는 login POST, Community create POST, Sheet update/soft delete PUT과 character-scoped list GET만 200으로 기록됐으며 Community detail GET은 없었다. `p6-t3-community-create-list.png`를 갱신했고 `look_at` 확인 결과 성공 배너와 목록이 보이며 명백한 한국어 clipping/layout 깨짐은 없었다. Console은 기능과 무관한 React DevTools 안내와 `/favicon.ico` 404뿐이었다.
|
||
- 범위 유지: Comments, Community detail GET/route, search, total/hasNext/fixedAtUtc, active filter, server-specific error key, P6-T4 mobile mutation capability는 구현하지 않았다.
|
||
|
||
**P6-T3 2차 review 보완 정정 기록 (2026-07-28):**
|
||
|
||
- 정정: 위 P6-T3 최초 RED 기록의 `CommunityPostFormPage` import 부재와 `createCommunityPost is not a function`, mock POST timeout은 §2.1 line 72의 "import 오류, test 환경 오류, 임시 mock 누락 같은 우발적 실패는 Red 증거로 인정하지 않는다" 규칙상 유효한 RED 증거가 아니다. 유효 RED는 2차 보완에서 추가한 `mock handlers reject non-multipart bodies and text upload fields`, `mock handlers update postImage preview and use a valid bundled audio preview`, `Community Sheet keeps save impossible while image preparation is pending`의 기대 실패다.
|
||
- 무엇을: Community mock parser를 `request.formData()` 우선 strict parser로 바꾸고 raw multipart fallback에도 같은 part/cardinality/file-field 검증을 적용했다. `text/plain` raw body, string `postImage`, string `audioFile`, update의 `audioFile`과 unknown/duplicate part를 400으로 거부한다. Community mock audio fixture는 기존 잘린 `audio/mp4`가 아니라 Audio mock의 decodable WAV data URL을 재사용한다. Sheet는 이미지 준비 중이거나 crop dialog가 열린 동안 `수정 저장`/고정/비활성화 mutation을 disabled 처리한다. `community-sheet.test.tsx`는 support를 분리해 250 pure LOC 아래로 낮췄다.
|
||
- 왜: 두 번째 review에서 playable Community mock audio, strict multipart boundary parsing, Sheet pending image race, update image URL proof, test file size, 문서 overclaim이 남았기 때문이다.
|
||
- RED: `npm run test:run -- src/features/community-posts/tests/community-contract.test.ts src/features/community-posts/tests/community-sheet.test.tsx`는 추가 직후 2 files 중 3 tests failed였다. 실패는 `text/plain` raw multipart가 200으로 통과, `previewCommunityPostAudioUrl`이 `data:audio/mp4;base64,AAAAIGZ0eXBtcDQy`라 WAV 기대 불일치, Sheet 이미지 준비 중 `수정 저장` button이 disabled가 아닌 문제였다.
|
||
- GREEN focused: `npm run test:run -- src/features/community-posts/tests/community-contract.test.ts src/features/community-posts/tests/community-sheet.test.tsx`는 2 files / 16 tests passed였다. `npm run e2e:mock -- tests/e2e/community.spec.ts --project=chromium`은 Chromium 1 test passed로 mock Community audio `data:audio/wav` metadata load, `errorCode=null`, duration > 0을 확인했다.
|
||
- 파일 지도 정정: 실제 P6-T3 추가/수정 파일은 `src/features/community-posts/validation/community-post-media-policy.ts`, `src/features/community-posts/components/CommunityPostForm.tsx`, `src/features/community-posts/tests/community-form.test.tsx`, `src/features/community-posts/tests/community-test-support.ts`, `tests/e2e/community.spec.ts`, `src/shared/mocks/{audio-content-fixtures.ts,community-post-fixtures.ts,community-post-mock-store.ts}`, `playwright.config.ts`를 포함한다. `community-post-schema.ts`, `community-media-policy.ts`와 과거 잘못 기재된 Community E2E 파일명은 실제 파일명이 아니므로 위 파일 지도에서 정정했다.
|
||
- 범위 유지: Comments, Community detail GET/route, search, total/hasNext/fixedAtUtc, active filter, server-specific error key, P6-T4 mobile mutation capability는 구현하지 않았다.
|
||
|
||
**P6-T3 2차 review 최종 검증 기록 (2026-07-28):**
|
||
|
||
- 무엇을: 2차 보완 후 Community mock parser, playable audio fixture, Sheet pending state, test size, 문서 기록을 현재 작업트리 기준으로 재검증했다.
|
||
- 왜: mock mode 통과와 문서 기록만으로 server integration 또는 P6-T4 완료를 과장하지 않고, P6-T3 blocker만 닫혔는지 확인하기 위해서다.
|
||
- 자동 검증: `npm run test:run -- src/features/community-posts src/shared/validation`은 5 files / 31 tests passed였다. `npm run e2e:mock -- tests/e2e/community.spec.ts --project=chromium`은 1 passed로 Community mock audio metadata load를 확인했다. `npm run typecheck`, `npm run lint`, `npm run build`, `git diff --check`는 모두 exit 0이었다. build는 226 modules transformed와 `dist/assets/index-Dw4zhRsK.js` 429.63 kB(gzip 115.07 kB)를 기록했다.
|
||
- LSP/size: `src/features/community-posts` diagnostics는 9 TSX files / 0 diagnostics, `src/shared/mocks` diagnostics는 19 TS files / 0 diagnostics, `tests/e2e` diagnostics는 10 TS files / 0 diagnostics였다. 단일 `playwright.config.ts`, `CommunityPostForm.tsx`, `community-post-mock-store.ts`, `community-form.test.tsx` fresh diagnostics는 일부 3초 timeout됐지만 directory diagnostics, typecheck, lint로 보완했다. 순수 LOC는 `community-contract.test.ts 165`, `community-form.test.tsx 162`, `community-list.test.tsx 92`, `community-sheet.test.tsx 155`, `community-test-support.ts 180`, `tests/e2e/community.spec.ts 34`, `community-post-mock-store.ts 221`, `community-post-fixtures.ts 75`, `audio-content-fixtures.ts 118`, `CommunityPostSheet.tsx 148`, `CommunityPostForm.tsx 131`, `playwright.config.ts 29`로 모두 250 이하이다.
|
||
- Browser QA: 기존 mock 서버 `http://127.0.0.1:8889`에서 Playwright Chromium으로 login → `/ai-characters/101/community-posts` 이동 → `커뮤니티 게시글 7003 오디오 플레이어` 확인 → `audio[src^='data:audio/wav']` metadata load를 실제 실행했다. 결과는 `kind="loaded"`, `errorCode=null`, `duration=1`, `readyState=4`, `srcPrefix="data:audio/wav;base64,"`였다. Network는 Community list GET 2건만 200으로 기록됐고 Community detail GET은 없었다. Console warning/error는 기능과 무관한 `/favicon.ico` 404뿐이었다.
|
||
- Visual evidence: fresh screenshot `p6-t3-community-audio-list.png`를 저장했고 PNG signature는 `89504e470d0a1a0a`, size는 207392 bytes였다. `look_at` 확인 결과 Community 목록과 오디오 플레이어가 보이고, 재생 시간 `0:00 / 0:01`, 재생/위치/볼륨/속도 controls가 표시되며, 명백한 한국어 clipping이나 깨진 레이아웃은 없었다.
|
||
- 남은 항목: P6-T4 mobile/viewport/focus/axe capability와 P6-GATE server mode integration은 아직 완료 표시하지 않는다. active-only 반환과 pagination 종료 metadata는 기존 외부 의존으로 유지한다.
|
||
|
||
**P6-T3 final review close 기록 (2026-07-28):**
|
||
|
||
- RED: `npm run test:run -- src/features/community-posts/tests/community-form.test.tsx src/features/community-posts/tests/community-contract.test.ts src/features/community-posts/tests/community-sheet.test.tsx`는 3 files 중 3 tests failed였다. 실패는 crop dialog open 상태에서 `생성`이 disabled가 아님, raw multipart declared/body boundary mismatch가 200으로 통과, Sheet player 접근성 쿼리가 전역 중복 name에 취약한 문제였다.
|
||
- GREEN focused: 같은 명령은 3 files / 22 tests passed였다. `CommunityPostForm`은 `cropSource !== null` 동안 disabled와 submit guard를 유지하고 apply 시 image error를 정리한다. raw multipart fallback은 `Content-Type`의 declared boundary와 body delimiter line이 다르면 null 처리한다. Sheet test는 전역 `<audio>` count 대신 list 외부 player와 Sheet dialog player를 accessible name으로 확인한다.
|
||
- 최종 검증: `npm run test:run -- src/features/community-posts src/shared/validation`는 5 files / 31 tests passed, `npm run e2e:mock -- tests/e2e/community.spec.ts --project=chromium`은 1 passed, `npm run typecheck`, `npm run lint`, `npm run build`, `git diff --check`는 모두 exit 0이다. `rg -n "community-post\.spec" docs/20260725_AI캐릭터관리자웹/plan-task.md`는 출력이 없다.
|
||
|
||
**P6-T3 final blocker 재검증 기록 (2026-07-28):**
|
||
|
||
- 무엇을: 최종 review에서 확인된 Community image MIME/확장자 불일치, 비동기 image 준비 stale 결과, raw multipart fallback 종료 경계, image 준비 중 선택 취소 blocker를 회귀 test와 최소 수정으로 닫았다.
|
||
- 왜: `.gif + image/png`, `.png + image/gif`, `.jpg + image/png` 같은 불일치 조합이 Community GIF 무가공 경로를 깨뜨릴 수 있고, 느린 image 준비 Promise가 최신 선택/취소 상태를 덮거나 submit을 영구 disabled 상태로 남길 수 있으며, mock raw multipart parser가 잘못된 body를 수용하면 계약 test가 실제 multipart 경계를 충분히 보장하지 못하기 때문이다.
|
||
- RED: `npm run test:run -- src/features/community-posts/tests/community-form.test.tsx src/features/community-posts/tests/community-sheet.test.tsx src/features/community-posts/tests/community-contract.test.ts`는 3 files 중 4 tests failed였다. 실패는 image MIME/extension mismatch 허용, stale crop dialog 표시, raw multipart trailing data 허용이었다. 추가 RED `npm run test:run -- src/features/community-posts/tests/community-form.test.tsx`는 1 failed로 image 준비 중 `선택 취소` 후 `생성` button이 disabled로 남는 문제를 확인했다.
|
||
- GREEN: `validateCommunityPostImageFile`이 확장자별 MIME 조합을 확인하고, `CommunityPostForm`/`CommunityPostSheet`가 image selection token으로 stale Promise 결과를 무시하며, form의 `file === null` 경로가 pending 상태를 즉시 해제한다. `parseRawMultipart`는 body 시작과 closing boundary/trailing data를 검사한다.
|
||
- 최종 자동 검증: `npm run test:run -- src/features/community-posts/tests/community-form.test.tsx src/features/community-posts/tests/community-sheet.test.tsx src/features/community-posts/tests/community-contract.test.ts`는 3 files / 27 tests passed, `npm run test:run -- src/features/community-posts src/shared/validation`은 5 files / 36 tests passed, `npm run e2e:mock -- tests/e2e/community.spec.ts --project=chromium`은 1 passed였다. `npm run typecheck`, `npm run lint`, `npm run build`, `git diff --check`는 모두 exit 0이었다.
|
||
- Diagnostics/size: `src/features/community-posts`와 `src/shared/mocks/community-post-mock-store.ts` LSP diagnostics는 0건이다. 순수 LOC는 `CommunityPostForm.tsx 143`, `CommunityPostSheet.tsx 159`, `community-post-media-policy.ts 55`, `community-post-mock-store.ts 224`, `community-form.test.tsx 217`, `community-sheet.test.tsx 176`, `community-contract.test.ts 181`로 모두 250 이하이다.
|
||
- 재리뷰: 최종 blocker 재리뷰는 PASS/HIGH로, MIME/확장자 불일치, stale async image selection, raw multipart 경계, form pending image 취소 blocker가 모두 root-cause 위치에서 닫혔고 신규 blocker가 없다고 판정했다. Context 재검토도 PASS/HIGH로 P6-T4와 server integration이 미완료 범위로 유지됨을 확인했다.
|
||
|
||
**P6-T4 Community 반응형·접근성 capability 완료 기록 (2026-07-28):**
|
||
|
||
- 무엇을: Community에 Series와 같은 `(min-width: 768px)` 관리 capability를 적용해 모바일은 목록·Sheet·첨부 오디오 조회만 허용하고, 생성 route/form과 Sheet 수정·고정·비활성화 action은 차단했다. tablet/desktop은 기존 생성·Sheet 관리 흐름을 유지했다.
|
||
- 왜: PRD `9`와 `P6-T4` 범위가 모바일 등록·수정·고정·비활성화를 제외하고, mock UI 단계에서 focus trap/복귀·keyboard·320px·200% zoom·axe evidence를 Phase Gate 전에 독립적으로 고정해야 하기 때문이다.
|
||
- RED: `npm run e2e:mock -- tests/e2e/community.spec.ts --project=chromium`은 7 tests 중 3 failed였다. 실패는 320px 생성 링크 노출, 320px 생성 route가 안내 대신 form을 표시, Sheet Escape가 닫히지 않는 문제였다.
|
||
- GREEN: `useCommunityManagementCapability`가 `(min-width: 768px)` 기준으로 관리 가능 viewport를 계산하고, `CommunityPostListPage`는 생성 link와 Sheet `canMutate`를 `character.isActive && canManageCommunity`로 제한한다. `CommunityPostFormPage`는 모바일에서 생성 안내 `PageState`만 렌더링한다. `CommunityPostSheet`는 Escape close와 기존 `useModalFocus` trap/복귀를 함께 검증한다.
|
||
- Review blocker: code-quality review는 Sheet 내부 `ImageCropDialog`가 Escape를 부모 Sheet로 전파해 crop context를 잃을 수 있다고 REVISE/HIGH로 지적했다. 추가 RED `npm run test:run -- src/shared/ui/__tests__/image-crop-dialog.test.tsx`는 5 tests 중 1 failed로 `onCancel`이 0회 호출되는 문제를 확인했다. `ImageCropDialog`가 Escape에서 `preventDefault`, `stopPropagation`, `onCancel`을 실행하고 Tab은 기존 trap에 위임하도록 고쳤으며 같은 명령은 5 tests passed였다.
|
||
- 최종 자동 검증: `npm run test:run -- src/features/community-posts src/shared/ui/__tests__/image-crop-dialog.test.tsx`는 5 files / 34 tests passed, `npm run e2e:mock -- tests/e2e/community.spec.ts --project=chromium`은 7 passed였다. `npm run typecheck`, `npm run lint`, `npm run build`, `git diff --check`는 모두 exit 0이었다.
|
||
- Visual QA blocker: 1차 visual QA는 320px 모바일 목록의 `게시글 열기` 버튼 CJK 줄바꿈과 fullPage screenshot evidence 크기 불일치를 REVISE/HIGH로 지적했다. 추가 RED `npm run test:run -- src/features/community-posts/tests/community-list.test.tsx`는 긴 visible button text로 1 failed였고, `CommunityPostListItem`/`CommunityPostList`의 visible label을 `열기`로 줄이되 full `aria-label`을 유지하며 open button과 desktop status cell에 `whitespace-nowrap`을 적용했다. 수정 후 같은 명령은 2 tests passed였다.
|
||
- Diagnostics/review: LSP diagnostics는 `src/features/community-posts/lib/use-community-management-capability.ts`, `CommunityPostListPage.tsx`, `CommunityPostFormPage.tsx`, `CommunityPostSheet.tsx`, `CommunityPostList.tsx`, `CommunityPostListItem.tsx`, `tests/e2e/community.spec.ts`, `src/shared/ui/image-crop-dialog.tsx`, `src/shared/ui/__tests__/image-crop-dialog.test.tsx` 모두 0건이다. Spec review는 PASS/HIGH, blocker 재리뷰는 PASS/HIGH였고, visual QA 재리뷰 A/B도 viewport screenshot `p6-t4-community-*-rerun.png` 4개와 DOM evidence 기준 PASS/HIGH로 신규 blocker가 없다고 판정했다.
|
||
- 남은 항목: P6-GATE server mode integration은 아직 완료 표시하지 않는다. active-only 반환과 pagination 종료 metadata는 기존 외부 의존으로 유지한다.
|
||
|
||
**P6-GATE Community mock gate 부분 검증 기록 (2026-07-28):**
|
||
|
||
- mock/UI gate: `npm run test:run -- src/features/community-posts src/shared/validation`은 5 files / 36 tests passed였다. `npm run e2e:mock -- tests/e2e/community.spec.ts`는 24 passed / 4 skipped였다. Skipped 4건은 기존 정책과 동일하게 audio metadata decode를 Chromium 증거로 한정한 2건과 WebKit range input Tab focus 불안정으로 제외한 Sheet keyboard traversal 2건이다.
|
||
- WebKit focus test 조정: 전체 browser project 실행에서 WebKit/mobile-safari가 range slider `재생 위치`에 Tab focus를 안정적으로 주지 못해 `Community sheet traps focus closes on Escape and returns focus`가 실패했다. `tests/e2e/audio-content.spec.ts`의 WebKit keyboard path 제외 정책과 맞춰 Community Sheet keyboard traversal도 WebKit에서 skip하고, Chromium/mobile-chrome에서 focus trap·Escape close·trigger focus return을 유지 검증한다.
|
||
- server integration: `npm run e2e -- tests/e2e/community.spec.ts`는 Playwright server-mode `testMatch`와 file filter 교집합이 없어 `No tests found`로 종료했다. 따라서 P6-GATE는 mock UI journey만 부분 통과로 기록하고, 실제 server integration 완료는 주장하지 않는다.
|
||
- 최종 공통 검증: `npm run typecheck`, `npm run lint`, `npm run build`, `git diff --check`는 모두 exit 0이었다. P6-GATE 완료 표시는 backend 외부 의존(`EXT-007`~`EXT-008`)과 server-mode 대상 spec 정합성이 해결된 뒤 재개한다.
|
||
|
||
**P6-R2 Community 상태 boolean label 정리 완료 기록 (2026-07-30):**
|
||
|
||
- 무엇을: Community desktop row와 mobile card의 고정·댓글·성인 상태 표시를 Community 전용 helper로 통일하고, raw `성인 true/false` 노출을 unit·mock E2E에서 막았다.
|
||
- 왜: `REV-P6-002`가 지적한 성인 여부 raw boolean 표시를 PRD §13의 운영자용 한국어 label 기준으로 닫기 위해서다.
|
||
- RED: `npm run test:run -- src/features/community-posts`는 변경 직후 `일반 · 댓글 허용 · 일반 콘텐츠`를 찾지 못하고 기존 DOM에 `성인 false`가 남아 실패했다.
|
||
- GREEN/REFACTOR: `formatCommunityPostStatus`가 `고정/일반 · 댓글 허용/댓글 차단 · 성인 콘텐츠/일반 콘텐츠`를 반환하고 `CommunityPostList`/`CommunityPostListItem`이 이를 재사용한다. API DTO·multipart payload와 mock mutation shape는 변경하지 않았다.
|
||
- 검증: `npm run test:run -- src/features/community-posts`는 5 files / 42 tests passed, `npm run e2e:mock -- tests/e2e/community.spec.ts --project=chromium`은 7 passed, `npm run e2e:mock -- tests/e2e/community.spec.ts`는 24 passed / 4 skipped였다.
|
||
- 남은 항목: `P6-R2` 범위 없음. P6-GATE server mode integration은 기존 외부 의존·server-mode 대상 spec 정합성 항목으로 별도 유지한다.
|
||
|
||
**P7-T1 FanTalk 계약 확인 완료 기록 (2026-07-28):**
|
||
|
||
- 무엇을: OpenAPI의 FanTalk 제공 범위를 목록 GET과 답변 POST로 확정하고, 별도 상세·답변 수정·답변 상태 filter·sort·유일성 오류 계약은 `EXT-004` 외부 의존으로 유지했다.
|
||
- 계약 증거: OpenAPI path는 `/api/v2/admin/ai-characters/{characterId}/fan-talks` GET(`listAiCharacterFanTalks`)과 `/api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies` POST(`createAiCharacterFanTalkReply`) 2개뿐이다. schema는 `FanTalkCreatorReply`, `FanTalkListItem`, `FanTalkListResponse`, `FanTalkReplyCreateRequest`, `FanTalkReplyResponse`, `FanTalkListApiResponse`, `FanTalkReplyApiResponse`가 제공됐다.
|
||
- 구현 기준: 목록은 `page`, `size`, `fanTalkCount`, `fanTalks`, `hasNext`, item `creatorReplies[]`를 소비한다. 답변 생성 request는 `{ content }`만 보내고 성공 DTO의 `fanTalkId`, `replyId`, `creatorMemberId`, `content`, `createdAtUtc`를 반영한다.
|
||
- 제외 기준: `/fan-talks/:fanTalkId` 직접 route, 상세 GET, 답변 수정·삭제, answer filter/sort query, 중복 생성 오류 status/message key 분기는 만들지 않는다. mock fixture도 목록·답변 생성 범위만 제공하고 미제공 계약을 추정하지 않는다.
|
||
- 검증: OpenAPI JSON parse로 FanTalk path/schema를 출력했고, 예상 외 FanTalk path가 없음을 확인하는 Node 검사는 `hasUnexpected:false`로 exit 0이었다. `rg -n 'FANTALK-0(0[7-9]|1[0-1])'`는 PRD/plan의 외부 의존·확정 범위를 추적했다.
|
||
|
||
**P7-T2 FanTalk 목록·답변 생성 완료 기록 (2026-07-28):**
|
||
|
||
- 무엇을: FanTalk 목록 route/page/component, list GET API, one reply POST Sheet/form, 제공 계약 범위의 mock store/handler와 mock E2E를 구현하고 `P7-T2` 체크박스 9개를 완료로 표시했다.
|
||
- 왜: OpenAPI가 제공한 목록과 답변 1회 생성 범위만 UI로 연결하고, 상세·수정·삭제·filter/sort·중복 오류 key처럼 미제공 계약은 추정하지 않기 위해서다.
|
||
- RED: `npm run e2e:mock -- tests/e2e/fan-talk.spec.ts --project=chromium`은 최초 `No tests found`였고, spec/config 추가 후 `FanTalk 목록 조회 실패`와 mock response shape 오류로 실패해 FanTalk mock GET/POST 부재를 확인했다.
|
||
- GREEN: `src/shared/mocks/fan-talk-{fixtures,mock-store,handlers}.ts`를 추가하고 `CharacterMockStore`/`handlers.ts`에 list GET과 reply POST만 연결했다. E2E는 API request만 수집해 `page/size` 외 `status/sort`가 전송되지 않고, `GET /fan-talks/7001` 상세 요청이 0건이며, 미답변 → 답변 생성 → 읽기 전용 답변 표시가 유지됨을 확인한다.
|
||
- Focused tests: `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`는 3 files / 8 tests passed였다.
|
||
- Mock E2E: `npm run e2e:mock -- tests/e2e/fan-talk.spec.ts`는 Chromium, WebKit, Mobile Chrome, Mobile Safari 4 tests passed였다.
|
||
- Static/build: `npm run typecheck`, `npm run lint`, `npm run build`, `git diff --check`는 모두 exit 0이었다. build는 235 modules transformed와 `dist/assets/index-BiTOuTXK.js` 441.40 kB(gzip 116.90 kB)를 기록했다.
|
||
- Review: 1차 독립 리뷰는 E2E가 query 없는 `GET /fan-talks/7001` 상세 요청을 놓칠 수 있다고 FAIL/HIGH로 지적했다. E2E request 수집을 `{ method, path, search }`로 바꿔 exact detail GET 0건을 검증하도록 수정했고, 위 focused/mock/static 검증을 새로 실행했다.
|
||
- OQ-009: 실제 FanTalk reply form은 짧은 단일 textarea와 저장 버튼만 제공한다. `content` 권고안은 2,000자이며 backend 호환 확인 전까지 client 상한은 구현하지 않았다.
|
||
- 남은 항목: FanTalk 상세, 답변 수정·삭제, 전체 결과 filter/sort, 중복 생성 오류 status/message key는 `EXT-004` 외부 계약 제공 전까지 구현하지 않는다. P7-GATE server integration은 아직 완료 표시하지 않는다.
|
||
|
||
**P7-T3 FanTalk 반응형·접근성 capability 완료 기록 (2026-07-28):**
|
||
|
||
- 무엇을: `tests/e2e/fan-talk.spec.ts`에 320/768/1280 viewport의 목록 조회·답변 작성, edit/delete action 부재, 320px keyboard viewport, keyboard-only Sheet create/focus return, 200% zoom, axe critical·serious 0건 검증을 추가하고 `P7-T3` 체크박스 3개를 완료로 표시했다.
|
||
- 왜: P7-T2의 제공 계약 범위인 목록 GET과 답변 POST capability가 viewport별로 축소되지 않고, 미제공 상세·수정·삭제·filter/sort 기능을 UI나 network에 섞지 않기 위해서다.
|
||
- 검증: `npm run e2e:mock -- tests/e2e/fan-talk.spec.ts`는 26 passed / 2 skipped였다. skipped 2건은 WebKit 계열의 button Tab focus 불안정으로 keyboard-only path를 Chromium/mobile-chrome 증거로 한정한 것이다. `npm run typecheck`와 `npm run lint`는 exit 0이었다. 단일 `tests/e2e/fan-talk.spec.ts` LSP diagnostics는 3초 timeout됐지만 typecheck/lint로 보완했다.
|
||
- Review: 독립 리뷰는 차단 findings 없음으로, P7-T3 요구사항 충족과 P7-T2 범위 보존을 확인했다. 잔여 리스크는 WebKit keyboard-only skip, CSS `zoom` 기반 200% 근사, 실제 모바일 soft keyboard가 아닌 320x480 viewport 검증이다.
|
||
- 남은 항목: P7-GATE server mode integration은 아직 완료 표시하지 않는다. FanTalk 상세, 답변 수정·삭제, 전체 결과 filter/sort, 중복 생성 오류 status/message key는 `EXT-004` 외부 계약 제공 전까지 구현하지 않는다.
|
||
|
||
**P7-GATE FanTalk mock gate 부분 검증 기록 (2026-07-28):**
|
||
|
||
- mock/unit gate: `npm run test:run -- src/features/fan-talks`는 3 files / 8 tests passed였다. `npm run e2e:mock -- tests/e2e/fan-talk.spec.ts`는 26 passed / 2 skipped였다. Skipped 2건은 WebKit 계열 button Tab focus 불안정으로 keyboard-only path를 Chromium/mobile-chrome 증거로 한정한 것이다.
|
||
- server integration: `npm run e2e -- tests/e2e/fan-talk.spec.ts`는 Playwright server-mode `testMatch`와 file filter 교집합이 없어 `No tests found`로 종료했다. 따라서 P7-GATE는 mock UI journey만 부분 통과로 기록하고, 실제 server integration 완료는 주장하지 않는다.
|
||
- 최종 공통 검증: `npm run typecheck`, `npm run lint`, `npm run build`, `git diff --check -- docs/20260725_AI캐릭터관리자웹/plan-task.md`는 모두 exit 0이었다. P7-GATE 완료 표시는 server-mode 대상 spec 정합성과 실제 개발 API FanTalk 목록·답변 생성 검증이 해결된 뒤 재개한다.
|
||
- 남은 항목: FanTalk 상세, 답변 수정·삭제, 전체 결과 filter/sort, 중복 생성 오류 status/message key는 `EXT-004` 외부 계약 제공 전까지 구현하지 않는다.
|
||
|
||
**P7-R1 FanTalk 날짜·성공 문구 현지화 완료 기록 (2026-07-30):**
|
||
|
||
- 무엇을: FanTalk desktop list, mobile card, Sheet 원문, 등록된 답변 날짜 표시를 `formatSeoulDateTime`으로 통일하고, 답변 등록/수정 성공 status를 `답변이 등록되었습니다.`, `답변이 수정되었습니다.`로 바꿨다. API DTO의 ID와 UTC field는 contract/model mapping에 그대로 둔다.
|
||
- RED: `npm run test:run -- src/features/fan-talks`는 raw UTC와 내부 ID success copy 기대 변경 후 3 files 중 2 failed / 3 failed tests로 실패했다. `npm run e2e:mock -- tests/e2e/fan-talk.spec.ts --project=chromium`은 목록 raw UTC 표시 assertion에서 1 failed / 6 passed로 실패했다.
|
||
- GREEN/REFACTOR: `FanTalkList.tsx`, `FanTalkListItem.tsx`, `FanTalkReplySheet.tsx`가 기존 디자인 token/class 구조를 유지한 채 공통 formatter를 사용한다. 생성/수정 success status는 action-centered Korean copy만 표시하고, 반환 reply content/date는 Sheet 본문에만 표시한다.
|
||
- Focused unit: `npm run test:run -- src/features/fan-talks`는 3 files / 13 tests passed였다.
|
||
- Mock E2E: `npm run e2e:mock -- tests/e2e/fan-talk.spec.ts --project=chromium`은 7 passed였고, 전체 `npm run e2e:mock -- tests/e2e/fan-talk.spec.ts`는 26 passed / 2 skipped였다. skipped 2건은 기존 WebKit 계열 keyboard-only Tab focus 제외 정책이다.
|
||
- Static/diagnostics: `src/features/fan-talks`와 `tests/e2e` LSP diagnostics는 오류 0건, `npm run typecheck`는 exit 0, targeted `git diff --check`는 no output이었다.
|
||
- 수동 확인 대체: mock E2E가 desktop/mobile/card/Sheet/keyboard/200% zoom/axe 경로를 실제 브라우저로 실행해 raw UTC와 내부 ID 노출 0건, reply ID mapping 회귀 0건을 확인했다.
|
||
|
||
**P8-T1 Comments 계약 부재 확인 및 Phase 제외 기록 (2026-07-28):**
|
||
|
||
- 무엇을: OpenAPI에서 댓글 CRUD path가 0건이고 summary schema `AudioContentComment`, `CommunityPostComment`만 제공됨을 확인해 `P8-T1` 체크박스 5개를 완료로 표시했다. `P8-T2`~`P8-T4`와 `P8-GATE`는 계약 제공 전까지 구현·완료 표시하지 않는다.
|
||
- 계약 증거: OpenAPI의 comment 관련 path는 `[]`였고, comment 관련 schema는 `AudioContentComment`, `CommunityPostComment`뿐이다. Audio 상세는 `commentList`/`commentCount`, Community 목록 item은 `firstComment`/`commentCount` 읽기용 요약만 포함한다.
|
||
- 제외 기준: Audio·Community 댓글 목록, 작성, 수정, soft delete endpoint/query/request/response DTO가 없다. root/direct reply 2단계 server rule과 fan 댓글 삭제 권한 오류 status/message key도 없다. 따라서 endpoint 이름 추정, client-only permission 완료 주장, mock DTO·browser fixture 생성은 하지 않는다.
|
||
- Screen inventory: Audio detail과 Community Sheet는 현재 summary 표시 또는 댓글 진입점 대기 상태만 가질 수 있다. 계약 제공 후 host Page/Sheet는 target 식별자와 query/cache 보존만 소유하고, Comments component가 2단계 thread·작성자별 action·권한 오류를 소유하며, form/dialog/live region은 Phase 1 shared contract를 재사용한다.
|
||
- 검증: OpenAPI JSON parse와 comment path/schema 출력은 exit 0으로 `commentPaths: []`를 확인했다. `rg -n "COMMENT-00[1-6]|댓글 CRUD|EXT-005|commentList|firstComment" docs/20260725_AI캐릭터관리자웹/prd.md docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/api-contract.openapi.json`는 PRD/plan/OpenAPI의 외부 의존과 summary field 추적을 확인했다.
|
||
- 남은 항목: `EXT-005`가 Audio·Community 댓글 CRUD와 fan 댓글 삭제 권한 오류 계약을 제공하면 `P8-T2`부터 재개한다. 계약 전에는 `src/features/comments`, `tests/e2e/comments.spec.ts`, Comments mock handler/fixture를 만들지 않는다.
|
||
|
||
**P8-GATE Comments 제외/대기 기록 (2026-07-28):**
|
||
|
||
- 판정: 핵심 댓글 CRUD 계약이 미제공이므로 Phase 8은 mock UI journey와 server integration을 실행하지 않고 제외/대기 상태로 둔다.
|
||
- 이유: Gate expected가 요구하는 Audio·Community 두 진입점의 2단계 댓글 CRUD·권한·모바일 최종 UI는 backend list/create/update/soft delete 계약과 권한 오류 계약이 있어야 검증할 수 있다. 현재 OpenAPI에는 summary DTO만 있어 mock fixture를 만들면 추정 구현이 된다.
|
||
- 남은 항목: `EXT-005` 제공 후 `npm run test:run -- src/features/comments`, `npm run e2e:mock -- tests/e2e/comments.spec.ts`, `npm run e2e -- tests/e2e/comments.spec.ts`, `npm run typecheck`, `npm run lint`, `npm run build`를 fresh 실행해 P8-GATE를 재판정한다.
|
||
|
||
**P9-T1 교차 journey·오류 회귀 완료 기록 (2026-07-28):**
|
||
|
||
- 무엇을: 활성 제공 범위만 대상으로 `tests/e2e/resource-workflows.spec.ts`와 `tests/e2e/error-mapping.spec.ts`를 추가해 교차 mock journey, mock 오류 message/security, 금지 detail/filter/sort request, 비활성 캐릭터 하위 mutation 차단, server no-fallback 회귀를 고정했다. Comments CRUD는 `EXT-005` 미제공으로 제외 상태를 유지했다.
|
||
- 왜: Phase 9는 미제공 계약을 포함한 전체 완료가 아니라, Phase 0~8에서 제공되거나 명시 제외된 활성 범위의 교차 회귀 증거만 남겨야 하기 때문이다.
|
||
- RED: 최초 `npm run e2e:mock -- tests/e2e/resource-workflows.spec.ts tests/e2e/error-mapping.spec.ts`는 `No tests found`로 시작했고, spec/config 추가 후 16 tests 중 resource journey, inactive FanTalk reply action, mock API error probe가 실패했다. 원인은 새 P9 spec 부재, 비활성 FanTalk 답변 affordance 노출, WebKit 계열 service worker controller 타이밍, 커뮤니티 저장 성공 UI 과잉 기대, 모바일에서 desktop-only Series mutation journey 실행이었다.
|
||
- GREEN: mock testMatch에 P9 spec을 추가하고, FanTalk 목록/card가 `character.isActive=false`일 때 `답변하기` button 대신 `읽기 전용`만 노출하도록 수정했다. `error-mapping.spec.ts`는 mock worker active와 controller를 기다린 뒤 공통 오류 fixture를 직접 probe한다. `resource-workflows.spec.ts`는 desktop-only mutation journey를 desktop project로 제한하고, mobile projects는 inactive mutation 차단 회귀만 유지한다.
|
||
- Focused 검증: `npm run test:run -- src/features/fan-talks`는 3 files / 9 tests passed였다. `npm run e2e:mock -- tests/e2e/resource-workflows.spec.ts tests/e2e/error-mapping.spec.ts`는 14 passed / 2 skipped였다. skipped 2건은 desktop-only mutation journey를 mobile-chrome/mobile-safari에서 제외한 것이다.
|
||
- 회귀/계약 검증: `npm run test:run -- src/features`는 26 files / 112 tests passed로 도메인 serializer, soft delete, media error, mock handler fixture 회귀를 재검증했다. `npm run e2e -- tests/e2e/server-mode-boundary.spec.ts`는 16 passed로 server mode 404/network error가 browser MSW/mock으로 fallback되지 않음을 확인했다.
|
||
- Static/build: `npm run typecheck`, `npm run lint`, `npm run build`, `git diff --check`는 모두 exit 0이었다. build는 235 modules transformed, `dist/assets/index-CQH-_zZD.js` 441.58 kB(gzip 117.05 kB)를 기록했다.
|
||
- Diagnostics: `FanTalkListItem.tsx`, `FanTalkListPage.tsx`, `fan-talk-list.test.tsx`는 LSP diagnostics 0건이었다. `FanTalkList.tsx`, `resource-workflows.spec.ts`, `error-mapping.spec.ts` 단일 fresh diagnostics는 반복 3초 timeout됐지만 `npm run typecheck`와 `npm run lint` 통과로 보완했다.
|
||
- 남은 항목: P9-T3 문서·품질 Gate는 아직 시작하지 않았다. P4/P5/P6/P7 server integration Gate 미완료와 P8 Comments 제외/대기는 기존 외부 의존·server-mode testMatch 상태로 유지한다.
|
||
|
||
**P9-T2 반응형·접근성·보안 회귀 완료 기록 (2026-07-28):**
|
||
|
||
- 무엇을: `tests/e2e/responsive-capabilities.spec.ts`와 `tests/e2e/accessibility.spec.ts`를 추가해 mock shell viewport/capability, 핵심 route axe, skip link/main focus, dialog focus return, reduced motion, bright theme, theme toggle 부재, 민감정보 비저장 smoke를 검증했다.
|
||
- 왜: Phase 9는 새 기능 추가가 아니라 활성 제공 범위가 PRD `9`, 접근성, 보안 저장소 기준을 계속 만족한다는 최신 교차 증거를 남겨야 하기 때문이다.
|
||
- RED: 최초 `npm run e2e:mock -- tests/e2e/responsive-capabilities.spec.ts tests/e2e/accessibility.spec.ts`는 `No tests found`로 시작했다. spec/config 추가 뒤에는 skip link focus, main focus, mobile theme-toggle absence assertion이 순차 실패해 테스트가 실제 회귀를 잡는 것을 확인했다.
|
||
- GREEN: mock testMatch에 P9-T2 spec을 추가하고, `ProtectedAdminShell`의 skip link가 `main`에 실제 focus를 넘기도록 `mainRef`와 `tabIndex={-1}`를 추가했다. keyboard traversal smoke는 desktop Chromium 증거로 한정하고, 없는 theme toggle은 `toHaveCount(0)`로 검증했다.
|
||
- Focused 검증: `npm run e2e:mock -- tests/e2e/responsive-capabilities.spec.ts tests/e2e/accessibility.spec.ts`는 17 passed / 3 skipped였다. skipped 3건은 keyboard focus traversal을 mobile Chrome, WebKit, mobile Safari에서 제외한 desktop Chromium 전용 증거다.
|
||
- 회귀/품질 검증: `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts`는 1 file / 3 tests passed였다. `npm run typecheck`, `npm run lint`, `npm run build`, `git diff --check`는 모두 exit 0이었다. build는 235 modules transformed, `dist/assets/index-BFpSc9vx.js` 441.68 kB(gzip 117.07 kB)를 기록했다.
|
||
- Diagnostics: `responsive-capabilities.spec.ts`, `playwright.config.ts`, `src/shared/mocks/__tests__/mode-boundary.test.ts`는 LSP diagnostics 0건이었다. `accessibility.spec.ts`와 `ProtectedAdminShell.tsx` 단일 fresh diagnostics는 3초 timeout됐지만 `npm run typecheck`와 `npm run lint` 통과로 보완했다.
|
||
- 수동 범위: 현재 자동 검증은 Playwright 설정의 Chromium/mobile Chrome project와 mock mode 기준이다. Edge/Safari/WebKit 실기기 인증은 현재 지원 범위에서 제외한다.
|
||
|
||
**P9-T3 문서·품질 Gate 진행 기록 (2026-07-28):**
|
||
|
||
- 무엇을: README에 지원 브라우저와 알려진 backend 제약을 추가하고, PRD의 `OQ-010` 감사 로그 조회 UI를 현재 릴리스 제외로 정렬했다. `P9-T3` 체크리스트 중 별도 code review와 mock/server 상태 정합성 항목을 제외한 문서 정합성 항목을 완료로 표시했다.
|
||
- 왜: P9-T1/T2의 활성 범위 검증 결과와 `EXT-001~011`, OQ-009, mock/server 완료 상태가 README·PRD·plan에서 같은 의미로 읽혀야 하기 때문이다.
|
||
- 어떻게: `python3 .opencode/skills/ui-ux-pro-max/scripts/search.py "animation accessibility z-index loading" --domain ux -n 12`는 12 results를 반환했고, loading feedback, z-index scale, lazy loading, reduced motion, loading button, 150-300ms motion 항목을 확인했다. `rg -n "OQ-010 \\| 제외|감사 로그 조회 UI는 현재 릴리스에서 제외|Known Backend Constraints|Browser Support|Edge와 실제 기기|EXT-00[1-7]|EXT-008|EXT-009|EXT-010|EXT-011|UI 확인 완료\\(mock\\)|실제 서버 연동 완료\\(server\\)" README.md docs/20260725_AI캐릭터관리자웹/prd.md docs/20260725_AI캐릭터관리자웹/plan-task.md`는 README/PRD/plan 추적 문자열을 확인했다. `git diff --check -- README.md docs/20260725_AI캐릭터관리자웹/prd.md docs/20260725_AI캐릭터관리자웹/plan-task.md`는 exit 0이었다.
|
||
- Markdown link 확인: `rg -n "\\[[^\\]]+\\]\\([^\\)]+\\)" README.md docs/20260725_AI캐릭터관리자웹/prd.md docs/20260725_AI캐릭터관리자웹/plan-task.md`로 문서 내 link 목록을 확인했고, 새로 추가한 README 문구에는 신규 link가 없다.
|
||
- Code review: 1차 Oracle review는 OQ-010 섹션 불일치, README backend 제약 누락, P9-T2 browser support 과완료, PRD 수용 기준별 증거 연결 부족을 지적했다. PRD 감사 로그 섹션, README Known Backend Constraints, P9-T2 browser support checklist, P9 수용 기준 증거 요약 표를 수정한 뒤 재검토에서 차단 findings 없음으로 확인했다. 남은 minor는 진행 기록의 제외 항목 문구였고 본 기록에서 정정했다. `P9-GATE`는 별도 Gate로 남겨 아직 실행하지 않았다.
|
||
- Mock/server 상태: `P9 수용 기준 증거 요약`과 README Known Backend Constraints를 기준으로 Character/Audio/Series/Community/FanTalk/Comments의 mock UI 완료, server integration 대기, 외부 의존 상태를 분리해 확인했다. P4/P5/P6/P7 server integration Gate 미완료와 P8 Comments 제외/대기는 완료로 표시하지 않는다.
|
||
- 남은 항목: P9-GATE는 별도 실행한다.
|
||
|
||
**P9-GATE 최종 검증 기록 (2026-07-28):**
|
||
|
||
- 무엇을: 활성 릴리스 범위의 install, typecheck, lint, unit, mock/server E2E, build, 금지 패턴을 최종 Gate로 실행했다. 외부 계약 대기 항목과 server-mode 도메인 integration 미제공 범위는 완료로 승격하지 않았다.
|
||
- 어떻게: `npm ci`는 307 packages added, 0 vulnerabilities였다. `npm run typecheck`, `npm run lint`, `npm run build`, `git diff --check`는 모두 exit 0이었다. 최신 build는 235 modules transformed, `dist/assets/index-LRzXvNqH.css`, `dist/assets/index-Bxe7R7m6.js`를 생성했다.
|
||
- 자동 검증: `npm run test:run`은 61 files / 247 tests passed였다. `npm run e2e:mock`은 최종 재실행에서 188 passed / 20 skipped였다. `npm run e2e`는 exit 0이었고 server allowlist 36 tests가 실패 없이 종료됐다.
|
||
- 1차 mock E2E 실패와 수정: 320px viewport + 200% zoom overflow는 mobile header breadcrumb가 폭을 밀어서 발생했고, `ProtectedAdminShell` breadcrumb를 small breakpoint 이상에서만 표시하도록 수정했다. mock shell 중복 GET 검증 실패는 React dev `StrictMode`의 effect 재실행으로 허용 API GET이 중복 기록된 것이며, request assertion을 중복 제거한 contract set 기준으로 바꿨다.
|
||
- 수정 후 focused 검증: `npm run e2e:mock -- tests/e2e/mock-preview-shell.spec.ts --project=chromium`은 6 passed, `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts --project=chromium`은 11 passed, `npm run e2e:mock -- tests/e2e/audio-content.spec.ts --project=chromium`은 5 passed였다.
|
||
- 금지 패턴 확인: `rg -n 'TODO|TBD|FIXME' src tests`와 `rg -n "externalCharacterId|SUNDAY|MONDAY|TUESDAY|WEDNESDAY|THURSDAY|FRIDAY|SATURDAY|state.?[:=].?['\"]OPEN" src --glob '!**/*.test.*' --glob '!**/*.spec.*' --glob '!**/__tests__/**'`는 모두 match 0건이었다.
|
||
- 판정: P9-GATE 명령군은 최종 실행 기준 0 failure/0 error다. P4/P5/P6/P7 server integration Gate 미완료와 P8 Comments 제외/대기는 `EXT-004~005`, `EXT-007~008` 및 server-mode testMatch 제약으로 계속 추적한다. `EXT-001~002`는 legacy 후보 확인, `EXT-009`는 `99999`, `EXT-010`은 파일 용량·MIME backend 동일 검증, `EXT-011`은 미정의 오류 기본 문구로 보정됐다.
|
||
|
||
**P9-GATE Oracle 미검증 정정 기록 (2026-07-28):**
|
||
|
||
- 무엇을: Oracle이 원래 작업 `ulw mode requested while executing plan-task.md Phase 3-9` 완료 여부를 비판적으로 검토했고 `NOT VERIFIED`로 판정했다.
|
||
- 왜: P9-GATE 기록은 존재하지만 `P4/P5/P6/P7-GATE`의 server integration 미완료, P8 Comments 제외/대기, `playwright.config.ts` server allowlist의 도메인 spec 제외, 구현 완료 정의 미체크 상태가 남아 있어 Phase 3~9 전체 완료로 검증할 수 없기 때문이다.
|
||
- 어떻게: P9 완료 조건의 `mock/server 전체 자동 Gate` 표현을 `활성 mock UI와 server allowlist 자동 Gate`로 낮추고, 도메인별 server integration은 backend 계약과 server-mode allowlist 충족 후 별도 완료한다고 정정했다. 현재 판정은 `활성 mock UI + P9 자동 Gate 통과, 도메인 server integration 미완료`다.
|
||
|
||
**EXT-003 Series 수정 계약 정정 기록 (2026-07-28):**
|
||
|
||
- 무엇을: `EXT-003`을 별도 backend 계약 항목에서 제외하고 필요 없음으로 정정했다.
|
||
- 왜: 시리즈 수정 화면은 상세 API가 `SeriesListItem`과 같은 `genreId`, enum `publishedDaysOfWeek`, enum `state`를 내려주면 기존 선택값을 안전하게 초기화할 수 있고, 장르 API는 option 목록 표시용으로만 호출하면 되기 때문이다.
|
||
- 어떻게: PRD `SERIES-017`과 `11.5 EXT-003`을 필요 없음으로 정정하고, Phase 5의 외부 의존·Gate·P9 요약에서 `EXT-003`을 제외하도록 갱신했다. 과거 검증 기록의 관련 언급은 이 정정 기록 이후 최신 판정으로 사용하지 않는다.
|
||
|
||
**OpenAPI 2.3.0 계약 보완 및 Phase 10 계획 — 2026-07-29:**
|
||
|
||
- 무엇을: PRD의 `EXT-001`, `EXT-002`, `EXT-005`, `EXT-007`, `EXT-008`을 해결로, `EXT-004`를 부분 해결로 정정했다. Audio 예약 공개 UTC 전송과 `timezone` 제거, v2 원작·장르 lookup, Community pagination, FanTalk 답변 수정·팬 원글 삭제, Audio·Community Comments 계약을 요구사항·수용 기준·endpoint 표에 반영하고 Phase 10의 `P10-T1~P10-T6`, `P10-GATE`를 추가했다.
|
||
- 왜: OpenAPI 2.3.0이 후속 operation/schema를 제공했고, 사용자 확인으로 FanTalk PUT의 `replyId=creatorReplies[].fanTalkId`, 댓글 작성자 판정 `writerId===creatorId`, 팬 작성 FanTalk 원글 DELETE 포함 범위가 확정됐기 때문이다. 완료된 Phase 3~9의 과거 증거는 덮어쓰지 않고 최신 구현을 별도 vertical slice로 추적해야 한다.
|
||
- 어떻게:
|
||
- OpenAPI assertion — `jq -e`로 version 2.3.0, 원작·장르·FanTalk DELETE `implemented`, FanTalk reply PUT `planned`, Audio `releaseDate.pattern=Z$`, Audio create의 `timezone` 부재, Community pagination required field, 댓글 operation 10개를 확인해 exit 0이었다.
|
||
- 구조 검증 — Node 검사에서 PRD 요구사항 ID 156개, 중복 0개였고 `P10-T1~P10-T6`, `P10-GATE`가 모두 계획 내 2회 이상 추적됐다.
|
||
- 링크 검증 — PRD·plan의 local Markdown link를 파일 시스템과 대조해 broken link 0건이었다.
|
||
- 문구 검증 — 해결된 EXT 6개 상태, `creatorReplies[].fanTalkId`, `writerId === creatorId`, UTC `Z`, Community pagination contract를 `rg`로 확인했다. Phase 10의 `TODO|TBD|FIXME|implement later|위와 동일|적절히 처리`는 0건이었다.
|
||
- 문서 검증 — `git diff --check -- docs/20260725_AI캐릭터관리자웹/prd.md docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/api-contract.openapi.json`은 exit 0이었다.
|
||
- 정정: 2026-07-28 P9 기록의 `EXT-001~002` legacy 후보, `EXT-004~005`, `EXT-007~008` 대기 판정은 당시 이력으로만 보존한다. 2026-07-29 이후 현재 판정은 PRD §11.5와 Phase 10을 우선한다.
|
||
- 남은 항목: 애플리케이션 코드는 이번 문서 보완 범위에서 변경하지 않았다. Phase 10 구현과 FanTalk reply PUT의 실제 server `implemented` 전환 검증이 남아 있다.
|
||
|
||
**FanTalk PUT 상태·Phase 10 계약 완전성 정정 — 2026-07-29:**
|
||
|
||
- 무엇을: 백엔드에서 FanTalk 답변 PUT이 이미 구현됐다는 사용자 확인에 따라 OpenAPI operation을 `implemented`로 정정했다. PRD의 대기 문구를 제거하고 `P10-T5`의 mock/client/server integration을 모두 활성화했다. 전체 계약 재대조에서 누락된 가격 `0..99999`, 미정의 오류 공통 문구, 기존 파일 client 검증과 backend 확인 경계를 `P10-T7`로 추가했으며 댓글 조회의 `page`·`size`와 target별 create request, FanTalk PUT 성공 response 처리도 Task interface와 RED에 명시했다.
|
||
- 왜: 이전 Phase 10은 `EXT-001~005`, `EXT-007~008` 중심으로만 작성돼 `EXT-009~011`의 실제 코드 변경·회귀 완료 조건이 없었고, 구현된 FanTalk PUT을 `planned`로 취급해 server integration을 불필요하게 대기시키고 있었기 때문이다.
|
||
- 어떻게:
|
||
- OpenAPI 검증 — `jq -e`로 version `2.3.0`, 전체 37개 operation의 `x-implementation-status=implemented`, FanTalk PUT `implemented`, Audio create/update·Community create price의 `minimum=0`, `maximum=99999`, Audio UTC `Z`와 no-timezone, Community pagination required field를 확인해 exit 0이었다.
|
||
- 요구사항·계획 구조 — Node 검사에서 확정·권고 요구사항 138개, 중복 0개, `P10-T1~P10-T7`과 `P10-GATE` 각 2회 이상 추적을 확인했다. 과거 판단을 보존하는 Phase 10 Decision Log 이전의 실행 Task·Gate 범위에서는 placeholder·오래된 `planned` 상태가 0건이었다.
|
||
- 문서 연결 — README·PRD·plan의 local Markdown link를 검사해 broken link 0건을 확인했다.
|
||
- 형식 — `git diff --check -- README.md docs/20260725_AI캐릭터관리자웹/prd.md docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/api-contract.openapi.json`은 exit 0이었다.
|
||
- 정정: 바로 앞 `OpenAPI 2.3.0 계약 보완 및 Phase 10 계획` 기록의 FanTalk PUT `planned`, `P10-T1~P10-T6`, PUT server 전환 대기 판정은 당시 문서 상태의 검증 이력으로만 보존한다. 현재 실행 기준은 OpenAPI의 PUT `implemented`와 `P10-T1~P10-T7`, `P10-GATE`다.
|
||
- 남은 항목: 애플리케이션 코드는 아직 변경하지 않았다. 현재 확정 요구사항과 제공 operation은 Phase 10 계획으로 구현 가능하다. 계약이 없는 FanTalk 별도 상세/filter/sort·중복 오류 key와 OpenAPI 미포함 인증 정식화는 현재 구현 범위 밖의 비차단 추적 항목이다.
|
||
|
||
**EXT-004 종결·Phase 5/8 실행 계획 보완 — 2026-07-29:**
|
||
|
||
- 무엇을: 현재 FanTalk UI에 필요하지 않은 별도 상세 GET·답변 상태 filter·sort와 중복 오류 전용 key를 제품 범위에서 제외하고 `EXT-004`를 해결로 종결했다. Phase 5의 미구현 Series CRUD가 image 계약까지 빠짐없이 P10-T3에서 실행되도록 schema·policy·TDD·검증 항목을 보강하고, Phase 8의 미구현 Comments는 추가 계약 대기 없이 P10-T6에서 구현하도록 최신 판정을 명시했다. Series·FanTalk·Comments의 server E2E spec이 현재 allowlist에서 실행되지 않는 문제도 각 Task와 P10-GATE의 실행 조건에 반영했다.
|
||
- 왜: 목록 item 기반 FanTalk Sheet는 상세 endpoint·filter·sort가 필요 없고, 정확한 중복 오류 key도 client 동작에 사용하지 않는다. 반면 답변 1개 불변식은 UI가 아닌 backend 수용 기준으로 검증해야 한다. Series는 CRUD operation만으로 구현 가능하지만 기존 P10-T3가 생성 필수·수정 optional image와 `FILE-005`, `FILE-015`를 Files/RED에 승계하지 않았고, 현재 `playwright.config.ts`의 server `testMatch`는 세 도메인 spec을 선택하지 않아 `npm run e2e -- <domain spec>`이 완료 증거가 될 수 없었다.
|
||
- 어떻게:
|
||
- OpenAPI parse — Series·Comments·FanTalk 관련 24개 operation을 검사해 모두 `x-implementation-status=implemented`, Comments operation은 정확히 10개임을 확인했다.
|
||
- PRD 구조 — 일반 요구사항 145개, ID 중복 0개, 허용되지 않은 상태 0개를 확인했다. `EXT-004=해결`, `FANTALK-002=제외`, `FANTALK-007=확정`, `FANTALK-008=제외` assertion도 통과했다.
|
||
- Phase 10 추적 — `SERIES-001~018`, `COMMENT-001~008`, `series-image-policy.ts`, Series·FanTalk·Comments Chromium server focused 명령과 `EXT-004` 해결 결정 7개 assertion에 누락이 없었다. Phase 10의 `TODO|TBD|FIXME|implement later|위와 동일|적절히 처리`는 match 0건이었다.
|
||
- 문서 연결 — PRD·plan의 local Markdown link 38개를 파일 시스템과 대조해 broken link 0건을 확인했다.
|
||
- 정정: 바로 앞 기록의 “계약이 없는 FanTalk 별도 상세/filter/sort·중복 오류 key”는 당시 판정으로 보존한다. 현재 판정은 계약 대기가 아니라 제품 범위 제외이며 `EXT-004`는 해결이다. 2026-07-28의 P5·P8 대기 체크박스와 검증 기록은 당시 이력으로 유지하고, 현재 구현은 P10-T3·P10-T6에서 진행한다.
|
||
- 남은 항목: 애플리케이션 코드와 `playwright.config.ts`는 아직 변경하지 않았다. client/mock 구현은 추가 backend 계약 없이 시작할 수 있다. 실제 server 완료에는 전용 개발 API 계정·재사용 가능한 fixture와 server allowlist 변경이 필요하며, 준비되지 않으면 mock 완료와 분리해 server integration 대기로 기록한다.
|
||
|
||
**Phase 3/5/8 체크박스 정합성 정정 — 2026-07-29:**
|
||
|
||
- 무엇을: 과거 Phase 3의 legacy 원작 lookup 항목, Phase 5 `P5-T3`·`P5-T5`의 Series CRUD/form/accessibility 항목, Phase 8 `P8-T2`~`P8-T4`의 Comments thread/permission/accessibility 항목을 최신 Phase 10 구현 기준으로 체크 표시했다. §6 구현 완료 정의는 최종 Gate 체크리스트로 분리해 미체크 상태를 유지했다.
|
||
- 왜: OpenAPI 2.3.0 후속 구현이 `P10-T1`, `P10-T3`, `P10-T6`에서 완료됐지만 과거 Phase 체크박스가 남아 있어 실제 구현 상태를 문서에서 구분하기 어려웠기 때문이다. legacy 원작 lookup은 구현하지 않고 v2 `searchAiCharacterOriginalWorks`로 대체했으며, Phase 8의 과거 계약 부재 상태는 이력으로만 보존한다.
|
||
- 어떻게:
|
||
- 구현 대조: codegraph로 `SeriesForm`, `series-contract.test.ts`, `CommentThread`, `comment-contract.test.ts`, `comment-thread.test.tsx`, `comments.spec.ts`를 확인해 Series v2 장르·CRUD·image policy와 Audio/Community 2단계 댓글·작성자별 action이 존재함을 대조했다.
|
||
- Focused unit: `npm run test:run -- src/features/series src/features/comments`는 7 files / 21 tests passed였다. 추가로 `npm run test:run -- src/features/characters src/features/series src/features/comments`는 13 files / 44 tests passed였다.
|
||
- Mock E2E: `npm run e2e:mock -- tests/e2e/series.spec.ts tests/e2e/comments.spec.ts --project=chromium --project=mobile-chrome`는 19 passed / 1 skipped였다. skipped 1건은 mobile-chrome의 Series keyboard-only 순서 조작 제외 정책이다.
|
||
- Server 분리: `npm run e2e -- tests/e2e/comments.spec.ts --project=chromium`은 3 passed였다. `npm run e2e -- tests/e2e/series.spec.ts --project=chromium`은 7 tests가 실제 실행됐지만 login fixture가 `/login`에 머물러 모두 실패했으므로 Series server integration은 완료로 표시하지 않는다.
|
||
- OQ-009 권고: Series form 권고안은 `title` 80자, `introduction` 2,000자, `keyword` 30자, `writer` 80자, `studio` 80자, `publishedDaysOfWeek`는 실제 요일 최대 7개 또는 `RANDOM` 단독이다. Comments `content` 권고안은 2,000자다. backend 호환 확인 전까지 client 상한은 구현하지 않는다.
|
||
- 문서 검증: `rg -n "^- \\[ \\]" plan-task.md` 결과는 §6 구현 완료 정의 14개만 남았다. `git diff --check -- docs/20260725_AI캐릭터관리자웹/plan-task.md`는 exit 0이었다. Markdown LSP는 `.md` 서버 미설정이라 실행할 수 없었다.
|
||
|
||
**Phase 0~10 전체 코드 리뷰·QA — 2026-07-29:**
|
||
|
||
- 무엇을: PRD, OpenAPI 2.3.0, 구현 계획을 기준으로 Phase 0~10의 현재 working tree를 정적 대조하고 focused unit, 전체 unit, typecheck, lint, production build, 7개 도메인 mock E2E를 실행했다. Phase별 결과는 `reviews/review-phase-{0..10}-20260729.md`에 각각 분리해 기록했다.
|
||
- 기준: commit `dd30e36323543e8f60e9983326503653e8001f12`, 리뷰 시작 시 tracked diff 36개·전체 변경 169개, macOS 26.0, Node v24.12.0, npm 11.7.0이다.
|
||
- 판정: Phase 0·2·7은 신규 확정 결함이 없다. Phase 1은 3건, Phase 3은 3건, Phase 4는 6건, Phase 5는 3건, Phase 6은 1건, Phase 8은 2건, Phase 9는 2건, Phase 10은 2건으로 총 22건을 확정했다. 심각도는 High 11건, Medium 10건, Low 1건이며 Blocker는 없다.
|
||
- plan 전환: 확정 항목을 완료 Task 재개방 없이 `P1-R6~R7`, `P3-R1~R2`, `P4-R1~R3`, `P5-R1~R2`, `P6-R1`, `P8-R1`, `P9-R1~R2`, `P10-R1~R2`의 신규 goal로 해당 Phase에 추가했다. 각 goal은 연결 리뷰 ID, Files, interface, RED/GREEN/REFACTOR, 실행 명령, 기대 결과와 수동 확인을 포함한다.
|
||
- 성공 검증: focused unit은 shared 27, Character 23, Audio+Series 46, Community+FanTalk+Comments 52 tests가 모두 통과했다. `npm run typecheck`, `npm run lint`, `npm run build`는 exit 0이고 production build는 249 modules transformed를 기록했다.
|
||
- 실패 검증: `npm run test:run`은 66 files / 290 tests 중 6 failed·284 passed로 종료했다. 실패는 `REV-P10-001`의 공통 오류 fallback migration 미완료로 추적한다. 7개 spec mock E2E는 병렬 WebKit timeout 4건 때문에 147 passed·17 skipped·4 failed였으나, 실패 4건을 WebKit 단일 worker로 재실행하면 4 passed / 28.4초였다. 이를 재현 가능한 앱 결함으로 추가 확정하지 않되 최초 전체 Gate 실패는 그대로 보존한다.
|
||
- 남은 항목: 위 신규 회귀 goal을 구현하고 전체 unit·mock E2E·server fixture 범위와 `P10-GATE`를 fresh 재실행해야 한다. 이번 리뷰에서는 애플리케이션 코드와 기존 완료 Task를 수정하지 않았다.
|
||
|
||
**P9-R1 inactive Character 하위 mutation 차단 완료 기록 — 2026-07-30:**
|
||
|
||
- 무엇을: `isActive=false` Character workspace에서 Character edit, Audio create/edit/comment, Series create/edit/link/order, Community post/comment mutation affordance와 직접 mutation route를 read-only guidance로 교체했다. 조회/list/detail/player/comment read-only 흐름은 유지했다.
|
||
- 왜: Phase 9 재리뷰 `REV-P9-001`에서 inactive workspace가 하위 mutation entrypoint를 계속 노출해 backend active-only 검증에만 의존하는 문제가 확인됐기 때문이다.
|
||
- 어떻게:
|
||
- RED: inactive Character 대상 Character edit, Audio create/edit/detail comment, Series create/edit/detail/order, Community sheet/comment 경로의 focused unit/E2E를 추가해 기존 mutation UI/route 노출을 실패로 확인했다.
|
||
- GREEN/REFACTOR: Page 단위 `character.isActive`와 viewport capability를 분리하고, `CommentThread`에는 `canMutate`, `CommunityPostSheet`에는 `canMutateComments`를 전달해 post mutation 권한과 comment mutation 권한을 분리했다.
|
||
- Focused unit: `npm run test:run -- src/features/characters src/features/audio-contents src/features/series src/features/community-posts src/features/comments`는 28 files / 152 tests passed였다.
|
||
- Mock E2E: `npm run e2e:mock -- tests/e2e/resource-workflows.spec.ts tests/e2e/character-workspace.spec.ts tests/e2e/audio-content.spec.ts tests/e2e/series.spec.ts tests/e2e/comments.spec.ts`는 103 passed / 13 skipped였다. 추가로 320px Community comment 회귀 단일 실행은 1 passed였다.
|
||
- Fresh gate: `npm run typecheck`, `npm run lint`, `npm run build`는 모두 exit 0이었다. `npm run build` production build는 252 modules transformed를 기록했다.
|
||
- 남은 항목: `P9-R2` field error 연결과 첫 오류 focus 회귀, 이후 `P10-R2`, `P9-GATE`, `P10-GATE`를 순서대로 진행한다.
|
||
|
||
**P9-R2 field error 연결과 첫 오류 focus 완료 기록 — 2026-07-30:**
|
||
|
||
- 무엇을: Character, Audio, Series, Community, FanTalk reply, Comment form의 주요 validation error에 stable ID와 `aria-describedby`/`aria-invalid` 연결을 추가하고 submit 뒤 DOM 순서 첫 invalid control에 focus하도록 적용했다.
|
||
- 왜: Phase 9 재리뷰 `REV-P9-002`에서 visible error 문구가 control과 programmatic하게 연결되지 않고 multi-error submit 뒤 첫 오류 focus가 보장되지 않는 문제가 확인됐기 때문이다.
|
||
- 어떻게:
|
||
- RED: 각 도메인 대표 form test에 error description 연결과 첫 invalid focus assertion을 추가해 기존 markup/focus 흐름의 실패를 확인했다.
|
||
- GREEN/REFACTOR: `focusFirstInvalidControl(form)` helper를 shared로 분리하고, React validation state 반영 뒤 `queueMicrotask`에서 form 내 첫 `aria-invalid="true"` control을 focus하도록 최소 적용했다.
|
||
- Focused unit: `npm run test:run -- src/features/audio-contents/tests/audio-form.test.tsx src/features/series/tests/series-form.test.tsx src/features/community-posts/tests/community-form.test.tsx src/features/fan-talks/tests/fan-talk-reply.test.tsx src/features/comments/tests/comment-thread.test.tsx`는 5 files / 35 tests passed였다. `npm run test:run -- src/features`는 35 files / 195 tests passed였다.
|
||
- Mock E2E: `npm run e2e:mock -- tests/e2e/accessibility.spec.ts`는 9 passed / 3 skipped였다. FanTalk/Comments 회귀 `npm run e2e:mock -- tests/e2e/fan-talk.spec.ts tests/e2e/comments.spec.ts`는 36 passed / 4 skipped였다.
|
||
- Fresh gate: `npm run typecheck`, `npm run lint`, `npm run build`는 모두 exit 0이었다. `npm run build` production build는 253 modules transformed를 기록했다.
|
||
- 남은 항목: `P10-R2`, `P9-GATE`, `P10-GATE`를 순서대로 진행한다.
|
||
|
||
**P10-R2 PRD OpenAPI 2.3.0 현재 상태 정합성 복구 완료 기록 — 2026-07-30:**
|
||
|
||
- 무엇을: PRD 현재 계약 설명의 최종 수정일, OpenAPI document version/path/operation 집계, Community pagination metadata 설명을 실제 OpenAPI 2.3.0 상태와 일치시켰다. Phase 10 재리뷰 `REV-P10-002` 상태도 수정 완료로 정리했다.
|
||
- 왜: PRD 현재형 문장이 OpenAPI 2.0.0 당시의 15 paths·23 operations와 Community metadata 미제공 상태를 계속 설명해 후속 작업자가 현재 계약을 오판할 수 있었기 때문이다.
|
||
- 어떻게:
|
||
- TDD 예외/RED 대체: `jq` actual 집계가 `version=2.3.0 paths=25 operations=37 statuses=implemented`임을 확인하고, stale 현재형 문구 검색에서 `version 2.0.0`, `15개 path`, `23개 operation`, `종료 metadata 계약이 제공되기 전`이 검출되는 것을 확인했다.
|
||
- GREEN: 현재 상태 문장만 2.3.0/25/37과 Community `totalCount`, `page`, `size`, `hasNext` metadata 제공 상태로 정정했다. 2026-07-28 Decision Log의 과거 이력은 보존했다.
|
||
- Verification: 수정 후 동일 `jq` 집계는 `version=2.3.0 paths=25 operations=37 statuses=implemented`였고, current-state stale 검색은 no output이었다. `git diff --check -- docs/20260725_AI캐릭터관리자웹`는 no output이었다.
|
||
- 남은 항목: `P9-GATE`, `P10-GATE`를 순서대로 진행한다.
|
||
|
||
**P9-GATE 재검증 완료 기록 — 2026-07-30:**
|
||
|
||
- 무엇을: `P9-R1`, `P9-R2`, `P10-R2` 완료 후 활성 릴리스 범위의 install, typecheck, lint, unit, mock/server E2E, build, 금지 패턴을 fresh Gate로 재실행했다. 도메인별 server integration은 전용 개발 API fixture가 필요한 범위와 분리했다.
|
||
- 왜: Phase 9 재리뷰 수정 후 exact `npm run e2e:mock`와 server allowlist Gate가 0 failure/0 error로 재현되어야 `P10-GATE`로 넘어갈 수 있기 때문이다.
|
||
- 어떻게:
|
||
- Mock E2E 안정화: `comments.spec.ts`의 320px Community 댓글 flow가 root 댓글 생성 직후 root reload와 답글 버튼 click이 겹치며 WebKit 첫 실행에서 답글 region을 못 여는 경합을 재현했다. 새 root 댓글 visible, 답글 버튼 enabled, 답글 region visible을 기다리도록 테스트 절차만 최소 보강했다.
|
||
- Server allowlist 정리: bare `npm run e2e`는 server boundary/auth/smoke/accessibility shell 36 tests만 실행하고, `series.spec.ts`, `fan-talk.spec.ts`, `comments.spec.ts`는 명시 file filter가 있을 때만 server `testMatch`에 포함되도록 분리했다. 이로써 전용 개발 API fixture 없는 도메인 mock journey가 bare server Gate를 실패시키지 않으며, focused server 명령은 `No tests found`가 아니게 유지된다.
|
||
- Install/static/unit: `npm ci`는 307 packages added, 0 vulnerabilities였다. `npm run typecheck`, `npm run lint`, `npm run test:run`, `npm run build`는 모두 exit 0이었다. 전체 unit은 72 files / 354 tests passed였고 production build는 253 modules transformed, `dist/assets/index-B2ehqvd_.js` 497.35 kB(gzip 129.53 kB)를 기록했다.
|
||
- E2E: 최종 exact `npm run e2e:mock`은 206 passed / 22 skipped였다. 직전 full run의 WebKit 계열 `/login` `page.goto` timeout 2건은 `npm run e2e:mock -- --last-failed --workers=1`에서 2 passed로 환경성 timing임을 확인했고, 이후 exact full mock 재실행이 0 failure로 통과했다. `npm run e2e`는 server allowlist 36 passed였다.
|
||
- 설정 검증: `VITE_API_MODE=server npx playwright test --list`는 36 tests / 4 files만 listing했고, `VITE_API_MODE=server npx playwright test tests/e2e/comments.spec.ts --project=chromium --list`는 Comments 3 tests를 listing해 focused domain server spec 교집합을 보존했다.
|
||
- 금지 패턴/형식: `rg -n "TODO|TBD|FIXME" src tests`, `rg -n "externalCharacterId|SUNDAY|MONDAY|TUESDAY|WEDNESDAY|THURSDAY|FRIDAY|SATURDAY|state.?[:=].?['\"]OPEN" src --glob '!**/*.test.*' --glob '!**/*.spec.*' --glob '!**/__tests__/**'`, `git diff --check`는 모두 no output이었다.
|
||
- Diagnostics/size: `tests/e2e/comments.spec.ts`와 `playwright.config.ts` LSP diagnostics는 0건이었다. line count는 `playwright.config.ts` 40 lines, `tests/e2e/comments.spec.ts` 133 lines다.
|
||
- 판정: P9-GATE는 최신 exact 명령 기준 0 failure/0 error다. P10 후속 계약 server integration은 `P10-GATE`에서 mock evidence와 별도로 판정한다.
|
||
|
||
**P10-GATE 검증 기록 — 2026-07-30 (server integration 미완료):**
|
||
|
||
- 무엇을: OpenAPI 2.3.0 후속 범위의 focused unit, mock E2E, focused server E2E, server allowlist, typecheck, lint, build, diff 검증을 실행했다. mock 성공이나 server spec 일부 통과를 Series/FanTalk 실제 server 완료로 처리하지 않았다.
|
||
- 자동 검증:
|
||
- `npm run test:run -- src/features/characters src/features/audio-contents src/features/series src/features/community-posts src/features/fan-talks src/features/comments src/shared/mocks`는 37 files / 193 tests passed였다.
|
||
- `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts tests/e2e/audio-content.spec.ts tests/e2e/series.spec.ts tests/e2e/community.spec.ts tests/e2e/fan-talk.spec.ts tests/e2e/comments.spec.ts tests/e2e/error-mapping.spec.ts`는 155 passed / 17 skipped였다.
|
||
- `npm run e2e -- tests/e2e/series.spec.ts tests/e2e/fan-talk.spec.ts tests/e2e/comments.spec.ts --project=chromium`은 17 tests 중 Comments 3 passed, Series/FanTalk 14 failed였다. 실패는 모두 login helper가 `/login`에 머문 `Expected /\/ai-characters$/` timeout으로, 전용 개발 API 계정·재사용 fixture 없이 mock journey용 Series/FanTalk spec을 server 완료 증거로 사용할 수 없음을 확인했다.
|
||
- 추가 확인: `VITE_API_MODE=server npx playwright test tests/e2e/series.spec.ts tests/e2e/fan-talk.spec.ts --project=chromium --list`는 14 tests in 2 files로 Series/FanTalk spec을 정상 수집했다. 두 spec은 `admin@test.com`/`password`와 mock fixture ID `101`, `5001`, `7001`, `7002`를 그대로 사용한다.
|
||
- 추가 확인: `curl -sS -i -X POST "https://test-character-admin.sodalive.net/admin/member/login" ...`와 production host 동일 probe는 모두 `Could not resolve host`로 실패했다. 현재 실행 환경에서는 개발 API host 접근도 불가하므로 credential 유효성 확인까지 진행하지 못했다.
|
||
- `npm run e2e`는 server allowlist 36 passed였다.
|
||
- `npm run typecheck`, `npm run lint`, `npm run build`, `git diff --check`는 모두 exit 0이었다. production build는 253 modules transformed, `dist/assets/index-B2ehqvd_.js` 497.35 kB(gzip 129.53 kB)를 기록했다.
|
||
- 판정: P10-GATE는 focused unit/mock/static/server allowlist와 Comments server focused 범위는 통과했지만, Series/FanTalk focused server integration이 실패했으므로 완료가 아니다. 개발 API host 접근, 전용 ADMIN 계정, 재사용 가능한 fixture ID가 준비되면 `npm run e2e -- tests/e2e/series.spec.ts tests/e2e/fan-talk.spec.ts --project=chromium`을 다시 실행해 별도 완료 판정한다.
|
||
- 남은 항목: Series active-only·CRUD server 결과, FanTalk 답변 수정·팬 원글 삭제·동시 POST 후 활성 답변 1개 server 결과, 실제 개발 API 파일 용량·MIME 거부 확인.
|
||
|
||
**P10-GATE 자동 범위 정정 및 수동 QA 전환 — 2026-07-30:**
|
||
|
||
**P5-R3 Series 표시 label 정합성 복구 진행 기록 — 2026-07-30:**
|
||
|
||
- 무엇을: Series 목록 table, mobile card, 상세 summary의 요일·state·boolean·genre 표시를 `series-display-labels.ts` helper로 통일했다. API DTO, form 초기화, create/update payload의 원본 enum·boolean·`genreId` 값은 유지했다.
|
||
- 왜: Phase 5 리뷰 `REV-P5-004`에서 운영자 화면에 `SUN`, `PROCEEDING`, `genreId`, `isAdult true`, `isActive true` 같은 내부 표현이 노출되는 문제가 확정됐기 때문이다.
|
||
- 어떻게:
|
||
- RED: `src/features/series/tests/series-routes.test.tsx`와 `tests/e2e/series.spec.ts`에 raw enum·boolean·field-name 노출 금지 assertion을 추가했고, 기존 구현에서는 사용자 표시 label 기대가 실패함을 확인했다.
|
||
- GREEN/REFACTOR: `formatSeriesPublishedDays`, `formatSeriesState`, `formatSeriesGenre`, `formatSeriesAudience`, `formatSeriesExposure`, `formatSeriesFlags`를 Series 전용 helper로 분리하고 `SeriesList`, `SeriesListItem`, `SeriesSummary`가 이를 사용하도록 정리했다. 독립 visual QA 1차에서 `장르 #id` fallback이 내부 ID 노출 가능성으로 지적돼 fallback을 `알 수 없는 장르`로 바꾸고 lookup miss 회귀 test를 추가했다.
|
||
- Focused unit: `npm run test:run -- src/features/series/tests/series-routes.test.tsx`는 1 file / 5 tests passed였다. `npm run test:run -- src/features/series`는 7 files / 29 tests passed였다.
|
||
- Mock E2E: `npm run e2e:mock -- tests/e2e/series.spec.ts --project=chromium`은 7 passed였다. fallback 수정 후 full `npm run e2e:mock -- tests/e2e/series.spec.ts`는 25 passed / 3 skipped였다.
|
||
- Fresh static gate: `npm run typecheck`, `npm run lint`, `npm run build`, `git diff --check`는 모두 exit 0 또는 no output이었다. `src/features/series` LSP diagnostics는 오류 0건이었다.
|
||
- Browser evidence: mock mode에서 1280px desktop list/detail과 320px mobile list/detail을 fresh capture했고, DOM raw leak check `/SUN|PROCEEDING|isAdult true|isActive true|genreId|장르 #/`는 네 화면 모두 false였다. PNG signature/dimensions는 정상 확인했다.
|
||
- 수동 확인: 사용자가 [Phase 5 현재 리뷰](./reviews/phase5-series-management.md)의 재현 및 검증 절차를 보고 수동으로 확인했다.
|
||
- 추가 보완: 독립 visual QA recheck B가 모바일 CJK 줄바꿈과 공통 header의 visible `characterId:`를 지적해, Series 모바일 안내문·카드 소개문에 `break-keep`을 적용하고 `CharacterWorkspaceLayout` header의 `characterId:` 표시를 제거했다. `CharacterWorkspaceLayout.test.tsx`는 header `characterId:` 미노출을 확인한다.
|
||
- 최종 확인: 최종 fresh capture 4개에서 `/SUN|PROCEEDING|isAdult true|isActive true|genreId|장르 #|characterId:/` 누출은 모두 false였다. 독립 visual QA 최종 A/B는 모두 PASS, BLOCKING 없음이었다.
|
||
- 남은 항목: `P5-R3` 범위 없음. Series 실제 server integration은 기존 Gate 정책에 따라 mock UI 증거와 분리해 추적한다.
|
||
|
||
- 무엇을: E2E용 ADMIN 계정 미제공 정책에 맞춰 P10-GATE 자동 완료 조건에서 Series/FanTalk 실제 server integration을 제외하고, 같은 Gate의 수동 QA 목록으로 분리했다.
|
||
- 왜: 계정 없는 상태에서 mock credential, sessionStorage 주입, mock 성공을 실제 server 완료 증거로 대체하면 검증 의미가 깨지기 때문이다. 자동 Gate는 focused unit, mock E2E, server allowlist, typecheck, lint, build, diff 검증으로 한정한다.
|
||
- 어떻게: P10-GATE 본문에서 계정 의존 server focused 명령을 자동 명령 목록에서 제거하고, Series active-only·CRUD, FanTalk 답변 수정·팬 원글 삭제·동시 POST reply 1개, 파일 용량·MIME 서버 거부를 수동 QA 체크리스트로 명시했다. Decision Log에는 E2E용 ADMIN 계정을 제공하지 않는 정책과 mock/session 주입을 server 완료 증거로 대체하지 않는 결정을 추가했다.
|
||
- 판정: P10-GATE 자동 Gate는 기존 검증 결과 기준 통과 상태로 정리하고, 실제 개발 API server integration은 수동 QA 대기 항목으로 유지한다.
|
||
|
||
**P8-R2 Comments mock 불변식 수정 검증 — 2026-07-30:**
|
||
|
||
- 무엇을: Comments mock store의 2단계 parent, AI 작성 row PUT, row-only DELETE 불변식을 복구하고 Audio·Community handler contract test를 추가했다.
|
||
- 왜: 기존 mock이 reply를 부모로 하는 3단계 댓글과 fan 직접 수정을 허용하고, root 삭제 시 직접 답글까지 함께 제거했기 때문이다.
|
||
- 어떻게:
|
||
- RED: `npm run test:run -- src/features/comments/tests/comment-contract.test.ts`는 1 file / 3 failed / 3 passed로 각 불변식 위반을 재현했다.
|
||
- GREEN: 같은 focused test는 1 file / 6 tests passed였다. `npm run test:run -- src/features/comments src/shared/mocks`는 8 files / 33 tests passed였다.
|
||
- UI/E2E: `npm run e2e:mock -- tests/e2e/comments.spec.ts --project=chromium`은 3 passed, 전체 `npm run e2e:mock -- tests/e2e/comments.spec.ts`는 10 passed / 2 skipped였다. 기존 UI의 fan PUT 0-request assertion은 유지했다.
|
||
- Static/diff: `npm run typecheck`, `npm run lint`, `npm run build`, `git diff --check -- docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/reviews/phase8-comments.md src/shared/mocks src/features/comments tests/e2e/comments.spec.ts`는 모두 exit 0이었다. 변경한 TypeScript 두 파일의 LSP diagnostics도 0건이었다.
|
||
- 남은 항목: 실제 server Comments integration은 별도 P8/P10 Gate 범위다.
|
||
|
||
**Phase 0~10 요청 기준 코드 리뷰·QA — 2026-07-31:**
|
||
|
||
- 무엇을: `prd.md`, `api-contract.openapi.json` 2.3.0, 현재 `plan-task.md`와 Phase 0~10 구현·test·리뷰 기록을 current working tree에서 다시 대조하고 Phase별 결과를 `reviews/phase0-*.md`부터 `phase10-*.md`까지 별도로 누적했다.
|
||
- 왜: 기존 수정 완료 기록과 실제 자동 Gate가 일치하는지 확인하고, 확정된 문제만 완료 Task를 되돌리지 않은 신규 회귀 Task로 전환하기 위해서다.
|
||
- 어떻게:
|
||
- 전체 unit: `npm run test:run` — exit 1, 78 files 중 2 failed / 76 passed, 394 tests 중 2 failed / 392 passed. 두 실패는 `REV-P9-008`의 mock E2E script·README·contract test 불일치다.
|
||
- focused 재현: `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts` — exit 1, 2 failed / 5 passed.
|
||
- 정적/build: `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod`, `git diff --check`, `git diff --cached --check` — 모두 exit 0. build는 기존 502.94kB chunk warning만 표시했다.
|
||
- server E2E: sandbox `listen EPERM`을 제품 실패와 분리해 승인된 로컬 실행으로 `npm run e2e` — 4 projects / 36 passed.
|
||
- mock E2E: `npm run e2e:mock` — Chromium 52 passed, WebKit 44 passed / 8 skipped, Mobile Chrome 47 passed / 5 skipped, Mobile Safari 43 passed / 9 skipped, 합계 186 passed / 22 skipped / 0 failed.
|
||
- script semantics: `npm run e2e:mock -- --list --project=chromium` — exit 0, filtered 인자를 전달해 52 tests 수집.
|
||
- OpenAPI: `jq` parse·version·path/operation·schema ref 검사 — 3.1.0 / 2.3.0, 25 paths / 37 implemented operations, 누락 schema ref 0건.
|
||
- 정적 금지 패턴: production `as any`, TypeScript 오류 숨김, TODO/TBD/FIXME, `externalCharacterId`, 민감정보 저장·console 출력 신규 사용을 검색했고 test assertion 외 확정 위반 0건.
|
||
- 판정: 기능·OpenAPI 소유의 신규 결함은 확인되지 않았다. Low 2건을 확정해 `REV-P9-008` → 신규 `P9-R8`, `REV-P10-008` → 신규 `P10-R7`로 전환했다. `P10-R7`은 `P9-R8` 완료 뒤 실행한다.
|
||
- 당시 후속 전환 항목: `P9-R8`, `P10-R7`. 현재 남은 항목은 실제 Edge/Safari 기기 최신 2개 major QA와 실제 개발 API Series/FanTalk/Comments/file policy 수동 QA다.
|
||
|
||
**Phase 0~10 요청 최종 재검증 — 2026-07-31:**
|
||
|
||
- 무엇을: Phase별 현재 리뷰의 마지막 판정을 fresh 전체 Gate와 다시 대조하고, transport 경계를 결합했을 때만 드러나는 인증·upload lifecycle 후보를 별도 진단 test로 검증했다.
|
||
- 자동 Gate: `npm run test:run` 78 files / 394 tests, 진단 코드 제거 후 기존 fetch/upload auth focused 2 files / 19 tests, `npm run e2e` 4 projects / 36 passed, `npm run e2e:mock` 4 projects / 186 passed / 22 skipped / 0 failed. `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod`, OpenAPI parse/version/25 paths/37 implemented operations/모든 `$ref`, staged·unstaged diff check가 통과했다.
|
||
- 신규 판정: `REV-P1-018`은 fetch+XHR 동시 401에서 callback 기대 1회 대비 실제 2회, `REV-P4-011`은 network error→late abort 뒤 새 session 401 callback 기대 총 2회 대비 실제 1회로 재현했다. 진단용 임시 test는 판정 후 제거했고 애플리케이션 코드는 수정하지 않았다.
|
||
- plan 전환: Phase 1 신규 `P1-R12`와 Phase 4 신규 `P4-R8`을 추가했다. 두 Task가 `upload-audio-content.ts`를 함께 수정하므로 `P1-R12` 완료 뒤 `P4-R8`을 실행한다.
|
||
- 남은 항목: 실제 Edge/Safari 기기 최신 2개 major QA, 실제 개발 API Series/FanTalk/Comments/file policy 수동 QA.
|
||
|
||
**P1-R12·P4-R8 리뷰 보완 완료 — 2026-07-31:**
|
||
|
||
- 무엇을: fetch client와 Audio XHR upload의 교차 401 만료 전환 중복, upload terminal cleanup 이중 정산을 수정했다.
|
||
- RED/GREEN: `P1-R12` RED는 `clearSession` 기대 2회 대비 실제 3회, reviewer blocker RED는 fetch-first 뒤 XHR 401에서 기대 1회 대비 실제 2회, `P4-R8` RED는 새 session 401 callback 기대 2회 대비 실제 1회였다. callback 직전 current token guard와 요청별 settled cleanup 적용 후 focused lifecycle 회귀는 3 files / 22 tests passed였다.
|
||
- 최종 검증: `npm run test:run` — 79 files / 397 tests passed. `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod`, targeted `git diff --check` — 모두 exit 0. build는 기존 503.04kB chunk warning만 표시했다. 변경 TS 파일 LSP diagnostics는 오류 0건이고 touched TS files는 모두 250 LOC 이하다.
|
||
- 기록: Phase 1·4 현재 리뷰의 `REV-P1-018`, `REV-P4-011`을 수정 완료로 갱신했다. E2E는 사용자 지시에 따라 개발 중 반복 실행하지 않고 실제 개발 API 수동 QA 대기는 유지한다.
|
||
|
||
**Phase 0~10 문서 기준 재리뷰 — 2026-07-31:**
|
||
|
||
- 무엇을: `prd.md`, `api-contract.openapi.json`, `plan-task.md`, Phase 0~10 현재 리뷰, 제품·test·설정을 current working tree에서 다시 대조하고 결과를 Phase별 review에 별도 누적했다.
|
||
- 정적/OpenAPI: `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod`는 exit 0이었다. build는 256 modules, 503.04kB JS chunk warning만 표시했다. `jq` 검사는 OpenAPI 3.1.0, document 2.3.0, 25 paths, 37 operations 전체 `implemented`, 누락 schema ref 0건을 확인했다.
|
||
- Unit: `npm run test:run -- --reporter=verbose` — exit 1, 79 files 중 5 failed / 74 passed, 397 tests 중 5 failed / 392 passed, 717.76초. 실패 후보 5개 spec focused는 exit 0, 5 files / 25 tests passed였다. 전체 Gate 비결정성을 `REV-P9-009` → `P9-R9`로 전환했다.
|
||
- Server E2E: sandbox 실행은 `listen EPERM 127.0.0.1:8888`로 exit 1이었고, 승인된 로컬 실행 `npm run e2e`는 Chromium·WebKit·Mobile Chrome·Mobile Safari 36 passed였다.
|
||
- Mock E2E: 승인된 bare `npm run e2e:mock`은 Chromium 52 passed 후 WebKit main 2 failed / 32 passed / 7 skipped로 exit 1이었고 후속 segment는 실행되지 않았다. 동일 두 실패를 WebKit focused로 재실행한 명령은 exit 0, 2 passed였다. browser Gate 비결정성을 `REV-P9-010` → `P9-R10`으로 전환했다.
|
||
- 기능·문서 판정: crop frame/저장 영역, Character create/edit 인가 probe, malformed route decode, Community GIF Blob URL, plan 현재형·필수 section 문제를 확정했다. `REV-P1-019~021` → `P1-R13~R15`, `REV-P6-004` → `P6-R4`, `REV-P10-009~010` → `P10-R8~R9`로 전환했다. 제품·test·OpenAPI 코드는 수정하지 않았다.
|
||
- 남은 항목: 신규 `P1-R13~R15`, `P6-R4`, `P9-R9~R10`, `P10-R8~R9` 구현·회귀 Gate, 실제 Edge/Safari 기기 QA, 실제 개발 API Series/FanTalk/Comments/file policy 수동 QA.
|
||
|
||
**리뷰 확정 발견사항 수정 결과 재점검 — 2026-07-31:**
|
||
|
||
- 무엇을: `P1-R13~R15`, `P6-R4`, `P9-R9~R10`, `P10-R8~R9`의 구현·문서 수정과 Chromium/mobile Chrome 제품 결정을 current staged working tree에서 다시 대조했다.
|
||
- 왜: 완료 기록을 실제 코드·test·실행 가능한 검증 명령으로 재현하고 남은 문제만 기존 완료 Task를 열지 않는 신규 회귀 Task로 전환하기 위해서다.
|
||
- 어떻게:
|
||
- Phase 1 focused 5 files / 46 tests, Phase 6 focused 3 files / 18 tests, Phase 9 docs contract 2 files / 7 tests가 통과했다.
|
||
- 전체 `npm run test:run`은 두 차례 연속 각각 81 files / 409 tests passed였다. `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod`, staged diff check도 exit 0이었다. build는 기존 500kB chunk warning만 표시했다.
|
||
- `npm run e2e:mock -- --list`는 Chromium/mobile Chrome 104 tests, `npm run e2e -- --list`는 같은 두 project 18 tests만 수집했다. Character/FanTalk focused mock E2E는 두 project 합계 4 passed였고, Phase 1 crop·Phase 6 Community Gate의 Chromium 4-spec 실행은 31 passed였다.
|
||
- OpenAPI는 3.1.0 / document 2.3.0, 25 paths / 37 operations, 누락 schema ref 0건이었다. Goal 필수 section 12/12와 26개 Markdown 파일의 local broken link 0건을 확인했다.
|
||
- 판정: `REV-P1-022` malformed route 보호 probe 우회, `REV-P9-011` Chromium-only 문서·Decision Log·smoke 경로 불일치, `REV-P10-011` §5.1 현행화 누락과 self/history match 검증 명령을 확정해 각각 신규 `P1-R16`, `P9-R11`, `P10-R10`으로 전환했다. 제품·test·설정 코드는 수정하지 않았다.
|
||
- 남은 항목: `P1-R16` → `P9-R11` → `P10-R10` 구현·문서 검증, 실제 crop pixel·stale ADMIN server QA와 실제 개발 API Series/FanTalk/Comments/file policy 수동 QA.
|
||
|
||
**P1-R16·P9-R11·P10-R10 수정 결과 재점검 — 2026-07-31:**
|
||
|
||
- 무엇을: 세 회귀 Task의 unstaged 제품·test·문서 수정과 현재 Chromium/mobile Chrome Gate를 코드 흐름, focused/full test와 원문 검증 명령으로 재검토했다.
|
||
- 자동 검증: 관련 6 files / 42 tests, 전체 81 files / 411 tests, `typecheck`, `lint`, 개발/운영 build와 staged·unstaged diff check가 통과했다. mock list 104 tests와 server list 18 tests는 Chromium/mobile Chrome 두 project만 수집했다. build는 기존 500kB chunk warning만 표시했다.
|
||
- 문서 검증: P10의 동작하도록 바로잡은 section-aware 검색은 0 matches였지만, 문서 원문의 이중 escape 명령은 section을 닫지 못해 과거 이력 10건을 다시 매치했다. local Markdown link는 26 files / broken 0이었다.
|
||
- 판정: 제품 route guard의 신규 runtime 실패는 확인되지 않았다. `REV-P1-023` 인가 matrix·focused Gate 누락, `REV-P9-012` Chromium-only 문서 종료 증거 누락, `REV-P10-012` 원문 명령·현재 상태 재현 실패를 확정해 각각 `P1-R17`, `P9-R12`, `P10-R11`로 전환했다.
|
||
- 남은 항목: `P1-R17` → `P9-R12` → `P10-R11`, 실제 crop pixel·stale ADMIN server QA와 실제 개발 API Series/FanTalk/Comments/file policy 수동 QA. WebKit·Mobile Safari는 지원·검증 범위에서 제외한다.
|
||
|
||
**P1-R17·P9-R12·P10-R11 수정 결과 재점검 — 2026-07-31:**
|
||
|
||
- 무엇을: 세 회귀 Task의 코드·test·문서 수정과 Chromium/mobile Chrome 수집 범위를 current working tree에서 재검토했다.
|
||
- 자동 검증: 관련 6 files / 48 tests, 전체 81 files / 417 tests, `typecheck`, `lint`, 개발/운영 build와 staged·unstaged diff check가 통과했다. mock list 104 tests와 server list 18 tests는 Chromium/mobile Chrome 두 project만 수집했다. build는 기존 500kB chunk warning만 표시했다.
|
||
- 문서·계약 검증: P10 원문 section-aware negative search와 dependency·필수 12 section·local Markdown link 검사는 exit 0이었다. OpenAPI는 3.1.0 / document 2.3.0, 25 paths / 37 operations 전체 `implemented`, 누락 schema ref 0건이었다.
|
||
- Phase별 판정: Phase 1 malformed route 인가 matrix에는 신규 문제가 없었다. `REV-P9-013` Decision Log 사유·문서 section scope 미종결과 `REV-P10-013` 최신 Progress/current-state 불일치를 Low로 확정해 각각 신규 `P9-R13`, `P10-R12`로 전환했다. 제품·Playwright 설정·OpenAPI는 수정하지 않았다.
|
||
- 남은 항목: `P9-R13` → `P10-R12`, 실제 crop pixel·stale ADMIN server QA와 실제 개발 API Series/FanTalk/Comments/file policy 수동 QA. WebKit·Mobile Safari는 지원·검증 범위에서 제외한다.
|
||
|
||
**P9-R13·P10-R12 수정 결과 재점검 — 2026-07-31:**
|
||
|
||
- 무엇을: `P9-R13`의 Chrome-only Decision Log·section contract와 `P10-R12`의 top/tail current-state·Phase 10 리뷰 metadata를 최신 상태로 정렬했다.
|
||
- 자동 검증: docs contract 2 files / 8 tests, 전체 unit, `typecheck`, `lint`, 개발/운영 build, Markdown link 검사와 `git diff --check`를 통과했다. mock list 104 tests와 server list 18 tests는 Chromium/mobile Chrome 두 project만 수집했다.
|
||
- 문서 판정: `P1-R17`, `P9-R12`, `P10-R11`, `P9-R13`, `P10-R12` 자동 보완은 완료됐고, 과거 Progress·Decision·finding은 이력으로 보존했다.
|
||
- 남은 항목: 실제 crop pixel 비교, stale ADMIN server 확인, 실제 개발 API Series/FanTalk/Comments/file policy 수동 QA. WebKit·Mobile Safari는 지원·검증 범위에서 제외한다.
|
||
|
||
**P9-R13·P10-R12 수정 결과 재리뷰 — 2026-07-31:**
|
||
|
||
- 무엇을: Chrome-only Decision Log·docs contract, plan top/tail·Task checklist와 Phase 10 리뷰 상태를 원문·negative-control·fresh Gate로 다시 대조했다.
|
||
- 자동 검증: docs contract 2 files / 8 tests, `typecheck`, `lint`, 개발/운영 build, OpenAPI parse·status/ref와 staged·unstaged diff check가 통과했다. mock list 104 tests와 server list 18 tests는 Chromium/mobile Chrome 두 project만 수집했다.
|
||
- full unit: 첫 fresh 실행은 Community Sheet 취소 후 focus가 trigger 대신 `닫기`로 복귀해 1 failed / 416 passed였고, 동일 test 단독 5회는 모두 통과했다. 두 번째 fresh full은 81 files / 417 tests passed해 full-load effect 경합으로 판정했다.
|
||
- Phase 9 판정: `REV-P9-014` full unit focus 반환 비결정성을 Medium, `REV-P9-015` PRD Decision Log·최신 종료 판정의 후속 H2/H3 false positive를 Low로 확정해 `P9-R14~R15`로 전환했다.
|
||
- Phase 10 판정: `REV-P10-014` top/checklist와 tail/review 완료 상태 충돌 및 OR 기반 검증 false positive를 Low로 확정해 `P10-R13`으로 전환했다. 제품·Playwright 설정·OpenAPI는 수정하지 않았다.
|
||
- 남은 항목: `P9-R14` → `P9-R15` → `P10-R13`, 실제 crop pixel·stale ADMIN server QA와 실제 개발 API Series/FanTalk/Comments/file policy 수동 QA. WebKit·Mobile Safari는 지원·검증 범위에서 제외한다.
|
||
|
||
**P10-R13 수정 검증 기록 — 2026-07-31:**
|
||
|
||
- 무엇을: `P9-R14`, `P9-R15`, `P10-R13` 순서로 자동 보완 Task를 완료하고 plan top/checklist/latest Progress/Phase 10 review를 같은 현재 상태로 맞췄다.
|
||
- 검증: docs contract 2 files / 9 tests, 전체 unit 81 files / 418 tests, `typecheck`, `lint`, `build:dev`, `build:prod`, 필수 12 section 검사와 `git diff --check`가 통과했다. build는 기존 500kB chunk warning만 표시했다.
|
||
- 남은 항목: 실제 crop pixel 비교, stale ADMIN server 확인, 실제 개발 API Series/FanTalk/Comments/file policy 수동 QA. WebKit·Mobile Safari는 지원 범위에서 제외하고 Chromium/mobile Chrome만 유지한다.
|
||
|
||
**P9-R14·P9-R15·P10-R13 수정 결과 재점검 — 2026-07-31:**
|
||
|
||
- 무엇을: 세 Task의 변경, 최신 Phase 9·10 review와 docs contract를 exact scope negative-control 및 fresh 자동 Gate로 재검토했다.
|
||
- 자동 검증: finding 기록 전 Community Sheet focused 5회는 각각 1 passed / 6 skipped, docs contract는 2 files / 9 tests, fresh full unit은 81 files / 418 tests였다. `typecheck`, `lint`, `build:dev`, `build:prod`가 통과했고 Playwright `--list`는 Chromium/mobile Chrome에서만 mock 104 tests와 server 18 tests를 수집했다. finding과 신규 Task를 current-state에 반영한 뒤 docs contract는 기존 `자동 보완 완료` 기대를 실패시켜 1 failed / 8 passed RED가 됐다.
|
||
- Phase 9 판정: focus test 수정은 재현됐다. `sectionAtHeading()`의 임의 suffix 허용과 과거 H2 대상 고정을 `REV-P9-016` Low로 확정해 신규 `P9-R16`으로 전환했다.
|
||
- Phase 10 판정: plan 상단 Progress 누락, 최신 Progress의 EOF scope, review metadata·종료 판정 미분리를 `REV-P10-015` Low로 확정해 `P9-R16` 뒤 신규 `P10-R14`로 전환했다.
|
||
- 남은 항목: `P9-R16` → `P10-R14`, 실제 crop pixel 비교, stale ADMIN server 확인, 실제 개발 API Series/FanTalk/Comments/file policy 수동 QA. WebKit·Mobile Safari는 실행하지 않았고 Chromium/mobile Chrome 지원 범위만 유지한다.
|
||
|
||
**P9-R16·P10-R14 수정 결과 재점검 — 2026-08-01:**
|
||
|
||
- 무엇을: exact heading·Progress helper와 negative-control, plan top/§7 Progress, Phase 9·10 current metadata·최신 결론을 current working tree에서 재검토했다.
|
||
- 자동 검증: finding 기록 전 docs contract 2 files / 9 tests, 전체 unit 81 files / 418 tests, `typecheck`, `lint`, 필수 12 section·Markdown link·`git diff --check`가 통과했다. Playwright `--list`는 Chromium/mobile Chrome에서만 mock 104 tests와 server 18 tests를 수집했다. 신규 finding과 Task를 current-state에 반영한 뒤 docs contract는 기존 `자동 보완 완료` 기대를 실패시켜 1 failed / 8 passed RED가 됐다.
|
||
- Phase 9 판정: exact heading·H3 경계 수정은 확인했다. Phase 9 metadata와 고정된 `## 24` 결론이 완료된 `P10-R14`를 후속으로 유지하고 trailing H2를 놓치는 문제를 `REV-P9-017` Low로 확정해 신규 `P9-R17`로 전환했다.
|
||
- Phase 10 판정: contract가 current Task checklist와 §7 em-dash형 Progress 대신 과거 checklist·Task-local 괄호형 기록을 검사하고, 최하단은 `P9-R16` → `P10-R14` 미완료를 유지하며 검증 H2 날짜도 실행일과 다른 문제를 `REV-P10-016` Low로 확정해 `P9-R17` 뒤 신규 `P10-R15`로 전환했다.
|
||
- 남은 항목: `P9-R17` → `P10-R15`, 실제 crop pixel 비교, stale ADMIN server 확인, 실제 개발 API Series/FanTalk/Comments/file policy 수동 QA. WebKit·Mobile Safari는 실행하지 않았고 Chromium/mobile Chrome 지원 범위만 유지한다.
|
||
|
||
**P9-R17·P10-R15 수정 검증 기록 — 2026-08-01:**
|
||
|
||
- 무엇을: `P9-R17`, `P10-R15` 순서로 Phase 9 metadata·실제 마지막 H2와 Phase 10 §7 Progress marker/current-state contract를 보완했다.
|
||
- 자동 검증: focused RED/GREEN 뒤 docs contract 2 files / 9 tests, 전체 unit 81 files / 418 tests, `typecheck`, `lint`, 필수 12 section·Markdown link·`git diff --check`가 통과했다. Playwright는 사용자 지시에 따라 실제 E2E 대신 Chromium/mobile Chrome `--list`만 수집했으며 mock 104 tests, server 18 tests였다.
|
||
- 판정: 자동 보완 Task는 완료됐다. 남은 항목은 실제 crop pixel 비교, stale ADMIN server 확인, 실제 개발 API Series/FanTalk/Comments/file policy 수동 QA다. WebKit·Mobile Safari는 실행하지 않았고 Chromium/mobile Chrome 지원 범위만 유지한다.
|
||
|
||
**P9-R17·P10-R15 수정 결과 재점검 — 2026-08-01:**
|
||
|
||
- 무엇을: 두 Task의 변경과 Phase 9·10 current review, docs contract helper를 current working tree와 synthetic mutation으로 다시 대조했다.
|
||
- 수정 확인: finding 기록 전 docs contract 2 files / 9 tests와 전체 unit 81 files / 418 tests가 통과했고 `typecheck`, `lint`, 필수 12 section·Markdown link·`git diff --check`도 exit 0이었다. Playwright `--list`는 Chromium/mobile Chrome에서만 mock 104 tests와 server 18 tests를 수집했다.
|
||
- current-state 전환: 신규 finding·Task를 반영한 docs contract는 2 files 중 1 failed / 1 passed, 9 tests 중 2 failed / 7 passed의 의도한 RED다. 실패는 `P9-R17` 범위에 후속 미완료 Task가 섞이는 문제와 완료 상태만 기대하는 Phase 10 current-state assertion에서 발생했다. 필수 section 12/12, 26개 Markdown 파일 local link broken 0, `git diff --check`는 통과했다.
|
||
- Phase 9 판정: `P9-R17` 수정은 확인했다. 완료 finding 상태·Task 범위 불일치 `REV-P9-018`과 fenced heading·동일 제목 최신 H2 오인 `REV-P9-019`를 Low로 확정해 `P9-R18` → `P9-R19`로 전환했다.
|
||
- Phase 10 판정: `P10-R15` 수정은 확인했다. 완료 finding 상태·자체 checklist·실제 최신 §7 Progress 누락을 `REV-P10-017` Low로 확정해 `P10-R16`으로 전환했다. 제품·OpenAPI·Playwright 설정의 신규 문제는 확인되지 않았다.
|
||
- 남은 항목: `P9-R18` → `P9-R19` → `P10-R16`, 실제 crop pixel 비교, stale ADMIN server 확인, 실제 개발 API Series/FanTalk/Comments/file policy 수동 QA. WebKit·Mobile Safari는 실행하지 않았고 Chromium/mobile Chrome 지원 범위만 유지한다.
|
||
|
||
**P9-R18~P9-R19·P10-R16 수정 검증 기록 — 2026-08-01:**
|
||
|
||
- 무엇을: Phase 9 finding·Task 경계와 최신 H2 탐색, Phase 10 finding·checklist·실제 마지막 독립 Progress contract를 순서대로 보완했다.
|
||
- RED/GREEN: `P9-R18` finding 상태 focused RED, `P9-R19` fenced heading·동일 제목 2건 RED/GREEN, `P10-R16` 고정 marker synthetic RED/GREEN과 current-state RED를 확인했다. 새 parser나 dependency 없이 기존 문자열 helper만 최소 확장했다.
|
||
- 자동 검증: Phase 9 focused 3 passed / 5 skipped, Phase 10 focused 2 passed / 7 skipped, docs contract 2 files / 12 tests passed였다.
|
||
- 전체 검증: `npm run test:run`은 81 files / 421 tests passed, `npm run typecheck`와 `npm run lint`는 exit 0이었다. 필수 section 12/12, 26개 Markdown 파일 local link broken 0, `git diff --check`도 통과했다. Playwright `--list`는 Chromium/mobile Chrome에서만 mock 104 tests와 server 18 tests를 수집했다.
|
||
- 판정: `P9-R18`, `P9-R19`, `P10-R16` 자동 보완 Task는 완료됐다. 실제 crop pixel 비교, stale ADMIN server 확인, 실제 개발 API Series/FanTalk/Comments/file policy 수동 QA는 별도 대기다. WebKit·Mobile Safari는 실행하지 않고 Chromium/mobile Chrome 지원 범위만 유지한다.
|
||
|
||
**P10-R16 수정 결과 재점검 — 2026-08-01:**
|
||
|
||
- 무엇을: `P10-R16`의 실제 마지막 Progress 선택을 독립 reviewer가 current helper와 동일 marker synthetic 관점에서 재검토했다.
|
||
- 판정: 마지막 marker 문자열을 첫 동일 occurrence로 다시 찾는 `REV-P10-018` Low를 확정해 신규 `P10-R17`로 전환했다. 나머지 finding/checklist/H2/top-tail 동기화에는 신규 문제가 없었다.
|
||
- 남은 항목: `P10-R17`, 실제 crop pixel 비교, stale ADMIN server 확인, 실제 개발 API Series/FanTalk/Comments/file policy 수동 QA. Chromium/mobile Chrome 지원 범위만 유지한다.
|
||
|
||
**ImageCropDialog advanced cropper 전환 검증 기록 — 2026-08-04:**
|
||
|
||
- 무엇을: 공통 `ImageCropDialog`의 custom 이동·zoom UI를 `react-advanced-cropper@0.20.1`로 교체하고, 기존 `aspect`, `maxWidth`, `noUpscale`, `renderCrop` 계약과 pending/error/single-flight 처리를 유지했다. 사용자의 명시적 결정에 따라 별도 방향·zoom 버튼은 제거하고 pointer와 keyboard 조작만 유지하도록 PRD `FILE-008`과 UI 원칙을 정렬했다.
|
||
- TDD/회귀: viewport·aspect·keyboard·null coordinates·orientation request·focus wrapper를 각각 RED/GREEN으로 확인했다. 독립 리뷰가 제기한 비중앙 좌표 이중 축척 후보는 `scalePreviewOffset`이 원본 크기가 아니라 `baseWidth`/`baseHeight`를 사용함을 확인했고, off-center 1:1과 210:297 좌표를 `calculateCropSourceRect`로 되돌리는 회귀 2건을 추가해 선택 source rect가 일치함을 검증했다. 적용 중 Escape가 disabled 취소 버튼을 우회하는 RED도 재현해 pending 동안 Escape를 무시하도록 정렬했다.
|
||
- 자동 검증: `npm run test:run`은 82 files / 438 tests passed, `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod`, `git diff --check`는 exit 0 또는 no output이었다. dev/prod build는 309 modules, JS 597.35kB(gzip 158.54~158.55kB)와 기존 500kB chunk warning을 기록했다.
|
||
- 브라우저 QA: `npm run e2e:mock -- tests/e2e/series.spec.ts --project=chromium`은 7 passed였다. mock UI 768/1280px에서 pointer drag, 방향키, `+`/`-`, 초기화, 취소/Escape, focus trap, 적용과 가로 overflow 0을 확인했고, 2400×1804 입력은 210:297 비율의 1000×1414px PNG로 생성됐다. 375px Series 생성은 기존 모바일 read-only 정책에 따라 crop form을 노출하지 않는다.
|
||
- 공급망: `npm audit --omit=dev`는 vulnerability 0건이다. 전체 `npm audit`의 high 1건은 ESLint가 사용하는 dev-only `brace-expansion@5.0.8` 경로이며 production cropper dependency에는 포함되지 않는다.
|
||
- 남은 항목: 실제 개발 API 파일 업로드 수동 QA는 기존 server integration 대기로 유지한다. 375px Series 안내의 기존 오탈자·줄바꿈은 crop surface 밖의 별도 UI 정리 범위다.
|
||
|
||
**P10-R17 수정 검증 기록 — 2026-08-01:**
|
||
|
||
- 무엇을: 동일 제목·날짜 Progress marker가 반복돼도 마지막 occurrence의 record를 선택하도록 보완했다.
|
||
- RED/GREEN: 중복 marker synthetic은 1 failed / 8 skipped에서 1 passed / 8 skipped로 전환했고 current-state contract에 `P10-R17` checklist·finding·최신 review를 포함했다.
|
||
- 자동 검증: docs contract 2 files / 12 tests, 전체 unit 81 files / 421 tests, `typecheck`, `lint`, 필수 section 12/12, 26개 Markdown 파일 local link broken 0과 `git diff --check`가 통과했다. Playwright `--list`는 Chromium/mobile Chrome에서만 mock 104 tests와 server 18 tests를 수집했다.
|
||
- 독립 재리뷰: 직전 동일 marker Important가 닫혔고 신규 Critical/Important/Minor 없음, 요청 범위 merge ready로 판정됐다.
|
||
- 판정: `P9-R18`~`P9-R19`, `P10-R16`~`P10-R17` 자동 보완 Task는 완료됐다. 실제 crop pixel 비교, stale ADMIN server 확인, 실제 개발 API Series/FanTalk/Comments/file policy 수동 QA는 별도 대기다. Chromium/mobile Chrome 지원 범위만 유지한다.
|