From e121ec1ee45da79071af5e44b2c5d24059dc330e Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Fri, 11 Jul 2025 12:18:37 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=8B=A0=EA=B7=9C=20=ED=99=88=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../ic_point.imageset/Contents.json | 2 +- .../ic_point.imageset/ic_point.png | Bin 2927 -> 1544 bytes .../Sources/Content/ContentItemView.swift | 86 ++++++++ .../Sources/Home/AudioContentMainItem.swift | 15 ++ SodaLive/Sources/Home/GetHomeResponse.swift | 21 ++ SodaLive/Sources/Home/HomeApi.swift | 73 +++++++ SodaLive/Sources/Home/HomeCurationView.swift | 18 ++ SodaLive/Sources/Home/HomeTabRepository.swift | 15 ++ SodaLive/Sources/Home/HomeTabView.swift | 204 ++++++++++++++++++ SodaLive/Sources/Home/HomeTabViewModel.swift | 35 +++ .../Home/RecommendChannelResponse.swift | 22 ++ SodaLive/Sources/Main/Home/HomeView.swift | 4 +- 12 files changed, 492 insertions(+), 3 deletions(-) create mode 100644 SodaLive/Sources/Content/ContentItemView.swift create mode 100644 SodaLive/Sources/Home/AudioContentMainItem.swift create mode 100644 SodaLive/Sources/Home/GetHomeResponse.swift create mode 100644 SodaLive/Sources/Home/HomeApi.swift create mode 100644 SodaLive/Sources/Home/HomeCurationView.swift create mode 100644 SodaLive/Sources/Home/HomeTabRepository.swift create mode 100644 SodaLive/Sources/Home/HomeTabView.swift create mode 100644 SodaLive/Sources/Home/HomeTabViewModel.swift create mode 100644 SodaLive/Sources/Home/RecommendChannelResponse.swift diff --git a/SodaLive/Resources/Assets.xcassets/ic_point.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_point.imageset/Contents.json index 7477522..963aa8f 100644 --- a/SodaLive/Resources/Assets.xcassets/ic_point.imageset/Contents.json +++ b/SodaLive/Resources/Assets.xcassets/ic_point.imageset/Contents.json @@ -1,6 +1,7 @@ { "images" : [ { + "filename" : "ic_point.png", "idiom" : "universal", "scale" : "1x" }, @@ -9,7 +10,6 @@ "scale" : "2x" }, { - "filename" : "ic_point.png", "idiom" : "universal", "scale" : "3x" } diff --git a/SodaLive/Resources/Assets.xcassets/ic_point.imageset/ic_point.png b/SodaLive/Resources/Assets.xcassets/ic_point.imageset/ic_point.png index d41e9a9fd75984648f512c1e816f7fad1a99bcec..27aa8dc9d77db9ad594a4d85b4736a8f5dedaea7 100644 GIT binary patch delta 1512 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 +} diff --git a/SodaLive/Sources/Home/HomeTabView.swift b/SodaLive/Sources/Home/HomeTabView.swift new file mode 100644 index 0000000..1a97643 --- /dev/null +++ b/SodaLive/Sources/Home/HomeTabView.swift @@ -0,0 +1,204 @@ +// +// HomeTabView.swift +// SodaLive +// +// Created by klaus on 7/10/25. +// + +import SwiftUI + +struct HomeTabView: View { + @StateObject var viewModel = ContentMainTabHomeViewModel() + @AppStorage("token") private var token: String = UserDefaults.string(forKey: UserDefaultsKey.token) + @AppStorage("role") private var role: String = UserDefaults.string(forKey: UserDefaultsKey.role) + + var body: some View { + BaseView(isLoading: $viewModel.isLoading) { + ZStack(alignment: .bottomTrailing) { + VStack(alignment: .leading, spacing: 0) { + HStack(spacing: 0) { + Image("img_text_logo") + + Spacer() + + if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { + Image("ic_can") + .onTapGesture { + AppState + .shared + .setAppStep(step: .canCharge(refresh: {})) + } + } + } + .padding(.horizontal, 24) + .padding(.vertical, 20) + + ScrollView(.vertical, showsIndicators: false) { + VStack(alignment: .leading, spacing: 48) { + VStack(alignment: .leading, spacing: 16) { + HStack(spacing: 0) { + Text("지금") + .font(.custom(Font.bold.rawValue, size: 26)) + .foregroundColor(.button) + + Text(" 라이브중") + .font(.custom(Font.bold.rawValue, size: 26)) + .foregroundColor(.white) + } + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 16) { + + } + } + } + + VStack(alignment: .leading, spacing: 16) { + HStack(spacing: 0) { + Text("인기") + .font(.custom(Font.bold.rawValue, size: 26)) + .foregroundColor(.button) + + Text(" 크리에이터") + .font(.custom(Font.bold.rawValue, size: 26)) + .foregroundColor(.white) + } + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 16) { + + } + } + } + + VStack(alignment: .leading, spacing: 16) { + HStack(spacing: 0) { + Text("최신") + .font(.custom(Font.bold.rawValue, size: 26)) + .foregroundColor(.button) + + Text(" 콘텐츠") + .font(.custom(Font.bold.rawValue, size: 26)) + .foregroundColor(.white) + } + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 16) { + + } + } + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 16) { + + } + } + } + + VStack(alignment: .leading, spacing: 16) { + HStack(spacing: 0) { + Text("오직") + .font(.custom(Font.bold.rawValue, size: 26)) + .foregroundColor(.button) + + Text(" 보이스온에서만") + .font(.custom(Font.bold.rawValue, size: 26)) + .foregroundColor(.white) + } + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 16) { + + } + } + } + + VStack(alignment: .leading, spacing: 16) { + HStack(spacing: 0) { + Text("요일별") + .font(.custom(Font.bold.rawValue, size: 26)) + .foregroundColor(.button) + + Text(" 시리즈") + .font(.custom(Font.bold.rawValue, size: 26)) + .foregroundColor(.white) + } + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 16) { + + } + } + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 16) { + + } + } + } + + VStack(alignment: .leading, spacing: 16) { + HStack(spacing: 0) { + Text("보온") + .font(.custom(Font.bold.rawValue, size: 26)) + .foregroundColor(.button) + + Text(" 주간 차트") + .font(.custom(Font.bold.rawValue, size: 26)) + .foregroundColor(.white) + } + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 16) { + + } + } + } + + VStack(alignment: .leading, spacing: 16) { + HStack(spacing: 0) { + Text("추천") + .font(.custom(Font.bold.rawValue, size: 26)) + .foregroundColor(.button) + + Text(" 채널") + .font(.custom(Font.bold.rawValue, size: 26)) + .foregroundColor(.white) + } + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 16) { + + } + } + } + + VStack(alignment: .leading, spacing: 16) { + HStack(spacing: 0) { + Text("무료") + .font(.custom(Font.bold.rawValue, size: 26)) + .foregroundColor(.button) + + Text(" 콘텐츠") + .font(.custom(Font.bold.rawValue, size: 26)) + .foregroundColor(.white) + } + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 16) { + + } + } + } + } + .padding(24) + } + } + } + } + } +} + +#Preview { + HomeTabView() +} diff --git a/SodaLive/Sources/Home/HomeTabViewModel.swift b/SodaLive/Sources/Home/HomeTabViewModel.swift new file mode 100644 index 0000000..80a6c62 --- /dev/null +++ b/SodaLive/Sources/Home/HomeTabViewModel.swift @@ -0,0 +1,35 @@ +// +// HomeTabViewModel.swift +// SodaLive +// +// Created by klaus on 7/10/25. +// + +import Foundation +import Combine + +enum SeriesPublishedDaysOfWeek: String, Encodable { + case SUN, MON, TUE, WED, THU, FRI, SAT, RANDOM +} + +final class HomeTabViewModel: ObservableObject { + private let repository = HomeTabRepository() + private var subscription = Set() + + @Published var errorMessage = "" + @Published var isShowPopup = false + @Published var isLoading = false + + @Published var liveList: [GetRoomListResponse] = [] + @Published var creatorRanking: [GetExplorerSectionCreatorResponse] = [] + @Published var latestContentThemeList: [String] = [] + @Published var latestContentList: [AudioContentMainItem] = [] + @Published var eventBannerList: GetEventResponse? = nil + @Published var originalAudioDramaList: [SeriesListItem] = [] + @Published var auditionList: [GetAuditionListItem] = [] + @Published var dayOfWeekSeriesList: [SeriesListItem] = [] + @Published var contentRanking: [GetAudioContentRankingItem] = [] + @Published var recommendChannelList: [RecommendChannelResponse] = [] + @Published var freeContentList: [AudioContentMainItem] = [] + @Published var curationList: [GetContentCurationResponse] = [] +} diff --git a/SodaLive/Sources/Home/RecommendChannelResponse.swift b/SodaLive/Sources/Home/RecommendChannelResponse.swift new file mode 100644 index 0000000..5add07c --- /dev/null +++ b/SodaLive/Sources/Home/RecommendChannelResponse.swift @@ -0,0 +1,22 @@ +// +// RecommendChannelResponse.swift +// SodaLive +// +// Created by klaus on 7/11/25. +// + +struct RecommendChannelResponse: Decodable { + let channelId: Int + let creatorNickname: String + let creatorProfileImageUrl: String + let contentCount: Int + let contentList: [RecommendChannelContentItem] +} + +struct RecommendChannelContentItem: Decodable { + let contentId: Int + let title: String + let thumbnailImageUrl: String + let likeCount: Int + let commentCount: Int +} diff --git a/SodaLive/Sources/Main/Home/HomeView.swift b/SodaLive/Sources/Main/Home/HomeView.swift index 479d848..5d0b2bc 100644 --- a/SodaLive/Sources/Main/Home/HomeView.swift +++ b/SodaLive/Sources/Main/Home/HomeView.swift @@ -18,7 +18,7 @@ struct HomeView: View { @StateObject var contentPlayerPlayManager = ContentPlayerPlayManager.shared private let liveView = LiveView() - private let contentView = ContentMainTabHomeView() + private let homeTabView = HomeTabView() @State private var isShowPlayer = false @AppStorage("token") private var token: String = UserDefaults.string(forKey: UserDefaultsKey.token) @@ -28,7 +28,7 @@ struct HomeView: View { ZStack(alignment: .bottom) { VStack(spacing: 0) { ZStack { - contentView + homeTabView .frame(width: viewModel.currentTab == .home ? proxy.size.width : 0) .opacity(viewModel.currentTab == .home ? 1.0 : 0.01)