feat(home): 홈 탭 shell을 연결한다

This commit is contained in:
Yu Sung
2026-06-27 00:09:04 +09:00
parent 5757705b12
commit 394e7ebc34
8 changed files with 354 additions and 13 deletions

View File

@@ -0,0 +1,13 @@
import SwiftUI
struct MainHomeFollowingView: View {
var body: some View {
MainPlaceholderTabView(title: "팔로잉")
}
}
struct MainHomeFollowingView_Previews: PreviewProvider {
static var previews: some View {
MainHomeFollowingView()
}
}

View File

@@ -0,0 +1,91 @@
import SwiftUI
struct MainHomeView: View {
let onTapCanCharge: () -> Void
let onTapSearch: () -> Void
let onTapNotificationList: () -> Void
let onTapLive: (Int) -> Void
let onTapCreator: (Int) -> Void
let onTapContent: (Int) -> Void
let onTapCharacter: (Int) -> Void
let onTapCommunity: (Int) -> Void
@State private var selectedTab: MainHomeTab = .recommendation
var body: some View {
VStack(spacing: 0) {
HomeTitleBar {
HStack(spacing: 14) {
Image("ic_bar_cash")
.onTapGesture { onTapCanCharge() }
Image("ic_bar_search")
.onTapGesture { onTapSearch() }
Image("ic_bar_bell")
.onTapGesture { onTapNotificationList() }
}
}
TextTabBar(
items: MainHomeTab.allCases,
selectedItem: $selectedTab,
title: { $0.title }
)
tabContent
.frame(maxWidth: .infinity, maxHeight: .infinity)
}
.background(Color.black.ignoresSafeArea())
}
@ViewBuilder
private var tabContent: some View {
switch selectedTab {
case .recommendation:
MainHomeRecommendationView(
onTapLive: onTapLive,
onTapCreator: onTapCreator,
onTapContent: onTapContent,
onTapCharacter: onTapCharacter,
onTapCommunity: onTapCommunity
)
case .ranking:
MainHomeRankingView()
case .following:
MainHomeFollowingView()
}
}
}
private enum MainHomeTab: CaseIterable, Hashable {
case recommendation
case ranking
case following
var title: String {
switch self {
case .recommendation:
return "추천"
case .ranking:
return "랭킹"
case .following:
return "팔로잉"
}
}
}
struct MainHomeView_Previews: PreviewProvider {
static var previews: some View {
MainHomeView(
onTapCanCharge: {},
onTapSearch: {},
onTapNotificationList: {},
onTapLive: { _ in },
onTapCreator: { _ in },
onTapContent: { _ in },
onTapCharacter: { _ in },
onTapCommunity: { _ in }
)
}
}

View File

@@ -0,0 +1,13 @@
import SwiftUI
struct MainHomeRankingView: View {
var body: some View {
MainPlaceholderTabView(title: "랭킹")
}
}
struct MainHomeRankingView_Previews: PreviewProvider {
static var previews: some View {
MainHomeRankingView()
}
}

View File

@@ -0,0 +1,45 @@
import SwiftUI
struct MainHomeRecommendationView: View {
let onTapLive: (Int) -> Void
let onTapCreator: (Int) -> Void
let onTapContent: (Int) -> Void
let onTapCharacter: (Int) -> Void
let onTapCommunity: (Int) -> Void
@StateObject private var viewModel = MainHomeRecommendationViewModel()
var body: some View {
ZStack {
Color.black
.ignoresSafeArea()
if viewModel.isLoading && viewModel.recommendations == nil {
ProgressView()
.progressViewStyle(CircularProgressViewStyle(tint: .white))
}
}
.onAppear {
if viewModel.recommendations == nil {
viewModel.fetchRecommendations()
}
}
.sodaToast(
isPresented: $viewModel.isShowPopup,
message: viewModel.errorMessage,
autohideIn: 2
)
}
}
struct MainHomeRecommendationView_Previews: PreviewProvider {
static var previews: some View {
MainHomeRecommendationView(
onTapLive: { _ in },
onTapCreator: { _ in },
onTapContent: { _ in },
onTapCharacter: { _ in },
onTapCommunity: { _ in }
)
}
}

