21 KiB
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.creatorMember1: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.ktMemberKindenum과memberKind필드 추가.
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/chat/character/ChatCharacter.ktcreatorMember: Member단방향OneToOne추가.
- Modify:
src/main/kotlin/kr/co/vividnext/sodalive/chat/character/repository/ChatCharacterRepository.ktcreatorMember조회/검증 메서드 추가.
- 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이면 차단.
- DM 생성/메시지 발송 대상이
- 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 마이그레이션 기반 추가
-
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 필드를 추가한다. - 구현 기준:
@Enumerated(value = EnumType.STRING) var memberKind: MemberKind = MemberKind.HUMANenum class MemberKind { HUMAN, AI_CHARACTER } - REFACTOR:
MemberRole과MemberKind의미가 섞이지 않도록 주석은 최소화하고, 정책 판단은 각 서비스 task에서 명시한다. - Verify:
- Run:
./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest - Expected: 기존 테스트 컴파일 및 통과.
- Run:
- Modify:
-
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_memberunique indexfk_chat_character_creator_memberforeign 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'상태가 되도록 생성한다. member.email은 nullable이므로 저장 프로시저 대신 backfill 중 임시 식별자로 사용할 수 있다. 단,chat_character.creator_member_id매핑 후 임시 email 값은 반드시NULL로 되돌린다.- 운영 반영 후 문제에 대비해 FK/index/연결 데이터/컬럼 제거 순서의 롤백 방법을 SQL 문서에 함께 기록한다.
- Verify:
- 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이어야 한다.
- SQL 내 검증 쿼리 포함:
- Create:
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을 추가한다.@OneToOne(fetch = FetchType.LAZY) @JoinColumn(name = "creator_member_id", nullable = false, unique = true) var creatorMember: Member? = null - Repository 메서드 기준:
fun findByCreatorMemberId(creatorMemberId: Long): ChatCharacter? fun existsByCreatorMemberId(creatorMemberId: Long): Boolean - REFACTOR:
ChatCharacter에서Memberimport만 추가하고, 기존 캐릭터 필드/관계는 변경하지 않는다. - Verify:
- Run:
./gradlew test --tests kr.co.vividnext.sodalive.chat.character.service.ChatCharacterCreatorMemberServiceTest - Expected: 관계 접근 컴파일 및 테스트 통과.
- Run:
- Modify:
-
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: 아래 테스트를 먼저 작성한다.
shouldCreateAiCharacterMemberAndCopyDisplayFieldsshouldSyncAiCharacterMemberDisplayFieldsshouldNotOverwriteHumanCreatorDisplayFields
- 테스트 기대:
- 생성 시
role = CREATOR,memberKind = AI_CHARACTER,email = null,password = "" Member.nickname = ChatCharacter.nameMember.profileImage = ChatCharacter.imagePathMember.introduce = ChatCharacter.description- 연결된
creatorMember.memberKind = HUMAN이면 표시 정보 덮어쓰기 없음.
- 생성 시
- GREEN: 서비스 API를 아래 기준으로 구현한다.
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.
- Run:
- Create:
-
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.
- Run:
- Modify:
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 조회/활성 검증 직후 아래 정책을 추가한다.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.
- Run:
- Modify:
-
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를 차단한다.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.
- Run:
- Modify:
-
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: 아래 테스트를 추가한다.
shouldRejectCreateRoomWhenCreatorIsAiCharacterMembershouldRejectSendTextMessageWhenOpponentIsAiCharacterMembermemberRepository.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이면 차단한다.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.
- Run:
- Modify:
Phase 4: 회귀 검증 및 문서 정리
-
Task 4.1: 핵심 단위 테스트 실행
- Files: 변경 없음
- TDD 예외 사유: 구현 완료 후 회귀 검증 task다.
- 대체 검증 방법: 관련 단일 테스트를 모두 실행한다.
- Run:
./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.
-
Task 4.2: 정적 검증 및 전체 회귀
- Files: 변경 없음
- TDD 예외 사유: 전체 회귀 검증 task다.
- 대체 검증 방법: Gradle 테스트와 ktlint를 실행한다.
- Run:
./gradlew ktlintCheck ./gradlew test - Expected: 두 명령 모두 PASS.
-
Task 4.3: 검증 기록 누적
- Modify:
docs/20260611_AI캐릭터_크리에이터기능_최소연결/plan-task.md - TDD 예외 사유: 문서 기록 task다.
- 대체 검증 방법: 실행한 명령, 목적, 결과를 아래 검증 기록 섹션에 누적한다.
- 기록 형식:
- `./gradlew test --tests ...` - 목적: [무엇을 검증했는지] - 결과: PASS 또는 실패 내용
- Modify:
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.
- 목적: 리뷰 보완. AI 캐릭터용 Member가 참여한 기존 DM 방에서
-
./gradlew ktlintCheck- 목적: 리뷰 보완 후 ktlint 규칙 준수 확인.
- 결과:
BUILD SUCCESSFUL in 8s.
-
./gradlew test- 목적: 리뷰 보완 후 전체 회귀 테스트 확인.
- 결과:
BUILD SUCCESSFUL in 1m 15s.
-
rg -n "CREATE PROCEDURE|CURSOR|CALL backfill_chat_character_creator_member|LAST_INSERT_ID|Rollback|임시 식별자" docs/20260611_AI캐릭터_크리에이터기능_최소연결- 목적: 운영 DB 반영 SQL을 저장 프로시저 없는 단순 SQL로 변경했고, 임시 email 식별자 기준과 롤백 절차가 문서에 남았는지 확인.
- 결과: 저장 프로시저/커서/CALL/LAST_INSERT_ID 패턴은 미검출.
alter-existing-tables.sql에 임시 식별자 정리와 Rollback 절차가 존재함을 확인.
-
./gradlew tasks --all- 목적: 문서 변경 후 Gradle 명령 유효성 확인.
- 결과:
BUILD SUCCESSFUL in 942ms.