# 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 항목은 없다.