# Phase 1 플랫폼·인증·공통 UI 코드 리뷰·QA
## 1. 리뷰 정보
| 항목 | 내용 |
|---|---|
| 리뷰 대상 | Phase 1 / API client, 인증·인가, shell, 공통 UI |
| 기준 commit 또는 working tree | `dd30e36323543e8f60e9983326503653e8001f12`, 2026-07-31 종합 재점검 당시 사용자 변경을 포함한 current working tree |
| 리뷰 일자 | 2026-07-31 |
| 리뷰어 | Codex |
| 기준 문서 | `prd.md`, `api-contract.openapi.json` 2.3.0, `plan-task.md` |
| 리뷰 상태 | 재리뷰 판정 완료, `REV-P1-023`/`P1-R17` 수정 완료 |
## 2. 리뷰 목적과 범위
### 목적
- 공통 API client, 인증 session, ADMIN role, shell과 공유 UI가 요구사항·계약을 충족하는지 확인한다.
- 이전 Phase 1 리뷰의 수정 완료 항목이 현재 코드와 test에서 유지되는지 점검한다.
### 포함 범위
- 코드: `src/shared/api`, `src/features/auth`, `src/layouts`, `src/shared/ui`
- 테스트: 관련 unit/integration, 전체 정적·브라우저 Gate
- 문서: `AUTH-*`, `P1-*`, OpenAPI 인증 operation
- 수동 검증: session storage·header·401 처리·keyboard/ARIA 정적 대조
### 제외 범위
- 실제 개발 서버 계정과 role fixture를 이용한 외부 로그인
- Phase 0의 API origin 정합성 결함 수정
## 3. 판정 기준
심각도는 `Blocker`, `High`, `Medium`, `Low`, 상태는 `후보`, `확정`, `오탐`, `보류`, `수정 완료`를 사용한다. 요구사항·계약 위반, 보안·데이터 위험, 회귀와 test 누락을 우선한다.
## 4. 검토한 근거
### 문서와 코드
- 요구사항: `AUTH-001`~`AUTH-013`, PRD §12~§14
- 계약: login/logout, bearer 인증, 공통 오류 envelope
- 계획: `P1-T1`~`P1-GATE`, 이전 회귀 Task
- 코드·테스트: `src/shared/api/**`, `src/features/auth/**`, `src/layouts/**`, `src/shared/ui/**`, 관련 `tests/e2e`
### 실행 환경
```text
macOS 26.0 / Node v24.12.0 / npm 11.7.0
Playwright: Chromium, Mobile Chrome
```
### 실행한 검증
| 명령 또는 수동 검증 | 결과 | 핵심 증거 |
|---|---|---|
| `npm run typecheck` / `npm run lint` | 성공 | 모두 exit 0 |
| `npm run test:run` | 성공 | 72 files, 354 tests passed |
| `npm run build` | 성공 | 253 modules transformed |
| `npm run e2e:mock` | 실패 | Phase 0 소유 origin 불일치 4건과 비재현 timeout 1건; Phase 1 신규 결함으로 중복 등록하지 않음 |
| `npm run e2e` | 실패 | Phase 0 소유 origin 불일치로 12 passed, 24 failed |
| 인증·공통 UI 정적 대조 | 성공 | 신규 확정 위반 없음 |
| 공통 pagination helper 2차 계약 대조 | 실패 | production 사용처가 없는 `createPageParams`가 공통 size를 `20..50`으로 clamp하고 음수 page를 허용 |
## 5. 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P1-014` | Low | 수정 완료 | 사용되지 않는 공통 pagination helper가 FanTalk 전용 size clamp를 전역 규칙처럼 고정한다 | `P1-R8` | `P1-R8` |
## 6. 발견 사항 상세
### REV-P1-014 — 사용되지 않는 공통 pagination helper가 FanTalk 전용 size clamp를 전역 규칙처럼 고정한다
- **심각도:** Low
- **상태:** 수정 완료
- **관련 요구사항:** PRD §11.1 공통 pagination
- **관련 계약:** OpenAPI 공통 `Page`, `Size`; FanTalk만 별도 `size=20..50`
- **소유 Task:** 신규 `P1-R8`
**관찰 내용**
`createPageParams`는 `size=1`을 20으로, `size=51`을 50으로 바꾸고 음수 page를 그대로 둔다. 이는 공통 `page >= 0`, `size >= 1`, 전역 maximum 없음과 다르다. production 사용처는 없고 단위 test만 이 동작을 “documented”로 고정해 미래 소비자가 잘못된 전역 규칙을 재사용할 위험이 있다.
**근거**
- 코드: `src/shared/api/pagination.ts:14-23`
- 테스트: `src/shared/api/__tests__/pagination.test.ts:17-37`
- 사용처 검색: 위 두 파일 외 `createPageParams` 참조 0건
- 문서: `prd.md:636`
- 계약: OpenAPI 공통 `Size`는 default 20, minimum 1이며 maximum이 없음
**재현 또는 검증 절차**
1. `createPageParams({ page: -1, size: 1 })`을 호출하면 `{ page: -1, size: 20 }`이 된다.
2. `createPageParams({ size: 51 })`을 호출하면 size가 50이 된다.
3. `rg -n 'createPageParams' src`로 production 소비자가 없고 helper test만 남아 있음을 확인한다.
**영향**
현재 화면에는 직접 영향이 없지만, 공통 API로 보이는 미사용 helper와 test가 계약과 반대인 규칙을 문서화해 다음 pagination 구현의 회귀 원인이 된다.
**권장 조치**
사용처가 없으므로 새 abstraction을 만들지 말고 `PageParams`, helper와 잘못된 동작 test를 제거한다. 도메인 adapter의 계약별 normalization과 실제 소비 중인 `PageData` type은 유지한다.
**판정 기록**
- 2026-07-30 — PRD·OpenAPI, helper 동작과 production 사용처 검색을 대조해 확정.
- 2026-07-30 — `P1-R8`에서 미사용 `PageParams`/`createPageParams`와 잘못된 clamp test를 삭제하고, `PageData` 및 도메인별 pagination contract 단위 회귀로 수정 완료를 확인했다.
## 7. 확정 항목의 plan·goal 전환
- `REV-P1-014` → `plan-task.md` 신규 `P1-R8`
- goal objective: `[P1-R8] 사용되지 않는 공통 pagination helper의 잘못된 전역 clamp를 제거한다.`
## 8. 리뷰 종료 판정
| 판정 항목 | 결과 | 근거 |
|---|---|---|
| 리뷰 범위 전체 확인 | 충족 | 코드·계약·test 대조 |
| 후보 항목 판정 완료 | 충족 | 1건 확정 |
| 확정 항목 plan 반영 | 충족 | `P1-R8` 추가 |
| 보류 항목의 담당·재개 조건 기록 | 해당 없음 | 보류 없음 |
| 검증 명령과 결과 기록 | 충족 | §4 |
**최종 결론:** 확정 발견 사항 수정 완료.
**남은 항목:** 없음. 기존 인증·공통 UI 회귀는 유지됐고 공통 browser Gate는 `P0-R2`에서 이미 복구됐다.
## 9. 수정 후 검증 기록
2026-07-30 `P1-R8`에서 애플리케이션 helper를 수정했다.
- RED 대체: `rg -n 'createPageParams|PageParams|PageData|normalizeSize' src`로 `createPageParams` production 사용처 0건과 test-only 사용을 확인했고, 기존 `npm run test:run -- src/shared/api/__tests__/pagination.test.ts`는 1 file / 5 tests passed로 stale clamp test가 통과했다.
- GREEN/회귀: 미사용 `PageParams`/`createPageParams`와 해당 test를 삭제했다. `rg -n 'createPageParams|type PageParams|import .*PageParams' src` — no matches. `npm run test:run -- src/shared/api src/features/characters src/features/audio-contents src/features/series src/features/community-posts src/features/comments src/features/fan-talks` — 35 files / 201 tests passed. `npm run typecheck`, `npm run lint` — exit 0.
## 10. 2026-07-31 재점검
### 실행·판정 요약
- **기준:** commit `dd30e36323543e8f60e9983326503653e8001f12`, 재점검 시작 시 working tree 변경 220개.
- **검증:** `npm run test:run` 72 files / 360 tests passed, `npm run typecheck`, `npm run lint`, `npm run build` exit 0, `npm run e2e` 36 passed.
- **신규 발견:** 1건. 기존 `REV-P1-014`는 수정 완료 상태를 유지한다.
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P1-015` | Medium | 수정 완료 | 여러 pagination 인스턴스가 같은 select ID를 사용한다 | `P1-R9` | `P1-R9` |
### REV-P1-015 — 여러 pagination 인스턴스가 같은 select ID를 사용한다
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** PRD §10.7 visible label·control 연결, keyboard 접근성
- **관련 계약:** 없음. 공통 UI DOM 식별자 경계
- **소유 Task:** 신규 `P1-R9`
**관찰 내용**
수정 전 `ResourcePagination`은 모든 인스턴스에 `id="resource-page-size"`와 같은 `htmlFor`를 고정했다. Comments는 root pagination과 하나 이상의 reply pagination을 동시에 렌더링할 수 있고 Community 목록 pagination도 열린 Sheet 뒤 DOM에 남으므로 한 문서에 같은 ID가 여러 개 생길 수 있었다.
**근거**
- 코드: `src/shared/ui/resource-pagination.tsx:15-18`
- 동시 소비: `src/features/comments/components/CommentThread.tsx:140,168`, `src/features/community-posts/pages/CommunityPostListPage.tsx:100`
- 테스트 공백: `src/shared/ui/__tests__/resource-pagination.test.tsx`는 단일 인스턴스만 렌더링한다.
**재현 또는 검증 절차**
1. `ResourcePagination` 두 개를 같은 container에 렌더링한다.
2. `document.querySelectorAll('#resource-page-size')`가 2개인지 확인한다.
3. 두 `페이지 크기` label의 `control`이 각 인스턴스가 아니라 첫 번째 동일 ID 해석에 의존하는지 확인한다.
**영향**
DOM ID 유일성이 깨지고 label 클릭·보조기기 탐색이 다른 pagination select를 가리킬 수 있다. root와 reply page를 함께 관리할 때 사용자가 잘못된 목록의 크기를 바꿀 위험이 있다.
**권장 수정 방향**
공개 prop를 추가하지 않고 React `useId` 등으로 인스턴스별 select ID를 만들며, 두 인스턴스 label/control 연결 회귀 test를 추가한다.
**판정 기록**
- 2026-07-31 — 공통 컴포넌트와 실제 다중 소비 구조를 정적 대조해 확정.
- 2026-07-31 — `P1-R9`에서 `ResourcePagination` 내부 ID를 React `useId`로 격리하고 multi-instance label/control 회귀 test와 Comments focused unit으로 수정 완료를 확인했다.
### plan·goal 전환 및 종료 판정
- `REV-P1-015` → `plan-task.md` 신규 `P1-R9`
- **최종 결론:** 신규 Medium 1건 수정 완료. Blocker/High 없음.
- **남은 위험:** 개발 중 E2E 반복 실행은 사용자 지시에 따라 생략했으며, Comments mock E2E는 최종 회귀 단계에서 필요 시 실행한다.
## 11. 최종 Phase별 점검 — 2026-07-31
- **검토 범위:** 인증/session/logout, 보호 route, API 오류 mapping, 공통 file·crop·pagination·modal UI를 `P1`과 PRD 공통 정책에 재대조했다.
- **실행 증거:** `src/app src/features/auth src/layouts src/styles` 10 files / 64 tests, `src/shared` 31 files / 117 tests 통과. `typecheck`·`lint`·`build`, server E2E 18 tests, mock Chromium E2E 57 tests도 통과했다.
- **판정:** `REV-P1-015`까지 수정 완료 상태가 유지되며 Phase 1 소유의 확정 신규 발견 사항 없음.
- **남은 위험:** 외부 인증 서버 credential 기반 수동 QA는 자동 검증 범위 밖이다.
- **신규 Task:** 없음.
## 12. 종합 재점검 — 2026-07-31
### `REV-P1-016` — crop 미리보기와 저장 결과가 서로 다른 좌표계를 사용함
| 항목 | 내용 |
|---|---|
| 심각도 | High |
| 상태 | 수정 완료 |
| 관련 요구사항·계약 | PRD `FILE-008`, Image crop UI 흐름 4, 수용 기준의 crop 미리보기·적용 일치 |
| 소유 Task | `P1-R10` |
| 코드 근거 | `src/shared/ui/image-crop-dialog.tsx:155`, `:166~178`, `:189~190`; `src/shared/lib/crop-image.ts:48~65` |
| test 근거 | `src/shared/ui/__tests__/image-crop-dialog.test.tsx`는 raw offset 전달만 확인하고 표시 px→원본 px 변환과 실제 aspect frame을 검증하지 않는다. |
**재현 또는 검증 절차**
1. 4,000×3,000 image를 1:1 crop dialog에 연다.
2. 현재 preview는 `max-h-64`에 의해 약 341×256 CSS px로 축소되지만 1:1 crop frame 없이 원본 4:3 전체를 표시한다.
3. preview를 10 CSS px 이동한다. 화면상 같은 이동은 원본 약 117px에 해당한다.
4. `calculateCropSourceRect`가 `offsetX=10`을 원본 10px로 직접 차감해 저장 영역이 preview와 다르게 이동하는 것을 확인한다.
**영향과 권장 조치**
운영자가 선택한 영역과 업로드되는 실제 image가 달라질 수 있고, Character·Audio의 1:1 및 Series 210:297에서는 frame 밖 영역까지 preview에 보인다. 표시 frame 크기와 image scale을 source 좌표로 환산하는 단일 계산을 사용하고, 고정/free aspect의 frame·canvas 결과를 pixel 회귀 test로 고정한다.
**판정 기록**
- 2026-07-31 — PRD의 “현재 crop 영역·결과 미리보기”와 CSS transform·canvas source rectangle 계산을 대조해 확정.
- 2026-07-31 — 공통 UI 소유 신규 `P1-R10`으로 전환. Character·Audio·Series·Community에는 중복 Task를 만들지 않는다.
- 2026-07-31 — `P1-R10`에서 preview frame 크기 기반 source offset 환산을 추가하고 shared·도메인 단위 회귀로 수정 완료 판정.
### `REV-P1-017` — crop 적용 재진입·오류 복구와 Blob URL 해제가 없음
| 항목 | 내용 |
|---|---|
| 심각도 | Medium |
| 상태 | 수정 완료 |
| 관련 요구사항·계약 | PRD §10.5 저장 중·오류/재시도, §13 일반 mutation 중복 차단; 공통 crop lifecycle |
| 소유 Task | `P1-R11` |
| 코드 근거 | `src/shared/ui/image-crop-dialog.tsx:166~180`, `:203~206`; Character create/edit, Audio, Series, Community crop source의 `URL.createObjectURL`/error-only revoke |
| test 근거 | shared crop dialog와 도메인 crop test에 renderer reject·적용 연타·성공/취소/교체/unmount의 `revokeObjectURL` assertion이 없다. |
**재현 또는 검증 절차**
1. `renderCrop`이 pending인 상태에서 적용을 연속 클릭하면 현재 `applyCrop`이 호출마다 새 Promise를 시작하는지 확인한다.
2. renderer를 reject하면 catch와 visible error가 없어 dialog에서 복구 안내를 제공하지 못하는지 확인한다.
3. 각 crop source helper에서 image load 성공 후 적용·취소·새 선택·unmount를 반복한다.
4. 성공 경로에서 `URL.revokeObjectURL` 호출이 없는 것을 확인한다.
**영향과 권장 조치**
중복 canvas 작업과 예외의 unhandled rejection이 발생할 수 있고, 최대 10MB image를 반복 선택하는 관리자 세션에서 Blob URL이 문서 수명까지 유지된다. 적용을 single-flight로 만들고 pending/error를 표시하며, 생성 주체가 idempotent release contract로 URL을 수명 종료 시 해제하도록 한다.
**판정 기록**
- 2026-07-31 — 공통 dialog와 다섯 소비 경로의 비동기·resource ownership을 정적으로 대조해 확정.
- 2026-07-31 — `P1-R10` 이후 실행할 신규 `P1-R11`로 전환.
- 2026-07-31 — `P1-R11`에서 공통 crop source `release` contract, dialog single-flight/error 상태, 다섯 소비 경로의 cleanup을 추가하고 focused shared·도메인 단위 및 정적 Gate로 수정 완료를 확인했다.
### Phase 1 결론
- **자동 검증:** app/auth/shared/layout/style 묶음 41 files / 181 tests passed, 전체 `typecheck`·`lint`·개발/운영 build exit 0.
- **판정:** 인증·API client·기존 shared UI 회귀는 통과했고 `REV-P1-016~017`은 `P1-R10~R11`에서 수정 완료됐다.
- **남은 위험:** 실제 인증 server credential 검증은 외부 수동 QA에서 추적한다. Safari/WebKit은 현재 지원 범위에서 제외한다.
## 13. 요청 기준 재리뷰 — 2026-07-31
- **검토 범위:** 인증/session/logout, 보호 route, API client, shared file·crop·pagination·modal UI와 관련 unit/server E2E.
- **검증:** 전체 394 unit 중 Phase 1 app/auth/shared 기능 test는 통과했고, `typecheck`·`lint`·개발/운영 build와 4-project server E2E 36 tests도 통과했다. 실패 2건은 `src/shared/mocks`의 script/document contract로 한정됐다.
- **판정:** `P1-R10~R11`을 포함한 Phase 1 기능 소유의 신규 결함은 없다. 실제 인증 server credential 수동 QA는 계속 별도다.
- **문서 교차 항목:** 상단 리뷰 상태와 최신 수정 완료 결론의 불일치는 `REV-P10-008`/`P10-R7`이 소유한다.
- **신규 Phase 1 Task:** 없음.
## 14. 최종 재검증 및 신규 판정 — 2026-07-31
### 발견 사항 요약
| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal |
|---|---|---|---|---|---|
| `REV-P1-018` | Medium | 수정 완료 | fetch와 XHR의 독립 401 latch가 같은 session 만료 전환을 두 번 실행한다 | `P1-R12` | `P1-R12` |
### REV-P1-018 — fetch와 XHR의 독립 401 latch가 같은 session 만료 전환을 두 번 실행한다
- **심각도:** Medium
- **상태:** 수정 완료
- **관련 요구사항:** `AUTH-005`, PRD §13 “여러 요청이 동시에 실패해도 로그인 이동과 알림을 한 번만 수행”
- **관련 계약:** HTTP 401 공통 처리. OpenAPI endpoint·DTO 변경 없음.
- **소유 Task:** 신규 `P1-R12`
**관찰 내용**
공통 fetch client와 Audio XHR upload는 각각 별도 `hasHandledAuthenticationExpiry`를 갖는다. 같은 session에서 두 transport가 동시에 401을 받으면 각 latch가 상대 transport의 처리를 보지 못해 `clearSession`과 `onAuthExpired`를 각각 실행한다.
**근거**
- 코드: `src/shared/api/client.ts:55-57,99-103`과 `src/features/audio-contents/api/upload-audio-content.ts:24-25,101-105`가 서로 독립된 count/latch를 사용한다.
- 조립: `src/app/App.tsx:195-201`의 fetch 만료 callback과 `src/app/protected-admin-shell.tsx:45-49`의 upload 만료 callback은 각각 `replaceWith`와 `navigateTo`를 호출한다.
- 테스트 누락: `client-auth.test.ts`와 `audio-upload.test.ts`는 transport 내부 burst만 검증하고 fetch+XHR 교차 burst는 검증하지 않는다.
- 문서: PRD `AUTH-005`, §13과 기존 `P1-R6`, `P4-R2`는 인증 제거·login 이동의 단일 실행을 요구한다.
**재현 또는 검증 절차**
1. 같은 mutable token과 `clearSession`/`onAuthExpired` spy를 `createApiClient`와 `uploadAudioContent`에 주입한다.
2. 두 요청을 동시에 시작하고 fetch response와 XHR response를 모두 401로 종료한다.
3. 임시 진단 test를 `npm run test:run -- src/features/audio-contents/tests/auth-expiry-coordination-repro.test.ts`로 실행했다.
4. 실제 결과는 exit 1, 1 failed였고 `clearSession` 기대 1회 대비 실제 2회였다. 진단 파일은 판정 후 제거해 제품 test 변경을 남기지 않았다.
**영향**
제한된 동시 만료 조건에서 session 제거·login history 변경·만료 안내가 중복될 수 있다. 데이터 손실이나 인증 우회는 확인되지 않았지만 PRD의 복구 흐름과 browser history 일관성을 위반한다.
**권장 조치**
새 전역 event bus를 만들지 않고 두 transport가 callback 직전 같은 현재 session 존재 여부를 확인하게 한다. 첫 handler가 session을 동기 제거하면 나머지는 만료 전환을 건너뛰고, 새 로그인 token에서는 다음 401을 다시 처리하는 교차 transport 회귀 test를 추가한다.
**판정 기록**
- 2026-07-31 — 정적 latch 대조 후 교차 transport 진단 test에서 callback 2회를 재현해 Medium 확정.
- 2026-07-31 — `plan-task.md` 신규 `P1-R12`로 전환. 애플리케이션 코드는 수정하지 않음.
- 2026-07-31 — `P1-R12`에서 callback 직전 현재 token guard를 추가해 수정 완료. reviewer blocker로 fetch-first 순서와 주입 auth authoritative token 처리를 보강했다. focused 3 files / 22 tests, auth/app/shared 회귀 13 files / 91 tests, 전체 unit 79 files / 397 tests, `npm run typecheck`, `npm run lint`, 개발/운영 build, LSP diagnostics가 통과했다.
### 종료 판정
- **자동 검증:** `P1-R12` focused 3 files / 22 tests, `src/app src/features/auth src/shared/api` 포함 회귀 13 files / 91 tests, 전체 unit 79 files / 397 tests, `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod`, targeted `git diff --check`, 변경 파일 LSP diagnostics가 통과했다. E2E는 사용자 지시에 따라 반복 실행하지 않았다.
- **최종 결론:** `REV-P1-018` 수정 완료. Blocker/High 없음.
- **남은 항목:** 실제 인증 server credential과 실기기 browser 수동 QA.
## 15. 2026-07-31 문서 기준 재리뷰
### 검토 범위와 제외
- **검토:** PRD `AUTH-*`, `FILE-*`, OpenAPI 2.3.0, `P1`·`P10` 후속 기록, `App` 보호 route probe, browser location parser, 공통 crop UI/source 계산과 관련 test를 current working tree에서 대조했다.
- **제외:** 실제 개발 API credential·stale role fixture가 필요한 server 수동 QA와 실제 image pixel 수동 비교는 자동 검증 범위에서 제외했다.
### 확정 발견 사항
#### `REV-P1-019` 고정 aspect crop frame과 저장 source rectangle 불일치
| 항목 | 내용 |
|---|---|
| 심각도 | High |
| 상태 | 수정 완료 |
| 관련 요구사항 | `FILE-005~008`, `FILE-010`, `FILE-012` |
| 소유 Task | `P1-R13` |
**근거**
- `src/shared/ui/image-crop-dialog.tsx:176-186`은 crop viewport가 아니라 CSS transform이 적용된 `
`의 `getBoundingClientRect()`를 `previewFrameWidth/Height`로 보낸다.
- 같은 파일 `:210-211`의 preview는 `overflow-hidden` container 안에 원본 aspect `
`만 렌더하며, 1:1 또는 `210:297` crop 경계를 보여 주는 고정 aspect frame이 없다.
- `image-crop-dialog.test.tsx:14-18,56-66`은 `HTMLImageElement.getBoundingClientRect()`를 인위적인 256×256로 만들어 request field만 검증한다. 실제 DOM frame aspect과 원본 4:3 image가 1:1 frame에 cover되는 동작은 검증하지 않는다.
- `crop-image.ts:52-57`은 전달된 rect에 다시 `zoom`을 나눈다. transformed image rect의 width/height에 이미 zoom이 반영되므로 offset scale이 zoom을 중복 반영할 수 있다.
**재현 및 영향**
1. 4,000×3,000 원본에 Character/Audio 1:1 policy를 적용하면 preview는 4:3 image 자체를 보이지만 `calculateCropSourceRect()`는 중앙 3,000×3,000을 저장 대상으로 선택한다.
2. 이동·확대 후 운영자가 확인한 구도와 실제 upload File의 pixel 영역이 달라질 수 있어 고정 aspect media 등록 결과를 신뢰할 수 없다.
**권장 조치·판정 기록**
- 실제 crop viewport를 고정 aspect로 렌더하고 비변환 viewport rect, image cover scale, offset/zoom을 한 좌표계에서 계산한다. 새 library는 필요하지 않다.
- 2026-07-31 — 시각 frame DOM과 source 계산 data flow를 대조해 High 확정. 완료된 `P1-R10`을 열지 않고 신규 `P1-R13`으로 전환했다. 제품 코드는 수정하지 않았다.
- 2026-07-31 — `P1-R13`에서 고정 aspect viewport를 렌더하고 해당 viewport rect를 source 계산에 전달하도록 수정했다. reviewer blocker였던 image cover geometry와 zero-overhang offset clamp를 보완한 뒤 shared crop focused 2 files / 20 tests, Character·Audio·Series·Community 단위 회귀 30 files / 183 tests, `typecheck`, `lint`, 개발/운영 build, LSP diagnostics가 통과했고 reviewer delta review `APPROVED`로 수정 완료 판정했다. E2E와 수동 pixel 비교는 사용자 지시에 따라 전체 Task 구현 후 필요 시 수행한다.
#### `REV-P1-020` Character 생성·수정 route가 보호 인가 probe를 건너뜀
| 항목 | 내용 |
|---|---|
| 심각도 | High |
| 상태 | 수정 완료 |
| 관련 요구사항 | `AUTH-005`, `AUTH-006`, PRD §13 401/403 복구 |
| 소유 Task | `P1-R14` |
**근거**
- `src/app/App.tsx:86-91,163-170`은 `isAiCharactersRoute(location.path)`가 true인 경우에만 관리자 권한 probe를 실행하고 결과 전까지 protected shell을 숨긴다.
- `src/app/browser-location.ts:264-266`의 `isAiCharactersRoute()`는 `routePaths.aiCharacterCreate`와 `getCharacterEditIdFromPath()`를 포함하지 않는다. 따라서 `/ai-characters/new`와 `/ai-characters/:id/edit`는 probe 없이 바로 shell을 렌더한다.
- `App.protected-errors.test.tsx`의 401/403 test는 `/ai-characters`만 검증하고, `App.test.tsx`의 create/edit route test는 403 없이 form 렌더만 검증한다.
**재현 및 영향**
1. ADMIN session이 로컬에 남아 있지만 server가 403을 반환하는 상태에서 두 URL을 직접 열면 공통 접근 거부 흐름을 건너뛴 수 있다.
2. create는 초기 하위 request 없이 form을 노출하고, edit은 detail 403을 공통 access-denied 전환이 아닌 화면 단위 오류로 보일 수 있어 `AUTH-006`의 권한 작업 미실행 보장을 깨뜨린다.
**권장 조치·판정 기록**
- 두 route를 기존 `isAiCharactersRoute` 보호 범위에 포함하고 create/edit 직접 URL의 401/403·probe pending 회귀 test를 추가한다.
- 2026-07-31 — route matcher→App probe→child render 경로를 정적 추적해 High 확정, 신규 `P1-R14`로 전환했다. 제품 코드는 수정하지 않았다.
- 2026-07-31 — `P1-R14`에서 `isAiCharactersRoute`가 `/ai-characters/new`와 `/ai-characters/:id/edit`를 포함하게 수정해 기존 보호 route probe를 재사용했다. RED 5 failures 재현 후 focused 3 files / 23 tests, app/auth/api 회귀 13 files / 84 tests, `typecheck`, `lint`, 개발/운영 build, LSP diagnostics가 통과했고 reviewer gate `APPROVED`로 수정 완료 판정했다. stale ADMIN 개발 API 수동 확인은 외부 credential 범위로 남는다.
#### `REV-P1-021` malformed percent-encoding route가 `URIError`로 SPA를 중단함
| 항목 | 내용 |
|---|---|
| 심각도 | Medium |
| 상태 | 수정 완료 |
| 관련 요구사항 | PRD §10.6 오류 복구, 공통 route 경계 |
| 소유 Task | `P1-R15` |
**근거·재현**
- `browser-location.ts:121-261`의 모든 param parser는 regex에 일치한 segment를 `decodeURIComponent()`로 바로 decode하며 예외을 처리하지 않는다. regex `[^/]+`는 `%`를 허용한다.
- `node -e "decodeURIComponent('%')"` 실행 결과는 exit 1, `URIError: URI malformed`였다. 동일 함수가 App render 중 route 판정에서 호출되므로 `/ai-characters/%`와 같은 직접 URL은 공통 오류 화면을 거치지 않고 render를 중단할 수 있다.
**영향·권장 조치·판정 기록**
- 외부 link·수동 URL에서 화면 복구가 불가능하지만 정상 URL·인증 우회는 확인되지 않아 Medium으로 판정했다.
- decode 실패를 `null`로 끝내는 작은 helper와 `/ai-characters` 안전 fallback, 정상 한글/ASCII ID 회귀 test를 추가한다.
- 2026-07-31 — 명령 재현과 route data flow를 대조해 확정, 신규 `P1-R15`로 전환했다. 제품 코드는 수정하지 않았다.
- 2026-07-31 — `P1-R15`에서 모든 route param decode를 `decodeRouteSegment` helper로 통일해 malformed percent-encoding을 `null`로 처리하고 character list fallback을 렌더하게 했다. RED 3 failures 재현 후 focused 2 files / 12 tests, app 회귀 5 files / 37 tests, `typecheck`, `lint`, 개발/운영 build, LSP diagnostics가 통과했고 reviewer gate `APPROVED`로 수정 완료 판정했다.
### 재리뷰 검증·종료 판정
- **자동 증거:** `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod`는 exit 0이고 server E2E는 4 projects / 36 passed였다. build는 503.04kB chunk warning만 표시했다. 전체 unit은 79 files 중 5 failed / 74 passed, 397 tests 중 5 failed / 392 passed였고 5개 관련 spec focused는 5 files / 25 tests passed였다. 전체 unit 비결정성은 `REV-P9-009`/`P9-R9`로 분리했다.
- **판정:** High 2건, Medium 1건을 확정해 `P1-R13~R15`로 전환했고, `REV-P1-019~021`은 수정 완료됐다.
- **남은 위험:** 실제 image frame→upload pixel 수동 비교, 실제 개발 API stale ADMIN 403, 실기기 browser QA는 신규 Task·기존 수동 QA 범위로 남는다.
## 16. 수정 결과 재리뷰 — 2026-07-31
### 검토 범위와 실행 증거
- `P1-R13~R15`의 crop viewport/source 계산, Character create/edit 보호 probe, malformed route parser와 관련 test를 current staged working tree에서 다시 대조했다.
- `npm run test:run -- src/shared/lib/crop-image.test.ts src/shared/ui/__tests__/image-crop-dialog.test.tsx src/app/browser-location.test.ts src/app/App.protected-errors.test.tsx src/app/App.test.tsx` — exit 0, 5 files / 46 tests passed.
- 전체 `npm run test:run` — 두 차례 연속 각각 81 files / 409 tests passed. `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod`, `git diff --cached --check`도 exit 0이었다.
- `npm run e2e:mock -- tests/e2e/character-workspace.spec.ts tests/e2e/audio-content.spec.ts tests/e2e/series.spec.ts tests/e2e/community.spec.ts --project=chromium` — exit 0, 31 tests passed. 지원 범위 밖 WebKit·Mobile Safari는 실행하지 않았다.
- 실제 crop pixel 수동 비교와 실제 개발 API stale ADMIN credential QA는 실행하지 않았다.
### `REV-P1-022` — malformed route fallback이 보호 route probe를 우회함
| 항목 | 내용 |
|---|---|
| 심각도 | High |
| 상태 | 수정 완료 |
| 관련 요구사항 | `AUTH-005~006`, `P1-R15` canonical fallback interface |
| 소유 Task | 신규 `P1-R16` |
**근거·재현**
- `src/app/browser-location.ts:42~53`은 route regex 중 하나가 맞으면 decode 성공 여부를 확인하지 않고 raw pathname을 snapshot으로 반환한다.
- malformed `/ai-characters/%`는 `isCharacterRoutePath()`에는 맞지만 `getCharacterIdFromPath()`의 `decodeRouteSegment()`가 `null`을 반환한다. 따라서 `isAiCharactersRoute()`는 false가 된다(`browser-location.ts:65~67,121~149,300~301`).
- `App.tsx:86~128,163~168`의 ADMIN 보호 probe와 pending shell 차단은 `isAiCharactersRoute()`가 true일 때만 동작한다.
- `ProtectedAdminShell`은 모든 parser가 `null`이면 Character 목록을 fallback으로 렌더한다(`protected-admin-shell.tsx:199~202`). 기존 malformed test는 목록 heading만 확인하고 401/403 probe와 shell 선노출을 검증하지 않는다(`App.test.tsx:110~118`).
**영향·권장 조치**
stale ADMIN session에서 malformed 직접 URL이 공통 403 access-denied 전환과 probe pending 경계를 건너뛰고 protected shell/list를 먼저 렌더할 수 있다. backend 권한 검사를 우회해 데이터를 성공 조회하는 증거는 없지만 `P1-R14`와 같은 fail-closed 요구사항을 위반하므로 High로 판정한다. route 후보의 필수 segment decode가 실패하면 `readRoutePath()` 단계에서 `routePaths.aiCharacters` snapshot으로 귀결시키고 malformed 401/403 회귀 test를 추가한다.
**판정 기록**
- 2026-07-31 — route snapshot → `isAiCharactersRoute` → App probe → shell fallback을 정적 추적하고 기존 test 누락을 대조해 High 확정.
- 2026-07-31 — 완료된 `P1-R15`를 다시 열지 않고 신규 `P1-R16`으로 전환. 애플리케이션 코드는 수정하지 않았다.
- 2026-07-31 — `P1-R16`에서 malformed route를 canonical Character 목록 snapshot으로 정규화해 기존 ADMIN 보호 probe를 재사용하도록 수정 완료.
### 종료 판정
- `REV-P1-019~020` 수정은 focused·full·정적/build 검증에서 유지됐다. `REV-P1-021`은 URIError 중단은 막았지만 fail-closed fallback이 불완전해 신규 `REV-P1-022`로 분리했고, `P1-R16`에서 수정 완료했다.
- **최종 결론:** `REV-P1-022` 수정 완료. 실제 crop pixel·stale ADMIN server·실기기 QA는 별도 수동 범위다.
## 17. P1-R16 수정 후 검증 기록 — 2026-07-31
- RED: `npm run test:run -- src/app/App.protected-shell.test.tsx` — 1 failed / 7 passed. malformed `/ai-characters/%` route에서 보호 probe pending 대신 `AI 캐릭터 목록을 불러오는 중`이 보여 shell/list fallback 선노출을 재현했다.
- GREEN/REFACTOR: `src/app/browser-location.ts`의 route 후보 반환 전 `isAiCharactersRoute()` 검사를 추가해 decode 실패 route를 `/ai-characters` snapshot으로 정규화했다. 별도 router dependency나 auth abstraction은 만들지 않았다.
- Focused 검증: `npm run test:run -- src/app/App.protected-shell.test.tsx src/app/browser-location.test.ts` — 2 files / 9 tests passed. 개발 중 E2E 반복 실행은 사용자 지시에 따라 생략했다.
- 통합 검증: `npm run test:run` — 81 files / 411 tests passed. `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0.
## 18. P1-R16 수정 결과 재점검 — 2026-07-31
### `REV-P1-023` — malformed route 인가 matrix와 focused 명령이 불완전함
| 항목 | 내용 |
|---|---|
| 심각도 | Medium |
| 상태 | 수정 완료 |
| 관련 요구사항 | `AUTH-005~006`, `REV-P1-022`, `P1-R16` 완료 증거·Phase 1 Gate |
| 소유 Task | 신규 `P1-R17` |
**근거·영향**
- `App.protected-shell.test.tsx`의 신규 회귀는 `/ai-characters/%`와 403 한 조합만 확인한다. 기존 `App.test.tsx`의 Audio/Series 복합 malformed 경로는 목록 heading만 확인해 보호 probe 우회가 재발해도 통과할 수 있다.
- malformed 401의 login 전환·session 제거를 직접 고정한 test가 없고 `P1-R16` 실행 명령은 신규 regression spec인 `App.protected-shell.test.tsx`를 포함하지 않는다.
- 현재 root-cause guard는 focused 42 tests와 full 411 tests에서 동작하지만 fail-closed 인가 경계의 완료 증거가 Task가 요구한 단일·복합 401/403보다 좁으므로 Medium test/Gate 회귀로 판정한다.
**권장 조치·판정 기록**
- 단일·복합 malformed path × 401/403을 data-driven test로 묶어 pending shell 미노출과 최종 auth 전환을 확인하고 focused 명령에 실제 spec을 포함한다.
- 2026-07-31 — 제품 코드의 현재 동작과 test/Task 완료 증거를 분리해 확정. 완료된 `P1-R16`을 다시 열지 않고 신규 `P1-R17`로 전환했다.
### 종료 판정
- 제품 route guard의 신규 runtime 실패는 확인되지 않았고 `P1-R17`에서 누락된 malformed route 인가 matrix와 focused Gate 증거를 보강했다.
- 검증: `npm run test:run -- src/app/App.protected-shell.test.tsx` — 1 file / 14 tests passed. `npm run test:run -- src/app/App.protected-shell.test.tsx src/app/App.protected-errors.test.tsx src/app/App.test.tsx src/app/browser-location.test.ts` — 4 files / 40 tests passed. `npm run test:run -- src/app src/features/auth src/shared/api` — 13 files / 94 tests passed. `npm run typecheck`, `npm run lint`, `npm run build:dev`, `npm run build:prod` — 모두 exit 0.
- **최종 결론:** `REV-P1-023`/`P1-R17` 수정 완료. 실제 stale ADMIN 개발 API 수동 QA는 별도다.
## 19. P1-R17 수정 결과 재점검 — 2026-07-31
### 검토 범위와 실행 증거
- `App.protected-shell.test.tsx`, `App.protected-errors.test.tsx`, `App.test.tsx`, `browser-location.test.ts`와 `P1-R17` 완료 기록을 대조했다.
- focused 4 files / 40 tests와 Phase 1을 포함한 전체 81 files / 417 tests가 통과했다. `typecheck`, `lint`, 개발/운영 build도 exit 0이었다.
- malformed 단일·Audio 복합·Series 복합 route × 401/403에서 probe pending 중 shell·logout 미노출, 401 session 제거·login 이동, 403 session 유지·access-denied 이동을 직접 확인했다.
### 발견 사항과 종료 판정
- 확정 발견 사항 없음. `P1-R16`의 canonical fallback 제품 코드와 `P1-R17`의 인가 회귀 matrix·focused Gate가 일치한다.
- 실제 crop pixel 비교와 stale ADMIN 개발 API 확인은 기존 수동 QA 대기로 유지하며 자동 test 통과로 대체하지 않는다.
- **최종 결론:** Phase 1 수정 검증 완료. 신규 회귀 Task 전환 없음.