diff --git a/SodaLive/Sources/App/AppStep.swift b/SodaLive/Sources/App/AppStep.swift index 352fc08..dd95cab 100644 --- a/SodaLive/Sources/App/AppStep.swift +++ b/SodaLive/Sources/App/AppStep.swift @@ -133,4 +133,6 @@ enum AppStep { case tempCanPayment(orderType: OrderType, contentId: Int, title: String, can: Int) case blockList + + case myBox } diff --git a/SodaLive/Sources/Content/Box/ContentBoxTabView.swift b/SodaLive/Sources/Content/Box/ContentBoxTabView.swift new file mode 100644 index 0000000..85f9b0a --- /dev/null +++ b/SodaLive/Sources/Content/Box/ContentBoxTabView.swift @@ -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) +} diff --git a/SodaLive/Sources/Content/Box/ContentBoxView.swift b/SodaLive/Sources/Content/Box/ContentBoxView.swift new file mode 100644 index 0000000..3af13bd --- /dev/null +++ b/SodaLive/Sources/Content/Box/ContentBoxView.swift @@ -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() +} diff --git a/SodaLive/Sources/Content/Box/ContentBoxViewModel.swift b/SodaLive/Sources/Content/Box/ContentBoxViewModel.swift new file mode 100644 index 0000000..6aebb3c --- /dev/null +++ b/SodaLive/Sources/Content/Box/ContentBoxViewModel.swift @@ -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 +} diff --git a/SodaLive/Sources/Content/Main/ContentMainView.swift b/SodaLive/Sources/Content/Main/ContentMainView.swift index 8d42fac..03539a3 100644 --- a/SodaLive/Sources/Content/Main/ContentMainView.swift +++ b/SodaLive/Sources/Content/Main/ContentMainView.swift @@ -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) diff --git a/SodaLive/Sources/Content/Playlist/ContentPlaylistItemView.swift b/SodaLive/Sources/Content/Playlist/ContentPlaylistItemView.swift new file mode 100644 index 0000000..36f3028 --- /dev/null +++ b/SodaLive/Sources/Content/Playlist/ContentPlaylistItemView.swift @@ -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" + ) + ) +} diff --git a/SodaLive/Sources/Content/Playlist/ContentPlaylistListRepository.swift b/SodaLive/Sources/Content/Playlist/ContentPlaylistListRepository.swift new file mode 100644 index 0000000..648128b --- /dev/null +++ b/SodaLive/Sources/Content/Playlist/ContentPlaylistListRepository.swift @@ -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() + + func getPlaylistList() -> AnyPublisher { + return api.requestPublisher(.getPlaylistList) + } +} diff --git a/SodaLive/Sources/Content/Playlist/ContentPlaylistListView.swift b/SodaLive/Sources/Content/Playlist/ContentPlaylistListView.swift new file mode 100644 index 0000000..016f5d9 --- /dev/null +++ b/SodaLive/Sources/Content/Playlist/ContentPlaylistListView.swift @@ -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..() + + @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.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) + } + } +} diff --git a/SodaLive/Sources/Content/Playlist/GetPlaylistsResponse.swift b/SodaLive/Sources/Content/Playlist/GetPlaylistsResponse.swift new file mode 100644 index 0000000..cf0c4d3 --- /dev/null +++ b/SodaLive/Sources/Content/Playlist/GetPlaylistsResponse.swift @@ -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 +} diff --git a/SodaLive/Sources/Content/Playlist/PlaylistApi.swift b/SodaLive/Sources/Content/Playlist/PlaylistApi.swift new file mode 100644 index 0000000..9fa8cae --- /dev/null +++ b/SodaLive/Sources/Content/Playlist/PlaylistApi.swift @@ -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))"] + } +} diff --git a/SodaLive/Sources/ContentView.swift b/SodaLive/Sources/ContentView.swift index a72f775..fb2b4ed 100644 --- a/SodaLive/Sources/ContentView.swift +++ b/SodaLive/Sources/ContentView.swift @@ -200,6 +200,9 @@ struct ContentView: View { case .blockList: BlockMemberListView() + case .myBox: + ContentBoxView() + default: EmptyView() .frame(width: 0, height: 0, alignment: .topLeading) diff --git a/SodaLive/Sources/MyPage/OrderList/OrderListAllInnerView.swift b/SodaLive/Sources/MyPage/OrderList/OrderListAllInnerView.swift new file mode 100644 index 0000000..9d6646c --- /dev/null +++ b/SodaLive/Sources/MyPage/OrderList/OrderListAllInnerView.swift @@ -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..