콘텐츠 메인
- 시리즈 탭 UI 페이지 생성
This commit is contained in:
		| @@ -141,4 +141,6 @@ enum AppStep { | ||||
|     case auditionRoleDetail(roleId: Int, auditionTitle: String) | ||||
|      | ||||
|     case searchChannel | ||||
|      | ||||
|     case contentMain(startTab: ContentMainTab) | ||||
| } | ||||
|   | ||||
| @@ -38,8 +38,13 @@ enum ContentApi { | ||||
|     case unpinContent(contentId: Int) | ||||
|     case getAudioContentByTheme(themeId: Int, isAdultContentVisible: Bool, contentType: ContentType, page: Int, size: Int, sort: ContentAllByThemeViewModel.Sort) | ||||
|     case generateUrl(contentId: Int) | ||||
|      | ||||
|     case getContentMainHome | ||||
|     case getPopularContentByCreator(creatorId: Int) | ||||
|      | ||||
|     case getContentMainSeries | ||||
|     case getRecommendSeriesListByGenre(genreId: Int) | ||||
|     case getRecommendSeriesByCreator(creatorId: Int) | ||||
| } | ||||
|  | ||||
| extension ContentApi: TargetType { | ||||
| @@ -141,6 +146,15 @@ extension ContentApi: TargetType { | ||||
|              | ||||
|         case .getPopularContentByCreator: | ||||
|             return "/v2/audio-content/main/home/popular-content-by-creator" | ||||
|              | ||||
|         case .getContentMainSeries: | ||||
|             return "/v2/audio-content/main/series" | ||||
|              | ||||
|         case .getRecommendSeriesListByGenre: | ||||
|             return "/v2/audio-content/main/series/recommend-by-genre" | ||||
|              | ||||
|         case .getRecommendSeriesByCreator: | ||||
|             return "/v2/audio-content/main/series/recommend-series-by-creator" | ||||
|         } | ||||
|     } | ||||
|      | ||||
| @@ -155,7 +169,7 @@ extension ContentApi: TargetType { | ||||
|         case .getMainBannerList, .getMainOrderList, .getNewContentUploadCreatorList, .getCurationList, .getAudioContentByTheme: | ||||
|             return .get | ||||
|              | ||||
|         case .getContentMainHome, .getPopularContentByCreator: | ||||
|         case .getContentMainHome, .getPopularContentByCreator, .getContentMainSeries, .getRecommendSeriesListByGenre, .getRecommendSeriesByCreator: | ||||
|             return .get | ||||
|              | ||||
|         case .likeContent, .modifyAudioContent, .modifyComment, .unpinContent: | ||||
| @@ -312,12 +326,20 @@ extension ContentApi: TargetType { | ||||
|              | ||||
|             return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) | ||||
|              | ||||
|         case .getContentMainHome: | ||||
|         case .getContentMainHome, .getContentMainSeries: | ||||
|             return .requestPlain | ||||
|              | ||||
|         case .getRecommendSeriesListByGenre(let genreId): | ||||
|             let parameters = ["genreId": genreId] | ||||
|             return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) | ||||
|              | ||||
|         case .getPopularContentByCreator(let creatorId): | ||||
|             let parameters = ["creatorId": creatorId] | ||||
|             return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) | ||||
|              | ||||
|         case .getRecommendSeriesByCreator(let creatorId): | ||||
|             let parameters = ["creatorId": creatorId] | ||||
|             return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|   | ||||
							
								
								
									
										34
									
								
								SodaLive/Sources/Content/Main/V2/ContentMainNoItemView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										34
									
								
								SodaLive/Sources/Content/Main/V2/ContentMainNoItemView.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,34 @@ | ||||
