검색 UI 추가

This commit is contained in:
Yu Sung 2025-03-27 08:54:08 +09:00
parent 82dc43b40f
commit c71e78fc88
16 changed files with 1023 additions and 8 deletions

View File

@ -142,7 +142,7 @@ enum AppStep {
case auditionRoleDetail(roleId: Int, auditionTitle: String)
case searchChannel
case search
case contentMain(startTab: ContentMainTab)

View File

@ -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()

View File

@ -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) {

View File

@ -10,6 +10,7 @@ import Kingfisher
struct SeriesDetailView: View {
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
@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

View File

@ -215,8 +215,8 @@ struct ContentView: View {
auditionTitle: auditionTitle
)
case .searchChannel:
SearchChannelView()
case .search:
SearchView()
case .contentMain(let startTab):
ContentMainViewV2(selectedTab: startTab)

View File

@ -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,

View File

@ -10,6 +10,8 @@ import SwiftUI
struct UserProfileView: View {
let userId: Int
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
@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()

View File

@ -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))"]
}
}

View File

@ -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..<itemsList.count, id: \.self) {
SearchContentItemView(item: itemsList[$0])
}
}
.padding(.horizontal, 13.3)
}
}
}
#Preview {
SearchContentListView(
itemsList: [
SearchResponseItem(
id: 1,
imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
title: "Title1",
nickname: "Tester1",
type: .CONTENT
),
SearchResponseItem(
id: 2,
imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
title: "Title2",
nickname: "Tester2",
type: .CONTENT
)
]
)
}

View File

@ -0,0 +1,45 @@
//
// SearchCreatorListView.swift
// SodaLive
//
// Created by klaus on 3/27/25.
//
import SwiftUI
struct SearchCreatorListView: View {
let itemsList: [SearchResponseItem]
var body: some View {
ScrollView(.vertical, showsIndicators: false) {
VStack(spacing: 13.3) {
ForEach(0..<itemsList.count, id: \.self) {
SearchCreatorItemView(item: itemsList[$0])
}
}
.padding(.horizontal, 13.3)
}
}
}
#Preview {
SearchCreatorListView(
itemsList: [
SearchResponseItem(
id: 1,
imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
title: "Tester",
nickname: "Tester",
type: .CREATOR
),
SearchResponseItem(
id: 2,
imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
title: "Tester2",
nickname: "Tester2",
type: .CREATOR
)
]
)
}

View File

@ -0,0 +1,61 @@
//
// SearchRepository.swift
// SodaLive
//
// Created by klaus on 3/27/25.
//
import Foundation
import CombineMoya
import Combine
import Moya
final class SearchRepository {
private let api = MoyaProvider<SearchApi>()
func searchUnified(keyword: String) -> AnyPublisher<Response, MoyaError> {
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<Response, MoyaError> {
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<Response, MoyaError> {
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<Response, MoyaError> {
return api.requestPublisher(
.searchSeriesList(
keyword: keyword,
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL,
page: page,
size: size
)
)
}
}

View File

@ -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
}

View File

@ -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..<itemsList.count, id: \.self) {
SearchSeriesItemView(item: itemsList[$0])
}
}
.padding(.horizontal, 13.3)
}
}
}
#Preview {
SearchSeriesListView(
itemsList: [
SearchResponseItem(
id: 1,
imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
title: "Title1",
nickname: "Tester1",
type: .SERIES
),
SearchResponseItem(
id: 2,
imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
title: "Title2",
nickname: "Tester2",
type: .SERIES
)
])
}

View File

@ -0,0 +1,239 @@
//
// SearchUnifiedView.swift
// SodaLive
//
// Created by klaus on 3/27/25.
//
import SwiftUI
import Kingfisher
struct SearchUnifiedView: View {
let creatorList: [SearchResponseItem]
let contentList: [SearchResponseItem]
let searchList: [SearchResponseItem]
let onTapMoreCreator: () -> 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..<itemList.count, id: \.self) {
let item = itemList[$0]
switch item.type {
case .CREATOR:
SearchCreatorItemView(item: item)
case .CONTENT:
SearchContentItemView(item: item)
case .SERIES:
SearchSeriesItemView(item: item)
}
}
Text("더보기 >")
.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: {}
)
}

View File

@ -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..<tabItemList.count, id: \.self) { index in
let tabItem = tabItemList[index]
Text(tabItem.title)
.font(.custom(Font.medium.rawValue,size: 16))
.foregroundColor(
viewModel.currentTab == tabItem.tab ?
.button :
.graybb
)
.padding(.horizontal, 12)
.onTapGesture {
if viewModel.currentTab != tabItem.tab {
viewModel.currentTab = tabItem.tab
}
}
}
}
.padding(.vertical, 15)
.padding(.horizontal, 13.3)
}
.padding(.bottom, 13.3)
switch viewModel.currentTab {
case .UNIFIED:
SearchUnifiedView(
creatorList: viewModel.searchUnifiedCreatorList,
contentList: viewModel.searchUnifiedContentList,
searchList: viewModel.searchUnifiedSeriesList,
onTapMoreCreator: {
viewModel.currentTab = .CREATOR
},
onTapMoreContent: {
viewModel.currentTab = .CONTENT
},
onTapMoreSeries: {
viewModel.currentTab = .SERIES
}
)
case .CREATOR:
SearchCreatorListView(itemsList: viewModel.searchCreatorItemList)
case .CONTENT:
SearchContentListView(itemsList: viewModel.searchContentItemList)
case .SERIES:
SearchSeriesListView(itemsList: viewModel.searchSeriesItemList)
}
}
if viewModel.searchUnifiedCreatorList.isEmpty &&
viewModel.searchUnifiedContentList.isEmpty &&
viewModel.searchUnifiedSeriesList.isEmpty &&
viewModel.keyword.count > 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()
}

View File

@ -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<AnyCancellable>()
@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<SearchUnifiedResponse>.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<SearchResponse>.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<SearchResponse>.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<SearchResponse>.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)
}
}
}