feat: 신규 홈 추가
This commit is contained in:
		@@ -1,6 +1,7 @@
 | 
				
			|||||||
{
 | 
					{
 | 
				
			||||||
  "images" : [
 | 
					  "images" : [
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
 | 
					      "filename" : "ic_point.png",
 | 
				
			||||||
      "idiom" : "universal",
 | 
					      "idiom" : "universal",
 | 
				
			||||||
      "scale" : "1x"
 | 
					      "scale" : "1x"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
@@ -9,7 +10,6 @@
 | 
				
			|||||||
      "scale" : "2x"
 | 
					      "scale" : "2x"
 | 
				
			||||||
    },
 | 
					    },
 | 
				
			||||||
    {
 | 
					    {
 | 
				
			||||||
      "filename" : "ic_point.png",
 | 
					 | 
				
			||||||
      "idiom" : "universal",
 | 
					      "idiom" : "universal",
 | 
				
			||||||
      "scale" : "3x"
 | 
					      "scale" : "3x"
 | 
				
			||||||
    }
 | 
					    }
 | 
				
			||||||
 
 | 
				
			|||||||
										
											Binary file not shown.
										
									
								
							| 
		 Before Width: | Height: | Size: 2.9 KiB After Width: | Height: | Size: 1.5 KiB  | 
							
								
								
									
										86
									
								
								SodaLive/Sources/Content/ContentItemView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										86
									
								
								SodaLive/Sources/Content/ContentItemView.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,86 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					//  ContentItemView.swift
 | 
				
			||||||
 | 
					//  SodaLive
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//  Created by klaus on 7/10/25.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import SwiftUI
 | 
				
			||||||
 | 
					import Kingfisher
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct ContentItemView: View {
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    let item: AudioContentMainItem
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    var body: some View {
 | 
				
			||||||
 | 
					        VStack(alignment: .leading, spacing: 0) {
 | 
				
			||||||
 | 
					            ZStack(alignment: .top) {
 | 
				
			||||||
 | 
					                KFImage(URL(string: item.coverImageUrl))
 | 
				
			||||||
 | 
					                    .cancelOnDisappear(true)
 | 
				
			||||||
 | 
					                    .resizable()
 | 
				
			||||||
 | 
					                    .scaledToFill()
 | 
				
			||||||
 | 
					                    .frame(width: 168, height: 168, alignment: .top)
 | 
				
			||||||
 | 
					                    .cornerRadius(16)
 | 
				
			||||||
 | 
					                
 | 
				
			||||||
 | 
					                HStack(alignment: .top, spacing: 0) {
 | 
				
			||||||
 | 
					                    Text("신작")
 | 
				
			||||||
 | 
					                        .font(.custom(Font.medium.rawValue, size: 12))
 | 
				
			||||||
 | 
					                        .foregroundColor(.white)
 | 
				
			||||||
 | 
					                        .padding(.horizontal, 10)
 | 
				
			||||||
 | 
					                        .padding(.vertical, 3)
 | 
				
			||||||
 | 
					                        .background(
 | 
				
			||||||
 | 
					                            LinearGradient(
 | 
				
			||||||
 | 
					                                gradient: Gradient(stops: [
 | 
				
			||||||
 | 
					                                    .init(color: Color(hex: "0001B1"), location: 0.24),
 | 
				
			||||||
 | 
					                                    .init(color: Color(hex: "3B5FF1"), location: 1.0)
 | 
				
			||||||
 | 
					                                ]),
 | 
				
			||||||
 | 
					                                startPoint: .top,
 | 
				
			||||||
 | 
					                                endPoint: .bottom
 | 
				
			||||||
 | 
					                            )
 | 
				
			||||||
 | 
					                        )
 | 
				
			||||||
 | 
					                        .cornerRadius(16, corners: [.topLeft, .bottomRight])
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    Spacer()
 | 
				
			||||||
 | 
					                    
 | 
				
			||||||
 | 
					                    if item.isPointAvailable {
 | 
				
			||||||
 | 
					                        Image("ic_point")
 | 
				
			||||||
 | 
					                            .padding(.top, 6)
 | 
				
			||||||
 | 
					                            .padding(.trailing, 6)
 | 
				
			||||||
 | 
					                    }
 | 
				
			||||||
 | 
					                }
 | 
				
			||||||
 | 
					            }
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            Text(item.title)
 | 
				
			||||||
 | 
					                .font(.custom(Font.medium.rawValue, size: 18))
 | 
				
			||||||
 | 
					                .foregroundColor(.white)
 | 
				
			||||||
 | 
					                .multilineTextAlignment(.leading)
 | 
				
			||||||
 | 
					                .fixedSize(horizontal: false, vertical: true)
 | 
				
			||||||
 | 
					                .lineLimit(1)
 | 
				
			||||||
 | 
					                .padding(.horizontal, 6)
 | 
				
			||||||
 | 
					                .padding(.top, 8)
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            Text(item.creatorNickname)
 | 
				
			||||||
 | 
					                .font(.custom(Font.medium.rawValue, size: 14))
 | 
				
			||||||
 | 
					                .foregroundColor(Color(hex: "78909C"))
 | 
				
			||||||
 | 
					                .lineLimit(1)
 | 
				
			||||||
 | 
					                .padding(.horizontal, 6)
 | 
				
			||||||
 | 
					                .padding(.top, 4)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					        .frame(width: 168)
 | 
				
			||||||
 | 
					        .onTapGesture { AppState.shared.setAppStep(step: .contentDetail(contentId: item.contentId)) }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#Preview {
 | 
				
			||||||
 | 
					    ContentItemView(
 | 
				
			||||||
 | 
					        item: AudioContentMainItem(
 | 
				
			||||||
 | 
					            contentId: 1,
 | 
				
			||||||
 | 
					            creatorId: 1,
 | 
				
			||||||
 | 
					            title: "동정개발일지",
 | 
				
			||||||
 | 
					            coverImageUrl: "https://cf.sodalive.net/audio_content_cover/5696/5696-cover-50066e61-6633-445b-9ae1-3749554d3f08-9514-1750756003835",
 | 
				
			||||||
 | 
					            creatorNickname: "오늘밤결제했습니다",
 | 
				
			||||||
 | 
					            isPointAvailable: true
 | 
				
			||||||
 | 
					        )
 | 
				
			||||||
 | 
					    )
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										15
									
								
								SodaLive/Sources/Home/AudioContentMainItem.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								SodaLive/Sources/Home/AudioContentMainItem.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					//  AudioContentMainItem.swift
 | 
				
			||||||
 | 
					//  SodaLive
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//  Created by klaus on 7/10/25.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct AudioContentMainItem: Decodable {
 | 
				
			||||||
 | 
					    let contentId: Int
 | 
				
			||||||
 | 
					    let creatorId: Int
 | 
				
			||||||
 | 
					    let title: String
 | 
				
			||||||
 | 
					    let coverImageUrl: String
 | 
				
			||||||
 | 
					    let creatorNickname: String
 | 
				
			||||||
 | 
					    let isPointAvailable: Bool
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										21
									
								
								SodaLive/Sources/Home/GetHomeResponse.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								SodaLive/Sources/Home/GetHomeResponse.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,21 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					//  GetHomeResponse.swift
 | 
				
			||||||
 | 
					//  SodaLive
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//  Created by klaus on 7/11/25.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct GetHomeResponse: Decodable {
 | 
				
			||||||
 | 
					    let liveList: [GetRoomListResponse]
 | 
				
			||||||
 | 
					    let creatorRanking: [GetExplorerSectionCreatorResponse]
 | 
				
			||||||
 | 
					    let latestContentThemeList: [String]
 | 
				
			||||||
 | 
					    let latestContentList: [AudioContentMainItem]
 | 
				
			||||||
 | 
					    let eventBannerList: GetEventResponse
 | 
				
			||||||
 | 
					    let originalAudioDramaList: [SeriesListItem]
 | 
				
			||||||
 | 
					    let auditionList: [GetAuditionListItem]
 | 
				
			||||||
 | 
					    let dayOfWeekSeriesList: [SeriesListItem]
 | 
				
			||||||
 | 
					    let contentRanking: [GetAudioContentRankingItem]
 | 
				
			||||||
 | 
					    let recommendChannelList: [RecommendChannelResponse]
 | 
				
			||||||
 | 
					    let freeContentList: [AudioContentMainItem]
 | 
				
			||||||
 | 
					    let curationList: [GetContentCurationResponse]
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										73
									
								
								SodaLive/Sources/Home/HomeApi.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										73
									
								
								SodaLive/Sources/Home/HomeApi.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,73 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					//  HomeApi.swift
 | 
				
			||||||
 | 
					//  SodaLive
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//  Created by klaus on 7/10/25.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Foundation
 | 
				
			||||||
 | 
					import Moya
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					enum HomeApi {
 | 
				
			||||||
 | 
					    case getHomeData(isAdultContentVisible: Bool, contentType: ContentType)
 | 
				
			||||||
 | 
					    case getLatestContentByTheme(theme: String, isAdultContentVisible: Bool, contentType: ContentType)
 | 
				
			||||||
 | 
					    case getDayOfWeekSeriesList(dayOfWeek: SeriesPublishedDaysOfWeek, isAdultContentVisible: Bool, contentType: ContentType)
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					extension HomeApi: TargetType {
 | 
				
			||||||
 | 
					    var baseURL: URL {
 | 
				
			||||||
 | 
					        return URL(string: BASE_URL)!
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    var path: String {
 | 
				
			||||||
 | 
					        switch self {
 | 
				
			||||||
 | 
					        case .getHomeData:
 | 
				
			||||||
 | 
					            return "/api/home"
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        case .getLatestContentByTheme:
 | 
				
			||||||
 | 
					            return "/api/home/latest-content"
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        case .getDayOfWeekSeriesList:
 | 
				
			||||||
 | 
					            return "/api/home/day-of-week-series"
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    var method: Moya.Method {
 | 
				
			||||||
 | 
					        return .get
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    var task: Moya.Task {
 | 
				
			||||||
 | 
					        switch self {
 | 
				
			||||||
 | 
					        case .getHomeData(let isAdultContentVisible, let contentType):
 | 
				
			||||||
 | 
					            let parameters = [
 | 
				
			||||||
 | 
					                "timezone": TimeZone.current.identifier,
 | 
				
			||||||
 | 
					                "isAdultContentVisible": isAdultContentVisible,
 | 
				
			||||||
 | 
					                "contentType": contentType
 | 
				
			||||||
 | 
					            ] as [String: Any]
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        case .getLatestContentByTheme(let theme, let isAdultContentVisible, let contentType):
 | 
				
			||||||
 | 
					            let parameters = [
 | 
				
			||||||
 | 
					                "theme": theme,
 | 
				
			||||||
 | 
					                "isAdultContentVisible": isAdultContentVisible,
 | 
				
			||||||
 | 
					                "contentType": contentType
 | 
				
			||||||
 | 
					            ] as [String: Any]
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					        case .getDayOfWeekSeriesList(let dayOfWeek, let isAdultContentVisible, let contentType):
 | 
				
			||||||
 | 
					            let parameters = [
 | 
				
			||||||
 | 
					                "dayOfWeek": dayOfWeek,
 | 
				
			||||||
 | 
					                "isAdultContentVisible": isAdultContentVisible,
 | 
				
			||||||
 | 
					                "contentType": contentType
 | 
				
			||||||
 | 
					            ] as [String: Any]
 | 
				
			||||||
 | 
					            
 | 
				
			||||||
 | 
					            return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
 | 
				
			||||||
 | 
					        }
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    var headers: [String : String]? {
 | 
				
			||||||
 | 
					        return ["Authorization": "Bearer \(UserDefaults.string(forKey: UserDefaultsKey.token))"]
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										18
									
								
								SodaLive/Sources/Home/HomeCurationView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										18
									
								
								SodaLive/Sources/Home/HomeCurationView.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,18 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					//  HomeCurationView.swift
 | 
				
			||||||
 | 
					//  SodaLive
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//  Created by klaus on 7/10/25.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import SwiftUI
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					struct HomeCurationView: View {
 | 
				
			||||||
 | 
					    var body: some View {
 | 
				
			||||||
 | 
					        VStack(spacing: 30) {}
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					#Preview {
 | 
				
			||||||
 | 
					    HomeCurationView()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										15
									
								
								SodaLive/Sources/Home/HomeTabRepository.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										15
									
								
								SodaLive/Sources/Home/HomeTabRepository.swift
									
									
									
									
									
										Normal file
									
								
							@@ -0,0 +1,15 @@
 | 
				
			|||||||
 | 
					//
 | 
				
			||||||
 | 
					//  HomeTabRepository.swift
 | 
				
			||||||
 | 
					//  SodaLive
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					//  Created by klaus on 7/10/25.
 | 
				
			||||||
 | 
					//
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					import Foundation
 | 
				
			||||||
 | 
					import CombineMoya
 | 
				
			||||||
 | 
					import Combine
 | 
				
			||||||
 | 
					import Moya
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					class HomeTabRepository {
 | 
				
			||||||
 | 
					    private let api = MoyaProvider<HomeApi>()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										204
									
								
								SodaLive/Sources/Home/HomeTabView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										204
									
								
								SodaLive/Sources/Home/HomeTabView.swift
									
									
									
									
									
										Normal file
									
								
							@@ -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()
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										35
									
								
								SodaLive/Sources/Home/HomeTabViewModel.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										35
									
								
								SodaLive/Sources/Home/HomeTabViewModel.swift
									
									
									
									
									
										Normal file
									
								
							@@ -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<AnyCancellable>()
 | 
				
			||||||
 | 
					    
 | 
				
			||||||
 | 
					    @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] = []
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										22
									
								
								SodaLive/Sources/Home/RecommendChannelResponse.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								SodaLive/Sources/Home/RecommendChannelResponse.swift
									
									
									
									
									
										Normal file
									
								
							@@ -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
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
@@ -18,7 +18,7 @@ struct HomeView: View {
 | 
				
			|||||||
    @StateObject var contentPlayerPlayManager = ContentPlayerPlayManager.shared
 | 
					    @StateObject var contentPlayerPlayManager = ContentPlayerPlayManager.shared
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    private let liveView = LiveView()
 | 
					    private let liveView = LiveView()
 | 
				
			||||||
    private let contentView = ContentMainTabHomeView()
 | 
					    private let homeTabView = HomeTabView()
 | 
				
			||||||
    
 | 
					    
 | 
				
			||||||
    @State private var isShowPlayer = false
 | 
					    @State private var isShowPlayer = false
 | 
				
			||||||
    @AppStorage("token") private var token: String = UserDefaults.string(forKey: UserDefaultsKey.token)
 | 
					    @AppStorage("token") private var token: String = UserDefaults.string(forKey: UserDefaultsKey.token)
 | 
				
			||||||
@@ -28,7 +28,7 @@ struct HomeView: View {
 | 
				
			|||||||
            ZStack(alignment: .bottom) {
 | 
					            ZStack(alignment: .bottom) {
 | 
				
			||||||
                VStack(spacing: 0) {
 | 
					                VStack(spacing: 0) {
 | 
				
			||||||
                    ZStack {
 | 
					                    ZStack {
 | 
				
			||||||
                        contentView
 | 
					                        homeTabView
 | 
				
			||||||
                            .frame(width: viewModel.currentTab == .home ? proxy.size.width : 0)
 | 
					                            .frame(width: viewModel.currentTab == .home ? proxy.size.width : 0)
 | 
				
			||||||
                            .opacity(viewModel.currentTab == .home ? 1.0 : 0.01)
 | 
					                            .opacity(viewModel.currentTab == .home ? 1.0 : 0.01)
 | 
				
			||||||
                        
 | 
					                        
 | 
				
			||||||
 
 | 
				
			|||||||
		Reference in New Issue
	
	Block a user