diff --git a/SodaLive.xcodeproj/project.pbxproj b/SodaLive.xcodeproj/project.pbxproj index 4239c65d..3808253c 100644 --- a/SodaLive.xcodeproj/project.pbxproj +++ b/SodaLive.xcodeproj/project.pbxproj @@ -7,6 +7,15 @@ objects = { /* Begin PBXBuildFile section */ + CC0201012FF0200000A5BBA3 /* CreatorChannelLiveReplayListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0200012FF0200000A5BBA3 /* CreatorChannelLiveReplayListItem.swift */; }; + CC0202012FF0200000A5BBA3 /* CreatorChannelLiveReplayListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0200012FF0200000A5BBA3 /* CreatorChannelLiveReplayListItem.swift */; }; + CC0203012FF0200000A5BBA3 /* CreatorChannelLiveReplayListItem.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0200012FF0200000A5BBA3 /* CreatorChannelLiveReplayListItem.swift */; }; + CC0201022FF0200000A5BBA3 /* CreatorChannelLiveStartButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0200022FF0200000A5BBA3 /* CreatorChannelLiveStartButton.swift */; }; + CC0202022FF0200000A5BBA3 /* CreatorChannelLiveStartButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0200022FF0200000A5BBA3 /* CreatorChannelLiveStartButton.swift */; }; + CC0203022FF0200000A5BBA3 /* CreatorChannelLiveStartButton.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0200022FF0200000A5BBA3 /* CreatorChannelLiveStartButton.swift */; }; + CC0201032FF0200000A5BBA3 /* CreatorChannelLiveTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0200032FF0200000A5BBA3 /* CreatorChannelLiveTabView.swift */; }; + CC0202032FF0200000A5BBA3 /* CreatorChannelLiveTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0200032FF0200000A5BBA3 /* CreatorChannelLiveTabView.swift */; }; + CC0203032FF0200000A5BBA3 /* CreatorChannelLiveTabView.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0200032FF0200000A5BBA3 /* CreatorChannelLiveTabView.swift */; }; CC01012A2FF0100000A5BBA3 /* ContentSort.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002A2FF0100000A5BBA3 /* ContentSort.swift */; }; CC01022A2FF0100000A5BBA3 /* ContentSort.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002A2FF0100000A5BBA3 /* ContentSort.swift */; }; CC01032A2FF0100000A5BBA3 /* ContentSort.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002A2FF0100000A5BBA3 /* ContentSort.swift */; }; @@ -1801,6 +1810,9 @@ /* End PBXBuildFile section */ /* Begin PBXFileReference section */ + CC0200012FF0200000A5BBA3 /* CreatorChannelLiveReplayListItem.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveReplayListItem.swift; sourceTree = ""; }; + CC0200022FF0200000A5BBA3 /* CreatorChannelLiveStartButton.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveStartButton.swift; sourceTree = ""; }; + CC0200032FF0200000A5BBA3 /* CreatorChannelLiveTabView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveTabView.swift; sourceTree = ""; }; CC01002A2FF0100000A5BBA3 /* ContentSort.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentSort.swift; sourceTree = ""; }; CC01002B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelSortBar.swift; sourceTree = ""; }; CC01002C2FF0100000A5BBA3 /* CreatorChannelSortBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelSortBottomSheet.swift; sourceTree = ""; }; @@ -2732,6 +2744,15 @@ /* End PBXFrameworksBuildPhase section */ /* Begin PBXGroup section */ + CC0200042FF0200000A5BBA3 /* Components */ = { + isa = PBXGroup; + children = ( + CC0200012FF0200000A5BBA3 /* CreatorChannelLiveReplayListItem.swift */, + CC0200022FF0200000A5BBA3 /* CreatorChannelLiveStartButton.swift */, + ); + path = Components; + sourceTree = ""; + }; CC0100312FF0100000A5BBA3 /* Models */ = { isa = PBXGroup; children = ( @@ -2754,7 +2775,9 @@ children = ( CC0100312FF0100000A5BBA3 /* Models */, CC0100322FF0100000A5BBA3 /* Repository */, + CC0200042FF0200000A5BBA3 /* Components */, CC0100302FF0100000A5BBA3 /* CreatorChannelLiveViewModel.swift */, + CC0200032FF0200000A5BBA3 /* CreatorChannelLiveTabView.swift */, ); path = Live; sourceTree = ""; @@ -5761,6 +5784,9 @@ CC01012E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift in Sources */, CC01012F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift in Sources */, CC0101302FF0100000A5BBA3 /* CreatorChannelLiveViewModel.swift in Sources */, + CC0201012FF0200000A5BBA3 /* CreatorChannelLiveReplayListItem.swift in Sources */, + CC0201022FF0200000A5BBA3 /* CreatorChannelLiveStartButton.swift in Sources */, + CC0201032FF0200000A5BBA3 /* CreatorChannelLiveTabView.swift in Sources */, 409E35082B0759DE00CCB985 /* MoyaProviderExtension.swift in Sources */, 40DDBD772B2B4FD700A2E8DA /* CreatorCommunityMoreItemView.swift in Sources */, 400EA7742A89C42C004992CF /* StartLiveRequest.swift in Sources */, @@ -6580,6 +6606,9 @@ CC01022E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift in Sources */, CC01022F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift in Sources */, CC0102302FF0100000A5BBA3 /* CreatorChannelLiveViewModel.swift in Sources */, + CC0202012FF0200000A5BBA3 /* CreatorChannelLiveReplayListItem.swift in Sources */, + CC0202022FF0200000A5BBA3 /* CreatorChannelLiveStartButton.swift in Sources */, + CC0202032FF0200000A5BBA3 /* CreatorChannelLiveTabView.swift in Sources */, 401B3F092FEEF10000A5BBA3 /* MainHomeRankingViewModel.swift in Sources */, 401A3FAC2FEEAFA000A5BBA3 /* MainHomeView.swift in Sources */, 401A3FAD2FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift in Sources */, @@ -7415,6 +7444,9 @@ CC01032E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift in Sources */, CC01032F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift in Sources */, CC0103302FF0100000A5BBA3 /* CreatorChannelLiveViewModel.swift in Sources */, + CC0203012FF0200000A5BBA3 /* CreatorChannelLiveReplayListItem.swift in Sources */, + CC0203022FF0200000A5BBA3 /* CreatorChannelLiveStartButton.swift in Sources */, + CC0203032FF0200000A5BBA3 /* CreatorChannelLiveTabView.swift in Sources */, 401B3F082FEEF10000A5BBA3 /* MainHomeRankingViewModel.swift in Sources */, 401A3FB02FEEAFA000A5BBA3 /* MainHomeView.swift in Sources */, 401A3FB12FEEAFA000A5BBA3 /* MainHomeRecommendationView.swift in Sources */, diff --git a/SodaLive/Resources/Assets.xcassets/v2/ic_new_player_play.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/v2/ic_new_player_play.imageset/Contents.json new file mode 100644 index 00000000..db63238a --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/v2/ic_new_player_play.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_new_player_play.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/v2/ic_new_player_play.imageset/ic_new_player_play.png b/SodaLive/Resources/Assets.xcassets/v2/ic_new_player_play.imageset/ic_new_player_play.png new file mode 100644 index 00000000..ba1303c3 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/v2/ic_new_player_play.imageset/ic_new_player_play.png differ diff --git a/SodaLive/Resources/Assets.xcassets/v2/ic_new_shield_large.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/v2/ic_new_shield_large.imageset/Contents.json new file mode 100644 index 00000000..52b5a483 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/v2/ic_new_shield_large.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_new_shield_large.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/v2/ic_new_shield_large.imageset/ic_new_shield_large.png b/SodaLive/Resources/Assets.xcassets/v2/ic_new_shield_large.imageset/ic_new_shield_large.png new file mode 100644 index 00000000..e9bdca3b Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/v2/ic_new_shield_large.imageset/ic_new_shield_large.png differ diff --git a/SodaLive/Resources/Assets.xcassets/v2/ic_new_shield_small.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/v2/ic_new_shield_small.imageset/Contents.json new file mode 100644 index 00000000..88e33bc0 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/v2/ic_new_shield_small.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_new_shield_small.png", + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/v2/ic_new_shield_small.imageset/ic_new_shield_small.png b/SodaLive/Resources/Assets.xcassets/v2/ic_new_shield_small.imageset/ic_new_shield_small.png new file mode 100644 index 00000000..fe0a42b7 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/v2/ic_new_shield_small.imageset/ic_new_shield_small.png differ diff --git a/SodaLive/Sources/I18n/I18n.swift b/SodaLive/Sources/I18n/I18n.swift index 1da41569..f9e93713 100644 --- a/SodaLive/Sources/I18n/I18n.swift +++ b/SodaLive/Sources/I18n/I18n.swift @@ -3164,6 +3164,7 @@ If you block this user, the following features will be restricted. enum CreatorChannelLive { static var totalLabel: String { pick(ko: "전체", en: "Total", ja: "全体") } + static var startLive: String { pick(ko: "라이브 시작하기", en: "Start live", ja: "ライブを開始") } enum Sort { static var latest: String { pick(ko: "최신순", en: "Latest", ja: "最新順") } static var popular: String { pick(ko: "인기순", en: "Popular", ja: "人気順") } diff --git a/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelAudioContentListItem.swift b/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelAudioContentListItem.swift index c294fdd1..8969c0c6 100644 --- a/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelAudioContentListItem.swift +++ b/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelAudioContentListItem.swift @@ -3,7 +3,7 @@ import SwiftUI struct CreatorChannelAudioContentListItem: View { let audioContent: CreatorChannelAudioContentResponse let action: () -> Void - + init( audioContent: CreatorChannelAudioContentResponse, action: @escaping () -> Void = {} @@ -11,19 +11,19 @@ struct CreatorChannelAudioContentListItem: View { 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) @@ -38,7 +38,7 @@ struct CreatorChannelAudioContentListItem: View { } .buttonStyle(.plain) } - + private var subtitle: String { [audioContent.duration, audioContent.seriesName] .compactMap { value in @@ -47,7 +47,7 @@ struct CreatorChannelAudioContentListItem: View { } .joined(separator: " • ") } - + private var thumbnail: some View { ZStack(alignment: .topLeading) { DownsampledKFImage( @@ -55,39 +55,39 @@ struct CreatorChannelAudioContentListItem: View { 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() } @@ -97,7 +97,7 @@ struct CreatorChannelAudioContentListItem: View { struct CreatorChannelAudioImageTag: View { let imageName: String - + var body: some View { Image(imageName) .resizable() @@ -110,7 +110,7 @@ struct CreatorChannelAudioFirstTag: View { var body: some View { ZStack { Color(hex: "FF34B8") - + Image("ic_content_tag_first_star") .resizable() .scaledToFit() @@ -120,6 +120,21 @@ struct CreatorChannelAudioFirstTag: View { } } +struct CreatorChannelAudioAdultTag: View { + var body: some View { + ZStack { + Color(hex: "FF4C3C") + + Image("ic_new_shield_small") + .resizable() + .scaledToFit() + .padding(2) + } + .frame(width: 18, height: 18) + .cornerRadius(4) + } +} + struct CreatorChannelAudioFreeTag: View { var body: some View { Text("무료") diff --git a/SodaLive/Sources/V2/CreatorChannel/Live/Components/CreatorChannelLiveReplayListItem.swift b/SodaLive/Sources/V2/CreatorChannel/Live/Components/CreatorChannelLiveReplayListItem.swift new file mode 100644 index 00000000..1bda0252 --- /dev/null +++ b/SodaLive/Sources/V2/CreatorChannel/Live/Components/CreatorChannelLiveReplayListItem.swift @@ -0,0 +1,221 @@ +import SwiftUI + +struct CreatorChannelLiveReplayListItem: 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() + } label: { + HStack(alignment: .center, spacing: SodaSpacing.s14) { + thumbnail + + VStack(alignment: .leading, spacing: SodaSpacing.s6) { + Text(audioContent.title) + .appFont(size: 16, weight: .bold) + .foregroundColor(.white) + .lineLimit(2) + .truncationMode(.tail) + + if let duration = audioContent.duration, !duration.isEmpty { + Text(duration) + .appFont(size: 14, weight: .medium) + .foregroundColor(Color.gray500) + .lineLimit(1) + } + } + .frame(maxWidth: .infinity, alignment: .leading) + + trailingState + } + .padding(.horizontal, SodaSpacing.s20) + .padding(.vertical, SodaSpacing.s8) + .frame(maxWidth: .infinity, alignment: .leading) + } + .buttonStyle(.plain) + } + + 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() + } + + Spacer() + + if audioContent.isAdult { + CreatorChannelAudioAdultTag() + .padding(.top, 6) + .padding(.trailing, 6) + } + } + } + + @ViewBuilder + private var bottomTagList: some View { + HStack(spacing: 0) { + if audioContent.isPointAvailable { + CreatorChannelAudioImageTag(imageName: "ic_content_tag_point") + } + + if audioContent.price == 0 { + CreatorChannelAudioFreeTag() + } + } + } + + @ViewBuilder + private var trailingState: some View { + if audioContent.isOwned { + playbackState(I18n.Content.Status.owned) + } else if audioContent.isRented { + playbackState(I18n.Content.Status.rented) + } else if audioContent.price == 0 { + playbackState("") + } else { + HStack(spacing: 2) { + Image("ic_bar_cash") + .resizable() + .scaledToFit() + .frame(width: 18, height: 18) + + Text(audioContent.price.comma()) + .appFont(size: 14, weight: .regular) + .foregroundColor(.white) + .lineLimit(1) + } + } + } + + private func playbackState(_ title: String) -> some View { + VStack(spacing: SodaSpacing.s4) { + ZStack { + Color.white + + Image("ic_new_player_play") + .resizable() + .scaledToFit() + .frame(width: 16, height: 16) + } + .frame(width: 28, height: 28) + .clipShape(Circle()) + + if !title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + Text(title) + .appFont(size: 12, weight: .medium) + .foregroundColor(Color.gray400) + .lineLimit(1) + } + } + } +} + +struct CreatorChannelLiveReplayListItem_Previews: PreviewProvider { + static var previews: some View { + VStack(spacing: SodaSpacing.s8) { + CreatorChannelLiveReplayListItem( + audioContent: CreatorChannelAudioContentResponse( + audioContentId: 1, + title: "라이브 다시듣기 제목이 두 줄까지 표시됩니다", + duration: "1:43:25", + imageUrl: "https://picsum.photos/300/300", + price: 10, + isAdult: true, + isPointAvailable: true, + isFirstContent: true, + seriesName: nil, + isOriginalSeries: true, + isOwned: true, + isRented: false + ) + ) + + CreatorChannelLiveReplayListItem( + audioContent: CreatorChannelAudioContentResponse( + audioContentId: 1, + title: "라이브 다시듣기 제목이 두 줄까지 표시됩니다", + duration: "1:43:25", + imageUrl: "https://picsum.photos/300/300", + price: 10, + isAdult: true, + isPointAvailable: true, + isFirstContent: true, + seriesName: nil, + isOriginalSeries: true, + isOwned: false, + isRented: true + ) + ) + + CreatorChannelLiveReplayListItem( + audioContent: CreatorChannelAudioContentResponse( + audioContentId: 1, + title: "라이브 다시듣기 제목이 두 줄까지 표시됩니다", + duration: "1:43:25", + imageUrl: "https://picsum.photos/300/300", + price: 3000, + isAdult: true, + isPointAvailable: true, + isFirstContent: true, + seriesName: nil, + isOriginalSeries: true, + isOwned: false, + isRented: false + ) + ) + + CreatorChannelLiveReplayListItem( + audioContent: CreatorChannelAudioContentResponse( + audioContentId: 1, + title: "라이브 다시듣기 제목이 두 줄까지 표시됩니다", + duration: "1:43:25", + imageUrl: "https://picsum.photos/300/300", + price: 0, + isAdult: true, + isPointAvailable: true, + isFirstContent: true, + seriesName: nil, + isOriginalSeries: true, + isOwned: false, + isRented: false + ) + ) + } + .background(Color.black) + .previewLayout(.sizeThatFits) + } +} diff --git a/SodaLive/Sources/V2/CreatorChannel/Live/Components/CreatorChannelLiveStartButton.swift b/SodaLive/Sources/V2/CreatorChannel/Live/Components/CreatorChannelLiveStartButton.swift new file mode 100644 index 00000000..4b36058c --- /dev/null +++ b/SodaLive/Sources/V2/CreatorChannel/Live/Components/CreatorChannelLiveStartButton.swift @@ -0,0 +1,35 @@ +import SwiftUI + +struct CreatorChannelLiveStartButton: View { + let action: () -> Void + + var body: some View { + Button(action: action) { + HStack(spacing: SodaSpacing.s6) { + Image("ic_new_create_live") + .resizable() + .scaledToFit() + .frame(width: 24, height: 24) + + Text(I18n.CreatorChannelLive.startLive) + .appFont(size: 16, weight: .bold) + .foregroundColor(.white) + .lineLimit(1) + } + .frame(maxWidth: .infinity) + .frame(height: 56) + .background(Color.soda400) + .clipShape(Capsule()) + } + .buttonStyle(.plain) + } +} + +struct CreatorChannelLiveStartButton_Previews: PreviewProvider { + static var previews: some View { + CreatorChannelLiveStartButton(action: {}) + .padding(SodaSpacing.s20) + .background(Color.black) + .previewLayout(.sizeThatFits) + } +} diff --git a/SodaLive/Sources/V2/CreatorChannel/Live/CreatorChannelLiveTabView.swift b/SodaLive/Sources/V2/CreatorChannel/Live/CreatorChannelLiveTabView.swift new file mode 100644 index 00000000..311c0725 --- /dev/null +++ b/SodaLive/Sources/V2/CreatorChannel/Live/CreatorChannelLiveTabView.swift @@ -0,0 +1,87 @@ +import SwiftUI + +struct CreatorChannelLiveTabView: View { + let creatorId: Int + let isOwnCreatorChannel: Bool + let onTapLive: (Int) -> Void + let onTapContent: (Int) -> Void + let onTapCreateLive: () -> Void + + @StateObject private var viewModel = CreatorChannelLiveViewModel() + @State private var isSortSheetPresented = false + + var body: some View { + ZStack(alignment: .bottom) { + content + + if isOwnCreatorChannel { + CreatorChannelLiveStartButton(action: onTapCreateLive) + .padding(.horizontal, SodaSpacing.s20) + .padding(.bottom, SodaSpacing.s20) + } + } + .background(Color.black) + .onAppear { + if viewModel.hasLoaded == false { + viewModel.fetchFirstPage(creatorId: creatorId) + } + } + .sheet(isPresented: $isSortSheetPresented) { + CreatorChannelSortBottomSheet(selectedSort: viewModel.selectedSort) { sort in + viewModel.selectSort(sort, creatorId: creatorId) + isSortSheetPresented = false + } + } + .sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 1) + } + + private var content: some View { + VStack(spacing: 0) { + CreatorChannelSortBar( + totalCount: viewModel.response?.liveReplayContentCount ?? 0, + selectedSort: viewModel.selectedSort, + onTapSort: { + isSortSheetPresented = true + } + ) + + LazyVStack(spacing: 0) { + CreatorChannelCurrentLiveSection( + currentLive: viewModel.response?.currentLive, + onTapLive: onTapLive + ) + .padding(.top, SodaSpacing.s14) + .padding(.bottom, SodaSpacing.s12) + + ForEach(viewModel.liveReplayContents) { audioContent in + CreatorChannelLiveReplayListItem(audioContent: audioContent) { + onTapContent(audioContent.audioContentId) + } + .onAppear { + viewModel.fetchNextPageIfNeeded(creatorId: creatorId, currentItem: audioContent) + } + } + + if viewModel.isLoadingNextPage { + ProgressView() + .padding(.vertical, SodaSpacing.s20) + } + } + .padding(.bottom, isOwnCreatorChannel ? 96 : SodaSpacing.s20) + } + } +} + +struct CreatorChannelLiveTabView_Previews: PreviewProvider { + static var previews: some View { + CreatorChannelLiveTabView( + creatorId: 1, + isOwnCreatorChannel: true, + onTapLive: { _ in }, + onTapContent: { _ in }, + onTapCreateLive: {} + ) + .background(Color.black) + .previewLayout(.sizeThatFits) + } +} diff --git a/docs/20260703_크리에이터_채널_라이브_탭/plan-task.md b/docs/20260703_크리에이터_채널_라이브_탭/plan-task.md index b5ed3855..54669e2d 100644 --- a/docs/20260703_크리에이터_채널_라이브_탭/plan-task.md +++ b/docs/20260703_크리에이터_채널_라이브_탭/plan-task.md @@ -179,7 +179,7 @@ ### Phase 4: 라이브 탭 UI -- [ ] **Task 4.1: 라이브 다시듣기 item 생성** +- [x] **Task 4.1: 라이브 다시듣기 item 생성** - 대상 파일: - 생성: `SodaLive/Sources/V2/CreatorChannel/Live/Components/CreatorChannelLiveReplayListItem.swift` - 확인: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelAudioContentListItem.swift` @@ -199,7 +199,7 @@ - 실행 명령: `rg "struct CreatorChannelLiveReplayListItem|CreatorChannelAudioContentResponse|audioContent.isOwned|audioContent.isRented|audioContent.isAdult|audioContent.isPointAvailable|audioContent.price == 0|소장중|대여중|action\\(\\)" SodaLive/Sources/V2/CreatorChannel/Live/Components/CreatorChannelLiveReplayListItem.swift` - 기대 결과: 다시듣기 item의 상태 표시 조건과 tap action이 확인된다. -- [ ] **Task 4.2: 라이브 시작하기 하단 CTA 생성** +- [x] **Task 4.2: 라이브 시작하기 하단 CTA 생성** - 대상 파일: - 생성: `SodaLive/Sources/V2/CreatorChannel/Live/Components/CreatorChannelLiveStartButton.swift` - 수정: `SodaLive/Sources/I18n/I18n.swift` @@ -215,7 +215,7 @@ - 실행 명령: `rg "struct CreatorChannelLiveStartButton|ic_new_create_live|I18n.CreatorChannelLive.startLive|Button\\(action: action\\)|Capsule|frame\\(maxWidth: \\.infinity" SodaLive/Sources/V2/CreatorChannel/Live/Components/CreatorChannelLiveStartButton.swift` - 기대 결과: 지정 아이콘과 문구를 사용하는 CTA 컴포넌트가 확인된다. -- [ ] **Task 4.3: 라이브 탭 화면 생성** +- [x] **Task 4.3: 라이브 탭 화면 생성** - 대상 파일: - 생성: `SodaLive/Sources/V2/CreatorChannel/Live/CreatorChannelLiveTabView.swift` - 확인: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelCurrentLiveSection.swift` @@ -306,3 +306,4 @@ - 2026-07-03: Phase 1~3 구현 완료. `rg` 기반 정적 검증, `plutil -lint SodaLive.xcodeproj/project.pbxproj`, `xcodebuild -workspace "SodaLive.xcworkspace" -list`를 확인함. - 2026-07-03: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`는 실행했으나, DerivedData `manifest.json` 작성 중 디스크 용량 부족(`No space left on device`, 여유 598Mi)으로 Swift 컴파일 전 실패함. - 2026-07-03: Phase 2.3 보완. 첫 페이지/정렬 변경/다음 페이지 조회 모두 성공 시에만 목록 교체 또는 append를 수행하고, 실패 시 기존 목록과 pagination 상태를 유지하도록 PRD/계획/구현을 정렬함. +- 2026-07-04: Phase 4 구현 완료. `CreatorChannelLiveReplayListItem`, `CreatorChannelLiveStartButton`, `CreatorChannelLiveTabView`를 추가하고 `I18n.CreatorChannelLive.startLive` 문구를 추가함. 저장소에 별도 XCTest 파일이 없어 Phase 4는 `rg` 정적 검색, `plutil -lint SodaLive.xcodeproj/project.pbxproj`, `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`로 검증했으며 `** BUILD SUCCEEDED **`를 확인함.