Files
sodalive-backend-spring-boot/docs/20260724_AI캐릭터_관리자_API/plan-task.md

94 KiB
Raw Blame History

AI 캐릭터 관리자 API 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: ADMIN이 AI 캐릭터용 Member로 로그인하지 않고 characterId 기준으로 캐릭터, 콘텐츠, 시리즈, 커뮤니티, FanTalk 답변을 안전하게 대리 관리하는 신규 v2 관리자 API를 구현한다.

Architecture: 신규 외부 경계는 /api/v2/admin/ai-characters 하위 controller/facade/application에 둔다. 공통 target resolver가 characterId -> ChatCharacter.creatorMember를 해석하고 CREATOR + AI_CHARACTER 불변식과 ownership을 먼저 검증한 뒤, 각 domain vertical slice가 기존 entity/repository/S3/CloudFront/event 컴포넌트를 테스트로 고정해 선택적으로 재사용한다.

Tech Stack: Kotlin, Spring Boot 2.7.14, Java 17, Spring Security, JPA/Hibernate, QueryDSL, MySQL, Gradle Wrapper, JUnit5.


Source of Truth

  • 요구사항 원본: .omx/specs/deep-interview-ai-character-admin-api.md
  • 2026-07-24 후속 확정 정책: 신규 prefix는 JWT ROLE_ADMIN + 현재 DB Member.role == ADMIN 이중 인가를 적용하고, stale ADMIN claim은 403으로 거부한다. 신규 prefix의 API application/controller/security filter 오류는 정확한 비2xx status + ApiResponse.error + Accept-Language 기반 KO/EN/JA message를 반환한다. 이 후속 정책이 원본과 충돌하면 후속 정책을 우선한다.
  • 2026-07-25 후속 확정 정책: 신규 prefix의 CORS는 현재 코드에 정의된 캐릭터 관리자 frontend Origin http://localhost:8888, https://test-character-admin.sodalive.net, https://character-admin.sodalive.net만 허용한다. 기존 범용 관리자 frontend와 creator frontend Origin은 허용하지 않는다.
  • 2026-07-25 2차 리뷰 후속 확정 정책: 공유 /admin/member/login, /member/logout는 기존 전역 Origin과 캐릭터 관리자 Origin의 합집합을 path-specific으로 허용한다. CORS 정책 거부 403 body는 API 오류 envelope 계약에서 제외한다. 신규 prefix의 406은 common.error.invalid_request, MissingPathVariableException은 500 common.error.unknown으로 처리하고, 405 Allow와 415 Accept 표준 header를 유지한다.
  • 문서 작성 규칙: docs/agent-guides/작업절차.md, docs/agent-guides/문서유지보수.md, docs/agent-guides/테스트스타일.md
  • 기존 AI 캐릭터 연결 문서: docs/20260611_AI캐릭터_크리에이터기능_최소연결/{prd.md,plan-task.md}

Endpoint Contract Summary

