From 0fd49a71f60b72e576c2bc36e5ccac22809d7345 Mon Sep 17 00:00:00 2001 From: Yu Sung Date: Fri, 14 Nov 2025 01:24:20 +0900 Subject: [PATCH] =?UTF-8?q?feat(home):=20=ED=99=88=20=EC=B6=94=EC=B2=9C=20?= =?UTF-8?q?=EC=BD=98=ED=85=90=EC=B8=A0=20=EC=84=B9=EC=85=98=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Content/ContentItemView.swift | 6 ++- SodaLive/Sources/Home/GetHomeResponse.swift | 1 + SodaLive/Sources/Home/HomeApi.swift | 12 ++++++ SodaLive/Sources/Home/HomeTabRepository.swift | 9 +++++ SodaLive/Sources/Home/HomeTabView.swift | 40 +++++++++++++++++++ SodaLive/Sources/Home/HomeTabViewModel.swift | 40 +++++++++++++++++++ 6 files changed, 106 insertions(+), 2 deletions(-) diff --git a/SodaLive/Sources/Content/ContentItemView.swift b/SodaLive/Sources/Content/ContentItemView.swift index 2a3394f..f8f53a2 100644 --- a/SodaLive/Sources/Content/ContentItemView.swift +++ b/SodaLive/Sources/Content/ContentItemView.swift @@ -14,12 +14,14 @@ struct ContentItemView: View { let item: AudioContentMainItem + var itemSize: CGFloat = 160 + var body: some View { VStack(alignment: .leading, spacing: 0) { ZStack(alignment: .top) { DownsampledKFImage( url: URL(string: item.coverImageUrl), - size: CGSize(width: 160, height: 160) + size: CGSize(width: itemSize, height: itemSize) ) .cornerRadius(16) @@ -51,7 +53,7 @@ struct ContentItemView: View { .padding(.horizontal, 6) .padding(.top, 4) } - .frame(width: 160) + .frame(width: itemSize) .onTapGesture { if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { AppState.shared.setAppStep(step: .contentDetail(contentId: item.contentId)) diff --git a/SodaLive/Sources/Home/GetHomeResponse.swift b/SodaLive/Sources/Home/GetHomeResponse.swift index 38ff21f..d8bca6f 100644 --- a/SodaLive/Sources/Home/GetHomeResponse.swift +++ b/SodaLive/Sources/Home/GetHomeResponse.swift @@ -20,4 +20,5 @@ struct GetHomeResponse: Decodable { let recommendChannelList: [RecommendChannelResponse] let freeContentList: [AudioContentMainItem] let pointAvailableContentList: [AudioContentMainItem] + let recommendContentList: [AudioContentMainItem] } diff --git a/SodaLive/Sources/Home/HomeApi.swift b/SodaLive/Sources/Home/HomeApi.swift index 9e6f667..db5299f 100644 --- a/SodaLive/Sources/Home/HomeApi.swift +++ b/SodaLive/Sources/Home/HomeApi.swift @@ -12,6 +12,7 @@ enum HomeApi { case getHomeData(isAdultContentVisible: Bool, contentType: ContentType) case getLatestContentByTheme(theme: String, isAdultContentVisible: Bool, contentType: ContentType) case getDayOfWeekSeriesList(dayOfWeek: SeriesPublishedDaysOfWeek, isAdultContentVisible: Bool, contentType: ContentType) + case getRecommendContents(isAdultContentVisible: Bool, contentType: ContentType) } extension HomeApi: TargetType { @@ -29,6 +30,9 @@ extension HomeApi: TargetType { case .getDayOfWeekSeriesList: return "/api/home/day-of-week-series" + + case .getRecommendContents: + return "/api/home/recommend-contents" } } @@ -63,6 +67,14 @@ extension HomeApi: TargetType { "contentType": contentType ] as [String: Any] + return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) + + case .getRecommendContents(let isAdultContentVisible, let contentType): + let parameters = [ + "isAdultContentVisible": isAdultContentVisible, + "contentType": contentType + ] as [String: Any] + return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) } } diff --git a/SodaLive/Sources/Home/HomeTabRepository.swift b/SodaLive/Sources/Home/HomeTabRepository.swift index 402e6d8..2b82717 100644 --- a/SodaLive/Sources/Home/HomeTabRepository.swift +++ b/SodaLive/Sources/Home/HomeTabRepository.swift @@ -41,4 +41,13 @@ class HomeTabRepository { ) ) } + + func getRecommendContents() -> AnyPublisher { + return api.requestPublisher( + .getRecommendContents( + isAdultContentVisible: UserDefaults.isAdultContentVisible(), + contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL + ) + ) + } } diff --git a/SodaLive/Sources/Home/HomeTabView.swift b/SodaLive/Sources/Home/HomeTabView.swift index 5ee856a..1a11209 100644 --- a/SodaLive/Sources/Home/HomeTabView.swift +++ b/SodaLive/Sources/Home/HomeTabView.swift @@ -327,6 +327,46 @@ struct HomeTabView: View { } } + if !viewModel.recommendContentList.isEmpty { + VStack(alignment: .leading, spacing: 16) { + HStack { + Text("추천 콘텐츠") + .font(.custom(Font.preBold.rawValue, size: 24)) + .foregroundColor(.white) + + Spacer() + + Image("ic_refresh") + .onTapGesture { + viewModel.refreshRecommendContents() + } + } + .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.recommendContentList.indices, id: \.self) { idx in + ContentItemView(item: viewModel.recommendContentList[idx], itemSize: width) + } + } + .padding(.horizontal, horizontalPadding) + } + } + Text(""" - 회사명 : 주식회사 소다라이브 diff --git a/SodaLive/Sources/Home/HomeTabViewModel.swift b/SodaLive/Sources/Home/HomeTabViewModel.swift index 878e0f5..bd24f19 100644 --- a/SodaLive/Sources/Home/HomeTabViewModel.swift +++ b/SodaLive/Sources/Home/HomeTabViewModel.swift @@ -33,6 +33,7 @@ final class HomeTabViewModel: ObservableObject { @Published var recommendChannelList: [RecommendChannelResponse] = [] @Published var freeContentList: [AudioContentMainItem] = [] @Published var pointAvailableContentList: [AudioContentMainItem] = [] + @Published var recommendContentList: [AudioContentMainItem] = [] func fetchData() { isLoading = true @@ -65,6 +66,7 @@ final class HomeTabViewModel: ObservableObject { self.recommendChannelList = data.recommendChannelList self.freeContentList = data.freeContentList self.pointAvailableContentList = data.pointAvailableContentList + self.recommendContentList = data.recommendContentList } else { if let message = decoded.message { self.errorMessage = message @@ -197,4 +199,42 @@ final class HomeTabViewModel: ObservableObject { } .store(in: &subscription) } + + func refreshRecommendContents() { + isLoading = true + + repository.getRecommendContents() + .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<[AudioContentMainItem]>.self, from: responseData) + + if let data = decoded.data, decoded.success { + self.recommendContentList = data + } else { + if let message = decoded.message { + self.errorMessage = message + } else { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + } + + self.isShowPopup = true + } + } catch { + self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." + self.isShowPopup = true + } + } + .store(in: &subscription) + } }