재생목록 만들기 페이지 추가
This commit is contained in:
parent
abc4a4f39d
commit
0fdb9edd23
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ic_check_blue.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
SodaLive/Resources/Assets.xcassets/ic_check_blue.imageset/ic_check_blue.png
vendored
Normal file
BIN
SodaLive/Resources/Assets.xcassets/ic_check_blue.imageset/ic_check_blue.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 631 B |
|
@ -0,0 +1,21 @@
|
||||||
|
{
|
||||||
|
"images" : [
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "1x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "2x"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"filename" : "ic_playlist_add.png",
|
||||||
|
"idiom" : "universal",
|
||||||
|
"scale" : "3x"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"info" : {
|
||||||
|
"author" : "xcode",
|
||||||
|
"version" : 1
|
||||||
|
}
|
||||||
|
}
|
BIN
SodaLive/Resources/Assets.xcassets/ic_playlist_add.imageset/ic_playlist_add.png
vendored
Normal file
BIN
SodaLive/Resources/Assets.xcassets/ic_playlist_add.imageset/ic_playlist_add.png
vendored
Normal file
Binary file not shown.
After Width: | Height: | Size: 962 B |
|
@ -11,44 +11,67 @@ struct ContentBoxView: View {
|
||||||
|
|
||||||
@StateObject var viewModel = ContentBoxViewModel()
|
@StateObject var viewModel = ContentBoxViewModel()
|
||||||
|
|
||||||
|
@State private var isShowCreatePlaylist = false
|
||||||
|
@State private var isReloadData = false
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
NavigationView {
|
ZStack {
|
||||||
VStack(spacing: 13.3) {
|
NavigationView {
|
||||||
DetailNavigationBar(title: "내 보관함")
|
VStack(spacing: 13.3) {
|
||||||
|
DetailNavigationBar(title: "내 보관함")
|
||||||
|
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
ScrollView(.horizontal, showsIndicators: false) {
|
||||||
HStack(spacing: 12) {
|
HStack(spacing: 12) {
|
||||||
ContentBoxTabView(
|
ContentBoxTabView(
|
||||||
title: "구매목록",
|
title: "구매목록",
|
||||||
isSelected: viewModel.currentTab == .orderlist
|
isSelected: viewModel.currentTab == .orderlist
|
||||||
)
|
)
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
if viewModel.currentTab != .orderlist {
|
if viewModel.currentTab != .orderlist {
|
||||||
viewModel.currentTab = .orderlist
|
viewModel.currentTab = .orderlist
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ContentBoxTabView(
|
||||||
|
title: "재생목록",
|
||||||
|
isSelected: viewModel.currentTab == .playlist
|
||||||
|
)
|
||||||
|
.onTapGesture {
|
||||||
|
if viewModel.currentTab != .playlist {
|
||||||
|
viewModel.currentTab = .playlist
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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)
|
.padding(.horizontal, 13.3)
|
||||||
} else {
|
}
|
||||||
OrderListAllInnerView()
|
|
||||||
|
if viewModel.currentTab == .playlist {
|
||||||
|
if isReloadData {
|
||||||
|
Color.clear
|
||||||
|
.onAppear {
|
||||||
|
DispatchQueue.main.asyncAfter(deadline: .now() + 0.3) {
|
||||||
|
isReloadData = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
ContentPlaylistListView(
|
||||||
|
onClickCreate: { isShowCreatePlaylist = true }
|
||||||
|
)
|
||||||
|
.padding(.bottom, 13.3)
|
||||||
|
.padding(.horizontal, 13.3)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
OrderListAllInnerView()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if isShowCreatePlaylist {
|
||||||
|
ContentPlaylistCreateView(
|
||||||
|
isShowing: $isShowCreatePlaylist,
|
||||||
|
reloadData: $isReloadData
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -14,7 +14,7 @@ enum ContentApi {
|
||||||
case likeContent(request: PutAudioContentLikeRequest)
|
case likeContent(request: PutAudioContentLikeRequest)
|
||||||
case registerComment(request: RegisterAudioContentCommentRequest)
|
case registerComment(request: RegisterAudioContentCommentRequest)
|
||||||
case orderAudioContent(request: OrderRequest)
|
case orderAudioContent(request: OrderRequest)
|
||||||
case getOrderList(page: Int, size: Int)
|
case getOrderList(orderType: OrderType?, page: Int, size: Int)
|
||||||
case addAllPlaybackTracking(request: AddAllPlaybackTrackingRequest)
|
case addAllPlaybackTracking(request: AddAllPlaybackTrackingRequest)
|
||||||
case getAudioContentThemeList
|
case getAudioContentThemeList
|
||||||
case uploadAudioContent(parameters: [MultipartFormData])
|
case uploadAudioContent(parameters: [MultipartFormData])
|
||||||
|
@ -180,12 +180,16 @@ extension ContentApi: TargetType {
|
||||||
case .orderAudioContent(let request):
|
case .orderAudioContent(let request):
|
||||||
return .requestJSONEncodable(request)
|
return .requestJSONEncodable(request)
|
||||||
|
|
||||||
case .getOrderList(let page, let size):
|
case .getOrderList(let orderType, let page, let size):
|
||||||
let parameters = [
|
var parameters = [
|
||||||
"page": page - 1,
|
"page": page - 1,
|
||||||
"size": size
|
"size": size
|
||||||
] as [String : Any]
|
] as [String : Any]
|
||||||
|
|
||||||
|
if let orderType = orderType {
|
||||||
|
parameters["orderType"] = orderType
|
||||||
|
}
|
||||||
|
|
||||||
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
|
||||||
|
|
||||||
case .addAllPlaybackTracking(let request):
|
case .addAllPlaybackTracking(let request):
|
||||||
|
|
|
@ -34,8 +34,8 @@ final class ContentRepository {
|
||||||
return api.requestPublisher(.orderAudioContent(request: OrderRequest(contentId: contentId, orderType: orderType)))
|
return api.requestPublisher(.orderAudioContent(request: OrderRequest(contentId: contentId, orderType: orderType)))
|
||||||
}
|
}
|
||||||
|
|
||||||
func getOrderList(page: Int, size: Int) -> AnyPublisher<Response, MoyaError> {
|
func getOrderList(orderType: OrderType?, page: Int, size: Int) -> AnyPublisher<Response, MoyaError> {
|
||||||
return api.requestPublisher(.getOrderList(page: page, size: size))
|
return api.requestPublisher(.getOrderList(orderType: orderType, page: page, size: size))
|
||||||
}
|
}
|
||||||
|
|
||||||
func addAllPlaybackTracking(request: AddAllPlaybackTrackingRequest) -> AnyPublisher<Response, MoyaError> {
|
func addAllPlaybackTracking(request: AddAllPlaybackTrackingRequest) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
|
|
@ -15,4 +15,8 @@ class ContentPlaylistListRepository {
|
||||||
func getPlaylistList() -> AnyPublisher<Response, MoyaError> {
|
func getPlaylistList() -> AnyPublisher<Response, MoyaError> {
|
||||||
return api.requestPublisher(.getPlaylistList)
|
return api.requestPublisher(.getPlaylistList)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func createPlaylist(request: CreatePlaylistRequest) -> AnyPublisher<Response, MoyaError> {
|
||||||
|
return api.requestPublisher(.createPlaylist(request: request))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -11,6 +11,8 @@ struct ContentPlaylistListView: View {
|
||||||
|
|
||||||
@ObservedObject var viewModel = ContentPlaylistListViewModel()
|
@ObservedObject var viewModel = ContentPlaylistListViewModel()
|
||||||
|
|
||||||
|
let onClickCreate: () -> Void
|
||||||
|
|
||||||
var body: some View {
|
var body: some View {
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
BaseView(isLoading: $viewModel.isLoading) {
|
||||||
VStack(spacing: 13.3) {
|
VStack(spacing: 13.3) {
|
||||||
|
@ -21,7 +23,9 @@ struct ContentPlaylistListView: View {
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
.background(Color.button)
|
.background(Color.button)
|
||||||
.cornerRadius(5.3)
|
.cornerRadius(5.3)
|
||||||
|
.contentShape(Rectangle())
|
||||||
.onTapGesture {
|
.onTapGesture {
|
||||||
|
onClickCreate()
|
||||||
}
|
}
|
||||||
|
|
||||||
if viewModel.playlists.isEmpty {
|
if viewModel.playlists.isEmpty {
|
||||||
|
@ -52,13 +56,31 @@ struct ContentPlaylistListView: View {
|
||||||
}
|
}
|
||||||
.frame(maxWidth: .infinity)
|
.frame(maxWidth: .infinity)
|
||||||
|
|
||||||
ForEach(0..<viewModel.playlists.count, id: \.self) { index in
|
ScrollView(.vertical) {
|
||||||
let playlist = viewModel.playlists[index]
|
LazyVStack(spacing: 11) {
|
||||||
|
ForEach(0..<viewModel.playlists.count, id: \.self) { index in
|
||||||
ContentPlaylistItemView(item: playlist)
|
let playlist = viewModel.playlists[index]
|
||||||
|
ContentPlaylistItemView(item: playlist)
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
.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.button)
|
||||||
|
.foregroundColor(Color.white)
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
.cornerRadius(20)
|
||||||
|
.padding(.bottom, 66.7)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
.onAppear {
|
.onAppear {
|
||||||
viewModel.getPlaylistList()
|
viewModel.getPlaylistList()
|
||||||
}
|
}
|
||||||
|
@ -67,5 +89,5 @@ struct ContentPlaylistListView: View {
|
||||||
}
|
}
|
||||||
|
|
||||||
#Preview {
|
#Preview {
|
||||||
ContentPlaylistListView()
|
ContentPlaylistListView(onClickCreate: {})
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,172 @@
|
||||||
|
//
|
||||||
|
// ContentPlaylistCreateView.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 12/7/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct ContentPlaylistCreateView: View {
|
||||||
|
@StateObject var viewModel = ContentPlaylistCreateViewModel()
|
||||||
|
|
||||||
|
@Binding var isShowing: Bool
|
||||||
|
@Binding var reloadData: Bool
|
||||||
|
|
||||||
|
@State private var isShowAddContentView = false
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
BaseView(isLoading: $viewModel.isLoading) {
|
||||||
|
VStack(alignment: .leading, spacing: 0) {
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Button {
|
||||||
|
isShowing = false
|
||||||
|
} label: {
|
||||||
|
Image("ic_back")
|
||||||
|
.resizable()
|
||||||
|
.frame(width: 20, height: 20)
|
||||||
|
|
||||||
|
Text("새 재생목록 만들기")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 18.3))
|
||||||
|
.foregroundColor(Color.grayee)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Text("저장")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 14.7))
|
||||||
|
.foregroundColor(Color.grayee)
|
||||||
|
.frame(minHeight: 48)
|
||||||
|
.onTapGesture {
|
||||||
|
viewModel.savePlaylist {
|
||||||
|
reloadData = true
|
||||||
|
isShowing = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(height: 50)
|
||||||
|
.padding(.horizontal, 13.3)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.background(Color.black)
|
||||||
|
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Text("재생목록 제목")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 16.7))
|
||||||
|
.foregroundColor(Color.grayee)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Text("\(viewModel.title.count)/30")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(Color.gray77)
|
||||||
|
.onChange(of: viewModel.title) { newValue in
|
||||||
|
if newValue.count > 30 {
|
||||||
|
viewModel.title = String(newValue.prefix(30))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.top, 26.7)
|
||||||
|
.padding(.horizontal, 13.3)
|
||||||
|
|
||||||
|
TextField("", text: $viewModel.title)
|
||||||
|
.autocapitalization(.none)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(Color.grayee)
|
||||||
|
.keyboardType(.webSearch)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.horizontal, 13.3)
|
||||||
|
.padding(.vertical, 17)
|
||||||
|
.background(Color.gray22)
|
||||||
|
.cornerRadius(6.7)
|
||||||
|
.padding(.top, 13.3)
|
||||||
|
.padding(.horizontal, 13.3)
|
||||||
|
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Text("재생목록 설명을 입력해 주세요")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 16.7))
|
||||||
|
.foregroundColor(Color.grayee)
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Text("\(viewModel.desc.count)/40")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(Color.gray77)
|
||||||
|
.onChange(of: viewModel.desc) { newValue in
|
||||||
|
if newValue.count > 40 {
|
||||||
|
viewModel.desc = String(newValue.prefix(40))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.top, 26.7)
|
||||||
|
.padding(.horizontal, 13.3)
|
||||||
|
|
||||||
|
TextField("", text: $viewModel.desc)
|
||||||
|
.autocapitalization(.none)
|
||||||
|
.disableAutocorrection(true)
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 13.3))
|
||||||
|
.foregroundColor(Color.grayee)
|
||||||
|
.keyboardType(.webSearch)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.padding(.horizontal, 13.3)
|
||||||
|
.padding(.vertical, 17)
|
||||||
|
.background(Color.gray22)
|
||||||
|
.cornerRadius(6.7)
|
||||||
|
.padding(.top, 13.3)
|
||||||
|
.padding(.horizontal, 13.3)
|
||||||
|
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Image("btn_plus_round")
|
||||||
|
|
||||||
|
Text("새로운 콘텐츠 추가/제거")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 14.7))
|
||||||
|
.foregroundColor(Color.button)
|
||||||
|
}
|
||||||
|
.padding(.top, 26.7)
|
||||||
|
.padding(.horizontal, 13.3)
|
||||||
|
.onTapGesture {
|
||||||
|
isShowAddContentView = true
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
|
LazyVStack(alignment: .leading, spacing: 13.3) {
|
||||||
|
ForEach(0..<viewModel.contentList.count, id: \.self) { index in
|
||||||
|
PlaylistCreateContentView(content: viewModel.contentList[index])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 13.3)
|
||||||
|
}
|
||||||
|
.padding(.vertical, 13.3)
|
||||||
|
}
|
||||||
|
.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.button)
|
||||||
|
.foregroundColor(Color.white)
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
.cornerRadius(20)
|
||||||
|
.padding(.bottom, 66.7)
|
||||||
|
Spacer()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if isShowAddContentView {
|
||||||
|
PlaylistAddContentView(
|
||||||
|
isShowing: $isShowAddContentView,
|
||||||
|
contentList: $viewModel.contentList
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
ContentPlaylistCreateView(
|
||||||
|
isShowing: .constant(true),
|
||||||
|
reloadData: .constant(false)
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,90 @@
|
||||||
|
//
|
||||||
|
// ContentPlaylistCreateViewModel.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 12/7/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
import Combine
|
||||||
|
|
||||||
|
final class ContentPlaylistCreateViewModel: ObservableObject {
|
||||||
|
private let repository = ContentPlaylistListRepository()
|
||||||
|
private var subscription = Set<AnyCancellable>()
|
||||||
|
|
||||||
|
@Published var isLoading = false
|
||||||
|
@Published var errorMessage = ""
|
||||||
|
@Published var isShowPopup = false
|
||||||
|
|
||||||
|
@Published var title: String = ""
|
||||||
|
@Published var desc: String = ""
|
||||||
|
var contentList = [AudioContentPlaylistContent]()
|
||||||
|
|
||||||
|
func savePlaylist(onSuccess: @escaping () -> Void) {
|
||||||
|
if (validate()) {
|
||||||
|
isLoading = true
|
||||||
|
|
||||||
|
let contentIdAndOrderList = contentList.mapIndexed { index, item in
|
||||||
|
PlaylistContentIdAndOrder(contentId: item.id, order: index + 1)
|
||||||
|
}
|
||||||
|
|
||||||
|
repository.createPlaylist(
|
||||||
|
request: CreatePlaylistRequest(
|
||||||
|
title: title,
|
||||||
|
desc: desc,
|
||||||
|
contentIdAndOrderList: contentIdAndOrderList
|
||||||
|
)
|
||||||
|
)
|
||||||
|
.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(ApiResponseWithoutData.self, from: responseData)
|
||||||
|
self.isLoading = false
|
||||||
|
|
||||||
|
if decoded.success {
|
||||||
|
onSuccess()
|
||||||
|
} 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private func validate() -> Bool {
|
||||||
|
if (title.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty || title.count < 3) {
|
||||||
|
errorMessage = "제목을 3자 이상 입력하세요"
|
||||||
|
isShowPopup = true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
if (contentList.isEmpty) {
|
||||||
|
errorMessage = "콘텐츠를 1개 이상 추가하세요"
|
||||||
|
isShowPopup = true
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
|
@ -0,0 +1,12 @@
|
||||||
|
//
|
||||||
|
// CreatePlaylistRequest.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 12/8/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
struct CreatePlaylistRequest: Encodable {
|
||||||
|
let title: String
|
||||||
|
let desc: String?
|
||||||
|
let contentIdAndOrderList: [PlaylistContentIdAndOrder]
|
||||||
|
}
|
|
@ -0,0 +1,89 @@
|
||||||
|
//
|
||||||
|
// PlaylistAddContentItemView.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 12/9/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Kingfisher
|
||||||
|
|
||||||
|
struct PlaylistAddContentItemView: View {
|
||||||
|
|
||||||
|
let item: GetAudioContentOrderListItem
|
||||||
|
let onClick: () -> Void
|
||||||
|
@State private var isChecked: Bool
|
||||||
|
|
||||||
|
init(item: GetAudioContentOrderListItem, onClick: @escaping () -> Void, isChecked: Bool) {
|
||||||
|
self.item = item
|
||||||
|
self.onClick = onClick
|
||||||
|
self._isChecked = State(initialValue: isChecked)
|
||||||
|
}
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 13.3) {
|
||||||
|
KFImage(URL(string: item.coverImageUrl))
|
||||||
|
.cancelOnDisappear(true)
|
||||||
|
.downsampling(size: CGSize(width: 40, height: 40))
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(width: 40, height: 40, alignment: .center)
|
||||||
|
.cornerRadius(5.3)
|
||||||
|
.clipped()
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 2.6) {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Text(item.themeStr)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 10))
|
||||||
|
.foregroundColor(Color(hex: "3bac6a"))
|
||||||
|
.padding(2.6)
|
||||||
|
.background(Color(hex: "28312b"))
|
||||||
|
.cornerRadius(2.6)
|
||||||
|
|
||||||
|
if let duration = item.duration {
|
||||||
|
Text(duration)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 10))
|
||||||
|
.foregroundColor(Color.gray77)
|
||||||
|
.padding(2.6)
|
||||||
|
.background(Color.gray22)
|
||||||
|
.cornerRadius(2.6)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(item.title)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 12))
|
||||||
|
.foregroundColor(Color.grayd2)
|
||||||
|
.lineLimit(2)
|
||||||
|
.truncationMode(.tail)
|
||||||
|
}
|
||||||
|
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Image(isChecked ? "ic_check_blue" : "ic_playlist_add")
|
||||||
|
.padding(8)
|
||||||
|
.onTapGesture {
|
||||||
|
onClick()
|
||||||
|
isChecked.toggle()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
PlaylistAddContentItemView(
|
||||||
|
item: GetAudioContentOrderListItem(
|
||||||
|
contentId: 1,
|
||||||
|
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||||
|
creatorNickname: "유저1",
|
||||||
|
title: "안녕하세요 오늘은 커버곡을 들려드릴께요....안녕하세요 오늘은 커버곡을 들려드릴께요....",
|
||||||
|
themeStr: "커버곡",
|
||||||
|
duration: "00:30:20",
|
||||||
|
isAdult: false,
|
||||||
|
orderType: .KEEP,
|
||||||
|
likeCount: 0,
|
||||||
|
commentCount: 0
|
||||||
|
),
|
||||||
|
onClick: {},
|
||||||
|
isChecked: true
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,128 @@
|
||||||
|
//
|
||||||
|
// PlaylistAddContentView.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 12/8/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
|
||||||
|
struct PlaylistAddContentView: View {
|
||||||
|
|
||||||
|
@StateObject var viewModel = OrderListAllViewModel()
|
||||||
|
|
||||||
|
@Binding var isShowing: Bool
|
||||||
|
@Binding var contentList: [AudioContentPlaylistContent]
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
VStack(alignment: .leading, spacing: 13.3) {
|
||||||
|
ZStack {
|
||||||
|
Text("새로운 콘텐츠 추가/제거")
|
||||||
|
.font(.custom(Font.bold.rawValue, size: 18.3))
|
||||||
|
.foregroundColor(Color.grayee)
|
||||||
|
|
||||||
|
HStack(spacing: 0) {
|
||||||
|
Spacer()
|
||||||
|
|
||||||
|
Text("닫기")
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 14.7))
|
||||||
|
.foregroundColor(Color.grayee)
|
||||||
|
.frame(minHeight: 48)
|
||||||
|
.onTapGesture { isShowing = false }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.frame(height: 50)
|
||||||
|
.frame(maxWidth: .infinity)
|
||||||
|
.background(Color.black)
|
||||||
|
|
||||||
|
HStack(alignment: .center, 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
ScrollView(.vertical, showsIndicators: false) {
|
||||||
|
LazyVStack(spacing: 10.7) {
|
||||||
|
ForEach(0..<viewModel.orderList.count, id: \.self) { index in
|
||||||
|
let item = viewModel.orderList[index]
|
||||||
|
PlaylistAddContentItemView(
|
||||||
|
item: item,
|
||||||
|
onClick: {
|
||||||
|
let isChecked = contentList.contains(where: {
|
||||||
|
$0.id == item.contentId
|
||||||
|
})
|
||||||
|
|
||||||
|
if isChecked {
|
||||||
|
contentList.removeAll(where: {
|
||||||
|
$0.id == item.contentId
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
contentList.append(
|
||||||
|
AudioContentPlaylistContent(
|
||||||
|
id: item.contentId,
|
||||||
|
title: item.title,
|
||||||
|
category: item.themeStr,
|
||||||
|
coverUrl: item.coverImageUrl,
|
||||||
|
duration: item.duration ?? "00:00:00",
|
||||||
|
creatorNickname: item.creatorNickname,
|
||||||
|
creatorProfileUrl: ""
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
isChecked: contentList.contains(where: {
|
||||||
|
$0.id == item.contentId
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.contentShape(Rectangle())
|
||||||
|
.padding(.horizontal, 13.3)
|
||||||
|
.padding(.top, 6.7)
|
||||||
|
.onAppear {
|
||||||
|
if index == viewModel.orderList.count - 1 {
|
||||||
|
viewModel.getOrderList(orderType: .KEEP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.onAppear {
|
||||||
|
viewModel.getOrderList(orderType: .KEEP)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
.padding(.horizontal, 13.3)
|
||||||
|
.frame(maxWidth: .infinity, maxHeight: .infinity)
|
||||||
|
.background(Color.black)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
PlaylistAddContentView(
|
||||||
|
isShowing: .constant(true),
|
||||||
|
contentList: .constant(
|
||||||
|
[
|
||||||
|
AudioContentPlaylistContent(
|
||||||
|
id: 1,
|
||||||
|
title: "안녕하세요 오늘은 커버곡을 들려드리려고 해요안녕하세요 오늘은 커버곡을 들려드리려고 해요안녕하세요 오늘은 커버곡을 들려드리려고 해요안녕하세요 오늘은 커버곡을 들려드리려고 해요안녕하세요 오늘은 커버곡을 들려드리려고 해요안녕하세요 오늘은 커버곡을 들려드리려고 해요",
|
||||||
|
category: "커버곡",
|
||||||
|
coverUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||||
|
duration: "00:30:20",
|
||||||
|
creatorNickname: "유저1",
|
||||||
|
creatorProfileUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||||
|
),
|
||||||
|
AudioContentPlaylistContent(
|
||||||
|
id: 2,
|
||||||
|
title: "안녕하세요 오늘은 커버곡을 들려드리려고 해요",
|
||||||
|
category: "커버곡",
|
||||||
|
coverUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||||
|
duration: "00:30:20",
|
||||||
|
creatorNickname: "유저2",
|
||||||
|
creatorProfileUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||||
|
)
|
||||||
|
]
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,11 @@
|
||||||
|
//
|
||||||
|
// PlaylistContentIdAndOrder.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 12/8/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
struct PlaylistContentIdAndOrder: Encodable {
|
||||||
|
let contentId: Int
|
||||||
|
let order: Int
|
||||||
|
}
|
|
@ -0,0 +1,66 @@
|
||||||
|
//
|
||||||
|
// PlaylistCreateContentView.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 12/8/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import SwiftUI
|
||||||
|
import Kingfisher
|
||||||
|
|
||||||
|
struct PlaylistCreateContentView: View {
|
||||||
|
|
||||||
|
let content: AudioContentPlaylistContent
|
||||||
|
|
||||||
|
var body: some View {
|
||||||
|
HStack(spacing: 13.3) {
|
||||||
|
KFImage(URL(string: content.coverUrl))
|
||||||
|
.cancelOnDisappear(true)
|
||||||
|
.downsampling(size: CGSize(width: 40, height: 40))
|
||||||
|
.resizable()
|
||||||
|
.scaledToFill()
|
||||||
|
.frame(width: 40, height: 40, alignment: .center)
|
||||||
|
.cornerRadius(5.3)
|
||||||
|
|
||||||
|
VStack(alignment: .leading, spacing: 6.7) {
|
||||||
|
HStack(spacing: 8) {
|
||||||
|
Text(content.category)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 8))
|
||||||
|
.foregroundColor(Color(hex: "3bac6a"))
|
||||||
|
.padding(2.6)
|
||||||
|
.background(Color(hex: "28312b"))
|
||||||
|
.cornerRadius(2.6)
|
||||||
|
|
||||||
|
Text(content.duration)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 8))
|
||||||
|
.foregroundColor(Color.gray77)
|
||||||
|
.padding(2.6)
|
||||||
|
.background(Color.gray22)
|
||||||
|
.cornerRadius(2.6)
|
||||||
|
}
|
||||||
|
|
||||||
|
Text(content.title)
|
||||||
|
.font(.custom(Font.medium.rawValue, size: 12))
|
||||||
|
.foregroundColor(Color.grayd2)
|
||||||
|
.multilineTextAlignment(.leading)
|
||||||
|
.lineLimit(3)
|
||||||
|
.truncationMode(.tail)
|
||||||
|
.fixedSize(horizontal: false, vertical: true)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#Preview {
|
||||||
|
PlaylistCreateContentView(
|
||||||
|
content: AudioContentPlaylistContent(
|
||||||
|
id: 1,
|
||||||
|
title: "안녕하세요 오늘은 커버곡을 들려드리려고 해요안녕하세요 오늘은 커버곡을 들려드리려고 해요안녕하세요 오늘은 커버곡을 들려드리려고 해요안녕하세요 오늘은 커버곡을 들려드리려고 해요안녕하세요 오늘은 커버곡을 들려드리려고 해요안녕하세요 오늘은 커버곡을 들려드리려고 해요",
|
||||||
|
category: "커버곡",
|
||||||
|
coverUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||||
|
duration: "00:30:20",
|
||||||
|
creatorNickname: "유저1",
|
||||||
|
creatorProfileUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||||
|
)
|
||||||
|
)
|
||||||
|
}
|
|
@ -0,0 +1,28 @@
|
||||||
|
//
|
||||||
|
// GetPlaylistDetailResponse.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 12/8/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
import Foundation
|
||||||
|
|
||||||
|
struct GetPlaylistDetailResponse: Decodable {
|
||||||
|
let playlistId: Int
|
||||||
|
let title: String
|
||||||
|
let desc: String
|
||||||
|
let createdDate: String
|
||||||
|
let contentCount: Int
|
||||||
|
let playlistCoverImageList: [String]
|
||||||
|
let contentList: [AudioContentPlaylistContent]
|
||||||
|
}
|
||||||
|
|
||||||
|
struct AudioContentPlaylistContent: Decodable {
|
||||||
|
let id: Int
|
||||||
|
let title: String
|
||||||
|
let category: String
|
||||||
|
let coverUrl: String
|
||||||
|
let duration: String
|
||||||
|
let creatorNickname: String
|
||||||
|
let creatorProfileUrl: String
|
||||||
|
}
|
|
@ -10,6 +10,7 @@ import Moya
|
||||||
|
|
||||||
enum PlaylistApi {
|
enum PlaylistApi {
|
||||||
case getPlaylistList
|
case getPlaylistList
|
||||||
|
case createPlaylist(request: CreatePlaylistRequest)
|
||||||
}
|
}
|
||||||
|
|
||||||
extension PlaylistApi: TargetType {
|
extension PlaylistApi: TargetType {
|
||||||
|
@ -19,7 +20,7 @@ extension PlaylistApi: TargetType {
|
||||||
|
|
||||||
var path: String {
|
var path: String {
|
||||||
switch self {
|
switch self {
|
||||||
case .getPlaylistList:
|
case .getPlaylistList, .createPlaylist:
|
||||||
return "/audio-content/playlist"
|
return "/audio-content/playlist"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -28,6 +29,9 @@ extension PlaylistApi: TargetType {
|
||||||
switch self {
|
switch self {
|
||||||
case .getPlaylistList:
|
case .getPlaylistList:
|
||||||
return .get
|
return .get
|
||||||
|
|
||||||
|
case .createPlaylist:
|
||||||
|
return .post
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -35,6 +39,9 @@ extension PlaylistApi: TargetType {
|
||||||
switch self {
|
switch self {
|
||||||
case .getPlaylistList:
|
case .getPlaylistList:
|
||||||
return .requestPlain
|
return .requestPlain
|
||||||
|
|
||||||
|
case .createPlaylist(let request):
|
||||||
|
return .requestJSONEncodable(request)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -0,0 +1,14 @@
|
||||||
|
//
|
||||||
|
// CollectionExtension.swift
|
||||||
|
// SodaLive
|
||||||
|
//
|
||||||
|
// Created by klaus on 12/8/24.
|
||||||
|
//
|
||||||
|
|
||||||
|
extension Collection {
|
||||||
|
func mapIndexed<T>(_ transform: (Index, Element) -> T) -> [T] {
|
||||||
|
return self.enumerated().map { index, element in
|
||||||
|
transform(self.index(startIndex, offsetBy: index), element)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -25,11 +25,11 @@ final class OrderListAllViewModel: ObservableObject {
|
||||||
var isLast = false
|
var isLast = false
|
||||||
private let pageSize = 10
|
private let pageSize = 10
|
||||||
|
|
||||||
func getOrderList() {
|
func getOrderList(orderType: OrderType? = nil) {
|
||||||
if (!isLast && !isLoading) {
|
if (!isLast && !isLoading) {
|
||||||
isLoading = true
|
isLoading = true
|
||||||
|
|
||||||
repository.getOrderList(page: page, size: pageSize)
|
repository.getOrderList(orderType: orderType, page: page, size: pageSize)
|
||||||
.sink { result in
|
.sink { result in
|
||||||
switch result {
|
switch result {
|
||||||
case .finished:
|
case .finished:
|
||||||
|
|
Loading…
Reference in New Issue