458 lines
24 KiB
Swift
458 lines
24 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()
|
|
|
|
@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()
|
|
|
|
var onTapPopularCharacterAllView: (() -> Void)? = nil
|
|
|
|
// CharacterView에서 전달받는 단일 진입 함수
|
|
private func handleCharacterSelection(_ characterId: Int) {
|
|
let trimmed = token.trimmingCharacters(in: .whitespacesAndNewlines)
|
|
guard !trimmed.isEmpty else {
|
|
AppState.shared.setAppStep(step: .login)
|
|
return
|
|
}
|
|
if auth == false {
|
|
pendingAction = {
|
|
AppState.shared
|
|
.setAppStep(step: .characterDetail(characterId: characterId))
|
|
}
|
|
isShowAuthConfirmView = true
|
|
return
|
|
}
|
|
AppState.shared.setAppStep(step: .characterDetail(characterId: characterId))
|
|
}
|
|
|
|
var body: some View {
|
|
BaseView(isLoading: $viewModel.isLoading) {
|
|
ZStack(alignment: .bottomTrailing) {
|
|
VStack(alignment: .leading, spacing: 0) {
|
|
HStack(spacing: 24) {
|
|
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))
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal, 24)
|
|
.padding(.vertical, 20)
|
|
|
|
ScrollView(.vertical, showsIndicators: false) {
|
|
VStack(alignment: .leading, spacing: 48) {
|
|
if !viewModel.liveList.isEmpty {
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
HStack(spacing: 0) {
|
|
Text("지금")
|
|
.font(.custom(Font.preBold.rawValue, size: 24))
|
|
.foregroundColor(.button)
|
|
|
|
Text(" 라이브중")
|
|
.font(.custom(Font.preBold.rawValue, size: 24))
|
|
.foregroundColor(.white)
|
|
}
|
|
.padding(.horizontal, 24)
|
|
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
HStack(spacing: 16) {
|
|
ForEach(0..<viewModel.liveList.count, id: \.self) { index in
|
|
HomeLiveItemView(item: viewModel.liveList[index]) { roomId in
|
|
if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
AppState.shared.setAppStep(
|
|
step: .liveDetail(
|
|
roomId: roomId,
|
|
onClickParticipant: {
|
|
AppState.shared.isShowPlayer = false
|
|
liveViewModel.enterLiveRoom(roomId: roomId)
|
|
},
|
|
onClickReservation: {},
|
|
onClickStart: {},
|
|
onClickCancel: {}
|
|
)
|
|
)
|
|
} else {
|
|
AppState.shared.setAppStep(step: .login)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
.padding(.horizontal, 24)
|
|
}
|
|
}
|
|
}
|
|
|
|
if !viewModel.creatorRanking.isEmpty {
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
HStack(spacing: 0) {
|
|
Text("인기")
|
|
.font(.custom(Font.preBold.rawValue, size: 24))
|
|
.foregroundColor(.button)
|
|
|
|
Text(" 크리에이터")
|
|
.font(.custom(Font.preBold.rawValue, size: 24))
|
|
.foregroundColor(.white)
|
|
}
|
|
.padding(.horizontal, 24)
|
|
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
HStack(spacing: 16) {
|
|
ForEach(0..<viewModel.creatorRanking.count, id: \.self) {
|
|
let item = viewModel.creatorRanking[$0]
|
|
HomeCreatorRankingItemView(
|
|
rank: $0 + 1,
|
|
item: item,
|
|
onClickFollow: { creatorId, follow in
|
|
if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
if follow {
|
|
viewModel.creatorFollow(creatorId: item.id, follow: true, notify: true)
|
|
} else {
|
|
viewModel.creatorFollow(creatorId: item.id, follow: false, notify: false)
|
|
}
|
|
} 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("오직")
|
|
.font(.custom(Font.preBold.rawValue, size: 24))
|
|
.foregroundColor(.button)
|
|
|
|
Text(" 보이스온에서만")
|
|
.font(.custom(Font.preBold.rawValue, size: 24))
|
|
.foregroundColor(.white)
|
|
}
|
|
.padding(.horizontal, 24)
|
|
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
HStack(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: "인기 캐릭터 채팅",
|
|
items: viewModel.popularCharacters,
|
|
isShowRank: true,
|
|
trailingTitle: "전체보기",
|
|
onTapTrailing: {
|
|
if let onTapPopularCharacterAllView = onTapPopularCharacterAllView {
|
|
onTapPopularCharacterAllView()
|
|
}
|
|
},
|
|
onTap: { ch in
|
|
handleCharacterSelection(ch.characterId)
|
|
}
|
|
)
|
|
}
|
|
|
|
if !viewModel.contentRanking.isEmpty {
|
|
HomeWeeklyChartView(contentList: viewModel.contentRanking)
|
|
}
|
|
|
|
if !viewModel.recommendChannelList.isEmpty {
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
HStack(spacing: 0) {
|
|
Text("추천")
|
|
.font(.custom(Font.preBold.rawValue, size: 24))
|
|
.foregroundColor(.button)
|
|
|
|
Text(" 채널")
|
|
.font(.custom(Font.preBold.rawValue, size: 24))
|
|
.foregroundColor(.white)
|
|
}
|
|
.padding(.horizontal, 24)
|
|
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
HStack(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("무료")
|
|
.font(.custom(Font.preBold.rawValue, size: 24))
|
|
.foregroundColor(.button)
|
|
|
|
Text(" 콘텐츠")
|
|
.font(.custom(Font.preBold.rawValue, size: 24))
|
|
.foregroundColor(.white)
|
|
}
|
|
.padding(.horizontal, 24)
|
|
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
HStack(spacing: 16) {
|
|
ForEach(0..<viewModel.freeContentList.count, id: \.self) { index in
|
|
ContentItemView(item: viewModel.freeContentList[index])
|
|
}
|
|
}
|
|
.padding(.horizontal, 24)
|
|
}
|
|
}
|
|
}
|
|
|
|
if !viewModel.curationList.isEmpty {
|
|
ForEach(0..<viewModel.curationList.count, id: \.self) { curationIndex in
|
|
let curation = viewModel.curationList[curationIndex]
|
|
VStack(alignment: .leading, spacing: 16) {
|
|
HStack(spacing: 0) {
|
|
Text(curation.title)
|
|
.font(.custom(Font.preBold.rawValue, size: 24))
|
|
.foregroundColor(.white)
|
|
}
|
|
.padding(.horizontal, 24)
|
|
|
|
ScrollView(.horizontal, showsIndicators: false) {
|
|
HStack(spacing: 16) {
|
|
ForEach(0..<curation.items.count, id: \.self) { index in
|
|
let item = curation.items[index]
|
|
ContentItemView(
|
|
item: AudioContentMainItem(
|
|
contentId: item.contentId,
|
|
creatorId: item.creatorId,
|
|
title: item.title,
|
|
coverImageUrl: item.coverImageUrl,
|
|
creatorNickname: item.creatorNickname,
|
|
isPointAvailable: item.isPointAvailable
|
|
)
|
|
)
|
|
}
|
|
}
|
|
.padding(.horizontal, 24)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
Text("""
|
|
- 회사명 : 주식회사 소다라이브
|
|
|
|
- 대표자 : 이재형
|
|
|
|
- 주소 : 경기도 성남시 분당구 황새울로335번길 10, 5층 563A호
|
|
|
|
- 사업자등록번호 : 870-81-03220
|
|
|
|
- 통신판매업신고 : 제2024-성남분당B-1012호
|
|
|
|
- 고객센터 : 02.2055.1477 (이용시간 10:00~19:00)
|
|
|
|
- 대표 이메일 : sodalive.official@gmail.com
|
|
""")
|
|
.font(.custom(Font.preRegular.rawValue, size: 11))
|
|
.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("콘텐츠 업로드")
|
|
.font(.custom(Font.preBold.rawValue, size: 13.3))
|
|
.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: "본인인증",
|
|
desc: "보이스온의 오픈월드 캐릭터톡은\n청소년 보호를 위해 본인인증한\n성인만 이용이 가능합니다.\n" +
|
|
"캐릭터톡 서비스를 이용하시려면\n본인인증을 하고 이용해주세요.",
|
|
confirmButtonTitle: "본인인증 하러가기",
|
|
confirmButtonAction: {
|
|
isShowAuthConfirmView = false
|
|
isShowAuthView = true
|
|
},
|
|
cancelButtonTitle: "취소",
|
|
cancelButtonAction: {
|
|
isShowAuthConfirmView = false
|
|
pendingAction = nil
|
|
},
|
|
textAlignment: .center
|
|
)
|
|
}
|
|
}
|
|
.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 = "본인인증 중 오류가 발생했습니다."
|
|
AppState.shared.isShowErrorPopup = true
|
|
isShowAuthView = false
|
|
}
|
|
.onDone { _ in
|
|
auth = true
|
|
isShowAuthView = false
|
|
if let action = pendingAction {
|
|
pendingAction = nil
|
|
action()
|
|
}
|
|
}
|
|
.onClose {
|
|
isShowAuthView = false
|
|
}
|
|
}
|
|
}
|
|
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) {
|
|
HStack {
|
|
Spacer()
|
|
Text(viewModel.errorMessage)
|
|
.padding(.vertical, 13.3)
|
|
.frame(width: screenSize().width - 66.7, alignment: .center)
|
|
.font(.custom(Font.preRegular.rawValue, size: 12))
|
|
.background(Color.button)
|
|
.foregroundColor(Color.white)
|
|
.multilineTextAlignment(.leading)
|
|
.cornerRadius(20)
|
|
.padding(.top, 66.7)
|
|
Spacer()
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
#Preview {
|
|
HomeTabView()
|
|
}
|