콘텐츠 메인

- 단편 탭 UI 페이지 생성
This commit is contained in:
Yu Sung 2025-02-22 01:41:03 +09:00
parent 6bd27c5301
commit e9e7403579
18 changed files with 1018 additions and 95 deletions

View File

@ -45,6 +45,11 @@ enum ContentApi {
case getContentMainSeries
case getRecommendSeriesListByGenre(genreId: Int)
case getRecommendSeriesByCreator(creatorId: Int)
case getContentMainContent
case getContentMainNewContentOfTheme(theme: String, isAdultContentVisible: Bool, contentType: ContentType)
case getDailyContentRanking(sortType: String)
case getRecommendContentByTag(tag: String)
}
extension ContentApi: TargetType {
@ -155,6 +160,18 @@ extension ContentApi: TargetType {
case .getRecommendSeriesByCreator:
return "/v2/audio-content/main/series/recommend-series-by-creator"
case .getContentMainContent:
return "/v2/audio-content/main/content"
case .getContentMainNewContentOfTheme:
return "/v2/audio-content/main/content/new-content-by-theme"
case .getDailyContentRanking:
return "/v2/audio-content/main/content/ranking"
case .getRecommendContentByTag:
return "/v2/audio-content/main/content/recommend-content-by-tag"
}
}
@ -169,7 +186,8 @@ extension ContentApi: TargetType {
case .getMainBannerList, .getMainOrderList, .getNewContentUploadCreatorList, .getCurationList, .getAudioContentByTheme:
return .get
case .getContentMainHome, .getPopularContentByCreator, .getContentMainSeries, .getRecommendSeriesListByGenre, .getRecommendSeriesByCreator:
case .getContentMainHome, .getPopularContentByCreator, .getContentMainSeries, .getRecommendSeriesListByGenre, .getRecommendSeriesByCreator, .getContentMainContent,
.getContentMainNewContentOfTheme, .getDailyContentRanking, .getRecommendContentByTag:
return .get
case .likeContent, .modifyAudioContent, .modifyComment, .unpinContent:
@ -326,7 +344,7 @@ extension ContentApi: TargetType {
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
case .getContentMainHome, .getContentMainSeries:
case .getContentMainHome, .getContentMainSeries, .getContentMainContent:
return .requestPlain
case .getRecommendSeriesListByGenre(let genreId):
@ -340,6 +358,22 @@ extension ContentApi: TargetType {
case .getRecommendSeriesByCreator(let creatorId):
let parameters = ["creatorId": creatorId]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
case .getContentMainNewContentOfTheme(let theme, let isAdultContentVisible, let contentType):
let parameters = [
"theme": theme,
"isAdultContentVisible": isAdultContentVisible,
"contentType": contentType
] as [String : Any]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
case .getDailyContentRanking(let sortType):
let parameters = ["sortType": sortType]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
case .getRecommendContentByTag(let tag):
let parameters = ["tag": tag]
return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString)
}
}

View File

@ -40,6 +40,7 @@ struct ContentMainRankingSortView: View {
}
}
}
.padding(.horizontal, 13.3)
}
}
}

View File

@ -0,0 +1,42 @@
//
// 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)
}
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))
}
func getRecommendContentByTag(tag: String) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.getRecommendContentByTag(tag: tag))
}
func getPopularContentByCreator(creatorId: Int) -> AnyPublisher<Response, MoyaError> {
return api.requestPublisher(.getPopularContentByCreator(creatorId: creatorId))
}
}

View File

@ -0,0 +1,104 @@
//
// 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: {},
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.salesCountRankContentList,
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()
}
}
}
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12))
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)
.padding(.bottom, 66.7)
Spacer()
}
}
}
}
#Preview {
ContentMainTabContentView()
}

View File

@ -0,0 +1,231 @@
//
// 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 salesCountRankContentList: [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.salesCountRankContentList = data.salesCountRankContentList
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.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)
}
}

View File

@ -43,6 +43,7 @@ struct ContentMainTabRankContentView: View {
.onTapGesture { onClickMore() }
}
}
.padding(.horizontal, 13.3)
if !sortList.isEmpty {
ContentMainRankingSortView(
@ -97,6 +98,7 @@ struct ContentMainTabRankContentView: View {
}
}
}
.padding(.horizontal, 13.3)
.frame(height: 207)
}
}

View File

