내 보관함, 재생목록 리스트 UI 추가

This commit is contained in:
Yu Sung
2024-12-07 05:32:05 +09:00
parent 627d6d9b7e
commit abc4a4f39d
14 changed files with 490 additions and 81 deletions

View File

@@ -0,0 +1,64 @@
//
// ContentPlaylistItemView.swift
// SodaLive
//
// Created by klaus on 12/7/24.
//
import SwiftUI
import Kingfisher
struct ContentPlaylistItemView: View {
let item: GetPlaylistsItem
var body: some View {
VStack(alignment: .leading, spacing: 8) {
HStack(spacing: 11) {
KFImage(URL(string: item.coverImageUrl))
.cancelOnDisappear(true)
.downsampling(size: CGSize(width: 66.7, height: 66.7))
.resizable()
.scaledToFill()
.frame(width: 66.7, height: 66.7, alignment: .center)
.cornerRadius(5.3)
.clipped()
VStack(alignment: .leading, spacing: 7) {
Text(item.title)
.font(.custom(Font.bold.rawValue, size: 14.7))
.foregroundColor(Color.grayd2)
.lineLimit(1)
if !item.desc.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
Text(item.desc)
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color.gray90)
.lineLimit(1)
}
Text("\(item.contentCount)")
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color.gray90)
.lineLimit(1)
}
}
Rectangle()
.frame(height: 1)
.foregroundColor(Color.gray55)
}
}
}
#Preview {
ContentPlaylistItemView(
item: GetPlaylistsItem(
id: 1,
title: "토스트",
desc: "테슬라 네",
contentCount: 2,
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
)
)
}

View File

@@ -0,0 +1,18 @@
//
// ContentPlaylistListRepository.swift
// SodaLive
//
// Created by klaus on 12/7/24.
//
import CombineMoya
import Combine
import Moya
class ContentPlaylistListRepository {
private let api = MoyaProvider<PlaylistApi>()
func getPlaylistList() -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.getPlaylistList)
}
}

View File

@@ -0,0 +1,71 @@
//
// ContentPlaylistListView.swift
// SodaLive
//
// Created by klaus on 12/7/24.
//
import SwiftUI
struct ContentPlaylistListView: View {
@ObservedObject var viewModel = ContentPlaylistListViewModel()
var body: some View {
BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 13.3) {
Text("+ 새 재생목록 만들기")
.font(.custom(Font.bold.rawValue, size: 14.7))
.foregroundColor(Color.white)
.padding(.vertical, 13.3)
.frame(maxWidth: .infinity)
.background(Color.button)
.cornerRadius(5.3)
.onTapGesture {
}
if viewModel.playlists.isEmpty {
VStack(spacing: 13.3) {
Text("재생목록이 비어있습니다.")
.font(.custom(Font.bold.rawValue, size: 14.7))
.foregroundColor(Color.grayee)
Text("자주 듣는 콘텐츠를\n재생목록으로 만들어 보세요.")
.font(.custom(Font.medium.rawValue, size: 11))
.foregroundColor(Color.grayee)
.multilineTextAlignment(.center)
}
.frame(maxWidth: .infinity, maxHeight: .infinity)
.background(Color.bg)
.cornerRadius(4.7)
} else {
HStack(spacing: 5.3) {
Text("전체")
.font(.custom(Font.medium.rawValue, size: 14.7))
.foregroundColor(Color.white)
Text("\(viewModel.totalCount)")
.font(.custom(Font.medium.rawValue, size: 12))
.foregroundColor(Color.gray90)
Spacer()
}
.frame(maxWidth: .infinity)
ForEach(0..<viewModel.playlists.count, id: \.self) { index in
let playlist = viewModel.playlists[index]
ContentPlaylistItemView(item: playlist)
}
}
}
.onAppear {
viewModel.getPlaylistList()
}
}
}
}
#Preview {
ContentPlaylistListView()
}

View File

@@ -0,0 +1,62 @@
//
// ContentPlaylistListViewModel.swift
// SodaLive
//
// Created by klaus on 12/7/24.
//
import Foundation
import Combine
final class ContentPlaylistListViewModel: ObservableObject {
private let repository = ContentPlaylistListRepository()
private var subscription = Set<AnyCancellable>()
@Published var isLoading = false
@Published var errorMessage = ""
@Published var isShowPopup = false
@Published var totalCount = 0
@Published var playlists = [GetPlaylistsItem]()
func getPlaylistList() {
if !isLoading {
isLoading = true
repository.getPlaylistList()
.sink { result in
switch result {
case .finished:
DEBUG_LOG("finish")
case .failure(let error):
ERROR_LOG(error.localizedDescription)
}
} receiveValue: { [unowned self] response in
let responseData = response.data
do {
let jsonDecoder = JSONDecoder()
let decoded = try jsonDecoder.decode(ApiResponse<GetPlaylistsResponse>.self, from: responseData)
if let data = decoded.data, decoded.success {
self.totalCount = data.totalCount
self.playlists.append(contentsOf: data.items)
} else {
if let message = decoded.message {
self.errorMessage = message
} else {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
}
self.isShowPopup = true
}
} catch {
self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다."
self.isShowPopup = true
}
self.isLoading = false
}
.store(in: &subscription)
}
}
}

View File

@@ -0,0 +1,19 @@
//
// GetPlaylistsResponse.swift
// SodaLive
//
// Created by klaus on 12/7/24.
//
struct GetPlaylistsResponse: Decodable {
let totalCount: Int
let items: [GetPlaylistsItem]
}
struct GetPlaylistsItem: Decodable {
let id: Int
let title: String
let desc: String
let contentCount: Int
let coverImageUrl: String
}

View File

@@ -0,0 +1,44 @@
//
// PlaylistApi.swift
// SodaLive
//
// Created by klaus on 12/7/24.
//
import Foundation
import Moya
enum PlaylistApi {
case getPlaylistList
}
extension PlaylistApi: TargetType {
var baseURL: URL {
return URL(string: BASE_URL)!
}
var path: String {
switch self {
case .getPlaylistList:
return "/audio-content/playlist"
}
}
var method: Moya.Method {
switch self {
case .getPlaylistList:
return .get
}
}
var task: Moya.Task {
switch self {
case .getPlaylistList:
return .requestPlain
}
}
var headers: [String : String]? {
return ["Authorization": "Bearer \(UserDefaults.string(forKey: UserDefaultsKey.token))"]
}
}