From e9e7403579dca83130124dc38e6d9738e9136547 Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Sat, 22 Feb 2025 01:41:03 +0900 Subject: [PATCH] =?UTF-8?q?=EC=BD=98=ED=85=90=EC=B8=A0=20=EB=A9=94?= =?UTF-8?q?=EC=9D=B8=20-=20=EB=8B=A8=ED=8E=B8=20=ED=83=AD=20UI=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20=EC=83=9D=EC=84=B1?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SodaLive/Sources/Content/ContentApi.swift | 38 ++- .../Ranking/ContentMainRankingSortView.swift | 1 + .../ContentMainTabContentRepository.swift | 42 ++++ .../Content/ContentMainTabContentView.swift | 104 ++++++++ .../ContentMainTabContentViewModel.swift | 231 ++++++++++++++++++ .../ContentMainTabRankContentView.swift | 2 + .../Content/ContentMainTagCurationView.swift | 231 ++++++++++++++++++ .../GetContentMainTabContentResponse.swift | 20 ++ .../Main/V2/ContentByChannelView.swift | 5 +- .../Main/V2/ContentMainContentThemeView.swift | 56 +++++ .../Main/V2/ContentMainCurationViewV2.swift | 87 +++++++ .../Main/V2/ContentMainNewContentViewV2.swift | 97 ++++++++ .../Content/Main/V2/ContentMainViewV2.swift | 2 +- .../Main/V2/GetContentCurationResponse.swift | 11 + .../Main/V2/Home/ContentMainTabHomeView.swift | 7 - .../V2/Series/ContentMainTabSeriesView.swift | 20 +- .../EventBanner/SectionEventBannerView.swift | 154 ++++++------ SodaLive/Sources/Live/LiveView.swift | 5 - 18 files changed, 1018 insertions(+), 95 deletions(-) create mode 100644 SodaLive/Sources/Content/Main/V2/Content/ContentMainTabContentRepository.swift create mode 100644 SodaLive/Sources/Content/Main/V2/Content/ContentMainTabContentView.swift create mode 100644 SodaLive/Sources/Content/Main/V2/Content/ContentMainTabContentViewModel.swift create mode 100644 SodaLive/Sources/Content/Main/V2/Content/ContentMainTagCurationView.swift create mode 100644 SodaLive/Sources/Content/Main/V2/Content/GetContentMainTabContentResponse.swift create mode 100644 SodaLive/Sources/Content/Main/V2/ContentMainContentThemeView.swift create mode 100644 SodaLive/Sources/Content/Main/V2/ContentMainCurationViewV2.swift create mode 100644 SodaLive/Sources/Content/Main/V2/ContentMainNewContentViewV2.swift create mode 100644 SodaLive/Sources/Content/Main/V2/GetContentCurationResponse.swift diff --git a/SodaLive/Sources/Content/ContentApi.swift b/SodaLive/Sources/Content/ContentApi.swift index 911cfd3..5fd5348 100644 --- a/SodaLive/Sources/Content/ContentApi.swift +++ b/SodaLive/Sources/Content/ContentApi.swift @@ -45,6 +45,11 @@ enum ContentApi { case getContentMainSeries case getRecommendSeriesListByGenre(genreId: 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 { @@ -155,6 +160,18 @@ extension ContentApi: TargetType { case .getRecommendSeriesByCreator: 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: return .get - case .getContentMainHome, .getPopularContentByCreator, .getContentMainSeries, .getRecommendSeriesListByGenre, .getRecommendSeriesByCreator: + case .getContentMainHome, .getPopularContentByCreator, .getContentMainSeries, .getRecommendSeriesListByGenre, .getRecommendSeriesByCreator, .getContentMainContent, + .getContentMainNewContentOfTheme, .getDailyContentRanking, .getRecommendContentByTag: return .get case .likeContent, .modifyAudioContent, .modifyComment, .unpinContent: @@ -326,7 +344,7 @@ extension ContentApi: TargetType { return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) - case .getContentMainHome, .getContentMainSeries: + case .getContentMainHome, .getContentMainSeries, .getContentMainContent: return .requestPlain case .getRecommendSeriesListByGenre(let genreId): @@ -340,6 +358,22 @@ extension ContentApi: TargetType { case .getRecommendSeriesByCreator(let creatorId): let parameters = ["creatorId": creatorId] 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) } } diff --git a/SodaLive/Sources/Content/Main/Ranking/ContentMainRankingSortView.swift b/SodaLive/Sources/Content/Main/Ranking/ContentMainRankingSortView.swift index 3a6bab9..5d1f878 100644 --- a/SodaLive/Sources/Content/Main/Ranking/ContentMainRankingSortView.swift +++ b/SodaLive/Sources/Content/Main/Ranking/ContentMainRankingSortView.swift @@ -40,6 +40,7 @@ struct ContentMainRankingSortView: View { } } } + .padding(.horizontal, 13.3) } } } diff --git a/SodaLive/Sources/Content/Main/V2/Content/ContentMainTabContentRepository.swift b/SodaLive/Sources/Content/Main/V2/Content/ContentMainTabContentRepository.swift new file mode 100644 index 0000000..be67ceb --- /dev/null +++ b/SodaLive/Sources/Content/Main/V2/Content/ContentMainTabContentRepository.swift @@ -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() + + func getContentMainContent() -> AnyPublisher { + return api.requestPublisher(.getContentMainContent) + } + + func getNewContentOfTheme(theme: String) -> AnyPublisher { + return api.requestPublisher( + .getContentMainNewContentOfTheme( + theme: theme, + isAdultContentVisible: UserDefaults.isAdultContentVisible(), + contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL + ) + ) + } + + func getContentRanking(sortType: String) -> AnyPublisher { + return api.requestPublisher(.getDailyContentRanking(sortType: sortType)) + } + + func getRecommendContentByTag(tag: String) -> AnyPublisher { + return api.requestPublisher(.getRecommendContentByTag(tag: tag)) + } + + func getPopularContentByCreator(creatorId: Int) -> AnyPublisher { + return api.requestPublisher(.getPopularContentByCreator(creatorId: creatorId)) + } +} diff --git a/SodaLive/Sources/Content/Main/V2/Content/ContentMainTabContentView.swift b/SodaLive/Sources/Content/Main/V2/Content/ContentMainTabContentView.swift new file mode 100644 index 0000000..9357aed --- /dev/null +++ b/SodaLive/Sources/Content/Main/V2/Content/ContentMainTabContentView.swift @@ -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() +} diff --git a/SodaLive/Sources/Content/Main/V2/Content/ContentMainTabContentViewModel.swift b/SodaLive/Sources/Content/Main/V2/Content/ContentMainTabContentViewModel.swift new file mode 100644 index 0000000..daa1c00 --- /dev/null +++ b/SodaLive/Sources/Content/Main/V2/Content/ContentMainTabContentViewModel.swift @@ -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() + + @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.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) + } +} diff --git a/SodaLive/Sources/Content/Main/V2/Content/ContentMainTabRankContentView.swift b/SodaLive/Sources/Content/Main/V2/Content/ContentMainTabRankContentView.swift index 8d0edac..83a6685 100644 --- a/SodaLive/Sources/Content/Main/V2/Content/ContentMainTabRankContentView.swift +++ b/SodaLive/Sources/Content/Main/V2/Content/ContentMainTabRankContentView.swift @@ -43,6 +43,7 @@ struct ContentMainTabRankContentView: View { .onTapGesture { onClickMore() } } } + .padding(.horizontal, 13.3) if !sortList.isEmpty { ContentMainRankingSortView( @@ -97,6 +98,7 @@ struct ContentMainTabRankContentView: View { } } } + .padding(.horizontal, 13.3) .frame(height: 207) } } diff --git a/SodaLive/Sources/Content/Main/V2/Content/ContentMainTagCurationView.swift b/SodaLive/Sources/Content/Main/V2/Content/ContentMainTagCurationView.swift new file mode 100644 index 0000000..8002259 --- /dev/null +++ b/SodaLive/Sources/Content/Main/V2/Content/ContentMainTagCurationView.swift @@ -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.. 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 } + ) +} diff --git a/SodaLive/Sources/Content/Main/V2/Content/GetContentMainTabContentResponse.swift b/SodaLive/Sources/Content/Main/V2/Content/GetContentMainTabContentResponse.swift new file mode 100644 index 0000000..94bbeb9 --- /dev/null +++ b/SodaLive/Sources/Content/Main/V2/Content/GetContentMainTabContentResponse.swift @@ -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] +} diff --git a/SodaLive/Sources/Content/Main/V2/ContentByChannelView.swift b/SodaLive/Sources/Content/Main/V2/ContentByChannelView.swift index dcbf6a6..f7eaacd 100644 --- a/SodaLive/Sources/Content/Main/V2/ContentByChannelView.swift +++ b/SodaLive/Sources/Content/Main/V2/ContentByChannelView.swift @@ -23,7 +23,8 @@ struct ContentByChannelView: View { VStack(alignment: .leading, spacing: 20) { Text(title) .font(.custom(Font.bold.rawValue, size: 18.3)) - .foregroundColor(Color(hex: "eeeeee")) + .foregroundColor(.grayee) + .padding(.horizontal, 13.3) ScrollView(.horizontal) { HStack(spacing: 22) { @@ -41,6 +42,7 @@ struct ContentByChannelView: View { } } } + .padding(.horizontal, 13.3) } LazyVGrid(columns: columns, spacing: 13.3) { @@ -111,6 +113,7 @@ struct ContentByChannelView: View { .setAppStep(step: .creatorDetail(userId: content.creatorId)) } } + .padding(.horizontal, 13.3) .onTapGesture { AppState .shared diff --git a/SodaLive/Sources/Content/Main/V2/ContentMainContentThemeView.swift b/SodaLive/Sources/Content/Main/V2/ContentMainContentThemeView.swift new file mode 100644 index 0000000..1bacfc7 --- /dev/null +++ b/SodaLive/Sources/Content/Main/V2/ContentMainContentThemeView.swift @@ -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.. 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.. 0 { 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) } @@ -235,7 +229,6 @@ struct ContentMainTabHomeView: View { } ) .padding(.top, 30) - .padding(.horizontal, 13.3) } Text(""" diff --git a/SodaLive/Sources/Content/Main/V2/Series/ContentMainTabSeriesView.swift b/SodaLive/Sources/Content/Main/V2/Series/ContentMainTabSeriesView.swift index 1df8604..2b5d77c 100644 --- a/SodaLive/Sources/Content/Main/V2/Series/ContentMainTabSeriesView.swift +++ b/SodaLive/Sources/Content/Main/V2/Series/ContentMainTabSeriesView.swift @@ -70,11 +70,6 @@ struct ContentMainTabSeriesView: View { 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) } @@ -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() + } + } } } diff --git a/SodaLive/Sources/Live/EventBanner/SectionEventBannerView.swift b/SodaLive/Sources/Live/EventBanner/SectionEventBannerView.swift index 1e529bb..0879ee5 100644 --- a/SodaLive/Sources/Live/EventBanner/SectionEventBannerView.swift +++ b/SodaLive/Sources/Live/EventBanner/SectionEventBannerView.swift @@ -10,93 +10,99 @@ import Kingfisher 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() let items: [EventItem] var body: some View { - GeometryReader { proxy in - VStack(spacing: 13.3) { - TabView(selection: $currentIndex) { - ForEach(0.. 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.. 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 { - timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect() + .tabViewStyle(PageTabViewStyle(indexDisplayMode: .never)) + .frame( + width: screenSize().width, + height: screenSize().width * 300 / 1000, + alignment: .center + ) + + HStack(spacing: 4) { + ForEach(0.. 0 { 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 {