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)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user