Files
sodalive-backend-spring-boot/docs/20261014_크리에이터이름언어별관리/plan-task.md
T

1048 lines
150 KiB
Markdown

# 크리에이터 이름 언어별 관리 구현 계획
> **실행 전제:** 2026-10-14 사용자 문서·DDL 확인 및 구현 승인(COR012). 제품 코드·테스트 구현을 진행한다. 기존 승인 대기 문구는 이 승인 이전 이력이며 운영 DB 적용·배포와 외부 클라이언트 완료는 별도다.
| 문서 항목 | 내용 |
|---|---|
| 상태 | COR022 전체롤백·실제HTTP/이벤트·전체회귀·lint·리뷰 완료 |
| 작성일 | 2026-10-14 |
| 요구사항 기준 | `docs/20261014_크리에이터이름언어별관리/prd.md` |
| API 기준 | COR014 관리자 목록 확장·GET 제거 및 COR015 제외 경계. COR016 전체 회귀에 기존 HTTP 테스트 포함 |
| 현재 Phase | COR022 완료 |
| 현재 활성 Goal | 없음. `MESSAGE-NAME-TRIM-T1` 완료 |
| 다음 Goal | 없음. 후속 변경은 별도 승인 후 진행 |
| 구현 목표 | 승인된 서버의 저장·관리·조회·검색·수신자별 이름 처리와 검증. 외부 클라이언트 미확인은 blocker 유지 |
## 목표
승인 후 운영자는 CREATOR의 일본어 이름을 수동 관리하고, 앱은 모든 크리에이터 표시 경로에서 요청 언어의 이름을 제공한다. 사용자 원본 닉네임과 과거 전달물은 유지한다.
## 현재 상태
| Phase | 상태 | 완료 Task | 활성/다음 Goal | 차단 또는 남은 조건 |
|---:|---|---:|---|---|
| 1. 조사와 기반 | 내부 구현·검증 완료, Gate 통과 | 3/3 | 완료 | 운영 DB 적용/배포는 별도 승인 |
| 2. HTTP와 검색 | 내부 연결·검증 완료, Gate 통과 | 2/2 | 완료 | inventory는 내부 서버 소비자 범위이며 전체 client 소비자는 미확인 |
| 3. 전달과 외부 연동 | 내부 서버 구현·검증 완료, 외부 연동 차단 | 1/2 | `P3-T2` 미완료 | EXT001~003과 provider/운영 DB 용량 증거 없음, `P3-GATE` 미완료 |
| 4. 최종 Gate | 내부 추적·리뷰 보완 완료, 외부 Gate 차단 | 2/2 | `P4-GATE` 외부 대기 | EXT001~003 및 운영 환경 검증만 남음 |
- 계획/DDL 파일 상태를 제품 구현 상태로 오인하지 않는다. 현재 실행 Goal은 상단 상태를 따른다.
- 별도 승인 뒤에도 미완료 Goal 하나만 활성화한다.
- 완료된 내부 단계의 checkbox는 실행 증거에 맞춰 표시한다. 외부 의존성이나 후속 reviewfix가 남은 Task/Gate는 완료 표시하지 않는다.
## 문서·DDL 후속 Goal
**Goal `DOCDDL`:** 기존 수동 언어관리 패턴으로 두 문서의 저장/해제 의미를 맞추고 `schema.sql` 초안을 작성한다.
- **시작 조건:** 사용자가 `creator_localized_name` 테이블명을 확정하고 문서·DDL 파일 작성만 승인했다. 사용자의 물리 DELETE 질문은 삭제 방식 지정이 아니다.
- **완료 증거:** 아래 세 파일의 정합성 및 MySQL DDL 정적 대조. 파일 작성 완료와 DB 미적용을 분리한다.
- **Files:** `docs/20261014_크리에이터이름언어별관리/prd.md`, `docs/20261014_크리에이터이름언어별관리/plan-task.md`, `docs/20261014_크리에이터이름언어별관리/schema.sql`만 변경/생성.
- **범위 밖:** DB 연결/SQL 실행, Kotlin·테스트·설정·빌드 변경, 제품 테스트/빌드, 배포, 다른 DDL, 이력 복원 UI.
- **TDD 예외 사유:** 문서와 미적용 DDL 산출물만 작성하므로 Kotlin 테스트를 만들거나 실행하지 않는다.
- **대체 검증:** 직접 소스와 문서를 읽고 `rg`로 저장 키/활성 조건/승인 경계를 대조한다. 기대 결과는 DB `JA`와 HTTP `ja` 구분, 비활성 제외, 재활성화 동일 행, 제품 checkbox 미완료다.
- [x] 두 문서의 현재 설계와 승인 경계를 보정하고 기존 DEC/COR/Progress를 보존한다.
- [x] `schema.sql` 파일을 작성한다. DB 미적용이며 `P1-T2` 완료가 아니다.
**독립 Gate `DOCDDL-GATE`:** 산출물 작성 후 상위 검토자가 세 파일의 저장 계약과 승인 범위를 판정한다. 제품 실행과 DB 적용은 Gate 범위 밖이다.
- [x] 독립 문서·DDL 정합성 정적 검토 PASS 및 판정 증거 기록.
- [x] `./gradlew tasks --all` 명령 유효성 확인 및 결과 기록. 제품 테스트/빌드는 아님.
## 범위
### 포함
- 수동 JA 별칭 저장/변경/해제. JA 별칭 우선 표시, 원본 fallback, 이름 변경 후 별칭 보존.
- COR015의 ContentMainTab/AudioContentCuration/레거시 커뮤니티·팬톡을 제외한 CREATOR 표시 표면, 레거시와 v2 HTTP, 혼합 작성자 목록, 검색, 푸시, live, WebSocket chat.
- 일본어 별칭과 기존 원본 닉네임을 함께 검색하며 기존 필터와 결과 개수를 보존.
- 언어별 역사 스냅샷 불변. 현재 조회는 요청 언어, 새 수신자별 payload는 수신자/세션 언어.
- 외부 운영 UI, 모바일 앱/client cache, RTC/live locale 연동은 외부 blocker 작업으로 추적하며 서버 완료로 간주하지 않음.
### 제외
- KO/EN 이름 관리, 자동 번역/음역, 일반 USER 이름 변경.
- 원본 `Member.nickname`, 원본 이름 편집 흐름, 인증/식별값 변경.
- 기존 역사 스냅샷 및 발송/전달 완료 payload의 일괄 수정.
- 기존 role-edit PUT 변경, 전역 JSON serialization 변환, request `LangContext`를 비동기 작업에서 사용.
- COR012 승인 범위의 production 코드·테스트는 수행한다. 운영 DB 적용과 배포는 별도 승인 대상이다.
## 기술적 제약과 제안 상태
- 확정 제품 요구는 PRD NAME001~007이다. 외부 작업은 PRD EXT001~003이다.
- 신규 재사용 도메인은 `kr.co.vividnext.sodalive.v2.creatorname` 제안. 구현 시 `application`, `domain`, `port`, `adapter`, `dto` 배치 원칙을 따른다.
- 저장 기술 결정: `(member_id, lang)` 고유(활성/비활성 전체, `is_active` 제외), displayName 중복 허용 및 `VARCHAR(255)`; entity는 `displayName @Column(nullable = false, length = 255)`. 255 Unicode code point 초과는 관리 경계에서 저장 전에 거부하고 자르지 않으며 Unicode 문자 제한은 없다. whitespace-only clear는 기존 승인 대기 제안이며, 그 외 문자는 그대로 저장.
- 테이블명 `creator_localized_name`은 사용자 확정. `lang: Lang`에 `@Enumerated(EnumType.STRING)`, `@Column(nullable = false, length = 10)`을 사용해 DB `lang VARCHAR(10)`에는 enum NAME `KO/EN/JA`를 저장한다. 이번 쓰기는 명시 `Lang.JA`만 허용하며 KO 기본값은 없다. 자유 문자열/새 enum/언어 converter는 만들지 않는다. 미래 언어는 기존 `Lang`과 요청 처리를 별도 확장하며 언어별 컬럼은 추가하지 않는다.
- clear는 JA 행의 `is_active=false`만 설정하고 `display_name`을 보존한다. 미등록 clear는 no-op. 비공백 저장은 비활성까지 member+lang으로 조회해 같은 ID의 값을 갱신하고 활성화한다. 마지막 비활성 값 하나만 유지하며 전체 개정 이력/복원 UI는 아니다. 원본 닉네임·다른 언어 행·과거 전달 snapshot은 불변이다.
- 당시 P1 계약은 JA 활성 CREATOR batch였다. 현재 조회는 모든 기존 `Lang`에서 활성 CREATOR 이름을 적용하고 비활성/미등록은 원본으로 fallback한다. 이는 COR013 JOIN-T1/T2에서 전환됐으며 관리 쓰기는 여전히 JA만이다. 기존 패턴 비교와 저장 계약 근거는 PRD 4절을 따른다.
- DDL 초안 위치는 `docs/20261014_크리에이터이름언어별관리/schema.sql`이며 파일 작성만 승인됐다. MySQL COMMENT/TIMESTAMP 규칙 적용, DB 미적용. signed BIGINT/FK는 기존 member_content_preference DDL 근거이며 실제 `member.id` 타입·engine 및 collation/contains·저장 한계는 운영 반영 전 확인하고 DB 실행은 별도 승인받는다.
- 승인 관리자 API: `GET/PUT /api/v2/admin/creators/{creatorId}/localized-names/ja`. COR012 승인 및 실제 JWT HTTP 테스트 확인. ADMIN 인증, `Member.role == CREATOR`만 대상이며 해당 role의 `Member.memberKind == AI_CHARACTER`도 포함한다. `memberKind == AI_CHARACTER`만으로 `role == USER`를 허용하지 않는다.
- 제안 GET body: `creatorId`, `locale: ja`(`Lang.code`), `originalNickname`, `displayName: nullable`. 미등록/비활성은 모두 null이며 남은 비활성 이름을 노출하지 않는다. 제안 PUT 필수 `displayName: String`, 빈 값 soft clear. HTTP locale은 기존 `Lang.fromCode`/`Lang.fromAcceptLanguage`로 경계에서 매핑하고 내부에는 `Lang`을 전달한다. 기존 `ApiResponse.ok/error`, `SodaException` 패턴 적용.
- 기존 `/admin/member` ADMIN 보호는 권한 패턴 참고일 뿐 신규 creator-admin UI/API 위치 확정이 아니다. `/creator-admin/member`의 CREATOR/AGENT 경로는 변경하지 않는다.
- 기존 batch service는 명시 `Lang`과 sparse CREATOR 별칭 map을 반환한다. COR013에서는 DB current-name 조회에 `CreatorNameJoin`과 단일 JOIN+COALESCE batch를 추가했다. 행별 쿼리, Jackson 전역 변환, principal 수정 금지.
- HTTP 경계에서만 `LangContext`를 읽는다. DTO field 이름/타입은 유지하고 필요한 projection에 내부 memberId/role을 최소 추가한다.
- 검색은 요청 locale과 무관하게 기존 nickname 조건 OR JA alias `EXISTS`다. 별칭 조건은 owner `member_id` 상관 + `lang=Lang.JA`(DB `JA`) + `is_active=true` + owner `Member.role=CREATOR`이며 list/count에 동일 적용한다. Explorer channel, SearchRepository creator branch, MemberQueryRepositoryImpl을 조사하고 기존 title/detail/tag/visibility/block/requester 및 USER 검색을 보존한다.
- 푸시 언어는 기존 `PushTokenInfo.languageCode` 그룹을 사용한다. 발신자 ID와 이름이 들어간 args 위치를 확인하고 그룹마다 args/title/body 및 `senderNicknameSnapshot`을 새로 구성한다.
- WebSocket handshake `Accept-Language`는 세션 attribute로 저장하고 기본 KO. locale 변경 때 reconnect는 클라이언트 계약 제안이다. Redis는 원본 메시지/발신 ID를 전달하고 broker가 세션별 payload를 새로 만든다.
- 같은 회원의 KO/JA 세션은 서로 다른 메시지를 받아야 한다. 공유 DTO mutation, 정규식 치환, 비동기 `LangContext` 사용은 금지한다. SEND_ACK와 HTTP chat response도 별도 locale 검증한다.
- 캐시와 materialization은 producer-write/consumer-read 근거로 분류한다. 기존 `LiveRecommendCacheService`는 3시간 캐시 키에 `Lang` 포함. 이는 다른 캐시/스냅샷의 자동 면제를 뜻하지 않는다.
- `PushNotificationList.senderNicknameSnapshot`의 기존 JPA/DB column mapping과 FCM provider limits는 read-only compatibility preflight로 확인하며, 변경은 이번 범위에 포함하지 않는다. 과거 notification row rewrite와 값 truncate는 금지. 외부 payload 한계는 기존 compatibility preflight에서 담당자와 확인한다.
- 테스트는 JUnit 5. 서비스/컨트롤러 통합 테스트는 `@SpringBootTest`, Redis 필요 시에만 `EmbeddedRedisInitializer` opt-in 한다.
- 외부 경로 미확인 상태에서 경로를 만들지 않는다. 소유자, 시스템/저장소, 계약, 완료 증거를 얻기 전 `[blocked]` 유지.
- `./gradlew tasks --all`은 계획 작성 중 실행해 성공했다. 모든 아래 product test/build/lint는 미래 명령이며 현재 실행하지 않았다.
## 전역 검토 초점
1. 별칭 입력의 원문 보존, 빈/승인된 공백-only clear의 행·값 보존/비활성화, 동일 ID 재활성화 여부. P1-T2 test.
2. 중복 이름이 허용되고 별칭 검색이 count/list에서 중복 행 없이 같은 결과인지. P2-T2 test.
3. 혼합 CREATOR/USER 목록에서 USER 원본 값과 기존 DTO 계약을 보존하는지. P2-T1 test.
4. 원본 닉네임 변경 후 JA 별칭이 유지되고 과거 스냅샷은 재작성되지 않는지. P1-T2 및 P3-T1 test.
5. 한 회원의 동시 KO/JA 세션과 서로 다른 푸시 언어가 각각 독립 payload를 받는지. P3-T1 test, EXT003 연동 검증.
## 파일 책임 지도
모든 경로는 저장소 루트 기준이다. 표 안에서 full path 뒤의 파일명만 적힌 경우 같은 행에서 가장 가까운 full path의 directory를 상속한다. 기존 경로는 현재 존재 여부를 검색/확인했다. `제안 생성`은 승인 후 새 경로이며 현재 존재하지 않는다. 이 seed inventory는 감사 종료 목록이 아니며 preflight에서 surface를 더 찾으면 계획을 먼저 갱신한다.
### 기존 소스, 조사/수정 후보
| 책임 | 확인된 파일 |
|---|---|
| 회원 원본/검색과 프로필 응답 | `src/main/kotlin/kr/co/vividnext/sodalive/member/Member.kt`; `src/main/kotlin/kr/co/vividnext/sodalive/member/MemberService.kt`; `MemberController.kt`; `ProfileResponse.kt`; `GetMemberProfileResponse.kt`; `MemberRepository.kt`의 `MemberQueryRepositoryImpl.findByNicknameAndOtherCondition` |
| 기존 ADMIN 근거 | `src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberController.kt`; `AdminMemberService.kt` |
| 검색 | `src/main/kotlin/kr/co/vividnext/sodalive/search/SearchRepository.kt`; `SearchController.kt`; `SearchService.kt`; `src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt` |
| 레거시 탐색 | `src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepository.kt` (`getSearchChannel`); `ExplorerService.kt` |
| 레거시 홈/랭킹/추천 | `src/main/kotlin/kr/co/vividnext/sodalive/api/home/HomeService.kt`; `src/main/kotlin/kr/co/vividnext/sodalive/rank/RankingService.kt`; `RankingRepository.kt`; `src/main/kotlin/kr/co/vividnext/sodalive/query/recommend/RecommendChannelQueryService.kt`; `RecommendChannelQueryRepository.kt` |
| 콘텐츠/라이브 | `src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt`; `src/main/kotlin/kr/co/vividnext/sodalive/content/series/ContentSeriesRepository.kt`; `src/main/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomService.kt`; `src/main/kotlin/kr/co/vividnext/sodalive/live/recommend/LiveRecommendCacheService.kt` |
| v2 홈/팔로잉 | `src/main/kotlin/kr/co/vividnext/sodalive/v2/api/home/application/HomeRecommendationFacade.kt`; `src/main/kotlin/kr/co/vividnext/sodalive/v2/home/following/adapter/out/persistence/DefaultHomeFollowingQueryRepository.kt`; `HomeFollowingNewsInbox.kt` |
| v2 랭킹/기프트 | `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/application/CreatorRankingQueryService.kt`; `src/main/kotlin/kr/co/vividnext/sodalive/v2/ranking/adapter/out/persistence/CreatorRankingSnapshot.kt`; `src/main/kotlin/kr/co/vividnext/sodalive/v2/gift/application/GiftQueryService.kt` |
| v2 content query adapters | `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/all/adapter/out/persistence/DefaultMainContentAllQueryRepository.kt`; `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/recommendation/adapter/out/persistence/DefaultAudioRecommendationQueryRepository.kt`; `src/main/kotlin/kr/co/vividnext/sodalive/v2/content/ranking/adapter/out/persistence/DefaultAudioRankingSnapshotPersistenceAdapter.kt` |
| v2 creator channel adapters | `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/home/adapter/out/persistence/DefaultCreatorChannelHomeQueryRepository.kt`; `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/community/adapter/out/persistence/DefaultCreatorChannelCommunityQueryRepository.kt`; `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/fantalk/adapter/out/persistence/DefaultCreatorChannelFanTalkQueryRepository.kt`; `src/main/kotlin/kr/co/vividnext/sodalive/v2/creator/channel/donation/adapter/out/persistence/DefaultCreatorChannelDonationQueryRepository.kt` |
| 푸시 | `src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt`; `src/main/kotlin/kr/co/vividnext/sodalive/fcm/notification/PushNotificationService.kt`; `PushNotificationList.kt` |
| v2 user-creator chat | `src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/service/UserCreatorChatService.kt`; `src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/websocket/UserCreatorChatWebSocketAuthInterceptor.kt`; `UserCreatorChatWebSocketHandler.kt`; `UserCreatorChatWebSocketSessionRegistry.kt`; `UserCreatorChatRoomMessageBroker.kt`; `src/main/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/dto/UserCreatorChatDtos.kt` |
### 신규 제안 소스, 승인 후 생성 가능
Root: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creatorname/`.
- Create: `domain/CreatorLocalizedName.kt`, `port/out/CreatorLocalizedNameRepository.kt`, `application/CreatorLocalizedNameService.kt`.
- Create: `adapter/out/persistence/CreatorLocalizedNameEntity.kt`, `adapter/out/persistence/DefaultCreatorLocalizedNameRepository.kt`.
- Create: `adapter/in/web/AdminCreatorLocalizedNameController.kt`, `dto/CreatorLocalizedNameRequest.kt`, `dto/CreatorLocalizedNameResponse.kt`.
- DDL 초안: `docs/20261014_크리에이터이름언어별관리/schema.sql`. 자동 migration이 아니며 `src/main/resources/db/migration` 배치/실행은 승인하지 않았다.
### 기존 테스트 근거와 future test target
| 목적 | 기존 테스트 파일, 존재 확인 |
|---|---|
| 레거시 탐색 이름 조립 | `src/test/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerServiceTest.kt` |
| 검색 서비스 | `src/test/kotlin/kr/co/vividnext/sodalive/search/SearchServiceTest.kt` |
| 푸시 전달 | `src/test/kotlin/kr/co/vividnext/sodalive/fcm/notification/PushNotificationServiceTest.kt`; `FcmSendListener` 전용 test 유무는 preflight에서 재검색 |
| chat service와 websocket | `src/test/kotlin/kr/co/vividnext/sodalive/v2/usercreatorchat/UserCreatorChatServiceTest.kt`; 같은 `websocket/`의 `UserCreatorChatWebSocketHandlerTest.kt`, `UserCreatorChatWebSocketAuthInterceptorTest.kt`, `UserCreatorChatRoomMessageBrokerTest.kt`, `UserCreatorChatRedisIntegrationTest.kt` |
| legacy home | `src/test/kotlin/kr/co/vividnext/sodalive/api/home/HomeServiceTest.kt` |
| v2 home/creator channel/gift | `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/HomeRecommendationControllerTest.kt`; `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/CreatorRankingControllerTest.kt`; `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/CreatorChannelHomeEndToEndTest.kt`; `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/gift/adapter/in/web/GiftControllerTest.kt` |
| v2 content/creator channel end-to-end | `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/adapter/in/web/MainContentAllEndToEndTest.kt`; `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/overview/adapter/in/web/ContentOverviewEndToEndTest.kt`; `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/recommendation/adapter/in/web/AudioRecommendationEndToEndTest.kt`; `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/community/adapter/in/web/CreatorChannelCommunityEndToEndTest.kt`; `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/fantalk/adapter/in/web/CreatorChannelFanTalkEndToEndTest.kt`; `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/donation/adapter/in/web/CreatorChannelDonationEndToEndTest.kt` |
| 제안 도메인/관리 API | 제안 생성: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creatorname/application/CreatorLocalizedNameServiceTest.kt`; 같은 `creatorname/adapter/in/web/AdminCreatorLocalizedNameControllerTest.kt` |
| 검색 repository alias | 기존 repository test 없음. 제안 생성 `src/test/kotlin/kr/co/vividnext/sodalive/search/SearchRepositoryLocalizedNameTest.kt`와 `src/test/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepositoryLocalizedNameTest.kt` |
| 푸시 event listener | 기존 FcmSendListener 전용 test 없음. 제안 생성 `src/test/kotlin/kr/co/vividnext/sodalive/fcm/FcmSendListenerLocalizedNameTest.kt`로 실제 language grouping/render를 검증 |
| push persistence capacity | `src/main/kotlin/kr/co/vividnext/sodalive/fcm/notification/PushNotificationList.kt` 기존 mapping 호환성 확인. 제안 `src/test/kotlin/kr/co/vividnext/sodalive/fcm/notification/PushNotificationListPersistenceTest.kt`로 255 Unicode code point의 저장·조회 및 과거 행 불변 검증 |
| member repository search | 제안 생성 `src/test/kotlin/kr/co/vividnext/sodalive/member/MemberRepositoryLocalizedNameTest.kt`로 기존 requester/role filters와 USER 결과를 검증 |
| 별칭 저장 persistence | 제안 생성 `src/test/kotlin/kr/co/vividnext/sodalive/v2/creatorname/adapter/out/persistence/CreatorLocalizedNamePersistenceTest.kt`로 member+lang unique, soft clear의 행/값 보존, 동일 ID 재활성화, 반복 주기, 중복 표시명/원본 불변을 검증 |
테스트 경로 표는 저장소 루트 기준이며 동일 표 행에서만 명시한 파일명 prefix를 상속한다. 신규 test는 위 제안 경로와 같은 package/class 명을 쓴다. 모든 planned execution command에는 아래처럼 구체 FQCN을 적는다. P1 audit가 추가 surface를 찾으면 RED 시작 전에 정확한 test path와 command를 계획에 추가한다.
### 외부 경로, 아직 특정 불가
| 의존 | 파일/시스템 경로 | 담당 역할 | 차단 해제 증거 |
|---|---|---|---|
| 운영자 UI | `[blocked: owner/repository/path 미확인]` | 운영 도구/관리 UI 소유자 | 저장소/경로, 인증과 배포 흐름, API 소비 test/통합 증거 |
| 모바일 표면과 raw nickname editor | `[blocked: app repository/path 미확인]` | 모바일/클라이언트 소유자 | 모든 표시 surface 및 원본 편집 흐름 목록, 편집기에 원본만 저장하는 test, locale 계약 |
| locale/cache/RTC | `[blocked: client/RTC repository/path 미확인]` | 클라이언트 및 메시징 담당자 | locale propagation, cache invalidation, reconnect 계약, KO/JA 동시 세션 end-to-end 결과 |
## Phase 1: 사전 조사, 저장 기반과 관리 API
**Phase 결과:** 기존 모든 내부 표시 경로·snapshot 유형의 근거표가 만들어지고 승인된 경우에만 별칭 저장/관리 API가 작동한다.
**선행조건:** 사용자 별도 구현 승인. 외부 blocker는 담당자/경로/계약이 확인되기 전 전체 제품 Goal을 완료 처리하지 않는다.
**Phase 완료 조건:** `P1-T1`~`P1-T3`와 `P1-GATE`의 완료 증거, 내부 스토리지/API 검증, 외부 범위 차단 현황 기록.
### Task 1.1 표시 경로 및 데이터 유형 감사
**Goal 실행 `P1-T1`:** 앱 크리에이터 이름의 모든 내부 producer/read path를 조사해 수정 대상과 스냅샷 정책을 근거와 함께 고정한다.
- **시작 조건:** 구현 승인이 기록되고 PRD NAME001~007/EXT001~003 검토 완료.
- **완료 증거:** 발견 표면과 test owner inventory, current lookup/snapshot/neutral cache/materialization 분류, 검색 누락 방지. 아래 파일 지도는 검증된 seed일 뿐 전수 완료 목록이나 감사 종료 조건이 아니다.
- **범위 밖:** 코드/테스트/설정/DDL 수정, 외부 저장소 파일 추정, 미분류 표면을 완료 처리.
- **TDD 예외 사유:** read-only 조사 task라 production 동작 test를 만들지 않는다.
- **대체 검증 방법:** `rg` 결과와 각 호출/producer/consumer 파일을 열어 evidence table 작성, 중복과 미분류를 서로 대조한다.
**Files:** 앞의 source/test seed inventory와 전체 `src/main/kotlin/kr/co/vividnext/sodalive` 추가 검색 결과. 반드시 `api/home/HomeService`, `rank/Ranking*`, `query/recommend/RecommendChannelQuery*`, `member/MemberRepository.findByNicknameAndOtherCondition`, 모든 v2 content/channel query adapter, `PushNotificationList.senderNicknameSnapshot`의 JPA/DB·downstream payload 한계까지 producer/read/write 근거를 확인한다. 외부 경로는 `[blocked]`.
- [x] 모든 내부 nickname/projection surface와 cache/materialization/snapshot을 조사해 세 inventory에 근거를 기록했다. 이는 저장소 내 client 전체 consumer를 확인했다는 뜻은 아니다.
- [x] `MemberQueryRepositoryImpl.findByNicknameAndOtherCondition`의 requester 조건과 USER/Admin/Agent/Bot/self 경계를 기록하고 CREATOR alias만 추가했다.
- [x] 발견한 owner memberId 누락 projection을 보완해 parent/resource ID를 alias key로 사용하지 않도록 연결했다.
- [x] inbox/ranking snapshot 및 중립 cache의 producer/read 소비를 구분하고 역사 데이터를 재작성하지 않았다.
- [x] push snapshot의 기존 mapping과 H2 저장 검증을 확인했다. 실제 MySQL/provider 용량은 별도 미검증으로 남겼다.
- [x] 모바일/운영 UI/RTC 경로는 저장소와 담당자가 미확인임을 기록하고 EXT001~003 `[blocked]`로 유지했다.
- [x] 현재 발견 범위는 관련 task와 blocker에 연결했다. 전 저장소 외부 표면 완전성은 주장하지 않는다.
### Task 1.2 별칭 저장과 이름 해석 service
**Goal 실행 `P1-T2`:** 승인된 저장 계약으로 Lang별 CREATOR 별칭의 소프트 lifecycle을 보존하고 요청 ID/lang에 대한 활성 별칭 map을 batch 조회한다.
- **시작 조건:** `P1-T1` 완료, DB 컬럼/engine/collation 한계와 `schema.sql` 검토, 별도 제품 구현 승인 및 필요한 DB 실행 승인. DDL 파일 작성 승인은 DB 적용 승인이 아니다.
- **완료 증거:** test로 저장·갱신·clear·중복·원본 보존·batch/fallback, `role == CREATOR` 허용 및 그 외 역할 거부를 확인. `memberKind == AI_CHARACTER`만인 `role == USER`는 반드시 거부한다.
- **범위 밖:** 자동 번역, 별칭 고유성, 원본 `Member.nickname` 변경, 행별 조회, 과거 데이터 rewrite.
**Files:**
- Create: 앞의 creatorname 제안 root 하위 `domain/CreatorLocalizedName.kt`, `port/out/CreatorLocalizedNameRepository.kt`, `application/CreatorLocalizedNameService.kt`, `adapter/out/persistence/CreatorLocalizedNameEntity.kt`, `adapter/out/persistence/DefaultCreatorLocalizedNameRepository.kt`.
- Test: 제안 `src/test/kotlin/kr/co/vividnext/sodalive/v2/creatorname/application/CreatorLocalizedNameServiceTest.kt` 및 `src/test/kotlin/kr/co/vividnext/sodalive/v2/creatorname/adapter/out/persistence/CreatorLocalizedNamePersistenceTest.kt`.
- DDL: `docs/20261014_크리에이터이름언어별관리/schema.sql`을 검토용 초안으로 참조한다. 운영 적용 전 실제 `member.id` 타입/engine와 collation 확인 및 별도 DB 실행 승인 필요. Member 원본 entity는 수정하지 않는다.
**Interfaces:**
- Consumes: `Member.id`, `Member.role`, 명시 `Lang`; PRD NAME001/002/005/007. 관리 입력은 Unicode code point 255자로 제한하며 초과를 쓰기 전에 거부한다.
- Produces: 당시 P1 구현은 JA 전용이었으며 nonJA 빈 map을 반환했다. COR013 현재 `findLocalizedNames(memberIds: Collection<Long>, lang: Lang): Map<Long, String>`은 모든 Lang의 활성 CREATOR 별칭만 포함한다. `findCurrentNames(ids, lang)`은 원본/별칭 COALESCE를 한 SQL로 반환한다. 관리 GET은 JA 정책대로 비활성/미등록을 null로 취급한다.
- Repository 제안: `findByMemberIdAndLang(memberId: Long, lang: Lang): CreatorLocalizedName?`는 비활성 포함 쓰기 조회. `findActiveLocalizedNames(memberIds: Collection<Long>, lang: Lang): Map<Long, String>`는 JA/활성/CREATOR 한정 읽기다. Entity는 `displayName @Column(nullable = false, length = 255)`, `lang: Lang` enum-string/length 10 및 `isActive: Boolean`, `(member_id, lang)` unique를 DDL과 일치시킨다.
- [x] **RED:** service/persistence의 저장 한도, Unicode 경계, clear/lifecycle, 중복 허용, role, batch와 불변 조건 테스트를 작성했다.
- [x] **RED 확인:** foundation RED 실행에서 의도된 HTTP 404 및 batch 미구현 assertion을 확인했다.
- [x] **GREEN:** 승인된 저장 계약과 JA 활성 CREATOR batch/fallback 구현을 완료했다.
- [x] **GREEN 확인:** foundation 18 tests 통과, 실패/skip 0. 실제 JWT HTTP GET/PUT 성공 및 미인증 401 확인.
- [x] **REFACTOR:** 최종 lint와 assemble 포함 전체 검증 성공 로그 확인. 실제 DB 적용은 미수행.
### Task 1.3 ADMIN 관리 경계
**Goal 실행 `P1-T3`:** ADMIN만 CREATOR의 일본어 별칭을 조회/저장/해제하고, 원본 이름과 역할 변경은 그대로 둔다.
- **시작 조건:** `P1-T2` 계약 확정, API 제안에 대한 문서/구현 승인, 운영 UI는 별도 blocker로 기록.
- **완료 증거:** GET/PUT 계약, clear 및 ADMIN 권한을 검증하고 `role=CREATOR`의 AI_CHARACTER kind 허용과 `role=USER, memberKind=AI_CHARACTER` 거부를 별도 단언한다.
- **범위 밖:** 관리자 UI 구축, 기존 role-edit endpoint 수정, endpoint를 승인된 공개 계약으로 간주.
**Files:** Create `src/main/kotlin/kr/co/vividnext/sodalive/v2/creatorname/adapter/in/web/AdminCreatorLocalizedNameController.kt`, `src/main/kotlin/kr/co/vividnext/sodalive/v2/creatorname/dto/CreatorLocalizedNameRequest.kt`, `src/main/kotlin/kr/co/vividnext/sodalive/v2/creatorname/dto/CreatorLocalizedNameResponse.kt`; proposed test `src/test/kotlin/kr/co/vividnext/sodalive/v2/creatorname/adapter/in/web/AdminCreatorLocalizedNameControllerTest.kt`. Reference only: existing `src/main/kotlin/kr/co/vividnext/sodalive/admin/member/AdminMemberController.kt`; do not change role-edit PUT or `src/main/kotlin/kr/co/vividnext/sodalive/creator/admin/member/CreatorAdminMemberController.kt`.
**Interfaces:**
- Consumes: `P1-T2` service/repository contract. Controller는 `/ja`를 기존 `Lang.fromCode`로 `Lang.JA`에 매핑해 명시 전달하며 DB 값은 `JA`다(쓰기 KO 기본값 없음).
- Produces: 제안 `GET/PUT /api/v2/admin/creators/{creatorId}/localized-names/ja`; GET `creatorId`, `locale=Lang.JA.code`(`ja`), `originalNickname`, nullable `displayName`(비활성/미등록은 null); PUT required `displayName: String`.
- [x] **RED:** 실제 API 응답 및 역할/권한 assertion을 구성했다.
- [x] **RED 확인:** foundation RED 기록에서 실제 endpoint 미구현 404를 확인했다.
- [x] **GREEN:** COR012 승인 API 및 기존 web/error/security 패턴을 구현했다.
- [x] **GREEN 확인:** 실제 JWT HTTP GET/PUT, 성공/오류 envelope, 미인증 401을 테스트했다.
- [x] **REFACTOR:** 전체 ktlint 및 assemble 포함 회귀 성공.
### Phase 1 완료 조건 및 Gate
- [x] `P1-T1`~`P1-T3` 구현/테스트 증거가 기록됐다.
- [x] 발견된 내부 경로와 확인된 외부 blocker를 연결하고 역사 snapshot 무변경 회귀를 통과했다.
- [x] DDL 한계와 COR012 API 승인 및 실제 DB 미검증 상태가 기록됐다.
- [x] `P1-GATE` 내부 검증 통과. 전체 회귀/ktlint/assemble은 통합 로그에서 성공.
**Phase 1 Gate `P1-GATE`:** 저장·관리 계약, 요구 추적, 직접 영향 회귀를 판정한다.
- **시작 조건:** Phase 1 Task 전체 완료, 요구사항/제안 API 승인 근거 존재.
- **완료 증거:** 아래 focused tests, lint, compile/package 결과 및 snapshot/원본 불변 확인이 기록됨.
- **범위 밖:** 외부 UI blocker를 내부 서버 성공으로 표시, test 삭제/완화.
```bash
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameServiceTest'
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.CreatorLocalizedNamePersistenceTest'
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creatorname.adapter.in.web.AdminCreatorLocalizedNameControllerTest'
./gradlew ktlintCheck
./gradlew assemble
```
**Expected:** 명령 exit code 0, JUnit 결과 성공, API field/role/clear 및 원본 불변 확인. Phase Gate는 전체 test suite를 실행하지 않고 `assemble`로 compile/package를 확인한다.
## Phase 2: 모든 HTTP 표시 경로와 검색
**Phase 결과:** 현재 이름을 조회하는 HTTP 요청은 명시 언어의 CREATOR 이름을 받고, 검색은 원본+JA 별칭을 유지하면서 중복을 만들지 않는다.
**선행조건:** `P1-GATE` 완료 및 `P1-T1` inventory에서 빠진 경로 없음.
**Phase 완료 조건:** `P2-T1`, `P2-T2`, `P2-GATE` 완료. 레거시/v2 응답 DTO 계약이 유지되고 locale boundary가 test됨.
### Task 2.1 HTTP read surface와 혼합 목록 적용
**과거 Goal 실행 `P2-T1`:** 당시 batch 기반 구현은 inventory의 HTTP current-name 응답에 alias override를 적용했다. COR013에서 직접 DB projection 경로는 JOIN으로 전환했으며, 과거 checkbox와 Progress는 그 시점의 이력이다.
- **시작 조건:** `P1-GATE` 완료, 각 surface의 projection과 명시 `Lang` entrypoint를 `P1-T1` evidence에서 확인.
- **당시 완료 증거:** 각 affected feature test에서 JA override, fallback, 혼합 USER 보존, field type/name 불변을 확인했다. COR013은 요청 Lang에 해당하는 활성 별칭도 조회한다.
- **범위 밖:** snapshot rewrite, 전역 serializer, path inventory 없이 wildcard 수정.
**Files:** seed 중 Member profile, Explorer, legacy `api/home`, `rank`, `query/recommend`, content/series, live, v2 home/following, gift, ranking, content/channel adapters. P1 inventory가 추가한 모든 HTTP current-name consumer만 수정한다.
**Tests:** 영향 surface마다 file map의 exact existing class 또는 P1 audit가 implementation 전 추가한 exact test path. Seed E2E targets: `src/test/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerServiceTest.kt`; `src/test/kotlin/kr/co/vividnext/sodalive/api/home/HomeServiceTest.kt`; `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/HomeRecommendationControllerTest.kt`; `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/home/CreatorRankingControllerTest.kt`; `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/all/adapter/in/web/MainContentAllEndToEndTest.kt`; `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/overview/adapter/in/web/ContentOverviewEndToEndTest.kt`; `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/content/recommendation/adapter/in/web/AudioRecommendationEndToEndTest.kt`; `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/creator/channel/home/CreatorChannelHomeEndToEndTest.kt`; community, fantalk, donation EndToEndTest paths in the test map; `src/test/kotlin/kr/co/vividnext/sodalive/v2/api/gift/adapter/in/web/GiftControllerTest.kt`.
**Interfaces:**
- 당시 P2 계약: `P1-T2` batch `Map<Long, String>`을 HTTP 경계에서 적용했다. COR013 이후 직접 DB projection은 요청 Lang의 JOIN+COALESCE 결과를 사용하며 기존 DTO contract를 유지한다. cache/recipient 및 그 밖의 명시된 경계만 batch를 유지한다.
- [x] **RED:** legacy/v2 consumer 테스트에서 JA 표시, KO/EN/fallback, 혼합 USER 및 공개 응답 불변 assertion을 작성했다.
- [x] **RED 확인:** 통합 RED에서 104개 중 35개 테스트가 이름 표시/handshake 등 미구현 assertion으로 실패했다. fixture/context 실패는 이후 보정하고 기대값은 완화하지 않았다.
- [x] **GREEN:** inventory에 연결한 legacy/v2 내부 HTTP surface에 owner ID batch render를 적용하고 기존 DTO field 계약을 유지했다.
- [x] **GREEN 확인:** 최신 전체 실행에서 2245개 테스트 통과. 개별 surface 통과 여부보다 전체 회귀 실행 결과를 기준으로 기록.
- [x] **REFACTOR:** 전체 ktlint 통과. Oracle이 별도로 제기한 쿼리 비용 및 helper 위치 보완은 P4-R1로 분리했고 후속 검증·리뷰 승인까지 완료했다.
### Task 2.2 검색 원본+JA 별칭
**Goal 실행 `P2-T2`:** 공개 creator/channel 검색과 기존 member nickname 검색에서 원본 및 JA 별칭을 함께 찾고 기존 결과/필터/count 의미를 보존한다.
- **시작 조건:** `P1-GATE` 완료, alias search가 request locale과 무관하게 JA임을 확인하고 DB contains/collation 의미를 보존한다.
- **완료 증거:** request가 KO여도 JA alias hit, alias clear 이후 alias-only 검색 miss, original match 지속, 중복 방지, 기존 USER/requester filters를 test로 검증.
- **범위 밖:** 콘텐츠/시리즈 title, character title, detail/tag를 creator alias 조건으로 오염시키기. 기존 콘텐츠 검색의 제목 등 조건과 필터는 그대로 둔다.
**Files:** Modify `src/main/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepository.kt` `getSearchChannel`; `src/main/kotlin/kr/co/vividnext/sodalive/search/SearchRepository.kt` creator-name branches in searchCreator/searchContent/searchSeries (not title/detail/tag predicates); and `src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt` `MemberQueryRepositoryImpl.findByNicknameAndOtherCondition`. Keep requester filters and existing USER matches. In searchCreator both `SearchResponseItem.title` and `.nickname` are creator-name fields and use the owner `memberId`; in content/series results title remains content/series title, while creator nickname remains separately searchable. Never key aliases by parent resource ID; add internal owner memberId projection where missing and keep public DTO unchanged.
**Tests:** existing `src/test/kotlin/kr/co/vividnext/sodalive/search/SearchServiceTest.kt`; proposed `src/test/kotlin/kr/co/vividnext/sodalive/search/SearchRepositoryLocalizedNameTest.kt`, `src/test/kotlin/kr/co/vividnext/sodalive/explorer/ExplorerQueryRepositoryLocalizedNameTest.kt`, `src/test/kotlin/kr/co/vividnext/sodalive/member/MemberRepositoryLocalizedNameTest.kt`.
**Interfaces:**
- Consumes: owner memberId 상관 JA alias `EXISTS` predicate(`lang=Lang.JA`/DB `JA`, `is_active=true`, owner `Member.role=CREATOR`), 기존 keyword와 표시 요청 `Lang`. HTTP `locale=ja`를 DB 문자열 비교값으로 쓰지 않는다.
- Produces: 기존 `searchCreatorTotalCount`/`searchCreatorList` 반환 타입, 기존 필터와 pagination.
- [x] **RED:** creator/channel/member 검색에서 원본 OR JA alias, 중복/clear, USER·기존 필터와 content title 불변 회귀를 작성했다.
- [x] **RED 확인:** 통합 RED run의 consumer 실패에 검색 별칭 누락 assertion이 포함됐다.
- [x] **GREEN:** Explorer, SearchRepository list/count 및 MemberRepository의 alias EXISTS를 owner/JA/active/CREATOR로 상관시켰다.
- [x] **GREEN 확인:** 최신 전체 회귀 통과. 검색 테스트는 이번 run에서 전체 suite로 검증됐다.
- [x] **REFACTOR:** lint 및 전체 회귀 통과. P4-R1에서 predicate helper 배치와 SQL 조회 비용을 보완한다.
### Phase 2 완료 조건 및 Gate
- [x] 저장소 inventory의 legacy/v2 서버 HTTP surface 연결 및 snapshot 불변 회귀를 검증했다. 이 확인은 외부 client 전체 surface 검증이 아니다.
- [x] 서버 공개 DTO schema와 raw nickname editor 경계를 변경하지 않았다. client가 raw endpoint를 어디서 쓰는지는 미확인이다.
- [x] 검색 list/count/page 중복 방지 assertion과 전체 회귀를 통과했다.
- [x] `P2-GATE` 내부 검증 통과.
**Phase 2 Gate `P2-GATE`:** 검색과 HTTP 표시 회귀를 확인한다.
- **시작 조건:** `P2-T1`, `P2-T2` 완료.
- **완료 증거:** 각 수정 surface의 test class 목록/실행 결과, API schema 비교, lint 결과.
- **범위 밖:** 범위 inventory 외 리팩터링.
```bash
./gradlew test --tests 'kr.co.vividnext.sodalive.explorer.ExplorerServiceTest'
./gradlew test --tests 'kr.co.vividnext.sodalive.explorer.ExplorerQueryRepositoryLocalizedNameTest'
./gradlew test --tests 'kr.co.vividnext.sodalive.api.home.HomeServiceTest'
./gradlew test --tests 'kr.co.vividnext.sodalive.search.SearchServiceTest'
./gradlew test --tests 'kr.co.vividnext.sodalive.search.SearchRepositoryLocalizedNameTest'
./gradlew test --tests 'kr.co.vividnext.sodalive.member.MemberRepositoryLocalizedNameTest'
./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.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.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.gift.adapter.in.web.GiftControllerTest'
./gradlew ktlintCheck
```
**Expected:** 위 seed focused tests와 실제 수정 surface test가 성공한다. P1 audit가 추가 발견한 surface는 실행 전에 exact test path/FQCN을 이 Task에 추가한다. 명령은 direct HTTP/read-surface 회귀 범위다.
## Phase 3: 수신자별 푸시, WebSocket/live와 외부 통합
**Phase 결과:** 신규 비동기 전달마다 각 수신자/세션 locale로 새 이름 payload가 만들어지고 외부 앱 계약까지 검증되거나 blocker로 명확히 남는다.
**선행조건:** Phase 2 Gate 완료, EXT001~003 담당 역할과 연동 계약 확인. 외부 계약이 미확인이면 영향 경로 task는 `[blocked]`, 완료 금지.
**Phase 완료 조건:** 내부 `P3-T1`, `P3-T2` 완료, 외부 blocker에 path/owner/재개 증거가 첨부되고 실제 통합 gate가 판정됨.
### Task 3.1 푸시와 WebSocket per-recipient rendering
**Goal 실행 `P3-T1`:** 푸시와 socket 채팅의 신규 payload를 수신자/세션 언어에 맞춰 독립 생성하고 기존 전달물은 보존한다.
- **시작 조건:** EXT003 locale 계약을 확인하거나 client session propagation 부분을 blocker로 분리한 승인 결정, locale source와 sender-name interpolation 위치 확인.
- **완료 증거:** 푸시 lang group별 argument/title/body/sender snapshot, 같은 user KO/JA sessions의 서로 다른 output, SEND_ACK 및 HTTP response locale, Redis payload neutrality 확인.
- **범위 밖:** PushNotificationList 과거 행 재작성 및 entity/DDL 용량 확장, regex substitution, shared DTO 변경, async request `LangContext` 사용.
**Files:** modify `src/main/kotlin/kr/co/vividnext/sodalive/fcm/FcmEvent.kt`; `fcm/notification/PushNotificationService.kt` if needed. Reference only: `fcm/notification/PushNotificationList.kt` mapping compatibility; no column changes or historical row rewrite. Modify existing chat paths in the source map; DTO only for proven contract need. Tests: proposed `src/test/kotlin/kr/co/vividnext/sodalive/fcm/FcmSendListenerLocalizedNameTest.kt` exercises actual recipient language groups and rendered notification arguments; existing PushNotificationService and chat/websocket tests. External client/RTC path `[blocked]`.
**Interfaces:**
- Consumes: `P1-T2` explicit Lang batch resolver. 푸시는 recipient `languageCode`; socket은 handshake에서 session attribute로 기록된 locale.
- Produces: push group마다 새 args/title/body/`senderNicknameSnapshot`; neutral Redis event(원본 message와 sender ID); broker는 각 session locale로 독립 payload 생성.
- [x] **RED:** FCM token locale 그룹, WS handshake/session, ACK/HTTP, 원본 event/DTO 불변 회귀를 작성했다.
- [x] **RED 확인:** consumer RED에서 FCM/WS locale assertion 실패를 확인하고 fixture/context 원인을 별도로 보정했다.
- [x] **GREEN:** 서버 FCM 그룹 render와 session locale별 WS payload, HTTP/message/live current-name 경로를 구현했다. truncate 및 과거 payload 수정은 없다.
- [x] **GREEN 확인:** 최종 전체 회귀 통과, WS localhost/JWT 통합 테스트 포함. 외부 앱 reconnect/provider 수신은 증명하지 않는다.
- [x] **REFACTOR:** 전체 ktlint/assemble 및 test run 성공.
### Task 3.2 live, cache/materialization 및 외부 소비자 통합
**Goal 실행 `P3-T2`:** live 표시, 비언어 캐시 및 수신자 client의 이름 수명을 검증해 current name과 불변 snapshot을 각각 올바르게 제공한다.
- **시작 조건:** `P1-T1` source evidence와 EXT001~003 owner/repository/path/contract 확인. 미확인 external work는 실행하지 않고 blocked 상태 유지.
- **완료 증거:** live/new response와 캐시 classification test, producer/consumer별 decision table, 모바일/관리 UI/RTC 통합 test owner가 제출한 증거.
- **범위 밖:** historical snapshot 재작성/자동 삭제, 모든 cache 일괄 invalidation, 외부 경로 추측.
**Files:** internal candidates in `src/main/kotlin/kr/co/vividnext/sodalive/`: `live/room/LiveRoomService.kt`, `live/recommend/LiveRecommendCacheService.kt` (repository/response exact path recheck), v2 ranking query/snapshot and following query/inbox paths in file map. Tests: existing `src/test/kotlin/kr/co/vividnext/sodalive/live/room/LiveRoomServiceTest.kt`; exact recommend/snapshot/following test paths from P1 inventory. EXT001 mobile/cache/raw editor, EXT002 admin UI, EXT003 RTC/client reconnect paths remain `[blocked]`.
**Interfaces:**
- Consumes: HTTP/service locale resolver; per-session lang contract from `P3-T1` where live/chat share session.
- Produces: current-name reads localized at request time; historical name snapshots retained; client/UI may complete only with external owner verification.
- [x] **RED:** live/current/cache 및 historical 불변 동작 테스트를 작성하고 실제 구현 전 실패 증거를 남겼다.
- [x] **RED 확인:** 소비자 RED 및 별도 fixture 보정으로 이름 assertion과 test infrastructure 문제를 구분했다.
- [x] **GREEN:** 내부 server current-name/cache consumer를 locale batch 경로로 연결하고 historical snapshot을 유지했다.
- [x] **GREEN 확인:** 전체 회귀 통과. 내부 live/cache 구현은 검증됐으나 client cache/raw editor/RTC 외부 증거 미제공.
- [x] **REFACTOR:** 내부 변경 범위 lint/assemble/회귀 통과. 외부 의존성 때문에 Task 자체와 P3 Gate는 미완료다.
### Phase 3 완료 조건 및 Gate
- [x] 서버 push, websocket session, live 수신자별 이름 회귀가 통과했고 historical payload 수정은 없다.
- [x] reconnect/cache lifetime external evidence가 없어 blocker로 남기고 제품 완료를 보류했다.
- [ ] 모든 CREATOR display surfaces와 raw original editor가 내부/외부 소유자에게 연결되고 client owner evidence가 제출됐다.
- [ ] `P3-GATE` 외부 연동 증거 전까지 미완료로 유지한다.
**Phase 3 Gate `P3-GATE`:** 다중 domain의 수신자별 전달 및 외부 연동 증거를 판정한다.
- **시작 조건:** `P3-T1`, `P3-T2` 완료. EXT001~003의 차단 해제 조건 충족.
- **완료 증거:** KO/JA 동시 세션 결과, language grouped push 결과, HTTP/ACK 확인, 모바일 표시/cache/raw editor test, RTC reconnect test.
- **범위 밖:** 외부 증거 없이 서버 코드만으로 앱 전체 완료 판정.
```bash
./gradlew test --tests 'kr.co.vividnext.sodalive.fcm.FcmSendListenerLocalizedNameTest'
./gradlew test --tests 'kr.co.vividnext.sodalive.fcm.notification.PushNotificationListPersistenceTest'
./gradlew test --tests 'kr.co.vividnext.sodalive.fcm.notification.PushNotificationServiceTest'
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest'
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatWebSocketAuthInterceptorTest'
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatWebSocketHandlerTest'
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatRoomMessageBrokerTest'
./gradlew test --tests 'kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatRedisIntegrationTest'
./gradlew test --tests 'kr.co.vividnext.sodalive.live.room.LiveRoomServiceTest'
./gradlew ktlintCheck
```
**Expected:** 실제 변경된 test class와 Redis initializer opt-in이 확인되고 모두 성공. 외부 test는 소유자가 제공한 실제 command/result를 따로 기록하며 서버 test 성공과 합쳐 주장하지 않는다.
## Phase 4: 최종 호환성 및 회귀 Gate
**Phase 결과:** 요구사항 전체의 추적, 내부/외부 범위, API 호환성, 저장/이름/전달 안전성이 검증된다.
**선행조건:** `P1-GATE`, `P2-GATE`, `P3-GATE` 완료 및 제품 변경 전체 승인 범위 기록.
**Phase 완료 조건:** `P4-GATE`와 요구사항 추적표, 전체 회귀 결과 또는 조건부 생략 근거, blocker 0건.
### Task 4.1 최종 요구 추적표
**Goal 실행 `P4-T1`:** 각 NAME/EXT 요구와 구현/검증 증거 사이에 빈 연결이 없는 최종 traceability table을 작성한다.
**Files:** `docs/20261014_크리에이터이름언어별관리/prd.md`, `docs/20261014_크리에이터이름언어별관리/plan-task.md` 및 선행 Task에 누적한 검증 증거.
- **시작 조건:** 선행 Phase Gate가 모두 통과했거나 사용자 승인으로 미포함 범위의 결정 문서화.
- **완료 증거:** NAME001~007/EXT001~003 각각 source owner, test/evidence, status, 미완료 blocker를 가진 traceability table.
- **범위 밖:** 제품 코드 수정, blocker를 `complete`로 대체.
- **TDD 예외 사유:** 문서만 수정하는 read-only traceability 검토 task.
- **대체 검증 방법:** 요구사항 표, 계획 Task, 실제 test output/외부 증거를 행별 대조한다. 빈 증거는 실패로 처리한다.
- [x] NAME/EXT 요구 ID를 내부 test/evidence 또는 명시 external blocker에 연결했다.
- [x] 세 inventory의 확인 항목을 계획과 대조했다. 전역 client surface 전수 검증이라고 주장하지 않는다.
- [x] historical snapshot과 neutral cache 분류 근거 경로를 inventory에 연결했다.
- [x] 현재 API schema와 서버 raw nickname editor 경계를 대조했다. 외부 client 실제 사용은 미확인이다.
### Task 4.2 리뷰 보완 P4-R1
**Goal 실행 `P4-R1`:** 독립 Oracle 리뷰에서 확인한 현재 이름 fallback 쿼리 비용과 helper 패키지 배치 문제를 보완하고 cache hit 회귀를 실제로 입증한다.
- **상태:** 수정·대상/전체 테스트·독립 리뷰 승인 완료. 운영 DB의 실측은 수행하지 않았다.
- **선행 조건:** 최신 source와 리뷰 finding을 다시 확인한다. 제안 경로는 수정 직전 실제 파일에서 검증한다.
- **Files:** `src/main/kotlin/kr/co/vividnext/sodalive/v2/creatorname/application/LegacyCreatorNameRenderer.kt`; `src/main/kotlin/kr/co/vividnext/sodalive/v2/creatorname/adapter/out/persistence/CreatorNamePredicates.kt`; `src/main/kotlin/kr/co/vividnext/sodalive/member/MemberRepository.kt`와 여섯 consumer 및 관련 cache test. 기존 root creatorname helper는 v2로 이동했다.
- **범위 밖:** 외부 앱/RTC/UI blocker, 운영 DB 적용, 검색 의미/공개 DTO 변경, SQL 실측 없는 성능 수치 주장.
- [x] **RED:** 현재 이름 조회의 SQL 수/엔티티 로드 및 실제 cache hit/raw entry 보존 단언을 추가했다.
- [x] **RED 확인:** 이름 조회 성능 단언 3건 실패 확인(JA 4/10회, KO·EN 3/9회, 엔티티 1/4). 캐시 증거 단언은 기존 구현으로 통과해 캐시 production 코드는 추가 수정하지 않았다.
- [x] **GREEN:** 여섯 이름 전용 fallback을 scalar ID/nickname 조회로 교체하고 공통 helper 두 개와 관련 테스트를 v2로 이동했다.
- [x] **GREEN 확인:** 타깃 71개 테스트 통과. JA SQL 2/2회, KO·EN 1/1회, 엔티티 로드 0/0, 빈 입력 SQL 0회. 실제 Redis raw entry 불변 및 방 목록 SQL 1회 재사용 확인.
- [x] **REFACTOR:** 전체 2249개 테스트, assemble·ktlint 성공 및 최종 Oracle APPROVE. 상위 build/실제 HTTP·WS 재검증도 통과했다.
**완료 판정:** `P4-R1` 완료. 외부 증거가 필요한 `P4-GATE`는 별도 차단 상태로 유지한다. 운영 DB에서 SQL 성능 실측은 하지 않았다.
### Phase 4 Gate
**Goal 실행 `P4-GATE`:** 여러 domain에 걸친 최종 회귀와 문서/외부 증거를 하나의 승인 가능한 판정으로 남긴다.
- **시작 조건:** `P4-T1`, 이전 Gate 전체 완료.
- **완료 증거:** 최종 회귀, lint/build, API contract 비교, external owner 증거 및 traceability 모두 통과.
- **범위 밖:** 실패 test 삭제/skip, external path의 추정, 승인되지 않은 후속 구현.
조건부 전체 검증 명령:
```bash
./gradlew test
./gradlew ktlintCheck
./gradlew build
```
`test`/`build`는 여러 domain과 공통 serialization/권한 경계를 건드린 경우 실행한다. 제품 승인이 전체 회귀를 요구하거나 targeted test만으로 영향을 판단할 수 없는 실패가 생겨도 실행한다. 해당 조건이 모두 없고 안전성 판단에 충분한 focused/feature 회귀가 있을 때만 전체 회귀를 생략하며, 그 사실·근거·대체 명령을 Progress에 남긴다. 이 계획 문서 작성 시 product test/build/lint는 실행하지 않았다.
## 실행 순서와 의존성
| 순서 | Goal | 선행조건 | 상태/병행 | 차단 시 다음 행동 |
|---:|---|---|---|---|
| 0 | 구현 승인 | 사용자 명시 승인 | 완료: COR012 | 운영 DB 적용·배포 별도 승인 |
| 1 | `P1-T1` | 승인 | 완료 | 외부 inventory 미확인은 blocker로 유지 |
| 2 | `P1-T2` | `P1-T1` 및 storage decision | 완료 | 운영 DB는 미적용 |
| 3 | `P1-T3` | `P1-T2` 및 COR012 API 승인 | 완료 | 운영 UI는 EXT002 blocker |
| 4 | `P1-GATE` | Phase 1 Task 완료 | 완료 | 내부 검증 통과 |
| 5 | `P2-T1` / `P2-T2` | `P1-GATE` | 완료 | 전체 회귀 통과, 외부 표면 미확인 |
| 6 | `P2-GATE` | Phase 2 Task 완료 | 완료 | 내부 검증 통과 |
| 7 | `P3-T1` | `P2-GATE` 및 내부 locale 경계 | 내부 완료 | 외부 client/provider 증거는 blocker |
| 8 | `P3-T2` / `P3-GATE` | P3 내부 구현 및 external evidence | 미완료 | EXT001~003 및 provider/DB blocker |
| 9 | `P4-T1` | 확인된 내부 증거 및 blocker 목록 | 기록 완료 | 외부 전체 범위 완료 아님 |
| 10 | `P4-R1` / `P4-GATE` | 리뷰 finding 수정/재검증 및 Gate 조건 | 서버 보완 완료 / 외부 차단 | 외부 evidence 수령 후 제품 Gate 재개 |
```text
COR012 → P1 내부 Gate → P2 내부 Gate → P3-T1 내부 검증
→ P3-T2/P3-GATE [EXT001~003 blocked]
→ P4-T1 evidence → P4-R1 reviewfix → P4-GATE [미완료]
```
## 요구사항 추적표
| ID | 계획 위치 | 핵심 검증 | 현재 상태 |
|---|---|---|---|
| NAME001 | P1-T2, P1-T3 | member ID로 저장/조회/clear, ADMIN 권한, CREATOR 대상 | 서버 구현 및 테스트 통과 |
| NAME002 | P1-T2, P2-T1, P3-T2 | JA 우선, missing fallback, original rename 뒤 유지 | 내부 서버 회귀 통과, 외부 surface 미검증 |
| NAME003 | P1-T1, P2-T1, P3-T1, EXT001/003 | 내부 inventory 및 mixed USER, 외부 client 전체 surface | 내부 테스트 통과, EXT001/003 blocked |
| NAME004 | P1-T1, P2-T2 | locale-independent original OR JA alias, count/list 중복 방지 | 서버 구현/회귀/리뷰 보완 통과 |
| NAME005 | P1-T3, P2-T1, EXT001 | 원본 편집/인증/USER flow 불변 | 서버 경계 회귀 통과, 실제 client editor EXT001 blocked |
| NAME006 | P1-T1, P3-T1, P3-T2 | snapshot 보존, 내부 current lookup/new payload locale별 | 내부 회귀 통과, 외부 앱/provider 증거 blocked |
| NAME007 | P1-T2, P1-T3 | empty/whitespace clear, 그 외 원문 문자 보존 | 계획 제안 |
| EXT001 | P3-T2, P3-GATE | 앱 표면/locale/cache/raw editor 소유자 test | `[blocked]` |
| EXT002 | P1-T3, P3-GATE | 실제 운영 UI 소유/인증/deployment/API 소비 test | `[blocked]` |
| EXT003 | P3-T1, P3-T2, P3-GATE | session locale, reconnect, receiver locale, simultaneous sessions | `[blocked]` |
## 변경 금지 및 결정 규칙
- COR012 승인된 서버 구현 범위는 진행한다. 운영 DB 적용과 운영 배포는 별도 승인이 필요하다.
- API route/DTO는 COR012 승인 및 실제 HTTP 테스트 근거가 있다. MySQL 물리 호환성은 실제 DB 적용 전 확인해야 한다.
- product 요구 범위를 바꾸면 PRD decision log를 먼저 갱신하고 plan/test를 재승인한다.
- 완료된 task의 checkbox/Progress/test 결과를 지우거나 덮지 않는다. 회귀 수정은 새 Task/Goal로 누적한다.
- 과거 snapshot/payload를 current-name 경로로 분류하거나 자동 변환하지 않는다.
- 외부 UI, 모바일, client cache, RTC 범위는 경로 미확인을 이유로 scope-out/completed 처리하지 않는다.
- test 삭제/skip/약화, DTO 이름·타입 변경, 글로벌 serializer trick, `LangContext` 비동기 오용을 금한다.
- MySQL DDL 초안은 모든 column/table COMMENT와 프로젝트 `TIMESTAMP` convention을 적용한다. displayName unique/인덱스나 추가 인덱스는 두지 않고 실제 DB 호환성은 운영 적용 전에 확인한다.
- 새로운 dependency는 필요성이 증명된 경우 소비 Task에서만 추가한다.
## Progress
아래 과거 기록은 당시 상태를 보존한다. 최신 구현/검증 상태는 마지막 누적 기록을 따른다.
### 문서 작성 검증, 2026-10-14
- 상태: 문서 검토 / 구현 승인 대기
- 무엇을: PRD decision log를 DEC001~008 개별 행으로 복원하고 DEC009를 append, 기술 제안/외부 blocker 분리, goal 계획 작성.
- 왜: 사용자 요구사항 추적과 승인 전 비활성 계획을 보장.
- 어떻게:
- `./gradlew tasks --all` — 성공, Gradle task 목록 출력. 문서 변경 유지보수 guide의 명령 유효성 확인만 수행.
- product test/build/lint — 실행하지 않음. 구현 승인 전이고 문서 전용 변경이므로 실행 불필요.
- 문서 경로 점검 — PRD, plan, sample, guide 및 계획에 적은 기존 소스/테스트 파일을 검색/대조.
- 남은 항목: 사용자 문서 검토 및 별도 구현 승인. 모바일/운영 UI/RTC 담당자와 경로/계약 미확인.
- 다음 행동: PRD와 계획을 검토하고 기술 제안/API 계약 승인 여부를 결정한다. 승인 전 구현 goal 생성하지 않음.
### 리뷰 보정 문서 검증, 2026-10-14
- 상태: 문서 검토 / 구현 승인 대기; 활성 Goal 없음.
- 무엇을: legacy home/rank/recommend, member nickname search, v2 content/channel paths, role/memberKind boundary, search tests, push persistence capacity를 inventory와 task에 추가했다.
- 어떻게: 기존 경로는 `rg --files`로 확인. `./gradlew test`, `./gradlew build`, `./gradlew assemble`, `./gradlew ktlintCheck`는 실행하지 않았다. product test는 구현 승인 전이며 이 보정은 문서만 변경한다.
- 남은 항목: 모든 product tests/build는 NOT RUN. 외부 운영 UI/mobile/cache/RTC 소유자·경로와 payload capacity 계약은 미확인/blocked.
- 다음 행동: 리뷰 재확인 및 별도 구현 승인 전까지 product 변경/Goal 실행 금지.
### 문서 최종 검토, 2026-10-14
- 상태: 문서 작성 및 에이전트 검토 완료 / 사용자 검토 및 구현 승인 대기. 제품 구현은 미착수다.
- 독립 검토: 레거시 홈/랭킹/추천 누락과 role/memberKind 경계 표현을 보완한 뒤 동일 검토자가 재검토하여 PASS, 중요한 잔여 발견 사항 없음으로 판정했다.
- 사용자 질문 반영: 별도 `creator_localized_name` 테이블의 목적, 원본 이름 저장 위치, 언어별 행 구성과 일본어 이름 해제 방식을 PRD에 명시했다. 실제 테이블/DDL은 생성하지 않았다.
- 문서 검증: `./gradlew tasks --all` exit 0, `BUILD SUCCESSFUL`(869ms). 두 문서 각각 `git diff --no-index --check -- /dev/null 문서경로` exit 0. `git status --short`에서 이번 문서 디렉터리만 추가 상태임을 확인했다.
- 검증 한계: Markdown LSP 진단 도구는 제공되지 않아 직접 문서를 읽고 경로·요구사항·체크박스·명령 및 공백 오류를 대조했다. 제품 테스트/빌드/lint와 외부 앱/운영 UI/RTC 실행 검증은 수행하지 않았다.
- 남은 조건: 사용자 문서 검토와 별도 구현 승인, EXT001~003의 소유 저장소/계약 확인 및 실제 DB/provider 용량 검증. 이 조건들은 문서 작성 완료와 제품 구현 완료를 구분한다.
외부 blocker의 상세 상태와 재개 조건은 앞의 `외부 경로, 아직 특정 불가` 표를 기준으로 한다. blocker가 남으면 영향 Task와 Phase Gate는 완료가 아니며, 승인 없는 scope change로 생략하지 않는다.
### DOCDDL 후속 작성 검증, 2026-10-14
- 상태: 세 산출물 작성 완료, 독립 `DOCDDL-GATE` 미완료. DB 미적용이며 P1~P4 제품 checkbox는 미완료 유지.
- 무엇을/왜: 사용자 확정 테이블명과 문서·DDL 작성 승인만 반영하고 COR008~010을 누적했다. `Lang` enum-string/soft clear는 기존 수동 관리 근거에 따른 기술 결정이지 사용자 삭제 방식 지정이 아니다.
- 어떻게: `read`로 직접 소스·기존 DDL·세 산출물을 읽고 `rg`로 member+lang/DB `JA`/HTTP `ja`, 비활성 제외·동일 ID 재활성화, COMMENT/TIMESTAMP/unique/FK 및 승인 경계를 정적으로 대조했다. 물리 DELETE 언급은 초기 선택·사용자 질문 이력 설명에만 남았다.
- 실행하지 않음: DB 연결/SQL 실행, Kotlin 테스트, lint/build/앱 QA. `./gradlew tasks --all`의 이번 변경 후 확인은 상위 검토자가 수행할 예정이며 아직 완료로 기록하지 않는다. Markdown/SQL LSP 도구도 제공되지 않았다.
- 남은 조건: 독립 Gate, 별도 제품/DB 실행 승인, 운영 `member.id` 타입·engine/collation/저장 한계 확인과 EXT001~003 계약. 과거 Progress의 DDL 미작성 기록은 당시 이력으로 보존하며 이번 파일 작성 기록이 우선한다.
### DOCDDL VARCHAR(255) 보정, 2026-10-14
- 상태: 세 산출물 보정 완료, 독립 `DOCDDL-GATE` 및 제품 구현 승인 대기. 제품 P1~P4 checkbox는 미완료 유지.
- 무엇을: `display_name` 저장을 `VARCHAR(255)`로 맞추고 PRD/계획의 현재 저장 계약과 경계 테스트를 보정했다. TEXT가 불필요하다는 사용자 확인과 기본 JPA nickname 표현을 따른 기술 결정을 구분했다.
- 어떻게: 직접 읽기와 `rg`로 현행 TEXT/300자·무제한 요구 잔여가 없음을 정적으로 확인했다. SQL/테스트/빌드 실행은 하지 않았다.
- 남은 조건: 독립 검토 pending; 실제 DB 물리 호환성 미확인.
### DOCDDL-GATE 최종 검증, 2026-10-14
- 상태: 문서·DDL 파일 작성 및 정적 Gate 완료. DB 미적용, P1~P4 제품 구현은 미착수다.
- 독립 검토: 새 검토자가 현재 세 산출물을 직접 대조해 PASS 판정. `Lang` enum DB `JA`/HTTP `ja`, soft clear·동일 ID 재활성화, 활성 JA/CREATOR 필터, `VARCHAR(255)`·255/256 code point 경계 계획, 승인 범위와 실제 DB 미검증 전제의 정합성을 확인했다.
- 실행 검증: `./gradlew tasks --all` exit 0, `BUILD SUCCESSFUL`(867ms). 세 파일 각각 `git diff --no-index --check -- /dev/null 파일경로` exit 0. `rg`로 DDL의 7개 컬럼·PK·member+lang unique·FK·COMMENT·TIMESTAMP·engine/charset 및 제품 checkbox 미완료를 직접 확인했다.
- 검증 범위: SQL은 MySQL 구문과 기존 DDL의 정적 대조만 수행했다. MySQL/SQL parser/SQL LSP 도구가 없어 실제 SQL 실행 검증은 하지 않았고, DB 접속·컨테이너 실행·제품 테스트/빌드/lint·외부 앱 QA도 수행하지 않았다.
- 남은 조건: 사용자 문서 검토 및 별도 제품/DB 실행 승인, 적용 환경의 `member.id` 타입·engine/collation·저장 호환성 확인, EXT001~003 계약. 파일 작성 완료는 실제 DB 테이블 생성이나 기능 완료가 아니다.
### 구현 승인 및 P1-T1 기준선, 2026-10-14
- 사용자 확인: "DDL + 문서 확인 / 구현 승인". 이어서 ULW 실행 요청. 과거 제품 구현 미승인 문구는 COR012로 대체한다. 운영 DB 적용·배포는 별도 승인 대상이다.
- 사용자 추가 제약: 커밋·푸시는 승인 없이 진행하지 않는다. 이번 실행은 작업 디렉터리 변경만 유지한다.
- 환경: 현재 `test` 브랜치, 시작 시 이 작업의 문서/DDL 3개만 미추적 상태. 테스트 설정은 H2 메모리 DB와 localhost embedded Redis를 사용하고 Firebase/Android Publisher는 비활성화한다. 실제 outbound가 필요한 테스트는 외부 client를 대체한다.
- 기준선: `./gradlew test --tests 'kr.co.vividnext.sodalive.admin.member.AdminMemberServiceTest' --tests 'kr.co.vividnext.sodalive.search.SearchServiceTest'` exit 0, `BUILD SUCCESSFUL`(35s). 종료 중 Redis reconnect warning은 변경 전에도 관찰된 경고이며 테스트 실패가 아니다.
- 처리 경계: 원본 `Member.nickname` 및 인증/편집 흐름은 불변. ID를 모은 batch 별칭 조회를 응답 경계에서 적용한다. 레거시 조회/검색, v2 조회, 수신자별 전달을 서로 다른 파일 소유 범위로 나눠 조사·회귀 테스트를 준비한다. Gradle 실행은 직렬화한다.
- 외부 범위: 모바일/관리 UI/RTC 저장소 및 클라이언트 동작은 여전히 확인되지 않았다. 서버 구현·테스트와 전체 제품 연동 완료를 구분한다.
- 중단 기준: 승인된 서버 시나리오의 실제 HTTP/실시간 결과, 테스트·빌드·정적 검증, 자원 정리와 독립 리뷰를 확인한 뒤 보고한다. 외부 미검증은 blocker로 명시한다.
### P1-T1~T3 구현 및 소비자 RED, 2026-10-14
- P1-T1: `legacy-name-inventory.md`, `v2-name-inventory.md`, `delivery-name-inventory.md`에 호출 경로/현재 조회/중립 캐시/스냅샷/원본 편집/외부 blocker 및 정확한 테스트 소유자를 기록했다. 운영·외부 클라이언트 근거는 미확인으로 유지한다.
- P1-T2/T3: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creatorname/` 운영 8개 파일과 테스트 4개 파일 구현. 기존 회원 행 잠금·오류 규약을 재사용하고 활성 JA CREATOR 배치 조회, 소프트 해제·동일 행 재활성화, 255 code point 상한 및 ADMIN 실제 HTTP를 검증했다.
- RED 증거: `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/creatorname-foundation-red.log`, `creatorname-persistence-red.log`, `creatorname-batch-red.log`. 실제 HTTP 404 및 미구현 batch 결과의 assertion 실패를 확인했다.
- GREEN 증거: 같은 임시 디렉터리 `creatorname-foundation-final-green.log`. `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creatorname.*' assemble --console=plain` 성공, 18개 테스트(서비스 9/persistence 5/HTTP 4), 실패·skip 0. 부모 통합 RED 실행에서도 이 18개는 모두 통과했다.
- 실제 표면: `AdminCreatorLocalizedNameControllerTest`는 random-port localhost에 실제 JWT GET/PUT을 전송한다. 성공 200/성공 envelope, 권한·업무 오류는 기존 200/오류 envelope, 미인증 401. `@DirtiesContext` 종료와 작성자 curl 연결 거부로 서버 정리를 확인했다.
- H2 차이: 테스트 DB는 UTF-16 VARCHAR 길이 차이를 피하려고 테스트 전용 VARCHAR(510)+255 code point CHECK를 사용한다. 운영 entity/DDL은 VARCHAR(255)이며 별도 persistence 매핑 테스트가 255 컬럼을 확인한다. 운영 MySQL 적용 검증을 대체하지 않는다.
- 독립 모듈 리뷰: 운영·테스트 파일 검토 결과 중요한 코드 결함 없음. `ktlintCheck`는 다른 소유자의 새 소비자 테스트 5파일 11개 서식 오류 때문에 실패하여 전체 P1 Gate는 미완료다. 해당 소유자가 수정하며 최종 통합 전에 재실행한다.
- 위 모듈 리뷰/lint 행은 이 당시 실행 상태를 기록한 이력이다. 아래 최종 전체 실행이 서식 보완 후 `ktlintCheck`를 포함해 통과했으므로 현재 P1 Gate는 통과 상태다.
- 소비자 RED: `./gradlew test --tests '*LocalizedName*' --tests 'kr.co.vividnext.sodalive.v2.api.content.all.adapter.in.web.MainContentAllEndToEndTest' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.community.adapter.in.web.CreatorChannelCommunityEndToEndTest' --tests 'kr.co.vividnext.sodalive.v2.api.home.CreatorRankingControllerTest' --tests 'kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest' --console=plain`에서 104개 중 35개 실패. `creatorname-consumer-red.log`에 현재 원본 이름/검색 누락/handshake 언어 부재 assertion을 기록했다. live 준비 데이터 오류 등 미구현과 무관한 실패는 소유자에게 따로 보정 요청했다.
- 실행 판단: 모듈 코드·18개 테스트 검증 후 서로 독립인 소비자 연결을 병행한다. 전체 Gate 실패를 성공 처리하지 않으며 Gradle 실행은 부모에서 직렬화한다. 커밋·푸시·운영 DB 적용은 하지 않았다.
### P2/P3 연결 및 통합 검증 착수, 2026-10-14
- 상태: 레거시·v2·전달 담당자의 코드 연결 완료 보고를 받아 통합 검증 중이다. GREEN이나 전체 Gate 완료로 아직 표시하지 않는다.
- 세부 목록: `legacy-name-inventory.md`의 L01~L43, `v2-name-inventory.md`의 HTTP/중립 캐시/원본 경계, `delivery-name-inventory.md`의 FCM 생산 22곳·세션·라이브·메시지 연결 상태를 참조한다. 목록의 '구현/대기'는 실행 검증 완료가 아니다.
- 최초 컴파일: `./gradlew compileTestKotlin --console=plain` 실패. `ContentSeriesService.kt`의 `renderSeries`가 이전 메서드 내부에 배치되어 unresolved reference가 발생했다. 통합 담당자가 이 오류와 직접 생성자·JPA slice 의존성 연결을 수정한다.
- 검증 소유권: 이후 Gradle 명령은 통합 담당자 한 명만 실행한다. 병행 작업자는 소스 읽기/리뷰만 수행하며 같은 build 디렉터리에 중복 Gradle을 실행하지 않는다.
- 코드 리뷰: Oracle이 전체 현재 diff와 신규 파일·시나리오를 읽고 있다. 원본/권한/언어별 캐시/검색 조건/소유자 ID/수신자별 이름의 불변식 및 누락 경로를 판정하며 결과 전 최종 완료 선언은 하지 않는다.
- 외부 경계와 승인: 운영 DB와 외부 앱/관리 UI/RTC는 아직 검증되지 않았다. 사용자 지시에 따라 커밋·푸시를 실행하지 않는다.
### 통합 전체 회귀 및 상태 갱신, 2026-10-14
- 상태: 서버 구현과 전체 회귀 통과. P4-R1 리뷰 보완 진행 중이며 외부 연동/최종 Gate는 미완료. 현재 활성 Goal은 `P4-R1`이다.
- 근거: `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/creatorname-integrated-full.log`. 최종 성공 실행은 322 suites, 2245 tests, failures 0, errors 0, skipped 0. `test`, `ktlintCheck`, `assemble` 포함 명령이 `BUILD SUCCESSFUL`로 종료됐다.
- 실행 이력: 이전 전체 실행은 2245 중 200 fixture/context 실패로 끝났고 로그에서 삭제하지 않았다. constructor/JPA/fixture/context 문제를 고쳤으며 테스트 기대를 약화하지 않고 마지막 전체 실행에서 통과했다.
- RED/GREEN: 기반의 genuine RED는 actual HTTP 404 및 batch 미구현; consumer RED는 104 tests 중 35개 이름/locale 실패를 기록했다. 최종 GREEN에는 기반 18 tests, consumer 및 전달/캐시/라이브 회귀가 포함된다.
- 실제 표면: 관리 API JWT GET/PUT HTTP 서버 port 53418, WebSocket JWT/Redis HTTP 서버 port 53427 통합 테스트가 실행됐다. 성공 API envelope는 200, 오류 envelope는 200, 미인증은 401. teardown curl은 두 포트 모두 연결 거부(exit 7)로 서버 종료를 확인했고 Hikari pool shutdown을 로그에서 확인했다. 종료 후 잔류 JVM/Redis 프로세스는 보고되지 않았다.
- P1/P2: 기반 저장·관리 및 P2 내부 legacy/v2 표시·검색 검증과 내부 Gate 완료. inventory가 다루는 서버 consumer는 확인했지만 외부 client 전체 display consumer의 전수 확인을 주장하지 않는다.
- P3: 내부 FCM/WS/live/message 구현과 테스트는 통과. EXT001 모바일 표면/raw editor/cache, EXT002 운영 UI, EXT003 client locale/reconnect/RTC, push provider 및 운영 DB 용량은 소유자/계약/운영 evidence 미확인으로 blocked이며 `P3-T2`, `P3-GATE` 미완료.
- P4-R1 리뷰 보완: Oracle이 현재 이름 fallback의 여섯 `findAllById` entity graph 조회를 scalar ID/nickname 조회로 교체, 두 common helper를 `v2.creatorname` 아래로 이동할 것을 지적했다. 실제 cache hit assertion도 아직 입증되지 않았다. 수정 담당자가 진행 중이며 이 문서 갱신 시점에 수정 또는 테스트/리뷰 승인 증거는 없다. 별도 P4-R1 task를 추가했으며 체크박스는 미완료다.
- 승인 경계: COR012는 문서의 DDL/API 구현 및 테스트를 승인했다. 운영 DB 적용·배포는 미승인 별도 작업이며 커밋·푸시는 사용자 승인 없이 하지 않는다.
- 검증 제한: `lsp_diagnostics` 도구가 없어 LSP 진단은 미수행. compile, 전체 test, assemble 및 ktlint는 위 성공 로그를 근거로 한다. 실제 MySQL/FCM provider/client 앱 QA는 수행하지 않았다.
- 다음 행동: P4-R1 구현 후 target 및 full regression, lint/build 재실행과 Oracle 승인 기록. 외부 blockers 해소 증거를 받기 전 제품/최종 Gate 완료 금지.
### P4-R1 및 서버 최종 검증 완료, 2026-10-15
- 상태: 승인된 서버 구현과 리뷰 보완 완료. 외부 연동 P3-T2/P3-GATE 및 제품 전체 P4-GATE는 EXT001~003으로 차단 유지하며 서버 완료와 구분한다.
- 수정 증거: `creatorname-reviewfix-files.txt`, `creatorname-reviewfix-target-red.log`, `creatorname-reviewfix-target-green.log`. 로그 공통 디렉터리는 `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/`다. 기존 이름 전용 엔티티 조회 6곳을 scalar로 교체했고 Member 관계 매핑이나 Gift의 엔티티 소비는 변경하지 않았다. helper 2개와 관련 테스트만 v2로 이동했다.
- 성능/캐시: 영속성 컨텍스트를 비운 1명/4명에서 JA SQL 2/2회, KO·EN 1/1회, entityLoadCount 0/0. 중복·누락 ID·USER·탈퇴 원본·빈 입력도 검증했다. 실제 `cache_ttl_10_minutes::getLatestFinishedLive` raw entry는 `host`로 유지됐고 KO→JA→원본 변경/별칭 해제 중 방 목록 query 실행은 1회였다.
- 전체 검증: `./gradlew test assemble ktlintCheck --continue --offline --console=plain` exit 0, 322 suites / 2249 tests / failures 0 / errors 0 / skipped 0. `creatorname-reviewfix-full.log`에 `BUILD SUCCESSFUL in 7m 24s`; 부모가 XML 집계를 별도로 확인했다. 이후 `./gradlew build --offline --console=plain`도 exit 0(3s, 검증 작업 UP-TO-DATE), `creatorname-parent-build.log`에 기록했다.
- 실제 표면 재실행: 부모가 `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creatorname.adapter.in.web.AdminCreatorLocalizedNameControllerTest' --tests 'kr.co.vividnext.sodalive.v2.usercreatorchat.websocket.UserCreatorChatLocalizedNameIntegrationTest' --offline --console=plain`을 실행했다. 13개/실패·오류·skip 0, `BUILD SUCCESSFUL in 38s`, `creatorname-parent-http-ws-qa.log`가 증거다. 이 좁은 재실행으로 현재 XML은 13개 결과이며 전체 2249개 증거는 직전 전체 실행 로그·집계를 따른다.
- 자원 정리: 실제 HTTP 54539/WS 54547의 Tomcat·Hikari 종료 XML 확인. 두 포트 후속 curl은 각각 exit 7(연결 거부), `pgrep -fl 'Gradle Test Executor|redis-server'` 결과 없음. 정상 Gradle daemon은 빌드 인프라로 유지한다.
- 독립 리뷰: Oracle이 수정분과 SQL/캐시 증거를 재검토해 APPROVE, 잔여 차단 코드 지적 없음으로 판정했다. 새 검토 세션은 이전 세션 재개 도구 오류로 교체했으며 기존 두 지적과 수정분만 재검토했다.
- 한계: LSP 진단 도구 미제공으로 Kotlin 컴파일·ktlint로 대체했다. H2 Unicode 길이 보정 테스트는 MySQL 운영 검증을 대체하지 않는다. 운영 DB 적용, 실제 모바일/관리 UI/RTC/provider, 배포는 미수행이며 커밋·푸시도 하지 않았다.
## COR013: 조회 경로 3종 리팩터링
기존 완료 체크와 로그는 당시 증거로 보존한다. 아래는 별도 사용자 승인 후속 작업이며 제품 범위 추가나 운영 DB 적용이 아니다. 기존 3개 inventory의 현재 조회/캐시/역사 분류를 따른다.
과거 P1~P4의 checkbox와 Progress는 당시 batch 기반 구현·검증 이력으로 보존한다. 현재 COR013 조회 정책은 이 절과 최신 inventory를 따른다. DB 직접 projection은 요청 Lang의 JOIN+COALESCE, 중립 cache/수신자별 전달 및 원본 entity 경계는 필요한 batch다. 관리와 검색은 JA 정책이다.
### JOIN-T1: 공통 조회 계약
- Objective: DB projection의 조인과 경계 batch를 기존 Lang으로 처리한다.
- Files: `src/main/kotlin/kr/co/vividnext/sodalive/v2/creatorname/adapter/out/persistence/CreatorNameJoin.kt`(신규), 같은 도메인의 서비스·port·repository·LegacyCreatorNameRenderer 및 해당 테스트.
- 시작: COR013 및 이번 creatorname/maincontent/search 기준선 성공 확인. 완료 증거: 요청 Lang별 JOIN fallback 및 단일 SQL batch, sparse override 의미 보존, 기존 관리 JA 테스트 통과.
- 계약: `CreatorNameJoin(owner: QMember, lang: Lang, alias: String)`는 `name`, `condition`, `displayName`을 제공한다. query owner가 명시적으로 leftJoin/on/select하며 owner별 alias는 충돌하지 않게 지정한다. `findLocalizedNames`는 모든 Lang의 sparse 활성 CREATOR map, 신규 `findCurrentNames(ids, lang)`는 존재하는 회원의 원본/별칭 COALESCE map 한 SQL이다. 빈 IDs는 0 SQL.
- [x] RED: EN 활성 별칭 읽기, JA/KO/EN current batch 단일 SQL, JOIN의 USER/미등록/비활성 보존 assertion 실패를 확인했다.
- [x] GREEN: 읽기 JA guard를 제거하고 공통 조인/batch를 구현했다. 관리자 get/save의 JA 정책과 검색 JA EXISTS는 유지했다.
- [x] REFACTOR: 최초 core 43개 통과 및 공통 쿼리 계약 확인. 이후 미사용 helper의 전용 테스트 1개 제거를 포함한 최종 전체 회귀는 JOIN-GATE에서 통과했다.
### JOIN-T2: DB 직접 projection 전환
- Objective: 기존 DB 조회가 표시 이름까지 반환하고 별칭 후속 SELECT/이름 복사 처리를 하지 않는다.
- Files: legacy inventory L01~L43의 DB query owner 및 caller, v2 inventory의 content/home/channel/recommendation/native SQL query 및 facade, delivery inventory의 live/message/DM DB 목록 query. 원본 편집·entity 저장·언어별 역사 producer는 제외한다.
- 시작: JOIN-T1 계약 확정. 소유 범위는 legacy / v2(creatorname·usercreatorchat 제외) / live·message·fcm·usercreatorchat·v2chat으로 나누며 공유 파일 writer는 한 명만 둔다.
- 구현 규칙: `ON(member_id, lang, is_active, CREATOR)`와 COALESCE. count/후보 ID pagination에는 불필요한 표시 조인 금지. GROUP BY와 native SQL은 원래 집계/필터/정렬을 보존하며 native lang은 enum NAME으로 bind한다. 공개 JSON owner ID 추가·원본 Member 변이·전역 serializer·표시 subselect 우회는 금지한다.
- [x] RED: 실제 main-content/search/혼합 writer repository에서 별칭 전용 후속 SELECT가 남는 실패를 확인했고 SQL assertion은 유지했다.
- [x] GREEN: 세 inventory가 명시한 직접 DB projection에 join을 적용하고 이름 전용 batch/copy를 제거했다. owner alias는 충돌 없이 분리하고 count/group/page는 보존했다.
- [x] REFACTOR: 소유자별 테스트 및 부모의 최종 전체 build/QA를 JOIN-GATE에서 확인했다.
### JOIN-T3 / JOIN-GATE: 경계 보존 및 최종 검증
- Objective: 중립 캐시·수신자별 batch와 언어별 역사 스냅샷의 기존 의미가 유지된다.
- Files: 기존 FCM/WS/cache 및 localized-name 통합 테스트, PRD/plan과 3개 inventory.
- 시작: JOIN-T2 변경 완료. 완료 증거: 아래 실제 테스트·빌드·HTTP/WS·캐시 검사와 독립 리뷰. 외부 모바일/UI/RTC 증거는 기존 blocker로 유지한다.
- [x] current-name batch 단일 JOIN, recipient sparse alias fallback, raw shared payload 및 이미 렌더된 역사 이름 보존 assertion을 확인했다.
- [x] 부모 소유 최종 `./gradlew test build ktlintCheck --continue --offline --console=plain` 성공, 2265개 테스트 및 실제 ADMIN HTTP/WS QA·자원 종료 증거를 기록했다.
- [x] 세 inventory의 잔여 batch 호출을 cache/recipient/principal/business entity/write-return/error 경계별로 대조했다. 단순 DB projection으로 남은 batch는 보고되지 않았다.
- 범위 밖: 커밋·푸시, 운영 DB 적용·배포, 관리자 다국어 쓰기/API 확대, 검색 언어 확대, 외부 클라이언트 구현.
### COR013 기준선
- `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creatorname.*' --tests 'kr.co.vividnext.sodalive.v2.api.content.all.adapter.in.web.MainContentAllEndToEndTest' --tests 'kr.co.vividnext.sodalive.search.SearchRepositoryLocalizedNameTest' --offline --console=plain` 성공(1m53s). 로그: `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/creatorname-join-baseline.log`.
- 쿼리 수 개선은 H2에서 실제 statement 수로 검증하며 운영 MySQL 지연 시간 개선을 측정 없이 주장하지 않는다. 모든 Gradle 실행은 직렬화한다.
### COR013 진행 및 최종 Gate 대기, 2026-10-15
- 상태: JOIN-T1 및 JOIN-T2 완료. JOIN-T3 경계 assertion 통과. `JOIN-GATE`의 최종 전체 build와 실제 HTTP/WS QA 결과는 부모 실행 중이므로 아직 미완료다. 외부 제품 Gate `P3-GATE`와 `P4-GATE`도 별도 blocker로 유지한다.
- 기준선: `creatorname-join-baseline.log` 성공(1m53s). core RED는 43개 중 13개 assertion 실패, core GREEN은 43개 통과(`creatorname-join-core-green.log`). 이는 현재 전체 회귀가 아니다.
- 소비자 RED: `creatorname-join-consumer-red.log`에서 3개 테스트 실패. 표시 결과 외에 standalone alias SELECT 기대 0/실제 1이 확인되어 assertion을 유지했다.
- 소비자 GREEN: `creatorname-join-first-green.log`에서 3개 통과(2m37s), alias 전용 SELECT 0 확인.
- 통합 시도: `creatorname-join-full.log`의 `./gradlew test build ktlintCheck --continue --offline`는 테스트/build 후 ktlint의 줄바꿈/import 정렬 오류 18파일로 실패했다. 성공으로 기록하지 않는다.
- 형식 보정: 동작 변경 없이 ktlint 형식만 수정한 뒤 `creatorname-join-lint-green.log`의 ktlintCheck 성공(23s)을 확인했다. 최종 전체 실행/HTTP·WS QA 완료를 이 결과로 대체하지 않는다.
- Oracle 3-path 검토는 material code finding 없이 APPROVE. 판정은 full runtime 결과 대기 조건을 포함한다.
- 부모 최종 실행 결과를 확인한 뒤 별도 기록한다. 전체 test 수는 집계 확인 전 기재하지 않는다. MySQL/provider/UI/RTC 실제 검증은 미수행이며 운영 DB 적용·배포·commit/push도 하지 않는다.
### COR013 JOIN-GATE 완료, 2026-10-15
- 상태: JOIN-T1/T2/T3 및 서버 JOIN-GATE 완료. 기존 외부 제품 P3-GATE/P4-GATE는 EXT001~003으로 차단된 상태이며 서버 완료와 구분한다.
- 조회 결과: DB 직접 projection은 명시 LEFT JOIN의 ON에 member/lang/active/CREATOR 조건을 두고 COALESCE를 선택한다. 후속 alias-only SELECT는 대표 main-content/search/live 추천에서 1→0회다. current-name cache batch는 기존 2조회 결합 대신 단일 joined scalar SQL, FCM/WS는 sparse recipient override, 이미 언어별로 저장된 알림 이름은 저장값 반환을 유지한다.
- 정리: 이번 전환으로 production caller가 없어진 `LegacyCreatorNameRenderer.render`와 `MemberQueryRepository.findNicknamesByIds`만 제거했다. passing 상태의 해당 generic-render 전용 테스트 1개도 제거해 총 테스트 수가 2266→2265가 됐다. 실제 batch의 모든 Lang/역할/누락/중복/비활성·현재 원본/SQL 수 단언은 보존했고 `creatorname-join-orphan-check.log`의 core 및 lint가 통과했다.
- 최종 명령: `./gradlew test build ktlintCheck --continue --offline --console=plain` exit 0, **2265 tests / failures 0 / errors 0 / skipped 0**, `BUILD SUCCESSFUL in 13m 45s`. 로그 `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/creatorname-join-release-check.log`; 부모가 XML 수치를 별도 집계했다. 앞선 2266개 통과+서식 오류 및 서식 보정 후 성공 로그는 이력으로 유지한다.
- 실제 표면: 최종 전체 실행에 실제 JWT ADMIN HTTP 4개, localhost HTTP/WebSocket 9개가 포함되어 모두 통과했다. 포트 58173/58186의 Hikari 종료 XML 및 부모 후속 curl exit 7을 확인했다. `pgrep -fl 'Gradle Test Executor|redis-server'` 결과 없음. 정상 Gradle daemon은 유지한다.
- 리뷰: Oracle COR013 정적 검토에 material finding 없음, 전체 runtime 성공으로 승인 조건 충족. 미사용 API 제거 수정분 재검토도 APPROVE로 종료했다. 이전 검토 세션 재개가 도구 timeout으로 실패해 새 검토자가 마지막 삭제 변경만 재검토했다.
- 한계: LSP 진단 도구는 없어 Kotlin 컴파일·ktlint로 검증했다. H2/로컬 Redis 검증은 운영 MySQL 실행 계획·실측 지연 시간 및 실제 모바일/provider/RTC 검증을 대신하지 않는다. 운영 DB 적용·배포·커밋·푸시는 수행하지 않았다.
## COR014: 관리자 회원 목록 통합
후속 COR015는 아래 COR014 기록을 변경하지 않고 문서 하단에 누적한다.
### ADMIN-LIST-T1: 목록 확장 및 쓰기 API 분리
- Objective: 관리자는 기존 회원 페이지 목록에서 원본과 모든 활성 언어별 이름을 함께 보고, 신규 일본어 PUT으로만 등록·수정·해제한다.
- 시작 조건: PRD COR014 사용자 적용 승인. 빌드 금지 요청을 유지한다.
- Files: `src/main/kotlin/kr/co/vividnext/sodalive/admin/member/GetAdminMemberListResponse.kt`, `AdminMemberService.kt`; `src/main/kotlin/kr/co/vividnext/sodalive/v2/creatorname/`의 service/port/persistence adapter 및 `AdminCreatorLocalizedNameController.kt`. Test: 기존 `admin/member/AdminMemberServiceTest.kt`, `v2/creatorname` service/controller 테스트 및 필요 시 신규 관리자 목록 계약 테스트.
- 계약: 목록 항목에 `localizedNames: Map<String, String>` 추가. 원본 nickname·기존 count/offset/limit 유지. 페이지 CREATOR IDs에 대한 활성 별칭 1회 조회, 언어 필터 없이 Lang.code 키로 조립한다. 미등록·비활성 언어 생략, 일반 회원은 빈 map. 목록·검색 4개 공통 변환 경로를 사용하며 간단 선택용 두 API는 변경하지 않는다.
- API: 전용 GET 제거, PUT과 `CreatorLocalizedNameResponse` 확인 응답 유지. 단건 조회 서비스는 운영 caller가 사라지면 제거하고 테스트는 목록/저장 결과/persistence 기준으로 같은 lifecycle 검증을 유지한다.
- 범위 밖: 앱 언어 선택/JA 검색, 관리자 콘텐츠 원본 표시, DDL/H2 보정, 신규 다국어 쓰기/POST/DELETE, 레거시 API 제거, commit/push/운영 적용.
- TDD 실행 예외: 사용자 빌드 금지에 따라 Gradle이 유발하는 컴파일·테스트도 실행하지 않는다. 테스트 코드를 먼저 작성하되 RED/GREEN 확인은 미수행으로 남긴다. 정적 점검을 실행 검증으로 표현하지 않는다.
- [x] 테스트 작성: 모든 언어 표시·헤더 독립·일반 회원/해제 제외·4개 목록/검색·페이지/count·batch 쿼리 수·GET 제거·PUT 정책의 계약 테스트를 작성/보정했다. 실행하지 않았다.
- [x] 구현: 목록 DTO/공통 조립/별칭 batch 연결 및 전용 GET/단건 서비스 제거. PUT 확인 응답과 기존 페이지 쿼리는 유지했다.
- [x] 정적 검토: caller와 타입/role/언어/페이지/GET PUT 경로 및 문서 일치를 직접 확인하고 독립 PASS(static) 판정을 받았다.
- [ ] 실행 검증: **미실행/차단**. 허용 후 focused 명령 `./gradlew test --tests 'kr.co.vividnext.sodalive.admin.member.*' --tests 'kr.co.vividnext.sodalive.v2.creatorname.*'`와 HTTP/전체 영향 회귀를 실행한다. 현재는 `tasks --all`도 실행하지 않는다.
- 완료 구분: 코드 적용 및 정적 리뷰 완료와 빌드·테스트/HTTP 검증 완료는 별도 상태로 기록한다.
### COR014 정적 적용 기록, 2026-10-15
- 변경: 운영 6개 파일(관리자 목록 DTO/서비스, 이름 service/port/adapter/controller), 기존 테스트 3개 파일(AdminMemberServiceTest, CreatorLocalizedNameServiceTest, AdminCreatorLocalizedNameControllerTest)을 수정했다. 별칭 조회는 `findAllLocalizedNames` → `findAllActiveLocalizedNames`로 연결되며 언어 WHERE 조건 없이 페이지의 활성 CREATOR 별칭만 반환한다.
- 보존: 회원 원본 nickname·role/loginType 문구 번역·count/offset/limit·순서, 간단 선택용 DTO, 관리자 일반 콘텐츠 조회, 앱의 COR013 JOIN·JA 검색, PUT 권한/soft clear/잠금/255 code point 계약은 변경하지 않았다. `CreatorLocalizedNameResponse`는 PUT에 계속 사용한다.
- 정적 증거: 상위 에이전트가 수정한 운영/테스트 9개 파일을 직접 읽고 `rg`로 제거된 `getLocalizedName` 참조가 없고 기존 관리자 목록 4개가 공통 변환을 사용하는 것을 확인했다. `git diff --check`와 130자/후행 공백 검색에서 지적이 없었다. 이는 컴파일/ktlint 실행 결과가 아니다.
- 독립 리뷰: 첫 리뷰 작업은 도구의 모델 지원 오류로 결과 없이 종료되어 다른 검토자로 재시도했다. 최종 결과 **PASS(static)**, 호출·nested map 타입·ADMIN 권한·페이지·언어 무필터·테스트 fixture/공통 예외 규약의 명백한 문제 없음.
- 미실행: 사용자 빌드 금지로 Gradle(`tasks --all` 포함), 컴파일, ktlint, 테스트, RED/GREEN, 실제 HTTP/앱/DB 실행을 전혀 하지 않았다. LSP 도구도 제공되지 않는다. 이전 전체 테스트 통과 기록을 COR014의 증거로 재사용하지 않는다.
- 남은 조건: 사용자 실행 허용 후 이 절의 focused 명령과 HTTP 계약을 검증해야 한다. 정적 적용 완료는 실행 검증 통과가 아니다. 커밋·푸시·운영 DB 적용·레거시 API 제거는 하지 않았다.
## COR015: 선택적 레거시 이름 롤백 및 테스트 정리
### LEGACY-TRIM-T1
- Objective: 승인된 네 레거시 영역은 원본 이름을 반환하고 v2/공유 업무를 보존하며 불필요한 이름 래퍼·테스트 의존성을 남기지 않는다.
- 시작 조건: 사용자 문서 정리·적용 승인 및 불필요한 테스트 동시 정리 요청. 기존 미커밋 변경을 보존한다.
- Files: `src/main/kotlin/kr/co/vividnext/sodalive/content/main/tab/`, `content/main/curation/`, 공유 `content/AudioContentRepository.kt`, `content/series/ContentSeriesService.kt`, `ContentSeriesRepository.kt`, `rank/RankingService.kt`, `RankingRepository.kt`; `explorer/ExplorerService.kt`, `ExplorerQueryRepository.kt`, `explorer/profile/creatorCommunity/` 및 comment repository. 이름 래퍼 `v2/creatorname/application/LegacyCreatorNameRenderer.kt`와 남은 소비자, 대응 `src/test/kotlin/kr/co/vividnext/sodalive/` 테스트/`support/LegacyCreatorNameTestConfiguration.kt`, 이 디렉터리 PRD/계획/inventory.
- 보존: API/DTO schema, 기존 저장·권한·번역·알림, v2 커뮤니티·팬톡 JOIN, 공유 content/rank 소비자의 이름 표시, COR014 관리자 목록/PUT, 기존 미사용 업무 메서드.
- 공유 경계: KO 별칭도 유효하므로 `Lang.KO`로 원본 조회를 흉내 내지 않는다. 공유 조회가 필요하면 이름 조회에만 명시 raw/null 경계를 사용하고 제목·본문 번역 언어는 유지한다.
- 테스트 정리: 제거된 기능의 전용 단언/래퍼 테스트만 삭제한다. batch의 활성/역할/현재 원본/SQL 수 회귀는 core에 보존하고 characterization/번역/권한 테스트는 생성자·fixture만 적응한다. 불필요 `@Import`는 실제 의존성을 확인한 뒤 제거한다.
- 시나리오 S1: 네 제외 영역은 활성 JA/KO 별칭이 있어도 원본 이름을 반환한다. repository/HTTP 테스트 코드를 보정하고 production caller·projection을 정적 대조한다.
- 시나리오 S2: v2와 공유 content/rank 및 기타 유지 대상은 요청 Lang 이름/미등록 원본을 유지한다. 기존 JOIN·core·v2 테스트 단언과 caller를 대조한다.
- 시나리오 S3: 제거 wrapper 참조가 없고 불필요 테스트 설정만 정리되며 COR014·업무/번역/알림 단언이 유지된다. `rg`, 직접 diff 검토, 독립 정적 리뷰로 확인한다.
- 실행 제한: Gradle(`tasks --all` 포함), 컴파일, 테스트, lint, 앱/HTTP/DB 실행을 하지 않는다. TDD RED/GREEN과 실제 표면 검증은 차단이며 정적 검토로 통과를 주장하지 않는다. LSP 도구도 미제공이다.
- [x] RED 코드: 기존 테스트를 읽고 변경 계약에 맞는 최소 회귀 단언을 먼저 작성/보정했다. 실행 실패 확인은 금지에 따라 미수행.
- [x] GREEN 코드: 선택적 롤백, wrapper 직접 core 전환, 불필요 테스트/fixture 정리. 테스트 통과를 뜻하지 않는다.
- [x] REFACTOR 정적 검토: 잔여 참조/공유 caller/테스트 보존/문서 일치 및 `git diff --check`, 독립 APPROVE(static).
- [ ] 실행 검증 [blocked]: 허용 후 `./gradlew test --tests 'kr.co.vividnext.sodalive.content.*' --tests 'kr.co.vividnext.sodalive.explorer.*' --tests 'kr.co.vividnext.sodalive.v2.creatorname.*' --tests 'kr.co.vividnext.sodalive.v2.api.admin.aicharacter.*' --tests 'kr.co.vividnext.sodalive.v2.api.creator.channel.*' --tests 'kr.co.vividnext.sodalive.admin.member.*'`와 영향 회귀·HTTP를 확인한다. 현재 실행하지 않는다.
- 종료 조건: 승인된 코드·테스트 정리가 정적 리뷰에서 blocker 없이 확인되면 즉시 종료하며 실행 검증은 미완료로 보고한다. 커밋·푸시·운영 적용은 범위 밖이다.
### COR015 적용·정적 검토 기록, 2026-10-15
- 전용 조회: ContentMainTab/AudioContentCuration/레거시 커뮤니티·팬톡의 이름 JOIN·projection·이름 전용 언어 전달·응답 치환을 제거했다. 기존 업무 메서드, 삭제 회원 접두사 처리, 본문 번역, 공유 FCM `titleMemberId`는 유지했다.
- 공유 조회: `findByTheme(lang=null)`, 원본 오디오드라마 `nameLang=null`, 콘텐츠 랭킹 `nameLang=null`, 크리에이터 랭킹 `lang=null`로 제외된 탭만 원본을 읽는다. HomeService 등 유지 소비자의 명시 Lang과 제목 번역 locale은 보존한다. 시리즈 조회의 테스트 mock 보존용 위임 overload는 남기지 않고 HomeService/테스트도 명시 이름 언어를 전달한다.
- 의존성: `LegacyCreatorNameRenderer.kt`와 `LegacyCreatorNameRendererTest.kt`를 삭제했다. 남은 호출자는 `CreatorLocalizedNameService` 직접 주입이며 `originalLocalizedNameService()`는 기존 mock fixture를 직접 반환한다. 삭제된 래퍼의 단건/다건 SQL 1회·엔티티 로드 0·빈 입력·탈퇴 원본·이름 변경 후 clear fallback 단언은 기존 core 서비스 테스트로 통합했다.
- 테스트 정리: 실제 core 빈이 불필요한 JPA 테스트의 설정 import를 제거하고, GiftQueryServiceTest/AudioContentCommentNameMetadataTest/core persistence·JOIN 테스트의 필요한 설정은 유지했다. 커뮤니티 번역·언어 감지·characterization 테스트의 제거된 생성자 의존성도 정리했다. v2·검색·프로필·후원 등의 회귀 단언은 유지했다.
- 최소 회귀: 기존 `AudioContentRepositoryLocalizedNameTest`에 테마·큐레이션/알람·랭킹·오리지널 시리즈 raw/shared 회귀 4개를 추가했다. 기존 Explorer HTTP/커뮤니티 통합 사례는 활성 JA/KO 별칭에도 원본인 경계로 보정하고 팬톡 작성·수정·차단 메시지 및 FCM ID 단언을 보강했다. 모두 미실행이다.
- 부모 정적 확인: `rg`에서 삭제 wrapper/helper/필드 참조 0건, 탭·큐레이션 이름 JOIN 및 이름 전용 LangContext 0건. 커뮤니티의 번역용 LangContext는 유지한다. 전용 조회 caller를 전체 검색해 다른 live caller가 없음을 확인했고, `git diff --check`는 exit 0이다. 미추적 테스트 4개도 개별 `git diff --no-index --check /dev/null <path>`에서 지적이 없었다.
- 실행 한계: 컴파일·LSP·Gradle·테스트·lint·HTTP·앱·DB 실행은 수행하지 않았다. 정적 일관성은 실제 SQL/컨텍스트/HTTP 성공 증거가 아니며 기존 COR013 테스트 결과를 이번 변경의 검증으로 재사용하지 않는다. 실행 자원을 만들지 않았으므로 QA 종료 대상도 없다.
- 독립 Gate: `ses_ec179d026ffeTbyj1KNLcvwUpg` 검토자가 S1~S3의 코드·테스트·caller·설정 의존성을 대조하여 **APPROVE(static)** 판정했다. 확정 blocker 및 정적으로 입증된 fixture 실패 없음. 상세 기록은 `reviews/cor015-legacy-trim-review.md`이며 실행 검증은 계속 차단 상태다.
## COR016: H2 번역 JSON fixture 보정
### JSON-FIXTURE-T1
- Objective: 운영 코드와 기대값을 변경하지 않고 두 번역 회귀 테스트가 정상 JSON 객체를 읽어 통과한다.
- 시작 조건: 사용자 최소 수정 및 테스트 통과 요청. 이전 실행 제한은 이번 테스트 검증에 대해 해제한다.
- Files: `src/test/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepositoryLocalizedNameTest.kt`와 기존 PRD/계획/리뷰 기록. 검증 중 확인된 이전 래퍼 전환의 `src/main/kotlin/kr/co/vividnext/sodalive/api/home/HomeService.kt` 인자 줄바꿈 1곳만 추가 보정하며 운영 동작 변경은 없다.
- 실패 증거: 사용자 전체 실행 XML은 2271건/실패2/오류0/skip0이다. 해당 클래스는 6건 중 랭킹 JSON 문자열 역직렬화 예외와 시리즈 번역 제목 fallback 단언 실패 2건이다. 랭킹/시리즈 SQL의 제목 번역 JOIN은 유지되어 있다.
- 원인: 두 fixture의 JPA String converter→H2 JSON 적재가 JSON 객체 대신 문자열을 만든다. 기존 DefaultMainContentAllQueryRepositoryTest는 persist/flush 후 FORMAT JSON으로 실제 객체를 적재한다. 그 방식만 재사용한다.
- S1: 랭킹 raw/KO/JA 이름 모드 모두 요청한 번역 제목과 올바른 닉네임을 반환한다. 기존 실패 테스트의 실제 H2/JPA 조회로 증명한다.
- S2: 시리즈 raw 이름 모드에서도 번역 제목을 반환하며 기본 JA/KO 이름 조회가 유지된다. 기존 실패 테스트의 실제 H2/JPA 조회로 증명한다.
- S3: 같은 클래스의 기존 4개 회귀 및 전체 테스트가 통과하고 이번 수정으로 src/main의 변경이 없다. 테스트 수는 2271을 유지한다.
- 범위 밖: 운영 converter/JSON 저장 정책/DDL 수정, 테스트 삭제·단언 완화·skip, 새 helper 계층, 운영 DB/배포/커밋/푸시.
- [x] RED 증거: 사용자 실행 XML의 예외·단언·실제 SQL과 현재 코드를 대조하여 두 실패를 확인했다. 테스트 fixture 결함이므로 제품 기능 RED와 구분한다.
- [x] GREEN: 두 fixture만 최소 보정하고 `./gradlew test --tests 'kr.co.vividnext.sodalive.content.AudioContentRepositoryLocalizedNameTest' --offline --console=plain` exit 0, 22초, 6건/실패0/오류0/skip0을 확인했다. 두 실패 사례의 기존 단언이 모두 통과했다.
- [x] 회귀: 전체 테스트 2271건 통과. ktlint의 줄바꿈 실패는 아래 이력대로 최소 보정 후 전체 ktlintCheck 통과를 확인했다. COR014/COR015를 포함한 실행 검증이며 전체 명령 첫 시도의 종료 코드 1은 보존한다.
- [x] 종료: 결과 XML/명령 종료 코드/운영 동작 불변/테스트 프로세스 종료를 확인했다. LSP 도구 미제공은 Kotlin 컴파일·ktlint 결과와 구분한다.
- 구현: 테스트 파일의 두 위치에 각 6줄(설명 주석 포함)만 추가했다. 기존 JPA flush 후 `jdbcTemplate.update`의 bound `? format json`으로 해당 content/series ID 및 `ja` 행만 보정한 뒤 clear한다. 단언·테스트 수는 그대로다.
- 운영 코드 보존: 수정 전후 `git diff -- src/main | shasum -a 256` 값이 `334607606753fad8ffee5ef24936be1fcce154238730357ba9576970a5c75149`로 동일하다. 이번 수정에는 운영 코드 변경이 없다.
- focused 종료: 테스트용 HikariPool-1과 EntityManagerFactory shutdown을 확인했다. 전체 회귀·ktlint 및 독립 좁은 리뷰 결과는 대기 중이다.
- 전체 실행 1차: `./gradlew test ktlintCheck --offline --console=plain` 종료 코드 1. 부모가 XML 323 suites/2271 tests/실패0/오류0/skip0을 직접 합산했다. 테스트는 모두 통과했지만 같은 테스트 파일 276행의 argument-list-wrapping 3건으로 ktlint가 실패했다. 로그: `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/creatorname-cor016-full.log`(12분 1초). 성공으로 덮어쓰지 않는다.
- 후속 최소 보정: 위 호출의 인자 줄바꿈만 수정한다. 실행 의미가 동일하므로 전체 2271건을 다시 실행하지 않고 대상 클래스6건과 전체 ktlint를 재검증한다. 전체 회귀 통과 증거는 1차 XML 합산이며 후속 focused 결과와 구분한다.
- 독립 수정 리뷰: `ses_ec1576ac3ffesO9K10ayy0JKzr`가 scoped update/flush/clear/기대값 보존을 확인하여 APPROVE 판정했다. 기존 리뷰 세션 재개는 도구 Task not found로 실패해 새 좁은 리뷰로 대체했다.
- lint 2차: 테스트 파일 줄바꿈 보정 후 재검증에서 기존 HomeService 178행 argument-list-wrapping 1건이 드러났다. 첫 실행은 test lint 실패로 main check 결과 출력 전에 종료됐다. 이전 래퍼 전환에서 생긴 호출 형식만 줄바꿈하고 focused+전체 lint를 재확인한다. 운영 코드 불변은 의미·동작 기준이며 이 형식 수정은 예외로 명시한다.
- 최종 명령: `./gradlew test --tests 'kr.co.vividnext.sodalive.content.AudioContentRepositoryLocalizedNameTest' --tests 'kr.co.vividnext.sodalive.api.home.HomeServiceTest' ktlintCheck --continue --offline --console=plain` exit 0, BUILD SUCCESSFUL 40초. Kotlin 컴파일과 전체 ktlintCheck 통과, XML 2 suites/7 tests/실패0/오류0/skip0을 부모가 확인했다. 전체 2271건은 줄바꿈 보정 전 실행 결과이며 현재 XML의 focused 7건과 혼동하지 않는다.
- 운영 영향: 테스트 fixture 12줄과 테스트 호출 줄바꿈, HomeService 호출 줄바꿈 1곳만 이번에 보정했다. HomeService 파일의 공백 제거 SHA256은 전후 `cb99eb4501b1ab3e2bab002bc445d8a600dc07c71e5e61108e130777c2e46a89`로 동일하다. 운영 로직/converter/DB schema와 테스트 단언은 변경하지 않았다.
- 종료 증거: 부모 `pgrep -fl 'Gradle Test Executor|redis-server|GradleWrapperMain'` 결과 없음. focused Hikari/JPA shutdown 확인. `git diff --check` 지적 없음. 별도 운영 앱/DB 연결은 하지 않았다.
- 잔여 경고: 기존 HomeService 101행 불필요한 `!!` 컴파일 경고 및 Gradle 9 비호환 deprecation 경고는 범위 밖이라 수정하지 않았다. 실패나 테스트 skip은 아니다. 운영 MySQL/provider/실제 모바일 검증은 수행하지 않았으며 커밋·푸시도 없다.
## COR017: 미사용 탭 완전 제외
### UNUSED-TAB-T1
- Objective: 미사용 탭 7파일이 HEAD와 같아지고 해당 탭을 위한 특별 raw 모드 없이 실사용 조회의 언어별 이름·제목 번역이 유지된다.
- 시작 조건: 사용자 범위 명확화 및 수정 승인. COR015의 탭 원본 이름 보장 해석만 대체하며 이전 증거는 보존한다.
- Files (main 공통 prefix `src/main/kotlin/kr/co/vividnext/sodalive/`): `content/main/tab/{alarm/AudioContentMainTabAlarmService,asmr/AudioContentMainTabAsmrService,content/AudioContentMainTabContentService,free/AudioContentMainTabFreeService,home/AudioContentMainTabHomeService,replay/AudioContentMainTabLiveReplayService,series/AudioContentMainTabSeriesService}.kt`; `content/AudioContentRepository.kt`, `rank/RankingRepository.kt`, `rank/RankingService.kt`, `content/series/ContentSeriesRepository.kt`, `content/series/ContentSeriesService.kt`, `api/home/HomeService.kt`. Test prefix `src/test/kotlin/kr/co/vividnext/sodalive/`: `api/home/HomeServiceTest.kt`, `content/AudioContentRepositoryLocalizedNameTest.kt`.
- 범위: 탭7파일에서 lang=null 6곳/nameLang=null 7곳을 제거한다. findByTheme의 탭 전용 nullable 분기를 제거하고 기존 기본 KO/명시 Lang 조회로 단순화한다. 랭킹·시리즈의 이름 언어를 제목 locale과 별개로 raw 지정하던 nameLang 인자/전달을 제거한다. 랭킹 creator 원본 전용 null 분기도 더 이상 필요하지 않으면 제거한다. Home caller/기존 mock을 새 단일 계약에 맞춘다.
- 보존: 공유 메서드를 쓰는 AudioContentMainService/AudioContentThemeService/HomeService의 언어 전달과 활성 이름/원본 fallback/제목 번역, 다른 실제 raw 조회·캐시·관리자 경계, COR014/커뮤니티·팬톡/AudioContentCuration, API·DDL·converter 및 H2 FORMAT JSON fixture.
- 테스트: 미사용 탭 raw 모드 단언과 전용 호출 사례만 제거한다. 실사용 메서드의 KO/JA 이름 및 제목 번역 단언은 유지/통합한다. 테스트 개수를 줄이기 위해 유효한 회귀를 삭제하지 않으며 실제 케이스 증감은 결과에 기록한다.
- S1: `git diff HEAD -- content/main/tab`가 비어 있고 null 인자가 없다. 파일 자체 삭제나 API 삭제가 아니다.
- S2: 실제 H2/JPA 조회에서 KO/JA 별칭과 미등록 원본, 각 locale의 번역 제목/미등록 번역 fallback이 유지된다. HomeService mock/호출 일치도 검증한다.
- S3: 전체 회귀 및 ktlint 통과, 실제 HTTP/WS 테스트 자원 종료, 유지 범위에 회귀 없음. 신규 의존성·추상화·호환 wrapper는 추가하지 않는다.
- [x] 기존 GREEN 기준: COR016 전체2271 및 focused7/lint 통과 기록과 현재 테스트를 읽고 축소 계약을 테스트에 먼저 반영했다. 기능 추가가 아닌 제외 범위 정리로 새 실패 기능 테스트는 만들지 않았다.
- [x] 최소 구현: 위15파일에 한정해 탭 전용 코드를 제거하고 모든 실제 caller를 적응했다. 부모가 공유 경계·테스트를 직접 읽고 탭 HEAD diff 없음과 nameLang 잔여 참조 0건을 확인했다.
- [x] focused: `./gradlew test --tests 'kr.co.vividnext.sodalive.content.AudioContentRepositoryLocalizedNameTest' --tests 'kr.co.vividnext.sodalive.api.home.HomeServiceTest' --tests 'kr.co.vividnext.sodalive.osiv.OsivLazyLoadingRegressionTest' ktlintCheck --continue --offline --console=plain` 최종 exit 0, 21초, 3 suites/17 tests/실패0/오류0/skip0 및 전체 ktlintCheck 통과.
- [x] 최종: `./gradlew test ktlintCheck --continue --offline --console=plain` exit 0, 전체2271건/실패0/오류0/skip0 및 ktlintCheck 통과, 독립 APPROVE, 테스트 자원 종료와 탭7파일 HEAD diff 없음을 확인했다.
- 범위 밖: 미사용 API 자체 삭제, 다른 모듈 raw 정책 재설계, 관련 없는 정리, 운영 DB/배포/커밋/푸시. LSP 미제공은 Kotlin 컴파일/ktlint로 보완하되 동일 도구라고 표현하지 않는다.
### COR017 구현·focused 기록
- 탭7파일의 13개 인자 추가를 되돌렸고 HEAD와 동일하다. 공유 운영6파일·테스트2파일의 나머지 유효한 기능 변경은 유지했다.
- 공유 조회: findByTheme/getCreatorRankings의 불필요 nullable 언어 분기를 제거했다. 랭킹·시리즈의 별도 nameLang 인자 없이 기존 locale로 이름과 제목을 조회한다. 랭킹의 이전 locale=null 경계와 큐레이션 raw 조회는 유지한다. Home의 시리즈 호출·mock도 기존 5인자 형태로 맞췄다.
- 테스트: repository6/Home1 메서드 수는 유지하면서 미사용 탭의 null 조합·알람/curationV2 호출 검증만 제거했다. 실제 사용 메서드의 KO/JA 별칭·번역 제목·원본 제목 fallback과 큐레이션2경로 검증 및 FORMAT JSON 적재는 유지했다.
- focused 첫 실행: 17건 중 소유자-ID 테스트1건 실패. 이름조회 기능 단언이 아니라 회원과 콘텐츠의 독립 자동증가 ID가 우연히 11로 같아진 fixture 가정이 원인이다. 같은 테스트 파일에서 ID가 같은 경우 다음 콘텐츠를 생성해 다른 ID를 확보했으며 assertNotEquals와 실제 결과 단언은 유지했다. 이어 최종17건 모두 통과했다.
- 로그: `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/creatorname-cor017-focused.log`에 첫 실패와 후속 성공을 누적했다. Hikari/JPA 종료와 focused 후 테스트 프로세스 잔존 없음이 확인됐다.
- 현재: 전체 회귀와 독립 리뷰 진행 중. 이전 전체2271 통과 기록을 COR017 전체 검증으로 대체하지 않는다.
### COR017 최종 검증 완료
- 최종 명령: `./gradlew test ktlintCheck --continue --offline --console=plain`, exit 0, BUILD SUCCESSFUL 8분 36초. 로그 `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/creatorname-cor017-full.log`의 `COR017_FULL_EXIT_CODE=0`과 ktlintCheck 성공 상태를 확인했다.
- 부모가 최종 XML을 직접 합산하여 **323 suites/2271 tests/실패0/오류0/skip0**을 확인했다. 테스트 메서드 수는 유지되었고, 미사용 탭 전용 조합만 제거하면서 실사용 언어·제목 검증을 보존했다. 이번 결과는 COR017 최종 소스 기준이다.
- 실제 표면: 전체 suite의 기존 H2/JPA·HTTP/WS 테스트가 통과했다. 테스트용 JPA/Hikari shutdown과 `pgrep -fl 'Gradle Test Executor|redis-server|GradleWrapperMain'` 결과 없음을 확인했다. 부모의 localhost:52527 후속 curl은 exit 7로 연결되지 않아 테스트 서버가 종료됐음을 확인했다.
- 독립 리뷰: `ses_ec125e618ffej4zvXb6whEPb0z`가 **APPROVE** 판정했다. S1 탭 무변경, S2 실사용 Lang/locale 보존, S3 테스트 계약·ID fixture 보정이 단언을 약화하지 않음을 확인했다. 리뷰 시 남아 있던 전체 실행 결과 대기는 위 최종 통과로 해소됐다.
- 최종 범위: 탭7파일이 HEAD와 같아 변경 목록에서 빠졌다. 공유 운영6파일·테스트2파일은 필요한 다른 변경이 남는다. `git diff --check` 지적 없음. 운영 DB·배포·커밋·푸시는 수행하지 않았고 LSP는 미제공이며 Kotlin 컴파일·ktlint로 검증했다. 실제 운영 MySQL/provider/모바일 검증은 기존 외부 범위다.
## COR018: 원본 이름의 언어 중립성 명확화
### ORIGINAL-NAME-T1
- Objective: 운영 코드 변경 없이 임의 언어 원본과 요청 Lang이 독립임을 기존 테스트로 검증하고 잘못 읽힐 수 있는 설명을 바로잡는다.
- 시작 조건: 사용자 테스트5파일+기존문서2파일 범위 확정·구현 승인. COR017 전체2271건 통과가 변경 전 기준이다.
- Files: `src/test/kotlin/kr/co/vividnext/sodalive/v2/creatorname/application/CreatorLocalizedNameServiceTest.kt`, `v2/creatorname/adapter/out/persistence/CreatorNameJoinTest.kt`, `v2/api/content/all/adapter/in/web/MainContentAllEndToEndTest.kt`, `v2/api/home/HomeRecommendationControllerTest.kt`, `message/MessageServiceLocalizedNameTest.kt`(첫 파일 이후 동일 test root prefix). 문서는 이 PRD/계획2파일만 변경하며 새 리뷰 문서도 만들지 않고 이 절에 판정을 기록한다.
- S1: 원본 `하늘`/`さくら`/`Alice` 각각과 요청 KO/JA/EN을 독립적으로 검증한다. 활성 별칭이 있으면 등록값, 미등록·비활성이면 해당 원본을 그대로 반환하며 USER도 보존한다. 실제 service/JOIN H2 query와 기존 SQL 수/페이지/역할 단언으로 확인한다.
- S2: 별칭 해제 후 요청 언어와 관계없이 변경된 현재 원본으로 돌아간다. 원본과 요청 언어가 같아 보이는 경우에도 활성 수동 별칭이 우선한다. 기존 lifecycle/단건·다건 SQL/빈 입력 단언을 유지한다.
- S3: 전체 콘텐츠 HTTP 및 메시지 목록에서 일본어 원본이 KO/EN 별칭 미등록 시 그대로 나온다. 테스트 설명에 미등록 조건을 명시한다. 홈 추천은 기존 기대값을 바꾸지 않고 설명만 정정한다.
- TDD 적용: 이번에는 이미 정상인 동작의 테스트 보강이다. 운영 코드를 임시로 망가뜨리거나 실패 단언을 만들지 않는다. 기존 GREEN을 기준으로 fixture/시나리오를 먼저 보강하고 실제 DB·HTTP 경로의 통과를 확인한다. 가짜 RED 성공을 기록하지 않는다.
- 범위 밖: src/main 전체, DB schema/DDL/converter/운영 설정, 자동 언어 감지·원본 언어 metadata, 관리자 쓰기 언어 확대, 신규 테스트 파일, 다른 API별 조합 복제, 커밋/푸시/운영 DB/배포.
- [x] 기존 테스트 검토·fixture 설명 및 원본 문자열 조합 보강. 기존 매개변수3행을 원본 문자열3종으로 바꾸고 각 행에서 요청 Lang3종을 순회한다. 새 메서드·파일은 추가하지 않았다.
- [x] focused: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creatorname.application.CreatorLocalizedNameServiceTest' --tests 'kr.co.vividnext.sodalive.v2.creatorname.adapter.out.persistence.CreatorNameJoinTest' --tests 'kr.co.vividnext.sodalive.v2.api.content.all.adapter.in.web.MainContentAllEndToEndTest' --tests 'kr.co.vividnext.sodalive.v2.api.home.HomeRecommendationControllerTest' --tests 'kr.co.vividnext.sodalive.message.MessageServiceLocalizedNameTest' ktlintCheck --continue --offline --console=plain` exit0, 1분55초, 5 suites/58 tests/실패0/오류0/skip0, 전체 ktlintCheck 통과.
- [x] 최종: `./gradlew test ktlintCheck --continue --offline --console=plain` exit0, 전체2271건/실패0/오류0/skip0, ktlintCheck 통과, 독립 리뷰 승인 및 테스트 자원 종료를 확인했다. src/main 불변과 승인7파일 범위를 유지했고 실행 수 증감은0이다.
- LSP 도구 미제공은 Kotlin 컴파일/ktlint로 보완한다. 실제 운영 MySQL/provider/모바일은 실행하지 않는다.
### COR018 구현·focused 증거
- 공통 service/current-name, JOIN projection, sparse 조회의 기존3개 매개변수 테스트를 원본3종×요청Lang3종으로 재구성했다. 활성 KO/JA/EN 별칭과 미등록·비활성·USER fallback을 구분하고 SQL수/엔티티 로드/페이지/역할 단언을 유지했다. 매개변수 실행 수는 9건 그대로이고 내부 원본·언어 조합은9→27개다.
- 원본 변경·해제 테스트는 `さくら`→`Alice`로 원본 변경 후 JA 별칭 유지, 해제 후 모든 요청 Lang에서 현재 `Alice` 반환 및 동일 행 ID/이름값 보존·비활성을 확인한다.
- MainContentAll HTTP는 KO 별칭 미등록 시 `改名さくら`를 반환하며 JA 등록값을 우선한다. 메시지 voice 조회는 KO/EN 별칭 미등록 시 `さくら`, JA는 등록값을 확인한다. 홈 추천 테스트는 설명만 정정했다.
- 운영 불변: 수정 전후 `git diff HEAD -- src/main | shasum -a 256`은 `fdbd17dd0ad3a85b6c35cff6ce3f6f35add2d6afb7ea6ae3869c3638c63ba2a6`로 동일하다. 신규 production/테스트 파일이나 다른 문서 변경은 없다.
- focused 로그: `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/creatorname-cor018-focused.log`. 부모가5파일 수정과 XML58건을 직접 확인했다. 종료 과정의 기존 embedded Redis 재접속 warning 및 Gradle deprecation은 확인했지만 실패는 없다. 전체 회귀·독립 리뷰·최종 종료 검증은 진행 중이다.
### COR018 최종 실행·리뷰 결과
- 최종 명령 `./gradlew test ktlintCheck --continue --offline --console=plain`: exit0, BUILD SUCCESSFUL 9분17초. 로그 `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/creatorname-cor018-full.log`의 `COR018_FULL_EXIT_CODE=0`과 ktlintCheck 성공 상태를 확인했다.
- 부모가 최종 XML을 직접 합산하여 **323 suites/2271 tests/실패0/오류0/skip0**을 확인했다. 대상5클래스는 service13/JOIN7/전체콘텐츠HTTP7/홈추천28/message3으로 합계58건이다. 이전 대비 JUnit 실행 건수는0 증감이며 기존 매개변수 행의 내부 조합만 보강했다.
- 독립 리뷰 `ses_ec10286d4ffeJKLqqL8wzJ9MAH`: **APPROVE(static)**, 구체적 차단 사항 없음. 원본3종×요청3언어, 동일 문자권 원본의 별칭 우선, 미등록/비활성/USER fallback, 이름 변경·해제 후 현재 원본 및 SQL/페이지 단언을 확인했다. 검토 시 진행 중이던 전체 실행도 위 결과로 완료됐다. 승인 범위 준수를 위해 별도 리뷰 파일은 만들지 않았다.
- 운영 코드 보존: 최종 `git diff HEAD -- src/main`의 SHA256은 시작 시 값 `fdbd17dd0ad3a85b6c35cff6ce3f6f35add2d6afb7ea6ae3869c3638c63ba2a6`과 동일하다. 신규 테스트/운영 파일·API·schema·설정 변경은 없다. 미사용 탭은 계속 HEAD와 동일하다.
- 실제 표면·종료: 전체 suite에 기존 실제 JWT ADMIN HTTP4건과 localhost HTTP/WebSocket·Redis9건이 포함되어 통과했다. JPA/Hikari shutdown을 확인했고 부모 `pgrep -fl 'Gradle Test Executor|redis-server|GradleWrapperMain'` 결과 없음, localhost:53324 및53337 후속 curl은 모두 exit7로 종료된 서버에 연결되지 않았다. `git diff --check` 지적 없음.
- 한계: Kotlin 컴파일·ktlint로 검증했으며 LSP 도구는 미제공이다. 별도 build/assemble 패키징, 운영 DB/MySQL·외부 provider·실제 모바일 검증은 하지 않았다. Redis 종료 시 재접속 warning과 Gradle9 deprecation은 기존 경고이며 실패나 skip은 없다. 커밋·푸시·배포는 수행하지 않았다.
## COR019: 레거시 콘텐츠 메인 변경 롤백
### LEGACY-MAIN-T1
- Objective: 두 메인 파일이 HEAD와 같고 전용 조회2개의 이름 JOIN 추가분이 제거되며 공유 실사용 이름/번역과 기존 업무는 유지된다.
- 시작 조건: 사용자 조사 결과 확인 후 롤백 승인. COR018 전체2271건 통과 기준을 보존한다.
- Files: `src/main/kotlin/kr/co/vividnext/sodalive/content/main/AudioContentMainController.kt`, `AudioContentMainService.kt`(같은 directory), `src/main/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepository.kt`의 `findByThemeFor2Weeks`/`findAudioContentByCurationId` 선언·구현만. 기존 PRD/계획/inventory 상태도 갱신한다.
- 제거: controller 신규 이름서비스/LangContext 주입과 신규크리에이터·큐레이션 cache-hit 후 이름 batch/copy2곳; service 신규명시Lang전달2곳; repository 전용2메서드의 추가lang/CreatorNameJoin/projection 변경.
- 보존: service의 기존 LangContext와 제목·테마 번역, 인증/차단/성인 필터/캐시/순서/페이지, shared findByTheme·playlist·getLatestContentByTheme, core/v2 기능 및 테스트. 공유 조회의 제외API 간접 영향은 별도 raw 모드로 막지 않는다.
- 테스트 범위: 직접 전용 추가 테스트·constructor fixture는0건이다. 문서 T09 신규 예정 파일은 실제 생성되지 않았으므로 삭제할 파일이 아니다. AudioContentRepositoryLocalizedNameTest6건, MainContentAllEndToEndTest, HomeServiceTest 및 공통 batch 테스트는 유지한다.
- S1: 두 메인 파일 `git diff HEAD` 없음. 전용2repository메서드는 원래 시그니처·원본 projection이며 남은 caller는 컴파일된다.
- S2: 기존 shared 조회와 v2/홈/메시지 등의 현지화·번역 테스트가 통과한다. 기존 테스트 삭제·단언 완화·skip은 없다.
- S3: 실제 메인 HTTP 요청으로 cache 후처리 제거·전용query 원본 반환과 기존 응답 구조/인증을 확인하고, 테스트용 서버·DB·스크립트 등 임시 검증 자원을 종료한다. 직접 전용 테스트 부재를 기존 전체 테스트 통과로 대체하지 않는다. 임시 검증 코드는 작업 디렉터리 밖에 두며 영구 테스트 파일로 남기지 않는다.
- TDD 기준: 정상 기준에서 기능 추가분을 제거하는 롤백이며 새 제품 로직이나 가짜 실패 테스트는 추가하지 않는다. 기존 테스트와 실제 표면 검증으로 이전 업무 보존을 확인한다.
- [x] 지정3파일 최소 롤백·모든 caller 정적 대조 및 두파일HEAD일치 확인. repository 전용2메서드 선언·구현도 HEAD 형태로 복원했다.
- [x] focused: `./gradlew test --tests 'kr.co.vividnext.sodalive.content.AudioContentRepositoryLocalizedNameTest' --tests 'kr.co.vividnext.sodalive.api.home.HomeServiceTest' --tests 'kr.co.vividnext.sodalive.osiv.OsivLazyLoadingRegressionTest' ktlintCheck --continue --offline --console=plain` exit0, 33초, 3 suites/17 tests/실패0/오류0/skip0 및 전체 ktlintCheck 통과.
- [x] 실제 메인 HTTP smoke 및 임시 검증 자원 정리. 실제 JWT localhost HTTP 인증4요청200·무인증1요청401과 이름/DTO/제목 단언이 통과했다. 임시 소스·init·빈 디렉터리를 제거했고 서버/Redis 종료를 확인했다.
- [x] 최종 `./gradlew test ktlintCheck --continue --offline --console=plain` exit0, 전체2271건/실패0/오류0/skip0 및 lint 통과, 독립 리뷰 승인과 실제 HTTP/종료·범위 확인 완료.
- 범위 밖: 다른 조회의 현지화 롤백, 원본=KO 취급, 공개 API/schema/DDL 변경, 기존 업무 메서드 삭제, 커밋/푸시/운영 DB/배포. LSP는 미제공이며 compile/ktlint 검증과 구분한다.
### COR019 적용·focused 기록
- 두 메인 파일의 HEAD diff가 없음을 부모가 확인했다. service의 기존 LangContext·번역·캐시·필터는 그대로이며 repository에서는 지정2메서드만 이름 추가분을 제거했다. 테스트 파일·단언·메서드는 수정/삭제하지 않았다.
- focused 로그 `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/creatorname-cor019-focused.log`에 `COR019_FOCUSED_EXIT_CODE=0`과 BUILD SUCCESSFUL 33초를 확인했다. 부모가 XML17건을 직접 합산했다.
- 직접 메인 경로 테스트가 없으므로 저장소 밖 `opencode/cor019-http-smoke/`에 임시 Kotlin 테스트·Gradle init만 준비했다. 실제 RANDOM_PORT HTTP/JWT/H2를 사용하고 target service/repository를 mock하지 않는다. 원본 일본어+활성KO/JA 별칭 fixture로 4개경로와 무인증을 확인한다. 영구 테스트 추가가 아니며 실행 후 제거한다.
- 현재 HTTP smoke와 독립 읽기 전용 리뷰 진행 중. 이 완료 후 임시 소스를 제거하고 기존 전체 suite를 실행한다.
### COR019 실제 HTTP·리뷰 결과
- 임시 HTTP 명령: `./gradlew --no-daemon --offline --console=plain -I /var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/cor019-http-smoke/init.gradle test --tests kr.co.vividnext.sodalive.cor019smoke.Cor019RollbackHttpSmokeTest`, exit0, BUILD SUCCESSFUL39초, 임시1case/실패0/오류0/skip0. 로그 `opencode/creatorname-cor019-http-smoke.log`에 요청/응답 및 `GRADLE_EXIT_CODE=0`을 보존했다.
- 실제 관측: JA 헤더+USER JWT의 `/audio-content/main/new-content-upload-creator`, `/curation-list`, `/new/all`은 원본 `原名花子`, `/new`는 의도된 shared 기본KO 별칭 `한국어 별칭`을 반환했다. 인증 응답은 모두200/success=true, 항목수·ID·DTO·콘텐츠 제목을 확인했다. 무인증 신규크리에이터 요청은401이었다. 원본 언어를 KO로 가정하지 않았다.
- 종료: Tomcat54687/Hikari/JPA 종료, Redis54683·HTTP54687 listener 없음, 부모 pgrep 관련프로세스없음 및 HTTP54687 curl exit7을 확인했다. 임시 Kotlin/init을 apply_patch로 삭제하고 빈 src/parent 디렉터리를 rmdir했다. 영구 테스트 추가·삭제·수정은 없다. 외부 로그만 증거로 남긴다.
- 독립 리뷰 `ses_ec0bfd190ffeJDgOdPfIjH1xf2`: **APPROVE(static)**, 차단사항 없음. 두파일HEAD일치, 전용2쿼리·callers, 기존번역/업무·공유조회/테스트보존을 확인했다. 메인 `/new`의 sharedKO 간접 동작은 승인된 범위이며 새raw모드로 바꾸지 않았다.
- 현재: 임시 init 없이 정상 전체 suite 실행 중. 최종 XML에 임시 smoke가 포함되지 않고2271건을 유지하는지도 확인한다.
### COR019 최종 완료 기록
- 일반 전체 명령 `./gradlew test ktlintCheck --continue --offline --console=plain`(-I 없음): exit0, BUILD SUCCESSFUL7분47초. 로그 `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/creatorname-cor019-full.log`의 `COR019_FULL_EXIT_CODE=0`과 ktlintCheck 성공을 확인했다.
- 부모가 최신 XML을 직접 합산해 **323 suites/2271 tests/실패0/오류0/skip0**을 확인했다. 기존 테스트 파일·단언·메서드를 추가/삭제/수정하지 않았다. 임시 Cor019RollbackHttpSmokeTest는 최종 XML/컴파일 클래스에 없고 외부 소스/init도 제거됐다.
- 최종 범위: 두 메인 파일 `git diff HEAD --exit-code` 출력 없음/exit0. AudioContentRepository의 전용2메서드 추가분만 롤백했고 다른 shared 조회는 유지한다. 운영 코드3파일 중2개는 변경 목록에서 빠지고1개는 다른 기능 diff가 남는다.
- 실제 HTTP5요청 결과와 independent APPROVE(static)의 실행 Gate는 모두 충족됐다. 전체 suite에 기존 ADMIN HTTP4건 및 HTTP/WS9건도 포함되어 통과했다. 서버·JPA/Hikari 종료를 확인했고, 부모 pgrep 관련프로세스없음 및 localhost:55007 curl exit7을 확인했다. 실행자도 HTTP54995·배포테스트 포트·이전 smoke 포트 종료를 확인했다.
- `git diff --check` 지적 없음. LSP 미제공은 Kotlin 컴파일/ktlint로 보완했다. 별도 패키징·운영 MySQL/외부 provider/실제 모바일 검증·운영 DB 적용·배포·커밋·푸시는 하지 않았다. 기존 Gradle9 deprecation 경고는 남아 있으나 실행 실패나 skip은 없다.
## COR020: 캐릭터 댓글 변경 롤백
### CHARACTER-COMMENT-TRIM-T1
- Objective: 두 운영 파일은 HEAD와 같고 캐릭터 댓글 이름 대체 테스트1개만 제거되며 나머지 기능과 검증은 유지된다.
- 시작 조건: 사용자 두 운영파일 롤백 및 해당 테스트 삭제 승인. 이전 전체2271건 통과 기준이다.
- Files: `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/comment/CharacterCommentService.kt`, `CharacterCommentRepository.kt`(동일 directory); `src/test/kotlin/kr/co/vividnext/sodalive/v2/creatorname/application/LegacyCurrentNameReadIntegrationTest.kt`의 지정1메서드·전용import/주입. 기존 PRD/계획/inventory 상태를 함께 갱신한다.
- 제거: 서비스 신규 LangContext와 표시이름 인자·tuple 조립 및 findDisplayComments/Replies/Comment 호출; repository 신규 QueryDSL 확장/interface/impl/JOIN. 기존 derived query와 원본 nickname 조립으로 복원한다. 지정 테스트의 ChatCharacter/CharacterComment/CharacterCommentService import와 characterCommentService 주입도 사용처가 없어지면 제거한다.
- 보존: addComment/addReply/delete/report·검증·이벤트·PageRequest/커서·건수, 기존 controller와 배너테스트2개, 통합테스트의 나머지8개·공통 helper·LangContext mock, 다른 core/HTTP/전달 이름 계약. 테스트 파일 전체는 삭제하지 않는다.
- S1: 두 운영 파일 git diff HEAD 없음, 제거한 query메서드 caller 잔존 없음, 컴파일 성공.
- S2: 통합테스트9→8개 및 캐릭터 controller 배너2개·관련 회귀 통과. 전체 실행 수는 다른 변경이 없으면2271→2270이며 실제 XML로 확인한다.
- S3: 작업디렉터리 밖 임시 검증으로 실제 댓글/답글 HTTP 및 최신댓글 조회에서 활성별칭이 있어도 저장된 원본을 확인한다. 본문·ID·건수와 인증/권한은 기존 계약대로 유지한다. 검증용 파일/프로세스/포트는 제거하고 로그만 남긴다. 영구 테스트는 추가하지 않는다.
- TDD 기준: 제거되는 기능의 passing 전용테스트를 승인에 따라 삭제하는 롤백이다. 다른 테스트 단언을 완화하거나 제품 코드를 임시로 망가뜨리는 RED는 만들지 않는다. 정상 기준과 실제 표면/회귀로 보존 범위를 확인한다.
- [x] 운영2파일 및 테스트1개 최소 롤백, 전용fixture 정리·모든 참조·HEAD일치 확인. 두 운영 파일 diff는 비어 있고 지정 테스트1개·전용import3개·주입1개만 제거했다.
- [x] focused: `./gradlew test --tests 'kr.co.vividnext.sodalive.v2.creatorname.application.LegacyCurrentNameReadIntegrationTest' --tests 'kr.co.vividnext.sodalive.chat.character.controller.ChatCharacterControllerTest' ktlintCheck --continue --offline --console=plain` exit0, BUILD SUCCESSFUL2분19초. 2 suites/10 tests(통합8+배너2)/실패0/오류0/skip0 및 전체 lint 통과.
- [x] 임시 댓글 HTTP/조회 smoke 및 검증 자원 정리. 실제 인증7요청200·비인증3요청401과 원본이름/본문/ID/건수/커서/최신댓글 단언 통과. 임시Kotlin/init/디렉터리 제거 및 서버/Redis 종료 확인.
- [x] 최종 `./gradlew test ktlintCheck --continue --offline --console=plain` exit0, 전체2270건/실패0/오류0/skip0 및 lint통과, 독립리뷰승인과 실제HTTP·임시case미포함·자원종료 확인.
- 범위 밖: 다른 캐릭터/이름 기능 변경, 신규 영구 테스트·설정·DDL·API, 커밋/푸시/운영 DB/배포. LSP 미제공은 Kotlin 컴파일·ktlint로 보완한다.
### COR020 적용·focused 기록
- 부모가 두 운영 파일 HEAD diff 없음, 제거된 custom query 참조 없음, 통합테스트 지정1개만 제거 및 나머지8개 유지 상태를 확인했다. 기존 배너테스트2개는 수정하지 않았다.
- 로그 `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/creatorname-cor020-focused.log`의 `GRADLE_EXIT_CODE=0`과 부모 XML10건 집계로 focused 통과를 확인했다. Hikari/JPA 종료 및 Redis56640 LISTEN 해제를 실행자가 확인했다.
- 현재: 임시 실제 댓글 HTTP 검증 준비와 독립 읽기 전용 리뷰 진행 중. 전체2270건 검증은 아직 미완료다.
### COR020 HTTP·독립 리뷰 증거
- 임시 명령은 저장소 밖 cor020-comment-smoke의 Kotlin 테스트 소스를 Gradle init으로만 추가해 실제 RANDOM_PORT/JWT/H2 경로를 실행했다. 첫 실행 exit0, BUILD SUCCESSFUL59초, 임시1case/실패0/오류0/skip0. 외부 로그 `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/creatorname-cor020-http-smoke.log`에 실제응답·cleanup·종료증거를 보존했다.
- 실제 HTTP: `/api/chat/character/1/comments`, `/comments/1/replies`의 페이지/빈끝페이지, 상세 `/api/chat/character/1`을 JA로 요청해 인증7개200/success=true와 비인증3개401을 확인했다. 활성JA/KO 별칭이 있어도 CREATOR는 `原名花子`, USER는 기존 이름이다. totalCount3/replyCount2, 답글original.replyCount0, 최신원댓글ID1과본문/커서를 확인했다. 최신댓글도 서비스 mock이 아닌 상세HTTP 응답으로 확인했다.
- 종료: fixture삭제수 답글2/원댓글1/캐릭터1/별칭2/선호1/회원2 및 token없음을 단언했다. Tomcat/JPA/Hikari종료, HTTP56749/Redis56747 listener없음, 부모 pgrep관련프로세스없음과 HTTPcurl exit7을 확인했다. 부모가 임시Kotlin/init을 apply_patch로 삭제하고 빈디렉터리를 제거했다. 정상 전체컴파일 후 임시case미포함도 확인할 예정이다.
- 독립 리뷰 `ses_ebd7fff7fffeYkwJ53C2vUyJ72`: APPROVE(static), 차단문제없음. 두운영파일HEAD일치와기존업무보존·지정테스트1개제거·다른8개및배너2개유지 확인. 전체실행Gate만대기다.
- 관측된 기존 테스트 환경 한계: 스모크 시작 시 H2 background scheduler의 SKIP LOCKED SQL 호환 오류와 종료시 scheduler/Lettuce warning이 기록됐다. 원래코드 변경 없이 실제요청/단언/자원종료는 통과했으며 운영 MySQL 동작 검증으로 간주하지 않는다.
- 현재: 임시init 없이 전체suite 실행 중이며 예상2270건 및 임시컴파일class/XML미포함을 확인한다.
### COR020 최종 완료 기록
- 최종 일반 명령 `./gradlew test ktlintCheck --continue --offline --console=plain`(-I 없음): exit0, BUILD SUCCESSFUL8분49초. 로그 `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/creatorname-cor020-full.log`의 `GRADLE_EXIT_CODE=0`과 ktlintCheck 성공 상태를 확인했다.
- 부모 XML 직접 합산: **323 suites/2270 tests/실패0/오류0/skip0**. 대상 통합테스트8·배너2 유지, 제거한 전용1건에 따라 전체2271→2270이다. 임시 smoke는 최종 XML/컴파일 클래스에 없고 소스/init도 제거됐다.
- 부모가 두 운영 파일 HEAD diff 없음과 삭제된 확장의 caller 없음, 실제 댓글/답글/상세HTTP 단언과 독립 APPROVE를 확인했다. 기존 댓글 업무와 나머지8통합테스트/2배너테스트는 보존했다.
- 자원 종료: JPA/Hikari 종료, 부모 pgrep 테스트executor/Redis/wrapper 없음 및 localhost57097 curl exit7. 실행자는 HTTP57085·deployment포트·Redis56822 종료도 확인했다. 무관한 기존 daemon/JVM은 종료하지 않았다.
- 전체 git diff --check 지적 없음. 별도 패키징·운영DB/배포·커밋·푸시·실제모바일/provider검증은 하지 않았다. 기존 Redis 종료재접속/Gradle deprecation 경고는 남았지만 테스트실패나skip은 없다.
- COR020 작업은 여기서 종료한다. 후속 ContentSeriesRepository/Service는 사용자 요청 순서대로 지금부터 영향 조사만 수행하며 구현 범위를 자동 확대하지 않는다.
## COR021: 시리즈 이름 변경 롤백
### SERIES-NAME-TRIM-T1
- Objective: 시리즈 운영2파일이 HEAD와 같고 이름 현지화 테스트 조각만 제거되며 제목 번역과 다른 기능 검증은 유지된다.
- 시작 조건: 후속 조사 후 사용자 "말한대로 진행하자" 승인. COR020 전체2270건 통과 기준이다.
- Files: `src/main/kotlin/kr/co/vividnext/sodalive/content/series/ContentSeriesRepository.kt`, `ContentSeriesService.kt`(동일 directory); `src/test/kotlin/kr/co/vividnext/sodalive/content/AudioContentRepositoryLocalizedNameTest.kt`의 시리즈1case. 기존 PRD/계획/inventory 상태 갱신.
- 제거: 저장소 getSeriesListV2/getSeriesByGenreListV2/getRecommendSeriesListV2/getOriginalAudioDramaList/getDayOfWeekSeriesListV2의 CreatorNameJoin·별칭projection/tuple추출·추가groupBy와 전용import2개. 서비스 CreatorLocalizedNameService import/주입 및 getSeriesDetail.nickname batch. 메서드/API/클래스 자체는 삭제하지 않는다.
- 보존: 기존 LangContext/Lang, seriesTranslation/genre/day표시/콘텐츠 번역, 성인/차단/권한/순서/count/page, 공통CreatorNameJoin/이름서비스 및 v2/관리자 독립조회, Home/Explorer의 service 호출·mock. 시리즈 작성자 이름이 해당 재사용 응답에서 원본으로 돌아가는 것은 승인 범위다.
- 테스트: shouldLocalizeOriginalSeriesNamesAndTitlesByRequestedLocale의 이름별칭2등록·별칭기대값·nickname단언만 제거하고 제목 검증으로 명명한다. JA 번역 제목/KO 미등록 원문제목 단언과 실제repository호출·FMT JSON fixture는 유지한다. 별칭helper의 암묵flush에 의존하지 않도록 JSON update 전 explicit flush를 보존한다. 테스트6개 및 전체2270개 수는 유지할 계획이다.
- S1: 두 운영 파일 HEAD diff 없음 및 public signature/caller 컴파일 성공.
- S2: 실제 H2 조회에서 JA 제목 번역과 KO 원문 fallback, 다른5개 repository 테스트 및 Home/SeriesMain/OSIV/독립v2 검증 통과. 이름부분만 삭제하며 제목단언을 완화하지 않는다.
- S3: 저장소 밖 임시 실제HTTP/JPA smoke로 활성별칭이 있어도 시리즈 작성자 원본을 반환하고 제목/페이지/권한을 유지함을 확인한다. 관련 서비스의 목록5경로·상세를 가능한 공통fixture로 확인하고, 별도v2는 기존테스트로 보존한다. 임시파일·서버·Redis를 정리한 뒤 일반전체suite에 임시case가 남지 않게 한다.
- [x] 지정 운영2파일 롤백 및 기존시리즈테스트 부분정리, 모든caller·HEAD일치 대조. 부모가 두파일HEADdiff없음과 제목단언/explicitflush/FMT JSON 보존을 확인했다.
- [x] focused: `./gradlew test --tests 'kr.co.vividnext.sodalive.content.AudioContentRepositoryLocalizedNameTest' --tests 'kr.co.vividnext.sodalive.api.home.HomeServiceTest' --tests 'kr.co.vividnext.sodalive.content.series.main.SeriesMainControllerTest' --tests 'kr.co.vividnext.sodalive.osiv.OsivLazyLoadingRegressionTest' ktlintCheck --continue --offline --console=plain` exit0, BUILD SUCCESSFUL52초, 4 suites/18 tests/실패0/오류0/skip0 및 lint통과.
- [x] 임시 시리즈 실제표면 검증 및 자원 정리. 목록5쿼리·상세·Home의 실제HTTP에서 원본작성자/번역/페이지/인증을 확인했다. 임시4파일과빈디렉터리를 제거하고 서버/Redis종료를 확인했다.
- [x] 최종 `./gradlew test ktlintCheck --continue --offline --console=plain` exit0, 전체2270건/실패0/오류0/skip0 및 lint통과, 독립리뷰승인·실제HTTP·임시case제외·자원종료 확인 완료.
- TDD 적용: 기존 GREEN에서 승인된 이름 기능만 제거하는 롤백이다. 나머지 유효한 테스트는 유지하며 새 제품로직·가짜RED·영구테스트파일은 추가하지 않는다.
- 범위 밖: 다른 서비스/별도v2의 현지화 제거, 제목번역삭제, API/DTO/schema/DDL 변경, 신규호환wrapper/raw모드, 커밋/푸시/운영DB/배포. LSP는 미제공이며 compile/ktlint 검증과 구분한다.
### COR021 적용·focused 기록
- 시리즈 운영2파일을 HEAD로 복원했다. 이름 처리 목록5곳/상세1곳과 전용import/주입만 제거됐으며 기존제목/장르/요일번역과 호출시그니처는 그대로다.
- 시리즈 테스트는 shouldLocalizeOriginalSeriesTitlesByRequestedLocale로 정정하고 별칭등록2곳/이름기대값/닉네임단언만 제거했다. JA번역제목·KO번역미등록원문fallback, FMT JSON 및명시flush를 유지했다. 다른5개테스트·공통import/주입은 변하지 않았고 테스트6건수도 유지됐다.
- 로그 `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/creatorname-cor021-focused.log`의 exit0 및 부모XML18건 집계로 검증했다. 운영2파일gitdiffHEAD없음, 범위밖변경없음, focused테스트자원종료를 확인했다.
- 현재 저장소 밖 cor021-series-smoke의 실제 HTTP/JPA 검증과 독립리뷰 진행 중. 임시하네스는 결과/응답/원본목록 검증방식을 기록하고 실행 후 삭제한다. Home비성공시 실제service/JPA 보완은 HTTP로 오인하지 않고 실패원인을 별도 명시한다.
### COR021 실제 HTTP·리뷰 결과
- 외부 임시init을 사용하는 실제 RANDOM_PORT/JWT/H2 smoke는 첫 실행 exit0, BUILD SUCCESSFUL40초, 임시1case/실패0/오류0/skip0이었다. 로그 `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/creatorname-cor021-http-smoke.log`에 `COR021_SMOKE_EXIT_CODE=0` 및 응답·runtime manifest·자원종료증거를 보존했다.
- 실제 인증HTTP9요청은 모두200/success=true, 무인증1요청은401이었다. 목록/추천/장르/요일/상세·Home 오리지널 목록에서 활성KO/JA 별칭 대신 원본 `原名花子`를 반환했다. JA 제목·장르·요일 번역, 상세 raw title+translated.title, KO 제목미등록fallback, 빈페이지/count·ID/콘텐츠수를 확인했다.
- 원본목록 검증방식은 `originalCoverage=HTTP /api/home`이며 서비스/JPA 보완 분기는 사용하지 않았다. 기존 전체 테스트나 mock을 직접HTTP 증거로 대신하지 않았다.
- fixture한정 삭제건수 `[1,1,1,1,1,1,1,1,1,2,1,2]` 및 viewer token없음, Tomcat/JPA/Hikari 종료를 확인했다. HTTP57562/Redis57556 listener없음과 부모curl57562 exit7/pgrep관련프로세스없음 확인 후, 부모가 임시Kotlin/init/manifest/runtime-manifest4파일과 빈디렉터리를 제거했다. 로그만 보존한다.
- 독립 리뷰 `ses_ebd52bd19ffeXYnsWluRcQAoJm`: APPROVE(static), 차단사항없음. 운영2파일HEAD일치·제목/업무보존·이름단언만제거·명시flush/FMT JSON 보존을 확인했다. 전체실행Gate만대기다.
- 관측사항: 기존 H2 스케줄러 SKIP LOCKED 오류와 Lettuce 종료경고가 있었지만 대상HTTP와자원종료는통과했다. H2 스모크 상세 에피소드 releaseDate가 `%2026.10.15`로 표시된 것도 관측했으며 이번 이름 롤백과 무관한 표시 처리라 수정하지 않았다. 운영환경에서 같은지는 미검증이다.
- 현재: 기존 실행자 재개가 도구 Task not found로 실패해 새 검증 실행자를 지정했다. 임시init 없이 정상 전체suite2270건 및 임시case미포함을 검증 중이다.
### COR021 최종 완료 기록
- 최종 일반 명령 `./gradlew test ktlintCheck --continue --offline --console=plain`(-I 없음): exit0, BUILD SUCCESSFUL11분8초. 로그 `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/creatorname-cor021-full.log`의 `COR021_GRADLE_EXIT=0`과 ktlintCheck 성공 상태를 확인했다.
- 부모가 최신 XML을 직접 합산하여 **323 suites/2270 tests/실패0/오류0/skip0**을 확인했다. repository6/Home1/SeriesMain1/OSIV10 유지, 이름단언만 정리하고 제목검증을 보존했으므로 전체 테스트 실행 수 증감은0이다.
- 부모가 두 시리즈 운영 파일 HEAD diff 없음과 임시 smoke 클래스/XML 미포함을 확인했다. 임시 소스/init/manifest/runtime 파일 및 디렉터리는 앞서 제거했다. 실제 시리즈5조회·상세·Home HTTP와 독립 APPROVE의 runtime Gate도 충족됐다.
- 최종 자원 확인: JPA/Hikari shutdown, 부모 pgrep 테스트executor/Redis/wrapper 없음 및 localhost57976 curl exit7. 실행자는 이전smoke57562/57556와 Redis57654도 종료됐음을 확인했다. 무관한 daemon/JVM은 유지했다.
- 전체 git diff --check 지적 없음. LSP 미제공은 Kotlin 컴파일/ktlint로 보완했다. 별도패키징·운영MySQL/provider/실제모바일 검증과 커밋·푸시·운영DB·배포는 하지 않았다. 기존 Redis 종료재접속/Gradle deprecation 및 앞서 기록한 H2 스모크 관측사항은 이번 이름 롤백 범위 밖으로 유지한다.
## COR022: 레거시 메시지 이름 변경 전체 롤백
### MESSAGE-NAME-TRIM-T1
- Objective: 레거시 메시지2파일이 HEAD와 같고 전용 projection/테스트파일만 제거되며 기존 메시지업무 및 공통FCM/v2 DM은 유지된다.
- 시작 조건: 사용자 조사 결과대로 전체롤백 승인. COR021 전체2270건 통과 기준이다.
- Files: `src/main/kotlin/kr/co/vividnext/sodalive/message/MessageService.kt`, `MessageRepository.kt`, `MessageListRow.kt`(동일 directory, 마지막은삭제); `src/test/kotlin/kr/co/vividnext/sodalive/message/MessageServiceLocalizedNameTest.kt` 전체삭제. 기존PRD/계획/delivery inventory 갱신.
- 제거: 서비스6목록의 신규Lang전달/MessageListRow조립2곳, 차단오류별칭조회2곳, 추가이름서비스주입/import, 발송FCM nameArgumentMemberIds2곳. repository6목록의 lang/row반환 및 공통sender/recipient별칭JOIN/projection을 기존 List<Message> 조회로 복원한다. 해당2파일 외 사용처 없는 MessageListRow 전체와 전용테스트3건만 삭제한다.
- 보존: MessageController·API·DTO·Message entity·count/page/filter·발송·보관·삭제·권한·본문/URL/시간/문구번역, 기존 LangContext, FCM senderMemberId, 공통이름서비스/JOIN/FCM/알림snapshot 및 v2 UserCreatorChat/WS 코드·테스트. 쿼리scalar 최적화도 롤백되므로 삭제테스트의 SQL12회/엔티티로드0 계약은 유지 대상이 아니다.
- 푸시 경계: 전체HEAD복원은 이름인자 매핑만 제거한다. 공통 FcmEvent.sendPush는 senderMemberId로 별칭을 여전히 조회하여 localizedSenderNickname을 알림저장에 전달할 수 있다. 본문원본과 snapshot별칭의 구분을 명시하고 공통sender 처리까지 임의 제거하지 않는다.
- S1: 두운영파일 git diff HEAD 없음, MessageListRow와삭제테스트파일/잔여참조없음, 전체컴파일성공. 기존API/업무메서드는 삭제하지 않는다.
- S2: 공통FCM/알림/v2 DM/WS focused·전체회귀 통과. 정상전체2270→2267 예상이며 실제XML로확인한다.
- S3: 저장소밖 임시 실제HTTP/JPA 검증으로 문자/음성6목록의원본이름·본문/URL/건수/인증과 발송이벤트 메타데이터 복원을 확인한다. 대상service/repository를 mock하지 않으며 외부 S3/FCM 전송만 격리할 수 있다. 임시파일/서버/Redis는 제거하고 기존suite에 임시case를 남기지 않는다.
- [x] 지정4파일 최소롤백/삭제 및전체caller·잔여참조·HEAD대조. 부모가 두 운영파일HEAD일치와src의MessageListRow/삭제테스트 참조없음을 확인했다.
- [x] focused: `./gradlew test --tests 'kr.co.vividnext.sodalive.fcm.FcmSendListenerLocalizedNameTest' --tests 'kr.co.vividnext.sodalive.fcm.notification.PushNotificationServiceTest' --tests 'kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatServiceTest' --tests 'kr.co.vividnext.sodalive.v2.usercreatorchat.UserCreatorChatNameJoinRepositoryTest' ktlintCheck --continue --offline --console=plain` exit0, BUILD SUCCESSFUL1분31초, 4 suites/33 tests/실패0/오류0/skip0 및lint통과.
- [x] 임시 실제메시지HTTP/발송계약 검증 및자원정리. 실제HTTP23요청과2발송이벤트단언 통과, fixture/JWT/서버/Redis종료와임시3파일·빈디렉터리제거 확인.
- [x] 최종 `./gradlew test ktlintCheck --continue --offline --console=plain` exit0, 전체2267건/실패0/오류0/skip0·lint통과, 독립리뷰승인과 실제HTTP/이벤트·임시case제외·자원종료 확인완료.
- TDD 적용: 승인된 현지화 기능/최적화를 제거하는 롤백이며 전용passing테스트3건도 범위와 함께 제거한다. 다른 테스트를 약화/skip하거나 신규제품로직/가짜RED를 추가하지 않는다.
- 범위 밖: 레거시메시지 API나업무자체삭제, 공통FCM/DM/WS롤백, 신규호환flags/rawmode, schema/DDL/운영설정변경, 커밋/푸시/운영DB/배포. LSP미제공은 Kotlin컴파일/ktlint와구분한다.
### COR022 적용·focused 기록
- MessageService/Repository는 HEAD와동일하며 MessageListRow16줄파일과 MessageServiceLocalizedNameTest185줄/3case파일을 삭제했다. senderMemberId·기존LangContext·API/업무는유지하고 추가nameArgumentMemberIds만 제거했다. 다른코드·테스트·설정·DDL은변경하지 않았다.
- 로그 `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/creatorname-cor022-focused.log`의 실제exit0과 부모XML33건집계로 focused통과를 확인했다. 실행자가 JPA/Hikari와 테스트executor/wrapper/Redis종료·Redis58314 listener없음을 확인했다. 기존Kotlin·Redis종료·Gradle경고는범위밖으로 유지했다.
- 현재 외부임시cor022-message-smoke의 실제JWT HTTP/JPA 검증과독립읽기전용리뷰진행중이다. 임시하네스는 S3업로드만 대체하고 실제sender/recipient목록·keep·차단오류 및 EventListener로 수집한2발송이벤트의 원본args/빈이름매핑/senderID를 확인하도록 작성했다. 공통FCM snapshot이 원본이라는 잘못된단언은 추가하지 않았다. 영구테스트파일은 추가하지 않는다.
### COR022 실제 HTTP·리뷰 기록
- 임시클래스는 외부 Gradle init으로만 추가했다. compileTestKotlin exit0/12초 후 실제HTTPsmoke 첫 실행도 exit0/33초, 임시1case/실패0/오류0/skip0이었다. 로그 `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/creatorname-cor022-http-smoke.log`에 actual응답·이벤트·XML·자원종료증거를 보존했다.
- 실제HTTP23요청: 6개목록 양방향12GET, 문자/음성2전송, 정상keep4건 및발신자keep거부·중복keep거부·차단후text/voice거부, 무인증401을 확인했다. 200응답22건에는 기존업무오류의 success=false 응답도 포함된다. CREATOR원본 `星野原名`, USER이름·본문·URL·날짜·DTO·건수와기존text isKept=false 계약을 유지했다.
- EventListener로 실제발행2이벤트의 args=[星野原名], nameArgumentMemberIds={}, senderMemberId=1 및메시지ID를 검증했다. 음성업로드는S3외부경계만mock했고 대상서비스/저장소/publisher는실제코드다. push토큰을만들지않아실제외부FCM전송이나legacy알림snapshot저장을검증한것은아니다. 공통snapshot은기존회귀로유지한다.
- 종료: 소유fixture/token삭제단언, Tomcat/JPA/Hikari종료, HTTP58366·Redis58363 listener/프로세스없음, 부모curl58366 exit7과pgrep없음확인. 임시Kotlin/init/README3파일과빈디렉터리를삭제했으며로그만남겼다. 별도report경로override는실행전제거해정상build경로를사용했다.
- 독립리뷰 `ses_ebd2ae7baffetOadUlZZicZIPD`: APPROVE, 정적차단문제없음. 2파일HEAD일치·전용파일/참조삭제·기존업무/공통FCM/v2보존 및 승인된본문/snapshot구분을 확인했다.
- 기존테스트환경의 H2 SKIP LOCKED scheduler오류와종료thread경고는관측했지만메시지단언은통과하고자원종료도확인했다. 운영환경에서같은문제라는뜻은아니며이번범위에서는수정하지않았다. 현재임시init없는정상전체2267건회귀실행중이다.
### COR022 최종 완료 기록
- 일반전체명령 `./gradlew test ktlintCheck --continue --offline --console=plain`(-I 없음): exit0, BUILD SUCCESSFUL7분55초. 로그 `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/creatorname-cor022-full.log`의 `COR022_GRADLE_EXIT=0`과lint성공상태를 확인했다.
- 부모가 최신XML을 직접 합산하여 **322 suites/2267 tests/실패0/오류0/skip0**을 확인했다. 기존2270건에서 승인된전용3건만제거됐다. 삭제테스트·임시smoke XML/HTML/컴파일클래스는없다. 컴파일출력의 빈cor022smoke디렉터리도 부모가 읽어0entries임을확인한뒤rmdir로제거했다.
- 부모가 MessageService/Repository HEAD diff없음 및src삭제심볼참조0건을 확인했다. 목록·발송·보관·삭제API와원본이름·차단문구번역·기존senderMemberId를보존한다. 공통FCM/v2DM/WS및관련회귀가통과했고이전판정의runtimeGate도충족했다.
- 종료확인: JPA/Hikari종료, 부모pgrep테스트executor/Redis/wrapper없음과localhost58709 curl exit7. 실행자는HTTP58697 및Redis58438도닫혔음을확인했다. 이전HTTPsmoke자원은별도정리기록대로종료됐다.
- 전체gitdiffcheck지적없음. 전체로그ERROR행0, 기존Redis종료재접속/Gradle9 deprecation경고는남았지만테스트실패나skip은없다. LSP미제공은Kotlin컴파일/ktlint로보완했다.
- 실제외부S3/FCM전송·운영MySQL·모바일·별도패키징·운영DB적용·배포·커밋·푸시는수행하지않았다. 푸시본문raw인자/공통senderSnapshot별칭가능경계는승인대로유지한다. 작업은여기서종료한다.
### 커밋 전 검증, 2026-10-16
- 사용자 `/commit-with-validation` 요청에 따라 기존 구현 변경과 문서를 커밋 대상으로 확인했다. 이전 커밋 미수행 기록은 당시 이력으로 보존한다.
- 여러 도메인의 변경과 패키징을 확인하기 위해 `./gradlew test build ktlintCheck --offline --console=plain`을 실행했다. exit 0, `BUILD SUCCESSFUL in 1m 7s`, bootJar 및 build 성공이다.
- test와 ktlintCheck는 Gradle의 `UP-TO-DATE` 판정으로 기존 결과를 재사용했다. 테스트 본문을 새로 실행한 것은 아니며 기존 HTML 보고서의 2267 tests / failures 0 / ignored 0을 확인했다.
- `git diff --check` 통과. 추적 파일 추가분의 대표 credential 패턴 검색에서 후보가 없었고 변경 목록에 환경설정·credential 파일은 없었다.
- 운영 DB 적용·배포·외부 provider 및 모바일 검증은 이번 커밋 검증에 포함하지 않는다.