feat(home): 홈 추천 콘텐츠 섹션 추가

This commit is contained in:
Yu Sung
2025-11-14 01:24:20 +09:00
parent 0902b1fe30
commit 0fd49a71f6
6 changed files with 106 additions and 2 deletions

View File

@@ -14,12 +14,14 @@ struct ContentItemView: View {
let item: AudioContentMainItem let item: AudioContentMainItem
var itemSize: CGFloat = 160
var body: some View { var body: some View {
VStack(alignment: .leading, spacing: 0) { VStack(alignment: .leading, spacing: 0) {
ZStack(alignment: .top) { ZStack(alignment: .top) {
DownsampledKFImage( DownsampledKFImage(
url: URL(string: item.coverImageUrl), url: URL(string: item.coverImageUrl),
size: CGSize(width: 160, height: 160) size: CGSize(width: itemSize, height: itemSize)
) )
.cornerRadius(16) .cornerRadius(16)
@@ -51,7 +53,7 @@ struct ContentItemView: View {
.padding(.horizontal, 6) .padding(.horizontal, 6)
.padding(.top, 4) .padding(.top, 4)
} }
.frame(width: 160) .frame(width: itemSize)
.onTapGesture { .onTapGesture {
if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty { if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
AppState.shared.setAppStep(step: .contentDetail(contentId: item.contentId)) AppState.shared.setAppStep(step: .contentDetail(contentId: item.contentId))

View File

@@ -20,4 +20,5 @@ struct GetHomeResponse: Decodable {
let recommendChannelList: [RecommendChannelResponse] let recommendChannelList: [RecommendChannelResponse]
let freeContentList: [AudioContentMainItem] let freeContentList: [AudioContentMainItem]
let pointAvailableContentList: [AudioContentMainItem] let pointAvailableContentList: [AudioContentMainItem]
let recommendContentList: [AudioContentMainItem]
} }

View File

@@ -12,6 +12,7 @@ enum HomeApi {
case getHomeData(isAdultContentVisible: Bool, contentType: ContentType) case getHomeData(isAdultContentVisible: Bool, contentType: ContentType)
case getLatestContentByTheme(theme: String, isAdultContentVisible: Bool, contentType: ContentType) case getLatestContentByTheme(theme: String, isAdultContentVisible: Bool, contentType: ContentType)
case getDayOfWeekSeriesList(dayOfWeek: SeriesPublishedDaysOfWeek, isAdultContentVisible: Bool, contentType: ContentType) case getDayOfWeekSeriesList(dayOfWeek: SeriesPublishedDaysOfWeek, isAdultContentVisible: Bool, contentType: ContentType)
case getRecommendContents(isAdultContentVisible: Bool, contentType: ContentType)
} }
extension HomeApi: TargetType { extension HomeApi: TargetType {
@@ -29,6 +30,9 @@ extension HomeApi: TargetType {
case .getDayOfWeekSeriesList: case .getDayOfWeekSeriesList:
return "/api/home/day-of-week-series" return "/api/home/day-of-week-series"
case .getRecommendContents:
return "/api/home/recommend-contents"
} }
} }
@@ -63,6 +67,14 @@ extension HomeApi: TargetType {
"contentType": contentType "contentType": contentType
] as [String: Any] ] 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) return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
} }
} }

View File

@@ -41,4 +41,13 @@ class HomeTabRepository {
) )
) )
} }
func getRecommendContents() -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(
.getRecommendContents(
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
)
)
}
} }

View File

@@ -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(""" Text("""
- 회사명 : 주식회사 소다라이브 - 회사명 : 주식회사 소다라이브

View File

@@ -33,6 +33,7 @@ final class HomeTabViewModel: ObservableObject {
@Published var recommendChannelList: [RecommendChannelResponse] = [] @Published var recommendChannelList: [RecommendChannelResponse] = []
@Published var freeContentList: [AudioContentMainItem] = [] @Published var freeContentList: [AudioContentMainItem] = []
@Published var pointAvailableContentList: [AudioContentMainItem] = [] @Published var pointAvailableContentList: [AudioContentMainItem] = []
@Published var recommendContentList: [AudioContentMainItem] = []
func fetchData() { func fetchData() {
isLoading = true isLoading = true
@@ -65,6 +66,7 @@ final class HomeTabViewModel: ObservableObject {
self.recommendChannelList = data.recommendChannelList self.recommendChannelList = data.recommendChannelList
self.freeContentList = data.freeContentList self.freeContentList = data.freeContentList
self.pointAvailableContentList = data.pointAvailableContentList self.pointAvailableContentList = data.pointAvailableContentList
self.recommendContentList = data.recommendContentList
} else { } else {
if let message = decoded.message { if let message = decoded.message {
self.errorMessage = message self.errorMessage = message
@@ -197,4 +199,42 @@ final class HomeTabViewModel: ObservableObject {
} }
.store(in: &subscription) .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)
}
} }