오디션 - 오디오 재생기능 추가
This commit is contained in:
		| @@ -13,6 +13,8 @@ struct AuditionApplicantItemView: View { | ||||
|     let item: GetAuditionRoleApplicantItem | ||||
|     let onClickVote: (Int) -> Void | ||||
|      | ||||
|     @StateObject var soundManager = AuditionSoundManager.shared | ||||
|      | ||||
|     var body: some View { | ||||
|         VStack(spacing: 5.3) { | ||||
|             HStack(spacing: 13.3) { | ||||
| @@ -26,8 +28,12 @@ struct AuditionApplicantItemView: View { | ||||
|                         .frame(width: 40, height: 40) | ||||
|                         .clipShape(Circle()) | ||||
|                      | ||||
|                     Image("ic_audition_play") | ||||
|                     Image(soundManager.applicantId == item.applicantId && soundManager.isPlaying ? "ic_audition_pause" : "ic_audition_play") | ||||
|                         .onTapGesture { | ||||
|                             soundManager.playOrPause( | ||||
|                                 applicantId: item.applicantId, | ||||
|                                 url: item.voiceUrl | ||||
|                             ) | ||||
|                         } | ||||
|                 } | ||||
|                  | ||||
| @@ -38,6 +44,17 @@ struct AuditionApplicantItemView: View { | ||||
|                             .foregroundColor(Color.white) | ||||
|                          | ||||
|                         Spacer() | ||||
|                          | ||||
|                         if soundManager.applicantId == item.applicantId { | ||||
|                             Text("\(secondsToMinutesSeconds(Int(soundManager.currentTime)))/\(secondsToMinutesSeconds(Int(soundManager.duration)))") | ||||
|                                 .font(.custom(Font.medium.rawValue, size: 12)) | ||||
|                                 .foregroundColor(Color.gray77) | ||||
|                         } | ||||
|                     } | ||||
|                      | ||||
|                     if soundManager.applicantId == item.applicantId { | ||||
|                         ProgressView(value: soundManager.currentTime, total: soundManager.duration) | ||||
|                             .progressViewStyle(LinearProgressViewStyle(tint: Color.button)) | ||||
|                     } | ||||
|                 } | ||||
|                  | ||||
| @@ -60,6 +77,13 @@ struct AuditionApplicantItemView: View { | ||||
|         } | ||||
|         .padding(.horizontal, 13.3) | ||||
|     } | ||||
|      | ||||
|     private func secondsToMinutesSeconds(_ seconds: Int) -> String { | ||||
|         let minute = String(format: "%02d", seconds / 60) | ||||
|         let second = String(format: "%02d", seconds % 60) | ||||
|          | ||||
|         return "\(minute):\(second)" | ||||
|     } | ||||
| } | ||||
|  | ||||
| #Preview { | ||||
|   | ||||
							
								
								
									
										126
									
								
								SodaLive/Sources/Audition/Detail/AuditionSoundManager.swift
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										126
									
								
								SodaLive/Sources/Audition/Detail/AuditionSoundManager.swift
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,126 @@ | ||||
| // | ||||
| //  AuditionSoundManager.swift | ||||
| //  SodaLive | ||||
| // | ||||
| //  Created by klaus on 1/7/25. | ||||
| // | ||||
|  | ||||
| import Foundation | ||||
| import AVKit | ||||
| import Combine | ||||
|  | ||||
| class AuditionSoundManager: NSObject, ObservableObject { | ||||
|     static let shared = AuditionSoundManager() | ||||
|      | ||||
|     @Published private (set) var isPlaying = false | ||||
|      | ||||
|     @Published var isLoading = false | ||||
|     @Published var errorMessage = "" | ||||
|     @Published var isShowPopup = false | ||||
|      | ||||
|     @Published var currentTime: Double = 0.0 | ||||
|     @Published private (set) var duration: Double = 0.0 | ||||
|      | ||||
|     private var player: AVAudioPlayer! | ||||
|     private var timer: Timer? | ||||
|     @Published private (set) var applicantId = -1 | ||||
|      | ||||
|     func resetPlayer() { | ||||
|         stop() | ||||
|     } | ||||
|      | ||||
|     func playOrPause(applicantId: Int, url: String) { | ||||
|         if applicantId <= 0 { | ||||
|             stop() | ||||
|         } | ||||
|          | ||||
|         if self.applicantId != applicantId { | ||||
|             stop() | ||||
|             setupPlayer(with: url) | ||||
|             self.applicantId = applicantId | ||||
|         } else { | ||||
|             if !isPlaying { | ||||
|                 player?.play() | ||||
|                 isPlaying = player?.isPlaying ?? false | ||||
|                 startTimer() | ||||
|             } else { | ||||
|                 player?.pause() | ||||
|                 isPlaying = player?.isPlaying ?? false | ||||
|                 stopTimer() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     private func stop() { | ||||
|         stopTimer() | ||||
|         isPlaying = false | ||||
|         isLoading = false | ||||
|         player?.stop() | ||||
|         player?.currentTime = 0 | ||||
|         currentTime = 0.0 | ||||
|         duration = 0.0 | ||||
|         applicantId = -1 | ||||
|     } | ||||
|      | ||||
|     private func setupPlayer(with url: String) { | ||||
|         guard let url = URL(string: url) else { | ||||
|             self.errorMessage = "오류가 발생했습니다. 다시 시도해 주세요." | ||||
|             self.isShowPopup = true | ||||
|             return | ||||
|         } | ||||
|          | ||||
|         isLoading = true | ||||
|         URLSession.shared.dataTask(with: url) { [unowned self] data, response, error in | ||||
|             guard let audioData = data else { | ||||
|                 self.isLoading = false | ||||
|                 return | ||||
|             } | ||||
|              | ||||
|             do { | ||||
|                 self.player = try AVAudioPlayer(data: audioData) | ||||
|                  | ||||
|                 DispatchQueue.main.async { | ||||
|                     self.player?.volume = 1 | ||||
|                     self.player?.delegate = self | ||||
|                     self.player?.prepareToPlay() | ||||
|                      | ||||
|                     self.player?.play() | ||||
|                     self.duration = self.player?.duration ?? 0.0 | ||||
|                     self.isPlaying = self.player.isPlaying | ||||
|                     self.isLoading = false | ||||
|                     self.startTimer() | ||||
|                 } | ||||
|             } catch { | ||||
|                 DispatchQueue.main.async { | ||||
|                     self.errorMessage = "오류가 발생했습니다. 다시 시도해 주세요." | ||||
|                     self.isShowPopup = true | ||||
|                     self.isLoading = false | ||||
|                 } | ||||
|             } | ||||
|         }.resume() | ||||
|     } | ||||
|      | ||||
|     private func startTimer() { | ||||
|         stopTimer() // 기존 타이머 제거 | ||||
|         timer = Timer.scheduledTimer(withTimeInterval: 0.5, repeats: true) { [weak self] _ in | ||||
|             self?.updateCurrentTime() | ||||
|         } | ||||
|     } | ||||
|      | ||||
|     // 타이머 정지 | ||||
|     private func stopTimer() { | ||||
|         timer?.invalidate() | ||||
|         timer = nil | ||||
|     } | ||||
|      | ||||
|     private func updateCurrentTime() { | ||||
|         guard let audioPlayer = player else { return } | ||||
|         currentTime = audioPlayer.currentTime | ||||
|     } | ||||
| } | ||||
|  | ||||
| extension AuditionSoundManager: AVAudioPlayerDelegate { | ||||
|     func audioPlayerDidFinishPlaying(_ player: AVAudioPlayer, successfully flag: Bool) { | ||||
|         stop() | ||||
|     } | ||||
| } | ||||
| @@ -14,6 +14,7 @@ struct AuditionRoleDetailView: View { | ||||
|      | ||||
|     @StateObject var viewModel = AuditionRoleDetailViewModel() | ||||
|     @StateObject var keyboardHandler = KeyboardHandler() | ||||
|     @StateObject var soundManager = AuditionSoundManager.shared | ||||
|      | ||||
|     @State private var isShowApplyMethodView = false | ||||
|     @State private var isShowSelectAudioView = false | ||||
| @@ -123,10 +124,28 @@ struct AuditionRoleDetailView: View { | ||||
|                         Spacer() | ||||
|                     } | ||||
|                 } | ||||
|                 .popup(isPresented: $soundManager.isShowPopup, type: .toast, position: .bottom, autohideIn: 2) { | ||||
|                     HStack { | ||||
|                         Spacer() | ||||
|                         Text(soundManager.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 | ||||
|                 } | ||||
|                 .onDisappear { | ||||
|                     soundManager.resetPlayer() | ||||
|                 } | ||||
|                  | ||||
|                 if let roleDetail = viewModel.auditionRoleDetail { | ||||
|                     Text(roleDetail.isAlreadyApplicant ? "오디션 재지원" : "오디션 지원") | ||||
| @@ -208,6 +227,10 @@ struct AuditionRoleDetailView: View { | ||||
|                     viewModel.isShowNotifyVote = false | ||||
|                 } | ||||
|             } | ||||
|              | ||||
|             if soundManager.isLoading { | ||||
|                 LoadingView() | ||||
|             } | ||||
|         } | ||||
|     } | ||||
|      | ||||
|   | ||||
		Reference in New Issue
	
	Block a user
	 Yu Sung
					Yu Sung