refactor(navigation): 전역 경로 기반 단일 내비게이션 흐름으로 전환한다
This commit is contained in:
@@ -7,13 +7,23 @@
|
|||||||
|
|
||||||
import Foundation
|
import Foundation
|
||||||
|
|
||||||
|
struct AppRoute: Hashable {
|
||||||
|
let id = UUID()
|
||||||
|
}
|
||||||
|
|
||||||
class AppState: ObservableObject {
|
class AppState: ObservableObject {
|
||||||
static let shared = AppState()
|
static let shared = AppState()
|
||||||
|
|
||||||
private var appStepBackStack = [AppStep]()
|
private var routeStepMap: [AppRoute: AppStep] = [:]
|
||||||
|
|
||||||
@Published var alreadyUpdatedMarketingInfo = false
|
@Published var alreadyUpdatedMarketingInfo = false
|
||||||
@Published private(set) var appStep: AppStep = .splash
|
@Published private(set) var appStep: AppStep = .splash
|
||||||
|
@Published private(set) var rootStep: AppStep = .splash
|
||||||
|
@Published var navigationPath: [AppRoute] = [] {
|
||||||
|
didSet {
|
||||||
|
syncStepWithNavigationPath()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
@Published var isShowPlayer = false {
|
@Published var isShowPlayer = false {
|
||||||
didSet {
|
didSet {
|
||||||
@@ -53,25 +63,49 @@ class AppState: ObservableObject {
|
|||||||
@Published var isShowErrorPopup = false
|
@Published var isShowErrorPopup = false
|
||||||
@Published var errorMessage = ""
|
@Published var errorMessage = ""
|
||||||
|
|
||||||
func setAppStep(step: AppStep) {
|
private func syncStepWithNavigationPath() {
|
||||||
switch step {
|
let validRoutes = Set(navigationPath)
|
||||||
case .splash, .main:
|
routeStepMap = routeStepMap.filter { validRoutes.contains($0.key) }
|
||||||
appStepBackStack.removeAll()
|
|
||||||
|
|
||||||
default:
|
if let route = navigationPath.last,
|
||||||
appStepBackStack.append(appStep)
|
let step = routeStepMap[route] {
|
||||||
|
appStep = step
|
||||||
|
} else {
|
||||||
|
appStep = rootStep
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func appStep(for route: AppRoute) -> AppStep? {
|
||||||
|
routeStepMap[route]
|
||||||
|
}
|
||||||
|
|
||||||
|
func setAppStep(step: AppStep) {
|
||||||
DispatchQueue.main.async {
|
DispatchQueue.main.async {
|
||||||
self.appStep = step
|
switch step {
|
||||||
|
case .splash, .main:
|
||||||
|
self.rootStep = step
|
||||||
|
self.routeStepMap.removeAll()
|
||||||
|
self.navigationPath.removeAll()
|
||||||
|
self.appStep = step
|
||||||
|
|
||||||
|
default:
|
||||||
|
let route = AppRoute()
|
||||||
|
self.routeStepMap[route] = step
|
||||||
|
self.navigationPath.append(route)
|
||||||
|
self.appStep = step
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func back() {
|
func back() {
|
||||||
if let step = appStepBackStack.popLast() {
|
DispatchQueue.main.async {
|
||||||
self.appStep = step
|
if self.navigationPath.isEmpty {
|
||||||
} else {
|
self.rootStep = .main
|
||||||
self.appStep = .main
|
self.appStep = .main
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
_ = self.navigationPath.popLast()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -14,105 +14,100 @@ struct NewCharacterListView: View {
|
|||||||
private let gridSpacing: CGFloat = 12
|
private let gridSpacing: CGFloat = 12
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
Group { BaseView(isLoading: $viewModel.isLoading) {
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
VStack(spacing: 8) {
|
||||||
VStack(spacing: 8) {
|
// Toolbar
|
||||||
// Toolbar
|
DetailNavigationBar(title: String(localized: "신규 캐릭터 전체보기"))
|
||||||
DetailNavigationBar(title: String(localized: "신규 캐릭터 전체보기"))
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 12) {
|
VStack(alignment: .leading, spacing: 12) {
|
||||||
// 전체 n개
|
// 전체 n개
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
Text("전체")
|
Text("전체")
|
||||||
.appFont(size: 12, weight: .regular)
|
.appFont(size: 12, weight: .regular)
|
||||||
.foregroundColor(Color(hex: "e2e2e2"))
|
.foregroundColor(Color(hex: "e2e2e2"))
|
||||||
Text(" \(viewModel.totalCount)")
|
Text(" \(viewModel.totalCount)")
|
||||||
.appFont(size: 12, weight: .regular)
|
.appFont(size: 12, weight: .regular)
|
||||||
.foregroundColor(Color(hex: "ff5c49"))
|
.foregroundColor(Color(hex: "ff5c49"))
|
||||||
Text("개")
|
Text("개")
|
||||||
.appFont(size: 12, weight: .regular)
|
.appFont(size: 12, weight: .regular)
|
||||||
.foregroundColor(Color(hex: "e2e2e2"))
|
.foregroundColor(Color(hex: "e2e2e2"))
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 24)
|
.padding(.horizontal, 24)
|
||||||
|
|
||||||
// Grid 3열
|
// Grid 3열
|
||||||
GeometryReader { geo in
|
GeometryReader { geo in
|
||||||
let width = (geo.size.width - (horizontalPadding * 2) - gridSpacing) / 2
|
let width = (geo.size.width - (horizontalPadding * 2) - gridSpacing) / 2
|
||||||
|
|
||||||
ScrollView(.vertical, showsIndicators: false) {
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
LazyVGrid(
|
LazyVGrid(
|
||||||
columns: Array(
|
columns: Array(
|
||||||
repeating: GridItem(
|
repeating: GridItem(
|
||||||
.flexible(),
|
.flexible(),
|
||||||
spacing: gridSpacing,
|
spacing: gridSpacing,
|
||||||
alignment: .topLeading
|
alignment: .topLeading
|
||||||
),
|
|
||||||
count: 2
|
|
||||||
),
|
),
|
||||||
alignment: .leading,
|
count: 2
|
||||||
spacing: gridSpacing
|
),
|
||||||
) {
|
alignment: .leading,
|
||||||
ForEach(viewModel.items.indices, id: \.self) { idx in
|
spacing: gridSpacing
|
||||||
let item = viewModel.items[idx]
|
) {
|
||||||
|
ForEach(viewModel.items.indices, id: \.self) { idx in
|
||||||
|
let item = viewModel.items[idx]
|
||||||
|
|
||||||
NavigationLink(value: item.characterId) {
|
CharacterItemView(
|
||||||
CharacterItemView(
|
character: item,
|
||||||
character: item,
|
size: width,
|
||||||
size: width,
|
rank: 0,
|
||||||
rank: 0,
|
isShowRank: false
|
||||||
isShowRank: false
|
)
|
||||||
)
|
.onAppear { viewModel.loadMoreIfNeeded(currentIndex: idx) }
|
||||||
.onAppear { viewModel.loadMoreIfNeeded(currentIndex: idx) }
|
.onTapGesture { AppState.shared.setAppStep(step: .characterDetail(characterId: item.characterId)) }
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
.padding(.horizontal, horizontalPadding)
|
}
|
||||||
|
.padding(.horizontal, horizontalPadding)
|
||||||
|
|
||||||
if viewModel.isLoadingMore {
|
if viewModel.isLoadingMore {
|
||||||
HStack {
|
HStack {
|
||||||
Spacer()
|
Spacer()
|
||||||
ProgressView()
|
ProgressView()
|
||||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||||
.padding(.vertical, 16)
|
.padding(.vertical, 16)
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(minHeight: 0, maxHeight: .infinity)
|
|
||||||
}
|
|
||||||
.padding(.vertical, 12)
|
|
||||||
.onAppear {
|
|
||||||
// 최초 1회만 로드하여 상세 진입 후 복귀 시 스크롤 위치가 유지되도록 함
|
|
||||||
if viewModel.items.isEmpty {
|
|
||||||
viewModel.fetch()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
.frame(minHeight: 0, maxHeight: .infinity)
|
||||||
}
|
}
|
||||||
.background(Color.black)
|
.padding(.vertical, 12)
|
||||||
}
|
.onAppear {
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) {
|
// 최초 1회만 로드하여 상세 진입 후 복귀 시 스크롤 위치가 유지되도록 함
|
||||||
GeometryReader { geo in
|
if viewModel.items.isEmpty {
|
||||||
HStack {
|
viewModel.fetch()
|
||||||
Spacer()
|
|
||||||
Text(viewModel.errorMessage)
|
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.frame(width: geo.size.width - 66.7, alignment: .center)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.background(Color.button)
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.center)
|
|
||||||
.cornerRadius(20)
|
|
||||||
.padding(.top, 66.7)
|
|
||||||
Spacer()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationDestination(for: Int.self) { characterId in
|
.background(Color.black)
|
||||||
CharacterDetailView(characterId: characterId)
|
}
|
||||||
|
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) {
|
||||||
|
GeometryReader { geo in
|
||||||
|
HStack {
|
||||||
|
Spacer()
|
||||||
|
Text(viewModel.errorMessage)
|
||||||
|
.padding(.vertical, 13.3)
|
||||||
|
.frame(width: geo.size.width - 66.7, alignment: .center)
|
||||||
|
.appFont(size: 12, weight: .medium)
|
||||||
|
.background(Color.button)
|
||||||
|
.foregroundColor(Color.white)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.cornerRadius(20)
|
||||||
|
.padding(.top, 66.7)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,91 +15,87 @@ struct OriginalWorkDetailView: View {
|
|||||||
let originalId: Int
|
let originalId: Int
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationStack {
|
Group { BaseView(isLoading: $viewModel.isLoading) {
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
ZStack(alignment: .top) {
|
||||||
ZStack(alignment: .top) {
|
if let imageUrl = viewModel.response?.imageUrl {
|
||||||
if let imageUrl = viewModel.response?.imageUrl {
|
KFImage(URL(string: imageUrl))
|
||||||
KFImage(URL(string: imageUrl))
|
.cancelOnDisappear(true)
|
||||||
.cancelOnDisappear(true)
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(width: screenSize().width, height: (168 * 288 / 306) + 56)
|
||||||
|
.clipped()
|
||||||
|
.blur(radius: 25)
|
||||||
|
}
|
||||||
|
|
||||||
|
Color.black.opacity(0.5).ignoresSafeArea()
|
||||||
|
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Image("ic_back")
|
||||||
.resizable()
|
.resizable()
|
||||||
.scaledToFill()
|
.frame(width: 24, height: 24)
|
||||||
.frame(width: screenSize().width, height: (168 * 288 / 306) + 56)
|
.onTapGesture {
|
||||||
.clipped()
|
AppState.shared.back()
|
||||||
.blur(radius: 25)
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
}
|
}
|
||||||
|
.padding(.horizontal, 24)
|
||||||
|
.frame(height: 56)
|
||||||
|
|
||||||
Color.black.opacity(0.5).ignoresSafeArea()
|
if let response = viewModel.response {
|
||||||
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
|
VStack(spacing: 0) {
|
||||||
|
OriginalWorkDetailHeaderView(item: response)
|
||||||
|
.padding(.horizontal, 24)
|
||||||
|
.padding(.bottom, 24)
|
||||||
|
|
||||||
VStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
HStack(spacing: 0) {
|
SeriesDetailTabView(
|
||||||
Image("ic_back")
|
title: I18n.Tab.character,
|
||||||
.resizable()
|
width: screenSize().width / 2,
|
||||||
.frame(width: 24, height: 24)
|
isSelected: viewModel.currentTab == .character
|
||||||
.onTapGesture {
|
) {
|
||||||
AppState.shared.back()
|
if viewModel.currentTab != .character {
|
||||||
|
viewModel.currentTab = .character
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SeriesDetailTabView(
|
||||||
|
title: I18n.Tab.workInfo,
|
||||||
|
width: screenSize().width / 2,
|
||||||
|
isSelected: viewModel.currentTab == .info
|
||||||
|
) {
|
||||||
|
if viewModel.currentTab != .info {
|
||||||
|
viewModel.currentTab = .info
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.background(Color.black)
|
||||||
|
|
||||||
Spacer()
|
Rectangle()
|
||||||
}
|
.foregroundColor(Color.gray90.opacity(0.5))
|
||||||
.padding(.horizontal, 24)
|
.frame(height: 1)
|
||||||
.frame(height: 56)
|
.frame(maxWidth: .infinity)
|
||||||
|
|
||||||
if let response = viewModel.response {
|
switch(viewModel.currentTab) {
|
||||||
ScrollView(.vertical, showsIndicators: false) {
|
case .info:
|
||||||
VStack(spacing: 0) {
|
OriginalWorkInfoView(response: response)
|
||||||
OriginalWorkDetailHeaderView(item: response)
|
default:
|
||||||
.padding(.horizontal, 24)
|
OriginalWorkCharacterView(characters: viewModel.characters)
|
||||||
.padding(.bottom, 24)
|
|
||||||
|
|
||||||
HStack(spacing: 0) {
|
|
||||||
SeriesDetailTabView(
|
|
||||||
title: I18n.Tab.character,
|
|
||||||
width: screenSize().width / 2,
|
|
||||||
isSelected: viewModel.currentTab == .character
|
|
||||||
) {
|
|
||||||
if viewModel.currentTab != .character {
|
|
||||||
viewModel.currentTab = .character
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
SeriesDetailTabView(
|
|
||||||
title: I18n.Tab.workInfo,
|
|
||||||
width: screenSize().width / 2,
|
|
||||||
isSelected: viewModel.currentTab == .info
|
|
||||||
) {
|
|
||||||
if viewModel.currentTab != .info {
|
|
||||||
viewModel.currentTab = .info
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.background(Color.black)
|
|
||||||
|
|
||||||
Rectangle()
|
|
||||||
.foregroundColor(Color.gray90.opacity(0.5))
|
|
||||||
.frame(height: 1)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
|
|
||||||
switch(viewModel.currentTab) {
|
|
||||||
case .info:
|
|
||||||
OriginalWorkInfoView(response: response)
|
|
||||||
default:
|
|
||||||
OriginalWorkCharacterView(characters: viewModel.characters)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
}
|
||||||
if viewModel.response == nil {
|
.onAppear {
|
||||||
viewModel.originalId = originalId
|
if viewModel.response == nil {
|
||||||
}
|
viewModel.originalId = originalId
|
||||||
}
|
|
||||||
.navigationDestination(for: Int.self) { characterId in
|
|
||||||
CharacterDetailView(characterId: characterId)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -129,14 +125,13 @@ struct OriginalWorkCharacterView: View {
|
|||||||
ForEach(characters.indices, id: \.self) { idx in
|
ForEach(characters.indices, id: \.self) { idx in
|
||||||
let item = characters[idx]
|
let item = characters[idx]
|
||||||
|
|
||||||
NavigationLink(value: item.characterId) {
|
CharacterItemView(
|
||||||
CharacterItemView(
|
character: item,
|
||||||
character: item,
|
size: width,
|
||||||
size: width,
|
rank: 0,
|
||||||
rank: 0,
|
isShowRank: false
|
||||||
isShowRank: false
|
)
|
||||||
)
|
.onTapGesture { AppState.shared.setAppStep(step: .characterDetail(characterId: item.characterId)) }
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, horizontalPadding)
|
.padding(.horizontal, horizontalPadding)
|
||||||
|
|||||||
@@ -9,11 +9,12 @@ import SwiftUI
|
|||||||
|
|
||||||
struct ContentAllByThemeView: View {
|
struct ContentAllByThemeView: View {
|
||||||
@StateObject var viewModel = ContentAllByThemeViewModel()
|
@StateObject var viewModel = ContentAllByThemeViewModel()
|
||||||
|
@State private var isInitialized = false
|
||||||
|
|
||||||
let themeId: Int
|
let themeId: Int
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
Group {
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
BaseView(isLoading: $viewModel.isLoading) {
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
DetailNavigationBar(title: viewModel.theme)
|
DetailNavigationBar(title: viewModel.theme)
|
||||||
@@ -111,8 +112,11 @@ struct ContentAllByThemeView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.themeId = themeId
|
if !isInitialized || viewModel.themeId != themeId {
|
||||||
viewModel.getContentList()
|
viewModel.themeId = themeId
|
||||||
|
viewModel.getContentList()
|
||||||
|
isInitialized = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,12 +10,13 @@ import SwiftUI
|
|||||||
struct ContentAllView: View {
|
struct ContentAllView: View {
|
||||||
|
|
||||||
@StateObject var viewModel = ContentAllViewModel()
|
@StateObject var viewModel = ContentAllViewModel()
|
||||||
|
@State private var isInitialized = false
|
||||||
|
|
||||||
var isFree: Bool = false
|
var isFree: Bool = false
|
||||||
var isPointAvailableOnly: Bool = false
|
var isPointAvailableOnly: Bool = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
Group {
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
BaseView(isLoading: $viewModel.isLoading) {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
DetailNavigationBar(title: isFree ? String(localized: "무료 콘텐츠 전체") : isPointAvailableOnly ? String(localized: "포인트 대여 전체") : String(localized: "콘텐츠 전체"))
|
DetailNavigationBar(title: isFree ? String(localized: "무료 콘텐츠 전체") : isPointAvailableOnly ? String(localized: "포인트 대여 전체") : String(localized: "콘텐츠 전체"))
|
||||||
@@ -78,63 +79,63 @@ struct ContentAllView: View {
|
|||||||
ForEach(viewModel.contentList.indices, id: \.self) { idx in
|
ForEach(viewModel.contentList.indices, id: \.self) { idx in
|
||||||
let item = viewModel.contentList[idx]
|
let item = viewModel.contentList[idx]
|
||||||
|
|
||||||
NavigationLink {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
ContentDetailView(contentId: item.contentId)
|
ZStack(alignment: .top) {
|
||||||
} label: {
|
DownsampledKFImage(
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
url: URL(string: item.coverImageUrl),
|
||||||
ZStack(alignment: .top) {
|
size: CGSize(width: itemSize, height: itemSize)
|
||||||
DownsampledKFImage(
|
)
|
||||||
url: URL(string: item.coverImageUrl),
|
.cornerRadius(16)
|
||||||
size: CGSize(width: itemSize, height: itemSize)
|
|
||||||
)
|
|
||||||
.cornerRadius(16)
|
|
||||||
|
|
||||||
HStack(alignment: .top, spacing: 0) {
|
HStack(alignment: .top, spacing: 0) {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
if item.isPointAvailable {
|
if item.isPointAvailable {
|
||||||
Image("ic_point")
|
Image("ic_point")
|
||||||
.padding(.top, 6)
|
.padding(.top, 6)
|
||||||
.padding(.trailing, 6)
|
.padding(.trailing, 6)
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
Text(item.title)
|
|
||||||
.appFont(size: 18, weight: .regular)
|
|
||||||
.foregroundColor(.white)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
|
||||||
.lineLimit(1)
|
|
||||||
.padding(.horizontal, 6)
|
|
||||||
.padding(.top, 8)
|
|
||||||
|
|
||||||
|
|
||||||
Text(item.creatorNickname)
|
|
||||||
.appFont(size: 14, weight: .regular)
|
|
||||||
.foregroundColor(Color(hex: "78909C"))
|
|
||||||
.lineLimit(1)
|
|
||||||
.padding(.horizontal, 6)
|
|
||||||
.padding(.top, 4)
|
|
||||||
}
|
}
|
||||||
.frame(width: itemSize)
|
|
||||||
.contentShape(Rectangle())
|
Text(item.title)
|
||||||
.onAppear {
|
.appFont(size: 18, weight: .regular)
|
||||||
if idx == viewModel.contentList.count - 1 {
|
.foregroundColor(.white)
|
||||||
viewModel.fetchData()
|
.multilineTextAlignment(.leading)
|
||||||
}
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
.lineLimit(1)
|
||||||
|
.padding(.horizontal, 6)
|
||||||
|
.padding(.top, 8)
|
||||||
|
|
||||||
|
|
||||||
|
Text(item.creatorNickname)
|
||||||
|
.appFont(size: 14, weight: .regular)
|
||||||
|
.foregroundColor(Color(hex: "78909C"))
|
||||||
|
.lineLimit(1)
|
||||||
|
.padding(.horizontal, 6)
|
||||||
|
.padding(.top, 4)
|
||||||
|
}
|
||||||
|
.frame(width: itemSize)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.onAppear {
|
||||||
|
if idx == viewModel.contentList.count - 1 {
|
||||||
|
viewModel.fetchData()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onTapGesture { AppState.shared.setAppStep(step: .contentDetail(contentId: item.contentId)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(horizontalPadding)
|
.padding(horizontalPadding)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.isFree = isFree
|
if !isInitialized || viewModel.isFree != isFree || viewModel.isPointAvailableOnly != isPointAvailableOnly {
|
||||||
viewModel.isPointAvailableOnly = isPointAvailableOnly
|
viewModel.isFree = isFree
|
||||||
viewModel.getThemeList()
|
viewModel.isPointAvailableOnly = isPointAvailableOnly
|
||||||
viewModel.fetchData()
|
viewModel.getThemeList()
|
||||||
|
viewModel.fetchData()
|
||||||
|
isInitialized = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,95 +14,92 @@ struct ContentNewAllItemView: View {
|
|||||||
let item: GetAudioContentMainItem
|
let item: GetAudioContentMainItem
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationLink {
|
VStack(alignment: .leading, spacing: 8) {
|
||||||
ContentDetailView(contentId: item.contentId)
|
ZStack(alignment: .bottom) {
|
||||||
} label: {
|
KFImage(URL(string: item.coverImageUrl))
|
||||||
VStack(alignment: .leading, spacing: 8) {
|
.cancelOnDisappear(true)
|
||||||
ZStack(alignment: .bottom) {
|
.downsampling(
|
||||||
KFImage(URL(string: item.coverImageUrl))
|
size: CGSize(
|
||||||
.cancelOnDisappear(true)
|
width: width,
|
||||||
.downsampling(
|
height: width
|
||||||
size: CGSize(
|
|
||||||
width: width,
|
|
||||||
height: width
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.resizable()
|
)
|
||||||
.scaledToFill()
|
.resizable()
|
||||||
.frame(width: width, height: width, alignment: .top)
|
.scaledToFill()
|
||||||
.cornerRadius(2.7)
|
.frame(width: width, height: width, alignment: .top)
|
||||||
|
.cornerRadius(2.7)
|
||||||
|
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
Spacer()
|
Spacer()
|
||||||
|
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
HStack(spacing: 2) {
|
HStack(spacing: 2) {
|
||||||
if item.price > 0 {
|
if item.price > 0 {
|
||||||
Image("ic_card_can_gray")
|
Image("ic_card_can_gray")
|
||||||
|
|
||||||
Text("\(item.price)")
|
Text("\(item.price)")
|
||||||
.appFont(size: 8.5, weight: .medium)
|
.appFont(size: 8.5, weight: .medium)
|
||||||
.foregroundColor(Color.white)
|
.foregroundColor(Color.white)
|
||||||
} else {
|
} else {
|
||||||
Text("무료")
|
Text("무료")
|
||||||
.appFont(size: 8.5, weight: .medium)
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(3)
|
|
||||||
.background(Color(hex: "333333").opacity(0.7))
|
|
||||||
.cornerRadius(10)
|
|
||||||
.padding(.leading, 2.7)
|
|
||||||
.padding(.bottom, 2.7)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
HStack(spacing: 2) {
|
|
||||||
Text(item.duration)
|
|
||||||
.appFont(size: 8.5, weight: .medium)
|
.appFont(size: 8.5, weight: .medium)
|
||||||
.foregroundColor(Color.white)
|
.foregroundColor(Color.white)
|
||||||
}
|
}
|
||||||
.padding(3)
|
|
||||||
.background(Color(hex: "333333").opacity(0.7))
|
|
||||||
.cornerRadius(10)
|
|
||||||
.padding(.trailing, 2.7)
|
|
||||||
.padding(.bottom, 2.7)
|
|
||||||
}
|
}
|
||||||
|
.padding(3)
|
||||||
|
.background(Color(hex: "333333").opacity(0.7))
|
||||||
|
.cornerRadius(10)
|
||||||
|
.padding(.leading, 2.7)
|
||||||
|
.padding(.bottom, 2.7)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
HStack(spacing: 2) {
|
||||||
|
Text(item.duration)
|
||||||
|
.appFont(size: 8.5, weight: .medium)
|
||||||
|
.foregroundColor(Color.white)
|
||||||
|
}
|
||||||
|
.padding(3)
|
||||||
|
.background(Color(hex: "333333").opacity(0.7))
|
||||||
|
.cornerRadius(10)
|
||||||
|
.padding(.trailing, 2.7)
|
||||||
|
.padding(.bottom, 2.7)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.frame(width: width, height: width)
|
|
||||||
|
|
||||||
Text(item.title)
|
|
||||||
.appFont(size: 13.3, weight: .medium)
|
|
||||||
.foregroundColor(Color(hex: "d2d2d2"))
|
|
||||||
.frame(width: width, alignment: .leading)
|
|
||||||
.multilineTextAlignment(.leading)
|
|
||||||
.fixedSize(horizontal: false, vertical: true)
|
|
||||||
.lineLimit(2)
|
|
||||||
|
|
||||||
HStack(spacing: 5.3) {
|
|
||||||
KFImage(URL(string: item.creatorProfileImageUrl))
|
|
||||||
.cancelOnDisappear(true)
|
|
||||||
.downsampling(
|
|
||||||
size: CGSize(
|
|
||||||
width: 21.3,
|
|
||||||
height: 21.3
|
|
||||||
)
|
|
||||||
)
|
|
||||||
.resizable()
|
|
||||||
.scaledToFill()
|
|
||||||
.frame(width: 21.3, height: 21.3)
|
|
||||||
.clipShape(Circle())
|
|
||||||
.onTapGesture { AppState.shared.setAppStep(step: .creatorDetail(userId: item.creatorId)) }
|
|
||||||
|
|
||||||
Text(item.creatorNickname)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.foregroundColor(Color(hex: "777777"))
|
|
||||||
.lineLimit(1)
|
|
||||||
}
|
|
||||||
.padding(.bottom, 10)
|
|
||||||
}
|
}
|
||||||
.frame(width: width)
|
.frame(width: width, height: width)
|
||||||
|
|
||||||
|
Text(item.title)
|
||||||
|
.appFont(size: 13.3, weight: .medium)
|
||||||
|
.foregroundColor(Color(hex: "d2d2d2"))
|
||||||
|
.frame(width: width, alignment: .leading)
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
.lineLimit(2)
|
||||||
|
|
||||||
|
HStack(spacing: 5.3) {
|
||||||
|
KFImage(URL(string: item.creatorProfileImageUrl))
|
||||||
|
.cancelOnDisappear(true)
|
||||||
|
.downsampling(
|
||||||
|
size: CGSize(
|
||||||
|
width: 21.3,
|
||||||
|
height: 21.3
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(width: 21.3, height: 21.3)
|
||||||
|
.clipShape(Circle())
|
||||||
|
.onTapGesture { AppState.shared.setAppStep(step: .creatorDetail(userId: item.creatorId)) }
|
||||||
|
|
||||||
|
Text(item.creatorNickname)
|
||||||
|
.appFont(size: 12, weight: .medium)
|
||||||
|
.foregroundColor(Color(hex: "777777"))
|
||||||
|
.lineLimit(1)
|
||||||
|
}
|
||||||
|
.padding(.bottom, 10)
|
||||||
}
|
}
|
||||||
|
.frame(width: width)
|
||||||
|
.onTapGesture { AppState.shared.setAppStep(step: .contentDetail(contentId: item.contentId)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,11 +10,12 @@ import SwiftUI
|
|||||||
struct ContentNewAllView: View {
|
struct ContentNewAllView: View {
|
||||||
|
|
||||||
@StateObject var viewModel = ContentNewAllViewModel()
|
@StateObject var viewModel = ContentNewAllViewModel()
|
||||||
|
@State private var isInitialized = false
|
||||||
|
|
||||||
let isFree: Bool
|
let isFree: Bool
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
Group {
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
BaseView(isLoading: $viewModel.isLoading) {
|
||||||
VStack(alignment: .leading, spacing: 13.3) {
|
VStack(alignment: .leading, spacing: 13.3) {
|
||||||
DetailNavigationBar(title: isFree ? "최신 무료 콘텐츠" : "최신 콘텐츠")
|
DetailNavigationBar(title: isFree ? "최신 무료 콘텐츠" : "최신 콘텐츠")
|
||||||
@@ -82,9 +83,12 @@ struct ContentNewAllView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.isFree = isFree
|
if !isInitialized || viewModel.isFree != isFree {
|
||||||
viewModel.getThemeList()
|
viewModel.isFree = isFree
|
||||||
viewModel.getNewContentList()
|
viewModel.getThemeList()
|
||||||
|
viewModel.getNewContentList()
|
||||||
|
isInitialized = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationBarHidden(true)
|
.navigationBarHidden(true)
|
||||||
|
|||||||
@@ -11,9 +11,10 @@ import Kingfisher
|
|||||||
struct ContentRankingAllView: View {
|
struct ContentRankingAllView: View {
|
||||||
|
|
||||||
@StateObject var viewModel = ContentRankingAllViewModel()
|
@StateObject var viewModel = ContentRankingAllViewModel()
|
||||||
|
@State private var isInitialized = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
Group {
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
BaseView(isLoading: $viewModel.isLoading) {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
DetailNavigationBar(title: "인기 콘텐츠")
|
DetailNavigationBar(title: "인기 콘텐츠")
|
||||||
@@ -44,97 +45,94 @@ struct ContentRankingAllView: View {
|
|||||||
LazyVStack(spacing: 20) {
|
LazyVStack(spacing: 20) {
|
||||||
ForEach(0..<viewModel.contentRankingItemList.count, id: \.self) { index in
|
ForEach(0..<viewModel.contentRankingItemList.count, id: \.self) { index in
|
||||||
let item = viewModel.contentRankingItemList[index]
|
let item = viewModel.contentRankingItemList[index]
|
||||||
NavigationLink {
|
HStack(spacing: 0) {
|
||||||
ContentDetailView(contentId: item.contentId)
|
KFImage(URL(string: item.coverImageUrl))
|
||||||
} label: {
|
.cancelOnDisappear(true)
|
||||||
HStack(spacing: 0) {
|
.downsampling(
|
||||||
KFImage(URL(string: item.coverImageUrl))
|
size: CGSize(
|
||||||
.cancelOnDisappear(true)
|
width: 66.7,
|
||||||
.downsampling(
|
height: 66.7
|
||||||
size: CGSize(
|
|
||||||
width: 66.7,
|
|
||||||
height: 66.7
|
|
||||||
)
|
|
||||||
)
|
)
|
||||||
.resizable()
|
)
|
||||||
.scaledToFill()
|
.resizable()
|
||||||
.frame(width: 66.7, height: 66.7, alignment: .top)
|
.scaledToFill()
|
||||||
.clipped()
|
.frame(width: 66.7, height: 66.7, alignment: .top)
|
||||||
.cornerRadius(5.3)
|
.clipped()
|
||||||
|
.cornerRadius(5.3)
|
||||||
|
|
||||||
Text("\(index + 1)")
|
Text("\(index + 1)")
|
||||||
.appFont(size: 16.7, weight: .bold)
|
.appFont(size: 16.7, weight: .bold)
|
||||||
.foregroundColor(Color(hex: "3bb9f1"))
|
.foregroundColor(Color(hex: "3bb9f1"))
|
||||||
.padding(.horizontal, 12)
|
.padding(.horizontal, 12)
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
HStack(spacing: 8) {
|
HStack(spacing: 8) {
|
||||||
Text(item.themeStr)
|
Text(item.themeStr)
|
||||||
.appFont(size: 8, weight: .medium)
|
.appFont(size: 8, weight: .medium)
|
||||||
.foregroundColor(Color(hex: "3bac6a"))
|
.foregroundColor(Color(hex: "3bac6a"))
|
||||||
.padding(2.6)
|
.padding(2.6)
|
||||||
.background(Color(hex: "28312b"))
|
.background(Color(hex: "28312b"))
|
||||||
.cornerRadius(2.6)
|
|
||||||
|
|
||||||
Text(item.duration)
|
|
||||||
.appFont(size: 8, weight: .medium)
|
|
||||||
.foregroundColor(Color(hex: "777777"))
|
|
||||||
.padding(2.6)
|
|
||||||
.background(Color(hex: "222222"))
|
|
||||||
.cornerRadius(2.6)
|
|
||||||
|
|
||||||
if item.isPointAvailable {
|
|
||||||
Text("포인트")
|
|
||||||
.appFont(size: 8, weight: .medium)
|
|
||||||
.foregroundColor(.white)
|
|
||||||
.padding(2.6)
|
|
||||||
.background(Color(hex: "7849bc"))
|
|
||||||
.cornerRadius(2.6)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(item.creatorNickname)
|
|
||||||
.appFont(size: 10.7, weight: .medium)
|
|
||||||
.foregroundColor(Color(hex: "777777"))
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
|
|
||||||
Text(item.title)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.foregroundColor(Color(hex: "d2d2d2"))
|
|
||||||
.lineLimit(2)
|
|
||||||
.padding(.top, 2.7)
|
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
if item.price > 0 {
|
|
||||||
HStack(spacing: 8) {
|
|
||||||
Image("ic_can")
|
|
||||||
.resizable()
|
|
||||||
.frame(width: 17, height: 17)
|
|
||||||
|
|
||||||
Text("\(item.price)")
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.foregroundColor(Color(hex: "909090"))
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
Text("무료")
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.foregroundColor(Color(hex: "ffffff"))
|
|
||||||
.padding(.horizontal, 5.3)
|
|
||||||
.padding(.vertical, 2.7)
|
|
||||||
.background(Color(hex: "cf5c37"))
|
|
||||||
.cornerRadius(2.6)
|
.cornerRadius(2.6)
|
||||||
|
|
||||||
|
Text(item.duration)
|
||||||
|
.appFont(size: 8, weight: .medium)
|
||||||
|
.foregroundColor(Color(hex: "777777"))
|
||||||
|
.padding(2.6)
|
||||||
|
.background(Color(hex: "222222"))
|
||||||
|
.cornerRadius(2.6)
|
||||||
|
|
||||||
|
if item.isPointAvailable {
|
||||||
|
Text("포인트")
|
||||||
|
.appFont(size: 8, weight: .medium)
|
||||||
|
.foregroundColor(.white)
|
||||||
|
.padding(2.6)
|
||||||
|
.background(Color(hex: "7849bc"))
|
||||||
|
.cornerRadius(2.6)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
Text(item.creatorNickname)
|
||||||
|
.appFont(size: 10.7, weight: .medium)
|
||||||
|
.foregroundColor(Color(hex: "777777"))
|
||||||
|
.padding(.vertical, 8)
|
||||||
|
|
||||||
|
Text(item.title)
|
||||||
|
.appFont(size: 12, weight: .medium)
|
||||||
|
.foregroundColor(Color(hex: "d2d2d2"))
|
||||||
|
.lineLimit(2)
|
||||||
|
.padding(.top, 2.7)
|
||||||
}
|
}
|
||||||
.frame(height: 66.7)
|
|
||||||
.contentShape(Rectangle())
|
Spacer()
|
||||||
.onAppear {
|
|
||||||
if index == viewModel.contentRankingItemList.count - 1 {
|
if item.price > 0 {
|
||||||
viewModel.getContentRanking()
|
HStack(spacing: 8) {
|
||||||
|
Image("ic_can")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 17, height: 17)
|
||||||
|
|
||||||
|
Text("\(item.price)")
|
||||||
|
.appFont(size: 12, weight: .medium)
|
||||||
|
.foregroundColor(Color(hex: "909090"))
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
Text("무료")
|
||||||
|
.appFont(size: 12, weight: .medium)
|
||||||
|
.foregroundColor(Color(hex: "ffffff"))
|
||||||
|
.padding(.horizontal, 5.3)
|
||||||
|
.padding(.vertical, 2.7)
|
||||||
|
.background(Color(hex: "cf5c37"))
|
||||||
|
.cornerRadius(2.6)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.frame(height: 66.7)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.onAppear {
|
||||||
|
if index == viewModel.contentRankingItemList.count - 1 {
|
||||||
|
viewModel.getContentRanking()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onTapGesture { AppState.shared.setAppStep(step: .contentDetail(contentId: item.contentId)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -165,8 +163,11 @@ struct ContentRankingAllView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.getContentRankingSortType()
|
if !isInitialized {
|
||||||
viewModel.getContentRanking()
|
viewModel.getContentRankingSortType()
|
||||||
|
viewModel.getContentRanking()
|
||||||
|
isInitialized = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,7 +21,7 @@ struct ContentBoxView: View {
|
|||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
NavigationView {
|
Group {
|
||||||
VStack(spacing: 13.3) {
|
VStack(spacing: 13.3) {
|
||||||
DetailNavigationBar(title: I18n.ContentBox.title)
|
DetailNavigationBar(title: I18n.ContentBox.title)
|
||||||
|
|
||||||
|
|||||||
@@ -11,9 +11,10 @@ struct ContentListView: View {
|
|||||||
|
|
||||||
let userId: Int
|
let userId: Int
|
||||||
@StateObject var viewModel = ContentListViewModel()
|
@StateObject var viewModel = ContentListViewModel()
|
||||||
|
@State private var isInitialized = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
Group {
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
BaseView(isLoading: $viewModel.isLoading) {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
@@ -128,17 +129,14 @@ struct ContentListView: View {
|
|||||||
|
|
||||||
ForEach(0..<viewModel.audioContentList.count, id: \.self) { index in
|
ForEach(0..<viewModel.audioContentList.count, id: \.self) { index in
|
||||||
let audioContent = viewModel.audioContentList[index]
|
let audioContent = viewModel.audioContentList[index]
|
||||||
NavigationLink {
|
ContentListItemView(item: audioContent)
|
||||||
ContentDetailView(contentId: audioContent.contentId)
|
.contentShape(Rectangle())
|
||||||
} label: {
|
.onAppear {
|
||||||
ContentListItemView(item: audioContent)
|
if index == viewModel.audioContentList.count - 1 {
|
||||||
.contentShape(Rectangle())
|
viewModel.getAudioContentList()
|
||||||
.onAppear {
|
|
||||||
if index == viewModel.audioContentList.count - 1 {
|
|
||||||
viewModel.getAudioContentList()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onTapGesture { AppState.shared.setAppStep(step: .contentDetail(contentId: audioContent.contentId)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 13.3)
|
.padding(.horizontal, 13.3)
|
||||||
@@ -147,9 +145,12 @@ struct ContentListView: View {
|
|||||||
.padding(.top, 13.3)
|
.padding(.top, 13.3)
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.userId = userId
|
if !isInitialized || viewModel.userId != userId {
|
||||||
viewModel.getCategoryList()
|
viewModel.userId = userId
|
||||||
viewModel.getAudioContentList()
|
viewModel.getCategoryList()
|
||||||
|
viewModel.getAudioContentList()
|
||||||
|
isInitialized = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
|
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
|
||||||
HStack {
|
HStack {
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import SwiftUI
|
|||||||
struct ContentCurationView: View {
|
struct ContentCurationView: View {
|
||||||
|
|
||||||
@StateObject var viewModel = ContentCurationViewModel()
|
@StateObject var viewModel = ContentCurationViewModel()
|
||||||
|
@State private var isInitialized = false
|
||||||
|
|
||||||
let title: String
|
let title: String
|
||||||
let curationId: Int
|
let curationId: Int
|
||||||
@@ -21,7 +22,7 @@ struct ContentCurationView: View {
|
|||||||
]
|
]
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
Group {
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
BaseView(isLoading: $viewModel.isLoading) {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
DetailNavigationBar(title: title)
|
DetailNavigationBar(title: title)
|
||||||
@@ -119,8 +120,11 @@ struct ContentCurationView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.curationId = curationId
|
if !isInitialized || viewModel.curationId != curationId {
|
||||||
viewModel.getContentList()
|
viewModel.curationId = curationId
|
||||||
|
viewModel.getContentList()
|
||||||
|
isInitialized = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ struct AudioContentCommentListView: View {
|
|||||||
@State private var isShowMemberProfilePopup: Bool = false
|
@State private var isShowMemberProfilePopup: Bool = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
Group {
|
||||||
ZStack {
|
ZStack {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
|
|||||||
@@ -422,12 +422,15 @@ struct ContentDetailView: View {
|
|||||||
.sheet(
|
.sheet(
|
||||||
isPresented: $isShowCommentListView,
|
isPresented: $isShowCommentListView,
|
||||||
content: {
|
content: {
|
||||||
AudioContentCommentListView(
|
NavigationStack {
|
||||||
isPresented: $isShowCommentListView,
|
AudioContentCommentListView(
|
||||||
creatorId: viewModel.audioContent!.creator.creatorId,
|
isPresented: $isShowCommentListView,
|
||||||
audioContentId: viewModel.audioContent!.contentId,
|
creatorId: viewModel.audioContent!.creator.creatorId,
|
||||||
isShowSecret: viewModel.audioContent!.existOrdered
|
audioContentId: viewModel.audioContent!.contentId,
|
||||||
)
|
isShowSecret: viewModel.audioContent!.existOrdered
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.toolbar(.hidden, for: .navigationBar)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) {
|
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) {
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import SwiftUI
|
|||||||
struct ContentMainAlarmAllView: View {
|
struct ContentMainAlarmAllView: View {
|
||||||
|
|
||||||
@StateObject var viewModel = ContentMainAlarmAllViewModel()
|
@StateObject var viewModel = ContentMainAlarmAllViewModel()
|
||||||
|
@State private var isInitialized = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
Group {
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
BaseView(isLoading: $viewModel.isLoading) {
|
||||||
VStack(alignment: .leading, spacing: 13.3) {
|
VStack(alignment: .leading, spacing: 13.3) {
|
||||||
DetailNavigationBar(title: "새로운 알람")
|
DetailNavigationBar(title: "새로운 알람")
|
||||||
@@ -81,7 +82,10 @@ struct ContentMainAlarmAllView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.getContentMainAlarmAll()
|
if !isInitialized {
|
||||||
|
viewModel.getContentMainAlarmAll()
|
||||||
|
isInitialized = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationBarHidden(true)
|
.navigationBarHidden(true)
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import SwiftUI
|
|||||||
struct ContentMainAsmrAllView: View {
|
struct ContentMainAsmrAllView: View {
|
||||||
|
|
||||||
@StateObject var viewModel = ContentNewAllViewModel()
|
@StateObject var viewModel = ContentNewAllViewModel()
|
||||||
|
@State private var isInitialized = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
Group {
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
BaseView(isLoading: $viewModel.isLoading) {
|
||||||
VStack(alignment: .leading, spacing: 13.3) {
|
VStack(alignment: .leading, spacing: 13.3) {
|
||||||
DetailNavigationBar(title: "새로운 ASMR")
|
DetailNavigationBar(title: "새로운 ASMR")
|
||||||
@@ -72,7 +73,14 @@ struct ContentMainAsmrAllView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.selectedTheme = "ASMR"
|
if !isInitialized {
|
||||||
|
if viewModel.selectedTheme != "ASMR" {
|
||||||
|
viewModel.selectedTheme = "ASMR"
|
||||||
|
} else if viewModel.newContentList.isEmpty {
|
||||||
|
viewModel.getNewContentList()
|
||||||
|
}
|
||||||
|
isInitialized = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationBarHidden(true)
|
.navigationBarHidden(true)
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ struct ContentMainViewV2: View {
|
|||||||
}
|
}
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
Group {
|
||||||
ZStack {
|
ZStack {
|
||||||
Color.black.ignoresSafeArea()
|
Color.black.ignoresSafeArea()
|
||||||
|
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import SwiftUI
|
|||||||
struct ContentMainIntroduceCreatorAllView: View {
|
struct ContentMainIntroduceCreatorAllView: View {
|
||||||
|
|
||||||
@StateObject var viewModel = ContentMainIntroduceCreatorAllViewModel()
|
@StateObject var viewModel = ContentMainIntroduceCreatorAllViewModel()
|
||||||
|
@State private var isInitialized = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
Group {
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
BaseView(isLoading: $viewModel.isLoading) {
|
||||||
VStack(spacing: 13.3) {
|
VStack(spacing: 13.3) {
|
||||||
DetailNavigationBar(title: "크리에이터 소개")
|
DetailNavigationBar(title: "크리에이터 소개")
|
||||||
@@ -48,7 +49,10 @@ struct ContentMainIntroduceCreatorAllView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.getIntroduceCreatorList()
|
if !isInitialized {
|
||||||
|
viewModel.getIntroduceCreatorList()
|
||||||
|
isInitialized = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationBarHidden(true)
|
.navigationBarHidden(true)
|
||||||
|
|||||||
@@ -10,9 +10,10 @@ import SwiftUI
|
|||||||
struct ContentMainReplayAllView: View {
|
struct ContentMainReplayAllView: View {
|
||||||
|
|
||||||
@StateObject var viewModel = ContentNewAllViewModel()
|
@StateObject var viewModel = ContentNewAllViewModel()
|
||||||
|
@State private var isInitialized = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
Group {
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
BaseView(isLoading: $viewModel.isLoading) {
|
||||||
VStack(alignment: .leading, spacing: 13.3) {
|
VStack(alignment: .leading, spacing: 13.3) {
|
||||||
DetailNavigationBar(title: "새로운 라이브 다시듣기")
|
DetailNavigationBar(title: "새로운 라이브 다시듣기")
|
||||||
@@ -72,7 +73,14 @@ struct ContentMainReplayAllView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.selectedTheme = "다시듣기"
|
if !isInitialized {
|
||||||
|
if viewModel.selectedTheme != "다시듣기" {
|
||||||
|
viewModel.selectedTheme = "다시듣기"
|
||||||
|
} else if viewModel.newContentList.isEmpty {
|
||||||
|
viewModel.getNewContentList()
|
||||||
|
}
|
||||||
|
isInitialized = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.navigationBarHidden(true)
|
.navigationBarHidden(true)
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import SwiftUI
|
|||||||
struct SeriesMainByGenreView: View {
|
struct SeriesMainByGenreView: View {
|
||||||
|
|
||||||
@StateObject var viewModel = SeriesMainByGenreViewModel()
|
@StateObject var viewModel = SeriesMainByGenreViewModel()
|
||||||
|
@State private var isInitialized = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
@@ -41,17 +42,16 @@ struct SeriesMainByGenreView: View {
|
|||||||
) {
|
) {
|
||||||
ForEach(viewModel.seriesList.indices, id: \.self) { index in
|
ForEach(viewModel.seriesList.indices, id: \.self) { index in
|
||||||
let item = viewModel.seriesList[index]
|
let item = viewModel.seriesList[index]
|
||||||
NavigationLink {
|
SeriesMainItemView(item: item, width: width, height: width * 227 / 160)
|
||||||
SeriesDetailView(seriesId: item.seriesId)
|
.contentShape(Rectangle())
|
||||||
} label: {
|
.onAppear {
|
||||||
SeriesMainItemView(item: item, width: width, height: width * 227 / 160)
|
if index == viewModel.seriesList.count - 1 {
|
||||||
.contentShape(Rectangle())
|
viewModel.getSeriesListByGenre()
|
||||||
.onAppear {
|
|
||||||
if index == viewModel.seriesList.count - 1 {
|
|
||||||
viewModel.getSeriesListByGenre()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onTapGesture {
|
||||||
|
AppState.shared.setAppStep(step: .seriesDetail(seriesId: item.seriesId))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, horizontalPadding)
|
.padding(.horizontal, horizontalPadding)
|
||||||
@@ -73,7 +73,10 @@ struct SeriesMainByGenreView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.getGenreList()
|
if !isInitialized {
|
||||||
|
viewModel.getGenreList()
|
||||||
|
isInitialized = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if viewModel.isLoading {
|
if viewModel.isLoading {
|
||||||
|
|||||||
@@ -75,17 +75,14 @@ struct SeriesMainDayOfWeekView: View {
|
|||||||
) {
|
) {
|
||||||
ForEach(viewModel.seriesList.indices, id: \.self) { index in
|
ForEach(viewModel.seriesList.indices, id: \.self) { index in
|
||||||
let item = viewModel.seriesList[index]
|
let item = viewModel.seriesList[index]
|
||||||
NavigationLink {
|
SeriesMainItemView(item: item, width: width, height: width * 227 / 160)
|
||||||
SeriesDetailView(seriesId: item.seriesId)
|
.contentShape(Rectangle())
|
||||||
} label: {
|
.onAppear {
|
||||||
SeriesMainItemView(item: item, width: width, height: width * 227 / 160)
|
if index == viewModel.seriesList.count - 1 {
|
||||||
.contentShape(Rectangle())
|
viewModel.getDayOfWeekSeriesList(dayOfWeek: dayOfWeek)
|
||||||
.onAppear {
|
|
||||||
if index == viewModel.seriesList.count - 1 {
|
|
||||||
viewModel.getDayOfWeekSeriesList(dayOfWeek: dayOfWeek)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onTapGesture { AppState.shared.setAppStep(step: .seriesDetail(seriesId: item.seriesId)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, horizontalPadding)
|
.padding(.horizontal, horizontalPadding)
|
||||||
|
|||||||
@@ -22,11 +22,8 @@ struct SeriesMainHomeBannerView: View {
|
|||||||
ForEach(0..<bannerList.count, id: \.self) { index in
|
ForEach(0..<bannerList.count, id: \.self) { index in
|
||||||
let item = bannerList[index]
|
let item = bannerList[index]
|
||||||
|
|
||||||
NavigationLink {
|
SeriesMainHomeBannerImageView(url: item.imagePath, width: width, height: height)
|
||||||
SeriesDetailView(seriesId: item.seriesId)
|
.onTapGesture { AppState.shared.setAppStep(step: .seriesDetail(seriesId: item.seriesId)) }
|
||||||
} label: {
|
|
||||||
SeriesMainHomeBannerImageView(url: item.imagePath, width: width, height: height)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
|
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
|
||||||
|
|||||||
@@ -10,6 +10,7 @@ import SwiftUI
|
|||||||
struct SeriesMainHomeView: View {
|
struct SeriesMainHomeView: View {
|
||||||
|
|
||||||
@StateObject var viewModel = SeriesMainHomeViewModel()
|
@StateObject var viewModel = SeriesMainHomeViewModel()
|
||||||
|
@State private var isInitialized = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
ZStack {
|
||||||
@@ -43,11 +44,10 @@ struct SeriesMainHomeView: View {
|
|||||||
LazyHStack(spacing: 16) {
|
LazyHStack(spacing: 16) {
|
||||||
ForEach(0..<viewModel.completedSeriesList.count, id: \.self) {
|
ForEach(0..<viewModel.completedSeriesList.count, id: \.self) {
|
||||||
let item = viewModel.completedSeriesList[$0]
|
let item = viewModel.completedSeriesList[$0]
|
||||||
NavigationLink {
|
SeriesMainItemView(item: item)
|
||||||
SeriesDetailView(seriesId: item.seriesId)
|
.onTapGesture {
|
||||||
} label: {
|
AppState.shared.setAppStep(step: .seriesDetail(seriesId: item.seriesId))
|
||||||
SeriesMainItemView(item: item)
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, 24)
|
.padding(.horizontal, 24)
|
||||||
@@ -89,11 +89,10 @@ struct SeriesMainHomeView: View {
|
|||||||
) {
|
) {
|
||||||
ForEach(viewModel.recommendSeriesList.indices, id: \.self) {
|
ForEach(viewModel.recommendSeriesList.indices, id: \.self) {
|
||||||
let item = viewModel.recommendSeriesList[$0]
|
let item = viewModel.recommendSeriesList[$0]
|
||||||
NavigationLink {
|
SeriesMainItemView(item: item, width: width, height: width * 227 / 160)
|
||||||
SeriesDetailView(seriesId: item.seriesId)
|
.onTapGesture {
|
||||||
} label: {
|
AppState.shared.setAppStep(step: .seriesDetail(seriesId: item.seriesId))
|
||||||
SeriesMainItemView(item: item, width: width, height: width * 227 / 160)
|
}
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(.horizontal, horizontalPadding)
|
.padding(.horizontal, horizontalPadding)
|
||||||
@@ -117,7 +116,10 @@ struct SeriesMainHomeView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.fetchHome()
|
if !isInitialized {
|
||||||
|
viewModel.fetchHome()
|
||||||
|
isInitialized = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if viewModel.isLoading {
|
if viewModel.isLoading {
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ struct SeriesMainView: View {
|
|||||||
@State private var selectedTab: InnerTab = .home
|
@State private var selectedTab: InnerTab = .home
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
Group {
|
||||||
BaseView {
|
BaseView {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
DetailNavigationBar(title: "시리즈 전체보기")
|
DetailNavigationBar(title: "시리즈 전체보기")
|
||||||
|
|||||||
@@ -9,7 +9,8 @@ import SwiftUI
|
|||||||
|
|
||||||
struct SeriesListAllView: View {
|
struct SeriesListAllView: View {
|
||||||
|
|
||||||
@ObservedObject var viewModel = SeriesListAllViewModel()
|
@StateObject var viewModel = SeriesListAllViewModel()
|
||||||
|
@State private var isInitialized = false
|
||||||
|
|
||||||
var creatorId: Int? = nil
|
var creatorId: Int? = nil
|
||||||
var creatorNickname: String? = nil
|
var creatorNickname: String? = nil
|
||||||
@@ -18,7 +19,7 @@ struct SeriesListAllView: View {
|
|||||||
var isCompleted = false
|
var isCompleted = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
Group {
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
BaseView(isLoading: $viewModel.isLoading) {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
if isCompleted {
|
if isCompleted {
|
||||||
@@ -48,17 +49,16 @@ struct SeriesListAllView: View {
|
|||||||
) {
|
) {
|
||||||
ForEach(0..<viewModel.seriesList.count, id: \.self) { index in
|
ForEach(0..<viewModel.seriesList.count, id: \.self) { index in
|
||||||
let item = viewModel.seriesList[index]
|
let item = viewModel.seriesList[index]
|
||||||
NavigationLink {
|
SeriesMainItemView(item: item, width: width, height: width * 227 / 160)
|
||||||
SeriesDetailView(seriesId: item.seriesId)
|
.contentShape(Rectangle())
|
||||||
} label: {
|
.onAppear {
|
||||||
SeriesMainItemView(item: item, width: width, height: width * 227 / 160)
|
if index == viewModel.seriesList.count - 1 {
|
||||||
.contentShape(Rectangle())
|
viewModel.getSeriesList()
|
||||||
.onAppear {
|
|
||||||
if index == viewModel.seriesList.count - 1 {
|
|
||||||
viewModel.getSeriesList()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onTapGesture {
|
||||||
|
AppState.shared.setAppStep(step: .seriesDetail(seriesId: item.seriesId))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.padding(horizontalPadding)
|
.padding(horizontalPadding)
|
||||||
@@ -67,10 +67,24 @@ struct SeriesListAllView: View {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.creatorId = creatorId
|
let hasFilterChanged =
|
||||||
viewModel.isOriginal = isOriginal
|
viewModel.creatorId != creatorId ||
|
||||||
viewModel.isCompleted = isCompleted
|
viewModel.isOriginal != isOriginal ||
|
||||||
viewModel.getSeriesList()
|
viewModel.isCompleted != isCompleted
|
||||||
|
|
||||||
|
if !isInitialized || hasFilterChanged {
|
||||||
|
if hasFilterChanged {
|
||||||
|
viewModel.page = 1
|
||||||
|
viewModel.isLast = false
|
||||||
|
viewModel.seriesList.removeAll()
|
||||||
|
}
|
||||||
|
|
||||||
|
viewModel.creatorId = creatorId
|
||||||
|
viewModel.isOriginal = isOriginal
|
||||||
|
viewModel.isCompleted = isCompleted
|
||||||
|
viewModel.getSeriesList()
|
||||||
|
isInitialized = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,303 +15,327 @@ struct ContentView: View {
|
|||||||
@State private var message = ""
|
@State private var message = ""
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
ZStack {
|
NavigationStack(path: $appState.navigationPath) {
|
||||||
Color.black.ignoresSafeArea()
|
ZStack {
|
||||||
|
Color.black.ignoresSafeArea()
|
||||||
|
|
||||||
if appState.isRestartApp {
|
if appState.isRestartApp {
|
||||||
EmptyView()
|
EmptyView()
|
||||||
} else {
|
} else {
|
||||||
HomeView()
|
HomeView()
|
||||||
|
}
|
||||||
|
|
||||||
|
if case .splash = appState.rootStep {
|
||||||
|
AppStepLayerView(step: .splash, canPgPaymentViewModel: canPgPaymentViewModel)
|
||||||
|
.navigationBarBackButtonHidden(true)
|
||||||
|
}
|
||||||
|
|
||||||
|
if isShowDialog {
|
||||||
|
SodaDialog(
|
||||||
|
title: I18n.Common.pointGrantTitle,
|
||||||
|
desc: message,
|
||||||
|
confirmButtonTitle: I18n.Common.confirm
|
||||||
|
) {
|
||||||
|
isShowDialog = false
|
||||||
|
message = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
.onReceive(NotificationCenter.default.publisher(for: .pointGranted)) {
|
||||||
switch appState.appStep {
|
if let msg = $0.object as? String {
|
||||||
case .splash:
|
self.message = msg
|
||||||
SplashView()
|
self.isShowDialog = true
|
||||||
|
}
|
||||||
case .login:
|
|
||||||
LoginView()
|
|
||||||
|
|
||||||
case .signUp:
|
|
||||||
SignUpView()
|
|
||||||
|
|
||||||
case .findPassword:
|
|
||||||
FindPasswordView()
|
|
||||||
|
|
||||||
case .textMessageDetail(let messageItem, let messageBox, let refresh):
|
|
||||||
TextMessageDetailView(messageItem: messageItem, messageBox: messageBox, refresh: refresh)
|
|
||||||
|
|
||||||
case .writeTextMessage(let userId, let nickname):
|
|
||||||
TextMessageWriteView(replySenderId: userId, replySenderNickname: nickname)
|
|
||||||
|
|
||||||
case .writeVoiceMessage(let userId, let nickname, let onRefresh):
|
|
||||||
VoiceMessageWriteView(replySenderId: userId, replySenderNickname: nickname, onRefresh: onRefresh)
|
|
||||||
|
|
||||||
case .settings:
|
|
||||||
SettingsView()
|
|
||||||
|
|
||||||
case .languageSettings:
|
|
||||||
LanguageSettingsView()
|
|
||||||
|
|
||||||
case .notices:
|
|
||||||
NoticeListView()
|
|
||||||
|
|
||||||
case .noticeDetail(let notice):
|
|
||||||
NoticeDetailView(notice: notice)
|
|
||||||
|
|
||||||
case .events:
|
|
||||||
EventListView()
|
|
||||||
|
|
||||||
case .eventDetail(let event):
|
|
||||||
EventDetailView(event: event)
|
|
||||||
|
|
||||||
case .terms:
|
|
||||||
TermsView(isPrivacyPolicy: false)
|
|
||||||
|
|
||||||
case .privacy:
|
|
||||||
TermsView(isPrivacyPolicy: true)
|
|
||||||
|
|
||||||
case .notificationSettings:
|
|
||||||
NotificationSettingsView()
|
|
||||||
|
|
||||||
case .contentViewSettings:
|
|
||||||
ContentSettingsView()
|
|
||||||
|
|
||||||
case .signOut:
|
|
||||||
SignOutView()
|
|
||||||
|
|
||||||
case .canStatus(let refresh):
|
|
||||||
CanStatusView(refresh: refresh)
|
|
||||||
|
|
||||||
case .canCharge(let refresh, let afterCompletionToGoBack):
|
|
||||||
CanChargeView(refresh: refresh, afterCompletionToGoBack: afterCompletionToGoBack)
|
|
||||||
|
|
||||||
case .canPayment(let canProduct, let refresh, let afterCompletionToGoBack):
|
|
||||||
CanPaymentView(canProduct: canProduct, refresh: refresh, afterCompletionToGoBack: afterCompletionToGoBack)
|
|
||||||
|
|
||||||
case .canPgPayment(let canResponse, let refresh, let afterCompletionToGoBack):
|
|
||||||
CanPgPaymentView(canResponse: canResponse, refresh: refresh, afterCompletionToGoBack: afterCompletionToGoBack)
|
|
||||||
.environmentObject(canPgPaymentViewModel)
|
|
||||||
|
|
||||||
case .liveReservation:
|
|
||||||
LiveReservationStatusView()
|
|
||||||
|
|
||||||
case .liveReservationCancel(let reservationId):
|
|
||||||
LiveReservationCancelView(reservationId: reservationId)
|
|
||||||
|
|
||||||
case .serviceCenter:
|
|
||||||
ServiceCenterView()
|
|
||||||
|
|
||||||
case .createContent:
|
|
||||||
ContentCreateView()
|
|
||||||
|
|
||||||
case .liveReservationComplete(let response):
|
|
||||||
LiveReservationCompleteView(reservationCompleteData: response)
|
|
||||||
|
|
||||||
case .creatorDetail(let userId):
|
|
||||||
UserProfileView(userId: userId)
|
|
||||||
|
|
||||||
case .followerList(let userId):
|
|
||||||
FollowerListView(userId: userId)
|
|
||||||
|
|
||||||
case .modifyContent(let contentId):
|
|
||||||
ContentModifyView(contentId: contentId)
|
|
||||||
|
|
||||||
case .contentListAll(let userId):
|
|
||||||
ContentListView(userId: userId)
|
|
||||||
|
|
||||||
case .contentDetail(let contentId):
|
|
||||||
ContentDetailView(contentId: contentId)
|
|
||||||
|
|
||||||
case .createLive(let timeSettingMode, let onSuccess):
|
|
||||||
LiveRoomCreateView(
|
|
||||||
timeSettingMode: timeSettingMode,
|
|
||||||
onSuccess: onSuccess
|
|
||||||
)
|
|
||||||
|
|
||||||
case .liveNowAll(let onClickParticipant):
|
|
||||||
LiveNowAllView(onClickParticipant: onClickParticipant)
|
|
||||||
|
|
||||||
case .liveReservationAll(let onClickReservation, let onClickStart, let onClickCancel, let onTapCreateLive):
|
|
||||||
LiveReservationAllView(
|
|
||||||
onClickReservation: onClickReservation,
|
|
||||||
onClickStart: onClickStart,
|
|
||||||
onClickCancel: onClickCancel,
|
|
||||||
onTapCreateLive: onTapCreateLive
|
|
||||||
)
|
|
||||||
|
|
||||||
case .modifyLive(let room):
|
|
||||||
LiveRoomEditView(room: room)
|
|
||||||
|
|
||||||
case .liveDetail(let roomId, let onClickParticipant, let onClickReservation, let onClickStart, let onClickCancel):
|
|
||||||
LiveDetailView(
|
|
||||||
roomId: roomId,
|
|
||||||
onClickParticipant: onClickParticipant,
|
|
||||||
onClickReservation: onClickReservation,
|
|
||||||
onClickStart: onClickStart,
|
|
||||||
onClickCancel: onClickCancel
|
|
||||||
)
|
|
||||||
|
|
||||||
case .modifyPassword:
|
|
||||||
ModifyPasswordView()
|
|
||||||
|
|
||||||
case .changeNickname:
|
|
||||||
NicknameUpdateView()
|
|
||||||
|
|
||||||
case .profileUpdate(let refresh):
|
|
||||||
ProfileUpdateView(refresh: refresh)
|
|
||||||
|
|
||||||
case .followingList:
|
|
||||||
FollowCreatorView()
|
|
||||||
|
|
||||||
case .orderListAll:
|
|
||||||
OrderListAllView()
|
|
||||||
|
|
||||||
case .userProfileDonationAll(let userId):
|
|
||||||
UserProfileDonationAllView(userId: userId)
|
|
||||||
|
|
||||||
case .channelDonationAll(let creatorId):
|
|
||||||
ChannelDonationAllView(creatorId: creatorId)
|
|
||||||
|
|
||||||
case .userProfileFanTalkAll(let userId):
|
|
||||||
UserProfileFanTalkAllView(userId: userId)
|
|
||||||
|
|
||||||
case .newContentAll(let isFree):
|
|
||||||
ContentNewAllView(isFree: isFree)
|
|
||||||
|
|
||||||
case .curationAll(let title, let curationId):
|
|
||||||
ContentCurationView(title: title, curationId: curationId)
|
|
||||||
|
|
||||||
case .contentRankingAll:
|
|
||||||
ContentRankingAllView()
|
|
||||||
|
|
||||||
case .creatorCommunityAll(let creatorId):
|
|
||||||
CreatorCommunityAllView(creatorId: creatorId)
|
|
||||||
|
|
||||||
case .creatorCommunityWrite(let onSuccess):
|
|
||||||
CreatorCommunityWriteView(onSuccess: onSuccess)
|
|
||||||
|
|
||||||
case .creatorCommunityModify(let postId, let onSuccess):
|
|
||||||
CreatorCommunityModifyView(postId: postId, onSuccess: onSuccess)
|
|
||||||
|
|
||||||
case .canCoupon(let refresh):
|
|
||||||
CanCouponView(refresh: refresh)
|
|
||||||
|
|
||||||
case .contentAllByTheme(let themeId):
|
|
||||||
ContentAllByThemeView(themeId: themeId)
|
|
||||||
|
|
||||||
case .seriesAll(let creatorId, let creatorNickname, let isOriginal, let isCompleted):
|
|
||||||
SeriesListAllView(creatorId: creatorId, creatorNickname: creatorNickname, isOriginal: isOriginal, isCompleted: isCompleted)
|
|
||||||
|
|
||||||
case .seriesDetail(let seriesId):
|
|
||||||
SeriesDetailView(seriesId: seriesId)
|
|
||||||
|
|
||||||
case .seriesContentAll(let seriesId, let seriesTitle):
|
|
||||||
SeriesContentAllView(seriesId: seriesId, seriesTitle: seriesTitle)
|
|
||||||
|
|
||||||
case .tempCanPayment(let orderType, let contentId, let title, let can):
|
|
||||||
CanPaymentTempView(orderType: orderType, contentId: contentId, title: title, can: can)
|
|
||||||
|
|
||||||
case .blockList:
|
|
||||||
BlockMemberListView()
|
|
||||||
|
|
||||||
case .myBox(let currentTab):
|
|
||||||
ContentBoxView(initCurrentTab: currentTab)
|
|
||||||
|
|
||||||
case .auditionDetail(let auditionId):
|
|
||||||
AuditionDetailView(auditionId: auditionId)
|
|
||||||
|
|
||||||
case .auditionRoleDetail(let roleId, let auditionTitle):
|
|
||||||
AuditionRoleDetailView(
|
|
||||||
roleId: roleId,
|
|
||||||
auditionTitle: auditionTitle
|
|
||||||
)
|
|
||||||
|
|
||||||
case .search:
|
|
||||||
SearchView()
|
|
||||||
|
|
||||||
case .contentMain(let startTab):
|
|
||||||
ContentMainViewV2(selectedTab: startTab)
|
|
||||||
|
|
||||||
case .completedSeriesAll:
|
|
||||||
CompletedSeriesView()
|
|
||||||
|
|
||||||
case .newAlarmContentAll:
|
|
||||||
ContentMainAlarmAllView()
|
|
||||||
|
|
||||||
case .newAsmrContentAll:
|
|
||||||
ContentMainAsmrAllView()
|
|
||||||
|
|
||||||
case .newReplayContentAll:
|
|
||||||
ContentMainReplayAllView()
|
|
||||||
|
|
||||||
case .introduceCreatorAll:
|
|
||||||
ContentMainIntroduceCreatorAllView()
|
|
||||||
|
|
||||||
case .message:
|
|
||||||
MessageView()
|
|
||||||
|
|
||||||
case .pointStatus(let refresh):
|
|
||||||
PointStatusView(refresh: refresh)
|
|
||||||
|
|
||||||
case .audition:
|
|
||||||
AuditionView()
|
|
||||||
|
|
||||||
case .characterDetail(let characterId):
|
|
||||||
CharacterDetailView(characterId: characterId)
|
|
||||||
|
|
||||||
case .chatRoom(let id):
|
|
||||||
ChatRoomView(roomId: id)
|
|
||||||
|
|
||||||
case .newCharacterAll:
|
|
||||||
NewCharacterListView()
|
|
||||||
|
|
||||||
case .originalWorkDetail(let originalId):
|
|
||||||
OriginalWorkDetailView(originalId: originalId)
|
|
||||||
|
|
||||||
case .contentAll(let isFree, let isPointOnly):
|
|
||||||
ContentAllView(isFree: isFree, isPointAvailableOnly: isPointOnly)
|
|
||||||
|
|
||||||
case .seriesMain:
|
|
||||||
SeriesMainView()
|
|
||||||
|
|
||||||
default:
|
|
||||||
EmptyView()
|
|
||||||
.frame(width: 0, height: 0, alignment: .topLeading)
|
|
||||||
}
|
}
|
||||||
|
.popup(isPresented: $appState.isShowErrorPopup, type: .toast, position: .top, autohideIn: 1) {
|
||||||
if isShowDialog {
|
GeometryReader { geo in
|
||||||
SodaDialog(
|
HStack {
|
||||||
title: I18n.Common.pointGrantTitle,
|
Spacer()
|
||||||
desc: message,
|
Text(appState.errorMessage)
|
||||||
confirmButtonTitle: I18n.Common.confirm
|
.padding(.vertical, 13.3)
|
||||||
) {
|
.frame(width: geo.size.width - 66.7, alignment: .center)
|
||||||
isShowDialog = false
|
.appFont(size: 12, weight: .medium)
|
||||||
message = ""
|
.background(Color.button)
|
||||||
|
.foregroundColor(Color.white)
|
||||||
|
.multilineTextAlignment(.center)
|
||||||
|
.cornerRadius(20)
|
||||||
|
.padding(.top, 66.7)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.navigationDestination(for: AppRoute.self) { route in
|
||||||
|
if let step = appState.appStep(for: route) {
|
||||||
|
AppStepLayerView(step: step, canPgPaymentViewModel: canPgPaymentViewModel)
|
||||||
|
.navigationBarBackButtonHidden(true)
|
||||||
|
} else {
|
||||||
|
EmptyView()
|
||||||
|
.frame(width: 0, height: 0, alignment: .topLeading)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.onReceive(NotificationCenter.default.publisher(for: .pointGranted)) {
|
}
|
||||||
if let msg = $0.object as? String {
|
}
|
||||||
self.message = msg
|
|
||||||
self.isShowDialog = true
|
struct AppStepLayerView: View {
|
||||||
}
|
let step: AppStep
|
||||||
}
|
@ObservedObject var canPgPaymentViewModel: CanPgPaymentViewModel
|
||||||
.popup(isPresented: $appState.isShowErrorPopup, type: .toast, position: .top, autohideIn: 1) {
|
|
||||||
GeometryReader { geo in
|
@ViewBuilder
|
||||||
HStack {
|
var body: some View {
|
||||||
Spacer()
|
switch step {
|
||||||
Text(appState.errorMessage)
|
case .splash:
|
||||||
.padding(.vertical, 13.3)
|
SplashView()
|
||||||
.frame(width: geo.size.width - 66.7, alignment: .center)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
case .login:
|
||||||
.background(Color.button)
|
LoginView()
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.multilineTextAlignment(.center)
|
case .signUp:
|
||||||
.cornerRadius(20)
|
SignUpView()
|
||||||
.padding(.top, 66.7)
|
|
||||||
Spacer()
|
case .findPassword:
|
||||||
}
|
FindPasswordView()
|
||||||
}
|
|
||||||
|
case .textMessageDetail(let messageItem, let messageBox, let refresh):
|
||||||
|
TextMessageDetailView(messageItem: messageItem, messageBox: messageBox, refresh: refresh)
|
||||||
|
|
||||||
|
case .writeTextMessage(let userId, let nickname):
|
||||||
|
TextMessageWriteView(replySenderId: userId, replySenderNickname: nickname)
|
||||||
|
|
||||||
|
case .writeVoiceMessage(let userId, let nickname, let onRefresh):
|
||||||
|
VoiceMessageWriteView(replySenderId: userId, replySenderNickname: nickname, onRefresh: onRefresh)
|
||||||
|
|
||||||
|
case .settings:
|
||||||
|
SettingsView()
|
||||||
|
|
||||||
|
case .languageSettings:
|
||||||
|
LanguageSettingsView()
|
||||||
|
|
||||||
|
case .notices:
|
||||||
|
NoticeListView()
|
||||||
|
|
||||||
|
case .noticeDetail(let notice):
|
||||||
|
NoticeDetailView(notice: notice)
|
||||||
|
|
||||||
|
case .events:
|
||||||
|
EventListView()
|
||||||
|
|
||||||
|
case .eventDetail(let event):
|
||||||
|
EventDetailView(event: event)
|
||||||
|
|
||||||
|
case .terms:
|
||||||
|
TermsView(isPrivacyPolicy: false)
|
||||||
|
|
||||||
|
case .privacy:
|
||||||
|
TermsView(isPrivacyPolicy: true)
|
||||||
|
|
||||||
|
case .notificationSettings:
|
||||||
|
NotificationSettingsView()
|
||||||
|
|
||||||
|
case .contentViewSettings:
|
||||||
|
ContentSettingsView()
|
||||||
|
|
||||||
|
case .signOut:
|
||||||
|
SignOutView()
|
||||||
|
|
||||||
|
case .canStatus(let refresh):
|
||||||
|
CanStatusView(refresh: refresh)
|
||||||
|
|
||||||
|
case .canCharge(let refresh, let afterCompletionToGoBack):
|
||||||
|
CanChargeView(refresh: refresh, afterCompletionToGoBack: afterCompletionToGoBack)
|
||||||
|
|
||||||
|
case .canPayment(let canProduct, let refresh, let afterCompletionToGoBack):
|
||||||
|
CanPaymentView(canProduct: canProduct, refresh: refresh, afterCompletionToGoBack: afterCompletionToGoBack)
|
||||||
|
|
||||||
|
case .canPgPayment(let canResponse, let refresh, let afterCompletionToGoBack):
|
||||||
|
CanPgPaymentView(canResponse: canResponse, refresh: refresh, afterCompletionToGoBack: afterCompletionToGoBack)
|
||||||
|
.environmentObject(canPgPaymentViewModel)
|
||||||
|
|
||||||
|
case .liveReservation:
|
||||||
|
LiveReservationStatusView()
|
||||||
|
|
||||||
|
case .liveReservationCancel(let reservationId):
|
||||||
|
LiveReservationCancelView(reservationId: reservationId)
|
||||||
|
|
||||||
|
case .serviceCenter:
|
||||||
|
ServiceCenterView()
|
||||||
|
|
||||||
|
case .createContent:
|
||||||
|
ContentCreateView()
|
||||||
|
|
||||||
|
case .liveReservationComplete(let response):
|
||||||
|
LiveReservationCompleteView(reservationCompleteData: response)
|
||||||
|
|
||||||
|
case .creatorDetail(let userId):
|
||||||
|
UserProfileView(userId: userId)
|
||||||
|
|
||||||
|
case .followerList(let userId):
|
||||||
|
FollowerListView(userId: userId)
|
||||||
|
|
||||||
|
case .modifyContent(let contentId):
|
||||||
|
ContentModifyView(contentId: contentId)
|
||||||
|
|
||||||
|
case .contentListAll(let userId):
|
||||||
|
ContentListView(userId: userId)
|
||||||
|
|
||||||
|
case .contentDetail(let contentId):
|
||||||
|
ContentDetailView(contentId: contentId)
|
||||||
|
|
||||||
|
case .createLive(let timeSettingMode, let onSuccess):
|
||||||
|
LiveRoomCreateView(
|
||||||
|
timeSettingMode: timeSettingMode,
|
||||||
|
onSuccess: onSuccess
|
||||||
|
)
|
||||||
|
|
||||||
|
case .liveNowAll(let onClickParticipant):
|
||||||
|
LiveNowAllView(onClickParticipant: onClickParticipant)
|
||||||
|
|
||||||
|
case .liveReservationAll(let onClickReservation, let onClickStart, let onClickCancel, let onTapCreateLive):
|
||||||
|
LiveReservationAllView(
|
||||||
|
onClickReservation: onClickReservation,
|
||||||
|
onClickStart: onClickStart,
|
||||||
|
onClickCancel: onClickCancel,
|
||||||
|
onTapCreateLive: onTapCreateLive
|
||||||
|
)
|
||||||
|
|
||||||
|
case .modifyLive(let room):
|
||||||
|
LiveRoomEditView(room: room)
|
||||||
|
|
||||||
|
case .liveDetail(let roomId, let onClickParticipant, let onClickReservation, let onClickStart, let onClickCancel):
|
||||||
|
LiveDetailView(
|
||||||
|
roomId: roomId,
|
||||||
|
onClickParticipant: onClickParticipant,
|
||||||
|
onClickReservation: onClickReservation,
|
||||||
|
onClickStart: onClickStart,
|
||||||
|
onClickCancel: onClickCancel
|
||||||
|
)
|
||||||
|
|
||||||
|
case .modifyPassword:
|
||||||
|
ModifyPasswordView()
|
||||||
|
|
||||||
|
case .changeNickname:
|
||||||
|
NicknameUpdateView()
|
||||||
|
|
||||||
|
case .profileUpdate(let refresh):
|
||||||
|
ProfileUpdateView(refresh: refresh)
|
||||||
|
|
||||||
|
case .followingList:
|
||||||
|
FollowCreatorView()
|
||||||
|
|
||||||
|
case .orderListAll:
|
||||||
|
OrderListAllView()
|
||||||
|
|
||||||
|
case .userProfileDonationAll(let userId):
|
||||||
|
UserProfileDonationAllView(userId: userId)
|
||||||
|
|
||||||
|
case .channelDonationAll(let creatorId):
|
||||||
|
ChannelDonationAllView(creatorId: creatorId)
|
||||||
|
|
||||||
|
case .userProfileFanTalkAll(let userId):
|
||||||
|
UserProfileFanTalkAllView(userId: userId)
|
||||||
|
|
||||||
|
case .newContentAll(let isFree):
|
||||||
|
ContentNewAllView(isFree: isFree)
|
||||||
|
|
||||||
|
case .curationAll(let title, let curationId):
|
||||||
|
ContentCurationView(title: title, curationId: curationId)
|
||||||
|
|
||||||
|
case .contentRankingAll:
|
||||||
|
ContentRankingAllView()
|
||||||
|
|
||||||
|
case .creatorCommunityAll(let creatorId):
|
||||||
|
CreatorCommunityAllView(creatorId: creatorId)
|
||||||
|
|
||||||
|
case .creatorCommunityWrite(let onSuccess):
|
||||||
|
CreatorCommunityWriteView(onSuccess: onSuccess)
|
||||||
|
|
||||||
|
case .creatorCommunityModify(let postId, let onSuccess):
|
||||||
|
CreatorCommunityModifyView(postId: postId, onSuccess: onSuccess)
|
||||||
|
|
||||||
|
case .canCoupon(let refresh):
|
||||||
|
CanCouponView(refresh: refresh)
|
||||||
|
|
||||||
|
case .contentAllByTheme(let themeId):
|
||||||
|
ContentAllByThemeView(themeId: themeId)
|
||||||
|
|
||||||
|
case .seriesAll(let creatorId, let creatorNickname, let isOriginal, let isCompleted):
|
||||||
|
SeriesListAllView(creatorId: creatorId, creatorNickname: creatorNickname, isOriginal: isOriginal, isCompleted: isCompleted)
|
||||||
|
|
||||||
|
case .seriesDetail(let seriesId):
|
||||||
|
SeriesDetailView(seriesId: seriesId)
|
||||||
|
|
||||||
|
case .seriesContentAll(let seriesId, let seriesTitle):
|
||||||
|
SeriesContentAllView(seriesId: seriesId, seriesTitle: seriesTitle)
|
||||||
|
|
||||||
|
case .tempCanPayment(let orderType, let contentId, let title, let can):
|
||||||
|
CanPaymentTempView(orderType: orderType, contentId: contentId, title: title, can: can)
|
||||||
|
|
||||||
|
case .blockList:
|
||||||
|
BlockMemberListView()
|
||||||
|
|
||||||
|
case .myBox(let currentTab):
|
||||||
|
ContentBoxView(initCurrentTab: currentTab)
|
||||||
|
|
||||||
|
case .auditionDetail(let auditionId):
|
||||||
|
AuditionDetailView(auditionId: auditionId)
|
||||||
|
|
||||||
|
case .auditionRoleDetail(let roleId, let auditionTitle):
|
||||||
|
AuditionRoleDetailView(
|
||||||
|
roleId: roleId,
|
||||||
|
auditionTitle: auditionTitle
|
||||||
|
)
|
||||||
|
|
||||||
|
case .search:
|
||||||
|
SearchView()
|
||||||
|
|
||||||
|
case .contentMain(let startTab):
|
||||||
|
ContentMainViewV2(selectedTab: startTab)
|
||||||
|
|
||||||
|
case .completedSeriesAll:
|
||||||
|
CompletedSeriesView()
|
||||||
|
|
||||||
|
case .newAlarmContentAll:
|
||||||
|
ContentMainAlarmAllView()
|
||||||
|
|
||||||
|
case .newAsmrContentAll:
|
||||||
|
ContentMainAsmrAllView()
|
||||||
|
|
||||||
|
case .newReplayContentAll:
|
||||||
|
ContentMainReplayAllView()
|
||||||
|
|
||||||
|
case .introduceCreatorAll:
|
||||||
|
ContentMainIntroduceCreatorAllView()
|
||||||
|
|
||||||
|
case .message:
|
||||||
|
MessageView()
|
||||||
|
|
||||||
|
case .pointStatus(let refresh):
|
||||||
|
PointStatusView(refresh: refresh)
|
||||||
|
|
||||||
|
case .audition:
|
||||||
|
AuditionView()
|
||||||
|
|
||||||
|
case .characterDetail(let characterId):
|
||||||
|
CharacterDetailView(characterId: characterId)
|
||||||
|
|
||||||
|
case .chatRoom(let id):
|
||||||
|
ChatRoomView(roomId: id)
|
||||||
|
|
||||||
|
case .newCharacterAll:
|
||||||
|
NewCharacterListView()
|
||||||
|
|
||||||
|
case .originalWorkDetail(let originalId):
|
||||||
|
OriginalWorkDetailView(originalId: originalId)
|
||||||
|
|
||||||
|
case .contentAll(let isFree, let isPointOnly):
|
||||||
|
ContentAllView(isFree: isFree, isPointAvailableOnly: isPointOnly)
|
||||||
|
|
||||||
|
case .seriesMain:
|
||||||
|
SeriesMainView()
|
||||||
|
|
||||||
|
case .main:
|
||||||
|
EmptyView()
|
||||||
|
.frame(width: 0, height: 0, alignment: .topLeading)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ struct CreatorCommunityCommentListView: View {
|
|||||||
@StateObject var viewModel = CreatorCommunityCommentListViewModel()
|
@StateObject var viewModel = CreatorCommunityCommentListViewModel()
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
Group {
|
||||||
ZStack {
|
ZStack {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
|
|||||||
@@ -56,12 +56,15 @@ struct CreatorCommunityAllView: View {
|
|||||||
.sheet(
|
.sheet(
|
||||||
isPresented: $viewModel.isShowCommentListView,
|
isPresented: $viewModel.isShowCommentListView,
|
||||||
content: {
|
content: {
|
||||||
CreatorCommunityCommentListView(
|
NavigationStack {
|
||||||
isPresented: $viewModel.isShowCommentListView,
|
CreatorCommunityCommentListView(
|
||||||
creatorId: creatorId,
|
isPresented: $viewModel.isShowCommentListView,
|
||||||
postId: viewModel.postId,
|
creatorId: creatorId,
|
||||||
isShowSecret: viewModel.isShowSecret
|
postId: viewModel.postId,
|
||||||
)
|
isShowSecret: viewModel.isShowSecret
|
||||||
|
)
|
||||||
|
}
|
||||||
|
.toolbar(.hidden, for: .navigationBar)
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ struct UserProfileView: View {
|
|||||||
@State private var isShowChannelDonationDialog: Bool = false
|
@State private var isShowChannelDonationDialog: Bool = false
|
||||||
@State private var didTriggerAutoBackOnLoadFailure: Bool = false
|
@State private var didTriggerAutoBackOnLoadFailure: Bool = false
|
||||||
@State private var isViewVisible: Bool = false
|
@State private var isViewVisible: Bool = false
|
||||||
|
@State private var loadedUserId: Int? = nil
|
||||||
|
|
||||||
@State private var maxCommunityPostHeight: CGFloat? = nil
|
@State private var maxCommunityPostHeight: CGFloat? = nil
|
||||||
|
|
||||||
@@ -639,7 +640,12 @@ struct UserProfileView: View {
|
|||||||
.onAppear {
|
.onAppear {
|
||||||
isViewVisible = true
|
isViewVisible = true
|
||||||
didTriggerAutoBackOnLoadFailure = false
|
didTriggerAutoBackOnLoadFailure = false
|
||||||
viewModel.getCreatorProfile(userId: userId)
|
|
||||||
|
if loadedUserId != userId || viewModel.creatorProfile == nil {
|
||||||
|
loadedUserId = userId
|
||||||
|
viewModel.getCreatorProfile(userId: userId)
|
||||||
|
}
|
||||||
|
|
||||||
AppState.shared.pushChannelId = 0
|
AppState.shared.pushChannelId = 0
|
||||||
}
|
}
|
||||||
.onDisappear {
|
.onDisappear {
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import SwiftUI
|
|||||||
|
|
||||||
struct OrderListAllInnerView: View {
|
struct OrderListAllInnerView: View {
|
||||||
@StateObject var viewModel = OrderListAllViewModel()
|
@StateObject var viewModel = OrderListAllViewModel()
|
||||||
|
@State private var isInitialized = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
BaseView(isLoading: $viewModel.isLoading) {
|
||||||
@@ -38,19 +39,16 @@ struct OrderListAllInnerView: View {
|
|||||||
|
|
||||||
ForEach(0..<viewModel.orderList.count, id: \.self) { index in
|
ForEach(0..<viewModel.orderList.count, id: \.self) { index in
|
||||||
let item = viewModel.orderList[index]
|
let item = viewModel.orderList[index]
|
||||||
NavigationLink {
|
OrderListItemView(item: item)
|
||||||
ContentDetailView(contentId: item.contentId)
|
.contentShape(Rectangle())
|
||||||
} label: {
|
.padding(.horizontal, 13.3)
|
||||||
OrderListItemView(item: item)
|
.padding(.top, 6.7)
|
||||||
.contentShape(Rectangle())
|
.onAppear {
|
||||||
.padding(.horizontal, 13.3)
|
if index == viewModel.orderList.count - 1 {
|
||||||
.padding(.top, 6.7)
|
viewModel.getOrderList()
|
||||||
.onAppear {
|
|
||||||
if index == viewModel.orderList.count - 1 {
|
|
||||||
viewModel.getOrderList()
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.onTapGesture { AppState.shared.setAppStep(step: .contentDetail(contentId: item.contentId)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -58,7 +56,10 @@ struct OrderListAllInnerView: View {
|
|||||||
.padding(.top, 13.3)
|
.padding(.top, 13.3)
|
||||||
}
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.getOrderList()
|
if !isInitialized {
|
||||||
|
viewModel.getOrderList()
|
||||||
|
isInitialized = true
|
||||||
|
}
|
||||||
}
|
}
|
||||||
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
|
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
|
||||||
HStack {
|
HStack {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import SwiftUI
|
|||||||
struct OrderListAllView: View {
|
struct OrderListAllView: View {
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
Group {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
Button {
|
Button {
|
||||||
|
|||||||
@@ -96,26 +96,23 @@ struct SearchCreatorItemView: View {
|
|||||||
let item: SearchResponseItem
|
let item: SearchResponseItem
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationLink {
|
HStack(spacing: 13.3) {
|
||||||
UserProfileView(userId: item.id)
|
KFImage(URL(string: item.imageUrl))
|
||||||
} label: {
|
.cancelOnDisappear(true)
|
||||||
HStack(spacing: 13.3) {
|
.downsampling(size: CGSize(width: 60, height: 60))
|
||||||
KFImage(URL(string: item.imageUrl))
|
.resizable()
|
||||||
.cancelOnDisappear(true)
|
.frame(width: 60, height: 60)
|
||||||
.downsampling(size: CGSize(width: 60, height: 60))
|
.clipShape(Circle())
|
||||||
.resizable()
|
|
||||||
.frame(width: 60, height: 60)
|
|
||||||
.clipShape(Circle())
|
|
||||||
|
|
||||||
Text(item.nickname)
|
Text(item.nickname)
|
||||||
.appFont(size: 13.3, weight: .medium)
|
.appFont(size: 13.3, weight: .medium)
|
||||||
.foregroundColor(Color.grayee)
|
.foregroundColor(Color.grayee)
|
||||||
|
|
||||||
Spacer()
|
Spacer()
|
||||||
}
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.contentShape(Rectangle())
|
|
||||||
}
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.onTapGesture { AppState.shared.setAppStep(step: .creatorDetail(userId: item.id)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,34 +120,31 @@ struct SearchContentItemView: View {
|
|||||||
let item: SearchResponseItem
|
let item: SearchResponseItem
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationLink {
|
HStack(spacing: 13.3) {
|
||||||
ContentDetailView(contentId: item.id)
|
KFImage(URL(string: item.imageUrl))
|
||||||
} label: {
|
.cancelOnDisappear(true)
|
||||||
HStack(spacing: 13.3) {
|
.downsampling(size: CGSize(width: 60, height: 60))
|
||||||
KFImage(URL(string: item.imageUrl))
|
.resizable()
|
||||||
.cancelOnDisappear(true)
|
.frame(width: 60, height: 60)
|
||||||
.downsampling(size: CGSize(width: 60, height: 60))
|
.cornerRadius(5.3)
|
||||||
.resizable()
|
|
||||||
.frame(width: 60, height: 60)
|
|
||||||
.cornerRadius(5.3)
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 6.7) {
|
VStack(alignment: .leading, spacing: 6.7) {
|
||||||
Text(item.title)
|
Text(item.title)
|
||||||
.appFont(size: 13.3, weight: .medium)
|
.appFont(size: 13.3, weight: .medium)
|
||||||
.foregroundColor(Color.grayee)
|
.foregroundColor(Color.grayee)
|
||||||
.multilineTextAlignment(.leading)
|
.multilineTextAlignment(.leading)
|
||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
|
|
||||||
Text(item.nickname)
|
Text(item.nickname)
|
||||||
.appFont(size: 10, weight: .medium)
|
.appFont(size: 10, weight: .medium)
|
||||||
.foregroundColor(Color.gray77)
|
.foregroundColor(Color.gray77)
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.contentShape(Rectangle())
|
Spacer()
|
||||||
}
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.onTapGesture { AppState.shared.setAppStep(step: .contentDetail(contentId: item.id)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -158,36 +152,33 @@ struct SearchSeriesItemView: View {
|
|||||||
let item: SearchResponseItem
|
let item: SearchResponseItem
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationLink {
|
HStack(spacing: 13.3) {
|
||||||
SeriesDetailView(seriesId: item.id)
|
KFImage(URL(string: item.imageUrl))
|
||||||
} label: {
|
.cancelOnDisappear(true)
|
||||||
HStack(spacing: 13.3) {
|
.downsampling(size: CGSize(width: 60, height: 85))
|
||||||
KFImage(URL(string: item.imageUrl))
|
.resizable()
|
||||||
.cancelOnDisappear(true)
|
.scaledToFill()
|
||||||
.downsampling(size: CGSize(width: 60, height: 85))
|
.frame(width: 60, height: 85)
|
||||||
.resizable()
|
.clipped()
|
||||||
.scaledToFill()
|
.cornerRadius(5.3)
|
||||||
.frame(width: 60, height: 85)
|
|
||||||
.clipped()
|
|
||||||
.cornerRadius(5.3)
|
|
||||||
|
|
||||||
VStack(alignment: .leading, spacing: 6.7) {
|
VStack(alignment: .leading, spacing: 6.7) {
|
||||||
Text(item.title)
|
Text(item.title)
|
||||||
.appFont(size: 13.3, weight: .medium)
|
.appFont(size: 13.3, weight: .medium)
|
||||||
.foregroundColor(Color.grayee)
|
.foregroundColor(Color.grayee)
|
||||||
.multilineTextAlignment(.leading)
|
.multilineTextAlignment(.leading)
|
||||||
.lineLimit(2)
|
.lineLimit(2)
|
||||||
|
|
||||||
Text(item.nickname)
|
Text(item.nickname)
|
||||||
.appFont(size: 10, weight: .medium)
|
.appFont(size: 10, weight: .medium)
|
||||||
.foregroundColor(Color.gray77)
|
.foregroundColor(Color.gray77)
|
||||||
}
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.contentShape(Rectangle())
|
Spacer()
|
||||||
}
|
}
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.onTapGesture { AppState.shared.setAppStep(step: .seriesDetail(seriesId: item.id)) }
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,7 @@ struct SearchView: View {
|
|||||||
]
|
]
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
Group {
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
BaseView(isLoading: $viewModel.isLoading) {
|
||||||
VStack(spacing: 0) {
|
VStack(spacing: 0) {
|
||||||
HStack(spacing: 0) {
|
HStack(spacing: 0) {
|
||||||
|
|||||||
@@ -76,6 +76,7 @@ struct SplashView: View {
|
|||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.toolbar(.hidden, for: .navigationBar)
|
||||||
.onAppear {
|
.onAppear {
|
||||||
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.5) {
|
||||||
AppState.shared.isRestartApp = false
|
AppState.shared.isRestartApp = false
|
||||||
|
|||||||
206
docs/20260306_내비게이션스택단일전환.md
Normal file
206
docs/20260306_내비게이션스택단일전환.md
Normal file
@@ -0,0 +1,206 @@
|
|||||||
|
# 20260306 내비게이션 스택 단일 전환 계획
|
||||||
|
|
||||||
|
## 작업 목표
|
||||||
|
- iOS 16 기준으로 앱 전역 내비게이션을 단일 `NavigationStack`으로 통합한다.
|
||||||
|
- `ContentView` 최상단 `appStep` 오버레이 방식(`switch appState.appStep`)을 점진적으로 제거한다.
|
||||||
|
- 뒤로가기 시 화면 재생성으로 인한 재조회(데이터 재로딩) 빈도를 줄인다.
|
||||||
|
|
||||||
|
## UI/UX 동결 원칙
|
||||||
|
- [x] 화면 레이아웃, 색상, 폰트, 문구, 컴포넌트 배치는 변경하지 않는다.
|
||||||
|
- [x] 정보 구조(탭 구성, 메뉴 진입 순서, 버튼 위치)는 유지한다.
|
||||||
|
- [x] 모달(`sheet`/`fullScreenCover`)과 푸시 전환의 사용 의도는 유지하고 라우팅 구현만 교체한다.
|
||||||
|
- [x] 뒤로가기 제스처/버튼 동작은 기존 사용자 기대와 동일하게 유지한다.
|
||||||
|
- [x] 변경 범위는 내비게이션 상태 관리(`AppStep`/`AppState`)와 라우팅 연결부로 한정한다.
|
||||||
|
|
||||||
|
## 사전 조사 체크리스트
|
||||||
|
- [x] 루트 렌더링 구조 확인 (`SodaLive/Sources/ContentView.swift`의 `switch appState.appStep` 기반 분기)
|
||||||
|
- [x] 라우트 정의 확인 (`SodaLive/Sources/App/AppStep.swift` 78개 `case`)
|
||||||
|
- [x] 전역 라우팅 상태 확인 (`SodaLive/Sources/App/AppState.swift`의 `setAppStep`, `back`, 내부 back stack)
|
||||||
|
- [x] `AppState.shared.setAppStep` 호출 분포 확인 (총 162회, 73개 파일)
|
||||||
|
- [x] `AppState.shared.back` 호출 분포 확인 (총 54회, 37개 파일)
|
||||||
|
- [x] `NavigationView` 사용 현황 확인 (18개 파일)
|
||||||
|
- [x] `NavigationStack` 사용 현황 확인 (2개 파일)
|
||||||
|
- [x] `navigationDestination(for:)` 사용 현황 확인 (2개 화면, `Int -> CharacterDetailView`)
|
||||||
|
- [x] 모달 기반 전환 현황 확인 (`sheet` 13회/7개 파일, `fullScreenCover` 4회/4개 파일)
|
||||||
|
- [x] 딥링크/푸시 진입 흐름 확인 (`AppDelegate` -> `AppState.push*` -> `SplashView`/`HomeView`)
|
||||||
|
|
||||||
|
## 분산 내비게이션 패턴 메모
|
||||||
|
- [x] 채팅 영역 2개 화면에서만 자체 `NavigationStack`을 보유하고 있어 앱 전역 스택과 분리되어 동작
|
||||||
|
- [x] `NavigationView`를 가진 부모 화면 + 하위 컴포넌트 `NavigationLink` 조합(중첩 구조)이 콘텐츠/시리즈/검색 화면에 광범위하게 분포
|
||||||
|
- [x] 댓글/공유/이미지뷰어/본인인증은 `sheet`/`fullScreenCover`로 분리되어 있어 스택 푸시와 별도 정책 필요
|
||||||
|
|
||||||
|
## 라우팅 핫스팟(우선 전환 대상)
|
||||||
|
- [x] `setAppStep` 상위 호출 파일 확인: `MyPageView.swift`(14), `LiveView.swift`(10), `SettingsView.swift`(8), `LiveReservation/SectionLiveReservationView.swift`(6), `SplashView.swift`(6)
|
||||||
|
- [x] `back` 상위 호출 파일 확인: `LiveDetailView.swift`(5), `ProfileUpdateViewModel.swift`(3), `CreatorCommunityModifyView.swift`(3), `CanPgPaymentView.swift`(3), `CanPgPaymentViewModel.swift`(3)
|
||||||
|
- [x] 루트 화면 재진입 영향 구간 확인: `SplashView`/`HomeView`에서 `setAppStep(.main)` 후 상세 스텝 연속 진입 패턴 존재
|
||||||
|
|
||||||
|
## 외부 레퍼런스 반영 메모
|
||||||
|
- [x] Apple 공식 `NavigationStack`/`NavigationPath` 문서 기준으로 value-based 라우팅(`path` + `navigationDestination`) 채택 필요 확인
|
||||||
|
- [x] `NavigationLink(isActive:)`/`selection` 기반 API deprecate 이슈 확인, 값 기반 링크(`NavigationLink(value:)`)로 정리 필요
|
||||||
|
- [x] 상태 복원 요구 시 `NavigationPath.CodableRepresentation` 기반 직렬화/복원 전략 적용 가능성 확인
|
||||||
|
- [x] 탭/플로우별 path 분리 패턴(오픈소스 코디네이터 예시) 확인
|
||||||
|
- [x] SwiftUI 목적지 재사용으로 인한 `onAppear` 동작 함정 사례 확인(필요 시 `.task(id:)`/id 기반 제어)
|
||||||
|
|
||||||
|
### 참고 링크
|
||||||
|
- https://developer.apple.com/documentation/swiftui/navigationstack
|
||||||
|
- https://developer.apple.com/documentation/swiftui/understanding-the-navigation-stack
|
||||||
|
- https://developer.apple.com/documentation/swiftui/navigationpath
|
||||||
|
- https://developer.apple.com/documentation/swiftui/navigationlink/init%28isactive%3Adestination%3Alabel%3A%29
|
||||||
|
- https://github.com/chocoford/ExcalidrawZ/blob/acb84e0f49ab943bb417ac5a7c036247ef55707b/ExcalidrawZ/Share/ShareView.swift#L56-L191
|
||||||
|
- https://github.com/wunax/strimr/blob/8ce61ac3fb540187f03e2c94d082dc3b86720165/Shared/Features/MainCoordinator.swift#L4-L147
|
||||||
|
- https://github.com/TortugaPower/BookPlayer/blob/a0a8f00a3a80e5aca706da6e8d28fa2231203bd7/BookPlayer/Profile/Passkey/PasskeyRegistrationView.swift#L31-L137
|
||||||
|
- https://github.com/twostraws/Inferno/blob/725c30a8b29957ba0fdef18aef1289e5c0092298/Sandbox/Inferno/ContentView.swift#L72-L91
|
||||||
|
|
||||||
|
## 구현 체크리스트
|
||||||
|
- [x] `AppRoute`(가칭) 설계: `NavigationStack`에 적합한 `Hashable` 라우트 모델 정의
|
||||||
|
- [x] `AppStep` -> `AppRoute` 매핑표 작성: 78개 스텝을 루트 전환/푸시 전환으로 분리
|
||||||
|
- [x] 클로저/비-Hashable 연관값 대응 설계: 콜백 전달이 필요한 스텝(`refresh`, `onSuccess` 등)의 안전한 브리지 전략 수립
|
||||||
|
- [x] 전역 내비게이션 코디네이터(가칭) 설계: `path`, `push`, `pop`, `reset` API 정의
|
||||||
|
- [x] `ContentView` 루트 구조 개편: 단일 `NavigationStack(path:)` + `navigationDestination` 등록 구조로 전환
|
||||||
|
- [x] 스플래시/로그인/메인 루트 상태 전이 재정의: 기존 `.splash`, `.main`, `.login` 동작 동등성 보장
|
||||||
|
- [x] 딥링크/푸시 라우팅 재배선: `pushRoomId/pushChannelId/pushAudioContentId/pushSeriesId/pushMessageId`를 path 전환으로 일원화
|
||||||
|
- [x] 기존 `AppState.shared.setAppStep` 호출부 점진 전환(모듈 우선순위 적용)
|
||||||
|
- [x] 기존 `AppState.shared.back` 호출부를 stack pop 동작으로 전환 (`DetailNavigationBar` 포함)
|
||||||
|
- [x] 중첩 내비게이션 정리: `NavigationView` 18개 제거 및 `NavigationStack` 2개(채팅 화면) 중첩 해소
|
||||||
|
- [x] `NavigationLink` 로컬 푸시와 전역 라우트의 역할 분리 규칙 확정
|
||||||
|
- [x] 데이터 재로딩 방지 점검: 복귀 시 재요청이 발생하는 목록/상세 화면의 `onAppear`/ViewModel 생명주기 정리
|
||||||
|
- [x] 단계별 마이그레이션 플래그 또는 호환 레이어(`setAppStep` 브리지) 적용 여부 결정
|
||||||
|
- [x] 모듈별 전환 순서 확정 (권장: Root -> Home/Content -> MyPage/Settings -> Live -> Message/Chat)
|
||||||
|
- [x] 완료 검증 시나리오 문서화 (뒤로가기, 탭 전환, 푸시 진입, 로그인 전환, 결제/충전 플로우)
|
||||||
|
|
||||||
|
## 스크롤 유지/재로딩 후속 보정
|
||||||
|
- [x] `ContentCurationView` 복귀 시 무조건 재조회 방지(`isInitialized` + `curationId` 변경 감지)
|
||||||
|
- [x] `ContentMainAlarmAllView` 최초 진입 1회 조회 가드 적용
|
||||||
|
- [x] `ContentMainAsmrAllView`/`ContentMainReplayAllView` 테마 재설정 기반 중복 조회 방지
|
||||||
|
- [x] `ContentMainIntroduceCreatorAllView` 최초 진입 1회 조회 가드 적용
|
||||||
|
- [x] `SeriesListAllView` 초기화 가드 및 필터 변경 시에만 목록 리셋/재조회
|
||||||
|
- [x] `SeriesMainHomeView`/`SeriesMainByGenreView` 복귀 재조회 방지
|
||||||
|
- [x] Series 화면의 남은 로컬 `NavigationLink`를 `AppState.setAppStep` 기반으로 전환해 루트 path 일관성 확보
|
||||||
|
- [x] 잔여 `NavigationLink` 2건(댓글 답글 로컬 플로우)은 의도적으로 유지
|
||||||
|
- [x] `UserProfileView` 복귀 시 무조건 `getCreatorProfile` 재호출 방지(최초 진입/미로딩 상태에만 조회)
|
||||||
|
- [x] 전역 step 화면에 기본 `Navigation` 뒤로가기 버튼 비노출 적용(`ContentView`의 `AppStepLayerView` 호출부 공통 처리)
|
||||||
|
- [x] 언어 변경 soft restart 시 스플래시 상단 오프셋 보정(`SplashView`에서 `toolbar(.hidden, for: .navigationBar)` 적용)
|
||||||
|
- [x] 댓글 리스트 시트에서 답글 보기/쓰기 동작 복구(`ContentDetailView`/`CreatorCommunityAllView` 시트 컨텐츠를 `NavigationStack`으로 감싸고 nav bar 숨김 적용)
|
||||||
|
|
||||||
|
## Navigation 컨테이너 정리 대상 파일
|
||||||
|
- [x] `SodaLive/Sources/MyPage/OrderList/OrderListAllView.swift` (`NavigationView` 제거)
|
||||||
|
- [x] `SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/Comment/CreatorCommunityCommentListView.swift` (`NavigationView` 제거)
|
||||||
|
- [x] `SodaLive/Sources/Search/SearchView.swift` (`NavigationView` 제거)
|
||||||
|
- [x] `SodaLive/Sources/Content/ContentListView.swift` (`NavigationView` 제거)
|
||||||
|
- [x] `SodaLive/Sources/Content/Box/ContentBoxView.swift` (`NavigationView` 제거)
|
||||||
|
- [x] `SodaLive/Sources/Content/All/ContentAllView.swift` (`NavigationView` 제거)
|
||||||
|
- [x] `SodaLive/Sources/Content/Main/V2/Alarm/All/ContentMainAlarmAllView.swift` (`NavigationView` 제거)
|
||||||
|
- [x] `SodaLive/Sources/Content/All/ContentRankingAllView.swift` (`NavigationView` 제거)
|
||||||
|
- [x] `SodaLive/Sources/Content/Curation/ContentCurationView.swift` (`NavigationView` 제거)
|
||||||
|
- [x] `SodaLive/Sources/Content/Main/V2/Replay/All/ContentMainReplayAllView.swift` (`NavigationView` 제거)
|
||||||
|
- [x] `SodaLive/Sources/Content/All/ByTheme/ContentAllByThemeView.swift` (`NavigationView` 제거)
|
||||||
|
- [x] `SodaLive/Sources/Content/All/ContentNewAllView.swift` (`NavigationView` 제거)
|
||||||
|
- [x] `SodaLive/Sources/Content/Main/V2/ContentMainViewV2.swift` (`NavigationView` 제거)
|
||||||
|
- [x] `SodaLive/Sources/Content/Main/V2/Free/All/ContentMainIntroduceCreatorAllView.swift` (`NavigationView` 제거)
|
||||||
|
- [x] `SodaLive/Sources/Content/Series/Main/SeriesMainView.swift` (`NavigationView` 제거)
|
||||||
|
- [x] `SodaLive/Sources/Content/Main/V2/Asmr/All/ContentMainAsmrAllView.swift` (`NavigationView` 제거)
|
||||||
|
- [x] `SodaLive/Sources/Content/Series/SeriesListAllView.swift` (`NavigationView` 제거)
|
||||||
|
- [x] `SodaLive/Sources/Content/Detail/Comment/AudioContentCommentListView.swift` (`NavigationView` 제거)
|
||||||
|
- [x] `SodaLive/Sources/Chat/Character/New/Views/NewCharacterListView.swift` (중첩 `NavigationStack` 제거)
|
||||||
|
- [x] `SodaLive/Sources/Chat/Original/Detail/OriginalWorkDetailView.swift` (중첩 `NavigationStack` 제거)
|
||||||
|
|
||||||
|
## 화면군 우선순위(초안)
|
||||||
|
- [x] P0: Root/App (`ContentView`, `AppState`, `AppStep`, `SplashView`, `HomeView` 푸시 진입 처리)
|
||||||
|
- [x] P1: Content/Series/Search (`NavigationView` 다수 분포 구간)
|
||||||
|
- [x] P1: MyPage/Settings (계정/설정/캔 결제 흐름)
|
||||||
|
- [x] P2: Live/Audition (콜백 기반 스텝 다수 구간)
|
||||||
|
- [x] P2: Message/Chat (일부 `NavigationStack` 선구현 화면 정합화)
|
||||||
|
|
||||||
|
## 리스크 및 대응 계획
|
||||||
|
- [x] 리스크: `AppStep` 연관값에 클로저가 많아 `NavigationStack`의 `Hashable` 경로 모델과 충돌 가능
|
||||||
|
- [x] 대응: 라우트에는 식별자/파라미터만 담고, 콜백은 코디네이터의 임시 액션 저장소(토큰 기반)로 분리
|
||||||
|
- [x] 리스크: 푸시/딥링크 타이밍(현재 `DispatchQueue.main.asyncAfter`) 의존 로직 회귀 가능
|
||||||
|
- [x] 대응: 루트 준비 완료 시점 이벤트를 기준으로 경로 적용 순서를 표준화
|
||||||
|
- [x] 리스크: 기존 `NavigationView` 내부 `NavigationLink` 동작과 전역 스택 충돌 가능
|
||||||
|
- [x] 대응: "로컬 화면 내부 계층"과 "앱 전역 화면 전환" 규칙을 문서화하고 중복 push 금지 가드 추가
|
||||||
|
|
||||||
|
## 검증 계획(구현 단계에서 수행)
|
||||||
|
- [x] `lsp_diagnostics`로 수정 파일 오류 0 확인
|
||||||
|
- [x] `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build`
|
||||||
|
- [x] `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`
|
||||||
|
- [x] `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" test`
|
||||||
|
- [x] `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" test`
|
||||||
|
|
||||||
|
## 검증 기록
|
||||||
|
- 무엇/왜/어떻게: 내비게이션 단일화 계획 수립을 위해 코드베이스 전수 탐색으로 현재 라우팅 구조와 분산 내비게이션 사용처를 먼저 계량했다.
|
||||||
|
- 실행 명령: `grep` 패턴 검색 (`NavigationView`, `NavigationStack`, `NavigationLink`, `navigationDestination`, `setAppStep`, `back`)
|
||||||
|
- 결과: `NavigationView` 18개 파일, `NavigationStack` 2개 파일, `AppState.shared.setAppStep` 162회, `AppState.shared.back` 54회 확인.
|
||||||
|
- 실행 명령: `ast_grep_search` (`NavigationView { $$$ }`, `NavigationStack { $$$ }`, `func setAppStep($$$) { $$$ }`)
|
||||||
|
- 결과: 중첩 `NavigationStack` 화면 2개와 전역 라우팅 함수 정의 위치(`AppState.swift`)를 교차 검증.
|
||||||
|
- 실행 명령: `lsp_symbols` (`ContentView.swift`, `AppState.swift`, `AppStep.swift`)
|
||||||
|
- 결과: 루트 분기 지점(`ContentView.body`), 전역 라우팅 API(`setAppStep`, `back`), 라우트 열거형(`AppStep`) 심볼 확인.
|
||||||
|
- 실행 명령: `rg -n "NavigationView|NavigationStack|setAppStep\(|AppState\.shared\.back\(|navigationDestination|NavigationLink" "SodaLive/Sources"`
|
||||||
|
- 결과: 로컬 환경에 `rg` 미설치(`command not found`)로 확인되어 `grep`/`ast-grep` 기반으로 대체 탐색 수행.
|
||||||
|
- 실행 명령: background `librarian` 조사 (`bg_0803bb96`)
|
||||||
|
- 결과: Apple 공식 문서 기준(`NavigationStack`, `NavigationPath`, deprecated `NavigationLink(isActive:)`)과 오픈소스 코디네이터 패턴(탭별 `NavigationPath`, 딥링크 path 매핑, 상태 복원) 근거 확보.
|
||||||
|
- 실행 명령: background `explore` 조사 (`bg_7711b310`)
|
||||||
|
- 결과: `NavigationView` 18개, `NavigationStack` 2개, `navigationDestination` 2개, 모달 전환(`sheet`/`fullScreenCover`) 분산 사용처를 파일 단위로 확정.
|
||||||
|
- 무엇/왜/어떻게: `AppState`에 `AppRoute` 기반 `navigationPath`를 도입하고 `setAppStep/back`를 path push/pop/reset 브리지로 전환했다. `ContentView`는 단일 루트 `NavigationStack(path:)` + `navigationDestination(for: AppRoute)` 구조로 교체하고, 기존 화면 매핑은 `AppStepLayerView`로 유지해 UI를 고정했다.
|
||||||
|
- 실행 명령: `ast_grep_replace` (`NavigationView { $$$ } -> Group { $$$ }`, `NavigationStack { $$$ } -> Group { $$$ }`)
|
||||||
|
- 결과: 대상 20개 파일(18 `NavigationView`, 2 `NavigationStack`)의 로컬 컨테이너 제거 완료.
|
||||||
|
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build`
|
||||||
|
- 결과: 1차 `AppStep` `Equatable` 비교 오류 수정 후 재실행에서 `** BUILD SUCCEEDED **`.
|
||||||
|
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`
|
||||||
|
- 결과: `** BUILD SUCCEEDED **`.
|
||||||
|
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" test`
|
||||||
|
- 결과: `Scheme SodaLive is not currently configured for the test action.`
|
||||||
|
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" test`
|
||||||
|
- 결과: `Scheme SodaLive-dev is not currently configured for the test action.`
|
||||||
|
- 무엇/왜/어떻게: 언어 변경 적용 후 `AppState.softRestart()` 경로에서 스플래시가 `NavigationStack` 컨텍스트 안에 렌더링되며 네비게이션 바 안전영역만큼 아래로 밀리는 현상을 확인했다. 스플래시 화면에서 네비게이션 바를 숨기도록 수정해 오프셋을 제거했다.
|
||||||
|
- 실행 명령: background 분석 `explore`/`librarian` (`bg_ff67655b`, `bg_750d2d2e`, `bg_cb65fc63`)
|
||||||
|
- 결과: 공통 원인으로 `ContentView`의 단일 `NavigationStack` 내 스플래시 렌더링 시 nav bar inset 개입이 확인되었고, Apple 문서 기준 `toolbar(_:for:)`로 navigation bar를 숨기는 방식이 유효함을 확인.
|
||||||
|
- 실행 명령: `lsp_diagnostics` (`SodaLive/Sources/Splash/SplashView.swift`)
|
||||||
|
- 결과: SourceKit 인덱싱 컨텍스트에서 `FirebaseRemoteConfig` 모듈 미해석 오탐(`No such module 'FirebaseRemoteConfig'`) 발생, 빌드로 실제 유효성 확인.
|
||||||
|
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build && xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`
|
||||||
|
- 결과: 두 스킴 모두 `** BUILD SUCCEEDED **`.
|
||||||
|
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" test; xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" test`
|
||||||
|
- 결과: 두 스킴 모두 `is not currently configured for the test action`으로 테스트 액션 미구성 상태.
|
||||||
|
- 무엇/왜/어떻게: 모든 페이지에서 시스템 기본 뒤로가기 버튼을 감추기 위해, 단일 루트 라우팅 지점인 `ContentView`의 `AppStepLayerView` 렌더링 2곳(루트 overlay / `navigationDestination`)에 공통으로 `.navigationBarBackButtonHidden(true)`를 적용했다.
|
||||||
|
- 실행 명령: `lsp_diagnostics` (`SodaLive/Sources/ContentView.swift`)
|
||||||
|
- 결과: SourceKit 인덱싱 컨텍스트 부재로 외부 타입 다수 미해석 오탐이 발생했고, 실제 유효성은 빌드로 확인.
|
||||||
|
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build`
|
||||||
|
- 결과: `** BUILD SUCCEEDED **` (중간 1회는 동시 빌드로 `build.db` lock 실패 후 단독 재실행 성공).
|
||||||
|
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`
|
||||||
|
- 결과: `** BUILD SUCCEEDED **`.
|
||||||
|
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" test; xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" test`
|
||||||
|
- 결과: 두 스킴 모두 `is not currently configured for the test action`으로 테스트 액션 미구성 상태 재확인.
|
||||||
|
- 실행 명령: `lsp_diagnostics` (수정 파일 일괄 점검)
|
||||||
|
- 결과: `ContentView.swift`/`AppState.swift` 등에서 SourceKit 워크스페이스 인덱싱 한계로 외부 타입 미해석 오탐이 발생했다. 실제 컴파일은 두 스킴 빌드 성공으로 검증.
|
||||||
|
- 무엇/왜/어떻게: 뒤로가기 시 스크롤 위치 초기화와 불필요 재로딩을 줄이기 위해, 목록/탭 화면의 `.onAppear` 무조건 조회를 초기 1회 가드로 정리하고 Series 계열 잔여 로컬 push를 `AppState` 경로 기반으로 통일했다.
|
||||||
|
- 실행 명령: `grep -n "NavigationLink\\s*\\(" "SodaLive/Sources/**/*.swift"`(동등 패턴 검색)
|
||||||
|
- 결과: `NavigationLink` 잔여는 댓글 답글 로컬 플로우 2건(`CreatorCommunityCommentItemView.swift`, `AudioContentCommentItemView.swift`)만 확인.
|
||||||
|
- 무엇/왜/어떻게: NavigationStack 마이그레이션 후 댓글 리스트(`AudioContentCommentListView`, `CreatorCommunityCommentListView`)가 `.sheet`로만 표시되면서 내부 `NavigationLink`가 네비게이션 컨텍스트 없이 렌더링되어 답글 보기/쓰기 진입이 무반응이 되었다. 두 시트 호출부(`ContentDetailView`, `CreatorCommunityAllView`)를 `NavigationStack`으로 감싸 reply push가 동작하도록 복구했다.
|
||||||
|
- 실행 명령: background 분석 `explore` (`bg_5ad57f11`, `bg_a7c4e178`)
|
||||||
|
- 결과: 공통 원인으로 “시트 내부 NavigationStack 부재 + item view의 NavigationLink 의존”이 확인됨.
|
||||||
|
- 실행 명령: `lsp_diagnostics` (`SodaLive/Sources/Content/Detail/ContentDetailView.swift`, `SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllView.swift`)
|
||||||
|
- 결과: SourceKit 인덱싱 컨텍스트에서 외부 모듈/타입 미해석 오탐이 발생했고, 실제 유효성은 빌드로 확인.
|
||||||
|
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build && xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`
|
||||||
|
- 결과: 두 스킴 모두 `** BUILD SUCCEEDED **`.
|
||||||
|
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" test; xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" test`
|
||||||
|
- 결과: 두 스킴 모두 `is not currently configured for the test action`으로 테스트 액션 미구성 상태.
|
||||||
|
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build`
|
||||||
|
- 결과: `** BUILD SUCCEEDED **`.
|
||||||
|
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`
|
||||||
|
- 결과: `** BUILD SUCCEEDED **`.
|
||||||
|
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" test`
|
||||||
|
- 결과: `Scheme SodaLive is not currently configured for the test action.`
|
||||||
|
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" test`
|
||||||
|
- 결과: `Scheme SodaLive-dev is not currently configured for the test action.`
|
||||||
|
- 실행 명령: `lsp_diagnostics` (이번 수정 8개 파일)
|
||||||
|
- 결과: SourceKit 인덱싱 컨텍스트 부재로 다수 오탐(`Cannot find ... in scope`)이 재현되었고, 문법/타입 안정성은 두 스킴 빌드 성공으로 최종 검증.
|
||||||
|
- 무엇/왜/어떻게: `UserProfileView`에서 콘텐츠 상세로 이동 후 뒤로 복귀 시 `.onAppear`가 매번 `getCreatorProfile`를 호출해 `creatorProfile = nil` 재초기화가 발생했고, 이로 인해 스크롤이 최상단으로 리셋되던 문제를 조회 조건 가드로 수정했다.
|
||||||
|
- 실행 명령: `lsp_diagnostics` (`SodaLive/Sources/Explorer/Profile/UserProfileView.swift`)
|
||||||
|
- 결과: SourceKit 인덱싱 컨텍스트에서 `Kingfisher` 모듈 미해석 오탐(`No such module 'Kingfisher'`)이 발생했으며, 실제 빌드로 유효성 확인.
|
||||||
|
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build`
|
||||||
|
- 결과: `** BUILD SUCCEEDED **`.
|
||||||
|
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" -configuration Debug build`
|
||||||
|
- 결과: `** BUILD SUCCEEDED **`.
|
||||||
|
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" test`
|
||||||
|
- 결과: `Scheme SodaLive is not currently configured for the test action.`
|
||||||
|
- 실행 명령: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive-dev" test`
|
||||||
|
- 결과: `Scheme SodaLive-dev is not currently configured for the test action.`
|
||||||
Reference in New Issue
Block a user