refactor: 사용하지 않는 파일 삭제
This commit is contained in:
@@ -1276,6 +1276,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"※ 인기 순위는 매주 업데이트됩니다." : {
|
"※ 인기 순위는 매주 업데이트됩니다." : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1324,6 +1325,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"※ 최근 2주간 등록된 새로운 ASMR 입니다." : {
|
"※ 최근 2주간 등록된 새로운 ASMR 입니다." : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1340,6 +1342,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"※ 최근 2주간 등록된 새로운 라이브 다시듣기 입니다." : {
|
"※ 최근 2주간 등록된 새로운 라이브 다시듣기 입니다." : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -1356,6 +1359,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"※ 최근 2주간 등록된 새로운 알람 입니다." : {
|
"※ 최근 2주간 등록된 새로운 알람 입니다." : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -4129,18 +4133,18 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"모집완료" : {
|
"모든 기기에서 로그아웃" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Recruitment closed"
|
"value" : "Log out from all devices"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ja" : {
|
"ja" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "募集終了"
|
"value" : "全端末からログアウト"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -4161,24 +4165,24 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"모든 기기에서 로그아웃" : {
|
"모서리 원을 드래그해서 크롭 영역 크기를 조정하세요" : {
|
||||||
|
|
||||||
|
},
|
||||||
|
"모집완료" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "Log out from all devices"
|
"value" : "Recruitment closed"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"ja" : {
|
"ja" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
"state" : "translated",
|
"state" : "translated",
|
||||||
"value" : "全端末からログアウト"
|
"value" : "募集終了"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
|
||||||
"모서리 원을 드래그해서 크롭 영역 크기를 조정하세요" : {
|
|
||||||
|
|
||||||
},
|
},
|
||||||
"모집중" : {
|
"모집중" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
@@ -7051,6 +7055,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"인기 시리즈" : {
|
"인기 시리즈" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -7339,6 +7344,7 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"자세히 >" : {
|
"자세히 >" : {
|
||||||
|
"extractionState" : "stale",
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
"stringUnit" : {
|
"stringUnit" : {
|
||||||
@@ -8656,22 +8662,6 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"캐릭터 정보" : {
|
|
||||||
"localizations" : {
|
|
||||||
"en" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "Character info"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"ja" : {
|
|
||||||
"stringUnit" : {
|
|
||||||
"state" : "translated",
|
|
||||||
"value" : "キャラクター情報"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"캔" : {
|
"캔" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
@@ -8688,6 +8678,22 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"캐릭터 정보" : {
|
||||||
|
"localizations" : {
|
||||||
|
"en" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "Character info"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"ja" : {
|
||||||
|
"stringUnit" : {
|
||||||
|
"state" : "translated",
|
||||||
|
"value" : "キャラクター情報"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
"캔 충전" : {
|
"캔 충전" : {
|
||||||
"localizations" : {
|
"localizations" : {
|
||||||
"en" : {
|
"en" : {
|
||||||
|
|||||||
@@ -154,14 +154,6 @@ enum AppStep {
|
|||||||
|
|
||||||
case completedSeriesAll
|
case completedSeriesAll
|
||||||
|
|
||||||
case newAlarmContentAll
|
|
||||||
|
|
||||||
case newAsmrContentAll
|
|
||||||
|
|
||||||
case newReplayContentAll
|
|
||||||
|
|
||||||
case introduceCreatorAll
|
|
||||||
|
|
||||||
case message
|
case message
|
||||||
|
|
||||||
case notificationList
|
case notificationList
|
||||||
|
|||||||
@@ -1,98 +0,0 @@
|
|||||||
//
|
|
||||||
// ContentMainAlarmAllView.swift
|
|
||||||
// SodaLive
|
|
||||||
//
|
|
||||||
// Created by klaus on 2/22/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct ContentMainAlarmAllView: View {
|
|
||||||
|
|
||||||
@StateObject var viewModel = ContentMainAlarmAllViewModel()
|
|
||||||
@State private var isInitialized = false
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Group {
|
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
|
||||||
VStack(alignment: .leading, spacing: 13.3) {
|
|
||||||
DetailNavigationBar(title: "새로운 알람")
|
|
||||||
|
|
||||||
Text("※ 최근 2주간 등록된 새로운 알람 입니다.")
|
|
||||||
.appFont(size: 14.7, weight: .medium)
|
|
||||||
.foregroundColor(.graybb)
|
|
||||||
.padding(.horizontal, 13.3)
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
.frame(width: screenSize().width, alignment: .leading)
|
|
||||||
.background(Color.gray22)
|
|
||||||
|
|
||||||
ContentMainNewContentThemeView(
|
|
||||||
themes: viewModel.themeList,
|
|
||||||
selectTheme: {
|
|
||||||
viewModel.selectedTheme = $0
|
|
||||||
},
|
|
||||||
selectedTheme: $viewModel.selectedTheme
|
|
||||||
)
|
|
||||||
.padding(.horizontal, 20)
|
|
||||||
|
|
||||||
HStack(spacing: 0) {
|
|
||||||
Text("전체")
|
|
||||||
.appFont(size: 13.3, weight: .medium)
|
|
||||||
.foregroundColor(Color(hex: "e2e2e2"))
|
|
||||||
|
|
||||||
Text("\(viewModel.totalCount)")
|
|
||||||
.appFont(size: 13.3, weight: .medium)
|
|
||||||
.foregroundColor(Color(hex: "ff5c49"))
|
|
||||||
.padding(.leading, 8)
|
|
||||||
|
|
||||||
Text("개")
|
|
||||||
.appFont(size: 13.3, weight: .medium)
|
|
||||||
.foregroundColor(Color(hex: "e2e2e2"))
|
|
||||||
.padding(.leading, 2)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 13.3)
|
|
||||||
|
|
||||||
ScrollView(.vertical, showsIndicators: false) {
|
|
||||||
let horizontalPadding: CGFloat = 16
|
|
||||||
let gridSpacing: CGFloat = 16
|
|
||||||
let itemSize = (screenSize().width - (horizontalPadding * 2) - gridSpacing) / 2
|
|
||||||
|
|
||||||
LazyVGrid(
|
|
||||||
columns: Array(
|
|
||||||
repeating: GridItem(
|
|
||||||
.flexible(),
|
|
||||||
spacing: gridSpacing,
|
|
||||||
alignment: .topLeading
|
|
||||||
),
|
|
||||||
count: 2
|
|
||||||
),
|
|
||||||
alignment: .leading,
|
|
||||||
spacing: gridSpacing
|
|
||||||
) {
|
|
||||||
ForEach(0..<viewModel.newContentList.count, id: \.self) { index in
|
|
||||||
ContentNewAllItemView(width: itemSize, item: viewModel.newContentList[index])
|
|
||||||
.onAppear {
|
|
||||||
if index == viewModel.newContentList.count - 1 {
|
|
||||||
viewModel.getContentMainAlarmAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 13.3)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onAppear {
|
|
||||||
if !isInitialized {
|
|
||||||
viewModel.getContentMainAlarmAll()
|
|
||||||
isInitialized = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navigationBarHidden(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
ContentMainAlarmAllView()
|
|
||||||
}
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
//
|
|
||||||
// ContentMainAlarmAllViewModel.swift
|
|
||||||
// SodaLive
|
|
||||||
//
|
|
||||||
// Created by klaus on 2/22/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Combine
|
|
||||||
|
|
||||||
final class ContentMainAlarmAllViewModel: ObservableObject {
|
|
||||||
|
|
||||||
private let repository = ContentMainTabAlarmRepository()
|
|
||||||
private var subscription = Set<AnyCancellable>()
|
|
||||||
|
|
||||||
@Published var errorMessage = ""
|
|
||||||
@Published var isShowPopup = false
|
|
||||||
@Published var isLoading = false
|
|
||||||
|
|
||||||
@Published var themeList = ["전체", "모닝콜", "슬립콜", "알람"]
|
|
||||||
@Published var newContentList = [GetAudioContentMainItem]()
|
|
||||||
|
|
||||||
@Published var selectedTheme = "전체" {
|
|
||||||
didSet {
|
|
||||||
page = 1
|
|
||||||
isLast = false
|
|
||||||
getContentMainAlarmAll()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@Published var totalCount = 0
|
|
||||||
|
|
||||||
var page = 1
|
|
||||||
var isLast = false
|
|
||||||
private let pageSize = 20
|
|
||||||
|
|
||||||
func getContentMainAlarmAll() {
|
|
||||||
if (!isLast && !isLoading) {
|
|
||||||
isLoading = true
|
|
||||||
|
|
||||||
repository.getContentMainAlarmAll(
|
|
||||||
theme: selectedTheme == "전체" ? "" : selectedTheme,
|
|
||||||
page: page,
|
|
||||||
size: pageSize
|
|
||||||
)
|
|
||||||
.sink { result in
|
|
||||||
switch result {
|
|
||||||
case .finished:
|
|
||||||
DEBUG_LOG("finish")
|
|
||||||
case .failure(let error):
|
|
||||||
ERROR_LOG(error.localizedDescription)
|
|
||||||
}
|
|
||||||
} receiveValue: { [unowned self] response in
|
|
||||||
self.isLoading = false
|
|
||||||
let responseData = response.data
|
|
||||||
|
|
||||||
do {
|
|
||||||
let jsonDecoder = JSONDecoder()
|
|
||||||
let decoded = try jsonDecoder.decode(ApiResponse<GetNewContentAllResponse>.self, from: responseData)
|
|
||||||
self.isLoading = false
|
|
||||||
|
|
||||||
if let data = decoded.data, decoded.success {
|
|
||||||
if page == 1 {
|
|
||||||
newContentList.removeAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
self.totalCount = data.totalCount
|
|
||||||
|
|
||||||
if !data.items.isEmpty {
|
|
||||||
page += 1
|
|
||||||
self.newContentList.append(contentsOf: 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)
|
|
||||||
} else {
|
|
||||||
isLoading = false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
//
|
|
||||||
// ContentMainTabAlarmRepository.swift
|
|
||||||
// SodaLive
|
|
||||||
//
|
|
||||||
// Created by klaus on 2/22/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import CombineMoya
|
|
||||||
import Combine
|
|
||||||
import Moya
|
|
||||||
|
|
||||||
final class ContentMainTabAlarmRepository {
|
|
||||||
|
|
||||||
private let api = MoyaProvider<ContentApi>()
|
|
||||||
|
|
||||||
func getContentMainAlarm() -> AnyPublisher<Response, MoyaError> {
|
|
||||||
return api.requestPublisher(
|
|
||||||
.getContentMainAlarm(
|
|
||||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
|
||||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getContentMainAlarmAll(theme: String, page: Int = 1, size: Int = 10) -> AnyPublisher<Response, MoyaError> {
|
|
||||||
return api.requestPublisher(
|
|
||||||
.getContentMainAlarmAll(
|
|
||||||
theme: theme,
|
|
||||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
|
||||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL,
|
|
||||||
page: page,
|
|
||||||
size: size
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
//
|
|
||||||
// ContentMainTabAlarmView.swift
|
|
||||||
// SodaLive
|
|
||||||
//
|
|
||||||
// Created by klaus on 2/22/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct ContentMainTabAlarmView: View {
|
|
||||||
|
|
||||||
@StateObject var viewModel = ContentMainTabAlarmViewModel()
|
|
||||||
|
|
||||||
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.alarmThemeList.isEmpty {
|
|
||||||
ContentMainNewContentViewV2(
|
|
||||||
title: "새로운 알람",
|
|
||||||
onClickMore: {
|
|
||||||
AppState.shared
|
|
||||||
.setAppStep(step: .newAlarmContentAll)
|
|
||||||
},
|
|
||||||
themeList: viewModel.alarmThemeList,
|
|
||||||
contentList: viewModel.newAlarmContentList
|
|
||||||
) {
|
|
||||||
viewModel.getContentMainAlarm(theme: $0)
|
|
||||||
}
|
|
||||||
.padding(.top, 30)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !viewModel.eventBannerList.isEmpty {
|
|
||||||
SectionEventBannerView(items: viewModel.eventBannerList)
|
|
||||||
.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 {
|
|
||||||
ContentMainTabAlarmView()
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
//
|
|
||||||
// ContentMainTabAlarmViewModel.swift
|
|
||||||
// SodaLive
|
|
||||||
//
|
|
||||||
// Created by klaus on 2/22/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Combine
|
|
||||||
|
|
||||||
final class ContentMainTabAlarmViewModel: ObservableObject {
|
|
||||||
private let repository = ContentMainTabAlarmRepository()
|
|
||||||
private var subscription = Set<AnyCancellable>()
|
|
||||||
|
|
||||||
@Published var errorMessage = ""
|
|
||||||
@Published var isShowPopup = false
|
|
||||||
@Published var isLoading = false
|
|
||||||
|
|
||||||
@Published var bannerList: [GetAudioContentBannerResponse] = []
|
|
||||||
@Published var alarmThemeList: [String] = []
|
|
||||||
@Published var newAlarmContentList: [GetAudioContentMainItem] = []
|
|
||||||
@Published var rankAlarmContentList: [GetAudioContentRankingItem] = []
|
|
||||||
@Published var eventBannerList: [EventItem] = []
|
|
||||||
@Published var curationList: [GetContentCurationResponse] = []
|
|
||||||
|
|
||||||
func fetchData() {
|
|
||||||
isLoading = true
|
|
||||||
repository.getContentMainAlarm()
|
|
||||||
.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<GetContentMainTabAlarmResponse>.self, from: responseData)
|
|
||||||
|
|
||||||
if let data = decoded.data, decoded.success {
|
|
||||||
self.bannerList = data.contentBannerList
|
|
||||||
self.alarmThemeList = ["전체"] + data.alarmThemeList
|
|
||||||
self.newAlarmContentList = data.newAlarmContentList
|
|
||||||
self.rankAlarmContentList = data.rankAlarmContentList
|
|
||||||
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 getContentMainAlarm(theme: String) {
|
|
||||||
isLoading = true
|
|
||||||
repository.getContentMainAlarmAll(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<GetNewContentAllResponse>.self, from: responseData)
|
|
||||||
|
|
||||||
if let data = decoded.data, decoded.success {
|
|
||||||
self.newAlarmContentList = 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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
//
|
|
||||||
// GetContentMainTabAlarmResponse.swift
|
|
||||||
// SodaLive
|
|
||||||
//
|
|
||||||
// Created by klaus on 2/22/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
|
|
||||||
struct GetContentMainTabAlarmResponse: Decodable {
|
|
||||||
let contentBannerList: [GetAudioContentBannerResponse]
|
|
||||||
let alarmThemeList: [String]
|
|
||||||
let newAlarmContentList: [GetAudioContentMainItem]
|
|
||||||
let rankAlarmContentList: [GetAudioContentRankingItem]
|
|
||||||
let eventBannerList: GetEventResponse
|
|
||||||
let curationList: [GetContentCurationResponse]
|
|
||||||
}
|
|
||||||
@@ -1,93 +0,0 @@
|
|||||||
//
|
|
||||||
// ContentMainAsmrAllView.swift
|
|
||||||
// SodaLive
|
|
||||||
//
|
|
||||||
// Created by klaus on 2/22/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct ContentMainAsmrAllView: View {
|
|
||||||
|
|
||||||
@StateObject var viewModel = ContentNewAllViewModel()
|
|
||||||
@State private var isInitialized = false
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Group {
|
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
|
||||||
VStack(alignment: .leading, spacing: 13.3) {
|
|
||||||
DetailNavigationBar(title: "새로운 ASMR")
|
|
||||||
|
|
||||||
Text("※ 최근 2주간 등록된 새로운 ASMR 입니다.")
|
|
||||||
.appFont(size: 14.7, weight: .medium)
|
|
||||||
.foregroundColor(.graybb)
|
|
||||||
.padding(.horizontal, 13.3)
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
.frame(width: screenSize().width, alignment: .leading)
|
|
||||||
.background(Color.gray22)
|
|
||||||
|
|
||||||
HStack(spacing: 0) {
|
|
||||||
Text("전체")
|
|
||||||
.appFont(size: 13.3, weight: .medium)
|
|
||||||
.foregroundColor(Color(hex: "e2e2e2"))
|
|
||||||
|
|
||||||
Text("\(viewModel.totalCount)")
|
|
||||||
.appFont(size: 13.3, weight: .medium)
|
|
||||||
.foregroundColor(Color(hex: "ff5c49"))
|
|
||||||
.padding(.leading, 8)
|
|
||||||
|
|
||||||
Text("개")
|
|
||||||
.appFont(size: 13.3, weight: .medium)
|
|
||||||
.foregroundColor(Color(hex: "e2e2e2"))
|
|
||||||
.padding(.leading, 2)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 13.3)
|
|
||||||
|
|
||||||
ScrollView(.vertical, showsIndicators: false) {
|
|
||||||
let horizontalPadding: CGFloat = 16
|
|
||||||
let gridSpacing: CGFloat = 16
|
|
||||||
let itemSize = (screenSize().width - (horizontalPadding * 2) - gridSpacing) / 2
|
|
||||||
|
|
||||||
LazyVGrid(
|
|
||||||
columns: Array(
|
|
||||||
repeating: GridItem(
|
|
||||||
.flexible(),
|
|
||||||
spacing: gridSpacing,
|
|
||||||
alignment: .topLeading
|
|
||||||
),
|
|
||||||
count: 2
|
|
||||||
),
|
|
||||||
alignment: .leading,
|
|
||||||
spacing: gridSpacing
|
|
||||||
) {
|
|
||||||
ForEach(0..<viewModel.newContentList.count, id: \.self) { index in
|
|
||||||
ContentNewAllItemView(width: itemSize, item: viewModel.newContentList[index])
|
|
||||||
.onAppear {
|
|
||||||
if index == viewModel.newContentList.count - 1 {
|
|
||||||
viewModel.getNewContentList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 13.3)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onAppear {
|
|
||||||
if !isInitialized {
|
|
||||||
if viewModel.selectedTheme != "ASMR" {
|
|
||||||
viewModel.selectedTheme = "ASMR"
|
|
||||||
} else if viewModel.newContentList.isEmpty {
|
|
||||||
viewModel.getNewContentList()
|
|
||||||
}
|
|
||||||
isInitialized = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navigationBarHidden(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
ContentMainAsmrAllView()
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
//
|
|
||||||
// ContentMainTabAsmrRepository.swift
|
|
||||||
// SodaLive
|
|
||||||
//
|
|
||||||
// Created by klaus on 2/22/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import CombineMoya
|
|
||||||
import Combine
|
|
||||||
import Moya
|
|
||||||
|
|
||||||
final class ContentMainTabAsmrRepository {
|
|
||||||
|
|
||||||
private let api = MoyaProvider<ContentApi>()
|
|
||||||
|
|
||||||
func getContentMainAsmr() -> AnyPublisher<Response, MoyaError> {
|
|
||||||
return api.requestPublisher(
|
|
||||||
.getContentMainAsmr(
|
|
||||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
|
||||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPopularContentByCreator(creatorId: Int) -> AnyPublisher<Response, MoyaError> {
|
|
||||||
return api.requestPublisher(
|
|
||||||
.getPopularAsmrContentByCreator(
|
|
||||||
creatorId: creatorId,
|
|
||||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
|
||||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
//
|
|
||||||
// ContentMainTabAsmrView.swift
|
|
||||||
// SodaLive
|
|
||||||
//
|
|
||||||
// Created by klaus on 2/22/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct ContentMainTabAsmrView: View {
|
|
||||||
|
|
||||||
@StateObject var viewModel = ContentMainTabAsmrViewModel()
|
|
||||||
|
|
||||||
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.newAsmrContentList.isEmpty {
|
|
||||||
ContentMainNewContentViewV2(
|
|
||||||
title: "새로운 ASMR",
|
|
||||||
onClickMore: {
|
|
||||||
AppState.shared
|
|
||||||
.setAppStep(step: .newAsmrContentAll)
|
|
||||||
},
|
|
||||||
themeList: [],
|
|
||||||
contentList: viewModel.newAsmrContentList
|
|
||||||
) { _ in }
|
|
||||||
.padding(.top, 30)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !viewModel.creatorList.isEmpty {
|
|
||||||
ContentByChannelView(
|
|
||||||
title: "채널별 추천 ASMR",
|
|
||||||
creatorList: viewModel.creatorList,
|
|
||||||
contentList: viewModel.salesCountRankContentList,
|
|
||||||
onClickCreator: {
|
|
||||||
viewModel.getPopularContentByCreator(creatorId: $0)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.padding(.top, 30)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !viewModel.eventBannerList.isEmpty {
|
|
||||||
SectionEventBannerView(items: viewModel.eventBannerList)
|
|
||||||
.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 {
|
|
||||||
ContentMainTabAsmrView()
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
//
|
|
||||||
// ContentMainTabAsmrViewModel.swift
|
|
||||||
// SodaLive
|
|
||||||
//
|
|
||||||
// Created by klaus on 2/22/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Combine
|
|
||||||
|
|
||||||
final class ContentMainTabAsmrViewModel: ObservableObject {
|
|
||||||
private let repository = ContentMainTabAsmrRepository()
|
|
||||||
private var subscription = Set<AnyCancellable>()
|
|
||||||
|
|
||||||
@Published var errorMessage = ""
|
|
||||||
@Published var isShowPopup = false
|
|
||||||
@Published var isLoading = false
|
|
||||||
|
|
||||||
@Published var bannerList: [GetAudioContentBannerResponse] = []
|
|
||||||
@Published var newAsmrContentList: [GetAudioContentMainItem] = []
|
|
||||||
@Published var creatorList: [ContentCreatorResponse] = []
|
|
||||||
@Published var salesCountRankContentList: [GetAudioContentRankingItem] = []
|
|
||||||
@Published var eventBannerList: [EventItem] = []
|
|
||||||
@Published var curationList: [GetContentCurationResponse] = []
|
|
||||||
|
|
||||||
func fetchData() {
|
|
||||||
isLoading = true
|
|
||||||
repository.getContentMainAsmr()
|
|
||||||
.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<GetContentMainTabAsmrResponse>.self, from: responseData)
|
|
||||||
|
|
||||||
if let data = decoded.data, decoded.success {
|
|
||||||
self.bannerList = data.contentBannerList
|
|
||||||
self.newAsmrContentList = data.newAsmrContentList
|
|
||||||
self.creatorList = data.creatorList
|
|
||||||
self.salesCountRankContentList = data.salesCountRankContentList
|
|
||||||
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 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.salesCountRankContentList = 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,15 +0,0 @@
|
|||||||
//
|
|
||||||
// GetContentMainTabAsmrResponse.swift
|
|
||||||
// SodaLive
|
|
||||||
//
|
|
||||||
// Created by klaus on 2/22/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
struct GetContentMainTabAsmrResponse: Decodable {
|
|
||||||
let contentBannerList: [GetAudioContentBannerResponse]
|
|
||||||
let newAsmrContentList: [GetAudioContentMainItem]
|
|
||||||
let creatorList: [ContentCreatorResponse]
|
|
||||||
let salesCountRankContentList: [GetAudioContentRankingItem]
|
|
||||||
let eventBannerList: GetEventResponse
|
|
||||||
let curationList: [GetContentCurationResponse]
|
|
||||||
}
|
|
||||||
@@ -12,10 +12,6 @@ enum ContentMainTab {
|
|||||||
case HOME
|
case HOME
|
||||||
case SERIES
|
case SERIES
|
||||||
case CONTENT
|
case CONTENT
|
||||||
case ALARM
|
|
||||||
case ASMR
|
|
||||||
case REPLAY
|
|
||||||
case FREE
|
|
||||||
}
|
}
|
||||||
|
|
||||||
struct TabItem {
|
struct TabItem {
|
||||||
@@ -35,10 +31,6 @@ struct ContentMainViewV2: View {
|
|||||||
TabItem(title: "홈", tab: .HOME),
|
TabItem(title: "홈", tab: .HOME),
|
||||||
TabItem(title: "시리즈", tab: .SERIES),
|
TabItem(title: "시리즈", tab: .SERIES),
|
||||||
TabItem(title: "단편", tab: .CONTENT),
|
TabItem(title: "단편", tab: .CONTENT),
|
||||||
TabItem(title: "모닝콜", tab: .ALARM),
|
|
||||||
TabItem(title: "ASMR", tab: .ASMR),
|
|
||||||
TabItem(title: "다시듣기", tab: .REPLAY),
|
|
||||||
TabItem(title: "무료", tab: .FREE)
|
|
||||||
]
|
]
|
||||||
|
|
||||||
init(selectedTab: ContentMainTab = .SERIES) {
|
init(selectedTab: ContentMainTab = .SERIES) {
|
||||||
@@ -115,14 +107,6 @@ struct ContentMainViewV2: View {
|
|||||||
ContentMainTabSeriesView()
|
ContentMainTabSeriesView()
|
||||||
case .CONTENT:
|
case .CONTENT:
|
||||||
ContentMainTabContentView()
|
ContentMainTabContentView()
|
||||||
case .ALARM:
|
|
||||||
ContentMainTabAlarmView()
|
|
||||||
case .ASMR:
|
|
||||||
ContentMainTabAsmrView()
|
|
||||||
case .REPLAY:
|
|
||||||
ContentMainTabReplayView()
|
|
||||||
case .FREE:
|
|
||||||
ContentMainTabFreeView()
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,66 +0,0 @@
|
|||||||
//
|
|
||||||
// ContentMainIntroduceCreatorAllView.swift
|
|
||||||
// SodaLive
|
|
||||||
//
|
|
||||||
// Created by klaus on 2/22/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct ContentMainIntroduceCreatorAllView: View {
|
|
||||||
|
|
||||||
@StateObject var viewModel = ContentMainIntroduceCreatorAllViewModel()
|
|
||||||
@State private var isInitialized = false
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Group {
|
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
|
||||||
VStack(spacing: 13.3) {
|
|
||||||
DetailNavigationBar(title: "크리에이터 소개")
|
|
||||||
|
|
||||||
ScrollView(.vertical, showsIndicators: false) {
|
|
||||||
let horizontalPadding: CGFloat = 16
|
|
||||||
let gridSpacing: CGFloat = 16
|
|
||||||
let itemSize = (screenSize().width - (horizontalPadding * 2) - gridSpacing) / 2
|
|
||||||
|
|
||||||
LazyVGrid(
|
|
||||||
columns: Array(
|
|
||||||
repeating: GridItem(
|
|
||||||
.flexible(),
|
|
||||||
spacing: gridSpacing,
|
|
||||||
alignment: .topLeading
|
|
||||||
),
|
|
||||||
count: 2
|
|
||||||
),
|
|
||||||
alignment: .leading,
|
|
||||||
spacing: gridSpacing
|
|
||||||
) {
|
|
||||||
ForEach(0..<viewModel.introduceCreatorList.count, id: \.self) { index in
|
|
||||||
let item = viewModel.introduceCreatorList[index]
|
|
||||||
ContentNewAllItemView(width: itemSize, item: item)
|
|
||||||
.onAppear {
|
|
||||||
if index == viewModel.introduceCreatorList.count - 1 {
|
|
||||||
viewModel.getIntroduceCreatorList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 13.3)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onAppear {
|
|
||||||
if !isInitialized {
|
|
||||||
viewModel.getIntroduceCreatorList()
|
|
||||||
isInitialized = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navigationBarHidden(true)
|
|
||||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
ContentMainIntroduceCreatorAllView()
|
|
||||||
}
|
|
||||||
@@ -1,74 +0,0 @@
|
|||||||
//
|
|
||||||
// ContentMainIntroduceCreatorAllViewModel.swift
|
|
||||||
// SodaLive
|
|
||||||
//
|
|
||||||
// Created by klaus on 2/22/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Combine
|
|
||||||
|
|
||||||
final class ContentMainIntroduceCreatorAllViewModel: ObservableObject {
|
|
||||||
private let repository = ContentMainTabFreeRepository()
|
|
||||||
private var subscription = Set<AnyCancellable>()
|
|
||||||
|
|
||||||
@Published var errorMessage = ""
|
|
||||||
@Published var isShowPopup = false
|
|
||||||
@Published var isLoading = false
|
|
||||||
|
|
||||||
@Published var introduceCreatorList: [GetAudioContentMainItem] = []
|
|
||||||
|
|
||||||
var page = 1
|
|
||||||
var isLast = false
|
|
||||||
private let size = 20
|
|
||||||
|
|
||||||
func getIntroduceCreatorList() {
|
|
||||||
if (!isLast && !isLoading) {
|
|
||||||
isLoading = true
|
|
||||||
repository.getIntroduceCreatorList(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
|
|
||||||
self.isLoading = false
|
|
||||||
let responseData = response.data
|
|
||||||
|
|
||||||
do {
|
|
||||||
let jsonDecoder = JSONDecoder()
|
|
||||||
let decoded = try jsonDecoder.decode(ApiResponse<[GetAudioContentMainItem]>.self, from: responseData)
|
|
||||||
self.isLoading = false
|
|
||||||
|
|
||||||
if let data = decoded.data, decoded.success {
|
|
||||||
if page == 1 {
|
|
||||||
introduceCreatorList.removeAll()
|
|
||||||
}
|
|
||||||
|
|
||||||
if !data.isEmpty {
|
|
||||||
page += 1
|
|
||||||
self.introduceCreatorList.append(contentsOf: data)
|
|
||||||
} 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,58 +0,0 @@
|
|||||||
//
|
|
||||||
// ContentMainTabFreeRepository.swift
|
|
||||||
// SodaLive
|
|
||||||
//
|
|
||||||
// Created by klaus on 2/22/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import CombineMoya
|
|
||||||
import Combine
|
|
||||||
import Moya
|
|
||||||
|
|
||||||
final class ContentMainTabFreeRepository {
|
|
||||||
|
|
||||||
private let api = MoyaProvider<ContentApi>()
|
|
||||||
|
|
||||||
func getContentMainFree() -> AnyPublisher<Response, MoyaError> {
|
|
||||||
return api.requestPublisher(
|
|
||||||
.getContentMainFree(
|
|
||||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
|
||||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getIntroduceCreatorList(page: Int, size: Int) -> AnyPublisher<Response, MoyaError> {
|
|
||||||
return api.requestPublisher(
|
|
||||||
.getIntroduceCreatorList(
|
|
||||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
|
||||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL,
|
|
||||||
page: page,
|
|
||||||
size: size
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getNewContentOfTheme(theme: String, page: Int = 1, size: Int = 20) -> AnyPublisher<Response, MoyaError> {
|
|
||||||
return api.requestPublisher(
|
|
||||||
.getNewFreeContentOfTheme(
|
|
||||||
theme: theme,
|
|
||||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
|
||||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL,
|
|
||||||
page: page,
|
|
||||||
size: size
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPopularContentByCreator(creatorId: Int) -> AnyPublisher<Response, MoyaError> {
|
|
||||||
return api.requestPublisher(
|
|
||||||
.getPopularFreeContentByCreator(
|
|
||||||
creatorId: creatorId,
|
|
||||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
|
||||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,87 +0,0 @@
|
|||||||
//
|
|
||||||
// ContentMainTabFreeView.swift
|
|
||||||
// SodaLive
|
|
||||||
//
|
|
||||||
// Created by klaus on 2/22/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct ContentMainTabFreeView: View {
|
|
||||||
|
|
||||||
@StateObject var viewModel = ContentMainTabFreeViewModel()
|
|
||||||
|
|
||||||
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 let introduceCreator = viewModel.introduceCreator {
|
|
||||||
ContentMainNewContentViewV2(
|
|
||||||
title: introduceCreator.title,
|
|
||||||
onClickMore: {
|
|
||||||
AppState.shared
|
|
||||||
.setAppStep(step: .introduceCreatorAll)
|
|
||||||
},
|
|
||||||
themeList: [],
|
|
||||||
contentList: introduceCreator.items
|
|
||||||
) { _ in }
|
|
||||||
.padding(.top, 30)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !viewModel.recommendSeriesList.isEmpty {
|
|
||||||
ContentMainNewOrRecommendSeriesView(
|
|
||||||
title: "추천 무료 시리즈",
|
|
||||||
recommendSeriesList: viewModel.recommendSeriesList
|
|
||||||
)
|
|
||||||
.padding(.top, 30)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !viewModel.themeList.isEmpty {
|
|
||||||
ContentMainNewContentViewV2(
|
|
||||||
title: "새로운 무료 콘텐츠",
|
|
||||||
onClickMore: {
|
|
||||||
AppState.shared
|
|
||||||
.setAppStep(step: .newContentAll(isFree: true))
|
|
||||||
},
|
|
||||||
themeList: viewModel.themeList,
|
|
||||||
contentList: viewModel.newFreeContentList
|
|
||||||
) {
|
|
||||||
viewModel.getNewContentOfTheme(theme: $0)
|
|
||||||
}
|
|
||||||
.padding(.top, 30)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !viewModel.creatorList.isEmpty {
|
|
||||||
ContentByChannelView(
|
|
||||||
title: "채널별 추천 무료 콘텐츠",
|
|
||||||
creatorList: viewModel.creatorList,
|
|
||||||
contentList: viewModel.playCountRankContentList,
|
|
||||||
onClickCreator: {
|
|
||||||
viewModel.getPopularContentByCreator(creatorId: $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 {
|
|
||||||
ContentMainTabFreeView()
|
|
||||||
}
|
|
||||||
@@ -1,151 +0,0 @@
|
|||||||
//
|
|
||||||
// ContentMainTabFreeViewModel.swift
|
|
||||||
// SodaLive
|
|
||||||
//
|
|
||||||
// Created by klaus on 2/22/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Combine
|
|
||||||
|
|
||||||
final class ContentMainTabFreeViewModel: ObservableObject {
|
|
||||||
private let repository = ContentMainTabFreeRepository()
|
|
||||||
private var subscription = Set<AnyCancellable>()
|
|
||||||
|
|
||||||
@Published var errorMessage = ""
|
|
||||||
@Published var isShowPopup = false
|
|
||||||
@Published var isLoading = false
|
|
||||||
|
|
||||||
@Published var bannerList: [GetAudioContentBannerResponse] = []
|
|
||||||
@Published var introduceCreator: GetContentCurationResponse? = nil
|
|
||||||
@Published var recommendSeriesList: [GetRecommendSeriesListResponse] = []
|
|
||||||
@Published var themeList: [String] = []
|
|
||||||
@Published var newFreeContentList: [GetAudioContentMainItem] = []
|
|
||||||
@Published var creatorList: [ContentCreatorResponse] = []
|
|
||||||
@Published var playCountRankContentList: [GetAudioContentRankingItem] = []
|
|
||||||
@Published var curationList: [GetContentCurationResponse] = []
|
|
||||||
|
|
||||||
func fetchData() {
|
|
||||||
isLoading = true
|
|
||||||
repository.getContentMainFree()
|
|
||||||
.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<GetContentMainTabFreeResponse>.self, from: responseData)
|
|
||||||
|
|
||||||
if let data = decoded.data, decoded.success {
|
|
||||||
self.bannerList = data.contentBannerList
|
|
||||||
self.introduceCreator = data.introduceCreator
|
|
||||||
self.recommendSeriesList = data.recommendSeriesList
|
|
||||||
self.newFreeContentList = data.newFreeContentList
|
|
||||||
self.creatorList = data.creatorList
|
|
||||||
self.playCountRankContentList = data.playCountRankContentList
|
|
||||||
self.curationList = data.curationList
|
|
||||||
|
|
||||||
self.themeList.removeAll()
|
|
||||||
self.themeList.append("전체")
|
|
||||||
self.themeList.append(contentsOf: data.themeList)
|
|
||||||
} 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)
|
|
||||||
.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.newFreeContentList = 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.playCountRankContentList = 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,17 +0,0 @@
|
|||||||
//
|
|
||||||
// GetContentMainTabFreeResponse.swift
|
|
||||||
// SodaLive
|
|
||||||
//
|
|
||||||
// Created by klaus on 2/22/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
struct GetContentMainTabFreeResponse: Decodable {
|
|
||||||
let contentBannerList: [GetAudioContentBannerResponse]
|
|
||||||
let introduceCreator: GetContentCurationResponse?
|
|
||||||
let recommendSeriesList: [GetRecommendSeriesListResponse]
|
|
||||||
let themeList: [String]
|
|
||||||
let newFreeContentList: [GetAudioContentMainItem]
|
|
||||||
let creatorList: [ContentCreatorResponse]
|
|
||||||
let playCountRankContentList: [GetAudioContentRankingItem]
|
|
||||||
let curationList: [GetContentCurationResponse]
|
|
||||||
}
|
|
||||||
@@ -1,38 +0,0 @@
|
|||||||
//
|
|
||||||
// ContentMainTabCategoryView.swift
|
|
||||||
// SodaLive
|
|
||||||
//
|
|
||||||
// Created by klaus on 2/20/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct ContentMainTabCategoryView: View {
|
|
||||||
|
|
||||||
let imageName: String
|
|
||||||
let title: String
|
|
||||||
let onClick: () -> Void
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(spacing: 5.3) {
|
|
||||||
Image(imageName)
|
|
||||||
.resizable()
|
|
||||||
.frame(width: 43, height: 43)
|
|
||||||
|
|
||||||
Text(title)
|
|
||||||
.appFont(size: 12, weight: .medium)
|
|
||||||
.foregroundColor(.gray77)
|
|
||||||
}
|
|
||||||
.onTapGesture {
|
|
||||||
onClick()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
ContentMainTabCategoryView(
|
|
||||||
imageName: "ic_category_series",
|
|
||||||
title: "시리즈",
|
|
||||||
onClick: {}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,47 +0,0 @@
|
|||||||
//
|
|
||||||
// ContentMainTabHomeNoticeView.swift
|
|
||||||
// SodaLive
|
|
||||||
//
|
|
||||||
// Created by klaus on 2/20/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct ContentMainTabHomeNoticeView: View {
|
|
||||||
|
|
||||||
let notice: NoticeItem
|
|
||||||
let onClick: (NoticeItem) -> Void
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
HStack(spacing: 0) {
|
|
||||||
Text(notice.title)
|
|
||||||
.appFont(size: 13.3, weight: .medium)
|
|
||||||
.foregroundColor(.white)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Text("자세히 >")
|
|
||||||
.appFont(size: 13.3, weight: .medium)
|
|
||||||
.foregroundColor(.white)
|
|
||||||
.onTapGesture {
|
|
||||||
onClick(notice)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 13.3)
|
|
||||||
.padding(.vertical, 10)
|
|
||||||
.background(Color.gray22)
|
|
||||||
.cornerRadius(5.3)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
ContentMainTabHomeNoticeView(
|
|
||||||
notice: NoticeItem(
|
|
||||||
title: "[업데이트] 1.28.0 버전 업데이트",
|
|
||||||
content: "test",
|
|
||||||
date: "2025-02-07"
|
|
||||||
)
|
|
||||||
) {
|
|
||||||
AppState.shared.setAppStep(step: .noticeDetail(notice: $0))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
//
|
|
||||||
// ContentMainTabHomeRepository.swift
|
|
||||||
// SodaLive
|
|
||||||
//
|
|
||||||
// Created by klaus on 2/20/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import CombineMoya
|
|
||||||
import Combine
|
|
||||||
import Moya
|
|
||||||
|
|
||||||
class ContentMainTabHomeRepository {
|
|
||||||
private let api = MoyaProvider<ContentApi>()
|
|
||||||
|
|
||||||
func getContentMainHome() -> AnyPublisher<Response, MoyaError> {
|
|
||||||
return api.requestPublisher(
|
|
||||||
.getContentMainHome(
|
|
||||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
|
||||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPopularContentByCreator(creatorId: Int) -> AnyPublisher<Response, MoyaError> {
|
|
||||||
return api.requestPublisher(
|
|
||||||
.getPopularContentByCreator(
|
|
||||||
creatorId: creatorId,
|
|
||||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
|
||||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getContentRanking(sortType: String = "매출") -> AnyPublisher<Response, MoyaError> {
|
|
||||||
return api.requestPublisher(
|
|
||||||
.getContentMainHomeContentRanking(
|
|
||||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
|
||||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL,
|
|
||||||
sortType: sortType
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,301 +0,0 @@
|
|||||||
//
|
|
||||||
// ContentMainTabHomeView.swift
|
|
||||||
// SodaLive
|
|
||||||
//
|
|
||||||
// Created by klaus on 2/20/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct ContentMainTabHomeView: View {
|
|
||||||
|
|
||||||
@StateObject var viewModel = ContentMainTabHomeViewModel()
|
|
||||||
@AppStorage("token") private var token: String = UserDefaults.string(forKey: UserDefaultsKey.token)
|
|
||||||
@AppStorage("role") private var role: String = UserDefaults.string(forKey: UserDefaultsKey.role)
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
|
||||||
ZStack(alignment: .bottomTrailing) {
|
|
||||||
ScrollView(.vertical, showsIndicators: false) {
|
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
|
||||||
HStack(spacing: 0) {
|
|
||||||
Text("보이스온")
|
|
||||||
.appFont(size: 21.3, weight: .bold)
|
|
||||||
.foregroundColor(Color.white)
|
|
||||||
.padding(.leading, 8)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
||||||
Image("ic_can")
|
|
||||||
.onTapGesture {
|
|
||||||
AppState
|
|
||||||
.shared
|
|
||||||
.setAppStep(step: .canCharge(refresh: {}))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 13.3)
|
|
||||||
|
|
||||||
if let notice = viewModel.noticeItem {
|
|
||||||
ContentMainTabHomeNoticeView(notice: notice) {
|
|
||||||
AppState.shared
|
|
||||||
.setAppStep(step: .noticeDetail(notice: $0))
|
|
||||||
}
|
|
||||||
.padding(.top, 15)
|
|
||||||
.padding(.horizontal, 13.3)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty &&
|
|
||||||
viewModel.bannerList.count > 0 {
|
|
||||||
ContentMainBannerViewV2(bannerList: viewModel.bannerList)
|
|
||||||
.padding(.top, 30)
|
|
||||||
.padding(.horizontal, 13.3)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
||||||
HStack(spacing: 0) {
|
|
||||||
Image("ic_title_search_black")
|
|
||||||
|
|
||||||
Text("검색어를 2글자 이상 입력하세요")
|
|
||||||
.appFont(size: 13.3, weight: .medium)
|
|
||||||
.foregroundColor(Color.gray55)
|
|
||||||
.keyboardType(.default)
|
|
||||||
.padding(.horizontal, 13.3)
|
|
||||||
|
|
||||||
Spacer()
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 21.3)
|
|
||||||
.frame(height: 50)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.background(Color.gray22)
|
|
||||||
.overlay(
|
|
||||||
RoundedRectangle(cornerRadius: 6.7)
|
|
||||||
.strokeBorder(lineWidth: 1)
|
|
||||||
.foregroundColor(Color.graybb)
|
|
||||||
)
|
|
||||||
.padding(.top, 30)
|
|
||||||
.padding(.horizontal, 13.3)
|
|
||||||
.onTapGesture {
|
|
||||||
UserDefaults.set("", forKey: .searchChannel)
|
|
||||||
AppState.shared.setAppStep(step: .search)
|
|
||||||
}
|
|
||||||
|
|
||||||
VStack(spacing: 13.3) {
|
|
||||||
HStack(spacing: 0) {
|
|
||||||
ContentMainTabCategoryView(
|
|
||||||
imageName: "ic_category_series",
|
|
||||||
title: "시리즈",
|
|
||||||
onClick: {
|
|
||||||
AppState.shared
|
|
||||||
.setAppStep(
|
|
||||||
step: .contentMain(
|
|
||||||
startTab: .SERIES
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
|
|
||||||
ContentMainTabCategoryView(
|
|
||||||
imageName: "ic_category_content",
|
|
||||||
title: "단편",
|
|
||||||
onClick: {
|
|
||||||
AppState.shared
|
|
||||||
.setAppStep(
|
|
||||||
step: .contentMain(
|
|
||||||
startTab: .CONTENT
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
|
|
||||||
ContentMainTabCategoryView(
|
|
||||||
imageName: "ic_category_alarm",
|
|
||||||
title: "모닝콜",
|
|
||||||
onClick: {
|
|
||||||
AppState.shared
|
|
||||||
.setAppStep(
|
|
||||||
step: .contentMain(
|
|
||||||
startTab: .ALARM
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
|
|
||||||
ContentMainTabCategoryView(
|
|
||||||
imageName: "ic_category_asmr",
|
|
||||||
title: "ASMR",
|
|
||||||
onClick: {
|
|
||||||
AppState.shared
|
|
||||||
.setAppStep(
|
|
||||||
step: .contentMain(
|
|
||||||
startTab: .ASMR
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
}
|
|
||||||
|
|
||||||
HStack(spacing: 0) {
|
|
||||||
ContentMainTabCategoryView(
|
|
||||||
imageName: "ic_category_replay",
|
|
||||||
title: "다시듣기",
|
|
||||||
onClick: {
|
|
||||||
AppState.shared
|
|
||||||
.setAppStep(
|
|
||||||
step: .contentMain(
|
|
||||||
startTab: .REPLAY
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
|
|
||||||
ContentMainTabCategoryView(
|
|
||||||
imageName: "ic_category_free",
|
|
||||||
title: "무료",
|
|
||||||
onClick: {
|
|
||||||
AppState.shared
|
|
||||||
.setAppStep(
|
|
||||||
step: .contentMain(
|
|
||||||
startTab: .FREE
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
|
|
||||||
ContentMainTabCategoryView(
|
|
||||||
imageName: "ic_category_audio_book",
|
|
||||||
title: "오디오북",
|
|
||||||
onClick: {
|
|
||||||
viewModel.errorMessage = "준비중입니다."
|
|
||||||
viewModel.isShowPopup = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
|
|
||||||
ContentMainTabCategoryView(
|
|
||||||
imageName: "ic_category_audio_toon",
|
|
||||||
title: "오디오툰",
|
|
||||||
onClick: {
|
|
||||||
viewModel.errorMessage = "준비중입니다."
|
|
||||||
viewModel.isShowPopup = true
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.vertical, 13.3)
|
|
||||||
.background(Color.gray22)
|
|
||||||
.cornerRadius(5.3)
|
|
||||||
.padding(.top, 30)
|
|
||||||
.padding(.horizontal, 13.3)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let response = viewModel.rankCreatorResponse {
|
|
||||||
ContentMainTabHomeRankCreatorView(response: response)
|
|
||||||
.padding(.top, 30)
|
|
||||||
.padding(.horizontal, 13.3)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !viewModel.rankSeriesList.isEmpty {
|
|
||||||
ContentMainTabHomeRankSeriesView(seriesList: viewModel.rankSeriesList)
|
|
||||||
.padding(.top, 30)
|
|
||||||
.padding(.horizontal, 13.3)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !viewModel.rankSortTypeList.isEmpty {
|
|
||||||
ContentMainTabRankContentView(
|
|
||||||
title: "인기 단편",
|
|
||||||
isMore: true,
|
|
||||||
onClickMore: {
|
|
||||||
if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
||||||
AppState.shared.setAppStep(step: .contentRankingAll)
|
|
||||||
} else {
|
|
||||||
AppState.shared.setAppStep(step: .login)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
sortList: !viewModel.rankSortTypeList.isEmpty ?
|
|
||||||
viewModel.rankSortTypeList :
|
|
||||||
[],
|
|
||||||
onClickSort: { viewModel.getContentRanking(sort: $0) },
|
|
||||||
contentList: viewModel.rankContentList
|
|
||||||
)
|
|
||||||
.padding(.top, 30)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty &&
|
|
||||||
viewModel.eventBannerList.count > 0 {
|
|
||||||
SectionEventBannerView(items: viewModel.eventBannerList)
|
|
||||||
.padding(.top, 30)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !viewModel.contentRankCreatorList.isEmpty {
|
|
||||||
ContentByChannelView(
|
|
||||||
title: "채널별 인기 콘텐츠",
|
|
||||||
creatorList: viewModel.contentRankCreatorList,
|
|
||||||
contentList: viewModel.salesCountRankContentList,
|
|
||||||
onClickCreator: {
|
|
||||||
viewModel.getPopularContentByCreator(creatorId: $0)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.padding(.top, 30)
|
|
||||||
}
|
|
||||||
|
|
||||||
Text("""
|
|
||||||
- 회사명 : 주식회사 소다라이브
|
|
||||||
|
|
||||||
- 대표자 : 이재형
|
|
||||||
|
|
||||||
- 주소 : 경기도 성남시 분당구 황새울로335번길 10, 5층 563A호
|
|
||||||
|
|
||||||
- 사업자등록번호 : 870-81-03220
|
|
||||||
|
|
||||||
- 통신판매업신고 : 제2024-성남분당B-1012호
|
|
||||||
|
|
||||||
- 고객센터 : 02.2055.1477 (이용시간 10:00~19:00)
|
|
||||||
|
|
||||||
- 대표 이메일 : sodalive.official@gmail.com
|
|
||||||
""")
|
|
||||||
.appFont(size: 11, weight: .medium)
|
|
||||||
.foregroundColor(Color.gray77)
|
|
||||||
.padding(.top, 30)
|
|
||||||
}
|
|
||||||
.onAppear {
|
|
||||||
viewModel.fetchData()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if role == MemberRole.CREATOR.rawValue {
|
|
||||||
HStack(spacing: 5) {
|
|
||||||
Image("ic_thumb_play")
|
|
||||||
.resizable()
|
|
||||||
.frame(width: 20, height: 20)
|
|
||||||
|
|
||||||
Text("콘텐츠 업로드")
|
|
||||||
.appFont(size: 13.3, weight: .bold)
|
|
||||||
.foregroundColor(.white)
|
|
||||||
}
|
|
||||||
.padding(13.3)
|
|
||||||
.background(Color(hex: "3bb9f1"))
|
|
||||||
.cornerRadius(44)
|
|
||||||
.padding(.trailing, 16.7)
|
|
||||||
.padding(.bottom, 16.7)
|
|
||||||
.onTapGesture {
|
|
||||||
AppState.shared.setAppStep(step: .createContent)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.sodaToast(isPresented: $viewModel.isShowPopup, message: viewModel.errorMessage, autohideIn: 2)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
ContentMainTabHomeView()
|
|
||||||
}
|
|
||||||
@@ -1,152 +0,0 @@
|
|||||||
//
|
|
||||||
// ContentMainTabHomeViewModel.swift
|
|
||||||
// SodaLive
|
|
||||||
//
|
|
||||||
// Created by klaus on 2/20/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Combine
|
|
||||||
|
|
||||||
final class ContentMainTabHomeViewModel: ObservableObject {
|
|
||||||
|
|
||||||
private let repository = ContentMainTabHomeRepository()
|
|
||||||
private var subscription = Set<AnyCancellable>()
|
|
||||||
|
|
||||||
@Published var errorMessage = ""
|
|
||||||
@Published var isShowPopup = false
|
|
||||||
@Published var isLoading = false
|
|
||||||
|
|
||||||
@Published var noticeItem: NoticeItem? = nil
|
|
||||||
@Published var bannerList = [GetAudioContentBannerResponse]()
|
|
||||||
@Published var rankCreatorResponse: GetExplorerSectionResponse? = nil
|
|
||||||
@Published var rankSeriesList = [SeriesListItem]()
|
|
||||||
@Published var rankSortTypeList: [String] = []
|
|
||||||
@Published var rankContentList: [GetAudioContentRankingItem] = []
|
|
||||||
@Published var eventBannerList: [EventItem] = []
|
|
||||||
@Published var contentRankCreatorList: [ContentCreatorResponse] = []
|
|
||||||
@Published var salesCountRankContentList: [GetAudioContentRankingItem] = []
|
|
||||||
|
|
||||||
func fetchData() {
|
|
||||||
isLoading = true
|
|
||||||
|
|
||||||
repository.getContentMainHome()
|
|
||||||
.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<GetContentMainTabHomeResponse>.self, from: responseData)
|
|
||||||
|
|
||||||
if let data = decoded.data, decoded.success {
|
|
||||||
self.noticeItem = data.latestNotice
|
|
||||||
self.bannerList = data.bannerList
|
|
||||||
self.rankCreatorResponse = data.rankCreatorList
|
|
||||||
self.rankSeriesList = data.rankSeriesList
|
|
||||||
self.rankSortTypeList = data.rankSortTypeList
|
|
||||||
self.rankContentList = data.rankContentList
|
|
||||||
self.eventBannerList = data.eventBannerList.eventList
|
|
||||||
self.contentRankCreatorList = data.contentRankCreatorList
|
|
||||||
self.salesCountRankContentList = data.salesCountRankContentList
|
|
||||||
} 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 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.salesCountRankContentList = 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,18 +0,0 @@
|
|||||||
//
|
|
||||||
// GetContentMainTabHomeResponse.swift
|
|
||||||
// SodaLive
|
|
||||||
//
|
|
||||||
// Created by klaus on 2/20/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
struct GetContentMainTabHomeResponse: Decodable {
|
|
||||||
let latestNotice: NoticeItem?
|
|
||||||
let bannerList: [GetAudioContentBannerResponse]
|
|
||||||
let rankCreatorList: GetExplorerSectionResponse
|
|
||||||
let rankSeriesList: [SeriesListItem]
|
|
||||||
let rankSortTypeList: [String]
|
|
||||||
let rankContentList: [GetAudioContentRankingItem]
|
|
||||||
let eventBannerList: GetEventResponse
|
|
||||||
let contentRankCreatorList: [ContentCreatorResponse]
|
|
||||||
let salesCountRankContentList: [GetAudioContentRankingItem]
|
|
||||||
}
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
//
|
|
||||||
// ContentMainTabHomeRankCreatorView.swift
|
|
||||||
// SodaLive
|
|
||||||
//
|
|
||||||
// Created by klaus on 2/20/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
import Kingfisher
|
|
||||||
|
|
||||||
struct ContentMainTabHomeRankCreatorView: View {
|
|
||||||
|
|
||||||
@AppStorage("token") private var token: String = UserDefaults.string(forKey: UserDefaultsKey.token)
|
|
||||||
|
|
||||||
let response: GetExplorerSectionResponse
|
|
||||||
|
|
||||||
let rankingCrawns = ["ic_crown_1", "ic_crown_2", "ic_crown_3"]
|
|
||||||
let rankingColors = [
|
|
||||||
[Color(hex: "ffdc00"), Color(hex: "ffb600")],
|
|
||||||
[Color(hex: "ffffff"), Color(hex: "9f9f9f")],
|
|
||||||
[Color(hex: "e6a77a"), Color(hex: "c67e4a")],
|
|
||||||
[Color(hex: "ffffff").opacity(0), Color(hex: "ffffff").opacity(0)]
|
|
||||||
]
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 0) {
|
|
||||||
if let desc = response.desc, !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
||||||
VStack(spacing: 8) {
|
|
||||||
Text("\(desc)")
|
|
||||||
.appFont(size: 14.7, weight: .bold)
|
|
||||||
.foregroundColor(Color.grayee)
|
|
||||||
|
|
||||||
Text("※ 인기 순위는 매주 업데이트됩니다.")
|
|
||||||
.appFont(size: 13.3, weight: .light)
|
|
||||||
.foregroundColor(Color.graybb)
|
|
||||||
}
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
.frame(maxWidth: .infinity)
|
|
||||||
.background(Color.gray22)
|
|
||||||
}
|
|
||||||
|
|
||||||
if let coloredTitle = response.coloredTitle, let color = response.color {
|
|
||||||
let titleArray = response.title.components(separatedBy: coloredTitle)
|
|
||||||
HStack(spacing: 0) {
|
|
||||||
Text(titleArray[0])
|
|
||||||
.appFont(size: 18.3, weight: .bold)
|
|
||||||
.foregroundColor(Color.grayee)
|
|
||||||
|
|
||||||
Text(coloredTitle)
|
|
||||||
.appFont(size: 18.3, weight: .bold)
|
|
||||||
.foregroundColor(Color(hex: color))
|
|
||||||
|
|
||||||
if titleArray.count > 1 {
|
|
||||||
Text(titleArray[1])
|
|
||||||
.appFont(size: 18.3, weight: .bold)
|
|
||||||
.foregroundColor(Color.grayee)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.top, token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty ? 0 : 30)
|
|
||||||
} else {
|
|
||||||
Text(response.title)
|
|
||||||
.appFont(size: 18.3, weight: .bold)
|
|
||||||
.foregroundColor(Color.grayee)
|
|
||||||
.padding(.top, 30)
|
|
||||||
}
|
|
||||||
|
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
|
||||||
HStack(spacing: 13.3) {
|
|
||||||
ForEach(0..<response.creators.count, id: \.self) { index in
|
|
||||||
let creator = response.creators[index]
|
|
||||||
VStack(spacing: 0) {
|
|
||||||
if let _ = response.desc {
|
|
||||||
ZStack {
|
|
||||||
KFImage(URL(string: creator.profileImageUrl))
|
|
||||||
.cancelOnDisappear(true)
|
|
||||||
.downsampling(size: CGSize(width: 90, height: 90))
|
|
||||||
.resizable()
|
|
||||||
.clipShape(Circle())
|
|
||||||
.frame(width: 90, height: 90)
|
|
||||||
.overlay(
|
|
||||||
Circle()
|
|
||||||
.stroke(
|
|
||||||
AngularGradient(colors: rankingColors[index < 4 ? index : 3], center: .center),
|
|
||||||
lineWidth: 3
|
|
||||||
)
|
|
||||||
)
|
|
||||||
|
|
||||||
if index < 3 {
|
|
||||||
VStack(alignment: .trailing, spacing: 0) {
|
|
||||||
Spacer()
|
|
||||||
|
|
||||||
Image(rankingCrawns[index])
|
|
||||||
.resizable()
|
|
||||||
.frame(width: 37, height: 37)
|
|
||||||
}
|
|
||||||
.frame(width: 93.3, height: 93.3, alignment: .trailing)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.frame(width: 93.3, height: 93.3)
|
|
||||||
} else {
|
|
||||||
KFImage(URL(string: creator.profileImageUrl))
|
|
||||||
.cancelOnDisappear(true)
|
|
||||||
.downsampling(size: CGSize(width: 93, height: 93))
|
|
||||||
.resizable()
|
|
||||||
.clipShape(Circle())
|
|
||||||
.frame(width: 93, height: 93)
|
|
||||||
}
|
|
||||||
|
|
||||||
Text(creator.nickname)
|
|
||||||
.appFont(size: 11.3, weight: .medium)
|
|
||||||
.foregroundColor(Color.grayee)
|
|
||||||
.lineLimit(1)
|
|
||||||
.frame(width: 93.3)
|
|
||||||
.padding(.top, 13.3)
|
|
||||||
|
|
||||||
Text(creator.tags)
|
|
||||||
.appFont(size: 10, weight: .medium)
|
|
||||||
.foregroundColor(Color.button)
|
|
||||||
.lineLimit(1)
|
|
||||||
.frame(width: 93.3)
|
|
||||||
.padding(.top, 3.3)
|
|
||||||
}
|
|
||||||
.contentShape(Rectangle())
|
|
||||||
.onTapGesture {
|
|
||||||
if !token.trimmingCharacters(in: .whitespacesAndNewlines).isEmpty {
|
|
||||||
AppState.shared
|
|
||||||
.setAppStep(step: .creatorDetail(userId: creator.id))
|
|
||||||
} else {
|
|
||||||
AppState.shared
|
|
||||||
.setAppStep(step: .login)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.top, 13.3)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
ContentMainTabHomeRankCreatorView(
|
|
||||||
response: GetExplorerSectionResponse(
|
|
||||||
title: "인기 크리에이터",
|
|
||||||
coloredTitle: "인기",
|
|
||||||
color: "ff5c49",
|
|
||||||
desc: "2025년 02월 10일 ~ 02월 16일",
|
|
||||||
creators: [
|
|
||||||
GetExplorerSectionCreatorResponse(
|
|
||||||
id: 1,
|
|
||||||
nickname: "User1",
|
|
||||||
tags: "",
|
|
||||||
profileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
|
||||||
follow: false
|
|
||||||
),
|
|
||||||
GetExplorerSectionCreatorResponse(
|
|
||||||
id: 2,
|
|
||||||
nickname: "User2",
|
|
||||||
tags: "",
|
|
||||||
profileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
|
||||||
follow: false
|
|
||||||
),
|
|
||||||
GetExplorerSectionCreatorResponse(
|
|
||||||
id: 3,
|
|
||||||
nickname: "User3",
|
|
||||||
tags: "",
|
|
||||||
profileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
|
||||||
follow: false
|
|
||||||
),
|
|
||||||
GetExplorerSectionCreatorResponse(
|
|
||||||
id: 4,
|
|
||||||
nickname: "User4",
|
|
||||||
tags: "",
|
|
||||||
profileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png",
|
|
||||||
follow: false
|
|
||||||
)
|
|
||||||
]
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
@@ -1,82 +0,0 @@
|
|||||||
//
|
|
||||||
// ContentMainTabHomeRankSeriesView.swift
|
|
||||||
// SodaLive
|
|
||||||
//
|
|
||||||
// Created by klaus on 2/20/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct ContentMainTabHomeRankSeriesView: View {
|
|
||||||
|
|
||||||
let seriesList: [SeriesListItem]
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
VStack(alignment: .leading, spacing: 13.3) {
|
|
||||||
Text("인기 시리즈")
|
|
||||||
.appFont(size: 18.3, weight: .bold)
|
|
||||||
.foregroundColor(Color.grayee)
|
|
||||||
|
|
||||||
ScrollView(.horizontal, showsIndicators: false) {
|
|
||||||
HStack(alignment: .top, spacing: 13.3) {
|
|
||||||
ForEach(0..<seriesList.count, id: \.self) {
|
|
||||||
let item = seriesList[$0]
|
|
||||||
SeriesListBigItemView(item: item, isVisibleCreator: true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
ContentMainTabHomeRankSeriesView(
|
|
||||||
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: 2,
|
|
||||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
|
||||||
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: false,
|
|
||||||
isPopular: true
|
|
||||||
),
|
|
||||||
SeriesListItem(
|
|
||||||
seriesId: 1,
|
|
||||||
title: "제목, 관심사,프로필+방장, 참여인원(어딘가..)",
|
|
||||||
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,93 +0,0 @@
|
|||||||
//
|
|
||||||
// ContentMainReplayAllView.swift
|
|
||||||
// SodaLive
|
|
||||||
//
|
|
||||||
// Created by klaus on 2/22/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct ContentMainReplayAllView: View {
|
|
||||||
|
|
||||||
@StateObject var viewModel = ContentNewAllViewModel()
|
|
||||||
@State private var isInitialized = false
|
|
||||||
|
|
||||||
var body: some View {
|
|
||||||
Group {
|
|
||||||
BaseView(isLoading: $viewModel.isLoading) {
|
|
||||||
VStack(alignment: .leading, spacing: 13.3) {
|
|
||||||
DetailNavigationBar(title: "새로운 라이브 다시듣기")
|
|
||||||
|
|
||||||
Text("※ 최근 2주간 등록된 새로운 라이브 다시듣기 입니다.")
|
|
||||||
.appFont(size: 14.7, weight: .medium)
|
|
||||||
.foregroundColor(.graybb)
|
|
||||||
.padding(.horizontal, 13.3)
|
|
||||||
.padding(.vertical, 8)
|
|
||||||
.frame(width: screenSize().width, alignment: .leading)
|
|
||||||
.background(Color.gray22)
|
|
||||||
|
|
||||||
HStack(spacing: 0) {
|
|
||||||
Text("전체")
|
|
||||||
.appFont(size: 13.3, weight: .medium)
|
|
||||||
.foregroundColor(Color(hex: "e2e2e2"))
|
|
||||||
|
|
||||||
Text("\(viewModel.totalCount)")
|
|
||||||
.appFont(size: 13.3, weight: .medium)
|
|
||||||
.foregroundColor(Color(hex: "ff5c49"))
|
|
||||||
.padding(.leading, 8)
|
|
||||||
|
|
||||||
Text("개")
|
|
||||||
.appFont(size: 13.3, weight: .medium)
|
|
||||||
.foregroundColor(Color(hex: "e2e2e2"))
|
|
||||||
.padding(.leading, 2)
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 13.3)
|
|
||||||
|
|
||||||
ScrollView(.vertical, showsIndicators: false) {
|
|
||||||
let horizontalPadding: CGFloat = 16
|
|
||||||
let gridSpacing: CGFloat = 16
|
|
||||||
let itemSize = (screenSize().width - (horizontalPadding * 2) - gridSpacing) / 2
|
|
||||||
|
|
||||||
LazyVGrid(
|
|
||||||
columns: Array(
|
|
||||||
repeating: GridItem(
|
|
||||||
.flexible(),
|
|
||||||
spacing: gridSpacing,
|
|
||||||
alignment: .topLeading
|
|
||||||
),
|
|
||||||
count: 2
|
|
||||||
),
|
|
||||||
alignment: .leading,
|
|
||||||
spacing: gridSpacing
|
|
||||||
) {
|
|
||||||
ForEach(0..<viewModel.newContentList.count, id: \.self) { index in
|
|
||||||
ContentNewAllItemView(width: itemSize, item: viewModel.newContentList[index])
|
|
||||||
.onAppear {
|
|
||||||
if index == viewModel.newContentList.count - 1 {
|
|
||||||
viewModel.getNewContentList()
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.padding(.horizontal, 13.3)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.onAppear {
|
|
||||||
if !isInitialized {
|
|
||||||
if viewModel.selectedTheme != "다시듣기" {
|
|
||||||
viewModel.selectedTheme = "다시듣기"
|
|
||||||
} else if viewModel.newContentList.isEmpty {
|
|
||||||
viewModel.getNewContentList()
|
|
||||||
}
|
|
||||||
isInitialized = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
.navigationBarHidden(true)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#Preview {
|
|
||||||
ContentMainReplayAllView()
|
|
||||||
}
|
|
||||||
@@ -1,35 +0,0 @@
|
|||||||
//
|
|
||||||
// ContentMainTabReplayRepository.swift
|
|
||||||
// SodaLive
|
|
||||||
//
|
|
||||||
// Created by klaus on 2/22/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import CombineMoya
|
|
||||||
import Combine
|
|
||||||
import Moya
|
|
||||||
|
|
||||||
final class ContentMainTabReplayRepository {
|
|
||||||
|
|
||||||
private let api = MoyaProvider<ContentApi>()
|
|
||||||
|
|
||||||
func getContentMainReplay() -> AnyPublisher<Response, MoyaError> {
|
|
||||||
return api.requestPublisher(
|
|
||||||
.getContentMainReplay(
|
|
||||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
|
||||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
func getPopularContentByCreator(creatorId: Int) -> AnyPublisher<Response, MoyaError> {
|
|
||||||
return api.requestPublisher(
|
|
||||||
.getPopularReplayContentByCreator(
|
|
||||||
creatorId: creatorId,
|
|
||||||
isAdultContentVisible: UserDefaults.isAdultContentVisible(),
|
|
||||||
contentType: ContentType(rawValue: UserDefaults.string(forKey: .contentPreference)) ?? ContentType.ALL
|
|
||||||
)
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,69 +0,0 @@
|
|||||||
//
|
|
||||||
// ContentMainTabReplayView.swift
|
|
||||||
// SodaLive
|
|
||||||
//
|
|
||||||
// Created by klaus on 2/22/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import SwiftUI
|
|
||||||
|
|
||||||
struct ContentMainTabReplayView: View {
|
|
||||||
|
|
||||||
@StateObject var viewModel = ContentMainTabReplayViewModel()
|
|
||||||
|
|
||||||
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.newReplayContentList.isEmpty {
|
|
||||||
ContentMainNewContentViewV2(
|
|
||||||
title: "새로운 라이브 다시듣기",
|
|
||||||
onClickMore: {
|
|
||||||
AppState.shared
|
|
||||||
.setAppStep(step: .newReplayContentAll)
|
|
||||||
},
|
|
||||||
themeList: [],
|
|
||||||
contentList: viewModel.newReplayContentList
|
|
||||||
) { _ in }
|
|
||||||
.padding(.top, 30)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !viewModel.creatorList.isEmpty {
|
|
||||||
ContentByChannelView(
|
|
||||||
title: "채널별 라이브 다시듣기",
|
|
||||||
creatorList: viewModel.creatorList,
|
|
||||||
contentList: viewModel.salesCountRankContentList,
|
|
||||||
onClickCreator: {
|
|
||||||
viewModel.getPopularContentByCreator(creatorId: $0)
|
|
||||||
}
|
|
||||||
)
|
|
||||||
.padding(.top, 30)
|
|
||||||
}
|
|
||||||
|
|
||||||
if !viewModel.eventBannerList.isEmpty {
|
|
||||||
SectionEventBannerView(items: viewModel.eventBannerList)
|
|
||||||
.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 {
|
|
||||||
ContentMainTabReplayView()
|
|
||||||
}
|
|
||||||
@@ -1,106 +0,0 @@
|
|||||||
//
|
|
||||||
// ContentMainTabReplayViewModel.swift
|
|
||||||
// SodaLive
|
|
||||||
//
|
|
||||||
// Created by klaus on 2/22/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
import Foundation
|
|
||||||
import Combine
|
|
||||||
|
|
||||||
final class ContentMainTabReplayViewModel: ObservableObject {
|
|
||||||
private let repository = ContentMainTabReplayRepository()
|
|
||||||
private var subscription = Set<AnyCancellable>()
|
|
||||||
|
|
||||||
@Published var errorMessage = ""
|
|
||||||
@Published var isShowPopup = false
|
|
||||||
@Published var isLoading = false
|
|
||||||
|
|
||||||
@Published var bannerList: [GetAudioContentBannerResponse] = []
|
|
||||||
@Published var newReplayContentList: [GetAudioContentMainItem] = []
|
|
||||||
@Published var creatorList: [ContentCreatorResponse] = []
|
|
||||||
@Published var salesCountRankContentList: [GetAudioContentRankingItem] = []
|
|
||||||
@Published var eventBannerList: [EventItem] = []
|
|
||||||
@Published var curationList: [GetContentCurationResponse] = []
|
|
||||||
|
|
||||||
func fetchData() {
|
|
||||||
isLoading = true
|
|
||||||
repository.getContentMainReplay()
|
|
||||||
.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<GetContentMainTabReplayResponse>.self, from: responseData)
|
|
||||||
|
|
||||||
if let data = decoded.data, decoded.success {
|
|
||||||
self.bannerList = data.contentBannerList
|
|
||||||
self.newReplayContentList = data.newLiveReplayContentList
|
|
||||||
self.creatorList = data.creatorList
|
|
||||||
self.salesCountRankContentList = data.salesCountRankContentList
|
|
||||||
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 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.salesCountRankContentList = 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,15 +0,0 @@
|
|||||||
//
|
|
||||||
// GetContentMainTabReplayResponse.swift
|
|
||||||
// SodaLive
|
|
||||||
//
|
|
||||||
// Created by klaus on 2/22/25.
|
|
||||||
//
|
|
||||||
|
|
||||||
struct GetContentMainTabReplayResponse: Decodable {
|
|
||||||
let contentBannerList: [GetAudioContentBannerResponse]
|
|
||||||
let newLiveReplayContentList: [GetAudioContentMainItem]
|
|
||||||
let creatorList: [ContentCreatorResponse]
|
|
||||||
let salesCountRankContentList: [GetAudioContentRankingItem]
|
|
||||||
let eventBannerList: GetEventResponse
|
|
||||||
let curationList: [GetContentCurationResponse]
|
|
||||||
}
|
|
||||||
@@ -296,18 +296,6 @@ struct AppStepLayerView: View {
|
|||||||
case .completedSeriesAll:
|
case .completedSeriesAll:
|
||||||
CompletedSeriesView()
|
CompletedSeriesView()
|
||||||
|
|
||||||
case .newAlarmContentAll:
|
|
||||||
ContentMainAlarmAllView()
|
|
||||||
|
|
||||||
case .newAsmrContentAll:
|
|
||||||
ContentMainAsmrAllView()
|
|
||||||
|
|
||||||
case .newReplayContentAll:
|
|
||||||
ContentMainReplayAllView()
|
|
||||||
|
|
||||||
case .introduceCreatorAll:
|
|
||||||
ContentMainIntroduceCreatorAllView()
|
|
||||||
|
|
||||||
case .message:
|
case .message:
|
||||||
MessageView()
|
MessageView()
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user