# 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 연결 - [x] **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: 관계 접근 컴파일 및 테스트 통과. - [x] **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. - [x] **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 차단 - [x] **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. - [x] **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. - [x] **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/UserCreatorChatServiceIntegrationTest.kt` - RED: 아래 테스트를 추가한다. - `shouldRejectCreateRoomWhenCreatorIsAiCharacterMember` - `shouldRejectSendTextMessageWhenOpponentIsAiCharacterMember` - `memberRepository.findById(creatorId)`는 `role = CREATOR`, `memberKind = AI_CHARACTER`인 Member를 반환한다. - `service.createOrGetRoom(user, creatorId)`는 예외를 던진다. - `roomRepository.save`와 `participantRepository.save`는 호출되지 않는다. - 기존 방에 AI 캐릭터용 Member가 참여한 상태에서 `sendTextMessage`는 예외를 던진다. - `messageRepository.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.UserCreatorChatServiceIntegrationTest --tests kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest` - Expected: 기존 DM 테스트와 신규 생성/발송 차단 테스트 PASS. --- ### Phase 4: 회귀 검증 및 문서 정리 - [x] **Task 4.1: 핵심 단위 테스트 실행** - Files: 변경 없음 - TDD 예외 사유: 구현 완료 후 회귀 검증 task다. - 대체 검증 방법: 관련 단일 테스트를 모두 실행한다. - Run: ```bash ./gradlew test \ --tests kr.co.vividnext.sodalive.chat.character.service.ChatCharacterCreatorMemberServiceTest \ --tests kr.co.vividnext.sodalive.chat.character.service.ChatCharacterCreatorMemberServiceIntegrationTest \ --tests kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest \ --tests kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceIntegrationTest \ --tests kr.co.vividnext.sodalive.creator.admin.member.CreatorAdminMemberServiceTest \ --tests kr.co.vividnext.sodalive.member.MemberServiceTest ``` - Expected: PASS. - [x] **Task 4.2: 정적 검증 및 전체 회귀** - Files: 변경 없음 - TDD 예외 사유: 전체 회귀 검증 task다. - 대체 검증 방법: Gradle 테스트와 ktlint를 실행한다. - Run: ```bash ./gradlew ktlintCheck ./gradlew test ``` - Expected: 두 명령 모두 PASS. - [x] **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`. - `./gradlew test --tests kr.co.vividnext.sodalive.chat.character.service.ChatCharacterCreatorMemberServiceTest` - 목적: Phase 2 RED 테스트가 신규 서비스/관계/repository/wiring 부재로 실패하는지 확인. - 결과: `compileTestKotlin`에서 `ChatCharacterCreatorMemberService`, `creatorMember`, `findByCreatorMemberId`, `existsByCreatorMemberId`, `creatorMemberService` 생성자 파라미터 unresolved reference로 실패. - `./gradlew test --tests kr.co.vividnext.sodalive.chat.character.service.ChatCharacterCreatorMemberServiceTest` - 목적: `ChatCharacter.creatorMember` 관계, repository 메서드, AI 캐릭터용 Member 생성/동기화, 캐릭터 생성/수정 wiring 검증. - 결과: `BUILD SUCCESSFUL in 11s`. - `./gradlew test --tests kr.co.vividnext.sodalive.admin.chat.character.AdminChatCharacterControllerTest` - 목적: 관리자 캐릭터 컨트롤러 생성자 변경 후 기존 성별 매핑 회귀 테스트 컴파일 및 통과 확인. - 결과: `BUILD SUCCESSFUL in 3s`. - `./gradlew ktlintCheck` - 목적: Phase 2 Kotlin production/test 변경의 ktlint 규칙 준수 확인. - 결과: `BUILD SUCCESSFUL in 14s`. - `./gradlew test --tests kr.co.vividnext.sodalive.member.MemberServiceTest --tests kr.co.vividnext.sodalive.creator.admin.member.CreatorAdminMemberServiceTest --tests kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest` - 목적: Phase 3 RED 검증. AI 캐릭터용 Member의 일반 로그인, 크리에이터 관리자 로그인, 유저-크리에이터 DM 방 생성이 기존 코드에서 차단되지 않음을 확인. - 결과: `CreatorAdminMemberServiceTest`, `MemberServiceTest`, `UserCreatorChatServiceTest`의 신규 차단 테스트 3건 실패. - `./gradlew test --tests kr.co.vividnext.sodalive.member.MemberServiceTest --tests kr.co.vividnext.sodalive.creator.admin.member.CreatorAdminMemberServiceTest --tests kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest` - 목적: Phase 3 GREEN 검증. 로그인/DM 차단 정책과 기존 DM 회귀 테스트 통과 확인. - 결과: `BUILD SUCCESSFUL in 10s`. - `./gradlew ktlintCheck` - 목적: Phase 3/4 Kotlin production/test 및 문서 변경 전 정적 규칙 준수 확인. - 결과: `BUILD SUCCESSFUL in 20s`. - `./gradlew test` - 목적: Phase 4 전체 회귀 테스트 확인. - 결과: `BUILD SUCCESSFUL in 1m 14s`. - `./gradlew test --tests kr.co.vividnext.sodalive.chat.character.service.ChatCharacterCreatorMemberServiceTest --tests kr.co.vividnext.sodalive.chat.character.service.ChatCharacterCreatorMemberServiceIntegrationTest --tests kr.co.vividnext.sodalive.member.MemberServiceTest --tests kr.co.vividnext.sodalive.creator.admin.member.CreatorAdminMemberServiceTest --tests kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceIntegrationTest --tests kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest` - 목적: 계획 진행 중 추가한 테스트의 mock 사용 적합성 재검토 후, 저장소/JPA 관계/로그인/DM 정책 검증을 Spring 컨텍스트 + H2 repository 기반 테스트로 전환했는지 확인. - 결과: `BUILD SUCCESSFUL in 34s`. - `./gradlew ktlintCheck` - 목적: 계획 관련 테스트 리팩터링 후 ktlint 규칙 준수 확인. - 결과: `BUILD SUCCESSFUL in 10s`. - `./gradlew test` - 목적: 계획 관련 테스트 리팩터링 후 전체 회귀 테스트 확인. - 결과: `BUILD SUCCESSFUL in 1m 20s`. - `./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceIntegrationTest --tests kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest` - 목적: 리뷰 보완. AI 캐릭터용 Member가 참여한 기존 DM 방에서 `sendTextMessage`도 `message.error.recipient_not_found`로 차단되고 메시지가 저장되지 않는지 확인. - 결과: `BUILD SUCCESSFUL in 31s`. - `./gradlew ktlintCheck` - 목적: 리뷰 보완 후 ktlint 규칙 준수 확인. - 결과: `BUILD SUCCESSFUL in 8s`. - `./gradlew test` - 목적: 리뷰 보완 후 전체 회귀 테스트 확인. - 결과: `BUILD SUCCESSFUL in 1m 15s`.