Files
voiceon-character-admin/docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md
2026-07-27 23:23:36 +09:00

1545 lines
116 KiB
Markdown

# AI 캐릭터 관리자 웹 Phase 2 코드 리뷰·QA 리포트
## 1. 리뷰 정보
| 항목 | 내용 |
| --- | --- |
| 리뷰 대상 | `plan-task.md` Phase 2 / `P2-T1`~`P2-T3`, `P2-GATE`, `P2-R1`~`P2-R16`과 현재 working tree 구현 |
| 기준 commit 또는 working tree | `b4841ff579a271771dc787b7c91b7ca2d24f5e85` 기준, 5차 재검증 시 tracked diff 37개 + untracked 1개 = working tree 38개 변경 항목 |
| 리뷰 일자 | 2026-07-27 |
| 리뷰어 | Codex |
| 기준 문서 | `docs/20260725_AI캐릭터관리자웹/prd.md`, `api-contract.md`, `plan-task.md` |
| 리뷰 기준 | `docs/agent-guide/review.md`, `docs/sample/sample-review.md` |
| 리뷰 상태 | 7차 독립 재검증 후 `P2-R16` 수정 완료 |
| 최종 결론 | Phase 2 구현·review·plan 정합성 충족, Phase 3 진행 가능 |
리뷰 시작 시 Phase 2 구현은 모두 index에 staged된 상태였다. 기존 staged 변경은 수정하지 않았고, 이 리뷰에서 별도 review 문서를 만들고 확정 항목을 `plan-task.md`의 신규 회귀 Task로만 전환했다.
2026-07-27 2차 재검증에서는 `P2-R1`~`P2-R3` 수정이 반영된 현재 working tree를 독립 검토했다. 애플리케이션 코드·test·설정·`plan-task.md`는 변경하지 않았고, 아래 `REV-P2-006`~`REV-P2-009`와 실제 검증 결과만 이 문서에 누적했다.
2026-07-27 3차 재검증에서는 `P2-R4`~`P2-R7` 수정 뒤의 문서 추적성, mobile menu의 mock banner·responsive inert 경계와 보호 route 오류 복구 UI를 다시 확인했다. `REV-P2-010`~`REV-P2-013`을 확정하고 `plan-task.md``P2-R8`~`P2-R10` 회귀 Task로 전환했으며, 애플리케이션 코드·test·설정은 변경하지 않았다.
2026-07-27 `P2-R10`에서는 보호 route 오류 page에 fail-closed 수동 retry를 추가하고 App unit, server boundary E2E, P2 focused Gate, 전체 unit·정적 검사·build와 실제 브라우저 시각 QA를 완료했다. `REV-P2-013`을 수정 완료로 판정해 3차 재검증의 남은 항목을 닫았다.
2026-07-27 4차 재검증에서는 `P2-R8`~`P2-R10` 반영 뒤 working tree 전체 집계, 보호 route pending UI와 npm script의 focused E2E 필터를 독립 확인했다. `REV-P2-014`~`REV-P2-016`을 확정하고 `plan-task.md``P2-R11`~`P2-R13` 회귀 Task로 전환했으며, 애플리케이션 코드·test·설정은 변경하지 않았다.
2026-07-27 `P2-R11`~`P2-R13`에서는 untracked 포함 working tree 추적성, 보호 route pending loading status와 mode별 focused E2E filter를 수정했다. `REV-P2-014`~`REV-P2-016`을 수정 완료로 판정했다.
2026-07-27 5차 재검증에서는 `P2-R11`~`P2-R13` 구현과 최신 실행 결과를 다시 대조했다. 제품 동작과 mode 경계 회귀는 통과했지만 `P2-R12`의 test 구조·개수 기록이 실제와 다른 `REV-P2-017` Low 1건을 확정하고 `plan-task.md``P2-R14` 회귀 Task로 전환했다. 애플리케이션 코드·test·설정은 변경하지 않았다.
2026-07-27 `P2-R14`에서는 `P2-R12` 완료 증거를 실제 App test 구조인 최초 pending 전용 test 1건 + 기존 404 retry test 보강 + 27 tests로 정정했다. 문서 검색·diff 검증과 App focused test를 완료해 `REV-P2-017`을 수정 완료로 판정했다.
2026-07-27 6차 재검증에서는 `P2-R14` 반영 뒤 구현·test·production·mode별 E2E와 review의 현재 상태를 다시 대조했다. 제품 동작과 mode 경계는 통과했지만 review §7이 이미 수정 완료된 `P2-R8`~`P2-R13`을 여전히 “아직 수정하지 않았다”고 표시하는 `REV-P2-018` Low 1건을 확정하고 `plan-task.md``P2-R15` 회귀 Task로 전환했다. 이후 `P2-R15`에서 review §7의 현재 상태 문구를 수정 완료 상태로 정정했다. 애플리케이션 코드·test·설정은 변경하지 않았다.
2026-07-27 7차 재검증에서는 `P2-R15` 반영 뒤 Phase 2 상단 현재 상태와 하단 최신 Progress를 Task 본문·review 종료 판정과 대조했다. `P2-R15`는 수정 완료지만 plan의 현재 상태는 최초 추가 당시 설명에 머물고, 최신 Progress는 여전히 `P2-R15` 수정 필요 상태로 끝나는 `REV-P2-019` Low 1건을 확정해 `P2-R16`으로 전환했다. 이후 `P2-R16`에서 Phase 2 현재 상태와 최신 Progress를 완료 상태로 정렬하고 전체 Gate를 재검증했다. 애플리케이션 코드·test·설정은 변경하지 않았다.
## 2. 리뷰 목적과 범위
### 2.1 목적
- Phase 2 완료 체크와 Gate 기록이 PRD `MOCK-001~009`, API Contract §1·§3, 실제 코드·test 결과와 일치하는지 확인한다.
- mock/server 명시적 경계, production 차단, no-auto-fallback, auth fixture와 지속 안내가 실제 브라우저에서도 유지되는지 확인한다.
- 자동화가 통과하더라도 보호 route, exact API origin, JWT 오류 status와 문서 추적성에 남은 공백을 독립적으로 판정한다.
### 2.2 포함 범위
- 코드·설정: `.env.example`, `package.json`, `playwright.config.ts`, `vite.config.ts`, `src/main.tsx`, `src/app`, `src/shared/config`, `src/shared/mocks`, `src/shared/ui/mock-mode-banner.tsx`
- 테스트: Phase 2 unit/contract test, mock/server boundary E2E, 기존 auth·accessibility·smoke 회귀 E2E
- 문서: README, agent environment/scripts 가이드, PRD `MOCK-001~009`, API Contract §1·§3, plan `P2-T1~P2-GATE`
- 런타임 검증: Chromium, WebKit, Mobile Chrome, Mobile Safari의 mock login/shell, 320px·200% zoom, axe, worker 등록, server 404·network error
### 2.3 제외 범위
- Phase 3 이후 Character·Audio·Series·Community·FanTalk·Comments 구현
- 실제 백엔드 내부 구현과 운영 데이터
- 계약 미제공 endpoint·DTO·오류의 추정
- 확정 문제의 코드·test 수정
- 실제 보조기기 수동 테스트와 디자인 정성 평가
## 3. 판정 기준
### 3.1 심각도
| 심각도 | 기준 |
| --- | --- |
| Blocker | Phase 완료 판정이나 출고 판단을 무효화하거나 핵심 보호 경계를 전혀 신뢰할 수 없게 만드는 문제 |
| High | 확정 요구사항·API Contract·보호 route를 위반하거나 mock 검증이 잘못된 production 경계를 통과시키는 문제 |
| Medium | 제한된 조건에서 발생하는 기능·접근성·복구 문제 |
| Low | 사용자 안내, 문서 정합성 또는 검증 추적성을 약화하는 문제 |
### 3.2 상태
| 상태 | 의미 | 후속 처리 |
| --- | --- | --- |
| 후보 | 근거를 발견했지만 아직 재현·판정하지 않음 | 검증 후 상태 변경 |
| 확정 | 코드·test·문서 또는 실행으로 문제가 확인됨 | 회귀 수정 Task·goal 후보 |
| 오탐 | 요구사항이나 실행 결과상 문제가 아님 | 근거를 남기고 종료 |
| 보류 | 외부 계약·환경·제품 결정이 필요함 | 담당 주체와 재개 조건 기록 |
| 수정 완료 | 수정과 관련 검증이 완료됨 | 실제 명령과 결과 연결 |
## 4. 검토한 근거
### 4.1 문서와 코드
- 요구사항: PRD `AUTH-003`, `MOCK-001~009`, 성공 기준의 mock/server/production 항목
- API Contract: §1.2 오류 status, §1.5 Mock Preview 계약 경계, §3.1~3.3 인증
- 계획: `P2-T1`, `P2-T2`, `P2-T3`, `P2-GATE`, Phase 1 `P1-R3`
- 코드: `src/main.tsx`, `src/app/App.tsx`, `src/app/admin-pages.tsx`, `src/shared/config/env.ts`, `src/shared/mocks`, `vite.config.ts`, `playwright.config.ts`
- 테스트: `src/app/App.test.tsx`, `src/shared/mocks`, `tests/e2e/mock-mode-boundary.spec.ts`, `mock-preview-shell.spec.ts`, `server-mode-boundary.spec.ts`, 기존 auth/accessibility/smoke spec
### 4.2 실행 환경
| 항목 | 값 |
| --- | --- |
| OS | macOS 26.0, Darwin 25.0.0 x86_64 |
| Node.js | v24.12.0 |
| npm | 11.7.0 |
| Playwright | 1.61.1 |
| 브라우저 프로젝트 | Chromium, WebKit, Mobile Chrome, Mobile Safari |
| server mode | `VITE_API_MODE=server`, `http://127.0.0.1:8888` |
| mock mode | `VITE_API_MODE=mock`, `http://127.0.0.1:8889` |
민감정보는 실행 환경과 기록에 포함하지 않았다.
### 4.3 실행한 자동 검증
| 명령 | 종료 | 결과와 핵심 증거 |
| --- | ---: | --- |
| `npm run test:run` | 0 | 34 files / 130 tests 통과 |
| `npm run test:run -- src/shared/config src/shared/mocks src/shared/ui/__tests__/mock-mode-banner.test.tsx` | 0 | Phase 2 focused 7 files / 20 tests 통과 |
| `npm run typecheck` | 0 | TypeScript 오류 0건 |
| `npm run lint` | 0 | ESLint 오류 0건 |
| `npm run build:dev` | 0 | 159 modules, JS 294.26 kB, gzip 88.61 kB |
| `npm run build:prod` | 0 | 159 modules, JS 294.25 kB, gzip 88.60 kB |
| `VITE_API_MODE=mock npm run build:prod` | 1 | 기대한 production 거부. `VITE_API_MODE=mock is only available during development` 확인 |
| `test ! -e dist/mockServiceWorker.js && ! rg -l -e 'startMockWorker' -e 'mockServiceWorker\.js' dist` | 0 | production 산출물에 worker 파일·bootstrap 문자열 없음 |
| `npm run e2e:mock -- tests/e2e/mock-preview-shell.spec.ts` | 1 | 최초 sandbox에서 `127.0.0.1:8889` listen `EPERM`, 실행 불가 사유 확인 |
| 같은 mock preview E2E를 로컬 listen 권한으로 재실행 | 0 | 4 projects / 20 tests 통과. login, logout 후 재login, 320px·200% zoom, axe critical·serious 0건 |
| `npm run e2e:mock -- tests/e2e/mock-mode-boundary.spec.ts` | 0 | 4 projects / 4 tests 통과, explicit mock worker controller 확인 |
| `npm run e2e -- tests/e2e/server-mode-boundary.spec.ts` | 0 | 4 projects / 12 tests 통과, server mode worker 0건과 404·network no-fallback 확인 |
| `npm run e2e -- tests/e2e/smoke.spec.ts tests/e2e/auth.spec.ts tests/e2e/accessibility-shell.spec.ts` | 0 | 4 projects / 20 tests 통과 |
| `git diff --check && git diff --cached --check` | 0 | whitespace 오류 0건 |
자동화는 모두 최종 통과했다. 아래 확정 항목은 현재 test가 잘못된 결과를 성공 조건으로 삼거나, 필요한 음성 조건을 검사하지 않아 통과 결과만으로 발견하지 못했다.
### 4.4 Mock handler 런타임 재현
Vite SSR로 실제 `src/shared/mocks/handlers.ts`를 로드하고 MSW Node server에 연결해 wrong origin과 logout token 상태를 확인했다.
```bash
node --input-type=module -e 'import { createServer as createViteServer } from "vite"; import { setupServer } from "msw/node"; const vite = await createViteServer({ appType: "custom", server: { middlewareMode: true, hmr: false } }); try { const { createMockHandlers, createMockStore } = await vite.ssrLoadModule("/src/shared/mocks/handlers.ts"); const mockServer = setupServer(...createMockHandlers(createMockStore())); mockServer.listen({ onUnhandledRequest: "error" }); try { const request = (path, token) => fetch(`https://wrong-origin.example${path}`, token ? { method: "POST", headers: { Authorization: `Bearer ${token}` } } : { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ email: "admin@test.com", password: "password" }) }); const wrongOriginLogin = await request("/admin/member/login"); const invalidLogout = await request("/member/logout", "invalid-token"); const firstLogout = await request("/member/logout", "mock-admin-jwt"); const revokedLogout = await request("/member/logout", "mock-admin-jwt"); console.log(JSON.stringify({ wrongOriginLogin: wrongOriginLogin.status, invalidLogout: invalidLogout.status, firstLogout: firstLogout.status, revokedLogout: revokedLogout.status })); } finally { mockServer.close(); } } finally { await vite.close(); }'
```
결과:
```json
{"wrongOriginLogin":200,"invalidLogout":200,"firstLogout":200,"revokedLogout":200}
```
명령은 exit 0으로 끝났다. sandbox가 Vite HMR WebSocket `0.0.0.0:24678` listen을 `EPERM`으로 거부했다는 경고가 있었지만, MSW가 처리한 네 HTTP status 재현은 완료됐다.
### 4.5 2차 독립 재검증 — 2026-07-27
| 명령 또는 검증 | 종료 | 결과와 핵심 증거 |
| --- | ---: | --- |
| `npm run test:run -- src/shared/config src/shared/mocks src/shared/ui/__tests__/mock-mode-banner.test.tsx` | 0 | 7 files / 23 tests 통과 |
| `npm run test:run` | 0 | 34 files / 136 tests 통과 |
| `npm run typecheck` | 0 | TypeScript 오류 0건 |
| `npm run lint` | 0 | ESLint 오류 0건 |
| `npm run build:dev` | 0 | 159 modules, JS 294.66 kB, gzip 88.66 kB |
| `npm run build:prod` | 0 | 159 modules, JS 294.65 kB, gzip 88.66 kB |
| `VITE_API_MODE=mock npm run build:prod` | 1 | 기대한 production guard. `VITE_API_MODE=mock is only available during development` 확인 |
| `test ! -e dist/mockServiceWorker.js` | 0 | production 산출물에 worker 파일 없음 |
| `rg -l -e startMockWorker -e 'mockServiceWorker\.js' dist` | 1 | 기대한 no-match. production JS에 mock bootstrap 문자열 없음 |
| `npm run e2e:mock -- tests/e2e/mock-preview-shell.spec.ts tests/e2e/mock-mode-boundary.spec.ts` | 0 | 4 projects / 24 tests 통과 |
| `npm run e2e -- tests/e2e/server-mode-boundary.spec.ts tests/e2e/smoke.spec.ts tests/e2e/auth.spec.ts tests/e2e/accessibility-shell.spec.ts` | 0 | 4 projects / 32 tests 통과 |
| `npm run e2e:mock` | 1 | 56 tests 중 36 통과 / 20 실패. mock mode에서 server 전용·기존 shell spec까지 함께 수집됨 |
| `npm run e2e -- --list` | 0 | server 명령이 mock 전용 spec을 포함한 6 files / 56 tests를 수집 |
| `npm run e2e:mock -- --list` | 0 | mock 명령도 같은 6 files / 56 tests를 수집 |
| `git diff --check HEAD` | 0 | whitespace 오류 0건 |
표준 focused·전체 unit, typecheck, lint, build와 명시적으로 분리한 E2E 묶음은 통과했다. 그러나 bare E2E script는 mode별 test 선택 경계가 없어 실패했으며, 현재 자동화에 없는 아래 세 런타임 경계도 별도 재현됐다.
### 4.6 2차 런타임 재현
#### 동일 세션 보호 route 재진입
로컬 server mode를 열고 Playwright one-off script에서 같은 `AuthSessionRecord`를 유지한 채 다음 순서로 실행했다.
1. `/ai-characters` 최초 진입의 StrictMode 2회 probe를 모두 200으로 응답한다.
2. SPA history로 `/login`에 이동하되 session을 지우지 않는다.
3. SPA history로 `/ai-characters`에 재진입하고 다음 probe를 404로 응답한다.
4. 오류 alert가 표시된 뒤 보호 `main`과 로그아웃 버튼 수를 확인한다.
결과:
```json
{"firstEntry":{"probes":2,"protectedMainCount":1},"probes":3,"path":"/ai-characters","protectedMainCount":1,"logoutCount":1,"alert":"보호 route 확인에 실패했습니다."}
```
#### Mock Preview 접근 거부 화면
mock mode에서 `sessionStorage``{ token: "mock-member-jwt", role: "ADMIN" }`을 저장하고 `/ai-characters`에 진입했다. 403 처리 후 결과는 다음과 같았다.
```json
{"path":"/access-denied","mockBannerCount":0,"heading":"접근 권한이 없습니다"}
```
#### Auth handler status·media type
Vite SSR로 현재 `createMockHandlers(createMockStore(), "https://api.example.com")`를 MSW Node server에 연결해 비ADMIN logout과 `text/plain` login을 호출했다.
```json
{"memberLogout":401,"textPlainLogin":200}
```
API Contract §1.2의 비ADMIN JWT 403과 지원하지 않는 media type 415에 모두 어긋난다.
### 4.7 3차 독립 재검증 — 2026-07-27
| 명령 또는 검증 | 종료 | 결과와 핵심 증거 |
| --- | ---: | --- |
| `npm run test:run -- src/shared/config src/shared/mocks src/shared/ui/__tests__/mock-mode-banner.test.tsx` | 0 | 7 files / 25 tests 통과 |
| `npm run test:run -- src/shared/mocks/__tests__/mock-preview-docs.test.ts` | 0 | 문서 반영 후 1 file / 3 tests 통과 |
| `npm run test:run` | 0 | 34 files / 141 tests 통과 |
| `npm run typecheck` | 0 | TypeScript 오류 0건 |
| `npm run lint` | 0 | ESLint 오류 0건 |
| `npm run build:dev`, `npm run build:prod` | 0 | development·production build 모두 160 modules 변환 성공 |
| `VITE_API_MODE=mock npm run build:prod` | 1 | 기대한 production guard. `VITE_API_MODE=mock is only available during development` 확인 |
| `test ! -e dist/mockServiceWorker.js` | 0 | production 산출물에 worker 파일 없음 |
| `rg -l -e startMockWorker -e 'mockServiceWorker\.js' dist` | 1 | 기대한 no-match. production JS에 mock bootstrap 문자열 없음 |
| `npm run e2e` | 0 | server mode 4 projects / 32 tests 통과 |
| `npm run e2e:mock` | 0 | mock mode 4 projects / 24 tests 통과 |
| `git diff HEAD --name-only \| wc -l` | 0 | 현재 변경 37개 경로. review 상단·종료 판정의 36개 기록과 불일치 확인 |
| `git diff --check HEAD` | 0 | whitespace 오류 0건 |
Mock Preview를 320px에서 열어 ADMIN login 후 mobile menu를 열고 1,200px로 viewport를 바꾸는 Chromium one-off 검증 결과는 다음과 같았다.
```json
{
"responses": [
{ "path": "/admin/member/login", "fromServiceWorker": true },
{ "path": "/api/v2/admin/ai-characters?page=0&size=20", "fromServiceWorker": true }
],
"before": {
"bannerInsideInert": false,
"bannerAriaHidden": null,
"mainInsideInert": true,
"overlayDisplay": "block"
},
"after1200px": {
"bannerInsideInert": false,
"bannerAriaHidden": null,
"mainInsideInert": true,
"mainInertAncestor": "true",
"overlayDisplay": "none"
}
}
```
두 API 응답은 실제로 Service Worker에서 반환되어 backend 요청 0건 경계는 확인됐다. 반면 menu open 상태에서 banner는 background inert 경계 밖에 남았고, desktop 전환 후 overlay만 숨겨진 채 main의 `inert`·`aria-hidden`이 유지됐다.
추가 코드·test 대조에서 `ProtectedRouteErrorPage`는 404·network probe 실패 시 alert만 렌더하고 retry·login 이동 control을 제공하지 않으며, 기존 App/server boundary test도 shell 비노출만 확인한다는 복구 공백을 확인했다.
### 4.8 4차 독립 재검증 — 2026-07-27
| 명령 또는 검증 | 종료 | 결과와 핵심 증거 |
| --- | ---: | --- |
| `npm run test:run -- src/app/App.test.tsx src/shared/ui/__tests__/mock-mode-banner.test.tsx` | 0 | 2 files / 28 tests 통과 |
| `npm run test:run` | 0 | 34 files / 145 tests 통과 |
| `npm run typecheck`, `npm run lint` | 0 | TypeScript·ESLint 오류 0건 |
| `npm run build:dev`, `npm run build:prod` | 0 | development·production 모두 160 modules 변환 성공 |
| `VITE_API_MODE=mock npm run build:prod` | 1 | 기대한 production guard. `VITE_API_MODE=mock is only available during development` 확인 |
| `test ! -e dist/mockServiceWorker.js` | 0 | production 산출물에 worker 파일 없음 |
| `rg -l -e startMockWorker -e 'mockServiceWorker\.js' dist` | 1 | 기대한 no-match. production JS에 mock bootstrap 문자열 없음 |
| `npm run e2e` | 0 | server mode 4 projects / 32 tests 통과 |
| `npm run e2e:mock` | 0 | mock mode 4 projects / 28 tests 통과 |
| `git diff HEAD --name-only \| wc -l` | 0 | tracked diff 37개 경로 |
| `git status --porcelain=v1 \| wc -l` | 0 | working tree 38개 변경 항목 |
| `git ls-files --others --exclude-standard` | 0 | `src/app/protected-admin-shell.tsx` 1개가 untracked임을 확인 |
| `npm run e2e -- --list tests/e2e/server-mode-boundary.spec.ts` | 0 | file filter와 script 고정 목록이 합쳐져 4 files / 32 tests 수집 |
| `VITE_API_MODE=server npx playwright test tests/e2e/server-mode-boundary.spec.ts --list` | 0 | 실제 boundary spec은 1 file / 12 tests이며 `P2-R10` 기대 16 tests에 미달 |
보호 route retry pending은 sandbox의 local listen·Chromium launch가 각각 `EPERM`·permission denied로 한 차례 실패한 뒤 허용된 로컬 실행으로 재검증했다. 404 화면에서 retry를 누르고 성공 응답을 350ms 지연한 결과는 다음과 같았다.
```json
{
"retryRequests": 1,
"pending": {
"rootText": "",
"rootChildCount": 0,
"mainCount": 0,
"statusCount": 0,
"alertCount": 0,
"activeElement": "BODY"
},
"complete": {
"mainCount": 1,
"logout": true
}
}
```
`P2-R9`의 banner inert·desktop breakpoint 수정과 `P2-R10`의 fail-closed retry 성공 경계는 unit과 4-browser E2E에서 유지됐다. 반면 untracked 파일을 제외한 범위 집계, pending 중 빈 root, hard-coded npm E2E script가 focused filter를 무효화해 network retry E2E 공백을 가리는 세 문제를 추가로 확정했다.
### 4.9 5차 독립 재검증 — 2026-07-27
| 명령 또는 검증 | 종료 | 결과와 핵심 증거 |
| --- | ---: | --- |
| `npm run test:run -- src/app/App.test.tsx` | 0 | 1 file / 27 tests 통과. 최초 probe pending 전용 test 1건과 기존 404 retry test의 pending assertion을 확인 |
| `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts` | 0 | 2 files / 6 tests 통과 |
| `npm run test:run` | 0 | 34 files / 147 tests 통과 |
| `npm run typecheck`, `npm run lint` | 0 | TypeScript·ESLint 오류 0건 |
| `npm run build:dev`, `npm run build:prod` | 0 | development·production 각각 160 modules 변환 성공 |
| `VITE_API_MODE=mock npm run build:prod` | 1 | 기대한 production guard. `VITE_API_MODE=mock is only available during development` 확인 |
| `npm run e2e -- --list tests/e2e/server-mode-boundary.spec.ts` | 0 | 1 file / 16 tests 수집 |
| `npm run e2e:mock -- --list tests/e2e/mock-preview-shell.spec.ts` | 0 | 1 file / 24 tests 수집 |
| `npm run e2e -- tests/e2e/server-mode-boundary.spec.ts` | 0 | 4 projects / 16 tests 통과. retry pending status와 network 재실패 경계 유지 |
| `npm run e2e:mock -- tests/e2e/mock-preview-shell.spec.ts` | 0 | 4 projects / 24 tests 통과 |
| `npm run e2e`, `npm run e2e:mock` | 0 | bare server 36 tests, bare mock 28 tests 통과 |
| `git diff HEAD --name-only \| wc -l` | 0 | tracked diff 37개 경로 |
| `git status --short --untracked-files=all \| wc -l` | 0 | working tree 38개 변경 항목 |
| `git ls-files --others --exclude-standard` | 0 | untracked `src/app/protected-admin-shell.tsx` 1개 확인 |
구현 대조 결과, 보호 route 최초 probe와 retry pending 동작은 각각 unit assertion과 server boundary E2E로 유지됐다. 그러나 `P2-R12`는 “두 test 추가”와 App 28 tests 이상을 완료 조건으로 체크한 반면 실제 App test는 27건이고, 구조도 최초 pending 전용 test 1건 추가 + 기존 retry test 보강이다. 이 완료 증거 불일치를 `REV-P2-017`로 확정했다.
### 4.10 6차 독립 재검증 — 2026-07-27
| 명령 또는 검증 | 종료 | 결과와 핵심 증거 |
| --- | ---: | --- |
| `npm run test:run` | 0 | 34 files / 147 tests 통과 |
| `npm run typecheck`, `npm run lint` | 0 | TypeScript·ESLint 오류 0건 |
| `npm run build:dev`, `npm run build:prod` | 0 | development·production 각각 160 modules 변환 성공 |
| `VITE_API_MODE=mock npm run build:prod` | 1 | 기대한 production guard. `VITE_API_MODE=mock is only available during development` 확인 |
| `npm run e2e` | 0 | 최초 sandbox listen `EPERM` 뒤 로컬 실행 권한으로 재실행해 4 projects / 36 tests 통과 |
| `npm run e2e:mock` | 0 | 최초 sandbox listen `EPERM` 뒤 로컬 실행 권한으로 재실행해 4 projects / 28 tests 통과 |
| `git diff --check HEAD` | 0 | review 문서 반영 전 whitespace 오류 0건 |
| review 현재 상태 대조 | 0 | §5·§6·§8은 `P2-R8`~`P2-R13` 수정 완료, §7은 같은 Task를 “아직 수정하지 않았다”고 표시하는 모순 2건 확인 |
| `npm run test:run -- src/shared/mocks/__tests__/mock-preview-docs.test.ts` | 0 | review·plan 반영 후 1 file / 3 tests 통과 |
| plan/review 대상 `git diff --check` | 0 | review·plan 반영 후 whitespace 오류 0건 |
Chromium one-off 검증에서는 mock login과 보호 route 응답이 모두 `fromServiceWorker=true`였고, 320px의 mobile menu·logout button 높이가 각각 60px이었다. 열린 mobile menu에서 `AI 캐릭터` link를 선택한 뒤 menu와 background `inert` 잔존은 0건이었다. 새 제품 동작 회귀는 재현되지 않았다.
### 4.11 7차 독립 재검증 — 2026-07-27
| 명령 또는 검증 | 종료 | 결과와 핵심 증거 |
| --- | ---: | --- |
| Phase 2 완료 상태 exact 검색 | 1 | 상단 현재 상태에 `P2-R15` 완료와 Phase 3 진행 가능 판정이 없음 |
| `tail -n 16 plan-task.md` 최신 Progress 검색 | 1 | 문서 끝이 6차 재검증의 `P2-R15` 수정 필요 상태로 끝남 |
| `P2-R15` Task·review 상태 대조 | 0 | Task 체크·inline 검증과 review §5·§6·§7·§8·§9는 수정 완료 및 열린 항목 없음을 표시 |
| `git diff --check HEAD` | 0 | review 반영 전 working tree whitespace 오류 0건 |
제품 코드와 실행 설정의 새 변경은 없다. 완료 상태의 source인 Phase 상단과 최신 누적 Progress가 소비자인 Phase 3 시작 판정으로 전파되지 않은 문서 정합성 문제를 `REV-P2-019`로 확정했다.
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
| --- | --- | --- | --- | --- | --- |
| REV-P2-001 | High | 수정 완료 | 404·network probe 실패 뒤 미검증 보호 shell을 렌더링 | R2.1 | P2-R1 |
| REV-P2-002 | High | 수정 완료 | mock handler가 API base URL이 아닌 모든 origin을 허용 | R2.2 | P2-R2 |
| REV-P2-003 | High | 수정 완료 | invalid·revoked JWT logout을 성공 응답으로 정규화 | R2.2 | P2-R2 |
| REV-P2-004 | Low | 수정 완료 | 보호 shell 빈 상태가 Character 연결 시점을 완료된 Phase 2로 표시 | R2.3 | P2-R3 |
| REV-P2-005 | Low | 수정 완료 | Phase 2 Files·하단 Progress가 실제 구현과 불일치 | R2.3 | P2-R3 |
| REV-P2-006 | High | 수정 완료 | 한 번 검증된 동일 세션의 재진입 probe 실패가 보호 shell을 다시 노출 | R2.4 | P2-R4 |
| REV-P2-007 | High | 수정 완료 | auth fixture가 비ADMIN status와 login media type 계약을 완화 | R2.5 | P2-R5 |
| REV-P2-008 | Medium | 수정 완료 | Mock Preview 403·보호 오류 화면에서 지속 안내가 사라짐 | R2.6 | P2-R6 |
| REV-P2-009 | Medium | 수정 완료 | bare server·mock E2E 명령이 반대 mode spec까지 함께 수집 | R2.7 | P2-R7 |
| REV-P2-010 | Low | 수정 완료 | Phase 2 완료 문서가 P2-R4~P2-R7 변경과 불일치 | R2.8 | P2-R8 |
| REV-P2-011 | Medium | 수정 완료 | 열린 mobile menu가 desktop 전환 후 숨은 overlay와 inert shell을 남김 | R2.9 | P2-R9 |
| REV-P2-012 | Medium | 수정 완료 | 열린 mobile menu의 background inert 경계에서 Mock Preview banner가 제외됨 | R2.9 | P2-R9 |
| REV-P2-013 | Medium | 수정 완료 | 보호 route 오류 화면에 fail-closed 재시도·복구 경로가 없음 | R2.10 | P2-R10 |
| REV-P2-014 | Low | 수정 완료 | working tree 범위 집계가 untracked 보호 shell을 제외 | R2.11 | P2-R11 |
| REV-P2-015 | Medium | 수정 완료 | 보호 route probe pending 중 root가 비어 진행 상태를 알리지 않음 | R2.12 | P2-R12 |
| REV-P2-016 | Medium | 수정 완료 | 고정 E2E script가 focused 필터와 network retry 증거 공백을 가림 | R2.13 | P2-R13 |
| REV-P2-017 | Low | 수정 완료 | P2-R12 test 증거가 실제 test 구조·실행 수와 불일치 | R2.14 | P2-R14 |
| REV-P2-018 | Low | 수정 완료 | plan 전환 절이 완료된 P2-R8~P2-R13을 미수정으로 표시 | R2.15 | P2-R15 |
| REV-P2-019 | Low | 수정 완료 | Phase 2 현재 상태·최신 Progress가 P2-R15 완료와 불일치 | R2.16 | P2-R16 |
1차 확정 5건, 2차 확정 4건, 3차 확정 4건, 4차 확정 3건, 5차·6차·7차 확정 Low 각 1건은 2026-07-27 후속 수정으로 완료됐다. 오탐·보류 항목은 없다.
## 6. 발견 사항 상세
### REV-P2-001 — 404·network probe 실패 뒤 미검증 보호 shell을 렌더링
- **심각도:** High
- **상태:** 수정 완료
- **관련 요구사항:** `AUTH-003`, `MOCK-003`
- **관련 계약:** API Contract §3.3, Phase 1 `P1-R3`
- **소유 Task:** 신규 R2.1, goal `P2-R1`
**관찰 내용**
저장된 ADMIN session으로 `/ai-characters`에 진입해 현재 DB 권한 확인용 목록 probe가 404 또는 network error로 실패하면 `routeError`가 설정된다. 렌더 guard는 `routeError === null`일 때만 shell을 숨기므로, probe가 성공하지 않았는데도 sidebar·header·logout·보호 `main`을 포함한 `ProtectedAdminShell`을 렌더한다.
**근거**
- 코드: `src/app/App.tsx:183-218`은 비401·비403 실패를 `routeError`로 저장하고, `src/app/App.tsx:243-251`은 오류가 있으면 token 검증 성공 여부와 무관하게 shell을 반환한다.
- 코드: `src/app/admin-pages.tsx:19-35`의 alert는 `ProtectedAdminShell` 내부 `main`에서만 렌더된다.
- 테스트: `tests/e2e/server-mode-boundary.spec.ts:28-63`은 404·network error에서 이 내부 alert가 보이는 것을 성공 조건으로 사용하며 실제 12 tests가 통과했다.
- 회귀 기준: `src/app/App.test.tsx:196-227`과 plan `P1-R3`은 stale ADMIN probe pending·403 동안 보호 shell 비노출을 고정한다.
**재현 또는 검증 절차**
1. `sessionStorage``{ token: "admin-token", role: "ADMIN" }` session을 둔다.
2. `GET /api/v2/admin/ai-characters?page=0&size=20`을 404로 응답하거나 network abort한다.
3. `/ai-characters`에 진입하면 `보호 route 확인에 실패했습니다.` alert와 함께 보호 shell `main`이 렌더된다.
4. 요구되는 결과는 probe 성공 전에는 보호 shell을 숨기고, no-fallback 오류를 shell 밖의 독립 오류 UI로 표시하는 것이다.
**영향**
현재 DB role을 확인할 수 없는 실패 상태를 ADMIN으로 간주해 fail-closed 보호 경계가 깨진다. 현재 Phase에는 실제 도메인 데이터·mutation이 없지만 Phase 3부터 같은 shell 아래 기능이 추가되면 일시적 404·network failure가 보호 navigation과 action surface 노출로 이어질 수 있다.
**권장 조치**
현재 token의 probe 성공 여부와 route 오류 표시 상태를 분리한다. 성공한 현재 token만 `ProtectedAdminShell`을 열고, 404·network 오류는 shell 밖에서 표시한다. 404·network와 이전 오류 뒤 새 session probe pending을 App test에 추가하고 server boundary E2E가 worker 0건과 보호 shell 비노출을 함께 확인하게 한다.
**판정 기록**
- 2026-07-27 — 코드 분기와 4-browser server boundary E2E를 대조해 보호 route 계약 회귀로 확정했다.
### REV-P2-002 — mock handler가 API base URL이 아닌 모든 origin을 허용
- **심각도:** High
- **상태:** 수정 완료
- **관련 요구사항:** `MOCK-002`
- **관련 계약:** API Contract §1.5
- **소유 Task:** 신규 R2.2, goal `P2-R2`
**관찰 내용**
login, logout, AI Character probe handler가 모두 `*/...` wildcard URL을 사용한다. 따라서 앱의 `VITE_API_BASE_URL`이 잘못되거나 request가 다른 origin으로 나가도 mock mode는 같은 성공 fixture를 반환한다.
**근거**
- 코드: `src/shared/mocks/handlers.ts:97-120`의 세 handler가 exact API base URL 대신 `*/admin/member/login`, `*/member/logout`, `*/api/v2/admin/ai-characters`에 매칭된다.
- 계약: API Contract §1.5는 mock mode도 production과 같은 URL을 사용하도록 요구한다.
- 실행: `https://wrong-origin.example/admin/member/login`에 유효 body를 전송한 런타임 재현이 200을 반환했다.
- 테스트 공백: auth handler test는 `https://api.example.com` 한 origin만 사용하고 wrong origin이 unhandled인지 확인하지 않는다.
**재현 또는 검증 절차**
1. `createMockHandlers(createMockStore())`를 MSW server에 등록한다.
2. 설정된 API base와 무관한 `https://wrong-origin.example/admin/member/login`에 유효 JSON을 보낸다.
3. 실제 결과는 200 ADMIN fixture다.
4. 요구되는 결과는 handler 비매칭이며 browser mock mode에서는 `onUnhandledRequest: "error"` 경계가 잘못된 URL을 드러내는 것이다.
**영향**
잘못된 API host, base URL 또는 URL 조합 회귀가 mock happy path에서 숨겨진다. mock Gate가 실제 server integration 완료 증거는 아니더라도, 동일 production URL 경계를 검증한다는 `MOCK-002`의 신뢰성이 사라진다.
**권장 조치**
`createMockHandlers`에 검증된 API base URL을 주입하고 `new URL(path, apiBaseUrl)`의 exact URL로 handler를 등록한다. wrong-origin request가 handler에 잡히지 않는 focused test를 추가한다.
**판정 기록**
- 2026-07-27 — wildcard 코드와 wrong-origin 200 런타임 결과로 API Contract 위반을 확정했다.
### REV-P2-003 — invalid·revoked JWT logout을 성공 응답으로 정규화
- **심각도:** High
- **상태:** 수정 완료
- **관련 요구사항:** `MOCK-002`, `AUTH-013`
- **관련 계약:** API Contract §1.2, §3.2~3.3
- **소유 Task:** 신규 R2.2, goal `P2-R2`
**관찰 내용**
logout handler는 Bearer 문자열 존재와 body 없음만 검사한 뒤 token 유효성·폐기 상태를 확인하지 않고 store에 추가하고 200을 반환한다. 임의 token과 이미 revoke된 admin token의 두 번째 logout도 성공한다.
**근거**
- 코드: `src/shared/mocks/handlers.ts:107-118``getTokenAccess`를 호출하지 않고 모든 non-empty token을 revoke한 뒤 `ok({})`를 반환한다.
- 계약: API Contract §1.2는 잘못됨·만료·폐기 JWT를 401로 규정한다.
- 실행: invalid token logout과 정상 logout 뒤 같은 admin token의 재logout이 모두 200이었다.
- 테스트 공백: `src/shared/mocks/__tests__/auth-handlers.test.ts:65-82`는 Bearer 없음과 body 있음만 확인한다.
**재현 또는 검증 절차**
1. 새 mock store에서 `Bearer invalid-token`으로 `POST /member/logout`을 호출한다.
2. `Bearer mock-admin-jwt`로 정상 logout한 뒤 같은 token으로 다시 호출한다.
3. 실제 결과는 두 요청 모두 200 success envelope다.
4. 요구되는 결과는 invalid·revoked JWT에 401 error envelope를 반환하는 것이다.
**영향**
mock preview에서 서버 logout 실패 경고와 폐기 token 계약을 검증할 수 없고, 실제 서버에서 발생할 401을 성공으로 오인한다. `AUTH-013`의 로컬 session 제거는 유지되더라도 실패 피드백 QA가 왜곡된다.
**권장 조치**
store mutation 전에 token 상태를 판정하고 invalid·revoked token은 API Contract의 401 envelope로 반환한다. 정상 login → logout → login 회귀와 invalid·double logout 경계 test를 함께 둔다. 공통 logout의 다른 회원 role 의미는 제공 계약 이상으로 추정하지 않는다.
**판정 기록**
- 2026-07-27 — invalid·revoked token 200을 런타임으로 재현해 오류 status 계약 위반을 확정했다.
### REV-P2-004 — 보호 shell 빈 상태가 Character 연결 시점을 완료된 Phase 2로 표시
- **심각도:** Low
- **상태:** 수정 완료
- **관련 요구사항:** Phase 지도와 사용자 상태 안내
- **관련 계약:** 없음
- **소유 Task:** 신규 R2.3, goal `P2-R3`
**관찰 내용**
현재 plan에서 Character workspace는 Phase 3이지만 보호 shell의 설명과 빈 상태 제목은 모두 “Phase 2에서 AI 캐릭터 목록이 연결됩니다.”라고 표시한다. Phase 2를 완료한 현재 화면에서도 다음 작업 시점을 잘못 안내한다.
**근거**
- 코드: `src/app/admin-pages.tsx:27,34`의 사용자 표시가 Phase 2를 가리킨다.
- 문서: `plan-task.md` Phase 지도와 `## Phase 3. Character workspace vertical slice`는 Character 목록/workspace를 Phase 3 소유로 둔다.
- 테스트: `src/app/App.test.tsx`도 기존 Phase 2 문구를 assertion해 잘못된 표시를 고정한다.
**재현 또는 검증 절차**
1. ADMIN으로 `/ai-characters` 보호 shell을 연다.
2. 설명과 empty state 제목에서 Phase 2 안내를 확인한다.
3. 실제 결과는 완료된 Phase 2를 다음 연결 시점으로 표시한다.
4. 요구되는 결과는 현재 계획의 Phase 3 안내다.
**영향**
기능 자체는 막지 않지만 운영자·reviewer가 Phase 2 완료 상태와 Character 구현 범위를 잘못 이해할 수 있다.
**권장 조치**
두 사용자 문구와 관련 App test를 Phase 3으로 갱신한다. Phase 3 기능 자체는 이 수정에 포함하지 않는다.
**판정 기록**
- 2026-07-27 — 실제 UI 문자열과 Phase 지도를 대조해 문서·사용자 안내 불일치로 확정했다.
### REV-P2-005 — Phase 2 Files·하단 Progress가 실제 구현과 불일치
- **심각도:** Low
- **상태:** 수정 완료
- **관련 요구사항:** `plan-task.md` 구현 완료 정의의 문서·구현 차이 0건, Goal 실행형 계획 유지보수 규칙
- **관련 계약:** 없음
- **소유 Task:** 신규 R2.3, goal `P2-R3`
**관찰 내용**
Phase 2 주요 Files는 존재하지 않는 `src/app/providers.tsx`, `fixtures.ts`, `store.ts`, `handlers.test.ts`, `store.test.ts`를 완료 산출물처럼 유지하고, 실제 `App.tsx`, `vite.config.ts`, `playwright.config.ts`, `contract.ts`, auth/docs/production test와 두 boundary E2E를 기록하지 않는다. 또한 리뷰 시작 시 하단 `## 7. 검증 기록`의 마지막 항목은 Phase 2 착수 전 계획 추가 기록이었고, 완료한 Phase 2 구현의 무엇을/왜/실제 명령·결과가 누적되지 않았다.
**근거**
- 문서: `plan-task.md` Phase 2 `주요 Files`와 실제 staged 28개 경로가 일치하지 않는다.
- 파일 검사: `src/app/providers.tsx`, `src/shared/mocks/fixtures.ts`, `store.ts`, `__tests__/handlers.test.ts`, `store.test.ts`는 존재하지 않는다.
- 실제 구현: `src/app/App.tsx`, `vite.config.ts`, `playwright.config.ts`, `src/shared/mocks/contract.ts`, 세부 contract/docs/production test와 `mock-mode-boundary.spec.ts`, `server-mode-boundary.spec.ts`가 Phase 2 변경에 포함된다.
- 문서: Task·Gate별 실행 기록은 Phase 본문에 있으나 하단 Progress에는 구현 완료 기록이 없었다. 이 리뷰 기록은 새로 누적했지만 실제 Phase 2 구현 Progress와 Files 정정은 아직 수행하지 않았다.
**재현 또는 검증 절차**
1. Phase 2 `주요 Files`의 각 경로 존재 여부를 검사한다.
2. `git diff --cached --name-status`의 실제 구현 경로와 대조한다.
3. `plan-task.md` 하단 `## 7. 검증 기록`의 마지막 구현 기록을 확인한다.
4. 실제 결과는 Files와 Progress 모두 현재 Phase 2 구현과 다르다.
**영향**
독립 reviewer가 어떤 파일이 Phase 2 소유인지, 어떤 최종 명령으로 완료했는지 plan의 표준 위치에서 재현하기 어렵다. 이후 goal 실행자가 존재하지 않는 파일을 전제로 작업할 수 있다.
**권장 조치**
Phase 2 주요 Files를 실제 최소 구조로 갱신하고, 하단 Progress에 기존 본문 기록을 지우지 않은 채 무엇을/왜/실제 명령·결과/남은 항목을 누적한다. 사용자 Phase 문구 정정과 함께 문서/App focused test를 실행한다.
**판정 기록**
- 2026-07-27 — 파일 존재 검사, staged name-status와 하단 Progress를 교차 확인해 문서 정합성 문제로 확정했다.
### REV-P2-006 — 한 번 검증된 동일 세션의 재진입 probe 실패가 보호 shell을 다시 노출
- **심각도:** High
- **상태:** 수정 완료
- **관련 요구사항:** `AUTH-003`, `MOCK-003`
- **관련 계약:** API Contract §3.3, 기존 `P2-R1`
- **소유 Task:** R2.4, goal `P2-R4`
**관찰 내용**
같은 session 객체가 한 번 보호 route probe를 통과하면 `verifiedProtectedRouteSession`이 유지된다. session을 지우지 않고 `/login`을 거쳐 `/ai-characters`에 다시 들어올 때 새 probe가 시작돼도 이전 검증 값과 현재 session이 이미 같으므로 보호 shell을 즉시 렌더한다. 두 번째 probe가 404 또는 network error로 실패해도 검증 값은 무효화되지 않아 오류 alert를 보호 shell 내부에 표시한다.
**근거**
- 코드: `src/app/App.tsx:189-190`이 오류와 검증 session을 component state로 보관한다.
- 코드: `src/app/App.tsx:198-233`은 새 probe 시작 시 기존 `verifiedProtectedRouteSession`을 무효화하지 않는다.
- 코드: `src/app/App.tsx:258-264`은 이전 검증 session과 현재 session의 객체 동일성만으로 shell 렌더를 허용한다.
- 테스트 공백: `src/app/App.test.tsx:208-294`는 최초 probe 실패와 logout → 새 session 재로그인은 다루지만, 성공한 동일 session의 route 이탈 → 재진입 실패는 다루지 않는다.
- 런타임: 최초 probe 2회 200 뒤 동일 session 재진입 probe를 404로 응답했을 때 `protectedMainCount=1`, `logoutCount=1`이었다.
**재현 또는 검증 절차**
1. ADMIN session으로 `/ai-characters` probe를 성공시켜 보호 shell을 연다.
2. session을 유지한 채 SPA navigation으로 `/login`에 이동한다.
3. `/ai-characters`로 다시 이동하고 새 probe를 404 또는 network error로 실패시킨다.
4. 실제 결과는 `보호 route 확인에 실패했습니다.` alert와 보호 `main`·로그아웃 버튼이 함께 남는다.
5. 요구되는 결과는 새 probe 성공 전과 실패 후에 보호 shell이 노출되지 않는 것이다.
**영향**
현재 DB ADMIN 여부를 다시 확인하지 못한 상태에서도 이전 성공을 재사용해 보호 navigation과 action surface를 노출한다. Phase 3 이후 mutation UI가 같은 shell에 추가되면 `P2-R1`의 fail-closed 보장이 route 재진입 경로에서 다시 깨진다.
**권장 조치**
검증 상태를 session 객체뿐 아니라 현재 probe 시도에 귀속하고, route 재진입 시 이전 성공을 먼저 무효화한다. 성공 → route 이탈 → 동일 session 재진입 pending·404·network·403을 하나의 회귀 test cycle로 고정한다.
**판정 기록**
- 2026-07-27 — 코드 상태 전이와 Chromium one-off Playwright 재현 결과로 확정했다.
- 2026-07-27 — `P2-R4`에서 route visit key 회귀 test와 server E2E로 수정 완료를 확인했다.
### REV-P2-007 — auth fixture가 비ADMIN status와 login media type 계약을 완화
- **심각도:** High
- **상태:** 수정 완료
- **관련 요구사항:** `AUTH-006`, `AUTH-008`, `MOCK-002`
- **관련 계약:** API Contract §1.2, §3.1~3.3
- **소유 Task:** R2.5, goal `P2-R5`
**관찰 내용**
logout handler는 `getTokenAccess(token) !== "admin"`인 모든 token을 401로 합쳐 이미 `denied`로 분류한 비ADMIN token도 403 대신 401을 반환한다. login handler는 request의 `Content-Type`을 확인하지 않고 `request.json()` 결과만 파싱해 `text/plain` JSON body도 200 ADMIN 응답으로 처리한다.
**근거**
- 코드: `src/shared/mocks/handlers.ts:37-45``memberToken``denied`로 분류한다.
- 코드: `src/shared/mocks/handlers.ts:114-125`는 logout에서 `denied``unauthorized`를 모두 401로 반환한다.
- 코드: `src/shared/mocks/handlers.ts:85-99`는 login Authorization과 JSON shape만 검사하고 media type을 확인하지 않는다.
- 계약: API Contract §1.2는 JWT role 비ADMIN을 403, 지원하지 않는 media type을 415로 규정한다. §3.1은 login `Content-Type: application/json`을 요구한다.
- 테스트 공백: `auth-handlers.test.ts`의 403 test는 Character probe만 확인하고 비ADMIN logout·잘못된 login media type을 확인하지 않는다.
- 런타임: 현재 handler는 비ADMIN logout 401, `text/plain` login 200을 반환했다.
**재현 또는 검증 절차**
1. `Bearer mock-member-jwt``POST /member/logout`을 body 없이 호출한다.
2. `Content-Type: text/plain`과 유효한 JSON 문자열 body로 `POST /admin/member/login`을 호출한다.
3. 실제 결과는 각각 401과 200이다.
4. 요구되는 결과는 API Contract에 따른 403과 415 오류 envelope다.
**영향**
mock mode가 실제 서버보다 넓은 요청을 성공시키고 오류 status를 다르게 반환한다. auth serializer·권한 처리 회귀가 mock happy path에서 숨겨져 `MOCK-002`의 production 동일 계약 검증 신뢰성이 낮아진다.
**권장 조치**
logout에서도 공통 token access 분기를 재사용해 `denied``unauthorized`를 구분하고, login JSON 파싱 전에 media type을 검증한다. 비ADMIN logout 403과 `text/plain`·누락·잘못된 media type 경계 test를 추가한다.
**판정 기록**
- 2026-07-27 — API Contract와 handler 분기를 대조하고 MSW Node runtime status를 재현해 확정했다.
- 2026-07-27 — `P2-R5`에서 비ADMIN logout 403과 non-JSON login 415 test로 수정 완료를 확인했다.
### REV-P2-008 — Mock Preview 403·보호 오류 화면에서 지속 안내가 사라짐
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** `MOCK-008`
- **관련 계약:** API Contract §1.5
- **소유 Task:** R2.6, goal `P2-R6`
**관찰 내용**
mock banner는 login과 검증 성공 후 `ProtectedAdminShell`에만 배치된다. 보호 probe가 403으로 전환한 `AccessDeniedPage`와 404·network error를 표시하는 `ProtectedRouteErrorPage`에는 banner가 없어 mock mode 화면이 실제 서버 화면처럼 보인다.
**근거**
- 코드: `src/app/App.tsx:235-247`, `264`는 login과 보호 shell에 `apiMode`를 전달한다.
- 코드: `src/app/App.tsx:254-261`은 접근 거부·보호 오류 화면을 banner 없이 직접 반환한다.
- 요구사항: `MOCK-008`은 mock mode 화면에 실제 서버가 아니라는 지속적으로 보이는 안내를 요구한다.
- 테스트 공백: App·mock E2E는 login과 성공 shell의 banner만 확인하고 403·404·network 상태는 확인하지 않는다.
- 런타임: mock mode에서 stale ADMIN 역할을 나타내는 `mock-member-jwt`로 진입한 `/access-denied``mockBannerCount`가 0이었다.
**재현 또는 검증 절차**
1. mock mode에서 `sessionStorage``{ token: "mock-member-jwt", role: "ADMIN" }`을 저장한다.
2. `/ai-characters`에 진입해 mock handler의 403 응답을 받는다.
3. `/access-denied` heading은 보이지만 `Mock Preview` status는 존재하지 않는다.
4. 요구되는 결과는 보호 shell을 노출하지 않으면서도 mock mode 안내는 지속되는 것이다.
**영향**
권한·연결 오류를 QA하는 사용자가 현재 응답이 mock fixture에서 나온 것인지 실제 서버에서 나온 것인지 구분하기 어렵다. 성공 화면보다 오류 화면에서 환경 혼동 가능성이 커진다.
**권장 조치**
Mock Preview 안내를 route별 page 내부가 아니라 모든 mock route state를 감싸는 공통 상위 경계에 한 번만 배치하고, login·성공 shell·403·404/network 상태 test를 추가한다.
**판정 기록**
- 2026-07-27 — 코드 composition과 Chromium mock runtime 결과로 확정했다.
- 2026-07-27 — `P2-R6`에서 mock 403·보호 오류 banner test와 mock E2E로 수정 완료를 확인했다.
### REV-P2-009 — bare server·mock E2E 명령이 반대 mode spec까지 함께 수집
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** `MOCK-001`, Phase 2 `P2-T1`, 실행 스크립트·README 동기화
- **관련 계약:** 없음
- **소유 Task:** R2.7, goal `P2-R7`
**관찰 내용**
`npm run e2e``npm run e2e:mock`은 환경 변수와 web server만 바꾸고 동일한 `tests/e2e` 전체를 실행한다. server 명령도 mock worker 등록을 요구하는 spec을, mock 명령도 server worker 0건과 page-level route interception을 전제로 한 spec을 함께 수집한다.
**근거**
- 설정: `package.json:16-17`의 두 script는 spec filter 없이 `playwright test`를 실행한다.
- 설정: `playwright.config.ts:3-22`는 web server mode만 고르고 mode별 `testMatch`, `testIgnore`, project dependency를 정의하지 않는다.
- 문서: README와 `docs/agent-guide/scripts.md`는 bare 두 명령을 각각 server mode와 mock mode Playwright로 안내한다.
- 실행: 두 `--list` 명령이 모두 같은 6 files / 56 tests를 수집했다.
- 실행: bare `npm run e2e:mock`은 server 전용·기존 shell spec 충돌로 36 통과 / 20 실패했다.
- 대조: mode에 맞는 spec을 명시한 mock 24 tests와 server 32 tests는 각각 모두 통과했다.
**재현 또는 검증 절차**
1. `npm run e2e -- --list``npm run e2e:mock -- --list`를 실행한다.
2. 두 명령이 모두 `mock-*.spec.ts``server-mode-boundary.spec.ts`를 포함한 같은 56 tests를 출력한다.
3. `npm run e2e:mock`을 bare로 실행하면 20 tests가 실패한다.
4. 요구되는 결과는 문서의 각 bare 명령이 자기 mode에 유효한 spec 집합만 실행해 exit 0을 반환하는 것이다.
**영향**
표준 QA 명령이 mode별로 독립 재현되지 않고, 사용자는 계획에 숨겨진 spec 경로를 알아야만 통과 결과를 얻는다. CI나 후속 Phase가 bare script를 사용하면 정상 변경도 실패로 판정한다.
**권장 조치**
현재 단일 Playwright config 안에서 `apiMode`에 따라 가장 작은 `testMatch` 또는 `testIgnore` 경계를 두고, bare 두 명령 자체를 contract test와 Gate에 포함한다. 별도 config 복제는 필요할 때까지 만들지 않는다.
**판정 기록**
- 2026-07-27 — package/config 추적, 동일 test 목록과 bare mock 실행 실패로 확정했다.
- 2026-07-27 — `P2-R7`에서 mode별 script contract, `--list`, bare E2E 실행으로 수정 완료를 확인했다.
### REV-P2-010 — Phase 2 완료 문서가 P2-R4~P2-R7 변경과 불일치
- **심각도:** Low
- **상태:** 수정 완료
- **관련 요구사항:** `plan-task.md` 구현 완료 정의의 문서·구현 차이 0건, Goal 실행형 계획 유지보수 규칙
- **관련 계약:** 없음
- **소유 Task:** R2.8, goal `P2-R8`
**관찰 내용**
`P2-R4`~`P2-R7`에서 새 코드·test 경로와 검증이 추가됐지만 Phase 2 `주요 Files`, review 상단 범위·working tree 경로 수와 종료 판정은 이전 상태를 유지한다. review는 코드·문서 정합성을 충족으로 판정하지만 현재 표준 위치만으로는 최종 변경 범위를 재현할 수 없다.
**근거**
- 문서: `plan-task.md:589-600``주요 Files``src/app/App.test.tsx`, `src/app/admin-pages.tsx`, `src/app/browser-location.ts`, `src/app/protected-admin-shell.tsx`가 없다.
- 문서: `plan-task.md:829``P2-R4`에서 `src/app/protected-admin-shell.tsx`를 새로 분리했다고 기록한다.
- 문서: 3차 반영 전 review 상단은 `P2-R3`까지만 대상으로 적고 변경 36개 경로라고 기록했으나 상세와 종료 판정은 `P2-R7`까지 완료됐다고 적었다.
- 실행: `git diff HEAD --name-only | wc -l`은 37개 경로를 반환했다.
- 규칙: `docs/agent-guide/goal-plan.md`는 Task별 정확한 Files·Interfaces와 범위 변경 전 plan 갱신을 요구한다.
**재현 또는 검증 절차**
1. `git diff HEAD --name-only`로 현재 변경 경로를 수집한다.
2. Phase 2 `주요 Files``P2-R4`~`P2-R7` 기록과 대조한다.
3. review 상단의 대상·경로 수와 `## 8. 리뷰 종료 판정`을 대조한다.
4. 실제 결과는 코드·test 경로 누락, 대상 범위와 경로 수 불일치, 정합성 충족 판정의 상충이다.
**영향**
독립 실행자와 reviewer가 최종 Phase 2 소유 파일과 최신 Gate 근거를 표준 위치에서 재현할 수 없다. 기능 동작에는 직접 영향이 없지만 완료 문서의 추적성과 종료 판정 신뢰성이 낮아진다.
**권장 조치**
문서 전용 `P2-R8`에서 Phase 2 Files, 완료 Task의 Files·Interfaces, review 범위·경로 수와 3차 검증 기록을 현재 working tree에 맞춰 누적 정정한다. 기존 완료 체크와 과거 수치는 삭제하지 않는다.
**판정 기록**
- 2026-07-27 — 현재 diff 37개 경로, plan Files와 review 메타데이터를 교차 확인해 문서 추적성 문제로 확정했다.
### REV-P2-011 — 열린 mobile menu가 desktop 전환 후 숨은 overlay와 inert shell을 남김
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** PRD §10.6 반응형 원칙, §10.7 접근성 최소 기준, `P2-T3`
- **관련 계약:** 없음
- **소유 Task:** R2.9, goal `P2-R9`
**관찰 내용**
320px에서 mobile menu를 연 뒤 viewport를 `lg` 이상으로 넓히면 overlay는 `lg:hidden`으로 사라지지만 `isMobileMenuOpen`은 계속 `true`다. 따라서 화면에 다시 나타난 desktop shell은 `inert``aria-hidden=true`를 유지하고 navigation·logout·main을 조작할 수 없다.
**근거**
- 코드: `src/app/protected-admin-shell.tsx:93``isMobileMenuOpen`만으로 shell background에 `inert``aria-hidden`을 적용한다.
- 코드: `src/app/protected-admin-shell.tsx:133`은 overlay를 CSS `lg:hidden`으로만 숨기며 breakpoint 전환 시 state를 닫는 처리가 없다.
- 테스트 공백: `tests/e2e/accessibility-shell.spec.ts``mock-preview-shell.spec.ts`는 고정 320px menu 동작만 확인하고 열린 상태의 1,024px·1,200px 전환을 검증하지 않는다.
- 런타임: 320px menu open에서 overlay `display=block`, main inert를 확인한 뒤 1,200px에서 overlay `display=none`, main inert ancestor의 `aria-hidden=true`가 그대로 남았다.
**재현 또는 검증 절차**
1. Mock Preview ADMIN shell을 320px viewport로 열고 mobile menu를 연다.
2. menu를 닫지 않고 viewport를 1,200px로 변경한다.
3. overlay의 computed `display``main.closest('[inert]')`를 확인한다.
4. 실제 결과는 overlay `display:none`, main의 inert ancestor `aria-hidden=true`다. 요구되는 결과는 breakpoint 전환과 함께 menu state·inert·aria-hidden을 해제하는 것이다.
**영향**
좁은 desktop 창을 넓히거나 responsive QA 중 breakpoint를 넘으면 보이는 desktop shell 전체가 pointer·keyboard·보조기기 조작 불가 상태가 된다. 다시 좁혀 menu를 닫기 전에는 화면 안에서 복구할 수 없다.
**권장 조치**
native viewport query를 사용해 `lg` 진입 시 mobile menu state를 닫고, breakpoint 자동 종료에서는 숨겨진 trigger로 focus를 복귀하지 않는다. 320px open → 1,024px·1,200px 전환 회귀 test와 desktop control 조작 E2E를 추가한다.
**판정 기록**
- 2026-07-27 — Chromium viewport 전환에서 hidden overlay와 inert shell 잔존을 직접 재현해 확정했다.
- 2026-07-27 — `P2-R9`에서 `matchMedia('(min-width: 1024px)')` change로 menu state를 닫고 focus 복귀를 차단했다. App focused unit과 mock e2e가 1,024px·1,200px 전환 뒤 mobile overlay·inert·`aria-hidden` 잔존 0건을 확인해 수정 완료로 판정했다.
### REV-P2-012 — 열린 mobile menu의 background inert 경계에서 Mock Preview banner가 제외됨
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** PRD §10.7 접근성 최소 기준, `MOCK-008`, `P2-R6`
- **관련 계약:** API Contract §1.5
- **소유 Task:** R2.9, goal `P2-R9`
**관찰 내용**
`P2-R6`에서 Mock Preview banner를 `RouteFrame`으로 올린 뒤 banner는 `ProtectedAdminShell`이 menu open 시 적용하는 background inert·`aria-hidden` 경계의 형제가 됐다. menu가 열려도 banner는 accessibility tree에 남아 overlay navigation과 background 상태가 동시에 노출된다.
**근거**
- 코드: `src/app/App.tsx:42-48``RouteFrame`은 banner와 `ProtectedAdminShell`을 형제로 렌더한다.
- 코드: `src/app/protected-admin-shell.tsx:93`의 inert·`aria-hidden`은 shell 내부 background에만 적용된다.
- 테스트 공백: App/mock E2E는 banner 표시와 menu focus containment을 각각 확인하지만 menu open 상태의 banner inert 귀속은 확인하지 않는다.
- 런타임: 320px menu open에서 main은 inert 내부였지만 banner의 `closest('[inert]')``null`, `aria-hidden``null`이었다.
**재현 또는 검증 절차**
1. mock mode ADMIN shell에서 mobile menu를 연다.
2. Mock Preview banner와 main 각각의 `closest('[inert]')`를 확인한다.
3. 실제 결과는 main만 inert background에 포함되고 banner는 포함되지 않는다.
4. 요구되는 결과는 시각적 banner를 유지하되 열린 navigation 외 background 전체가 같은 inert·`aria-hidden` 경계에 포함되는 것이다.
**영향**
키보드 focus는 menu 안에 머물지만 screen reader virtual cursor는 background banner를 계속 탐색할 수 있어 overlay 맥락과 background 차단 의미가 불일치한다.
**권장 조치**
성공 shell에서는 banner와 shell 본문을 같은 background inert container로 조합하고, login·403·보호 오류 화면의 공통 banner 배치는 유지한다. menu open 상태의 banner role 미노출과 menu close 후 복원을 unit/E2E로 고정한다.
**판정 기록**
- 2026-07-27 — DOM inert 귀속과 aria-hidden 값을 Chromium에서 확인해 접근성 경계 회귀로 확정했다.
- 2026-07-27 — `P2-R9`에서 성공 shell의 `MockModeBanner``ProtectedAdminShell` background inert container 안으로 이동했다. 320px menu open 상태의 unit/e2e가 banner와 main의 같은 inert·`aria-hidden` 경계를 확인해 수정 완료로 판정했다.
### REV-P2-013 — 보호 route 오류 화면에 fail-closed 재시도·복구 경로가 없음
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** PRD §10.5 오류 화면 상태, `MOCK-003`, `P2-R1`
- **관련 계약:** API Contract §1.2, §1.5
- **소유 Task:** R2.10, goal `P2-R10`
**관찰 내용**
저장된 ADMIN session의 보호 route probe가 404 또는 network error로 실패하면 shell 밖 오류 alert만 표시된다. 오류 page에는 재시도, 로그인 이동 또는 session 종료 control이 없어 일시적인 연결 실패가 복구돼도 사용자는 browser refresh·주소 이동에 의존해야 한다.
**근거**
- 코드: `src/app/App.tsx:32-40``ProtectedRouteErrorPage`는 message를 담은 `p[role=alert]`만 렌더한다.
- 코드: `src/app/App.tsx:138-143`은 미검증 상태에서 이 page만 반환하며 retry attempt를 시작할 action을 전달하지 않는다.
- 요구사항: PRD §10.5는 오류 상태에 서버의 한국어 message와 재시도를 요구하고 `MOCK-003`은 server mode 오류를 실제 오류 UI로 처리하도록 한다.
- 테스트 공백: `src/app/App.test.tsx:224-263``tests/e2e/server-mode-boundary.spec.ts:28-68`은 alert와 shell·logout 비노출만 확인하고 연결 복구 뒤 사용자 retry를 검증하지 않는다.
**재현 또는 검증 절차**
1. ADMIN session으로 `/ai-characters`에 진입하고 probe를 404 또는 network error로 실패시킨다.
2. `보호 route 확인에 실패했습니다.` alert가 표시된 뒤 button·link를 확인한다.
3. 실제 결과는 interactive recovery control 0건이다.
4. 요구되는 결과는 보호 shell을 계속 숨긴 채 사용자가 명시적으로 probe를 재시도하고, 성공한 현재 attempt 뒤에만 shell을 여는 것이다.
**영향**
일시적인 network 장애에서도 앱 내부 복구가 불가능하다. 사용자가 browser refresh를 알지 못하면 보호 화면 진입을 완료할 수 없고, 오류 상태의 접근 가능한 recovery 기준도 충족하지 못한다.
**권장 조치**
자동 retry나 mock fallback을 추가하지 않고 오류 page에 keyboard-accessible retry button을 제공한다. 404 → retry 성공, network → retry 실패·성공을 test하고 모든 비성공 attempt에서 shell·logout 비노출을 유지한다. 정상 오류 envelope의 한국어 message는 보존하고 network·invalid response에는 공통 안내를 사용한다.
**판정 기록**
- 2026-07-27 — 오류 page 코드와 App/server boundary test를 PRD 오류 상태 기준에 대조해 복구 경로 누락으로 확정했다.
- 2026-07-27 — `P2-R10`에서 retry attempt state와 native retry button을 추가했다. 404 → retry 성공, network → retry 실패 unit과 server mode E2E가 성공 전 shell·logout 비노출, 서버 message 보존, mock worker 0건을 확인해 수정 완료로 판정했다.
### REV-P2-014 — working tree 범위 집계가 untracked 보호 shell을 제외
- **심각도:** Low
- **상태:** 수정 완료
- **관련 요구사항:** Goal 실행형 계획 유지보수 규칙, 리뷰 범위 전체 확인
- **관련 계약:** 없음
- **소유 Task:** R2.11, goal `P2-R11`
**관찰 내용**
`P2-R8``git diff HEAD --name-only`의 37개 결과를 현재 working tree 전체 범위로 사용해 review를 종료했다. 하지만 새 `src/app/protected-admin-shell.tsx`는 untracked라 이 명령에 포함되지 않으며 실제 status에는 38개 변경 항목이 있다.
**근거**
- 실행: `git diff HEAD --name-only | wc -l`은 37, `git status --porcelain=v1 | wc -l`은 38을 반환했다.
- 실행: `git ls-files --others --exclude-standard``src/app/protected-admin-shell.tsx`를 반환했다.
- 문서: `plan-task.md:954-960``P2-R8` 검증은 `git diff HEAD --name-only`만 사용하고 untracked 확인 명령이 없다.
- 문서: 4차 반영 전 review 상단과 종료 판정은 37개를 현재 working tree 전체 범위로 기록했다.
**재현 또는 검증 절차**
1. `git diff HEAD --name-only | wc -l`로 tracked diff를 센다.
2. `git status --porcelain=v1 | wc -l``git ls-files --others --exclude-standard`를 실행한다.
3. 실제 결과는 tracked 37개, untracked 1개, working tree 전체 38개 항목이다.
4. 요구되는 결과는 review 범위와 대체 검증이 tracked·untracked를 구분해 전체 38개를 추적하는 것이다.
**영향**
코드 동작에는 직접 영향이 없지만 새 파일이 review 대상·diff 감사에서 빠질 수 있다. 문서 정합성 복구를 완료했다는 `P2-R8`의 검증 방법과 종료 판정 신뢰성이 다시 낮아진다.
**권장 조치**
문서 전용 `P2-R11`에서 과거 37개 tracked 결과는 보존하고 현재 범위를 tracked 37개 + untracked 1개로 명시한다. 후속 검증은 `git status --short --untracked-files=all`과 untracked 목록을 함께 사용한다.
**판정 기록**
- 2026-07-27 — tracked diff, porcelain status와 untracked 목록의 수치 차이를 직접 확인해 문서 범위 누락으로 확정했다.
- 2026-07-27 — `P2-R11`에서 현재 범위를 tracked 37개 + untracked 1개 = 38개로 정정하고 `git status --short --untracked-files=all`과 untracked 목록 검증을 누적해 수정 완료로 판정했다.
### REV-P2-015 — 보호 route probe pending 중 root가 비어 진행 상태를 알리지 않음
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** PRD §10.5 loading·300ms 비동기 피드백, §10.7 비동기 상태 live region, `P2-R10`
- **관련 계약:** API Contract §1.5
- **소유 Task:** R2.12, goal `P2-R12`
**관찰 내용**
최초 보호 route probe 또는 오류 화면의 수동 retry가 pending이면 미검증 branch가 `null`을 반환한다. 350ms 이상 응답이 지연돼도 화면과 접근성 tree에 loading 안내가 없고, retry button을 누른 keyboard focus는 제거된 element에서 `BODY`로 이동한다.
**근거**
- 코드: `src/app/App.tsx:161-167`은 현재 attempt가 미검증이고 error가 없으면 `null`을 반환한다.
- 코드: `src/app/App.tsx:171-173`은 retry click에서 error를 먼저 지우고 retry key를 증가시켜 같은 빈 branch로 전환한다.
- 테스트 공백: `src/app/App.test.tsx:397-400`은 retry pending의 shell·logout 비노출만 확인하고 visible status·loading feedback은 확인하지 않는다.
- 요구사항: PRD §10.5는 첫 loading 상태와 300ms 이상 작업의 시각 피드백을, §10.7은 비동기 결과·오류의 적절한 live region을 요구한다.
- 런타임: retry 성공 응답을 350ms 지연했을 때 `#root` child 0개, `main`·`status`·`alert` 0개, active element `BODY`를 Chromium에서 확인했다.
**재현 또는 검증 절차**
1. ADMIN session의 probe를 404로 응답해 retry button을 표시한다.
2. 다음 성공 응답을 350ms 이상 지연하고 retry button을 누른다.
3. pending 동안 `#root`, landmark, `role=status|alert`와 active element를 확인한다.
4. 실제 결과는 빈 root와 `BODY` focus다. 요구되는 결과는 보호 shell은 숨기되 `RouteFrame` 안의 지속적인 한국어 loading status를 제공하는 것이다.
**영향**
느린 네트워크에서 사용자는 retry가 시작됐는지, 앱이 멈췄는지 구분할 수 없다. 보조기기 사용자에게는 비동기 진행 상태가 전혀 전달되지 않으며 초기 보호 route 진입도 같은 빈 화면을 공유한다.
**권장 조치**
새 loading 시스템을 만들지 않고 기존 `PageState` loading variant를 미검증 branch에 조합한다. 최초 probe와 retry pending unit test, 350ms 지연 server boundary E2E로 visible status 1건과 보호 shell·logout 0건을 함께 고정한다.
**판정 기록**
- 2026-07-27 — 코드 branch와 350ms Chromium 지연 재현을 PRD 비동기 상태 기준에 대조해 확정했다.
- 2026-07-27 — `P2-R12`에서 미검증 branch에 기존 `PageState` loading status를 표시하고 App focused test와 server boundary E2E로 pending 중 status 1건, 보호 shell·logout 0건을 확인해 수정 완료로 판정했다.
### REV-P2-016 — 고정 E2E script가 focused 필터와 network retry 증거 공백을 가림
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** `P2-R7`, `P2-R10` 완료 증거, Goal 실행형 계획 검증 규칙
- **관련 계약:** API Contract §1.5
- **소유 Task:** R2.13, goal `P2-R13`
**관찰 내용**
`e2e`·`e2e:mock` script가 mode별 모든 spec 경로를 명령 자체에 고정한다. 따라서 plan에 기록된 `npm run e2e -- tests/e2e/server-mode-boundary.spec.ts`는 focused 실행이 아니라 고정 4개 spec을 모두 실행한다. 이 32 tests 통과가 `P2-R10`의 server boundary 기대 16 tests 미달과 network retry E2E 부재를 가렸다.
**근거**
- 설정: `package.json:16-17`은 server 4개, mock 2개 spec을 script 인수로 고정한다.
- 실행: `npm run e2e -- --list tests/e2e/server-mode-boundary.spec.ts`는 4 files / 32 tests를 수집했다.
- 대조 실행: `VITE_API_MODE=server npx playwright test tests/e2e/server-mode-boundary.spec.ts --list`는 1 file / 12 tests만 수집했다.
- 계획: `plan-task.md``P2-R10` 기대 결과는 server boundary 4 projects / 16 tests 이상이다.
- 테스트 공백: `tests/e2e/server-mode-boundary.spec.ts:76-93`은 최초 network error만 확인하고 retry button을 누르지 않는다. network retry 재실패는 `src/app/App.test.tsx:405-430`에만 있다.
- 기록: `P2-R10` RED/GREEN E2E는 focused 명령이 32 tests를 실행한 결과를 boundary E2E 증거로 사용했다.
**재현 또는 검증 절차**
1. npm script를 통한 server boundary `--list`와 직접 Playwright boundary `--list`를 각각 실행한다.
2. 전자는 4 files / 32 tests, 후자는 1 file / 12 tests임을 확인한다.
3. boundary spec에서 network error test가 retry를 수행하는지 확인한다.
4. 요구되는 결과는 bare mode allowlist를 유지하면서 CLI file filter는 1개 file만 수집하고, boundary spec이 network retry 재실패까지 포함해 16 tests 이상을 실행하는 것이다.
**영향**
focused 회귀 명령이 느리고 범위를 오해하게 하며, 무관한 spec 통과 수가 Task별 완료 기준 미달을 숨긴다. network retry의 실제 browser no-fallback·fail-closed 경계가 unit test에만 의존한다.
**권장 조치**
script에는 mode env와 `playwright test`만 두고 mode별 exact allowlist는 단일 `playwright.config.ts``testMatch`로 이동해 CLI filter와 교집합으로 동작하게 한다. network error → retry → network 재실패 E2E를 추가하고 focused 16, bare server 36, bare mock 28 tests를 각각 검증한다.
**판정 기록**
- 2026-07-27 — npm/direct list 결과와 boundary spec 시나리오 수를 `P2-R10` 기대 결과에 대조해 검증 증거 회귀로 확정했다.
- 2026-07-27 — `P2-R13`에서 npm scripts를 bare `playwright test`로 되돌리고 mode별 allowlist를 `playwright.config.ts` `testMatch`로 이동했다. focused list 1 file / 16 tests, network retry 재실패 E2E 16 tests 통과를 확인해 수정 완료로 판정했다.
### REV-P2-017 — P2-R12 test 증거가 실제 test 구조·실행 수와 불일치
- **심각도:** Low
- **상태:** 수정 완료
- **관련 요구사항:** Goal 실행형 계획의 완료 증거·Progress 정합성 규칙, 리뷰 종료 조건
- **관련 계약:** 없음
- **소유 Task:** R2.14, goal `P2-R14`
**관찰 내용**
`P2-R12`의 완료 체크는 최초 probe pending과 retry pending “두 test를 추가”했고 App test가 28건 이상이어야 한다고 기록한다. 실제 파일은 최초 pending 전용 test 1건을 추가하고 기존 404 retry test에 pending assertion을 보강한 27 tests 구조이며, 수정 검증 기록도 이를 “신규 pending status tests 2건”이라고 잘못 설명한다.
**근거**
- 코드: `src/app/App.test.tsx``test(` 선언은 27건이다.
- 코드: 최초 pending은 `shows an accessible status while the initial protected route probe is pending` 전용 test로 추가됐고, retry pending은 기존 `retries a protected route 404 and reveals the shell only after the current retry succeeds` test 안에 assertion이 추가됐다.
- 실행: `npm run test:run -- src/app/App.test.tsx`는 1 file / 27 tests를 통과했다.
- 계획: `plan-task.md``P2-R12` TDD 절차는 두 test 추가를 완료로 체크하고 기대 결과를 App 28 tests 이상으로 둔다.
- 기록: plan/review의 `P2-R11~P2-R13 수정 검증`은 “신규 pending status tests 2건”과 GREEN 27 tests를 동시에 기록한다.
**재현 또는 검증 절차**
1. `rg -c '^test\(' src/app/App.test.tsx`를 실행해 27을 확인한다.
2. `rg -n 'initial protected route probe is pending|retries a protected route 404' src/app/App.test.tsx`로 최초 전용 test 1건과 기존 retry test 보강 구조를 확인한다.
3. `P2-R12`의 TDD 체크·기대 결과와 `P2-R11~P2-R13 수정 검증`의 RED/GREEN 기록을 대조한다.
**영향**
제품 동작과 접근성 feedback은 unit·E2E로 유지되지만, 완료하지 않은 수치 기준을 완료로 체크하고 test case와 검증 시나리오를 혼용해 후속 reviewer가 TDD 증거를 오해하게 된다.
**권장 조치**
중복 retry test를 추가하지 않는다. `P2-R14` 문서 전용 정정으로 실제 구조를 “최초 전용 test 1건 추가 + 기존 retry test 보강, App 27 tests”로 명시하고, 이전 잘못된 표현과 정정 사유를 새 Progress에 보존한 뒤 문서 diff·검색 검증을 누적한다.
**판정 기록**
- 2026-07-27 — 현재 App test 선언 27건, test 이름·assertion 위치, focused 실행 27 tests와 plan/review의 28 tests·두 신규 test 주장을 직접 대조해 확정했다.
- 2026-07-27 — `P2-R14`에서 plan/review의 P2-R12 완료 증거를 최초 pending 전용 test 1건 + 기존 404 retry test 보강 + App 27 tests로 정정했다. App focused test와 stale 표현 검색·문서 diff 검사를 통과해 수정 완료로 판정했다.
- 2026-07-27 — 제품 동작은 server boundary 16 tests와 full unit 147 tests에서 유지됨을 확인해 심각도를 Low로 판정하고 문서 전용 `P2-R14`로 전환했다.
### REV-P2-018 — plan 전환 절이 완료된 P2-R8~P2-R13을 미수정으로 표시
- **심각도:** Low
- **상태:** 수정 완료
- **관련 요구사항:** Goal 실행형 계획의 완료 증거·Progress 정합성 규칙, 리뷰 종료 조건
- **관련 계약:** 없음
- **소유 Task:** R2.15, goal `P2-R15`
**관찰 내용**
review 요약과 각 발견 상세, 종료 판정은 `REV-P2-010`~`REV-P2-016``P2-R8`~`P2-R13`을 모두 수정 완료로 표시한다. 그러나 같은 문서의 §7은 3차 회귀 Task `P2-R8`~`P2-R10`과 4차 회귀 Task `P2-R11`~`P2-R13`을 각각 “아직 수정하지 않았다”고 현재형으로 표시한다.
**근거**
- 문서: review §5의 `REV-P2-010`~`REV-P2-016` 상태는 모두 `수정 완료`다.
- 문서: review §6의 각 판정 기록과 §9의 `P2-R9`, `P2-R10`, `P2-R11~P2-R13` 수정 검증은 실제 수정·검증 완료를 기록한다.
- 문서: review §7은 3차·4차 확정 항목을 현재도 “아직 수정하지 않았다”고 표시한다.
- 문서: review §8은 열린 확정 항목 없음과 코드·문서 정합성 충족을 결론으로 표시한다.
**재현 또는 검증 절차**
1. `rg -n '아직 수정하지 않았다|REV-P2-01[0-6]|P2-R(8|9|10|11|12|13)' docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md`를 실행한다.
2. §5·§6·§8의 수정 완료 상태와 §7의 미수정 표현 두 곳을 대조한다.
3. §9의 과거 시점 “남은 항목”은 이력 보존 대상이고, §7은 현재 plan 전환 상태를 설명하는 절임을 구분한다.
**영향**
제품 동작과 검증 결과에는 영향이 없지만, Phase 3 실행자나 reviewer가 `P2-R8`~`P2-R13`을 다시 실행해야 하는 열린 Task로 오해할 수 있고 리뷰 종료 판정의 신뢰도가 낮아진다.
**권장 조치**
`P2-R15` 문서 전용 정정으로 review §7의 두 현재 상태 문장만 실제 수정 완료 상태로 바꾼다. §9의 당시 남은 항목과 과거 검증 기록은 삭제하지 않고, stale 문자열 검색과 문서 diff 검사를 누적한다.
**판정 기록**
- 2026-07-27 — review §5·§6·§7·§8과 실제 `P2-R8`~`P2-R13` 수정 검증 기록을 대조해 현재 상태 모순 2건을 확인하고 Low로 확정했다.
- 2026-07-27 — 제품 코드·test·mode 경계는 full unit 147, server E2E 36, mock E2E 28 tests에서 유지돼 문서 전용 `P2-R15`로 전환했다.
- 2026-07-27 — `P2-R15`에서 review §7의 3차·4차 회귀 Task 설명을 수정 완료 상태로 정정했다. 과거 검증 기록은 보존하고 stale 현재 상태 검색과 문서 diff 검사를 통과해 수정 완료로 판정했다.
### REV-P2-019 — Phase 2 현재 상태·최신 Progress가 P2-R15 완료와 불일치
- **심각도:** Low
- **상태:** 수정 완료
- **관련 요구사항:** Goal 실행형 계획의 현재 상태·Progress 누적 규칙, 리뷰 종료 조건
- **관련 계약:** 없음
- **소유 Task:** R2.16, goal `P2-R16`
**관찰 내용**
`P2-R15` Task 본문과 review는 수정 완료 및 열린 확정 항목 없음을 표시한다. 그러나 plan의 Phase 2 상단 현재 상태는 최초 추가 당시 설명에 머물고, 하단 최신 Progress는 6차 재검증의 `P2-R15` 수정 필요 상태로 끝난다.
**근거**
- 문서: plan Phase 2 상단 현재 상태는 완료 범위와 Phase 3 진행 가능 여부를 표시하지 않는다.
- 문서: plan의 `P2-R15` 체크와 inline 수정 검증은 모두 완료됐다.
- 문서: plan 하단 마지막 Progress는 `P2-R15`를 남은 항목으로 표시한다.
- 문서: review §5·§6·§7·§8·§9는 `REV-P2-018`, `P2-R15` 수정 완료와 열린 항목 없음을 표시한다.
**재현 또는 검증 절차**
1. Phase 2 완료 상태 exact 검색을 실행해 no match와 exit 1을 확인한다.
2. `tail -n 16 docs/20260725_AI캐릭터관리자웹/plan-task.md`에서 마지막 Progress가 `P2-R15` 수정 필요 상태인지 확인한다.
3. 같은 문서의 `P2-R15` 체크·inline 검증 및 review 종료 판정과 대조한다.
**영향**
제품 동작에는 영향이 없지만 Phase 3 실행자가 완료된 `P2-R15`를 열린 선행 작업으로 오해하거나 Phase 2 완료 여부를 다시 판정해야 한다.
**권장 조치**
`P2-R16` 문서 전용 정정으로 Phase 2 상단 현재 상태를 실제 완료 범위와 Phase 3 진행 가능 상태로 갱신하고, 6차 당시 남은 항목은 보존한 채 하단에 최신 수정 검증을 누적한다.
**판정 기록**
- 2026-07-27 — Phase 2 상단 상태, `P2-R15` Task·review 완료 상태와 하단 최신 Progress를 대조해 현재 상태 모순을 Low로 확정했다.
- 2026-07-27 — 제품 코드·test·설정 변경이 없는 문서 정합성 문제이므로 문서 전용 `P2-R16`으로 전환했다.
- 2026-07-27 — `P2-R16`에서 Phase 2 상단 현재 상태와 하단 최신 Progress를 실제 완료 상태로 정렬하고 문서 검색·전체 Phase 2 Gate를 통과해 수정 완료로 판정했다.
## 7. 확정 항목의 plan·goal 전환
1차 확정 5건은 기존 완료 체크를 되돌리지 않고 `plan-task.md`의 Phase 2에 다음 회귀 수정 Task로 추가했다.
### Task R2.1 — 보호 route 검증 실패 시 fail-closed 복구
- 연결 review: `REV-P2-001`
- goal: `P2-R1`
- 핵심 완료 증거: 404·network·새 session pending에서 보호 shell 비노출, shell 밖 오류 UI, App/server/auth E2E와 P2-GATE
- 범위 밖: 인증/session 아키텍처 교체, Phase 3 구현
### Task R2.2 — Mock Preview API origin·JWT 오류 계약 복구
- 연결 review: `REV-P2-002`, `REV-P2-003`
- goal: `P2-R2`
- 핵심 완료 증거: exact API origin, invalid·revoked JWT 401, 정상 auth preview 회귀 없음, focused handler test·mock E2E·P2-GATE
- 범위 밖: 계약 미제공 DTO·role 의미 추정과 도메인 fixture
### Task R2.3 — Phase 번호 표시·구현 문서 정합성 복구
- 연결 review: `REV-P2-004`, `REV-P2-005`
- goal: `P2-R3`
- 핵심 완료 증거: Phase 3 사용자 문구, 실제 Files, 하단 Phase 2 Progress, 문서/App focused test와 diff check
- 범위 밖: Phase 3 기능 구현과 기존 완료 기록 삭제
2차 재검증의 새 확정 4건도 `plan-task.md``R2.4`~`R2.7` 회귀 Task로 전환했고 2026-07-27 수정·검증을 완료했다.
### Task R2.4 — 동일 세션 재진입 fail-closed 복구
- 연결 review: `REV-P2-006`
- goal: `P2-R4`
- 핵심 완료 증거: 최초 성공 뒤 동일 session 재진입 pending·404·network·403에서 보호 shell 비노출, focused App test·server boundary E2E·P2 Gate
- 범위 밖: 인증/session 저장 방식 전면 교체, Phase 3 구현
### Task R2.5 — Auth fixture status·media type 계약 복구
- 연결 review: `REV-P2-007`
- goal: `P2-R5`
- 핵심 완료 증거: 비ADMIN logout 403, invalid·revoked 401 유지, non-JSON login 415, 정상 login/logout 회귀 없음
- 범위 밖: 제공되지 않은 login credential 정책과 회원 role 의미 확장
### Task R2.6 — 모든 mock route state의 지속 안내 복구
- 연결 review: `REV-P2-008`
- goal: `P2-R6`
- 핵심 완료 증거: login·성공 shell·403·404/network에서 banner 표시, server mode 미표시, 접근성·320px 회귀 없음
- 범위 밖: 오류 화면 디자인 개편
### Task R2.7 — Mode별 bare E2E 실행 경계 복구
- 연결 review: `REV-P2-009`
- goal: `P2-R7`
- 핵심 완료 증거: bare `npm run e2e``npm run e2e:mock`이 각 mode 유효 spec만 실행해 exit 0, 목록·실행 contract test
- 범위 밖: Playwright config 복제와 CI 전면 재구성
3차 재검증의 새 확정 4건은 `plan-task.md``R2.8`~`R2.10` 회귀 Task로 전환했으며 2026-07-27 수정·검증을 완료했다.
### Task R2.8 — Phase 2 완료 문서 추적성 복구
- 연결 review: `REV-P2-010`
- goal: `P2-R8`
- 핵심 완료 증거: Phase 2 Files, 완료 Task Files·Interfaces, review 범위·경로 수·종료 판정과 최신 검증 기록 일치
- 범위 밖: 애플리케이션 코드·test·설정 변경과 기존 기록 삭제
### Task R2.9 — Mock Preview 모바일 메뉴의 반응형·inert 경계 복구
- 연결 review: `REV-P2-011`, `REV-P2-012`
- goal: `P2-R9`
- 핵심 완료 증거: menu open background에 banner 포함, `lg` 전환 시 menu·inert·aria-hidden 해제, hidden trigger focus 복귀 없음, App/mock/accessibility E2E와 P2 Gate
- 범위 밖: Admin shell 전면 교체, Phase 3 UI와 신규 breakpoint 체계
### Task R2.10 — 보호 route 오류의 fail-closed 재시도 복구
- 연결 review: `REV-P2-013`
- goal: `P2-R10`
- 핵심 완료 증거: 404·network 오류의 수동 retry, 재시도 비성공 중 shell·logout 비노출, 성공한 현재 attempt 뒤 shell 표시, App/server boundary E2E와 P2 Gate
- 범위 밖: 자동 retry·mock fallback, Phase 3 목록 오류 UI와 API client 전면 교체
4차 재검증의 새 확정 3건은 `plan-task.md``R2.11`~`R2.13` 회귀 Task로 전환했으며 2026-07-27 수정·검증을 완료했다.
### Task R2.11 — Phase 2 working tree 전체 경로 집계 복구
- 연결 review: `REV-P2-014`
- goal: `P2-R11`
- 핵심 완료 증거: tracked 37개 + untracked 1개 = working tree 38개 항목과 plan/review 현재 범위 일치, untracked 포함 검증 명령
- 범위 밖: 애플리케이션 코드·test·설정 변경과 과거 tracked 실행 기록 삭제
### Task R2.12 — 보호 route probe 대기 상태의 접근 가능한 피드백 복구
- 연결 review: `REV-P2-015`
- goal: `P2-R12`
- 핵심 완료 증거: 최초·retry pending의 visible live status, 빈 root 0건, 보호 shell·logout 비노출, App/server boundary E2E와 P2 Gate
- 범위 밖: 보호 shell skeleton 선노출, 자동 retry·mock fallback과 router 전면 교체
### Task R2.13 — Mode별 focused E2E 필터와 network retry 증거 복구
- 연결 review: `REV-P2-016`
- goal: `P2-R13`
- 핵심 완료 증거: bare mode allowlist 유지, CLI file filter 1 file 수집, network retry 재실패 E2E, focused·bare E2E와 P2 Gate
- 범위 밖: Playwright config 복제와 project matrix·CI 전면 재구성
5차 재검증의 새 확정 Low 1건은 `plan-task.md``R2.14` 회귀 Task로 전환했으며 2026-07-27 수정·검증을 완료했다.
### Task R2.14 — P2-R12 test 증거 정합성 복구
- 연결 review: `REV-P2-017`
- goal: `P2-R14`
- 핵심 완료 증거: 최초 pending 전용 test 1건 + 기존 retry test 보강 + App 27 tests라는 실제 구조와 plan/review 기록 일치, 문서 diff·검색 검증
- 범위 밖: 애플리케이션 코드·test·설정 변경, 중복 retry test 추가와 기존 실행 이력 삭제
6차 재검증의 새 확정 Low 1건은 `plan-task.md``R2.15` 회귀 Task로 전환했으며 2026-07-27 수정·검증을 완료했다.
### Task R2.15 — review plan 전환 절의 현재 수정 상태 정합성 복구
- 연결 review: `REV-P2-018`
- goal: `P2-R15`
- 핵심 완료 증거: review §7의 `P2-R8`~`P2-R13` 상태가 §5·§6·§8과 일치, 과거 이력 보존, stale 현재 상태 검색·문서 diff 검사
- 범위 밖: 애플리케이션 코드·test·설정 변경, 과거 남은 항목과 검증 기록 삭제
7차 재검증의 새 확정 Low 1건은 `plan-task.md``R2.16` 회귀 Task로 전환했으며 2026-07-27 수정·검증을 완료했다.
### Task R2.16 — Phase 2 현재 상태·하단 Progress 정합성 복구
- 연결 review: `REV-P2-019`
- goal: `P2-R16`
- 핵심 완료 증거: Phase 2 상단 완료 상태, 과거 6차 기록 보존, 최신 Progress의 `P2-R15`·`P2-R16` 완료와 Phase 3 진행 가능 판정 일치
- 범위 밖: 애플리케이션 코드·test·설정 변경, 과거 기록 삭제와 Phase 3 구현
### create_goal objective 초안
```text
[P2-R1|P2-R2|P2-R3]의 연결된 확정 review 항목을 수정하고 회귀를 방지한다.
plan-task.md에 추가된 해당 회귀 수정 Task 하나만 수행한다.
실패 재현, 최소 수정, focused test, 관련 E2E, P2-GATE와 누적 기록이 모두 끝나기 전에는 complete로 표시하지 않는다.
관련 없는 리팩터링, Phase 3 기능 구현과 계약 추정은 범위 밖이다.
```
2차 초안 objective:
```text
[P2-R4|P2-R5|P2-R6|P2-R7]의 연결 review 항목 하나를 수정하고 회귀를 방지한다.
plan-task.md에 신규 회귀 Task로 전환된 해당 범위만 수행한다.
실패 재현, 최소 수정, focused test, 관련 mode E2E와 P2-GATE 기록이 끝나기 전에는 complete로 표시하지 않는다.
Phase 3 기능, 계약 추정과 관련 없는 리팩터링은 범위 밖이다.
```
3차 초안 objective:
```text
[P2-R8|P2-R9|P2-R10]의 연결 review 항목을 수정하고 Phase 2 완료 문서, mobile menu 접근성 회귀 또는 보호 route 오류 복구를 완료한다.
plan-task.md에 신규 회귀 Task로 전환된 해당 범위만 수행한다.
TDD 예외 대체 검증 또는 RED/GREEN/REFACTOR, focused test, 관련 mode E2E와 P2-GATE 기록이 끝나기 전에는 complete로 표시하지 않는다.
Phase 3 기능, 기존 기록 삭제와 Admin shell 전면 재구성은 범위 밖이다.
```
4차 초안 objective:
```text
[P2-R11|P2-R12|P2-R13]의 연결 review 항목 하나를 수정하고 Phase 2 working tree 추적성, 보호 route pending 피드백 또는 focused E2E 증거를 복구한다.
plan-task.md에 신규 회귀 Task로 전환된 해당 범위만 수행한다.
TDD 예외 대체 검증 또는 RED/GREEN/REFACTOR, focused test, 관련 mode E2E와 P2-GATE 기록이 끝나기 전에는 complete로 표시하지 않는다.
Phase 3 기능, 자동 fallback과 Playwright config 복제·CI 전면 재구성은 범위 밖이다.
```
5차 초안 objective:
```text
P2-R14의 연결 review 항목을 수정하고 P2-R12 test 증거의 문서 정합성을 복구한다.
plan-task.md에 신규 회귀 Task로 전환된 문서 정정 범위만 수행한다.
실제 App test 구조·focused 결과 대조, plan/review 정정 기록, 문서 검색·diff 검증이 끝나기 전에는 complete로 표시하지 않는다.
애플리케이션 코드·test·설정 변경, 중복 test 추가와 기존 실행 이력 삭제는 범위 밖이다.
```
6차 초안 objective:
```text
P2-R15의 연결 review 항목을 수정하고 review plan 전환 절의 현재 상태 정합성을 복구한다.
plan-task.md에 신규 회귀 Task로 전환된 문서 정정 범위만 수행한다.
review §5·§6·§7·§8 대조, 과거 이력 보존, stale 문자열 검색과 문서 diff 검증이 끝나기 전에는 complete로 표시하지 않는다.
애플리케이션 코드·test·설정 변경과 과거 검증 기록 삭제는 범위 밖이다.
```
7차 초안 objective:
```text
P2-R16의 연결 review 항목을 수정하고 Phase 2 현재 상태와 하단 최신 Progress의 정합성을 복구한다.
plan-task.md에 신규 회귀 Task로 전환된 문서 정정 범위만 수행한다.
완료 상태 exact 검색, 최신 Progress 대조, 과거 이력 보존, 문서 diff와 Phase 2 Gate 검증이 끝나기 전에는 complete로 표시하지 않는다.
애플리케이션 코드·test·설정 변경과 Phase 3 구현은 범위 밖이다.
```
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
| --- | --- | --- |
| 리뷰 범위 전체 확인 | 충족 | Phase 2 tracked diff 37개 + untracked 1개 = working tree 38개 항목, 관련 문서·unit·E2E·build와 mock browser runtime 경계를 대조 |
| 후보 항목 판정 완료 | 충족 | 1~7차 확정 19건 모두 수정 완료, 후보·오탐·보류 0건 |
| 확정 항목 plan 반영 | 충족 | `REV-P2-019``P2-R16` 회귀 Task로 전환하고 수정 완료함 |
| 보류 항목의 담당·재개 조건 기록 | 해당 없음 | 보류 항목 없음 |
| 검증 명령과 결과 기록 | 충족 | focused unit, focused server/mock list, server boundary 16, mock preview 24, typecheck/lint, working tree 집계와 문서 diff 검사를 기록 |
| Blocker·High 확정 이슈 0건 | 충족 | `REV-P2-006`, `REV-P2-007` 수정 완료 |
| Medium 확정 이슈 0건 | 충족 | `REV-P2-015~016` 수정 완료 |
| Low 확정 이슈 0건 | 충족 | `REV-P2-019`, `P2-R16` 수정 완료 |
| 코드와 문서 정합성 | 충족 | Phase 2 상단 현재 상태·하단 최신 Progress와 review 종료 판정 일치 |
**최종 결론:** Phase 2 구현·review·plan 정합성 충족, Phase 3 진행 가능
**남은 항목:** 현재 review의 열린 확정 항목 없음. mock 통과는 Phase 3의 실제 server integration 완료로 간주하지 않는다.
## 9. 수정 후 검증 기록
기존 기록을 삭제하거나 덮어쓰지 않고 차수별로 누적한다.
### 수정 검증 미수행 — 2026-07-27
- 무엇을: Phase 2 구현과 기존 test·설정은 수정하지 않고, 확정 5건의 review 문서와 `P2-R1~P2-R3` 회귀 Task만 작성했다.
- 왜: 이번 요청은 코드 리뷰·검증·기록 범위이며 확정 문제의 구현 수정은 별도 goal로 분리해야 한다.
- 어떻게:
- 수정 전 기준 검증은 4절의 unit, typecheck, lint, build, production guard, mock/server E2E와 handler 런타임 재현으로 완료했다.
- 수정 후 focused test와 P2-GATE는 아직 실행 대상이 아니다.
- 남은 항목: `REV-P2-001~005`, `P2-R1~P2-R3` 실행.
### P2-R1~P2-R3 수정 검증 — 2026-07-27
- 무엇을: `REV-P2-001~005``P2-R1~P2-R3` 범위에서 수정했다. 보호 route 404·network error는 shell 밖 오류 UI로 fail-closed 처리하고, mock handler는 exact API origin에만 매칭하며 invalid·revoked JWT logout은 401로 반환한다. Phase 2 placeholder 문구와 Files·Progress 기록도 실제 구현에 맞췄다.
- 왜: Phase 2 mock/server 경계가 실제 API 계약과 Phase 1 보호 route 경계를 우회하지 않게 하고, 완료 기록이 재현 가능한 파일·명령과 일치하게 하기 위해서다.
- 어떻게:
- RED: `npm run test:run -- src/app/App.test.tsx src/shared/mocks/__tests__/auth-handlers.test.ts` — App 2건, auth handler 2건이 기대대로 실패했다.
- RED: `npm run test:run -- src/app/App.test.tsx src/shared/mocks/__tests__/mock-preview-docs.test.ts` — Phase 3 placeholder와 Phase 2 Files·Progress 기록 불일치로 3 tests가 기대대로 실패했다.
- GREEN focused: `npm run test:run -- src/app/App.test.tsx src/shared/mocks/__tests__/auth-handlers.test.ts src/shared/mocks/browser.test.ts` — 3 files / 27 tests 통과.
- GREEN docs/App: `npm run test:run -- src/app/App.test.tsx src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 2 files / 21 tests 통과.
- P2 focused Gate: `npm run test:run -- src/shared/config src/shared/mocks src/shared/ui/__tests__/mock-mode-banner.test.tsx` — 7 files / 23 tests 통과.
- Surface: `npm run e2e -- tests/e2e/server-mode-boundary.spec.ts` — 4 projects / 12 tests 통과. `npm run e2e:mock -- tests/e2e/mock-preview-shell.spec.ts` — 4 projects / 20 tests 통과. `npm run e2e -- tests/e2e/auth.spec.ts tests/e2e/accessibility-shell.spec.ts` — 4 projects / 16 tests 통과.
- Final Gate: `npm run test:run` — 34 files / 135 tests 통과. `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod`, `git diff --check` — 모두 성공.
- Production guard: `VITE_API_MODE=mock npm run build:prod``VITE_API_MODE=mock is only available during development`로 기대대로 거부.
- LSP diagnostics: `src/app` directory 3 files / 0 diagnostics, `src/shared/mocks` directory 8 files / 0 diagnostics. `App.tsx` 단일 fresh diagnostics는 timeout이었고 directory diagnostics와 typecheck로 보완했다.
- 남은 항목: 현재 리뷰의 열린 Blocker·High·Low 항목 없음. Phase 3 이후 server integration은 별도 Phase 증거로 기록한다.
### Reviewer gate 보강 — 2026-07-27
- 무엇을: reviewer gate에서 같은 ADMIN token을 재사용하는 logout → login 뒤 probe 실패 시 이전 `verifiedProtectedRouteToken`이 남아 보호 shell을 열 수 있다는 P2-R1 blocker를 확인하고 수정했다.
- 왜: mock preview는 logout 후 재login 때 같은 `mock-admin-jwt`를 재활성화하므로, fresh session 테스트만으로는 현재 session의 probe 성공 여부를 충분히 증명하지 못하기 때문이다.
- 어떻게:
- RED: `npm run test:run -- src/app/App.test.tsx``clears a previous protected route verification before reusing the same token after login` 1건이 보호 shell `main` 노출로 기대대로 실패했다.
- GREEN: 같은 command — 1 file / 19 tests 통과. session이 `null`이 되면 route error와 verified token을 초기화하도록 수정했다.
- Focused regression: `npm run test:run -- src/app/App.test.tsx src/shared/mocks/__tests__/auth-handlers.test.ts src/shared/mocks/browser.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 4 files / 31 tests 통과.
- Surface regression: `npm run e2e -- tests/e2e/server-mode-boundary.spec.ts` — 4 projects / 12 tests 통과. `npm run e2e:mock -- tests/e2e/mock-preview-shell.spec.ts` — 4 projects / 20 tests 통과.
- Reviewer 재검토: 동일 reviewer가 delta를 재검토해 PASS를 반환했다. 세션 객체 기반 검증과 세션별 오류 귀속이 동일 token 재로그인 경로를 차단하고, 신규 blocker 없음으로 판정했다.
- 남은 항목: reviewer gate blocker 없음. 최종 `npm run test:run` — 34 files / 136 tests 통과, `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod`, `git diff --check` 성공, `VITE_API_MODE=mock npm run build:prod` production guard 거부를 확인했다.
### 2차 독립 재검증 — 2026-07-27
- 무엇을: `P2-R1~P2-R3` 수정이 반영된 현재 working tree를 다시 검토해 `REV-P2-006~009` High 2건·Medium 2건을 확정했다. 기존 `REV-P2-001~005`의 상세 상태는 1차 수정 검증 기록에 맞춰 `수정 완료`로 정정했다.
- 왜: 기존 reviewer gate가 logout → 새 session 재로그인과 지정 spec Gate만 다뤘기 때문에, 동일 session route 재진입·mock 오류 화면·auth media/status·bare mode 명령 경계가 검증되지 않았다.
- 어떻게:
- Focused: `npm run test:run -- src/shared/config src/shared/mocks src/shared/ui/__tests__/mock-mode-banner.test.tsx` — 7 files / 23 tests 통과.
- Full unit: `npm run test:run` — 34 files / 136 tests 통과.
- Static/build: `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod`, `git diff --check HEAD` — 모두 exit 0.
- Production boundary: `VITE_API_MODE=mock npm run build:prod` — 기대대로 exit 1 거부. worker 파일과 mock bootstrap 문자열은 production 산출물에서 0건.
- Filtered mock E2E: `npm run e2e:mock -- tests/e2e/mock-preview-shell.spec.ts tests/e2e/mock-mode-boundary.spec.ts` — 4 projects / 24 tests 통과.
- Filtered server regression: `npm run e2e -- tests/e2e/server-mode-boundary.spec.ts tests/e2e/smoke.spec.ts tests/e2e/auth.spec.ts tests/e2e/accessibility-shell.spec.ts` — 4 projects / 32 tests 통과.
- Bare mock E2E: `npm run e2e:mock` — 56 tests 중 36 통과 / 20 실패. 두 mode의 `--list`는 동일한 6 files / 56 tests를 수집했다.
- Runtime: 동일 session 재진입 404에서 보호 `main`·logout 각 1건, mock 403 화면 banner 0건, 비ADMIN logout 401, `text/plain` login 200을 확인했다.
- 변경 범위: 이 review 문서만 누적 갱신했다. 코드·test·설정·`plan-task.md`는 변경하지 않았다.
- 남은 항목: `REV-P2-006~009``P2-R4~P2-R7`로 전환하고 별도 수정 goal에서 처리해야 한다.
### P2-R4~P2-R7 수정 검증 — 2026-07-27
- 무엇을: `REV-P2-006~009``plan-task.md``R2.4`~`R2.7`로 전환하고 수정했다. 동일 session route 재진입은 visit key로 fail-closed 처리하고, auth fixture는 비ADMIN logout 403·unsupported media type 415를 반환한다. Mock Preview banner는 모든 mock route state의 공통 frame에 배치했고, bare E2E script는 mode별 유효 spec만 실행한다.
- 왜: Phase 2 보호 route와 mock/server 경계가 성공 path뿐 아니라 재진입·오류·표준 실행 명령에서도 같은 계약을 유지해야 하기 때문이다.
- 어떻게:
- RED: `npm run test:run -- src/app/App.test.tsx` — 동일 session 재진입 404에서 보호 `main`이 남아 1 test가 실패했다.
- RED: `npm run test:run -- src/shared/mocks/__tests__/auth-handlers.test.ts` — 비ADMIN logout 401과 `text/plain` login 200으로 2 tests가 실패했다.
- RED: `npm run test:run -- src/app/App.test.tsx` — mock 403·보호 오류 화면의 `Mock Preview` status 부재로 2 tests가 실패했다.
- RED: `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts` — bare E2E script가 mode별 spec 경계를 명시하지 않아 1 test가 실패했다.
- GREEN focused: `npm run test:run -- src/app/App.test.tsx src/shared/mocks/__tests__/auth-handlers.test.ts src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 4 files / 37 tests 통과.
- Full unit/static/build: `npm run test:run` — 34 files / 141 tests 통과. `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` 모두 exit 0.
- E2E list: `npm run e2e -- --list` — server 4 files / 32 tests, `npm run e2e:mock -- --list` — mock 2 files / 24 tests.
- Bare E2E: `npm run e2e` — 32 tests 통과, `npm run e2e:mock` — 24 tests 통과.
- Production/diff: `VITE_API_MODE=mock npm run build:prod` — 기대대로 exit 1, `test ! -e dist/mockServiceWorker.js`, `git diff --check HEAD` — exit 0.
- LSP/size: `src/app` 4 files / 0 diagnostics, `src/shared/mocks` 8 files / 0 diagnostics. `App.tsx``ProtectedAdminShell` 분리 후 153 pure LOC, 새 shell은 131 pure LOC다.
- 남은 항목: reviewer gate blocker 없음. mock 통과는 후속 도메인 server integration 완료로 간주하지 않는다.
### 3차 독립 재검증 — 2026-07-27
- 무엇을: `P2-R4`~`P2-R7` 수정 뒤 Phase 2 문서 추적성, actual Service Worker 응답, mobile menu open 상태의 background inert·desktop breakpoint 전환과 보호 route 오류 recovery를 재검토했다.
- 왜: 고정 viewport와 개별 banner/menu·오류 alert test만으로는 공통 `RouteFrame` 이동 뒤의 accessibility composition, responsive state 전환과 사용자의 fail-closed 복구를 증명하지 못하기 때문이다.
- 어떻게:
- `npm run test:run -- src/shared/config src/shared/mocks src/shared/ui/__tests__/mock-mode-banner.test.tsx` — 7 files / 25 tests 통과.
- `npm run test:run` — 34 files / 141 tests 통과.
- `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0.
- `VITE_API_MODE=mock npm run build:prod` — 기대대로 exit 1, production worker 파일·bootstrap 문자열 0건.
- `npm run e2e` — 4 projects / 32 tests 통과. `npm run e2e:mock` — 4 projects / 24 tests 통과.
- Chromium one-off — login·AI Character probe 응답 `fromServiceWorker=true`; 320px menu open에서 banner inert 귀속 없음, 1,200px 전환 뒤 overlay `display:none`과 main inert·`aria-hidden=true` 잔존을 확인.
- 코드·test 대조 — `ProtectedRouteErrorPage`의 interactive recovery control 0건과 App/server boundary의 retry test 0건을 확인.
- `git diff HEAD --name-only | wc -l` — 37개 경로, `git diff --check HEAD` — exit 0.
- 문서 반영 검증 — `npm run test:run -- src/shared/mocks/__tests__/mock-preview-docs.test.ts`는 1 file / 3 tests 통과, review 상세 ID 13개·Phase 2 회귀 Task 10개를 확인했고 stale 3차 범위 문자열과 문서 whitespace 오류는 0건.
- 변경 범위: 이 review 문서와 `plan-task.md``REV-P2-010~013`, `P2-R8`~`P2-R10` 판정·후속 계획만 추가했다. 애플리케이션 코드·test·설정은 변경하지 않았다.
- 남은 항목: `P2-R8`~`P2-R10`을 별도 goal로 실행하고 수정 후 검증 기록을 누적해야 한다.
### P2-R9 수정 검증 — 2026-07-27
- 무엇을: `REV-P2-011`, `REV-P2-012``P2-R9` 범위에서 수정했다. Mock Preview 성공 shell의 banner를 mobile menu background inert container 안으로 이동하고, `lg=1024px` viewport match에서 mobile menu state를 닫아 hidden overlay와 inert shell 잔존을 제거했다.
- 왜: mobile overlay가 열렸을 때 background banner가 보조기기 탐색에 남지 않아야 하며, responsive 전환 뒤 보이는 desktop shell이 pointer·keyboard·screen reader에서 즉시 사용 가능해야 하기 때문이다.
- 어떻게:
- RED unit: `npm run test:run -- src/app/App.test.tsx src/shared/ui/__tests__/mock-mode-banner.test.tsx` — 신규 App tests 2건이 기대대로 실패했다. 실패 핵심은 `mock banner inert background not found``모바일 주 메뉴` 잔존이다.
- RED e2e: `npm run e2e:mock -- tests/e2e/mock-preview-shell.spec.ts` — 4 browser projects에서 신규 mock shell test가 모두 기대대로 실패했다. 실패 핵심은 open state의 `bannerInert=false`, `bannerHidden=false`다.
- GREEN unit: 같은 focused unit command — 2 files / 26 tests 통과.
- GREEN e2e: 같은 mock e2e command — 28 tests 통과. 320px open state의 banner/main inert 경계와 1,024px·1,200px desktop 전환 뒤 desktop navigation·logout 표시를 확인했다.
- Regression: `npm run e2e -- tests/e2e/accessibility-shell.spec.ts` — 32 tests 통과. `npm run typecheck`, `npm run lint` — 모두 exit 0.
- LSP diagnostics: `src/app/App.tsx`, `src/app/protected-admin-shell.tsx`, `src/app/App.test.tsx`, `tests/e2e/mock-preview-shell.spec.ts` 모두 0 diagnostics.
- P2 focused Gate: `npm run test:run -- src/shared/config src/shared/mocks src/shared/ui/__tests__/mock-mode-banner.test.tsx` — P2-R9 문서 기록 반영 전에는 docs contract 1 test가 실패했다. 기록 반영 후 재실행해 7 files / 25 tests 통과했다.
- 남은 항목: `REV-P2-013`, `P2-R10` 보호 route 오류 retry 복구.
### P2-R10 수정 검증 — 2026-07-27
- 무엇을: `REV-P2-013``P2-R10` 범위에서 수정했다. 보호 route 오류 page에 native retry button을 추가하고 현재 session·route visit·retry attempt에 귀속된 결과만 사용해 성공 전 shell을 fail-closed로 유지했다. 정상 `ApiError`의 서버 message와 network 오류의 안전한 공통 안내를 구분했다.
- 왜: 일시적인 404·network 오류가 복구된 뒤 browser refresh 없이 재진입할 수 있어야 하고, stale 또는 실패한 retry가 보호 shell을 열어서는 안 되기 때문이다.
- 어떻게:
- RED unit: `npm run test:run -- src/app/App.test.tsx` — 1 file / 26 tests 중 신규 2 tests가 `보호 route 다시 시도` button 부재로 기대대로 실패했고 24 tests는 통과했다.
- RED E2E: `npm run e2e -- tests/e2e/server-mode-boundary.spec.ts` — 32 tests 중 신규 retry 시나리오가 4 browser project에서 button 부재로 기대대로 실패했고 28 tests는 통과했다.
- GREEN unit: 같은 App command — 1 file / 26 tests 통과. 404 `없습니다.` 보존, network 재실패의 공통 안내·retry 유지, pending 중 shell·logout 비노출과 성공 뒤 shell 표시를 확인했다.
- GREEN E2E: 같은 server command — 4 projects / 32 tests 통과. 명시적 retry 전후 browser MSW worker 0건과 성공 전 shell·logout 0건을 확인했다.
- Regression: `npm run test:run -- src/shared/config src/shared/mocks src/shared/ui/__tests__/mock-mode-banner.test.tsx` — 7 files / 25 tests 통과. `npm run test:run` — 34 files / 145 tests 통과.
- Static/build: `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0. no-excuse 검사도 변경 TS/TSX 3 files / 위반 0건이었다.
- LSP diagnostics: `src/app` 4 files, `tests/e2e` 6 files에서 diagnostics 0건.
- 수동·시각 확인: server mode 실제 브라우저 375px·768px·1,280px에서 button 높이 44px, `:focus-visible=true`, 한국어 clipping·비정상 줄바꿈 0건을 확인했다. retry 성공 뒤 shell·logout 표시와 mock worker 0건을 확인했고 기능 무결성·CJK 정밀 검토가 모두 PASS였다.
- Diff: `git diff --check -- src/app/App.tsx src/app/App.test.tsx tests/e2e/server-mode-boundary.spec.ts docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md` — exit 0.
- 남은 항목: 현재 리뷰의 열린 확정 항목 없음. mock 통과는 후속 도메인 server integration 완료로 간주하지 않는다.
### 4차 독립 재검증 — 2026-07-27
- 무엇을: `P2-R8`~`P2-R10` 반영 뒤 working tree 전체 집계, 보호 route 최초·retry pending 상태와 mode별 focused E2E 실행 증거를 재검토했다.
- 왜: 통과한 자동화가 untracked 파일을 포함한 전체 범위, 300ms 이상 비동기 피드백과 Task가 지정한 단일 E2E spec 실행을 실제로 증명하는지 확인하기 위해서다.
- 어떻게:
- Focused unit: `npm run test:run -- src/app/App.test.tsx src/shared/ui/__tests__/mock-mode-banner.test.tsx` — 2 files / 28 tests 통과.
- Full unit/static/build: `npm run test:run` — 34 files / 145 tests 통과. `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0, build는 각 160 modules 변환.
- Production boundary: `VITE_API_MODE=mock npm run build:prod` — 기대대로 exit 1. `test ! -e dist/mockServiceWorker.js` — exit 0, production JS mock bootstrap 문자열 검색 — 기대한 no-match exit 1.
- Bare E2E: `npm run e2e` — 4 projects / 32 tests 통과. `npm run e2e:mock` — 4 projects / 28 tests 통과.
- Working tree: `git diff HEAD --name-only | wc -l` — tracked 37개, `git status --porcelain=v1 | wc -l` — 전체 38개, untracked `src/app/protected-admin-shell.tsx` 1개 확인.
- Focused list 대조: npm script 경유 server boundary list는 4 files / 32 tests, 직접 Playwright list는 1 file / 12 tests. `P2-R10` 기대 16 tests와 network retry E2E가 충족되지 않았다.
- Chromium one-off: local listen·browser sandbox 실패 뒤 권한 허용 재실행. 404 retry 성공 응답을 350ms 지연했을 때 `#root` child, `main`, `status`, `alert`가 모두 0개이고 active element가 `BODY`였으며 응답 뒤 shell·logout은 표시됐다.
- 문서 반영 검증: `npm run test:run -- src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 1 file / 3 tests 통과. review 상세 ID 16개, Phase 2 회귀 Task 13개와 문서 whitespace 오류 0건 확인.
- 변경 범위: 이 review 문서와 `plan-task.md``REV-P2-014~016`, `P2-R11`~`P2-R13` 판정·후속 계획만 추가했다. 애플리케이션 코드·test·설정은 변경하지 않았다.
- 남은 항목: `P2-R11`~`P2-R13`을 별도 goal로 실행하고 수정 후 검증 기록을 누적해야 한다.
### P2-R11~P2-R13 수정 검증 — 2026-07-27
- 무엇을: `REV-P2-014~016``P2-R11`~`P2-R13` 범위에서 수정했다. working tree 범위는 tracked 37개 + untracked `src/app/protected-admin-shell.tsx` 1개로 정정했고, 보호 route pending에는 기존 `PageState` loading status를 표시했다. E2E script의 spec allowlist는 `playwright.config.ts` `testMatch`로 옮기고 network retry 재실패 E2E를 추가했다.
- 왜: 완료 증거가 untracked 파일, 접근 가능한 300ms 이상 pending feedback, focused E2E file filter와 실제 network retry 경계를 빠뜨리지 않게 하기 위해서다.
- 어떻게:
- P2-R11 대체 검증: `git diff HEAD --name-only | wc -l` — tracked 37개, `git status --short --untracked-files=all | wc -l` — 전체 38개, `git ls-files --others --exclude-standard``src/app/protected-admin-shell.tsx` 1개.
- P2-R12 RED: `npm run test:run -- src/app/App.test.tsx` — 최초 pending 전용 test 1건과 기존 404 retry test의 pending assertion이 `role="status"` 부재로 기대대로 실패했다.
- P2-R12 GREEN: 같은 command — 1 file / 27 tests 통과. 최초 probe와 retry pending 중 `role="status"`, 보호 `main`·logout 0건을 확인했다.
- P2-R13 RED: `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts` — npm script와 Playwright config contract 2 tests가 기대대로 실패했다.
- P2-R13 GREEN focused: `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 2 files / 6 tests 통과. `npm run e2e -- --list tests/e2e/server-mode-boundary.spec.ts` — 1 file / 16 tests, `npm run e2e:mock -- --list tests/e2e/mock-preview-shell.spec.ts` — 1 file / 24 tests.
- Surface: `npm run e2e -- tests/e2e/server-mode-boundary.spec.ts` — 16 tests 통과. `npm run e2e:mock -- tests/e2e/mock-preview-shell.spec.ts` — 24 tests 통과.
- LSP diagnostics: `src/app/App.tsx`, `src/app/App.test.tsx`, `playwright.config.ts`, `src/shared/mocks/__tests__/mode-boundary.test.ts` 0 diagnostics. `tests/e2e/server-mode-boundary.spec.ts` 단일 fresh diagnostics는 timeout이었고 focused E2E와 typecheck로 보완한다.
- Final Gate: `npm run test:run` — 34 files / 147 tests 통과. `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0. `npm run e2e` — 36 tests 통과, `npm run e2e:mock` — 28 tests 통과.
- Diff: `git diff --check -- docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md tests/e2e/server-mode-boundary.spec.ts package.json playwright.config.ts README.md docs/agent-guide/scripts.md src/app/App.tsx src/app/App.test.tsx src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts` — exit 0.
- 남은 항목: Phase 2 review의 열린 확정 항목 없음. mock 통과는 후속 도메인 server integration 완료로 간주하지 않는다.
### 5차 독립 재검증 — 2026-07-27
- 무엇을: `P2-R11`~`P2-R13` 반영 뒤 구현·test·plan·review의 완료 증거를 다시 대조해 `REV-P2-017` Low 1건을 확정하고 `P2-R14`로 전환했다.
- 왜: 자동 검증 통과뿐 아니라 완료 체크의 test case 수·구조와 review의 현재 수정 상태가 실제 결과와 일치하는지 확인하기 위해서다.
- 어떻게:
- Focused unit: `npm run test:run -- src/app/App.test.tsx` — 1 file / 27 tests 통과. `rg -c '^test\(' src/app/App.test.tsx` — 27건. 최초 pending 전용 test 1건과 기존 retry test의 pending assertion 보강을 확인했다.
- Mode/docs unit: `npm run test:run -- src/shared/mocks/__tests__/mode-boundary.test.ts src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 2 files / 6 tests 통과.
- Full unit/static/build: `npm run test:run` — 34 files / 147 tests 통과. `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0, build는 각각 160 modules 변환.
- Production boundary: `VITE_API_MODE=mock npm run build:prod` — 기대대로 exit 1과 `VITE_API_MODE=mock is only available during development` 확인. production 산출물의 worker 파일·mock bootstrap 문자열은 0건.
- Focused list/E2E: server boundary list 1 file / 16 tests, mock preview list 1 file / 24 tests. 최초 sandbox listen `EPERM` 뒤 허용된 로컬 실행으로 server boundary 16 tests, mock preview 24 tests 통과.
- Bare E2E: `npm run e2e` — 36 tests, `npm run e2e:mock` — 28 tests 통과.
- Working tree: tracked diff 37개, untracked `src/app/protected-admin-shell.tsx` 1개, 전체 status 38개 항목을 재확인했다.
- 문서 대조: `P2-R12`의 “두 test 추가”·App 28 tests 이상 완료 체크와 실제 27 tests 구조가 불일치함을 확인했다.
- 변경 범위: 이 review 문서와 `plan-task.md``REV-P2-017`, `P2-R14` 판정·후속 계획만 추가했다. 애플리케이션 코드·test·설정은 변경하지 않았다.
- 남은 항목: `P2-R14`에서 실제 test 구조·개수를 plan/review에 정정하고 문서 검색·diff 검증을 누적해야 한다.
### P2-R14 수정 검증 — 2026-07-27
- 무엇을: `REV-P2-017``P2-R14` 범위에서 수정했다. P2-R12 완료 증거를 실제 App test 구조인 최초 pending 전용 test 1건 + 기존 404 retry test의 pending assertion 보강 + App 27 tests로 정정했다.
- 왜: 완료 문서가 test case 수와 검증 시나리오 수를 혼동하면 후속 reviewer가 P2-R12의 실제 회귀 고정 범위를 잘못 판단할 수 있기 때문이다.
- 어떻게:
- 대체 RED: `rg -c '^test\(' src/app/App.test.tsx` — 27건. `rg -n 'initial protected route probe is pending|retries a protected route 404' src/app/App.test.tsx` — 최초 pending 전용 test 1건과 기존 404 retry test 1건을 확인했다.
- GREEN docs: plan/review의 P2-R12 TDD 절차·기대 결과·수정 검증 기록과 종료 판정을 실제 test 구조와 일치시켰다.
- Focused: `npm run test:run -- src/app/App.test.tsx` — 1 file / 27 tests 통과.
- 문서 검증: stale `신규 pending status tests 2건` 검색 결과 0건, `git diff --check -- docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md` — exit 0.
- 남은 항목: Phase 2 review의 열린 확정 항목 없음. 애플리케이션 코드·test·설정은 변경하지 않았다.
### 6차 독립 재검증 — 2026-07-27
- 무엇을: `P2-R14` 반영 뒤 구현·test·production·mode별 E2E와 review 현재 상태를 대조해 `REV-P2-018` Low 1건을 확정했다.
- 왜: 완료된 `P2-R8`~`P2-R13`의 상태가 review 모든 현재 상태 절에서 같은 의미로 표시되는지 확인하기 위해서다.
- 어떻게:
- `npm run test:run` — 34 files / 147 tests 통과.
- `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0.
- `VITE_API_MODE=mock npm run build:prod` — 기대대로 exit 1 거부.
- sandbox listen `EPERM` 후 로컬 실행 권한으로 `npm run e2e` 36 tests, `npm run e2e:mock` 28 tests 통과.
- Chromium one-off — mock API 응답 `fromServiceWorker=true`, 320px mobile menu·logout 60px, menu link 선택 뒤 menu·inert 잔존 0건.
- 문서 대조 — review §7의 stale “아직 수정하지 않았다” 현재 상태 2건과 §5·§6·§8의 수정 완료 상태 모순을 확인했다.
- 문서 반영 후 `npm run test:run -- src/shared/mocks/__tests__/mock-preview-docs.test.ts` — 1 file / 3 tests 통과, plan/review 대상 `git diff --check` — exit 0.
- 변경 범위: 이 review 문서와 `plan-task.md``REV-P2-018`, `P2-R15` 판정·후속 계획만 추가했다. 애플리케이션 코드·test·설정은 변경하지 않았다.
- 남은 항목: `P2-R15`에서 review §7 현재 상태를 정정하고 stale 문자열 검색·문서 diff 검증을 누적해야 한다.
### P2-R15 수정 검증 — 2026-07-27
- 무엇을: `REV-P2-018``P2-R15` 범위에서 수정했다. review §7의 `P2-R8`~`P2-R10`, `P2-R11`~`P2-R13` 전환 설명을 현재 수정 완료 상태로 정정했다.
- 왜: review 요약·상세·종료 판정은 이미 수정 완료로 표시하는데 plan 전환 절만 미수정 현재형으로 남아 Phase 3 실행자가 열린 Task로 오해할 수 있었기 때문이다.
- 어떻게:
- 대체 RED: review §7 stale 현재 상태 검색 — 2건이 기존 stale 현재 상태로 확인됐다.
- GREEN docs: review §7의 두 문장을 수정 완료 상태로 정정하고, `REV-P2-018` 상태·발견 요약·종료 판정을 수정 완료로 맞췄다. §9의 당시 남은 항목과 과거 검증 기록은 삭제하지 않았다.
- 검증: review §7 stale 현재 상태 검색 — no matches. `git diff --check -- docs/20260725_AI캐릭터관리자웹/plan-task.md docs/20260725_AI캐릭터관리자웹/reviews/review-phase-2.md` — exit 0.
- 남은 항목: 현재 review의 열린 확정 항목 없음. 애플리케이션 코드·test·설정은 변경하지 않았다.
### 7차 독립 재검증 — 2026-07-27
- 무엇을: `P2-R15` 반영 뒤 Phase 2 상단 현재 상태, Task 본문, review 종료 판정과 하단 최신 Progress를 대조해 `REV-P2-019` Low 1건을 확정했다.
- 왜: Phase 3 시작 판정은 과거 검증 이력이 아니라 plan의 현재 상태와 최신 누적 Progress를 소비하므로 두 위치가 완료 사실을 함께 표시해야 하기 때문이다.
- 어떻게:
- 대체 RED 완료 상태 검색 — 기대한 Phase 2 완료·Phase 3 진행 가능 exact 상태가 없어 exit 1.
- 대체 RED 최신 Progress 검색 — 문서 끝에 `P2-R15` 수정 완료와 열린 항목 없음 기록이 없어 exit 1.
- `P2-R15` Task 체크·inline 검증과 review §5·§6·§7·§8·§9는 수정 완료임을 확인했다.
- `git diff --check HEAD` — review 반영 전 exit 0.
- 변경 범위: 이 review 문서와 `plan-task.md``REV-P2-019`, `P2-R16` 판정·후속 계획만 추가했다. 애플리케이션 코드·test·설정은 변경하지 않았다.
- 남은 항목: `P2-R16`에서 Phase 2 현재 상태와 최신 Progress를 정정하고 문서·Phase 2 Gate 검증을 누적해야 한다.
### P2-R16 수정 검증 — 2026-07-27
- 무엇을: `REV-P2-019``P2-R16` 범위에서 수정했다. Phase 2 상단 현재 상태와 하단 최신 Progress를 `P2-R16`까지 완료 및 Phase 3 진행 가능 상태로 정렬했다.
- 왜: 완료 상태를 소비하는 Phase 3 실행자가 과거 6차 남은 항목을 현재 열린 Task로 오해하지 않도록 현재 상태와 누적 이력을 구분해야 하기 때문이다.
- 어떻게:
- 대체 GREEN: Phase 2 완료 상태 exact 검색과 최신 Progress 검색이 각각 1건 이상 일치했고, 6차 당시 `P2-R15` 남은 항목은 이력으로 보존됐다.
- Full unit/static/build: `npm run test:run` — 34 files / 147 tests 통과. `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0, build는 각각 160 modules 변환.
- Production boundary: `VITE_API_MODE=mock npm run build:prod` — 기대한 guard로 exit 1. production worker 파일과 mock bootstrap 문자열은 0건.
- E2E: `npm run e2e` — 4 projects / 36 tests 통과. `npm run e2e:mock` — 4 projects / 28 tests 통과.
- Diff: plan/review 대상 `git diff --check` — exit 0.
- 남은 항목: 현재 review의 열린 확정 항목 없음. `P2-GATE`와 모든 Phase 2 회귀 수정이 완료돼 Phase 3 진행 가능. mock 통과는 Phase 3의 실제 server integration 완료로 간주하지 않는다.