재생 목록 수정 페이지 추가

This commit is contained in:
Yu Sung 2024-12-10 14:06:34 +09:00
parent 8d60e500c0
commit 9ca1493255
6 changed files with 509 additions and 144 deletions

View File

@ -27,4 +27,8 @@ class ContentPlaylistListRepository {
func deletePlaylist(playlistId: Int) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.deletePlaylist(playlistId: playlistId))
}
func updatePlaylist(playlistId: Int, request: UpdatePlaylistRequest) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.updatePlaylist(playlistId: playlistId, request: request))
}
}

View File

@ -18,8 +18,15 @@ struct ContentPlaylistDetailView: View {
@State private var isShowPopupMenu = false
@State private var isShowDeleteConfirm = false
@State private var isShowModify = false
var body: some View {
BaseView(isLoading: $viewModel.isLoading) {
if reloadData {
Color.clear
LoadingView()
} else {
VStack(spacing: 21.3) {
HStack(spacing: 5.3) {
Image("ic_back")
@ -35,6 +42,7 @@ struct ContentPlaylistDetailView: View {
Image("ic_edit_white")
.padding(8)
.onTapGesture {
isShowModify = true
}
Image("ic_seemore_vertical_white")
@ -99,7 +107,7 @@ struct ContentPlaylistDetailView: View {
.background(Color.graybb)
.cornerRadius(4)
VStack(spacing: 6.7) {
VStack(alignment: .leading, spacing: 6.7) {
Text(response.title)
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color.grayd2)
@ -188,6 +196,7 @@ struct ContentPlaylistDetailView: View {
.onAppear {
viewModel.playlistId = playlistId
}
}
if isShowPopupMenu {
ZStack {
@ -243,6 +252,14 @@ struct ContentPlaylistDetailView: View {
)
}
}
if isShowModify {
ContentPlaylistModifyView(
playlistId: playlistId,
isShowing: $isShowModify,
reloadData: $reloadData
)
}
}
}
}

View File

@ -0,0 +1,177 @@
//
// ContentPlaylistModifyView.swift
// SodaLive
//
// Created by klaus on 12/10/24.
//
import SwiftUI
struct ContentPlaylistModifyView: View {
@StateObject var viewModel = ContentPlaylistModifyViewModel()
let playlistId: Int
@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.modifyPlaylist {
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()
}
}
.onAppear {
viewModel.playlistId = playlistId
}
if isShowAddContentView {
PlaylistAddContentView(
isShowing: $isShowAddContentView,
contentList: $viewModel.contentList
)
}
}
}
}
#Preview {
ContentPlaylistModifyView(
playlistId: 0,
isShowing: .constant(true),
reloadData: .constant(false)
)
}

View File

@ -0,0 +1,145 @@
//
// ContentPlaylistModifyViewModel.swift
// SodaLive
//
// Created by klaus on 12/10/24.
//
import Foundation
import Combine
final class ContentPlaylistModifyViewModel: 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]()
@Published var response: GetPlaylistDetailResponse? = nil
var playlistId: Int = 0 {
didSet {
if playlistId > 0 {
getPlaylistDetail()
}
}
}
private func getPlaylistDetail() {
isLoading = true
repository.getPlaylistDetail(playlistId: playlistId)
.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<GetPlaylistDetailResponse>.self, from: responseData)
if let data = decoded.data, decoded.success {
self.response = data
self.title = data.title
self.desc = data.desc
self.contentList.append(contentsOf: data.contentList)
} 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)
}
func modifyPlaylist(onSuccess: @escaping () -> Void) {
if let response = response, validate() {
isLoading = true
let contentIdAndOrderList = contentList.mapIndexed { index, item in
PlaylistContentIdAndOrder(contentId: item.id, order: index + 1)
}
let request = UpdatePlaylistRequest(
title: self.title != response.title ? self.title : nil,
desc: self.desc != response.desc ? self.desc : nil,
contentIdAndOrderList: contentIdAndOrderList
)
repository.updatePlaylist(
playlistId: playlistId,
request: request
)
.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
}
}

View File

@ -0,0 +1,12 @@
//
// UpdatePlaylistRequest.swift
// SodaLive
//
// Created by klaus on 12/10/24.
//
struct UpdatePlaylistRequest: Encodable {
let title: String?
let desc: String?
let contentIdAndOrderList: [PlaylistContentIdAndOrder]
}

View File

@ -13,6 +13,7 @@ enum PlaylistApi {
case createPlaylist(request: CreatePlaylistRequest)
case getPlaylistDetail(playlistId: Int)
case deletePlaylist(playlistId: Int)
case updatePlaylist(playlistId: Int, request: UpdatePlaylistRequest)
}
extension PlaylistApi: TargetType {
@ -30,6 +31,9 @@ extension PlaylistApi: TargetType {
case .deletePlaylist(let playlistId):
return "/audio-content/playlist/\(playlistId)"
case .updatePlaylist(let playlistId, _):
return "/audio-content/playlist/\(playlistId)"
}
}
@ -43,6 +47,9 @@ extension PlaylistApi: TargetType {
case .deletePlaylist:
return .delete
case .updatePlaylist:
return .put
}
}
@ -53,6 +60,9 @@ extension PlaylistApi: TargetType {
case .createPlaylist(let request):
return .requestJSONEncodable(request)
case .updatePlaylist(_, let request):
return .requestJSONEncodable(request)
}
}