# Phase 5 커뮤니티 관리 리뷰 ## 1. 리뷰 정보 | 항목 | 내용 | |---|---| | 리뷰 대상 | Phase 5 / 커뮤니티 3개 operation | | 기준 commit 또는 working tree | `2f93e2c9` + 현재 Phase 5~7 working tree | | 리뷰 일자 | 2026-07-28 | | 리뷰어 | Codex | | 기준 문서 | `prd.md`, `plan-task.md`, `api-contract.openapi.json` | | 리뷰 상태 | 후속 수정 및 Gate 완료 | ## 2. 리뷰 목적과 범위 ### 목적 - PRD Feature E와 OpenAPI Community 3개 operation을 facade/legacy service/test에 대조한다. - multipart JSON 오류, 미지 필드, pagination과 side-effect 차단 순서를 점검한다. ### 포함 범위 - `AiCharacterAdminCommunityPostController`, `Facade`, `Repository`, DTO - 관리자 community 테스트와 재사용하는 `CreatorCommunityService` - OpenAPI Community path/schema와 plan Phase 5 ### 제외 범위 - production 수정, legacy/public community 계약 변경, 테스트 실행 ## 3. 판정 기준 | 심각도 | 기준 | |---|---| | Blocker | 소유권 우회 또는 데이터 손실 | | High | 잘못된 요청이 500/side effect로 이어지는 주요 계약 위반 | | Medium | pagination 또는 제한된 request schema 위반 | | Low | 유지보수성 또는 문서 정합성 문제 | ## 4. 검토한 근거 | 근거 | 판정 | |---|---| | `AiCharacterAdminCommunityPostFacade.kt:36`~`:50` | create는 raw JSON을 legacy service에 전달하고 update는 기본 ObjectMapper로 직접 parse | | `CreatorCommunityService.kt:73`~`:87` | create JSON을 기본 ObjectMapper로 parse한 뒤 media 검증·side effect 진행 | | `AiCharacterAdminExceptionHandler.kt:58`~`:71` | Jackson parse 예외 전용 400 변환이 없고 미분류 예외는 500 | | OpenAPI `:1156`~`:1176` | create/update request는 필수 필드와 `additionalProperties: false`를 정의 | | `AiCharacterAdminCommunityPostFacade.kt:68`~`:81`, `:141`~`:145` | 목록에 `size in 1..50`을 강제 | | OpenAPI `Size` parameter `:519` | minimum 1만 있고 maximum은 없음 | | create/update 테스트 | request part 누락은 검증하지만 malformed/missing JSON field/unknown field는 직접 검증하지 않음 | ### 실행한 검증 | 명령 또는 수동 검증 | 결과 | 핵심 증거 | |---|---|---| | parse 흐름·예외 handler·schema 정적 추적 | 성공 | parse 예외의 500 가능성과 unknown-field 허용 경계 확인 | | pagination 계약 대조 | 성공 | runtime 최대 50과 OpenAPI maximum 부재 확인 | | Gradle/컴파일/테스트 | 미실행 | 사용자 요청에 따라 실행하지 않음 | ## 5. 발견 사항 요약 | ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal | |---|---|---|---|---|---| | `REV-025` | High | 처리 완료 | multipart JSON parse 오류가 500이 될 수 있고 미지 필드를 허용 | `Task 5.7` | `P5-R1` | | `REV-026` | Medium | 처리 완료 | OpenAPI에 없는 목록 size 50 상한 | `Task 5.7` | `P5-R1` | ## 6. 발견 사항 상세 ### REV-025 — Community JSON 오류 경계 불일치 - **심각도:** High - **상태:** 처리 완료 - **관련 요구사항:** PRD Feature E, 공통 오류/side-effect 계약 - **관련 계약:** 잘못된 request는 400 `common.error.invalid_request`, request schema는 미지 필드 금지 - **소유 Task:** `Task 5.7`, `P5-R1` **관찰 내용** create는 raw request 문자열을 legacy service에 넘기고 update는 facade에서 기본 ObjectMapper로 읽는다. 두 경로 모두 `FAIL_ON_UNKNOWN_PROPERTIES`를 활성화하지 않으며 `JsonProcessingException`을 관리자 API 예외로 변환하지 않는다. **영향** malformed JSON이나 필수 non-null 필드 누락이 공통 handler의 500 `common.error.unknown`으로 분류될 수 있다. 미지 필드는 무시되어 잘못된 요청이 mutation과 S3/event 경로까지 진행될 수 있다. **권장 조치** legacy 호출 전에 create/update DTO를 strict reader로 검증하고 Jackson mapping 오류를 400 `common.error.invalid_request`로 변환한다. malformed, 필수 필드 누락, 미지 필드의 DB/S3/event 0회를 actual endpoint로 고정한다. **처리 결과** create/update `request` part를 legacy service 호출 전에 strict reader로 검증하고 Jackson parse/mapping 오류를 400 `common.error.invalid_request`로 변환했다. create malformed·필수 field 누락·미지 field, update malformed·미지 field의 actual endpoint no-side-effect 테스트를 추가했다. OpenAPI상 update request에는 required field가 없어 update 필수 field 누락 케이스는 계약 밖으로 제외했다. ### REV-026 — 계약에 없는 Community size 상한 - **심각도:** Medium - **상태:** 처리 완료 - **관련 요구사항:** 레거시 목록 query/pagination 유지 - **관련 계약:** 공통 `Size` parameter는 default 20, minimum 1이며 maximum 없음 - **소유 Task:** `Task 5.7`, `P5-R1` **관찰 내용** facade는 `size !in 1..50`을 400으로 거부한다. OpenAPI 단일 원본에는 maximum 50이 없으므로 `size=51`은 계약상 유효하다. **권장 조치** OpenAPI를 임의 변경하지 않고 관리자 facade의 상한 guard만 제거한다. page 음수와 size 1 미만 검증은 유지한다. **처리 결과** 목록의 `size <= 50` 상한 guard만 제거하고 `page < 0`, `size < 1` 검증은 유지했다. `size=51` actual endpoint 요청이 정상 pagination으로 처리되는 테스트를 추가했다. ## 7. plan·goal 전환 `plan-task.md` Phase 5의 `Task 5.7` / `P5-R1`과 `P5-R1-GATE`를 완료 처리했다. 기존 `P5-GATE` 완료 이력은 유지한다. ## 8. 리뷰 종료 판정 | 판정 항목 | 결과 | 근거 | |---|---|---| | operation/path 대조 | 충족 | Community 3개 route 존재 | | JSON 오류/schema 대조 | 충족 | strict parse와 parse 예외 400 변환, unknown-field 거부 회귀 통과 | | pagination 대조 | 충족 | 계약에 없는 maximum 50 제거, `size=51` 회귀 통과 | | ownership 선검증 | 충족 | target/post owner 확인은 mutation 전 수행 | | 실행 검증 | 충족 | focused 테스트, community/common 회귀, `ktlintCheck`, `git diff --check` 통과 | **최종 결론:** 후속 수정 및 Gate 완료 **남은 항목:** 없음. 다음 Goal은 `P6-R1`이다. ## 9. 2차 정적 리뷰 및 판정 — 2026-07-28 ### 리뷰 정보와 검증 범위 - 기준 commit/working tree: `2f93e2c9` + 현재 working tree - 기준 문서: PRD Feature E, plan Phase 5, OpenAPI Community 3개 operation - 검토 범위: fixed update facade/legacy service/repository, transaction·lock 경계와 concurrency test - 검증 방식: 두 병렬 transaction의 count/read/update 순서를 코드와 테스트로 정적 추적했다. 사용자 요청에 따라 컴파일과 테스트는 실행하지 않았다. ### 추가 발견 사항 요약 | ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal | |---|---|---|---|---|---| | `REV-033` | High | 처리 완료 | 최대 고정 3개가 실제 병렬 요청에서 보장되지 않음 | `Task 5.8` | `P5-R2` | ### REV-033 — lock 없는 count-then-update 경쟁 조건 - **심각도:** High - **상태:** 처리 완료 - **관련 요구사항:** PRD Feature E의 최대 고정 게시글 수 3개 - **관련 계약:** plan transaction/concurrency 고려사항과 `Task 5.5`의 동시 요청 완료 증거 - **소유 Task:** `Task 5.8`, `P5-R2` **관찰 내용** legacy fixed update는 활성 고정 수를 조회한 뒤 별도 게시글 entity의 `isFixed`를 변경한다. owner 또는 고정 집합을 잠그는 lock/constraint가 없으므로, 고정 2개 상태에서 서로 다른 게시글을 고정하는 두 transaction이 모두 count 2를 읽고 커밋하면 최종 고정 수는 4개가 된다. `AiCharacterAdminCommunityPostConcurrencyTest`는 이름과 달리 두 요청을 순서대로 호출한다. plan `Task 5.5`도 실제 병렬 재현을 하지 못했다고 기록하면서 Task objective·완료 증거와 Phase acceptance를 완료 처리했다. **근거** - 코드: `AiCharacterAdminCommunityPostFacade.kt:62`~`:69`은 lock 없이 legacy fixed update를 호출한다. - 코드: `CreatorCommunityService.kt:247`~`:262`는 `countBy...` 후 서로 다른 post를 갱신한다. - 테스트: `AiCharacterAdminCommunityPostConcurrencyTest.kt:55`~`:79`는 세 번째 요청 완료 후 네 번째 요청을 실행하는 순차 시나리오다. - 계획: `Task 5.5` objective/완료 증거는 동시 요청을 요구하지만 `:2849`~`:2852`에서 실제 병렬 요청은 미검증이라고 명시한다. - 기존 코드: `MemberRepository.findByIdForUpdate` owner row pessimistic lock을 재사용할 수 있다. **정적 재현 절차** 1. 같은 owner에 active fixed post 2개와 미고정 post 2개를 준비한다. 2. 두 독립 transaction이 서로 다른 미고정 post를 `isFixed=true`로 수정한다. 3. lock이 없으므로 두 transaction 모두 count 2를 읽을 수 있다. 4. 서로 다른 row를 갱신해 둘 다 커밋하면 최종 active fixed count는 4가 된다. **영향** PRD의 최대 3개 데이터 불변식이 깨지고 관리자 목록 정렬·운영 정책이 비결정적이 된다. 단순 순차 회귀는 통과하므로 현재 테스트 통과만으로 문제를 탐지할 수 없다. **권장 조치** 신규 DDL이나 dependency 없이 기존 `MemberRepository.findByIdForUpdate`로 fixed/unfixed count·update 전에 owner를 잠근다. 두 독립 transaction과 barrier/lock probe를 사용하는 결정적 병렬 테스트로 최종 3개, 한 요청 400, 실패 side effect 0건을 검증하고 sleep·반복 확률 기반 테스트는 사용하지 않는다. **처리 결과** fixed 변경 요청에서 legacy count/update 전에 `MemberRepository.findByIdForUpdate`로 owner row를 잠그도록 변경했다. 두 병렬 요청이 같은 count 경계에 진입하는 결정적 회귀 테스트를 추가했고, 최종 고정 수 3개와 한 요청 400을 확인했다. **판정 기록** - 2026-07-28 — 코드·plan 완료 증거·테스트 실행 구조로 경쟁 조건을 확정. 테스트는 사용자 요청에 따라 미실행. ### plan·goal 전환 `plan-task.md` Phase 5에 `Task 5.8` / `P5-R2`와 `P5-R2-GATE`를 추가했다. 기존 `Task 5.5` 완료 이력은 되돌리지 않고 실제 동시성 보완을 새 Goal로 추적한다. `P5-R2`와 `P5-R2-GATE`를 완료 처리했다. ### 2차 리뷰 종료 판정 | 판정 항목 | 결과 | 근거 | |---|---|---| | operation/schema 대조 | 충족 | Community 3개 mapping과 JSON 경계 유지 | | 순차 fixed 정책 | 충족 | 세 번째 성공·네 번째 거부 테스트 존재 | | 실제 동시성 불변식 | 충족 | owner row lock과 결정적 병렬 회귀 통과 | | plan 반영 | 충족 | `Task 5.8`, `P5-R2`, `P5-R2-GATE` | | 실행 검증 | 충족 | focused concurrency, community/common 회귀, `ktlintCheck`, `git diff --check` 통과 | **최종 결론:** 후속 수정 및 Gate 완료 **남은 항목:** 없음. 다음 Goal은 `P7-R2`다. ## 10. 3차 정적 리뷰 및 판정 — 2026-07-28 ### 리뷰 범위와 방식 - 기준 commit/working tree: `2f93e2c9` + 현재 working tree - 기준 문서: PRD Feature E, plan Phase 5, OpenAPI Community 3개 operation - 검토 범위: 목록·생성·수정 facade, strict multipart JSON, owner-scoped query, fixed owner lock과 관련 테스트 - 검증 방식: 코드·문서·테스트 정적 추적. 컴파일과 테스트는 실행하지 않았다. ### 발견 사항과 판정 확정 발견 사항 없음. | 판정 항목 | 결과 | 근거 | |---|---|---| | operation/schema | 충족 | Community 3개 mapping과 OpenAPI 경계 유지 | | ownership | 충족 | active target과 owner post 검증 유지 | | JSON/multipart | 충족 | strict parse와 malformed/unknown-field 400 유지 | | fixed 동시성 | 충족 | owner row lock이 count/update 앞에서 수행됨 | | plan 전환 | 해당 없음 | Phase 5 신규 Task 불필요 | **최종 결론:** Phase 5 추가 수정 없음 **남은 항목:** `P7-R3`에서 Community/common 회귀를 통합 재검증한다. ## 11. 4차 정적 리뷰 및 판정 — 2026-07-29 ### 리뷰 범위와 방식 - 기준 commit/working tree: `2f93e2c9` + 현재 working tree - 기준 문서: PRD Feature E, plan Phase 5, OpenAPI Community 3개 operation - 검토 범위: 목록·생성·수정, owner query, strict JSON, 고정 수 lock과 soft delete - 검증 방식: 코드·schema·테스트 정적 대조. 컴파일과 테스트는 실행하지 않았다. ### 발견 사항과 판정 확정 발견 사항 없음. | 판정 항목 | 결과 | 근거 | |---|---|---| | operation/schema | 충족 | Community 3개 mapping과 OpenAPI 경계 유지 | | ownership | 충족 | active target과 owner post를 mutation 전에 확인 | | 고정/soft delete | 충족 | owner row lock과 fixed 상태 동시 해제 유지 | | 오류/부작용 | 충족 | strict JSON과 target/owner 실패 선검증 유지 | | plan 전환 | 해당 없음 | Phase 5 신규 Task 불필요 | **최종 결론:** Phase 5 추가 수정 없음 **남은 항목:** 없음. ## 12. 5차 정적 리뷰 및 판정 — 2026-07-29 ### 확인된 문제 #### `REV-043` — 커뮤니티 primitive의 required·null 계약 미강제 - **심각도:** High - **상태:** 처리 완료 - **계약:** OpenAPI create request는 `isCommentAvailable`, `isAdult`를 required non-null boolean으로 정의하고, `price`와 update의 `isFixed`도 nullable로 선언하지 않는다. - **구현:** create DTO의 boolean/price는 Kotlin primitive이고 update `isFixed`는 nullable이라, strict reader가 미지 필드만 거부하면 누락·explicit null을 계약대로 구분하지 못한다. - **근거:** Jackson Kotlin/databind 2.13.5 기본 설정에서 create primitive는 false·0으로 보정될 수 있고, update `isFixed: null`은 필드 생략과 같은 null로 처리된다. - **영향:** 계약상 invalid 요청이 생성 mutation을 진행하거나 성공 no-op update로 처리될 수 있다. ### 보완 결과 | 항목 | 판정 | |---|---| | 신규 Task | `Task 5.9` / `P5-R3` 처리 완료 | | 시작 조건 | `P4-R4-GATE` 완료 후 실행 | | Gate | `P5-R3-GATE` 완료 | | RED | required boolean 누락·null, `price: null`, `isFixed: null`이 400 기대 실패 | | GREEN | v2 create/update 경계 required/non-null 검증, 생략 의미 유지 | | 범위 제한 | 전역 mapper·레거시 service·OpenAPI 변경 없음 | ### 실행 검증 | 명령 또는 검증 | 결과 | 핵심 증거 | |---|---|---| | invalid primitive RED focused | 실패 확인 | 신규 6건이 400 기대 실패 | | invalid primitive GREEN focused | 통과 | `BUILD SUCCESSFUL in 41s` | | create/update focused | 통과 | 리뷰 보완 후 `BUILD SUCCESSFUL in 34s` | | community/common 영향 범위 회귀 | 통과 | 리뷰 보완 후 `BUILD SUCCESSFUL in 1m 2s` | | `ktlintCheck`, `git diff --check` | 통과 | `ktlintCheck`는 `BUILD SUCCESSFUL in 13s`, diff check 출력 없음 | **최종 결론:** Phase 5 `REV-043` 보완 완료 **다음 Goal:** `P5-R4`. ## 13. Community 목록 요구사항 변경 판정 — 2026-07-29 ### 확정 요구사항 - **추적 ID:** `DEC-P5-LIST-001` - GET 목록에서 실제 응답 생성에 사용하지 않는 `timezone` query를 제거한다. - 성공 `data`는 직접 배열 대신 `AiCharacterAdminCommunityPostListResponse(totalCount, page, size, hasNext, items)`를 반환한다. - `totalCount`와 `hasNext`는 target creatorMember 소유 active 게시글만 기준으로 계산한다. - `items`의 기존 18개 필드와 고정 우선 정렬, owner/inactive 격리, page/size 오류 정책은 유지한다. ### 설계 판정 | 항목 | 판정 | 근거 | |---|---|---| | query | `page`, `size`만 유지 | timezone은 facade에서 유효성 검사 외 사용되지 않음 | | response | 전용 pagination wrapper 추가 | UI가 전체 개수와 추가 로딩 필요 여부를 판단해야 함 | | count | active owner count query 1개 추가 | totalCount가 필요해 size+1 조회만으로는 충족 불가 | | hasNext | `pageable.offset + items.size < totalCount` | 마지막·범위 밖 page를 단순하게 처리 | | 기존 item | 변경 없음 | 요청 범위 밖 schema 변경 방지 | | 공용 추상화 | 추가하지 않음 | 단일 endpoint 전용 DTO가 최소 변경 | ### plan 전환 - 신규 Task: `Task 5.10` / `P5-R4` - Gate: `P5-R4-GATE` - 시작 조건: `P5-R3-GATE` 완료 - 통합 조건: `P7-R5` 시작 전에 `P5-R4-GATE` 완료 ### 처리 결과 `P5-R4`에서 controller/facade의 `timezone` query와 미사용 검증을 제거하고, repository에 active owner count query를 추가했다. 성공 `data`는 `AiCharacterAdminCommunityPostListResponse(totalCount, page, size, hasNext, items)`로 반환한다. 기존 item 18개 필드, 고정 우선 정렬, owner/inactive 격리, `page < 0`·`size < 1` 오류 정책과 문서에 없는 size 상한 부재는 유지했다. OpenAPI Community GET status는 `implemented`로 복구했다. ### 실행 검증 | 명령 또는 검증 | 결과 | 핵심 증거 | |---|---|---| | query RED focused | 실패 확인 | 신규 3건이 400/직접 배열 응답 차이로 실패 | | query GREEN focused | 통과 | `BUILD SUCCESSFUL in 1m 28s` | | query+contract focused | 통과 | `BUILD SUCCESSFUL in 37s` | | community/common 영향 범위 회귀 | 통과 | `BUILD SUCCESSFUL in 1m 6s` | | OpenAPI jq assertion | 통과 | `true` | | `ktlintCheck`, `git diff --check` | 통과 | `ktlintCheck`는 `BUILD SUCCESSFUL in 12s`, diff check 출력 없음 | **최종 결론:** Phase 5 `DEC-P5-LIST-001` 목록 계약 정합화 및 Gate 완료 **다음 Goal:** `P7-R5`. ## 14. 커뮤니티 댓글 후속 검토 — 2026-07-29 ### 확인 결과 - **`REV-048` / High / 구현 대기:** 신규 v2 관리자 경계에 target 소유 커뮤니티 게시글의 댓글·답글 조회/작성/수정/삭제 5개 operation이 없다. - 조회는 필수 `timezone`과 `page`, `size`, 레거시 `totalCount/items`를 유지한다. - 작성자는 target `creatorMember`, 수정은 target 작성 활성 row만 허용한다. 삭제는 target 소유 게시글의 row를 작성자와 관계없이 soft delete하고 cascade하지 않으며 이미 비활성이면 성공 no-op이다. - 답글 `parentId`는 같은 게시글의 활성 원댓글이어야 한다. ### plan 전환 - 신규 Task: `Task 5.11` / `P5-R5` - Gate: `P5-R5-GATE` - 범위 밖: 캐릭터 직접 댓글, hard delete·cascade, legacy/public endpoint 변경 사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다. **최종 결론:** Phase 5 커뮤니티 댓글 CRUD 구현 필요 **다음 Goal:** `P5-R5`. ## 15. 커뮤니티 댓글 CRUD 구현 및 Gate — 2026-07-29 - 무엇을: `REV-048`을 처리했다. - 왜: target AI 소유 커뮤니티 게시글의 댓글·답글 조회/작성/수정/삭제 5개 operation이 신규 v2 관리자 경계에 없었기 때문이다. - 어떻게: - RED: `AiCharacterAdminCommunityPostCommentTest`에 root/reply 목록, target AI 작성, parent 검증, target 작성자 수정 제한, row-only soft delete, 요청 오류 계약 테스트 7건을 추가했고 미구현 route로 실패했다. - GREEN: `AiCharacterAdminCommunityPostController`에 5개 route를 추가하고, facade에서 target/owner/parent/actor를 선검증한 뒤 기존 `CreatorCommunityService` 댓글 조회·작성·수정 의미를 재사용했다. - Gate: focused 댓글 테스트, community/common 영향 범위 회귀, `ktlintCheck`, `git diff --check`를 fresh 실행했다. - 결과: `REV-048` 처리 완료. 캐릭터 직접 댓글, hard delete, cascade, legacy/public endpoint 변경은 추가하지 않았다. **최종 결론:** Phase 5 커뮤니티 댓글 후속 기능 종결 **다음 Goal:** `P6-R2`. ## 16. 커뮤니티 댓글 UTC 날짜 계약 변경 리뷰 및 판정 — 2026-07-29 ### 리뷰 범위와 방식 - 기준 문서: PRD Feature E, OpenAPI 2.2.0 Community 8개 operation, `DEC-UTC-DATE-001` - 검토 범위: 커뮤니티 댓글·답글 GET의 controller/facade/repository와 관련 테스트 - 검증 방식: 문서·코드·테스트 정적 대조. 사용자 요청에 따라 컴파일과 테스트는 실행하지 않았다. ### `REV-051` — High — 처리 완료: 커뮤니티 댓글 2개 GET의 timezone/UTC 계약 불일치 - 처리 전 댓글·답글 GET은 필수 `timezone` query를 controller/facade/repository로 전달하고, 각 댓글 `date`를 요청 timezone에 맞춘 표시 문자열로 반환한다. - 승인된 최신 계약은 `timezone` query 없이 `page`, `size`만 받고 기존 `totalCount`, `items`, `date` 필드명을 유지하되 `date` 값을 ISO-8601 UTC(`Z`)로 반환한다. - 댓글 작성·수정·삭제의 actor/owner/parent/soft delete 의미와 커뮤니티 게시글 목록의 pagination wrapper는 변경 대상이 아니다. ### 판정 | 항목 | 결과 | 근거 | |---|---|---| | route 수 | 유지 | Community 8개 operation 자체는 변경 없음 | | 댓글·답글 query | 처리 완료 | v2 controller/facade에서 필수 `timezone`을 제거하고 `page`·`size`만 사용 | | 댓글 `date` | 처리 완료 | owner-scoped 조회 결과를 `createdAt.toUtcIso()`로 재매핑해 UTC `date-time` 반환 | | 기존 댓글 의미 | 유지 | pagination·ownership·block/secret·mutation 정책 변경 없이 영향 범위 회귀 통과 | | legacy/public 격리 | 충족 | 기존 community 댓글 timezone service/repository 계약을 변경하지 않음 | | OpenAPI 상태 | 처리 완료 | 영향 2개 operation을 `implemented`로 동기화 | ### plan·goal 전환 - 신규 Task: `Task 5.12` / `P5-R6` - Gate: `P5-R6-GATE` - 시작 조건: `P3-R14-GATE` - 완료 조건: 댓글·답글 actual GET UTC exact JSON, 기존 pagination/ownership/block/secret 의미와 legacy/public 회귀 ### `P5-R6` / `P5-R6-GATE` 처리 결과 - RED: production 변경 전 `AiCharacterAdminCommunityPostCommentTest`는 timezone 없는 root/reply GET이 400을 반환해 2건 실패했고 `BUILD FAILED in 38s`였다. - GREEN: controller/facade의 timezone 입력·검증을 제거하고 legacy 조회 결과의 `date`만 `createdAt.toUtcIso()`로 재매핑한 뒤 같은 focused 테스트는 `BUILD SUCCESSFUL in 43s`였다. - Gate: community/common·legacy 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 11s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 26s`였고, OpenAPI는 36개 `implemented`와 0개 `alignment-required`, `git diff --check`는 출력 없음을 확인했다. **최종 결론:** `REV-051` 처리 완료, Phase 5 UTC 계약 정합화 완료 **다음 Goal:** `P7-R7`. ## 17. 6차 통합 정적 리뷰 및 판정 — 2026-07-29 ### 리뷰 범위와 방식 - 기준 문서: PRD Feature E, OpenAPI Community 8개 operation - 검토 범위: Community controller의 multipart/JSON mapping, 댓글 facade와 관련 actual endpoint 테스트 - 기준 상태: 현재 working tree - 검증 방식: 문서·코드·테스트 정적 대조. 사용자 요청에 따라 Gradle, 컴파일, 테스트는 실행하지 않았다. ### `REV-053` — High — 처리 완료 - OpenAPI는 댓글 POST와 PUT의 requestBody media type을 `application/json` 하나로 정의하고 415 response를 선언한다. - 검토 당시 두 controller mapping에는 `consumes = [MediaType.APPLICATION_JSON_VALUE]`가 없었다. - body를 `String`으로 받으므로 mapping 단계에서 media type을 제한하지 않으면 `text/plain` 같은 요청이 `HttpMediaTypeNotSupportedException`으로 차단되지 않고 handler/parser까지 진입할 수 있다. - 기존 댓글 테스트는 정상·오류 JSON 요청을 모두 `application/json`으로만 보내 미지원 media type과 415 `Accept` header/no-side-effect를 고정하지 않는다. - 외부 HTTP 요청 수용 범위와 명시된 415가 달라 High로 판정한다. ### plan 전환 - 신규 Task: `Task 5.13` / `P5-R7` - Gate: `P5-R7-GATE` - 최소 수정: 댓글 POST·PUT mapping에 JSON `consumes` 추가 - 완료 조건: 정상 JSON 회귀, 미지원 media type의 KO/EN/JA 415 envelope, `Accept` header, 작성 insert/event 0회와 수정 row 불변 - 범위 밖: facade/parser·댓글 actor/owner/parent 의미, OpenAPI·legacy/public API 변경 ### 처리 결과 - 댓글 POST·PUT mapping에 `consumes = [MediaType.APPLICATION_JSON_VALUE]`를 추가했다. - actual endpoint 회귀에서 KO/EN/JA `text/plain` 요청의 localized 415 `ApiResponse.error`, `Accept: application/json`, 작성 insert/event 0회와 수정 row 불변을 확인했다. ### `REV-058` — High — 처리 완료 - OpenAPI와 계약 설명은 게시글 생성·수정 multipart의 `request` part Content-Type을 `application/json`으로 고정한다. - 두 controller는 `@RequestPart("request") request: String`으로 받아 part 자체의 media type을 검사하지 않는다. - 정상 테스트는 JSON media type만 사용하며 미지원/누락 part media type의 415 `Accept` header와 S3/DB/event no-side-effect를 고정하지 않는다. - 같은 shared converter와 signature에서 AudioContent의 `text/plain` 성공 테스트가 있어 permissive binding을 정적으로 확인할 수 있다. ### 추가 plan 전환 - 신규 Task: `Task 5.14` / `P5-R8` - Gate: `P5-R8-GATE` - 최소 수정: 기존 strict String reader는 유지하고 v2 multipart 경계에서 part-level JSON media type만 강제 - 완료 조건: POST·PUT 정상 JSON 회귀, 미지원/누락 media type의 KO/EN/JA 415 envelope, `Accept` header, S3/DB/event no-side-effect ### `P5-R8` / `P5-R8-GATE` 처리 결과 - RED: production 변경 전 `AiCharacterAdminCommunityPostCreateTest`와 `AiCharacterAdminCommunityPostUpdateTest`에 KO/EN/JA `text/plain` 및 Content-Type 누락 `request` part 415 matrix를 추가했고, focused 명령은 12개 invocation이 415 기대 실패로 `BUILD FAILED in 56s`였다. - GREEN: controller POST·PUT 경계에서 `request` part의 JSON 호환 media type만 확인하도록 추가했다. facade strict reader, media/fixed/owner 의미, OpenAPI schema는 변경하지 않았다. - Gate: 같은 focused 명령은 `BUILD SUCCESSFUL in 59s`, community/common 영향 범위와 `AiCharacterAdminErrorContractTest` 회귀는 `BUILD SUCCESSFUL in 55s`였다. **최종 결론:** `REV-053`, `REV-058` 처리 완료, Phase 5 HTTP media type 계약 정합화 완료 **다음 Goal:** `P6-R3`. ## 18. 7차 통합 정적 리뷰 및 판정 — 2026-07-29 ### 리뷰 범위와 방식 - 기준 문서: PRD Feature E, OpenAPI `CommunityPostCreateMultipart`·`CommunityPostUpdateMultipart` - 검토 범위: Community POST·PUT controller의 multipart binding과 media/fixed/owner mutation 테스트 - 검증 방식: 현재 working tree의 문서·코드·테스트를 정적으로 대조했다. 사용자 요청에 따라 컴파일과 테스트는 실행하지 않았다. ### `REV-063` — Medium — 생성·수정의 서로 다른 허용 part 집합을 강제하지 않음 - OpenAPI는 생성에 `audioFile`, `postImage`, `request`, 수정에 `postImage`, `request`만 허용하고 두 schema 모두 `additionalProperties: false`다. - controller는 각 `@RequestPart`와 `request` media type만 처리하며 전체 part 이름을 검사하지 않는다. - 특히 수정 요청에 OpenAPI가 금지한 `audioFile`이나 임의 `unexpected` part를 추가해도 해당 part가 무시된 채 게시글 mutation이 진행될 수 있다. ### plan 전환 | 항목 | 내용 | |---|---| | 신규 Task | `Task 5.15` / `P5-R9` | | Gate | `P5-R9-GATE` | | RED | 생성·수정 미정의 part, 수정 `audioFile`, S3·DB·event 결과 | | GREEN | 생성 `{audioFile, postImage, request}`, 수정 `{postImage, request}` exact allow-list | | 회귀 | 정상 media/fixed/owner, request part 누락·415 | ### `P5-R9` / `P5-R9-GATE` 처리 결과 - RED: production 변경 전 Community POST `unexpected`, PUT `unexpected`·`audioFile` actual endpoint 테스트를 추가했고, focused 명령은 3개 케이스 모두 400 기대 실패로 `BUILD FAILED in 3m 23s`였다. - GREEN: Community controller POST는 `{audioFile, postImage, request}`, PUT은 `{postImage, request}` exact allow-list를 적용해 초과 part를 400 `common.error.invalid_request`로 거부한다. media/fixed/owner 의미와 OpenAPI schema는 변경하지 않았다. - Gate: focused 명령은 `BUILD SUCCESSFUL in 2m 30s`, community/common 영향 범위 회귀는 `BUILD SUCCESSFUL in 1m 44s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 55s`였다. **최종 결론:** `REV-063` 처리 완료, Phase 5 multipart part 이름 계약 정합화 완료 **다음 Goal:** `P7-R9`. ## 19. 8차 통합 정적 리뷰 및 판정 — 2026-07-29 ### 리뷰 범위와 방식 - 기준 문서: OpenAPI Community create/update multipart schema의 operation별 허용 part와 `additionalProperties: false` - 검토 범위: Community POST·PUT controller와 미정의 part·media/fixed/owner 회귀 테스트 - 기준 상태: 현재 working tree - 리뷰어/상태: Codex / 판정 완료 - 검증 방식: 문서·코드·테스트 소스 정적 대조. 사용자 지시에 따라 컴파일과 테스트는 실행하지 않았다. ### `REV-069` — Medium — 일반 form-field multipart part가 allow-list 우회 - `AiCharacterAdminCommunityPostController.kt:63-69`는 operation별 allow-list를 받지만 실제 검사는 `fileMap.keys`에 한정한다. - 기존 create/update 회귀는 `AiCharacterAdminCommunityPostCreateTest.kt:202-210`과 `AiCharacterAdminCommunityPostUpdateTest.kt:256-268`에서 filename이 있는 `MockMultipartFile`만 사용한다. - filename 없는 일반 form-field `unexpected`는 생성 `{audioFile, postImage, request}`, 수정 `{postImage, request}` 계약을 우회할 수 있어 Medium으로 확정한다. ### plan 전환 | 항목 | 내용 | |---|---| | 신규 Task | `Task 5.16` / `P5-R10` | | Gate | `P5-R10-GATE` | | RED | filename 없는 미정의 part의 POST·PUT 400과 S3·DB·event no-side-effect | | GREEN | servlet 전체 part 이름을 operation별 allow-list와 비교 | | 범위 제한 | media/fixed/concurrency·OpenAPI·전역 resolver·legacy/public 변경 없음 | **최종 결론:** Phase 5 보완 필요 — `REV-069` 확정 **다음 Goal:** `P5-R10` (`P4-R10-GATE` 완료 후). ## 20. 8차 후속 수정 및 Gate — 2026-07-29 - 무엇을: `REV-069`의 Community POST·PUT filename 없는 일반 form-field multipart part 우회를 보완했다. - 왜: 생성 `{audioFile, postImage, request}`, 수정 `{postImage, request}` 외 일반 form-field part가 기존 파일 map 검사만으로는 mutation 전 거부되지 않았기 때문이다. - 어떻게: create/update focused test에 filename 없는 `unexpected` part KO/EN/JA actual endpoint 회귀를 추가하고, controller가 `fileMap.keys`와 servlet `parts` 이름을 모두 operation별 allow-list와 비교하게 했다. - 결과: RED 묶음에서 신규 multipart/genre 36건 실패를 확인했고, 보완 후 focused GREEN 묶음은 `BUILD SUCCESSFUL in 1m 17s`였다. 영향 범위 회귀와 lint 결과는 `P7-R10-GATE`에 통합 기록한다. **최종 결론:** `REV-069` 처리 완료. Phase 5 후속 Gate 완료. **남은 항목:** 없음. ## 21. 9차 정적 리뷰 및 판정 — 2026-07-29 ### 리뷰 범위와 방식 - 기준 문서: PRD Feature F의 Community 요구사항, OpenAPI Community 8개 operation - 검토 범위: 게시글 목록·생성·수정, 댓글 CRUD, owner/actor/parent, multipart·concurrency 경계 - 검증 방식: 현재 working tree의 문서·production·test 소스를 정적으로 대조했다. 사용자 지시에 따라 컴파일과 테스트는 실행하지 않았다. ### 판정 및 plan 전환 - Community 8개 operation과 target owner, 댓글 actor/parent, exact multipart, 고정 제한 동시성 경계를 대조했다. - 기존 완료 finding 이후 신규 확정 finding은 없다. - Phase 5 신규 Task/Gate 없음. **최종 결론:** Phase 5 추가 수정 없음. **남은 항목:** Phase 3 보완 뒤 `P7-R11` 통합 재판정. ## 22. 10차 정적 리뷰 및 판정 — 2026-07-30 ### 리뷰 범위와 방식 - 기준 문서: PRD Feature E, OpenAPI Community 8개 operation - 검토 범위: 게시글 목록·생성·수정, 고정 동시성, 댓글 CRUD, owner/actor/parent와 multipart 경계 - 검증 방식: 현재 working tree의 문서·production·test 소스를 정적으로 대조했다. 사용자 지시에 따라 컴파일과 테스트는 실행하지 않았다. ### 판정 - Community 8개 operation과 controller mapping, active owner pagination wrapper·고정 우선 정렬이 일치한다. - 게시글 생성·수정의 strict request와 operation별 multipart part, 최대 고정 3개 owner lock·soft delete 정리가 유지된다. - 댓글의 target AI 작성/수정, 동일 게시글 활성 원댓글, row-only soft delete와 UTC 응답 계약이 유지된다. - 신규 확정 finding이 없어 Phase 5 회귀 수정 Task/Gate를 추가하지 않는다. **최종 결론:** Phase 5 요구사항 충족, 추가 수정 없음. **남은 항목:** 없음.