diff --git a/SodaLive.xcodeproj/project.pbxproj b/SodaLive.xcodeproj/project.pbxproj index c89be573..41664efe 100644 --- a/SodaLive.xcodeproj/project.pbxproj +++ b/SodaLive.xcodeproj/project.pbxproj @@ -43,6 +43,9 @@ 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 */; }; + CC01011B2FF0100000A5BBA3 /* CreatorChannelSeriesSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01001B2FF0100000A5BBA3 /* CreatorChannelSeriesSection.swift */; }; + CC01021B2FF0100000A5BBA3 /* CreatorChannelSeriesSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01001B2FF0100000A5BBA3 /* CreatorChannelSeriesSection.swift */; }; + CC01031B2FF0100000A5BBA3 /* CreatorChannelSeriesSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01001B2FF0100000A5BBA3 /* CreatorChannelSeriesSection.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 */; }; @@ -1774,6 +1777,7 @@ 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 = ""; }; + CC01001B2FF0100000A5BBA3 /* CreatorChannelSeriesSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelSeriesSection.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 = ""; }; @@ -2700,6 +2704,7 @@ CC0100182FF0100000A5BBA3 /* CreatorChannelScheduleSection.swift */, CC0100192FF0100000A5BBA3 /* CreatorChannelAudioContentListItem.swift */, CC01001A2FF0100000A5BBA3 /* CreatorChannelAudioSection.swift */, + CC01001B2FF0100000A5BBA3 /* CreatorChannelSeriesSection.swift */, ); path = Components; sourceTree = ""; @@ -5641,6 +5646,7 @@ CC0101182FF0100000A5BBA3 /* CreatorChannelScheduleSection.swift in Sources */, CC0101192FF0100000A5BBA3 /* CreatorChannelAudioContentListItem.swift in Sources */, CC01011A2FF0100000A5BBA3 /* CreatorChannelAudioSection.swift in Sources */, + CC01011B2FF0100000A5BBA3 /* CreatorChannelSeriesSection.swift in Sources */, CC0101082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */, CC0101112FF0100000A5BBA3 /* CreatorChannelTitleBar.swift in Sources */, CC0101122FF0100000A5BBA3 /* CreatorChannelHeaderSection.swift in Sources */, @@ -6445,6 +6451,7 @@ CC0102182FF0100000A5BBA3 /* CreatorChannelScheduleSection.swift in Sources */, CC0102192FF0100000A5BBA3 /* CreatorChannelAudioContentListItem.swift in Sources */, CC01021A2FF0100000A5BBA3 /* CreatorChannelAudioSection.swift in Sources */, + CC01021B2FF0100000A5BBA3 /* CreatorChannelSeriesSection.swift in Sources */, CC0102082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */, CC0102112FF0100000A5BBA3 /* CreatorChannelTitleBar.swift in Sources */, CC0102122FF0100000A5BBA3 /* CreatorChannelHeaderSection.swift in Sources */, @@ -7265,6 +7272,7 @@ CC0103182FF0100000A5BBA3 /* CreatorChannelScheduleSection.swift in Sources */, CC0103192FF0100000A5BBA3 /* CreatorChannelAudioContentListItem.swift in Sources */, CC01031A2FF0100000A5BBA3 /* CreatorChannelAudioSection.swift in Sources */, + CC01031B2FF0100000A5BBA3 /* CreatorChannelSeriesSection.swift in Sources */, CC0103082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */, CC0103112FF0100000A5BBA3 /* CreatorChannelTitleBar.swift in Sources */, CC0103122FF0100000A5BBA3 /* CreatorChannelHeaderSection.swift in Sources */, diff --git a/SodaLive/Resources/Assets.xcassets/v2/ic_series_original.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/v2/ic_series_original.imageset/Contents.json new file mode 100644 index 00000000..f870b794 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/v2/ic_series_original.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_series_original.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_series_original.imageset/ic_series_original.png b/SodaLive/Resources/Assets.xcassets/v2/ic_series_original.imageset/ic_series_original.png new file mode 100644 index 00000000..29708072 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/v2/ic_series_original.imageset/ic_series_original.png differ diff --git a/SodaLive/Resources/Assets.xcassets/v2/img_new_only.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/v2/img_new_only.imageset/Contents.json new file mode 100644 index 00000000..bcee9859 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/v2/img_new_only.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "img_new_only.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/img_new_only.imageset/img_new_only.png b/SodaLive/Resources/Assets.xcassets/v2/img_new_only.imageset/img_new_only.png new file mode 100644 index 00000000..a36e818c Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/v2/img_new_only.imageset/img_new_only.png differ diff --git a/SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift b/SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift index e7fbf12f..2a6df557 100644 --- a/SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift +++ b/SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift @@ -159,7 +159,8 @@ struct CreatorChannelView: View { onTapLive: showLiveDetail, onTapContent: showContentDetail, onTapDonate: showDonationDialog, - onTapSchedule: handleScheduleTap + onTapSchedule: handleScheduleTap, + onTapSeries: showSeriesDetail ) } else { CreatorChannelPlaceholderTabView(title: viewModel.selectedTab.title) @@ -201,6 +202,11 @@ struct CreatorChannelView: View { ) } + private func showSeriesDetail(_ seriesId: Int) { + guard seriesId > 0 else { return } + AppState.shared.setAppStep(step: .seriesDetail(seriesId: seriesId)) + } + private func showCommunity(_ creatorId: Int) { guard creatorId > 0 else { return } AppState.shared.setAppStep(step: .creatorCommunityAll(creatorId: creatorId)) diff --git a/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelSeriesSection.swift b/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelSeriesSection.swift new file mode 100644 index 00000000..f327b462 --- /dev/null +++ b/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelSeriesSection.swift @@ -0,0 +1,117 @@ +import SwiftUI + +struct CreatorChannelSeriesSection: View { + let series: [CreatorChannelSeriesResponse] + let onSelectTab: (CreatorChannelTab) -> Void + let onTapSeries: (Int) -> Void + + init( + series: [CreatorChannelSeriesResponse], + onSelectTab: @escaping (CreatorChannelTab) -> Void, + onTapSeries: @escaping (Int) -> Void = { _ in } + ) { + self.series = series + self.onSelectTab = onSelectTab + self.onTapSeries = onTapSeries + } + + var body: some View { + if series.isEmpty == false { + VStack(alignment: .leading, spacing: SodaSpacing.s14) { + SectionTitle(title: I18n.CreatorChannelHome.series) { + onSelectTab(.series) + } + + ScrollView(.horizontal, showsIndicators: false) { + HStack(alignment: .top, spacing: SodaSpacing.s8) { + ForEach(series) { item in + CreatorChannelSeriesCard(series: item) { + onTapSeries(item.seriesId) + } + } + } + .padding(.leading, SodaSpacing.s14) + .padding(.trailing, SodaSpacing.s20) + } + } + .padding(.top, SodaSpacing.s20) + } + } +} + +private struct CreatorChannelSeriesCard: View { + let series: CreatorChannelSeriesResponse + let action: () -> Void + + private let baseDeviceWidth: CGFloat = 402 + private let maxThumbnailWidth: CGFloat = 163 + private let maxThumbnailHeight: CGFloat = 230 + + private var scale: CGFloat { + min(1, UIScreen.main.bounds.width / baseDeviceWidth) + } + + private var thumbnailWidth: CGFloat { + maxThumbnailWidth * scale + } + + private var thumbnailHeight: CGFloat { + maxThumbnailHeight * scale + } + + var body: some View { + Button(action: action) { + thumbnail + } + .buttonStyle(.plain) + } + + private var thumbnail: some View { + ZStack(alignment: .topLeading) { + DownsampledKFImage( + url: URL(string: series.coverImageUrl), + size: CGSize(width: thumbnailWidth, height: thumbnailHeight) + ) + .background(Color.gray800) + + if series.isOriginal { + HStack(spacing: 4) { + Image("ic_series_original") + .resizable() + .scaledToFit() + .frame(width: 14, height: 14) + + Image("img_new_only") + .resizable() + .scaledToFit() + .frame(height: 14) + } + .padding(.horizontal, 8) + .padding(.vertical, 5) + .background(Color.gray900) + } + } + .frame(width: thumbnailWidth, height: thumbnailHeight) + .clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous)) + } +} + +struct CreatorChannelSeriesSection_Previews: PreviewProvider { + static var previews: some View { + CreatorChannelSeriesSection( + series: (1...3).map { index in + CreatorChannelSeriesResponse( + seriesId: index, + title: "시리즈 제목", + coverImageUrl: "https://picsum.photos/300/420?\(index)", + numberOfContent: 10, + isNew: index.isMultiple(of: 2), + isOriginal: true + ) + }, + 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 14ae2d01..01ec86e7 100644 --- a/SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift +++ b/SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift @@ -7,6 +7,7 @@ struct CreatorChannelHomeView: View { let onTapContent: (Int) -> Void let onTapDonate: () -> Void let onTapSchedule: (CreatorActivityType, Int) -> Void + let onTapSeries: (Int) -> Void init( response: CreatorChannelHomeResponse, @@ -14,7 +15,8 @@ struct CreatorChannelHomeView: View { onTapLive: @escaping (Int) -> Void = { _ in }, onTapContent: @escaping (Int) -> Void = { _ in }, onTapDonate: @escaping () -> Void = {}, - onTapSchedule: @escaping (CreatorActivityType, Int) -> Void = { _, _ in } + onTapSchedule: @escaping (CreatorActivityType, Int) -> Void = { _, _ in }, + onTapSeries: @escaping (Int) -> Void = { _ in } ) { self.response = response self.onSelectTab = onSelectTab @@ -22,6 +24,7 @@ struct CreatorChannelHomeView: View { self.onTapContent = onTapContent self.onTapDonate = onTapDonate self.onTapSchedule = onTapSchedule + self.onTapSeries = onTapSeries } var body: some View { @@ -54,6 +57,12 @@ struct CreatorChannelHomeView: View { onSelectTab: onSelectTab, onTapContent: onTapContent ) + + CreatorChannelSeriesSection( + series: response.series, + onSelectTab: onSelectTab, + onTapSeries: onTapSeries + ) } .frame(maxWidth: .infinity, alignment: .leading) .background(Color.black) diff --git a/docs/20260701_크리에이터_채널_홈/plan-task.md b/docs/20260701_크리에이터_채널_홈/plan-task.md index 55f51a34..0f114056 100644 --- a/docs/20260701_크리에이터_채널_홈/plan-task.md +++ b/docs/20260701_크리에이터_채널_홈/plan-task.md @@ -450,17 +450,18 @@ ### Phase 11: 시리즈 섹션 -- [ ] **Task 11.1: 시리즈 섹션 구현** +- [x] **Task 11.1: 시리즈 섹션 구현** - 대상 파일: - 생성: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelSeriesSection.swift` - 수정: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` - 작업 내용: - `series.isEmpty`이면 섹션을 표시하지 않는다. - - 각 item의 `seriesId`, `title`, `coverImageUrl`, `numberOfContent`, `isNew`, `isOriginal`을 표시한다. + - 각 item의 `seriesId`, `coverImageUrl`, `isNew`, `isOriginal`을 사용한다. + - 카드에는 시리즈 제목과 콘텐츠 수를 표시하지 않는다. - 전체보기는 부모에서 주입받은 `onSelectTab(.series)` closure로 연결한다. - item tap은 기존 `AppStep.seriesDetail(seriesId:)` 흐름으로 연결한다. - 검증 기준: - - 실행 명령: `rg "struct CreatorChannelSeriesSection|seriesId|coverImageUrl|numberOfContent|isNew|isOriginal|onSelectTab|\\.series|seriesDetail" SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelSeriesSection.swift SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` + - 실행 명령: `rg "struct CreatorChannelSeriesSection|seriesId|coverImageUrl|isNew|isOriginal|onSelectTab|\\.series|seriesDetail" SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelSeriesSection.swift SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` - 기대 결과: 시리즈 카드, 전체보기, 상세 진입이 확인된다. ### Phase 12: 커뮤니티 섹션