Files

59 KiB

P3 이름 전달 경로 인벤토리와 GREEN 검증 인계

COR022 최신 범위

  • 레거시 MessageService/Repository는 이번이름변경을 HEAD로 되돌리고 MessageListRow 및 MessageServiceLocalizedNameTest3건을 제거한다. 목록6개와차단오류의이름치환·이벤트nameArgumentMemberIds2곳만 원래형태로돌아가며 API/발송/보관/삭제업무는유지한다.
  • 공통FCM/v2 DM/WS는유지한다. 레거시발송 본문args는원본이지만 기존senderMemberId로 공통알림senderNicknameSnapshot이현지화될수있는경계를그대로둔다. 아래T-MESSAGE·JOIN목록은이전이력이며최신검증은 plan-task.md MESSAGE-NAME-TRIM-T1을 따른다.
  • 적용완료: 두운영파일HEAD일치·전용2파일삭제, 실제HTTP23요청/발송이벤트2건 및전체2267건/lint통과를확인했다. 임시검증소스·클래스·서버·Redis는정리했고영구테스트3건만제거됐다.

상태와 경계

  • 기준: prd.md NAME002/003/005/006, COR012, plan-task.md P3-T1/P3-T2. 이 파일은 부모 에이전트의 실행 인계이며 원 PRD/계획은 수정하지 않았다.
  • 1차 인계에서는 대표 RED 테스트 4개 파일과 이 인벤토리만 작성했다. 이어진 부모의 GREEN 위임으로 아래 소유 서버 경로를 구현하고 실제 fixture 회귀를 보강했다. core Member/Lang/creatorname 모듈, 설정 파일, DDL, 외부 저장소는 수정하지 않았다. 커밋/푸시/Gradle 실행은 하지 않았다.
  • 부모의 /var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/creatorname-consumer-red.log는 104개 중 35개 실패를 기록한다. FCM/WS/message/live의 이름 및 handshake assertion RED를 읽어 확인했다. live 상세 1건은 아래 fixture 보정 대상이다. 이 작업의 GREEN 실행 결과는 아직 없으며 부모가 직렬 검증한다.
  • 아래 1차 조사/설계 표는 경로 추적 이력이다. 최종 구현 상태와 변경 파일 및 실행 대기는 문서 끝의 GREEN 구현 상태가 우선한다. 테이블 부재/컴파일/컨텍스트 오류를 RED 성공으로 간주하지 않는다.
  • 기반 계약: kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService.findLocalizedNames(Collection<Long>, Lang): Map<Long, String>. JA + 활성 + 실제 Member.role=CREATOR만 반환하고 다른 언어/미등록/비활성/USER는 원본 fallback한다. HTTP 경계 외 LangContext 접근, 행별 조회, 전역 JSON 변환은 금지한다.
  • 테스트는 MemberRepository.saveAndFlush 후 승인된 creator_localized_name 테이블에 JDBC로 실제 JA 행을 넣는다. 이름 resolver/repository/서비스는 mock하지 않는다. USER alias 행은 read-side role 필터를 검증하기 위한 의도적인 fixture이며 관리 API 허용 사례가 아니다.
  • 새 통합 테스트의 mock은 외부 전송 경계인 FcmService, S3Uploader만 쓴다. 기존 Mockito 단위 테스트 및 Redis-only 컨텍스트는 새 필수 생성자 의존성을 명시적으로 제공하도록 조정했다. Redis는 각 클래스의 EmbeddedRedisInitializer opt-in으로 제한한다. 테스트 JWT는 공개된 테스트용 64-byte secret을 클래스 속성으로 지정한다. 라이브 토큰은 더미 자격증명으로 로컬 서명만 수행하며 실제 RTC 연결은 하지 않는다.

1차 테스트와 예상 RED 이력

모든 경로는 저장소 루트 기준이다. T-FCM/T-WS/T-LIVE/T-MESSAGE는 아래 정확한 테스트 파일을 뜻한다.

식별자 테스트 파일 실제 실행 경로와 단언 기존 동작의 예상 RED
T-FCM src/test/kotlin/kr/co/vividnext/sodalive/fcm/FcmSendListenerLocalizedNameTest.kt 실제 UserCreatorChatService.sendTextMessageByWebSocket가 발행한 DM 이벤트, 실제 LiveRoomService.cancelLive가 발행한 취소 이벤트를 @RecordApplicationEvents로 수집. 실제 listener target을 동일 테스트 트랜잭션에서 호출하고 outbound 경계만 관찰. JA/KO 토큰 그룹별 title/body/새 notification snapshot, 255 code point 별칭 저장·재조회, 원본 이벤트 args/title 불변. 이름과 같은 creator.*$ 방 제목을 본문 인자로 유지. 이름 변경 뒤 새 알림을 생산해도 과거 저장 행이 불변인지 재조회 JA DM 본문과 snapshot이 dm-creator, JA 라이브 제목과 snapshot이 creator.*$로 남음. KO/문구 번역/과거 행 보존은 통과해야 함
T-WS src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatLocalizedNameIntegrationTest.kt 실제 JWT/Redis 인증으로 handshake JA/KO/EN/미지원/누락 헤더 속성. RANDOM_PORT + StandardWebSocketClient로 localhost 연결, JOIN 후 실제 DB 저장과 Redis pubsub를 거쳐 같은 회원의 KO/JA 두 세션 비교. 별도 테스트로 JA 발신자의 SEND_ACK와 KO 수신 MESSAGE 비교. 실제 HTTP open/messages 언어별 이름도 확인 handshake의 attributes["lang"] 없음. JA MESSAGE/ACK/open/messages 이름이 original-creator로 남음. ID/body/mine/requestId는 보존되어야 함
T-LIVE src/test/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomLocalizedNameIntegrationTest.kt 실제 JPA/Redis membership을 사용한 getRoomInfo, getRoomDetail, getUserProfile. speaker/manager CREATOR 별칭, USER/비활성 fallback, membership role 보존. donation/roulette 메시지의 UUID/본문/secret 필터, JA→KO 응답 독립성과 공유 Redis 원본 불변. 최근 종료 라이브 KO→JA 캐시 읽기 JA host/manager/현재 후원·룰렛 표시 이름 및 최근 종료 라이브가 원본으로 남음. KO/USER/비활성/secret 필터/Redis 원본 단언은 통과해야 함
T-MESSAGE src/test/kotlin/kr/co/vividnext/sodalive/message/MessageServiceLocalizedNameTest.kt 실제 Message sender/recipient 관계 저장 후 문자/음성 sent/received/keep 6개 읽기. CREATOR 발신자/수신자 양쪽, USER 이름 보존, JA/KO/EN, 원본 본문/ID/voice URL 보존 JA senderNickname/recipientNickname가 원본으로 남음

1차 작성 수는 FCM 3개, WS 8개 실행 사례(언어 parameterized 4개 포함), live 4개, legacy message 2개로 총 17개였다. GREEN 위임 후 현재 수는 FCM 6개, WS 9개, live 8개, legacy message 2개로 총 25개다. 추가 사례는 실제 오디오 답글 생산자의 title owner/actor 분리(CREATOR/USER), 실제 기프트 자유 문구, 음성 REST/WS 언어 독립, 중립 feed/current fallback, 예약/방문/추천/후원/하트 현재 이름이다. 전체 P3 외부 수용 기준의 완료를 뜻하지 않는다.

부모 전용 실행 명령(이번 작업에서 미실행):

./gradlew test --tests 'kr.co.vividnext.sodalive.fcm.FcmSendListenerLocalizedNameTest'
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatLocalizedNameIntegrationTest'
./gradlew test --tests 'kr.co.vividnext.sodalive.live.room.LiveRoomLocalizedNameIntegrationTest'
./gradlew test --tests 'kr.co.vividnext.sodalive.message.MessageServiceLocalizedNameTest'