View File

@@ -1,7 +1,7 @@
import Foundation
import Combine
final class MainHomeViewModel: ObservableObject {
final class MainHomeRecommendationViewModel: ObservableObject {
private let repository = MainHomeRecommendationRepository()
private var subscription = Set<AnyCancellable>()

View File

@@ -153,7 +153,16 @@ struct MainView: View {
private var contentView: some View {
switch viewModel.currentTab {
case .home:
MainPlaceholderTabView(title: MainTab.home.title)
MainHomeView(
onTapCanCharge: handleHomeCanChargeTap,
onTapSearch: handleHomeSearchTap,
onTapNotificationList: handleHomeNotificationListTap,
onTapLive: handleRecommendationLiveTap,
onTapCreator: handleRecommendationCreatorTap,
onTapContent: handleRecommendationContentTap,
onTapCharacter: handleRecommendationCharacterTap,
onTapCommunity: handleRecommendationCommunityTap
)
case .content:
MainPlaceholderTabView(title: MainTab.content.title)
case .chat:
@@ -416,6 +425,106 @@ struct MainView: View {
navigationAction()
}
private func handleHomeCanChargeTap() {
appState.setAppStep(step: .canCharge(refresh: {}))
}
private func handleHomeSearchTap() {
appState.setAppStep(step: .search)
}
private func handleHomeNotificationListTap() {
appState.setAppStep(step: .notificationList)
}
private func handleRecommendationLiveTap(roomId: Int) {
guard roomId > 0 else { return }
performRecommendationDetailAction {
openLiveDetail(roomId: roomId)
}
}
private func handleRecommendationCreatorTap(creatorId: Int) {
guard creatorId > 0 else { return }
performRecommendationDetailAction {
appState.setAppStep(step: .creatorDetail(userId: creatorId))
}
}
private func handleRecommendationContentTap(contentId: Int) {
guard contentId > 0 else { return }
performRecommendationDetailAction {
appState.setAppStep(step: .contentDetail(contentId: contentId))
}
}
private func handleRecommendationCharacterTap(characterId: Int) {
guard characterId > 0 else { return }
performRecommendationDetailAction {
appState.setAppStep(step: .characterDetail(characterId: characterId))
}
}
private func handleRecommendationCommunityTap(creatorId: Int) {
guard creatorId > 0 else { return }
performRecommendationDetailAction {
appState.setAppStep(step: .creatorCommunityAll(creatorId: creatorId))
}
}
private func performRecommendationDetailAction(
requiresAdultContent: Bool = false,
action: @escaping () -> Void
) {
let trimmed = token.trimmingCharacters(in: .whitespacesAndNewlines)
guard !trimmed.isEmpty else {
AppState.shared.setAppStep(step: .login)
return
}
if requiresAdultContent {
let normalizedCountryCode = UserDefaults
.string(forKey: .countryCode)
.trimmingCharacters(in: .whitespacesAndNewlines)
.uppercased()
let isKoreanCountry = normalizedCountryCode.isEmpty || normalizedCountryCode == "KR"
if isKoreanCountry && auth == false {
pendingAction = action
isShowAuthConfirmView = true
return
}
if !UserDefaults.isAdultContentVisible() {
pendingAction = nil
moveToContentSettingsWithGuideToast()
return
}
}
action()
}
private func openLiveDetail(roomId: Int) {
appState.setAppStep(
step: .liveDetail(
roomId: roomId,
onClickParticipant: {
appState.isShowPlayer = false
liveViewModel.enterLiveRoom(roomId: roomId)
},
onClickReservation: {},
onClickStart: {},
onClickCancel: {}
)
)
}
private func moveToContentSettingsWithGuideToast() {
AppState.shared.setPendingContentSettingsGuideMessage(I18n.Settings.adultContentEnableGuide)
AppState.shared.setAppStep(step: .contentViewSettings)
}
private func confirmExternalNavigation() {
guard pendingExternalNavigationAction != nil else {
isShowLeaveLiveNavigationDialog = false