docs(aicharacter): 크리에이터 연결 계획을 추가한다
This commit is contained in:
261
docs/20260611_AI캐릭터_크리에이터기능_최소연결/plan-task.md
Normal file
261
docs/20260611_AI캐릭터_크리에이터기능_최소연결/plan-task.md
Normal file
@@ -0,0 +1,261 @@
|
||||
# AI 캐릭터 크리에이터 기능 최소 연결 Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** 모든 `ChatCharacter`를 `Member(role = CREATOR, memberKind = AI_CHARACTER)`와 1:1로 연결해 로그인/DM을 제외한 기존 크리에이터 기능을 최소 변경으로 재사용한다.
|
||||
|
||||
**Architecture:** 기존 크리에이터 기능의 소유자는 계속 `Member`로 유지한다. `ChatCharacter`는 `creatorMember`를 단방향 `OneToOne`으로 가지며, AI 캐릭터용 Member의 표시 정보는 `ChatCharacter` 값에서 스냅샷으로 동기화한다. 사람/AI 주체 구분은 `Member.memberKind`로 처리하고, 로그인/DM만 명시적으로 차단한다.
|
||||
|
||||
**Tech Stack:** Kotlin, Spring Boot 2.7.14, Java 17, Spring Security, JPA/Hibernate, QueryDSL, MySQL, Gradle Wrapper, JUnit5, Mockito.
|
||||
|
||||
---
|
||||
|
||||
## Scope
|
||||
- 포함: `MemberKind` 추가, `ChatCharacter.creatorMember` 1:1 관계 추가, MySQL DDL/backfill SQL, AI 캐릭터용 Member 생성/표시 정보 동기화, 로그인 차단, DM 차단.
|
||||
- 제외: `creator_identity` 도입, `ChatCharacter` 독립 소유자화, 검색 카테고리 개편, `Member:ChatCharacter = 1:N`, AI 캐릭터용 콘텐츠/라이브/커뮤니티 대리 생성 API 설계.
|
||||
|
||||
## File Map
|
||||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt`
|
||||
- `MemberKind` enum과 `memberKind` 필드 추가.
|
||||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/ChatCharacter.kt`
|
||||
- `creatorMember: Member` 단방향 `OneToOne` 추가.
|
||||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/repository/ChatCharacterRepository.kt`
|
||||
- `creatorMember` 조회/검증 메서드 추가.
|
||||
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterCreatorMemberService.kt`
|
||||
- AI 캐릭터용 Member 생성 및 표시 정보 동기화 책임.
|
||||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterService.kt`
|
||||
- 캐릭터 생성/수정 시 `ChatCharacterCreatorMemberService` 호출.
|
||||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt`
|
||||
- 일반 로그인에서 `memberKind = AI_CHARACTER` 차단.
|
||||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/member/CreatorAdminMemberService.kt`
|
||||
- 크리에이터 관리자 로그인에서 `memberKind = AI_CHARACTER` 차단.
|
||||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/service/UserCreatorChatService.kt`
|
||||
- DM 생성/메시지 발송 대상이 `memberKind = AI_CHARACTER`이면 차단.
|
||||
- Create: `docs/20260611_AI캐릭터_크리에이터기능_최소연결/alter-existing-tables.sql`
|
||||
- 운영 DB 반영용 MySQL DDL/backfill/검증 SQL.
|
||||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterCreatorMemberServiceTest.kt`
|
||||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/UserCreatorChatServiceTest.kt`
|
||||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/creator/admin/member/CreatorAdminMemberServiceTest.kt`
|
||||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/member/MemberServiceTest.kt`
|
||||
|
||||
---
|
||||
|
||||
### Phase 1: MemberKind 및 DB 마이그레이션 기반 추가
|
||||
|
||||
- [x] **Task 1.1: `MemberKind` 필드 추가**
|
||||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt`
|
||||
- Test: 기존 컴파일 회귀
|
||||
- RED: `Member.memberKind`를 참조하는 최소 컴파일 테스트 또는 이후 task 테스트를 먼저 작성하면 현재 컴파일이 실패해야 한다.
|
||||
- GREEN: `Member` 생성자에 기본값 `MemberKind.HUMAN`을 가진 non-null 필드를 추가한다.
|
||||
- 구현 기준:
|
||||
```kotlin
|
||||
@Enumerated(value = EnumType.STRING)
|
||||
var memberKind: MemberKind = MemberKind.HUMAN
|
||||
```
|
||||
```kotlin
|
||||
enum class MemberKind {
|
||||
HUMAN, AI_CHARACTER
|
||||
}
|
||||
```
|
||||
- REFACTOR: `MemberRole`과 `MemberKind` 의미가 섞이지 않도록 주석은 최소화하고, 정책 판단은 각 서비스 task에서 명시한다.
|
||||
- Verify:
|
||||
- Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest`
|
||||
- Expected: 기존 테스트 컴파일 및 통과.
|
||||
|
||||
- [x] **Task 1.2: 운영 DB DDL/backfill SQL 작성**
|
||||
- Create: `docs/20260611_AI캐릭터_크리에이터기능_최소연결/alter-existing-tables.sql`
|
||||
- TDD 예외 사유: 운영 DDL 문서 작성은 단위 테스트 대상이 아니다.
|
||||
- 대체 검증 방법: SQL 문법과 PRD 요구사항을 수동 점검하고 검증 쿼리를 포함한다.
|
||||
- SQL 작성 기준:
|
||||
- `member.member_kind varchar(30) not null default 'HUMAN' comment 'Member 주체 종류: HUMAN, AI_CHARACTER'`
|
||||
- `chat_character.creator_member_id bigint null comment '크리에이터 기능 주체 Member ID'`
|
||||
- `uk_chat_character_creator_member` unique index
|
||||
- `fk_chat_character_creator_member` foreign key
|
||||
- 기존 모든 `chat_character`별 AI 캐릭터용 Member 생성
|
||||
- 생성된 Member를 `chat_character.creator_member_id`에 연결
|
||||
- 검증 후 `chat_character.creator_member_id not null` 전환
|
||||
- SQL backfill은 `email = null`, `password = ''`, `role = 'CREATOR'`, `member_kind = 'AI_CHARACTER'`로 생성한다.
|
||||
- MySQL에서 insert된 Member ID를 안전하게 매핑하기 위해 저장 프로시저 또는 임시 매핑 테이블을 사용한다. `email`을 임시 식별자로 사용하지 않는다.
|
||||
- Verify:
|
||||
- SQL 내 검증 쿼리 포함:
|
||||
```sql
|
||||
select count(*) as invalid_ai_character_member_count
|
||||
from member
|
||||
where member_kind = 'AI_CHARACTER' and role <> 'CREATOR';
|
||||
|
||||
select count(*) as missing_creator_member_count
|
||||
from chat_character
|
||||
where creator_member_id is null;
|
||||
```
|
||||
- Expected: 운영 반영 전 두 검증 쿼리 결과가 모두 0이어야 한다.
|
||||
|
||||
---
|
||||
|
||||
### Phase 2: ChatCharacter와 AI 캐릭터용 Member 연결
|
||||
|
||||
- [ ] **Task 2.1: `ChatCharacter.creatorMember` 관계 추가**
|
||||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/ChatCharacter.kt`
|
||||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/repository/ChatCharacterRepository.kt`
|
||||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterCreatorMemberServiceTest.kt`
|
||||
- RED: 테스트에서 `ChatCharacter.creatorMember`에 접근하거나 `findByCreatorMemberId`를 호출하게 작성해 컴파일 실패를 확인한다.
|
||||
- GREEN: `ChatCharacter`에 단방향 `OneToOne`을 추가한다.
|
||||
```kotlin
|
||||
@OneToOne(fetch = FetchType.LAZY)
|
||||
@JoinColumn(name = "creator_member_id", nullable = false, unique = true)
|
||||
var creatorMember: Member? = null
|
||||
```
|
||||
- Repository 메서드 기준:
|
||||
```kotlin
|
||||
fun findByCreatorMemberId(creatorMemberId: Long): ChatCharacter?
|
||||
fun existsByCreatorMemberId(creatorMemberId: Long): Boolean
|
||||
```
|
||||
- REFACTOR: `ChatCharacter`에서 `Member` import만 추가하고, 기존 캐릭터 필드/관계는 변경하지 않는다.
|
||||
- Verify:
|
||||
- Run: `./gradlew test --tests kr.co.vividnext.sodalive.chat.character.service.ChatCharacterCreatorMemberServiceTest`
|
||||
- Expected: 관계 접근 컴파일 및 테스트 통과.
|
||||
|
||||
- [ ] **Task 2.2: AI 캐릭터용 Member 생성/표시 정보 동기화 서비스 추가**
|
||||
- Create: `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterCreatorMemberService.kt`
|
||||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterCreatorMemberServiceTest.kt`
|
||||
- RED: 아래 테스트를 먼저 작성한다.
|
||||
- `shouldCreateAiCharacterMemberAndCopyDisplayFields`
|
||||
- `shouldSyncAiCharacterMemberDisplayFields`
|
||||
- `shouldNotOverwriteHumanCreatorDisplayFields`
|
||||
- 테스트 기대:
|
||||
- 생성 시 `role = CREATOR`, `memberKind = AI_CHARACTER`, `email = null`, `password = ""`
|
||||
- `Member.nickname = ChatCharacter.name`
|
||||
- `Member.profileImage = ChatCharacter.imagePath`
|
||||
- `Member.introduce = ChatCharacter.description`
|
||||
- 연결된 `creatorMember.memberKind = HUMAN`이면 표시 정보 덮어쓰기 없음.
|
||||
- GREEN: 서비스 API를 아래 기준으로 구현한다.
|
||||
```kotlin
|
||||
fun ensureAiCharacterCreatorMember(chatCharacter: ChatCharacter): Member
|
||||
fun syncAiCharacterCreatorMemberDisplayFields(chatCharacter: ChatCharacter)
|
||||
```
|
||||
- 구현 정책:
|
||||
- `chatCharacter.creatorMember == null`이면 AI 캐릭터용 Member를 생성하고 연결한다.
|
||||
- `chatCharacter.creatorMember.memberKind == AI_CHARACTER`이면 표시 정보를 동기화한다.
|
||||
- `chatCharacter.creatorMember.memberKind == HUMAN`이면 사람 크리에이터 프로필을 덮어쓰지 않는다.
|
||||
- REFACTOR: 동기화 로직은 `ChatCharacterService`에 직접 흩뿌리지 않고 이 서비스로 모은다.
|
||||
- Verify:
|
||||
- Run: `./gradlew test --tests kr.co.vividnext.sodalive.chat.character.service.ChatCharacterCreatorMemberServiceTest`
|
||||
- Expected: PASS.
|
||||
|
||||
- [ ] **Task 2.3: 캐릭터 생성/수정 흐름에 AI 캐릭터용 Member 연결**
|
||||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterService.kt`
|
||||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/admin/chat/character/AdminChatCharacterController.kt`
|
||||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterCreatorMemberServiceTest.kt`
|
||||
- RED: 캐릭터 생성 후 `creatorMember`가 생성되고, 이미지 저장 후 `Member.profileImage`가 갱신되는 테스트를 작성한다.
|
||||
- GREEN:
|
||||
- `createChatCharacter` 또는 `createChatCharacterWithDetails` 저장 후 `ensureAiCharacterCreatorMember`를 호출한다.
|
||||
- 관리자 등록 컨트롤러에서 이미지 저장 후 `chatCharacter.imagePath`를 설정하고 저장한 뒤 `syncAiCharacterCreatorMemberDisplayFields`를 호출한다.
|
||||
- `updateChatCharacterWithDetails`에서 이름/설명/이미지 변경 후 `syncAiCharacterCreatorMemberDisplayFields`를 호출한다.
|
||||
- REFACTOR: 외부 API 호출, S3 업로드, 원작 연결, 언어 감지 이벤트 흐름은 기존 순서를 유지한다.
|
||||
- Verify:
|
||||
- Run: `./gradlew test --tests kr.co.vividnext.sodalive.chat.character.service.ChatCharacterCreatorMemberServiceTest`
|
||||
- Expected: PASS.
|
||||
|
||||
---
|
||||
|
||||
### Phase 3: 로그인 및 DM 차단
|
||||
|
||||
- [ ] **Task 3.1: 일반 로그인에서 AI 캐릭터용 Member 차단**
|
||||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt`
|
||||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/member/MemberServiceTest.kt`
|
||||
- RED: `memberKind = AI_CHARACTER`인 Member가 일반 로그인 요청 시 인증 매니저 호출 전에 예외가 발생하는 테스트를 작성한다.
|
||||
- GREEN: `MemberService.login(...)`의 Member 조회/활성 검증 직후 아래 정책을 추가한다.
|
||||
```kotlin
|
||||
if (member.memberKind == MemberKind.AI_CHARACTER) {
|
||||
throw SodaException(messageKey = "common.error.bad_credentials")
|
||||
}
|
||||
```
|
||||
- REFACTOR: 기존 `provider`, `isCreator`, `isAdmin` 검증 순서는 불필요하게 바꾸지 않는다.
|
||||
- Verify:
|
||||
- Run: `./gradlew test --tests kr.co.vividnext.sodalive.member.MemberServiceTest`
|
||||
- Expected: AI 캐릭터 로그인 차단 테스트 PASS.
|
||||
|
||||
- [ ] **Task 3.2: 크리에이터 관리자 로그인에서 AI 캐릭터용 Member 차단**
|
||||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/member/CreatorAdminMemberService.kt`
|
||||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/creator/admin/member/CreatorAdminMemberServiceTest.kt`
|
||||
- RED: `memberKind = AI_CHARACTER`, `role = CREATOR`인 Member가 크리에이터 관리자 로그인 요청 시 `common.error.bad_credentials` 예외가 발생하는 테스트를 작성한다.
|
||||
- GREEN: `CreatorAdminMemberService.login(email, password)`에서 role 검증 전 또는 직후 AI 캐릭터용 Member를 차단한다.
|
||||
```kotlin
|
||||
if (member.memberKind == MemberKind.AI_CHARACTER) {
|
||||
throw SodaException(messageKey = "common.error.bad_credentials")
|
||||
}
|
||||
```
|
||||
- REFACTOR: `AGENT` 로그인 허용 정책은 변경하지 않는다.
|
||||
- Verify:
|
||||
- Run: `./gradlew test --tests kr.co.vividnext.sodalive.creator.admin.member.CreatorAdminMemberServiceTest`
|
||||
- Expected: PASS.
|
||||
|
||||
- [ ] **Task 3.3: 유저-크리에이터 DM에서 AI 캐릭터용 Member 차단**
|
||||
- Modify: `src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/service/UserCreatorChatService.kt`
|
||||
- Test: `src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/UserCreatorChatServiceTest.kt`
|
||||
- RED: `shouldRejectCreateRoomWhenCreatorIsAiCharacterMember` 테스트를 추가한다.
|
||||
- `memberRepository.findById(creatorId)`는 `role = CREATOR`, `memberKind = AI_CHARACTER`인 Member를 반환한다.
|
||||
- `service.createOrGetRoom(user, creatorId)`는 예외를 던진다.
|
||||
- `roomRepository.save`와 `participantRepository.save`는 호출되지 않는다.
|
||||
- GREEN: `validateRecipient` 또는 `createOrGetRoom`에서 recipient가 AI 캐릭터용 Member이면 차단한다.
|
||||
```kotlin
|
||||
if (recipient.memberKind == MemberKind.AI_CHARACTER) {
|
||||
throw SodaException(messageKey = "message.error.recipient_not_found")
|
||||
}
|
||||
```
|
||||
- REFACTOR: 기존 비활성/본인/차단 검증 메시지와 우선순위를 불필요하게 바꾸지 않는다.
|
||||
- Verify:
|
||||
- Run: `./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest`
|
||||
- Expected: 기존 DM 테스트와 신규 차단 테스트 PASS.
|
||||
|
||||
---
|
||||
|
||||
### Phase 4: 회귀 검증 및 문서 정리
|
||||
|
||||
- [ ] **Task 4.1: 핵심 단위 테스트 실행**
|
||||
- Files: 변경 없음
|
||||
- TDD 예외 사유: 구현 완료 후 회귀 검증 task다.
|
||||
- 대체 검증 방법: 관련 단일 테스트를 모두 실행한다.
|
||||
- Run:
|
||||
```bash
|
||||
./gradlew test \
|
||||
--tests kr.co.vividnext.sodalive.chat.character.service.ChatCharacterCreatorMemberServiceTest \
|
||||
--tests kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest \
|
||||
--tests kr.co.vividnext.sodalive.creator.admin.member.CreatorAdminMemberServiceTest \
|
||||
--tests kr.co.vividnext.sodalive.member.MemberServiceTest
|
||||
```
|
||||
- Expected: PASS.
|
||||
|
||||
- [ ] **Task 4.2: 정적 검증 및 전체 회귀**
|
||||
- Files: 변경 없음
|
||||
- TDD 예외 사유: 전체 회귀 검증 task다.
|
||||
- 대체 검증 방법: Gradle 테스트와 ktlint를 실행한다.
|
||||
- Run:
|
||||
```bash
|
||||
./gradlew ktlintCheck
|
||||
./gradlew test
|
||||
```
|
||||
- Expected: 두 명령 모두 PASS.
|
||||
|
||||
- [ ] **Task 4.3: 검증 기록 누적**
|
||||
- Modify: `docs/20260611_AI캐릭터_크리에이터기능_최소연결/plan-task.md`
|
||||
- TDD 예외 사유: 문서 기록 task다.
|
||||
- 대체 검증 방법: 실행한 명령, 목적, 결과를 아래 검증 기록 섹션에 누적한다.
|
||||
- 기록 형식:
|
||||
```markdown
|
||||
- `./gradlew test --tests ...`
|
||||
- 목적: [무엇을 검증했는지]
|
||||
- 결과: PASS 또는 실패 내용
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Verification Log
|
||||
- `./gradlew tasks --all`
|
||||
- 목적: 문서 변경 후 Gradle 명령 유효성 확인.
|
||||
- 결과: 최초 실행은 sandbox가 `/Users/.../gradle-8.1.1-bin.zip.lck` 파일에 접근하지 못해 실패. 권한 승인 후 재실행하여 `BUILD SUCCESSFUL in 13s`.
|
||||
- `./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest`
|
||||
- 목적: `Member.memberKind` 추가 후 Phase 1 계획에 명시된 기존 DM 테스트 컴파일 및 회귀 확인.
|
||||
- 결과: `BUILD SUCCESSFUL in 2m 3s`.
|
||||
- `./gradlew tasks --all`
|
||||
- 목적: Phase 1 문서 및 운영 DB 반영용 SQL 추가 후 Gradle 명령 유효성 재확인.
|
||||
- 결과: `BUILD SUCCESSFUL in 8s`.
|
||||
Reference in New Issue
Block a user