docs(creatorname): 언어별 이름 관리 요구사항과 검증 기록을 정리한다

This commit is contained in:
2026-10-16 14:03:08 +09:00
parent 47ccea7ef6
commit 9da27d3feb
8 changed files with 2541 additions and 0 deletions
@@ -0,0 +1,233 @@
# 크리에이터 이름 언어별 관리 PRD
> 문서 상태: COR022 레거시 메시지 전체롤백·검증 완료. 두운영파일HEAD일치, 전용projection/테스트3건 제거 후 전체2267건/실패0/오류0/skip0과lint통과, 실제HTTP23요청·발송이벤트2건·독립리뷰승인을 확인했다. 공통FCM/v2 DM은 유지하고 임시자원은 정리했다. 운영DB적용·배포·커밋·푸시는 하지 않았다.
## 문서 정보
| 항목 | 내용 |
|---|---|
| 작성일 / 최종 수정일 | 2026-10-14 / 2026-10-16 |
| 대상 제품 | 앱 내 크리에이터 표시 이름의 수동 언어별 관리 |
| 구현 계획 | `plan-task.md`, COR012 승인, 서버 구현 진행 기록 및 잔여 Gate 포함 |
| API Contract | COR012 승인, 구현 및 HTTP 테스트로 확인 |
| DDL 산출물 | `schema.sql` 초안 파일 작성 승인, DB 미적용·별도 승인 전 실행 금지 |
| 구현 승인 | COR012 승인. 운영 DB 적용·배포는 별도 승인 필요하며 외부 클라이언트 의존성은 차단 상태 |
## 1. 목표와 범위
운영자가 크리에이터의 일본어 이름을 수동 관리하고, 앱의 크리에이터 표시 이름을 요청 언어에 따라 제공한다. 관리 쓰기/API 범위는 JA만이다. 조회는 기존 `Lang` 전부를 사용하며, 해당 언어에 활성 이름이 있으면 적용하고 없으면 현재 원본 이름으로 fallback한다. KO/EN 관리 기능은 포함하지 않는다. 저장 구조는 기존 `Lang`을 재사용한다.
앱 크리에이터 표시 경로가 대상이되 COR017에서 사용자가 미사용이라고 확인한 AudioContentMainTab 계열은 이번 변경에서 완전히 제외한다. 탭 파일은 기존 HEAD와 같게 유지하며, 미사용 탭의 원본 닉네임 반환을 보장하기 위한 null 인자·별도 raw 모드·테스트는 요구하지 않는다. 공유 조회는 실제 사용 호출자의 언어별 이름과 제목 번역을 보존하며, 그 변경이 미사용 탭 결과에 간접 영향을 주는 것은 허용한다. AudioContentCuration 및 레거시 커뮤니티·팬톡은 COR015의 원본 이름 계약을 유지한다. API 자체와 기존 저장·권한·제목/본문 번역·알림 업무는 삭제하지 않는다. 그 밖의 레거시/v2 HTTP 및 v2 커뮤니티·팬톡은 유지한다. 사용자별 신규 전달은 각 수신자/세션 언어로 렌더링한다. 기존 언어별 역사 스냅샷과 이미 전달된 payload는 수정하지 않는다.
일반 USER 이름, `Member.nickname` 원본, 원본 닉네임 편집기, 로그인·인증·원본 식별 흐름은 변경하지 않는다. 자동 번역도 하지 않는다. JA 별칭은 원본 닉네임이 나중에 변경되어도 계속 우선한다. 일본어 별칭은 원본 닉네임 검색과 함께 검색 가능해야 한다.
원본은 언어가 아니라 현재 `Member.nickname`에 저장된 문자열이다. 한국어·일본어·영어 등 어떤 언어일 수도 있으며 `Lang.KO`와 동의어가 아니다. 요청 언어의 활성 CREATOR 별칭이 있으면 원본 문자열의 언어와 무관하게 별칭이 우선하고, 없거나 해제됐으면 현재 원본을 그대로 반환한다. 원본이 일본어여도 JA 별칭이 따로 등록되어 있으면 등록값을 우선한다. `Lang.KO` 기본값은 요청 언어의 기본값일 뿐 원본 언어 지정이 아니다. 원본 언어 필드나 자동 언어 감지는 추가하지 않는다.
COR019에서 `AudioContentMainController`와 `AudioContentMainService`의 이번 이름 변경도 제외한다. 두 파일은 HEAD와 같게 되돌리고, 이 서비스만 사용하는 `findByThemeFor2Weeks`/`findAudioContentByCurationId`의 이름 언어 인자·JOIN만 제거한다. 공유 `findByTheme`의 현지화는 다른 실제 사용 API 때문에 유지하며, 이를 제외 API에 한해 차단하는 별도 raw 모드는 추가하지 않는다. 기존 제목·테마 번역, 캐시, 권한·차단·성인 필터는 유지한다. 전용으로 추가된 테스트는 실제 없으므로 공유 테스트를 삭제하지 않는다.
COR020에서 `CharacterCommentService`/`CharacterCommentRepository`의 작성자 이름 현지화 변경도 제외한다. 두 파일을 HEAD 형태로 복원하고 댓글 목록·답글의 원댓글·최신 댓글은 기존 저장 nickname을 사용한다. 댓글 작성/답글/삭제/신고·권한·커서·건수·언어 감지 이벤트는 변경하지 않는다. 해당 이름 대체 기능 전용 통합테스트1개만 제거하며 배너·다른 이름 조회 테스트는 유지한다.
COR021에서 `ContentSeriesRepository`/`ContentSeriesService`의 이름 현지화 변경도 HEAD로 되돌린다. 저장소5개 목록 조회의 이름 JOIN 및 서비스 상세 이름 batch만 제거되며 기존 제목·소개·장르·요일 번역과 권한·필터·순서·페이지·건수는 보존한다. 이 서비스를 재사용하는 HomeService의 오리지널/요일별 목록과 ExplorerService의 프로필 시리즈 작성자 이름도 원본으로 돌아가는 영향을 포함한다. 공통 CreatorNameJoin/CreatorLocalizedNameService 및 별도 v2 시리즈 조회는 삭제하지 않는다. 관련 테스트1개에서는 이름 현지화 단언만 제거하고 제목 번역·원문 fallback 검증을 유지한다.
COR022에서 레거시 `/message`의 MessageService/MessageRepository 이름 변경 전체를 HEAD로 되돌린다. 문자·음성의 발신/수신/보관 목록은 기존 엔티티 조회와 원본 이름을 사용하고 차단 오류의 이름 치환도 제거한다. MessageListRow와 전용 현지화 테스트3건은 제거하되 API·발송·보관·삭제·권한·본문/시간/URL 처리 자체는 유지한다. 전체 롤백에는 문자·음성 FCM 이벤트의 nameArgumentMemberIds 추가분2곳 제거도 포함된다. 기존 senderMemberId와 공통 FCM 처리는 남으므로 푸시 본문은 원본 인자를 사용하되 알림 senderNicknameSnapshot은 수신 언어 별칭으로 저장될 수 있다. 이는 공통 알림 기능의 별도 계약이며 v2 DM/WS나 과거 snapshot까지 롤백하지 않는다.
운영자는 문자 종류 제한 없이 JA 이름을 입력하며, 비공백 값은 문자 변경이나 음역 없이 보존한다. 저장 길이는 255 code point이며, UTF-8 byte나 UTF-16 code unit이 아닌 Unicode code point 기준으로 센다. 임의 Unicode를 허용하고 255 초과 입력은 쓰기 전에 관리 경계에서 거부하며 자르지 않는다. 빈 문자열 저장은 JA 이름 해제다. 공백-only 해석, 중복 별칭의 DB 제약/회원 ID 처리, ADMIN route/대상 판정은 기술 제안이다.
인터뷰 명확도 자체 평가: 약 0.10. 차원별 점수는 Goal 1.0, Scope 0.9, Constraints 0.9, Success 0.9, Context 0.75다. 제품 질문은 정리됐으며 외부 시스템 경로·소유자 계약은 별도 blocker로 남는다.
## 2. 제품 요구사항
| ID | 상태 | 요구사항 및 수용 사례 |
|---|---|---|
| NAME001 | 확정 | 관리자가 CREATOR 회원의 JA 이름을 수동 저장·갱신·해제한다. 빈 값 저장은 해제다. |
| NAME002 | 확정 | JA 요청은 JA 별칭을 원본보다 우선하고, 미등록이면 현재 `Member.nickname`으로 fallback한다. 원본 변경 후에도 별칭을 유지한다. |
| NAME003 | COR022 범위 보정 | AudioContentMainTab/MainController/Service, CharacterCommentService/Repository, ContentSeriesService/Repository 및 레거시 message/MessageService/Repository의 이번 이름 변경을 제외하며 별도 호환 raw 모드를 만들지 않는다. AudioContentCuration/레거시 커뮤니티·팬톡의 기존 제외 계약과 나머지 앱 표면의 언어별 이름·USER 원본 보존은 유지한다. 공통FCM과 별도v2 커뮤니티·팬톡·시리즈·DM/WS는 적용 대상이다. |
| NAME004 | 확정 | JA 별칭은 요청 locale과 무관하게 원본 닉네임 검색과 함께 검색되고 기존 contains/collation 의미와 필터가 유지된다. |
| NAME005 | 확정 | 일반 USER 이름, 원본 닉네임 편집, 인증/식별 목적의 원본 사용을 변경하지 않는다. |
| NAME006 | 확정 | 기존 언어별 역사 스냅샷과 이미 발송된 payload는 변경하지 않는다. 현재 이름 조회는 요청 언어, 신규 수신자별 payload는 수신자/세션 언어를 따른다. |
| NAME007 | COR012 승인·구현 | 빈 문자열 및 공백-only는 별칭 해제로 처리한다. 그 밖의 입력 문자는 그대로 보존하며 자동 번역·음역은 없다. 저장 상한은 255 code point다. |
| NAME008 | COR014 승인 | 관리자 회원 페이지 목록·검색의 기존 `nickname`은 원본을 유지하고, `localizedNames: Map<String, String>`에 CREATOR의 모든 활성 언어별 등록 이름을 함께 반환한다. 키는 Lang.code, 미등록/해제 언어는 생략하며 미등록·일반 회원은 빈 map이다. 이름 필드는 Accept-Language로 하나를 선택하지 않는다. |
| NAME009 | COR014 승인 | 닉네임 전용 관리자 GET을 제거하고 기존 일본어 PUT의 등록·수정·소프트 해제를 유지한다. PUT의 저장 결과 응답은 유지하며 관리자 일반 콘텐츠 조회는 원본 닉네임을 유지한다. |
| NAME010 | COR018 명확화 | 원본 nickname은 언어 중립 값이다. 한국어·일본어·영어 원본 각각에 대해 요청 언어의 활성 별칭 우선/미등록·해제 시 현재 원본 fallback을 검증한다. 원본과 별칭 문자열이 같은 언어라는 이유로 별칭을 무시하지 않는다. |
### 핵심 수용 사례
| 조건 | 결과 |
|---|---|
| JA 별칭 등록 후 원본 닉네임 변경 | JA 표시/검색은 별칭을 유지하고, KO/EN은 해당 언어의 활성 이름이 있으면 적용하고 없으면 현재 원본을 사용 |
| 같은 JA 별칭을 두 CREATOR에 저장 | 중복 표시명 허용 및 `(member_id, lang)` 고유 기술 설계가 구현 승인될 경우 모두 저장, 결과 회원은 중복 행 없이 구분 |
| JA 별칭이 빈 문자열 | 기술 계획은 행/값을 보존하고 비활성화, 원본 이름 표시 및 원본 검색 지속; 미등록이면 no-op |
| 해제 후 비공백 별칭을 재저장 | 비활성 포함 member+lang 조회로 같은 ID의 행 갱신·재활성화, 반복해도 한 행 유지 |
| JA 별칭이 공백만 | 별칭 해제로 처리하는 기술 제안 승인 시 원본 표시/검색 |
| CREATOR와 USER가 한 작성자 목록에 함께 있음 | CREATOR만 이름 대체, USER는 원래 값 유지 |
| 같은 사용자의 KO/JA 채팅 세션 동시 연결 | 각각 해당 세션 언어에 맞는 신규 payload 수신 |
| 기존 언어별 스냅샷 또는 과거 발송 payload | 과거 값 불변 |
| 언어 중립 캐시/랭킹 materialization | 생산자와 소비자를 조사한 후 언어별 표시 요구 충족 여부를 분류 |
## 3. 외부 의존 및 차단
| ID | 외부 의존 | 담당 역할 | 영향 | 재개 조건 |
|---|---|---|---|---|
| EXT001 | 모바일/클라이언트 전체 표시 표면, locale 전달, client cache와 원본 이름 편집 흐름 | 모바일/클라이언트 소유자 | 서버 변경만으로 전체 앱 노출과 원본 편집 안전을 입증할 수 없음 | 표면 inventory, locale 계약, 캐시 무효화/수명, 원본 편집 경로 및 담당자 확인 |
| EXT002 | 운영자 관리 UI와 배포/인증 연결 | 운영도구/관리 UI 소유자 | 서버 관리 API만으로 운영자 사용자 흐름 완료 불가 | UI 저장소·담당자·인증 주체·배포 위치·연동 방식 확인 |
| EXT003 | RTC/live/chat 세션 locale 및 알림 수신자 언어 계약 | 클라이언트 및 메시징/알림 담당자 | 세션별 언어와 재연결 동작 확인 없이는 전달 언어를 보장할 수 없음 | locale 전달 방식, 변경 시 reconnect 계약, 수신자 언어 결정 시점과 end-to-end 검증 경로 확정 |
외부 경로는 범위에서 제외되지 않는다. 소유 저장소와 파일 경로가 확인되지 않은 외부 작업은 `[blocked]`로 남긴다. 서버 계획의 완료로 앱 전체 요구를 완료 처리하지 않는다.
## 4. 기술 조사와 구현 계약
테이블명과 문서·DDL 파일 작성 범위는 사용자 확정이다. `Lang`/소프트 lifecycle은 기존 수동 관리 패턴에 근거한 기술 결정이며 사용자가 삭제 방식을 직접 선택했다는 뜻이 아니다. COR012에서 서버 구현과 테스트가 승인됐다. API는 승인·구현됐고 운영 DB 적용은 별도 승인 대상이다.
### 별도 이름 테이블과 수동 관리 설계
기존 회원 테이블에 언어별 닉네임 컬럼을 추가하는 방식이 아니라, **크리에이터의 언어별 표시 이름만 저장하는 별도 테이블**을 설계한다. 테이블명은 사용자 확인으로 `creator_localized_name` 확정이다. DDL 초안은 같은 디렉터리의 `schema.sql`에 작성하며 DB에는 적용하지 않는다.
| 데이터 | 저장 위치와 의미 |
|---|---|
| 원본 닉네임 | 기존 `Member.nickname`, 변경하지 않음 |
| 크리에이터 회원 ID | 별도 테이블의 `member_id`, 기존 회원 식별자 참조 |
| 표시 언어 | entity `lang: Lang`, `@Enumerated(EnumType.STRING)` 및 `@Column(nullable = false, length = 10)`; DB `lang VARCHAR(10)`에 enum NAME `KO/EN/JA` 저장, 이번 쓰기는 명시 `JA`만 |
| 수동 등록 이름 | 별도 테이블의 `display_name VARCHAR(255)`, 해당 언어의 표시 이름; 255 Unicode code point 초과는 관리 경계에서 거부하고 자르지 않음 |
| 활성 상태 | `is_active`, 해제 시 false로 바꾸고 이름은 보존; 표시/별칭 검색에는 true만 사용 |
`UNIQUE(member_id, lang)`은 활성/비활성 모두에 적용하고 `is_active`를 포함하지 않는다. clear는 기존 JA 행의 `is_active=false`만 설정해 `display_name`을 보존하며, 없는 행은 no-op이다. 비공백 저장은 member+lang으로 비활성까지 찾아 같은 ID를 갱신·활성화한다. 원본 `Member.nickname`, 다른 언어 행, 과거 언어별 snapshot/전달 payload는 유지한다. 마지막 비활성 값 하나를 남기는 설계이지 모든 개정 이력이나 복원 UI가 아니다.
앱 표시 조회는 요청 `Lang`과 일치하는 활성 이름 및 대상 `Member.role=CREATOR`를 적용한다. 비활성/미등록은 현재 원본 이름으로 fallback한다. 읽기에 JA 전용 gate는 두지 않는다. 관리자 목록은 COR014에 따라 원본과 모든 활성 언어 이름을 분리해 제공하며 전용 관리 GET은 제거한다. 별칭 검색은 요청 언어와 무관하게 활성 JA·CREATOR만 포함한다.
HTTP path/응답의 `locale: ja`는 `Lang.code`이며 DB에는 소문자 `ja`가 아닌 enum NAME `JA`를 저장한다. 요청 경계는 기존 `Lang.fromCode`/`Lang.fromAcceptLanguage`를 사용하고 서비스·repository에는 명시 `Lang`을 전달한다. 자유 문자열/새 enum/언어 converter를 추가하지 않는다. 배너의 KO 기본값은 복사하지 않으며 작성자가 언어를 명시한다. 미래 언어는 기존 `Lang`과 요청 처리를 별도 확장해야 하지만 언어별 컬럼 추가는 필요 없다.
### 기존 패턴 비교와 근거
| 근거 경로·심볼 | 재사용 또는 차이 |
|---|---|
| `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/ChatCharacterBanner.kt:31` (`lang`, `isActive`), `src/main/kotlin/kr/co/vividnext/sodalive/event/Event.kt:24` (`lang`, `isActive`) | 수동 언어별 관리의 기존 `Lang` enum-string/활성 상태 재사용. length 10은 새 DDL과 맞추는 명시 매핑이며 기존 KO 기본값은 재사용하지 않음 |
| `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/service/ChatCharacterBannerService.kt:140` (`deleteBanner`), `src/main/kotlin/kr/co/vividnext/sodalive/admin/event/banner/AdminEventBannerService.kt:202` (`delete`) | `isActive=false` 소프트 해제 근거. 이름의 동일 행 재활성화는 이번 별도 기술 계획이며 배너가 지원한다고 주장하지 않음 |
| `src/main/kotlin/kr/co/vividnext/sodalive/chat/character/repository/ChatCharacterBannerRepository.kt:16` (`findByIsActiveTrueAndLangOrderBySortOrderAsc`) | 기존 Lang 인자·활성 필터 읽기 패턴 재사용. 배너의 KO 대체 정책은 이름 조회에 복사하지 않음 |
| `src/main/kotlin/kr/co/vividnext/sodalive/content/translation/ContentTranslation.kt:15` (`UniqueConstraint`, `contentId`, `locale`) | 소유자+언어별 고유 행만 참고. 자동 번역의 `locale: String` 저장과 달리 수동 이름은 `lang: Lang`을 사용 |
| `src/main/kotlin/kr/co/vividnext/sodalive/i18n/Lang.kt:6` (`code`, `fromCode`, `fromAcceptLanguage`), `src/main/kotlin/kr/co/vividnext/sodalive/i18n/LangInterceptor.kt:18` (`preHandle`) | 기존 KO/EN/JA enum 및 HTTP 경계 매핑 재사용, 요청 언어를 저장 문자열로 새로 정의하지 않음 |
| `src/main/kotlin/kr/co/vividnext/sodalive/common/BaseEntity.kt:16` (`id`, `createdAt`, `updatedAt`) | 관리 ID/생성·수정 시각 구조 참고. SQL 기본 TIMESTAMP 정의는 문서 유지보수 규칙 적용 |
초기 물리 DELETE 설명은 단순한 최소 기술 선택이었으며 프로젝트 관례의 필수 조건이 아니었다. 사용자는 그 이유와 기존 언어 저장 패턴 재사용을 질문했으며 소프트 삭제를 직접 선택한 것은 아니다. 위 수동 관리 근거로 현재 기술 설계를 보정했다.
| 주제 | 제안 | 근거 및 제한 |
|---|---|---|
| 도메인/저장 | `kr.co.vividnext.sodalive.v2.creatorname` 신규 도메인 및 member+lang 고유, displayName non-unique, `VARCHAR(255)` 저장; 신규 entity는 `displayName @Column(nullable = false, length = 255)` | 신규 도메인은 v2 경량 헥사고날 구조. 255 code point를 관리 경계에서 검증하고 초과 입력은 거부한다. DDL 초안 작성만 승인, 실제 DB 호환성은 미검증. 중복 표시명 정책은 인터뷰 확정 선택이 아님 |
| 관리 API | COR014: `PUT /api/v2/admin/creators/{creatorId}/localized-names/ja`만 유지, ADMIN 보호 | 필수 `displayName: String`, 빈 값 soft clear. 기존 PUT 결과 `creatorId`, `locale: ja`, `originalNickname`, nullable `displayName` 및 ApiResponse 유지. 독립 GET은 제거하고 조회는 기존 관리자 회원 목록에 통합한다. 이번 변경의 HTTP 실행 검증은 미수행이다. |
| 관리 검증 | `Member.role == CREATOR`인 대상만 허용하며, 그 중 별도 `Member.memberKind == AI_CHARACTER`도 포함. `memberKind`만 AI_CHARACTER인 `role == USER`는 허용하지 않음 | ADMIN 보호와 role 판정은 근거 기반 기술 제안. 신규 message key만 제안하며 기존 role-edit PUT 불변 |
| 공통 이름 조회 | DB 직접 projection은 `CreatorNameJoin`으로 요청 Lang의 LEFT JOIN+COALESCE를 사용한다. 중립 캐시·수신자별 전달·principal·쓰기 결과 경계는 batch를 사용하며 현재 원본이 필요한 batch는 JOIN+COALESCE 단일 SQL, sparse 별칭이 필요한 곳은 `findLocalizedNames`를 쓴다. | 읽기는 모든 기존 `Lang`에서 활성/CREATOR만 적용하며 JA-only gate가 없다. 쓰기 조회는 비활성 포함 member+lang. 행별 쿼리·전역 Jackson 변환·principal 변경 금지. `LangContext`는 HTTP 경계에서만 읽음 |
| HTTP/DTO 호환 | 레거시/v2 응답 DTO의 field 이름·타입 보존. 필요한 조회 projection에만 내부 memberId/role 추가 | 원본 닉네임 편집 API는 로컬라이즈 값을 원본으로 저장하지 않도록 외부 계약 확인 필요 |
| 검색 | 기존 원본 조건 OR alias `EXISTS`(owner `member_id` 상관, `lang=Lang.JA`/DB `JA`, `is_active=true`, owner `Member.role=CREATOR`); containsIgnoreCase 의미 유지 | 요청 locale과 무관하게 활성 JA만 별칭 검색. Explorer channel, SearchRepository creator branch, MemberRepository의 list/count에 동일 조건과 기존 필터/USER 검색 보존. 콘텐츠·시리즈 제목·detail·tag 검색은 별칭 대상 아님 |
| 푸시/저장 한계 | `FcmSendListener.sendPush`에서 언어별 payload 생성. 기존 `PushNotificationList.senderNicknameSnapshot` mapping이 255자 이름을 보존하는지 확인 | 기존 mapping/컬럼 확장은 이번 계획에 포함하지 않는다. 과거 알림 재작성·이름 자르기 금지. 실제 저장 및 외부 payload 호환성 문제가 확인되면 별도 결정 전 해당 적용은 차단한다. |
| WebSocket | handshake `Accept-Language`를 session attribute로 저장, 미전달 기본 KO. locale 변경은 reconnect 계약 제안 | Redis pubsub에는 원본 메시지/발신 ID 유지, broker에서 세션별 새 payload 구성. 공유 DTO 변이 금지. ACK와 HTTP 응답도 별도 확인 |
| 캐시/스냅샷 | 각 producer/read path를 조사해 현재 이름 조회와 불변 historical snapshot을 구별 | 기존 `LiveRecommendCacheService`의 3시간 캐시는 key에 Lang을 포함. 다른 캐시/랭킹/inbox는 자동 면제하지 않음 |
`schema.sql`은 단일 CREATE TABLE 초안이며 MySQL 컬럼/테이블 COMMENT와 기본 TIMESTAMP 정의를 따른다. signed BIGINT와 member FK/engine/charset 초안 근거는 `docs/plan-task/20260326_member_content_preference_ddl.sql:12`, COMMENT/TIMESTAMP 및 소유자+언어 unique/FK 형태 근거는 `docs/20260910_크리에이터커뮤니티게시물본문번역/schema.sql:5`다. 복합 unique가 FK 지원 인덱스이며 추가 인덱스·displayName 고유성/인덱스·언어 기본값·cascade·check·trigger를 두지 않는다. displayName은 `VARCHAR(255)`이며 실제 DB 호환성은 미검증이다. **DDL 파일 작성 완료와 DB 적용은 별개다.** 운영 반영 전에 실제 `member.id` 타입(부호 포함)·engine과 collation/contains 및 저장 호환성을 확인하고 DB 실행을 별도 승인받는다. 현재는 DB 연결/실행이나 운영 호환성을 검증하지 않았다.
## 5. 근거 경로 및 범위 추적
주요 근거와 seed inventory는 계획 문서에 실제 전체 경로 및 test target으로 분류했다. 목록은 감사 시작점이지 전수 완료 선언이 아니다. 기존 `MemberRepository` nickname 검색 경로와 legacy home/rank/recommend 및 v2 content/channel query adapter를 빠짐없이 조사한다.
이 저장소에는 모바일 앱 및 운영 UI 소스가 없다. 외부 API, 캐시 및 RTC 소유 경로는 계약이 확인되기 전 구체 경로를 추정하지 않는다.
## 6. 의사결정 기록
현재 후속 실행 범위는 COR022를 따른다. 이전 실행 금지 및 정적 승인 기록은 당시 이력으로 보존한다.
기존 결정 로그의 행은 요약하지 않고 각각 보존한다. 아래 DEC 항목은 최초 결정의 주제를 명시한 기록이다. COR 행은 그 결정을 보정하고 우선 적용한다.
| 날짜 | ID | 상태 | 결정 |
|---|---|---|---|
| 2026-10-14 | DEC001 | 확정 | 운영자가 일본어 이름을 수동 관리한다. 자동 생성/번역하지 않는다. |
| 2026-10-14 | DEC002 | 확정 | 입력 문자 종류 제한을 두지 않는다. |
| 2026-10-14 | DEC003 | 확정 | JA 이름은 일본어 표시에 우선 적용한다. 미등록 시 원본 이름을 사용한다. |
| 2026-10-14 | DEC004 | 확정 | 원본 `Member.nickname`은 보존한다. |
| 2026-10-14 | DEC005 | 확정 | 앱의 크리에이터 표시 표면을 대상으로 하며 혼합 목록에서도 CREATOR만 변경한다. |
| 2026-10-14 | DEC006 | 확정 | 미래 언어 가능성을 고려할 수 있지만 현재 KO/EN 이름 관리 기능은 만들지 않는다. |
| 2026-10-14 | DEC007 | 확정 | JA 별칭은 기존 원본 닉네임 검색과 함께 검색한다. |
| 2026-10-14 | DEC008 | 확정 | JA 이름 빈 값 저장은 별칭 제거이며 원본 fallback으로 돌아간다. |
| 2026-10-14 | COR001 | 정정 | 별칭은 원본 닉네임 변경 이후에도 우선한다. 미래 언어 준비는 기술 제안일 뿐 제품 범위가 아니다. |
| 2026-10-14 | COR002 | 정정 | 일반 USER와 원본 편집/인증/식별 흐름은 불변. 제품 업무 길이 제한을 추가하지 않는다. 중복 별칭 허용 및 회원 ID+locale 저장은 기술 제안이며 직접 확정된 인터뷰 선택은 아니다. |
| 2026-10-14 | COR003 | 정정, OQ-001 해결 | 원본 이름 검색을 유지하면서 JA 별칭 검색을 더한다. |
| 2026-10-14 | COR004 | 정정, OQ-002 해결 | 빈 문자열 저장으로 JA 이름을 해제한다. 공백-only 해제는 기술 해석 제안으로 별도 승인 대기다. |
| 2026-10-14 | COR005 | 정정, OQ-003 해결 | 과거 언어별 스냅샷/전달 payload는 불변. 현재 조회는 요청 언어, 새 수신자별 payload는 해당 수신자/세션 언어. 언어 중립 캐시를 자동 제외하지 않는다. |
| 2026-10-14 | COR006 | 정정 | `/admin/member` ADMIN 권한은 참고 근거일 뿐 신규 API 소유권 계약이 아니다. `/creator-admin/member` CREATOR/AGENT 관리 화면 대상이 아니다. |
| 2026-10-14 | COR007 | 정정 | PRD와 plan-task 문서 작성만 승인되었다. 구현/테스트/설정/DDL은 별도 승인 전 금지. |
| 2026-10-14 | DEC009 | 확정 | 기존 언어별 과거 스냅샷과 이미 전달된 payload는 변경하지 않는다. 요청 시 현재 이름을 조회하는 경로는 요청 언어, 신규 수신자별 payload는 수신자/세션 언어를 적용한다. |
| 2026-10-14 | COR008 | 후속 확정, COR007 범위 보정 | 사용자 확인으로 테이블명 `creator_localized_name` 확정, 기존 두 문서 보정과 같은 디렉터리 `schema.sql` 파일 작성만 승인. DB 적용·Kotlin·테스트·설정·운영 배포는 미승인이다. |
| 2026-10-14 | COR009 | 근거 기반 기술 결정 | 기존 수동 관리의 `Lang` enum-string과 `isActive`를 재사용한다. DB 키는 member+lang/enum NAME `JA`, HTTP locale은 `Lang.code`의 `ja`다. 초기 물리 DELETE는 단순 기술 선택이지 프로젝트 강제 관례나 사용자 요구가 아니며, 사용자가 소프트 삭제를 직접 선택한 것도 아니다. |
| 2026-10-14 | COR010 | 기술 lifecycle 보정 | clear는 행/값 보존·비활성화, absent는 no-op. 표시/검색/관리 GET은 비활성 제외, 재저장은 비활성 포함 조회로 동일 ID 재활성화. unique는 활성/비활성 전체 member+lang. 전체 이력/복원 기능은 포함하지 않는다. |
| 2026-10-14 | COR011 | 기술 저장 한계 보정 | 사용자는 TEXT가 불필요하다고 확인했다. `VARCHAR(255)`는 기본 JPA nickname 표현에 맞춘 기술 결정이며 사용자가 255를 직접 선택한 것은 아니다. 현재 저장은 255 Unicode code point로 제한하고 초과는 관리 경계에서 거부한다. COR002의 무길이 제한 표현은 현재 저장 한도에 대해 superseded이며 문자 종류 제한 없음은 유지한다. |
| 2026-10-14 | COR012 | 사용자 구현 승인 | 사용자: "DDL + 문서 확인 / 구현 승인" 및 ulw. 현재 문서의 API·저장·소프트 해제·길이·검색·전달 설계를 구현하고 테스트한다. 앞선 제품 구현 미승인 문구를 대체한다. 운영 DB 적용·배포는 승인하지 않았고 외부 저장소 계약은 확인 전 완료로 간주하지 않는다. |
| 2026-10-15 | COR013 | 사용자 조회 방식 수정 승인 | "3가지 경로에 각각 적절한 방식으로 조회하도록 수정하자". DB 직접 이름 projection은 요청 Lang의 LEFT JOIN+COALESCE, 중립 캐시·수신자별 전달 및 기존 원본 엔티티/쓰기 결과 경계는 batch, 이미 언어별로 렌더된 역사 스냅샷은 저장값을 유지한다. 읽기의 JA 전용 제한은 제거하되 검색 JA·관리 /ja 및 저장 정책은 유지한다. |
| 2026-10-15 | COR014 | 사용자 관리자 조회 통합 적용 승인 | "문서에 반영하고 적용한다". 별도 닉네임 GET 대신 기존 `/admin/member/list`와 공통 응답을 쓰는 목록·검색 4개 경로에 모든 활성 언어 이름을 추가한다. PUT의 등록·수정·해제와 결과 응답은 유지한다. 관리자 콘텐츠 원본 표시 및 레거시 관리자 회원 목록은 보존한다. 빌드·테스트 실행 없이 수정·정적 검토만 진행한다. |
| 2026-10-15 | COR015 | 사용자 선택적 롤백·테스트 정리 승인 | ContentMainTab/AudioContentCuration/레거시 커뮤니티·팬톡의 이번 이름 처리만 제거한다. v2와 공유 저장·권한·번역·알림, COR014 관리자 목록/PUT은 유지한다. LegacyCreatorNameRenderer 위임 래퍼는 제거하고 필요한 호출자는 core service를 직접 사용한다. 제거 기능만 검증하는 테스트와 불필요 fixture/import는 정리하되 기존 업무·v2·core 회귀 단언은 보존한다. 기존 미사용 메서드 자체 삭제는 제외하며 빌드·테스트 실행 및 커밋·푸시는 하지 않는다. |
| 2026-10-15 | COR016 | 사용자 최소 수정·통과 검증 승인 | 사용자가 전체 실행의 두 실패를 확인하고 최소 수정하여 통과시키도록 요청했다. AudioContentRepositoryLocalizedNameTest의 H2 번역 payload 적재만 기존 FORMAT JSON 패턴으로 보정한다. 운영 코드/converter/DDL/요구사항/테스트 기대값은 보존한다. 대상 및 회귀 테스트 실행을 재개하되 운영 DB 적용·배포·커밋·푸시는 하지 않는다. |
| 2026-10-15 | COR017 | 사용자 미사용 탭 완전 제외 승인 | AudioContentMainTab*은 사용하지 않으므로 이번 기능 변경에서 배제해도 무관함을 확인하고 수정 승인했다. 탭 7파일의 null 전달 13곳을 되돌려 HEAD diff를 없애고, 공유 운영6파일·테스트2파일에서는 미사용 탭 전용 raw 인자/분기/검증만 정리한다. 실사용 이름·제목 번역과 기존 다른 raw 경계는 유지한다. |
| 2026-10-15 | COR018 | 사용자 테스트·문서7파일 보정 승인 | 원본은 KO가 아니라 임의 언어의 저장값임을 명확히 했다. 공통 service/JOIN 테스트2파일, 대표 HTTP/메시지 테스트3파일, 기존 PRD/계획2파일만 보강한다. 운영 로직은 이미 요청 언어 별칭→원본 fallback이므로 변경하지 않는다. 조건을 생략한 KO 원본 설명을 고치고 기존 테스트에 원본 문자열3종 및 일본어 원본 HTTP/메시지 사례를 반영한다. 신규 테스트 파일·원본 언어 필드·자동 감지·쓰기 언어 확대는 제외한다. |
| 2026-10-15 | COR019 | 사용자 콘텐츠 메인 롤백 승인 | 조사 후 운영3파일(메인 controller/service 전체 변경 및 repository 전용2메서드 이름 변경)의 롤백을 승인했다. 두 메인 파일은 변경 목록에서 제외하고 실제 사용하는 공유 repository·core·v2 테스트는 보존한다. 전용 추가 테스트0건이므로 테스트 삭제는 하지 않는다. 새로운 원본 보호 모드·API 삭제·운영 적용은 포함하지 않는다. |
| 2026-10-16 | COR020 | 사용자 캐릭터 댓글 롤백·테스트 제거 승인 | CharacterCommentService/Repository 두 운영파일 변경을 HEAD로 되돌리고 LegacyCurrentNameReadIntegrationTest의 shouldLocalizeCharacterCommentWritersWithoutChangingCharacterNameOrUserReplies 1개 및 전용3import/서비스주입을 제거한다. 나머지8개 통합테스트와 캐릭터 배너 테스트는 유지한다. 신규 영구 테스트·API·원본 보호 모드 추가는 제외한다. |
| 2026-10-16 | COR021 | 사용자 시리즈 롤백·제목 검증 보존 승인 | 조사 결과 설명대로 운영2파일의 이름 처리6곳(목록JOIN5+상세batch1)을 롤백한다. 홈·프로필 재사용 경로에도 이름 표시 영향이 있음을 설명하고 진행 승인받았다. AudioContentRepositoryLocalizedNameTest의 혼합 시리즈 테스트는 이름별칭 fixture/기대값/단언만 정리하고 제목 번역·fallback 및 H2 FORMAT JSON 처리를 보존한다. 테스트 메서드/파일 전체 삭제나 새 raw 모드 추가는 하지 않는다. |
| 2026-10-16 | COR022 | 사용자 메시지 전체 롤백 승인 | 조사 결과대로 MessageService/Repository 전체HEAD복원, 전용 MessageListRow 및 MessageServiceLocalizedNameTest(3건) 파일 삭제를 승인했다. 푸시 본문 메타데이터2곳 제거·senderMemberId 및 공통 snapshot 현지화 유지라는 영향을 포함한다. 레거시 API/업무 자체와 공통 FCM·v2 DM/WS 테스트는 삭제하지 않는다. |
DEC003/005/007/008은 각각 원래 사용자 선택을 요약한 결정 기록이다. 공백-only 해석, 중복 별칭의 구체 저장 제약, 구체 API route/ADMIN 관리 경계는 후속 기술 제안이지 별도 인터뷰에서 명시된 사용자 선택이 아니다.
COR002의 member+locale·무길이 제한 표현과 COR007의 DDL 작성 금지는 당시 기록으로 보존하며 현재 범위/저장 결정은 COR008~011이 우선한다.
## 7. 진행 판정
- `plan-task.md`는 NAME001~007, EXT001~003을 구체 task와 검증에 연결한다.
- COR013의 JOIN-T1 공통 계약과 JOIN-T2 DB projection 전환은 구현 및 focused 증거 확인 완료. JOIN-T3 경계 assertion은 통과했으나 최종 전체 build/QA Gate 결과는 대기 중이다.
- 외부 모바일/운영 UI/RTC 계약은 차단 조건·담당 역할·재개 증거를 유지한다.
- COR012까지의 과거 전체 검증은 아래 이력으로 보존한다. COR013 최종 실행은 현재 미완료이며 `plan-task.md`의 COR013 진행 기록만 근거로 삼는다. 외부 앱 전체 표시, 운영 UI, RTC/client 계약 및 운영 DB/provider 한계도 미검증이며 제품 완료를 선언하지 않는다.
- 운영 DB 적용·운영 배포는 별도 승인 대상이다. 커밋·푸시는 별도 사용자 승인 없이 수행하지 않는다.
## 8. 구현 및 검토 상태, 2026-10-14
- 기반 저장/API, legacy 및 v2 내부 HTTP/검색 소비자, 서버 FCM/WebSocket/live/message 경로가 구현됐다. 세 inventory의 변경 기록과 `/var/folders/yh/8xsbvpsj5wg2qnxzxdp11_gm0000gn/T/opencode/creatorname-integrated-full.log`의 최종 성공 실행을 근거로 한다.
- 당시 최종 전체 실행: 322 suites, 2245 tests, failures 0, errors 0, skipped 0. `assemble`와 `ktlintCheck` 포함 명령이 `BUILD SUCCESSFUL`로 종료됐다. 이는 COR013 전 과거 이력이다. 이전 200 fixture/context 실패 실행과 로그도 당시 기록으로 보존한다.
- 테스트가 실제 관리 API JWT HTTP를 사용했다. 테스트 서버는 HTTP 53418, WebSocket 53427에서 기동됐고 teardown curl은 양 포트 연결 거부(exit 7)를 확인했다. Hikari pool 종료를 로그에서 확인했으며 테스트 후 잔류 JVM/Redis 프로세스는 보고되지 않았다.
- 전체 앱 클라이언트 표시/원본 편집 안전성, 운영 관리 UI, RTC reconnect/provider 및 실제 운영 DB 적용은 확인되지 않았다. EXT001~003과 운영 DB/provider 호환성은 blocker다.
- Oracle 검토에서 두 보완점이 확인됐다. 현재 이름 fallback 경로의 `findAllById` entity graph 조회를 scalar ID/nickname 조회로 바꾸고, 공통 helper 두 개를 `v2.creatorname` 하위로 이동한다. cache 실제 hit를 입증하는 테스트 assertion도 추가할 필요가 있다. 수정 담당자가 작업 중이며 아직 수정·재검증·리뷰 승인 완료로 기록하지 않는다.
- 검증 환경에 `lsp_diagnostics` 도구가 없어 LSP 진단은 수행하지 못했다. compile, 전체 테스트, assemble, ktlint 결과는 위 실행 로그가 근거다.
## 9. 최종 서버 검증, 2026-10-15
- 리뷰 지적 2건 해결: 이름 전용 현재 원본 조회를 `MemberQueryRepository.findNicknamesByIds` scalar 조회로 교체했고 공통 renderer/predicate를 `v2.creatorname` 아래로 이동했다. 1명/4명 조회에서 JA SQL 2/2회, KO·EN 1/1회, 엔티티 로드 0/0을 테스트로 확인했다.
- COR013 전 당시 전체 회귀: 322 suites, **2249 tests / failures 0 / errors 0 / skipped 0**, `test assemble ktlintCheck --continue --offline --console=plain` 성공. 이후 상위 에이전트의 `./gradlew build --offline --console=plain`도 성공했다. 이 결과는 COR013 이후 최종 검증을 뜻하지 않는다.
- 실제 표면 재검증: 상위 에이전트가 ADMIN HTTP 4개와 localhost HTTP/WebSocket 9개를 별도 실행해 **13개 모두 통과**했다. 포트 54539/54547의 Tomcat·Hikari 종료와 후속 curl exit 7, 테스트 JVM/Redis 잔존 없음도 확인했다.
- 최종 독립 리뷰: Oracle의 수정분 재검토 **APPROVE**, 중요한 잔여 코드 지적 없음. 실제 캐시 entry의 원본과 방 목록 쿼리 1회 재사용까지 검증했다.
- 외부 모바일/운영 UI/RTC 및 실제 provider/운영 MySQL 호환성은 미검증으로 유지한다. 운영 DB 적용·배포·커밋·푸시는 실행하지 않았다. 상세 명령·로그는 계획 문서 최종 기록을 따른다.
## 10. 조회 경로별 처리, COR013
1. DB에서 이름을 직접 조회하는 목록·projection은 `CreatorNameJoin(owner, lang, alias)`로 `creator_localized_name`을 LEFT JOIN한다. 회원 ID·요청 언어·활성 별칭·CREATOR 역할 조건은 ON에 두고 `COALESCE(display_name, nickname)`를 선택한다. 후속 별칭 SELECT와 이름 전용 copy/map을 제거한다. 검색 조건·정렬·집계·페이지·공개 DTO는 보존한다.
2. 중립 캐시와 Redis payload, 수신자/세션 언어가 늦게 정해지는 전달, 인증 principal 및 기존 원본 엔티티를 유지해야 하는 쓰기/오류 경계는 필요한 batch를 유지한다. 현재 원본까지 필요한 batch는 member+별칭 JOIN의 단일 SQL로 처리하고 sparse 별칭 map은 명시 fallback 의미가 있는 곳에만 쓴다. 세 inventory의 잔여 호출 분류를 따른다.
3. 이미 언어별로 렌더해 저장한 역사 이름은 그대로 반환한다. 중립 랭킹/inbox/cache는 historical rendered snapshot과 구별하며, cache hit에서도 현재 이름을 렌더한다.
읽기 서비스와 저장소는 전달받은 기존 `Lang`을 사용한다. JA/KO/EN 각각 활성 이름이 있으면 해당 값을, 없거나 비활성이면 원본을 반환한다. 현재 관리자 입력 /ja 및 저장의 일본어 범위, 검색의 JA 별칭 EXISTS는 이번 조회 리팩터링에서 확장하지 않는다. `Member.nickname` 원본과 인증/편집 경계는 그대로 둔다. 승인된 세 inventory의 COR013 최신 구간이 개별 소비자와 남은 batch를 분류한다.
### COR013 구현 및 검증 상태, 2026-10-15
- JOIN-T1 공통 `CreatorNameJoin` 및 모든 Lang의 sparse/current-name 조회 계약, JOIN-T2의 직접 DB projection 전환은 구현됐다. legacy/v2/live/message/DM 목록 경로는 각 inventory의 COR013 manifest를 기준으로 한다.
- JOIN별 owner alias는 고유하게 지정한다. 삭제 prefix 정리는 raw 원본 이름에만 적용하며 수동 별칭 원문은 바꾸지 않는다. 기존 count/group/page/filter와 공개 DTO는 보존한다.
- 기준선은 `creatorname-join-baseline.log` 성공(1m53s). core 43개 RED 중 13개 assertion 실패 뒤 GREEN 43개 통과(`creatorname-join-core-green.log`); consumer 3개 RED는 이름 출력이 아닌 추가 alias SELECT 기대 0/실제 1을 포함했다(`creatorname-join-consumer-red.log`). 수정 후 consumer 3개 통과, alias SELECT 0 확인(`creatorname-join-first-green.log`, 2m37s).
- 첫 전체 실행 `creatorname-join-full.log`은 테스트 2266개가 통과했으나 ktlint 서식 오류로 실패했다. 서식 보정 후 `creatorname-join-final-green.log`의 전체 test/build/ktlint가 성공했다. 이후 실사용이 없어진 보조 메서드 2개와 해당 passing helper 테스트 1개만 제거하고 전체 검증을 다시 실행했다.
- 최종 `./gradlew test build ktlintCheck --continue --offline --console=plain` 성공: **2265 tests / failures 0 / errors 0 / skipped 0**, `creatorname-join-release-check.log`의 `BUILD SUCCESSFUL in 13m 45s`. 실제 ADMIN JWT HTTP 4개와 localhost HTTP/WebSocket 9개도 포함되어 통과했다.
- 대표 콘텐츠·검색·라이브 추천의 별칭 전용 후속 SELECT는 각각 1회에서 0회로 줄었다. 캐시 current-name batch는 요청 Lang에 무관하게 1 SQL이며 sparse 수신자 override와 역사 스냅샷 보존도 검증했다. 운영 지연 시간 개선을 측정한 것은 아니다.
- Oracle 3-path 리뷰에 중대한 코드 지적이 없었고, 미사용 보조 메서드 제거 후 수정분 재검토도 APPROVE다. 최종 실행 결과로 runtime 조건을 충족했다. HTTP 58173/WS 58186 종료 및 후속 curl exit 7, 테스트 JVM/Redis 잔존 없음도 확인했다.
- 제품 전체 완료는 별개다. 모바일/client, 운영 UI, RTC/provider 및 실제 MySQL 호환성은 외부 확인이 필요하며 운영 DB 적용·배포·커밋·푸시는 수행하지 않는다.
## 11. 관리자 조회 통합, COR014
- 대상: `/admin/member/list`, `/admin/member/search`, `/admin/member/creator/list`, `/admin/member/creator/search`. 공통 `GetAdminMemberListResponseItem`에 `localizedNames`만 추가하고 기존 필드·totalCount·순서·페이지 크기는 유지한다. 이는 의도적인 관리자 JSON 응답 확장이다.
- 원본 `nickname`은 그대로 두고 CREATOR의 모든 활성 등록 이름을 `{"ja":"등록 이름","en":"등록 이름"}` 형태로 제공한다. 특정 언어로 필터링하거나 원본을 미등록 언어에 채우지 않는다. soft 해제된 값은 노출하지 않으며 일반 회원과 별칭 없는 회원은 `{}`다.
- 회원 페이지를 먼저 읽고 해당 페이지의 CREATOR ID들에 대한 모든 활성 언어 이름을 한 번에 조회한다. 다언어 1:N 조인으로 회원 페이지/건수가 증가하지 않도록 기존 페이지 쿼리는 보존하고 회원별 추가 조회는 하지 않는다. 역할·로그인 방식 등의 기존 번역 문구는 유지한다.
- `/search-by-nickname`과 `/creator/all/list`의 별도 간단 DTO, 관리자 콘텐츠/시리즈/댓글의 원본 닉네임 표시, 일반 앱의 COR013 언어별 표시·JA 검색은 변경하지 않는다.
- 독립 GET과 더 이상 운영 호출이 없는 단건 조회 서비스는 제거한다. `CreatorLocalizedNameResponse`는 PUT 확인 응답에 계속 사용하므로 유지한다. 별도 POST/DELETE, 전체 언어 신규 GET, 레거시 API 제거, H2 보정/DDL 변경은 추가하지 않는다.
- 실행 제한: 테스트를 먼저 작성하되 사용자 빌드 금지에 따라 Gradle·컴파일·테스트·앱 실행을 하지 않는다. 실제 RED/GREEN·HTTP 성공은 미검증으로 기록하며 이전 통과 수치를 이번 변경의 증거로 재사용하지 않는다.
### COR014 적용 결과
- 공통 관리자 목록 항목에 `localizedNames`를 추가했다. 기존 페이지 조회 후 CREATOR ID의 모든 활성 이름을 단일 scalar 조회로 묶으며, `nickname`과 기존 페이지/count 쿼리는 유지한다. 간단 선택 API와 관리자 콘텐츠 원본 이름 조회는 변경하지 않았다.
- 전용 닉네임 GET 및 미사용 `getLocalizedName` 서비스를 제거했다. PUT과 그 확인 응답 `CreatorLocalizedNameResponse`는 유지한다. 새 전체 언어 GET이나 별도 POST/DELETE는 추가하지 않았다.
- 관리자 4개 목록/검색, Accept-Language 독립, 다언어 페이지 중복 방지, USER·해제 제외, 빈 ID/일괄 쿼리 수, 제거한 GET, 기존 PUT lifecycle·권한·255/256 code point 테스트 코드를 작성·보정했다. 실행하지 않았다.
- 독립 리뷰는 **PASS(static)**이며 명백한 호출·타입·권한·페이지 계약 문제는 발견되지 않았다. 빌드/테스트/DB/HTTP 실행 성공을 의미하지 않는다. 사용하지 않는 레거시 API 제거와 H2 보정 변경은 이번 범위 밖으로 유지했다.