feat(creator): 최신 오디오 섹션을 연결한다

This commit is contained in:
Yu Sung
2026-07-03 03:34:36 +09:00
parent 0e358d73c3
commit f514a7ac40
7 changed files with 235 additions and 4 deletions

View File

@@ -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 = "<group>"; };
CC0100132FF0100000A5BBA3 /* CreatorChannelTabBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelTabBar.swift; sourceTree = "<group>"; };
CC0100142FF0100000A5BBA3 /* CreatorChannelCurrentLiveSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelCurrentLiveSection.swift; sourceTree = "<group>"; };
CC0100152FF0100000A5BBA3 /* CreatorChannelLatestAudioSection.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLatestAudioSection.swift; sourceTree = "<group>"; };
0BBDC3ECBBE049EAA4057D3A /* PostChannelDonationRequest.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = PostChannelDonationRequest.swift; sourceTree = "<group>"; };
0EC05859619BDC03056911E7 /* MainTab.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainTab.swift; sourceTree = "<group>"; };
189350EA101CF12EEBA6EC1D /* MainTabBarView.swift */ = {isa = PBXFileReference; includeInIndex = 1; lastKnownFileType = sourcecode.swift; path = MainTabBarView.swift; sourceTree = "<group>"; };
@@ -2670,6 +2674,7 @@
children = (
CC0100072FF0100000A5BBA3 /* CreatorChannelPlaceholderTabView.swift */,
CC0100142FF0100000A5BBA3 /* CreatorChannelCurrentLiveSection.swift */,
CC0100152FF0100000A5BBA3 /* CreatorChannelLatestAudioSection.swift */,
);
path = Components;
sourceTree = "<group>";
@@ -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 */,

View File

@@ -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
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 324 B

View File

@@ -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: [],

View File

@@ -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)
}
}

View File

@@ -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)

View File

@@ -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`로 완료됐다.