내 보관함, 재생목록 리스트 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

@ -133,4 +133,6 @@ enum AppStep {
case tempCanPayment(orderType: OrderType, contentId: Int, title: String, can: Int)
case blockList
case myBox
}

View File

@ -0,0 +1,31 @@
//
// ContentBoxTabView.swift
// SodaLive
//
// Created by klaus on 12/7/24.
//
import SwiftUI
struct ContentBoxTabView: View {
let title: String
let isSelected: Bool
var body: some View {
Text(title)
.font(.custom(Font.medium.rawValue, size: 14.7))
.foregroundColor(isSelected ? Color.button : Color.gray77)
.padding(.vertical, 8.3)
.padding(.horizontal, 13.3)
.overlay(
RoundedRectangle(cornerRadius: 26.7)
.strokeBorder(lineWidth: 1)
.foregroundColor(isSelected ? Color.button : Color.gray77)
)
}
}
#Preview {
ContentBoxTabView(title: "재생목록", isSelected: false)
}

View File

@ -0,0 +1,58 @@
//
// ContentBoxView.swift
// SodaLive
//
// Created by klaus on 12/7/24.
//
import SwiftUI
struct ContentBoxView: View {
@StateObject var viewModel = ContentBoxViewModel()
var body: some View {
NavigationView {
VStack(spacing: 13.3) {
DetailNavigationBar(title: "내 보관함")
ScrollView(.horizontal, showsIndicators: false) {
HStack(spacing: 12) {
ContentBoxTabView(
title: "구매목록",
isSelected: viewModel.currentTab == .orderlist
)
.onTapGesture {
if viewModel.currentTab != .orderlist {
viewModel.currentTab = .orderlist
}
}
ContentBoxTabView(
title: "재생목록",
isSelected: viewModel.currentTab == .playlist
)
.onTapGesture {
if viewModel.currentTab != .playlist {
viewModel.currentTab = .playlist
}
}
}
.padding(.horizontal, 13.3)
}
if viewModel.currentTab == .playlist {
ContentPlaylistListView()
.padding(.bottom, 13.3)
.padding(.horizontal, 13.3)
} else {
OrderListAllInnerView()
}
}
}
}
}
#Preview {
ContentBoxView()
}

View File

@ -0,0 +1,17 @@
//
// ContentBoxViewModel.swift
// SodaLive
//
// Created by klaus on 12/7/24.
//
import Foundation
import Combine
final class ContentBoxViewModel: ObservableObject {
enum CurrentTab: String {
case playlist, orderlist
}
@Published var currentTab: CurrentTab = .orderlist
}

View File

@ -26,7 +26,7 @@ struct ContentMainView: View {
Image("ic_content_keep")
.onTapGesture {
AppState.shared.setAppStep(step: .orderListAll)
AppState.shared.setAppStep(step: .myBox)
}
}
.padding(.bottom, 26.7)

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))"]
}
}

View File

@ -200,6 +200,9 @@ struct ContentView: View {
case .blockList:
BlockMemberListView()
case .myBox:
ContentBoxView()
default:
EmptyView()
.frame(width: 0, height: 0, alignment: .topLeading)

View File

@ -0,0 +1,84 @@
//
// OrderListAllInnerView.swift
// SodaLive
//
// Created by klaus on 12/7/24.
//
import SwiftUI
struct OrderListAllInnerView: View {
@StateObject var viewModel = OrderListAllViewModel()
var body: some View {
BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 0) {
HStack(spacing: 0) {
Text("")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee"))
Text("\(viewModel.totalCount)")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "dd4500"))
Text("")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee"))
Spacer()
}
.padding(.horizontal, 13.3)
.padding(.top, 13.3)
ScrollViewReader { reader in
ScrollView(.vertical, showsIndicators: false) {
LazyVStack(spacing: 10.7) {
ScrollerToTop(reader: reader, scrollOnChange: $viewModel.scrollToTop)
ForEach(0..<viewModel.orderList.count, id: \.self) { index in
let item = viewModel.orderList[index]
NavigationLink {
ContentDetailView(contentId: item.contentId)
} label: {
OrderListItemView(item: item)
.contentShape(Rectangle())
.padding(.horizontal, 13.3)
.padding(.top, 6.7)
.onAppear {
if index == viewModel.orderList.count - 1 {
viewModel.getOrderList()
}
}
}
}
}
}
}
.padding(.top, 13.3)
}
.onAppear {
viewModel.getOrderList()
}
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12))
.background(Color(hex: "3bb9f1"))
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)
.padding(.bottom, 66.7)
Spacer()
}
}
}
}
}
#Preview {
OrderListAllInnerView()
}

View File

@ -9,93 +9,29 @@ import SwiftUI
struct OrderListAllView: View {
@StateObject var viewModel = OrderListAllViewModel()
var body: some View {
NavigationView {
BaseView(isLoading: $viewModel.isLoading) {
VStack(spacing: 0) {
HStack(spacing: 0) {
Button {
AppState.shared.back()
} label: {
Image("ic_back")
.resizable()
.frame(width: 20, height: 20)
Text("콘텐츠 보관함")
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color(hex: "eeeeee"))
}
VStack(spacing: 0) {
HStack(spacing: 0) {
Button {
AppState.shared.back()
} label: {
Image("ic_back")
.resizable()
.frame(width: 20, height: 20)
Spacer()
}
.padding(.horizontal, 13.3)
.frame(height: 50)
.background(Color.black)
HStack(spacing: 0) {
Text("")
.font(.custom(Font.medium.rawValue, size: 13.3))
Text("콘텐츠 보관함")
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color(hex: "eeeeee"))
Text("\(viewModel.totalCount)")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "dd4500"))
Text("")
.font(.custom(Font.medium.rawValue, size: 13.3))
.foregroundColor(Color(hex: "eeeeee"))
Spacer()
}
.padding(.horizontal, 13.3)
.padding(.top, 13.3)
ScrollViewReader { reader in
ScrollView(.vertical, showsIndicators: false) {
LazyVStack(spacing: 10.7) {
ScrollerToTop(reader: reader, scrollOnChange: $viewModel.scrollToTop)
ForEach(0..<viewModel.orderList.count, id: \.self) { index in
let item = viewModel.orderList[index]
NavigationLink {
ContentDetailView(contentId: item.contentId)
} label: {
OrderListItemView(item: item)
.contentShape(Rectangle())
.padding(.horizontal, 13.3)
.padding(.top, 6.7)
.onAppear {
if index == viewModel.orderList.count - 1 {
viewModel.getOrderList()
}
}
}
}
}
}
}
.padding(.top, 13.3)
}
.onAppear {
viewModel.getOrderList()
}
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12))
.background(Color(hex: "3bb9f1"))
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)
.padding(.bottom, 66.7)
Spacer()
}
Spacer()
}
.padding(.horizontal, 13.3)
.frame(height: 50)
.background(Color.black)
OrderListAllInnerView()
}
}
}