@ -0,0 +1,231 @@
//
// 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("태그별 추천 콘텐츠")
.font(.custom(Font.bold.rawValue, size: 18.3))
.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])
.font(.custom(Font.medium.rawValue, size: 10))
.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) {
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)
VStack(spacing: 0) {
Spacer()
HStack(spacing: 0) {
HStack(spacing: 2) {
if item.price > 0 {
Image("ic_card_can_gray")
Text("\(item.price)")
.font(.custom(Font.medium.rawValue, size: 8.5))
.foregroundColor(Color.white)
} else {
Text("무료")
.font(.custom(Font.medium.rawValue, size: 8.5))
.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)
.font(.custom(Font.medium.rawValue, size: 8.5))
.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)
.font(.custom(Font.medium.rawValue, size: 13.3))
.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)
.font(.custom(Font.medium.rawValue, size: 12))
.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"
),
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"
),
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"
),
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"
)
],
selectTag: { _ in }
)
}

View File

@ -0,0 +1,20 @@
//
// 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 salesCountRankContentList: [GetAudioContentRankingItem]
let eventBannerList: GetEventResponse
let tagList: [String]
let tagCurationContentList: [GetAudioContentMainItem]
let curationList: [GetContentCurationResponse]
}

View File

@ -23,7 +23,8 @@ struct ContentByChannelView: View {
VStack(alignment: .leading, spacing: 20) {
Text(title)
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color(hex: "eeeeee"))
.foregroundColor(.grayee)
.padding(.horizontal, 13.3)
ScrollView(.horizontal) {
HStack(spacing: 22) {
@ -41,6 +42,7 @@ struct ContentByChannelView: View {
}
}
}
.padding(.horizontal, 13.3)
}
LazyVGrid(columns: columns, spacing: 13.3) {
@ -111,6 +113,7 @@ struct ContentByChannelView: View {
.setAppStep(step: .creatorDetail(userId: content.creatorId))
}
}
.padding(.horizontal, 13.3)
.onTapGesture {
AppState
.shared

View File

@ -0,0 +1,56 @@
//
// ContentMainContentThemeView.swift
// SodaLive
//
// Created by klaus on 2/21/25.
//
import SwiftUI
struct ContentMainContentThemeView: View {
let themeList: [String]
let selectTheme: (String) -> Void
@Binding var selectedTheme: String
var body: some View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .top, spacing: 8) {
ForEach(0..<themeList.count, id: \.self) { index in
let theme = themeList[index]
Text(theme)
.font(.custom(Font.medium.rawValue, size: 14.7))
.foregroundColor(selectedTheme == theme ? Color.button : Color.gray77)
.padding(.horizontal, 13.3)
.padding(.vertical, 9.3)
.border(
selectedTheme == theme ? Color.button : Color.grayee,
width: 1
)
.cornerRadius(16.7)
.overlay(
RoundedRectangle(cornerRadius: CGFloat(16.7))
.stroke(lineWidth: 1)
.foregroundColor(selectedTheme == theme ? Color.button : Color.grayee)
)
.onTapGesture {
if selectedTheme != theme {
selectedTheme = theme
selectTheme(theme)
}
}
}
}
.padding(.horizontal, 13.3)
}
}
}
#Preview {
ContentMainContentThemeView(
themeList: ["전체", "test", "test2"],
selectTheme: { _ in },
selectedTheme: .constant("전체")
)
}

View File

@ -0,0 +1,87 @@
//
// 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)
.font(.custom(Font.bold.rawValue, size: 18.3))
.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"
),
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"
),
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"
)
]
)
]
)
}

View File

@ -0,0 +1,97 @@
//
// 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)
.font(.custom(Font.bold.rawValue, size: 18.3))
.foregroundColor(Color(hex: "eeeeee"))
Spacer()
Image("ic_forward")
.resizable()
.frame(width: 20, height: 20)
.onTapGesture {
}
}
.padding(.horizontal, 13.3)
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 {
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"
),
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"
),
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"
)
],
selectTheme: { _ in }
)
}

View File

