feat(home): 팔로잉 탭 API 상태를 추가한다
This commit is contained in:
@@ -246,6 +246,18 @@
|
|||||||
401B3F072FEEF10000A5BBA3 /* MainHomeRankingRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401B3F0C2FEEF10000A5BBA3 /* MainHomeRankingRepository.swift */; };
|
401B3F072FEEF10000A5BBA3 /* MainHomeRankingRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401B3F0C2FEEF10000A5BBA3 /* MainHomeRankingRepository.swift */; };
|
||||||
401B3F082FEEF10000A5BBA3 /* MainHomeRankingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401B3F0D2FEEF10000A5BBA3 /* MainHomeRankingViewModel.swift */; };
|
401B3F082FEEF10000A5BBA3 /* MainHomeRankingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401B3F0D2FEEF10000A5BBA3 /* MainHomeRankingViewModel.swift */; };
|
||||||
401B3F092FEEF10000A5BBA3 /* MainHomeRankingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401B3F0D2FEEF10000A5BBA3 /* MainHomeRankingViewModel.swift */; };
|
401B3F092FEEF10000A5BBA3 /* MainHomeRankingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401B3F0D2FEEF10000A5BBA3 /* MainHomeRankingViewModel.swift */; };
|
||||||
|
401C10112FEF300000A5BBA3 /* HomeFollowingTabResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C10012FEF300000A5BBA3 /* HomeFollowingTabResponse.swift */; };
|
||||||
|
401C10122FEF300000A5BBA3 /* MainHomeFollowingApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C10022FEF300000A5BBA3 /* MainHomeFollowingApi.swift */; };
|
||||||
|
401C10132FEF300000A5BBA3 /* MainHomeFollowingRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C10032FEF300000A5BBA3 /* MainHomeFollowingRepository.swift */; };
|
||||||
|
401C10142FEF300000A5BBA3 /* MainHomeFollowingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C10042FEF300000A5BBA3 /* MainHomeFollowingViewModel.swift */; };
|
||||||
|
401C10212FEF300000A5BBA3 /* HomeFollowingTabResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C10012FEF300000A5BBA3 /* HomeFollowingTabResponse.swift */; };
|
||||||
|
401C10222FEF300000A5BBA3 /* MainHomeFollowingApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C10022FEF300000A5BBA3 /* MainHomeFollowingApi.swift */; };
|
||||||
|
401C10232FEF300000A5BBA3 /* MainHomeFollowingRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C10032FEF300000A5BBA3 /* MainHomeFollowingRepository.swift */; };
|
||||||
|
401C10242FEF300000A5BBA3 /* MainHomeFollowingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C10042FEF300000A5BBA3 /* MainHomeFollowingViewModel.swift */; };
|
||||||
|
401C10312FEF300000A5BBA3 /* HomeFollowingTabResponse.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C10012FEF300000A5BBA3 /* HomeFollowingTabResponse.swift */; };
|
||||||
|
401C10322FEF300000A5BBA3 /* MainHomeFollowingApi.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C10022FEF300000A5BBA3 /* MainHomeFollowingApi.swift */; };
|
||||||
|
401C10332FEF300000A5BBA3 /* MainHomeFollowingRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C10032FEF300000A5BBA3 /* MainHomeFollowingRepository.swift */; };
|
||||||
|
401C10342FEF300000A5BBA3 /* MainHomeFollowingViewModel.swift in Sources */ = {isa = PBXBuildFile; fileRef = 401C10042FEF300000A5BBA3 /* MainHomeFollowingViewModel.swift */; };
|
||||||
40235DED2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40235DEC2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift */; };
|
40235DED2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40235DEC2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift */; };
|
||||||
40235DEE2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40235DEC2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift */; };
|
40235DEE2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40235DEC2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift */; };
|
||||||
40235DF02E37983D00B09A87 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40235DEF2E37983D00B09A87 /* PersistenceController.swift */; };
|
40235DF02E37983D00B09A87 /* PersistenceController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 40235DEF2E37983D00B09A87 /* PersistenceController.swift */; };
|
||||||
@@ -1780,6 +1792,10 @@
|
|||||||
401B3F0B2FEEF10000A5BBA3 /* MainHomeRankingApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeRankingApi.swift; sourceTree = "<group>"; };
|
401B3F0B2FEEF10000A5BBA3 /* MainHomeRankingApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeRankingApi.swift; sourceTree = "<group>"; };
|
||||||
401B3F0C2FEEF10000A5BBA3 /* MainHomeRankingRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeRankingRepository.swift; sourceTree = "<group>"; };
|
401B3F0C2FEEF10000A5BBA3 /* MainHomeRankingRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeRankingRepository.swift; sourceTree = "<group>"; };
|
||||||
401B3F0D2FEEF10000A5BBA3 /* MainHomeRankingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeRankingViewModel.swift; sourceTree = "<group>"; };
|
401B3F0D2FEEF10000A5BBA3 /* MainHomeRankingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeRankingViewModel.swift; sourceTree = "<group>"; };
|
||||||
|
401C10012FEF300000A5BBA3 /* HomeFollowingTabResponse.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeFollowingTabResponse.swift; sourceTree = "<group>"; };
|
||||||
|
401C10022FEF300000A5BBA3 /* MainHomeFollowingApi.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeFollowingApi.swift; sourceTree = "<group>"; };
|
||||||
|
401C10032FEF300000A5BBA3 /* MainHomeFollowingRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeFollowingRepository.swift; sourceTree = "<group>"; };
|
||||||
|
401C10042FEF300000A5BBA3 /* MainHomeFollowingViewModel.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MainHomeFollowingViewModel.swift; sourceTree = "<group>"; };
|
||||||
40235DEC2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RecentContent+CoreDataClass.swift"; sourceTree = "<group>"; };
|
40235DEC2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = "RecentContent+CoreDataClass.swift"; sourceTree = "<group>"; };
|
||||||
40235DEF2E37983D00B09A87 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; };
|
40235DEF2E37983D00B09A87 /* PersistenceController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PersistenceController.swift; sourceTree = "<group>"; };
|
||||||
40235DF22E37987300B09A87 /* RecentContentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentContentService.swift; sourceTree = "<group>"; };
|
40235DF22E37987300B09A87 /* RecentContentService.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = RecentContentService.swift; sourceTree = "<group>"; };
|
||||||
@@ -2779,6 +2795,9 @@
|
|||||||
401A3FB92FEEAFA000A5BBA3 /* Following */ = {
|
401A3FB92FEEAFA000A5BBA3 /* Following */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
|
401C10052FEF300000A5BBA3 /* Models */,
|
||||||
|
401C10062FEF300000A5BBA3 /* Repository */,
|
||||||
|
401C10042FEF300000A5BBA3 /* MainHomeFollowingViewModel.swift */,
|
||||||
401A3FB72FEEAFA000A5BBA3 /* MainHomeFollowingView.swift */,
|
401A3FB72FEEAFA000A5BBA3 /* MainHomeFollowingView.swift */,
|
||||||
);
|
);
|
||||||
path = Following;
|
path = Following;
|
||||||
@@ -2817,6 +2836,23 @@
|
|||||||
path = Repository;
|
path = Repository;
|
||||||
sourceTree = "<group>";
|
sourceTree = "<group>";
|
||||||
};
|
};
|
||||||
|
401C10052FEF300000A5BBA3 /* Models */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
401C10012FEF300000A5BBA3 /* HomeFollowingTabResponse.swift */,
|
||||||
|
);
|
||||||
|
path = Models;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
|
401C10062FEF300000A5BBA3 /* Repository */ = {
|
||||||
|
isa = PBXGroup;
|
||||||
|
children = (
|
||||||
|
401C10022FEF300000A5BBA3 /* MainHomeFollowingApi.swift */,
|
||||||
|
401C10032FEF300000A5BBA3 /* MainHomeFollowingRepository.swift */,
|
||||||
|
);
|
||||||
|
path = Repository;
|
||||||
|
sourceTree = "<group>";
|
||||||
|
};
|
||||||
40235DEA2E3797B000B09A87 /* Recent */ = {
|
40235DEA2E3797B000B09A87 /* Recent */ = {
|
||||||
isa = PBXGroup;
|
isa = PBXGroup;
|
||||||
children = (
|
children = (
|
||||||
@@ -5427,6 +5463,10 @@
|
|||||||
401A3FE02FEEB00900A5BBA3 /* MainHomeBusinessInfoSection.swift in Sources */,
|
401A3FE02FEEB00900A5BBA3 /* MainHomeBusinessInfoSection.swift in Sources */,
|
||||||
401A3FAA2FEEAFA000A5BBA3 /* MainHomeRankingView.swift in Sources */,
|
401A3FAA2FEEAFA000A5BBA3 /* MainHomeRankingView.swift in Sources */,
|
||||||
401A3FAB2FEEAFA000A5BBA3 /* MainHomeFollowingView.swift in Sources */,
|
401A3FAB2FEEAFA000A5BBA3 /* MainHomeFollowingView.swift in Sources */,
|
||||||
|
401C10112FEF300000A5BBA3 /* HomeFollowingTabResponse.swift in Sources */,
|
||||||
|
401C10122FEF300000A5BBA3 /* MainHomeFollowingApi.swift in Sources */,
|
||||||
|
401C10132FEF300000A5BBA3 /* MainHomeFollowingRepository.swift in Sources */,
|
||||||
|
401C10142FEF300000A5BBA3 /* MainHomeFollowingViewModel.swift in Sources */,
|
||||||
409143302A85652F00EEBFB1 /* CanChargeView.swift in Sources */,
|
409143302A85652F00EEBFB1 /* CanChargeView.swift in Sources */,
|
||||||
40975CA32B572298004E2F1B /* LiveRoomOverlayStrokeTextButton.swift in Sources */,
|
40975CA32B572298004E2F1B /* LiveRoomOverlayStrokeTextButton.swift in Sources */,
|
||||||
40235DED2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift in Sources */,
|
40235DED2E3797EB00B09A87 /* RecentContent+CoreDataClass.swift in Sources */,
|
||||||
@@ -6187,6 +6227,10 @@
|
|||||||
401A3FE12FEEB00900A5BBA3 /* MainHomeBusinessInfoSection.swift in Sources */,
|
401A3FE12FEEB00900A5BBA3 /* MainHomeBusinessInfoSection.swift in Sources */,
|
||||||
401A3FAE2FEEAFA000A5BBA3 /* MainHomeRankingView.swift in Sources */,
|
401A3FAE2FEEAFA000A5BBA3 /* MainHomeRankingView.swift in Sources */,
|
||||||
401A3FAF2FEEAFA000A5BBA3 /* MainHomeFollowingView.swift in Sources */,
|
401A3FAF2FEEAFA000A5BBA3 /* MainHomeFollowingView.swift in Sources */,
|
||||||
|
401C10212FEF300000A5BBA3 /* HomeFollowingTabResponse.swift in Sources */,
|
||||||
|
401C10222FEF300000A5BBA3 /* MainHomeFollowingApi.swift in Sources */,
|
||||||
|
401C10232FEF300000A5BBA3 /* MainHomeFollowingRepository.swift in Sources */,
|
||||||
|
401C10242FEF300000A5BBA3 /* MainHomeFollowingViewModel.swift in Sources */,
|
||||||
B3AF650899B8FC84E0C0CDA0 /* MainPlaceholderTabView.swift in Sources */,
|
B3AF650899B8FC84E0C0CDA0 /* MainPlaceholderTabView.swift in Sources */,
|
||||||
CA65AD0D677B4A8E9CAC0E15 /* MainTab.swift in Sources */,
|
CA65AD0D677B4A8E9CAC0E15 /* MainTab.swift in Sources */,
|
||||||
A0643C829BE7377DC09112CB /* MainTabBarButton.swift in Sources */,
|
A0643C829BE7377DC09112CB /* MainTabBarButton.swift in Sources */,
|
||||||
@@ -6977,6 +7021,10 @@
|
|||||||
401A3FE22FEEB00900A5BBA3 /* MainHomeBusinessInfoSection.swift in Sources */,
|
401A3FE22FEEB00900A5BBA3 /* MainHomeBusinessInfoSection.swift in Sources */,
|
||||||
401A3FB22FEEAFA000A5BBA3 /* MainHomeRankingView.swift in Sources */,
|
401A3FB22FEEAFA000A5BBA3 /* MainHomeRankingView.swift in Sources */,
|
||||||
401A3FB32FEEAFA000A5BBA3 /* MainHomeFollowingView.swift in Sources */,
|
401A3FB32FEEAFA000A5BBA3 /* MainHomeFollowingView.swift in Sources */,
|
||||||
|
401C10312FEF300000A5BBA3 /* HomeFollowingTabResponse.swift in Sources */,
|
||||||
|
401C10322FEF300000A5BBA3 /* MainHomeFollowingApi.swift in Sources */,
|
||||||
|
401C10332FEF300000A5BBA3 /* MainHomeFollowingRepository.swift in Sources */,
|
||||||
|
401C10342FEF300000A5BBA3 /* MainHomeFollowingViewModel.swift in Sources */,
|
||||||
ACD71056E97E13E48941ABBB /* MainPlaceholderTabView.swift in Sources */,
|
ACD71056E97E13E48941ABBB /* MainPlaceholderTabView.swift in Sources */,
|
||||||
33BA3AB731849284E62AE04B /* MainTab.swift in Sources */,
|
33BA3AB731849284E62AE04B /* MainTab.swift in Sources */,
|
||||||
B11681AC02BF51C6E2FA6FF3 /* MainTabBarButton.swift in Sources */,
|
B11681AC02BF51C6E2FA6FF3 /* MainTabBarButton.swift in Sources */,
|
||||||
|
|||||||
@@ -3042,6 +3042,64 @@ If you block this user, the following features will be restricted.
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
enum HomeFollowing {
|
||||||
|
static var emptyStateMessage: String {
|
||||||
|
pick(
|
||||||
|
ko: "팔로잉 소식이 아직 없어요.\n관심 있는 크리에이터를 팔로우해 보세요.",
|
||||||
|
en: "No following updates yet.\nFollow creators you care about.",
|
||||||
|
ja: "フォロー中の新着情報はまだありません。\n気になるクリエイターをフォローしてみましょう。"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static var loadFailedMessage: String {
|
||||||
|
pick(
|
||||||
|
ko: "팔로잉 정보를 불러오지 못했습니다.\n잠시 후 다시 시도해 주세요.",
|
||||||
|
en: "Unable to load following updates.\nPlease try again later.",
|
||||||
|
ja: "フォロー情報を読み込めませんでした。\nしばらくしてからもう一度お試しください。"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static var loginRequiredMessage: String {
|
||||||
|
pick(
|
||||||
|
ko: "로그인 후 팔로잉 소식을 확인할 수 있어요.",
|
||||||
|
en: "Log in to see following updates.",
|
||||||
|
ja: "ログインするとフォロー中の新着情報を確認できます。"
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
static var loginButtonTitle: String {
|
||||||
|
pick(ko: "로그인", en: "Log in", ja: "ログイン")
|
||||||
|
}
|
||||||
|
|
||||||
|
static var allButtonTitle: String {
|
||||||
|
pick(ko: "전체", en: "All", ja: "すべて")
|
||||||
|
}
|
||||||
|
|
||||||
|
static var onAirTitle: String {
|
||||||
|
pick(ko: "On Air", en: "On Air", ja: "On Air")
|
||||||
|
}
|
||||||
|
|
||||||
|
static var recentChatTitle: String {
|
||||||
|
pick(ko: "최근 대화", en: "Recent chats", ja: "最近のトーク")
|
||||||
|
}
|
||||||
|
|
||||||
|
static var monthlyScheduleTitle: String {
|
||||||
|
pick(ko: "이달의 스케줄", en: "This month's schedule", ja: "今月のスケジュール")
|
||||||
|
}
|
||||||
|
|
||||||
|
static var recentNewsTitle: String {
|
||||||
|
pick(ko: "최근 소식", en: "Recent news", ja: "最近のニュース")
|
||||||
|
}
|
||||||
|
|
||||||
|
static var audioTag: String {
|
||||||
|
pick(ko: "오디오", en: "Audio", ja: "オーディオ")
|
||||||
|
}
|
||||||
|
|
||||||
|
static var photoTag: String {
|
||||||
|
pick(ko: "화보", en: "Photo", ja: "フォト")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
enum HomeRanking {
|
enum HomeRanking {
|
||||||
static var emptyStateMessage: String {
|
static var emptyStateMessage: String {
|
||||||
pick(
|
pick(
|
||||||
|
|||||||
@@ -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 onTapCommunity: (Int) -> Void
|
||||||
let onTapBanner: (RecommendationBannerResponse) -> Void
|
let onTapBanner: (RecommendationBannerResponse) -> Void
|
||||||
let onTapFollowAll: (@escaping () -> Void) -> Void
|
let onTapFollowAll: (@escaping () -> Void) -> Void
|
||||||
|
let onSelectFollowingTab: () -> Bool
|
||||||
|
|
||||||
@State private var selectedTab: MainHomeTab = .recommendation
|
@State private var selectedTab: MainHomeTab = .recommendation
|
||||||
|
|
||||||
@@ -31,7 +32,7 @@ struct MainHomeView: View {
|
|||||||
|
|
||||||
TextTabBar(
|
TextTabBar(
|
||||||
items: MainHomeTab.allCases,
|
items: MainHomeTab.allCases,
|
||||||
selectedItem: $selectedTab,
|
selectedItem: selectedTabBinding,
|
||||||
title: { $0.title }
|
title: { $0.title }
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -41,6 +42,19 @@ struct MainHomeView: View {
|
|||||||
.background(Color.black.ignoresSafeArea())
|
.background(Color.black.ignoresSafeArea())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private var selectedTabBinding: Binding<MainHomeTab> {
|
||||||
|
Binding(
|
||||||
|
get: { selectedTab },
|
||||||
|
set: { newValue in
|
||||||
|
if newValue == .following, onSelectFollowingTab() == false {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
selectedTab = newValue
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
@ViewBuilder
|
@ViewBuilder
|
||||||
private var tabContent: some View {
|
private var tabContent: some View {
|
||||||
switch selectedTab {
|
switch selectedTab {
|
||||||
@@ -91,7 +105,8 @@ struct MainHomeView_Previews: PreviewProvider {
|
|||||||
onTapCharacter: { _ in },
|
onTapCharacter: { _ in },
|
||||||
onTapCommunity: { _ in },
|
onTapCommunity: { _ in },
|
||||||
onTapBanner: { _ in },
|
onTapBanner: { _ in },
|
||||||
onTapFollowAll: { action in action() }
|
onTapFollowAll: { action in action() },
|
||||||
|
onSelectFollowingTab: { true }
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -163,7 +163,8 @@ struct MainView: View {
|
|||||||
onTapCharacter: handleRecommendationCharacterTap,
|
onTapCharacter: handleRecommendationCharacterTap,
|
||||||
onTapCommunity: handleRecommendationCommunityTap,
|
onTapCommunity: handleRecommendationCommunityTap,
|
||||||
onTapBanner: handleRecommendationBannerTap,
|
onTapBanner: handleRecommendationBannerTap,
|
||||||
onTapFollowAll: handleRecommendationFollowAllTap
|
onTapFollowAll: handleRecommendationFollowAllTap,
|
||||||
|
onSelectFollowingTab: handleFollowingTabSelection
|
||||||
)
|
)
|
||||||
case .content:
|
case .content:
|
||||||
MainPlaceholderTabView(title: MainTab.content.title)
|
MainPlaceholderTabView(title: MainTab.content.title)
|
||||||
@@ -484,6 +485,15 @@ struct MainView: View {
|
|||||||
performRecommendationDetailAction(action: action)
|
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) {
|
private func handleRecommendationBannerTap(_ item: RecommendationBannerResponse) {
|
||||||
if let eventItem = item.eventItem {
|
if let eventItem = item.eventItem {
|
||||||
|
|||||||
@@ -73,7 +73,7 @@
|
|||||||
|
|
||||||
### Phase 1: 기준점 점검과 탭 guard 설계
|
### Phase 1: 기준점 점검과 탭 guard 설계
|
||||||
|
|
||||||
- [ ] **Task 1.1: PRD와 기존 팔로잉 placeholder 확인**
|
- [x] **Task 1.1: PRD와 기존 팔로잉 placeholder 확인**
|
||||||
- 대상 파일:
|
- 대상 파일:
|
||||||
- 확인: `docs/20260630_메인_홈_팔로잉_탭/prd.md`
|
- 확인: `docs/20260630_메인_홈_팔로잉_탭/prd.md`
|
||||||
- 확인: `SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingView.swift`
|
- 확인: `SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingView.swift`
|
||||||
@@ -86,7 +86,7 @@
|
|||||||
- 실행 명령: `sed -n '1,120p' SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingView.swift && sed -n '1,120p' SodaLive/Sources/V2/Main/Home/MainHomeView.swift`
|
- 실행 명령: `sed -n '1,120p' SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingView.swift && sed -n '1,120p' SodaLive/Sources/V2/Main/Home/MainHomeView.swift`
|
||||||
- 기대 결과: 팔로잉 탭은 placeholder이고, `MainHomeView`는 탭 shell 역할만 담당한다.
|
- 기대 결과: 팔로잉 탭은 placeholder이고, `MainHomeView`는 탭 shell 역할만 담당한다.
|
||||||
|
|
||||||
- [ ] **Task 1.2: 팔로잉 탭 선택 로그인 guard 연결**
|
- [x] **Task 1.2: 팔로잉 탭 선택 로그인 guard 연결**
|
||||||
- 대상 파일:
|
- 대상 파일:
|
||||||
- 수정: `SodaLive/Sources/V2/Main/Home/MainHomeView.swift`
|
- 수정: `SodaLive/Sources/V2/Main/Home/MainHomeView.swift`
|
||||||
- 수정: `SodaLive/Sources/V2/Main/MainView.swift`
|
- 수정: `SodaLive/Sources/V2/Main/MainView.swift`
|
||||||
@@ -105,7 +105,7 @@
|
|||||||
|
|
||||||
### Phase 2: API, Repository, Response model, ViewModel
|
### Phase 2: API, Repository, Response model, ViewModel
|
||||||
|
|
||||||
- [ ] **Task 2.1: 팔로잉 응답 모델 생성**
|
- [x] **Task 2.1: 팔로잉 응답 모델 생성**
|
||||||
- 대상 파일:
|
- 대상 파일:
|
||||||
- 생성: `SodaLive/Sources/V2/Main/Home/Following/Models/HomeFollowingTabResponse.swift`
|
- 생성: `SodaLive/Sources/V2/Main/Home/Following/Models/HomeFollowingTabResponse.swift`
|
||||||
- 작업 내용:
|
- 작업 내용:
|
||||||
@@ -117,7 +117,7 @@
|
|||||||
- 실행 명령: `rg "struct HomeFollowingTabResponse|struct ChatRoomListItemResponse|enum CreatorActivityType|struct FollowingNewsResponse|struct FollowingCreatorRankingNewsResponse|struct FollowingContentNewsResponse|struct FollowingContentRankingNewsResponse|struct FollowingCommunityPostNewsResponse|enum FollowingNewsType|unknown\\(" SodaLive/Sources/V2/Main/Home/Following/Models/HomeFollowingTabResponse.swift`
|
- 실행 명령: `rg "struct HomeFollowingTabResponse|struct ChatRoomListItemResponse|enum CreatorActivityType|struct FollowingNewsResponse|struct FollowingCreatorRankingNewsResponse|struct FollowingContentNewsResponse|struct FollowingContentRankingNewsResponse|struct FollowingCommunityPostNewsResponse|enum FollowingNewsType|unknown\\(" SodaLive/Sources/V2/Main/Home/Following/Models/HomeFollowingTabResponse.swift`
|
||||||
- 기대 결과: PRD의 응답 모델과 unknown enum 처리가 확인된다.
|
- 기대 결과: PRD의 응답 모델과 unknown enum 처리가 확인된다.
|
||||||
|
|
||||||
- [ ] **Task 2.2: 팔로잉 API endpoint 생성**
|
- [x] **Task 2.2: 팔로잉 API endpoint 생성**
|
||||||
- 대상 파일:
|
- 대상 파일:
|
||||||
- 생성: `SodaLive/Sources/V2/Main/Home/Following/Repository/MainHomeFollowingApi.swift`
|
- 생성: `SodaLive/Sources/V2/Main/Home/Following/Repository/MainHomeFollowingApi.swift`
|
||||||
- 확인: `SodaLive/Sources/V2/Main/Home/Recommendation/Repository/MainHomeRecommendationApi.swift`
|
- 확인: `SodaLive/Sources/V2/Main/Home/Recommendation/Repository/MainHomeRecommendationApi.swift`
|
||||||
@@ -130,7 +130,7 @@
|
|||||||
- 실행 명령: `rg "enum MainHomeFollowingApi|getFollowing|/api/v2/home/following|Authorization|requestPlain|TargetType" SodaLive/Sources/V2/Main/Home/Following/Repository/MainHomeFollowingApi.swift`
|
- 실행 명령: `rg "enum MainHomeFollowingApi|getFollowing|/api/v2/home/following|Authorization|requestPlain|TargetType" SodaLive/Sources/V2/Main/Home/Following/Repository/MainHomeFollowingApi.swift`
|
||||||
- 기대 결과: 팔로잉 전용 API endpoint와 인증 헤더가 확인된다.
|
- 기대 결과: 팔로잉 전용 API endpoint와 인증 헤더가 확인된다.
|
||||||
|
|
||||||
- [ ] **Task 2.3: 팔로잉 Repository 생성**
|
- [x] **Task 2.3: 팔로잉 Repository 생성**
|
||||||
- 대상 파일:
|
- 대상 파일:
|
||||||
- 생성: `SodaLive/Sources/V2/Main/Home/Following/Repository/MainHomeFollowingRepository.swift`
|
- 생성: `SodaLive/Sources/V2/Main/Home/Following/Repository/MainHomeFollowingRepository.swift`
|
||||||
- 확인: `SodaLive/Sources/V2/Main/Home/Ranking/Repository/MainHomeRankingRepository.swift`
|
- 확인: `SodaLive/Sources/V2/Main/Home/Ranking/Repository/MainHomeRankingRepository.swift`
|
||||||
@@ -141,7 +141,7 @@
|
|||||||
- 실행 명령: `rg "final class MainHomeFollowingRepository|MoyaProvider<MainHomeFollowingApi>|getFollowing\\(\\)|AnyPublisher<Response, MoyaError>" SodaLive/Sources/V2/Main/Home/Following/Repository/MainHomeFollowingRepository.swift`
|
- 실행 명령: `rg "final class MainHomeFollowingRepository|MoyaProvider<MainHomeFollowingApi>|getFollowing\\(\\)|AnyPublisher<Response, MoyaError>" SodaLive/Sources/V2/Main/Home/Following/Repository/MainHomeFollowingRepository.swift`
|
||||||
- 기대 결과: 기존 V2 홈 Repository와 같은 패턴이 확인된다.
|
- 기대 결과: 기존 V2 홈 Repository와 같은 패턴이 확인된다.
|
||||||
|
|
||||||
- [ ] **Task 2.4: 팔로잉 ViewModel 생성**
|
- [x] **Task 2.4: 팔로잉 ViewModel 생성**
|
||||||
- 대상 파일:
|
- 대상 파일:
|
||||||
- 생성: `SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingViewModel.swift`
|
- 생성: `SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingViewModel.swift`
|
||||||
- 확인: `SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationViewModel.swift`
|
- 확인: `SodaLive/Sources/V2/Main/Home/Recommendation/MainHomeRecommendationViewModel.swift`
|
||||||
@@ -159,7 +159,7 @@
|
|||||||
- 실행 명령: `rg "final class MainHomeFollowingViewModel|fetchFollowing|ApiResponse<HomeFollowingTabResponse>|isLoginRequired|emptyStateMessage|loadFailedMessage|hasLoaded|ERROR_LOG" SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingViewModel.swift`
|
- 실행 명령: `rg "final class MainHomeFollowingViewModel|fetchFollowing|ApiResponse<HomeFollowingTabResponse>|isLoginRequired|emptyStateMessage|loadFailedMessage|hasLoaded|ERROR_LOG" SodaLive/Sources/V2/Main/Home/Following/MainHomeFollowingViewModel.swift`
|
||||||
- 기대 결과: API 디코딩, 로그인 필요 상태, empty/API 실패 문구 처리가 확인된다.
|
- 기대 결과: API 디코딩, 로그인 필요 상태, empty/API 실패 문구 처리가 확인된다.
|
||||||
|
|
||||||
- [ ] **Task 2.5: 팔로잉 I18n 문구 추가**
|
- [x] **Task 2.5: 팔로잉 I18n 문구 추가**
|
||||||
- 대상 파일:
|
- 대상 파일:
|
||||||
- 수정: `SodaLive/Sources/I18n/I18n.swift`
|
- 수정: `SodaLive/Sources/I18n/I18n.swift`
|
||||||
- 작업 내용:
|
- 작업 내용:
|
||||||
@@ -467,3 +467,8 @@
|
|||||||
- 2026-06-30: `SodaLive/Sources/V2/Main/Home/MainHomeView.swift`의 탭 전환이 `TextTabBar` binding 기반임을 확인해 custom binding setter 방식으로 로그인 guard 계획을 세웠다.
|
- 2026-06-30: `SodaLive/Sources/V2/Main/Home/MainHomeView.swift`의 탭 전환이 `TextTabBar` binding 기반임을 확인해 custom binding setter 방식으로 로그인 guard 계획을 세웠다.
|
||||||
- 2026-06-30: `SodaLive/Sources/ContentView.swift`에서 `.followingList -> FollowCreatorView()`, `.chatRoom(id:) -> ChatRoomView(roomId:)` 라우팅을 확인했다.
|
- 2026-06-30: `SodaLive/Sources/ContentView.swift`에서 `.followingList -> FollowCreatorView()`, `.chatRoom(id:) -> ChatRoomView(roomId:)` 라우팅을 확인했다.
|
||||||
- 2026-06-30: `SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllView.swift`와 `AppState`의 pending community post 구조가 `creatorId`를 요구함을 확인해 `COMMUNITY_POST` tap 라우팅 제약을 계획에 명시했다.
|
- 2026-06-30: `SodaLive/Sources/Explorer/Profile/CreatorCommunity/All/CreatorCommunityAllView.swift`와 `AppState`의 pending community post 구조가 `creatorId`를 요구함을 확인해 `COMMUNITY_POST` tap 라우팅 제약을 계획에 명시했다.
|
||||||
|
- 2026-06-30: Phase 1-2 구현을 진행해 `MainHomeView`의 팔로잉 탭 custom `Binding` guard, `MainView.handleFollowingTabSelection()`, 팔로잉 응답 모델/API/Repository/ViewModel, `I18n.HomeFollowing` 문구를 추가했다.
|
||||||
|
- 2026-06-30: `rg "onSelectFollowingTab|Binding\(|selectedTab = \.following|setAppStep\(step: \.login\)|TextTabBar" SodaLive/Sources/V2/Main/Home/MainHomeView.swift SodaLive/Sources/V2/Main/MainView.swift`로 팔로잉 탭 guard 연결을 확인했다.
|
||||||
|
- 2026-06-30: `rg "struct HomeFollowingTabResponse|struct ChatRoomListItemResponse|enum CreatorActivityType|struct FollowingNewsResponse|struct FollowingCreatorRankingNewsResponse|struct FollowingContentNewsResponse|struct FollowingContentRankingNewsResponse|struct FollowingCommunityPostNewsResponse|enum FollowingNewsType|unknown\(" SodaLive/Sources/V2/Main/Home/Following/Models/HomeFollowingTabResponse.swift`로 Phase 2 응답 모델과 unknown enum 처리를 확인했다.
|
||||||
|
- 2026-06-30: `rg "enum MainHomeFollowingApi|getFollowing|/api/v2/home/following|Authorization|requestPlain|TargetType|final class MainHomeFollowingRepository|MoyaProvider<MainHomeFollowingApi>|AnyPublisher<Response, MoyaError>" SodaLive/Sources/V2/Main/Home/Following/Repository`로 API/Repository 패턴을 확인했다.
|
||||||
|
- 2026-06-30: `xcodebuild -workspace "SodaLive.xcworkspace" -scheme "SodaLive" -configuration Debug build`를 실행해 빌드 성공을 확인했다. 기존 프로젝트의 duplicate build file/run script warning은 남아 있으나 Phase 1-2 컴파일 오류는 없었다.
|
||||||
|
|||||||
Reference in New Issue
Block a user