From 32174cd0afcdf5efeb4a173436b566f6a7cb2976 Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Fri, 3 Jul 2026 14:34:27 +0900 Subject: [PATCH] =?UTF-8?q?feat(creator):=20=EC=98=A4=EB=94=94=EC=98=A4=20?= =?UTF-8?q?=EC=84=B9=EC=85=98=EC=9D=84=20=EC=97=B0=EA=B2=B0=ED=95=9C?= =?UTF-8?q?=EB=8B=A4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SodaLive.xcodeproj/project.pbxproj | 16 ++ .../CreatorChannelAudioContentListItem.swift | 157 ++++++++++++++++++ .../CreatorChannelAudioSection.swift | 83 +++++++++ .../CreatorChannelLatestAudioSection.swift | 37 ----- .../Home/CreatorChannelHomeView.swift | 6 + docs/20260701_크리에이터_채널_홈/plan-task.md | 2 +- 6 files changed, 263 insertions(+), 38 deletions(-) create mode 100644 SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelAudioContentListItem.swift create mode 100644 SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelAudioSection.swift diff --git a/SodaLive.xcodeproj/project.pbxproj b/SodaLive.xcodeproj/project.pbxproj index 8ed506b5..c89be573 100644 --- a/SodaLive.xcodeproj/project.pbxproj +++ b/SodaLive.xcodeproj/project.pbxproj @@ -37,6 +37,12 @@ CC0101182FF0100000A5BBA3 /* CreatorChannelScheduleSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100182FF0100000A5BBA3 /* CreatorChannelScheduleSection.swift */; }; CC0102182FF0100000A5BBA3 /* CreatorChannelScheduleSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100182FF0100000A5BBA3 /* CreatorChannelScheduleSection.swift */; }; CC0103182FF0100000A5BBA3 /* CreatorChannelScheduleSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100182FF0100000A5BBA3 /* CreatorChannelScheduleSection.swift */; }; + CC0101192FF0100000A5BBA3 /* CreatorChannelAudioContentListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100192FF0100000A5BBA3 /* CreatorChannelAudioContentListItem.swift */; }; + CC0102192FF0100000A5BBA3 /* CreatorChannelAudioContentListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100192FF0100000A5BBA3 /* CreatorChannelAudioContentListItem.swift */; }; + CC0103192FF0100000A5BBA3 /* CreatorChannelAudioContentListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100192FF0100000A5BBA3 /* CreatorChannelAudioContentListItem.swift */; }; + CC01011A2FF0100000A5BBA3 /* CreatorChannelAudioSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01001A2FF0100000A5BBA3 /* CreatorChannelAudioSection.swift */; }; + CC01021A2FF0100000A5BBA3 /* CreatorChannelAudioSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01001A2FF0100000A5BBA3 /* CreatorChannelAudioSection.swift */; }; + CC01031A2FF0100000A5BBA3 /* CreatorChannelAudioSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01001A2FF0100000A5BBA3 /* CreatorChannelAudioSection.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 */; }; @@ -1766,6 +1772,8 @@ CC0100162FF0100000A5BBA3 /* CreatorChannelDonationSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelDonationSection.swift; sourceTree = ""; }; CC0100172FF0100000A5BBA3 /* CreatorChannelNoticeSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelNoticeSection.swift; sourceTree = ""; }; CC0100182FF0100000A5BBA3 /* CreatorChannelScheduleSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelScheduleSection.swift; sourceTree = ""; }; + CC0100192FF0100000A5BBA3 /* CreatorChannelAudioContentListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelAudioContentListItem.swift; sourceTree = ""; }; + CC01001A2FF0100000A5BBA3 /* CreatorChannelAudioSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelAudioSection.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 = ""; }; @@ -2690,6 +2698,8 @@ CC0100162FF0100000A5BBA3 /* CreatorChannelDonationSection.swift */, CC0100172FF0100000A5BBA3 /* CreatorChannelNoticeSection.swift */, CC0100182FF0100000A5BBA3 /* CreatorChannelScheduleSection.swift */, + CC0100192FF0100000A5BBA3 /* CreatorChannelAudioContentListItem.swift */, + CC01001A2FF0100000A5BBA3 /* CreatorChannelAudioSection.swift */, ); path = Components; sourceTree = ""; @@ -5629,6 +5639,8 @@ CC0101162FF0100000A5BBA3 /* CreatorChannelDonationSection.swift in Sources */, CC0101172FF0100000A5BBA3 /* CreatorChannelNoticeSection.swift in Sources */, CC0101182FF0100000A5BBA3 /* CreatorChannelScheduleSection.swift in Sources */, + CC0101192FF0100000A5BBA3 /* CreatorChannelAudioContentListItem.swift in Sources */, + CC01011A2FF0100000A5BBA3 /* CreatorChannelAudioSection.swift in Sources */, CC0101082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */, CC0101112FF0100000A5BBA3 /* CreatorChannelTitleBar.swift in Sources */, CC0101122FF0100000A5BBA3 /* CreatorChannelHeaderSection.swift in Sources */, @@ -6431,6 +6443,8 @@ CC0102162FF0100000A5BBA3 /* CreatorChannelDonationSection.swift in Sources */, CC0102172FF0100000A5BBA3 /* CreatorChannelNoticeSection.swift in Sources */, CC0102182FF0100000A5BBA3 /* CreatorChannelScheduleSection.swift in Sources */, + CC0102192FF0100000A5BBA3 /* CreatorChannelAudioContentListItem.swift in Sources */, + CC01021A2FF0100000A5BBA3 /* CreatorChannelAudioSection.swift in Sources */, CC0102082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */, CC0102112FF0100000A5BBA3 /* CreatorChannelTitleBar.swift in Sources */, CC0102122FF0100000A5BBA3 /* CreatorChannelHeaderSection.swift in Sources */, @@ -7249,6 +7263,8 @@ CC0103162FF0100000A5BBA3 /* CreatorChannelDonationSection.swift in Sources */, CC0103172FF0100000A5BBA3 /* CreatorChannelNoticeSection.swift in Sources */, CC0103182FF0100000A5BBA3 /* CreatorChannelScheduleSection.swift in Sources */, + CC0103192FF0100000A5BBA3 /* CreatorChannelAudioContentListItem.swift in Sources */, + CC01031A2FF0100000A5BBA3 /* CreatorChannelAudioSection.swift in Sources */, CC0103082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */, CC0103112FF0100000A5BBA3 /* CreatorChannelTitleBar.swift in Sources */, CC0103122FF0100000A5BBA3 /* CreatorChannelHeaderSection.swift in Sources */, diff --git a/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelAudioContentListItem.swift b/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelAudioContentListItem.swift new file mode 100644 index 00000000..c294fdd1 --- /dev/null +++ b/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelAudioContentListItem.swift @@ -0,0 +1,157 @@ +import SwiftUI + +struct CreatorChannelAudioContentListItem: View { + let audioContent: CreatorChannelAudioContentResponse + let action: () -> Void + + init( + audioContent: CreatorChannelAudioContentResponse, + action: @escaping () -> Void = {} + ) { + self.audioContent = audioContent + self.action = action + } + + var body: some View { + Button(action: action) { + HStack(alignment: .center, spacing: SodaSpacing.s14) { + thumbnail + + VStack(alignment: .leading, spacing: 2) { + Text(audioContent.title) + .appFont(size: 16, weight: .bold) + .foregroundColor(.white) + .lineLimit(1) + .truncationMode(.tail) + + if !subtitle.isEmpty { + Text(subtitle) + .appFont(size: 14, weight: .medium) + .foregroundColor(Color.gray500) + .lineLimit(1) + .truncationMode(.tail) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + } + .frame(width: 346, alignment: .leading) + } + .buttonStyle(.plain) + } + + private var subtitle: String { + [audioContent.duration, audioContent.seriesName] + .compactMap { value in + guard let value, !value.isEmpty else { return nil } + return value + } + .joined(separator: " • ") + } + + private var thumbnail: some View { + ZStack(alignment: .topLeading) { + DownsampledKFImage( + url: audioContent.imageUrl.flatMap(URL.init(string:)), + size: CGSize(width: 88, height: 88) + ) + .background(Color.gray800) + + VStack(alignment: .leading, spacing: 0) { + topTagList + + Spacer(minLength: 0) + + bottomTagList + } + } + .frame(width: 88, height: 88) + .clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous)) + } + + @ViewBuilder + private var topTagList: some View { + HStack(spacing: 0) { + if audioContent.isOriginalSeries == true { + CreatorChannelAudioImageTag(imageName: "ic_content_tag_original") + } + + if audioContent.isFirstContent { + CreatorChannelAudioFirstTag() + } + } + } + + @ViewBuilder + private var bottomTagList: some View { + HStack(spacing: 0) { + if audioContent.isPointAvailable { + CreatorChannelAudioImageTag(imageName: "ic_content_tag_point") + } + + if audioContent.price == 0 { + CreatorChannelAudioFreeTag() + } + } + } +} + +struct CreatorChannelAudioImageTag: View { + let imageName: String + + var body: some View { + Image(imageName) + .resizable() + .scaledToFit() + .frame(width: 24, height: 24) + } +} + +struct CreatorChannelAudioFirstTag: View { + var body: some View { + ZStack { + Color(hex: "FF34B8") + + Image("ic_content_tag_first_star") + .resizable() + .scaledToFit() + .padding(SodaSpacing.s4) + } + .frame(width: 24, height: 24) + } +} + +struct CreatorChannelAudioFreeTag: View { + var body: some View { + Text("무료") + .appFont(size: 14, weight: .semibold) + .foregroundColor(.white) + .lineLimit(1) + .padding(SodaSpacing.s4) + .frame(height: 24) + .background(Color.soda900) + } +} + +struct CreatorChannelAudioContentListItem_Previews: PreviewProvider { + static var previews: some View { + CreatorChannelAudioContentListItem( + audioContent: CreatorChannelAudioContentResponse( + audioContentId: 1, + title: "콘텐츠 이름이 한 줄 콘텐츠 이름이 한 줄 콘텐츠 이름이 한 줄", + duration: "3:00", + imageUrl: "https://picsum.photos/300/300", + price: 0, + isAdult: false, + isPointAvailable: true, + isFirstContent: true, + seriesName: "시리즈 이름", + isOriginalSeries: true, + isOwned: false, + isRented: false + ) + ) + .padding(SodaSpacing.s14) + .background(Color.black) + .previewLayout(.sizeThatFits) + } +} diff --git a/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelAudioSection.swift b/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelAudioSection.swift new file mode 100644 index 00000000..782090cf --- /dev/null +++ b/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelAudioSection.swift @@ -0,0 +1,83 @@ +import SwiftUI + +struct CreatorChannelAudioSection: View { + let audioContents: [CreatorChannelAudioContentResponse] + let onSelectTab: (CreatorChannelTab) -> Void + let onTapContent: (Int) -> Void + + private let homeVisibleCount = 9 + private let itemsPerColumn = 3 + + init( + audioContents: [CreatorChannelAudioContentResponse], + onSelectTab: @escaping (CreatorChannelTab) -> Void, + onTapContent: @escaping (Int) -> Void = { _ in } + ) { + self.audioContents = audioContents + self.onSelectTab = onSelectTab + self.onTapContent = onTapContent + } + + var body: some View { + if audioContents.isEmpty == false { + VStack(alignment: .leading, spacing: SodaSpacing.s14) { + SectionTitle(title: I18n.CreatorChannelHome.audio) { + onSelectTab(.audio) + } + + ScrollView(.horizontal, showsIndicators: false) { + HStack(alignment: .top, spacing: SodaSpacing.s8) { + ForEach(audioContentColumns.indices, id: \.self) { index in + VStack(alignment: .leading, spacing: SodaSpacing.s8) { + ForEach(audioContentColumns[index]) { audioContent in + CreatorChannelAudioContentListItem(audioContent: audioContent) { + onTapContent(audioContent.audioContentId) + } + } + } + } + } + .padding(.leading, SodaSpacing.s14) + .padding(.trailing, SodaSpacing.s20) + } + } + .padding(.top, SodaSpacing.s20) + } + } + + private var visibleAudioContents: [CreatorChannelAudioContentResponse] { + Array(audioContents.prefix(homeVisibleCount)) + } + + private var audioContentColumns: [[CreatorChannelAudioContentResponse]] { + stride(from: 0, to: visibleAudioContents.count, by: itemsPerColumn).map { index in + Array(visibleAudioContents[index..