@ -116,7 +116,7 @@ struct ContentMainViewV2: View {
case .SERIES:
ContentMainTabSeriesView()
case .CONTENT:
EmptyView()
ContentMainTabContentView()
case .ALARM:
EmptyView()
case .ASMR:

View File

@ -0,0 +1,11 @@
//
// GetContentCurationResponse.swift
// SodaLive
//
// Created by klaus on 2/21/25.
//
struct GetContentCurationResponse: Decodable {
let title: String
let items: [GetAudioContentMainItem]
}

View File

@ -212,16 +212,10 @@ struct ContentMainTabHomeView: View {
contentList: viewModel.rankContentList
)
.padding(.top, 30)
.padding(.horizontal, 13.3)
}
if viewModel.eventBannerList.count > 0 {
SectionEventBannerView(items: viewModel.eventBannerList)
.frame(
width: viewModel.eventBannerList.count > 0 ? screenSize().width : 0,
height: viewModel.eventBannerList.count > 0 ? screenSize().width * 300 / 1000 : 0,
alignment: .center
)
.padding(.top, 30)
}
@ -235,7 +229,6 @@ struct ContentMainTabHomeView: View {
}
)
.padding(.top, 30)
.padding(.horizontal, 13.3)
}
Text("""

View File

@ -70,11 +70,6 @@ struct ContentMainTabSeriesView: View {
if !viewModel.eventBannerList.isEmpty {
SectionEventBannerView(items: viewModel.eventBannerList)
.frame(
width: viewModel.eventBannerList.count > 0 ? screenSize().width : 0,
height: viewModel.eventBannerList.count > 0 ? screenSize().width * 300 / 1000 : 0,
alignment: .center
)
.padding(.top, 30)
}
@ -88,6 +83,21 @@ struct ContentMainTabSeriesView: View {
}
}
}
.popup(isPresented: $viewModel.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) {
HStack {
Spacer()
Text(viewModel.errorMessage)
.padding(.vertical, 13.3)
.frame(width: screenSize().width - 66.7, alignment: .center)
.font(.custom(Font.medium.rawValue, size: 12))
.background(Color.button)
.foregroundColor(Color.white)
.multilineTextAlignment(.leading)
.cornerRadius(20)
.padding(.bottom, 66.7)
Spacer()
}
}
}
}

View File

@ -10,13 +10,12 @@ import Kingfisher
struct SectionEventBannerView: View {
@State private var currentIndex = -1
@State private var currentIndex = 0
@State private var timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect()
let items: [EventItem]
var body: some View {
GeometryReader { proxy in
VStack(spacing: 13.3) {
TabView(selection: $currentIndex) {
ForEach(0..<items.count, id: \.self) { index in
@ -26,13 +25,17 @@ struct SectionEventBannerView: View {
.cancelOnDisappear(true)
.downsampling(
size: CGSize(
width: proxy.size.width,
height: proxy.size.height
width: screenSize().width,
height: screenSize().width * 300 / 1000
)
)
.resizable()
.scaledToFill()
.frame(width: proxy.size.width, height: proxy.size.height, alignment: .center)
.frame(
width: screenSize().width,
height: screenSize().width * 300 / 1000,
alignment: .center
)
.tag(index)
.onTapGesture {
if let _ = item.detailImageUrl {
@ -46,13 +49,17 @@ struct SectionEventBannerView: View {
.cancelOnDisappear(true)
.downsampling(
size: CGSize(
width: proxy.size.width,
height: proxy.size.height
width: screenSize().width,
height: screenSize().width * 300 / 1000
)
)
.resizable()
.scaledToFill()
.frame(width: proxy.size.width, height: proxy.size.height, alignment: .center)
.frame(
width: screenSize().width,
height: screenSize().width * 300 / 1000,
alignment: .center
)
.tag(index)
.onTapGesture {
if let _ = item.detailImageUrl {
@ -66,15 +73,15 @@ struct SectionEventBannerView: View {
}
.tabViewStyle(PageTabViewStyle(indexDisplayMode: .never))
.frame(
width: proxy.size.width,
height: proxy.size.height,
width: screenSize().width,
height: screenSize().width * 300 / 1000,
alignment: .center
)
HStack(spacing: 4) {
ForEach(0..<items.count, id: \.self) { index in
Capsule()
.foregroundColor(index == currentIndex ? Color(hex: "3bb9f1") : Color(hex: "909090"))
.foregroundColor(index == currentIndex ? .button : .gray90)
.frame(
width: index == currentIndex ? 18 : 6,
height: 6
@ -102,7 +109,6 @@ struct SectionEventBannerView: View {
}
}
}
}
struct SectionEventBannerView_Previews: PreviewProvider {
static var previews: some View {

View File

@ -73,11 +73,6 @@ struct LiveView: View {
if viewModel.eventBannerItems.count > 0 {
SectionEventBannerView(items: viewModel.eventBannerItems)
.frame(
width: viewModel.eventBannerItems.count > 0 ? screenSize().width : 0,
height: viewModel.eventBannerItems.count > 0 ? screenSize().width * 300 / 1000 : 0,
alignment: .center
)
}
if viewModel.communityPostItems.count > 0 {