feat(content): 추천 오디오 탭을 추가한다
This commit is contained in:
12
SodaLive/Sources/V2/Main/Content/MainContentTab.swift
Normal file
12
SodaLive/Sources/V2/Main/Content/MainContentTab.swift
Normal file
@@ -0,0 +1,12 @@
|
||||
import Foundation
|
||||
|
||||
enum MainContentTab: CaseIterable, Hashable {
|
||||
case recommendation
|
||||
|
||||
var title: String {
|
||||
switch self {
|
||||
case .recommendation:
|
||||
return I18n.MainContentRecommendation.tabTitle
|
||||
}
|
||||
}
|
||||
}
|
||||
64
SodaLive/Sources/V2/Main/Content/MainContentView.swift
Normal file
64
SodaLive/Sources/V2/Main/Content/MainContentView.swift
Normal file
@@ -0,0 +1,64 @@
|
||||
import SwiftUI
|
||||
|
||||
struct MainContentView: View {
|
||||
let onTapCanCharge: () -> Void
|
||||
let onTapSearch: () -> Void
|
||||
let onTapNotificationList: () -> Void
|
||||
let onTapContent: (Int) -> Void
|
||||
let onTapBanner: (AudioBannerResponse) -> Void
|
||||
let onTapSeries: (Int) -> Void
|
||||
|
||||
@State private var selectedTab: MainContentTab = .recommendation
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
HomeTitleBar {
|
||||
HStack(spacing: SodaSpacing.s14) {
|
||||
Image("ic_bar_cash")
|
||||
.onTapGesture { onTapCanCharge() }
|
||||
|
||||
Image("ic_bar_search")
|
||||
.onTapGesture { onTapSearch() }
|
||||
|
||||
Image("ic_bar_bell")
|
||||
.onTapGesture { onTapNotificationList() }
|
||||
}
|
||||
}
|
||||
|
||||
TextTabBar(
|
||||
items: MainContentTab.allCases,
|
||||
selectedItem: $selectedTab,
|
||||
title: { $0.title }
|
||||
)
|
||||
|
||||
tabContent
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
}
|
||||
.background(Color.black.ignoresSafeArea())
|
||||
}
|
||||
|
||||
@ViewBuilder
|
||||
private var tabContent: some View {
|
||||
switch selectedTab {
|
||||
case .recommendation:
|
||||
MainContentRecommendationView(
|
||||
onTapContent: onTapContent,
|
||||
onTapBanner: onTapBanner,
|
||||
onTapSeries: onTapSeries
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct MainContentView_Previews: PreviewProvider {
|
||||
static var previews: some View {
|
||||
MainContentView(
|
||||
onTapCanCharge: {},
|
||||
onTapSearch: {},
|
||||
onTapNotificationList: {},
|
||||
onTapContent: { _ in },
|
||||
onTapBanner: { _ in },
|
||||
onTapSeries: { _ in }
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
import SwiftUI
|
||||
|
||||
struct MainContentAudioBannerSection: View {
|
||||
let items: [AudioBannerResponse]
|
||||
let onTapBanner: (AudioBannerResponse) -> Void
|
||||
|
||||
var body: some View {
|
||||
BannerCarouselSection(
|
||||
items: items,
|
||||
id: fallbackId,
|
||||
imageUrl: { $0.imageUrl },
|
||||
action: onTapBanner
|
||||
)
|
||||
}
|
||||
|
||||
private func fallbackId(for item: AudioBannerResponse, index: Int) -> String {
|
||||
if let eventItem = item.eventItem {
|
||||
return "content-banner-\(index)-event-\(eventItem.id)"
|
||||
}
|
||||
|
||||
if let creatorId = item.creatorId {
|
||||
return "content-banner-\(index)-creator-\(creatorId)"
|
||||
}
|
||||
|
||||
if let seriesId = item.seriesId {
|
||||
return "content-banner-\(index)-series-\(seriesId)"
|
||||
}
|
||||
|
||||
if let link = item.link, !link.isEmpty {
|
||||
return "content-banner-\(index)-link-\(link)"
|
||||
}
|
||||
|
||||
return "content-banner-\(index)-image-\(item.imageUrl)"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
import SwiftUI
|
||||
|
||||
struct MainContentAudioEmptyStateView: View {
|
||||
let message: String
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: SodaSpacing.s12) {
|
||||
Image(systemName: "waveform")
|
||||
.font(.system(size: 32, weight: .semibold))
|
||||
.foregroundColor(Color.gray600)
|
||||
|
||||
Text(message)
|
||||
.appFont(.body1)
|
||||
.foregroundColor(Color.gray400)
|
||||
.multilineTextAlignment(.center)
|
||||
}
|
||||
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||
.padding(SodaSpacing.s20)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
import SwiftUI
|
||||
|
||||
struct MainContentAudioHorizontalCardSection: View {
|
||||
let title: String
|
||||
let items: [AudioCardResponse]
|
||||
let onTapContent: (Int) -> Void
|
||||
|
||||
var body: some View {
|
||||
if !items.isEmpty {
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s12) {
|
||||
SectionTitle(title: title)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(alignment: .top, spacing: SodaSpacing.s14) {
|
||||
ForEach(items) { item in
|
||||
Button {
|
||||
onTapContent(item.audioContentId)
|
||||
} label: {
|
||||
AudioContentThumbnailCard(
|
||||
item: AudioContentThumbnailCardItem(audioCard: item),
|
||||
size: .medium
|
||||
)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, SodaSpacing.s20)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import SwiftUI
|
||||
|
||||
struct MainContentAudioListCarouselSection: View {
|
||||
let title: String
|
||||
let items: [AudioCardResponse]
|
||||
let onTapContent: (Int) -> Void
|
||||
|
||||
private var chunks: [[AudioCardResponse]] {
|
||||
stride(from: 0, to: items.count, by: 3).map {
|
||||
Array(items[$0..<min($0 + 3, items.count)])
|
||||
}
|
||||
}
|
||||
|
||||
private var rowWidth: CGFloat {
|
||||
AudioContentListRowStyle.compactWidth()
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if !items.isEmpty {
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s12) {
|
||||
SectionTitle(title: title)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(alignment: .top, spacing: SodaSpacing.s14) {
|
||||
ForEach(Array(chunks.enumerated()), id: \.offset) { _, chunk in
|
||||
VStack(spacing: SodaSpacing.s12) {
|
||||
ForEach(chunk) { item in
|
||||
AudioContentListRow(
|
||||
item: rowItem(from: item),
|
||||
style: .compact(width: rowWidth, showsAdultTag: true)
|
||||
) {
|
||||
onTapContent(item.audioContentId)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(width: rowWidth)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, SodaSpacing.s20)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func rowItem(from item: AudioCardResponse) -> AudioContentListRowItem {
|
||||
AudioContentListRowItem(
|
||||
id: item.audioContentId,
|
||||
title: item.title,
|
||||
subtitle: item.creatorNickname,
|
||||
imageUrl: item.imageUrl,
|
||||
price: item.price,
|
||||
isAdult: item.isAdult,
|
||||
isPointAvailable: item.isPointAvailable,
|
||||
isFirstContent: item.isFirstContent,
|
||||
isOriginalSeries: item.isOriginalSeries,
|
||||
isOwned: false,
|
||||
isRented: false
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
import SwiftUI
|
||||
|
||||
struct MainContentMostCommentedAudioSection: View {
|
||||
let title: String
|
||||
let items: [CommentedAudioResponse]
|
||||
let onTapContent: (Int) -> Void
|
||||
|
||||
private let thumbnailSize: CGFloat = 88
|
||||
|
||||
private var cardWidth: CGFloat {
|
||||
UIScreen.main.bounds.width - (SodaSpacing.s14 * 2)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
if !items.isEmpty {
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s14) {
|
||||
SectionTitle(title: title)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(alignment: .top, spacing: SodaSpacing.s4) {
|
||||
ForEach(items) { item in
|
||||
card(item)
|
||||
}
|
||||
}
|
||||
.padding(.leading, SodaSpacing.s14)
|
||||
.padding(.trailing, SodaSpacing.s20)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private func card(_ item: CommentedAudioResponse) -> some View {
|
||||
Button {
|
||||
onTapContent(item.audioContentId)
|
||||
} label: {
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s14) {
|
||||
audioSummary(item)
|
||||
|
||||
commentPreview(item)
|
||||
}
|
||||
.padding(SodaSpacing.s14)
|
||||
.frame(width: cardWidth, alignment: .leading)
|
||||
.background(Color.gray900)
|
||||
.clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
|
||||
private func audioSummary(_ item: CommentedAudioResponse) -> some View {
|
||||
HStack(alignment: .center, spacing: SodaSpacing.s14) {
|
||||
DownsampledKFImage(
|
||||
url: URL(string: item.imageUrl),
|
||||
size: CGSize(width: thumbnailSize, height: thumbnailSize)
|
||||
)
|
||||
.background(Color.gray800)
|
||||
.clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous))
|
||||
|
||||
Text(item.title)
|
||||
.appFont(size: 16, weight: .bold)
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
}
|
||||
}
|
||||
|
||||
private func commentPreview(_ item: CommentedAudioResponse) -> some View {
|
||||
HStack(alignment: .center, spacing: SodaSpacing.s14) {
|
||||
DownsampledKFImage(
|
||||
url: URL(string: item.latestCommentWriterProfileImageUrl),
|
||||
size: CGSize(width: 28, height: 28)
|
||||
)
|
||||
.background(Color.gray700)
|
||||
.clipShape(Circle())
|
||||
|
||||
Text(item.latestComment)
|
||||
.appFont(size: 14, weight: .regular)
|
||||
.foregroundColor(.white)
|
||||
.lineLimit(2)
|
||||
.truncationMode(.tail)
|
||||
.frame(maxWidth: .infinity, alignment: .leading)
|
||||
|
||||
Image(systemName: "chevron.down")
|
||||
.font(.system(size: 12, weight: .medium))
|
||||
.foregroundColor(.white)
|
||||
.frame(width: 18, height: 18)
|
||||
}
|
||||
.padding(SodaSpacing.s12)
|
||||
.background(Color.gray800)
|
||||
.clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
import SwiftUI
|
||||
|
||||
struct MainContentRecommendedAudioGridSection: View {
|
||||
let title: String
|
||||
let items: [AudioCardResponse]
|
||||
let onTapContent: (Int) -> Void
|
||||
|
||||
private let columns = [
|
||||
GridItem(.flexible(), spacing: SodaSpacing.s14),
|
||||
GridItem(.flexible(), spacing: SodaSpacing.s14)
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
if !items.isEmpty {
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s12) {
|
||||
SectionTitle(title: title)
|
||||
|
||||
LazyVGrid(columns: columns, alignment: .leading, spacing: SodaSpacing.s20) {
|
||||
ForEach(items) { item in
|
||||
Button {
|
||||
onTapContent(item.audioContentId)
|
||||
} label: {
|
||||
GeometryReader { proxy in
|
||||
let width = proxy.size.width
|
||||
|
||||
AudioContentThumbnailCard(
|
||||
item: AudioContentThumbnailCardItem(audioCard: item),
|
||||
width: width
|
||||
)
|
||||
}
|
||||
.aspectRatio(0.78, contentMode: .fit)
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, SodaSpacing.s20)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,56 @@
|
||||
import SwiftUI
|
||||
|
||||
struct MainContentVoiceOnOnlySection: View {
|
||||
let title: String
|
||||
let items: [OriginalSeriesResponse]
|
||||
let onTapSeries: (Int) -> Void
|
||||
|
||||
var body: some View {
|
||||
if !items.isEmpty {
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s12) {
|
||||
SectionTitle(title: title)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(alignment: .top, spacing: SodaSpacing.s14) {
|
||||
ForEach(items) { item in
|
||||
Button {
|
||||
onTapSeries(item.seriesId)
|
||||
} label: {
|
||||
ZStack(alignment: .topLeading) {
|
||||
DownsampledKFImage(
|
||||
url: URL(string: item.coverImageUrl),
|
||||
size: CGSize(width: 122, height: 172)
|
||||
)
|
||||
.background(Color.gray800)
|
||||
|
||||
originalTag
|
||||
}
|
||||
.frame(width: 122, height: 172)
|
||||
.clipShape(RoundedRectangle(cornerRadius: SodaSpacing.s14, style: .continuous))
|
||||
}
|
||||
.buttonStyle(.plain)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, SodaSpacing.s20)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private var originalTag: some View {
|
||||
HStack(spacing: SodaSpacing.s4) {
|
||||
Image("ic_series_original")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(width: 14, height: 14)
|
||||
|
||||
Image("img_new_only")
|
||||
.resizable()
|
||||
.scaledToFit()
|
||||
.frame(height: 14)
|
||||
}
|
||||
.padding(.horizontal, SodaSpacing.s8)
|
||||
.padding(.vertical, 5)
|
||||
.background(Color.gray900)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
import SwiftUI
|
||||
|
||||
struct MainContentRecommendationView: View {
|
||||
let onTapContent: (Int) -> Void
|
||||
let onTapBanner: (AudioBannerResponse) -> Void
|
||||
let onTapSeries: (Int) -> Void
|
||||
|
||||
@StateObject private var viewModel = MainContentRecommendationViewModel()
|
||||
|
||||
var body: some View {
|
||||
ZStack {
|
||||
Color.black
|
||||
.ignoresSafeArea()
|
||||
|
||||
if viewModel.isLoading && viewModel.recommendations == nil {
|
||||
ProgressView()
|
||||
.progressViewStyle(CircularProgressViewStyle(tint: .white))
|
||||
} else if viewModel.shouldShowEmptyState {
|
||||
MainContentAudioEmptyStateView(message: viewModel.emptyStateMessage)
|
||||
} else {
|
||||
ScrollView(showsIndicators: false) {
|
||||
VStack(alignment: .leading, spacing: SodaSpacing.s24) {
|
||||
if let recommendations = viewModel.recommendations {
|
||||
MainContentAudioBannerSection(
|
||||
items: recommendations.banners,
|
||||
onTapBanner: onTapBanner
|
||||
)
|
||||
|
||||
MainContentAudioHorizontalCardSection(
|
||||
title: I18n.MainContentRecommendation.latestAudioSectionTitle,
|
||||
items: recommendations.latestAudios,
|
||||
onTapContent: onTapContent
|
||||
)
|
||||
|
||||
MainContentAudioListCarouselSection(
|
||||
title: I18n.MainContentRecommendation.newAndHotSectionTitle,
|
||||
items: recommendations.newAndHotAudios,
|
||||
onTapContent: onTapContent
|
||||
)
|
||||
|
||||
MainContentVoiceOnOnlySection(
|
||||
title: I18n.MainContentRecommendation.voiceOnOnlySectionTitle,
|
||||
items: recommendations.originalSeries,
|
||||
onTapSeries: onTapSeries
|
||||
)
|
||||
|
||||
MainContentAudioHorizontalCardSection(
|
||||
title: I18n.MainContentRecommendation.freeAudioSectionTitle,
|
||||
items: recommendations.freeAudios,
|
||||
onTapContent: onTapContent
|
||||
)
|
||||
|
||||
MainContentAudioHorizontalCardSection(
|
||||
title: I18n.MainContentRecommendation.pointAudioSectionTitle,
|
||||
items: recommendations.pointAudios,
|
||||
onTapContent: onTapContent
|
||||
)
|
||||
|
||||
MainContentMostCommentedAudioSection(
|
||||
title: I18n.MainContentRecommendation.mostCommentedAudioSectionTitle,
|
||||
items: recommendations.mostCommentedAudios,
|
||||
onTapContent: onTapContent
|
||||
)
|
||||
|
||||
MainContentRecommendedAudioGridSection(
|
||||
title: I18n.MainContentRecommendation.recommendedAudioSectionTitle,
|
||||
items: recommendations.recommendedAudios,
|
||||
onTapContent: onTapContent
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, SodaSpacing.s20)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if !viewModel.hasLoaded {
|
||||
viewModel.fetchRecommendations()
|
||||
}
|
||||
}
|
||||
.sodaToast(
|
||||
isPresented: $viewModel.isShowPopup,
|
||||
message: viewModel.errorMessage,
|
||||
autohideIn: 2
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,72 @@
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
final class MainContentRecommendationViewModel: ObservableObject {
|
||||
private let repository = MainContentRecommendationRepository()
|
||||
private var subscription = Set<AnyCancellable>()
|
||||
|
||||
@Published var isLoading = false
|
||||
@Published var isShowPopup = false
|
||||
@Published var errorMessage = ""
|
||||
@Published var recommendations: AudioRecommendationsResponse?
|
||||
@Published var hasLoaded = false
|
||||
|
||||
var emptyStateMessage: String {
|
||||
if !errorMessage.isEmpty {
|
||||
return errorMessage
|
||||
}
|
||||
|
||||
return I18n.MainContentRecommendation.emptyStateMessage
|
||||
}
|
||||
|
||||
var shouldShowEmptyState: Bool {
|
||||
hasLoaded && !isLoading && (recommendations?.isEmpty ?? true)
|
||||
}
|
||||
|
||||
func fetchRecommendations() {
|
||||
guard !isLoading else { return }
|
||||
|
||||
isLoading = true
|
||||
|
||||
repository.getAudioRecommendations()
|
||||
.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.errorMessage = I18n.MainContentRecommendation.loadFailedMessage
|
||||
self.isShowPopup = true
|
||||
self.isLoading = false
|
||||
self.hasLoaded = true
|
||||
}
|
||||
} receiveValue: { [weak self] response in
|
||||
guard let self else { return }
|
||||
|
||||
let responseData = response.data
|
||||
|
||||
do {
|
||||
let jsonDecoder = JSONDecoder()
|
||||
let decoded = try jsonDecoder.decode(ApiResponse<AudioRecommendationsResponse>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.errorMessage = ""
|
||||
self.recommendations = data
|
||||
} else {
|
||||
self.errorMessage = decoded.message ?? I18n.MainContentRecommendation.loadFailedMessage
|
||||
self.isShowPopup = true
|
||||
}
|
||||
} catch {
|
||||
ERROR_LOG(error.localizedDescription)
|
||||
self.errorMessage = I18n.MainContentRecommendation.loadFailedMessage
|
||||
self.isShowPopup = true
|
||||
}
|
||||
|
||||
self.isLoading = false
|
||||
self.hasLoaded = true
|
||||
}
|
||||
.store(in: &subscription)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
import Foundation
|
||||
|
||||
struct AudioRecommendationsResponse: Decodable {
|
||||
let banners: [AudioBannerResponse]
|
||||
let originalSeries: [OriginalSeriesResponse]
|
||||
let latestAudios: [AudioCardResponse]
|
||||
let newAndHotAudios: [AudioCardResponse]
|
||||
let freeAudios: [AudioCardResponse]
|
||||
let pointAudios: [AudioCardResponse]
|
||||
let mostCommentedAudios: [CommentedAudioResponse]
|
||||
let recommendedAudios: [AudioCardResponse]
|
||||
|
||||
var isEmpty: Bool {
|
||||
banners.isEmpty &&
|
||||
originalSeries.isEmpty &&
|
||||
latestAudios.isEmpty &&
|
||||
newAndHotAudios.isEmpty &&
|
||||
freeAudios.isEmpty &&
|
||||
pointAudios.isEmpty &&
|
||||
mostCommentedAudios.isEmpty &&
|
||||
recommendedAudios.isEmpty
|
||||
}
|
||||
}
|
||||
|
||||
struct AudioBannerResponse: Decodable, Hashable, Identifiable {
|
||||
var id: String { "\(imageUrl)-\(creatorId ?? 0)-\(seriesId ?? 0)-\(link ?? "")" }
|
||||
|
||||
let imageUrl: String
|
||||
let eventItem: EventItem?
|
||||
let creatorId: Int?
|
||||
let seriesId: Int?
|
||||
let link: String?
|
||||
}
|
||||
|
||||
struct OriginalSeriesResponse: Decodable, Hashable, Identifiable {
|
||||
var id: Int { seriesId }
|
||||
|
||||
let seriesId: Int
|
||||
let coverImageUrl: String
|
||||
}
|
||||
|
||||
struct AudioCardResponse: Decodable, Hashable, Identifiable {
|
||||
var id: Int { audioContentId }
|
||||
|
||||
let audioContentId: Int
|
||||
let title: String
|
||||
let duration: String
|
||||
let imageUrl: String
|
||||
let price: Int
|
||||
let isAdult: Bool
|
||||
let isPointAvailable: Bool
|
||||
let isFirstContent: Bool
|
||||
let isOriginalSeries: Bool
|
||||
let creatorNickname: String
|
||||
}
|
||||
|
||||
struct CommentedAudioResponse: Decodable, Hashable, Identifiable {
|
||||
var id: Int { audioContentId }
|
||||
|
||||
let audioContentId: Int
|
||||
let title: String
|
||||
let imageUrl: String
|
||||
let latestComment: String
|
||||
let latestCommentWriterProfileImageUrl: String
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
import Foundation
|
||||
import Moya
|
||||
|
||||
enum MainContentRecommendationApi {
|
||||
case getAudioRecommendations
|
||||
}
|
||||
|
||||
extension MainContentRecommendationApi: TargetType {
|
||||
var baseURL: URL {
|
||||
return URL(string: BASE_URL)!
|
||||
}
|
||||
|
||||
var path: String {
|
||||
switch self {
|
||||
case .getAudioRecommendations:
|
||||
return "/api/v2/audio/recommendations"
|
||||
}
|
||||
}
|
||||
|
||||
var method: Moya.Method {
|
||||
switch self {
|
||||
case .getAudioRecommendations:
|
||||
return .get
|
||||
}
|
||||
}
|
||||
|
||||
var task: Moya.Task {
|
||||
switch self {
|
||||
case .getAudioRecommendations:
|
||||
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 MainContentRecommendationRepository {
|
||||
private let api = MoyaProvider<MainContentRecommendationApi>()
|
||||
|
||||
func getAudioRecommendations() -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(.getAudioRecommendations)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user