feat(creator): 정렬 팝업을 적용한다

This commit is contained in:
Yu Sung
2026-07-04 17:11:19 +09:00
parent c4c18341a6
commit 0dd111f5d6
10 changed files with 306 additions and 161 deletions

View File

@@ -1708,7 +1708,7 @@
CC0101292FF0100000A5BBA3 /* CreatorChannelFloatingActionMenu.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC0100292FF0100000A5BBA3 /* CreatorChannelFloatingActionMenu.swift */; };
CC01012A2FF0100000A5BBA3 /* ContentSort.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002A2FF0100000A5BBA3 /* ContentSort.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 */; };
CC01012E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002E2FF0100000A5BBA3 /* CreatorChannelLiveApi.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 */; };
CC01022A2FF0100000A5BBA3 /* ContentSort.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002A2FF0100000A5BBA3 /* ContentSort.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 */; };
CC01022E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002E2FF0100000A5BBA3 /* CreatorChannelLiveApi.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 */; };
CC01032A2FF0100000A5BBA3 /* ContentSort.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002A2FF0100000A5BBA3 /* ContentSort.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 */; };
CC01032E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = CC01002E2FF0100000A5BBA3 /* CreatorChannelLiveApi.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>"; };
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>"; };
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>"; };
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>"; };
@@ -5313,7 +5313,7 @@
CC0100132FF0100000A5BBA3 /* CreatorChannelTabBar.swift */,
CC0100292FF0100000A5BBA3 /* CreatorChannelFloatingActionMenu.swift */,
CC01002B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift */,
CC01002C2FF0100000A5BBA3 /* CreatorChannelSortBottomSheet.swift */,
CC01002C2FF0100000A5BBA3 /* CreatorChannelSortContextPopup.swift */,
CC0200012FF0200000A5BBA3 /* CreatorChannelAudioContentListItem.swift */,
);
path = Components;
@@ -5846,7 +5846,7 @@
CC0101102FF0100000A5BBA3 /* CreatorChannelTab.swift in Sources */,
CC01012A2FF0100000A5BBA3 /* ContentSort.swift in Sources */,
CC01012B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift in Sources */,
CC01012C2FF0100000A5BBA3 /* CreatorChannelSortBottomSheet.swift in Sources */,
CC01012C2FF0100000A5BBA3 /* CreatorChannelSortContextPopup.swift in Sources */,
CC01012D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift in Sources */,
CC01012E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift in Sources */,
CC01012F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift in Sources */,
@@ -6675,7 +6675,7 @@
CC0102102FF0100000A5BBA3 /* CreatorChannelTab.swift in Sources */,
CC01022A2FF0100000A5BBA3 /* ContentSort.swift in Sources */,
CC01022B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift in Sources */,
CC01022C2FF0100000A5BBA3 /* CreatorChannelSortBottomSheet.swift in Sources */,
CC01022C2FF0100000A5BBA3 /* CreatorChannelSortContextPopup.swift in Sources */,
CC01022D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift in Sources */,
CC01022E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift in Sources */,
CC01022F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift in Sources */,
@@ -7520,7 +7520,7 @@
CC0103102FF0100000A5BBA3 /* CreatorChannelTab.swift in Sources */,
CC01032A2FF0100000A5BBA3 /* ContentSort.swift in Sources */,
CC01032B2FF0100000A5BBA3 /* CreatorChannelSortBar.swift in Sources */,
CC01032C2FF0100000A5BBA3 /* CreatorChannelSortBottomSheet.swift in Sources */,
CC01032C2FF0100000A5BBA3 /* CreatorChannelSortContextPopup.swift in Sources */,
CC01032D2FF0100000A5BBA3 /* CreatorChannelLiveTabResponse.swift in Sources */,
CC01032E2FF0100000A5BBA3 /* CreatorChannelLiveApi.swift in Sources */,
CC01032F2FF0100000A5BBA3 /* CreatorChannelLiveRepository.swift in Sources */,

