feat(ai-character): 관리자 기능 기반을 추가한다

This commit is contained in:
2026-07-22 01:43:08 +09:00
parent 5b700892c3
commit 3f4d7b237f
39 changed files with 5657 additions and 114 deletions

View File

@@ -18,6 +18,7 @@
- 메뉴는 클라이언트의 typed static configuration이 소유한다. Backend의 기존 `GET /menu` 및 메뉴 코드는 수정하지 않는다.
- Kotlin package는 v2지만 HTTP base path는 `/admin/ai-characters`다. `/v2`, `/admin/v2`, `/v2/admin` prefix를 추가하지 않는다.
- 신규 웹 Operation은 PRD에 명시된 66개다. 기존 `PUT /audio-content/upload-complete`는 호환 계약이므로 신규 Operation 수에 포함하지 않는다.
- 기존 `PUT /audio-content/upload-complete`는 인증 정보 없음·유효하지 않은 JWT에 `401`, 인증됐지만 `ADMIN`/`BOT`이 아닌 역할에 `403`을 반환한다. 기존 Request와 `ADMIN`/`BOT` 성공 Response는 유지한다.
- `ORIGINAL-WORK-01`~`ORIGINAL-WORK-08``/admin/ai-characters/original-works`의 global 원작 CRUD·검색·캐릭터 배정 계약이다. legacy `/admin/chat/original/**`의 Method·Path·Request·성공 Response와 일반 사용자용 `/api/chat/original/**`는 호환을 위해 유지한다.
- `CONTENT-08`이라는 신규 Endpoint, V1/V2 dispatcher와 v2 completion use case를 만들지 않는다. v2 콘텐츠도 기존 row·S3 계약을 따라 현재 callback이 동일하게 처리한다.
- AWS S3 Trigger worker 코드, worker 스케줄, metadata 계약 및 AWS Trigger 설정은 생성·수정하지 않는다.
@@ -212,24 +213,25 @@ Phase 0 기존 callback·소비자 계약 고정
### Phase 0: 기존 callback·소비자 계약 고정
- [ ] **Task 0.1: 변경 전 callback·소비자 API 계약을 회귀 테스트로 고정**
- [x] **Task 0.1: 변경 전 callback·소비자 API 계약을 회귀 테스트로 고정**
- Files:
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/content/AudioContentUploadCompletionContractTest.kt`
- Verify: `src/test/kotlin/kr/co/vividnext/sodalive/admin/chat/character/AdminChatCharacterControllerTest.kt`
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/admin/chat/original/AdminOriginalWorkControllerContractTest.kt`
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/chat/original/controller/OriginalWorkControllerContractTest.kt`
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/legacy/LegacyAdminSearchQueryContractTest.kt`
- Verify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/series/adapter/in/web/CreatorChannelSeriesControllerTest.kt`
- Verify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityControllerTest.kt`
- Verify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/fantalk/adapter/in/web/CreatorChannelFanTalkControllerTest.kt`
- RED: TDD 예외 사유: 변경 대상이 아닌 기존 callback·소비자 계약을 characterization test로 고정하는 작업이므로 의도적인 production 결함을 먼저 만들지 않는다.
- 대체 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.content.AudioContentUploadCompletionContractTest --tests kr.co.vividnext.sodalive.admin.chat.character.AdminChatCharacterControllerTest --tests kr.co.vividnext.sodalive.admin.chat.original.AdminOriginalWorkControllerContractTest --tests kr.co.vividnext.sodalive.chat.original.controller.OriginalWorkControllerContractTest --tests kr.co.vividnext.sodalive.v2.api.creator.channel.series.adapter.in.web.CreatorChannelSeriesControllerTest --tests kr.co.vividnext.sodalive.v2.api.creator.channel.community.adapter.in.web.CreatorChannelCommunityControllerTest --tests kr.co.vividnext.sodalive.v2.api.creator.channel.fantalk.adapter.in.web.CreatorChannelFanTalkControllerTest`를 실행해 기존 `PUT /audio-content/upload-complete`, legacy 캐릭터·원작 관리자, 일반 사용자용 원작 API, BOT/ADMIN 인가, Request/Response 소비자 API 계약 중 현재 구현과 어긋난 지점이 있으면 먼저 조사하고, 모두 일치하면 최초 통과 결과를 baseline으로 기록한다. legacy 원작 mutation은 이후 같은 v2 정책으로 수렴하므로 이 Task에서는 Method·Path·Request·성공 Response를 고정하고 잘못된 mutation을 성공시키는 내부 동작을 호환 계약으로 고정하지 않는다.
- 대체 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.content.AudioContentUploadCompletionContractTest --tests kr.co.vividnext.sodalive.admin.chat.character.AdminChatCharacterControllerTest --tests kr.co.vividnext.sodalive.admin.chat.original.AdminOriginalWorkControllerContractTest --tests kr.co.vividnext.sodalive.chat.original.controller.OriginalWorkControllerContractTest --tests kr.co.vividnext.sodalive.legacy.LegacyAdminSearchQueryContractTest --tests kr.co.vividnext.sodalive.v2.api.creator.channel.series.adapter.in.web.CreatorChannelSeriesControllerTest --tests kr.co.vividnext.sodalive.v2.api.creator.channel.community.adapter.in.web.CreatorChannelCommunityControllerTest --tests kr.co.vividnext.sodalive.v2.api.creator.channel.fantalk.adapter.in.web.CreatorChannelFanTalkControllerTest`를 실행해 callback의 Method·Path·Request·`ADMIN`/`BOT` 성공 Response·인가 status, legacy 캐릭터·원작 관리자의 Method·Path·Request·성공 Response, 일반 사용자용 원작 API와 소비자 API 계약 중 현재 구현과 어긋난 지점이 있으면 먼저 조사하고, 모두 일치하면 최초 통과 결과를 baseline으로 기록한다. callback은 인증 정보 없음·유효하지 않은 JWT `401`, 인증됐지만 `ADMIN`/`BOT`이 아닌 역할 `403`을 고정한다. legacy 검색은 실제 repository/service로 검색 field, 활성·삭제 제외, 정렬과 pagination을 고정한다. legacy 원작 mutation은 이후 같은 v2 정책으로 수렴하므로 이 Task에서는 Method·Path·Request·성공 Response를 고정하고 잘못된 mutation을 성공시키는 내부 동작을 호환 계약으로 고정하지 않는다.
- GREEN: 이 Task에서는 신규 route를 구현하지 않는다. callback과 기존 소비자 API가 현재 상태에서 통과하는지 baseline을 기록한다.
- REFACTOR: 테스트 fixture는 실제 Spring mapping과 응답 surface를 검증하며 운영 코드를 위한 범용 endpoint registry를 만들지 않는다.
- 기대 결과: 기존 callback, legacy 캐릭터·원작 관리자와 소비자 API 계약이 초록색 baseline으로 고정된다.
### Phase 1: 공통 관리자 계약, 보안, 대상 해석
- [ ] **Task 1.1: 공통 page/응답/multipart JSON 계약 구현**
- [x] **Task 1.1: 공통 page/응답/multipart JSON 계약 구현**
- Files:
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/dto/AdminCommonDtos.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/application/AdminPagePolicy.kt`
@@ -238,30 +240,34 @@ Phase 0 기존 callback·소비자 계약 고정
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/application/AdminPagePolicyTest.kt`
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AdminJsonRequestParserTest.kt`
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AdminImagePartValidatorTest.kt`
- RED: page `null/-1/0`, size `null/0/1/20/50/51`, page response `hasNext`, multipart JSON과 일반 JSON body의 필수 key 누락/명시적 `null` 구분, 이미지 bytes의 실제 MIME과 `allowGif` 조건 테스트한다.
- RED: page `null/-1/0`, size `null/0/1/20/50/51`, page response `hasNext`, multipart JSON과 일반 JSON body의 필수 key 누락/명시적 `null` 구분, 빈 multipart JSON과 단일 root 뒤의 추가 root·garbage 거부, 이미지 bytes의 실제 MIME `image/jpeg`, `image/png`, `image/gif`, 10MB·한 변 20,000px·총 40,000,000 pixels 초과 거부를 테스트한다. PNG는 ancillary payload 합계 1MB·chunk 4,096개 상한과 `ignoreMetadata=true`를 검증한다. GIF는 최대 500 frame, extension 1,024개, extension당 sub-block 64개, extension payload 합계 1MB, 전체 frame 누적 40,000,000 pixels 상한을 검증하고, logical canvas·모든 frame header의 동일 상한, 선언 pixel 수와 정확히 일치하는 LZW 출력, 조기 EOI·연속 clear·EOI 뒤 data와 첫 frame이 정상이지만 후속 frame decode가 손상된 입력 거부를 고정한다. 실제 format 확인 시 모든 GIF frame을 각각 1x1 출력 영역으로 decode하는지와 `allowGif` 조건 테스트한다.
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.application.AdminPagePolicyTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AdminJsonRequestParserTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AdminImagePartValidatorTest`
- 통과 확인: GREEN 구현 후 위 실패 확인 명령을 다시 실행해 `BUILD SUCCESSFUL`을 확인한다.
- GREEN: `AdminPageResponse`, 공통 mutation/comment request와 PRD의 정규화 규칙만 구현한다. `AdminJsonRequestParser`는 multipart JSON string과 `JsonNode` 모두에서 required nullable key를 검증하고, `AdminImagePartValidator`는 v2 web adapter에서 실제 MIME을 검사한다. domain port에는 admin DTO나 Spring `Pageable`을 넘기지 않고 정규화된 offset/limit을 전달한다.
- GREEN: `AdminPageResponse`, 공통 mutation/comment request와 PRD의 정규화 규칙만 구현한다. `AdminJsonRequestParser`는 multipart JSON string과 `JsonNode` 모두에서 required nullable key를 검증하고, multipart JSON string은 parser 전용 strict reader로 단일 root만 허용한다. `AdminImagePartValidator`는 v2 web adapter에서 실제 MIME을 검사한다. PNG ImageIO 입력은 `ignoreMetadata=true`로 metadata를 읽지 않고, container preflight에서 ancillary payload 합계 1MB와 chunk 4,096개를 제한한다. GIF preflight는 최대 500 frame, extension 1,024개, extension당 sub-block 64개, extension payload 합계 1MB, 전체 frame 누적 40,000,000 pixels를 제한하고 각 frame LZW 출력이 선언 pixel 수와 정확히 일치하는지 bounded code parser로 확인한다. 실제 format 확인 decode는 모든 frame의 1x1 출력 영역으로 제한하고, 입력 유래 decoder 예외를 잘못된 이미지로 처리한다. domain port에는 admin DTO나 Spring `Pageable`을 넘기지 않고 정규화된 offset/limit을 전달한다.
- REFACTOR: 기존 `CreatorChannel*QueryPolicy`는 size 1~19 처리 계약이 다르므로 수정하거나 재사용하지 않는다.
- 기대 결과: 모든 관리자 목록과 multipart update가 하나의 명시적 계약을 사용한다.
- 기대 결과: 모든 관리자 목록과 multipart update가 하나의 명시적 계약을 사용하고, image part는 bytes·container 구조·metadata·frame·LZW 작업량의 bounded 안전 경계를 공유한다.
- [ ] **Task 1.2: `/admin/ai-characters/**` 전용 오류·인증 응답 경계 구현**
- [x] **Task 1.2: `/admin/ai-characters/**` 전용 오류·인증 응답 경계 구현**
- Files:
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/common/SodaException.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/common/SodaExceptionHandler.kt`
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AiCharacterAdminExceptionHandler.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/security/AiCharacterAdminAuthenticationEntryPoint.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/security/AiCharacterAdminAccessDeniedHandler.kt`
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AiCharacterAdminSecurityIntegrationTest.kt`
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AiCharacterAdminLoginJwtIntegrationTest.kt`
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AiCharacterAdminExceptionHandlerTest.kt`
- RED: 기존 `POST /admin/member/login` 응답 token으로 신규 API 호출 성공, 무JWT 401, 잘못된 JWT 401, `USER/CREATOR/AGENT/CONTENT_MANAGER` 403, ADMIN 통과 및 400/404/409/500/502별 `ApiResponse` body와 `errorProperty`를 검증한다. 같은 예외가 legacy route에서는 기존 HTTP 200 관례를 유지하는 회귀 케이스도 추가한다.
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AiCharacterAdminSecurityIntegrationTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AiCharacterAdminExceptionHandlerTest`
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/legacy/LegacySodaExceptionHttpStatusContractTest.kt`
- Verify: `src/test/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberLoginServiceTest.kt`
- RED: 기존 `POST /admin/member/login` 응답 token으로 신규 API 호출 성공, 무JWT 401, 잘못된 JWT 401, `USER/CREATOR/AGENT/CONTENT_MANAGER` 403, ADMIN 통과 및 400/404/409/500/502별 `ApiResponse` body와 `errorProperty`를 검증한다. 실제 Spring Boot context에서 두 production advice와 `SecurityConfig`를 함께 로드하고, DispatcherServlet에 연결된 test `MultipartResolver`가 handler 선택 전에 실패할 때 admin 경로는 400, legacy 경로는 기존 HTTP 200과 unknown 메시지를 유지하는지도 검증한다.
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AiCharacterAdminSecurityIntegrationTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AiCharacterAdminLoginJwtIntegrationTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AiCharacterAdminExceptionHandlerTest --tests kr.co.vividnext.sodalive.legacy.LegacySodaExceptionHttpStatusContractTest --tests kr.co.vividnext.sodalive.admin.member.AdminMemberLoginServiceTest`
- 통과 확인: GREEN 구현 후 위 실패 확인 명령을 다시 실행해 `BUILD SUCCESSFUL`을 확인한다.
- GREEN: `SodaException` 생성자 끝에 legacy 기본 동작을 보존하는 선택 HTTP status를 추가하고, 우선순위가 높은 admin Controller 범위 advice와 path-specific security handler만 그 status를 응답에 사용한다.
- REFACTOR: 기존 `SodaExceptionHandler`, `JwtAuthenticationEntryPoint`, `JwtAccessDeniedHandler`의 응답을 변경하지 않는다.
- 기대 결과: 신규 관리자 API만 PRD 20장의 status/envelope를 사용하고 legacy API는 영향받지 않는다.
- [ ] **Task 1.3: AI 캐릭터 관리자 대상 해석과 owner 입력 차단 구현**
- [x] **Task 1.3: AI 캐릭터 관리자 대상 해석과 owner 입력 차단 구현**
- Files:
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/domain/AiCharacterAdminTarget.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/application/AiCharacterAdminTargetResolver.kt`
@@ -276,17 +282,17 @@ Phase 0 기존 callback·소비자 계약 고정
- REFACTOR: JPA `ChatCharacter`, `Member`, Q type은 persistence adapter 밖으로 노출하지 않는다.
- 기대 결과: 모든 character-scoped facade가 동일한 대상 해석 결과를 사용한다.
- [ ] **Task 1.4: direct after-commit 실행과 구조화 관리자 audit 기반 구현**
- [x] **Task 1.4: direct after-commit 실행과 구조화 관리자 audit 기반 구현**
- Files:
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/common/application/AfterCommitExecutor.kt`
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/application/AiCharacterAdminAuditLogger.kt`
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/common/application/AfterCommitExecutorTest.kt`
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/common/application/AfterCommitEventBoundaryIntegrationTest.kt`
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/application/AiCharacterAdminAuditLoggerTest.kt`
- RED: direct callback의 commit 후 1회 실행, rollback/동일 command 재시도에서 미실행·비중복, transaction 안에서 publish한 기존 FCM/언어 event listener가 commit 후 실행되고 유실되지 않는지, mutation 성공/실패 audit field와 민감 본문 미기록을 검증한다. global 원작 CRUD는 nullable character field를 허용하고 원작 배정·해제는 캐릭터별 context를 요구하는지도 고정한다.
- RED: direct callback의 commit 후 1회 실행과 rollback 미실행을 검증하고, 실제 `TransactionTemplate`에서 첫 attempt가 rollback동일 command 재시도해 commit하면 callback이 두 attempt 합계 1회 실행되는 기존 동작을 characterization으로 고정한다. `AfterCommitExecutor`에는 command identity가 없으므로 attempt 간 dedup은 요구하지 않는다. 기존 `FcmEvent`는 commit 후, `LanguageDetectEvent`는 commit 후·rollback 시, `LanguageTranslationEvent(waitTransactionCommit=true)`는 commit 후 listener 동작을 각각 검증하고 세 listener의 `AFTER_COMMIT` 선언도 고정한다. mutation 성공/실패 audit field와 민감 본문 미기록을 검증한다. global 원작 context는 `ORIGINAL_WORK``CREATE/UPDATE/DELETE`만, 원작 배정·해제는 캐릭터별 `ORIGINAL_WORK_CHARACTER` context만 허용한다. character-scoped context의 `creatorMemberId``UNASSIGN`에서만 nullable로 두되, factory가 정상/과거 불일치 해제 타입을 구분하지 않는다. factory 검증을 우회하는 public `copy`도 노출하지 않는다.
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.common.application.AfterCommitExecutorTest --tests kr.co.vividnext.sodalive.v2.common.application.AfterCommitEventBoundaryIntegrationTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.application.AiCharacterAdminAuditLoggerTest`
- 통과 확인: GREEN 구현 후 위 실패 확인 명령을 다시 실행해 `BUILD SUCCESSFUL`을 확인한다.
- GREEN: `AfterCommitExecutor`는 home news 같은 direct callback만 Spring transaction synchronization에 등록한다. 기존 `FcmEvent`, `LanguageDetectEvent`, `LanguageTranslationEvent(waitTransactionCommit=true)`는 transaction 안에서 publish하고 각 listener의 AFTER_COMMIT 경계를 유지한다. structured audit logger는 global/character-scoped context를 명시적으로 구분하는 정도로만 추가하며 audit table이나 AOP framework 만들지 않는다.
- GREEN: `AfterCommitExecutor`는 home news 같은 direct callback만 Spring transaction synchronization에 등록한다. 기존 `FcmEvent`, `LanguageDetectEvent`, `LanguageTranslationEvent(waitTransactionCommit=true)`는 transaction 안에서 publish하고 각 listener의 AFTER_COMMIT 경계를 유지한다. structured audit logger는 private factory로 global/character-scoped context와 action/resource/creator 조합을 강제하되 정상/과거 불일치 해제용 별도 command나 factory를 만들지 않으며, audit table이나 AOP framework 만들지 않는다.
- REFACTOR: 비밀번호, JWT, system prompt 전체, 댓글/게시글 본문, 업로드 파일 내용이 logger argument에 들어갈 수 없도록 audit context를 ID와 enum 중심으로 제한한다.
- 기대 결과: 이후 facade와 event adapter가 같은 commit/audit 원칙을 반복 구현하지 않는다.
@@ -402,7 +408,7 @@ Phase 0 기존 callback·소비자 계약 고정
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/admin/chat/original/AdminOriginalWorkControllerContractTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/osiv/OsivLazyLoadingRegressionTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/AiCharacterAdminRouteInventoryTest.kt`
- RED: PRD 11.7의 8개 exact method/path, Query, 전체 envelope JSON field·nullable·UTC, 생성/수정 multipart part와 11개 key, 이미지 실제 MIME/GIF 거부, DELETE JSON body와 proxy 통과, ADMIN 인가와 정확한 400/404/409/500/502를 MockMvc로 고정한다. assignment response의 요청 순서 전체 ID와 작업 후 전체 `characterCount`, 같은 원작 멱등 ID, nullable `creatorId`, global CRUD audit의 nullable character field와 배정·해제 캐릭터별 audit도 검증한다. legacy 원작 5개 mutation route는 Method·Path·Request·성공 `data=null`을 유지하면서 같은 v2 원작 input port를 호출하고, legacy update의 null은 잠금 transaction 안에서 현재 값 유지로 병합되며 연결 삭제·부분 배정·오귀속 해제는 더 이상 성공하지 않는지 확인한다.
- RED: PRD 11.7의 8개 exact method/path, Query, 전체 envelope JSON field·nullable·UTC, 생성/수정 multipart part와 11개 key, 이미지 실제 MIME/GIF 거부, DELETE JSON body와 proxy 통과, ADMIN 인가와 정확한 400/404/409/500/502를 MockMvc로 고정한다. assignment response의 요청 순서 전체 ID와 작업 후 전체 `characterCount`, 같은 원작 멱등 ID, nullable `creatorId`, global CRUD audit의 nullable character field와 배정·해제 캐릭터별 audit도 검증한다. 해제 audit은 캐릭터의 `creatorMemberId`를 확인할 수 있으면 실제 값을 기록하고, 연결 정보가 없거나 해석할 수 없는 과거 불일치 상태에서만 `null`을 기록한다. legacy 원작 5개 mutation route는 Method·Path·Request·성공 `data=null`을 유지하면서 같은 v2 원작 input port를 호출하고, legacy update의 null은 잠금 transaction 안에서 현재 값 유지로 병합되며 연결 삭제·부분 배정·오귀속 해제는 더 이상 성공하지 않는지 확인한다.
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AdminOriginalWorkControllerIntegrationTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.AiCharacterAdminRouteInventoryTest --tests kr.co.vividnext.sodalive.admin.chat.original.LegacyOriginalWorkMutationAdapterTest --tests kr.co.vividnext.sodalive.admin.chat.original.AdminOriginalWorkControllerContractTest --tests kr.co.vividnext.sodalive.osiv.OsivLazyLoadingRegressionTest`
- 통과 확인: GREEN 구현 후 위 실패 확인 명령을 다시 실행해 `BUILD SUCCESSFUL`을 확인한다.
- GREEN: 신규 Controller는 `AdminJsonRequestParser`, `AdminImagePartValidator`와 실제 `adminMemberId`만 사용하고 facade는 원작 use case 호출, Response 변환과 audit만 담당한다. `ORIGINAL-WORK-08``Content-Type: application/json` DELETE body를 명시적으로 매핑한다. legacy 원작 compatibility adapter는 legacy DTO와 multipart의 non-null update field만 v2 호환 patch command로 변환한다. 현재 값 병합은 adapter 선조회가 아니라 v2 application이 원작 row를 잠근 transaction 안에서 수행한다. 원작 조회는 기존 service를 유지하고 legacy 원작 5개 direct mutation만 v2 input으로 수렴한다. legacy 캐릭터가 아직 호출하는 `assignOneCharacter`는 Phase 10의 전체 캐릭터 호환 전환 전까지만 남기고, 나머지 미사용 direct mutation 메서드와 그로 인해 불필요해진 dependency만 제거해 OSIV 회귀 fixture의 constructor를 맞춘다.
@@ -433,10 +439,10 @@ Phase 0 기존 callback·소비자 계약 고정
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/content/AudioContentUploadCompletionContractTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/content/AudioContentServiceTest.kt`
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/content/AudioContentReleaseQueryTest.kt`
- RED: 기존 callback의 method/path/BOT·ADMIN/Request/`data={}`는 그대로인 상태에서 v2 생성과 동일한 기존 row가 정상 완료되는지 검증한다. 처리 중 삭제된 `releaseDate=null` row와 비활성 creator row는 callback이 output path·duration을 기록해도 raw `content.isActive=false`와 공개 FCM/home news 0회를 유지해야 한다. 비활성 creator의 과거 불일치 row가 raw `content.isActive=true`이면 callback 후 `false`로 보정한다. 활성 creator의 즉시 공개는 최초 false→true에서만 공개 side effect를 내며 동일 callback 재시도는 이를 중복하지 않아야 한다. 예약 공개 query는 `isActive=false`, non-null due releaseDate, non-null duration, 활성 creator를 모두 만족하는 row만 반환하는지 검증한다.
- RED: 기존 callback의 method/path/Request와 `ADMIN`/`BOT` 성공 `data={}`는 그대로이고, 인증 정보 없음·유효하지 않은 JWT는 `401`, 인증됐지만 `ADMIN`/`BOT`이 아니면 `403`인 상태에서 v2 생성과 동일한 기존 row가 정상 완료되는지 검증한다. 처리 중 삭제된 `releaseDate=null` row와 비활성 creator row는 callback이 output path·duration을 기록해도 raw `content.isActive=false`와 공개 FCM/home news 0회를 유지해야 한다. 비활성 creator의 과거 불일치 row가 raw `content.isActive=true`이면 callback 후 `false`로 보정한다. 활성 creator의 즉시 공개는 최초 false→true에서만 공개 side effect를 내며 동일 callback 재시도는 이를 중복하지 않아야 한다. 예약 공개 query는 `isActive=false`, non-null due releaseDate, non-null duration, 활성 creator를 모두 만족하는 row만 반환하는지 검증한다.
- 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.content.AudioContentUploadCompletionContractTest --tests kr.co.vividnext.sodalive.content.AudioContentServiceTest --tests kr.co.vividnext.sodalive.content.AudioContentReleaseQueryTest`
- 통과 확인: GREEN 구현 후 위 실패 확인 명령을 다시 실행해 `BUILD SUCCESSFUL`을 확인한다.
- GREEN: 기존 `AudioContentService.uploadComplete`의 공개 조건에 non-null releaseDate, 활성 creator와 최초 활성 전이를 추가하고 비활성 creator의 raw `content.isActive``false`로 유지·보정한다. 기존 release query에는 활성 creator 조건을 추가한다. Controller, Request/Response, scheduler component의 cron·lock은 변경하지 않는다.
- GREEN: 기존 `AudioContentService.uploadComplete`의 공개 조건에 non-null releaseDate, 활성 creator와 최초 활성 전이를 추가하고 비활성 creator의 raw `content.isActive``false`로 유지·보정한다. 기존 release query에는 활성 creator 조건을 추가한다. Controller의 Method·Path·Request와 `ADMIN`/`BOT` 성공 Response, scheduler component의 cron·lock은 변경하지 않는다.
- REFACTOR: 신규 callback Controller/DTO/use case, V1/V2 dispatcher, pipeline metadata와 v2 scheduler가 생기지 않았는지 diff를 확인한다.
- 기대 결과: 기존 callback과 scheduler를 모든 콘텐츠가 공용하면서 삭제 콘텐츠와 비활성 creator를 다시 공개하지 않는다.
@@ -860,7 +866,7 @@ Phase 0 기존 callback·소비자 계약 고정
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/AiCharacterAdminInactiveStateEndToEndTest.kt`
- Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/AiCharacterAdminAuditEndToEndTest.kt`
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/AiCharacterAdminRouteInventoryTest.kt`
- RED: route inventory가 PRD의 method/path 66개와 중복·누락·추가 없이 정확히 일치하는지 먼저 검증한다. 이어서 리소스군마다 다른 character 소유 Path ID는 404, Request body의 잘못된 parent/root 귀속은 400과 해당 field, inactive character의 GET·DELETE 허용과 그 외 mutation 409, inactive parent mutation 409, 논리 리소스 DELETE retry 200, 실제 ADMIN과 대행 creator 구분, 모든 mutation success/failure audit를 parameterized E2E로 작성한다. global 원작은 character 선택 없이 접근되고, 0 이하·미존재·삭제 ID의 정확한 400/404/409, 연결 존재 삭제 409, 이미 삭제된 상태 우선의 반복 DELETE 200, 배정·해제 전건 검증·원자성, 다른 원작 이동과 audit nullable/캐릭터별 규칙도 포함한다. 같은 원작 재배정은 요청 순서 ID 전체를 반환하는 멱등 성공이고 이미 해제된 관계의 ORIGINAL-WORK-08 재시도는 귀속 불일치 400인지 구분한다. legacy 원작 mutation 5개와 캐릭터 mutation 2개 전체도 각각 같은 v2 원작·캐릭터 command를 사용하며 동시 삭제·배정에서 삭제 원작 참조를 만들지 않는지 포함한다.
- RED: route inventory가 PRD의 method/path 66개와 중복·누락·추가 없이 정확히 일치하는지 먼저 검증한다. 이어서 리소스군마다 다른 character 소유 Path ID는 404, Request body의 잘못된 parent/root 귀속은 400과 해당 field, inactive character의 GET·DELETE 허용과 그 외 mutation 409, inactive parent mutation 409, 논리 리소스 DELETE retry 200, 실제 ADMIN과 대행 creator 구분, 모든 mutation success/failure audit를 parameterized E2E로 작성한다. global 원작은 character 선택 없이 접근되고, 0 이하·미존재·삭제 ID의 정확한 400/404/409, 연결 존재 삭제 409, 이미 삭제된 상태 우선의 반복 DELETE 200, 배정·해제 전건 검증·원자성, 다른 원작 이동과 audit nullable/캐릭터별 규칙도 포함한다. 정상/과거 불일치 해제를 별도 command나 factory로 나누지 않되, 해제 audit은 확인 가능한 실제 `creatorMemberId`를 기록하고 연결 정보가 없거나 해석할 수 없을 때만 `null`을 기록한다. 같은 원작 재배정은 요청 순서 ID 전체를 반환하는 멱등 성공이고 이미 해제된 관계의 ORIGINAL-WORK-08 재시도는 귀속 불일치 400인지 구분한다. legacy 원작 mutation 5개와 캐릭터 mutation 2개 전체도 각각 같은 v2 원작·캐릭터 command를 사용하며 동시 삭제·배정에서 삭제 원작 참조를 만들지 않는지 포함한다.
- 실패 확인: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.admin.aicharacter.AiCharacterAdmin*EndToEndTest' --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.AiCharacterAdminRouteInventoryTest`
- 통과 확인: GREEN 구현 후 위 실패 확인 명령을 다시 실행해 `BUILD SUCCESSFUL`을 확인한다.
- GREEN: 누락된 owner/status/audit 연결만 각 facade/use case에 보완한다.
@@ -929,7 +935,7 @@ Phase 0 기존 callback·소비자 계약 고정
13. `awk '$0=="### 27.8 Copy-Paste Frontend Development Prompt"{section=1; next} section && $0=="```text"{capture=1} capture{print} capture && $0=="```"{exit}' docs/20260720_AI캐릭터_관리자기능/prd.md | shasum -a 256` 결과가 `5956ddc152c026937728381d625859bdea65b9a2f16a39b200f0d6b3660a73e1`인지 확인한다.
14. `find docs/20260720_AI캐릭터_관리자기능 -type f -name '*.sql' -print` 출력이 0건인지 확인한다.
15. `rg -o '^### Phase [0-9]+' docs/20260720_AI캐릭터_관리자기능/plan-task.md | wc -l` 결과가 12인지 확인한다.
16. `rg -o '^- \[ \] \*\*Task [0-9]+\.[0-9]+:' docs/20260720_AI캐릭터_관리자기능/plan-task.md | wc -l` 결과가 37인지 확인한다.
16. `rg -o '^- \[[ x]\] \*\*Task [0-9]+\.[0-9]+:' docs/20260720_AI캐릭터_관리자기능/plan-task.md | wc -l` 결과가 37인지 확인한다.
17. `rg -c '^ - 실패 확인:' docs/20260720_AI캐릭터_관리자기능/plan-task.md``rg -c '^ - 통과 확인:' docs/20260720_AI캐릭터_관리자기능/plan-task.md` 결과가 각각 35인지 확인한다.
18. `rg -c '^ - RED: TDD 예외' docs/20260720_AI캐릭터_관리자기능/plan-task.md` 결과가 2인지 확인한다.
19. `rg -n $'\t| +$' docs/20260720_AI캐릭터_관리자기능/prd.md docs/20260720_AI캐릭터_관리자기능/plan-task.md docs/20260720_AI캐릭터_관리자기능/frontend-original-work-prompt.md` 출력이 0건인지 확인하고, 각 문서의 code fence 개수가 짝수인지 확인한다. untracked 문서는 `git diff --check`만으로 검사되지 않으므로 파일 자체 검사도 수행한다.
@@ -959,12 +965,36 @@ Phase 0 기존 callback·소비자 계약 고정
## 5. 구현 시 검증 기록
이 문서를 생성한 현재는 구현 전이므로 Task checkbox를 모두 미체크로 유지한다. 구현 에이전트는 각 Task에서 실제로 실행한 명령, RED 실패 원인, GREEN 성공 결과와 미실행 외부 Gate를 이 절에 누적한다.
구현 에이전트는 각 Task에서 실제로 실행한 명령, RED 실패 원인, GREEN 성공 결과와 미실행 외부 Gate를 이 절에 누적한다. Phase 0/1은 완료했고 Phase 2 이후 구현 Task는 미착수 상태로 유지한다.
- 문서 생성 검증: 아래 “문서 자체 검증 기록”에만 기록한다.
- 코드 구현 검증: 아직 실행하지 않음.
- 코드 구현 검증: Phase 0/1 구현 중 아래 명령을 실행해 `BUILD SUCCESSFUL`을 확인했다.
- RED 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.application.AdminPagePolicyTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AdminJsonRequestParserTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AdminImagePartValidatorTest --tests kr.co.vividnext.sodalive.v2.aicharacter.application.AiCharacterAdminTargetResolverTest --tests kr.co.vividnext.sodalive.v2.common.application.AfterCommitExecutorTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.application.AiCharacterAdminAuditLoggerTest`가 미구현 클래스 참조로 실패했다.
- 리뷰 4차 RED 확인: parser trailing root·garbage 2건, 이미지 전체 decode·dimension·pixel 3건, audit invalid 조합 3건과 public `copy` 1건이 각각 기존 구현에서 예상한 이유로 실패했다. Phase 0 legacy fixed-wire 보강과 rollback→retry는 기존 동작을 고정하는 characterization test라 production 변경 없이 통과했다.
- 리뷰 5차 RED 확인: malformed GIF의 `IndexOutOfBoundsException`·입력 유래 `IllegalArgumentException`, 20,001px logical canvas, 40MP 초과 후속 frame 4건과 character-scoped `ORIGINAL_WORK`, `UNASSIGN``creatorMemberId=null` audit 2건이 기존 구현에서 예상한 이유로 실패했다. 빈 multipart JSON, 실제 legacy 검색 query, multipart downstream 전달 보강은 기존 동작을 고정하는 characterization test로 추가했다.
- Phase 0 baseline: `./gradlew test --tests kr.co.vividnext.sodalive.content.AudioContentUploadCompletionContractTest --tests kr.co.vividnext.sodalive.admin.chat.character.AdminChatCharacterControllerTest --tests kr.co.vividnext.sodalive.admin.chat.original.AdminOriginalWorkControllerContractTest --tests kr.co.vividnext.sodalive.chat.original.controller.OriginalWorkControllerContractTest --tests kr.co.vividnext.sodalive.v2.api.creator.channel.series.adapter.in.web.CreatorChannelSeriesControllerTest --tests kr.co.vividnext.sodalive.v2.api.creator.channel.community.adapter.in.web.CreatorChannelCommunityControllerTest --tests kr.co.vividnext.sodalive.v2.api.creator.channel.fantalk.adapter.in.web.CreatorChannelFanTalkControllerTest` 통과.
- Phase 1 GREEN: `./gradlew test --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.application.AdminPagePolicyTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AdminJsonRequestParserTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AdminImagePartValidatorTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AiCharacterAdminSecurityIntegrationTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AiCharacterAdminLoginJwtIntegrationTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AiCharacterAdminExceptionHandlerTest --tests kr.co.vividnext.sodalive.v2.aicharacter.application.AiCharacterAdminTargetResolverTest --tests kr.co.vividnext.sodalive.v2.aicharacter.adapter.out.persistence.DefaultAiCharacterPersistenceAdapterTest --tests kr.co.vividnext.sodalive.v2.common.application.AfterCommitExecutorTest --tests kr.co.vividnext.sodalive.v2.common.application.AfterCommitEventBoundaryIntegrationTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.application.AiCharacterAdminAuditLoggerTest` 통과. 이 명령의 `AdminImagePartValidatorTest`는 10MB 초과 거부와 JPEG/PNG/GIF 실제 MIME·GIF 허용 조건을 고정한다.
- Phase 0+1 통합 targeted: 위 Phase 0/1 대상 테스트 전체를 한 Gradle 명령으로 실행해 통과.
- 리뷰 보강 targeted: `./gradlew test --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AiCharacterAdminExceptionHandlerTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AiCharacterAdminSecurityIntegrationTest --tests kr.co.vividnext.sodalive.v2.aicharacter.application.AiCharacterAdminTargetResolverTest --tests kr.co.vividnext.sodalive.v2.common.application.AfterCommitEventBoundaryIntegrationTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.application.AiCharacterAdminAuditLoggerTest --tests kr.co.vividnext.sodalive.admin.chat.character.AdminChatCharacterControllerTest --tests kr.co.vividnext.sodalive.admin.chat.original.AdminOriginalWorkControllerContractTest` 통과. 이 보강은 legacy 캐릭터·원작 관리자 mutation 성공 `ApiResponse` surface, `/admin/ai-characters` segment 경계, exception handler status 행렬, target resolver 404/409 행렬, 기존 event listener AFTER_COMMIT 경계, audit `PIN` action을 포함한다.
- 리뷰 2차 보강 targeted: `./gradlew test --tests 'kr.co.vividnext.sodalive.admin.member.AdminMemberLoginServiceTest' --tests 'kr.co.vividnext.sodalive.admin.chat.character.AdminChatCharacterControllerTest' --tests 'kr.co.vividnext.sodalive.admin.chat.original.AdminOriginalWorkControllerContractTest' --tests 'kr.co.vividnext.sodalive.v2.common.application.AfterCommitEventBoundaryIntegrationTest'` 통과. 이 보강은 `/admin/member/login`이 생성한 token의 기존 `TokenProvider` 검증·인증 복원, legacy 캐릭터·원작 mutation의 `data` 부재 계약, 일반 event publish의 commit/rollback transactional listener 경계를 포함한다.
- 리뷰 3차 보강 targeted: `./gradlew test --tests kr.co.vividnext.sodalive.v2.common.application.AfterCommitEventBoundaryIntegrationTest`, `./gradlew test --tests kr.co.vividnext.sodalive.content.AudioContentUploadCompletionContractTest`, `./gradlew test --tests kr.co.vividnext.sodalive.chat.original.controller.OriginalWorkControllerContractTest --tests kr.co.vividnext.sodalive.admin.chat.character.AdminChatCharacterControllerTest --tests kr.co.vividnext.sodalive.admin.chat.original.AdminOriginalWorkControllerContractTest`, `./gradlew test --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AiCharacterAdminLoginJwtIntegrationTest` 통과. 이 보강은 실제 FCM event listener의 commit, 언어 감지 listener의 commit/rollback, `waitTransactionCommit=true` 언어 번역 listener의 commit과 세 listener의 `AFTER_COMMIT` 선언, upload-complete production `SecurityConfig`의 ADMIN/BOT JWT 허용·USER 403·익명 401, 일반 원작·legacy 원작·legacy 캐릭터 응답 field, CONTENT_MANAGER 실제 로그인 JWT의 신규 관리자 API 403을 포함한다.
- Task 1.4 retry characterization targeted: `./gradlew test --tests kr.co.vividnext.sodalive.v2.common.application.AfterCommitExecutorTest --tests kr.co.vividnext.sodalive.v2.common.application.AfterCommitEventBoundaryIntegrationTest` 통과. 실제 `TransactionTemplate`에서 첫 attempt rollback 후 동일 command를 재시도해 commit하면 direct callback은 두 attempt 합계 1회 실행됐고, 새 테스트가 production `AfterCommitExecutor` 변경 없이 통과해 현 구현의 회귀 계약으로 기록했다.
- 리뷰 4차 수정 targeted: `./gradlew --no-daemon test --tests kr.co.vividnext.sodalive.chat.original.controller.OriginalWorkControllerContractTest --tests kr.co.vividnext.sodalive.admin.chat.character.AdminChatCharacterControllerTest --tests kr.co.vividnext.sodalive.admin.chat.original.AdminOriginalWorkControllerContractTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AdminJsonRequestParserTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AdminImagePartValidatorTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.application.AiCharacterAdminAuditLoggerTest --tests kr.co.vividnext.sodalive.v2.common.application.AfterCommitExecutorTest --tests kr.co.vividnext.sodalive.v2.common.application.AfterCommitEventBoundaryIntegrationTest` 통과. production 원작 공개 matcher, legacy 캐릭터·원작 검색과 캐릭터 multipart 전체 wire, trailing JSON 거부, 이미지 decode·dimension·pixel 경계, audit action/resource/creator 조합과 public `copy` 부재, rollback 후 direct callback 재시도를 함께 검증했다.
- 리뷰 5차 수정 targeted: `./gradlew --no-daemon test --tests kr.co.vividnext.sodalive.chat.original.controller.OriginalWorkControllerContractTest --tests kr.co.vividnext.sodalive.admin.chat.character.AdminChatCharacterControllerTest --tests kr.co.vividnext.sodalive.admin.chat.original.AdminOriginalWorkControllerContractTest --tests kr.co.vividnext.sodalive.legacy.LegacyAdminSearchQueryContractTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AdminJsonRequestParserTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AdminImagePartValidatorTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.application.AiCharacterAdminAuditLoggerTest --tests kr.co.vividnext.sodalive.v2.common.application.AfterCommitExecutorTest --tests kr.co.vividnext.sodalive.v2.common.application.AfterCommitEventBoundaryIntegrationTest` 통과. GIF logical canvas·모든 frame header와 malformed decoder 경계, audit global/character creator 불변식, 실제 JPA legacy 검색 field·상태·정렬·pagination, 캐릭터 외부 API exact method/path/JSON 및 원작 update DTO 전달을 함께 검증했다.
- 리뷰 6차 RED 확인: `./gradlew --no-daemon test --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AdminImagePartValidatorTest`에서 첫 frame은 정상이지만 LZW 데이터가 손상된 두 번째 frame을 기존 구현이 decode하지 않아 예외가 발생하지 않았고, 16건 중 신규 테스트 1건이 `AssertionFailedError`로 실패했다.
- 리뷰 6차 수정 targeted: `./gradlew --no-daemon test --tests kr.co.vividnext.sodalive.content.AudioContentUploadCompletionContractTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AdminImagePartValidatorTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.application.AiCharacterAdminAuditLoggerTest` 통과. 33건의 실패·오류가 0건이며, GIF 모든 frame의 1x1 decode, callback의 `ADMIN`/`BOT` 성공·`USER` 403·유효하지 않은 JWT와 익명 401, 해제 audit factory의 nullable 경계를 함께 검증했다.
- Full regression: `./gradlew --no-daemon test` 통과. 리뷰 6차 GIF 구현 후 재실행도 `BUILD SUCCESSFUL in 4m 26s`로 통과했다.
- Lint: `./gradlew --no-daemon ktlintCheck` 통과. 리뷰 6차 최종 Kotlin 변경 후 재실행도 `BUILD SUCCESSFUL in 26s`로 통과했다.
- 리뷰 7차 RED 확인: `./gradlew --no-daemon test --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AdminImagePartValidatorTest`에서 PNG ancillary chunk와 GIF extension payload가 ImageIO custom provider까지 도달해 신규 preflight 테스트 2건이 실패했다. `./gradlew --no-daemon test --tests kr.co.vividnext.sodalive.legacy.LegacySodaExceptionHttpStatusContractTest`는 legacy malformed multipart 메시지 계약 변경을 신규 테스트로 고정했다. Phase 1 누락 테스트는 hard-coded ID 전달, missing character, `creatorMember=null`, JsonNode missing-key 경계를 보강했다.
- 리뷰 7차 수정 targeted: `./gradlew --no-daemon test --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AdminImagePartValidatorTest`, `./gradlew --no-daemon test --tests kr.co.vividnext.sodalive.legacy.LegacySodaExceptionHttpStatusContractTest`, `./gradlew --no-daemon test --tests kr.co.vividnext.sodalive.v2.aicharacter.application.AiCharacterAdminTargetResolverTest --tests kr.co.vividnext.sodalive.v2.aicharacter.adapter.out.persistence.DefaultAiCharacterPersistenceAdapterTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AdminJsonRequestParserTest`가 각각 통과했다. 이어서 위 5개 테스트 class를 한 Gradle 명령으로 묶어 재실행해 `BUILD SUCCESSFUL in 1m 2s`를 확인했다.
- 리뷰 7차 최종 검증: reviewer gate의 PNG length overflow와 정상 metadata 오탐 지적을 반영한 뒤 `./gradlew --no-daemon test --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AdminImagePartValidatorTest``BUILD SUCCESSFUL in 30s`, `./gradlew --no-daemon ktlintCheck``BUILD SUCCESSFUL in 23s`, `./gradlew --no-daemon test``BUILD SUCCESSFUL in 7m 14s`로 통과했다. LSP는 기존과 동일하게 현재 환경에 `kotlin-ls`가 설치되어 있지 않아 실행하지 못했다.
- 리뷰 8차 RED 확인: PNG metadata reader flag, GIF extension 개수·sub-block 개수·누적 frame pixels·초과 LZW 출력 5건이 기존 구현에서 ImageIO 전에 거부되지 않아 `AdminImagePartValidatorTest`의 신규 assertion으로 실패했다.
- 리뷰 8차 수정 targeted: `./gradlew --no-daemon test --tests kr.co.vividnext.sodalive.content.AudioContentUploadCompletionContractTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AdminImagePartValidatorTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AiCharacterAdminSecurityIntegrationTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AiCharacterAdminLoginJwtIntegrationTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AiCharacterAdminExceptionHandlerTest --tests kr.co.vividnext.sodalive.legacy.LegacySodaExceptionHttpStatusContractTest --tests kr.co.vividnext.sodalive.admin.member.AdminMemberLoginServiceTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.application.AdminPagePolicyTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AdminJsonRequestParserTest --tests kr.co.vividnext.sodalive.v2.aicharacter.application.AiCharacterAdminTargetResolverTest --tests kr.co.vividnext.sodalive.v2.aicharacter.adapter.out.persistence.DefaultAiCharacterPersistenceAdapterTest --tests kr.co.vividnext.sodalive.v2.common.application.AfterCommitExecutorTest --tests kr.co.vividnext.sodalive.v2.common.application.AfterCommitEventBoundaryIntegrationTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.application.AiCharacterAdminAuditLoggerTest``BUILD SUCCESSFUL in 4m 51s`로 통과했다. 정확한 PNG ancillary/chunk, GIF frame/extension/sub-block/cumulative pixel 및 LZW 조기 종료·연속 clear·EOI 뒤 data 경계와 실제 Spring Boot DispatcherServlet multipart pre-handler admin 400/legacy 200 계약을 포함한다.
- 리뷰 8차 lint: `./gradlew --no-daemon ktlintCheck``BUILD SUCCESSFUL in 1m 3s`로 통과했다.
- 리뷰 8차 full regression: `./gradlew --no-daemon test``BUILD SUCCESSFUL in 14m 7s`로 통과했다.
- LSP: 현재 환경에 `kotlin-ls`가 설치되어 있지 않아 `lsp_diagnostics`는 실행하지 못했다.
- DB schema migration: 없음.
- 구현 기준 commit/기존 dirty·untracked manifest: 구현 시작 시 기록.
- 구현 기준 commit/기존 dirty·untracked manifest: 시작 기준 commit `5b700892`, 기존 dirty/untracked 없음.
- staging worker/Trigger E2E: 아직 실행하지 않음.
- production 배포: 이 계획의 코드 작성 단계만으로 완료 처리하지 않음.
@@ -986,4 +1016,6 @@ Phase 0 기존 callback·소비자 계약 고정
- 원작 보강 후 계약 검증: PRD의 JSON code block 51개와 Frontend delta의 label JSON 값 20개(Operation Request/Response 12개, 공통 오류 3개, nullable 값 5개)가 모두 유효하다. PRD와 Frontend delta의 원작 `(Operation ID, Method, Path)`는 8/8, Operation Request/Response JSON의 key·value·배열·null 구조는 12/12 exact equality다. 원작 DTO nullable·배정 응답 의미, legacy mutation 5개와 캐릭터 mutation 2개의 v2 수렴, 잠금·보상·오류 경계는 PRD와 plan에서 일치한다.
- 원작 보강 후 Markdown/DB 검증: 세 문서의 code fence 수가 각각 146개, 2개, 2개로 짝이 맞고 trailing whitespace·tab과 SQL 파일은 0건이다.
- 원작 보강 후 Gradle 구성 검증: `./gradlew tasks --all` 실행 결과 `BUILD SUCCESSFUL`이다.
- 리뷰 6차 문서·Gradle 구성 검증: `./gradlew --no-daemon tasks --all``BUILD SUCCESSFUL in 7s`, `git diff --check`와 세 문서의 trailing whitespace·tab 검사는 출력 0건이다. code fence 수는 각각 146개, 2개, 2개로 짝이 맞고 callback 전체 Response를 호환 대상으로 표현하는 잔여 문구도 0건이다.
- 리뷰 8차 문서 검증: Task 1.1/1.2 checkbox를 완료로 갱신했고 `git diff --check`, 세 문서의 trailing whitespace·tab 및 code fence 짝 검사를 실행해 이상 없음을 확인했다. `./gradlew tasks --all`은 기존 검증 결과를 유지하며, 이번 재실행은 Gradle wrapper lock 파일 권한 오류로 실행하지 못했다.
- 미실행 외부 Gate: 구현 전 문서 검증 단계이므로 실제 운영 데이터의 삭제 원작 연결 건수, MySQL 8 동시성, staging worker/Trigger E2E와 production 배포는 실행하지 않았다.