Files
sodalive-backend-spring-boot/docs/20260611_AI캐릭터_크리에이터기능_최소연결/plan-task.md

17 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: 모든 ChatCharacterMember(role = CREATOR, memberKind = AI_CHARACTER)와 1:1로 연결해 로그인/DM을 제외한 기존 크리에이터 기능을 최소 변경으로 재사용한다.

Architecture: 기존 크리에이터 기능의 소유자는 계속 Member로 유지한다. ChatCharactercreatorMember를 단방향 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 마이그레이션 기반 추가

  • 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.HUMAN
      
      enum class MemberKind {
          HUMAN, AI_CHARACTER
      }
      
    • REFACTOR: MemberRoleMemberKind 의미가 섞이지 않도록 주석은 최소화하고, 정책 판단은 각 서비스 task에서 명시한다.
    • Verify:
      • Run: ./gradlew test --tests kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest
      • Expected: 기존 테스트 컴파일 및 통과.
  • 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 내 검증 쿼리 포함:
        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을 추가한다.
      @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에서 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를 아래 기준으로 구현한다.
      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 조회/활성 검증 직후 아래 정책을 추가한다.
      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를 차단한다.
      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.saveparticipantRepository.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.UserCreatorChatServiceTest
      • Expected: 기존 DM 테스트와 신규 차단 테스트 PASS.

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.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:
      ./gradlew ktlintCheck
      ./gradlew test
      
    • Expected: 두 명령 모두 PASS.
  • Task 4.3: 검증 기록 누적

    • Modify: docs/20260611_AI캐릭터_크리에이터기능_최소연결/plan-task.md
    • TDD 예외 사유: 문서 기록 task다.
    • 대체 검증 방법: 실행한 명령, 목적, 결과를 아래 검증 기록 섹션에 누적한다.
    • 기록 형식:
      - `./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.