View File

@@ -6,7 +6,7 @@ struct CreatorChannelAudioTabView: View {
let onTapContent: (Int) -> Void
@StateObject private var viewModel = CreatorChannelAudioViewModel()
@State private var isSortSheetPresented = false
@State private var isSortPopupPresented = false
var body: some View {
content
@@ -16,12 +16,6 @@ struct CreatorChannelAudioTabView: View {
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)
}
@@ -30,6 +24,8 @@ struct CreatorChannelAudioTabView: View {
if isEmptyState {
emptyStateView
} else {
ZStack(alignment: .topTrailing) {
VStack(spacing: 0) {
CapsuleTabBar(
items: viewModel.displayThemes,
selectedItem: Binding(
@@ -41,8 +37,9 @@ struct CreatorChannelAudioTabView: View {
CreatorChannelSortBar(
selectedSort: viewModel.selectedSort,
totalCount: viewModel.response?.audioContentCount,
onTapSort: {
isSortSheetPresented = true
isSortPopupPresented.toggle()
}
)
@@ -72,6 +69,35 @@ struct CreatorChannelAudioTabView: View {
}
.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
}
}
}
}

View File

@@ -2,26 +2,49 @@ import SwiftUI
struct CreatorChannelSortBar: View {
let selectedSort: ContentSort
let totalCount: Int?
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 {
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()
Button(action: onTapSort) {
HStack(spacing: SodaSpacing.s4) {
Text(selectedSort.title)
.appFont(size: 14, weight: .medium)
.foregroundColor(Color.white)
.appFont(size: 16, weight: .regular)
.foregroundColor(Color.gray400)
Image("ic_new_sort")
.font(.system(size: 12, weight: .semibold))
.frame(width: 18, height: 18)
}
.contentShape(Rectangle())
}
.buttonStyle(.plain)
}
.padding(.horizontal, SodaSpacing.s20)
.padding(.horizontal, SodaSpacing.s14)
.frame(height: 52)
.background(Color.black)
}
@@ -29,7 +52,7 @@ struct CreatorChannelSortBar: View {
struct CreatorChannelSortBar_Previews: PreviewProvider {
static var previews: some View {
CreatorChannelSortBar(selectedSort: .latest, onTapSort: {})
CreatorChannelSortBar(selectedSort: .latest, totalCount: 23, onTapSort: {})
.previewLayout(.sizeThatFits)
}
}

View File

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

View File

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

View File

@@ -7,7 +7,7 @@ struct CreatorChannelLiveTabView: View {
let onTapContent: (Int) -> Void
@StateObject private var viewModel = CreatorChannelLiveViewModel()
@State private var isSortSheetPresented = false
@State private var isSortPopupPresented = false
var body: some View {
content
@@ -17,12 +17,6 @@ struct CreatorChannelLiveTabView: View {
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)
}
@@ -31,10 +25,13 @@ struct CreatorChannelLiveTabView: View {
if isEmptyState {
emptyStateView
} else {
ZStack(alignment: .topTrailing) {
VStack(spacing: 0) {
CreatorChannelSortBar(
selectedSort: viewModel.selectedSort,
totalCount: viewModel.response?.liveReplayContentCount,
onTapSort: {
isSortSheetPresented = true
isSortPopupPresented.toggle()
}
)
@@ -62,6 +59,32 @@ struct CreatorChannelLiveTabView: View {
}
.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
}
}
}
}

View File

@@ -22,7 +22,7 @@
- 홈 탭의 `CreatorChannelCurrentLiveSection`과 기존 `showLiveDetail(_:)`, `showContentDetail(_:)`, `showCreateLive()` 흐름을 재사용한다.
- `CreatorChannelLiveResponse``CreatorChannelAudioContentResponse`는 기존 `SodaLive/Sources/V2/CreatorChannel/Home/Models/CreatorChannelHomeResponse.swift` 타입을 우선 재사용한다. 타입 이동은 이번 구현에 필수일 때만 수행한다.
- `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/**` 아래에 둔다.
- 최초 요청은 `page = 0`, `size = 20`, `sort = LATEST`로 고정한다.
- `hasNext == true`일 때만 `page + 1` 추가 요청을 수행하고, 추가 요청 중 중복 호출을 막는다.
@@ -34,7 +34,7 @@
### 생성
- `SodaLive/Sources/V2/CreatorChannel/Models/ContentSort.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/Repository/CreatorChannelLiveApi.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`
- 기대 결과: 첫 페이지 로딩, 정렬 변경, 다음 페이지 로딩 상태와 성공 시에만 목록 교체/append하는 동작이 확인된다.
### Phase 3: sort-bar와 정렬 BottomSheet
### Phase 3: sort-bar와 정렬 context popup
- [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`
- 기대 결과: 현재 정렬과 터치 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`로 둔다.
- 항목은 `LATEST`, `POPULAR`, `PRICE_HIGH`, `PRICE_LOW` 순서로 표시한다.
- Figma context menu의 `추천순`은 표시하지 않는다.
- 선택된 항목은 배경색 또는 체크 아이콘 없이도 충분히 구분되는 foreground/background 상태를 적용한다.
- 검증 기준:
- 실행 명령: `rg "struct CreatorChannelSortBottomSheet|selectedSort: ContentSort|ContentSort.allCases|onSelect|ForEach|sort.title" SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelSortBottomSheet.swift`
- 기대 결과: ContentSort 4개 항목 기반 BottomSheet가 확인된다.
- 실행 명령: `rg "추천순|recommend" 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개 항목 기반 context popup이 확인된다.
- 실행 명령: `rg "추천순|recommend" SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelSortContextPopup.swift`
- 기대 결과: 검색 결과가 없다.
### Phase 4: 라이브 탭 UI
@@ -223,13 +223,13 @@
- 입력값은 `creatorId: Int`, `isOwnCreatorChannel: Bool`, `onTapLive: (Int) -> Void`, `onTapContent: (Int) -> Void`로 둔다.
- 내부에서 `@StateObject private var viewModel = CreatorChannelLiveViewModel()`을 사용한다.
- `onAppear`에서 `hasLoaded == false`이면 `fetchFirstPage(creatorId:)`를 호출한다.
- sort-bar 터치 시 `.sheet` 또는 기존 앱 sheet 패턴으로 `CreatorChannelSortBottomSheet`를 표시한다.
- sort-bar 터치 시 `CreatorChannelSortContextPopup`를 표시한다.
- `currentLive != nil`이면 `CreatorChannelCurrentLiveSection(currentLive:onTapLive:)`를 표시한다.
- `liveReplayContents``LazyVStack`으로 표시하고 각 item의 `onAppear`에서 `fetchNextPageIfNeeded`를 호출한다.
- item tap은 `onTapContent(audioContent.audioContentId)`로 연결한다.
- 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이 확인된다.
- [x] **Task 4.4: 라이브 탭 Empty 상태 추가**
@@ -321,9 +321,9 @@
- 확인: `SodaLive/Sources/V2/CreatorChannel`
- 확인: `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 핵심 구현 지점이 모두 검색된다.
- [x] **Task 7.2: 빌드 검증**
@@ -342,7 +342,7 @@
- 작업 내용:
- 라이브 탭 진입 시 첫 요청이 `page=0`, `size=20`, `sort=LATEST`로 호출되는지 확인한다.
- sort-bar에 `최신순`이 표시되는지 확인한다.
- sort-bar 터치 시 BottomSheet가 표시되고 4개 정렬만 보이는지 확인한다.
- sort-bar 터치 시 context popup이 표시되고 4개 정렬만 보이는지 확인한다.
- 정렬 변경 시 첫 페이지부터 다시 조회되는지 확인한다.
- `currentLive != nil`인 경우 홈 탭 현재 라이브 UI와 동일한 카드가 표시되는지 확인한다.
- 마지막 item 노출 시 `hasNext == true`이면 `page + 1`이 호출되고 목록이 append되는지 확인한다.
@@ -355,6 +355,47 @@
- 기대 결과: PRD Success Criteria를 모두 만족한다.
- 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: 문서 작성 단계. 구현/빌드 검증은 아직 수행하지 않음.
@@ -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 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: 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.3 수동 동작 검증은 blocked. iOS Simulator는 사용 가능하나 실제 로그인 세션, 타인/본인 크리에이터 채널 테스트 데이터, 라이브/다시듣기 응답 데이터, 네트워크 요청 관찰 수단이 없어 CLI 환경에서 PRD Success Criteria 전체를 완료 검증하지 않음.
- 2026-07-04: 리뷰 반영. sort-bar 카운트 표시 요구사항이 제거되어 PRD/계획서를 현재 정렬 표시 기준으로 갱신함.
- 2026-07-04: 리뷰 반영. 본인 채널 라이브 시작하기 CTA를 `CreatorChannelLiveTabView` 내부 scroll content overlay에서 `CreatorChannelView` 최상위 overlay로 이동해 화면 하단 고정 구조로 변경함.
- 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 **`를 확인함.

View File

@@ -19,7 +19,7 @@ Figma 참조:
- 현재 크리에이터 채널 공통 shell은 홈 탭 중심으로 구현되어 있고, `라이브` 탭의 실제 목록 API, 정렬, 페이지네이션이 연결되어 있지 않다.
- 라이브 탭은 홈 탭의 현재 진행 중인 라이브 UI를 재사용해야 하지만, 라이브 다시듣기 목록은 가격, 포인트 사용 가능, 19금, 소장중, 대여중 상태를 함께 표시해야 하므로 별도 item 요구사항이 필요하다.
- sort-bar는 라이브 탭 전용이 아니라 다른 탭에서도 같은 형태로 재사용될 수 있어 채널 공통 컴포넌트 경계가 필요하다.
- 정렬 선택 UI는 Figma anchored context menu 형태가 있으나, 모바일 하단 접근성과 옵션 확장성을 고려해 앱 구현 방식 결정이 필요하다.
- 정렬 선택 UI는 기존 BottomSheet에서 Figma와 같은 anchored context popup 형태로 변경해야 한다.
- 본인 채널의 라이브 탭에서는 하단에 고정된 `라이브 시작하기` CTA가 필요하며, 일반 사용자 채널 진입과 표시 조건이 달라야 한다.
## 3. Goals
@@ -28,7 +28,8 @@ Figma 참조:
- 기본 정렬은 `LATEST`로 호출하고, 서버 응답의 `sort`를 현재 선택 정렬 상태로 반영한다.
- sort-bar는 `SodaLive/Sources/V2/CreatorChannel/Components/**` 아래에 생성해 라이브 탭 외 다른 탭에서도 재사용할 수 있게 한다.
- 정렬 문구는 `I18n`에 ko/en/ja 다국어로 추가한다.
- 정렬 선택 UI는 BottomSheet 방식을 채택한다.
- sort-bar는 전체 개수가 `0` 이상으로 내려오면 `전체 23` 형식으로 개수를 표시한다.
- 정렬 선택 UI는 Figma와 같은 context popup 방식을 채택한다.
- 현재 진행 중인 라이브는 크리에이터 채널 홈의 `CreatorChannelCurrentLiveSection` UI를 재사용한다.
- 라이브 다시듣기 item은 Figma 기준으로 썸네일, 제목, duration, 가격/포인트/무료/19금/소장중/대여중 상태를 표시한다.
- `hasNext == true`이면 스크롤 하단 진입 시 현재 응답의 `page + 1` 페이지를 추가 조회한다.
@@ -140,25 +141,26 @@ enum ContentSort: String, Decodable, CaseIterable {
### 7.3 Sort-bar
- 생성 위치: `SodaLive/Sources/V2/CreatorChannel/Components/**`
- sort-bar는 현재 정렬명과 아래 방향 아이콘을 표시한다.
- `totalCount``0` 이상이면 sort-bar 좌측에 `전체 {totalCount}`를 표시한다.
- `totalCount``nil`이거나 음수이면 전체 개수 영역은 숨긴다.
- Figma 기준 높이는 `52pt`, 좌우 horizontal padding은 기존 `SodaSpacing` 토큰으로 맞춘다.
- 정렬명은 `I18n`에 추가한다.
- sort-bar는 다른 탭에서도 재사용 가능하도록 `selectedSort`, `availableSorts`, `onTapSort` 같은 일반화된 입력을 받는다.
- 이번 범위에서는 Figma의 `리스트형` view mode 옵션은 사용하지 않는다.
### 7.4 정렬 선택 UI
- 정렬 선택 UI는 BottomSheet 방식으로 구현한다.
- 선택 사유:
- 모바일 한 손 조작에서 화면 하단 sheet가 우측 상단 anchored menu보다 터치 접근성이 좋다.
- 정렬 옵션이 4개이고 이후 탭별 옵션 추가 가능성이 있어 sheet가 선택 영역과 dismiss 동작을 안정적으로 제공한다.
- iOS/Material 계열 최신 모바일 앱에서 필터/정렬 같은 목록 제어는 sheet 패턴이 일반적이며, context menu는 짧은 보조 action에 더 적합하다.
- 정렬 sheet 항목:
- 정렬 선택 UI는 Figma `290:9041`처럼 sort-bar 우측 정렬 버튼 아래에 붙는 context popup으로 구현한다.
- popup은 `Color.gray700` 1pt border, `14pt` corner radius, `Color.gray900` 기본 배경을 사용한다.
- 선택된 항목은 `Color.gray800` 배경으로 표시한다.
- popup 밖을 터치하거나 항목을 선택하면 닫힌다.
- 정렬 popup 항목:
- `LATEST`: 최신순
- `POPULAR`: 인기순
- `PRICE_HIGH`: 가격 높은 순
- `PRICE_LOW`: 가격 낮은 순
- Figma `290:9041`에는 `추천순`이 있으나 API `ContentSort`에 없으므로 구현하지 않는다.
- 현재 선택된 sort는 시각적으로 선택 상태를 표시한다.
- sort item 터치 시 sheet를 닫고 첫 페이지를 재조회한다.
- sort item 터치 시 popup을 닫고 첫 페이지를 재조회한다.
### 7.5 현재 진행 중인 라이브
- `currentLive != nil`이면 sort-bar 아래 또는 Figma 배치 기준 위치에 현재 진행 중인 라이브 UI를 표시한다.
@@ -212,7 +214,7 @@ enum ContentSort: String, Decodable, CaseIterable {
- sort-bar는 tab-bar 아래에 붙어 보이고, 목록 콘텐츠와 겹치지 않아야 한다.
- Empty 상태에서는 sort-bar와 list가 보이지 않아야 한다.
- 다시듣기 item의 썸네일, 제목, 가격/상태 영역은 긴 제목과 다국어 문구에서도 겹치지 않아야 한다.
- BottomSheet는 dim 또는 drag/dismiss 동작이 기존 앱 sheet 패턴과 일관되어야 한다.
- context popup은 sort-bar 우측 버튼 아래에 겹치지 않게 표시되고, 목록 콘텐츠 위에 떠야 한다.
- 로딩 중에는 기존 목록을 과도하게 흔들지 않고, 첫 페이지 로딩과 추가 페이지 로딩 상태를 구분한다. 목록 교체와 append는 조회 성공 시에만 수행한다.
## 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}`가 호출된다.
- 기본 sort는 `LATEST`이며 sort-bar에는 `최신순`이 표시된다.
- 최초 요청은 `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`이고 `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`가 성공한다.
## 11. Decisions
- 정렬 메뉴 표시는 BottomSheet 방식을 채택한다.
- 정렬 메뉴 표시는 sort-bar 우측 버튼 아래 anchored context popup 방식을 채택한다.
- 첫 페이지의 `page` 시작값은 `0`으로 한다.
- 요청 `size` 기본값은 `20`으로 한다.
- Figma 정렬 메뉴 예시에 있는 `추천순`은 API enum에 없으므로 제외한다.

View File

@@ -16,7 +16,7 @@
- 기존 `CreatorChannelView`의 공통 shell, header, sticky tab-bar, title bar 동작은 유지한다.
- `CreatorChannelTab.audio` 콘텐츠만 신규 `CreatorChannelAudioTabView`로 교체한다.
- `ContentSort`, `CreatorChannelSortBar`, `CreatorChannelSortBottomSheet`, `CapsuleTabBar`, `CreatorChannelAudioContentListItem`을 재사용한다.
- `ContentSort`, `CreatorChannelSortBar`, 정렬 context popup, `CapsuleTabBar`, `CreatorChannelAudioContentListItem`을 재사용한다.
- 전체 테마는 API 모델에 섞지 않고 오디오 탭 표시 모델에서만 합성한다.
- `themeId == nil`이면 query parameter를 보내지 않는다.
- `duration == nil` 또는 빈 문자열인 콘텐츠는 ViewModel에서 목록에 포함하지 않는다.
@@ -134,6 +134,21 @@
- 실행 명령: `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`을 확인했다.
@@ -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: `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: 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 **`를 확인함.

View File

@@ -21,7 +21,8 @@ Figma 참조:
- `page`, `size`, `sort`, 선택된 `themeId`를 query parameter로 전달한다.
- 기본값은 `page=0`, `size=20`, `sort=LATEST`, `themeId=nil`이다.
- theme list는 `CapsuleTabBar`를 사용하고, 맨 앞에 전체 항목을 합성한다.
- 정렬 선택 방식은 라이브 탭과 동일한 `CreatorChannelSortBar` + `CreatorChannelSortBottomSheet` 재사용한다.
- 정렬 선택 방식은 라이브 탭과 동일한 `CreatorChannelSortBar` + context popup을 재사용한다.
- sort-bar는 `audioContentCount``0` 이상이면 `전체 {audioContentCount}`를 표시한다.
- 콘텐츠 item은 `CreatorChannelAudioContentListItem`을 재사용한다.
- 전체 테마이고 본인 채널이 아닐 때만 소장률 UI를 표시한다.
- 본인 채널의 오디오 탭에는 `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`가 전송되지 않는다.
- `themes` 앞에 전체 탭이 표시되고, 응답 `themeId == nil`이면 전체가 선택된다.
- 테마 선택 시 선택한 `themeId`로 첫 페이지를 다시 조회한다.
- 정렬 선택 UI 동작은 라이브 탭과 동일하다.
- 정렬 선택 UI는 BottomSheet가 아니라 sort-bar 우측 버튼 아래 anchored context popup으로 표시되고, 동작은 라이브 탭과 동일하다.
- 본인 채널이 아니고 전체 테마일 때만 소장률 UI가 표시된다.
- 본인 채널이거나 테마 필터 상태이면 소장률 UI가 표시되지 않는다.
- `duration`이 없는 콘텐츠는 목록에 표시되지 않는다.
@@ -113,9 +114,10 @@ data class CreatorChannelAudioThemeResponse(
## 7. Technical Constraints
- 기능 변경은 `SodaLive/Sources/V2/CreatorChannel/**` 하위에서 해결한다.
- 오디오 탭 전용 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를 추가한다.
## 8. Verification Notes
- 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: Figma `290:9034`, `290:9031`을 추가 확인해 sort-bar 전체 개수 표시와 정렬 context popup 전환 요구사항을 반영했다.