532 lines
28 KiB
Swift
532 lines
28 KiB
Swift
//
|
|
// HomeTabView.swift
|
|
// SodaLive
|
|
//
|
|
// Created by klaus on 7/10/25.
|
|
//
|
|
|
|
import SwiftUI
|
|
import Bootpay
|
|
import BootpayUI
|
|
|
|
struct HomeTabView: View {
|
|
@StateObject var viewModel = HomeTabViewModel()
|
|
@StateObject var liveViewModel = LiveViewModel()
|
|
@StateObject var mypageViewModel = MyPageViewModel()
|
|
|
|
@AppStorage("token") private var token: String = UserDefaults.string(forKey: UserDefaultsKey.token)
|
|
@AppStorage("role") private var role: String = UserDefaults.string(forKey: UserDefaultsKey.role)
|
|
@AppStorage("auth") private var auth: Bool = UserDefaults.bool(forKey: UserDefaultsKey.auth)
|
|
|
|
@State private var isShowAuthView: Bool = false
|
|
@State private var isShowAuthConfirmView: Bool = false
|
|
@State private var pendingAction: (() -> Void)? = nil
|
|
@State private var payload = Payload()
|
|
@State private var isShowUnfollowConfirmDialog = false
|
|
@State private var pendingUnfollowCreatorId: Int? = nil
|
|
@State private var pendingUnfollowCreatorName = ""
|
|
|
|
let onTapPopularCharacterAllView: (() -> Void)?
|
|
let onTapLiveNowItem: ((Int, Bool) -> Void)?
|
|
|
|
init(
|
|
onTapPopularCharacterAllView: (() -> Void)? = nil,
|
|
onTapLiveNowItem: ((Int, Bool) -> Void)? = nil
|
|
) {
|
|
self.onTapPopularCharacterAllView = onTapPopularCharacterAllView
|
|
self.onTapLiveNowItem = onTapLiveNowItem
|
|
}
|
|
|
|
// CharacterView에서 전달받는 단일 진입 함수
|
|
private func handleCharacterSelection(_ characterId: Int) {
|
|
let trimmed = token.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard !trimmed.isEmpty else {
|
|
AppState.shared.setAppStep(step: .login)
|
|
return
|
|
}
|
|
|
|
let normalizedCountryCode = UserDefaults
|
|
.string(forKey: .countryCode)
|
|
.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
.uppercased()
|
|
let isKoreanCountry = normalizedCountryCode.isEmpty || normalizedCountryCode == "KR"
|
|
|
|
if isKoreanCountry && auth == false {
|
|
pendingAction = {
|
|
AppState.shared
|
|
.setAppStep(step: .characterDetail(characterId: characterId))
|
|
}
|
|
isShowAuthConfirmView = true
|
|
return
|
|
}
|
|
|
|
if !UserDefaults.isAdultContentVisible() {
|
|
pendingAction = nil
|
|
AppState.shared.errorMessage = I18n.Settings.adultContentEnableGuide
|
|
AppState.shared.isShowErrorPopup = true
|
|
AppState.shared.setAppStep(step: .contentViewSettings)
|
|
return
|
|
}
|
|
|
|
AppState.shared.setAppStep(step: .characterDetail(characterId: characterId))
|
|
}
|
|
|
|
private func handleLiveNowItemTap(roomId: Int, isAdult: Bool) {
|
|
onTapLiveNowItem?(roomId, isAdult)
|
|
}
|
|
|
|
var body: some View {
|
|
BaseView(isLoading: $viewModel.isLoading) {
|
|
ZStack(alignment: .bottomTrailing) {
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
HStack(spacing: 20) {
|
|
Image("img_text_logo")
|
|
|
|
Spacer()
|
|
|
|
if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
Image("ic_search_white")
|
|
.onTapGesture {
|
|
AppState
|
|
.shared
|
|
.setAppStep(step: .search)
|
|
}
|
|
|
|
Image("ic_can")
|
|
.onTapGesture {
|
|
AppState
|
|
.shared
|
|
.setAppStep(step: .canCharge(refresh: {}))
|
|
}
|
|
|
|
Image("ic_storage")
|
|
.onTapGesture {
|
|
AppState
|
|
.shared
|
|
.setAppStep(step: .myBox(currentTab: .orderlist))
|
|
}
|
|
|
|
Image("ic_bell")
|
|
.onTapGesture {
|
|
AppState.shared
|
|
.setAppStep(step: .notificationList)
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal, 24)
|
|
.padding(.vertical, 20)
|
|
|
|
ScrollView(.vertical, showsIndicators: false) {
|
|
VStack(alignment: .leading, spacing: 48) {
|
|
if !viewModel.liveList.isEmpty {
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
Text(I18n.Home.liveNowSectionTitle)
|
|
.appFont(size: 24, weight: .bold)
|
|
.foregroundColor(.white)
|
|
.padding(.horizontal, 24)
|
|
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
LazyHStack(spacing: 16) {
|
|
ForEach(0..<viewModel.liveList.count, id: \.self) { index in
|
|
let item = viewModel.liveList[index]
|
|
HomeLiveItemView(item: item) { roomId in
|
|
handleLiveNowItemTap(
|
|
roomId: roomId,
|
|
isAdult: item.isAdult
|
|
)
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal, 24)
|
|
}
|
|
}
|
|
}
|
|
|
|
if !viewModel.creatorRanking.isEmpty {
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
Text(I18n.Home.popularCreatorSectionTitle)
|
|
.appFont(size: 24, weight: .bold)
|
|
.foregroundColor(.white)
|
|
.padding(.horizontal, 24)
|
|
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
LazyHStack(spacing: 16) {
|
|
ForEach(viewModel.creatorRanking.indices, id: \.self) { index in
|
|
let item = viewModel.creatorRanking[index]
|
|
HomeCreatorRankingItemView(
|
|
rank: index + 1,
|
|
item: $viewModel.creatorRanking[index],
|
|
onClickFollow: { creatorId, follow in
|
|
if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
if follow {
|
|
viewModel.creatorFollow(creatorId: item.id, follow: true, notify: true)
|
|
} else {
|
|
pendingUnfollowCreatorId = creatorId
|
|
pendingUnfollowCreatorName = item.nickname
|
|
isShowUnfollowConfirmDialog = true
|
|
}
|
|
} else {
|
|
AppState.shared
|
|
.setAppStep(step: .login)
|
|
}
|
|
}
|
|
)
|
|
.onTapGesture {
|
|
if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
AppState.shared
|
|
.setAppStep(step: .creatorDetail(userId: item.id))
|
|
} else {
|
|
AppState.shared
|
|
.setAppStep(step: .login)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal, 24)
|
|
}
|
|
}
|
|
}
|
|
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
HomeLatestContentView(
|
|
onClickMore: {
|
|
if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
AppState.shared
|
|
.setAppStep(step: .newContentAll(isFree: false))
|
|
} else {
|
|
AppState.shared
|
|
.setAppStep(step: .login)
|
|
}
|
|
},
|
|
themeList: viewModel.latestContentThemeList,
|
|
contentList: viewModel.latestContentList
|
|
) {
|
|
viewModel.getLatestContentByTheme(theme: $0)
|
|
}
|
|
}
|
|
|
|
if !viewModel.eventBannerList.isEmpty {
|
|
ContentMainBannerViewV2(bannerList: viewModel.eventBannerList)
|
|
}
|
|
|
|
if !viewModel.originalAudioDramaList.isEmpty {
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
HStack(spacing: 0) {
|
|
Text(I18n.Home.onlyOnVoiceOnSectionTitle)
|
|
.appFont(size: 24, weight: .bold)
|
|
.foregroundColor(.white)
|
|
|
|
Spacer()
|
|
|
|
Text(I18n.Common.viewAll)
|
|
.appFont(size: 14, weight: .regular)
|
|
.foregroundColor(.init(hex: "78909C"))
|
|
.onTapGesture {
|
|
AppState.shared
|
|
.setAppStep(step: .seriesAll(isOriginal: true))
|
|
}
|
|
}
|
|
.padding(.horizontal, 24)
|
|
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
LazyHStack(spacing: 16) {
|
|
ForEach(0..<viewModel.originalAudioDramaList.count, id: \.self) {
|
|
SeriesItemView(item: viewModel.originalAudioDramaList[$0])
|
|
}
|
|
}
|
|
.padding(.horizontal, 24)
|
|
}
|
|
}
|
|
}
|
|
|
|
Image("img_banner_audition")
|
|
.resizable()
|
|
.scaledToFit()
|
|
.padding(.horizontal, 24)
|
|
.onTapGesture {
|
|
if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
AppState.shared
|
|
.setAppStep(step: .audition)
|
|
} else {
|
|
AppState.shared
|
|
.setAppStep(step: .login)
|
|
}
|
|
}
|
|
|
|
DayOfWeekSeriesView(seriesList: viewModel.dayOfWeekSeriesList) {
|
|
viewModel.getDayOfWeekSeriesList(dayOfWeek: $0)
|
|
}
|
|
|
|
// 인기 캐릭터 섹션
|
|
if !viewModel.popularCharacters.isEmpty {
|
|
CharacterSectionView(
|
|
title: I18n.Home.popularCharacterChatSectionTitle,
|
|
items: viewModel.popularCharacters,
|
|
isShowRank: true,
|
|
trailingTitle: I18n.Common.viewAll,
|
|
onTapTrailing: {
|
|
if let onTapPopularCharacterAllView = onTapPopularCharacterAllView {
|
|
onTapPopularCharacterAllView()
|
|
}
|
|
},
|
|
onTap: { ch in
|
|
handleCharacterSelection(ch.characterId)
|
|
}
|
|
)
|
|
}
|
|
|
|
HomeWeeklyChartView(contentList: viewModel.contentRanking) {
|
|
viewModel.getContentRankingBySort(sort: $0)
|
|
}
|
|
|
|
if !viewModel.recommendChannelList.isEmpty {
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
Text(I18n.Home.recommendChannelSectionTitle)
|
|
.appFont(size: 24, weight: .bold)
|
|
.foregroundColor(.white)
|
|
.padding(.horizontal, 24)
|
|
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
LazyHStack(spacing: 16) {
|
|
ForEach(0..<viewModel.recommendChannelList.count, id: \.self) {
|
|
RecommendChannelItemView(item: viewModel.recommendChannelList[$0])
|
|
}
|
|
}
|
|
.padding(.horizontal, 24)
|
|
}
|
|
}
|
|
}
|
|
|
|
if !viewModel.freeContentList.isEmpty {
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
HStack(spacing: 0) {
|
|
Text(I18n.Home.freeContentSectionTitle)
|
|
.appFont(size: 24, weight: .bold)
|
|
.foregroundColor(.white)
|
|
|
|
Spacer()
|
|
|
|
Text(I18n.Common.viewAll)
|
|
.appFont(size: 14, weight: .regular)
|
|
.foregroundColor(.init(hex: "78909C"))
|
|
.onTapGesture {
|
|
AppState.shared
|
|
.setAppStep(step: .contentAll(isFree: true, isPointOnly: false))
|
|
}
|
|
}
|
|
.padding(.horizontal, 24)
|
|
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
LazyHStack(spacing: 16) {
|
|
ForEach(0..<viewModel.freeContentList.count, id: \.self) { index in
|
|
ContentItemView(item: viewModel.freeContentList[index])
|
|
}
|
|
}
|
|
.padding(.horizontal, 24)
|
|
}
|
|
}
|
|
}
|
|
|
|
if !viewModel.pointAvailableContentList.isEmpty {
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
HStack(spacing: 0) {
|
|
Text(I18n.Home.pointRentalContentSectionTitle)
|
|
.appFont(size: 24, weight: .bold)
|
|
.foregroundColor(.white)
|
|
|
|
Spacer()
|
|
|
|
Text(I18n.Common.viewAll)
|
|
.appFont(size: 14, weight: .regular)
|
|
.foregroundColor(.init(hex: "78909C"))
|
|
.onTapGesture {
|
|
AppState.shared
|
|
.setAppStep(step: .contentAll(isFree: false, isPointOnly: true))
|
|
}
|
|
}
|
|
.padding(.horizontal, 24)
|
|
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
LazyHStack(spacing: 16) {
|
|
ForEach(0..<viewModel.pointAvailableContentList.count, id: \.self) { index in
|
|
ContentItemView(item: viewModel.pointAvailableContentList[index])
|
|
}
|
|
}
|
|
.padding(.horizontal, 24)
|
|
}
|
|
}
|
|
}
|
|
|
|
if !viewModel.recommendContentList.isEmpty {
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
HStack {
|
|
Text(I18n.Home.recommendContentSectionTitle)
|
|
.appFont(size: 24, weight: .bold)
|
|
.foregroundColor(.white)
|
|
|
|
Spacer()
|
|
|
|
Image("ic_refresh")
|
|
.onTapGesture {
|
|
viewModel.refreshRecommendContents()
|
|
}
|
|
}
|
|
.padding(.horizontal, 24)
|
|
|
|
let horizontalPadding: CGFloat = 24
|
|
let gridSpacing: CGFloat = 16
|
|
let width = (screenSize().width - (horizontalPadding * 2) - gridSpacing) / 2
|
|
|
|
LazyVGrid(
|
|
columns: Array(
|
|
repeating: GridItem(
|
|
.flexible(),
|
|
spacing: gridSpacing,
|
|
alignment: .topLeading
|
|
),
|
|
count: 2
|
|
),
|
|
alignment: .leading,
|
|
spacing: gridSpacing
|
|
) {
|
|
ForEach(viewModel.recommendContentList.indices, id: \.self) { idx in
|
|
ContentItemView(item: viewModel.recommendContentList[idx], itemSize: width)
|
|
}
|
|
}
|
|
.padding(.horizontal, horizontalPadding)
|
|
}
|
|
}
|
|
|
|
Text(I18n.Settings.companyInfo)
|
|
.appFont(size: 11, weight: .regular)
|
|
.foregroundColor(Color.gray77)
|
|
.padding(.horizontal, 13.3)
|
|
}
|
|
.padding(.vertical, 24)
|
|
}
|
|
}
|
|
|
|
if role == MemberRole.CREATOR.rawValue {
|
|
HStack(spacing: 5) {
|
|
Image("ic_thumb_play")
|
|
.resizable()
|
|
.frame(width: 20, height: 20)
|
|
|
|
Text(I18n.CreateContent.uploadAction)
|
|
.appFont(size: 13.3, weight: .bold)
|
|
.foregroundColor(.white)
|
|
}
|
|
.padding(13.3)
|
|
.background(Color(hex: "3bb9f1"))
|
|
.cornerRadius(44)
|
|
.padding(.trailing, 16.7)
|
|
.padding(.bottom, 16.7)
|
|
.onTapGesture {
|
|
AppState.shared.setAppStep(step: .createContent)
|
|
}
|
|
}
|
|
|
|
if isShowAuthConfirmView {
|
|
SodaDialog(
|
|
title: I18n.Chat.Auth.dialogTitle,
|
|
desc: I18n.Chat.Auth.dialogDescription,
|
|
confirmButtonTitle: I18n.Chat.Auth.goToVerification,
|
|
confirmButtonAction: {
|
|
isShowAuthConfirmView = false
|
|
isShowAuthView = true
|
|
},
|
|
cancelButtonTitle: I18n.Common.cancel,
|
|
cancelButtonAction: {
|
|
isShowAuthConfirmView = false
|
|
pendingAction = nil
|
|
},
|
|
textAlignment: .center
|
|
)
|
|
}
|
|
|
|
if isShowUnfollowConfirmDialog {
|
|
SodaDialog(
|
|
title: I18n.MemberChannel.unfollowConfirmTitle,
|
|
desc: I18n.MemberChannel.unfollowConfirmDescription(pendingUnfollowCreatorName),
|
|
confirmButtonTitle: I18n.Common.confirm,
|
|
confirmButtonAction: {
|
|
isShowUnfollowConfirmDialog = false
|
|
guard let creatorId = pendingUnfollowCreatorId else {
|
|
pendingUnfollowCreatorId = nil
|
|
pendingUnfollowCreatorName = ""
|
|
return
|
|
}
|
|
if let index = viewModel.creatorRanking.firstIndex(
|
|
where: { $0.id == creatorId }
|
|
) {
|
|
viewModel.creatorRanking[index].follow = false
|
|
}
|
|
pendingUnfollowCreatorId = nil
|
|
pendingUnfollowCreatorName = ""
|
|
viewModel.creatorFollow(creatorId: creatorId, follow: false, notify: false)
|
|
},
|
|
cancelButtonTitle: I18n.Common.cancel,
|
|
cancelButtonAction: {
|
|
isShowUnfollowConfirmDialog = false
|
|
pendingUnfollowCreatorId = nil
|
|
pendingUnfollowCreatorName = ""
|
|
},
|
|
textAlignment: .center
|
|
)
|
|
}
|
|
|
|
if liveViewModel.isShowPasswordDialog {
|
|
LiveRoomPasswordDialog(
|
|
isShowing: $liveViewModel.isShowPasswordDialog,
|
|
can: liveViewModel.secretOrPasswordDialogCan,
|
|
confirmAction: liveViewModel.passwordDialogConfirmAction
|
|
)
|
|
}
|
|
}
|
|
.onAppear {
|
|
payload.applicationId = BOOTPAY_APP_ID
|
|
payload.price = 0
|
|
payload.pg = "다날"
|
|
payload.method = "본인인증"
|
|
payload.orderName = "본인인증"
|
|
payload.authenticationId = "\(UserDefaults.string(forKey: .nickname))__\(String(NSTimeIntervalSince1970))"
|
|
|
|
viewModel.fetchData()
|
|
}
|
|
.fullScreenCover(isPresented: $isShowAuthView) {
|
|
BootpayUI(payload: payload, requestType: BootpayRequest.TYPE_AUTHENTICATION)
|
|
.onConfirm { _ in
|
|
true
|
|
}
|
|
.onCancel { _ in
|
|
isShowAuthView = false
|
|
}
|
|
.onError { _ in
|
|
AppState.shared.errorMessage = I18n.Chat.Auth.authenticationError
|
|
AppState.shared.isShowErrorPopup = true
|
|
isShowAuthView = false
|
|
}
|
|
.onDone {
|
|
DEBUG_LOG("onDone: \($0)")
|
|
mypageViewModel.authVerify($0) {
|
|
auth = true
|
|
isShowAuthView = false
|
|
if let action = pendingAction {
|
|
pendingAction = nil
|
|
action()
|
|
}
|
|
}
|
|
}
|
|
.onClose {
|
|
isShowAuthView = false
|
|
}
|
|
}
|
|
}
|
|
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
HomeTabView()
|
|
}
|