diff --git a/SodaLive/Sources/I18n/I18n.swift b/SodaLive/Sources/I18n/I18n.swift index fafd0b18..de2f6593 100644 --- a/SodaLive/Sources/I18n/I18n.swift +++ b/SodaLive/Sources/I18n/I18n.swift @@ -3567,6 +3567,14 @@ If you block this user, the following features will be restricted. ja: "まだサポートがありません。\n最初のサポートをしてみましょう!" ) } + + static func defaultMessage(_ can: Int) -> String { + pick( + ko: "\(can)캔을 후원하였습니다", + en: "Donated \(can) cans.", + ja: "\(can)canをサポートしました。" + ) + } } enum HomeRecommendation { diff --git a/SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationCard.swift b/SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationCard.swift index ca8846e9..cf88df27 100644 --- a/SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationCard.swift +++ b/SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationCard.swift @@ -57,7 +57,7 @@ struct CreatorChannelDonationCard: View { .scaledToFit() .frame(width: 18, height: 18) - Text("\(donation.can)캔") + Text(I18n.LiveChat.canWithUnit(donation.can)) .appFont(size: 14, weight: .regular) .foregroundColor(.white) .lineLimit(1) @@ -72,7 +72,7 @@ struct CreatorChannelDonationCard: View { extension CreatorChannelDonationResponse { var messageText: String { let trimmedMessage = message.trimmingCharacters(in: .whitespacesAndNewlines) - return trimmedMessage.isEmpty ? "\(can)캔을 후원하였습니다" : trimmedMessage + return trimmedMessage.isEmpty ? I18n.CreatorChannelDonation.defaultMessage(can) : trimmedMessage } func relativeTimeText(now: Date = Date()) -> String { diff --git a/SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationRankingSection.swift b/SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationRankingSection.swift index d6732564..7c2e9457 100644 --- a/SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationRankingSection.swift +++ b/SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationRankingSection.swift @@ -7,7 +7,7 @@ struct CreatorChannelDonationRankingSection: View { var body: some View { if rankings.isEmpty == false { VStack(alignment: .leading, spacing: SodaSpacing.s14) { - Text("후원 랭킹") + Text(I18n.Explorer.donationRankingTitle) .appFont(size: 20, weight: .bold) .foregroundColor(.white) .frame(maxWidth: .infinity, alignment: .leading) @@ -19,7 +19,7 @@ struct CreatorChannelDonationRankingSection: View { } Button(action: onTapViewAll) { - Text("전체보기") + Text(I18n.CreatorChannelHome.viewAll) .appFont(size: 16, weight: .medium) .foregroundColor(.white) .frame(maxWidth: .infinity) diff --git a/docs/20260704_크리에이터_채널_후원_탭/plan-task.md b/docs/20260704_크리에이터_채널_후원_탭/plan-task.md index 4ec838b9..9a881454 100644 --- a/docs/20260704_크리에이터_채널_후원_탭/plan-task.md +++ b/docs/20260704_크리에이터_채널_후원_탭/plan-task.md @@ -263,3 +263,54 @@ - 2026-08-14: 사용자 인터뷰로 후원 탭 empty 문구의 ko/en/ja 값을 확정하고 기존 후원 버튼 title 재사용을 Phase 6 미완료 후속 Task로 추가했다. 이번 작업은 문서만 보강했으며 Swift 구현과 빌드는 수행하지 않았다. - 2026-08-14: Phase 6 구현을 완료했다. 후원 탭 empty 문구를 ko/en/ja I18n으로 교체하고 버튼은 기존 `I18n.ContentDetail.DonationDialog.title`을 재사용했으며 `Color.soda400` 배경과 white 아이콘·텍스트를 유지했다. 23개 정적 계약, `git diff --check`, iPhone 16 Pro 시뮬레이터 Debug 빌드가 통과했고, 세 언어 캡처와 두 독립 색상·CJK 리뷰에서 차단 항목 없이 PASS를 확인했다. + +### Phase 7: 후원 랭킹·공용 카드 다국어 회귀 수정 + +**Phase 결과:** 후원 랭킹과 홈·후원 탭 공용 후원 카드의 고정 UI 문구가 ko/en/ja 앱 언어에 맞게 표시된다. + +**선행조건:** `REV-P3-001`, `REV-P3-002` 확정과 PRD의 `DEC-DONATION-I18N-001`, `DEC-DONATION-I18N-002`. + +#### Task 7.1: 랭킹·공용 카드의 런타임 하드코딩 제거 + +**Goal 실행 `P7-R1`:** 확정된 네 런타임 하드코딩을 기존 I18n 재사용과 하나의 카드 전용 fallback 문구로 교체한다. + +- **시작 조건:** `docs/20260704_크리에이터_채널_후원_탭/reviews/phase3-donation-localization-audit.md`의 `REV-P3-001`, `REV-P3-002`. +- **완료 증거:** 정적 계약 검사, ko/en/ja 수동 확인, Debug 빌드, 검증 기록 누적. +- **범위 밖:** Preview fixture 번역, 신규 dependency·formatter abstraction, 호출 화면의 레이아웃·action 변경. +- **TDD 예외 사유:** 현재 프로젝트에 테스트 번들 타깃이 없고 변경 대상은 분기 없는 I18n 문구 연결이다. +- **대체 검증 방법:** 하드코딩 부재와 I18n 참조를 `rg`로 확인하고, ko/en/ja Simulator에서 두 호출 화면을 수동 확인한다. + +**Files:** + +- Modify: `SodaLive/Sources/I18n/I18n.swift` +- Modify: `SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationRankingSection.swift` +- Modify: `SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationCard.swift` +- Verify: `SodaLive/Sources/V2/CreatorChannel/Donation/CreatorChannelDonationTabView.swift` +- Verify: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelDonationSection.swift` + +- [x] 랭킹 title을 `I18n.Explorer.donationRankingTitle`로 교체한다. +- [x] 전체보기 action 문구를 `I18n.CreatorChannelHome.viewAll`로 교체한다. +- [x] can pill을 `I18n.LiveChat.canWithUnit(_:)`으로 교체한다. +- [x] 빈 메시지용 `I18n.CreatorChannelDonation.defaultMessage(_:)`를 ko/en/ja로 추가하고 공용 카드에서 사용한다. +- [x] 아래 Gate 결과를 검증 기록에 누적한다. + +#### Phase 7 Gate + +```bash +rg -n "I18n\.Explorer\.donationRankingTitle|I18n\.CreatorChannelHome\.viewAll" SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationRankingSection.swift +rg -n "I18n\.LiveChat\.canWithUnit|I18n\.CreatorChannelDonation\.defaultMessage" SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationCard.swift +rg -n "defaultMessage|Donated .* cans|サポートしました" SodaLive/Sources/I18n/I18n.swift +git diff --check +xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build +``` + +**Expected:** 기존 I18n 3개와 신규 fallback 1개가 연결되고 런타임 한국어 하드코딩이 제거되며 `git diff --check`와 Debug 빌드가 성공한다. + +수동 검증: + +- [x] ko/en/ja에서 후원 탭 랭킹 title과 전체보기 action을 확인한다. +- [x] ko/en/ja에서 빈 메시지·메시지 있음 카드를 후원 탭과 홈 탭에서 각각 확인한다. + +- 2026-08-26: `CreatorChannelDonationRankingSection`, `CreatorChannelDonationCard`와 직접 호출 경로를 점검해 `REV-P3-001`, `REV-P3-002`를 확정하고 `P7-R1`을 추가했다. 사용자 요청에 따라 이번 작업에서는 문서만 작성했으며 Swift·I18n 구현과 Gate 실행은 남겨 두었다. +- 2026-08-26: Phase 7 구현을 완료했다. 랭킹 title·전체보기·can pill은 기존 I18n을 재사용하고, 공용 카드의 빈 메시지 fallback만 ko/en/ja로 추가했다. 변경 전 정적 계약은 두 항목 모두 exit 1, 변경 후 Gate와 `git diff --check`는 exit 0, `SodaLive-dev` Debug 빌드는 `BUILD SUCCEEDED`, 독립 리뷰는 `APPROVED`였다. 사용자 요청에 따라 visual QA는 실행하지 않았으므로 실제 화면 ko/en/ja 수동 검증 두 항목은 미완료로 유지한다. +- 2026-08-26: 사용자 수동 테스트 완료 확인에 따라 Phase 7의 ko/en/ja 실제 화면 검증 두 항목을 완료 처리했다. diff --git a/docs/20260704_크리에이터_채널_후원_탭/prd.md b/docs/20260704_크리에이터_채널_후원_탭/prd.md index 034b8612..b04a8fda 100644 --- a/docs/20260704_크리에이터_채널_후원_탭/prd.md +++ b/docs/20260704_크리에이터_채널_후원_탭/prd.md @@ -15,6 +15,7 @@ Figma 참조: - 기존 홈 탭에는 채널 후원 요약 섹션과 후원하기 액션이 있지만, 후원 탭 전용 API, 랭킹 섹션, 스크롤 페이징이 없다. - 후원 후 홈 탭 후원 섹션은 갱신되지만, 후원 탭이 구현되면 후원 탭 목록도 함께 갱신되어야 한다. - 후원 탭 empty 문구와 `후원하기` 버튼이 한국어로 하드코딩되어 앱 언어 설정에 맞게 표시되지 않는다. +- 후원 랭킹 title·전체보기 action과 공용 후원 카드의 can 표기·빈 메시지 fallback이 한국어로 하드코딩되어 영어·일본어 설정에서도 한국어가 노출된다. ## 3. Goals - `CreatorChannelTab.donation` 선택 시 후원 탭 API를 호출하고 응답 데이터로 화면을 구성한다. @@ -30,6 +31,7 @@ Figma 참조: - `donationCount == 0` empty 상태에서는 하단 고정 후원하기 버튼을 숨긴다. - `donationCount != 0 || !rankings.isEmpty`이면 홈 탭 plus button 위치에 icon-only 후원하기 버튼을 표시한다. - 후원 성공 후 후원 탭 데이터와 홈 탭 데이터가 갱신되어야 한다. +- 후원 랭킹과 공용 후원 카드의 고정 UI 문구는 기존 I18n을 우선 재사용하고, 재사용할 수 없는 빈 메시지 fallback만 `I18n.CreatorChannelDonation`에 추가한다. ## 4. Non-Goals - 크리에이터 채널 공통 shell, header, sticky tab-bar 동작을 다시 설계하지 않는다. @@ -92,12 +94,12 @@ data class CreatorChannelDonationResponse( - `rankings`가 있으면 Figma `290:9097` 및 `665:19056` 기준의 카드로 표시한다. - 후원 랭킹은 최대 8명만 표시하고, 한 줄에 4명씩 표시한다. - 카드 배경은 `Color.gray900`, corner radius는 14pt, 내부 padding과 item 간격은 14pt 기준을 사용한다. -- title은 `후원 랭킹`을 표시한다. +- title은 기존 `I18n.Explorer.donationRankingTitle`을 재사용해 한국어 `후원랭킹`, 영어 `Donation ranking`, 일본어 `後援ランキング`을 표시한다. - item은 75pt 원형 프로필, 중앙 정렬 닉네임, 프로필 하단에 rank number overlay를 표시한다. - 랭킹 번호는 응답 배열 순서 기준으로 `index + 1`을 표시한다. - 닉네임은 1줄 말줄임 처리한다. - `profileImage`는 `DownsampledKFImage`로 표시하고, 비어 있거나 실패하면 기존 프로필 placeholder 관례를 따른다. -- Figma의 `전체보기` 버튼을 표시한다. +- Figma의 `전체보기` 버튼은 기존 `I18n.CreatorChannelHome.viewAll`을 재사용한다. - `전체보기` 터치 시 기존 `AppStep.userProfileDonationAll(userId:)`로 이동해 `UserProfileDonationAllView`를 연다. - `userId`에는 현재 크리에이터 채널의 `creatorId`를 전달한다. @@ -109,7 +111,7 @@ data class CreatorChannelDonationResponse( - `createdAtUtc` 상대 시간 - `can` - `message` -- 후원 금액 pill은 기존 can icon과 `{can}캔` 형식을 사용한다. +- 후원 금액 pill은 기존 can icon과 `I18n.LiveChat.canWithUnit(_:)`을 재사용해 ko/en/ja 단위 표기를 적용한다. - 카드 상단 배경색은 홈 탭 후원 카드의 `can` 구간 색상을 재사용한다. - `1...50`: `#E2E2E2` - `51...100`: `#73EE01` @@ -117,7 +119,10 @@ data class CreatorChannelDonationResponse( - `501...`: `#FF4C3C` - `can >= 501`인 후원 item은 상단 영역에서 `nickname`을 white, 상대 시간을 `gray100`으로 표시한다. - 그 외 후원 `can` 구간의 `nickname`과 상대 시간 글자색은 기존 표시 색상을 유지한다. -- `message`가 빈 문자열이면 기존 홈 탭과 동일하게 `{can}캔을 후원하였습니다`를 표시한다. +- `message`가 빈 문자열이면 홈 탭과 후원 탭에서 공용 `I18n.CreatorChannelDonation.defaultMessage(_:)`를 사용한다. + - 한국어: `{can}캔을 후원하였습니다` + - 영어: `Donated {can} cans.` + - 일본어: `{can}canをサポートしました。` - 서버 응답에 비밀 후원 여부가 없으므로, 클라이언트는 서버가 내려준 `nickname`, `profileImageUrl`, `message`를 그대로 표시한다. ### 5.5 Pagination @@ -162,6 +167,8 @@ data class CreatorChannelDonationResponse( - `rankings`가 있으면 `후원 랭킹` 카드에 프로필 그리드와 rank number가 표시된다. - `후원 랭킹` 카드의 `전체보기`를 터치하면 `UserProfileDonationAllView`로 이동한다. - `donations` 목록은 기존 홈 탭 후원 카드와 같은 후원 금액별 상단 컬러, can pill, 상대 시간, 메시지 표시 규칙을 따른다. +- 앱 언어가 ko/en/ja일 때 후원 랭킹 title·전체보기 action·can pill·빈 메시지 fallback이 해당 언어로 표시된다. +- 공용 `CreatorChannelDonationCard`를 사용하는 홈 탭과 후원 탭에서 동일한 다국어 결과가 표시된다. - 목록 하단 도달 시 `hasNext == true`이면 다음 페이지가 append된다. - `rankings.isEmpty && donationCount == 0`이면 Figma `290:9009` empty view가 표시되고, 하단 고정 후원하기 버튼은 숨겨진다. - Empty 문구가 지정된 ko/en/ja 값과 줄바꿈으로 표시되고, 버튼 문구는 기존 `I18n.ContentDetail.DonationDialog.title`을 사용한다. @@ -179,6 +186,8 @@ data class CreatorChannelDonationResponse( - 후원 item UI는 기존 홈 탭 구현과 중복을 줄이기 위해 공용 컴포넌트로 추출하거나, 홈/후원 탭 양쪽에서 같은 하위 컴포넌트를 사용하도록 최소 범위로 조정한다. - 신규 문구가 필요하면 `SodaLive/Sources/I18n/I18n.swift`에 ko/en/ja를 추가한다. - Empty 문구는 `pick(ko:en:ja:)`로 관리하고, 후원 버튼은 기존 `I18n.ContentDetail.DonationDialog.title`을 재사용한다. 신규 후원 버튼 문구, route, API, dependency는 추가하지 않는다. +- 랭킹 title은 `I18n.Explorer.donationRankingTitle`, 전체보기는 `I18n.CreatorChannelHome.viewAll`, can pill은 `I18n.LiveChat.canWithUnit(_:)`을 재사용한다. +- 빈 후원 메시지 fallback만 `I18n.CreatorChannelDonation.defaultMessage(_:)`로 추가하며 신규 dependency나 공용 formatter abstraction은 만들지 않는다. - 프로젝트 설정 변경은 신규 Swift 파일 target 등록이 필요한 경우에만 수행한다. ## 8. Open Questions @@ -190,3 +199,13 @@ data class CreatorChannelDonationResponse( - 2026-07-04: Figma `get_design_context`와 `get_screenshot`으로 empty view `290:9009`를 확인했다. - 2026-07-04: `UserProfileDonationAllView`와 `AppStep.userProfileDonationAll(userId:)` 기존 라우팅을 확인해 랭킹 전체보기 재사용 경계를 정했다. - 2026-08-14: 사용자 인터뷰로 후원 탭 empty 문구의 ko/en/ja 값을 확정했다. `후원하기`는 기존 `I18n.ContentDetail.DonationDialog.title`을 재사용하며 이번 작업에서는 Swift 구현을 수행하지 않는다. +- 2026-08-26: `CreatorChannelDonationRankingSection`, 공용 `CreatorChannelDonationCard`, 두 컴포넌트의 호출 경로를 정적 점검해 런타임 하드코딩 4곳을 확정했다. Preview fixture의 샘플 문구는 런타임 제품 문구가 아니므로 제외했다. +- 2026-08-26: 사용자 인터뷰로 랭킹 title은 기존 `I18n.Explorer.donationRankingTitle`을 재사용하고, 빈 후원 메시지는 카드 전용 ko/en/ja 문구를 추가하기로 결정했다. 이번 작업에서는 문서만 갱신하고 Swift·I18n 구현과 빌드는 수행하지 않는다. +- 2026-08-26: Phase 7의 다국어 요구사항을 구현했다. `§5.3`, `§5.4`, `§6`, `§7`의 랭킹 title·전체보기·can pill·빈 메시지 fallback과 홈·후원 탭 공용 적용을 정적 계약, Debug 빌드, 독립 리뷰로 확인했다. 이 PRD에는 체크박스 항목이 없으며, 구현 체크 상태는 `plan-task.md` Phase 7에 기록했다. 사용자 요청에 따라 visual QA는 실행하지 않았다. + +## 10. Decision Log + +| 날짜 | ID | 상태 | 결정 | 근거 | 영향 범위 | +|---|---|---|---|---|---| +| 2026-08-26 | DEC-DONATION-I18N-001 | 확정 | 랭킹 title은 기존 `I18n.Explorer.donationRankingTitle`을 재사용한다. | 사용자 인터뷰에서 기존 `후원랭킹` 표기를 선택했다. | Ranking section | +| 2026-08-26 | DEC-DONATION-I18N-002 | 확정 | 빈 후원 메시지는 `I18n.CreatorChannelDonation.defaultMessage(_:)` 전용 문구로 관리한다. | 공개 카드에서 `You donated`가 작성자를 오인하게 하므로 사용자 인터뷰에서 전용 문구를 선택했다. | Home·Donation 공용 card | diff --git a/docs/20260704_크리에이터_채널_후원_탭/reviews/phase3-donation-localization-audit.md b/docs/20260704_크리에이터_채널_후원_탭/reviews/phase3-donation-localization-audit.md new file mode 100644 index 00000000..59b6830a --- /dev/null +++ b/docs/20260704_크리에이터_채널_후원_탭/reviews/phase3-donation-localization-audit.md @@ -0,0 +1,181 @@ +# 후원 랭킹·공용 카드 다국어 점검 보고서 + +## 1. 리뷰 정보 + +| 항목 | 내용 | +|---|---| +| 리뷰 대상 | Phase 3 / Task 3.2~3.3 및 직접 호출 경로 | +| 기준 commit 또는 working tree | `e46198f87a1b6a4357f5bb5b5358e8635383b3a6`; 사용자 소유 `project.pbxproj`, `CreatorChannelDonationRankingSection.swift` 변경은 리뷰 범위에서 보존 | +| 리뷰 일자 | 2026-08-26 | +| 리뷰어 | Codex | +| 기준 문서 | `docs/20260704_크리에이터_채널_후원_탭/prd.md`, `docs/20260704_크리에이터_채널_후원_탭/plan-task.md` | +| 리뷰 상태 | 판정 완료, 구현 대기 | + +## 2. 리뷰 목적과 범위 + +### 목적 + +- `CreatorChannelDonationRankingSection`과 `CreatorChannelDonationCard`의 런타임 고정 문구가 앱 언어 설정을 따르는지 확인한다. +- 두 컴포넌트의 직접 호출 경로에 같은 유형의 미적용 문구가 있는지 확인한다. + +### 포함 범위 + +- 코드: `CreatorChannelDonationRankingSection.swift`, `CreatorChannelDonationCard.swift` +- 호출 경로: `CreatorChannelDonationTabView.swift`, `CreatorChannelDonationCountBar.swift`, `CreatorChannelDonationEmptyView.swift`, `CreatorChannelDonationFloatingButton.swift`, `CreatorChannelDonationSection.swift`, `CreatorChannelView.swift` +- 공용 경계: `DateParser.relativeTimeText`, 관련 `I18n` 정의 + +### 제외 범위 + +- `PreviewProvider`의 닉네임·메시지 등 개발용 fixture +- 후원 기능 밖의 크리에이터 채널 섹션과 기존 전역 I18n 문구 품질 +- Swift·I18n 구현, Simulator 실행, 빌드 + +## 3. 판정 기준 + +런타임에서 사용자가 보는 고정 문구가 `I18n`을 거치지 않아 ko/en/ja 설정에 따라 바뀌지 않으면 확정 문제로 판정한다. 서버 데이터, 숫자 순위, URL, asset name과 Preview fixture는 하드코딩 다국어 대상에서 제외한다. + +## 4. 검토한 근거 + +### 문서와 코드 + +- 요구사항: PRD `§5.3`, `§5.4`, `§7` +- 계획: 기존 `Task 3.2`, `Task 3.3`, 신규 `P7-R1` +- 코드: Ranking section의 title·action, 공용 card의 can pill·빈 메시지 fallback +- 호출 관계: 공용 card는 후원 탭과 홈 탭에서 모두 사용한다. + +### 실행 환경 + +```text +OS: macOS 26.0 (25A354) +검증 방식: rg·sed·git diff 기반 정적 점검 +Simulator/빌드: 사용자 지시에 따라 미실행 +``` + +### 실행한 검증 + +| 명령 또는 수동 검증 | 결과 | 핵심 증거 | +|---|---|---| +| `rg -n --glob '*.swift' 'Text\(|I18n\.|relativeTimeText' <대상 및 호출 경로>` | 성공 | 랭킹 2곳, 카드 2곳의 런타임 하드코딩 확인 | +| `rg -n 'CreatorChannelDonationRankingSection\|CreatorChannelDonationCard' SodaLive/Sources` | 성공 | 후원 탭·홈 탭 직접 호출 위치 확인 | +| `sed -n`으로 관련 View와 `DateParser.swift`, `I18n.swift` 확인 | 성공 | count bar·empty·dialog·상대 시간은 이미 I18n 사용 | +| ko/en/ja Simulator 확인 | 불가 | 이번 작업은 문서 전용이며 코드 구현을 금지함 | + +## 5. 발견 사항 요약 + +| ID | 심각도 | 상태 | 제목 | 소유 Task | 후속 goal | +|---|---|---|---|---|---| +| `REV-P3-001` | Medium | 확정 | 랭킹 title과 전체보기 action이 한국어로 고정됨 | `Task 7.1` | `P7-R1` | +| `REV-P3-002` | Medium | 확정 | 공용 카드의 can pill과 빈 메시지가 한국어로 고정됨 | `Task 7.1` | `P7-R1` | + +## 6. 발견 사항 상세 + +### REV-P3-001 — 랭킹 title과 전체보기 action이 한국어로 고정됨 + +- **심각도:** Medium +- **상태:** 확정 +- **관련 요구사항:** PRD `§5.3` +- **관련 계약:** 없음 +- **소유 Task:** `Task 7.1` / `P7-R1` + +**관찰 내용** + +`CreatorChannelDonationRankingSection`의 title과 action label이 문자열 literal을 사용해 영어·일본어 설정에서도 한국어로 표시된다. + +**근거** + +- 코드: `CreatorChannelDonationRankingSection.swift:10`, `:22` +- 기존 I18n: `I18n.Explorer.donationRankingTitle`, `I18n.CreatorChannelHome.viewAll` +- 인터뷰 결정: title은 기존 `후원랭킹` 키를 재사용한다. + +**재현 또는 검증 절차** + +1. 소스에서 `Text("후원랭킹")`, `Text("전체보기")`를 확인한다. +2. 두 값이 `LanguageHeaderProvider.current`를 읽는 `I18n`을 거치지 않는 것을 확인한다. +3. 앱 언어 변경과 무관하게 literal이 유지되는 구조임을 판정한다. + +**영향** + +후원 랭킹이 표시되는 채널에서 영어·일본어 사용자에게 한국어 title과 action이 노출된다. + +**권장 조치** + +기존 `I18n.Explorer.donationRankingTitle`과 `I18n.CreatorChannelHome.viewAll`을 직접 재사용한다. 신규 title·action 키는 추가하지 않는다. + +**판정 기록** + +- 2026-08-26 — 정적 코드와 기존 I18n 정의로 확정. +- 2026-08-26 — 사용자 인터뷰에서 기존 `후원랭킹` 키 재사용을 확정. + +### REV-P3-002 — 공용 카드의 can pill과 빈 메시지가 한국어로 고정됨 + +- **심각도:** Medium +- **상태:** 확정 +- **관련 요구사항:** PRD `§5.4` +- **관련 계약:** 없음 +- **소유 Task:** `Task 7.1` / `P7-R1` + +**관찰 내용** + +`CreatorChannelDonationCard`의 can pill과 빈 `message` fallback이 한국어 literal을 조합한다. 이 카드는 후원 탭과 홈 탭에서 공용이므로 두 화면이 함께 영향을 받는다. + +**근거** + +- 코드: `CreatorChannelDonationCard.swift:60`, `:75` +- 호출: `CreatorChannelDonationTabView.swift:38`, `CreatorChannelDonationSection.swift:38` +- 기존 I18n: `I18n.LiveChat.canWithUnit(_:)`은 동일한 ko/en/ja can 형식을 제공한다. +- 인터뷰 결정: 공개 카드의 영어 fallback은 `Donated {can} cans.` 전용 문구를 사용한다. + +**재현 또는 검증 절차** + +1. 소스에서 `Text("\(donation.can)캔")`과 `"\(can)캔을 후원하였습니다"`를 확인한다. +2. `message`가 공백이면 fallback이 런타임에 표시되는 것을 코드 경로로 확인한다. +3. 공용 카드의 두 호출 위치를 확인한다. + +**영향** + +영어·일본어 사용자에게 can 단위와 빈 메시지가 한국어로 표시되며, 홈 탭과 후원 탭에 동시에 노출된다. + +**권장 조치** + +can pill은 `I18n.LiveChat.canWithUnit(_:)`을 재사용한다. 빈 메시지만 `I18n.CreatorChannelDonation.defaultMessage(_:)`로 추가하고 ko `{can}캔을 후원하였습니다`, en `Donated {can} cans.`, ja `{can}canをサポートしました。`를 제공한다. + +**판정 기록** + +- 2026-08-26 — 정적 코드와 두 직접 호출 위치로 확정. +- 2026-08-26 — 사용자 인터뷰에서 카드 전용 fallback을 확정. + +### 연결 경로 추가 점검 + +- `CreatorChannelDonationCountBar`: `I18n.CreatorChannelCommunity.totalLabel` 사용. +- `CreatorChannelDonationEmptyView`: empty 문구와 후원 action 모두 I18n 사용. +- `CreatorChannelDonationFloatingButton`: 사용자 노출 텍스트가 없는 icon-only 컴포넌트. +- `CreatorChannelDonationSection`: section·empty·후원 action 문구 모두 I18n 사용. +- `CreatorChannelView`: 연결된 후원 dialog 문구는 I18n 사용. +- `DateParser.relativeTimeText`: `I18n.Time`을 사용. + +추가 확정 발견 사항은 없다. + +## 7. 확정 항목의 plan·goal 전환 + +`plan-task.md`에 `Phase 7`, `Task 7.1`, `P7-R1`을 추가했다. 사용자 지시에 따라 goal 생성과 구현은 수행하지 않았다. + +## 8. 리뷰 종료 판정 + +| 판정 항목 | 결과 | 근거 | +|---|---|---| +| 리뷰 범위 전체 확인 | 충족 | 두 대상과 직접 호출·공용 시간 경계 확인 | +| 후보 항목 판정 완료 | 충족 | `REV-P3-001`, `REV-P3-002` 확정 | +| 확정 항목 plan 반영 | 충족 | `Phase 7 / Task 7.1 / P7-R1` | +| 보류 항목의 담당·재개 조건 기록 | 해당 없음 | 인터뷰 결정 완료 | +| 검증 명령과 결과 기록 | 충족 | 정적 점검 명령과 Simulator 미실행 사유 기록 | + +**최종 결론:** 수정 goal 필요 + +**남은 항목:** `P7-R1`의 Swift·I18n 구현과 ko/en/ja Gate 검증. 이번 작업에서는 수행하지 않는다. + +## 9. 수정 후 검증 기록 + +- 2026-08-26 (구현 전): 아직 구현하지 않아 기록 없음. +- 2026-08-26: `REV-P3-001`, `REV-P3-002`를 `Task 7.1`에서 수정했다. 랭킹 title·전체보기 action·can pill은 확정된 기존 I18n을 재사용하고, 공용 카드 빈 메시지 fallback만 `I18n.CreatorChannelDonation.defaultMessage(_:)`로 추가했다. +- 2026-08-26: 변경 전 정적 계약은 랭킹과 카드 모두 exit 1, 변경 후 Phase 7 Gate와 `git diff --check`는 exit 0이었다. `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`는 `BUILD SUCCEEDED`, 독립 리뷰는 `APPROVED`였다. +- 2026-08-26: 사용자 요청에 따라 visual QA를 실행하지 않았다. 따라서 ko/en/ja 실제 화면 수동 검증은 미실행 상태로 `plan-task.md`에 남겼다.