# AI 캐릭터 관리자 웹 구현 계획 > **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` (recommended) or `superpowers:executing-plans` to implement this plan Phase-by-Phase. 모든 구현 항목은 체크박스(`- [ ]`)로 추적한다. **Goal:** ADMIN이 로그인한 뒤 AI 캐릭터를 선택하고, 선택한 캐릭터 문맥에서 Character·Audio·Series·Community·FanTalk·Comments를 안전하게 관리하는 독립 React SPA를 구현한다. **Architecture:** 프로젝트 세팅과 공통 플랫폼·인증/인가를 먼저 완결한 뒤, 각 도메인을 API·상태·화면·오류·반응형·접근성·E2E까지 포함한 vertical slice로 구현한다. 모든 하위 리소스는 URL의 `characterId`를 기준으로 격리하고, 공통 API client가 envelope parsing, 인증 header, `Accept-Language: ko`, 401/403을 담당한다. 두 개 이상의 Phase에서 동일한 의미로 반복될 것이 확정된 UI·파일·미디어 컴포넌트는 Phase 1에서 먼저 만들고, 그 밖의 UI는 도메인 Phase 안에서 작게 나눈 뒤 실제 재사용 근거가 생길 때 shared로 올린다. **Tech Stack:** React, TypeScript, Vite, Tailwind CSS, shadcn/ui, React Router, TanStack Query, React Hook Form, Zod, Axios/XHR upload adapter, date-fns/date-fns-tz, dnd-kit, Lucide React, Vitest, React Testing Library, MSW, Playwright, axe-core. | 문서 항목 | 내용 | |---|---| | 상태 | Phase 0~2 완료, Phase 3 Character workspace 착수 전 | | 최초 작성일 | 2026-07-25 | | 재작성일 | 2026-07-26 | | 요구사항 기준 | [prd.md](./prd.md) | | API 기준 | [api-contract.openapi.json](./api-contract.openapi.json) | ## 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 목록은 활성 상태 query와 client-side 활성 필터를 추가하지 않는다. active-only 반환 보장은 외부 의존으로 추적하고 서버 반환값을 그대로 사용한다. - Character·Audio·Series soft delete 성공 후 해당 목록으로 이동한다. Community는 열린 Sheet를 닫고 현재 목록에서 제거한다. 모두 성공 알림을 표시한다. - 워크스페이스 상세 성공 응답의 Character가 `isActive=false`이면 모든 하위 mutation 진입점을 차단한다. soft delete 직후에는 목록 이동을 우선한다. - Series 생성 payload에는 `state`를 넣지 않는다. 수정에서 state를 바꾸지 않으면 key를 생략한다. - Series state에 `OPEN`, 요일에 `MONDAY` 같은 보정 전 enum을 사용하지 않는다. - multipart의 JSON part 이름은 `request`로 고정하고, optional 교체 파일을 보내지 않으면 기존 media를 유지한다. - 목록은 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. Phase 8 Comments 계약이 없으면 `P8-T1`의 계약 부재·재개 조건과 `P8-GATE` 제외/대기 증거를 남긴 뒤 Phase 9로 진행한다. 6. Phase 9는 제공 계약과 명시적 제외 범위만 대상으로 실행하고 결과를 `활성 범위 완료`로 기록한다. 미제공 계약을 포함한 `전체 기능 완료`로 표현하지 않는다. 7. backend 계약이 후속 도착하면 완료 Phase를 묵시적으로 다시 열지 않고 별도 vertical slice를 계획해 구현한 뒤 영향받는 Phase Gate와 `P9-GATE`를 다시 실행한다. 이 전략은 추가 사용자 결정을 기다리기 위한 임시 우회가 아니라, 제공 범위의 UI와 공통 품질을 먼저 완성해 전체 lead time을 줄이는 확정 실행 방식이다. ### 2.5 Goal 기능 운영 규칙 - `create_goal`에는 동시에 하나의 미완료 goal만 등록한다. Phase 전체가 아니라 아래에 `Goal 실행`으로 표시한 Task 하나를 기본 단위로 사용한다. - goal objective는 해당 Task의 `Goal 실행`, `시작 조건`, `완료 증거`, `범위 밖`을 함께 복사해 등록한다. 사용자가 명시적으로 요청하지 않으면 token budget을 설정하지 않는다. - 활성 goal이 있으면 새 goal을 만들지 않고 같은 Task를 이어서 수행한다. 체크박스 일부만 끝났거나 검증·기록이 남았으면 goal을 완료 처리하지 않는다. - `완료 증거`와 해당 Task의 체크박스를 모두 충족하고 `plan-task.md` 검증 기록까지 누적한 뒤에만 goal을 `complete`로 갱신한다. - 외부 계약이나 권한 같은 동일 차단 사유가 최초 시도와 자동 후속을 포함해 3회 연속 반복되고, 문서화·독립 작업 등 의미 있는 진전도 불가능할 때만 goal을 `blocked`로 갱신한다. 그 전에는 가능한 범위를 계속 수행한다. - 계약 미제공으로 기능을 제외할 때는 PRD 결정 기록 → 제공된 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 → one reply; detail/edit/filter는 계약 대기 | | 8 | Comments vertical slice | Phase 4 + Phase 6 | Audio/Community thread → permission별 CRUD | | 9 | 교차 회귀·인수인계 | 활성 범위의 Phase 0~8 | 전체 journey, viewport, axe, security, mock/server build | 기본 진행 순서는 Phase 번호를 따른다. 다만 Phase 5·6·7은 자신의 선행조건과 계약이 충족되면 병행할 수 있고, 외부 계약으로 막힌 Phase가 다른 독립 Phase를 막지 않는다. ### 3.1 Phase 3~9 OpenAPI 준비 상태 | Phase | 현재 구현 가능 범위 | 외부 의존 또는 제외 범위 | 판정 | |---:|---|---|---| | 3 Character | 활성 기본 목록·레거시 검색·상세·필수 image 생성·허용 field 수정·soft delete request와 workspace UI | original work lookup, 검색 결과 active-only 보장, 도메인 오류 key | 핵심 UI 가능, 일부 server 수용 기준 대기 | | 4 Audio | 테마·제목 검색·상세·생성·허용 field 수정·재생·upload | active-only 반환, backend file/container 검증, 오류 key; status filter와 수정 audio/schedule/theme/series는 계약상 제외 | 제공 operation 범위 구현 가능 | | 5 Series | 목록·상세 조회, 연결 후보 검색·연결·해제·전체 순서 | genre lookup이 생성 차단, edit DTO가 수정 차단, active-only·오류 key 대기 | CRUD 전체는 차단, 조회·연결·순서 부분 가능 | | 6 Community | timezone 목록·Sheet·생성·허용 field 수정·고정·soft delete request·media | active-only, pagination 종료 metadata, backend file 검증과 오류 key | 핵심 UI 가능, 목록 종료·server 수용 기준 대기 | | 7 FanTalk | page 목록과 답변 1회 생성 | 상세·답변 수정·전체 결과 filter/sort·유일성 오류 | 제공 operation 범위 구현 가능 | | 8 Comments | 계약 독립적인 shell·상태 inventory | 두 target의 댓글 CRUD·2단계·권한 오류 전체 | network slice 차단 | | 9 Final | 제공 계약과 명시적 제외 범위의 교차 회귀 | 미제공 P0 범위를 포함한 전체 릴리스 완료 주장 | 활성 범위 Gate 후 가능 | OQ-009를 포함한 프론트엔드 제품 결정 절차는 확정됐다. 위 표의 대기 사항은 추가 사용자 결정이 아니라 backend OpenAPI 보완 또는 명시적 후속/제외 기록이 필요한 외부 의존이다. ```text Phase 0 Setup └─ Phase 1 Platform + Auth/Authz + Shared Components └─ 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 ┘ ``` ## 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와 4개 Playwright project smoke 및 검증 기록. - **범위 밖:** 인증과 실제 도메인 route. - [x] Vitest, jsdom, React Testing Library, jest-dom을 구성한다. - [x] 각 test 뒤 DOM·mock·storage가 정리되는 공통 setup을 만든다. - [x] Playwright에 desktop Chromium/WebKit과 mobile Chrome/Safari viewport project, Vite webServer를 구성한다. - [x] fresh environment에서 Chromium/WebKit browser binary를 설치하는 명령을 README와 Gate에 포함한다. - [x] ``, `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 webkit 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` 완료 후 같은 명령을 재실행해 모두 성공했다. --- ## 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` 성공형이 `errorProperty` 생략과 `null`을 모두 수용하고 오류형은 비2xx status·`message`·`errorProperty`를 보존하는 test를 작성한다. - [x] `PageData`와 `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건으로 정리했다. --- ## 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`와 실제 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`와 모든 회귀 수정 완료를 확인할 수 있다. --- ## 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, `EXT-011` 신규 Character 오류 message key. `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 when `CHAR-013` contract is available: `src/features/characters/components/OriginalWorkCombobox.tsx` - Create: `src/features/characters/tests/character-api.test.ts` - Create: `src/features/characters/tests/{character-list,character-form}.test.tsx` - Create: `src/layouts/CharacterWorkspaceLayout.tsx` - Create: `src/layouts/CharacterWorkspaceLayout.test.tsx` - Create: `tests/e2e/character-workspace.spec.ts` - Modify: `src/app/router.tsx`, `src/app/route-paths.ts` #### Phase 3 Task 실행 계약 | Goal | Files | Interfaces | TDD·검증 기준 | |---|---|---|---| | `P3-T1` | Modify: `docs/20260725_AI캐릭터관리자웹/prd.md`, `docs/20260725_AI캐릭터관리자웹/plan-task.md`; Read: `docs/20260725_AI캐릭터관리자웹/api-contract.openapi.json`; Test: 없음 | Consumes: OpenAPI `Character*` schema·4 operation. Produces: `CharacterListResponse`, `CharacterDetailResponse`, create/update multipart와 screen inventory | **TDD 예외:** 외부 계약 조사 Task다. `node -e "JSON.parse(require('fs').readFileSync('docs/20260725_AI캐릭터관리자웹/api-contract.openapi.json','utf8'))"`와 `rg -n 'CHAR-0(1[2-8]|0[1-9])' docs/20260725_AI캐릭터관리자웹/prd.md docs/20260725_AI캐릭터관리자웹/plan-task.md`가 exit 0인지 확인한다. 수동 확인: 제공·미제공 범위가 PRD와 일치한다. | | `P3-T2` | Create: `src/features/characters/api/character-api.ts`, `src/features/characters/model/types.ts`, `src/features/characters/pages/{CharacterListPage,CharacterDetailPage}.tsx`, `src/features/characters/components/{CharacterList,CharacterListItem,CharacterProfile}.tsx`, `src/features/characters/tests/character-list.test.tsx`, `src/layouts/CharacterWorkspaceLayout.tsx`, `src/layouts/CharacterWorkspaceLayout.test.tsx`; Modify: `src/app/router.tsx`, `src/app/route-paths.ts` | Consumes: `CharacterListResponse`, `CharacterDetailResponse`. Produces: `getCharacters({searchTerm,page,size})`, `getCharacter(characterId)`, list/workspace route | **TDD 적용:** `npm run test:run -- src/features/characters/tests/character-list.test.tsx src/layouts/CharacterWorkspaceLayout.test.tsx`; 기대 `exit 0`. 수동 확인: searchTerm request와 deep link/read-only 상태. | | `P3-T3` | Create: `src/features/characters/schemas/character-schema.ts`, `src/features/characters/validation/character-image-policy.ts`, `src/features/characters/pages/CharacterFormPage.tsx`, `src/features/characters/components/{CharacterForm,CharacterImageField}.tsx`, `src/features/characters/tests/{character-api.test.ts,character-form.test.tsx}`; Modify: `src/features/characters/api/character-api.ts` | Consumes: Character create/update multipart. Produces: `createCharacter`, `updateCharacter`, `deactivateCharacter`, form serializer | **TDD 적용:** `npm run test:run -- src/features/characters/tests/character-api.test.ts src/features/characters/tests/character-form.test.tsx`; 기대 `exit 0`. 수동 확인: 필수 image/systemPrompt, crop, 저장·목록 이동. | | `P3-T4` | Modify: `src/features/characters/pages/{CharacterListPage,CharacterDetailPage,CharacterFormPage}.tsx`, `src/features/characters/components/{CharacterList,CharacterListItem,CharacterProfile,CharacterForm,CharacterImageField}.tsx`; Test: `tests/e2e/character-workspace.spec.ts` | Consumes: P3-T2/T3 UI. Produces: viewport·keyboard capability evidence | **TDD 적용:** `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts`; 기대 지원 project 전부 통과. 수동 확인: 320/768/1280px, 200% zoom, keyboard, axe. | `P3-T2`~`P3-T4`는 각 row의 test에 가장 작은 실패 assertion을 먼저 추가해 RED를 확인하고, 최소 구현으로 같은 명령을 통과시킨 뒤 관련 feature test·typecheck·lint를 실행한다. 각 Task 마지막에는 RED, GREEN, REFACTOR와 수동 확인의 실제 결과를 `§7 검증 기록`에 누적한다. ### Task 3.1 Phase 계약 확인 **Goal 실행 `P3-T1`:** Character 구현 계약, mock scenario와 화면 component map을 확정한다. - **시작 조건:** `P2-GATE` 완료, PRD `CHAR-001~018`, `MOCK-001~009`와 OpenAPI Character 4개 operation/schema 확인. - **완료 증거:** 체크박스 전체, 제공 계약 또는 제외 결정의 세 문서 일치, 상태/action inventory. - **범위 밖:** 계약을 추정한 production adapter와 Character 화면 구현. - [ ] OpenAPI에서 목록 `searchTerm/page/size`, `data.totalCount/content`, item `id`, 상세 `characterUUID/originalWork`, mutation `data=null`을 contract fixture로 고정한다. - [ ] original work lookup endpoint, DTO, search/page를 확인하고, 미제공이면 network control을 제외한다. `originalWorkId` 미선택 serializer는 허용된 omit 또는 `null` 중 하나를 contract test로 고정한다. - [ ] OpenAPI 공통 400/401/403/404/405/406/415/500과 `ApiErrorResponse`를 fixture에 기록한다. Character 전용 message key는 미제공으로 표시하고 분기하지 않는다. - [ ] 목록·상세·form·workspace의 상태/action inventory를 작성하고 Page는 route/query/policy 조합, feature component는 Character 표시·입력 규칙, shared component는 Phase 1 contract를 사용하도록 component map을 확정한다. - [ ] 제공 계약 범위의 Character seed, loading·empty·error·success와 CRUD/deactivate browser handler 시나리오를 확정한다. 계약 미제공 original work network fixture는 만들지 않는다. ### Task 3.2 목록·선택·workspace **Goal 실행 `P3-T2`:** Character 목록·검색·선택과 URL 기반 workspace 복원을 완성한다. - **시작 조건:** `P3-T1` 완료. - **완료 증거:** 체크박스 전체, route/list/workspace test와 read-only/error 상태 검증 기록. - **범위 밖:** Character 생성·수정·비활성화 form. - [ ] Character 목록·생성 path에는 `characterId`가 없고 하위 resource route에만 선택한 `characterId`가 들어가는 contract test를 작성한다. - [ ] 목록 UI의 `search` 상태를 API `searchTerm`으로 직렬화하고 `page`, `size` URL query 보존, `data.content` 역직렬화와 loading·empty·error·retry test를 작성한다. - [ ] 목록 request에 `isActive`·`activeStatus`가 없고 client-side 활성 filter도 없는 contract test를 작성한다. active-only 보장은 외부 의존으로 남긴다. - [ ] Character 선택 시 URL의 `characterId`로 workspace에 진입하고 새로고침·deep link가 동작하는 test를 작성한다. - [ ] workspace header에 image, name, active 상태, `characterId`와 탭·breadcrumb를 표시한다. - [ ] 상세 성공 응답이 `isActive=false`이면 read-only 배너와 중앙 write policy로 모든 mutation 진입점을 차단한다. - [ ] 상세 400/404/500은 공통 오류 화면을 사용하고 비활성 ID 응답 정책을 client가 추정하지 않는다. - [ ] 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` 결정과 검증 기록. - **범위 밖:** 계약 미제공 original-work integration과 하위 도메인 mutation. - [ ] create multipart가 필수 `image`와 필수 `request` JSON part를 보내고 request에 `name`, `systemPrompt`, `description`을 포함하며 `isActive`, `externalCharacterId`를 포함하지 않는 test를 작성한다. - [ ] 일반 update는 `isActive`를 생략하고 soft delete만 `isActive=false`를 보내며 `true`를 보내지 않는 test를 작성한다. - [ ] create-only `region`을 수정 화면에서 읽기 전용으로 표시하고 update payload에 보내지 않는 test를 작성한다. - [ ] name·systemPrompt·description visible label, field error, 중복 제출 방지, dirty-form 이탈 확인을 test한다. - [ ] OpenAPI optional scalar와 tags·hobbies·values·goals·relationships·personalities·backgrounds·memories 반복 입력을 create/update schema에 맞게 직렬화하는 test를 작성한다. - [ ] Character image의 JPEG/PNG·10MB, `1:1`, 최대 800×800, no-upscale, crop 이동·zoom·reset·preview·취소·적용·keyboard 대안을 test한다. - [ ] crop 취소·교체 파일 미선택이 기존 image를 유지하고 기존 image 제거 UI는 없음을 test한다. - [ ] original work 계약이 제공됐다면 이름 검색 Combobox와 canonical 미선택 payload를 contract test로 고정한다. - [ ] 상세의 `characterUUID`는 읽기 전용으로 표시할 수 있지만 `externalCharacterId`로 이름을 바꾸지 않는다. 계약에 없는 creator member ID·nickname DTO/UI는 만들지 않고 creator 생성·동기화도 client가 수행하지 않는다. - [ ] create의 `data=null` 성공 후 목록을 무효화해 이동하고, update는 기존 `characterId`의 list/detail cache를 무효화한다. - [ ] 비활성화 AlertDialog가 영향·복원 미지원·hard delete 미지원을 설명하는 test를 작성한다. - [ ] soft delete 성공 후 목록 cache 무효화·재조회, 목록 이동과 성공 toast를 확인하고 상세에 머물지 않는다. 비활성 항목이 서버 결과에서 제외되는지는 active-only 계약 제공 후 server mode에서 검증한다. - [ ] 초기 Character form을 실제 페이지에서 확인한 뒤 `name`, `systemPrompt`, `description`과 tags·hobbies·values·goals·relationships·personalities·backgrounds·memories의 최대 길이·개수 권고안을 작성한다. backend 호환 확인 전에는 상한을 구현하지 않는다. - [ ] mock store가 create/update/deactivate 후 목록·상세를 같은 server response contract로 갱신하는 E2E를 작성한다. ### Task 3.4 Character 반응형·접근성 **Goal 실행 `P3-T4`:** Character viewport capability와 keyboard·zoom·axe 기준을 검증한다. - **시작 조건:** `P3-T2`, `P3-T3`의 활성 범위 완료. - **완료 증거:** 체크박스 전체, 320/768/1280px·200% zoom·keyboard·axe E2E 기록. - **범위 밖:** 다음 Phase 도메인과 모바일 mutation. - [ ] desktop/tablet에서는 전체 관리 action을 제공한다. - [ ] mobile에서는 목록·검색·상세만 제공하고 create/edit/deactivate route 직접 진입도 desktop 안내로 종료한다. - [ ] Table이 mobile Card로 바뀌어도 동일한 accessible name과 핵심 상태를 유지한다. - [ ] keyboard-only로 search → select → tabs → form → dialog를 완료한다. - [ ] 320/768/1280px, 200% zoom, axe critical·serious 0건을 Phase E2E에서 확인한다. ### Phase 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 -- tests/e2e/character-workspace.spec.ts 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와 일치하는지 본다. --- ## 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 반환 보장, `EXT-011` Audio 도메인 오류 message key, `EXT-010` backend 파일/container/codec 검증 계약, `EXT-009` optional P1 price 상한. status filter·답변 없는 status badge는 현재 범위에서 제외한다. **주요 Files:** - Create: `src/features/audio-contents/api/{audio-content-api,audio-content-theme-api,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캐릭터관리자웹/prd.md`, `docs/20260725_AI캐릭터관리자웹/plan-task.md`; Read: `docs/20260725_AI캐릭터관리자웹/api-contract.openapi.json`; Test: 없음 | Consumes: `AudioContent*`, `AudioTheme*` schema·5 Audio operation. Produces: list/detail/create/update/theme contract map과 screen inventory | **TDD 예외:** 외부 계약 조사 Task다. OpenAPI JSON parse와 `rg -n 'AUDIO-0(2[7-9]|3[0-3])'` 문서 추적 검사를 실행한다. 기대 `exit 0`. 수동 확인: status·seriesIds·수정 file 제외가 명시된다. | | `P4-T2` | Create: `src/features/audio-contents/api/audio-content-api.ts`, `src/features/audio-contents/model/types.ts`, `src/features/audio-contents/pages/{AudioContentListPage,AudioContentDetailPage}.tsx`, `src/features/audio-contents/components/{AudioContentList,AudioContentListItem,AudioContentSummary}.tsx`, `src/features/audio-contents/tests/{audio-list,audio-player}.test.tsx` | Consumes: `AudioContentListResponse`, `AudioContentDetailResponse`. Produces: `getAudioContents({characterId,search_word,page,size})`, `getAudioContent({characterId,contentId,timezone})`와 player UI | **TDD 적용:** `npm run test:run -- src/features/audio-contents/tests/audio-list.test.tsx src/features/audio-contents/tests/audio-player.test.tsx`; 기대 `exit 0`. 수동 확인: 2자 검색, timezone, 단일 재생·no-auto-refetch. | | `P4-T3` | Create: `src/features/audio-contents/api/{audio-content-theme-api,upload-audio-content}.ts`, `src/features/audio-contents/schemas/audio-content-schema.ts`, `src/features/audio-contents/validation/audio-cover-policy.ts`, `src/features/audio-contents/pages/AudioContentFormPage.tsx`, `src/features/audio-contents/components/{AudioContentForm,AudioContentThemeSelect,ReleaseScheduleField}.tsx`, `src/features/audio-contents/tests/{audio-contract.test.ts,audio-upload.test.ts,audio-form.test.tsx}`; Modify: `src/features/audio-contents/api/audio-content-api.ts` | Consumes: `AudioContentCreateRequest`, `AudioContentUpdateRequest`, `AudioContentTheme`. Produces: `createAudioContent`, `updateAudioContent`, `deactivateAudioContent`, upload adapter | **TDD 적용:** `npm run test:run -- src/features/audio-contents/tests/audio-contract.test.ts src/features/audio-contents/tests/audio-upload.test.ts src/features/audio-contents/tests/audio-form.test.tsx`; 기대 `exit 0`. 수동 확인: contentFile, local releaseDate, theme, 진행률·취소·재시도. | | `P4-T4` | Modify: `src/features/audio-contents/pages/{AudioContentListPage,AudioContentDetailPage,AudioContentFormPage}.tsx`, `src/features/audio-contents/components/{AudioContentList,AudioContentListItem,AudioContentSummary,AudioContentForm,AudioContentThemeSelect,ReleaseScheduleField}.tsx`; Test: `tests/e2e/audio-content.spec.ts` | Consumes: P4-T2/T3 UI. Produces: viewport·keyboard capability evidence | **TDD 적용:** `npm run e2e:mock -- tests/e2e/audio-content.spec.ts`; 기대 지원 project 전부 통과. 수동 확인: 320px player, 200% zoom, keyboard, axe. | `P4-T2`~`P4-T4`는 각 row의 focused test로 RED → GREEN → REFACTOR를 실행하고, 관련 feature test·typecheck·lint 결과와 수동 확인을 `§7 검증 기록`에 누적한다. ### Task 4.1 Phase 계약 확인 **Goal 실행 `P4-T1`:** Audio 오류·price·theme·상태 계약, mock scenario와 component map을 확정한다. - **시작 조건:** `P3-T2`, `P2-GATE` 완료, PRD `AUDIO-001~033`, `MOCK-001~009`와 OpenAPI Audio·theme operation/schema 확인. - **완료 증거:** 체크박스 전체, contract fixture와 상태/action inventory의 세 문서 일치. - **범위 밖:** 오류 status/key 또는 price 상한 추정과 Audio UI 구현. - [ ] OpenAPI 공통 오류 status·shape를 fixture에 기록하고 Audio 전용 message key와 backend container·codec 오류 계약은 미제공으로 표시한다. 정확한 fixture를 추정하지 않는다. - [ ] 오디오 테마 목록 `GET /api/v2/admin/ai-characters/audio-content-themes`가 query/body 없이 호출되고 `data[]`의 `id`, `theme`, `image`를 반환하는 contract fixture를 기록한다. - [ ] price 최대값이 제공되면 schema와 경계 test를 추가하고, 없으면 상한을 만들지 않는다. - [ ] 목록 `search_word/page/size`, `data.totalCount/items`, 상세 필수 `timezone`, 생성 `contentFile/coverImage/request`, 생성 `data.contentId`, 수정 `data=null`을 contract fixture로 고정한다. - [ ] status query·status field가 없음을 고정하고 client status filter·status enum을 만들지 않는다. - [ ] 목록·상세·player·form/upload의 상태/action inventory를 작성하고 Page는 route/query/policy 조합, feature component는 Audio 규칙, shared component는 Phase 1 contract를 사용하도록 component map을 확정한다. - [ ] 제공 계약 범위의 Audio seed와 목록·상세·player·theme·upload CRUD browser handler 시나리오를 확정한다. ### Task 4.2 목록·상세·player **Goal 실행 `P4-T2`:** Audio 목록·상세 복원과 안전한 단일 재생 흐름을 완성한다. - **시작 조건:** `P4-T1` 완료. - **완료 증거:** 체크박스 전체, list/detail/player test와 signed URL 비기록·no-auto-refetch 기록. - **범위 밖:** 생성·수정·upload form. - [ ] UI 검색어가 2자 이상일 때만 API `search_word`로 직렬화되고 `page`, `size` URL 상태와 `data.items` 역직렬화, loading·empty·error·retry가 동작하는 test를 작성한다. - [ ] status query·활성 query·client-side status/active filter request가 0회임을 test한다. - [ ] Audio detail route의 직접 진입과 새로고침에서 `timezone=Asia/Seoul`을 보내 같은 resource를 복원하는 test를 작성한다. - [ ] 목록과 상세가 Phase 1 `AdminAudioPlayer`를 조합하고 play/pause, seek, current/duration, volume, speed, keyboard를 지원하는 integration test를 작성한다. - [ ] 한 player 재생 시 기존 player가 정지되고 명시적 download button이 없음을 test한다. - [ ] media error를 signed URL 만료로 추정하지 않고 일반 오류·수동 재시도·페이지 새로고침 안내를 표시한다. - [ ] media error만으로 목록/detail GET과 `play()`가 자동 재호출되지 않는 test를 작성한다. - [ ] signed URL이 log·storage·분석 event로 전달되지 않는 test를 작성한다. - [ ] 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 상한. - [ ] 생성 multipart의 `contentFile`, `coverImage`, `request`가 필수이고 수정에는 optional `coverImage`와 `request`만 있으며 content file 교체 part·UI가 없음을 test한다. - [ ] Audio cover가 Phase 1 `FileField`·`ImageCropDialog`의 JPEG/PNG·10MB, `1:1`, 최대 800px, no-upscale profile을 조합하는 test를 작성한다. - [ ] MP3/AAC/M4A 허용, WAV 거부, extension/MIME 조합을 test한다. - [ ] `.m4a + audio/x-m4a`만 호환 조합으로 허용하고 실제 container·codec 판정은 server 책임으로 둔다. - [ ] `1,024,000,000 bytes` 허용, `1,024,000,001 bytes` 거부 경계 test를 작성한다. - [ ] price는 0 이상 정수 “캔”으로 입력·format한다. - [ ] 생성 form은 오디오 테마 목록을 불러와 visible label이 있는 선택 UI를 제공하고, 미선택 제출을 차단하며 선택한 `themeId`를 create payload에 포함하는 test를 작성한다. - [ ] 즉시 공개 기본값은 날짜 입력을 비활성화·초기화하고 `releaseDate=null`, `timezone="Asia/Seoul"`을 보낸다. - [ ] 예약 공개는 미래 Asia/Seoul 시각만 받고 `yyyy-MM-dd HH:mm` 문자열과 `timezone="Asia/Seoul"`을 보내며 UTC `Z`로 변환하지 않는 test를 작성한다. - [ ] 수정 form은 계약에 없는 release schedule·theme·series·content file 변경 control을 만들지 않고 기존 값을 읽기 전용으로 표시한다. - [ ] create request에 필수 `title`, `detail`, `tags`, `price`, 유효한 `themeId`가 있고 `status`, `isActive`, `seriesIds`가 없음을 contract test로 고정한다. - [ ] create optional purchase/limited/adult/preview/point/comment/detail/language field의 enum·type·OpenAPI default와 serializer를 test한다. - [ ] update request는 `title`, `detail`, `tags`, `price`, `isAdult`, `isActive`, `isPointAvailable`, `isCommentAvailable` 이외 field를 보내지 않고 soft delete에만 `isActive=false`를 보낸다. - [ ] upload 진행률, AbortController 취소, 전체 재시도, 실패 후 form/file 상태 보존을 test한다. - [ ] 415 server 오류를 field 안내로 보존하고 resumable upload는 만들지 않는다. - [ ] create 성공의 `data.contentId`로 상세에 이동하고, update/soft delete의 `data=null` 성공은 기존 ID cache를 무효화한다. soft delete 후 목록 이동과 toast를 제공하며 active-only 제거는 외부 계약 제공 후 server mode에서 검증한다. - [ ] 초기 Audio form을 실제 페이지에서 확인한 뒤 `title`, `detail`, `tags`의 최대 길이 권고안을 작성한다. backend 호환 확인 전에는 상한을 구현하지 않는다. - [ ] mock handler가 multipart request contract를 검증하고 create/update/deactivate 후 같은 store의 list/detail을 갱신하는 E2E를 작성한다. ### Task 4.4 Audio 반응형·접근성 **Goal 실행 `P4-T4`:** Audio viewport capability와 player/form 접근성 기준을 검증한다. - **시작 조건:** `P4-T2`, `P4-T3`의 활성 범위 완료. - **완료 증거:** 체크박스 전체, 320px·keyboard·200% zoom·axe E2E 기록. - **범위 밖:** 모바일 create/edit/deactivate/upload. - [ ] mobile에서는 목록·상세·player만 제공하고 create/edit/deactivate/upload는 숨김이 아닌 route capability로 차단한다. - [ ] desktop/tablet에서는 모든 form·upload action을 제공한다. - [ ] 320px에서 player control, error text, 긴 title이 overflow하지 않는 E2E를 작성한다. - [ ] keyboard-only player/form, 200% zoom, axe critical·serious 0건을 확인한다. ### Phase 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 요청·화면에 일치하는지 본다. --- ## 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·edit DTO 외부 의존은 제공 또는 명시적 후속/제외 상태로 종결. - **실행 순서:** 계약 확인 → 목록/상세 → 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`)은 유효한 `genreId`가 필요한 생성 flow를 차단한다. 상세의 표시 문자열을 update enum/ID로 안전하게 복원할 `EXT-003` edit DTO(`SERIES-017`), `EXT-007` active-only 반환 보장과 `EXT-011` 도메인별 오류 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/edit DTO 차단과 제공된 link/order 범위가 분리된다. | | `P5-T2` | Create: `src/features/series/api/series-api.ts`, `src/features/series/model/types.ts`, `src/features/series/pages/{SeriesListPage,SeriesDetailPage}.tsx`, `src/features/series/components/{SeriesList,SeriesListItem,SeriesSummary}.tsx`, `src/features/series/tests/series-contract.test.ts` | Consumes: `SeriesListResponse`, `SeriesDetailResponse`. Produces: `getSeries({characterId,page,size})`, `getSeriesDetail({characterId,seriesId})`와 read-only 조회 UI | **TDD 적용:** `npm run test:run -- src/features/series/tests/series-contract.test.ts`; 기대 `exit 0`. 수동 확인: 목록 enum과 상세 표시 문자열, 직접 링크 조회. | | `P5-T3` | genre/edit 계약 제공 후 Create: `src/features/series/schemas/series-schema.ts`, `src/features/series/validation/series-image-policy.ts`, `src/features/series/pages/SeriesFormPage.tsx`, `src/features/series/components/{SeriesForm,PublishedDaysField,GenreCombobox}.tsx`, `src/features/series/tests/series-form.test.tsx`; Modify: `src/features/series/api/series-api.ts`, `src/features/series/tests/series-contract.test.ts` | Consumes: create/update multipart와 genre/edit DTO. Produces: `createSeries`, `updateSeries`, `deactivateSeries` | **TDD 적용:** genre lookup·edit DTO 제공 후 `npm run test:run -- src/features/series/tests/series-contract.test.ts src/features/series/tests/series-form.test.tsx`; 기대 `exit 0`. 수동 확인: keyword/image/state와 직접 edit 초기화. | | `P5-T4` | Create: `src/features/series/pages/SeriesOrderPage.tsx`, `src/features/series/components/{SeriesContents,SeriesOrderList}.tsx`, `src/features/series/tests/{series-contents,series-order}.test.tsx`; Modify: `src/features/series/api/series-api.ts`, `src/features/series/pages/SeriesDetailPage.tsx` | Consumes: `SeriesContentListResponse`, `SeriesContentSearchItem`, `SeriesContentAddRequest`, `SeriesOrderUpdateRequest`. Produces: `searchUnlinkedContents`, `addSeriesContents`, `removeSeriesContent`, `updateSeriesOrder` | **TDD 적용:** `npm run test:run -- src/features/series/tests/series-contents.test.tsx src/features/series/tests/series-order.test.tsx`; 기대 `exit 0`. 수동 확인: contentIdList·ids request와 keyboard reorder. | | `P5-T5` | Modify: `src/features/series/pages/{SeriesListPage,SeriesDetailPage,SeriesFormPage,SeriesOrderPage}.tsx`, `src/features/series/components/{SeriesList,SeriesListItem,SeriesSummary,SeriesForm,PublishedDaysField,GenreCombobox,SeriesContents,SeriesOrderList}.tsx`; Test: `tests/e2e/series.spec.ts` | Consumes: P5-T2~T4 활성 UI. Produces: viewport·keyboard capability evidence | **TDD 적용:** `npm run e2e:mock -- tests/e2e/series.spec.ts`; 기대 활성 계약 범위 통과. 수동 확인: 320px 조회, 200% zoom, keyboard, axe. | `P5-T2`~`P5-T5`는 각 row의 focused test로 RED → GREEN → REFACTOR를 실행한다. 외부 계약 때문에 RED test의 기대 동작 자체를 정할 수 없으면 test를 skip하지 않고 해당 network 범위를 시작하지 않으며, 대체 검증과 남은 조건을 `§7 검증 기록`에 남긴다. ### Task 5.1 Phase 계약 확인 **Goal 실행 `P5-T1`:** Series genre·연결 후보·전체 순서·오류 계약, mock scenario와 component map을 확정한다. - **시작 조건:** `P4-T2` 완료, PRD `SERIES-001~018`, `MOCK-001~009`와 OpenAPI Series 9개 operation/schema 확인. - **완료 증거:** 체크박스 전체, 제공 계약 또는 제외 결정의 세 문서 일치, 상태/action inventory. - **범위 밖:** 계약 없는 genre lookup·edit DTO·active-only·도메인별 오류 동작의 추정 구현. - [ ] genre lookup endpoint·DTO·search/page 계약을 기록한다. - [ ] 연결 후보 `GET .../contents/search?search_word=...`, 연결 `{contentIdList}`, 해제 body 없는 DELETE, 전체 순서 `{ids}` 계약을 기록한다. - [ ] 목록 `data.totalCount/items`와 `page/size`로 전체 Series page를 누락 없이 읽는 방식을 기록한다. 도메인별 누락 ID·동시 변경 오류 key는 제공되지 않았음을 외부 의존으로 남긴다. - [ ] 계약이 없는 genre lookup과 active-only 보장은 추측 구현하지 않고 영향 범위를 문서에서 먼저 확인한다. - [ ] 목록·상세·form·연결·순서 화면의 상태/action inventory를 작성하고 Page는 route/query/policy 조합, feature component는 Series 규칙, shared component는 Phase 1 contract를 사용하도록 component map을 확정한다. - [ ] 제공 계약 범위만 Series 목록·상세·연결·순서 browser fixture로 만들고 genre·edit DTO·active-only·충돌 계약 미제공 부분은 mock에서도 추정하지 않는다. ### Task 5.2 Series 목록·상세 조회 **Goal 실행 `P5-T2`:** OpenAPI가 제공한 Series 목록·상세 조회와 서로 다른 응답 DTO 표시 규칙을 완성한다. - **시작 조건:** `P5-T1` 완료. - **완료 증거:** 체크박스 전체, list/detail contract·route·state test와 검증 기록. - **범위 밖:** Series 생성·수정·비활성화와 Audio 연결·해제·전체 순서 저장. - [ ] list가 `page`, `size`만 보내고 `data.totalCount/items`를 소비하며 활성 query·client 활성 filter 없이 loading·empty·error·retry를 제공하는 test를 작성한다. - [ ] Series detail route의 직접 진입과 새로고침에서 같은 resource를 복원하는 test를 작성한다. - [ ] 목록의 enum field와 상세의 표시용 `publishedDaysOfWeek`, `genre`, `keywords`, 한국어 `state` 문자열을 각 응답 DTO 그대로 표시하고 서로 역변환하지 않는 test를 작성한다. - [ ] 제공된 목록·상세 계약만 mock handler로 만들고 loading·empty·error·retry와 직접 링크 조회를 확인한다. ### Task 5.3 Series 생성·수정·비활성화 **Goal 실행 `P5-T3`:** 계약 제공 후 Series form, enum·요일·image와 soft delete 규칙을 완성한다. - **시작 조건:** `P5-T2` 완료. genre lookup과 직접 링크 수정 form의 `genreId`·요일 enum·state enum을 제공하는 edit DTO/mapping 계약이 제공됨. - **완료 증거:** 체크박스 전체, contract/form/image/deactivate test와 검증 기록. - **범위 밖:** Audio 연결·해제와 전체 순서 저장. - [ ] enum은 `PROCEEDING | SUSPEND | COMPLETE`, 요일은 `SUN~SAT | RANDOM`만 허용한다. - [ ] 생성 multipart에 필수 `image`와 `request`가 있고 request의 필수 `title`, `introduction`, `publishedDaysOfWeek`, `keyword`를 보내며 `state`, `isActive`, `keywords`가 없음을 test한다. - [ ] 수정에서 state 미선택은 key 생략, 선택은 유효 enum만 전송하고 `null`은 보내지 않는다. - [ ] `RANDOM`은 단독, 실제 요일은 하나 이상이어야 하는 schema·UI test를 작성한다. - [ ] genre 이름 검색 후 유효한 `genreId`만 전송하는 Combobox를 test한다. OpenAPI binding 기본값 `0`은 선택값으로 허용하지 않는다. - [ ] Series image JPEG/PNG·10MB, `210:297`, `height=round(width×297÷210)`, 최대 1000×1414, 1px 오차, no-upscale을 test한다. - [ ] edit DTO의 `genreId`·요일 enum·state enum으로 직접 링크 form을 초기화하고 상세의 표시용 문자열을 update enum/ID로 역변환하지 않는 test를 작성한다. - [ ] create-only `keyword`를 수정 화면에서 읽기 전용으로 표시하고 update payload에 보내지 않는 test를 작성한다. - [ ] create/update/soft delete의 `data=null`을 처리하고 일반 update와 soft delete의 `isActive` 규칙, 목록 재조회·이동·toast를 test한다. - [ ] mock store로 Series CRUD 후 list enum과 detail 표시 문자열의 서로 다른 DTO가 일관되게 갱신되는 UI를 확인한다. - [ ] 초기 Series form을 실제 페이지에서 확인한 뒤 `title`, `introduction`, `keyword`, `writer`, `studio`, `publishedDaysOfWeek`의 최대 길이·개수 권고안을 작성한다. backend 호환 확인 전에는 상한을 구현하지 않는다. ### Task 5.4 Audio 연결·해제·전체 순서 **Goal 실행 `P5-T4`:** Series Audio 연결·해제와 서버가 반환한 Series 전체 순서를 안전하게 관리한다. - **시작 조건:** `P5-T1`, `P5-T2` 완료. - **완료 증거:** 체크박스 전체, link/unlink/reorder contract·interaction test, 충돌 보존, `OQ-009` 결정 기록. - **범위 밖:** 계약 없는 active-only·도메인별 충돌 동작의 추정 구현. - [ ] 현재 연결 Audio 목록은 `page/size`와 `data.totalCount/items`를 사용하고 제공되지 않은 search query를 보내지 않으며 상세 cache를 동기화한다. - [ ] 후보는 `GET .../contents/search?search_word=...` 결과만 사용하고 이미 연결된 항목을 중복 선택하지 않는다. - [ ] 연결 POST는 `{ contentIdList }`, 해제 DELETE는 body 없음임을 contract test로 고정한다. - [ ] 연결 해제 전 대상 title과 영향을 AlertDialog로 확인한다. - [ ] 순서 mode는 `totalCount`와 page/size로 Series 전체를 읽고 최종 순서의 모든 ID를 `{ ids }`로 한 번에 보낸다. - [ ] drag-and-drop과 동일한 결과를 keyboard·위/아래 button으로 만들 수 있는 test를 작성한다. - [ ] server의 누락 ID·동시 충돌 오류에서 기존 화면 순서를 보존하고 재조회/재시도 안내를 제공한다. - [ ] 초기 연결·순서 UI를 실제 페이지에서 확인한 뒤 `contentIdList`, `ids`의 최대 개수 권고안을 작성한다. backend 호환 확인 전에는 상한을 구현하지 않는다. - [ ] 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. - [ ] mobile은 목록·상세·연결 콘텐츠 조회만 제공하고 CRUD·연결·순서 action을 route capability로 차단한다. - [ ] desktop/tablet에서 전체 관리 흐름을 제공한다. - [ ] keyboard-only 요일·genre·연결·정렬, 320px 조회, 200% zoom, axe critical·serious 0건을 확인한다. ### 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·edit DTO 제공 후에는 필수 image와 `keyword`로 Series 생성 → 수정 → soft delete UI까지 통과하고 잘못된 enum·`keywords/contentIds/seriesIds` payload가 생성되지 않는다. 미제공 계약 범위는 대기로 유지하되 완료된 조회·연결·순서 상태를 되돌리지 않는다. **수동 확인:** 현재 계약으로 320px 조회와 desktop/tablet 연결·keyboard reorder를 확인한다. genre/edit DTO 제공 후 생성·수정 form도 확인하고, 표시용 상세 문자열이 update payload로 역변환되지 않는지 network에서 본다. --- ## Phase 6. Community vertical slice **목표:** 별도 상세 route/GET 없이 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 또는 종료 규칙, `EXT-011` Community 오류 message key, `EXT-010` backend file 검증 계약, `EXT-009` optional P1 price 상한. 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/schemas/community-post-schema.ts` - Create: `src/features/community-posts/validation/community-media-policy.ts` - Create: `src/features/community-posts/pages/CommunityPostListPage.tsx` - Create: `src/features/community-posts/components/{CommunityPostList,CommunityPostListItem,CommunityPostForm,CommunityPostSheet}.tsx` - Create: `src/features/community-posts/tests/community-contract.test.ts` - Create: `src/features/community-posts/tests/{community-list,community-sheet}.test.tsx` - Create: `tests/e2e/community-post.spec.ts` #### Phase 6 Task 실행 계약 | Goal | Files | Interfaces | TDD·검증 기준 | |---|---|---|---| | `P6-T1` | Modify: `docs/20260725_AI캐릭터관리자웹/prd.md`, `docs/20260725_AI캐릭터관리자웹/plan-task.md`; Read: `docs/20260725_AI캐릭터관리자웹/api-contract.openapi.json`; Test: 없음 | Consumes: `CommunityPost*` schema·3 operation. Produces: list/create/update contract map, 외부 의존과 Sheet inventory | **TDD 예외:** 외부 계약 조사 Task다. OpenAPI JSON parse와 `rg -n 'COMMUNITY-01[2-5]'` 문서 추적 검사를 실행한다. 기대 `exit 0`. 수동 확인: timezone·배열 data·null mutation·미제공 pagination이 분리된다. | | `P6-T2` | Create: `src/features/community-posts/api/community-post-api.ts`, `src/features/community-posts/model/types.ts`, `src/features/community-posts/pages/CommunityPostListPage.tsx`, `src/features/community-posts/components/{CommunityPostList,CommunityPostListItem,CommunityPostSheet}.tsx`, `src/features/community-posts/tests/{community-contract.test.ts,community-list.test.tsx,community-sheet.test.tsx}` | Consumes: `CommunityPostListApiResponse`, update multipart. Produces: `getCommunityPosts({characterId,timezone,page,size})`, `updateCommunityPost`, collection Sheet cache policy | **TDD 적용:** `npm run test:run -- src/features/community-posts/tests/community-contract.test.ts src/features/community-posts/tests/community-list.test.tsx src/features/community-posts/tests/community-sheet.test.tsx`; 기대 `exit 0`. 수동 확인: detail GET 0회, pin/deactivate 후 refetch. | | `P6-T3` | Create: `src/features/community-posts/schemas/community-post-schema.ts`, `src/features/community-posts/validation/community-media-policy.ts`, `src/features/community-posts/components/CommunityPostForm.tsx`; Modify: `src/features/community-posts/api/community-post-api.ts`, `src/features/community-posts/components/CommunityPostSheet.tsx`, `src/features/community-posts/tests/{community-contract.test.ts,community-sheet.test.tsx}` | Consumes: `CommunityPostCreateRequest`, `CommunityPostUpdateRequest`, multipart part names. Produces: `createCommunityPost`, form serializer와 media policy | **TDD 적용:** `npm run test:run -- src/features/community-posts/tests/community-contract.test.ts src/features/community-posts/tests/community-sheet.test.tsx src/shared/validation`; 기대 `exit 0`. 수동 확인: postImage/audioFile, update audio/price 없음, audioUrl 재생. | | `P6-T4` | Modify: `src/features/community-posts/pages/CommunityPostListPage.tsx`, `src/features/community-posts/components/{CommunityPostList,CommunityPostListItem,CommunityPostForm,CommunityPostSheet}.tsx`; Test: `tests/e2e/community-post.spec.ts` | Consumes: P6-T2/T3 UI. Produces: viewport·Sheet focus capability evidence | **TDD 적용:** `npm run e2e:mock -- tests/e2e/community-post.spec.ts`; 기대 지원 project 전부 통과. 수동 확인: 320px, focus trap/복귀, 200% zoom, axe. | `P6-T2`~`P6-T4`는 각 row의 focused test로 RED → GREEN → REFACTOR를 실행하고 관련 feature test·typecheck·lint 결과와 수동 확인을 `§7 검증 기록`에 누적한다. ### Task 6.1 Phase 계약 확인 **Goal 실행 `P6-T1`:** Community 오류·media·price 계약, mock scenario와 목록/Sheet component map을 확정한다. - **시작 조건:** `P2-GATE`, `P4-T2` 완료, PRD `COMMUNITY-001~015`, `MOCK-001~009`와 OpenAPI Community 3개 operation/schema 확인. - **완료 증거:** 체크박스 전체, contract fixture와 상태/action inventory의 세 문서 일치. - **범위 밖:** price 상한·오류 key 추정과 Comments 구현. - [ ] OpenAPI 공통 오류 status·shape를 fixture에 기록하고 Community 전용 message key와 media upload 오류 계약은 미제공으로 표시한다. 정확한 fixture를 추정하지 않는다. - [ ] price 최대값이 제공되면 Audio와 같은 정책으로 갱신하고, 없으면 0 이상 정수만 유지한다. - [ ] 목록 필수 `timezone`, `page/size`, 배열 `data`, 생성 `audioFile/postImage/request`, 수정 `postImage/request`, mutation `data=null`을 contract fixture로 고정한다. - [ ] 목록에 total/hasNext와 `isActive/fixedAtUtc`가 없음을 기록하고 해당 값을 fixture에서 추가하지 않는다. - [ ] 목록·Sheet·form/media의 상태/action inventory를 작성하고 Page는 collection query/policy 조합, feature component는 Community 규칙, shared component는 Phase 1 contract를 사용하도록 component map을 확정한다. - [ ] Community active list, Sheet, pin, media와 CRUD의 deterministic browser fixture 시나리오를 확정한다. ### Task 6.2 목록·collection Sheet **Goal 실행 `P6-T2`:** 전용 detail route/GET 없는 목록과 collection Sheet mutation 흐름을 완성한다. - **시작 조건:** `P6-T1` 완료. - **완료 증거:** 체크박스 전체, list/Sheet/router/cache/pin/deactivate test와 detail GET 0회 기록. - **범위 밖:** 댓글과 제공 계약에 없는 Community 검색. - [ ] 목록이 필수 `timezone=Asia/Seoul`과 `page/size`를 보내고 배열 `data`를 소비하며 loading·empty·error·retry와 URL query 보존을 제공하는지 test한다. 제공 계약에 없는 Community `search`, total, hasNext를 만들지 않는다. - [ ] 목록 item을 source로 Sheet를 열고 전용 detail GET을 0회 호출하는 test를 작성한다. - [ ] `/community-posts/:postId`, `/edit` route가 존재하지 않는 router test를 작성한다. - [ ] Sheet의 조회·수정·고정/해제·비활성화가 목록 cache와 같은 server response를 사용한다. - [ ] 고정/해제 후 mutation `data=null`을 처리하고 목록을 재조회해 `isFixed`를 갱신한다. 계약에 없는 `fixedAtUtc`는 표시하지 않는다. - [ ] soft delete request가 `isActive=false`, `isFixed=false`를 보내고 성공 `data=null`을 처리하는 contract test를 작성한다. - [ ] soft delete 성공 시 Sheet 종료, 목록 재조회와 성공 toast를 확인한다. 비활성 항목 제거는 active-only 계약 제공 후 server mode에서 검증한다. - [ ] mock mode도 전용 detail GET 없이 list store만으로 Sheet와 pin/deactivate 최종 UI를 갱신한다. ### Task 6.3 게시글 form·첨부 media **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. - [ ] 생성 payload에 `isActive`가 없고 일반 update/soft delete가 공통 `isActive` 규칙을 지키는 test를 작성한다. - [ ] create의 필수 content·isCommentAvailable·isAdult, optional price와 multipart `postImage/audioFile`을 test하고 생성 request에 `isFixed`가 없음을 고정한다. - [ ] update는 optional `postImage`와 content·isCommentAvailable·isAdult·isActive·isFixed만 보내며 price·audioFile 교체 UI/request가 없음을 test한다. - [ ] JPEG/PNG는 자유 ratio crop·최대 800px·no-upscale을 적용한다. - [ ] GIF는 Community에서만 허용하고 crop Dialog/canvas/re-encode 없이 원본 ratio·animation을 유지한다. - [ ] GIF 원본 width 800px은 허용하고 801px은 제출 전에 거부한다. - [ ] 첨부 Audio는 Phase 1의 공통 audio file policy와 `FileField`를 Phase 4와 동일하게 조합해 MP3/AAC/M4A, x-m4a, `1,024,000,000 bytes`, WAV 거부 규칙을 재사용한다. - [ ] 첨부 Audio가 있으면 목록 Card/row와 Sheet에 공통 player를 렌더링한다. - [ ] media error가 detail/list refetch·URL 재발급·자동 play를 발생시키지 않는다. - [ ] 사용자 새로고침이나 mutation cache invalidation으로 목록이 정상 재조회된 때만 새 `audioUrl`을 사용한다. - [ ] 초기 Community Sheet/form을 실제 페이지에서 확인한 뒤 `content`의 최대 길이 권고안을 작성한다. backend 호환 확인 전에는 상한을 구현하지 않는다. - [ ] 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 기록. - **범위 밖:** 모바일 등록·수정·고정·비활성화. - [ ] mobile은 목록·Sheet 조회·첨부 재생만 제공하고 등록·수정·고정·비활성화를 route/action policy로 차단한다. - [ ] desktop/tablet에는 전체 관리 흐름을 제공한다. - [ ] 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-post.spec.ts npm run e2e -- tests/e2e/community-post.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로 복귀하는지 본다. --- ## 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. - [ ] 목록 `page/size`, `data.fanTalkCount/fanTalks/page/size/hasNext`, item `creatorReplies`와 답변 POST `{content}`·성공 DTO를 contract fixture로 고정한다. - [ ] 별도 상세·답변 수정 endpoint, answer filter와 sort query가 없음을 기록하고 임시 route·query를 만들지 않는다. - [ ] 답변 1개를 server가 원자적으로 강제하는 방식과 중복 생성 비2xx status/message key를 기록한다. - [ ] 미제공 범위에는 임시 endpoint·DTO·production adapter를 만들지 않고 후속 재개 조건을 문서화한다. - [ ] 목록·reply Sheet/form의 상태/action inventory를 작성하고 Page는 route/query/policy 조합, feature component는 FanTalk 규칙, shared component는 Phase 1 contract를 사용하도록 component map을 확정한다. - [ ] 제공된 목록·답변 생성 범위만 browser fixture로 만들고 상세·수정·filter/sort·유일성 오류는 mock에서도 추정하지 않는다. ### Task 7.2 목록·답변 생성 **Goal 실행 `P7-T2`:** backend 순서를 유지하는 목록과 답변 1회 생성 흐름을 완성한다. - **시작 조건:** `P7-T1` 완료. - **완료 증거:** 체크박스 전체, list/reply contract·UI test, 중복 제출 차단·오류 복구, OQ-009 후속 검토 기록. - **범위 밖:** 별도 상세, 답변 수정·삭제·두 번째 답변, 전체 결과 filter/sort와 계약 없는 network 동작. - [ ] 목록은 `page`, `size`를 URL에 보존하고 backend 반환 순서와 `hasNext`를 사용한다. 전체/미답변/답변 완료 filter와 client 재정렬은 만들지 않는다. - [ ] loading·empty·error·retry와 목록 새로고침을 test하고 `/fan-talks/:fanTalkId` route·상세 GET이 0건임을 검증한다. - [ ] 목록 item의 `creatorReplies`가 비어 있을 때만 POST form을 표시하고 답변이 있으면 읽기 전용으로 표시하며 edit/delete UI는 만들지 않는다. - [ ] 빠른 두 번 제출에도 POST가 한 번만 호출되는 test를 작성한다. - [ ] 답변 POST 성공 DTO의 `fanTalkId`, `replyId`, `creatorMemberId`, `content`, `createdAtUtc`를 반영하고 해당 목록 page를 재조회한다. - [ ] server 중복 오류를 받으면 현재 목록 page를 재조회해 `creatorReplies`를 갱신하고 status/key를 추정 분기하지 않는다. - [ ] 저장 중 중복 제출 차단, visible label, 오류 연결, 성공 live feedback을 test한다. - [ ] 초기 FanTalk reply form을 실제 페이지에서 확인한 뒤 `content` 최대 길이 권고안을 작성한다. backend 호환 확인 전에는 상한을 구현하지 않는다. - [ ] 제공 계약 범위의 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별 기능 축소와 답변 삭제. - [ ] desktop/tablet/mobile 모두 목록 조회·답변 작성을 제공하고 수정 action은 제공하지 않는다. - [ ] 320px에서 keyboard가 reply input/submit을 가리지 않는 E2E를 작성한다. - [ ] 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가 발생하지 않는지 본다. --- ## 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`. OpenAPI의 Audio 상세 `commentList`와 Community 목록 `firstComment`는 읽기용 요약일 뿐 CRUD 계약이 아니다. Audio·Community 댓글 목록/작성/수정/soft delete endpoint·DTO, 2단계 강제와 fan 댓글 삭제 권한 오류가 제공되기 전에는 이 Phase 전체를 추측 구현하지 않는다. **주요 Files:** - Create: `src/features/comments/api/comment-api.ts` - Create: `src/features/comments/model/{types,comment-target}.ts` - Create: `src/features/comments/schemas/comment-schema.ts` - Create: `src/features/comments/components/{CommentThread,CommentForm,CommentActions,CommunityPostCommentsSheet}.tsx` - Create: `src/features/comments/tests/comment-contract.test.ts` - Create: `src/features/comments/tests/{comment-thread,comment-permissions}.test.tsx` - Create: `tests/e2e/comments.spec.ts` - Modify: `AudioContentDetailPage.tsx`, `CommunityPostSheet.tsx` #### 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 완료 주장. - [ ] Audio·Community target별 endpoint, query, DTO, page, 작성/수정/soft delete 응답을 기록한다. - [ ] root/direct reply 정확히 2단계인 server rule과 fan content 삭제 권한 오류 status/message key를 기록한다. - [ ] 계약이 없으면 target endpoint를 이름만 보고 추정하거나 client-only permission을 완료로 간주하지 않는다. - [ ] Audio detail·Community Sheet 진입별 thread/form/action inventory를 작성하고 host Page/Sheet는 target·query 조합, Comments component는 thread·permission 규칙, shared component는 Phase 1 contract를 사용하도록 component map을 확정한다. - [ ] 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. - [ ] UI target은 Audio와 Community를 명시적으로 구분하고 각 제공 endpoint로만 요청하는 contract test를 작성한다. - [ ] Community 상세 GET 없이 목록 item의 `characterId/postId`로 Comments Sheet를 연다. - [ ] Comments Sheet 종료 후 Community page·scroll 상태를 보존한다. - [ ] root와 direct reply만 렌더링하고 reply에는 reply action이 없음을 test한다. - [ ] long content, loading·empty·error·retry, page 갱신을 양 target에서 test한다. ### Task 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. - [ ] AI Character 작성 root/reply에는 edit·soft delete를 제공한다. - [ ] fan 작성 root/reply에는 edit를 제공하지 않고 운영 soft delete만 제공한다. - [ ] fan edit request는 type과 UI 양쪽에서 생성할 수 없음을 test한다. - [ ] delete 전 대상과 영향을 확인하고 server 계약에 따라 tombstone 또는 목록 갱신을 적용한다. - [ ] Character workspace read-only 정책이 모든 comment mutation도 차단하는 test를 작성한다. - [ ] 중복 제출, server permission 오류, session 401/403이 공통 정책을 따르는지 test한다. - [ ] 댓글 CRUD 계약 제공 후 초기 Comment thread/form을 실제 페이지에서 확인하고 `content` 최대 길이 권고안을 작성한다. backend 호환 확인 전에는 상한을 구현하지 않는다. - [ ] 계약 제공 후 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 기록. - **범위 밖:** 모바일 기능 축소. - [ ] desktop/tablet/mobile 모두 조회·작성·수정·soft delete를 제공한다. - [ ] 320px에서 긴 댓글, reply indentation, action menu, keyboard 입력이 overflow하지 않는 E2E를 작성한다. - [ ] keyboard-only root/reply 작성·수정·delete dialog, focus 복귀, 200% zoom, axe critical·serious 0건을 확인한다. ### Phase 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건인지 확인한다. --- ## 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/server 전체 자동 Gate 0 failure/0 error와 수동 검증 기록. - **실행 순서:** 교차 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. - [ ] login → Character select → Audio immediate/scheduled create/play → Series link/order → Community Sheet → FanTalk list/reply → Comments 중 계약이 제공된 활성 범위 journey를 검증한다. - [ ] 400/401/403/404/405/406/415/500 fixture가 공통 한국어 message와 올바른 route/session 처리를 하는지 검증한다. - [ ] Character·Audio·Series soft delete는 목록 이동, Community soft delete는 Sheet 종료·목록 제거로 끝나는지 검증한다. - [ ] inactive Character workspace에서 모든 하위 mutation request가 0건인지 검증한다. - [ ] media error로 Audio/Community GET·URL 재발급·자동 `play()`가 발생하지 않는지 검증한다. - [ ] 생성·일반 수정·soft delete serializer 불변식을 모든 도메인 fixture에서 다시 검증한다. - [ ] 모든 활성 도메인의 mock handler가 정규화 endpoint·DTO를 사용하고 실제 backend request 0건으로 최종 UI journey를 재현하는지 검증한다. - [ ] 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 인증. - [ ] PRD `9` 기능 matrix를 table-driven E2E data로 검증한다. - [ ] 320, 640, 768, 1024, 1280px와 landscape에서 overflow·가려진 keyboard·44px target을 확인한다. - [ ] desktop Chrome/Edge/Safari와 mobile Chrome/Safari 최신 2개 주요 버전 범위를 실제 지원 환경에서 확인한다. - [ ] 모든 핵심 route에서 axe critical·serious 위반 0건을 확인한다. - [ ] keyboard-only, first-error-focus, dialog focus 복귀, skip link, live region, reduced motion, 200% zoom을 수동 검증한다. - [ ] system dark mode에서도 밝은 token을 유지하고 theme toggle이 없음을 검증한다. - [ ] JWT, password, signed URL, file body가 log·storage·분석 event에 남지 않는지 검증한다. - [ ] 각 Page가 승인된 component map대로 route/query/permission과 component 조합만 담당하고, domain 상호작용이 feature/shared component test로 분리됐는지 review한다. - [ ] PRD `10.9`의 UX 검증 검색을 다시 실행하고 채택·제외 결과를 기록한다. - [ ] 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 재실행 기록. - **범위 밖:** 결정되지 않은 계약을 문서상 확정하는 행위. - [ ] 활성 범위의 P0 외부 의존이 0건인지, 아니면 구현 전에 명시적으로 후속/제외 결정됐는지 확인한다. - [ ] OQ-009의 확정 절차에 따라 각 초기 UI의 최대 길이·배열 개수 권고안, backend 호환 결과와 실제 값 또는 “상한 추가 없음”을 기록하고 중복 checklist를 남기지 않는다. - [ ] `OQ-010` 감사 로그 UI가 현재 릴리스 non-goal임을 결정 기록과 맞춘다. - [ ] 실제 구현과 다른 제품 결정은 PRD 결정 기록 → plan 순으로 갱신한다. API 사실이 달라졌다면 backend가 제공한 새 OpenAPI 계약을 먼저 반영한 뒤 두 문서를 맞춘다. - [ ] PRD 수용 기준마다 자동 test 또는 수동 검증 증거를 연결한다. - [ ] README에 install, env, run, test, build, 지원 브라우저, 알려진 backend 제약을 기록한다. - [ ] plan 하단 검증 기록에 무엇을/왜/어떻게와 실제 명령·성공/실패/불가 사유를 누적한다. - [ ] 별도 code review를 받고 지적사항 수정 후 관련 Phase Gate와 전체 Gate를 다시 실행한다. - [ ] 각 도메인의 `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를 대조한다. ## 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~033`, Audio 관련 `FILE`, `MOCK`, §9 | `src/features/audio-contents`, `tests/e2e/audio-content.spec.ts` | | 5 | `SERIES-001~018`, Series 관련 `FILE`, `MOCK`, §9 | `src/features/series`, `tests/e2e/series.spec.ts` | | 6 | `COMMUNITY-001~015`, Community 관련 `FILE`, `MOCK`, §9 | `src/features/community-posts`, `tests/e2e/community-post.spec.ts` | | 7 | `FANTALK-001~011`, `MOCK`, §9 | `src/features/fan-talks`, `tests/e2e/fan-talk.spec.ts` | | 8 | `COMMENT-001~006`, `MOCK`, §9 | `src/features/comments`, `tests/e2e/comments.spec.ts` | | 9 | §9~10, §12~14, 활성 범위 전체 | 전체 unit/integration/mock·server E2E/build | `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를 회귀 검증한다. ## 6. 구현 완료 정의 - [ ] Phase 0~8의 활성 범위 Gate와 Phase 9 전체 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 > 표기는 실행 당시 근거를 보존하기 위해 수정하지 않는다. ```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, Series edit DTO, active-only 보장, Community pagination 종료 metadata, FanTalk 상세·수정·filter/sort· 유일성 오류, Comments CRUD·권한 오류와 도메인별 오류 key는 Phase별 외부 의존으로 남는다. ### EXT-006 인증 계약 기록·제공 범위 우선 실행 결정 — 2026-07-28 - 무엇을: `EXT-006`에 현재 구현된 로그인·로그아웃 endpoint, header, body, 성공 data, client 처리와 검증 근거를 기록했다. Phase 3부터 제공된 OpenAPI 범위를 먼저 구현해 Phase 9 활성 범위 Gate까지 진행하고 미제공 계약은 후속 vertical slice로 보완하는 실행 전략을 확정했다. - 왜: 이미 동작하는 인증을 구현 대기로 오해하지 않게 하고, backend가 정식 OpenAPI를 작성할 때 현재 프론트엔드 계약을 바로 대조할 수 있게 하며, 독립 기능을 계약 대기 때문에 직렬로 지연하지 않기 위해서다. - 어떻게: - 구현 대조 — `src/features/auth/api/auth-api.ts`, `src/features/auth/model/auth-session.tsx`, auth contract/session/mock test와 `tests/e2e/auth.spec.ts`에서 두 endpoint와 client 동작을 확인했다. - 문서 구조 검사 — `EXT-006` endpoint·비차단 상태, Phase 3~9 진행 문구, `P8-GATE`·`P9-GATE` 후속 규칙, `EXT-001~011`, Markdown link·fence를 검사해 모두 통과했다. - focused unit — `npm run test:run -- src/features/auth/tests/auth-api.test.ts src/features/auth/tests/auth-session.test.tsx src/shared/mocks/__tests__/auth-handlers.test.ts`는 3 files / 26 tests 통과. - 인증 E2E — `npm run e2e -- tests/e2e/auth.spec.ts`는 최초 sandbox local listen `EPERM`으로 실패했고, 포트 권한을 허용한 동일 명령 재실행에서 4 browser projects / 4 tests 통과. - `git diff --check` — 출력 없이 exit 0. - 변경 범위: `prd.md`, `plan-task.md`만 보완했으며 `api-contract.openapi.json`과 애플리케이션 코드·test·설정은 변경하지 않았다. - 남은 항목: backend가 `EXT-006` 두 operation을 정식 OpenAPI에 포함하거나 별도 version 계약으로 고정하면 현재 기록과 대조해 정규화한다. 다른 외부 의존은 제공 범위 구현을 차단하지 않고 각 후속 vertical slice에서 보완한다.