From c71e78fc88dc3d28c2eda3b884b1d3c8a1179456 Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Thu, 27 Mar 2025 08:54:08 +0900 Subject: [PATCH] =?UTF-8?q?=EA=B2=80=EC=83=89=20UI=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- SodaLive/Sources/App/AppStep.swift | 2 +- .../Content/Main/ContentMainView.swift | 2 +- .../Main/V2/Home/ContentMainTabHomeView.swift | 2 +- .../Series/Detail/SeriesDetailView.swift | 11 +- SodaLive/Sources/ContentView.swift | 4 +- .../Sources/CustomView/FocusedTextField.swift | 2 +- .../Explorer/Profile/UserProfileView.swift | 10 +- SodaLive/Sources/Search/SearchApi.swift | 92 ++++++ .../Search/SearchContentListView.swift | 45 +++ .../Search/SearchCreatorListView.swift | 45 +++ .../Sources/Search/SearchRepository.swift | 61 ++++ SodaLive/Sources/Search/SearchResponse.swift | 29 ++ .../Sources/Search/SearchSeriesListView.swift | 44 +++ .../Sources/Search/SearchUnifiedView.swift | 239 +++++++++++++++ SodaLive/Sources/Search/SearchView.swift | 165 +++++++++++ SodaLive/Sources/Search/SearchViewModel.swift | 278 ++++++++++++++++++ 16 files changed, 1023 insertions(+), 8 deletions(-) create mode 100644 SodaLive/Sources/Search/SearchApi.swift create mode 100644 SodaLive/Sources/Search/SearchContentListView.swift create mode 100644 SodaLive/Sources/Search/SearchCreatorListView.swift create mode 100644 SodaLive/Sources/Search/SearchRepository.swift create mode 100644 SodaLive/Sources/Search/SearchResponse.swift create mode 100644 SodaLive/Sources/Search/SearchSeriesListView.swift create mode 100644 SodaLive/Sources/Search/SearchUnifiedView.swift create mode 100644 SodaLive/Sources/Search/SearchView.swift create mode 100644 SodaLive/Sources/Search/SearchViewModel.swift diff --git a/SodaLive/Sources/App/AppStep.swift b/SodaLive/Sources/App/AppStep.swift index fc6d484..4498ead 100644 --- a/SodaLive/Sources/App/AppStep.swift +++ b/SodaLive/Sources/App/AppStep.swift @@ -142,7 +142,7 @@ enum AppStep { case auditionRoleDetail(roleId: Int, auditionTitle: String) - case searchChannel + case search case contentMain(startTab: ContentMainTab) diff --git a/SodaLive/Sources/Content/Main/ContentMainView.swift b/SodaLive/Sources/Content/Main/ContentMainView.swift index edb9df7..2c509d1 100644 --- a/SodaLive/Sources/Content/Main/ContentMainView.swift +++ b/SodaLive/Sources/Content/Main/ContentMainView.swift @@ -61,7 +61,7 @@ struct ContentMainView: View { .padding(.horizontal, 13.3) .onTapGesture { UserDefaults.set("", forKey: .searchChannel) - AppState.shared.setAppStep(step: .searchChannel) + AppState.shared.setAppStep(step: .search) } ContentMainCreatorRankingView() diff --git a/SodaLive/Sources/Content/Main/V2/Home/ContentMainTabHomeView.swift b/SodaLive/Sources/Content/Main/V2/Home/ContentMainTabHomeView.swift index 19a0393..45b26b9 100644 --- a/SodaLive/Sources/Content/Main/V2/Home/ContentMainTabHomeView.swift +++ b/SodaLive/Sources/Content/Main/V2/Home/ContentMainTabHomeView.swift @@ -81,7 +81,7 @@ struct ContentMainTabHomeView: View { .padding(.horizontal, 13.3) .onTapGesture { UserDefaults.set("", forKey: .searchChannel) - AppState.shared.setAppStep(step: .searchChannel) + AppState.shared.setAppStep(step: .search) } VStack(spacing: 13.3) { diff --git a/SodaLive/Sources/Content/Series/Detail/SeriesDetailView.swift b/SodaLive/Sources/Content/Series/Detail/SeriesDetailView.swift index de6893f..bd82e48 100644 --- a/SodaLive/Sources/Content/Series/Detail/SeriesDetailView.swift +++ b/SodaLive/Sources/Content/Series/Detail/SeriesDetailView.swift @@ -10,6 +10,7 @@ import Kingfisher struct SeriesDetailView: View { + @Environment(\.presentationMode) var presentationMode: Binding @ObservedObject var viewModel = SeriesDetailViewModel() let seriesId: Int @@ -35,7 +36,13 @@ struct SeriesDetailView: View { Image("ic_back") .resizable() .frame(width: 20, height: 20) - .onTapGesture { AppState.shared.back() } + .onTapGesture { + if presentationMode.wrappedValue.isPresented { + presentationMode.wrappedValue.dismiss() + } else { + AppState.shared.back() + } + } Spacer() } @@ -232,6 +239,8 @@ struct SeriesDetailView: View { } } } + .navigationTitle("") + .navigationBarBackButtonHidden() } .onAppear { viewModel.seriesId = seriesId diff --git a/SodaLive/Sources/ContentView.swift b/SodaLive/Sources/ContentView.swift index 6626fa8..e05aed3 100644 --- a/SodaLive/Sources/ContentView.swift +++ b/SodaLive/Sources/ContentView.swift @@ -215,8 +215,8 @@ struct ContentView: View { auditionTitle: auditionTitle ) - case .searchChannel: - SearchChannelView() + case .search: + SearchView() case .contentMain(let startTab): ContentMainViewV2(selectedTab: startTab) diff --git a/SodaLive/Sources/CustomView/FocusedTextField.swift b/SodaLive/Sources/CustomView/FocusedTextField.swift index c8b7970..9602278 100644 --- a/SodaLive/Sources/CustomView/FocusedTextField.swift +++ b/SodaLive/Sources/CustomView/FocusedTextField.swift @@ -37,7 +37,7 @@ struct FocusedTextField: UIViewRepresentable { textField.autocapitalizationType = .none textField.autocorrectionType = .no - let placeholder = "채널명을 입력해 보세요" + let placeholder = "검색" textField.placeholder = placeholder textField.attributedPlaceholder = NSAttributedString( string: placeholder, diff --git a/SodaLive/Sources/Explorer/Profile/UserProfileView.swift b/SodaLive/Sources/Explorer/Profile/UserProfileView.swift index 0b0f84d..6afbb6f 100644 --- a/SodaLive/Sources/Explorer/Profile/UserProfileView.swift +++ b/SodaLive/Sources/Explorer/Profile/UserProfileView.swift @@ -10,6 +10,8 @@ import SwiftUI struct UserProfileView: View { let userId: Int + + @Environment(\.presentationMode) var presentationMode: Binding @StateObject var viewModel = UserProfileViewModel() @State private var memberId: Int = 0 @@ -24,7 +26,11 @@ struct UserProfileView: View { VStack(spacing: 0) { HStack(spacing: 0) { Button { - AppState.shared.back() + if presentationMode.wrappedValue.isPresented { + presentationMode.wrappedValue.dismiss() + } else { + AppState.shared.back() + } } label: { Image("ic_back") .resizable() @@ -250,6 +256,8 @@ struct UserProfileView: View { } } } + .navigationTitle("") + .navigationBarBackButtonHidden() .popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) { HStack { Spacer() diff --git a/SodaLive/Sources/Search/SearchApi.swift b/SodaLive/Sources/Search/SearchApi.swift new file mode 100644 index 0000000..d2e4d98 --- /dev/null +++ b/SodaLive/Sources/Search/SearchApi.swift @@ -0,0 +1,92 @@ +// +// SearchApi.swift +// SodaLive +// +// Created by klaus on 3/27/25. +// + +import Foundation +import Moya + +enum SearchApi { + case searchUnified(keyword: String, isAdultContentVisible: Bool, contentType: ContentType) + case searchCreatorList(keyword: String, isAdultContentVisible: Bool, contentType: ContentType, page: Int, size: Int) + case searchContentList(keyword: String, isAdultContentVisible: Bool, contentType: ContentType, page: Int, size: Int) + case searchSeriesList(keyword: String, isAdultContentVisible: Bool, contentType: ContentType, page: Int, size: Int) +} + +extension SearchApi: TargetType { + var baseURL: URL { + return URL(string: BASE_URL)! + } + + var path: String { + switch self { + case .searchUnified: + return "/search" + + case .searchCreatorList: + return "/search/creators" + + case .searchContentList: + return "/search/contents" + + case .searchSeriesList: + return "/search/series" + } + } + + var method: Moya.Method { + return .get + } + + var task: Moya.Task { + switch self { + case .searchUnified(let keyword, let isAdultContentVisible, let contentType): + let parameters = [ + "keyword": keyword, + "isAdultContentVisible": isAdultContentVisible, + "contentType": contentType + ] as [String : Any] + + return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) + + case .searchCreatorList(let keyword, let isAdultContentVisible, let contentType, let page, let size): + let parameters = [ + "keyword": keyword, + "isAdultContentVisible": isAdultContentVisible, + "contentType": contentType, + "page": page - 1, + "size": size + ] as [String : Any] + + return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) + + case .searchContentList(let keyword, let isAdultContentVisible, let contentType, let page, let size): + let parameters = [ + "keyword": keyword, + "isAdultContentVisible": isAdultContentVisible, + "contentType": contentType, + "page": page - 1, + "size": size + ] as [String : Any] + + return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) + + case .searchSeriesList(let keyword, let isAdultContentVisible, let contentType, let page, let size): + let parameters = [ + "keyword": keyword, + "isAdultContentVisible": isAdultContentVisible, + "contentType": contentType, + "page": page - 1, + "size": size + ] as [String : Any] + + return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) + } + } + + var headers: [String : String]? { + return ["Authorization": "Bearer \(UserDefaults.string(forKey: UserDefaultsKey.token))"] + } +} diff --git a/SodaLive/Sources/Search/SearchContentListView.swift b/SodaLive/Sources/Search/SearchContentListView.swift new file mode 100644 index 0000000..d5d6e39 --- /dev/null +++ b/SodaLive/Sources/Search/SearchContentListView.swift @@ -0,0 +1,45 @@ +// +// SearchContentListView.swift +// SodaLive +// +// Created by klaus on 3/27/25. +// + +import SwiftUI + +struct SearchContentListView: View { + + let itemsList: [SearchResponseItem] + + var body: some View { + ScrollView(.vertical, showsIndicators: false) { + VStack(spacing: 13.3) { + ForEach(0..() + + func searchUnified(keyword: String) -> AnyPublisher { + return api.requestPublisher( + .searchUnified( + keyword: keyword, + isAdultContentVisible: UserDefaults.isAdultContentVisible(), + contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL + ) + ) + } + + func searchCreatorList(keyword: String, page: Int, size: Int) -> AnyPublisher { + return api.requestPublisher( + .searchCreatorList( + keyword: keyword, + isAdultContentVisible: UserDefaults.isAdultContentVisible(), + contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL, + page: page, + size: size + ) + ) + } + + func searchContentList(keyword: String, page: Int, size: Int) -> AnyPublisher { + return api.requestPublisher( + .searchContentList( + keyword: keyword, + isAdultContentVisible: UserDefaults.isAdultContentVisible(), + contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL, + page: page, + size: size + ) + ) + } + + func searchSeriesList(keyword: String, page: Int, size: Int) -> AnyPublisher { + return api.requestPublisher( + .searchSeriesList( + keyword: keyword, + isAdultContentVisible: UserDefaults.isAdultContentVisible(), + contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL, + page: page, + size: size + ) + ) + } +} diff --git a/SodaLive/Sources/Search/SearchResponse.swift b/SodaLive/Sources/Search/SearchResponse.swift new file mode 100644 index 0000000..e110fb8 --- /dev/null +++ b/SodaLive/Sources/Search/SearchResponse.swift @@ -0,0 +1,29 @@ +// +// SearchResponse.swift +// SodaLive +// +// Created by klaus on 3/27/25. +// + +struct SearchUnifiedResponse: Decodable { + let creatorList: [SearchResponseItem] + let contentList: [SearchResponseItem] + let seriesList: [SearchResponseItem] +} + +struct SearchResponse: Decodable { + let totalCount: Int + let items: [SearchResponseItem] +} + +struct SearchResponseItem: Decodable { + let id: Int + let imageUrl: String + let title: String + let nickname: String + let type: SearchResponseType +} + +enum SearchResponseType: String, Decodable { + case CREATOR, CONTENT, SERIES +} diff --git a/SodaLive/Sources/Search/SearchSeriesListView.swift b/SodaLive/Sources/Search/SearchSeriesListView.swift new file mode 100644 index 0000000..da30c01 --- /dev/null +++ b/SodaLive/Sources/Search/SearchSeriesListView.swift @@ -0,0 +1,44 @@ +// +// SearchSeriesListView.swift +// SodaLive +// +// Created by klaus on 3/27/25. +// + +import SwiftUI + +struct SearchSeriesListView: View { + + let itemsList: [SearchResponseItem] + + var body: some View { + ScrollView(.vertical, showsIndicators: false) { + VStack(spacing: 13.3) { + ForEach(0.. Void + let onTapMoreContent: () -> Void + let onTapMoreSeries: () -> Void + + var body: some View { + ScrollView(.vertical, showsIndicators: false) { + VStack(spacing: 30) { + if !creatorList.isEmpty { + SearchUnifiedItemView( + title: "채널", + itemList: creatorList, + onTapMore: onTapMoreCreator + ) + .frame(maxWidth: .infinity) + .padding(.horizontal, 13.3) + } + + if !contentList.isEmpty { + SearchUnifiedItemView( + title: "콘텐츠", + itemList: contentList, + onTapMore: onTapMoreContent + ) + .frame(maxWidth: .infinity) + .padding(.horizontal, 13.3) + } + + if !searchList.isEmpty { + SearchUnifiedItemView( + title: "시리즈", + itemList: searchList, + onTapMore: onTapMoreSeries + ) + .frame(maxWidth: .infinity) + .padding(.horizontal, 13.3) + } + } + } + } +} + +struct SearchUnifiedItemView: View { + let title: String + let itemList: [SearchResponseItem] + let onTapMore: () -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 13.3) { + Text(title) + .font(.custom(Font.bold.rawValue, size: 16)) + .foregroundColor(.grayee) + + ForEach(0..") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(.grayee) + .padding(.vertical, 10) + .frame(maxWidth: .infinity) + .background(Color.gray33.opacity(0.7)) + .onTapGesture { onTapMore() } + } + } +} + +struct SearchCreatorItemView: View { + let item: SearchResponseItem + + var body: some View { + NavigationLink { + UserProfileView(userId: item.id) + } label: { + HStack(spacing: 13.3) { + KFImage(URL(string: item.imageUrl)) + .cancelOnDisappear(true) + .downsampling(size: CGSize(width: 60, height: 60)) + .resizable() + .frame(width: 60, height: 60) + .clipShape(Circle()) + + Text(item.nickname) + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color.grayee) + + Spacer() + } + .frame(maxWidth: .infinity) + .contentShape(Rectangle()) + } + } +} + +struct SearchContentItemView: View { + let item: SearchResponseItem + + var body: some View { + NavigationLink { + ContentDetailView(contentId: item.id) + } label: { + HStack(spacing: 13.3) { + KFImage(URL(string: item.imageUrl)) + .cancelOnDisappear(true) + .downsampling(size: CGSize(width: 60, height: 60)) + .resizable() + .frame(width: 60, height: 60) + .cornerRadius(5.3) + + VStack(alignment: .leading, spacing: 6.7) { + Text(item.title) + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color.grayee) + + Text(item.nickname) + .font(.custom(Font.medium.rawValue, size: 10)) + .foregroundColor(Color.gray77) + } + + Spacer() + } + .frame(maxWidth: .infinity) + .contentShape(Rectangle()) + } + } +} + +struct SearchSeriesItemView: View { + let item: SearchResponseItem + + var body: some View { + NavigationLink { + SeriesDetailView(seriesId: item.id) + } label: { + HStack(spacing: 13.3) { + KFImage(URL(string: item.imageUrl)) + .cancelOnDisappear(true) + .downsampling(size: CGSize(width: 60, height: 85)) + .resizable() + .scaledToFill() + .frame(width: 60, height: 85) + .clipped() + .cornerRadius(5.3) + + VStack(alignment: .leading, spacing: 6.7) { + Text(item.title) + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(Color.grayee) + + Text(item.nickname) + .font(.custom(Font.medium.rawValue, size: 10)) + .foregroundColor(Color.gray77) + } + + Spacer() + } + .frame(maxWidth: .infinity) + .contentShape(Rectangle()) + } + } +} + + +#Preview { + SearchUnifiedView( + creatorList: [ + SearchResponseItem( + id: 1, + imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", + title: "Tester", + nickname: "Tester", + type: .CREATOR + ) + ], + contentList: [ + SearchResponseItem( + id: 1, + imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", + title: "Title1", + nickname: "Tester", + type: .CONTENT + ), + + SearchResponseItem( + id: 2, + imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", + title: "Title2", + nickname: "Tester2", + type: .CONTENT + ) + ], + searchList: [ + SearchResponseItem( + id: 1, + imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", + title: "Title1", + nickname: "Tester", + type: .SERIES + ), + + SearchResponseItem( + id: 2, + imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", + title: "Title2", + nickname: "Tester2", + type: .SERIES + )], + onTapMoreCreator: {}, + onTapMoreContent: {}, + onTapMoreSeries: {} + ) +} diff --git a/SodaLive/Sources/Search/SearchView.swift b/SodaLive/Sources/Search/SearchView.swift new file mode 100644 index 0000000..46f99b5 --- /dev/null +++ b/SodaLive/Sources/Search/SearchView.swift @@ -0,0 +1,165 @@ +// +// SearchView.swift +// SodaLive +// +// Created by klaus on 3/27/25. +// + +import SwiftUI +import Kingfisher + +struct SearchViewTabItem { + let title: String + let tab: SearchViewModel.CurrentTab +} + +struct SearchView: View { + + @StateObject var viewModel = SearchViewModel() + @State private var isFocused: Bool = false + + let tabItemList = [ + SearchViewTabItem(title: "통합", tab: .UNIFIED), + SearchViewTabItem(title: "채널", tab: .CREATOR), + SearchViewTabItem(title: "콘텐츠", tab: .CONTENT), + SearchViewTabItem(title: "시리즈", tab: .SERIES) + ] + + var body: some View { + NavigationView { + BaseView(isLoading: $viewModel.isLoading) { + VStack(spacing: 0) { + HStack(spacing: 0) { + Button { + AppState.shared.back() + } label: { + Image("ic_back") + .resizable() + .frame(width: 20, height: 20) + } + .padding(13.3) + + HStack(spacing: 0) { + Image("ic_title_search_black") + + FocusedTextField( + text: $viewModel.keyword, + isFirstResponder: isFocused + ) + .padding(.horizontal, 13.3) + .onAppear { + DispatchQueue.main.asyncAfter(deadline: .now() + 0.2) { + isFocused = true + } + } + } + .padding(.horizontal, 21.3) + .frame(height: 50) + .frame(maxWidth: .infinity) + .background(Color.gray22) + .overlay( + RoundedRectangle(cornerRadius: 6.7) + .strokeBorder(lineWidth: 1) + .foregroundColor(Color.graybb) + ) + } + .padding(.trailing, 13.3) + + if !viewModel.searchUnifiedCreatorList.isEmpty || + !viewModel.searchUnifiedContentList.isEmpty || + !viewModel.searchUnifiedSeriesList.isEmpty { + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 8) { + ForEach(0.. 2 { + Text("검색 결과가 없습니다.") + .font(.custom(Font.medium.rawValue, size: 18.3)) + .foregroundColor(.white) + .padding(.top, 20) + } + + Spacer() + } + .popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .top, autohideIn: 2) { + GeometryReader { geo in + HStack { + Spacer() + Text(viewModel.errorMessage) + .padding(.vertical, 13.3) + .frame(width: geo.size.width - 66.7, alignment: .center) + .font(.custom(Font.medium.rawValue, size: 12)) + .background(Color.button) + .foregroundColor(Color.white) + .multilineTextAlignment(.center) + .cornerRadius(20) + .padding(.top, 66.7) + Spacer() + } + } + } + .onAppear { + if viewModel.keyword.isEmpty { + viewModel.keyword = UserDefaults.string(forKey: .searchChannel) + } + } + } + } + } +} + +#Preview { + SearchView() +} diff --git a/SodaLive/Sources/Search/SearchViewModel.swift b/SodaLive/Sources/Search/SearchViewModel.swift new file mode 100644 index 0000000..fd71b01 --- /dev/null +++ b/SodaLive/Sources/Search/SearchViewModel.swift @@ -0,0 +1,278 @@ +// +// SearchViewModel.swift +// SodaLive +// +// Created by klaus on 3/27/25. +// + +import Foundation +import Combine + +final class SearchViewModel: ObservableObject { + enum CurrentTab: String { + case UNIFIED, CREATOR, CONTENT, SERIES + } + + private let repository = SearchRepository() + private var subscription = Set() + + @Published var currentTab: CurrentTab = .UNIFIED { + didSet { + if currentTab == .CREATOR && searchCreatorItemList.isEmpty { + self.searchCreatorList() + } else if currentTab == .CONTENT && searchContentItemList.isEmpty { + self.searchContentList() + } else if currentTab == .SERIES && searchSeriesItemList.isEmpty { + self.searchSeriesList() + } + } + } + + @Published var errorMessage = "" + @Published var isShowPopup = false + @Published var isLoading = false + + @Published var keyword = "" + + @Published var searchUnifiedCreatorList: [SearchResponseItem] = [] + @Published var searchUnifiedContentList: [SearchResponseItem] = [] + @Published var searchUnifiedSeriesList: [SearchResponseItem] = [] + + @Published var searchCreatorItemList: [SearchResponseItem] = [] + @Published var searchContentItemList: [SearchResponseItem] = [] + @Published var searchSeriesItemList: [SearchResponseItem] = [] + + var searchCreatorPage = 1 + var searchContentPage = 1 + var searchSeriesPage = 1 + + var isSearchCreatorLast = false + var isSearchContentLast = false + var isSearchSeriesLast = false + + private var pageSize = 20 + + init() { + _keyword = Published(initialValue: "") + $keyword + .debounce(for: .seconds(0.3), scheduler: RunLoop.main) + .sink { [unowned self] value in + UserDefaults.set(value, forKey: .searchChannel) + if value.count > 1 { + self.searchUnified() + } else { + self.initList() + } + } + .store(in: &subscription) + } + + func initList() { + searchCreatorPage = 1 + searchContentPage = 1 + searchSeriesPage = 1 + + isSearchCreatorLast = false + isSearchContentLast = false + isSearchSeriesLast = false + + searchUnifiedCreatorList.removeAll() + searchUnifiedContentList.removeAll() + searchUnifiedSeriesList.removeAll() + + searchCreatorItemList.removeAll() + searchContentItemList.removeAll() + searchSeriesItemList.removeAll() + } + + func searchUnified() { + if !isLoading { + if currentTab != .UNIFIED { + currentTab = .UNIFIED + } + + initList() + + isLoading = true + + repository.searchUnified(keyword: keyword) + .sink { result in + switch result { + case .finished: + DEBUG_LOG("finish") + case .failure(let error): + ERROR_LOG(error.localizedDescription) + } + } receiveValue: { [unowned self] response in + self.isLoading = false + let responseData = response.data + + do { + let jsonDecoder = JSONDecoder() + let decoded = try jsonDecoder.decode(ApiResponse.self, from: responseData) + + if let data = decoded.data, decoded.success { + DEBUG_LOG("test: \(data)") + searchUnifiedCreatorList.append(contentsOf: data.creatorList) + searchUnifiedContentList.append(contentsOf: data.contentList) + searchUnifiedSeriesList.append(contentsOf: data.seriesList) + } else { + if let message = decoded.message { + self.errorMessage = message + } else { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + } + + self.isShowPopup = true + } + } catch { + DEBUG_LOG("error: \(error)") + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.isShowPopup = true + } + } + .store(in: &subscription) + } + } + + func searchCreatorList() { + if !isLoading && !isSearchCreatorLast { + isLoading = true + + repository.searchCreatorList(keyword: keyword, page: searchCreatorPage, size: pageSize) + .sink { result in + switch result { + case .finished: + DEBUG_LOG("finish") + case .failure(let error): + ERROR_LOG(error.localizedDescription) + } + } receiveValue: { [unowned self] response in + self.isLoading = false + let responseData = response.data + + do { + let jsonDecoder = JSONDecoder() + let decoded = try jsonDecoder.decode(ApiResponse.self, from: responseData) + + if let data = decoded.data, decoded.success { + DEBUG_LOG("test: \(data)") + self.searchCreatorPage += 1 + self.searchCreatorItemList.append(contentsOf: data.items) + + if data.items.isEmpty { + self.isSearchCreatorLast = true + } + } else { + if let message = decoded.message { + self.errorMessage = message + } else { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + } + + self.isShowPopup = true + } + } catch { + ERROR_LOG("test: \(error)") + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.isShowPopup = true + } + } + .store(in: &subscription) + } + } + + func searchContentList() { + if !isLoading && !isSearchContentLast { + isLoading = true + + repository.searchContentList(keyword: keyword, page: searchContentPage, size: pageSize) + .sink { result in + switch result { + case .finished: + DEBUG_LOG("finish") + case .failure(let error): + ERROR_LOG(error.localizedDescription) + } + } receiveValue: { [unowned self] response in + self.isLoading = false + let responseData = response.data + + do { + let jsonDecoder = JSONDecoder() + let decoded = try jsonDecoder.decode(ApiResponse.self, from: responseData) + + if let data = decoded.data, decoded.success { + DEBUG_LOG("test: \(data)") + self.searchContentPage += 1 + self.searchContentItemList.append(contentsOf: data.items) + + if data.items.isEmpty { + self.isSearchContentLast = true + } + } else { + if let message = decoded.message { + self.errorMessage = message + } else { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + } + + self.isShowPopup = true + } + } catch { + ERROR_LOG("test: \(error)") + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.isShowPopup = true + } + } + .store(in: &subscription) + } + } + + func searchSeriesList() { + if !isLoading && !isSearchSeriesLast { + isLoading = true + + repository.searchSeriesList(keyword: keyword, page: searchSeriesPage, size: pageSize) + .sink { result in + switch result { + case .finished: + DEBUG_LOG("finish") + case .failure(let error): + ERROR_LOG(error.localizedDescription) + } + } receiveValue: { [unowned self] response in + self.isLoading = false + let responseData = response.data + + do { + let jsonDecoder = JSONDecoder() + let decoded = try jsonDecoder.decode(ApiResponse.self, from: responseData) + + if let data = decoded.data, decoded.success { + DEBUG_LOG("test; \(data)") + self.searchSeriesPage += 1 + self.searchSeriesItemList.append(contentsOf: data.items) + + if data.items.isEmpty { + self.isSearchSeriesLast = true + } + } else { + if let message = decoded.message { + self.errorMessage = message + } else { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + } + + self.isShowPopup = true + } + } catch { + ERROR_LOG("test: \(error)") + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.isShowPopup = true + } + } + .store(in: &subscription) + } + } +}