# AI 캐릭터 관리자 웹 구현 계획 > **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` (recommended) or `superpowers:executing-plans` to implement this plan Phase-by-Phase. 모든 구현 항목은 체크박스(`- [ ]`)로 추적한다. **Goal:** ADMIN이 로그인한 뒤 AI 캐릭터를 선택하고, 선택한 캐릭터 문맥에서 Character·Audio·Series·Community·FanTalk·Comments를 안전하게 관리하는 독립 React SPA를 구현한다. **Architecture:** 프로젝트 세팅과 공통 플랫폼·인증/인가를 먼저 완결한 뒤, 각 도메인을 API·상태·화면·오류·반응형·접근성·E2E까지 포함한 vertical slice로 구현한다. 모든 하위 리소스는 URL의 `characterId`를 기준으로 격리하고, 공통 API client가 envelope parsing, 인증 header, `Accept-Language: ko`, 401/403을 담당한다. 두 개 이상의 Phase에서 동일한 의미로 반복될 것이 확정된 UI·파일·미디어 컴포넌트는 Phase 1에서 먼저 만들고, 그 밖의 UI는 도메인 Phase 안에서 작게 나눈 뒤 실제 재사용 근거가 생길 때 shared로 올린다. **Tech Stack:** React, TypeScript, Vite, Tailwind CSS, TanStack Query, Zod, native history routing, fetch/XHR upload adapter, Vitest, React Testing Library, MSW, Playwright, axe-core. | 문서 항목 | 내용 | |---|---| | 상태 | 2026-08-04 `P6-R5` FileField 이미지 preview·Community 생성 media flow·공용 CAN 가격 계약 구현, 표준 배율 browser QA와 최종 review 완료. `P4-R10`, `P9-R18`~`P9-R19`, `P10-R16`~`P10-R17` 자동 보완 완료, 실제 crop pixel·stale ADMIN·Series/FanTalk/Comments/file policy 수동 QA 대기 | | 최초 작성일 | 2026-07-25 | | 재작성일 | 2026-07-26 | | 요구사항 기준 | [prd.md](./prd.md) | | API 기준 | [api-contract.openapi.json](./api-contract.openapi.json) | | 현재 활성 Goal | `P6-R5` 완료 | ## 목표 ADMIN이 로그인한 뒤 AI 캐릭터를 선택하고, 선택한 캐릭터 문맥에서 Character·Audio·Series·Community·FanTalk·Comments를 안전하게 관리하는 독립 React SPA를 완성한다. ## 현재 상태 | 구분 | 현재 상태 | 남은 조건 | |---|---|---| | 자동 검증 범위 | `P4-R10`, `P9-R18`~`P9-R19`, `P10-R16`~`P10-R17` current-state contract 자동 보완 완료 | 실제 crop pixel·stale ADMIN·개발 API 수동 QA | | 실제 개발 API | Series/FanTalk/Comments/file policy 수동 QA 대기 | ADMIN credential과 고정 fixture 필요 | | 브라우저·인가 수동 QA | 실제 crop pixel 비교와 stale ADMIN server 확인 대기 | Chromium/mobile Chrome 환경과 stale ADMIN fixture 필요 | | 문서 구조 | `P10-R9`에서 Goal 실행형 필수 section navigation 복구 | 기존 Progress·Decision 이력은 보존 | ## 범위의 포함·제외 ### 포함 - AI 캐릭터 관리자 SPA의 인증, Character workspace, Audio, Series, Community, FanTalk, Comments mock/client 구현과 회귀 보완. - OpenAPI 2.3.0에서 제공된 v2 lookup, UTC, pagination, FanTalk 수정·삭제, Comments, 가격·오류·파일 정책 반영. - mock/client 자동 검증과 실제 개발 API 수동 QA 대기 상태의 분리 기록. ### 제외 - FanTalk 별도 상세/filter/sort, 중복 오류 key, 복원·hard delete, refresh token, 자동 mock fallback. - 실제 개발 API 수동 QA 완료 주장. credential·fixture가 준비되기 전에는 완료로 표시하지 않는다. - WebKit·Mobile Safari는 지원 범위에서 제외한다. E2E는 Chromium/mobile Chrome만 실행한다. ## 기술적 제약 - 현재 설치 stack은 `package.json` 기준 React, TypeScript, Vite, Tailwind CSS, TanStack Query, Zod, Vitest, React Testing Library, MSW, Playwright, axe-core다. - routing은 native history 기반이고, transport는 fetch/XHR upload adapter를 사용한다. - API 계약은 `api-contract.openapi.json`을 우선하며 제공되지 않은 endpoint, DTO, enum, 오류 status/key, validation 상한은 추정하지 않는다. - JWT와 ADMIN role은 `sessionStorage`에만 저장하고 민감정보를 console, 분석 이벤트, 영구 저장소에 기록하지 않는다. ## 하나 이상의 Phase 실행 Phase는 [Phase 지도](#3-phase-지도)와 [Phase 0](#phase-0-프로젝트-세팅)부터 [Phase 10](#phase-10-openapi-230-후속-계약-vertical-slices)까지의 본문이 소유한다. 완료된 Phase는 다시 열지 않고 후속 문제는 `P-R<번호>` 회귀 Task로 추적한다. ## 실행 순서와 의존성 - 기본 순서는 Phase 0 → 1 → 2 → 3 → 4 → 5/6/7 → 8 → 9 → 10이다. - 자동 보완 Task는 완료됐다. 이후 남은 수동 QA와 필요 시 E2E는 Chromium/mobile Chrome만 실행한다. - 동시에 하나의 미완료 goal만 운용하고, 완료된 Task의 체크박스와 검증 기록은 되돌리지 않는다. ## 변경 금지 항목 - OpenAPI에 없는 계약 추정, mock 성공을 server 완료로 대체, 실제 server QA 완료 허위 표시. - 실패 test 삭제·skip·timeout 상향·근거 없는 browser project 제거로 Gate를 통과시키는 변경. - 제품 범위 밖 기능 추가, 새 dependency 설치, 과거 Progress·Decision Log 삭제 또는 덮어쓰기. ## 의사결정 및 중단 규칙 - 외부 계약이나 권한이 필요한 범위는 담당 주체, 영향, 재개 조건을 기록하고 수동 QA 대기로 유지한다. - 같은 차단 사유가 3회 반복되고 문서화 외 진전이 불가능할 때만 blocked로 판정한다. - 사용자 직접 지시는 현재 검증 범위에 우선한다. 2026-07-31 이후 지원·자동 검증 browser는 Chromium/mobile Chrome만 사용한다. ## Progress 상세 진행 기록은 [검증 기록](#7-검증-기록)에 누적한다. `P4-R10`은 가격 기본값 0, native number stepper, 조건부 유료 옵션과 preview duration offset을 자동 검증 완료했다. `P9-R18`~`P9-R19`, `P10-R16`~`P10-R17`에서 finding·Task·H2·실제 마지막 Progress contract를 보완해 자동 보완 완료로 판정했다. 실제 crop pixel·stale ADMIN·Series/FanTalk/Comments/file policy 수동 QA는 별도 대기다. ## Decision Log 주요 결정은 [Phase 운영 규칙](#2-phase-운영-규칙), [Phase 3~10 OpenAPI 준비 상태](#31-phase-310-openapi-준비-상태), [요구사항 추적표](#5-요구사항-추적표), [검증 기록](#7-검증-기록)에 보존한다. 과거 결정은 삭제하지 않고 후속 정정만 누적한다. ## 발견된 문제 제품·Playwright 설정의 신규 문제는 없다. `P4-R10`은 2026-08-04에 확정된 Audio form UX 정책을 현재 구현과 정렬했고, 가격 native stepper와 `0..99999` 검증을 Chromium/mobile Chrome mock E2E로 확인했다. `P9-R18`, `P9-R19`, `P10-R16`, `P10-R17`로 finding·Task 경계, fenced·중복 H2, 최신 및 중복 Progress marker contract를 보완했다. 실제 crop pixel·stale ADMIN과 개발 API Series/FanTalk/Comments/file policy 수동 QA가 남아 있다. ## 최종 보고 형식 최종 보고는 한국어로 작성하고, 완료한 Goal ID, 변경 파일, 실행한 검증 명령과 결과, 실행하지 않은 E2E/수동 QA의 사유, 남은 위험을 구분해 요약한다. ## 1. 전역 제약 - 계획 보완 단계에서는 PRD·API Contract·plan과 연결 가이드만 동기화하고, 애플리케이션 코드와 프로젝트 설정은 해당 Task goal 실행 때 변경한다. - endpoint, query, multipart part, request/response field, required 여부, status와 오류 응답은 `api-contract.openapi.json`을 우선한다. OpenAPI에 표현되지 않는 제품·UI·운영 정책은 PRD를 따른다. - OpenAPI에 없는 기존 인증 `POST /admin/member/login`, `POST /member/logout`은 PRD `11.5 EXT-006 현재 구현 기준 계약`과 완료된 Phase 1 contract test를 임시 기준으로 유지한다. 정식 계약 제공 전에도 기존 인증을 다시 구현하거나 제거하지 않으며 Phase 3~9 진행을 차단하지 않는다. - 로그인은 `POST /admin/member/login`, 로그아웃은 body 없는 `POST /member/logout`을 사용한다. - JWT와 ADMIN role은 `sessionStorage`에만 저장한다. refresh token과 자동 갱신은 구현하지 않는다. - 모든 요청에 `Accept-Language: ko`를 보내고, 로그인 이외의 보호 요청과 로그아웃에 Bearer token을 보낸다. - `externalCharacterId`는 type, DTO, payload, fixture, UI에 만들지 않는다. - 모든 생성 payload에는 `isActive`를 넣지 않는다. - Character·Audio·Series·Community 일반 수정에는 `isActive`를 넣지 않고 soft delete에만 `isActive=false`를 보낸다. `isActive=true`, 복원, hard delete는 구현하지 않는다. - Character·Audio·Series·Community 목록은 서버가 `isActive=true`인 항목만 반환한다. 활성 상태 query와 client-side 활성 필터를 추가하지 않고 서버 반환값을 그대로 사용한다. - Character·Audio·Series soft delete 성공 후 해당 목록으로 이동한다. Community는 열린 Sheet를 닫고 현재 목록에서 제거한다. 모두 성공 알림을 표시한다. - 워크스페이스 상세 성공 응답의 Character가 `isActive=false`이면 모든 하위 mutation 진입점을 차단한다. soft delete 직후에는 목록 이동을 우선한다. - Series 생성 payload에는 `state`를 넣지 않는다. 수정에서 state를 바꾸지 않으면 key를 생략한다. - Series state에 `OPEN`, 요일에 `MONDAY` 같은 보정 전 enum을 사용하지 않는다. - AI 캐릭터 관리자 API에는 `timezone` query/body field를 보내지 않는다. 오디오 예약 공개의 Asia/Seoul 입력은 client에서 ISO-8601 UTC `Z`로 변환하고 즉시 공개는 `releaseDate=null`을 보낸다. - Audio 생성·수정과 Community 생성의 `price`는 `0..99999` 정수만 허용하고 schema·form에서 `-1`, `100000`, 소수를 거부한다. - multipart의 JSON part 이름은 `request`로 고정하고, optional 교체 파일을 보내지 않으면 기존 media를 유지한다. - 목록은 operation별 OpenAPI pagination을 사용한다. 공통 page는 기본 0·최소 0, size는 기본 20·최소 1이고 FanTalk size만 20..50으로 보정된다. 검색을 제공하는 목록은 약 300ms debounce를 일관되게 적용하고 URL query와 기존 화면 데이터를 유지한다. - image 영역은 비율과 크기를 예약하고 목록 image는 lazy load한다. 날짜·가격 공통 formatter는 Phase 1에서 만들고 상태 label은 각 도메인이 `StatusBadge`에 주입한다. - 모바일 기능 범위는 PRD `9`를 각 도메인 Phase에서 함께 구현한다. 반응형 정책을 마지막에 덧붙이지 않는다. - 초기 릴리스는 밝은 테마만 제공한다. main/primary는 `#00BDF7`, primary foreground는 `#062B36`, 흰 배경의 link/focus ring은 `#007EA8`이다. - JWT, password, signed URL, 업로드 파일 본문을 console, 분석 이벤트, 오류 리포트, 영구 저장소에 기록하지 않는다. - 제공 계약에 없는 문자열·배열 최대값, price 상한, 오류 status/key를 추정하지 않는다. - 새 dependency는 해당 Phase에서 실제로 필요한 최소 항목만 추가한다. shadcn component와 폴더도 소비 시점에 생성한다. - Page는 routing·query·permission·component 조합만 담당한다. 도메인 표시와 상호작용은 feature component로, 두 Phase 이상에서 의미와 동작이 같은 것은 shared component로 분리한다. - 단순 markup 한 조각, 한 화면 전용 UI, 서로 다른 도메인 규칙을 하나의 범용 prop API로 합치기 위한 component는 만들지 않는다. - 모든 기능은 실패하는 test를 먼저 만들고 최소 구현으로 통과시킨다. - API mode는 `server | mock`을 명시적으로 선택한다. 기본 `npm run dev`는 실제 개발 API, `npm run dev:mock`은 browser MSW를 사용한다. - 실제 API의 404·network error를 감지한 자동 mock fallback은 만들지 않고 production build에서는 mock mode를 거부한다. - 제공 계약이 있는 기능만 contract 기반 browser fixture를 만들며, 계약 자체가 없으면 최종 network UI를 추정 구현하지 않는다. - 각 도메인은 `UI 확인 완료(mock)`와 `실제 서버 연동 완료(server)`를 별도 증거로 기록하며 mock Gate만으로 Phase 전체 완료를 주장하지 않는다. ## 2. Phase 운영 규칙 ### 2.1 독립 검증 규칙 각 도메인 Phase는 다음 결과를 모두 가진다. 1. 사용자가 직접 확인할 수 있는 하나 이상의 완결된 흐름 2. 해당 Phase가 소유하는 API contract test 3. loading·empty·error·success 상태 4. 해당 viewport 범위와 keyboard·접근성 검증 5. Phase 전용 E2E와 공통 typecheck·lint·build 결과 6. 제공 계약 범위의 mock preview E2E와 backend 구현 후 server integration 결과를 구분한 기록 각 Task는 다음 Red/Green loop를 따른다. 1. Task에 적힌 test file에 가장 작은 실패 test를 추가한다. 2. 해당 test만 실행해 의도한 assertion 실패인지 확인한다. 3. 그 test를 통과시키는 최소 구현을 작성한다. 4. 관련 feature test 전체를 실행한다. 5. refactor 후 typecheck·lint와 Phase E2E를 다시 실행한다. import 오류, test 환경 오류, 임시 mock 누락 같은 우발적 실패는 Red 증거로 인정하지 않는다. ### 2.2 컴포넌트 조합 규칙 각 도메인 Phase는 코드를 작성하기 전에 다음 component map을 해당 Phase의 `주요 Files`와 checklist에 반영한다. | 분류 | 책임 | 예시 | |---|---|---| | Page | route param/query, data loading, permission, navigation, component 조합 | `CharacterListPage`, `AudioContentDetailPage` | | Feature component | 한 도메인의 표시·입력·상호작용 규칙 | `CharacterForm`, `PublishedDaysField` | | Shared component | 두 개 이상 Phase에서 같은 의미·동작으로 재사용 | `PageState`, `ConfirmDeactivateDialog`, `ImageCropDialog` | | shadcn primitive | 접근 가능한 저수준 control | `Button`, `Dialog`, `Table`, `Form` | - 화면별 loading·empty·error·success·read-only·mobile 상태와 주요 action을 먼저 inventory한다. - Page에 큰 JSX와 form/media 로직을 직접 쌓지 않고, 독립 test가 가능한 feature/shared component를 조합한다. - 기존 shadcn/shared component로 표현할 수 있으면 새 wrapper를 만들지 않는다. - shared component는 domain DTO나 endpoint를 import하지 않고 controlled value, slot, callback으로 조합한다. - 재사용을 위해 boolean prop를 계속 늘리기보다 작은 component와 composition을 사용한다. - component map과 실제 화면 구성이 달라지면 구현 전에 이 계획의 `주요 Files`를 먼저 갱신한다. ### 2.3 미결·외부 의존 처리 전역 Backend Contract Phase를 만들지 않는다. 각 도메인 Phase의 첫 Task에서 그 도메인에 필요한 계약만 확인한다. | 분류 | 처리 규칙 | |---|---| | 계약이 제공됨 | `api-contract.openapi.json`의 operation/schema를 contract test로 고정한 뒤 구현한다. 계약 자체의 변경은 backend가 제공한 새 버전을 받은 경우에만 반영한다. | | 구현됐지만 OpenAPI에서 누락됨 | PRD에 현재 endpoint·request/response·검증 근거를 기록하고 기존 회귀 test를 유지한다. 정식 계약 전에는 동작을 확장하지 않지만 독립 Phase 진행은 차단하지 않는다. 현재 해당 항목은 `EXT-006` 인증뿐이다. | | 안전한 확정 기본값이 있음 | 문서에 적힌 최소 규칙만 구현한다. 예: price 상한 미제공 시 `0 이상 정수`만 검증한다. | | 계약 없이 안전하게 구현할 수 없음 | endpoint·DTO·오류를 추측하지 않는다. 해당 최소 기능 또는 Phase를 현재 릴리스에서 제외하기 전에 PRD 결정 기록, API Contract, 이 계획을 함께 갱신한다. | | 구현 중 불필요하다고 판단 | 활성 체크 항목을 제거하되 PRD 결정 기록에 삭제 이유와 날짜를 남긴다. 과거 결정 기록은 지우지 않는다. | | 계약이 후속 도착 | 완료한 Phase를 묵시적으로 다시 열지 않고 별도 후속 vertical slice를 계획한다. | - `OQ-009`의 결정 절차는 확정됐다. 각 도메인의 초기 UI를 만든 뒤 실제 페이지에서 문자열 입력 공간과 반복 항목 사용성을 검토하고 최대값 권고안을 정한다. backend 호환 확인 후 PRD·OpenAPI 계약·schema·경계 test를 같은 변경에서 갱신하며, 그전에는 임의 상한을 추가하지 않는다. - `OQ-010` 감사 로그 조회 UI는 현재 릴리스 구현 항목을 만들지 않는다. 포함하기로 바뀌면 backend 조회 계약을 포함한 별도 Phase로 다시 계획한다. - P0 외부 의존이 남아 있으면 영향을 받는 network flow를 완료로 표시하지 않는다. 다른 독립 Phase는 계속 진행할 수 있다. - 이미지 최대 `10MB`의 정확한 byte 경계처럼 표현만으로 단일 값이 정해지지 않는 항목은 첫 파일 Phase에서 결정 기록과 contract를 먼저 보정한다. ### 2.4 제공 범위 우선 실행·후속 보완 전략 전체 일정을 미제공 backend 계약 하나에 직렬화하지 않는다. Phase 3부터 각 Phase의 OpenAPI 제공 범위를 먼저 구현하고, 독립적으로 진행 가능한 후속 Phase를 계속 수행해 Phase 9의 활성 릴리스 범위 Gate까지 완료한다. 1. 각 Phase의 계약 확인 Task에서 제공 operation과 외부 의존을 먼저 분리한다. 2. 제공된 endpoint·DTO로 안전하게 구현할 수 있는 Task는 mock/server 상태를 구분해 구현·검증한다. 3. 계약이 없는 기능은 endpoint·DTO·fixture·UI 완료 상태를 추정하지 않고 해당 Task 또는 network 범위만 `외부 계약 대기` 후속으로 기록한다. 4. 외부 의존 때문에 실행하지 않은 범위가 있더라도 완료된 독립 Task를 되돌리지 않고 다음 Phase를 진행한다. 5. 2026-07-28 당시 Phase 8 Comments 계약 부재는 `P8-T1`과 `P8-GATE` 이력으로 보존한다. 2026-07-29 OpenAPI 2.3.0의 댓글 계약 도착 후 구현은 Phase 10에서 새 goal로 진행한다. 6. Phase 9는 제공 계약과 명시적 제외 범위만 대상으로 실행하고 결과를 `활성 범위 완료`로 기록한다. 미제공 계약을 포함한 `전체 기능 완료`로 표현하지 않는다. 7. backend 계약이 후속 도착하면 완료 Phase를 묵시적으로 다시 열지 않고 별도 vertical slice를 계획해 구현한 뒤 영향받는 회귀와 최종 Gate를 다시 실행한다. OpenAPI 2.3.0 후속 범위는 Phase 10이 소유한다. 이 전략은 추가 사용자 결정을 기다리기 위한 임시 우회가 아니라, 제공 범위의 UI와 공통 품질을 먼저 완성해 전체 lead time을 줄이는 확정 실행 방식이다. ### 2.5 Goal 기능 운영 규칙 - `create_goal`에는 동시에 하나의 미완료 goal만 등록한다. Phase 전체가 아니라 아래에 `Goal 실행`으로 표시한 Task 하나를 기본 단위로 사용한다. - goal objective는 해당 Task의 `Goal 실행`, `시작 조건`, `완료 증거`, `범위 밖`을 함께 복사해 등록한다. 사용자가 명시적으로 요청하지 않으면 token budget을 설정하지 않는다. - 활성 goal이 있으면 새 goal을 만들지 않고 같은 Task를 이어서 수행한다. 체크박스 일부만 끝났거나 검증·기록이 남았으면 goal을 완료 처리하지 않는다. - `완료 증거`와 해당 Task의 체크박스를 모두 충족하고 `plan-task.md` 검증 기록까지 누적한 뒤에만 goal을 `complete`로 갱신한다. - 외부 계약이나 권한 같은 동일 차단 사유가 최초 시도와 자동 후속을 포함해 3회 연속 반복되고, 문서화·독립 작업 등 의미 있는 진전도 불가능할 때만 goal을 `blocked`로 갱신한다. 그 전에는 가능한 범위를 계속 수행한다. - 계약 미제공으로 기능을 제외할 때는 PRD 결정 기록 → 제공된 OpenAPI 또는 외부 의존 상태 → 이 계획의 활성 checklist 순서로 갱신해야 한다. 이 문서화가 끝나기 전에는 goal을 완료 처리하지 않는다. - 각 Phase는 자신의 Task goal을 번호 순서로 완료한 뒤 Phase Gate를 마지막 goal로 실행한다. Gate goal은 실제 명령 결과와 수동 검증 결과를 기록한 뒤 완료한다. - Phase 0~1처럼 이미 완료 체크된 범위는 새 구현 goal로 다시 만들지 않는다. 회귀나 기록 정합성 보정이 필요하면 별도 수정 goal을 만들고 기존 검증 기록을 덮어쓰지 않는다. Goal objective 권장 형식: ```text [Goal ID]의 목표를 plan-task.md에 정의된 범위로 수행한다. 시작 조건과 전역 제약을 확인하고, 체크박스를 Red/Green loop로 완료한다. 완료 증거의 검증과 문서 기록까지 끝나기 전에는 complete로 표시하지 않는다. 범위 밖 항목은 구현하지 않으며, 외부 계약은 추정하지 않는다. ``` ## 3. Phase 지도 | Phase | 결과 | 선행조건 | 독립 검증 핵심 | |---:|---|---|---| | 0 | 프로젝트 세팅 | 없음 | fresh install, root smoke, unit/E2E/build | | 1 | 공통 플랫폼·인증/인가·컴포넌트 기반 | Phase 0 | shared component contract + login → protected shell → refresh restore → logout/401/403 | | 2 | 개발 전용 Mock Preview 기반 | Phase 1 | explicit mock/server mode, browser MSW, mock banner, no production fallback | | 3 | Character workspace | Phase 2 | list/search → create/select → detail/edit → deactivate | | 4 | Audio vertical slice | Phase 3의 workspace core | list/search/detail/play → create/edit/upload → deactivate | | 5 | Series vertical slice | Phase 4의 Audio 조회 API | list/detail → 계약 제공 후 CRUD; content link/unlink → full reorder | | 6 | Community vertical slice | Phase 4의 media/file primitive | list → collection Sheet edit/pin → media play → deactivate | | 7 | FanTalk vertical slice | Phase 3 | list → reply create/update, fan root delete; 별도 detail/filter/sort는 제품 범위 제외 | | 8 | Comments vertical slice | Phase 4 + Phase 6 | Audio/Community thread → permission별 CRUD | | 9 | 교차 회귀·인수인계 | 활성 범위의 Phase 0~8 | 전체 journey, viewport, axe, security, mock/server build | | 10 | OpenAPI 2.3.0 후속 계약 vertical slices | Phase 9 이력 + OpenAPI 2.3.0 | v2 lookup, UTC, pagination, FanTalk 수정·삭제, Comments, 가격·오류·파일 정책, active-only server 검증 | 기본 진행 순서는 Phase 번호를 따른다. 다만 Phase 5·6·7은 자신의 선행조건과 계약이 충족되면 병행할 수 있고, 외부 계약으로 막힌 Phase가 다른 독립 Phase를 막지 않는다. ### 3.1 Phase 3~10 OpenAPI 준비 상태 | Phase | 현재 구현 가능 범위 | 외부 의존 또는 제외 범위 | 판정 | |---:|---|---|---| | 3 Character | 기존 목록·상세·기본 create/update/soft delete mock UI 완료 | 없음. v2 원작 검색 선택기는 Phase 10에서 구현 완료 | `EXT-001` 해결, legacy 후보 제거 완료 | | 4 Audio | 목록·상세·create/update/soft delete mock UI와 UTC `releaseDate` 전송 완료 | status filter와 수정 schedule/theme/file은 계약상 제외 | `EXT-007` 해결, 실제 개발 API 수동 QA 대기 | | 5 Series | 목록·상세·연결·해제·전체 순서, v2 활성 장르, CRUD form mock/client 완료 | 실제 개발 API Series 수동 QA | `EXT-002` 해결, 상세는 `SeriesListItem`으로 확정 | | 6 Community | 목록·Sheet·create/update/pin/soft delete, pagination object, `timezone` 제거 mock/client 완료 | 실제 개발 API Community 수동 QA | `EXT-007~008` 해결, active-only 반환 확인 대기 | | 7 FanTalk | page 목록, 답변 생성·수정, 팬 원글 삭제 mock/client UI 완료 | 별도 상세/filter/sort·중복 오류 key는 현재 제품 범위에서 제외 | `EXT-004` 해결, 실제 개발 API 수동 QA만 분리 대기 | | 8 Comments | Audio·Community 댓글 2단계 thread와 mock/client UI 완료 | 실제 개발 API Comments 수동 QA | `EXT-005` 해결, Phase 10 network slice 구현 완료 | | 9 Final | 제공 계약과 명시적 제외 범위의 교차 회귀 | 미제공 P0 범위를 포함한 전체 릴리스 완료 주장 | 활성 범위 Gate 후 가능 | | 10 Follow-up | 원작·장르 lookup, UTC, Community pagination, FanTalk 수정·삭제, Comments, 가격·오류·파일 정책 | 제품 범위에서 제외한 FanTalk 별도 상세/filter/sort·중복 오류 key와 인증 정식화 | 현재 확정 범위의 client/mock/server 구현 가능 | OQ-009를 포함한 프론트엔드 제품 결정 절차는 확정됐다. 위 표의 대기 사항은 추가 사용자 결정이 아니라 backend OpenAPI 보완 또는 명시적 후속/제외 기록이 필요한 외부 의존이다. ```text Phase 0 Setup └─ Phase 1 Platform + Auth/Authz + Shared Components └─ Phase 2 Mock Preview Foundation └─ Phase 3 Character Workspace ├─ Phase 4 Audio ──┬─ Phase 5 Series │ └─ Phase 6 Community ──┐ └─ Phase 7 FanTalk ├─ Phase 9 Final Phase 4 + Phase 6 ── Phase 8 Comments ┘ └─ Phase 10 OpenAPI 2.3.0 Follow-up ``` ## 4. 파일 책임 지도 ```text src/ app/ # entry, providers, router, route constants components/ui/ # 실제로 추가한 shadcn primitive만 보관 features/ auth/ characters/ audio-contents/ series/ community-posts/ fan-talks/ comments/ layouts/ # Admin shell, Character workspace shared/ api/ # envelope, client, query, pagination, errors config/ # runtime env hooks/ # 공통 query/form interaction hook lib/ # 공통 formatter/media helper test/ # MSW, render helper, fixtures ui/ # Phase 1에서 확정한 공통 UI와 후속 검증된 추출물 validation/ # 공통 file/media policy styles/ tests/e2e/ ``` feature 내부의 `api/`, `components/`, `model/`, `pages/`, `schemas/`, `tests/`는 실제 파일이 생길 때만 만든다. 한 구현만을 위한 interface, factory, registry는 만들지 않는다. 모든 Page는 해당 feature의 component와 shared component를 조합해 구성한다. --- ## Phase 0. 프로젝트 세팅 **목표:** 비즈니스 기능 없이도 동일한 명령으로 개발·test·build할 수 있는 React SPA 기반을 만든다. **Phase Goal `P0`:** Task 0.1 → 0.2 → Phase 0 Gate 순서로 재현 가능한 SPA 기반을 완성한다. - **시작 조건:** 없음. - **완료 조건:** `P0-T1`, `P0-T2`, `P0-GATE` 완료와 fresh install 검증 기록. - **현재 상태:** 완료. 재검증은 기존 goal을 다시 열지 않고 별도 회귀 수정 goal로 수행한다. **주요 Files:** - Create: `package.json`, `package-lock.json` - Create: `index.html`, `vite.config.ts` - Create: `tsconfig.json`, `tsconfig.app.json`, `tsconfig.node.json` - Create: `eslint.config.js` - Create: `.env.example`, `README.md` - Create: `playwright.config.ts` - Create: `src/main.tsx`, `src/app/App.tsx`, `src/app/App.test.tsx` - Create: `src/shared/config/env.ts`, `src/shared/config/env.test.ts` - Create: `src/shared/test/setup.ts` - Create: `tests/e2e/smoke.spec.ts` ### Task 0.1 런타임·패키지 기반 **Goal 실행 `P0-T1`:** Node/npm, Vite/TypeScript 진입점, runtime env와 최소 script를 재현 가능하게 구성한다. - **시작 조건:** 지원 Node 버전과 환경 변수 기준 확인. - **완료 증거:** 체크박스 전체, env/App focused test, typecheck·lint·개발/프로덕션 build 및 검증 기록. - **범위 밖:** router, server-state, form, drag-and-drop, 도메인 기능. - [x] `mise.toml`의 Node `24.12.0`을 기준으로 npm package와 lockfile을 생성한다. - [x] React + TypeScript + Vite 진입점과 `@` path alias를 구성한다. - [x] `VITE_API_BASE_URL`만 `.env.example`에 문서화하고 token·password 같은 비밀값을 넣지 않는다. - [x] runtime env 누락·잘못된 URL을 앱 시작 전에 설명 가능한 오류로 차단하는 test를 작성한다. - [x] unit test는 `vi.stubEnv`, Playwright webServer는 명시적 test URL로 `VITE_API_BASE_URL`을 주입해 `.env.example` 자동 로드를 전제하지 않는다. - [x] `dev`, `build`, `typecheck`, `lint`, `test`, `test:run`, `e2e` script를 정의한다. - [x] 이 Phase에 필요하지 않은 router, server-state, form, drag-and-drop dependency는 아직 설치하지 않는다. ### Task 0.2 test 기반 **Goal 실행 `P0-T2`:** unit·DOM·E2E test 기반과 접근 가능한 최소 App shell을 완성한다. - **시작 조건:** `P0-T1` 완료. - **완료 증거:** 체크박스 전체, root unit test와 현재 Playwright project smoke 및 검증 기록. - **범위 밖:** 인증과 실제 도메인 route. - [x] Vitest, jsdom, React Testing Library, jest-dom을 구성한다. - [x] 각 test 뒤 DOM·mock·storage가 정리되는 공통 setup을 만든다. - [x] Playwright에 desktop Chromium과 mobile Chrome viewport project, Vite webServer를 구성한다. - [x] fresh environment에서 Chromium browser binary를 설치하는 명령을 README와 Gate에 포함한다. - [x] ``, `main` landmark, root content를 확인하는 unit test를 먼저 실패시킨 뒤 최소 App shell을 만든다. - [x] 동일 shell이 각 Playwright project에서 열리는 smoke E2E를 만든다. ### Phase 0 Gate **Goal 실행 `P0-GATE`:** Phase 0의 fresh install·unit·E2E·build 재현성을 최종 판정한다. - **시작 조건:** `P0-T1`, `P0-T2` 완료. - **완료 증거:** 아래 명령 모두 0 exit code, Expected와 실제 결과의 검증 기록. - **범위 밖:** 실패와 무관한 기능 추가. 실패는 소유 Task 수정 goal로 되돌린다. ```bash npm ci npx playwright install chromium npm run typecheck npm run lint npm run test:run -- src/app/App.test.tsx src/shared/config/env.test.ts npm run e2e -- tests/e2e/smoke.spec.ts npm run build:dev npm run build:prod ``` **Expected:** fresh install 후 typecheck·lint·unit·E2E·production build가 모두 0 exit code이며 환경 변수 오류가 test로 고정된다. **1차 구현 검증 기록 (2026-07-26):** - 무엇을: Phase 0의 React + TypeScript + Vite 프로젝트 기반, runtime env 검증, Vitest/RTL/jsdom, Playwright Chromium/WebKit 및 mobile viewport smoke, README와 npm scripts를 구현했다. - 왜: 후속 Phase가 동일한 명령으로 개발·test·build할 수 있고, `VITE_API_BASE_URL` 누락/오류와 root shell 회귀를 자동 검증하기 위해서다. - 어떻게: `npm ci` 성공(0 vulnerabilities), `npx playwright install chromium webkit` 성공, `VITE_API_BASE_URL=http://127.0.0.1:4010 npm run typecheck` 성공, `VITE_API_BASE_URL=http://127.0.0.1:4010 npm run lint` 성공, `VITE_API_BASE_URL=http://127.0.0.1:4010 npm run test:run -- src/app/App.test.tsx src/shared/config/env.test.ts` 성공(2 files/4 tests passed), `VITE_API_BASE_URL=http://127.0.0.1:4010 npm run e2e -- tests/e2e/smoke.spec.ts` 성공(4 projects passed), `VITE_API_BASE_URL=http://127.0.0.1:4010 npm run build` 성공. - 정정/참고: `@playwright/test@1.62.0`의 WebKit binary가 현재 macOS 26.0 x86_64 환경에서 시작 전 `Segmentation fault: 11`로 실패해, Phase 0 Gate 충족을 위해 `@playwright/test@1.61.1`로 고정했다. **2차 설정 정정 기록 (2026-07-26):** - 무엇을: 개발 서버 포트를 `8888`로 고정하고, 개발/프로덕션 API base URL과 build 명령을 분리했다. - 왜: 실행 환경별 API endpoint를 명확히 분리하고, AGENTS.md를 짧게 유지하면서 상세 지침을 `docs/agent-guide/`의 세부 문서에서 관리하기 위해서다. - 어떻게: `npm run typecheck`, `npm run lint`, `npm run test:run -- src/app/App.test.tsx src/shared/config/env.test.ts`, `npm run e2e -- tests/e2e/smoke.spec.ts`, `npm run build:dev`, `npm run build:prod`가 성공했다. - 정정: 기존 1차 기록의 `VITE_API_BASE_URL=http://127.0.0.1:4010`과 단일 `npm run build`는 당시 실행 기록으로 보존하되, 현재 Gate 기준은 `.env.development`, `.env.production`, `build:dev`, `build:prod`를 사용한다. ### Task R0.1 — 테스트 격리와 Phase 0 증거 정합성 복구 **Goal 실행 `P0-R1`:** Phase 0 테스트 격리와 루트 셸·Playwright 증거 정합성을 복구하고 회귀를 방지한다. - 연결 리뷰: [Phase 0·1 리뷰](./reviews/review-phase-0-1.md) — `REV-P0-001`, `REV-P0-002`, `REV-P0-003` - 시작 조건: review finding을 보존하고 기존 완료 체크를 되돌리지 않는다. - 완료 증거: stubbed global 복원 회귀 test, root `main`과 대표 content assertion, 현재 Playwright mode URL 전략과 실행 가능한 agent-guide 문서 경로 정정 기록, P0-GATE 통과. - 범위 밖: Phase 1 인증 동작 전면 변경, test framework 교체. - [x] `src/shared/test/setup-isolation.test.ts`로 `vi.stubGlobal` 누출을 실패 재현한 뒤 `restoreAllMocks`·`unstubAllGlobals` 정리를 추가했다. - [x] `src/app/App.test.tsx`와 `tests/e2e/smoke.spec.ts`에서 root `main` 안의 로그인 대표 콘텐츠를 검증한다. - [x] 존재하지 않는 `docs/agent-guide/README.md` 참조를 실제 `docs/agent-guide/` 세부 문서 관리 방식으로 정정했다. - [x] P0-GATE를 재실행하고 결과를 아래에 누적했다. **P0-R1 수정 검증 기록 (2026-07-27):** - RED: `npm run test:run -- src/app/App.test.tsx src/features/auth/tests/login-page.test.tsx src/features/auth/tests/auth-session.test.tsx src/shared/lib/crop-image.test.ts src/shared/validation/file-media-policy.test.ts src/shared/ui/__tests__/file-field.test.tsx src/shared/ui/__tests__/admin-audio-player.test.tsx src/shared/ui/__tests__/confirm-deactivate-dialog.test.tsx src/shared/ui/__tests__/unsaved-changes-guard.test.tsx src/shared/ui/__tests__/image-crop-dialog.test.tsx src/shared/test/setup-isolation.test.ts` — 기대 실패 확인, 11 files failed / 13 failed / 40 passed. P0-R1 관련으로 stubbed global 복원과 root shell assertion 실패를 확인했다. - GREEN focused: `npm run test:run -- src/app/App.test.tsx src/shared/config/env.test.ts src/shared/test/setup-isolation.test.ts` — 성공, 3 files / 18 tests passed. - P0-GATE: `npm ci` — 성공, 307 packages, 0 vulnerabilities. `npx playwright install chromium webkit` — 성공. `npm run typecheck` — 성공. `npm run lint` — 성공. `npm run test:run -- src/app/App.test.tsx src/shared/config/env.test.ts src/shared/test/setup-isolation.test.ts` — 성공, 18 tests passed. `npm run e2e -- tests/e2e/smoke.spec.ts` — 성공, 4 projects / 4 tests passed. `npm run build:dev` — 성공. `npm run build:prod` — 성공. - 참고: 첫 Gate 병렬 실행 중 `npm ci`가 `node_modules`를 재설치하는 동안 동시에 실행한 `tsc/playwright` 명령이 bin 삭제로 실패했다. `npm ci` 완료 후 같은 명령을 재실행해 모두 성공했다. ### Task R0.2 — API origin 문서·E2E 단일 source 복구 **Goal 실행 `P0-R2`:** 현재 `.env` API origin을 실행 문서와 Playwright route/probe가 함께 사용하게 해 mock/server Gate의 host drift를 제거한다. - **연결 리뷰:** [Phase 0 현재 리뷰](./reviews/phase0-project-foundation.md) — `REV-P0-004` - **시작 조건:** `.env.development=https://test-api.sodalive.net`, `.env.production=https://api.sodalive.net`인 현재 working tree와 `VITE_API_BASE_URL` runtime 검증. - **완료 증거:** stale origin에서 실패하는 mock/server E2E RED, origin 단일 source 적용, focused/full mock·server E2E와 문서/diff 검증 기록. - **Files:** Modify: `README.md`, `docs/agent-guide/environment.md`, `tests/e2e/auth.spec.ts`, `tests/e2e/accessibility-shell.spec.ts`, `tests/e2e/server-mode-boundary.spec.ts`, `tests/e2e/error-mapping.spec.ts`, `tests/e2e/comments-test-support.ts`, 필요 시 Playwright 전용 env helper/config. - **Interfaces:** 앱, browser mock handler, Playwright route/probe는 같은 개발 API base URL에서 파생하고 개발/프로덕션 host를 여러 파일에 상수로 복제하지 않는다. - **범위 밖:** `.env`를 과거 host로 되돌리기, unhandled request를 bypass/warn으로 완화하기, 실제 ADMIN credential 발급. - [x] **RED:** 현재 `.env.development`에서 과거 `test-character-admin` route/probe가 매칭되지 않아 mock error-mapping과 server auth/boundary E2E가 실패함을 기록했다. - [x] **GREEN:** Playwright가 현재 개발 API origin을 한 source에서 읽도록 수정하고 README·환경 가이드의 현재값을 정렬했다. - [x] **REFACTOR:** 과거 host literal 검색을 0건으로 만들고 focused Comments server route와 full mock/server Gate를 재실행했다. - **실행 명령:** `npm run e2e:mock -- tests/e2e/error-mapping.spec.ts`; `npm run e2e:mock`; `npm run e2e`; `npm run e2e -- tests/e2e/comments.spec.ts --project=chromium`; `rg -n 'test-character-admin|character-admin\\.sodalive\\.net' README.md docs/agent-guide tests/e2e`; `npm run typecheck`; `npm run lint`; `npm run build`. - **기대 결과:** 모든 자동 명령 exit 0, Playwright unhandled request 0건, 현재 개발/프로덕션 origin 문서 불일치 0건. - **수동 확인:** mock mode는 현재 개발 origin을 intercept하고 server mode는 browser MSW 없이 같은 origin으로 요청하는지 network에서 확인한다. **P0-R2 수정 검증 기록 (2026-07-30):** - RED: `npm run e2e:mock -- tests/e2e/error-mapping.spec.ts` — 4 failed / 4 passed. 과거 `test-character-admin` 요청이 MSW `onUnhandledRequest: "error"`에 걸려 기대 401 대신 500을 반환했다. `npm run e2e -- tests/e2e/server-mode-boundary.spec.ts --project=chromium` — 2 failed / 2 passed. stale route가 현재 `.env.development` origin 요청을 intercept하지 못해 404 한국어 message와 network retry assertion이 실패했다. - GREEN: `tests/e2e/api-base-url.ts`를 추가해 E2E route/probe가 `.env.development`의 `VITE_API_BASE_URL`을 읽게 하고, `README.md`와 `docs/agent-guide/environment.md`를 현재 개발/프로덕션 origin으로 정렬했다. `npm run e2e:mock -- tests/e2e/error-mapping.spec.ts` — 8 passed. `npm run e2e -- tests/e2e/server-mode-boundary.spec.ts --project=chromium` — 4 passed. - REFACTOR/회귀: `rg -n 'test-character-admin|character-admin\.sodalive\.net' README.md docs/agent-guide tests/e2e` — no matches. `npm run e2e -- tests/e2e/comments.spec.ts --project=chromium` — 3 passed. `npm run e2e` — 36 passed. `npm run e2e:mock` 단일 실행은 900초 제한으로 188/228 진행 중 timeout됐고, 같은 allowlist를 project별로 분할해 `chromium` 57 passed, `webkit` 49 passed / 8 skipped, `mobile-chrome` 52 passed / 5 skipped, `mobile-safari` 48 passed / 9 skipped를 확인했다. - Type/LSP/build: `npm run typecheck`, `npm run lint`, `npm run build` — 모두 exit 0. LSP diagnostics: `tests/e2e` 18 files / 오류 0건, 변경 E2E 파일 개별 오류 0건. - 수동/표면 확인: mock `error-mapping`은 현재 개발 origin을 MSW handler가 intercept해 한국어 401/403/415를 반환했고, server mode는 browser MSW 없이 같은 origin route/probe에서 404·network retry와 Comments server route가 통과했다. timeout으로 남은 Vite mock server PID는 종료해 포트 8888/8889 listen 0건을 확인했다. ### Task R0.3 — 운영체제 메타데이터의 저장소 유입 차단 **Goal 실행 `P0-R3`:** 문서 디렉터리에 index된 `.DS_Store`를 제거하고 같은 운영체제 메타데이터가 다시 version control 대상이 되지 않게 한다. - **연결 리뷰:** [Phase 0 현재 리뷰](./reviews/phase0-project-foundation.md) — `REV-P0-005` - **시작 조건:** `git ls-files 'docs/20260725_AI캐릭터관리자웹/.DS_Store'`가 index entry를 반환하고 `.gitignore`에 `.DS_Store` 규칙이 없는 현재 working tree. - **완료 증거:** 해당 index entry 삭제, 저장소 전역 ignore 규칙, 추적 `.DS_Store` 0건과 targeted diff 검증 기록. - **Files:** Modify: `.gitignore`; Delete: `docs/20260725_AI캐릭터관리자웹/.DS_Store`; Test: 없음 — runtime 동작이 없는 repository hygiene 변경이다. - **Interfaces:** 애플리케이션 runtime, package script, 문서 내용에는 영향을 주지 않는다. - **범위 밖:** 다른 사용자 산출물 삭제, `.gitignore` 전면 정리, 애플리케이션 코드·dependency 변경. - **P0-GATE 연결:** runtime Gate 재실행 대신 repository 정적 검증을 수행하고 `git diff --check`가 통과하기 전에는 Task를 완료하지 않는다. - **TDD 예외 사유:** 운영체제 metadata의 index 포함 여부와 ignore 규칙은 runtime 동작이 없어 unit test로 실패를 만드는 것보다 Git index 정적 검사가 직접적인 증거다. - **대체 검증 방법:** cleanup 전후 `git ls-files`와 `.gitignore` 검색을 비교하고 targeted `git diff --check` 결과를 기록한다. - [x] **RED 대체:** `git ls-files --stage | rg '\.DS_Store'`와 `git status --short --untracked-files=all | rg '\.DS_Store'`로 현재 staged entry를 재현했다. - [x] `.DS_Store`를 index와 working tree에서 제거하고 저장소 전역 ignore 규칙을 한 줄 추가했다. - [x] 추적 entry 0건, ignore 적용, whitespace 오류 0건을 확인하고 Progress에 실제 결과를 기록했다. - **실행 명령:** `git ls-files | rg '(^|/)\.DS_Store$'`; `rg -n '^\.DS_Store$' .gitignore`; `git diff --check -- .gitignore docs/20260725_AI캐릭터관리자웹`. - **기대 결과:** 첫 명령은 no matches, ignore 검색은 정확히 한 규칙을 반환하고 diff 검증은 exit 0. - **수동 확인:** 없음. binary 운영체제 메타데이터 제거는 index와 ignore 정적 결과로 판정한다. **P0-R3 수정 검증 기록 (2026-07-31):** - RED 대체: `git ls-files --stage | rg '\.DS_Store'`와 `git status --short --untracked-files=all | rg '\.DS_Store'`에서 `docs/20260725_AI캐릭터관리자웹/.DS_Store` staged entry를 확인했다. `rg -n '^\.DS_Store$' .gitignore`는 no matches였다. - GREEN: `.gitignore`에 `.DS_Store` 규칙을 추가하고 `git rm --cached -- docs/20260725_AI캐릭터관리자웹/.DS_Store`와 working tree 파일 삭제로 운영체제 metadata를 제거했다. - 검증: `git ls-files | rg '(^|/)\.DS_Store$'` — no matches. `git status --short --untracked-files=all | rg '\.DS_Store'` — no matches. `rg -n '^\.DS_Store$' .gitignore` — `7:.DS_Store`. `git diff --check -- .gitignore docs/20260725_AI캐릭터관리자웹` — exit 0. - LSP: `.gitignore`는 extension 없는 ignore 파일이라 설정된 LSP 서버가 없어 diagnostics 대상이 아니다. runtime 동작 없는 repository hygiene 변경이므로 정적 Git 검증으로 판정했다. --- ## Phase 1. 공통 플랫폼·인증/인가·컴포넌트 기반 **목표:** ADMIN 인증 흐름과 보호된 Admin shell을 완결하고, Phase 2 이후 화면이 조합해 사용할 공통 UI·form·file·media component contract를 제공한다. **Phase Goal `P1`:** Task 1.1 → 1.6과 Phase 1 Gate를 완료해 도메인 Phase가 사용할 공통 contract를 제공한다. - **시작 조건:** `P0-GATE` 완료. - **완료 조건:** `P1-T1`~`P1-T6`, `P1-GATE` 완료와 auth/shared contract 검증 기록. - **현재 상태:** 완료. 회귀나 기록 정합성 수정은 별도 수정 goal로 수행한다. **요구사항:** `AUTH-001~013`, `UX-001~002`, PRD `7`, `10.1~10.8`, `11.1~11.2`, `12`의 공통 항목. **주요 Files:** - Create: `components.json`, `src/styles/globals.css` - Create: `src/app/providers.tsx`, `src/app/router.tsx`, `src/app/route-paths.ts` - Create: `src/shared/api/{types,api-error,client,pagination,query-client}.ts` - Create: `src/shared/api/__tests__/{client,pagination}.test.ts` - Create: `src/shared/test/{server,handlers,render}.ts` - Create: `src/shared/hooks/use-search-params-state.ts` - Create: `src/shared/ui/{page-state,status-badge,search-toolbar,resource-pagination,responsive-resource-list}.tsx` - Create: `src/shared/ui/{confirm-deactivate-dialog,unsaved-changes-guard,file-field,image-crop-dialog,upload-progress}.tsx` - Create: `src/shared/ui/{admin-audio-player,audio-playback-provider}.tsx` - Create: `src/shared/validation/{file-validation,image-policy,audio-file-policy}.ts` - Create: `src/shared/validation/{file-validation,image-policy,audio-file-policy}.test.ts` - Create: `src/shared/lib/{crop-image,format-date,format-price}.ts` - Create: `src/shared/lib/{crop-image,format-date,format-price}.test.ts` - Create: `src/shared/ui/__tests__/{page-state,resource-list,confirm-deactivate-dialog,unsaved-changes-guard}.test.tsx` - Create: `src/shared/ui/__tests__/{file-field,image-crop-dialog,upload-progress,admin-audio-player}.test.tsx` - Create: `src/features/auth/api/auth-api.ts` - Create: `src/features/auth/model/auth-session.tsx` - Create: `src/features/auth/model/auth-session-storage.ts` - Create: `src/features/auth/schemas/login-schema.ts` - Create: `src/features/auth/pages/{LoginPage,AccessDeniedPage}.tsx` - Create: `src/features/auth/components/ProtectedRoute.tsx` - Create: `src/features/auth/tests/auth-api.test.ts` - Create: `src/features/auth/tests/{login-page,protected-route}.test.tsx` - Create: `src/layouts/AdminLayout.tsx` - Create: `tests/e2e/auth.spec.ts`, `tests/e2e/accessibility-shell.spec.ts` **Phase 1 공통 컴포넌트 소비처 Matrix:** | Shared component | 확정 소비 Phase | 공통 책임 | |---|---|---| | `PageState` | 1~7 | loading·empty·error·retry | | `StatusBadge` | 2~6 | semantic tone과 label/icon slot | | `SearchToolbar` | 2, 3, 4 | controlled search, filter slot, debounce callback | | `ResourcePagination` | 2~7 | `PageData` 기반 page/size control | | `ResponsiveResourceList` | 2~6 | desktop/mobile rendering slot | | `ConfirmDeactivateDialog` | 2~5 | 대상명·영향·확인 callback | | `UnsavedChangesGuard` | 2~7 | dirty form route 이탈 확인 | | `FileField`, `ImageCropDialog` | 2~5 | file input과 주입된 crop policy 실행 | | `UploadProgress` | 3, 5 | 진행률·취소·재시도 callback | | `AdminAudioPlayer`, `AudioPlaybackProvider` | 3, 5 | native audio control과 단일 재생 상태 | 이 표는 PRD에 이미 확정된 반복 소비만 포함한다. `PageHeader`는 실제 구현·소비가 없고 현재 필요한 반복 소비 컴포넌트가 아니므로 만들지 않는다. 각 도메인 Phase에서 필요하다고 확인될 때 추가한다. ### Task 1.1 최소 디자인 시스템 **Goal 실행 `P1-T1`:** light-only token과 실제 소비되는 최소 shadcn primitive를 접근성 기준으로 고정한다. - **시작 조건:** `P0-GATE` 완료, PRD `10.1~10.9` 확인. - **완료 증거:** 체크박스 전체, token/component test, UX 검색 채택·제외 기록, typecheck·lint·build. - **범위 밖:** dark mode, 전체 primitive 선행 scaffold, 도메인 전용 UI. - [x] PRD `10.9`의 `ui-ux-pro-max` design-system 검색을 실행하고 관리자 제품에 맞는 채택·제외 결과를 작업 기록에 남긴다. - [x] Tailwind CSS와 shadcn/ui CSS variable mode를 구성한다. - [x] `brand-500=#00BDF7`, hover `#00A9DE`, active `#009DCE`, primary foreground `#062B36`, link/ring `#007EA8`을 primitive → semantic → component token으로 연결한다. - [x] 핵심 foreground 대비, control boundary 3:1, primary 위 흰색 금지를 token test로 고정한다. - [x] 밝은 `:root` token만 만들고 `.dark`, ThemeProvider, theme toggle, system dark 연동이 없음을 test한다. - [x] Korean system font stack, mobile input 16px, 44px target, focus ring, reduced motion, semantic z-index를 base style에 둔다. - [x] 상태 Badge가 text label을 포함하고 색상만으로 상태를 전달하지 않는 test를 작성한다. - [x] icon-only action에는 accessible name과 Tooltip이 있고 필수 control boundary·focus indicator가 인접 배경 대비 3:1 이상인지 test한다. - [x] Auth와 공통 상태에 실제 필요한 shadcn component만 추가한다. 전체 component를 선행 scaffold하지 않는다. ### Task 1.2 공통 API·server state **Goal 실행 `P1-T2`:** 인증 저장 adapter와 API envelope·오류·pagination·query 정책을 독립 contract로 완성한다. - **시작 조건:** `P1-T1` 완료, PRD/API Contract 공통 규칙 확인. - **완료 증거:** 체크박스 전체, API/session/pagination/query test와 typecheck·lint·build 기록. - **범위 밖:** 로그인 화면, 보호 route, 도메인 endpoint adapter. - [x] `AuthSessionRecord = { token: string; role: "ADMIN" }`의 읽기·저장·제거 adapter를 먼저 만들고, API client는 React provider가 아니라 이 adapter의 token reader/clear callback에만 의존한다. - [x] `ApiResponse` 성공형이 `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건으로 정리했다. ### Task R1.6 — 모든 보호 요청 401의 인증 만료 처리 **Goal 실행 `P1-R6`:** 응답 body 형식과 무관하게 보호 요청의 HTTP 401에서 session을 한 번 제거하고 login으로 이동한다. - **연결 리뷰:** [Phase 1 재리뷰](./reviews/review-phase-1-20260729.md) — `REV-P1-011` - **시작 조건:** 기존 concurrent 401 burst 규칙과 `UNKNOWN_API_ERROR_MESSAGE`를 보존한다. - **Files:** Modify: `src/shared/api/client.ts`, `src/shared/api/__tests__/client-auth.test.ts`, `src/shared/api/__tests__/client.test.ts`; Test: 동일 경로. - **Interfaces:** `createApiClient({clearSession,onAuthExpired})`; malformed JSON/envelope·empty body 401도 `ApiError.status=401`을 유지한다. - **범위 밖:** refresh token, 403 session 제거, XHR upload adapter(`P4-R2`). - [x] **RED:** malformed JSON, empty body, schema 불일치 401 각각에서 `clearSession/onAuthExpired`가 한 burst에 1회 호출되는 실패 test를 추가했다. - [x] **GREEN:** HTTP status를 기준으로 인증 만료를 먼저 처리하되 기존 오류 정규화와 concurrent request coalescing을 유지했다. - [x] **REFACTOR:** 정상 오류 envelope 401과 malformed 401의 중복 분기를 status 기반 단일 처리로 정리하고 공통 API 회귀를 실행했다. - **실행 명령:** `npm run test:run -- src/shared/api/__tests__/client-auth.test.ts src/shared/api/__tests__/client.test.ts`; `npm run test:run -- src/features/auth src/app`. - **기대 결과:** exit 0, 모든 보호 401에서 burst별 clear/redirect 1회, 403 clear 0회. - **수동 확인:** session이 있는 보호 route에서 empty-body 401을 반환했을 때 login 이동과 만료 안내를 확인한다. **P1-R6 수정 검증 기록 (2026-07-30):** - RED: `npm run test:run -- src/shared/api/__tests__/client-auth.test.ts src/shared/api/__tests__/client.test.ts` — malformed JSON, empty body, schema mismatch 401에서 `clearSession` 0회로 3 tests가 기대대로 실패했다. - GREEN focused: 같은 command — 성공, 2 files / 21 tests passed. 보호 401의 body 형식과 무관하게 burst별 `clearSession/onAuthExpired` 1회를 확인했고 403 clear 0회 회귀를 유지했다. - LSP diagnostics: `src/shared/api/client.ts`, `src/shared/api/__tests__/client-auth.test.ts` — 오류 0건. - Auth/session focused regression: `npm run test:run -- src/features/auth/tests/auth-session.test.tsx src/features/auth/tests/login-page.test.tsx src/app/App.test.tsx src/app/App.protected-auth.test.tsx` — 성공, 3 files / 24 tests passed. - 참고: 계획상 broad regression `npm run test:run -- src/features/auth src/app`은 6 failed / 47 passed로 종료했다. 실패 6건은 `REV-P10-001`/`P10-R1`의 공통 오류 fallback stale assertion 범위(`API 응답 형식이 올바르지 않습니다.` vs `알 수 없는 오류가 발생했습니다.`)와 일치해 `P1-R6` 완료 판정에서 제외하고 기존 후속 Task로 유지한다. ### Task R1.7 — zoom no-upscale와 touch pinch crop 복구 **Goal 실행 `P1-R7`:** 현재 zoom의 선택 source pixel을 넘지 않는 crop 결과와 pointer pinch 입력을 제공한다. - **연결 리뷰:** [Phase 1 재리뷰](./reviews/review-phase-1-20260729.md) — `REV-P1-012`, `REV-P1-013` - **시작 조건:** `FILE-012`, PRD `10.4`, 기존 keyboard/range/reset contract 유지. - **Files:** Modify: `src/shared/lib/crop-image.ts`, `src/shared/lib/crop-image.test.ts`, `src/shared/ui/image-crop-dialog.tsx`, `src/shared/ui/__tests__/image-crop-dialog.test.tsx`; Test: 동일 경로. - **Interfaces:** output size는 현재 `CropSourceRect.width/height`와 resource `maxWidth` 중 작은 값; pinch는 두 pointer 거리 변화로 기존 `zoom 1..3`을 갱신한다. - **범위 밖:** resource별 aspect/maxWidth 변경, GIF crop, 외부 gesture dependency. - [x] **RED:** zoom 1.5에서 600px source crop이 400px 이하 output을 생성하는 pixel test와 두 pointer pinch가 zoom을 바꾸는 Dialog test를 실패시킨다. - [x] **GREEN:** output size에 현재 zoom을 전달하고 native pointer map으로 최소 pinch를 구현한다. - [x] **REFACTOR:** drag와 pinch pointer cleanup을 정리하고 Character/Audio/Series/Community crop 회귀를 실행한다. - **실행 명령:** `npm run test:run -- src/shared/lib/crop-image.test.ts src/shared/ui/__tests__/image-crop-dialog.test.tsx src/features/characters src/features/audio-contents src/features/series src/features/community-posts`. - **기대 결과:** exit 0, `noUpscale=true`의 output/source scale `<=1`, 기존 keyboard/range test와 resource aspect test 통과. - **수동 확인:** tablet touch에서 drag·pinch·range·keyboard 대안과 예상 결과 pixel 표시를 확인한다. **P1-R7 수정 검증 기록 (2026-07-30):** - RED: `npm run test:run -- src/shared/lib/crop-image.test.ts src/shared/ui/__tests__/image-crop-dialog.test.tsx` — 2 failed / 9 passed. `calculateCropOutputSize`는 zoom 1.5에서도 600×600을 반환했고, Dialog two-pointer pinch는 zoom을 변경하지 않아 예상 결과 400×400을 찾지 못했다. - GREEN focused: 같은 command — 성공, 2 files / 14 tests passed. `noUpscale=true`에서 zoom 1.5의 output 400×400, 210:297 zoom 1.8의 source 이하 output 236×333, tiny source 1×1, aspect 2 tiny source 1×1, free aspect tiny source 1×1, range/keyboard output 재계산, two-pointer pinch zoom 1.5 apply payload와 `touch-action: none`을 확인했다. - Broad crop regression: `npm run test:run -- src/shared/lib/crop-image.test.ts src/shared/ui/__tests__/image-crop-dialog.test.tsx src/features/characters src/features/audio-contents src/features/series src/features/community-posts` — 성공, 23 files / 119 tests passed. - Surface E2E: `npm run e2e:mock -- tests/e2e/series.spec.ts --project=chromium` — 성공, 7 tests passed. Series 생성 흐름에서 실제 브라우저의 `이미지 crop` Dialog open/apply 경로를 확인했다. - Type/LSP: `npm run typecheck`, `npm run lint` — 성공(exit 0). `lsp_diagnostics src/shared/ui`, `tests/e2e` — 오류 0건. `src/shared/lib/crop-image.ts`, `src/shared/lib/crop-image.test.ts` 단일 fresh diagnostics는 3초 timeout이었고 typecheck/lint로 보완 확인했다. - Reviewer blocker 보강: 1차 `P1-R7` review는 210:297 zoom 1.8에서 output 236×334가 source 236×333을 1px 초과하고, crop preview에 `touch-action: none`이 없어 실제 touch pinch가 browser 기본 gesture에 취소될 수 있다고 BLOCKED 판정했다. 추가 RED `npm run test:run -- src/shared/lib/crop-image.test.ts src/shared/ui/__tests__/image-crop-dialog.test.tsx`는 2 failed / 11 passed였고, tiny source 0×0 붕괴 RED `npm run test:run -- src/shared/lib/crop-image.test.ts`는 1 failed / 6 passed였다. 2차 review는 aspect 2 tiny source가 1×0으로 붕괴한다고 BLOCKED 판정했고 추가 RED `npm run test:run -- src/shared/lib/crop-image.test.ts`는 1 failed / 6 passed였다. 3차 review는 free aspect tiny source가 1×2로 source height를 초과한다고 BLOCKED 판정했고 추가 RED `npm run test:run -- src/shared/lib/crop-image.test.ts`는 2 failed / 5 passed였다. 수정 후 위 focused/broad/E2E/type/lint/diff 검증이 통과했다. - 수동 확인 한계: 실제 tablet touch 장비 QA는 이 작업 환경에서 수행하지 못했다. native pointer event 회귀 test로 drag·pinch·range·keyboard 대안을 자동 검증했으며, 실기기 touch 확인은 릴리스 QA 항목으로 유지한다. ### Task R1.8 — 사용되지 않는 공통 pagination helper 계약 정리 **Goal 실행 `P1-R8`:** 사용되지 않는 공통 pagination helper의 FanTalk 전용 `20..50` size clamp와 page 최소값 누락을 제거해 공통 계약을 오해할 여지를 없앤다. - **연결 리뷰:** [Phase 1 현재 리뷰](./reviews/phase1-platform-auth-shared-ui.md) — `REV-P1-014` - **시작 조건:** PRD §11.1의 공통 `page >= 0`, `size >= 1`, 전역 최대값 없음과 FanTalk만 `size=20..50`인 예외 계약. - **완료 증거:** production 사용처 검색, stale helper 제거, 도메인 adapter pagination 회귀와 전체 정적 Gate 통과. - **Files:** Modify: `src/shared/api/pagination.ts`, `src/shared/api/__tests__/pagination.test.ts`; Test: 각 도메인 contract test. - **Interfaces:** 실제 소비 중인 `PageData` type은 유지한다. production 소비자가 없는 `PageParams`, `createPageParams`와 잘못된 clamp test를 제거하고 도메인별 adapter의 계약별 normalization은 보존한다. - **범위 밖:** 모든 도메인 pagination의 새 공통 abstraction, FanTalk의 명시적 `20..50` 보정 변경, 화면 page-size 선택지 변경. - [x] **RED 대체:** `rg -n 'createPageParams' src`로 production 사용처 0건과 test-only 사용을 기록하고, 현재 helper가 `{page:-1,size:1}`과 `{size:51}`을 공통 계약과 다르게 처리함을 고정했다. - [x] **GREEN:** 사용되지 않는 `PageParams`, `createPageParams`와 그 동작 test를 삭제하고 실제 소비 중인 `PageData` type만 유지했다. - [x] **REFACTOR:** 새 helper를 만들지 않고 Character/Audio/Series/Community/Comments/FanTalk adapter의 기존 contract test로 각 pagination 예외를 확인했다. - **실행 명령:** `rg -n 'createPageParams' src`; `npm run test:run -- src/shared/api src/features/characters src/features/audio-contents src/features/series src/features/community-posts src/features/comments src/features/fan-talks`; `npm run typecheck`; `npm run lint`. - **기대 결과:** production `createPageParams` 사용 0건, 공통 최대 `50` 오해를 고정하는 test 0건, 모든 명령 exit 0. - **수동 확인:** 없음. 사용되지 않는 내부 helper 제거 Task이므로 contract test와 정적 검색으로 대체한다. **P1-R8 수정 검증 기록 (2026-07-30):** - RED 대체: `rg -n 'createPageParams|PageParams|PageData|normalizeSize' src`에서 `createPageParams` production 사용처 0건, `src/shared/api/__tests__/pagination.test.ts` test-only 사용과 `src/shared/api/pagination.ts` 정의만 확인했다. 기존 `npm run test:run -- src/shared/api/__tests__/pagination.test.ts`는 1 file / 5 tests passed로 잘못된 공통 size `20..50` clamp test가 통과함을 확인했다. - GREEN: `src/shared/api/pagination.ts`에서 `PageParams`와 `createPageParams`를 삭제하고 `PageData`만 유지했다. `src/shared/api/__tests__/pagination.test.ts`는 `PageData` shape test만 남겼다. - REFACTOR/회귀: `rg -n 'createPageParams|type PageParams|import .*PageParams' src` — no matches. `npm run test:run -- src/shared/api src/features/characters src/features/audio-contents src/features/series src/features/community-posts src/features/comments src/features/fan-talks` — 35 files / 201 tests passed. `npm run typecheck`, `npm run lint` — 모두 exit 0. - LSP diagnostics: 변경 파일 단일 fresh diagnostics는 3초 제한으로 timeout됐고, `npm run typecheck`와 `npm run lint` 성공으로 보완 확인했다. ### Task R1.9 — 공통 pagination control ID 격리 **Goal 실행 `P1-R9`:** 한 화면에 둘 이상의 `ResourcePagination`이 렌더링돼도 각 페이지 크기 label이 자신의 select와 유일하게 연결되도록 한다. - **연결 리뷰:** [Phase 1 현재 리뷰](./reviews/phase1-platform-auth-shared-ui.md) — `REV-P1-015` - **시작 조건:** PRD §10.7의 visible label·연결 가능한 접근성 이름과 Comments의 root/reply pagination 동시 렌더링 구조. - **완료 증거:** 두 인스턴스의 중복 ID를 재현하는 실패 test, React `useId` 기반 최소 수정, shared UI·Comments focused unit과 mock E2E·정적 Gate 통과. - **Files:** Modify: `src/shared/ui/resource-pagination.tsx`, `src/shared/ui/__tests__/resource-pagination.test.tsx`; Test: 필요 시 `src/features/comments/tests/comment-thread.test.tsx`, `tests/e2e/comments.spec.ts`. - **Interfaces:** 페이지·크기 변경 callback과 URL/API pagination 값은 바꾸지 않고 인스턴스별 select ID와 `htmlFor`만 유일하게 만든다. - **범위 밖:** 새 pagination abstraction, page-size option 변경, 도메인별 pagination query 정책 변경. - [x] **RED:** 한 container에 `ResourcePagination` 두 개를 렌더링해 select ID 중복과 두 label이 같은 control을 가리키는 실패 test를 작성했다. - [x] **GREEN:** 컴포넌트 인스턴스별 안정적인 ID를 만들고 각 label의 `htmlFor`를 해당 select에 연결했다. - [x] **REFACTOR:** 공개 prop는 늘리지 않고 기존 component 내부에서 ID 생성을 끝내며 단일 인스턴스 동작을 보존했다. - **실행 명령:** `npm run test:run -- src/shared/ui/__tests__/resource-pagination.test.tsx src/features/comments`; `npm run e2e:mock -- tests/e2e/comments.spec.ts --project=chromium`; `npm run typecheck`; `npm run lint`; `npm run build`. - **기대 결과:** 모든 명령 exit 0, 동시 렌더링된 pagination select ID 중복 0건, 각 visible label과 control 연결 일치. - **수동 확인:** Community Sheet에서 root와 reply pagination이 함께 보이는 fixture로 각 페이지 크기 label을 클릭해 대응 select가 focus되는지 확인한다. **P1-R9 수정 검증 기록 (2026-07-31):** - RED: `npm run test:run -- src/shared/ui/__tests__/resource-pagination.test.tsx` — 1 failed / 2 passed. 두 `ResourcePagination`의 label `control`이 같은 `id="resource-page-size"` 해석에 의존해 첫 번째 select를 가리키는 실패를 확인했다. - GREEN focused: `npm run test:run -- src/shared/ui/__tests__/resource-pagination.test.tsx src/features/comments` — 성공, 3 files / 14 tests passed. React `useId` 기반으로 select ID 중복 0건과 각 label/control 연결 일치를 확인했다. - 개발 중 E2E: 사용자 지시에 따라 전체 Task 완료 전 반복 실행하지 않았다. 지정 mock Comments E2E는 최종 회귀 단계에서 필요 시 실행한다. ### Task R1.10 — crop frame과 저장 source 좌표 일치 복구 **Goal 실행 `P1-R10`:** `ImageCropDialog`에서 보이는 crop frame·이동·확대 결과와 canvas가 저장하는 원본 source rectangle을 같은 좌표계로 계산한다. - **연결 리뷰:** [Phase 1 현재 리뷰](./reviews/phase1-platform-auth-shared-ui.md) — `REV-P1-016` - **시작 조건:** PRD `FILE-008`, Image crop UI 흐름 4, 현재 `ImageCropDialog`의 CSS px offset과 `calculateCropSourceRect`의 원본 px offset 직접 공유. - **완료 증거:** 축소 표시된 대형 원본과 고정/free aspect 각각의 미리보기-저장 좌표 실패 test, 단일 좌표 변환 최소 구현, shared crop unit과 Character·Audio·Series·Community focused 회귀 및 mock Chromium crop 흐름 통과. - **Files:** Modify: `src/shared/ui/image-crop-dialog.tsx`, `src/shared/lib/crop-image.ts`, `src/shared/ui/__tests__/image-crop-dialog.test.tsx`, `src/shared/lib/crop-image.test.ts`; Test: `src/features/characters/tests/CharacterCreatePage.test.tsx`, `src/features/audio-contents/tests/audio-form.test.tsx`, `src/features/series/tests/series-form.test.tsx`, `src/features/community-posts/tests/community-form.test.tsx`, `tests/e2e/character-workspace.spec.ts`, `tests/e2e/audio-content.spec.ts`, `tests/e2e/series.spec.ts`, `tests/e2e/community.spec.ts`. - **Interfaces:** `CropRenderRequest`의 offset 의미를 표시 frame 기준으로 명확히 하고, preview transform과 `calculateCropSourceRect`가 동일한 scale·clamp를 사용한다. resource별 aspect와 최대 출력 크기는 바꾸지 않는다. - **범위 밖:** 새 crop library/dependency, 회전·필터·다중 crop, 서버 파일 계약 변경. - **Phase 1 Gate 연결:** shared crop focused unit, 네 도메인 crop 회귀, mock Chromium crop journey, `typecheck`·`lint`·build가 모두 통과하기 전에는 종결하지 않는다. - [x] **RED:** 4,000×3,000 원본이 341×256px로 표시될 때 10 CSS px 이동이 원본 scale에 맞게 환산되고, 1:1·210:297 frame 밖 영역이 결과에 포함되지 않아야 하는 실패 test를 작성했다. - [x] **GREEN:** 실제 preview frame 크기와 표시 image scale을 기준으로 source offset을 계산하고, 고정/free aspect frame과 canvas source rectangle을 일치시켰다. - [x] **REFACTOR:** 좌표 계산을 shared crop 함수 한 곳에 유지하고 도메인별 보정값이나 새 상태 library를 만들지 않은 채 관련 회귀를 실행했다. - **실행 명령:** `npm run test:run -- src/shared/lib/crop-image.test.ts src/shared/ui/__tests__/image-crop-dialog.test.tsx`; `npm run test:run -- src/features/characters src/features/audio-contents src/features/series src/features/community-posts`; `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts tests/e2e/audio-content.spec.ts tests/e2e/series.spec.ts tests/e2e/community.spec.ts --project=chromium`; `npm run typecheck`; `npm run lint`; `npm run build`. - **기대 결과:** 모든 명령 exit 0, preview frame과 저장 결과의 source rectangle 일치, resource별 aspect·no-upscale 회귀 0건. - **수동 확인:** 4:3 대형 image를 Character/Audio의 1:1, Series의 210:297, Community의 free crop에서 이동·확대하고 적용 전 frame과 적용 후 preview의 같은 영역을 비교한다. **P1-R10 수정 검증 기록 (2026-07-31):** - RED: `npm run test:run -- src/shared/lib/crop-image.test.ts src/shared/ui/__tests__/image-crop-dialog.test.tsx` — 2 failed / 14 passed. 4,000×3,000 원본의 1:1 preview frame에서 10 CSS px 이동이 sourceX 490으로 계산됐고, Dialog의 `renderCrop` request에는 `previewFrameWidth/Height`가 없어 기대대로 실패했다. - GREEN: `CropRenderRequest`와 `calculateCropSourceRect`가 적용 시점의 preview frame 크기를 optional로 받아 CSS px offset을 source px로 환산하게 했다. `ImageCropDialog`는 preview image의 `getBoundingClientRect()`를 request에 전달한다. - Focused: 같은 command — 2 files / 16 tests passed. 1:1과 210:297 frame source rect 환산, 기존 zoom/no-upscale/keyboard/pinch/free ratio 회귀를 확인했다. - 도메인 회귀: `npm run test:run -- src/features/characters` — 6 files / 43 tests passed. `npm run test:run -- src/features/audio-contents` — 8 files / 55 tests passed. `npm run test:run -- src/features/series` — 9 files / 38 tests passed. `npm run test:run -- src/features/community-posts`는 180초 timeout이 아닌 1 failed / 43 passed로 `community-sheet.test.tsx` 목록 로딩 실패가 재현됐으나, 같은 spec 단독은 1 file / 7 tests passed, `community-form.test.tsx` 단독은 1 file / 10 tests passed로 crop 변경 실패와 분리했다. - LSP diagnostics: `src/shared/lib/crop-image.ts`, `src/shared/ui/image-crop-dialog.tsx`, `src/shared/ui/__tests__/image-crop-dialog.test.tsx`, `src/shared/lib`, `src/shared/ui` — 오류 0건. `src/shared/lib/crop-image.test.ts` 단일 diagnostics는 timeout됐고 directory diagnostics로 보완했다. - E2E/수동 확인: 사용자 지시에 따라 개발 중 반복 E2E는 실행하지 않고 최종 회귀 단계에서 필요 시 mock Chromium crop 흐름을 실행한다. ### Task R1.11 — crop 적용 비동기·Blob URL 수명 복구 **Goal 실행 `P1-R11`:** crop 적용을 single-flight로 처리하고 실패를 복구 가능하게 표시하며 생성한 Blob URL을 적용·취소·교체·unmount에서 정확히 한 번 해제한다. - **연결 리뷰:** [Phase 1 현재 리뷰](./reviews/phase1-platform-auth-shared-ui.md) — `REV-P1-017` - **시작 조건:** `P1-R10` 완료, 현재 다섯 crop source 생성 경로가 성공한 `URL.createObjectURL`을 해제하지 않고 `ImageCropDialog.applyCrop`이 pending/error 상태를 갖지 않는 구조. - **완료 증거:** 적용 연타·render reject·URL 교체/취소/unmount 실패 test, 최소 pending/error/cleanup contract, shared와 네 소비 도메인 회귀 및 정적 Gate 통과. - **Files:** Create: `src/shared/lib/create-image-crop-source.ts`, `src/shared/lib/create-image-crop-source.test.ts`; Modify: `src/shared/ui/image-crop-dialog.tsx`, `src/shared/ui/__tests__/image-crop-dialog.test.tsx`, `src/features/characters/pages/CharacterCreatePage.tsx`, `src/features/characters/pages/CharacterEditPage.tsx`, `src/features/characters/tests/CharacterCreatePage.test.tsx`, `src/features/characters/tests/CharacterEditPage.test.tsx`, `src/features/audio-contents/pages/AudioContentFormPage.tsx`, `src/features/audio-contents/tests/audio-form.test.tsx`, `src/features/series/pages/SeriesFormPage.tsx`, `src/features/series/tests/series-form-crop.test.tsx`, `src/features/community-posts/lib/create-community-post-crop-source.ts`, `src/features/community-posts/tests/community-form.test.tsx`. - **Interfaces:** crop source는 자신이 만든 Blob URL의 idempotent release 책임을 노출한다. 적용 pending 동안 적용·취소·이동 control의 재진입을 막고 `role=status`를 표시하며 reject 시 dialog와 원본 선택을 유지해 재시도할 수 있게 한다. - **범위 밖:** upload 진행률, File 자체 폐기, 범용 async hook, 외부 image editor dependency. - **Phase 1 Gate 연결:** shared lifecycle unit과 각 도메인 cancel/stale/reject 회귀, `typecheck`·`lint`·build가 통과하기 전에는 완료하지 않는다. - [x] **RED:** 적용 연타 시 renderer 1회, reject 후 visible alert·재시도, 적용·취소·새 선택·unmount마다 소유 Blob URL `revokeObjectURL` 정확히 1회를 기대하는 실패 test를 작성한다. - [x] **GREEN:** 공통 crop source의 idempotent release와 dialog single-flight/error 상태를 최소 구현하고 각 소비 화면이 수명 종료 시 release한다. - [x] **REFACTOR:** 중복된 다섯 `createObjectURL` helper를 공통 함수로만 합치고 도메인 form state나 upload contract는 바꾸지 않는다. - **실행 명령:** `npm run test:run -- src/shared/lib/create-image-crop-source.test.ts src/shared/ui/__tests__/image-crop-dialog.test.tsx`; `npm run test:run -- src/features/characters src/features/audio-contents src/features/series src/features/community-posts`; `npm run typecheck`; `npm run lint`; `npm run build`; `git diff --check`. - **기대 결과:** 모든 명령 exit 0, pending 중 renderer 최대 1회, reject 후 입력 보존·재시도 가능, 소유 Blob URL 누수·중복 revoke 0건. - **수동 확인:** 10MB에 가까운 image를 연속 선택·취소·적용하고 느린 crop renderer 조건에서 진행 안내, control 비활성화, 실패 후 재시도를 확인한다. **P1-R11 수정 검증 기록 (2026-07-31):** - RED: `npm run test:run -- src/shared/lib/create-image-crop-source.test.ts src/shared/ui/__tests__/image-crop-dialog.test.tsx`는 구현 전 `create-image-crop-source` import 부재와 `ImageCropDialog` pending/error/single-flight 부재로 실패했다. - GREEN: `createImageCropSource`가 소유 Blob URL의 idempotent `release`를 노출하고 load error에서 즉시 revoke한다. `ImageCropDialog`는 적용 중 `role=status`, control disabled, renderer reject `role=alert`, 재시도를 제공한다. - REFACTOR: Character create/edit, Audio, Series, Community create/sheet의 중복 `createObjectURL` helper를 공통 helper로만 합쳤고, 각 crop source 교체·취소·적용·unmount와 stale selection에서 `release`를 호출한다. 도메인 form state와 upload contract는 변경하지 않았다. - Focused 검증: `npm run test:run -- src/shared/lib/create-image-crop-source.test.ts src/shared/ui/__tests__/image-crop-dialog.test.tsx` — 2 files / 11 tests passed. 인접 도메인 단독 검증은 Character create/edit, Audio form 3종, Series form — 6 files / 57 tests passed, Community form — 1 file / 10 tests passed, Community sheet 단독 — 1 file / 7 tests passed였다. - 정적 Gate: `src/shared/lib`, `src/shared/ui`, `src/features/characters/pages`, `src/features/audio-contents`, `src/features/series`, `src/features/community-posts` LSP diagnostics 0건. `npm run typecheck`, `npm run lint`, `npm run build`는 exit 0이었다. `npm run build`는 기존 bundle size warning(502.94 kB chunk)을 표시했다. - 넓은 회귀 참고: `npm run test:run -- src/features/characters src/features/audio-contents src/features/series src/features/community-posts`는 `audio-list.test.tsx` 1 failed 후 timeout됐고, 별도 crop 인접 재실행은 모두 통과해 이번 crop lifecycle 변경과 분리했다. E2E와 10MB 수동 확인은 사용자 지시에 따라 최종 회귀 단계에서 필요 시 실행한다. ### Task R1.12 — fetch·XHR 교차 401 만료 전환 단일화 **Goal 실행 `P1-R12`:** 공통 fetch client와 Audio XHR upload가 동시에 401을 받아도 현재 session의 제거·login 이동을 합쳐 한 번만 수행하고, 새 로그인 session에서는 다음 만료를 다시 처리한다. - **연결 리뷰:** [Phase 1 현재 리뷰](./reviews/phase1-platform-auth-shared-ui.md) — `REV-P1-018` - **시작 조건:** PRD `AUTH-005`, §13의 동시 401 단일 처리, 기존 완료 `P1-R6`과 `P4-R2`의 transport별 401 burst contract. - **완료 증거:** 같은 mutable session dependency를 사용하는 fetch request와 XHR upload가 동시에 401일 때 callback 2회를 재현하는 실패 test, transport 간 현재 session guard 최소 수정, auth/API·Audio upload focused 및 Phase 1 Gate 검증 기록. - **Files:** Modify: `src/shared/api/client.ts`, `src/shared/api/__tests__/client-auth.test.ts`, `src/features/audio-contents/api/upload-audio-content.ts`, `src/features/audio-contents/tests/audio-upload.test.ts`; Create: `src/features/audio-contents/tests/audio-upload-auth-lifecycle.test.ts`. - **Interfaces:** 두 transport는 401 처리 시 현재 protected session 존재 여부를 같은 `getToken` 의미로 확인하고, 첫 handler가 동기적으로 session을 제거한 뒤의 handler는 `clearSession`/`onAuthExpired`를 다시 호출하지 않는다. 새 login token이 생기면 다음 401 burst는 다시 처리한다. - **범위 밖:** refresh token, 전역 event bus, router 교체, XHR→fetch 전환, 403 정책 변경. - **Phase 1 Gate 연결:** cross-transport RED/GREEN, 기존 transport별 malformed/동시 401 회귀, app/auth/shared unit, `typecheck`·`lint`·build가 통과하기 전에는 완료 처리하지 않는다. - **검증 기록:** RED callback 실제 횟수, GREEN 뒤 동일·후속 session burst 횟수와 Gate 결과를 이 Task 하단과 Phase 1 리뷰에 누적한다. - [x] **RED:** mutable token을 공유하는 `createApiClient` request와 `uploadAudioContent`를 동시에 401로 종료해 `clearSession`/`onAuthExpired` 각각 1회를 기대하고 현재 3회 호출 실패를 확인했다. 이어 새 token을 설정한 다음 burst는 다시 1회 처리돼야 한다. - [x] **GREEN:** 두 transport가 callback 실행 직전 현재 session 존재를 확인하고 첫 clear 뒤 후속 handler가 만료 전환을 건너뛰도록 최소 수정했다. - [x] **REFACTOR:** 기존 transport 내부 burst latch와 malformed response mapping을 유지하고 인증 coordinator나 새 dependency를 추가하지 않았다. - **실행 명령:** `npm run test:run -- src/shared/api/__tests__/client-auth.test.ts src/features/audio-contents/tests/audio-upload.test.ts src/features/audio-contents/tests/audio-upload-auth-lifecycle.test.ts`; `npm run test:run -- src/app src/features/auth src/shared/api src/features/audio-contents/tests/audio-upload.test.ts src/features/audio-contents/tests/audio-upload-auth-lifecycle.test.ts`; `npm run typecheck`; `npm run lint`; `npm run build:dev`; `npm run build:prod`; `git diff --check`. - **기대 결과:** 모든 명령 exit 0, 같은 session의 fetch+XHR 401 callback 각각 총 1회, 새 session의 다음 401 callback 각각 추가 1회, 기존 403·malformed body·upload cancel/retry 회귀 0건. - **수동 확인:** upload 진행 중 다른 보호 조회도 401을 반환하게 해 로그인 이동·만료 안내·browser history 변화가 한 번인지 확인하고, 다시 로그인한 뒤 같은 만료 흐름이 재동작하는지 확인한다. **P1-R12 수정 검증 기록 (2026-07-31):** - RED: `npm run test:run -- src/shared/api/__tests__/client-auth.test.ts src/features/audio-contents/tests/audio-upload.test.ts` — 1 failed / 19 passed. 신규 교차 transport test에서 `clearSession` 기대 2회 대비 실제 3회로 같은 session의 fetch+XHR 401 만료 전환 중복을 재현했다. - GREEN: `client.ts`와 `upload-audio-content.ts`가 401 callback 직전 현재 token 존재를 다시 확인하게 수정했다. 같은 focused 명령은 2 files / 20 tests passed였다. - REFACTOR/회귀: 새 coordinator나 dependency 없이 `readToken` helper만 중복 제거에 사용했고, lifecycle test를 별도 파일로 분리해 touched TS files를 250 LOC 이하로 유지했다. reviewer blocker로 fetch-first 순서를 추가한 뒤 `auth` 주입 시 storage fallback을 쓰지 않도록 보완했다. `npm run test:run -- src/shared/api/__tests__/client-auth.test.ts src/features/audio-contents/tests/audio-upload.test.ts src/features/audio-contents/tests/audio-upload-auth-lifecycle.test.ts` — 3 files / 22 tests passed. `npm run test:run -- src/app src/features/auth src/shared/api src/features/audio-contents/tests/audio-upload.test.ts` — 13 files / 91 tests passed. `npm run test:run` — 79 files / 397 tests passed. `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod`, targeted `git diff --check` — 모두 exit 0. build는 기존 503.04kB chunk warning만 표시했다. LSP diagnostics는 변경 파일·디렉터리 기준 오류 0건이었다. - E2E/수동 확인: 사용자 지시에 따라 개발 중 E2E는 반복 실행하지 않고 모든 Task 구현 뒤 필요 시 수행한다. ### Task R1.13 — crop frame과 저장 영역 시각적 일치 복구 **Goal 실행 `P1-R13`:** 고정 aspect image crop에서 운영자가 보는 frame과 canvas가 저장하는 source rectangle을 하나의 비변환 좌표계로 일치시킨다. - **연결 리뷰:** [Phase 1 현재 리뷰](./reviews/phase1-platform-auth-shared-ui.md) — `REV-P1-019` - **시작 조건:** PRD `FILE-005~008`, `FILE-010`, `FILE-012`, 기존 `P1-R10` 완료 기록과 현재 `ImageCropDialog`이 transformed ``의 rect를 crop frame 크기로 사용하는 구조. - **완료 증거:** 4:3 원본의 1:1과 `210:297` 고정 frame에서 보이는 영역과 저장 source rectangle 불일치를 재현하는 실패 test, 실제 crop viewport rect 기반 최소 수정, shared·4개 소비 도메인 회귀와 browser 수동 비교 기록. - **Files:** Modify: `src/shared/ui/image-crop-dialog.tsx`, `src/shared/lib/crop-image.ts`; Test: `src/shared/ui/__tests__/image-crop-dialog.test.tsx`, `src/shared/lib/crop-image.test.ts`, `tests/e2e/character-workspace.spec.ts`, `tests/e2e/audio-content.spec.ts`, `tests/e2e/series.spec.ts`, `tests/e2e/community.spec.ts`. - **Interfaces:** `previewFrameWidth/Height`는 CSS transform이 적용된 image rect가 아니라 실제 crop viewport를 의미한다. image cover scale, offset, zoom과 clamp는 한 번만 변환하고 resource별 aspect·최대 폭·no-upscale 계약은 유지한다. - **범위 밖:** 외부 crop library, 회전·필터, 도메인별 보정값, 서버 file 계약 변경. - **Phase 1 Gate 연결:** shared crop focused unit, Character·Audio·Series·Community crop 회귀, mock Chromium crop journey, `typecheck`·`lint`·개발/운영 build가 모두 통과하기 전에는 완료하지 않는다. - [x] **RED:** 4,000×3,000 원본에 1:1과 `210:297` policy를 적용해 crop viewport의 실제 aspect, 이동·확대 후 저장 source rectangle, frame 밖 pixel 미포함을 기대하는 실패 test를 작성했다. - [x] **GREEN:** 고정 aspect crop viewport를 보이게 렌더하고 그 viewport의 비변환 rect를 source 계산에 전달하는 최소 구현을 적용했다. - [x] **REFACTOR:** preview→source 변환과 clamp를 shared crop 함수 한 곳에 유지하고 zoom이 `getBoundingClientRect()`와 수식에 중복 반영되지 않는지 회귀했다. - [x] RED/GREEN/REFACTOR, focused·도메인 단위·정적 검증 결과를 Progress와 Phase 1 리뷰에 누적했다. E2E와 수동 pixel 비교는 사용자 지시에 따라 전체 Task 구현 후 필요 시 수행한다. - **실행 명령:** `npm run test:run -- src/shared/lib/crop-image.test.ts src/shared/ui/__tests__/image-crop-dialog.test.tsx`; `npm run test:run -- src/features/characters src/features/audio-contents src/features/series src/features/community-posts`; `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts tests/e2e/audio-content.spec.ts tests/e2e/series.spec.ts tests/e2e/community.spec.ts --project=chromium`; `npm run typecheck`; `npm run lint`; `npm run build:dev`; `npm run build:prod`. - **기대 결과:** 모든 명령 exit 0, 고정 aspect frame과 저장 영역 불일치 0건, zoom/offset 중복 scale 0건, free aspect·no-upscale 회귀 0건. - **수동 확인:** 4:3 대형 image를 Character/Audio 1:1과 Series `210:297`에서 이동·확대한 뒤 적용 전 frame과 적용 후 preview가 같은 영역인지 비교한다. **P1-R13 수정 검증 기록 (2026-07-31):** - RED: `npm run test:run -- src/shared/ui/__tests__/image-crop-dialog.test.tsx` — 1 failed / 9 passed. 신규 `ImageCropDialog measures the visible crop viewport instead of the transformed image`가 `이미지 crop viewport` label 부재로 실패해 고정 crop viewport가 없는 현재 구조를 재현했다. - GREEN: `ImageCropDialog`에 고정 aspect viewport를 렌더하고 `applyCrop`이 image rect 대신 viewport rect를 `previewFrameWidth/Height`로 전달하게 했다. 1차 reviewer blocker였던 image box/frame 불일치는 source aspect 기준 cover image 배치로 보완했다. - REFACTOR/회귀: `src/shared/lib/crop-image.test.ts`에 4,000×3,000 원본의 1:1·`210:297` viewport+zoom source rect 회귀를 추가하고, zero-overhang 축에서 화면 offset과 저장 offset이 함께 clamp되는 dialog 회귀를 추가했다. `npm run test:run -- src/shared/lib/crop-image.test.ts src/shared/ui/__tests__/image-crop-dialog.test.tsx` — 2 files / 20 tests passed. `npm run test:run -- src/features/characters src/features/audio-contents src/features/series src/features/community-posts` — 30 files / 183 tests passed. - 정적 검증: `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0. build는 기존 500kB chunk warning을 표시했다. LSP diagnostics는 `image-crop-dialog.tsx`, `image-crop-dialog.test.tsx`, `crop-image.test.ts` 오류 0건이다. - 리뷰어 게이트: 1차 review에서 cover geometry와 offset clamp blocker 2건을 지적받아 보완했고, 2차 delta review에서 `APPROVED` 판정을 받았다. - 환경 정리: 검증 중 `/var/.../T` 여유 공간 부족으로 Vitest가 `ENOSPC`를 반환해, 워크스페이스 밖 `/var/.../T/opencode`의 재생성 가능한 DerivedData/cache 임시 폴더를 삭제했다. `df` 기준 여유 공간은 531MiB에서 15GiB로 회복된 뒤 검증을 재실행했다. - E2E/수동: 개발 중 E2E는 사용자 지시에 따라 매 Task마다 수행하지 않고 전체 Task 구현 후 필요 시 수행한다. 실제 pixel 수동 비교도 같은 최종 확인 범위로 남긴다. ### Task R1.14 — Character 생성·수정 route 보호 인가 probe 복구 **Goal 실행 `P1-R14`:** `/ai-characters/new`와 `/ai-characters/:characterId/edit`도 다른 관리자 route와 같이 서버 보호 route probe의 401/403 정책을 먼저 적용한다. - **연결 리뷰:** [Phase 1 현재 리뷰](./reviews/phase1-platform-auth-shared-ui.md) — `REV-P1-020` - **시작 조건:** `P1-R13` 완료, PRD `AUTH-005~006`, `isAiCharactersRoute()`가 Character list/detail과 하위 resource route는 포함하지만 create/edit route는 제외하는 현재 구조. - **완료 증거:** create/edit 직접 URL의 probe 403에서 접근 거부 화면, 401에서 session 정리·login 이동, probe 완료 전 하위 form/API 미실행을 고정하는 실패 test와 Phase 1 Gate 기록. - **Files:** Modify: `src/app/browser-location.ts`, 필요 시 `src/app/App.tsx`; Create: `src/app/browser-location.test.ts`; Modify: `src/app/App.protected-errors.test.tsx`, `src/app/App.test.tsx`. - **Interfaces:** `isAiCharactersRoute` 보호 route 판정은 list, create, detail, edit와 모든 하위 resource route를 포함한다. 403은 session을 유지한 채 `/access-denied`, 401은 session을 제거하고 `/login`으로 이동하는 기존 계약을 변경하지 않는다. - **범위 밖:** router library 도입, 서버 인증 계약 변경, role refresh, 새 access-denied 화면. - [x] **RED:** create/edit 직접 route에서 보호 probe 403을 반환할 때 form이 노출되거나 일반 error로 남는 현재 실패와, 401의 login 이동을 재현했다. - [x] **GREEN:** 두 route를 보호 route 판정에 포함하는 최소 수정으로 기존 probe 흐름을 재사용했다. - [x] **REFACTOR:** route matcher 진리표를 unit test로 고정하고 path 문자열 중복과 별도 auth abstraction을 추가하지 않았다. - [x] RED/GREEN/REFACTOR, focused·auth/app 회귀·정적/build 결과를 Progress와 Phase 1 리뷰에 누적했다. stale ADMIN 수동 확인은 실제 credential이 필요한 외부 수동 QA로 남긴다. - **실행 명령:** `npm run test:run -- src/app/browser-location.test.ts src/app/App.protected-errors.test.tsx src/app/App.test.tsx`; `npm run test:run -- src/app src/features/auth src/shared/api`; `npm run typecheck`; `npm run lint`; `npm run build:dev`; `npm run build:prod`. - **기대 결과:** 모든 명령 exit 0, create/edit probe 전 form·하위 request 0건, 403 접근 거부·401 login 전환 회귀 0건. - **수동 확인:** stale ADMIN session으로 create/edit URL을 직접 열고 개발 API 403에서 form이 잠시라도 노출되지 않으며 접근 거부 화면으로 이동하는지 확인한다. **P1-R14 수정 검증 기록 (2026-07-31):** - RED: `npm run test:run -- src/app/browser-location.test.ts src/app/App.protected-errors.test.tsx src/app/App.test.tsx` — 2 failed files / 5 failed / 18 passed. `isAiCharactersRoute`가 create/edit를 false로 판정했고, `/ai-characters/new`, `/ai-characters/101/edit`의 401/403 probe 전환 대신 form 또는 detail error가 노출되는 실패를 재현했다. - GREEN: `isAiCharactersRoute`에 `routePaths.aiCharacterCreate`와 `getCharacterEditIdFromPath(path)`를 추가해 기존 보호 route probe를 재사용했다. focused `npm run test:run -- src/app/browser-location.test.ts src/app/App.protected-errors.test.tsx src/app/App.test.tsx` — 3 files / 23 tests passed. - REFACTOR/회귀: 새 `src/app/browser-location.test.ts`의 list/create/detail/edit 진리표로 matcher를 고정했다. `npm run test:run -- src/app src/features/auth src/shared/api` — 13 files / 84 tests passed. - 정적 검증: LSP diagnostics `browser-location.ts`, `browser-location.test.ts`, `App.protected-errors.test.tsx` 오류 0건. `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0. build는 기존 500kB chunk warning을 표시했다. - 리뷰어 게이트: P1-R14 delta review에서 create/edit 보호 probe 합류, 401/403 form 미노출 test, 문서 기록 범위가 기준을 충족해 `APPROVED` 판정을 받았다. - 수동 확인: stale ADMIN 개발 API credential이 필요한 외부 수동 QA는 자동 검증 범위 밖으로 유지한다. ### Task R1.15 — 잘못된 percent-encoding route 안전 처리 **Goal 실행 `P1-R15`:** 잘못된 percent-encoding이 포함된 직접 URL이 `URIError`로 SPA를 중단하지 않고 안전한 기본 route로 정규화되게 한다. - **연결 리뷰:** [Phase 1 현재 리뷰](./reviews/phase1-platform-auth-shared-ui.md) — `REV-P1-021` - **시작 조건:** `P1-R14` 완료, `browser-location.ts`의 route regex가 `%`와 같은 segment를 허용하고 모든 parser가 `decodeURIComponent` 예외를 처리하지 않는 현재 구조. - **완료 증거:** 단일·복합 param route의 malformed `%` 재현 test, 예외 없는 공통 decode helper와 `/ai-characters` fallback, 정상 한글/ASCII ID route 회귀 및 Phase 1 Gate 기록. - **Files:** Modify: `src/app/browser-location.ts`, `src/app/browser-location.test.ts`, `src/app/App.test.tsx`. - **Interfaces:** route param decode 실패는 `null`로 판정해 `readRoutePath()`가 `/ai-characters`로 정규화하고, 정상 percent-encoded ID와 navigation API는 유지한다. - **범위 밖:** 404 page 신설, URL schema 재설계, router dependency, 백엔드 ID 제약 추정. - [x] **RED:** character/audio/series 단일·복합 param에 `%`, `%E0%A4%A`를 넣었을 때 `URIError`가 발생하는 실패 test를 작성했다. - [x] **GREEN:** 예외를 내부에서 종료하는 작은 decode helper로 parser를 공통화하고 잘못된 path를 기본 목록으로 보냈다. - [x] **REFACTOR:** 모든 `decodeURIComponent` 호출이 helper 하나를 통하도록 정리하고 정상 인코딩 route 회귀를 실행했다. - [x] RED/GREEN/REFACTOR, focused·app 회귀·정적/build 결과를 Progress와 Phase 1 리뷰에 누적했다. malformed URL 수동 확인은 최종 browser 확인 범위로 남긴다. - **실행 명령:** `npm run test:run -- src/app/browser-location.test.ts src/app/App.test.tsx`; `npm run test:run -- src/app`; `npm run typecheck`; `npm run lint`; `npm run build:dev`; `npm run build:prod`. - **기대 결과:** 모든 명령 exit 0, malformed encoding 예외 0건, `/ai-characters` 안전 fallback, 정상 route 회귀 0건. **P1-R15 수정 검증 기록 (2026-07-31):** - RED: `npm run test:run -- src/app/App.test.tsx` — 1 failed file / 3 failed / 8 passed. `/ai-characters/%`, `/ai-characters/%E0%A4%A/audio-contents/1`, `/ai-characters/101/series/%E0%A4%A`가 각각 `URIError: URI malformed`로 실패했다. - GREEN: `browser-location.ts`에 `decodeRouteSegment` helper를 추가하고 모든 route param decode가 malformed segment에서 `null`을 반환하게 했다. `npm run test:run -- src/app/browser-location.test.ts src/app/App.test.tsx` — 2 files / 12 tests passed. - REFACTOR/회귀: `rg -n "decodeURIComponent" src/app/browser-location.ts` — helper 내부 1건만 남았다. `npm run test:run -- src/app` — 5 files / 37 tests passed. - 정적 검증: LSP diagnostics `browser-location.ts`, `App.test.tsx` 오류 0건. `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0. build는 기존 500kB chunk warning을 표시했다. - 리뷰어 게이트: P1-R15 review에서 malformed route 예외 처리, helper 단일화, scope 제한, 문서 기록이 기준을 충족해 `APPROVED` 판정을 받았다. - 수동 확인: malformed URL browser 직접 입력 확인은 전체 Task 구현 후 필요 시 수행하는 E2E/수동 QA 범위로 남긴다. - **수동 확인:** 로그인/비로그인 상태에서 `/ai-characters/%`를 직접 열어 white screen 없이 안전한 화면으로 정규화되는지 확인한다. ### Task R1.16 — malformed route의 보호 probe 경계 복구 **Goal 실행 `P1-R16`:** decode할 수 없는 percent-encoding route를 canonical Character 목록 snapshot으로 정규화해 기존 ADMIN 보호 probe를 거친 뒤에만 shell을 렌더한다. - **연결 리뷰:** [Phase 1 현재 리뷰](./reviews/phase1-platform-auth-shared-ui.md) — `REV-P1-022` - **시작 조건:** `P1-R15` 완료 기록과 `readRoutePath()`가 regex에 맞는 malformed path를 raw `RoutePath`로 반환해 `isAiCharactersRoute()`가 false가 되는 재리뷰 근거. - **완료 증거:** malformed 단일·복합 param route가 보호 probe 403에서 shell을 노출하지 않고 `/access-denied`로 전환되는 실패 test, canonical fallback snapshot 최소 수정, 정상 인코딩·create/edit·하위 route 회귀와 Phase 1 Gate 기록. - **Files:** Modify: `src/app/browser-location.ts`, `src/app/App.protected-shell.test.tsx`; Test: `src/app/browser-location.test.ts`. - **Interfaces:** route regex 일치 뒤 필수 segment 중 하나라도 decode 실패하면 `readRoutePath()`는 `routePaths.aiCharacters`를 반환한다. 정상 encoded route와 `navigateTo` API는 유지한다. - **범위 밖:** router dependency, 404 page, backend ID 제약, 보호 probe 자체 재설계. - **Phase 1 Gate 연결:** malformed 401/403 focused test, app/auth/api 회귀, `typecheck`·`lint`·개발/운영 build가 모두 통과하기 전에는 완료하지 않는다. - [x] **RED:** ADMIN session과 malformed 단일 route에서 보호 probe 403을 반환할 때 현재 shell/list fallback이 먼저 렌더되는 실패 test를 작성했다. - [x] **GREEN:** `readRoutePath()`의 route 후보를 반환하기 전에 `isAiCharactersRoute()`를 통과시키고 실패 시 Character 목록 snapshot으로 귀결시킨다. - [x] **REFACTOR:** 정상 create/edit route parser를 유지하고 별도 auth abstraction이나 route table 재설계를 추가하지 않았다. - [x] RED/GREEN/REFACTOR와 focused 결과를 Progress와 Phase 1 리뷰에 누적했다. Phase 1 정적/build Gate는 `P9-R11`, `P10-R10`까지 완료한 뒤 사용자 지시에 따라 통합 실행한다. - **실행 명령:** `npm run test:run -- src/app/browser-location.test.ts src/app/App.protected-errors.test.tsx src/app/App.test.tsx`; `npm run test:run -- src/app src/features/auth src/shared/api`; `npm run typecheck`; `npm run lint`; `npm run build:dev`; `npm run build:prod`. - **기대 결과:** 모든 명령 exit 0, malformed route의 probe 우회·protected shell 선노출 0건, 401 login·403 access-denied 전환과 정상 route 회귀 0건. - **수동 확인:** stale ADMIN session으로 `/ai-characters/%`를 직접 열고 보호 probe가 끝나기 전 shell이 보이지 않으며 403 응답에서 접근 거부 화면으로 이동하는지 확인한다. **P1-R16 수정 검증 기록 (2026-07-31):** - RED: `npm run test:run -- src/app/App.protected-shell.test.tsx` — 1 failed / 7 passed. 신규 malformed route test가 기대한 `보호 route 확인 중` 대신 `AI 캐릭터 목록을 불러오는 중`을 받아 shell/list fallback 선노출을 재현했다. - GREEN/REFACTOR: `readRoutePath()`가 route regex 후보를 raw path로 반환하기 전에 `isAiCharactersRoute()`를 통과시키고, decode 실패로 보호 route 판정이 불가능하면 canonical `/ai-characters` snapshot으로 귀결되게 했다. 새 router dependency, auth abstraction, route table 재설계는 추가하지 않았다. - Focused 검증: `npm run test:run -- src/app/App.protected-shell.test.tsx src/app/browser-location.test.ts` — 2 files / 9 tests passed. 수동 QA와 E2E는 사용자 지시에 따라 전체 Task 구현 후 필요 시 실행한다. - 통합 검증: `npm run test:run` — 81 files / 411 tests passed. `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0. ### Task R1.17 — malformed route 인가 회귀 matrix·focused Gate 보강 **Goal 실행 `P1-R17`:** `P1-R16`의 canonical fallback을 단일·복합 malformed route와 401/403 전체 인가 경계에서 고정하고 실제 regression spec이 focused Gate에 포함되게 한다. - **연결 리뷰:** [Phase 1 현재 리뷰](./reviews/phase1-platform-auth-shared-ui.md) — `REV-P1-023` - **시작 조건:** `P1-R16` 제품 코드는 focused·full unit에서 통과하지만 신규 test는 `/ai-characters/%`+403 한 건뿐이고 Task 실행 명령은 `App.protected-shell.test.tsx`를 누락한 재리뷰 근거. - **완료 증거:** malformed 단일·Audio/Series 복합 route의 401/403 matrix에서 probe pending 중 shell 미노출, 401 login·session 제거, 403 access-denied·session 유지와 정상 encoded route 회귀, 실제 regression spec을 포함한 focused 명령 결과. - **Files:** Modify: `src/app/App.protected-shell.test.tsx`, `docs/20260725_AI캐릭터관리자웹/plan-task.md`, `docs/20260725_AI캐릭터관리자웹/reviews/phase1-platform-auth-shared-ui.md`; Test: 기존 `src/app/App.test.tsx`, `src/app/App.protected-errors.test.tsx`, `src/app/browser-location.test.ts`. - **Interfaces:** 제품 route parser와 App 보호 probe는 변경하지 않는다. test는 기존 MSW handler·session helper를 재사용하고 status/path matrix만 확장한다. matrix가 실제 제품 결함을 재현할 때만 root-cause 코드 수정을 이 Task에 포함한다. - **범위 밖:** router dependency, 별도 auth abstraction, backend ID 제약, E2E browser 확대. - **Phase 1 Gate 연결:** malformed 인가 matrix, app/auth/api 회귀, `typecheck`·`lint`·개발/운영 build가 모두 통과하기 전에는 완료하지 않는다. - [x] **TDD 예외 사유:** root-cause 제품 수정은 `P1-R16`에서 이미 GREEN이며 이번 산출물은 누락된 regression matrix와 실행 명령이다. 현재 test inventory와 focused 명령 누락을 RED 대체 증거로 사용했다. - [x] 단일·복합 malformed path × 401/403을 data-driven test로 고정하고 pending 중 protected shell·logout 미노출과 최종 auth 전환을 검증했다. - [x] `P1-R16` 실행 명령에 `App.protected-shell.test.tsx`를 포함하고 정상 encoded/create/edit route 회귀를 함께 실행했다. - [x] focused·Phase 1 회귀·정적/build 결과와 실제 test 수를 Progress와 Phase 1 리뷰에 누적했다. - **대체 검증 방법:** 현재 test matrix inventory, 필요 시 guard negative-control, focused/full unit과 정적/build Gate. - **실행 명령:** `npm run test:run -- src/app/App.protected-shell.test.tsx src/app/App.protected-errors.test.tsx src/app/App.test.tsx src/app/browser-location.test.ts`; `npm run test:run -- src/app src/features/auth src/shared/api`; `npm run typecheck`; `npm run lint`; `npm run build:dev`; `npm run build:prod`; `git diff --check`. - **기대 결과:** 모든 명령 exit 0, malformed 단일·복합 401/403 matrix 누락 0건, probe 전 shell·logout 노출 0건, 401/403·정상 route 회귀 0건. - **수동 확인:** 없음. 보호 경계는 deterministic MSW pending/status matrix로 검증하며 실제 stale ADMIN 개발 API 확인은 별도 수동 QA다. **P1-R17 수정 검증 기록 (2026-07-31):** - RED 대체: `REV-P1-023`에서 `/ai-characters/%`+403 한 건과 목록 heading만 보는 복합 malformed route test, focused 명령의 `App.protected-shell.test.tsx` 누락을 확인했다. 제품 guard는 `P1-R16`에서 이미 GREEN이라 production code는 변경하지 않았다. - GREEN focused: `src/app/App.protected-shell.test.tsx`에 단일·Audio 복합·Series 복합 malformed path × 401/403 matrix를 추가했다. `npm run test:run -- src/app/App.protected-shell.test.tsx` — 1 file / 14 tests passed. - REFACTOR/회귀: `npm run test:run -- src/app/App.protected-shell.test.tsx src/app/App.protected-errors.test.tsx src/app/App.test.tsx src/app/browser-location.test.ts` — 4 files / 40 tests passed. `npm run test:run -- src/app src/features/auth src/shared/api` — 13 files / 94 tests passed. - 정적/build: `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0. build는 기존 500kB 초과 chunk warning만 표시했다. E2E와 실제 stale ADMIN 개발 API 수동 QA는 사용자 지시에 따라 전체 Task 이후 필요 시/credential 준비 시로 유지한다. --- ## Phase 2. 개발 전용 Mock Preview 기반 **목표:** backend endpoint 구현 전에도 제공된 API Contract 범위의 최종 UI를 브라우저에서 확인하고, 명시적 전환으로 실제 개발 API와 같은 frontend code를 사용한다. **Phase Goal `P2`:** Task 2.1 → 2.3과 Phase 2 Gate로 browser MSW 기반 mock preview와 server mode 경계를 완성한다. - **시작 조건:** `P1-GATE` 완료. - **완료 조건:** `P2-T1`~`P2-T3`, `P2-GATE` 완료. mock/server 분리와 production 차단이 자동 test와 E2E로 고정됨. - **현재 상태:** 완료. `P2-T1`~`P2-T3`, `P2-GATE`, `P2-R1`~`P2-R16`의 구현·수정·검증이 끝났으며 Phase 3 진행 가능. **요구사항:** `MOCK-001~009`, PRD `11.1~11.2`, `12`, `13`. **주요 Files:** - Modify: `package.json`, `.env.example`, `src/shared/config/env.ts`, `src/shared/config/env.test.ts` - Modify: `src/app/App.tsx`, `src/main.tsx`, `vite.config.ts`, `playwright.config.ts` - Modify: `src/app/App.test.tsx`, `src/app/admin-pages.tsx`, `src/app/browser-location.ts` - Create: `src/app/protected-admin-shell.tsx` - Create: `src/shared/mocks/{browser,handlers,contract}.ts` - Create: `src/shared/mocks/__tests__/{mode-boundary,auth-handlers,mock-preview-docs,production-graph}.test.ts` - Create: `src/shared/mocks/browser.test.ts` - Create: `src/shared/ui/mock-mode-banner.tsx` - Create: `src/shared/ui/__tests__/mock-mode-banner.test.tsx` - Create: `public/mockServiceWorker.js` - Create: `tests/e2e/{mock-mode-boundary,mock-preview-shell,server-mode-boundary}.spec.ts` - Modify when implementation is complete: `README.md`, `docs/agent-guide/{environment,scripts}.md` ### Task 2.1 explicit API mode·production 경계 **Goal 실행 `P2-T1`:** `server | mock` mode와 dev-only browser worker bootstrap을 구성하고 자동 fallback·production mock을 차단한다. - **시작 조건:** `P1-GATE` 완료, PRD `MOCK-001~006` 확인. - **완료 증거:** env/script/bootstrap focused test, `dev` server 기본값, `dev:mock` explicit mode, production mock 거부와 404 no-fallback test. - **범위 밖:** Character 이후 도메인 fixture와 실제 backend endpoint 구현. - [x] `VITE_API_MODE=server | mock`을 검증하고 누락 기본값은 `server`로 고정하는 실패 test를 작성한다. - [x] `npm run dev:mock`만 Vite mock mode와 browser worker를 시작하고 기본 `npm run dev`는 실제 `VITE_API_BASE_URL`을 사용하는 script test를 작성한다. - [x] Playwright가 같은 spec을 explicit mode로 실행할 수 있도록 `e2e:mock`과 server mode 명령을 정의하고 각 webServer 환경을 test한다. - [x] `import.meta.env.DEV && apiMode === "mock"`일 때만 browser module을 dynamic import하고 worker 준비 후 React를 mount한다. - [x] production mode에서 `mock`을 설정하면 build/start 전에 설명 가능한 오류로 거부하고 worker·fixture가 production graph에서 실행되지 않는 test를 작성한다. - [x] `server` mode의 404·network error가 browser mock을 시작하거나 응답을 교체하지 않는 test를 작성한다. - [x] worker의 unhandled request 정책은 error로 두어 누락된 handler가 실제 backend로 조용히 통과하지 않게 한다. **P2-T1 구현 검증 기록 (2026-07-27):** - RED: `npm run test:run -- src/shared/config/env.test.ts src/shared/mocks/__tests__/mode-boundary.test.ts`는 mode/default/script 미구현으로 2 files 중 5 tests가 기대대로 실패했고, `VITE_API_MODE=mock npm run e2e -- tests/e2e/mock-mode-boundary.spec.ts`는 worker 미등록과 unhandled 정책 미구현으로 8 tests가 실패했다. - GREEN focused: `npm run test:run -- src/shared/config/env.test.ts src/shared/mocks/browser.test.ts src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/api/__tests__/client.test.ts`는 4 files / 18 tests 통과, `npm run e2e:mock -- tests/e2e/mock-mode-boundary.spec.ts`는 4 browser projects 통과, `npm run e2e -- tests/e2e/server-mode-boundary.spec.ts`는 4 browser projects 통과했다. - Production boundary: `VITE_API_MODE=mock npm run build:prod`는 guard 추가 전에는 부당하게 성공했고, guard 추가 후 `VITE_API_MODE=mock is only available during development` 오류로 기대대로 거부됐다. - Quality: `npm run typecheck`, `npm run lint`, `npm run test:run`(31 files / 117 tests), `npm run build:dev`, `npm run build:prod`, `git diff --check`를 성공했다. Playwright로 `http://127.0.0.1:8888/login`과 `http://127.0.0.1:8889/login`을 직접 열어 server mode는 worker registration 0건, mock mode는 `/mockServiceWorker.js` controller 등록을 확인했고, QA용 dev server 포트 `8888`·`8889`가 비었음을 확인했다. **P2-T1 리뷰 보강 기록 (2026-07-27):** - RED: `npm run test:run -- src/shared/mocks/__tests__/production-graph.test.ts src/shared/mocks/__tests__/mode-boundary.test.ts`는 production build 산출물에 `mockServiceWorker.js`가 복사되어 실패했고, `npm run e2e -- tests/e2e/server-mode-boundary.spec.ts`는 404·network error 후 no-fallback 증거가 없어 8 tests가 실패했다. - GREEN: production mode에서 `publicDir` 복사를 끄고 production graph test를 실제 `NODE_ENV=production` build 조건으로 맞춘 뒤 같은 focused unit은 2 files / 3 tests 통과했다. server mode E2E는 404·network error 요청 후에도 browser MSW registration 0건임을 확인해 4 browser projects / 12 tests 통과했다. - Re-review 보강: production graph test가 output file path까지 검사하도록 보강했고, server 404·network error는 오류 alert 표시 후 worker registration 0건을 확인하도록 `App`의 route error 표시 조건을 수정했다. `npm run test:run -- src/shared/mocks/__tests__/production-graph.test.ts src/shared/mocks/__tests__/mode-boundary.test.ts src/app/App.test.tsx`는 3 files / 16 tests 통과했고, `npm run e2e -- tests/e2e/server-mode-boundary.spec.ts`는 4 browser projects / 12 tests 통과했다. - Guide sync: `VITE_API_MODE`, `dev:mock`, `e2e:mock`, production mock 거부 기준을 `docs/agent-guide/environment.md`와 `docs/agent-guide/scripts.md`에 반영했다. ### Task 2.2 공통 preview session·fixture store **Goal 실행 `P2-T2`:** production API 경계를 그대로 사용하는 deterministic auth fixture, in-memory store와 mock mode 안내를 완성한다. - **시작 조건:** `P2-T1` 완료, PRD `AUTH-001~013`과 Phase 1에서 검증한 기존 인증 계약 확인. 인증 operation은 현 OpenAPI 범위 밖이며 `EXT-006`으로 유지한다. - **완료 증거:** auth handler/store/banner focused test와 login → protected shell mock preview E2E 기록. - **범위 밖:** 도메인별 endpoint handler와 계약 미제공 fixture. - [x] fixture는 정규화 `ApiResponse`와 실제 endpoint·header·body contract를 사용하고 별도 mock DTO를 만들지 않는다. - [x] ADMIN login·logout, 401·403 fixture와 새로고침 시 seed로 초기화되는 in-memory store를 만든다. - [x] mock mutation은 같은 store의 목록·상세 응답을 갱신하고 browser storage에 domain fixture를 영구 저장하지 않는다. - [x] mock mode임을 지속적으로 표시하는 접근 가능한 banner와 server mode에서 banner가 없는 test를 작성한다. - [x] JWT·password·signed URL·multipart body가 mock logger와 storage에 기록되지 않는 test를 작성한다. - [x] handler와 fixture가 `src/shared/test/server.ts`의 Node test lifecycle을 변경하지 않고 필요한 contract factory만 공유하게 한다. **P2-T2 실행 기록 (2026-07-27):** - RED: `npm run test:run -- src/shared/mocks/__tests__/auth-handlers.test.ts src/shared/ui/__tests__/mock-mode-banner.test.tsx src/app/App.test.tsx src/shared/mocks/browser.test.ts`는 `@/shared/mocks/handlers`·`@/shared/ui/mock-mode-banner` 미구현, App mock banner 부재, browser worker handler 미등록 기대 실패로 4 files 중 5 failures가 발생했다. `npm run e2e:mock -- tests/e2e/mock-preview-shell.spec.ts --project=chromium`은 mock login handler 부재로 `/login`에 머물러 기대대로 실패했다. - GREEN: `createMockHandlers(createMockStore())`가 `POST /admin/member/login`, `POST /member/logout`, `GET /api/v2/admin/ai-characters?page=0&size=20`을 정규화 envelope로 처리하고 logout mutation 후 같은 store에서 token을 폐기하며 새 store 생성 시 seed로 초기화한다. 리뷰 보강으로 logout 후 재login 시 같은 store에서 ADMIN token이 다시 활성화되도록 고정했다. 같은 focused unit 명령은 4 files / 25 tests 통과했고, `npm run e2e:mock -- tests/e2e/mock-preview-shell.spec.ts`는 4 browser projects / 8 tests 통과했다. - Sensitive data/storage: handler는 logger를 추가하지 않았고, focused test에서 login password/JWT 요청 후 `localStorage`, mock `sessionStorage`, cookie, IndexedDB, console log/warn/error에 fixture domain data와 민감값을 기록하지 않음을 확인했다. `src/shared/test/server.ts` lifecycle은 변경하지 않았다. ### Task 2.3 Mock Preview 반응형·문서화 **Goal 실행 `P2-T3`:** mock mode의 browser 접근성·반응형과 실행 문서를 검증해 사용자가 최종 UI 확인 mode를 명확히 선택하게 한다. - **시작 조건:** `P2-T1`, `P2-T2` 완료. - **완료 증거:** 320px·desktop mock shell E2E, axe 결과, README와 environment/scripts 가이드의 실제 명령 동기화 기록. - **범위 밖:** 도메인별 최종 UI와 server integration 완료 주장. - [x] mock banner가 320px·200% zoom에서 핵심 control을 가리지 않고 axe critical·serious 위반이 없는지 E2E로 확인한다. - [x] README에 `npm run dev`와 `npm run dev:mock`, mode 차이, mock data reset, production 금지와 no-auto-fallback을 기록한다. - [x] 구현이 완료된 뒤 `docs/agent-guide/environment.md`와 `scripts.md`에 실제 환경 변수와 명령을 추가한다. - [x] 후속 도메인 Phase가 handler·fixture·mock E2E를 소유한다는 규칙을 문서화한다. **P2-T3 실행 기록 (2026-07-27):** - RED: `npm run test:run -- src/shared/mocks/__tests__/mock-preview-docs.test.ts src/shared/mocks/__tests__/mode-boundary.test.ts`는 README에 실제 `dev`/`dev:mock`/`e2e`/`e2e:mock` script, mode 차이, mock data reset, production 금지, no-auto-fallback 기록이 없고 `docs/agent-guide/environment.md`와 `scripts.md`에 reset·domain handler/fixture/mock E2E 소유 규칙이 없어 2 tests가 기대대로 실패했다. `npm run e2e:mock -- tests/e2e/mock-preview-shell.spec.ts --project=chromium`은 새 320px·200% zoom 및 axe 확인을 포함해 5 tests가 통과해 기존 mock shell UI는 문서 보강 전에도 요구 접근성 동작을 만족함을 확인했다. - GREEN: README에 `npm run dev`/`dev:mock`/`e2e`/`e2e:mock`의 실제 명령, server mode와 mock mode 차이, mock data reset, production mock 거부, no-auto-fallback, 후속 도메인 handler·fixture·mock E2E 소유 규칙을 추가했다. `docs/agent-guide/environment.md`에는 `VITE_API_MODE=server | mock`, reset, production 금지, no-auto-fallback을 동기화하고 `docs/agent-guide/scripts.md`에는 실제 script와 domain ownership rule을 동기화했다. 같은 focused unit 명령은 2 files / 4 tests 통과했고, `npm run e2e:mock -- tests/e2e/mock-preview-shell.spec.ts --project=chromium`은 5 tests 통과했다. 최종 확인으로 `npm run e2e:mock -- tests/e2e/mock-preview-shell.spec.ts`는 4 browser projects / 20 tests 통과, `npm run typecheck`, `npm run lint`, `git diff --check`도 통과했다. ### Phase 2 Gate **Goal 실행 `P2-GATE`:** mock/server mode 경계, auth preview와 production 차단을 최종 판정한다. - **시작 조건:** `P2-T1`~`P2-T3` 완료. - **완료 증거:** 아래 명령과 Expected 통과 및 Phase 2 Progress 기록. - **범위 밖:** mock 성공을 실제 도메인 server integration 성공으로 간주하는 것. ```bash npm run test:run -- src/shared/config src/shared/mocks src/shared/ui/__tests__/mock-mode-banner.test.tsx npm run e2e:mock -- tests/e2e/mock-preview-shell.spec.ts npm run typecheck npm run lint npm run build:dev npm run build:prod ``` **Expected:** `npm run dev:mock`에서는 ADMIN login → protected shell과 mock banner가 동작하고 실제 backend 요청은 0건이다. 기본 server mode는 실제 API 오류를 그대로 처리하며 production build는 browser mock을 활성화하지 않는다. **P2-GATE 실행 기록 (2026-07-27):** - `npm run test:run -- src/shared/config src/shared/mocks src/shared/ui/__tests__/mock-mode-banner.test.tsx` — 7 files / 20 tests 통과. - `npm run e2e:mock -- tests/e2e/mock-preview-shell.spec.ts` — 4 browser projects / 20 tests 통과. mock login → protected shell, logout 후 재login, 320px·200% zoom banner/control, axe critical·serious 0을 확인했다. - `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0. - LSP diagnostics: `src/shared/mocks/__tests__/mock-preview-docs.test.ts` clean, `tests/e2e` directory 6 files / 0 diagnostics. 단일 `mock-preview-shell.spec.ts` LSP 호출은 3초 fresh diagnostics timeout이었으나 directory diagnostics와 typecheck/lint로 보완했다. - 판정: Phase 2 mock/server mode 경계, auth preview, production build 경계, mock preview 반응형·접근성·문서화 완료. mock 통과는 후속 도메인 server integration 완료로 간주하지 않는다. ### Task R2.1 — 보호 route 검증 실패 시 fail-closed 복구 **Goal 실행 `P2-R1`:** Phase 2 no-auto-fallback 오류 UI가 Phase 1의 보호 shell 비노출 경계를 우회하지 않게 하고 회귀를 방지한다. - 연결 리뷰: [Phase 2 리뷰](./reviews/review-phase-2.md) — `REV-P2-001` - 시작 조건: - 저장된 ADMIN session으로 `/ai-characters`에 진입한 뒤 probe가 404 또는 network error로 실패할 때 보호 shell이 노출되는 현재 동작을 실패 test로 재현한다. - 완료 증거: - probe 성공 전과 401·403·404·network error에서 보호 shell 비노출 - 404·network error는 mock fallback 없이 보호 shell 밖의 오류 UI로 표시 - focused App test, server mode boundary E2E, auth E2E와 P2-GATE 실행 기록 - 범위 밖: - 인증 방식, session 저장 방식 또는 API client 전면 교체 - Phase 3 Character 화면 구현 - [x] 404·network error와 이전 오류 뒤 새 session probe pending에서 보호 shell이 노출되는 실패 test를 추가한다. - [x] 성공한 현재 token probe만 보호 shell을 열고 실패 오류는 shell 밖에서 표시하는 최소 상태 전이를 구현한다. - [x] `src/app/App.test.tsx`, `tests/e2e/server-mode-boundary.spec.ts`, 기존 auth E2E와 P2-GATE를 실행한다. - [x] 결과를 plan-task.md 검증 기록과 [Phase 2 리뷰 문서](./reviews/review-phase-2.md)에 누적한다. **P2-R1 수정 검증 기록 (2026-07-27):** - RED: `npm run test:run -- src/app/App.test.tsx src/shared/mocks/__tests__/auth-handlers.test.ts` — 404·network error에서 보호 shell `banner/main`이 렌더되어 App test 2건이 기대대로 실패했다. - GREEN focused: `npm run test:run -- src/app/App.test.tsx src/shared/mocks/__tests__/auth-handlers.test.ts src/shared/mocks/browser.test.ts` — 3 files / 27 tests 통과. - Surface: `npm run e2e -- tests/e2e/server-mode-boundary.spec.ts` — 4 browser projects / 12 tests 통과. 404·network error 후 mock worker 0건, 보호 shell `main` 0건, logout button 0건을 확인했다. - Regression: `npm run e2e -- tests/e2e/auth.spec.ts tests/e2e/accessibility-shell.spec.ts` — 4 browser projects / 16 tests 통과. - Reviewer blocker 보강: 같은 token 재사용 logout → login 뒤 probe 실패 시 이전 검증 token이 남는 회귀를 추가로 확인했다. RED `npm run test:run -- src/app/App.test.tsx` — 1 test failed. GREEN 같은 command — 1 file / 19 tests 통과. 보강 후 `npm run test:run -- src/app/App.test.tsx src/shared/mocks/__tests__/auth-handlers.test.ts src/shared/mocks/browser.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 4 files / 31 tests 통과, `npm run e2e -- tests/e2e/server-mode-boundary.spec.ts` — 12 tests 통과, `npm run e2e:mock -- tests/e2e/mock-preview-shell.spec.ts` — 20 tests 통과. #### create_goal objective 초안 — P2-R1 - objective: P2-R1의 확정 review 항목 REV-P2-001을 수정하고 Phase 1 보호 route 경계의 회귀를 방지한다. plan-task.md에 추가된 Task R2.1만 수행한다. - 완료 조건: 404·network error를 포함한 probe 비성공 상태에서 보호 shell 비노출, 독립 오류 UI, focused test, server/auth E2E, P2-GATE와 누적 기록이 모두 확인된다. - 금지 조건: 인증·session 아키텍처 교체나 Phase 3 Character 구현을 포함하지 않는다. - 중단 조건: 보호 route 확인 endpoint 자체가 변경됐다는 외부 계약이 확인되면 계약 문서를 먼저 갱신한다. ### Task R2.2 — Mock Preview API origin·JWT 오류 계약 복구 **Goal 실행 `P2-R2`:** browser mock handler를 설정된 API base URL에만 결합하고 invalid·revoked JWT logout을 계약 status로 반환한다. - 연결 리뷰: [Phase 2 리뷰](./reviews/review-phase-2.md) — `REV-P2-002`, `REV-P2-003` - 시작 조건: - 잘못된 origin의 login이 200을 반환하고 invalid·revoked JWT logout이 200을 반환하는 현재 동작을 각각 실패 test로 재현한다. - 완료 증거: - 설정된 `VITE_API_BASE_URL`의 정확한 URL·method만 handler가 처리하고 다른 origin은 `onUnhandledRequest: "error"` 경계에 남음 - invalid·revoked JWT logout은 Phase 1에서 검증한 기존 인증 계약의 401 오류 envelope를 반환 - 정상 login → logout → login 흐름과 403 fixture 회귀 없음 - focused handler test, mock preview E2E와 P2-GATE 실행 기록 - 범위 밖: - 계약 미제공 endpoint·DTO·회원 role 정책 추정 - 도메인별 fixture 선행 구현 - [x] wrong-origin login과 invalid·두 번째 logout의 현재 200 응답을 실패 test로 고정한다. - [x] handler 생성 시 API base URL을 주입해 production endpoint URL에만 매칭한다. - [x] token access 상태를 확인한 뒤 logout store를 변경하고 invalid·revoked token에 401을 반환한다. - [x] auth handler focused test, mock preview E2E와 P2-GATE를 실행한다. - [x] 결과를 plan-task.md 검증 기록과 [Phase 2 리뷰 문서](./reviews/review-phase-2.md)에 누적한다. **P2-R2 수정 검증 기록 (2026-07-27):** - RED: `npm run test:run -- src/app/App.test.tsx src/shared/mocks/__tests__/auth-handlers.test.ts` — wrong-origin login이 200, invalid JWT logout이 200으로 응답해 auth handler test 2건이 기대대로 실패했다. - GREEN focused: `npm run test:run -- src/app/App.test.tsx src/shared/mocks/__tests__/auth-handlers.test.ts src/shared/mocks/browser.test.ts` — 3 files / 27 tests 통과. `createMockHandlers(store, apiBaseUrl)` exact origin, invalid·revoked JWT 401, browser worker runtime env 주입을 확인했다. - Surface: `npm run e2e:mock -- tests/e2e/mock-preview-shell.spec.ts` — 4 browser projects / 20 tests 통과. 정상 mock login → protected shell, logout 후 재login, 320px·200% zoom, axe critical·serious 0건을 확인했다. - P2 focused Gate: `npm run test:run -- src/shared/config src/shared/mocks src/shared/ui/__tests__/mock-mode-banner.test.tsx` — 7 files / 23 tests 통과. #### create_goal objective 초안 — P2-R2 - objective: P2-R2의 확정 review 항목 REV-P2-002와 REV-P2-003을 수정하고 mock/production API 계약 경계를 일치시킨다. plan-task.md에 추가된 Task R2.2만 수행한다. - 완료 조건: exact API origin, invalid·revoked JWT 401, 기존 정상 auth preview, focused test, mock E2E, P2-GATE와 누적 기록이 모두 확인된다. - 금지 조건: 계약 미제공 endpoint·DTO·회원 role 의미를 추정하거나 도메인 fixture를 추가하지 않는다. - 중단 조건: logout의 non-ADMIN token 정책이 현재 계약만으로 판정되지 않으면 해당 경로는 확장하지 않고 백엔드 확인 항목으로 기록한다. ### Task R2.3 — Phase 번호 표시·구현 문서 정합성 복구 **Goal 실행 `P2-R3`:** Phase 2 완료 뒤 남은 사용자 표시와 plan의 Files·Progress를 실제 구현에 맞게 정렬한다. - 연결 리뷰: [Phase 2 리뷰](./reviews/review-phase-2.md) — `REV-P2-004`, `REV-P2-005` - 시작 조건: - 보호 shell 빈 상태가 Character 연결 시점을 Phase 2로 표시하고, Phase 2 주요 Files와 하단 Progress가 실제 변경 경로·완료 기록과 다른 상태를 확인한다. - 완료 증거: - 빈 상태 문구와 관련 test가 Character 구현 Phase 3을 가리킴 - Phase 2 주요 Files가 실제 수정·생성·test 경로와 일치함 - 하단 검증 기록에 Phase 2 구현의 무엇을/왜/실제 명령·결과/남은 항목이 누적됨 - 문서 test, App focused test와 `git diff --check` 실행 기록 - 범위 밖: - Phase 3 Character 기능 구현 - 기존 Phase 2 구현·Gate 기록 삭제 또는 덮어쓰기 - [x] 현재 Phase 2 문구와 Files·Progress 불일치를 test와 파일 존재 검사로 고정한다. - [x] 사용자 표시를 Phase 3으로 바꾸고 Phase 2 주요 Files를 실제 경로로 갱신한다. - [x] Phase 2 구현·Gate 검증을 하단 Progress에 기존 기록을 보존한 채 누적한다. - [x] 문서/App focused test와 `git diff --check`를 실행한다. - [x] 결과를 plan-task.md 검증 기록과 [Phase 2 리뷰 문서](./reviews/review-phase-2.md)에 누적한다. **P2-R3 수정 검증 기록 (2026-07-27):** - RED: `npm run test:run -- src/app/App.test.tsx src/shared/mocks/__tests__/mock-preview-docs.test.ts` — Phase 3 placeholder와 실제 Phase 2 Files·Progress 기록이 없어 2 files 중 3 tests가 기대대로 실패했다. - GREEN focused: 같은 command — 2 files / 21 tests 통과. - Final Gate: 1차 `npm run test:run` — 34 files / 135 tests 통과. Reviewer blocker 보강 후 최종 `npm run test:run` — 34 files / 136 tests 통과. `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod`, `git diff --check` 모두 성공. - Production guard: `VITE_API_MODE=mock npm run build:prod` — 기대대로 `VITE_API_MODE=mock is only available during development` 오류로 거부됐다. - LSP diagnostics: `src/app` directory 3 files / 0 diagnostics, `src/shared/mocks` directory 8 files / 0 diagnostics. `App.tsx` 단일 fresh diagnostics는 timeout이었고 directory diagnostics와 typecheck로 보완 확인했다. #### create_goal objective 초안 — P2-R3 - objective: P2-R3의 확정 review 항목 REV-P2-004와 REV-P2-005를 수정하고 Phase 번호 표시와 계획 문서 추적성을 복구한다. plan-task.md에 추가된 Task R2.3만 수행한다. - 완료 조건: 사용자 문구, 실제 Files, 하단 Progress, focused test와 문서 검증 기록이 서로 일치한다. - 금지 조건: Phase 3 기능 구현이나 기존 완료 기록 삭제·덮어쓰기를 수행하지 않는다. - 중단 조건: Phase 번호가 다시 변경되면 현재 기록을 지우지 않고 새 결정 기록을 먼저 추가한다. ### Task R2.4 — 동일 세션 재진입 fail-closed 복구 **Goal 실행 `P2-R4`:** 최초 보호 route 검증 성공 뒤 같은 session으로 route를 이탈·재진입해도 새 probe 성공 전에는 보호 shell을 다시 열지 않는다. - 연결 리뷰: [Phase 2 리뷰](./reviews/review-phase-2.md) — `REV-P2-006` - 시작 조건: - 같은 `AuthSessionRecord`로 `/ai-characters` 성공 → `/login` 이탈 → `/ai-characters` 재진입 뒤 probe 실패 시 보호 shell이 노출되는 현재 동작을 실패 test로 재현한다. - 완료 증거: - 최초 성공 뒤 동일 session 재진입 pending·404·network·403에서 보호 shell 비노출 - focused App test, server boundary E2E와 P2 Gate 실행 기록 - 범위 밖: - 인증/session 저장 방식 전면 교체 - Phase 3 Character 화면 구현 - [x] 같은 session route 이탈·재진입 뒤 pending·404 실패에서 보호 shell이 노출되는 실패 test를 추가한다. - [x] route 이탈 또는 새 보호 route probe 시작 시 이전 검증 상태가 새 진입을 열지 못하게 최소 상태 전이를 구현한다. - [x] focused App test와 server boundary E2E를 실행한다. - [x] 결과를 plan-task.md 검증 기록과 [Phase 2 리뷰 문서](./reviews/review-phase-2.md)에 누적한다. **P2-R4 수정 검증 기록 (2026-07-27):** - RED: `npm run test:run -- src/app/App.test.tsx` — 동일 session `/login` 이탈 후 `/ai-characters` 재진입 404에서 보호 `main`이 남아 1 test가 기대대로 실패했다. - GREEN focused: 같은 command — 1 file / 20 tests 통과. route visit key로 이전 보호 route 검증을 새 진입에 재사용하지 않게 했다. - Refactor guard: `ProtectedAdminShell`을 `src/app/protected-admin-shell.tsx`로 분리해 `App.tsx`를 153 pure LOC로 낮췄고, App focused test와 lint가 통과했다. ### Task R2.5 — Auth fixture status·media type 계약 복구 **Goal 실행 `P2-R5`:** mock auth fixture가 API Contract의 비ADMIN status와 login media type 경계를 완화하지 않게 한다. - 연결 리뷰: [Phase 2 리뷰](./reviews/review-phase-2.md) — `REV-P2-007` - 시작 조건: - 비ADMIN logout 401과 `text/plain` login 200을 실패 test로 재현한다. - 완료 증거: - 비ADMIN logout 403, invalid·revoked token logout 401 유지 - login은 `application/json`만 허용하고 지원하지 않는 media type은 415 오류 envelope 반환 - 정상 login/logout 회귀 없음 - 범위 밖: - 제공되지 않은 login credential 정책과 회원 role 의미 확장 - [x] 비ADMIN logout 403과 non-JSON login 415 실패 test를 추가한다. - [x] logout token access 분기와 login media type 검증을 계약 status에 맞춘다. - [x] auth handler focused test와 mock auth preview 회귀를 실행한다. - [x] 결과를 plan-task.md 검증 기록과 [Phase 2 리뷰 문서](./reviews/review-phase-2.md)에 누적한다. **P2-R5 수정 검증 기록 (2026-07-27):** - RED: `npm run test:run -- src/shared/mocks/__tests__/auth-handlers.test.ts` — 비ADMIN logout이 401, `text/plain` login이 200으로 응답해 2 tests가 기대대로 실패했다. - GREEN focused: 같은 command — 1 file / 10 tests 통과. 비ADMIN logout은 403, invalid·revoked logout은 401 유지, unsupported media type login은 415로 고정했다. ### Task R2.6 — 모든 mock route state의 지속 안내 복구 **Goal 실행 `P2-R6`:** mock mode의 login·성공 shell·403·보호 오류 상태에서 Mock Preview 안내를 지속 표시한다. - 연결 리뷰: [Phase 2 리뷰](./reviews/review-phase-2.md) — `REV-P2-008` - 시작 조건: - mock mode 403과 404/network 보호 오류 화면에서 banner가 사라지는 현재 동작을 실패 test로 재현한다. - 완료 증거: - mock mode login·성공 shell·403·404/network에서 banner 표시 - server mode에서는 banner 미표시 유지 - 접근성·320px 회귀 없음 - 범위 밖: - 오류 화면 디자인 개편 - [x] 403과 보호 오류 화면의 mock banner 실패 test를 추가한다. - [x] Mock Preview 안내를 모든 mock route state의 공통 상위 경계에 한 번만 배치한다. - [x] focused App/banner test와 mock preview E2E를 실행한다. - [x] 결과를 plan-task.md 검증 기록과 [Phase 2 리뷰 문서](./reviews/review-phase-2.md)에 누적한다. **P2-R6 수정 검증 기록 (2026-07-27):** - RED: `npm run test:run -- src/app/App.test.tsx` — mock mode access denied와 보호 route 오류 화면에서 `Mock Preview` status가 없어 2 tests가 기대대로 실패했다. - GREEN focused: 같은 command — 1 file / 22 tests 통과. login·성공 shell·403·보호 오류가 공통 `RouteFrame`의 banner를 사용한다. ### Task R2.7 — Mode별 bare E2E 실행 경계 복구 **Goal 실행 `P2-R7`:** bare `npm run e2e`와 `npm run e2e:mock`이 각 mode에 유효한 spec만 수집·실행하게 한다. - 연결 리뷰: [Phase 2 리뷰](./reviews/review-phase-2.md) — `REV-P2-009` - 시작 조건: - 두 bare E2E 명령의 `--list`가 같은 spec 집합을 수집하고 mock bare 실행이 실패하는 현재 동작을 실패 test로 재현한다. - 완료 증거: - bare `npm run e2e`와 `npm run e2e:mock`이 각 mode 유효 spec만 실행해 exit 0 - 목록·실행 contract test와 scripts 문서가 일치 - 범위 밖: - Playwright config 복제와 CI 전면 재구성 - [x] package script 또는 Playwright 설정의 mode별 spec 경계 실패 test를 추가한다. - [x] 가장 작은 script/config 변경으로 server 전용·mock 전용 spec 수집을 분리한다. - [x] `--list`, bare E2E 실행과 docs sync test를 실행한다. - [x] 결과를 plan-task.md 검증 기록과 [Phase 2 리뷰 문서](./reviews/review-phase-2.md)에 누적한다. **P2-R7 수정 검증 기록 (2026-07-27):** - RED: `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts` — bare `e2e` script가 mode별 spec 경계를 명시하지 않아 1 test가 기대대로 실패했다. - GREEN focused: `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 2 files / 5 tests 통과. - 목록 검증: `npm run e2e -- --list` — server 4 files / 32 tests, `npm run e2e:mock -- --list` — mock 2 files / 24 tests로 분리됐다. - Surface: `npm run e2e` — 32 tests 통과, `npm run e2e:mock` — 24 tests 통과. **P2-R4~P2-R7 통합 검증 기록 (2026-07-27):** - `npm run test:run -- src/app/App.test.tsx src/shared/mocks/__tests__/auth-handlers.test.ts src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 4 files / 37 tests 통과. - `npm run test:run` — 34 files / 141 tests 통과. - `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0. - `VITE_API_MODE=mock npm run build:prod` — 기대대로 `VITE_API_MODE=mock is only available during development`로 exit 1. - `test ! -e dist/mockServiceWorker.js`, `git diff --check HEAD` — 모두 exit 0. - LSP diagnostics: `src/app` directory 4 files / 0 diagnostics, `src/shared/mocks` directory 8 files / 0 diagnostics. ### Task R2.8 — Phase 2 완료 문서 추적성 복구 **Goal 실행 `P2-R8`:** `P2-R4`~`P2-R7` 이후의 실제 변경 경로·검증 범위와 Phase 2 plan/review 메타데이터를 일치시킨다. - 연결 리뷰: [Phase 2 리뷰](./reviews/review-phase-2.md) — `REV-P2-010` - 시작 조건: - 3차 review 반영 전 `git diff HEAD --name-only`은 37개 경로인데 review 상단과 종료 판정은 36개로 기록된 상태였음을 확인한다. - Phase 2 `주요 Files`가 `src/app/App.test.tsx`, `src/app/admin-pages.tsx`, `src/app/browser-location.ts`, `src/app/protected-admin-shell.tsx`를 포함하지 않는 상태를 확인한다. - 완료 증거: - Phase 2 `주요 Files`가 `P2-R4`~`P2-R7`까지의 실제 코드·test 경로와 일치함 - review 대상·working tree 경로 수·종료 판정과 3차 재검증 기록이 현재 범위와 일치함 - 완료된 회귀 Task의 Files·Interfaces와 검증 근거가 독립 실행자가 추측하지 않을 수준으로 기록됨 - 문서 대체 검증과 `git diff --check` 결과가 plan/review에 누적됨 - 범위 밖: - 애플리케이션 코드·test·설정 변경 - 기존 완료 체크와 과거 검증 기록 삭제 또는 덮어쓰기 **Files:** - Modify: `docs/20260725_AI캐릭터관리자웹/plan-task.md` - Modify: `docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md` - Test: 없음 — 이 Task는 완료 문서의 소유 경로와 검증 메타데이터만 정정한다. **Interfaces:** - Consumes: `git diff HEAD --name-only`, `P2-R4`~`P2-R7` 구현·검증 기록, `REV-P2-010` - Produces: Phase 3 실행자와 reviewer가 사용할 최신 Phase 2 Files·review 범위·검증 기준 **TDD 예외 사유:** 실행 동작을 변경하지 않는 문서 정합성 수정이므로 실패 test를 추가하면 제품 동작과 무관한 문자열 고정 test만 늘어난다. **대체 검증 방법:** 현재 변경 경로와 Phase 2 Files·review 범위를 직접 대조하고, review ID·goal 연결과 Markdown diff를 명령으로 검사한다. - [x] Phase 2 `주요 Files`에 `P2-R4`~`P2-R7`의 실제 코드·test 경로를 추가한다. - [x] review 상단의 대상·working tree 기준과 종료 판정을 3차 재검증 범위에 맞춘다. - [x] 완료된 회귀 Task의 Files·Interfaces·검증 근거 누락을 기존 기록을 보존한 채 보완한다. - [x] 아래 대체 검증을 실행하고 실제 결과를 plan/review에 누적한다. **P2-R8 수정 검증 기록 (2026-07-27):** - 대체 RED: `git diff HEAD --name-only`로 기존 tracked 변경 37개 경로를 확인했고, `rg -n 'P2-R(10|[1-9])|REV-P2-0(0[1-9]|1[0-3])|주요 Files|기준 commit 또는 working tree' docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md`에서 `REV-P2-010`과 `P2-R8`이 문서 추적성 미충족 상태로 남아 있음을 확인했다. - GREEN: Phase 2 `주요 Files`에 `src/app/App.test.tsx`, `src/app/admin-pages.tsx`, `src/app/browser-location.ts`, `src/app/protected-admin-shell.tsx`를 추가하고, review 상단·요약·종료 판정·수정 후 검증 기록을 `P2-R8` 완료와 `P2-R9`~`P2-R10` 잔여 상태로 정렬했다. - 검증: `git diff --check -- docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md` — exit 0. **검증 기준:** - **실행 명령:** `git diff HEAD --name-only`, `rg -n 'P2-R(10|[1-9])|REV-P2-0(0[1-9]|1[0-3])|주요 Files|기준 commit 또는 working tree' docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md`, `git diff --check -- docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md` - **기대 결과:** 실제 변경 경로와 문서의 소유 Files·review 범위 차이 0건, review ID와 goal 연결 누락 0건, whitespace 오류 0건. - **수동 확인:** review 상단·발견 요약·plan 전환·종료 판정·수정 후 검증 기록이 모두 `P2-R1`~`P2-R10`의 현재 상태를 같은 의미로 표시한다. ### Task R2.9 — Mock Preview 모바일 메뉴의 반응형·inert 경계 복구 **Goal 실행 `P2-R9`:** Mock Preview 모바일 메뉴가 열린 상태에서도 banner를 배경 inert 경계에 포함하고 desktop breakpoint 전환 시 숨은 overlay와 inert 상태를 함께 해제한다. - 연결 리뷰: [Phase 2 리뷰](./reviews/review-phase-2.md) — `REV-P2-011`, `REV-P2-012` - 시작 조건: - 320px에서 모바일 메뉴를 열면 Mock Preview banner의 `closest('[inert]')`가 `null`인 현재 동작을 browser 재현으로 확인한다. - 같은 상태에서 viewport를 1,200px로 넓히면 overlay는 `display:none`이지만 `main`의 inert ancestor와 `aria-hidden=true`가 남는 현재 동작을 browser 재현으로 확인한다. - 완료 증거: - 모바일 메뉴가 열린 동안 banner와 shell 본문이 같은 background inert·`aria-hidden` 경계에 포함됨 - viewport가 `lg` 이상으로 바뀌면 mobile menu state와 inert·`aria-hidden`이 해제되고 desktop navigation·logout·main을 keyboard와 pointer로 사용할 수 있음 - breakpoint 자동 종료 시 숨겨진 mobile trigger로 focus를 복귀하지 않음 - App focused test, mock responsive E2E, 기존 accessibility shell E2E와 P2 Gate 실행 기록 - 범위 밖: - Admin shell navigation 구조 전면 교체 - Phase 3 Character UI와 새로운 breakpoint 체계 도입 **Files:** - Modify: `src/app/App.tsx` - Modify: `src/app/protected-admin-shell.tsx` - Test: `src/app/App.test.tsx` - Test: `tests/e2e/mock-preview-shell.spec.ts` **Interfaces:** - Consumes: `MockModeBanner({ apiMode }: { readonly apiMode: ApiMode })`, `ProtectedAdminShell({ routeError }: { readonly routeError: string | null })`, Tailwind `lg=1024px` shell breakpoint - Produces: `ProtectedAdminShell({ routeError, apiMode }: { readonly routeError: string | null; readonly apiMode: ApiMode })`와 mobile overlay 표시 여부·background `inert`·`aria-hidden`이 항상 같은 상태인 composition **TDD 절차:** - [x] **RED: 실패 테스트 작성/실패 확인** — `src/app/App.test.tsx`에 mock banner가 열린 menu의 inert background에 포함되는 test와 `lg` 전환 시 menu state가 닫히는 test를 추가하고, `npm run test:run -- src/app/App.test.tsx src/shared/ui/__tests__/mock-mode-banner.test.tsx`가 두 assertion에서 실패하는지 확인한다. - [x] **GREEN: 최소 구현/통과 확인** — banner와 shell을 하나의 background inert 경계로 조합하고 native viewport change에서 mobile state만 닫는 최소 구현으로 같은 명령을 통과시킨다. - [x] **REFACTOR: 정리/회귀 확인** — focus 복귀 조건과 breakpoint 이름을 정리한 뒤 App focused test, mock preview E2E, 기존 accessibility shell E2E와 P2 focused Gate를 실행한다. - [x] TDD 단계와 아래 검증 기준의 실제 결과를 plan/review에 누적한다. **검증 기준:** - **실행 명령:** `npm run test:run -- src/app/App.test.tsx src/shared/ui/__tests__/mock-mode-banner.test.tsx`, `npm run test:run -- src/shared/config src/shared/mocks src/shared/ui/__tests__/mock-mode-banner.test.tsx`, `npm run e2e:mock -- tests/e2e/mock-preview-shell.spec.ts`, `npm run e2e -- tests/e2e/accessibility-shell.spec.ts`, `npm run typecheck`, `npm run lint` - **기대 결과:** focused unit 2 files / 26 tests 이상, P2 focused 7 files / 25 tests 이상, mock preview 4 projects / 24 tests 이상, accessibility shell 4 projects / 12 tests, type·lint 오류 0건; 1,024px·1,200px 전환 뒤 hidden mobile overlay와 inert background 잔존 0건. - **수동 확인:** 320px에서 메뉴를 열면 banner와 본문이 보조기기 탐색에서 제외되고 menu만 탐색 가능하며, 열린 상태로 1,024px와 1,200px로 넓히면 desktop navigation·logout·main이 즉시 다시 동작한다. ### Task R2.10 — 보호 route 오류의 fail-closed 재시도 복구 **Goal 실행 `P2-R10`:** 보호 route probe의 404·network 오류 화면에서 보호 shell을 열지 않은 채 사용자가 명시적으로 재시도할 수 있게 한다. - 연결 리뷰: [Phase 2 리뷰](./reviews/review-phase-2.md) — `REV-P2-013` - 시작 조건: - `P2-R9` 완료. - 저장된 ADMIN session의 probe가 404 또는 network error로 실패하면 오류 alert만 있고 재시도·이동 control이 없는 현재 동작을 확인한다. - 완료 증거: - 404·network error 화면에 keyboard로 사용할 수 있는 명시적 재시도 control 제공 - 재시도 중과 재실패 상태에서 보호 shell·navigation·logout 비노출 유지 - 재시도 probe가 성공한 뒤에만 현재 session·route visit의 보호 shell 표시 - server가 제공한 정상 오류 envelope의 한국어 message는 보존하고 network·invalid response에는 안전한 공통 안내 사용 - App focused test, server boundary E2E와 P2 Gate 실행 기록 - 범위 밖: - 자동 retry·자동 mock fallback - Phase 3 Character 목록 오류 UI와 API client 전면 교체 **Files:** - Modify: `src/app/App.tsx` - Test: `src/app/App.test.tsx` - Test: `tests/e2e/server-mode-boundary.spec.ts` **Interfaces:** - Consumes: `ProtectedRouteErrorPage({ message }: { readonly message: string })`, `BrowserLocationSnapshot.visitKey`, `ApiError.message` - Produces: `ProtectedRouteErrorPage({ message, onRetry }: { readonly message: string; readonly onRetry: () => void })`와 수동 retry마다 새 probe attempt를 식별하는 App state **TDD 절차:** - [x] **RED: 실패 테스트 작성/실패 확인** — `src/app/App.test.tsx`에 404 → retry 성공과 network → retry 실패의 fail-closed test를 추가하고 `npm run test:run -- src/app/App.test.tsx`가 retry control 부재로 두 test에서 실패하는지 확인한다. - [x] **GREEN: 최소 구현/통과 확인** — 오류 page에 retry button과 현재 session·route에 귀속된 새 probe attempt만 추가해 같은 명령을 통과시킨다. - [x] **REFACTOR: 정리/회귀 확인** — 오류 message·attempt state 이름을 정리한 뒤 App focused test, server boundary E2E, P2 focused Gate와 전체 unit을 실행한다. - [x] TDD 단계와 아래 검증 기준의 실제 결과를 plan/review에 누적한다. **검증 기준:** - **실행 명령:** `npm run test:run -- src/app/App.test.tsx`, `npm run test:run -- src/shared/config src/shared/mocks src/shared/ui/__tests__/mock-mode-banner.test.tsx`, `npm run test:run`, `npm run e2e -- tests/e2e/server-mode-boundary.spec.ts`, `npm run typecheck`, `npm run lint` - **기대 결과:** App 1 file / 26 tests 이상, P2 focused 7 files / 25 tests 이상, 전체 34 files / 145 tests 이상, server boundary 4 projects / 16 tests 이상, type·lint 오류 0건; retry 성공 전 보호 `main`·logout button 0건. - **수동 확인:** 404와 offline 상태에서 retry button의 accessible name·focus indicator를 확인하고, 실패 중 shell이 보이지 않으며 연결 복구 후 한 번의 수동 retry로 shell이 열린다. ### Task R2.11 — Phase 2 working tree 전체 경로 집계 복구 **Goal 실행 `P2-R11`:** tracked diff와 untracked 파일을 함께 집계해 Phase 2 review 범위와 완료 문서가 실제 working tree 전체를 누락 없이 표시하게 한다. - 연결 리뷰: [Phase 2 리뷰](./reviews/review-phase-2.md) — `REV-P2-014` - 시작 조건: - `P2-R8` 완료. - `git diff HEAD --name-only | wc -l`은 37을 반환하지만 `git status --porcelain=v1 | wc -l`은 38을 반환하고 `git ls-files --others --exclude-standard`에 `src/app/protected-admin-shell.tsx`가 존재함을 확인한다. - 완료 증거: - review의 현재 working tree 범위를 tracked 37개와 untracked 1개를 포함한 38개 변경 항목으로 기록 - `P2-R8` 대체 검증과 후속 reviewer 명령이 untracked 파일을 빠뜨리지 않는 `git status --short --untracked-files=all` 기준을 사용 - plan/review의 현재 범위·종료 판정·검증 기록과 실제 working tree 항목 차이 0건 - 범위 밖: - 애플리케이션 코드·test·설정 변경 - 과거 시점의 37개 tracked diff 실행 결과 삭제 또는 덮어쓰기 **Files:** - Modify: `docs/20260725_AI캐릭터관리자웹/plan-task.md` - Modify: `docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md` - Test: 없음 — working tree 집계 명령과 완료 문서 메타데이터만 정정한다. **Interfaces:** - Consumes: `git diff HEAD --name-only`, `git status --short --untracked-files=all`, `git ls-files --others --exclude-standard`, `REV-P2-014` - Produces: tracked·untracked를 모두 포함하는 Phase 2 working tree 범위와 reviewer 검증 기준 **TDD 예외 사유:** 실행 동작을 변경하지 않는 문서·검증 명령 정정이므로 제품 test가 아닌 실제 Git 상태 대조가 실패·성공 증거다. **대체 검증 방법:** tracked diff 수, 전체 status 항목 수와 untracked 목록을 각각 수집해 합계와 문서 범위를 대조한다. - [x] `P2-R8`의 대체 검증 기준에 `git status --short --untracked-files=all`과 untracked 확인 명령을 추가한다. - [x] review 상단·4차 검증·종료 판정의 현재 working tree 범위를 tracked 37개 + untracked 1개 = 38개 항목으로 정렬한다. - [x] 과거 37개 tracked diff 기록은 당시 실행 결과로 보존하고 현재 전체 범위와 구분한다. - [x] 아래 대체 검증과 문서 diff 검사를 실행하고 결과를 plan/review에 누적한다. **검증 기준:** - **실행 명령:** `git diff HEAD --name-only | wc -l`, `git status --short --untracked-files=all | wc -l`, `git ls-files --others --exclude-standard`, `git diff --check -- docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md` - **기대 결과:** 현재 기준 tracked diff 37개, untracked `src/app/protected-admin-shell.tsx` 1개, working tree 전체 38개 변경 항목, 현재 문서 범위 누락과 whitespace 오류 0건. - **수동 확인:** review 상단·4차 검증·발견 요약·plan 전환·종료 판정이 `P2-R1`~`P2-R13`의 현재 상태와 38개 working tree 항목을 같은 의미로 표시한다. ### Task R2.12 — 보호 route probe 대기 상태의 접근 가능한 피드백 복구 **Goal 실행 `P2-R12`:** 최초 보호 route 확인과 수동 retry가 진행되는 동안 보호 shell을 숨긴 채 사용자와 보조기기에 명시적인 loading 상태를 제공한다. - 연결 리뷰: [Phase 2 리뷰](./reviews/review-phase-2.md) — `REV-P2-015` - 시작 조건: - `P2-R10` 완료. - retry 응답을 350ms 지연하면 `#root` child 0개, `main`·`status`·`alert` 0개이고 active element가 `BODY`인 현재 Chromium 동작을 확인한다. - 완료 증거: - 최초 probe와 retry pending 모두 `RouteFrame` 안에 지속적으로 보이는 한국어 loading 상태와 `role="status"` 제공 - pending 동안 보호 `main`·navigation·logout 비노출과 mock mode banner 지속 표시 유지 - 성공·404·network·401·403의 기존 fail-closed 분기와 수동 retry 동작 회귀 없음 - App focused test, 지연된 server boundary E2E, P2 focused Gate와 전체 unit 실행 기록 - 범위 밖: - 보호 shell skeleton 선노출 - 자동 retry·자동 mock fallback과 전역 router 도입 **Files:** - Modify: `src/app/App.tsx` - Test: `src/app/App.test.tsx` - Test: `tests/e2e/server-mode-boundary.spec.ts` **Interfaces:** - Consumes: `PageState({ state: "loading", title, description })`, `RouteFrame({ apiMode, children })`, `ProtectedRouteVerification` - Produces: 현재 session·route visit·retry attempt가 미검증인 동안 렌더되는 `RouteFrame` + accessible loading `PageState` **TDD 절차:** - [x] **RED: 실패 테스트 작성/실패 확인** — `src/app/App.test.tsx`에 최초 probe pending 전용 test를 추가하고 기존 404 retry test에 retry pending assertion을 보강해 두 pending 시나리오가 `role="status"`를 제공하면서 보호 shell을 숨기는지 확인한다. `npm run test:run -- src/app/App.test.tsx`가 status 부재로 실패하는지 확인한다. - [x] **GREEN: 최소 구현/통과 확인** — 기존 `PageState` loading variant를 미검증 branch에 조합하는 최소 구현으로 같은 명령을 통과시킨다. - [x] **REFACTOR: 정리/회귀 확인** — loading copy와 branch 이름을 정리하고 기존 404 retry E2E에 350ms pending status assertion을 추가한 뒤 focused·전체 회귀를 실행한다. - [x] TDD 단계와 아래 검증 기준의 실제 결과를 plan/review에 누적한다. **검증 기준:** - **실행 명령:** `npm run test:run -- src/app/App.test.tsx`, `VITE_API_MODE=server npx playwright test tests/e2e/server-mode-boundary.spec.ts`, `npm run test:run -- src/shared/config src/shared/mocks src/shared/ui/__tests__/mock-mode-banner.test.tsx`, `npm run test:run`, `npm run typecheck`, `npm run lint` - **기대 결과:** App 1 file / 27 tests, server boundary 4 projects / 12 tests 이상, P2 focused 7 files / 25 tests 이상, 전체 34 files / 147 tests 이상, type·lint 오류 0건; 최초 pending 전용 test 1건과 기존 retry test 보강으로 두 pending 시나리오에서 visible `role="status"` 1건과 보호 `main`·logout 0건. - **수동 확인:** 375px server mode에서 최초 진입과 retry 응답을 각각 350ms 이상 지연해 한국어 loading 안내가 보이고 빈 root가 발생하지 않으며, 성공 뒤에만 shell이 열린다. ### Task R2.13 — Mode별 focused E2E 필터와 network retry 증거 복구 **Goal 실행 `P2-R13`:** bare mode 분리를 유지하면서 CLI spec 필터가 실제로 focused 실행되게 하고 network retry 실패를 server boundary E2E로 고정한다. - 연결 리뷰: [Phase 2 리뷰](./reviews/review-phase-2.md) — `REV-P2-016` - 시작 조건: - `P2-R12` 완료. - `npm run e2e -- --list tests/e2e/server-mode-boundary.spec.ts`가 4 files / 32 tests를 수집하지만 직접 `VITE_API_MODE=server npx playwright test tests/e2e/server-mode-boundary.spec.ts --list`는 1 file / 12 tests만 수집함을 확인한다. - `P2-R10` 검증 기준은 server boundary 16 tests 이상을 요구하지만 현재 spec은 3 scenarios / 12 tests이고 network error test는 retry를 실행하지 않음을 확인한다. - 완료 증거: - bare `npm run e2e`는 server 4개 spec만, bare `npm run e2e:mock`은 mock 2개 spec만 실행 - 두 npm script 뒤에 전달한 file filter가 고정 spec 목록과 합쳐지지 않고 해당 file만 수집 - server boundary에 network error → 수동 retry → network 재실패의 retry 유지·shell 비노출·worker 0건 E2E 추가 - mode boundary contract, focused list/실행, bare mode E2E, P2 Gate와 문서 동기화 기록 - 범위 밖: - Playwright config 복제 - project matrix·worker 수·CI 전체 구조 변경 **Files:** - Modify: `package.json` - Modify: `playwright.config.ts` - Modify: `README.md` - Modify: `docs/agent-guide/scripts.md` - Test: `src/shared/mocks/__tests__/mode-boundary.test.ts` - Test: `src/shared/mocks/__tests__/mock-preview-docs.test.ts` - Test: `tests/e2e/server-mode-boundary.spec.ts` **Interfaces:** - Consumes: `VITE_API_MODE=server | mock`, Playwright `testMatch`, npm argument forwarding, 현재 server 4개·mock 2개 spec allowlist - Produces: file 목록을 내장하지 않는 `e2e`·`e2e:mock` scripts와 server `testMatch=["**/server-mode-boundary.spec.ts", "**/smoke.spec.ts", "**/auth.spec.ts", "**/accessibility-shell.spec.ts"]`, mock `testMatch=["**/mock-preview-shell.spec.ts", "**/mock-mode-boundary.spec.ts"]`; CLI file filter와 mode allowlist의 교집합 실행 계약 **TDD 절차:** - [x] **RED: 실패 테스트 작성/실패 확인** — mode boundary test가 npm scripts의 고정 spec 목록 제거와 config의 mode별 exact allowlist를 요구하게 하고 `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts`가 현재 script/config로 실패하는지 확인한다. - [x] **RED: surface 실패 확인** — network 재실패 retry E2E를 추가하고 현재 구현에서 동작을 확인하되, `npm run e2e -- --list tests/e2e/server-mode-boundary.spec.ts`가 불필요한 4 files를 수집해 focused list assertion을 실패시키는지 확인한다. - [x] **GREEN: 최소 구현/통과 확인** — script는 각각 `VITE_API_MODE=server playwright test`, `VITE_API_MODE=mock playwright test`만 유지하고 config의 `apiMode`별 `testMatch`에 Interfaces의 exact glob allowlist를 옮겨 contract·focused list·network retry E2E를 통과시킨다. - [x] **REFACTOR: 정리/회귀 확인** — mode allowlist 상수와 README/scripts 문구를 정리한 뒤 focused·bare E2E와 P2 Gate를 실행한다. - [x] TDD 단계와 아래 검증 기준의 실제 결과를 plan/review에 누적한다. **검증 기준:** - **실행 명령:** `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts`, `npm run e2e -- --list tests/e2e/server-mode-boundary.spec.ts`, `npm run e2e:mock -- --list tests/e2e/mock-preview-shell.spec.ts`, `npm run e2e -- tests/e2e/server-mode-boundary.spec.ts`, `npm run e2e`, `npm run e2e:mock`, `npm run test:run -- src/shared/config src/shared/mocks src/shared/ui/__tests__/mock-mode-banner.test.tsx`, `npm run typecheck`, `npm run lint` - **기대 결과:** focused server boundary 1 file / 16 tests 이상, focused mock preview 1 file / 24 tests, bare server 4 files / 36 tests 이상, bare mock 2 files / 28 tests, P2 focused 7 files / 25 tests 이상, type·lint 오류 0건; network retry 재실패 뒤 retry button 1건, 보호 `main`·logout·mock worker 0건. - **수동 확인:** 없음 — mode/file 수집 목록, network retry와 worker 경계는 Playwright list·실행 결과로 결정적으로 검증한다. ### Task R2.14 — P2-R12 test 증거 정합성 복구 **Goal 실행 `P2-R14`:** P2-R12의 실제 test case 구조·실행 수를 plan/review의 완료 증거와 사실대로 정렬한다. - 연결 리뷰: [Phase 2 리뷰](./reviews/review-phase-2.md) — `REV-P2-017` - 시작 조건: - `P2-R11`~`P2-R13` 구현·검증 완료. - `rg -c '^test\(' src/app/App.test.tsx`와 focused 실행이 모두 27 tests를 반환한다. - 최초 pending은 전용 test 1건, retry pending은 기존 404 retry test의 assertion 보강이지만 `P2-R12` 체크와 수정 기록은 두 신규 test·28 tests 이상으로 표시한다. - 완료 증거: - P2-R12의 현재 완료 증거가 최초 pending 전용 test 1건 추가 + 기존 retry test 보강 + App 27 tests로 일치 - 이전 “두 신규 test·28 tests 이상” 표현과 정정 사유를 새 Progress에 보존하고 실제 실행 결과를 덮어쓰지 않음 - plan/review 검색·diff 검사와 App focused test 결과를 수정 검증 기록에 누적 - 범위 밖: - 애플리케이션 코드·test·설정 변경 - 같은 retry pending 동작을 중복 검증하는 test case 추가 - 기존 실행 이력 삭제 또는 제품 동작 완료 주장 변경 **Files:** - Modify: `docs/20260725_AI캐릭터관리자웹/plan-task.md` - Modify: `docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md` - Test: 없음 — 구현·test 동작은 바꾸지 않고 완료 문서의 사실관계와 현재 상태만 정정한다. **Interfaces:** - Consumes: `REV-P2-017`, `src/app/App.test.tsx`의 27 test declarations, `P2-R12` 완료 기록 - Produces: actual test case와 검증 시나리오를 구분한 P2-R12 완료 증거와 정정 기록 **TDD 예외 사유:** 제품 코드와 test를 변경하지 않는 문서 정정이며, 새 test case를 추가하면 이미 존재하는 retry pending assertion을 중복하게 된다. 실제 test 선언·focused 결과와 문서 문자열 대조를 실패·성공 증거로 사용한다. **대체 검증 방법:** App test 선언 수·두 pending 시나리오의 위치·focused 실행 수를 plan/review 문구와 대조하고, stale 표현 검색과 문서 diff 검사를 실행한다. - [x] `rg -c '^test\(' src/app/App.test.tsx`와 두 pending test 위치를 수집해 27 tests 구조를 재확인한다. - [x] P2-R12 체크·기대 결과·수정 검증 기록을 최초 전용 test 1건 + 기존 retry test 보강 + App 27 tests로 정정하고 이전 표현·정정 사유를 새 기록에 남긴다. - [x] review 상단·발견 요약·종료 판정을 `REV-P2-017` 수정 완료와 `P2-R14` 완료 상태로 갱신한다. - [x] 아래 대체 검증과 문서 diff 검사를 실행하고 실제 결과를 plan/review에 누적한다. **P2-R14 수정 검증 기록 (2026-07-27):** - 대체 RED: `rg -c '^test\(' src/app/App.test.tsx` — 27건. `rg -n 'initial protected route probe is pending|retries a protected route 404' src/app/App.test.tsx` — 최초 pending 전용 test 1건과 기존 404 retry test 1건을 확인했다. 기존 P2-R12 기록은 두 신규 test·App 28 tests 이상으로 표시돼 실제 구조와 불일치했다. - GREEN: P2-R12 TDD 절차·기대 결과와 P2-R11~P2-R13 수정 검증 기록을 최초 pending 전용 test 1건 + 기존 retry test 보강 + App 27 tests로 정정했다. 이전 잘못된 표현과 정정 사유는 이 P2-R14 기록과 review `REV-P2-017`에 보존했다. - Focused: `npm run test:run -- src/app/App.test.tsx` — 1 file / 27 tests 통과. - 검증: `rg -n '^[[:space:]]+- P2-R12 RED:.*신규 pending status tests 2건' docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md` — no matches. `git diff --check -- docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md` — exit 0. **검증 기준:** - **실행 명령:** `rg -c '^test\(' src/app/App.test.tsx`, `rg -n 'initial protected route probe is pending|retries a protected route 404' src/app/App.test.tsx`, `npm run test:run -- src/app/App.test.tsx`, `rg -n '^[[:space:]]+- P2-R12 RED:.*신규 pending status tests 2건' docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md`, `git diff --check -- docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md` - **기대 결과:** App test 선언·focused 실행 27건, 최초 pending 전용 test와 기존 retry test 각 1건 확인, 정정 대상인 잘못된 RED 기록 검색 결과 0건, 문서 whitespace 오류 0건. - **수동 확인:** plan/review가 test case 수와 검증 시나리오 수를 구분하고 P2-R12의 실제 완료 증거를 같은 의미로 표시한다. ### Task R2.15 — review plan 전환 절의 현재 수정 상태 정합성 복구 **Goal 실행 `P2-R15`:** Phase 2 review의 plan·goal 전환 절이 `P2-R8`~`P2-R13`의 현재 수정 완료 상태를 요약·상세·종료 판정과 일치하게 표시하도록 정정한다. - 연결 리뷰: [Phase 2 리뷰](./reviews/review-phase-2.md) — `REV-P2-018` - 시작 조건: - review 요약·상세·종료 판정은 `REV-P2-010`~`REV-P2-016`과 `P2-R8`~`P2-R13`을 수정 완료로 표시한다. - 같은 review의 `7. 확정 항목의 plan·goal 전환`은 3차·4차 회귀 Task를 여전히 “아직 수정하지 않았다”고 표시하는 모순을 확인한다. - 완료 증거: - review §7의 `P2-R8`~`P2-R13` 전환 설명이 각 Task의 실제 수정 완료 상태와 일치함 - 과거 시점의 발견·수정 전 검증 기록은 삭제하거나 현재 상태처럼 다시 쓰지 않고 그대로 보존함 - stale 현재 상태 표현 검색 결과 0건과 문서 whitespace 오류 0건 - 범위 밖: - 애플리케이션 코드·test·설정 변경 - 과거 검증 기록과 당시의 남은 항목 삭제 또는 덮어쓰기 - `P2-R8`~`P2-R13` 구현·검증의 재수행 **Files:** - Modify: `docs/20260725_AI캐릭터관리자웹/plan-task.md` - Modify: `docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md` - Test: 없음 — 완료된 review의 현재 상태 문구만 정정하는 문서 Task다. **Interfaces:** - Consumes: `REV-P2-010`~`REV-P2-016`, `P2-R8`~`P2-R13` 수정 검증 기록, review §5·§6·§8 - Produces: review §7과 요약·상세·종료 판정이 같은 현재 상태를 표시하는 Phase 2 인수인계 문서 **TDD 예외 사유:** 제품 동작이나 실행 가능한 계약을 바꾸지 않는 문서 상태 정정이므로 새 제품 test를 추가하지 않는다. **대체 검증 방법:** review §5·§6·§7·§8의 상태를 직접 대조하고 stale 현재 상태 문자열 검색과 Markdown diff 검사를 실행한다. - [x] review §7의 3차 회귀 Task 설명을 `P2-R8`~`P2-R10` 수정 완료 상태로 정정한다. - [x] review §7의 4차 회귀 Task 설명을 `P2-R11`~`P2-R13` 수정 완료 상태로 정정한다. - [x] 과거 시점의 수정 전 기록과 남은 항목이 보존됐는지 확인한다. - [x] 아래 대체 검증을 실행하고 실제 결과를 plan/review에 누적한다. **P2-R15 수정 검증 기록 (2026-07-27):** - 대체 RED: `rg -n '3차 재검증의 새 확정 4건.*아직 수정하지 않았다|4차 재검증의 새 확정 3건.*아직 수정하지 않았다' docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md` — 2건. review §7이 완료된 `P2-R8`~`P2-R13`을 여전히 미수정 현재 상태로 표시했다. - GREEN: review §7의 3차·4차 회귀 Task 설명을 2026-07-27 수정·검증 완료 상태로 정정하고, review의 `REV-P2-018` 상태·발견 요약·종료 판정을 수정 완료로 맞췄다. §9의 당시 남은 항목과 과거 검증 기록은 보존했다. - 검증: 같은 stale 현재 상태 검색 — no matches. `git diff --check -- docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md` — exit 0. **검증 기준:** - **실행 명령:** `rg -n '3차 재검증의 새 확정 4건.*아직 수정하지 않았다|4차 재검증의 새 확정 3건.*아직 수정하지 않았다' docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md`, `git diff --check -- docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md` - **기대 결과:** stale 현재 상태 검색 결과 0건, 문서 whitespace 오류 0건, review §5·§6·§7·§8의 `P2-R8`~`P2-R13` 상태 의미 차이 0건. - **수동 확인:** review §9의 당시 남은 항목 기록은 이력으로 보존되고 §7의 현재 전환 상태만 수정 완료로 정정됐는지 확인한다. ### Task R2.16 — Phase 2 현재 상태·하단 Progress 정합성 복구 **Goal 실행 `P2-R16`:** Phase 2 상단 현재 상태와 하단 최신 Progress가 `P2-R15`까지의 실제 수정 완료 및 Phase 3 진행 가능 상태를 일치하게 표시하도록 정정한다. - 연결 리뷰: [Phase 2 리뷰](./reviews/review-phase-2.md) — `REV-P2-019` - 시작 조건: - `P2-R15` Task 본문과 review는 수정 완료 및 열린 확정 항목 없음을 표시한다. - Phase 2 상단 현재 상태는 최초 추가 당시 설명만 유지하고, 하단 최신 Progress는 `P2-R15` 수정 필요 상태로 끝나는 모순을 확인한다. - 완료 증거: - Phase 2 상단 현재 상태가 `P2-T1`~`P2-T3`, `P2-GATE`, `P2-R1`~`P2-R16` 완료와 Phase 3 진행 가능 상태를 명시함 - 6차 재검증의 당시 남은 항목은 이력으로 보존하고 하단에 `P2-R16` 수정 검증을 누적함 - 최신 Progress, review 요약·종료 판정과 Phase 2 상단 상태의 의미 차이 0건 - 범위 밖: - 애플리케이션 코드·test·설정 변경 - 과거 검증 기록과 당시 남은 항목 삭제 또는 덮어쓰기 - Phase 3 기능 구현 **Files:** - Modify: `docs/20260725_AI캐릭터관리자웹/plan-task.md` - Modify: `docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md` - Test: 없음 — 완료된 Phase의 현재 상태와 누적 Progress만 정정하는 문서 Task다. **Interfaces:** - Consumes: `REV-P2-019`, `P2-R15` 수정 검증, Phase 2 상단 현재 상태와 하단 Progress - Produces: Phase 3 시작 조건 판정에 사용할 일관된 Phase 2 완료 상태 **TDD 예외 사유:** 제품 동작이나 실행 가능한 계약을 바꾸지 않는 문서 상태 정정이므로 새 제품 test를 추가하지 않는다. **대체 검증 방법:** 완료 상태 exact 문자열과 최신 Progress를 검색하고, 과거 6차 기록 보존 및 Markdown diff를 확인한다. - [x] Phase 2 상단 현재 상태를 실제 완료 범위와 Phase 3 진행 가능 상태로 정정한다. - [x] 6차 재검증의 당시 남은 항목을 보존하고 하단에 `P2-R16` 수정 검증을 누적한다. - [x] review의 `REV-P2-019` 상태·plan 전환·종료 판정을 수정 완료로 갱신한다. - [x] 아래 대체 검증과 Phase 2 전체 Gate를 실행하고 실제 결과를 plan/review에 누적한다. **P2-R16 수정 검증 기록 (2026-07-27):** - 대체 RED: Phase 2 완료 상태 exact 검색과 `tail` 기반 최신 Progress 검색은 모두 no match, exit 1이었다. 상단은 최초 추가 설명에 머물고 문서 끝은 `P2-R15` 수정 필요 상태였다. - GREEN: Phase 2 상단에 `P2-R16`까지 완료 및 Phase 3 진행 가능 상태를 명시하고, 6차 당시 남은 항목은 보존한 채 하단에 `P2-R16` 수정 검증을 누적했다. review의 `REV-P2-019`와 종료 판정도 수정 완료로 정렬했다. - Phase 2 Gate: `npm run test:run` — 34 files / 147 tests 통과. `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0, build는 각각 160 modules 변환. `VITE_API_MODE=mock npm run build:prod` — 기대한 guard로 exit 1. - E2E: `npm run e2e` — 4 projects / 36 tests 통과. `npm run e2e:mock` — 4 projects / 28 tests 통과. - Production: `dist/mockServiceWorker.js` 없음, production JS의 `startMockWorker`·`mockServiceWorker.js` 검색은 기대한 no-match exit 1. - 문서 검증: Phase 2 완료 상태와 최신 Progress exact 검색, 과거 6차 `P2-R15` 남은 항목 보존 검색, plan/review `git diff --check`를 통과했다. **검증 기준:** - **실행 명령:** Phase 2 완료 상태 exact 검색, `tail` 기반 최신 Progress 확인, 과거 `P2-R15` 남은 항목 보존 검색, `git diff --check`, Phase 2 전체 Gate - **기대 결과:** 현재 상태·최신 Progress 의미 차이 0건, 과거 기록 보존, 문서 whitespace 오류 0건, Phase 2 자동 Gate 0 failure/0 error - **수동 확인:** Phase 3 실행자가 과거 6차 기록을 열린 현재 Task로 오해하지 않고 `P2-GATE`와 모든 회귀 수정 완료를 확인할 수 있다. --- > **계약 이력 안내 (2026-07-29):** Phase 3~9 본문과 §7의 실행 기록은 > OpenAPI 2.0.0 기준으로 수행된 당시 상태를 보존한다. 원작·장르 lookup, > UTC 전송, Community pagination, FanTalk 수정·삭제, Comments와 active-only > 계약의 최신 실행 기준은 OpenAPI 2.3.0과 **Phase 10**이다. 과거 Phase의 > 완료 체크를 다시 열거나 과거 RED/GREEN 증거를 현재 계약 증거로 > 재해석하지 않는다. ## Phase 3. Character workspace vertical slice **목표:** ADMIN이 Character를 검색·생성·선택하고 workspace에서 상세·수정·soft delete 요청과 후속 목록 재조회를 완료한다. **Phase Goal `P3`:** Task 3.1 → 3.4와 Phase 3 Gate로 Character workspace vertical slice를 완성한다. - **시작 조건:** `P2-GATE` 완료. - **완료 조건:** `P3-T1`~`P3-T4`, `P3-GATE` 완료. 외부 의존은 구현 또는 명시적 제외 결정으로 종결. - **실행 순서:** 계약 확인 → 목록/workspace → mutation → 반응형·접근성. **요구사항:** `CHAR-001~018`, `FILE-001~002`, `FILE-008~010`, `FILE-012`, PRD `7`, `9`의 Character 범위. **외부 의존:** `EXT-007`/`CHAR-012`의 `searchTerm` 지정 결과에 대한 active-only 보장. `EXT-001`/`CHAR-013` original work lookup은 legacy 후보 `GET /admin/chat/original/search?searchTerm={검색어}`로 구현 계획에 포함한다. `searchTerm` 생략 시 활성 목록은 OpenAPI에 명시돼 있다. `originalWorkId` 미선택은 key 생략과 `null`이 모두 가능하며 frontend canonical serializer만 하나로 고정한다. **주요 Files:** - Create: `src/features/characters/api/character-api.ts` - Create: `src/features/characters/model/types.ts` - Create: `src/features/characters/schemas/character-schema.ts` - Create: `src/features/characters/validation/character-image-policy.ts` - Create: `src/features/characters/pages/{CharacterListPage,CharacterDetailPage,CharacterFormPage}.tsx` - Create: `src/features/characters/components/{CharacterList,CharacterListItem,CharacterProfile,CharacterForm,CharacterImageField}.tsx` - Create: `src/features/characters/components/OriginalWorkCombobox.tsx` - Create: `src/features/characters/tests/character-api.test.ts` - Create: `src/features/characters/tests/{character-list,character-form}.test.tsx` - Create: `src/layouts/CharacterWorkspaceLayout.tsx` - Create: `src/layouts/CharacterWorkspaceLayout.test.tsx` - Create: `tests/e2e/character-workspace.spec.ts` - Modify: `src/app/router.tsx`, `src/app/route-paths.ts` #### Phase 3 Task 실행 계약 | Goal | Files | Interfaces | TDD·검증 기준 | |---|---|---|---| | `P3-T1` | Modify: `docs/20260725_AI캐릭터관리자웹/prd.md`, `docs/20260725_AI캐릭터관리자웹/plan-task.md`; Read: `docs/20260725_AI캐릭터관리자웹/api-contract.openapi.json`; Test: 없음 | Consumes: OpenAPI `Character*` schema·4 operation. Produces: `CharacterListResponse`, `CharacterDetailResponse`, create/update multipart와 screen inventory | **TDD 예외:** 외부 계약 조사 Task다. `node -e "JSON.parse(require('fs').readFileSync('docs/20260725_AI캐릭터관리자웹/api-contract.openapi.json','utf8'))"`와 `rg -n 'CHAR-0(1[2-8]|0[1-9])' docs/20260725_AI캐릭터관리자웹/prd.md docs/20260725_AI캐릭터관리자웹/plan-task.md`가 exit 0인지 확인한다. 수동 확인: 제공·미제공 범위가 PRD와 일치한다. | | `P3-T2` | Create: `src/features/characters/api/character-api.ts`, `src/features/characters/model/types.ts`, `src/features/characters/pages/{CharacterListPage,CharacterDetailPage}.tsx`, `src/features/characters/components/{CharacterList,CharacterListItem,CharacterProfile}.tsx`, `src/features/characters/tests/character-list.test.tsx`, `src/layouts/CharacterWorkspaceLayout.tsx`, `src/layouts/CharacterWorkspaceLayout.test.tsx`; Modify: `src/app/router.tsx`, `src/app/route-paths.ts` | Consumes: `CharacterListResponse`, `CharacterDetailResponse`. Produces: `getCharacters({searchTerm,page,size})`, `getCharacter(characterId)`, list/workspace route | **TDD 적용:** `npm run test:run -- src/features/characters/tests/character-list.test.tsx src/layouts/CharacterWorkspaceLayout.test.tsx`; 기대 `exit 0`. 수동 확인: searchTerm request와 deep link/read-only 상태. | | `P3-T3` | Create: `src/features/characters/schemas/character-schema.ts`, `src/features/characters/validation/character-image-policy.ts`, `src/features/characters/pages/CharacterFormPage.tsx`, `src/features/characters/components/{CharacterForm,CharacterImageField,OriginalWorkCombobox}.tsx`, `src/features/characters/tests/{character-api.test.ts,character-form.test.tsx}`; Modify: `src/features/characters/api/character-api.ts` | Consumes: Character create/update multipart, legacy original work lookup `GET /admin/chat/original/search?searchTerm={검색어}`. Produces: `createCharacter`, `updateCharacter`, `deactivateCharacter`, `searchOriginalWorks`, form serializer | **TDD 적용:** `npm run test:run -- src/features/characters/tests/character-api.test.ts src/features/characters/tests/character-form.test.tsx`; 기대 `exit 0`. 수동 확인: 필수 image/systemPrompt, 원작 검색 선택, crop, 저장·목록 이동. | | `P3-T4` | Modify: `src/features/characters/pages/{CharacterListPage,CharacterDetailPage,CharacterFormPage}.tsx`, `src/features/characters/components/{CharacterList,CharacterListItem,CharacterProfile,CharacterForm,CharacterImageField}.tsx`; Test: `tests/e2e/character-workspace.spec.ts` | Consumes: P3-T2/T3 UI. Produces: viewport·keyboard capability evidence | **TDD 적용:** `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts`; 기대 지원 project 전부 통과. 수동 확인: 320/768/1280px, 200% zoom, keyboard, axe. | `P3-T2`~`P3-T4`는 각 row의 test에 가장 작은 실패 assertion을 먼저 추가해 RED를 확인하고, 최소 구현으로 같은 명령을 통과시킨 뒤 관련 feature test·typecheck·lint를 실행한다. 각 Task 마지막에는 RED, GREEN, REFACTOR와 수동 확인의 실제 결과를 `§7 검증 기록`에 누적한다. ### Task 3.1 Phase 계약 확인 **Goal 실행 `P3-T1`:** Character 구현 계약, mock scenario와 화면 component map을 확정한다. - **시작 조건:** `P2-GATE` 완료, PRD `CHAR-001~018`, `MOCK-001~009`와 OpenAPI Character 4개 operation/schema 확인. - **완료 증거:** 체크박스 전체, 제공 계약 또는 제외 결정의 세 문서 일치, 상태/action inventory. - **범위 밖:** 계약을 추정한 production adapter와 Character 화면 구현. - [x] OpenAPI에서 목록 `searchTerm/page/size`, `data.totalCount/content`, item `id`, 상세 `characterUUID/originalWork`, mutation `data=null`을 contract fixture로 고정한다. - [x] original work lookup endpoint, DTO, search/page를 확인하고, 미제공이면 network control을 제외한다. `originalWorkId` 미선택 serializer는 허용된 omit 또는 `null` 중 하나를 contract test로 고정한다. - [x] OpenAPI 공통 400/401/403/404/405/406/415/500과 `ApiErrorResponse`를 fixture에 기록한다. Character 전용 message key는 미제공으로 표시하고 분기하지 않는다. - [x] 목록·상세·form·workspace의 상태/action inventory를 작성하고 Page는 route/query/policy 조합, feature component는 Character 표시·입력 규칙, shared component는 Phase 1 contract를 사용하도록 component map을 확정한다. - [x] 제공 계약 범위의 Character seed, loading·empty·error·success, legacy original work lookup과 CRUD/deactivate browser handler 시나리오를 확정한다. **P3-T1 확정 기록:** - OpenAPI 제공 범위: `GET/POST /api/v2/admin/ai-characters`, `GET/PUT /api/v2/admin/ai-characters/{characterId}`만 Character operation으로 사용한다. 목록은 query `searchTerm`, `page`, `size`와 응답 `data.totalCount`, `data.content[]`, item `id`를 fixture 기준으로 삼는다. 상세는 `characterUUID`, `originalWork`를 포함한다. 생성·수정 성공은 `NullSuccessResponse`의 `data=null`만 신뢰하고 새 ID나 상세 DTO를 추정하지 않는다. multipart part는 create `image`, `request`, update optional `image`, required `request`만 사용한다. - Original work lookup: OpenAPI에는 없지만 legacy 후보 `GET /admin/chat/original/search?searchTerm={검색어}`를 사용한다. P3-T3에서 response DTO를 contract test로 고정하고 `OriginalWorkCombobox` network control을 만든다. `originalWorkId` 미선택 serializer는 P3-T3 contract test에서 key 생략을 canonical로 고정한다. - 오류 fixture: Character 4개 operation의 공통 오류는 OpenAPI `ApiErrorResponse` shape, 즉 `success=false`, `message`, `data=null`, `errorProperty`만 사용한다. 400/401/403/404/405/406/415/500 status fixture를 공통 오류로 둔다. OpenAPI 밖의 Character 전용 message key는 분기하지 않고 미정의 오류는 기본 문구로 처리한다. - 상태·action inventory: list는 `searchTerm`, `page`, `size`, `items`, `totalCount`, `selectedCharacterId`, `loading`, `empty`, `error`, `retry`; detail은 `characterId`, `profile`, `isActive`, `readOnly`, `loading`, `error`, `refresh`; form은 `mode`, `fields`, `image`, `dirty`, `submitting`, `fieldErrors`, `submit`, `cancel`, `deactivate`; workspace는 route `characterId`, active tab, breadcrumb, read-only policy, reload action을 가진다. - component map: Page는 route param, URL query, data loading, navigation, read-only policy를 조합한다. feature component는 Character 목록 표시, 상세 표시, form 입력·직렬화, image 선택 규칙만 맡는다. shared component는 Phase 1의 `PageState`, `AppLayout`, `Button`, form primitive, dialog contract를 재사용한다. - mock scenario: deterministic seed는 활성 Character 2건과 비활성 상세 1건, originalWork가 있는 상세 1건과 `null` 상세 1건을 둔다. handler는 list/detail loading, empty, common error, success, legacy original work lookup, create/update/deactivate 성공 및 공통 오류를 제공한다. create/update/deactivate 후 in-memory store의 list/detail을 OpenAPI 응답 shape로 갱신한다. ### Task 3.2 목록·선택·workspace **Goal 실행 `P3-T2`:** Character 목록·검색·선택과 URL 기반 workspace 복원을 완성한다. - **시작 조건:** `P3-T1` 완료. - **완료 증거:** 체크박스 전체, route/list/workspace test와 read-only/error 상태 검증 기록. - **범위 밖:** Character 생성·수정·비활성화 form. - [x] Character 목록·생성 path에는 `characterId`가 없고 하위 resource route에만 선택한 `characterId`가 들어가는 contract test를 작성한다. - [x] 목록 UI의 `search` 상태를 API `searchTerm`으로 직렬화하고 `page`, `size` URL query 보존, `data.content` 역직렬화와 loading·empty·error·retry test를 작성한다. - [x] 목록 request에 `isActive`·`activeStatus`가 없고 client-side 활성 filter도 없는 contract test를 작성한다. active-only 보장은 외부 의존으로 남긴다. - [x] Character 선택 시 URL의 `characterId`로 workspace에 진입하고 새로고침·deep link가 동작하는 test를 작성한다. - [x] workspace header에 image, name, active 상태, `characterId`와 탭·breadcrumb를 표시한다. - [x] 상세 성공 응답이 `isActive=false`이면 read-only 배너와 중앙 write policy로 모든 mutation 진입점을 차단한다. - [x] 상세 400/404/500은 공통 오류 화면을 사용하고 비활성 ID 응답 정책을 client가 추정하지 않는다. - [x] browser MSW handler와 deterministic seed로 목록 → 선택 → workspace deep link의 최종 UI를 `dev:mock`에서 확인한다. ### Task 3.3 생성·수정·soft delete **Goal 실행 `P3-T3`:** Character multipart 생성·수정·soft delete와 image/original-work 규칙을 완성한다. - **시작 조건:** `P3-T1`, `P3-T2` 완료. - **완료 증거:** 체크박스 전체, serializer/form/image/deactivate test, `OQ-009` 결정과 검증 기록. - **범위 밖:** 하위 도메인 mutation. v2 original-work lookup이 별도 제공되지 않으면 legacy `GET /admin/chat/original/search?searchTerm={검색어}`를 사용한다. - [x] create multipart가 필수 `image`와 필수 `request` JSON part를 보내고 request에 `name`, `systemPrompt`, `description`을 포함하며 `isActive`, `externalCharacterId`를 포함하지 않는 test를 작성한다. - [x] 일반 update는 `isActive`를 생략하고 soft delete만 `isActive=false`를 보내며 `true`를 보내지 않는 test를 작성한다. - [x] create-only `region`을 수정 화면에서 읽기 전용으로 표시하고 update payload에 보내지 않는 test를 작성한다. - [x] name·systemPrompt·description visible label, field error, 중복 제출 방지, dirty-form 이탈 확인을 test한다. - [x] OpenAPI optional scalar와 tags·hobbies·values·goals·relationships·personalities·backgrounds·memories 반복 입력을 create/update schema에 맞게 직렬화하는 test를 작성한다. - [x] Character image의 JPEG/PNG·10MB, `1:1`, 최대 800×800, no-upscale, crop 이동·zoom·reset·preview·취소·적용·keyboard 대안을 test한다. - [x] crop 취소·교체 파일 미선택이 기존 image를 유지하고 기존 image 제거 UI는 없음을 test한다. - [x] Phase 10 `P10-T1`에서 v2 `searchAiCharacterOriginalWorks` 기반 원작 검색 Combobox와 `originalWorkId` 선택 payload를 contract test로 고정한다. 미선택 payload는 canonical 생략으로 고정하고 legacy lookup은 제거한다. - [x] 상세의 `characterUUID`는 읽기 전용으로 표시할 수 있지만 `externalCharacterId`로 이름을 바꾸지 않는다. 계약에 없는 creator member ID·nickname DTO/UI는 만들지 않고 creator 생성·동기화도 client가 수행하지 않는다. - [x] create의 `data=null` 성공 후 목록 route로 이동해 목록을 재조회하고, update는 기존 `characterId` 상세 route로 재진입해 상세를 재조회한다. 현재 Character 화면은 React Query cache가 아니라 route 재진입 직접 요청으로 동기화한다. - [x] 비활성화 AlertDialog가 영향·복원 미지원·hard delete 미지원을 설명하는 test를 작성한다. - [x] soft delete 성공 후 목록 route 이동과 목록 재조회, 성공 toast를 확인하고 상세에 머물지 않는다. 비활성 항목이 서버 결과에서 제외되는지는 active-only 계약 제공 후 server mode에서 검증한다. - [x] 초기 Character form을 실제 페이지에서 확인한 뒤 `name`, `systemPrompt`, `description`과 tags·hobbies·values·goals·relationships·personalities·backgrounds·memories의 최대 길이·개수 권고안을 작성한다. backend 호환 확인 전에는 상한을 구현하지 않는다. - [x] mock store가 create/update/deactivate 후 목록·상세를 같은 server response contract로 갱신하는 E2E를 작성한다. ### Task 3.4 Character 반응형·접근성 **Goal 실행 `P3-T4`:** Character viewport capability와 keyboard·zoom·axe 기준을 검증한다. - **시작 조건:** `P3-T2`, `P3-T3`의 활성 범위 완료. - **완료 증거:** 체크박스 전체, 320/768/1280px·200% zoom·keyboard·axe E2E 기록. - **범위 밖:** 다음 Phase 도메인과 모바일 mutation. - [x] desktop/tablet에서는 전체 관리 action을 제공한다. - [x] mobile에서는 목록·검색·상세만 제공하고 create/edit/deactivate route 직접 진입도 desktop 안내로 종료한다. - [x] Table이 mobile Card로 바뀌어도 동일한 accessible name과 핵심 상태를 유지한다. - [x] keyboard-only로 search → select → tabs → form → dialog를 완료한다. - [x] 320/768/1280px, 200% zoom, axe critical·serious 0건을 Phase E2E에서 확인한다. ### Phase 3 Gate **Goal 실행 `P3-GATE`:** Character mock UI journey와 실제 server integration 상태를 분리해 판정한다. - **시작 조건:** `P3-T1`~`P3-T4` 완료. - **완료 증거:** 아래 명령과 Expected 통과, 외부 의존의 구현/제외 상태 기록. - **범위 밖:** 실패와 무관한 Audio 기능. ```bash npm run test:run -- src/features/characters src/layouts/CharacterWorkspaceLayout.test.tsx npm run e2e:mock -- tests/e2e/character-workspace.spec.ts npm run e2e npm run typecheck npm run lint npm run build ``` **Expected:** mock mode에서 login → Character 검색/생성 → 선택/workspace → 수정 → soft delete 요청 → 목록 재조회 UI가 OpenAPI request/response shape로 통과한다. server mode 결과는 별도로 기록하며 active-only·original work 계약이 없으면 fixture로 보장을 추정하지 않고 연동 대기로 남긴다. **수동 확인:** 320/768/1280px와 200% zoom에서 검색·workspace·필수 image 생성·수정·비활성화 Dialog를 keyboard-only로 확인하고, `searchTerm`, multipart part와 mutation 후 network 요청이 OpenAPI와 일치하는지 본다. ### Task R3.1 — Character optional form 계약 완성 **Goal 실행 `P3-R1`:** Character create/update form에서 확정된 optional scalar와 반복 배열을 편집·직렬화한다. - **연결 리뷰:** [Phase 3 재리뷰](./reviews/review-phase-3-20260729.md) — `REV-P3-001` - **시작 조건:** OpenAPI `CharacterCreateRequest/CharacterUpdateRequest`와 `CHAR-003`, `CHAR-018`. - **Files:** Modify: `src/features/characters/pages/CharacterCreatePage.tsx`, `src/features/characters/pages/CharacterEditPage.tsx`, `src/features/characters/api/character-api.ts`, `src/features/characters/tests/CharacterCreatePage.test.tsx`, `src/features/characters/tests/CharacterEditPage.test.tsx`, `src/features/characters/tests/character-api.test.ts`, `tests/e2e/character-workspace.spec.ts`. - **Interfaces:** scalar `age/gender/mbti/speechPattern/speechStyle/appearance/region/originalTitle/originalLink/characterType`; arrays `tags/hobbies/values/goals/relationships/personalities/backgrounds/memories`; update `region`은 읽기 전용. - **범위 밖:** 계약에 없는 최대 길이·배열 개수, creator field, 새로운 form framework 도입. - [x] **RED:** create/update에서 각 scalar·array 대표값과 null/empty canonical payload를 검증하는 form/API test를 실패시킨다. - [x] **GREEN:** 기존 두 Page 안에서 section별 최소 field/row editor를 추가하고 현재 multipart serializer로 전송한다. - [x] **REFACTOR:** 반복 row helper는 실제 8개 소비만 공유하고 dirty/clear semantics를 정리한다. - **실행 명령:** `npm run test:run -- src/features/characters`; `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts`. - **기대 결과:** exit 0, OpenAPI property 누락 0건, update region request 0건, 생성 필수 field 회귀 0건. - **수동 확인:** desktop/tablet create/edit에서 모든 section 입력·추가·삭제와 저장 후 재조회 값을 확인한다. **Progress — 2026-07-30 P3-R1 완료** - **RED:** `npm run test:run -- src/features/characters/tests/CharacterCreatePage.test.tsx src/features/characters/tests/CharacterEditPage.test.tsx`에서 신규 3개 test가 `나이`, `원작 제목` 등 누락 label 때문에 실패하는 것을 확인했다. - **GREEN:** `CharacterOptionalFields`와 optional field serialization helper를 추가하고 create/edit page에서 optional scalar·반복 배열을 multipart `request`에 직렬화했다. `npm run test:run -- src/features/characters/tests/CharacterCreatePage.test.tsx src/features/characters/tests/CharacterEditPage.test.tsx src/features/characters/tests/character-api.test.ts` 결과 3 files / 18 tests passed. - **REFACTOR/회귀:** `npm run test:run -- src/features/characters` 결과 6 files / 26 tests passed. `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts` 결과 41 passed / 3 skipped. optional 입력 추가로 keyboard-only E2E의 tab 한도만 80회로 조정했다. `npm run typecheck`, `npm run lint`, `npm run build` 모두 exit 0. - **정정:** 독립 리뷰에서 수정 상세 응답에 없는 `originalTitle/originalLink`를 일반 저장 시 `null`로 전송할 수 있는 blocker를 확인했다. RED `npm run test:run -- src/features/characters/tests/CharacterEditPage.test.tsx`는 2 failed로 해당 전송을 재현했고, touched flag를 추가해 사용자가 입력·삭제한 경우에만 update payload에 포함하도록 수정했다. 이후 `npm run test:run -- src/features/characters/tests/CharacterCreatePage.test.tsx src/features/characters/tests/CharacterEditPage.test.tsx src/features/characters/tests/character-api.test.ts`는 3 files / 18 tests passed, `npm run test:run -- src/features/characters`는 6 files / 26 tests passed, `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts`는 41 passed / 3 skipped, `npm run typecheck`, `npm run lint`, `npm run build`는 모두 exit 0이었다. ### Task R3.2 — Character form 실패 복구와 image format 검증 **Goal 실행 `P3-R2`:** Character mutation 실패 뒤 입력을 유지해 재시도하고 JPEG/PNG extension↔MIME pair를 제출 전에 검증한다. - **연결 리뷰:** [Phase 3 재리뷰](./reviews/review-phase-3-20260729.md) — `REV-P3-002`, `REV-P3-003` - **시작 조건:** `P3-R1` form 구조 또는 충돌 없는 현재 form, `FILE-002`, 공통 `ApiError`. - **Files:** Create: `src/features/characters/validation/character-image-policy.ts`; Modify: `CharacterCreatePage.tsx`, `CharacterEditPage.tsx`, 해당 page test. - **Interfaces:** `.jpg/.jpeg ↔ image/jpeg`, `.png ↔ image/png`, 10,485,760 bytes; form error는 server message를 보존하고 `finally`로 pending을 해제한다. - **범위 밖:** backend image sniffing, 공통 crop 계산(`P1-R7`). - [x] **RED:** create/update rejection 뒤 button 재활성·오류 표시·두 번째 성공과 양방향 MIME mismatch 거부 test를 작성한다. - [x] **GREEN:** form-level try/catch/finally와 Character image policy를 적용한다. - [x] **REFACTOR:** create/edit 중복 validation·error copy만 작게 공유하고 Character 전체 회귀를 실행한다. - **실행 명령:** `npm run test:run -- src/features/characters`; `npm run typecheck`; `npm run lint`. - **기대 결과:** exit 0, 실패 뒤 재시도 1회 성공, mismatch multipart 0건. - **수동 확인:** server 500 후 입력 유지·재시도와 잘못된 filename/MIME inline 오류를 확인한다. **Progress — 2026-07-30 P3-R2 완료** - **RED:** `npm run test:run -- src/features/characters/tests/CharacterCreatePage.test.tsx src/features/characters/tests/CharacterEditPage.test.tsx` 결과 2 files failed, 6 failed / 13 passed, 2 unhandled `ApiError`였다. 신규 retry test는 서버 메시지를 찾지 못했고, mismatch test는 `JPEG 또는 PNG 파일만 업로드하세요.` inline 오류가 없어 실패했다. - **GREEN:** `src/features/characters/validation/character-image-policy.ts`를 추가해 `.jpg/.jpeg ↔ image/jpeg`, `.png ↔ image/png`, 10MB 제한을 검증하고, create/edit page에서 file 선택 시 crop source 전에 inline 오류를 표시했다. mutation submit은 `ApiError` message를 form 오류로 표시하고 `finally`에서 submit 상태를 해제한다. `npm run test:run -- src/features/characters/tests/CharacterCreatePage.test.tsx src/features/characters/tests/CharacterEditPage.test.tsx src/features/characters/tests/character-api.test.ts` 결과 3 files / 24 tests passed. - **REFACTOR/회귀:** `npm run test:run -- src/features/characters` 결과 6 files / 32 tests passed. `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts` 병렬 실행은 axe-heavy direct route test가 30초 제한에 걸렸고, focused Chromium 재실행은 1 passed였다. 해당 test에만 60초 timeout을 부여한 뒤 `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts --workers=1` 결과 41 passed / 3 skipped였다. `npm run typecheck`, `npm run lint`, `npm run build`는 모두 exit 0이었다. LSP diagnostics도 변경 TS/TSX와 E2E 파일에서 오류 없음이었다. ### Task R3.3 — 워크스페이스 탭 keyboard semantics와 운영 문구 복구 **Goal 실행 `P3-R3`:** 캐릭터 워크스페이스의 탭/내비게이션 의미와 keyboard 이동을 일치시키고 내부 Task 문구를 운영자 안내에서 제거한다. - **연결 리뷰:** [Phase 3 현재 리뷰](./reviews/phase3-character-workspace.md) — `REV-P3-004`, `REV-P3-005` - **시작 조건:** PRD `7.2`, `14.2`의 1차 탭·keyboard-only 이동과 비활성 read-only 안내. - **완료 증거:** 실패하는 keyboard/accessible-role test, 최소 수정, Character focused unit·mock E2E·접근성 Gate와 검증 기록. - **Files:** Modify: `src/layouts/CharacterWorkspaceLayout.tsx`, `src/layouts/CharacterWorkspaceLayout.test.tsx`, `tests/e2e/character-workspace.spec.ts`, 필요 시 `tests/e2e/accessibility.spec.ts`. - **Interfaces:** ARIA tab pattern을 유지하면 활성 tab만 page Tab 순서에 두고 좌우 화살표·tabpanel 연결을 구현한다. 페이지 내비게이션으로 단순화하면 `nav`/link와 `aria-current="page"`를 사용해 tab 역할을 제거한다. - **범위 밖:** route 구조 변경, 새 router dependency, 비활성 Character 정책 변경. - [x] **RED:** 현재 다섯 `role=tab`이 모두 Tab 순서에 들어가고 좌우 화살표로 이동하지 않는 실패 test, 배너에 `P3-T2`가 노출되는 실패 test를 작성한다. - [x] **GREEN:** 한 가지 일관된 접근성 패턴만 적용하고 배너를 운영 영향·복원 미지원 중심의 한국어 문구로 교체한다. - [x] **REFACTOR:** active 상태와 panel 연결 중복을 작은 data structure로 정리하되 범용 tab abstraction은 만들지 않는다. - **실행 명령:** `npm run test:run -- src/layouts src/features/characters`; `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts tests/e2e/accessibility.spec.ts`; `npm run typecheck`; `npm run lint`; `npm run build`. - **기대 결과:** 모든 명령 exit 0, 선언한 role과 keyboard 동작 불일치 0건, 사용자 화면의 `P3-T2` 노출 0건. - **수동 확인:** keyboard-only로 프로필 → 오디오 → 시리즈 → 커뮤니티 → FanTalk 이동과 비활성 배너 문구를 확인한다. **Progress — 2026-07-30 P3-R3 완료** - **RED:** `npm run test:run -- src/layouts/CharacterWorkspaceLayout.test.tsx` 결과 1 file failed, 2 failed / 3 passed. 신규 native navigation expectation은 `캐릭터 워크스페이스 내비게이션` role을 찾지 못하고 기존 `tablist/tab`을 노출해 실패했으며, 비활성 배너는 `P3-T2` 내부 문구를 계속 표시해 운영 문구 assertion이 실패했다. - **GREEN:** `CharacterWorkspaceLayout`의 workspace 전환 UI를 `nav` + native link + `aria-current="page"` 패턴으로 단일화하고, active link data structure로 중복을 줄였다. 비활성 배너는 조회 전용 운영 영향, 복원 기능 미지원, 영구 삭제와 삭제 후 복구 미제공을 설명하는 한국어 문구로 교체했다. `npm run test:run -- src/layouts/CharacterWorkspaceLayout.test.tsx` 결과 1 file / 5 tests passed. - **REFACTOR/회귀:** `npm run test:run -- src/features/characters src/layouts/CharacterWorkspaceLayout.test.tsx src/features/community-posts/tests/community-list.test.tsx src/features/fan-talks/tests/fan-talk-list.test.tsx` 결과 9 files / 45 tests passed. `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts --project=chromium` 결과 11 passed. `npm run typecheck`와 `npm run lint`는 모두 exit 0이었다. 변경 TS/TSX 파일 LSP diagnostics는 모두 오류 0건이었다. ### Task R3.4 — 워크스페이스 header characterId 복구 **Goal 실행 `P3-R4`:** 모든 캐릭터 워크스페이스 header에 현재 numeric `characterId`를 다시 표시해 운영자가 작업 대상을 식별할 수 있게 한다. - **연결 리뷰:** [Phase 3 현재 리뷰](./reviews/phase3-character-workspace.md) — `REV-P3-006` - **시작 조건:** PRD §7.2의 이미지·이름·활성 상태·`characterId` 상시 표시 기준. - **완료 증거:** 현재 ID 미노출 assertion을 뒤집은 실패 test, header 최소 표시 수정, Character/layout focused unit·mock E2E·접근성 Gate 통과. - **Files:** Modify: `src/layouts/CharacterWorkspaceLayout.tsx`, `src/layouts/CharacterWorkspaceLayout.test.tsx`, `tests/e2e/character-workspace.spec.ts`; Docs: `P3-R4` 수정 후 검증 기록. - **Interfaces:** API의 numeric `character.id`를 운영 label과 함께 표시하고 `characterUUID` 프로필 필드는 별도 값으로 유지한다. - **범위 밖:** route ID 형식 변경, UUID를 `characterId`로 대체, 전체 상세 metadata 재배치. - [x] **RED:** active/inactive workspace header 모두 현재 `characterId`를 표시해야 한다는 실패 test를 작성하고 기존 미노출 assertion을 제거했다. - [x] **GREEN:** 이름·상태 주변에 구분되는 `characterId` label/value를 표시했다. - [x] **REFACTOR:** Series visual QA의 raw 내부 field-name 검색은 요구사항상 표시해야 하는 header ID를 예외 처리하고 실제 원치 않는 raw label 검사는 유지했다. - **실행 명령:** `npm run test:run -- src/layouts/CharacterWorkspaceLayout.test.tsx src/features/characters`; `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts --project=chromium`; `npm run typecheck`; `npm run lint`; `npm run build`. - **기대 결과:** 모든 명령 exit 0, 모든 workspace header의 현재 numeric ID 표시, 다른 캐릭터 ID 혼입 0건. - **수동 확인:** Character 101/202의 프로필·Audio·Series·Community·FanTalk header에서 각각 올바른 ID를 확인한다. **P3-R4 수정 검증 기록 (2026-07-31):** - RED: `npm run test:run -- src/layouts/CharacterWorkspaceLayout.test.tsx` — 2 failed / 3 passed. active `101`과 inactive `202` workspace header에서 `characterId:` 표시를 찾지 못해 기대대로 실패했다. - GREEN focused: `npm run test:run -- src/layouts/CharacterWorkspaceLayout.test.tsx src/features/characters` — 성공, 7 files / 41 tests passed. header의 이름·상태 근처에 numeric `characterId`가 표시되고 기존 프로필 `characterUUID`는 별도 값으로 유지됨을 확인했다. - REFACTOR: `rg -n 'characterId:|raw|SUN|PROCEEDING|genreId' tests/e2e src/features/series src/layouts`로 자동 raw-leak assertion을 확인했고, 현재 Series raw label 검사는 `characterId:`를 금지하지 않아 추가 변경하지 않았다. - 개발 중 E2E: 사용자 지시에 따라 전체 Task 완료 전 반복 실행하지 않았다. 지정 mock Character E2E는 최종 회귀 단계에서 필요 시 실행한다. ### Task R3.5 — Character URL query 계약 키 정렬 **Goal 실행 `P3-R5`:** Character 목록의 브라우저 검색 상태를 API 계약과 같은 `searchTerm` query로 읽고 쓰며 직접 링크 복원을 보장한다. - **연결 리뷰:** [Phase 3 현재 리뷰](./reviews/phase3-character-workspace.md) — `REV-P3-007` - **시작 조건:** PRD §7.1과 OpenAPI `GET /api/v2/admin/ai-characters`의 `searchTerm`, `page`, `size` query 계약. - **완료 증거:** `?searchTerm=루나` 직접 진입과 검색 변경 URL assertion의 실패 test, 목록 URL parser/serializer 최소 수정, focused unit·mock E2E·정적 Gate 통과. - **Files:** Modify: `src/features/characters/pages/CharacterListPage.tsx`, `src/features/characters/tests/character-list.test.tsx`, `tests/e2e/character-workspace.spec.ts`. - **Interfaces:** 브라우저 URL과 API request 모두 `searchTerm`을 사용하고 `page`, `size`, 300ms 검색 동작은 유지한다. - **범위 밖:** legacy `search` query의 장기 호환 layer, router dependency 도입, server-side filter 추가. - [x] **RED:** `?searchTerm=루나&page=1&size=20` 직접 진입이 input·request를 복원하고 검색 변경 URL에 `search`가 없어야 한다는 실패 test를 작성했다. - [x] **GREEN:** URL read/write key를 `searchTerm`으로 교체하고 adapter 인자를 그대로 연결했다. - [x] **REFACTOR:** 내부 state 이름은 필요한 범위에서만 정리하고 별도 query abstraction을 만들지 않았다. - **실행 명령:** `npm run test:run -- src/features/characters/tests/character-list.test.tsx`; `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts --project=chromium`; `npm run typecheck`; `npm run lint`; `npm run build`. - **기대 결과:** 모든 명령 exit 0, Character URL의 검색 key는 `searchTerm` 하나이고 직접 링크·검색·pagination 왕복이 일치. - **수동 확인:** 검색 결과 2페이지 URL을 새 탭에서 열어 같은 검색어·page·size와 목록을 복원하는지 확인한다. **P3-R5 수정 검증 기록 (2026-07-31):** - RED: `npm run test:run -- src/features/characters/tests/character-list.test.tsx` — 2 failed. 핵심 실패는 `?searchTerm=루나&page=1&size=20` 직접 진입 뒤 API request의 `searchTerm`이 `null`인 점이었다. 두 번째 loading flow는 같은 parser 불일치로 초기 대기 Promise가 열리지 않아 timeout됐다. - GREEN focused: `npm run test:run -- src/features/characters/tests/character-list.test.tsx` — 성공, 1 file / 2 tests passed. 직접 링크 input·request 복원과 검색 변경 URL의 `searchTerm` 사용, `search=` 미사용을 확인했다. - REFACTOR/회귀: `npm run test:run -- src/features/characters` — 성공, 6 files / 36 tests passed. `npm run typecheck`, `npm run lint`, `npm run build`, `git diff --check` — 모두 exit 0 또는 no output. LSP diagnostics: `CharacterListPage.tsx`, `character-list.test.tsx` 오류 0건. - 개발 중 E2E: 사용자 지시에 따라 전체 Task 완료 전 반복 실행하지 않았다. 지정 mock Character E2E는 최종 회귀 단계에서 필요 시 실행한다. ### Task R3.6 — Character image crop 확정 경계 보강 **Goal 실행 `P3-R6`:** Character 생성·수정에서 원본 파일을 crop 적용 전에 mutation payload로 확정하지 않고 준비·취소·연속 선택 경계를 안전하게 처리한다. - **연결 리뷰:** [Phase 3 현재 리뷰](./reviews/phase3-character-workspace.md) — `REV-P3-008` - **시작 조건:** `FILE-007`, `FILE-009~010`, PRD §10.5 저장 중 feedback와 Character image 필수/optional 계약. - **완료 증거:** 원본 제출·crop 취소·준비 중 제출·stale selection을 재현하는 실패 test, staged selection 최소 수정, Character focused unit·mock E2E·file Gate 통과. - **Files:** Modify: `src/features/characters/pages/CharacterCreatePage.tsx`, `src/features/characters/pages/CharacterEditPage.tsx`, 관련 Character tests와 `tests/e2e/character-workspace.spec.ts`; 필요 시 기존 Character image helper. - **Interfaces:** committed `image`는 crop `onApply` 결과만 가리킨다. 준비/crop 중 제출은 비활성화·guard하고, 취소는 생성의 새 선택을 제거하며 수정의 기존 서버 media를 유지한다. 마지막 선택만 dialog를 열고 준비 실패를 inline 표시한다. - **범위 밖:** 공통 crop dialog 계산 변경, 새 범용 upload state library, backend image sniffing. - [x] **RED:** create의 crop 미적용 원본 제출과 취소 뒤 제출, create/edit의 pending source 준비 중 submit, 느린 첫 선택이 빠른 둘째 선택을 덮는 경우를 실패 test로 고정했다. - [x] **GREEN:** raw file과 committed crop result를 분리하고 selection token·준비 상태·오류 처리·submit guard를 추가했다. - [x] **REFACTOR:** 새 전역 abstraction 없이 create/edit 내부의 작은 중복 상태로 유지하고, 기존 Character App 회귀 test는 crop apply 경로를 지나도록 정렬했다. - **실행 명령:** `npm run test:run -- src/features/characters`; `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts --project=chromium`; `npm run typecheck`; `npm run lint`; `npm run build`. - **기대 결과:** 모든 명령 exit 0, Character multipart의 uncropped 원본 0건, 취소·실패·stale source의 mutation 0건, 정상 crop 결과만 전송. - **수동 확인:** 느린 이미지에서 연속 선택·취소·적용·즉시 제출을 반복해 선택한 마지막 crop 결과만 preview와 request에 반영되는지 확인한다. #### P3-R6 수정 검증 기록 — 2026-07-31 - RED: `npm run test:run -- src/features/characters/tests/CharacterCreatePage.test.tsx src/features/characters/tests/CharacterEditPage.test.tsx` — 기대 실패 확인, 2 files failed / 6 failed / 23 passed 및 reject 준비 실패 unhandled rejection을 확인했다. 핵심 실패는 생성 pending submit button 미비활성, 생성 crop cancel 뒤 raw file 잔존, 수정 pending submit button 미비활성, 준비 reject inline 오류 미표시였다. - GREEN: `CharacterCreatePage.tsx`와 `CharacterEditPage.tsx`에서 committed `image`를 crop apply 결과만 보유하도록 분리하고, selection id·준비 상태·stale ignore·reject inline 오류·submit disabled/guard를 추가했다. `npm run test:run -- src/features/characters/tests/CharacterCreatePage.test.tsx src/features/characters/tests/CharacterEditPage.test.tsx` — 성공, 2 files / 29 tests passed. - Character 회귀: `npm run test:run -- src/features/characters` — 성공, 6 files / 43 tests passed. `character-mutation-reload.test.tsx`는 App 생성 성공 경로가 실제 crop dialog 적용 후 저장하도록 보강했다. - 정적 검증: `npm run typecheck`, `npm run lint`, `npm run build`, `git diff --check` — 모두 성공(exit 0). Build는 255 modules transformed, production bundle 생성 완료. - LSP: `lsp_diagnostics`는 `CharacterCreatePage.tsx`, `CharacterEditPage.tsx`, `character-mutation-reload.test.tsx`에서 오류 0건이었다. `CharacterCreatePage.test.tsx`, `CharacterEditPage.test.tsx` 단일 fresh diagnostics는 timeout이었고, `lsp_diagnostics src/features/characters`와 `npm run typecheck`로 characters 디렉터리 오류 0건을 보완 확인했다. - E2E: 사용자 지시가 “개발 중 unit tests만, 전체 Task 이후 필요 시 E2E”였으므로 `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts --project=chromium`은 이번 P3-R6 개발 중 실행하지 않고 후속 최종 회귀 단계로 이연했다. --- ## Phase 4. Audio vertical slice **목표:** 선택 Character의 Audio를 제목 검색·검수·발행·수정·비활성화하고 대용량 upload를 안전하게 제어한다. **Phase Goal `P4`:** Task 4.1 → 4.4와 Phase 4 Gate로 Audio 조회·재생·발행·upload slice를 완성한다. - **시작 조건:** Phase 3 workspace core(`P3-T2`)와 `P2-GATE` 완료. - **완료 조건:** `P4-T1`~`P4-T4`, `P4-GATE` 완료. 오류·price 계약은 제공값 또는 명시된 최소 규칙으로 종결. - **실행 순서:** 계약 확인 → 목록/상세/player → form/upload → 반응형·접근성. **요구사항:** `AUDIO-001~033`, `FILE-001~002`, `FILE-006~009`, `FILE-012~013`, PRD `9`의 Audio 범위. **외부 의존:** `EXT-007` active-only 반환 보장. price 상한은 `99999`이고, 파일 용량·MIME 등 backend 동일 검증은 확인됐다. OpenAPI 밖의 Audio 도메인 오류 message key는 분기하지 않는다. status filter·답변 없는 status badge는 현재 범위에서 제외한다. **주요 Files:** - Create: `src/features/audio-contents/api/{audio-content-api,audio-content-theme-api,upload-audio-content}.ts` - Create: `src/features/audio-contents/model/types.ts` - Create: `src/features/audio-contents/schemas/audio-content-schema.ts` - Create: `src/features/audio-contents/validation/audio-cover-policy.ts` - Create: `src/features/audio-contents/pages/{AudioContentListPage,AudioContentDetailPage,AudioContentFormPage}.tsx` - Create: `src/features/audio-contents/components/{AudioContentList,AudioContentListItem,AudioContentSummary,AudioContentForm,AudioContentThemeSelect,ReleaseScheduleField,SeriesMultiCombobox}.tsx` - Create: `src/features/audio-contents/tests/{audio-contract,audio-upload}.test.ts` - Create: `src/features/audio-contents/tests/{audio-list,audio-player,audio-form}.test.tsx` - Create: `tests/e2e/audio-content.spec.ts` #### Phase 4 Task 실행 계약 | Goal | Files | Interfaces | TDD·검증 기준 | |---|---|---|---| | `P4-T1` | Modify: `docs/20260725_AI캐릭터관리자웹/plan-task.md`; Read: `docs/20260725_AI캐릭터관리자웹/prd.md`, `docs/20260725_AI캐릭터관리자웹/api-contract.openapi.json`; Test: 없음 | Consumes: `AudioContent*`, `AudioTheme*` schema·5 Audio operation. Produces: list/detail/create/update/theme contract map과 screen inventory | **TDD 예외:** 외부 계약 조사 Task다. OpenAPI JSON parse와 `rg -n 'AUDIO-0(2[7-9]|3[0-3])'` 문서 추적 검사를 실행한다. 기대 `exit 0`. 수동 확인: status·seriesIds·수정 file 제외가 명시된다. | | `P4-T2` | Create: `src/features/audio-contents/api/audio-content-api.ts`, `src/features/audio-contents/model/types.ts`, `src/features/audio-contents/pages/{AudioContentListPage,AudioContentDetailPage}.tsx`, `src/features/audio-contents/components/{AudioContentList,AudioContentListItem,AudioContentSummary}.tsx`, `src/features/audio-contents/tests/{audio-list,audio-player}.test.tsx` | Consumes: `AudioContentListResponse`, `AudioContentDetailResponse`. Produces: `getAudioContents({characterId,search_word,page,size})`, `getAudioContent({characterId,contentId,timezone})`와 player UI | **TDD 적용:** `npm run test:run -- src/features/audio-contents/tests/audio-list.test.tsx src/features/audio-contents/tests/audio-player.test.tsx`; 기대 `exit 0`. 수동 확인: 2자 검색, timezone, 단일 재생·no-auto-refetch. | | `P4-T3` | Create: `src/features/audio-contents/api/{audio-content-theme-api,upload-audio-content}.ts`, `src/features/audio-contents/schemas/audio-content-schema.ts`, `src/features/audio-contents/validation/audio-cover-policy.ts`, `src/features/audio-contents/pages/AudioContentFormPage.tsx`, `src/features/audio-contents/components/{AudioContentForm,AudioContentThemeSelect,ReleaseScheduleField}.tsx`, `src/features/audio-contents/tests/{audio-contract.test.ts,audio-upload.test.ts,audio-form.test.tsx}`; Modify: `src/features/audio-contents/api/audio-content-api.ts` | Consumes: `AudioContentCreateRequest`, `AudioContentUpdateRequest`, `AudioContentTheme`. Produces: `createAudioContent`, `updateAudioContent`, `deactivateAudioContent`, upload adapter | **TDD 적용:** `npm run test:run -- src/features/audio-contents/tests/audio-contract.test.ts src/features/audio-contents/tests/audio-upload.test.ts src/features/audio-contents/tests/audio-form.test.tsx`; 기대 `exit 0`. 수동 확인: contentFile, local releaseDate, theme, 진행률·취소·재시도. | | `P4-T4` | Modify: `src/features/audio-contents/pages/{AudioContentListPage,AudioContentDetailPage,AudioContentFormPage}.tsx`, `src/features/audio-contents/components/{AudioContentList,AudioContentListItem,AudioContentSummary,AudioContentForm,AudioContentThemeSelect,ReleaseScheduleField}.tsx`; Test: `tests/e2e/audio-content.spec.ts` | Consumes: P4-T2/T3 UI. Produces: viewport·keyboard capability evidence | **TDD 적용:** `npm run e2e:mock -- tests/e2e/audio-content.spec.ts`; 기대 지원 project 전부 통과. 수동 확인: 320px player, 200% zoom, keyboard, axe. | `P4-T2`~`P4-T4`는 각 row의 focused test로 RED → GREEN → REFACTOR를 실행하고, 관련 feature test·typecheck·lint 결과와 수동 확인을 `§7 검증 기록`에 누적한다. ### Task 4.1 Phase 계약 확인 **Goal 실행 `P4-T1`:** Audio 오류·price·theme·상태 계약, mock scenario와 component map을 확정한다. - **시작 조건:** `P3-T2`, `P2-GATE` 완료, PRD `AUDIO-001~033`, `MOCK-001~009`와 OpenAPI Audio·theme operation/schema 확인. - **완료 증거:** 체크박스 전체, contract fixture와 상태/action inventory의 세 문서 일치. - **범위 밖:** 오류 status/key 또는 price 상한 추정과 Audio UI 구현. - [x] OpenAPI 공통 오류 status·shape를 fixture에 기록하고 Audio 전용 message key와 backend container·codec 오류 계약은 미제공으로 표시한다. 정확한 fixture를 추정하지 않는다. - [x] 오디오 테마 목록 `GET /api/v2/admin/ai-characters/audio-content-themes`가 query/body 없이 호출되고 `data[]`의 `id`, `theme`, `image`를 반환하는 contract fixture를 기록한다. - [x] price 최대값이 제공되면 schema와 경계 test를 추가하고, 없으면 상한을 만들지 않는다. - [x] 목록 `search_word/page/size`, `data.totalCount/items`, 상세 필수 `timezone`, 생성 `contentFile/coverImage/request`, 생성 `data.contentId`, 수정 `data=null`을 contract fixture로 고정한다. - [x] status query·status field가 없음을 고정하고 client status filter·status enum을 만들지 않는다. - [x] 목록·상세·player·form/upload의 상태/action inventory를 작성하고 Page는 route/query/policy 조합, feature component는 Audio 규칙, shared component는 Phase 1 contract를 사용하도록 component map을 확정한다. - [x] 제공 계약 범위의 Audio seed와 목록·상세·player·theme·upload CRUD browser handler 시나리오를 확정한다. ### Task 4.2 목록·상세·player **Goal 실행 `P4-T2`:** Audio 목록·상세 복원과 안전한 단일 재생 흐름을 완성한다. - **시작 조건:** `P4-T1` 완료. - **완료 증거:** 체크박스 전체, list/detail/player test와 signed URL 비기록·no-auto-refetch 기록. - **범위 밖:** 생성·수정·upload form. - [x] UI 검색어가 2자 이상일 때만 API `search_word`로 직렬화되고 `page`, `size` URL 상태와 `data.items` 역직렬화, loading·empty·error·retry가 동작하는 test를 작성한다. - [x] status query·활성 query·client-side status/active filter request가 0회임을 test한다. - [x] Audio detail route의 직접 진입과 새로고침에서 `timezone=Asia/Seoul`을 보내 같은 resource를 복원하는 test를 작성한다. - [x] 목록과 상세가 Phase 1 `AdminAudioPlayer`를 조합하고 play/pause, seek, current/duration, volume, speed, keyboard를 지원하는 integration test를 작성한다. - [x] 한 player 재생 시 기존 player가 정지되고 명시적 download button이 없음을 test한다. - [x] media error를 signed URL 만료로 추정하지 않고 일반 오류·수동 재시도·페이지 새로고침 안내를 표시한다. - [x] media error만으로 목록/detail GET과 `play()`가 자동 재호출되지 않는 test를 작성한다. - [x] signed URL이 log·storage·분석 event로 전달되지 않는 test를 작성한다. - [x] mock mode는 실제 endpoint shape와 local preview audio asset으로 목록 → 상세 → 단일 재생 최종 UI를 확인한다. ### Task 4.3 발행 form·upload **Goal 실행 `P4-T3`:** theme 선택, 즉시/예약 발행, multipart media와 진행률·취소·재시도를 완성한다. - **시작 조건:** `P4-T1`, `P4-T2` 완료. - **완료 증거:** 체크박스 전체, contract/form/upload/file-boundary test, `OQ-009` 결정과 검증 기록. - **범위 밖:** resumable upload, client codec 판정, 계약 없는 price 상한. - [x] 생성 multipart의 `contentFile`, `coverImage`, `request`가 필수이고 수정에는 optional `coverImage`와 `request`만 있으며 content file 교체 part·UI가 없음을 test한다. - [x] Audio cover가 Phase 1 `FileField`·`ImageCropDialog`의 JPEG/PNG·10MB, `1:1`, 최대 800px, no-upscale profile을 조합하는 test를 작성한다. - [x] MP3/AAC/M4A 허용, WAV 거부, extension/MIME 조합을 test한다. - [x] `.m4a + audio/x-m4a`만 호환 조합으로 허용하고 실제 container·codec 판정은 server 책임으로 둔다. - [x] `1,024,000,000 bytes` 허용, `1,024,000,001 bytes` 거부 경계 test를 작성한다. - [x] price는 0 이상 정수 “캔”으로 입력·format한다. - [x] 생성 form은 오디오 테마 목록을 불러와 visible label이 있는 선택 UI를 제공하고, 미선택 제출을 차단하며 선택한 `themeId`를 create payload에 포함하는 test를 작성한다. - [x] 즉시 공개 기본값은 날짜 입력을 비활성화·초기화하고 `releaseDate=null`, `timezone="Asia/Seoul"`을 보낸다. - [x] 예약 공개는 미래 Asia/Seoul 시각만 받고 `yyyy-MM-dd HH:mm` 문자열과 `timezone="Asia/Seoul"`을 보내며 UTC `Z`로 변환하지 않는 test를 작성한다. - [x] 수정 form은 계약에 없는 release schedule·theme·series·content file 변경 control을 만들지 않고 기존 값을 읽기 전용으로 표시한다. - [x] create request에 필수 `title`, `detail`, `tags`, `price`, 유효한 `themeId`가 있고 `status`, `isActive`, `seriesIds`가 없음을 contract test로 고정한다. - [x] create optional purchase/limited/adult/preview/point/comment/detail/language field의 enum·type·OpenAPI default와 serializer를 test한다. - [x] update request는 `title`, `detail`, `tags`, `price`, `isAdult`, `isActive`, `isPointAvailable`, `isCommentAvailable` 이외 field를 보내지 않고 soft delete에만 `isActive=false`를 보낸다. - [x] upload 진행률, AbortController 취소, 전체 재시도, 실패 후 form/file 상태 보존을 test한다. - [x] 415 server 오류를 field 안내로 보존하고 resumable upload는 만들지 않는다. - [x] create 성공의 `data.contentId`로 상세에 이동하고, update/soft delete의 `data=null` 성공은 기존 ID cache를 무효화한다. soft delete 후 목록 이동과 toast를 제공하며 active-only 제거는 외부 계약 제공 후 server mode에서 검증한다. - [x] 초기 Audio form을 실제 페이지에서 확인한 뒤 `title`, `detail`, `tags`의 최대 길이 권고안을 작성한다. backend 호환 확인 전에는 상한을 구현하지 않는다. - [x] mock handler가 multipart request contract를 검증하고 create/update/deactivate 후 같은 store의 list/detail을 갱신하는 E2E를 작성한다. ### Task 4.4 Audio 반응형·접근성 **Goal 실행 `P4-T4`:** Audio viewport capability와 player/form 접근성 기준을 검증한다. - **시작 조건:** `P4-T2`, `P4-T3`의 활성 범위 완료. - **완료 증거:** 체크박스 전체, 320px·keyboard·200% zoom·axe E2E 기록. - **범위 밖:** 모바일 create/edit/deactivate/upload. - [x] mobile에서는 목록·상세·player만 제공하고 create/edit/deactivate/upload는 숨김이 아닌 route capability로 차단한다. - [x] desktop/tablet에서는 모든 form·upload action을 제공한다. - [x] 320px에서 player control, error text, 긴 title이 overflow하지 않는 E2E를 작성한다. - [x] keyboard-only player/form, 200% zoom, axe critical·serious 0건을 확인한다. ### Phase 4 Gate **Goal 실행 `P4-GATE`:** Audio mock UI journey와 실제 server integration 상태를 분리해 판정한다. - **시작 조건:** `P4-T1`~`P4-T4` 완료. - **완료 증거:** 아래 명령과 Expected 통과 및 Phase 검증 기록. - **범위 밖:** 실패와 무관한 Series/Community 구현. ```bash npm run test:run -- src/features/audio-contents src/shared/ui src/shared/validation npm run e2e:mock -- tests/e2e/audio-content.spec.ts npm run e2e -- tests/e2e/audio-content.spec.ts npm run typecheck npm run lint npm run build ``` **Expected:** mock mode에서 OpenAPI `contentFile`·`releaseDate` 계약으로 Audio 즉시/예약 생성 → 진행률/취소/재시도 → 검색·상세 재생 → 허용 field 수정 → soft delete 요청의 UI가 통과하고 media error가 자동 refetch·자동 재생을 0회 발생시킨다. status filter·seriesIds·content file 교체 request는 0건이며 server mode와 active-only 결과는 별도로 기록한다. **수동 확인:** 320px player와 desktop/tablet form을 keyboard-only로 확인하고, `search_word`, `timezone`, `contentFile`, `releaseDate`, theme field와 수정 금지 control이 실제 network 요청·화면에 일치하는지 본다. ### Task R4.1 — Audio create 설정과 raw price validation 완성 **Goal 실행 `P4-R1`:** Audio create optional 설정을 form에서 편집하고 음수·소수 가격을 변형하지 않고 거부한다. - **연결 리뷰:** [Phase 4 재리뷰](./reviews/review-phase-4-20260729.md) — `REV-P4-001`, `REV-P4-002` - **시작 조건:** `AUDIO-019`, `AUDIO-033`, OpenAPI `AudioContentCreateRequest`. - **Files:** Modify: `AudioContentForm.tsx`, `audio-content-form-helpers.ts`, `audio-content-schema.ts`, `audio-form.test.tsx`, `audio-contract.test.ts`, `tests/e2e/audio-content.spec.ts`. - **Interfaces:** `purchaseOption/limited/isAdult/isGeneratePreview/isOnlyRental/isPointAvailable/isCommentAvailable/isFullDetailVisible/previewStartTime/previewEndTime/languageCode`; raw price는 `0..99999` integer만 serialize. - **범위 밖:** OpenAPI 밖 상관관계 validation, update에서 계약상 수정 불가한 field. - [x] **RED:** optional control별 대표 payload와 `-1`, `1.5` request 0건 test를 작성한다. - [x] **GREEN:** contract default로 초기화한 visible control과 raw/display price state를 최소 구현한다. - [x] **REFACTOR:** request builder에 hard-coded field를 제거하고 create schema/form/E2E를 재실행한다. - **실행 명령:** `npm run test:run -- src/features/audio-contents/tests/audio-form.test.tsx src/features/audio-contents/tests/audio-contract.test.ts`; `npm run e2e:mock -- tests/e2e/audio-content.spec.ts`. - **기대 결과:** exit 0, optional property 입력 누락 0건, `-1/1.5/100000` request 0건, `0/99999` 허용. - **수동 확인:** desktop/tablet에서 설정별 label·keyboard 입력과 가격 오류 focus를 확인한다. - **완료 기록(2026-07-30):** create 설정 control·payload와 raw price invalid 상태를 구현하고 `REV-P4-001`, `REV-P4-002`를 수정 완료 처리했다. `npm run test:run -- src/features/audio-contents` 8 files / 35 tests passed, `npm run e2e:mock -- tests/e2e/audio-content.spec.ts` 21 passed / 3 skipped, `npm run typecheck`, `npm run lint`, `npm run build`, targeted `git diff --check` 통과. ### Task R4.2 — upload 중복 제출과 401 만료 복구 **Goal 실행 `P4-R2`:** Audio upload pending 동안 POST를 한 번만 실행하고 XHR 401을 공통 session expiry 흐름으로 처리한다. - **연결 리뷰:** [Phase 4 재리뷰](./reviews/review-phase-4-20260729.md) — `REV-P4-003`, `REV-P4-004` - **시작 조건:** `P1-R6`의 인증 만료 interface 또는 동일 callback contract 확정. - **Files:** Modify: `AudioContentForm.tsx`, `upload-audio-content.ts`, `audio-form-upload.test.tsx`, `audio-upload.test.ts`, `AudioContentFormPage.tsx`, app API dependency 조합. - **Interfaces:** upload adapter는 token reader와 `onAuthExpired`를 주입받고 protected 401 burst에서 session clear/login 1회; form은 pending ref와 disabled 상태를 가진다. - **범위 밖:** resumable upload, refresh token, Axios migration. - [x] **RED:** unresolved upload에 submit 두 번 시 POST 2건인 실패와 XHR malformed/normal 401에서 session이 남는 실패 test를 추가한다. - [x] **GREEN:** synchronous pending guard/disabled button과 공통 auth expiry dependency를 적용한다. - [x] **REFACTOR:** abort/error/success 모든 종료 경로에서 pending/controller cleanup을 한곳으로 정리한다. - **실행 명령:** `npm run test:run -- src/features/audio-contents/tests/audio-form-upload.test.tsx src/features/audio-contents/tests/audio-upload.test.ts src/shared/api/__tests__/client-auth.test.ts`. - **기대 결과:** exit 0, pending 중 upload request 1건, 401 clear/login 1회, cancel 후 재제출 가능. - **수동 확인:** 느린 upload double-click과 만료 token upload의 login 복귀를 확인한다. - **완료 기록(2026-07-30):** pending upload 중 submit 2회가 upload request 1건으로 고정되고 cancel 후 재제출 가능하도록 form guard를 추가했다. XHR upload adapter는 token reader/session clear/login callback을 주입받아 protected 401 burst에서 clear/login 1회만 실행한다. RED 확인: form은 `expected 2 to be 1`, upload는 `clearSession 0회`로 실패. GREEN/검증: focused 2 files / 19 tests passed, Audio 전체 8 files / 38 tests passed, `npm run typecheck`, `npm run lint`, `npm run build`, targeted `git diff --check` 통과. ### Task R4.3 — Audio media accept와 cover format pair 정렬 **Goal 실행 `P4-R3`:** validator와 file picker가 같은 M4A MIME 집합을 사용하고 cover extension↔MIME 불일치를 거부한다. - **연결 리뷰:** [Phase 4 재리뷰](./reviews/review-phase-4-20260729.md) — `REV-P4-005`, `REV-P4-006` - **시작 조건:** `AUDIO-014`, `AUDIO-024`, `FILE-002`. - **Files:** Modify: `AudioContentForm.tsx`, `audio-content-form-helpers.ts`, `audio-cover-policy.ts`, `audio-contract.test.ts`, `file-media-policy.test.ts`. - **Interfaces:** input accept는 `AUDIO_FILE_POLICY.allowedMimeTypes`; cover mapping은 jpg/jpeg→image/jpeg, png→image/png. - **범위 밖:** container/codec sniffing, backend 오류 key. - [x] **RED:** accept에 `audio/mp4` 포함과 cover 양방향 mismatch 거부 test를 작성한다. - [x] **GREEN:** shared audio policy에서 accept를 생성하고 cover pair map을 적용한다. - [x] **REFACTOR:** 중복 MIME literal을 제거하고 Community audio 회귀를 실행한다. - **실행 명령:** `npm run test:run -- src/shared/validation/file-media-policy.test.ts src/features/audio-contents src/features/community-posts`. - **기대 결과:** exit 0, canonical/compat M4A 2종 허용, mismatch cover 0건. - **수동 확인:** Chrome/Safari file chooser에서 `.m4a` 선택 가능 여부를 확인한다. - **완료 기록(2026-07-30):** Audio file picker `accept`를 `AUDIO_FILE_POLICY.allowedMimeTypes`에서 생성하고, cover `.jpg/.jpeg→image/jpeg`, `.png→image/png` mismatch를 upload 전 `mime` 오류로 차단했다. RED 확인: accept는 `audio/mp4` 누락으로 실패, cover mismatch는 `{ ok: true }`로 실패. GREEN/검증: focused 3 files / 24 tests passed, Audio+Community 회귀 12 files / 76 tests passed, `npm run typecheck`, `npm run lint`, `npm run build`, targeted `git diff --check` 통과. ### Task R4.4 — Audio 일반 update와 soft delete schema 분리 **Goal 실행 `P4-R4`:** Audio 일반 update에서 `isActive`를 금지하고 soft delete adapter만 `{ isActive:false }`를 전송하게 한다. - **연결 리뷰:** [Phase 4 현재 리뷰](./reviews/phase4-audio-content.md) — `REV-P4-007` - **시작 조건:** `AUDIO-007`, OpenAPI `AudioContentUpdateRequest`, 기존 `deactivateAudioContent`. - **완료 증거:** `isActive=true/null` 실패 test, schema/API 최소 수정, Audio focused unit·mock E2E·Phase Gate와 검증 기록. - **Files:** Modify: `src/features/audio-contents/schemas/audio-content-schema.ts`, `src/features/audio-contents/api/audio-content-api.ts`, `src/features/audio-contents/tests/audio-contract.test.ts`, `src/shared/mocks/audio-content-mock-store.ts`. - **Interfaces:** 일반 `AudioContentUpdateRequest`에는 `isActive`가 없고, 별도 deactivate schema는 `z.strictObject({isActive:z.literal(false)})`만 허용한다. - **범위 밖:** OpenAPI JSON 축소, Audio 복원, backend partial update 의미 변경. - [x] **RED:** 일반 update schema/body가 `isActive=true/null/false`를 거부하고 deactivate body만 false를 허용하는 실패 test를 작성한다. - [x] **GREEN:** 일반 update와 deactivate request schema/API helper/mock parser를 분리한다. - [x] **REFACTOR:** 기존 update test의 금지된 `isActive:true` 성공 assertion을 제거하고 Community·Series와 같은 불변식 용어로 정리한다. - **실행 명령:** `npm run test:run -- src/features/audio-contents src/shared/mocks`; `npm run e2e:mock -- tests/e2e/audio-content.spec.ts`; `npm run typecheck`; `npm run lint`; `npm run build`. - **기대 결과:** 모든 명령 exit 0, 일반 Audio update의 `isActive` request 0건, deactivate false 이외 parse 0건. - **수동 확인:** Audio 수정 저장과 비활성화 network multipart의 `request` part를 각각 확인한다. **Progress — 2026-07-30 P4-R4 완료** - **RED:** `npm run test:run -- src/features/audio-contents/tests/audio-contract.test.ts`에서 normal update의 `isActive:false`가 거부되지 않아 실패하는 것을 확인했다. - **GREEN:** `audioContentUpdateRequestSchema`에서 `isActive`를 제거하고 `audioContentDeactivateRequestSchema`를 `z.strictObject({ isActive: z.literal(false) })`로 분리했다. `deactivateAudioContent`는 별도 deactivate body helper를 사용하고, mock parser/store도 normal update와 deactivate 경로를 분리했다. `npm run test:run -- src/features/audio-contents/tests/audio-contract.test.ts` 결과 1 file / 10 tests passed였다. - **REFACTOR/회귀:** Audio MSW handler를 `src/shared/mocks/audio-content-handlers.ts`로 추출해 `handlers.ts`를 238 lines로 낮췄고, `npm run test:run -- src/features/audio-contents src/shared/mocks` 결과 14 files / 67 tests passed였다. `npm run e2e:mock -- tests/e2e/audio-content.spec.ts` 결과 21 passed / 3 skipped였고, skipped 3건은 기존 keyboard-only platform policy다. `npm run typecheck`, `npm run lint`, `npm run build` 모두 exit 0이었다. 변경 TS 파일 LSP diagnostics는 오류 0건이었고, `git diff --check`도 출력 없이 통과했다. ### Task R4.5 — Audio URL query 계약 키 정렬 **Goal 실행 `P4-R5`:** Audio 목록의 브라우저 검색 상태를 API 계약과 같은 `search_word` query로 읽고 쓰며 직접 링크 복원을 보장한다. - **연결 리뷰:** [Phase 4 현재 리뷰](./reviews/phase4-audio-content.md) — `REV-P4-008` - **시작 조건:** PRD §7.1과 OpenAPI Audio 목록의 `search_word`, `page`, `size` query 계약. - **완료 증거:** `?search_word=루나` 직접 진입과 검색 변경 URL assertion의 실패 test, URL parser/serializer 최소 수정, Audio focused unit·mock E2E·정적 Gate 통과. - **Files:** Modify: `src/features/audio-contents/pages/AudioContentListPage.tsx`, `src/features/audio-contents/tests/audio-list.test.tsx`, `tests/e2e/audio-content.spec.ts`. - **Interfaces:** 브라우저 URL과 API request 모두 `search_word`를 사용하고 2자 이상 검색, `page`, `size` 동작은 유지한다. - **범위 밖:** legacy `search` query 호환 layer, 검색 최소 글자 수 변경, router dependency 도입. - [x] **RED:** `?search_word=루나&page=1&size=20` 직접 진입이 input·request를 복원하고 검색 변경 URL에 `search`가 없어야 한다는 실패 test를 작성한다. - [x] **GREEN:** URL read/write key를 `search_word`로 교체한다. - [x] **REFACTOR:** Character와 query helper를 성급히 공유하지 않고 Audio page 내부 변경으로 끝낸다. - **실행 명령:** `npm run test:run -- src/features/audio-contents/tests/audio-list.test.tsx`; `npm run e2e:mock -- tests/e2e/audio-content.spec.ts --project=chromium`; `npm run typecheck`; `npm run lint`; `npm run build`. - **기대 결과:** 모든 명령 exit 0, Audio URL의 검색 key는 `search_word` 하나이고 직접 링크·검색·pagination 왕복이 일치. - **수동 확인:** 검색 결과 URL을 새로고침·새 탭 진입해 검색어와 목록이 유지되는지 확인한다. **Progress — 2026-07-31 P4-R5 완료** - **RED:** `npm run test:run -- src/features/audio-contents/tests/audio-list.test.tsx`에서 `?search_word=루나&page=1&size=20` 직접 진입 후 검색 input이 빈 값으로 남아 실패하는 것을 확인했다. - **GREEN:** `AudioContentListPage`의 URL read/write key만 `search_word`로 교체했다. `npm run test:run -- src/features/audio-contents/tests/audio-list.test.tsx` 결과 1 file / 2 tests passed였다. - **REFACTOR/회귀:** Character와 공통 query helper를 공유하지 않고 Audio page 내부 변경으로 끝냈다. `npm run test:run -- src/features/audio-contents` 첫 실행은 기존 `audio-list.test.tsx` 병렬 loading timeout 양상으로 실패했으나, 같은 명령 재실행 결과 8 files / 46 tests passed였다. 사용자 지시에 따라 mock E2E는 전체 Task 완료 전까지 보류했다. `npm run typecheck`, `npm run lint`, `npm run build`, `git diff --check`는 모두 exit 0 또는 no output이었다. 변경 TS 파일 LSP diagnostics와 Audio directory diagnostics는 오류 0건이었다. ### Task R4.6 — Audio cover crop 준비 경계 보강 **Goal 실행 `P4-R6`:** Audio 생성·수정에서 cover crop source 준비와 연속 선택을 추적하고 crop 확정 전 저장을 차단한다. - **연결 리뷰:** [Phase 4 현재 리뷰](./reviews/phase4-audio-content.md) — `REV-P4-009` - **시작 조건:** `FILE-006~009`, PRD §10.5 저장 중 feedback와 Audio cover 생성 필수/수정 optional 계약. - **완료 증거:** 준비 중 edit 저장·stale source·준비 실패를 재현하는 실패 test, staged selection 최소 수정, Audio focused unit·mock E2E·file Gate 통과. - **Files:** Modify: `src/features/audio-contents/components/AudioContentForm.tsx`, 관련 Audio form/file tests와 `tests/e2e/audio-content.spec.ts`. - **Interfaces:** `coverImage`는 crop 적용 결과만 보유하고 준비/crop 중 submit을 비활성화·guard한다. 마지막 선택만 유효하며 취소는 기존 서버 cover를 유지하고 준비 실패를 inline 표시한다. - **범위 밖:** 대용량 audio upload 상태 변경, 공통 crop 계산 변경, 범용 upload state library. - [x] **RED:** edit에서 crop source 준비 중 저장, 느린 첫 선택의 늦은 dialog, 준비 reject 후 무반응을 실패 test로 고정한다. - [x] **GREEN:** cover selection token·준비 상태·오류 처리와 submit guard를 추가한다. - [x] **REFACTOR:** 기존 `UploadState`와 cover 준비 상태의 역할을 분리하고 필요한 표시 문구만 Audio form에 둔다. - **실행 명령:** `npm run test:run -- src/features/audio-contents`; `npm run e2e:mock -- tests/e2e/audio-content.spec.ts --project=chromium`; `npm run typecheck`; `npm run lint`; `npm run build`. - **기대 결과:** 모든 명령 exit 0, 준비/crop 미확정 cover의 저장 0건, stale dialog 0건, 정상 crop result와 기존 cover 유지 경계 일치. - **수동 확인:** 기존 cover가 있는 수정 화면에서 느린 파일 두 개를 연속 선택하고 취소·적용·즉시 저장해 마지막 의도가 보존되는지 확인한다. **Progress — 2026-07-31 P4-R6 완료** - **RED:** `npm run test:run -- src/features/audio-contents/tests/audio-form-upload.test.tsx src/features/audio-contents/tests/audio-form-update.test.tsx`에서 준비 중 저장 guard, stale selection, reject 오류 경계가 실패하는 것을 확인했다. - **GREEN:** `AudioContentForm`에 cover selection token, `isCoverPreparing`, crop dialog pending guard, reject inline 오류를 추가했다. `coverImage`는 crop 적용 결과만 commit하고, 취소는 수정 화면의 기존 서버 cover 유지 계약을 보존한다. focused 재실행 결과 2 files / 18 tests passed였다. - **REFACTOR/회귀:** `FieldErrors` 타입을 `Partial>`로 줄여 `AudioContentForm.tsx` 순수 LOC를 247로 낮췄다. `npm run test:run -- src/features/audio-contents` 결과 8 files / 53 tests passed였다. 사용자 지시에 따라 mock E2E는 전체 Task 완료 전까지 보류했다. `npm run typecheck`, `npm run lint`, `npm run build`, `git diff --check`는 모두 exit 0 또는 no output이었다. Audio directory LSP diagnostics는 16 TSX files / 오류 0건이었다. ### Task R4.7 — Audio cover 원본 정책 사전 검증 복구 **Goal 실행 `P4-R7`:** Audio 생성·수정에서 새 cover 원본의 확장자·MIME pair와 exact byte 상한을 crop source 생성 전에 검증한다. - **연결 리뷰:** [Phase 4 현재 리뷰](./reviews/phase4-audio-content.md) — `REV-P4-010` - **시작 조건:** `FILE-001~003`, `FILE-006~007`, PRD `Image crop UI 흐름` 1~3단계와 기존 `validateAudioCoverFile` 정책. - **완료 증거:** `10,485,761 bytes`, GIF, 확장자/MIME 불일치 cover가 `createCropSource`를 호출하거나 crop Dialog를 열지 않는 실패 test, 정상 JPEG/PNG crop 회귀 test, Phase 4 focused·mock E2E·정적 Gate 통과. - **Files:** Modify: `src/features/audio-contents/components/AudioContentForm.tsx`, 관련 Audio form tests와 필요 시 `tests/e2e/audio-content.spec.ts`. - **Interfaces:** 새 cover는 `validateAudioCoverFile` 성공 뒤에만 `createCropSource`로 전달한다. 실패 시 기존 `coverErrorMessage` 계열 inline 오류를 표시하고 create/edit 모두 기존 committed cover 상태를 보존한다. - **범위 밖:** crop 계산·출력 크기 변경, audio 파일 upload 정책 변경, 새 공통 validation abstraction, server validation 변경. - **Phase 4 Gate 연결:** 기존 완료 기록은 보존하되 `P4-R7` focused test와 mock Chromium Audio 시나리오, `typecheck`·`lint`·`build` 통과 전에는 이번 리뷰 회귀를 종결하지 않는다. - **검증 기록:** RED/GREEN 명령·test 수·exit code와 수동 확인 결과를 이 Task 하단과 Phase 4 리뷰 §9 이후에 날짜순으로 누적한다. - [x] **RED:** create/edit form에서 oversized PNG와 `.png`/`image/jpeg` 불일치 파일을 선택해 `createCropSource` 0회, crop Dialog 0개, 정책 오류 표시를 기대하는 실패 test를 작성한다. - [x] **GREEN:** `selectCoverImage`의 crop 준비 전에 기존 Audio cover validator를 호출하고 실패 상태만 최소 처리한다. - [x] **REFACTOR:** 정책 값과 메시지를 복제하지 않고 기존 validator·오류 변환을 재사용하며 Series/Character flow를 새 abstraction으로 묶지 않는다. - **실행 명령:** `npm run test:run -- src/features/audio-contents`; `npm run e2e:mock -- tests/e2e/audio-content.spec.ts --project=chromium`; `npm run typecheck`; `npm run lint`; `npm run build`; `git diff --check`. - **기대 결과:** 모든 명령 exit 0, invalid 원본의 crop source 호출·Dialog 노출·upload request 0건, valid JPEG/PNG의 기존 crop·create/edit 흐름 유지. - **수동 확인:** 생성·수정 화면에서 oversized/mismatched/GIF/valid PNG를 차례로 선택해 inline 오류, 기존 cover 유지, 정상 crop 적용을 확인한다. **Progress — 2026-07-31 P4-R7 완료** - **RED:** `npm run test:run -- src/features/audio-contents/tests/audio-form-upload.test.tsx src/features/audio-contents/tests/audio-form-update.test.tsx`는 2 files 중 2 tests failed / 18 passed였다. oversized PNG와 `.png`/`image/jpeg` replacement cover 모두 정책 오류가 없고 crop Dialog로 진입해 의도한 실패를 확인했다. - **GREEN:** `AudioContentForm.selectCoverImage`가 `coverErrorMessage`를 crop source 준비 전에 재사용하게 수정했다. 같은 focused command는 2 files / 20 tests passed였고, invalid 원본에서 `createCropSource` 0회·crop Dialog 0개·inline 오류 표시를 확인했다. - **REFACTOR/회귀:** 새 공통 abstraction 없이 기존 validator·오류 변환만 재사용했다. `AudioContentForm.tsx` 순수 LOC는 249로 250 이하이며, `npm run typecheck`, `npm run lint`, `npm run build`는 exit 0이었다. `src/features/audio-contents` LSP diagnostics는 16 TSX files / 오류 0건이었다. 개발 중 E2E는 사용자 지시에 따라 보류했다. - **참고:** `npm run test:run -- src/features/audio-contents`와 단독 `npm run test:run -- src/features/audio-contents/tests/audio-list.test.tsx`는 `Audio list restores and serializes search_word only...`가 `오디오 콘텐츠` heading을 찾지 못하고 loading 상태에서 실패했다. 실패 경로는 Audio 목록 조회 test이며 이번 `P4-R7`의 cover 선택/form 변경 경로와 분리된다. ### Task R4.8 — Audio upload 종료 처리의 단일 정산 보장 **Goal 실행 `P4-R8`:** Audio XHR upload의 성공·HTTP 실패·network 실패·취소가 경합해도 요청별 종료 처리를 한 번만 수행하고, 같은 보호 요청 burst의 401 만료 처리를 한 번만 유지한다. - **연결 리뷰:** [Phase 4 현재 리뷰](./reviews/phase4-audio-content.md) — `REV-P4-011` - **시작 조건:** `P1-R12` 완료, PRD `AUDIO-017`, `AUTH-005`, §13의 동시 401 단일 처리, 기존 완료 `P4-R2`의 upload 401 burst contract. - **완료 증거:** signal이 연결된 upload의 network error 뒤 abort가 발화한 다음 첫 session과 새 로그인 session의 protected upload가 각각 401을 반환하는 실패 test, 요청별 settle/cleanup 최소 수정, Audio focused·Phase 4 Gate와 검증 기록. - **Files:** Modify: `src/features/audio-contents/api/upload-audio-content.ts`, `src/features/audio-contents/tests/audio-upload.test.ts`; Create: `src/features/audio-contents/tests/audio-upload-auth-lifecycle.test.ts`. - **Interfaces:** `uploadAudioContent`는 각 XHR의 resolve/reject, 보호 upload count 감소와 `AbortSignal` listener 해제를 terminal event 종류와 무관하게 정확히 한 번 수행한다. 같은 protected upload burst의 `clearSession`/`onAuthExpired`는 최대 한 번 호출한다. - **범위 밖:** fetch 기반 upload 전환, resumable upload, 새 상태 관리 library, 공통 API client 구조 변경, backend idempotency 변경. - **Phase 4 Gate 연결:** focused RED/GREEN 뒤 Audio 전체 unit, mock Chromium Audio 시나리오, `typecheck`·`lint`·build·diff 검증이 통과하기 전에는 완료 처리하지 않는다. - **검증 기록:** RED의 실제 callback 횟수, GREEN/REFACTOR test 수와 Gate 결과를 이 Task 하단과 Phase 4 리뷰에 날짜순으로 누적한다. - [x] **RED:** `audio-upload.test.ts`에 network error로 먼저 reject된 signal-bearing upload를 뒤늦게 abort한 다음 첫 session과 새 로그인 session의 401을 차례로 발생시켜 `clearSession`/`onAuthExpired`가 session별 1회, 총 2회여야 한다는 test를 추가하고 현재 총 1회만 호출되는 실패를 확인했다. - [x] **GREEN:** 요청별 settled guard와 공통 terminal cleanup을 최소 적용해 count 감소·listener 해제·Promise settle을 한 번만 수행했다. - [x] **REFACTOR:** `onload`, `onerror`, abort가 같은 정산 경계를 사용하도록 중복만 정리하고 기존 success/progress/415/malformed response/cancel retry contract를 유지했다. - **실행 명령:** `npm run test:run -- src/features/audio-contents/tests/audio-upload.test.ts src/features/audio-contents/tests/audio-upload-auth-lifecycle.test.ts`; `npm run test:run -- src/features/audio-contents`; `npm run e2e:mock -- tests/e2e/audio-content.spec.ts --project=chromium`; `npm run typecheck`; `npm run lint`; `npm run build:dev`; `npm run build:prod`; `git diff --check`. - **기대 결과:** 모든 명령 exit 0, 신규 lifecycle test에서 두 session의 401 callback 각각 1회, 기존 upload progress·cancel·retry·오류 mapping 회귀 0건. - **수동 확인:** 느린 network에서 upload 오류와 취소를 인접하게 발생시킨 뒤 재시도하고, 이후 401 응답에서 로그인 이동·만료 안내가 중복되지 않는지 확인한다. **P4-R8 수정 검증 기록 (2026-07-31):** - RED: `npm run test:run -- src/features/audio-contents/tests/audio-upload.test.ts` — 1 failed / 11 passed. 신규 lifecycle test에서 network error 후 late abort가 다음 session 401을 삼켜 `clearSession` 기대 2회 대비 실제 1회였다. - GREEN: `uploadAudioContent`에 요청별 `isSettled` guard와 공통 terminal cleanup을 추가해 resolve/reject/count 감소/AbortSignal listener 제거가 terminal 종류와 무관하게 한 번만 실행되도록 했다. 같은 focused 명령은 1 file / 12 tests passed였다. - REFACTOR/회귀: 기존 success/progress/415/malformed response/cancel retry contract를 유지했다. reviewer blocker로 fetch-first 순서 회귀를 추가한 뒤 `auth` 주입 시 storage fallback을 쓰지 않도록 보완했다. `npm run test:run -- src/shared/api/__tests__/client-auth.test.ts src/features/audio-contents/tests/audio-upload.test.ts src/features/audio-contents/tests/audio-upload-auth-lifecycle.test.ts` — 3 files / 22 tests passed. `npm run test:run -- src/features/audio-contents` — 9 files / 58 tests passed. `npm run test:run -- src/app src/features/auth src/shared/api src/features/audio-contents/tests/audio-upload.test.ts` — 13 files / 91 tests passed. `npm run test:run` — 79 files / 397 tests passed. `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod`, targeted `git diff --check` — 모두 exit 0. build는 기존 503.04kB chunk warning만 표시했다. LSP diagnostics는 변경 파일·디렉터리 기준 오류 0건이었다. - E2E/수동 확인: 사용자 지시에 따라 개발 중 E2E는 반복 실행하지 않고 모든 Task 구현 뒤 필요 시 수행한다. ### Task R4.9 — 공통 오디오 플레이어 compact UI **Goal 실행 `P4-R9`:** 공통 `AdminAudioPlayer`를 image·video 영역 없는 compact audio-only UI로 개선해 오디오 콘텐츠 목록·상세와 커뮤니티 목록·Sheet에 동일하게 적용한다. - **Reference packet:** `.omo/evidence/p4-r9-reference/plyr-audio-{375,768,1280}.png`, `.omo/evidence/p4-r9-reference/media-chrome-audio-{375,768,1280}.png`. Plyr의 밝은 52px compact bar를 primary surface로, Media Chrome의 시간·배속·음량 control anatomy를 secondary structure로 사용한다. - **시작 조건:** PRD `AUDIO-021`, `AUDIO-026`, `AUDIO-034`와 기존 `AdminAudioPlayer`·`AudioPlaybackProvider` 단일 재생 계약. - **완료 증거:** 아래 RED/GREEN/REFACTOR 체크박스, focused·전체 unit·mock E2E·typecheck·lint·build 통과, Chromium/mobile Chrome 실제 화면 screenshot과 visual QA 판정, QA resource teardown 기록. - **범위 밖:** 새 audio player dependency, waveform, playlist, download control, cover/poster/video viewport, API·DTO·signed URL lifecycle 변경. **Files:** - Modify: `src/shared/ui/admin-audio-player.tsx` - Modify: `src/shared/ui/__tests__/admin-audio-player.test.tsx` - Verify unchanged: `src/features/audio-contents/components/AudioContentList.tsx` - Verify unchanged: `src/features/audio-contents/components/AudioContentListItem.tsx` - Verify unchanged: `src/features/audio-contents/pages/AudioContentDetailPage.tsx` - Verify unchanged: `src/features/community-posts/components/CommunityPostList.tsx` - Verify unchanged: `src/features/community-posts/components/CommunityPostListItem.tsx` - Verify unchanged: `src/features/community-posts/components/CommunityPostSheet.tsx` - Test: `src/features/audio-contents/tests/audio-list.test.tsx` - Test: `src/features/audio-contents/tests/audio-player.test.tsx` - Test: `src/features/community-posts/tests/community-list.test.tsx` - Test: `src/features/community-posts/tests/community-sheet.test.tsx` - E2E: `tests/e2e/audio-content.spec.ts` - E2E: `tests/e2e/community.spec.ts` **Interfaces:** - Consumes: `AdminAudioPlayerProps { playerId, src, title }`, `useAudioPlayback`, native `HTMLAudioElement` events. - Produces: 기존 props와 재생 동작을 바꾸지 않는 compact audio-only control; `재생 진행`과 `재생 설정` accessible group. **시나리오 계약:** | 시나리오 | 이진 통과 조건 | 실제 surface | 자동 test | |---|---|---|---| | Happy path | 오디오 목록에서 재생 버튼을 누르면 버튼 이름이 `일시정지`로 바뀌고, 상시 설명 label 없는 재생·진행 slider·현재/전체 시간·배속·음량 control이 표준 viewport에서 Plyr/Media Chrome처럼 한 줄에 표시된다. | mock Chromium `/ai-characters/101/audio-contents`에서 `page.click({ name: "재생" })` 후 reference-fidelity screenshot | `src/shared/ui/__tests__/admin-audio-player.test.tsx`의 `AdminAudioPlayer matches the compact reference control anatomy` | | Edge | 320px viewport와 200% zoom에서 player의 `scrollWidth <= clientWidth`이고 모든 control이 viewport 안에 있다. | mock mobile Chrome 목록·상세 screenshot과 overflow probe | `tests/e2e/audio-content.spec.ts`의 기존 320px·200% zoom player 시나리오 | | Adjacent regression | 커뮤니티 목록과 Sheet가 같은 player 구조를 사용하고 두 player 중 새 항목 재생 시 기존 항목이 pause되며 오류·수동 재시도 계약이 유지된다. | mock Chromium `/ai-characters/101/community-posts` 목록·Sheet 재생과 screenshot | `src/shared/ui/__tests__/admin-audio-player.test.tsx`, `src/features/community-posts/tests/community-sheet.test.tsx`, `tests/e2e/community.spec.ts` | **TDD 절차:** - [x] **RED: 실패 테스트 작성/실패 확인** — `src/shared/ui/__tests__/admin-audio-player.test.tsx`에 다음 reference-anatomy test를 작성한다. production code를 수정하기 전에 mock Chromium의 오디오 콘텐츠 목록·상세와 커뮤니티 목록·Sheet 현재 화면을 reference screenshot으로 저장한다. ```tsx test("AdminAudioPlayer matches the compact reference control anatomy", () => { render(); const player = screen.getByRole("group", { name: "샘플 오디오 오디오 플레이어" }); const controls = within(player).getByRole("group", { name: "재생 제어" }); expect(within(controls).getByRole("button", { name: "재생" })).toBeInTheDocument(); expect(within(controls).getByRole("slider", { name: "재생 위치" })).toBeInTheDocument(); expect(within(controls).getByRole("combobox", { name: "재생 속도" })).toBeInTheDocument(); expect(within(controls).getByRole("slider", { name: "볼륨" })).toBeInTheDocument(); expect(within(player).queryByText("볼륨")).not.toBeInTheDocument(); expect(within(player).queryByText("재생 속도")).not.toBeInTheDocument(); expect(player.querySelector("img")).not.toBeInTheDocument(); expect(player.querySelector("video")).not.toBeInTheDocument(); }); ``` `npm run test:run -- src/shared/ui/__tests__/admin-audio-player.test.tsx -t "compact reference control anatomy"` 실행 시 `재생 제어` group을 찾지 못해 실패해야 한다. - [x] **GREEN: 최소 구현/통과 확인** — `src/shared/ui/admin-audio-player.tsx`의 기존 state·handler·native `