# 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, Lang): Map`이다. 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, 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 ```