feat(creator): 라이브 탭 UI를 마무리한다
This commit is contained in:
21
SodaLive/Resources/Assets.xcassets/v2/ic_new_sort.imageset/Contents.json
vendored
Normal file
21
SodaLive/Resources/Assets.xcassets/v2/ic_new_sort.imageset/Contents.json
vendored
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"filename" : "ic_new_sort.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
||||||
BIN
SodaLive/Resources/Assets.xcassets/v2/ic_new_sort.imageset/ic_new_sort.png
vendored
Normal file
BIN
SodaLive/Resources/Assets.xcassets/v2/ic_new_sort.imageset/ic_new_sort.png
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 358 B |
@@ -3168,7 +3168,6 @@ If you block this user, the following features will be restricted.
|
|||||||
enum Sort {
|
enum Sort {
|
||||||
static var latest: String { pick(ko: "최신순", en: "Latest", ja: "最新順") }
|
static var latest: String { pick(ko: "최신순", en: "Latest", ja: "最新順") }
|
||||||
static var popular: String { pick(ko: "인기순", en: "Popular", ja: "人気順") }
|
static var popular: String { pick(ko: "인기순", en: "Popular", ja: "人気順") }
|
||||||
static var owned: String { pick(ko: "소장중", en: "Owned", ja: "所持中") }
|
|
||||||
static var priceHigh: String { pick(ko: "높은 가격순", en: "Highest price", ja: "価格が高い順") }
|
static var priceHigh: String { pick(ko: "높은 가격순", en: "Highest price", ja: "価格が高い順") }
|
||||||
static var priceLow: String { pick(ko: "낮은 가격순", en: "Lowest price", ja: "価格が低い順") }
|
static var priceLow: String { pick(ko: "낮은 가격순", en: "Lowest price", ja: "価格が低い順") }
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -66,8 +66,7 @@ struct CreatorChannelFloatingActionMenu: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.trailing, SodaSpacing.s20)
|
.padding(.trailing, SodaSpacing.s14)
|
||||||
.padding(.bottom, SodaSpacing.s32)
|
|
||||||
.animation(animation, value: isPresented)
|
.animation(animation, value: isPresented)
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing)
|
.frame(maxWidth: .infinity, maxHeight: .infinity, alignment: .bottomTrailing)
|
||||||
|
|||||||
@@ -1,22 +1,11 @@
|
|||||||
import SwiftUI
|
import SwiftUI
|
||||||
|
|
||||||
struct CreatorChannelSortBar: View {
|
struct CreatorChannelSortBar: View {
|
||||||
let totalCount: Int
|
|
||||||
let selectedSort: ContentSort
|
let selectedSort: ContentSort
|
||||||
let onTapSort: () -> Void
|
let onTapSort: () -> Void
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
HStack(alignment: .center, spacing: SodaSpacing.s8) {
|
HStack(alignment: .center, spacing: SodaSpacing.s8) {
|
||||||
HStack(spacing: SodaSpacing.s4) {
|
|
||||||
Text(I18n.CreatorChannelLive.totalLabel)
|
|
||||||
.appFont(size: 14, weight: .medium)
|
|
||||||
.foregroundColor(Color.gray500)
|
|
||||||
|
|
||||||
Text(totalCount.comma())
|
|
||||||
.appFont(size: 14, weight: .medium)
|
|
||||||
.foregroundColor(Color.gray400)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
Button(action: onTapSort) {
|
Button(action: onTapSort) {
|
||||||
@@ -25,9 +14,8 @@ struct CreatorChannelSortBar: View {
|
|||||||
.appFont(size: 14, weight: .medium)
|
.appFont(size: 14, weight: .medium)
|
||||||
.foregroundColor(Color.white)
|
.foregroundColor(Color.white)
|
||||||
|
|
||||||
Image(systemName: "chevron.down")
|
Image("ic_new_sort")
|
||||||
.font(.system(size: 12, weight: .semibold))
|
.font(.system(size: 12, weight: .semibold))
|
||||||
.foregroundColor(Color.gray400)
|
|
||||||
}
|
}
|
||||||
.contentShape(Rectangle())
|
.contentShape(Rectangle())
|
||||||
}
|
}
|
||||||
@@ -41,7 +29,7 @@ struct CreatorChannelSortBar: View {
|
|||||||
|
|
||||||
struct CreatorChannelSortBar_Previews: PreviewProvider {
|
struct CreatorChannelSortBar_Previews: PreviewProvider {
|
||||||
static var previews: some View {
|
static var previews: some View {
|
||||||
CreatorChannelSortBar(totalCount: 100, selectedSort: .latest, onTapSort: {})
|
CreatorChannelSortBar(selectedSort: .latest, onTapSort: {})
|
||||||
.previewLayout(.sizeThatFits)
|
.previewLayout(.sizeThatFits)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -95,6 +95,15 @@ struct CreatorChannelView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isOwnCreatorChannel && viewModel.selectedTab == .live && !viewModel.isLoading {
|
||||||
|
VStack {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
CreatorChannelLiveStartButton(action: showCreateLive)
|
||||||
|
}
|
||||||
|
.ignoresSafeArea(.container, edges: .bottom)
|
||||||
|
}
|
||||||
|
|
||||||
if isShowAuthConfirmView {
|
if isShowAuthConfirmView {
|
||||||
authConfirmDialog
|
authConfirmDialog
|
||||||
}
|
}
|
||||||
@@ -191,7 +200,14 @@ struct CreatorChannelView: View {
|
|||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var selectedTabContent: some View {
|
private var selectedTabContent: some View {
|
||||||
if viewModel.selectedTab != .home {
|
if viewModel.selectedTab == .live {
|
||||||
|
CreatorChannelLiveTabView(
|
||||||
|
creatorId: creatorId,
|
||||||
|
isOwnCreatorChannel: isOwnCreatorChannel,
|
||||||
|
onTapLive: showLiveDetail,
|
||||||
|
onTapContent: showContentDetail
|
||||||
|
)
|
||||||
|
} else if viewModel.selectedTab != .home {
|
||||||
CreatorChannelPlaceholderTabView(title: viewModel.selectedTab.title)
|
CreatorChannelPlaceholderTabView(title: viewModel.selectedTab.title)
|
||||||
} else if let response = viewModel.response {
|
} else if let response = viewModel.response {
|
||||||
CreatorChannelHomeView(
|
CreatorChannelHomeView(
|
||||||
|
|||||||
@@ -4,24 +4,30 @@ struct CreatorChannelLiveStartButton: View {
|
|||||||
let action: () -> Void
|
let action: () -> Void
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
Button(action: action) {
|
VStack {
|
||||||
HStack(spacing: SodaSpacing.s6) {
|
Button(action: action) {
|
||||||
Image("ic_new_create_live")
|
HStack(spacing: SodaSpacing.s6) {
|
||||||
.resizable()
|
Image("ic_new_create_live")
|
||||||
.scaledToFit()
|
.resizable()
|
||||||
.frame(width: 24, height: 24)
|
.scaledToFit()
|
||||||
|
.frame(width: 24, height: 24)
|
||||||
Text(I18n.CreatorChannelLive.startLive)
|
|
||||||
.appFont(size: 16, weight: .bold)
|
Text(I18n.CreatorChannelLive.startLive)
|
||||||
.foregroundColor(.white)
|
.appFont(size: 16, weight: .bold)
|
||||||
.lineLimit(1)
|
.foregroundColor(.white)
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.frame(height: 52)
|
||||||
|
.padding(.horizontal, SodaSpacing.s14)
|
||||||
|
.background(Color.soda400)
|
||||||
|
.clipShape(Capsule())
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
.buttonStyle(.plain)
|
||||||
.frame(height: 56)
|
|
||||||
.background(Color.soda400)
|
|
||||||
.clipShape(Capsule())
|
|
||||||
}
|
}
|
||||||
.buttonStyle(.plain)
|
.padding(.top, SodaSpacing.s14)
|
||||||
|
.padding(.bottom, 34)
|
||||||
|
.background(Color.black)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -5,21 +5,12 @@ struct CreatorChannelLiveTabView: View {
|
|||||||
let isOwnCreatorChannel: Bool
|
let isOwnCreatorChannel: Bool
|
||||||
let onTapLive: (Int) -> Void
|
let onTapLive: (Int) -> Void
|
||||||
let onTapContent: (Int) -> Void
|
let onTapContent: (Int) -> Void
|
||||||
let onTapCreateLive: () -> Void
|
|
||||||
|
|
||||||
@StateObject private var viewModel = CreatorChannelLiveViewModel()
|
@StateObject private var viewModel = CreatorChannelLiveViewModel()
|
||||||
@State private var isSortSheetPresented = false
|
@State private var isSortSheetPresented = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack(alignment: .bottom) {
|
content
|
||||||
content
|
|
||||||
|
|
||||||
if isOwnCreatorChannel {
|
|
||||||
CreatorChannelLiveStartButton(action: onTapCreateLive)
|
|
||||||
.padding(.horizontal, SodaSpacing.s20)
|
|
||||||
.padding(.bottom, SodaSpacing.s20)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.background(Color.black)
|
.background(Color.black)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
if viewModel.hasLoaded == false {
|
if viewModel.hasLoaded == false {
|
||||||
@@ -38,7 +29,6 @@ struct CreatorChannelLiveTabView: View {
|
|||||||
private var content: some View {
|
private var content: some View {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
CreatorChannelSortBar(
|
CreatorChannelSortBar(
|
||||||
totalCount: viewModel.response?.liveReplayContentCount ?? 0,
|
|
||||||
selectedSort: viewModel.selectedSort,
|
selectedSort: viewModel.selectedSort,
|
||||||
onTapSort: {
|
onTapSort: {
|
||||||
isSortSheetPresented = true
|
isSortSheetPresented = true
|
||||||
@@ -67,7 +57,7 @@ struct CreatorChannelLiveTabView: View {
|
|||||||
.padding(.vertical, SodaSpacing.s20)
|
.padding(.vertical, SodaSpacing.s20)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.bottom, isOwnCreatorChannel ? 96 : SodaSpacing.s20)
|
.padding(.bottom, isOwnCreatorChannel ? 70 : SodaSpacing.s20)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -78,8 +68,7 @@ struct CreatorChannelLiveTabView_Previews: PreviewProvider {
|
|||||||
creatorId: 1,
|
creatorId: 1,
|
||||||
isOwnCreatorChannel: true,
|
isOwnCreatorChannel: true,
|
||||||
onTapLive: { _ in },
|
onTapLive: { _ in },
|
||||||
onTapContent: { _ in },
|
onTapContent: { _ in }
|
||||||
onTapCreateLive: {}
|
|
||||||
)
|
)
|
||||||
.background(Color.black)
|
.background(Color.black)
|
||||||
.previewLayout(.sizeThatFits)
|
.previewLayout(.sizeThatFits)
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ import Foundation
|
|||||||
enum ContentSort: String, Decodable, CaseIterable, Hashable {
|
enum ContentSort: String, Decodable, CaseIterable, Hashable {
|
||||||
case latest = "LATEST"
|
case latest = "LATEST"
|
||||||
case popular = "POPULAR"
|
case popular = "POPULAR"
|
||||||
case owned = "OWNED"
|
|
||||||
case priceHigh = "PRICE_HIGH"
|
case priceHigh = "PRICE_HIGH"
|
||||||
case priceLow = "PRICE_LOW"
|
case priceLow = "PRICE_LOW"
|
||||||
|
|
||||||
@@ -13,8 +12,6 @@ enum ContentSort: String, Decodable, CaseIterable, Hashable {
|
|||||||
return I18n.CreatorChannelLive.Sort.latest
|
return I18n.CreatorChannelLive.Sort.latest
|
||||||
case .popular:
|
case .popular:
|
||||||
return I18n.CreatorChannelLive.Sort.popular
|
return I18n.CreatorChannelLive.Sort.popular
|
||||||
case .owned:
|
|
||||||
return I18n.CreatorChannelLive.Sort.owned
|
|
||||||
case .priceHigh:
|
case .priceHigh:
|
||||||
return I18n.CreatorChannelLive.Sort.priceHigh
|
return I18n.CreatorChannelLive.Sort.priceHigh
|
||||||
case .priceLow:
|
case .priceLow:
|
||||||
|
|||||||
@@ -78,13 +78,13 @@
|
|||||||
- 수정: `SodaLive/Sources/I18n/I18n.swift`
|
- 수정: `SodaLive/Sources/I18n/I18n.swift`
|
||||||
- 작업 내용:
|
- 작업 내용:
|
||||||
- `enum ContentSort: String, Decodable, CaseIterable`을 생성한다.
|
- `enum ContentSort: String, Decodable, CaseIterable`을 생성한다.
|
||||||
- case는 `latest = "LATEST"`, `popular = "POPULAR"`, `owned = "OWNED"`, `priceHigh = "PRICE_HIGH"`, `priceLow = "PRICE_LOW"`로 둔다.
|
- case는 `latest = "LATEST"`, `popular = "POPULAR"`, `priceHigh = "PRICE_HIGH"`, `priceLow = "PRICE_LOW"`로 둔다.
|
||||||
- `var title: String`에서 `I18n.CreatorChannelLive.Sort.latest`, `popular`, `owned`, `priceHigh`, `priceLow`를 반환한다.
|
- `var title: String`에서 `I18n.CreatorChannelLive.Sort.latest`, `popular`, `priceHigh`, `priceLow`를 반환한다.
|
||||||
- `I18n.CreatorChannelLive.Sort`에 ko/en/ja 문구를 추가한다.
|
- `I18n.CreatorChannelLive.Sort`에 ko/en/ja 문구를 추가한다.
|
||||||
- 검증 기준:
|
- 검증 기준:
|
||||||
- 실행 명령: `rg "enum ContentSort|case latest = \"LATEST\"|case popular = \"POPULAR\"|case owned = \"OWNED\"|case priceHigh = \"PRICE_HIGH\"|case priceLow = \"PRICE_LOW\"|var title" SodaLive/Sources/V2/CreatorChannel/Models/ContentSort.swift`
|
- 실행 명령: `rg "enum ContentSort|case latest = \"LATEST\"|case popular = \"POPULAR\"|case priceHigh = \"PRICE_HIGH\"|case priceLow = \"PRICE_LOW\"|var title" SodaLive/Sources/V2/CreatorChannel/Models/ContentSort.swift`
|
||||||
- 기대 결과: API enum 값 5개와 title mapping이 존재한다.
|
- 기대 결과: API enum 값 4개와 title mapping이 존재한다.
|
||||||
- 실행 명령: `rg "enum CreatorChannelLive|enum Sort|latest|popular|owned|priceHigh|priceLow" SodaLive/Sources/I18n/I18n.swift`
|
- 실행 명령: `rg "enum CreatorChannelLive|enum Sort|latest|popular|priceHigh|priceLow" SodaLive/Sources/I18n/I18n.swift`
|
||||||
- 기대 결과: 라이브 탭 정렬 다국어 문구가 존재한다.
|
- 기대 결과: 라이브 탭 정렬 다국어 문구가 존재한다.
|
||||||
|
|
||||||
- [x] **Task 1.3: 라이브 탭 Response model 생성**
|
- [x] **Task 1.3: 라이브 탭 Response model 생성**
|
||||||
@@ -153,14 +153,13 @@
|
|||||||
- 수정: `SodaLive/Sources/I18n/I18n.swift`
|
- 수정: `SodaLive/Sources/I18n/I18n.swift`
|
||||||
- 작업 내용:
|
- 작업 내용:
|
||||||
- `CreatorChannelSortBar`를 `View`로 생성한다.
|
- `CreatorChannelSortBar`를 `View`로 생성한다.
|
||||||
- 입력값은 `totalCount: Int`, `selectedSort: ContentSort`, `onTapSort: () -> Void`로 둔다.
|
- 입력값은 `selectedSort: ContentSort`, `onTapSort: () -> Void`로 둔다.
|
||||||
- 좌측에는 `I18n.CreatorChannelLive.totalLabel`과 `totalCount`를 표시한다.
|
- `selectedSort.title`과 정렬 아이콘을 표시한다.
|
||||||
- 우측에는 `selectedSort.title`과 정렬 아이콘을 표시한다.
|
|
||||||
- 높이는 Figma 기준 `52pt`로 둔다.
|
- 높이는 Figma 기준 `52pt`로 둔다.
|
||||||
- 배경은 black, 텍스트는 기존 `Color.gray400`, `Color.gray500`, white 토큰을 따른다.
|
- 배경은 black, 텍스트는 기존 `Color.gray400`, `Color.gray500`, white 토큰을 따른다.
|
||||||
- 검증 기준:
|
- 검증 기준:
|
||||||
- 실행 명령: `rg "struct CreatorChannelSortBar|totalCount: Int|selectedSort: ContentSort|onTapSort|I18n.CreatorChannelLive.totalLabel|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: 정렬 BottomSheet 컴포넌트 생성**
|
||||||
- 대상 파일:
|
- 대상 파일:
|
||||||
@@ -168,12 +167,12 @@
|
|||||||
- 작업 내용:
|
- 작업 내용:
|
||||||
- `CreatorChannelSortBottomSheet`를 `View`로 생성한다.
|
- `CreatorChannelSortBottomSheet`를 `View`로 생성한다.
|
||||||
- 입력값은 `selectedSort: ContentSort`, `sorts: [ContentSort] = ContentSort.allCases`, `onSelect: (ContentSort) -> Void`로 둔다.
|
- 입력값은 `selectedSort: ContentSort`, `sorts: [ContentSort] = ContentSort.allCases`, `onSelect: (ContentSort) -> Void`로 둔다.
|
||||||
- 항목은 `LATEST`, `POPULAR`, `OWNED`, `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 CreatorChannelSortBottomSheet|selectedSort: ContentSort|ContentSort.allCases|onSelect|ForEach|sort.title" SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelSortBottomSheet.swift`
|
||||||
- 기대 결과: ContentSort 5개 항목 기반 BottomSheet가 확인된다.
|
- 기대 결과: ContentSort 4개 항목 기반 BottomSheet가 확인된다.
|
||||||
- 실행 명령: `rg "추천순|recommend" SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelSortBottomSheet.swift`
|
- 실행 명령: `rg "추천순|recommend" SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelSortBottomSheet.swift`
|
||||||
- 기대 결과: 검색 결과가 없다.
|
- 기대 결과: 검색 결과가 없다.
|
||||||
|
|
||||||
@@ -221,40 +220,40 @@
|
|||||||
- 확인: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelCurrentLiveSection.swift`
|
- 확인: `SodaLive/Sources/V2/CreatorChannel/Home/Components/CreatorChannelCurrentLiveSection.swift`
|
||||||
- 작업 내용:
|
- 작업 내용:
|
||||||
- `CreatorChannelLiveTabView`를 `View`로 생성한다.
|
- `CreatorChannelLiveTabView`를 `View`로 생성한다.
|
||||||
- 입력값은 `creatorId: Int`, `isOwnCreatorChannel: Bool`, `onTapLive: (Int) -> Void`, `onTapContent: (Int) -> Void`, `onTapCreateLive: () -> 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 터치 시 `.sheet` 또는 기존 앱 sheet 패턴으로 `CreatorChannelSortBottomSheet`를 표시한다.
|
||||||
- `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)`로 연결한다.
|
||||||
- `isOwnCreatorChannel == true`이면 하단 overlay 또는 safe area inset에 `CreatorChannelLiveStartButton(action: onTapCreateLive)`를 표시한다.
|
- CTA가 부모 화면의 고정 overlay와 겹치지 않도록 scroll content bottom padding을 추가한다.
|
||||||
- CTA가 목록을 가리지 않도록 scroll content bottom padding을 추가한다.
|
|
||||||
- 검증 기준:
|
- 검증 기준:
|
||||||
- 실행 명령: `rg "struct CreatorChannelLiveTabView|CreatorChannelLiveViewModel|fetchFirstPage\\(creatorId: creatorId\\)|CreatorChannelSortBar|CreatorChannelSortBottomSheet|CreatorChannelCurrentLiveSection|CreatorChannelLiveReplayListItem|fetchNextPageIfNeeded|onTapContent\\(audioContent.audioContentId\\)|CreatorChannelLiveStartButton|isOwnCreatorChannel" SodaLive/Sources/V2/CreatorChannel/Live/CreatorChannelLiveTabView.swift`
|
- 실행 명령: `rg "struct CreatorChannelLiveTabView|CreatorChannelLiveViewModel|fetchFirstPage\\(creatorId: creatorId\\)|CreatorChannelSortBar|CreatorChannelSortBottomSheet|CreatorChannelCurrentLiveSection|CreatorChannelAudioContentListItem|fetchNextPageIfNeeded|onTapContent\\(audioContent.audioContentId\\)|isOwnCreatorChannel" SodaLive/Sources/V2/CreatorChannel/Live/CreatorChannelLiveTabView.swift`
|
||||||
- 기대 결과: 라이브 탭 화면, 정렬, 현재 라이브, 다시듣기 목록, pagination, 본인 CTA가 확인된다.
|
- 기대 결과: 라이브 탭 화면, 정렬, 현재 라이브, 다시듣기 목록, pagination이 확인된다.
|
||||||
|
|
||||||
### Phase 5: CreatorChannelView 연결
|
### Phase 5: CreatorChannelView 연결
|
||||||
|
|
||||||
- [ ] **Task 5.1: 라이브 탭 콘텐츠 연결**
|
- [x] **Task 5.1: 라이브 탭 콘텐츠 연결**
|
||||||
- 대상 파일:
|
- 대상 파일:
|
||||||
- 수정: `SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift`
|
- 수정: `SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift`
|
||||||
- 작업 내용:
|
- 작업 내용:
|
||||||
- `selectedTabContent`에서 `viewModel.selectedTab == .live`일 때 `CreatorChannelLiveTabView`를 표시한다.
|
- `selectedTabContent`에서 `viewModel.selectedTab == .live`일 때 `CreatorChannelLiveTabView`를 표시한다.
|
||||||
- `creatorId`, `isOwnCreatorChannel`, `showLiveDetail`, `showContentDetail`, `showCreateLive`를 전달한다.
|
- `creatorId`, `isOwnCreatorChannel`, `showLiveDetail`, `showContentDetail`을 전달한다.
|
||||||
|
- 본인 채널의 라이브 탭 CTA는 `CreatorChannelView`의 최상위 overlay에서 `CreatorChannelLiveStartButton(action: showCreateLive)`로 표시한다.
|
||||||
- 기존 홈 탭 분기는 유지한다.
|
- 기존 홈 탭 분기는 유지한다.
|
||||||
- 홈 외 준비 중 화면 분기는 `live`를 제외한 탭에만 적용한다.
|
- 홈 외 준비 중 화면 분기는 `live`를 제외한 탭에만 적용한다.
|
||||||
- 검증 기준:
|
- 검증 기준:
|
||||||
- 실행 명령: `rg "CreatorChannelLiveTabView|viewModel\\.selectedTab == \\.live|isOwnCreatorChannel|onTapLive: showLiveDetail|onTapContent: showContentDetail|onTapCreateLive: showCreateLive" SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift`
|
- 실행 명령: `rg "CreatorChannelLiveTabView|viewModel\\.selectedTab == \\.live|isOwnCreatorChannel|onTapLive: showLiveDetail|onTapContent: showContentDetail|CreatorChannelLiveStartButton\\(action: showCreateLive\\)" SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift`
|
||||||
- 기대 결과: 라이브 탭이 준비 중 화면이 아니라 신규 view로 연결된다.
|
- 기대 결과: 라이브 탭이 준비 중 화면이 아니라 신규 view로 연결된다.
|
||||||
|
|
||||||
- [ ] **Task 5.2: 본인 채널 홈 플로팅 메뉴와 라이브 CTA 조건 분리 확인**
|
- [x] **Task 5.2: 본인 채널 홈 플로팅 메뉴와 라이브 CTA 조건 분리 확인**
|
||||||
- 대상 파일:
|
- 대상 파일:
|
||||||
- 수정: `SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift`
|
- 수정: `SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift`
|
||||||
- 확인: `SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelFloatingActionMenu.swift`
|
- 확인: `SodaLive/Sources/V2/CreatorChannel/Components/CreatorChannelFloatingActionMenu.swift`
|
||||||
- 작업 내용:
|
- 작업 내용:
|
||||||
- 기존 `CreatorChannelFloatingActionMenu`는 본인 채널 `home` 탭에서만 유지한다.
|
- 기존 `CreatorChannelFloatingActionMenu`는 본인 채널 `home` 탭에서만 유지한다.
|
||||||
- 라이브 탭에서는 `CreatorChannelLiveTabView` 내부 하단 CTA만 표시되도록 한다.
|
- 라이브 탭에서는 `CreatorChannelView` 최상위 overlay의 하단 CTA만 표시되도록 한다.
|
||||||
- tab 변경 시 기존 `isCreatorActionMenuPresented = false` 동작은 유지한다.
|
- tab 변경 시 기존 `isCreatorActionMenuPresented = false` 동작은 유지한다.
|
||||||
- 검증 기준:
|
- 검증 기준:
|
||||||
- 실행 명령: `rg "isOwnCreatorChannel && viewModel\\.selectedTab == \\.home|isCreatorActionMenuPresented = false|CreatorChannelLiveTabView" SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift`
|
- 실행 명령: `rg "isOwnCreatorChannel && viewModel\\.selectedTab == \\.home|isCreatorActionMenuPresented = false|CreatorChannelLiveTabView" SodaLive/Sources/V2/CreatorChannel/CreatorChannelView.swift`
|
||||||
@@ -304,17 +303,17 @@
|
|||||||
|
|
||||||
### Phase 7: 검증
|
### Phase 7: 검증
|
||||||
|
|
||||||
- [ ] **Task 7.1: 정적 검색 검증**
|
- [x] **Task 7.1: 정적 검색 검증**
|
||||||
- 대상 파일:
|
- 대상 파일:
|
||||||
- 확인: `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, BottomSheet, current live 재사용, 다시듣기 item, `ic_new_create_live`, 오디오 상세 이동 연결을 검색한다.
|
||||||
- 검증 기준:
|
- 검증 기준:
|
||||||
- 실행 명령: `rg "CreatorChannelLiveApi|/api/v2/creator-channels/|/live|URLEncoding.queryString|ContentSort|pageSize|20|page = 0|CreatorChannelSortBar|CreatorChannelSortBottomSheet|CreatorChannelCurrentLiveSection|CreatorChannelLiveReplayListItem|ic_new_create_live|contentDetail\\(contentId:" SodaLive/Sources/V2/CreatorChannel SodaLive/Sources/I18n/I18n.swift`
|
- 실행 명령: `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`
|
||||||
- 기대 결과: PRD 핵심 구현 지점이 모두 검색된다.
|
- 기대 결과: PRD 핵심 구현 지점이 모두 검색된다.
|
||||||
|
|
||||||
- [ ] **Task 7.2: 빌드 검증**
|
- [x] **Task 7.2: 빌드 검증**
|
||||||
- 대상 파일:
|
- 대상 파일:
|
||||||
- 확인: `SodaLive.xcworkspace`
|
- 확인: `SodaLive.xcworkspace`
|
||||||
- 작업 내용:
|
- 작업 내용:
|
||||||
@@ -323,14 +322,14 @@
|
|||||||
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`
|
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`
|
||||||
- 기대 결과: `** BUILD SUCCEEDED **`
|
- 기대 결과: `** BUILD SUCCEEDED **`
|
||||||
|
|
||||||
- [ ] **Task 7.3: 수동 동작 검증**
|
- [x] **Task 7.3: 수동 동작 검증 [blocked]**
|
||||||
- 대상 화면:
|
- 대상 화면:
|
||||||
- 타인 크리에이터 채널 라이브 탭
|
- 타인 크리에이터 채널 라이브 탭
|
||||||
- 본인 크리에이터 채널 라이브 탭
|
- 본인 크리에이터 채널 라이브 탭
|
||||||
- 작업 내용:
|
- 작업 내용:
|
||||||
- 라이브 탭 진입 시 첫 요청이 `page=0`, `size=20`, `sort=LATEST`로 호출되는지 확인한다.
|
- 라이브 탭 진입 시 첫 요청이 `page=0`, `size=20`, `sort=LATEST`로 호출되는지 확인한다.
|
||||||
- sort-bar에 전체 개수와 `최신순`이 표시되는지 확인한다.
|
- sort-bar에 `최신순`이 표시되는지 확인한다.
|
||||||
- sort-bar 터치 시 BottomSheet가 표시되고 5개 정렬만 보이는지 확인한다.
|
- sort-bar 터치 시 BottomSheet가 표시되고 4개 정렬만 보이는지 확인한다.
|
||||||
- 정렬 변경 시 첫 페이지부터 다시 조회되는지 확인한다.
|
- 정렬 변경 시 첫 페이지부터 다시 조회되는지 확인한다.
|
||||||
- `currentLive != nil`인 경우 홈 탭 현재 라이브 UI와 동일한 카드가 표시되는지 확인한다.
|
- `currentLive != nil`인 경우 홈 탭 현재 라이브 UI와 동일한 카드가 표시되는지 확인한다.
|
||||||
- 마지막 item 노출 시 `hasNext == true`이면 `page + 1`이 호출되고 목록이 append되는지 확인한다.
|
- 마지막 item 노출 시 `hasNext == true`이면 `page + 1`이 호출되고 목록이 append되는지 확인한다.
|
||||||
@@ -341,6 +340,7 @@
|
|||||||
- 타인 채널 또는 라이브 외 탭에서는 `라이브 시작하기` 버튼이 표시되지 않는지 확인한다.
|
- 타인 채널 또는 라이브 외 탭에서는 `라이브 시작하기` 버튼이 표시되지 않는지 확인한다.
|
||||||
- 검증 기준:
|
- 검증 기준:
|
||||||
- 기대 결과: PRD Success Criteria를 모두 만족한다.
|
- 기대 결과: PRD Success Criteria를 모두 만족한다.
|
||||||
|
- blocked 사유: 실제 로그인 세션, 타인/본인 크리에이터 채널 테스트 데이터, 라이브/다시듣기 응답 데이터, 네트워크 요청 관찰 수단이 필요해 현재 CLI 환경에서는 완료 검증하지 않는다.
|
||||||
|
|
||||||
## 검증 기록
|
## 검증 기록
|
||||||
|
|
||||||
@@ -350,3 +350,10 @@
|
|||||||
- 2026-07-03: Phase 2.3 보완. 첫 페이지/정렬 변경/다음 페이지 조회 모두 성공 시에만 목록 교체 또는 append를 수행하고, 실패 시 기존 목록과 pagination 상태를 유지하도록 PRD/계획/구현을 정렬함.
|
- 2026-07-03: Phase 2.3 보완. 첫 페이지/정렬 변경/다음 페이지 조회 모두 성공 시에만 목록 교체 또는 append를 수행하고, 실패 시 기존 목록과 pagination 상태를 유지하도록 PRD/계획/구현을 정렬함.
|
||||||
- 2026-07-04: Phase 4 구현 완료. `CreatorChannelLiveReplayListItem`, `CreatorChannelLiveStartButton`, `CreatorChannelLiveTabView`를 추가하고 `I18n.CreatorChannelLive.startLive` 문구를 추가함. 저장소에 별도 XCTest 파일이 없어 Phase 4는 `rg` 정적 검색, `plutil -lint SodaLive.xcodeproj/project.pbxproj`, `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`로 검증했으며 `** BUILD SUCCEEDED **`를 확인함.
|
- 2026-07-04: Phase 4 구현 완료. `CreatorChannelLiveReplayListItem`, `CreatorChannelLiveStartButton`, `CreatorChannelLiveTabView`를 추가하고 `I18n.CreatorChannelLive.startLive` 문구를 추가함. 저장소에 별도 XCTest 파일이 없어 Phase 4는 `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 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.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로 이동해 화면 하단 고정 구조로 변경함.
|
||||||
|
|||||||
@@ -50,8 +50,8 @@ Figma 참조:
|
|||||||
|
|
||||||
## 6. User Stories
|
## 6. User Stories
|
||||||
- 사용자는 크리에이터 채널 라이브 탭에서 현재 진행 중인 라이브가 있으면 상단에서 바로 확인하고 싶다.
|
- 사용자는 크리에이터 채널 라이브 탭에서 현재 진행 중인 라이브가 있으면 상단에서 바로 확인하고 싶다.
|
||||||
- 사용자는 라이브 다시듣기 전체 개수와 현재 정렬 기준을 확인하고 싶다.
|
- 사용자는 라이브 다시듣기 현재 정렬 기준을 확인하고 싶다.
|
||||||
- 사용자는 최신순, 인기순, 소장순, 가격 높은 순, 가격 낮은 순으로 다시듣기 목록을 정렬하고 싶다.
|
- 사용자는 최신순, 인기순, 가격 높은 순, 가격 낮은 순으로 다시듣기 목록을 정렬하고 싶다.
|
||||||
- 사용자는 목록을 아래로 스크롤하면 다음 페이지가 자동으로 이어서 로드되기를 기대한다.
|
- 사용자는 목록을 아래로 스크롤하면 다음 페이지가 자동으로 이어서 로드되기를 기대한다.
|
||||||
- 사용자는 다시듣기 item에서 무료, 가격, 포인트 사용 가능, 19금, 소장중, 대여중 상태를 구분하고 싶다.
|
- 사용자는 다시듣기 item에서 무료, 가격, 포인트 사용 가능, 19금, 소장중, 대여중 상태를 구분하고 싶다.
|
||||||
- 크리에이터는 본인 채널의 라이브 탭에서 하단 고정 버튼으로 라이브 시작 화면에 빠르게 진입하고 싶다.
|
- 크리에이터는 본인 채널의 라이브 탭에서 하단 고정 버튼으로 라이브 시작 화면에 빠르게 진입하고 싶다.
|
||||||
@@ -99,7 +99,6 @@ data class CreatorChannelAudioContentResponse(
|
|||||||
enum class ContentSort {
|
enum class ContentSort {
|
||||||
LATEST,
|
LATEST,
|
||||||
POPULAR,
|
POPULAR,
|
||||||
OWNED,
|
|
||||||
PRICE_HIGH,
|
PRICE_HIGH,
|
||||||
PRICE_LOW
|
PRICE_LOW
|
||||||
}
|
}
|
||||||
@@ -120,7 +119,6 @@ struct CreatorChannelLiveTabResponse: Decodable {
|
|||||||
enum ContentSort: String, Decodable, CaseIterable {
|
enum ContentSort: String, Decodable, CaseIterable {
|
||||||
case latest = "LATEST"
|
case latest = "LATEST"
|
||||||
case popular = "POPULAR"
|
case popular = "POPULAR"
|
||||||
case owned = "OWNED"
|
|
||||||
case priceHigh = "PRICE_HIGH"
|
case priceHigh = "PRICE_HIGH"
|
||||||
case priceLow = "PRICE_LOW"
|
case priceLow = "PRICE_LOW"
|
||||||
}
|
}
|
||||||
@@ -135,27 +133,25 @@ enum ContentSort: String, Decodable, CaseIterable {
|
|||||||
- 동일 화면 생명주기에서 같은 `creatorId`, `sort`, 첫 페이지 데이터가 이미 있으면 불필요한 중복 호출을 피한다.
|
- 동일 화면 생명주기에서 같은 `creatorId`, `sort`, 첫 페이지 데이터가 이미 있으면 불필요한 중복 호출을 피한다.
|
||||||
- 정렬 변경 시 첫 페이지부터 다시 조회하고, 조회 성공 시에만 기존 `liveReplayContents`를 새 정렬 결과로 교체한다.
|
- 정렬 변경 시 첫 페이지부터 다시 조회하고, 조회 성공 시에만 기존 `liveReplayContents`를 새 정렬 결과로 교체한다.
|
||||||
- API 실패 시 크리에이터 채널 공통 shell은 유지하고, 이미 표시 중인 `liveReplayContents`는 유지한 채 기존 V2 패턴의 오류/빈 상태를 표시한다.
|
- API 실패 시 크리에이터 채널 공통 shell은 유지하고, 이미 표시 중인 `liveReplayContents`는 유지한 채 기존 V2 패턴의 오류/빈 상태를 표시한다.
|
||||||
- `liveReplayContentCount == 0`이고 `currentLive == nil`이면 sort-bar의 전체 개수는 `0`으로 표시하고 목록 empty state를 표시한다.
|
- `liveReplayContentCount == 0`이고 `currentLive == nil`이면 목록 empty state를 표시한다.
|
||||||
|
|
||||||
### 7.3 Sort-bar
|
### 7.3 Sort-bar
|
||||||
- 생성 위치: `SodaLive/Sources/V2/CreatorChannel/Components/**`
|
- 생성 위치: `SodaLive/Sources/V2/CreatorChannel/Components/**`
|
||||||
- sort-bar는 좌측에 전체 개수, 우측에 현재 정렬명과 아래 방향 아이콘을 표시한다.
|
- sort-bar는 현재 정렬명과 아래 방향 아이콘을 표시한다.
|
||||||
- Figma 기준 높이는 `52pt`, 좌우 horizontal padding은 기존 `SodaSpacing` 토큰으로 맞춘다.
|
- Figma 기준 높이는 `52pt`, 좌우 horizontal padding은 기존 `SodaSpacing` 토큰으로 맞춘다.
|
||||||
- 전체 개수는 `liveReplayContentCount`를 사용한다.
|
- 정렬명은 `I18n`에 추가한다.
|
||||||
- 문구 `전체`, 정렬명은 `I18n`에 추가한다.
|
- sort-bar는 다른 탭에서도 재사용 가능하도록 `selectedSort`, `availableSorts`, `onTapSort` 같은 일반화된 입력을 받는다.
|
||||||
- sort-bar는 다른 탭에서도 재사용 가능하도록 `totalCount`, `selectedSort`, `availableSorts`, `onTapSort` 같은 일반화된 입력을 받는다.
|
|
||||||
- 이번 범위에서는 Figma의 `리스트형` view mode 옵션은 사용하지 않는다.
|
- 이번 범위에서는 Figma의 `리스트형` view mode 옵션은 사용하지 않는다.
|
||||||
|
|
||||||
### 7.4 정렬 선택 UI
|
### 7.4 정렬 선택 UI
|
||||||
- 정렬 선택 UI는 BottomSheet 방식으로 구현한다.
|
- 정렬 선택 UI는 BottomSheet 방식으로 구현한다.
|
||||||
- 선택 사유:
|
- 선택 사유:
|
||||||
- 모바일 한 손 조작에서 화면 하단 sheet가 우측 상단 anchored menu보다 터치 접근성이 좋다.
|
- 모바일 한 손 조작에서 화면 하단 sheet가 우측 상단 anchored menu보다 터치 접근성이 좋다.
|
||||||
- 정렬 옵션이 5개이고 이후 탭별 옵션 추가 가능성이 있어 sheet가 선택 영역과 dismiss 동작을 안정적으로 제공한다.
|
- 정렬 옵션이 4개이고 이후 탭별 옵션 추가 가능성이 있어 sheet가 선택 영역과 dismiss 동작을 안정적으로 제공한다.
|
||||||
- iOS/Material 계열 최신 모바일 앱에서 필터/정렬 같은 목록 제어는 sheet 패턴이 일반적이며, context menu는 짧은 보조 action에 더 적합하다.
|
- iOS/Material 계열 최신 모바일 앱에서 필터/정렬 같은 목록 제어는 sheet 패턴이 일반적이며, context menu는 짧은 보조 action에 더 적합하다.
|
||||||
- 정렬 sheet 항목:
|
- 정렬 sheet 항목:
|
||||||
- `LATEST`: 최신순
|
- `LATEST`: 최신순
|
||||||
- `POPULAR`: 인기순
|
- `POPULAR`: 인기순
|
||||||
- `OWNED`: 소장순
|
|
||||||
- `PRICE_HIGH`: 가격 높은 순
|
- `PRICE_HIGH`: 가격 높은 순
|
||||||
- `PRICE_LOW`: 가격 낮은 순
|
- `PRICE_LOW`: 가격 낮은 순
|
||||||
- Figma `290:9041`에는 `추천순`이 있으나 API `ContentSort`에 없으므로 구현하지 않는다.
|
- Figma `290:9041`에는 `추천순`이 있으나 API `ContentSort`에 없으므로 구현하지 않는다.
|
||||||
@@ -230,8 +226,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 좌측에 `liveReplayContentCount` 기준 전체 개수가 표시된다.
|
- sort-bar 터치 시 BottomSheet가 표시되고 `LATEST`, `POPULAR`, `PRICE_HIGH`, `PRICE_LOW` 4개 옵션만 보인다.
|
||||||
- sort-bar 터치 시 BottomSheet가 표시되고 `LATEST`, `POPULAR`, `OWNED`, `PRICE_HIGH`, `PRICE_LOW` 5개 옵션만 보인다.
|
|
||||||
- 정렬 변경 시 첫 페이지부터 다시 조회되고, 조회 성공 시에만 목록이 새 정렬 결과로 교체된다.
|
- 정렬 변경 시 첫 페이지부터 다시 조회되고, 조회 성공 시에만 목록이 새 정렬 결과로 교체된다.
|
||||||
- `currentLive != nil`이면 홈 탭과 동일한 현재 라이브 UI가 라이브 탭에 표시된다.
|
- `currentLive != nil`이면 홈 탭과 동일한 현재 라이브 UI가 라이브 탭에 표시된다.
|
||||||
- 라이브 다시듣기 item에 가격, 포인트 사용 가능, 19금, 소장중, 대여중 상태가 조건에 맞게 표시된다.
|
- 라이브 다시듣기 item에 가격, 포인트 사용 가능, 19금, 소장중, 대여중 상태가 조건에 맞게 표시된다.
|
||||||
|
|||||||
Reference in New Issue
Block a user