refactor: 사용하지 않는 파일 삭제
This commit is contained in:
@@ -1,64 +0,0 @@
|
||||
//
|
||||
// ContentMainTabContentRepository.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/21/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CombineMoya
|
||||
import Combine
|
||||
import Moya
|
||||
|
||||
final class ContentMainTabContentRepository {
|
||||
|
||||
private let api = MoyaProvider<ContentApi>()
|
||||
|
||||
func getContentMainContent() -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getContentMainContent(
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func getNewContentOfTheme(theme: String) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getContentMainNewContentOfTheme(
|
||||
theme: theme,
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func getContentRanking(sortType: String) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getDailyContentRanking(
|
||||
sortType: sortType,
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func getRecommendContentByTag(tag: String) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getRecommendContentByTag(
|
||||
tag: tag,
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func getPopularContentByCreator(creatorId: Int) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getContentMainContentPopularContentByCreator(
|
||||
creatorId: creatorId,
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,93 +0,0 @@
|
||||
//
|
||||
// ContentMainTabContentView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/21/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainTabContentView: View {
|
||||
|
||||
@StateObject var viewModel = ContentMainTabContentViewModel()
|
||||
|
||||
var body: some View {
|
||||
BaseView(isLoading: $viewModel.isLoading) {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
VStack(spacing: 0) {
|
||||
if !viewModel.bannerList.isEmpty {
|
||||
ContentMainBannerViewV2(bannerList: viewModel.bannerList)
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
|
||||
if !viewModel.contentThemeList.isEmpty {
|
||||
ContentMainNewContentViewV2(
|
||||
title: "새로운 단편",
|
||||
onClickMore: {
|
||||
AppState.shared
|
||||
.setAppStep(step: .newContentAll(isFree: false))
|
||||
},
|
||||
themeList: viewModel.contentThemeList,
|
||||
contentList: viewModel.newContentList
|
||||
) {
|
||||
viewModel.getNewContentOfTheme(theme: $0)
|
||||
}
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.rankSortTypeList.isEmpty {
|
||||
ContentMainTabRankContentView(
|
||||
title: "일간 랭킹",
|
||||
isMore: false,
|
||||
onClickMore: {},
|
||||
sortList: viewModel.rankSortTypeList,
|
||||
onClickSort: { viewModel.getContentRanking(sort: $0) },
|
||||
contentList: viewModel.rankContentList
|
||||
)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.contentRankCreatorList.isEmpty {
|
||||
ContentByChannelView(
|
||||
title: "채널별 추천 단편",
|
||||
creatorList: viewModel.contentRankCreatorList,
|
||||
contentList: viewModel.likeCountRankContentList,
|
||||
onClickCreator: {
|
||||
viewModel.getPopularContentByCreator(creatorId: $0)
|
||||
}
|
||||
)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.eventBannerList.isEmpty {
|
||||
SectionEventBannerView(items: viewModel.eventBannerList)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.tagList.isEmpty {
|
||||
ContentMainTagCurationView(
|
||||
tagList: viewModel.tagList,
|
||||
contentList: viewModel.tagCurationContentList
|
||||
) {
|
||||
viewModel.getRecommendContentByTag(tag: $0)
|
||||
}
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.curationList.isEmpty {
|
||||
ContentMainCurationViewV2(curationList: viewModel.curationList)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.fetchData()
|
||||
}
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainTabContentView()
|
||||
}
|
||||
@@ -1,231 +0,0 @@
|
||||
//
|
||||
// ContentMainTabContentViewModel.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/21/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
final class ContentMainTabContentViewModel: ObservableObject {
|
||||
private let repository = ContentMainTabContentRepository()
|
||||
private var subscription = Set<AnyCancellable>()
|
||||
|
||||
@Published var errorMessage = ""
|
||||
@Published var isShowPopup = false
|
||||
@Published var isLoading = false
|
||||
|
||||
@Published var bannerList: [GetAudioContentBannerResponse] = []
|
||||
@Published var contentThemeList: [String] = []
|
||||
@Published var newContentList: [GetAudioContentMainItem] = []
|
||||
@Published var rankSortTypeList: [String] = []
|
||||
@Published var rankContentList: [GetAudioContentRankingItem] = []
|
||||
@Published var contentRankCreatorList: [ContentCreatorResponse] = []
|
||||
@Published var likeCountRankContentList: [GetAudioContentRankingItem] = []
|
||||
@Published var eventBannerList: [EventItem] = []
|
||||
@Published var tagList: [String] = []
|
||||
@Published var tagCurationContentList: [GetAudioContentMainItem] = []
|
||||
@Published var curationList: [GetContentCurationResponse] = []
|
||||
|
||||
func fetchData() {
|
||||
isLoading = true
|
||||
repository.getContentMainContent()
|
||||
.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<GetContentMainTabContentResponse>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.bannerList = data.bannerList
|
||||
self.contentThemeList = ["전체"] + data.contentThemeList
|
||||
self.newContentList = data.newContentList
|
||||
self.rankSortTypeList = data.rankSortTypeList
|
||||
self.rankContentList = data.rankContentList
|
||||
self.contentRankCreatorList = data.contentRankCreatorList
|
||||
self.likeCountRankContentList = data.likeCountRankContentList
|
||||
self.eventBannerList = data.eventBannerList.eventList
|
||||
self.tagList = data.tagList
|
||||
self.tagCurationContentList = data.tagCurationContentList
|
||||
self.curationList = data.curationList
|
||||
} 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 getNewContentOfTheme(theme: String) {
|
||||
isLoading = true
|
||||
|
||||
repository.getNewContentOfTheme(theme: theme == "전체" ? "" : theme)
|
||||
.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<[GetAudioContentMainItem]>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.newContentList = data
|
||||
} 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 getContentRanking(sort: String = "매출") {
|
||||
isLoading = true
|
||||
repository.getContentRanking(sortType: sort)
|
||||
.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<[GetAudioContentRankingItem]>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.rankContentList = data
|
||||
} 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 getRecommendContentByTag(tag: String) {
|
||||
isLoading = true
|
||||
repository.getRecommendContentByTag(tag: tag)
|
||||
.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<[GetAudioContentMainItem]>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.tagCurationContentList = data
|
||||
} 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 getPopularContentByCreator(creatorId: Int) {
|
||||
isLoading = true
|
||||
repository.getPopularContentByCreator(creatorId: creatorId)
|
||||
.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<[GetAudioContentRankingItem]>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.likeCountRankContentList = data
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
@@ -1,165 +0,0 @@
|
||||
//
|
||||
// ContentMainTabRankContentView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/20/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
import Kingfisher
|
||||
|
||||
struct ContentMainTabRankContentView: View {
|
||||
|
||||
@AppStorage("token") private var token: String = UserDefaults.string(forKey: UserDefaultsKey.token)
|
||||
|
||||
let rows = [
|
||||
GridItem(.fixed(60), alignment: .leading),
|
||||
GridItem(.fixed(60), alignment: .leading),
|
||||
GridItem(.fixed(60), alignment: .leading)
|
||||
]
|
||||
|
||||
let title: String
|
||||
|
||||
let isMore: Bool
|
||||
let onClickMore: () -> Void
|
||||
|
||||
let sortList: [String]
|
||||
let onClickSort: (String) -> Void
|
||||
|
||||
let contentList: [GetAudioContentRankingItem]
|
||||
|
||||
@State private var selectedSort = ""
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 13.3) {
|
||||
HStack(spacing: 0) {
|
||||
Text(title)
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
|
||||
Spacer()
|
||||
|
||||
if isMore {
|
||||
Image("ic_forward")
|
||||
.onTapGesture { onClickMore() }
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
if !sortList.isEmpty {
|
||||
ContentMainRankingSortView(
|
||||
sorts: sortList,
|
||||
selectSort: {
|
||||
selectedSort = $0
|
||||
onClickSort($0)
|
||||
},
|
||||
selectedSort: $selectedSort
|
||||
)
|
||||
}
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHGrid(rows: rows, spacing: 13.3) {
|
||||
ForEach(0..<contentList.count, id: \.self) { index in
|
||||
let content = contentList[index]
|
||||
HStack(spacing: 0) {
|
||||
KFImage(URL(string: content.coverImageUrl))
|
||||
.cancelOnDisappear(true)
|
||||
.downsampling(
|
||||
size: CGSize(
|
||||
width: 60,
|
||||
height: 60
|
||||
)
|
||||
)
|
||||
.resizable()
|
||||
.frame(width: 60, height: 60)
|
||||
.cornerRadius(2.7)
|
||||
|
||||
Text("\(index + 1)")
|
||||
.appFont(size: 16.7, weight: .bold)
|
||||
.foregroundColor(.button)
|
||||
.padding(.horizontal, 12)
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(content.title)
|
||||
.lineLimit(2)
|
||||
.appFont(size: 13.3, weight: .medium)
|
||||
.foregroundColor(.grayd2)
|
||||
|
||||
Text(content.creatorNickname)
|
||||
.appFont(size: 11, weight: .medium)
|
||||
.foregroundColor(.gray77)
|
||||
}
|
||||
}
|
||||
.frame(width: screenSize().width * 0.66, alignment: .leading)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
AppState.shared
|
||||
.setAppStep(step: .contentDetail(contentId: content.contentId))
|
||||
} else {
|
||||
AppState.shared
|
||||
.setAppStep(step: .login)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
.frame(height: 207)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if !sortList.isEmpty {
|
||||
selectedSort = sortList[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainTabRankContentView(
|
||||
title: "인기 단편",
|
||||
isMore: true,
|
||||
onClickMore: {},
|
||||
sortList: ["매출", "댓글", "좋아요"],
|
||||
onClickSort: { _ in },
|
||||
contentList: [
|
||||
GetAudioContentRankingItem(
|
||||
contentId: 1,
|
||||
title: "안녕하세요 오늘은 커버곡을 들려드릴께요....안녕하세요 오늘은 커버곡을 들려드릴께요....",
|
||||
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
themeStr: "커버곡",
|
||||
price: 100,
|
||||
duration: "00:30:20",
|
||||
creatorId: 1,
|
||||
creatorNickname: "유저1",
|
||||
isPointAvailable: false,
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
GetAudioContentRankingItem(
|
||||
contentId: 2,
|
||||
title: "안녕하세요 오늘은 커버곡을 들려드릴께요....",
|
||||
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
themeStr: "커버곡",
|
||||
price: 0,
|
||||
duration: "00:30:20",
|
||||
creatorId: 2,
|
||||
creatorNickname: "유저2",
|
||||
isPointAvailable: false,
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
GetAudioContentRankingItem(
|
||||
contentId: 3,
|
||||
title: "안녕하세요 오늘은 커버곡을 들려드릴께요....안녕하세요 오늘은 커버곡을 들려드릴께요....",
|
||||
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
themeStr: "커버곡",
|
||||
price: 50,
|
||||
duration: "00:30:20",
|
||||
creatorId: 3,
|
||||
creatorNickname: "유저3",
|
||||
isPointAvailable: true,
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
@@ -1,245 +0,0 @@
|
||||
//
|
||||
// ContentMainTagCurationView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/22/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct ContentMainTagCurationView: View {
|
||||
|
||||
let tagList: [String]
|
||||
let contentList: [GetAudioContentMainItem]
|
||||
let selectTag: (String) -> Void
|
||||
|
||||
let tagColumns = [
|
||||
GridItem(.flexible()),
|
||||
GridItem(.flexible()),
|
||||
GridItem(.flexible()),
|
||||
GridItem(.flexible())
|
||||
]
|
||||
|
||||
let contentColumns = [
|
||||
GridItem(.flexible()),
|
||||
GridItem(.flexible()),
|
||||
GridItem(.flexible())
|
||||
]
|
||||
|
||||
@State private var selectedTag = ""
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
Text("태그별 추천 콘텐츠")
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(.grayee)
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
LazyVGrid(columns: tagColumns, spacing: 6) {
|
||||
ForEach(0..<tagList.count, id: \.self) { index in
|
||||
let tag = tagList[index]
|
||||
Text(tagList[index])
|
||||
.appFont(size: 10, weight: .medium)
|
||||
.foregroundColor(
|
||||
selectedTag == tag ?
|
||||
.button:
|
||||
.gray77
|
||||
)
|
||||
.padding(.vertical, 10)
|
||||
.frame(width: (screenSize().width - 18 - 26.7) / 4)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: 2.6)
|
||||
.strokeBorder(lineWidth: 1)
|
||||
.foregroundColor(
|
||||
selectedTag == tag ?
|
||||
.button:
|
||||
.gray77
|
||||
)
|
||||
)
|
||||
.onTapGesture {
|
||||
if selectedTag != tag {
|
||||
selectedTag = tag
|
||||
selectTag(tag)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
LazyVGrid(columns: contentColumns, spacing: 13.3) {
|
||||
ForEach(0..<contentList.count, id: \.self) { index in
|
||||
ContentMainTagCurationContentView(
|
||||
item: contentList[index],
|
||||
itemWidth: (screenSize().width - 40) / 3
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
.onAppear {
|
||||
selectedTag = tagList[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentMainTagCurationContentView: View {
|
||||
|
||||
let item: GetAudioContentMainItem
|
||||
let itemWidth: CGFloat
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
ZStack(alignment: .bottom) {
|
||||
ZStack(alignment: .topTrailing) {
|
||||
KFImage(URL(string: item.coverImageUrl))
|
||||
.cancelOnDisappear(true)
|
||||
.downsampling(
|
||||
size: CGSize(
|
||||
width: itemWidth,
|
||||
height: itemWidth
|
||||
)
|
||||
)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: itemWidth, height: itemWidth, alignment: .top)
|
||||
.cornerRadius(2.7)
|
||||
|
||||
if item.isPointAvailable {
|
||||
Image("ic_point")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
.padding(.top, 2.7)
|
||||
.padding(.trailing, 2.7)
|
||||
}
|
||||
}
|
||||
|
||||
VStack(spacing: 0) {
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 0) {
|
||||
HStack(spacing: 2) {
|
||||
if item.price > 0 {
|
||||
Image("ic_card_can_gray")
|
||||
|
||||
Text("\(item.price)")
|
||||
.appFont(size: 8.5, weight: .medium)
|
||||
.foregroundColor(Color.white)
|
||||
} else {
|
||||
Text("무료")
|
||||
.appFont(size: 8.5, weight: .medium)
|
||||
.foregroundColor(Color.white)
|
||||
}
|
||||
}
|
||||
.padding(3)
|
||||
.background(Color.gray33.opacity(0.7))
|
||||
.cornerRadius(10)
|
||||
.padding(.leading, 2.7)
|
||||
.padding(.bottom, 2.7)
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack(spacing: 2) {
|
||||
Text(item.duration)
|
||||
.appFont(size: 8.5, weight: .medium)
|
||||
.foregroundColor(Color.white)
|
||||
}
|
||||
.padding(3)
|
||||
.background(Color.gray33.opacity(0.7))
|
||||
.cornerRadius(10)
|
||||
.padding(.trailing, 2.7)
|
||||
.padding(.bottom, 2.7)
|
||||
}
|
||||
}
|
||||
}
|
||||
.frame(width: itemWidth, height: itemWidth)
|
||||
|
||||
Text(item.title)
|
||||
.appFont(size: 13.3, weight: .medium)
|
||||
.foregroundColor(Color.grayd2)
|
||||
.frame(width: itemWidth, alignment: .leading)
|
||||
.multilineTextAlignment(.leading)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.lineLimit(2)
|
||||
|
||||
HStack(spacing: 5.3) {
|
||||
KFImage(URL(string: item.creatorProfileImageUrl))
|
||||
.cancelOnDisappear(true)
|
||||
.downsampling(
|
||||
size: CGSize(
|
||||
width: 21.3,
|
||||
height: 21.3
|
||||
)
|
||||
)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 21.3, height: 21.3)
|
||||
.clipShape(Circle())
|
||||
.onTapGesture { AppState.shared.setAppStep(step: .creatorDetail(userId: item.creatorId)) }
|
||||
|
||||
Text(item.creatorNickname)
|
||||
.appFont(size: 12, weight: .medium)
|
||||
.foregroundColor(.gray77)
|
||||
.lineLimit(1)
|
||||
}
|
||||
.padding(.bottom, 10)
|
||||
}
|
||||
.onTapGesture {
|
||||
AppState.shared
|
||||
.setAppStep(step: .contentDetail(contentId: item.contentId))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainTagCurationView(
|
||||
tagList: ["test", "test2", "test3", "test4", "test5", "test6", "test7"],
|
||||
contentList: [
|
||||
GetAudioContentMainItem(
|
||||
contentId: 1,
|
||||
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
title: "ㅓ처랴햐햫햐햐",
|
||||
creatorId: 8,
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
creatorNickname: "유저1",
|
||||
price: 100,
|
||||
duration: "00:00:30",
|
||||
isPointAvailable: true
|
||||
),
|
||||
GetAudioContentMainItem(
|
||||
contentId: 2,
|
||||
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
title: "ㅓ처랴햐햫햐햐",
|
||||
creatorId: 8,
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
creatorNickname: "유저2",
|
||||
price: 0,
|
||||
duration: "00:00:30",
|
||||
isPointAvailable: false
|
||||
),
|
||||
GetAudioContentMainItem(
|
||||
contentId: 3,
|
||||
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
title: "ㅓ처랴햐햫햐햐",
|
||||
creatorId: 8,
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
creatorNickname: "유저3",
|
||||
price: 1000,
|
||||
duration: "00:00:30",
|
||||
isPointAvailable: false
|
||||
),
|
||||
GetAudioContentMainItem(
|
||||
contentId: 4,
|
||||
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
title: "ㅓ처랴햐햫햐햐",
|
||||
creatorId: 8,
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
creatorNickname: "유저3",
|
||||
price: 50000,
|
||||
duration: "00:00:30",
|
||||
isPointAvailable: false
|
||||
)
|
||||
],
|
||||
selectTag: { _ in }
|
||||
)
|
||||
}
|
||||
@@ -1,20 +0,0 @@
|
||||
//
|
||||
// GetContentMainTabContentResponse.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/21/25.
|
||||
//
|
||||
|
||||
struct GetContentMainTabContentResponse: Decodable {
|
||||
let bannerList: [GetAudioContentBannerResponse]
|
||||
let contentThemeList: [String]
|
||||
let newContentList: [GetAudioContentMainItem]
|
||||
let rankSortTypeList: [String]
|
||||
let rankContentList: [GetAudioContentRankingItem]
|
||||
let contentRankCreatorList: [ContentCreatorResponse]
|
||||
let likeCountRankContentList: [GetAudioContentRankingItem]
|
||||
let eventBannerList: GetEventResponse
|
||||
let tagList: [String]
|
||||
let tagCurationContentList: [GetAudioContentMainItem]
|
||||
let curationList: [GetContentCurationResponse]
|
||||
}
|
||||
@@ -1,220 +0,0 @@
|
||||
//
|
||||
// ContentByChannelView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/20/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct ContentByChannelView: View {
|
||||
|
||||
@AppStorage("token") private var token: String = UserDefaults.string(forKey: UserDefaultsKey.token)
|
||||
|
||||
let title: String
|
||||
let creatorList: [ContentCreatorResponse]
|
||||
let contentList: [GetAudioContentRankingItem]
|
||||
let onClickCreator: (Int) -> Void
|
||||
|
||||
@State private var selectedCreatorId = 0
|
||||
|
||||
let columns = [
|
||||
GridItem(.flexible(), spacing: 13.3),
|
||||
GridItem(.flexible(), spacing: 13.3)
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 20) {
|
||||
Text(title)
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(.grayee)
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 22) {
|
||||
ForEach(0..<creatorList.count, id: \.self) { index in
|
||||
let item = creatorList[index]
|
||||
|
||||
ContentCreatorView(
|
||||
isSelected: item.creatorId == selectedCreatorId,
|
||||
item: item
|
||||
)
|
||||
.onTapGesture {
|
||||
let creatorId = item.creatorId
|
||||
selectedCreatorId = creatorId
|
||||
onClickCreator(creatorId)
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
|
||||
LazyVGrid(columns: columns, spacing: 13.3) {
|
||||
ForEach(0..<contentList.count, id: \.self) { index in
|
||||
let content = contentList[index]
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
ZStack(alignment:.bottom) {
|
||||
GeometryReader { geometry in
|
||||
ZStack(alignment: .topTrailing) {
|
||||
KFImage(URL(string: content.coverImageUrl))
|
||||
.cancelOnDisappear(true)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: geometry.size.width, height: geometry.size.width)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 5.3))
|
||||
.clipped()
|
||||
|
||||
if content.isPointAvailable {
|
||||
Image("ic_point")
|
||||
.padding(.top, 2.7)
|
||||
.padding(.trailing, 2.7)
|
||||
}
|
||||
}
|
||||
}
|
||||
.aspectRatio(1, contentMode: .fit)
|
||||
|
||||
HStack(spacing: 0) {
|
||||
HStack(spacing: 2) {
|
||||
if content.price > 0 {
|
||||
Image("ic_card_can_gray_32")
|
||||
}
|
||||
|
||||
Text(content.price > 0 ? "\(content.price)" : "무료")
|
||||
.appFont(size: 12, weight: .medium)
|
||||
.foregroundColor(Color.white)
|
||||
}
|
||||
.padding(4)
|
||||
.background(Color.gray33.opacity(0.7))
|
||||
.cornerRadius(10)
|
||||
|
||||
Spacer()
|
||||
|
||||
Text(content.duration)
|
||||
.appFont(size: 12, weight: .medium)
|
||||
.foregroundColor(Color.white)
|
||||
.padding(4)
|
||||
.background(Color.gray33.opacity(0.7))
|
||||
.cornerRadius(10)
|
||||
}
|
||||
.padding(.horizontal, 2.7)
|
||||
.padding(.bottom, 2.7)
|
||||
}
|
||||
|
||||
Text(content.title)
|
||||
.appFont(size: 13.3, weight: .medium)
|
||||
.foregroundColor(.grayd2)
|
||||
.lineLimit(1)
|
||||
|
||||
HStack(spacing: 5.3) {
|
||||
KFImage(URL(string: content.creatorProfileImageUrl))
|
||||
.cancelOnDisappear(true)
|
||||
.downsampling(size: CGSize(width: 21, height: 21))
|
||||
.resizable()
|
||||
.frame(width: 21, height: 21)
|
||||
.clipShape(Circle())
|
||||
.clipped()
|
||||
|
||||
Text(content.creatorNickname)
|
||||
.appFont(size: 10, weight: .medium)
|
||||
.foregroundColor(.gray77)
|
||||
}
|
||||
.onTapGesture {
|
||||
if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
AppState.shared
|
||||
.setAppStep(step: .creatorDetail(userId: content.creatorId))
|
||||
} else {
|
||||
AppState.shared
|
||||
.setAppStep(step: .login)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onTapGesture {
|
||||
if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
||||
AppState.shared
|
||||
.setAppStep(step: .contentDetail(contentId: content.contentId))
|
||||
} else {
|
||||
AppState.shared
|
||||
.setAppStep(step: .login)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
.onAppear {
|
||||
if !self.creatorList.isEmpty {
|
||||
selectedCreatorId = creatorList[0].creatorId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentByChannelView(
|
||||
title: "채널별 인기 콘텐츠",
|
||||
creatorList: [
|
||||
ContentCreatorResponse(
|
||||
creatorId: 1,
|
||||
creatorNickname: "유저1",
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
ContentCreatorResponse(
|
||||
creatorId: 2,
|
||||
creatorNickname: "유저2",
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
)
|
||||
],
|
||||
contentList: [
|
||||
GetAudioContentRankingItem(
|
||||
contentId: 1,
|
||||
title: "안녕하세요 오늘은 커버곡을 들려드릴께요....안녕하세요 오늘은 커버곡을 들려드릴께요....",
|
||||
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
themeStr: "커버곡",
|
||||
price: 100,
|
||||
duration: "00:30:20",
|
||||
creatorId: 1,
|
||||
creatorNickname: "유저1",
|
||||
isPointAvailable: true,
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
GetAudioContentRankingItem(
|
||||
contentId: 2,
|
||||
title: "안녕하세요 오늘은 커버곡을 들려드릴께요....",
|
||||
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
themeStr: "커버곡",
|
||||
price: 0,
|
||||
duration: "00:30:20",
|
||||
creatorId: 1,
|
||||
creatorNickname: "유저1",
|
||||
isPointAvailable: false,
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
GetAudioContentRankingItem(
|
||||
contentId: 3,
|
||||
title: "안녕하세요 오늘은 커버곡을 들려드릴께요....안녕하세요 오늘은 커버곡을 들려드릴께요....",
|
||||
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
themeStr: "커버곡",
|
||||
price: 50,
|
||||
duration: "00:30:20",
|
||||
creatorId: 1,
|
||||
creatorNickname: "유저1",
|
||||
isPointAvailable: false,
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
GetAudioContentRankingItem(
|
||||
contentId: 4,
|
||||
title: "안녕하세요 오늘은 커버곡을 들려드릴께요....안녕하세요 오늘은 커버곡을 들려드릴께요....",
|
||||
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
themeStr: "커버곡",
|
||||
price: 50,
|
||||
duration: "00:30:20",
|
||||
creatorId: 1,
|
||||
creatorNickname: "유저1",
|
||||
isPointAvailable: false,
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
)
|
||||
]
|
||||
) { _ in }
|
||||
}
|
||||
@@ -1,57 +0,0 @@
|
||||
//
|
||||
// ContentCreatorView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/20/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct ContentCreatorView: View {
|
||||
|
||||
let isSelected: Bool
|
||||
let item: ContentCreatorResponse
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 13.3) {
|
||||
KFImage(URL(string: item.creatorProfileImageUrl))
|
||||
.cancelOnDisappear(true)
|
||||
.downsampling(size: CGSize(width: 60, height: 60))
|
||||
.resizable()
|
||||
.frame(width: 60, height: 60)
|
||||
.clipShape(Circle())
|
||||
.overlay(
|
||||
Circle()
|
||||
.strokeBorder(lineWidth: 3)
|
||||
.foregroundColor(
|
||||
.button
|
||||
.opacity(isSelected ? 1 : 0)
|
||||
)
|
||||
)
|
||||
|
||||
Text(item.creatorNickname)
|
||||
.appFont(size: 11.3, weight: .medium)
|
||||
.foregroundColor(
|
||||
isSelected ?
|
||||
Color.button :
|
||||
Color.graybb
|
||||
|
||||
)
|
||||
.lineLimit(1)
|
||||
.truncationMode(.tail)
|
||||
}
|
||||
.frame(width: 60)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentCreatorView(
|
||||
isSelected: true,
|
||||
item: ContentCreatorResponse(
|
||||
creatorId: 1,
|
||||
creatorNickname: "유저1",
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
)
|
||||
)
|
||||
}
|
||||
@@ -1,90 +0,0 @@
|
||||
//
|
||||
// ContentMainCurationViewV2.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/21/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainCurationViewV2: View {
|
||||
|
||||
let curationList: [GetContentCurationResponse]
|
||||
|
||||
var body: some View {
|
||||
LazyVStack(spacing: 30) {
|
||||
ForEach(0..<curationList.count, id: \.self) { index in
|
||||
let curation = curationList[index]
|
||||
ContentMainCurationItemViewV2(curation: curation)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentMainCurationItemViewV2: View {
|
||||
let curation: GetContentCurationResponse
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
Text(curation.title)
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(.grayee)
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 13.3) {
|
||||
ForEach(0..<curation.items.count, id: \.self) { index in
|
||||
let item = curation.items[index]
|
||||
ContentMainItemView(item: item)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainCurationViewV2(
|
||||
curationList: [
|
||||
GetContentCurationResponse(
|
||||
title: "test1",
|
||||
items: [
|
||||
GetAudioContentMainItem(
|
||||
contentId: 1,
|
||||
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
title: "ㅓ처랴햐햫햐햐",
|
||||
creatorId: 8,
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
creatorNickname: "유저1",
|
||||
price: 10,
|
||||
duration: "00:00:30",
|
||||
isPointAvailable: true
|
||||
),
|
||||
GetAudioContentMainItem(
|
||||
contentId: 2,
|
||||
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
title: "ㅓ처랴햐햫햐햐",
|
||||
creatorId: 8,
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
creatorNickname: "유저2",
|
||||
price: 10,
|
||||
duration: "00:00:30",
|
||||
isPointAvailable: false
|
||||
),
|
||||
GetAudioContentMainItem(
|
||||
contentId: 3,
|
||||
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
title: "ㅓ처랴햐햫햐햐",
|
||||
creatorId: 8,
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
creatorNickname: "유저3",
|
||||
price: 10,
|
||||
duration: "00:00:30",
|
||||
isPointAvailable: false
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
@@ -1,103 +0,0 @@
|
||||
//
|
||||
// ContentMainNewContentViewV2.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/21/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainNewContentViewV2: View {
|
||||
|
||||
let title: String
|
||||
let onClickMore: () -> Void
|
||||
let themeList: [String]
|
||||
let contentList: [GetAudioContentMainItem]
|
||||
|
||||
let selectTheme: (String) -> Void
|
||||
@State private var selectedTheme = "전체"
|
||||
|
||||
var body: some View {
|
||||
LazyVStack(spacing: 13.3) {
|
||||
HStack(spacing: 0) {
|
||||
Text(title)
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(.grayee)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image("ic_forward")
|
||||
.resizable()
|
||||
.frame(width: 20, height: 20)
|
||||
.onTapGesture { onClickMore() }
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
if !themeList.isEmpty {
|
||||
ContentMainContentThemeView(
|
||||
themeList: themeList,
|
||||
selectTheme: selectTheme,
|
||||
selectedTheme: $selectedTheme
|
||||
)
|
||||
}
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 13.3) {
|
||||
ForEach(0..<contentList.count, id: \.self) { index in
|
||||
ContentMainItemView(item: contentList[index])
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if !themeList.isEmpty {
|
||||
selectedTheme = themeList[0]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainNewContentViewV2(
|
||||
title: "새로운 단편",
|
||||
onClickMore: {},
|
||||
themeList: ["전체", "테스트1", "테스트2"],
|
||||
contentList: [
|
||||
GetAudioContentMainItem(
|
||||
contentId: 1,
|
||||
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
title: "ㅓ처랴햐햫햐햐",
|
||||
creatorId: 8,
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
creatorNickname: "유저1",
|
||||
price: 10,
|
||||
duration: "00:00:30",
|
||||
isPointAvailable: false
|
||||
),
|
||||
GetAudioContentMainItem(
|
||||
contentId: 2,
|
||||
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
title: "ㅓ처랴햐햫햐햐",
|
||||
creatorId: 8,
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
creatorNickname: "유저2",
|
||||
price: 10,
|
||||
duration: "00:00:30",
|
||||
isPointAvailable: false
|
||||
),
|
||||
GetAudioContentMainItem(
|
||||
contentId: 3,
|
||||
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
title: "ㅓ처랴햐햫햐햐",
|
||||
creatorId: 8,
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
creatorNickname: "유저3",
|
||||
price: 10,
|
||||
duration: "00:00:30",
|
||||
isPointAvailable: false
|
||||
)
|
||||
],
|
||||
selectTheme: { _ in }
|
||||
)
|
||||
}
|
||||
@@ -1,34 +0,0 @@
|
||||
//
|
||||
// ContentMainNoItemView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/21/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainNoItemView: View {
|
||||
var body: some View {
|
||||
VStack(spacing: 0) {
|
||||
Image("ic_no_item")
|
||||
.resizable()
|
||||
.frame(width: 60, height: 60)
|
||||
|
||||
Text("마이페이지에서 본인인증을 해주세요")
|
||||
.appFont(size: 13, weight: .medium)
|
||||
.foregroundColor(.graybb)
|
||||
.fixedSize(horizontal: false, vertical: true)
|
||||
.multilineTextAlignment(.center)
|
||||
.lineSpacing(8)
|
||||
.padding(.vertical, 8)
|
||||
}
|
||||
.padding(.vertical, 16.7)
|
||||
.frame(maxWidth: .infinity)
|
||||
.background(Color.bg)
|
||||
.cornerRadius(4.7)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainNoItemView()
|
||||
}
|
||||
@@ -1,235 +0,0 @@
|
||||
//
|
||||
// ContentMainViewV2.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/21/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
enum ContentMainTab {
|
||||
case HOME
|
||||
case SERIES
|
||||
case CONTENT
|
||||
}
|
||||
|
||||
struct TabItem {
|
||||
let title: String
|
||||
let tab: ContentMainTab
|
||||
}
|
||||
|
||||
struct ContentMainViewV2: View {
|
||||
|
||||
@StateObject var contentPlayManager = ContentPlayManager.shared
|
||||
@StateObject var contentPlayerPlayManager = ContentPlayerPlayManager.shared
|
||||
|
||||
@State private var selectedTab: ContentMainTab = .SERIES
|
||||
@State private var isShowPlayer = false
|
||||
|
||||
let tabItemList = [
|
||||
TabItem(title: "홈", tab: .HOME),
|
||||
TabItem(title: "시리즈", tab: .SERIES),
|
||||
TabItem(title: "단편", tab: .CONTENT),
|
||||
]
|
||||
|
||||
init(selectedTab: ContentMainTab = .SERIES) {
|
||||
self._selectedTab = State(initialValue: selectedTab)
|
||||
}
|
||||
|
||||
var body: some View {
|
||||
Group {
|
||||
ZStack {
|
||||
Color.black.ignoresSafeArea()
|
||||
|
||||
VStack(spacing: 0) {
|
||||
HStack(spacing: 0) {
|
||||
Text("콘텐츠 마켓")
|
||||
.appFont(size: 21.3, weight: .bold)
|
||||
.foregroundColor(Color.button)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image("ic_content_keep")
|
||||
.onTapGesture {
|
||||
AppState.shared.setAppStep(step: .myBox(currentTab: .orderlist))
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
ScrollViewReader { proxy in
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 8) {
|
||||
ForEach(0..<tabItemList.count, id: \.self) { index in
|
||||
let tabItem = tabItemList[index]
|
||||
|
||||
Text(tabItem.title)
|
||||
.appFont(size: 16, weight: selectedTab == tabItem.tab ? .bold : .medium)
|
||||
.foregroundColor(
|
||||
selectedTab == tabItem.tab ?
|
||||
.button :
|
||||
.graybb
|
||||
)
|
||||
.padding(.horizontal, 12)
|
||||
.onTapGesture {
|
||||
if selectedTab != tabItem.tab {
|
||||
selectedTab = tabItem.tab
|
||||
proxy.scrollTo(tabItem.tab, anchor: .center)
|
||||
}
|
||||
}
|
||||
.id(tabItem.tab)
|
||||
}
|
||||
}
|
||||
.padding(.vertical, 15)
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
.onAppear {
|
||||
withAnimation {
|
||||
proxy.scrollTo(selectedTab, anchor: .center)
|
||||
}
|
||||
}
|
||||
.onChange(of: selectedTab) { newTab in
|
||||
withAnimation {
|
||||
if newTab == .HOME {
|
||||
AppState.shared.back()
|
||||
} else {
|
||||
proxy.scrollTo(newTab, anchor: .center)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ZStack {
|
||||
switch selectedTab {
|
||||
case .HOME:
|
||||
EmptyView()
|
||||
case .SERIES:
|
||||
ContentMainTabSeriesView()
|
||||
case .CONTENT:
|
||||
ContentMainTabContentView()
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if contentPlayerPlayManager.isShowingMiniPlayer {
|
||||
HStack(spacing: 0) {
|
||||
KFImage(URL(string: contentPlayerPlayManager.coverImageUrl))
|
||||
.cancelOnDisappear(true)
|
||||
.downsampling(
|
||||
size: CGSize(
|
||||
width: 36.7,
|
||||
height: 36.7
|
||||
)
|
||||
)
|
||||
.resizable()
|
||||
.frame(width: 36.7, height: 36.7)
|
||||
.cornerRadius(5.3)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2.3) {
|
||||
Text(contentPlayerPlayManager.title)
|
||||
.appFont(size: 13, weight: .medium)
|
||||
.foregroundColor(Color.grayee)
|
||||
.lineLimit(2)
|
||||
|
||||
Text(contentPlayerPlayManager.nickname)
|
||||
.appFont(size: 11, weight: .medium)
|
||||
.foregroundColor(Color.grayd2)
|
||||
}
|
||||
.padding(.horizontal, 10.7)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(contentPlayerPlayManager.isPlaying ? "ic_noti_pause" : "btn_bar_play")
|
||||
.resizable()
|
||||
.frame(width: 25, height: 25)
|
||||
.onTapGesture {
|
||||
contentPlayerPlayManager.playOrPause()
|
||||
}
|
||||
|
||||
Image("ic_noti_stop")
|
||||
.resizable()
|
||||
.frame(width: 25, height: 25)
|
||||
.padding(.leading, 16)
|
||||
.onTapGesture { contentPlayerPlayManager.resetPlayer() }
|
||||
}
|
||||
.padding(.vertical, 10.7)
|
||||
.padding(.horizontal, 13.3)
|
||||
.background(Color.gray22)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
isShowPlayer = true
|
||||
}
|
||||
}
|
||||
|
||||
if contentPlayManager.isShowingMiniPlayer {
|
||||
HStack(spacing: 0) {
|
||||
KFImage(URL(string: contentPlayManager.coverImage))
|
||||
.cancelOnDisappear(true)
|
||||
.downsampling(
|
||||
size: CGSize(
|
||||
width: 36.7,
|
||||
height: 36.7
|
||||
)
|
||||
)
|
||||
.resizable()
|
||||
.frame(width: 36.7, height: 36.7)
|
||||
.cornerRadius(5.3)
|
||||
|
||||
VStack(alignment: .leading, spacing: 2.3) {
|
||||
Text(contentPlayManager.title)
|
||||
.appFont(size: 13, weight: .medium)
|
||||
.foregroundColor(Color.grayee)
|
||||
.lineLimit(2)
|
||||
|
||||
Text(contentPlayManager.nickname)
|
||||
.appFont(size: 11, weight: .medium)
|
||||
.foregroundColor(Color.grayd2)
|
||||
}
|
||||
.padding(.horizontal, 10.7)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image(contentPlayManager.isPlaying ? "ic_noti_pause" : "btn_bar_play")
|
||||
.resizable()
|
||||
.frame(width: 25, height: 25)
|
||||
.onTapGesture {
|
||||
if contentPlayManager.isPlaying {
|
||||
contentPlayManager.pauseAudio()
|
||||
} else {
|
||||
contentPlayManager
|
||||
.playAudio(contentId: contentPlayManager.contentId)
|
||||
}
|
||||
}
|
||||
|
||||
Image("ic_noti_stop")
|
||||
.resizable()
|
||||
.frame(width: 25, height: 25)
|
||||
.padding(.leading, 16)
|
||||
.onTapGesture { contentPlayManager.stopAudio() }
|
||||
}
|
||||
.padding(.vertical, 10.7)
|
||||
.padding(.horizontal, 13.3)
|
||||
.background(Color.gray22)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
AppState.shared
|
||||
.setAppStep(
|
||||
step: .contentDetail(contentId: contentPlayManager.contentId)
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if isShowPlayer {
|
||||
ContentPlayerView(isShowing: $isShowPlayer, playlist: [])
|
||||
}
|
||||
}
|
||||
.navigationBarHidden(true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainViewV2(selectedTab: .SERIES)
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
//
|
||||
// GetContentCurationResponse.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/21/25.
|
||||
//
|
||||
|
||||
struct GetContentCurationResponse: Decodable {
|
||||
let title: String
|
||||
let items: [GetAudioContentMainItem]
|
||||
}
|
||||
@@ -1,72 +0,0 @@
|
||||
//
|
||||
// CompletedSeriesView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/22/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct CompletedSeriesView: View {
|
||||
|
||||
@StateObject var viewModel = CompletedSeriesViewModel()
|
||||
|
||||
private let columns = [
|
||||
GridItem(.flexible()),
|
||||
GridItem(.flexible()),
|
||||
GridItem(.flexible())
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
BaseView(isLoading: $viewModel.isLoading) {
|
||||
VStack(spacing: 13.3) {
|
||||
DetailNavigationBar(title: "완결 시리즈")
|
||||
|
||||
HStack(alignment: .center, spacing: 0) {
|
||||
Text("전체")
|
||||
.appFont(size: 13.3, weight: .medium)
|
||||
.foregroundColor(.graye2)
|
||||
|
||||
Text("\(viewModel.totalCount)")
|
||||
.appFont(size: 13.3, weight: .medium)
|
||||
.foregroundColor(.mainRed)
|
||||
.padding(.leading, 6.7)
|
||||
|
||||
Text("개")
|
||||
.appFont(size: 13.3, weight: .medium)
|
||||
.foregroundColor(.graye2)
|
||||
|
||||
Spacer()
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
LazyVGrid(columns: columns, spacing: 13.3) {
|
||||
ForEach(0..<viewModel.rankCompleteSeriesList.count, id: \.self) { index in
|
||||
let item = viewModel.rankCompleteSeriesList[index]
|
||||
|
||||
SeriesListItemView(
|
||||
itemWidth: (screenSize().width - (13.3 * 4)) / 3,
|
||||
item: item
|
||||
)
|
||||
.onAppear {
|
||||
if index == viewModel.rankCompleteSeriesList.count - 1 {
|
||||
viewModel.getCompletedSeries()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
.onAppear {
|
||||
viewModel.getCompletedSeries()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
CompletedSeriesView()
|
||||
}
|
||||
@@ -1,73 +0,0 @@
|
||||
//
|
||||
// CompletedSeriesViewModel.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/22/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
final class CompletedSeriesViewModel: ObservableObject {
|
||||
private let repository = ContentMainTabSeriesRepository()
|
||||
private var subscription = Set<AnyCancellable>()
|
||||
|
||||
@Published var errorMessage = ""
|
||||
@Published var isShowPopup = false
|
||||
@Published var isLoading = false
|
||||
|
||||
@Published var rankCompleteSeriesList: [SeriesListItem] = []
|
||||
@Published var totalCount = 0
|
||||
|
||||
var isLast = false
|
||||
var page = 1
|
||||
private let size = 20
|
||||
|
||||
func getCompletedSeries() {
|
||||
if !isLast && !isLoading {
|
||||
isLoading = true
|
||||
|
||||
repository.getCompletedSeries(page: page, size: size)
|
||||
.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<GetSeriesListResponse>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
page += 1
|
||||
|
||||
if (data.items.count > 0) {
|
||||
self.totalCount = data.totalCount
|
||||
self.rankCompleteSeriesList = data.items
|
||||
} else {
|
||||
isLast = true
|
||||
}
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,83 +0,0 @@
|
||||
//
|
||||
// ContentMainCompletedSeriesView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/21/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainCompletedSeriesView: View {
|
||||
|
||||
let itemList: [SeriesListItem]
|
||||
let onClickMore: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 13.3) {
|
||||
HStack(spacing: 0) {
|
||||
Text("완결 시리즈")
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(.grayee)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image("ic_forward")
|
||||
.onTapGesture {
|
||||
onClickMore()
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 13.3) {
|
||||
ForEach(0..<itemList.count, id: \.self) { index in
|
||||
let item = itemList[index]
|
||||
SeriesListBigItemView(
|
||||
item: item,
|
||||
isVisibleCreator: true
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainCompletedSeriesView(
|
||||
itemList: [
|
||||
SeriesListItem(
|
||||
seriesId: 1,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: true
|
||||
),
|
||||
SeriesListItem(
|
||||
seriesId: 1,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: true
|
||||
)
|
||||
],
|
||||
onClickMore: {}
|
||||
)
|
||||
}
|
||||
@@ -1,115 +0,0 @@
|
||||
//
|
||||
// ContentMainNewOrRecommendSeriesView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/21/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct ContentMainNewOrRecommendSeriesView: View {
|
||||
|
||||
let title: String
|
||||
let recommendSeriesList: [GetRecommendSeriesListResponse]
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
Text(title)
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 13.3) {
|
||||
ForEach(0..<recommendSeriesList.count, id: \.self) { index in
|
||||
let item = recommendSeriesList[index]
|
||||
ContentMainNewOrRecommendSeriesItemView(item: item)
|
||||
.onTapGesture {
|
||||
AppState.shared
|
||||
.setAppStep(step: .seriesDetail(seriesId: item.seriesId))
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentMainNewOrRecommendSeriesItemView: View {
|
||||
|
||||
let item: GetRecommendSeriesListResponse
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 9) {
|
||||
KFImage(URL(string: item.imageUrl))
|
||||
.cancelOnDisappear(true)
|
||||
.downsampling(size: CGSize(width: 267, height: 141.3))
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 267, height: 141.3)
|
||||
.clipShape(RoundedRectangle(cornerRadius: 5))
|
||||
|
||||
Text(item.title)
|
||||
.appFont(size: 13.3, weight: .medium)
|
||||
.foregroundColor(.grayee)
|
||||
|
||||
HStack(spacing: 5.3) {
|
||||
KFImage(URL(string: item.creatorProfileImageUrl))
|
||||
.cancelOnDisappear(true)
|
||||
.downsampling(size: CGSize(width: 30, height: 30))
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: 30, height: 30)
|
||||
.clipShape(Circle())
|
||||
|
||||
Text(item.creatorNickname)
|
||||
.appFont(size: 10, weight: .medium)
|
||||
.foregroundColor(.gray77)
|
||||
}
|
||||
.onTapGesture {
|
||||
AppState.shared
|
||||
.setAppStep(step: .creatorDetail(userId: item.creatorId))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainNewOrRecommendSeriesView(
|
||||
title: "추천 무료 시리즈",
|
||||
recommendSeriesList: [
|
||||
GetRecommendSeriesListResponse(
|
||||
seriesId: 1,
|
||||
title: "시리즈 1",
|
||||
imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
creatorId: 1,
|
||||
creatorNickname: "유저1",
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
GetRecommendSeriesListResponse(
|
||||
seriesId: 1,
|
||||
title: "시리즈 1",
|
||||
imageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
creatorId: 1,
|
||||
creatorNickname: "유저1",
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
/*
|
||||
contentId: 1,
|
||||
title: "안녕하세요 오늘은 커버곡을 들려드릴께요....안녕하세요 오늘은 커버곡을 들려드릴께요....",
|
||||
coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
themeStr: "커버곡",
|
||||
price: 100,
|
||||
duration: "00:30:20",
|
||||
creatorId: 1,
|
||||
creatorNickname: "유저1",
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
|
||||
|
||||
*/
|
||||
@@ -1,116 +0,0 @@
|
||||
//
|
||||
// ContentMainSeriesByGenreView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/21/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainSeriesByGenreView: View {
|
||||
|
||||
let genreList: [GetSeriesGenreListResponse]
|
||||
let itemList: [SeriesListItem]
|
||||
let onClickGenre: (Int) -> Void
|
||||
|
||||
@State private var selectedGenreId = 0
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
Text("장르별 추천 시리즈")
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(Color.grayee)
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
ContentMainSeriesGenreView(
|
||||
genreList: genreList,
|
||||
selectGenre: {
|
||||
selectedGenreId = $0
|
||||
onClickGenre($0)
|
||||
},
|
||||
selectedGenreId: $selectedGenreId
|
||||
)
|
||||
|
||||
if !itemList.isEmpty {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 13.3) {
|
||||
ForEach(0..<itemList.count, id: \.self) { index in
|
||||
let item = itemList[index]
|
||||
|
||||
SeriesListBigItemView(
|
||||
item: item,
|
||||
isVisibleCreator: true
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
} else {
|
||||
ContentMainNoItemView()
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if !genreList.isEmpty {
|
||||
selectedGenreId = genreList[0].id
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainSeriesByGenreView(
|
||||
genreList: [
|
||||
GetSeriesGenreListResponse(id: 1, genre: "test"),
|
||||
GetSeriesGenreListResponse(id: 2, genre: "test2"),
|
||||
GetSeriesGenreListResponse(id: 3, genre: "test3")
|
||||
],
|
||||
itemList: [
|
||||
SeriesListItem(
|
||||
seriesId: 1,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: false,
|
||||
isPopular: true
|
||||
),
|
||||
SeriesListItem(
|
||||
seriesId: 2,
|
||||
title: "제목2",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "랜덤",
|
||||
isComplete: false,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: false
|
||||
),
|
||||
SeriesListItem(
|
||||
seriesId: 2,
|
||||
title: "제목2",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "랜덤",
|
||||
isComplete: false,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: false
|
||||
)
|
||||
],
|
||||
onClickGenre: { _ in }
|
||||
)
|
||||
}
|
||||
@@ -1,158 +0,0 @@
|
||||
//
|
||||
// ContentMainSeriesCurationView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/21/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainSeriesCurationView: View {
|
||||
|
||||
let curationList: [GetSeriesCurationResponse]
|
||||
|
||||
var body: some View {
|
||||
LazyVStack(spacing: 30) {
|
||||
ForEach(0..<curationList.count, id: \.self) { index in
|
||||
let curation = curationList[index]
|
||||
ContentMainSeriesCurationItemView(curation: curation)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
struct ContentMainSeriesCurationItemView: View {
|
||||
|
||||
let curation: GetSeriesCurationResponse
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
Text(curation.title)
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(.grayee)
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 13.3) {
|
||||
ForEach(0..<curation.items.count, id: \.self) { index in
|
||||
let item = curation.items[index]
|
||||
SeriesListBigItemView(item: item, isVisibleCreator: true)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainSeriesCurationView(
|
||||
curationList: [
|
||||
GetSeriesCurationResponse(
|
||||
title: "test",
|
||||
items: [
|
||||
SeriesListItem(
|
||||
seriesId: 1,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: true
|
||||
),
|
||||
SeriesListItem(
|
||||
seriesId: 1,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: true
|
||||
)
|
||||
]
|
||||
),
|
||||
GetSeriesCurationResponse(
|
||||
title: "test2",
|
||||
items: [
|
||||
SeriesListItem(
|
||||
seriesId: 1,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: true
|
||||
),
|
||||
SeriesListItem(
|
||||
seriesId: 1,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: true
|
||||
)
|
||||
]
|
||||
),
|
||||
GetSeriesCurationResponse(
|
||||
title: "test3",
|
||||
items: [
|
||||
SeriesListItem(
|
||||
seriesId: 1,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: true
|
||||
),
|
||||
SeriesListItem(
|
||||
seriesId: 1,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: true
|
||||
)
|
||||
]
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
@@ -1,58 +0,0 @@
|
||||
//
|
||||
// ContentMainSeriesGenreView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/21/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainSeriesGenreView: View {
|
||||
let genreList: [GetSeriesGenreListResponse]
|
||||
let selectGenre: (Int) -> Void
|
||||
|
||||
@Binding var selectedGenreId: Int
|
||||
|
||||
var body: some View {
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(alignment: .top, spacing: 8) {
|
||||
ForEach(0..<genreList.count, id: \.self) { index in
|
||||
let genre = genreList[index]
|
||||
Text(genre.genre)
|
||||
.appFont(size: 14.7, weight: .medium)
|
||||
.foregroundColor(selectedGenreId == genre.id ? Color.button : Color.gray77)
|
||||
.padding(.horizontal, 13.3)
|
||||
.padding(.vertical, 9.3)
|
||||
.border(
|
||||
selectedGenreId == genre.id ? Color.button : Color.grayee,
|
||||
width: 1
|
||||
)
|
||||
.cornerRadius(16.7)
|
||||
.overlay(
|
||||
RoundedRectangle(cornerRadius: CGFloat(16.7))
|
||||
.stroke(lineWidth: 1)
|
||||
.foregroundColor(selectedGenreId == genre.id ? Color.button : Color.grayee)
|
||||
)
|
||||
.onTapGesture {
|
||||
if selectedGenreId != genre.id {
|
||||
selectGenre(genre.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainSeriesGenreView(
|
||||
genreList: [
|
||||
GetSeriesGenreListResponse(id: 1, genre: "test"),
|
||||
GetSeriesGenreListResponse(id: 2, genre: "test2"),
|
||||
GetSeriesGenreListResponse(id: 3, genre: "test3")
|
||||
],
|
||||
selectGenre: { _ in },
|
||||
selectedGenreId: .constant(2)
|
||||
)
|
||||
}
|
||||
@@ -1,141 +0,0 @@
|
||||
//
|
||||
// ContentMainSeriesRankingView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/21/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct ContentMainSeriesRankingView: View {
|
||||
|
||||
let seriesList: [SeriesListItem]
|
||||
|
||||
let rows = [
|
||||
GridItem(.fixed(85), alignment: .leading),
|
||||
GridItem(.fixed(85), alignment: .leading),
|
||||
GridItem(.fixed(85), alignment: .leading)
|
||||
]
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
Text("일간 랭킹")
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(.grayee)
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
LazyHGrid(rows: rows, spacing: 13.3) {
|
||||
ForEach(0..<seriesList.count, id: \.self) { index in
|
||||
let series = seriesList[index]
|
||||
HStack(spacing: 0) {
|
||||
KFImage(URL(string: series.coverImage))
|
||||
.cancelOnDisappear(true)
|
||||
.downsampling(
|
||||
size: CGSize(
|
||||
width: 60,
|
||||
height: 85
|
||||
)
|
||||
)
|
||||
.resizable()
|
||||
.frame(width: 60, height: 85)
|
||||
.cornerRadius(2.7)
|
||||
|
||||
Text("\(index + 1)")
|
||||
.appFont(size: 16.7, weight: .bold)
|
||||
.foregroundColor(.button)
|
||||
.padding(.horizontal, 12)
|
||||
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
Text(series.title)
|
||||
.lineLimit(2)
|
||||
.appFont(size: 13.3, weight: .medium)
|
||||
.foregroundColor(.grayd2)
|
||||
|
||||
Text(series.creator.nickname)
|
||||
.appFont(size: 11, weight: .medium)
|
||||
.foregroundColor(.gray77)
|
||||
}
|
||||
}
|
||||
.frame(width: screenSize().width * 0.66, alignment: .leading)
|
||||
.contentShape(Rectangle())
|
||||
.onTapGesture {
|
||||
AppState
|
||||
.shared
|
||||
.setAppStep(step: .seriesDetail(seriesId: series.seriesId))
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainSeriesRankingView(
|
||||
seriesList: [
|
||||
SeriesListItem(
|
||||
seriesId: 1,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: true
|
||||
),
|
||||
SeriesListItem(
|
||||
seriesId: 1,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: true
|
||||
),
|
||||
SeriesListItem(
|
||||
seriesId: 3,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: true
|
||||
),
|
||||
SeriesListItem(
|
||||
seriesId: 4,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: true
|
||||
)
|
||||
]
|
||||
)
|
||||
}
|
||||
@@ -1,55 +0,0 @@
|
||||
//
|
||||
// ContentMainTabSeriesRepository.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/20/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import CombineMoya
|
||||
import Combine
|
||||
import Moya
|
||||
|
||||
final class ContentMainTabSeriesRepository {
|
||||
private let api = MoyaProvider<ContentApi>()
|
||||
|
||||
func getContentMainSeries() -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getContentMainSeries(
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func getRecommendSeriesListByGenre(genreId: Int) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getRecommendSeriesListByGenre(
|
||||
genreId: genreId,
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func getRecommendSeriesByCreator(creatorId: Int) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getRecommendSeriesByCreator(
|
||||
creatorId: creatorId,
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
||||
)
|
||||
)
|
||||
}
|
||||
|
||||
func getCompletedSeries(page: Int, size: Int) -> AnyPublisher<Response, MoyaError> {
|
||||
return api.requestPublisher(
|
||||
.getCompletedSeries(
|
||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL,
|
||||
page: page,
|
||||
size: size
|
||||
)
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -1,95 +0,0 @@
|
||||
//
|
||||
// ContentMainTabSeriesView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/20/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainTabSeriesView: View {
|
||||
|
||||
@StateObject var viewModel = ContentMainTabSeriesViewModel()
|
||||
|
||||
var body: some View {
|
||||
BaseView(isLoading: $viewModel.isLoading) {
|
||||
ScrollView(.vertical, showsIndicators: false) {
|
||||
VStack(spacing: 0) {
|
||||
if !viewModel.bannerList.isEmpty {
|
||||
ContentMainBannerViewV2(bannerList: viewModel.bannerList)
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
|
||||
if !viewModel.originalAudioDramaList.isEmpty {
|
||||
ContentMainOriginalAudioDramaView(itemList: viewModel.originalAudioDramaList) {
|
||||
}
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.rankSeriesList.isEmpty {
|
||||
ContentMainSeriesRankingView(seriesList: viewModel.rankSeriesList)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.genreList.isEmpty {
|
||||
ContentMainSeriesByGenreView(
|
||||
genreList: viewModel.genreList,
|
||||
itemList: viewModel.recommendSeriesList
|
||||
) {
|
||||
viewModel.getRecommendSeriesListByGenre(genreId: $0)
|
||||
}
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.newSeriesList.isEmpty {
|
||||
ContentMainNewOrRecommendSeriesView(
|
||||
title: "새로운 시리즈",
|
||||
recommendSeriesList: viewModel.newSeriesList
|
||||
)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.rankCompleteSeriesList.isEmpty {
|
||||
ContentMainCompletedSeriesView(
|
||||
itemList: viewModel.rankCompleteSeriesList,
|
||||
onClickMore: {
|
||||
AppState.shared
|
||||
.setAppStep(step: .completedSeriesAll)
|
||||
}
|
||||
)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.seriesRankCreatorList.isEmpty {
|
||||
SeriesByChannelView(
|
||||
title: "채널별 추천 시리즈",
|
||||
creatorList: viewModel.seriesRankCreatorList,
|
||||
seriesList: viewModel.recommendSeriesByChannel
|
||||
) {
|
||||
viewModel.getRecommendSeriesByCreator(creatorId: $0)
|
||||
}
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.eventBannerList.isEmpty {
|
||||
SectionEventBannerView(items: viewModel.eventBannerList)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
|
||||
if !viewModel.curationList.isEmpty {
|
||||
ContentMainSeriesCurationView(curationList: viewModel.curationList)
|
||||
.padding(.top, 30)
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
viewModel.fetchData()
|
||||
}
|
||||
}
|
||||
}
|
||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainTabSeriesView()
|
||||
}
|
||||
@@ -1,157 +0,0 @@
|
||||
//
|
||||
// ContentMainTabSeriesViewModel.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/20/25.
|
||||
//
|
||||
|
||||
import Foundation
|
||||
import Combine
|
||||
|
||||
final class ContentMainTabSeriesViewModel: ObservableObject {
|
||||
private let repository = ContentMainTabSeriesRepository()
|
||||
private let contentRepository = ContentRepository()
|
||||
private var subscription = Set<AnyCancellable>()
|
||||
|
||||
@Published var errorMessage = ""
|
||||
@Published var isShowPopup = false
|
||||
@Published var isLoading = false
|
||||
|
||||
@Published var bannerList: [GetAudioContentBannerResponse] = []
|
||||
@Published var originalAudioDramaList: [SeriesListItem] = []
|
||||
@Published var rankSeriesList: [SeriesListItem] = []
|
||||
@Published var genreList: [GetSeriesGenreListResponse] = []
|
||||
@Published var recommendSeriesList: [SeriesListItem] = []
|
||||
@Published var newSeriesList: [GetRecommendSeriesListResponse] = []
|
||||
@Published var rankCompleteSeriesList: [SeriesListItem] = []
|
||||
@Published var seriesRankCreatorList: [ContentCreatorResponse] = []
|
||||
@Published var recommendSeriesByChannel: [SeriesListItem] = []
|
||||
@Published var eventBannerList: [EventItem] = []
|
||||
@Published var curationList: [GetSeriesCurationResponse] = []
|
||||
|
||||
func fetchData() {
|
||||
isLoading = true
|
||||
|
||||
repository.getContentMainSeries()
|
||||
.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<GetContentMainTabSeriesResponse>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.bannerList = data.contentBannerList
|
||||
self.originalAudioDramaList = data.originalAudioDrama
|
||||
self.rankSeriesList = data.rankSeriesList
|
||||
self.genreList = data.genreList
|
||||
self.recommendSeriesList = data.recommendSeriesList
|
||||
self.newSeriesList = data.newSeriesList
|
||||
self.rankCompleteSeriesList = data.rankCompleteSeriesList
|
||||
self.seriesRankCreatorList = data.seriesRankCreatorList
|
||||
self.recommendSeriesByChannel = data.recommendSeriesByChannel
|
||||
self.eventBannerList = data.eventBannerList.eventList
|
||||
self.curationList = data.curationList
|
||||
} 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 getRecommendSeriesListByGenre(genreId: Int) {
|
||||
isLoading = true
|
||||
repository.getRecommendSeriesListByGenre(genreId: genreId)
|
||||
.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<[SeriesListItem]>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.recommendSeriesList = data
|
||||
} 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 getRecommendSeriesByCreator(creatorId: Int) {
|
||||
recommendSeriesByChannel = []
|
||||
isLoading = true
|
||||
repository.getRecommendSeriesByCreator(creatorId: creatorId)
|
||||
.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<[SeriesListItem]>.self, from: responseData)
|
||||
|
||||
if let data = decoded.data, decoded.success {
|
||||
self.recommendSeriesByChannel = data
|
||||
} 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)
|
||||
}
|
||||
}
|
||||
@@ -5,35 +5,7 @@
|
||||
// Created by klaus on 2/20/25.
|
||||
//
|
||||
|
||||
struct GetContentMainTabSeriesResponse: Decodable {
|
||||
let contentBannerList: [GetAudioContentBannerResponse]
|
||||
let originalAudioDrama: [SeriesListItem]
|
||||
let rankSeriesList: [SeriesListItem]
|
||||
let genreList: [GetSeriesGenreListResponse]
|
||||
let recommendSeriesList: [SeriesListItem]
|
||||
let newSeriesList: [GetRecommendSeriesListResponse]
|
||||
let rankCompleteSeriesList: [SeriesListItem]
|
||||
let seriesRankCreatorList: [ContentCreatorResponse]
|
||||
let recommendSeriesByChannel: [SeriesListItem]
|
||||
let eventBannerList: GetEventResponse
|
||||
let curationList: [GetSeriesCurationResponse]
|
||||
}
|
||||
|
||||
struct GetSeriesGenreListResponse: Decodable {
|
||||
let id: Int
|
||||
let genre: String
|
||||
}
|
||||
|
||||
struct GetRecommendSeriesListResponse: Decodable {
|
||||
let seriesId: Int
|
||||
let title: String
|
||||
let imageUrl: String
|
||||
let creatorId: Int
|
||||
let creatorNickname: String
|
||||
let creatorProfileImageUrl: String
|
||||
}
|
||||
|
||||
struct GetSeriesCurationResponse: Decodable {
|
||||
let title: String
|
||||
let items: [SeriesListItem]
|
||||
}
|
||||
|
||||
@@ -1,108 +0,0 @@
|
||||
//
|
||||
// ContentMainOriginalAudioDramaItemView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/21/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
import Kingfisher
|
||||
|
||||
struct ContentMainOriginalAudioDramaItemView: View {
|
||||
|
||||
let itemWidth: CGFloat
|
||||
let item: SeriesListItem
|
||||
let isAll: Bool
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 8) {
|
||||
ZStack {
|
||||
KFImage(URL(string: item.coverImage))
|
||||
.cancelOnDisappear(true)
|
||||
.downsampling(
|
||||
size: CGSize(
|
||||
width: itemWidth,
|
||||
height: (itemWidth * 636) / 450
|
||||
)
|
||||
)
|
||||
.resizable()
|
||||
.scaledToFill()
|
||||
.frame(width: itemWidth, height: (itemWidth * 636) / 450, alignment: .center)
|
||||
.cornerRadius(5)
|
||||
.clipped()
|
||||
.onTapGesture {
|
||||
AppState.shared
|
||||
.setAppStep(step: .seriesDetail(seriesId: item.seriesId))
|
||||
}
|
||||
|
||||
VStack(alignment: .leading, spacing: 0) {
|
||||
HStack(spacing: 3.3) {
|
||||
if !item.isComplete && item.isNew {
|
||||
SeriesItemBadgeView(title: "신작", backgroundColor: .button)
|
||||
}
|
||||
|
||||
if item.isComplete {
|
||||
SeriesItemBadgeView(title: "완결", backgroundColor: Color(hex: "002abd"))
|
||||
}
|
||||
|
||||
if item.isPopular {
|
||||
SeriesItemBadgeView(title: "인기", backgroundColor: Color(hex: "ec6033"))
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
if !isAll {
|
||||
SeriesItemBadgeView(title: "총 \(item.numberOfContent)화", backgroundColor: Color.gray33.opacity(0.7))
|
||||
}
|
||||
}
|
||||
|
||||
Spacer()
|
||||
|
||||
HStack {
|
||||
Spacer()
|
||||
|
||||
if isAll {
|
||||
SeriesItemBadgeView(title: "총 \(item.numberOfContent)화", backgroundColor: Color.gray33.opacity(0.7))
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(3.3)
|
||||
}
|
||||
.frame(width: itemWidth, height: (itemWidth * 636) / 450, alignment: .center)
|
||||
|
||||
Text(item.title)
|
||||
.appFont(size: 12, weight: .medium)
|
||||
.foregroundColor(Color.grayee)
|
||||
.lineLimit(1)
|
||||
|
||||
if isAll {
|
||||
Text(item.publishedDaysOfWeek)
|
||||
.appFont(size: 11, weight: .medium)
|
||||
.foregroundColor(Color.gray77)
|
||||
}
|
||||
}
|
||||
.frame(width: itemWidth)
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainOriginalAudioDramaItemView(
|
||||
itemWidth: 150,
|
||||
item: SeriesListItem(
|
||||
seriesId: 1,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: false,
|
||||
isPopular: true
|
||||
),
|
||||
isAll: false
|
||||
)
|
||||
}
|
||||
@@ -1,82 +0,0 @@
|
||||
//
|
||||
// ContentMainOriginalAudioDramaView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/21/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct ContentMainOriginalAudioDramaView: View {
|
||||
|
||||
let itemList: [SeriesListItem]
|
||||
let onClickMore: () -> Void
|
||||
|
||||
var body: some View {
|
||||
VStack(spacing: 13.3) {
|
||||
HStack(spacing: 0) {
|
||||
Text("오리지널 오디오 드라마")
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(.grayee)
|
||||
|
||||
Spacer()
|
||||
|
||||
Image("ic_forward")
|
||||
.onTapGesture { onClickMore() }
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
ScrollView(.horizontal, showsIndicators: false) {
|
||||
HStack(spacing: 13.3) {
|
||||
ForEach(0..<itemList.count, id: \.self) { index in
|
||||
let item = itemList[index]
|
||||
|
||||
ContentMainOriginalAudioDramaItemView(
|
||||
itemWidth: 150,
|
||||
item: item,
|
||||
isAll: false
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
ContentMainOriginalAudioDramaView(
|
||||
itemList: [
|
||||
SeriesListItem(
|
||||
seriesId: 1,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: false,
|
||||
isPopular: true
|
||||
),
|
||||
SeriesListItem(
|
||||
seriesId: 2,
|
||||
title: "제목2",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "랜덤",
|
||||
isComplete: false,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: false
|
||||
)
|
||||
]
|
||||
) {}
|
||||
}
|
||||
@@ -1,123 +0,0 @@
|
||||
//
|
||||
// SeriesByChannelView.swift
|
||||
// SodaLive
|
||||
//
|
||||
// Created by klaus on 2/21/25.
|
||||
//
|
||||
|
||||
import SwiftUI
|
||||
|
||||
struct SeriesByChannelView: View {
|
||||
|
||||
let title: String
|
||||
let creatorList: [ContentCreatorResponse]
|
||||
let seriesList: [SeriesListItem]
|
||||
let onClickCreator: (Int) -> Void
|
||||
|
||||
@State private var selectedCreatorId = 0
|
||||
|
||||
var body: some View {
|
||||
VStack(alignment: .leading, spacing: 13.3) {
|
||||
Text(title)
|
||||
.appFont(size: 18.3, weight: .bold)
|
||||
.foregroundColor(Color(hex: "eeeeee"))
|
||||
.padding(.horizontal, 13.3)
|
||||
|
||||
ScrollView(.horizontal) {
|
||||
HStack(spacing: 22) {
|
||||
ForEach(0..<creatorList.count, id: \.self) { index in
|
||||
let item = creatorList[index]
|
||||
|
||||
ContentCreatorView(
|
||||
isSelected: item.creatorId == selectedCreatorId,
|
||||
item: item
|
||||
)
|
||||
.onTapGesture {
|
||||
let creatorId = item.creatorId
|
||||
|
||||
if creatorId != selectedCreatorId {
|
||||
selectedCreatorId = creatorId
|
||||
onClickCreator(creatorId)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
|
||||
if seriesList.isEmpty {
|
||||
ContentMainNoItemView()
|
||||
} else {
|
||||
ScrollView(.horizontal) {
|
||||
HStack(spacing: 13.3) {
|
||||
ForEach(0..<seriesList.count, id: \.self) { index in
|
||||
let item = seriesList[index]
|
||||
|
||||
SeriesListBigItemView(
|
||||
item: item,
|
||||
isVisibleCreator: true
|
||||
)
|
||||
}
|
||||
}
|
||||
.padding(.horizontal, 13.3)
|
||||
}
|
||||
}
|
||||
}
|
||||
.onAppear {
|
||||
if !self.creatorList.isEmpty {
|
||||
selectedCreatorId = creatorList[0].creatorId
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#Preview {
|
||||
SeriesByChannelView(
|
||||
title: "채널별 추천 시리즈",
|
||||
creatorList: [
|
||||
ContentCreatorResponse(
|
||||
creatorId: 1,
|
||||
creatorNickname: "유저1",
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
ContentCreatorResponse(
|
||||
creatorId: 2,
|
||||
creatorNickname: "유저2",
|
||||
creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
)
|
||||
],
|
||||
seriesList: [
|
||||
SeriesListItem(
|
||||
seriesId: 1,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: true
|
||||
),
|
||||
SeriesListItem(
|
||||
seriesId: 1,
|
||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
||||
coverImage: "https://test-cf.sodalive.net/profile/default-profile.png",
|
||||
publishedDaysOfWeek: "매주 수, 토요일",
|
||||
isComplete: true,
|
||||
creator: SeriesListItemCreator(
|
||||
creatorId: 1,
|
||||
nickname: "creator",
|
||||
profileImage: "https://test-cf.sodalive.net/profile/default-profile.png"
|
||||
),
|
||||
numberOfContent: 10,
|
||||
isNew: true,
|
||||
isPopular: true
|
||||
)
|
||||
],
|
||||
onClickCreator: { _ in }
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user