diff --git a/docs/20260725_AI캐릭터관리자웹/api-contract.md b/docs/20260725_AI캐릭터관리자웹/api-contract.md index 3b5cc0b..7925da9 100644 --- a/docs/20260725_AI캐릭터관리자웹/api-contract.md +++ b/docs/20260725_AI캐릭터관리자웹/api-contract.md @@ -12,6 +12,8 @@ 이 문서는 대화로 제공된 API Contract를 저장소에 영속적으로 보존하고 인터뷰에서 확정된 보정사항을 적용한 프론트엔드 기준 계약이다. 실제 백엔드 구현을 다른 저장소에서 추정하지 않는다. “백엔드 제공 대기”로 표시한 endpoint와 세부 규칙은 프론트엔드 Open Question이 아니며, 백엔드 계약이 제공되기 전에는 해당 network integration을 구현하지 않는다. +goal 실행 시 계약 입력은 해당 Task의 `시작 조건`이 가리키는 이 문서 section을 사용한다. 제공 대기 계약을 추정해 goal을 완료하지 않으며, 계약이 새로 제공되거나 제외 결정이 나면 PRD 결정 기록 → 이 문서 → `plan-task.md` 순서로 갱신한다. + ## 1. 공통 규칙 ### 1.1 성공 응답 @@ -114,7 +116,7 @@ type SeriesPublishedDay = ### 2.1 이미지 업로드와 crop -모든 image part의 원본 파일 크기는 최대 10MB다. 확장자 문자열만 신뢰하지 않고 실제 MIME을 함께 검증한다. +모든 image part의 원본 파일 크기는 최대 `10,485,760 bytes`다. `10,485,760 bytes`는 허용하고 `10,485,761 bytes`부터 거부한다. 확장자 문자열만 신뢰하지 않고 실제 MIME을 함께 검증한다. | resource | 허용 확장자 | 허용 MIME | crop aspect ratio | 등록 결과 최대 가로 폭 | 결과 세로 | |---|---|---|---:|---:|---| @@ -333,6 +335,28 @@ Content-Type: `multipart/form-data` ## 5. 오디오 콘텐츠 +### 5.0 오디오 콘텐츠 테마 목록 + +`GET /api/v2/admin/ai-characters/audio-content-themes` + +- Query 없음 +- Request body 없음 + +Response `data`: + +```json +[ + { + "themeId": 11, + "themeName": "ASMR", + "imageUrl": "https://cdn.example.com/audio-content-theme/asmr.png" + } +] +``` + +- 오디오 콘텐츠 생성 UI는 이 목록에서 테마를 선택하게 하고, 선택한 `themeId`를 생성 request JSON에 포함한다. +- 테마 목록은 선택 캐릭터 path를 포함하지 않는 공통 관리자 조회다. + ### 5.1 목록·검색 `GET /api/v2/admin/ai-characters/{characterId}/audio-contents?search=밤&status=OPEN&page=0&size=20` @@ -418,6 +442,7 @@ Content-Type: `multipart/form-data` { title: string description: string + themeId: number price: number isAdult: boolean releaseDateUtc: string | null @@ -426,6 +451,7 @@ Content-Type: `multipart/form-data` ``` - `isActive`와 `status`를 보내지 않는다. +- `themeId`는 필수이며 `GET /api/v2/admin/ai-characters/audio-content-themes` 응답에서 선택한 값이다. - 즉시 공개는 `releaseDateUtc=null`이다. - 예약 공개는 Asia/Seoul 미래 시각을 UTC `Z` 문자열로 변환해 보낸다. - Response `data`는 오디오 상세와 같다. diff --git a/docs/20260725_AI캐릭터관리자웹/plan-task.md b/docs/20260725_AI캐릭터관리자웹/plan-task.md index 0952173..0351f3b 100644 --- a/docs/20260725_AI캐릭터관리자웹/plan-task.md +++ b/docs/20260725_AI캐릭터관리자웹/plan-task.md @@ -10,7 +10,7 @@ | 문서 항목 | 내용 | |---|---| -| 상태 | 구현 전 재계획 완료 | +| 상태 | Phase 0~1 완료, Phase 2 착수 전 | | 최초 작성일 | 2026-07-25 | | 재작성일 | 2026-07-26 | | 요구사항 기준 | [prd.md](./prd.md) | @@ -100,6 +100,26 @@ import 오류, test 환경 오류, 임시 mock 누락 같은 우발적 실패는 - P0 외부 의존이 남아 있으면 영향을 받는 network flow를 완료로 표시하지 않는다. 다른 독립 Phase는 계속 진행할 수 있다. - 이미지 최대 `10MB`의 정확한 byte 경계처럼 표현만으로 단일 값이 정해지지 않는 항목은 첫 파일 Phase에서 결정 기록과 contract를 먼저 보정한다. +### 2.4 Goal 기능 운영 규칙 + +- `create_goal`에는 동시에 하나의 미완료 goal만 등록한다. Phase 전체가 아니라 아래에 `Goal 실행`으로 표시한 Task 하나를 기본 단위로 사용한다. +- goal objective는 해당 Task의 `Goal 실행`, `시작 조건`, `완료 증거`, `범위 밖`을 함께 복사해 등록한다. 사용자가 명시적으로 요청하지 않으면 token budget을 설정하지 않는다. +- 활성 goal이 있으면 새 goal을 만들지 않고 같은 Task를 이어서 수행한다. 체크박스 일부만 끝났거나 검증·기록이 남았으면 goal을 완료 처리하지 않는다. +- `완료 증거`와 해당 Task의 체크박스를 모두 충족하고 `plan-task.md` 검증 기록까지 누적한 뒤에만 goal을 `complete`로 갱신한다. +- 외부 계약이나 권한 같은 동일 차단 사유가 최초 시도와 자동 후속을 포함해 3회 연속 반복되고, 문서화·독립 작업 등 의미 있는 진전도 불가능할 때만 goal을 `blocked`로 갱신한다. 그 전에는 가능한 범위를 계속 수행한다. +- 계약 미제공으로 기능을 제외할 때는 PRD 결정 기록 → `api-contract.md` → 이 계획의 활성 checklist 순서로 갱신해야 한다. 이 문서화가 끝나기 전에는 goal을 완료 처리하지 않는다. +- 각 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 | 결과 | 선행조건 | 독립 검증 핵심 | @@ -161,6 +181,12 @@ feature 내부의 `api/`, `components/`, `model/`, `pages/`, `schemas/`, `tests/ **목표:** 비즈니스 기능 없이도 동일한 명령으로 개발·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` @@ -176,6 +202,12 @@ feature 내부의 `api/`, `components/`, `model/`, `pages/`, `schemas/`, `tests/ ### 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 같은 비밀값을 넣지 않는다. @@ -186,6 +218,12 @@ feature 내부의 `api/`, `components/`, `model/`, `pages/`, `schemas/`, `tests/ ### Task 0.2 test 기반 +**Goal 실행 `P0-T2`:** unit·DOM·E2E test 기반과 접근 가능한 최소 App shell을 완성한다. + +- **시작 조건:** `P0-T1` 완료. +- **완료 증거:** 체크박스 전체, root unit test와 4개 Playwright project smoke 및 검증 기록. +- **범위 밖:** 인증과 실제 도메인 route. + - [x] Vitest, jsdom, React Testing Library, jest-dom을 구성한다. - [x] 각 test 뒤 DOM·mock·storage가 정리되는 공통 setup을 만든다. - [x] Playwright에 desktop Chromium/WebKit과 mobile Chrome/Safari viewport project, Vite webServer를 구성한다. @@ -195,6 +233,12 @@ feature 내부의 `api/`, `components/`, `model/`, `pages/`, `schemas/`, `tests/ ### Phase 0 Gate +**Goal 실행 `P0-GATE`:** Phase 0의 fresh install·unit·E2E·build 재현성을 최종 판정한다. + +- **시작 조건:** `P0-T1`, `P0-T2` 완료. +- **완료 증거:** 아래 명령 모두 0 exit code, Expected와 실제 결과의 검증 기록. +- **범위 밖:** 실패와 무관한 기능 추가. 실패는 소유 Task 수정 goal로 되돌린다. + ```bash npm ci npx playwright install chromium webkit @@ -218,16 +262,43 @@ npm run build:prod **2차 설정 정정 기록 (2026-07-26):** - 무엇을: 개발 서버 포트를 `8888`로 고정하고, 개발/프로덕션 API base URL과 build 명령을 분리했다. -- 왜: 실행 환경별 API endpoint를 명확히 분리하고, AGENTS.md를 짧게 유지하면서 상세 지침을 `docs/agent-guide/README.md`에서 관리하기 위해서다. +- 왜: 실행 환경별 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 증거 정합성을 복구하고 회귀를 방지한다. + +- 연결 리뷰: `REV-P0-001`, `REV-P0-002`, `REV-P0-003` +- 시작 조건: review finding을 보존하고 기존 완료 체크를 되돌리지 않는다. +- 완료 증거: stubbed global 복원 회귀 test, root `main`과 대표 content assertion, 현재 Playwright mode URL 전략과 실행 가능한 agent-guide 문서 경로 정정 기록, P0-GATE 통과. +- 범위 밖: Phase 1 인증 동작 전면 변경, test framework 교체. + +- [x] `src/shared/test/setup-isolation.test.ts`로 `vi.stubGlobal` 누출을 실패 재현한 뒤 `restoreAllMocks`·`unstubAllGlobals` 정리를 추가했다. +- [x] `src/app/App.test.tsx`와 `tests/e2e/smoke.spec.ts`에서 root `main` 안의 로그인 대표 콘텐츠를 검증한다. +- [x] 존재하지 않는 `docs/agent-guide/README.md` 참조를 실제 `docs/agent-guide/` 세부 문서 관리 방식으로 정정했다. +- [x] P0-GATE를 재실행하고 결과를 아래에 누적했다. + +**P0-R1 수정 검증 기록 (2026-07-27):** + +- RED: `npm run test:run -- src/app/App.test.tsx src/features/auth/tests/login-page.test.tsx src/features/auth/tests/auth-session.test.tsx src/shared/lib/crop-image.test.ts src/shared/validation/file-media-policy.test.ts src/shared/ui/__tests__/file-field.test.tsx src/shared/ui/__tests__/admin-audio-player.test.tsx src/shared/ui/__tests__/confirm-deactivate-dialog.test.tsx src/shared/ui/__tests__/unsaved-changes-guard.test.tsx src/shared/ui/__tests__/image-crop-dialog.test.tsx src/shared/test/setup-isolation.test.ts` — 기대 실패 확인, 11 files failed / 13 failed / 40 passed. P0-R1 관련으로 stubbed global 복원과 root shell assertion 실패를 확인했다. +- GREEN focused: `npm run test:run -- src/app/App.test.tsx src/shared/config/env.test.ts src/shared/test/setup-isolation.test.ts` — 성공, 3 files / 18 tests passed. +- P0-GATE: `npm ci` — 성공, 307 packages, 0 vulnerabilities. `npx playwright install chromium webkit` — 성공. `npm run typecheck` — 성공. `npm run lint` — 성공. `npm run test:run -- src/app/App.test.tsx src/shared/config/env.test.ts src/shared/test/setup-isolation.test.ts` — 성공, 18 tests passed. `npm run e2e -- tests/e2e/smoke.spec.ts` — 성공, 4 projects / 4 tests passed. `npm run build:dev` — 성공. `npm run build:prod` — 성공. +- 참고: 첫 Gate 병렬 실행 중 `npm ci`가 `node_modules`를 재설치하는 동안 동시에 실행한 `tsc/playwright` 명령이 bin 삭제로 실패했다. `npm ci` 완료 후 같은 명령을 재실행해 모두 성공했다. + --- ## Phase 1. 공통 플랫폼·인증/인가·컴포넌트 기반 **목표:** ADMIN 인증 흐름과 보호된 Admin shell을 완결하고, Phase 2 이후 화면이 조합해 사용할 공통 UI·form·file·media component contract를 제공한다. +**Phase Goal `P1`:** Task 1.1 → 1.6과 Phase 1 Gate를 완료해 도메인 Phase가 사용할 공통 contract를 제공한다. + +- **시작 조건:** `P0-GATE` 완료. +- **완료 조건:** `P1-T1`~`P1-T6`, `P1-GATE` 완료와 auth/shared contract 검증 기록. +- **현재 상태:** 완료. 회귀나 기록 정합성 수정은 별도 수정 goal로 수행한다. + **요구사항:** `AUTH-001~013`, `UX-001~002`, PRD `7`, `10.1~10.8`, `11.1~11.2`, `12`의 공통 항목. **주요 Files:** @@ -238,7 +309,7 @@ npm run build:prod - 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,page-header,search-toolbar,resource-pagination,responsive-resource-list}.tsx` +- 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` @@ -262,7 +333,7 @@ npm run build:prod | Shared component | 확정 소비 Phase | 공통 책임 | |---|---|---| -| `PageHeader`, `PageState` | 1~7 | 제목·action slot, loading·empty·error·retry | +| `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 | @@ -273,86 +344,128 @@ npm run build:prod | `UploadProgress` | 3, 5 | 진행률·취소·재시도 callback | | `AdminAudioPlayer`, `AudioPlaybackProvider` | 3, 5 | native audio control과 단일 재생 상태 | -이 표는 PRD에 이미 확정된 반복 소비만 포함한다. 각 도메인 Phase의 component map은 이 배치를 검증하고 domain component 구성을 추가하며, shared component의 존재 근거를 새로 만드는 선행조건이 아니다. +이 표는 PRD에 이미 확정된 반복 소비만 포함한다. `PageHeader`는 실제 구현·소비가 없고 현재 필요한 반복 소비 컴포넌트가 아니므로 만들지 않는다. 각 도메인 Phase에서 필요하다고 확인될 때 추가한다. ### Task 1.1 최소 디자인 시스템 -- [ ] PRD `10.9`의 `ui-ux-pro-max` design-system 검색을 실행하고 관리자 제품에 맞는 채택·제외 결과를 작업 기록에 남긴다. -- [ ] Tailwind CSS와 shadcn/ui CSS variable mode를 구성한다. -- [ ] `brand-500=#00BDF7`, hover `#00A9DE`, active `#009DCE`, primary foreground `#062B36`, link/ring `#007EA8`을 primitive → semantic → component token으로 연결한다. -- [ ] 핵심 foreground 대비, control boundary 3:1, primary 위 흰색 금지를 token test로 고정한다. -- [ ] 밝은 `:root` token만 만들고 `.dark`, ThemeProvider, theme toggle, system dark 연동이 없음을 test한다. -- [ ] Korean system font stack, mobile input 16px, 44px target, focus ring, reduced motion, semantic z-index를 base style에 둔다. -- [ ] 상태 Badge가 text label을 포함하고 색상만으로 상태를 전달하지 않는 test를 작성한다. -- [ ] icon-only action에는 accessible name과 Tooltip이 있고 필수 control boundary·focus indicator가 인접 배경 대비 3:1 이상인지 test한다. -- [ ] Auth와 공통 상태에 실제 필요한 shadcn component만 추가한다. 전체 component를 선행 scaffold하지 않는다. +**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 -- [ ] `AuthSessionRecord = { token: string; role: "ADMIN" }`의 읽기·저장·제거 adapter를 먼저 만들고, API client는 React provider가 아니라 이 adapter의 token reader/clear callback에만 의존한다. -- [ ] `ApiResponse` 성공형이 `errorProperty` 생략과 `null`을 모두 수용하고 오류형은 비2xx status·`message`·`errorProperty`를 보존하는 test를 작성한다. -- [ ] `PageData`와 `page=0`, `size=20`, size 최소 20·최대 50 보정을 test한다. 문서에 없는 음수 page 동작은 추정하지 않는다. -- [ ] 모든 요청에 `Accept-Language: ko`를 붙이고 로그인 요청에는 Authorization을 제외하는 test를 작성한다. -- [ ] 보호 요청과 logout에만 현재 session의 Bearer token을 붙이는 test를 작성한다. -- [ ] 400/404/405/415/500의 서버 한국어 message를 공통 `ApiError`가 보존하는 MSW test를 작성한다. -- [ ] 동시에 여러 401이 와도 session clear·알림·login redirect가 한 번만 발생하는 test를 작성한다. -- [ ] 403은 session을 지우지 않고 AccessDenied 상태로 전달하는 test를 작성한다. -- [ ] logger가 JWT, password, signed URL, multipart body를 받지 않는 test를 작성한다. -- [ ] TanStack Query provider와 공통 retry 정책을 구성하되 401/403과 mutation을 무조건 재시도하지 않는다. +**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 -- [ ] email 형식, password 필수, visible label, 오류 연결, first-invalid-focus test를 작성한다. -- [ ] login이 `POST /admin/member/login`에 `{ email, password }` JSON만 보내는 contract test를 작성한다. -- [ ] 응답의 `data.token`과 `data.role="ADMIN"`만 유효 session으로 인정한다. -- [ ] 성공 session을 `sessionStorage`에만 저장하고 같은 탭 새로고침에서 복원하는 test를 작성한다. -- [ ] token 누락, role 누락·비ADMIN이면 보호 route를 렌더링하지 않고 저장도 하지 않는 test를 작성한다. -- [ ] `localStorage`, IndexedDB, cookie에 인증 정보가 기록되지 않는 test를 작성한다. -- [ ] refresh endpoint 호출이 0건임을 확인한다. -- [ ] logout이 Bearer header와 body 없이 `POST /member/logout`을 한 번 호출하는 test를 작성한다. -- [ ] logout 성공·비2xx·network error 모두 local session을 제거하고 `/login`으로 이동하며, 실패 때만 서버 확인 실패 경고를 표시하고 session을 복원하지 않는 test를 작성한다. +**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 -- [ ] 미인증 사용자가 보호 content를 한 프레임도 보지 않고 `/login`으로 이동하는 test를 작성한다. -- [ ] 401은 session 제거 후 login, 403은 AccessDeniedPage로 가는 route test를 작성한다. -- [ ] desktop sidebar, mobile Sheet, header, logout, breadcrumb, skip link, `main` landmark를 구현한다. -- [ ] keyboard로 login, navigation, logout을 완료하고 dialog/menu focus가 trigger로 복귀하는 test를 작성한다. -- [ ] `/ai-characters`에는 Phase 2가 교체할 명시적 빈 route state만 두고 가짜 도메인 데이터를 만들지 않는다. -- [ ] 320px과 200% zoom에서 shell overflow와 가려진 control이 없는지 E2E로 확인한다. -- [ ] shell route의 axe critical·serious 위반 0건을 확인한다. +**Goal 실행 `P1-T4`:** 인증·권한 route 처리와 반응형·접근 가능한 Admin shell을 완성한다. + +- **시작 조건:** `P1-T3` 완료. +- **완료 증거:** 체크박스 전체, route/shell test, auth·accessibility E2E와 viewport/axe 기록. +- **범위 밖:** Phase 2 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 2가 교체할 명시적 빈 route state만 두고 가짜 도메인 데이터를 만들지 않는다. +- [x] 320px과 200% zoom에서 shell overflow와 가려진 control이 없는지 E2E로 확인한다. +- [x] shell route의 axe critical·serious 위반 0건을 확인한다. ### Task 1.5 공통 화면·form component -- [ ] 위 소비처 Matrix를 contract test와 component API에 대조하고 모든 shared component가 두 Phase 이상에서 같은 의미로 사용되는지 확인한다. -- [ ] `PageState`가 loading·empty·error·retry를 접근 가능한 status/alert와 keyboard action으로 표현하는 test를 작성한다. -- [ ] `StatusBadge`가 domain label·icon/보조 문구를 slot으로 받고 색상만으로 상태를 전달하지 않는 test를 작성한다. -- [ ] `SearchToolbar`는 controlled search/filter slot과 약 300ms debounce·URL query callback만 제공하고 특정 endpoint query를 알지 않게 한다. -- [ ] `ResourcePagination`은 공통 `PageData`로 page/size를 제어하고 disabled·accessible name·keyboard 동작을 제공한다. -- [ ] `ResponsiveResourceList`는 desktop/mobile rendering slot만 제공하고 domain column·DTO·action을 prop union으로 내장하지 않는다. -- [ ] `ConfirmDeactivateDialog`는 대상명·영향 설명·확인 callback을 조합하고 Switch로 대체되지 않게 test한다. -- [ ] `UnsavedChangesGuard`는 dirty 상태에서만 route 이탈을 확인하고 저장 성공 후 해제되며 focus를 trigger로 복귀한다. -- [ ] UTC 시각의 Asia/Seoul 표시와 0 이상 정수 “캔” 표시를 공통 formatter로 고정하고 domain status label은 formatter에 넣지 않는다. -- [ ] 각 component는 독립 RTL test를 먼저 통과시킨 뒤 Admin shell에서 최소 한 번 실제 조합해 integration test를 작성한다. +**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 -- [ ] image `10MB`의 정확한 byte 기준을 backend와 맞춰 PRD·API Contract·경계 test에 기록한다. 미확정이면 `FileField`는 주입된 `maxBytes`만 검증하고 도메인 정책 완료를 주장하지 않는다. -- [ ] `FileField`는 visible label, 설명·오류 연결, accept 안내, keyboard activation, 선택 취소와 controlled `File | null` contract만 제공한다. -- [ ] 공통 file validation은 주입된 allowed extension·MIME·maxBytes를 함께 확인한다. JPEG/PNG·GIF 같은 resource별 allowed set은 각 도메인 policy가 소유한다. -- [ ] 공통 audio policy는 MP3/AAC/M4A, `.m4a + audio/x-m4a`, `1,024,000,000 bytes` 경계, WAV 거부를 표현하되 실제 container·codec을 client에서 판정하지 않는다. -- [ ] `ImageCropDialog`는 주입된 aspect/max width 정책으로 이동·zoom·reset·preview·취소·적용·keyboard/button 대안·no-upscale 결과를 제공한다. -- [ ] crop interaction은 검증된 단일 dependency가 native pointer/Canvas 직접 구현보다 코드·접근성 위험을 줄이는지 확인해 하나만 선택하고, Canvas는 결과 File 생성에만 사용한다. 선택 근거는 작업 기록에 남긴다. -- [ ] 공통 image policy는 `aspect`, `maxWidth`, `noUpscale`, crop 적용 여부를 받는 domain-neutral contract만 정의한다. Character·Audio·Series·Community profile과 GIF 예외는 각 feature Phase가 소유한다. -- [ ] `UploadProgress`는 진행률·취소·재시도 callback과 상태 표시만 담당하고 Axios request나 domain form을 직접 소유하지 않는다. -- [ ] `AdminAudioPlayer`는 native audio를 감싸 play/pause·seek·time·volume·speed·keyboard·일반 오류·수동 재시도를 제공하고 download와 자동 refetch/자동 play를 만들지 않는다. -- [ ] `AudioPlaybackProvider`가 동시에 하나의 player만 재생되게 하며 signed URL을 log·storage에 전달하지 않는 test를 작성한다. -- [ ] 공통 file/media component는 endpoint·query cache·domain DTO를 import하지 않는 dependency test 또는 review checklist를 통과한다. +**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 +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 @@ -361,12 +474,109 @@ 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 계약으로 확정한다. + +- 연결 리뷰: `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 단일 요청 계약을 복구한다. + +- 연결 리뷰: `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 2 권한 기능. + +- [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 형식 조합 계약을 복구한다. + +- 연결 리뷰: `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 경계를 복구한다. + +- 연결 리뷰: `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 증거로 복구한다. + +- 연결 리뷰: `REV-P1-001` +- 상태: 완료. +- 완료한 부분: P1-GATE 명령은 `src/app/App.test.tsx`를 포함하도록 보정했고, P1-R2/R3/R4/R5의 자동·E2E 검증 기록을 누적했다. +- 결정: `PageHeader`는 구현·소비가 없고 현재 필요한 반복 소비 컴포넌트가 아니므로 Phase 1 shared 산출물에서 제거한다. 필요하다고 판단되는 도메인 Phase에서 추가한다. +- 완료 증거: P1-R2 exact byte와 PageHeader 폐기 결정을 반영했고, closure Gate를 재실행해 열린 Blocker·High·Medium 0건으로 정리했다. + --- ## Phase 2. Character workspace vertical slice **목표:** ADMIN이 active Character를 검색·생성·선택하고 workspace에서 상세·수정·soft delete까지 완료한다. +**Phase Goal `P2`:** Task 2.1 → 2.4와 Phase 2 Gate로 Character workspace vertical slice를 완성한다. + +- **시작 조건:** `P1-GATE` 완료. +- **완료 조건:** `P2-T1`~`P2-T4`, `P2-GATE` 완료. 외부 의존은 구현 또는 명시적 제외 결정으로 종결. +- **실행 순서:** 계약 확인 → 목록/workspace → mutation → 반응형·접근성. + **요구사항:** `CHAR-001~014`, `FILE-001~002`, `FILE-008~010`, `FILE-012`, PRD `7`, `9`의 Character 범위. **외부 의존:** `CHAR-013` original work lookup·미선택 직렬화, 신규 Character 오류 계약. @@ -389,6 +599,12 @@ npm run build ### Task 2.1 Phase 계약 확인 +**Goal 실행 `P2-T1`:** Character 구현 계약과 화면 component map을 확정한다. + +- **시작 조건:** `P1-GATE` 완료, PRD `CHAR-001~014`와 API Contract §4 확인. +- **완료 증거:** 체크박스 전체, 제공 계약 또는 제외 결정의 세 문서 일치, 상태/action inventory. +- **범위 밖:** 계약을 추정한 production adapter와 Character 화면 구현. + - [ ] original work lookup endpoint, DTO, search/page, 미선택 `null`/omit canonical form을 확인한다. - [ ] 계약이 없으면 original work network control과 serializer를 추측하지 않고, 현재 slice에서 제외할 범위를 PRD·API Contract·plan에 먼저 기록한다. - [ ] Character 도메인 오류의 비2xx status와 message key를 contract fixture에 기록한다. @@ -396,6 +612,12 @@ npm run build ### Task 2.2 목록·선택·workspace +**Goal 실행 `P2-T2`:** active Character 목록·검색·선택과 URL 기반 workspace 복원을 완성한다. + +- **시작 조건:** `P2-T1` 완료. +- **완료 증거:** 체크박스 전체, route/list/workspace test와 read-only/error 상태 검증 기록. +- **범위 밖:** Character 생성·수정·비활성화 form. + - [ ] Character 목록·생성 path에는 `characterId`가 없고 하위 resource route에만 선택한 `characterId`가 들어가는 contract test를 작성한다. - [ ] active-only 목록의 `search`, `page`, `size` URL query 보존과 loading·empty·error·retry test를 작성한다. - [ ] 목록 request에 `isActive`·`activeStatus`가 없고 client-side 활성 filter도 없는 contract test를 작성한다. @@ -406,6 +628,12 @@ npm run build ### Task 2.3 생성·수정·soft delete +**Goal 실행 `P2-T3`:** Character multipart 생성·수정·soft delete와 image/original-work 규칙을 완성한다. + +- **시작 조건:** `P2-T1`, `P2-T2` 완료. +- **완료 증거:** 체크박스 전체, serializer/form/image/deactivate test, `OQ-009` 결정과 검증 기록. +- **범위 밖:** 계약 미제공 original-work integration과 하위 도메인 mutation. + - [ ] create multipart가 `request` JSON part와 optional image만 보내며 `isActive`, `externalCharacterId`를 포함하지 않는 test를 작성한다. - [ ] 일반 update는 `isActive`를 생략하고 soft delete만 `isActive=false`를 보내며 `true`를 보내지 않는 test를 작성한다. - [ ] name·description visible label, field error, 중복 제출 방지, dirty-form 이탈 확인을 test한다. @@ -420,6 +648,12 @@ npm run build ### Task 2.4 Character 반응형·접근성 +**Goal 실행 `P2-T4`:** Character viewport capability와 keyboard·zoom·axe 기준을 검증한다. + +- **시작 조건:** `P2-T2`, `P2-T3`의 활성 범위 완료. +- **완료 증거:** 체크박스 전체, 320/768/1280px·200% zoom·keyboard·axe E2E 기록. +- **범위 밖:** 다음 Phase 도메인과 모바일 mutation. + - [ ] desktop/tablet에서는 전체 관리 action을 제공한다. - [ ] mobile에서는 목록·검색·상세만 제공하고 create/edit/deactivate route 직접 진입도 desktop 안내로 종료한다. - [ ] Table이 mobile Card로 바뀌어도 동일한 accessible name과 핵심 상태를 유지한다. @@ -428,6 +662,12 @@ npm run build ### Phase 2 Gate +**Goal 실행 `P2-GATE`:** Character 전체 journey와 공통 품질 Gate를 최종 판정한다. + +- **시작 조건:** `P2-T1`~`P2-T4` 완료. +- **완료 증거:** 아래 명령과 Expected 통과, 외부 의존의 구현/제외 상태 기록. +- **범위 밖:** 실패와 무관한 Audio 기능. + ```bash npm run test:run -- src/features/characters src/layouts/CharacterWorkspaceLayout.test.tsx npm run e2e -- tests/e2e/character-workspace.spec.ts @@ -444,31 +684,50 @@ npm run build **목표:** 선택 Character의 Audio를 검색·검수·발행·수정·비활성화하고 대용량 upload를 안전하게 제어한다. -**요구사항:** `AUDIO-001~026`, `FILE-001~002`, `FILE-006~009`, `FILE-012~013`, PRD `9`의 Audio 범위. +**Phase Goal `P3`:** Task 3.1 → 3.4와 Phase 3 Gate로 Audio 조회·재생·발행·upload slice를 완성한다. + +- **시작 조건:** Phase 2 workspace core(`P2-T2`)와 `P1-T6` 완료. +- **완료 조건:** `P3-T1`~`P3-T4`, `P3-GATE` 완료. 오류·price 계약은 제공값 또는 명시된 최소 규칙으로 종결. +- **실행 순서:** 계약 확인 → 목록/상세/player → form/upload → 반응형·접근성. + +**요구사항:** `AUDIO-001~028`, `FILE-001~002`, `FILE-006~009`, `FILE-012~013`, PRD `9`의 Audio 범위. **외부 의존:** Audio 도메인 오류 계약, optional P1 price 상한. price 상한이 없으면 `0 이상 정수`만 적용한다. **주요 Files:** -- Create: `src/features/audio-contents/api/{audio-content-api,series-options-api,upload-audio-content}.ts` +- Create: `src/features/audio-contents/api/{audio-content-api,audio-content-theme-api,series-options-api,upload-audio-content}.ts` - Create: `src/features/audio-contents/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,ReleaseScheduleField,SeriesMultiCombobox}.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` ### Task 3.1 Phase 계약 확인 +**Goal 실행 `P3-T1`:** Audio 오류·price·theme·상태 계약과 component map을 확정한다. + +- **시작 조건:** `P2-T2`, `P1-T6` 완료, PRD `AUDIO-001~028`와 API Contract §5 확인. +- **완료 증거:** 체크박스 전체, contract fixture와 상태/action inventory의 세 문서 일치. +- **범위 밖:** 오류 status/key 또는 price 상한 추정과 Audio UI 구현. + - [ ] Audio 신규 오류 status/message key와 backend container·codec 오류 fixture를 기록한다. +- [ ] 오디오 테마 목록 `GET /api/v2/admin/ai-characters/audio-content-themes`가 query/body 없이 호출되고 `themeId`, `themeName`, `imageUrl` 배열을 반환하는 contract fixture를 기록한다. - [ ] price 최대값이 제공되면 schema와 경계 test를 추가하고, 없으면 상한을 만들지 않는다. - [ ] status query 미전송 시 server가 결과 집합을 결정한다는 계약을 유지하고 client fixture에서 임의 집합을 강제하지 않는다. - [ ] 목록·상세·player·form/upload의 상태/action inventory를 작성하고 Page는 route/query/policy 조합, feature component는 Audio 규칙, shared component는 Phase 1 contract를 사용하도록 component map을 확정한다. ### Task 3.2 목록·상세·player +**Goal 실행 `P3-T2`:** Audio active-only 목록·상세 복원과 안전한 단일 재생 흐름을 완성한다. + +- **시작 조건:** `P3-T1` 완료. +- **완료 증거:** 체크박스 전체, list/detail/player test와 signed URL 비기록·no-auto-refetch 기록. +- **범위 밖:** 생성·수정·upload form. + - [ ] status type과 filter가 `OPEN | SCHEDULED`만 허용하고 서버 값을 client가 재계산하지 않는 test를 작성한다. - [ ] 검색·status·page URL 보존, active-only request, loading·empty·error·retry를 test한다. - [ ] Audio detail route의 직접 진입과 새로고침에서 같은 resource를 복원하는 test를 작성한다. @@ -480,17 +739,24 @@ npm run build ### Task 3.3 발행 form·upload +**Goal 실행 `P3-T3`:** theme 선택, 즉시/예약 발행, multipart media와 진행률·취소·재시도를 완성한다. + +- **시작 조건:** `P3-T1`, `P3-T2` 완료. +- **완료 증거:** 체크박스 전체, contract/form/upload/file-boundary test, `OQ-009` 결정과 검증 기록. +- **범위 밖:** resumable upload, client codec 판정, 계약 없는 price 상한. + - [ ] 생성은 cover image와 audio file 필수, 수정 교체 파일은 optional이며 미전송 시 기존 media 유지임을 test한다. - [ ] Audio cover가 Phase 1 `FileField`·`ImageCropDialog`의 JPEG/PNG·10MB, `1:1`, 최대 800px, no-upscale profile을 조합하는 test를 작성한다. - [ ] MP3/AAC/M4A 허용, WAV 거부, extension/MIME 조합을 test한다. - [ ] `.m4a + audio/x-m4a`만 호환 조합으로 허용하고 실제 container·codec 판정은 server 책임으로 둔다. - [ ] `1,024,000,000 bytes` 허용, `1,024,000,001 bytes` 거부 경계 test를 작성한다. - [ ] price는 0 이상 정수 “캔”으로 입력·format한다. +- [ ] 생성 form은 오디오 테마 목록을 불러와 visible label이 있는 선택 UI를 제공하고, 미선택 제출을 차단하며 선택한 `themeId`를 create payload에 포함하는 test를 작성한다. - [ ] 즉시 공개 기본값은 날짜 입력을 비활성화·초기화하고 `releaseDateUtc=null`을 보낸다. - [ ] 예약 공개는 미래 Asia/Seoul 시각만 받고 UTC ISO-8601 `Z`로 변환하는 test를 작성한다. - [ ] 수정 form은 server `releaseDateUtc/status`로 초기화하고 사용자가 바꾸지 않으면 기존 값을 유지한다. - [ ] 제공된 active Series 목록 endpoint를 사용하는 options request와 `seriesIds` 다중 선택·keyboard 제거를 test한다. -- [ ] create payload에 `status`, `isActive`가 없고 update/soft delete의 `isActive` 규칙이 지켜지는 contract test를 작성한다. +- [ ] create payload에 필수 `themeId`가 있고 `status`, `isActive`가 없으며 update/soft delete의 `isActive` 규칙이 지켜지는 contract test를 작성한다. - [ ] upload 진행률, AbortController 취소, 전체 재시도, 실패 후 form/file 상태 보존을 test한다. - [ ] 415 server 오류를 field 안내로 보존하고 resumable upload는 만들지 않는다. - [ ] 저장 성공은 detail/list cache를 갱신하고 soft delete 성공은 active-only 목록 이동과 toast로 끝낸다. @@ -498,6 +764,12 @@ npm run build ### Task 3.4 Audio 반응형·접근성 +**Goal 실행 `P3-T4`:** Audio viewport capability와 player/form 접근성 기준을 검증한다. + +- **시작 조건:** `P3-T2`, `P3-T3`의 활성 범위 완료. +- **완료 증거:** 체크박스 전체, 320px·keyboard·200% zoom·axe E2E 기록. +- **범위 밖:** 모바일 create/edit/deactivate/upload. + - [ ] mobile에서는 목록·상세·player만 제공하고 create/edit/deactivate/upload는 숨김이 아닌 route capability로 차단한다. - [ ] desktop/tablet에서는 모든 form·upload action을 제공한다. - [ ] 320px에서 player control, error text, 긴 title이 overflow하지 않는 E2E를 작성한다. @@ -505,6 +777,12 @@ npm run build ### Phase 3 Gate +**Goal 실행 `P3-GATE`:** Audio journey, upload 복구와 media 오류 불변식을 최종 판정한다. + +- **시작 조건:** `P3-T1`~`P3-T4` 완료. +- **완료 증거:** 아래 명령과 Expected 통과 및 Phase 검증 기록. +- **범위 밖:** 실패와 무관한 Series/Community 구현. + ```bash npm run test:run -- src/features/audio-contents src/shared/ui src/shared/validation npm run e2e -- tests/e2e/audio-content.spec.ts @@ -521,6 +799,12 @@ npm run build **목표:** 선택 Character의 Series를 생성·수정·비활성화하고 Audio 연결·해제와 활성 Series 전체 순서를 관리한다. +**Phase Goal `P4`:** Task 4.1 → 4.4와 Phase 4 Gate로 Series CRUD·연결·전체 순서 slice를 완성한다. + +- **시작 조건:** `P3-T2`의 Audio 조회 API 완료. +- **완료 조건:** `P4-T1`~`P4-T4`, `P4-GATE` 완료. 계약 없는 genre·연결·순서는 구현 또는 명시적 제외 결정으로 종결. +- **실행 순서:** 계약 확인 → CRUD → 연결/순서 → 반응형·접근성. + **요구사항:** `SERIES-001~013`, `FILE-001~002`, `FILE-005`, `FILE-007~009`, `FILE-012`, `FILE-015`, PRD `9`의 Series 범위. **외부 의존:** genre lookup(`SERIES-011`), 연결 후보, 50개 초과 전체 로딩, 누락 ID, 동시 충돌, 신규 오류 계약. @@ -539,6 +823,12 @@ npm run build ### Task 4.1 Phase 계약 확인 +**Goal 실행 `P4-T1`:** Series genre·연결 후보·전체 순서·오류 계약과 component map을 확정한다. + +- **시작 조건:** `P3-T2` 완료, PRD `SERIES-001~013`와 API Contract §6 확인. +- **완료 증거:** 체크박스 전체, 제공 계약 또는 제외 결정의 세 문서 일치, 상태/action inventory. +- **범위 밖:** 계약 없는 genre/연결/순서 network 구현. + - [ ] genre lookup endpoint·DTO·search/page 계약을 기록한다. - [ ] 선택 Character의 연결 가능한 active Audio 후보 계약을 기록한다. - [ ] 활성 Series가 50개를 넘을 때 전체를 누락 없이 읽는 방식과 누락 ID·동시 변경 충돌 오류를 기록한다. @@ -547,6 +837,12 @@ npm run build ### Task 4.2 Series CRUD +**Goal 실행 `P4-T2`:** Series CRUD, enum·요일·image와 soft delete 규칙을 완성한다. + +- **시작 조건:** `P4-T1` 완료. +- **완료 증거:** 체크박스 전체, contract/form/image/list/detail/deactivate test와 검증 기록. +- **범위 밖:** Audio 연결·해제와 전체 순서 저장. + - [ ] list가 active-only이며 활성 query를 보내지 않고 loading·empty·error·retry를 제공하는 test를 작성한다. - [ ] Series detail route의 직접 진입과 새로고침에서 같은 resource를 복원하는 test를 작성한다. - [ ] enum은 `PROCEEDING | SUSPEND | COMPLETE`, 요일은 `SUN~SAT | RANDOM`만 허용한다. @@ -559,6 +855,12 @@ npm run build ### Task 4.3 Audio 연결·해제·전체 순서 +**Goal 실행 `P4-T3`:** Series Audio 연결·해제와 active Series 전체 순서를 안전하게 관리한다. + +- **시작 조건:** `P4-T1`, `P4-T2` 완료 및 관련 P0 계약 제공. +- **완료 증거:** 체크박스 전체, link/unlink/reorder contract·interaction test, 충돌 보존, `OQ-009` 결정 기록. +- **범위 밖:** 계약 없는 후보/전체 로딩/충돌 동작의 추정 구현. + - [ ] 현재 연결 Audio 목록의 search/page와 상세 cache 동기화를 test한다. - [ ] 후보는 선택 Character의 active Audio로 제한하고 이미 연결된 항목을 중복 선택하지 않는다. - [ ] 연결 POST는 `{ contentIds }`, 해제 DELETE는 body 없음임을 contract test로 고정한다. @@ -570,12 +872,24 @@ npm run build ### Task 4.4 Series 반응형·접근성 +**Goal 실행 `P4-T4`:** Series viewport capability와 form·연결·정렬 접근성을 검증한다. + +- **시작 조건:** `P4-T2`, `P4-T3`의 활성 범위 완료. +- **완료 증거:** 체크박스 전체, 320px·keyboard·200% zoom·axe E2E 기록. +- **범위 밖:** 모바일 CRUD·연결·순서 mutation. + - [ ] mobile은 목록·상세·연결 콘텐츠 조회만 제공하고 CRUD·연결·순서 action을 route capability로 차단한다. - [ ] desktop/tablet에서 전체 관리 흐름을 제공한다. - [ ] keyboard-only 요일·genre·연결·정렬, 320px 조회, 200% zoom, axe critical·serious 0건을 확인한다. ### Phase 4 Gate +**Goal 실행 `P4-GATE`:** Series CRUD·연결·전체 reorder journey와 serializer 불변식을 판정한다. + +- **시작 조건:** `P4-T1`~`P4-T4` 완료. +- **완료 증거:** 아래 명령과 Expected 통과, 외부 의존 상태와 Phase 검증 기록. +- **범위 밖:** 실패와 무관한 Community/FanTalk 구현. + ```bash npm run test:run -- src/features/series npm run e2e -- tests/e2e/series.spec.ts @@ -592,6 +906,12 @@ npm run build **목표:** 별도 상세 route/GET 없이 active Community 목록과 Sheet만으로 게시글 등록·조회·수정·고정·비활성화·첨부 재생을 완료한다. +**Phase Goal `P5`:** Task 5.1 → 5.4와 Phase 5 Gate로 목록 기반 Community Sheet·media slice를 완성한다. + +- **시작 조건:** `P1-T6` file/media primitive와 `P3-T2` player integration 완료. +- **완료 조건:** `P5-T1`~`P5-T4`, `P5-GATE` 완료. 오류·price는 제공 계약 또는 최소 규칙으로 종결. +- **실행 순서:** 계약 확인 → 목록/Sheet → form/media → 반응형·접근성. + **요구사항:** `COMMUNITY-001~011`, `FILE-001~004`, `FILE-007~009`, `FILE-011~014`, PRD `9`의 Community 범위. **외부 의존:** Community 신규 오류 계약, optional P1 price 상한. Comments는 Phase 7에서 연결한다. @@ -610,12 +930,24 @@ npm run build ### Task 5.1 Phase 계약 확인 +**Goal 실행 `P5-T1`:** Community 오류·media·price 계약과 목록/Sheet component map을 확정한다. + +- **시작 조건:** `P1-T6`, `P3-T2` 완료, PRD `COMMUNITY-001~011`과 API Contract §7 확인. +- **완료 증거:** 체크박스 전체, contract fixture와 상태/action inventory의 세 문서 일치. +- **범위 밖:** price 상한·오류 key 추정과 Comments 구현. + - [ ] Community 오류 status/message key와 media upload 오류 fixture를 기록한다. - [ ] price 최대값이 제공되면 Audio와 같은 정책으로 갱신하고, 없으면 0 이상 정수만 유지한다. - [ ] 목록·Sheet·form/media의 상태/action inventory를 작성하고 Page는 collection query/policy 조합, feature component는 Community 규칙, shared component는 Phase 1 contract를 사용하도록 component map을 확정한다. ### Task 5.2 목록·collection Sheet +**Goal 실행 `P5-T2`:** 전용 detail route/GET 없는 active 목록과 collection Sheet mutation 흐름을 완성한다. + +- **시작 조건:** `P5-T1` 완료. +- **완료 증거:** 체크박스 전체, list/Sheet/router/cache/pin/deactivate test와 detail GET 0회 기록. +- **범위 밖:** 댓글과 제공 계약에 없는 Community 검색. + - [ ] active-only 목록의 `page/size`, loading·empty·error·retry와 URL query 보존을 test한다. 제공 계약에 없는 Community `search` query나 현재 page 한정 client 검색은 만들지 않는다. - [ ] 목록 item을 source로 Sheet를 열고 전용 detail GET을 0회 호출하는 test를 작성한다. - [ ] `/community-posts/:postId`, `/edit` route가 존재하지 않는 router test를 작성한다. @@ -626,6 +958,12 @@ npm run build ### Task 5.3 게시글 form·첨부 media +**Goal 실행 `P5-T3`:** Community form, JPEG/PNG/GIF와 audio 첨부·재생 규칙을 완성한다. + +- **시작 조건:** `P5-T1`, `P5-T2` 완료. +- **완료 증거:** 체크박스 전체, serializer/form/media/file-boundary/player test, `OQ-009` 결정 기록. +- **범위 밖:** GIF 재인코딩, URL 갱신 전용 요청, Comments. + - [ ] 생성 payload에 `isActive`가 없고 일반 update/soft delete가 공통 `isActive` 규칙을 지키는 test를 작성한다. - [ ] content, price 0 이상 정수, isAdult, isFixed와 optional image/audio를 test한다. - [ ] JPEG/PNG는 자유 ratio crop·최대 800px·no-upscale을 적용한다. @@ -639,12 +977,24 @@ npm run build ### Task 5.4 Community 반응형·접근성 +**Goal 실행 `P5-T4`:** Community viewport capability와 Sheet/media/form 접근성을 검증한다. + +- **시작 조건:** `P5-T2`, `P5-T3`의 활성 범위 완료. +- **완료 증거:** 체크박스 전체, focus trap/복귀·320px·keyboard·200% zoom·axe E2E 기록. +- **범위 밖:** 모바일 등록·수정·고정·비활성화. + - [ ] mobile은 목록·Sheet 조회·첨부 재생만 제공하고 등록·수정·고정·비활성화를 route/action policy로 차단한다. - [ ] desktop/tablet에는 전체 관리 흐름을 제공한다. - [ ] Sheet focus trap/복귀, keyboard media/form, 320px overflow, 200% zoom, axe critical·serious 0건을 확인한다. ### Phase 5 Gate +**Goal 실행 `P5-GATE`:** Community 목록/Sheet CRUD·pin·media journey와 no-detail 불변식을 판정한다. + +- **시작 조건:** `P5-T1`~`P5-T4` 완료. +- **완료 증거:** 아래 명령과 Expected 통과 및 Phase 검증 기록. +- **범위 밖:** 실패와 무관한 FanTalk/Comments 구현. + ```bash npm run test:run -- src/features/community-posts src/shared/validation npm run e2e -- tests/e2e/community-post.spec.ts @@ -661,6 +1011,12 @@ npm run build **목표:** 모든 viewport에서 FanTalk를 최신순·답변 상태로 조회하고 답변을 한 번 작성한 뒤 기존 답변만 수정한다. +**Phase Goal `P6`:** Task 6.1 → 6.3과 Phase 6 Gate로 FanTalk 조회·단일 답변·수정 slice를 완성한다. + +- **시작 조건:** `P2-T2` workspace core 완료. +- **완료 조건:** 핵심 계약이 제공되면 `P6-T1`~`P6-T3`, `P6-GATE` 완료. 미제공이면 Phase 제외/후속 결정 문서화로 종결. +- **실행 순서:** 계약 확인 → 목록/답변 → 반응형·접근성. + **요구사항:** `FANTALK-001~008`, PRD `9`의 FanTalk 범위. **외부 의존:** 목록·상세·답변 수정 endpoint/DTO, filter/sort, reply uniqueness의 원자적 강제와 중복 오류 계약. 핵심 계약이 없으면 이 Phase 전체를 추측 구현하지 않는다. @@ -679,6 +1035,12 @@ npm run build ### Task 6.1 Phase 계약 확인 +**Goal 실행 `P6-T1`:** FanTalk 목록·상세·수정·유일성·오류 계약과 component map을 확정한다. + +- **시작 조건:** `P2-T2` 완료, PRD `FANTALK-001~008`와 API Contract §8~9 확인. +- **완료 증거:** 체크박스 전체, 제공 계약 또는 Phase 제외/후속 결정의 세 문서 일치. +- **범위 밖:** 임시 endpoint·placeholder DTO·production mock adapter. + - [ ] 목록·상세·답변 수정 endpoint, request/response DTO, page/filter/latest sort, ownership error를 기록한다. - [ ] 답변 1개를 server가 원자적으로 강제하는 방식과 중복 생성 비2xx status/message key를 기록한다. - [ ] 계약이 없으면 임시 endpoint·placeholder DTO·mock production adapter를 만들지 않고 Phase 제외/후속 결정을 문서화한다. @@ -686,6 +1048,12 @@ npm run build ### Task 6.2 목록·답변 생성·수정 +**Goal 실행 `P6-T2`:** 최신순/filter 목록과 답변 1회 생성·기존 답변 수정 흐름을 완성한다. + +- **시작 조건:** `P6-T1`에서 핵심 계약 제공 확인. +- **완료 증거:** 체크박스 전체, list/detail/reply contract·UI test, 중복 제출/오류 복구, `OQ-009` 결정 기록. +- **범위 밖:** 답변 삭제·두 번째 답변과 계약 없는 network 동작. + - [ ] 기본 목록은 최신순 전체이며 전체/미답변/답변 완료 filter와 page를 URL에 보존한다. - [ ] loading·empty·error·retry와 direct detail/refresh를 test한다. - [ ] 답변이 없을 때만 POST form을, 있으면 edit form만 표시하고 delete UI는 만들지 않는다. @@ -697,12 +1065,24 @@ npm run build ### Task 6.3 FanTalk 반응형·접근성 +**Goal 실행 `P6-T3`:** 모든 viewport의 FanTalk 응대 capability와 접근성을 검증한다. + +- **시작 조건:** `P6-T2` 완료. +- **완료 증거:** 체크박스 전체, 320px keyboard viewport·keyboard-only·200% zoom·axe E2E 기록. +- **범위 밖:** viewport별 기능 축소와 답변 삭제. + - [ ] desktop/tablet/mobile 모두 조회·답변 작성·수정을 제공한다. - [ ] 320px에서 keyboard가 reply input/submit을 가리지 않는 E2E를 작성한다. - [ ] keyboard-only filter/detail/create/edit, 200% zoom, axe critical·serious 0건을 확인한다. ### Phase 6 Gate +**Goal 실행 `P6-GATE`:** FanTalk 단일 답변 불변식과 모든 viewport journey를 최종 판정한다. + +- **시작 조건:** `P6-T1`~`P6-T3` 활성 범위 완료. +- **완료 증거:** 아래 명령과 Expected 통과 또는 Phase 제외 결정의 검증 기록. +- **범위 밖:** 계약 미제공 상태에서 network integration 완료 주장. + ```bash npm run test:run -- src/features/fan-talks npm run e2e -- tests/e2e/fan-talk.spec.ts @@ -719,6 +1099,12 @@ npm run build **목표:** Audio detail과 Community Sheet 양쪽에서 같은 2단계 댓글 UX를 제공하고 작성자별 수정·soft delete 권한을 일관되게 적용한다. +**Phase Goal `P7`:** Task 7.1 → 7.4와 Phase 7 Gate로 두 target의 2단계 Comments slice를 완성한다. + +- **시작 조건:** `P3-T2` Audio detail과 `P5-T2` Community Sheet 완료. +- **완료 조건:** 핵심 계약이 제공되면 `P7-T1`~`P7-T4`, `P7-GATE` 완료. 미제공이면 Phase 제외/후속 결정 문서화로 종결. +- **실행 순서:** 계약 확인 → target/thread → 권한 action → 반응형·접근성. + **요구사항:** `COMMENT-001~006`, PRD `9`의 Comments 범위. **외부 의존:** Audio·Community 댓글 목록/작성/수정/soft delete endpoint·DTO, 2단계 강제, fan 댓글 삭제 권한 오류. 핵심 계약이 없으면 이 Phase 전체를 추측 구현하지 않는다. @@ -736,6 +1122,12 @@ npm run build ### Task 7.1 Phase 계약 확인 +**Goal 실행 `P7-T1`:** 두 댓글 target의 CRUD·2단계·권한 오류 계약과 component map을 확정한다. + +- **시작 조건:** `P3-T2`, `P5-T2` 완료, PRD `COMMENT-001~006`과 API Contract §9 확인. +- **완료 증거:** 체크박스 전체, 제공 계약 또는 Phase 제외/후속 결정의 세 문서 일치. +- **범위 밖:** endpoint 이름 추정과 client-only permission 완료 주장. + - [ ] Audio·Community target별 endpoint, query, DTO, page, 작성/수정/soft delete 응답을 기록한다. - [ ] root/direct reply 정확히 2단계인 server rule과 fan content 삭제 권한 오류 status/message key를 기록한다. - [ ] 계약이 없으면 target endpoint를 이름만 보고 추정하거나 client-only permission을 완료로 간주하지 않는다. @@ -743,6 +1135,12 @@ npm run build ### Task 7.2 target adapter·2단계 thread +**Goal 실행 `P7-T2`:** Audio/Community target adapter와 정확히 2단계인 comment thread 조회를 완성한다. + +- **시작 조건:** `P7-T1`에서 핵심 계약 제공 확인. +- **완료 증거:** 체크박스 전체, target contract/thread/state/page test와 Community 상태 보존 기록. +- **범위 밖:** 작성자별 mutation action. + - [ ] UI target은 Audio와 Community를 명시적으로 구분하고 각 제공 endpoint로만 요청하는 contract test를 작성한다. - [ ] Community 상세 GET 없이 목록 item의 `characterId/postId`로 Comments Sheet를 연다. - [ ] Comments Sheet 종료 후 Community page·scroll 상태를 보존한다. @@ -751,6 +1149,12 @@ npm run build ### Task 7.3 작성자별 action +**Goal 실행 `P7-T3`:** AI Character와 fan 작성자별 수정·soft delete 권한을 일관되게 적용한다. + +- **시작 조건:** `P7-T1`, `P7-T2` 완료. +- **완료 증거:** 체크박스 전체, permission/mutation/session/read-only test, `OQ-009` 결정 기록. +- **범위 밖:** fan edit와 reply의 reply. + - [ ] AI Character 작성 root/reply에는 edit·soft delete를 제공한다. - [ ] fan 작성 root/reply에는 edit를 제공하지 않고 운영 soft delete만 제공한다. - [ ] fan edit request는 type과 UI 양쪽에서 생성할 수 없음을 test한다. @@ -761,12 +1165,24 @@ npm run build ### Task 7.4 Comments 반응형·접근성 +**Goal 실행 `P7-T4`:** 모든 viewport의 Comments CRUD capability와 thread/form/dialog 접근성을 검증한다. + +- **시작 조건:** `P7-T2`, `P7-T3` 완료. +- **완료 증거:** 체크박스 전체, 320px·keyboard-only·focus 복귀·200% zoom·axe E2E 기록. +- **범위 밖:** 모바일 기능 축소. + - [ ] desktop/tablet/mobile 모두 조회·작성·수정·soft delete를 제공한다. - [ ] 320px에서 긴 댓글, reply indentation, action menu, keyboard 입력이 overflow하지 않는 E2E를 작성한다. - [ ] keyboard-only root/reply 작성·수정·delete dialog, focus 복귀, 200% zoom, axe critical·serious 0건을 확인한다. ### Phase 7 Gate +**Goal 실행 `P7-GATE`:** 두 target의 2단계 CRUD·권한·모바일 journey를 최종 판정한다. + +- **시작 조건:** `P7-T1`~`P7-T4` 활성 범위 완료. +- **완료 증거:** 아래 명령과 Expected 통과 또는 Phase 제외 결정의 검증 기록. +- **범위 밖:** 계약 미제공 상태에서 network integration 완료 주장. + ```bash npm run test:run -- src/features/comments npm run e2e -- tests/e2e/comments.spec.ts @@ -783,6 +1199,12 @@ npm run build **목표:** 새 기능을 추가하지 않고 활성 릴리스 범위 전체가 PRD, API Contract, 보안, 반응형, 접근성 기준을 만족한다는 최신 증거를 남긴다. +**Phase Goal `P8`:** Task 8.1 → 8.3과 Phase 8 Gate로 활성 릴리스 범위의 최종 인수 증거를 완성한다. + +- **시작 조건:** 활성 범위의 `P0-GATE`~`P7-GATE` 완료 또는 명시적 제외 결정. +- **완료 조건:** `P8-T1`~`P8-T3`, `P8-GATE` 완료, 전체 자동 Gate 0 failure/0 error와 수동 검증 기록. +- **실행 순서:** 교차 journey/오류 → 반응형/접근성/보안 → 문서/품질 → 전체 Gate. + **주요 Files:** - Create: `tests/e2e/{resource-workflows,error-mapping,responsive-capabilities,accessibility}.spec.ts` @@ -791,6 +1213,12 @@ npm run build ### Task 8.1 교차 journey·오류 회귀 +**Goal 실행 `P8-T1`:** 활성 도메인의 교차 journey, 오류 매핑과 serializer/security 불변식을 회귀 검증한다. + +- **시작 조건:** 활성 범위의 Phase 0~7 Gate 완료 또는 제외 결정. +- **완료 증거:** 체크박스 전체, resource-workflows/error-mapping E2E와 request 0회·serializer fixture 기록. +- **범위 밖:** 새 기능과 계약 미제공 제외 범위의 가짜 journey. + - [ ] login → Character select → Audio immediate/scheduled create/play → Series link/order → Community Sheet → FanTalk → Comments의 활성 범위 journey를 검증한다. - [ ] 400/401/403/404/405/415/500 fixture가 공통 한국어 message와 올바른 route/session 처리를 하는지 검증한다. - [ ] Character·Audio·Series soft delete는 목록 이동, Community soft delete는 Sheet 종료·목록 제거로 끝나는지 검증한다. @@ -800,6 +1228,12 @@ npm run build ### Task 8.2 반응형·접근성·보안 회귀 +**Goal 실행 `P8-T2`:** 지원 viewport/browser의 capability, 접근성과 민감정보 비기록을 전역 검증한다. + +- **시작 조건:** `P8-T1` 완료. +- **완료 증거:** 체크박스 전체, table-driven E2E·axe·수동 browser/keyboard/zoom/security·UX 검색 기록. +- **범위 밖:** 다크 모드 구현과 외부 WCAG 인증. + - [ ] PRD `9` 기능 matrix를 table-driven E2E data로 검증한다. - [ ] 320, 640, 768, 1024, 1280px와 landscape에서 overflow·가려진 keyboard·44px target을 확인한다. - [ ] desktop Chrome/Edge/Safari와 mobile Chrome/Safari 최신 2개 주요 버전 범위를 실제 지원 환경에서 확인한다. @@ -812,6 +1246,12 @@ npm run build ### Task 8.3 문서·품질 Gate +**Goal 실행 `P8-T3`:** 요구사항 추적, 외부 의존·Open Question, README와 검증 기록을 실제 구현에 맞춘다. + +- **시작 조건:** `P8-T1`, `P8-T2` 완료. +- **완료 증거:** 체크박스 전체, PRD 수용 기준별 증거 연결, 독립 code review 수정과 관련 Gate 재실행 기록. +- **범위 밖:** 결정되지 않은 계약을 문서상 확정하는 행위. + - [ ] 활성 범위의 P0 외부 의존이 0건인지, 아니면 구현 전에 명시적으로 후속/제외 결정됐는지 확인한다. - [ ] `OQ-009`를 각 도메인별 확정 또는 “상한 추가 없음”으로 종결하고 중복 checklist를 남기지 않는다. - [ ] `OQ-010` 감사 로그 UI가 현재 릴리스 non-goal임을 결정 기록과 맞춘다. @@ -823,6 +1263,12 @@ npm run build ### Phase 8 Gate +**Goal 실행 `P8-GATE`:** 활성 릴리스 전체의 install·type·lint·unit·E2E·build·금지 패턴을 최종 판정한다. + +- **시작 조건:** `P8-T1`~`P8-T3` 완료. +- **완료 증거:** 아래 명령이 0 failure/0 error이고 Expected 및 수동 검증 결과가 최종 검증 기록에 누적됨. +- **범위 밖:** Gate 통과를 위한 test 삭제·완화, production placeholder, 계약 추정. + ```bash set -e @@ -855,7 +1301,7 @@ assert_no_match "externalCharacterId|SUNDAY|MONDAY|TUESDAY|WEDNESDAY|THURSDAY|FR | 0 | React+TypeScript+Vite, 지원 브라우저 기반 | `src/app/App.test.tsx`, `tests/e2e/smoke.spec.ts` | | 1 | `AUTH-001~013`, `UX-001~002`, `FILE`의 domain-neutral component mechanics, §7, §10 공통, §11.1~11.2, §12 | `src/shared`, `src/features/auth`, `tests/e2e/auth.spec.ts` | | 2 | `CHAR-001~014`, Character 관련 `FILE`, §7, §9 | `src/features/characters`, `tests/e2e/character-workspace.spec.ts` | -| 3 | `AUDIO-001~026`, Audio 관련 `FILE`, §9 | `src/features/audio-contents`, `tests/e2e/audio-content.spec.ts` | +| 3 | `AUDIO-001~028`, Audio 관련 `FILE`, §9 | `src/features/audio-contents`, `tests/e2e/audio-content.spec.ts` | | 4 | `SERIES-001~013`, Series 관련 `FILE`, §9 | `src/features/series`, `tests/e2e/series.spec.ts` | | 5 | `COMMUNITY-001~011`, Community 관련 `FILE`, §9 | `src/features/community-posts`, `tests/e2e/community-post.spec.ts` | | 6 | `FANTALK-001~008`, §9 | `src/features/fan-talks`, `tests/e2e/fan-talk.spec.ts` | @@ -906,3 +1352,137 @@ assert_no_match "externalCharacterId|SUNDAY|MONDAY|TUESDAY|WEDNESDAY|THURSDAY|FR - `git -c core.quotePath=false diff --name-only` — `docs/20260725_AI캐릭터관리자웹/plan-task.md` 1개만 확인, 애플리케이션 코드 변경 0건. - 애플리케이션 test/build — 이번 단계는 계획 전용이고 아직 `package.json`이 없어 실행하지 않음. - 남은 항목: 후속 구현 전체와 각 도메인에 기록한 backend 외부 의존 계약. + +### Task 1.1 구현 — 2026-07-26 + +- 무엇을: PRD `10.1~10.9` 기반의 밝은 관리자 디자인 시스템을 `DESIGN.md`와 `src/styles/globals.css`에 문서화·구현하고, Tailwind CSS Vite 플러그인과 shadcn/ui CSS variable mode `components.json`을 최소 구성했다. `StatusBadge`와 `IconOnlyAction` 원시 컴포넌트만 추가했다. +- 왜: Phase 1 이후 UI가 raw hex나 임의 스타일을 반복하지 않고, `#00BDF7` 고정 브랜드 색과 접근성 대비·light-only 범위를 테스트로 잠그기 위해서다. +- 어떻게: + - `python3 .opencode/skills/ui-ux-pro-max/scripts/search.py "enterprise internal admin console data tables forms file upload operational dashboard neutral compact" --design-system --variance 2 --motion 2 --density 9 -p "AI Character Admin" -f markdown` — 성공. 채택: dense operational dashboard, subtle motion, status/focus/reduced-motion/accessibility 항목. 제외: green palette, white primary foreground, dark mode, remote Fira fonts, oversized landing typography, glass/continuous animation, GSAP transition. + - `python3 .opencode/skills/ui-ux-pro-max/scripts/search.py "animation accessibility z-index loading" --domain ux -n 12` — 성공, ux-guidelines.csv에서 12건 반환. 채택: 300ms 초과 작업의 loading feedback, semantic z-index scale, reduced motion, 150-300ms micro-interaction, decorative infinite animation 금지. 제외/후속: lazy-loaded media, loading buttons, skeleton, route loading은 Task 1.1에 async page·media·form submit·router surface가 없어 후속 실제 소비 Task에서 적용한다. + - RED `npm run test:run -- src/styles/__tests__/design-system.test.ts src/shared/ui/__tests__/status-badge.test.tsx src/shared/ui/__tests__/icon-only-action.test.tsx src/app/App.test.tsx` — 기대 실패 확인. `DESIGN.md`, `src/styles/globals.css`, `StatusBadge`, `IconOnlyAction` 미구현으로 3 files failed / 1 passed. + - GREEN `npm run test:run -- src/styles/__tests__/design-system.test.ts src/shared/ui/__tests__/status-badge.test.tsx src/shared/ui/__tests__/icon-only-action.test.tsx src/app/App.test.tsx` — 성공, 4 files / 7 tests passed. + - `npm run test:run -- src/styles/__tests__/design-system.test.ts src/shared/ui/__tests__/status-badge.test.tsx src/shared/ui/__tests__/icon-only-action.test.tsx src/app/App.test.tsx src/shared/config/env.test.ts` — 성공, 5 files / 10 tests passed. + - `npm run typecheck` — 성공. + - `npm run lint` — 성공. + - `npm run e2e -- --project=chromium tests/e2e/smoke.spec.ts` — 성공, Chromium 1 test passed. Root `lang="ko"`와 `main`의 `AI 캐릭터 관리자`를 실제 브라우저에서 확인했다. + - `npm run build:dev` — 성공, Vite development build 완료. + - `npm run build:prod` — 성공, Vite production build 완료. + - LSP diagnostics — 변경 TypeScript/TSX 파일 모두 오류 0건. + - `rg -n "#[0-9A-Fa-f]{6}|rgb\(|hsl\(|oklch\(" src --glob '!src/styles/globals.css' --glob '!**/*.test.*' --glob '!**/__tests__/**'` — no output, production feature code raw color 0건. + - `rg -n "\.dark\b|ThemeProvider|theme toggle|next-themes|prefers-color-scheme" src components.json package.json --glob '!**/*.test.*' --glob '!**/__tests__/**'` — no output, dark/theme integration 0건. +- 남은 항목: Task 1.1 없음. Auth, router, API, shell, file/media 컴포넌트와 추가 shadcn component는 후속 Task에서 실제 소비 시점에만 추가한다. + +### Task 1.1 리뷰 차단 수정 — 2026-07-26 + +- 무엇을: Task 1.1 spec/code-quality review blocker를 수정했다. foreground 대비 test를 추가하고, PRD `10.9` UX 검색 기록을 보강했으며, app tsconfig의 Node globals 오염을 test 전용 tsconfig로 분리했다. `IconOnlyAction`의 중복 accessible description과 중복 CSS utility를 제거하고, status/link/info semantic token을 Tailwind theme에 노출했다. +- 왜: Task 1.1이 spec review와 code-quality review를 통과하려면 core foreground contrast, UX 검색 증거, production/browser tsconfig 순도, tooltip 접근성, Tailwind semantic token 노출이 모두 필요하기 때문이다. +- 어떻게: + - RED `npm run test:run -- src/styles/__tests__/design-system.test.ts` — 기대 실패 확인. 신규 core foreground contrast test가 `Missing --foreground`로 실패해 `--foreground`/`--background`/`--card` alias contrast 검증 공백을 드러냈다. + - `python3 .opencode/skills/ui-ux-pro-max/scripts/search.py "animation accessibility z-index loading" --domain ux -n 12` — 성공, ux-guidelines.csv에서 12건 반환. 채택: loading feedback, stacking context/z-index scale, reduced motion, 150-300ms duration. 제외/후속: lazy loading, skeleton/loading buttons, route loading은 Task 1.1에 실제 async/media/form/router surface가 없어 후속 소비 시점에 적용한다. + - GREEN `npm run test:run -- src/styles/__tests__/design-system.test.ts src/shared/ui/__tests__/icon-only-action.test.tsx` — 성공, 2 files / 7 tests passed. + - `npm run test:run -- src/styles/__tests__/design-system.test.ts src/shared/ui/__tests__/status-badge.test.tsx src/shared/ui/__tests__/icon-only-action.test.tsx src/app/App.test.tsx src/shared/config/env.test.ts` — 성공, 5 files / 12 tests passed. + - `npm run typecheck` — 성공. `tsconfig.app.json`에는 Node type을 두지 않고 `tsconfig.test.json`에만 test용 Node/Jest-DOM type을 둔다. + - `npm run lint` — 성공. + - `npm run build:dev` — 성공, Vite development build 완료. + - `npm run build:prod` — 성공, Vite production build 완료. + - `npm run e2e -- --project=chromium tests/e2e/smoke.spec.ts` — 성공, Chromium 1 test passed. Root `lang="ko"`와 `main`의 `AI 캐릭터 관리자` smoke behavior 유지 확인. + - LSP diagnostics — `src/shared/ui/icon-only-action.tsx`, `src/shared/ui/__tests__/icon-only-action.test.tsx`, `tsconfig.json`, `tsconfig.app.json`, `tsconfig.test.json` 오류 0건. `src/styles` 디렉터리 진단 오류 0건. + - `rg -n '"node"' tsconfig.app.json` — no output, production/browser app tsconfig Node type 0건. + - `rg -n "#[0-9A-Fa-f]{6}|rgb\(|hsl\(|oklch\(" src --glob '!src/styles/globals.css' --glob '!**/*.test.*' --glob '!**/__tests__/**'` — no output, production feature code raw color 0건. + - `rg -n "\.dark\b|ThemeProvider|theme toggle|next-themes|prefers-color-scheme" src components.json package.json --glob '!**/*.test.*' --glob '!**/__tests__/**'` — no output, dark/theme integration 0건. +- 남은 항목: Task 1.1 없음. `.gitignore`는 작업 전 dirty 상태였고 이번 수정에서 건드리지 않았다. + +### Task 1.2 구현 — 2026-07-26 + +- 무엇을: `sessionStorage` 기반 ADMIN session adapter, Zod envelope parser와 `ApiError`/`AccessDeniedError` API client, pagination helper, MSW test server, TanStack Query client/provider를 최소 구성했다. API client는 React에 의존하지 않고 token reader·clear·auth-expired callback을 주입받으며, API 요청을 console에 기록하지 않는다. +- 왜: 이후 로그인·보호 route·도메인 API가 동일한 Korean locale, Bearer, 401/403, 오류 envelope, page size, retry 정책을 재구현하지 않도록 공통 경계를 먼저 고정하기 위해서다. +- 어떻게: + - `npm install @tanstack/react-query zod` 및 `npm install --save-dev msw` — 성공. Task 1.2에서 실제 소비하는 의존성만 추가. + - RED `npm run test:run -- src/features/auth/model/auth-session-storage.test.ts src/shared/api/__tests__/pagination.test.ts src/shared/api/__tests__/query-client.test.ts src/shared/api/__tests__/client.test.ts` — 기대 실패. 아직 없는 session/API/pagination/query 모듈의 import를 해결하지 못해 4 files failed. + - GREEN 같은 focused command — 성공, 4 files / 25 tests passed. success envelope 2형태, locale/Bearer, 5개 HTTP 오류 status, concurrent 401, 403, no-console, pagination, retry와 session adapter를 확인. + - test file 순도 검토 뒤 client contract와 authenticated request contract를 분리한 focused command — 성공, 5 files / 25 tests passed. 각 TypeScript test file은 200 pure LOC 이하로 유지. + - `npm run typecheck` — 성공. + - `npm run lint` — 성공. + - `npm run build:dev` 및 `npm run build:prod` — 성공. + - LSP diagnostics — `src/features/auth/model/auth-session-storage.ts`, `src/shared/api/*`, 해당 test 및 `src/main.tsx`, `src/shared/test/*` 오류 0건. + - `npm run dev` 후 Playwright로 `http://127.0.0.1:8888/` 접근 — 성공. 페이지 title `AI 캐릭터 관리자`와 `main > h1` shell 렌더를 확인했다. console은 기존 favicon 미구현에 따른 `/favicon.ico` 404만 1건이었다. + - `npm run test:run` 전체 실행은 Vitest가 `tests/e2e/smoke.spec.ts`를 함께 수집해 Playwright의 `test()` 호출 오류로 실패했다. Task 1.2 focused Vitest suite와 기존 unit suite 37 tests는 통과했으며, E2E 파일 분리는 이번 Task 범위 밖으로 유지한다. +- 남은 항목: Task 1.2 없음. login request payload/session React state/logout UI, protected route와 access-denied page는 Task 1.3~1.4에서 연결한다. + +### Task 1.2 리뷰 차단 수정 — 2026-07-26 + +- 무엇을: 401 처리 latch를 client lifetime이 아닌 in-flight protected request burst 단위로 재설정하고, 모든 caller-supplied `Authorization`을 제거한 뒤 현재 session의 보호 요청 Bearer만 다시 설정했다. app/test TypeScript project를 분리하고 Vitest가 Playwright E2E spec을 수집하지 않도록 했다. +- 왜: 이후 로그인 후 동일 client가 다시 401을 받는 경우에도 session clear·redirect가 동작해야 하며, public/login 요청이 caller header로 인증 정보를 유출하거나 unit test runner가 E2E runner를 실행하면 안 되기 때문이다. +- 어떻게: + - RED `npm run test:run -- src/shared/api/__tests__/client-auth.test.ts` — 기대 실패, caller-supplied Bearer가 login에 전달되고 두 번째 concurrent 401 burst의 callback 횟수가 1회로 고정됨을 확인했다. + - GREEN 같은 command — 성공, 1 file / 6 tests passed. public Authorization 제거와 burst별 401 callback 재실행을 확인했다. + - `npm run test:run -- src/styles/__tests__/design-system.test.ts src/shared/ui/__tests__/status-badge.test.tsx src/shared/ui/__tests__/icon-only-action.test.tsx src/app/App.test.tsx src/shared/config/env.test.ts src/features/auth/model/auth-session-storage.test.ts src/shared/api/__tests__/pagination.test.ts src/shared/api/__tests__/query-client.test.ts src/shared/api/__tests__/client.test.ts src/shared/api/__tests__/client-auth.test.ts` — 성공, 10 files / 37 tests passed. + - `npm run test:run` — 성공, 10 files / 37 tests passed. `tests/e2e/**` Playwright spec은 Vitest default exclude를 보존한 채 제외했다. + - `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 성공. + - `npx tsc --project tsconfig.app.json --listFiles --pretty false | rg 'src/shared/test/(setup|server)\.ts$'` — no output. `tsconfig.test.json` listFiles에는 `src/shared/test/setup.ts`, `server.ts` 포함 확인. + - `npm run e2e -- --project=chromium tests/e2e/smoke.spec.ts` — 성공, Chromium 1 test passed. + - LSP diagnostics — 변경 TypeScript와 tsconfig/Vite config 오류 0건. `git diff --check` — 성공. +- 남은 항목: Task 1.2 없음. 이후 Task 1.3~1.4에서 login/session UI와 route 연결만 구현한다. + +### Task 1.3 구현 — 2026-07-26 + +- 무엇을: 로그인 request/response contract, `sessionStorage` 전용 ADMIN session provider/model, 실패해도 local session을 제거하는 logout 흐름, visible label·field error 연결·first invalid focus를 갖춘 `LoginPage`를 추가했다. Router, ProtectedRoute, Admin shell, `/login` route wiring은 Task 1.4 범위로 남겨 두었다. +- 왜: Task 1.4 route 조합 전에 로그인 API 경계, session 복원/제거 규칙, 저장소 금지 규칙, logout 실패 정책, 접근 가능한 로그인 form contract를 독립 테스트로 고정하기 위해서다. +- 어떻게: + - RED `npm run test:run -- src/features/auth/tests/auth-api.test.ts src/features/auth/tests/auth-session.test.tsx src/features/auth/tests/login-page.test.tsx` — 기대 실패. 아직 없는 `auth-api`, `auth-session`, `LoginPage` import 3건으로 3 files failed. + - GREEN 같은 focused command — 성공, 3 files / 17 tests passed. login JSON body·Authorization 없음·refresh 0건, token/role invalid reject, sessionStorage-only restore, logout success/non-2xx/network failure 제거·`/login` navigation·failure warning, LoginPage label/error/focus를 확인했다. + - refactor 후 focused command 재실행 — 성공, 3 files / 17 tests passed. + - `npm run test:run` — 성공, 13 files / 54 tests passed. + - `npm run typecheck` — 성공. + - `npm run lint` — 성공. 중간에 Fast Refresh warning 1건을 `auth-session-context.ts` 분리로 제거한 뒤 재실행했다. + - `npm run build:dev` — 성공, Vite development build 완료. + - `npm run build:prod` — 성공, Vite production build 완료. + - `npm run e2e -- tests/e2e/smoke.spec.ts` — 성공, chromium/webkit/mobile-chrome/mobile-safari 4 tests passed. 현재 App root shell은 Task 1.4 전 상태로 유지됨을 확인했다. + - `GIT_MASTER=1 git diff --check` — 성공. + - LSP diagnostics — `auth-api.ts`, `auth-session-context.ts`, `login-schema.ts`, `LoginPage.tsx`, 3개 auth test 파일 오류 0건. `auth-session.tsx` 개별 파일 진단은 반복 timeout이었고, `src/features/auth/model` 디렉터리 진단 0건 및 `npm run typecheck` 성공으로 보완 확인했다. + - changed file pure LOC — `auth-api.ts` 27, `auth-session-context.ts` 24, `auth-session.tsx` 53, `login-schema.ts` 6, `LoginPage.tsx` 118, `auth-api.test.ts` 89, `auth-session.test.tsx` 130, `login-page.test.tsx` 36으로 모두 200 LOC 이하. +- 남은 항목: Task 1.3 없음. LoginPage browser visual QA는 아직 route/App surface에 연결하지 말라는 Task 1.3 범위 제한 때문에 RTL과 기존 root smoke로만 검증했고, 실제 `/login` route browser QA는 Task 1.4에서 수행한다. + +### Task 1.4 Wave 2 구현 — 2026-07-26 + +- 무엇을: `/ai-characters`에 ADMIN session 전용 shell, `/access-denied` route, 401 login redirect, 403 access denied redirect, logout control, breadcrumb, skip link, mobile menu, 명시적 Phase 2 empty state를 연결했다. +- 왜: Phase 2 도메인 데이터 구현 전에 보호 route·권한 오류·session expiry·shell 접근성 contract를 먼저 고정하기 위해서다. +- 어떻게: + - RED `npm run test:run -- src/app/App.test.tsx` — 기대 실패, 기존 placeholder shell 때문에 신규 shell/401/403/mobile/logout 기대 6건 실패. + - GREEN 같은 focused command — 성공, 1 file / 9 tests passed. + - `npm run typecheck` — 성공. + - `npm run lint` — 성공. 중간에 effect 내부 동기 `setState` lint를 비동기 성공 경로로 옮긴 뒤 재실행했다. + - `npm run test:run -- src/app/App.test.tsx src/shared/api/__tests__/client-auth.test.ts src/styles/__tests__/design-system.test.ts` — 성공, 3 files / 21 tests passed. + - `npm run build:prod` — 성공. + - Playwright 수동 QA — `/login`, ADMIN session `/ai-characters`, mobile menu Escape focus return, logout `/login`, unauth `/ai-characters` redirect, 403 `/access-denied`와 protected empty state 미노출을 확인했다. + - LSP diagnostics — `browser-location.ts` 오류 0건. `App.tsx`, `App.test.tsx`, `route-paths.ts` 개별 fresh 진단은 반복 timeout이었고, TypeScript LSP alive 상태와 `npm run typecheck` 성공으로 보완 확인했다. + - changed file pure LOC — `App.tsx` 214, `App.test.tsx` 136, `browser-location.ts` 24, `route-paths.ts` 6. `App.tsx`는 200-250 경고 구간이므로 다음 shell 확장 전 분리 후보로 남긴다. +- 남은 항목: Task 1.4의 320px/200% zoom E2E와 axe critical·serious 0건 확인은 이번 Wave 2 기대 범위 밖이라 체크하지 않았다. + +### Task 1.6 구현 — 2026-07-27 + +- 무엇을: 공통 `FileField`, file validation, audio file policy, image policy contract, `ImageCropDialog`, `UploadProgress`, `AdminAudioPlayer`, `AudioPlaybackProvider`를 공유 레이어에 추가하고 `DESIGN.md` Section 5에 primitive contract를 기록했다. +- 왜: Phase 2 이후 도메인 form이 endpoint/query cache/domain DTO 없이 파일·crop·upload 표시·audio playback mechanics를 재사용하도록 하기 위해서다. +- 어떻게: + - RED `npm run test:run -- src/shared/validation/file-media-policy.test.ts src/shared/lib/crop-image.test.ts src/shared/ui/__tests__/file-field.test.tsx src/shared/ui/__tests__/image-crop-dialog.test.tsx src/shared/ui/__tests__/upload-progress.test.tsx src/shared/ui/__tests__/admin-audio-player.test.tsx src/shared/ui/__tests__/file-media-dependency-boundary.test.ts` — 기대 실패. 아직 없는 shared file/media module import와 dependency boundary 대상 파일 부재로 7 files failed. + - GREEN 같은 command — 성공, 7 files / 19 tests passed. 주입 `maxBytes` 10MB 경계, audio `1,024,000,000 bytes`, `.m4a + audio/x-m4a`, WAV 거부, no codec sniffing, no-upscale crop output, keyboard/button 대안, upload callback-only, audio no autoplay/refetch/download, signed URL no log/storage, dependency boundary를 확인했다. + - crop dependency 결정: `package.json`에 crop dependency가 없고 이번 Phase는 raw pointer drag/pinch가 필수가 아니므로 새 dependency 없이 native button/range/keyboard 이동을 선택했다. Canvas는 결과 `File` 생성 함수에만 사용한다. + - `npm run test:run -- src/shared` — 성공, 21 files / 57 tests passed. + - `npm run typecheck`, `npm run lint`, `npm run build` — 모두 성공. + - LSP diagnostics — `src/shared`와 `src/shared/ui` directory 진단 0건. 일부 개별 file fresh 진단은 timeout이었고 TypeScript LSP alive, `npm run typecheck`, `npm run build` 성공으로 보완 확인했다. + - changed file pure LOC — 최대 `admin-audio-player.tsx` 122, `image-crop-dialog.tsx` 118로 모두 200 LOC 이하. + +### Goal 실행 계약 보완 — 2026-07-27 + +- 무엇을: 완료된 Phase 0~1을 포함한 Phase 0~8, 구현 Task 34개, Phase Gate 9개에 goal ID, objective, 시작 조건, 완료 증거와 범위 밖을 추가했다. goal의 단일 활성 작업, 완료·차단 판정, 외부 계약 처리와 검증 기록 규칙을 공통 운영 규칙으로 명시했다. +- 왜: goal 기능이 Task 하나를 독립 실행 단위로 이어서 수행하고, 일부 구현만으로 완료하거나 외부 계약을 추정하지 않으며, Phase Gate까지 객관적인 증거로 종결할 수 있게 하기 위해서다. +- 어떻게: + - `rg -c '^## Phase [0-8]\.' docs/20260725_AI캐릭터관리자웹/plan-task.md` — Phase 9개 확인. + - `rg -c '^\*\*Phase Goal `P[0-8]`' docs/20260725_AI캐릭터관리자웹/plan-task.md` — Phase Goal 9개 확인. + - `awk '/^## Phase 0\./,/^## 5\./ { if ($0 ~ /^### Task [0-8]\.[0-9]+ /) n++ } END { print n }' docs/20260725_AI캐릭터관리자웹/plan-task.md` — 구현 Task 34개 확인. + - `rg -c '^\*\*Goal 실행 `P[0-8]-T[0-9]+`' docs/20260725_AI캐릭터관리자웹/plan-task.md` — Task goal 34개 확인. + - `rg -c '^### Phase [0-8] Gate$'`와 `rg -c '^\*\*Goal 실행 `P[0-8]-GATE`'` — Gate와 Gate goal 각각 9개 확인. + - PRD `AUDIO-027~028`, API Contract §5.0/§5.3, Phase 3 요구사항·contract/form checklist 대조 — `themeId`와 공통 테마 목록 계약이 일치함을 확인. + - 애플리케이션 test/build — 문서와 goal 실행 계약만 변경해 실행하지 않음. +- 남은 항목: Phase 2 이후 구현과 각 도메인의 backend 제공 대기 계약. diff --git a/docs/20260725_AI캐릭터관리자웹/prd.md b/docs/20260725_AI캐릭터관리자웹/prd.md index 9811061..5c9261f 100644 --- a/docs/20260725_AI캐릭터관리자웹/prd.md +++ b/docs/20260725_AI캐릭터관리자웹/prd.md @@ -26,6 +26,7 @@ 3. 사용자 인터뷰 결정과 최초 API Contract가 충돌하면 이 문서의 “API 계약 보정사항”을 우선한다. 4. 미결 사항과 외부 의존 계약은 추측으로 구현하지 않는다. **미결**에는 추천안을, **외부 의존**에는 제공 주체와 영향을 함께 기록한다. 5. 완료된 미결 사항과 제공 완료된 외부 의존 계약은 결정일과 결정 내용을 “결정 기록”에 추가한 뒤 관련 수용 기준까지 갱신한다. +6. goal 실행의 objective·순서·완료 증거·범위는 `plan-task.md`의 `Phase Goal`과 `Goal 실행`을 기준으로 한다. goal 수행 중 제품 결정이 바뀌면 이 문서의 결정 기록과 요구사항을 먼저 갱신한다. --- @@ -231,6 +232,8 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘 | AUDIO-024 | 확정 | `audio/x-m4a`는 `.m4a` 파일에 한해 호환 MIME으로 허용한다. 실제 MP4/M4A 컨테이너·코덱 검증을 통과해야 하며 다른 확장자와의 조합은 거부한다. | | AUDIO-025 | 확정 | 오디오 soft delete 성공 시 선택 캐릭터의 오디오 active-only 목록으로 이동하고 성공 알림을 표시한다. 현재 상세 화면에 머물지 않는다. | | AUDIO-026 | 확정 | 재생 오류를 signed URL 만료로 구분하거나 추정하지 않는다. media error만으로 상세·목록을 자동 재조회하거나 자동 재생하지 않고, 일반 오류와 수동 재시도·페이지 새로고침 안내를 제공한다. | +| AUDIO-027 | 확정 | 오디오 콘텐츠 생성 시 `themeId`는 필수이며, 프론트엔드는 `GET /api/v2/admin/ai-characters/audio-content-themes`로 테마 목록을 불러와 선택 UI를 제공한다. | +| AUDIO-028 | 확정 | 오디오 콘텐츠 테마 목록 조회는 query/body 없이 호출하며 응답의 `themeId`, `themeName`, `imageUrl`만 사용한다. | 수정 화면은 즉시 공개로 재초기화하지 않는다. 서버의 기존 `releaseDateUtc`와 `status`로 공개 방식과 날짜를 초기화하고, 관리자가 바꾸지 않으면 기존 값을 유지한다. @@ -314,7 +317,7 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘 | ID | 상태 | 요구사항 | |---|---|---| -| FILE-001 | 확정 | 캐릭터·오디오 cover·시리즈·커뮤니티 image의 최대 크기는 10MB다. | +| FILE-001 | 확정 | 캐릭터·오디오 cover·시리즈·커뮤니티 image의 최대 크기는 `10,485,760 bytes` 이하다. `10,485,761 bytes`부터 거부한다. | | FILE-002 | 확정 | 기본 image 형식은 JPEG(`.jpg`/`.jpeg`, `image/jpeg`)와 PNG(`.png`, `image/png`)다. WebP 등 다른 형식은 허용하지 않는다. | | FILE-003 | 확정 | GIF(`.gif`, `image/gif`)는 커뮤니티 image에서만 허용한다. 캐릭터·시리즈·오디오 cover에서는 거부한다. | | FILE-004 | 확정 | 커뮤니티 JPEG/PNG image는 자유 aspect ratio로 크롭하며 결과의 최대 가로 폭은 800px, 세로는 선택한 crop ratio에 따라 결정한다. | @@ -334,7 +337,7 @@ AI 캐릭터를 생성하고, AI 캐릭터가 사람 크리에이터처럼 콘 #### Image crop UI 흐름 -1. 새 image 선택 직후 resource별 MIME과 10MB 제한을 먼저 검증한다. +1. 새 image 선택 직후 resource별 MIME과 `10,485,760 bytes` 이하 제한을 먼저 검증한다. 2. 커뮤니티 GIF이면 원본 가로를 검사한다. 800px 초과는 inline 오류로 차단하고, 800px 이하는 crop Dialog 없이 원본 비율과 animation을 유지한다. 3. 캐릭터 image, 커뮤니티 JPEG/PNG, 시리즈 image, 오디오 콘텐츠 cover이면 crop Dialog를 열고 resource별 자유/고정 aspect ratio를 적용한다. 4. crop Dialog에는 현재 crop 영역과 예상 결과 크기를 표시한다. @@ -602,6 +605,7 @@ Phase 2~6에서 추가되는 오류는 구현 전에 정확한 비2xx status와 | 인증 | POST | `/member/logout` | 제공됨, 공통 endpoint, Bearer header, body 없음 | | 캐릭터 | GET, POST | `/api/v2/admin/ai-characters` | 제공됨, 필드 보정 필요 | | 캐릭터 | GET, PUT | `/api/v2/admin/ai-characters/{characterId}` | 제공됨, 필드 보정 필요 | +| 오디오 테마 | GET | `/api/v2/admin/ai-characters/audio-content-themes` | 제공됨, query/body 없음 | | 오디오 | GET, POST | `/api/v2/admin/ai-characters/{characterId}/audio-contents` | 제공됨, 생성 필드 보정 필요 | | 오디오 | GET, PUT | `.../audio-contents/{contentId}` | 제공됨 | | 시리즈 | GET, POST | `/api/v2/admin/ai-characters/{characterId}/series` | 제공됨, enum 보정 필요 | @@ -623,6 +627,7 @@ Phase 2~6에서 추가되는 오류는 구현 전에 정확한 비2xx status와 | 생성 `isActive` | Character, Audio, Community 예시에 존재 | 모든 생성 요청에서 제거, 서버가 초기값 결정 | | 수정 `isActive` | 수정 예시에 존재 | 일반 수정에서는 key를 생략하고 soft delete에만 `false`를 보낸다. `true`는 전송하지 않는다. | | Audio status | 예시에 `OPEN` | 허용값은 `OPEN`, `SCHEDULED`이며 서버 계산 | +| Audio create `themeId` | 누락 | 생성 시 필수이며 오디오 테마 목록 endpoint에서 선택한 `themeId`를 보낸다. | | Series state | 예시에 `OPEN` | `PROCEEDING`, `SUSPEND`, `COMPLETE`만 허용 | | Series 생성 state | 요청 예시에 `OPEN` | 생성 요청에서 state 제거 | | Series 수정 state | 필수처럼 표현 | 선택 필드, 미선택 시 생략하여 기존 값 유지 | @@ -692,7 +697,7 @@ Phase 2~6에서 추가되는 오류는 구현 전에 정확한 비2xx status와 - MP3, AAC, M4A 업로드의 진행률·취소·재시도와 `1,024,000,000 bytes` 허용·`1,024,000,001 bytes` 거부 경계 검증이 동작한다. - 오디오 콘텐츠와 커뮤니티 첨부 audio가 동일한 확장자·MIME·최대 크기·재생 길이 정책을 사용한다. - `.m4a`는 `audio/mp4`와 `audio/x-m4a`를 허용하되 호환 MIME도 실제 MP4/M4A container·codec 검증을 통과해야 한다. -- 모든 image upload가 10MB와 resource별 JPEG/PNG/GIF 허용 범위를 적용한다. +- 모든 image upload가 `10,485,760 bytes` 이하 제한과 resource별 JPEG/PNG/GIF 허용 범위를 적용한다. - 캐릭터 image는 `1:1`로 크롭하고 최대 800×800px 결과를 업로드한다. - 커뮤니티 JPEG/PNG는 자유 비율·최대 800px, Series는 `210:297`·최대 1,000px, Audio cover는 `1:1`·최대 800px crop 결과를 업로드한다. - JPEG/PNG crop 영역이 resource별 최대 출력 폭보다 작으면 확대하지 않고 가능한 원본 pixel 크기로 업로드한다. @@ -700,6 +705,7 @@ Phase 2~6에서 추가되는 오류는 구현 전에 정확한 비2xx status와 - 커뮤니티 GIF는 crop Dialog를 열지 않고 원본 비율과 animation을 유지해 등록하며, 원본 가로가 800px을 초과하면 제출 전에 거부한다. - crop Dialog에서 이동·확대/축소·초기화·미리보기·취소/적용을 keyboard와 pointer로 완료할 수 있다. - 오디오 목록·상세와 커뮤니티 목록 항목의 첨부 오디오가 재생된다. +- 오디오 콘텐츠 생성 시 테마 목록을 query/body 없이 조회하고 선택한 `themeId`를 request JSON에 포함한다. - media error를 signed URL 만료로 구분하지 않고 일반 재생 오류를 표시한다. 오류만으로 API를 자동 재조회하거나 자동 재생하지 않으며 사용자가 수동 재시도하거나 페이지를 새로고침할 수 있다. - 커뮤니티는 전용 상세 GET이나 상세·수정 route 없이 목록 응답 기반 Sheet에서 조회·수정·고정·비활성화·댓글 진입을 제공한다. - Series에 `OPEN`이나 `MONDAY` 같은 잘못된 값을 보내지 않는다. @@ -750,7 +756,8 @@ Phase 2~6에서 추가되는 오류는 구현 전에 정확한 비2xx status와 | 2026-07-25 | MP3, AAC, M4A, 최대 decimal 1,024MB(`1,024,000,000 bytes`), 재생 길이 무제한을 사용한다. | | 2026-07-25 | `audio/x-m4a`는 `.m4a` 파일에만 허용하고 실제 MP4/M4A container·codec을 검증한다. | | 2026-07-25 | 커뮤니티 첨부 audio도 오디오 콘텐츠와 같은 MP3/AAC/M4A, 최대 1,024MB, 재생 길이 무제한 정책을 사용한다. | -| 2026-07-25 | 공통 image upload 최대 크기는 10MB로 한다. | +| 2026-07-25 | 공통 image upload 최대 크기는 10MB로 한다. exact byte 값은 백엔드 확인 후 PRD·API 계약·구현 상수·경계 test를 같은 값으로 정렬한다. | +| 2026-07-27 | 공통 image upload 10MB의 exact byte를 `10,485,760 bytes`로 확정하고 `10,485,761 bytes`부터 거부한다. | | 2026-07-25 | image는 JPEG/PNG를 허용하고 GIF는 커뮤니티에만 허용한다. Character는 1:1·800px, Community JPEG/PNG는 자유 비율·800px, Series는 210:297·1,000px, Audio cover는 1:1·800px crop UI를 제공한다. Community GIF는 crop·resize하지 않고 원본 가로 800px 초과 시 등록을 거부한다. | | 2026-07-25 | JPEG/PNG crop 결과는 선택한 원본 crop 영역보다 확대하지 않고 resource별 800px/1,000px을 최대 출력 폭으로만 사용한다. | | 2026-07-25 | Series crop 세로는 `round(width × 297 ÷ 210)`으로 계산하고 최대 1,000×1,414px, 검증 오차 1px을 적용한다. | @@ -765,3 +772,4 @@ Phase 2~6에서 추가되는 오류는 구현 전에 정확한 비2xx status와 | 2026-07-26 | 커뮤니티 전용 상세 GET과 상세·수정 route를 추가하지 않는다. 목록 응답 기반 Sheet를 사용하고 첨부 audio URL 갱신 전용 요청은 만들지 않는다. 사용자 새로고침이나 mutation 후 cache 무효화 등 일반 목록 재조회가 있으면 새 응답 값을 사용한다. | | 2026-07-26 | 문자열 최대 길이와 배열 최대 개수는 초기 UI 작성 후 각 페이지에서 권고값을 정하고 백엔드 호환 확인 후 확정한다. 그 전에는 제공 계약에 없는 최대값을 추정하지 않는다. | | 2026-07-26 | 댓글 API·팬 댓글 삭제 권한 오류와 FanTalk 목록·상세·답변 수정·중복 답변 계약은 백엔드 제공 대기 사항으로 분류하고 프론트엔드 Open Questions에서 제외한다. | +| 2026-07-27 | 오디오 콘텐츠 생성에는 `themeId`가 필수이며, 테마 선택지는 `GET /api/v2/admin/ai-characters/audio-content-themes`에서 query/body 없이 조회한다. | diff --git a/docs/20260725_AI캐릭터관리자웹/review/review-phase-0-1.md b/docs/20260725_AI캐릭터관리자웹/review/review-phase-0-1.md new file mode 100644 index 0000000..270cd58 --- /dev/null +++ b/docs/20260725_AI캐릭터관리자웹/review/review-phase-0-1.md @@ -0,0 +1,774 @@ +# AI 캐릭터 관리자 웹 Phase 0·1 코드 리뷰·QA 리포트 + +## 1. 리뷰 정보 + +| 항목 | 내용 | +| --- | --- | +| 리뷰 대상 | docs/20260725_AI캐릭터관리자웹/plan-task.md의 Phase 0, Phase 1 및 대응 구현 | +| 기준 commit 또는 working tree | f8a313031c397b9dd795883c698aecaa8038b575 기준, 리뷰 시작 시 기존 변경 36개 경로 | +| 리뷰 일자 | 2026-07-27 | +| 리뷰어 | Codex와 독립 판정 sub-agent | +| 기준 문서 | docs/20260725_AI캐릭터관리자웹/prd.md, api-contract.md, plan-task.md | +| 리뷰 기준 | docs/agent-guide/review.md, docs/sample/sample-review.md | +| 리뷰 상태 | 판정 완료, 후속 수정 미실행 | +| 최종 결론 | 수정 goal 필요 | + +리뷰 시작 시 워킹 트리는 사용자 작업을 포함해 이미 변경 상태였다. 기존 변경은 리뷰 기준선으로 보존했으며, 코드와 기존 문서는 수정하지 않았다. 이 파일만 신규 리뷰 산출물로 작성했다. + +리뷰 파일 위치는 같은 PRD 디렉터리를 기본값으로 삼는 일반 규칙보다 구체적인 사용자 지시를 적용해 docs/20260725_AI캐릭터관리자웹/review 아래로 정했다. + +## 2. 리뷰 목적과 범위 + +### 2.1 목적 + +- Phase 0·1의 완료 체크가 PRD, API 계약, 구현, 테스트 증거와 일치하는지 확인한다. +- 정적 코드 검토만으로 끝내지 않고 문서에 명시된 Gate와 실제 브라우저 시나리오를 실행한다. +- 통과한 자동화가 놓치는 계약, 접근 제어, 파일 처리, 접근성 회귀를 확인한다. +- 확정 문제는 수정하지 않고 재현 가능한 회귀 Task와 goal 초안까지만 남긴다. + +### 2.2 포함 범위 + +- 코드: src/app, src/features/auth, src/layouts, src/shared와 관련 구성 파일 +- 테스트: src/app/App.test.tsx, src/shared, src/features/auth, src/layouts, tests/e2e +- 문서: prd.md, api-contract.md, plan-task.md의 Phase 0·1 완료 조건과 누적 기록 +- 수동 검증: 실제 브라우저의 인증, 파일 처리, modal, keyboard, 접근성 동작 + +### 2.3 제외 범위 + +- Phase 2 이후 도메인 CRUD 구현 +- 백엔드 내부 구현과 운영 데이터 +- 확정 문제의 코드 수정 +- 기존 PRD, API 계약, 계획 문서의 직접 수정 +- 디자인 정성 평가와 실제 보조기기 수동 테스트 + +## 3. 판정 기준 + +### 3.1 심각도 + +| 심각도 | 기준 | +| --- | --- | +| Blocker | Phase 완료 판정이나 출고 판단을 무효화하거나 핵심 보호 경계를 신뢰할 수 없게 만드는 문제 | +| High | 권한, 핵심 계약, 주요 사용자 흐름 또는 파일 결과를 잘못 처리하는 문제 | +| Medium | 접근성, 중복 요청, 컴포넌트 상호작용 등 중요한 품질 계약 위반 | +| Low | 현재 영향은 제한적이지만 회귀 방지나 문서·검증 신뢰도를 약화하는 문제 | + +### 3.2 상태 + +| 상태 | 의미 | 후속 처리 | +| --- | --- | --- | +| 후보 | 근거를 발견했지만 아직 재현·판정하지 않음 | 검증 후 상태 변경 | +| 확정 | 코드, 테스트, 문서 또는 실행으로 문제가 확인됨 | 회귀 수정 Task·goal 후보 | +| 오탐 | 요구사항이나 실행 결과상 문제가 아님 | 근거를 남기고 종료 | +| 보류 | 외부 계약·환경·제품 결정이 필요함 | 담당 주체와 재개 조건 기록 | +| 수정 완료 | 수정과 관련 검증이 완료됨 | 명령과 결과 연결 | + +## 4. 검토한 근거 + +### 4.1 문서와 코드 + +- 요구사항: prd.md의 프로젝트 기반, AUTH, FILE, ERROR, 접근성 및 성공 기준 +- API 계약: api-contract.md의 인증, 관리자 목록 probe, 이미지·음성 파일 계약 +- 계획: plan-task.md의 P0-T1~P0-GATE, P1-T1~P1-GATE와 누적 구현 기록 +- 코드: src/app, src/features/auth, src/layouts, src/shared 및 Vite·Playwright·test 구성 +- 테스트: App unit, shared·auth·layout unit, smoke·auth·accessibility E2E + +### 4.2 실행 환경 + +| 항목 | 값 | +| --- | --- | +| OS | macOS 26.0, Darwin 25.0 x86_64 | +| Node.js | v24.12.0 | +| npm | 11.7.0 | +| Playwright | 1.61.1 | +| 브라우저 프로젝트 | Chromium, Mobile Chrome, WebKit, Mobile Safari | +| API mode | 저장소의 .env.development 및 .env.production | + +8888 포트에는 리뷰 시작 전부터 같은 저장소의 Vite 서버가 실행 중이었다. 공식 E2E는 Playwright의 기존 서버 재사용 설정으로 수행했고, 별도로 8889 포트에 새 Vite 서버를 띄워 루트 접근, /login 이동, html lang=ko, main landmark 1개, 제목 관리자 로그인을 다시 확인한 뒤 해당 서버만 종료했다. + +### 4.3 실행한 자동 검증 + +| 명령 | 종료 | 결과 | +| --- | ---: | --- | +| npm ci | 0 | 307 packages 설치 | +| npx playwright install chromium webkit | 0 | 대상 브라우저 설치 확인 | +| npm run typecheck | 0 | 오류 0건, 최종 재실행도 통과 | +| npm run lint | 0 | 오류 0건, 최종 재실행도 통과 | +| npm run test:run -- src/app/App.test.tsx src/shared/config/env.test.ts | 0 | 2 files, 14 tests 통과 | +| npm run e2e -- tests/e2e/smoke.spec.ts | 1 | 최초 sandbox 실행에서 127.0.0.1:8888 listen EPERM으로 실행 불가 | +| npm run e2e -- tests/e2e/smoke.spec.ts | 0 | 권한 허용 후 4 projects, 4 tests 통과 | +| npm run build:dev | 0 | 157 modules, JS 292.99 kB, gzip 88.18 kB | +| npm run build:prod | 0 | 157 modules, JS 292.99 kB, gzip 88.18 kB | +| npm run test:run -- src/shared src/features/auth src/layouts | 0 | 25 files, 79 tests 통과 | +| npm run e2e -- tests/e2e/auth.spec.ts tests/e2e/accessibility-shell.spec.ts | 0 | 4 projects, 16 tests 통과 | +| npm run build | 0 | production build, 157 modules | +| npm run test:run | 0 | 전체 27 files, 96 tests 통과 | +| npx vitest list src/shared src/features/auth src/layouts | 0 | 79 tests 목록화, src/app/App.test.tsx는 포함되지 않음 | +| npm exec vite -- --host 127.0.0.1 --port 8889 --strictPort | 검증 후 종료 | 새 서버 ready 687ms, 루트 접근 뒤 /login 이동 확인 | +| node --input-type=module -e Playwright 루트 셸 검증 스크립트 | 0 | url=/login, lang=ko, main=1, heading=관리자 로그인 | +| git diff --check | 0 | 공백 오류 없음 | + +Phase 1 접근성 E2E에는 320×640 viewport와 CSS zoom 2 조건, axe critical·serious 위반 0건 확인이 포함돼 있다. 자동화는 모두 최종 통과했지만, 아래 실제 재현 항목은 현재 테스트가 검사하지 않아 통과 결과만으로 완료 조건을 충족했다고 볼 수 없다. + +### 4.4 실제 브라우저·런타임 재현 + +임시 Playwright 및 브라우저 모듈 스크립트는 파일을 만들지 않고 실행했다. + +| 시나리오 | 실제 결과 | 판정 연결 | +| --- | --- | --- | +| 저장된 ADMIN 세션 + 목록 probe를 1.5초 뒤 403으로 응답 | 403 전 /ai-characters에서 보호 셸과 main이 보였고, 응답 뒤 /access-denied로 이동 | REV-P1-002 | +| 로그인 400 응답에 한국어 message 제공 | 화면에는 서버 message 대신 로그인에 실패했습니다. 표시 | REV-P1-006 | +| 지연된 logout 응답 중 버튼 double click | POST /member/logout 2회 발생 | REV-P1-007 | +| 1200×600 이미지를 1:1, no-upscale로 처리 | 800×800 결과, 하단 픽셀 alpha 0으로 200px 투명 영역 발생 | REV-P1-003 | +| 음성 확장자·MIME 불일치 조합 | mp3+audio/mp4, aac+audio/mpeg, m4a+audio/aac가 모두 허용됨 | REV-P1-004 | +| ConfirmDeactivateDialog를 외부 버튼에서 열기 | 초기 focus가 외부에 남고 Tab으로 dialog 밖 이동 | REV-P1-005 | +| FileField error 렌더링 | aria-describedby와 alert는 있으나 aria-invalid는 null | REV-P1-008 | +| AdminAudioPlayer 속도 select에서 Enter | 재생 토글이 호출됨 | REV-P1-009 | + +## 5. 발견 사항 요약 + +| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal | +| --- | --- | --- | --- | --- | --- | +| REV-P1-001 | Blocker | 수정 완료 | Phase 1 완료 표시가 누락 산출물·통합·Gate 증거와 충돌 | R1.1 | P1-R1 | +| REV-P1-002 | High | 수정 완료 | stale ADMIN 확인 전 보호 셸 노출과 계약 밖 probe size 사용 | R1.3 | P1-R3 | +| REV-P1-003 | High | 수정 완료 | 이미지 crop이 잘못된 크기·투명 영역을 생성 | R1.4 | P1-R4 | +| REV-P1-004 | High | 수정 완료 | 음성 확장자와 MIME의 허용 조합을 강제하지 않음 | R1.4 | P1-R4 | +| REV-P1-006 | High | 수정 완료 | 로그인 서버 한국어 오류 message를 일반 문구로 폐기 | R1.3 | P1-R3 | +| REV-P1-010 | High | 수정 완료 | image 10MB의 정확한 byte 계약을 확정하지 않고 완료 처리 | R1.2 | P1-R2 | +| REV-P1-005 | Medium | 수정 완료 | Phase 1 modal의 focus trap·trigger 복귀 계약 미구현 | R1.5 | P1-R5 | +| REV-P1-007 | Medium | 수정 완료 | logout 연속 입력으로 요청이 중복 전송됨 | R1.3 | P1-R3 | +| REV-P1-008 | Medium | 수정 완료 | FileField 오류 상태에 aria-invalid가 없음 | R1.5 | P1-R5 | +| REV-P1-009 | Medium | 수정 완료 | audio player 하위 control의 key가 재생 토글로 전파됨 | R1.5 | P1-R5 | +| REV-P0-001 | Low | 수정 완료 | 테스트 정리가 stubbed global과 mock 구현을 완전히 복원하지 않음 | R0.1 | P0-R1 | +| REV-P0-002 | Low | 수정 완료 | 루트 셸 unit 회귀 조건 중 main과 root content 검증이 없음 | R0.1 | P0-R1 | +| REV-P0-003 | Low | 수정 완료 | Playwright URL 주입·문서 링크 체크와 실제 구성·기록이 불일치 | R0.1 | P0-R1 | +| REV-P0-004 | Low | 오탐 | ignored mise.toml만으로 fresh setup 실패를 확정할 수 없음 | 없음 | 없음 | + +수정 후 상태는 수정 완료 13건, 오탐 1건이다. 열린 Blocker·High·Medium 항목은 없다. + +## 6. 발견 사항 상세 + +### REV-P1-001 — Phase 1 완료 표시가 필수 산출물·통합·Gate 증거와 충돌 + +- 심각도: Blocker +- 상태: 확정 +- 관련 요구사항: Phase 1 완료 조건, 공유 UI 산출물·소비 계약 +- 관련 계약: 없음 +- 소유 Task: 신규 R1.1, goal P1-R1 +- 근거: + - plan-task.md:275-279에서 Phase 1을 완료로 표시한다. + - 최초 plan-task.md는 src/shared/ui/page-header.tsx를 산출물·소비 관계로 선언했지만 해당 파일은 존재하지 않았다. + - 최초 plan-task.md는 모든 Task 1.5 컴포넌트를 독립 테스트하고 Admin shell에서 최소 1회 조합했다고 완료 처리했다. + - 실제 앱 조합 지점인 src/app/admin-pages.tsx:1-35에서는 PageState만 사용하고 나머지 Task 1.5 공유 컴포넌트는 앱·feature·layout에서 소비되지 않는다. + - Task 1.5 구현 기록과 plan-task.md:438-454의 P1-GATE 종합 실행 기록이 없다. + - P1-GATE 명령의 대상은 src/shared, src/features/auth, src/layouts라서 보호 route와 셸을 검증하는 src/app/App.test.tsx 11개가 제외된다. 실제 test list에서도 제외를 확인했다. +- 재현 또는 검증: + - page-header.tsx 존재 여부와 전체 import를 검색하고, P1-GATE에 App test가 포함되는지 비교한다. +- 테스트 근거: + - 최초 Phase 1 명령이 나열한 79 tests에는 src/app/App.test.tsx의 11 tests가 없고, PageHeader 또는 모든 Task 1.5 컴포넌트의 shell 소비를 증명하는 test도 없었다. +- 영향: + - Phase 1 완료 여부와 다음 Phase 진입 판단을 신뢰할 수 없다. + - 누락된 통합 및 보호 route 회귀가 Gate 통과로 오인된다. +- 권고: + - 기존 완료 체크를 되돌리지 말고 P1-R1 회귀 Task를 추가한다. + - PageHeader 계약을 구현하거나 폐기 결정을 문서화하고, 현재 shell에서 실제 소비되는 공유 컴포넌트의 통합 증거를 남긴다. + - P1-GATE에 src/app 검증을 포함하고 누적 실행 기록을 추가한 뒤에만 회귀 goal을 완료한다. +- 판정 기록: + - 2026-07-27 — 파일 존재, import 소비, test list, 누적 기록을 교차 확인해 완료 판정을 무효화하는 Blocker로 확정했다. + - 2026-07-27 — `PageHeader`는 구현·소비가 없고 현재 필요한 반복 소비 컴포넌트가 아니므로 신규 구현하지 않고 Phase 1 shared 산출물에서 제거하기로 결정했다. 현재 Admin shell 통합 증거는 실제 소비 중인 `PageState`와 `src/app/App.test.tsx` 포함 Gate로 확인한다. + - 2026-07-27 — P1-R2 완료 후 closure Gate를 재실행했고 `PageHeader` 폐기 결정과 `PageState` shell 소비 증거를 plan-task.md에 누적해 수정 완료로 닫았다. + +### REV-P1-002 — stale ADMIN 판정 전 보호 셸 노출과 계약 밖 probe size + +- 심각도: High +- 상태: 확정 +- 관련 요구사항: AUTH-003, AUTH-006, 오류·권한 처리 +- 관련 계약: api-contract.md:52-54, 63-65 +- 소유 Task: 신규 R1.3, goal P1-R3 +- 근거: + - src/app/App.tsx:172-211은 보호 목록 probe를 비동기로 시작한다. + - src/app/App.tsx:225-233은 저장된 role이 ADMIN이면 probe 응답 전에 ProtectedAdminShell을 즉시 렌더링한다. + - 1.5초 지연 403 재현에서 응답 전 보호 heading과 main이 보였고, 응답 후에야 /access-denied로 이동했다. + - prd.md:160-176, 588-590과 api-contract.md:52-54는 stale ADMIN의 403 차단을 요구한다. +- 재현 또는 검증: + - sessionStorage에 ADMIN 세션을 넣고 관리자 목록 요청을 1.5초 뒤 403으로 응답시켜 응답 전후 URL과 보호 DOM을 비교한다. +- 테스트 근거: + - src/app/App.test.tsx:139-149는 403 뒤 최종 이동만 확인하며 응답 전 보호 DOM 비노출과 query size를 검사하지 않는다. +- 영향: + - 서버가 권한을 거부한 세션에서도 보호 화면이 일시 노출된다. 현재 Phase 1 셸은 빈 상태지만 이후 데이터가 연결되면 노출 범위가 커진다. +- 추가 관찰: + - 이 finding의 확정 범위에는 probe가 size=1을 사용해 prd.md:580의 허용 최소 size 20을 위반하는 문제도 포함한다. +- 추가 영향: + - 공통 pagination 요청 정책을 우회하고 서버가 size를 최소 20으로 보정하므로 1건만 조회한다는 probe 의도가 성립하지 않는다. +- 권고: + - 권한 확인 중에는 보호 셸을 렌더링하지 않는 명시적 pending 상태를 둔다. + - probe size를 20으로 맞추거나 공통 pagination helper를 사용한다. + - 지연 403 전후 DOM을 검사하는 회귀 테스트를 먼저 추가한다. +- 판정 기록: + - 2026-07-27 — 지연 403 전 보호 셸 노출을 실제 브라우저에서 재현하고 size query를 계약과 대조해 두 문제를 하나의 인증 probe finding으로 확정했다. + +### REV-P1-003 — 이미지 crop 결과 계산과 렌더링 오류 + +- 심각도: High +- 상태: 확정 +- 관련 요구사항: 이미지 crop·자유 비율·pointer 조작·성공 기준 +- 관련 계약: api-contract.md:121-136 +- 소유 Task: 신규 R1.4, goal P1-R4 +- 근거: + - src/shared/lib/crop-image.ts:26-29는 sourceWidth만으로 출력 크기를 계산하고 sourceHeight와 선택 crop 영역을 사용하지 않는다. + - src/shared/lib/crop-image.ts:45-47은 source crop rectangle 없이 원본 전체를 출력 canvas에 그린다. + - 1200×600 입력을 1:1 no-upscale로 처리하면 올바른 최대 정사각형은 600×600이어야 하나 800×800이 생성됐고 하단 200px이 투명했다. + - src/shared/ui/image-crop-dialog.tsx:13-17은 숫자 aspect만 받고 자유 비율 선택을 제공하지 않는다. + - src/shared/ui/image-crop-dialog.tsx:56-84에는 keyboard 조작만 있고 pointer drag·pinch 조작이 없다. + - prd.md:323-331, 342-344, 476, 701-706과 api-contract.md:121-136의 crop 결과·조작 계약과 충돌한다. +- 재현 또는 검증: + - 1200×600 단색 PNG를 1:1, max 800, no-upscale로 처리하고 출력 dimensions와 상·하단 alpha를 읽는다. +- 테스트 근거: + - src/shared/lib/crop-image.test.ts:6은 현재 잘못된 800×800 결과를 기대하고 실제 pixel·alpha를 검사하지 않는다. + - src/shared/ui/__tests__/image-crop-dialog.test.tsx는 정사각형 입력과 button·keyboard만 다루며 자유 비율과 pointer 조작을 검사하지 않는다. +- 영향: + - 업로드 이미지의 실제 내용, 비율, 크기와 투명도가 잘못될 수 있다. +- 권고: + - source crop rectangle을 단일 진실 원천으로 계산·렌더링한다. + - 실제 픽셀과 alpha를 확인하는 비정사각형 이미지 회귀 테스트를 먼저 추가한다. + - 자유 비율과 pointer 조작을 동일 상태 모델에 연결한다. +- 판정 기록: + - 2026-07-27 — 1200×600 입력에서 800×800 출력과 하단 alpha 0을 재현하고 UI 조작 경로를 대조해 확정했다. + +### REV-P1-004 — 음성 확장자·MIME 허용 조합 미검증 + +- 심각도: High +- 상태: 확정 +- 관련 요구사항: FILE-013 +- 관련 계약: api-contract.md:492-500 +- 소유 Task: 신규 R1.4, goal P1-R4 +- 근거: + - src/shared/validation/audio-file-policy.ts:4-22는 허용 확장자 집합과 MIME 집합을 독립적으로 검사하고 x-m4a만 별도 처리한다. + - 실제 실행에서 voice.mp3+audio/mp4, voice.aac+audio/mpeg, voice.m4a+audio/aac가 모두 ok=true였다. + - prd.md:332와 api-contract.md:492-500은 형식별 확장자·MIME 쌍을 정의한다. +- 재현 또는 검증: + - 브라우저 모듈에서 위 세 File 조합을 audio 정책 함수에 전달하고 반환값을 기록한다. +- 테스트 근거: + - src/shared/validation/file-media-policy.test.ts:39-54는 정상 조합과 x-m4a 예외만 다루며 canonical MIME이 다른 확장자와 결합된 거부 조건은 없다. +- 영향: + - 파일 내용 형식과 이름이 불일치해도 클라이언트 사전 검증을 통과한다. +- 권고: + - 명시적 확장자↔MIME 조합표를 단일 정책으로 만들고 허용·거부 table test를 먼저 추가한다. +- 판정 기록: + - 2026-07-27 — 계약상 불일치하는 세 조합이 모두 허용되는 것을 런타임 모듈 실행으로 재현해 확정했다. + +### REV-P1-005 — modal focus trap·trigger 복귀 계약 미구현 + +- 심각도: Medium +- 상태: 확정 +- 관련 요구사항: modal keyboard·focus 접근성 +- 관련 계약: 없음 +- 소유 Task: 신규 R1.5, goal P1-R5 +- 근거: + - src/shared/ui/confirm-deactivate-dialog.tsx:9-33과 image-crop-dialog.tsx:31-130에는 focus trap과 trigger focus return 처리가 없다. + - src/shared/ui/unsaved-changes-guard.tsx:21-25는 취소 시 trigger focus 복귀만 부분 구현한다. + - 실제 ConfirmDeactivateDialog 재현에서 초기 focus가 외부 버튼에 남았고 Tab으로 dialog 밖으로 이동했다. + - prd.md:509-511은 keyboard-only dialog 사용, focus trap, 닫힌 후 trigger 복귀를 요구한다. + - initial focus와 Escape close도 현재 구현에 없지만 PRD의 명시 문구가 아니므로 이번 확정 판정의 근거에는 포함하지 않는다. +- 재현 또는 검증: + - 외부 trigger에 focus한 상태에서 dialog를 열고 activeElement, Tab 순환과 닫은 뒤 focus를 순서대로 관찰한다. +- 테스트 근거: + - confirm-deactivate-dialog.test.tsx는 copy와 click callback만, image-crop-dialog.test.tsx는 control 동작만 검사한다. + - unsaved-changes-guard.test.tsx의 취소 focus 복귀 외에는 세 modal 공통 focus trap·복귀 test가 없다. +- 영향: + - keyboard와 보조기기 사용자가 modal 맥락을 잃거나 배경 UI로 이동할 수 있다. +- 권고: + - 세 modal의 focus trap과 trigger 복귀를 테스트로 먼저 고정하고 가장 작은 공통 primitive 또는 각 컴포넌트의 직접 구현으로 충족한다. +- 판정 기록: + - 2026-07-27 — 초기 focus와 Tab 이동을 실제 브라우저에서 확인하고 세 구현을 정적으로 대조해 확정했다. + +### REV-P1-006 — 로그인 서버 오류 message 폐기 + +- 심각도: High +- 상태: 확정 +- 관련 요구사항: 로그인 오류 처리, 서버 한국어 message 우선 +- 관련 계약: 공통 API error envelope +- 소유 Task: 신규 R1.3, goal P1-R3 +- 근거: + - src/shared/api/client.ts:103-113은 ApiError의 서버 message를 보존한다. + - src/features/auth/pages/LoginPage.tsx:65-70은 모든 Error를 로그인에 실패했습니다.로 치환한다. + - 서버가 이메일 또는 비밀번호가 올바르지 않습니다.를 반환한 400 재현에서도 일반 문구만 표시됐다. + - prd.md:578, 588은 서버 한국어 message 우선 표시를 요구한다. +- 재현 또는 검증: + - login API를 한국어 message가 있는 400으로 intercept하고 화면 alert text를 응답 message와 비교한다. +- 테스트 근거: + - src/features/auth/tests/login-page.test.tsx:62-74는 실패 후 button 재활성화만 확인하며 rejection message 표시를 검사하지 않는다. +- 영향: + - 사용자가 서버가 제공한 구체적 복구 정보를 받지 못하고 프런트·백엔드 계약이 끊긴다. +- 권고: + - ApiError message를 우선 사용하고 message가 없을 때만 안전한 fallback을 사용한다. +- 판정 기록: + - 2026-07-27 — 400 응답의 서버 message와 화면 표시를 비교해 소실을 확정했다. + +### REV-P1-007 — logout 중복 요청 + +- 심각도: Medium +- 상태: 확정 +- 관련 요구사항: logout 단일 요청 +- 관련 계약: api-contract.md:210 +- 소유 Task: 신규 R1.3, goal P1-R3 +- 근거: + - src/app/App.tsx:130의 logout 버튼에는 pending disable이 없다. + - src/features/auth/model/auth-session.tsx:40-57에는 in-flight 요청을 합치거나 막는 guard가 없다. + - 지연 응답 중 double click 재현에서 POST /member/logout이 2회 발생했다. + - prd.md:659, api-contract.md:210과 plan-task.md:380은 logout API 한 번 호출을 요구한다. +- 재현 또는 검증: + - logout 응답을 지연시키고 버튼을 double click한 뒤 route handler 호출 수를 계수한다. +- 테스트 근거: + - src/features/auth/tests/auth-session.test.tsx:133-169는 각 응답별 single click 결과만 확인하며 in-flight 연속 입력의 request 수를 검사하지 않는다. +- 영향: + - 중복 네트워크 요청과 중복 오류 처리가 발생할 수 있다. +- 권고: + - session layer의 단일 in-flight 보장과 버튼 pending 상태를 회귀 테스트로 고정한다. +- 판정 기록: + - 2026-07-27 — 지연 응답 중 double click으로 logout POST 2회를 계수해 확정했다. + +### REV-P1-008 — FileField 오류 상태의 aria-invalid 누락 + +- 심각도: Medium +- 상태: 확정 +- 관련 요구사항: 필드 오류 접근성 +- 관련 계약: 없음 +- 소유 Task: 신규 R1.5, goal P1-R5 +- 근거: + - src/shared/ui/file-field.tsx:38, 45는 오류 설명 연결과 alert를 제공하지만 input에 aria-invalid를 설정하지 않는다. + - 실제 오류 렌더링에서 aria-invalid 값은 null이었다. + - prd.md:490은 필드 오류 시 aria-invalid와 설명 연결을 함께 요구한다. +- 재현 또는 검증: + - error prop이 있는 FileField를 렌더링하고 input의 aria-invalid, aria-describedby와 alert를 읽는다. +- 테스트 근거: + - src/shared/ui/__tests__/file-field.test.tsx:6-27은 accessible description을 확인하지만 aria-invalid assertion이 없다. +- 영향: + - 보조기기가 파일 입력의 유효하지 않은 상태를 일관되게 전달하지 못한다. +- 권고: + - error 존재 여부를 aria-invalid에 연결하고 접근성 이름·설명·상태를 함께 검증한다. +- 판정 기록: + - 2026-07-27 — error 상태 input의 aria-invalid=null을 실제 렌더링으로 확인해 확정했다. + +### REV-P1-009 — audio player 하위 control key 전파 + +- 심각도: Medium +- 상태: 확정 +- 관련 요구사항: keyboard-only 조작과 control event 경계 +- 관련 계약: 없음 +- 소유 Task: 신규 R1.5, goal P1-R5 +- 근거: + - src/shared/ui/admin-audio-player.tsx:58-63의 group key handler가 event target을 구분하지 않고 Enter·Space를 재생 토글로 처리한다. + - 속도 select에 focus한 뒤 Enter를 누른 실제 재현에서 play가 1회 호출됐다. +- 재현 또는 검증: + - 속도 select에 focus하고 Enter를 누른 뒤 mocked audio play 호출 수를 확인한다. +- 테스트 근거: + - src/shared/ui/__tests__/admin-audio-player.test.tsx:58-77은 group 자체에 Space를 보내며 하위 combobox·button의 key 격리를 검사하지 않는다. +- 영향: + - select나 button을 keyboard로 조작할 때 의도하지 않은 재생·정지가 함께 발생한다. +- 권고: + - container 자체가 target일 때만 shortcut을 처리하거나 descendant interactive control을 명시적으로 제외한다. +- 판정 기록: + - 2026-07-27 — 속도 select의 Enter 입력이 play를 호출하는 것을 실제 브라우저에서 재현해 확정했다. + +### REV-P1-010 — image 10MB exact byte 계약 미확정 상태로 완료 처리 + +- 심각도: High +- 상태: 확정 +- 관련 요구사항: image 최대 용량과 첫 파일 Phase 계약 확정 +- 관련 계약: api-contract.md:119 +- 소유 Task: 신규 R1.2, goal P1-R2 +- 근거: + - plan-task.md:101은 첫 파일 Phase에서 모호한 10MB를 정확한 byte 값으로 확정하도록 요구한다. + - plan-task.md:426은 백엔드와 값을 맞추고 문서·경계 테스트에 기록했다고 완료 처리한다. + - 최초 prd.md:320과 api-contract.md:119에는 10MB만 있고 10,000,000인지 10,485,760인지 확정 근거가 없었다. + - src/shared의 10×1024×1024 테스트 값은 주입 예시일 뿐 채택된 백엔드 계약 증거가 아니다. +- 재현 또는 검증: + - 계획의 완료 체크와 PRD, API 계약, 구현 상수, 경계 테스트에서 exact byte와 출처를 검색해 서로 비교한다. +- 테스트 근거: + - src/shared/validation/file-media-policy.test.ts:27-37의 10×1024×1024는 caller 주입 예시이며 채택된 image domain 상수나 backend 계약 출처를 검증하지 않는다. +- 영향: + - 경계 파일에서 프런트와 백엔드의 허용 결과가 달라질 수 있고 완료 체크를 재현할 수 없다. +- 외부 의존성: + - 이 finding에서 확정한 문제는 exact byte 근거가 없는데도 완료 처리한 사실이다. + - 정확한 숫자 값 자체는 이번 판정 대상이 아니며 백엔드 소유자 확인 전 추정하지 않는다. 값이 확인되기 전에는 R1.2를 완료할 수 없다. +- 권고: + - 백엔드 exact byte를 확인한 뒤 PRD, API 계약, 구현 상수, 경계 테스트를 같은 Task에서 정렬한다. +- 판정 기록: + - 2026-07-27 — 완료 체크와 PRD·API 계약·테스트 근거를 비교해 근거 없는 완료 처리를 확정했다. 실제 숫자 값은 백엔드 확인이 필요한 외부 입력으로 분리했다. + - 2026-07-27 — 백엔드 exact byte를 `10,485,760 bytes`로 확정하고 PRD, API 계약, `IMAGE_MAX_BYTES`, `limit-1/limit/limit+1` 경계 테스트를 같은 값으로 정렬했다. + +### REV-P0-001 — 공통 테스트 정리의 global·mock 복원 누락 + +- 심각도: Low +- 상태: 확정 +- 관련 요구사항: Phase 0 test isolation +- 관련 계약: 없음 +- 소유 Task: 신규 R0.1, goal P0-R1 +- 근거: + - src/shared/test/setup.ts:11-17은 vi.clearAllMocks()만 호출한다. + - src/features/auth/tests/auth-session.test.tsx:78은 indexedDB를 stubGlobal로 교체하지만 공통 정리에 unstubAllGlobals가 없다. + - clearAllMocks는 호출 이력만 지우고 stubbed global이나 mock 구현을 복원하지 않는다. + - plan-task.md:228은 각 테스트 뒤 DOM, mock, storage가 깨끗해야 한다고 명시한다. +- 재현 또는 검증: + - 공통 afterEach 호출과 stubGlobal 사용 지점을 대조하고 후속 테스트 전에 unstub 경로가 있는지 검색한다. +- 테스트 근거: + - 전체 96 tests는 통과하지만 stubbed indexedDB가 후속 test 전에 원복되는지를 직접 증명하는 격리 test가 없다. +- 영향: + - 같은 파일의 후속 테스트가 이전 global 또는 mock 구현에 의존할 수 있다. 현재 Vitest 파일 격리가 파급을 줄이지만 계약 공백은 남는다. +- 권고: + - 누출을 증명하는 실패 테스트를 먼저 만들고 restoreAllMocks·unstubAllGlobals 등 필요한 최소 정리를 추가한다. +- 판정 기록: + - 2026-07-27 — 공통 cleanup과 실제 stubGlobal 사용을 대조해 복원 공백을 확정했다. + +### REV-P0-002 — 루트 셸 unit 회귀 조건 누락 + +- 심각도: Low +- 상태: 확정 +- 관련 요구사항: Phase 0 root shell +- 관련 계약: 없음 +- 소유 Task: 신규 R0.1, goal P0-R1 +- 근거: + - plan-task.md:224, 231은 root unit에서 html lang, main landmark, root content를 검증하도록 요구한다. + - src/app/App.test.tsx:46-50의 root test는 lang만 확인한다. + - tests/e2e/smoke.spec.ts:3-8도 lang과 login heading은 확인하지만 heading이 main 안에 있는지 검증하지 않는다. +- 재현 또는 검증: + - Phase 0 계획의 root assertion 목록을 App root test와 smoke test의 실제 assertion에 하나씩 대응한다. +- 테스트 근거: + - src/app/App.test.tsx:46-50은 lang만, tests/e2e/smoke.spec.ts:3-8은 lang과 login heading만 확인해 main 안의 root content 관계가 비어 있다. +- 영향: + - 루트 main 또는 콘텐츠 배치가 깨져도 명시된 Phase 0 unit 회귀 조건이 감지하지 못한다. +- 권고: + - root render에서 main과 대표 콘텐츠의 포함 관계를 직접 검증한다. +- 판정 기록: + - 2026-07-27 — 계획의 세 assertion과 root unit·smoke assertion을 대조해 누락을 확정했다. + +### REV-P0-003 — Phase 0 Playwright 구성·기록 불일치 + +- 심각도: Low +- 상태: 확정 +- 관련 요구사항: Phase 0 환경·검증 재현성 +- 관련 계약: test API base URL +- 소유 Task: 신규 R0.1, goal P0-R1 +- 근거: + - plan-task.md:215는 Playwright webServer가 test API URL을 명시적으로 주입한다고 완료 처리한다. + - playwright.config.ts:5-9의 command는 npm run dev이며 env를 주입하지 않고 .env.development 로딩에 의존한다. + - plan-task.md:267은 다시 mode 파일 사용을 현재 전략으로 기록해 같은 계획 안에서도 설명이 다르다. + - 최초 plan-task.md:265가 연결한 docs/agent-guide/README.md는 현재 존재하지 않았다. +- 재현 또는 검증: + - config의 webServer command와 mode 파일을 읽고 계획의 두 기록을 비교한 뒤 링크 대상 파일 존재를 검사한다. +- 테스트 근거: + - smoke 4 projects와 dev·prod build는 현재 mode URL 동작만 확인하며 명시 env 주입 또는 문서 링크 존재를 검증하지 않는다. +- 영향: + - 체크만 보고는 E2E API 대상과 Phase 0 증거 문서를 재현하기 어렵다. 현재 mode별 URL 자체는 정상이다. +- 권고: + - 채택 전략을 하나로 명시하고 실행 가능한 링크·명령과 일치시키는 보정 기록을 기존 완료 체크 아래에 추가한다. +- 판정 기록: + - 2026-07-27 — 현재 config, 계획 체크, 후속 기록과 문서 경로를 비교해 문서·증거 정합성 문제로 확정했다. + - 2026-07-27 — plan-task.md의 존재하지 않는 `docs/agent-guide/README.md` 참조를 실제 `docs/agent-guide/` 세부 문서 관리 방식으로 정정했다. + +### REV-P0-004 — ignored mise.toml이 fresh setup을 깨뜨린다는 후보 + +- 심각도: Low +- 상태: 오탐 +- 관련 요구사항: Phase 0 fresh setup +- 관련 계약: 없음 +- 소유 Task: 없음 +- 근거: + - mise.toml은 ignore 대상이라 저장소만 새로 받은 환경에는 포함되지 않을 수 있다. + - 그러나 README가 Node.js 24.12.0을 명시하므로 mise.toml 부재만으로 Phase 0 fresh setup 실패를 확정할 수 없다. +- 재현 또는 검증: + - ignore 상태와 README의 설치 전제 문서를 함께 확인한다. +- 테스트 근거: + - 별도 test 대상은 아니며 README의 Node 24.12.0 설치 전제가 대체 재현 경로다. +- 결론: + - 현재 계약 위반으로 확정하지 않으며 회귀 Task를 만들지 않는다. +- 판정 기록: + - 2026-07-27 — README의 Node 24.12.0 명시가 대체 재현 경로를 제공하므로 오탐으로 종료했다. + +## 7. 회귀 수정 Task와 goal 초안 + +아래 내용은 최초 리뷰 당시 plan-task.md에 반영하지 않은 초안이다. 2026-07-27 수정 실행 후 실제 수행·보류 상태는 plan-task.md에 누적했고, 이 절의 체크박스는 해당 실행 결과와 맞춰 정정한다. 기존 완료 체크는 되돌리지 않고, 후속 수정 시 해당 Phase 아래에 회귀 Task와 누적 검증 기록으로 추가한다. + +### Task R0.1 — 테스트 격리와 Phase 0 증거 정합성 복구 + +**Goal 실행 `P0-R1`:** Phase 0 테스트 격리와 루트 셸·Playwright 증거 정합성을 복구하고 회귀를 방지한다. + +- 연결 리뷰: REV-P0-001, REV-P0-002, REV-P0-003 +- 시작 조건: + - 세 리뷰 ID와 현재 재현 근거를 Task 본문에 연결한다. + - 기존 사용자 변경과 리뷰 기준 커밋을 보존한다. +- 완료 증거: + - stubbed global 누출을 보여주는 실패 테스트 + - root render의 main과 대표 content 실패 테스트 + - 채택한 Playwright API URL 전략과 실제 config가 일치한다는 검증 + - focused test, P0-GATE, smoke 4 projects 결과와 누적 기록 +- 범위 밖: + - Phase 1 인증 동작 변경 + - 전체 테스트 프레임워크 교체 +- 실행 체크: + - [x] 실패하는 격리·루트 셸 테스트를 먼저 추가한다. + - [x] 공통 cleanup과 root assertion을 최소 변경으로 충족한다. + - [x] Playwright env 전략과 끊어진 문서 링크를 보정 기록으로 정렬한다. + - [x] focused test와 P0-GATE를 실행한다. + - [x] 명령, 종료 코드, 테스트 수를 plan-task.md에 누적 기록한다. + +#### create_goal objective 초안 — P0-R1 + +- objective: P0-R1의 확정 review 항목 REV-P0-001, REV-P0-002, REV-P0-003을 수정하고 회귀를 방지한다. plan-task.md에 추가할 Task R0.1만 수행한다. +- 완료 조건: 위 완료 증거가 모두 존재하고 P0-GATE가 통과하며 plan-task.md에 누적 기록이 추가된 상태다. +- 금지 조건: 기존 완료 체크를 해제하거나 관련 없는 리팩터링을 포함하지 않는다. +- 중단 조건: 채택할 env 전략이 기존 PRD와 충돌하면 임의 결정하지 않고 문서 소유자 확인 상태를 기록한다. + +### Task R1.1 — Phase 1 산출물·통합·Gate 완료 증거 복구 + +**Goal 실행 `P1-R1`:** Phase 1 완료 판정을 재현 가능한 산출물·통합·Gate 증거로 복구한다. + +- 연결 리뷰: REV-P1-001 +- 시작 조건: + - P0-R1과 P1-R2~P1-R5의 수정·검증 기록이 완료돼 Phase 1 closure Gate를 실행할 수 있다. + - PageHeader 폐기 결정과 공유 UI 소비 계약을 PRD·계획 기준으로 확인한다. + - 현재 P1-GATE가 src/app을 제외한다는 실패 증거를 보존한다. +- 완료 증거: + - PageHeader 계약의 승인된 폐기 결정 기록 + - 선언된 Task 1.5 컴포넌트의 실제 Admin shell 소비 테스트 + - src/app/App.test.tsx를 포함하는 P1-GATE + - Task 1.5 구현 기록과 P1-GATE 누적 실행 기록 + - REV-P1-001~010의 확정 항목에 열린 Blocker·High·Medium 상태가 없다는 closure 확인 +- 범위 밖: + - Phase 2 도메인 CRUD + - 공유 UI의 전면 재설계 +- 실행 체크: + - [x] P0-R1과 P1-R2~P1-R5의 완료 기록과 잔여 finding을 먼저 확인한다. + - [x] 누락 산출물·소비·Gate 범위를 드러내는 검증을 수행하고 `PageHeader` 폐기 결정을 문서화한다. + - [x] 승인된 계약에 맞춰 `PageHeader`를 신규 구현하지 않고 계획 산출물·소비 Matrix에서 제거한다. + - [x] focused test와 보정된 P1-GATE를 실행한다. + - [x] 320px, zoom 2, axe 결과를 다시 확인한다. + - [x] 구현·Gate 명령과 결과를 plan-task.md에 누적 기록한다. + +#### create_goal objective 초안 — P1-R1 + +- objective: P1-R1의 확정 review 항목 REV-P1-001을 수정하고 회귀를 방지한다. plan-task.md에 추가할 Task R1.1만 수행한다. +- 완료 조건: P0-R1과 P1-R2~P1-R5 완료 후 누락 산출물 결정, 실제 소비 증거, src/app 포함 Gate, 누적 기록과 열린 Blocker·High·Medium 0건이 모두 확인된다. +- 금지 조건: Phase 2 기능을 선행 구현하거나 기존 완료 체크를 되돌리지 않는다. +- 중단 조건: PageHeader를 다시 요구하는 제품 결정이 나오면 현재 폐기 결정을 되돌리지 않고 별도 후속 Task로 추가한다. + +### Task R1.2 — image 최대 용량 exact byte 계약 확정 + +**Goal 실행 `P1-R2`:** image 10MB 표현을 백엔드와 동일한 exact byte 계약으로 확정한다. + +- 연결 리뷰: REV-P1-010 +- 시작 조건: + - 백엔드 exact byte와 근거를 확보하기 전에는 이 goal을 생성하거나 시작하지 않는다. + - 백엔드 소유자가 image 최대 byte를 명시적으로 확인한다. + - 확인 전 10,000,000 또는 10,485,760을 임의 채택하지 않는다. +- 완료 증거: + - PRD와 API 계약의 exact byte 값 + - 같은 값을 사용하는 프런트 정책 상수 + - limit-1, limit, limit+1 경계 테스트 + - 실제 백엔드 계약 출처와 누적 검증 기록 +- 범위 밖: + - 이미지 압축 정책 추가 + - 백엔드 업로드 구현 변경 +- 실행 체크: + - [x] 백엔드 exact byte와 근거를 확인한다. + - [x] 현재 모호성을 드러내는 경계 테스트를 먼저 추가한다. + - [x] 문서, 상수, 테스트를 하나의 값으로 정렬한다. + - [x] focused test와 P1-GATE를 실행한다. + - [x] 확인 출처와 실행 결과를 plan-task.md에 기록한다. + +#### create_goal objective 초안 — P1-R2 + +- objective: P1-R2의 확정 review 항목 REV-P1-010을 수정하고 회귀를 방지한다. plan-task.md에 추가할 Task R1.2만 수행한다. +- 완료 조건: 소유자 확인, 두 계약 문서, 구현 상수, 세 경계 테스트, Gate 기록이 같은 값을 사용한다. +- 금지 조건: 외부 확인 없이 값을 추정하거나 audio 용량 계약까지 확장하지 않는다. +- 중단 조건: 시작 조건이 충족되지 않으면 goal을 생성하지 않고 Task에 외부 의존성·소유자·재개 조건만 기록한다. + +### Task R1.3 — 인증 보호·오류·logout 회귀 수정 + +**Goal 실행 `P1-R3`:** Phase 1 인증의 보호 경계, 오류 피드백, logout 단일 요청 계약을 복구한다. + +- 연결 리뷰: REV-P1-002, REV-P1-006, REV-P1-007 +- 시작 조건: + - 지연 403 전 보호 셸 노출, 계약 밖 size=1 probe, 서버 message 소실, logout 2회 호출을 각각 실패 테스트로 재현한다. +- 완료 증거: + - stale ADMIN probe pending·403 동안 보호 셸 비노출 + - 목록 계약 범위 안의 probe size와 request assertion + - 서버 한국어 message 우선 표시와 fallback + - logout in-flight 중 API 1회 호출 + - focused auth/App test, auth E2E, P1-GATE 실행 기록 +- 범위 밖: + - 인증 방식 또는 token 저장 방식 교체 + - Phase 2 권한 체계 추가 +- 실행 체크: + - [x] 네 회귀 조건을 각각 실패 테스트로 먼저 고정한다. + - [x] 보호 pending 상태, message 전달, 단일 in-flight를 최소 변경으로 구현한다. + - [x] probe size를 API 계약 범위로 맞춘다. + - [x] focused test, auth E2E, P1-GATE를 실행한다. + - [x] 결과를 plan-task.md에 누적 기록한다. + +#### create_goal objective 초안 — P1-R3 + +- objective: P1-R3의 확정 review 항목 REV-P1-002, REV-P1-006, REV-P1-007을 수정하고 회귀를 방지한다. plan-task.md에 추가할 Task R1.3만 수행한다. +- 완료 조건: 네 실패 조건의 테스트가 수정 후 통과하고 auth E2E와 P1-GATE가 통과하며 누적 기록이 존재한다. +- 금지 조건: 세션 아키텍처 전면 교체나 Phase 2 권한 기능을 포함하지 않는다. +- 중단 조건: 서버 message 노출 정책이 문서와 다르다는 별도 보안 결정이 확인되면 해당 결정을 먼저 문서화한다. + +### Task R1.4 — crop·음성 파일 정책 회귀 수정 + +**Goal 실행 `P1-R4`:** Phase 1 파일·미디어 입력의 crop 결과와 audio 형식 조합 계약을 복구한다. + +- 연결 리뷰: REV-P1-003, REV-P1-004 +- 시작 조건: + - 1200×600→1:1 no-upscale 픽셀 실패와 세 MIME 불일치 허용을 테스트로 재현한다. +- 완료 증거: + - source crop rectangle 기반 출력 + - 600×600 이하 no-upscale와 투명 영역 없는 픽셀 결과 + - 자유 비율과 pointer drag 조작 + - 확장자↔MIME 허용 조합 table test + - focused shared test와 P1-GATE 기록 +- 범위 밖: + - 외부 crop 라이브러리 도입 + - 서버 파일 변환 구현 +- 실행 체크: + - [x] 출력 크기·픽셀·alpha와 MIME 조합 실패 테스트를 먼저 추가한다. + - [x] crop 계산과 drawImage source rectangle을 최소 변경으로 정렬한다. + - [x] 자유 비율과 pointer 조작을 기존 dialog 상태에 연결한다. + - [x] 명시적 audio 조합표를 적용한다. + - [x] focused test와 P1-GATE를 실행하고 결과를 누적 기록한다. + +#### create_goal objective 초안 — P1-R4 + +- objective: P1-R4의 확정 review 항목 REV-P1-003, REV-P1-004를 수정하고 회귀를 방지한다. plan-task.md에 추가할 Task R1.4만 수행한다. +- 완료 조건: 실제 픽셀 회귀, pointer 조작 회귀, MIME table test, P1-GATE가 모두 통과하고 기록된다. +- 금지 조건: 새 의존성이나 서버 측 변환을 추가하지 않는다. +- 중단 조건: browser별 canvas 차이가 발견되면 허용 오차를 추정하지 않고 재현 결과와 결정 필요 항목을 기록한다. + +### Task R1.5 — 공유 UI keyboard·focus 접근성 회귀 수정 + +**Goal 실행 `P1-R5`:** Phase 1 공유 UI의 modal focus, 필드 오류 상태, keyboard event 경계를 복구한다. + +- 연결 리뷰: REV-P1-005, REV-P1-008, REV-P1-009 +- 시작 조건: + - modal focus 이탈, FileField aria-invalid 누락, audio child key 전파를 실패 테스트로 재현한다. +- 완료 증거: + - 세 modal의 focus trap과 닫힌 뒤 trigger focus return + - FileField의 aria-invalid와 설명 연결 + - audio player descendant interactive control의 Enter·Space 격리 + - focused component test, 320×640 zoom 2, axe, P1-GATE 기록 +- 범위 밖: + - 공유 UI 전체 교체 + - 시각 디자인 재작업 +- 실행 체크: + - [x] keyboard·focus 회귀 테스트를 먼저 추가한다. + - [x] 필요한 최소 focus trap·복귀 관리와 event target guard를 구현한다. + - [x] FileField 상태 속성을 보정한다. + - [x] focused test, 접근성 E2E, P1-GATE를 실행한다. + - [x] 결과를 plan-task.md에 누적 기록한다. + +#### create_goal objective 초안 — P1-R5 + +- objective: P1-R5의 확정 review 항목 REV-P1-005, REV-P1-008, REV-P1-009를 수정하고 회귀를 방지한다. plan-task.md에 추가할 Task R1.5만 수행한다. +- 완료 조건: 세 회귀 묶음과 접근성 E2E, P1-GATE가 통과하고 plan-task.md 누적 기록이 존재한다. +- 금지 조건: 접근성 primitive를 이유로 전면 UI 리팩터링을 수행하지 않는다. +- 중단 조건: 공통 modal primitive 도입이 현재 세 컴포넌트 직접 수정보다 커지면 직접 수정안을 우선 비교하고 결정 근거를 기록한다. + +## 8. 리뷰 종료 판정 + +| 판정 항목 | 결과 | 근거 | +| --- | --- | --- | +| 리뷰 범위 전체 확인 | 충족 | Phase 0·1 문서, 코드, 테스트와 완료 증거를 대조 | +| 후보 항목 판정 완료 | 충족 | 확정 13건, 오탐 1건으로 판정 | +| 확정 항목 plan 반영 | 충족 | R0.1, R1.1~R1.5 실행 기록과 closure 결과를 plan-task.md와 이 보고서에 누적 | +| 외부 조건의 담당·재개 조건 기록 | 충족 | REV-P1-010의 exact byte를 `10,485,760 bytes`로 확정하고 PRD·API·상수·경계 테스트를 정렬 | +| 검증 명령과 결과 기록 | 충족 | exit code, test 수, 실패·불가 사유, 브라우저 관찰과 closure Gate 결과를 기록 | +| Blocker·High 확정 이슈 0건 | 충족 | 수정 완료 13건, 오탐 1건이며 열린 Blocker·High·Medium 항목 없음 | +| 코드와 기존 문서 정합성 | 충족 | P1-R1/R2 closure에서 코드와 PRD·API·plan·review를 같은 계약으로 정렬 | + +초기 리뷰에서 확인된 계약 위반과 Phase 1 완료 증거 공백은 수정 goal로 정리됐다. Phase 1 완료 판정은 아래 수정 후 검증 기록의 최신 closure Gate를 기준으로 사용한다. + +**최종 결론:** 수정 완료 + +**남은 항목:** 없음. Phase 2 이후 구현과 각 도메인의 외부 의존 계약은 기존 계획 범위대로 후속 Phase에서 다룬다. + +## 9. 수정 후 검증 기록 + +기존 기록을 삭제하거나 덮어쓰지 않고 차수별로 누적한다. + +### 수정 검증 미수행 — 2026-07-27 + +- 무엇을: 코드와 기존 문서는 수정하지 않았고 회귀 Task·goal 초안만 작성했다. +- 왜: 확정 문제를 수정하지 말라는 리뷰 범위 지시를 준수했다. +- 어떻게: + - 수정 전 기준 검증은 4절의 명령과 실제 브라우저 재현으로 완료했다. + - 수정 후 focused test와 Phase Gate는 아직 실행 대상이 아니다. +- 남은 항목: REV-P0-001~003, REV-P1-001~010 중 확정 13건과 P0-R1, P1-R1~R5 실행. + +### 수정 검증 1차 — 2026-07-27 + +- 무엇을: `P0-R1`, `P1-R3`, `P1-R4`, `P1-R5`를 구현하고 검증했다. 당시 `P1-R2`는 백엔드 exact byte 확인 전 시작 금지 조건 때문에 보류로 기록했고, `P1-R1` closure는 `P1-R2`와 `PageHeader` 계약 결정이 남아 차단 상태로 기록했다. +- 코드 수정 연결: + - `REV-P0-001`: `src/shared/test/setup.ts`에 mock/global 복원을 추가하고 `src/shared/test/setup-isolation.test.ts`로 누출 회귀를 고정했다. + - `REV-P0-002`: `src/app/App.test.tsx`, `tests/e2e/smoke.spec.ts`에서 root `main` 안 대표 콘텐츠를 검증한다. + - `REV-P0-003`: 현재 Gate 전략을 `plan-task.md`의 P0-R1 기록에 누적했다. + - `REV-P1-002`: stale ADMIN probe pending 동안 보호 shell을 렌더하지 않고 probe query를 `size=20`으로 보정했다. + - `REV-P1-003`: crop output size와 Canvas render를 source crop rectangle 기반으로 고쳤고 자유 비율·pointer drag를 추가했다. + - `REV-P1-004`: audio 확장자↔MIME 조합표를 적용했다. + - `REV-P1-005`: `ConfirmDeactivateDialog`, `UnsavedChangesGuard`, `ImageCropDialog`에 focus trap과 trigger focus return을 적용했다. + - `REV-P1-006`: 로그인 실패 시 서버 한국어 `message`를 우선 표시한다. + - `REV-P1-007`: logout in-flight 중복 호출을 단일 promise로 합쳤다. + - `REV-P1-008`: `FileField` error 상태에 `aria-invalid="true"`를 연결했다. + - `REV-P1-009`: `AdminAudioPlayer` container shortcut이 descendant control key를 처리하지 않게 했다. +- 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 및 Gate: + - 같은 focused command — 성공, 11 files / 55 tests passed. + - `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. + - `npm run test:run -- src/shared src/features/auth src/layouts src/app/App.test.tsx` — 성공, 27 files / 102 tests passed. + - `npm run test:run` — 성공, 28 files / 108 tests passed. + - `npm ci` — 성공, 307 packages, 0 vulnerabilities. + - `npx playwright install chromium webkit` — 성공. + - `npm run typecheck` — 성공. + - `npm run lint` — 성공. + - `npm run build:dev` — 성공. + - `npm run build:prod` 및 `npm run build` — 성공. + - `npm run e2e -- tests/e2e/smoke.spec.ts` — 성공, 4 projects / 4 tests passed. + - `npm run e2e -- tests/e2e/auth.spec.ts tests/e2e/accessibility-shell.spec.ts` — 성공, 4 projects / 16 tests passed. + - `git diff --check` — 성공. + - LSP diagnostics — `src`, `src/shared/ui`, 주요 변경 TS/TSX와 E2E 파일에서 오류 0건. `App.tsx`와 `tests/e2e/auth.spec.ts` 개별 fresh diagnostics는 timeout이 있었고, `src` directory diagnostics 0건 및 `npm run typecheck` 성공으로 보완 확인했다. +- 참고: 첫 Gate 병렬 실행 중 `npm ci`가 `node_modules`를 재설치하는 동안 동시에 실행한 `tsc/playwright` 명령은 bin 삭제로 실패했다. 재설치 완료 후 같은 Gate 명령을 재실행해 성공 결과만 위에 기록했다. + +### 실행 체크 재검증 및 정정 — 2026-07-27 + +- 무엇을: 리뷰 리포트 실행 체크 전체를 재검증해 `P0-R1`의 문서 링크 공백과 `P1-R4`의 pixel·alpha 회귀 test 공백을 구현까지 완료했다. +- 어떻게: + - 존재하지 않는 `docs/agent-guide/README.md` 참조를 `docs/agent-guide/` 세부 문서 관리 방식으로 정정했다. + - `src/shared/lib/crop-image.test.ts`에 1200×600 → 1:1 no-upscale 600×600 출력과 상·하단 alpha 유지 회귀 test를 추가했다. + - `npm run test:run -- src/shared/lib/crop-image.test.ts` — 성공, 1 file / 4 tests passed. + - `npm run test:run -- src/shared src/features/auth src/layouts src/app/App.test.tsx` — 성공, 27 files / 102 tests passed. + - `npm run test:run` — 성공, 28 files / 108 tests passed. + - `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. +- 당시 결과: `P0-R1`, `P1-R3`, `P1-R4`, `P1-R5` 실행 체크는 구현·검증 완료였다. `P1-R2`는 백엔드 exact byte 확인 전 보류, `P1-R1`은 `P1-R2`와 `PageHeader` 결정 전 차단 상태였다. +- 당시 남은 항목: + - `REV-P1-010`: 백엔드 exact byte 확인 필요. 확인 전 값을 추정하지 않는다. + - `REV-P1-001`: `REV-P1-010` 보류 해소와 `PageHeader` 계약 유지·폐기 결정 후 closure Gate 재실행 필요. + +### P1-R1/R2 closure 점검 — 2026-07-27 + +- 무엇을: `P1-R2` exact byte를 `10,485,760 bytes`로 확정하고 `P1-R1`의 `PageHeader` 계약을 폐기 결정으로 정리했다. +- 코드·문서 수정 연결: + - `REV-P1-010`: PRD, API 계약, `IMAGE_MAX_BYTES`, `limit-1/limit/limit+1` 경계 테스트를 같은 값으로 정렬했다. + - `REV-P1-001`: `PageHeader`를 Phase 1 shared 산출물·소비처 Matrix에서 제거하고, 현재 shell 통합 증거는 실제 소비 중인 `PageState`와 App 포함 Gate로 제한했다. +- RED: + - `npm run test:run -- src/shared/validation/file-media-policy.test.ts` — `policy.maxBytes` 미구현으로 실패 확인. + - 같은 command — `IMAGE_MAX_BYTES` export 누락으로 실패 확인. +- GREEN 및 Gate: + - `npm run test:run -- src/shared/validation/file-media-policy.test.ts` — 성공, 1 file / 7 tests passed. + - `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. + - `npm run test:run -- src/shared src/features/auth src/app/App.test.tsx` — 성공, 27 files / 103 tests passed. + - `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건을 포함한다. + - `npm run test:run` — 성공, 28 files / 109 tests passed. + - `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` 성공으로 보완 확인했다. +- 결과: `REV-P1-001`과 `REV-P1-010`을 수정 완료로 닫았다. 열린 Blocker·High·Medium 항목은 없다.