오디션 배역 상세 페이지 추가
This commit is contained in:
		
							
								
								
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/ic_audition_pause.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/ic_audition_pause.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| { | ||||
|   "images" : [ | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "1x" | ||||
|     }, | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "2x" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "ic_audition_pause.png", | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "3x" | ||||
|     } | ||||
|   ], | ||||
|   "info" : { | ||||
|     "author" : "xcode", | ||||
|     "version" : 1 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/ic_audition_pause.imageset/ic_audition_pause.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/ic_audition_pause.imageset/ic_audition_pause.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 293 B | 
							
								
								
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/ic_audition_play.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/ic_audition_play.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| { | ||||
|   "images" : [ | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "1x" | ||||
|     }, | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "2x" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "ic_audition_play.png", | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "3x" | ||||
|     } | ||||
|   ], | ||||
|   "info" : { | ||||
|     "author" : "xcode", | ||||
|     "version" : 1 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/ic_audition_play.imageset/ic_audition_play.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/ic_audition_play.imageset/ic_audition_play.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.4 KiB | 
							
								
								
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/ic_heart_vote.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/ic_heart_vote.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| { | ||||
|   "images" : [ | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "1x" | ||||
|     }, | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "2x" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "ic_heart_vote.png", | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "3x" | ||||
|     } | ||||
|   ], | ||||
|   "info" : { | ||||
|     "author" : "xcode", | ||||
|     "version" : 1 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/ic_heart_vote.imageset/ic_heart_vote.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/ic_heart_vote.imageset/ic_heart_vote.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 860 B | 
| @@ -137,4 +137,6 @@ enum AppStep { | ||||
|     case myBox | ||||
|      | ||||
|     case auditionDetail(auditionId: Int) | ||||
|      | ||||
|     case auditionRoleDetail(roleId: Int) | ||||
| } | ||||
|   | ||||
| @@ -0,0 +1,72 @@ | ||||
| // | ||||
| //  AuditionApplicantItemView.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 1/7/25. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
| import Kingfisher | ||||
|  | ||||
| struct AuditionApplicantItemView: View { | ||||
|      | ||||
|     let item: GetAuditionRoleApplicantItem | ||||
|      | ||||
|     var body: some View { | ||||
|         VStack(spacing: 5.3) { | ||||
|             HStack(spacing: 13.3) { | ||||
|                 ZStack { | ||||
|                     KFImage(URL(string: item.profileImageUrl)) | ||||
|                         .cancelOnDisappear(true) | ||||
|                         .downsampling(size: CGSize(width: 40, height: 40)) | ||||
|                         .resizable() | ||||
|                         .aspectRatio(1, contentMode: .fit) | ||||
|                         .scaledToFill() | ||||
|                         .frame(width: 40, height: 40) | ||||
|                         .clipShape(Circle()) | ||||
|                      | ||||
|                     Image("ic_audition_play") | ||||
|                         .onTapGesture { | ||||
|                         } | ||||
|                 } | ||||
|                  | ||||
|                 VStack(spacing: 8) { | ||||
|                     HStack(spacing: 0) { | ||||
|                         Text(item.nickname.count > 9 ? "\(item.nickname.prefix(9))..." : item.nickname) | ||||
|                             .font(.custom(Font.medium.rawValue, size: 12)) | ||||
|                             .foregroundColor(Color.white) | ||||
|                          | ||||
|                         Spacer() | ||||
|                     } | ||||
|                 } | ||||
|                  | ||||
|                 VStack(spacing: 2.3) { | ||||
|                     Image("ic_heart_vote") | ||||
|                      | ||||
|                     Text("\(item.voteCount)") | ||||
|                         .font(.custom(Font.medium.rawValue, size: 12)) | ||||
|                         .foregroundColor(Color.gray77) | ||||
|                 } | ||||
|             } | ||||
|             .padding(.vertical, 18.7) | ||||
|              | ||||
|             Color.gray55 | ||||
|                 .frame(maxWidth: .infinity) | ||||
|                 .frame(height: 1) | ||||
|         } | ||||
|         .padding(.horizontal, 13.3) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #Preview { | ||||
|     AuditionApplicantItemView( | ||||
|         item: GetAuditionRoleApplicantItem( | ||||
|             applicantId: 1, | ||||
|             memberId: 2, | ||||
|             nickname: "유저일유저일유저일유저일", | ||||
|             profileImageUrl: "https://test-cf.sodalive.net/profile/default-profile.png", | ||||
|             voiceUrl: "", | ||||
|             voteCount: 777 | ||||
|         ) | ||||
|     ) | ||||
| } | ||||
| @@ -0,0 +1,10 @@ | ||||
| // | ||||
| //  AuditionApplicantSortType.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 1/6/25. | ||||
| // | ||||
|  | ||||
| enum AuditionApplicantSortType: String, Codable { | ||||
|     case NEWEST, LIKE | ||||
| } | ||||
| @@ -0,0 +1,20 @@ | ||||
| // | ||||
| //  GetAuditionApplicantListResponse.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 1/6/25. | ||||
| // | ||||
|  | ||||
| struct GetAuditionApplicantListResponse: Decodable { | ||||
|     let totalCount: Int | ||||
|     let items: [GetAuditionRoleApplicantItem] | ||||
| } | ||||
|  | ||||
| struct GetAuditionRoleApplicantItem: Decodable { | ||||
|     let applicantId: Int | ||||
|     let memberId: Int | ||||
|     let nickname: String | ||||
|     let profileImageUrl: String | ||||
|     let voiceUrl: String | ||||
|     let voteCount: Int | ||||
| } | ||||
| @@ -11,6 +11,8 @@ import Moya | ||||
| enum AuditionApi { | ||||
|     case getAuditionList(page: Int, size: Int) | ||||
|     case getAuditionDetail(auditionId: Int) | ||||
|     case getAuditionRoleDetail(auditionRoleId: Int) | ||||
|     case getAuditionApplicantList(auditionRoleId: Int, sortType: AuditionApplicantSortType, page: Int, size: Int) | ||||
| } | ||||
|  | ||||
| extension AuditionApi: TargetType { | ||||
| @@ -20,19 +22,24 @@ extension AuditionApi: TargetType { | ||||
|      | ||||
|     var path: String { | ||||
|         switch self { | ||||
|              | ||||
|         case .getAuditionList: | ||||
|             return "/audition" | ||||
|              | ||||
|         case .getAuditionDetail(let auditionId): | ||||
|             return "/audition/\(auditionId)" | ||||
|              | ||||
|         case .getAuditionRoleDetail(let auditionRoleId): | ||||
|             return "/audition/role/\(auditionRoleId)" | ||||
|              | ||||
|         case .getAuditionApplicantList: | ||||
|             return "/audition/applicant" | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     var method: Moya.Method { | ||||
|         switch self { | ||||
|              | ||||
|         case .getAuditionList, .getAuditionDetail: | ||||
|         case .getAuditionList, .getAuditionDetail, . getAuditionRoleDetail, .getAuditionApplicantList: | ||||
|             return .get | ||||
|         } | ||||
|     } | ||||
| @@ -48,8 +55,18 @@ extension AuditionApi: TargetType { | ||||
|              | ||||
|             return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) | ||||
|              | ||||
|         case .getAuditionDetail: | ||||
|         case .getAuditionDetail, .getAuditionRoleDetail: | ||||
|             return .requestPlain | ||||
|              | ||||
|         case .getAuditionApplicantList(let auditionRoleId, let sortType, let page, let size): | ||||
|             let parameters = [ | ||||
|                 "auditionRoleId": auditionRoleId, | ||||
|                 "page": page - 1, | ||||
|                 "size": size, | ||||
|                 "sortType": sortType | ||||
|             ] as [String : Any] | ||||
|              | ||||
|             return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|   | ||||
| @@ -20,4 +20,19 @@ final class AuditionRepository { | ||||
|     func getAuditionDetail(auditionId: Int) -> AnyPublisher<Response, MoyaError> { | ||||
|         return api.requestPublisher(.getAuditionDetail(auditionId: auditionId)) | ||||
|     } | ||||
|      | ||||
|     func getAuditionRoleDetail(auditionRoleId: Int) -> AnyPublisher<Response, MoyaError> { | ||||
|         return api.requestPublisher(.getAuditionRoleDetail(auditionRoleId: auditionRoleId)) | ||||
|     } | ||||
|      | ||||
|     func getAuditionApplicantList(auditionRoleId: Int, sortType: AuditionApplicantSortType, page: Int, size: Int) -> AnyPublisher<Response, MoyaError> { | ||||
|         return api.requestPublisher( | ||||
|             .getAuditionApplicantList( | ||||
|                 auditionRoleId: auditionRoleId, | ||||
|                 sortType: sortType, | ||||
|                 page: page, | ||||
|                 size: size | ||||
|             ) | ||||
|         ) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -45,7 +45,13 @@ struct AuditionDetailView: View { | ||||
|                              | ||||
|                             LazyVStack(spacing: 15) { | ||||
|                                 ForEach(0..<response.roleList.count, id: \.self) { | ||||
|                                     let role = response.roleList[$0] | ||||
|                                      | ||||
|                                     AuditionDetailRoleItemView(item: response.roleList[$0]) | ||||
|                                         .onTapGesture { | ||||
|                                             AppState.shared | ||||
|                                                 .setAppStep(step: .auditionRoleDetail(roleId: role.roleId)) | ||||
|                                         } | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|   | ||||
| @@ -20,7 +20,7 @@ final class AuditionDetailViewModel: ObservableObject { | ||||
|     @Published var response: GetAuditionDetailResponse? = nil | ||||
|     @Published var title: String = "보이스온" | ||||
|      | ||||
|     func getAuditionDetail(auditionId: Int, onFailure: () -> Void) { | ||||
|     func getAuditionDetail(auditionId: Int, onFailure: @escaping () -> Void) { | ||||
|         isLoading = true | ||||
|          | ||||
|         repository.getAuditionDetail(auditionId: auditionId) | ||||
|   | ||||
							
								
								
									
										130
									
								
								SodaLive/Sources/Audition/Role/AuditionRoleDetailView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										130
									
								
								SodaLive/Sources/Audition/Role/AuditionRoleDetailView.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,130 @@ | ||||
| // | ||||
| //  AuditionRoleDetailView.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 1/6/25. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
| import Kingfisher | ||||
|  | ||||
| struct AuditionRoleDetailView: View { | ||||
|      | ||||
|     let roleId: Int | ||||
|      | ||||
|     @StateObject var viewModel = AuditionRoleDetailViewModel() | ||||
|      | ||||
|     var body: some View { | ||||
|         BaseView(isLoading: $viewModel.isLoading) { | ||||
|             ZStack(alignment: .bottomTrailing) { | ||||
|                 VStack(spacing: 0) { | ||||
|                     DetailNavigationBar(title: viewModel.name) | ||||
|                      | ||||
|                     ScrollView(.vertical, showsIndicators: false) { | ||||
|                         LazyVStack(spacing: 15) { | ||||
|                             if let roleDetail = viewModel.auditionRoleDetail { | ||||
|                                 KFImage(URL(string: roleDetail.imageUrl)) | ||||
|                                     .cancelOnDisappear(true) | ||||
|                                     .downsampling(size: CGSize(width: 1000, height: 350)) | ||||
|                                     .resizable() | ||||
|                                     .aspectRatio(1000/350, contentMode: .fit) | ||||
|                                     .frame(maxWidth: .infinity) | ||||
|                                     .cornerRadius(6.7) | ||||
|                                     .padding(.top, 3) | ||||
|                                  | ||||
|                                 HStack(spacing: 14) { | ||||
|                                     if let url = URL(string: roleDetail.originalWorkUrl), UIApplication.shared.canOpenURL(url) { | ||||
|                                         Text("원작 보러가기") | ||||
|                                             .font(.custom(Font.bold.rawValue, size: 16)) | ||||
|                                             .foregroundColor(Color.button) | ||||
|                                             .padding(.vertical, 12) | ||||
|                                             .frame(maxWidth: .infinity) | ||||
|                                             .overlay( | ||||
|                                                 RoundedRectangle(cornerRadius: 10) | ||||
|                                                     .stroke(Color.button, lineWidth: 1) | ||||
|                                             ) | ||||
|                                             .contentShape(Rectangle()) | ||||
|                                             .onTapGesture { UIApplication.shared.open(url) } | ||||
|                                     } | ||||
|                                      | ||||
|                                     if let url = URL(string: roleDetail.auditionScriptUrl), UIApplication.shared.canOpenURL(url) { | ||||
|                                         Text("오디션 대본 확인") | ||||
|                                             .font(.custom(Font.bold.rawValue, size: 16)) | ||||
|                                             .foregroundColor(Color.button) | ||||
|                                             .padding(.vertical, 12) | ||||
|                                             .frame(maxWidth: .infinity) | ||||
|                                             .overlay( | ||||
|                                                 RoundedRectangle(cornerRadius: 10) | ||||
|                                                     .stroke(Color.button, lineWidth: 1) | ||||
|                                             ) | ||||
|                                             .contentShape(Rectangle()) | ||||
|                                             .onTapGesture { UIApplication.shared.open(url) } | ||||
|                                     } | ||||
|                                 } | ||||
|                                  | ||||
|                                 VStack(alignment: .leading, spacing: 13.3) { | ||||
|                                     Text("오디션 캐릭터 정보") | ||||
|                                         .font(.custom(Font.bold.rawValue, size: 14.7)) | ||||
|                                         .foregroundColor(Color.grayee) | ||||
|                                      | ||||
|                                     ExpandableTextView(text: roleDetail.information) | ||||
|                                 } | ||||
|                             } | ||||
|                              | ||||
|                             if viewModel.applicantList.isEmpty { | ||||
|                                 Text("지원자가 없습니다.") | ||||
|                                     .font(.custom(Font.medium.rawValue, size: 13)) | ||||
|                                     .foregroundColor(Color.grayee) | ||||
|                                     .padding(.top, 15) | ||||
|                             } else { | ||||
|                                 VStack(spacing: 5.3) { | ||||
|                                     ForEach(0..<viewModel.applicantList.count, id: \.self) { | ||||
|                                         let applicant = viewModel.applicantList[$0] | ||||
|                                          | ||||
|                                         AuditionApplicantItemView(item: applicant) | ||||
|                                             .padding(.bottom, $0 == viewModel.applicantList.count - 1 ? 33 : 0) | ||||
|                                     } | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                         .padding(.horizontal, 13.3) | ||||
|                     } | ||||
|                 } | ||||
|                 .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() | ||||
|                     } | ||||
|                 } | ||||
|                 .onAppear { | ||||
|                     viewModel.onFailure = { AppState.shared.back() } | ||||
|                     viewModel.auditionRoleId = roleId | ||||
|                 } | ||||
|                  | ||||
|                 if let roleDetail = viewModel.auditionRoleDetail { | ||||
|                     Text(roleDetail.isAlreadyApplicant ? "오디션 재지원" : "오디션 지원") | ||||
|                         .font(.custom(Font.bold.rawValue, size: 15.3)) | ||||
|                         .foregroundColor(Color.white) | ||||
|                         .padding(14) | ||||
|                         .background(Color.button) | ||||
|                         .cornerRadius(44) | ||||
|                         .padding(.trailing, 19) | ||||
|                         .padding(.bottom, 19) | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #Preview { | ||||
|     AuditionRoleDetailView(roleId: 1) | ||||
| } | ||||
							
								
								
									
										172
									
								
								SodaLive/Sources/Audition/Role/AuditionRoleDetailViewModel.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										172
									
								
								SodaLive/Sources/Audition/Role/AuditionRoleDetailViewModel.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,172 @@ | ||||
| // | ||||
| //  AuditionRoleDetailViewModel.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 1/6/25. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
| import Combine | ||||
|  | ||||
| final class AuditionRoleDetailViewModel: ObservableObject { | ||||
|      | ||||
|     private let repository = AuditionRepository() | ||||
|     private var subscription = Set<AnyCancellable>() | ||||
|      | ||||
|     @Published var errorMessage = "" | ||||
|     @Published var isShowPopup = false | ||||
|     @Published var isLoading = false | ||||
|      | ||||
|     @Published var totalCount = 0 | ||||
|     @Published var applicantList = [GetAuditionRoleApplicantItem]() | ||||
|      | ||||
|     @Published var name = "보이스온" | ||||
|     @Published var auditionRoleDetail: GetAuditionRoleDetailResponse? = nil | ||||
|      | ||||
|     @Published var sortType = AuditionApplicantSortType.NEWEST { | ||||
|         didSet { | ||||
|             page = 1 | ||||
|             isLast = false | ||||
|             getAuditionRoleDetail() | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     var page = 1 | ||||
|     var isLast = false | ||||
|     private var pageSize = 10 | ||||
|      | ||||
|     var auditionRoleId = -1 { | ||||
|         didSet { | ||||
|             if auditionRoleId > 0 { | ||||
|                 getAuditionRoleDetail() | ||||
|             } else { | ||||
|                 onFailure() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     var onFailure: () -> Void = {} | ||||
|      | ||||
|     func getAuditionRoleDetail() { | ||||
|         isLoading = true | ||||
|          | ||||
|         let auditionRoleDetail = repository.getAuditionRoleDetail(auditionRoleId: auditionRoleId) | ||||
|         let auditionApplicantList = repository.getAuditionApplicantList(auditionRoleId: auditionRoleId, sortType: sortType, page: page, size: pageSize) | ||||
|          | ||||
|         Publishers | ||||
|             .CombineLatest(auditionRoleDetail, auditionApplicantList) | ||||
|             .sink { result in | ||||
|                 switch result { | ||||
|                 case .finished: | ||||
|                     DEBUG_LOG("finish") | ||||
|                 case .failure(let error): | ||||
|                     ERROR_LOG(error.localizedDescription) | ||||
|                 } | ||||
|             } receiveValue: { [unowned self] (roleDetailResponse, applicantListResponse) in | ||||
|                 let roleDetail = roleDetailResponse.data | ||||
|                 let applicantList = applicantListResponse.data | ||||
|                  | ||||
|                 let jsonDecoder = JSONDecoder() | ||||
|                  | ||||
|                 do { | ||||
|                     let roleDetailDecoded = try jsonDecoder.decode(ApiResponse<GetAuditionRoleDetailResponse>.self, from: roleDetail) | ||||
|                     if let data = roleDetailDecoded.data, roleDetailDecoded.success { | ||||
|                         self.name = data.name | ||||
|                         self.auditionRoleDetail = data | ||||
|                     } else { | ||||
|                         if let message = roleDetailDecoded.message { | ||||
|                             self.errorMessage = message | ||||
|                         } else { | ||||
|                             self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." | ||||
|                         } | ||||
|                          | ||||
|                         self.isShowPopup = true | ||||
|                     } | ||||
|                 } catch { | ||||
|                     self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." | ||||
|                     self.isShowPopup = true | ||||
|                 } | ||||
|                  | ||||
|                 do { | ||||
|                     let applicantListDecoded = try jsonDecoder.decode(ApiResponse<GetAuditionApplicantListResponse>.self, from: applicantList) | ||||
|                     if let data = applicantListDecoded.data, applicantListDecoded.success { | ||||
|                         self.totalCount = data.totalCount | ||||
|                         self.applicantList.append(contentsOf: data.items) | ||||
|                          | ||||
|                         if data.items.isEmpty { | ||||
|                             isLast = true | ||||
|                         } else { | ||||
|                             page += 1 | ||||
|                         } | ||||
|                     } else { | ||||
|                         if let message = applicantListDecoded.message { | ||||
|                             self.errorMessage = message | ||||
|                         } else { | ||||
|                             self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." | ||||
|                         } | ||||
|                          | ||||
|                         self.isShowPopup = true | ||||
|                         DispatchQueue.main.asyncAfter(deadline: .now() + 2) { | ||||
|                             self.onFailure() | ||||
|                         } | ||||
|                     } | ||||
|                 } catch { | ||||
|                     self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." | ||||
|                     self.isShowPopup = true | ||||
|                     DispatchQueue.main.asyncAfter(deadline: .now() + 2) { | ||||
|                         self.onFailure() | ||||
|                     } | ||||
|                 } | ||||
|                  | ||||
|                 self.isLoading = false | ||||
|             } | ||||
|             .store(in: &subscription) | ||||
|     } | ||||
|      | ||||
|     func getAuditionApplicantList() { | ||||
|         if !isLoading && !isLast { | ||||
|             isLoading = true | ||||
|              | ||||
|             repository.getAuditionApplicantList(auditionRoleId: auditionRoleId, sortType: sortType, 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 | ||||
|                     let responseData = response.data | ||||
|                     self.isLoading = false | ||||
|                      | ||||
|                     do { | ||||
|                         let jsonDecoder = JSONDecoder() | ||||
|                         let decoded = try jsonDecoder.decode(ApiResponse<GetAuditionApplicantListResponse>.self, from: responseData) | ||||
|                          | ||||
|                         if let data = decoded.data, decoded.success { | ||||
|                             self.totalCount = data.totalCount | ||||
|                             self.applicantList.append(contentsOf: data.items) | ||||
|                              | ||||
|                             if data.items.isEmpty { | ||||
|                                 isLast = true | ||||
|                             } else { | ||||
|                                 page += 1 | ||||
|                             } | ||||
|                         } else { | ||||
|                             if let message = decoded.message { | ||||
|                                 self.errorMessage = message | ||||
|                             } else { | ||||
|                                 self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." | ||||
|                             } | ||||
|                              | ||||
|                             self.isShowPopup = true | ||||
|                         } | ||||
|                     } catch { | ||||
|                         self.errorMessage = "다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." | ||||
|                         self.isShowPopup = true | ||||
|                     } | ||||
|                 } | ||||
|                 .store(in: &subscription) | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -0,0 +1,16 @@ | ||||
| // | ||||
| //  GetAuditionRoleDetailResponse.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 1/6/25. | ||||
| // | ||||
|  | ||||
| struct GetAuditionRoleDetailResponse: Decodable { | ||||
|     let auditionRoleId: Int | ||||
|     let name: String | ||||
|     let imageUrl: String | ||||
|     let information: String | ||||
|     let originalWorkUrl: String | ||||
|     let auditionScriptUrl: String | ||||
|     let isAlreadyApplicant: Bool | ||||
| } | ||||
| @@ -206,6 +206,9 @@ struct ContentView: View { | ||||
|             case .auditionDetail(let auditionId): | ||||
|                 AuditionDetailView(auditionId: auditionId) | ||||
|                  | ||||
|             case .auditionRoleDetail(let roleId): | ||||
|                 AuditionRoleDetailView(roleId: roleId) | ||||
|                  | ||||
|             default: | ||||
|                 EmptyView() | ||||
|                     .frame(width: 0, height: 0, alignment: .topLeading) | ||||
|   | ||||
| @@ -14,7 +14,7 @@ struct ExpandableTextView: View { | ||||
|     let text: String | ||||
|      | ||||
|     var body: some View { | ||||
|         let customFont = UIFont(name: Font.medium.rawValue, size: 12) ?? UIFont.systemFont(ofSize: 12) | ||||
|         let customFont = UIFont(name: Font.medium.rawValue, size: 12.9) ?? UIFont.systemFont(ofSize: 12.9) | ||||
|         let lineHeight = customFont.lineHeight | ||||
|          | ||||
|         VStack(alignment: .leading) { | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Yu Sung
					Yu Sung