테스트 listener 호출은 @Async/REQUIRES_NEW proxy를 의도적으로 우회한다. rollback 예정인 fixture를 다른 트랜잭션에서 읽는 오류 없이 실제 언어 그룹화/문구 렌더링/DB 저장을 검증한다. async after-commit 스케줄링 자체는 이 테스트의 검증 대상이 아니다. 반면 WS 테스트는 fixture를 commit한 뒤 실제 네트워크/Redis/handler/service/broker 전체를 사용한다. 기존 localhost StandardWebSocketClient 선례는 검색되지 않았으며 기존 UserCreatorChatWebSocketHandshakeIntegrationTest의 endpoint/인증 계약과 UserCreatorChatRedisIntegrationTest의 실제 pubsub 선례를 결합했다.

1차 FCM 경계와 메타데이터 설계

기존 구현은 src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt 한 파일의 FcmEvent/FcmSendListener다. 별도의 FcmSendEvent/FcmSendListEvent 타입은 없다. 1차 조사 시 sendPush는 PushTokenInfo.languageCode로 그룹화하고 Lang.fromAcceptLanguage를 적용하지만 원본 args와 title을 그대로 사용했다. titleKey와 messageKey는 같은 args 배열을 사용한다. 이제 아래 명시 metadata로 그룹별 새 렌더링을 수행한다.

GREEN에서 부모와 호출부 소유자가 합의하여 적용한 명시 정보:

  • 기존 senderMemberId는 알림 행의 발신자 snapshot 주체로 유지한다. 이미 이름을 전달하는 모든 호출부에 있다. creatorId, 수신자 ID, resource ID를 sender ID로 추정하지 않는다.
  • 확정 필드 titleMemberId: Long? = null: raw title 전체가 회원 이름인 경우 그 회원 ID. 보통 senderMemberId와 같지만 오디오 답글은 다르다.
  • 확정 필드 nameArgumentMemberIds: Map<Int, Long> = emptyMap(): 0-based args 위치→실제 이름 소유 회원 ID. 현재 대상은 legacy 문자/음성, v2 DM/voice의 [0]뿐이다. 문자열 종류/값 일치/정규식/messageKey 추론으로 이름을 찾지 않는다.
  • language group마다 sender/title/arg ID의 합집합을 batch 조회하고 별도 args/title/body/senderNicknameSnapshot을 만든다. 원본 이벤트/공유 DTO/저장한 과거 알림을 mutate하지 않는다. 실제 생산자에서 발행한 이벤트를 테스트하므로 metadata가 추가되면 테스트 fixture에 가짜 필드를 붙일 필요 없이 실제 호출부 수정이 검증된다.
  • PushNotificationService.saveNotification은 language group에서 렌더링한 sender snapshot을 저장하도록 연결한다. resolveSenderSnapshot의 기존 명시 snapshot/profile/fallback 의미를 보존한다. PushNotificationList에는 senderMemberId가 없으므로 과거 목록 조회 때 현재 회원과 다시 연결하지 않는다.

전 FcmEvent 발행 지점 22곳

아래 경로의 공통 prefix는 src/main/kotlin/kr/co/vividnext/sodalive/다. 파일 경로와 메서드를 모두 명시했다. 행 번호는 1차 조사 기준이며 이후 수정으로 이동할 수 있다. titleId/argIds는 위 확정 metadata 값이다. [외부 소유]는 이 작업의 소유 범위 밖이지만 서버 내부 파일이라는 뜻이다. 각 행의 최종 처리 상태는 끝의 상태 표에 연결했다.

# 생산 파일:행 / 메서드 이벤트·현재 이름 위치·이름 아닌 값 GREEN titleId / argIds / sender snapshot 주체 소유/검증
1 fcm/FcmController.kt:23 send 개별 INDIVIDUAL; 운영자 자유 title/message, 이름 슬롯 없음 null / empty / 기존 없음 P3; 임의 텍스트 불변
2 fcm/FcmController.kt:34 send 전체 ALL; 운영자 자유 title/message, 이름 슬롯 없음 null / empty / 기존 없음 P3; 임의 텍스트 불변
3 message/MessageService.kt:74 sendTextMessage SEND_MESSAGE; titleKey=message.fcm.title, args[0]=sender.nickname, messageKey=text_received null / {0: sender.id} / sender.id P3; T-MESSAGE + 기존 FCM 문구
4 message/MessageService.kt:149 sendVoiceMessage SEND_MESSAGE; args[0]=sender.nickname, messageKey=voice_received null / {0: sender.id} / sender.id P3; T-MESSAGE + 기존 FCM 문구
5 v2/usercreatorchat/service/UserCreatorChatService.kt:235 publishMessagePush INDIVIDUAL; text/voice 공통 발행. args[0]=sender.nickname, titleKey=dm_title 또는 title, room.id는 deepLinkId null / {0: sender.id} / sender.id P3; T-FCM 실제 DM 생산자
6 live/room/LiveRoomService.kt:503 createLiveRoom CREATE_LIVE; raw title=createdRoom.member.nickname; args[0]=createdRoom.title(방 제목) createdRoom.member.id / empty / 기존 같은 ID P3; T-FCM 취소와 같은 title 패턴
7 live/room/LiveRoomService.kt:685 startLive START_LIVE; raw title=room.member.nickname; args[0]=room.title(방 제목) room.member.id / empty / 기존 같은 ID P3; 기존 live regression
8 live/room/LiveRoomService.kt:760 cancelLive CANCEL_LIVE; raw title=room.member.nickname; args[0]=room.title, 예약 취소 전 수집한 pushTokens 사용 room.member.id / empty / 기존 같은 ID P3; T-FCM 실제 취소 생산자
9 explorer/ExplorerService.kt:683 saveNotice CHANGE_NOTICE; title=member.nickname, 고정 notice messageKey, args 없음 member.id / empty / 기존 member.id [외부 소유: legacy worker]
10 explorer/profile/creatorCommunity/CreatorCommunityService.kt:139 createCommunityPost CHANGE_NOTICE; title=member.nickname, new_post messageKey member.id / empty / 기존 member.id [외부 소유: legacy worker]
11 explorer/profile/creatorCommunity/CreatorCommunityService.kt:532 createCommunityPostComment INDIVIDUAL; title=댓글 작성 member.nickname. 수신 creatorId와 작성자 다름 member.id / empty / 기존 member.id [외부 소유: legacy worker]
12 content/comment/AudioContentCommentService.kt:81 registerComment CREATE_CONTENT_COMMENT; title은 답글이면 parent.member.nickname, 일반 댓글이면 member.nickname. args[0]=audioContent.title parent?.member?.id ?: member.id / empty / senderMemberId=member.id 유지 [외부 소유: legacy worker]; 부모 댓글 작성자와 행위자 각각 CREATOR/USER 조합 회귀 필요
13 content/AudioContentService.kt:492 uploadComplete 완료 안내 INDIVIDUAL; titleKey=upload_complete_title, message=audioContent.title. 본문은 이름 아님 null / empty / 기존 audioContent.member.id [외부 소유: legacy worker]; 생산자 추가 metadata 불필요, 새 sender snapshot만 언어별
14 content/AudioContentService.kt:511 uploadComplete 즉시 공개 UPLOAD_CONTENT; title=audioContent.member.nickname; args[0]=콘텐츠 제목 audioContent.member.id / empty / 기존 같은 ID [외부 소유: legacy worker]
15 content/AudioContentService.kt:541 releaseContent UPLOAD_CONTENT; title=audioContent.member.nickname; args[0]=콘텐츠 제목 audioContent.member.id / empty / 기존 같은 ID [외부 소유: legacy worker]
16 admin/live/AdminLiveService.kt:341 cancelNotStartedLive4hoursPassed CANCEL_LIVE; title=room.member.nickname; args[0]=방 제목 room.member.id / empty / 기존 같은 ID 부모의 후속 권한 부여로 titleMemberId 한 줄만 이 작업에서 연결
17 can/charge/event/ChargeEventService.kt:80 applyOtherEvent INDIVIDUAL SYSTEM; title=chargeEvent.title(이벤트 제목), args[0]=additionalCan(숫자) null / empty / 없음 [외부 소유: legacy]; 수정 불필요
18 can/charge/event/ChargeEventService.kt:104 applyFirstChargeEvent INDIVIDUAL SYSTEM; 고정 titleKey, args[0]=additionalCan null / empty / 없음 [외부 소유: legacy]; 수정 불필요
19 v2/can/charge/event/ChargeEventJobService.kt:187 publishPaidNotification FIRST_CHARGE INDIVIDUAL SYSTEM; 고정 titleKey, args[0]=job.additionalCan null / empty / 없음 [외부 소유: v2]; 수정 불필요
20 v2/can/charge/event/ChargeEventJobService.kt:200 publishPaidNotification 기타 INDIVIDUAL SYSTEM; title=job.methodSnapshot, args[0]=job.additionalCan null / empty / 없음 [외부 소유: v2]; 수정 불필요
21 admin/audition/AdminAuditionService.kt:94 updateAudition IN_PROGRESS_AUDITION; 고정 titleKey, args[0]=audition.title(오디션 제목) null / empty / 없음 [외부 소유: parent/admin]; 수정 불필요
22 v2/gift/application/GiftPushService.kt:115 publishGiftPush INDIVIDUAL GIFT; 11개 공개 알림 메서드가 고정 title/message로 위임. memberId는 수신자, applicationNo는 deepLinkId null / empty / 없음 [외부 소유: v2]; 회원 표시 이름이 없으므로 수정 불필요

