diff --git a/SodaLive/Resources/Assets.xcassets/ic_card_can_gray_32.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_card_can_gray_32.imageset/Contents.json new file mode 100644 index 0000000..71a95d9 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/ic_card_can_gray_32.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_card_can_gray_32.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/ic_card_can_gray_32.imageset/ic_card_can_gray_32.png b/SodaLive/Resources/Assets.xcassets/ic_card_can_gray_32.imageset/ic_card_can_gray_32.png new file mode 100644 index 0000000..5ff0e30 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/ic_card_can_gray_32.imageset/ic_card_can_gray_32.png differ diff --git a/SodaLive/Resources/Assets.xcassets/ic_category_alarm.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_category_alarm.imageset/Contents.json new file mode 100644 index 0000000..82cd410 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/ic_category_alarm.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_category_alarm.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/ic_category_alarm.imageset/ic_category_alarm.png b/SodaLive/Resources/Assets.xcassets/ic_category_alarm.imageset/ic_category_alarm.png new file mode 100644 index 0000000..65c1ed1 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/ic_category_alarm.imageset/ic_category_alarm.png differ diff --git a/SodaLive/Resources/Assets.xcassets/ic_category_asmr.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_category_asmr.imageset/Contents.json new file mode 100644 index 0000000..1e88b32 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/ic_category_asmr.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_category_asmr.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/ic_category_asmr.imageset/ic_category_asmr.png b/SodaLive/Resources/Assets.xcassets/ic_category_asmr.imageset/ic_category_asmr.png new file mode 100644 index 0000000..f411f57 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/ic_category_asmr.imageset/ic_category_asmr.png differ diff --git a/SodaLive/Resources/Assets.xcassets/ic_category_audio_book.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_category_audio_book.imageset/Contents.json new file mode 100644 index 0000000..b06b199 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/ic_category_audio_book.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_category_audio_book.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/ic_category_audio_book.imageset/ic_category_audio_book.png b/SodaLive/Resources/Assets.xcassets/ic_category_audio_book.imageset/ic_category_audio_book.png new file mode 100644 index 0000000..4c107a3 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/ic_category_audio_book.imageset/ic_category_audio_book.png differ diff --git a/SodaLive/Resources/Assets.xcassets/ic_category_audio_toon.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_category_audio_toon.imageset/Contents.json new file mode 100644 index 0000000..f8cd459 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/ic_category_audio_toon.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_category_audio_toon.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/ic_category_audio_toon.imageset/ic_category_audio_toon.png b/SodaLive/Resources/Assets.xcassets/ic_category_audio_toon.imageset/ic_category_audio_toon.png new file mode 100644 index 0000000..55aac47 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/ic_category_audio_toon.imageset/ic_category_audio_toon.png differ diff --git a/SodaLive/Resources/Assets.xcassets/ic_category_content.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_category_content.imageset/Contents.json new file mode 100644 index 0000000..2097cd0 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/ic_category_content.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_category_content.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/ic_category_content.imageset/ic_category_content.png b/SodaLive/Resources/Assets.xcassets/ic_category_content.imageset/ic_category_content.png new file mode 100644 index 0000000..98eacd4 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/ic_category_content.imageset/ic_category_content.png differ diff --git a/SodaLive/Resources/Assets.xcassets/ic_category_free.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_category_free.imageset/Contents.json new file mode 100644 index 0000000..fc17503 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/ic_category_free.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_category_free.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/ic_category_free.imageset/ic_category_free.png b/SodaLive/Resources/Assets.xcassets/ic_category_free.imageset/ic_category_free.png new file mode 100644 index 0000000..1085485 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/ic_category_free.imageset/ic_category_free.png differ diff --git a/SodaLive/Resources/Assets.xcassets/ic_category_replay.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_category_replay.imageset/Contents.json new file mode 100644 index 0000000..098a34e --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/ic_category_replay.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_category_replay.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/ic_category_replay.imageset/ic_category_replay.png b/SodaLive/Resources/Assets.xcassets/ic_category_replay.imageset/ic_category_replay.png new file mode 100644 index 0000000..9309716 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/ic_category_replay.imageset/ic_category_replay.png differ diff --git a/SodaLive/Resources/Assets.xcassets/ic_category_series.imageset/Contents.json b/SodaLive/Resources/Assets.xcassets/ic_category_series.imageset/Contents.json new file mode 100644 index 0000000..c81b5a9 --- /dev/null +++ b/SodaLive/Resources/Assets.xcassets/ic_category_series.imageset/Contents.json @@ -0,0 +1,21 @@ +{ + "images" : [ + { + "idiom" : "universal", + "scale" : "1x" + }, + { + "idiom" : "universal", + "scale" : "2x" + }, + { + "filename" : "ic_category_series.png", + "idiom" : "universal", + "scale" : "3x" + } + ], + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/SodaLive/Resources/Assets.xcassets/ic_category_series.imageset/ic_category_series.png b/SodaLive/Resources/Assets.xcassets/ic_category_series.imageset/ic_category_series.png new file mode 100644 index 0000000..c5d7724 Binary files /dev/null and b/SodaLive/Resources/Assets.xcassets/ic_category_series.imageset/ic_category_series.png differ diff --git a/SodaLive/Sources/Content/ContentApi.swift b/SodaLive/Sources/Content/ContentApi.swift index e8f7dc6..3d8c534 100644 --- a/SodaLive/Sources/Content/ContentApi.swift +++ b/SodaLive/Sources/Content/ContentApi.swift @@ -38,6 +38,8 @@ enum ContentApi { case unpinContent(contentId: Int) case getAudioContentByTheme(themeId: Int, isAdultContentVisible: Bool, contentType: ContentType, page: Int, size: Int, sort: ContentAllByThemeViewModel.Sort) case generateUrl(contentId: Int) + case getContentMainHome + case getPopularContentByCreator(creatorId: Int) } extension ContentApi: TargetType { @@ -133,6 +135,12 @@ extension ContentApi: TargetType { case .generateUrl(let contentId): return "/audio-content/\(contentId)/generate-url" + + case .getContentMainHome: + return "/v2/audio-content/main/home" + + case .getPopularContentByCreator: + return "/v2/audio-content/main/home/popular-content-by-creator" } } @@ -147,6 +155,9 @@ extension ContentApi: TargetType { case .getMainBannerList, .getMainOrderList, .getNewContentUploadCreatorList, .getCurationList, .getAudioContentByTheme: return .get + case .getContentMainHome, .getPopularContentByCreator: + return .get + case .likeContent, .modifyAudioContent, .modifyComment, .unpinContent: return .put @@ -300,6 +311,13 @@ extension ContentApi: TargetType { ] as [String : Any] return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) + + case .getContentMainHome: + return .requestPlain + + case .getPopularContentByCreator(let creatorId): + let parameters = ["creatorId": creatorId] + return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) } } diff --git a/SodaLive/Sources/Content/Main/GetAudioContentMainResponse.swift b/SodaLive/Sources/Content/Main/GetAudioContentMainResponse.swift index e4c8b17..60b21dd 100644 --- a/SodaLive/Sources/Content/Main/GetAudioContentMainResponse.swift +++ b/SodaLive/Sources/Content/Main/GetAudioContentMainResponse.swift @@ -8,7 +8,7 @@ import Foundation struct GetAudioContentMainResponse: Decodable { - let newContentUploadCreatorList: [GetNewContentUploadCreator] + let newContentUploadCreatorList: [ContentCreatorResponse] let bannerList: [GetAudioContentBannerResponse] let orderList: [GetAudioContentMainItem] let themeList: [String] @@ -33,9 +33,10 @@ struct GetAudioContentRankingItem: Decodable { let duration: String let creatorId: Int let creatorNickname: String + let creatorProfileImageUrl: String } -struct GetNewContentUploadCreator: Decodable { +struct ContentCreatorResponse: Decodable { let creatorId: Int let creatorNickname: String let creatorProfileImageUrl: String diff --git a/SodaLive/Sources/Content/Main/V2/Content/ContentMainTabRankContentView.swift b/SodaLive/Sources/Content/Main/V2/Content/ContentMainTabRankContentView.swift new file mode 100644 index 0000000..8d0edac --- /dev/null +++ b/SodaLive/Sources/Content/Main/V2/Content/ContentMainTabRankContentView.swift @@ -0,0 +1,154 @@ +// +// ContentMainTabRankContentView.swift +// SodaLive +// +// Created by klaus on 2/20/25. +// + +import SwiftUI + +import Kingfisher + +struct ContentMainTabRankContentView: View { + + let rows = [ + GridItem(.fixed(60), alignment: .leading), + GridItem(.fixed(60), alignment: .leading), + GridItem(.fixed(60), alignment: .leading) + ] + + let title: String + + let isMore: Bool + let onClickMore: () -> Void + + let sortList: [String] + let onClickSort: (String) -> Void + + let contentList: [GetAudioContentRankingItem] + + @State private var selectedSort = "" + + var body: some View { + VStack(spacing: 13.3) { + HStack(spacing: 0) { + Text(title) + .font(.custom(Font.bold.rawValue, size: 18.3)) + .foregroundColor(Color(hex: "eeeeee")) + + Spacer() + + if isMore { + Image("ic_forward") + .onTapGesture { onClickMore() } + } + } + + if !sortList.isEmpty { + ContentMainRankingSortView( + sorts: sortList, + selectSort: { + selectedSort = $0 + onClickSort($0) + }, + selectedSort: $selectedSort + ) + } + + ScrollView(.horizontal, showsIndicators: false) { + LazyHGrid(rows: rows, spacing: 13.3) { + ForEach(0.. Void + + @State private var selectedCreatorId = 0 + + let columns = [GridItem(.flexible()), GridItem(.flexible())] + + var body: some View { + VStack(alignment: .leading, spacing: 20) { + Text(title) + .font(.custom(Font.bold.rawValue, size: 18.3)) + .foregroundColor(Color(hex: "eeeeee")) + + ScrollView(.horizontal) { + HStack(spacing: 22) { + ForEach(0.. 0 { + Image("ic_card_can_gray_32") + } + + Text(content.price > 0 ? "\(content.price)" : "무료") + .font(.custom(Font.medium.rawValue, size: 12)) + .foregroundColor(Color.white) + } + .padding(4) + .background(Color.gray33.opacity(0.7)) + .cornerRadius(10) + + Spacer() + + Text(content.duration) + .font(.custom(Font.medium.rawValue, size: 12)) + .foregroundColor(Color.white) + .padding(4) + .background(Color.gray33.opacity(0.7)) + .cornerRadius(10) + } + .padding(.horizontal, 2.7) + .padding(.bottom, 2.7) + } + + Text(content.title) + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(.grayd2) + .lineLimit(1) + + HStack(spacing: 5.3) { + KFImage(URL(string: content.creatorProfileImageUrl)) + .cancelOnDisappear(true) + .downsampling(size: CGSize(width: 21, height: 21)) + .resizable() + .frame(width: 21, height: 21) + .clipShape(Circle()) + .clipped() + + Text(content.creatorNickname) + .font(.custom(Font.medium.rawValue, size: 10)) + .foregroundColor(.gray77) + } + .onTapGesture { + AppState + .shared + .setAppStep(step: .creatorDetail(userId: content.creatorId)) + } + } + .onTapGesture { + AppState + .shared + .setAppStep(step: .contentDetail(contentId: content.contentId)) + } + } + } + + } + .onAppear { + if !self.creatorList.isEmpty { + selectedCreatorId = creatorList[0].creatorId + } + } + } +} + +#Preview { + ContentByChannelView( + title: "채널별 인기 콘텐츠", + creatorList: [ + ContentCreatorResponse( + creatorId: 1, + creatorNickname: "유저1", + creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png" + ), + ContentCreatorResponse( + creatorId: 2, + creatorNickname: "유저2", + creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png" + ) + ], + contentList: [ + GetAudioContentRankingItem( + contentId: 1, + title: "안녕하세요 오늘은 커버곡을 들려드릴께요....안녕하세요 오늘은 커버곡을 들려드릴께요....", + coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", + themeStr: "커버곡", + price: 100, + duration: "00:30:20", + creatorId: 1, + creatorNickname: "유저1", + creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png" + ), + GetAudioContentRankingItem( + contentId: 2, + title: "안녕하세요 오늘은 커버곡을 들려드릴께요....", + coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", + themeStr: "커버곡", + price: 0, + duration: "00:30:20", + creatorId: 1, + creatorNickname: "유저1", + creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png" + ), + GetAudioContentRankingItem( + contentId: 3, + title: "안녕하세요 오늘은 커버곡을 들려드릴께요....안녕하세요 오늘은 커버곡을 들려드릴께요....", + coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", + themeStr: "커버곡", + price: 50, + duration: "00:30:20", + creatorId: 1, + creatorNickname: "유저1", + creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png" + ), + GetAudioContentRankingItem( + contentId: 4, + title: "안녕하세요 오늘은 커버곡을 들려드릴께요....안녕하세요 오늘은 커버곡을 들려드릴께요....", + coverImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", + themeStr: "커버곡", + price: 50, + duration: "00:30:20", + creatorId: 1, + creatorNickname: "유저1", + creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png" + ) + ] + ) { _ in } +} diff --git a/SodaLive/Sources/Content/Main/V2/ContentCreatorView.swift b/SodaLive/Sources/Content/Main/V2/ContentCreatorView.swift new file mode 100644 index 0000000..daa4a23 --- /dev/null +++ b/SodaLive/Sources/Content/Main/V2/ContentCreatorView.swift @@ -0,0 +1,54 @@ +// +// ContentCreatorView.swift +// SodaLive +// +// Created by klaus on 2/20/25. +// + +import SwiftUI +import Kingfisher + +struct ContentCreatorView: View { + + let isSelected: Bool + let item: ContentCreatorResponse + + var body: some View { + VStack(spacing: 13.3) { + KFImage(URL(string: item.creatorProfileImageUrl)) + .cancelOnDisappear(true) + .downsampling(size: CGSize(width: 60, height: 60)) + .resizable() + .frame(width: 60, height: 60) + .clipShape(Circle()) + .overlay( + Circle() + .strokeBorder(lineWidth: 3) + .foregroundColor( + .button + .opacity(isSelected ? 1 : 0) + ) + ) + + Text(item.creatorNickname) + .font(.custom(Font.medium.rawValue, size: 11.3)) + .foregroundColor( + isSelected ? + Color.button : + Color.graybb + + ) + } + } +} + +#Preview { + ContentCreatorView( + isSelected: true, + item: ContentCreatorResponse( + creatorId: 1, + creatorNickname: "유저1", + creatorProfileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png" + ) + ) +} diff --git a/SodaLive/Sources/Content/Main/V2/ContentMainBannerViewV2.swift b/SodaLive/Sources/Content/Main/V2/ContentMainBannerViewV2.swift new file mode 100644 index 0000000..3ff828b --- /dev/null +++ b/SodaLive/Sources/Content/Main/V2/ContentMainBannerViewV2.swift @@ -0,0 +1,134 @@ +// +// ContentMainBannerViewV2.swift +// SodaLive +// +// Created by klaus on 2/20/25. +// + +import SwiftUI + +import Kingfisher + +struct ContentMainBannerViewV2: View { + + let bannerList: [GetAudioContentBannerResponse] + + @State var currentIndex = 0 + @State var timer = Timer.publish(every: 3, on: .main, in: .common).autoconnect() + + @State var width: CGFloat = 0 + @State var height: CGFloat = 0 + + var body: some View { + VStack(spacing: 0) { + TabView(selection: $currentIndex) { + ForEach(0.. 0, let url = URL(string: link), UIApplication.shared.canOpenURL(url) { + UIApplication.shared.open(url) + } + } + } + } +} diff --git a/SodaLive/Sources/Content/Main/V2/Home/Category/ContentMainTabCategoryView.swift b/SodaLive/Sources/Content/Main/V2/Home/Category/ContentMainTabCategoryView.swift new file mode 100644 index 0000000..40b2256 --- /dev/null +++ b/SodaLive/Sources/Content/Main/V2/Home/Category/ContentMainTabCategoryView.swift @@ -0,0 +1,38 @@ +// +// 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) + .font(.custom(Font.medium.rawValue, size: 12)) + .foregroundColor(.gray77) + } + .onTapGesture { + onClick() + } + } +} + +#Preview { + ContentMainTabCategoryView( + imageName: "ic_category_series", + title: "시리즈", + onClick: {} + ) +} diff --git a/SodaLive/Sources/Content/Main/V2/Home/ContentMainTabHomeNoticeView.swift b/SodaLive/Sources/Content/Main/V2/Home/ContentMainTabHomeNoticeView.swift new file mode 100644 index 0000000..7e6acca --- /dev/null +++ b/SodaLive/Sources/Content/Main/V2/Home/ContentMainTabHomeNoticeView.swift @@ -0,0 +1,47 @@ +// +// 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) + .font(.custom(Font.medium.rawValue, size: 13.3)) + .foregroundColor(.white) + + Spacer() + + Text("자세히 >") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .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)) + } +} diff --git a/SodaLive/Sources/Content/Main/V2/Home/ContentMainTabHomeRepository.swift b/SodaLive/Sources/Content/Main/V2/Home/ContentMainTabHomeRepository.swift new file mode 100644 index 0000000..f723da6 --- /dev/null +++ b/SodaLive/Sources/Content/Main/V2/Home/ContentMainTabHomeRepository.swift @@ -0,0 +1,23 @@ +// +// ContentMainTabHomeRepository.swift +// SodaLive +// +// Created by klaus on 2/20/25. +// + +import Foundation +import CombineMoya +import Combine +import Moya + +class ContentMainTabHomeRepository { + private let api = MoyaProvider() + + func getContentMainHome() -> AnyPublisher { + return api.requestPublisher(.getContentMainHome) + } + + func getPopularContentByCreator(creatorId: Int) -> AnyPublisher { + return api.requestPublisher(.getPopularContentByCreator(creatorId: creatorId)) + } +} diff --git a/SodaLive/Sources/Content/Main/V2/Home/ContentMainTabHomeView.swift b/SodaLive/Sources/Content/Main/V2/Home/ContentMainTabHomeView.swift new file mode 100644 index 0000000..dfa50dd --- /dev/null +++ b/SodaLive/Sources/Content/Main/V2/Home/ContentMainTabHomeView.swift @@ -0,0 +1,238 @@ +// +// ContentMainTabHomeView.swift +// SodaLive +// +// Created by klaus on 2/20/25. +// + +import SwiftUI + +struct ContentMainTabHomeView: View { + + @StateObject var viewModel = ContentMainTabHomeViewModel() + + var body: some View { + BaseView(isLoading: $viewModel.isLoading) { + ScrollView(.vertical, showsIndicators: false) { + VStack(alignment: .leading, spacing: 0) { + HStack(spacing: 0) { + Text("콘텐츠 마켓") + .font(.custom(Font.bold.rawValue, size: 21.3)) + .foregroundColor(Color.button) + + Spacer() + + Image("ic_content_keep") + .onTapGesture { + AppState.shared.setAppStep(step: .myBox(currentTab: .orderlist)) + } + } + .padding(.bottom, 26.7) + .padding(.horizontal, 13.3) + + if let notice = viewModel.noticeItem { + ContentMainTabHomeNoticeView(notice: notice) { + AppState.shared + .setAppStep(step: .noticeDetail(notice: $0)) + } + .padding(.horizontal, 13.3) + } + + if viewModel.bannerList.count > 0 { + ContentMainBannerViewV2(bannerList: viewModel.bannerList) + .padding(.top, 30) + .padding(.horizontal, 13.3) + } + + HStack(spacing: 0) { + Image("ic_title_search_black") + + Text("채널명을 입력해 보세요") + .font(.custom(Font.medium.rawValue, size: 13.3)) + .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: .searchChannel) + } + + VStack(spacing: 13.3) { + HStack(spacing: 0) { + ContentMainTabCategoryView( + imageName: "ic_category_series", + title: "시리즈", + onClick: {} + ) + .frame(maxWidth: .infinity) + + ContentMainTabCategoryView( + imageName: "ic_category_content", + title: "단편", + onClick: {} + ) + .frame(maxWidth: .infinity) + + ContentMainTabCategoryView( + imageName: "ic_category_audio_book", + title: "오디오북", + onClick: {} + ) + .frame(maxWidth: .infinity) + + ContentMainTabCategoryView( + imageName: "ic_category_alarm", + title: "모닝콜", + onClick: {} + ) + .frame(maxWidth: .infinity) + } + + HStack(spacing: 0) { + ContentMainTabCategoryView( + imageName: "ic_category_asmr", + title: "ASMR", + onClick: {} + ) + .frame(maxWidth: .infinity) + + ContentMainTabCategoryView( + imageName: "ic_category_replay", + title: "다시듣기", + onClick: {} + ) + .frame(maxWidth: .infinity) + + ContentMainTabCategoryView( + imageName: "ic_category_audio_toon", + title: "오디오툰", + onClick: {} + ) + .frame(maxWidth: .infinity) + + ContentMainTabCategoryView( + imageName: "ic_category_free", + title: "무료", + onClick: {} + ) + .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: { + AppState.shared.setAppStep(step: .contentRankingAll) + }, + sortList: !viewModel.rankSortTypeList.isEmpty ? + viewModel.rankSortTypeList : + [], + onClickSort: { viewModel.getContentRanking(sort: $0) }, + 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) + } + + if !viewModel.contentRankCreatorList.isEmpty { + ContentByChannelView( + title: "채널별 인기 콘텐츠", + creatorList: viewModel.contentRankCreatorList, + contentList: viewModel.salesCountRankContentList, + onClickCreator: { + viewModel.getPopularContentByCreator(creatorId: $0) + } + ) + .padding(.top, 30) + .padding(.horizontal, 13.3) + } + + Text(""" +- 회사명 : 주식회사 소다라이브 + +- 대표자 : 이재형 + +- 주소 : 경기도 성남시 분당구 황새울로335번길 10, 5층 563A호 + +- 사업자등록번호 : 870-81-03220 + +- 통신판매업신고 : 제2024-성남분당B-1012호 + +- 고객센터 : 02.2055.1477 (이용시간 10:00~19:00) + +- 대표 이메일 : sodalive.official@gmail.com +""") + .font(.custom(Font.medium.rawValue, size: 11)) + .foregroundColor(Color.gray77) + .padding(.top, 30) + .padding(.horizontal, 13.3) + } + .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 { + ContentMainTabHomeView() +} diff --git a/SodaLive/Sources/Content/Main/V2/Home/ContentMainTabHomeViewModel.swift b/SodaLive/Sources/Content/Main/V2/Home/ContentMainTabHomeViewModel.swift new file mode 100644 index 0000000..95ce5d4 --- /dev/null +++ b/SodaLive/Sources/Content/Main/V2/Home/ContentMainTabHomeViewModel.swift @@ -0,0 +1,152 @@ +// +// ContentMainTabHomeViewModel.swift +// SodaLive +// +// Created by klaus on 2/20/25. +// + +import Foundation +import Combine + +final class ContentMainTabHomeViewModel: ObservableObject { + + private let repository = ContentMainTabHomeRepository() + private let contentRepository = ContentRepository() + private var subscription = Set() + + @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.self, from: responseData) + + if let data = decoded.data, decoded.success { + self.noticeItem = data.latestNotice + self.bannerList = data.bannerList + self.rankCreatorResponse = data.rankCreatorList + 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 + contentRepository.getContentRanking(page: 1, size: 12, 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.self, from: responseData) + + if let data = decoded.data, decoded.success { + self.rankContentList = 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) + } + + 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) + } +} diff --git a/SodaLive/Sources/Content/Main/V2/Home/GetContentMainTabHomeResponse.swift b/SodaLive/Sources/Content/Main/V2/Home/GetContentMainTabHomeResponse.swift new file mode 100644 index 0000000..e79b04f --- /dev/null +++ b/SodaLive/Sources/Content/Main/V2/Home/GetContentMainTabHomeResponse.swift @@ -0,0 +1,18 @@ +// +// 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] +} diff --git a/SodaLive/Sources/Content/Main/V2/Home/Rank/ContentMainTabHomeRankCreatorView.swift b/SodaLive/Sources/Content/Main/V2/Home/Rank/ContentMainTabHomeRankCreatorView.swift new file mode 100644 index 0000000..a1a16c8 --- /dev/null +++ b/SodaLive/Sources/Content/Main/V2/Home/Rank/ContentMainTabHomeRankCreatorView.swift @@ -0,0 +1,170 @@ +// +// ContentMainTabHomeRankCreatorView.swift +// SodaLive +// +// Created by klaus on 2/20/25. +// + +import SwiftUI +import Kingfisher + +struct ContentMainTabHomeRankCreatorView: View { + + 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 { + VStack(spacing: 8) { + Text("\(desc)") + .font(.custom(Font.bold.rawValue, size: 14.7)) + .foregroundColor(Color.grayee) + + Text("※ 인기 순위는 매주 업데이트됩니다.") + .font(.custom(Font.light.rawValue, size: 13.3)) + .foregroundColor(Color.graybb) + } + .padding(.vertical, 8) + .frame(maxWidth: .infinity) + .background(Color.gray22) + .padding(.top, 13.3) + } + + if let coloredTitle = response.coloredTitle, let color = response.color { + let titleArray = response.title.components(separatedBy: coloredTitle) + HStack(spacing: 0) { + Text(titleArray[0]) + .font(.custom(Font.bold.rawValue, size: 18.3)) + .foregroundColor(Color.grayee) + + Text(coloredTitle) + .font(.custom(Font.bold.rawValue, size: 18.3)) + .foregroundColor(Color(hex: color)) + + if titleArray.count > 1 { + Text(titleArray[1]) + .font(.custom(Font.bold.rawValue, size: 18.3)) + .foregroundColor(Color.grayee) + } + } + .padding(.top, 30) + } else { + Text(response.title) + .font(.custom(Font.bold.rawValue, size: 18.3)) + .foregroundColor(Color.grayee) + .padding(.top, 30) + } + + ScrollView(.horizontal, showsIndicators: false) { + HStack(spacing: 13.3) { + ForEach(0..