Files
voiceon-character-admin/docs/20260725_AI캐릭터관리자웹/reviews/phase1-platform-auth-shared-ui.md

35 KiB
Raw Blame History

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-001AUTH-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

실행 환경

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

관찰 내용

createPageParamssize=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-014plan-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' srccreatePageParams 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-015plan-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. calculateCropSourceRectoffsetX=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~017P1-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의 처리를 보지 못해 clearSessiononAuthExpired를 각각 실행한다.

근거

  • 코드: src/shared/api/client.ts:55-57,99-103src/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은 각각 replaceWithnavigateTo를 호출한다.
  • 테스트 누락: client-auth.test.tsaudio-upload.test.ts는 transport 내부 burst만 검증하고 fetch+XHR 교차 burst는 검증하지 않는다.
  • 문서: PRD AUTH-005, §13과 기존 P1-R6, P4-R2는 인증 제거·login 이동의 단일 실행을 요구한다.

재현 또는 검증 절차

  1. 같은 mutable token과 clearSession/onAuthExpired spy를 createApiClientuploadAudioContent에 주입한다.
  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이 적용된 <img>getBoundingClientRect()previewFrameWidth/Height로 보낸다.
  • 같은 파일 :210-211의 preview는 overflow-hidden container 안에 원본 aspect <img>만 렌더하며, 1:1 또는 210:297 crop 경계를 보여 주는 고정 aspect frame이 없다.
  • image-crop-dialog.test.tsx:14-18,56-66HTMLImageElement.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-170isAiCharactersRoute(location.path)가 true인 경우에만 관리자 권한 probe를 실행하고 결과 전까지 protected shell을 숨긴다.
  • src/app/browser-location.ts:264-266isAiCharactersRoute()routePaths.aiCharacterCreategetCharacterEditIdFromPath()를 포함하지 않는다. 따라서 /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.tsP1-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 전환 없음.