From 02b56ac6daa178982ee4dc19c3ae74f132077cbc Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Sat, 4 Jul 2026 22:44:48 +0900 Subject: [PATCH] =?UTF-8?q?feat(creator):=20=ED=9B=84=EC=9B=90=20=ED=83=AD?= =?UTF-8?q?=20UI=20=EC=A1=B0=EA=B0=81=EC=9D=84=20=EC=B6=94=EA=B0=80?= =?UTF-8?q?=ED=95=9C=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../CreatorChannelDonationCountBar.swift | 29 +++++ .../CreatorChannelDonationEmptyView.swift | 49 ++++++++ ...CreatorChannelDonationFloatingButton.swift | 39 +++++++ ...CreatorChannelDonationRankingSection.swift | 109 ++++++++++++++++++ 4 files changed, 226 insertions(+) create mode 100644 SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationCountBar.swift create mode 100644 SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationEmptyView.swift create mode 100644 SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationFloatingButton.swift create mode 100644 SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationRankingSection.swift diff --git a/SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationCountBar.swift b/SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationCountBar.swift new file mode 100644 index 00000000..73a83535 --- /dev/null +++ b/SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationCountBar.swift @@ -0,0 +1,29 @@ +import SwiftUI + +struct CreatorChannelDonationCountBar: View { + let donationCount: Int + + var body: some View { + HStack(spacing: SodaSpacing.s4) { + Text("전체") + .appFont(size: 16, weight: .medium) + .foregroundColor(.white) + + Text("\(donationCount)") + .appFont(size: 16, weight: .medium) + .foregroundColor(Color.gray500) + + Spacer() + } + .padding(.horizontal, SodaSpacing.s14) + .frame(height: 52) + .background(Color.black) + } +} + +struct CreatorChannelDonationCountBar_Previews: PreviewProvider { + static var previews: some View { + CreatorChannelDonationCountBar(donationCount: 23) + .previewLayout(.sizeThatFits) + } +} diff --git a/SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationEmptyView.swift b/SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationEmptyView.swift new file mode 100644 index 00000000..013cdfa3 --- /dev/null +++ b/SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationEmptyView.swift @@ -0,0 +1,49 @@ +import SwiftUI + +struct CreatorChannelDonationEmptyView: View { + let showsDonateButton: Bool + let onTapDonate: () -> Void + + var body: some View { + VStack(spacing: SodaSpacing.s20) { + Text("아직 후원이 없습니다.\n처음으로 크리에이터를 후원해 보세요!") + .appFont(size: 16, weight: .medium) + .foregroundColor(Color.gray500) + .multilineTextAlignment(.center) + .lineSpacing(SodaSpacing.s4) + + if showsDonateButton { + Button(action: onTapDonate) { + HStack(spacing: SodaSpacing.s6) { + Image("ic_new_donation") + .resizable() + .renderingMode(.template) + .foregroundColor(.white) + .scaledToFit() + .frame(width: 20, height: 20) + + Text("후원하기") + .appFont(size: 16, weight: .medium) + .foregroundColor(.white) + .lineLimit(1) + } + .padding(.horizontal, SodaSpacing.s20) + .frame(height: 44) + .background(Color.soda400) + .clipShape(Capsule()) + } + .buttonStyle(.plain) + } + } + .frame(maxWidth: .infinity) + .padding(.vertical, 48) + } +} + +struct CreatorChannelDonationEmptyView_Previews: PreviewProvider { + static var previews: some View { + CreatorChannelDonationEmptyView(showsDonateButton: true, onTapDonate: {}) + .background(Color.black) + .previewLayout(.sizeThatFits) + } +} diff --git a/SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationFloatingButton.swift b/SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationFloatingButton.swift new file mode 100644 index 00000000..65b211a9 --- /dev/null +++ b/SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationFloatingButton.swift @@ -0,0 +1,39 @@ +import SwiftUI + +struct CreatorChannelDonationFloatingButton: View { + let action: () -> Void + + var body: some View { + VStack { + Spacer() + + HStack { + Spacer() + + Button(action: action) { + Image("ic_new_donation") + .resizable() + .renderingMode(.template) + .foregroundColor(.white) + .scaledToFit() + .frame(width: 32, height: 32) + .frame(width: 66, height: 66) + .background(Color.soda400) + .clipShape(Capsule()) + } + .buttonStyle(.plain) + .padding(.trailing, SodaSpacing.s14) + } + } + .frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing) + } +} + +struct CreatorChannelDonationFloatingButton_Previews: PreviewProvider { + static var previews: some View { + ZStack { + Color.black.ignoresSafeArea() + CreatorChannelDonationFloatingButton(action: {}) + } + } +} diff --git a/SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationRankingSection.swift b/SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationRankingSection.swift new file mode 100644 index 00000000..d6732564 --- /dev/null +++ b/SodaLive/Sources/V2/CreatorChannel/Donation/Components/CreatorChannelDonationRankingSection.swift @@ -0,0 +1,109 @@ +import SwiftUI + +struct CreatorChannelDonationRankingSection: View { + let rankings: [MemberDonationRankingResponse] + let onTapViewAll: () -> Void + + var body: some View { + if rankings.isEmpty == false { + VStack(alignment: .leading, spacing: SodaSpacing.s14) { + Text("후원 랭킹") + .appFont(size: 20, weight: .bold) + .foregroundColor(.white) + .frame(maxWidth: .infinity, alignment: .leading) + + LazyVGrid(columns: columns, spacing: SodaSpacing.s14) { + ForEach(displayRankings.indices, id: \.self) { rank in + rankingItem(rank: rank, ranking: displayRankings[rank]) + } + } + + Button(action: onTapViewAll) { + Text("전체보기") + .appFont(size: 16, weight: .medium) + .foregroundColor(.white) + .frame(maxWidth: .infinity) + .frame(height: 36) + .overlay( + Capsule() + .stroke(Color.white.opacity(0.3), lineWidth: 1) + ) + .contentShape(Rectangle()) + } + .buttonStyle(.plain) + } + .padding(SodaSpacing.s14) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color.gray900) + .clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous)) + .padding(.horizontal, SodaSpacing.s14) + .padding(.bottom, SodaSpacing.s14) + } + } + + private var displayRankings: [MemberDonationRankingResponse] { + Array(rankings.prefix(8)) + } + + private var columns: [GridItem] { + [ + GridItem(.flexible(), spacing: SodaSpacing.s14), + GridItem(.flexible(), spacing: SodaSpacing.s14), + GridItem(.flexible(), spacing: SodaSpacing.s14), + GridItem(.flexible(), spacing: SodaSpacing.s14) + ] + } + + private func rankingItem(rank: Int, ranking: MemberDonationRankingResponse) -> some View { + VStack(spacing: SodaSpacing.s8) { + GeometryReader { proxy in + let imageSize = min(proxy.size.width, 75) + + ZStack(alignment: .bottom) { + DownsampledKFImage( + url: URL(string: ranking.profileImage), + size: CGSize(width: imageSize, height: imageSize) + ) + .frame(width: imageSize, height: imageSize) + .background(Color.gray800) + .clipShape(Circle()) + + Text("\(rank + 1)") + .appFont(size: 28, weight: .regular, family: .pattaya) + .foregroundColor(.white) + .shadow(color: Color.black.opacity(0.8), radius: 3, x: 0, y: 2) + .shadow(color: Color.black.opacity(0.4), radius: 1, x: 0, y: 0) + .offset(y: SodaSpacing.s4) + .padding(.bottom, -12) + } + .frame(width: imageSize, height: imageSize) + .frame(maxWidth: .infinity, maxHeight: .infinity) + } + .aspectRatio(1, contentMode: .fit) + + Text(ranking.nickname) + .appFont(size: 14, weight: .medium) + .foregroundColor(.white) + .lineLimit(1) + .truncationMode(.tail) + .multilineTextAlignment(.center) + .frame(maxWidth: 75) + } + .frame(maxWidth: .infinity) + } +} + +struct CreatorChannelDonationRankingSection_Previews: PreviewProvider { + static var previews: some View { + CreatorChannelDonationRankingSection( + rankings: [ + MemberDonationRankingResponse(userId: 1, nickname: "첫 번째 팬", profileImage: "https://picsum.photos/120/120?1", donationCan: 100), + MemberDonationRankingResponse(userId: 2, nickname: "두 번째 팬", profileImage: "https://picsum.photos/120/120?2", donationCan: 80), + MemberDonationRankingResponse(userId: 3, nickname: "세 번째 팬", profileImage: "https://picsum.photos/120/120?3", donationCan: 60) + ], + onTapViewAll: {} + ) + .background(Color.black) + .previewLayout(.sizeThatFits) + } +}