오디션 지원 기능 추가
This commit is contained in:
		
							
								
								
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/ic_mic_color_button.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/ic_mic_color_button.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| { | ||||
|   "images" : [ | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "1x" | ||||
|     }, | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "2x" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "ic_mic_color_button.png", | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "3x" | ||||
|     } | ||||
|   ], | ||||
|   "info" : { | ||||
|     "author" : "xcode", | ||||
|     "version" : 1 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/ic_mic_color_button.imageset/ic_mic_color_button.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/ic_mic_color_button.imageset/ic_mic_color_button.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.1 KiB | 
							
								
								
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/ic_note_square.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/ic_note_square.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| { | ||||
|   "images" : [ | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "1x" | ||||
|     }, | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "2x" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "ic_note_square.png", | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "3x" | ||||
|     } | ||||
|   ], | ||||
|   "info" : { | ||||
|     "author" : "xcode", | ||||
|     "version" : 1 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/ic_note_square.imageset/ic_note_square.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/ic_note_square.imageset/ic_note_square.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 1.1 KiB | 
							
								
								
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/ic_upload.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										21
									
								
								SodaLive/Resources/Assets.xcassets/ic_upload.imageset/Contents.json
									
									
									
									
										vendored
									
									
										Normal file
									
								
							| @@ -0,0 +1,21 @@ | ||||
| { | ||||
|   "images" : [ | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "1x" | ||||
|     }, | ||||
|     { | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "2x" | ||||
|     }, | ||||
|     { | ||||
|       "filename" : "ic_upload.png", | ||||
|       "idiom" : "universal", | ||||
|       "scale" : "3x" | ||||
|     } | ||||
|   ], | ||||
|   "info" : { | ||||
|     "author" : "xcode", | ||||
|     "version" : 1 | ||||
|   } | ||||
| } | ||||
							
								
								
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/ic_upload.imageset/ic_upload.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										
											BIN
										
									
								
								SodaLive/Resources/Assets.xcassets/ic_upload.imageset/ic_upload.png
									
									
									
									
										vendored
									
									
										Normal file
									
								
							
										
											Binary file not shown.
										
									
								
							| After Width: | Height: | Size: 861 B | 
| @@ -0,0 +1,11 @@ | ||||
| // | ||||
| //  ApplyAuditionRoleRequest.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 1/7/25. | ||||
| // | ||||
|  | ||||
| struct ApplyAuditionRoleRequest: Encodable { | ||||
|     let roleId: Int | ||||
|     let phoneNumber: String | ||||
| } | ||||
							
								
								
									
										108
									
								
								SodaLive/Sources/Audition/Applicant/ApplyMethodView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										108
									
								
								SodaLive/Sources/Audition/Applicant/ApplyMethodView.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,108 @@ | ||||
