docs(creatorname): 언어별 이름 관리 요구사항과 검증 기록을 정리한다
This commit is contained in:
@@ -0,0 +1,324 @@
|
||||
# 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 외부 수용 기준의 완료를 뜻하지 않는다.
|
||||
|
||||
부모 전용 실행 명령(이번 작업에서 미실행):
|
||||
|
||||
```bash
|
||||
./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 결과를 별도 기록한다.
|
||||
|
||||
```bash
|
||||
./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에서 직렬 실행**한다.
|
||||
|
||||
```bash
|
||||
./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는 그대로다.
|
||||
Reference in New Issue
Block a user