기프트의 11개 입구는 sendTrackingDeadlineReminder, sendRecipientAddressDeadlineReminder, sendAutoCanceledToSender, sendTrackingRegisteredToRecipient, sendArrivedAtMailboxToSender, sendArrivedAtMailboxToRecipient, sendInspectionCompleted, sendUndeliverableToSender, sendUndeliverableToRecipient, sendDeliveredToSender, sendDeliveredToRecipient다. 모두 공통 22번 발행으로 수렴하며 수신자 ID를 발신자로 복사하면 안 된다.

FCM 저장·외부 capacity 분류

생산/읽기 파일 유형 결정/증거 테스트/남은 검증
src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt sendPush → fcm/notification/PushNotificationService.kt saveNotification 신규 recipient-language payload 및 언어별 immutable snapshot 생산 token language를 정규화한 Lang을 명시 전달. render 후 저장해야 함 T-FCM
src/main/kotlin/kr/co/vividnext/sodalive/fcm/notification/PushNotificationList.kt 기존 역사 snapshot senderNicknameSnapshot은 @Column(nullable=false) 기본 길이 255, message는 TEXT, languageCode 길이 8. sender ID 필드 없음. 기존 entity/DDL 확장 금지 T-FCM 255자 신규 저장/과거 행 보존; 실제 MySQL charset/collation/컬럼 사양은 미확인
src/main/kotlin/kr/co/vividnext/sodalive/fcm/notification/PushNotificationListRepository.kt getNotificationList → PushNotificationService.getNotificationList 역사 snapshot 읽기 senderNicknameSnapshot을 직접 projection, locale 필터는 LangContext, 회원 join 없음. 현재 alias로 재조회하지 않음 저장 행은 T-FCM historical 대조군. 목록 query는 read-only 근거와 기존 src/test/kotlin/kr/co/vividnext/sodalive/fcm/notification/PushNotificationServiceTest.kt; H2에 JSON_CONTAINS 지원 fixture가 없어 새 실제 목록 SQL 테스트는 미포함
src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmService.kt send 외부 provider 전달 title/body를 Firebase Notification에 그대로 넣으며 truncate 없음. 이번 test는 mock 경계까지이고 provider 전송하지 않음 [blocked] provider의 byte/payload 제한과 255 Unicode 별칭 호환성, 실제 모바일 수신 증거는 메시징 소유자 필요

WS/HTTP chat 생산·읽기 지도

공통 prefix는 src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/다. 모든 신규 payload의 sender ID는 message.participant.member.id이며 roomId/opponentId로 이름 조회하지 않는다.

파일/메서드 유형·생산 → 읽기 GREEN 경계 검증
websocket/UserCreatorChatWebSocketAuthInterceptor.kt beforeHandshake 실제 JWT MemberAdapter → session.attributes Accept-Language를 Lang.fromAcceptLanguage로 읽어 lang: Lang 저장. 제안 상수 LANG_ATTRIBUTE="lang". 누락/미지원은 KO. authentication/memberId는 그대로 T-WS handshake
websocket/UserCreatorChatWebSocketHandler.kt handleSendText request text → service 저장 → SEND_ACK 직렬화 세션 Lang을 service에 명시 전달하거나 ACK 경계에서 batch 렌더링. request scope를 WS callback에서 조회하지 않음. ACK mine=true, requestId 보존 T-WS sender ACK
service/UserCreatorChatService.kt sendTextMessageByWebSocket/deliverRealtime/websocketMessagePayload 현재 DTO를 먼저 JSON 문자열로 만들어 broker.publish(roomId, opponentId, payload)에 전달 중립 senderId와 원본 DTO/본문을 보존해 publish. recipient 세션 언어를 이 단계에서 결정하지 않음. 현재 broker의 기존 4개 생성자 의존성으로 테스트 가능하며 미구현 생성자를 테스트에서 호출하지 않음 T-WS 실제 송수신
websocket/UserCreatorChatRoomMessageBroker.kt publish/onMessage Redis channel v2:user-creator-chat:ws:room → sessionRegistry.findSessions(roomId, memberId) → 같은 pre-serialized 문자열을 모두 전송 최소 변경은 기존 envelope의 payload가 가진 senderId/원본 값을 neutral source로 유지하고 onMessage에서 해석한 뒤 세션별 새 DTO/envelope 직렬화. KO/JA Lang별 batch map 조회. 공유 객체 변이/본문 치환 금지. 중립 메시지에 요청 언어 이름을 미리 굳히지 않음 T-WS same-member 2-session. 기존 src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatRoomMessageBrokerTest.kt, UserCreatorChatRedisIntegrationTest.kt 회귀도 부모에서 실행
service/UserCreatorChatService.kt openRoom/getMessages/toMessageItemDto DB 현재 opponent.nickname/sender.nickname → HTTP 응답 HTTP 요청 경계 Lang으로 opponentId/조회 page senderIds batch. sendVoiceMessage/deliverRestMessage의 HTTP message도 별도 request Lang, broker neutral DTO와 독립 T-WS HTTP open/messages; voice HTTP/S3 경계는 후속 GREEN 회귀 필요
websocket/UserCreatorChatWebSocketSessionRegistry.kt roomId+memberId → 복수 세션 이미 복수 세션 지원. 회원 하나의 locale을 전역 저장하지 않음. registry 자체 변경 불필요 기존 src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketSessionRegistryTest.kt
repository/UserCreatorChatRoomRepository.kt findDmChatListRooms opponent.member.nickname → 외부 소유 v2/chat/dto/ChatRoomListQueryDto의 targetName 내부 projection에 실제 opponent memberId가 없음. roomId를 alias key로 사용 금지. 아래 cross-owner 조정 필요 기존 src/test/kotlin/kr/co/vividnext/sodalive/v2/chat/ChatRoomListServiceTest.kt, HomeFollowing 관련 회귀

WS 외부 client locale 변경은 reconnect로 새 handshake를 하도록 계약해야 한다. 기존 연결의 locale 자동 변경/메시지 타입 추가는 이번 내부 테스트만으로 승인하거나 구현하지 않는다.

live 생산·읽기와 캐시 분류

공통 prefix는 src/main/kotlin/kr/co/vividnext/sodalive/다. 이 표의 member ID는 표시 이름의 실제 소유자이며 resource ID가 아니다.

