feat(home): 홈 추천 콘텐츠 섹션 추가
This commit is contained in:
@@ -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))
|
||||||
|
|||||||
@@ -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]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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("""
|
||||||
- 회사명 : 주식회사 소다라이브
|
- 회사명 : 주식회사 소다라이브
|
||||||
|
|
||||||
|
|||||||
@@ -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)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user