diff --git a/SodaLive/Sources/Chat/Character/CharacterApi.swift b/SodaLive/Sources/Chat/Character/CharacterApi.swift index 7099003..e358b09 100644 --- a/SodaLive/Sources/Chat/Character/CharacterApi.swift +++ b/SodaLive/Sources/Chat/Character/CharacterApi.swift @@ -15,6 +15,7 @@ enum CharacterApi { case getMyCharacterImageList(characterId: Int64, page: Int, size: Int) case purchaseCharacterImage(imageId: Int) case getRecentCharacters(page: Int, size: Int) + case refreshRecommendCharacters } extension CharacterApi: TargetType { @@ -39,6 +40,9 @@ extension CharacterApi: TargetType { case .getRecentCharacters: return "/api/chat/character/recent" + + case .refreshRecommendCharacters: + return "/api/chat/character/recommend" } } @@ -54,7 +58,7 @@ extension CharacterApi: TargetType { var task: Moya.Task { switch self { - case .getCharacterHome, .getCharacterDetail: + case .getCharacterHome, .getCharacterDetail, .refreshRecommendCharacters: return .requestPlain case .getRecentCharacters(let page, let size): diff --git a/SodaLive/Sources/Chat/Character/CharacterHomeResponse.swift b/SodaLive/Sources/Chat/Character/CharacterHomeResponse.swift index 8cdfa27..f427f84 100644 --- a/SodaLive/Sources/Chat/Character/CharacterHomeResponse.swift +++ b/SodaLive/Sources/Chat/Character/CharacterHomeResponse.swift @@ -10,4 +10,5 @@ struct CharacterHomeResponse: Decodable { let recentCharacters: [RecentCharacter] let popularCharacters: [Character] let newCharacters: [Character] + let recommendCharacters: [Character] } diff --git a/SodaLive/Sources/Chat/Character/CharacterRepository.swift b/SodaLive/Sources/Chat/Character/CharacterRepository.swift index 02f3c95..15891e1 100644 --- a/SodaLive/Sources/Chat/Character/CharacterRepository.swift +++ b/SodaLive/Sources/Chat/Character/CharacterRepository.swift @@ -16,4 +16,8 @@ class CharacterRepository { func getCharacterMain() -> AnyPublisher { return api.requestPublisher(.getCharacterHome) } + + func refreshRecommendCharacters() -> AnyPublisher { + return api.requestPublisher(.refreshRecommendCharacters) + } } diff --git a/SodaLive/Sources/Chat/Character/CharacterView.swift b/SodaLive/Sources/Chat/Character/CharacterView.swift index 911c869..fc93115 100644 --- a/SodaLive/Sources/Chat/Character/CharacterView.swift +++ b/SodaLive/Sources/Chat/Character/CharacterView.swift @@ -63,6 +63,51 @@ struct CharacterView: View { } ) } + + if !viewModel.recommendCharacters.isEmpty { + VStack(alignment: .leading, spacing: 16) { + HStack { + Text("추천 캐릭터") + .font(.custom(Font.preBold.rawValue, size: 24)) + .foregroundColor(.white) + + Spacer() + + Image("ic_refresh") + .onTapGesture { + viewModel.refreshRecommendCharacters() + } + } + .padding(.horizontal, 24) + + let horizontalPadding: CGFloat = 24 + let gridSpacing: CGFloat = 16 + let width = (screenSize().width - (horizontalPadding * 2) - gridSpacing) / 2 + + LazyVGrid( + columns: Array( + repeating: GridItem( + .flexible(), + spacing: gridSpacing, + alignment: .topLeading + ), + count: 2 + ), + alignment: .leading, + spacing: gridSpacing + ) { + ForEach(viewModel.recommendCharacters.indices, id: \.self) { idx in + CharacterItemView( + character: viewModel.recommendCharacters[idx], + size: width, + rank: idx + 1, + isShowRank: false + ) + } + } + .padding(.horizontal, horizontalPadding) + } + } } .padding(.bottom, 24) } diff --git a/SodaLive/Sources/Chat/Character/CharacterViewModel.swift b/SodaLive/Sources/Chat/Character/CharacterViewModel.swift index 574a87f..8c3bbe1 100644 --- a/SodaLive/Sources/Chat/Character/CharacterViewModel.swift +++ b/SodaLive/Sources/Chat/Character/CharacterViewModel.swift @@ -15,6 +15,7 @@ final class CharacterViewModel: ObservableObject { @Published private(set) var recentCharacters: [RecentCharacter] = [] @Published private(set) var popularCharacters: [Character] = [] @Published private(set) var newCharacters: [Character] = [] + @Published private(set) var recommendCharacters: [Character] = [] @Published var isLoading: Bool = false @Published var errorMessage: String = "" @@ -48,6 +49,46 @@ final class CharacterViewModel: ObservableObject { self.recentCharacters = data.recentCharacters self.popularCharacters = data.popularCharacters self.newCharacters = data.newCharacters + self.recommendCharacters = data.recommendCharacters + } else { + if let message = decoded.message { + self.errorMessage = message + } else { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + } + + self.isShowPopup = true + } + self.isLoading = false + } catch { + self.isLoading = false + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.isShowPopup = true + } + } + .store(in: &subscription) + } + + func refreshRecommendCharacters() { + isLoading = true + + repository.refreshRecommendCharacters() + .sink { result in + switch result { + case .finished: + DEBUG_LOG("finish") + case .failure(let error): + ERROR_LOG(error.localizedDescription) + } + } receiveValue: { response in + let responseData = response.data + + do { + let jsonDecoder = JSONDecoder() + let decoded = try jsonDecoder.decode(ApiResponse<[Character]>.self, from: responseData) + + if let data = decoded.data, decoded.success { + self.recommendCharacters = data } else { if let message = decoded.message { self.errorMessage = message