| // | ||||
| //  ApplyMethodView.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 1/7/25. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| struct ApplyMethodView: View { | ||||
|      | ||||
|     @Binding var isShowing: Bool | ||||
|      | ||||
|     let onClickSelectAudioFile: () -> Void | ||||
|     let onClickRecording: () -> Void | ||||
|      | ||||
|     var body: some View { | ||||
|         ZStack { | ||||
|             Color.black.opacity(0.7).ignoresSafeArea() | ||||
|                 .onTapGesture { | ||||
|                     isShowing = false | ||||
|                 } | ||||
|              | ||||
|             VStack(spacing: 0) { | ||||
|                 HStack(spacing: 0) { | ||||
|                     Spacer() | ||||
|                     Image("ic_noti_stop") | ||||
|                         .onTapGesture { | ||||
|                             isShowing = false | ||||
|                         } | ||||
|                 } | ||||
|                  | ||||
|                 Text("오디션 지원방식") | ||||
|                     .font(.custom(Font.bold.rawValue, size: 18.3)) | ||||
|                     .foregroundColor(Color.graybb) | ||||
|                     .padding(.top, 33.3) | ||||
|                  | ||||
|                 HStack(spacing: 13.3) { | ||||
|                     HStack(spacing: 3) { | ||||
|                         Image("ic_upload") | ||||
|                          | ||||
|                         Text("파일 업로드") | ||||
|                             .font(.custom(Font.medium.rawValue, size: 14.7)) | ||||
|                             .foregroundColor(Color.button) | ||||
|                     } | ||||
|                     .padding(.vertical, 8) | ||||
|                     .frame(maxWidth: .infinity) | ||||
|                     .background(Color.bg) | ||||
|                     .cornerRadius(5.3) | ||||
|                     .overlay( | ||||
|                         RoundedRectangle(cornerRadius: 8) | ||||
|                             .stroke() | ||||
|                             .foregroundColor(Color.button) | ||||
|                     ) | ||||
|                     .contentShape(Rectangle()) | ||||
|                     .onTapGesture { | ||||
|                         onClickSelectAudioFile() | ||||
|                     } | ||||
|                      | ||||
|                     HStack(spacing: 3) { | ||||
|                         Image("ic_mic_color_button") | ||||
|                          | ||||
|                         Text("바로 녹음") | ||||
|                             .font(.custom(Font.medium.rawValue, size: 14.7)) | ||||
|                             .foregroundColor(Color.button) | ||||
|                     } | ||||
|                     .padding(.vertical, 8) | ||||
|                     .frame(maxWidth: .infinity) | ||||
|                     .background(Color.bg) | ||||
|                     .cornerRadius(5.3) | ||||
|                     .overlay( | ||||
|                         RoundedRectangle(cornerRadius: 8) | ||||
|                             .stroke() | ||||
|                             .foregroundColor(Color.button) | ||||
|                     ) | ||||
|                     .contentShape(Rectangle()) | ||||
|                     .onTapGesture { | ||||
|                         onClickRecording() | ||||
|                     } | ||||
|                 } | ||||
|                 .padding(.top, 21.3) | ||||
|                  | ||||
|                 HStack(spacing: 0) { | ||||
|                     Text("※ 파일은 mp3, aac만 업로드 가능") | ||||
|                         .font(.custom(Font.medium.rawValue, size: 12)) | ||||
|                         .foregroundColor(Color.gray77) | ||||
|                         .padding(.top, 13.3) | ||||
|                      | ||||
|                     Spacer() | ||||
|                 } | ||||
|             } | ||||
|             .padding(.horizontal, 16.7) | ||||
|             .padding(.vertical, 13.3) | ||||
|             .frame(maxWidth: .infinity) | ||||
|             .background(Color.gray22) | ||||
|             .cornerRadius(10.3) | ||||
|             .padding(.horizontal, 13.3) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #Preview { | ||||
|     ApplyMethodView( | ||||
|         isShowing: .constant(true), | ||||
|         onClickSelectAudioFile: {}, | ||||
|         onClickRecording: {} | ||||
|     ) | ||||
| } | ||||
| @@ -0,0 +1,206 @@ | ||||
| // | ||||
| //  AuditionApplicantRecordingView.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 1/7/25. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| struct AuditionApplicantRecordingView: View { | ||||
|     @StateObject var soundManager = CreatorCommunitySoundManager() | ||||
|      | ||||
|     @Binding var isShowing: Bool | ||||
|     @Binding var isShowPopup: Bool | ||||
|     @Binding var errorMessage: String | ||||
|      | ||||
|     let onClickCompleteRecording: (String, Data) -> Void | ||||
|      | ||||
|     @State private var tempFileName = "" | ||||
|      | ||||
|     var body: some View { | ||||
|         ZStack { | ||||
|             Color.black.opacity(0.7) | ||||
|                 .ignoresSafeArea() | ||||
|              | ||||
|             GeometryReader { proxy in | ||||
|                 VStack { | ||||
|                     Spacer() | ||||
|                  | ||||
|                     VStack { | ||||
|                         VStack(spacing: 0) { | ||||
|                             HStack(spacing: 0) { | ||||
|                                 Text("오디션 녹음") | ||||
|                                     .font(.custom(Font.bold.rawValue, size: 18.3)) | ||||
|                                     .foregroundColor(.white) | ||||
|                                  | ||||
|                                 Spacer() | ||||
|                                  | ||||
|                                 Image("ic_close_white") | ||||
|                                     .resizable() | ||||
|                                     .frame(width: 20, height: 20) | ||||
|                                     .onTapGesture { isShowing = false } | ||||
|                             } | ||||
|                             .padding(.horizontal, 26.7) | ||||
|                             .padding(.top, 26.7) | ||||
|                         } | ||||
|                          | ||||
|                         Text(soundManager.timeString) | ||||
|                             .font(.custom(Font.bold.rawValue, size: 33.3)) | ||||
|                             .foregroundColor(.white) | ||||
|                             .padding(.top, 80) | ||||
|                          | ||||
|                         switch soundManager.recordMode { | ||||
|                         case .RECORD: | ||||
|                             if !soundManager.isLoading { | ||||
|                                 Image(soundManager.isRecording ? "ic_record_stop" : "ic_record") | ||||
|                                     .resizable() | ||||
|                                     .frame(width: 70, height: 70) | ||||
|                                     .padding(.vertical, 52.3) | ||||
|                                     .onTapGesture { | ||||
|                                         if !soundManager.isLoading { | ||||
|                                             if !soundManager.isRecording { | ||||
|                                                 tempFileName = "voiceon_now_voice_\(Int(Date().timeIntervalSince1970 * 1000)).m4a" | ||||
|                                                 soundManager.startRecording(tempFileName) | ||||
|                                             } else { | ||||
|                                                 soundManager.stopRecording() | ||||
|                                                 soundManager.recordMode = .PLAY | ||||
|                                             } | ||||
|                                         } | ||||
|                                     } | ||||
|                             } | ||||
|                              | ||||
|                         case .PLAY: | ||||
|                             if !soundManager.isLoading { | ||||
|                                 VStack(spacing: 0) { | ||||
|                                     HStack(spacing: 0) { | ||||
|                                         Spacer() | ||||
|                                          | ||||
|                                         Text("삭제") | ||||
|                                             .font(.custom(Font.medium.rawValue, size: 15.3)) | ||||
|                                             .foregroundColor(Color.graybb.opacity(0)) | ||||
|                                          | ||||
|                                         Spacer() | ||||
|                                          | ||||
|                                         Image( | ||||
|                                             !soundManager.isPlaying ? | ||||
|                                             "ic_record_play" : | ||||
|                                                 "ic_record_pause" | ||||
|                                         ) | ||||
|                                         .onTapGesture { | ||||
|                                             if !soundManager.isLoading { | ||||
|                                                 if !soundManager.isPlaying { | ||||
|                                                     soundManager.playAudio() | ||||
|                                                 } else { | ||||
|                                                     soundManager.stopAudio() | ||||
|                                                 } | ||||
|                                             } | ||||
|                                         } | ||||
|                                          | ||||
|                                         Spacer() | ||||
|                                          | ||||
|                                         Text("삭제") | ||||
|                                             .font(.custom(Font.medium.rawValue, size: 15.3)) | ||||
|                                             .foregroundColor(Color.graybb) | ||||
|                                             .onTapGesture { | ||||
|                                                 soundManager.stopAudio() | ||||
|                                                 soundManager.deleteAudioFile() | ||||
|                                                 soundManager.recordMode = .RECORD | ||||
|                                             } | ||||
|                                          | ||||
|                                         Spacer() | ||||
|                                     } | ||||
|                                     .padding(.vertical, 52.3) | ||||
|                                      | ||||
|                                     HStack(spacing: 13.3) { | ||||
|                                         Text("다시 녹음") | ||||
|                                             .font(.custom(Font.bold.rawValue, size: 18.3)) | ||||
|                                             .foregroundColor(Color.button) | ||||
|                                             .frame(width: (proxy.size.width - 40) / 3, height: 50) | ||||
|                                             .background(Color.button.opacity(0.2)) | ||||
|                                             .cornerRadius(10) | ||||
|                                             .overlay( | ||||
|                                                 RoundedRectangle(cornerRadius: 10) | ||||
|                                                     .stroke(Color.button, lineWidth: 1.3) | ||||
|                                             ) | ||||
|                                             .onTapGesture { | ||||
|                                                 soundManager.stopAudio() | ||||
|                                                 soundManager.deleteAudioFile() | ||||
|                                                 soundManager.recordMode = .RECORD | ||||
|                                             } | ||||
|                                          | ||||
|                                         Text("녹음완료") | ||||
|                                             .font(.custom(Font.bold.rawValue, size: 18.3)) | ||||
|                                             .foregroundColor(.white) | ||||
|                                             .frame(width: (proxy.size.width - 40) * 2 / 3, height: 50) | ||||
|                                             .background(Color.button) | ||||
|                                             .cornerRadius(10) | ||||
|                                             .onTapGesture { | ||||
|                                                 do { | ||||
|                                                     let soundData = try Data(contentsOf: soundManager.getAudioFileURL()) | ||||
|                                                     onClickCompleteRecording(tempFileName, soundData) | ||||
|                                                 } catch { | ||||
|                                                     errorMessage = "녹음파일을 생성하지 못했습니다.\n다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." | ||||
|                                                     isShowPopup = true | ||||
|                                                 } | ||||
|                                             } | ||||
|                                     } | ||||
|                                     .padding(.bottom, 40) | ||||
|                                     .padding(.horizontal, 13.3) | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                          | ||||
|                         if proxy.safeAreaInsets.bottom > 0 { | ||||
|                             Rectangle() | ||||
|                                 .foregroundColor(Color.gray22) | ||||
|                                 .frame(width: proxy.size.width, height: 15.3) | ||||
|                         } | ||||
|                          | ||||
|                         if soundManager.isLoading { | ||||
|                             LoadingView() | ||||
|                         } | ||||
|                     } | ||||
|                     .background(Color(hex: "222222")) | ||||
|                     .cornerRadius(16.7, corners: [.topLeft, .topRight]) | ||||
|                 } | ||||
|                 .edgesIgnoringSafeArea(.bottom) | ||||
|                 .onAppear { | ||||
|                     soundManager.prepareRecording() | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         .popup(isPresented: $soundManager.isShowPopup, type: .toast, position: .top, autohideIn: 2) { | ||||
|             GeometryReader { geo in | ||||
|                 HStack { | ||||
|                     Spacer() | ||||
|                     Text(soundManager.errorMessage) | ||||
|                         .padding(.vertical, 13.3) | ||||
|                         .padding(.horizontal, 6.7) | ||||
|                         .frame(width: screenSize().width - 66.7, alignment: .center) | ||||
|                         .font(.custom(Font.medium.rawValue, size: 12)) | ||||
|                         .background(Color.button) | ||||
|                         .foregroundColor(Color.white) | ||||
|                         .multilineTextAlignment(.center) | ||||
|                         .cornerRadius(20) | ||||
|                         .padding(.top, 66.7) | ||||
|                     Spacer() | ||||
|                 } | ||||
|                 .onDisappear { | ||||
|                     if soundManager.onClose { | ||||
|                         isShowing = false | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #Preview { | ||||
|     AuditionApplicantRecordingView( | ||||
|         isShowing: .constant(false), | ||||
|         isShowPopup: .constant(false), | ||||
|         errorMessage: .constant(""), | ||||
|         onClickCompleteRecording: { _, _ in } | ||||
|     ) | ||||
| } | ||||
							
								
								
									
										165
									
								
								SodaLive/Sources/Audition/Applicant/AuditionApplyView.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										165
									
								
								SodaLive/Sources/Audition/Applicant/AuditionApplyView.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,165 @@ | ||||
| // | ||||
| //  AuditionApplyView.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 1/7/25. | ||||
| // | ||||
|  | ||||
| import SwiftUI | ||||
|  | ||||
| struct AuditionApplyView: View { | ||||
|      | ||||
|     @Binding var isShowing: Bool | ||||
|     @Binding var phoneNumber: String | ||||
|     let filename: String | ||||
|     let onClickApply: () -> Void | ||||
|      | ||||
|     @State private var isShowPopup = false | ||||
|     @State private var errorMessage = "" | ||||
|      | ||||
|     @State private var isShow: Bool = false | ||||
|     @State private var isAgree = false | ||||
|      | ||||
|     var body: some View { | ||||
|         ZStack { | ||||
|             Color.black.opacity(0.7).ignoresSafeArea() | ||||
|                 .onTapGesture { | ||||
|                     isShowing = false | ||||
|                 } | ||||
|              | ||||
|             VStack(spacing: 0) { | ||||
|                 Spacer() | ||||
|                  | ||||
|                 if isShow { | ||||
|                     VStack(alignment: .leading, spacing: 0) { | ||||
|                         HStack(spacing: 0) { | ||||
|                             Text("오디션 지원") | ||||
|                                 .font(.custom(Font.medium.rawValue, size: 18.3)) | ||||
|                                 .foregroundColor(.white) | ||||
|                              | ||||
|                             Spacer() | ||||
|                              | ||||
|                             Image("ic_noti_stop") | ||||
|                                 .onTapGesture { | ||||
|                                     isShowing = false | ||||
|                                 } | ||||
|                         } | ||||
|                          | ||||
|                         Text("녹음파일") | ||||
|                             .font(.custom(Font.bold.rawValue, size: 16.7)) | ||||
|                             .foregroundColor(.grayee) | ||||
|                             .padding(.top, 20) | ||||
|                          | ||||
|                         HStack(spacing: 4) { | ||||
|                             Image("ic_note_square") | ||||
|                              | ||||
|                             Text(filename) | ||||
|                                 .font(.custom(Font.medium.rawValue, size: 13.3)) | ||||
|                                 .foregroundColor(.grayd2) | ||||
|                              | ||||
|                             Spacer() | ||||
|                         } | ||||
|                         .padding(.horizontal, 13.3) | ||||
|                         .padding(.vertical, 8) | ||||
|                         .frame(maxWidth: .infinity) | ||||
|                         .background(Color.bg) | ||||
|                         .cornerRadius(5.3) | ||||
|                         .padding(.top, 10) | ||||
|                          | ||||
|                         Text("연락처") | ||||
|                             .font(.custom(Font.bold.rawValue, size: 16.7)) | ||||
|                             .foregroundColor(.grayee) | ||||
|                             .padding(.top, 15) | ||||
|                          | ||||
|                         TextField("합격시 받을 연락처를 남겨주세요", text: $phoneNumber) | ||||
|                             .autocapitalization(.none) | ||||
|                             .disableAutocorrection(true) | ||||
|                             .keyboardType(.decimalPad) | ||||
|                             .font(.custom(Font.medium.rawValue, size: 13.3)) | ||||
|                             .foregroundColor(.grayee) | ||||
|                             .padding(.horizontal, 13.3) | ||||
|                             .padding(.vertical, 17) | ||||
|                             .frame(maxWidth: .infinity) | ||||
|                             .background(Color.bg) | ||||
|                             .cornerRadius(5.3) | ||||
|                             .padding(.top, 10) | ||||
|                          | ||||
|                         HStack(alignment: .top, spacing: 13.3) { | ||||
|                             Image(isAgree ? "btn_select_checked" : "btn_select_normal") | ||||
|                                 .resizable() | ||||
|                                 .frame(width: 20, height: 20) | ||||
|                              | ||||
|                             Text("보이스온 오디오 드라마 오디션 합격시 개인 연락을 위한 개인 정보(연락처) 수집 및 활용에 동의합니다.\n오디션 지원자는 개인정보 수집 및 활용 동의에 거부할 권리가 있으며 비동의시 오디션 지원은 취소 됩니다.") | ||||
|                                 .font(.custom(Font.medium.rawValue, size: 13.3)) | ||||
|                                 .foregroundColor(Color.grayee) | ||||
|                                 .lineSpacing(3) | ||||
|                         } | ||||
|                         .frame(maxWidth: .infinity, alignment: .leading) | ||||
|                         .padding(.top, 30) | ||||
|                         .onTapGesture { | ||||
|                             isAgree.toggle() | ||||
|                         } | ||||
|                          | ||||
|                         Text("오디션 지원하기") | ||||
|                             .font(.custom(Font.bold.rawValue, size: 13.3)) | ||||
|                             .foregroundColor(Color.grayee) | ||||
|                             .padding(.vertical, 13.3) | ||||
|                             .frame(maxWidth: .infinity) | ||||
|                             .background(Color.button) | ||||
|                             .cornerRadius(8) | ||||
|                             .padding(.top, 35) | ||||
|                             .onTapGesture { | ||||
|                                 if !isAgree { | ||||
|                                     errorMessage = "연락처 수집 및 활용에 동의하셔야 오디션 지원이 가능합니다." | ||||
|                                     isShowPopup = true | ||||
|                                     return | ||||
|                                 } | ||||
|                                  | ||||
|                                 onClickApply() | ||||
|                             } | ||||
|                     } | ||||
|                     .frame(maxWidth:.infinity) | ||||
|                     .padding(.horizontal, 16) | ||||
|                     .padding(.top, 16) | ||||
|                     .padding(.bottom, 32) | ||||
|                     .background(Color.gray22) | ||||
|                     .cornerRadius(10, corners: [.topLeft, .topRight]) | ||||
|                     .transition(.move(edge: .bottom)) | ||||
|                     .animation(.easeInOut(duration: 0.5), value: isShow) | ||||
|                 } | ||||
|             } | ||||
|             .ignoresSafeArea() | ||||
|         } | ||||
|         .popup(isPresented: $isShowPopup, type: .toast, position: .top, autohideIn: 2) { | ||||
|             GeometryReader { geo in | ||||
|                 HStack { | ||||
|                     Spacer() | ||||
|                     Text(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(.center) | ||||
|                         .cornerRadius(20) | ||||
|                         .padding(.top, 66.7) | ||||
|                     Spacer() | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|         .onAppear { | ||||
|             withAnimation { | ||||
|                 isShow = true | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #Preview { | ||||
|     AuditionApplyView( | ||||
|         isShowing: .constant(true), | ||||
|         phoneNumber: .constant(""), | ||||
|         filename: "now_voice_9292939.m4a", | ||||
|         onClickApply: {} | ||||
|     ) | ||||
| } | ||||
| @@ -13,6 +13,7 @@ enum AuditionApi { | ||||
|     case getAuditionDetail(auditionId: Int) | ||||
|     case getAuditionRoleDetail(auditionRoleId: Int) | ||||
|     case getAuditionApplicantList(auditionRoleId: Int, sortType: AuditionApplicantSortType, page: Int, size: Int) | ||||
|     case applyAudition(parameters: [MultipartFormData]) | ||||
| } | ||||
|  | ||||
| extension AuditionApi: TargetType { | ||||
| @@ -33,6 +34,9 @@ extension AuditionApi: TargetType { | ||||
|              | ||||
|         case .getAuditionApplicantList: | ||||
|             return "/audition/applicant" | ||||
|              | ||||
|         case .applyAudition: | ||||
|             return "/audition/applicant" | ||||
|         } | ||||
|     } | ||||
|      | ||||
| @@ -41,6 +45,9 @@ extension AuditionApi: TargetType { | ||||
|              | ||||
|         case .getAuditionList, .getAuditionDetail, . getAuditionRoleDetail, .getAuditionApplicantList: | ||||
|             return .get | ||||
|              | ||||
|         case .applyAudition: | ||||
|             return .post | ||||
|         } | ||||
|     } | ||||
|      | ||||
| @@ -67,6 +74,9 @@ extension AuditionApi: TargetType { | ||||
|             ] as [String : Any] | ||||
|              | ||||
|             return .requestParameters(parameters: parameters, encoding: URLEncoding.queryString) | ||||
|              | ||||
|         case .applyAudition(let parameters): | ||||
|             return .uploadMultipart(parameters) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|   | ||||
| @@ -35,4 +35,8 @@ final class AuditionRepository { | ||||
|             ) | ||||
|         ) | ||||
|     } | ||||
|      | ||||
|     func applyAudition(parameters: [MultipartFormData]) -> AnyPublisher<Response, MoyaError> { | ||||
|         return api.requestPublisher(.applyAudition(parameters: parameters)) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -13,6 +13,12 @@ struct AuditionRoleDetailView: View { | ||||
|     let roleId: Int | ||||
|      | ||||
|     @StateObject var viewModel = AuditionRoleDetailViewModel() | ||||
|     @StateObject var keyboardHandler = KeyboardHandler() | ||||
|      | ||||
|     @State private var isShowApplyMethodView = false | ||||
|     @State private var isShowSelectAudioView = false | ||||
|     @State private var isShowRecordingView = false | ||||
|     @State private var isShowApplyView = false | ||||
|      | ||||
|     var body: some View { | ||||
|         BaseView(isLoading: $viewModel.isLoading) { | ||||
| @@ -83,6 +89,14 @@ struct AuditionRoleDetailView: View { | ||||
|                                          | ||||
|                                         AuditionApplicantItemView(item: applicant) | ||||
|                                             .padding(.bottom, $0 == viewModel.applicantList.count - 1 ? 33 : 0) | ||||
|                                          | ||||
|                                         if $0 == viewModel.applicantList.count - 1 { | ||||
|                                             Color.clear | ||||
|                                                 .frame(height: 0) | ||||
|                                                 .onAppear { | ||||
|                                                     viewModel.getAuditionApplicantList() | ||||
|                                                 } | ||||
|                                         } | ||||
|                                     } | ||||
|                                 } | ||||
|                             } | ||||
| @@ -119,8 +133,96 @@ struct AuditionRoleDetailView: View { | ||||
|                         .cornerRadius(44) | ||||
|                         .padding(.trailing, 19) | ||||
|                         .padding(.bottom, 19) | ||||
|                         .onTapGesture { | ||||
|                             isShowApplyMethodView = true | ||||
|                         } | ||||
|                 } | ||||
|             } | ||||
|             .fileImporter( | ||||
|                 isPresented: $isShowSelectAudioView, | ||||
|                 allowedContentTypes: [.audio], | ||||
|                 allowsMultipleSelection: false | ||||
|             ) { result in | ||||
|                 handleFileImport(result: result) | ||||
|             } | ||||
|              | ||||
|             if isShowApplyMethodView { | ||||
|                 ApplyMethodView( | ||||
|                     isShowing: $isShowApplyMethodView, | ||||
|                     onClickSelectAudioFile: { | ||||
|                         isShowApplyMethodView = false | ||||
|                         isShowSelectAudioView = true | ||||
|                     }, | ||||
|                     onClickRecording: { | ||||
|                         isShowApplyMethodView = false | ||||
|                         isShowRecordingView = true | ||||
|                     } | ||||
|                 ) | ||||
|             } | ||||
|              | ||||
|             if isShowRecordingView { | ||||
|                 AuditionApplicantRecordingView( | ||||
|                     isShowing: $isShowRecordingView, | ||||
|                     isShowPopup: $viewModel.isShowPopup, | ||||
|                     errorMessage: $viewModel.errorMessage, | ||||
|                     onClickCompleteRecording: { fileName, soundData in | ||||
|                         viewModel.fileName = fileName | ||||
|                         viewModel.soundData = soundData | ||||
|                         isShowRecordingView = false | ||||
|                         isShowApplyView = true | ||||
|                     } | ||||
|                 ) | ||||
|             } | ||||
|              | ||||
|             if isShowApplyView { | ||||
|                 AuditionApplyView( | ||||
|                     isShowing: $isShowApplyView, | ||||
|                     phoneNumber: $viewModel.phoneNumber, | ||||
|                     filename: viewModel.fileName, | ||||
|                     onClickApply: { | ||||
|                         viewModel.applyAudition { | ||||
|                             isShowApplyView = false | ||||
|                             isShowRecordingView = false | ||||
|                         } | ||||
|                     } | ||||
|                 ) | ||||
|                 .offset(y: 0 - (keyboardHandler.keyboardHeight / 10)) | ||||
|                 .onDisappear { | ||||
|                     viewModel.soundData = nil | ||||
|                     viewModel.fileName = "" | ||||
|                     viewModel.deleteAllRecordingFilesWithNamePrefix("voiceon_now_voice") | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private func handleFileImport(result: Result<[URL], Error>) { | ||||
|         switch result { | ||||
|         case .success(let url): | ||||
|             let fileUrl = url[0] | ||||
|              | ||||
|             if fileUrl.startAccessingSecurityScopedResource() { | ||||
|                 defer { | ||||
|                     fileUrl.stopAccessingSecurityScopedResource() | ||||
|                 } | ||||
|                  | ||||
|                 if let data = try? Data(contentsOf: fileUrl) { | ||||
|                     viewModel.soundData = data | ||||
|                     viewModel.fileName = fileUrl.lastPathComponent | ||||
|                     isShowApplyView = true | ||||
|                 } else { | ||||
|                     viewModel.errorMessage = "콘텐츠 파일을 불러오지 못했습니다.\n다시 선택해 주세요" | ||||
|                     viewModel.isShowPopup = true | ||||
|                 } | ||||
|             } else { | ||||
|                 viewModel.errorMessage = "콘텐츠 파일을 불러오지 못했습니다.\n다시 선택해 주세요" | ||||
|                 viewModel.isShowPopup = true | ||||
|             } | ||||
|              | ||||
|         case .failure(let error): | ||||
|             DEBUG_LOG("error: \(error.localizedDescription)") | ||||
|             viewModel.errorMessage = "콘텐츠 파일을 불러오지 못했습니다.\n다시 선택해 주세요" | ||||
|             viewModel.isShowPopup = true | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -6,6 +6,7 @@ | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
| import Moya | ||||
| import Combine | ||||
|  | ||||
| final class AuditionRoleDetailViewModel: ObservableObject { | ||||
| @@ -31,6 +32,10 @@ final class AuditionRoleDetailViewModel: ObservableObject { | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     @Published var fileName = "" | ||||
|     @Published var soundData: Data? = nil | ||||
|     @Published var phoneNumber = "" | ||||
|      | ||||
|     var page = 1 | ||||
|     var isLast = false | ||||
|     private var pageSize = 10 | ||||
| @@ -169,4 +174,109 @@ final class AuditionRoleDetailViewModel: ObservableObject { | ||||
|                 .store(in: &subscription) | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     func applyAudition(onSuccess: @escaping () -> Void) { | ||||
|         if phoneNumber.count != 11 { | ||||
|             errorMessage = "잘못된 연락처 입니다.\n다시 입력해 주세요." | ||||
|             isShowPopup = true | ||||
|             return | ||||
|         } | ||||
|          | ||||
|         guard let soundData = soundData else { | ||||
|             errorMessage = "잘못된 녹음 파일 입니다.\n다시 선택해 주세요." | ||||
|             isShowPopup = true | ||||
|             return | ||||
|         } | ||||
|          | ||||
|         isLoading = true | ||||
|          | ||||
|         let request = ApplyAuditionRoleRequest(roleId: auditionRoleId, phoneNumber: phoneNumber) | ||||
|         var multipartData = [MultipartFormData]() | ||||
|          | ||||
|         let encoder = JSONEncoder() | ||||
|         encoder.outputFormatting = .withoutEscapingSlashes | ||||
|         let jsonData = try? encoder.encode(request) | ||||
|          | ||||
|         if let jsonData = jsonData { | ||||
|             multipartData.append(MultipartFormData(provider: .data(jsonData), name: "request")) | ||||
|             multipartData.append( | ||||
|                 MultipartFormData( | ||||
|                     provider: .data(soundData), | ||||
|                     name: "contentFile", | ||||
|                     fileName: fileName, | ||||
|                     mimeType: "audio/*" | ||||
|                 ) | ||||
|             ) | ||||
|              | ||||
|             repository.applyAudition(parameters: multipartData) | ||||
|                 .sink { result in | ||||
|                     switch result { | ||||
|                     case .finished: | ||||
|                         DEBUG_LOG("finish") | ||||
|                     case .failure(let error): | ||||
|                         ERROR_LOG(error.localizedDescription) | ||||
|                     } | ||||
|                 } receiveValue: { response in | ||||
|                     self.isLoading = false | ||||
|                     let responseData = response.data | ||||
|                      | ||||
|                     do { | ||||
|                         let jsonDecoder = JSONDecoder() | ||||
|                         let decoded = try jsonDecoder.decode(ApiResponseWithoutData.self, from: responseData) | ||||
|                          | ||||
|                         if decoded.success { | ||||
|                             self.errorMessage = "오디션 지원이 완료되었습니다." | ||||
|                             self.isShowPopup = true | ||||
|                             self.deleteAllRecordingFilesWithNamePrefix("voiceon_") | ||||
|                             self.applicantList = [] | ||||
|                             self.totalCount = 0 | ||||
|                              | ||||
|                             self.page = 1 | ||||
|                             self.isLast = false | ||||
|                             self.getAuditionRoleDetail() | ||||
|                              | ||||
|                             onSuccess() | ||||
|                         } else { | ||||
|                             if let message = decoded.message { | ||||
|                                 self.errorMessage = message | ||||
|                             } else { | ||||
|                                 self.errorMessage = "오디션 지원을 완료하지 못했습니다.\n다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." | ||||
|                             } | ||||
|                              | ||||
|                             self.isShowPopup = true | ||||
|                         } | ||||
|                     } catch { | ||||
|                         self.errorMessage = "오디션 지원을 완료하지 못했습니다.\n다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." | ||||
|                         self.isShowPopup = true | ||||
|                     } | ||||
|                 } | ||||
|                 .store(in: &subscription) | ||||
|         } else { | ||||
|             self.errorMessage = "오디션 지원을 완료하지 못했습니다.\n다시 시도해 주세요.\n계속 같은 문제가 발생할 경우 고객센터로 문의 주시기 바랍니다." | ||||
|             self.isShowPopup = true | ||||
|             self.isLoading = false | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     func deleteAllRecordingFilesWithNamePrefix(_ prefix: String) { | ||||
|         let fileManager = FileManager.default | ||||
|         let documentsURL = getDocumentsDirectory() | ||||
|  | ||||
|         do { | ||||
|             let fileURLs = try fileManager.contentsOfDirectory(at: documentsURL, includingPropertiesForKeys: nil, options: []) | ||||
|             for fileURL in fileURLs { | ||||
|                 if fileURL.lastPathComponent.hasPrefix(prefix) { | ||||
|                     try fileManager.removeItem(at: fileURL) | ||||
|                     DEBUG_LOG("녹음 파일 삭제 성공: \(fileURL)") | ||||
|                 } | ||||
|             } | ||||
|         } catch { | ||||
|             DEBUG_LOG("녹음 파일 삭제 실패: \(error.localizedDescription)") | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private func getDocumentsDirectory() -> URL { | ||||
|         let paths = FileManager.default.urls(for: .documentDirectory, in: .userDomainMask) | ||||
|         return paths[0] | ||||
|     } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Yu Sung
					Yu Sung