From c136f5bbc3bc683df4d3bb3d49f27e1b07bf97df Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Fri, 3 Jul 2026 11:45:53 +0900 Subject: [PATCH] =?UTF-8?q?feat(creator):=20=EC=B1=84=EB=84=90=20=ED=9B=84?= =?UTF-8?q?=EC=9B=90=20=EC=84=B9=EC=85=98=EC=9D=84=20=EC=97=B0=EA=B2=B0?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SodaLive.xcodeproj/project.pbxproj | 8 + SodaLive/Sources/I18n/I18n.swift | 2 +- .../CreatorChannel/CreatorChannelView.swift | 65 ++++- .../CreatorChannelDonationSection.swift | 251 ++++++++++++++++++ .../Home/CreatorChannelHomeView.swift | 11 +- docs/20260701_크리에이터_채널_홈/plan-task.md | 31 ++- 6 files changed, 358 insertions(+), 10 deletions(-) create mode 100644 SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelDonationSection.swift diff --git a/SodaLive.xcodeproj/project.pbxproj b/SodaLive.xcodeproj/project.pbxproj index 51e016f0..b06c5a14 100644 --- a/SodaLive.xcodeproj/project.pbxproj +++ b/SodaLive.xcodeproj/project.pbxproj @@ -28,6 +28,9 @@ CC0101152FF0100000A5BBA3 /* CreatorChannelLatestAudioSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100152FF0100000A5BBA3 /* CreatorChannelLatestAudioSection.swift */; }; CC0102152FF0100000A5BBA3 /* CreatorChannelLatestAudioSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100152FF0100000A5BBA3 /* CreatorChannelLatestAudioSection.swift */; }; CC0103152FF0100000A5BBA3 /* CreatorChannelLatestAudioSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100152FF0100000A5BBA3 /* CreatorChannelLatestAudioSection.swift */; }; + CC0101162FF0100000A5BBA3 /* CreatorChannelDonationSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100162FF0100000A5BBA3 /* CreatorChannelDonationSection.swift */; }; + CC0102162FF0100000A5BBA3 /* CreatorChannelDonationSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100162FF0100000A5BBA3 /* CreatorChannelDonationSection.swift */; }; + CC0103162FF0100000A5BBA3 /* CreatorChannelDonationSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100162FF0100000A5BBA3 /* CreatorChannelDonationSection.swift */; }; CC0101082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100082FF0100000A5BBA3 /* CreatorChannelView.swift */; }; CC0102082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100082FF0100000A5BBA3 /* CreatorChannelView.swift */; }; CC0103082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100082FF0100000A5BBA3 /* CreatorChannelView.swift */; }; @@ -1754,6 +1757,7 @@ CC0100132FF0100000A5BBA3 /* CreatorChannelTabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelTabBar.swift; sourceTree = ""; }; CC0100142FF0100000A5BBA3 /* CreatorChannelCurrentLiveSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelCurrentLiveSection.swift; sourceTree = ""; }; CC0100152FF0100000A5BBA3 /* CreatorChannelLatestAudioSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLatestAudioSection.swift; sourceTree = ""; }; + CC0100162FF0100000A5BBA3 /* CreatorChannelDonationSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelDonationSection.swift; sourceTree = ""; }; 0BBDC3ECBBE049EAA4057D3A /* PostChannelDonationRequest.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PostChannelDonationRequest.swift; sourceTree = ""; }; 0EC05859619BDC03056911E7 /* MainTab.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainTab.swift; sourceTree = ""; }; 189350EA101CF12EEBA6EC1D /* MainTabBarView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainTabBarView.swift; sourceTree = ""; }; @@ -2675,6 +2679,7 @@ CC0100072FF0100000A5BBA3 /* CreatorChannelPlaceholderTabView.swift */, CC0100142FF0100000A5BBA3 /* CreatorChannelCurrentLiveSection.swift */, CC0100152FF0100000A5BBA3 /* CreatorChannelLatestAudioSection.swift */, + CC0100162FF0100000A5BBA3 /* CreatorChannelDonationSection.swift */, ); path = Components; sourceTree = ""; @@ -5611,6 +5616,7 @@ CC0101072FF0100000A5BBA3 /* CreatorChannelPlaceholderTabView.swift in Sources */, CC0101142FF0100000A5BBA3 /* CreatorChannelCurrentLiveSection.swift in Sources */, CC0101152FF0100000A5BBA3 /* CreatorChannelLatestAudioSection.swift in Sources */, + CC0101162FF0100000A5BBA3 /* CreatorChannelDonationSection.swift in Sources */, CC0101082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */, CC0101112FF0100000A5BBA3 /* CreatorChannelTitleBar.swift in Sources */, CC0101122FF0100000A5BBA3 /* CreatorChannelHeaderSection.swift in Sources */, @@ -6410,6 +6416,7 @@ CC0102072FF0100000A5BBA3 /* CreatorChannelPlaceholderTabView.swift in Sources */, CC0102142FF0100000A5BBA3 /* CreatorChannelCurrentLiveSection.swift in Sources */, CC0102152FF0100000A5BBA3 /* CreatorChannelLatestAudioSection.swift in Sources */, + CC0102162FF0100000A5BBA3 /* CreatorChannelDonationSection.swift in Sources */, CC0102082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */, CC0102112FF0100000A5BBA3 /* CreatorChannelTitleBar.swift in Sources */, CC0102122FF0100000A5BBA3 /* CreatorChannelHeaderSection.swift in Sources */, @@ -7225,6 +7232,7 @@ CC0103072FF0100000A5BBA3 /* CreatorChannelPlaceholderTabView.swift in Sources */, CC0103142FF0100000A5BBA3 /* CreatorChannelCurrentLiveSection.swift in Sources */, CC0103152FF0100000A5BBA3 /* CreatorChannelLatestAudioSection.swift in Sources */, + CC0103162FF0100000A5BBA3 /* CreatorChannelDonationSection.swift in Sources */, CC0103082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */, CC0103112FF0100000A5BBA3 /* CreatorChannelTitleBar.swift in Sources */, CC0103122FF0100000A5BBA3 /* CreatorChannelHeaderSection.swift in Sources */, diff --git a/SodaLive/Sources/I18n/I18n.swift b/SodaLive/Sources/I18n/I18n.swift index 28013b0b..7aa83d35 100644 --- a/SodaLive/Sources/I18n/I18n.swift +++ b/SodaLive/Sources/I18n/I18n.swift @@ -3165,7 +3165,7 @@ If you block this user, the following features will be restricted. enum CreatorChannelHome { 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: "Channel donations", ja: "チャンネル応援") } + static var donation: String { pick(ko: "후원", en: "Donations", 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: "オーディオ") } diff --git a/SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift b/SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift index 51dcbf10..b3fe1063 100644 --- a/SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift +++ b/SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift @@ -4,8 +4,10 @@ struct CreatorChannelView: View { let creatorId: Int @StateObject private var viewModel = CreatorChannelViewModel() + @StateObject private var channelDonationViewModel = ChannelDonationViewModel() @Environment(\.dismiss) private var dismiss @State private var tabBarMinY: CGFloat = .greatestFiniteMagnitude + @State private var isShowChannelDonationDialog = false private let titleBarHeight: CGFloat = 56 private let tabBarHeight: CGFloat = 52 @@ -65,6 +67,28 @@ struct CreatorChannelView: View { if viewModel.isLoading { LoadingView() } + + if isShowChannelDonationDialog { + LiveRoomDonationDialogView( + isShowing: $isShowChannelDonationDialog, + isAudioContentDonation: false, + messageLimit: 100, + secretLabel: I18n.MemberChannel.secretDonationLabel, + secretMinimumCanMessage: I18n.MemberChannel.secretDonationMinimumCanMessage, + shouldPrefixSecretInMessagePlaceholder: false, + onClickDonation: { can, message, isSecret in + channelDonationViewModel.postChannelDonation( + can: can, + message: message, + isSecret: isSecret, + reloadAfterSuccess: false, + onSuccess: { + viewModel.fetchHome(creatorId: creatorId) + } + ) + } + ) + } } } .navigationBarBackButtonHidden(true) @@ -74,6 +98,7 @@ struct CreatorChannelView: View { } } .sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 1) + .sodaToast(isPresented: $channelDonationViewModel.isShowPopup, message: channelDonationViewModel.errorMessage, autohideIn: 2) } private func titleBar(backgroundProgress: CGFloat) -> some View { @@ -131,7 +156,8 @@ struct CreatorChannelView: View { CreatorChannelHomeView( response: response, onSelectTab: selectTab, - onTapContent: showContentDetail + onTapContent: showContentDetail, + onTapDonate: showDonationDialog ) } else { CreatorChannelPlaceholderTabView(title: viewModel.selectedTab.title) @@ -158,6 +184,12 @@ struct CreatorChannelView: View { private func showContentDetail(_ contentId: Int) { AppState.shared.setAppStep(step: .contentDetail(contentId: contentId)) } + + private func showDonationDialog() { + guard let creatorId = viewModel.response?.creator.creatorId else { return } + channelDonationViewModel.setCreatorId(creatorId, shouldFetch: false) + isShowChannelDonationDialog = true + } } struct CreatorChannelView_Previews: PreviewProvider { @@ -194,7 +226,36 @@ struct CreatorChannelView_Previews: PreviewProvider { isOwned: false, isRented: false ), - channelDonations: [], + channelDonations: [ + CreatorChannelDonationResponse( + nickname: "팬 이름 50", + profileImageUrl: "https://picsum.photos/120/120?1", + can: 50, + message: "크리에이터가 커뮤니티에 올린 글이 보이는 부분 크리에이터가 커뮤니티에 올린 글이 보이는 부분", + createdAtUtc: "2026-07-03T03:44:00Z" + ), + CreatorChannelDonationResponse( + nickname: "팬 이름 100", + profileImageUrl: "https://picsum.photos/120/120?2", + can: 100, + message: "51~100캔 후원 컬러를 확인하는 Preview 샘플입니다.", + createdAtUtc: "2026-07-03T03:43:00Z" + ), + CreatorChannelDonationResponse( + nickname: "팬 이름 500", + profileImageUrl: "https://picsum.photos/120/120?3", + can: 500, + message: "101~500캔 후원 컬러를 확인하는 Preview 샘플입니다.", + createdAtUtc: "2026-07-03T03:42:00Z" + ), + CreatorChannelDonationResponse( + nickname: "팬 이름 501", + profileImageUrl: "https://picsum.photos/120/120?4", + can: 501, + message: "501캔 이상 후원 컬러를 확인하는 Preview 샘플입니다.", + createdAtUtc: "2026-07-03T03:41:00Z" + ) + ], notices: [], schedules: [], audioContents: [], diff --git a/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelDonationSection.swift b/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelDonationSection.swift new file mode 100644 index 00000000..9eb1f7af --- /dev/null +++ b/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelDonationSection.swift @@ -0,0 +1,251 @@ +import SwiftUI + +struct CreatorChannelDonationSection: View { + let channelDonations: [CreatorChannelDonationResponse] + let onSelectTab: (CreatorChannelTab) -> Void + let onTapDonate: () -> Void + + init( + channelDonations: [CreatorChannelDonationResponse], + onSelectTab: @escaping (CreatorChannelTab) -> Void, + onTapDonate: @escaping () -> Void = {} + ) { + self.channelDonations = channelDonations + self.onSelectTab = onSelectTab + self.onTapDonate = onTapDonate + } + + var body: some View { + VStack(alignment: .leading, spacing: SodaSpacing.s14) { + SectionTitle(title: I18n.CreatorChannelHome.donation) { + onSelectTab(.donation) + } + + if channelDonations.isEmpty { + CreatorChannelDonationEmptyCard(onTapDonate: onTapDonate) + .padding(.horizontal, SodaSpacing.s14) + } else { + ScrollView(.horizontal, showsIndicators: false) { + HStack(alignment: .top, spacing: SodaSpacing.s4) { + ForEach(channelDonations.indices, id: \.self) { index in + CreatorChannelDonationCard(donation: channelDonations[index]) + .frame(width: 374) + } + } + .padding(.leading, SodaSpacing.s14) + .padding(.trailing, SodaSpacing.s14) + } + + Button(action: onTapDonate) { + HStack(spacing: SodaSpacing.s6) { + Image("ic_new_donation") + .resizable() + .renderingMode(.template) + .foregroundColor(.white) + .scaledToFit() + .frame(width: 20, height: 20) + + Text(I18n.ContentDetail.DonationDialog.title) + .appFont(size: 16, weight: .medium) + .foregroundColor(.white) + .lineLimit(1) + } + .frame(maxWidth: .infinity) + .padding(SodaSpacing.s12) + .overlay( + Capsule() + .stroke(Color.white.opacity(0.3), lineWidth: 1) + ) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + .padding(.horizontal, SodaSpacing.s14) + } + } + .padding(.top, SodaSpacing.s20) + } +} + +private struct CreatorChannelDonationEmptyCard: View { + let onTapDonate: () -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 0) { + Text("처음으로 크리에이터를\n후원해 보세요!") + .appFont(size: 24, weight: .bold) + .foregroundColor(.white) + .lineSpacing(0) + .padding(.top, SodaSpacing.s20) + .padding(.leading, SodaSpacing.s20) + + Spacer(minLength: 0) + + Button(action: onTapDonate) { + HStack(spacing: SodaSpacing.s6) { + Image("ic_new_donation") + .resizable() + .renderingMode(.template) + .foregroundColor(.black) + .scaledToFit() + .frame(width: 20, height: 20) + + Text(I18n.ContentDetail.DonationDialog.title) + .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)) + } +} + +private struct CreatorChannelDonationCard: View { + let donation: CreatorChannelDonationResponse + + var body: some View { + VStack(alignment: .leading, spacing: SodaSpacing.s14) { + header + + Text(donation.messageText) + .appFont(size: 16, weight: .regular) + .foregroundColor(.white) + .lineLimit(2) + .truncationMode(.tail) + .frame(maxWidth: .infinity, alignment: .leading) + .padding(.horizontal, SodaSpacing.s14) + } + .padding(.bottom, SodaSpacing.s14) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color.gray900) + .clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous)) + } + + private var header: some View { + HStack(alignment: .center, spacing: SodaSpacing.s8) { + DownsampledKFImage( + url: URL(string: donation.profileImageUrl), + size: CGSize(width: 42, height: 42) + ) + .background(Color.white.opacity(0.6)) + .clipShape(Circle()) + + VStack(alignment: .leading, spacing: 2) { + Text(donation.nickname) + .appFont(size: 14, weight: .medium) + .foregroundColor(.black) + .lineLimit(1) + .truncationMode(.tail) + + Text(donation.relativeTimeText()) + .appFont(size: 14, weight: .regular) + .foregroundColor(Color.gray700) + .lineLimit(1) + } + .frame(maxWidth: .infinity, alignment: .leading) + + canBadge + } + .padding(SodaSpacing.s14) + .background(donation.tierColor) + } + + private var canBadge: some View { + HStack(spacing: 2) { + Image("ic_bar_cash") + .resizable() + .scaledToFit() + .frame(width: 18, height: 18) + + Text("\(donation.can)캔") + .appFont(size: 14, weight: .regular) + .foregroundColor(.white) + .lineLimit(1) + } + .padding(.horizontal, SodaSpacing.s6) + .padding(.vertical, SodaSpacing.s4) + .background(Color.gray900) + .clipShape(Capsule()) + } +} + +private extension CreatorChannelDonationResponse { + var messageText: String { + let trimmedMessage = message.trimmingCharacters(in: .whitespacesAndNewlines) + return trimmedMessage.isEmpty ? "\(can)캔을 후원하였습니다" : trimmedMessage + } + + func relativeTimeText(now: Date = Date()) -> String { + DateParser.relativeTimeText(fromUTC: createdAtUtc, fallback: createdAtUtc, now: now) + } + + var tierColor: Color { + switch can { + case 1...50: + return Color(hex: "E2E2E2") + case 51...100: + return Color(hex: "73EE01") + case 101...500: + return Color(hex: "00EAFF") + default: + return Color(hex: "FF4C3C") + } + } +} + +struct CreatorChannelDonationSection_Previews: PreviewProvider { + static var previews: some View { + VStack(spacing: SodaSpacing.s20) { + CreatorChannelDonationSection( + channelDonations: [], + onSelectTab: { _ in } + ) + + CreatorChannelDonationSection( + channelDonations: [ + CreatorChannelDonationResponse( + nickname: "팬 이름 501", + profileImageUrl: "https://picsum.photos/120/120?4", + can: 501, + message: "501캔 이상 후원 컬러를 확인하는 Preview 샘플입니다.", + createdAtUtc: "2026-07-03T03:41:00Z" + ), + CreatorChannelDonationResponse( + nickname: "팬 이름 50", + profileImageUrl: "https://picsum.photos/120/120?1", + can: 50, + message: "크리에이터가 커뮤니티에 올린 글이 보이는 부분 크리에이터가 커뮤니티에 올린 글이 보이는 부분", + createdAtUtc: "2026-07-03T03:44:00Z" + ), + CreatorChannelDonationResponse( + nickname: "팬 이름 100", + profileImageUrl: "https://picsum.photos/120/120?2", + can: 100, + message: "51~100캔 후원 컬러를 확인하는 Preview 샘플입니다.", + createdAtUtc: "2026-07-03T03:43:00Z" + ), + CreatorChannelDonationResponse( + nickname: "팬 이름 500", + profileImageUrl: "https://picsum.photos/120/120?3", + can: 500, + message: "101~500캔 후원 컬러를 확인하는 Preview 샘플입니다.", + createdAtUtc: "2026-07-03T03:42:00Z" + ) + ], + onSelectTab: { _ in } + ) + } + .background(Color.black) + .previewLayout(.sizeThatFits) + } +} diff --git a/SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift b/SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift index 7f81b883..47c8f5d6 100644 --- a/SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift +++ b/SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift @@ -5,17 +5,20 @@ struct CreatorChannelHomeView: View { let onSelectTab: (CreatorChannelTab) -> Void let onTapLive: (Int) -> Void let onTapContent: (Int) -> Void + let onTapDonate: () -> Void init( response: CreatorChannelHomeResponse, onSelectTab: @escaping (CreatorChannelTab) -> Void, onTapLive: @escaping (Int) -> Void = { _ in }, - onTapContent: @escaping (Int) -> Void = { _ in } + onTapContent: @escaping (Int) -> Void = { _ in }, + onTapDonate: @escaping () -> Void = {} ) { self.response = response self.onSelectTab = onSelectTab self.onTapLive = onTapLive self.onTapContent = onTapContent + self.onTapDonate = onTapDonate } var body: some View { @@ -29,6 +32,12 @@ struct CreatorChannelHomeView: View { latestAudioContent: response.latestAudioContent, onTapContent: onTapContent ) + + CreatorChannelDonationSection( + channelDonations: response.channelDonations, + onSelectTab: onSelectTab, + onTapDonate: onTapDonate + ) } .frame(maxWidth: .infinity, alignment: .leading) .background(Color.black) diff --git a/docs/20260701_크리에이터_채널_홈/plan-task.md b/docs/20260701_크리에이터_채널_홈/plan-task.md index bd5f932d..4f9ec081 100644 --- a/docs/20260701_크리에이터_채널_홈/plan-task.md +++ b/docs/20260701_크리에이터_채널_홈/plan-task.md @@ -367,19 +367,36 @@ ### Phase 7: 채널 후원 섹션 -- [ ] **Task 7.1: 채널 후원 섹션 구현** +- [x] **Task 7.1: 채널 후원 섹션 구현** - 대상 파일: - 생성: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelDonationSection.swift` - 수정: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` - 확인: `SodaLive/Sources/App/AppStep.swift` - 확인: `SodaLive/Sources/ContentView.swift` + - 디자인 기준: + - Figma 후원 아이템: `567:17927`, `https://www.figma.com/design/HmN1yNdJ3EIpqknFL0Hkab/-%EA%B3%B5%EC%9C%A0%EC%9A%A9-%EB%B3%B4%EC%9D%B4%EC%8A%A4%EC%98%A8-UI-UX-%EA%B8%B0%ED%9A%8D%EB%AC%B8%EC%84%9C?node-id=567-17927&m=dev` - 작업 내용: - - `channelDonations.isEmpty`이면 섹션을 표시하지 않는다. - - 각 item의 `nickname`, `profileImageUrl`, `can`, `message`, `createdAtUtc`를 표시한다. - - 전체보기는 기존 `AppStep.channelDonationAll(creatorId:)`로 연결한다. + - `channelDonations.isEmpty`여도 섹션을 숨기지 않고 Figma `290:8820` 기준의 빈 후원 UI를 표시한다. + - 각 item은 Figma 후원 아이템처럼 외곽 card 배경을 `#202020`, corner radius 14, 내부 vertical spacing 14 기준으로 구성한다. + - item 상단의 팬 정보/후원 금액 영역은 `profileImageUrl`, `nickname`, `createdAtUtc`, `can`을 표시한다. + - `profileImageUrl`은 42x42 원형 이미지로 표시하고, 이미지가 없거나 로드 실패 시 기존 프로필 placeholder 패턴을 따른다. + - `nickname`은 14pt medium, `createdAtUtc` 상대 시간은 14pt regular로 표시한다. + - 후원 금액은 우측 pill에 기존 can icon과 `{can}캔` 형식으로 표시한다. + - 팬 이름과 후원 금액을 포함하는 상단 영역의 배경색은 후원 금액 `can`에 따라 다르게 적용한다. + - `1...50`: `#E2E2E2` + - `51...100`: `#73EE01` + - `101...500`: `#00EAFF` + - `501...`: `#FF4C3C` + - 상단 영역 배경이 밝은 색상이므로 `nickname`은 black, `createdAtUtc`는 기존 gray 700 계열로 표시하고, 금액 pill은 `#202020` 배경과 white text를 유지한다. + - `message`는 상단 영역 아래에 16pt regular, white text로 표시하고, 긴 문장은 화면 폭 안에서 줄바꿈한다. + - 전체보기는 기존 `AppStep.channelDonationAll(creatorId:)`로 이동하지 않고, 부모에서 주입받은 `onSelectTab(.donation)` closure로 현재 크리에이터 채널의 후원 tab을 선택한다. + - 빈 후원 UI는 section title 아래 `#202020` card, `처음으로 크리에이터를\n후원해 보세요!` 문구, white capsule `후원하기` 버튼, black `ic_new_donation` icon/text를 표시한다. + - 후원하기 버튼은 기존 `UserProfileView`에서 사용하던 `LiveRoomDonationDialogView`를 그대로 표시한다. + - Dialog 설정은 기존 채널 후원과 동일하게 `isAudioContentDonation: false`, `messageLimit: 100`, `I18n.MemberChannel.secretDonationLabel`, `I18n.MemberChannel.secretDonationMinimumCanMessage`, `shouldPrefixSecretInMessagePlaceholder: false`를 사용한다. + - 후원 전송은 기존 `ChannelDonationViewModel.postChannelDonation`을 재사용하고, 성공 시 현재 채널 홈 API를 다시 호출해 후원 섹션에 즉시 반영한다. - 검증 기준: - - 실행 명령: `rg "struct CreatorChannelDonationSection|channelDonations|CreatorChannelDonationResponse|channelDonationAll|createdAtUtc|can" SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelDonationSection.swift SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` - - 기대 결과: 빈 후원 섹션 숨김과 기존 후원 전체보기 route 연결이 확인된다. + - 실행 명령: `rg "struct CreatorChannelDonationSection|CreatorChannelDonationEmptyCard|처음으로 크리에이터를|ic_new_donation|LiveRoomDonationDialogView|ChannelDonationViewModel|postChannelDonation|fetchHome\(creatorId: creatorId\)|createdAtUtc|can|#E2E2E2|#73EE01|#00EAFF|#FF4C3C|onSelectTab|\\.donation|channelDonationAll" SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelDonationSection.swift` + - 기대 결과: 빈 후원 UI, 후원 금액별 상단 영역 컬러, 기존 후원 Dialog 재사용, 후원 성공 후 홈 API 재조회, 후원 tab 전체보기 연결이 확인되고 신규 구현 경로에서 `channelDonationAll` route를 사용하지 않는다. ### Phase 8: 공지 섹션 @@ -620,6 +637,8 @@ - 2026-07-02: 이미 완료된 Phase 1~2 기록은 보존하고, 후속 구조 정리를 위해 Task 2.4 `공통 CreatorChannel shell로 구조 정리`를 추가했다. - 2026-07-02: 사용자 확인 사항을 반영해 title bar Figma URL 3종과 사용할 아이콘 asset(`ic_new_bar_back`, `ic_new_follow`, `ic_new_following`, `ic_new_more`, `ic_bar_bell`, `ic_bar_bell_fill`)을 Task 3.1 및 최종 회귀 검색 기준에 추가했다. - 2026-07-03: Phase 5 현재 라이브 섹션을 구현했다. Figma `290:8950` 기준으로 LIVE pill, 시작 시간, 제목, 커버 이미지/placeholder, 유료 캔 배지, 성인 표시를 반영하고 `CreatorChannelHomeView`에 `currentLive` optional 기반으로 연결했다. +- 2026-07-03: Phase 7 채널 후원 섹션을 구현했다. Figma `296:14908`, 후원 아이템 `567:17927` 기준으로 후원 금액별 상단 컬러(`1...50` `#E2E2E2`, `51...100` `#73EE01`, `101...500` `#00EAFF`, `501...` `#FF4C3C`), `ic_new_donation` 후원하기 버튼, `.donation` tab 전체보기 전환, Preview 금액 구간별 샘플을 반영했다. `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` 성공으로 신규 파일 target 포함과 컴파일을 검증했다. +- 2026-07-03: 사용자 요청에 따라 Phase 7 후원하기 버튼을 기존 `LiveRoomDonationDialogView`/`ChannelDonationViewModel.postChannelDonation` 흐름으로 연결하고, 성공 시 `CreatorChannelViewModel.fetchHome(creatorId:)`로 현재 채널 홈 데이터를 다시 불러와 후원 섹션에 즉시 반영하도록 계획을 갱신했다. 후원 내역이 없는 경우 Figma `290:8820` 기준의 빈 후원 UI를 표시하도록 Task 7.1에 반영했다. - 2026-07-03: Phase 6 최신 오디오 콘텐츠 섹션을 구현했다. Figma `296:14898`, tag `567:18272`, `567:18274` 기준으로 88px 썸네일, 상단/하단 tag overlay, `New` label, 제목, 재생시간을 반영하고 `latestAudioContent` optional 기반 숨김 및 `AppStep.contentDetail(contentId:)` 상세 진입을 연결했다. `rg` 체크리스트와 `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build` 성공으로 검증했다. - 2026-07-03: Phase 4를 완료해 `CreatorChannelView`에서 홈 탭은 `CreatorChannelHomeView`, 홈 외 탭은 `CreatorChannelPlaceholderTabView(title: selectedTab.title)`로 분기하고, 홈 섹션 전체보기용 tab 선택 closure가 `.audio`, `.series`, `.community`, `.fanTalk`, `.donation`으로 전환되도록 연결했다. - 2026-07-02: 기존 `UserProfile` 구현을 확인해 title bar 팔로우/팔로우 취소/알림 설정/알림 설정 취소를 `UserRepository.creatorFollow(creatorId:follow:notify:)`로 처리하고, `ic_bar_bell`/`ic_bar_bell_fill` 터치 시 각각 `notify: true`/`notify: false` 호출이 일어나도록 계획에 반영했다.