생산 파일/메서드 → 소비 파일/메서드 유형·ID/Lang 출처 최소 GREEN 적용 또는 보존 결정 검증/첫 단계 미포함
live/room/LiveRoomService.kt enterLive/setSpeaker/setListener/setManager → live/room/info/LiveRoomInfo.kt → LiveRoomMember.kt Redis 언어 중립 membership. 인증 Member 또는 request.memberId로 재조회한 Member의 원본 nickname/id writer 원본 유지. LiveRoomInfo/LiveRoomMember 저장 객체를 request locale로 덮어쓰지 않음 T-LIVE actual Redis
위 membership → live/room/LiveRoomService.kt getRoomInfo current live render; host Member.id + 각 member.id, HTTP LangContext 경계 speaker/listener/manager를 새 복사본으로 렌더링. LiveRoomMember.copy는 생성자 밖 var role을 자동 복사하지 않으므로 role을 보존해야 함. 저장 권한/equality 흐름에 localized nickname을 집어넣지 않음 T-LIVE info, role/Redis 원본
위 membership 또는 reservation Member 관계 → live/room/LiveRoomService.kt getRoomDetail/getUserProfile current-name DTO. detail manager=room.member.id, users=각 id, profile=user.id 한 요청의 IDs로 batch 조회 후 DTO names만 대체. USER/비활성 fallback. 내부 membership 권한 검사는 원본 객체로 유지 T-LIVE detail/profile
live/room/LiveRoomService.kt donation/donationV2, live/roulette/v2/RouletteService.kt spinRoulette → LiveRoomInfo.addDonationMessage/addRouletteMessage → getDonationMessageList Redis 언어 중립 live feed. memberId/nickname을 원본으로 저장, 본문/UUID/isSecret 별도 current live read의 복사본 nickname만 batch override, secret 필터 유지. writer의 nickname/본문/canMessage/UUID를 재작성하지 않음. 이미 client에 전달한 payload도 불변 T-LIVE donation+roulette; 실제 결제/roulette spin 자체는 미실행
live/room/LiveRoomRepository.kt getLiveRoomListNow/ReservationWithDate/WithoutDate → LiveRoomService.getRoomList 현재 DB Member 관계; creatorId 이미 있음. HTTP LangContext 목록 전체 creatorIds batch 후 creatorNickname만 대체 후속 focused src/test/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomServiceAdultVisibilityPolicyTest.kt; 소유 범위 밖 api/home/HomeService, api/live/LiveApiService가 호출하므로 공유 서비스에서 해결
live/room/LiveRoomRepository.kt getDonationList → LiveRoomService.getDonationStatus 금액 집계 materialization 아닌 요청 시 현재 member.nickname/ID projection donationList memberId로 batch, 금액/secret 집계/정렬 불변. 역사 닉네임 스냅샷 아님 T-LIVE 후속 DB 금액 fixture 필요
live/room/LiveRoomRepository.kt getHeartList → LiveRoomService.getHeartList 요청 시 현재 member.nickname, 반환 projection에 memberId 없음 내부 donor memberId 추가 필요. 공개 DTO field 추가 없이 내부 projection/경계 연결 T-LIVE 후속 DB 금액 fixture 필요
live/reservation/LiveReservationService.kt makeReservation/getReservationList/getReservation 예약은 member 관계만 저장. nickname/masterNickname는 매번 room.member.nickname 현재 이름 조회로 분류. room.member.id와 HTTP LangContext batch. 예약 시각/가격/취소 규칙 불변 기존 src/test/kotlin/kr/co/vividnext/sodalive/live/reservation/LiveReservationServiceIntegrationTest.kt 후속 이름 회귀
live/room/visit/LiveRoomVisitService.kt roomVisit → getRecentVisitRoomUsers 방문 기록은 member 관계/시각, 이름은 현재 Member→GetRoomDetailUser 현재 이름 조회. 반환 members IDs batch + 명시 요청 Lang. 사용자/역할 필터 불변 새 테스트 경로 필요 시 GREEN 전 부모 계획에 추가; 첫 단계 테스트 없음
live/recommend/LiveRecommendController.kt → LiveRecommendService.getRecommendLive → LiveRecommendCacheService.getRecommendLive → LiveRecommendRepository.getRecommendLive banner read-through cache, key=getRecommendLive:{memberId or guest}:{isAdult}:{lang.name()}, cache_ttl_3_hours. configs/RedisConfig.kt TTL 3시간 실제 payload는 GetRecommendLiveResponse(imageUrl, creatorId)뿐이며 nickname 없음. 이름 캐시가 아니므로 alias 적용 불필요. 이미지 안 텍스트는 서버 alias 치환 대상 아님 src/test/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendRepositoryTest.kt, LiveRecommendServiceTest.kt 기존 선례. 분류는 read-only 증거, 새 RED 없음
live/recommend/LiveRecommendRepository.kt getOnAirRecommendChannelList/getRecommendChannelList/getOnAirFollowingChannelList/getFollowingChannelList/getCreatorFollowingAllList → LiveRecommendService 같은 public read 메서드들 캐시 밖 현재 nickname+creatorId 목록. HTTP 경계 언어를 아직 안 받는 메서드 있음 controller에서 LangContext 선택 후 service에 명시 Lang 전달하고 반환 IDs batch. count/필터/합치기/정렬은 변경하지 않음 기존 위 두 recommend 테스트에 후속 이름 회귀 필요
live/room/LiveRoomRepository.kt getLatestFinishedLive → LiveRoomService.getLatestFinishedLive → LiveRoomController 및 외부 소유 api/live/LiveApiService.kt 현재 Member.nickname을 포함한 read-through cache, key=getLatestFinishedLive:{memberId or guest}, cache_ttl_10_minutes/TTL 10분. Lang 없음 종료된 라이브여도 nickname 역사 snapshot이 아님. 중립 캐시 밖 request-time batch 렌더링 또는 Lang별 key 분리가 필요. 단순히 cache 내부 이름만 현지화하면 다른 요청 언어로 샘 T-LIVE KO→JA 최근 종료 라이브. 테스트는 현행 cache key를 정리함; GREEN에서 key를 바꾸면 정리 key도 맞춰야 함
live/room/LiveRoomService.kt enterLive의 blocked-by-host 오류 / message/MessageService.kt blocked-by-recipient 오류 현재 회원 이름이 삽입되는 사용자 메시지 ID 기반 현재 이름을 해당 HTTP 요청 언어로 렌더링. 원본 인증 Member는 수정하지 않음 후속 예외 회귀 필요

GetRecentRoomInfoResponse는 title/notice/cover/인원/genderRestriction뿐이라 회원 이름 적용 대상이 아니다. live signature/menu/roulette의 상품명·메뉴·결과 문구를 회원 별칭으로 치환하지 않는다. v2 roulette은 shared feed의 이름 writer인 것만 확인했고 원본 writer 변경을 요구하지 않는다.

legacy message 분류

생산 → 읽기 유형/필요 ID GREEN 검증
src/main/kotlin/kr/co/vividnext/sodalive/message/MessageService.kt sendTextMessage/sendVoiceMessage → message/Message.kt DB는 sender/recipient Member 관계와 본문/voice path만 저장, nickname snapshot 없음 저장/원본 Member 보존. FCM name metadata는 위 #3/#4 T-MESSAGE 실제 관계 저장
message/MessageRepository.kt sent/received/keep text/voice → MessageService.getTextMessageResponse/getVoiceMessageResponse 현재 이름 조회. 각 sender.id와 recipient.id 모두 필요 한 페이지 양쪽 memberIds 합집합 batch, request Lang으로 DTO만 대체. 본문/voice URL/시간/keep/delete/count 불변 T-MESSAGE 6개 public read 메서드

