feat(creator-channel): 홈 빈 상태 동작을 보강한다
This commit is contained in:
@@ -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 currentLive: String { pick(ko: "현재 라이브", en: "Live now", ja: "現在ライブ") }
|
||||||
static var latestAudio: String { pick(ko: "최신 오디오 콘텐츠", en: "Latest audio", ja: "最新オーディオ") }
|
static var latestAudio: String { pick(ko: "최신 오디오 콘텐츠", en: "Latest audio", ja: "最新オーディオ") }
|
||||||
static var donation: String { pick(ko: "후원", en: "Donations", 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 notice: String { pick(ko: "공지", en: "Notice", ja: "お知らせ") }
|
||||||
static var schedule: String { pick(ko: "스케줄", en: "Schedule", ja: "スケジュール") }
|
static var schedule: String { pick(ko: "스케줄", en: "Schedule", ja: "スケジュール") }
|
||||||
static var audio: String { pick(ko: "오디오", en: "Audio", ja: "オーディオ") }
|
static var audio: String { pick(ko: "오디오", en: "Audio", ja: "オーディオ") }
|
||||||
static var series: String { pick(ko: "시리즈", en: "Series", ja: "シリーズ") }
|
static var series: String { pick(ko: "시리즈", en: "Series", ja: "シリーズ") }
|
||||||
static var community: String { pick(ko: "커뮤니티", en: "Community", ja: "コミュニティ") }
|
static var community: String { pick(ko: "커뮤니티", en: "Community", ja: "コミュニティ") }
|
||||||
static var fanTalk: String { pick(ko: "팬레터", en: "Fan Letter", 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 introduce: String { pick(ko: "소개", en: "Introduction", ja: "紹介") }
|
||||||
static var activity: String { pick(ko: "활동", en: "Activity", ja: "アクティビティ") }
|
static var activity: String { pick(ko: "활동", en: "Activity", ja: "アクティビティ") }
|
||||||
static var sns: String { pick(ko: "SNS", en: "SNS", ja: "SNS") }
|
static var sns: String { pick(ko: "SNS", en: "SNS", ja: "SNS") }
|
||||||
|
|||||||
@@ -382,6 +382,7 @@ struct CreatorChannelView: View {
|
|||||||
onTapLive: showLiveDetail,
|
onTapLive: showLiveDetail,
|
||||||
onTapContent: showContentDetail,
|
onTapContent: showContentDetail,
|
||||||
onTapDonate: showDonationDialog,
|
onTapDonate: showDonationDialog,
|
||||||
|
onTapFanTalkWrite: showFanTalkWriteView,
|
||||||
onTapSchedule: handleScheduleTap,
|
onTapSchedule: handleScheduleTap,
|
||||||
onTapSeries: showSeriesDetail,
|
onTapSeries: showSeriesDetail,
|
||||||
onTapCommunityLike: viewModel.likeCommunityPost,
|
onTapCommunityLike: viewModel.likeCommunityPost,
|
||||||
|
|||||||
@@ -78,10 +78,15 @@ private struct CreatorChannelDonationEmptyCard: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
Text("처음으로 크리에이터를\n후원해 보세요!")
|
Text(I18n.CreatorChannelHome.donationEmptyMessage)
|
||||||
.appFont(size: 24, weight: .bold)
|
.appFont(size: 24, weight: .bold)
|
||||||
.foregroundColor(.white)
|
.foregroundColor(.white)
|
||||||
.lineSpacing(0)
|
.lineSpacing(0)
|
||||||
|
.lineLimit(2)
|
||||||
|
.frame(
|
||||||
|
maxWidth: LanguageHeaderProvider.current == "ja" ? 210 : .infinity,
|
||||||
|
alignment: .leading
|
||||||
|
)
|
||||||
.padding(.top, SodaSpacing.s20)
|
.padding(.top, SodaSpacing.s20)
|
||||||
.padding(.leading, SodaSpacing.s20)
|
.padding(.leading, SodaSpacing.s20)
|
||||||
|
|
||||||
|
|||||||
@@ -2,23 +2,78 @@ import SwiftUI
|
|||||||
|
|
||||||
struct CreatorChannelFanTalkSection: View {
|
struct CreatorChannelFanTalkSection: View {
|
||||||
let fanTalk: CreatorChannelFanTalkSummaryResponse
|
let fanTalk: CreatorChannelFanTalkSummaryResponse
|
||||||
|
let showsWriteButton: Bool
|
||||||
let onSelectTab: (CreatorChannelTab) -> Void
|
let onSelectTab: (CreatorChannelTab) -> Void
|
||||||
|
let onTapWrite: () -> Void
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
if let latestFanTalk = fanTalk.latestFanTalk {
|
VStack(alignment: .leading, spacing: SodaSpacing.s14) {
|
||||||
VStack(alignment: .leading, spacing: SodaSpacing.s14) {
|
SectionTitle(title: I18n.CreatorChannelHome.fanTalk) {
|
||||||
SectionTitle(title: I18n.CreatorChannelHome.fanTalk) {
|
onSelectTab(.fanTalk)
|
||||||
onSelectTab(.fanTalk)
|
}
|
||||||
}
|
|
||||||
|
|
||||||
|
if let latestFanTalk = fanTalk.latestFanTalk {
|
||||||
CreatorChannelFanTalkCard(
|
CreatorChannelFanTalkCard(
|
||||||
totalCount: fanTalk.totalCount,
|
totalCount: fanTalk.totalCount,
|
||||||
latestFanTalk: latestFanTalk
|
latestFanTalk: latestFanTalk
|
||||||
)
|
)
|
||||||
.padding(.horizontal, SodaSpacing.s14)
|
.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"
|
createdAtUtc: "2026-07-03T03:44:00Z"
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
onSelectTab: { _ in }
|
showsWriteButton: true,
|
||||||
|
onSelectTab: { _ in },
|
||||||
|
onTapWrite: {}
|
||||||
)
|
)
|
||||||
.background(Color.black)
|
.background(Color.black)
|
||||||
.previewLayout(.sizeThatFits)
|
.previewLayout(.sizeThatFits)
|
||||||
|
|||||||
@@ -7,6 +7,7 @@ struct CreatorChannelHomeView: View {
|
|||||||
let onTapLive: (Int) -> Void
|
let onTapLive: (Int) -> Void
|
||||||
let onTapContent: (Int) -> Void
|
let onTapContent: (Int) -> Void
|
||||||
let onTapDonate: () -> Void
|
let onTapDonate: () -> Void
|
||||||
|
let onTapFanTalkWrite: () -> Void
|
||||||
let onTapSchedule: (CreatorActivityType, Int) -> Void
|
let onTapSchedule: (CreatorActivityType, Int) -> Void
|
||||||
let onTapSeries: (Int) -> Void
|
let onTapSeries: (Int) -> Void
|
||||||
let onTapCommunityLike: (Int) -> Void
|
let onTapCommunityLike: (Int) -> Void
|
||||||
@@ -20,6 +21,7 @@ struct CreatorChannelHomeView: View {
|
|||||||
onTapLive: @escaping (Int) -> Void = { _ in },
|
onTapLive: @escaping (Int) -> Void = { _ in },
|
||||||
onTapContent: @escaping (Int) -> Void = { _ in },
|
onTapContent: @escaping (Int) -> Void = { _ in },
|
||||||
onTapDonate: @escaping () -> Void = {},
|
onTapDonate: @escaping () -> Void = {},
|
||||||
|
onTapFanTalkWrite: @escaping () -> Void = {},
|
||||||
onTapSchedule: @escaping (CreatorActivityType, Int) -> Void = { _, _ in },
|
onTapSchedule: @escaping (CreatorActivityType, Int) -> Void = { _, _ in },
|
||||||
onTapSeries: @escaping (Int) -> Void = { _ in },
|
onTapSeries: @escaping (Int) -> Void = { _ in },
|
||||||
onTapCommunityLike: @escaping (Int) -> Void = { _ in },
|
onTapCommunityLike: @escaping (Int) -> Void = { _ in },
|
||||||
@@ -32,6 +34,7 @@ struct CreatorChannelHomeView: View {
|
|||||||
self.onTapLive = onTapLive
|
self.onTapLive = onTapLive
|
||||||
self.onTapContent = onTapContent
|
self.onTapContent = onTapContent
|
||||||
self.onTapDonate = onTapDonate
|
self.onTapDonate = onTapDonate
|
||||||
|
self.onTapFanTalkWrite = onTapFanTalkWrite
|
||||||
self.onTapSchedule = onTapSchedule
|
self.onTapSchedule = onTapSchedule
|
||||||
self.onTapSeries = onTapSeries
|
self.onTapSeries = onTapSeries
|
||||||
self.onTapCommunityLike = onTapCommunityLike
|
self.onTapCommunityLike = onTapCommunityLike
|
||||||
@@ -90,7 +93,9 @@ struct CreatorChannelHomeView: View {
|
|||||||
|
|
||||||
CreatorChannelFanTalkSection(
|
CreatorChannelFanTalkSection(
|
||||||
fanTalk: response.fanTalk,
|
fanTalk: response.fanTalk,
|
||||||
onSelectTab: onSelectTab
|
showsWriteButton: isOwnCreatorChannel == false,
|
||||||
|
onSelectTab: onSelectTab,
|
||||||
|
onTapWrite: onTapFanTalkWrite
|
||||||
)
|
)
|
||||||
|
|
||||||
CreatorChannelIntroduceSection(introduce: response.introduce)
|
CreatorChannelIntroduceSection(introduce: response.introduce)
|
||||||
|
|||||||
@@ -23,7 +23,7 @@
|
|||||||
- 여러 화면에서 재사용할 근거가 명확한 UI만 `SodaLive/Sources/V2/Component/**`로 올린다.
|
- 여러 화면에서 재사용할 근거가 명확한 UI만 `SodaLive/Sources/V2/Component/**`로 올린다.
|
||||||
- 기존 V2 홈의 `TargetType`, Repository, ViewModel, `ApiResponse<T>` 디코딩 패턴을 따른다.
|
- 기존 V2 홈의 `TargetType`, Repository, ViewModel, `ApiResponse<T>` 디코딩 패턴을 따른다.
|
||||||
- `Long` 계열 id/count는 Swift `Int`로 통일한다.
|
- `Long` 계열 id/count는 Swift `Int`로 통일한다.
|
||||||
- 빈 섹션은 section title과 empty state까지 포함해 표시하지 않는다.
|
- 빈 섹션은 기본적으로 표시하지 않되, 채널 후원과 팬레터는 지정된 다국어 empty state를 표시한다.
|
||||||
- API 실패 시 전체 화면 error state로 전환하지 않고 title bar, tab-bar, 홈 외 tab placeholder처럼 서버 데이터 없이 가능한 기본 shell을 유지한다.
|
- API 실패 시 전체 화면 error state로 전환하지 않고 title bar, tab-bar, 홈 외 tab placeholder처럼 서버 데이터 없이 가능한 기본 shell을 유지한다.
|
||||||
- `화보` 탭, 화보 섹션, 활동의 화보 row는 구현하지 않는다.
|
- `화보` 탭, 화보 섹션, 활동의 화보 row는 구현하지 않는다.
|
||||||
- DM 버튼과 DM 진입은 구현하지 않는다.
|
- DM 버튼과 DM 진입은 구현하지 않는다.
|
||||||
@@ -593,7 +593,7 @@
|
|||||||
- 대화하기 액션은 최신 코드 기준처럼 `CreatorChannelHeaderSection` 내부에 유지한다.
|
- 대화하기 액션은 최신 코드 기준처럼 `CreatorChannelHeaderSection` 내부에 유지한다.
|
||||||
- `CreatorChannelHomeView`에서 홈 탭 콘텐츠 순서를 최신 코드 기준과 동일하게 조립한다.
|
- `CreatorChannelHomeView`에서 홈 탭 콘텐츠 순서를 최신 코드 기준과 동일하게 조립한다.
|
||||||
- 홈 탭 콘텐츠 순서: 현재 라이브, 최신 오디오, 채널 후원, 공지, 스케줄, 오디오, 시리즈, 커뮤니티, 팬Talk, 소개, 활동, SNS.
|
- 홈 탭 콘텐츠 순서: 현재 라이브, 최신 오디오, 채널 후원, 공지, 스케줄, 오디오, 시리즈, 커뮤니티, 팬Talk, 소개, 활동, SNS.
|
||||||
- 각 섹션은 자신의 데이터가 없으면 숨긴다.
|
- 각 섹션은 자신의 데이터가 없으면 숨긴다.
|
||||||
- API 실패 placeholder 상태에서는 데이터 기반 섹션을 표시하지 않는다.
|
- 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`
|
- 실행 명령: `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: 사용자 요청에 따라 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.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을 완료 처리했다.
|
- 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를 확인했다.
|
||||||
|
|||||||
@@ -41,6 +41,7 @@ Figma 전체 화면 기준 공통 shell은 아래를 따른다.
|
|||||||
- 후속 UI 보정(2026-07-20): 채널 홈 후원 섹션의 카드 너비가 `374pt`로 고정되어 있어 디바이스 너비가 `402pt`보다 작은 화면에서는 기준 디자인과 같은 비율을 유지하지 못한다.
|
- 후속 UI 보정(2026-07-20): 채널 홈 후원 섹션의 카드 너비가 `374pt`로 고정되어 있어 디바이스 너비가 `402pt`보다 작은 화면에서는 기준 디자인과 같은 비율을 유지하지 못한다.
|
||||||
- 후속 연결(2026-08-03): 내 채널의 헤더 팔로워 수(`팔로워 n명`)는 표시만 되고 있어, 사용자가 자신을 팔로우하는 유저 목록으로 바로 이동할 수 없다.
|
- 후속 연결(2026-08-03): 내 채널의 헤더 팔로워 수(`팔로워 n명`)는 표시만 되고 있어, 사용자가 자신을 팔로우하는 유저 목록으로 바로 이동할 수 없다.
|
||||||
- 후속 누락(2026-08-14): 다른 크리에이터 채널의 title bar 더보기 버튼은 표시되지만 `CreatorChannelView.titleBar`의 `onTapMore`가 빈 closure라 사용자 차단/신고 메뉴가 열리지 않는다.
|
- 후속 누락(2026-08-14): 다른 크리에이터 채널의 title bar 더보기 버튼은 표시되지만 `CreatorChannelView.titleBar`의 `onTapMore`가 빈 closure라 사용자 차단/신고 메뉴가 열리지 않는다.
|
||||||
|
- 후속 누락(2026-08-14): 홈 탭의 후원 empty 문구가 한국어로 하드코딩되어 있고, 팬레터가 없으면 섹션 전체가 숨겨져 사용자가 첫 후원·팬레터 작성 방법을 확인할 수 없다.
|
||||||
|
|
||||||
## 3. Goals
|
## 3. Goals
|
||||||
- `creatorId`로 `GET /api/v2/creator-channels/{creatorId}/home`을 호출하고 응답 데이터로 크리에이터 채널 공통 shell과 홈 탭 콘텐츠를 구성한다.
|
- `creatorId`로 `GET /api/v2/creator-channels/{creatorId}/home`을 호출하고 응답 데이터로 크리에이터 채널 공통 shell과 홈 탭 콘텐츠를 구성한다.
|
||||||
@@ -56,7 +57,7 @@ Figma 전체 화면 기준 공통 shell은 아래를 따른다.
|
|||||||
- title bar는 `isFollow`, `isNotify` 상태에 따라 Figma의 Unfollow, Follow+알림 설정, Follow+알림 해제 상태를 표시한다.
|
- title bar는 `isFollow`, `isNotify` 상태에 따라 Figma의 Unfollow, Follow+알림 설정, Follow+알림 해제 상태를 표시한다.
|
||||||
- 홈 tab 외 tab-bar 항목 및 홈 섹션의 전체보기 destination은 placeholder 형태로 먼저 연결한다.
|
- 홈 tab 외 tab-bar 항목 및 홈 섹션의 전체보기 destination은 placeholder 형태로 먼저 연결한다.
|
||||||
- V2 하위의 재사용 가능한 공용 컴포넌트는 우선 재사용한다.
|
- V2 하위의 재사용 가능한 공용 컴포넌트는 우선 재사용한다.
|
||||||
- 빈 섹션은 section title과 empty state를 표시하지 않고 숨긴다.
|
- 빈 섹션은 기본적으로 숨기되, 채널 후원과 팬레터는 지정된 다국어 empty state를 표시한다.
|
||||||
- 크리에이터 채널의 라이브 예약 스케줄 항목을 누르면 현재 채널 화면 위에 기존 `LiveDetailView`가 즉시 표시되고, 닫으면 같은 채널 화면으로 돌아온다.
|
- 크리에이터 채널의 라이브 예약 스케줄 항목을 누르면 현재 채널 화면 위에 기존 `LiveDetailView`가 즉시 표시되고, 닫으면 같은 채널 화면으로 돌아온다.
|
||||||
- `.liveReplay` 스케줄 항목을 누르면 `targetId`를 콘텐츠 ID로 사용해 `.audio`와 동일한 기존 오디오 콘텐츠 상세 페이지로 이동한다.
|
- `.liveReplay` 스케줄 항목을 누르면 `targetId`를 콘텐츠 ID로 사용해 `.audio`와 동일한 기존 오디오 콘텐츠 상세 페이지로 이동한다.
|
||||||
- 이미 종료된 라이브의 `LiveDetailView`를 열면 `이미 종료된 라이브 입니다.` 토스트를 표시하고 `LiveDetailView`/DIM overlay를 닫아 진입 전 화면을 유지한다.
|
- 이미 종료된 라이브의 `LiveDetailView`를 열면 `이미 종료된 라이브 입니다.` 토스트를 표시하고 `LiveDetailView`/DIM overlay를 닫아 진입 전 화면을 유지한다.
|
||||||
@@ -156,7 +157,7 @@ struct CreatorChannelHomeResponse: Decodable {
|
|||||||
- API 실패 시에는 전체 화면 error state로 전환하지 않고 placeholder 기반으로 일부 UI를 유지한다.
|
- API 실패 시에는 전체 화면 error state로 전환하지 않고 placeholder 기반으로 일부 UI를 유지한다.
|
||||||
- API 실패 placeholder는 title bar, tab-bar, 홈 외 tab placeholder destination처럼 서버 데이터 없이 표시 가능한 공통 shell을 유지하는 방식으로 처리한다.
|
- API 실패 placeholder는 title bar, tab-bar, 홈 외 tab placeholder destination처럼 서버 데이터 없이 표시 가능한 공통 shell을 유지하는 방식으로 처리한다.
|
||||||
- 선택된 tab 상태와 홈 API 호출 상태는 공통 shell의 ViewModel에서 관리한다.
|
- 선택된 tab 상태와 홈 API 호출 상태는 공통 shell의 ViewModel에서 관리한다.
|
||||||
- 응답을 성공적으로 받은 뒤 각 섹션은 자신의 데이터가 비어 있으면 section title과 empty state를 표시하지 않고 섹션 전체를 숨긴다.
|
- 응답을 성공적으로 받은 뒤 각 섹션은 자신의 데이터가 비어 있으면 기본적으로 숨긴다. 채널 후원과 팬레터는 예외로 section title과 지정된 empty state를 표시한다.
|
||||||
- 동일 화면 생명주기 안에서 불필요한 중복 호출은 피한다.
|
- 동일 화면 생명주기 안에서 불필요한 중복 호출은 피한다.
|
||||||
- pull-to-refresh가 기존 크리에이터 상세 화면에 있다면 같은 패턴을 검토한다. 기존 패턴이 없으면 이번 범위에서 새로 추가하지 않는다.
|
- pull-to-refresh가 기존 크리에이터 상세 화면에 있다면 같은 패턴을 검토한다. 기존 패턴이 없으면 이번 범위에서 새로 추가하지 않는다.
|
||||||
|
|
||||||
@@ -255,7 +256,12 @@ Figma 참조:
|
|||||||
|
|
||||||
- `channelDonations`를 사용해 채널 후원 요약 목록을 표시한다.
|
- `channelDonations`를 사용해 채널 후원 요약 목록을 표시한다.
|
||||||
- 각 item은 `nickname`, `profileImageUrl`, `can`, `message`, `createdAtUtc`를 표시한다.
|
- 각 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:)` 또는 동일한 기존 흐름이 있으면 연결한다.
|
- 전체보기 action은 기존 `AppStep.channelDonationAll(creatorId:)` 또는 동일한 기존 흐름이 있으면 연결한다.
|
||||||
- 기존 흐름 연결이 어렵다면 후원 tab placeholder로 이동한다.
|
- 기존 흐름 연결이 어렵다면 후원 tab placeholder로 이동한다.
|
||||||
- 카드 너비는 기준 디바이스 너비 `402pt`, 최대 카드 너비 `374pt`를 사용한다.
|
- 카드 너비는 기준 디바이스 너비 `402pt`, 최대 카드 너비 `374pt`를 사용한다.
|
||||||
@@ -319,7 +325,18 @@ Figma 참조:
|
|||||||
### 7.15 팬Talk 섹션
|
### 7.15 팬Talk 섹션
|
||||||
|
|
||||||
- `fanTalk.totalCount`와 `fanTalk.latestFanTalk`를 사용한다.
|
- `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`를 표시한다.
|
- 최신 팬Talk가 있으면 `nickname`, `profileImageUrl`, `content`, `languageCode`, `createdAtUtc`를 표시한다.
|
||||||
- 전체보기 action은 크리에이터 채널의 팬Talk 탭으로 이동한다.
|
- 전체보기 action은 크리에이터 채널의 팬Talk 탭으로 이동한다.
|
||||||
|
|
||||||
@@ -388,6 +405,8 @@ Figma 참조:
|
|||||||
- 신규 asset은 프로젝트 asset catalog에 이미 존재하는 `ic_new_talk`을 사용한다. `ic_new_dm`은 추후 DM 버튼 작업에서 사용한다.
|
- 신규 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`를 사용한다.
|
- 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`에 추가한다.
|
- 신규 문구가 다국어 대상이면 `SodaLive/Sources/I18n/I18n.swift`에 추가한다.
|
||||||
|
- 이번 후속 empty 문구와 팬레터 작성 버튼은 `pick(ko:en:ja:)`로 관리하며 View에 언어별 문구를 직접 작성하지 않는다.
|
||||||
|
- 홈 후원 empty 버튼은 기존 `I18n.ContentDetail.DonationDialog.title`을 재사용하고 중복 후원 버튼 문구를 추가하지 않는다.
|
||||||
- Figma에서 제공되는 localhost asset URL은 앱 코드에 넣지 않는다.
|
- Figma에서 제공되는 localhost asset URL은 앱 코드에 넣지 않는다.
|
||||||
- 프로젝트 설정 변경은 신규 파일이 Xcode project에 자동 포함되지 않는 구조일 때만 수행한다.
|
- 프로젝트 설정 변경은 신규 파일이 Xcode project에 자동 포함되지 않는 구조일 때만 수행한다.
|
||||||
- Kotlin `Long`으로 정의된 id/count 계열 값은 Swift에서 `Int`로 통일한다.
|
- Kotlin `Long`으로 정의된 id/count 계열 값은 Swift에서 `Int`로 통일한다.
|
||||||
@@ -418,7 +437,13 @@ Figma 참조:
|
|||||||
- tab-bar 아래 콘텐츠는 선택된 tab에 따라 교체된다.
|
- tab-bar 아래 콘텐츠는 선택된 tab에 따라 교체된다.
|
||||||
- 홈 tab에는 현재 라이브, 최신 오디오 콘텐츠, 채널 후원, 공지, 스케줄, 오디오, 시리즈, 커뮤니티, 팬Talk, 소개, 활동, SNS 섹션이 API 데이터 기반으로 표시된다.
|
- 홈 tab에는 현재 라이브, 최신 오디오 콘텐츠, 채널 후원, 공지, 스케줄, 오디오, 시리즈, 커뮤니티, 팬Talk, 소개, 활동, SNS 섹션이 API 데이터 기반으로 표시된다.
|
||||||
- SNS 섹션은 `ic_sns_*` asset 아이콘과 유효한 SNS URL만 사용해 표시된다.
|
- 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가 표시되지 않는다.
|
- `화보` 탭, 화보 섹션, 활동의 화보 row가 표시되지 않는다.
|
||||||
- 홈 외 tab-bar 항목 선택 시 선택한 탭 제목을 표시하는 placeholder page가 표시된다.
|
- 홈 외 tab-bar 항목 선택 시 선택한 탭 제목을 표시하는 placeholder page가 표시된다.
|
||||||
- 홈 섹션의 전체보기 action은 기존 화면 또는 placeholder destination으로 이동한다.
|
- 홈 섹션의 전체보기 action은 기존 화면 또는 placeholder destination으로 이동한다.
|
||||||
@@ -464,3 +489,4 @@ Figma 참조:
|
|||||||
- 2026-08-03: 사용자 요청에 따라 내 채널의 헤더 팔로워 수를 기존 레거시 `FollowerListView`로 연결하는 범위를 추가했다. 요청 문구의 `팔로잉 n명`은 현재 UI와 API 계약상 `팔로워 n명`, 즉 해당 채널을 팔로우하는 사용자 목록으로 해석한다.
|
- 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: `UserProfileView`가 `ProfileReportMenuView`를 최상위 `ZStack` overlay로 표시하며 `UserBlockConfirmDialogView`, `UserReportDialogView`, `ProfileReportDialogView`에 연결하는 기존 구현을 확인했다. `CreatorChannelView.titleBar`의 빈 `onTapMore`를 같은 흐름에 연결하고 새 BottomSheet View나 `.sheet`를 추가하지 않는 최소 변경으로 요구사항을 확정했다.
|
||||||
- 2026-08-14: 사용자 제안에 따라 차단 성공 후 현재 `CreatorChannelView`를 `dismiss()`해 진입 이전 화면으로 돌아가도록 확정했다. 이 동작에서는 차단 후 메뉴를 다시 표시할 필요가 없으므로 `CreatorChannelHomeResponse`에 차단 상태를 추가하거나 강제로 홈 route를 설정하지 않는다.
|
- 2026-08-14: 사용자 제안에 따라 차단 성공 후 현재 `CreatorChannelView`를 `dismiss()`해 진입 이전 화면으로 돌아가도록 확정했다. 이 동작에서는 차단 후 메뉴를 다시 표시할 필요가 없으므로 `CreatorChannelHomeResponse`에 차단 상태를 추가하거나 강제로 홈 route를 설정하지 않는다.
|
||||||
|
- 2026-08-14: 사용자 인터뷰로 홈 후원·팬레터 empty state의 한국어·일본어 문구, 자연스러운 영어 번역, 팬레터 작성 버튼 연결, 내 채널에서 작성 버튼 숨김을 확정했다. 기존 후원 버튼 문구는 `I18n.ContentDetail.DonationDialog.title`을 재사용하며 이번 작업에서는 Swift 구현을 수행하지 않는다.
|
||||||
|
|||||||
Reference in New Issue
Block a user