| // | ||||
| //  ContentMainNoItemView.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2/21/25. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| struct ContentMainNoItemView: View { | ||||
|     var body: some View { | ||||
|         VStack(spacing: 0) { | ||||
|             Image("ic_no_item") | ||||
|                 .resizable() | ||||
|                 .frame(width: 60, height: 60) | ||||
|              | ||||
|             Text("마이페이지에서 본인인증을 해주세요") | ||||
|                 .font(.custom(Font.medium.rawValue, size: 13)) | ||||
|                 .foregroundColor(.graybb) | ||||
|                 .fixedSize(horizontal: false, vertical: true) | ||||
|                 .multilineTextAlignment(.center) | ||||
|                 .lineSpacing(8) | ||||
|                 .padding(.vertical, 8) | ||||
|         } | ||||
|         .padding(.vertical, 16.7) | ||||
|         .frame(maxWidth: .infinity) | ||||
|         .background(Color.bg) | ||||
|         .cornerRadius(4.7) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #Preview { | ||||
|     ContentMainNoItemView() | ||||
| } | ||||
| @@ -7,12 +7,131 @@ | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| enum ContentMainTab { | ||||
|     case HOME | ||||
|     case SERIES | ||||
|     case CONTENT | ||||
|     case ALARM | ||||
|     case ASMR | ||||
|     case REPLAY | ||||
|     case FREE | ||||
| } | ||||
|  | ||||
| struct TabItem { | ||||
|     let title: String | ||||
|     let tab: ContentMainTab | ||||
| } | ||||
|  | ||||
| struct ContentMainViewV2: View { | ||||
|      | ||||
|     @State private var selectedTab: ContentMainTab = .SERIES | ||||
|      | ||||
|     let tabItemList = [ | ||||
|         TabItem(title: "홈", tab: .HOME), | ||||
|         TabItem(title: "시리즈", tab: .SERIES), | ||||
|         TabItem(title: "단편", tab: .CONTENT), | ||||
|         TabItem(title: "모닝콜", tab: .ALARM), | ||||
|         TabItem(title: "ASMR", tab: .ASMR), | ||||
|         TabItem(title: "다시듣기", tab: .REPLAY), | ||||
|         TabItem(title: "무료", tab: .FREE) | ||||
|     ] | ||||
|      | ||||
|     init(selectedTab: ContentMainTab = .SERIES) { | ||||
|         self._selectedTab = State(initialValue: selectedTab) | ||||
|     } | ||||
|      | ||||
|     var body: some View { | ||||
|         Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) | ||||
|         ZStack { | ||||
|              | ||||
|             Color.black.ignoresSafeArea() | ||||
|              | ||||
|             VStack(spacing: 0) { | ||||
|                 HStack(spacing: 0) { | ||||
|                     Text("콘텐츠 마켓") | ||||
|                         .font(.custom(Font.bold.rawValue, size: 21.3)) | ||||
|                         .foregroundColor(Color.button) | ||||
|                      | ||||
|                     Spacer() | ||||
|                      | ||||
|                     Image("ic_content_keep") | ||||
|                         .onTapGesture { | ||||
|                             AppState.shared.setAppStep(step: .myBox(currentTab: .orderlist)) | ||||
|                         } | ||||
|                 } | ||||
|                 .padding(.horizontal, 13.3) | ||||
|                  | ||||
|                 ScrollViewReader { proxy in | ||||
|                     ScrollView(.horizontal, showsIndicators: false) { | ||||
|                         HStack(spacing: 8) { | ||||
|                             ForEach(0..<tabItemList.count, id: \.self) { index in | ||||
|                                 let tabItem = tabItemList[index] | ||||
|                                  | ||||
|                                 Text(tabItem.title) | ||||
|                                     .font( | ||||
|                                         .custom( | ||||
|                                             selectedTab == tabItem.tab ? | ||||
|                                             Font.bold.rawValue : | ||||
|                                                 Font.medium.rawValue, | ||||
|                                             size: 16 | ||||
|                                         ) | ||||
|                                     ) | ||||
|                                     .foregroundColor( | ||||
|                                         selectedTab == tabItem.tab ? | ||||
|                                             .button : | ||||
|                                                 .graybb | ||||
|                                     ) | ||||
|                                     .padding(.horizontal, 12) | ||||
|                                     .onTapGesture { | ||||
|                                         if selectedTab != tabItem.tab { | ||||
|                                             selectedTab = tabItem.tab | ||||
|                                             proxy.scrollTo(tabItem.tab, anchor: .center) | ||||
|                                         } | ||||
|                                     } | ||||
|                                     .id(tabItem.tab) | ||||
|                             } | ||||
|                         } | ||||
|                         .padding(.vertical, 15) | ||||
|                         .padding(.horizontal, 13.3) | ||||
|                     } | ||||
|                     .onAppear { | ||||
|                         withAnimation { | ||||
|                             proxy.scrollTo(selectedTab, anchor: .center) | ||||
|                         } | ||||
|                     } | ||||
|                     .onChange(of: selectedTab) { newTab in | ||||
|                         withAnimation { | ||||
|                             if newTab == .HOME { | ||||
|                                 AppState.shared.back() | ||||
|                             } else { | ||||
|                                 proxy.scrollTo(newTab, anchor: .center) | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                  | ||||
|                 ZStack { | ||||
|                     switch selectedTab { | ||||
|                     case .HOME: | ||||
|                         EmptyView() | ||||
|                     case .SERIES: | ||||
|                         ContentMainTabSeriesView() | ||||
|                     case .CONTENT: | ||||
|                         EmptyView() | ||||
|                     case .ALARM: | ||||
|                         EmptyView() | ||||
|                     case .ASMR: | ||||
|                         EmptyView() | ||||
|                     case .REPLAY: | ||||
|                         EmptyView() | ||||
|                     case .FREE: | ||||
|                         EmptyView() | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #Preview { | ||||
|     ContentMainViewV2() | ||||
|     ContentMainViewV2(selectedTab: .SERIES) | ||||
| } | ||||
|   | ||||
| @@ -76,28 +76,52 @@ struct ContentMainTabHomeView: View { | ||||
|                             ContentMainTabCategoryView( | ||||
|                                 imageName: "ic_category_series", | ||||
|                                 title: "시리즈", | ||||
|                                 onClick: {} | ||||
|                                 onClick: { | ||||
|                                     AppState.shared | ||||
|                                         .setAppStep( | ||||
|                                             step: .contentMain( | ||||
|                                                 startTab: .SERIES | ||||
|                                             ) | ||||
|                                         ) | ||||
|                                 } | ||||
|                             ) | ||||
|                             .frame(maxWidth: .infinity) | ||||
|                              | ||||
|                             ContentMainTabCategoryView( | ||||
|                                 imageName: "ic_category_content", | ||||
|                                 title: "단편", | ||||
|                                 onClick: {} | ||||
|                                 onClick: { | ||||
|                                     AppState.shared | ||||
|                                         .setAppStep( | ||||
|                                             step: .contentMain( | ||||
|                                                 startTab: .CONTENT | ||||
|                                             ) | ||||
|                                         ) | ||||
|                                 } | ||||
|                             ) | ||||
|                             .frame(maxWidth: .infinity) | ||||
|                              | ||||
|                             ContentMainTabCategoryView( | ||||
|                                 imageName: "ic_category_audio_book", | ||||
|                                 title: "오디오북", | ||||
|                                 onClick: {} | ||||
|                                 onClick: { | ||||
|                                     viewModel.errorMessage = "준비중입니다." | ||||
|                                     viewModel.isShowPopup = true | ||||
|                                 } | ||||
|                             ) | ||||
|                             .frame(maxWidth: .infinity) | ||||
|                              | ||||
|                             ContentMainTabCategoryView( | ||||
|                                 imageName: "ic_category_alarm", | ||||
|                                 title: "모닝콜", | ||||
|                                 onClick: {} | ||||
|                                 onClick: { | ||||
|                                     AppState.shared | ||||
|                                         .setAppStep( | ||||
|                                             step: .contentMain( | ||||
|                                                 startTab: .ALARM | ||||
|                                             ) | ||||
|                                         ) | ||||
|                                 } | ||||
|                             ) | ||||
|                             .frame(maxWidth: .infinity) | ||||
|                         } | ||||
| @@ -106,28 +130,52 @@ struct ContentMainTabHomeView: View { | ||||
|                             ContentMainTabCategoryView( | ||||
|                                 imageName: "ic_category_asmr", | ||||
|                                 title: "ASMR", | ||||
|                                 onClick: {} | ||||
|                                 onClick: { | ||||
|                                     AppState.shared | ||||
|                                         .setAppStep( | ||||
|                                             step: .contentMain( | ||||
|                                                 startTab: .ASMR | ||||
|                                             ) | ||||
|                                         ) | ||||
|                                 } | ||||
|                             ) | ||||
|                             .frame(maxWidth: .infinity) | ||||
|                              | ||||
|                             ContentMainTabCategoryView( | ||||
|                                 imageName: "ic_category_replay", | ||||
|                                 title: "다시듣기", | ||||
|                                 onClick: {} | ||||
|                                 onClick: { | ||||
|                                     AppState.shared | ||||
|                                         .setAppStep( | ||||
|                                             step: .contentMain( | ||||
|                                                 startTab: .REPLAY | ||||
|                                             ) | ||||
|                                         ) | ||||
|                                 } | ||||
|                             ) | ||||
|                             .frame(maxWidth: .infinity) | ||||
|                              | ||||
|                             ContentMainTabCategoryView( | ||||
|                                 imageName: "ic_category_audio_toon", | ||||
|                                 title: "오디오툰", | ||||
|                                 onClick: {} | ||||
|                                 onClick: { | ||||
|                                     viewModel.errorMessage = "준비중입니다." | ||||
|                                     viewModel.isShowPopup = true | ||||
|                                 } | ||||
|                             ) | ||||
|                             .frame(maxWidth: .infinity) | ||||
|                              | ||||
|                             ContentMainTabCategoryView( | ||||
|                                 imageName: "ic_category_free", | ||||
|                                 title: "무료", | ||||
|                                 onClick: {} | ||||
|                                 onClick: { | ||||
|                                     AppState.shared | ||||
|                                         .setAppStep( | ||||
|                                             step: .contentMain( | ||||
|                                                 startTab: .FREE | ||||
|                                             ) | ||||
|                                         ) | ||||
|                                 } | ||||
|                             ) | ||||
|                             .frame(maxWidth: .infinity) | ||||
|                         } | ||||
|   | ||||
| @@ -0,0 +1,83 @@ | ||||
| // | ||||
| //  ContentMainCompletedSeriesView.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2/21/25. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| struct ContentMainCompletedSeriesView: View { | ||||
|      | ||||
|     let itemList: [SeriesListItem] | ||||
|     let onClickMore: () -> Void | ||||
|      | ||||
|     var body: some View { | ||||
|         VStack(spacing: 13.3) { | ||||
|             HStack(spacing: 0) { | ||||
|                 Text("완결 시리즈") | ||||
|                     .font(.custom(Font.bold.rawValue, size: 18.3)) | ||||
|                     .foregroundColor(.grayee) | ||||
|                  | ||||
|                 Spacer() | ||||
|                  | ||||
|                 Image("ic_forward") | ||||
|                     .onTapGesture { | ||||
|                         onClickMore() | ||||
|                     } | ||||
|             } | ||||
|             .padding(.horizontal, 13.3) | ||||
|              | ||||
|             ScrollView(.horizontal, showsIndicators: false) { | ||||
|                 HStack(spacing: 13.3) { | ||||
|                     ForEach(0..<itemList.count, id: \.self) { index in | ||||
|                         let item = itemList[index] | ||||
|                         SeriesListBigItemView( | ||||
|                             item: item, | ||||
|                             isVisibleCreator: true | ||||
|                         ) | ||||
|                     } | ||||
|                 } | ||||
|                 .padding(.horizontal, 13.3) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #Preview { | ||||
|     ContentMainCompletedSeriesView( | ||||
|         itemList: [ | ||||
|             SeriesListItem( | ||||
|                 seriesId: 1, | ||||
|                 title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)", | ||||
|                 coverImage: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                 publishedDaysOfWeek: "매주 수, 토요일", | ||||
|                 isComplete: true, | ||||
|                 creator: SeriesListItemCreator( | ||||
|                     creatorId: 1, | ||||
|                     nickname: "creator", | ||||
|                     profileImage: "https://test-cf.sodalive.net/profile/default-profile.png" | ||||
|                 ), | ||||
|                 numberOfContent: 10, | ||||
|                 isNew: true, | ||||
|                 isPopular: true | ||||
|             ), | ||||
|             SeriesListItem( | ||||
|                 seriesId: 1, | ||||
|                 title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)", | ||||
|                 coverImage: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                 publishedDaysOfWeek: "매주 수, 토요일", | ||||
|                 isComplete: true, | ||||
|                 creator: SeriesListItemCreator( | ||||
|                     creatorId: 1, | ||||
|                     nickname: "creator", | ||||
|                     profileImage: "https://test-cf.sodalive.net/profile/default-profile.png" | ||||
|                 ), | ||||
|                 numberOfContent: 10, | ||||
|                 isNew: true, | ||||
|                 isPopular: true | ||||
|             ) | ||||
|         ], | ||||
|         onClickMore: {} | ||||
|     ) | ||||
| } | ||||
| @@ -0,0 +1,115 @@ | ||||
| // | ||||
| //  ContentMainNewOrRecommendSeriesView.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2/21/25. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
| import Kingfisher | ||||
|  | ||||
| struct ContentMainNewOrRecommendSeriesView: View { | ||||
|      | ||||
|     let title: String | ||||
|     let recommendSeriesList: [GetRecommendSeriesListResponse] | ||||
|      | ||||
|     var body: some View { | ||||
|         VStack(alignment: .leading, spacing: 13.3) { | ||||
|             Text(title) | ||||
|                 .font(.custom(Font.bold.rawValue, size: 18.3)) | ||||
|                 .foregroundColor(Color(hex: "eeeeee")) | ||||
|                 .padding(.horizontal, 13.3) | ||||
|              | ||||
|             ScrollView(.horizontal, showsIndicators: false) { | ||||
|                 HStack(spacing: 13.3) { | ||||
|                     ForEach(0..<recommendSeriesList.count, id: \.self) { index in | ||||
|                         let item = recommendSeriesList[index] | ||||
|                         ContentMainNewOrRecommendSeriesItemView(item: item) | ||||
|                             .onTapGesture { | ||||
|                                 AppState.shared | ||||
|                                     .setAppStep(step: .seriesDetail(seriesId: item.seriesId)) | ||||
|                             } | ||||
|                     } | ||||
|                 } | ||||
|                 .padding(.horizontal, 13.3) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct ContentMainNewOrRecommendSeriesItemView: View { | ||||
|      | ||||
|     let item: GetRecommendSeriesListResponse | ||||
|      | ||||
|     var body: some View { | ||||
|         VStack(alignment: .leading, spacing: 9) { | ||||
|             KFImage(URL(string: item.imageUrl)) | ||||
|                 .cancelOnDisappear(true) | ||||
|                 .downsampling(size: CGSize(width: 267, height: 141.3)) | ||||
|                 .resizable() | ||||
|                 .scaledToFill() | ||||
|                 .frame(width: 267, height: 141.3) | ||||
|                 .clipShape(RoundedRectangle(cornerRadius: 5)) | ||||
|              | ||||
|             Text(item.title) | ||||
|                 .font(.custom(Font.medium.rawValue, size: 13.3)) | ||||
|                 .foregroundColor(.grayee) | ||||
|              | ||||
|             HStack(spacing: 5.3) { | ||||
|                 KFImage(URL(string: item.creatorProfileImageUrl)) | ||||
|                     .cancelOnDisappear(true) | ||||
|                     .downsampling(size: CGSize(width: 30, height: 30)) | ||||
|                     .resizable() | ||||
|                     .scaledToFill() | ||||
|                     .frame(width: 30, height: 30) | ||||
|                     .clipShape(Circle()) | ||||
|                  | ||||
|                 Text(item.creatorNickname) | ||||
|                     .font(.custom(Font.medium.rawValue, size: 10)) | ||||
|                     .foregroundColor(.gray77) | ||||
|             } | ||||
|             .onTapGesture { | ||||
|                 AppState.shared | ||||
|                     .setAppStep(step: .creatorDetail(userId: item.creatorId)) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #Preview { | ||||
|     ContentMainNewOrRecommendSeriesView( | ||||
|         title: "추천 무료 시리즈", | ||||
|         recommendSeriesList: [ | ||||
|             GetRecommendSeriesListResponse( | ||||
|                 seriesId: 1, | ||||
|                 title: "시리즈 1", | ||||
|                 imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                 creatorId: 1, | ||||
|                 creatorNickname: "유저1", | ||||
|                 creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png" | ||||
|             ), | ||||
|             GetRecommendSeriesListResponse( | ||||
|                 seriesId: 1, | ||||
|                 title: "시리즈 1", | ||||
|                 imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                 creatorId: 1, | ||||
|                 creatorNickname: "유저1", | ||||
|                 creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png" | ||||
|             ) | ||||
|         ] | ||||
|     ) | ||||
| } | ||||
|  | ||||
| /* | ||||
|  contentId: 1, | ||||
|  title: "안녕하세요 오늘은 커버곡을 들려드릴께요....안녕하세요 오늘은 커버곡을 들려드릴께요....", | ||||
|  coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|  themeStr: "커버곡", | ||||
|  price: 100, | ||||
|  duration: "00:30:20", | ||||
|  creatorId: 1, | ||||
|  creatorNickname: "유저1", | ||||
|  creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png" | ||||
|   | ||||
|   | ||||
|  */ | ||||
| @@ -8,11 +8,109 @@ | ||||
| import SwiftUI | ||||
|  | ||||
| struct ContentMainSeriesByGenreView: View { | ||||
|      | ||||
|     let genreList: [GetSeriesGenreListResponse] | ||||
|     let itemList: [SeriesListItem] | ||||
|     let onClickGenre: (Int) -> Void | ||||
|      | ||||
|     @State private var selectedGenreId = 0 | ||||
|      | ||||
|     var body: some View { | ||||
|         Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) | ||||
|         VStack(alignment: .leading, spacing: 13.3) { | ||||
|             Text("장르별 추천 시리즈") | ||||
|                 .font(.custom(Font.bold.rawValue, size: 18.3)) | ||||
|                 .foregroundColor(Color.grayee) | ||||
|                 .padding(.horizontal, 13.3) | ||||
|              | ||||
|             ContentMainSeriesGenreView( | ||||
|                 genreList: genreList, | ||||
|                 selectGenre: { | ||||
|                     selectedGenreId = $0 | ||||
|                     onClickGenre($0) | ||||
|                 }, | ||||
|                 selectedGenreId: $selectedGenreId | ||||
|             ) | ||||
|              | ||||
|             if !itemList.isEmpty { | ||||
|                 ScrollView(.horizontal, showsIndicators: false) { | ||||
|                     HStack(spacing: 13.3) { | ||||
|                         ForEach(0..<itemList.count, id: \.self) { index in | ||||
|                             let item = itemList[index] | ||||
|                              | ||||
|                             SeriesListBigItemView( | ||||
|                                 item: item, | ||||
|                                 isVisibleCreator: true | ||||
|                             ) | ||||
|                         } | ||||
|                     } | ||||
|                     .padding(.horizontal, 13.3) | ||||
|                 } | ||||
|             } else { | ||||
|                 ContentMainNoItemView() | ||||
|             } | ||||
|         } | ||||
|         .onAppear { | ||||
|             if !genreList.isEmpty { | ||||
|                 selectedGenreId = genreList[0].id | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #Preview { | ||||
|     ContentMainSeriesByGenreView() | ||||
|     ContentMainSeriesByGenreView( | ||||
|         genreList: [ | ||||
|             GetSeriesGenreListResponse(id: 1, genre: "test"), | ||||
|             GetSeriesGenreListResponse(id: 2, genre: "test2"), | ||||
|             GetSeriesGenreListResponse(id: 3, genre: "test3") | ||||
|         ], | ||||
|         itemList: [ | ||||
|             SeriesListItem( | ||||
|                 seriesId: 1, | ||||
|                 title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)", | ||||
|                 coverImage: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                 publishedDaysOfWeek: "매주 수, 토요일", | ||||
|                 isComplete: true, | ||||
|                 creator: SeriesListItemCreator( | ||||
|                     creatorId: 1, | ||||
|                     nickname: "creator", | ||||
|                     profileImage: "https://test-cf.sodalive.net/profile/default-profile.png" | ||||
|                 ), | ||||
|                 numberOfContent: 10, | ||||
|                 isNew: false, | ||||
|                 isPopular: true | ||||
|             ), | ||||
|             SeriesListItem( | ||||
|                 seriesId: 2, | ||||
|                 title: "제목2", | ||||
|                 coverImage: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                 publishedDaysOfWeek: "랜덤", | ||||
|                 isComplete: false, | ||||
|                 creator: SeriesListItemCreator( | ||||
|                     creatorId: 1, | ||||
|                     nickname: "creator", | ||||
|                     profileImage: "https://test-cf.sodalive.net/profile/default-profile.png" | ||||
|                 ), | ||||
|                 numberOfContent: 10, | ||||
|                 isNew: true, | ||||
|                 isPopular: false | ||||
|             ), | ||||
|             SeriesListItem( | ||||
|                 seriesId: 2, | ||||
|                 title: "제목2", | ||||
|                 coverImage: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                 publishedDaysOfWeek: "랜덤", | ||||
|                 isComplete: false, | ||||
|                 creator: SeriesListItemCreator( | ||||
|                     creatorId: 1, | ||||
|                     nickname: "creator", | ||||
|                     profileImage: "https://test-cf.sodalive.net/profile/default-profile.png" | ||||
|                 ), | ||||
|                 numberOfContent: 10, | ||||
|                 isNew: true, | ||||
|                 isPopular: false | ||||
|             ) | ||||
|         ], | ||||
|         onClickGenre: { _ in } | ||||
|     ) | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,158 @@ | ||||
| // | ||||
| //  ContentMainSeriesCurationView.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2/21/25. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| struct ContentMainSeriesCurationView: View { | ||||
|      | ||||
|     let curationList: [GetSeriesCurationResponse] | ||||
|      | ||||
|     var body: some View { | ||||
|         LazyVStack(spacing: 30) { | ||||
|             ForEach(0..<curationList.count, id: \.self) { index in | ||||
|                 let curation = curationList[index] | ||||
|                 ContentMainSeriesCurationItemView(curation: curation) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| struct ContentMainSeriesCurationItemView: View { | ||||
|      | ||||
|     let curation: GetSeriesCurationResponse | ||||
|      | ||||
|     var body: some View { | ||||
|         VStack(alignment: .leading, spacing: 13.3) { | ||||
|             Text(curation.title) | ||||
|                 .font(.custom(Font.bold.rawValue, size: 18.3)) | ||||
|                 .foregroundColor(.grayee) | ||||
|                 .padding(.horizontal, 13.3) | ||||
|              | ||||
|             ScrollView(.horizontal, showsIndicators: false) { | ||||
|                 HStack(spacing: 13.3) { | ||||
|                     ForEach(0..<curation.items.count, id: \.self) { index in | ||||
|                         let item = curation.items[index] | ||||
|                         SeriesListBigItemView(item: item, isVisibleCreator: true) | ||||
|                     } | ||||
|                 } | ||||
|                 .padding(.horizontal, 13.3) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #Preview { | ||||
|     ContentMainSeriesCurationView( | ||||
|         curationList: [ | ||||
|             GetSeriesCurationResponse( | ||||
|                 title: "test", | ||||
|                 items: [ | ||||
|                     SeriesListItem( | ||||
|                         seriesId: 1, | ||||
|                         title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)", | ||||
|                         coverImage: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                         publishedDaysOfWeek: "매주 수, 토요일", | ||||
|                         isComplete: true, | ||||
|                         creator: SeriesListItemCreator( | ||||
|                             creatorId: 1, | ||||
|                             nickname: "creator", | ||||
|                             profileImage: "https://test-cf.sodalive.net/profile/default-profile.png" | ||||
|                         ), | ||||
|                         numberOfContent: 10, | ||||
|                         isNew: true, | ||||
|                         isPopular: true | ||||
|                     ), | ||||
|                     SeriesListItem( | ||||
|                         seriesId: 1, | ||||
|                         title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)", | ||||
|                         coverImage: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                         publishedDaysOfWeek: "매주 수, 토요일", | ||||
|                         isComplete: true, | ||||
|                         creator: SeriesListItemCreator( | ||||
|                             creatorId: 1, | ||||
|                             nickname: "creator", | ||||
|                             profileImage: "https://test-cf.sodalive.net/profile/default-profile.png" | ||||
|                         ), | ||||
|                         numberOfContent: 10, | ||||
|                         isNew: true, | ||||
|                         isPopular: true | ||||
|                     ) | ||||
|                 ] | ||||
|             ), | ||||
|             GetSeriesCurationResponse( | ||||
|                 title: "test2", | ||||
|                 items: [ | ||||
|                     SeriesListItem( | ||||
|                         seriesId: 1, | ||||
|                         title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)", | ||||
|                         coverImage: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                         publishedDaysOfWeek: "매주 수, 토요일", | ||||
|                         isComplete: true, | ||||
|                         creator: SeriesListItemCreator( | ||||
|                             creatorId: 1, | ||||
|                             nickname: "creator", | ||||
|                             profileImage: "https://test-cf.sodalive.net/profile/default-profile.png" | ||||
|                         ), | ||||
|                         numberOfContent: 10, | ||||
|                         isNew: true, | ||||
|                         isPopular: true | ||||
|                     ), | ||||
|                     SeriesListItem( | ||||
|                         seriesId: 1, | ||||
|                         title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)", | ||||
|                         coverImage: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                         publishedDaysOfWeek: "매주 수, 토요일", | ||||
|                         isComplete: true, | ||||
|                         creator: SeriesListItemCreator( | ||||
|                             creatorId: 1, | ||||
|                             nickname: "creator", | ||||
|                             profileImage: "https://test-cf.sodalive.net/profile/default-profile.png" | ||||
|                         ), | ||||
|                         numberOfContent: 10, | ||||
|                         isNew: true, | ||||
|                         isPopular: true | ||||
|                     ) | ||||
|                 ] | ||||
|             ), | ||||
|             GetSeriesCurationResponse( | ||||
|                 title: "test3", | ||||
|                 items: [ | ||||
|                     SeriesListItem( | ||||
|                         seriesId: 1, | ||||
|                         title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)", | ||||
|                         coverImage: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                         publishedDaysOfWeek: "매주 수, 토요일", | ||||
|                         isComplete: true, | ||||
|                         creator: SeriesListItemCreator( | ||||
|                             creatorId: 1, | ||||
|                             nickname: "creator", | ||||
|                             profileImage: "https://test-cf.sodalive.net/profile/default-profile.png" | ||||
|                         ), | ||||
|                         numberOfContent: 10, | ||||
|                         isNew: true, | ||||
|                         isPopular: true | ||||
|                     ), | ||||
|                     SeriesListItem( | ||||
|                         seriesId: 1, | ||||
|                         title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)", | ||||
|                         coverImage: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                         publishedDaysOfWeek: "매주 수, 토요일", | ||||
|                         isComplete: true, | ||||
|                         creator: SeriesListItemCreator( | ||||
|                             creatorId: 1, | ||||
|                             nickname: "creator", | ||||
|                             profileImage: "https://test-cf.sodalive.net/profile/default-profile.png" | ||||
|                         ), | ||||
|                         numberOfContent: 10, | ||||
|                         isNew: true, | ||||
|                         isPopular: true | ||||
|                     ) | ||||
|                 ] | ||||
|             ) | ||||
|         ] | ||||
|     ) | ||||
| } | ||||
| @@ -8,11 +8,51 @@ | ||||
| import SwiftUI | ||||
|  | ||||
| struct ContentMainSeriesGenreView: View { | ||||
|     let genreList: [GetSeriesGenreListResponse] | ||||
|     let selectGenre: (Int) -> Void | ||||
|      | ||||
|     @Binding var selectedGenreId: Int | ||||
|      | ||||
|     var body: some View { | ||||
|         Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) | ||||
|         ScrollView(.horizontal, showsIndicators: false) { | ||||
|             HStack(alignment: .top, spacing: 8) { | ||||
|                 ForEach(0..<genreList.count, id: \.self) { index in | ||||
|                     let genre = genreList[index] | ||||
|                     Text(genre.genre) | ||||
|                         .font(.custom(Font.medium.rawValue, size: 14.7)) | ||||
|                         .foregroundColor(selectedGenreId == genre.id ? Color.button : Color.gray77) | ||||
|                         .padding(.horizontal, 13.3) | ||||
|                         .padding(.vertical, 9.3) | ||||
|                         .border( | ||||
|                             selectedGenreId == genre.id ? Color.button : Color.grayee, | ||||
|                             width: 1 | ||||
|                         ) | ||||
|                         .cornerRadius(16.7) | ||||
|                         .overlay( | ||||
|                             RoundedRectangle(cornerRadius: CGFloat(16.7)) | ||||
|                                 .stroke(lineWidth: 1) | ||||
|                                 .foregroundColor(selectedGenreId == genre.id ? Color.button : Color.grayee) | ||||
|                         ) | ||||
|                         .onTapGesture { | ||||
|                             if selectedGenreId != genre.id { | ||||
|                                 selectGenre(genre.id) | ||||
|                             } | ||||
|                         } | ||||
|                 } | ||||
|             } | ||||
|             .padding(.horizontal, 13.3) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #Preview { | ||||
|     ContentMainSeriesGenreView() | ||||
|     ContentMainSeriesGenreView( | ||||
|         genreList: [ | ||||
|             GetSeriesGenreListResponse(id: 1, genre: "test"), | ||||
|             GetSeriesGenreListResponse(id: 2, genre: "test2"), | ||||
|             GetSeriesGenreListResponse(id: 3, genre: "test3") | ||||
|         ], | ||||
|         selectGenre: { _ in }, | ||||
|         selectedGenreId: .constant(2) | ||||
|     ) | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,141 @@ | ||||
| // | ||||
| //  ContentMainSeriesRankingView.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2/21/25. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
| import Kingfisher | ||||
|  | ||||
| struct ContentMainSeriesRankingView: View { | ||||
|      | ||||
|     let seriesList: [SeriesListItem] | ||||
|      | ||||
|     let rows = [ | ||||
|         GridItem(.flexible(), alignment: .leading), | ||||
|         GridItem(.flexible(), alignment: .leading), | ||||
|         GridItem(.flexible(), alignment: .leading) | ||||
|     ] | ||||
|      | ||||
|     var body: some View { | ||||
|         VStack(alignment: .leading, spacing: 13.3) { | ||||
|             Text("일간 랭킹") | ||||
|                 .font(.custom(Font.bold.rawValue, size: 18.3)) | ||||
|                 .foregroundColor(.grayee) | ||||
|                 .padding(.horizontal, 13.3) | ||||
|              | ||||
|             ScrollView(.horizontal, showsIndicators: false) { | ||||
|                 LazyHGrid(rows: rows, spacing: 13.3) { | ||||
|                     ForEach(0..<seriesList.count, id: \.self) { index in | ||||
|                         let series = seriesList[index] | ||||
|                         HStack(spacing: 0) { | ||||
|                             KFImage(URL(string: series.coverImage)) | ||||
|                                 .cancelOnDisappear(true) | ||||
|                                 .downsampling( | ||||
|                                     size: CGSize( | ||||
|                                         width: 60, | ||||
|                                         height: 85 | ||||
|                                     ) | ||||
|                                 ) | ||||
|                                 .resizable() | ||||
|                                 .frame(width: 60, height: 85) | ||||
|                                 .cornerRadius(2.7) | ||||
|                              | ||||
|                             Text("\(index + 1)") | ||||
|                                 .font(.custom(Font.bold.rawValue, size: 16.7)) | ||||
|                                 .foregroundColor(.button) | ||||
|                                 .padding(.horizontal, 12) | ||||
|                              | ||||
|                             VStack(alignment: .leading, spacing: 8) { | ||||
|                                 Text(series.title) | ||||
|                                     .lineLimit(2) | ||||
|                                     .font(.custom(Font.medium.rawValue, size: 13.3)) | ||||
|                                     .foregroundColor(.grayd2) | ||||
|                                  | ||||
|                                 Text(series.creator.nickname) | ||||
|                                     .font(.custom(Font.medium.rawValue, size: 11)) | ||||
|                                     .foregroundColor(.gray77) | ||||
|                             } | ||||
|                         } | ||||
|                         .frame(maxWidth: screenSize().width * 0.66, alignment: .leading) | ||||
|                         .contentShape(Rectangle()) | ||||
|                         .onTapGesture { | ||||
|                             AppState | ||||
|                                 .shared | ||||
|                                 .setAppStep(step: .seriesDetail(seriesId: series.seriesId)) | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 .padding(.horizontal, 13.3) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #Preview { | ||||
|     ContentMainSeriesRankingView( | ||||
|         seriesList: [ | ||||
|             SeriesListItem( | ||||
|                 seriesId: 1, | ||||
|                 title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)", | ||||
|                 coverImage: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                 publishedDaysOfWeek: "매주 수, 토요일", | ||||
|                 isComplete: true, | ||||
|                 creator: SeriesListItemCreator( | ||||
|                     creatorId: 1, | ||||
|                     nickname: "creator", | ||||
|                     profileImage: "https://test-cf.sodalive.net/profile/default-profile.png" | ||||
|                 ), | ||||
|                 numberOfContent: 10, | ||||
|                 isNew: true, | ||||
|                 isPopular: true | ||||
|             ), | ||||
|             SeriesListItem( | ||||
|                 seriesId: 1, | ||||
|                 title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)", | ||||
|                 coverImage: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                 publishedDaysOfWeek: "매주 수, 토요일", | ||||
|                 isComplete: true, | ||||
|                 creator: SeriesListItemCreator( | ||||
|                     creatorId: 1, | ||||
|                     nickname: "creator", | ||||
|                     profileImage: "https://test-cf.sodalive.net/profile/default-profile.png" | ||||
|                 ), | ||||
|                 numberOfContent: 10, | ||||
|                 isNew: true, | ||||
|                 isPopular: true | ||||
|             ), | ||||
|             SeriesListItem( | ||||
|                 seriesId: 3, | ||||
|                 title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)", | ||||
|                 coverImage: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                 publishedDaysOfWeek: "매주 수, 토요일", | ||||
|                 isComplete: true, | ||||
|                 creator: SeriesListItemCreator( | ||||
|                     creatorId: 1, | ||||
|                     nickname: "creator", | ||||
|                     profileImage: "https://test-cf.sodalive.net/profile/default-profile.png" | ||||
|                 ), | ||||
|                 numberOfContent: 10, | ||||
|                 isNew: true, | ||||
|                 isPopular: true | ||||
|             ), | ||||
|             SeriesListItem( | ||||
|                 seriesId: 4, | ||||
|                 title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)", | ||||
|                 coverImage: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                 publishedDaysOfWeek: "매주 수, 토요일", | ||||
|                 isComplete: true, | ||||
|                 creator: SeriesListItemCreator( | ||||
|                     creatorId: 1, | ||||
|                     nickname: "creator", | ||||
|                     profileImage: "https://test-cf.sodalive.net/profile/default-profile.png" | ||||
|                 ), | ||||
|                 numberOfContent: 10, | ||||
|                 isNew: true, | ||||
|                 isPopular: true | ||||
|             ) | ||||
|         ] | ||||
|     ) | ||||
| } | ||||
| @@ -6,3 +6,22 @@ | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
| import CombineMoya | ||||
| import Combine | ||||
| import Moya | ||||
|  | ||||
| final class ContentMainTabSeriesRepository { | ||||
|     private let api = MoyaProvider<ContentApi>() | ||||
|      | ||||
|     func getContentMainSeries() -> AnyPublisher<Response, MoyaError> { | ||||
|         return api.requestPublisher(.getContentMainSeries) | ||||
|     } | ||||
|      | ||||
|     func getRecommendSeriesListByGenre(genreId: Int) -> AnyPublisher<Response, MoyaError> { | ||||
|         return api.requestPublisher(.getRecommendSeriesListByGenre(genreId: genreId)) | ||||
|     } | ||||
|      | ||||
|     func getRecommendSeriesByCreator(creatorId: Int) -> AnyPublisher<Response, MoyaError> { | ||||
|         return api.requestPublisher(.getRecommendSeriesByCreator(creatorId: creatorId)) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -8,8 +8,86 @@ | ||||
| import SwiftUI | ||||
|  | ||||
| struct ContentMainTabSeriesView: View { | ||||
|      | ||||
|     @StateObject var viewModel = ContentMainTabSeriesViewModel() | ||||
|      | ||||
|     var body: some View { | ||||
|         Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) | ||||
|         BaseView(isLoading: $viewModel.isLoading) { | ||||
|             ScrollView(.vertical, showsIndicators: false) { | ||||
|                 VStack(spacing: 0) { | ||||
|                     if !viewModel.bannerList.isEmpty { | ||||
|                         ContentMainBannerViewV2(bannerList: viewModel.bannerList) | ||||
|                             .padding(.horizontal, 13.3) | ||||
|                     } | ||||
|                      | ||||
|                     if !viewModel.originalAudioDramaList.isEmpty { | ||||
|                         ContentMainOriginalAudioDramaView(itemList: viewModel.originalAudioDramaList) { | ||||
|                         } | ||||
|                         .padding(.top, 30) | ||||
|                     } | ||||
|                      | ||||
|                     if !viewModel.rankSeriesList.isEmpty { | ||||
|                         ContentMainSeriesRankingView(seriesList: viewModel.rankSeriesList) | ||||
|                             .padding(.top, 30) | ||||
|                     } | ||||
|                      | ||||
|                     if !viewModel.genreList.isEmpty { | ||||
|                         ContentMainSeriesByGenreView( | ||||
|                             genreList: viewModel.genreList, | ||||
|                             itemList: viewModel.recommendSeriesList | ||||
|                         ) { | ||||
|                             viewModel.getRecommendSeriesListByGenre(genreId: $0) | ||||
|                         } | ||||
|                         .padding(.top, 30) | ||||
|                     } | ||||
|                      | ||||
|                     if !viewModel.newSeriesList.isEmpty { | ||||
|                         ContentMainNewOrRecommendSeriesView( | ||||
|                             title: "새로운 시리즈", | ||||
|                             recommendSeriesList: viewModel.newSeriesList | ||||
|                         ) | ||||
|                         .padding(.top, 30) | ||||
|                     } | ||||
|                      | ||||
|                     if !viewModel.rankCompleteSeriesList.isEmpty { | ||||
|                         ContentMainCompletedSeriesView( | ||||
|                             itemList: viewModel.rankCompleteSeriesList, | ||||
|                             onClickMore: {} | ||||
|                         ) | ||||
|                         .padding(.top, 30) | ||||
|                     } | ||||
|                      | ||||
|                     if !viewModel.seriesRankCreatorList.isEmpty { | ||||
|                         SeriesByChannelView( | ||||
|                             title: "채널별 추천 시리즈", | ||||
|                             creatorList: viewModel.seriesRankCreatorList, | ||||
|                             seriesList: viewModel.recommendSeriesByChannel | ||||
|                         ) { | ||||
|                             viewModel.getRecommendSeriesByCreator(creatorId: $0) | ||||
|                         } | ||||
|                         .padding(.top, 30) | ||||
|                     } | ||||
|                      | ||||
|                     if !viewModel.eventBannerList.isEmpty { | ||||
|                         SectionEventBannerView(items: viewModel.eventBannerList) | ||||
|                             .frame( | ||||
|                                 width: viewModel.eventBannerList.count > 0 ? screenSize().width : 0, | ||||
|                                 height: viewModel.eventBannerList.count > 0 ? screenSize().width * 300 / 1000 : 0, | ||||
|                                 alignment: .center | ||||
|                             ) | ||||
|                             .padding(.top, 30) | ||||
|                     } | ||||
|                      | ||||
|                     if !viewModel.curationList.isEmpty { | ||||
|                         ContentMainSeriesCurationView(curationList: viewModel.curationList) | ||||
|                             .padding(.top, 30) | ||||
|                     } | ||||
|                 } | ||||
|                 .onAppear { | ||||
|                     viewModel.fetchData() | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -6,3 +6,152 @@ | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
| import Combine | ||||
|  | ||||
| final class ContentMainTabSeriesViewModel: ObservableObject { | ||||
|     private let repository = ContentMainTabSeriesRepository() | ||||
|     private let contentRepository = ContentRepository() | ||||
|     private var subscription = Set<AnyCancellable>() | ||||
|      | ||||
|     @Published var errorMessage = "" | ||||
|     @Published var isShowPopup = false | ||||
|     @Published var isLoading = false | ||||
|      | ||||
|     @Published var bannerList: [GetAudioContentBannerResponse] = [] | ||||
|     @Published var originalAudioDramaList: [SeriesListItem] = [] | ||||
|     @Published var rankSeriesList: [SeriesListItem] = [] | ||||
|     @Published var genreList: [GetSeriesGenreListResponse] = [] | ||||
|     @Published var recommendSeriesList: [SeriesListItem] = [] | ||||
|     @Published var newSeriesList: [GetRecommendSeriesListResponse] = [] | ||||
|     @Published var rankCompleteSeriesList: [SeriesListItem] = [] | ||||
|     @Published var seriesRankCreatorList: [ContentCreatorResponse] = [] | ||||
|     @Published var recommendSeriesByChannel: [SeriesListItem] = [] | ||||
|     @Published var eventBannerList: [EventItem] = [] | ||||
|     @Published var curationList: [GetSeriesCurationResponse] = [] | ||||
|      | ||||
|     func fetchData() { | ||||
|         isLoading = true | ||||
|          | ||||
|         repository.getContentMainSeries() | ||||
|             .sink { result in | ||||
|                 switch result { | ||||
|                 case .finished: | ||||
|                     DEBUG_LOG("finish") | ||||
|                 case .failure(let error): | ||||
|                     ERROR_LOG(error.localizedDescription) | ||||
|                 } | ||||
|             } receiveValue: { [unowned self] response in | ||||
|                 let responseData = response.data | ||||
|                  | ||||
|                 do { | ||||
|                     let jsonDecoder = JSONDecoder() | ||||
|                     let decoded = try jsonDecoder.decode(ApiResponse<GetContentMainTabSeriesResponse>.self, from: responseData) | ||||
|                      | ||||
|                     if let data = decoded.data, decoded.success { | ||||
|                         self.bannerList = data.contentBannerList | ||||
|                         self.originalAudioDramaList = data.originalAudioDrama | ||||
|                         self.rankSeriesList = data.rankSeriesList | ||||
|                         self.genreList = data.genreList | ||||
|                         self.recommendSeriesList = data.recommendSeriesList | ||||
|                         self.newSeriesList = data.newSeriesList | ||||
|                         self.rankCompleteSeriesList = data.rankCompleteSeriesList | ||||
|                         self.seriesRankCreatorList = data.seriesRankCreatorList | ||||
|                         self.recommendSeriesByChannel = data.recommendSeriesByChannel | ||||
|                         self.eventBannerList = data.eventBannerList.eventList | ||||
|                         self.curationList = data.curationList | ||||
|                     } else { | ||||
|                         if let message = decoded.message { | ||||
|                             self.errorMessage = message | ||||
|                         } else { | ||||
|                             self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." | ||||
|                         } | ||||
|                          | ||||
|                         self.isShowPopup = true | ||||
|                     } | ||||
|                 } catch { | ||||
|                     self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." | ||||
|                     self.isShowPopup = true | ||||
|                 } | ||||
|                  | ||||
|                 self.isLoading = false | ||||
|             } | ||||
|             .store(in: &subscription) | ||||
|     } | ||||
|      | ||||
|     func getRecommendSeriesListByGenre(genreId: Int) { | ||||
|         isLoading = true | ||||
|         repository.getRecommendSeriesListByGenre(genreId: genreId) | ||||
|             .sink { result in | ||||
|                 switch result { | ||||
|                 case .finished: | ||||
|                     DEBUG_LOG("finish") | ||||
|                 case .failure(let error): | ||||
|                     ERROR_LOG(error.localizedDescription) | ||||
|                 } | ||||
|             } receiveValue: { [unowned self] response in | ||||
|                 let responseData = response.data | ||||
|                  | ||||
|                 do { | ||||
|                     let jsonDecoder = JSONDecoder() | ||||
|                     let decoded = try jsonDecoder.decode(ApiResponse<[SeriesListItem]>.self, from: responseData) | ||||
|                      | ||||
|                     if let data = decoded.data, decoded.success { | ||||
|                         self.recommendSeriesList = data | ||||
|                     } else { | ||||
|                         if let message = decoded.message { | ||||
|                             self.errorMessage = message | ||||
|                         } else { | ||||
|                             self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." | ||||
|                         } | ||||
|                          | ||||
|                         self.isShowPopup = true | ||||
|                     } | ||||
|                 } catch { | ||||
|                     self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." | ||||
|                     self.isShowPopup = true | ||||
|                 } | ||||
|                  | ||||
|                 self.isLoading = false | ||||
|             } | ||||
|             .store(in: &subscription) | ||||
|     } | ||||
|      | ||||
|     func getRecommendSeriesByCreator(creatorId: Int) { | ||||
|         seriesRankCreatorList = [] | ||||
|         isLoading = true | ||||
|         repository.getRecommendSeriesByCreator(creatorId: creatorId) | ||||
|             .sink { result in | ||||
|                 switch result { | ||||
|                 case .finished: | ||||
|                     DEBUG_LOG("finish") | ||||
|                 case .failure(let error): | ||||
|                     ERROR_LOG(error.localizedDescription) | ||||
|                 } | ||||
|             } receiveValue: { [unowned self] response in | ||||
|                 let responseData = response.data | ||||
|                  | ||||
|                 do { | ||||
|                     let jsonDecoder = JSONDecoder() | ||||
|                     let decoded = try jsonDecoder.decode(ApiResponse<[SeriesListItem]>.self, from: responseData) | ||||
|                      | ||||
|                     if let data = decoded.data, decoded.success { | ||||
|                         self.recommendSeriesByChannel = data | ||||
|                     } else { | ||||
|                         if let message = decoded.message { | ||||
|                             self.errorMessage = message | ||||
|                         } else { | ||||
|                             self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." | ||||
|                         } | ||||
|                          | ||||
|                         self.isShowPopup = true | ||||
|                     } | ||||
|                 } catch { | ||||
|                     self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." | ||||
|                     self.isShowPopup = true | ||||
|                 } | ||||
|                  | ||||
|                 self.isLoading = false | ||||
|             } | ||||
|             .store(in: &subscription) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -5,4 +5,35 @@ | ||||
| //  Created by klaus on 2/20/25. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
| struct GetContentMainTabSeriesResponse: Decodable { | ||||
|     let contentBannerList: [GetAudioContentBannerResponse] | ||||
|     let originalAudioDrama: [SeriesListItem] | ||||
|     let rankSeriesList: [SeriesListItem] | ||||
|     let genreList: [GetSeriesGenreListResponse] | ||||
|     let recommendSeriesList: [SeriesListItem] | ||||
|     let newSeriesList: [GetRecommendSeriesListResponse] | ||||
|     let rankCompleteSeriesList: [SeriesListItem] | ||||
|     let seriesRankCreatorList: [ContentCreatorResponse] | ||||
|     let recommendSeriesByChannel: [SeriesListItem] | ||||
|     let eventBannerList: GetEventResponse | ||||
|     let curationList: [GetSeriesCurationResponse] | ||||
| } | ||||
|  | ||||
| struct GetSeriesGenreListResponse: Decodable { | ||||
|     let id: Int | ||||
|     let genre: String | ||||
| } | ||||
|  | ||||
| struct GetRecommendSeriesListResponse: Decodable { | ||||
|     let seriesId: Int | ||||
|     let title: String | ||||
|     let imageUrl: String | ||||
|     let creatorId: Int | ||||
|     let creatorNickname: String | ||||
|     let creatorProfileImageUrl: String | ||||
| } | ||||
|  | ||||
| struct GetSeriesCurationResponse: Decodable { | ||||
|     let title: String | ||||
|     let items: [SeriesListItem] | ||||
| } | ||||
|   | ||||
| @@ -6,13 +6,103 @@ | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
| import Kingfisher | ||||
|  | ||||
| struct ContentMainOriginalAudioDramaItemView: View { | ||||
|      | ||||
|     let itemWidth: CGFloat | ||||
|     let item: SeriesListItem | ||||
|     let isAll: Bool | ||||
|      | ||||
|     var body: some View { | ||||
|         Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) | ||||
|         VStack(alignment: .leading, spacing: 8) { | ||||
|             ZStack { | ||||
|                 KFImage(URL(string: item.coverImage)) | ||||
|                     .cancelOnDisappear(true) | ||||
|                     .downsampling( | ||||
|                         size: CGSize( | ||||
|                             width: itemWidth, | ||||
|                             height: (itemWidth * 636) / 450 | ||||
|                         ) | ||||
|                     ) | ||||
|                     .resizable() | ||||
|                     .scaledToFill() | ||||
|                     .frame(width: itemWidth, height: (itemWidth * 636) / 450, alignment: .center) | ||||
|                     .cornerRadius(5) | ||||
|                     .clipped() | ||||
|                     .onTapGesture { | ||||
|                         AppState.shared | ||||
|                             .setAppStep(step: .seriesDetail(seriesId: item.seriesId)) | ||||
|                     } | ||||
|                  | ||||
|                 VStack(alignment: .leading, spacing: 0) { | ||||
|                     HStack(spacing: 3.3) { | ||||
|                         if !item.isComplete && item.isNew { | ||||
|                             SeriesItemBadgeView(title: "신작", backgroundColor: .button) | ||||
|                         } | ||||
|                          | ||||
|                         if item.isComplete { | ||||
|                             SeriesItemBadgeView(title: "완결", backgroundColor: Color(hex: "002abd")) | ||||
|                         } | ||||
|                          | ||||
|                         if item.isPopular { | ||||
|                             SeriesItemBadgeView(title: "인기", backgroundColor: Color(hex: "ec6033")) | ||||
|                         } | ||||
|                          | ||||
|                         Spacer() | ||||
|                          | ||||
|                         if !isAll { | ||||
|                             SeriesItemBadgeView(title: "총 \(item.numberOfContent)화", backgroundColor: Color.gray33.opacity(0.7)) | ||||
|                         } | ||||
|                     } | ||||
|                      | ||||
|                     Spacer() | ||||
|                      | ||||
|                     HStack { | ||||
|                         Spacer() | ||||
|                          | ||||
|                         if isAll { | ||||
|                             SeriesItemBadgeView(title: "총 \(item.numberOfContent)화", backgroundColor: Color.gray33.opacity(0.7)) | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 .padding(3.3) | ||||
|             } | ||||
|             .frame(width: itemWidth, height: (itemWidth * 636) / 450, alignment: .center) | ||||
|              | ||||
|             Text(item.title) | ||||
|                 .font(.custom(Font.medium.rawValue, size: 12)) | ||||
|                 .foregroundColor(Color.grayee) | ||||
|                 .lineLimit(1) | ||||
|              | ||||
|             if isAll { | ||||
|                 Text(item.publishedDaysOfWeek) | ||||
|                     .font(.custom(Font.medium.rawValue, size: 11)) | ||||
|                     .foregroundColor(Color.gray77) | ||||
|             } | ||||
|         } | ||||
|         .frame(width: itemWidth) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #Preview { | ||||
|     ContentMainOriginalAudioDramaItemView() | ||||
|     ContentMainOriginalAudioDramaItemView( | ||||
|         itemWidth: 150, | ||||
|         item: SeriesListItem( | ||||
|             seriesId: 1, | ||||
|             title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)", | ||||
|             coverImage: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|             publishedDaysOfWeek: "매주 수, 토요일", | ||||
|             isComplete: true, | ||||
|             creator: SeriesListItemCreator( | ||||
|                 creatorId: 1, | ||||
|                 nickname: "creator", | ||||
|                 profileImage: "https://test-cf.sodalive.net/profile/default-profile.png" | ||||
|             ), | ||||
|             numberOfContent: 10, | ||||
|             isNew: false, | ||||
|             isPopular: true | ||||
|         ), | ||||
|         isAll: false | ||||
|     ) | ||||
| } | ||||
|   | ||||
| @@ -8,11 +8,75 @@ | ||||
| import SwiftUI | ||||
|  | ||||
| struct ContentMainOriginalAudioDramaView: View { | ||||
|      | ||||
|     let itemList: [SeriesListItem] | ||||
|     let onClickMore: () -> Void | ||||
|      | ||||
|     var body: some View { | ||||
|         Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) | ||||
|         VStack(spacing: 13.3) { | ||||
|             HStack(spacing: 0) { | ||||
|                 Text("오리지널 오디오 드라마") | ||||
|                     .font(.custom(Font.bold.rawValue, size: 18.3)) | ||||
|                     .foregroundColor(.grayee) | ||||
|                  | ||||
|                 Spacer() | ||||
|                  | ||||
|                 Image("ic_forward") | ||||
|                     .onTapGesture { onClickMore() } | ||||
|             } | ||||
|             .padding(.horizontal, 13.3) | ||||
|              | ||||
|             ScrollView(.horizontal, showsIndicators: false) { | ||||
|                 HStack(spacing: 13.3) { | ||||
|                     ForEach(0..<itemList.count, id: \.self) { index in | ||||
|                         let item = itemList[index] | ||||
|                          | ||||
|                         ContentMainOriginalAudioDramaItemView( | ||||
|                             itemWidth: 150, | ||||
|                             item: item, | ||||
|                             isAll: false | ||||
|                         ) | ||||
|                     } | ||||
|                 } | ||||
|                 .padding(.horizontal, 13.3) | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #Preview { | ||||
|     ContentMainOriginalAudioDramaView() | ||||
|     ContentMainOriginalAudioDramaView( | ||||
|         itemList: [ | ||||
|             SeriesListItem( | ||||
|                 seriesId: 1, | ||||
|                 title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)", | ||||
|                 coverImage: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                 publishedDaysOfWeek: "매주 수, 토요일", | ||||
|                 isComplete: true, | ||||
|                 creator: SeriesListItemCreator( | ||||
|                     creatorId: 1, | ||||
|                     nickname: "creator", | ||||
|                     profileImage: "https://test-cf.sodalive.net/profile/default-profile.png" | ||||
|                 ), | ||||
|                 numberOfContent: 10, | ||||
|                 isNew: false, | ||||
|                 isPopular: true | ||||
|             ), | ||||
|             SeriesListItem( | ||||
|                 seriesId: 2, | ||||
|                 title: "제목2", | ||||
|                 coverImage: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                 publishedDaysOfWeek: "랜덤", | ||||
|                 isComplete: false, | ||||
|                 creator: SeriesListItemCreator( | ||||
|                     creatorId: 1, | ||||
|                     nickname: "creator", | ||||
|                     profileImage: "https://test-cf.sodalive.net/profile/default-profile.png" | ||||
|                 ), | ||||
|                 numberOfContent: 10, | ||||
|                 isNew: true, | ||||
|                 isPopular: false | ||||
|             ) | ||||
|         ] | ||||
|     ) {} | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,123 @@ | ||||
| // | ||||
| //  SeriesByChannelView.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 2/21/25. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| struct SeriesByChannelView: View { | ||||
|      | ||||
|     let title: String | ||||
|     let creatorList: [ContentCreatorResponse] | ||||
|     let seriesList: [SeriesListItem] | ||||
|     let onClickCreator: (Int) -> Void | ||||
|      | ||||
|     @State private var selectedCreatorId = 0 | ||||
|      | ||||
|     var body: some View { | ||||
|         VStack(alignment: .leading, spacing: 20) { | ||||
|             Text(title) | ||||
|                 .font(.custom(Font.bold.rawValue, size: 18.3)) | ||||
|                 .foregroundColor(Color(hex: "eeeeee")) | ||||
|                 .padding(.horizontal, 13.3) | ||||
|              | ||||
|             ScrollView(.horizontal) { | ||||
|                 HStack(spacing: 22) { | ||||
|                     ForEach(0..<creatorList.count, id: \.self) { index in | ||||
|                         let item = creatorList[index] | ||||
|                          | ||||
|                         ContentCreatorView( | ||||
|                             isSelected: item.creatorId == selectedCreatorId, | ||||
|                             item: item | ||||
|                         ) | ||||
|                         .onTapGesture { | ||||
|                             let creatorId = item.creatorId | ||||
|                              | ||||
|                             if creatorId != selectedCreatorId { | ||||
|                                 selectedCreatorId = creatorId | ||||
|                                 onClickCreator(creatorId) | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 .padding(.horizontal, 13.3) | ||||
|             } | ||||
|              | ||||
|             if seriesList.isEmpty { | ||||
|                 ContentMainNoItemView() | ||||
|             } else { | ||||
|                 ScrollView(.horizontal) { | ||||
|                     HStack(spacing: 22) { | ||||
|                         ForEach(0..<seriesList.count, id: \.self) { index in | ||||
|                             let item = seriesList[index] | ||||
|                              | ||||
|                             SeriesListBigItemView( | ||||
|                                 item: item, | ||||
|                                 isVisibleCreator: true | ||||
|                             ) | ||||
|                         } | ||||
|                     } | ||||
|                     .padding(.horizontal, 13.3) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         .onAppear { | ||||
|             if !self.creatorList.isEmpty { | ||||
|                 selectedCreatorId = creatorList[0].creatorId | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #Preview { | ||||
|     SeriesByChannelView( | ||||
|         title: "채널별 추천 시리즈", | ||||
|         creatorList: [ | ||||
|             ContentCreatorResponse( | ||||
|                 creatorId: 1, | ||||
|                 creatorNickname: "유저1", | ||||
|                 creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png" | ||||
|             ), | ||||
|             ContentCreatorResponse( | ||||
|                 creatorId: 2, | ||||
|                 creatorNickname: "유저2", | ||||
|                 creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png" | ||||
|             ) | ||||
|         ], | ||||
|         seriesList: [ | ||||
|             SeriesListItem( | ||||
|                 seriesId: 1, | ||||
|                 title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)", | ||||
|                 coverImage: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                 publishedDaysOfWeek: "매주 수, 토요일", | ||||
|                 isComplete: true, | ||||
|                 creator: SeriesListItemCreator( | ||||
|                     creatorId: 1, | ||||
|                     nickname: "creator", | ||||
|                     profileImage: "https://test-cf.sodalive.net/profile/default-profile.png" | ||||
|                 ), | ||||
|                 numberOfContent: 10, | ||||
|                 isNew: true, | ||||
|                 isPopular: true | ||||
|             ), | ||||
|             SeriesListItem( | ||||
|                 seriesId: 1, | ||||
|                 title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)", | ||||
|                 coverImage: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|                 publishedDaysOfWeek: "매주 수, 토요일", | ||||
|                 isComplete: true, | ||||
|                 creator: SeriesListItemCreator( | ||||
|                     creatorId: 1, | ||||
|                     nickname: "creator", | ||||
|                     profileImage: "https://test-cf.sodalive.net/profile/default-profile.png" | ||||
|                 ), | ||||
|                 numberOfContent: 10, | ||||
|                 isNew: true, | ||||
|                 isPopular: true | ||||
|             ) | ||||
|         ], | ||||
|         onClickCreator: { _ in } | ||||
|     ) | ||||
| } | ||||
| @@ -215,6 +215,9 @@ struct ContentView: View { | ||||
|             case .searchChannel: | ||||
|                 SearchChannelView() | ||||
|                  | ||||
|             case .contentMain(let startTab): | ||||
|                 ContentMainViewV2(selectedTab: startTab) | ||||
|                  | ||||
|             default: | ||||
|                 EmptyView() | ||||
|                     .frame(width: 0, height: 0, alignment: .topLeading) | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Yu Sung
					Yu Sung