부모와 다른 소유자에게 필요한 정확한 수정 목록

  • P3 후속 구현의 직접 파일: src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt(metadata + recipient group render), fcm/notification/PushNotificationService.kt(이미 렌더링한 새 sender snapshot 저장), v2/usercreatorchat/websocket/UserCreatorChatWebSocketAuthInterceptor.kt(세션 Lang), UserCreatorChatWebSocketHandler.kt(ACK), UserCreatorChatRoomMessageBroker.kt(session별 render), v2/usercreatorchat/service/UserCreatorChatService.kt(중립 publish/HTTP names/FCM args metadata), message/MessageService.kt(현재 이름·FCM metadata), live/room/LiveRoomService.kt(live current reads/copy/FCM title metadata/종료 라이브 캐시 경계), live/recommend/LiveRecommendController.kt 및 LiveRecommendService.kt(명시 Lang/current names), live/reservation/LiveReservationService.kt, live/room/visit/LiveRoomVisitService.kt. full prefix는 모두 src/main/kotlin/kr/co/vividnext/sodalive/다.
  • 필요한 내부 projection만 추가: live/room/LiveRoomRepository.kt getHeartList의 실제 donor memberId. live/room/like/GetLiveRoomHeartListResponse.kt 공개 스키마를 늘리지 말고 내부 projection 경계를 마련한다. GetRoomInfoResponse, LiveRoomDonationMessage는 이미 ID가 있어 공개 필드 추가 불필요. LiveRoomMember/LiveRoomInfo/roulette writer 저장 locale 변경 불필요.
  • legacy worker에 넘길 FCM 변경: src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerService.kt saveNotice(#9), explorer/profile/creatorCommunity/CreatorCommunityService.kt createCommunityPost/createCommunityPostComment(#10/#11), content/AudioContentService.kt uploadComplete/releaseContent(#14/#15), content/comment/AudioContentCommentService.kt registerComment(#12)에 위 titleMemberId만 명시. 기존 args는 그대로 보존하고, 이름 인자가 아니므로 새 nameArgumentMemberIds만 empty로 둔다. 특히 #12의 senderMemberId를 parent ID로 바꾸면 sender snapshot의 행위자 의미가 깨진다.
  • 부모/admin 소유자에게 넘길 FCM 변경: src/main/kotlin/kr/co/vividnext/sodalive/admin/live/AdminLiveService.kt cancelNotStartedLive4hoursPassed(#16)에 room.member.id의 title metadata. admin/audition은 이름 슬롯이 없어 수정하지 않는다.
  • v2 worker와 동시에 합의해야 할 공유 projection: src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/repository/UserCreatorChatRoomRepository.kt findDmChatListRooms가 쓰는 외부 소유 v2/chat/dto/ChatRoomListQueryDto에 실제 opponentMemberId를 내부 필드로 추가해야 한다. 소비자 v2/chat/service/ChatRoomListService.kt getRooms/toResponse 및 v2/api/home/following/application/HomeFollowingFacade.kt 경유 이름을 request Lang으로 렌더링하도록 v2 worker가 조정한다. AI row producer src/main/kotlin/kr/co/vividnext/sodalive/chat/room/repository/ChatRoomRepository.kt findAiChatListRooms도 같은 생성자를 쓰므로 대응 필요. public targetName/roomId 타입은 그대로다. 이 첫 단계에서 해당 파일은 수정하지 않았다.
  • 기반 담당자: creatorname 서비스/테이블 매핑은 기반 작업자가 제공한다. 이 작업은 core Member/Lang/serializer/config/DDL을 변경하지 않는다. 기반 없이 테이블 부재로 실패한 실행은 의미 있는 RED 증거가 아니다.

외부 계약 차단

의존 소유자/경로 내부 확인과 차단 해제 증거
EXT001 앱 전체 표시·원본 편집·client cache [blocked: 모바일 저장소/파일/담당자 미확인] 서버 DTO 스키마를 유지하더라도 원본 편집기에 localized 이름이 저장되지 않는다는 앱 증거 필요. 이름 cache invalidation/수명/표면 목록 확인 필요
EXT002 운영 UI [blocked: 운영 UI 저장소/경로/인증 주체 미확인] 이번 전달 계층 테스트로 관리자 UI 저장 흐름을 완료 처리하지 않음
EXT003 WS session locale [blocked: client/messaging 저장소·담당자 미확인] handshake Accept-Language 지원, 언어 변경 시 reconnect, 같은 회원 KO/JA 다중 세션의 실제 앱 검증. 서버 localhost 테스트는 모바일 계약 확인을 대신하지 않음
EXT003 RTC/live 표시 소비 [blocked: RTC/client 표시 경로 미확인] 서버 RTC UID/RTM account는 member.id 문자열 및 worker UID이고 이름을 RTC에 등록하는 코드 없음. live HTTP names와 클라이언트/RTM display cache의 매핑/갱신 시점 확인 필요
push provider / 운영 DB capacity [blocked: 메시징/DB 담당자의 실제 설정 및 증거 필요] 기존 push snapshot mapping 255와 test H2 저장만 확인 가능. 운영 MySQL charset/column, FCM payload byte 제한, 255 code point/다중 code unit 별칭의 실제 모바일 전송 호환성은 별도 확인. truncate/과거 행 rewrite/column 확장으로 우회하지 않음

이번 정적 점검과 인계 한계

  • rg -n 'FcmEvent\(' src/main/kotlin로 선언 1개와 실제 생산 22곳을 대조하고 각 호출부의 title/args/sender ID를 열어 확인했다. 실제 FcmService send는 listener 단일 진입이며 기프트도 위 단일 이벤트 발행이다.
  • 기존 생성자/DTO/서비스 메서드, JPA/Redis 저장과 조회, HTTP endpoint, JWT 생성/검증, request-scoped LangContext, 테스트 초기화 선례를 읽어 테스트를 작성했다. 테스트 안에 미구현 service constructor 호출/placeholder/Disabled/실제 Firebase·S3 전송은 없다.
  • rg로 신규 테스트의 130자 초과 줄/후행 공백/TODO/FIXME/Disabled/mock 직접 생성 패턴이 없음을 확인했다. mock bean은 FcmService/S3Uploader뿐이다. 이 작업에 lsp_diagnostics 도구는 제공되지 않았다. Gradle/test/build/lint/실제 localhost QA는 사용자 지시에 따라 미실행이며 부모가 직렬 실행한다.
  • 외부 앱/RTC 계약과 실제 MySQL/FCM capacity는 내부 구현으로 해제되지 않는다. 아래 추가 통합 사례도 부모의 직렬 GREEN 실행 전까지 통과를 주장하지 않는다.

GREEN 구현 상태

이 절은 후속 GREEN 위임의 현재 상태다. 모든 구현/대기는 코드 연결과 정적 대조 완료, 부모 runtime 검증 대기를 의미한다. 원 PRD/plan이나 제품 Gate를 완료 처리하지 않았다.

inventory 행/표면 처리 상태 구현 및 불변 조건 연결 테스트
FCM #3/#4/#5 구현/대기 legacy text/voice 및 v2 DM/voice가 nameArgumentMemberIds=mapOf(0 to sender.id) 명시. event의 원본 args는 유지 T-FCM, T-MESSAGE, UserCreatorChatServiceTest
FCM #6/#7/#8/#16 구현/대기 live 생성/시작/취소 및 admin 자동 취소에 titleMemberId 명시. 방 제목 args는 이름 metadata 없음. admin 파일은 허용된 한 줄만 수정 T-FCM 실제 live 취소, 기존 live 회귀
FCM #9/#10/#11/#12/#14/#15 타 소유자 연결 정적 확인/대기 legacy worker가 실제 파일에 titleMemberId 추가한 것을 확인. #12는 parent?.member?.id ?: member.id, senderMemberId는 행위자 그대로. 이 작업은 해당 생산 파일을 수정하지 않음 T-FCM 실제 registerComment 생산자, CREATOR/USER actor 각 1개
FCM #1/#2/#17/#18/#19/#20/#21/#22 의도적 보존/대기 자유 제목/본문, 이벤트·오디션·콘텐츠 제목, 숫자 args를 이름으로 추측하지 않음. sender ID 없는 기존 fallback 의미 유지 T-FCM 실제 GiftPushService 생산자; 기존 FcmService/PushNotificationService 회귀
FCM #13 및 새 sender snapshot 구현/대기 sender/title/name-args ID 합집합을 정규화한 token Lang 그룹마다 batch 조회. title/body/args는 그룹별 새 값, 새 notification nickname만 sparse alias로 override. 기존 profile/명시 snapshot/default fallback은 기존 resolver 그대로 T-FCM 255자, 역사 행 불변, 기존 PushNotificationServiceTest
역사 notification 읽기/용량 의도적 보존/외부 blocked sender ID 없는 기존 rows와 목록 projection, JPA/DDL 길이를 변경하지 않음. provider/운영 DB capacity는 계속 blocked T-FCM historical 대조군; 실제 목록 JSON_CONTAINS SQL 미검증
WS handshake / ACK 구현/대기 LANG_ATTRIBUTE="lang"에 Lang 저장, 누락/미지원 KO. handler가 sendTextMessageByWebSocket(..., lang: Lang)에 필수 명시. WS callback에는 LangContext 접근 없음 T-WS 실제 JWT/localhost, 기존 handler/auth tests
WS pubsub / 복수 세션 구현/대기 기존 UserCreatorChatRoomPublishedMessage(roomId, memberId, payload: String) wire 유지. 원본 senderId/name/body가 포함된 중립 payload를 publish. broker가 세션 Lang별 batch 조회 후 각 세션에 새 ObjectNode copy. MESSAGE 내 senderNickname 필드만 변경 T-WS 같은 회원 KO/JA 동시 세션, 기존 broker/Redis integration
chat HTTP open/messages/voice 구현/대기 HTTP 요청 Lang으로 현재 opponent/page sender 이름을 batch 렌더링. voice HTTP DTO와 WS neutral DTO는 독립. body/mine/requestId/ID/voice URL 유지 T-WS HTTP 및 새 voice REST/WS 사례
chat 목록 공유 projection 계약 연결 정적 확인/대기 UserCreatorChatRoomRepository.findDmChatListRooms constructor 끝에 opponent.member.id 추가. v2 worker의 ChatRoomListQueryDto 마지막 opponentMemberId: Long?와 소비 batch lookup을 실제 파일에서 확인. 이 작업은 v2/chat/AI repository를 수정하지 않음 기존 v2 ChatRoomListService/Controller 및 HomeFollowing tests를 부모가 실행
live Redis membership / donation / roulette writer 의도적 원본 보존/대기 LiveRoomInfo, LiveRoomMember, donation/roulette writer 저장 구조와 이름은 바꾸지 않음. 응답에서만 copy. role은 copy 후 명시 복원, secret 필터와 UUID/본문 유지 T-LIVE role/secret/Redis 원본
live info/detail/profile/list 구현/대기 실제 memberIds로 current Member batch와 활성 JA alias를 결합. 목록과 shared Redis base 이름도 현재 원본 fallback. 권한 검사에 localized 객체를 넣지 않음 T-LIVE info/detail/profile/fallback, 기존 AdultVisibilityPolicyTest
live donation/heart 집계 구현/대기 donation userId 활용. heart는 내부 LiveRoomHeartListRow(memberId, ...) projection 추가 후 기존 공개 3-field DTO로 변환. 금액/순서/secret 합계 불변 T-LIVE 실제 UseCan fixture
live 예약 / 최근 방문 구현/대기 reservation owner IDs 및 visitor IDs batch. 예약 read transaction과 room/owner fetch join으로 OSIV off 및 owner N+1 방지. 일반 사용자/역할/예약 정책 필터 유지 T-LIVE 예약 생성·목록·상세 및 방문자, 기존 LiveReservationServiceIntegrationTest
live 추천 / 팔로잉 구현/대기 LiveRecommendService의 HTTP 호출 경계 LangContext를 사용해 기존 api/live caller signature를 유지. on-air early return과 off-air 결합 모두 한 batch 렌더링. 이름 없는 3시간 banner cache는 수정하지 않음 T-LIVE 실제 추천/팔로잉, 기존 LiveRecommendService/Repository tests
최근 종료 live 10분 cache 구현/대기 같은 기존 key/TTL의 @Cacheable을 repository query 경계로 이동해 중립 DTO만 캐시. 서비스는 캐시 밖에서 현재 Member 이름+alias batch로 새 응답. 원본 변경/별칭 해제도 cached nickname로 fallback하지 않음 T-LIVE KO→JA→별칭 해제/현재 원본 변경, 기존 key 정리 유지
legacy message 현재 이름 / 오류 문구 구현/대기 text/voice sent/received/keep의 sender+recipient 합집합 Member 및 alias batch. 기존 Message/Member 저장 불변. blocked recipient/host 오류의 표시 이름도 현재 요청 언어로 렌더링 T-MESSAGE 및 기존 live 정책 tests

fixture 및 기존 테스트 정합성

  • 부모 RED XML의 live 상세 예외는 MemberContentPreferenceService.initializeDefaultPreference:61 → getStoredPreference(REQUIRES_NEW) → LiveRoomService.getRoomDetail:538이다. 미커밋 test Member를 새 트랜잭션이 못 보는 fixture 문제라 해당 상세 테스트만 Propagation.NOT_SUPPORTED로 저장을 commit하고 method DirtiesContext로 DB fixture를 격리했다. 원래 policy assertion은 삭제/완화하지 않았다. 같은 실제 preference 경계를 타는 새 추천 테스트에도 동일 격리를 사용한다.
  • 기존 UserCreatorChatService/LiveRoomService/LiveRecommendService Mockito constructor는 새 필수 resolver와 HTTP LangContext를 명시한다. Redis-only broker integration은 필수 resolver를 @MockBean으로 공급한다. 실제 새 이름 통합 테스트는 계속 실제 JPA 이름 resolver를 사용한다.
  • voice 추가 사례는 실제 JPA 전송 서비스를 HTTP request context에서 직접 호출하고 실제 localhost WS 수신과 비교한다. S3만 mock한다. multipart HTTP POST 자체의 네트워크 E2E를 실행한 증거는 아니며, open/messages GET은 기존 테스트가 실제 HTTP로 호출한다.
  • WebSocket send의 필수 Lang 인자를 모든 기존 test call/stub/verify에 추가했다. PushNotificationService의 필수 localizedSenderNickname 인자를 기존 fallback tests에는 null로 명시해 원래 동작 검증을 유지한다. nullable/optional resolver constructor로 우회하지 않았다.
  • 제공된 creatorname-foundation-final-lint.log의 11개 진단은 content/explorer/member/search 테스트에만 있으며 이 작업 소유 파일은 없었다. 그 파일은 병렬 legacy 소유자에게 남기고, 이 작업 변경 파일의 import/인자 줄바꿈/130자/후행 공백은 정적으로 점검했다. ktlint를 실행했다고 주장하지 않는다.

이 GREEN 위임에서 변경한 정확한 파일

공통 main prefix: src/main/kotlin/kr/co/vividnext/sodalive/.

  • fcm/FcmEvent.kt
  • fcm/notification/PushNotificationService.kt
  • v2/usercreatorchat/websocket/UserCreatorChatWebSocketAuthInterceptor.kt
  • v2/usercreatorchat/websocket/UserCreatorChatWebSocketHandler.kt
  • v2/usercreatorchat/websocket/UserCreatorChatRoomMessageBroker.kt
  • v2/usercreatorchat/service/UserCreatorChatService.kt
  • v2/usercreatorchat/repository/UserCreatorChatRoomRepository.kt
  • message/MessageService.kt
  • live/room/LiveRoomService.kt
  • live/room/LiveRoomRepository.kt
  • live/room/like/GetLiveRoomHeartListResponse.kt
  • live/recommend/LiveRecommendService.kt
  • live/reservation/LiveReservationService.kt
  • live/reservation/LiveReservationRepository.kt
  • live/room/visit/LiveRoomVisitService.kt
  • admin/live/AdminLiveService.kt (titleMemberId metadata 한 줄만)

공통 test prefix: src/test/kotlin/kr/co/vividnext/sodalive/.

  • fcm/FcmSendListenerLocalizedNameTest.kt
  • fcm/notification/PushNotificationServiceTest.kt
  • v2/usercreatorchat/UserCreatorChatServiceTest.kt
  • v2/usercreatorchat/UserCreatorChatServiceIntegrationTest.kt
  • v2/usercreatorchat/websocket/UserCreatorChatLocalizedNameIntegrationTest.kt
  • v2/usercreatorchat/websocket/UserCreatorChatRoomMessageBrokerTest.kt
  • v2/usercreatorchat/websocket/UserCreatorChatRedisIntegrationTest.kt
  • v2/usercreatorchat/websocket/UserCreatorChatWebSocketHandlerTest.kt
  • live/room/LiveRoomLocalizedNameIntegrationTest.kt
  • live/room/LiveRoomServiceTest.kt
  • live/room/LiveRoomServiceAdultVisibilityPolicyTest.kt
  • live/recommend/LiveRecommendServiceTest.kt

문서는 이 delivery-name-inventory.md만 갱신했다. T-MESSAGE 신규 통합 테스트는 1차 작성본을 그대로 사용한다. main PRD/plan-task, foundation/Member/Lang, legacy worker 소유 생산 파일, v2/chat 및 AI repository는 이 작업에서 수정하지 않았다.

부모의 직렬 GREEN 검증 명령

아래는 미실행 명령이다. 병렬 쓰기가 끝난 뒤 부모가 실행하고, assertion/compile/lint 결과를 별도 기록한다.

./gradlew test --tests 'kr.co.vividnext.sodalive.fcm.FcmSendListenerLocalizedNameTest'
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatLocalizedNameIntegrationTest'
./gradlew test --tests 'kr.co.vividnext.sodalive.live.room.LiveRoomLocalizedNameIntegrationTest'
./gradlew test --tests 'kr.co.vividnext.sodalive.message.MessageServiceLocalizedNameTest'
./gradlew test --tests 'kr.co.vividnext.sodalive.fcm.notification.PushNotificationServiceTest' --tests 'kr.co.vividnext.sodalive.fcm.FcmServiceTest'
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest' --tests 'kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceIntegrationTest' --tests 'kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.*'
./gradlew test --tests 'kr.co.vividnext.sodalive.live.room.LiveRoomServiceTest' --tests 'kr.co.vividnext.sodalive.live.room.LiveRoomServiceAdultVisibilityPolicyTest' --tests 'kr.co.vividnext.sodalive.live.reservation.LiveReservationServiceIntegrationTest' --tests 'kr.co.vividnext.sodalive.live.recommend.LiveRecommendServiceTest' --tests 'kr.co.vividnext.sodalive.live.recommend.LiveRecommendRepositoryTest'
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.chat.ChatRoomListServiceTest' --tests 'kr.co.vividnext.sodalive.v2.chat.ChatRoomListControllerTest'
./gradlew ktlintCheck
./gradlew assemble

동일 localhost/Redis surface 테스트가 부모의 GREEN 및 수동 사용 근거를 제공할 때까지 서버 runtime 완료를 주장하지 않는다. 외부 앱·RTC reconnect·provider/운영 DB capacity blocked 항목은 그대로 유지한다. 새 SDK 기능, DB 적용, historical row rewrite, truncate, 커밋/푸시는 하지 않았다.

COR013 Delivery JOIN 전환 인계, 2026-10-15

이 절은 앞의 batch 기반 구현 이력보다 우선하는 현재 전환 manifest다. PRD 10절과 계획의 JOIN-T1~T3를 읽고 delivery 소유 코드만 수정했다. main PRD/plan-task, core creatorname/Member/Lang, 다른 legacy/v2 도메인, 검색 JA 및 관리자 /ja 쓰기 정책은 수정하지 않았다. 부모가 제공한 core 43개 통과와 이전 전체 2249개 통과는 이번 변경 후 실행 결과가 아니다.

부모 RED 로그 creatorname-join-consumer-red.log에서 LiveRoomLocalizedNameIntegrationTest의 추천/팔로잉 테스트가 이름 출력이 아닌 별칭 후속 SELECT 단언으로 실패한 것을 확인했다. 이번 작업은 read/rg/apply_patch만 사용했다. Gradle, 테스트, 빌드, lint, HTTP/WS 실행, 에이전트, git/커밋/푸시, 운영 DB 접근은 하지 않았다. 아래 상태는 부모 직렬 GREEN 실행 준비, PASS 판정이 아니다.

전환 Manifest

아래 main 경로의 공통 prefix는 src/main/kotlin/kr/co/vividnext/sodalive/다. 모든 직접 DB 표시 query는 요청 Lang의 명시 LEFT JOIN과 COALESCE를 사용한다. member ID·Lang·active·CREATOR는 ON 조건이며 기존 업무 WHERE/count/페이지/금액 조건과 공개 JSON 필드는 유지한다.

정확한 파일 전환 내용
live/recommend/LiveRecommendRepository.kt; live/recommend/LiveRecommendService.kt on-air/off-air 추천·팔로잉 및 전체 팔로잉 5개 projection에서 CreatorNameJoin 사용. 서비스 이름 map/copy와 resolver 주입 제거. 20개 early return도 이미 조인된 결과를 반환. count에는 이름 조인을 추가하지 않음
live/room/LiveRoomRepository.kt; live/room/LiveRoomListRow.kt; live/room/LiveRoomService.kt NOW/날짜별 예약/예정 목록은 원본 room과 조인된 이름을 함께 반환. 태그·예약·권한에 필요한 원본 room/member를 변이하지 않으며 이름 후속 조회는 제거. 상세 host도 별도 read query에서 이름을 함께 선택. 금융/쓰기에서 사용하는 기존 getLiveRoom/getLiveRoomAndAccountId는 원본 entity 그대로
live/room/GetLiveRoomUserProfileResponse.kt; live/room/detail/GetRoomDetailResponse.kt 프로필은 원본 Member와 조인된 이름을 반환하여 Redis membership/권한 비교는 원본으로 하고 공개 nickname만 resolved 값 사용. 상세 manager 생성자도 query의 resolved 이름을 받음. 공개 응답 field/type은 불변
live/room/like/GetLiveRoomHeartListResponse.kt 내부 donor-ID batch 전용 LiveRoomHeartListRow 제거. 하트 query가 기존 공개 3-field item을 직접 반환. 후원/하트 합계·secret 노출·정렬은 원래 수식 유지
live/reservation/LiveReservationRepository.kt; live/reservation/LiveReservationListRow.kt; live/reservation/LiveReservationService.kt 예약 목록/단건을 scalar projection으로 전환. 예약 전 live 상세의 booker 목록도 이름 조인된 GetRoomDetailUser 반환. 생성·취소·가격·환불 원본 객체와 시간/cover fallback은 유지. 생성 응답만 기존 쓰기 경계 sparse batch 유지
live/room/visit/LiveRoomVisitRepository.kt; live/room/visit/LiveRoomVisitService.kt 최근 방문자 query가 resolved 이름과 profile URL을 직접 projection. 기존 사용자/역할 필터 보존, 이름 map/copy와 서비스 resolver/host 주입 제거
message/MessageRepository.kt; message/MessageListRow.kt; message/MessageService.kt text/voice sent/received/keep 6개 목록의 sender/recipient를 서로 다른 QMember/이름 alias로 조인한 scalar row로 반환. findMessageNames 및 원본+별칭 2-query 결합 제거. write/keep/delete/FCM 원본 metadata와 기존 text isKept=false 동작 보존
v2/usercreatorchat/repository/UserCreatorChatMessageRepository.kt; v2/usercreatorchat/dto/UserCreatorChatMessageRow.kt; v2/usercreatorchat/service/UserCreatorChatService.kt HTTP history는 sender 이름을 조인한 scalar row 반환. cursor/active/id-desc query와 createdAt 응답 정렬·mine·본문·URL·시간 의미 보존. 신규 전송의 neutral publisher/ACK/voice 응답 경계는 별도로 유지
v2/usercreatorchat/repository/UserCreatorChatParticipantRepository.kt HTTP open 상대 프로필만 findActiveOpponentProfile(roomId, memberId, lang) JOIN projection 사용. 송신 검증에서 필요한 원본 findActiveOpponent는 유지
v2/usercreatorchat/repository/UserCreatorChatRoomRepository.kt; v2/chat/service/ChatRoomListService.kt DM opponent 목록이 요청 Lang 이름을 직접 조인. 통합 목록 서비스의 이름-only batch/copy와 resolver 주입 제거. 활성 참여자·최신 메시지·팔로우·cursor·AI/DM 병합 순서 유지
v2/chat/dto/ChatRoomListDtos.kt; chat/room/repository/ChatRoomRepository.kt batch 전용 내부 opponentMemberId 제거, 공유 query DTO는 7필드. AI select-new도 동일 생성자로 맞추고 pc.character.name 그대로 선택. AI creator alias는 캐릭터 이름으로 사용하지 않음

Cross-Owner 및 내부 계약

  • 기반은 제공된 CreatorNameJoin(owner: QMember, lang: Lang, alias: String)의 name/condition/displayName과 CreatorLocalizedNameService.findCurrentNames(ids, lang)를 사용한다. 기반 파일은 수정하지 않았다. LiveRoomService.findCurrentNames는 이제 core의 단일 JOIN SELECT에 위임한다.
  • Live recommendation/list/donation/heart/reservation/visitor repository의 표시 query에는 필수 Lang을 전달한다. 기존 HTTP 서비스 경계의 LangContext가 선택하며 외부 api/live, api/home 호출 signature는 유지한다.
  • ChatRoomListService.getRooms(member, lang, filter, cursor, limit, followedCreatorsOnly)는 그대로다. v2 writer 소유 HomeFollowingFacade의 기존 명시 Lang 호출은 수정할 필요가 없다. 공유 AI repository에서는 위 7-field query DTO 생성자만 조정했다.
  • DM repository의 findDmChatListRooms는 followedCreatorsOnly 다음, pageable 앞에 필수 lang: Lang을 받는다. history는 findMessageHistory(chatRoom, cursor: Long?, lang: Lang, pageable)로 통합했다. JPQL :lang에는 enum 객체를 전달하고 CREATOR role은 실제 MemberRole.CREATOR enum literal로 ON에 둔다. 문자열 "EN"/"CREATOR"로 Hibernate enum 인자를 우회하지 않는다.
  • 중립 종료-live repository cache에는 Lang 인자를 추가하지 않았다. 기존 getLatestFinishedLive:{memberId or guest} key/10분 TTL과 raw DTO entry는 유지하며, miss/hit 모두 서비스에서 현재 이름을 렌더링한다.

잔여 Batch 전수 목록

현재 소유 소스의 findLocalizedNames 직접 호출은 아래 9곳, 현재 원본까지 필요한 호출은 LiveRoomService.findCurrentNames의 core 위임 1곳으로 수렴한다. 단순 DB 목록을 이미 materialized됐다는 이유로 남긴 예외는 없다.

경계 정확한 호출 위치 남기는 이유/보존 의미
중립 live membership live/room/LiveRoomService.kt#getRoomInfo → findCurrentNames Redis speaker/listener/manager와 host ID를 한 번에 현재 이름으로 렌더링. 복사본의 role, 원본 membership 및 금융/권한 객체 보존
중립 live 상세 참가자 같은 파일 getRoomDetail의 on-air Redis branch → findCurrentNames Redis 참가자만 한 번 렌더링. host와 예약 전 DB booker branch는 JOIN으로 전환되어 이 batch에 포함되지 않음
중립 live feed 같은 파일 getDonationMessageList → findCurrentNames donation/roulette Redis feed를 기존 secret 필터 후 복사. UUID/본문/공유 원본 보존
중립 read-through cache 같은 파일 getLatestFinishedLive → findCurrentNames raw 캐시 재사용 후 한 JOIN SELECT로 현재 원본+해당 언어 이름 조회. 원본 변경/별칭 해제도 캐시의 옛 이름으로 fallback하지 않음
이미 검증에 필요한 원본 entity 같은 파일 enterLive의 차단/참여제한 오류 2곳 → findLocalizedNames 원본 room/host로 입장 검증 후 오류 문구만 sparse override. principal/host 원본은 불변
쓰기 반환 live/reservation/LiveReservationService.kt#makeReservation → findLocalizedNames 가격/결제/예약에 이미 필요한 room/owner 원본을 보존하고 생성 응답 이름만 렌더링
쓰기 검증 오류 message/MessageService.kt#sendTextMessage, sendVoiceMessage → findLocalizedNames 이미 검증한 recipient를 변경하지 않고 blocked 오류 문구만 렌더링. DB history는 batch 없음
ACK/쓰기 반환 v2/usercreatorchat/service/UserCreatorChatService.kt#sendTextMessageByWebSocket, deliverRestMessage → findLocalizedNames 명시 sender-session Lang/HTTP Lang별 응답만 sparse 렌더링. 원본 신규 메시지와 neutral pubsub/FCM snapshot은 별도 유지
수신자별 전달 fcm/FcmEvent.kt#FcmSendListener.sendPush → findLocalizedNames recipient Lang 그룹마다 sender/title/name-args ID 합집합의 sparse map 사용. 명시 snapshot/profile/raw/default fallback을 유지하며 current-name 조회로 바꾸지 않음
세션별 전달 v2/usercreatorchat/websocket/UserCreatorChatRoomMessageBroker.kt#onMessage → findLocalizedNames 같은 수신자의 복수 세션 Lang별 독립 payload. publisher payload가 이미 있으므로 DB JOIN 강제 없음

PushNotificationListRepository/PushNotificationService의 이미 렌더된 과거 senderNicknameSnapshot 목록은 변경하지 않았다. FCM title owner/name-argument metadata도 그대로다. LiveRecommendCacheService의 3시간 banner payload에는 이름이 없으며 변경하지 않았다. 예약 booker의 raw Member 조회는 취소/환불 등 기존 업무용이고 표시 이름 batch 호출이 아니다.

테스트 변경과 직렬 실행 인계

아래 test 경로의 공통 prefix는 src/test/kotlin/kr/co/vividnext/sodalive/다. 새 테스트 및 단언은 아직 실행하지 않았다.

  • live/room/LiveRoomLocalizedNameIntegrationTest.kt: 기존 추천 RED 단언 유지. 추천·팔로잉 전체에서 standalone name SELECT 금지, 활성 EN 읽기, NOW/날짜 예약/예정 목록 및 예약 booker/list/detail의 USER/미등록/비활성 행 보존과 query count 보강. 예약 read 2회/방문 read 2회/후원+하트 3회 단언. 실제 종료 cache의 raw entry와 room query 1회 재사용을 유지하고 KO/JA/EN current-name 조회는 매 호출 1회 단언.
  • message/MessageServiceLocalizedNameTest.kt: 기존 JA/KO/EN·본문·USER·voice 검증 보존. 6개 목록의 활성 EN 및 USER alias 무시, 비활성 fallback, count+list 합계 12회와 entity load 0 단언 추가.
  • 신규 v2/usercreatorchat/UserCreatorChatNameJoinRepositoryTest.kt: 실제 JPQL/DB로 모든 Lang의 DM 목록/history/opponent-profile 이름·USER/미등록/비활성 fallback, 한 query/entity load 0, inactive 메시지 제외와 history cursor 검증. 실제 AI/DM 통합 service+repositories로 AI 이름 불변·EN DM·팔로우·페이지 cursor 및 두 페이지 총 query 4회 검증. Redis가 불필요한 @DataJpaTest이며 새 mock 없음.
  • live/recommend/LiveRecommendRepositoryTest.kt; live/recommend/LiveRecommendServiceTest.kt; live/room/LiveRoomServiceAdultVisibilityPolicyTest.kt; v2/usercreatorchat/UserCreatorChatServiceTest.kt; v2/chat/ChatRoomListServiceTest.kt: 변경된 필수 Lang/projection/생성자 계약에 적응. 제거된 이름 서비스에 빈 optional stub/호환 생성자를 추가하지 않았다.
  • 기존 실제 localhost HTTP/WS/Redis 통합 테스트, FCM outbound/S3 mock 테스트 및 관리자 쓰기 정책 테스트는 삭제·완화하지 않았다.

부모 전용 명령이며 병렬 작업이 모두 끝난 뒤 한 Gradle runner에서 직렬 실행한다.

./gradlew test --tests 'kr.co.vividnext.sodalive.live.room.LiveRoomLocalizedNameIntegrationTest' --tests 'kr.co.vividnext.sodalive.message.MessageServiceLocalizedNameTest' --tests 'kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatNameJoinRepositoryTest' --offline --console=plain
./gradlew test --tests 'kr.co.vividnext.sodalive.live.*' --tests 'kr.co.vividnext.sodalive.message.*' --tests 'kr.co.vividnext.sodalive.fcm.*' --tests 'kr.co.vividnext.sodalive.v2.usercreatorchat.*' --tests 'kr.co.vividnext.sodalive.v2.chat.*' --tests 'kr.co.vividnext.sodalive.v2.api.home.following.*' --offline --console=plain
./gradlew test build ktlintCheck --continue --offline --console=plain

정적 확인: 변경 메서드의 전체 호출부와 direct mock 생성자를 rg로 대조했다. 소유 코드에서 findNicknamesByIds, findMessageNames, localizeChannels, opponentMemberId, LiveRoomHeartListRow 참조는 제거됐다. 변경 파일의 130자 초과/후행 공백을 점검했다. LSP 도구는 제공되지 않았으며 compile/JPQL 실행/테스트 통과 및 실제 HTTP/WS 동작은 부모 직렬 검증 전까지 주장하지 않는다. 외부 앱/RTC/provider/운영 DB blocker는 그대로다.