diff --git a/SodaLive/Sources/I18n/I18n.swift b/SodaLive/Sources/I18n/I18n.swift index fe27f7fc..edda681d 100644 --- a/SodaLive/Sources/I18n/I18n.swift +++ b/SodaLive/Sources/I18n/I18n.swift @@ -3427,12 +3427,26 @@ If you block this user, the following features will be restricted. static var currentLive: String { pick(ko: "현재 라이브", en: "Live now", ja: "現在ライブ") } static var latestAudio: String { pick(ko: "최신 오디오 콘텐츠", en: "Latest audio", ja: "最新オーディオ") } static var donation: String { pick(ko: "후원", en: "Donations", ja: "サポート") } + static var donationEmptyMessage: String { + pick( + ko: "처음으로 크리에이터를\n후원해 보세요!", + en: "Be the first to\nsupport this creator!", + ja: "最初のサポートをしてみましょう!" + ) + } static var notice: String { pick(ko: "공지", en: "Notice", ja: "お知らせ") } static var schedule: String { pick(ko: "스케줄", en: "Schedule", ja: "スケジュール") } static var audio: String { pick(ko: "오디오", en: "Audio", ja: "オーディオ") } static var series: String { pick(ko: "시리즈", en: "Series", ja: "シリーズ") } static var community: String { pick(ko: "커뮤니티", en: "Community", ja: "コミュニティ") } static var fanTalk: String { pick(ko: "팬레터", en: "Fan Letter", ja: "ファンレター") } + static var fanTalkEmptyMessage: String { + pick( + ko: "당신의 첫 응원을\n기다리고 있어요!", + en: "We're waiting for\nyour first fan letter!", + ja: "あなたの最初のファンレターを待っています!" + ) + } static var introduce: String { pick(ko: "소개", en: "Introduction", ja: "紹介") } static var activity: String { pick(ko: "활동", en: "Activity", ja: "アクティビティ") } static var sns: String { pick(ko: "SNS", en: "SNS", ja: "SNS") } diff --git a/SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift b/SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift index a06e93e6..4f80c7a4 100644 --- a/SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift +++ b/SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift @@ -382,6 +382,7 @@ struct CreatorChannelView: View { onTapLive: showLiveDetail, onTapContent: showContentDetail, onTapDonate: showDonationDialog, + onTapFanTalkWrite: showFanTalkWriteView, onTapSchedule: handleScheduleTap, onTapSeries: showSeriesDetail, onTapCommunityLike: viewModel.likeCommunityPost, diff --git a/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelDonationSection.swift b/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelDonationSection.swift index ce18af50..f55b5954 100644 --- a/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelDonationSection.swift +++ b/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelDonationSection.swift @@ -78,10 +78,15 @@ private struct CreatorChannelDonationEmptyCard: View { var body: some View { VStack(alignment: .leading, spacing: 0) { - Text("처음으로 크리에이터를\n후원해 보세요!") + Text(I18n.CreatorChannelHome.donationEmptyMessage) .appFont(size: 24, weight: .bold) .foregroundColor(.white) .lineSpacing(0) + .lineLimit(2) + .frame( + maxWidth: LanguageHeaderProvider.current == "ja" ? 210 : .infinity, + alignment: .leading + ) .padding(.top, SodaSpacing.s20) .padding(.leading, SodaSpacing.s20) diff --git a/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelFanTalkSection.swift b/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelFanTalkSection.swift index 0d7d8b0e..1c6156f6 100644 --- a/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelFanTalkSection.swift +++ b/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelFanTalkSection.swift @@ -2,23 +2,78 @@ import SwiftUI struct CreatorChannelFanTalkSection: View { let fanTalk: CreatorChannelFanTalkSummaryResponse + let showsWriteButton: Bool let onSelectTab: (CreatorChannelTab) -> Void + let onTapWrite: () -> Void var body: some View { - if let latestFanTalk = fanTalk.latestFanTalk { - VStack(alignment: .leading, spacing: SodaSpacing.s14) { - SectionTitle(title: I18n.CreatorChannelHome.fanTalk) { - onSelectTab(.fanTalk) - } + VStack(alignment: .leading, spacing: SodaSpacing.s14) { + SectionTitle(title: I18n.CreatorChannelHome.fanTalk) { + onSelectTab(.fanTalk) + } + if let latestFanTalk = fanTalk.latestFanTalk { CreatorChannelFanTalkCard( totalCount: fanTalk.totalCount, latestFanTalk: latestFanTalk ) .padding(.horizontal, SodaSpacing.s14) + } else { + CreatorChannelFanTalkEmptyCard( + showsWriteButton: showsWriteButton, + onTapWrite: onTapWrite + ) + .padding(.horizontal, SodaSpacing.s14) } - .padding(.top, SodaSpacing.s20) } + .padding(.top, SodaSpacing.s20) + } +} + +private struct CreatorChannelFanTalkEmptyCard: View { + let showsWriteButton: Bool + let onTapWrite: () -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + Text(I18n.CreatorChannelHome.fanTalkEmptyMessage) + .appFont(size: 24, weight: .bold) + .foregroundColor(.white) + .lineSpacing(0) + .padding(.top, SodaSpacing.s20) + .padding(.leading, SodaSpacing.s20) + + Spacer(minLength: 0) + + if showsWriteButton { + Button(action: onTapWrite) { + HStack(spacing: SodaSpacing.s6) { + Image("ic_plus_no_bg") + .resizable() + .renderingMode(.template) + .foregroundColor(.black) + .scaledToFit() + .frame(width: 20, height: 20) + + Text(I18n.CreatorChannelFanTalk.writeAction) + .appFont(size: 16, weight: .medium) + .foregroundColor(.black) + .lineLimit(1) + } + .frame(maxWidth: .infinity) + .padding(SodaSpacing.s12) + .background(Color.white) + .clipShape(Capsule()) + } + .buttonStyle(.plain) + .padding(.horizontal, SodaSpacing.s20) + .padding(.bottom, SodaSpacing.s20) + } + } + .frame(height: 196) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color.gray900) + .clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous)) } } @@ -77,7 +132,9 @@ struct CreatorChannelFanTalkSection_Previews: PreviewProvider { createdAtUtc: "2026-07-03T03:44:00Z" ) ), - onSelectTab: { _ in } + showsWriteButton: true, + onSelectTab: { _ in }, + onTapWrite: {} ) .background(Color.black) .previewLayout(.sizeThatFits) diff --git a/SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift b/SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift index 441dc689..603e5144 100644 --- a/SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift +++ b/SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift @@ -7,6 +7,7 @@ struct CreatorChannelHomeView: View { let onTapLive: (Int) -> Void let onTapContent: (Int) -> Void let onTapDonate: () -> Void + let onTapFanTalkWrite: () -> Void let onTapSchedule: (CreatorActivityType, Int) -> Void let onTapSeries: (Int) -> Void let onTapCommunityLike: (Int) -> Void @@ -20,6 +21,7 @@ struct CreatorChannelHomeView: View { onTapLive: @escaping (Int) -> Void = { _ in }, onTapContent: @escaping (Int) -> Void = { _ in }, onTapDonate: @escaping () -> Void = {}, + onTapFanTalkWrite: @escaping () -> Void = {}, onTapSchedule: @escaping (CreatorActivityType, Int) -> Void = { _, _ in }, onTapSeries: @escaping (Int) -> Void = { _ in }, onTapCommunityLike: @escaping (Int) -> Void = { _ in }, @@ -32,6 +34,7 @@ struct CreatorChannelHomeView: View { self.onTapLive = onTapLive self.onTapContent = onTapContent self.onTapDonate = onTapDonate + self.onTapFanTalkWrite = onTapFanTalkWrite self.onTapSchedule = onTapSchedule self.onTapSeries = onTapSeries self.onTapCommunityLike = onTapCommunityLike @@ -90,7 +93,9 @@ struct CreatorChannelHomeView: View { CreatorChannelFanTalkSection( fanTalk: response.fanTalk, - onSelectTab: onSelectTab + showsWriteButton: isOwnCreatorChannel == false, + onSelectTab: onSelectTab, + onTapWrite: onTapFanTalkWrite ) CreatorChannelIntroduceSection(introduce: response.introduce) diff --git a/docs/20260701_크리에이터_채널_홈/plan-task.md b/docs/20260701_크리에이터_채널_홈/plan-task.md index 45056c81..c2a88456 100644 --- a/docs/20260701_크리에이터_채널_홈/plan-task.md +++ b/docs/20260701_크리에이터_채널_홈/plan-task.md @@ -23,7 +23,7 @@ - 여러 화면에서 재사용할 근거가 명확한 UI만 `SodaLive/Sources/V2/Component/**`로 올린다. - 기존 V2 홈의 `TargetType`, Repository, ViewModel, `ApiResponse` 디코딩 패턴을 따른다. - `Long` 계열 id/count는 Swift `Int`로 통일한다. -- 빈 섹션은 section title과 empty state까지 포함해 표시하지 않는다. +- 빈 섹션은 기본적으로 표시하지 않되, 채널 후원과 팬레터는 지정된 다국어 empty state를 표시한다. - API 실패 시 전체 화면 error state로 전환하지 않고 title bar, tab-bar, 홈 외 tab placeholder처럼 서버 데이터 없이 가능한 기본 shell을 유지한다. - `화보` 탭, 화보 섹션, 활동의 화보 row는 구현하지 않는다. - DM 버튼과 DM 진입은 구현하지 않는다. @@ -593,7 +593,7 @@ - 대화하기 액션은 최신 코드 기준처럼 `CreatorChannelHeaderSection` 내부에 유지한다. - `CreatorChannelHomeView`에서 홈 탭 콘텐츠 순서를 최신 코드 기준과 동일하게 조립한다. - 홈 탭 콘텐츠 순서: 현재 라이브, 최신 오디오, 채널 후원, 공지, 스케줄, 오디오, 시리즈, 커뮤니티, 팬Talk, 소개, 활동, SNS. - - 각 섹션은 자신의 데이터가 없으면 숨긴다. +- 각 섹션은 자신의 데이터가 없으면 숨긴다. - API 실패 placeholder 상태에서는 데이터 기반 섹션을 표시하지 않는다. - 검증 기준: - 실행 명령: `rg -n "ZStack|overlay|Color\\.clear|backgroundProgress|CreatorChannelTitleBar|CreatorChannelHeaderSection|CreatorChannelTabBar|CreatorChannelHomeView|CreatorChannelTalkActionSection|CreatorChannelCurrentLiveSection|CreatorChannelLatestAudioSection|CreatorChannelDonationSection|CreatorChannelNoticeSection|CreatorChannelScheduleSection|CreatorChannelAudioSection|CreatorChannelSeriesSection|CreatorChannelCommunitySection|CreatorChannelFanTalkSection|CreatorChannelIntroduceSection|CreatorChannelActivitySection|CreatorChannelSnsSection" SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` @@ -905,3 +905,62 @@ - 2026-08-14: 사용자 요청에 따라 UI 검증은 자동 캡처·자동 시각 리뷰 대신 Task 23.2의 사용자 수동 UI 검토 체크리스트로 수행한다. 체크리스트 확인 전까지 Phase 23 UI 검증은 완료로 표시하지 않는다. - 2026-08-14: Task 23.1 구현과 독립 코드 리뷰를 완료했다. `CreatorChannelView`의 빈 `onTapMore`를 기존 `ProfileReportMenuView`와 세 신고/차단 다이얼로그에 연결했고, `CreatorChannelViewModel`에는 기존 Repository를 사용하는 차단/신고 요청만 추가했다. 차단 성공 callback은 현재 채널의 `dismiss()`만 호출한다. GREEN source contract와 `git diff --check`가 exit 0이었고, `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`에서 `** BUILD SUCCEEDED **`를 확인했다. Task 23.2는 사용자 수동 UI 검토 9개 항목이 남아 있어 미완료로 유지한다. - 2026-08-14: 사용자가 Task 23.2의 수동 UI 검토 9개 항목을 모두 확인했다. 자동 검증과 사용자 수동 검토가 모두 완료되어 Phase 23을 완료 처리했다. + +### Phase 24: 홈 후원·팬레터 empty state 다국어 보강 + +- [x] **Task 24.1: empty state 다국어 문구 정의** + - 대상 파일: + - 수정: `SodaLive/Sources/I18n/I18n.swift` + - 작업 내용: + - 홈 후원 문구를 한국어 `처음으로 크리에이터를\n후원해 보세요!`, 영어 `Be the first to\nsupport this creator!`, 일본어 `最初のサポートをしてみましょう!`로 정의한다. + - 홈 팬레터 문구를 한국어 `당신의 첫 응원을\n기다리고 있어요!`, 영어 `We're waiting for\nyour first fan letter!`, 일본어 `あなたの最初のファンレターを待っています!`로 정의한다. + - 팬레터 작성 버튼을 한국어 `응원남기기`, 영어 `Send a fan letter`, 일본어 `ファンレターを送る`로 정의한다. + - `\n`이 지정된 언어에서만 명시적 줄바꿈을 유지한다. + - 검증 기준: + - 실행 명령: `rg -n "처음으로 크리에이터를|Be the first to|最初のサポート|당신의 첫 응원을|We're waiting for|あなたの最初のファンレター|응원남기기|Send a fan letter|ファンレターを送る" SodaLive/Sources/I18n/I18n.swift` + - 기대 결과: 두 empty state 문구와 팬레터 작성 버튼이 `pick(ko:en:ja:)` 기반으로 확인된다. + +- [x] **Task 24.2: 홈 후원 empty 문구 다국어 적용** + - 대상 파일: + - 수정: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelDonationSection.swift` + - 작업 내용: + - `CreatorChannelDonationEmptyCard`의 한국어 하드코딩 문구를 Task 24.1의 I18n 값으로 교체한다. + - 후원 버튼 문구는 기존 `I18n.ContentDetail.DonationDialog.title`을 유지하고 중복 I18n 키를 만들지 않는다. + - 후원 버튼은 `Color.white` 배경과 black 아이콘·텍스트를 유지한다. + - 검증 기준: + - 실행 명령: `rg -n "CreatorChannelDonationEmptyCard|I18n\.|처음으로 크리에이터를|ContentDetail\.DonationDialog\.title" SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelDonationSection.swift` + - 기대 결과: empty 문구는 I18n을 사용하고 후원 버튼은 기존 title을 재사용하며 한국어 문구가 View에 하드코딩되어 있지 않다. + +- [x] **Task 24.3: 홈 팬레터 empty card와 작성 연결 추가** + - 대상 파일: + - 수정: `SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift` + - 수정: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 수정: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelFanTalkSection.swift` + - 작업 내용: + - `latestFanTalk == nil`일 때 섹션을 숨기지 않고 홈 후원 empty card와 동일한 구조의 팬레터 empty card를 표시한다. + - empty card에 Task 24.1의 문구와 작성 버튼을 표시하고 기존 팬레터 작성 route로 연결한다. + - 내 크리에이터 채널에서는 empty card를 유지하되 작성 버튼을 숨긴다. + - 작성 버튼은 `Color.white` 배경과 black 아이콘·텍스트를 사용한다. + - 신규 route, API, 공용 abstraction을 추가하지 않는다. + - 검증 기준: + - 실행 명령: `rg -n "latestFanTalk|isOwnCreatorChannel|onTapWrite|creatorChannelFanTalkWrite|I18n\." SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelFanTalkSection.swift` + - 기대 결과: 팬레터 empty card, 기존 작성 route 연결, 내 채널 버튼 숨김 조건이 확인된다. + +- [x] **Task 24.4: 정적 검사와 Debug 빌드 검증** + - 대상 파일: + - 확인: `SodaLive/Sources/I18n/I18n.swift` + - 확인: `SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift` + - 확인: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 확인: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelDonationSection.swift` + - 확인: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelFanTalkSection.swift` + - 작업 내용: + - 세 언어 문구, 줄바꿈, 기존 route와 기존 후원 버튼 title 재사용 여부를 정적 검사한다. + - whitespace 오류와 공식 Debug 빌드를 검증한다. + - 검증 기준: + - 실행 명령: `git diff --check` + - 기대 결과: 출력 없이 exit code 0이다. + - 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` + - 기대 결과: `** BUILD SUCCEEDED **`. + +- 2026-08-14: 사용자 인터뷰로 홈 후원·팬레터 empty state의 ko/en/ja 문구, 팬레터 작성 버튼 연결, 내 채널 버튼 숨김, 기존 후원 버튼 I18n 재사용을 확정해 Phase 24에 미완료 후속 Task로 추가했다. 이번 작업은 문서만 보강했으며 Swift 구현과 빌드는 수행하지 않았다. +- 2026-08-14: Phase 24 구현을 완료했다. 홈 후원·팬레터 empty 문구에 ko/en/ja I18n을 적용하고, 팬레터 작성 버튼을 기존 작성 route에 연결했으며 내 채널에서는 버튼을 숨겼다. 홈 두 CTA는 `Color.white` 배경과 black 아이콘·텍스트를 사용한다. 23개 정적 계약, `git diff --check`, iPhone 16 Pro 시뮬레이터 `SodaLive-dev` Debug 빌드가 통과했고, 15개 ko/en/ja 캡처와 두 독립 시각 리뷰에서 차단 항목 없이 PASS를 확인했다. diff --git a/docs/20260701_크리에이터_채널_홈/prd.md b/docs/20260701_크리에이터_채널_홈/prd.md index 662ed6c1..53a4c0c0 100644 --- a/docs/20260701_크리에이터_채널_홈/prd.md +++ b/docs/20260701_크리에이터_채널_홈/prd.md @@ -41,6 +41,7 @@ Figma 전체 화면 기준 공통 shell은 아래를 따른다. - 후속 UI 보정(2026-07-20): 채널 홈 후원 섹션의 카드 너비가 `374pt`로 고정되어 있어 디바이스 너비가 `402pt`보다 작은 화면에서는 기준 디자인과 같은 비율을 유지하지 못한다. - 후속 연결(2026-08-03): 내 채널의 헤더 팔로워 수(`팔로워 n명`)는 표시만 되고 있어, 사용자가 자신을 팔로우하는 유저 목록으로 바로 이동할 수 없다. - 후속 누락(2026-08-14): 다른 크리에이터 채널의 title bar 더보기 버튼은 표시되지만 `CreatorChannelView.titleBar`의 `onTapMore`가 빈 closure라 사용자 차단/신고 메뉴가 열리지 않는다. +- 후속 누락(2026-08-14): 홈 탭의 후원 empty 문구가 한국어로 하드코딩되어 있고, 팬레터가 없으면 섹션 전체가 숨겨져 사용자가 첫 후원·팬레터 작성 방법을 확인할 수 없다. ## 3. Goals - `creatorId`로 `GET /api/v2/creator-channels/{creatorId}/home`을 호출하고 응답 데이터로 크리에이터 채널 공통 shell과 홈 탭 콘텐츠를 구성한다. @@ -56,7 +57,7 @@ Figma 전체 화면 기준 공통 shell은 아래를 따른다. - title bar는 `isFollow`, `isNotify` 상태에 따라 Figma의 Unfollow, Follow+알림 설정, Follow+알림 해제 상태를 표시한다. - 홈 tab 외 tab-bar 항목 및 홈 섹션의 전체보기 destination은 placeholder 형태로 먼저 연결한다. - V2 하위의 재사용 가능한 공용 컴포넌트는 우선 재사용한다. -- 빈 섹션은 section title과 empty state를 표시하지 않고 숨긴다. +- 빈 섹션은 기본적으로 숨기되, 채널 후원과 팬레터는 지정된 다국어 empty state를 표시한다. - 크리에이터 채널의 라이브 예약 스케줄 항목을 누르면 현재 채널 화면 위에 기존 `LiveDetailView`가 즉시 표시되고, 닫으면 같은 채널 화면으로 돌아온다. - `.liveReplay` 스케줄 항목을 누르면 `targetId`를 콘텐츠 ID로 사용해 `.audio`와 동일한 기존 오디오 콘텐츠 상세 페이지로 이동한다. - 이미 종료된 라이브의 `LiveDetailView`를 열면 `이미 종료된 라이브 입니다.` 토스트를 표시하고 `LiveDetailView`/DIM overlay를 닫아 진입 전 화면을 유지한다. @@ -156,7 +157,7 @@ struct CreatorChannelHomeResponse: Decodable { - API 실패 시에는 전체 화면 error state로 전환하지 않고 placeholder 기반으로 일부 UI를 유지한다. - API 실패 placeholder는 title bar, tab-bar, 홈 외 tab placeholder destination처럼 서버 데이터 없이 표시 가능한 공통 shell을 유지하는 방식으로 처리한다. - 선택된 tab 상태와 홈 API 호출 상태는 공통 shell의 ViewModel에서 관리한다. -- 응답을 성공적으로 받은 뒤 각 섹션은 자신의 데이터가 비어 있으면 section title과 empty state를 표시하지 않고 섹션 전체를 숨긴다. +- 응답을 성공적으로 받은 뒤 각 섹션은 자신의 데이터가 비어 있으면 기본적으로 숨긴다. 채널 후원과 팬레터는 예외로 section title과 지정된 empty state를 표시한다. - 동일 화면 생명주기 안에서 불필요한 중복 호출은 피한다. - pull-to-refresh가 기존 크리에이터 상세 화면에 있다면 같은 패턴을 검토한다. 기존 패턴이 없으면 이번 범위에서 새로 추가하지 않는다. @@ -255,7 +256,12 @@ Figma 참조: - `channelDonations`를 사용해 채널 후원 요약 목록을 표시한다. - 각 item은 `nickname`, `profileImageUrl`, `can`, `message`, `createdAtUtc`를 표시한다. -- `channelDonations`가 비어 있으면 채널 후원 섹션을 숨긴다. +- `channelDonations`가 비어 있으면 채널 후원 섹션과 empty card를 표시한다. +- Empty 문구는 아래 언어별 값을 사용한다. `\n`은 명시적 줄바꿈이다. + - 한국어: `처음으로 크리에이터를\n후원해 보세요!` + - 영어: `Be the first to\nsupport this creator!` + - 일본어: `最初のサポートをしてみましょう!` +- Empty card의 후원 버튼 문구는 신규 키를 만들지 않고 기존 `I18n.ContentDetail.DonationDialog.title`을 재사용한다. - 전체보기 action은 기존 `AppStep.channelDonationAll(creatorId:)` 또는 동일한 기존 흐름이 있으면 연결한다. - 기존 흐름 연결이 어렵다면 후원 tab placeholder로 이동한다. - 카드 너비는 기준 디바이스 너비 `402pt`, 최대 카드 너비 `374pt`를 사용한다. @@ -319,7 +325,18 @@ Figma 참조: ### 7.15 팬Talk 섹션 - `fanTalk.totalCount`와 `fanTalk.latestFanTalk`를 사용한다. -- `latestFanTalk == nil`이면 팬Talk 섹션을 숨긴다. +- `latestFanTalk == nil`이면 팬레터 섹션과 후원 empty card와 동일한 구조의 empty card를 표시한다. +- Empty 문구는 아래 언어별 값을 사용한다. `\n`은 명시적 줄바꿈이다. + - 한국어: `당신의 첫 응원을\n기다리고 있어요!` + - 영어: `We're waiting for\nyour first fan letter!` + - 일본어: `あなたの最初のファンレターを待っています!` +- Empty card에는 아래 언어별 팬레터 작성 버튼을 표시한다. + - 한국어: `응원남기기` + - 영어: `Send a fan letter` + - 일본어: `ファンレターを送る` +- 후원·팬레터 empty card의 CTA는 `Color.white` 배경과 black 아이콘·텍스트를 사용한다. +- 팬레터 작성 버튼은 기존 팬레터 작성 화면으로 연결한다. +- 내 크리에이터 채널에서는 empty card를 유지하되 팬레터 작성 버튼을 숨긴다. - 최신 팬Talk가 있으면 `nickname`, `profileImageUrl`, `content`, `languageCode`, `createdAtUtc`를 표시한다. - 전체보기 action은 크리에이터 채널의 팬Talk 탭으로 이동한다. @@ -388,6 +405,8 @@ Figma 참조: - 신규 asset은 프로젝트 asset catalog에 이미 존재하는 `ic_new_talk`을 사용한다. `ic_new_dm`은 추후 DM 버튼 작업에서 사용한다. - title bar 아이콘은 프로젝트 asset catalog의 `ic_new_bar_back`, `ic_new_follow`, `ic_new_following`, `ic_new_more`, `ic_bar_bell`, `ic_bar_bell_colored`를 사용한다. - 신규 문구가 다국어 대상이면 `SodaLive/Sources/I18n/I18n.swift`에 추가한다. +- 이번 후속 empty 문구와 팬레터 작성 버튼은 `pick(ko:en:ja:)`로 관리하며 View에 언어별 문구를 직접 작성하지 않는다. +- 홈 후원 empty 버튼은 기존 `I18n.ContentDetail.DonationDialog.title`을 재사용하고 중복 후원 버튼 문구를 추가하지 않는다. - Figma에서 제공되는 localhost asset URL은 앱 코드에 넣지 않는다. - 프로젝트 설정 변경은 신규 파일이 Xcode project에 자동 포함되지 않는 구조일 때만 수행한다. - Kotlin `Long`으로 정의된 id/count 계열 값은 Swift에서 `Int`로 통일한다. @@ -418,7 +437,13 @@ Figma 참조: - tab-bar 아래 콘텐츠는 선택된 tab에 따라 교체된다. - 홈 tab에는 현재 라이브, 최신 오디오 콘텐츠, 채널 후원, 공지, 스케줄, 오디오, 시리즈, 커뮤니티, 팬Talk, 소개, 활동, SNS 섹션이 API 데이터 기반으로 표시된다. - SNS 섹션은 `ic_sns_*` asset 아이콘과 유효한 SNS URL만 사용해 표시된다. -- 데이터가 비어 있는 섹션은 표시되지 않는다. +- 데이터가 비어 있는 섹션은 기본적으로 표시되지 않지만, 채널 후원과 팬레터는 지정된 empty state를 표시한다. +- 홈 후원 empty 문구가 한국어 `처음으로 크리에이터를\n후원해 보세요!`, 영어 `Be the first to\nsupport this creator!`, 일본어 `最初のサポートをしてみましょう!`로 표시된다. +- 홈 팬레터 empty 문구가 한국어 `당신의 첫 응원을\n기다리고 있어요!`, 영어 `We're waiting for\nyour first fan letter!`, 일본어 `あなたの最初のファンレターを待っています!`로 표시된다. +- 홈 팬레터 empty card의 작성 버튼은 한국어 `응원남기기`, 영어 `Send a fan letter`, 일본어 `ファンレターを送る`로 표시되고 기존 팬레터 작성 화면으로 이동한다. +- 내 크리에이터 채널에서는 홈 팬레터 empty card는 표시되지만 작성 버튼은 표시되지 않는다. +- 홈 후원 empty 버튼은 기존 `I18n.ContentDetail.DonationDialog.title`을 사용한다. +- 홈 후원·팬레터 empty 버튼은 `Color.white` 배경과 black 아이콘·텍스트로 표시된다. - `화보` 탭, 화보 섹션, 활동의 화보 row가 표시되지 않는다. - 홈 외 tab-bar 항목 선택 시 선택한 탭 제목을 표시하는 placeholder page가 표시된다. - 홈 섹션의 전체보기 action은 기존 화면 또는 placeholder destination으로 이동한다. @@ -464,3 +489,4 @@ Figma 참조: - 2026-08-03: 사용자 요청에 따라 내 채널의 헤더 팔로워 수를 기존 레거시 `FollowerListView`로 연결하는 범위를 추가했다. 요청 문구의 `팔로잉 n명`은 현재 UI와 API 계약상 `팔로워 n명`, 즉 해당 채널을 팔로우하는 사용자 목록으로 해석한다. - 2026-08-14: `UserProfileView`가 `ProfileReportMenuView`를 최상위 `ZStack` overlay로 표시하며 `UserBlockConfirmDialogView`, `UserReportDialogView`, `ProfileReportDialogView`에 연결하는 기존 구현을 확인했다. `CreatorChannelView.titleBar`의 빈 `onTapMore`를 같은 흐름에 연결하고 새 BottomSheet View나 `.sheet`를 추가하지 않는 최소 변경으로 요구사항을 확정했다. - 2026-08-14: 사용자 제안에 따라 차단 성공 후 현재 `CreatorChannelView`를 `dismiss()`해 진입 이전 화면으로 돌아가도록 확정했다. 이 동작에서는 차단 후 메뉴를 다시 표시할 필요가 없으므로 `CreatorChannelHomeResponse`에 차단 상태를 추가하거나 강제로 홈 route를 설정하지 않는다. +- 2026-08-14: 사용자 인터뷰로 홈 후원·팬레터 empty state의 한국어·일본어 문구, 자연스러운 영어 번역, 팬레터 작성 버튼 연결, 내 채널에서 작성 버튼 숨김을 확정했다. 기존 후원 버튼 문구는 `I18n.ContentDetail.DonationDialog.title`을 재사용하며 이번 작업에서는 Swift 구현을 수행하지 않는다.