diff --git a/SodaLive.xcodeproj/project.pbxproj b/SodaLive.xcodeproj/project.pbxproj index 8df1c0ed..51e016f0 100644 --- a/SodaLive.xcodeproj/project.pbxproj +++ b/SodaLive.xcodeproj/project.pbxproj @@ -25,6 +25,9 @@ CC0101142FF0100000A5BBA3 /* CreatorChannelCurrentLiveSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100142FF0100000A5BBA3 /* CreatorChannelCurrentLiveSection.swift */; }; CC0102142FF0100000A5BBA3 /* CreatorChannelCurrentLiveSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100142FF0100000A5BBA3 /* CreatorChannelCurrentLiveSection.swift */; }; CC0103142FF0100000A5BBA3 /* CreatorChannelCurrentLiveSection.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100142FF0100000A5BBA3 /* CreatorChannelCurrentLiveSection.swift */; }; + 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 */; }; 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 */; }; @@ -1750,6 +1753,7 @@ CC0100122FF0100000A5BBA3 /* CreatorChannelHeaderSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelHeaderSection.swift; sourceTree = ""; }; 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 = ""; }; 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 = ""; }; @@ -2670,6 +2674,7 @@ children = ( CC0100072FF0100000A5BBA3 /* CreatorChannelPlaceholderTabView.swift */, CC0100142FF0100000A5BBA3 /* CreatorChannelCurrentLiveSection.swift */, + CC0100152FF0100000A5BBA3 /* CreatorChannelLatestAudioSection.swift */, ); path = Components; sourceTree = ""; @@ -5605,6 +5610,7 @@ CC0101062FF0100000A5BBA3 /* CreatorChannelHomeView.swift in Sources */, CC0101072FF0100000A5BBA3 /* CreatorChannelPlaceholderTabView.swift in Sources */, CC0101142FF0100000A5BBA3 /* CreatorChannelCurrentLiveSection.swift in Sources */, + CC0101152FF0100000A5BBA3 /* CreatorChannelLatestAudioSection.swift in Sources */, CC0101082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */, CC0101112FF0100000A5BBA3 /* CreatorChannelTitleBar.swift in Sources */, CC0101122FF0100000A5BBA3 /* CreatorChannelHeaderSection.swift in Sources */, @@ -6403,6 +6409,7 @@ CC0102062FF0100000A5BBA3 /* CreatorChannelHomeView.swift in Sources */, CC0102072FF0100000A5BBA3 /* CreatorChannelPlaceholderTabView.swift in Sources */, CC0102142FF0100000A5BBA3 /* CreatorChannelCurrentLiveSection.swift in Sources */, + CC0102152FF0100000A5BBA3 /* CreatorChannelLatestAudioSection.swift in Sources */, CC0102082FF0100000A5BBA3 /* CreatorChannelView.swift in Sources */, CC0102112FF0100000A5BBA3 /* CreatorChannelTitleBar.swift in Sources */, CC0102122FF0100000A5BBA3 /* CreatorChannelHeaderSection.swift in Sources */, @@ -7217,6 +7224,7 @@ CC0103062FF0100000A5BBA3 /* CreatorChannelHomeView.swift in Sources */, CC0103072FF0100000A5BBA3 /* CreatorChannelPlaceholderTabView.swift in Sources */, CC0103142FF0100000A5BBA3 /* CreatorChannelCurrentLiveSection.swift in Sources */, + CC0103152FF0100000A5BBA3 /* CreatorChannelLatestAudioSection.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_content_tag_first_star.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/v2/ic_content_tag_first_star.imageset/Contents.json new file mode 100644 index 00000000..4fd53b5b --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/v2/ic_content_tag_first_star.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "filename" : "ic_content_tag_first_star.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_content_tag_first_star.imageset/ic_content_tag_first_star.png b/SodaLive/Resources/Assets.xcassets/v2/ic_content_tag_first_star.imageset/ic_content_tag_first_star.png new file mode 100644 index 00000000..e81e30f7 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/v2/ic_content_tag_first_star.imageset/ic_content_tag_first_star.png differ diff --git a/SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift b/SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift index 9f10df73..51dcbf10 100644 --- a/SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift +++ b/SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift @@ -130,7 +130,8 @@ struct CreatorChannelView: View { } else if let response = viewModel.response { CreatorChannelHomeView( response: response, - onSelectTab: selectTab + onSelectTab: selectTab, + onTapContent: showContentDetail ) } else { CreatorChannelPlaceholderTabView(title: viewModel.selectedTab.title) @@ -153,6 +154,10 @@ struct CreatorChannelView: View { viewModel.selectedTab = tab } } + + private func showContentDetail(_ contentId: Int) { + AppState.shared.setAppStep(step: .contentDetail(contentId: contentId)) + } } struct CreatorChannelView_Previews: PreviewProvider { @@ -175,7 +180,20 @@ struct CreatorChannelView_Previews: PreviewProvider { isNotify: true ), currentLive: nil, - latestAudioContent: nil, + latestAudioContent: CreatorChannelAudioContentResponse( + audioContentId: 1, + title: "최신 오디오 콘텐츠 제목", + duration: "1:43:25", + imageUrl: "https://picsum.photos/300/300", + price: 0, + isAdult: false, + isPointAvailable: true, + isFirstContent: true, + seriesName: nil, + isOriginalSeries: nil, + isOwned: false, + isRented: false + ), channelDonations: [], notices: [], schedules: [], diff --git a/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelLatestAudioSection.swift b/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelLatestAudioSection.swift new file mode 100644 index 00000000..51a663f9 --- /dev/null +++ b/SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelLatestAudioSection.swift @@ -0,0 +1,175 @@ +import SwiftUI + +struct CreatorChannelLatestAudioSection: View { + let latestAudioContent: CreatorChannelAudioContentResponse? + let onTapContent: (Int) -> Void + + init( + latestAudioContent: CreatorChannelAudioContentResponse?, + onTapContent: @escaping (Int) -> Void = { _ in } + ) { + self.latestAudioContent = latestAudioContent + self.onTapContent = onTapContent + } + + var body: some View { + if let latestAudioContent { + VStack(alignment: .leading, spacing: SodaSpacing.s8) { + SectionTitle(title: I18n.CreatorChannelHome.latestAudio) + + CreatorChannelLatestAudioCard(audioContent: latestAudioContent) { + onTapContent(latestAudioContent.audioContentId) + } + .padding(.horizontal, SodaSpacing.s14) + } + .padding(.top, SodaSpacing.s20) + } + } +} + +private struct CreatorChannelLatestAudioCard: View { + let audioContent: CreatorChannelAudioContentResponse + let action: () -> Void + + var body: some View { + Button(action: action) { + HStack(alignment: .center, spacing: SodaSpacing.s14) { + thumbnail + + VStack(alignment: .leading, spacing: SodaSpacing.s6) { + Text("New") + .font(.custom("Pattaya", size: 14)) + .foregroundColor(Color.soda400) + .lineLimit(1) + + Text(audioContent.title) + .appFont(.body1) + .foregroundColor(.white) + .lineLimit(1) + .truncationMode(.tail) + + if let duration = audioContent.duration, !duration.isEmpty { + Text(duration) + .appFont(size: 14, weight: .medium) + .foregroundColor(Color.gray500) + .lineLimit(1) + .truncationMode(.tail) + } + + } + .frame(maxWidth: .infinity, alignment: .leading) + } + .padding(SodaSpacing.s14) + .frame(maxWidth: .infinity, alignment: .leading) + .background(Color.gray900) + .clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous)) + } + .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() + } + } + } + + @ViewBuilder + private var bottomTagList: some View { + HStack(spacing: 0) { + if audioContent.isPointAvailable { + CreatorChannelAudioImageTag(imageName: "ic_content_tag_point") + } + + if audioContent.price == 0 { + CreatorChannelAudioFreeTag() + } + } + } +} + +private struct CreatorChannelAudioImageTag: View { + let imageName: String + + var body: some View { + Image(imageName) + .resizable() + .scaledToFit() + .frame(width: 24, height: 24) + } +} + +private 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) + } +} + +private 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 CreatorChannelLatestAudioSection_Previews: PreviewProvider { + static var previews: some View { + CreatorChannelLatestAudioSection( + latestAudioContent: CreatorChannelAudioContentResponse( + audioContentId: 1, + title: "콘텐츠 이름이 한 줄 콘텐츠 이름이 한 줄 콘텐츠 이름이 한 줄", + duration: "1:43:25", + imageUrl: "https://picsum.photos/300/300", + price: 0, + isAdult: true, + isPointAvailable: true, + isFirstContent: true, + seriesName: "시리즈명", + isOriginalSeries: true, + isOwned: false, + isRented: false + ) + ) + .background(Color.black) + .previewLayout(.sizeThatFits) + } +} diff --git a/SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift b/SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift index 5ccf219e..7f81b883 100644 --- a/SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift +++ b/SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift @@ -4,15 +4,18 @@ struct CreatorChannelHomeView: View { let response: CreatorChannelHomeResponse let onSelectTab: (CreatorChannelTab) -> Void let onTapLive: (Int) -> Void + let onTapContent: (Int) -> Void init( response: CreatorChannelHomeResponse, onSelectTab: @escaping (CreatorChannelTab) -> Void, - onTapLive: @escaping (Int) -> Void = { _ in } + onTapLive: @escaping (Int) -> Void = { _ in }, + onTapContent: @escaping (Int) -> Void = { _ in } ) { self.response = response self.onSelectTab = onSelectTab self.onTapLive = onTapLive + self.onTapContent = onTapContent } var body: some View { @@ -21,6 +24,11 @@ struct CreatorChannelHomeView: View { currentLive: response.currentLive, onTapLive: onTapLive ) + + CreatorChannelLatestAudioSection( + latestAudioContent: response.latestAudioContent, + onTapContent: onTapContent + ) } .frame(maxWidth: .infinity, alignment: .leading) .background(Color.black) diff --git a/docs/20260701_크리에이터_채널_홈/plan-task.md b/docs/20260701_크리에이터_채널_홈/plan-task.md index 287eebd3..bd5f932d 100644 --- a/docs/20260701_크리에이터_채널_홈/plan-task.md +++ b/docs/20260701_크리에이터_채널_홈/plan-task.md @@ -351,7 +351,7 @@ ### Phase 6: 최신 오디오 콘텐츠 섹션 -- [ ] **Task 6.1: 최신 오디오 콘텐츠 섹션 구현** +- [x] **Task 6.1: 최신 오디오 콘텐츠 섹션 구현** - 대상 파일: - 생성: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelLatestAudioSection.swift` - 수정: `SodaLive/Sources/V2/CreatorChannel/Home/CreatorChannelHomeView.swift` @@ -620,6 +620,7 @@ - 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 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` 호출이 일어나도록 계획에 반영했다. - 2026-07-03: Task 3.4 구현을 완료했다. `CreatorChannelView`에서 geometry preference로 tab-bar 위치를 추적하고, sticky 기준을 디바이스 최상단이 아닌 title bar 하단(`safeAreaInsets.top + 56`)으로 계산하도록 변경했다. `rg "PreferenceKey|GeometryReader|ZStack|overlay|backgroundProgress|Color\\.clear|opacity|isTabBarSticky|clamp|safeAreaInsets|CreatorChannelTitleBar|CreatorChannelTabBar" ...` 기준 키워드를 확인했고, `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`가 `BUILD SUCCEEDED`로 완료됐다.