# AI 캐릭터 관리자 기능 Implementation Plan > **For agentic workers:** REQUIRED SUB-SKILL: Use `superpowers:subagent-driven-development` (recommended) or `superpowers:executing-plans` to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking. **Goal:** 기존 관리자 로그인, legacy 원작 API와 AWS upload-complete 계약을 유지하면서 `ROLE_ADMIN` 전용 AI 캐릭터 관리자 API 66개를 v2 도메인 경계 안에 구현하고, 원작 관리와 AI 캐릭터의 비동기 콘텐츠·커뮤니티 운영을 안전하게 제공한다. **Architecture:** 신규 web adapter와 DTO는 `kr.co.vividnext.sodalive.v2.admin.aicharacter`가 소유하고, 업무 규칙은 `v2.aicharacter`, `v2.originalwork`, `v2.content.*`, `v2.creator.channel.*`의 각 도메인이 소유한다. admin facade는 실제 `ADMIN` principal의 ID를 유지하고, character-scoped 요청에서만 Path의 `characterId`를 `AiCharacterAdminTarget`으로 해석한다. global 원작 facade는 원작 use case를 직접 조정하고 배정·해제에서만 캐릭터 집합을 검증한다. 기존 JPA entity와 QueryDSL Q type은 v2 persistence adapter에서만 사용하고, 기존 Service·Repository·web DTO는 신규 v2 업무 로직에서 호출하지 않는다. **Tech Stack:** Kotlin, Java 17, Spring Boot 2.7.14, Spring Security, Spring Data JPA, QueryDSL, AWS S3/CloudFront infrastructure client, JUnit 5, MockMvc, Gradle Wrapper, ktlint --- ## 0. 구현 범위와 고정 결정 - 기준 문서: `docs/20260720_AI캐릭터_관리자기능/prd.md` - 이 계획은 Backend 구현만 다룬다. PRD 27장의 Frontend handoff는 클라이언트 구현 입력이며 Backend 완료 조건에 포함하지 않는다. - 기존 `POST /admin/member/login`을 그대로 사용한다. 신규 로그인, 사칭 토큰, menu/capability Endpoint를 만들지 않는다. - 메뉴는 클라이언트의 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 설정은 생성·수정하지 않는다. - v2 전용 예약 공개 scheduler를 만들지 않는다. 기존 scheduler component의 cron·lock은 유지하고 활성 creator만 선택하도록 기존 release query의 안전 조건만 보강한다. - 이번 기능을 위한 DB 테이블·컬럼·인덱스·JPA mapping, DDL, backfill과 데이터 migration을 생성·수정하지 않는다. - 콘텐츠 `status`는 기존 `isActive`, `releaseDate`, `duration`과 creator 활성 상태에서 계산한다. `content` 경로는 Signed URL 발급 전 canonical output key 검증에만 사용한다. - 캐릭터 삭제 시 소유 콘텐츠의 기존 raw `isActive`만 `false`로 전환하고 `releaseDate`, `content`, `duration`과 구매 이력은 보존한다. 이는 기존 컬럼의 논리 상태 변경이며 schema·JPA mapping 변경이 아니다. - 캐릭터 삭제가 실제 commit된 경우에만 `default`, `cache_ttl_3_days`, `popularCharacters_24h_locale` cache를 1회 clear한다. 동적 cache key 열거, Redis key scan과 범용 cache invalidation framework는 만들지 않는다. - `previewStartTime`, `previewEndTime`은 생성 Request에서 기존 S3 metadata로만 전달하고 DB와 목록·상세 Response에는 저장하지 않는다. - 신규 v2 domain/application/port는 v2 외부의 legacy Controller, Service, Repository, Request/Response DTO를 호출하지 않는다. - 원작은 `v2.originalwork`가 소유하고 기존 `OriginalWork` 관련 entity와 `ChatCharacter.originalWork` 관계는 persistence adapter에서만 사용한다. 원작용 table·column·index·JPA mapping·DDL·backfill·data migration을 만들지 않는다. - 원작 삭제는 연결 캐릭터가 0명일 때만 `isDeleted=true`로 전환한다. 배정·해제는 전건 검증 후 한 transaction에서 처리하며 누락 ID를 무시하는 부분 성공을 허용하지 않는다. - legacy 원작 등록·수정·삭제·배정·해제는 원작 호환 adapter가 같은 v2 원작 input port를 호출한다. legacy 캐릭터 등록·수정 전체는 캐릭터 호환 adapter가 같은 v2 캐릭터 command를 호출하고 해당 command가 v2 원작 참조·잠금 정책을 사용한다. v2는 legacy Controller·Service·DTO를 참조하지 않으며 legacy 조회 API는 기존 구현을 유지한다. - 원작 관계 mutation은 양수 연결 대상 또는 Path 원작 row가 있으면 해당 원작을 `PESSIMISTIC_WRITE`로 먼저 잠그고 캐릭터 row를 ID 오름차순으로 잠근 뒤 재검증한다. `CHAR-04`의 `null`과 legacy 수정의 `0` 해제는 대상 원작이 없으므로 캐릭터 row만 잠그고 재검증한다. 원작 생성과 제목이 실제 바뀌는 수정은 MySQL `SERIALIZABLE` transaction과 deadlock·serialization 실패 시 최대 1회 새 transaction 재시도로 동시성 불변식을 지킨다. - 이미 삭제된 원작의 반복 DELETE는 연결 수보다 먼저 판정해 성공한다. 배포 전 삭제 원작 연결 불일치가 1건 이상이면 자동 migration을 만들지 않고 별도 승인된 보정을 완료하기 전 mutation 전환을 활성화하지 않는다. - 원작 이미지 port는 저장과 해당 request가 새로 만든 object 삭제를 지원한다. 원작 command에도 서버 생성 `requestId`를 사용한다. 이미지 저장 자체의 실패는 502다. 이미지 저장 후 재시도 가능한 DB 실패는 해당 attempt object 삭제 보상이 성공한 뒤 최대 1회 새 transaction으로 재시도한다. 재조회에서 실제 중복이 확인되면 409이고, 비재시도 DB 실패·재시도 소진·보상 실패는 500이다. - 신규 `CHAR-03`의 `originalWorkId=null`은 미연결, `CHAR-04`의 명시적 `null`은 해제이며 신규 계약은 `0` sentinel을 허용하지 않는다. legacy 호환 adapter는 기존 등록의 `0`을 미연결로, 수정의 `0`만 해제로 변환한다. - legacy 캐릭터 등록의 `originalWorkId=null`과 `0`은 미연결, 수정의 `null`은 변경 없음, `0`은 해제다. 호환 adapter는 이 네 경우를 v2 command의 명시적 의미로 변환한다. - 캐릭터 삭제가 기존 공개 화면에 반영되도록 legacy 소비자 query와 cache에 필요한 active guard만 최소 보강한다. 이는 v2 업무 로직에서 legacy Service·Repository를 호출하거나 재사용하는 것이 아니다. - 공통 JWT, `ApiResponse`, 기존 JPA entity/Q type, S3·CloudFront·외부 캐릭터 client는 PRD가 허용한 adapter 경계 뒤에서만 재사용한다. - 기존 소비자용 v2 시리즈·커뮤니티·FanTalk query는 마스킹과 소비자 필터가 있어 관리자 조회에 직접 재사용하지 않는다. 같은 v2 도메인 아래 관리자 전용 projection과 use case를 추가한다. - 독립 리소스는 논리 삭제한다. 시리즈-콘텐츠와 Member-creator tag 연결 해제만 join row 물리 삭제가 가능하고, `CategoryContent`는 기존 `isActive` 모델을 유지한다. - 모든 절대 날짜·시간 응답은 UTC `Z`로 직렬화한다. duration과 preview offset `HH:mm:ss`는 timezone 변환 대상이 아니다. - 시간 의존 서비스는 생성자에 `Clock`을 받고 운영 기본값은 `Clock.systemUTC()`로 둔다. 테스트는 fixed `Clock`을 사용한다. - 알림·번역처럼 기존 `@TransactionalEventListener(AFTER_COMMIT)`가 처리하는 event는 transaction 안에서 publish해 listener가 commit 후 실행하게 한다. home news처럼 transactional listener를 거치지 않는 직접 호출만 `AfterCommitExecutor`에 등록한다. 외부 캐릭터 API/S3는 commit 전 실행하되 실패·rollback·commit 예외를 보상한다. 어느 경로든 rollback, 동일 상태 전이 재시도, 멱등 삭제 재시도에서 외부 효과를 중복 실행하지 않는다. - 순수 policy는 JUnit unit test, application은 outbound port fake/mock test, persistence는 `@DataJpaTest`와 기존 `QueryDslConfig` 패턴, HTTP 계약은 `@SpringBootTest`+MockMvc로 검증한다. Redis가 실제 경로에 필요한 경우에만 기존 embedded Redis fixture를 사용한다. ## 1. 성공 기준과 Operation 추적 ### 1.1 Operation coverage | 구현 Task | Operation ID | 수 | |---|---|---:| | Task 2.4 | `CHAR-01`~`CHAR-04` | 4 | | Task 2.7 | `ORIGINAL-WORK-01`~`ORIGINAL-WORK-08` | 8 | | Task 10.2 | `CHAR-05` | 1 | | Task 4.3 | `CONTENT-01`~`CONTENT-07` | 7 | | Task 5.2 | `CONTENT-COMMENT-01`~`CONTENT-COMMENT-05` | 5 | | Task 6.2 | `CATEGORY-01`~`CATEGORY-09` | 9 | | Task 7.2 | `SERIES-01`~`SERIES-11` | 11 | | Task 8.2 | `COMMUNITY-POST-01`~`COMMUNITY-POST-06` | 6 | | Task 8.4 | `COMMUNITY-COMMENT-01`~`COMMUNITY-COMMENT-05` | 5 | | Task 9.2 | `FAN-TALK-01`~`FAN-TALK-05` | 5 | | Task 9.4 | `NOTICE-01`~`NOTICE-02`, `CREATOR-TAG-01`, `CHANNEL-PROFILE-01`~`CHANNEL-PROFILE-02` | 5 | | 합계 | 신규 관리자 Operation | 66 | ### 1.2 공통 완료 조건 - 모든 신규 Controller가 class level `@PreAuthorize("hasRole('ADMIN')")`를 사용한다. - 비로그인은 401, ADMIN 외 인증 사용자는 403이며 두 경우 모두 `ApiResponse` 오류 JSON을 반환한다. - 400/404/409/500/502 오류가 PRD 20장의 `errorProperty`와 HTTP status를 따른다. - page는 음수이면 0, size는 1 미만이면 20, 50 초과이면 50으로 정규화한다. 기존 소비자용 page 정책은 변경하지 않는다. - character-scoped 작업은 `AiCharacterAdminTarget` 해석 후 각 리소스 소유권을 다시 검증한다. - global 원작 CRUD는 character target 없이 수행하고, 배정·해제만 Request의 전체 AI 캐릭터 집합과 현재 원작 귀속을 검증한다. - 목록·상세·mutation JSON field, nullable, 기본값, 정렬, UTC 규칙은 PRD 9장과 각 Operation의 Request/Response JSON을 그대로 따른다. - `CONTENT-01`, `CONTENT-02`의 Signed URL과 만료 시각은 같은 절대 `Instant`를 사용하고 `Cache-Control: private, no-store`를 반환한다. - 사람 관리자 mutation은 `adminMemberId`, `characterId`, `creatorMemberId`, `action`, `resourceType`, `resourceId`, `result`를 구조화 로그에 남긴다. global 원작 CRUD의 두 캐릭터 field는 `null`이고 배정·해제는 캐릭터별 로그를 남긴다. - 신규 v2 domain/application/port에서 legacy Service·Repository·web DTO와 JPA entity/Q type import가 검출되지 않는다. - 기존 로그인, 메뉴, callback, 예약 공개 scheduler와 소비자용 v2 API의 회귀 테스트가 통과한다. ### 1.3 구현 전 기술 Gate 다음은 제품·API·UX 미결정 사항이 아니라 외부 시스템의 실제 지원 범위를 확인하는 구현 선행 Gate다. 1. Weraser 외부 캐릭터 API의 create/update idempotency 전달 방식, 생성 결과 식별자, nullable field 삭제·빈 목록 전체 삭제 표현, remote 현재값 조회, 보상 update 지원 여부를 운영 계약 또는 실제 client 문서와 대조한다. 2. 지원하지 않는 기능을 임의 header나 가짜 delete 호출로 만들지 않는다. PRD의 보상 요구를 충족할 수 없으면 코드 작성 전에 PRD에 지원 가능한 보상·divergence 처리 경계를 명시하고 승인받는다. 3. 외부 캐릭터 레코드를 물리 삭제하거나 `inactive_*`로 rename하지 않는다. 4. 실제 worker/Trigger 코드는 이 저장소 밖에 있으므로 staging에서 v2 생성 input object가 기존 callback으로 완료되는 E2E를 배포 전 1회 확인한다. S3 저장과 DB commit 사이에 callback이 도착할 때 worker가 조회 실패를 retry하는지도 확인하며, 이 검증을 위해 worker 코드·설정이나 새 callback을 만들지는 않는다. 5. 배포 전 운영 데이터에서 `isDeleted=true` 원작을 참조하는 캐릭터 건수를 읽기 전용으로 확인한다. 0건이면 mutation 전환을 진행한다. 1건 이상이면 자동 해제·backfill을 추가하지 않고 대상과 영향 범위를 보고해 별도 승인된 데이터 보정을 완료한 뒤 mutation 전환을 활성화한다. 6. H2 test만으로 MySQL 잠금 의미를 대신하지 않는다. 배포 전 MySQL 8 환경의 서로 다른 connection에서 동일 제목 생성·수정, 원작 삭제 대 배정·해제·CHAR-03/04를 동시에 실행해 중복 활성 제목과 삭제 원작 참조가 모두 0건인지 확인한다. 실패하면 schema 변경 없이 안전하다고 간주하지 않고 배포를 중단해 PRD를 재검토한다. ## 2. 파일 구조 계획 ### 2.1 공통 관리자 계약 - 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` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/application/AiCharacterAdminAuditLogger.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AdminJsonRequestParser.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AdminImagePartValidator.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/main/kotlin/kr/co/vividnext/sodalive/v2/common/application/AfterCommitExecutor.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/common/SodaException.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt` ### 2.2 AI 캐릭터 - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/domain/AiCharacterAdminTarget.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/domain/AiCharacterModels.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/domain/AiCharacterPolicy.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/application/AiCharacterQueryService.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/application/AiCharacterCommandService.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/application/AiCharacterAdminTargetResolver.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/port/out/AiCharacterPersistencePort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/port/out/AiCharacterExternalPort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/port/out/AiCharacterImageStoragePort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/port/out/AiCharacterEventPort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/adapter/out/persistence/DefaultAiCharacterPersistenceAdapter.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/adapter/out/external/WeraserAiCharacterAdapter.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/adapter/out/storage/S3AiCharacterImageStorageAdapter.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/adapter/out/event/AiCharacterEventAdapter.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/adapter/out/cache/AiCharacterVisibilityCacheInvalidator.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/dto/AdminAiCharacterDtos.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/application/AdminAiCharacterFacade.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AdminAiCharacterController.kt` ### 2.3 원작 - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/originalwork/domain/OriginalWorkAdminModels.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/originalwork/domain/OriginalWorkAdminPolicy.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/originalwork/application/OriginalWorkAdminService.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/originalwork/port/in/OriginalWorkAdminUseCase.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/originalwork/port/out/OriginalWorkAdminPort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/originalwork/port/out/OriginalWorkImageStoragePort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/originalwork/port/out/OriginalWorkEventPort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/originalwork/adapter/out/persistence/DefaultOriginalWorkAdminRepository.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/originalwork/adapter/out/storage/S3OriginalWorkImageStorageAdapter.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/originalwork/adapter/out/event/OriginalWorkEventAdapter.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/dto/AdminOriginalWorkDtos.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/application/AdminOriginalWorkFacade.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AdminOriginalWorkController.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/original/LegacyOriginalWorkMutationAdapter.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/LegacyAiCharacterMutationAdapter.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/original/AdminOriginalWorkController.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/original/service/AdminOriginalWorkService.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/AdminChatCharacterController.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/osiv/OsivLazyLoadingRegressionTest.kt` ### 2.4 콘텐츠와 기존 callback·scheduler 호환 - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/management/domain/ContentModels.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/management/domain/ContentPolicy.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/management/domain/ContentStatusPolicy.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/management/domain/ContentOutputKeyPolicy.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/management/application/ContentManagementService.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/management/port/in/ContentCreatorDeactivationUseCase.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/management/port/out/ContentManagementPort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/management/port/out/ContentFileStoragePort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/management/port/out/SignedAudioUrlPort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/management/port/out/ContentEventPort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/management/adapter/out/persistence/DefaultContentManagementRepository.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/management/adapter/out/storage/S3ContentFileStorageAdapter.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/management/adapter/out/cloudfront/CloudFrontSignedAudioUrlAdapter.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/management/adapter/out/event/ContentEventAdapter.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/dto/AdminAiContentDtos.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/application/AdminAiContentFacade.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AdminAiContentController.kt` - Verify: `src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentController.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt` - Verify: `src/main/kotlin/kr/co/vividnext/sodalive/scheduler/AudioContentReleaseScheduledTask.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/aws/cloudfront/AudioContentCloudFront.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/aws/s3/S3Uploader.kt` ### 2.5 나머지 creator 작업 도메인 - Content comment: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/comment` - Content category: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/category` - Series admin extension: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/series` - Community admin extension: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community` - FanTalk admin extension: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk` - Channel notice: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/notice` - Channel profile: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/profile` - 위 패키지의 정확한 파일은 해당 Phase의 Task에 명시한다. ## 3. 구현·배포 순서 ```text Phase 0 기존 callback·소비자 계약 고정 -> Phase 1 공통 계약/보안/대상 해석 -> Phase 2 CHAR-01~04와 ORIGINAL-WORK-01~08 -> Phase 3 계산 status와 기존 callback/scheduler 안전 조건 -> Phase 4~9 콘텐츠·하위 도메인 53개 Operation -> Phase 10 CHAR-05 삭제 cascade/legacy 호환 -> Phase 11 전체 계약/경계/회귀 검증 ``` 운영 DB schema 선행 작업은 없다. 배포 diff에서 `content`·원작 관련 테이블 DDL, JPA 컬럼·관계 mapping, V1/V2 dispatcher와 신규 scheduler가 추가되지 않았는지 확인한다. ### 3.1 TDD 공통 실행 규칙 - 각 Task의 `실패 확인` 명령은 production 변경 전에 실행해 신규 기대가 실패하는지 확인한다. - 구현 시작 전에 기준 commit hash와 이미 존재하는 dirty/untracked file manifest를 5장에 기록한다. Task 11.3의 금지 변경 검증은 이 기준 이후 이번 구현이 만든 diff만 판정한다. - `GREEN` 구현 직후에는 같은 명령을 다시 실행해 `BUILD SUCCESSFUL`을 확인한다. 이 재실행은 각 Task의 GREEN 검증 항목이며 실제 결과를 5장에 기록한다. - `REFACTOR` 후에도 같은 명령과 Task에 명시된 인접 회귀 test를 다시 실행한다. - `TDD 예외` Task는 명시한 대체 검증을 실행하고 의도적인 실패를 만들지 않는다. - Gradle test는 kapt 임시 파일 충돌을 피하도록 문서 순서대로 실행하며 서로 병렬 실행하지 않는다. --- ### Phase 0: 기존 callback·소비자 계약 고정 - [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.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: 공통 관리자 계약, 보안, 대상 해석 - [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` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AdminJsonRequestParser.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AdminImagePartValidator.kt` - 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` 구분, 빈 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를 검증하고, 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가 하나의 명시적 계약을 사용하고, image part는 bytes·container 구조·metadata·frame·LZW 작업량의 bounded 안전 경계를 공유한다. - [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` - 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는 영향받지 않는다. - [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` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/port/out/AiCharacterPersistencePort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/adapter/out/persistence/DefaultAiCharacterPersistenceAdapter.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/application/AiCharacterAdminTargetResolverTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/adapter/out/persistence/DefaultAiCharacterPersistenceAdapterTest.kt` - RED: 존재하지 않는 character 404, 연결 Member 없음/`role!=CREATOR`/`memberKind!=AI_CHARACTER` 404, 생성·수정 시 둘 중 하나 비활성 409, 삭제·상태 조회 시 비활성 target 반환을 검증한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.aicharacter.application.AiCharacterAdminTargetResolverTest --tests kr.co.vividnext.sodalive.v2.aicharacter.adapter.out.persistence.DefaultAiCharacterPersistenceAdapterTest` - 통과 확인: GREEN 구현 후 위 실패 확인 명령을 다시 실행해 `BUILD SUCCESSFUL`을 확인한다. - GREEN: PRD의 여섯 필드만 갖는 `AiCharacterAdminTarget`을 반환하고, web body에는 `creatorId`/writer ID를 추가하지 않는다. - REFACTOR: JPA `ChatCharacter`, `Member`, Q type은 persistence adapter 밖으로 노출하지 않는다. - 기대 결과: 모든 character-scoped facade가 동일한 대상 해석 결과를 사용한다. - [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 미실행을 검증하고, 실제 `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는 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 원칙을 반복 구현하지 않는다. ### Phase 2: AI 캐릭터와 원작 관리 (`CHAR-01`~`CHAR-04`, `ORIGINAL-WORK-01`~`ORIGINAL-WORK-08`) - [ ] **Task 2.1: 캐릭터 policy, 관리자 projection과 persistence command 구현** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/domain/AiCharacterModels.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/domain/AiCharacterPolicy.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/port/out/AiCharacterPersistencePort.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/adapter/out/persistence/DefaultAiCharacterPersistenceAdapter.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/domain/AiCharacterPolicyTest.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/adapter/out/persistence/DefaultAiCharacterPersistenceAdapterTest.kt` - RED: characterType, age, ISO region, 양수 원작 ID의 `isDeleted=false` 검증, 생성 `null` 미연결, 수정 명시적 `null` 해제와 `0` 거부, 중첩 문자열 trim, 이름 예약/중복, 목록 검색·상태·정렬, 상세의 systemPrompt 위치, character와 AI creator Member의 unique 1:1 동시 저장·동기화를 검증한다. 신규 Member는 `role=CREATOR`, `memberKind=AI_CHARACTER`, `email=null`, `password=""`이고 일반·크리에이터 관리자 로그인 대상이 아님을 고정한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.aicharacter.domain.AiCharacterPolicyTest --tests kr.co.vividnext.sodalive.v2.aicharacter.adapter.out.persistence.DefaultAiCharacterPersistenceAdapterTest` - 통과 확인: GREEN 구현 후 위 실패 확인 명령을 다시 실행해 `BUILD SUCCESSFUL`을 확인한다. - GREEN: 관리자 전용 domain model/projection과 v2 persistence query/command를 구현한다. 캐릭터 생성 시 `role=CREATOR`, `memberKind=AI_CHARACTER` Member를 연결하고 수정 시 nickname/profileImage/introduce를 동기화한다. - REFACTOR: legacy `AdminChatCharacterService`, repository와 DTO를 주입하지 않고 기존 entity mapping만 adapter에서 사용한다. - 기대 결과: 외부 시스템과 web 계층 없이도 캐릭터 데이터 규칙이 고정된다. - [ ] **Task 2.2: 외부 캐릭터·이미지 port와 보상 경계 구현** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/port/out/AiCharacterExternalPort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/port/out/AiCharacterImageStoragePort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/adapter/out/external/WeraserAiCharacterAdapter.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/adapter/out/storage/S3AiCharacterImageStorageAdapter.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/aws/s3/S3Uploader.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/adapter/out/external/WeraserAiCharacterAdapterTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/adapter/out/storage/S3AiCharacterImageStorageAdapterTest.kt` - RED: 기술 Gate에서 확인한 create/update/idempotency 계약, 검증 완료 이미지의 upload/delete 보상, timeout/4xx/5xx의 502 매핑을 먼저 고정한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.aicharacter.adapter.out.external.WeraserAiCharacterAdapterTest --tests kr.co.vividnext.sodalive.v2.aicharacter.adapter.out.storage.S3AiCharacterImageStorageAdapterTest` - 통과 확인: GREEN 구현 후 위 실패 확인 명령을 다시 실행해 `BUILD SUCCESSFUL`을 확인한다. - GREEN: command별 `requestId`를 두 port에 전달하고, 외부/파일 생성 후 DB 실패 시 지원되는 보상을 실행한다. 보상 미지원·실패는 request/resource/stage만 orphan/divergence 로그에 남긴다. - REFACTOR: 외부 API key, bucket, 원본 prompt나 파일을 로그에 기록하지 않고 기존 설정 주입 방식을 유지한다. - 기대 결과: 외부 실패가 부분 성공으로 반환되지 않고 재처리 근거가 남는다. - [ ] **Task 2.3: 캐릭터 query/create/update service와 side effect 구현** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/application/AiCharacterQueryService.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/application/AiCharacterCommandService.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/port/out/AiCharacterEventPort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/adapter/out/event/AiCharacterEventAdapter.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/application/AiCharacterQueryServiceTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/application/AiCharacterCommandServiceTest.kt` - RED: 생성의 외부 create→로컬 character/Member flush로 ID 확보→`characters/{characterId}` 이미지 저장→DB commit 각 단계 실패와 commit 예외 보상, 수정 remote 성공 후 DB 실패의 compensating update/divergence를 검증한다. 수정에서는 nullable field·`originalWorkId`의 명시적 null과 목록의 `[]`가 local/remote 양쪽에서 실제 clear로 표현되는지, 양수 원작의 `isDeleted=false` 검증과 `0` 거부, 이미지 생략 유지, region 변경 불가, 비활성 수정 409, 등록 description 언어 감지와 수정 번역의 commit 후 1회 실행도 검증한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.aicharacter.application.AiCharacterQueryServiceTest --tests kr.co.vividnext.sodalive.v2.aicharacter.application.AiCharacterCommandServiceTest` - 통과 확인: GREEN 구현 후 위 실패 확인 명령을 다시 실행해 `BUILD SUCCESSFUL`을 확인한다. - GREEN: query/create/update orchestration만 구현하고 delete cascade는 하위 도메인 deactivation port가 준비되는 Phase 10까지 보류한다. 외부·파일 작업이 포함된 command는 `TransactionTemplate` 경계 밖에서 commit 예외까지 포착해 생성 리소스 삭제 또는 remote compensating update를 실행한다. - REFACTOR: requestId 생성, 보상 순서, after-commit event 발행을 작은 private 함수로만 분리하고 범용 workflow engine을 만들지 않는다. - 기대 결과: CHAR-01~04의 업무 흐름이 web DTO와 분리되어 검증된다. - [ ] **Task 2.4: CHAR-01~04 DTO, facade, Controller contract 구현** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/dto/AdminAiCharacterDtos.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/application/AdminAiCharacterFacade.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AdminAiCharacterController.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AdminAiCharacterControllerIntegrationTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/AiCharacterAdminRouteInventoryTest.kt` - RED: PRD 11장의 CHAR-01/02 JSON 전체 field·nullable·UTC, CHAR-03/04 multipart part 이름·필수 key·기본값과 이미지 실제 MIME/GIF 거부, 목록에 systemPrompt 미노출, 생성된 AI Member의 일반·크리에이터 관리자 로그인 거부, ADMIN 인가, 400/404/409/502, audit success/failure를 MockMvc로 고정한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AdminAiCharacterControllerIntegrationTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.AiCharacterAdminRouteInventoryTest` - 통과 확인: GREEN 구현 후 위 실패 확인 명령을 다시 실행해 `BUILD SUCCESSFUL`을 확인한다. - GREEN: Controller는 JSON key와 image bytes를 외부 호출 전에 검증하고 principal에서 실제 `adminMemberId`만 추출해 facade에 전달한다. facade는 target 해석과 use case 호출/응답 변환/audit만 담당한다. - REFACTOR: multipart `request`는 `AdminJsonRequestParser`를 사용하고 DTO에 `creatorId`, `characterId`, `isActive` writable field를 추가하지 않는다. route inventory는 이 Task에서 구현한 CHAR-01~04만 실제 mapping과 대조하고 이후 Phase가 자기 Operation을 누적한다. - 기대 결과: CHAR-01~04 mapping이 route inventory에서 통과하고 PRD JSON으로 호출 가능하다. - [ ] **Task 2.5: 원작 policy, 관리자 projection과 기존 mapping 기반 persistence 구현** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/originalwork/domain/OriginalWorkAdminModels.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/originalwork/domain/OriginalWorkAdminPolicy.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/originalwork/port/out/OriginalWorkAdminPort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/originalwork/adapter/out/persistence/DefaultOriginalWorkAdminRepository.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/originalwork/domain/OriginalWorkAdminPolicyTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/originalwork/adapter/out/persistence/DefaultOriginalWorkAdminRepositoryTest.kt` - RED: 필수 문자열 trim·빈 값, nullable 빈 문자열의 null 정규화, http/https 링크, 링크·태그 첫 등장 순서 보존과 중복 제거, trim·대소문자 무시 생성/수정 제목 충돌과 현재 ID 제외, 수정 11개 key 전체 교체를 검증한다. link ID·tag-mapping ID 명시 정렬과 요청 순서 재생성, `isDeleted=false` 목록·대소문자 무시 검색·`createdAt DESC,id DESC`·공통 page, 상세, creator 누락을 포함한 활성·비활성 연결 캐릭터 목록과 전체 `ChatCharacter` 기준 `characterCount`, 연결 0건 삭제·연결 존재 409·이미 삭제된 상태 우선의 반복 삭제도 기존 entity와 관계를 사용한 persistence test로 고정한다. 양수 target/path 원작 `PESSIMISTIC_WRITE`와 캐릭터 ID 오름차순 잠금 query, null 해제의 캐릭터 단독 잠금도 test에서 확인한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.originalwork.domain.OriginalWorkAdminPolicyTest --tests kr.co.vividnext.sodalive.v2.originalwork.adapter.out.persistence.DefaultOriginalWorkAdminRepositoryTest` - 통과 확인: GREEN 구현 후 위 실패 확인 명령을 다시 실행해 `BUILD SUCCESSFUL`을 확인한다. - GREEN: v2 원작 command/query record와 policy를 만들고 adapter가 `EntityManager`·QueryDSL로 기존 `OriginalWork`, link, tag와 `ChatCharacter.originalWork`를 읽고 쓴다. 양수 연결 대상 또는 Path 원작이 있는 relation mutation은 원작을 먼저, 캐릭터를 ID 오름차순으로 잠그고 잠금 뒤 상태·귀속을 재검증한다. `CHAR-04`의 `null`과 legacy 수정의 `0` 해제는 캐릭터만 잠그고 재검증한다. domain/application에는 legacy entity·Q type을 반환하지 않는다. - REFACTOR: legacy `OriginalWorkRepository`, `OriginalWorkTagRepository`, `ChatCharacterRepository`를 주입하지 않고 새 `@Entity`, relation mapping, DDL 또는 data migration을 만들지 않는다. - 기대 결과: 원작 CRUD·검색·연결 조회의 데이터 규칙이 legacy service 없이 고정된다. - [ ] **Task 2.6: 원작 application, 이미지·언어 event와 원자적 캐릭터 배정 구현** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/originalwork/application/OriginalWorkAdminService.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/originalwork/port/in/OriginalWorkAdminUseCase.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/originalwork/port/out/OriginalWorkImageStoragePort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/originalwork/port/out/OriginalWorkEventPort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/originalwork/adapter/out/storage/S3OriginalWorkImageStorageAdapter.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/originalwork/adapter/out/event/OriginalWorkEventAdapter.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/aws/s3/S3Uploader.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/application/AiCharacterCommandService.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/originalwork/application/OriginalWorkAdminServiceTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/originalwork/adapter/out/storage/S3OriginalWorkImageStorageAdapterTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/aws/s3/S3UploaderTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/originalwork/adapter/out/event/OriginalWorkEventAdapterTest.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/application/AiCharacterCommandServiceTest.kt` - RED: 생성 DB·`originals/{originalWorkId}/...` 이미지 저장·commit 각 실패의 새 이미지 보상, 수정 이미지 생략 유지와 실패 보상, 기존 이미지 보존, 삭제 시 이미지 미삭제를 검증한다. storage port의 `store(originalWorkId, validatedImage, requestId, attemptNumber)`와 `delete(objectKey)`, 서버 생성 `requestId`, attempt별 고유 key, 저장 실패 502, 저장 후 비재시도 DB 실패와 보상 성공·실패의 500 및 구조화 ERROR orphan 로그를 구분한다. 첫 attempt가 이미지 저장 후 commit deadlock이면 첫 key를 삭제한 뒤에만 두 번째 `REQUIRES_NEW` `SERIALIZABLE` transaction을 시작하고, 두 번째 성공 후 S3에는 최종 key 하나만 남아야 한다. 첫 key 보상 실패 시 재시도하지 않으며, retry 뒤 실제 중복만 409이고 재시도 소진은 500인지 검증한다. 생성 언어 감지와 정규화된 `title`, `contentType`, `category`, `description`, `tags`가 실제 바뀐 수정의 번역 갱신은 commit 후 1회, 다른 field 변경·rollback·동일 값 수정·배정·해제는 0회여야 하며 감지 결과 commit 전 후속 번역이 시작되지 않아야 한다. 배정은 중복 없는 전건 활성 AI 캐릭터 검증 후 다른 원작에서 이동하고 같은 원작 반복은 멱등해야 하며, 해제는 creator 상태와 무관하게 기존 캐릭터의 Path 원작 귀속을 전건 검증한다. 배정의 누락·비활성 ID 또는 해제의 누락·미연결·다른 원작 ID가 있으면 부분 변경 0건인지 확인한다. CHAR-03/04도 같은 `isDeleted=false`, 신규 null/`0` 규칙 및 relation lock을 사용하는지 고정한다. concurrent 배정과 삭제, 연결 변경과 삭제를 latch 기반 통합 test로 교차 실행해 삭제 원작 참조가 0건인지 검증하고, 동시 동일 제목 생성·수정의 transaction isolation과 재시도 정책도 확인한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.originalwork.application.OriginalWorkAdminServiceTest --tests kr.co.vividnext.sodalive.v2.originalwork.adapter.out.storage.S3OriginalWorkImageStorageAdapterTest --tests kr.co.vividnext.sodalive.v2.originalwork.adapter.out.event.OriginalWorkEventAdapterTest --tests kr.co.vividnext.sodalive.v2.aicharacter.application.AiCharacterCommandServiceTest --tests kr.co.vividnext.sodalive.aws.s3.S3UploaderTest` - 통과 확인: GREEN 구현 후 위 실패 확인 명령을 다시 실행해 `BUILD SUCCESSFUL`을 확인한다. - GREEN: 원작 CRUD와 배정·해제 transaction을 `OriginalWorkAdminService`와 input port에 구현한다. image port에는 `originalWorkId`, 검증된 image, requestId와 attempt number를 전달하고 반환된 exact object key를 attempt별로 기록한다. 비transactional orchestration은 외부 이미지 저장을 포함한 각 DB attempt를 하나의 `REQUIRES_NEW` `TransactionTemplate.execute`로 감싸 commit 예외까지 포착하며 outer REQUIRED transaction이나 중첩 template을 만들지 않는다. 생성과 제목이 실제 바뀌는 수정에만 `SERIALIZABLE` isolation을 적용하고, 제목이 바뀌지 않는 수정은 기본 isolation을 사용한다. retry 가능한 실패는 해당 attempt key 보상 성공 후에만 새 transaction으로 최대 한 번 재시도한다. 재조회에서 실제 중복이 확인되면 409로, 비재시도·재시도 소진 또는 보상 실패는 500으로 끝낸다. 언어 event는 원작 transaction commit 후 시작하고 캐릭터 command는 같은 v2 원작 참조·잠금 규칙을 사용한다. - REFACTOR: legacy 원작 Service/Repository/DTO, Controller의 S3 직접 호출, v2 public/domain command의 `0` sentinel, 부분 성공과 범용 workflow engine을 도입하지 않는다. legacy adapter의 기존 등록 `0 -> 미연결`, 수정 `0 -> 해제` 변환은 호환 경계에만 둔다. 공용 `S3Uploader` 확장은 정확한 bucket·object key를 받는 `delete` 한 메서드로 제한하고 존재 확인·prefix 삭제·정리 scheduler를 추가하지 않는다. - 기대 결과: 이미지·DB·언어 side effect와 캐릭터 관계 변경이 명시적인 실패·rollback 계약을 갖는다. - [ ] **Task 2.7: ORIGINAL-WORK-01~08 DTO, facade, Controller contract 구현** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/dto/AdminOriginalWorkDtos.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/application/AdminOriginalWorkFacade.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AdminOriginalWorkController.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/original/LegacyOriginalWorkMutationAdapter.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/original/AdminOriginalWorkController.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/original/service/AdminOriginalWorkService.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AdminOriginalWorkControllerIntegrationTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/admin/chat/original/LegacyOriginalWorkMutationAdapterTest.kt` - 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도 검증한다. 해제 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를 맞춘다. - REFACTOR: 신규 v2에 별도 `/search`, legacy와 같은 `/register`, `/update`, `/assign-characters`, `/unassign-characters`, 원작 복구 또는 공개 원작 Endpoint를 추가하지 않는다. v2가 legacy adapter나 DTO를 import하지 않고 legacy Controller가 신규 Controller를 호출하지 않는다. - 기대 결과: CHAR-01~04와 ORIGINAL-WORK-01~08의 12개 mapping이 route inventory를 통과하고 PRD JSON으로 호출 가능하며, legacy 원작 5개 mutation도 외부 성공 계약을 유지한 채 같은 불변식을 적용한다. ### Phase 3: 계산 status와 기존 callback·scheduler 안전 조건 - [ ] **Task 3.1: 기존 컬럼 기반 content status와 Signed output key 정책 구현** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/management/domain/ContentStatusPolicy.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/management/domain/ContentOutputKeyPolicy.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/content/management/domain/ContentStatusPolicyTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/content/management/domain/ContentOutputKeyPolicyTest.kt` - RED: `!isActive && releaseDate==null -> DELETED`, creator 비활성 `SUSPENDED`, active `PUBLISHED`, duration null `PROCESSING`, 가공 완료·미래 공개 `SCHEDULED`, 나머지 `SUSPENDED` 우선순위와 fixed UTC `Clock`을 표 기반 test로 고정한다. output key는 정확한 `output/{contentId}/...`만 허용하고 URI scheme, host, 선행 slash/backslash, query/fragment, percent encoding, 빈 segment, `.`/`..`, 다른 content ID와 input/raw/preview를 거부하는지 검증한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.content.management.domain.ContentStatusPolicyTest --tests kr.co.vividnext.sodalive.v2.content.management.domain.ContentOutputKeyPolicyTest` - 통과 확인: GREEN 구현 후 위 실패 확인 명령을 다시 실행해 `BUILD SUCCESSFUL`을 확인한다. - GREEN: DB에 status를 저장하지 않는 순수 `ContentStatusPolicy`와 Signed URL 발급 직전에만 호출하는 `ContentOutputKeyPolicy`를 구현한다. - REFACTOR: 계산 status와 output key 검증을 결합하거나 key를 정규화해 통과시키지 않는다. 한 요청의 목록 필터와 Response는 같은 `now`를 사용한다. - 기대 결과: 신규 컬럼 없이 PRD 12.2의 status와 Signed URL 안전 조건을 재현한다. - [ ] **Task 3.2: 기존 callback·예약 공개의 삭제 및 비활성 creator guard 보강** - Files: - Verify: `src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentController.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt` - Verify: `src/main/kotlin/kr/co/vividnext/sodalive/scheduler/AudioContentReleaseScheduledTask.kt` - 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/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의 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를 다시 공개하지 않는다. ### Phase 4: 콘텐츠 관리 API (`CONTENT-01`~`CONTENT-07`) - [ ] **Task 4.1: 콘텐츠 생성·수정·삭제·고정 domain policy와 persistence 구현** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/management/domain/ContentModels.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/management/domain/ContentPolicy.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/management/application/ContentManagementService.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/management/port/out/ContentManagementPort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/management/port/out/ContentEventPort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/management/adapter/out/persistence/DefaultContentManagementRepository.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/management/adapter/out/event/ContentEventAdapter.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/content/management/domain/ContentPolicyTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/content/management/application/ContentManagementServiceTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/content/management/adapter/out/persistence/DefaultContentManagementRepositoryTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/content/management/adapter/out/event/ContentEventAdapterTest.kt` - RED: title/detail, price 0/1~4/5, 테마 12~14, purchaseOption, limited, preview pair/15초, 무료 preview, rental/full-detail 파생 규칙, 수정 불가 field, 기존 유료 무료 전환 금지, 소유권, 계산 status 전체의 삭제, 공개·최대 3개 pin 교체와 생성·수정 언어 작업의 commit/rollback/retry를 테스트한다. 생성은 `isActive=false`, `duration=null`, `releaseDate=request 값 또는 fixed Clock now`이고 status 필터와 Response 계산이 Task 3.1 policy와 일치하는지도 persistence test로 고정한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.content.management.domain.ContentPolicyTest --tests kr.co.vividnext.sodalive.v2.content.management.application.ContentManagementServiceTest --tests kr.co.vividnext.sodalive.v2.content.management.adapter.out.persistence.DefaultContentManagementRepositoryTest --tests kr.co.vividnext.sodalive.v2.content.management.adapter.out.event.ContentEventAdapterTest` - 통과 확인: GREEN 구현 후 위 실패 확인 명령을 다시 실행해 `BUILD SUCCESSFUL`을 확인한다. - GREEN: 생성은 기존 `content` row에 `isActive=false`, non-null releaseDate와 input path를 저장하고, 삭제는 `isActive=false`, `releaseDate=null`로 기록한다. status는 저장하지 않고 Task 3.1 policy로 계산하며 pin은 조건부 update/가장 오래된 pin 교체로 구현한다. 생성·수정은 언어 코드가 없으면 감지, 있으면 번역 작업을 commit 후 1회 예약한다. - REFACTOR: legacy `CreatorAdminContentService`, `AudioContentRepository`, theme service를 호출하지 않고 persistence adapter의 자체 query로 소유권과 기준정보를 검증한다. - 기대 결과: CONTENT command 규칙과 원자성이 web/storage와 독립적으로 고정된다. - [ ] **Task 4.2: 콘텐츠 S3 업로드, Signed URL과 UTC 만료 계약 구현** - Files: - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/management/application/ContentManagementService.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/content/management/application/ContentManagementServiceTest.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/management/port/out/ContentFileStoragePort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/management/port/out/SignedAudioUrlPort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/management/adapter/out/storage/S3ContentFileStorageAdapter.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/management/adapter/out/cloudfront/CloudFrontSignedAudioUrlAdapter.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/aws/cloudfront/AudioContentCloudFront.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/aws/s3/S3Uploader.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/content/management/adapter/out/storage/S3ContentFileStorageAdapterTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/content/management/adapter/out/cloudfront/CloudFrontSignedAudioUrlAdapterTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/aws/cloudfront/AudioContentCloudFrontTest.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/content/AudioContentUploadCompletionContractTest.kt` - RED: 빈 audio 거부, 검증 완료 cover와 `input/{contentId}/{contentId}-content-...` key, 기존 `generateFileName(prefix = "${contentId}-content")` basename 규칙과 그 basename을 유지한 output key가 callback의 content ID 검증을 통과하는지 검증한다. `generate_preview` 및 선택 preview metadata만 전달되고 새 pipeline/callback metadata는 추가되지 않는지, preview 값이 DB projection이나 Response에 저장되지 않는지, 부분 업로드 보상, `(duration HH + 2)시간`, fixed Clock의 URL policy/응답 expiresAt 동일성, 서명 실패 500/no fallback도 검증한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.content.management.adapter.out.storage.S3ContentFileStorageAdapterTest --tests kr.co.vividnext.sodalive.v2.content.management.adapter.out.cloudfront.CloudFrontSignedAudioUrlAdapterTest --tests kr.co.vividnext.sodalive.aws.cloudfront.AudioContentCloudFrontTest --tests kr.co.vividnext.sodalive.content.AudioContentUploadCompletionContractTest` - 통과 확인: GREEN 구현 후 위 실패 확인 명령을 다시 실행해 `BUILD SUCCESSFUL`을 확인한다. - GREEN: `AudioContentCloudFront`에 절대 `expiresAt: Instant` overload를 추가하고 기존 상대 TTL 함수를 유지한다. `ContentManagementService.create`는 `TransactionTemplate` 안에서 content ID를 만든 뒤 기존 filename 생성 규칙으로 `input/{contentId}/{contentId}-content-...` 업로드를 수행하고, execute/commit 예외를 바깥에서 포착해 DB rollback과 이미 생성된 object 보상을 실행한다. query service는 한 번 얻은 `now`에서 expiresAt을 계산해 port와 응답에 같은 값을 쓴다. - REFACTOR: 기존 callback이 저장한 canonical output key와 duration은 query에서 읽기만 하고 생성된 Signed URL과 expiresAt은 domain/persistence에 저장하지 않는다. cover만 공통 CDN 절대 URL 변환을 사용한다. - 기대 결과: 계산 status가 SCHEDULED/PUBLISHED이고 canonical key+duration이 있을 때만 Signed URL을 받고 raw path는 노출되지 않는다. - [ ] **Task 4.3: CONTENT-01~07 DTO, facade, Controller contract 구현** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/dto/AdminAiContentDtos.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/application/AdminAiContentFacade.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AdminAiContentController.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AdminAiContentControllerIntegrationTest.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/AiCharacterAdminRouteInventoryTest.kt` - RED: CONTENT-01 검색/계산 status/유효 isActive 필터/정렬/전체 field와 `previewStartTime`, `previewEndTime` 미포함, CONTENT-02 재조회 URL 갱신과 두 preview field 미포함, 01/02 `private,no-store`, 03 multipart preview 입력·metadata/default/계산 PROCESSING과 cover 실제 MIME/GIF 거부, 04 immutable field와 선택 cover 검증, 05 delete, 06 pin, 07 비페이징 active theme JSON과 모든 오류/audit를 MockMvc로 고정한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AdminAiContentControllerIntegrationTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.AiCharacterAdminRouteInventoryTest` - 통과 확인: GREEN 구현 후 위 실패 확인 명령을 다시 실행해 `BUILD SUCCESSFUL`을 확인한다. - GREEN: facade가 target을 해석하고 콘텐츠 use case를 호출하며 DTO는 PRD 12장의 실제 JSON 이름과 nullability를 그대로 반환한다. status와 유효 isActive는 기존 필드와 creator 활성 상태로 계산하고 preview offset은 Response DTO에 두지 않는다. 파일 저장과 보상 orchestration은 `ContentManagementService`가 소유한다. - REFACTOR: 목록 query에서 구매·성인 선호·차단 마스킹을 적용하지 않고, 브라우저용 URL refresh Endpoint를 추가하지 않는다. - 기대 결과: CONTENT-01~07이 route inventory와 클라이언트 JSON 계약을 통과한다. ### Phase 5: 콘텐츠 댓글 API (`CONTENT-COMMENT-01`~`CONTENT-COMMENT-05`) - [ ] **Task 5.1: 콘텐츠 댓글·답글 domain, persistence와 side effect 구현** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/comment/domain/ContentCommentModels.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/comment/domain/ContentCommentPolicy.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/comment/application/ContentCommentManagementService.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/comment/port/out/ContentCommentManagementPort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/comment/port/out/ContentCommentEventPort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/comment/adapter/out/persistence/DefaultContentCommentManagementRepository.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/comment/adapter/out/event/ContentCommentEventAdapter.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/content/comment/domain/ContentCommentPolicyTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/content/comment/application/ContentCommentManagementServiceTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/content/comment/adapter/out/persistence/DefaultContentCommentManagementRepositoryTest.kt` - RED: 루트/직접 답글 정렬, 동일 콘텐츠의 활성 루트에만 답글, 답글의 답글 거부, AI 작성자만 수정, 작성자 또는 콘텐츠 소유자의 삭제, 다른 owner의 Path 콘텐츠·댓글은 404, body의 부모 미존재·다른 콘텐츠 귀속·중첩 답글은 400과 `parentCommentId`, 반복 삭제, 필터 값과 무관한 활성 직접 replyCount를 검증한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.content.comment.domain.ContentCommentPolicyTest --tests kr.co.vividnext.sodalive.v2.content.comment.application.ContentCommentManagementServiceTest --tests kr.co.vividnext.sodalive.v2.content.comment.adapter.out.persistence.DefaultContentCommentManagementRepositoryTest` - 통과 확인: GREEN 구현 후 위 실패 확인 명령을 다시 실행해 `BUILD SUCCESSFUL`을 확인한다. - GREEN: 댓글 writer는 body가 아니라 resolved `creatorMemberId`로 고정하고, 콘텐츠 알림과 언어 감지는 생성 transaction commit 후에만 발행한다. - REFACTOR: legacy `AudioContentCommentService/Repository`를 호출하지 않고 entity/Q type은 persistence adapter에서만 변환한다. - 기대 결과: 본문 수정 권한과 소유자 moderation 권한이 서로 분리된다. - [ ] **Task 5.2: CONTENT-COMMENT-01~05 DTO, facade, Controller contract 구현** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/dto/AdminContentCommentDtos.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/application/AdminContentCommentFacade.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AdminContentCommentController.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AdminContentCommentControllerIntegrationTest.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/AiCharacterAdminRouteInventoryTest.kt` - RED: 두 목록의 root/direct-child 범위와 정렬, Query `isActive` 생략 시 true 및 false 명시 조회, PRD 13장의 response JSON, create 기본값, update body에 content만 허용, 타 작성자 update 404/403 대신 소유권 은닉 404, 소유 콘텐츠 타인 댓글 delete 성공과 audit를 MockMvc로 검증한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AdminContentCommentControllerIntegrationTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.AiCharacterAdminRouteInventoryTest` - 통과 확인: GREEN 구현 후 위 실패 확인 명령을 다시 실행해 `BUILD SUCCESSFUL`을 확인한다. - GREEN: facade가 character/content/comment 귀속을 순서대로 검증하고 공통 `AdminCommentUpdateRequest`와 mutation response를 매핑한다. - REFACTOR: client가 writer/creator ID를 주입할 수 있는 field를 만들지 않는다. - 기대 결과: CONTENT-COMMENT-01~05가 route inventory를 통과한다. ### Phase 6: 콘텐츠 카테고리 API (`CATEGORY-01`~`CATEGORY-09`) - [ ] **Task 6.1: 콘텐츠 카테고리 domain, 구성·순서 persistence와 event 구현** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/category/domain/ContentCategoryModels.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/category/domain/ContentCategoryPolicy.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/category/application/ContentCategoryManagementService.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/category/port/in/ContentCategoryCreatorDeactivationUseCase.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/category/port/out/ContentCategoryManagementPort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/category/port/out/ContentCategoryEventPort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/category/adapter/out/persistence/DefaultContentCategoryManagementRepository.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/category/adapter/out/event/ContentCategoryEventAdapter.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/content/category/domain/ContentCategoryPolicyTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/content/category/application/ContentCategoryManagementServiceTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/content/category/adapter/out/persistence/DefaultContentCategoryManagementRepositoryTest.kt` - RED: title trim/2자/활성 중복, create contentIds 전건 동일 소유·활성·중복 없음, 활성 카테고리 전체 order 집합, 포함/가용 목록 정렬, add 전건 rollback, inactive link 재활성화, active 중복 409, 반복 remove `[]`, category delete 시 link 비활성화를 검증한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.content.category.domain.ContentCategoryPolicyTest --tests kr.co.vividnext.sodalive.v2.content.category.application.ContentCategoryManagementServiceTest --tests kr.co.vividnext.sodalive.v2.content.category.adapter.out.persistence.DefaultContentCategoryManagementRepositoryTest` - 통과 확인: GREEN 구현 후 위 실패 확인 명령을 다시 실행해 `BUILD SUCCESSFUL`을 확인한다. - GREEN: category와 `CategoryContent`의 기존 테이블을 adapter에서 사용하고 create/title update의 언어 감지·번역을 commit 후 1회 발행한다. - REFACTOR: category-content는 물리 삭제하지 않고 기존 `isActive` 및 `orders` 의미를 유지한다. - 기대 결과: 카테고리 mutation과 구성 변경이 전건 검증 후 하나의 transaction으로 수행된다. - [ ] **Task 6.2: CATEGORY-01~09 DTO, facade, Controller contract 구현** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/dto/AdminContentCategoryDtos.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/application/AdminContentCategoryFacade.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AdminContentCategoryController.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AdminContentCategoryControllerIntegrationTest.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/AiCharacterAdminRouteInventoryTest.kt` - RED: CATEGORY-01 page/filter/order/count, 02~04 mutation JSON, 05 정확한 전체 순서, 06/07 포함·가용 page, 08/09 affected IDs와 오류/audit를 PRD 18.1~18.7 JSON으로 검증한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AdminContentCategoryControllerIntegrationTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.AiCharacterAdminRouteInventoryTest` - 통과 확인: GREEN 구현 후 위 실패 확인 명령을 다시 실행해 `BUILD SUCCESSFUL`을 확인한다. - GREEN: target의 creator ID만 use case에 넘기고 모든 ID 집합 오류를 요청 전체 실패로 매핑한다. - REFACTOR: 콘텐츠 응답에 raw cover path를 노출하지 않고 공통 CDN absolute URL만 사용한다. - 기대 결과: CATEGORY-01~09가 route inventory를 통과한다. ### Phase 7: 시리즈 API (`SERIES-01`~`SERIES-11`) - [ ] **Task 7.1: 관리자 시리즈 domain, persistence, image와 event 구현** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/series/domain/CreatorSeriesAdminModels.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/series/domain/CreatorSeriesAdminPolicy.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/series/application/CreatorSeriesAdminService.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/series/port/in/SeriesCreatorDeactivationUseCase.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/series/port/out/CreatorSeriesAdminPort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/series/port/out/SeriesCoverStoragePort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/series/port/out/CreatorSeriesEventPort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/series/adapter/out/persistence/DefaultCreatorSeriesAdminRepository.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/series/adapter/out/storage/S3SeriesCoverStorageAdapter.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/series/adapter/out/event/CreatorSeriesEventAdapter.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/series/domain/CreatorSeriesAdminPolicyTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/series/application/CreatorSeriesAdminServiceTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/series/adapter/out/persistence/DefaultCreatorSeriesAdminRepositoryTest.kt` - RED: 요일 허용값/빈 배열/RANDOM 단독, 활성 genre, update nullable writer/studio와 필수 key, 상태, 동일 소유권, 검증 완료 이미지의 저장 보상, 포함·가용 정렬, add 전건 rollback과 기존 활성 연결 중복 409, join row 물리 제거/반복 remove, 활성 시리즈 전체 order 집합, 언어 event commit 후 1회를 검증한다. SERIES-11 장르 응답에는 PRD에 없는 순서 assertion을 추가하지 않는다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.series.domain.CreatorSeriesAdminPolicyTest --tests kr.co.vividnext.sodalive.v2.creator.channel.series.application.CreatorSeriesAdminServiceTest --tests kr.co.vividnext.sodalive.v2.creator.channel.series.adapter.out.persistence.DefaultCreatorSeriesAdminRepositoryTest` - 통과 확인: GREEN 구현 후 위 실패 확인 명령을 다시 실행해 `BUILD SUCCESSFUL`을 확인한다. - GREEN: 기존 `Series`, `SeriesContent`, genre 테이블을 persistence adapter에서 사용하고 관리자 projection을 별도로 구현한다. - REFACTOR: 기존 `CreatorChannelSeriesQueryService`와 소비자 DTO는 수정하지 않고, 공통화가 정확히 일치하는 CDN 변환 외에는 관리자 query에 끌어오지 않는다. - 기대 결과: 관리자 시리즈 계약이 소비자 필터/마스킹과 독립적으로 동작한다. - [ ] **Task 7.2: SERIES-01~11 DTO, facade, Controller contract 구현** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/dto/AdminSeriesDtos.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/application/AdminSeriesFacade.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AdminSeriesController.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AdminSeriesControllerIntegrationTest.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/AiCharacterAdminRouteInventoryTest.kt` - RED: SERIES-01의 search/isActive/state 필터와 `order ASC, seriesId ASC`, SERIES-01/02 field·nullable·UTC, SERIES-03 문자열 trim/nonblank·`isAdult=false`·nullable writer/studio 기본값, 03/04 multipart·누락/null 구분과 이미지 실제 MIME/GIF 거부, 05 logical delete 후 포함 content 보존, 06/07 page/정렬, 08 기존 활성 연결 중복 409와 08/09 affected IDs, 10 전체 order, 11 비페이징 genre와 오류/audit를 MockMvc로 고정한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AdminSeriesControllerIntegrationTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.AiCharacterAdminRouteInventoryTest` - 통과 확인: GREEN 구현 후 위 실패 확인 명령을 다시 실행해 `BUILD SUCCESSFUL`을 확인한다. - GREEN: PRD 14장의 method/path/request/response를 그대로 mapping하고 image part 생략 시 기존 이미지를 유지한다. - REFACTOR: PRD가 요일 response 배열과 genre metadata의 순서를 정의하지 않았으므로 특정 순서 assertion을 만들지 않고 값의 보존만 검증한다. - 기대 결과: SERIES-01~11이 route inventory를 통과한다. ### Phase 8: 커뮤니티 게시글·댓글 API - [ ] **Task 8.1: 관리자 커뮤니티 게시글 domain, persistence, file과 event 구현** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/domain/CreatorCommunityAdminModels.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/domain/CreatorCommunityAdminPolicy.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/application/CreatorCommunityAdminPostService.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/port/in/CommunityCreatorDeactivationUseCase.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/port/out/CreatorCommunityAdminPort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/port/out/CreatorCommunityFilePort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/port/out/CreatorCommunityEventPort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/adapter/out/persistence/DefaultCreatorCommunityAdminRepository.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/adapter/out/storage/S3CreatorCommunityFileAdapter.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/adapter/out/event/CreatorCommunityEventAdapter.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/domain/CreatorCommunityAdminPolicyTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/application/CreatorCommunityAdminPostServiceTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/adapter/out/persistence/DefaultCreatorCommunityAdminRepositoryTest.kt` - RED: 전체 본문 관리자 projection, fixed/created 정렬, content trim 후 nonblank, price 기본 0과 0 이상, 유료 또는 audio 첨부 시 image 필수, update의 price/audio 불변, image 유지, delete 소유권과 하위 댓글 row 보존, 활성 게시글만 fixed, 캐릭터별 최대 3개 및 네 번째 고정은 자동 교체 없이 409, 검증 완료 파일의 부분 업로드 보상, 구독자 알림과 무료 home news commit 후 1회를 검증한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.community.domain.CreatorCommunityAdminPolicyTest --tests kr.co.vividnext.sodalive.v2.creator.channel.community.application.CreatorCommunityAdminPostServiceTest --tests kr.co.vividnext.sodalive.v2.creator.channel.community.adapter.out.persistence.DefaultCreatorCommunityAdminRepositoryTest` - 통과 확인: GREEN 구현 후 위 실패 확인 명령을 다시 실행해 `BUILD SUCCESSFUL`을 확인한다. - GREEN: 게시글 persistence/storage/event port와 관리자 전용 service를 구현한다. storage adapter는 web 경계에서 검증된 file만 받아 저장하고 DB commit 실패 시 생성 object를 보상한다. - REFACTOR: 단일 커뮤니티 audio 검증을 전역 media framework로 일반화하지 않고 게시글 web/storage 경계에 둔다. - 기대 결과: 유료 본문도 축약·마스킹 없이 관리자에게 반환되고 업로드 실패가 부분 성공을 남기지 않는다. - [ ] **Task 8.2: COMMUNITY-POST-01~06 DTO, facade, Controller contract 구현** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/dto/AdminCommunityPostDtos.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/application/AdminCommunityPostFacade.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AdminCommunityAudioPartValidator.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AdminCommunityPostController.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AdminCommunityAudioPartValidatorTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AdminCommunityPostControllerIntegrationTest.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/AiCharacterAdminRouteInventoryTest.kt` - RED: POST-01/02 전체 JSON과 nullable/UTC, 03 multipart 조합·기본값, audio bytes의 실제 MIME `audio/mp4|audio/x-m4a|audio/aac`, 무료 이미지 GIF 거부와 유료 이미지 GIF 허용, 04 허용 field만 수정, 05 logical delete, 06 fixed와 ADMIN/owner/error/audit를 PRD 15장 기준으로 검증한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AdminCommunityAudioPartValidatorTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AdminCommunityPostControllerIntegrationTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.AiCharacterAdminRouteInventoryTest` - 통과 확인: GREEN 구현 후 위 실패 확인 명령을 다시 실행해 `BUILD SUCCESSFUL`을 확인한다. - GREEN: web adapter가 Tika 기반으로 file bytes를 검사하고 확장자만 신뢰하지 않는다. 이미지는 공통 `AdminImagePartValidator`에 유료 여부를 전달한다. facade는 target과 post 소유권을 검증한 뒤 service를 호출하고 CDN URL만 응답한다. - REFACTOR: 기존 소비자용 `CreatorChannelCommunityQueryService`의 유료 마스킹과 차단 조건을 관리자 응답에 적용하지 않는다. - 기대 결과: COMMUNITY-POST-01~06이 route inventory를 통과한다. - [ ] **Task 8.3: 관리자 커뮤니티 댓글 domain과 persistence 구현** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/application/CreatorCommunityAdminCommentService.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/domain/CreatorCommunityAdminModels.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/domain/CreatorCommunityAdminPolicy.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/port/out/CreatorCommunityAdminPort.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/adapter/out/persistence/DefaultCreatorCommunityAdminRepository.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/domain/CreatorCommunityAdminPolicyTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/application/CreatorCommunityAdminCommentServiceTest.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/adapter/out/persistence/DefaultCreatorCommunityAdminRepositoryTest.kt` - RED: 루트/직접 답글 정렬, 동일 활성 post의 활성 root만 parent 허용, 중첩 답글 거부, AI 작성자만 update, 작성자 또는 post 소유자 delete, secret/작성자 정보, 필터 값과 무관한 활성 직접 replyCount, 다른 owner의 Path post·comment는 404, body 부모 귀속·중첩 우회는 400과 `parentCommentId`를 검증한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.community.domain.CreatorCommunityAdminPolicyTest --tests kr.co.vividnext.sodalive.v2.creator.channel.community.application.CreatorCommunityAdminCommentServiceTest --tests kr.co.vividnext.sodalive.v2.creator.channel.community.adapter.out.persistence.DefaultCreatorCommunityAdminRepositoryTest` - 통과 확인: GREEN 구현 후 위 실패 확인 명령을 다시 실행해 `BUILD SUCCESSFUL`을 확인한다. - GREEN: resolved creator ID를 writer로 사용하고 post/comment/root 귀속을 persistence에서 한 번 더 확인한다. - REFACTOR: 콘텐츠 댓글과 규칙이 비슷해도 서로 다른 entity/알림 계약을 범용 comment engine으로 합치지 않는다. - 기대 결과: community comment mutation 권한이 PRD 19장의 순서로 검증된다. - [ ] **Task 8.4: COMMUNITY-COMMENT-01~05 DTO, facade, Controller contract 구현** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/dto/AdminCommunityCommentDtos.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/application/AdminCommunityCommentFacade.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AdminCommunityCommentController.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AdminCommunityCommentControllerIntegrationTest.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/AiCharacterAdminRouteInventoryTest.kt` - RED: COMMENT-01/02 JSON·root/direct child·정렬과 Query `isActive` 생략 시 true 및 false 명시 조회, 03 기본 parent/isSecret 및 body parent 귀속 오류 400, 04 content-only update, 05 소유자 moderation, 다른 owner의 Path resource 404와 audit를 MockMvc로 검증한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AdminCommunityCommentControllerIntegrationTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.AiCharacterAdminRouteInventoryTest` - 통과 확인: GREEN 구현 후 위 실패 확인 명령을 다시 실행해 `BUILD SUCCESSFUL`을 확인한다. - GREEN: 공통 update DTO를 재사용하되 community 전용 response와 use case를 사용한다. - REFACTOR: 선택 캐릭터가 작성하지 않은 댓글에는 update 권한을 만들지 않는다. - 기대 결과: COMMUNITY-COMMENT-01~05가 route inventory를 통과한다. ### Phase 9: FanTalk, 채널 공지와 프로필 API - [ ] **Task 9.1: 관리자 FanTalk 조회·답글·moderation domain과 persistence 구현** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/domain/CreatorFanTalkAdminModels.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/domain/CreatorFanTalkAdminPolicy.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/application/CreatorFanTalkAdminService.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/port/out/CreatorFanTalkAdminPort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/port/out/CreatorFanTalkEventPort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/adapter/out/persistence/DefaultCreatorFanTalkAdminRepository.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/adapter/out/event/CreatorFanTalkEventAdapter.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/domain/CreatorFanTalkAdminPolicyTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/application/CreatorFanTalkAdminServiceTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/adapter/out/persistence/DefaultCreatorFanTalkAdminRepositoryTest.kt` - RED: 목록에 활성 creator replies 중첩/replyId 포함, 별도 reply query 없음, root target 귀속, AI writer 답글만 update/delete, root owner moderation, 반복 delete, 비활성 creator 신규 답글 409, reply create 언어 감지 commit 후 1회를 검증한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.fantalk.domain.CreatorFanTalkAdminPolicyTest --tests kr.co.vividnext.sodalive.v2.creator.channel.fantalk.application.CreatorFanTalkAdminServiceTest --tests kr.co.vividnext.sodalive.v2.creator.channel.fantalk.adapter.out.persistence.DefaultCreatorFanTalkAdminRepositoryTest` - 통과 확인: GREEN 구현 후 위 실패 확인 명령을 다시 실행해 `BUILD SUCCESSFUL`을 확인한다. - GREEN: FanTalk root와 reply를 관리자 projection으로 함께 조회하고 root 삭제 시 답글 row를 물리 삭제하지 않는다. - REFACTOR: 기존 소비자용 `CreatorChannelFanTalkQueryService`는 수정하지 않고 reply 전용 GET use case를 만들지 않는다. - 기대 결과: 사용자 이력을 보존하면서 선택 캐릭터 답글만 관리할 수 있다. - [ ] **Task 9.2: FAN-TALK-01~05 DTO, facade, Controller contract 구현** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/dto/AdminFanTalkDtos.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/application/AdminFanTalkFacade.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AdminFanTalkController.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AdminFanTalkControllerIntegrationTest.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/AiCharacterAdminRouteInventoryTest.kt` - RED: FAN-TALK-01 page item과 중첩 replies JSON, 02/03 reply response, 04 reply delete, 05 root delete, 다른 creator reply 수정/삭제 차단과 audit를 PRD 17장 기준으로 검증한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AdminFanTalkControllerIntegrationTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.AiCharacterAdminRouteInventoryTest` - 통과 확인: GREEN 구현 후 위 실패 확인 명령을 다시 실행해 `BUILD SUCCESSFUL`을 확인한다. - GREEN: 목록 하나로 root와 replies를 반환하고 네 mutation route만 추가한다. - REFACTOR: `/replies` GET mapping이 route inventory에 존재하지 않는지 negative assertion을 유지한다. - 기대 결과: FAN-TALK-01~05만 노출되고 불필요한 답글 조회 API가 생기지 않는다. - [ ] **Task 9.3: 채널 공지·프로필·creator tag domain과 persistence 구현** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/notice/domain/CreatorChannelNoticeModels.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/notice/application/CreatorChannelNoticeService.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/notice/port/out/CreatorChannelNoticePort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/notice/port/out/CreatorChannelNoticeEventPort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/notice/adapter/out/persistence/DefaultCreatorChannelNoticeRepository.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/notice/adapter/out/event/CreatorChannelNoticeEventAdapter.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/profile/domain/CreatorChannelProfileModels.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/profile/domain/CreatorChannelProfilePolicy.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/profile/application/CreatorChannelProfileService.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/profile/port/out/CreatorChannelProfilePort.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/profile/adapter/out/persistence/DefaultCreatorChannelProfileRepository.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/notice/application/CreatorChannelNoticeServiceTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/profile/domain/CreatorChannelProfilePolicyTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/profile/application/CreatorChannelProfileServiceTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/profile/adapter/out/persistence/DefaultCreatorChannelProfileRepositoryTest.kt` - RED: 공지 없음의 empty/null, upsert와 실제 변경 때만 알림, URL empty 또는 http(s) absolute, donation period, tagIds 중복/active 전건 검증·전체 교체, 빈 tag, metadata 정렬, `kakaoOpenChatUrl <-> Member.websiteUrl`, UTC update를 검증한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.creator.channel.notice.application.CreatorChannelNoticeServiceTest --tests kr.co.vividnext.sodalive.v2.creator.channel.profile.domain.CreatorChannelProfilePolicyTest --tests kr.co.vividnext.sodalive.v2.creator.channel.profile.application.CreatorChannelProfileServiceTest --tests kr.co.vividnext.sodalive.v2.creator.channel.profile.adapter.out.persistence.DefaultCreatorChannelProfileRepositoryTest` - 통과 확인: GREEN 구현 후 위 실패 확인 명령을 다시 실행해 `BUILD SUCCESSFUL`을 확인한다. - GREEN: 공지는 `ChannelNotice`, 프로필은 `Member`와 `MemberCreatorTag`를 adapter에서 사용한다. tag 전체 교체 시 join row만 제거/생성하고 tag 자체는 변경하지 않는다. - REFACTOR: `ChatCharacter.tags`와 Member creator tag를 동기화하지 않고, 이름·이미지·소개 update를 profile API에 중복 구현하지 않는다. - 기대 결과: 캐릭터 원본 정보와 채널 운영 설정의 책임이 분리된다. - [ ] **Task 9.4: NOTICE/CREATOR-TAG/CHANNEL-PROFILE DTO, facade, Controller contract 구현** - Files: - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/dto/AdminChannelSettingsDtos.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/application/AdminChannelNoticeFacade.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/application/AdminChannelProfileFacade.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AdminChannelNoticeController.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AdminChannelProfileController.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AdminChannelSettingsControllerIntegrationTest.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/AiCharacterAdminRouteInventoryTest.kt` - RED: NOTICE-01/02, CREATOR-TAG-01, PROFILE-01/02의 exact path와 Request/Response JSON, tag 비페이징, 빈 문자열 의미, `donationRankingPeriod` key 누락 거부와 명시적 null 허용, 다른 owner 404, invalid URL/tag 400, audit를 검증한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AdminChannelSettingsControllerIntegrationTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.AiCharacterAdminRouteInventoryTest` - 통과 확인: GREEN 구현 후 위 실패 확인 명령을 다시 실행해 `BUILD SUCCESSFUL`을 확인한다. - GREEN: 공지와 프로필 facade가 각 도메인 service만 호출하고 응답 날짜를 UTC `Z`로 변환한다. - REFACTOR: 별도 POST notice, menu, profile image/name update Endpoint를 추가하지 않는다. - 기대 결과: CHAR-05를 제외한 65개 Operation이 route inventory를 통과한다. ### Phase 10: 캐릭터 삭제 cascade와 legacy 비활성화 - [ ] **Task 10.1: 도메인별 creator 비활성화 use case와 단일 transaction cascade 구현** - Files: - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/management/application/ContentManagementService.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/management/port/in/ContentCreatorDeactivationUseCase.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/management/port/out/ContentManagementPort.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/management/adapter/out/persistence/DefaultContentManagementRepository.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/category/application/ContentCategoryManagementService.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/category/port/in/ContentCategoryCreatorDeactivationUseCase.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/series/application/CreatorSeriesAdminService.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/series/port/in/SeriesCreatorDeactivationUseCase.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/application/CreatorCommunityAdminPostService.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/port/in/CommunityCreatorDeactivationUseCase.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/application/AiCharacterCommandService.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/port/out/AiCharacterEventPort.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/adapter/out/event/AiCharacterEventAdapter.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/port/out/AiCharacterPersistencePort.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/adapter/out/persistence/DefaultAiCharacterPersistenceAdapter.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/content/management/application/ContentManagementServiceTest.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/content/management/adapter/out/persistence/DefaultContentManagementRepositoryTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/application/AiCharacterDeletionServiceIntegrationTest.kt` - RED: 한 transaction에서 character/Member inactive, 소유 콘텐츠 raw `isActive=false`, active series/post/category inactive, FanTalk/replies 불변, join/이력 물리 삭제 0건, 중간 실패 전체 rollback, 반복 삭제 동일 응답을 검증한다. 콘텐츠의 `releaseDate`, `content`, `duration`과 구매 이력은 보존하면서 기존 미삭제 콘텐츠 `SUSPENDED`, 기존 삭제 콘텐츠 `DELETED`로 계산되는지도 함께 검증한다. 실제 active→inactive 전이에서만 deactivation event 1회, rollback·반복 삭제에서는 0회인지 고정한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.content.management.application.ContentManagementServiceTest --tests kr.co.vividnext.sodalive.v2.content.management.adapter.out.persistence.DefaultContentManagementRepositoryTest --tests kr.co.vividnext.sodalive.v2.aicharacter.application.AiCharacterDeletionServiceIntegrationTest` - 통과 확인: GREEN 구현 후 위 실패 확인 명령을 다시 실행해 `BUILD SUCCESSFUL`을 확인한다. - GREEN: `AiCharacterCommandService.delete`가 content, category, series, community의 좁은 deactivation input port를 호출하고 마지막에 character와 Member를 비활성화한다. content port는 소유 row의 raw `isActive`만 `false`로 전환하며 모든 port는 같은 transaction에 참여한다. 실제 active→inactive 전이에서만 같은 transaction 안에 deactivation event를 1회 publish한다. - REFACTOR: aicharacter persistence adapter가 다른 도메인 테이블을 직접 update하지 않게 하고 FanTalk deactivation port는 만들지 않는다. - 기대 결과: PRD 7.4의 cascade가 물리 삭제 없이 원자적으로 완료된다. - [ ] **Task 10.2: CHAR-05, legacy 비활성화·복구 거부와 공개 콘텐츠·채팅 차단 구현** - Files: - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/application/AdminAiCharacterFacade.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AdminAiCharacterController.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/LegacyAiCharacterMutationAdapter.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/AdminChatCharacterController.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/original/service/AdminOriginalWorkService.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/controller/ChatCharacterController.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterService.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/image/CharacterImageController.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/image/CharacterImageService.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/comment/CharacterCommentController.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/comment/CharacterCommentService.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/chat/room/service/ChatRoomService.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/chat/room/repository/ChatRoomRepository.kt` - Verify: `src/main/kotlin/kr/co/vividnext/sodalive/chat/room/controller/ChatRoomController.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/chat/quota/room/ChatRoomQuotaService.kt` - Verify: `src/main/kotlin/kr/co/vividnext/sodalive/chat/quota/room/ChatRoomQuotaController.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/adapter/in/web/AdminAiCharacterControllerIntegrationTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/admin/chat/character/LegacyAiCharacterMutationAdapterTest.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/admin/chat/character/AdminChatCharacterControllerTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterInactiveTransitionTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/chat/character/InactiveAiCharacterPublicSurfaceTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/chat/room/service/InactiveAiCharacterChatAccessTest.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/chat/quota/room/ChatRoomQuotaControllerTest.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/chat/quota/room/ChatRoomQuotaServiceTest.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/AiCharacterAdminRouteInventoryTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/content/InactiveAiCharacterPublicContentVisibilityTest.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/content/PurchasedContentPlaybackAfterAiCharacterDeletionTest.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/content/AudioContentServiceTest.kt` - Verify: `src/main/kotlin/kr/co/vividnext/sodalive/content/order/OrderRepository.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentCommentService.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerServiceTest.kt` - Verify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/application/CreatorChannelFanTalkQueryService.kt` - Verify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/adapter/out/persistence/DefaultCreatorChannelFanTalkQueryRepository.kt` - Verify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/adapter/out/persistence/DefaultCreatorChannelFanTalkQueryRepositoryTest.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt` - Verify: `src/main/kotlin/kr/co/vividnext/sodalive/search/SearchRepository.kt` - Verify: `src/main/kotlin/kr/co/vividnext/sodalive/query/recommend/RecommendChannelQueryRepository.kt` - Verify: `src/main/kotlin/kr/co/vividnext/sodalive/query/recommend/RecommendChannelQueryService.kt` - Verify: `src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/AudioContentMainTabRepository.kt` - Verify: `src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/content/ContentMainTabTagCurationRepository.kt` - Verify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/all/adapter/out/persistence/DefaultMainContentAllQueryRepository.kt` - Verify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/adapter/out/persistence/DefaultAudioRecommendationQueryRepository.kt` - Verify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/audio/adapter/out/persistence/DefaultCreatorChannelAudioQueryRepository.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/adapter/out/persistence/AudioRankingSnapshotRepository.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/adapter/out/persistence/DefaultAudioRankingSnapshotPersistenceAdapterTest.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/CreatorRankingSnapshotRepository.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/DefaultCreatorRankingSnapshotRepositoryTest.kt` - Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/adapter/out/cache/AiCharacterVisibilityCacheInvalidator.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/adapter/out/cache/AiCharacterVisibilityCacheInvalidatorTest.kt` - Verify: `src/main/kotlin/kr/co/vividnext/sodalive/configs/RedisConfig.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/rank/RankingRepository.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/rank/RankingRepositoryTest.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepository.kt` - Create: `src/test/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepositoryTest.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/content/main/banner/AudioContentBannerRepository.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/content/main/banner/AudioContentBannerRepositoryTest.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/content/series/main/banner/SeriesBannerRepository.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/content/series/main/banner/ContentSeriesBannerServiceIntegrationTest.kt` - Modify: `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/repository/ChatCharacterBannerRepository.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterBannerServiceIntegrationTest.kt` - RED: CHAR-05 exact response/반복 삭제/audit, legacy active→false가 같은 cascade 호출, false→true 409, 이름·표시 정보 보존, 외부 rename/delete 미호출을 검증한다. legacy 캐릭터 등록·수정 2개는 기존 multipart/body ID/nullable patch/성공 `data=null`을 유지하면서 전체 외부·이미지·DB mutation을 v2 캐릭터 command로 호출한다. 등록의 `originalWorkId=null`·`0`은 미연결, 수정의 `null`은 변경 없음·`0`은 해제이며, 원작 최종 재검증 실패도 같은 보상 경계를 사용하는지 검증한다. 삭제 후 공개 캐릭터 상세·이미지 목록/구매·댓글 조회/작성, 방 생성/입장/session 상태/목록/메시지 조회·전송·구매/초기화, room quota 캔·광고 구매를 모두 차단하고, 목록에서는 제외하며 캔 차감이나 외부 session 호출 전에 실패하는지도 검증한다. 삭제 캐릭터의 콘텐츠는 legacy/v2 목록·검색·추천·크리에이터 채널·content/creator latest/previous ranking snapshot·legacy creator ranking·공개 캐릭터/콘텐츠/시리즈 banner와 미구매 상세에서 제외한다. 공개 콘텐츠 댓글·답글 조회·신규 등록·본문 수정·재활성화는 차단하고 권한 있는 논리 삭제만 허용한다. 현재 v2 FanTalk 탭과 legacy FanTalk 목록·신규 원문 등록은 비활성 creator를 거부하고 기존 FanTalk row는 보존하는지 검증한다. 기존 KEEP/RENTAL 구매자는 `AudioContentService.getDetail`·`generateUrl`에서 전체 Signed URL 재생을 유지하되 댓글과 관련 콘텐츠는 받지 않는다. ranking snapshot과 연결형 banner row는 변경하지 않고 `EVENT`, `LINK`처럼 연결 대상이 없는 banner의 공개 결과도 바꾸지 않는다. `default`, `cache_ttl_3_days`, `popularCharacters_24h_locale`를 pre-warm한 뒤 commit 시 stale DTO가 사라지고 rollback·반복 삭제에서는 cache clear 0회인지 검증한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.adapter.in.web.AdminAiCharacterControllerIntegrationTest --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.AiCharacterAdminRouteInventoryTest --tests kr.co.vividnext.sodalive.admin.chat.character.LegacyAiCharacterMutationAdapterTest --tests kr.co.vividnext.sodalive.admin.chat.character.AdminChatCharacterControllerTest --tests kr.co.vividnext.sodalive.chat.character.service.ChatCharacterInactiveTransitionTest --tests kr.co.vividnext.sodalive.chat.character.InactiveAiCharacterPublicSurfaceTest --tests kr.co.vividnext.sodalive.chat.room.service.InactiveAiCharacterChatAccessTest --tests kr.co.vividnext.sodalive.chat.quota.room.ChatRoomQuotaControllerTest --tests kr.co.vividnext.sodalive.chat.quota.room.ChatRoomQuotaServiceTest --tests kr.co.vividnext.sodalive.content.AudioContentServiceTest --tests kr.co.vividnext.sodalive.content.InactiveAiCharacterPublicContentVisibilityTest --tests kr.co.vividnext.sodalive.content.PurchasedContentPlaybackAfterAiCharacterDeletionTest --tests kr.co.vividnext.sodalive.explorer.ExplorerServiceTest --tests kr.co.vividnext.sodalive.v2.creator.channel.fantalk.adapter.out.persistence.DefaultCreatorChannelFanTalkQueryRepositoryTest --tests kr.co.vividnext.sodalive.v2.content.ranking.adapter.out.persistence.DefaultAudioRankingSnapshotPersistenceAdapterTest --tests kr.co.vividnext.sodalive.v2.ranking.adapter.out.persistence.DefaultCreatorRankingSnapshotRepositoryTest --tests kr.co.vividnext.sodalive.v2.aicharacter.adapter.out.cache.AiCharacterVisibilityCacheInvalidatorTest --tests kr.co.vividnext.sodalive.rank.RankingRepositoryTest --tests kr.co.vividnext.sodalive.explorer.ExplorerQueryRepositoryTest --tests kr.co.vividnext.sodalive.content.main.banner.AudioContentBannerRepositoryTest --tests kr.co.vividnext.sodalive.content.series.main.banner.ContentSeriesBannerServiceIntegrationTest --tests kr.co.vividnext.sodalive.chat.character.service.ChatCharacterBannerServiceIntegrationTest` - 통과 확인: GREEN 구현 후 위 실패 확인 명령을 다시 실행해 `BUILD SUCCESSFUL`을 확인한다. - GREEN: 신규 DELETE와 legacy 비활성화가 동일한 v2 delete use case를 호출한다. legacy 캐릭터 compatibility adapter는 register를 v2 create command로, update의 non-null field를 v2 patch command로, 등록의 null·0을 미연결로, 수정의 null을 변경 없음·0을 명시적 원작 해제로, `isActive=false`를 delete로 변환하며 inactive→true 복구를 거부한다. 이 전환 후 `AdminOriginalWorkService.assignOneCharacter`를 제거한다. `ChatCharacterService`의 공통 active-character guard를 소비성·외부 호출 전에 적용한다. 기존 공개 목록·검색·추천은 content deactivation을 통해 inactive 콘텐츠를 제외한다. `AudioContentService` 직접 상세는 미구매자에게 비활성 creator/content를 거부하고 구매자에게 재생 경로만 유지한다. `AudioContentCommentService`는 공개 댓글·답글 조회와 신규 등록·본문 수정·재활성화를 거부하고 권한 있는 논리 삭제만 허용한다. 기존 v2 FanTalk 조회의 active creator 조건은 유지하고 `ExplorerService.writeCheers`, `getCreatorProfileCheers`는 대상 creator가 비활성이면 저장·조회 전에 거부한다. audio/creator latest·previous ranking snapshot native query와 legacy creator ranking, 공개 언어별 character/content/series banner query는 현재 연결 대상의 active 상태를 확인한다. AFTER_COMMIT listener는 deactivation event를 받아 기존 세 cache namespace를 clear한다. - REFACTOR: legacy Controller에서 외부 캐릭터·이미지·DB orchestration과 원작 직접 연결을 제거하고 호환 Request 변환과 `ApiResponse.ok(null)`만 남긴다. 이미 `content.isActive`를 검사하는 공개 query에는 중복 creator predicate를 추가하지 않고 snapshot을 삭제·갱신하거나 ranking job을 강제 실행하지 않는다. 동적 key prefix 삭제나 신규 cache abstraction을 만들지 않는다. - 기대 결과: CHAR-05를 포함한 신규 66개 mapping이 route inventory를 통과하고, 삭제 캐릭터와 미구매 콘텐츠는 로컬 공개·소비 진입점에서 즉시 차단되며 기존 구매 재생은 유지된다. ### Phase 11: 전체 계약, 보안, 경계와 회귀 검증 - [ ] **Task 11.1: 66개 Operation의 소유권·상태·audit 교차 시나리오 검증** - Files: - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/AiCharacterAdminOwnershipEndToEndTest.kt` - 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/캐릭터별 규칙도 포함한다. 정상/과거 불일치 해제를 별도 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에 보완한다. - REFACTOR: E2E fixture를 공유하되 production에 범용 CRUD/impersonation abstraction을 추가하지 않는다. - 기대 결과: route inventory의 66개 mapping과 공통 보안 경계가 모두 통과한다. - [ ] **Task 11.2: side effect와 기존 callback·scheduler 호환 통합 검증** - Files: - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/AiCharacterAdminSideEffectIntegrationTest.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/originalwork/application/OriginalWorkAdminServiceTest.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/originalwork/adapter/out/event/OriginalWorkEventAdapterTest.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/aicharacter/adapter/out/cache/AiCharacterVisibilityCacheInvalidatorTest.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/content/AudioContentUploadCompletionContractTest.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/content/AudioContentServiceTest.kt` - Modify: `src/test/kotlin/kr/co/vividnext/sodalive/content/AudioContentReleaseQueryTest.kt` - RED: PRD 21.3 side effect 표의 관리자 command를 commit/rollback/retry로 실행하고 0회 또는 정확히 1회인지 검증한다. 원작 생성 언어 감지와 정규화된 `title`, `contentType`, `category`, `description`, `tags` 중 실제 변경된 수정의 번역은 commit 후 1회, 다른 field 변경·동일 값·배정·해제·rollback에서는 0회여야 한다. 감지 결과 transaction이 commit되기 전에 후속 번역이 시작되지 않는지도 포함한다. v2 생성과 같은 기존 row가 현재 callback과 scheduler에서 즉시/예약 공개되고, 삭제 row·비활성 creator·동일 callback 재시도에서는 공개 FCM/home news가 0회 또는 최초 1회를 넘지 않는지도 함께 검증한다. 캐릭터 삭제 cache clear도 commit 시 1회, rollback·반복 삭제 시 0회여야 한다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.AiCharacterAdminSideEffectIntegrationTest --tests kr.co.vividnext.sodalive.v2.originalwork.application.OriginalWorkAdminServiceTest --tests kr.co.vividnext.sodalive.v2.originalwork.adapter.out.event.OriginalWorkEventAdapterTest --tests kr.co.vividnext.sodalive.v2.aicharacter.adapter.out.cache.AiCharacterVisibilityCacheInvalidatorTest --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: 각 command는 동일 상태인지 먼저 판별하고 실제 최초 상태 전이가 있을 때만 기존 transactional listener용 event를 transaction 안에서 publish한다. direct home-news 호출만 `AfterCommitExecutor`에 등록하며 별도 lifecycle/completion 결과 모델은 만들지 않는다. - REFACTOR: 이벤트 이름과 payload는 resource ID와 필요한 최소 값만 포함하고 대용량 본문을 넣지 않는다. - 기대 결과: rollback과 멱등 재시도가 외부 알림·번역·home news 또는 cache clear를 중복 생성하지 않는다. - [ ] **Task 11.3: v2 의존성·Endpoint 및 DB schema·JPA·migration 변경 금지 검증** - Files: - Create: `src/test/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter/AiCharacterAdminArchitectureTest.kt` - Verify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter` - Verify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/aicharacter` - Verify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/originalwork` - Verify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/content` - Verify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel` - RED: 이 계획에서 신규 생성·수정한 v2 class manifest를 기준으로 domain/application/port의 legacy Service/Repository/web DTO 및 JPA entity/Q type import, `/v2` HTTP prefix, 신규 login/menu/callback/reply-GET mapping, legacy 형태의 원작 `/search`·`/register`·`/update` mapping, 신규 v2 `@Entity`/`@Table` 선언이 있으면 실패하는 architecture test를 작성한다. legacy compatibility adapter가 v2 input port를 호출하는 방향은 허용하되 v2 package가 해당 adapter나 legacy DTO를 import하면 실패한다. 기존 v2 파일의 선행 부채를 이번 변경 위반으로 오탐하지 않는다. - 실패 확인: `./gradlew test --tests kr.co.vividnext.sodalive.v2.admin.aicharacter.AiCharacterAdminArchitectureTest` - 통과 확인: GREEN 구현 후 위 실패 확인 명령을 다시 실행해 `BUILD SUCCESSFUL`을 확인한다. - GREEN: 위반 import/mapping만 adapter 또는 기존 허용 경계로 이동한다. - REFACTOR: 정적 검증을 위해 새 architecture dependency를 추가하지 않고 classpath/reflection과 소스 resource 검사로 충분히 구현한다. - 추가 검증: - `rg -n '"/(v2/admin|admin/v2)|upload-complete|member/login|"/menu' src/main/kotlin/kr/co/vividnext/sodalive/v2/admin/aicharacter` - `rg -n 'lifecycle_status|upload_pipeline_version|ContentUploadCompletionUseCase|V2ContentReleaseScheduler|AudioContentUploadCompletionDispatcher' src/main/kotlin` - 5장에 기록한 구현 시작 기준 commit과 dirty/untracked manifest를 현재 changed/untracked file manifest와 대조해 이번 구현이 생성·수정한 `*.sql`이 0건인지 확인한다. - 같은 manifest에서 기존 `@Entity` source 수정 0건과 신규 v2 production source의 `@Entity`, `@Table` 선언 0건을 확인한다. - 구현 시작 기준과 비교해 `AudioContent.kt`, `AudioContentController.kt`, `AudioContentReleaseScheduledTask.kt`에 diff가 없는지 확인한다. - 구현 시작 기준과 비교해 `OriginalWork.kt`, `OriginalWorkLink.kt`, `OriginalWorkTag.kt`, `OriginalWorkTagMapping.kt`, `ChatCharacter.originalWork` JPA mapping에 diff가 없는지 확인한다. - `docs/20260720_AI캐릭터_관리자기능` 아래 SQL 파일이 0건인지 확인한다. - 위 `rg`는 출력 0건이 기대 결과이며, match 없음에 따른 exit code 1은 위반 검출 실패가 아니다. - 기대 결과: PRD 7.6과 21.1~21.4의 경계 위반, 금지 Endpoint, DB schema·JPA mapping·migration과 pipeline 분기 추가가 0건이다. - [ ] **Task 11.4: 최종 targeted/full regression, 포맷과 문서 검증** - Files: - Modify: `docs/20260720_AI캐릭터_관리자기능/plan-task.md` - Verify: `docs/20260720_AI캐릭터_관리자기능/prd.md` - Verify: `docs/20260720_AI캐릭터_관리자기능/frontend-original-work-prompt.md` - RED: TDD 예외. 이 Task는 구현 완료 후 전체 회귀와 문서/API/DB schema·JPA·migration 비변경 정합성을 확인하는 최종 gate다. - GREEN: 다음 순서로 검증한다. 1. `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.admin.aicharacter.*'` 2. `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.aicharacter.*'` 3. `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.originalwork.*'` 4. `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.content.*'` 5. `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.*'` 6. `./gradlew test --tests 'kr.co.vividnext.sodalive.content.AudioContentUploadCompletion*' --tests kr.co.vividnext.sodalive.content.AudioContentServiceTest --tests 'kr.co.vividnext.sodalive.scheduler.*'` 7. `./gradlew test` 8. `./gradlew ktlintCheck` 9. `./gradlew tasks --all` 10. `git diff --check` 11. `rg -o '^\| .(CHAR|ORIGINAL-WORK|CONTENT|CONTENT-COMMENT|CATEGORY|SERIES|COMMUNITY-POST|COMMUNITY-COMMENT|FAN-TALK|NOTICE|CREATOR-TAG|CHANNEL-PROFILE)-[0-9]{2}.' docs/20260720_AI캐릭터_관리자기능/prd.md | sort -u | wc -l` 결과가 66인지 확인한다. 12. `rg -c '^ORIGINAL-WORK-0[1-8] (GET|POST|PUT|DELETE) ' docs/20260720_AI캐릭터_관리자기능/frontend-original-work-prompt.md` 결과가 8인지 확인한다. 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 '^- \[[ 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`만으로 검사되지 않으므로 파일 자체 검사도 수행한다. 20. `rg -c 'nullable JSON:$' docs/20260720_AI캐릭터_관리자기능/frontend-original-work-prompt.md` 결과가 5인지 확인하고, PRD 11.7의 nullable response field와 CHAR-02 `originalWork` nullable 계약이 해당 JSON 값 및 Frontend test에 모두 나타나는지 대조한다. 21. PRD의 fenced JSON 전체와 `frontend-original-work-prompt.md`의 `JSON:` label 값 전체를 JSON parser로 읽어 각각 51/51, 20/20이 유효한지 확인한다. 이어서 PRD 11.7과 Frontend delta의 `(Operation ID, Method, Path)` 8개가 exact equality인지 대조하고, Operation별 Request/Response JSON `(Operation ID, Request|Response)` 12개가 모두 존재하며 key, value, 배열과 null 구조가 12/12 exact equality인지 확인한다. - REFACTOR: 실패가 발생하면 해당 Phase의 가장 좁은 RED test로 돌아가 최소 수정한 뒤 targeted와 전체 검증을 다시 수행한다. - 문서화: 각 Task 완료 시 checkbox와 실제 RED/GREEN 명령 결과를 갱신한다. 실행하지 않은 staging worker E2E와 production 배포를 완료로 표시하지 않는다. - 기대 결과: 66개 신규 Operation, 기존 callback/scheduler 호환, 원작 관리, 삭제 cascade, 보안·audit·경계 검증이 모두 통과한다. ## 4. 명시적 비범위 확인 다음 항목은 구현 중 발견해도 이 계획에 임의로 추가하지 않는다. - Frontend 프로젝트, 메뉴, route guard, SEO/noindex, Jenkins frontend build 설정 - 이미 적용된 PRD 27.8 Frontend baseline 프롬프트의 수정. 원작용 별도 delta 프롬프트만 문서로 제공한다. - 신규 관리자 로그인, AI 캐릭터 사칭 token, Backend menu/capability Endpoint - legacy `/admin/chat/original/**`의 Method·Path·Request·성공 Response 제거·변경 또는 일반 사용자용 `/api/chat/original/**` 변경. legacy mutation을 v2 input port로 수렴시키는 내부 변경은 범위에 포함한다. - 원작 복구, 연결 캐릭터의 암묵적 일괄 해제, 원작 이미지 정리 작업 - 신규 upload-complete Endpoint, AWS worker/Trigger/worker schedule 변경 - 콘텐츠·원작 schema·JPA mapping 변경, DDL, backfill, 선제 데이터 migration - V1/V2 callback dispatcher, v2 completion use case, v2 전용 예약 공개 scheduler - FanTalk reply 별도 GET Endpoint - 라이브, DM, 정산, 후원 분석, 시그니처 후원 - 캐릭터 복구, 삭제 리소스 자동 복원 - 영속 audit table, 범용 impersonation framework, 범용 CRUD engine - legacy API 전체의 HTTP status/error envelope 일괄 변경 ## 5. 구현 시 검증 기록 구현 에이전트는 각 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 `5b700892`, 기존 dirty/untracked 없음. - staging worker/Trigger E2E: 아직 실행하지 않음. - production 배포: 이 계획의 코드 작성 단계만으로 완료 처리하지 않음. ## 6. 문서 자체 검증 기록 - 초기 문서 검증 기록: DDL 설계 당시 Operation 58개, Phase 12개, Task 40개, RED/GREEN 명령 36쌍, TDD 예외 Task 4개, `./gradlew tasks --all`의 `BUILD SUCCESSFUL in 12s`를 확인했다. - 무DDL 보강 후 Operation coverage: PRD의 고유 신규 Operation ID 58개와 1.1 표 합계 58개가 일치한다. - 무DDL 보강 후 구조 검증: Phase 12개, Task 34개, RED 실행 명령 32개, 대응 GREEN 통과 확인 32개다. 나머지 2개 Task는 사유와 대체 검증을 명시한 TDD 예외다. - 무DDL 보강 후 Task 필드 검증: 모든 Task에 `Files`, `RED`, `GREEN`, `REFACTOR`, 기대 결과가 있다. - 무DDL 보강 후 금지 범위 검증: placeholder, 신규 login/menu/callback/FanTalk reply GET, `/v2` 관리자 URL prefix, Frontend 구현 Task, 콘텐츠 DDL·pipeline 분기·v2 scheduler 구현 Task가 모두 0건이다. - 무DDL 보강 후 Markdown 검증: code fence 짝 일치, trailing whitespace 0건, tab 0건이다. - 원작 보강 전 Frontend baseline API 계약 검증: Frontend catalog의 `AUTH-01` 1개와 신규 관리자 Operation 58개가 중복 없이 존재하고, 59개 모두 Request JSON 표기와 유효한 Response JSON을 가진다. Request body가 있는 Operation은 25개이며 Response에 `previewStartTime`, `previewEndTime`은 0건이다. - 최종 삭제·공개 경계 검증: 기존 컬럼의 논리 상태 변경, 구매 콘텐츠 재생 예외, 댓글·FanTalk 차단, ranking·banner 현재 활성 상태 결합, 지정된 cache 3종 무효화가 PRD와 Task 10.1~10.2에서 일치한다. - 최종 callback 계약 검증: CONTENT-03의 `input/{contentId}/{contentId}-content-...` basename, 기존 callback·scheduler 재사용, 신규 worker·callback·pipeline 분기 금지가 PRD와 Task 0.1, 3.2, 4.2, 11.2에서 일치한다. - 최종 DB 변경 금지 검증: 문서 디렉터리의 SQL 파일 0건이며, 계획에 DB table·column·index·JPA mapping·DDL·backfill·migration 생성·수정 Task가 없다. - 원작 보강 전 Gradle 구성 검증: `./gradlew tasks --all` 실행 결과 `BUILD SUCCESSFUL in 1s`다. - 원작 보강 후 Operation 검증: PRD의 고유 Backend Operation 66개와 `frontend-original-work-prompt.md`의 `ORIGINAL-WORK-01`~`08` 8개가 일치한다. 기존 27.8 fenced block hash는 `5956ddc152c026937728381d625859bdea65b9a2f16a39b200f0d6b3660a73e1`로 유지됐다. - 원작 보강 후 구조 검증: Phase 12개, Task 37개, 실패 확인 35개, 통과 확인 35개, TDD 예외 2개이며 모든 37개 Task에 `Files`, `RED`, `GREEN`, `REFACTOR`, 기대 결과가 있다. - 원작 보강 후 계약 검증: 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 배포는 실행하지 않았다.