신규 API prefix는 /api/v2/admin/ai-characters로 한다. 기존 /admin/*, /creator-admin/*, 공개 /api/v2/creator-channels/*의 성공·오류 status/body/message 계약은 변경하지 않는다.

모든 성공 응답은 기존 관례처럼 ApiResponse.ok(...) wrapper를 사용한다. API application/controller/security filter 오류는 오류 의미에 맞는 HTTP status와 ApiResponse.error(...) wrapper를 사용한다.

characterId는 target resource endpoint의 외부 대상 식별자다. 캐릭터 목록/검색은 선택된 target이 없어 characterId를 받지 않고, 캐릭터 생성은 새 ChatCharacter를 만드는 endpoint라 path characterId를 받지 않는다.

모든 목록/검색 endpoint는 page 기본값 0, size 기본값 20, 최소 20, 최대 50 보정을 적용하고 경계값 테스트를 둔다.

{
  "success": true,
  "message": null,
  "data": {}
}

공통 API 오류 응답은 success=false, 현지화된 message, data=null, errorProperty=null을 포함하고 2xx로 normalize하지 않는다. Accept-Language: ko|en|ja에 따라 KO/EN/JA를 반환하며, 없거나 지원하지 않는 언어는 KO로 fallback한다. security filter 단계도 MVC interceptor에 의존하지 않고 header를 직접 해석한다.

오류 HTTP status message key
JWT 없음·잘못됨·만료·폐기 401 common.error.bad_credentials
JWT role 비ADMIN 403 common.error.access_denied
JWT ADMIN + 현재 DB role 비ADMIN stale claim 403 common.error.access_denied
request binding·target 미존재·creatorMember 누락·role/memberKind 불변식 위반 400 common.error.invalid_request
신규 prefix 미매핑 경로 404 common.error.invalid_request
지원하지 않는 HTTP method 405 common.error.invalid_request
지원하지 않는 응답 media type 406 common.error.invalid_request
지원하지 않는 요청 media type 415 common.error.invalid_request
MissingPathVariableException·예상하지 못한 서버 오류 500 common.error.unknown

405 응답은 표준 Allow header를, 415 응답은 표준 Accept header를 유지한다.

{
  "success": false,
  "message": "Invalid request.",
  "data": null,
  "errorProperty": null
}

신규 prefix는 캐릭터 관리자 Origin http://localhost:8888, https://test-character-admin.sodalive.net, https://character-admin.sodalive.net만 허용한다. 공유 /admin/member/login, /member/logout는 기존 전역 Origin과 캐릭터 관리자 Origin의 합집합만 path-specific으로 허용하며, 다른 legacy/public 경로의 허용 범위는 변경하지 않는다. 허용되지 않은 Origin, method 또는 header가 Spring CORS 계층에서 403으로 정책 거부되면 handler 진입 전 종료되는 브라우저 보안 경계이므로 그 응답의 body, content type, 현지화 및 ApiResponse.error envelope는 외부 계약으로 고정하지 않는다.

Phase 2~6에서 추가되는 domain/client/server 오류는 구현 전에 각 Task에서 정확한 비2xx status와 KO/EN/JA message key를 고정하고 같은 envelope를 적용한다. 신규 prefix 전용 오류 처리는 legacy/public endpoint에 적용하지 않는다.

캐릭터 목록/검색

GET /api/v2/admin/ai-characters?search=루나&page=0&size=20

Query parameters:

{
  "search": "루나",
  "page": 0,
  "size": 20
}

Response data:

{
  "totalCount": 1,
  "page": 0,
  "size": 20,
  "hasNext": false,
  "items": [
    {
      "characterId": 101,
      "name": "루나",
      "description": "달빛을 좋아하는 AI 캐릭터",
      "imageUrl": "https://cdn.example.com/characters/luna.png",
      "creatorMemberId": 9001,
      "creatorNickname": "루나",
      "originalWorkId": 31,
      "externalCharacterId": "ext-luna-001",
      "isActive": true,
      "createdAtUtc": "2026-07-24T00:00:00Z"
    }
  ]
}

캐릭터 상세

GET /api/v2/admin/ai-characters/{characterId}

Response data:

{
  "characterId": 101,
  "name": "루나",
  "description": "달빛을 좋아하는 AI 캐릭터",
  "imageUrl": "https://cdn.example.com/characters/luna.png",
  "creatorMemberId": 9001,
  "creatorNickname": "루나",
  "creatorProfileImageUrl": "https://cdn.example.com/characters/luna.png",
  "creatorIntroduce": "달빛을 좋아하는 AI 캐릭터",
  "originalWorkId": 31,
  "externalCharacterId": "ext-luna-001",
  "isActive": true,
  "createdAtUtc": "2026-07-24T00:00:00Z",
  "updatedAtUtc": "2026-07-24T00:00:00Z"
}

캐릭터 생성

POST /api/v2/admin/ai-characters

Content-Type: multipart/form-data

Form fields:

{
  "image": "File | optional",
  "request": {
    "name": "루나",
    "description": "달빛을 좋아하는 AI 캐릭터",
    "originalWorkId": 31,
    "externalCharacterId": "ext-luna-001",
    "isActive": true
  }
}

Response data: 캐릭터 상세와 동일하다.

캐릭터 수정/비활성화

PUT /api/v2/admin/ai-characters/{characterId}

Content-Type: multipart/form-data

Form fields:

{
  "image": "File | optional",
  "request": {
    "name": "루나",
    "description": "수정된 소개",
    "originalWorkId": 31,
    "externalCharacterId": "ext-luna-001",
    "isActive": false
  }
}

Response data: 캐릭터 상세와 동일하다. isActive=false는 soft delete 의미다.

오디오 콘텐츠 목록/검색

GET /api/v2/admin/ai-characters/{characterId}/audio-contents?search=밤&status=OPEN&page=0&size=20

Query parameters:

{
  "search": "밤",
  "status": "OPEN",
  "page": 0,
  "size": 20
}

Response data:

{
  "totalCount": 1,
  "page": 0,
  "size": 20,
  "hasNext": false,
  "items": [
    {
      "contentId": 501,
      "title": "밤 산책",
      "coverImageUrl": "https://cdn.example.com/audio/501-cover.png",
      "audioSignedUrl": "https://cdn.example.com/signed/audio/501.m4a?Expires=...",
      "price": 1000,
      "isAdult": false,
      "isActive": true,
      "releaseDateUtc": "2026-07-25T00:00:00Z",
      "status": "OPEN"
    }
  ]
}

오디오 콘텐츠 상세

GET /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}

Response data:

{
  "contentId": 501,
  "title": "밤 산책",
  "description": "조용한 밤 산책 오디오",
  "coverImageUrl": "https://cdn.example.com/audio/501-cover.png",
  "audioSignedUrl": "https://cdn.example.com/signed/audio/501.m4a?Expires=...",
  "price": 1000,
  "isAdult": false,
  "isActive": true,
  "releaseDateUtc": "2026-07-25T00:00:00Z",
  "status": "OPEN",
  "seriesIds": [701],
  "createdAtUtc": "2026-07-24T00:00:00Z",
  "updatedAtUtc": "2026-07-24T00:00:00Z"
}

오디오 콘텐츠 생성

POST /api/v2/admin/ai-characters/{characterId}/audio-contents

Content-Type: multipart/form-data

Form fields:

{
  "coverImage": "File",
  "audioFile": "File",
  "request": {
    "title": "밤 산책",
    "description": "조용한 밤 산책 오디오",
    "price": 1000,
    "isAdult": false,
    "isActive": true,
    "releaseDateUtc": "2026-07-25T00:00:00Z",
    "seriesIds": [701]
  }
}

Response data: 오디오 콘텐츠 상세와 동일하다.

오디오 콘텐츠 수정/soft delete

PUT /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}

Content-Type: multipart/form-data

Form fields:

{
  "coverImage": "File | optional",
  "audioFile": "File | optional",
  "request": {
    "title": "밤 산책 수정",
    "description": "수정된 설명",
    "price": 1200,
    "isAdult": false,
    "isActive": false,
    "releaseDateUtc": null,
    "seriesIds": [701]
  }
}

Response data: 오디오 콘텐츠 상세와 동일하다.

시리즈 목록

GET /api/v2/admin/ai-characters/{characterId}/series?page=0&size=20

Query parameters:

{
  "page": 0,
  "size": 20
}

Response data:

{
  "totalCount": 1,
  "page": 0,
  "size": 20,
  "hasNext": false,
  "items": [
    {
      "seriesId": 701,
      "title": "루나의 밤",
      "introduction": "밤을 주제로 한 시리즈",
      "coverImageUrl": "https://cdn.example.com/series/701.png",
      "genreId": 3,
      "isAdult": false,
      "state": "OPEN",
      "isActive": true,
      "orders": 1
    }
  ]
}

시리즈 상세

GET /api/v2/admin/ai-characters/{characterId}/series/{seriesId}

Response data:

{
  "seriesId": 701,
  "title": "루나의 밤",
  "introduction": "밤을 주제로 한 시리즈",
  "coverImageUrl": "https://cdn.example.com/series/701.png",
  "publishedDaysOfWeek": ["MONDAY", "WEDNESDAY"],
  "genreId": 3,
  "keywords": ["밤", "산책"],
  "isAdult": false,
  "state": "OPEN",
  "isActive": true,
  "writer": "루나",
  "studio": "소다라이브",
  "orders": 1
}

시리즈 생성

POST /api/v2/admin/ai-characters/{characterId}/series

Content-Type: multipart/form-data

Form fields:

{
  "image": "File | optional",
  "request": {
    "title": "루나의 밤",
    "introduction": "밤을 주제로 한 시리즈",
    "publishedDaysOfWeek": ["MONDAY", "WEDNESDAY"],
    "genreId": 3,
    "keywords": ["밤", "산책"],
    "isAdult": false,
    "state": "OPEN",
    "writer": "루나",
    "studio": "소다라이브"
  }
}

Response data: 시리즈 상세와 동일하다.

시리즈 수정/soft delete

PUT /api/v2/admin/ai-characters/{characterId}/series/{seriesId}

Content-Type: multipart/form-data

Form fields:

{
  "image": "File | optional",
  "request": {
    "title": "루나의 밤 수정",
    "introduction": "수정된 소개",
    "publishedDaysOfWeek": ["FRIDAY"],
    "genreId": 3,
    "keywords": ["밤"],
    "isAdult": false,
    "state": "OPEN",
    "isActive": false,
    "writer": "루나",
    "studio": "소다라이브"
  }
}

Response data: 시리즈 상세와 동일하다.

시리즈 콘텐츠 조회

GET /api/v2/admin/ai-characters/{characterId}/series/{seriesId}/contents?search=밤&page=0&size=20

Query parameters:

{
  "search": "밤",
  "page": 0,
  "size": 20
}

Response data:

{
  "totalCount": 1,
  "page": 0,
  "size": 20,
  "hasNext": false,
  "items": [
    {
      "contentId": 501,
      "title": "밤 산책",
      "coverImageUrl": "https://cdn.example.com/audio/501-cover.png",
      "isAdult": false,
      "orders": 1
    }
  ]
}

시리즈 콘텐츠 연결

POST /api/v2/admin/ai-characters/{characterId}/series/{seriesId}/contents

Request body:

{
  "contentIds": [501, 502]
}

Response data: 시리즈 상세와 동일하다.

시리즈 콘텐츠 연결 해제

DELETE /api/v2/admin/ai-characters/{characterId}/series/{seriesId}/contents/{contentId}

Request body 없음.

Response data: 시리즈 상세와 동일하다.

시리즈 순서 변경

PUT /api/v2/admin/ai-characters/{characterId}/series/orders

Request body:

{
  "seriesIds": [701, 702, 703]
}

Response data: 시리즈 목록과 동일하다.

커뮤니티 게시글 목록

GET /api/v2/admin/ai-characters/{characterId}/community-posts?page=0&size=20

Query parameters:

{
  "page": 0,
  "size": 20
}

Response data:

{
  "totalCount": 1,
  "page": 0,
  "size": 20,
  "hasNext": false,
  "items": [
    {
      "postId": 801,
      "content": "오늘의 소식입니다.",
      "imageUrl": "https://cdn.example.com/community/801.png",
      "audioSignedUrl": null,
      "price": 0,
      "isAdult": false,
      "isFixed": true,
      "fixedAtUtc": "2026-07-24T00:00:00Z",
      "isActive": true,
      "createdAtUtc": "2026-07-24T00:00:00Z"
    }
  ]
}

커뮤니티 게시글 등록

POST /api/v2/admin/ai-characters/{characterId}/community-posts

Content-Type: multipart/form-data

Form fields:

{
  "image": "File | optional",
  "audioFile": "File | optional",
  "request": {
    "content": "오늘의 소식입니다.",
    "price": 0,
    "isAdult": false,
    "isFixed": false,
    "isActive": true
  }
}

Response data:

{
  "postId": 801,
  "content": "오늘의 소식입니다.",
  "imageUrl": "https://cdn.example.com/community/801.png",
  "audioSignedUrl": null,
  "price": 0,
  "isAdult": false,
  "isFixed": false,
  "fixedAtUtc": null,
  "isActive": true,
  "createdAtUtc": "2026-07-24T00:00:00Z",
  "updatedAtUtc": "2026-07-24T00:00:00Z"
}

커뮤니티 게시글 수정/고정/soft delete

PUT /api/v2/admin/ai-characters/{characterId}/community-posts/{postId}

Content-Type: multipart/form-data

Form fields:

{
  "image": "File | optional",
  "audioFile": "File | optional",
  "request": {
    "content": "수정된 소식입니다.",
    "price": 0,
    "isAdult": false,
    "isFixed": false,
    "isActive": false
  }
}

Response data: 커뮤니티 게시글 등록 응답과 동일하다. isActive=false이면 isFixed=false, fixedAtUtc=null이어야 한다.

FanTalk 답변 작성

POST /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies

Request body:

{
  "content": "응원해줘서 고마워요!"
}

Response data:

{
  "fanTalkId": 901,
  "replyId": 902,
  "creatorMemberId": 9001,
  "content": "응원해줘서 고마워요!",
  "createdAtUtc": "2026-07-24T00:00:00Z"
}

Phase 1: 공통 ADMIN 인증과 AI 캐릭터 target resolver 기반

공통 Task 실행 규칙

  • 각 구현 Task는 RED: 실패 테스트 작성/실패 확인, GREEN: 최소 구현/통과 확인, REFACTOR: 정리/회귀 확인을 포함한다.
  • 테스트 작성이 현실적으로 불가능한 검증 전용 Task는 TDD 예외 사유대체 검증 방법을 Task에 명시한다.
  • 기존 business method 재사용 전 특성화/회귀 테스트는 신규 v2 use-case RED 테스트와 분리한다. 특성화 테스트는 기존 legacy/creator-admin 구현을 대상으로 먼저 통과해 baseline을 고정하고, 그 결과를 신규 v2 RED 기대값으로 옮긴다.
  • Phase 2~6의 모든 신규 오류 RED는 정확한 비2xx status, ApiResponse.error shape, Accept-Language KO/EN/JA message를 함께 검증한다. 새 오류 분기는 status, message key, 3개 언어 message, 테스트가 모두 정해지기 전 완료 처리하지 않는다.

목표

모든 신규 API가 공유할 JWT ADMIN + 현재 DB ADMIN 이중 인가, prefix 전용 오류 envelope/i18n, characterId 기반 target 해석, CREATOR + AI_CHARACTER 불변식, ownership no-side-effect 검증 기반을 만든다.

범위와 비범위

  • 포함: 신규 v2 admin package 골격, JWT role + 현재 DB role 이중 인가, prefix 전용 security/application 오류 처리, 공통 target resolver/use-case, no-side-effect 테스트 fixture.
  • 제외: 캐릭터/콘텐츠/시리즈/커뮤니티/FanTalk 실제 domain 기능 구현.

선행 Phase 및 의존성

  • 선행 Phase 없음.
  • 기존 ChatCharacter.creatorMember, MemberRole.CREATOR, MemberKind.AI_CHARACTER가 존재해야 한다.

API endpoint와 request/response contract

  • 모든 후속 endpoint에 공통 적용한다.
  • target resource request는 JWT ROLE_ADMIN과 현재 DB Member.role == ADMIN을 모두 만족하는 principal과 path/query/body의 characterId를 받는다. 캐릭터 목록/검색과 생성은 Endpoint Contract Summary의 예외를 따른다.
  • resolver output은 내부 전용 AiCharacterAdminTarget(characterId, chatCharacter, creatorMember)로 계획한다.
  • Phase 1 API 실패 응답은 Endpoint Contract Summary의 400/401/403/404/405/406/415/500과 ApiResponse.error/KO·EN·JA 계약을 따르며 domain side effect가 없어야 한다. Spring CORS 정책 거부 403 body는 해당 envelope 계약의 예외다.

entity, repository, service 변경

  • Entity: 변경 없음.
  • Repository: ChatCharacterRepositorycharacterIdcreatorMemberLEFT JOIN FETCH하는 query를 추가해 반환 직후 연관 객체가 초기화되도록 한다.
  • Service/Application: AiCharacterAdminTargetResolver 생성.
  • Controller/Facade: kr.co.vividnext.sodalive.v2.api.admin.aicharacter 하위 신규 controller/facade 패키지 생성.

DB migration

  • 없음. 신규 DDL, 신규 migration 파일을 만들지 않는다.

transaction과 concurrency 고려사항

  • target resolver는 read-only transaction으로 동작한다.
  • 후속 write use-case는 target 검증을 write transaction 시작부에서 먼저 수행한다.
  • target 검증 실패는 transaction rollback-only가 필요 없는 사전 실패로 끝나야 한다.

보안 및 개인정보 위험

  • JWT ROLE_ADMIN만 신뢰하지 않고 TokenProvider가 이미 조회한 MemberAdapter.member.role을 신규 prefix 인가에서 함께 확인한다. TokenProvider의 전역 authority 계산은 변경하지 않는다.
  • JWT ADMIN + 현재 DB 비ADMIN stale claim과 MemberAdapter가 아닌 principal은 target resolver 실행 전에 403으로 거부한다.
  • creatorMember를 인증 principal로 교체하지 않는다.
  • creatorMemberId를 관리자 입력값으로 신뢰하지 않는다.
  • Phase 1 resolver의 잘못된 target 요청은 Hibernate 통계로 DB insert/update/delete 0건을 검증한다. resolver는 S3, 외부 API, 이벤트 발행 의존성을 갖지 않으며 Phase 2~6 write slice에서 각 외부 부작용 0건을 별도 검증한다.

acceptance criteria

  • JWT ADMIN + 현재 DB ADMIN 요청만 유효한 AI character target을 resolver로 해석할 수 있다.
  • JWT 없음·잘못됨·만료·폐기는 401, JWT 비ADMIN 또는 현재 DB 비ADMIN은 403, character 미존재·creatorMember 미존재·target role/memberKind 불일치는 400이다.
  • 위 API 오류는 모두 ApiResponse.errorAccept-Language에 따른 KO/EN/JA message를 반환한다. Spring CORS 정책 거부 403 body는 해당 envelope 계약의 예외다.
  • 405는 Allow, 415는 Accept header를 유지하고, 지원하지 않는 응답 media type은 406 common.error.invalid_request, MissingPathVariableException은 500 common.error.unknown으로 반환한다.
  • Phase 1 resolver 실패는 DB insert/update/delete가 0건이다. S3, 외부 API, 이벤트 부작용은 해당 의존성이 처음 도입되는 Phase 2~6 write slice에서 검증한다.

targeted test

  • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminTargetResolverTest.kt
  • Integration Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminTargetResolverIntegrationTest.kt
  • RED: resolver 미구현 상태에서 shouldResolveAiCharacterCreatorMemberForAdminTarget, shouldRejectMissingCharacterWithoutSideEffect, shouldRejectHumanCreatorMemberWithoutSideEffect 테스트를 작성해 실패를 확인한다.
  • GREEN: resolver와 최소 repository query를 구현해 resolver 테스트를 통과시킨다.
  • Controller 권한 테스트: AiCharacterAdminAuthorizationTest에서 JWT role × 현재 DB role 매트릭스와 stale claim을 고정하고, Phase 2~6 controller test에서 신규 endpoint 전체가 같은 이중 인가를 공유하는지 검증한다.
  • 오류 계약 테스트: AiCharacterAdminErrorContractTest에서 400/401/403/404/405/406/415/500, 405 Allow, 415 Accept, JWT filter 예외, KO/EN/JA body와 legacy fallback을 검증한다. CORS는 미매핑 fallback뿐 아니라 실제 mapped endpoint와 /admin/member/login, /member/logout의 허용·거부 Origin/preflight를 검증하고, 정책 거부 403 body에는 envelope를 요구하지 않는다.
  • Run: ./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'

전체 회귀 테스트 영향

  • 기존 endpoint를 건드리지 않아야 한다.
  • 신규 package 추가로 component scan과 security 설정 충돌이 없어야 한다.

rollback 전략

  • 신규 v2 admin controller/facade/resolver/error/security package와 관련 테스트를 제거한다.
  • SecurityConfig.kt, WebConfig.kt, ExceptionHandlerFilter.kt, TokenProvider.kt, ChatCharacterRepository.kt의 Phase 1 변경을 함께 되돌린다.
  • DB rollback은 없다.

권장 commit 경계

  • feat: add ai character admin target resolver

  • Task 1.1: resolver RED 테스트 작성

    • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminTargetResolverTest.kt
    • RED: 유효 target, missing character, wrong role, wrong memberKind, missing creatorMember, cross-owner fixture를 먼저 작성하고 실패를 확인한다.
    • GREEN: 구현 전 Task라 production code를 변경하지 않는다.
    • REFACTOR: fixture 중복만 정리하고 테스트 의미는 약화하지 않는다.
    • Verify: ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminTargetResolverTest
    • 검증 기록: 무엇: resolver RED 테스트. 왜: resolver와 findByIdWithCreatorMember 미구현을 실제 실패로 고정하기 위해. 어떻게: 위 명령을 실행했다. 결과: compileTestKotlin이 두 미구현 항목으로 실패했다.
  • Task 1.2: resolver 최소 구현

    • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/application/AiCharacterAdminTargetResolver.kt
    • Modify: src/main/kotlin/kr/co/vividnext/sodalive/chat/character/repository/ChatCharacterRepository.kt
    • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminTargetResolverIntegrationTest.kt
    • RED: Task 1.1 실패 테스트가 같은 실패 이유로 남아 있음을 확인한다.
    • GREEN: characterIdChatCharactercreatorMember를 조회하고 strict validation을 적용한다.
    • REFACTOR: resolver/repository naming과 예외 메시지를 인접 v2 관례에 맞추고 회귀 테스트를 재실행한다.
    • Verify: ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminTargetResolverTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminTargetResolverIntegrationTest
    • 검증 기록: 무엇: resolver 최소 구현과 repository/ownership 통합 검증. 왜: mock 기반 테스트만으로 실제 조회 동작을 확인할 수 없었기 때문이다. 어떻게: ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminTargetResolverTest./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminTargetResolverIntegrationTest를 실행했다. 결과: 두 실행 모두 BUILD SUCCESSFUL이었다.
  • Task 1.3: ADMIN 권한 controller smoke 테스트

    • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAuthorizationTest.kt
    • RED: 최소 sample controller 또는 Phase 2 첫 controller 기준 anonymous/non-admin 접근 실패와 admin 접근 성공 테스트를 먼저 작성해 실패를 확인한다.
    • GREEN: 공통 security 설정 또는 controller annotation을 최소 구현해 테스트를 통과시킨다.
    • REFACTOR: Phase 2~6의 모든 신규 endpoint controller test가 같은 권한 매트릭스를 따르도록 test helper를 정리한다.
    • Verify: ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest
    • 검증 기록: 무엇: sample route ADMIN 권한 smoke. 왜: 신규 prefix의 ADMIN rule 적용 전후를 확인하기 위해. 어떻게: 위 명령으로 권한 테스트를 실행했다. 결과: 적용 전 권한 실패를 확인했고, /api/v2/admin/ai-characters/** ADMIN rule 적용 후 BUILD SUCCESSFUL이었다.
  • Task 1.4: JWT claim + 현재 DB ADMIN 이중 인가

    • Confirm: src/main/kotlin/kr/co/vividnext/sodalive/jwt/TokenProvider.kt, src/main/kotlin/kr/co/vividnext/sodalive/member/MemberAdapter.kt

    • Modify: src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt, src/main/kotlin/kr/co/vividnext/sodalive/jwt/TokenProvider.kt

    • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAuthorizationTest.kt, src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminLoginJwtIntegrationTest.kt, src/test/kotlin/kr/co/vividnext/sodalive/jwt/TokenProviderTest.kt

    • RED: UsernamePasswordAuthenticationToken(MemberAdapter(currentMember), token, jwtAuthorities) fixture로 JWT ADMIN + DB ADMIN 200, JWT 비ADMIN + DB ADMIN 403, JWT ADMIN + DB 비ADMIN stale claim 403, ADMIN authority + 비MemberAdapter principal 403을 작성한다. stale claim은 현재 구현에서 200이므로 이 실패를 확인한다.

    • GREEN: 신규 prefix의 matcher 하나에서 아래 세 조건을 AND로 묶고 MemberAdapter, legacy matcher는 변경하지 않는다. TokenProvider는 JWT subject 누락/비숫자 값이 500으로 누수되지 않도록 common.error.bad_credentials로만 보정하며, 전역 authority 계산과 token 저장소 검증 의미는 변경하지 않는다.

      .antMatchers("/api/v2/admin/ai-characters/**")
      .access(
          "hasRole('ADMIN') and " +
              "principal instanceof T(kr.co.vividnext.sodalive.member.MemberAdapter) and " +
              "principal.member.role == T(kr.co.vividnext.sodalive.member.MemberRole).ADMIN"
      )
      
    • REFACTOR: production SecurityConfig matcher의 비확산은 full-context AiCharacterAdminAuthorizationTest/phase1-legacy-sample로 확인한다. 기존 AdminAgentReadControllerSecurityTest, AdminContentControllerSecurityTest는 각자의 자체 security chain을 사용하는 controller 회귀 증거로 구분하며 production matcher 비확산의 증거로 해석하지 않는다.

    • Verify: ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminLoginJwtIntegrationTest --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest

    • 검증 기록: 무엇: JWT claim과 현재 DB ADMIN 이중 인가. 왜: stale ADMIN claim과 비MemberAdapter principal이 허용되면 안 되기 때문이다. 어떻게: ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest를 실행했다. 결과: 6개 중 해당 2개가 403 기대 대비 200으로 실패한 RED를 확인했고, 세 조건을 AND로 적용한 뒤 같은 명령이 BUILD SUCCESSFUL이었다.

  • Task 1.5: 신규 prefix 오류 envelope/status/i18n 계약

    • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/error/AiCharacterAdminApiException.kt
    • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/error/AiCharacterAdminExceptionHandler.kt
    • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/error/AiCharacterAdminErrorResponseWriter.kt
    • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/security/AiCharacterAdminSecurityErrorHandler.kt
    • Modify: src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt, src/main/kotlin/kr/co/vividnext/sodalive/configs/WebConfig.kt, src/main/kotlin/kr/co/vividnext/sodalive/common/ExceptionHandlerFilter.kt, src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/application/AiCharacterAdminTargetResolver.kt
    • Confirm: src/main/kotlin/kr/co/vividnext/sodalive/i18n/Lang.kt, src/main/kotlin/kr/co/vividnext/sodalive/i18n/SodaMessageSource.kt
    • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt, src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAccessDeniedErrorContractTest.kt, src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminTargetResolverTest.kt
    • RED: legacy anonymous/revoked-token/지원하지 않는 method 오류의 현재 status/body를 먼저 통과하는 특성화 baseline으로 고정한다. 신규 prefix의 anonymous·잘못된 JWT·폐기 JWT 401, JWT/DB role 불충족 403, invalid request/target 400, 미매핑 경로 404, 지원하지 않는 method 405, 지원하지 않는 media type 415, 예상하지 못한 controller/JWT filter 오류 500을 KO/EN/JA로 parameterized 검증한다. 확정된 캐릭터 관리자 Origin의 404/405/415와 실제 JWT header를 요청하는 미매핑 경로 preflight에 CORS 응답 header가 적용되는지 확인한다. 허용된 Origin의 API 오류는 success=false, localized message, JSON content type을 확인하고, 기존 범용 관리자와 creator frontend Origin의 CORS 정책 거부는 403과 CORS 허용 header 부재만 확인한다. 현재 sendError, hardcoded KO, 기본 Spring error body 때문에 실패하는 것을 확인한다.
    • GREEN: AiCharacterAdminErrorResponseWriterLang.fromAcceptLanguageSodaMessageSourceApiResponse.error를 만들고 JSON을 기록하게 한다. AiCharacterAdminSecurityErrorHandler는 401 common.error.bad_credentials와 403 common.error.access_denied를 위 writer에 위임한다. SecurityConfig는 신규 prefix matcher에만 이 handler를 선택하고 기존 JwtAuthenticationEntryPoint/JwtAccessDeniedHandler를 fallback으로 유지한다. ExceptionHandlerFilter도 신규 prefix에서 잡은 JWT 예외만 신규 401 handler로 위임하고 legacy branch는 그대로 둔다.
    • GREEN: target resolver는 400 + common.error.invalid_request를 가진 AiCharacterAdminApiException을 던진다. URI matcher로 신규 prefix에만 적용되는 AiCharacterAdminExceptionHandler는 controller 선택 전 오류까지 처리해 명시적 API 예외, request binding 400, method 405, media type 415, controller AccessDeniedException 403, 예상하지 못한 오류 500을 각각 정확한 status와 localized ApiResponse.error로 반환한다. 낮은 우선순위의 prefix fallback handler는 미매핑 경로를 404로 반환하고 캐릭터 관리자 Origin 전용 CORS 설정을 적용한다. 단순 SodaException 교체나 전역 SodaExceptionHandler 변경은 하지 않는다.
    • GREEN: ExceptionHandlerFilter가 잡은 폐기 JWT 등 알려진 인증 실패만 401로 보내고, JWT 처리 중 예상하지 못한 예외는 위 prefix exception handler에 위임해 500 common.error.unknown으로 반환한다.
    • REFACTOR: raw message key 노출, MVC LangInterceptor 의존, legacy 오류 응답 변경이 없는지 확인하고 신규/legacy contract 테스트를 함께 재실행한다.
    • Verify: ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAccessDeniedErrorContractTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminTargetResolverTest
    • 검증 기록: 무엇: 신규 prefix 오류 envelope/status/i18n 계약. 왜: 신규 401/403/400/500 응답이 기존 body·message와 달랐기 때문이다. 어떻게: ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest를 실행했다. 결과: 최초 15개 중 13개의 status/content type/message 불일치 RED를 확인했고, prefix 기반 handler와 writer 적용 후 44개 invocation이 모두 통과해 BUILD SUCCESSFUL이었다.
  • Task 1.6: Phase 1 코드 리뷰 후속 보완

    • Modify: docs/20260724_AI캐릭터_관리자_API/prd.md, docs/20260724_AI캐릭터_관리자_API/plan-task.md, src/main/kotlin/kr/co/vividnext/sodalive/configs/WebConfig.kt, src/main/kotlin/kr/co/vividnext/sodalive/jwt/TokenProvider.kt, src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/error/AiCharacterAdminExceptionHandler.kt, src/test/kotlin/kr/co/vividnext/sodalive/jwt/TokenProviderTest.kt, src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAuthorizationTest.kt, src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt, src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAccessDeniedErrorContractTest.kt, src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminLoginJwtIntegrationTest.kt
    • RED: 서명과 subject는 유효하지만 auth claim이 누락, 공백 또는 빈 분할 항목을 포함한 JWT가 common.error.bad_credentials로 거부되지 않는 테스트를 작성하고 실패를 확인한다.
    • GREEN: TokenProviderauth claim을 authority로 변환하기 전에 문자열 타입, 공백 여부와 각 분할 항목을 검증하고 잘못된 claim은 common.error.bad_credentials로 거부하는 최소 구현을 적용한다.
    • REFACTOR: Phase 1 테스트 전용 controller를 각 테스트 클래스 내부 nested class로 이동하고 @TestComponent로 component scan에서 제외한 뒤 해당 테스트 context에만 명시적으로 import한다. 테스트 fixture 범위만 바꾸는 구조 정리이므로 별도 동작 RED 대신 targeted/full-context 회귀와 application context 시작 성공으로 검증한다.
    • CORS: 현재 코드의 캐릭터 관리자 Origin http://localhost:8888, https://test-character-admin.sodalive.net, https://character-admin.sodalive.net만 허용하고 기존 범용 관리자/creator Origin은 거부하는 정책으로 PRD/plan과 CORS 계약 테스트를 동기화한다.
    • 2차 리뷰 RED: 신규 prefix의 실제 mapped endpoint와 공유 /admin/member/login, /member/logout에서 캐릭터 관리자 Origin 요청/preflight가 허용되지 않는 실패를 확인한다. 응답 media type 협상 실패 406, 405 Allow header, 415 Accept header, MissingPathVariableException 500 계약 테스트를 추가해 현재 동작과의 불일치를 확인한다.
    • 2차 리뷰 GREEN: 신규 prefix는 캐릭터 관리자 Origin만 허용하는 기존 정책을 유지하고, 두 공유 인증 경로에만 기존 전역 Origin과 캐릭터 관리자 Origin의 합집합을 적용한다. AiCharacterAdminExceptionHandler는 406을 common.error.invalid_request, MissingPathVariableException을 500 common.error.unknown으로 분류하고 405/415 표준 header를 보존한다. Spring CORS 정책 거부 403 body는 localized ApiResponse.error envelope 계약에서 제외한다.
    • 2차 리뷰 REFACTOR: 실제 mapped endpoint, 두 공유 인증 경로와 미매핑 fallback의 허용·거부 Origin/preflight를 함께 회귀하고, path-specific CORS 확장이 다른 legacy/public 경로로 확산되지 않았는지 확인한다.
    • Verify: ./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'
    • 검증 기록: 무엇: malformed auth claim, CORS, HTTP 오류, 테스트 fixture 격리 후속 보완. 왜: claim 누수와 실제 mapped/shared path CORS·405/406/415/500 계약 누락을 해소하기 위해. 어떻게: ./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' --tests kr.co.vividnext.sodalive.admin.partner.agent.read.AdminAgentReadControllerSecurityTest --tests kr.co.vividnext.sodalive.admin.content.AdminContentControllerSecurityTest를 실행했다. 결과: malformed claim 7개 invocation RED 후 9개가 통과했고, fallback 보완 후 targeted+legacy 136개가 모두 통과해 BUILD SUCCESSFUL이었다. 추가 로그인/로그아웃 CORS 보완 뒤 AiCharacterAdminLoginJwtIntegrationTestBUILD SUCCESSFUL이었다.
  • Task 1.7: Phase 1 후속 리뷰 전체 반영

    • Modify: src/main/kotlin/kr/co/vividnext/sodalive/configs/SecurityConfig.kt, src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/security/AiCharacterAdminSecurityErrorHandler.kt
    • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt, src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAuthorizationTest.kt, src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAccessDeniedErrorContractTest.kt, src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminLoginJwtIntegrationTest.kt, src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminTargetResolverIntegrationTest.kt
    • RED: full-context AiCharacterAdminLoginJwtIntegrationTest에 허용 Origin의 PROPFIND 400/i18n/CORS, raw double slash 400/CORS, 미허용 Origin 403/Access-Control-Allow-Origin 부재와 legacy RequestRejectedException 유지 계약을 추가하고, 신규 prefix 세 요청이 RequestRejectedException으로 실패하며 legacy fallback은 통과하는 production-before RED를 확인한다.
    • GREEN: SecurityConfig가 기존 AiCharacterAdminSecurityErrorHandler를 global RequestRejectedHandler로 등록하고, handler는 신규 prefix에만 400/CORS 계약을 적용한다. setUnsafeAllowAnyHttpMethod(true) 없이 허용된 캐릭터 관리자 Origin에는 CORS header를 포함한 400 common.error.invalid_request와 현지화된 ApiResponse.error를, 미허용 Origin에는 기존 Spring CORS 정책과 같은 body 계약 없는 403을 반환한다. legacy/public은 DefaultRequestRejectedHandler에 위임해 기존 RequestRejectedException 동작을 유지한다. Spring 5.3의 비표준 method enum 한계는 CORS 검사 request에만 GET wrapper를 사용해 우회하고 실제 firewall method 허용 범위는 확장하지 않는다.
    • REFACTOR: AiCharacterAdminErrorContractTest, AiCharacterAdminAuthorizationTest, AiCharacterAdminAccessDeniedErrorContractTest를 production @SpringBootTest + MockMvc + EmbeddedRedis full context로 전환한다. 기존 ErrorContract의 표준 POST -> GET-only mapping 405, Allow: GET, CORS 계약도 full context에서 회귀한다. AiCharacterAdminLoginJwtIntegrationTestMemberTokenRepository.deleteAll()@AfterEach에 실행해 Redis token fixture를 cleanup한다. AiCharacterAdminTargetResolverIntegrationTest는 repository 조회 직후 Hibernate.isInitialized(found.creatorMember)를 단언해 production LEFT JOIN FETCH가 실제 회귀 방지에 필요함을 고정한다. Phase 1 production에는 Bean Validation provider를 추가하지 않고, MethodArgumentNotValidException은 test-only endpoint에서 의존성 없이 직접 던져 handler 분기를 검증한다. 이 Task의 실행 명령과 결과는 먼저 이 Task 아래에 기록하고, phase/전체 aggregate만 문서 하단 검증 기록에 누적한다.
    • Verify: ./gradlew test --rerun-tasks --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' --tests kr.co.vividnext.sodalive.admin.partner.agent.read.AdminAgentReadControllerSecurityTest --tests kr.co.vividnext.sodalive.admin.content.AdminContentControllerSecurityTest
    • 검증 기록(RED): 무엇: 신규 prefix firewall와 legacy fallback 계약. 왜: production firewall 거부가 신규 API 오류 계약 밖으로 탈출하는지 확인하기 위해. 어떻게: ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminLoginJwtIntegrationTest를 실행했다. 결과: 17개 중 신규 prefix 3개가 RequestRejectedException으로 실패했고 legacy fallback 테스트는 통과했다.
    • 검증 기록(GREEN): 무엇: prefix-aware global RequestRejectedHandler. 왜: 신규 prefix만 400/i18n/CORS로 변환하고 legacy/public 동작을 보존하기 위해. 어떻게: RED와 동일한 명령을 실행했다. 결과: 17/17, BUILD SUCCESSFUL을 확인했다.
    • 검증 기록(REFACTOR): 무엇: 네 core controller security/error 클래스의 production full-context 계약과 Redis fixture 격리. 왜: slice 설정이 아닌 실제 security/CORS/filter 구성을 검증하기 위해. 어떻게: ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAccessDeniedErrorContractTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminLoginJwtIntegrationTest를 실행했다. 결과: 115/115, failure/error 0, BUILD SUCCESSFUL을 확인했다.
    • 검증 기록(FETCH JOIN): 무엇: repository 조회 직후 creatorMember 초기화의 non-vacuous 회귀 계약. 왜: resolver transaction 내부 접근만으로 fetch join 누락이 가려지는 것을 막기 위해. 어떻게: production query의 LEFT JOIN FETCH를 임시로 LEFT JOIN으로 바꾸고 ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminTargetResolverIntegrationTest를 실행했다. 결과: 4개 중 fetch 테스트 1개가 line 66에서 실패해 BUILD FAILED(38초)을 확인했다. 즉시 LEFT JOIN FETCH를 복원했고, 복원 상태는 하단 최신 canonical 154/154에 포함되어 통과했다.
    • 검증 기록(직접 400 분기): 무엇: malformed JSON의 HttpMessageNotReadableException, test-only endpoint에서 의존성 없이 직접 던진 MethodArgumentNotValidException, 실제 multipart 필수 part 누락의 MissingServletRequestPartException 각 KO/EN/JA 총 9 invocation. 왜: exact resolvedException 타입과 localized 400 envelope를 각 handler 분기에서 직접 고정하기 위해. 어떻게: ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest' --rerun-tasks를 실행했다. 결과: 98/98, failure/error/skipped 0, BUILD SUCCESSFUL을 확인했고 production/build dependency 변경은 없었다.

Phase 2: AI 캐릭터 관리 vertical slice

목표

AI 캐릭터 목록/검색/상세/생성/수정/비활성화를 신규 ADMIN v2 API로 제공하고 레거시 관리자 동작 parity를 고정한다.

범위와 비범위

  • 포함: character CRUD API, 외부 캐릭터 API 연동, 이미지 저장, 원작 연결, 언어 감지/번역 이벤트, creatorMember 생성/표시 정보 동기화 parity.
  • 제외: hard delete, cascade delete, 기존 legacy admin endpoint 변경.

선행 Phase 및 의존성

  • Phase 1 resolver와 ADMIN 권한 기반이 선행되어야 한다.
  • 기존 ChatCharacterService, ChatCharacterCreatorMemberService, image/S3/event 관련 컴포넌트 동작을 특성화해야 한다.

API endpoint와 request/response contract

  • GET /api/v2/admin/ai-characters?search=&page=&size= -> AiCharacterAdminListResponse(totalCount, items, page, size, hasNext)
  • GET /api/v2/admin/ai-characters/{characterId} -> AiCharacterAdminDetailResponse
  • POST /api/v2/admin/ai-characters multipart image?, request: CreateAiCharacterAdminRequest -> detail
  • PUT /api/v2/admin/ai-characters/{characterId} multipart image?, request: UpdateAiCharacterAdminRequest -> detail
  • UpdateAiCharacterAdminRequest.isActive=false는 soft delete 의미다.

entity, repository, service 변경

  • Entity: 변경 없음.
  • Repository: character 목록/검색용 query adapter 추가 가능.
  • Service: 신규 AiCharacterAdminCharacterFacade/ApplicationService에서 기존 하위 service를 선택적으로 재사용한다.
  • DTO: 신규 admin v2 전용 request/response DTO 생성.

DB migration

  • 없음.

transaction과 concurrency 고려사항

  • 생성/수정은 단일 transaction에서 character, relation, creatorMember 표시 정보 동기화를 완료한다.
  • 외부 API/S3/event 순서는 기존 레거시 동작 특성화 결과를 따른다.
  • 중복 이름 검증은 기존 정책을 유지하며 동시 생성 시 DB/서비스 레벨 실패가 부분 저장을 남기지 않아야 한다.

보안 및 개인정보 위험

  • 목록/상세 응답에 AI creatorMember 로그인 credential, token, private storage path를 노출하지 않는다.
  • ADMIN 외 접근을 허용하지 않는다.

acceptance criteria

  • 목록/검색/상세는 AI 캐릭터 관리자 화면에 필요한 필드를 반환한다.
  • 생성/수정은 레거시 관리자와 동일한 business side effect를 만든다.
  • 비활성화는 isActive=false이며 row와 연결 Member/콘텐츠를 삭제하지 않는다.

targeted test

  • Characterization: LegacyChatCharacterAdminCharacterizationTest에서 기존 character admin create/update/soft delete 결과와 외부 API·S3·event failure order, transaction/compensation 계약을 통과 상태로 고정한다.
  • V2 RED/GREEN: AiCharacterAdminCharacterControllerTest, AiCharacterAdminCharacterServiceTest.
  • Run: ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*'

전체 회귀 테스트 영향

  • 기존 AdminChatCharacterController 및 public character 조회 응답이 변하지 않아야 한다.
  • creatorMember 동기화 기존 테스트가 계속 통과해야 한다.

rollback 전략

  • 신규 character admin v2 route/facade만 제거한다.
  • 이미 생성/수정된 정상 데이터는 기존 관리자와 같은 domain 데이터라 별도 schema rollback이 없다.

권장 commit 경계

  • feat: add ai character admin character slice

  • Task 2.1: 기존 character parity 특성화 baseline 고정

    • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/LegacyChatCharacterAdminCharacterizationTest.kt
    • CHARACTERIZE: 기존 character admin 구현을 대상으로 중복 이름, 외부 API·S3·event 호출/실패 순서와 transaction/compensation, original work 연결, 언어 감지/번역 이벤트, creatorMember 표시 정보 동기화, isActive=false 및 연결 Member/콘텐츠 미삭제 결과를 고정한다.
    • BASELINE: 신규 v2 production code 변경 전에 특성화 테스트가 기존 구현을 대상으로 통과함을 확인한다.
    • REFACTOR: fixture와 assertion naming만 정리하고 parity baseline은 변경하지 않는다.
    • Verify: ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.LegacyChatCharacterAdminCharacterizationTest
  • Task 2.2: character controller/facade/DTO 구현

    • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/*
    • RED: Task 2.1 baseline에서 옮긴 AiCharacterAdminCharacterServiceTest와 controller 권한/페이지네이션 경계 테스트를 작성하고 신규 v2 미구현으로 실패함을 확인한다.
    • RED: 현재 신규 dependency 금지 제약에 따라 기존 Spring binding 또는 수동 validation 전략을 우선하고, 실제 DTO의 invalid 요청 통합 계약을 추가한다. Bean Validation provider가 반드시 필요하면 구현 전에 PRD/계획과 dependency 허용 범위를 명시적으로 변경하고 승인을 받는다.
    • GREEN: endpoint contract summary의 character endpoint를 구현하고 목록/검색 page/size 기본값·최소·최대 보정을 적용한다.
    • REFACTOR: 신규 DTO가 legacy/public DTO를 외부 계약으로 재노출하지 않는지 확인하고 회귀 테스트를 재실행한다.
    • Verify: ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*'

Phase 3: 오디오 콘텐츠 관리와 signed URL vertical slice

목표

선택한 AI 캐릭터 소유 오디오 콘텐츠 목록/검색/상세/생성/수정/soft delete와 관리자 재생용 signed URL을 제공한다.

범위와 비범위

  • 포함: 콘텐츠 owner 검증, 기존 파일 처리/가격/공개/예약/번역/알림 parity, AudioContentCloudFront 재사용, private path 비노출.
  • 제외: 콘텐츠 구매/좋아요/댓글, content upload/processing pipeline 변경, community audio 30분 정책 통합.

선행 Phase 및 의존성

  • Phase 1 target resolver.
  • 콘텐츠 생성/수정/delete 기존 동작 특성화 테스트.

API endpoint와 request/response contract

  • GET /api/v2/admin/ai-characters/{characterId}/audio-contents
  • GET /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}
  • POST /api/v2/admin/ai-characters/{characterId}/audio-contents
  • PUT /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}
  • response item/detail에는 contentId, title, status, isActive, releaseDate, coverImageUrl, audioSignedUrl?, 가격/성인/공개 상태 등 기존 관리자 화면 필드를 포함한다.

entity, repository, service 변경

  • Entity: 변경 없음.
  • Repository: AudioContent owner-scoped query adapter 추가 가능.
  • Service: 신규 content admin application service에서 기존 creator/admin content service를 테스트로 고정 후 선택 위임 또는 최소 추출한다.
  • CloudFront: 기존 AudioContentCloudFront 그대로 주입해 사용한다.

DB migration

  • 없음.

transaction과 concurrency 고려사항

  • write transaction 시작 직후 target과 content.member.id == creatorMember.id를 검증한다.
  • S3 업로드, 이벤트 발행 순서는 기존 동작 parity를 따른다.
  • soft delete는 기존 콘텐츠 삭제 동작처럼 isActive=false, 필요한 경우 releaseDate=null을 유지한다.

보안 및 개인정보 위험

  • private S3 object path, signed key material을 응답하지 않는다.
  • 다른 캐릭터 content ID 접근은 4xx와 no side effect다.

acceptance criteria

  • target 캐릭터 소유 콘텐츠만 조회/변경된다.
  • signed URL 만료 계산이 기존 creator admin policy와 동일하다는 테스트가 있다.
  • signed URL 만료 계산·path 처리에서 실제 기존 구현에서 관찰되는 edge case가 특성화 테스트로 고정된다.
  • invalid target/ownership 실패 시 DB/S3/event side effect가 없다.

targeted test

  • Characterization: LegacyCreatorAdminAudioContentCharacterizationTest, AudioContentCloudFrontCharacterizationTest.
  • V2 RED/GREEN: AiCharacterAdminAudioContentServiceTest, AiCharacterAdminAudioSignedUrlTest.
  • Run: ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*'

전체 회귀 테스트 영향

  • 기존 CreatorAdminContentController, AdminContentController, public content 조회 테스트가 통과해야 한다.

rollback 전략

  • 신규 content v2 admin route/facade를 제거한다.
  • 신규 DDL이 없으므로 schema rollback은 없다.

권장 commit 경계

  • feat: add ai character admin content slice

  • Task 3.1: 기존 콘텐츠와 signed URL 특성화 baseline 고정

    • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/LegacyCreatorAdminAudioContentCharacterizationTest.kt, src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AudioContentCloudFrontCharacterizationTest.kt
    • CHARACTERIZE: 기존 creator/admin content 구현을 대상으로 검증, 파일 처리, 가격, 공개/예약, 번역/알림, soft delete, upload/processing pipeline 결과를 고정한다.
    • CHARACTERIZE: 기존 creator admin signed URL 만료 계산식과 만료 계산·path 처리에서 실제로 관찰되는 edge case, private path 비노출 계약을 고정한다.
    • BASELINE: 신규 v2 production code 변경 전에 두 특성화 테스트가 기존 구현을 대상으로 통과함을 확인한다.
    • REFACTOR: 테스트 fixture만 정리하고 content/signed URL parity baseline은 변경하지 않는다.
    • Verify: ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*CharacterizationTest'
  • Task 3.2: content controller/facade/DTO 구현

    • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/*
    • RED: Task 3.1 baseline에서 옮긴 content parity, cross-character content 접근, signed URL/private path 계약 테스트와 controller 권한/페이지네이션 경계 테스트를 작성하고 신규 v2 미구현으로 실패함을 확인한다.
    • GREEN: content endpoint와 owner-scoped query/write를 구현하고 목록/검색 page/size 기본값·최소·최대 보정을 적용한다.
    • REFACTOR: signed URL/private path mapping과 기존 pipeline 재사용 경계를 정리하고 회귀 테스트를 재실행한다.
    • Verify: ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*'

Phase 4: 시리즈 관리 vertical slice

목표

선택한 AI 캐릭터 소유 시리즈 CRUD, soft delete, 콘텐츠 연결/해제/검색/순서 관리를 owner-safe v2 경로로 제공한다.

범위와 비범위

  • 포함: 시리즈 목록/상세/생성/수정/soft delete, 콘텐츠 연결/해제, 시리즈 콘텐츠 조회/검색, owner-scoped 순서 변경, 기존 creator series behavior parity 특성화.
  • 제외: 기존 CreatorAdminContentSeriesController.updateSeriesOrders(ids) 계약 변경.

선행 Phase 및 의존성

  • Phase 1 target resolver.
  • Phase 3 content owner query를 재사용할 수 있다.
  • 기존 creator series 생성/수정/soft delete, 콘텐츠 연결/해제, 조회/검색, 순서 변경 behavior를 통과하는 특성화 테스트로 먼저 고정해야 한다.

API endpoint와 request/response contract

  • GET/POST/PUT /api/v2/admin/ai-characters/{characterId}/series...
  • GET /series/{seriesId}/contents query: search?, page, size
  • POST /series/{seriesId}/contents request: AddAiCharacterAdminSeriesContentsRequest(contentIds: List<Long>)
  • DELETE /series/{seriesId}/contents/{contentId}
  • PUT /series/orders request: UpdateAiCharacterAdminSeriesOrdersRequest(seriesIds: List<Long>)

entity, repository, service 변경

  • Entity: 변경 없음.
  • Repository: SeriesSeriesContent owner-scoped query/update adapter 추가.
  • Service: 신규 series application service에서 기존 CRUD 핵심을 테스트 후 재사용하되, 모든 write 전에 series/content owner를 검증한다.

DB migration

  • 없음.

transaction과 concurrency 고려사항

  • 순서 변경은 동일 owner의 모든 series ID를 한 transaction에서 검증 후 갱신한다.
  • 콘텐츠 연결/해제는 series와 content owner를 모두 검증한 뒤 수행한다.
  • 동시에 순서 변경 요청이 들어오면 마지막 transaction 결과가 반영되는 기존 단순 정책을 유지하되 cross-owner 갱신은 절대 허용하지 않는다.

보안 및 개인정보 위험

  • ID-only order update로 다른 creator series를 변경하지 못해야 한다.
  • 연결 가능한 content 검색은 target owner 범위로 제한한다.

acceptance criteria

  • 기존 creator series의 CRUD, soft delete, 콘텐츠 연결/해제, 조회/검색, 순서 변경 behavior parity가 특성화 테스트로 고정된다.
  • 모든 시리즈/콘텐츠 ID는 target creatorMember 소유일 때만 변경된다.
  • soft delete는 isActive=false이며 활성 조회에서 제외된다.
  • 기존 owner-less order update 취약 경로가 신규 v2 API에는 없다.

targeted test

  • Characterization: LegacyCreatorAdminSeriesCharacterizationTest.
  • V2 RED/GREEN: AiCharacterAdminSeriesServiceTest.
  • Run: ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*'

전체 회귀 테스트 영향

  • 기존 creator-admin series endpoint 계약은 유지한다.
  • 기존 series query/order 테스트가 있으면 함께 실행한다.

rollback 전략

  • 신규 series v2 admin route/facade를 제거한다.
  • 기존 data model 변경이 없으므로 schema rollback은 없다.

권장 commit 경계

  • feat: add ai character admin series slice

  • Task 4.1: 기존 series parity 특성화 baseline 고정

    • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/LegacyCreatorAdminSeriesCharacterizationTest.kt
    • CHARACTERIZE: 기존 creator series 구현을 대상으로 목록/상세/생성/수정/soft delete, inactive 조회 정책, 콘텐츠 연결/해제, 조회/검색, 순서 변경의 결과·검증·side effect를 고정한다.
    • BASELINE: 신규 v2 production code 변경 전에 특성화 테스트가 기존 구현을 대상으로 통과함을 확인한다.
    • REFACTOR: fixture와 assertion naming만 정리하고 series parity baseline은 변경하지 않는다.
    • Verify: ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.LegacyCreatorAdminSeriesCharacterizationTest
  • Task 4.2: series controller/facade/DTO 구현

    • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/*
    • RED: Task 4.1 baseline에서 옮긴 series parity, cross-owner detail/update/delete/order, cross-owner content attach, 시리즈 콘텐츠 검색, pagination 경계값 테스트와 controller 권한 테스트를 작성하고 신규 v2 미구현으로 실패함을 확인한다.
    • GREEN: owner-scoped series endpoint와 시리즈 콘텐츠 search/page/size 조회를 구현한다.
    • REFACTOR: 기존 owner-less order update를 참조하지 않는지 확인하고 회귀 테스트를 재실행한다.
    • Verify: ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*'

Phase 5: 커뮤니티 게시글 관리 vertical slice

목표

선택한 AI 캐릭터 소유 커뮤니티 게시글 등록, 수정, 고정/해제, soft delete와 관리자 조회를 제공한다.

범위와 비범위

  • 포함: owner-scoped community query/write, 최대 고정 3개, soft delete 시 fixed 상태 제거, 이미지/오디오/유료 게시글 검증, 기존 알림/최근 소식 side effect parity.
  • 제외: 구매/좋아요/댓글 관리, public community 조회 정책 변경.

선행 Phase 및 의존성

  • Phase 1 target resolver.
  • 기존 community write behavior 특성화 테스트.

API endpoint와 request/response contract

  • GET /api/v2/admin/ai-characters/{characterId}/community-posts
  • POST /api/v2/admin/ai-characters/{characterId}/community-posts
  • PUT /api/v2/admin/ai-characters/{characterId}/community-posts/{postId}
  • update request는 isFixed, isActive, 본문/이미지/오디오/가격 필드를 포함한다.

entity, repository, service 변경

  • Entity: 변경 없음.
  • Repository: creatorMember owner-scoped community post query adapter 추가 가능.
  • Service: 신규 community application service에서 target owner 검증 후 기존 핵심 로직을 선택 재사용한다.

DB migration

  • 없음.

transaction과 concurrency 고려사항

  • 고정 게시글 수 검증과 고정 처리는 같은 transaction에서 수행한다.
  • soft delete는 같은 transaction에서 isActive=false, isFixed=false, fixedAt=null을 함께 적용한다.
  • 동시 고정 요청은 기존 최대 3개 정책이 깨지지 않도록 repository count와 update 순서를 테스트한다.

보안 및 개인정보 위험

  • 다른 character/HUMAN creator 게시글 수정, 고정, soft delete를 차단한다.
  • 유료 게시글의 접근 정책과 파일 경로 노출 정책을 기존 동작과 맞춘다.

acceptance criteria

  • target creatorMember 소유 게시글만 조회/변경된다.
  • 최대 고정 수 3개 정책이 유지된다.
  • soft delete된 게시글은 fixed 상태와 fixedAt이 제거된다.
  • invalid target/ownership 실패 시 DB/S3/event side effect가 없다.

targeted test

  • Characterization: LegacyCommunityPostCharacterizationTest.
  • V2 RED/GREEN: AiCharacterAdminCommunityPostServiceTest.
  • Run: ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*'

전체 회귀 테스트 영향

  • 기존 v2 community 조회와 legacy community write 테스트가 통과해야 한다.

rollback 전략

  • 신규 community v2 admin route/facade를 제거한다.
  • 신규 DDL이 없으므로 schema rollback은 없다.

권장 commit 경계

  • feat: add ai character admin community slice

  • Task 5.1: 기존 community behavior 특성화 baseline 고정

    • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/LegacyCommunityPostCharacterizationTest.kt
    • CHARACTERIZE: 기존 community 구현을 대상으로 max fixed count, image/audio/paid post validation, notification/recent-news side effect, fixed post soft delete clearing의 결과와 실패 계약을 고정한다.
    • BASELINE: 신규 v2 production code 변경 전에 특성화 테스트가 기존 구현을 대상으로 통과함을 확인한다.
    • REFACTOR: media fixture와 event spy 중복만 정리하고 side-effect parity baseline은 변경하지 않는다.
    • Verify: ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.LegacyCommunityPostCharacterizationTest
  • Task 5.2: community controller/facade/DTO 구현

    • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/*
    • RED: Task 5.1 baseline에서 옮긴 community parity, cross-owner update, invalid target no-side-effect 테스트와 controller 권한/페이지네이션 경계 테스트를 작성하고 신규 v2 미구현으로 실패함을 확인한다.
    • GREEN: owner-scoped community endpoint를 구현하고 목록 page/size 기본값·최소·최대 보정을 적용한다.
    • REFACTOR: public community 조회 DTO/로직을 무비판적으로 복제하지 않았는지 확인하고 회귀 테스트를 재실행한다.
    • Verify: ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*'

Phase 6: FanTalk 답변 vertical slice

목표

선택한 AI 캐릭터가 자신의 활성 root FanTalk에만 creator reply를 작성하는 v2 관리자 API를 제공한다.

범위와 비범위

  • 포함: root FanTalk 존재/활성/owner 검증, creator reply 저장, 언어 감지와 기존 응답 의미 parity.
  • 제외: FanTalk 원글 작성, nested reply, 구매/댓글형 기능, AI 캐릭터 일반 사용자 활동.

선행 Phase 및 의존성

  • Phase 1 target resolver.
  • 기존 FanTalk 저장 엔티티와 응답 DTO 의미 특성화.

API endpoint와 request/response contract

  • POST /api/v2/admin/ai-characters/{characterId}/fan-talks/{fanTalkId}/replies
  • Request: CreateAiCharacterAdminFanTalkReplyRequest(content: String)
  • Response: AiCharacterAdminFanTalkReplyResponse(fanTalkId, replyId, creatorMemberId, content, createdAtUtc)

entity, repository, service 변경

  • Entity: 변경 없음.
  • Repository: CreatorCheers 또는 FanTalk repository에 root/active/creator owner 조회 adapter 추가 가능.
  • Service: 신규 FanTalk reply application service 구현. target 검증 후 기존 저장/언어 감지 로직을 필요한 만큼 재사용한다.

DB migration

  • 없음.

transaction과 concurrency 고려사항

  • root FanTalk 조회와 reply 저장은 같은 transaction에서 수행한다.
  • 중복 답변 허용 여부는 기존 domain 정책을 따른다. 기존 정책이 없다면 이번 API는 별도 중복 차단을 추가하지 않는다.

보안 및 개인정보 위험

  • 다른 character FanTalk, HUMAN creator FanTalk, inactive FanTalk, nested parent에는 답변하지 않는다.
  • AI character Member 로그인/impersonation 없이 writer/creator만 해석된 creatorMember로 저장한다.

acceptance criteria

  • target AI character는 자신의 활성 root FanTalk에만 답변할 수 있다.
  • cross-character, nested parent, inactive/missing FanTalk는 4xx이며 reply 저장과 이벤트 발행이 없다.
  • 저장된 답변의 writer/creator는 해석된 creatorMember와 일관된다.

targeted test

  • Characterization: LegacyFanTalkReplyCharacterizationTest.
  • V2 RED/GREEN: AiCharacterAdminFanTalkReplyServiceTest.
  • Run: ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.*'

전체 회귀 테스트 영향

  • 기존 FanTalk 조회/작성 관련 테스트가 통과해야 한다.

rollback 전략

  • 신규 FanTalk reply v2 admin route/facade를 제거한다.
  • 신규 DDL이 없으므로 schema rollback은 없다.

권장 commit 경계

  • feat: add ai character admin fan talk reply slice

  • Task 6.1: 기존 FanTalk reply 의미 특성화 baseline 고정

    • Test: src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/LegacyFanTalkReplyCharacterizationTest.kt
    • CHARACTERIZE: 기존 FanTalk 구현을 대상으로 valid root reply의 언어 감지, 응답 DTO 의미, writer/creator 저장 결과를 고정한다.
    • BASELINE: 신규 v2 production code 변경 전에 특성화 테스트가 기존 구현을 대상으로 통과함을 확인한다.
    • REFACTOR: FanTalk root/reply fixture만 정리하고 언어 감지·응답·writer/creator baseline은 변경하지 않는다.
    • Verify: ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.LegacyFanTalkReplyCharacterizationTest
  • Task 6.2: FanTalk reply controller/facade/DTO 구현

    • Create: src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/*
    • RED: Task 6.1 baseline에서 옮긴 FanTalk reply parity, cross-character/nested/inactive/missing reject, writer/creator 일관성, no-side-effect 테스트와 controller 권한 테스트를 작성하고 신규 v2 미구현으로 실패함을 확인한다.
    • GREEN: root/active/owner 검증 후 reply 저장 endpoint를 구현한다.
    • REFACTOR: nested reply 금지와 writer/creator mapping을 정리하고 회귀 테스트를 재실행한다.
    • Verify: ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.*'

Phase 7: Final Integration & Quality Gate

목표

신규 AI 캐릭터 관리자 API 전체가 source spec을 충족하고 기존 legacy/public 계약을 회귀시키지 않았음을 검증한다.

범위와 비범위

  • 포함: 전체 targeted test, 기존 회귀 테스트, ktlint, dependency/DDL/API contract 점검, 문서 검증 기록 누적.
  • 제외: 신규 기능 추가, unrelated refactor.

선행 Phase 및 의존성

  • Phase 1~6 완료.

API endpoint와 request/response contract

  • Endpoint Contract Summary의 모든 endpoint가 구현되어야 한다.
  • 모든 신규 endpoint가 JWT ADMIN + 현재 DB ADMIN 이중 인가와 공통 오류 envelope/i18n을 공유해야 한다.
  • legacy/public endpoint URI와 성공·오류 status/body/message diff가 없어야 한다.

entity, repository, service 변경

  • 신규 변경 없음. Phase 1~6 변경의 누락 import, unused code, package 의존 방향만 정리한다.
  • 신규 v2 application/domain이 기존 controller 또는 v2 API response DTO를 역참조하지 않는지 점검한다.

DB migration

  • 없음. 새 DDL/migration 파일이 없는지 확인한다.

transaction과 concurrency 고려사항

  • 각 write slice의 transaction 시작부 target/ownership 검증이 유지되는지 점검한다.
  • 동시성 관련 targeted test가 실패 없이 통과해야 한다.

보안 및 개인정보 위험

  • JWT 또는 현재 DB role이 비ADMIN인 접근, stale ADMIN claim, AI login/token/impersonation, private path 노출, cross-owner write가 없는지 전체 점검한다.

acceptance criteria

  • Phase별 targeted test가 모두 통과한다.
  • 모든 신규 endpoint에서 stale ADMIN claim은 403이고, 등록된 모든 API 오류 분기는 정확한 비2xx status + ApiResponse.error + KO/EN/JA message를 반환한다. Spring CORS 정책 거부 403 body는 envelope 계약의 예외다.
  • legacy/public 401/403/domain 오류의 status/body/message가 특성화 baseline과 동일하다.
  • ./gradlew test가 통과한다.
  • Kotlin 파일 변경이 있으면 ./gradlew ktlintCheck가 통과한다.
  • 신규 dependency와 신규 DDL이 없다.
  • source spec acceptance criteria 25개를 각 Phase 결과와 대조해 누락이 없다.

targeted test

  • Run:
    ./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'
    ./gradlew test
    ./gradlew ktlintCheck
    

전체 회귀 테스트 영향

  • legacy /admin/*, /creator-admin/*, public /api/v2/* 관련 테스트 전체가 회귀 범위다.
  • 기존 성공·오류 request/response 계약 변경이 없음을 controller/DTO diff와 legacy 오류 특성화 테스트로 확인한다.

rollback 전략

  • 신규 /api/v2/admin/ai-characters controller bean 비활성화 또는 신규 package 제거로 기능 표면을 되돌린다.
  • DB schema 변경이 없으므로 rollback은 code revert 중심이다.

권장 commit 경계

  • test: verify ai character admin api integration

  • Task 7.1: 전체 targeted/regression test 실행

    • TDD 예외 사유: 구현 완료 후 검증 전용 Task라 신규 실패 테스트를 작성하지 않는다.
    • 대체 검증 방법: Phase 1~6 targeted test, AiCharacterAdminAuthorizationTest, AiCharacterAdminErrorContractTest, 기존 admin security test와 전체 회귀 테스트를 실행한다.
    • REFACTOR: 실패가 있으면 관련 Phase Task로 되돌려 최소 수정 후 다시 실행한다.
    • Verify: 위 targeted command와 ./gradlew test를 실행하고 결과를 이 문서 하단 검증 기록에 남긴다.
  • Task 7.2: API contract와 변경 범위 점검

    • TDD 예외 사유: diff/architecture 검증 전용 Task라 신규 실패 테스트를 작성하지 않는다.
    • 대체 검증 방법: legacy/public 성공·오류 status/body/message 특성화 baseline 통과, controller/DTO schema 변경 없음, 신규 dependency 없음, 신규 DDL 없음, 신규 v2 application/domain에서 기존 controller와 v2 response DTO 역참조 없음.
    • REFACTOR: 불필요한 import, 역방향 의존, 관련 없는 변경을 제거하고 diff를 다시 확인한다.
    • Verify: git diff --name-only, ./gradlew ktlintCheck

검증 기록

  • 계획 작성 단계: 코드 변경 없음.
  • 문서 규칙 확인: docs/agent-guides/작업절차.md, docs/agent-guides/문서유지보수.md, docs/agent-guides/테스트스타일.md, docs/prd/sample-prd.md를 확인하고 docs/20260724_AI캐릭터_관리자_API/{prd.md,plan-task.md} 형식으로 작성했다.
  • 금지어 확인: 계획 문서 금지어 검색 명령 실행 결과 없음.
  • Phase 항목 확인: rg -n "^### Phase|#### 목표|#### 범위와 비범위|#### 선행 Phase|#### API endpoint|#### entity, repository, service 변경|#### DB migration|#### transaction과 concurrency|#### 보안 및 개인정보|#### acceptance criteria|#### targeted test|#### 전체 회귀 테스트 영향|#### rollback 전략|#### 권장 commit 경계|Final Integration" "docs/20260724_AI캐릭터_관리자_API/plan-task.md"로 Phase 1~7 전체에 필수 항목이 있음을 확인했다.
  • 명령 유효성 확인: ./gradlew tasks --all 실행 결과 BUILD SUCCESSFUL.
  • Markdown diagnostics: .md 확장자용 LSP 서버가 설정되어 있지 않아 lsp_diagnostics는 실행 불가.
  • Endpoint Contract 보강: frontend HTTP 호출 기준으로 query parameter, request body, multipart form fields, response data JSON 예시를 추가했다.
  • Endpoint Contract 보강 검증: 계획 문서 금지어 검색 결과 없음. .md 확장자용 LSP 서버가 없어 diagnostics는 실행 불가.
  • 문서 동기화 보강: source spec 기준 characterId 예외, 시리즈 콘텐츠 검색, endpoint별 ADMIN 권한 테스트, parity RED 범위, 의존 방향 검증, PRD 가드레일, pagination 경계값, RED/GREEN/REFACTOR task 규칙을 반영했다.
  • 문서 동기화 검증: Readprd.mdplan-task.md의 반영 라인을 확인했다. git status --short 결과 docs/20260724_AI캐릭터_관리자_API/는 현재 untracked 디렉터리로 표시된다. ./gradlew tasks --all은 이번 세션에서 120초, 300초 제한 모두 초과해 종료 결과를 확인하지 못했다.
  • 문서 동기화 보강(2차): 기존 legacy/creator-admin 구현을 먼저 통과하는 특성화 baseline과 신규 v2 RED를 분리했고, character/content/series/community/FanTalk 재사용·parity 경계에 반영했다. 시리즈 CRUD·연결·해제·조회·순서 behavior parity와 signed URL 만료 계산식·edge case 특성화를 PRD와 Task에 명시했다.
  • 문서 동기화 재검증(4번 제외): source spec acceptance criteria 25개 추적 검사 25/25, actor/scope/non-goal/architecture 가드레일 11/11을 확인했고 git diff --check 결과 문제가 없었다. plan 상단의 실행 skill 지침은 사용자 요청에 따라 변경·판정 범위에서 제외했다.
  • 기존 기록은 보존한다. 각 Task의 실행 명령과 결과 요약은 해당 Task 아래에 누적하고, phase/전체 회귀·전체 빌드·포맷·문서 범위 확인만 이 섹션에 누적한다.
  • Phase 1 RED: ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminTargetResolverTest 실행 결과 AiCharacterAdminTargetResolverChatCharacterRepository.findByIdWithCreatorMember 미구현으로 compileTestKotlin 실패를 확인했다.
  • Phase 1 GREEN: ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminTargetResolverTest 실행 결과 BUILD SUCCESSFUL.
  • Phase 1 ADMIN 권한 RED/GREEN: ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest에서 sample route 권한 실패를 확인한 뒤 /api/v2/admin/ai-characters/** ADMIN rule을 적용해 BUILD SUCCESSFUL을 확인했다.
  • Phase 1 targeted 검증: ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' 실행 결과 BUILD SUCCESSFUL.
  • Phase 1 lint 검증: ./gradlew ktlintCheck 실행 결과 BUILD SUCCESSFUL.
  • Phase 1 LSP diagnostics: 현재 도구 목록에 LSP diagnostics tool이 노출되어 있지 않아 실행하지 못했고, 대신 Kotlin compile/test와 ktlintCheck로 대체 검증했다.
  • Phase 1 reviewer gate: 1차 리뷰에서 invalid target 4xx 미충족, production SecurityConfig 미검증, cross-owner fixture 부족을 지적받아 수정했고, 재리뷰 결과 남은 blocking finding 없음으로 승인받았다.
  • Phase 1 추가 리뷰 반영: mock 기반 resolver 테스트만으로 실제 repository/ownership 동작을 검증하지 못한다는 지적에 따라 AiCharacterAdminTargetResolverIntegrationTest를 추가했다. ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminTargetResolverIntegrationTest 실행 결과 BUILD SUCCESSFUL.
  • Phase 1 no-side-effect 근거 보정: 현재 검증은 resolver 자체가 DB row를 변경하지 않는다는 통합 테스트와 resolver가 S3/외부 API/event 의존성을 갖지 않는다는 구조에 한정한다. Phase 2~6 write vertical slice의 S3, 외부 API, 이벤트 no-side-effect는 각 slice 테스트에서 별도로 검증한다.
  • Phase 1 후속 정책 반영 전 한계: 기존 ADMIN smoke는 JWT authority만 검증했고 현재 DB role/stale claim 및 신규 prefix 오류 envelope/i18n을 검증하지 않았다. 2026-07-24 후속 확정 정책은 Task 1.4~1.5에서 RED/GREEN으로 보완한다.
  • Phase 1 후속 정책 문서 갱신: JWT ADMIN + 현재 DB ADMIN 이중 인가, stale claim 403, 신규 prefix의 비2xx ApiResponse.error/KO·EN·JA 계약, legacy 오류 응답 불변 조건을 PRD와 계획에 반영하고 미완료 Task 1.4~1.5를 추가했다.
  • Phase 1 후속 정책 문서 자체 검토: 금지어/미확정 문구 검색 결과 없음, 요구사항 추적 검색으로 stale claim·오류 envelope·i18n· legacy fallback·Task 1.4~1.5 반영을 확인했고 git diff --check 결과 문제가 없었다.
  • Phase 1 후속 정책 명령 유효성: ./gradlew tasks --all 실행 결과 BUILD SUCCESSFUL.
  • Task 1.4 RED: ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest 실행 결과 6개 중 stale ADMIN claim과 비MemberAdapter principal이 403 기대 대비 200으로 통과해 2개 실패함을 확인했다.
  • Task 1.4 GREEN: 신규 prefix의 단일 access 식에서 JWT ADMIN, MemberAdapter principal, 현재 DB ADMIN을 AND로 검증한 뒤 동일 테스트 실행 결과 BUILD SUCCESSFUL.
  • Task 1.4 reviewer gate: 별도 read-only 리뷰에서 스펙 준수와 코드 품질 모두 승인됐고 Critical/Important/Minor finding이 없음을 확인했다.
  • Task 1.5 최초 RED: legacy 오류 baseline 2개는 통과했고 신규 401/403/400/500 계약은 15개 중 13개가 status/content type/ message 불일치로 실패함을 확인했다. target resolver의 기존 ResponseStatusException을 신규 API 예외로 교체하는 테스트도 새 예외 미구현 상태의 compileTestKotlin 실패로 RED를 확인했다.
  • Task 1.5 확장 RED: handler 선택 전 오류와 filter 내부 장애까지 포함해 오류 계약 39개를 실행한 결과 신규 prefix의 404/405/ 415 및 예상하지 못한 JWT filter 오류 500에 해당하는 12개만 실패했고 legacy baseline은 통과했다. 미등록 message key fallback은 44개 중 해당 KO/EN/JA 3개 실패로 별도 RED를 확인했다.
  • Task 1.5 GREEN: URI prefix 기반 exception resolver, 낮은 우선순위 404 fallback mapping, prefix 전용 security handler, known 인증 실패 401/그 외 filter 예외 500 분리, 미등록 message key의 localized unknown fallback을 구현했다. ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest 실행 결과 44개 invocation 모두 통과해 BUILD SUCCESSFUL을 확인했다.
  • Task 1.5 기존 slice 회귀: production SecurityConfig를 import하는 기존 WebMvc test 5개와 신규 authorization/error contract test를 함께 실행한 결과 BUILD SUCCESSFUL을 확인했다. 신규 writer/security handler/exception resolver는 SecurityConfig의 명시적 bean으로 등록해 slice와 실제 application 구성을 동일하게 유지했다.
  • Task 1.5 reviewer gate: 최초 read-only 리뷰의 405/415 handler-less 경로, 비인증 filter 예외의 401 오분류, 기존 WebMvc slice 빈 누락, 미매핑 404 지적을 모두 보완했다. 재리뷰 결과 Critical/Important/Minor finding 없이 승인됐다.
  • Phase 1 후속 정책 최종 targeted/legacy 회귀: ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' --tests kr.co.vividnext.sodalive.admin.partner.agent.read.AdminAgentReadControllerSecurityTest --tests kr.co.vividnext.sodalive.admin.content.AdminContentControllerSecurityTest 실행 결과 BUILD SUCCESSFUL(28초)을 확인했다.
  • Phase 1 후속 정책 전체 회귀: 최신 작업 트리에서 ./gradlew test 실행 결과 BUILD SUCCESSFUL(4분 22초)을 확인했다.
  • Phase 1 후속 정책 lint: 최신 작업 트리에서 ./gradlew ktlintCheck 실행 결과 BUILD SUCCESSFUL(17초)을 확인했다.
  • Phase 1 후속 정책 diff 무결성: git diff --check 통과, conflict marker 없음, build 설정·dependency·DDL 변경 없음을 확인했다.
  • Task 1.5 최종 리뷰 보완 RED/GREEN: JWT parse 사이 만료 JwtException 401과 인증 저장소 장애 500을 추가했을 때 50개 중 해당 6개 실패를 확인한 뒤 known credential failure만 401로 분류해 50개 모두 통과했다. 필수 request header 누락 400은 53개 중 해당 KO/EN/JA 3개 실패를 확인한 뒤 ServletRequestBindingException을 400으로 분류해 모두 통과했다.
  • Task 1.5 CORS RED/GREEN: 허용된 관리자 Origin의 404/405/415와 실제 authorization,content-type header를 요청하는 미매핑 prefix preflight를 추가했을 때 57개 중 해당 4개 실패를 확인했다. fallback mapping에 기존 전역 설정과 동일한 CORS 설정을 적용한 뒤 57개 모두 통과했다.
  • Phase 1 no-side-effect 검증 강화: AiCharacterAdminTargetResolverIntegrationTest에서 Hibernate statistics를 초기화한 뒤 invalid target resolver 호출과 flush 후 entity insert/update/delete가 각각 0건임을 직접 검증했다. resolver 단위·통합 및 오류 계약 테스트를 함께 실행한 결과 BUILD SUCCESSFUL(27초)을 확인했다.
  • Phase 1 최종 reviewer gate: 인증 예외 분류, request binding, fallback CORS, DB no-write, 문서/rollback을 독립 read-only로 재검토한 결과 Critical/Important/Minor finding이 없음을 확인했다.
  • Phase 1 최신 targeted/legacy 회귀: ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' --tests kr.co.vividnext.sodalive.admin.partner.agent.read.AdminAgentReadControllerSecurityTest --tests kr.co.vividnext.sodalive.admin.content.AdminContentControllerSecurityTest 실행 결과 BUILD SUCCESSFUL(28초)을 확인했다.
  • Phase 1 최신 전체 회귀: 최종 코드 작업 트리에서 ./gradlew test 실행 결과 BUILD SUCCESSFUL(4분 17초)을 확인했다.
  • Phase 1 최신 lint: import 순서 1건을 수정한 뒤 ./gradlew ktlintCheck 재실행 결과 BUILD SUCCESSFUL(10초)을 확인했다.
  • Phase 1 최신 diff 무결성: git diff --checkgit diff --cached --check가 모두 통과했고 conflict marker가 없으며 build 설정, dependency, DDL 변경이 없음을 확인했다.
  • Phase 1 코드 리뷰 차단 이슈 보완: 리뷰에서 지적된 staged/untracked 누락을 재확인한 결과 최신 작업 트리는 Phase 1 신규 테스트 2개(AiCharacterAdminAccessDeniedErrorContractTest, AiCharacterAdminLoginJwtIntegrationTest)와 AccessDeniedException 403 수정이 모두 변경 세트에 포함되어 있음을 확인했다. TokenProvider subject parsing 보정과 관련 테스트, rollback 범위를 Task 1.4/1.5와 Phase 1 rollback 전략에 반영했다.
  • Phase 1 코드 리뷰 차단 이슈 재검증: ./gradlew test --rerun-tasks --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' --tests 'kr.co.vividnext.sodalive.jwt.TokenProviderTest' 단독 실행 결과 BUILD SUCCESSFUL을 확인했다. 이전 QA의 Gradle TestOutputStore EOF는 동시/강제 실행 환경에서 발생한 결과 저장소 문제로 보며, 현재 단독 fresh rerun에서는 재현되지 않았다.
  • Phase 1 코드 리뷰 차단 이슈 최종 검증: 문서 보정 후 git status --short --untracked-files=all에서 untracked 파일이 없고 git diff --name-only 결과가 비어 있음을 확인했다. git diff --cached --check, git diff --check, ./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' --tests 'kr.co.vividnext.sodalive.jwt.TokenProviderTest', ./gradlew test --tests 'kr.co.vividnext.sodalive.admin.partner.agent.read.AdminAgentReadControllerSecurityTest' --tests 'kr.co.vividnext.sodalive.admin.content.AdminContentControllerSecurityTest', ./gradlew ktlintCheck 실행 결과 모두 BUILD SUCCESSFUL을 확인했다.
  • Task 1.6 CORS 계약 재확인: 현재 WebConfig 기준 전용 Origin http://localhost:8888, https://test-character-admin.sodalive.net, https://character-admin.sodalive.net 세 개를 PRD/plan에 명시했다. preflight 테스트는 세 Origin 모두 허용하고 기존 범용 관리자/creator Origin 네 개를 거부하는지 고정했다.
  • Task 1.6 JWT RED/GREEN: TokenProviderTest 9개 중 추가한 누락·빈 값·빈 분할 항목·비문자열 auth claim 7개 invocation이 실패하는 RED를 확인했다. claim을 authority로 변환하기 전 검증해 common.error.bad_credentials로 변환한 뒤 9개 모두 통과했다.
  • Task 1.6 테스트 fixture 격리: 전용 controller 6개를 각 테스트의 nested @TestComponent로 이동하고 @Import로만 등록했다. 초기 nested 이동 후 명시 등록이 누락된 WebMvc 요청 37개가 404로 실패한 것을 확인한 뒤 보정했고, 독립 재리뷰의 component scan 지적을 @TestComponent로 해소했다.
  • Task 1.6 최종 targeted/레거시 회귀: ./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' 실행 결과 103개 모두 통과했다. 기존 AdminAgentReadControllerSecurityTest, AdminContentControllerSecurityTest 보안 회귀도 BUILD SUCCESSFUL을 확인했다.
  • Task 1.6 최종 전체 회귀/lint: 최종 소스 상태에서 ./gradlew testBUILD SUCCESSFUL(4분 55초), ./gradlew ktlintCheck --rerun-tasksBUILD SUCCESSFUL(19초)를 확인했다.
  • Task 1.6 최종 reviewer/diff gate: 독립 read-only 재리뷰 결과 CORS, malformed auth, fixture scan 격리에 잔여 finding이 없음을 확인했다. git diff --check, git diff --cached --check가 통과했고 conflict marker와 untracked 파일이 없음을 확인했다.
  • Task 1.6 2차 리뷰 CORS RED/GREEN: 실제 mapped endpoint, 공유 /admin/member/login, /member/logout의 캐릭터 관리자 Origin 요청/preflight를 추가했을 때 AiCharacterAdminLoginJwtIntegrationTest 6개 중 3개 실패를 확인했다. 두 공유 인증 exact path에만 기존 전역 Origin과 캐릭터 관리자 Origin 합집합을 적용한 뒤, 미등록 Origin 거부까지 포함한 7개가 모두 통과했다. 신규 prefix의 기존 범용 관리자 Origin 거부와 실제 로그인·로그아웃도 함께 검증했다.
  • Task 1.6 2차 리뷰 HTTP 오류 RED/GREEN: 406, 405 Allow, 415 Accept/Accept-Patch, MissingPathVariableException 500 계약을 추가해 신규 13개 실패를 확인했다. AiCharacterAdminExceptionHandler에 Spring 기본 HTTP 의미를 보존하는 최소 분기와 header 처리를 추가한 뒤 AiCharacterAdminErrorContractTest 80개가 모두 통과했다.
  • Task 1.6 2차 리뷰 최종 targeted/레거시 회귀: TokenProviderTestaicharacter.* 114개, 기존 AdminAgentReadControllerSecurityTestAdminContentControllerSecurityTest 11개를 함께 실행해 총 125개 모두 통과했고 BUILD SUCCESSFUL(42초)을 확인했다.
  • Task 1.6 2차 리뷰 전체 회귀/lint: 최종 소스 상태에서 ./gradlew testBUILD SUCCESSFUL(5분 4초), ./gradlew ktlintCheck --rerun-tasksBUILD SUCCESSFUL(20초)을 확인했다.
  • Task 1.6 2차 리뷰 최종 gate: 공유 인증 exact path가 전역 fallback보다 먼저 적용되고 신규 prefix/fallback은 전용 Origin을 유지하는지 독립 read-only로 재검토했다. git diff --check, git diff --cached --check가 통과했고 conflict marker와 untracked 파일이 없음을 확인했다.
  • Task 1.6 후속 리뷰 mapped write preflight 보완: 기존 신규 prefix write preflight 검증이 fallback /api/v2/admin/ai-characters/unmapped-path만 타는 한계를 확인했다. 테스트 controller에 실제 mapped /api/v2/admin/ai-characters/error-contract/write-preflightPOST/PUT/PATCH/DELETE 매핑을 추가하고, 해당 경로 preflight에서 Access-Control-Allow-OriginAccess-Control-Allow-Methods를 함께 검증하도록 보완했다.
  • Task 1.6 후속 리뷰 fallback write preflight 보완: actual mapping과 fallback이 서로 다른 CORS 설정을 사용하므로, mapped endpoint 검증과 별도로 fallback /api/v2/admin/ai-characters/unmapped-path에서도 POST/PUT/PATCH/DELETE preflight의 Access-Control-Allow-OriginAccess-Control-Allow-Methods를 검증하도록 보완했다.
  • Task 1.6 후속 리뷰 추가 계약 보완: wrong-role USER + AI_CHARACTER target은 resolver가 400으로 거부하고 DB insert/update/delete 없이 기존 role/memberKind를 유지하는지 고정했다. 캐릭터 관리자 Origin이 공유 인증 외 legacy/public 경로로 확산되지 않는지 확인했고, 406 Not Acceptable 응답도 허용 Origin에서는 localized ApiResponseAccess-Control-Allow-Origin을 함께 반환하는지 검증했다.
  • Task 1.6 후속 리뷰 계약 테스트 현황: mapped write preflight 보완 후 AiCharacterAdminErrorContractTest 85개, AiCharacterAdminLoginJwtIntegrationTest 8개, AiCharacterAdminTargetResolverIntegrationTest 4개 기준으로 후속 리뷰 항목을 회귀했다.
  • Task 1.6 후속 리뷰 최종 targeted/레거시 회귀: ./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' --tests kr.co.vividnext.sodalive.admin.partner.agent.read.AdminAgentReadControllerSecurityTest --tests kr.co.vividnext.sodalive.admin.content.AdminContentControllerSecurityTest 실행 결과 targeted+legacy 132개가 모두 통과했고 BUILD SUCCESSFUL(40초)을 확인했다.
  • Task 1.6 후속 리뷰 전체 회귀/lint: mapped write preflight 보완 후 소스 상태에서 ./gradlew test는 전체 1,259개 기준 BUILD SUCCESSFUL(4분 53초), ./gradlew ktlintCheck --rerun-tasksBUILD SUCCESSFUL(37초)을 확인했다.
  • Task 1.6 후속 리뷰 fallback 보완 후 계약 테스트 현황: fallback write preflight 보완 후 AiCharacterAdminErrorContractTest 89개, AiCharacterAdminLoginJwtIntegrationTest 8개, AiCharacterAdminTargetResolverIntegrationTest 4개 기준으로 후속 리뷰 항목을 회귀했다.
  • Task 1.6 후속 리뷰 fallback 보완 후 최종 targeted/레거시 회귀: ./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' --tests kr.co.vividnext.sodalive.admin.partner.agent.read.AdminAgentReadControllerSecurityTest --tests kr.co.vividnext.sodalive.admin.content.AdminContentControllerSecurityTest 실행 결과 targeted+legacy 136개가 모두 통과했고 BUILD SUCCESSFUL(1분 38초)을 확인했다.
  • Task 1.6 후속 리뷰 fallback 보완 후 전체 회귀/lint: fallback write preflight 보완 후 최종 소스 상태에서 ./gradlew test는 전체 1,263개 기준 BUILD SUCCESSFUL(5분 58초), ./gradlew ktlintCheck --rerun-tasksBUILD SUCCESSFUL(35초)을 확인했다.
  • Task 1.6 추가 코드 리뷰 보완: 실제 logout 후 동일 JWT로 신규 prefix 보호 경로를 호출하면 localized 401을 반환하는지 AiCharacterAdminLoginJwtIntegrationTest에 통합 테스트로 고정했다. 공유 /admin/member/login, /member/logout CORS preflight는 WebConfig의 기존 전역 Origin과 캐릭터 관리자 Origin 합집합 전체를 허용하는 parameterized test로 확장했다. Phase 1 targeted Run 명령에는 TokenProviderTest를 포함하도록 보정했다.
  • Task 1.6 추가 코드 리뷰 보완 검증: ./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminLoginJwtIntegrationTest 실행 결과 BUILD SUCCESSFUL을 확인했다.
  • Task 1.6 추가 코드 리뷰 보완 최종 회귀/lint: ./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' 실행 결과 BUILD SUCCESSFUL(44초), ./gradlew ktlintCheck 실행 결과 BUILD SUCCESSFUL(25초)을 확인했다.
  • Phase 1 최신 canonical fresh targeted/legacy 회귀: ./gradlew test --rerun-tasks --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' --tests kr.co.vividnext.sodalive.admin.partner.agent.read.AdminAgentReadControllerSecurityTest --tests kr.co.vividnext.sodalive.admin.content.AdminContentControllerSecurityTest 실행 결과 145/145, failure/error 0, BUILD SUCCESSFUL(3분 12초)을 확인했다.
  • Phase 1 최신 전체 fresh 회귀: ./gradlew test --rerun-tasks 실행 결과 1,272/1,272, failure/error 0, BUILD SUCCESSFUL(6분 49초)을 확인했다.
  • Phase 1 최신 lint: ./gradlew ktlintCheck --rerun-tasks 실행 결과 BUILD SUCCESSFUL(17초)을 확인했다.
  • Phase 1 최신 명령 유효성: ./gradlew tasks --all 실행 결과 BUILD SUCCESSFUL이며 test, ktlintCheck task가 존재함을 확인했다.
  • Phase 1 최종 보강 후 canonical fresh targeted/legacy 회귀: ./gradlew test --rerun-tasks --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' --tests kr.co.vividnext.sodalive.admin.partner.agent.read.AdminAgentReadControllerSecurityTest --tests kr.co.vividnext.sodalive.admin.content.AdminContentControllerSecurityTest 실행 결과 9개 XML class, 154/154, failure/error/skipped 0, BUILD SUCCESSFUL(5분)을 확인했다.
  • Phase 1 최종 보강 후 전체 fresh 회귀: ./gradlew test --rerun-tasks 실행 결과 243개 XML class, 1,281/1,281, failure/error/skipped 0, BUILD SUCCESSFUL(10분 2초)을 확인했다.
  • Phase 1 최종 보강 후 lint: ./gradlew ktlintCheck --rerun-tasks 실행 결과 7개 task가 실행됐고 BUILD SUCCESSFUL(29초)을 확인했다.