feat(home): 팔로잉 탭 API 상태를 추가한다
This commit is contained in:
@@ -0,0 +1,73 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
final class MainHomeFollowingViewModel: ObservableObject {
|
||||
private let repository = MainHomeFollowingRepository()
|
||||
private var subscription = Set<AnyCancellable>()
|
||||
|
||||
@Published var isLoading = false
|
||||
@Published var response: HomeFollowingTabResponse?
|
||||
@Published var isLoginRequired = false
|
||||
@Published var message = ""
|
||||
@Published var hasLoaded = false
|
||||
|
||||
func fetchFollowing() {
|
||||
isLoading = true
|
||||
message = ""
|
||||
|
||||
repository.getFollowing()
|
||||
.sink { [weak self] result in
|
||||
guard let self else { return }
|
||||
|
||||
switch result {
|
||||
case .finished:
|
||||
DEBUG_LOG("finish")
|
||||
case .failure(let error):
|
||||
ERROR_LOG(error.localizedDescription)
|
||||
self.response = nil
|
||||
self.isLoginRequired = false
|
||||
self.message = I18n.HomeFollowing.loadFailedMessage
|
||||
self.hasLoaded = true
|
||||
self.isLoading = false
|
||||
}
|
||||
} receiveValue: { [weak self] response in
|
||||
guard let self else { return }
|
||||
|
||||
let responseData = response.data
|
||||
|
||||
do {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
let decoded = try jsonDecoder.decode(ApiResponse<HomeFollowingTabResponse>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.isLoginRequired = data.isLoginRequired
|
||||
self.response = data.isLoginRequired ? nil : data
|
||||
self.message = data.isLoginRequired || data.hasContent ? "" : I18n.HomeFollowing.emptyStateMessage
|
||||
} else {
|
||||
self.response = nil
|
||||
self.isLoginRequired = false
|
||||
self.message = I18n.HomeFollowing.loadFailedMessage
|
||||
}
|
||||
} catch {
|
||||
ERROR_LOG(error.localizedDescription)
|
||||
self.response = nil
|
||||
self.isLoginRequired = false
|
||||
self.message = I18n.HomeFollowing.loadFailedMessage
|
||||
}
|
||||
|
||||
self.hasLoaded = true
|
||||
self.isLoading = false
|
||||
}
|
||||
.store(in: &subscription)
|
||||
}
|
||||
}
|
||||
|
||||
private extension HomeFollowingTabResponse {
|
||||
var hasContent: Bool {
|
||||
!followingCreators.isEmpty ||
|
||||
!onAirLives.isEmpty ||
|
||||
!recentChats.isEmpty ||
|
||||
!monthlySchedules.isEmpty ||
|
||||
!recentNews.isEmpty
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,154 @@
|
||||
import Foundation
|
||||
|
||||
struct HomeFollowingTabResponse: Decodable {
|
||||
let isLoginRequired: Bool
|
||||
let followingCreators: [FollowingCreatorResponse]
|
||||
let onAirLives: [FollowingLiveResponse]
|
||||
let recentChats: [ChatRoomListItemResponse]
|
||||
let monthlySchedules: [FollowingScheduleResponse]
|
||||
let recentNews: [FollowingNewsResponse]
|
||||
}
|
||||
|
||||
struct FollowingCreatorResponse: Decodable, Identifiable {
|
||||
let creatorId: Int
|
||||
let creatorNickname: String
|
||||
let creatorProfileImageUrl: String
|
||||
|
||||
var id: Int { creatorId }
|
||||
}
|
||||
|
||||
struct FollowingLiveResponse: Decodable, Identifiable {
|
||||
let liveId: Int
|
||||
let creatorProfileImageUrl: String
|
||||
let creatorNickname: String
|
||||
let title: String
|
||||
let startedAtUtc: String
|
||||
|
||||
var id: Int { liveId }
|
||||
}
|
||||
|
||||
struct ChatRoomListItemResponse: Decodable, Identifiable {
|
||||
let roomId: Int
|
||||
let chatType: String
|
||||
let targetName: String
|
||||
let targetImageUrl: String
|
||||
let lastMessage: String
|
||||
let lastMessageAt: String
|
||||
|
||||
var id: Int { roomId }
|
||||
}
|
||||
|
||||
struct FollowingScheduleResponse: Decodable, Identifiable {
|
||||
let scheduleId: String
|
||||
let creatorId: Int
|
||||
let creatorProfileImageUrl: String
|
||||
let creatorNickname: String
|
||||
let title: String
|
||||
let type: CreatorActivityType
|
||||
let targetId: Int
|
||||
let scheduledAtUtc: String
|
||||
let isOnAir: Bool
|
||||
|
||||
var id: String { scheduleId }
|
||||
}
|
||||
|
||||
enum CreatorActivityType: Decodable, Hashable {
|
||||
case live
|
||||
case audio
|
||||
case community
|
||||
case liveReplay
|
||||
case unknown(String)
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
let code = try container.decode(String.self)
|
||||
|
||||
switch code {
|
||||
case "LIVE":
|
||||
self = .live
|
||||
case "AUDIO":
|
||||
self = .audio
|
||||
case "COMMUNITY":
|
||||
self = .community
|
||||
case "LIVE_REPLAY":
|
||||
self = .liveReplay
|
||||
default:
|
||||
self = .unknown(code)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct FollowingNewsResponse: Decodable, Identifiable {
|
||||
let newsId: String
|
||||
let type: FollowingNewsType
|
||||
let visibleFromAtUtc: String
|
||||
let creatorRanking: FollowingCreatorRankingNewsResponse?
|
||||
let audioContent: FollowingContentNewsResponse?
|
||||
let photoContent: FollowingContentNewsResponse?
|
||||
let contentRanking: FollowingContentRankingNewsResponse?
|
||||
let communityPost: FollowingCommunityPostNewsResponse?
|
||||
|
||||
var id: String { newsId }
|
||||
}
|
||||
|
||||
struct FollowingCreatorRankingNewsResponse: Decodable {
|
||||
let rank: Int
|
||||
let creatorId: Int
|
||||
let nickname: String
|
||||
let profileImageUrl: String
|
||||
}
|
||||
|
||||
struct FollowingContentNewsResponse: Decodable {
|
||||
let contentId: Int
|
||||
let contentImageUrl: String?
|
||||
let title: String
|
||||
let creatorProfileImageUrl: String
|
||||
let creatorNickname: String
|
||||
}
|
||||
|
||||
struct FollowingContentRankingNewsResponse: Decodable {
|
||||
let rank: Int
|
||||
let contentId: Int
|
||||
let contentImageUrl: String?
|
||||
let title: String
|
||||
}
|
||||
|
||||
struct FollowingCommunityPostNewsResponse: Decodable {
|
||||
let postId: Int
|
||||
let creatorProfileImage: String
|
||||
let creatorNickname: String
|
||||
let imageUrl: String?
|
||||
let content: String
|
||||
let createdAt: String
|
||||
let likeCount: Int
|
||||
let commentCount: Int
|
||||
}
|
||||
|
||||
enum FollowingNewsType: Decodable, Hashable {
|
||||
case creatorRanking
|
||||
case contentRanking
|
||||
case communityPost
|
||||
case audioContent
|
||||
case photoContent
|
||||
case unknown(String)
|
||||
|
||||
init(from decoder: Decoder) throws {
|
||||
let container = try decoder.singleValueContainer()
|
||||
let code = try container.decode(String.self)
|
||||
|
||||
switch code {
|
||||
case "CREATOR_RANKING":
|
||||
self = .creatorRanking
|
||||
case "CONTENT_RANKING":
|
||||
self = .contentRanking
|
||||
case "COMMUNITY_POST":
|
||||
self = .communityPost
|
||||
case "AUDIO_CONTENT":
|
||||
self = .audioContent
|
||||
case "PHOTO_CONTENT":
|
||||
self = .photoContent
|
||||
default:
|
||||
self = .unknown(code)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,38 @@
|
||||
import Foundation
|
||||
|
||||
import Moya
|
||||
|
||||
enum MainHomeFollowingApi {
|
||||
case getFollowing
|
||||
}
|
||||
|
||||
extension MainHomeFollowingApi: TargetType {
|
||||
var baseURL: URL {
|
||||
return URL(string: BASE_URL)!
|
||||
}
|
||||
|
||||
var path: String {
|
||||
switch self {
|
||||
case .getFollowing:
|
||||
return "/api/v2/home/following"
|
||||
}
|
||||
}
|
||||
|
||||
var method: Moya.Method {
|
||||
switch self {
|
||||
case .getFollowing:
|
||||
return .get
|
||||
}
|
||||
}
|
||||
|
||||
var task: Moya.Task {
|
||||
switch self {
|
||||
case .getFollowing:
|
||||
return .requestPlain
|
||||
}
|
||||
}
|
||||
|
||||
var headers: [String: String]? {
|
||||
return ["Authorization": "Bearer \(UserDefaults.string(forKey: UserDefaultsKey.token))"]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
import Foundation
|
||||
import CombineMoya
|
||||
import Combine
|
||||
import Moya
|
||||
|
||||
final class MainHomeFollowingRepository {
|
||||
private let api = MoyaProvider<MainHomeFollowingApi>()
|
||||
|
||||
func getFollowing() -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(.getFollowing)
|
||||
}
|
||||
}
|
||||
@@ -11,6 +11,7 @@ struct MainHomeView: View {
|
||||
let onTapCommunity: (Int) -> Void
|
||||
let onTapBanner: (RecommendationBannerResponse) -> Void
|
||||
let onTapFollowAll: (@escaping () -> Void) -> Void
|
||||
let onSelectFollowingTab: () -> Bool
|
||||
|
||||
@State private var selectedTab: MainHomeTab = .recommendation
|
||||
|
||||
@@ -31,7 +32,7 @@ struct MainHomeView: View {
|
||||
|
||||
TextTabBar(
|
||||
items: MainHomeTab.allCases,
|
||||
selectedItem: $selectedTab,
|
||||
selectedItem: selectedTabBinding,
|
||||
title: { $0.title }
|
||||
)
|
||||
|
||||
@@ -41,6 +42,19 @@ struct MainHomeView: View {
|
||||
.background(Color.black.ignoresSafeArea())
|
||||
}
|
||||
|
||||
private var selectedTabBinding: Binding<MainHomeTab> {
|
||||
Binding(
|
||||
get: { selectedTab },
|
||||
set: { newValue in
|
||||
if newValue == .following, onSelectFollowingTab() == false {
|
||||
return
|
||||
}
|
||||
|
||||
selectedTab = newValue
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var tabContent: some View {
|
||||
switch selectedTab {
|
||||
@@ -91,7 +105,8 @@ struct MainHomeView_Previews: PreviewProvider {
|
||||
onTapCharacter: { _ in },
|
||||
onTapCommunity: { _ in },
|
||||
onTapBanner: { _ in },
|
||||
onTapFollowAll: { action in action() }
|
||||
onTapFollowAll: { action in action() },
|
||||
onSelectFollowingTab: { true }
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -163,7 +163,8 @@ struct MainView: View {
|
||||
onTapCharacter: handleRecommendationCharacterTap,
|
||||
onTapCommunity: handleRecommendationCommunityTap,
|
||||
onTapBanner: handleRecommendationBannerTap,
|
||||
onTapFollowAll: handleRecommendationFollowAllTap
|
||||
onTapFollowAll: handleRecommendationFollowAllTap,
|
||||
onSelectFollowingTab: handleFollowingTabSelection
|
||||
)
|
||||
case .content:
|
||||
MainPlaceholderTabView(title: MainTab.content.title)
|
||||
@@ -484,6 +485,15 @@ struct MainView: View {
|
||||
performRecommendationDetailAction(action: action)
|
||||
}
|
||||
|
||||
private func handleFollowingTabSelection() -> Bool {
|
||||
let trimmed = token.trimmingCharacters(in: .whitespacesAndNewlines)
|
||||
guard !trimmed.isEmpty else {
|
||||
AppState.shared.setAppStep(step: .login)
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
private func handleRecommendationBannerTap(_ item: RecommendationBannerResponse) {
|
||||
if let eventItem = item.eventItem {
|
||||
|
||||
Reference in New Issue
Block a user