콘텐츠 메인
- 단편 탭 UI 페이지 생성
This commit is contained in:
		| @@ -45,6 +45,11 @@ enum ContentApi { | |||||||
|     case getContentMainSeries |     case getContentMainSeries | ||||||
|     case getRecommendSeriesListByGenre(genreId: Int) |     case getRecommendSeriesListByGenre(genreId: Int) | ||||||
|     case getRecommendSeriesByCreator(creatorId: Int) |     case getRecommendSeriesByCreator(creatorId: Int) | ||||||
|  |      | ||||||
|  |     case getContentMainContent | ||||||
|  |     case getContentMainNewContentOfTheme(theme: String, isAdultContentVisible: Bool, contentType: ContentType) | ||||||
|  |     case getDailyContentRanking(sortType: String) | ||||||
|  |     case getRecommendContentByTag(tag: String) | ||||||
| } | } | ||||||
|  |  | ||||||
| extension ContentApi: TargetType { | extension ContentApi: TargetType { | ||||||
| @@ -155,6 +160,18 @@ extension ContentApi: TargetType { | |||||||
|              |              | ||||||
|         case .getRecommendSeriesByCreator: |         case .getRecommendSeriesByCreator: | ||||||
|             return "/v2/audio-content/main/series/recommend-series-by-creator" |             return "/v2/audio-content/main/series/recommend-series-by-creator" | ||||||
|  |              | ||||||
|  |         case .getContentMainContent: | ||||||
|  |             return "/v2/audio-content/main/content" | ||||||
|  |              | ||||||
|  |         case .getContentMainNewContentOfTheme: | ||||||
|  |             return "/v2/audio-content/main/content/new-content-by-theme" | ||||||
|  |              | ||||||
|  |         case .getDailyContentRanking: | ||||||
|  |             return "/v2/audio-content/main/content/ranking" | ||||||
|  |              | ||||||
|  |         case .getRecommendContentByTag: | ||||||
|  |             return "/v2/audio-content/main/content/recommend-content-by-tag" | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|      |      | ||||||
| @@ -169,7 +186,8 @@ extension ContentApi: TargetType { | |||||||
|         case .getMainBannerList, .getMainOrderList, .getNewContentUploadCreatorList, .getCurationList, .getAudioContentByTheme: |         case .getMainBannerList, .getMainOrderList, .getNewContentUploadCreatorList, .getCurationList, .getAudioContentByTheme: | ||||||
|             return .get |             return .get | ||||||
|              |              | ||||||
|         case .getContentMainHome, .getPopularContentByCreator, .getContentMainSeries, .getRecommendSeriesListByGenre, .getRecommendSeriesByCreator: |         case .getContentMainHome, .getPopularContentByCreator, .getContentMainSeries, .getRecommendSeriesListByGenre, .getRecommendSeriesByCreator, .getContentMainContent, | ||||||
|  |                 .getContentMainNewContentOfTheme, .getDailyContentRanking, .getRecommendContentByTag: | ||||||
|             return .get |             return .get | ||||||
|              |              | ||||||
|         case .likeContent, .modifyAudioContent, .modifyComment, .unpinContent: |         case .likeContent, .modifyAudioContent, .modifyComment, .unpinContent: | ||||||
| @@ -326,7 +344,7 @@ extension ContentApi: TargetType { | |||||||
|              |              | ||||||
|             return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) |             return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) | ||||||
|              |              | ||||||
|         case .getContentMainHome, .getContentMainSeries: |         case .getContentMainHome, .getContentMainSeries, .getContentMainContent: | ||||||
|             return .requestPlain |             return .requestPlain | ||||||
|              |              | ||||||
|         case .getRecommendSeriesListByGenre(let genreId): |         case .getRecommendSeriesListByGenre(let genreId): | ||||||
| @@ -340,6 +358,22 @@ extension ContentApi: TargetType { | |||||||
|         case .getRecommendSeriesByCreator(let creatorId): |         case .getRecommendSeriesByCreator(let creatorId): | ||||||
|             let parameters = ["creatorId": creatorId] |             let parameters = ["creatorId": creatorId] | ||||||
|             return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) |             return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) | ||||||
|  |              | ||||||
|  |         case .getContentMainNewContentOfTheme(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 .getDailyContentRanking(let sortType): | ||||||
|  |             let parameters = ["sortType": sortType] | ||||||
|  |             return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) | ||||||
|  |              | ||||||
|  |         case .getRecommendContentByTag(let tag): | ||||||
|  |             let parameters = ["tag": tag] | ||||||
|  |             return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|      |      | ||||||
|   | |||||||
| @@ -40,6 +40,7 @@ struct ContentMainRankingSortView: View { | |||||||
|                         } |                         } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |             .padding(.horizontal, 13.3) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
| @@ -0,0 +1,42 @@ | |||||||
|  | // | ||||||
|  | //  ContentMainTabContentRepository.swift | ||||||
|  | //  SodaLive | ||||||
|  | // | ||||||
|  | //  Created by klaus on 2/21/25. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import Foundation | ||||||
|  | import CombineMoya | ||||||
|  | import Combine | ||||||
|  | import Moya | ||||||
|  |  | ||||||
|  | final class ContentMainTabContentRepository { | ||||||
|  |      | ||||||
|  |     private let api = MoyaProvider<ContentApi>() | ||||||
|  |      | ||||||
|  |     func getContentMainContent() -> AnyPublisher<Response, MoyaError> { | ||||||
|  |         return api.requestPublisher(.getContentMainContent) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     func getNewContentOfTheme(theme: String) -> AnyPublisher<Response, MoyaError> { | ||||||
|  |         return api.requestPublisher( | ||||||
|  |             .getContentMainNewContentOfTheme( | ||||||
|  |                 theme: theme, | ||||||
|  |                 isAdultContentVisible: UserDefaults.isAdultContentVisible(), | ||||||
|  |                 contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL | ||||||
|  |             ) | ||||||
|  |         ) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     func getContentRanking(sortType: String) -> AnyPublisher<Response, MoyaError> { | ||||||
|  |         return api.requestPublisher(.getDailyContentRanking(sortType: sortType)) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     func getRecommendContentByTag(tag: String) -> AnyPublisher<Response, MoyaError> { | ||||||
|  |         return api.requestPublisher(.getRecommendContentByTag(tag: tag)) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     func getPopularContentByCreator(creatorId: Int) -> AnyPublisher<Response, MoyaError> { | ||||||
|  |         return api.requestPublisher(.getPopularContentByCreator(creatorId: creatorId)) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,104 @@ | |||||||
|  | // | ||||||
|  | //  ContentMainTabContentView.swift | ||||||
|  | //  SodaLive | ||||||
|  | // | ||||||
|  | //  Created by klaus on 2/21/25. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import SwiftUI | ||||||
|  |  | ||||||
|  | struct ContentMainTabContentView: View { | ||||||
|  |      | ||||||
|  |     @StateObject var viewModel = ContentMainTabContentViewModel() | ||||||
|  |      | ||||||
|  |     var body: some View { | ||||||
|  |         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.contentThemeList.isEmpty { | ||||||
|  |                         ContentMainNewContentViewV2( | ||||||
|  |                             title: "새로운 단편", | ||||||
|  |                             onClickMore: {}, | ||||||
|  |                             themeList: viewModel.contentThemeList, | ||||||
|  |                             contentList: viewModel.newContentList | ||||||
|  |                         ) { | ||||||
|  |                             viewModel.getNewContentOfTheme(theme: $0) | ||||||
|  |                         } | ||||||
|  |                         .padding(.top, 30) | ||||||
|  |                     } | ||||||
|  |                      | ||||||
|  |                     if !viewModel.rankSortTypeList.isEmpty { | ||||||
|  |                         ContentMainTabRankContentView( | ||||||
|  |                             title: "일간 랭킹", | ||||||
|  |                             isMore: false, | ||||||
|  |                             onClickMore: {}, | ||||||
|  |                             sortList: viewModel.rankSortTypeList, | ||||||
|  |                             onClickSort: { viewModel.getContentRanking(sort: $0) }, | ||||||
|  |                             contentList: viewModel.rankContentList | ||||||
|  |                         ) | ||||||
|  |                         .padding(.top, 30) | ||||||
|  |                     } | ||||||
|  |                      | ||||||
|  |                     if !viewModel.contentRankCreatorList.isEmpty { | ||||||
|  |                         ContentByChannelView( | ||||||
|  |                             title: "채널별 추천 단편", | ||||||
|  |                             creatorList: viewModel.contentRankCreatorList, | ||||||
|  |                             contentList: viewModel.salesCountRankContentList, | ||||||
|  |                             onClickCreator: { | ||||||
|  |                                 viewModel.getPopularContentByCreator(creatorId: $0) | ||||||
|  |                             } | ||||||
|  |                         ) | ||||||
|  |                         .padding(.top, 30) | ||||||
|  |                     } | ||||||
|  |                      | ||||||
|  |                     if !viewModel.eventBannerList.isEmpty { | ||||||
|  |                         SectionEventBannerView(items: viewModel.eventBannerList) | ||||||
|  |                             .padding(.top, 30) | ||||||
|  |                     } | ||||||
|  |                      | ||||||
|  |                     if !viewModel.tagList.isEmpty { | ||||||
|  |                         ContentMainTagCurationView( | ||||||
|  |                             tagList: viewModel.tagList, | ||||||
|  |                             contentList: viewModel.tagCurationContentList | ||||||
|  |                         ) { | ||||||
|  |                             viewModel.getRecommendContentByTag(tag: $0) | ||||||
|  |                         } | ||||||
|  |                         .padding(.top, 30) | ||||||
|  |                     } | ||||||
|  |                      | ||||||
|  |                     if !viewModel.curationList.isEmpty { | ||||||
|  |                         ContentMainCurationViewV2(curationList: viewModel.curationList) | ||||||
|  |                         .padding(.top, 30) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 .onAppear { | ||||||
|  |                     viewModel.fetchData() | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         .popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) { | ||||||
|  |             HStack { | ||||||
|  |                 Spacer() | ||||||
|  |                 Text(viewModel.errorMessage) | ||||||
|  |                     .padding(.vertical, 13.3) | ||||||
|  |                     .frame(width: screenSize().width - 66.7, alignment: .center) | ||||||
|  |                     .font(.custom(Font.medium.rawValue, size: 12)) | ||||||
|  |                     .background(Color.button) | ||||||
|  |                     .foregroundColor(Color.white) | ||||||
|  |                     .multilineTextAlignment(.leading) | ||||||
|  |                     .cornerRadius(20) | ||||||
|  |                     .padding(.bottom, 66.7) | ||||||
|  |                 Spacer() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #Preview { | ||||||
|  |     ContentMainTabContentView() | ||||||
|  | } | ||||||
| @@ -0,0 +1,231 @@ | |||||||
|  | // | ||||||
|  | //  ContentMainTabContentViewModel.swift | ||||||
|  | //  SodaLive | ||||||
|  | // | ||||||
|  | //  Created by klaus on 2/21/25. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import Foundation | ||||||
|  | import Combine | ||||||
|  |  | ||||||
|  | final class ContentMainTabContentViewModel: ObservableObject { | ||||||
|  |     private let repository = ContentMainTabContentRepository() | ||||||
|  |     private var subscription = Set<AnyCancellable>() | ||||||
|  |      | ||||||
|  |     @Published var errorMessage = "" | ||||||
|  |     @Published var isShowPopup = false | ||||||
|  |     @Published var isLoading = false | ||||||
|  |      | ||||||
|  |     @Published var bannerList: [GetAudioContentBannerResponse] = [] | ||||||
|  |     @Published var contentThemeList: [String] = [] | ||||||
|  |     @Published var newContentList: [GetAudioContentMainItem] = [] | ||||||
|  |     @Published var rankSortTypeList: [String] = [] | ||||||
|  |     @Published var rankContentList: [GetAudioContentRankingItem] = [] | ||||||
|  |     @Published var contentRankCreatorList: [ContentCreatorResponse] = [] | ||||||
|  |     @Published var salesCountRankContentList: [GetAudioContentRankingItem] = [] | ||||||
|  |     @Published var eventBannerList: [EventItem] = [] | ||||||
|  |     @Published var tagList: [String] = [] | ||||||
|  |     @Published var tagCurationContentList: [GetAudioContentMainItem] = [] | ||||||
|  |     @Published var curationList: [GetContentCurationResponse] = [] | ||||||
|  |      | ||||||
|  |     func fetchData() { | ||||||
|  |         isLoading = true | ||||||
|  |         repository.getContentMainContent() | ||||||
|  |             .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<GetContentMainTabContentResponse>.self, from: responseData) | ||||||
|  |                      | ||||||
|  |                     if let data = decoded.data, decoded.success { | ||||||
|  |                         self.bannerList = data.bannerList | ||||||
|  |                         self.contentThemeList = ["전체"] + data.contentThemeList | ||||||
|  |                         self.newContentList = data.newContentList | ||||||
|  |                         self.rankSortTypeList = data.rankSortTypeList | ||||||
|  |                         self.rankContentList = data.rankContentList | ||||||
|  |                         self.contentRankCreatorList = data.contentRankCreatorList | ||||||
|  |                         self.salesCountRankContentList = data.salesCountRankContentList | ||||||
|  |                         self.eventBannerList = data.eventBannerList.eventList | ||||||
|  |                         self.tagList = data.tagList | ||||||
|  |                         self.tagCurationContentList = data.tagCurationContentList | ||||||
|  |                         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 getNewContentOfTheme(theme: String) { | ||||||
|  |         isLoading = true | ||||||
|  |          | ||||||
|  |         repository.getNewContentOfTheme(theme: theme == "전체" ? "" : theme) | ||||||
|  |             .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<[GetAudioContentMainItem]>.self, from: responseData) | ||||||
|  |                      | ||||||
|  |                     if let data = decoded.data, decoded.success { | ||||||
|  |                         self.newContentList = 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 getContentRanking(sort: String = "매출") { | ||||||
|  |         isLoading = true | ||||||
|  |         repository.getContentRanking(sortType: sort) | ||||||
|  |             .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<[GetAudioContentRankingItem]>.self, from: responseData) | ||||||
|  |                      | ||||||
|  |                     if let data = decoded.data, decoded.success { | ||||||
|  |                         self.rankContentList = 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 getRecommendContentByTag(tag: String) { | ||||||
|  |         isLoading = true | ||||||
|  |         repository.getRecommendContentByTag(tag: tag) | ||||||
|  |             .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<[GetAudioContentMainItem]>.self, from: responseData) | ||||||
|  |                      | ||||||
|  |                     if let data = decoded.data, decoded.success { | ||||||
|  |                         self.tagCurationContentList = 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 getPopularContentByCreator(creatorId: Int) { | ||||||
|  |         isLoading = true | ||||||
|  |         repository.getPopularContentByCreator(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<[GetAudioContentRankingItem]>.self, from: responseData) | ||||||
|  |                      | ||||||
|  |                     if let data = decoded.data, decoded.success { | ||||||
|  |                         self.salesCountRankContentList = 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) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -43,6 +43,7 @@ struct ContentMainTabRankContentView: View { | |||||||
|                         .onTapGesture { onClickMore() } |                         .onTapGesture { onClickMore() } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|  |             .padding(.horizontal, 13.3) | ||||||
|              |              | ||||||
|             if !sortList.isEmpty { |             if !sortList.isEmpty { | ||||||
|                 ContentMainRankingSortView( |                 ContentMainRankingSortView( | ||||||
| @@ -97,6 +98,7 @@ struct ContentMainTabRankContentView: View { | |||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|  |                 .padding(.horizontal, 13.3) | ||||||
|                 .frame(height: 207) |                 .frame(height: 207) | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|   | |||||||
| @@ -0,0 +1,231 @@ | |||||||
|  | // | ||||||
|  | //  ContentMainTagCurationView.swift | ||||||
|  | //  SodaLive | ||||||
|  | // | ||||||
|  | //  Created by klaus on 2/22/25. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import SwiftUI | ||||||
|  | import Kingfisher | ||||||
|  |  | ||||||
|  | struct ContentMainTagCurationView: View { | ||||||
|  |      | ||||||
|  |     let tagList: [String] | ||||||
|  |     let contentList: [GetAudioContentMainItem] | ||||||
|  |     let selectTag: (String) -> Void | ||||||
|  |      | ||||||
|  |     let tagColumns = [ | ||||||
|  |         GridItem(.flexible()), | ||||||
|  |         GridItem(.flexible()), | ||||||
|  |         GridItem(.flexible()), | ||||||
|  |         GridItem(.flexible()) | ||||||
|  |     ] | ||||||
|  |      | ||||||
|  |     let contentColumns = [ | ||||||
|  |         GridItem(.flexible()), | ||||||
|  |         GridItem(.flexible()), | ||||||
|  |         GridItem(.flexible()) | ||||||
|  |     ] | ||||||
|  |      | ||||||
|  |     @State private var selectedTag = "" | ||||||
|  |      | ||||||
|  |     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) | ||||||
|  |              | ||||||
|  |             LazyVGrid(columns: tagColumns, spacing: 6) { | ||||||
|  |                 ForEach(0..<tagList.count, id: \.self) { index in | ||||||
|  |                     let tag = tagList[index] | ||||||
|  |                     Text(tagList[index]) | ||||||
|  |                         .font(.custom(Font.medium.rawValue, size: 10)) | ||||||
|  |                         .foregroundColor( | ||||||
|  |                             selectedTag == tag ? | ||||||
|  |                                 .button: | ||||||
|  |                                 .gray77 | ||||||
|  |                         ) | ||||||
|  |                         .padding(.vertical, 10) | ||||||
|  |                         .frame(width: (screenSize().width - 18 - 26.7) / 4) | ||||||
|  |                         .overlay( | ||||||
|  |                             RoundedRectangle(cornerRadius: 2.6) | ||||||
|  |                                 .strokeBorder(lineWidth: 1) | ||||||
|  |                                 .foregroundColor( | ||||||
|  |                                     selectedTag == tag ? | ||||||
|  |                                         .button: | ||||||
|  |                                         .gray77 | ||||||
|  |                                 ) | ||||||
|  |                         ) | ||||||
|  |                         .onTapGesture { | ||||||
|  |                             if selectedTag != tag { | ||||||
|  |                                 selectedTag = tag | ||||||
|  |                                 selectTag(tag) | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             .padding(.horizontal, 13.3) | ||||||
|  |              | ||||||
|  |             LazyVGrid(columns: contentColumns, spacing: 13.3) { | ||||||
|  |                 ForEach(0..<contentList.count, id: \.self) { index in | ||||||
|  |                     ContentMainTagCurationContentView( | ||||||
|  |                         item: contentList[index], | ||||||
|  |                         itemWidth: (screenSize().width - 40) / 3 | ||||||
|  |                     ) | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             .padding(.horizontal, 13.3) | ||||||
|  |         } | ||||||
|  |         .onAppear { | ||||||
|  |             selectedTag = tagList[0] | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct ContentMainTagCurationContentView: View { | ||||||
|  |      | ||||||
|  |     let item: GetAudioContentMainItem | ||||||
|  |     let itemWidth: CGFloat | ||||||
|  |      | ||||||
|  |     var body: some View { | ||||||
|  |         VStack(alignment: .leading, spacing: 8) { | ||||||
|  |             ZStack(alignment: .bottom) { | ||||||
|  |                 KFImage(URL(string: item.coverImageUrl)) | ||||||
|  |                     .cancelOnDisappear(true) | ||||||
|  |                     .downsampling( | ||||||
|  |                         size: CGSize( | ||||||
|  |                             width: itemWidth, | ||||||
|  |                             height: itemWidth | ||||||
|  |                         ) | ||||||
|  |                     ) | ||||||
|  |                     .resizable() | ||||||
|  |                     .scaledToFill() | ||||||
|  |                     .frame(width: itemWidth, height: itemWidth, alignment: .top) | ||||||
|  |                     .cornerRadius(2.7) | ||||||
|  |                  | ||||||
|  |                 VStack(spacing: 0) { | ||||||
|  |                     Spacer() | ||||||
|  |                      | ||||||
|  |                     HStack(spacing: 0) { | ||||||
|  |                         HStack(spacing: 2) { | ||||||
|  |                             if item.price > 0 { | ||||||
|  |                                 Image("ic_card_can_gray") | ||||||
|  |                                  | ||||||
|  |                                 Text("\(item.price)") | ||||||
|  |                                     .font(.custom(Font.medium.rawValue, size: 8.5)) | ||||||
|  |                                     .foregroundColor(Color.white) | ||||||
|  |                             } else { | ||||||
|  |                                 Text("무료") | ||||||
|  |                                     .font(.custom(Font.medium.rawValue, size: 8.5)) | ||||||
|  |                                     .foregroundColor(Color.white) | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                         .padding(3) | ||||||
|  |                         .background(Color.gray33.opacity(0.7)) | ||||||
|  |                         .cornerRadius(10) | ||||||
|  |                         .padding(.leading, 2.7) | ||||||
|  |                         .padding(.bottom, 2.7) | ||||||
|  |                          | ||||||
|  |                         Spacer() | ||||||
|  |                          | ||||||
|  |                         HStack(spacing: 2) { | ||||||
|  |                             Text(item.duration) | ||||||
|  |                                 .font(.custom(Font.medium.rawValue, size: 8.5)) | ||||||
|  |                                 .foregroundColor(Color.white) | ||||||
|  |                         } | ||||||
|  |                         .padding(3) | ||||||
|  |                         .background(Color.gray33.opacity(0.7)) | ||||||
|  |                         .cornerRadius(10) | ||||||
|  |                         .padding(.trailing, 2.7) | ||||||
|  |                         .padding(.bottom, 2.7) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             .frame(width: itemWidth, height: itemWidth) | ||||||
|  |              | ||||||
|  |             Text(item.title) | ||||||
|  |                 .font(.custom(Font.medium.rawValue, size: 13.3)) | ||||||
|  |                 .foregroundColor(Color.grayd2) | ||||||
|  |                 .frame(width: itemWidth, alignment: .leading) | ||||||
|  |                 .multilineTextAlignment(.leading) | ||||||
|  |                 .fixedSize(horizontal: false, vertical: true) | ||||||
|  |                 .lineLimit(2) | ||||||
|  |              | ||||||
|  |             HStack(spacing: 5.3) { | ||||||
|  |                 KFImage(URL(string: item.creatorProfileImageUrl)) | ||||||
|  |                     .cancelOnDisappear(true) | ||||||
|  |                     .downsampling( | ||||||
|  |                         size: CGSize( | ||||||
|  |                             width: 21.3, | ||||||
|  |                             height: 21.3 | ||||||
|  |                         ) | ||||||
|  |                     ) | ||||||
|  |                     .resizable() | ||||||
|  |                     .scaledToFill() | ||||||
|  |                     .frame(width: 21.3, height: 21.3) | ||||||
|  |                     .clipShape(Circle()) | ||||||
|  |                     .onTapGesture { AppState.shared.setAppStep(step: .creatorDetail(userId: item.creatorId)) } | ||||||
|  |                  | ||||||
|  |                 Text(item.creatorNickname) | ||||||
|  |                     .font(.custom(Font.medium.rawValue, size: 12)) | ||||||
|  |                     .foregroundColor(.gray77) | ||||||
|  |                     .lineLimit(1) | ||||||
|  |             } | ||||||
|  |             .padding(.bottom, 10) | ||||||
|  |         } | ||||||
|  |         .onTapGesture { | ||||||
|  |             AppState.shared | ||||||
|  |                 .setAppStep(step: .contentDetail(contentId: item.contentId)) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #Preview { | ||||||
|  |     ContentMainTagCurationView( | ||||||
|  |         tagList: ["test", "test2", "test3", "test4", "test5", "test6", "test7"], | ||||||
|  |         contentList: [ | ||||||
|  |             GetAudioContentMainItem( | ||||||
|  |                 contentId: 1, | ||||||
|  |                 coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||||
|  |                 title: "ㅓ처랴햐햫햐햐", | ||||||
|  |                 creatorId: 8, | ||||||
|  |                 creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||||
|  |                 creatorNickname: "유저1", | ||||||
|  |                 price: 100, | ||||||
|  |                 duration: "00:00:30" | ||||||
|  |             ), | ||||||
|  |             GetAudioContentMainItem( | ||||||
|  |                 contentId: 2, | ||||||
|  |                 coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||||
|  |                 title: "ㅓ처랴햐햫햐햐", | ||||||
|  |                 creatorId: 8, | ||||||
|  |                 creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||||
|  |                 creatorNickname: "유저2", | ||||||
|  |                 price: 0, | ||||||
|  |                 duration: "00:00:30" | ||||||
|  |             ), | ||||||
|  |             GetAudioContentMainItem( | ||||||
|  |                 contentId: 3, | ||||||
|  |                 coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||||
|  |                 title: "ㅓ처랴햐햫햐햐", | ||||||
|  |                 creatorId: 8, | ||||||
|  |                 creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||||
|  |                 creatorNickname: "유저3", | ||||||
|  |                 price: 1000, | ||||||
|  |                 duration: "00:00:30" | ||||||
|  |             ), | ||||||
|  |             GetAudioContentMainItem( | ||||||
|  |                 contentId: 4, | ||||||
|  |                 coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||||
|  |                 title: "ㅓ처랴햐햫햐햐", | ||||||
|  |                 creatorId: 8, | ||||||
|  |                 creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||||
|  |                 creatorNickname: "유저3", | ||||||
|  |                 price: 50000, | ||||||
|  |                 duration: "00:00:30" | ||||||
|  |             ) | ||||||
|  |         ], | ||||||
|  |         selectTag: { _ in } | ||||||
|  |     ) | ||||||
|  | } | ||||||
| @@ -0,0 +1,20 @@ | |||||||
|  | // | ||||||
|  | //  GetContentMainTabContentResponse.swift | ||||||
|  | //  SodaLive | ||||||
|  | // | ||||||
|  | //  Created by klaus on 2/21/25. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | struct GetContentMainTabContentResponse: Decodable { | ||||||
|  |     let bannerList: [GetAudioContentBannerResponse] | ||||||
|  |     let contentThemeList: [String] | ||||||
|  |     let newContentList: [GetAudioContentMainItem] | ||||||
|  |     let rankSortTypeList: [String] | ||||||
|  |     let rankContentList: [GetAudioContentRankingItem] | ||||||
|  |     let contentRankCreatorList: [ContentCreatorResponse] | ||||||
|  |     let salesCountRankContentList: [GetAudioContentRankingItem] | ||||||
|  |     let eventBannerList: GetEventResponse | ||||||
|  |     let tagList: [String] | ||||||
|  |     let tagCurationContentList: [GetAudioContentMainItem] | ||||||
|  |     let curationList: [GetContentCurationResponse] | ||||||
|  | } | ||||||
| @@ -23,7 +23,8 @@ struct ContentByChannelView: View { | |||||||
|         VStack(alignment: .leading, spacing: 20) { |         VStack(alignment: .leading, spacing: 20) { | ||||||
|             Text(title) |             Text(title) | ||||||
|                 .font(.custom(Font.bold.rawValue, size: 18.3)) |                 .font(.custom(Font.bold.rawValue, size: 18.3)) | ||||||
|                 .foregroundColor(Color(hex: "eeeeee")) |                 .foregroundColor(.grayee) | ||||||
|  |                 .padding(.horizontal, 13.3) | ||||||
|              |              | ||||||
|             ScrollView(.horizontal) { |             ScrollView(.horizontal) { | ||||||
|                 HStack(spacing: 22) { |                 HStack(spacing: 22) { | ||||||
| @@ -41,6 +42,7 @@ struct ContentByChannelView: View { | |||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|  |                 .padding(.horizontal, 13.3) | ||||||
|             } |             } | ||||||
|              |              | ||||||
|             LazyVGrid(columns: columns, spacing: 13.3) { |             LazyVGrid(columns: columns, spacing: 13.3) { | ||||||
| @@ -111,6 +113,7 @@ struct ContentByChannelView: View { | |||||||
|                                 .setAppStep(step: .creatorDetail(userId: content.creatorId)) |                                 .setAppStep(step: .creatorDetail(userId: content.creatorId)) | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|  |                     .padding(.horizontal, 13.3) | ||||||
|                     .onTapGesture { |                     .onTapGesture { | ||||||
|                         AppState |                         AppState | ||||||
|                             .shared |                             .shared | ||||||
|   | |||||||
| @@ -0,0 +1,56 @@ | |||||||
|  | // | ||||||
|  | //  ContentMainContentThemeView.swift | ||||||
|  | //  SodaLive | ||||||
|  | // | ||||||
|  | //  Created by klaus on 2/21/25. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import SwiftUI | ||||||
|  |  | ||||||
|  | struct ContentMainContentThemeView: View { | ||||||
|  |      | ||||||
|  |     let themeList: [String] | ||||||
|  |     let selectTheme: (String) -> Void | ||||||
|  |      | ||||||
|  |     @Binding var selectedTheme: String | ||||||
|  |      | ||||||
|  |     var body: some View { | ||||||
|  |         ScrollView(.horizontal, showsIndicators: false) { | ||||||
|  |             HStack(alignment: .top, spacing: 8) { | ||||||
|  |                 ForEach(0..<themeList.count, id: \.self) { index in | ||||||
|  |                     let theme = themeList[index] | ||||||
|  |                     Text(theme) | ||||||
|  |                         .font(.custom(Font.medium.rawValue, size: 14.7)) | ||||||
|  |                         .foregroundColor(selectedTheme == theme ? Color.button : Color.gray77) | ||||||
|  |                         .padding(.horizontal, 13.3) | ||||||
|  |                         .padding(.vertical, 9.3) | ||||||
|  |                         .border( | ||||||
|  |                             selectedTheme == theme ? Color.button : Color.grayee, | ||||||
|  |                             width: 1 | ||||||
|  |                         ) | ||||||
|  |                         .cornerRadius(16.7) | ||||||
|  |                         .overlay( | ||||||
|  |                             RoundedRectangle(cornerRadius: CGFloat(16.7)) | ||||||
|  |                                 .stroke(lineWidth: 1) | ||||||
|  |                                 .foregroundColor(selectedTheme == theme ? Color.button : Color.grayee) | ||||||
|  |                         ) | ||||||
|  |                         .onTapGesture { | ||||||
|  |                             if selectedTheme != theme { | ||||||
|  |                                 selectedTheme = theme | ||||||
|  |                                 selectTheme(theme) | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             .padding(.horizontal, 13.3) | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #Preview { | ||||||
|  |     ContentMainContentThemeView( | ||||||
|  |         themeList: ["전체", "test", "test2"], | ||||||
|  |         selectTheme: { _ in }, | ||||||
|  |         selectedTheme: .constant("전체") | ||||||
|  |     ) | ||||||
|  | } | ||||||
| @@ -0,0 +1,87 @@ | |||||||
|  | // | ||||||
|  | //  ContentMainCurationViewV2.swift | ||||||
|  | //  SodaLive | ||||||
|  | // | ||||||
|  | //  Created by klaus on 2/21/25. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import SwiftUI | ||||||
|  |  | ||||||
|  | struct ContentMainCurationViewV2: View { | ||||||
|  |      | ||||||
|  |     let curationList: [GetContentCurationResponse] | ||||||
|  |      | ||||||
|  |     var body: some View { | ||||||
|  |         LazyVStack(spacing: 30) { | ||||||
|  |             ForEach(0..<curationList.count, id: \.self) { index in | ||||||
|  |                 let curation = curationList[index] | ||||||
|  |                 ContentMainCurationItemViewV2(curation: curation) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | struct ContentMainCurationItemViewV2: View { | ||||||
|  |     let curation: GetContentCurationResponse | ||||||
|  |      | ||||||
|  |     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] | ||||||
|  |                         ContentMainItemView(item: item) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 .padding(.horizontal, 13.3) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #Preview { | ||||||
|  |     ContentMainCurationViewV2( | ||||||
|  |         curationList: [ | ||||||
|  |             GetContentCurationResponse( | ||||||
|  |                 title: "test1", | ||||||
|  |                 items: [ | ||||||
|  |                     GetAudioContentMainItem( | ||||||
|  |                         contentId: 1, | ||||||
|  |                         coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||||
|  |                         title: "ㅓ처랴햐햫햐햐", | ||||||
|  |                         creatorId: 8, | ||||||
|  |                         creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||||
|  |                         creatorNickname: "유저1", | ||||||
|  |                         price: 10, | ||||||
|  |                         duration: "00:00:30" | ||||||
|  |                     ), | ||||||
|  |                     GetAudioContentMainItem( | ||||||
|  |                         contentId: 2, | ||||||
|  |                         coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||||
|  |                         title: "ㅓ처랴햐햫햐햐", | ||||||
|  |                         creatorId: 8, | ||||||
|  |                         creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||||
|  |                         creatorNickname: "유저2", | ||||||
|  |                         price: 10, | ||||||
|  |                         duration: "00:00:30" | ||||||
|  |                     ), | ||||||
|  |                     GetAudioContentMainItem( | ||||||
|  |                         contentId: 3, | ||||||
|  |                         coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||||
|  |                         title: "ㅓ처랴햐햫햐햐", | ||||||
|  |                         creatorId: 8, | ||||||
|  |                         creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||||
|  |                         creatorNickname: "유저3", | ||||||
|  |                         price: 10, | ||||||
|  |                         duration: "00:00:30" | ||||||
|  |                     ) | ||||||
|  |                 ] | ||||||
|  |             ) | ||||||
|  |         ] | ||||||
|  |     ) | ||||||
|  | } | ||||||
| @@ -0,0 +1,97 @@ | |||||||
|  | // | ||||||
|  | //  ContentMainNewContentViewV2.swift | ||||||
|  | //  SodaLive | ||||||
|  | // | ||||||
|  | //  Created by klaus on 2/21/25. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import SwiftUI | ||||||
|  |  | ||||||
|  | struct ContentMainNewContentViewV2: View { | ||||||
|  |      | ||||||
|  |     let title: String | ||||||
|  |     let onClickMore: () -> Void | ||||||
|  |     let themeList: [String] | ||||||
|  |     let contentList: [GetAudioContentMainItem] | ||||||
|  |      | ||||||
|  |     let selectTheme: (String) -> Void | ||||||
|  |     @State private var selectedTheme = "전체" | ||||||
|  |      | ||||||
|  |     var body: some View { | ||||||
|  |         LazyVStack(spacing: 13.3) { | ||||||
|  |             HStack(spacing: 0) { | ||||||
|  |                 Text(title) | ||||||
|  |                     .font(.custom(Font.bold.rawValue, size: 18.3)) | ||||||
|  |                     .foregroundColor(Color(hex: "eeeeee")) | ||||||
|  |                  | ||||||
|  |                 Spacer() | ||||||
|  |                  | ||||||
|  |                 Image("ic_forward") | ||||||
|  |                     .resizable() | ||||||
|  |                     .frame(width: 20, height: 20) | ||||||
|  |                     .onTapGesture { | ||||||
|  |                     } | ||||||
|  |             } | ||||||
|  |             .padding(.horizontal, 13.3) | ||||||
|  |              | ||||||
|  |             ContentMainContentThemeView( | ||||||
|  |                 themeList: themeList, | ||||||
|  |                 selectTheme: selectTheme, | ||||||
|  |                 selectedTheme: $selectedTheme | ||||||
|  |             ) | ||||||
|  |              | ||||||
|  |             ScrollView(.horizontal, showsIndicators: false) { | ||||||
|  |                 HStack(spacing: 13.3) { | ||||||
|  |                     ForEach(0..<contentList.count, id: \.self) { index in | ||||||
|  |                         ContentMainItemView(item: contentList[index]) | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 .padding(.horizontal, 13.3) | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         .onAppear { | ||||||
|  |             selectedTheme = themeList[0] | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #Preview { | ||||||
|  |     ContentMainNewContentViewV2( | ||||||
|  |         title: "새로운 단편", | ||||||
|  |         onClickMore: {}, | ||||||
|  |         themeList: ["전체", "테스트1", "테스트2"], | ||||||
|  |         contentList: [ | ||||||
|  |             GetAudioContentMainItem( | ||||||
|  |                 contentId: 1, | ||||||
|  |                 coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||||
|  |                 title: "ㅓ처랴햐햫햐햐", | ||||||
|  |                 creatorId: 8, | ||||||
|  |                 creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||||
|  |                 creatorNickname: "유저1", | ||||||
|  |                 price: 10, | ||||||
|  |                 duration: "00:00:30" | ||||||
|  |             ), | ||||||
|  |             GetAudioContentMainItem( | ||||||
|  |                 contentId: 2, | ||||||
|  |                 coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||||
|  |                 title: "ㅓ처랴햐햫햐햐", | ||||||
|  |                 creatorId: 8, | ||||||
|  |                 creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||||
|  |                 creatorNickname: "유저2", | ||||||
|  |                 price: 10, | ||||||
|  |                 duration: "00:00:30" | ||||||
|  |             ), | ||||||
|  |             GetAudioContentMainItem( | ||||||
|  |                 contentId: 3, | ||||||
|  |                 coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||||
|  |                 title: "ㅓ처랴햐햫햐햐", | ||||||
|  |                 creatorId: 8, | ||||||
|  |                 creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||||
|  |                 creatorNickname: "유저3", | ||||||
|  |                 price: 10, | ||||||
|  |                 duration: "00:00:30" | ||||||
|  |             ) | ||||||
|  |         ], | ||||||
|  |         selectTheme: { _ in } | ||||||
|  |     ) | ||||||
|  | } | ||||||
| @@ -116,7 +116,7 @@ struct ContentMainViewV2: View { | |||||||
|                     case .SERIES: |                     case .SERIES: | ||||||
|                         ContentMainTabSeriesView() |                         ContentMainTabSeriesView() | ||||||
|                     case .CONTENT: |                     case .CONTENT: | ||||||
|                         EmptyView() |                         ContentMainTabContentView() | ||||||
|                     case .ALARM: |                     case .ALARM: | ||||||
|                         EmptyView() |                         EmptyView() | ||||||
|                     case .ASMR: |                     case .ASMR: | ||||||
|   | |||||||
| @@ -0,0 +1,11 @@ | |||||||
|  | // | ||||||
|  | //  GetContentCurationResponse.swift | ||||||
|  | //  SodaLive | ||||||
|  | // | ||||||
|  | //  Created by klaus on 2/21/25. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | struct GetContentCurationResponse: Decodable { | ||||||
|  |     let title: String | ||||||
|  |     let items: [GetAudioContentMainItem] | ||||||
|  | } | ||||||
| @@ -212,16 +212,10 @@ struct ContentMainTabHomeView: View { | |||||||
|                             contentList: viewModel.rankContentList |                             contentList: viewModel.rankContentList | ||||||
|                         ) |                         ) | ||||||
|                         .padding(.top, 30) |                         .padding(.top, 30) | ||||||
|                         .padding(.horizontal, 13.3) |  | ||||||
|                     } |                     } | ||||||
|                      |                      | ||||||
|                     if viewModel.eventBannerList.count > 0 { |                     if viewModel.eventBannerList.count > 0 { | ||||||
|                         SectionEventBannerView(items: viewModel.eventBannerList) |                         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) |                             .padding(.top, 30) | ||||||
|                     } |                     } | ||||||
|                      |                      | ||||||
| @@ -235,7 +229,6 @@ struct ContentMainTabHomeView: View { | |||||||
|                             } |                             } | ||||||
|                         ) |                         ) | ||||||
|                         .padding(.top, 30) |                         .padding(.top, 30) | ||||||
|                         .padding(.horizontal, 13.3) |  | ||||||
|                     } |                     } | ||||||
|                      |                      | ||||||
|                     Text(""" |                     Text(""" | ||||||
|   | |||||||
| @@ -70,11 +70,6 @@ struct ContentMainTabSeriesView: View { | |||||||
|                      |                      | ||||||
|                     if !viewModel.eventBannerList.isEmpty { |                     if !viewModel.eventBannerList.isEmpty { | ||||||
|                         SectionEventBannerView(items: viewModel.eventBannerList) |                         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) |                             .padding(.top, 30) | ||||||
|                     } |                     } | ||||||
|                      |                      | ||||||
| @@ -88,6 +83,21 @@ struct ContentMainTabSeriesView: View { | |||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|         } |         } | ||||||
|  |         .popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) { | ||||||
|  |             HStack { | ||||||
|  |                 Spacer() | ||||||
|  |                 Text(viewModel.errorMessage) | ||||||
|  |                     .padding(.vertical, 13.3) | ||||||
|  |                     .frame(width: screenSize().width - 66.7, alignment: .center) | ||||||
|  |                     .font(.custom(Font.medium.rawValue, size: 12)) | ||||||
|  |                     .background(Color.button) | ||||||
|  |                     .foregroundColor(Color.white) | ||||||
|  |                     .multilineTextAlignment(.leading) | ||||||
|  |                     .cornerRadius(20) | ||||||
|  |                     .padding(.bottom, 66.7) | ||||||
|  |                 Spacer() | ||||||
|  |             } | ||||||
|  |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|  |  | ||||||
|   | |||||||
| @@ -10,93 +10,99 @@ import Kingfisher | |||||||
|  |  | ||||||
| struct SectionEventBannerView: View { | struct SectionEventBannerView: View { | ||||||
|      |      | ||||||
|     @State private var currentIndex = -1 |     @State private var currentIndex = 0 | ||||||
|     @State private var timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect() |     @State private var timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect() | ||||||
|      |      | ||||||
|     let items: [EventItem] |     let items: [EventItem] | ||||||
|      |      | ||||||
|     var body: some View { |     var body: some View { | ||||||
|         GeometryReader { proxy in |         VStack(spacing: 13.3) { | ||||||
|             VStack(spacing: 13.3) { |             TabView(selection: $currentIndex) { | ||||||
|                 TabView(selection: $currentIndex) { |                 ForEach(0..<items.count, id: \.self) { index in | ||||||
|                     ForEach(0..<items.count, id: \.self) { index in |                     let item = items[index] | ||||||
|                         let item = items[index] |                     if let url = item.thumbnailImageUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { | ||||||
|                         if let url = item.thumbnailImageUrl.addingPercentEncoding(withAllowedCharacters: .urlQueryAllowed) { |                         KFImage(URL(string: url)) | ||||||
|                             KFImage(URL(string: url)) |                             .cancelOnDisappear(true) | ||||||
|                                 .cancelOnDisappear(true) |                             .downsampling( | ||||||
|                                 .downsampling( |                                 size: CGSize( | ||||||
|                                     size: CGSize( |                                     width: screenSize().width, | ||||||
|                                         width: proxy.size.width, |                                     height: screenSize().width * 300 / 1000 | ||||||
|                                         height: proxy.size.height |  | ||||||
|                                     ) |  | ||||||
|                                 ) |                                 ) | ||||||
|                                 .resizable() |                             ) | ||||||
|                                 .scaledToFill() |                             .resizable() | ||||||
|                                 .frame(width: proxy.size.width, height: proxy.size.height, alignment: .center) |                             .scaledToFill() | ||||||
|                                 .tag(index) |  | ||||||
|                                 .onTapGesture { |  | ||||||
|                                     if let _ = item.detailImageUrl { |  | ||||||
|                                         AppState.shared.setAppStep(step: .eventDetail(event: item)) |  | ||||||
|                                     } else if let link = item.link, link.trimmingCharacters(in: .whitespaces).count > 0, let url = URL(string: link), UIApplication.shared.canOpenURL(url) { |  | ||||||
|                                         UIApplication.shared.open(url) |  | ||||||
|                                     } |  | ||||||
|                                 } |  | ||||||
|                         } else { |  | ||||||
|                             KFImage(URL(string: item.thumbnailImageUrl)) |  | ||||||
|                                 .cancelOnDisappear(true) |  | ||||||
|                                 .downsampling( |  | ||||||
|                                     size: CGSize( |  | ||||||
|                                         width: proxy.size.width, |  | ||||||
|                                         height: proxy.size.height |  | ||||||
|                                     ) |  | ||||||
|                                 ) |  | ||||||
|                                 .resizable() |  | ||||||
|                                 .scaledToFill() |  | ||||||
|                                 .frame(width: proxy.size.width, height: proxy.size.height, alignment: .center) |  | ||||||
|                                 .tag(index) |  | ||||||
|                                 .onTapGesture { |  | ||||||
|                                     if let _ = item.detailImageUrl { |  | ||||||
|                                         AppState.shared.setAppStep(step: .eventDetail(event: item)) |  | ||||||
|                                     } else if let link = item.link, link.trimmingCharacters(in: .whitespaces).count > 0, let url = URL(string: link), UIApplication.shared.canOpenURL(url) { |  | ||||||
|                                         UIApplication.shared.open(url) |  | ||||||
|                                     } |  | ||||||
|                                 } |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 } |  | ||||||
|                 .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) |  | ||||||
|                 .frame( |  | ||||||
|                     width: proxy.size.width, |  | ||||||
|                     height: proxy.size.height, |  | ||||||
|                     alignment: .center |  | ||||||
|                 ) |  | ||||||
|                  |  | ||||||
|                 HStack(spacing: 4) { |  | ||||||
|                     ForEach(0..<items.count, id: \.self) { index in |  | ||||||
|                         Capsule() |  | ||||||
|                             .foregroundColor(index == currentIndex ? Color(hex: "3bb9f1") : Color(hex: "909090")) |  | ||||||
|                             .frame( |                             .frame( | ||||||
|                                 width: index == currentIndex ? 18 : 6, |                                 width: screenSize().width, | ||||||
|                                 height: 6 |                                 height: screenSize().width * 300 / 1000, | ||||||
|  |                                 alignment: .center | ||||||
|                             ) |                             ) | ||||||
|                             .tag(index) |                             .tag(index) | ||||||
|  |                             .onTapGesture { | ||||||
|  |                                 if let _ = item.detailImageUrl { | ||||||
|  |                                     AppState.shared.setAppStep(step: .eventDetail(event: item)) | ||||||
|  |                                 } else if let link = item.link, link.trimmingCharacters(in: .whitespaces).count > 0, let url = URL(string: link), UIApplication.shared.canOpenURL(url) { | ||||||
|  |                                     UIApplication.shared.open(url) | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                     } else { | ||||||
|  |                         KFImage(URL(string: item.thumbnailImageUrl)) | ||||||
|  |                             .cancelOnDisappear(true) | ||||||
|  |                             .downsampling( | ||||||
|  |                                 size: CGSize( | ||||||
|  |                                     width: screenSize().width, | ||||||
|  |                                     height: screenSize().width * 300 / 1000 | ||||||
|  |                                 ) | ||||||
|  |                             ) | ||||||
|  |                             .resizable() | ||||||
|  |                             .scaledToFill() | ||||||
|  |                             .frame( | ||||||
|  |                                 width: screenSize().width, | ||||||
|  |                                 height: screenSize().width * 300 / 1000, | ||||||
|  |                                 alignment: .center | ||||||
|  |                             ) | ||||||
|  |                             .tag(index) | ||||||
|  |                             .onTapGesture { | ||||||
|  |                                 if let _ = item.detailImageUrl { | ||||||
|  |                                     AppState.shared.setAppStep(step: .eventDetail(event: item)) | ||||||
|  |                                 } else if let link = item.link, link.trimmingCharacters(in: .whitespaces).count > 0, let url = URL(string: link), UIApplication.shared.canOpenURL(url) { | ||||||
|  |                                     UIApplication.shared.open(url) | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|             .onAppear { |             .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) | ||||||
|                 timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect() |             .frame( | ||||||
|  |                 width: screenSize().width, | ||||||
|  |                 height: screenSize().width * 300 / 1000, | ||||||
|  |                 alignment: .center | ||||||
|  |             ) | ||||||
|  |              | ||||||
|  |             HStack(spacing: 4) { | ||||||
|  |                 ForEach(0..<items.count, id: \.self) { index in | ||||||
|  |                     Capsule() | ||||||
|  |                         .foregroundColor(index == currentIndex ? .button : .gray90) | ||||||
|  |                         .frame( | ||||||
|  |                             width: index == currentIndex ? 18 : 6, | ||||||
|  |                             height: 6 | ||||||
|  |                         ) | ||||||
|  |                         .tag(index) | ||||||
|  |                 } | ||||||
|             } |             } | ||||||
|             .onDisappear { |         } | ||||||
|                 timer.upstream.connect().cancel() |         .onAppear { | ||||||
|             } |             timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect() | ||||||
|             .onReceive(timer) { _ in |         } | ||||||
|                 DispatchQueue.main.async { |         .onDisappear { | ||||||
|                     withAnimation { |             timer.upstream.connect().cancel() | ||||||
|                         if currentIndex == items.count - 1 { |         } | ||||||
|                             currentIndex = 0 |         .onReceive(timer) { _ in | ||||||
|                         } else { |             DispatchQueue.main.async { | ||||||
|                             currentIndex += 1 |                 withAnimation { | ||||||
|                         } |                     if currentIndex == items.count - 1 { | ||||||
|  |                         currentIndex = 0 | ||||||
|  |                     } else { | ||||||
|  |                         currentIndex += 1 | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|   | |||||||
| @@ -73,11 +73,6 @@ struct LiveView: View { | |||||||
|                              |                              | ||||||
|                             if viewModel.eventBannerItems.count > 0 { |                             if viewModel.eventBannerItems.count > 0 { | ||||||
|                                 SectionEventBannerView(items: viewModel.eventBannerItems) |                                 SectionEventBannerView(items: viewModel.eventBannerItems) | ||||||
|                                     .frame( |  | ||||||
|                                         width: viewModel.eventBannerItems.count > 0 ? screenSize().width : 0, |  | ||||||
|                                         height: viewModel.eventBannerItems.count > 0 ? screenSize().width * 300 / 1000 : 0, |  | ||||||
|                                         alignment: .center |  | ||||||
|                                     ) |  | ||||||
|                             } |                             } | ||||||
|                              |                              | ||||||
|                             if viewModel.communityPostItems.count > 0 { |                             if viewModel.communityPostItems.count > 0 { | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	 Yu Sung
					Yu Sung