콘텐츠 메인
- 모닝콜 탭 UI 페이지 생성
This commit is contained in:
		| @@ -50,6 +50,9 @@ enum ContentApi { | |||||||
|     case getContentMainNewContentOfTheme(theme: String, isAdultContentVisible: Bool, contentType: ContentType) |     case getContentMainNewContentOfTheme(theme: String, isAdultContentVisible: Bool, contentType: ContentType) | ||||||
|     case getDailyContentRanking(sortType: String) |     case getDailyContentRanking(sortType: String) | ||||||
|     case getRecommendContentByTag(tag: String) |     case getRecommendContentByTag(tag: String) | ||||||
|  |      | ||||||
|  |     case getContentMainAlarm | ||||||
|  |     case getContentMainAlarmAll(theme: String, page: Int, size: Int) | ||||||
| } | } | ||||||
|  |  | ||||||
| extension ContentApi: TargetType { | extension ContentApi: TargetType { | ||||||
| @@ -172,6 +175,12 @@ extension ContentApi: TargetType { | |||||||
|              |              | ||||||
|         case .getRecommendContentByTag: |         case .getRecommendContentByTag: | ||||||
|             return "/v2/audio-content/main/content/recommend-content-by-tag" |             return "/v2/audio-content/main/content/recommend-content-by-tag" | ||||||
|  |              | ||||||
|  |         case .getContentMainAlarm: | ||||||
|  |             return "/v2/audio-content/main/alarm" | ||||||
|  |              | ||||||
|  |         case .getContentMainAlarmAll: | ||||||
|  |             return "/v2/audio-content/main/alarm/all" | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|      |      | ||||||
| @@ -187,7 +196,7 @@ extension ContentApi: TargetType { | |||||||
|             return .get |             return .get | ||||||
|              |              | ||||||
|         case .getContentMainHome, .getPopularContentByCreator, .getContentMainSeries, .getRecommendSeriesListByGenre, .getRecommendSeriesByCreator, .getContentMainContent, |         case .getContentMainHome, .getPopularContentByCreator, .getContentMainSeries, .getRecommendSeriesListByGenre, .getRecommendSeriesByCreator, .getContentMainContent, | ||||||
|                 .getContentMainNewContentOfTheme, .getDailyContentRanking, .getRecommendContentByTag: |                 .getContentMainNewContentOfTheme, .getDailyContentRanking, .getRecommendContentByTag, .getContentMainAlarm, .getContentMainAlarmAll: | ||||||
|             return .get |             return .get | ||||||
|              |              | ||||||
|         case .likeContent, .modifyAudioContent, .modifyComment, .unpinContent: |         case .likeContent, .modifyAudioContent, .modifyComment, .unpinContent: | ||||||
| @@ -344,7 +353,7 @@ extension ContentApi: TargetType { | |||||||
|              |              | ||||||
|             return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) |             return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) | ||||||
|              |              | ||||||
|         case .getContentMainHome, .getContentMainSeries, .getContentMainContent: |         case .getContentMainHome, .getContentMainSeries, .getContentMainContent, .getContentMainAlarm: | ||||||
|             return .requestPlain |             return .requestPlain | ||||||
|              |              | ||||||
|         case .getRecommendSeriesListByGenre(let genreId): |         case .getRecommendSeriesListByGenre(let genreId): | ||||||
| @@ -374,6 +383,15 @@ extension ContentApi: TargetType { | |||||||
|         case .getRecommendContentByTag(let tag): |         case .getRecommendContentByTag(let tag): | ||||||
|             let parameters = ["tag": tag] |             let parameters = ["tag": tag] | ||||||
|             return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) |             return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) | ||||||
|  |              | ||||||
|  |         case .getContentMainAlarmAll(let theme, let page, let size): | ||||||
|  |             let parameters = [ | ||||||
|  |                 "theme": theme, | ||||||
|  |                 "page": page - 1, | ||||||
|  |                 "size": size | ||||||
|  |             ] as [String : Any] | ||||||
|  |              | ||||||
|  |             return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
|      |      | ||||||
|   | |||||||
| @@ -0,0 +1,24 @@ | |||||||
|  | // | ||||||
|  | //  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) | ||||||
|  |     } | ||||||
|  |      | ||||||
|  |     func getContentMainAlarmAll(theme: String, page: Int = 1, size: Int = 10) -> AnyPublisher<Response, MoyaError> { | ||||||
|  |         return api.requestPublisher(.getContentMainAlarmAll(theme: theme, page: page, size: size)) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,70 @@ | |||||||
|  | // | ||||||
|  | //  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: {}, | ||||||
|  |                             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() | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |         .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 { | ||||||
|  |     ContentMainTabAlarmView() | ||||||
|  | } | ||||||
| @@ -0,0 +1,106 @@ | |||||||
|  | // | ||||||
|  | //  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) | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -0,0 +1,17 @@ | |||||||
|  | // | ||||||
|  | //  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] | ||||||
|  | } | ||||||
| @@ -0,0 +1,18 @@ | |||||||
|  | // | ||||||
|  | //  ContentMainTabAsmrView.swift | ||||||
|  | //  SodaLive | ||||||
|  | // | ||||||
|  | //  Created by klaus on 2/22/25. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import SwiftUI | ||||||
|  |  | ||||||
|  | struct ContentMainTabAsmrView: View { | ||||||
|  |     var body: some View { | ||||||
|  |         Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #Preview { | ||||||
|  |     ContentMainTabAsmrView() | ||||||
|  | } | ||||||
| @@ -0,0 +1,14 @@ | |||||||
|  | // | ||||||
|  | //  ContentMainTabAsmrViewModel.swift | ||||||
|  | //  SodaLive | ||||||
|  | // | ||||||
|  | //  Created by klaus on 2/22/25. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import Foundation | ||||||
|  |  | ||||||
|  | final class ContentMainTabAsmrViewModel: ObservableObject { | ||||||
|  |     @Published var errorMessage = "" | ||||||
|  |     @Published var isShowPopup = false | ||||||
|  |     @Published var isLoading = false | ||||||
|  | } | ||||||
| @@ -118,13 +118,13 @@ struct ContentMainViewV2: View { | |||||||
|                     case .CONTENT: |                     case .CONTENT: | ||||||
|                         ContentMainTabContentView() |                         ContentMainTabContentView() | ||||||
|                     case .ALARM: |                     case .ALARM: | ||||||
|                         EmptyView() |                         ContentMainTabAlarmView() | ||||||
|                     case .ASMR: |                     case .ASMR: | ||||||
|                         EmptyView() |                         ContentMainTabAsmrView() | ||||||
|                     case .REPLAY: |                     case .REPLAY: | ||||||
|                         EmptyView() |                         ContentMainTabReplayView() | ||||||
|                     case .FREE: |                     case .FREE: | ||||||
|                         EmptyView() |                         ContentMainTabFreeView() | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             } |             } | ||||||
|   | |||||||
| @@ -0,0 +1,26 @@ | |||||||
|  | // | ||||||
|  | //  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) { | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |         } | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #Preview { | ||||||
|  |     ContentMainTabFreeView() | ||||||
|  | } | ||||||
| @@ -0,0 +1,14 @@ | |||||||
|  | // | ||||||
|  | //  ContentMainTabFreeViewModel.swift | ||||||
|  | //  SodaLive | ||||||
|  | // | ||||||
|  | //  Created by klaus on 2/22/25. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import Foundation | ||||||
|  |  | ||||||
|  | final class ContentMainTabFreeViewModel: ObservableObject { | ||||||
|  |     @Published var errorMessage = "" | ||||||
|  |     @Published var isShowPopup = false | ||||||
|  |     @Published var isLoading = false | ||||||
|  | } | ||||||
| @@ -0,0 +1,18 @@ | |||||||
|  | // | ||||||
|  | //  ContentMainTabReplayView.swift | ||||||
|  | //  SodaLive | ||||||
|  | // | ||||||
|  | //  Created by klaus on 2/22/25. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import SwiftUI | ||||||
|  |  | ||||||
|  | struct ContentMainTabReplayView: View { | ||||||
|  |     var body: some View { | ||||||
|  |         Text(/*@START_MENU_TOKEN@*/"Hello, World!"/*@END_MENU_TOKEN@*/) | ||||||
|  |     } | ||||||
|  | } | ||||||
|  |  | ||||||
|  | #Preview { | ||||||
|  |     ContentMainTabReplayView() | ||||||
|  | } | ||||||
| @@ -0,0 +1,14 @@ | |||||||
|  | // | ||||||
|  | //  ContentMainTabReplayViewModel.swift | ||||||
|  | //  SodaLive | ||||||
|  | // | ||||||
|  | //  Created by klaus on 2/22/25. | ||||||
|  | // | ||||||
|  |  | ||||||
|  | import Foundation | ||||||
|  |  | ||||||
|  | final class ContentMainTabReplayViewModel: ObservableObject { | ||||||
|  |     @Published var errorMessage = "" | ||||||
|  |     @Published var isShowPopup = false | ||||||
|  |     @Published var isLoading = false | ||||||
|  | } | ||||||
		Reference in New Issue
	
	Block a user
	 Yu Sung
					Yu Sung