feat(creator): 정렬 팝업을 적용한다
This commit is contained in:
@@ -1708,7 +1708,7 @@
|
|||||||
CC0101292FF0100000A5BBA3 /* CreatorChannelFloatingActionMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100292FF0100000A5BBA3 /* CreatorChannelFloatingActionMenu.swift */; };
|
CC0101292FF0100000A5BBA3 /* CreatorChannelFloatingActionMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100292FF0100000A5BBA3 /* CreatorChannelFloatingActionMenu.swift */; };
|
||||||
CC01012A2FF0100000A5BBA3 /* ContentSort.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002A2FF0100000A5BBA3 /* ContentSort.swift */; };
|
CC01012A2FF0100000A5BBA3 /* ContentSort.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002A2FF0100000A5BBA3 /* ContentSort.swift */; };
|
||||||
CC01012B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift */; };
|
CC01012B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift */; };
|
||||||
CC01012C2FF0100000A5BBA3 /* CreatorChannelSortBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002C2FF0100000A5BBA3 /* CreatorChannelSortBottomSheet.swift */; };
|
CC01012C2FF0100000A5BBA3 /* CreatorChannelSortContextPopup.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002C2FF0100000A5BBA3 /* CreatorChannelSortContextPopup.swift */; };
|
||||||
CC01012D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift */; };
|
CC01012D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift */; };
|
||||||
CC01012E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift */; };
|
CC01012E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift */; };
|
||||||
CC01012F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift */; };
|
CC01012F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift */; };
|
||||||
@@ -1741,7 +1741,7 @@
|
|||||||
CC0102292FF0100000A5BBA3 /* CreatorChannelFloatingActionMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100292FF0100000A5BBA3 /* CreatorChannelFloatingActionMenu.swift */; };
|
CC0102292FF0100000A5BBA3 /* CreatorChannelFloatingActionMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100292FF0100000A5BBA3 /* CreatorChannelFloatingActionMenu.swift */; };
|
||||||
CC01022A2FF0100000A5BBA3 /* ContentSort.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002A2FF0100000A5BBA3 /* ContentSort.swift */; };
|
CC01022A2FF0100000A5BBA3 /* ContentSort.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002A2FF0100000A5BBA3 /* ContentSort.swift */; };
|
||||||
CC01022B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift */; };
|
CC01022B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift */; };
|
||||||
CC01022C2FF0100000A5BBA3 /* CreatorChannelSortBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002C2FF0100000A5BBA3 /* CreatorChannelSortBottomSheet.swift */; };
|
CC01022C2FF0100000A5BBA3 /* CreatorChannelSortContextPopup.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002C2FF0100000A5BBA3 /* CreatorChannelSortContextPopup.swift */; };
|
||||||
CC01022D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift */; };
|
CC01022D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift */; };
|
||||||
CC01022E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift */; };
|
CC01022E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift */; };
|
||||||
CC01022F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift */; };
|
CC01022F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift */; };
|
||||||
@@ -1774,7 +1774,7 @@
|
|||||||
CC0103292FF0100000A5BBA3 /* CreatorChannelFloatingActionMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100292FF0100000A5BBA3 /* CreatorChannelFloatingActionMenu.swift */; };
|
CC0103292FF0100000A5BBA3 /* CreatorChannelFloatingActionMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100292FF0100000A5BBA3 /* CreatorChannelFloatingActionMenu.swift */; };
|
||||||
CC01032A2FF0100000A5BBA3 /* ContentSort.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002A2FF0100000A5BBA3 /* ContentSort.swift */; };
|
CC01032A2FF0100000A5BBA3 /* ContentSort.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002A2FF0100000A5BBA3 /* ContentSort.swift */; };
|
||||||
CC01032B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift */; };
|
CC01032B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift */; };
|
||||||
CC01032C2FF0100000A5BBA3 /* CreatorChannelSortBottomSheet.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002C2FF0100000A5BBA3 /* CreatorChannelSortBottomSheet.swift */; };
|
CC01032C2FF0100000A5BBA3 /* CreatorChannelSortContextPopup.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002C2FF0100000A5BBA3 /* CreatorChannelSortContextPopup.swift */; };
|
||||||
CC01032D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift */; };
|
CC01032D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift */; };
|
||||||
CC01032E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift */; };
|
CC01032E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift */; };
|
||||||
CC01032F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift */; };
|
CC01032F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift */; };
|
||||||
@@ -2670,7 +2670,7 @@
|
|||||||
CC0100292FF0100000A5BBA3 /* CreatorChannelFloatingActionMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelFloatingActionMenu.swift; sourceTree = "<group>"; };
|
CC0100292FF0100000A5BBA3 /* CreatorChannelFloatingActionMenu.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelFloatingActionMenu.swift; sourceTree = "<group>"; };
|
||||||
CC01002A2FF0100000A5BBA3 /* ContentSort.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentSort.swift; sourceTree = "<group>"; };
|
CC01002A2FF0100000A5BBA3 /* ContentSort.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ContentSort.swift; sourceTree = "<group>"; };
|
||||||
CC01002B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelSortBar.swift; sourceTree = "<group>"; };
|
CC01002B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelSortBar.swift; sourceTree = "<group>"; };
|
||||||
CC01002C2FF0100000A5BBA3 /* CreatorChannelSortBottomSheet.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelSortBottomSheet.swift; sourceTree = "<group>"; };
|
CC01002C2FF0100000A5BBA3 /* CreatorChannelSortContextPopup.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelSortContextPopup.swift; sourceTree = "<group>"; };
|
||||||
CC01002D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveTabResponse.swift; sourceTree = "<group>"; };
|
CC01002D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveTabResponse.swift; sourceTree = "<group>"; };
|
||||||
CC01002E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveApi.swift; sourceTree = "<group>"; };
|
CC01002E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveApi.swift; sourceTree = "<group>"; };
|
||||||
CC01002F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveRepository.swift; sourceTree = "<group>"; };
|
CC01002F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CreatorChannelLiveRepository.swift; sourceTree = "<group>"; };
|
||||||
@@ -5313,7 +5313,7 @@
|
|||||||
CC0100132FF0100000A5BBA3 /* CreatorChannelTabBar.swift */,
|
CC0100132FF0100000A5BBA3 /* CreatorChannelTabBar.swift */,
|
||||||
CC0100292FF0100000A5BBA3 /* CreatorChannelFloatingActionMenu.swift */,
|
CC0100292FF0100000A5BBA3 /* CreatorChannelFloatingActionMenu.swift */,
|
||||||
CC01002B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift */,
|
CC01002B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift */,
|
||||||
CC01002C2FF0100000A5BBA3 /* CreatorChannelSortBottomSheet.swift */,
|
CC01002C2FF0100000A5BBA3 /* CreatorChannelSortContextPopup.swift */,
|
||||||
CC0200012FF0200000A5BBA3 /* CreatorChannelAudioContentListItem.swift */,
|
CC0200012FF0200000A5BBA3 /* CreatorChannelAudioContentListItem.swift */,
|
||||||
);
|
);
|
||||||
path = Components;
|
path = Components;
|
||||||
@@ -5846,7 +5846,7 @@
|
|||||||
CC0101102FF0100000A5BBA3 /* CreatorChannelTab.swift in Sources */,
|
CC0101102FF0100000A5BBA3 /* CreatorChannelTab.swift in Sources */,
|
||||||
CC01012A2FF0100000A5BBA3 /* ContentSort.swift in Sources */,
|
CC01012A2FF0100000A5BBA3 /* ContentSort.swift in Sources */,
|
||||||
CC01012B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift in Sources */,
|
CC01012B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift in Sources */,
|
||||||
CC01012C2FF0100000A5BBA3 /* CreatorChannelSortBottomSheet.swift in Sources */,
|
CC01012C2FF0100000A5BBA3 /* CreatorChannelSortContextPopup.swift in Sources */,
|
||||||
CC01012D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift in Sources */,
|
CC01012D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift in Sources */,
|
||||||
CC01012E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift in Sources */,
|
CC01012E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift in Sources */,
|
||||||
CC01012F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift in Sources */,
|
CC01012F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift in Sources */,
|
||||||
@@ -6675,7 +6675,7 @@
|
|||||||
CC0102102FF0100000A5BBA3 /* CreatorChannelTab.swift in Sources */,
|
CC0102102FF0100000A5BBA3 /* CreatorChannelTab.swift in Sources */,
|
||||||
CC01022A2FF0100000A5BBA3 /* ContentSort.swift in Sources */,
|
CC01022A2FF0100000A5BBA3 /* ContentSort.swift in Sources */,
|
||||||
CC01022B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift in Sources */,
|
CC01022B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift in Sources */,
|
||||||
CC01022C2FF0100000A5BBA3 /* CreatorChannelSortBottomSheet.swift in Sources */,
|
CC01022C2FF0100000A5BBA3 /* CreatorChannelSortContextPopup.swift in Sources */,
|
||||||
CC01022D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift in Sources */,
|
CC01022D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift in Sources */,
|
||||||
CC01022E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift in Sources */,
|
CC01022E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift in Sources */,
|
||||||
CC01022F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift in Sources */,
|
CC01022F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift in Sources */,
|
||||||
@@ -7520,7 +7520,7 @@
|
|||||||
CC0103102FF0100000A5BBA3 /* CreatorChannelTab.swift in Sources */,
|
CC0103102FF0100000A5BBA3 /* CreatorChannelTab.swift in Sources */,
|
||||||
CC01032A2FF0100000A5BBA3 /* ContentSort.swift in Sources */,
|
CC01032A2FF0100000A5BBA3 /* ContentSort.swift in Sources */,
|
||||||
CC01032B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift in Sources */,
|
CC01032B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift in Sources */,
|
||||||
CC01032C2FF0100000A5BBA3 /* CreatorChannelSortBottomSheet.swift in Sources */,
|
CC01032C2FF0100000A5BBA3 /* CreatorChannelSortContextPopup.swift in Sources */,
|
||||||
CC01032D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift in Sources */,
|
CC01032D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift in Sources */,
|
||||||
CC01032E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift in Sources */,
|
CC01032E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift in Sources */,
|
||||||
CC01032F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift in Sources */,
|
CC01032F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift in Sources */,
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ struct CreatorChannelAudioTabView: View {
|
|||||||
let onTapContent: (Int) -> Void
|
let onTapContent: (Int) -> Void
|
||||||
|
|
||||||
@StateObject private var viewModel = CreatorChannelAudioViewModel()
|
@StateObject private var viewModel = CreatorChannelAudioViewModel()
|
||||||
@State private var isSortSheetPresented = false
|
@State private var isSortPopupPresented = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
content
|
content
|
||||||
@@ -16,12 +16,6 @@ struct CreatorChannelAudioTabView: View {
|
|||||||
viewModel.fetchFirstPage(creatorId: creatorId)
|
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)
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,6 +24,8 @@ struct CreatorChannelAudioTabView: View {
|
|||||||
if isEmptyState {
|
if isEmptyState {
|
||||||
emptyStateView
|
emptyStateView
|
||||||
} else {
|
} else {
|
||||||
|
ZStack(alignment: .topTrailing) {
|
||||||
|
VStack(spacing: 0) {
|
||||||
CapsuleTabBar(
|
CapsuleTabBar(
|
||||||
items: viewModel.displayThemes,
|
items: viewModel.displayThemes,
|
||||||
selectedItem: Binding(
|
selectedItem: Binding(
|
||||||
@@ -41,8 +37,9 @@ struct CreatorChannelAudioTabView: View {
|
|||||||
|
|
||||||
CreatorChannelSortBar(
|
CreatorChannelSortBar(
|
||||||
selectedSort: viewModel.selectedSort,
|
selectedSort: viewModel.selectedSort,
|
||||||
|
totalCount: viewModel.response?.audioContentCount,
|
||||||
onTapSort: {
|
onTapSort: {
|
||||||
isSortSheetPresented = true
|
isSortPopupPresented.toggle()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -72,6 +69,35 @@ struct CreatorChannelAudioTabView: View {
|
|||||||
}
|
}
|
||||||
.padding(.bottom, isOwnCreatorChannel ? 70 : SodaSpacing.s20)
|
.padding(.bottom, isOwnCreatorChannel ? 70 : SodaSpacing.s20)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isSortPopupPresented {
|
||||||
|
Color.black
|
||||||
|
.opacity(0.001)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
.onTapGesture {
|
||||||
|
isSortPopupPresented = false
|
||||||
|
}
|
||||||
|
|
||||||
|
CreatorChannelSortContextPopup(selectedSort: viewModel.selectedSort) { sort in
|
||||||
|
viewModel.selectSort(sort, creatorId: creatorId)
|
||||||
|
isSortPopupPresented = false
|
||||||
|
}
|
||||||
|
.padding(.top, 104)
|
||||||
|
.padding(.trailing, SodaSpacing.s14)
|
||||||
|
.zIndex(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .topTrailing)
|
||||||
|
.onChange(of: viewModel.selectedSort) { _ in
|
||||||
|
isSortPopupPresented = false
|
||||||
|
}
|
||||||
|
.onChange(of: viewModel.selectedTheme) { _ in
|
||||||
|
isSortPopupPresented = false
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
isSortPopupPresented = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,26 +2,49 @@ import SwiftUI
|
|||||||
|
|
||||||
struct CreatorChannelSortBar: View {
|
struct CreatorChannelSortBar: View {
|
||||||
let selectedSort: ContentSort
|
let selectedSort: ContentSort
|
||||||
|
let totalCount: Int?
|
||||||
let onTapSort: () -> Void
|
let onTapSort: () -> Void
|
||||||
|
|
||||||
|
init(
|
||||||
|
selectedSort: ContentSort,
|
||||||
|
totalCount: Int? = nil,
|
||||||
|
onTapSort: @escaping () -> Void
|
||||||
|
) {
|
||||||
|
self.selectedSort = selectedSort
|
||||||
|
self.totalCount = totalCount
|
||||||
|
self.onTapSort = onTapSort
|
||||||
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(alignment: .center, spacing: SodaSpacing.s8) {
|
HStack(alignment: .center, spacing: SodaSpacing.s8) {
|
||||||
|
if let totalCount, totalCount >= 0 {
|
||||||
|
HStack(spacing: SodaSpacing.s4) {
|
||||||
|
Text(I18n.CreatorChannelLive.totalLabel)
|
||||||
|
.appFont(size: 16, weight: .medium)
|
||||||
|
.foregroundColor(Color.white)
|
||||||
|
|
||||||
|
Text("\(totalCount)")
|
||||||
|
.appFont(size: 16, weight: .medium)
|
||||||
|
.foregroundColor(Color.gray500)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Button(action: onTapSort) {
|
Button(action: onTapSort) {
|
||||||
HStack(spacing: SodaSpacing.s4) {
|
HStack(spacing: SodaSpacing.s4) {
|
||||||
Text(selectedSort.title)
|
Text(selectedSort.title)
|
||||||
.appFont(size: 14, weight: .medium)
|
.appFont(size: 16, weight: .regular)
|
||||||
.foregroundColor(Color.white)
|
.foregroundColor(Color.gray400)
|
||||||
|
|
||||||
Image("ic_new_sort")
|
Image("ic_new_sort")
|
||||||
.font(.system(size: 12, weight: .semibold))
|
.frame(width: 18, height: 18)
|
||||||
}
|
}
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.buttonStyle(.plain)
|
||||||
}
|
}
|
||||||
.padding(.horizontal, SodaSpacing.s20)
|
.padding(.horizontal, SodaSpacing.s14)
|
||||||
.frame(height: 52)
|
.frame(height: 52)
|
||||||
.background(Color.black)
|
.background(Color.black)
|
||||||
}
|
}
|
||||||
@@ -29,7 +52,7 @@ struct CreatorChannelSortBar: View {
|
|||||||
|
|
||||||
struct CreatorChannelSortBar_Previews: PreviewProvider {
|
struct CreatorChannelSortBar_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
CreatorChannelSortBar(selectedSort: .latest, onTapSort: {})
|
CreatorChannelSortBar(selectedSort: .latest, totalCount: 23, onTapSort: {})
|
||||||
.previewLayout(.sizeThatFits)
|
.previewLayout(.sizeThatFits)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct CreatorChannelSortBottomSheet: View {
|
|
||||||
let selectedSort: ContentSort
|
|
||||||
let sorts: [ContentSort]
|
|
||||||
let onSelect: (ContentSort) -> Void
|
|
||||||
|
|
||||||
init(
|
|
||||||
selectedSort: ContentSort,
|
|
||||||
sorts: [ContentSort] = ContentSort.allCases,
|
|
||||||
onSelect: @escaping (ContentSort) -> Void
|
|
||||||
) {
|
|
||||||
self.selectedSort = selectedSort
|
|
||||||
self.sorts = sorts
|
|
||||||
self.onSelect = onSelect
|
|
||||||
}
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
ForEach(sorts, id: \.self) { sort in
|
|
||||||
Button {
|
|
||||||
onSelect(sort)
|
|
||||||
} label: {
|
|
||||||
Text(sort.title)
|
|
||||||
.appFont(size: 16, weight: selectedSort == sort ? .bold : .medium)
|
|
||||||
.foregroundColor(selectedSort == sort ? Color.white : Color.gray400)
|
|
||||||
.frame(maxWidth: .infinity, alignment: .leading)
|
|
||||||
.padding(.horizontal, SodaSpacing.s20)
|
|
||||||
.frame(height: 52)
|
|
||||||
.background(selectedSort == sort ? Color.gray900 : Color.black)
|
|
||||||
}
|
|
||||||
.buttonStyle(.plain)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.vertical, SodaSpacing.s8)
|
|
||||||
.background(Color.black)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
struct CreatorChannelSortBottomSheet_Previews: PreviewProvider {
|
|
||||||
static var previews: some View {
|
|
||||||
CreatorChannelSortBottomSheet(selectedSort: .latest, onSelect: { _ in })
|
|
||||||
.previewLayout(.sizeThatFits)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct CreatorChannelSortContextPopup: View {
|
||||||
|
let selectedSort: ContentSort
|
||||||
|
let sorts: [ContentSort]
|
||||||
|
let onSelect: (ContentSort) -> Void
|
||||||
|
|
||||||
|
init(
|
||||||
|
selectedSort: ContentSort,
|
||||||
|
sorts: [ContentSort] = ContentSort.allCases,
|
||||||
|
onSelect: @escaping (ContentSort) -> Void
|
||||||
|
) {
|
||||||
|
self.selectedSort = selectedSort
|
||||||
|
self.sorts = sorts
|
||||||
|
self.onSelect = onSelect
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
ForEach(sorts, id: \.self) { sort in
|
||||||
|
Button {
|
||||||
|
onSelect(sort)
|
||||||
|
} label: {
|
||||||
|
Text(sort.title)
|
||||||
|
.appFont(size: 16, weight: .medium)
|
||||||
|
.foregroundColor(Color.white)
|
||||||
|
.lineLimit(1)
|
||||||
|
.frame(minWidth: 92, alignment: .leading)
|
||||||
|
.padding(.horizontal, SodaSpacing.s12)
|
||||||
|
.padding(.vertical, SodaSpacing.s8)
|
||||||
|
.background(selectedSort == sort ? Color.gray800 : Color.gray900)
|
||||||
|
}
|
||||||
|
.buttonStyle(.plain)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.background(Color.gray900)
|
||||||
|
.clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous))
|
||||||
|
.overlay(
|
||||||
|
RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous)
|
||||||
|
.stroke(Color.gray700, lineWidth: 1)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct CreatorChannelSortContextPopup_Previews: PreviewProvider {
|
||||||
|
static var previews: some View {
|
||||||
|
CreatorChannelSortContextPopup(selectedSort: .latest, onSelect: { _ in })
|
||||||
|
.previewLayout(.sizeThatFits)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ struct CreatorChannelLiveTabView: View {
|
|||||||
let onTapContent: (Int) -> Void
|
let onTapContent: (Int) -> Void
|
||||||
|
|
||||||
@StateObject private var viewModel = CreatorChannelLiveViewModel()
|
@StateObject private var viewModel = CreatorChannelLiveViewModel()
|
||||||
@State private var isSortSheetPresented = false
|
@State private var isSortPopupPresented = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
content
|
content
|
||||||
@@ -17,12 +17,6 @@ struct CreatorChannelLiveTabView: View {
|
|||||||
viewModel.fetchFirstPage(creatorId: creatorId)
|
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)
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -31,10 +25,13 @@ struct CreatorChannelLiveTabView: View {
|
|||||||
if isEmptyState {
|
if isEmptyState {
|
||||||
emptyStateView
|
emptyStateView
|
||||||
} else {
|
} else {
|
||||||
|
ZStack(alignment: .topTrailing) {
|
||||||
|
VStack(spacing: 0) {
|
||||||
CreatorChannelSortBar(
|
CreatorChannelSortBar(
|
||||||
selectedSort: viewModel.selectedSort,
|
selectedSort: viewModel.selectedSort,
|
||||||
|
totalCount: viewModel.response?.liveReplayContentCount,
|
||||||
onTapSort: {
|
onTapSort: {
|
||||||
isSortSheetPresented = true
|
isSortPopupPresented.toggle()
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -62,6 +59,32 @@ struct CreatorChannelLiveTabView: View {
|
|||||||
}
|
}
|
||||||
.padding(.bottom, isOwnCreatorChannel ? 70 : SodaSpacing.s20)
|
.padding(.bottom, isOwnCreatorChannel ? 70 : SodaSpacing.s20)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isSortPopupPresented {
|
||||||
|
Color.black
|
||||||
|
.opacity(0.001)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
.onTapGesture {
|
||||||
|
isSortPopupPresented = false
|
||||||
|
}
|
||||||
|
|
||||||
|
CreatorChannelSortContextPopup(selectedSort: viewModel.selectedSort) { sort in
|
||||||
|
viewModel.selectSort(sort, creatorId: creatorId)
|
||||||
|
isSortPopupPresented = false
|
||||||
|
}
|
||||||
|
.padding(.top, 52)
|
||||||
|
.padding(.trailing, SodaSpacing.s14)
|
||||||
|
.zIndex(1)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity, alignment: .topTrailing)
|
||||||
|
.onChange(of: viewModel.selectedSort) { _ in
|
||||||
|
isSortPopupPresented = false
|
||||||
|
}
|
||||||
|
.onDisappear {
|
||||||
|
isSortPopupPresented = false
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -22,7 +22,7 @@
|
|||||||
- 홈 탭의 `CreatorChannelCurrentLiveSection`과 기존 `showLiveDetail(_:)`, `showContentDetail(_:)`, `showCreateLive()` 흐름을 재사용한다.
|
- 홈 탭의 `CreatorChannelCurrentLiveSection`과 기존 `showLiveDetail(_:)`, `showContentDetail(_:)`, `showCreateLive()` 흐름을 재사용한다.
|
||||||
- `CreatorChannelLiveResponse`와 `CreatorChannelAudioContentResponse`는 기존 `SodaLive/Sources/V2/CreatorChannel/Home/Models/CreatorChannelHomeResponse.swift` 타입을 우선 재사용한다. 타입 이동은 이번 구현에 필수일 때만 수행한다.
|
- `CreatorChannelLiveResponse`와 `CreatorChannelAudioContentResponse`는 기존 `SodaLive/Sources/V2/CreatorChannel/Home/Models/CreatorChannelHomeResponse.swift` 타입을 우선 재사용한다. 타입 이동은 이번 구현에 필수일 때만 수행한다.
|
||||||
- `ContentSort`는 라이브 탭 외 콘텐츠 탭에서 재사용 가능하도록 `SodaLive/Sources/V2/CreatorChannel/Models/**` 아래에 둔다.
|
- `ContentSort`는 라이브 탭 외 콘텐츠 탭에서 재사용 가능하도록 `SodaLive/Sources/V2/CreatorChannel/Models/**` 아래에 둔다.
|
||||||
- sort-bar와 정렬 BottomSheet는 `SodaLive/Sources/V2/CreatorChannel/Components/**` 아래에 둔다.
|
- sort-bar와 정렬 context popup은 `SodaLive/Sources/V2/CreatorChannel/Components/**` 아래에 둔다.
|
||||||
- 라이브 탭 전용 API, Repository, ViewModel, View, item UI는 `SodaLive/Sources/V2/CreatorChannel/Live/**` 아래에 둔다.
|
- 라이브 탭 전용 API, Repository, ViewModel, View, item UI는 `SodaLive/Sources/V2/CreatorChannel/Live/**` 아래에 둔다.
|
||||||
- 최초 요청은 `page = 0`, `size = 20`, `sort = LATEST`로 고정한다.
|
- 최초 요청은 `page = 0`, `size = 20`, `sort = LATEST`로 고정한다.
|
||||||
- `hasNext == true`일 때만 `page + 1` 추가 요청을 수행하고, 추가 요청 중 중복 호출을 막는다.
|
- `hasNext == true`일 때만 `page + 1` 추가 요청을 수행하고, 추가 요청 중 중복 호출을 막는다.
|
||||||
@@ -34,7 +34,7 @@
|
|||||||
### 생성
|
### 생성
|
||||||
- `SodaLive/Sources/V2/CreatorChannel/Models/ContentSort.swift`
|
- `SodaLive/Sources/V2/CreatorChannel/Models/ContentSort.swift`
|
||||||
- `SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelSortBar.swift`
|
- `SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelSortBar.swift`
|
||||||
- `SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelSortBottomSheet.swift`
|
- `SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelSortContextPopup.swift`
|
||||||
- `SodaLive/Sources/V2/CreatorChannel/Live/Models/CreatorChannelLiveTabResponse.swift`
|
- `SodaLive/Sources/V2/CreatorChannel/Live/Models/CreatorChannelLiveTabResponse.swift`
|
||||||
- `SodaLive/Sources/V2/CreatorChannel/Live/Repository/CreatorChannelLiveApi.swift`
|
- `SodaLive/Sources/V2/CreatorChannel/Live/Repository/CreatorChannelLiveApi.swift`
|
||||||
- `SodaLive/Sources/V2/CreatorChannel/Live/Repository/CreatorChannelLiveRepository.swift`
|
- `SodaLive/Sources/V2/CreatorChannel/Live/Repository/CreatorChannelLiveRepository.swift`
|
||||||
@@ -145,7 +145,7 @@
|
|||||||
- 실행 명령: `rg "final class CreatorChannelLiveViewModel|isLoadingNextPage|liveReplayContents|selectedSort|private let pageSize|fetchFirstPage\\(creatorId: Int\\)|fetchNextPageIfNeeded|selectSort|ApiResponse<CreatorChannelLiveTabResponse>|page \\+ 1|hasNext" SodaLive/Sources/V2/CreatorChannel/Live/CreatorChannelLiveViewModel.swift`
|
- 실행 명령: `rg "final class CreatorChannelLiveViewModel|isLoadingNextPage|liveReplayContents|selectedSort|private let pageSize|fetchFirstPage\\(creatorId: Int\\)|fetchNextPageIfNeeded|selectSort|ApiResponse<CreatorChannelLiveTabResponse>|page \\+ 1|hasNext" SodaLive/Sources/V2/CreatorChannel/Live/CreatorChannelLiveViewModel.swift`
|
||||||
- 기대 결과: 첫 페이지 로딩, 정렬 변경, 다음 페이지 로딩 상태와 성공 시에만 목록 교체/append하는 동작이 확인된다.
|
- 기대 결과: 첫 페이지 로딩, 정렬 변경, 다음 페이지 로딩 상태와 성공 시에만 목록 교체/append하는 동작이 확인된다.
|
||||||
|
|
||||||
### Phase 3: sort-bar와 정렬 BottomSheet
|
### Phase 3: sort-bar와 정렬 context popup
|
||||||
|
|
||||||
- [x] **Task 3.1: sort-bar 공통 컴포넌트 생성**
|
- [x] **Task 3.1: sort-bar 공통 컴포넌트 생성**
|
||||||
- 대상 파일:
|
- 대상 파일:
|
||||||
@@ -161,19 +161,19 @@
|
|||||||
- 실행 명령: `rg "struct CreatorChannelSortBar|selectedSort: ContentSort|onTapSort|selectedSort.title|frame\\(height: 52" SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelSortBar.swift`
|
- 실행 명령: `rg "struct CreatorChannelSortBar|selectedSort: ContentSort|onTapSort|selectedSort.title|frame\\(height: 52" SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelSortBar.swift`
|
||||||
- 기대 결과: 현재 정렬과 터치 action이 있는 sort-bar가 확인된다.
|
- 기대 결과: 현재 정렬과 터치 action이 있는 sort-bar가 확인된다.
|
||||||
|
|
||||||
- [x] **Task 3.2: 정렬 BottomSheet 컴포넌트 생성**
|
- [x] **Task 3.2: 정렬 context popup 컴포넌트 생성**
|
||||||
- 대상 파일:
|
- 대상 파일:
|
||||||
- 생성: `SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelSortBottomSheet.swift`
|
- 생성: `SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelSortContextPopup.swift`
|
||||||
- 작업 내용:
|
- 작업 내용:
|
||||||
- `CreatorChannelSortBottomSheet`를 `View`로 생성한다.
|
- `CreatorChannelSortContextPopup`를 `View`로 생성한다.
|
||||||
- 입력값은 `selectedSort: ContentSort`, `sorts: [ContentSort] = ContentSort.allCases`, `onSelect: (ContentSort) -> Void`로 둔다.
|
- 입력값은 `selectedSort: ContentSort`, `sorts: [ContentSort] = ContentSort.allCases`, `onSelect: (ContentSort) -> Void`로 둔다.
|
||||||
- 항목은 `LATEST`, `POPULAR`, `PRICE_HIGH`, `PRICE_LOW` 순서로 표시한다.
|
- 항목은 `LATEST`, `POPULAR`, `PRICE_HIGH`, `PRICE_LOW` 순서로 표시한다.
|
||||||
- Figma context menu의 `추천순`은 표시하지 않는다.
|
- Figma context menu의 `추천순`은 표시하지 않는다.
|
||||||
- 선택된 항목은 배경색 또는 체크 아이콘 없이도 충분히 구분되는 foreground/background 상태를 적용한다.
|
- 선택된 항목은 배경색 또는 체크 아이콘 없이도 충분히 구분되는 foreground/background 상태를 적용한다.
|
||||||
- 검증 기준:
|
- 검증 기준:
|
||||||
- 실행 명령: `rg "struct CreatorChannelSortBottomSheet|selectedSort: ContentSort|ContentSort.allCases|onSelect|ForEach|sort.title" SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelSortBottomSheet.swift`
|
- 실행 명령: `rg "struct CreatorChannelSortContextPopup|selectedSort: ContentSort|ContentSort.allCases|onSelect|ForEach|sort.title" SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelSortContextPopup.swift`
|
||||||
- 기대 결과: ContentSort 4개 항목 기반 BottomSheet가 확인된다.
|
- 기대 결과: ContentSort 4개 항목 기반 context popup이 확인된다.
|
||||||
- 실행 명령: `rg "추천순|recommend" SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelSortBottomSheet.swift`
|
- 실행 명령: `rg "추천순|recommend" SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelSortContextPopup.swift`
|
||||||
- 기대 결과: 검색 결과가 없다.
|
- 기대 결과: 검색 결과가 없다.
|
||||||
|
|
||||||
### Phase 4: 라이브 탭 UI
|
### Phase 4: 라이브 탭 UI
|
||||||
@@ -223,13 +223,13 @@
|
|||||||
- 입력값은 `creatorId: Int`, `isOwnCreatorChannel: Bool`, `onTapLive: (Int) -> Void`, `onTapContent: (Int) -> Void`로 둔다.
|
- 입력값은 `creatorId: Int`, `isOwnCreatorChannel: Bool`, `onTapLive: (Int) -> Void`, `onTapContent: (Int) -> Void`로 둔다.
|
||||||
- 내부에서 `@StateObject private var viewModel = CreatorChannelLiveViewModel()`을 사용한다.
|
- 내부에서 `@StateObject private var viewModel = CreatorChannelLiveViewModel()`을 사용한다.
|
||||||
- `onAppear`에서 `hasLoaded == false`이면 `fetchFirstPage(creatorId:)`를 호출한다.
|
- `onAppear`에서 `hasLoaded == false`이면 `fetchFirstPage(creatorId:)`를 호출한다.
|
||||||
- sort-bar 터치 시 `.sheet` 또는 기존 앱 sheet 패턴으로 `CreatorChannelSortBottomSheet`를 표시한다.
|
- sort-bar 터치 시 `CreatorChannelSortContextPopup`를 표시한다.
|
||||||
- `currentLive != nil`이면 `CreatorChannelCurrentLiveSection(currentLive:onTapLive:)`를 표시한다.
|
- `currentLive != nil`이면 `CreatorChannelCurrentLiveSection(currentLive:onTapLive:)`를 표시한다.
|
||||||
- `liveReplayContents`는 `LazyVStack`으로 표시하고 각 item의 `onAppear`에서 `fetchNextPageIfNeeded`를 호출한다.
|
- `liveReplayContents`는 `LazyVStack`으로 표시하고 각 item의 `onAppear`에서 `fetchNextPageIfNeeded`를 호출한다.
|
||||||
- item tap은 `onTapContent(audioContent.audioContentId)`로 연결한다.
|
- item tap은 `onTapContent(audioContent.audioContentId)`로 연결한다.
|
||||||
- CTA가 부모 화면의 고정 overlay와 겹치지 않도록 scroll content bottom padding을 추가한다.
|
- CTA가 부모 화면의 고정 overlay와 겹치지 않도록 scroll content bottom padding을 추가한다.
|
||||||
- 검증 기준:
|
- 검증 기준:
|
||||||
- 실행 명령: `rg "struct CreatorChannelLiveTabView|CreatorChannelLiveViewModel|fetchFirstPage\\(creatorId: creatorId\\)|CreatorChannelSortBar|CreatorChannelSortBottomSheet|CreatorChannelCurrentLiveSection|CreatorChannelAudioContentListItem|fetchNextPageIfNeeded|onTapContent\\(audioContent.audioContentId\\)|isOwnCreatorChannel" SodaLive/Sources/V2/CreatorChannel/Live/CreatorChannelLiveTabView.swift`
|
- 실행 명령: `rg "struct CreatorChannelLiveTabView|CreatorChannelLiveViewModel|fetchFirstPage\\(creatorId: creatorId\\)|CreatorChannelSortBar|CreatorChannelSortContextPopup|CreatorChannelCurrentLiveSection|CreatorChannelAudioContentListItem|fetchNextPageIfNeeded|onTapContent\\(audioContent.audioContentId\\)|isOwnCreatorChannel" SodaLive/Sources/V2/CreatorChannel/Live/CreatorChannelLiveTabView.swift`
|
||||||
- 기대 결과: 라이브 탭 화면, 정렬, 현재 라이브, 다시듣기 목록, pagination이 확인된다.
|
- 기대 결과: 라이브 탭 화면, 정렬, 현재 라이브, 다시듣기 목록, pagination이 확인된다.
|
||||||
|
|
||||||
- [x] **Task 4.4: 라이브 탭 Empty 상태 추가**
|
- [x] **Task 4.4: 라이브 탭 Empty 상태 추가**
|
||||||
@@ -321,9 +321,9 @@
|
|||||||
- 확인: `SodaLive/Sources/V2/CreatorChannel`
|
- 확인: `SodaLive/Sources/V2/CreatorChannel`
|
||||||
- 확인: `SodaLive/Sources/I18n/I18n.swift`
|
- 확인: `SodaLive/Sources/I18n/I18n.swift`
|
||||||
- 작업 내용:
|
- 작업 내용:
|
||||||
- API endpoint, query parameter, 정렬 enum, `page=0`, `size=20`, sort-bar, BottomSheet, current live 재사용, 다시듣기 item, `ic_new_create_live`, 오디오 상세 이동 연결을 검색한다.
|
- API endpoint, query parameter, 정렬 enum, `page=0`, `size=20`, sort-bar, context popup, current live 재사용, 다시듣기 item, `ic_new_create_live`, 오디오 상세 이동 연결을 검색한다.
|
||||||
- 검증 기준:
|
- 검증 기준:
|
||||||
- 실행 명령: `rg "CreatorChannelLiveApi|/api/v2/creator-channels/|/live|URLEncoding.queryString|ContentSort|pageSize|20|page = 0|CreatorChannelSortBar|CreatorChannelSortBottomSheet|CreatorChannelCurrentLiveSection|CreatorChannelAudioContentListItem|ic_new_create_live|contentDetail\\(contentId:" SodaLive/Sources/V2/CreatorChannel SodaLive/Sources/I18n/I18n.swift SodaLive/Sources/App/AppStep.swift`
|
- 실행 명령: `rg "CreatorChannelLiveApi|/api/v2/creator-channels/|/live|URLEncoding.queryString|ContentSort|pageSize|20|page = 0|CreatorChannelSortBar|CreatorChannelSortContextPopup|CreatorChannelCurrentLiveSection|CreatorChannelAudioContentListItem|ic_new_create_live|contentDetail\\(contentId:" SodaLive/Sources/V2/CreatorChannel SodaLive/Sources/I18n/I18n.swift SodaLive/Sources/App/AppStep.swift`
|
||||||
- 기대 결과: PRD 핵심 구현 지점이 모두 검색된다.
|
- 기대 결과: PRD 핵심 구현 지점이 모두 검색된다.
|
||||||
|
|
||||||
- [x] **Task 7.2: 빌드 검증**
|
- [x] **Task 7.2: 빌드 검증**
|
||||||
@@ -342,7 +342,7 @@
|
|||||||
- 작업 내용:
|
- 작업 내용:
|
||||||
- 라이브 탭 진입 시 첫 요청이 `page=0`, `size=20`, `sort=LATEST`로 호출되는지 확인한다.
|
- 라이브 탭 진입 시 첫 요청이 `page=0`, `size=20`, `sort=LATEST`로 호출되는지 확인한다.
|
||||||
- sort-bar에 `최신순`이 표시되는지 확인한다.
|
- sort-bar에 `최신순`이 표시되는지 확인한다.
|
||||||
- sort-bar 터치 시 BottomSheet가 표시되고 4개 정렬만 보이는지 확인한다.
|
- sort-bar 터치 시 context popup이 표시되고 4개 정렬만 보이는지 확인한다.
|
||||||
- 정렬 변경 시 첫 페이지부터 다시 조회되는지 확인한다.
|
- 정렬 변경 시 첫 페이지부터 다시 조회되는지 확인한다.
|
||||||
- `currentLive != nil`인 경우 홈 탭 현재 라이브 UI와 동일한 카드가 표시되는지 확인한다.
|
- `currentLive != nil`인 경우 홈 탭 현재 라이브 UI와 동일한 카드가 표시되는지 확인한다.
|
||||||
- 마지막 item 노출 시 `hasNext == true`이면 `page + 1`이 호출되고 목록이 append되는지 확인한다.
|
- 마지막 item 노출 시 `hasNext == true`이면 `page + 1`이 호출되고 목록이 append되는지 확인한다.
|
||||||
@@ -355,6 +355,47 @@
|
|||||||
- 기대 결과: PRD Success Criteria를 모두 만족한다.
|
- 기대 결과: PRD Success Criteria를 모두 만족한다.
|
||||||
- blocked 사유: 실제 로그인 세션, 타인/본인 크리에이터 채널 테스트 데이터, 라이브/다시듣기 응답 데이터, 네트워크 요청 관찰 수단이 필요해 현재 CLI 환경에서는 완료 검증하지 않는다.
|
- blocked 사유: 실제 로그인 세션, 타인/본인 크리에이터 채널 테스트 데이터, 라이브/다시듣기 응답 데이터, 네트워크 요청 관찰 수단이 필요해 현재 CLI 환경에서는 완료 검증하지 않는다.
|
||||||
|
|
||||||
|
### Phase 8: sort-bar 전체 개수와 정렬 context popup 변경
|
||||||
|
|
||||||
|
- [x] **Task 8.1: sort-bar 전체 개수 표시 추가**
|
||||||
|
- 대상 파일:
|
||||||
|
- 수정: `SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelSortBar.swift`
|
||||||
|
- 수정: `SodaLive/Sources/V2/CreatorChannel/Live/CreatorChannelLiveTabView.swift`
|
||||||
|
- 수정: `SodaLive/Sources/V2/CreatorChannel/Audio/CreatorChannelAudioTabView.swift`
|
||||||
|
- 작업 내용:
|
||||||
|
- `CreatorChannelSortBar`에 `totalCount: Int?` 입력을 추가한다.
|
||||||
|
- `totalCount >= 0`이면 좌측에 `전체 {totalCount}`를 표시한다.
|
||||||
|
- 라이브 탭은 `liveReplayContentCount`, 오디오 탭은 `audioContentCount`를 전달한다.
|
||||||
|
- 검증 기준:
|
||||||
|
- 실행 명령: `rg "totalCount|전체|liveReplayContentCount|audioContentCount" SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelSortBar.swift SodaLive/Sources/V2/CreatorChannel/Live/CreatorChannelLiveTabView.swift SodaLive/Sources/V2/CreatorChannel/Audio/CreatorChannelAudioTabView.swift`
|
||||||
|
- 기대 결과: sort-bar 입력과 두 탭의 count 전달이 확인된다.
|
||||||
|
|
||||||
|
- [x] **Task 8.2: 정렬 UI를 context popup으로 변경**
|
||||||
|
- 대상 파일:
|
||||||
|
- 수정: `SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelSortContextPopup.swift`
|
||||||
|
- 수정: `SodaLive/Sources/V2/CreatorChannel/Live/CreatorChannelLiveTabView.swift`
|
||||||
|
- 수정: `SodaLive/Sources/V2/CreatorChannel/Audio/CreatorChannelAudioTabView.swift`
|
||||||
|
- 작업 내용:
|
||||||
|
- 기존 정렬 컴포넌트 내용을 anchored context popup 형태로 교체한다.
|
||||||
|
- popup은 `ContentSort.allCases` 4개만 표시한다.
|
||||||
|
- 선택 항목은 `Color.gray800`, 기본 항목은 `Color.gray900` 배경을 사용한다.
|
||||||
|
- sort-bar 터치 시 `.sheet`가 아니라 현재 화면 위 overlay로 popup을 표시한다.
|
||||||
|
- popup 밖 터치와 항목 선택 시 popup을 닫는다.
|
||||||
|
- 검증 기준:
|
||||||
|
- 실행 명령: `rg "CreatorChannelSortContextPopup|isSortPopupPresented|overlay|Color.gray700|cornerRadius: SodaSpacing.s14|\\.sheet\\(isPresented: \\$isSort" SodaLive/Sources/V2/CreatorChannel`
|
||||||
|
- 기대 결과: context popup 구현과 사용처가 확인되고 정렬 sheet 사용처는 검색되지 않는다.
|
||||||
|
|
||||||
|
- [x] **Task 8.3: 변경 검증**
|
||||||
|
- 대상 파일:
|
||||||
|
- 확인: `SodaLive/Sources/V2/CreatorChannel`
|
||||||
|
- 확인: `docs/20260703_크리에이터_채널_라이브_탭`
|
||||||
|
- 확인: `docs/20260704_크리에이터_채널_오디오_탭`
|
||||||
|
- 검증 기준:
|
||||||
|
- 실행 명령: `git diff --check -- docs/20260703_크리에이터_채널_라이브_탭 docs/20260704_크리에이터_채널_오디오_탭 SodaLive/Sources/V2/CreatorChannel`
|
||||||
|
- 기대 결과: 공백 오류 없이 성공한다.
|
||||||
|
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`
|
||||||
|
- 기대 결과: 빌드 성공.
|
||||||
|
|
||||||
## 검증 기록
|
## 검증 기록
|
||||||
|
|
||||||
- 2026-07-03: 문서 작성 단계. 구현/빌드 검증은 아직 수행하지 않음.
|
- 2026-07-03: 문서 작성 단계. 구현/빌드 검증은 아직 수행하지 않음.
|
||||||
@@ -365,9 +406,13 @@
|
|||||||
- 2026-07-04: Phase 6 리팩토링 완료. `CreatorChannelLiveReplayListItem`을 공용 `CreatorChannelAudioContentListItem`으로 이동/이름 변경하고, 홈 전용 `CreatorChannelAudioContentListItem`을 `CreatorChannelHomeAudioContentListItem`으로 변경함. `rg` 정적 검색, `plutil -lint SodaLive.xcodeproj/project.pbxproj`, `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`로 검증했으며 `** BUILD SUCCEEDED **`를 확인함.
|
- 2026-07-04: Phase 6 리팩토링 완료. `CreatorChannelLiveReplayListItem`을 공용 `CreatorChannelAudioContentListItem`으로 이동/이름 변경하고, 홈 전용 `CreatorChannelAudioContentListItem`을 `CreatorChannelHomeAudioContentListItem`으로 변경함. `rg` 정적 검색, `plutil -lint SodaLive.xcodeproj/project.pbxproj`, `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`로 검증했으며 `** BUILD SUCCEEDED **`를 확인함.
|
||||||
- 2026-07-04: Phase 5 구현 완료. `CreatorChannelView`의 `.live` 탭 분기에 `CreatorChannelLiveTabView`를 연결하고 `creatorId`, `isOwnCreatorChannel`, `showLiveDetail`, `showContentDetail`, `showCreateLive`를 전달함. 기존 `CreatorChannelFloatingActionMenu`는 본인 채널 `home` 탭 조건으로 유지함. `rg` 정적 검색, `plutil -lint SodaLive.xcodeproj/project.pbxproj`, `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`로 검증했으며 `** BUILD SUCCEEDED **`를 확인함.
|
- 2026-07-04: Phase 5 구현 완료. `CreatorChannelView`의 `.live` 탭 분기에 `CreatorChannelLiveTabView`를 연결하고 `creatorId`, `isOwnCreatorChannel`, `showLiveDetail`, `showContentDetail`, `showCreateLive`를 전달함. 기존 `CreatorChannelFloatingActionMenu`는 본인 채널 `home` 탭 조건으로 유지함. `rg` 정적 검색, `plutil -lint SodaLive.xcodeproj/project.pbxproj`, `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`로 검증했으며 `** BUILD SUCCEEDED **`를 확인함.
|
||||||
- 2026-07-04: `ContentSort`에서 소장 정렬 옵션을 제거하고 PRD/계획서의 정렬 옵션을 4개 기준으로 갱신함. `rg` 정적 검색, `plutil -lint SodaLive.xcodeproj/project.pbxproj`, `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`로 검증했으며 `** BUILD SUCCEEDED **`를 확인함.
|
- 2026-07-04: `ContentSort`에서 소장 정렬 옵션을 제거하고 PRD/계획서의 정렬 옵션을 4개 기준으로 갱신함. `rg` 정적 검색, `plutil -lint SodaLive.xcodeproj/project.pbxproj`, `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`로 검증했으며 `** BUILD SUCCEEDED **`를 확인함.
|
||||||
- 2026-07-04: Phase 7.1 정적 검색 검증 완료. API endpoint, query parameter, 정렬 enum 4개, pagination 기본값, sort-bar, BottomSheet, 현재 라이브 재사용, 공용 다시듣기 item, `ic_new_create_live`, 오디오 상세 이동 연결을 확인함. `plutil -lint SodaLive.xcodeproj/project.pbxproj`도 `OK`를 확인함.
|
- 2026-07-04: Phase 7.1 정적 검색 검증 완료. API endpoint, query parameter, 정렬 enum 4개, pagination 기본값, sort-bar, context popup, 현재 라이브 재사용, 공용 다시듣기 item, `ic_new_create_live`, 오디오 상세 이동 연결을 확인함. `plutil -lint SodaLive.xcodeproj/project.pbxproj`도 `OK`를 확인함.
|
||||||
- 2026-07-04: Phase 7.2 빌드 검증 완료. 최초 `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`는 DerivedData `build.db` lock으로 실패했으나, 5초 후 동일 명령 재시도에서 `** BUILD SUCCEEDED **`를 확인함.
|
- 2026-07-04: Phase 7.2 빌드 검증 완료. 최초 `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`는 DerivedData `build.db` lock으로 실패했으나, 5초 후 동일 명령 재시도에서 `** BUILD SUCCEEDED **`를 확인함.
|
||||||
- 2026-07-04: Phase 7.3 수동 동작 검증은 blocked. iOS Simulator는 사용 가능하나 실제 로그인 세션, 타인/본인 크리에이터 채널 테스트 데이터, 라이브/다시듣기 응답 데이터, 네트워크 요청 관찰 수단이 없어 CLI 환경에서 PRD Success Criteria 전체를 완료 검증하지 않음.
|
- 2026-07-04: Phase 7.3 수동 동작 검증은 blocked. iOS Simulator는 사용 가능하나 실제 로그인 세션, 타인/본인 크리에이터 채널 테스트 데이터, 라이브/다시듣기 응답 데이터, 네트워크 요청 관찰 수단이 없어 CLI 환경에서 PRD Success Criteria 전체를 완료 검증하지 않음.
|
||||||
- 2026-07-04: 리뷰 반영. sort-bar 카운트 표시 요구사항이 제거되어 PRD/계획서를 현재 정렬 표시 기준으로 갱신함.
|
- 2026-07-04: 리뷰 반영. sort-bar 카운트 표시 요구사항이 제거되어 PRD/계획서를 현재 정렬 표시 기준으로 갱신함.
|
||||||
- 2026-07-04: 리뷰 반영. 본인 채널 라이브 시작하기 CTA를 `CreatorChannelLiveTabView` 내부 scroll content overlay에서 `CreatorChannelView` 최상위 overlay로 이동해 화면 하단 고정 구조로 변경함.
|
- 2026-07-04: 리뷰 반영. 본인 채널 라이브 시작하기 CTA를 `CreatorChannelLiveTabView` 내부 scroll content overlay에서 `CreatorChannelView` 최상위 overlay로 이동해 화면 하단 고정 구조로 변경함.
|
||||||
- 2026-07-04: 요구사항 추가 반영. 진행 중인 라이브가 없고 라이브 다시듣기 콘텐츠가 0개인 경우 sort-bar/list를 숨기고 Empty 문구만 표시하도록 PRD/계획/구현을 갱신함.
|
- 2026-07-04: 요구사항 추가 반영. 진행 중인 라이브가 없고 라이브 다시듣기 콘텐츠가 0개인 경우 sort-bar/list를 숨기고 Empty 문구만 표시하도록 PRD/계획/구현을 갱신함.
|
||||||
|
- 2026-07-04: Figma `290:9034`, `290:9031`을 확인하고 Phase 8을 추가함. sort-bar 전체 개수 표시와 context popup 전환을 후속 변경 범위로 정의함.
|
||||||
|
- 2026-07-04: Phase 8 구현 완료. `CreatorChannelSortBar`에 `totalCount`를 추가하고 라이브/오디오 탭에서 각각 `liveReplayContentCount`, `audioContentCount`를 전달함. 기존 정렬 타입을 `CreatorChannelSortContextPopup`으로 교체하고 `.sheet` 사용을 overlay popup으로 변경함.
|
||||||
|
- 2026-07-04: Phase 8 검증 완료. `rg "totalCount|전체|liveReplayContentCount|audioContentCount" ...`, `rg "CreatorChannelSortContextPopup|isSortPopupPresented|Color.gray700|cornerRadius: SodaSpacing.s14|\\.sheet\\(isPresented: \\$isSort" ...`, `git diff --check -- docs/20260703_크리에이터_채널_라이브_탭 docs/20260704_크리에이터_채널_오디오_탭 SodaLive/Sources/V2/CreatorChannel`, `plutil -lint SodaLive.xcodeproj/project.pbxproj`를 확인함. 샌드박스 안 빌드는 Xcode 캐시 권한 문제로 실패했으나, 샌드박스 밖 `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`에서 `** BUILD SUCCEEDED **`를 확인함.
|
||||||
|
- 2026-07-04: 후속 반영. 정렬 popup 파일명을 `CreatorChannelSortContextPopup.swift`로 변경하고 Xcode project 참조와 문서 참조를 갱신함. 이전 정렬 파일명/타입 참조가 남지 않았음을 확인하고, 샌드박스 밖 `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`에서 `** BUILD SUCCEEDED **`를 확인함.
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ Figma 참조:
|
|||||||
- 현재 크리에이터 채널 공통 shell은 홈 탭 중심으로 구현되어 있고, `라이브` 탭의 실제 목록 API, 정렬, 페이지네이션이 연결되어 있지 않다.
|
- 현재 크리에이터 채널 공통 shell은 홈 탭 중심으로 구현되어 있고, `라이브` 탭의 실제 목록 API, 정렬, 페이지네이션이 연결되어 있지 않다.
|
||||||
- 라이브 탭은 홈 탭의 현재 진행 중인 라이브 UI를 재사용해야 하지만, 라이브 다시듣기 목록은 가격, 포인트 사용 가능, 19금, 소장중, 대여중 상태를 함께 표시해야 하므로 별도 item 요구사항이 필요하다.
|
- 라이브 탭은 홈 탭의 현재 진행 중인 라이브 UI를 재사용해야 하지만, 라이브 다시듣기 목록은 가격, 포인트 사용 가능, 19금, 소장중, 대여중 상태를 함께 표시해야 하므로 별도 item 요구사항이 필요하다.
|
||||||
- sort-bar는 라이브 탭 전용이 아니라 다른 탭에서도 같은 형태로 재사용될 수 있어 채널 공통 컴포넌트 경계가 필요하다.
|
- sort-bar는 라이브 탭 전용이 아니라 다른 탭에서도 같은 형태로 재사용될 수 있어 채널 공통 컴포넌트 경계가 필요하다.
|
||||||
- 정렬 선택 UI는 Figma에 anchored context menu 형태가 있으나, 모바일 하단 접근성과 옵션 확장성을 고려해 앱 구현 방식 결정이 필요하다.
|
- 정렬 선택 UI는 기존 BottomSheet에서 Figma와 같은 anchored context popup 형태로 변경해야 한다.
|
||||||
- 본인 채널의 라이브 탭에서는 하단에 고정된 `라이브 시작하기` CTA가 필요하며, 일반 사용자 채널 진입과 표시 조건이 달라야 한다.
|
- 본인 채널의 라이브 탭에서는 하단에 고정된 `라이브 시작하기` CTA가 필요하며, 일반 사용자 채널 진입과 표시 조건이 달라야 한다.
|
||||||
|
|
||||||
## 3. Goals
|
## 3. Goals
|
||||||
@@ -28,7 +28,8 @@ Figma 참조:
|
|||||||
- 기본 정렬은 `LATEST`로 호출하고, 서버 응답의 `sort`를 현재 선택 정렬 상태로 반영한다.
|
- 기본 정렬은 `LATEST`로 호출하고, 서버 응답의 `sort`를 현재 선택 정렬 상태로 반영한다.
|
||||||
- sort-bar는 `SodaLive/Sources/V2/CreatorChannel/Components/**` 아래에 생성해 라이브 탭 외 다른 탭에서도 재사용할 수 있게 한다.
|
- sort-bar는 `SodaLive/Sources/V2/CreatorChannel/Components/**` 아래에 생성해 라이브 탭 외 다른 탭에서도 재사용할 수 있게 한다.
|
||||||
- 정렬 문구는 `I18n`에 ko/en/ja 다국어로 추가한다.
|
- 정렬 문구는 `I18n`에 ko/en/ja 다국어로 추가한다.
|
||||||
- 정렬 선택 UI는 BottomSheet 방식을 채택한다.
|
- sort-bar는 전체 개수가 `0` 이상으로 내려오면 `전체 23` 형식으로 개수를 표시한다.
|
||||||
|
- 정렬 선택 UI는 Figma와 같은 context popup 방식을 채택한다.
|
||||||
- 현재 진행 중인 라이브는 크리에이터 채널 홈의 `CreatorChannelCurrentLiveSection` UI를 재사용한다.
|
- 현재 진행 중인 라이브는 크리에이터 채널 홈의 `CreatorChannelCurrentLiveSection` UI를 재사용한다.
|
||||||
- 라이브 다시듣기 item은 Figma 기준으로 썸네일, 제목, duration, 가격/포인트/무료/19금/소장중/대여중 상태를 표시한다.
|
- 라이브 다시듣기 item은 Figma 기준으로 썸네일, 제목, duration, 가격/포인트/무료/19금/소장중/대여중 상태를 표시한다.
|
||||||
- `hasNext == true`이면 스크롤 하단 진입 시 현재 응답의 `page + 1` 페이지를 추가 조회한다.
|
- `hasNext == true`이면 스크롤 하단 진입 시 현재 응답의 `page + 1` 페이지를 추가 조회한다.
|
||||||
@@ -140,25 +141,26 @@ enum ContentSort: String, Decodable, CaseIterable {
|
|||||||
### 7.3 Sort-bar
|
### 7.3 Sort-bar
|
||||||
- 생성 위치: `SodaLive/Sources/V2/CreatorChannel/Components/**`
|
- 생성 위치: `SodaLive/Sources/V2/CreatorChannel/Components/**`
|
||||||
- sort-bar는 현재 정렬명과 아래 방향 아이콘을 표시한다.
|
- sort-bar는 현재 정렬명과 아래 방향 아이콘을 표시한다.
|
||||||
|
- `totalCount`가 `0` 이상이면 sort-bar 좌측에 `전체 {totalCount}`를 표시한다.
|
||||||
|
- `totalCount`가 `nil`이거나 음수이면 전체 개수 영역은 숨긴다.
|
||||||
- Figma 기준 높이는 `52pt`, 좌우 horizontal padding은 기존 `SodaSpacing` 토큰으로 맞춘다.
|
- Figma 기준 높이는 `52pt`, 좌우 horizontal padding은 기존 `SodaSpacing` 토큰으로 맞춘다.
|
||||||
- 정렬명은 `I18n`에 추가한다.
|
- 정렬명은 `I18n`에 추가한다.
|
||||||
- sort-bar는 다른 탭에서도 재사용 가능하도록 `selectedSort`, `availableSorts`, `onTapSort` 같은 일반화된 입력을 받는다.
|
- sort-bar는 다른 탭에서도 재사용 가능하도록 `selectedSort`, `availableSorts`, `onTapSort` 같은 일반화된 입력을 받는다.
|
||||||
- 이번 범위에서는 Figma의 `리스트형` view mode 옵션은 사용하지 않는다.
|
- 이번 범위에서는 Figma의 `리스트형` view mode 옵션은 사용하지 않는다.
|
||||||
|
|
||||||
### 7.4 정렬 선택 UI
|
### 7.4 정렬 선택 UI
|
||||||
- 정렬 선택 UI는 BottomSheet 방식으로 구현한다.
|
- 정렬 선택 UI는 Figma `290:9041`처럼 sort-bar 우측 정렬 버튼 아래에 붙는 context popup으로 구현한다.
|
||||||
- 선택 사유:
|
- popup은 `Color.gray700` 1pt border, `14pt` corner radius, `Color.gray900` 기본 배경을 사용한다.
|
||||||
- 모바일 한 손 조작에서 화면 하단 sheet가 우측 상단 anchored menu보다 터치 접근성이 좋다.
|
- 선택된 항목은 `Color.gray800` 배경으로 표시한다.
|
||||||
- 정렬 옵션이 4개이고 이후 탭별 옵션 추가 가능성이 있어 sheet가 선택 영역과 dismiss 동작을 안정적으로 제공한다.
|
- popup 밖을 터치하거나 항목을 선택하면 닫힌다.
|
||||||
- iOS/Material 계열 최신 모바일 앱에서 필터/정렬 같은 목록 제어는 sheet 패턴이 일반적이며, context menu는 짧은 보조 action에 더 적합하다.
|
- 정렬 popup 항목:
|
||||||
- 정렬 sheet 항목:
|
|
||||||
- `LATEST`: 최신순
|
- `LATEST`: 최신순
|
||||||
- `POPULAR`: 인기순
|
- `POPULAR`: 인기순
|
||||||
- `PRICE_HIGH`: 가격 높은 순
|
- `PRICE_HIGH`: 가격 높은 순
|
||||||
- `PRICE_LOW`: 가격 낮은 순
|
- `PRICE_LOW`: 가격 낮은 순
|
||||||
- Figma `290:9041`에는 `추천순`이 있으나 API `ContentSort`에 없으므로 구현하지 않는다.
|
- Figma `290:9041`에는 `추천순`이 있으나 API `ContentSort`에 없으므로 구현하지 않는다.
|
||||||
- 현재 선택된 sort는 시각적으로 선택 상태를 표시한다.
|
- 현재 선택된 sort는 시각적으로 선택 상태를 표시한다.
|
||||||
- sort item 터치 시 sheet를 닫고 첫 페이지를 재조회한다.
|
- sort item 터치 시 popup을 닫고 첫 페이지를 재조회한다.
|
||||||
|
|
||||||
### 7.5 현재 진행 중인 라이브
|
### 7.5 현재 진행 중인 라이브
|
||||||
- `currentLive != nil`이면 sort-bar 아래 또는 Figma 배치 기준 위치에 현재 진행 중인 라이브 UI를 표시한다.
|
- `currentLive != nil`이면 sort-bar 아래 또는 Figma 배치 기준 위치에 현재 진행 중인 라이브 UI를 표시한다.
|
||||||
@@ -212,7 +214,7 @@ enum ContentSort: String, Decodable, CaseIterable {
|
|||||||
- sort-bar는 tab-bar 아래에 붙어 보이고, 목록 콘텐츠와 겹치지 않아야 한다.
|
- sort-bar는 tab-bar 아래에 붙어 보이고, 목록 콘텐츠와 겹치지 않아야 한다.
|
||||||
- Empty 상태에서는 sort-bar와 list가 보이지 않아야 한다.
|
- Empty 상태에서는 sort-bar와 list가 보이지 않아야 한다.
|
||||||
- 다시듣기 item의 썸네일, 제목, 가격/상태 영역은 긴 제목과 다국어 문구에서도 겹치지 않아야 한다.
|
- 다시듣기 item의 썸네일, 제목, 가격/상태 영역은 긴 제목과 다국어 문구에서도 겹치지 않아야 한다.
|
||||||
- BottomSheet는 dim 또는 drag/dismiss 동작이 기존 앱 sheet 패턴과 일관되어야 한다.
|
- context popup은 sort-bar 우측 버튼 아래에 겹치지 않게 표시되고, 목록 콘텐츠 위에 떠야 한다.
|
||||||
- 로딩 중에는 기존 목록을 과도하게 흔들지 않고, 첫 페이지 로딩과 추가 페이지 로딩 상태를 구분한다. 목록 교체와 append는 조회 성공 시에만 수행한다.
|
- 로딩 중에는 기존 목록을 과도하게 흔들지 않고, 첫 페이지 로딩과 추가 페이지 로딩 상태를 구분한다. 목록 교체와 append는 조회 성공 시에만 수행한다.
|
||||||
|
|
||||||
## 9. Technical Constraints
|
## 9. Technical Constraints
|
||||||
@@ -229,7 +231,7 @@ enum ContentSort: String, Decodable, CaseIterable {
|
|||||||
- 라이브 탭 선택 시 `GET /api/v2/creator-channels/{creatorId}/live?page={page}&size={size}&sort={sort}`가 호출된다.
|
- 라이브 탭 선택 시 `GET /api/v2/creator-channels/{creatorId}/live?page={page}&size={size}&sort={sort}`가 호출된다.
|
||||||
- 기본 sort는 `LATEST`이며 sort-bar에는 `최신순`이 표시된다.
|
- 기본 sort는 `LATEST`이며 sort-bar에는 `최신순`이 표시된다.
|
||||||
- 최초 요청은 `page=0`, `size=20`으로 호출된다.
|
- 최초 요청은 `page=0`, `size=20`으로 호출된다.
|
||||||
- sort-bar 터치 시 BottomSheet가 표시되고 `LATEST`, `POPULAR`, `PRICE_HIGH`, `PRICE_LOW` 4개 옵션만 보인다.
|
- sort-bar 터치 시 context popup이 표시되고 `LATEST`, `POPULAR`, `PRICE_HIGH`, `PRICE_LOW` 4개 옵션만 보인다.
|
||||||
- 정렬 변경 시 첫 페이지부터 다시 조회되고, 조회 성공 시에만 목록이 새 정렬 결과로 교체된다.
|
- 정렬 변경 시 첫 페이지부터 다시 조회되고, 조회 성공 시에만 목록이 새 정렬 결과로 교체된다.
|
||||||
- `currentLive != nil`이면 홈 탭과 동일한 현재 라이브 UI가 라이브 탭에 표시된다.
|
- `currentLive != nil`이면 홈 탭과 동일한 현재 라이브 UI가 라이브 탭에 표시된다.
|
||||||
- `currentLive == nil`이고 `liveReplayContentCount == 0`이면 sort-bar/list 없이 `크리에이터가 라이브를 준비중입니다.\n기대해 주세요!` Empty 문구만 표시된다.
|
- `currentLive == nil`이고 `liveReplayContentCount == 0`이면 sort-bar/list 없이 `크리에이터가 라이브를 준비중입니다.\n기대해 주세요!` Empty 문구만 표시된다.
|
||||||
@@ -242,7 +244,7 @@ enum ContentSort: String, Decodable, CaseIterable {
|
|||||||
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`가 성공한다.
|
- `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`가 성공한다.
|
||||||
|
|
||||||
## 11. Decisions
|
## 11. Decisions
|
||||||
- 정렬 메뉴 표시는 BottomSheet 방식을 채택한다.
|
- 정렬 메뉴 표시는 sort-bar 우측 버튼 아래 anchored context popup 방식을 채택한다.
|
||||||
- 첫 페이지의 `page` 시작값은 `0`으로 한다.
|
- 첫 페이지의 `page` 시작값은 `0`으로 한다.
|
||||||
- 요청 `size` 기본값은 `20`으로 한다.
|
- 요청 `size` 기본값은 `20`으로 한다.
|
||||||
- Figma 정렬 메뉴 예시에 있는 `추천순`은 API enum에 없으므로 제외한다.
|
- Figma 정렬 메뉴 예시에 있는 `추천순`은 API enum에 없으므로 제외한다.
|
||||||
|
|||||||
@@ -16,7 +16,7 @@
|
|||||||
|
|
||||||
- 기존 `CreatorChannelView`의 공통 shell, header, sticky tab-bar, title bar 동작은 유지한다.
|
- 기존 `CreatorChannelView`의 공통 shell, header, sticky tab-bar, title bar 동작은 유지한다.
|
||||||
- `CreatorChannelTab.audio` 콘텐츠만 신규 `CreatorChannelAudioTabView`로 교체한다.
|
- `CreatorChannelTab.audio` 콘텐츠만 신규 `CreatorChannelAudioTabView`로 교체한다.
|
||||||
- `ContentSort`, `CreatorChannelSortBar`, `CreatorChannelSortBottomSheet`, `CapsuleTabBar`, `CreatorChannelAudioContentListItem`을 재사용한다.
|
- `ContentSort`, `CreatorChannelSortBar`, 정렬 context popup, `CapsuleTabBar`, `CreatorChannelAudioContentListItem`을 재사용한다.
|
||||||
- 전체 테마는 API 모델에 섞지 않고 오디오 탭 표시 모델에서만 합성한다.
|
- 전체 테마는 API 모델에 섞지 않고 오디오 탭 표시 모델에서만 합성한다.
|
||||||
- `themeId == nil`이면 query parameter를 보내지 않는다.
|
- `themeId == nil`이면 query parameter를 보내지 않는다.
|
||||||
- `duration == nil` 또는 빈 문자열인 콘텐츠는 ViewModel에서 목록에 포함하지 않는다.
|
- `duration == nil` 또는 빈 문자열인 콘텐츠는 ViewModel에서 목록에 포함하지 않는다.
|
||||||
@@ -134,6 +134,21 @@
|
|||||||
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`
|
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`
|
||||||
- 기대 결과: 빌드 성공.
|
- 기대 결과: 빌드 성공.
|
||||||
|
|
||||||
|
### Phase 5: 공용 sort-bar 후속 변경 반영
|
||||||
|
|
||||||
|
- [x] **Task 5.1: 전체 개수와 context popup 재사용 확인**
|
||||||
|
- 대상 파일:
|
||||||
|
- 수정: `SodaLive/Sources/V2/CreatorChannel/Audio/CreatorChannelAudioTabView.swift`
|
||||||
|
- 확인: `SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelSortBar.swift`
|
||||||
|
- 확인: `SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelSortContextPopup.swift`
|
||||||
|
- 작업 내용:
|
||||||
|
- `CreatorChannelSortBar`에 `audioContentCount`를 전달한다.
|
||||||
|
- 정렬 선택은 `.sheet`가 아닌 공용 context popup overlay를 사용한다.
|
||||||
|
- popup 항목은 기존 `ContentSort.allCases` 4개를 유지한다.
|
||||||
|
- 검증 기준:
|
||||||
|
- 실행 명령: `rg "audioContentCount|CreatorChannelSortContextPopup|isSortPopupPresented|\\.sheet\\(isPresented: \\$isSort" SodaLive/Sources/V2/CreatorChannel/Audio/CreatorChannelAudioTabView.swift SodaLive/Sources/V2/CreatorChannel/Components`
|
||||||
|
- 기대 결과: 오디오 탭의 count 전달과 popup 사용이 확인되고 정렬 sheet 사용처는 검색되지 않는다.
|
||||||
|
|
||||||
## 검증 기록
|
## 검증 기록
|
||||||
|
|
||||||
- 2026-07-04: `get_design_context`와 `get_screenshot`으로 Figma `290:9015`, `290:9029`, `290:8965`, `665:19008`을 확인했다.
|
- 2026-07-04: `get_design_context`와 `get_screenshot`으로 Figma `290:9015`, `290:9029`, `290:8965`, `665:19008`을 확인했다.
|
||||||
@@ -141,3 +156,7 @@
|
|||||||
- 2026-07-04: `git diff --check -- docs/20260704_크리에이터_채널_오디오_탭 SodaLive/Sources/V2/CreatorChannel SodaLive/Sources/I18n/I18n.swift SodaLive.xcodeproj/project.pbxproj` 실행 결과 출력 없이 성공했다.
|
- 2026-07-04: `git diff --check -- docs/20260704_크리에이터_채널_오디오_탭 SodaLive/Sources/V2/CreatorChannel SodaLive/Sources/I18n/I18n.swift SodaLive.xcodeproj/project.pbxproj` 실행 결과 출력 없이 성공했다.
|
||||||
- 2026-07-04: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`는 `SodaLive.xcworkspace`를 workspace로 인식하지 못해 실패했다.
|
- 2026-07-04: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`는 `SodaLive.xcworkspace`를 workspace로 인식하지 못해 실패했다.
|
||||||
- 2026-07-04: 대체로 `xcodebuild -project "SodaLive.xcodeproj" -scheme "SodaLive-dev" -configuration Debug build`를 샌드박스 밖에서 실행했다. SwiftPM 패키지 해석과 외부 패키지 컴파일은 진행됐으나 기존 외부 모듈 `Bootpay`, `BootpayUI`, `AgoraRtmKit`, `GoogleSignIn`, `YandexMobileAds`를 찾지 못해 앱 타깃 SwiftDriver 단계에서 실패했다.
|
- 2026-07-04: 대체로 `xcodebuild -project "SodaLive.xcodeproj" -scheme "SodaLive-dev" -configuration Debug build`를 샌드박스 밖에서 실행했다. SwiftPM 패키지 해석과 외부 패키지 컴파일은 진행됐으나 기존 외부 모듈 `Bootpay`, `BootpayUI`, `AgoraRtmKit`, `GoogleSignIn`, `YandexMobileAds`를 찾지 못해 앱 타깃 SwiftDriver 단계에서 실패했다.
|
||||||
|
- 2026-07-04: Figma `290:9034`, `290:9031`을 확인하고 Phase 5를 추가함. 오디오 탭도 공용 sort-bar 전체 개수 표시와 정렬 context popup 전환을 재사용하도록 범위를 갱신함.
|
||||||
|
- 2026-07-04: Phase 5 구현 완료. `CreatorChannelAudioTabView`에서 `audioContentCount`를 `CreatorChannelSortBar`에 전달하고 정렬 UI를 `.sheet`에서 `CreatorChannelSortContextPopup` overlay로 변경함.
|
||||||
|
- 2026-07-04: Phase 5 검증 완료. `rg "audioContentCount|CreatorChannelSortContextPopup|isSortPopupPresented|\\.sheet\\(isPresented: \\$isSort" ...`, `git diff --check -- docs/20260703_크리에이터_채널_라이브_탭 docs/20260704_크리에이터_채널_오디오_탭 SodaLive/Sources/V2/CreatorChannel`, `plutil -lint SodaLive.xcodeproj/project.pbxproj`를 확인함. 샌드박스 밖 `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`에서 `** BUILD SUCCEEDED **`를 확인함.
|
||||||
|
- 2026-07-04: 후속 반영. 정렬 popup 파일명을 `CreatorChannelSortContextPopup.swift`로 변경하고 Xcode project 참조와 문서 참조를 갱신함. 이전 정렬 파일명/타입 참조가 남지 않았음을 확인하고, 샌드박스 밖 `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`에서 `** BUILD SUCCEEDED **`를 확인함.
|
||||||
|
|||||||
@@ -21,7 +21,8 @@ Figma 참조:
|
|||||||
- `page`, `size`, `sort`, 선택된 `themeId`를 query parameter로 전달한다.
|
- `page`, `size`, `sort`, 선택된 `themeId`를 query parameter로 전달한다.
|
||||||
- 기본값은 `page=0`, `size=20`, `sort=LATEST`, `themeId=nil`이다.
|
- 기본값은 `page=0`, `size=20`, `sort=LATEST`, `themeId=nil`이다.
|
||||||
- theme list는 `CapsuleTabBar`를 사용하고, 맨 앞에 전체 항목을 합성한다.
|
- theme list는 `CapsuleTabBar`를 사용하고, 맨 앞에 전체 항목을 합성한다.
|
||||||
- 정렬 선택 방식은 라이브 탭과 동일한 `CreatorChannelSortBar` + `CreatorChannelSortBottomSheet`를 재사용한다.
|
- 정렬 선택 방식은 라이브 탭과 동일한 `CreatorChannelSortBar` + context popup을 재사용한다.
|
||||||
|
- sort-bar는 `audioContentCount`가 `0` 이상이면 `전체 {audioContentCount}`를 표시한다.
|
||||||
- 콘텐츠 item은 `CreatorChannelAudioContentListItem`을 재사용한다.
|
- 콘텐츠 item은 `CreatorChannelAudioContentListItem`을 재사용한다.
|
||||||
- 전체 테마이고 본인 채널이 아닐 때만 소장률 UI를 표시한다.
|
- 전체 테마이고 본인 채널이 아닐 때만 소장률 UI를 표시한다.
|
||||||
- 본인 채널의 오디오 탭에는 `ic_new_upload_audio` 아이콘을 쓰는 하단 CTA를 표시한다.
|
- 본인 채널의 오디오 탭에는 `ic_new_upload_audio` 아이콘을 쓰는 하단 CTA를 표시한다.
|
||||||
@@ -103,7 +104,7 @@ data class CreatorChannelAudioThemeResponse(
|
|||||||
- 오디오 탭 진입 시 `GET /api/v2/creator-channels/{creatorId}/audio?page=0&size=20&sort=LATEST`가 호출되고, 전체 선택 상태에서는 `themeId`가 전송되지 않는다.
|
- 오디오 탭 진입 시 `GET /api/v2/creator-channels/{creatorId}/audio?page=0&size=20&sort=LATEST`가 호출되고, 전체 선택 상태에서는 `themeId`가 전송되지 않는다.
|
||||||
- `themes` 앞에 전체 탭이 표시되고, 응답 `themeId == nil`이면 전체가 선택된다.
|
- `themes` 앞에 전체 탭이 표시되고, 응답 `themeId == nil`이면 전체가 선택된다.
|
||||||
- 테마 선택 시 선택한 `themeId`로 첫 페이지를 다시 조회한다.
|
- 테마 선택 시 선택한 `themeId`로 첫 페이지를 다시 조회한다.
|
||||||
- 정렬 선택 UI와 동작은 라이브 탭과 동일하다.
|
- 정렬 선택 UI는 BottomSheet가 아니라 sort-bar 우측 버튼 아래 anchored context popup으로 표시되고, 동작은 라이브 탭과 동일하다.
|
||||||
- 본인 채널이 아니고 전체 테마일 때만 소장률 UI가 표시된다.
|
- 본인 채널이 아니고 전체 테마일 때만 소장률 UI가 표시된다.
|
||||||
- 본인 채널이거나 테마 필터 상태이면 소장률 UI가 표시되지 않는다.
|
- 본인 채널이거나 테마 필터 상태이면 소장률 UI가 표시되지 않는다.
|
||||||
- `duration`이 없는 콘텐츠는 목록에 표시되지 않는다.
|
- `duration`이 없는 콘텐츠는 목록에 표시되지 않는다.
|
||||||
@@ -113,9 +114,10 @@ data class CreatorChannelAudioThemeResponse(
|
|||||||
## 7. Technical Constraints
|
## 7. Technical Constraints
|
||||||
- 기능 변경은 `SodaLive/Sources/V2/CreatorChannel/**` 하위에서 해결한다.
|
- 기능 변경은 `SodaLive/Sources/V2/CreatorChannel/**` 하위에서 해결한다.
|
||||||
- 오디오 탭 전용 View, ViewModel, Repository, API, 모델은 `SodaLive/Sources/V2/CreatorChannel/Audio/**` 아래에 둔다.
|
- 오디오 탭 전용 View, ViewModel, Repository, API, 모델은 `SodaLive/Sources/V2/CreatorChannel/Audio/**` 아래에 둔다.
|
||||||
- 공용 item, sort bar, sort bottom sheet, `CapsuleTabBar`는 기존 구현을 재사용한다.
|
- 공용 item, sort bar, sort context popup, `CapsuleTabBar`는 기존 구현을 재사용한다.
|
||||||
- 신규 문구는 `SodaLive/Sources/I18n/I18n.swift`에 ko/en/ja를 추가한다.
|
- 신규 문구는 `SodaLive/Sources/I18n/I18n.swift`에 ko/en/ja를 추가한다.
|
||||||
|
|
||||||
## 8. Verification Notes
|
## 8. Verification Notes
|
||||||
- 2026-07-04: Figma `get_design_context`와 `get_screenshot`으로 `290:9015`, `290:9029`, `290:8965`, `665:19008`을 확인했다.
|
- 2026-07-04: Figma `get_design_context`와 `get_screenshot`으로 `290:9015`, `290:9029`, `290:8965`, `665:19008`을 확인했다.
|
||||||
- 2026-07-04: `CreatorChannelLiveTabView`, `CreatorChannelLiveViewModel`, `CreatorChannelSortBar`, `CapsuleTabBar`, `CreatorChannelAudioContentListItem` 기존 구현을 확인해 재사용 경계를 확정했다.
|
- 2026-07-04: `CreatorChannelLiveTabView`, `CreatorChannelLiveViewModel`, `CreatorChannelSortBar`, `CapsuleTabBar`, `CreatorChannelAudioContentListItem` 기존 구현을 확인해 재사용 경계를 확정했다.
|
||||||
|
- 2026-07-04: Figma `290:9034`, `290:9031`을 추가 확인해 sort-bar 전체 개수 표시와 정렬 context popup 전환 요구사항을 반영했다.
|
||||||
|
|||||||
Reference in New Issue
Block a user