35 KiB
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§14AUTH-013, PRD §12 - 계약: login/logout, bearer 인증, 공통 오류 envelope
- 계획:
P1-T1~P1-GATE, 이전 회귀 Task - 코드·테스트:
src/shared/api/**,src/features/auth/**,src/layouts/**,src/shared/ui/**, 관련tests/e2e
실행 환경
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이 없음
재현 또는 검증 절차
createPageParams({ page: -1, size: 1 })을 호출하면{ page: -1, size: 20 }이 된다.createPageParams({ size: 51 })을 호출하면 size가 50이 된다.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로createPageParamsproduction 사용처 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:run72 files / 360 tests passed,npm run typecheck,npm run lint,npm run buildexit 0,npm run e2e36 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는 단일 인스턴스만 렌더링한다.
재현 또는 검증 절차
ResourcePagination두 개를 같은 container에 렌더링한다.document.querySelectorAll('#resource-page-size')가 2개인지 확인한다.- 두
페이지 크기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를 ReactuseId로 격리하고 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/styles10 files / 64 tests,src/shared31 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을 검증하지 않는다. |
재현 또는 검증 절차
- 4,000×3,000 image를 1:1 crop dialog에 연다.
- 현재 preview는
max-h-64에 의해 약 341×256 CSS px로 축소되지만 1:1 crop frame 없이 원본 4:3 전체를 표시한다. - preview를 10 CSS px 이동한다. 화면상 같은 이동은 원본 약 117px에 해당한다.
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이 없다. |
재현 또는 검증 절차
renderCrop이 pending인 상태에서 적용을 연속 클릭하면 현재applyCrop이 호출마다 새 Promise를 시작하는지 확인한다.- renderer를 reject하면 catch와 visible error가 없어 dialog에서 복구 안내를 제공하지 못하는지 확인한다.
- 각 crop source helper에서 image load 성공 후 적용·취소·새 선택·unmount를 반복한다.
- 성공 경로에서
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 sourcereleasecontract, 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 이동의 단일 실행을 요구한다.
재현 또는 검증 절차
- 같은 mutable token과
clearSession/onAuthExpiredspy를createApiClient와uploadAudioContent에 주입한다. - 두 요청을 동시에 시작하고 fetch response와 XHR response를 모두 401로 종료한다.
- 임시 진단 test를
npm run test:run -- src/features/audio-contents/tests/auth-expiry-coordination-repro.test.ts로 실행했다. - 실제 결과는 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-R12focused 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, targetedgit 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이 적용된<img>의getBoundingClientRect()를previewFrameWidth/Height로 보낸다.- 같은 파일
:210-211의 preview는overflow-hiddencontainer 안에 원본 aspect<img>만 렌더하며, 1:1 또는210:297crop 경계를 보여 주는 고정 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을 중복 반영할 수 있다.
재현 및 영향
- 4,000×3,000 원본에 Character/Audio 1:1 policy를 적용하면 preview는 4:3 image 자체를 보이지만
calculateCropSourceRect()는 중앙 3,000×3,000을 저장 대상으로 선택한다. - 이동·확대 후 운영자가 확인한 구도와 실제 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 reviewAPPROVED로 수정 완료 판정했다. 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 렌더만 검증한다.
재현 및 영향
- ADMIN session이 로컬에 남아 있지만 server가 403을 반환하는 상태에서 두 URL을 직접 열면 공통 접근 거부 흐름을 건너뛴 수 있다.
- 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 gateAPPROVED로 수정 완료 판정했다. 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를decodeRouteSegmenthelper로 통일해 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 gateAPPROVED로 수정 완료 판정했다.
재리뷰 검증·종료 판정
- 자동 증거:
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-characterssnapshot으로 정규화했다. 별도 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 전환 없음.