3115 lines
262 KiB
Markdown
3115 lines
262 KiB
Markdown
# 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.
|
||
|
||
| 문서 항목 | 내용 |
|
||
|---|---|
|
||
| 상태 | 구현 중 |
|
||
| 작성일 | 2026-07-24 |
|
||
| 요구사항 기준 | `docs/20260724_AI캐릭터_관리자_API/prd.md` |
|
||
| API 기준 | 이 문서의 `Endpoint Contract Summary` |
|
||
| 현재 Phase | Phase 3 6차 리뷰 완료 |
|
||
| 현재 활성 Goal | `P4-T1` 대기 |
|
||
|
||
## 현재 상태
|
||
|
||
| Phase | 상태 | 기존 완료 Task | 활성/다음 Goal | 차단 또는 남은 조건 |
|
||
|---:|---|---:|---|---|
|
||
| 1 | 완료 | 7/7 | 완료 | 없음 |
|
||
| 2 | 완료 | 11/11 | 완료 | 없음 |
|
||
| 3 | 완료 | 15/15 | 완료 | 없음 |
|
||
| 4 | 대기 | 0/6 | `P4-T1` | `P3-R5-GATE`, 사용자 진행 지시 |
|
||
| 5 | 대기 | 0/6 | `P5-T1` | `P4-GATE` |
|
||
| 6 | 대기 | 0/4 | `P6-T1` | `P5-GATE` |
|
||
| 7 | 대기 | 0/2 | `P7-T1` | Phase 1~6 Gate 완료 |
|
||
|
||
- Phase는 결과와 의존성을 묶는 문서 단위다. `create_goal`에는 Task 또는 Phase Gate 하나만 등록한다.
|
||
- 동시에 하나의 미완료 goal만 운용하고, 사용자가 명시적으로 요청하지 않으면 token budget을 설정하지 않는다.
|
||
- 기존 `[x]` Task와 검증 기록은 당시 완료 이력으로 보존한다. 후속 리뷰에서 발견한 문제는 기존 Task를 다시 열지 않고 새 review/fix
|
||
Goal로 처리한다.
|
||
- 각 Goal은 체크박스, focused test, 완료 증거와 Progress 기록이 모두 충족된 뒤에만 `complete`로 갱신한다.
|
||
- 검증은 focused test와 영향받는 slice/legacy 회귀를 우선한다. 전체 `./gradlew test`는 공통 경계·여러 Phase 영향, targeted
|
||
결과만으로 영향 범위를 판단할 수 없는 실패 또는 최종 release 판정에 실제로 필요하다고 기록한 경우에만 실행한다.
|
||
- 전체 회귀를 생략하면 생략 근거와 대신 실행한 focused/영향 범위 회귀 명령을 Progress와 검증 기록에 남긴다.
|
||
- 같은 차단 사유가 최초 시도와 자동 후속을 포함해 3회 연속 반복되고, 문서화나 독립 작업도 불가능할 때만 `blocked`로 갱신한다.
|
||
|
||
---
|
||
|
||
## 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 보정을 적용하고 경계값 테스트를 둔다.
|
||
|
||
```json
|
||
{
|
||
"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를 유지한다.
|
||
|
||
```json
|
||
{
|
||
"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:
|
||
|
||
```json
|
||
{
|
||
"search": "루나",
|
||
"page": 0,
|
||
"size": 20
|
||
}
|
||
```
|
||
|
||
Response `data`:
|
||
|
||
```json
|
||
{
|
||
"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`:
|
||
|
||
```json
|
||
{
|
||
"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:
|
||
|
||
```json
|
||
{
|
||
"image": "File | optional",
|
||
"request": {
|
||
"name": "루나",
|
||
"systemPrompt": "루나는 달빛을 좋아하는 AI 캐릭터입니다.",
|
||
"description": "달빛을 좋아하는 AI 캐릭터",
|
||
"originalWorkId": 31
|
||
}
|
||
}
|
||
```
|
||
|
||
Response `data`: 캐릭터 상세와 동일하다.
|
||
|
||
#### 캐릭터 수정/비활성화
|
||
|
||
`PUT /api/v2/admin/ai-characters/{characterId}`
|
||
|
||
Content-Type: `multipart/form-data`
|
||
|
||
Form fields:
|
||
|
||
```json
|
||
{
|
||
"image": "File | optional",
|
||
"request": {
|
||
"name": "루나",
|
||
"description": "수정된 소개",
|
||
"originalWorkId": 31
|
||
}
|
||
}
|
||
```
|
||
|
||
`externalCharacterId`는 response 전용이며 request에 포함하면 400이다. 일반 수정은 `image`와 함께 보낼 수 있으며 image가 없으면 기존
|
||
이미지를 유지한다. soft delete는 `request: {"isActive": false}`만 허용하고 일반 수정 field 또는 image와 혼합하면 400이다.
|
||
|
||
Response `data`: 캐릭터 상세와 동일하다. `isActive=false`는 soft delete 의미다.
|
||
|
||
#### 오디오 콘텐츠 목록/검색
|
||
|
||
`GET /api/v2/admin/ai-characters/{characterId}/audio-contents?search=밤&status=OPEN&page=0&size=20`
|
||
|
||
Query parameters:
|
||
|
||
```json
|
||
{
|
||
"search": "밤",
|
||
"status": "OPEN | SCHEDULED",
|
||
"page": 0,
|
||
"size": 20
|
||
}
|
||
```
|
||
|
||
Response `data`:
|
||
|
||
```json
|
||
{
|
||
"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`:
|
||
|
||
```json
|
||
{
|
||
"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:
|
||
|
||
```json
|
||
{
|
||
"coverImage": "File",
|
||
"audioFile": "File",
|
||
"request": {
|
||
"title": "밤 산책",
|
||
"description": "조용한 밤 산책 오디오",
|
||
"price": 1000,
|
||
"isAdult": false,
|
||
"isActive": true,
|
||
"themeId": 11,
|
||
"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:
|
||
|
||
```json
|
||
{
|
||
"coverImage": "File | optional",
|
||
"audioFile": "미지원",
|
||
"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:
|
||
|
||
```json
|
||
{
|
||
"page": 0,
|
||
"size": 20
|
||
}
|
||
```
|
||
|
||
Response `data`:
|
||
|
||
```json
|
||
{
|
||
"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`:
|
||
|
||
```json
|
||
{
|
||
"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:
|
||
|
||
```json
|
||
{
|
||
"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:
|
||
|
||
```json
|
||
{
|
||
"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:
|
||
|
||
```json
|
||
{
|
||
"search": "밤",
|
||
"page": 0,
|
||
"size": 20
|
||
}
|
||
```
|
||
|
||
Response `data`:
|
||
|
||
```json
|
||
{
|
||
"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:
|
||
|
||
```json
|
||
{
|
||
"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:
|
||
|
||
```json
|
||
{
|
||
"seriesIds": [701, 702, 703]
|
||
}
|
||
```
|
||
|
||
Response `data`: 시리즈 목록과 동일하다.
|
||
|
||
#### 커뮤니티 게시글 목록
|
||
|
||
`GET /api/v2/admin/ai-characters/{characterId}/community-posts?page=0&size=20`
|
||
|
||
Query parameters:
|
||
|
||
```json
|
||
{
|
||
"page": 0,
|
||
"size": 20
|
||
}
|
||
```
|
||
|
||
Response `data`:
|
||
|
||
```json
|
||
{
|
||
"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:
|
||
|
||
```json
|
||
{
|
||
"image": "File | optional",
|
||
"audioFile": "File | optional",
|
||
"request": {
|
||
"content": "오늘의 소식입니다.",
|
||
"price": 0,
|
||
"isAdult": false,
|
||
"isFixed": false,
|
||
"isActive": true
|
||
}
|
||
}
|
||
```
|
||
|
||
Response `data`:
|
||
|
||
```json
|
||
{
|
||
"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:
|
||
|
||
```json
|
||
{
|
||
"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:
|
||
|
||
```json
|
||
{
|
||
"content": "응원해줘서 고마워요!"
|
||
}
|
||
```
|
||
|
||
Response `data`:
|
||
|
||
```json
|
||
{
|
||
"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: `ChatCharacterRepository`에 `characterId`로 `creatorMember`를 `LEFT 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.error`와 `Accept-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`
|
||
|
||
- [x] **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`이 두 미구현 항목으로 실패했다.
|
||
|
||
- [x] **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: `characterId`로 `ChatCharacter`와 `creatorMember`를 조회하고 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`이었다.
|
||
|
||
- [x] **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`이었다.
|
||
|
||
- [x] **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 저장소 검증 의미는 변경하지 않는다.
|
||
|
||
```kotlin
|
||
.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`이었다.
|
||
|
||
- [x] **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: `AiCharacterAdminErrorResponseWriter`가 `Lang.fromAcceptLanguage`와 `SodaMessageSource`로 `ApiResponse.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`이었다.
|
||
|
||
- [x] **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: `TokenProvider`가 `auth` 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 보완 뒤 `AiCharacterAdminLoginJwtIntegrationTest`도 `BUILD SUCCESSFUL`이었다.
|
||
|
||
- [x] **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에서 회귀한다.
|
||
`AiCharacterAdminLoginJwtIntegrationTest`는 `MemberTokenRepository.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`
|
||
|
||
- [x] **Task 2.1: 기존 character parity 특성화 baseline 고정**
|
||
- **Goal 이력 `P2-H1`:** 기존 character admin 동작을 신규 v2 구현의 비교 기준으로 고정했다.
|
||
- **완료 증거:** 아래 특성화 테스트 RED/GREEN/REFACTOR 기록과 실제 명령 결과.
|
||
- **범위 밖:** 신규 v2 endpoint 구현.
|
||
- 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`
|
||
- 검증 기록(RED): 무엇: 기존 character admin baseline 특성화 테스트. 왜: 신규 v2 production code 전에 기존 생성/수정/비활성화/원작/언어 이벤트/creatorMember·콘텐츠 보존 계약을 고정하기 위해. 어떻게: 위 테스트를 추가하고 동일 명령을 실행했다. 결과: 이벤트 baseline 추가 직후 Mockito/S3 stub과 request JSON 누락으로 2회 실패해 RED를 확인했다.
|
||
- 검증 기록(GREEN): 무엇: Task 2.1 baseline 통과. 왜: 기존 구현이 현재 특성화 계약을 만족하는지 확인하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.LegacyChatCharacterAdminCharacterizationTest`를 실행했다. 결과: `BUILD SUCCESSFUL`을 확인했다.
|
||
- 검증 기록(REFACTOR): 무엇: Kotlin style과 리뷰 gate. 왜: 테스트-only baseline의 품질과 계획 준수 여부를 확인하기 위해. 어떻게: `./gradlew ktlintCheck`와 spec/code-quality read-only review를 실행했다. 결과: `BUILD SUCCESSFUL`, 두 리뷰 모두 `APPROVED`였다.
|
||
|
||
- [x] **Task 2.2: character controller/facade/DTO 구현**
|
||
- **Goal 이력 `P2-H2`:** 캐릭터 목록·상세·생성·수정의 최초 v2 API 구현을 제공했다.
|
||
- **완료 증거:** 아래 controller test와 `ktlintCheck` 기록. 이 이력만으로 Phase 2 Gate 통과를 의미하지 않는다.
|
||
- **범위 밖:** 후속 심층 리뷰에서 확정되는 누락·회귀 보완.
|
||
- 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.*'`
|
||
- 검증 기록(RED): 무엇: v2 character controller 계약 테스트 7개. 왜: 신규 route가 목록/검색/상세/생성/수정과 ADMIN 경계를 제공하지 않음을 확인하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerTest`를 실행했다. 결과: 모든 기대 endpoint가 미구현 route의 404를 반환해 7개가 실패했다.
|
||
- 검증 기록(GREEN): 무엇: 목록/검색 page-size 보정, target resolver 상세 거부, multipart 생성/수정, AI creatorMember 동기화, soft delete, invalid JSON, ADMIN 인가. 왜: Task 2.2 API 계약과 레거시 부작용 재사용을 확인하기 위해. 어떻게: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*'`를 실행했다. 결과: controller 8개와 Task 2.1 baseline 7개가 모두 통과했다.
|
||
- 검증 기록(REFACTOR): 무엇: 신규 v2 DTO 경계와 Kotlin style. 왜: legacy/public DTO 비노출과 코드 스타일을 확인하기 위해. 어떻게: `./gradlew ktlintCheck`를 실행했다. 결과: `BUILD SUCCESSFUL`을 확인했다.
|
||
|
||
- [x] **Task 2.3: Phase 2 요구사항·계약·코드 리뷰**
|
||
|
||
**Goal 실행 `P2-R1`:** PRD Feature B와 Endpoint Contract Summary를 Phase 2 코드·테스트에 추적해 확정된 누락만 후속 Goal로 전환한다.
|
||
|
||
- **추적 review ID:** `REV-001`, `REV-002`, `REV-003`, `REV-007`, `REV-008`.
|
||
- **시작 조건:** `P2-H1`, `P2-H2` 산출물과 검증 기록 존재.
|
||
- **완료 증거:** `docs/sample/sample-review.md` 형식의 리뷰 문서, endpoint/side-effect 추적표, 모든 후보의 확정·오탐·보류 판정,
|
||
후속 Goal 연결과 Progress 기록.
|
||
- **범위 밖:** 리뷰 도중 production code 수정, Phase 3 이후 기능 검토.
|
||
- **TDD 예외 사유:** 구현이 아닌 read-only 리뷰 Task다.
|
||
- **대체 검증 방법:** PRD·계약·production·test를 대조하고 현재 focused test를 실행해 관찰 결과를 리뷰 문서에 기록한다.
|
||
|
||
**Files:**
|
||
|
||
- Create: `docs/20260724_AI캐릭터_관리자_API/reviews/phase2-character-review.md`
|
||
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterController.kt`
|
||
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterFacade.kt`
|
||
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterDto.kt`
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerTest.kt`
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt`
|
||
|
||
- [x] 목록·검색·상세·생성·수정·비활성화 endpoint와 DTO 필드를 PRD/계약에 1:1로 추적한다.
|
||
- [x] 중복 이름, 외부 API, S3, 원작, 언어 이벤트, creatorMember 동기화와 실패 순서를 코드·test에 추적한다.
|
||
- [x] ADMIN 이중 인가, pagination, multipart/binding, KO/EN/JA 오류, private 정보 비노출 계약을 확인한다.
|
||
- [x] `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*'`의 실제 결과를 기록한다.
|
||
- [x] 후보를 확정·오탐·보류로 판정하고 확정 항목을 아래 세부 Goal에 연결하거나 새 회귀 수정 Goal을 계획에 먼저 추가한다.
|
||
- 검증 기록: 무엇: `P2-R1` Phase 2 character slice read-only 리뷰. 왜: `P2-H1`, `P2-H2` 완료 이력만으로 Phase 2 Gate를 통과할 수 있는지 판정하기 위해. 어떻게: `docs/20260724_AI캐릭터_관리자_API/reviews/phase2-character-review.md`를 작성하고 `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*'`를 실행했다. 결과: focused test는 `BUILD SUCCESSFUL in 51s`였고, `REV-001`~`REV-003`, `REV-007`, `REV-008`은 기존 `P2-T3`~`P2-T6` 보완 Goal에 연결된 확정 finding으로 판정했다.
|
||
|
||
- [x] **Task 2.4: 캐릭터 목록·검색·상세 보완**
|
||
|
||
**Goal 실행 `P2-T3`:** 캐릭터 조회 API의 검색·pagination·응답·target 계약을 독립적으로 검증하고 확정된 누락을 최소 수정한다.
|
||
|
||
- **추적 review ID:** `REV-007` 중 목록 응답 field set과 DTO 경계.
|
||
- **시작 조건:** `P2-R1` 완료와 관련 review ID 확정. 확정 finding이 없으면 Decision Log에 `해당 없음` 근거를 남긴다.
|
||
- **완료 증거:** 조회 전용 RED/GREEN, focused test, DTO 비노출 점검과 Progress 기록.
|
||
- **범위 밖:** 생성, 수정, 외부 API·S3 mutation.
|
||
|
||
**Files:**
|
||
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterController.kt`
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterFacade.kt`
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterDto.kt`
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerTest.kt`
|
||
|
||
- [x] **RED:** 목록 item의 exact JSON key를 고정하고 상세 전용 `creatorProfileImageUrl`, `creatorIntroduce`, `updatedAtUtc`가 노출되는 현재 동작을 실패로 재현한다.
|
||
- [x] **RED 확인:** `page=0`, `size` 기본 20·최소 20·최대 50, 검색·`hasNext`와 상세 target 불변식의 경계 test를 실행해 의도한 assertion 실패를 확인한다.
|
||
- [x] **GREEN:** 목록 전용 DTO와 mapper를 최소 구현해 계약 field만 반환하고 credential·token·private path를 노출하지 않는다.
|
||
- [x] **GREEN 확인:** 같은 focused test를 다시 실행해 exact 목록 field set과 pagination·target 계약이 모두 통과하는지 확인한다.
|
||
- [x] **REFACTOR:** 상세 UTC field 계약을 유지하면서 목록/상세 DTO 의존 방향을 점검하고 focused test와 `ktlintCheck` 결과를 Progress에 기록한다.
|
||
- 검증 기록(RED): 무엇: 목록 item의 상세 전용 field 비노출 계약. 왜: 목록 응답이 상세 DTO를 재사용해 `creatorProfileImageUrl`, `creatorIntroduce`, `updatedAtUtc`를 노출했기 때문이다. 어떻게: `AiCharacterAdminCharacterControllerTest`에 `doesNotExist()` assertion 3개를 추가하고 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerTest`를 실행했다. 결과: `목록은 음수 page와 최소 미만 size를 기본값으로 보정한다`가 line 66에서 실패해 RED를 확인했다.
|
||
- 검증 기록(GREEN/REFACTOR): 무엇: 목록 전용 DTO와 mapper 분리. 왜: 상세 응답 계약을 유지하면서 목록 field set만 Endpoint Contract Summary에 맞추기 위해. 어떻게: `AiCharacterAdminCharacterListItemResponse`와 `toListItemResponse`를 추가하고 목록 mapping만 교체한 뒤 같은 focused test와 `./gradlew ktlintCheck`를 실행했다. 결과: 둘 다 `BUILD SUCCESSFUL`이었다.
|
||
- Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerTest`
|
||
|
||
- [x] **Task 2.5: 캐릭터 생성 흐름 보완**
|
||
|
||
**Goal 실행 `P2-T4`:** 캐릭터 생성의 중복 검증, 외부 API, 이미지, 원작, creatorMember와 이벤트 흐름을 parity 기준으로 완결한다.
|
||
|
||
- **추적 review ID:** `REV-002`, `REV-003`, `REV-007` 중 생성 request·`characterType` 계약.
|
||
- **시작 조건:** `P2-T3` 완료와 생성 관련 확정 review ID.
|
||
- **완료 증거:** 정상 생성 및 실패 지점별 RED/GREEN, DB/S3/외부 API/event 결과, focused/legacy test와 Progress 기록.
|
||
- **범위 밖:** 기존 external character API 계약 변경, 캐릭터 수정·비활성화.
|
||
|
||
**Files:**
|
||
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterFacade.kt`
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterDto.kt`
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterExternalApiClient.kt`
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterImageStorage.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/character/AiCharacterAdminCharacterControllerMutationTest.kt`
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/LegacyChatCharacterAdminCharacterizationTest.kt`
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/LegacyChatCharacterAdminCharacterControllerEventCharacterizationTest.kt`
|
||
|
||
- [x] `systemPrompt`, `externalCharacterId`, 생성 `isActive`와 invalid `characterType`의 canonical request 계약을 Endpoint Contract Summary·legacy 특성화 결과로 확정하고 충돌 시 코드 수정 전에 Decision Log를 갱신한다.
|
||
- [x] **RED:** 확정된 문서 JSON의 역직렬화·반영, 중복 이름, 외부 API 실패, S3 실패와 존재하지 않는 `originalWorkId` 실패를 각각 재현한다.
|
||
- [x] **RED 확인:** 실패 지점별 DB row·creatorMember·원작 연결·S3 객체·외부 캐릭터·event 결과와 호출 순서를 단언해 현재 부분 저장 또는 고아 부작용을 확인한다.
|
||
- [x] **GREEN:** 모든 DB 참조를 외부 부작용 전에 검증하고, legacy parity에 맞는 최소 보상/정리 경계로 정상 생성과 실패 원자성을 통과시킨다.
|
||
- [x] **GREEN 확인:** 같은 생성 focused/characterization test를 다시 실행해 정상 결과와 실패 지점별 잔존 상태가 확정 계약과 일치하는지 확인한다.
|
||
- [x] **REFACTOR:** creatorMember 표시 정보와 언어 이벤트를 포함한 focused/legacy characterization test 및 `ktlintCheck` 결과를 Progress에 기록한다.
|
||
- 검증 기록(RED): 무엇: v2 생성 request와 실패 순서 계약. 왜: 외부 ID·생성 활성 상태가 무시되고, 존재하지 않는 원작이 외부 생성 뒤에 실패했기 때문이다. 어떻게: `AiCharacterAdminCharacterControllerMutationTest`에 서버 소유 field, invalid `characterType`, 중복 이름, 원작, 외부 API, S3 실패와 creatorMember·언어 이벤트 assertion을 추가하고 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest`를 실행했다. 결과: 서버 소유 field는 400 기대 대비 200, 원작 실패는 외부 요청 0 기대 대비 1로 실패했다. S3 DB assertion의 최초 실패는 class-level test transaction 관찰 오류였으므로 해당 test만 transaction 밖에서 재실행해 DB rollback을 확인했다.
|
||
- 검증 기록(GREEN): 무엇: 서버 소유 create field 거부와 원작 선검증. 왜: `externalCharacterId`/생성 `isActive`를 client 입력으로 받지 않고, 존재하지 않는 원작에서 외부 캐릭터를 만들지 않기 위해. 어떻게: create DTO의 수신 field를 명시적으로 거부하고 facade에서 원작을 외부 호출 전에 조회한 뒤 같은 focused test를 실행했다. 결과: 10개 test가 `BUILD SUCCESSFUL`이었다.
|
||
- 검증 기록(REFACTOR): 무엇: 생성 focused/legacy parity 회귀와 formatting. 왜: 정상 원작 연결, AI creatorMember 표시 정보, 언어 감지 event, duplicate·외부 API·S3 실패의 결과를 legacy 특성화와 함께 유지하기 위해. 어떻게: 아래 Verify 명령과 `./gradlew ktlintCheck`를 실행했다. 결과: focused/legacy 명령은 `BUILD SUCCESSFUL in 40s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 11s`였다. 전체 `./gradlew test`는 task 범위가 character create slice이고 focused/legacy 명령으로 직접 영향 범위를 확인하므로 실행하지 않았다.
|
||
- Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.LegacyChatCharacterAdminCharacterizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.LegacyChatCharacterAdminCharacterControllerEventCharacterizationTest`
|
||
|
||
- [x] **Task 2.6: 캐릭터 수정·비활성화 흐름 보완**
|
||
|
||
**Goal 실행 `P2-T5`:** 캐릭터 수정과 `isActive=false`가 표시 정보를 동기화하고 연결 Member·콘텐츠를 보존하도록 완결한다.
|
||
|
||
- **추적 review ID:** `REV-002`, `REV-003`, `REV-007` 중 수정 request·응답·soft-delete parity.
|
||
- **시작 조건:** `P2-T4` 완료와 수정·비활성화 관련 확정 review ID.
|
||
- **완료 증거:** 수정·이미지 유지/교체·soft delete RED/GREEN, 보존/no-partial-update 검증과 Progress 기록.
|
||
- **범위 밖:** hard delete, cascade delete, 복원 API.
|
||
|
||
**Files:**
|
||
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterFacade.kt`
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterDto.kt`
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterExternalApiClient.kt`
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterImageStorage.kt`
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterMapper.kt`
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt`
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/LegacyChatCharacterAdminCharacterizationTest.kt`
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/LegacyChatCharacterAdminCharacterControllerEventCharacterizationTest.kt`
|
||
|
||
- [x] `externalCharacterId`, 일반 수정과 `isActive=false` 혼합 요청, invalid `characterType`의 canonical update 계약을 확정하고 충돌 시 Decision Log를 먼저 갱신한다.
|
||
- [x] **RED:** 문서 PUT JSON의 field 반영, soft delete와 일반 수정 혼합, image 동시 요청, 외부 수정 성공 후 S3/DB 실패, `updatedAtUtc`의 flush 전 mapping을 각각 재현한다.
|
||
- [x] **RED 확인:** soft delete 성공·실패에서 row·Member·콘텐츠 보존, 미참조 S3 객체 0건, 외부/DB 상태 일치와 응답 timestamp가 후속 GET과 같은지 확인한다.
|
||
- [x] **GREEN:** 확정 계약에 맞춰 혼합 요청을 명시적으로 처리하고, 불필요한 upload를 차단하며 외부/S3/DB 보상 경계와 flush 후 response mapping을 최소 구현한다.
|
||
- [x] **GREEN 확인:** 같은 수정 focused/characterization test를 다시 실행해 field 반영, 보상 결과, soft-delete 보존과 timestamp가 모두 통과하는지 확인한다.
|
||
- [x] **REFACTOR:** creatorMember 표시 정보·번역 event와 legacy `characterType` 동작을 포함한 focused/characterization test 및 `ktlintCheck` 결과를 Progress에 기록한다.
|
||
- 검증 기록(RED): 무엇: PUT의 서버 소유 external ID, `isActive=false` 혼합 image, 존재하지 않는 원작과 flush 전 timestamp 계약. 왜: 기존 구현이 client external ID를 무시하고 soft delete 전에 image를 업로드하며, 원작 검증 후 외부 수정과 이전 `updatedAtUtc`를 반환했기 때문이다. 어떻게: `AiCharacterAdminCharacterControllerMutationTest`에 해당 회귀, image 유지·교체, S3 실패 경계, creatorMember·콘텐츠 보존과 번역 event test를 추가하고 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest`를 실행했다. 결과: 16건 중 server-owned ID, mixed soft delete, missing original work, post-flush timestamp 4건이 의도대로 실패했다.
|
||
- 검증 기록(GREEN/REFACTOR): 무엇: canonical update 계약과 post-flush 응답. 왜: soft delete의 고아 image를 막고 정상 수정의 creatorMember 동기화·번역 event·timestamp 및 외부/S3/DB 실패 경계를 고정하기 위해. 어떻게: `externalCharacterId` 명시 거부, `isActive=false` 혼합 거부, 원작 선검증, `flush()` 후 response mapping을 적용한 뒤 아래 Verify 명령과 `./gradlew ktlintCheck`를 실행했다. 결과: 모두 `BUILD SUCCESSFUL`이었다. 전체 `./gradlew test`는 변경 범위가 character update slice이고 지정 focused/legacy 회귀가 직접 영향 범위를 포함하므로 실행하지 않았다.
|
||
- Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.LegacyChatCharacterAdminCharacterizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.LegacyChatCharacterAdminCharacterControllerEventCharacterizationTest`
|
||
|
||
- [x] **Task 2.7: Phase 2 보안·오류·회귀 보완**
|
||
|
||
**Goal 실행 `P2-T6`:** Phase 2의 모든 endpoint가 공통 ADMIN·오류·CORS 계약을 공유하고 legacy/public 계약을 회귀시키지 않음을 고정한다.
|
||
|
||
- **추적 review ID:** `REV-001`, `REV-007`, `REV-008`.
|
||
- **시작 조건:** `P2-T5` 완료 또는 앞선 Goal의 근거 있는 `해당 없음` 판정.
|
||
- **완료 증거:** endpoint 권한 매트릭스, 정확한 오류 status/key/KO·EN·JA, legacy 회귀와 Progress 기록.
|
||
- **범위 밖:** Phase 1 공통 security/error 구조 재설계, Phase 3 기능.
|
||
|
||
**Files:**
|
||
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterController.kt`
|
||
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerTest.kt`
|
||
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt`
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAuthorizationTest.kt`
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt`
|
||
|
||
- [x] **RED:** `/api/v2/admin/ai-characters/unmapped-path`가 detail `Long` binding에 잡혀 404 대신 400이 되는 KO/EN/JA·허용 Origin CORS 4건을 현재 Phase 1 오류 계약 test로 재현한다.
|
||
- [x] **RED:** 목록·상세·생성·수정 각각의 JWT role × DB role, stale ADMIN claim과 binding·multipart·domain/client/server 오류의 exact status/key/KO·EN·JA를 parameterized test로 고정한다.
|
||
- [x] **RED 확인:** 오류 계약과 실제 endpoint matrix를 실행해 404 회귀 4건과 누락된 인가·i18n assertion이 의도대로 실패하는지 확인한다.
|
||
- [x] **GREEN:** numeric `characterId`만 resource handler에 매핑되도록 최소 수정하고, Phase 2 오류 의미를 확정된 message key와 `ApiResponse.error`로 반환한다.
|
||
- [x] **GREEN 확인:** 같은 오류·인가 focused test를 다시 실행해 실제 endpoint의 status/header/envelope와 KO/EN/JA가 모두 통과하는지 확인한다.
|
||
- [x] **REFACTOR:** 실제 test 파일 목록과 targeted 명령을 대조해 존재하지 않는 `AiCharacterAdminCharacterServiceTest` 참조 및 과거 test 수 기록은 삭제하지 않고 정정 기록을 누적한다.
|
||
- [x] 기존 admin/public character contract, 신규 DTO 의존 방향과 Phase 2 focused test·`ktlintCheck` 결과를 Progress에 기록한다.
|
||
- 정정 기록: `AiCharacterAdminCharacterServiceTest`는 현재 존재하지 않는 과거 계획 참조다. P2-T6의 실제 범위는 `AiCharacterAdminCharacterControllerTest`, `AiCharacterAdminCharacterControllerMutationTest`, 공통 `AiCharacterAdminAuthorizationTest`, `AiCharacterAdminErrorContractTest`이며 기존 완료 이력과 과거 test 수 기록은 삭제하지 않았다.
|
||
- 검증 기록(RED): 무엇: 실제 character controller가 문자열 미매핑 경로를 404 fallback으로 넘기는지와 endpoint matrix. 왜: `/{characterId}`의 `Long` binding이 fallback 404 계약을 400으로 바꾸고 있었기 때문이다. 어떻게: 아래 Verify 명령을 production 변경 전 실행했다. 결과: 전체 146건 중 `unmapped-path` KO/EN/JA와 허용 Origin CORS 4건만 404 기대 대비 400으로 실패했고, 새 목록 binding·상세 target·생성/수정 multipart i18n 및 실제 endpoint non-ADMIN/CORS assertion은 통과했다.
|
||
- 검증 기록(GREEN/REFACTOR): 무엇: numeric path 제약과 Phase 2 ADMIN/error/CORS/legacy 회귀. 왜: 문자열 segment는 fallback 404로, 숫자 resource와 기존 public/legacy는 기존 계약으로 유지해야 하기 때문이다. 어떻게: controller의 GET/PUT path를 `[0-9]+`로 제한한 뒤 아래 Verify 명령과 `./gradlew ktlintCheck`를 실행했다. 결과: focused command는 `BUILD SUCCESSFUL in 1m 26s`, ktlint는 `BUILD SUCCESSFUL in 34s`였고, P2-GATE는 이 Task 범위 밖으로 미완료 상태를 유지한다.
|
||
- Verify: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest`
|
||
|
||
#### Phase 2 Gate
|
||
|
||
**Goal 실행 `P2-GATE`:** Phase 2 캐릭터 관리의 PRD 추적성, 정상·실패 흐름과 legacy 회귀를 최종 판정한다.
|
||
|
||
- [x] **`P2-GATE` 완료:** 시작 조건과 완료 증거를 모두 충족하고 현재 상태표와 Progress를 갱신한다.
|
||
|
||
- **시작 조건:** `P2-R1`, `P2-T3`~`P2-T6` 완료 또는 근거 있는 `해당 없음` 판정.
|
||
- **완료 증거:** 아래 명령 성공, review 후보 0건, 확정 finding 처리 완료와 Progress 기록.
|
||
- **범위 밖:** Gate 통과를 위한 test 삭제·완화, Phase 3 기능 수정.
|
||
|
||
- [x] `REV-001`~`REV-003`, `REV-007`, `REV-008`의 계약 결정·failure matrix·수정 test와 실제 결과가 각 소유 Goal의 Progress에 연결됐다.
|
||
- [x] 캐릭터 목록 exact key, 문서 mutation JSON, 동시 중복 결과, 외부/S3/DB 보상, post-flush `updatedAtUtc`와 실제 endpoint 권한·i18n matrix에 미결정 항목이 없다.
|
||
- [x] 완료 이력의 누락 test 파일·test 수·failure-order 증거는 원문을 삭제하지 않고 최신 정정 기록으로 재현 가능하게 남겼다.
|
||
- 검증 기록: 무엇: `P2-GATE` Phase 2 최종 판정. 왜: `P2-R1`, `P2-T3`~`P2-T6`의 확정 finding 처리와 Gate 명령 성공을 확인하기 위해. 어떻게: 아래 세 Gate 명령을 실행했다. 결과: character focused 명령은 최초 병렬 실행 중 XML test result write 충돌로 실패했으나 동일 명령 단독 재실행은 `BUILD SUCCESSFUL in 1m 11s`였다. authorization/error 명령은 `BUILD SUCCESSFUL in 1m 30s`, `ktlintCheck`는 `BUILD SUCCESSFUL`이었다. `git diff --check`도 통과했다.
|
||
|
||
```bash
|
||
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*'
|
||
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
|
||
./gradlew ktlintCheck
|
||
```
|
||
|
||
**Expected:** 모든 명령 exit code 0, Phase 2 review의 후보·확정 미처리 항목 0건, 관련 legacy/public 계약 diff 없음.
|
||
|
||
#### Phase 2 후속 리뷰 보완
|
||
|
||
- [x] **Task 2.8: 실제 character endpoint 보안·오류·실패 경계 증거 보강**
|
||
|
||
**Goal 실행 `P2-R2`:** `REV-009`에서 확인한 실제 character endpoint별 인가·CORS·오류와 mutation 실패 경계를 non-vacuous 회귀 test로 고정한다.
|
||
|
||
- **추적 review ID:** `REV-009`.
|
||
- **시작 조건:** 기존 `P2-GATE` 완료 이력과 `phase2-character-review.md` 2차 리뷰 판정 존재.
|
||
- **완료 증거:** 아래 실제 endpoint test, 공통 authorization/error 회귀, `ktlintCheck`와 Progress 기록.
|
||
- **범위 밖:** 공통 Phase 1 security/error 재설계, external API 보상 endpoint·신규 DDL 추가, Phase 3 기능.
|
||
- **TDD 예외 사유:** 현재 production 실패가 아니라 완료 기록 대비 직접 검증 증거 누락이 확정된 test 보강 Task다.
|
||
- **대체 검증 방법:** 실제 endpoint test를 먼저 추가하고, 현재 동작이 계약을 만족하면 production code 변경 없이 통과 증거를 기록한다. 계약 불일치가 재현될 때만 해당 assertion의 RED를 확인하고 최소 수정한다.
|
||
|
||
**Files:**
|
||
|
||
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterController.kt`
|
||
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterFacade.kt`
|
||
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerTest.kt`
|
||
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt`
|
||
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAuthorizationTest.kt`
|
||
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt`
|
||
|
||
- [x] **CONTRACT TEST:** 목록·상세·생성·수정 실제 경로에서 JWT role × 현재 DB role과 stale ADMIN claim을 parameterized test로 고정한다.
|
||
- [x] **CONTRACT TEST:** 실제 GET/POST/PUT의 허용·거부 Origin/preflight와 대표 binding·domain·client·server 오류의 exact status/key/KO·EN·JA envelope를 고정한다.
|
||
- [x] **FAILURE TEST:** 중복·원작·external API·S3·DB 실패에서 DB/creatorMember/originalWork/S3/external/event 결과를 직접 단언하고 기존 non-compensated external 경계를 유지한다.
|
||
- [x] **GREEN:** 새 test가 현재 계약 불일치를 재현할 때만 가장 작은 production 수정으로 통과시키고, 이미 통과하면 production code를 변경하지 않는다.
|
||
- [x] **REFACTOR:** character focused와 legacy characterization, 공통 authorization/error 및 `ktlintCheck` 결과를 Progress에 기록한다.
|
||
- 검증 기록: 무엇: `REV-009`의 실제 character endpoint 증거 보강. 왜: 기존 Gate 기록이 네 endpoint의 stale claim, allow/deny CORS preflight, KO/EN/JA 실패 envelope를 직접 매트릭스로 고정했다는 증거가 부족했기 때문이다. 어떻게: `AiCharacterAdminCharacterControllerTest`에 목록·상세·생성·수정 실제 경로의 stale ADMIN claim 403과 허용/거부 Origin preflight를 추가하고, `AiCharacterAdminCharacterControllerMutationTest`의 external API 실패, 생성 S3 실패, 수정 S3 실패를 KO/EN/JA envelope와 잔존 DB/S3/external 상태 단언으로 확장했다. 결과: production code 변경 없이 아래 focused 명령이 `BUILD SUCCESSFUL in 1m 47s`였다.
|
||
|
||
```bash
|
||
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
|
||
./gradlew ktlintCheck
|
||
```
|
||
|
||
#### Phase 2 후속 리뷰 Gate
|
||
|
||
**Goal 실행 `P2-R2-GATE`:** `REV-009`의 직접 증거를 재검토하고 Phase 2 후속 리뷰를 종결한다.
|
||
|
||
- [x] **`P2-R2-GATE` 완료:** `P2-R2` 완료 후 fresh 검증과 리뷰 문서 수정 후 기록을 남긴다.
|
||
- **시작 조건:** `P2-R2` 완료.
|
||
- **완료 증거:** `REV-009` 수정 완료, 위 두 명령 성공, `phase2-character-review.md` 최신 결론과 Progress 동기화.
|
||
- **범위 밖:** 기존 `P2-GATE` 이력 수정, Phase 3 production 변경.
|
||
- 검증 기록: 무엇: `P2-R2-GATE` 후속 리뷰 종결. 왜: `REV-009`의 직접 증거가 추가됐고 Phase 3 후속 보완으로 넘어갈 수 있는지 판정하기 위해. 어떻게: `phase2-character-review.md`에 3차 후속 검증 기록을 누적하고 위 focused 명령을 fresh 실행했다. 결과: `BUILD SUCCESSFUL in 1m 47s`였고, `REV-009`는 처리 완료로 판정했다. `ktlintCheck`는 Phase 3 후속 보완까지 완료한 뒤 공통으로 실행해 전체 후속 범위 검증 기록에 남긴다.
|
||
|
||
#### Phase 2 4차 리뷰 보완
|
||
|
||
- [x] **Task 2.9: character DB·event 실패 경계 증거 보강**
|
||
|
||
**Goal 실행 `P2-R3`:** `REV-012`에서 남은 character 생성·수정의 DB/event 실패 후 내부·외부 부작용 경계를 실제 흐름으로 고정한다.
|
||
|
||
- **추적 review ID:** `REV-012`.
|
||
- **시작 조건:** 기존 `P2-R2-GATE` 완료 이력과 `phase2-character-review.md` 4차 리뷰 판정 존재.
|
||
- **완료 증거:** 실제 mutation failure test, character/common 회귀, `ktlintCheck`와 Progress 기록.
|
||
- **범위 밖:** external character API 보상 endpoint 추가, 신규 DDL, Phase 3 이후 production 변경.
|
||
- **TDD 예외 사유:** 현재 production 실패가 아니라 `Task 2.8` 완료 기록 대비 직접 검증 증거 누락이 확정된 test 보강 Task다.
|
||
- **대체 검증 방법:** 실제 DB flush/save 또는 event publish 실패를 먼저 재현하고, 현재 transaction·비보상 경계가 계약과 일치하면 production code 변경 없이 관찰 결과를 고정한다.
|
||
|
||
**Files:**
|
||
|
||
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterFacade.kt`
|
||
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt`
|
||
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/LegacyChatCharacterAdminCharacterizationTest.kt`
|
||
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt`
|
||
|
||
- [x] **FAILURE CHARACTERIZATION:** 생성·수정의 DB flush/save와 event publish 실패를 실제 transaction 경계에서 재현한다.
|
||
- [x] **CONTRACT TEST:** 각 실패 뒤 ChatCharacter·creatorMember·originalWork·event와 이미 발생한 external/S3 결과를 직접 단언한다.
|
||
- [x] **CONTRACT TEST:** 대표 실패의 exact HTTP status와 KO/EN/JA `ApiResponse.error`를 실제 mutation endpoint에서 확인한다.
|
||
- [x] **GREEN:** 현재 계약 위반이 재현될 때만 최소 production 수정으로 통과시키고, 기존 비보상 경계와 일치하면 test-only로 종료한다.
|
||
- [x] **REFACTOR:** character/common focused 회귀와 `ktlintCheck` 결과를 Progress와 리뷰 수정 후 기록에 누적한다.
|
||
- 검증 기록: 무엇: `REV-012`의 character 생성·수정 실패 경계 증거를 보강했다. 왜: 기존 완료 기록이 external/S3 실패는 확인했지만 event publish 실패와 KO/EN/JA 대표 실패 경계를 직접 고정하지 않았기 때문이다. 어떻게: `AiCharacterAdminCharacterControllerMutationTest`에 external/S3 실패 locale matrix와 facade 직접 event 실패 특성화를 추가했다. 결과: production code 변경 없이 mutation focused 명령은 `BUILD SUCCESSFUL in 1m 1s`였다.
|
||
|
||
```bash
|
||
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
|
||
./gradlew ktlintCheck
|
||
```
|
||
|
||
#### Phase 2 4차 리뷰 Gate
|
||
|
||
**Goal 실행 `P2-R3-GATE`:** `REV-012`의 DB/event 실패 경계 증거를 재검토하고 Phase 2 4차 리뷰를 종결한다.
|
||
|
||
- [x] **`P2-R3-GATE` 완료:** `P2-R3` 완료 후 fresh 검증과 리뷰 문서 수정 후 기록을 남긴다.
|
||
- **시작 조건:** `P2-R3` 완료.
|
||
- **완료 증거:** `REV-012` 처리 완료, 위 두 명령 성공, `phase2-character-review.md` 최신 결론과 Progress 동기화.
|
||
- **범위 밖:** 기존 Phase 2 완료 이력 수정, Phase 3 production 변경.
|
||
- 검증 기록: 무엇: Phase 2 4차 리뷰의 `REV-012` 처리를 종결했다. 왜: Phase 3 4차 보완으로 넘어가기 전 character failure evidence 완료 여부를 판정하기 위해. 어떻게: character mutation focused test와 최종 `ktlintCheck`를 실행하고 `phase2-character-review.md`를 처리 완료로 갱신했다. 결과: mutation focused 명령은 `BUILD SUCCESSFUL in 1m 1s`, 최종 `ktlintCheck`는 `BUILD SUCCESSFUL in 17s`였다.
|
||
|
||
#### Phase 2 5차 리뷰 보완
|
||
|
||
- [x] **Task 2.10: character 실제 transaction DB·event 실패 경계 완결**
|
||
|
||
**Goal 실행 `P2-R4`:** `REV-015`의 생성·수정 persistence/event 실패를 actual endpoint와 Spring transaction 경계에서 재현하고 내부 rollback·외부 비보상 결과를 고정한다.
|
||
|
||
- **추적 review ID:** `REV-015`.
|
||
- **시작 조건:** 기존 `P2-R3-GATE` 완료 이력과 `phase2-character-review.md` 5차 리뷰 판정 존재.
|
||
- **완료 증거:** actual POST/PUT failure test, transaction 종료 뒤 DB 재조회, character/common 회귀와 `ktlintCheck` 결과 및 Progress 기록.
|
||
- **범위 밖:** external character API 보상 endpoint, S3 object 정리 정책, 신규 DDL, Phase 3 이후 production 변경.
|
||
- **TDD 예외 사유:** 현재 production 결함보다 기존 `REV-012` 완료 기록의 transaction/rollback 직접 증거 누락이 확정된 회귀 검증 Task다.
|
||
- **대체 검증 방법:** test transaction 밖 actual endpoint와 repository/publisher failure injection으로 production proxy를 통과시키고, 요청 종료 뒤 내부 DB와 외부 interaction을 재조회한다. 계약 위반이 재현될 때만 최소 production 수정으로 전환한다.
|
||
|
||
**Files:**
|
||
|
||
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterFacade.kt`
|
||
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/repository/ChatCharacterRepository.kt`
|
||
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt`
|
||
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt`
|
||
|
||
- [x] **FAILURE CHARACTERIZATION:** `@Transactional(propagation = Propagation.NOT_SUPPORTED)` actual POST/PUT에서 `ApplicationEventPublisher` 실패를 주입하고 500 `common.error.unknown` envelope을 확인한다.
|
||
- [x] **PERSISTENCE FAILURE:** `@SpyBean ChatCharacterRepository`로 생성 `save`와 수정 `flush()` 실패를 각각 주입해 external 호출 뒤 transaction rollback 순서를 재현한다.
|
||
- [x] **CONTRACT TEST:** 생성 실패 뒤 ChatCharacter·creatorMember·originalWork 부재, 수정 실패 뒤 기존 character·creatorMember·originalWork 상태 유지, S3/event interaction과 external 호출 횟수를 직접 단언한다.
|
||
- [x] **CONTRACT TEST:** 대표 event/persistence 실패의 KO/EN/JA exact HTTP status/message를 actual mutation endpoint에서 확인한다.
|
||
- [x] **GREEN:** 현재 transaction 계약 위반이 재현될 때만 가장 작은 production 수정으로 통과시키고, 기존 rollback·비보상 경계와 일치하면 test-only로 종료한다.
|
||
- [x] **REFACTOR:** direct `createFacade` event failure test를 actual endpoint 증거로 대체하거나 역할을 명확히 축소하고 character/common 회귀와 `ktlintCheck` 결과를 Progress에 기록한다.
|
||
|
||
```bash
|
||
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
|
||
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
|
||
./gradlew ktlintCheck
|
||
```
|
||
|
||
- 검증 기록: 무엇: `REV-015`의 생성·수정 event/save/flush 실패가 실제 MockMvc POST/PUT와 facade transaction 경계를 통과해 내부 DB rollback과 외부 비보상 호출을 보이는지 고정했다. 왜: direct facade 호출은 Spring transaction proxy 및 요청 종료 뒤 DB 상태를 증명하지 못했기 때문이다. 어떻게: `NOT_SUPPORTED` test에서 facade proxy target의 publisher mock과 `@SpyBean ChatCharacterRepository` failure를 주입하고, `TransactionTemplate` 재조회로 상태를 확인했다. 결과: 초기 RED는 `@MockBean`이 이미 생성된 facade field를 대체하지 못해 실제 event listener가 실행되고 200이 반환된 것으로 확인됐으며, 실제 proxy target에 같은 mock을 교체한 뒤 KO/EN/JA 500 envelope, 생성 내부 state 부재, 수정 기존 state 유지, 외부 호출 1회와 image 없는 S3 미호출이 통과했다. production 변경은 없었다. `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest`는 `BUILD SUCCESSFUL in 1m 8s`, 영향 범위 character/auth/error 회귀는 `BUILD SUCCESSFUL in 1m 59s`, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 42s`였다.
|
||
|
||
#### Phase 2 5차 리뷰 Gate
|
||
|
||
**Goal 실행 `P2-R4-GATE`:** `REV-015`의 actual transaction·rollback 증거를 재검토하고 Phase 2 5차 리뷰를 종결한다.
|
||
|
||
- [x] **`P2-R4-GATE` 완료:** `P2-R4` 완료 후 위 명령을 fresh 실행하고 리뷰 문서·Progress를 갱신한다.
|
||
- **시작 조건:** `P2-R4` 완료.
|
||
- **완료 증거:** `REV-015` 수정 완료, actual endpoint transaction evidence, focused/영향 범위 회귀와 lint 성공.
|
||
- **범위 밖:** 기존 Phase 2 완료 이력 수정, Phase 3 production 변경.
|
||
- 진행 기록: `P2-R4` 구현과 fresh 검증을 완료했고, 5차 재리뷰에서 `REV-015` 보완 완료와 Phase 2 Gate 종료를 확인했다.
|
||
|
||
#### Phase 2 6차 리뷰 보완
|
||
|
||
- [x] **Task 2.11: 캐릭터 생성 Endpoint Contract Summary 동기화**
|
||
|
||
**Goal 실행 `P2-R5`:** `REV-018`의 캐릭터 생성 예시를 `DEC-P2-T4-001` 및 production request 계약과 일치시키고
|
||
actual endpoint 회귀로 확인한다.
|
||
|
||
- **추적 review ID:** `REV-018`.
|
||
- **시작 조건:** 기존 `P2-R4-GATE` 완료 이력과 `phase2-character-review.md` 6차 리뷰 판정 존재.
|
||
- **완료 증거:** Endpoint Contract Summary 생성 JSON 정정, 정상 생성·서버 소유 field 거부 actual endpoint 회귀,
|
||
`git diff --check`와 Progress 기록.
|
||
- **범위 밖:** production DTO/facade 변경, 캐릭터 생성 behavior 변경, 외부 API 계약 변경, Phase 3 이후 production 변경.
|
||
- **TDD 예외 사유:** production과 기존 actual endpoint test는 확정 계약을 충족하고 문서 예시만 반대로 남은 문서 정합성
|
||
수정 Task다.
|
||
- **대체 검증 방법:** 생성 예시의 exact field를 문자열 검색으로 확인하고 기존 정상 생성·서버 소유 field 거부 actual endpoint
|
||
test를 재실행한다.
|
||
|
||
**Files:**
|
||
|
||
- Modify: `docs/20260724_AI캐릭터_관리자_API/plan-task.md`
|
||
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterDto.kt`
|
||
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterFacade.kt`
|
||
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/character/AiCharacterAdminCharacterControllerMutationTest.kt`
|
||
|
||
- [x] **CONTRACT DOC:** POST 생성 예시에 필수 `systemPrompt`를 추가하고 request의 `externalCharacterId`, `isActive`를
|
||
제거한다.
|
||
- [x] **CONTRACT DOC 확인:** 생성 예시와 `DEC-P2-T4-001`을 대조해 request field와 response 전용 field가 일치하는지
|
||
확인한다.
|
||
- [x] **CONTRACT TEST:** `systemPrompt`를 포함하고 서버 소유 field를 제외한 actual POST가 성공하며 response에 외부 API가
|
||
반환한 `externalCharacterId`와 서버 생성 `isActive=true`가 있는지 확인한다.
|
||
- [x] **REJECTION TEST:** `externalCharacterId` 또는 생성 `isActive`가 포함된 actual POST가 외부/S3/DB/event 부작용 전
|
||
400으로 거부되는 기존 회귀를 확인한다.
|
||
- [x] **REFACTOR:** production 변경 없이 문서 diff와 focused test 결과를 Progress에 기록한다.
|
||
- 검증 기록: 무엇: `REV-018`의 캐릭터 생성 Endpoint Contract Summary 예시를 production 생성 request 계약과 동기화했다. 왜: 예시가 필수 `systemPrompt`를 누락하고 서버 소유 `externalCharacterId`, `isActive`를 포함했기 때문이다. 어떻게: `rg -n -A 12 'POST /api/v2/admin/ai-characters' docs/20260724_AI캐릭터_관리자_API/plan-task.md`로 예시 field를 확인하고 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest`를 실행했다. 결과: 예시는 `systemPrompt` 포함 및 서버 소유 field 제외로 확인됐고 focused test는 `BUILD SUCCESSFUL in 30s`였다.
|
||
|
||
```bash
|
||
rg -n -A 12 'POST /api/v2/admin/ai-characters' docs/20260724_AI캐릭터_관리자_API/plan-task.md
|
||
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest
|
||
git diff --check
|
||
```
|
||
|
||
#### Phase 2 6차 리뷰 Gate
|
||
|
||
**Goal 실행 `P2-R5-GATE`:** `REV-018`의 문서 계약 정합성을 재검토하고 Phase 2 6차 리뷰를 종결한다.
|
||
|
||
- [x] **`P2-R5-GATE` 완료:** `P2-R5` 완료 후 위 문서/focused 검증을 fresh 실행하고 리뷰 문서·Progress를 갱신한다.
|
||
- **시작 조건:** `P2-R5` 완료.
|
||
- **완료 증거:** `REV-018` 처리 완료, Endpoint Contract Summary·Decision Log·production 계약 일치, focused test와
|
||
diff check 성공.
|
||
- **범위 밖:** Gate에서 production code 수정, 기존 Phase 2 완료 이력 변경, Phase 3 production 변경.
|
||
- 검증 기록: 무엇: `P2-R5-GATE`에서 `REV-018` 문서 계약 정합성을 종결했다. 왜: Phase 3 6차 보완의 시작 조건이 `P2-R5-GATE` 완료이기 때문이다. 어떻게: `P2-R5` focused 검증 결과와 `phase2-character-review.md` 6차 판정을 대조했다. 결과: production 변경 없이 `REV-018` 처리 완료로 판정했다.
|
||
|
||
---
|
||
|
||
### 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/audio-content-themes`
|
||
- Request: query/body 없음.
|
||
- Response `data`:
|
||
|
||
```json
|
||
[
|
||
{
|
||
"themeId": 11,
|
||
"themeName": "ASMR",
|
||
"imageUrl": "https://cdn.example.com/audio-content-theme/asmr.png"
|
||
}
|
||
]
|
||
```
|
||
|
||
- 기존 크리에이터 관리자 콘텐츠 등록 화면의 콘텐츠 테마(카테고리) 조회와 같은 기능이다.
|
||
- 기존 내부/legacy DTO의 `id`, `theme`, `image` 필드명은 frontend 계약으로 노출하지 않고, 신규 v2 DTO의 `themeId`, `themeName`, `imageUrl`만 사용한다.
|
||
- `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`
|
||
- multipart `coverImage`, `audioFile`, `request` JSON string part를 사용한다.
|
||
- `request` JSON은 `title`, `description`, `tags`, `price`, `purchaseOption`, `limited`, `isAdult`, `isActive`, `themeId`, `releaseDateUtc?`, `seriesIds`, `isGeneratePreview`, `isOnlyRental`, `isPointAvailable`, `isCommentAvailable`, `isFullDetailVisible`, `previewStartTime`, `previewEndTime`, `languageCode`를 포함한다.
|
||
- legacy `CreateAudioContentRequest`의 `detail`은 v2 `description`, `releaseDate`는 UTC ISO-8601 `releaseDateUtc`로 받으며 facade에서 기존 pipeline 입력으로 변환한다.
|
||
- `PUT /api/v2/admin/ai-characters/{characterId}/audio-contents/{contentId}`
|
||
- `audioFile` 교체는 기존 creator/admin 수정 pipeline에 없는 동작이므로 Phase 3 범위에서는 제공하지 않는다. 오디오 파일 교체가 필요하면 별도 upload/processing parity 설계 후 추가한다.
|
||
- response item은 현 v2 목록 계약을 유지한다. response detail에는 기존 `GetAudioContentDetailResponse`의 필드 전체를 포함하고, `description`, `audioSignedUrl`, `releaseDateUtc`, `seriesIds`, `createdAtUtc`, `updatedAtUtc` 같은 v2 관리자 필드도 유지한다. 구매·좋아요·핀·추천·댓글 목록처럼 viewer 상태가 필요한 legacy 상세 필드는 관리자 상세에서 안전한 기본값을 반환한다.
|
||
|
||
#### 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`
|
||
|
||
- [x] **Task 3.1: 기존 콘텐츠와 signed URL 특성화 baseline 고정**
|
||
- **Goal 이력 `P3-H1`:** 기존 creator/admin 콘텐츠와 signed URL 동작을 신규 v2 비교 기준으로 고정했다.
|
||
- **완료 증거:** 아래 특성화 테스트와 `ktlintCheck` 실행 기록.
|
||
- **범위 밖:** 신규 v2 content endpoint 구현.
|
||
- 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'`
|
||
- 검증 기록 (2026-07-26): 위 특성화 테스트 6건과 `./gradlew ktlintCheck`가 모두 통과했다.
|
||
|
||
- [x] **Task 3.2: content controller/facade/DTO 구현**
|
||
- **Goal 이력 `P3-H2`:** 콘텐츠 조회·생성·수정의 최초 v2 API 구현을 제공했다.
|
||
- **완료 증거:** 아래 RED/GREEN 및 후속 보완 기록. 이 이력만으로 Phase 3 Gate 통과를 의미하지 않는다.
|
||
- **범위 밖:** 후속 심층 리뷰에서 확정되는 누락·회귀 보완.
|
||
- 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.*'`
|
||
- 구현 기록(2026-07-26): `GET` 목록/검색·상세와 `PUT` 수정/soft delete를 target resolver와 owner-scoped query로 구현했다. 응답은 v2 DTO만 사용하며 private content path 필드를 반환하지 않는다.
|
||
- 구현 보완(2026-07-27): `POST` 생성을 기존 `AudioContentService.createAudioContent` 재사용 방식으로 제공한다. v2 adapter에서 `themeId`, ISO-8601 `releaseDateUtc`, owner-scoped `seriesIds` 연결을 처리하고, 업로드 완료 전 `isActive` 직접 활성화는 기존 processing pipeline parity를 위해 수행하지 않는다.
|
||
- 검증 기록(RED, 2026-07-26): `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentControllerTest'`를 실행했고, 신규 route 부재로 목록은 404, 상세와 PUT은 404/405여서 의도대로 실패함을 확인했다.
|
||
- 검증 기록(GREEN, 2026-07-26): `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*'`와 `./gradlew ktlintCheck`를 실행해 모두 `BUILD SUCCESSFUL`을 확인했다.
|
||
- 검토 보완(2026-07-26): 상세 응답에 target owner 범위의 활성 series ID 목록을 추가하고, 다른 캐릭터 콘텐츠 PUT이 DB를 변경하지 않는 통합 테스트를 추가했다.
|
||
- 재검증(2026-07-26): `./gradlew test --rerun-tasks --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*'`를 실행해 `BUILD SUCCESSFUL`을 확인했다.
|
||
- 검토 보완(2026-07-27): 생성/수정의 `seriesIds` owner-scoped 연결, 생성의 ISO-8601 `releaseDateUtc` 변환, POST 제공 문서 불일치를 보완했다. 수정 `audioFile` 교체는 기존 creator/admin 수정 pipeline에 없어 Phase 3에서 미지원으로 명시했다.
|
||
|
||
- [x] **Task 3.3: Phase 3 요구사항·계약·코드 리뷰**
|
||
|
||
**Goal 실행 `P3-R1`:** PRD Feature C와 Endpoint Contract Summary를 콘텐츠 코드·테스트에 추적해 확정된 누락만 후속 Goal로 전환한다.
|
||
|
||
- **추적 review ID:** `REV-004`, `REV-005`, `REV-006`, `REV-007`, `REV-008`.
|
||
- **시작 조건:** `P2-GATE`, `P3-H1`, `P3-H2` 완료 증거 존재.
|
||
- **완료 증거:** `docs/sample/sample-review.md` 형식의 리뷰 문서, endpoint·pipeline·side-effect 추적표, 모든 후보 판정과 Progress 기록.
|
||
- **범위 밖:** 리뷰 도중 production code 수정, series/community 기능 검토.
|
||
- **TDD 예외 사유:** 구현이 아닌 read-only 리뷰 Task다.
|
||
- **대체 검증 방법:** PRD·계약·production·test를 대조하고 focused test를 실행해 실제 결과를 리뷰 문서에 기록한다.
|
||
|
||
**Files:**
|
||
|
||
- Create: `docs/20260724_AI캐릭터_관리자_API/reviews/phase3-audio-content-review.md`
|
||
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentController.kt`
|
||
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt`
|
||
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentDto.kt`
|
||
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentRepository.kt`
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentControllerTest.kt`
|
||
|
||
- [x] 테마·목록·검색·상세·생성·수정·soft delete endpoint와 DTO 필드를 PRD/계약에 추적한다.
|
||
- [x] signed URL TTL/path, private path 비노출, viewer 상태 기본값을 production·test에 추적한다.
|
||
- [x] 생성/update pipeline, 파일, 가격, 공개·예약, 번역·알림, `seriesIds`, 날짜 변환과 실패 순서를 확인한다.
|
||
- [x] owner 검증, no-side-effect, ADMIN 인가, 오류 i18n, pagination/multipart 계약을 확인한다.
|
||
- [x] `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*'` 결과와 후보 판정을 리뷰 문서에 기록한다.
|
||
- 검증 기록: 무엇: Phase 3 오디오 콘텐츠 slice의 read-only 요구사항·계약·코드 리뷰. 왜: `P3-H1`, `P3-H2` 완료 이력 이후 `REV-004`~`REV-008`의 실제 확정 여부와 후속 소유 Goal을 고정하기 위해. 어떻게: `docs/20260724_AI캐릭터_관리자_API/reviews/phase3-audio-content-review.md`에 PRD Feature C, Endpoint Contract Summary, production/test 대조표와 발견 사항을 기록하고 `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*'`를 실행했다. 결과: focused test는 `BUILD SUCCESSFUL in 2s`였고, `REV-004`~`REV-008`은 각각 `P3-T3`~`P3-T7`의 기존 소유 Goal로 연결했다. 리뷰 Task이므로 production code는 수정하지 않았다.
|
||
|
||
- [x] **Task 3.4: 활성 콘텐츠 테마 API 보완**
|
||
|
||
**Goal 실행 `P3-T3`:** 활성 콘텐츠 테마를 전용 `themeId/themeName/imageUrl` DTO로 반환하는 관리자 API를 완결한다.
|
||
|
||
- **추적 review ID:** `REV-007`, `REV-008` 중 v2 DTO 경계와 endpoint 인가.
|
||
- **시작 조건:** `P3-R1` 완료와 테마 endpoint review 판정.
|
||
- **완료 증거:** request body 없는 GET, 활성 필터, DTO field contract RED/GREEN과 Progress 기록.
|
||
- **범위 밖:** 테마 CRUD, legacy DTO 외부 노출.
|
||
|
||
**Files:**
|
||
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentController.kt`
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentDto.kt`
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt`
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentThemeControllerTest.kt`
|
||
|
||
- [x] **RED:** 테마 endpoint 부재 또는 계약 불일치와 legacy field 노출을 재현하는 가장 작은 실패 test를 작성한다.
|
||
- [x] **RED 확인:** focused test를 실행해 의도한 route·field assertion 실패를 확인한다.
|
||
- [x] **GREEN:** 활성 테마만 `themeId`, `themeName`, `imageUrl`로 반환하는 최소 구현을 작성한다.
|
||
- [x] **GREEN 확인:** request body 없음, exact field set과 ADMIN 이중 인가를 포함한 focused test 성공을 확인한다.
|
||
- [x] **REFACTOR:** v2 DTO 경계만 정리하고 테마 focused test와 `ktlintCheck` 결과를 Progress에 기록한다.
|
||
- 검증 기록: 무엇: `P3-T3`의 활성 콘텐츠 테마 endpoint 계약 증거를 전용 focused test로 분리했다. 왜: production 동작은 이미 `AiCharacterAdminAudioContentController`/`Facade`/DTO에서 충족하고 있었지만, `REV-007`, `REV-008` 기준 Gate 증거가 단일 대형 controller test에 섞여 있었기 때문이다. 어떻게: `AiCharacterAdminAudioContentThemeControllerTest`를 추가해 활성 필터, orders 정렬, `themeId/themeName/imageUrl` exact field, legacy `id/theme/image` 비노출과 anonymous 401을 검증하고 기존 controller test의 중복 테마 케이스를 제거했다. 결과: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentThemeControllerTest`는 `BUILD SUCCESSFUL in 35s`, 테마+기존 controller focused 회귀는 `BUILD SUCCESSFUL in 48s`, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 11s`였다. production code는 추가하지 않았다.
|
||
- Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentThemeControllerTest`
|
||
|
||
- [x] **Task 3.5: 콘텐츠 목록·상세·signed URL 보완**
|
||
|
||
**Goal 실행 `P3-T4`:** owner-scoped 콘텐츠 조회와 signed URL·상세 DTO 계약을 독립적으로 완결한다.
|
||
|
||
- **추적 review ID:** `REV-005`, `REV-007` 중 상세 parity와 v2 전용 DTO 경계.
|
||
- **시작 조건:** `P3-T3` 완료와 조회/signed URL 관련 review 판정.
|
||
- **완료 증거:** 검색·status·pagination·상세·TTL/path/private 정보 RED/GREEN과 Progress 기록.
|
||
- **범위 밖:** 콘텐츠 생성·수정, community audio 30분 정책.
|
||
|
||
**Files:**
|
||
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt`
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentDto.kt`
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentMapper.kt`
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentRepository.kt`
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentQueryTest.kt`
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AudioContentCloudFrontCharacterizationTest.kt`
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/content/AudioContentServiceTest.kt`
|
||
|
||
- [x] **RED:** `purchaseOption=RENT_ONLY`, entity `isOnlyRental=true`, 미래·과거 `releaseDate` 조합에서 legacy 상세와 다른 `isOnlyRental`, `purchaseOption`, `releaseDate`를 재현한다.
|
||
- [x] **RED:** 응답의 creator·buyer·other content·comment·translation 중첩 타입이 legacy/public DTO package에 직접 의존하는 현재 경계를 검출하고 exact JSON key를 고정한다.
|
||
- [x] **RED 확인:** query/legacy baseline test를 실행해 세 compatibility field와 금지 DTO 의존이 의도대로 실패하는지 확인한다.
|
||
- [x] **GREEN:** legacy 파생 규칙과 현지화된 `releaseDate` 의미를 유지하고 UTC 원본은 `releaseDateUtc`에만 반환하며, 동일 JSON을 v2 전용 중첩 DTO로 최소 매핑한다.
|
||
- [x] **GREEN 확인:** 같은 query/legacy baseline test를 다시 실행해 legacy compatibility field, `releaseDateUtc`와 exact JSON schema가 모두 통과하는지 확인한다.
|
||
- [x] **REFACTOR:** owner·검색·status·pagination, 활성 owner-scoped `seriesIds`, viewer 기본값, signed URL TTL/path와 private 정보 비노출을 함께 회귀한다.
|
||
- [x] 조회/signed URL focused test와 `ktlintCheck` 결과를 Progress에 기록한다.
|
||
- 검증 기록(RED): 무엇: 상세 응답의 legacy `releaseDate` 의미, RENT_ONLY 파생값과 v2 전용 중첩 DTO 경계. 왜: `REV-005`, `REV-007`에서 상세 DTO가 legacy/public 중첩 DTO에 직접 의존하고, 과거 공개일을 legacy `releaseDate`에도 노출하고 있었기 때문이다. 어떻게: `AiCharacterAdminAudioContentQueryTest`를 추가하고 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentQueryTest`를 실행했다. 결과: 2건이 의도대로 실패했다. 하나는 `releaseDate`가 존재해서 실패했고, 하나는 legacy nested DTO package 누출 assertion으로 실패했다.
|
||
- 검증 기록(GREEN/REFACTOR): 무엇: 상세 응답의 `releaseDateUtc` 전용 노출, RENT_ONLY 파생 규칙, v2 전용 중첩 DTO. 왜: 관리자 상세는 UTC 원본을 `releaseDateUtc`에만 고정하고, response DTO는 legacy/public DTO 타입을 외부 계약으로 재노출하지 않아야 하기 때문이다. 어떻게: `AiCharacterAdminAudioContentDto`에 v2 중첩 response DTO를 추가하고, `AiCharacterAdminAudioContentMapper`의 `releaseDate`, `isOnlyRental`, `purchaseOption`, `creator` mapping만 최소 수정했다. 결과: `AiCharacterAdminAudioContentQueryTest`는 `BUILD SUCCESSFUL in 29s`, 계획서 Verify 묶음은 `BUILD SUCCESSFUL in 41s`, `./gradlew ktlintCheck`는 최초 unused import 2건으로 실패 후 정리 재실행에서 `BUILD SUCCESSFUL in 10s`였다.
|
||
- Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentQueryTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AudioContentCloudFrontCharacterizationTest --tests kr.co.vividnext.sodalive.content.AudioContentServiceTest`
|
||
|
||
- [x] **Task 3.6: 콘텐츠 생성 pipeline 보완**
|
||
|
||
**Goal 실행 `P3-T5`:** 콘텐츠 생성의 multipart 입력, legacy field 변환, 파일·processing·series 연결과 side effect parity를 완결한다.
|
||
|
||
- **추적 review ID:** `REV-006`, `REV-007`, `REV-008` 중 생성 binding·field·pipeline 특성화.
|
||
- **시작 조건:** `P3-T4` 완료와 생성 pipeline 관련 review 판정.
|
||
- **완료 증거:** 전체 생성 field·파일·날짜·series RED/GREEN, 실패 순서와 focused/legacy test 기록.
|
||
- **범위 밖:** upload/processing pipeline 정책 변경, 오디오 파일 교체.
|
||
|
||
**Files:**
|
||
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentController.kt`
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt`
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentDto.kt`
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCreateTest.kt`
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/LegacyCreatorAdminAudioContentCharacterizationTest.kt`
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt`
|
||
|
||
- [x] `tags` 필수 여부와 생성 `isActive=false`의 canonical 계약을 legacy pipeline·Endpoint Contract Summary로 확정하고 충돌 시 코드 수정 전에 Decision Log를 갱신한다.
|
||
- [x] **RED:** `coverImage`, `audioFile`, `request` 각 part 누락에서 Kotlin nullable 때문에 `MissingServletRequestPartException`이 발생하지 않는 현재 binding과 KO/EN/JA envelope 차이, facade·DB·S3·event 호출 0건 기대를 재현한다.
|
||
- [x] **RED:** 생성 request 전체 field, `description/releaseDateUtc` 변환, `tags` 누락, `isActive=false`, target·theme·`seriesIds` 오류와 S3/processing/event 실패 순서를 각각 고정한다.
|
||
- [x] **RED 확인:** create/error/legacy characterization test를 실행해 part별 exception·field 계약·failure order가 의도대로 실패하는지 확인한다.
|
||
- [x] **GREEN:** 필수 file part를 non-null binding으로 만들고 확정된 field 계약, 외부 부작용 전 참조 검증과 legacy upload/processing parity를 최소 구현한다.
|
||
- [x] **GREEN 확인:** 같은 test를 다시 실행해 part별 400/i18n, 정상 생성과 실패 후 DB/S3/event 결과가 모두 통과하는지 확인한다.
|
||
- [x] **REFACTOR:** cover/audio upload, 가격·공개·예약·번역·알림 및 실패 후 DB/S3/event 결과를 characterization/focused test로 회귀하고 `ktlintCheck` 결과를 Progress에 기록한다.
|
||
- 검증 기록(RED): 무엇: 콘텐츠 생성의 필수 multipart part와 legacy 기본 계약. 왜: `REV-006`에서 생성 binding·field·failure-order 증거가 분리되지 않았고, `tags` 누락과 `isActive=true` 요청의 canonical 동작을 확정해야 했기 때문이다. 어떻게: `AiCharacterAdminAudioContentCreateTest`를 추가하고 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest`를 실행했다. 결과: 정상 생성 케이스가 200 기대 대비 500으로 실패했고, 원인은 production 계약이 아니라 test fixture의 `AmazonS3Client.getUrl(String, String)` 미설정으로 `S3Uploader.putS3`에서 null URL이 발생한 것이었다.
|
||
- 검증 기록(GREEN/REFACTOR): 무엇: 생성 필수 part 400, 업로드 전 S3 0회, `tags` 누락 허용, `isActive=true` 요청의 legacy processing 기본값. 왜: 신규 v2 생성은 기존 upload/processing pipeline을 바꾸지 않고 adapter 계약만 고정해야 하기 때문이다. 어떻게: test fixture에 `amazonS3Client.getUrl(...)` mock만 추가하고 production code는 변경하지 않았다. 결과: `AiCharacterAdminAudioContentCreateTest`는 `BUILD SUCCESSFUL`, create+legacy+error contract 회귀는 `BUILD SUCCESSFUL`, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 10s`였다.
|
||
- Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.LegacyCreatorAdminAudioContentCharacterizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest`
|
||
|
||
- [x] **Task 3.7: 콘텐츠 수정·soft delete 보완**
|
||
|
||
**Goal 실행 `P3-T6`:** 콘텐츠 수정, cover 유지/교체, series 재연결과 soft delete를 owner-safe하게 완결한다.
|
||
|
||
- **추적 review ID:** `REV-004`.
|
||
- **시작 조건:** `P3-T5` 완료와 수정·삭제 관련 review 판정.
|
||
- **완료 증거:** 수정·soft delete·audioFile 미지원·cross-owner RED/GREEN과 Progress 기록.
|
||
- **범위 밖:** audio file 교체, hard delete.
|
||
|
||
**Files:**
|
||
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt`
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentRepository.kt`
|
||
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/series/SeriesContent.kt`
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentUpdateTest.kt`
|
||
|
||
- [x] **RED:** 기존 `SeriesContent.orders`, row ID, `createdAt`이 있는 콘텐츠에 동일 `seriesIds`를 PUT했을 때 전부 삭제·재생성되는 현재 동작을 실패 test로 고정한다.
|
||
- [x] **RED 확인:** 동일 ID, 추가 ID, 제거 ID를 각각 요청해 교집합 metadata 보존과 차집합만 insert/delete한다는 기대가 현재 실패하는지 확인한다.
|
||
- [x] **GREEN:** 기존 연결과 요청 ID의 차집합만 변경하고 교집합 row의 ID·`orders`·`createdAt`을 보존하는 최소 구현을 작성한다.
|
||
- [x] **GREEN 확인:** 같은 update test를 다시 실행해 동일 집합 no-op, 교집합 metadata 보존과 차집합 변경만 발생하는지 확인한다.
|
||
- [x] **REFACTOR:** cover 유지/교체, 날짜, `audioFile` 미지원, `isActive=false`와 cross-owner/invalid series의 DB/S3/event no-side-effect를 회귀한다.
|
||
- [x] 수정 focused test와 `ktlintCheck` 결과를 Progress에 기록한다.
|
||
- 검증 기록(RED): 무엇: 동일 `seriesIds` 수정 시 기존 `SeriesContent` metadata 보존. 왜: `REV-004`에서 기존 구현이 연결을 모두 삭제·재생성해 row ID, `orders`, `createdAt`을 소실했기 때문이다. 어떻게: `AiCharacterAdminAudioContentUpdateTest`를 추가하고 focused 실행했다. 결과: 최초 focused test는 200 기대 대비 500으로 실패했고, 원인은 응답 매핑의 CloudFront private key fixture 문제임을 로그로 확인한 뒤 test fixture에 `AudioContentCloudFront` mock을 추가했다.
|
||
- 검증 기록(GREEN/REFACTOR): 무엇: `replaceSeriesIds`가 요청 ID와 기존 연결의 차집합만 변경하고 교집합 row를 보존하도록 수정했다. 왜: 동일 series 연결의 metadata를 유지하면서 제거·추가만 반영해야 하기 때문이다. 어떻게: `requestedIds`, 기존 연결 ID set을 비교해 삭제 대상만 remove하고 신규 ID만 persist했다. 결과: `AiCharacterAdminAudioContentUpdateTest`는 `BUILD SUCCESSFUL in 46s`, update+controller+legacy+error contract 회귀는 `BUILD SUCCESSFUL in 1m 21s`였다. 기존 controller 회귀 2건은 앞선 `P3-T4` 확정 계약(`releaseDate` 미노출, `RENT_ONLY` 파생값)에 맞춰 기대값만 갱신했다.
|
||
- Verify: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentUpdateTest`
|
||
|
||
- [x] **Task 3.8: 콘텐츠 ownership·오류·legacy 회귀 보완**
|
||
|
||
**Goal 실행 `P3-T7`:** Phase 3 모든 endpoint의 ownership·ADMIN·오류 계약과 legacy/public 회귀를 고정한다.
|
||
|
||
- **추적 review ID:** `REV-006`, `REV-007`, `REV-008`.
|
||
- **시작 조건:** `P3-T6` 완료 또는 앞선 Goal의 근거 있는 `해당 없음` 판정.
|
||
- **완료 증거:** 권한 매트릭스, 정확한 domain/client 오류 status/key/KO·EN·JA, no-side-effect와 legacy 회귀 기록.
|
||
- **범위 밖:** Phase 1 공통 handler 재설계, Phase 4 series 기능.
|
||
|
||
**Files:**
|
||
|
||
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentControllerTest.kt`
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentOwnershipTest.kt`
|
||
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAuthorizationTest.kt`
|
||
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt`
|
||
|
||
- [x] **RED:** 테마·목록·상세·생성·수정 endpoint 각각의 JWT role × DB role, stale ADMIN claim과 허용·거부 Origin을 parameterized test로 고정한다.
|
||
- [x] **RED:** target/content/theme/series/date와 세 multipart part 누락의 exact status, exception type, message key와 KO/EN/JA envelope를 실제 endpoint에서 고정한다.
|
||
- [x] **RED 확인:** 실제 endpoint matrix와 legacy characterization을 실행해 누락된 인가·i18n·failure-order assertion이 의도대로 실패하는지 확인한다.
|
||
- [x] **GREEN:** 확정된 domain/client/server 오류만 최소 매핑하고 ownership 실패 시 DB insert/update/delete, S3, event 0건을 보장한다.
|
||
- [x] **GREEN 확인:** 같은 endpoint/error/ownership test를 다시 실행해 status/header/envelope, KO/EN/JA와 no-side-effect가 모두 통과하는지 확인한다.
|
||
- [x] **REFACTOR:** legacy characterization에 validation·파일·가격·공개/예약·번역/알림·failure order를 보강하고 signed URL edge case와 함께 실행한다.
|
||
- [x] 실제 test 파일 목록과 targeted 명령을 대조해 존재하지 않는 `AiCharacterAdminAudioContentServiceTest`, `AiCharacterAdminAudioSignedUrlTest` 참조와 과거 test 수는 삭제하지 않고 정정 기록을 누적한다.
|
||
- [x] `AiCharacterAdminAudioContentThemeControllerTest`, `AiCharacterAdminAudioContentQueryTest`, `AiCharacterAdminAudioContentCreateTest`, `AiCharacterAdminAudioContentUpdateTest`, `AiCharacterAdminAudioContentOwnershipTest`의 파일 존재와 각 소유 계약 통과를 확인한다.
|
||
- [x] creator/admin/public content 회귀, 신규 DTO 의존 방향과 focused test·`ktlintCheck` 결과를 Progress에 기록한다.
|
||
- 검증 기록(RED): malformed `characterId`/`contentId` 6건이 미매핑 404 EN envelope을 기대한 새 ownership focused test에서 GET 400, write 415를 반환해 18건 중 6건이 실패했다.
|
||
- 검증 기록(GREEN/REFACTOR): 모든 resource path를 `[0-9]+`로 제한한 뒤 malformed path 404, 실제 5개 endpoint의 non-ADMIN/stale claim 403, 테마 CORS allow/deny, unknown target 생성 S3 0회를 고정했다. common authorization/error test의 KO·EN·JA matrix와 기존 content/legacy characterization을 재사용했다. focused는 `BUILD SUCCESSFUL in 1m 5s`, content+authorization+error 회귀는 `BUILD SUCCESSFUL in 2m 8s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 29s`였다. `AiCharacterAdminAudioContentServiceTest`, `AiCharacterAdminAudioSignedUrlTest`는 현재 존재하지 않는 과거 계획 참조이며 이 기록으로 정정한다.
|
||
- Verify: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest`
|
||
|
||
#### Phase 3 Gate
|
||
|
||
**Goal 실행 `P3-GATE`:** Phase 3 콘텐츠 관리와 signed URL의 PRD 추적성, pipeline 안전성과 legacy 회귀를 최종 판정한다.
|
||
|
||
- [x] **`P3-GATE` 완료:** 시작 조건과 완료 증거를 모두 충족하고 현재 상태표와 Progress를 갱신한다.
|
||
|
||
- **시작 조건:** `P3-R1`, `P3-T3`~`P3-T7` 완료 또는 근거 있는 `해당 없음` 판정.
|
||
- **완료 증거:** 아래 명령 성공, review 후보 0건, 확정 finding 처리 완료와 Progress 기록.
|
||
- **범위 밖:** Gate 실패와 무관한 Phase 4 기능 구현.
|
||
|
||
- [x] `REV-004`~`REV-008`의 response parity matrix, multipart exception, series metadata, DTO 경계와 test 증거가 각 소유 Goal의 Progress에 연결됐다.
|
||
- [x] 동일 `seriesIds`의 row metadata 보존, legacy `releaseDate`·rental 파생값, 세 필수 part와 실제 endpoint 권한·i18n matrix에 미결정 항목이 없다.
|
||
- [x] 완료 이력의 누락 test 파일·test 수·characterization 범위는 원문을 삭제하지 않고 최신 정정 기록으로 재현 가능하게 남겼다.
|
||
- 검증 기록: 무엇: `P3-GATE` Phase 3 최종 판정. 왜: `P3-R1`, `P3-T3`~`P3-T7`의 확정 finding 처리와 Gate 명령 성공을 확인하기 위해. 어떻게: 아래 세 Gate 명령을 fresh 실행했다. 결과: content focused 명령은 `BUILD SUCCESSFUL in 2m 15s`, authorization/error 명령은 `BUILD SUCCESSFUL in 1m 29s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 1s`였다. `git diff --check`는 `P3-T7` 완료 전 실행에서 출력 없음이었다. `REV-004`~`REV-008`의 Phase 3 소유 항목은 처리 완료로 판정했다.
|
||
|
||
```bash
|
||
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*'
|
||
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
|
||
./gradlew ktlintCheck
|
||
```
|
||
|
||
**Expected:** 모든 명령 exit code 0, Phase 3 review의 후보·확정 미처리 항목 0건, Phase 4가 소비할 owner query 계약 확정.
|
||
|
||
#### Phase 3 후속 리뷰 보완
|
||
|
||
- [x] **Task 3.9: 콘텐츠 생성 pipeline·multipart 오류 증거 보강**
|
||
|
||
**Goal 실행 `P3-R2`:** `REV-010`의 생성 field adapter, 선검증, multipart 오류와 S3/processing/event 실패 순서를 실제 endpoint에서 고정한다.
|
||
|
||
- **추적 review ID:** `REV-010`.
|
||
- **시작 조건:** `P2-R2-GATE` 완료와 기존 `P3-GATE` 완료 이력 존재.
|
||
- **완료 증거:** 생성 actual endpoint/legacy characterization test, 실패 지점별 관찰 결과, `ktlintCheck`와 Progress 기록.
|
||
- **범위 밖:** legacy upload/processing 정책 변경, 추정에 의한 S3 보상 추가, audio file 교체.
|
||
- **TDD 예외 사유:** 현재 production 실패가 아니라 `P3-T5` 완료 기록 대비 직접 검증 증거 누락이 확정된 test 보강 Task다.
|
||
- **대체 검증 방법:** legacy와 actual endpoint를 characterization하고 불일치가 재현될 때만 RED/GREEN으로 최소 수정한다.
|
||
|
||
**Files:**
|
||
|
||
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentController.kt`
|
||
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt`
|
||
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCreateTest.kt`
|
||
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/LegacyCreatorAdminAudioContentCharacterizationTest.kt`
|
||
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt`
|
||
|
||
- [x] **CONTRACT TEST:** `coverImage`, `audioFile`, `request` 누락의 실제 endpoint KO/EN/JA status/key/envelope와 facade·DB·S3·event 0건을 고정한다.
|
||
- [x] **CONTRACT TEST:** 생성 request 전체 field의 legacy adapter 결과와 target/theme/series 선검증을 확인한다.
|
||
- [x] **FAILURE CHARACTERIZATION:** cover upload, audio upload와 event 실패 지점별 DB/S3/event 결과를 legacy parity와 대조하고 비트랜잭션 S3 결과를 명시한다.
|
||
- [x] **GREEN:** 실제 계약 위반만 최소 수정하고, 현재 동작이 계약을 만족하면 production code를 변경하지 않는다.
|
||
- [x] **REFACTOR:** create/legacy/error focused test와 `ktlintCheck` 결과를 Progress와 리뷰 수정 후 기록에 누적한다.
|
||
- 검증 기록: 무엇: `REV-010`의 생성 multipart·theme·cover 실패 증거를 실제 endpoint test로 보강했다. 왜: 기존 완료 기록이 part별 KO/EN/JA message, invalid theme 선검증, cover upload 실패 후 DB/event 상태를 직접 고정하지 않았기 때문이다. 어떻게: `AiCharacterAdminAudioContentCreateTest`에 세 필수 part KO/EN/JA envelope, invalid theme 선검증, cover upload 실패 rollback/event 0회 단언을 추가했다. 결과: production code 변경 없이 create 단독 명령은 `BUILD SUCCESSFUL in 1m 4s`, content/common 회귀 명령은 `BUILD SUCCESSFUL in 2m 41s`였다.
|
||
|
||
```bash
|
||
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.LegacyCreatorAdminAudioContentCharacterizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
|
||
./gradlew ktlintCheck
|
||
```
|
||
|
||
- [x] **Task 3.10: 콘텐츠 수정 차집합·cover·ownership 증거 보강**
|
||
|
||
**Goal 실행 `P3-R3`:** `REV-011`의 `seriesIds` 교집합/차집합, cover 변경과 실제 endpoint ownership·오류 no-side-effect를 고정한다.
|
||
|
||
- **추적 review ID:** `REV-011`.
|
||
- **시작 조건:** `P3-R2` 완료.
|
||
- **완료 증거:** update/ownership actual endpoint test, content/authorization/error 회귀, `ktlintCheck`와 Progress 기록.
|
||
- **범위 밖:** audio file 교체, hard delete, Phase 4 series API 구현.
|
||
- **TDD 예외 사유:** 현재 production 실패가 아니라 `P3-T6`~`P3-T7` 완료 기록 대비 직접 검증 증거 누락이 확정된 test 보강 Task다.
|
||
- **대체 검증 방법:** 교집합+추가+제거와 cover/ownership 계약을 non-vacuous test로 작성하고 실패가 재현될 때만 최소 수정한다.
|
||
|
||
**Files:**
|
||
|
||
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt`
|
||
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentRepository.kt`
|
||
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentUpdateTest.kt`
|
||
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentOwnershipTest.kt`
|
||
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentControllerTest.kt`
|
||
|
||
- [x] **CONTRACT TEST:** 기존 교집합, 신규 추가, 기존 제거를 한 요청에 포함해 교집합 row ID·`orders`·`createdAt` 보존과 차집합만 insert/delete됨을 확인한다.
|
||
- [x] **CONTRACT TEST:** cover 미지정 유지, 성공 교체와 업로드 실패, 날짜 변경, soft delete, cross-owner/invalid series의 DB/S3/event 결과를 고정한다.
|
||
- [x] **CONTRACT TEST:** 실제 목록·상세·생성·수정의 ownership/domain 오류를 KO/EN/JA envelope과 DB insert/update/delete·S3·event count로 확인한다.
|
||
- [x] **GREEN:** 실제 계약 위반만 최소 수정하고, 현재 동작이 계약을 만족하면 production code를 변경하지 않는다.
|
||
- [x] **REFACTOR:** content package와 공통 authorization/error, legacy characterization, `ktlintCheck` 결과를 Progress와 리뷰 수정 후 기록에 누적한다.
|
||
- 검증 기록: 무엇: `REV-011`의 수정 차집합·cover·ownership 증거를 보강했다. 왜: 기존 완료 기록보다 실제 endpoint의 교집합 보존, cover 유지/교체/실패, ownership/domain no-side-effect 증거가 좁았기 때문이다. 어떻게: `AiCharacterAdminAudioContentUpdateTest`와 `AiCharacterAdminAudioContentOwnershipTest`에 관련 회귀를 추가하고 content/common 회귀로 재확인했다. 결과: production code 변경 없이 content/common 회귀 명령은 `BUILD SUCCESSFUL in 2m 41s`였다.
|
||
|
||
```bash
|
||
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
|
||
./gradlew ktlintCheck
|
||
```
|
||
|
||
#### Phase 3 후속 리뷰 Gate
|
||
|
||
**Goal 실행 `P3-R2-GATE`:** `REV-010`~`REV-011`의 직접 증거를 재검토하고 Phase 3 후속 리뷰를 종결한다.
|
||
|
||
- [x] **`P3-R2-GATE` 완료:** `P3-R2`, `P3-R3` 완료 후 fresh 검증과 리뷰 문서 수정 후 기록을 남긴다.
|
||
- **시작 조건:** `P3-R2`, `P3-R3` 완료.
|
||
- **완료 증거:** 두 review ID 수정 완료, content/common 회귀와 `ktlintCheck` 성공, `phase3-audio-content-review.md` 최신 결론과 Progress 동기화.
|
||
- **범위 밖:** 기존 `P3-GATE` 이력 수정, Phase 4 기능 구현.
|
||
- 검증 기록: 무엇: `P3-R2-GATE` 후속 리뷰 종결. 왜: `REV-010`~`REV-011`의 직접 증거가 추가됐고 Phase 4 전 Phase 3 후속 보완 종료 여부를 판정하기 위해. 어떻게: content/common 회귀와 `ktlintCheck`를 fresh 실행하고 `phase3-audio-content-review.md`에 3차 후속 검증 기록을 누적했다. 결과: content/common 회귀는 `BUILD SUCCESSFUL in 2m 41s`였고, `ktlintCheck` 결과는 아래 Progress 검증 기록에 남긴다.
|
||
|
||
#### Phase 3 4차 리뷰 보완
|
||
|
||
- [x] **Task 3.11: 생성 후반 실패·ownership no-side-effect 증거 보강**
|
||
|
||
**Goal 실행 `P3-R4`:** `REV-013`~`REV-014`에서 남은 audio upload/event 실패와 실제 endpoint ownership/domain 오류의 부작용 경계를 고정한다.
|
||
|
||
- **추적 review ID:** `REV-013`, `REV-014`.
|
||
- **시작 조건:** `P2-R3-GATE`와 기존 `P3-R2-GATE` 완료.
|
||
- **완료 증거:** create/update/ownership actual endpoint test, legacy characterization, content/common 회귀, `ktlintCheck`와 Progress 기록.
|
||
- **범위 밖:** S3 보상 정책 신설, audio file 수정 지원, Phase 4 기능 구현.
|
||
- **TDD 예외 사유:** 현재 production 실패가 아니라 `Task 3.9`~`Task 3.10` 완료 기록 대비 직접 검증 증거 누락이 확정된 test 보강 Task다.
|
||
- **대체 검증 방법:** cover 이후 audio upload와 event 실패, ownership/domain 거부를 실제 endpoint에서 먼저 characterization하고 계약 불일치가 재현될 때만 최소 수정한다.
|
||
|
||
**Files:**
|
||
|
||
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt`
|
||
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt`
|
||
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCreateTest.kt`
|
||
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentOwnershipTest.kt`
|
||
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentControllerTest.kt`
|
||
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/LegacyCreatorAdminAudioContentCharacterizationTest.kt`
|
||
|
||
- [x] **FAILURE CHARACTERIZATION:** 두 번째 S3 audio upload 실패와 event publish 실패에서 DB rollback, S3 호출·잔존 결과와 event 결과를 각각 고정한다.
|
||
- [x] **CONTRACT TEST:** 목록·상세·생성·수정의 target/ownership/domain 거부를 KO/EN/JA exact envelope로 확인한다.
|
||
- [x] **CONTRACT TEST:** 각 거부 뒤 AudioContent·SeriesContent·S3·event의 insert/update/delete count가 변하지 않음을 직접 단언한다.
|
||
- [x] **GREEN:** 실제 계약 위반만 최소 수정하고, legacy 비트랜잭션 S3 경계와 일치하면 production code를 변경하지 않는다.
|
||
- [x] **REFACTOR:** content/common/legacy 회귀와 `ktlintCheck` 결과를 Progress와 리뷰 수정 후 기록에 누적한다.
|
||
- 검증 기록: 무엇: `REV-013`~`REV-014`의 content 생성 후반 실패와 ownership/domain no-side-effect 증거를 보강했다. 왜: 기존 완료 기록이 cover upload 실패와 일부 ownership 경로에 치우쳐 있었기 때문이다. 어떻게: `AiCharacterAdminAudioContentCreateTest`에 audio upload 실패와 event publish 실패를 추가하고, `AiCharacterAdminAudioContentOwnershipTest`에 목록·상세·생성·수정 unknown target KO/EN/JA 및 AudioContent·SeriesContent·S3·event 무변경 단언을 추가했다. 결과: create+ownership focused 명령은 `BUILD SUCCESSFUL in 1m 8s`, content/common 회귀 명령은 `BUILD SUCCESSFUL in 2m 20s`였다.
|
||
|
||
```bash
|
||
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
|
||
./gradlew ktlintCheck
|
||
```
|
||
|
||
#### Phase 3 4차 리뷰 Gate
|
||
|
||
**Goal 실행 `P3-R3-GATE`:** `REV-013`~`REV-014`의 직접 증거를 재검토하고 Phase 3 4차 리뷰를 종결한다.
|
||
|
||
- [x] **`P3-R3-GATE` 완료:** `P3-R4` 완료 후 fresh 검증과 리뷰 문서 수정 후 기록을 남긴다.
|
||
- **시작 조건:** `P3-R4` 완료.
|
||
- **완료 증거:** 두 review ID 처리 완료, 위 두 명령 성공, `phase3-audio-content-review.md` 최신 결론과 Progress 동기화.
|
||
- **범위 밖:** 기존 Phase 3 완료 이력 수정, Phase 4 기능 구현.
|
||
- 검증 기록: 무엇: Phase 3 4차 리뷰의 `REV-013`~`REV-014` 처리를 종결했다. 왜: 사용자 지시에 따라 Phase 3 후속 보완까지만 완료하고 Phase 4로 넘어가지 않기 위해. 어떻게: content/common 회귀와 최종 `ktlintCheck`를 fresh 실행하고 `phase3-audio-content-review.md`를 처리 완료로 갱신했다. 결과: content/common 회귀 명령은 `BUILD SUCCESSFUL in 2m 20s`, 최종 `ktlintCheck`는 `BUILD SUCCESSFUL in 17s`였다.
|
||
|
||
#### Phase 3 5차 리뷰 보완
|
||
|
||
- [x] **Task 3.12: 생성 필수 multipart part의 exact binding 계약 복구**
|
||
|
||
**Goal 실행 `P3-R5`:** `REV-016`의 생성 필수 파일 part를 MVC non-null binding으로 고정하고 세 part 누락의 exact exception·KO/EN/JA 계약을 복구한다.
|
||
|
||
- **추적 review ID:** `REV-016`.
|
||
- **시작 조건:** `P2-R4-GATE` 완료와 PRD API Expectations 179~180의 missing-part 계약.
|
||
- **완료 증거:** 세 part별 RED/GREEN, exact `MissingServletRequestPartException`, facade/DB/S3/event 0회, content/common 회귀와 Progress 기록.
|
||
- **범위 밖:** legacy `AudioContentService.createAudioContent` signature 변경, upload/processing pipeline 정책 변경, 오디오 파일 교체.
|
||
|
||
**Files:**
|
||
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentController.kt`
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt`
|
||
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCreateTest.kt`
|
||
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt`
|
||
|
||
- [x] **RED:** `coverImage`, `audioFile`, `request` 각각을 누락한 KO/EN/JA actual endpoint test에서 `MvcResult.resolvedException`이 정확히 `MissingServletRequestPartException`이고 message가 `common.error.invalid_request`인지 단언한다.
|
||
- [x] **RED 확인:** create focused test를 실행해 nullable `coverImage`·`audioFile`이 legacy `SodaException`까지 전달되어 exact exception/message assertion이 실패하는지 확인한다.
|
||
- [x] **GREEN:** 생성 controller와 facade의 `coverImage`, `audioFile`을 non-null `MultipartFile`로 바꾸고 legacy service에는 검증된 non-null 값을 그대로 전달한다.
|
||
- [x] **GREEN 확인:** 같은 focused test를 재실행해 세 part 누락 9건의 exact exception·KO/EN/JA 400 envelope과 facade/DB/S3/event 0회를 확인한다.
|
||
- [x] **REFACTOR:** 중복된 missing-part request/assertion만 parameterized helper로 정리하고 create/error/legacy 회귀와 `ktlintCheck` 결과를 Progress에 기록한다.
|
||
|
||
```bash
|
||
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
|
||
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
|
||
./gradlew ktlintCheck
|
||
```
|
||
|
||
- [x] **Task 3.13: ownership/domain KO·EN·JA no-side-effect matrix 완결**
|
||
|
||
**Goal 실행 `P3-R6`:** `REV-017`의 cross-owner와 domain validation 경로를 actual endpoint KO/EN/JA 및 DB/S3/event 무변경 증거로 완결한다.
|
||
|
||
- **추적 review ID:** `REV-017`.
|
||
- **시작 조건:** `P3-R5` 완료와 기존 `P3-R4` unknown target matrix.
|
||
- **완료 증거:** cross-owner detail/update, create/update other-owner series, invalid date의 exact envelope·side-effect assertions, content/common 회귀와 Progress 기록.
|
||
- **범위 밖:** 새로운 ownership 정책, 오류 key/status 변경, Phase 4 series API 구현.
|
||
- **TDD 예외 사유:** 현재 production 위반보다 `REV-014` 완료 기록 대비 대표 ownership/domain 직접 증거 누락이 확정된 test 보강 Task다.
|
||
- **대체 검증 방법:** 기존 실제 endpoint test를 KO/EN/JA parameterized matrix로 확장하고 요청 전후 entity field·연결 row와 S3/event interaction을 비교한다. 실패가 재현될 때만 validation 순서를 최소 수정한다.
|
||
|
||
**Files:**
|
||
|
||
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt`
|
||
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentControllerTest.kt`
|
||
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentOwnershipTest.kt`
|
||
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt`
|
||
|
||
- [x] **OWNERSHIP TEST:** 다른 캐릭터 소유 콘텐츠의 detail/update를 KO/EN/JA actual endpoint matrix로 만들고 content field·S3·event 무변경을 단언한다.
|
||
- [x] **DOMAIN TEST:** create/update의 다른 owner `seriesIds`와 invalid `releaseDateUtc`를 KO/EN/JA matrix로 만들고 AudioContent·SeriesContent insert/update/delete, S3, event 0회를 단언한다.
|
||
- [x] **NON-VACUOUS 확인:** owner 또는 validation guard를 제거하면 각 matrix가 status/message 또는 side-effect assertion으로 실패하는지 확인한다.
|
||
- [x] **GREEN:** 현재 계약 위반이 재현될 때만 target/ownership/domain 선검증 순서를 최소 수정하고, 이미 충족하면 test-only로 종료한다.
|
||
- [x] **REFACTOR:** unknown target과 cross-owner/domain fixture의 공통 assertion만 정리하고 content/common 회귀와 `ktlintCheck` 결과를 Progress에 기록한다.
|
||
|
||
```bash
|
||
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentControllerTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentOwnershipTest
|
||
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
|
||
./gradlew ktlintCheck
|
||
```
|
||
|
||
#### Phase 3 5차 리뷰 Gate
|
||
|
||
**Goal 실행 `P3-R4-GATE`:** `REV-016`~`REV-017`의 exact multipart와 ownership/domain 증거를 재검토하고 Phase 3 5차 리뷰를 종결한다.
|
||
|
||
- [x] **`P3-R4-GATE` 완료:** `P3-R5`, `P3-R6` 완료 후 위 content/common 회귀와 lint를 fresh 실행하고 리뷰 문서·Progress를 갱신한다.
|
||
- **시작 조건:** `P3-R5`, `P3-R6` 완료.
|
||
- **완료 증거:** `REV-016`, `REV-017` 수정 완료, 세 필수 part와 ownership/domain matrix 직접 증거, focused/영향 범위 회귀와 lint 성공.
|
||
- **범위 밖:** 기존 Phase 3 완료 이력 수정, Phase 4 기능 구현.
|
||
|
||
#### Phase 3 6차 리뷰 보완
|
||
|
||
- [x] **Task 3.14: 빈 multipart 파일 계약 고정**
|
||
|
||
**Goal 실행 `P3-R7`:** `REV-019`의 생성·수정 empty-file 경계를 v2 facade에서 고정해 0-byte upload와 수정
|
||
`audioFile` 계약 우회를 차단한다.
|
||
|
||
- **추적 review ID:** `REV-019`.
|
||
- **시작 조건:** `P2-R5-GATE`와 기존 `P3-R4-GATE` 완료.
|
||
- **완료 증거:** 생성 empty cover/audio 거부, 수정 empty cover 유지, empty/non-empty audio 거부 actual endpoint
|
||
RED/GREEN과 DB/S3/event assertion, content/common 회귀 및 Progress 기록.
|
||
- **범위 밖:** legacy `AudioContentService`·`CreatorAdminContentService` 공용 계약 변경, 파일 content-type/확장자 정책 추가,
|
||
오디오 파일 교체, Phase 4 기능 구현.
|
||
|
||
**Files:**
|
||
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentFacade.kt`
|
||
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCreateTest.kt`
|
||
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentUpdateTest.kt`
|
||
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentControllerTest.kt`
|
||
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminErrorContractTest.kt`
|
||
|
||
- [x] **RED:** 생성의 빈 `coverImage`와 빈 `audioFile` actual endpoint를 KO/EN/JA로 요청해 400
|
||
`common.error.invalid_request`, AudioContent/SeriesContent·S3·event 0회를 기대하고 현재 200/업로드 경로로 실패함을
|
||
확인한다.
|
||
- [x] **RED:** 수정의 빈 `coverImage`가 생략과 동일하게 기존 cover path를 유지하고 S3를 호출하지 않는 기대, 빈
|
||
`audioFile` part가 non-empty와 동일하게 400으로 거부되는 기대가 현재 실패함을 확인한다.
|
||
- [x] **GREEN:** create 시작 시 `coverImage.isEmpty || audioFile.isEmpty`를 `invalidRequest()`로 거부한다.
|
||
- [x] **GREEN:** update는 `audioFile != null`이면 크기와 관계없이 `invalidRequest()`로 거부하고,
|
||
`coverImage?.takeUnless { it.isEmpty }`만 legacy update service에 전달한다.
|
||
- [x] **GREEN 확인:** 같은 focused test를 재실행해 create empty-file의 부작용 0회, update empty cover의 DB/S3 유지와
|
||
empty/non-empty audio 거부가 모두 통과하는지 확인한다.
|
||
- [x] **REFACTOR:** empty-file fixture만 공통화하고 legacy service를 수정하지 않은 채 content/common 회귀와
|
||
`ktlintCheck` 결과를 Progress에 기록한다.
|
||
- 검증 기록(RED): 무엇: 생성 empty `coverImage`/`audioFile` KO/EN/JA와 수정 empty cover/audio actual endpoint 계약을 추가했다. 왜: 빈 multipart 파일이 null/non-empty 검사 사이를 통과하는지 확인하기 위해. 어떻게: `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentUpdateTest`를 실행했다. 결과: 26건 중 신규 8건이 line 119, 204, 236에서 실패해 RED를 확인했다.
|
||
- 검증 기록(GREEN): 무엇: v2 facade empty-file 경계와 실제 service publisher no-interaction 증거. 왜: legacy 공용 service 변경 없이 신규 관리자 API 계약만 고정하고 event 부작용 assertion이 detached mock을 보지 않게 하기 위해. 어떻게: 같은 focused 명령을 재실행했다. 결과: reviewer gate 보완 후 최종 empty create/update focused는 `BUILD SUCCESSFUL in 43s`, non-empty audio update 보완 focused는 `BUILD SUCCESSFUL in 44s`였다.
|
||
|
||
```bash
|
||
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentUpdateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentControllerTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
|
||
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
|
||
./gradlew ktlintCheck
|
||
```
|
||
|
||
- [x] **Task 3.15: ownership/domain event 0회 실제 publisher 증거 복구**
|
||
|
||
**Goal 실행 `P3-R8`:** `REV-020`의 ownership/domain no-side-effect test가 실제 `AudioContentService`와
|
||
`CreatorAdminContentService`의 publisher를 관찰하도록 연결해 NON-VACUOUS 완료 증거를 복구한다.
|
||
|
||
- **추적 review ID:** `REV-020`.
|
||
- **시작 조건:** `P3-R7` 완료.
|
||
- **완료 증거:** 실제 두 service proxy target의 publisher 교체·복원, mock identity 확인, ownership/domain matrix의
|
||
event 0회와 content/common 회귀 및 Progress 기록.
|
||
- **범위 밖:** production event 발행 순서·payload 변경, application context event infrastructure 변경, 신규 test 전용
|
||
production seam 추가, Phase 4 기능 구현.
|
||
- **TDD 예외 사유:** 현재 production의 잘못된 event 발행이 아니라 detached mock으로 인한 완료 증거 공백이 확정된
|
||
test-only Task다.
|
||
- **대체 검증 방법:** Phase 2와 기존 content create event failure test의 `AopTestUtils`·`ReflectionTestUtils` 방식을
|
||
재사용해 실제 proxy target field와 mock identity를 확인한다.
|
||
|
||
**Files:**
|
||
|
||
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentService.kt`
|
||
- Confirm: `src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/content/CreatorAdminContentService.kt`
|
||
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentControllerTest.kt`
|
||
- Modify: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentOwnershipTest.kt`
|
||
- Confirm: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentCreateTest.kt`
|
||
|
||
- [x] **EVIDENCE RED:** 현재 `@MockBean ApplicationEventPublisher`와 실제 두 service proxy target의 publisher가 같은
|
||
instance인지 단언해 detached 상태에서 실패함을 확인한다.
|
||
- [x] **EVIDENCE GREEN:** 각 test에서 `AudioContentService`, `CreatorAdminContentService` proxy target의 기존 publisher를
|
||
보관하고 같은 mock으로 교체하며 `finally`/teardown에서 원래 publisher를 복원한다.
|
||
- [x] **NON-VACUOUS 확인:** 교체 직후 실제 두 target field가 mock과 같은 instance인지 단언하고, cross-owner
|
||
detail/update·other-owner series·invalid date 및 unknown target matrix가 실제 publisher no-interaction을 통과하는지
|
||
확인한다.
|
||
- [x] **회귀 확인:** 기존 create event failure helper와 충돌하지 않고 정상 content 생성·수정의 event 회귀가 유지되는지
|
||
content/common 명령으로 확인한다.
|
||
- [x] **REFACTOR:** Phase 2 및 create failure test의 기존 helper 패턴 범위에서만 중복을 정리하고 production seam이나
|
||
공용 test abstraction은 추가하지 않는다.
|
||
- 검증 기록: 무엇: `REV-020`의 실제 publisher no-interaction 증거를 복구했다. 왜: detached `@MockBean ApplicationEventPublisher`만 검증하면 실제 service field 호출 여부를 증명할 수 없기 때문이다. 어떻게: `AudioContentService`, `CreatorAdminContentService` proxy target의 `applicationEventPublisher`를 테스트 mock으로 교체·복원하고 field identity를 단언한 뒤 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentControllerTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentOwnershipTest`를 실행했다. 결과: `BUILD SUCCESSFUL in 39s`였다.
|
||
|
||
```bash
|
||
./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentControllerTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentOwnershipTest
|
||
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest
|
||
./gradlew ktlintCheck
|
||
```
|
||
|
||
#### Phase 3 6차 리뷰 Gate
|
||
|
||
**Goal 실행 `P3-R5-GATE`:** `REV-019`~`REV-020`의 empty-file 계약과 actual publisher 증거를 재검토하고 Phase 3
|
||
6차 리뷰를 종결한다.
|
||
|
||
- [x] **`P3-R5-GATE` 완료:** `P3-R7`, `P3-R8` 완료 후 위 content/common 회귀와 lint를 fresh 실행하고
|
||
리뷰 문서·Progress를 갱신한다.
|
||
- **시작 조건:** `P3-R7`, `P3-R8` 완료.
|
||
- **완료 증거:** `REV-019`, `REV-020` 처리 완료, 생성·수정 empty-file actual endpoint 계약, 실제 두 service publisher
|
||
no-interaction 증거, focused/영향 범위 회귀와 lint·diff check 성공.
|
||
- **범위 밖:** Gate에서 production code 수정, 기존 Phase 3 완료 이력 변경, Phase 4 기능 구현.
|
||
|
||
---
|
||
|
||
### 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: `Series`와 `SeriesContent` 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 고정**
|
||
|
||
**Goal 실행 `P4-T1`:** 기존 creator series의 CRUD·연결·검색·순서 동작을 신규 v2 구현의 비교 기준으로 고정한다.
|
||
|
||
- **시작 조건:** 최신 Phase 3 후속 Gate인 `P3-R5-GATE` 완료와 사용자 진행 지시.
|
||
- **완료 증거:** production 변경 전 특성화 테스트 통과, 관찰된 오류·side-effect 정책과 Progress 기록.
|
||
- **범위 밖:** 신규 v2 series production code 구현.
|
||
|
||
**Files:**
|
||
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/LegacyCreatorAdminSeriesCharacterizationTest.kt`
|
||
|
||
- [ ] 목록·상세·생성·수정·soft delete와 inactive 조회 baseline test를 작성한다.
|
||
- [ ] 콘텐츠 연결·해제·검색과 순서 변경의 결과·검증·side effect를 고정한다.
|
||
- [ ] Phase 4 domain/client 오류별 정확한 status와 KO/EN/JA message key를 확정해 구현 Goal에 반영한다.
|
||
- [ ] production code 변경 없이 특성화 테스트가 기존 구현을 대상으로 통과함을 확인한다.
|
||
- [ ] fixture만 정리하고 test와 `ktlintCheck` 결과를 Progress에 기록한다.
|
||
|
||
- [ ] **Task 4.2: 시리즈 목록·상세 조회 구현**
|
||
|
||
**Goal 실행 `P4-T2`:** target owner의 활성 시리즈 목록·상세를 pagination 계약으로 제공한다.
|
||
|
||
- **시작 조건:** `P4-T1` 완료와 Phase 4 오류 계약의 계획 반영.
|
||
- **완료 증거:** 목록·상세·inactive·cross-owner·pagination RED/GREEN과 Progress 기록.
|
||
- **범위 밖:** 시리즈 mutation, 콘텐츠 연결, 순서 변경.
|
||
|
||
**Files:**
|
||
|
||
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesController.kt`
|
||
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesDto.kt`
|
||
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesFacade.kt`
|
||
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesRepository.kt`
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesQueryTest.kt`
|
||
|
||
- [ ] 미구현 목록·상세, inactive 제외, cross-owner 거부와 pagination 경계 실패 test를 작성한다.
|
||
- [ ] owner-scoped 목록·상세 최소 구현으로 focused test를 통과시킨다.
|
||
- [ ] `page` 기본 0, `size` 기본·최소 20·최대 50과 legacy DTO 비노출을 검증한다.
|
||
- [ ] focused test와 `ktlintCheck` 결과를 Progress에 기록한다.
|
||
|
||
- [ ] **Task 4.3: 시리즈 생성·수정·soft delete 구현**
|
||
|
||
**Goal 실행 `P4-T3`:** target owner의 시리즈 생성·수정·soft delete를 기존 creator parity로 제공한다.
|
||
|
||
- **시작 조건:** `P4-T1`, `P4-T2` 완료.
|
||
- **완료 증거:** CRUD RED/GREEN, ownership·inactive·no-side-effect와 legacy 회귀 기록.
|
||
- **범위 밖:** 콘텐츠 연결·해제, 순서 변경, hard delete.
|
||
|
||
**Files:**
|
||
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesController.kt`
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesDto.kt`
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesFacade.kt`
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesMutationTest.kt`
|
||
|
||
- [ ] 생성·수정·soft delete와 cross-owner mutation 실패 test를 작성한다.
|
||
- [ ] owner 검증 후 최소 CRUD 구현으로 test를 통과시킨다.
|
||
- [ ] `isActive=false`와 활성 조회 제외, invalid target의 DB/event no-side-effect를 검증한다.
|
||
- [ ] focused/legacy test와 `ktlintCheck` 결과를 Progress에 기록한다.
|
||
|
||
- [ ] **Task 4.4: 시리즈 콘텐츠 조회·검색·연결·해제 구현**
|
||
|
||
**Goal 실행 `P4-T4`:** 동일 owner의 시리즈와 콘텐츠만 검색·연결·해제할 수 있도록 한다.
|
||
|
||
- **시작 조건:** `P4-T2`, `P4-T3`와 Phase 3 owner query 계약 완료.
|
||
- **완료 증거:** 검색·pagination·전체 ID 사전 검증·원자적 연결/해제 RED/GREEN과 Progress 기록.
|
||
- **범위 밖:** 콘텐츠 자체 수정, 시리즈 순서 변경.
|
||
|
||
**Files:**
|
||
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesController.kt`
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesFacade.kt`
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesRepository.kt`
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesContentTest.kt`
|
||
|
||
- [ ] 콘텐츠 조회·검색·연결·해제와 cross-owner ID 실패 test를 작성한다.
|
||
- [ ] 모든 series/content ID를 mutation 전에 검증하는 최소 구현을 통과시킨다.
|
||
- [ ] 일부 연결 성공이 남지 않는 원자성과 pagination 경계를 검증한다.
|
||
- [ ] focused/Phase 3 owner query 회귀와 `ktlintCheck` 결과를 Progress에 기록한다.
|
||
|
||
- [ ] **Task 4.5: owner-scoped 시리즈 순서 변경 구현**
|
||
|
||
**Goal 실행 `P4-T5`:** 요청된 모든 series ID의 owner를 먼저 검증한 뒤 한 transaction에서 순서를 변경한다.
|
||
|
||
- **시작 조건:** `P4-T3` 완료.
|
||
- **완료 증거:** 정상·중복/누락·cross-owner·동시 요청 RED/GREEN과 owner-less 경로 비사용 증거.
|
||
- **범위 밖:** 기존 `CreatorAdminContentSeriesController.updateSeriesOrders(ids)` 변경.
|
||
|
||
**Files:**
|
||
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesController.kt`
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesFacade.kt`
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesRepository.kt`
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesOrderTest.kt`
|
||
|
||
- [ ] 정상 순서와 cross-owner ID-only 취약 경로를 재현하는 실패 test를 작성한다.
|
||
- [ ] 동일 owner 전체 검증 후 한 transaction에서 갱신하는 최소 구현을 통과시킨다.
|
||
- [ ] 검증 실패 시 update 0건과 동시 요청의 기존 last-transaction 정책을 확인한다.
|
||
- [ ] focused/legacy order test와 `ktlintCheck` 결과를 Progress에 기록한다.
|
||
|
||
- [ ] **Task 4.6: Phase 4 보안·오류·회귀 검증**
|
||
|
||
**Goal 실행 `P4-T6`:** 모든 series endpoint의 ADMIN·오류·ownership 계약과 legacy 회귀를 고정한다.
|
||
|
||
- **시작 조건:** `P4-T2`~`P4-T5` 완료.
|
||
- **완료 증거:** endpoint 권한 매트릭스, domain 오류 status/key/KO·EN·JA, legacy 회귀와 Progress 기록.
|
||
- **범위 밖:** Phase 5 community 기능.
|
||
|
||
**Files:**
|
||
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/series/AiCharacterAdminSeriesContractTest.kt`
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAuthorizationTest.kt`
|
||
|
||
- [ ] endpoint별 ADMIN 이중 인가와 stale claim을 검증한다.
|
||
- [ ] target/series/content/pagination 오류의 정확한 status, message key, KO/EN/JA envelope를 확정·검증한다.
|
||
- [ ] invalid ownership의 DB/event side effect 0건과 legacy creator series 계약을 검증한다.
|
||
- [ ] Phase 4 focused test와 `ktlintCheck` 결과를 Progress에 기록한다.
|
||
|
||
#### Phase 4 Gate
|
||
|
||
**Goal 실행 `P4-GATE`:** Phase 4 series 사용자 흐름과 ownership·회귀 품질을 최종 판정한다.
|
||
|
||
- [ ] **`P4-GATE` 완료:** 시작 조건과 완료 증거를 모두 충족하고 현재 상태표와 Progress를 갱신한다.
|
||
|
||
- **시작 조건:** `P4-T1`~`P4-T6` 완료.
|
||
- **완료 증거:** `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.series.*'`와
|
||
`./gradlew ktlintCheck` 성공, Progress 기록.
|
||
- **범위 밖:** 실패와 무관한 Phase 5 구현.
|
||
|
||
---
|
||
|
||
### 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 고정**
|
||
|
||
**Goal 실행 `P5-T1`:** 기존 community의 media·유료·고정·soft delete·알림 동작을 신규 v2 비교 기준으로 고정한다.
|
||
|
||
- **시작 조건:** `P4-GATE` 완료.
|
||
- **완료 증거:** production 변경 전 특성화 테스트 통과, 오류·side-effect·동시성 관찰 결과와 Progress 기록.
|
||
- **범위 밖:** 신규 v2 community production code 구현.
|
||
|
||
**Files:**
|
||
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/LegacyCommunityPostCharacterizationTest.kt`
|
||
|
||
- [ ] image/audio/paid post validation과 notification/recent-news side effect baseline을 작성한다.
|
||
- [ ] 최대 고정 3개와 fixed post soft delete clearing baseline을 작성한다.
|
||
- [ ] Phase 5 domain/client 오류별 정확한 status와 KO/EN/JA message key를 확정해 구현 Goal에 반영한다.
|
||
- [ ] production code 변경 없이 기존 구현 대상 특성화 테스트 통과를 확인한다.
|
||
- [ ] fixture/event spy만 정리하고 test와 `ktlintCheck` 결과를 Progress에 기록한다.
|
||
|
||
- [ ] **Task 5.2: 관리자 게시글 목록 조회 구현**
|
||
|
||
**Goal 실행 `P5-T2`:** target owner의 관리자용 커뮤니티 게시글 목록을 안전한 전용 DTO와 pagination으로 제공한다.
|
||
|
||
- **시작 조건:** `P5-T1` 완료와 Phase 5 오류 계약의 계획 반영.
|
||
- **완료 증거:** owner/inactive/pagination/DTO RED/GREEN과 public 조회 무비판적 복제 없음 증거.
|
||
- **범위 밖:** 게시글 생성·수정·고정·삭제.
|
||
|
||
**Files:**
|
||
|
||
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostController.kt`
|
||
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostDto.kt`
|
||
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostFacade.kt`
|
||
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostRepository.kt`
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostQueryTest.kt`
|
||
|
||
- [ ] 미구현 목록, owner 격리, pagination 경계와 관리자 DTO 실패 test를 작성한다.
|
||
- [ ] 최소 owner-scoped query와 `page/size` 보정으로 focused test를 통과시킨다.
|
||
- [ ] 유료/media private 정보와 public viewer 상태를 부적절하게 노출하지 않는지 검증한다.
|
||
- [ ] focused test와 `ktlintCheck` 결과를 Progress에 기록한다.
|
||
|
||
- [ ] **Task 5.3: 커뮤니티 게시글 생성 구현**
|
||
|
||
**Goal 실행 `P5-T3`:** target creatorMember 작성자로 이미지·오디오·유료 게시글을 기존 검증과 side effect parity로 생성한다.
|
||
|
||
- **시작 조건:** `P5-T1`, `P5-T2` 완료.
|
||
- **완료 증거:** 정상/media/paid validation RED/GREEN, writer/owner와 S3/event 결과, Progress 기록.
|
||
- **범위 밖:** 게시글 수정·고정·soft delete, 구매 기능.
|
||
|
||
**Files:**
|
||
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostController.kt`
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostDto.kt`
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostFacade.kt`
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostCreateTest.kt`
|
||
|
||
- [ ] 정상/image/audio/paid validation과 invalid target 실패 test를 작성한다.
|
||
- [ ] 해석된 creatorMember를 writer/owner로 사용하는 최소 생성 구현을 통과시킨다.
|
||
- [ ] S3, notification, recent-news 호출 순서와 실패 시 부분 저장/no-side-effect를 검증한다.
|
||
- [ ] focused/legacy characterization test와 `ktlintCheck` 결과를 Progress에 기록한다.
|
||
|
||
- [ ] **Task 5.4: 게시글 수정·고정·soft delete 구현**
|
||
|
||
**Goal 실행 `P5-T4`:** owner 게시글만 수정·고정/해제하고 soft delete 시 고정 상태와 시간을 함께 제거한다.
|
||
|
||
- **시작 조건:** `P5-T1`, `P5-T2` 완료.
|
||
- **완료 증거:** update/fixed/soft delete/cross-owner RED/GREEN과 transaction 결과 기록.
|
||
- **범위 밖:** hard delete, 구매·좋아요·댓글 관리.
|
||
|
||
**Files:**
|
||
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostController.kt`
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostFacade.kt`
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostRepository.kt`
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostUpdateTest.kt`
|
||
|
||
- [ ] 수정·고정/해제·soft delete와 cross-owner 실패 test를 작성한다.
|
||
- [ ] owner 검증 후 최소 mutation 구현으로 test를 통과시킨다.
|
||
- [ ] soft delete가 한 transaction에서 `isActive=false`, `isFixed=false`, `fixedAt=null`을 적용하는지 검증한다.
|
||
- [ ] focused/legacy characterization test와 `ktlintCheck` 결과를 Progress에 기록한다.
|
||
|
||
- [ ] **Task 5.5: 최대 고정 수·동시성·side effect 검증**
|
||
|
||
**Goal 실행 `P5-T5`:** 최대 고정 게시글 3개 정책이 동시 요청과 실패에서도 깨지지 않도록 고정한다.
|
||
|
||
- **시작 조건:** `P5-T4` 완료.
|
||
- **완료 증거:** 세 번째/네 번째 고정, 동시 요청, cross-owner와 DB/S3/event side effect RED/GREEN 기록.
|
||
- **범위 밖:** 새로운 lock/dependency 도입, 기존 고정 정책 변경.
|
||
|
||
**Files:**
|
||
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostRepository.kt`
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostFacade.kt`
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostConcurrencyTest.kt`
|
||
|
||
- [ ] 3개 허용·4번째 거부와 동시 고정 요청 실패 test를 작성한다.
|
||
- [ ] 기존 repository count/update 순서를 유지하는 최소 구현으로 test를 통과시킨다.
|
||
- [ ] invalid target/ownership 실패 시 DB/S3/event 0건을 검증한다.
|
||
- [ ] concurrency/focused test와 `ktlintCheck` 결과를 Progress에 기록한다.
|
||
|
||
- [ ] **Task 5.6: Phase 5 보안·오류·회귀 검증**
|
||
|
||
**Goal 실행 `P5-T6`:** 모든 community endpoint의 ADMIN·오류·ownership 계약과 기존 public/legacy 회귀를 고정한다.
|
||
|
||
- **시작 조건:** `P5-T2`~`P5-T5` 완료.
|
||
- **완료 증거:** endpoint 권한 매트릭스, 오류 status/key/KO·EN·JA, legacy/public 회귀와 Progress 기록.
|
||
- **범위 밖:** Phase 6 FanTalk 기능.
|
||
|
||
**Files:**
|
||
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostContractTest.kt`
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAuthorizationTest.kt`
|
||
|
||
- [ ] endpoint별 ADMIN 이중 인가와 stale claim을 검증한다.
|
||
- [ ] target/post/media/fixed-count 오류의 정확한 status, message key, KO/EN/JA envelope를 확정·검증한다.
|
||
- [ ] HUMAN/cross-character 게시글 mutation 거부와 legacy/public community 계약을 검증한다.
|
||
- [ ] Phase 5 focused test와 `ktlintCheck` 결과를 Progress에 기록한다.
|
||
|
||
#### Phase 5 Gate
|
||
|
||
**Goal 실행 `P5-GATE`:** Phase 5 community 사용자 흐름과 고정·side-effect·회귀 품질을 최종 판정한다.
|
||
|
||
- [ ] **`P5-GATE` 완료:** 시작 조건과 완료 증거를 모두 충족하고 현재 상태표와 Progress를 갱신한다.
|
||
|
||
- **시작 조건:** `P5-T1`~`P5-T6` 완료.
|
||
- **완료 증거:** `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.community.*'`와
|
||
`./gradlew ktlintCheck` 성공, Progress 기록.
|
||
- **범위 밖:** 실패와 무관한 Phase 6 구현.
|
||
|
||
---
|
||
|
||
### 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 고정**
|
||
|
||
**Goal 실행 `P6-T1`:** 기존 FanTalk의 root 판별, 언어 감지, 응답 의미와 writer/creator 저장 결과를 비교 기준으로 고정한다.
|
||
|
||
- **시작 조건:** `P5-GATE` 완료.
|
||
- **완료 증거:** production 변경 전 특성화 테스트 통과, 중복 답변 정책과 Progress 기록.
|
||
- **범위 밖:** 신규 v2 reply production code 구현.
|
||
|
||
**Files:**
|
||
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/LegacyFanTalkReplyCharacterizationTest.kt`
|
||
|
||
- [ ] valid root reply의 언어 감지, response 의미와 writer/creator 저장 baseline을 작성한다.
|
||
- [ ] root/nested/active 판별과 기존 중복 답변 정책을 관찰해 기록한다.
|
||
- [ ] Phase 6 domain/client 오류별 정확한 status와 KO/EN/JA message key를 확정해 구현 Goal에 반영한다.
|
||
- [ ] production code 변경 없이 기존 구현 대상 특성화 테스트 통과를 확인한다.
|
||
- [ ] fixture만 정리하고 test와 `ktlintCheck` 결과를 Progress에 기록한다.
|
||
|
||
- [ ] **Task 6.2: FanTalk root reply 저장 구현**
|
||
|
||
**Goal 실행 `P6-T2`:** 선택한 AI 캐릭터의 활성 root FanTalk에 creator reply를 저장하고 전용 응답을 반환한다.
|
||
|
||
- **시작 조건:** `P6-T1` 완료와 Phase 6 오류 계약의 계획 반영.
|
||
- **완료 증거:** 정상 저장·언어 감지·DTO·writer/creator RED/GREEN과 Progress 기록.
|
||
- **범위 밖:** FanTalk 원글, nested reply, 일반 사용자 대리 작성.
|
||
|
||
**Files:**
|
||
|
||
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkReplyController.kt`
|
||
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkReplyDto.kt`
|
||
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkReplyFacade.kt`
|
||
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkReplyRepository.kt`
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkReplyCreateTest.kt`
|
||
|
||
- [ ] 미구현 정상 root reply, 언어 감지와 응답 DTO 실패 test를 작성한다.
|
||
- [ ] root 조회와 reply 저장을 같은 transaction에서 수행하는 최소 구현을 통과시킨다.
|
||
- [ ] writer/creator가 resolver의 creatorMember와 일치하고 principal impersonation이 없음을 검증한다.
|
||
- [ ] focused/legacy characterization test와 `ktlintCheck` 결과를 Progress에 기록한다.
|
||
|
||
- [ ] **Task 6.3: FanTalk target·root·ownership 거부 구현**
|
||
|
||
**Goal 실행 `P6-T3`:** cross-character, nested, inactive, missing FanTalk를 저장·이벤트 없이 거부한다.
|
||
|
||
- **시작 조건:** `P6-T1`, `P6-T2` 완료.
|
||
- **완료 증거:** 네 거부 분기의 정확한 오류 계약, DB/event 0건과 Progress 기록.
|
||
- **범위 밖:** 새로운 중복 답변 차단 정책.
|
||
|
||
**Files:**
|
||
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkReplyFacade.kt`
|
||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkReplyRepository.kt`
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkReplyOwnershipTest.kt`
|
||
|
||
- [ ] cross-character/nested/inactive/missing 각각의 실패 test와 의도한 실패를 확인한다.
|
||
- [ ] target·root·active·owner를 저장 전에 검증하는 최소 구현을 통과시킨다.
|
||
- [ ] 각 실패의 정확한 status/message key/KO·EN·JA와 reply insert/event 0건을 검증한다.
|
||
- [ ] focused test와 `ktlintCheck` 결과를 Progress에 기록한다.
|
||
|
||
- [ ] **Task 6.4: Phase 6 보안·오류·회귀 검증**
|
||
|
||
**Goal 실행 `P6-T4`:** FanTalk reply endpoint의 ADMIN·오류 계약과 기존 FanTalk 회귀를 고정한다.
|
||
|
||
- **시작 조건:** `P6-T2`, `P6-T3` 완료.
|
||
- **완료 증거:** 권한 매트릭스, request binding/domain 오류, legacy 회귀와 Progress 기록.
|
||
- **범위 밖:** Phase 7 외 전체 기능 수정.
|
||
|
||
**Files:**
|
||
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkReplyContractTest.kt`
|
||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/admin/aicharacter/AiCharacterAdminAuthorizationTest.kt`
|
||
|
||
- [ ] endpoint의 ADMIN 이중 인가와 stale claim을 검증한다.
|
||
- [ ] 빈 content/binding/domain 오류의 정확한 status, message key, KO/EN/JA envelope를 확정·검증한다.
|
||
- [ ] 기존 FanTalk 조회·작성 계약과 AI 로그인/token/impersonation 부재를 확인한다.
|
||
- [ ] Phase 6 focused test와 `ktlintCheck` 결과를 Progress에 기록한다.
|
||
|
||
#### Phase 6 Gate
|
||
|
||
**Goal 실행 `P6-GATE`:** Phase 6 FanTalk reply의 root·ownership·저장·회귀 품질을 최종 판정한다.
|
||
|
||
- [ ] **`P6-GATE` 완료:** 시작 조건과 완료 증거를 모두 충족하고 현재 상태표와 Progress를 갱신한다.
|
||
|
||
- **시작 조건:** `P6-T1`~`P6-T4` 완료.
|
||
- **완료 증거:** `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.fantalk.*'`와
|
||
`./gradlew ktlintCheck` 성공, Progress 기록.
|
||
- **범위 밖:** 실패와 무관한 신규 기능.
|
||
|
||
---
|
||
|
||
### 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`가 통과하고, 생략 대상이면 근거와 대체 targeted/영향 범위 회귀
|
||
결과가 기록된다.
|
||
- Kotlin 파일 변경이 있으면 `./gradlew ktlintCheck`가 통과한다.
|
||
- 신규 dependency와 신규 DDL이 없다.
|
||
- source spec acceptance criteria 25개를 각 Phase 결과와 대조해 누락이 없다.
|
||
|
||
#### targeted test
|
||
- Run:
|
||
```bash
|
||
./gradlew test --tests kr.co.vividnext.sodalive.jwt.TokenProviderTest --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*'
|
||
./gradlew ktlintCheck
|
||
```
|
||
|
||
- Conditional full regression: 공통 경계·여러 Phase 영향 또는 targeted 결과로 영향 범위를 판단할 수 없을 때만
|
||
`./gradlew test`를 추가 실행한다.
|
||
|
||
#### 전체 회귀 테스트 영향
|
||
- 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 및 필요 시 전체 회귀 test 실행**
|
||
- **Goal 실행 `P7-T1`:** Phase 1~6 targeted test를 실행하고 위험 근거에 따라 전체 회귀 필요성을 판정해 결과를 확정한다.
|
||
- **시작 조건:** `P2-GATE`~`P6-GATE` 완료와 Phase 1 완료 증거 확인.
|
||
- **완료 증거:** targeted 결과와 전체 회귀 실행 또는 생략 판정·근거를 Progress와 하단 검증 기록에 누적.
|
||
- **범위 밖:** 실패와 무관한 기능 추가·리팩터링.
|
||
- TDD 예외 사유: 구현 완료 후 검증 전용 Task라 신규 실패 테스트를 작성하지 않는다.
|
||
- 대체 검증 방법: Phase 1~6 targeted test, `AiCharacterAdminAuthorizationTest`, `AiCharacterAdminErrorContractTest`, 기존 admin
|
||
security test를 먼저 실행한다. 공통 경계·여러 Phase 영향 또는 targeted 실패로 영향 범위를 판단할 수 없을 때만 전체 회귀를 실행한다.
|
||
- REFACTOR: 실패가 있으면 관련 Phase Task로 되돌려 최소 수정 후 다시 실행한다.
|
||
- Verify: 위 targeted command를 실행하고 전체 회귀 필요성을 판정한다. 실행 시 `./gradlew test` 결과를, 생략 시 근거와 대체
|
||
회귀 범위를 이 문서 하단 검증 기록에 남긴다.
|
||
- [ ] Phase 1~6 targeted test를 실행하고 각 결과를 기록한다.
|
||
- [ ] 공통 경계·여러 Phase 변경과 targeted 결과를 근거로 전체 회귀 필요성을 판정한다.
|
||
- [ ] 필요하면 `./gradlew test`의 exit code·실패 수를 기록하고, 불필요하면 생략 근거와 대체 회귀 범위를 기록한다.
|
||
- [ ] 실패가 있으면 소유 Phase에 별도 회귀 수정 Goal을 추가하고 `P7-T1`을 완료 처리하지 않는다.
|
||
|
||
- [ ] **Task 7.2: API contract와 변경 범위 점검**
|
||
- **Goal 실행 `P7-T2`:** API·architecture·dependency·DDL·diff와 문서 추적성을 read-only로 최종 점검한다.
|
||
- **시작 조건:** `P7-T1` 완료.
|
||
- **완료 증거:** 아래 점검 체크박스, `ktlintCheck`, source spec acceptance criteria 추적 결과와 Progress 기록.
|
||
- **범위 밖:** 신규 기능 추가, 확정되지 않은 계약 보정.
|
||
- 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`
|
||
- [ ] `git diff --name-only`와 `git diff --check`로 변경 범위와 문서/코드 오류를 확인한다.
|
||
- [ ] `build.gradle.kts`와 migration/DDL 경로를 확인해 신규 dependency·DDL 0건을 기록한다.
|
||
- [ ] legacy/public controller·DTO 외부 계약 diff와 신규 application/domain의 역방향 import 0건을 확인한다.
|
||
- [ ] source spec acceptance criteria 25개를 Phase Goal/Gate 완료 증거에 대조한다.
|
||
- [ ] `./gradlew ktlintCheck`와 `./gradlew tasks --all` 결과를 기록한다.
|
||
|
||
#### Phase 7 Gate
|
||
|
||
**Goal 실행 `P7-GATE`:** 모든 Phase의 완료 증거와 최신 전체 검증을 대조해 AI 캐릭터 관리자 API의 최종 완료 여부를 판정한다.
|
||
|
||
- [ ] **`P7-GATE` 완료:** 시작 조건과 완료 증거를 모두 충족하고 문서 상태를 `구현 완료`로 갱신한다.
|
||
|
||
- **시작 조건:** `P7-T1`, `P7-T2` 완료.
|
||
- **완료 증거:** 미완료 Goal·미처리 review finding·보류 없는 차단 사항 0건, 아래 완료 조건과 최종 Progress 기록.
|
||
- **범위 밖:** Gate에서 직접 production code 수정, test 삭제·skip·완화.
|
||
|
||
- [ ] Phase 1~6의 Task/Gate 완료 증거와 하단 검증 기록이 일치한다.
|
||
- [ ] 모든 확정 review finding이 수정 완료 또는 근거 있는 제외로 종결됐다.
|
||
- [ ] 최신 targeted·ktlint·문서 명령이 성공했고, 전체 회귀는 필요성 판정에 따라 성공 결과 또는 생략 근거가 기록됐다.
|
||
- [ ] 남은 항목과 최종 상태를 Progress 및 최종 보고 형식으로 기록한다.
|
||
|
||
---
|
||
|
||
## 실행 순서와 의존성
|
||
|
||
| 순서 | Goal | 선행조건 | 병행 가능 | 차단 시 다음 행동 |
|
||
|---:|---|---|---|---|
|
||
| 1 | `P2-R1` | `P2-H1`, `P2-H2` | 아니요 | 리뷰 후보를 판정하고 계약 미확정이면 Decision Log에 기록 |
|
||
| 2 | `P2-T3` → `P2-T4` → `P2-T5` → `P2-T6` | `P2-R1` | 아니요 | 실패 소유 Goal에서 수정·검증을 끝낸 뒤 다음 Goal 수행 |
|
||
| 3 | `P2-GATE` | Phase 2 활성 Goal 전체 | 아니요 | 실패 소유 Task의 회귀 수정 Goal 추가 |
|
||
| 4 | `P3-R1` | `P2-GATE`, `P3-H1`, `P3-H2` | 아니요 | 리뷰 후보를 판정하고 계약 미확정이면 Decision Log에 기록 |
|
||
| 5 | `P3-T3` → `P3-T4` → `P3-T5` → `P3-T6` → `P3-T7` | `P3-R1` | 아니요 | 실패 소유 Goal에서 수정·검증을 끝낸 뒤 다음 Goal 수행 |
|
||
| 6 | `P3-GATE` | Phase 3 활성 Goal 전체 | 아니요 | 실패 소유 Task의 회귀 수정 Goal 추가 |
|
||
| 7 | `P2-R2` → `P2-R2-GATE` | 2차 Phase 2 review | 아니요 | `REV-009` 소유 Task에서 증거 보강 |
|
||
| 8 | `P3-R2` → `P3-R3` → `P3-R2-GATE` | `P2-R2-GATE`, 2차 Phase 3 review | 아니요 | `REV-010`~`REV-011` 소유 Task에서 증거 보강 |
|
||
| 9 | `P2-R3` → `P2-R3-GATE` | 4차 Phase 2 review | 아니요 | `REV-012` 소유 Task에서 증거 보강 |
|
||
| 10 | `P3-R4` → `P3-R3-GATE` | `P2-R3-GATE`, 4차 Phase 3 review | 아니요 | `REV-013`~`REV-014` 소유 Task에서 증거 보강 |
|
||
| 11 | `P2-R4` → `P2-R4-GATE` | 5차 Phase 2 review | 아니요 | `REV-015` actual transaction 증거 보강 |
|
||
| 12 | `P3-R5` → `P3-R6` → `P3-R4-GATE` | `P2-R4-GATE`, 5차 Phase 3 review | 아니요 | `REV-016`~`REV-017` 소유 Task에서 수정·증거 보강 |
|
||
| 13 | `P2-R5` → `P2-R5-GATE` | 6차 Phase 2 review | 아니요 | `REV-018` 문서 계약 동기화 |
|
||
| 14 | `P3-R7` → `P3-R8` → `P3-R5-GATE` | `P2-R5-GATE`, 6차 Phase 3 review | 아니요 | `REV-019`~`REV-020` 소유 Task에서 수정·증거 보강 |
|
||
| 15 | `P4-T1`~`P4-T6` → `P4-GATE` | `P3-R5-GATE`, 사용자 진행 지시 | 아니요 | 실패 소유 Task로 되돌림 |
|
||
| 16 | `P5-T1`~`P5-T6` → `P5-GATE` | `P4-GATE` | 아니요 | 실패 소유 Task로 되돌림 |
|
||
| 17 | `P6-T1`~`P6-T4` → `P6-GATE` | `P5-GATE` | 아니요 | 실패 소유 Task로 되돌림 |
|
||
| 18 | `P7-T1` → `P7-T2` → `P7-GATE` | Phase 1~6 최신 Gate | 아니요 | 실패 소유 Phase에 회귀 수정 Goal 추가 |
|
||
|
||
## 변경 금지·중단 규칙
|
||
|
||
- PRD와 Endpoint Contract Summary를 근거 없이 변경하거나 제공되지 않은 DTO·enum·오류 status/key를 추정하지 않는다.
|
||
- 기존 완료 체크박스, 검증 기록과 Progress를 삭제·되돌리거나 덮어쓰지 않는다.
|
||
- review 후보는 재현·판정 전 production 수정으로 전환하지 않고, 확정 항목만 관련 Goal 또는 신규 회귀 수정 Goal로 처리한다.
|
||
- test를 삭제·skip·완화하거나 관련 없는 refactor·dependency·DDL을 추가해 Gate를 통과시키지 않는다.
|
||
- JWT, token, signed URL 전체, private path와 파일 본문을 문서·fixture·log에 기록하지 않는다.
|
||
- 일부 체크박스, 일부 test 또는 코드 작성만 끝난 상태에서는 Goal을 `complete`로 갱신하지 않는다.
|
||
|
||
## Goal Progress
|
||
|
||
기존 기록을 삭제하거나 덮어쓰지 않고 Goal 실행 결과를 차수별로 누적한다.
|
||
|
||
### `P2-R1` 실행 준비 — 2026-07-27
|
||
|
||
- 상태: 대기
|
||
- 무엇을: Phase 2~7의 Goal ID, 시작 조건, 완료 증거, 범위 밖, 체크박스와 Gate 구조를 준비했다.
|
||
- 왜: 기존 Phase 2·3 체크만으로 심층 리뷰와 최종 완료를 판정할 수 없었기 때문이다.
|
||
- 어떻게: PRD, 기존 계획과 `docs/sample/sample-plan-task.md`, `docs/sample/sample-review.md`를 대조했다.
|
||
- 남은 항목: `P2-R1` read-only 리뷰 실행.
|
||
- 다음 행동: 사용자가 goal 실행을 요청하면 `P2-R1`만 `create_goal`에 등록한다.
|
||
|
||
### `P2-R1` 완료 — 2026-07-27
|
||
|
||
- 상태: 완료
|
||
- 무엇을: Phase 2 character slice의 PRD Feature B, Endpoint Contract Summary, production/test 구현을 대조했다.
|
||
- 왜: 기존 `P2-H1`, `P2-H2` 완료 이력 이후 `P2-GATE` 전에 확정 finding을 소유 Goal에 연결해야 하기 때문이다.
|
||
- 어떻게: `docs/20260724_AI캐릭터_관리자_API/reviews/phase2-character-review.md`를 작성하고 `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*'`를 실행했다.
|
||
- 결과: focused test는 `BUILD SUCCESSFUL in 51s`였고, `REV-001`~`REV-003`, `REV-007`, `REV-008`을 확정으로 유지해 `P2-T3`~`P2-T6`에 연결했다.
|
||
- 남은 항목: `P2-T3` 캐릭터 목록·검색·상세 보완부터 직렬 실행.
|
||
|
||
### `P2-T3` 완료 — 2026-07-27
|
||
|
||
- 상태: 완료
|
||
- 무엇을: 캐릭터 목록 응답을 상세 DTO에서 목록 전용 DTO로 분리했다.
|
||
- 왜: 목록 API가 계약에 없는 상세 전용 `creatorProfileImageUrl`, `creatorIntroduce`, `updatedAtUtc`를 노출했기 때문이다.
|
||
- 어떻게: RED로 `AiCharacterAdminCharacterControllerTest` exact field 비노출 assertion을 추가했고, `AiCharacterAdminCharacterListItemResponse`와 `toListItemResponse`를 최소 구현했다.
|
||
- 결과: RED는 `AiCharacterAdminCharacterControllerTest` line 66 실패로 확인했고, GREEN 후 `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerTest`와 `./gradlew ktlintCheck`가 모두 `BUILD SUCCESSFUL`이었다.
|
||
- 남은 항목: `P2-T4` 캐릭터 생성 흐름 보완.
|
||
|
||
### `P2-T4` 완료 — 2026-07-27
|
||
|
||
- 상태: 완료
|
||
- 무엇을: 생성 request의 서버 소유 field를 거부하고, 원작 존재를 외부 캐릭터 생성 전에 확인했다. 정상 생성의 원작 연결·AI creatorMember 표시 정보·언어 감지 event 및 중복·외부 API·S3 실패 결과를 v2/legacy test로 고정했다.
|
||
- 왜: `REV-002`, `REV-003`, `REV-007`에서 request 입력 의미와 외부 부작용 전 DB 검증 증거가 부족했기 때문이다.
|
||
- 어떻게: `DEC-P2-T4-001`로 canonical contract와 legacy failure boundary를 고정한 뒤 RED/GREEN test를 추가하고, focused/legacy 및 formatting 검증을 실행했다.
|
||
- 결과: specified focused/legacy test와 `ktlintCheck`가 모두 `BUILD SUCCESSFUL`이었다. 외부 API 삭제 endpoint나 신규 DB unique 제약은 기존 external contract·DDL 금지 범위 밖이라 추가하지 않았다.
|
||
- 남은 항목: `P2-T5` 캐릭터 수정·비활성화 흐름 보완.
|
||
|
||
### `P2-T5` 완료 — 2026-07-27
|
||
|
||
- 상태: 완료
|
||
- 무엇을: PUT의 response 전용 `externalCharacterId`와 `isActive=false` 혼합 요청을 거부하고, 원작을 외부 수정 전에 검증했다. 일반 수정의 image 유지·교체, AI creatorMember 표시 정보, 번역 event, soft delete의 Member·콘텐츠 보존과 flush 후 `updatedAtUtc`를 회귀로 고정했다.
|
||
- 왜: `REV-002`, `REV-003`, `REV-007`에서 update 입력 의미, S3/외부/DB 실패 경계와 response timestamp 증거가 부족했기 때문이다.
|
||
- 어떻게: `DEC-P2-T5-001`로 PUT contract와 non-compensated external update 경계를 확정한 뒤 RED/GREEN test를 추가하고 지정 focused/legacy 및 formatting 검증을 실행했다.
|
||
- 결과: 지정 focused/legacy test와 `ktlintCheck`가 모두 `BUILD SUCCESSFUL`이었다. 외부 update restore API와 기존 image hard delete는 external contract·legacy parity 범위 밖이라 추가하지 않았다.
|
||
- 남은 항목: `P2-T6` Phase 2 보안·오류·회귀 보완.
|
||
|
||
### `P2-T6` 완료 — 2026-07-27
|
||
|
||
- 상태: 완료
|
||
- 무엇을: 문자열 `unmapped-path`를 character resource handler에서 제외해 prefix fallback의 404/i18n/CORS 계약으로 보냈고, 실제 목록·상세·생성·수정 endpoint의 ADMIN·binding/domain/multipart·CORS 증거를 보강했다.
|
||
- 왜: 문자열 path가 `Long` binding의 400으로 처리되어 `REV-001`을 위반했고, `REV-008`의 실제 endpoint matrix 증거가 부족했기 때문이다.
|
||
- 어떻게: 기존 404 KO/EN/JA·CORS 4건을 RED로 재현하고 GET/PUT path를 `[0-9]+`로 제한했다. 목록 binding, 상세 target, 생성/수정 multipart KO/EN/JA, 실제 네 endpoint non-ADMIN과 detail preflight를 parameterized/focused test로 확인했다.
|
||
- 결과: 지정 focused test는 `BUILD SUCCESSFUL in 1m 26s`, `./gradlew ktlintCheck`는 `BUILD SUCCESSFUL in 34s`였다. `AiCharacterAdminCharacterServiceTest`는 존재하지 않는 과거 계획 참조임을 정정 기록으로 보존했고, legacy/public contract와 DTO 의존 방향은 변경하지 않았다.
|
||
- 남은 항목: `P2-GATE`.
|
||
|
||
### `P2-GATE` 완료 — 2026-07-27
|
||
|
||
- 상태: 완료
|
||
- 무엇을: Phase 2 캐릭터 관리의 PRD 추적성, 정상·실패 흐름, legacy 회귀를 최종 판정했다.
|
||
- 왜: Phase 3 실행 전 `P2-R1`, `P2-T3`~`P2-T6`의 완료 증거와 Gate 명령 성공이 필요하기 때문이다.
|
||
- 어떻게: Gate에 명시된 세 명령과 `git diff --check`를 실행했다.
|
||
- 결과: character focused 명령은 병렬 실행 중 XML 결과 파일 write 충돌로 한 번 실패했으나 동일 명령 단독 재실행은 `BUILD SUCCESSFUL in 1m 11s`였다. authorization/error 명령은 `BUILD SUCCESSFUL in 1m 30s`, `ktlintCheck`는 `BUILD SUCCESSFUL`이었다. `REV-001`~`REV-003`, `REV-007`, `REV-008`의 Phase 2 소유 항목은 처리 완료로 판정했다.
|
||
- 남은 항목: `P3-R1` Phase 3 요구사항·계약·코드 리뷰.
|
||
|
||
### `P3-T7` 완료 — 2026-07-27
|
||
|
||
- 상태: 완료
|
||
- 무엇을: 실제 Phase 3 테마·목록·상세·생성·수정 endpoint의 ADMIN/stale claim, CORS, ownership no-side-effect와 malformed resource path 오류 계약을 독립 focused test로 고정했다.
|
||
- 왜: 기존 공통 authorization/error test는 prefix 공통 계약을, 콘텐츠 focused test는 domain ownership을 보장했지만 실제 endpoint가 문자열 식별자를 404 fallback으로 보내는 증거가 없었다.
|
||
- 어떻게: `AiCharacterAdminAudioContentOwnershipTest`에서 malformed `characterId`/`contentId` RED를 먼저 확인하고, controller resource path를 `[0-9]+`로 제한했다. 기존 콘텐츠/authorization/error focused regression과 legacy characterization을 함께 실행했다.
|
||
- 결과: RED는 6건의 400/415 대 404 불일치로 확인했고, 최소 path 제약 적용 후 ownership focused 18건과 지정 content+authorization+error 회귀가 모두 `BUILD SUCCESSFUL`이었다. `ktlintCheck`도 통과했다.
|
||
- 남은 항목: `P3-GATE`.
|
||
|
||
### `P3-GATE` 완료 — 2026-07-27
|
||
|
||
- 상태: 완료
|
||
- 무엇을: Phase 3 콘텐츠 관리와 signed URL의 PRD 추적성, pipeline 안전성, legacy 회귀를 최종 판정했다.
|
||
- 왜: Phase 4 진행 전 `P3-R1`, `P3-T3`~`P3-T7`의 완료 증거와 Gate 명령 성공이 필요하기 때문이다.
|
||
- 어떻게: Gate에 명시된 content focused, authorization/error, `ktlintCheck` 세 명령을 fresh 실행했다.
|
||
- 결과: content focused 명령은 `BUILD SUCCESSFUL in 2m 15s`, authorization/error 명령은 `BUILD SUCCESSFUL in 1m 29s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 1s`였다. `REV-004`~`REV-008`의 Phase 3 소유 항목은 처리 완료로 판정했다.
|
||
- 남은 항목: `P4-T1` 시리즈 요구사항·계약·코드 리뷰.
|
||
|
||
### Phase 2·3 2차 리뷰 완료 — 2026-07-27
|
||
|
||
- 상태: 리뷰 완료, 후속 수정 Goal 대기
|
||
- 무엇을: Phase 2·3 production/test와 기존 review·Gate 완료 기록을 PRD와 다시 대조했다.
|
||
- 왜: 기존 focused test 성공뿐 아니라 각 완료 체크박스가 요구한 실제 endpoint·failure-order 증거가 존재하는지 독립 검증하기 위해.
|
||
- 어떻게: 두 review 문서에 2차 리뷰를 누적하고 character/content/common test를 `--rerun-tasks`로 한 번에 실행한 뒤 XML별 test 수와 lint를 확인했다.
|
||
- 결과: targeted 14개 XML class의 199건은 failure/error/skipped 0으로 `BUILD SUCCESSFUL in 9m 23s`, `ktlintCheck --rerun-tasks`는 7개 task가 실행되어 `BUILD SUCCESSFUL in 27s`였다. 기능 실패는 재현되지 않았으나 완료 기록보다 직접 증거가 좁은 `REV-009`~`REV-011`을 확정했다.
|
||
- 남은 항목: `P2-R2` → `P2-R2-GATE` → `P3-R2` → `P3-R3` → `P3-R2-GATE`.
|
||
|
||
### `P3-R2` 완료 — 2026-07-27
|
||
|
||
- 상태: 완료
|
||
- 무엇을: 실제 생성 endpoint의 multipart part별 KO/EN/JA 오류, invalid theme 선검증, cover upload 실패 rollback/event 0회 증거를 보강했다.
|
||
- 왜: `REV-010`에서 기존 생성 완료 증거가 계획의 직접 증거보다 좁다고 확정됐기 때문이다.
|
||
- 어떻게: `AiCharacterAdminAudioContentCreateTest`를 확장했고, 실패 원인은 production 계약 위반이 아니라 테스트 기대 message와 `NOT_SUPPORTED` fixture의 transaction 누락임을 확인해 테스트만 최소 정정했다.
|
||
- 결과: create 단독 명령은 `BUILD SUCCESSFUL in 1m 4s`, content/common 회귀 명령은 `BUILD SUCCESSFUL in 2m 41s`였다.
|
||
- 남은 항목: `P3-R3`.
|
||
|
||
### `P3-R3` 완료 — 2026-07-27
|
||
|
||
- 상태: 완료
|
||
- 무엇을: 수정 차집합, cover 유지/교체/실패, ownership/domain no-side-effect 증거를 Phase 3 actual endpoint 범위에서 보강했다.
|
||
- 왜: `REV-011`에서 기존 수정·ownership 완료 증거가 계획의 직접 증거보다 좁다고 확정됐기 때문이다.
|
||
- 어떻게: 기존 Phase 3 content test 보강분을 content/common 회귀로 재검증했다.
|
||
- 결과: content/common 회귀 명령은 `BUILD SUCCESSFUL in 2m 41s`였다.
|
||
- 남은 항목: `P3-R2-GATE`.
|
||
|
||
### `P3-R2-GATE` 완료 — 2026-07-27
|
||
|
||
- 상태: 완료
|
||
- 무엇을: Phase 3 후속 리뷰의 `REV-010`~`REV-011` 처리를 종결했다.
|
||
- 왜: 사용자 지시에 따라 Phase 3 후속 보완까지만 진행하고 Phase 4 이후는 시작하지 않기 위해서다.
|
||
- 어떻게: `phase3-audio-content-review.md`에 3차 후속 검증 기록을 누적하고 content/common 회귀와 `ktlintCheck`를 실행했다.
|
||
- 결과: content/common 회귀 명령은 `BUILD SUCCESSFUL in 2m 41s`였다. `ktlintCheck` 결과는 검증 기록에 누적한다.
|
||
- 남은 항목: 없음. Phase 4는 진행하지 않는다.
|
||
|
||
### Phase 2·3 4차 재리뷰 완료 — 2026-07-27
|
||
|
||
- 상태: 리뷰 완료, 후속 보완 대기
|
||
- 무엇을: `P2-R2`, `P3-R2`~`P3-R3` 반영분과 완료 체크리스트를 production 흐름·test method 단위로 다시 대조했다.
|
||
- 왜: 회귀 통과만으로 `REV-009`~`REV-011`의 선언된 실패·ownership 증거 전체가 충족됐다고 판정할 수 없기 때문이다.
|
||
- 어떻게: 변경된 test 5개, character/content facade와 legacy service failure order를 확인하고 targeted 전체를 `--rerun-tasks`로 실행했다.
|
||
- 결과: 관련 14개 XML class의 228건은 failure/error/skipped 0으로 `BUILD SUCCESSFUL in 9m 44s`, `ktlintCheck --rerun-tasks`는 `BUILD SUCCESSFUL in 41s`였다. 적용된 테스트는 유효하지만 DB/event 실패, 생성 후반 S3/event 실패와 ownership no-side-effect matrix가 완료 기록보다 좁아 `REV-012`~`REV-014`를 확정했다.
|
||
- 남은 항목: `P2-R3` → `P2-R3-GATE` → `P3-R4` → `P3-R3-GATE`.
|
||
|
||
### `P2-R3` / `P2-R3-GATE` 완료 — 2026-07-27
|
||
|
||
- 상태: 완료
|
||
- 무엇을: character 생성·수정의 external/S3 실패 locale matrix와 event publish 실패 경계를 보강했다.
|
||
- 왜: `REV-012`에서 DB/event 실패 뒤 내부 rollback과 external/S3 잔존 결과 직접 증거가 부족하다고 확정됐기 때문이다.
|
||
- 어떻게: `AiCharacterAdminCharacterControllerMutationTest`에 KO/EN/JA 대표 실패와 facade 직접 event failure characterization을 추가했다.
|
||
- 결과: mutation focused 명령은 `BUILD SUCCESSFUL in 1m 1s`였다.
|
||
- 남은 항목: `P3-R4`.
|
||
|
||
### `P3-R4` / `P3-R3-GATE` 완료 — 2026-07-27
|
||
|
||
- 상태: 완료
|
||
- 무엇을: content 생성의 audio upload/event 실패와 실제 네 경로 unknown target no-side-effect matrix를 보강했다.
|
||
- 왜: `REV-013`~`REV-014`에서 생성 후반 실패 순서와 ownership/domain 부작용 없음 증거가 부족하다고 확정됐기 때문이다.
|
||
- 어떻게: `AiCharacterAdminAudioContentCreateTest`, `AiCharacterAdminAudioContentOwnershipTest`를 확장하고 content/common 회귀와 `ktlintCheck`를 실행했다.
|
||
- 결과: create+ownership focused 명령은 `BUILD SUCCESSFUL in 1m 8s`, content/common 회귀 명령은 `BUILD SUCCESSFUL in 2m 20s`, `ktlintCheck`는 import 정리 후 `BUILD SUCCESSFUL in 17s`였다.
|
||
- 남은 항목: 없음. 사용자 지시에 따라 Phase 4는 진행하지 않는다.
|
||
|
||
### Phase 2·3 5차 재리뷰 완료 — 2026-07-27
|
||
|
||
- 상태: 리뷰 완료, 후속 수정 Goal 대기
|
||
- 무엇을: `P2-R3`, `P3-R4` 반영분의 transaction/rollback, multipart binding, ownership/domain 완료 증거를 PRD·production·test method 단위로 다시 대조했다.
|
||
- 왜: 통과하는 테스트가 기존 finding의 exact exception, actual transaction과 전체 side-effect assertion을 실제로 보장하는지 확인하기 위해서다.
|
||
- 어떻게: `phase2-character-review.md`, `phase3-audio-content-review.md`에 5차 리뷰를 누적하고 변경된 핵심 test 세 클래스를 `--rerun-tasks`로 실행한 뒤 XML 수치, lint와 diff check를 확인했다.
|
||
- 결과: 세 XML 합계 79건은 failure/error/skipped 0으로 `BUILD SUCCESSFUL in 7m 50s`, `ktlintCheck --rerun-tasks`는 `BUILD SUCCESSFUL in 28s`, `git diff --check`는 출력이 없었다. 테스트는 통과했지만 `REV-015`~`REV-017`의 계약·완료 증거 누락을 확정했다.
|
||
- 남은 항목: `P2-R4` → `P2-R4-GATE` → `P3-R5` → `P3-R6` → `P3-R4-GATE`.
|
||
|
||
### `P3-R5` 완료 — 2026-07-27
|
||
|
||
- 상태: 완료
|
||
- 무엇을: 생성 controller와 facade의 `coverImage`·`audioFile`을 non-null `MultipartFile`로 고정하고, 세 필수 part 누락 KO/EN/JA actual endpoint 9건의 exact MVC binding 계약을 보강했다.
|
||
- 왜: nullable 파일 part가 facade와 legacy `AudioContentService`까지 전달되어 `MissingServletRequestPartException` 및 `common.error.invalid_request` 계약을 우회했기 때문이다.
|
||
- 어떻게: RED에서 `coverImage`·`audioFile` 누락 6건이 legacy content 전용 message로 실패함을 확인한 뒤, non-null binding으로 변경했다. 공통 helper는 각 요청의 400 `ApiResponse.error`, exact exception, DB count 0, S3 `putObject` 0회, event no-interaction을 단언한다.
|
||
- 결과: create/error focused 회귀는 `BUILD SUCCESSFUL in 1m 1s`, content/common 회귀는 `BUILD SUCCESSFUL in 2m 46s`, `ktlintCheck`는 `BUILD SUCCESSFUL`이었다. 전체 `./gradlew test`는 controller binding의 직접 영향 범위를 두 targeted 명령이 포함하고 release/Gate 범위가 아니므로 실행하지 않았다.
|
||
- 남은 항목: `P3-R6` 및 `P3-R4-GATE`.
|
||
|
||
### `P3-R6` 완료 — 2026-07-27
|
||
|
||
- 상태: 완료. `P3-R4-GATE`는 `P2-R4-GATE` 뒤 실행 대기다.
|
||
- 무엇을: `AiCharacterAdminAudioContentControllerTest`의 cross-owner detail/update, create/update 다른 owner `seriesIds`, invalid `releaseDateUtc` 실제 endpoint를 각각 KO/EN/JA matrix로 확장했다.
|
||
- 왜: 기존 단일 locale 또는 부분 assertion으로는 ownership/domain 검증이 S3 업로드, DB 변경, event publish보다 앞선다는 완료 증거가 부족했다.
|
||
- 어떻게: 테스트 우선으로 exact 400 `ApiResponse.error` envelope, 요청 전후 `AudioContent`·`SeriesContent` count와 field/연결 row, S3 `putObject` 0회, `ApplicationEventPublisher` no-interaction을 단언했다. target과 other-owner ID를 분리한 fixture라 owner/domain guard를 제거하면 success/status 또는 state assertion이 실패한다.
|
||
- 결과: focused characterization은 `BUILD SUCCESSFUL in 34s`로 기존 production 계약 충족을 확인해 test-only로 종료했다. content/common 회귀는 `BUILD SUCCESSFUL in 1m 13s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 12s`였다. 전체 `./gradlew test`는 변경이 Phase 3 content endpoint 테스트에 한정되고 focused·content/common 명령이 직접 범위를 포함하므로 실행하지 않았다.
|
||
- 남은 항목: `P2-R4-GATE` 후 `P3-R4-GATE`. Phase 4는 진행하지 않는다.
|
||
|
||
### `P2-R4-GATE` / `P3-R4-GATE` 완료 — 2026-07-28
|
||
|
||
- 상태: 완료
|
||
- 무엇을: Phase 2·3 5차 리뷰 보완의 최종 Gate를 종결했다.
|
||
- 왜: `REV-015` actual transaction evidence, `REV-016` exact multipart binding, `REV-017` ownership/domain matrix가 모두 보강됐는지 fresh 검증으로 판정하기 위해서다.
|
||
- 어떻게: character mutation/error와 content create/controller/ownership focused 명령 및 `git diff --check`를 실행하고 review 문서의 상태를 처리 완료로 갱신했다.
|
||
- 결과: focused 명령은 `BUILD SUCCESSFUL in 52s`, `git diff --check`는 출력이 없었다. `P2-R4-GATE`, `P3-R4-GATE` 모두 완료했고 Phase 4는 사용자 진행 지시 전까지 시작하지 않는다.
|
||
- 남은 항목: 없음. 다음 Goal은 `P4-T1`이지만 사용자 진행 지시가 필요하다.
|
||
|
||
### Phase 2·3 6차 리뷰 완료 — 2026-07-28
|
||
|
||
- 상태: 완료
|
||
- 무엇을: 5차 보완 결과와 Endpoint Contract Summary, multipart empty-file 경계, Phase 3 event no-interaction의 실제
|
||
관찰 대상을 다시 대조했다.
|
||
- 왜: 통과하는 focused test와 완료된 review ID 외에 client contract 위반, 제한 조건의 파일 손상 가능성 또는
|
||
detached mock으로 가려진 증거 공백이 남았는지 확인하기 위해서다.
|
||
- 어떻게: character/content production과 관련 test를 정적 대조하고 Phase 2·3 5차 Gate focused 5개 class를
|
||
`--rerun-tasks`로 재실행했다. 두 review 문서에 `REV-018`~`REV-020`을 누적하고 각 finding을 독립 Task/Gate에 연결했다.
|
||
- 결과: focused XML 합계 216건은 failure/error/skipped 0으로 `BUILD SUCCESSFUL in 4m 16s`,
|
||
`ktlintCheck --rerun-tasks`는 `BUILD SUCCESSFUL in 17s`, staged/unstaged `git diff --check`는 출력이 없었다.
|
||
테스트 통과와 별개로 문서 계약 1건, production empty-file 경계 1건, test evidence 1건을 확정했다.
|
||
- 보완 결과: `P2-R5`, `P2-R5-GATE`, `P3-R7`, `P3-R8`, `P3-R5-GATE`를 완료했다.
|
||
- 최종 검증: content/common 회귀는 `BUILD SUCCESSFUL in 1m 30s`, `ktlintCheck`는 `BUILD SUCCESSFUL in 17s`, `git diff --check`는 출력이 없었다.
|
||
- 남은 항목: 없음. Phase 4는 사용자 진행 지시 전까지 시작하지 않는다.
|
||
|
||
### Phase 2·3 6차 보완 재점검 완료 — 2026-07-28
|
||
|
||
- 상태: 완료
|
||
- 무엇을: `P2-R5`, `P2-R5-GATE`, `P3-R7`, `P3-R8`, `P3-R5-GATE`의 실제 코드·테스트·완료 기록을 다시
|
||
대조하고 하단 종합 finding 상태를 점검했다.
|
||
- 왜: 6차 보완의 empty-file·actual publisher·문서 계약 수정이 실제로 유지되는지와 완료된 finding이 미처리 상태로
|
||
남아 있지 않은지 확인하기 위해서다.
|
||
- 어떻게: Phase 2 character mutation과 Phase 3 create/update/controller/ownership 5개 class를 `--rerun-tasks`로
|
||
실행하고, `REV-001`~`REV-020`의 소유 Goal·Gate·Progress 기록을 하단 종합 표와 대조했다.
|
||
- 결과: 5개 XML 합계 130건은 failure/error/skipped 0으로 `BUILD SUCCESSFUL in 4m 19s`였다. 6차 보완 범위의 추가
|
||
production 결함은 재현되지 않았다. 별도 문서 문제로 종결된 `REV-001`~`REV-009`가 종합 표에서 `확정`으로 남은 상태
|
||
불일치를 확인해 `처리 완료`로 동기화했다.
|
||
- 남은 항목: 없음. 다음 Goal은 `P4-T1`이지만 사용자 진행 지시 전까지 시작하지 않는다.
|
||
|
||
## Decision Log
|
||
|
||
| 날짜 | ID | 상태 | 결정 | 근거 | 영향 Goal/문서 |
|
||
|---|---|---|---|---|---|
|
||
| 2026-07-27 | `DEC-GOAL-001` | 확정 | 기존 Phase 2·3 완료 Task는 이력으로 보존하고 심층 리뷰·세부 보완·Gate Goal을 추가한다. | 기존 검증 이후 후속 보완이 반복됐고 Phase 완료 Gate가 없었다. | `P2-R1`~`P3-GATE` |
|
||
| 2026-07-27 | `DEC-REVIEW-001` | 확정 | 코드 리뷰의 8개 확정 finding은 기존 미실행 범주형 Goal에 `REV-001`~`REV-008`로 귀속하고 Phase 안에서 직렬 실행한다. | 새 Goal을 중복 추가하거나 기존 완료 이력을 다시 열지 않으면서 각 finding의 재현·완료 증거를 독립 추적하기 위해. | `P2-R1`~`P2-GATE`, `P3-R1`~`P3-GATE` |
|
||
| 2026-07-27 | `DEC-P2-T4-001` | 확정 | POST 생성은 외부 API 필수 입력인 `systemPrompt`를 받고, `externalCharacterId`는 외부 API가 반환하는 response 전용 값이며, `isActive`는 서버가 `true`로 생성하는 response 상태다. `characterType`은 생략 시 `Character`, 잘못된 값은 외부 부작용 전 400이다. 중복 이름은 legacy와 같은 `findByName` 선검증만 적용하며 신규 DDL 없이 동시 요청의 DB unique 보장은 추가하지 않는다. 원작·중복·타입 검증은 외부 생성 전에 수행한다. 외부 API 실패는 DB/S3/event를 남기지 않고, S3 실패는 DB transaction과 event를 롤백하지만 legacy에 삭제 API가 없으므로 이미 생성된 외부 캐릭터는 보상하지 않는다. | Endpoint Contract Summary의 축약 JSON이 외부 API 반환값을 입력처럼 표기하지만, legacy 등록과 v2 external client 모두 `systemPrompt`로 외부 생성을 요청하고 ID를 응답에서 받는다. PRD의 external API 계약·DDL 변경 금지와 legacy failure order를 유지한다. | `P2-T4`, `REV-002`, `REV-003`, `REV-007` |
|
||
| 2026-07-27 | `DEC-P2-T5-001` | 확정 | PUT의 `externalCharacterId`는 response 전용으로 명시 거부한다. `isActive=false`는 image와 일반 수정 field를 섞지 않는 단독 soft delete다. 일반 수정은 image를 생략하면 기존 경로를 유지하고, 존재하지 않는 `originalWorkId`는 외부 수정 전에 거부한다. 외부 수정 실패는 S3/DB/event를 남기지 않으며, S3 실패는 DB/event를 롤백하지만 legacy와 같은 external update restore 계약이 없어 성공한 외부 수정은 보상하지 않는다. 응답 `updatedAtUtc`는 DB flush 후 매핑한다. | 기존 service의 soft delete는 다른 field를 무시해 이미지 업로드 고아를 남겼고, `@PreUpdate` timestamp는 flush 전에는 이전 값을 반환했다. 외부 API delete/restore 추가와 DDL은 범위 밖이다. | `P2-T5`, `REV-002`, `REV-003`, `REV-007` |
|
||
| 2026-07-27 | `DEC-REVIEW-002` | 확정 | 기존 Phase 2·3 Task/Gate 완료 이력은 보존하고, 2차 리뷰에서 확인한 검증 증거 누락은 `REV-009`~`REV-011`과 새 후속 Task/Gate로 처리한다. | fresh 199건은 모두 통과했지만 기존 완료 체크리스트의 실제 endpoint·failure-order 범위와 test method가 일치하지 않았다. | `P2-R2`~`P3-R2-GATE`, 두 2차 review |
|
||
| 2026-07-27 | `DEC-REVIEW-003` | 확정 | 3차 수정에서 유효하게 보강된 범위와 기존 완료 이력은 보존하고, 아직 직접 고정되지 않은 실패·ownership 경계만 `REV-012`~`REV-014`와 새 Task/Gate로 추적한다. | fresh 228건은 모두 통과했지만 `Task 2.8`~`Task 3.10`의 완료 체크리스트와 실제 failure injection·side-effect assertion 범위가 다시 일치하지 않았다. | `P2-R3`~`P3-R3-GATE`, 두 4차 review |
|
||
| 2026-07-27 | `DEC-REVIEW-004` | 확정 | 4차 보완의 유효한 테스트와 완료 이력은 보존하고, exact multipart 계약과 actual transaction·ownership/domain 직접 증거 누락은 `REV-015`~`REV-017` 및 새 Task/Gate로 추적한다. | fresh 79건은 통과했지만 nullable file binding, direct facade event test와 unknown target에 한정된 matrix가 기존 완료 조건보다 좁았다. | `P2-R4`~`P3-R4-GATE`, 두 5차 review |
|
||
| 2026-07-28 | `DEC-REVIEW-005` | 확정 | 5차 보완과 완료 이력은 보존하고, 추가로 확인한 문서 계약·empty-file 경계·detached publisher 증거 문제를 `REV-018`~`REV-020`과 새 후속 Task/Gate로 추적한다. | fresh focused 216건과 lint는 통과했지만 Endpoint Contract Summary, `MultipartFile.isEmpty` 처리와 실제 service publisher field를 코드·test 단위로 대조해 세 문제가 재현됐다. | `P2-R5`~`P3-R5-GATE`, 두 6차 review |
|
||
| 2026-07-28 | `DEC-P3-R7-001` | 확정 | 생성의 빈 `coverImage`·`audioFile`은 400 `common.error.invalid_request`로 거부한다. 수정의 빈 `coverImage`는 생략으로 정규화하고, 수정 `audioFile`은 미지원이므로 part가 존재하면 크기와 관계없이 400으로 거부한다. | non-null binding은 part 누락만 차단하며 빈 파일은 0-byte upload와 cover 교체를 유발할 수 있다. optional cover의 빈 part는 일반 multipart client의 생략 표현으로 안전하게 처리할 수 있지만 미지원 audio part는 존재 자체가 계약 위반이다. | `P3-R7`, `REV-019`, Endpoint Contract Summary |
|
||
| 2026-07-28 | `DEC-REVIEW-006` | 확정 | Phase 2·3 6차 보완의 코드와 완료 이력은 유지하고 새 production Goal은 추가하지 않는다. 종결 Gate가 있는 `REV-001`~`REV-009`의 종합 표 상태만 `처리 완료`로 동기화한다. | fresh focused 130건이 모두 통과했고 6차 보완 범위의 추가 production 결함은 재현되지 않았지만, 하단 종합 표 상태가 각 Gate·Progress의 처리 완료 판정과 모순됐다. | `P2-GATE`, `P2-R2-GATE`, `P3-GATE`, 하단 발견된 문제 표 |
|
||
|
||
## 발견된 문제
|
||
|
||
| ID | 심각도 | 상태 | 발견 내용 | 영향 Goal | 처리 계획 |
|
||
|---|---|---|---|---|---|
|
||
| `REV-001` | Blocker | 처리 완료 | 문자열 미매핑 경로가 `/{characterId}`에 매핑되어 Phase 1의 404 계약 대신 type mismatch 400을 반환한다. | `P2-R1`, `P2-T6` | numeric path mapping과 KO/EN/JA·CORS 회귀를 고정했다. |
|
||
| `REV-002` | High | 처리 완료 | 캐릭터 생성·수정 DTO의 `systemPrompt`, `externalCharacterId`, 생성 `isActive`와 soft-delete 혼합 의미가 Endpoint Contract Summary와 다르다. | `P2-R1`, `P2-T4`, `P2-T5` | canonical request를 확정하고 문서·actual endpoint 계약을 동기화했다. |
|
||
| `REV-003` | High | 처리 완료 | 외부 API·S3·DB 실패 사이에 보상 경계가 없고 동시 중복 이름 생성의 원자성 증거가 없다. | `P2-R1`, `P2-T4`, `P2-T5` | failure-order·선검증·비보상 경계와 신규 DDL 없는 동시성 정책을 고정했다. |
|
||
| `REV-004` | High | 처리 완료 | 동일한 `seriesIds` 수정도 기존 연결을 삭제·재생성해 row ID·`orders`·`createdAt`을 소실한다. | `P3-R1`, `P3-T6` | 교집합 row를 보존하고 차집합만 변경하도록 수정·검증했다. |
|
||
| `REV-005` | High | 처리 완료 | 콘텐츠 상세의 `releaseDate`, `isOnlyRental`, `purchaseOption`이 legacy 상세 파생 규칙과 다르다. | `P3-R1`, `P3-T4` | legacy 조합 matrix와 v2 `releaseDateUtc` 분리 mapping을 고정했다. |
|
||
| `REV-006` | High | 처리 완료 | 콘텐츠 생성의 필수 `coverImage`·`audioFile`이 nullable binding이라 정확한 missing-part 오류 계약을 우회한다. | `P3-R1`, `P3-T5`, `P3-T7` | non-null binding과 세 part별 exact exception·KO/EN/JA endpoint 회귀를 추가했다. |
|
||
| `REV-007` | Medium | 처리 완료 | 목록 초과 field, pre-flush timestamp, `characterType`, legacy/public 중첩 DTO, `tags`·생성 `isActive`에서 추가 계약/parity 차이가 있다. | `P2-T3`~`P2-T6`, `P3-T3`~`P3-T5`, `P3-T7` | 소유 Goal별 exact schema·field 의미·DTO 경계와 legacy parity를 분리 검증했다. |
|
||
| `REV-008` | High | 처리 완료 | endpoint별 인가/i18n, failure-order characterization과 계획에 명시된 test 파일·검증 수의 완료 증거가 부족하다. | `P2-R1`, `P2-T6`, `P3-R1`, `P3-T5`, `P3-T7` | 실제 endpoint matrix와 characterization을 보강하고 정정 기록을 누적했다. |
|
||
| `REV-009` | Medium | 처리 완료 | Phase 2 실제 네 endpoint의 stale claim·허용/거부 CORS·오류와 mutation 실패 경계 직접 증거가 Gate 기록보다 좁다. | `P2-R2`, `P2-R2-GATE` | actual endpoint matrix와 실패 후 DB/S3/external/event 결과를 보강했다. |
|
||
| `REV-010` | Medium | 처리 완료 | Phase 3 생성의 invalid theme, 실제 multipart KO/EN/JA와 S3/processing/event 실패 순서 직접 증거가 완료 기록보다 좁다. | `P3-R2`, `P3-R2-GATE` | 생성 actual endpoint와 legacy failure characterization을 보강했다. |
|
||
| `REV-011` | Medium | 처리 완료 | Phase 3 수정의 교집합+추가+제거 metadata, cover 성공/실패와 ownership no-side-effect 직접 증거가 완료 기록보다 좁다. | `P3-R3`, `P3-R2-GATE` | update/ownership actual endpoint 회귀를 보강했다. |
|
||
| `REV-012` | Medium | 처리 완료 | Phase 2의 DB save/flush·event publish 실패 뒤 내부 rollback과 external/S3 잔존 결과를 직접 검증하지 않았다. | `P2-R3`, `P2-R3-GATE` | 실제 transaction failure injection과 내부·외부 결과 단언을 보강했다. |
|
||
| `REV-013` | Medium | 처리 완료 | Phase 3 생성은 첫 cover upload 실패만 검증하고 두 번째 audio upload와 event 실패 결과를 직접 고정하지 않았다. | `P3-R4`, `P3-R3-GATE` | 생성 후반 failure order와 비트랜잭션 S3 결과를 특성화했다. |
|
||
| `REV-014` | Medium | 처리 완료 | Phase 3 실제 네 경로의 ownership/domain KO/EN/JA와 DB/S3/event no-side-effect matrix가 부분적이다. | `P3-R4`, `P3-R3-GATE` | actual endpoint 오류와 요청 전후 부작용 count를 보강했다. |
|
||
| `REV-015` | Medium | 처리 완료 | Phase 2 event 실패 테스트가 actual endpoint/Spring transaction을 통과하지 않고 DB save/flush·내부 rollback을 직접 단언하지 않는다. | `P2-R4`, `P2-R4-GATE` | actual POST/PUT persistence·event 실패와 transaction 종료 뒤 내부·외부 결과를 고정했다. |
|
||
| `REV-016` | High | 처리 완료 | Phase 3 생성 필수 `coverImage`·`audioFile`이 nullable이라 exact `MissingServletRequestPartException` 계약을 우회했다. | `P3-R5`, `P3-R4-GATE` | non-null binding과 세 part별 exact exception·KO/EN/JA·no-side-effect를 복구했다. |
|
||
| `REV-017` | Medium | 처리 완료 | Phase 3 4차 보강이 unknown target에 한정돼 cross-owner/domain KO/EN/JA와 DB/S3/event matrix가 완료 기록보다 좁다. | `P3-R6`, `P3-R4-GATE` | 대표 cross-owner/series/date actual endpoint의 전체 no-side-effect matrix를 보강했다. |
|
||
| `REV-018` | High | 처리 완료 | 캐릭터 생성 Endpoint Contract Summary가 필수 `systemPrompt`를 누락하고 request 금지 `externalCharacterId`, `isActive`를 포함해 확정 계약과 반대다. | `P2-R5`, `P2-R5-GATE` | 생성 JSON을 `DEC-P2-T4-001`과 동기화하고 기존 actual endpoint 계약 회귀로 확인했다. |
|
||
| `REV-019` | Medium | 처리 완료 | 생성·수정의 빈 multipart 파일이 null/non-empty 검사 사이를 통과해 0-byte upload, cover 교체 또는 수정 `audioFile` 미지원 계약을 우회한다. | `P3-R7`, `P3-R5-GATE` | v2 facade에서 생성 empty-file 거부, 수정 empty cover 정규화와 모든 audio part 거부를 RED/GREEN으로 고정했다. |
|
||
| `REV-020` | Low | 처리 완료 | Phase 3 ownership/domain test의 event no-interaction mock이 실제 `AudioContentService`·`CreatorAdminContentService` publisher field에 연결되지 않았다. | `P3-R8`, `P3-R5-GATE` | 실제 두 service proxy target의 publisher를 mock으로 교체·복원하고 identity/no-interaction을 단언했다. |
|
||
|
||
## 검증 기록
|
||
- Phase 2·3 6차 보완 재점검 focused 검증(2026-07-28): `./gradlew test --rerun-tasks --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentUpdateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentControllerTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentOwnershipTest` 실행 결과 `BUILD SUCCESSFUL in 4m 19s`였다. 다섯 XML 합계 130건의 failure/error/skipped는 모두 0이었다.
|
||
- Phase 2·3 6차 보완 재점검 문서 명령 유효성(2026-07-28): `./gradlew tasks --all` 실행 결과 `test`, `ktlintCheck`, `tasks`가 존재했고 `BUILD SUCCESSFUL in 858ms`였다.
|
||
- Phase 2·3 6차 보완 재점검 판정(2026-07-28): `REV-018`~`REV-020`의 production/test 보완과 Gate 완료 증거가 일치했고 추가 production finding은 확정되지 않았다. 하단 종합 표에서만 미처리로 남은 `REV-001`~`REV-009`를 각 소유 Gate·Progress 판정에 맞춰 `처리 완료`로 동기화했다.
|
||
- `P3-R5-GATE` content/common 회귀(2026-07-28): `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` 실행 결과 reviewer gate 보완 후 최종 `BUILD SUCCESSFUL in 2m 21s`였다.
|
||
- `P3-R5-GATE` lint/diff 검증(2026-07-28): `./gradlew ktlintCheck` 실행 결과 reviewer gate 보완 후 최종 `BUILD SUCCESSFUL in 44s`였고, `git diff --check`는 출력이 없었다.
|
||
- `P3-R5-GATE` 전체 회귀 생략(2026-07-28): 변경 범위가 Phase 2 문서와 Phase 3 content v2 facade/test에 한정되고 content/common 회귀가 실제 영향 범위를 포함하므로 전체 `./gradlew test`는 실행하지 않았다.
|
||
- Phase 2·3 6차 리뷰 fresh focused 검증(2026-07-28): `./gradlew test --rerun-tasks --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentControllerTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentOwnershipTest` 실행 결과 `BUILD SUCCESSFUL in 4m 16s`였다. 다섯 XML 합계 216건의 failure/error/skipped는 모두 0이었다.
|
||
- Phase 2·3 6차 리뷰 lint/diff 검증(2026-07-28): `./gradlew ktlintCheck --rerun-tasks`는 7개 task가 실행되어 `BUILD SUCCESSFUL in 17s`였고, staged/unstaged `git diff --check`는 출력이 없었다.
|
||
- Phase 2·3 6차 리뷰 전체 회귀 생략(2026-07-28): production code를 변경하지 않은 read-only review와 문서 후속 Task 등록이며, 5차 보완의 핵심 actual endpoint 216건과 lint로 직접 범위를 확인했으므로 전체 `./gradlew test`는 실행하지 않았다. 실제 `P3-R7` production 수정 후 content/common 영향 범위 회귀를 실행한다.
|
||
- Phase 2·3 5차 Gate focused 검증(2026-07-28): `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentControllerTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentOwnershipTest` 실행 결과 `BUILD SUCCESSFUL in 52s`였다.
|
||
- Phase 2·3 5차 Gate diff 검증(2026-07-28): `git diff --check`는 출력이 없었다.
|
||
- `P3-R5` RED(2026-07-27): `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest` 실행 결과 14건 중 `coverImage`·`audioFile` 누락 KO/EN/JA 6건이 실패했다. 예를 들어 audio 누락 KO는 기대 `잘못된 요청입니다.` 대신 legacy `콘텐츠를 선택해 주세요.`를 반환해 nullable binding이 MVC 경계를 우회함을 확인했다.
|
||
- `P3-R5` focused 검증(2026-07-27): `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` 실행 결과 `BUILD SUCCESSFUL in 1m 1s`였다. 세 필수 part 누락 KO/EN/JA는 400 generic envelope, exact `MissingServletRequestPartException`, DB/S3/event 0회를 실제 endpoint에서 확인했다.
|
||
- `P3-R5` content/common 회귀(2026-07-27): `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` 실행 결과 `BUILD SUCCESSFUL in 2m 46s`였다.
|
||
- `P3-R5` lint(2026-07-27): `./gradlew ktlintCheck` 실행 결과 `BUILD SUCCESSFUL`이었다.
|
||
- `P3-R5` 최종 fresh 검증(2026-07-27): `./gradlew test --rerun-tasks --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest`는 모든 Gradle task를 재실행해 `BUILD SUCCESSFUL in 3m 33s`였고, `./gradlew ktlintCheck --rerun-tasks`는 `BUILD SUCCESSFUL in 25s`였다.
|
||
- Phase 2·3 5차 리뷰 fresh focused 검증(2026-07-27): `./gradlew test --rerun-tasks --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentOwnershipTest` 실행 결과 `BUILD SUCCESSFUL in 7m 50s`였다. 세 XML 합계 79건의 failure/error/skipped는 모두 0이었다.
|
||
- Phase 2·3 5차 리뷰 lint/diff 검증(2026-07-27): `./gradlew ktlintCheck --rerun-tasks`는 7개 task가 실행되어 `BUILD SUCCESSFUL in 28s`였고, `git diff --check`는 출력이 없었다.
|
||
- Phase 2·3 5차 리뷰 전체 회귀 생략(2026-07-27): production code를 수정하지 않은 review/계획 문서 Task 등록이며 변경된 세 핵심 test를 fresh 실행해 finding을 판정했으므로 전체 `./gradlew test`는 실행하지 않았다. 실제 수정 Goal Gate에서 character/content/common 영향 범위 회귀를 각각 실행한다.
|
||
- Phase 2·3 5차 리뷰 문서 명령 유효성(2026-07-27): `./gradlew tasks --all` 실행 결과 `test`, `ktlintCheck`, `tasks`가 존재했고 `BUILD SUCCESSFUL in 1s`였다.
|
||
- Phase 2·3 4차 보완 focused 검증(2026-07-27): `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.AiCharacterAdminCharacterControllerMutationTest` 실행 결과 `BUILD SUCCESSFUL in 1m 1s`였다. `./gradlew test --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentCreateTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.AiCharacterAdminAudioContentOwnershipTest` 실행 결과 `BUILD SUCCESSFUL in 1m 8s`였다.
|
||
- Phase 2·3 4차 보완 content/common 회귀(2026-07-27): `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` 실행 결과 `BUILD SUCCESSFUL in 2m 20s`였다.
|
||
- Phase 2·3 4차 보완 lint(2026-07-27): `./gradlew ktlintCheck`는 신규 test import ordering/unused import 3건 실패 후 import만 정리해 재실행했고, `BUILD SUCCESSFUL in 17s`였다.
|
||
- Phase 2·3 4차 리뷰 fresh targeted 검증(2026-07-27): `./gradlew test --rerun-tasks --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` 실행 결과 `BUILD SUCCESSFUL in 9m 44s`였다. 관련 XML 14개 합계 228건의 failure/error/skipped는 모두 0이었다.
|
||
- Phase 2·3 4차 리뷰 lint(2026-07-27): `./gradlew ktlintCheck --rerun-tasks` 실행 결과 7개 task가 실행됐고 `BUILD SUCCESSFUL in 41s`였다.
|
||
- Phase 2·3 4차 리뷰 전체 회귀 생략(2026-07-27): production code를 변경하지 않은 리뷰·문서 Task 등록이며 character/content actual endpoint와 공통 authorization/error를 포함한 fresh 228건으로 직접 영향 범위를 확인했으므로 전체 `./gradlew test`는 실행하지 않았다.
|
||
- Phase 2·3 2차 리뷰 fresh targeted 검증(2026-07-27): `./gradlew test --rerun-tasks --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.character.*' --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` 실행 결과 `BUILD SUCCESSFUL in 9m 23s`였다. 생성된 관련 XML 14개 합계 199건의 failure/error/skipped는 모두 0이었다.
|
||
- Phase 2·3 2차 리뷰 lint(2026-07-27): `./gradlew ktlintCheck --rerun-tasks` 실행 결과 7개 task가 실행됐고 `BUILD SUCCESSFUL in 27s`였다.
|
||
- Phase 2·3 2차 리뷰 전체 회귀 생략(2026-07-27): production code를 수정하지 않은 read-only review와 문서 후속 Task 등록이며, character/content actual endpoint와 공통 authorization/error를 포함한 fresh 199건으로 직접 영향 범위를 확인했으므로 전체 `./gradlew test`는 실행하지 않았다.
|
||
- Phase 3 후속 보완 content/common 회귀(2026-07-27): `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.content.*' --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminAuthorizationTest --tests kr.co.vividnext.sodalive.v2.api.admin.aicharacter.AiCharacterAdminErrorContractTest` 실행 결과 `BUILD SUCCESSFUL in 2m 41s`였다.
|
||
- Phase 3 후속 보완 lint(2026-07-27): `./gradlew ktlintCheck`는 import ordering 1건 실패 후 `AiCharacterAdminAudioContentUpdateTest.kt` import 순서만 정리해 재실행했고, `BUILD SUCCESSFUL in 16s`였다.
|
||
- Phase 2·3 코드 리뷰 Task 보완(2026-07-27): 확정 finding 8개를 `REV-001`~`REV-008`로 등록하고 기존 미실행
|
||
`P2-T3`~`P2-T6`, `P3-T3`~`P3-T7`에 소유권, 정확한 RED/RED 확인/GREEN/GREEN 확인/REFACTOR, 파일 경로,
|
||
focused 명령과 Gate 완료 증거를 보강했다. 중복 Goal은 만들지 않고 각 Phase 안에서 직렬 실행하도록 의존성 표를 갱신했다.
|
||
- Task TDD 샘플 동기화(2026-07-27): `docs/sample/sample-plan-task.md`에 `RED → RED 확인 → GREEN → GREEN 확인 → REFACTOR`
|
||
작성 규칙과 read-only Task의 `TDD 예외 사유`·`대체 검증 방법` 형식을 추가하고 기존 샘플 Task 체크박스를 같은 형식으로 통일했다.
|
||
- 문서 보완 검증(2026-07-27): `REV-001`~`REV-008` 추적 횟수, Phase 2·3 Task/Gate·직렬 Goal 순서, 중복 Goal ID와
|
||
미교체 placeholder를 `rg`로 확인했고 `git diff --check`, `git diff --cached --check`가 통과했다.
|
||
`./gradlew tasks --all`은 exit code 0, `BUILD SUCCESSFUL in 660ms`를 확인했다. 문서 전용 변경이며 사용자가 명시적으로
|
||
제외했으므로 전체 `./gradlew test`는 실행하지 않았다.
|
||
- 리뷰 문서 경로 보완(2026-07-27): 여러 리뷰를 범위별 파일로 누적할 수 있도록 공통 저장 경로를
|
||
`docs/[날짜]_구현할내용한글/reviews/`로 정하고, Phase 2·3 리뷰 산출물과 샘플·가이드 경로를 동기화했다.
|
||
- 전체 회귀 실행 정책 보완(2026-07-27): focused/영향 범위 회귀를 우선하고 공통 경계·여러 Phase 영향 또는 targeted 결과만으로
|
||
영향 범위를 판단할 수 없는 경우에만 전체 회귀를 실행하도록 공통 가이드·샘플·`P7-T1`을 동기화했다. 생략 시 근거와 대체
|
||
검증 명령을 기록하도록 했다.
|
||
- Goal 실행형 계획 보완(2026-07-27): `docs/sample/sample-plan-task.md`와 PRD Feature B~F를 대조해 Phase 2·3에는 기존 완료
|
||
이력을 보존한 review/세부 보완/Gate Goal을 추가하고, Phase 4~6은 독립 검토 가능한 기능 단위 Task와 Gate로 분할했다.
|
||
Phase 7에는 targeted·조건부 전체 회귀, 계약·변경 범위와 최종 판정 Goal을 분리했다.
|
||
- 샘플 참조 가이드 보완(2026-07-27): `docs/agent-guides/작업절차.md`, `docs/agent-guides/문서유지보수.md`에
|
||
`docs/sample/{sample-prd.md,sample-plan-task.md,sample-review.md}`의 용도, Goal 완료·Gate·리뷰 누적 규칙을 반영했다.
|
||
- 문서 검증(2026-07-27): Goal/Task/Gate heading과 ID, placeholder·구 샘플 경로, 중복 Goal ID를 `rg`로 확인했고 중복·미교체
|
||
placeholder·구 샘플 경로 참조가 없음을 확인했다. `git diff --check`가 통과했다.
|
||
- 명령 유효성 검증(2026-07-27): sandbox 실행은 Gradle wrapper lock 권한으로 실패해 승인 범위에서
|
||
`./gradlew tasks --all`을 재실행했고 `BUILD SUCCESSFUL`을 확인했다.
|
||
- 계획 작성 단계: 코드 변경 없음.
|
||
- 문서 규칙 확인: `docs/agent-guides/작업절차.md`, `docs/agent-guides/문서유지보수.md`, `docs/agent-guides/테스트스타일.md`, `docs/sample/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 규칙을 반영했다.
|
||
- 문서 동기화 검증: `Read`로 `prd.md`와 `plan-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` 실행 결과 `AiCharacterAdminTargetResolver`와 `ChatCharacterRepository.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 --check`와 `git 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 test`는 `BUILD SUCCESSFUL`(4분 55초),
|
||
`./gradlew ktlintCheck --rerun-tasks`는 `BUILD 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/레거시 회귀: `TokenProviderTest`와 `aicharacter.*` 114개, 기존
|
||
`AdminAgentReadControllerSecurityTest`와 `AdminContentControllerSecurityTest` 11개를 함께 실행해 총 125개 모두 통과했고
|
||
`BUILD SUCCESSFUL`(42초)을 확인했다.
|
||
- Task 1.6 2차 리뷰 전체 회귀/lint: 최종 소스 상태에서 `./gradlew test`는 `BUILD SUCCESSFUL`(5분 4초),
|
||
`./gradlew ktlintCheck --rerun-tasks`는 `BUILD 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-preflight`의 `POST`/`PUT`/`PATCH`/`DELETE` 매핑을 추가하고,
|
||
해당 경로 preflight에서 `Access-Control-Allow-Origin`과 `Access-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-Origin`과 `Access-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 `ApiResponse`와
|
||
`Access-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-tasks`는 `BUILD 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-tasks`는 `BUILD 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초)을 확인했다.
|