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는 그대로다.
|
||||||
@@ -0,0 +1,480 @@
|
|||||||
|
# Legacy HTTP·검색 크리에이터 이름 적용 목록
|
||||||
|
|
||||||
|
## COR021 최신 범위
|
||||||
|
|
||||||
|
- ContentSeriesRepository의 목록 이름JOIN5곳과 ContentSeriesService 상세이름batch1곳을 HEAD로 되돌린다. 이 레거시 서비스 재사용자인 Home/Explorer의 시리즈 작성자 이름도 원본으로 돌아가며 기존 제목/장르/요일 번역은 유지한다.
|
||||||
|
- 공통 이름 기반과 별도v2·관리자 조회는 유지한다. 직접 관련된 AudioContentRepositoryLocalizedNameTest의 시리즈case는 이름단언만 정리하고 제목 번역/fallback을 보존한다. 최신 검증은 plan-task.md SERIES-NAME-TRIM-T1을 따른다.
|
||||||
|
- 적용 완료: 운영2파일HEAD일치, 제목검증case유지, 실제 시리즈·Home HTTP 및 전체2270건/lint통과를 확인했다. 영구테스트수증감은 없고 임시검증코드는 제거했다.
|
||||||
|
|
||||||
|
## COR020 최신 범위
|
||||||
|
|
||||||
|
- L42 CharacterCommentService/Repository 이름 변경을 HEAD로 되돌리고 해당 이름 대체 통합테스트1개만 제거한다. 댓글 작성·삭제·신고·권한·커서·건수·언어 감지와 다른 테스트는 유지한다.
|
||||||
|
- 실제 제거 대상은 LegacyCurrentNameReadIntegrationTest의 shouldLocalizeCharacterCommentWritersWithoutChangingCharacterNameOrUserReplies이며 T20 신규 예정 별도 파일은 생성되지 않았다. 아래 이력과 구분하고 최신 검증은 plan-task.md CHARACTER-COMMENT-TRIM-T1을 따른다.
|
||||||
|
- 적용 완료: 운영2파일HEAD일치, 전용test1건 제거 후 전체2270건/lint통과. 실제 댓글·답글·상세HTTP10요청도 확인했고 임시 검증 코드는 제거했다.
|
||||||
|
|
||||||
|
## COR019 최신 범위
|
||||||
|
|
||||||
|
- AudioContentMainController/Service의 이름 변경을 롤백하고 두 파일의 HEAD diff를 제거한다. L24의 전용 findByThemeFor2Weeks/findAudioContentByCurationId 이름 추가분과 L25·메인 큐레이션의 controller cache-hit 후 이름 batch를 제거한다.
|
||||||
|
- shared findByTheme는 AudioContentThemeService가 계속 사용하므로 유지한다. 전용 Main 테스트는 현재 없으며 T09는 계획 이력이다. 다른 실제 조회의 테스트는 삭제하지 않는다. 자세한 검증은 plan-task.md LEGACY-MAIN-T1을 따른다.
|
||||||
|
- 적용 완료: 두 메인 파일 HEAD와 동일, repository 전용2메서드 롤백. 임시 실제 HTTP5요청 통과 후 검증 코드는 제거했고 기존 전체2271건 및 lint 통과를 확인했다. 영구 테스트 증감은 없다.
|
||||||
|
|
||||||
|
## COR017 최신 범위
|
||||||
|
|
||||||
|
- AudioContentMainTab*은 사용자 확인상 미사용이다. 해당 7개 서비스 파일은 HEAD와 같은 상태로 되돌리고 이름 원본 보장을 위한 null 인자·공유 raw 모드·전용 검증을 제거한다. 공유 조회 변경의 미사용 탭 간접 영향은 허용한다.
|
||||||
|
- 실제 사용 중인 Main/Theme/Home 조회의 언어별 이름과 제목 번역은 유지한다. AudioContentCuration 및 레거시 커뮤니티·팬톡 등 나머지 COR015 범위는 바꾸지 않는다.
|
||||||
|
- 아래 COR015의 탭 raw 보장 규칙은 당시 이력이며 이 절로 대체한다. 구체 파일·검증은 plan-task.md UNUSED-TAB-T1을 따른다.
|
||||||
|
|
||||||
|
## COR015 현재 범위 보정
|
||||||
|
|
||||||
|
- 아래 COR012/COR013 표와 실행 기록은 과거 이력이다. 현재 범위는 PRD COR015가 우선한다. 이번 후속 변경은 코드·테스트 정리 및 정적 검토만 하며 실행 검증은 금지되어 있다.
|
||||||
|
- L13/L14 레거시 팬톡, L26~L30 큐레이션·탭, L37~L39 레거시 커뮤니티의 이름 표시 적용을 제거한다. API·기존 번역·권한·저장·알림 업무는 유지한다.
|
||||||
|
- L17/L18/L24/L31의 공유 조회는 호출자별로 구분한다. 제외된 탭/큐레이션은 원본 이름을 받고, 다른 API의 언어별 이름과 제목·본문 번역은 유지한다. 원본 경계는 KO 언어를 의미하지 않는다.
|
||||||
|
- `LegacyCreatorNameRenderer`는 제거하고 필요한 소비자는 `CreatorLocalizedNameService`를 직접 주입한다. T22 래퍼 전용 테스트는 제거하되 실제 core batch의 SQL 수·빈 입력·역할·현재 원본 fallback 회귀는 core 테스트에 보존한다.
|
||||||
|
- T04/T17의 레거시 별칭 기대값은 원본 경계에 맞춰 최소 보정한다. 검색·프로필·후원·기타 유지 대상의 별칭 테스트, characterization·번역·권한 테스트, v2 테스트는 삭제하지 않는다. 불필요한 테스트 설정 import와 제거된 생성자 인자만 정리한다.
|
||||||
|
|
||||||
|
## 단계와 계약
|
||||||
|
|
||||||
|
- 기준: 같은 디렉터리 `prd.md`의 NAME002~006 및 COR012, `plan-task.md`의 P2-T1/P2-T2.
|
||||||
|
- 현재 단계는 **legacy GREEN 소스 연결 및 정적 계약 감사, 부모 직렬 검증 대기**다. 아래 첫 테스트 단계 기록은 보존하며 구현 연결 결과는 마지막 절에 누적한다. runtime PASS나 제품 완료 기록이 아니다.
|
||||||
|
- 첫 단계에서는 `src/main`을 수정하지 않았다. 후속 부모 지시로 이번에는 legacy 소스·constructor fixture·추가 회귀를 작성했다. main PRD/plan, foundation/v2/async 소스, 운영 DDL은 수정하지 않았고 Gradle·커밋·푸시도 수행하지 않았다.
|
||||||
|
- 범위: non-v2 현재 앱 HTTP 표시·검색. `content/main/tab/replay`는 라이브 오디오 콘텐츠 탭이므로 포함한다.
|
||||||
|
- 별도 slice: `fcm/*`, `live/*`, `message/*`와 비동기 발송. `admin/*`, `creator/admin/*`, `partner/*`, auth/login 원본 이름은 수정 제외한다. 공유 `Series` 엔티티를 읽는 앱 서비스는 포함하되 creator-admin 응답은 바꾸지 않는다.
|
||||||
|
- 공통 구현은 `kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameService.findLocalizedNames(Collection<Long>, Lang): Map<Long, String>`이다. JA 활성 CREATOR만 이름 map에 포함하고 나머지는 기존 원본을 사용한다.
|
||||||
|
- 응답 조립 직전 실제 owner member IDs를 모아 batch 조회한다. `LangContext`는 HTTP 경계에서만 읽고 내부 호출에는 명시 `Lang`을 전달한다. 행별 이름 조회, `Member.nickname` 변이, principal 변이, 공통 entity getter, 전역 Jackson 변환은 금지한다.
|
||||||
|
- 공개 DTO field 이름·타입은 보존한다. content/series/cheers/post/order/receipt/character ID를 이름 map key로 쓰지 않는다. owner ID가 없는 projection은 내부 record로 보충하고 HTTP에는 새 필드를 노출하지 않는다.
|
||||||
|
- 검색은 요청 언어와 무관하게 기존 nickname 조건 **OR 활성 JA alias EXISTS**다. alias는 owner `member_id`, `lang=JA`, `is_active=true`, owner `role=CREATOR`로 상관시킨다. 제목·소개·본문·태그·장르 조건과 기존 visibility/block/requester/count/page 조건은 바꾸지 않는다.
|
||||||
|
|
||||||
|
## 이번에 작성한 테스트와 RED 인계
|
||||||
|
|
||||||
|
아래 경로는 저장소 루트 기준이다. 모두 기존 실제 API/repository를 호출하며 미래 production helper나 reflection 없이 동작한다. 별칭 fixture는 회원을 flush한 뒤 `JdbcTemplate`으로 `creator_localized_name(member_id, lang, display_name, is_active, created_at, updated_at)`에 넣는다. foundation의 entity 매핑으로 생성되는 테스트 테이블을 사용하며 수동 CREATE TABLE을 하지 않는다. USER/EN 행은 관리 API로 생성할 수 없는 오염 데이터까지 읽기 경계가 방어하는지 검사하는 fixture다.
|
||||||
|
|
||||||
|
| Owner | 실제 테스트 파일 | 사례 수 | 보호하는 동작 / 의도된 RED |
|
||||||
|
|---|---|---:|---|
|
||||||
|
| T01 | `src/test/kotlin/kr/co/vividnext/sodalive/search/SearchRepositoryLocalizedNameTest.kt` | 5 | original OR JA alias, containsIgnoreCase, 동일 회원 양쪽 매칭 중복 없음, 중복 별칭 owner 분리, list/count/page 일치, soft clear 및 동일 행 재활성화. creator 활성/role/태그 활성/양방향 차단, content 성인/활성/duration/limited/차단, series 성인/episode 성인/활성/duration/차단/조인 중복 유지. content/series title 불변 및 series owner와 episode owner 구분. 미구현 alias list/count assertion에서 실패해야 한다. |
|
||||||
|
| T02 | `src/test/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepositoryLocalizedNameTest.kt` | 2 | KO 채널 검색에서도 JA alias, 원본 유지, CREATOR/self/isActive 필터 및 중복 없음, soft clear/EN 제외와 재활성화. 미구현 alias-only hit에서 실패해야 한다. |
|
||||||
|
| T03 | `src/test/kotlin/kr/co/vividnext/sodalive/member/MemberRepositoryLocalizedNameTest.kt` | 3 | CREATOR requester의 USER 원본 매칭 유지, USER requester의 USER 제외, ADMIN/AGENT/BOT/self 제외, USER+AI_CHARACTER 별칭 제외, soft clear/EN 제외 및 원본 변경 후 재활성화. CREATOR alias hit 누락에서 실패해야 한다. 해당 repository에는 현재 count API가 없으므로 가상의 count API를 만들지 않는다. |
|
||||||
|
| T04 | `src/test/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerLocalizedNameEndToEndTest.kt` | 3 | 실제 MockMvc HTTP: JA profile/detail 이름, CREATOR 팬톡 부모/답글과 USER 작성자 혼합, KO/EN 원본, 원본 변경 뒤 별칭 유지 및 clear/missing fallback. `/search` creator `title`+`nickname` JA, content/series 제목 불변·실제 owner 이름 JA, KO `/search/creators` alias hit. 기존 원본 vs 별칭 응답 assertion에서 실패해야 한다. |
|
||||||
|
| T05 | `src/test/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepositoryLocalizedNameTest.kt` | 2 | 실제 `getLatestContentByTheme(locale=ja)`가 content ID와 다른 owner ID로 JA creatorNickname을 반환, USER 유지, KO/EN 원본, 제목 불변. 공개/미래/성인/활성/duration/차단 필터 유지와 원본 변경 후 soft clear fallback. 현재 원본 vs 별칭 ownername assertion에서 실패해야 한다. |
|
||||||
|
|
||||||
|
### 부모 실행 명령
|
||||||
|
|
||||||
|
아래 명령은 **아직 실행하지 않았다**. foundation 파일 저장 및 foundation 검증이 끝난 뒤 부모가 순서대로 실행한다. RED는 compile/context/fixture 오류가 아니라 위 요구 동작 assertion 실패여야 한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.search.SearchRepositoryLocalizedNameTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.explorer.ExplorerQueryRepositoryLocalizedNameTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.member.MemberRepositoryLocalizedNameTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.content.AudioContentRepositoryLocalizedNameTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.explorer.ExplorerLocalizedNameEndToEndTest'
|
||||||
|
```
|
||||||
|
|
||||||
|
T01/T02는 기존 constructor로 QueryDSL repository를 직접 만든다. GREEN에서 dependency가 추가되면 테스트 constructor도 명시적으로 맞춘다. T03/T05는 JPA slice다. GREEN에서 공통 service/adapter dependency를 주입할 경우 해당 slice의 `@Import`를 명시적으로 보완한다. T04는 `@SpringBootTest`+MockMvc이며 기존 `EmbeddedRedisInitializer` opt-in, rollback 트랜잭션과 독립 H2 DB를 사용한다. T05도 독립 H2 DB로 owner/resource ID fixture 충돌을 피한다.
|
||||||
|
|
||||||
|
## 전체 source edit map
|
||||||
|
|
||||||
|
아래 모든 소스 경로의 공통 prefix는 `src/main/kotlin/kr/co/vividnext/sodalive/`다. 각 셀은 그 prefix 아래 **정확한 경로**이며 wildcard 변경 지시가 아니다. `CURRENT`는 요청 시 현재 이름 조회, `NEUTRAL-CACHE`는 언어 중립 응답 캐시, `RAW`는 원본 편집·인증 경계, `IMMUTABLE`은 기존 역사 값/발송물을 뜻한다. Test owner T01~T05는 이번에 작성했고 T06 이후는 후속 GREEN 전에 추가할 명시적 테스트 소유자다.
|
||||||
|
|
||||||
|
### 검색·회원·탐색
|
||||||
|
|
||||||
|
| ID | 실제 경로와 심볼 / caller | 응답 타입·이름 및 실제 owner | 유형·언어 / 최소 수정 경계 | Test owner |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| L01 | `search/SearchRepository.kt`: `searchCreatorList`, `searchCreatorTotalCount`; `search/SearchService.kt`: `searchUnified`, `searchCreatorList`; `search/SearchController.kt` | `SearchResponseItem.id=member.id`; **creator branch의 `title`와 `nickname` 모두 이름** | CURRENT. list/count에 같은 alias EXISTS. service 응답 조립에서 Lang별 batch map으로 두 이름 field만 대체. | T01, T04; 기존 `search/SearchServiceTest.kt` constructor 회귀 |
|
||||||
|
| L02 | 같은 `search/SearchRepository.kt`: `searchContentList`, `searchContentTotalCount`; `search/SearchService.kt`: `searchContentList`, `searchUnified` | `SearchResponseItem.id=audioContent.id`; `nickname=audioContent.member.nickname`. **owner ID projection 없음** | CURRENT. 원본 nickname branch만 OR alias로 확장. 내부 owner ID projection 추가; content title/detail/theme/hashTag predicates·adult/gender/duration/limited/block/count/group/page 유지. | T01, T04 |
|
||||||
|
| L03 | 같은 `search/SearchRepository.kt`: `searchSeriesList`, `searchSeriesTotalCount`; `search/SearchService.kt`: `searchSeriesList`, `searchUnified` | `id=series.id`; `nickname=series.member.nickname`. **owner ID projection 없음**, episode owner와 series owner는 다를 수 있음 | CURRENT. series owner 상관 alias; episode content visibility/gender 조건과 title/introduction/genre/keyword/group/count/page 유지. 내부 owner ID 필요. | T01, T04 |
|
||||||
|
| L04 | `explorer/ExplorerQueryRepository.kt`: `getSearchChannel`; `explorer/ExplorerService.kt`: `getSearchChannel`; `explorer/ExplorerController.kt` | `GetRoomDetailUser.nickname`, 반환 Member의 `id` | CURRENT. query original OR alias, 활성 CREATOR/self 제외 유지; service의 양방향 차단 후 응답 mapping에서 batch. DTO 파일이 `live/room/detail`에 있어도 전역 constructor는 변경하지 않는다. | T02, T04 후속 channel HTTP 사례 |
|
||||||
|
| L05 | `member/MemberRepository.kt`: `MemberQueryRepositoryImpl.findByNicknameAndOtherCondition`; `member/MemberService.kt`: `searchMember`; `member/MemberController.kt` | 혼합 `GetRoomDetailUser.nickname`, 결과 Member `id` | CURRENT. creator alias만 OR, ADMIN/AGENT/BOT/self 제외 및 requester USER의 USER 제외 유지. 현재 query에 isActive/count 조건은 없으므로 이번 작업에서 임의 추가하지 않음. service 양방향 block 유지·배치 표시. | T03, T06 |
|
||||||
|
| L06 | `member/MemberRepository.kt`: `getMemberProfile`; `member/MemberService.kt`: `getMemberProfile` | `GetMemberProfileResponse.nickname`; 대상 `memberId`가 owner, `myMemberId`는 차단 조건 | CURRENT. 기존 대상 ID로 batch하여 응답 대체. | T06 |
|
||||||
|
| L07 | `member/MemberService.kt`: `getMyPage`; `member/myPage/MyPageResponse.kt` | `MyPageResponse.nickname`, 인증 Member `id` | CURRENT display. principal을 바꾸지 말고 DTO에서만 alias 적용. raw editor의 `ProfileResponse`와 구분. | T06 |
|
||||||
|
| L08 | `member/block/BlockMemberRepository.kt`: `getBlockedMemberList`; `member/MemberService.kt`: `getBlockedMemberList` | `GetBlockedMemberListItem.nickname`, blockedMember `id` | CURRENT 혼합 목록. blocked member owner IDs로 batch, 기존 block 활성·member 활성/count 유지. | T06 |
|
||||||
|
| L09 | `explorer/ExplorerService.kt`: `getExplorer`, `getCreatorRank`; `explorer/ExplorerQueryRepository.kt`: `getCreatorRankings`, `getNewCreators`, `findCreatorByGender` | `GetExplorerSectionCreatorResponse.nickname`, 해당 Member `id` | CURRENT. `CreatorRanking`은 순위/member 참조만 저장하며 nickname snapshot 아님. 기존 `Member.toExplorerSectionCreator` 전역 변경 대신 service 조립 DTO에서 batch. | T04 후속 섹션 사례; 기존 `explorer/ExplorerServiceTest.kt` |
|
||||||
|
| L10 | `explorer/ExplorerService.kt`: `getCreatorProfile`, `getCreatorDetail` | `CreatorResponse.nickname`, `GetCreatorDetailResponse.nickname`, `creatorAccount.id` | CURRENT. 기존 `langContext.lang`을 명시 Lang로 공통 service에 전달. profile creator와 중첩 하위 service 결과를 각각 실제 owner로 처리. 이름을 넣는 blocked-access 메시지도 같은 creator 표시 계약 대상, auth 식별값 아님. | T04 |
|
||||||
|
| L11 | `explorer/ExplorerQueryRepository.kt`: `getSimilarCreatorList`, `getRandomCreatorList`; `explorer/ExplorerService.kt`: `getCreatorProfile` | `SimilarCreatorResponse.nickname`, `userId=후보 creator member.id` | CURRENT. 이미 owner ID 보유. profile assembly에서 batch; random/order/tag/fallback query 로직 변경 없음. | T04 후속 similar creator 사례 |
|
||||||
|
| L12 | `explorer/ExplorerQueryRepository.kt`: `getFollowerList`; `explorer/ExplorerService.kt`: `getFollowerList` | `GetFollowerListDto` / `GetFollowerListResponseItem.nickname`; `userId=팔로워 member.id`, creatorId 아님 | CURRENT 혼합 목록. role/ID 보유; follower DTO 조립에서 batch. count·block·follow/notify 의미 유지. | T04 후속 follower 사례 |
|
||||||
|
| L13 | `explorer/ExplorerQueryRepository.kt`: `getCheersList`; `explorer/ExplorerService.kt`: `getCreatorProfileCheers`, `getCreatorProfile` | `GetCheersResponseItem.nickname`, 부모·답글 각각 `memberId=writer.id`; `cheersId/creatorId` 아님 | CURRENT 혼합 작성자. 부모·답글 ID를 한 번에 모아 batch, `removeDeletedNicknamePrefix()` 원본 fallback 유지. 날짜·parent·block·count·pagination 유지. | T04 |
|
||||||
|
| L14 | `explorer/ExplorerService.kt`: `writeCheers`, `modifyCheers` | HTTP `CreatorChannelFanTalkResponse.writerNickname`, `creatorReplies[].writerNickname`; 각각 writer member ID | CURRENT HTTP 쓰기 응답. 공유 v2 `from` factory 전역 수정하지 말고 legacy caller에서 응답 대체; async event는 후속 slice. | T04 후속 쓰기 응답 사례; 기존 `explorer/ExplorerServiceTest.kt`, `explorer/ExplorerControllerTest.kt` |
|
||||||
|
|
||||||
|
### 홈·랭킹·추천·캐시
|
||||||
|
|
||||||
|
| ID | 실제 경로와 심볼 / caller | 응답 타입·이름 및 실제 owner | 유형·언어 / 최소 수정 경계 | Test owner |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| L15 | `api/home/HomeService.kt`: `fetchData`; `rank/RankingRepository.kt`: `getCreatorRankings` | `GetHomeResponse.creatorRanking[].nickname`, 순위 Member `id` | CURRENT. HomeService에 이미 `LangContext` 있음. follow state 유지, home의 creator DTO만 batch. | T07 |
|
||||||
|
| L16 | `rank/RankingService.kt`: `getCreatorRanking`, `fetchCreatorBySellContentCountRankTop20`, `fetchCreatorBySeriesRevenueRankTop20`; `rank/RankingRepository.kt`: 같은 이름의 query | explorer creator DTO 또는 `ContentCreatorResponse.creatorNickname`, `creatorId=member.id` | CURRENT 순위 materialization/집계의 **현재 이름**. 순위 데이터가 과거 기간 기준이어도 query가 현재 member를 join하므로 이름은 역사 snapshot 아님. service에서 batch. | T08 |
|
||||||
|
| L17 | `rank/RankingRepository.kt`: `getAudioContentRanking`, `fetchCreatorContentBySalesCountTop4`, `fetchCreatorContentByLikeCountTop4`, `fetchFreeContentByCreatorIdTop4`; `rank/RankingService.kt`: 대응 메서드 | `GetAudioContentRankingItem.creatorNickname`, `creatorId=member.id`; `contentId`는 별도 | CURRENT. repository에서 title 번역만 기존 locale로 처리, service에서 owner 이름 batch. `api/home/HomeService.getContentRankingBySort`와 모든 content tab caller까지 도달. | T08, T07, T09 |
|
||||||
|
| L18 | `rank/RankingService.kt`: `getSeriesRanking`, `getSeriesAllRankingByGenre`, `getCompleteSeriesRanking`, `fetchCreatorSeriesBySales`, `seriesToSeriesListItem`; `rank/RankingRepository.kt`: 대응 query | `GetSeriesListResponse.SeriesListItem.creator.nickname`, `series.member.id`, `seriesId` 아님 | CURRENT. 공통 series response assembly에서 batch, creator-admin Series entity·writer/studio 변경 없음. | T08, T09 |
|
||||||
|
| L19 | `query/recommend/RecommendChannelQueryRepository.kt`: `getRecommendChannelList`; `query/recommend/RecommendChannelQueryService.kt`: `getRecommendChannel`; `api/home/HomeService.kt`: `fetchData` | `RecommendChannelResponse.creatorNickname`, `channelId=member.id`; contentList resource ID와 별도 | NEUTRAL-CACHE. `@Cacheable(default)` key는 `recommendChannel:{memberId 또는 guest}:{isAdult}:{contentType}`로 Lang 없음. cached raw response는 공유 DTO 변이 없이 **cache hit 이후 caller에서 copy+batch**하여 현재 이름 적용. 캐시 안에 JA를 저장하거나 cached DTO를 mutate하면 KO/JA 오염. 이미 존재하는 content title locale cache 문제와 이름 변경을 혼동하지 않음. | T07, T10 |
|
||||||
|
| L20 | `explorer/profile/CreatorDonationRankingQueryRepository.kt`: `getMemberDonationRanking`; `explorer/profile/CreatorDonationRankingService.kt`: `getMemberDonationRanking`; `explorer/ExplorerService.kt`: profile/donation ranking | `MemberDonationRankingResponse.nickname`, **donor 응답 `userId`**(query record는 `memberId`), 순위 대상 creatorId 아님 | NEUTRAL-CACHE. query→response→Redis list write→cache hit 반환 양쪽 확인. key `creator_donation_ranking_v2:{creatorId}:{period}:{offset}:{limit}:{withDonationCan}`, TTL 다음 월요일, Lang 없음. legacy 응답 조립에서 hit/miss 모두 donor IDs로 copy+batch. count 캐시는 숫자뿐이므로 이름 변경 없음. v2 adapter consumer는 별도 slice. | T11, T04 후속 donor 사례 |
|
||||||
|
| L21 | `explorer/ExplorerQueryRepository.kt`: `getMemberDonationRanking` | `MemberDonationRankingResponse.nickname`, donor `account.id` | CURRENT. 현재 ExplorerService는 L20의 service를 호출하므로 이 메서드는 legacy direct query 경로의 남은 raw projection이다. 실제 앱 caller가 추가되지 않으면 불필요한 독립 수정은 하지 않음. | T11, source caller 확인 |
|
||||||
|
|
||||||
|
L19/L20의 cache hit에서는 alias map에 값이 없을 때 cached nickname을 곧바로 **현재 원본**이라고 간주하면 안 된다. 원본 변경 후 soft clear가 일어나면 cached 원본도 stale할 수 있다. 후속 구현은 cache hit 이후 owner IDs로 현재 Member 원본을 batch 재조회하는 경계(`member/MemberRepository.kt`의 scalar `findNicknamesByIds`)를 두고 현재 원본 fallback과 alias map을 함께 응답에 적용한다. 캐시 entry/과거 record를 재작성하지 않는다. T10/T11에 원본 변경→alias clear→같은 cache hit의 current fallback 사례를 포함한다.
|
||||||
|
|
||||||
|
### 오디오·시리즈·메인 탭·주문·플레이리스트
|
||||||
|
|
||||||
|
| ID | 실제 경로와 심볼 / caller | 응답 타입·이름 및 실제 owner | 유형·언어 / 최소 수정 경계 | Test owner |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| L22 | `content/AudioContentService.kt`: `getDetail`; `content/AudioContentRepository.kt`: `findByIdAndActive`; `explorer/ExplorerQueryRepository.kt`: `getMember` | `GetAudioContentDetailResponse.creator.nickname` / `AudioContentCreator`, `audioContent.member.id` | CURRENT. 상세 조립에서 creator owner batch, 기존 title/detail/theme 번역과 접근/구매/visibility 유지. | T12; 기존 `content/AudioContentServiceTest.kt` |
|
||||||
|
| L23 | `content/AudioContentRepository.kt`: `getLatestContentByTheme`; `content/AudioContentService.kt`: `getLatestContentByTheme`; `api/home/HomeService.kt`: latest/free/point/recommend buckets | `AudioContentMainItem.creatorNickname`, `creatorId=member.id`, contentId와 별도 | CURRENT. query method가 `locale`를 이미 받으며 translated-title tuple와 locale-null projection 두 경로 모두 이름 대응 필요. tuple 응답 조립에 명시 Lang의 batch map 사용; service/홈 caller는 현재 locale 전달 유지. title 번역과 owner name 분리. | T05, T07 |
|
||||||
|
| L24 | `content/AudioContentRepository.kt`: `findByTheme`, `findAlarmContentByTheme`, `findByThemeFor2Weeks`, `findAudioContentByCurationId`, `findAudioContentByCurationIdV2`, `findContentByCurationId`, `getAudioContentRanking`; `content/main/AudioContentMainService.kt`: `getNewContentByTheme`, `getNewContentFor2WeeksByTheme`, `getAudioContentMainBannerList`, `getAudioContentCurationListWithPaging`; `content/AudioContentService.kt`: `getAudioContentRanking` | `GetAudioContentMainItem.creatorNickname` / `GetAudioContentRankingItem.creatorNickname`, `creatorId=member.id` | CURRENT. 각 owner ID는 기존 projection 보유. 해당 service assembly에서 batch 또는 명시 Lang를 query 응답 조립까지 전달. 필터/count/정렬 변경 금지. | T09, T12; T05는 대표 query이고 이 행 전체 검증을 대체하지 않음 |
|
||||||
|
| L25 | `content/AudioContentRepository.kt`: `getNewContentUploadCreatorList`; `content/main/AudioContentMainService.kt`: `getNewContentUploadCreatorList` | `ContentCreatorResponse.creatorNickname`, `creatorId=member.id` | CURRENT. service에서 batch, creator별 contentList 기존 로직 유지. | T09 |
|
||||||
|
| L26 | `content/main/curation/AudioContentCurationQueryRepository.kt`: `findByCurationId`; `content/main/curation/AudioContentCurationService.kt`: `getCurationContent`, `getContentCurationList` | `GetAudioContentMainItem.creatorNickname`, `creatorId=member.id` | CURRENT. curation ID를 owner로 쓰지 않음. service에서 batch; active/genre/adult/block/count 유지. | T09 |
|
||||||
|
| L27 | `content/main/tab/content/ContentMainTabTagCurationRepository.kt`: `getTagCurationContentList`; `content/main/tab/content/ContentMainTabTagCurationService.kt`: `getTagCurationContentList` | `GetAudioContentMainItem.creatorNickname`, `creatorId=member.id` | CURRENT. tag curation content query caller에서 명시 Lang+batch. | T09 |
|
||||||
|
| L28 | `content/main/tab/AudioContentMainTabRepository.kt`: `findCreatorByThemeContent`, `findCreatorWithHasFreeContent` | `ContentCreatorResponse.creatorNickname`, `creatorId=member.id` | CURRENT. caller는 `content/main/tab/asmr/AudioContentMainTabAsmrService.kt`, `content/main/tab/replay/AudioContentMainTabLiveReplayService.kt`, `content/main/tab/free/AudioContentMainTabFreeService.kt`의 `fetchData`. 배치 표시 후 creator별 인기 콘텐츠는 L17을 재사용. | T09 |
|
||||||
|
| L29 | `content/main/tab/RecommendSeriesRepository.kt`: `getNewSeriesList`, `getRecommendSeriesList`; `content/main/tab/free/AudioContentMainTabFreeService.kt`, `content/main/tab/series/AudioContentMainTabSeriesService.kt`: `fetchData` | `GetRecommendSeriesListResponse.creatorNickname`, `creatorId=series.member.id` | CURRENT. 추천 row의 ID/seriesId가 아닌 creatorId로 service batch. | T09 |
|
||||||
|
| L30 | `content/main/tab/home/AudioContentMainTabHomeService.kt`, `content/main/tab/content/AudioContentMainTabContentService.kt`, `content/main/tab/alarm/AudioContentMainTabAlarmService.kt`, `content/main/tab/series/AudioContentMainTabSeriesService.kt` 및 L28의 세 service: `fetchData`와 개별 content/rank/creator/series 조회 메서드 | 중첩 content creatorNickname / series.creator.nickname / ContentCreatorResponse.creatorNickname | CURRENT. 각 nested producer L17/L18/L24~29/L31 적용 누락 확인. 기존 반환된 공통 DTO를 재로컬라이즈하거나 parent/tab ID로 lookup하지 않음. 별도 wildcard 리팩터링 없음. | T09 |
|
||||||
|
| L31 | `content/series/ContentSeriesRepository.kt`: `getSeriesListV2`, `getSeriesByGenreListV2`, `getRecommendSeriesListV2`, `getOriginalAudioDramaList`, `findByCurationIdV2`, `getDayOfWeekSeriesListV2`; `content/series/ContentSeriesService.kt`: `getSeriesList`, `getSeriesListByGenre`, `getRecommendSeriesList`, `getOriginalAudioDramaList`, `fetchSeriesByCurationId`, `getDayOfWeekSeriesList` | `GetSeriesListResponse.SeriesListItem.creator.nickname`, `creatorId=series.member.id`; seriesId는 resource | CURRENT. **V2라는 메서드 이름이어도 non-v2 파일의 레거시 caller이므로 포함**. 기존 locale은 series title 번역용. 각 반환 목록 조립에서 owner batch; title/genre/price/adult/block/count/요일/정렬 유지. | T13, T07, T09; 기존 `content/series/main/SeriesMainControllerTest.kt` |
|
||||||
|
| L32 | `content/series/ContentSeriesService.kt`: `getSeriesDetail`; `content/series/ContentSeriesRepository.kt`: `getSeriesDetail` | `GetSeriesDetailResponse.creator.nickname`, `series.member.id` | CURRENT. 상세 DTO 조립에서 batch. `Series.writer`, `studio`, title/introduction/keyword는 creator nickname 아님. | T13 |
|
||||||
|
| L33 | `content/comment/AudioContentCommentRepository.kt`: `findByContentId`, `getAudioContentCommentReplyList`; `content/comment/AudioContentCommentService.kt`: `getCommentList`, `getCommentReplyList`; `content/AudioContentService.kt`: `getDetail` | `GetAudioContentCommentListItem.nickname`, `writerId=comment.member.id` | CURRENT 혼합 작성자. parent/child 각 writer ID batch; 상세의 commentList도 포함. deleted-prefix fallback, active/block/secret/count/page 유지. blocked-by-creator HTTP 에러의 이름도 creator owner로 표시, FCM args는 제외. | T14, T12 |
|
||||||
|
| L34 | `content/order/OrderRepository.kt`: `getAudioContentOrderList`, `getAudioContentMainOrderList`; `content/order/OrderService.kt`: 대응 메서드 | `GetAudioContentOrderListItem.creatorNickname` / `GetAudioContentMainItem.creatorNickname`, audioContent owner `creatorId` | CURRENT. 주문 날짜는 역사지만 이름은 query가 현재 Member를 join함. service 응답 batch, KEEP/RENTAL/expiry/isActive/count/page 유지. | T15 |
|
||||||
|
| L35 | `content/order/OrderRepository.kt`: `findBuyerListByContentId`; `content/AudioContentService.kt`: `getDetail`; `content/GetAudioContentDetailResponse.kt`: `ContentBuyer` | `ContentBuyer.nickname`, **order.member.id가 buyer owner**, 현재 projection은 nickname/profile만 있음 | CURRENT 혼합 구매자. content creator ID로 조회하면 오동작. 내부 buyer member ID record를 추가하고 상세 assembly batch, 공개 ContentBuyer 타입 보존. | T12, T15 |
|
||||||
|
| L36 | `content/AudioContentRepository.kt`: `fetchContentForPlaylist`; `content/playlist/AudioContentPlaylistService.kt`: `getPlaylistDetail`; `content/playlist/AudioContentPlaylist.kt`: `AudioContentPlaylistContent` | `AudioContentPlaylistContent.creatorNickname`, **projection owner ID 없음**; `id=content.id` | CURRENT. Redis playlist는 content IDs/order/endDate만 저장. getPlaylistDetail에서 SQL 현재 이름 재조회하므로 historical name snapshot 아님. 내부 memberId 추가하여 query→service batch. Redis 저장 record 이름 rewrite/새 필드 없음. | T16 |
|
||||||
|
|
||||||
|
### 커뮤니티·후원·사용 내역·캐릭터 작성자·오디션
|
||||||
|
|
||||||
|
| ID | 실제 경로와 심볼 / caller | 응답 타입·이름 및 실제 owner | 유형·언어 / 최소 수정 경계 | Test owner |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| L37 | `explorer/profile/creatorCommunity/CreatorCommunityRepository.kt`: `getCommunityPostList`, `getCommunityPost`, `getLatestPostListFromCreatorsYouFollow`; `explorer/profile/creatorCommunity/CreatorCommunityService.kt`: `getCommunityPostList`, `getCommunityPostDetail`, `getLatestPostListFromCreatorsYouFollow`; `explorer/profile/creatorCommunity/SelectCommunityPostResponse.kt`: `toCommunityPostListResponse` | `GetCommunityPostListResponse.creatorNickname` / `SelectCommunityPostResponse.creatorNickname`, post owner `creatorId` | CURRENT. 본문 번역과 별개로 이름 batch. postId/firstComment writer를 owner와 혼동하지 않음. paid mask/adult/pin/block/count 유지. 상세·구매 시 blocked message의 creator 이름도 HTTP current-display. | T17 |
|
||||||
|
| L38 | `explorer/profile/creatorCommunity/CreatorCommunity.kt`: `toCommunityPostListResponse`; `explorer/profile/creatorCommunity/CreatorCommunityService.kt`: `purchasePost` | 같은 `GetCommunityPostListResponse.creatorNickname`, entity `member.id` | CURRENT 구매 HTTP 응답. entity builder의 모든 호출을 전역 변경하지 말고 legacy service의 반환 DTO에서 override. v2/admin consumers는 별도 scope. | T17 |
|
||||||
|
| L39 | `explorer/profile/creatorCommunity/comment/CreatorCommunityCommentRepository.kt`: `findByPostId`, `getCommunityCommentReplyList`; `explorer/profile/creatorCommunity/CreatorCommunityService.kt`: `getCommunityPostCommentList`, `getCommentReplyList` 및 post list/detail의 firstComment | `GetCommunityPostCommentListItem.nickname`, `writerId=comment.member.id` | CURRENT mixed writer. 부모·reply·firstComment 각각 writer IDs batch, deleted-prefix/secret/block/count/page 유지. | T17 |
|
||||||
|
| L40 | `explorer/profile/channelDonation/ChannelDonationMessageRepository.kt`: `getChannelDonationMessageList`; `explorer/profile/channelDonation/ChannelDonationService.kt`: `getChannelDonationList`, `getChannelDonationListForProfile` | `GetChannelDonationListItem.nickname`, **donor `memberId`**, creatorId는 목록 filter | CURRENT mixed donor. 기존 secret/default message/삭제 prefix/adult/visibility/cursor 유지, donor member IDs batch. HTTP recipient render이며 FCM 전달은 제외. | T18 |
|
||||||
|
| L41 | `can/CanRepository.kt`: `CanQueryRepositoryImpl.getCanUseStatus`; `can/UseCanQueryDto.kt`; `can/CanService.kt`: `getCanUseStatus` | `GetCanUseStatusResponseItem.title` 안 room/audio/community/recipient creator name. `UseCanQueryDto.id=useCan.id`; **각 owner ID 없음** | CURRENT receipt: 현재 member를 join하며 이름 snapshot column 없음. `qRoomMember.id`, `qAudioContentMember.id`, `qCommunityPostMember.id`, `qRecipientMember.id` 내부 projection을 추가하고 service가 title를 만들기 전에 한 번 batch. payment gateway/refund/amount/distinct/page·resource title 우선 fallback 유지. live 이름도 이 HTTP receipt의 조회는 포함하되 `live/*` production 수정은 하지 않음. | T19 |
|
||||||
|
| L42 | `chat/character/comment/CharacterCommentService.kt`: `listComments`, `getReplies`, `getLatestComment`, `toCommentResponse`, `toReplyResponse`; `chat/character/comment/CharacterCommentDto.kt` | `CharacterCommentResponse.memberNickname`, `CharacterReplyResponse.memberNickname`, entity writer `member.id` | CURRENT mixed writer. characterId는 owner 아님. 부모·답글 목록+original 및 latest에서 writer IDs batch하고 Lang 명시 전달. 캐릭터 자체 `name`/본문 번역은 변경 없음. | T20 |
|
||||||
|
| L43 | `audition/applicant/AuditionApplicantRepository.kt`: `getAuditionApplicantList`; `audition/applicant/AuditionApplicantService.kt`: `getAuditionApplicantList`; `audition/applicant/AuditionApplicantController.kt` | `GetAuditionRoleApplicantItem.nickname`, **applicant memberId**, applicantId는 resource | CURRENT mixed applicant. service assembly batch, applicant/member 활성·role filter·vote count·sort/page 유지. admin audition은 제외. | T21 |
|
||||||
|
|
||||||
|
## 원본·불변·비이름 경계
|
||||||
|
|
||||||
|
| 경로와 심볼 | 분류와 결정 | 보존 검증 owner |
|
||||||
|
|---|---|---|
|
||||||
|
| `member/Member.kt`: `nickname`, `toExplorerSectionCreator`; `member/MemberAdapter.kt`; `member/MemberService.kt`: `loadUserByUsername` | RAW. entity/principal 원본 불변. `toExplorerSectionCreator`는 앱/관리/기타 공용이므로 이름 getter/global mapper를 변형하지 않고 앱 caller의 DTO를 변경한다. | T04, T06; 기존 Member 서비스·보안 테스트 |
|
||||||
|
| `member/MemberController.kt`: `getMember` (`GET /member`), `changeNickname`, profile PUT; `member/MemberService.kt`: `getMember`, `updateNickname`, `profileUpdate`; `member/ProfileResponse.kt`, `member/ProfileUpdateRequest.kt` | RAW EDITOR 경계. GET·PUT가 동일 ProfileResponse를 쓰고 nickname editing input을 제공한다. 기존 raw 응답/입력을 임의 로컬라이즈하지 않는다. 외부 client가 GET을 표시에도 쓰는지는 EXT001 확인 전 blocked이며 서버 구현 완료로 raw editor 안전을 증명하지 않는다. 별도 current display는 L06/L07. | T06; 외부 EXT001 |
|
||||||
|
| `member/MemberRepository.kt`: `findByNickname`, `existsByNickname`, `findNicknamesWithPrefix`; `member/MemberService.kt`: `duplicateCheckNickname`, `validateNickname`; `member/nickname/NicknameGenerateService.kt` | RAW 식별·중복·생성. alias search 대상은 `findByNicknameAndOtherCondition`만, raw unique 검사나 자동 닉네임 생성은 변경 없음. | T06 |
|
||||||
|
| `member/nickname/NicknameChangeLog.kt`: `prevNickname`; `member/MemberService.kt`: `updateNickname` | IMMUTABLE 변경 전 이름 기록. 기존 row rewrite 없음. 별칭 해제 시 과거 이름이 아니라 현재 Member.nickname으로 fallback. | T03, T04 |
|
||||||
|
| `member/login/LoginResponse.kt`; `member/MemberService.kt`: login/signup/findOrRegister; `member/auth/*`, `member/social/*`, `admin/*`, `creator/admin/*`, `partner/*` | RAW/제외. login·인증·원본 식별·관리/파트너 계약을 바꾸지 않는다. role+AI_CHARACTER인 CREATOR 표시는 포함, AI_CHARACTER kind만인 USER는 불변. | T03; 기존 auth/admin 테스트 |
|
||||||
|
| `chat/character/service/ChatCharacterCreatorMemberService.kt`: `ensureAiCharacterCreatorMember`, `syncAiCharacterCreatorMemberDisplayFields`, `syncDisplayFields` | RAW 동기화. ChatCharacter.name에서 원본 Member.nickname을 만든다. 별칭은 별도 테이블이므로 이 producer를 로컬라이즈하거나 원본 sync를 중단하지 않음. | 기존 `chat/character/service/ChatCharacterCreatorMemberServiceTest.kt`, `ChatCharacterCreatorMemberServiceIntegrationTest.kt` |
|
||||||
|
| `content/AudioContentService.kt`: `publishContentUploadedAfterCommit`; `explorer/profile/creatorCommunity/CreatorCommunityService.kt`: `publishCommunityPostCreatedAfterCommit` | 신규 v2 following event producer. 과거 following rows는 rewrite하지 않는다. v2 producer/consumer inventory slice의 책임이며 이 단계에서 원본 args를 변형하지 않음. | v2 following slice |
|
||||||
|
| `content/AudioContentService.kt`: uploadComplete/releaseContent; `explorer/ExplorerService.kt`: saveNotice; community/comment FcmEvent args | 새 async recipient payload는 이후 P3 slice. HTTP 이름 변경을 FCM title/args 변경으로 확대하지 않음. | async slice |
|
||||||
|
| `content/AudioContentRepository.kt`: `findByCreatorId`, other/previous/next-content; `content/series/content/ContentSeriesContentRepository.kt`; content/series banner, theme/category/tag/genre title | 비이름 field. `GetAudioContentListItem`, `OtherContentResponse`, series-content item에는 creator nickname 없음. existing content title/본문/character.name·Series.writer/studio는 creator localized-name으로 대체하지 않음. | T01, T04, T05, T12, T13 |
|
||||||
|
| L19/L20 cached nickname, Redis playlist detail | NEUTRAL-CACHE/current lookup, **과거 이름 snapshot 면제 아님**. cache hit 뒤 표시 대체하고 shared cache DTO 원본 유지. playlist 이름은 SQL current 조회. | T10, T11, T16 |
|
||||||
|
|
||||||
|
## 후속 테스트 소유자
|
||||||
|
|
||||||
|
아래 경로의 공통 prefix는 `src/test/kotlin/kr/co/vividnext/sodalive/`다. `신규 예정` 파일은 **아직 생성하지 않았다**. 이번 대표 15개 사례가 inventory 전체 GREEN 검증을 대신하지 않으며 해당 표면을 구현하기 전에 이 소유자의 RED 사례를 보강한다.
|
||||||
|
|
||||||
|
| Owner | 정확한 테스트 경로 | 상태·핵심 사례 |
|
||||||
|
|---|---|---|
|
||||||
|
| T06 | `member/MemberServiceTest.kt`, `member/MemberControllerTest.kt`; 신규 예정 `member/MemberLocalizedNameEndToEndTest.kt` | 기존 두 파일 있음. profile/myPage/search/block mixed USER, raw ProfileResponse·nickname edit·auth 불변. |
|
||||||
|
| T07 | `api/home/HomeServiceTest.kt`; 신규 예정 `api/home/HomeLocalizedNameEndToEndTest.kt` | 기존 있음. creator ranking·중첩 content/series/recommend/current names, guest KO/JA. |
|
||||||
|
| T08 | 신규 예정 `rank/RankingLocalizedNameEndToEndTest.kt` | creator/content/series/current owner 및 period 결과·title 불변. |
|
||||||
|
| T09 | 신규 예정 `content/main/LegacyContentMainLocalizedNameEndToEndTest.kt` | main/curation/tag/free/content/home/alarm/asmr/replay/series 탭의 creator와 ownernames. |
|
||||||
|
| T10 | 신규 예정 `query/recommend/RecommendChannelLocalizedNameCacheTest.kt` | 같은 member KO→JA→KO cache hit, alias update 후 current name, 원본 변경+clear 후 현재 원본 fallback, cached raw DTO 불변. |
|
||||||
|
| T11 | 신규 예정 `explorer/profile/CreatorDonationRankingLocalizedNameCacheTest.kt` | donor CREATOR/USER 혼합, cache hit/miss와 Lang 순서·current alias 갱신·원본 변경+clear fallback, count cache 불변. |
|
||||||
|
| T12 | `content/AudioContentServiceTest.kt`; 신규 예정 `content/AudioContentLocalizedNameEndToEndTest.kt` | 기존 있음. 상세 creator·mixed buyers·comments, owner ID와 contentId 구분, 원본/번역 title 유지. |
|
||||||
|
| T13 | 신규 예정 `content/series/ContentSeriesLocalizedNameEndToEndTest.kt`; 기존 `content/series/main/SeriesMainControllerTest.kt` | 목록/genre/recommend/original/curation/dayOfWeek/detail ownernames, writer/studio/title 불변. |
|
||||||
|
| T14 | 신규 예정 `content/comment/AudioContentCommentLocalizedNameEndToEndTest.kt` | mixed 부모/답글 및 상세 nested 목록, deleted original fallback/secret/block/page. |
|
||||||
|
| T15 | 신규 예정 `content/order/OrderLocalizedNameEndToEndTest.kt` | 주문 목록 현재 creator와 상세 mixed buyer, rental/expiry/count/public schema. |
|
||||||
|
| T16 | 신규 예정 `content/playlist/AudioContentPlaylistLocalizedNameEndToEndTest.kt` | Redis metadata는 그대로, detail SQL 이름만 current owner alias·원본 fallback·순서. |
|
||||||
|
| T17 | `explorer/profile/creatorCommunity/CreatorCommunityServiceTest.kt`, `CreatorCommunityTranslationEndToEndTest.kt`; 신규 예정 `explorer/profile/creatorCommunity/CreatorCommunityLocalizedNameEndToEndTest.kt` | 기존 두 파일 있음. post/detail/following/firstComment/comment/reply mixed owners·본문 번역 유지. |
|
||||||
|
| T18 | `explorer/profile/channelDonation/ChannelDonationServiceTest.kt`, `ChannelDonationMessageRepositoryTest.kt`; 신규 예정 `explorer/profile/channelDonation/ChannelDonationLocalizedNameEndToEndTest.kt` | 기존 두 파일 있음. donor aliases는 channel creator와 독립; USER·secret/default text 불변. |
|
||||||
|
| T19 | `can/CanServiceTest.kt`, `can/CanControllerIntegrationTest.kt`; 신규 예정 `can/CanReceiptLocalizedNameEndToEndTest.kt` | 기존 두 파일 있음. recipient/audio/community/room owner ID 구분, title 우선순위와 원본 resource title·receipt 금액 유지. |
|
||||||
|
| T20 | 신규 예정 `chat/character/comment/CharacterCommentLocalizedNameEndToEndTest.kt` | mixed comment/reply/latest/original, character name과 writer creator name 구분. |
|
||||||
|
| T21 | 신규 예정 `audition/applicant/AuditionApplicantLocalizedNameEndToEndTest.kt` | CREATOR/USER applicant owner, applicantId≠memberId, likes/count/order 유지. |
|
||||||
|
|
||||||
|
## 정적 확인과 남은 검증
|
||||||
|
|
||||||
|
- `rg`로 non-v2 nickname/projection 사용을 전체 검색하고 실제 caller/service/DTO를 읽어 목록에 연결했다. 추가 발견은 blocked member, audition applicant, buyerList, recommend series, receipt의 owner ID 누락이다.
|
||||||
|
- foundation `CreatorLocalizedNameEntity.kt`의 실제 table/column names와 fixture SQL을 대조했다. `JA` enum NAME·member FK·timestamp 값을 일치시켰다.
|
||||||
|
- 신규 테스트의 130자 초과/trailing whitespace 검색 결과 0건. production helper, reflection, 강제 실패 placeholder는 추가하지 않았다.
|
||||||
|
- Gradle/test/build/lint와 실제 HTTP 실행은 의도적으로 미실행이다. 부모의 serialized RED가 다음 검증 단계이며 이 문서는 RED 성공 또는 GREEN 완료를 주장하지 않는다.
|
||||||
|
- 현재 도구 목록에 `lsp_diagnostics`가 없어 Kotlin LSP 진단을 수행하지 못했다. 컴파일/context 오류가 보이면 부모 RED에서 먼저 수정하고 요구 동작 assertion 실패를 별도로 기록해야 한다.
|
||||||
|
- 외부 EXT001 raw editor/client display/cache 계약과 운영 DDL/MySQL collation은 이 테스트 단계에서 검증하지 않았다. 기존 문서의 승인 이전 문구를 덮어쓰지 않고 COR012를 기준으로 삼았다.
|
||||||
|
|
||||||
|
## 후속 GREEN 구현 연결 기록
|
||||||
|
|
||||||
|
### 실행 근거와 상태
|
||||||
|
|
||||||
|
- 부모 제공 RED: `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/creatorname-consumer-red.log`. foundation 이후 기존 consumer 테스트가 실행됐고 별칭 hit/표시 assertion 실패를 확인했다. 이 worker는 Gradle을 실행하지 않았다.
|
||||||
|
- HTTP fixture 오류도 별도로 확인했다. `build/test-results/test/TEST-kr.co.vividnext.sodalive.explorer.ExplorerLocalizedNameEndToEndTest.xml`의 profile와 unified 요청은 `success=false`, 로그인 오류와 `data=null`이다. stack은 `MemberContentPreferenceService.initializeDefaultPreference`→`resolveForQuery(REQUIRES_NEW)`다. 미커밋 viewer가 별도 트랜잭션에서 보이지 않았으며 JSON field명이 잘못된 것이 아니다. `TestTransaction`으로 fixture를 먼저 커밋하고 다음 검증 트랜잭션을 시작하도록 수정했다. 별칭/원본/title/owner ID 기대값을 완화하지 않았다.
|
||||||
|
- 부모 lint 로그 `creatorname-foundation-final-lint.log`에 나온 5개 신규 테스트의 JDBC 인자를 각각 별도 줄로 수정했다. main 새 import 순서와 130자 길이도 정적으로 대조했다.
|
||||||
|
- `v2/creatorname/application/LegacyCreatorNameRenderer.kt`는 명시 Lang의 페이지 batch/copy와 중립 캐시의 현재 원본+별칭 map을 제공한다. 내부에서 foundation의 정확한 `findLocalizedNames(Collection<Long>, Lang)`를 호출한다. 요청 컨텍스트/기본-null service/전역 serializer/Member 변이는 없다.
|
||||||
|
- `v2/creatorname/adapter/out/persistence/CreatorNamePredicates.kt`의 owner 상관 `EXISTS`를 SearchRepository 6개 list/count 조건, Explorer channel, MemberQuery 검색에 적용했다. 원본 containsIgnoreCase 및 기존 다른 검색 조건·필터·group/count/page는 유지했다.
|
||||||
|
|
||||||
|
### L01~L43 연결 결과
|
||||||
|
|
||||||
|
| Inventory | 구현 경계 / 정적 판단 | runtime 상태 |
|
||||||
|
|---|---|---|
|
||||||
|
| L01~L03 | SearchService가 Lang을 명시 전달. creator title+nickname 대체. `search/SearchResponse.kt`의 `SearchNameRow`에 content/series 실제 ownerId를 내부 projection으로 추가하고 공개 SearchResponseItem으로 변환. | 부모 GREEN 대기 |
|
||||||
|
| L04~L08 | Explorer/Member 검색·타 회원 profile·myPage·blocked list의 response assembly에 batch/copy 적용. 원본 편집 GET/PUT·login·nickname 식별/변경은 기존 raw 그대로. | 부모 GREEN 대기 |
|
||||||
|
| L09~L14 | 탐색 섹션, profile/detail/blocked error, similar creator, follower, 부모/답글 cheers, fanTalk 쓰기 응답을 owner별 batch/copy로 연결. profile 내 live manager는 같은 profile creator의 이름을 복사하되 live 소스/저장 객체는 수정하지 않음. | 부모 GREEN 대기 |
|
||||||
|
| L15~L18 | Home creator ranking, RankingService의 creator/content/series 공통 assembly와 각각의 Top4/Top20 소비자를 연결. 순위/가격/수량/소유/요일/title fields는 그대로. | 부모 GREEN 대기 |
|
||||||
|
| L19~L20 | HomeService와 ExplorerService에서 neutral cache hit 뒤 현재 Member 원본을 batch 재조회하고 alias를 합쳐 새 DTO로 반환. recommend/donation cache producer·key·TTL·저장 entry는 그대로. | 부모 GREEN 대기 |
|
||||||
|
| L21 | `ExplorerQueryRepository.getMemberDonationRanking`의 실제 앱 caller가 없음을 `rg`로 다시 확인. 현재 앱은 cached CreatorDonationRankingService를 사용하므로 사용되지 않는 query를 별도로 수정하지 않음. | 연결 제외: 사용되지 않는 projection |
|
||||||
|
| L22~L23 | Audio 상세 creator/comment/buyer IDs 합집합, latest-content locale query의 owner IDs로 batch. title/detail/theme 번역은 기존 로직. `ContentBuyerRow`는 내부 projection이며 공개 ContentBuyer에는 ID를 추가하지 않음. | 부모 GREEN 대기 |
|
||||||
|
| L24~L30 | main/theme/curation/tag 및 alarm/content/free/asmr/replay/series 탭에 전체 응답 owner IDs batch 적용. Home tab은 이미 수정된 RankingService를 재사용하여 별도 중복 렌더링하지 않음. cached main 목록은 controller에서 cache hit 뒤 처리한다. | 부모 GREEN 대기 |
|
||||||
|
| L31~L32 | ContentSeriesService의 모든 목록·genre/recommend/original/curation/dayOfWeek/detail 경계를 연결. 공유 repository/Series entity/admin output을 전역 변경하지 않음. | 부모 GREEN 대기 |
|
||||||
|
| L33 | Audio 댓글/답글은 명시 Lang 인자를 추가. 앱 AudioContentCommentController만 요청 Lang을 전달하고 기존 shared ADMIN 호출은 기본 KO로 raw 이름 보존. 상세 nested comments도 포함. | 부모 GREEN 대기 |
|
||||||
|
| L34~L36 | Order response assembly, 내부 ContentBuyerRow, 내부 PlaylistContentRow로 실제 owner IDs 확보. Redis playlist metadata 및 공개 playlist/buyer DTO는 그대로. | 부모 GREEN 대기 |
|
||||||
|
| L37~L39 | Community 목록/detail/following/purchase, firstComment, 댓글/답글을 연결. 댓글 shared ADMIN consumer가 있어 기본 KO/앱 controller 명시 Lang 경계를 분리. entity/v2 DTO factory 및 번역/paid mask는 그대로. | 부모 GREEN 대기 |
|
||||||
|
| L40~L43 | donor 목록, Can internal DTO의 room/audio/community/recipient IDs, character comment/reply/latest writers, audition applicant memberIds를 batch로 연결. receipt title 우선순위·금액·resource 제목, USER 이름, count/page/filter는 그대로. | 부모 GREEN 대기 |
|
||||||
|
|
||||||
|
### GREEN 감사에서 추가 확인한 캐시·공유 경계
|
||||||
|
|
||||||
|
- L24/L25의 `content/main/AudioContentMainService.kt`는 `newContentUploadCreatorList`와 `getAudioContentCurationListWithPaging`의 raw creator 이름 포함 결과를 `default` cache에 저장한다. `content/main/AudioContentMainController.kt`에서 cache hit 뒤 전체 owner IDs의 현재 이름을 조회해 복사했다.
|
||||||
|
- `content/AudioContentService.kt.getAudioContentRanking`은 Lang 없는 `contentRanking` key와 `cache_ttl_3_days`에 이름 포함 결과를 저장한다. `content/AudioContentController.kt`에서 cache hit 뒤 현재 이름을 batch/copy 처리했다.
|
||||||
|
- L24에서 사용하는 `content/theme/AudioContentThemeService.kt.getContentByTheme`에도 이름 포함 item이 있어 연결했다. theme/title 자체의 번역 계약은 변경하지 않았다.
|
||||||
|
- v2 ADMIN `AiCharacterAdminCommunityPostFacade.comments/commentReplies`, `AiCharacterAdminAudioContentFacade.comments/commentReplies`가 legacy comment 서비스를 실제 재사용한다. 이 service 읽기의 기본 Lang은 KO이고 앱 controller만 요청 Lang을 명시한다. v2 ADMIN 파일을 수정하거나 route/principal로 언어를 추론하지 않았다.
|
||||||
|
- 기존 neutral cache의 콘텐츠 제목/날짜 locale 문제나 visibility 수명은 이번 이름 변경으로 확장 수정하지 않았다. aliases와 현재 원본의 표시만 cache 밖에서 갱신한다.
|
||||||
|
|
||||||
|
### 소유한 FCM 호출 metadata
|
||||||
|
|
||||||
|
`delivery-name-inventory.md` #9/#10/#11/#12/#14/#15의 6곳에 `titleMemberId`를 명시했다. Explorer notice/community post/comment는 실제 작성 member ID, audio upload/release는 audio owner ID, audio reply는 `parent?.member?.id ?: member.id`다. 기존 title/args/content 제목·deepLink·senderMemberId는 그대로다. 특히 reply snapshot의 sender는 실제 commenter이며 title owner와 다를 수 있다. 중앙 FcmEvent/async/live/message/v2 소스는 수정하지 않았다.
|
||||||
|
|
||||||
|
### 추가 회귀와 constructor fixture
|
||||||
|
|
||||||
|
추가 테스트는 **아직 실행하지 않았다**. 이번 원래 15개와 아래 13개를 합쳐 대상 이름 회귀는 28개 실행 사례다. 새 회귀의 RED/GREEN을 관찰했다고 주장하지 않으며 부모가 별도 검증 기록을 남긴다.
|
||||||
|
|
||||||
|
| Owner | 정확한 테스트 파일 | 사례 / 보호 경계 |
|
||||||
|
|---|---|---|
|
||||||
|
| T22 | `src/test/kotlin/kr/co/vividnext/sodalive/v2/creatorname/application/LegacyCreatorNameRendererTest.kt` | 기존 2 + 리뷰 회귀 4: 실제 foundation/JPA batch의 1 select(작은/중복 큰 페이지), KO 0 alias select, USER/missing/원본 DTO 보존; neutral cache의 원본 변경+clear 현재 fallback. 현재 이름 경로는 서로 다른 1/4명에서 JA 2 SQL, KO/EN 1 SQL, entityLoadCount 0 및 빈 입력 0 SQL. |
|
||||||
|
| T23 | `src/test/kotlin/kr/co/vividnext/sodalive/v2/creatorname/application/LegacyCurrentNameReadIntegrationTest.kt` | 9: profile/myPage/blocked/raw editor, 주문+Redis playlist, mixed buyer 상세, community post/firstComment/reply+shared raw default, donor, receipt 실제 projection, character writers, applicant memberId, donor neutral Redis cache hit/원본 변경+clear/과거 DTO·cache entry 불변. |
|
||||||
|
| T24 | `src/test/kotlin/kr/co/vividnext/sodalive/content/comment/AudioContentCommentNameMetadataTest.kt` | 2 parameterized: parent CREATOR/commenter USER와 반대 조합. 실제 JPA/댓글 service를 호출하고 outbound event publisher만 capture하여 title owner≠snapshot sender, 원본 title 및 content title args/empty name slots 유지. |
|
||||||
|
|
||||||
|
기존 직접 constructor fixture는 새 dependency를 명시적으로 추가했다. legacy JPA slice에는 `src/test/kotlin/kr/co/vividnext/sodalive/support/LegacyCreatorNameTestConfiguration.kt`를 import했다. 이 configuration은 renderer, foundation service, adapter를 제공한다. 기존 isolated unit fixture의 `originalLegacyNameRenderer()`는 실제 renderer와 이름 없는 dependency doubles를 사용하며 production default-null 호환성 경로가 아니다.
|
||||||
|
|
||||||
|
소유 범위 밖의 v2/foundation/admin/partner/async JPA slice도 AudioContent custom repository를 스캔한다면 위 test configuration import가 필요하다. 해당 파일은 직접 수정하지 않았다. 직접 constructor가 있는 `v2/api/admin/aicharacter/community/LegacyCommunityPostCharacterizationTest.kt`와 `v2/api/admin/aicharacter/fantalk/LegacyFanTalkReplyCharacterizationTest.kt`도 소유자가 `nameRenderer` 인자를 명시해야 한다. 이 항목은 부모에 전달했으며 해당 소유자 수정 여부는 부모의 최종 compile에서 확인한다.
|
||||||
|
|
||||||
|
### 정확한 변경 소스 manifest
|
||||||
|
|
||||||
|
아래 공통 prefix는 `src/main/kotlin/kr/co/vividnext/sodalive/`다. 기존 row의 query producer는 current/raw 데이터를 유지하고 실제 response assembly만 수정한 경우도 포함해 연결 경계를 표시했다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
v2/creatorname/application/LegacyCreatorNameRenderer.kt
|
||||||
|
v2/creatorname/adapter/out/persistence/CreatorNamePredicates.kt
|
||||||
|
search/SearchRepository.kt
|
||||||
|
search/SearchResponse.kt
|
||||||
|
search/SearchService.kt
|
||||||
|
member/MemberRepository.kt
|
||||||
|
member/MemberService.kt
|
||||||
|
explorer/ExplorerQueryRepository.kt
|
||||||
|
explorer/ExplorerService.kt
|
||||||
|
explorer/profile/creatorCommunity/CreatorCommunityService.kt
|
||||||
|
explorer/profile/creatorCommunity/CreatorCommunityController.kt
|
||||||
|
explorer/profile/channelDonation/ChannelDonationService.kt
|
||||||
|
api/home/HomeService.kt
|
||||||
|
rank/RankingService.kt
|
||||||
|
content/AudioContentRepository.kt
|
||||||
|
content/AudioContentService.kt
|
||||||
|
content/AudioContentController.kt
|
||||||
|
content/GetAudioContentDetailResponse.kt
|
||||||
|
content/series/ContentSeriesService.kt
|
||||||
|
content/comment/AudioContentCommentService.kt
|
||||||
|
content/comment/AudioContentCommentController.kt
|
||||||
|
content/order/OrderRepository.kt
|
||||||
|
content/order/OrderService.kt
|
||||||
|
content/playlist/AudioContentPlaylist.kt
|
||||||
|
content/playlist/AudioContentPlaylistService.kt
|
||||||
|
content/main/AudioContentMainService.kt
|
||||||
|
content/main/AudioContentMainController.kt
|
||||||
|
content/main/curation/AudioContentCurationService.kt
|
||||||
|
content/main/tab/content/ContentMainTabTagCurationService.kt
|
||||||
|
content/main/tab/content/AudioContentMainTabContentService.kt
|
||||||
|
content/main/tab/free/AudioContentMainTabFreeService.kt
|
||||||
|
content/main/tab/alarm/AudioContentMainTabAlarmService.kt
|
||||||
|
content/main/tab/asmr/AudioContentMainTabAsmrService.kt
|
||||||
|
content/main/tab/replay/AudioContentMainTabLiveReplayService.kt
|
||||||
|
content/main/tab/series/AudioContentMainTabSeriesService.kt
|
||||||
|
content/theme/AudioContentThemeService.kt
|
||||||
|
can/CanRepository.kt
|
||||||
|
can/CanService.kt
|
||||||
|
can/UseCanQueryDto.kt
|
||||||
|
chat/character/comment/CharacterCommentService.kt
|
||||||
|
audition/applicant/AuditionApplicantService.kt
|
||||||
|
```
|
||||||
|
|
||||||
|
변경 테스트 manifest의 공통 prefix는 `src/test/kotlin/kr/co/vividnext/sodalive/`다. 기존 JPA 파일의 변경은 필요한 test configuration import만이며 본래 테스트 로직을 정리하거나 완화하지 않았다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
support/LegacyCreatorNameTestConfiguration.kt
|
||||||
|
v2/creatorname/application/LegacyCreatorNameRendererTest.kt
|
||||||
|
v2/creatorname/application/LegacyCurrentNameReadIntegrationTest.kt
|
||||||
|
content/comment/AudioContentCommentNameMetadataTest.kt
|
||||||
|
search/SearchRepositoryLocalizedNameTest.kt
|
||||||
|
search/SearchServiceTest.kt
|
||||||
|
explorer/ExplorerQueryRepositoryLocalizedNameTest.kt
|
||||||
|
explorer/ExplorerLocalizedNameEndToEndTest.kt
|
||||||
|
explorer/ExplorerServiceTest.kt
|
||||||
|
member/MemberRepositoryLocalizedNameTest.kt
|
||||||
|
member/MemberServiceContentPreferenceTest.kt
|
||||||
|
member/MemberServiceCacheEvictionTest.kt
|
||||||
|
content/AudioContentRepositoryLocalizedNameTest.kt
|
||||||
|
content/AudioContentServiceTest.kt
|
||||||
|
api/home/HomeServiceTest.kt
|
||||||
|
can/CanServiceTest.kt
|
||||||
|
explorer/profile/creatorCommunity/CreatorCommunityServiceTest.kt
|
||||||
|
explorer/profile/creatorCommunity/CreatorCommunityTranslationEndToEndTest.kt
|
||||||
|
explorer/profile/channelDonation/ChannelDonationServiceTest.kt
|
||||||
|
explorer/profile/channelDonation/ChannelDonationMessageRepositoryTest.kt
|
||||||
|
content/CreatorCommunityLanguageDetectTest.kt
|
||||||
|
content/CreatorCommunityLanguageDetectionCacheConcurrencyTest.kt
|
||||||
|
content/main/banner/AudioContentBannerRepositoryTest.kt
|
||||||
|
event/EventRepositoryTest.kt
|
||||||
|
member/contentpreference/MemberContentPreferenceIntegrationTest.kt
|
||||||
|
osiv/OsivLazyLoadingRegressionTest.kt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 부모 전용 GREEN 명령과 검증 한계
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.search.SearchRepositoryLocalizedNameTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.explorer.ExplorerQueryRepositoryLocalizedNameTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.member.MemberRepositoryLocalizedNameTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.content.AudioContentRepositoryLocalizedNameTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.explorer.ExplorerLocalizedNameEndToEndTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creatorname.application.LegacyCreatorNameRendererTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creatorname.application.LegacyCurrentNameReadIntegrationTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.content.comment.AudioContentCommentNameMetadataTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.search.SearchServiceTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.content.AudioContentServiceTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.explorer.ExplorerServiceTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityServiceTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.CreatorCommunityTranslationEndToEndTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.explorer.profile.channelDonation.ChannelDonationServiceTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.api.home.HomeServiceTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.member.MemberServiceContentPreferenceTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.member.MemberServiceCacheEvictionTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.can.CanServiceTest'
|
||||||
|
./gradlew ktlintCheck
|
||||||
|
./gradlew assemble
|
||||||
|
```
|
||||||
|
|
||||||
|
- 현재 확인은 source caller/constructor/DTO/owner ID/Lang 및 cache producer/consumer 대조다. public response field 이름·타입과 Member/principal/original editor 불변을 소스에서 점검했다.
|
||||||
|
- 변경 코드의 길이/후행 공백과 신규 테스트의 placeholder/Disabled/reflection을 `rg`로 점검했다. lint 로그의 인자 줄바꿈 수정은 실제 ktlint 재실행 PASS를 뜻하지 않는다.
|
||||||
|
- kapt가 생성할 `QSearchNameRow`, `QContentBuyerRow`, `QPlaylistContentRow` 및 변경 QUseCanQueryDto constructor는 부모 compile/test가 검증한다. LSP 도구는 제공되지 않았다.
|
||||||
|
- Gradle, build, lint, 실제 HTTP/SDK/Redis QA는 이번 GREEN worker에서 실행하지 않았다. 앱 전체/외부 cache/editor/RTC/provider 및 운영 DB는 기존 blocker를 유지한다.
|
||||||
|
|
||||||
|
## COR013: DB 직접 이름 JOIN 전환
|
||||||
|
|
||||||
|
### 근거와 검증 상태
|
||||||
|
|
||||||
|
- 기준은 PRD 10절과 JOIN-T1~JOIN-T3다. 위의 이전 JA 전용 batch 구현·검증 기록은 당시 이력이며 아래 분류가 현재 조회 경계다.
|
||||||
|
- 부모 제공 실제 RED 로그는 `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/creatorname-join-consumer-red.log`다. `SearchRepositoryLocalizedNameTest.shouldSearchContentOwnerAliasWithMatchingCountAndExistingContentFilters`의 표시 값은 맞았지만 standalone alias SELECT가 0이 아닌 1이었다. 해당 SQL assertion은 그대로 유지했다.
|
||||||
|
- 공통 `CreatorNameJoin(owner, lang, alias)`의 member ID·요청 Lang·활성 이름·CREATOR 조건을 ON으로 연결한다. 목록 SQL이 COALESCE 이름을 반환하며 count와 후보 ID 쿼리에는 표시 JOIN을 추가하지 않는다. JOIN별 alias는 owner별로 분리했다.
|
||||||
|
- 이 worker는 Gradle, 테스트, build, ktlint, 실제 HTTP를 실행하지 않았다. LSP 도구도 제공되지 않았다. 아래는 소스·호출자·fixture 연결 및 정적 점검 결과이며 runtime GREEN 판정은 부모의 직렬 실행에 남긴다.
|
||||||
|
|
||||||
|
### L01~L43 현재 분류
|
||||||
|
|
||||||
|
| Inventory | 현재 구현 / 실제 쿼리와 경계 |
|
||||||
|
|---|---|
|
||||||
|
| L01~L03 | `SearchRepository.searchCreatorList/searchContentList/searchSeriesList`가 요청 Lang JOIN으로 공개 `SearchResponseItem`을 직접 투영한다. creator title+nickname을 함께 선택하고 content/series title은 그대로다. `SearchNameRow`와 후속 alias batch를 제거했다. JA EXISTS 검색 및 count·필터·page는 유지한다. |
|
||||||
|
| L04~L06, L08 | `ExplorerQueryRepository.getSearchChannel`, `MemberQueryRepositoryImpl.findByNicknameAndOtherCondition/getMemberProfile`, `BlockMemberQueryRepositoryImpl.getBlockedMemberList`에서 표시 이름을 함께 선택한다. 검색 결과는 Member를 변이하지 않는 기존 `GetRoomDetailUser`로 반환한다. 앱 caller는 요청 Lang을 명시한다. |
|
||||||
|
| L07 | `MemberService.getMyPage`는 인증 principal의 raw Member를 그대로 사용하므로 DTO 경계 sparse batch를 유지한다. 원본 편집·login·nickname 식별은 raw다. |
|
||||||
|
| L09, L11~L13 | `ExplorerQueryRepository.getCreatorRankings/getNewCreators/findCreatorByGender/getSimilarCreatorList/getRandomCreatorList/getFollowerList/getCheersList`에 JOIN을 적용했다. creator DTO 및 cheers 부모·답글 이름은 쿼리에서 선택한다. cheers 답글은 페이지 부모 IDs의 별도 원래 데이터 조회에 같은 JOIN을 적용하며 단독 alias 조회를 하지 않는다. count·차단·부모/답글 노출 의미는 유지한다. |
|
||||||
|
| L10, L14 | profile/detail은 role·차단·노출 및 중첩 조립에 필요한 raw Member 경계, fanTalk 작성/수정은 저장 entity 반환 경계다. 기존 sparse batch로 HTTP DTO/차단 오류에만 표시한다. FCM 원본 args/title과 principal은 바꾸지 않는다. |
|
||||||
|
| L15~L18 | `RankingRepository.getCreatorRankings/getAudioContentRanking/getSeriesRanking/getCompleteSeriesRanking/getSeriesAllRankingByGenre/fetchCreatorBySellContentCountRankTop20/fetchCreatorContentBySalesCountTop4/fetchCreatorBySeriesRevenueRankTop20/fetchCreatorSeriesBySales/fetchFreeContentByCreatorIdTop4/fetchCreatorContentByLikeCountTop4`에서 이름을 선택한다. creator는 기존 DTO, series는 원본 entity+SQL 표시 이름 tuple로 조립하고 RankingService/HomeService의 이름 전용 batch를 제거했다. 집계·요일·후속 content count/new 판정은 그대로다. |
|
||||||
|
| L19~L20 | 추천 채널·donation ranking의 cache producer, DTO, key, TTL은 raw·언어 중립으로 유지한다. `HomeService.fetchData`, `ExplorerService.renderDonationRanking`이 cache hit 뒤 공통 단일 JOIN current-name batch를 호출한다. cached 원본을 현재 fallback으로 간주하지 않는다. |
|
||||||
|
| L21 | `ExplorerQueryRepository.getMemberDonationRanking`도 명시 Lang이 있으면 donor JOIN을 선택하도록 전환했다. 실제 앱 caller는 여전히 L20 cached service다. null Lang의 기존 raw 모드를 보존하고 새 caller는 만들지 않았다. |
|
||||||
|
| L22~L23 | Audio 상세는 권한·구매·번역에 필요한 원본 audio/member 경계의 creator sparse batch만 유지한다. buyer와 nested comment는 각 SQL에서 이름을 선택한다. `AudioContentQueryRepositoryImpl.getLatestContentByTheme`의 locale 경로는 같은 title tuple SQL에 JOIN을 추가했고 repository renderer 의존성을 제거했다. locale=null은 기존 raw query 모드다. |
|
||||||
|
| L24~L27 | `findByTheme/findAlarmContentByTheme/findByThemeFor2Weeks/findAudioContentByCurationId/findAudioContentByCurationIdV2/findContentByCurationId`, `AudioContentCurationQueryRepository.findByCurationId`, `ContentMainTabTagCurationRepository.getTagCurationContentList`가 명시 Lang JOIN을 지원한다. 앱 main/theme/curation/tag caller는 Lang을 전달하고 후속 이름 copy/batch를 제거했다. 중립 cache를 만드는 `findAudioContentByCurationId`의 null Lang과 `getAudioContentRanking` raw producer는 유지한다. |
|
||||||
|
| L25 | `getNewContentUploadCreatorList`는 이름 포함 중립 cache producer이므로 raw다. `AudioContentMainController`의 cache-hit 이후 current-name batch를 유지한다. |
|
||||||
|
| L28~L30 | `AudioContentMainTabRepository.findCreatorByThemeContent/findCreatorWithHasFreeContent`, `RecommendSeriesRepository.getNewSeriesList/getRecommendSeriesList`가 JOIN으로 반환한다. alarm/content/free/asmr/replay/series의 모든 직접 content/curation/creator/series caller에 Lang을 전달했고 탭 전체 응답을 다시 복사하던 alias batch를 제거했다. home 탭은 변경된 RankingService/ContentSeriesService를 그대로 재사용한다. |
|
||||||
|
| L31~L32 | `ContentSeriesQueryRepositoryImpl.getSeriesListV2/getSeriesByGenreListV2/getRecommendSeriesListV2/getOriginalAudioDramaList/findByCurationIdV2/getDayOfWeekSeriesListV2`의 기존 locale tuple SQL에 JOIN을 추가했다. 목록 service의 renderSeries를 제거했다. 상세는 접근/번역/가격/요일에 필요한 raw Series entity 경계의 sparse batch를 유지한다. writer/studio/title과 공용 entity는 그대로다. |
|
||||||
|
| L33 | `AudioContentCommentQueryRepositoryImpl.findByContentId/getAudioContentCommentReplyList`가 writer 이름을 선택한다. 앱 controller 및 Audio 상세는 요청 Lang, 기존 shared ADMIN 호출은 `lang=null`의 명시 raw 모드다. KO를 raw의 대용으로 사용하지 않는다. secret/block/count/page와 원본 deleted-prefix fallback은 보존한다. |
|
||||||
|
| L34~L36 | `OrderQueryRepositoryImpl.getAudioContentOrderList/getAudioContentMainOrderList/findBuyerListByContentId`, `AudioContentQueryRepositoryImpl.fetchContentForPlaylist`에서 이름을 선택한다. 공개 buyer/playlist DTO로 직접 투영해 `ContentBuyerRow`/`PlaylistContentRow`와 이름 전용 owner IDs·copy/batch를 제거했다. Redis playlist metadata, 순서, KEEP/RENTAL/expiry 조건은 그대로다. |
|
||||||
|
| L37~L39 | `CreatorCommunityQueryRepositoryImpl.getCommunityPostList/getCommunityPost/getLatestPostListFromCreatorsYouFollow`, `CreatorCommunityCommentQueryRepositoryImpl.findByPostId/getCommunityCommentReplyList`가 JOIN 이름을 선택한다. firstComment에도 요청 Lang을 전달한다. 목록/detail의 renderPosts를 제거했고 구매 쓰기 반환의 creator sparse batch만 유지한다. shared ADMIN 댓글 기본값은 null raw, 앱만 요청 Lang이다. 본문 번역·유료 마스크·secret/filter는 그대로다. |
|
||||||
|
| L40 | `ChannelDonationMessageQueryRepositoryImpl.getChannelDonationMessageList`가 donor entity+SQL 표시 이름 tuple을 반환한다. service의 donor alias batch를 제거했다. 원본 이름의 deleted-prefix fallback, secret/default message·날짜·정렬·count는 보존한다. |
|
||||||
|
| L41 | `CanQueryRepositoryImpl.getCanUseStatus`의 room/audio/community/recipient owner 4개에 각각 고유 JOIN을 연결했다. CanService는 받은 이름으로 기존 title 우선순위를 적용한다. 후속 이름 map, copy와 `UseCanQueryDto`의 이름 batch 전용 owner IDs를 제거했다. 금액·결제/refund/distinct/page 및 resource 제목은 그대로다. |
|
||||||
|
| L42 | `CharacterCommentQueryRepositoryImpl.findDisplayComments/findDisplayReplies/findDisplayComment`가 writer 이름을 같은 SQL에서 선택한다. list/latest 및 replies의 original도 이 query를 사용하며 service의 alias batch를 제거했다. 원본 entity를 변경하지 않고 기존 comment/reply DTO로 조립한다. cursor·active·정렬·reply count와 캐릭터 자체 이름은 그대로다. |
|
||||||
|
| L43 | `AuditionApplicantQueryRepositoryImpl.getAuditionApplicantList`가 applicant member JOIN 이름을 투영한다. service에 Lang을 명시하고 alias batch를 제거했다. vote 집계·role·활성·page 조건은 유지한다. |
|
||||||
|
|
||||||
|
### 남은 이름 batch 호출: 정확히 15곳
|
||||||
|
|
||||||
|
아래는 non-v2 담당 소스에서 `nameRenderer.findCurrentNames/findLocalizedNames/render`를 검색한 결과다. 직접 DB 목록을 이미 조회했다는 이유로 남긴 batch는 없다.
|
||||||
|
|
||||||
|
| 분류 | 실제 호출 위치 | 이유 |
|
||||||
|
|---|---|---|
|
||||||
|
| NEUTRAL-CACHE, current 5곳 | `api/home/HomeService.fetchData`; `explorer/ExplorerService.renderDonationRanking`; `content/main/AudioContentMainController.getNewContentUploadCreatorList/getAudioContentCurationListWithPaging`; `content/AudioContentController.getAudioContentRanking` | cache hit 이후 현재 원본+요청 Lang 별칭을 공통 단일 JOIN batch로 읽고 새 응답을 만든다. 원본 cache payload는 유지한다. |
|
||||||
|
| PRINCIPAL, sparse 1곳 | `member/MemberService.getMyPage` | 인증 principal을 변이하지 않고 표시 DTO만 렌더한다. |
|
||||||
|
| RAW ENTITY DETAIL, sparse 4곳 | `explorer/ExplorerService.getCreatorDetail/getCreatorProfile`; `content/AudioContentService.getDetail`; `content/series/ContentSeriesService.getSeriesDetail` | role/접근/구매/가격/번역 등 기존 비즈니스 조립에 필요한 raw entity의 이름을 덮어쓰지 않는다. |
|
||||||
|
| WRITE / ERROR, sparse 5곳 | `explorer/ExplorerService.writeCheers`의 차단 오류, `renderFanTalk`의 작성/수정 반환; `content/comment/AudioContentCommentService.registerComment`의 차단 오류; `explorer/profile/creatorCommunity/CreatorCommunityService.purchasePost`의 차단 오류 및 구매 반환 | 저장·검증에 사용한 raw entity와 기존 event args/title을 유지하고 HTTP 반환/오류만 렌더한다. |
|
||||||
|
|
||||||
|
별칭 표시가 없는 원본 인증/editor/admin/partner 및 역사 저장 이름은 기존 계약을 유지한다. FCM metadata 변경 이력은 위 절 그대로이며 COR013에서 해당 event title/args를 다시 수정하지 않았다.
|
||||||
|
|
||||||
|
### 이번 source manifest
|
||||||
|
|
||||||
|
공통 prefix는 `src/main/kotlin/kr/co/vividnext/sodalive/`다. 공유 core/helper/test support, v2, live/message/fcm, chat room repository 및 부모 소유 PRD/plan은 수정하지 않았다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
search/SearchRepository.kt
|
||||||
|
search/SearchResponse.kt
|
||||||
|
member/MemberRepository.kt
|
||||||
|
member/MemberService.kt
|
||||||
|
member/block/BlockMemberRepository.kt
|
||||||
|
explorer/ExplorerQueryRepository.kt
|
||||||
|
explorer/ExplorerService.kt
|
||||||
|
explorer/profile/creatorCommunity/CreatorCommunityRepository.kt
|
||||||
|
explorer/profile/creatorCommunity/CreatorCommunityService.kt
|
||||||
|
explorer/profile/creatorCommunity/comment/CreatorCommunityCommentRepository.kt
|
||||||
|
explorer/profile/channelDonation/ChannelDonationMessageRepository.kt
|
||||||
|
explorer/profile/channelDonation/ChannelDonationService.kt
|
||||||
|
api/home/HomeService.kt
|
||||||
|
rank/RankingRepository.kt
|
||||||
|
rank/RankingService.kt
|
||||||
|
content/AudioContentRepository.kt
|
||||||
|
content/AudioContentService.kt
|
||||||
|
content/GetAudioContentDetailResponse.kt
|
||||||
|
content/comment/AudioContentCommentRepository.kt
|
||||||
|
content/comment/AudioContentCommentService.kt
|
||||||
|
content/series/ContentSeriesRepository.kt
|
||||||
|
content/series/ContentSeriesService.kt
|
||||||
|
content/order/OrderRepository.kt
|
||||||
|
content/order/OrderService.kt
|
||||||
|
content/playlist/AudioContentPlaylist.kt
|
||||||
|
content/playlist/AudioContentPlaylistService.kt
|
||||||
|
content/main/AudioContentMainService.kt
|
||||||
|
content/main/curation/AudioContentCurationQueryRepository.kt
|
||||||
|
content/main/curation/AudioContentCurationService.kt
|
||||||
|
content/main/tab/AudioContentMainTabRepository.kt
|
||||||
|
content/main/tab/RecommendSeriesRepository.kt
|
||||||
|
content/main/tab/content/ContentMainTabTagCurationRepository.kt
|
||||||
|
content/main/tab/content/ContentMainTabTagCurationService.kt
|
||||||
|
content/main/tab/content/AudioContentMainTabContentService.kt
|
||||||
|
content/main/tab/free/AudioContentMainTabFreeService.kt
|
||||||
|
content/main/tab/alarm/AudioContentMainTabAlarmService.kt
|
||||||
|
content/main/tab/asmr/AudioContentMainTabAsmrService.kt
|
||||||
|
content/main/tab/replay/AudioContentMainTabLiveReplayService.kt
|
||||||
|
content/main/tab/series/AudioContentMainTabSeriesService.kt
|
||||||
|
content/theme/AudioContentThemeService.kt
|
||||||
|
can/CanRepository.kt
|
||||||
|
can/CanService.kt
|
||||||
|
can/UseCanQueryDto.kt
|
||||||
|
chat/character/comment/CharacterCommentRepository.kt
|
||||||
|
chat/character/comment/CharacterCommentService.kt
|
||||||
|
audition/applicant/AuditionApplicantRepository.kt
|
||||||
|
audition/applicant/AuditionApplicantService.kt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 이번 test caller manifest와 부모 직렬 검증
|
||||||
|
|
||||||
|
공통 prefix는 `src/test/kotlin/kr/co/vividnext/sodalive/`다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
search/SearchRepositoryLocalizedNameTest.kt
|
||||||
|
can/CanServiceTest.kt
|
||||||
|
api/home/HomeServiceTest.kt
|
||||||
|
osiv/OsivLazyLoadingRegressionTest.kt
|
||||||
|
explorer/profile/channelDonation/ChannelDonationMessageRepositoryTest.kt
|
||||||
|
explorer/profile/channelDonation/ChannelDonationServiceTest.kt
|
||||||
|
explorer/profile/creatorCommunity/CreatorCommunityServiceTest.kt
|
||||||
|
```
|
||||||
|
|
||||||
|
- Search 직접 constructor의 renderer 인자를 제거했다. 기존 standalone alias SELECT=0 assertion은 완화하지 않았고 같은 콘텐츠 사례에 EN 표시 이름 선택 및 EN 이름으로 검색하지 않는 JA 검색 계약 assertion을 추가했다.
|
||||||
|
- Can fixture의 제거된 내부 owner ID 인자와 renderer constructor 인자를 제거했다. Home/Community stub은 앱이 전달하는 실제 Lang을 명시했다. donation fixture는 entity+이름 tuple과 Lang matcher로 적응했다. 기존 name/filter/secret/USER/count/page 기대값은 유지했다.
|
||||||
|
- donation repository의 기존 fetch-member 사례는 `deleted_viewer3` 원본 entity가 그대로이고 SQL 표시 이름만 `viewer3`임을 함께 확인하도록 보강했다. OSIV 사례는 repository가 반환하는 기존 explorer DTO의 tags를 검증하며 세션 밖 사용 경계는 유지한다.
|
||||||
|
- `AudioContentQueryRepositoryImpl`은 더 이상 renderer 주입을 요구하지 않는다. 기존 JPA test configuration import는 다른 service/core fixture까지 함께 제공하는 경우가 있어 일괄 삭제하지 않았다. 다른 owner 소스·테스트를 수정해야 하는 확인된 컴파일 변경은 없다. 전체 compile과 v2 ADMIN의 null raw 호출 회귀는 부모가 확인한다.
|
||||||
|
- `rg`로 전환 query/caller, 제거된 내부 row/ID 참조, 15개 batch 경계, 추가 import 정렬·사용 및 130자/후행 공백을 대조했다. Kotlin compile/LSP 또는 runtime 검증을 대신하지 않는다.
|
||||||
|
- 추가 정적 delimiter 검사 대상 74개 repository/service 파일에서 괄호 불일치 0건, 담당 Kotlin 소스·변경 테스트의 130자 초과/후행 공백 검색 0건이었다. 삭제한 `SearchNameRow/ContentBuyerRow/PlaylistContentRow` 및 이름 batch 전용 Can owner ID의 Kotlin 참조도 0건이다.
|
||||||
|
|
||||||
|
아래는 **이 worker에서 실행하지 않은** 부모 직렬 검증 명령이다. 모든 parallel writer가 종료된 뒤 실행한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.search.SearchRepositoryLocalizedNameTest' --tests 'kr.co.vividnext.sodalive.content.AudioContentRepositoryLocalizedNameTest' --tests 'kr.co.vividnext.sodalive.explorer.ExplorerQueryRepositoryLocalizedNameTest' --tests 'kr.co.vividnext.sodalive.member.MemberRepositoryLocalizedNameTest' --offline --console=plain
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.explorer.ExplorerLocalizedNameEndToEndTest' --tests 'kr.co.vividnext.sodalive.v2.creatorname.application.LegacyCurrentNameReadIntegrationTest' --tests 'kr.co.vividnext.sodalive.explorer.profile.channelDonation.*' --tests 'kr.co.vividnext.sodalive.explorer.profile.creatorCommunity.*' --tests 'kr.co.vividnext.sodalive.can.CanServiceTest' --tests 'kr.co.vividnext.sodalive.api.home.HomeServiceTest' --tests 'kr.co.vividnext.sodalive.osiv.OsivLazyLoadingRegressionTest' --offline --console=plain
|
||||||
|
./gradlew test build ktlintCheck --continue --offline --console=plain
|
||||||
|
```
|
||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,233 @@
|
|||||||
|
# 크리에이터 이름 언어별 관리 PRD
|
||||||
|
|
||||||
|
> 문서 상태: COR022 레거시 메시지 전체롤백·검증 완료. 두운영파일HEAD일치, 전용projection/테스트3건 제거 후 전체2267건/실패0/오류0/skip0과lint통과, 실제HTTP23요청·발송이벤트2건·독립리뷰승인을 확인했다. 공통FCM/v2 DM은 유지하고 임시자원은 정리했다. 운영DB적용·배포·커밋·푸시는 하지 않았다.
|
||||||
|
|
||||||
|
## 문서 정보
|
||||||
|
|
||||||
|
| 항목 | 내용 |
|
||||||
|
|---|---|
|
||||||
|
| 작성일 / 최종 수정일 | 2026-10-14 / 2026-10-16 |
|
||||||
|
| 대상 제품 | 앱 내 크리에이터 표시 이름의 수동 언어별 관리 |
|
||||||
|
| 구현 계획 | `plan-task.md`, COR012 승인, 서버 구현 진행 기록 및 잔여 Gate 포함 |
|
||||||
|
| API Contract | COR012 승인, 구현 및 HTTP 테스트로 확인 |
|
||||||
|
| DDL 산출물 | `schema.sql` 초안 파일 작성 승인, DB 미적용·별도 승인 전 실행 금지 |
|
||||||
|
| 구현 승인 | COR012 승인. 운영 DB 적용·배포는 별도 승인 필요하며 외부 클라이언트 의존성은 차단 상태 |
|
||||||
|
|
||||||
|
## 1. 목표와 범위
|
||||||
|
|
||||||
|
운영자가 크리에이터의 일본어 이름을 수동 관리하고, 앱의 크리에이터 표시 이름을 요청 언어에 따라 제공한다. 관리 쓰기/API 범위는 JA만이다. 조회는 기존 `Lang` 전부를 사용하며, 해당 언어에 활성 이름이 있으면 적용하고 없으면 현재 원본 이름으로 fallback한다. KO/EN 관리 기능은 포함하지 않는다. 저장 구조는 기존 `Lang`을 재사용한다.
|
||||||
|
|
||||||
|
앱 크리에이터 표시 경로가 대상이되 COR017에서 사용자가 미사용이라고 확인한 AudioContentMainTab 계열은 이번 변경에서 완전히 제외한다. 탭 파일은 기존 HEAD와 같게 유지하며, 미사용 탭의 원본 닉네임 반환을 보장하기 위한 null 인자·별도 raw 모드·테스트는 요구하지 않는다. 공유 조회는 실제 사용 호출자의 언어별 이름과 제목 번역을 보존하며, 그 변경이 미사용 탭 결과에 간접 영향을 주는 것은 허용한다. AudioContentCuration 및 레거시 커뮤니티·팬톡은 COR015의 원본 이름 계약을 유지한다. API 자체와 기존 저장·권한·제목/본문 번역·알림 업무는 삭제하지 않는다. 그 밖의 레거시/v2 HTTP 및 v2 커뮤니티·팬톡은 유지한다. 사용자별 신규 전달은 각 수신자/세션 언어로 렌더링한다. 기존 언어별 역사 스냅샷과 이미 전달된 payload는 수정하지 않는다.
|
||||||
|
|
||||||
|
일반 USER 이름, `Member.nickname` 원본, 원본 닉네임 편집기, 로그인·인증·원본 식별 흐름은 변경하지 않는다. 자동 번역도 하지 않는다. JA 별칭은 원본 닉네임이 나중에 변경되어도 계속 우선한다. 일본어 별칭은 원본 닉네임 검색과 함께 검색 가능해야 한다.
|
||||||
|
|
||||||
|
원본은 언어가 아니라 현재 `Member.nickname`에 저장된 문자열이다. 한국어·일본어·영어 등 어떤 언어일 수도 있으며 `Lang.KO`와 동의어가 아니다. 요청 언어의 활성 CREATOR 별칭이 있으면 원본 문자열의 언어와 무관하게 별칭이 우선하고, 없거나 해제됐으면 현재 원본을 그대로 반환한다. 원본이 일본어여도 JA 별칭이 따로 등록되어 있으면 등록값을 우선한다. `Lang.KO` 기본값은 요청 언어의 기본값일 뿐 원본 언어 지정이 아니다. 원본 언어 필드나 자동 언어 감지는 추가하지 않는다.
|
||||||
|
|
||||||
|
COR019에서 `AudioContentMainController`와 `AudioContentMainService`의 이번 이름 변경도 제외한다. 두 파일은 HEAD와 같게 되돌리고, 이 서비스만 사용하는 `findByThemeFor2Weeks`/`findAudioContentByCurationId`의 이름 언어 인자·JOIN만 제거한다. 공유 `findByTheme`의 현지화는 다른 실제 사용 API 때문에 유지하며, 이를 제외 API에 한해 차단하는 별도 raw 모드는 추가하지 않는다. 기존 제목·테마 번역, 캐시, 권한·차단·성인 필터는 유지한다. 전용으로 추가된 테스트는 실제 없으므로 공유 테스트를 삭제하지 않는다.
|
||||||
|
|
||||||
|
COR020에서 `CharacterCommentService`/`CharacterCommentRepository`의 작성자 이름 현지화 변경도 제외한다. 두 파일을 HEAD 형태로 복원하고 댓글 목록·답글의 원댓글·최신 댓글은 기존 저장 nickname을 사용한다. 댓글 작성/답글/삭제/신고·권한·커서·건수·언어 감지 이벤트는 변경하지 않는다. 해당 이름 대체 기능 전용 통합테스트1개만 제거하며 배너·다른 이름 조회 테스트는 유지한다.
|
||||||
|
|
||||||
|
COR021에서 `ContentSeriesRepository`/`ContentSeriesService`의 이름 현지화 변경도 HEAD로 되돌린다. 저장소5개 목록 조회의 이름 JOIN 및 서비스 상세 이름 batch만 제거되며 기존 제목·소개·장르·요일 번역과 권한·필터·순서·페이지·건수는 보존한다. 이 서비스를 재사용하는 HomeService의 오리지널/요일별 목록과 ExplorerService의 프로필 시리즈 작성자 이름도 원본으로 돌아가는 영향을 포함한다. 공통 CreatorNameJoin/CreatorLocalizedNameService 및 별도 v2 시리즈 조회는 삭제하지 않는다. 관련 테스트1개에서는 이름 현지화 단언만 제거하고 제목 번역·원문 fallback 검증을 유지한다.
|
||||||
|
|
||||||
|
COR022에서 레거시 `/message`의 MessageService/MessageRepository 이름 변경 전체를 HEAD로 되돌린다. 문자·음성의 발신/수신/보관 목록은 기존 엔티티 조회와 원본 이름을 사용하고 차단 오류의 이름 치환도 제거한다. MessageListRow와 전용 현지화 테스트3건은 제거하되 API·발송·보관·삭제·권한·본문/시간/URL 처리 자체는 유지한다. 전체 롤백에는 문자·음성 FCM 이벤트의 nameArgumentMemberIds 추가분2곳 제거도 포함된다. 기존 senderMemberId와 공통 FCM 처리는 남으므로 푸시 본문은 원본 인자를 사용하되 알림 senderNicknameSnapshot은 수신 언어 별칭으로 저장될 수 있다. 이는 공통 알림 기능의 별도 계약이며 v2 DM/WS나 과거 snapshot까지 롤백하지 않는다.
|
||||||
|
|
||||||
|
운영자는 문자 종류 제한 없이 JA 이름을 입력하며, 비공백 값은 문자 변경이나 음역 없이 보존한다. 저장 길이는 255 code point이며, UTF-8 byte나 UTF-16 code unit이 아닌 Unicode code point 기준으로 센다. 임의 Unicode를 허용하고 255 초과 입력은 쓰기 전에 관리 경계에서 거부하며 자르지 않는다. 빈 문자열 저장은 JA 이름 해제다. 공백-only 해석, 중복 별칭의 DB 제약/회원 ID 처리, ADMIN route/대상 판정은 기술 제안이다.
|
||||||
|
|
||||||
|
인터뷰 명확도 자체 평가: 약 0.10. 차원별 점수는 Goal 1.0, Scope 0.9, Constraints 0.9, Success 0.9, Context 0.75다. 제품 질문은 정리됐으며 외부 시스템 경로·소유자 계약은 별도 blocker로 남는다.
|
||||||
|
|
||||||
|
## 2. 제품 요구사항
|
||||||
|
|
||||||
|
| ID | 상태 | 요구사항 및 수용 사례 |
|
||||||
|
|---|---|---|
|
||||||
|
| NAME001 | 확정 | 관리자가 CREATOR 회원의 JA 이름을 수동 저장·갱신·해제한다. 빈 값 저장은 해제다. |
|
||||||
|
| NAME002 | 확정 | JA 요청은 JA 별칭을 원본보다 우선하고, 미등록이면 현재 `Member.nickname`으로 fallback한다. 원본 변경 후에도 별칭을 유지한다. |
|
||||||
|
| NAME003 | COR022 범위 보정 | AudioContentMainTab/MainController/Service, CharacterCommentService/Repository, ContentSeriesService/Repository 및 레거시 message/MessageService/Repository의 이번 이름 변경을 제외하며 별도 호환 raw 모드를 만들지 않는다. AudioContentCuration/레거시 커뮤니티·팬톡의 기존 제외 계약과 나머지 앱 표면의 언어별 이름·USER 원본 보존은 유지한다. 공통FCM과 별도v2 커뮤니티·팬톡·시리즈·DM/WS는 적용 대상이다. |
|
||||||
|
| NAME004 | 확정 | JA 별칭은 요청 locale과 무관하게 원본 닉네임 검색과 함께 검색되고 기존 contains/collation 의미와 필터가 유지된다. |
|
||||||
|
| NAME005 | 확정 | 일반 USER 이름, 원본 닉네임 편집, 인증/식별 목적의 원본 사용을 변경하지 않는다. |
|
||||||
|
| NAME006 | 확정 | 기존 언어별 역사 스냅샷과 이미 발송된 payload는 변경하지 않는다. 현재 이름 조회는 요청 언어, 신규 수신자별 payload는 수신자/세션 언어를 따른다. |
|
||||||
|
| NAME007 | COR012 승인·구현 | 빈 문자열 및 공백-only는 별칭 해제로 처리한다. 그 밖의 입력 문자는 그대로 보존하며 자동 번역·음역은 없다. 저장 상한은 255 code point다. |
|
||||||
|
| NAME008 | COR014 승인 | 관리자 회원 페이지 목록·검색의 기존 `nickname`은 원본을 유지하고, `localizedNames: Map<String, String>`에 CREATOR의 모든 활성 언어별 등록 이름을 함께 반환한다. 키는 Lang.code, 미등록/해제 언어는 생략하며 미등록·일반 회원은 빈 map이다. 이름 필드는 Accept-Language로 하나를 선택하지 않는다. |
|
||||||
|
| NAME009 | COR014 승인 | 닉네임 전용 관리자 GET을 제거하고 기존 일본어 PUT의 등록·수정·소프트 해제를 유지한다. PUT의 저장 결과 응답은 유지하며 관리자 일반 콘텐츠 조회는 원본 닉네임을 유지한다. |
|
||||||
|
| NAME010 | COR018 명확화 | 원본 nickname은 언어 중립 값이다. 한국어·일본어·영어 원본 각각에 대해 요청 언어의 활성 별칭 우선/미등록·해제 시 현재 원본 fallback을 검증한다. 원본과 별칭 문자열이 같은 언어라는 이유로 별칭을 무시하지 않는다. |
|
||||||
|
|
||||||
|
### 핵심 수용 사례
|
||||||
|
|
||||||
|
| 조건 | 결과 |
|
||||||
|
|---|---|
|
||||||
|
| JA 별칭 등록 후 원본 닉네임 변경 | JA 표시/검색은 별칭을 유지하고, KO/EN은 해당 언어의 활성 이름이 있으면 적용하고 없으면 현재 원본을 사용 |
|
||||||
|
| 같은 JA 별칭을 두 CREATOR에 저장 | 중복 표시명 허용 및 `(member_id, lang)` 고유 기술 설계가 구현 승인될 경우 모두 저장, 결과 회원은 중복 행 없이 구분 |
|
||||||
|
| JA 별칭이 빈 문자열 | 기술 계획은 행/값을 보존하고 비활성화, 원본 이름 표시 및 원본 검색 지속; 미등록이면 no-op |
|
||||||
|
| 해제 후 비공백 별칭을 재저장 | 비활성 포함 member+lang 조회로 같은 ID의 행 갱신·재활성화, 반복해도 한 행 유지 |
|
||||||
|
| JA 별칭이 공백만 | 별칭 해제로 처리하는 기술 제안 승인 시 원본 표시/검색 |
|
||||||
|
| CREATOR와 USER가 한 작성자 목록에 함께 있음 | CREATOR만 이름 대체, USER는 원래 값 유지 |
|
||||||
|
| 같은 사용자의 KO/JA 채팅 세션 동시 연결 | 각각 해당 세션 언어에 맞는 신규 payload 수신 |
|
||||||
|
| 기존 언어별 스냅샷 또는 과거 발송 payload | 과거 값 불변 |
|
||||||
|
| 언어 중립 캐시/랭킹 materialization | 생산자와 소비자를 조사한 후 언어별 표시 요구 충족 여부를 분류 |
|
||||||
|
|
||||||
|
## 3. 외부 의존 및 차단
|
||||||
|
|
||||||
|
| ID | 외부 의존 | 담당 역할 | 영향 | 재개 조건 |
|
||||||
|
|---|---|---|---|---|
|
||||||
|
| EXT001 | 모바일/클라이언트 전체 표시 표면, locale 전달, client cache와 원본 이름 편집 흐름 | 모바일/클라이언트 소유자 | 서버 변경만으로 전체 앱 노출과 원본 편집 안전을 입증할 수 없음 | 표면 inventory, locale 계약, 캐시 무효화/수명, 원본 편집 경로 및 담당자 확인 |
|
||||||
|
| EXT002 | 운영자 관리 UI와 배포/인증 연결 | 운영도구/관리 UI 소유자 | 서버 관리 API만으로 운영자 사용자 흐름 완료 불가 | UI 저장소·담당자·인증 주체·배포 위치·연동 방식 확인 |
|
||||||
|
| EXT003 | RTC/live/chat 세션 locale 및 알림 수신자 언어 계약 | 클라이언트 및 메시징/알림 담당자 | 세션별 언어와 재연결 동작 확인 없이는 전달 언어를 보장할 수 없음 | locale 전달 방식, 변경 시 reconnect 계약, 수신자 언어 결정 시점과 end-to-end 검증 경로 확정 |
|
||||||
|
|
||||||
|
외부 경로는 범위에서 제외되지 않는다. 소유 저장소와 파일 경로가 확인되지 않은 외부 작업은 `[blocked]`로 남긴다. 서버 계획의 완료로 앱 전체 요구를 완료 처리하지 않는다.
|
||||||
|
|
||||||
|
## 4. 기술 조사와 구현 계약
|
||||||
|
|
||||||
|
테이블명과 문서·DDL 파일 작성 범위는 사용자 확정이다. `Lang`/소프트 lifecycle은 기존 수동 관리 패턴에 근거한 기술 결정이며 사용자가 삭제 방식을 직접 선택했다는 뜻이 아니다. COR012에서 서버 구현과 테스트가 승인됐다. API는 승인·구현됐고 운영 DB 적용은 별도 승인 대상이다.
|
||||||
|
|
||||||
|
### 별도 이름 테이블과 수동 관리 설계
|
||||||
|
|
||||||
|
기존 회원 테이블에 언어별 닉네임 컬럼을 추가하는 방식이 아니라, **크리에이터의 언어별 표시 이름만 저장하는 별도 테이블**을 설계한다. 테이블명은 사용자 확인으로 `creator_localized_name` 확정이다. DDL 초안은 같은 디렉터리의 `schema.sql`에 작성하며 DB에는 적용하지 않는다.
|
||||||
|
|
||||||
|
| 데이터 | 저장 위치와 의미 |
|
||||||
|
|---|---|
|
||||||
|
| 원본 닉네임 | 기존 `Member.nickname`, 변경하지 않음 |
|
||||||
|
| 크리에이터 회원 ID | 별도 테이블의 `member_id`, 기존 회원 식별자 참조 |
|
||||||
|
| 표시 언어 | entity `lang: Lang`, `@Enumerated(EnumType.STRING)` 및 `@Column(nullable = false, length = 10)`; DB `lang VARCHAR(10)`에 enum NAME `KO/EN/JA` 저장, 이번 쓰기는 명시 `JA`만 |
|
||||||
|
| 수동 등록 이름 | 별도 테이블의 `display_name VARCHAR(255)`, 해당 언어의 표시 이름; 255 Unicode code point 초과는 관리 경계에서 거부하고 자르지 않음 |
|
||||||
|
| 활성 상태 | `is_active`, 해제 시 false로 바꾸고 이름은 보존; 표시/별칭 검색에는 true만 사용 |
|
||||||
|
|
||||||
|
`UNIQUE(member_id, lang)`은 활성/비활성 모두에 적용하고 `is_active`를 포함하지 않는다. clear는 기존 JA 행의 `is_active=false`만 설정해 `display_name`을 보존하며, 없는 행은 no-op이다. 비공백 저장은 member+lang으로 비활성까지 찾아 같은 ID를 갱신·활성화한다. 원본 `Member.nickname`, 다른 언어 행, 과거 언어별 snapshot/전달 payload는 유지한다. 마지막 비활성 값 하나를 남기는 설계이지 모든 개정 이력이나 복원 UI가 아니다.
|
||||||
|
|
||||||
|
앱 표시 조회는 요청 `Lang`과 일치하는 활성 이름 및 대상 `Member.role=CREATOR`를 적용한다. 비활성/미등록은 현재 원본 이름으로 fallback한다. 읽기에 JA 전용 gate는 두지 않는다. 관리자 목록은 COR014에 따라 원본과 모든 활성 언어 이름을 분리해 제공하며 전용 관리 GET은 제거한다. 별칭 검색은 요청 언어와 무관하게 활성 JA·CREATOR만 포함한다.
|
||||||
|
|
||||||
|
HTTP path/응답의 `locale: ja`는 `Lang.code`이며 DB에는 소문자 `ja`가 아닌 enum NAME `JA`를 저장한다. 요청 경계는 기존 `Lang.fromCode`/`Lang.fromAcceptLanguage`를 사용하고 서비스·repository에는 명시 `Lang`을 전달한다. 자유 문자열/새 enum/언어 converter를 추가하지 않는다. 배너의 KO 기본값은 복사하지 않으며 작성자가 언어를 명시한다. 미래 언어는 기존 `Lang`과 요청 처리를 별도 확장해야 하지만 언어별 컬럼 추가는 필요 없다.
|
||||||
|
|
||||||
|
### 기존 패턴 비교와 근거
|
||||||
|
|
||||||
|
| 근거 경로·심볼 | 재사용 또는 차이 |
|
||||||
|
|---|---|
|
||||||
|
| `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/ChatCharacterBanner.kt:31` (`lang`, `isActive`), `src/main/kotlin/kr/co/vividnext/sodalive/event/Event.kt:24` (`lang`, `isActive`) | 수동 언어별 관리의 기존 `Lang` enum-string/활성 상태 재사용. length 10은 새 DDL과 맞추는 명시 매핑이며 기존 KO 기본값은 재사용하지 않음 |
|
||||||
|
| `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterBannerService.kt:140` (`deleteBanner`), `src/main/kotlin/kr/co/vividnext/sodalive/admin/event/banner/AdminEventBannerService.kt:202` (`delete`) | `isActive=false` 소프트 해제 근거. 이름의 동일 행 재활성화는 이번 별도 기술 계획이며 배너가 지원한다고 주장하지 않음 |
|
||||||
|
| `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/repository/ChatCharacterBannerRepository.kt:16` (`findByIsActiveTrueAndLangOrderBySortOrderAsc`) | 기존 Lang 인자·활성 필터 읽기 패턴 재사용. 배너의 KO 대체 정책은 이름 조회에 복사하지 않음 |
|
||||||
|
| `src/main/kotlin/kr/co/vividnext/sodalive/content/translation/ContentTranslation.kt:15` (`UniqueConstraint`, `contentId`, `locale`) | 소유자+언어별 고유 행만 참고. 자동 번역의 `locale: String` 저장과 달리 수동 이름은 `lang: Lang`을 사용 |
|
||||||
|
| `src/main/kotlin/kr/co/vividnext/sodalive/i18n/Lang.kt:6` (`code`, `fromCode`, `fromAcceptLanguage`), `src/main/kotlin/kr/co/vividnext/sodalive/i18n/LangInterceptor.kt:18` (`preHandle`) | 기존 KO/EN/JA enum 및 HTTP 경계 매핑 재사용, 요청 언어를 저장 문자열로 새로 정의하지 않음 |
|
||||||
|
| `src/main/kotlin/kr/co/vividnext/sodalive/common/BaseEntity.kt:16` (`id`, `createdAt`, `updatedAt`) | 관리 ID/생성·수정 시각 구조 참고. SQL 기본 TIMESTAMP 정의는 문서 유지보수 규칙 적용 |
|
||||||
|
|
||||||
|
초기 물리 DELETE 설명은 단순한 최소 기술 선택이었으며 프로젝트 관례의 필수 조건이 아니었다. 사용자는 그 이유와 기존 언어 저장 패턴 재사용을 질문했으며 소프트 삭제를 직접 선택한 것은 아니다. 위 수동 관리 근거로 현재 기술 설계를 보정했다.
|
||||||
|
|
||||||
|
| 주제 | 제안 | 근거 및 제한 |
|
||||||
|
|---|---|---|
|
||||||
|
| 도메인/저장 | `kr.co.vividnext.sodalive.v2.creatorname` 신규 도메인 및 member+lang 고유, displayName non-unique, `VARCHAR(255)` 저장; 신규 entity는 `displayName @Column(nullable = false, length = 255)` | 신규 도메인은 v2 경량 헥사고날 구조. 255 code point를 관리 경계에서 검증하고 초과 입력은 거부한다. DDL 초안 작성만 승인, 실제 DB 호환성은 미검증. 중복 표시명 정책은 인터뷰 확정 선택이 아님 |
|
||||||
|
| 관리 API | COR014: `PUT /api/v2/admin/creators/{creatorId}/localized-names/ja`만 유지, ADMIN 보호 | 필수 `displayName: String`, 빈 값 soft clear. 기존 PUT 결과 `creatorId`, `locale: ja`, `originalNickname`, nullable `displayName` 및 ApiResponse 유지. 독립 GET은 제거하고 조회는 기존 관리자 회원 목록에 통합한다. 이번 변경의 HTTP 실행 검증은 미수행이다. |
|
||||||
|
| 관리 검증 | `Member.role == CREATOR`인 대상만 허용하며, 그 중 별도 `Member.memberKind == AI_CHARACTER`도 포함. `memberKind`만 AI_CHARACTER인 `role == USER`는 허용하지 않음 | ADMIN 보호와 role 판정은 근거 기반 기술 제안. 신규 message key만 제안하며 기존 role-edit PUT 불변 |
|
||||||
|
| 공통 이름 조회 | DB 직접 projection은 `CreatorNameJoin`으로 요청 Lang의 LEFT JOIN+COALESCE를 사용한다. 중립 캐시·수신자별 전달·principal·쓰기 결과 경계는 batch를 사용하며 현재 원본이 필요한 batch는 JOIN+COALESCE 단일 SQL, sparse 별칭이 필요한 곳은 `findLocalizedNames`를 쓴다. | 읽기는 모든 기존 `Lang`에서 활성/CREATOR만 적용하며 JA-only gate가 없다. 쓰기 조회는 비활성 포함 member+lang. 행별 쿼리·전역 Jackson 변환·principal 변경 금지. `LangContext`는 HTTP 경계에서만 읽음 |
|
||||||
|
| HTTP/DTO 호환 | 레거시/v2 응답 DTO의 field 이름·타입 보존. 필요한 조회 projection에만 내부 memberId/role 추가 | 원본 닉네임 편집 API는 로컬라이즈 값을 원본으로 저장하지 않도록 외부 계약 확인 필요 |
|
||||||
|
| 검색 | 기존 원본 조건 OR alias `EXISTS`(owner `member_id` 상관, `lang=Lang.JA`/DB `JA`, `is_active=true`, owner `Member.role=CREATOR`); containsIgnoreCase 의미 유지 | 요청 locale과 무관하게 활성 JA만 별칭 검색. Explorer channel, SearchRepository creator branch, MemberRepository의 list/count에 동일 조건과 기존 필터/USER 검색 보존. 콘텐츠·시리즈 제목·detail·tag 검색은 별칭 대상 아님 |
|
||||||
|
| 푸시/저장 한계 | `FcmSendListener.sendPush`에서 언어별 payload 생성. 기존 `PushNotificationList.senderNicknameSnapshot` mapping이 255자 이름을 보존하는지 확인 | 기존 mapping/컬럼 확장은 이번 계획에 포함하지 않는다. 과거 알림 재작성·이름 자르기 금지. 실제 저장 및 외부 payload 호환성 문제가 확인되면 별도 결정 전 해당 적용은 차단한다. |
|
||||||
|
| WebSocket | handshake `Accept-Language`를 session attribute로 저장, 미전달 기본 KO. locale 변경은 reconnect 계약 제안 | Redis pubsub에는 원본 메시지/발신 ID 유지, broker에서 세션별 새 payload 구성. 공유 DTO 변이 금지. ACK와 HTTP 응답도 별도 확인 |
|
||||||
|
| 캐시/스냅샷 | 각 producer/read path를 조사해 현재 이름 조회와 불변 historical snapshot을 구별 | 기존 `LiveRecommendCacheService`의 3시간 캐시는 key에 Lang을 포함. 다른 캐시/랭킹/inbox는 자동 면제하지 않음 |
|
||||||
|
|
||||||
|
`schema.sql`은 단일 CREATE TABLE 초안이며 MySQL 컬럼/테이블 COMMENT와 기본 TIMESTAMP 정의를 따른다. signed BIGINT와 member FK/engine/charset 초안 근거는 `docs/plan-task/20260326_member_content_preference_ddl.sql:12`, COMMENT/TIMESTAMP 및 소유자+언어 unique/FK 형태 근거는 `docs/20260910_크리에이터커뮤니티게시물본문번역/schema.sql:5`다. 복합 unique가 FK 지원 인덱스이며 추가 인덱스·displayName 고유성/인덱스·언어 기본값·cascade·check·trigger를 두지 않는다. displayName은 `VARCHAR(255)`이며 실제 DB 호환성은 미검증이다. **DDL 파일 작성 완료와 DB 적용은 별개다.** 운영 반영 전에 실제 `member.id` 타입(부호 포함)·engine과 collation/contains 및 저장 호환성을 확인하고 DB 실행을 별도 승인받는다. 현재는 DB 연결/실행이나 운영 호환성을 검증하지 않았다.
|
||||||
|
|
||||||
|
## 5. 근거 경로 및 범위 추적
|
||||||
|
|
||||||
|
주요 근거와 seed inventory는 계획 문서에 실제 전체 경로 및 test target으로 분류했다. 목록은 감사 시작점이지 전수 완료 선언이 아니다. 기존 `MemberRepository` nickname 검색 경로와 legacy home/rank/recommend 및 v2 content/channel query adapter를 빠짐없이 조사한다.
|
||||||
|
|
||||||
|
이 저장소에는 모바일 앱 및 운영 UI 소스가 없다. 외부 API, 캐시 및 RTC 소유 경로는 계약이 확인되기 전 구체 경로를 추정하지 않는다.
|
||||||
|
|
||||||
|
## 6. 의사결정 기록
|
||||||
|
|
||||||
|
현재 후속 실행 범위는 COR022를 따른다. 이전 실행 금지 및 정적 승인 기록은 당시 이력으로 보존한다.
|
||||||
|
|
||||||
|
기존 결정 로그의 행은 요약하지 않고 각각 보존한다. 아래 DEC 항목은 최초 결정의 주제를 명시한 기록이다. COR 행은 그 결정을 보정하고 우선 적용한다.
|
||||||
|
|
||||||
|
| 날짜 | ID | 상태 | 결정 |
|
||||||
|
|---|---|---|---|
|
||||||
|
| 2026-10-14 | DEC001 | 확정 | 운영자가 일본어 이름을 수동 관리한다. 자동 생성/번역하지 않는다. |
|
||||||
|
| 2026-10-14 | DEC002 | 확정 | 입력 문자 종류 제한을 두지 않는다. |
|
||||||
|
| 2026-10-14 | DEC003 | 확정 | JA 이름은 일본어 표시에 우선 적용한다. 미등록 시 원본 이름을 사용한다. |
|
||||||
|
| 2026-10-14 | DEC004 | 확정 | 원본 `Member.nickname`은 보존한다. |
|
||||||
|
| 2026-10-14 | DEC005 | 확정 | 앱의 크리에이터 표시 표면을 대상으로 하며 혼합 목록에서도 CREATOR만 변경한다. |
|
||||||
|
| 2026-10-14 | DEC006 | 확정 | 미래 언어 가능성을 고려할 수 있지만 현재 KO/EN 이름 관리 기능은 만들지 않는다. |
|
||||||
|
| 2026-10-14 | DEC007 | 확정 | JA 별칭은 기존 원본 닉네임 검색과 함께 검색한다. |
|
||||||
|
| 2026-10-14 | DEC008 | 확정 | JA 이름 빈 값 저장은 별칭 제거이며 원본 fallback으로 돌아간다. |
|
||||||
|
| 2026-10-14 | COR001 | 정정 | 별칭은 원본 닉네임 변경 이후에도 우선한다. 미래 언어 준비는 기술 제안일 뿐 제품 범위가 아니다. |
|
||||||
|
| 2026-10-14 | COR002 | 정정 | 일반 USER와 원본 편집/인증/식별 흐름은 불변. 제품 업무 길이 제한을 추가하지 않는다. 중복 별칭 허용 및 회원 ID+locale 저장은 기술 제안이며 직접 확정된 인터뷰 선택은 아니다. |
|
||||||
|
| 2026-10-14 | COR003 | 정정, OQ-001 해결 | 원본 이름 검색을 유지하면서 JA 별칭 검색을 더한다. |
|
||||||
|
| 2026-10-14 | COR004 | 정정, OQ-002 해결 | 빈 문자열 저장으로 JA 이름을 해제한다. 공백-only 해제는 기술 해석 제안으로 별도 승인 대기다. |
|
||||||
|
| 2026-10-14 | COR005 | 정정, OQ-003 해결 | 과거 언어별 스냅샷/전달 payload는 불변. 현재 조회는 요청 언어, 새 수신자별 payload는 해당 수신자/세션 언어. 언어 중립 캐시를 자동 제외하지 않는다. |
|
||||||
|
| 2026-10-14 | COR006 | 정정 | `/admin/member` ADMIN 권한은 참고 근거일 뿐 신규 API 소유권 계약이 아니다. `/creator-admin/member` CREATOR/AGENT 관리 화면 대상이 아니다. |
|
||||||
|
| 2026-10-14 | COR007 | 정정 | PRD와 plan-task 문서 작성만 승인되었다. 구현/테스트/설정/DDL은 별도 승인 전 금지. |
|
||||||
|
| 2026-10-14 | DEC009 | 확정 | 기존 언어별 과거 스냅샷과 이미 전달된 payload는 변경하지 않는다. 요청 시 현재 이름을 조회하는 경로는 요청 언어, 신규 수신자별 payload는 수신자/세션 언어를 적용한다. |
|
||||||
|
| 2026-10-14 | COR008 | 후속 확정, COR007 범위 보정 | 사용자 확인으로 테이블명 `creator_localized_name` 확정, 기존 두 문서 보정과 같은 디렉터리 `schema.sql` 파일 작성만 승인. DB 적용·Kotlin·테스트·설정·운영 배포는 미승인이다. |
|
||||||
|
| 2026-10-14 | COR009 | 근거 기반 기술 결정 | 기존 수동 관리의 `Lang` enum-string과 `isActive`를 재사용한다. DB 키는 member+lang/enum NAME `JA`, HTTP locale은 `Lang.code`의 `ja`다. 초기 물리 DELETE는 단순 기술 선택이지 프로젝트 강제 관례나 사용자 요구가 아니며, 사용자가 소프트 삭제를 직접 선택한 것도 아니다. |
|
||||||
|
| 2026-10-14 | COR010 | 기술 lifecycle 보정 | clear는 행/값 보존·비활성화, absent는 no-op. 표시/검색/관리 GET은 비활성 제외, 재저장은 비활성 포함 조회로 동일 ID 재활성화. unique는 활성/비활성 전체 member+lang. 전체 이력/복원 기능은 포함하지 않는다. |
|
||||||
|
| 2026-10-14 | COR011 | 기술 저장 한계 보정 | 사용자는 TEXT가 불필요하다고 확인했다. `VARCHAR(255)`는 기본 JPA nickname 표현에 맞춘 기술 결정이며 사용자가 255를 직접 선택한 것은 아니다. 현재 저장은 255 Unicode code point로 제한하고 초과는 관리 경계에서 거부한다. COR002의 무길이 제한 표현은 현재 저장 한도에 대해 superseded이며 문자 종류 제한 없음은 유지한다. |
|
||||||
|
| 2026-10-14 | COR012 | 사용자 구현 승인 | 사용자: "DDL + 문서 확인 / 구현 승인" 및 ulw. 현재 문서의 API·저장·소프트 해제·길이·검색·전달 설계를 구현하고 테스트한다. 앞선 제품 구현 미승인 문구를 대체한다. 운영 DB 적용·배포는 승인하지 않았고 외부 저장소 계약은 확인 전 완료로 간주하지 않는다. |
|
||||||
|
| 2026-10-15 | COR013 | 사용자 조회 방식 수정 승인 | "3가지 경로에 각각 적절한 방식으로 조회하도록 수정하자". DB 직접 이름 projection은 요청 Lang의 LEFT JOIN+COALESCE, 중립 캐시·수신자별 전달 및 기존 원본 엔티티/쓰기 결과 경계는 batch, 이미 언어별로 렌더된 역사 스냅샷은 저장값을 유지한다. 읽기의 JA 전용 제한은 제거하되 검색 JA·관리 /ja 및 저장 정책은 유지한다. |
|
||||||
|
| 2026-10-15 | COR014 | 사용자 관리자 조회 통합 적용 승인 | "문서에 반영하고 적용한다". 별도 닉네임 GET 대신 기존 `/admin/member/list`와 공통 응답을 쓰는 목록·검색 4개 경로에 모든 활성 언어 이름을 추가한다. PUT의 등록·수정·해제와 결과 응답은 유지한다. 관리자 콘텐츠 원본 표시 및 레거시 관리자 회원 목록은 보존한다. 빌드·테스트 실행 없이 수정·정적 검토만 진행한다. |
|
||||||
|
| 2026-10-15 | COR015 | 사용자 선택적 롤백·테스트 정리 승인 | ContentMainTab/AudioContentCuration/레거시 커뮤니티·팬톡의 이번 이름 처리만 제거한다. v2와 공유 저장·권한·번역·알림, COR014 관리자 목록/PUT은 유지한다. LegacyCreatorNameRenderer 위임 래퍼는 제거하고 필요한 호출자는 core service를 직접 사용한다. 제거 기능만 검증하는 테스트와 불필요 fixture/import는 정리하되 기존 업무·v2·core 회귀 단언은 보존한다. 기존 미사용 메서드 자체 삭제는 제외하며 빌드·테스트 실행 및 커밋·푸시는 하지 않는다. |
|
||||||
|
| 2026-10-15 | COR016 | 사용자 최소 수정·통과 검증 승인 | 사용자가 전체 실행의 두 실패를 확인하고 최소 수정하여 통과시키도록 요청했다. AudioContentRepositoryLocalizedNameTest의 H2 번역 payload 적재만 기존 FORMAT JSON 패턴으로 보정한다. 운영 코드/converter/DDL/요구사항/테스트 기대값은 보존한다. 대상 및 회귀 테스트 실행을 재개하되 운영 DB 적용·배포·커밋·푸시는 하지 않는다. |
|
||||||
|
| 2026-10-15 | COR017 | 사용자 미사용 탭 완전 제외 승인 | AudioContentMainTab*은 사용하지 않으므로 이번 기능 변경에서 배제해도 무관함을 확인하고 수정 승인했다. 탭 7파일의 null 전달 13곳을 되돌려 HEAD diff를 없애고, 공유 운영6파일·테스트2파일에서는 미사용 탭 전용 raw 인자/분기/검증만 정리한다. 실사용 이름·제목 번역과 기존 다른 raw 경계는 유지한다. |
|
||||||
|
| 2026-10-15 | COR018 | 사용자 테스트·문서7파일 보정 승인 | 원본은 KO가 아니라 임의 언어의 저장값임을 명확히 했다. 공통 service/JOIN 테스트2파일, 대표 HTTP/메시지 테스트3파일, 기존 PRD/계획2파일만 보강한다. 운영 로직은 이미 요청 언어 별칭→원본 fallback이므로 변경하지 않는다. 조건을 생략한 KO 원본 설명을 고치고 기존 테스트에 원본 문자열3종 및 일본어 원본 HTTP/메시지 사례를 반영한다. 신규 테스트 파일·원본 언어 필드·자동 감지·쓰기 언어 확대는 제외한다. |
|
||||||
|
| 2026-10-15 | COR019 | 사용자 콘텐츠 메인 롤백 승인 | 조사 후 운영3파일(메인 controller/service 전체 변경 및 repository 전용2메서드 이름 변경)의 롤백을 승인했다. 두 메인 파일은 변경 목록에서 제외하고 실제 사용하는 공유 repository·core·v2 테스트는 보존한다. 전용 추가 테스트0건이므로 테스트 삭제는 하지 않는다. 새로운 원본 보호 모드·API 삭제·운영 적용은 포함하지 않는다. |
|
||||||
|
| 2026-10-16 | COR020 | 사용자 캐릭터 댓글 롤백·테스트 제거 승인 | CharacterCommentService/Repository 두 운영파일 변경을 HEAD로 되돌리고 LegacyCurrentNameReadIntegrationTest의 shouldLocalizeCharacterCommentWritersWithoutChangingCharacterNameOrUserReplies 1개 및 전용3import/서비스주입을 제거한다. 나머지8개 통합테스트와 캐릭터 배너 테스트는 유지한다. 신규 영구 테스트·API·원본 보호 모드 추가는 제외한다. |
|
||||||
|
| 2026-10-16 | COR021 | 사용자 시리즈 롤백·제목 검증 보존 승인 | 조사 결과 설명대로 운영2파일의 이름 처리6곳(목록JOIN5+상세batch1)을 롤백한다. 홈·프로필 재사용 경로에도 이름 표시 영향이 있음을 설명하고 진행 승인받았다. AudioContentRepositoryLocalizedNameTest의 혼합 시리즈 테스트는 이름별칭 fixture/기대값/단언만 정리하고 제목 번역·fallback 및 H2 FORMAT JSON 처리를 보존한다. 테스트 메서드/파일 전체 삭제나 새 raw 모드 추가는 하지 않는다. |
|
||||||
|
| 2026-10-16 | COR022 | 사용자 메시지 전체 롤백 승인 | 조사 결과대로 MessageService/Repository 전체HEAD복원, 전용 MessageListRow 및 MessageServiceLocalizedNameTest(3건) 파일 삭제를 승인했다. 푸시 본문 메타데이터2곳 제거·senderMemberId 및 공통 snapshot 현지화 유지라는 영향을 포함한다. 레거시 API/업무 자체와 공통 FCM·v2 DM/WS 테스트는 삭제하지 않는다. |
|
||||||
|
|
||||||
|
DEC003/005/007/008은 각각 원래 사용자 선택을 요약한 결정 기록이다. 공백-only 해석, 중복 별칭의 구체 저장 제약, 구체 API route/ADMIN 관리 경계는 후속 기술 제안이지 별도 인터뷰에서 명시된 사용자 선택이 아니다.
|
||||||
|
COR002의 member+locale·무길이 제한 표현과 COR007의 DDL 작성 금지는 당시 기록으로 보존하며 현재 범위/저장 결정은 COR008~011이 우선한다.
|
||||||
|
|
||||||
|
## 7. 진행 판정
|
||||||
|
|
||||||
|
- `plan-task.md`는 NAME001~007, EXT001~003을 구체 task와 검증에 연결한다.
|
||||||
|
- COR013의 JOIN-T1 공통 계약과 JOIN-T2 DB projection 전환은 구현 및 focused 증거 확인 완료. JOIN-T3 경계 assertion은 통과했으나 최종 전체 build/QA Gate 결과는 대기 중이다.
|
||||||
|
- 외부 모바일/운영 UI/RTC 계약은 차단 조건·담당 역할·재개 증거를 유지한다.
|
||||||
|
- COR012까지의 과거 전체 검증은 아래 이력으로 보존한다. COR013 최종 실행은 현재 미완료이며 `plan-task.md`의 COR013 진행 기록만 근거로 삼는다. 외부 앱 전체 표시, 운영 UI, RTC/client 계약 및 운영 DB/provider 한계도 미검증이며 제품 완료를 선언하지 않는다.
|
||||||
|
- 운영 DB 적용·운영 배포는 별도 승인 대상이다. 커밋·푸시는 별도 사용자 승인 없이 수행하지 않는다.
|
||||||
|
|
||||||
|
## 8. 구현 및 검토 상태, 2026-10-14
|
||||||
|
|
||||||
|
- 기반 저장/API, legacy 및 v2 내부 HTTP/검색 소비자, 서버 FCM/WebSocket/live/message 경로가 구현됐다. 세 inventory의 변경 기록과 `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/creatorname-integrated-full.log`의 최종 성공 실행을 근거로 한다.
|
||||||
|
- 당시 최종 전체 실행: 322 suites, 2245 tests, failures 0, errors 0, skipped 0. `assemble`와 `ktlintCheck` 포함 명령이 `BUILD SUCCESSFUL`로 종료됐다. 이는 COR013 전 과거 이력이다. 이전 200 fixture/context 실패 실행과 로그도 당시 기록으로 보존한다.
|
||||||
|
- 테스트가 실제 관리 API JWT HTTP를 사용했다. 테스트 서버는 HTTP 53418, WebSocket 53427에서 기동됐고 teardown curl은 양 포트 연결 거부(exit 7)를 확인했다. Hikari pool 종료를 로그에서 확인했으며 테스트 후 잔류 JVM/Redis 프로세스는 보고되지 않았다.
|
||||||
|
- 전체 앱 클라이언트 표시/원본 편집 안전성, 운영 관리 UI, RTC reconnect/provider 및 실제 운영 DB 적용은 확인되지 않았다. EXT001~003과 운영 DB/provider 호환성은 blocker다.
|
||||||
|
- Oracle 검토에서 두 보완점이 확인됐다. 현재 이름 fallback 경로의 `findAllById` entity graph 조회를 scalar ID/nickname 조회로 바꾸고, 공통 helper 두 개를 `v2.creatorname` 하위로 이동한다. cache 실제 hit를 입증하는 테스트 assertion도 추가할 필요가 있다. 수정 담당자가 작업 중이며 아직 수정·재검증·리뷰 승인 완료로 기록하지 않는다.
|
||||||
|
- 검증 환경에 `lsp_diagnostics` 도구가 없어 LSP 진단은 수행하지 못했다. compile, 전체 테스트, assemble, ktlint 결과는 위 실행 로그가 근거다.
|
||||||
|
|
||||||
|
## 9. 최종 서버 검증, 2026-10-15
|
||||||
|
|
||||||
|
- 리뷰 지적 2건 해결: 이름 전용 현재 원본 조회를 `MemberQueryRepository.findNicknamesByIds` scalar 조회로 교체했고 공통 renderer/predicate를 `v2.creatorname` 아래로 이동했다. 1명/4명 조회에서 JA SQL 2/2회, KO·EN 1/1회, 엔티티 로드 0/0을 테스트로 확인했다.
|
||||||
|
- COR013 전 당시 전체 회귀: 322 suites, **2249 tests / failures 0 / errors 0 / skipped 0**, `test assemble ktlintCheck --continue --offline --console=plain` 성공. 이후 상위 에이전트의 `./gradlew build --offline --console=plain`도 성공했다. 이 결과는 COR013 이후 최종 검증을 뜻하지 않는다.
|
||||||
|
- 실제 표면 재검증: 상위 에이전트가 ADMIN HTTP 4개와 localhost HTTP/WebSocket 9개를 별도 실행해 **13개 모두 통과**했다. 포트 54539/54547의 Tomcat·Hikari 종료와 후속 curl exit 7, 테스트 JVM/Redis 잔존 없음도 확인했다.
|
||||||
|
- 최종 독립 리뷰: Oracle의 수정분 재검토 **APPROVE**, 중요한 잔여 코드 지적 없음. 실제 캐시 entry의 원본과 방 목록 쿼리 1회 재사용까지 검증했다.
|
||||||
|
- 외부 모바일/운영 UI/RTC 및 실제 provider/운영 MySQL 호환성은 미검증으로 유지한다. 운영 DB 적용·배포·커밋·푸시는 실행하지 않았다. 상세 명령·로그는 계획 문서 최종 기록을 따른다.
|
||||||
|
|
||||||
|
## 10. 조회 경로별 처리, COR013
|
||||||
|
|
||||||
|
1. DB에서 이름을 직접 조회하는 목록·projection은 `CreatorNameJoin(owner, lang, alias)`로 `creator_localized_name`을 LEFT JOIN한다. 회원 ID·요청 언어·활성 별칭·CREATOR 역할 조건은 ON에 두고 `COALESCE(display_name, nickname)`를 선택한다. 후속 별칭 SELECT와 이름 전용 copy/map을 제거한다. 검색 조건·정렬·집계·페이지·공개 DTO는 보존한다.
|
||||||
|
2. 중립 캐시와 Redis payload, 수신자/세션 언어가 늦게 정해지는 전달, 인증 principal 및 기존 원본 엔티티를 유지해야 하는 쓰기/오류 경계는 필요한 batch를 유지한다. 현재 원본까지 필요한 batch는 member+별칭 JOIN의 단일 SQL로 처리하고 sparse 별칭 map은 명시 fallback 의미가 있는 곳에만 쓴다. 세 inventory의 잔여 호출 분류를 따른다.
|
||||||
|
3. 이미 언어별로 렌더해 저장한 역사 이름은 그대로 반환한다. 중립 랭킹/inbox/cache는 historical rendered snapshot과 구별하며, cache hit에서도 현재 이름을 렌더한다.
|
||||||
|
|
||||||
|
읽기 서비스와 저장소는 전달받은 기존 `Lang`을 사용한다. JA/KO/EN 각각 활성 이름이 있으면 해당 값을, 없거나 비활성이면 원본을 반환한다. 현재 관리자 입력 /ja 및 저장의 일본어 범위, 검색의 JA 별칭 EXISTS는 이번 조회 리팩터링에서 확장하지 않는다. `Member.nickname` 원본과 인증/편집 경계는 그대로 둔다. 승인된 세 inventory의 COR013 최신 구간이 개별 소비자와 남은 batch를 분류한다.
|
||||||
|
|
||||||
|
### COR013 구현 및 검증 상태, 2026-10-15
|
||||||
|
|
||||||
|
- JOIN-T1 공통 `CreatorNameJoin` 및 모든 Lang의 sparse/current-name 조회 계약, JOIN-T2의 직접 DB projection 전환은 구현됐다. legacy/v2/live/message/DM 목록 경로는 각 inventory의 COR013 manifest를 기준으로 한다.
|
||||||
|
- JOIN별 owner alias는 고유하게 지정한다. 삭제 prefix 정리는 raw 원본 이름에만 적용하며 수동 별칭 원문은 바꾸지 않는다. 기존 count/group/page/filter와 공개 DTO는 보존한다.
|
||||||
|
- 기준선은 `creatorname-join-baseline.log` 성공(1m53s). core 43개 RED 중 13개 assertion 실패 뒤 GREEN 43개 통과(`creatorname-join-core-green.log`); consumer 3개 RED는 이름 출력이 아닌 추가 alias SELECT 기대 0/실제 1을 포함했다(`creatorname-join-consumer-red.log`). 수정 후 consumer 3개 통과, alias SELECT 0 확인(`creatorname-join-first-green.log`, 2m37s).
|
||||||
|
- 첫 전체 실행 `creatorname-join-full.log`은 테스트 2266개가 통과했으나 ktlint 서식 오류로 실패했다. 서식 보정 후 `creatorname-join-final-green.log`의 전체 test/build/ktlint가 성공했다. 이후 실사용이 없어진 보조 메서드 2개와 해당 passing helper 테스트 1개만 제거하고 전체 검증을 다시 실행했다.
|
||||||
|
- 최종 `./gradlew test build ktlintCheck --continue --offline --console=plain` 성공: **2265 tests / failures 0 / errors 0 / skipped 0**, `creatorname-join-release-check.log`의 `BUILD SUCCESSFUL in 13m 45s`. 실제 ADMIN JWT HTTP 4개와 localhost HTTP/WebSocket 9개도 포함되어 통과했다.
|
||||||
|
- 대표 콘텐츠·검색·라이브 추천의 별칭 전용 후속 SELECT는 각각 1회에서 0회로 줄었다. 캐시 current-name batch는 요청 Lang에 무관하게 1 SQL이며 sparse 수신자 override와 역사 스냅샷 보존도 검증했다. 운영 지연 시간 개선을 측정한 것은 아니다.
|
||||||
|
- Oracle 3-path 리뷰에 중대한 코드 지적이 없었고, 미사용 보조 메서드 제거 후 수정분 재검토도 APPROVE다. 최종 실행 결과로 runtime 조건을 충족했다. HTTP 58173/WS 58186 종료 및 후속 curl exit 7, 테스트 JVM/Redis 잔존 없음도 확인했다.
|
||||||
|
- 제품 전체 완료는 별개다. 모바일/client, 운영 UI, RTC/provider 및 실제 MySQL 호환성은 외부 확인이 필요하며 운영 DB 적용·배포·커밋·푸시는 수행하지 않는다.
|
||||||
|
|
||||||
|
## 11. 관리자 조회 통합, COR014
|
||||||
|
|
||||||
|
- 대상: `/admin/member/list`, `/admin/member/search`, `/admin/member/creator/list`, `/admin/member/creator/search`. 공통 `GetAdminMemberListResponseItem`에 `localizedNames`만 추가하고 기존 필드·totalCount·순서·페이지 크기는 유지한다. 이는 의도적인 관리자 JSON 응답 확장이다.
|
||||||
|
- 원본 `nickname`은 그대로 두고 CREATOR의 모든 활성 등록 이름을 `{"ja":"등록 이름","en":"등록 이름"}` 형태로 제공한다. 특정 언어로 필터링하거나 원본을 미등록 언어에 채우지 않는다. soft 해제된 값은 노출하지 않으며 일반 회원과 별칭 없는 회원은 `{}`다.
|
||||||
|
- 회원 페이지를 먼저 읽고 해당 페이지의 CREATOR ID들에 대한 모든 활성 언어 이름을 한 번에 조회한다. 다언어 1:N 조인으로 회원 페이지/건수가 증가하지 않도록 기존 페이지 쿼리는 보존하고 회원별 추가 조회는 하지 않는다. 역할·로그인 방식 등의 기존 번역 문구는 유지한다.
|
||||||
|
- `/search-by-nickname`과 `/creator/all/list`의 별도 간단 DTO, 관리자 콘텐츠/시리즈/댓글의 원본 닉네임 표시, 일반 앱의 COR013 언어별 표시·JA 검색은 변경하지 않는다.
|
||||||
|
- 독립 GET과 더 이상 운영 호출이 없는 단건 조회 서비스는 제거한다. `CreatorLocalizedNameResponse`는 PUT 확인 응답에 계속 사용하므로 유지한다. 별도 POST/DELETE, 전체 언어 신규 GET, 레거시 API 제거, H2 보정/DDL 변경은 추가하지 않는다.
|
||||||
|
- 실행 제한: 테스트를 먼저 작성하되 사용자 빌드 금지에 따라 Gradle·컴파일·테스트·앱 실행을 하지 않는다. 실제 RED/GREEN·HTTP 성공은 미검증으로 기록하며 이전 통과 수치를 이번 변경의 증거로 재사용하지 않는다.
|
||||||
|
|
||||||
|
### COR014 적용 결과
|
||||||
|
|
||||||
|
- 공통 관리자 목록 항목에 `localizedNames`를 추가했다. 기존 페이지 조회 후 CREATOR ID의 모든 활성 이름을 단일 scalar 조회로 묶으며, `nickname`과 기존 페이지/count 쿼리는 유지한다. 간단 선택 API와 관리자 콘텐츠 원본 이름 조회는 변경하지 않았다.
|
||||||
|
- 전용 닉네임 GET 및 미사용 `getLocalizedName` 서비스를 제거했다. PUT과 그 확인 응답 `CreatorLocalizedNameResponse`는 유지한다. 새 전체 언어 GET이나 별도 POST/DELETE는 추가하지 않았다.
|
||||||
|
- 관리자 4개 목록/검색, Accept-Language 독립, 다언어 페이지 중복 방지, USER·해제 제외, 빈 ID/일괄 쿼리 수, 제거한 GET, 기존 PUT lifecycle·권한·255/256 code point 테스트 코드를 작성·보정했다. 실행하지 않았다.
|
||||||
|
- 독립 리뷰는 **PASS(static)**이며 명백한 호출·타입·권한·페이지 계약 문제는 발견되지 않았다. 빌드/테스트/DB/HTTP 실행 성공을 의미하지 않는다. 사용하지 않는 레거시 API 제거와 H2 보정 변경은 이번 범위 밖으로 유지했다.
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
# COR015 레거시 이름 정리 정적 리뷰
|
||||||
|
|
||||||
|
## 리뷰 정보
|
||||||
|
|
||||||
|
- 날짜: 2026-10-15
|
||||||
|
- 대상: 기존 미커밋 COR012~014 변경 위의 COR015 선택적 롤백·테스트 정리. 전체 HEAD diff를 이번 변경으로 간주하지 않는다.
|
||||||
|
- 기준: 같은 디렉터리 상위 `prd.md` COR015, `plan-task.md` LEGACY-TRIM-T1의 S1~S3.
|
||||||
|
- 독립 검토 세션: `ses_ec179d026ffeTbyj1KNLcvwUpg`.
|
||||||
|
- 판정: **APPROVE(static)**. 확정 발견 사항 없음. 실행 검증 승인이나 테스트 통과 판정은 아니다.
|
||||||
|
|
||||||
|
## 확인 결과
|
||||||
|
|
||||||
|
| 기준 | 정적 확인 |
|
||||||
|
|---|---|
|
||||||
|
| S1 제외 영역 원본 표시 | 7개 콘텐츠 탭의 중첩 목록·랭킹·추천·태그·큐레이션, 레거시 커뮤니티 목록/상세/첫 댓글/댓글/답글/구매 및 팬톡 목록/작성/수정에서 원본을 사용한다. 기존 삭제 회원 접두사 처리를 보존한다. |
|
||||||
|
| S2 유지 대상 보존 | nullable 이름 언어는 공유 조회의 원본 모드만 제어하며 제목 번역 언어는 유지한다. Home 등 유지 호출자는 요청 Lang을 전달한다. v2 커뮤니티·팬톡 JOIN, 관리자 회원 목록의 localizedNames와 PUT을 보존한다. |
|
||||||
|
| S3 의존성·테스트 정리 | 삭제 래퍼/helper/필드의 src 참조가 없다. 테스트 생성자·조회 인자는 production과 일치하며 실제 core 빈이 필요한 4개 테스트에 설정을 유지한다. 래퍼의 SQL 수·역할·빈 입력·현재 원본 fallback 단언은 core 서비스 테스트에 통합했다. |
|
||||||
|
|
||||||
|
검토한 주요 테스트는 `AudioContentRepositoryLocalizedNameTest`, `ExplorerLocalizedNameEndToEndTest`,
|
||||||
|
`LegacyCurrentNameReadIntegrationTest`, `ExplorerServiceTest`, `LegacyFanTalkReplyCharacterizationTest`,
|
||||||
|
`CreatorLocalizedNameServiceTest`다. 삭제 테스트는 제거된 래퍼 전용이며 기존 업무·번역·권한 및 v2 회귀는 유지한다.
|
||||||
|
새 raw/shared fixture와 단언에서 정적으로 확정 가능한 실패는 발견하지 못했다.
|
||||||
|
|
||||||
|
## 검증과 한계
|
||||||
|
|
||||||
|
- 부모와 검토자의 `GIT_MASTER=1 git diff --check`: 오류 없음.
|
||||||
|
- 부모의 `rg` 잔여 참조/공유 caller 대조 및 미추적 변경 테스트 4개 `git diff --no-index --check`: 지적 없음.
|
||||||
|
- 사용자 제한으로 Gradle·빌드·컴파일·테스트·lint·앱·HTTP·DB 실행은 하지 않았다. LSP 도구도 제공되지 않는다.
|
||||||
|
- 실제 SQL 실행, Spring 컨텍스트 기동, HTTP 회귀는 미검증이다. COR013의 과거 통과 기록으로 대체하지 않는다.
|
||||||
|
- 검증용 프로세스·포트·DB 연결을 생성하지 않았다. 운영 적용·배포·커밋·푸시도 하지 않았다.
|
||||||
|
- 확정 회귀 수정 Task 전환 항목 없음. 실행 허용 후 COR014/COR015의 focused·영향 회귀 검증이 필요하다.
|
||||||
|
|
||||||
|
## COR016 실행 결과에 따른 후속 판정
|
||||||
|
|
||||||
|
- 사용자 전체 실행에서 2271건 중 2건 실패가 확인됐다. 앞선 정적 리뷰는 H2 번역 JSON fixture 차이를 놓쳤으며 실행 통과 증거로 사용하지 않는다.
|
||||||
|
- 확정 항목 `REV-COR016-001`: AudioContentRepositoryLocalizedNameTest의 두 새 번역 fixture가 JSON 객체 대신 JSON 문자열로 저장된다. 랭킹은 converter 역직렬화 예외, 시리즈는 빈 번역 제목의 원문 fallback으로 실패한다. 요구사항·기대값은 유효하고 두 테스트 모두 유지 대상이다.
|
||||||
|
- 소유 Goal: `JSON-FIXTURE-T1`. 수정은 두 fixture에 기존 H2 FORMAT JSON 적재 패턴을 추가하는 것으로 제한했다. 운영 코드와 단언은 그대로다.
|
||||||
|
- focused 검증: `./gradlew test --tests 'kr.co.vividnext.sodalive.content.AudioContentRepositoryLocalizedNameTest' --offline --console=plain` exit 0, 6건 모두 통과, 22초. 실제 H2/JPA 조회에서 닉네임 raw/KO/JA와 번역 제목 단언을 확인했다.
|
||||||
|
- 전체 회귀·ktlint 및 수정분 독립 리뷰: 진행 중. 그 결과 확인 전 전체 완료로 간주하지 않는다.
|
||||||
|
|
||||||
|
### COR016 최종 결과
|
||||||
|
|
||||||
|
- 독립 좁은 리뷰 `ses_ec1576ac3ffesO9K10ayy0JKzr`: **APPROVE**. H2-only scoped update와 flush/clear 순서, 단언 보존을 확인했다.
|
||||||
|
- 전체 테스트: 323 suites/2271 tests/실패0/오류0/skip0. 첫 복합 명령은 test lint 줄바꿈 오류로 종료 코드 1이므로 전체 명령 성공으로 기록하지 않는다.
|
||||||
|
- 후속 형식 보정: 테스트의 인자 줄바꿈과 이전 HomeService 래퍼 전환 호출의 줄바꿈 1곳을 정리했다. 운영 의미와 기대값 변화는 없다.
|
||||||
|
- 최종 focused+전체 ktlint 명령: 종료 코드 0, 40초, 대상 클래스6건+HomeService1건 모두 통과하고 ktlintCheck도 통과했다. 전체 회귀 재실행은 형식 변경뿐이므로 생략했다.
|
||||||
|
- `REV-COR016-001` 수정·검증 완료. 부모가 전체/후속 XML 집계와 테스트 프로세스 잔존 없음, HomeService 공백 제거 내용 동일을 확인했다. 상세 명령·로그는 plan-task.md의 COR016 기록을 따른다.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
# COR017 미사용 탭 제외 리뷰
|
||||||
|
|
||||||
|
## 범위와 판정
|
||||||
|
|
||||||
|
- 날짜: 2026-10-15
|
||||||
|
- 기준: PRD COR017, 계획 UNUSED-TAB-T1. 기존 COR012~016 전체 diff가 아니라 미사용 탭 전용 코드 제거 변경만 평가한다.
|
||||||
|
- 검토 세션: `ses_ec125e618ffej4zvXb6whEPb0z`.
|
||||||
|
- 판정: **APPROVE**, 차단 문제 없음. 미사용 탭의 원본 닉네임 보존은 요구하지 않는다.
|
||||||
|
|
||||||
|
## 근거
|
||||||
|
|
||||||
|
| 기준 | 결과 |
|
||||||
|
|---|---|
|
||||||
|
| S1 탭 무변경 | content/main/tab의 git diff HEAD가 비어 있다. 7개 서비스의 명시 null 인자13곳이 제거됐으며 API/파일 자체는 유지한다. |
|
||||||
|
| S2 실사용 조회 보존 | Main/Theme/Home의 언어 전달과 랭킹·시리즈 제목 locale을 유지한다. nameLang 인자를 제거하고 랭킹의 기존 nullable locale 경계 및 큐레이션 raw 조회는 보존한다. |
|
||||||
|
| S3 테스트 유효성 | 실사용 KO/JA 이름·USER/원본 fallback·제목 번역/fallback·큐레이션2경로 검증과 FORMAT JSON 적재를 유지한다. ID 충돌 시 다음 고유 콘텐츠 ID를 사용하는 fixture 보정은 기존 assertNotEquals와 기능 단언을 약화하지 않는다. |
|
||||||
|
|
||||||
|
## 실행 증거
|
||||||
|
|
||||||
|
- focused 최초17건 중 fixture ID 가정 실패1건을 확인하고 같은 지정 테스트 파일만 보정했다. 최종17건 및 ktlint 통과, 21초. 두 결과는 creatorname-cor017-focused.log에 보존했다.
|
||||||
|
- 전체: `./gradlew test ktlintCheck --continue --offline --console=plain` exit0, 8분36초. 부모가 XML을 합산해 323 suites/2271 tests/실패0/오류0/skip0을 확인했다. 로그는 임시 opencode 디렉터리의 creatorname-cor017-full.log다.
|
||||||
|
- 실제 H2/JPA·기존 HTTP/WS 회귀 포함. JPA/Hikari 종료, 관련 프로세스 잔존 없음, localhost:52527 후속 curl 연결 실패(exit7)를 확인했다.
|
||||||
|
- 최종 탭 HEAD diff 없음과 git diff --check 무지적. 검토 시 대기하던 전체 실행 Gate도 해소됐다.
|
||||||
|
- 신규 회귀 수정 Task 전환 항목 없음. 운영 DB/배포/커밋/푸시는 하지 않았으며 운영 MySQL·실제 모바일/provider 검증은 수행하지 않았다.
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
-- DDL 파일 작성 완료, DB 미적용. 별도 승인 전 실행 금지
|
||||||
|
-- 운영 적용 전 실제 member.id 타입(부호 포함), member 테이블 engine 및 collation을 확인한다.
|
||||||
|
|
||||||
|
CREATE TABLE creator_localized_name (
|
||||||
|
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '식별자',
|
||||||
|
member_id BIGINT NOT NULL COMMENT '크리에이터 회원 ID (member.id 참조)',
|
||||||
|
lang VARCHAR(10) NOT NULL COMMENT 'Lang enum 이름 (KO/EN/JA), 현재 관리 대상 JA',
|
||||||
|
display_name VARCHAR(255) NOT NULL COMMENT '수동 등록 표시 이름 (비활성 시 마지막 값 보존)',
|
||||||
|
is_active TINYINT(1) NOT NULL DEFAULT 1 COMMENT '활성 여부 (0: 이름 해제, 1: 표시 및 별칭 검색 대상)',
|
||||||
|
created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '생성 시각',
|
||||||
|
updated_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '수정 시각',
|
||||||
|
PRIMARY KEY (id),
|
||||||
|
CONSTRAINT uk_creator_localized_name_member_lang UNIQUE (member_id, lang),
|
||||||
|
CONSTRAINT fk_creator_localized_name_member
|
||||||
|
FOREIGN KEY (member_id) REFERENCES member (id)
|
||||||
|
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='크리에이터 언어별 수동 표시 이름';
|
||||||
@@ -0,0 +1,370 @@
|
|||||||
|
# P2 v2 HTTP 이름 적용 지도 및 GREEN 인계
|
||||||
|
|
||||||
|
> 2026-10-15 COR013의 현재 구현 상태는 문서 마지막의 **COR013 JOIN 전환 manifest**를 따른다. 위아래의 COR012 batch 설계/검증 이력은 당시 기록으로 보존한다. COR013 runtime GREEN은 부모의 직렬 검증 전까지 미확인이다.
|
||||||
|
|
||||||
|
## 상태와 경계
|
||||||
|
|
||||||
|
- 기준: `prd.md` COR012 및 `plan-task.md` P2-T1, NAME002/003/005/006. 첫 단계의 통합 테스트 4개에 이어 아래 앱 표면의 운영 연결과 필수 생성자/fixture 적응을 구현했다. PRD/plan, 설정, DDL, core/legacy 전달 코드는 수정하지 않았다.
|
||||||
|
- **구현됨, 부모 직렬 GREEN 검증 대기**다. 부모 RED 로그 `creatorname-consumer-red.log` 100~110행에서 네 테스트의 실제 이름 assertion 실패를 확인했다. 아래 모든 HTTP 행과 blocked-message 행은 구현했으며 테스트/빌드/린트 통과나 수동 HTTP QA 완료를 주장하지 않는다.
|
||||||
|
- 경로 약어: `M/` = `src/main/kotlin/kr/co/vividnext/sodalive/`, `T/` = `src/test/kotlin/kr/co/vividnext/sodalive/`. 아래 경로는 이 prefix를 붙인 정확한 기존 파일이다.
|
||||||
|
- `v2.usercreatorchat`, `v2.creatorname`은 다른 소유자의 수정 영역이다. 추가 승인된 `v2.chat` DTO/service/controller 및 `chat/room/repository/ChatRoomRepository.kt`의 AI projection만 이 세션에서 수정했다. 검색/legacy/비동기 전달은 부모 작업이다.
|
||||||
|
|
||||||
|
## 공통 적용 결정
|
||||||
|
|
||||||
|
1. 기존 `M/i18n/LangInterceptor.kt#preHandle`이 `Accept-Language`를 해석한다. 각 HTTP controller에서 `LangContext.lang`을 읽어 facade의 명시 `lang: Lang` 인자로 전달한다. 이름 기능 때문에 내부 서비스에서 request scope를 새로 읽거나 언어를 다시 추정하지 않는다. 기존 콘텐츠 번역 로직의 언어 선택은 별도 기능으로 보존한다.
|
||||||
|
2. facade가 조회 결과의 **실제 이름 주인 member ID**를 합쳐 `CreatorLocalizedNameService.findLocalizedNames(memberIds, lang)`을 batch 호출한다. 응답 전 domain/record `copy`로 `map[id] ?: originalName`을 선택한다. 하나의 목록에 DB 행별 호출을 하지 않는다. 공유 DTO, JPA `Member.nickname`, principal, 저장 snapshot을 변이하지 않고 공개 DTO field 이름/타입도 유지한다.
|
||||||
|
3. CREATOR가 댓글 작성자/답글 작성자/후원자로 등장해도 적용 대상이다. `writerId`, donor `memberId`로 찾으며 채널 owner ID를 대신 쓰지 않는다. USER는 같은 이름 필드여도 그대로다. foundation의 활성 JA + 현재 `Member.role == CREATOR` 필터를 사용하고 `memberKind`만으로 판정하지 않는다.
|
||||||
|
4. 중립 ranking snapshot 이름은 현재 원본이 아니다. 이 두 ranking consumer는 owner IDs로 `MemberRepository.findNicknamesByIds`도 한 번 호출해 엔티티 로드 없이 현재 원본을 확보하고 JA map을 우선한다. KO/EN 또는 JA 미등록은 **현재** nickname으로 fallback한다. 삭제된 owner의 기존 표시 정책은 유지하며 저장 row를 갱신하지 않는다. 차단 마스킹된 creator ID `0`/빈 이름을 복구하지 않는다.
|
||||||
|
|
||||||
|
## 작성한 실제 회귀 테스트
|
||||||
|
|
||||||
|
| 정확한 기존 파일 / 새 심볼 | 실제 경로와 기대 동작 |
|
||||||
|
|---|---|
|
||||||
|
| `T/v2/api/content/all/adapter/in/web/MainContentAllEndToEndTest.kt#shouldSelectLocalizedCreatorNamesWithoutChangingAudioTitles` | `GET /api/v2/audio/contents?type=AUDIO`: 저장 후 원본을 변경해도 JA 별칭 우선, KO는 변경된 원본, JA 미등록 작성자는 원본, audio title 불변. 별도 USER를 먼저 저장해 content ID와 owner member ID를 분리하며 내부 owner ID는 JSON에 노출하지 않는다. |
|
||||||
|
| `T/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityEndToEndTest.kt#shouldLocalizeCreatorAuthorsAndLatestRepliesWithoutChangingUserNames` | `GET /api/v2/creator-channels/community-posts/{postId}`: post creator, CREATOR root comment, USER root comment, CREATOR latestReply를 함께 저장한다. JA는 CREATOR만 별칭이며 USER는 잔존 별칭 row가 있어도 원본이다. 본문은 이름 치환 대상이 아니다. |
|
||||||
|
| `T/v2/api/home/CreatorRankingControllerTest.kt#shouldRenderCurrentLocalizedNamesWithoutRewritingRankingSnapshots` | `GET /api/v2/home/rankings/creators`: 원본 이름으로 중립 snapshot 저장 후 회원 원본 변경. JA 별칭/JA 미등록 현재 원본/KO 현재 원본을 검증하고 DB snapshot 이름과 회원 원본 보존을 단언한다. |
|
||||||
|
| `T/v2/api/home/HomeRecommendationControllerTest.kt#shouldLocalizeNativeDebutCreatorResultsByRequestLanguage` | `GET /api/v2/home/recommendations/debut-creators`: 실제 live 활동으로 native CTE 후보를 만든다. 원본 변경 후 KO 현재 원본과 JA 별칭을 검증한다. repository/query service를 mock하지 않는다. |
|
||||||
|
|
||||||
|
픽스처는 기존 `saveMember`, `saveAudioContent`, `saveCommunity`, `saveCommunityComment`, `saveSnapshot`, `saveLiveRoom`과 실제 JPA 저장을 재사용한다. 별칭만 허용된 H2 `JdbcTemplate` INSERT로 저장했다. `member_id`, enum NAME `'JA'`, `display_name`, `is_active`, BaseEntity 시각 컬럼을 명시하며 production DDL 실행/새 schema 생성/반사 호출은 없다. USER의 alias row는 관리 API가 USER 저장을 허용한다는 뜻이 아니라 역할 변경 후 잔존 행과 같은 읽기 필터 회귀를 재현한다. 기존 번역/외부 미디어 mock은 유지했으며 이름 서비스는 mock하지 않았다.
|
||||||
|
|
||||||
|
### 부모 RED 명령
|
||||||
|
|
||||||
|
foundation 준비 후 한 번에 하나씩 실행한다. 첫 단계는 새 메서드만 좁혀서 실제 이름 assertion 실패인지 확인한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.content.all.adapter.in.web.MainContentAllEndToEndTest.shouldSelectLocalizedCreatorNamesWithoutChangingAudioTitles'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.community.adapter.in.web.CreatorChannelCommunityEndToEndTest.shouldLocalizeCreatorAuthorsAndLatestRepliesWithoutChangingUserNames'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.home.CreatorRankingControllerTest.shouldRenderCurrentLocalizedNamesWithoutRewritingRankingSnapshots'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest.shouldLocalizeNativeDebutCreatorResultsByRequestLanguage'
|
||||||
|
```
|
||||||
|
|
||||||
|
예상 RED 지점은 각각 `creatorNickname` JA 기대값, post/comment/reply JA 이름, ranking `nickname` JA 기대값, native debut `creatorNickname` JA 기대값이다. 테이블 미생성, 컴파일/컨텍스트 실패, SQL fixture 실패는 RED 증거로 취급하지 않는다. GREEN 뒤에는 같은 명령과 아래 HTTP owner의 전체 class 회귀를 수행한다.
|
||||||
|
|
||||||
|
### foundation 선행 인터페이스 확인
|
||||||
|
|
||||||
|
- 조사 시작 때 파일이 없었지만 인계 전 실제 소스를 다시 읽었다. `M/v2/creatorname/application/CreatorLocalizedNameService.kt#findLocalizedNames(memberIds: Collection<Long>, lang: Lang): Map<Long, String>`와 `#saveLocalizedName(creatorId: Long, lang: Lang, displayName: String)`가 현재 존재한다.
|
||||||
|
- `M/v2/creatorname/adapter/out/persistence/CreatorLocalizedNameEntity.kt`는 `creator_localized_name` 및 위 INSERT 컬럼, enum-string `Lang`, BaseEntity 상속과 일치한다. `M/v2/creatorname/adapter/in/web/AdminCreatorLocalizedNameController.kt`의 ADMIN PUT도 확인했다. 시그니처 추측이나 임시 타입은 추가하지 않았다.
|
||||||
|
- 후속 상태: 부모가 foundation 18 tests PASS 및 검토 완료를 전달했고 consumer RED도 실행했다. 초기 조사 당시의 repository `emptyMap()` 미구현 관찰은 더 이상 현재 blocker가 아니다. 이번 소비자는 실제 foundation 필수 생성자 의존성을 사용하며 nullable/optional service 우회가 없다.
|
||||||
|
|
||||||
|
## HTTP 표면별 정확한 적용 지점
|
||||||
|
|
||||||
|
아래 모든 행은 **구현됨, GREEN 대기**다. 각 controller는 `LangContext.lang`을 명시 전달하고 facade는 batch와 immutable copy를 수행한다. 아래 초기 설계와 달리 following news는 각 subtype 대신 상위 `HomeFollowingNews.creatorMemberId` 하나에 joined owner를 보존했다. live는 `HomeFollowingLive.creatorMemberId`를 사용한다. 공개 DTO에는 두 필드를 추가하지 않았다.
|
||||||
|
|
||||||
|
| 표면 / 응답 이름 | 기존 파일과 심볼 / 필요한 최소 변경 | HTTP test owner |
|
||||||
|
|---|---|---|
|
||||||
|
| 홈 추천 `lives`, `recentlyActiveCreators`, `recentDebutCreators`, `firstAudioContents`, `genreCreators[].creators`, `cheerCreators`, `popularCommunityPosts`의 `creatorNickname` | `M/v2/api/home/adapter/in/web/HomeRecommendationController.kt#getHomeRecommendations/getLives/getDebutCreators` → `M/v2/api/home/application/HomeRecommendationFacade.kt#getHomeRecommendations/getLives/getRecentDebutCreators`. 섹션 결과를 모은 후 ID 합집합 batch. banner의 국가 기반 `bannerLang`은 이름 언어로 재사용 금지. `HomeFirstAudioContentRecord.creatorId`는 이미 존재한다. | `T/v2/api/home/HomeRecommendationControllerTest.kt` |
|
||||||
|
| 홈 live `creatorNickname` | 위 `getLives` 및 `M/v2/api/home/live/adapter/in/web/HomeOnAirLiveController.kt#getOnAirLives` → `M/v2/api/home/live/application/HomeOnAirLiveFacade.kt#getOnAirLives`. 공유 `M/v2/recommendation/port/out/HomeRecommendationQueryPort.kt#HomeLiveRecommendationRecord`에 내부 `creatorMemberId` 추가, `M/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt#findLiveRecommendations`의 QueryDSL constructor projection에 `member.id` 추가. `liveRoomId/roomId`로 alias 조회 금지. | `T/v2/api/home/live/adapter/in/web/HomeOnAirLiveEndToEndTest.kt` |
|
||||||
|
| 홈 creator ranking `items[].nickname` | `M/v2/api/home/adapter/in/web/CreatorRankingController.kt#getCreatorRankings` → `M/v2/api/home/application/HomeCreatorRankingFacade.kt#getCreatorRankings`. `M/v2/ranking/application/CreatorRankingQueryService.kt#getCreatorRankings` 결과의 `items[].creatorId`를 batch key로 사용. snapshot 원본이 아닌 현재 Member fallback도 batch 조회. 기존 block mask와 rank/rankChange/isNew 유지. | `T/v2/api/home/CreatorRankingControllerTest.kt` |
|
||||||
|
| 팔로잉 creator/live/schedule/news 이름 및 recentChats | `M/v2/api/home/following/adapter/in/web/HomeFollowingController.kt#getFollowingTab` → `M/v2/api/home/following/application/HomeFollowingFacade.kt#getFollowingTab`. `M/v2/home/following/domain/HomeFollowing.kt`: `HomeFollowingCreator.creatorId`, `HomeFollowingSchedule.creatorId`, `HomeFollowingCreatorRankingNews.creatorId` 재사용. `HomeFollowingLive`, `HomeFollowingContentNews`, `HomeFollowingCommunityPostNews`에 내부 owner 추가. `M/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepository.kt#findOnAirLives/findRecentNews/toHomeFollowingNews`에 현재 join한 `creator.id` selection/전달 추가. `audioContent`, `photoContent`, `communityPost`의 이름과 creatorRanking `nickname`을 분기별 copy. recentChats는 아래 v2.chat 소유 경계 사용. | `T/v2/api/home/following/adapter/in/web/HomeFollowingEndToEndTest.kt` |
|
||||||
|
| 전체 콘텐츠 audio/series `creatorNickname` | `M/v2/api/content/all/adapter/in/web/MainContentAllController.kt#getContents` → `M/v2/api/content/all/application/MainContentAllFacade.kt#getContents`. `M/v2/content/all/domain/MainContentAll.kt#MainContentAllAudio/MainContentAllSeries`에 내부 `creatorMemberId` 추가. `M/v2/content/all/adapter/out/persistence/DefaultMainContentAllQueryRepository.kt#findAudios/findSeries`에서 전달. audio row는 owner를 이미 select하지만 domain에서 버린다. series는 `member.id` selection도 추가. title 번역/정렬/pagination 불변. | `T/v2/api/content/all/adapter/in/web/MainContentAllEndToEndTest.kt` |
|
||||||
|
| 콘텐츠 추천 5종 audio card `creatorNickname` | `M/v2/api/content/recommendation/adapter/in/web/AudioRecommendationController.kt#getRecommendations` → `M/v2/api/content/recommendation/application/AudioRecommendationFacade.kt#getRecommendations`. `M/v2/content/recommendation/domain/AudioRecommendation.kt#AudioCard`에 내부 owner 추가. `M/v2/content/recommendation/adapter/out/persistence/DefaultAudioRecommendationQueryRepository.kt#audioRows/toAudioCards`의 이미 select한 `audioContent.member.id`를 card까지 전달. `latestAudios/newAndHotAudios/freeAudios/pointAudios/recommendedAudios` 합집합 batch. `OriginalSeries`, `CommentedAudio`는 이름 필드 없음. | `T/v2/api/content/recommendation/adapter/in/web/AudioRecommendationEndToEndTest.kt` |
|
||||||
|
| 콘텐츠 overview `items[].creatorNickname` | `M/v2/api/content/overview/adapter/in/web/ContentOverviewController.kt#getContents` → `M/v2/api/content/overview/application/ContentOverviewFacade.kt#getContents/getNewAndHotContents/getFirstAudioContents`. NEW_AND_HOT은 위 `AudioCard` owner 추가 사용. FIRST_AUDIO는 `M/v2/recommendation/port/out/HomeRecommendationQueryPort.kt#HomeFirstAudioContentRecord.creatorId`가 이미 있으므로 새 owner 필드 불필요. native first-audio CTE 결과 이름도 batch copy. | `T/v2/api/content/overview/adapter/in/web/ContentOverviewEndToEndTest.kt` |
|
||||||
|
| 오디오 ranking `items[].creatorNickname` | `M/v2/api/content/ranking/adapter/in/web/AudioRankingController.kt#getRankings` → `M/v2/api/content/ranking/application/AudioRankingFacade.kt#getRankings`. `M/v2/content/ranking/domain/AudioRanking.kt#AudioRankingItem`에 내부 `creatorMemberId` 추가. `M/v2/content/ranking/application/AudioRankingQueryService.kt#toItem`에서 기존 snapshot record owner를 버리지 않고 전달. facade가 현재 Member + alias batch로 선택. locale별 content title은 손대지 않음. | `T/v2/api/content/ranking/adapter/in/web/AudioRankingControllerTest.kt` |
|
||||||
|
| 채널 home `creator.nickname`, `notices/communities[].creatorNickname`, `fanTalk.latestFanTalk.nickname`, `channelDonations[].nickname` | `M/v2/api/creator/channel/home/adapter/in/web/CreatorChannelHomeController.kt#getHome` → `M/v2/api/creator/channel/home/application/CreatorChannelHomeFacade.kt#getHome`. `M/v2/creator/channel/home/domain/CreatorChannelHome.kt`의 creator/post/member IDs 재사용. donation만 내부 donor memberId 추가: 같은 파일 `CreatorChannelDonation`, `M/v2/creator/channel/home/port/out/CreatorChannelHomeQueryPort.kt#CreatorChannelDonationRecord`, `M/v2/creator/channel/home/adapter/out/persistence/DefaultCreatorChannelHomeQueryRepository.kt#findChannelDonations`, `M/v2/creator/channel/home/application/CreatorChannelHomeQueryService.kt#toDomain`. 모든 이름 주인 합집합 batch, fan/donor가 CREATOR인 경우도 적용. | `T/v2/api/creator/channel/home/CreatorChannelHomeEndToEndTest.kt` |
|
||||||
|
| 채널 community tab/detail 및 댓글/답글 `creatorNickname/writerNickname` | `M/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityController.kt#getCommunityTab/getCommunityPostDetail/getCommunityComments/getCommunityReplies` → `M/v2/api/creator/channel/community/application/CreatorChannelCommunityFacade.kt`의 같은 네 심볼. `M/v2/creator/channel/community/domain/CreatorChannelCommunityTab.kt#CreatorChannelCommunityPost.creatorId`, `M/v2/creator/channel/community/domain/CreatorChannelCommunityPostDetail.kt#CreatorChannelCommunityComment/CreatorChannelCommunityReply.writerId`가 존재. detail은 post/comment/latestReply ID를 합쳐 한 번 lookup. 별도 댓글/답글 endpoint도 각각 처리. translator의 postId와 이름의 memberId를 혼용하지 않는다. | `T/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityEndToEndTest.kt` |
|
||||||
|
| 채널 fanTalk와 creatorReplies `writerNickname` | `M/v2/api/creator/channel/fantalk/adapter/in/web/CreatorChannelFanTalkController.kt#getFanTalkTab` → `M/v2/api/creator/channel/fantalk/application/CreatorChannelFanTalkFacade.kt#getFanTalkTab`. `M/v2/creator/channel/fantalk/domain/CreatorChannelFanTalkTab.kt#CreatorChannelFanTalk/CreatorChannelFanTalkReply.writerId` 합집합으로 batch. USER 팬 이름 보존, CREATOR 작성자/답글은 적용. | `T/v2/api/creator/channel/fantalk/adapter/in/web/CreatorChannelFanTalkEndToEndTest.kt` |
|
||||||
|
| 채널 donation `rankings[].nickname`, `donations[].nickname` | `M/v2/api/creator/channel/donation/adapter/in/web/CreatorChannelDonationController.kt#getDonationTab` → `M/v2/api/creator/channel/donation/application/CreatorChannelDonationFacade.kt#getDonationTab`. `M/v2/creator/channel/donation/domain/CreatorChannelDonationTab.kt#CreatorChannelDonationRanking.userId`는 실제 donor member ID다. donation list는 donor ID를 추가: 같은 파일 `CreatorChannelDonation`, `M/v2/creator/channel/donation/port/out/CreatorChannelDonationQueryPort.kt#CreatorChannelDonationRecord`, `M/v2/creator/channel/donation/adapter/out/persistence/DefaultCreatorChannelDonationQueryRepository.kt#findChannelDonations`, `M/v2/creator/channel/donation/application/CreatorChannelDonationQueryService.kt#toDomain`. USER 불변, CREATOR donor만 적용. | `T/v2/api/creator/channel/donation/adapter/in/web/CreatorChannelDonationEndToEndTest.kt` |
|
||||||
|
| 기프트 목록/상세 `counterpartNickname`, `giftInfo.recipientCreatorNickname/senderNickname` | `M/v2/api/gift/adapter/in/web/GiftController.kt#getGifts/getGiftDetail`가 명시 Lang 전달. facade 없는 기존 구조이므로 `M/v2/gift/application/GiftQueryService.kt#getGifts/getGiftDetail/toGiftListItem`의 결과 조립에 resolver 주입. 목록은 이미 batch 조회한 `membersById`의 counterpart IDs, 상세는 이미 읽은 sender/recipient IDs를 묶어 lookup. `applicationNo/giftId`는 key가 아님. `senderInfo.name`, 수령인 실명/주소, categoryNameSnapshot, status/sizeName은 치환 금지. command 응답에는 회원 이름이 없어 수정하지 않음. | `T/v2/api/gift/adapter/in/web/GiftControllerTest.kt` |
|
||||||
|
|
||||||
|
## 정상 응답 밖의 표시 경계
|
||||||
|
|
||||||
|
- 채널 audio/series/live 정상 응답에는 creator 이름이 없다. `M/v2/api/creator/channel/common/dto/CreatorChannelAudioContentResponse.kt`, `M/v2/api/creator/channel/audio/dto/CreatorChannelAudioTabResponse.kt`, `M/v2/api/creator/channel/series/dto/CreatorChannelSeriesTabResponse.kt`, `M/v2/api/creator/channel/live/dto/CreatorChannelLiveTabResponse.kt`의 title/seriesName을 creator 이름으로 치환하거나 필드를 신설하지 않는다.
|
||||||
|
- 단, 아래 일곱 query service는 blocked-access 메시지에 `String.format(messageTemplate, creator.nickname)`을 넣는다. 이 이름도 사용자에게 표시된다. 각각 controller → facade → 해당 함수로 `lang: Lang`을 전달하고 **오류 분기에서만** 대상 creator 한 명을 resolver batch API로 조회해 format한다. 정상 응답 facade lookup과 중복 조회하지 않는다. 기존 번역/권한/차단 정책과 message key는 유지한다.
|
||||||
|
|
||||||
|
| 정확한 query service / 심볼 | 명시 Lang 전달 facade | 기존 HTTP test owner |
|
||||||
|
|---|---|---|
|
||||||
|
| `M/v2/creator/channel/home/application/CreatorChannelHomeQueryService.kt#getHome` | `M/v2/api/creator/channel/home/application/CreatorChannelHomeFacade.kt#getHome` | `T/v2/api/creator/channel/home/CreatorChannelHomeEndToEndTest.kt` |
|
||||||
|
| `M/v2/creator/channel/community/application/CreatorChannelCommunityQueryService.kt#getCommunityTab` | `M/v2/api/creator/channel/community/application/CreatorChannelCommunityFacade.kt#getCommunityTab` | `T/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityEndToEndTest.kt` |
|
||||||
|
| `M/v2/creator/channel/fantalk/application/CreatorChannelFanTalkQueryService.kt#getFanTalkTab` | `M/v2/api/creator/channel/fantalk/application/CreatorChannelFanTalkFacade.kt#getFanTalkTab` | `T/v2/api/creator/channel/fantalk/adapter/in/web/CreatorChannelFanTalkEndToEndTest.kt` |
|
||||||
|
| `M/v2/creator/channel/donation/application/CreatorChannelDonationQueryService.kt#getDonationTab` | `M/v2/api/creator/channel/donation/application/CreatorChannelDonationFacade.kt#getDonationTab` | `T/v2/api/creator/channel/donation/adapter/in/web/CreatorChannelDonationEndToEndTest.kt` |
|
||||||
|
| `M/v2/creator/channel/audio/application/CreatorChannelAudioQueryService.kt#getAudioTab` | `M/v2/api/creator/channel/audio/application/CreatorChannelAudioFacade.kt#getAudioTab` | `T/v2/api/creator/channel/audio/adapter/in/web/CreatorChannelAudioEndToEndTest.kt` |
|
||||||
|
| `M/v2/creator/channel/series/application/CreatorChannelSeriesQueryService.kt#getSeriesTab` | `M/v2/api/creator/channel/series/application/CreatorChannelSeriesFacade.kt#getSeriesTab` | `T/v2/api/creator/channel/series/adapter/in/web/CreatorChannelSeriesEndToEndTest.kt` |
|
||||||
|
| `M/v2/creator/channel/live/application/CreatorChannelLiveQueryService.kt#getLiveTab` | `M/v2/api/creator/channel/live/application/CreatorChannelLiveFacade.kt#getLiveTab` | `T/v2/api/creator/channel/live/adapter/in/web/CreatorChannelLiveEndToEndTest.kt` |
|
||||||
|
|
||||||
|
audio/series/live의 controller 파일은 각각 `M/v2/api/creator/channel/audio/adapter/in/web/CreatorChannelAudioController.kt`, `M/v2/api/creator/channel/series/adapter/in/web/CreatorChannelSeriesController.kt`, `M/v2/api/creator/channel/live/adapter/in/web/CreatorChannelLiveController.kt`다. 성공 DTO는 그대로 두고 위 오류 경계의 명시 Lang 인자만 전달한다.
|
||||||
|
|
||||||
|
## neutral snapshot / native producer-consumer 분류
|
||||||
|
|
||||||
|
| 기존 파일 / 심볼 | 분류와 구현 결정 |
|
||||||
|
|---|---|
|
||||||
|
| `M/v2/ranking/adapter/out/persistence/DefaultCreatorRankingAggregationRepository.kt#aggregateAllCandidates/toCandidate` → `M/v2/ranking/application/CreatorRankingSnapshotRefreshService.kt` → `M/v2/ranking/adapter/out/persistence/CreatorRankingSnapshot.kt` → `M/v2/ranking/adapter/out/persistence/DefaultCreatorRankingSnapshotRepository.kt` → `CreatorRankingQueryService#getCreatorRankings` | native SQL이 당시 원본 `Member.nickname`을 저장하며 Lang 컬럼/수신자별 렌더링이 없는 **중립 materialization**이다. 역사 면제 아님. 조회 현재 이름 선택만 추가하고 저장 snapshot/rank/score는 불변. 새 ranking 회귀가 이 구분을 고정한다. |
|
||||||
|
| `M/v2/content/ranking/adapter/out/persistence/DefaultAudioRankingAggregationRepository.kt#AGGREGATION_SQL` → `M/v2/content/ranking/application/AudioRankingSnapshotRefreshService.kt` → `M/v2/content/ranking/adapter/out/persistence/AudioRankingSnapshot.kt` → `M/v2/content/ranking/adapter/out/persistence/DefaultAudioRankingSnapshotPersistenceAdapter.kt#toRecords/toRecord` → `AudioRankingQueryService#toItem` | `c.member_id`, `m.nickname`을 저장하는 중립 creator 이름. consumer가 locale별 `ContentTranslation` title을 고르지만 creator 이름이 언어별 역사 데이터가 되는 것은 아니다. owner 전달 + 현재 이름 batch 필요. title/renderedPayload/기존 snapshot row는 불변. |
|
||||||
|
| `M/v2/recommendation/adapter/out/persistence/RecommendationSnapshot.kt` 및 `M/v2/recommendation/application/RecommendationSnapshotRefreshService.kt` → `M/v2/recommendation/application/HomeRecommendationQueryService.kt#findCheerCreatorRecommendations/findPopularCommunityRecommendations` | section/targetId/score 후보 snapshot이고 표시 이름의 언어 snapshot이 아니다. detail 조회(`findCheerCreatorRecommendationDetails`, `findPopularCommunityRecommendationDetails`)에서 현재 member ID와 원본을 얻어 facade에서 batch. targetId는 section에 따라 creator 또는 community resource이므로 alias key로 그대로 사용하지 않음. |
|
||||||
|
| `M/v2/home/following/application/HomeFollowingNewsPublishService.kt#publishToFollowers` → `M/v2/home/following/adapter/out/persistence/HomeFollowingNewsInbox.kt` → `DefaultHomeFollowingQueryRepository#findRecentNews/toHomeFollowingNews` | Lang 없는 중립 inbox. producer는 raw creatorNickname/title/body를 저장하지만 **HTTP consumer는 creator를 join해 현재 nickname을 다시 읽는다**. creator-ranking title에 저장된 과거 nickname은 현재 응답의 이름 원천이 아니다. consumer의 joined owner ID를 전달해 언어 선택. inbox 저장값/producer는 수정하지 않음. |
|
||||||
|
| `DefaultHomeRecommendationQueryRepository#findRecentlyActiveCreators/findRecentDebutCreators/findFirstAudioContents/findCreatorsByGenre` | native SQL에서 현재 creator 이름을 읽는 current lookup. 결과 record의 creatorId로 batch한다. 새 native debut HTTP 회귀는 CTE/query/service를 실제 실행하도록 작성했다. |
|
||||||
|
| `M/v2/content/recommendation/adapter/out/persistence/DefaultAudioRecommendationQueryRepository.kt` | card의 이름은 현재 QueryDSL member join. native most-commented 조회에는 creator 이름 응답이 없어 comment 본문/title을 치환하지 않음. recommendation candidate snapshot도 현재 card를 다시 조회하므로 이름 저장 변경 불필요. |
|
||||||
|
|
||||||
|
이 v2 조사에서 **이미 수신 언어로 렌더된 역사 이름 snapshot으로 면제할 HTTP 이름 필드는 확인되지 않았다**. `snapshot`이라는 클래스명, content title 번역 또는 과거 생성 시점만으로 면제하지 않는다.
|
||||||
|
|
||||||
|
## 다른 소유자와의 연결 및 원본 유지 영역
|
||||||
|
|
||||||
|
- `M/v2/chat/controller/ChatRoomListController.kt#getRooms` → `M/v2/chat/service/ChatRoomListService.kt#getRooms/toResponse`: DM의 `targetName`은 creator 또는 USER 상대 회원 이름이므로 대상이다. AI `targetName`은 `M/chat/room/repository/ChatRoomRepository.kt#findAiChatListRooms`의 `pc.character.name`이며 **캐릭터 제목이므로 제외**한다. 최근 메시지 본문도 이름 문자열 치환 대상이 아니다.
|
||||||
|
- 초기 조사에서 DM owner ID는 `M/v2/chat/dto/ChatRoomListDtos.kt#ChatRoomListQueryDto`에서 누락되어 있었다. 후속 승인에 따라 DTO/AI projection은 이 세션이, `M/v2/usercreatorchat/repository/UserCreatorChatRoomRepository.kt#findDmChatListRooms`의 `opponent.member.id` projection은 delivery 소유자가 각각 구현했다. 이 세션은 DM repository를 수정하지 않았다. roomId/character creatorMemberId로 이름을 치환하지 않는다.
|
||||||
|
- 후속 구현: `ChatRoomListService`가 최종 `pageRows`의 DM member IDs를 batch 처리한다. `HomeFollowingFacade`는 같은 명시 Lang으로 이미 선택된 recentChats를 받아 다시 치환하지 않는다. 실제 계약은 `ChatRoomListQueryDto(roomId, chatType, targetName, targetImagePath, lastMessage, messageType, lastMessageAt, opponentMemberId: Long?)`다. trailing 필드에 default는 없다. AI는 `cast(null as long)`, delivery 소유자가 변경한 DM JPQL은 마지막 `opponent.member.id`를 전달함을 read/rg로 확인했다. 공개 `ChatRoomListItemResponse`는 변경하지 않았다. 해당 소유자의 변경은 이 세션의 수정 파일에 포함하지 않는다.
|
||||||
|
- 앱이 아닌 운영 식별/편집 응답은 원본 유지: `M/v2/gift/application/GiftAdminQueryService.kt#getGifts/getGiftDetail` 및 `M/v2/api/admin/gift/adapter/in/web/AdminGiftController.kt`, `M/v2/api/admin/aicharacter/content/AiCharacterAdminAudioContentMapper.kt#toResponse`, `M/v2/api/admin/aicharacter/community/AiCharacterAdminCommunityPostFacade.kt#toResponse`, `M/v2/api/admin/aicharacter/fantalk/AiCharacterAdminFanTalkFacade.kt#list/createReply/updateReply`. 앱 DTO를 재사용하더라도 DTO 전역 변환을 적용하지 않는다. admin nickname 검색/원본 편집/배송 실명은 별칭 표시 대상과 분리한다.
|
||||||
|
- `M/v2/api/home/application/HomeRecommendationFacade.kt#HomeAiCharacterRecommendationRecord.toItem`의 `name`, content/series/live title, event banner, `GiftStatus.displayName`, `GiftSize.displayName`은 creator 이름이 아니며 수정하지 않는다. ADMIN charge/ranking job API는 작업 상태 응답이며 creator display-name 필드가 없다.
|
||||||
|
|
||||||
|
## GREEN 생성자·fixture 적응 지도
|
||||||
|
|
||||||
|
현재 테스트 생성자와 의존성을 읽고 `rg`로 직접 생성 지점을 대조해 아래 적응을 수행했다. 기존 facade 단위 테스트는 필수 resolver mock과 명시 Lang을 전달한다. controller slice의 `@MockBean LangContext` getter는 `Lang.KO`로 stub했다. 새 기능의 실제 DB/HTTP 검증은 위 통합 테스트가 담당하며 기존 assertion은 완화하지 않았다. `GiftQueryServiceTest`에는 foundation service/repository를 실제 import했다.
|
||||||
|
|
||||||
|
| 정확한 기존 test 파일 | 최소 적응 |
|
||||||
|
|---|---|
|
||||||
|
| `T/v2/api/home/application/HomeRecommendationFacadeTest.kt`; `T/v2/api/home/HomeRecommendationControllerTest.kt` | 현재 `HomeRecommendationFacade(queryService, preferenceService, "https://cdn.test")`. 새 resolver 및 `lang` 호출 인자 추가. 후자는 장애 로그 test에서도 facade를 3곳 직접 생성하므로 Spring 주입만 보고 끝내지 않음. |
|
||||||
|
| `T/v2/api/home/live/application/HomeOnAirLiveFacadeTest.kt`; `T/v2/recommendation/application/HomeRecommendationQueryServiceTest.kt` | facade resolver/lang 추가와 `HomeLiveRecommendationRecord` owner fixture 추가. QueryDSL positional constructor와 모든 fake port live record도 동기화. |
|
||||||
|
| `T/v2/api/home/following/application/HomeFollowingFacadeTest.kt`; `T/v2/api/home/following/dto/HomeFollowingTabResponseTest.kt`; `T/v2/home/following/application/HomeFollowingQueryServiceTest.kt` | facade resolver/lang 및 chat service 명시 Lang stub. `HomeFollowingLive/ContentNews/CommunityPostNews` 내부 owner fixture 추가. 공개 response DTO와 JSON 필드는 추가하지 않음. |
|
||||||
|
| `T/v2/api/content/all/application/MainContentAllFacadeTest.kt`; `T/v2/content/all/application/MainContentAllQueryServiceTest.kt`; `T/v2/api/content/all/dto/MainContentAllTabResponseTest.kt` | 현재 `MainContentAllFacade(queryService)`에 resolver 추가, facade 호출 lang 추가. `MainContentAllAudio/Series`의 필수 내부 owner fixture를 모두 채움. |
|
||||||
|
| `T/v2/api/content/recommendation/application/AudioRecommendationFacadeTest.kt`; `T/v2/content/recommendation/application/AudioRecommendationQueryServiceTest.kt`; `T/v2/api/content/overview/application/ContentOverviewFacadeTest.kt`; `T/v2/api/content/overview/dto/ContentOverviewPageResponseTest.kt` | `AudioRecommendationFacade(queryService)` 및 overview의 기존 두 query service/preference/host 의존성은 유지하고 resolver/lang 추가. 모든 `AudioCard` fixture owner 추가. FIRST_AUDIO record는 기존 creatorId 사용. |
|
||||||
|
| `T/v2/api/content/ranking/application/AudioRankingFacadeTest.kt`; `T/v2/api/content/ranking/dto/AudioRankingResponseTest.kt` | `AudioRankingFacade(queryService)`에 resolver 및 현재 Member batch 조회 의존성/lang 추가. `AudioRankingItem` 내부 owner fixture 추가. contentId를 owner로 쓰지 않는 assertion. |
|
||||||
|
| `T/v2/api/creator/channel/home/application/CreatorChannelHomeFacadeTest.kt`; `T/v2/creator/channel/home/application/CreatorChannelHomeQueryServiceTest.kt`; `T/v2/api/creator/channel/home/adapter/in/web/CreatorChannelHomeControllerTest.kt` | home facade resolver/lang, donor record/domain owner fixture, query service의 오류 분기 resolver/lang 의존성 및 controller facade stubs 조정. `CreatorChannelHomeQueryServiceTest`의 donation copy 기대값에도 필드 전파. |
|
||||||
|
| `T/v2/api/creator/channel/community/application/CreatorChannelCommunityFacadeTest.kt`; `T/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityControllerTest.kt`; `T/v2/creator/channel/community/application/CreatorChannelCommunityQueryServiceTest.kt` | 현재 `CreatorChannelCommunityFacade(service)`를 여러 메서드에서 직접 생성. resolver/lang 추가, detail/comment/latestReply 합집합 검증. query service 오류 분기에도 명시 Lang/resolver 전달. |
|
||||||
|
| `T/v2/api/creator/channel/fantalk/application/CreatorChannelFanTalkFacadeTest.kt`; `T/v2/creator/channel/fantalk/application/CreatorChannelFanTalkQueryServiceTest.kt`; `T/v2/api/creator/channel/donation/application/CreatorChannelDonationFacadeTest.kt`; `T/v2/creator/channel/donation/application/CreatorChannelDonationQueryServiceTest.kt`; `T/v2/creator/channel/donation/domain/CreatorChannelDonationQueryPolicyTest.kt` | facade/query service resolver와 명시 Lang, donor record/domain owner fixture 추가. writer/donor가 USER면 원본, CREATOR면 alias. ranking은 `LegacyCreatorChannelDonationRankingAdapter`가 넘긴 donor `userId` 재사용. |
|
||||||
|
| `T/v2/api/creator/channel/audio/application/CreatorChannelAudioFacadeTest.kt`; `T/v2/creator/channel/audio/application/CreatorChannelAudioQueryServiceTest.kt`; `T/v2/api/creator/channel/series/application/CreatorChannelSeriesFacadeTest.kt`; `T/v2/creator/channel/series/application/CreatorChannelSeriesQueryServiceTest.kt`; `T/v2/api/creator/channel/live/application/CreatorChannelLiveFacadeTest.kt`; `T/v2/creator/channel/live/application/CreatorChannelLiveQueryServiceTest.kt` | 성공 title DTO는 그대로. blocked-access 이름을 위한 명시 Lang forwarding 및 query service resolver 생성자 추가. 기존 controller MockMvc forwarding stubs의 추가 Lang도 일치시킴. |
|
||||||
|
| `T/v2/gift/application/GiftQueryServiceTest.kt#queryService`; `T/v2/api/gift/adapter/in/web/GiftControllerTest.kt` | 기존 helper는 여섯 repository + host로 `GiftQueryService`를 직접 생성한다. resolver 추가, `getGifts/getGiftDetail`에 Lang 지정. `@DataJpaTest` helper에는 실제 foundation service/repository를 구성하거나 import하며 조회 서비스를 mock해 버리지 않음. HTTP는 실제 queryService를 사용하고 기존 commandService mock은 유지. |
|
||||||
|
| `T/v2/chat/ChatRoomListServiceTest.kt`; `T/v2/chat/ChatRoomListControllerTest.kt` | service/controller 직접 생성 인자와 query DTO의 DM owner/AI null fixture, Lang 호출 인자를 조정. AI character name 불변과 DM USER 보존 확인. |
|
||||||
|
| `T/v2/content/all/adapter/out/persistence/DefaultMainContentAllQueryRepositoryTest.kt`; `T/v2/content/recommendation/adapter/out/persistence/DefaultAudioRecommendationQueryRepositoryTest.kt`; `T/v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepositoryTest.kt`; `T/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepositoryTest.kt` | 기존 실제 persistence test에서 새 내부 owner ID가 resource ID와 다른 fixture로 올바르게 전달되는지 단언. projection constructor 순서와 current joined ID 전달 검증. |
|
||||||
|
|
||||||
|
위 표 외에도 owner 필드를 추가하는 각 data class의 생성 위치는 GREEN 직전에 전체 `M/`, `T/`에서 다시 `rg`한다. repository의 `Projections.constructor`, JPQL `select new`, Kotlin positional fixture는 공개 JSON 변화 없이 모두 맞춰야 한다. optional owner default `0`으로 컴파일만 통과시키지 않는다.
|
||||||
|
|
||||||
|
## GREEN 구현 범위와 검증 소유
|
||||||
|
|
||||||
|
- 운영 수정 후보는 위 HTTP 표의 controller/facade, owner를 잃는 domain/port/projection adapter, 두 ranking의 현재 이름 batch consumer, 기프트 query result 조립, 채널 오류 분기 query service로 제한한다. response DTO `from`은 기존 공개 필드를 그대로 복사하므로 원칙적으로 수정할 필요 없다.
|
||||||
|
- 읽기 근거로만 등장한 snapshot entity/producer/native aggregation, following inbox producer, admin 원본 편집기는 수정 후보가 아니다. 두 타 소유 영역은 조정 전 수정 금지다. `v2.chat` DM projection은 소유자 인계 항목이다.
|
||||||
|
- 첫 4개 테스트는 대표 RED이지 전체 surface 완료 증거가 아니다. GREEN에서는 HTTP 표의 각 owner class에 JA/KO/EN/미등록/비활성/혼합 USER 및 필요한 owner-ID 회귀를 추가하고 해당 exact class만 우선 실행한다. fixture/생성자 적응은 기존 단위/adapter 회귀로 확인한다.
|
||||||
|
- 검증 실행 예시는 `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.gift.adapter.in.web.GiftControllerTest'`, `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.home.following.adapter.in.web.HomeFollowingEndToEndTest'`다. 다른 행도 `T/` 아래 package와 class를 그대로 FQCN으로 사용한다. 단계 마지막에 부모가 `./gradlew ktlintCheck`, `./gradlew assemble`을 직렬 실행한다.
|
||||||
|
- 이 단계는 부모 RED 증거와 소스·fixture·필드·시그니처의 read/rg 대조만 수행했다. Gradle/서버 실행/DB 연결/commit/push는 하지 않았다. 사용 가능한 도구에 `lsp_diagnostics`가 없어 IDE 진단도 수행하지 못했다. 부모의 실제 GREEN 결과를 기다린다.
|
||||||
|
|
||||||
|
### 추가 확인한 중립 후원 랭킹 캐시
|
||||||
|
|
||||||
|
`M/explorer/profile/CreatorDonationRankingService.kt#getMemberDonationRanking`의 Redis key는 Lang을 포함하지 않고 raw nickname이 담긴 `MemberDonationRankingListResponse`를 캐시한다. 이 파일과 캐시 저장은 수정하지 않았다. `CreatorChannelDonationFacade`는 legacy adapter가 넘긴 donor `userId`를 batch Member 조회해 현재 원본으로 fallback하고 같은 aggregate의 JA map을 우선한다. 목록 donor와 ranking donor의 alias lookup은 합집합 한 번이다. 따라서 중립 donor cache도 역사 면제로 남기지 않았다.
|
||||||
|
|
||||||
|
### 정적 점검 결과와 남은 검증
|
||||||
|
|
||||||
|
- content/live/ranking/following/donor 내부 owner 생성 위치를 `src/main`, `src/test` 전체에서 `rg`해 모든 직접 생성 fixture를 적응했다. JPQL `select new`의 AI/DM trailing ID와 QueryDSL constructor의 donor ID 첫 인자/live owner 마지막 인자를 대조했다.
|
||||||
|
- `findLocalizedNames`는 정상 목록/aggregate별 한 번이며 blocked-access 경로는 오류 분기에서만 한 명을 batch API로 조회한다. snapshot current fallback은 batch Member 조회이고 `0L` 마스킹은 ID 집합에서 제외한다. `LangContext`는 새 이름 기능에서 controller 경계만 읽는다.
|
||||||
|
- public response DTO, snapshot entity/producer, inbox producer, core/legacy, `FcmEvent`는 수정하지 않았다. AI character name/content title/본문 및 배송 실명·주소는 치환하지 않는다.
|
||||||
|
- 부모의 `creatorname-foundation-final-lint.log`를 읽었으며 첫 단계 v2 테스트 4개에 해당하는 lint 지적은 없었다. 다른 소유자 테스트의 lint는 변경하지 않았다. 변경 파일의 길이/공백을 `rg`로 점검했으며 전체 lint 실행을 대신한 것으로 주장하지 않는다.
|
||||||
|
- 길이 검색에서 `DefaultHomeRecommendationQueryRepository.kt`의 기존 raw SQL 두 줄은 130자를 넘는다. 이번 변경은 live owner projection 한 필드이며 그 SQL 본문은 변경하지 않았다. 이 검색 결과를 새 Kotlin lint 실패로 단정하거나 무관한 SQL 포맷 변경으로 확장하지 않는다.
|
||||||
|
- 내부 연결의 알려진 누락은 없다. 남은 불확실성은 실제 compile/test/lint, H2/Hibernate의 typed-null JPQL 실행 및 전체 HTTP 응답 회귀다. 특히 `v2.chat`의 AI/DM 생성자 계약은 부모 통합 GREEN에서 검증해야 한다.
|
||||||
|
|
||||||
|
### 부모 직렬 GREEN 명령
|
||||||
|
|
||||||
|
먼저 위 네 메서드의 동일 명령으로 GREEN을 확인한 뒤 다음 정확한 기존 class 회귀를 실행한다. 명령은 이 세션에서 실행하지 않았다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew compileTestKotlin
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.home.CreatorRankingControllerTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.home.live.adapter.in.web.HomeOnAirLiveEndToEndTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.home.following.adapter.in.web.HomeFollowingEndToEndTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.content.all.adapter.in.web.MainContentAllEndToEndTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.content.overview.adapter.in.web.ContentOverviewEndToEndTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.content.recommendation.adapter.in.web.AudioRecommendationEndToEndTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.content.ranking.adapter.in.web.AudioRankingControllerTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.home.CreatorChannelHomeEndToEndTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.community.adapter.in.web.CreatorChannelCommunityEndToEndTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.fantalk.adapter.in.web.CreatorChannelFanTalkEndToEndTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.donation.adapter.in.web.CreatorChannelDonationEndToEndTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.audio.adapter.in.web.CreatorChannelAudioEndToEndTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.series.adapter.in.web.CreatorChannelSeriesEndToEndTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.live.adapter.in.web.CreatorChannelLiveEndToEndTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.gift.adapter.in.web.GiftControllerTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.gift.application.GiftQueryServiceTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.chat.ChatRoomListServiceTest'
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.chat.ChatRoomListControllerTest'
|
||||||
|
./gradlew ktlintCheck
|
||||||
|
./gradlew assemble
|
||||||
|
```
|
||||||
|
|
||||||
|
### 이번 GREEN 단계 실제 운영 수정 파일
|
||||||
|
|
||||||
|
아래는 `M/` 기준이다. 기존 표의 나머지 producer/DTO/admin 파일은 읽기 근거이지 수정 파일이 아니다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
v2/api/content/all/adapter/in/web/MainContentAllController.kt
|
||||||
|
v2/api/content/all/application/MainContentAllFacade.kt
|
||||||
|
v2/api/content/overview/adapter/in/web/ContentOverviewController.kt
|
||||||
|
v2/api/content/overview/application/ContentOverviewFacade.kt
|
||||||
|
v2/api/content/recommendation/adapter/in/web/AudioRecommendationController.kt
|
||||||
|
v2/api/content/recommendation/application/AudioRecommendationFacade.kt
|
||||||
|
v2/api/content/ranking/adapter/in/web/AudioRankingController.kt
|
||||||
|
v2/api/content/ranking/application/AudioRankingFacade.kt
|
||||||
|
v2/content/all/domain/MainContentAll.kt
|
||||||
|
v2/content/all/adapter/out/persistence/DefaultMainContentAllQueryRepository.kt
|
||||||
|
v2/content/recommendation/domain/AudioRecommendation.kt
|
||||||
|
v2/content/recommendation/adapter/out/persistence/DefaultAudioRecommendationQueryRepository.kt
|
||||||
|
v2/content/ranking/domain/AudioRanking.kt
|
||||||
|
v2/content/ranking/application/AudioRankingQueryService.kt
|
||||||
|
v2/api/home/adapter/in/web/HomeRecommendationController.kt
|
||||||
|
v2/api/home/application/HomeRecommendationFacade.kt
|
||||||
|
v2/api/home/adapter/in/web/CreatorRankingController.kt
|
||||||
|
v2/api/home/application/HomeCreatorRankingFacade.kt
|
||||||
|
v2/api/home/live/adapter/in/web/HomeOnAirLiveController.kt
|
||||||
|
v2/api/home/live/application/HomeOnAirLiveFacade.kt
|
||||||
|
v2/api/home/following/adapter/in/web/HomeFollowingController.kt
|
||||||
|
v2/api/home/following/application/HomeFollowingFacade.kt
|
||||||
|
v2/recommendation/port/out/HomeRecommendationQueryPort.kt
|
||||||
|
v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt
|
||||||
|
v2/home/following/domain/HomeFollowing.kt
|
||||||
|
v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepository.kt
|
||||||
|
v2/api/creator/channel/home/adapter/in/web/CreatorChannelHomeController.kt
|
||||||
|
v2/api/creator/channel/home/application/CreatorChannelHomeFacade.kt
|
||||||
|
v2/creator/channel/home/domain/CreatorChannelHome.kt
|
||||||
|
v2/creator/channel/home/port/out/CreatorChannelHomeQueryPort.kt
|
||||||
|
v2/creator/channel/home/adapter/out/persistence/DefaultCreatorChannelHomeQueryRepository.kt
|
||||||
|
v2/creator/channel/home/application/CreatorChannelHomeQueryService.kt
|
||||||
|
v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityController.kt
|
||||||
|
v2/api/creator/channel/community/application/CreatorChannelCommunityFacade.kt
|
||||||
|
v2/creator/channel/community/application/CreatorChannelCommunityQueryService.kt
|
||||||
|
v2/api/creator/channel/fantalk/adapter/in/web/CreatorChannelFanTalkController.kt
|
||||||
|
v2/api/creator/channel/fantalk/application/CreatorChannelFanTalkFacade.kt
|
||||||
|
v2/creator/channel/fantalk/application/CreatorChannelFanTalkQueryService.kt
|
||||||
|
v2/api/creator/channel/donation/adapter/in/web/CreatorChannelDonationController.kt
|
||||||
|
v2/api/creator/channel/donation/application/CreatorChannelDonationFacade.kt
|
||||||
|
v2/creator/channel/donation/domain/CreatorChannelDonationTab.kt
|
||||||
|
v2/creator/channel/donation/port/out/CreatorChannelDonationQueryPort.kt
|
||||||
|
v2/creator/channel/donation/adapter/out/persistence/DefaultCreatorChannelDonationQueryRepository.kt
|
||||||
|
v2/creator/channel/donation/application/CreatorChannelDonationQueryService.kt
|
||||||
|
v2/api/creator/channel/audio/adapter/in/web/CreatorChannelAudioController.kt
|
||||||
|
v2/api/creator/channel/audio/application/CreatorChannelAudioFacade.kt
|
||||||
|
v2/creator/channel/audio/application/CreatorChannelAudioQueryService.kt
|
||||||
|
v2/api/creator/channel/series/adapter/in/web/CreatorChannelSeriesController.kt
|
||||||
|
v2/api/creator/channel/series/application/CreatorChannelSeriesFacade.kt
|
||||||
|
v2/creator/channel/series/application/CreatorChannelSeriesQueryService.kt
|
||||||
|
v2/api/creator/channel/live/adapter/in/web/CreatorChannelLiveController.kt
|
||||||
|
v2/api/creator/channel/live/application/CreatorChannelLiveFacade.kt
|
||||||
|
v2/creator/channel/live/application/CreatorChannelLiveQueryService.kt
|
||||||
|
v2/api/gift/adapter/in/web/GiftController.kt
|
||||||
|
v2/gift/application/GiftQueryService.kt
|
||||||
|
v2/chat/dto/ChatRoomListDtos.kt
|
||||||
|
v2/chat/service/ChatRoomListService.kt
|
||||||
|
v2/chat/controller/ChatRoomListController.kt
|
||||||
|
chat/room/repository/ChatRoomRepository.kt
|
||||||
|
```
|
||||||
|
|
||||||
|
### 이번 GREEN 단계 실제 테스트 적응 파일
|
||||||
|
|
||||||
|
아래는 `T/` 기준이다. 첫 단계 네 assertion의 기대값은 유지했다. 이 단계에서는 그중 `HomeRecommendationControllerTest`의 기존 직접 facade 생성/호출도 적응했으며 다른 세 RED 파일은 변경할 필요가 없었다.
|
||||||
|
|
||||||
|
```text
|
||||||
|
v2/api/content/all/application/MainContentAllFacadeTest.kt
|
||||||
|
v2/api/content/all/adapter/in/web/MainContentAllControllerTest.kt
|
||||||
|
v2/api/content/all/dto/MainContentAllTabResponseTest.kt
|
||||||
|
v2/content/all/application/MainContentAllQueryServiceTest.kt
|
||||||
|
v2/api/content/overview/application/ContentOverviewFacadeTest.kt
|
||||||
|
v2/api/content/overview/adapter/in/web/ContentOverviewControllerTest.kt
|
||||||
|
v2/api/content/overview/dto/ContentOverviewPageResponseTest.kt
|
||||||
|
v2/api/content/recommendation/application/AudioRecommendationFacadeTest.kt
|
||||||
|
v2/api/content/recommendation/adapter/in/web/AudioRecommendationControllerTest.kt
|
||||||
|
v2/content/recommendation/application/AudioRecommendationQueryServiceTest.kt
|
||||||
|
v2/api/content/ranking/application/AudioRankingFacadeTest.kt
|
||||||
|
v2/api/content/ranking/dto/AudioRankingResponseTest.kt
|
||||||
|
v2/api/home/application/HomeRecommendationFacadeTest.kt
|
||||||
|
v2/api/home/HomeRecommendationControllerTest.kt
|
||||||
|
v2/api/home/live/application/HomeOnAirLiveFacadeTest.kt
|
||||||
|
v2/api/home/live/adapter/in/web/HomeOnAirLiveControllerTest.kt
|
||||||
|
v2/api/home/following/application/HomeFollowingFacadeTest.kt
|
||||||
|
v2/api/home/following/adapter/in/web/HomeFollowingControllerTest.kt
|
||||||
|
v2/api/home/following/dto/HomeFollowingTabResponseTest.kt
|
||||||
|
v2/home/following/application/HomeFollowingQueryServiceTest.kt
|
||||||
|
v2/recommendation/application/HomeRecommendationQueryServiceTest.kt
|
||||||
|
v2/api/creator/channel/home/application/CreatorChannelHomeFacadeTest.kt
|
||||||
|
v2/api/creator/channel/home/adapter/in/web/CreatorChannelHomeControllerTest.kt
|
||||||
|
v2/creator/channel/home/application/CreatorChannelHomeQueryServiceTest.kt
|
||||||
|
v2/api/creator/channel/community/application/CreatorChannelCommunityFacadeTest.kt
|
||||||
|
v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityControllerTest.kt
|
||||||
|
v2/creator/channel/community/application/CreatorChannelCommunityQueryServiceTest.kt
|
||||||
|
v2/api/creator/channel/fantalk/application/CreatorChannelFanTalkFacadeTest.kt
|
||||||
|
v2/api/creator/channel/fantalk/adapter/in/web/CreatorChannelFanTalkControllerTest.kt
|
||||||
|
v2/creator/channel/fantalk/application/CreatorChannelFanTalkQueryServiceTest.kt
|
||||||
|
v2/api/creator/channel/donation/application/CreatorChannelDonationFacadeTest.kt
|
||||||
|
v2/api/creator/channel/donation/adapter/in/web/CreatorChannelDonationControllerTest.kt
|
||||||
|
v2/creator/channel/donation/application/CreatorChannelDonationQueryServiceTest.kt
|
||||||
|
v2/creator/channel/donation/domain/CreatorChannelDonationQueryPolicyTest.kt
|
||||||
|
v2/api/creator/channel/audio/application/CreatorChannelAudioFacadeTest.kt
|
||||||
|
v2/api/creator/channel/audio/adapter/in/web/CreatorChannelAudioControllerTest.kt
|
||||||
|
v2/creator/channel/audio/application/CreatorChannelAudioQueryServiceTest.kt
|
||||||
|
v2/api/creator/channel/series/application/CreatorChannelSeriesFacadeTest.kt
|
||||||
|
v2/api/creator/channel/series/adapter/in/web/CreatorChannelSeriesControllerTest.kt
|
||||||
|
v2/creator/channel/series/application/CreatorChannelSeriesQueryServiceTest.kt
|
||||||
|
v2/api/creator/channel/live/application/CreatorChannelLiveFacadeTest.kt
|
||||||
|
v2/api/creator/channel/live/adapter/in/web/CreatorChannelLiveControllerTest.kt
|
||||||
|
v2/creator/channel/live/application/CreatorChannelLiveQueryServiceTest.kt
|
||||||
|
v2/gift/application/GiftQueryServiceTest.kt
|
||||||
|
v2/chat/ChatRoomListServiceTest.kt
|
||||||
|
v2/chat/ChatRoomListControllerTest.kt
|
||||||
|
```
|
||||||
|
|
||||||
|
## COR013 JOIN 전환 manifest, 2026-10-15
|
||||||
|
|
||||||
|
### 범위와 인계 상태
|
||||||
|
|
||||||
|
- 승인 근거는 `prd.md` 10절과 `plan-task.md` JOIN-T1~T3다. 이 단계는 `v2.creatorname`, `v2.usercreatorchat`, `v2.chat` 및 core/legacy를 수정하지 않았다. `v2.chat` 전체는 delivery 소유자에게 넘어갔으며 아래 팔로잉 recentChats의 기존 명시 Lang 계약만 소비한다.
|
||||||
|
- 부모 RED 로그는 `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/creatorname-join-consumer-red.log`다. `MainContentAllEndToEndTest.shouldSelectLocalizedCreatorNamesWithoutChangingAudioTitles`는 이름 출력 assertion 뒤 standalone alias SELECT 기대 0/실제 1에서 실패했다. 기존 2249 전체 PASS와 JOIN 기반 43 PASS는 변경 전/공통 모듈 기준선이며 이번 v2 결과로 재사용하지 않는다.
|
||||||
|
- 모든 소유 current DB 이름 projection은 기존 owner에 `CreatorNameJoin(owner, lang, uniqueAlias)`의 `name/condition/displayName`을 명시 LEFT JOIN/ON/select한다. 조건은 공통 helper의 owner ID + 요청 Lang + active + CREATOR ON이다. JA-only 읽기 guard를 만들지 않았으며 관리자 /ja 쓰기와 JA 검색은 그대로다.
|
||||||
|
- 아래 DB 읽기에서 facade 별칭 service 의존성, 후속 이름 SELECT와 이름 전용 copy/map을 제거했다. 제목 번역, 페이지 구성, CDN 변환 및 차단 마스킹 등 이름과 다른 기존 변환은 유지한다. DTO는 `val` 기반이며 회원·snapshot·inbox entity를 변이하지 않는다.
|
||||||
|
|
||||||
|
### DB 읽기 전환 지점
|
||||||
|
|
||||||
|
각 행의 경로는 앞서 정의한 `M/` prefix를 적용한다. query adapter 외에 같은 행의 service/port/facade도 함께 수정했다.
|
||||||
|
|
||||||
|
| 경로/심볼 | 현재 구현과 호출 계약 |
|
||||||
|
|---|---|
|
||||||
|
| `v2/content/all/adapter/out/persistence/DefaultMainContentAllQueryRepository.kt#findAudios/findAudioRows/findSeries` | 기존 `locale`을 `Lang.fromCode(locale)`로 재사용한다. audio/series의 실제 owner 이름을 JOIN으로 읽고 인기순 audio GROUP BY에는 raw nickname과 별칭 displayName을 함께 명시했다. `countAudios/countSeries/findSeriesIds`에는 이름 조인을 넣지 않았다. `v2/content/all/application/MainContentAllQueryService.kt#getContents(..., lang: Lang)`와 `v2/api/content/all/application/MainContentAllFacade.kt`가 언어를 전달한다. `v2/content/all/port/out/MainContentAllQueryPort.kt`의 기존 locale 계약은 유지한다. |
|
||||||
|
| `v2/content/recommendation/adapter/out/persistence/DefaultAudioRecommendationQueryRepository.kt#audioRows/toAudioCards` | 최신/무료/포인트/ID 기반 New&Hot·추천 card 모두 같은 JOIN을 사용한다. 기존 locale을 재사용하며 most-commented native query는 이름 출력이 없어 변경하지 않았다. `v2/content/recommendation/application/AudioRecommendationQueryService.kt#getRecommendations(member, lang)/findNewAndHotAudios(member, offset, limit, lang)`와 `v2/api/content/recommendation/application/AudioRecommendationFacade.kt`, `v2/api/content/overview/application/ContentOverviewFacade.kt`에서 전달한다. |
|
||||||
|
| `v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt#findLiveRecommendations/findCheerCreatorRecommendationDetails/findPopularCommunityRecommendationDetails` | live creator, 응원 creator, community owner 각각 고유 alias의 QueryDSL JOIN이다. community 집계 GROUP BY에 별칭 displayName을 추가하고 기존 countDistinct·필터·순서를 유지한다. `v2/recommendation/port/out/HomeRecommendationQueryPort.kt`와 `v2/recommendation/application/HomeRecommendationQueryService.kt`에 필수 Lang을 전달한다. |
|
||||||
|
| `v2/recommendation/adapter/out/persistence/DefaultHomeRecommendationQueryRepository.kt#findRecentlyActiveCreators/findRecentDebutCreators/findFirstAudioContents/findCreatorsByGenre` | native 현재 SELECT 네 곳에 LEFT JOIN + COALESCE를 넣었다. 최근 활동/장르는 후보 ranking/grouping 뒤 outer display owner에 조인하고 debut/first-audio는 현재 member에 조인한다. native `:lang`은 `lang.name` 또는 `Lang.fromCode(locale).name`으로 bind한다. CTE/window/rank/order/limit, 후보 ID/count query와 snapshot 생성 SQL은 보존한다. `findGenreCreatorRecommendations`의 private helper까지 Lang을 전달하며 first-audio port는 기존 locale을 사용한다. |
|
||||||
|
| `v2/api/home/application/HomeRecommendationFacade.kt`; `v2/api/home/live/application/HomeOnAirLiveFacade.kt` | 요청 Lang을 위 query service에 명시 전달하고 JOIN된 record를 그대로 응답 조립에 사용한다. country 기반 `bannerLang`은 배너 전용이며 creator 이름 언어로 쓰지 않는다. |
|
||||||
|
| `v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepository.kt#findFollowingCreators/findOnAirLives/findMonthlySchedules/findRecentNews` | 팔로잉 creator/on-air/live schedule/audio schedule/news의 현재 owner 이름을 JOIN으로 읽는다. `v2/home/following/port/out/HomeFollowingQueryPort.kt`의 네 메서드와 `v2/home/following/application/HomeFollowingQueryService.kt#findHomeFollowing(member, lang)`에 필수 Lang을 추가했다. news는 중립 inbox의 이름이 아니라 이미 조인한 현재 creator 이름을 선택한다. |
|
||||||
|
| `v2/api/home/following/application/HomeFollowingFacade.kt#getFollowingTab` | 위 JOIN 결과를 사용하며 `ChatRoomListService.getRooms(member, lang=lang, followedCreatorsOnly=true, ...)` 계약은 그대로다. recentChats를 다시 이름 치환하지 않는다. |
|
||||||
|
| `v2/creator/channel/home/adapter/out/persistence/DefaultCreatorChannelHomeQueryRepository.kt#findCreator/findChannelDonations/findFanTalkSummary` | creator/donor/latest-fanTalk writer에 각각 JOIN한다. `v2/creator/channel/home/port/out/CreatorChannelHomeQueryPort.kt`의 세 읽기 메서드에 필수 Lang을 추가했고 `v2/creator/channel/home/application/CreatorChannelHomeQueryService.kt`에서 전달한다. notices/communities도 아래 community service에 같은 Lang을 전달한다. `v2/api/creator/channel/home/application/CreatorChannelHomeFacade.kt`의 batch와 copy들을 제거했다. |
|
||||||
|
| `v2/creator/channel/community/adapter/out/persistence/DefaultCreatorChannelCommunityQueryRepository.kt#findCreator/findCommunityPosts/findHomeCommunityPosts/findCommunityPost/findCommunityComments/findLatestRepliesByCommentIds/findCommunityReplies` | post creator와 각 comment/reply writer를 JOIN한다. tuple alias는 `postCreatorDisplayName/commentWriterDisplayName`으로 지정하고 raw 회원 nickname field를 alias로 쓰지 않는다. `v2/creator/channel/community/port/out/CreatorChannelCommunityQueryPort.kt`의 해당 메서드와 `v2/creator/channel/community/application/CreatorChannelCommunityQueryService.kt#findHomeCommunityPosts/getCommunityPostDetail/getCommunityComments/getCommunityReplies`에 필수 Lang을 추가했다. `v2/api/creator/channel/community/application/CreatorChannelCommunityFacade.kt`는 JOIN 결과를 바로 매핑한다. |
|
||||||
|
| `v2/creator/channel/fantalk/adapter/out/persistence/DefaultCreatorChannelFanTalkQueryRepository.kt#findCreator/findFanTalks/findCreatorReplies` | creator와 root/reply writer 이름을 JOIN한다. `v2/creator/channel/fantalk/port/out/CreatorChannelFanTalkQueryPort.kt` 세 메서드에 필수 Lang, `v2/creator/channel/fantalk/application/CreatorChannelFanTalkQueryService.kt`의 private reply helper까지 전달한다. `v2/api/creator/channel/fantalk/application/CreatorChannelFanTalkFacade.kt`의 alias dependency/copy를 제거했다. |
|
||||||
|
| `v2/creator/channel/donation/adapter/out/persistence/DefaultCreatorChannelDonationQueryRepository.kt#findCreator/findChannelDonations` | creator/donor의 직접 DB 읽기를 JOIN으로 바꿨다. `v2/creator/channel/donation/port/out/CreatorChannelDonationQueryPort.kt` 두 메서드에 필수 Lang, `v2/creator/channel/donation/application/CreatorChannelDonationQueryService.kt`가 전달한다. `v2/api/creator/channel/donation/application/CreatorChannelDonationFacade.kt`는 DB donation 목록에 후속 치환하지 않고 중립 cache ranking만 current batch한다. |
|
||||||
|
| `v2/creator/channel/audio/adapter/out/persistence/DefaultCreatorChannelAudioQueryRepository.kt#findCreator`; `v2/creator/channel/series/adapter/out/persistence/DefaultCreatorChannelSeriesQueryRepository.kt#findCreator`; `v2/creator/channel/live/adapter/out/persistence/DefaultCreatorChannelLiveQueryRepository.kt#findCreator` | 각 `v2/creator/channel/{audio,series,live}/port/out/CreatorChannel{Audio,Series,Live}QueryPort.kt#findCreator`는 필수 Lang을 받는다. 각 `v2/creator/channel/{audio,series,live}/application/CreatorChannel{Audio,Series,Live}QueryService.kt`의 blocked-access 이름은 이 record.nickname을 그대로 사용한다. 후속 resolver 주입/오류 분기 alias SELECT를 제거했다. 정상 DTO의 제목/콘텐츠 필드는 그대로다. |
|
||||||
|
| `v2/ranking/adapter/out/persistence/DefaultCreatorRankingSnapshotRepository.kt#findLatestVisibleSnapshots` | DB의 중립 snapshot을 조회하면서 현재 Member/별칭을 LEFT JOIN한다. `COALESCE(alias, current nickname, snapshot nickname)`로 삭제된 owner의 기존 fallback도 유지하며 snapshot entity를 수정하지 않는다. 기존 최신 visible 조건/rankNo 순서를 QueryDSL로 동일하게 적용했다. `v2/ranking/port/out/CreatorRankingSnapshotPort.kt#findLatestVisibleSnapshots(type, nowUtc, lang)`와 `v2/ranking/application/CreatorRankingQueryService.kt#getCreatorRankings(viewerMemberId, lang)`는 필수 Lang이다. `v2/api/home/application/HomeCreatorRankingFacade.kt`의 Member scalar batch와 alias batch/copy를 모두 제거했다. |
|
||||||
|
| `v2/content/ranking/adapter/out/persistence/DefaultAudioRankingSnapshotPersistenceAdapter.kt#findLatestVisibleSnapshots` | 위와 같이 현재 owner/name을 snapshot 읽기에서 JOIN하며 기존 locale을 재사용한다. 제목 번역은 별개로 유지한다. `v2/content/ranking/application/AudioRankingQueryService.kt#getRankings(type, member, lang)` → 기존 snapshot port locale을 전달한다. `v2/api/content/ranking/application/AudioRankingFacade.kt`의 Member scalar batch와 alias batch/copy를 제거했다. |
|
||||||
|
|
||||||
|
`v2/creator/channel/{audio,series,live}` 행의 중괄호는 세 기존 파일의 일대일 대응이며 신규 추정 경로가 아니다. 관리 AI community/fanTalk은 별도 raw repository를 사용함을 확인했으므로 앱 query에 admin용 KO 기본값/null-language fallback을 추가하지 않았다. 원래 언어 중립 snapshot writer가 쓰는 집계기간/raw 조회 계약도 바꾸지 않았다.
|
||||||
|
|
||||||
|
`MainContentAllQueryService`, `AudioRecommendationQueryService`, `AudioRankingQueryService`, `HomeRecommendationQueryService`, `HomeFollowingQueryService`는 요청 Lang을 명시 전달받으며 더 이상 필요 없는 `LangContext` 생성자 의존성을 제거했다. 기존 locale port를 그대로 쓰는 경우 코드 ↔ enum 매핑에는 `Lang.fromCode`만 사용한다. 나머지 채널 서비스의 기존 본문/제목 번역 context는 이번 이름 조회와 무관한 동작으로 유지한다.
|
||||||
|
|
||||||
|
혼합 writer/donor record에는 조회 결과의 `isLocalizedName`을 보존해 원본 `deleted_` prefix 정리만 유지하고 별칭 원문은 정리하지 않는다. 이 flag는 내부 `CreatorChannelCommunityCommentRecord`, `CreatorChannelFanTalkRecord/ReplyRecord`, 두 `CreatorChannelDonationRecord`, 홈 `CreatorChannelFanTalkRecord`에만 있으며 공개 DTO에는 추가하지 않는다. raw fixture의 기본 false와 QueryDSL의 실제 alias-row 존재 projection을 구분한다.
|
||||||
|
|
||||||
|
### 남은 batch와 미전환 읽기의 분류
|
||||||
|
|
||||||
|
| 정확한 경계 | 유지 이유와 현재 처리 |
|
||||||
|
|---|---|
|
||||||
|
| `M/v2/api/creator/channel/donation/application/CreatorChannelDonationFacade.kt#findCurrentNames` | `M/v2/creator/channel/donation/adapter/out/legacy/LegacyCreatorChannelDonationRankingAdapter.kt` → `M/explorer/profile/CreatorDonationRankingService.kt#getMemberDonationRanking`은 Lang 없는 Redis `creator_donation_ranking_v2:...`에 raw nickname을 저장한다. cache hit는 `cachedData.rankings`를 그대로 반환하므로 DB JOIN으로 치환할 수 없는 중립 재사용 경계다. rankings donor ID만 `findCurrentNames` 한 번으로 현재 별칭/원본을 함께 조회한다. 별도 `Member.findNicknamesByIds`는 제거했다. cache entry/producer는 변이하지 않는다. |
|
||||||
|
| `M/v2/gift/application/GiftQueryService.kt#getGifts/getGiftDetail` | 기프트·배송 업무 엔티티와 sender/recipient 회원을 이미 읽어 방향/상태/존재 검증, profile, 주소 공개 범위를 조립하는 기존 경계다. 이름만을 위해 기프트 query/permission DTO를 재설계하지 않았다. 각 use case에서 `findLocalizedNames` 한 번의 sparse batch와 이미 로드한 nickname fallback을 유지하며 추가 Member 이름 query는 없다. 실명/주소/category snapshot/status/size는 치환하지 않는다. |
|
||||||
|
| 두 ranking adapter의 previous/raw 조회와 snapshot refresh/aggregation | 이전 데이터는 rank 변화 계산 또는 중립 writer 입력이며 그 이름을 직접 HTTP 이름으로 노출하지 않는다. latest 표시 read만 현재 이름 JOIN으로 바꿨다. 실제 `v2/ranking`, `v2/content/ranking` 읽기에는 Redis/Cacheable 및 `.getFirst()` 기반 공유 raw ranking cache가 없고 DB snapshot을 직접 소비함을 read/rg로 확인했다. test fake의 ArrayDeque `firstOrNull`은 cache producer가 아니다. |
|
||||||
|
| `M/v2/home/following/adapter/out/persistence/HomeFollowingNewsInbox.kt`와 publish service | 언어 중립 저장값은 그대로다. 이미 현재 creator를 조인하는 HTTP consumer는 위에서 JOIN 전환했으므로 역사 snapshot 면제가 아니다. |
|
||||||
|
| admin/원본 역할 확인, AI 캐릭터 제목, content/series/live title | 운영 식별/원본 편집과 이름 아닌 필드는 기존 raw 정책을 유지한다. 언어별로 렌더되어 저장된 historical 이름으로 면제한 소유 v2 HTTP 필드는 없다. |
|
||||||
|
| `v2.creatorname`, `v2.usercreatorchat`, `v2.chat`, core/legacy/delivery | 다른 소유자 범위다. 공통 helper/batch 계약만 소비했으며 공유 파일을 수정하지 않았다. chat recentChats의 기존 Lang 호출은 보존했다. |
|
||||||
|
|
||||||
|
소유 범위 `findLocalizedNames/findCurrentNames/findNicknamesByIds` 재검색 결과는 위 기프트 두 호출과 donation cache의 current batch 한 호출뿐이다. 단순 current DB projection으로 남긴 alias batch는 없으며 entity/snapshot/inbox 원본도 변경하지 않았다.
|
||||||
|
|
||||||
|
### 테스트/생성자 적응과 정적 증거
|
||||||
|
|
||||||
|
- 기존 facade tests의 resolver mock/MemberRepository 생성자 인자를 제거했고 query service tests의 `LangContext` 인자는 필요한 곳만 제거했다. HomeRecommendation의 모든 직접 facade 생성(장애 로그 tests 포함), 채널 seven fake ports, home/following fake ports, 두 ranking fake ports 및 snapshot adapter 직접 생성자를 현재 계약으로 맞췄다. `DefaultAudioRankingSnapshotPersistenceAdapterTest`는 기존 EntityManager로 `JPAQueryFactory`를 전달한다. nullable dependency로 테스트를 우회하지 않는다.
|
||||||
|
- `T/v2/api/content/all/adapter/in/web/MainContentAllEndToEndTest.kt#shouldSelectLocalizedCreatorNamesWithoutChangingAudioTitles`의 실제 standalone alias SQL counter 기대값 0을 그대로 유지했다. 같은 파일의 `#shouldReadActiveEnglishCreatorNameAndFallbackWhenInactive`는 활성 EN 읽기, 비활성 EN 원본 fallback, POPULAR 집계/page 개수와 제목 불변을 검증하도록 추가했다.
|
||||||
|
- `T/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityEndToEndTest.kt#shouldLocalizeCreatorAuthorsAndLatestRepliesWithoutChangingUserNames`의 JA/USER/본문 assertion을 유지하고 `deleted_`로 시작하는 별칭도 post/root comment/latest reply에서 원문 그대로인지 추가했다. 기존 삭제 원본 이름 정리 tests의 기대값은 완화하지 않았다.
|
||||||
|
- 추가로 적응한 ranking tests는 `T/v2/ranking/application/CreatorRankingQueryServiceTest.kt`, `T/v2/ranking/application/CreatorRankingSnapshotRefreshServiceTest.kt`, `T/v2/ranking/adapter/out/persistence/DefaultCreatorRankingSnapshotRepositoryTest.kt`, `T/v2/content/ranking/application/AudioRankingQueryServiceTest.kt`, `T/v2/content/ranking/adapter/out/persistence/DefaultAudioRankingSnapshotPersistenceAdapterTest.kt`다. reflection 기반 service method 검사에도 Lang type을 추가했다. 일본어/영어 번역 기대 tests는 기존 언어를 명시 인자로 옮겼다.
|
||||||
|
- `read/rg` 및 임시 정적 점검으로 34개 관련 계약의 언어/locale 참조 468곳을 대조했고 알려진 누락은 0이었다. `src/main`, `src/test`의 직접 생성자/override, v2 외부 import/caller, 기존 생성 QMember/QSnapshot field와 native `:lang` 네 곳의 enum NAME binding을 대조했다. 이는 컴파일/실제 SQL 실행을 대신하는 성공 주장이 아니다.
|
||||||
|
- 이 writer는 요청대로 Gradle/서버/DB/agent/commit/push를 실행하지 않았다. `lsp_diagnostics` 도구도 제공되지 않아 미실행이다. 남은 runtime 확인은 H2/MySQL 대응 JOIN/COALESCE와 boolean/enum binding, 전체 consumer 회귀 및 parent GREEN이다.
|
||||||
|
|
||||||
|
### 부모 직렬 GREEN 명령, 이 단계 미실행
|
||||||
|
|
||||||
|
먼저 새 SQL counter RED의 같은 메서드와 EN/grouping 회귀를 실행하고, 아래 module/HTTP consumer 회귀 및 빌드/린트를 한 번에 하나씩 실행한다.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./gradlew compileTestKotlin --offline --console=plain
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.content.all.adapter.in.web.MainContentAllEndToEndTest' --offline --console=plain
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.community.adapter.in.web.CreatorChannelCommunityEndToEndTest' --offline --console=plain
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.content.*' --tests 'kr.co.vividnext.sodalive.v2.api.content.*' --offline --console=plain
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.recommendation.*' --tests 'kr.co.vividnext.sodalive.v2.home.following.*' --tests 'kr.co.vividnext.sodalive.v2.ranking.*' --tests 'kr.co.vividnext.sodalive.v2.api.home.*' --offline --console=plain
|
||||||
|
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creator.channel.*' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.*' --tests 'kr.co.vividnext.sodalive.v2.gift.application.GiftQueryServiceTest' --tests 'kr.co.vividnext.sodalive.v2.api.gift.adapter.in.web.GiftControllerTest' --offline --console=plain
|
||||||
|
./gradlew ktlintCheck assemble --offline --console=plain
|
||||||
|
```
|
||||||
|
|
||||||
|
정적 인계 상태는 **parent GREEN 준비**이며 위 명령의 PASS/실제 HTTP QA 완료를 기록하지 않는다. 공통 및 다른 writer 변경이 합쳐진 전체 회귀와 실제 자원 종료 검증은 부모 JOIN-GATE 소유다.
|
||||||
Reference in New Issue
Block a user