diff --git a/alsongDalsong/ASAudioKit/ASAudioAnalyzer.swift b/alsongDalsong/ASAudioKit/ASAudioAnalyzer.swift index 3d53ce4..299fc78 100644 --- a/alsongDalsong/ASAudioKit/ASAudioAnalyzer.swift +++ b/alsongDalsong/ASAudioKit/ASAudioAnalyzer.swift @@ -3,45 +3,49 @@ import Foundation public enum ASAudioAnalyzer { public static func analyze(data: Data, samplesCount: Int) async throws -> [CGFloat] { - let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".m4a") - try data.write(to: tempURL) - let file = try AVAudioFile(forReading: tempURL) - - guard - let format = AVAudioFormat( - commonFormat: .pcmFormatFloat32, - sampleRate: file.fileFormat.sampleRate, - channels: file.fileFormat.channelCount, - interleaved: false - ), - let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: AVAudioFrameCount(file.length)) - else { - return [] + do { + let tempURL = FileManager.default.temporaryDirectory.appendingPathComponent(UUID().uuidString + ".m4a") + try data.write(to: tempURL) + let file = try AVAudioFile(forReading: tempURL) + + guard + let format = AVAudioFormat( + commonFormat: .pcmFormatFloat32, + sampleRate: file.fileFormat.sampleRate, + channels: file.fileFormat.channelCount, + interleaved: false + ), + let buffer = AVAudioPCMBuffer(pcmFormat: format, frameCapacity: AVAudioFrameCount(file.length)) + else { + return [] + } + + try file.read(into: buffer) + guard let floatChannelData = buffer.floatChannelData else { + return [] + } + + let frameLength = Int(buffer.frameLength) + let samples = Array(UnsafeBufferPointer(start: floatChannelData[0], count: frameLength)) + var result = [CGFloat]() + let chunkedSamples = samples.chunked(into: samples.count / samplesCount) + + for chunk in chunkedSamples { + let squaredSum = chunk.reduce(0) { $0 + $1 * $1 } + let averagePower = squaredSum / Float(chunk.count) + let decibels = 10 * log10(max(averagePower, Float.ulpOfOne)) + + let newAmplitude = 1.8 * pow(10.0, decibels / 20.0) + let clampedAmplitude = min(max(CGFloat(newAmplitude), 0), 1) + result.append(clampedAmplitude) + } + + try? FileManager.default.removeItem(at: tempURL) + + return result + } catch { + throw ASAudioErrors(type: .analyze, reason: error.localizedDescription, file: #file, line: #line) } - - try file.read(into: buffer) - guard let floatChannelData = buffer.floatChannelData else { - return [] - } - - let frameLength = Int(buffer.frameLength) - let samples = Array(UnsafeBufferPointer(start: floatChannelData[0], count: frameLength)) - var result = [CGFloat]() - let chunkedSamples = samples.chunked(into: samples.count / samplesCount) - - for chunk in chunkedSamples { - let squaredSum = chunk.reduce(0) { $0 + $1 * $1 } - let averagePower = squaredSum / Float(chunk.count) - let decibels = 10 * log10(max(averagePower, Float.ulpOfOne)) - - let newAmplitude = 1.8 * pow(10.0, decibels / 20.0) - let clampedAmplitude = min(max(CGFloat(newAmplitude), 0), 1) - result.append(clampedAmplitude) - } - - try? FileManager.default.removeItem(at: tempURL) - - return result } } diff --git a/alsongDalsong/ASAudioKit/ASAudioErrors.swift b/alsongDalsong/ASAudioKit/ASAudioErrors.swift new file mode 100644 index 0000000..25cf16c --- /dev/null +++ b/alsongDalsong/ASAudioKit/ASAudioErrors.swift @@ -0,0 +1,19 @@ +import Foundation + +struct ASAudioErrors: LocalizedError { + let type: ErrorType + let reason: String + let file: String + let line: Int + + enum ErrorType { + case analyze + case startPlaying, getDuration + case configureAudioSession + case startRecording + } + + var errorDescription: String? { + return "[\(URL(fileURLWithPath: file).lastPathComponent):\(line)] \(type) 에러: \n\(reason)" + } +} diff --git a/alsongDalsong/ASAudioKit/ASAudioKit.xcodeproj/project.pbxproj b/alsongDalsong/ASAudioKit/ASAudioKit.xcodeproj/project.pbxproj index e0249bd..60b0cb2 100644 --- a/alsongDalsong/ASAudioKit/ASAudioKit.xcodeproj/project.pbxproj +++ b/alsongDalsong/ASAudioKit/ASAudioKit.xcodeproj/project.pbxproj @@ -12,6 +12,7 @@ 4E8907C22CE2489A00D5B547 /* ASAudioKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E89071E2CE23D1B00D5B547 /* ASAudioKit.framework */; platformFilter = ios; }; 4EB1ED372CE88E500012FFBA /* ASAudioKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 4E89071E2CE23D1B00D5B547 /* ASAudioKit.framework */; }; 4EB1ED382CE88E500012FFBA /* ASAudioKit.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 4E89071E2CE23D1B00D5B547 /* ASAudioKit.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; + 8593659D2D3F77FE0086C8C4 /* ASAudioErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 8593659C2D3F77F60086C8C4 /* ASAudioErrors.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -50,6 +51,7 @@ 4E89071E2CE23D1B00D5B547 /* ASAudioKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ASAudioKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 4E8907BE2CE2489A00D5B547 /* ASAudioKitTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ASAudioKitTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 4EB1EC802CE7AA160012FFBA /* ASAudioDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ASAudioDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 8593659C2D3F77F60086C8C4 /* ASAudioErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASAudioErrors.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ @@ -123,6 +125,7 @@ isa = PBXGroup; children = ( 1B7EB01A2CFDA83B00B2BE2A /* ASAudioAnalyzer.swift */, + 8593659C2D3F77F60086C8C4 /* ASAudioErrors.swift */, 4E8907AD2CE240D400D5B547 /* ASAudioKit */, 4E8907BF2CE2489A00D5B547 /* ASAudioKitTests */, 4EB1EC812CE7AA160012FFBA /* ASAudioDemo */, @@ -303,6 +306,7 @@ buildActionMask = 2147483647; files = ( 1B7EB01C2CFDA83B00B2BE2A /* ASAudioAnalyzer.swift in Sources */, + 8593659D2D3F77FE0086C8C4 /* ASAudioErrors.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/alsongDalsong/ASAudioKit/ASAudioKit/ASAudioPlayer/ASAudioPlayer.swift b/alsongDalsong/ASAudioKit/ASAudioKit/ASAudioPlayer/ASAudioPlayer.swift index 5495dfa..a7b562e 100644 --- a/alsongDalsong/ASAudioKit/ASAudioKit/ASAudioPlayer/ASAudioPlayer.swift +++ b/alsongDalsong/ASAudioKit/ASAudioKit/ASAudioPlayer/ASAudioPlayer.swift @@ -13,28 +13,29 @@ public actor ASAudioPlayer: NSObject { public var onPlaybackFinished: (@Sendable () async -> Void)? /// 녹음파일을 재생하고 옵션에 따라 재생시간을 설정합니다. - public func startPlaying(data: Data, option: PlayType = .full) { - configureAudioSession() + public func startPlaying(data: Data, option: PlayType = .full) throws { do { + try configureAudioSession() audioPlayer = try AVAudioPlayer(data: data) audioPlayer?.delegate = self audioPlayer?.prepareToPlay() audioPlayer?.isMeteringEnabled = true } catch { // TODO: 오디오 객체생성 실패 시 처리 + throw ASAudioErrors(type: .startPlaying, reason: error.localizedDescription, file: #file, line: #line) } switch option { - case .full: - audioPlayer?.play() - case let .partial(time): - audioPlayer?.currentTime = 0 - audioPlayer?.play() - DispatchQueue.main.asyncAfter(deadline: .now() + TimeInterval(time)) { - Task { - await self.stopPlaying() - } + case .full: + audioPlayer?.play() + case let .partial(time): + audioPlayer?.currentTime = 0 + audioPlayer?.play() + DispatchQueue.main.asyncAfter(deadline: .now() + TimeInterval(time)) { + Task { + await self.stopPlaying() } + } } } @@ -51,12 +52,13 @@ public actor ASAudioPlayer: NSObject { } /// 녹음파일의 총 녹음시간을 리턴합니다. - public func getDuration(data: Data) -> TimeInterval { + public func getDuration(data: Data) throws -> TimeInterval { if audioPlayer == nil { do { audioPlayer = try AVAudioPlayer(data: data) } catch { // TODO: 오디오 객체생성 실패 시 처리 + throw ASAudioErrors(type: .getDuration, reason: error.localizedDescription, file: #file, line: #line) } } return audioPlayer?.duration ?? 0 @@ -66,13 +68,14 @@ public actor ASAudioPlayer: NSObject { onPlaybackFinished = handler } - private func configureAudioSession() { + private func configureAudioSession() throws { do { let session = AVAudioSession.sharedInstance() try session.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker]) try session.setActive(true, options: .notifyOthersOnDeactivation) } catch { // TODO: 세션 설정 실패에 따른 처리 + throw ASAudioErrors(type: .configureAudioSession, reason: error.localizedDescription, file: #file, line: #line) } } } diff --git a/alsongDalsong/ASAudioKit/ASAudioKit/ASAudioRecorder/ASAudioRecorder.swift b/alsongDalsong/ASAudioKit/ASAudioKit/ASAudioRecorder/ASAudioRecorder.swift index fb5aa18..0d95fab 100644 --- a/alsongDalsong/ASAudioKit/ASAudioKit/ASAudioRecorder/ASAudioRecorder.swift +++ b/alsongDalsong/ASAudioKit/ASAudioKit/ASAudioRecorder/ASAudioRecorder.swift @@ -5,33 +5,34 @@ public actor ASAudioRecorder { public init() {} /// 녹음 후 저장될 파일의 위치를 지정하여 녹음합니다. - public func startRecording(url: URL) { - configureAudioSession() - let settings = [ - AVFormatIDKey: Int(kAudioFormatMPEG4AAC), - AVSampleRateKey: 12000, - AVNumberOfChannelsKey: 1, - AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue, - ] - + public func startRecording(url: URL) throws { do { + try configureAudioSession() + let settings = [ + AVFormatIDKey: Int(kAudioFormatMPEG4AAC), + AVSampleRateKey: 12000, + AVNumberOfChannelsKey: 1, + AVEncoderAudioQualityKey: AVAudioQuality.high.rawValue, + ] audioRecorder = try AVAudioRecorder(url: url, settings: settings) audioRecorder?.prepareToRecord() audioRecorder?.isMeteringEnabled = true audioRecorder?.record() } catch { // TODO: AVAudioRecorder 객체 생성 실패 시에 대한 처리 + throw ASAudioErrors(type: .startRecording, reason: error.localizedDescription, file: #file, line: #line) } } /// 오디오 세션을 설정합니다. - private func configureAudioSession() { + private func configureAudioSession() throws { do { let session = AVAudioSession.sharedInstance() try session.setCategory(.playAndRecord, mode: .default, options: [.defaultToSpeaker]) try session.setActive(true, options: .notifyOthersOnDeactivation) } catch { // TODO: 세션 설정 실패에 따른 처리 + throw ASAudioErrors(type: .configureAudioSession, reason: error.localizedDescription, file: #file, line: #line) } } diff --git a/alsongDalsong/ASDecoder/ASDecoder.xcodeproj/project.pbxproj b/alsongDalsong/ASDecoder/ASDecoder.xcodeproj/project.pbxproj index 1061884..3f301f6 100644 --- a/alsongDalsong/ASDecoder/ASDecoder.xcodeproj/project.pbxproj +++ b/alsongDalsong/ASDecoder/ASDecoder.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 1B06307A2CE64E7C005300BF /* ASDecoder.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1B702E612CE5193F00124F73 /* ASDecoder.framework */; }; 1B06307B2CE64E7C005300BF /* ASDecoder.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 1B702E612CE5193F00124F73 /* ASDecoder.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 1B702E6A2CE5193F00124F73 /* ASDecoder.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1B702E612CE5193F00124F73 /* ASDecoder.framework */; }; + 8593659A2D3F71E80086C8C4 /* ASDecoderErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859365982D3F71E40086C8C4 /* ASDecoderErrors.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -47,6 +48,7 @@ 1B06306B2CE64E74005300BF /* ASDecoderDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ASDecoderDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 1B702E612CE5193F00124F73 /* ASDecoder.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ASDecoder.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 1B702E692CE5193F00124F73 /* ASDecoderTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ASDecoderTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; + 859365982D3F71E40086C8C4 /* ASDecoderErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASDecoderErrors.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -104,6 +106,7 @@ 1B702E572CE5193F00124F73 = { isa = PBXGroup; children = ( + 859365982D3F71E40086C8C4 /* ASDecoderErrors.swift */, 1B702E632CE5193F00124F73 /* ASDecoder */, 1B702E6D2CE5193F00124F73 /* ASDecoderTests */, 1B06306C2CE64E74005300BF /* ASDecoderDemo */, @@ -285,6 +288,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 8593659A2D3F71E80086C8C4 /* ASDecoderErrors.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/alsongDalsong/ASDecoder/ASDecoder/ASDecoder.swift b/alsongDalsong/ASDecoder/ASDecoder/ASDecoder.swift index b9887c2..8240e3a 100644 --- a/alsongDalsong/ASDecoder/ASDecoder/ASDecoder.swift +++ b/alsongDalsong/ASDecoder/ASDecoder/ASDecoder.swift @@ -2,11 +2,15 @@ import Foundation public enum ASDecoder { public static func decode(_: T.Type, from data: Data) throws -> T { - let decoder = JSONDecoder() - decoder.keyDecodingStrategy = .convertFromSnakeCase - decoder.dateDecodingStrategy = .iso8601 + do { + let decoder = JSONDecoder() + decoder.keyDecodingStrategy = .convertFromSnakeCase + decoder.dateDecodingStrategy = .iso8601 - return try decoder.decode(T.self, from: data) + return try decoder.decode(T.self, from: data) + } catch { + throw ASDecoderErrors(type: .decode, reason: error.localizedDescription, file: #file, line: #line) + } } public static func handleResponse(result: Result) async throws -> T { diff --git a/alsongDalsong/ASDecoder/ASDecoderErrors.swift b/alsongDalsong/ASDecoder/ASDecoderErrors.swift new file mode 100644 index 0000000..329f882 --- /dev/null +++ b/alsongDalsong/ASDecoder/ASDecoderErrors.swift @@ -0,0 +1,16 @@ +import Foundation + +struct ASDecoderErrors: LocalizedError { + let type: ErrorType + let reason: String + let file: String + let line: Int + + enum ErrorType { + case decode + } + + var errorDescription: String? { + return "[\(URL(fileURLWithPath: file).lastPathComponent):\(line)] \(type) 에러: \n\(reason)" + } +} diff --git a/alsongDalsong/ASEncoder/ASEncoder.xcodeproj/project.pbxproj b/alsongDalsong/ASEncoder/ASEncoder.xcodeproj/project.pbxproj index 61a33e6..da3f9f2 100644 --- a/alsongDalsong/ASEncoder/ASEncoder.xcodeproj/project.pbxproj +++ b/alsongDalsong/ASEncoder/ASEncoder.xcodeproj/project.pbxproj @@ -10,6 +10,7 @@ 1B1327F02CE5F26400EF706D /* ASEncoder.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1B702E2F2CE5152600124F73 /* ASEncoder.framework */; }; 1B1327F12CE5F26400EF706D /* ASEncoder.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 1B702E2F2CE5152600124F73 /* ASEncoder.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; 1B702E382CE5152600124F73 /* ASEncoder.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1B702E2F2CE5152600124F73 /* ASEncoder.framework */; }; + 859365972D3F71390086C8C4 /* ASEncoderErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859365952D3F71310086C8C4 /* ASEncoderErrors.swift */; }; /* End PBXBuildFile section */ /* Begin PBXContainerItemProxy section */ @@ -47,6 +48,7 @@ 1B702E2F2CE5152600124F73 /* ASEncoder.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ASEncoder.framework; sourceTree = BUILT_PRODUCTS_DIR; }; 1B702E372CE5152600124F73 /* ASEncoderTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ASEncoderTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 1BDDABBD2CE5E2E000147881 /* ASEncoderDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ASEncoderDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; + 859365952D3F71310086C8C4 /* ASEncoderErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASEncoderErrors.swift; sourceTree = ""; }; /* End PBXFileReference section */ /* Begin PBXFileSystemSynchronizedRootGroup section */ @@ -104,6 +106,7 @@ 1B702E252CE5152600124F73 = { isa = PBXGroup; children = ( + 859365952D3F71310086C8C4 /* ASEncoderErrors.swift */, 1B702E312CE5152600124F73 /* ASEncoder */, 1B702E3B2CE5152600124F73 /* ASEncoderTests */, 1BDDABBE2CE5E2E000147881 /* ASEncoderDemo */, @@ -278,6 +281,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 859365972D3F71390086C8C4 /* ASEncoderErrors.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/alsongDalsong/ASEncoder/ASEncoder/ASEncoder.swift b/alsongDalsong/ASEncoder/ASEncoder/ASEncoder.swift index e5555ba..f6c2cd3 100644 --- a/alsongDalsong/ASEncoder/ASEncoder/ASEncoder.swift +++ b/alsongDalsong/ASEncoder/ASEncoder/ASEncoder.swift @@ -2,11 +2,15 @@ import Foundation public enum ASEncoder { public static func encode(_ value: T) throws -> Data { - let encoder = JSONEncoder() - encoder.dateEncodingStrategy = .iso8601 - encoder.keyEncodingStrategy = .useDefaultKeys - encoder.outputFormatting = [.prettyPrinted] + do { + let encoder = JSONEncoder() + encoder.dateEncodingStrategy = .iso8601 + encoder.keyEncodingStrategy = .useDefaultKeys + encoder.outputFormatting = [.prettyPrinted] - return try encoder.encode(value) + return try encoder.encode(value) + } catch { + throw ASEncoderErrors(type: .encode, reason: error.localizedDescription, file: #file, line: #line) + } } } diff --git a/alsongDalsong/ASEncoder/ASEncoderErrors.swift b/alsongDalsong/ASEncoder/ASEncoderErrors.swift new file mode 100644 index 0000000..f3fe0cd --- /dev/null +++ b/alsongDalsong/ASEncoder/ASEncoderErrors.swift @@ -0,0 +1,16 @@ +import Foundation + +struct ASEncoderErrors: LocalizedError { + let type: ErrorType + let reason: String + let file: String + let line: Int + + enum ErrorType { + case encode + } + + var errorDescription: String? { + return "[\(URL(fileURLWithPath: file).lastPathComponent):\(line)] \(type) 에러: \n\(reason)" + } +} diff --git a/alsongDalsong/ASMusicKit/ASMusicErrors.swift b/alsongDalsong/ASMusicKit/ASMusicErrors.swift new file mode 100644 index 0000000..941709a --- /dev/null +++ b/alsongDalsong/ASMusicKit/ASMusicErrors.swift @@ -0,0 +1,19 @@ +import Foundation + +struct ASMusicErrors: LocalizedError { + let type: ErrorType + let reason: String + let file: String + let line: Int + + enum ErrorType { + case notAuthorized + case search + case playListHasNoSongs + } + + var errorDescription: String? { + return "[\(URL(fileURLWithPath: file).lastPathComponent):\(line)] \(type) 에러: \n\(reason)" + } +} + diff --git a/alsongDalsong/ASMusicKit/ASMusicKit.xcodeproj/project.pbxproj b/alsongDalsong/ASMusicKit/ASMusicKit.xcodeproj/project.pbxproj index 73bb427..ee4660f 100644 --- a/alsongDalsong/ASMusicKit/ASMusicKit.xcodeproj/project.pbxproj +++ b/alsongDalsong/ASMusicKit/ASMusicKit.xcodeproj/project.pbxproj @@ -7,6 +7,7 @@ objects = { /* Begin PBXBuildFile section */ + 859365942D3F709D0086C8C4 /* ASMusicErrors.swift in Sources */ = {isa = PBXBuildFile; fileRef = 859365912D3F70960086C8C4 /* ASMusicErrors.swift */; }; 8D7F5A862CF88C32002EAB0F /* ASEntity.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 8D7F5A852CF88C25002EAB0F /* ASEntity.framework */; }; 8D7F5A872CF88C32002EAB0F /* ASEntity.framework in Embed Frameworks */ = {isa = PBXBuildFile; fileRef = 8D7F5A852CF88C25002EAB0F /* ASEntity.framework */; settings = {ATTRIBUTES = (CodeSignOnCopy, RemoveHeadersOnCopy, ); }; }; /* End PBXBuildFile section */ @@ -37,6 +38,7 @@ /* Begin PBXFileReference section */ 1635A4282CE59833002246E0 /* ASMusicKit.framework */ = {isa = PBXFileReference; explicitFileType = wrapper.framework; includeInIndex = 0; path = ASMusicKit.framework; sourceTree = BUILT_PRODUCTS_DIR; }; + 859365912D3F70960086C8C4 /* ASMusicErrors.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ASMusicErrors.swift; sourceTree = ""; }; 8D7F5A802CF88C25002EAB0F /* ASEntity.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = ASEntity.xcodeproj; path = ../ASEntity/ASEntity.xcodeproj; sourceTree = ""; }; /* End PBXFileReference section */ @@ -79,6 +81,7 @@ 8D9402E22CE37DA1009D21C4 = { isa = PBXGroup; children = ( + 859365912D3F70960086C8C4 /* ASMusicErrors.swift */, 8D4F31732CE3805D00E13720 /* ASMusicKit */, 1635A4282CE59833002246E0 /* ASMusicKit.framework */, 8D7F5A7F2CF88C25002EAB0F /* Frameworks */, @@ -186,6 +189,7 @@ isa = PBXSourcesBuildPhase; buildActionMask = 2147483647; files = ( + 859365942D3F709D0086C8C4 /* ASMusicErrors.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; }; diff --git a/alsongDalsong/ASMusicKit/ASMusicKit/ASMusicAPI.swift b/alsongDalsong/ASMusicKit/ASMusicKit/ASMusicAPI.swift index 640ab5f..ce3cb03 100644 --- a/alsongDalsong/ASMusicKit/ASMusicKit/ASMusicAPI.swift +++ b/alsongDalsong/ASMusicKit/ASMusicKit/ASMusicAPI.swift @@ -55,10 +55,10 @@ public struct ASMusicAPI { return music } } catch { - throw ASMusicError.searchError + throw ASMusicErrors(type: .search, reason: error.localizedDescription, file: #file, line: #line) } default: - throw ASMusicError.notAuthorized + throw ASMusicErrors(type: .notAuthorized, reason: "", file: #file, line: #line) } } @@ -73,7 +73,7 @@ public struct ASMusicAPI { let playlistWithTrack = try await playlist.with([.tracks]) guard let tracks = playlistWithTrack.tracks else { - throw ASMusicError.playListHasNoSongs + throw ASMusicErrors(type: .playListHasNoSongs, reason: "", file: #file, line: #line) } if let song = tracks.randomElement() { @@ -87,28 +87,11 @@ public struct ASMusicAPI { ) } } catch { - throw ASMusicError.playListHasNoSongs + throw ASMusicErrors(type: .search, reason: "", file: #file, line: #line) } default: - throw ASMusicError.notAuthorized + throw ASMusicErrors(type: .notAuthorized, reason: "", file: #file, line: #line) } return ASEntity.Music(id: "nil", title: nil, artist: nil, artworkUrl: nil, previewUrl: nil, artworkBackgroundColor: nil) } } - -enum ASMusicError: Error, LocalizedError { - case notAuthorized - case searchError - case playListHasNoSongs - - var errorDescription: String? { - switch self { - case .notAuthorized: - "애플 뮤직에 접근하는 권한이 없습니다." - case .searchError: - "노래 검색 중 오류가 발생했습니다." - case .playListHasNoSongs: - "플레이리스트에 노래가 없습니다." - } - } -} diff --git a/alsongDalsong/ASNetworkKit/ASNetworkKit/ASNetworkErrors.swift b/alsongDalsong/ASNetworkKit/ASNetworkKit/ASNetworkErrors.swift index d84c26e..cf736ee 100644 --- a/alsongDalsong/ASNetworkKit/ASNetworkKit/ASNetworkErrors.swift +++ b/alsongDalsong/ASNetworkKit/ASNetworkKit/ASNetworkErrors.swift @@ -1,27 +1,29 @@ import Foundation -public enum ASNetworkErrors: Error, LocalizedError { - case serverError(message: String) - case urlError - case responseError - case FirebaseSignInError - case FirebaseSignOutError - case FirebaseListenerError - +public struct ASNetworkErrors: LocalizedError { + let type: ErrorType + let reason: String + let file: String + let line: Int + + public init(type: ErrorType, reason: String, file: String, line: Int) { + self.type = type + self.reason = reason + self.file = file + self.line = line + } + + public enum ErrorType { + case serverError + case urlError + case getAvatarUrls + case firebaseSignIn, firebaseSignOut + case firebaseListener + case responseError + } + public var errorDescription: String? { - switch self { - case let .serverError(message: message): - return message - case .urlError: - return "URL에러: URL이 제대로 입력되지 않았습니다." - case .responseError: - return "응답 에러: 서버에서 응답이 없거나 잘못된 응답이 왔습니다." - case .FirebaseSignInError: - return "파이어베이스 에러: 익명 로그인에 실패했습니다." - case .FirebaseSignOutError: - return "파이어베이스 에러: 로그아웃에 실패했습니다." - case .FirebaseListenerError: - return "파이어베이스 에러: 해당 데이터베이스를 가져오는데 실패했습니다." - } + return "[\(URL(fileURLWithPath: file).lastPathComponent):\(line)] \(type) 에러: \(reason)" } } + diff --git a/alsongDalsong/ASNetworkKit/ASNetworkKit/ASNetworkManager.swift b/alsongDalsong/ASNetworkKit/ASNetworkKit/ASNetworkManager.swift index a1e29e5..e07cb59 100644 --- a/alsongDalsong/ASNetworkKit/ASNetworkKit/ASNetworkManager.swift +++ b/alsongDalsong/ASNetworkKit/ASNetworkKit/ASNetworkManager.swift @@ -17,7 +17,9 @@ struct ASNetworkManager: ASNetworkManagerProtocol { body: Data? = nil, option: CacheOption = .both ) async throws -> Data { - guard let url = endpoint.url else { throw ASNetworkErrors.urlError } + guard let url = endpoint.url else { + throw ASNetworkErrors(type: .urlError, reason: "", file: #file, line: #line) + } if let cache = try await loadCache(from: url, option: option) { return cache } let updatedEndpoint = updateEndpoint(type: type, endpoint: endpoint, body: body) @@ -45,7 +47,9 @@ struct ASNetworkManager: ASNetworkManagerProtocol { } private func urlRequest(for endpoint: any Endpoint) throws -> URLRequest { - guard let url = endpoint.url else { throw ASNetworkErrors.urlError } + guard let url = endpoint.url else { + throw ASNetworkErrors(type: .urlError, reason: "", file: #file, line: #line) + } return RequestBuilder(using: url) .setHeader(endpoint.headers) .setHttpMethod(endpoint.method) @@ -55,7 +59,7 @@ struct ASNetworkManager: ASNetworkManagerProtocol { private func validate(response: URLResponse) throws { guard let httpResponse = response as? HTTPURLResponse else { - throw ASNetworkErrors.responseError + throw ASNetworkErrors(type: .responseError, reason: "", file: #file, line: #line) } let statusCode = StatusCode(statusCode: httpResponse.statusCode) @@ -63,7 +67,7 @@ struct ASNetworkManager: ASNetworkManagerProtocol { case .success, .noContent: break default: - throw ASNetworkErrors.serverError(message: statusCode.description) + throw ASNetworkErrors(type: .serverError, reason: "", file: #file, line: #line) } } } diff --git a/alsongDalsong/ASNetworkKit/ASNetworkKit/Firebase/ASFirebaseAuth.swift b/alsongDalsong/ASNetworkKit/ASNetworkKit/Firebase/ASFirebaseAuth.swift index 1a1c31f..e86374b 100644 --- a/alsongDalsong/ASNetworkKit/ASNetworkKit/Firebase/ASFirebaseAuth.swift +++ b/alsongDalsong/ASNetworkKit/ASNetworkKit/Firebase/ASFirebaseAuth.swift @@ -10,7 +10,9 @@ public final class ASFirebaseAuth: ASFirebaseAuthProtocol { public func signIn(nickname: String, avatarURL: URL?) async throws { do { - guard let myID = ASFirebaseAuth.myID else { throw ASNetworkErrors.FirebaseSignInError } + guard let myID = ASFirebaseAuth.myID else { + throw ASNetworkErrors(type: .firebaseSignIn, reason: "ASFirebaseAuth.myID is nil", file: #file, line: #line) + } let player = Player(id: myID, avatarUrl: avatarURL, nickname: nickname, order: 0) let playerData = try ASEncoder.encode(player) let dict = try JSONSerialization.jsonObject(with: playerData, options: .allowFragments) as? [String: Any] @@ -24,17 +26,19 @@ public final class ASFirebaseAuth: ASFirebaseAuthProtocol { } } } catch { - throw ASNetworkErrors.FirebaseSignInError + throw ASNetworkErrors(type: .firebaseSignIn, reason: error.localizedDescription, file: #file, line: #line) } } public func signOut() async throws { do { - guard let userID = ASFirebaseAuth.myID else { throw ASNetworkErrors.FirebaseSignOutError } + guard let userID = ASFirebaseAuth.myID else { + throw ASNetworkErrors(type: .firebaseSignOut, reason: "ASFirebaseAuth.myID is nil", file: #file, line: #line) + } try await databaseRef.child("players").child(userID).removeValue() try Auth.auth().signOut() } catch { - throw ASNetworkErrors.FirebaseSignOutError + throw ASNetworkErrors(type: .firebaseSignOut, reason: error.localizedDescription, file: #file, line: #line) } } diff --git a/alsongDalsong/ASNetworkKit/ASNetworkKit/Firebase/ASFirebaseDatabase.swift b/alsongDalsong/ASNetworkKit/ASNetworkKit/Firebase/ASFirebaseDatabase.swift index 8ef3ab5..58e2820 100644 --- a/alsongDalsong/ASNetworkKit/ASNetworkKit/Firebase/ASFirebaseDatabase.swift +++ b/alsongDalsong/ASNetworkKit/ASNetworkKit/Firebase/ASFirebaseDatabase.swift @@ -15,14 +15,14 @@ final class ASFirebaseDatabase: ASFirebaseDatabaseProtocol { } guard let document = documentSnapshot, document.exists else { - return self.roomPublisher.send(completion: .failure(ASNetworkErrors.FirebaseListenerError)) + return self.roomPublisher.send(completion: .failure(ASNetworkErrors(type: .firebaseListener, reason: error?.localizedDescription ?? "", file: #file, line: #line))) } do { let room = try document.data(as: Room.self) return self.roomPublisher.send(room) } catch { - return self.roomPublisher.send(completion: .failure(ASNetworkErrors.FirebaseListenerError)) + return self.roomPublisher.send(completion: .failure(ASNetworkErrors(type: .firebaseListener, reason: error.localizedDescription, file: #file, line: #line))) } } diff --git a/alsongDalsong/ASNetworkKit/ASNetworkKit/Firebase/ASFirebaseStorage.swift b/alsongDalsong/ASNetworkKit/ASNetworkKit/Firebase/ASFirebaseStorage.swift index de09fe6..9ac1e1b 100644 --- a/alsongDalsong/ASNetworkKit/ASNetworkKit/Firebase/ASFirebaseStorage.swift +++ b/alsongDalsong/ASNetworkKit/ASNetworkKit/Firebase/ASFirebaseStorage.swift @@ -10,7 +10,7 @@ final class ASFirebaseStorage: ASFirebaseStorageProtocol { let result = try await avatarRef.listAll() return try await fetchDownloadURLs(from: result.items) } catch { - throw ASNetworkErrors.responseError + throw ASNetworkErrors(type: .getAvatarUrls, reason: error.localizedDescription, file: #file, line: #line) } } diff --git a/alsongDalsong/ASRepository/ASRepository/ASRepositoryErrors.swift b/alsongDalsong/ASRepository/ASRepository/ASRepositoryErrors.swift new file mode 100644 index 0000000..a5ce41a --- /dev/null +++ b/alsongDalsong/ASRepository/ASRepository/ASRepositoryErrors.swift @@ -0,0 +1,21 @@ +import Foundation + +struct ASRepositoryErrors: LocalizedError { + let type: ErrorType + let reason: String + let file: String + let line: Int + + enum ErrorType { + case submitMusic + case getAvatarUrls + case postRecording, postResetGame + case uploadRecording + case createRoom, joinRoom, leaveRoom, startGame, changeMode, changeRecordOrder, resetGame, sendRequest + case submitAnswer + } + + var errorDescription: String? { + return "[\(URL(fileURLWithPath: file).lastPathComponent):\(line)] \(type) 에러: \n\(reason)" + } +} diff --git a/alsongDalsong/ASRepository/ASRepository/Repositories/AnswersRepository.swift b/alsongDalsong/ASRepository/ASRepository/Repositories/AnswersRepository.swift index 241ffb2..2568279 100644 --- a/alsongDalsong/ASRepository/ASRepository/Repositories/AnswersRepository.swift +++ b/alsongDalsong/ASRepository/ASRepository/Repositories/AnswersRepository.swift @@ -38,14 +38,18 @@ final class AnswersRepository: AnswersRepositoryProtocol { } func submitMusic(answer: ASEntity.Music) async throws -> Bool { - let queryItems = [URLQueryItem(name: "userId", value: ASFirebaseAuth.myID), - URLQueryItem(name: "roomNumber", value: mainRepository.number.value)] - let endPoint = FirebaseEndpoint(path: .submitMusic, method: .post) - .update(\.queryItems, with: queryItems) + do { + let queryItems = [URLQueryItem(name: "userId", value: ASFirebaseAuth.myID), + URLQueryItem(name: "roomNumber", value: mainRepository.number.value)] + let endPoint = FirebaseEndpoint(path: .submitMusic, method: .post) + .update(\.queryItems, with: queryItems) - let body = try ASEncoder.encode(answer) - let response = try await networkManager.sendRequest(to: endPoint, type: .json, body: body, option: .none) - let responseDict = try ASDecoder.decode([String: String].self, from: response) - return !responseDict.isEmpty + let body = try ASEncoder.encode(answer) + let response = try await networkManager.sendRequest(to: endPoint, type: .json, body: body, option: .none) + let responseDict = try ASDecoder.decode([String: String].self, from: response) + return !responseDict.isEmpty + } catch { + throw ASRepositoryErrors(type: .submitMusic, reason: error.localizedDescription, file: #file, line: #line) + } } } diff --git a/alsongDalsong/ASRepository/ASRepository/Repositories/AvatarRepository.swift b/alsongDalsong/ASRepository/ASRepository/Repositories/AvatarRepository.swift index 82a177e..3e1219d 100644 --- a/alsongDalsong/ASRepository/ASRepository/Repositories/AvatarRepository.swift +++ b/alsongDalsong/ASRepository/ASRepository/Repositories/AvatarRepository.swift @@ -18,7 +18,7 @@ final class AvatarRepository: AvatarRepositoryProtocol { let urls = try await self.storageManager.getAvatarUrls() return urls } catch { - throw error + throw ASRepositoryErrors(type: .getAvatarUrls, reason: error.localizedDescription, file: #file, line: #line) } } } diff --git a/alsongDalsong/ASRepository/ASRepository/Repositories/DataDownloadRepository.swift b/alsongDalsong/ASRepository/ASRepository/Repositories/DataDownloadRepository.swift index 9e86ed2..d13cab9 100644 --- a/alsongDalsong/ASRepository/ASRepository/Repositories/DataDownloadRepository.swift +++ b/alsongDalsong/ASRepository/ASRepository/Repositories/DataDownloadRepository.swift @@ -1,3 +1,4 @@ +import ASLogKit import ASNetworkKit import ASRepositoryProtocol @@ -14,6 +15,7 @@ final class DataDownloadRepository: DataDownloadRepositoryProtocol { let data = try await networkManager.sendRequest(to: endpoint, type: .none, body: nil, option: .both) return data } catch { + Logger.error("DataDownloadRepository.swift downloadData() 에러: \n\(error.localizedDescription)") return nil } } diff --git a/alsongDalsong/ASRepository/ASRepository/Repositories/MainRepository.swift b/alsongDalsong/ASRepository/ASRepository/Repositories/MainRepository.swift index 726ae8e..f19785e 100644 --- a/alsongDalsong/ASRepository/ASRepository/Repositories/MainRepository.swift +++ b/alsongDalsong/ASRepository/ASRepository/Repositories/MainRepository.swift @@ -86,37 +86,45 @@ final class MainRepository: MainRepositoryProtocol { } func postRecording(_ record: Data) async throws -> Bool { - let queryItems = [URLQueryItem(name: "userId", value: ASFirebaseAuth.myID), - URLQueryItem(name: "roomNumber", value: number.value)] - let endPoint = FirebaseEndpoint(path: .uploadRecording, method: .post) - .update(\.queryItems, with: queryItems) + do { + let queryItems = [URLQueryItem(name: "userId", value: ASFirebaseAuth.myID), + URLQueryItem(name: "roomNumber", value: number.value)] + let endPoint = FirebaseEndpoint(path: .uploadRecording, method: .post) + .update(\.queryItems, with: queryItems) - let response = try await networkManager.sendRequest( - to: endPoint, - type: .multipart, - body: record, - option: .none - ) - let responseDict = try ASDecoder.decode([String: Bool].self, from: response) - guard let success = responseDict["success"] else { return false } - return success + let response = try await networkManager.sendRequest( + to: endPoint, + type: .multipart, + body: record, + option: .none + ) + let responseDict = try ASDecoder.decode([String: Bool].self, from: response) + guard let success = responseDict["success"] else { return false } + return success + } catch { + throw ASRepositoryErrors(type: .postRecording, reason: error.localizedDescription, file: #file, line: #line) + } } func postResetGame() async throws -> Bool { - let queryItems = [URLQueryItem(name: "userId", value: ASFirebaseAuth.myID), - URLQueryItem(name: "roomNumber", value: number.value)] - let endPoint = FirebaseEndpoint(path: .resetGame, method: .post) - .update(\.queryItems, with: queryItems) + do { + let queryItems = [URLQueryItem(name: "userId", value: ASFirebaseAuth.myID), + URLQueryItem(name: "roomNumber", value: number.value)] + let endPoint = FirebaseEndpoint(path: .resetGame, method: .post) + .update(\.queryItems, with: queryItems) + + let response = try await networkManager.sendRequest( + to: endPoint, + type: .none, + body: nil, + option: .none + ) - let response = try await networkManager.sendRequest( - to: endPoint, - type: .none, - body: nil, - option: .none - ) - - let responseDict = try ASDecoder.decode([String: Bool].self, from: response) - guard let success = responseDict["success"] else { return false } - return success + let responseDict = try ASDecoder.decode([String: Bool].self, from: response) + guard let success = responseDict["success"] else { return false } + return success + } catch { + throw ASRepositoryErrors(type: .postResetGame, reason: error.localizedDescription, file: #file, line: #line) + } } } diff --git a/alsongDalsong/ASRepository/ASRepository/Repositories/RecordsRepository.swift b/alsongDalsong/ASRepository/ASRepository/Repositories/RecordsRepository.swift index 790d9a7..517d01b 100644 --- a/alsongDalsong/ASRepository/ASRepository/Repositories/RecordsRepository.swift +++ b/alsongDalsong/ASRepository/ASRepository/Repositories/RecordsRepository.swift @@ -40,7 +40,11 @@ final class RecordsRepository: RecordsRepositoryProtocol { } func uploadRecording(_ record: Data) async throws -> Bool { - return try await mainRepository.postRecording(record) + do { + return try await mainRepository.postRecording(record) + } catch { + throw ASRepositoryErrors(type: .uploadRecording, reason: error.localizedDescription, file: #file, line: #line) + } } private func findRecord(records: [ASEntity.Record]?, diff --git a/alsongDalsong/ASRepository/ASRepository/Repositories/RoomActionRepository.swift b/alsongDalsong/ASRepository/ASRepository/Repositories/RoomActionRepository.swift index 12b1a11..5276916 100644 --- a/alsongDalsong/ASRepository/ASRepository/Repositories/RoomActionRepository.swift +++ b/alsongDalsong/ASRepository/ASRepository/Repositories/RoomActionRepository.swift @@ -20,77 +20,109 @@ final class RoomActionRepository: RoomActionRepositoryProtocol { } func createRoom(nickname: String, avatar: URL) async throws -> String { - try await self.authManager.signIn(nickname: nickname, avatarURL: avatar) - let response: [String: String]? = try await self.sendRequest( - endpointPath: .createRoom, - requestBody: ["hostID": ASFirebaseAuth.myID] - ) - guard let roomNumber = response?["number"] as? String else { - throw ASNetworkErrors.responseError + do { + try await self.authManager.signIn(nickname: nickname, avatarURL: avatar) + let response: [String: String]? = try await self.sendRequest( + endpointPath: .createRoom, + requestBody: ["hostID": ASFirebaseAuth.myID] + ) + guard let roomNumber = response?["number"] as? String else { + throw ASNetworkErrors(type: .responseError, reason: "", file: #file, line: #line) + } + return roomNumber + } catch { + throw ASRepositoryErrors(type: .createRoom, reason: error.localizedDescription, file: #file, line: #line) } - return roomNumber } func joinRoom(nickname: String, avatar: URL, roomNumber: String) async throws -> Bool { - let player = try await self.authManager.signIn(nickname: nickname, avatarURL: avatar) - let response: [String: String]? = try await self.sendRequest( - endpointPath: .joinRoom, - requestBody: ["roomNumber": roomNumber, "userId": ASFirebaseAuth.myID] - ) - guard let roomNumberResponse = response?["number"] as? String else { - throw ASNetworkErrors.responseError + do { + let player = try await self.authManager.signIn(nickname: nickname, avatarURL: avatar) + let response: [String: String]? = try await self.sendRequest( + endpointPath: .joinRoom, + requestBody: ["roomNumber": roomNumber, "userId": ASFirebaseAuth.myID] + ) + guard let roomNumberResponse = response?["number"] as? String else { + throw ASNetworkErrors(type: .responseError, reason: "", file: #file, line: #line) + } + return roomNumberResponse == roomNumber + } catch { + throw ASRepositoryErrors(type: .joinRoom, reason: error.localizedDescription, file: #file, line: #line) } - return roomNumberResponse == roomNumber } func leaveRoom() async throws -> Bool { - self.mainRepository.disconnectRoom() - try await self.authManager.signOut() - return true + do { + self.mainRepository.disconnectRoom() + try await self.authManager.signOut() + return true + } catch { + throw ASRepositoryErrors(type: .leaveRoom, reason: error.localizedDescription, file: #file, line: #line) + } } func startGame(roomNumber: String) async throws -> Bool { - let response: [String: Bool]? = try await self.sendRequest( - endpointPath: .gameStart, - requestBody: ["roomNumber": roomNumber, "userId": ASFirebaseAuth.myID] - ) - guard let response = response?["success"] as? Bool else { - throw ASNetworkErrors.responseError + do { + let response: [String: Bool]? = try await self.sendRequest( + endpointPath: .gameStart, + requestBody: ["roomNumber": roomNumber, "userId": ASFirebaseAuth.myID] + ) + guard let response = response?["success"] as? Bool else { + throw ASNetworkErrors(type: .responseError, reason: "", file: #file, line: #line) + } + return response + } catch { + throw ASRepositoryErrors(type: .startGame, reason: error.localizedDescription, file: #file, line: #line) } - return response } func changeMode(roomNumber: String, mode: Mode) async throws -> Bool { - let response: [String: Bool] = try await self.sendRequest( - endpointPath: .changeMode, - requestBody: ["roomNumber": roomNumber, "userId": ASFirebaseAuth.myID, "mode": mode.rawValue] - ) - guard let isSuccess = response["success"] as? Bool else { - throw ASNetworkErrors.responseError + do { + let response: [String: Bool] = try await self.sendRequest( + endpointPath: .changeMode, + requestBody: ["roomNumber": roomNumber, "userId": ASFirebaseAuth.myID, "mode": mode.rawValue] + ) + guard let isSuccess = response["success"] as? Bool else { + throw ASNetworkErrors(type: .responseError, reason: "", file: #file, line: #line) + } + return isSuccess + } catch { + throw ASRepositoryErrors(type: .changeMode, reason: error.localizedDescription, file: #file, line: #line) } - return isSuccess } func changeRecordOrder(roomNumber: String) async throws -> Bool { - let response: [String: Bool] = try await self.sendRequest( - endpointPath: .changeRecordOrder, - requestBody: ["roomNumber": roomNumber, "userId": ASFirebaseAuth.myID] - ) - guard let isSuccess = response["success"] as? Bool else { - throw ASNetworkErrors.responseError + do { + let response: [String: Bool] = try await self.sendRequest( + endpointPath: .changeRecordOrder, + requestBody: ["roomNumber": roomNumber, "userId": ASFirebaseAuth.myID] + ) + guard let isSuccess = response["success"] as? Bool else { + throw ASNetworkErrors(type: .responseError, reason: "", file: #file, line: #line) + } + return isSuccess + } catch { + throw ASRepositoryErrors(type: .changeRecordOrder, reason: error.localizedDescription, file: #file, line: #line) } - return isSuccess } func resetGame() async throws -> Bool { - return try await mainRepository.postResetGame() + do { + return try await mainRepository.postResetGame() + } catch { + throw ASRepositoryErrors(type: .resetGame, reason: error.localizedDescription, file: #file, line: #line) + } } private func sendRequest(endpointPath: FirebaseEndpoint.Path, requestBody: [String: Any]) async throws -> T { - let endpoint = FirebaseEndpoint(path: endpointPath, method: .post) - let body = try JSONSerialization.data(withJSONObject: requestBody, options: []) - let data = try await networkManager.sendRequest(to: endpoint, type: .json, body: body, option: .none) - let response = try JSONDecoder().decode(T.self, from: data) - return response + do { + let endpoint = FirebaseEndpoint(path: endpointPath, method: .post) + let body = try JSONSerialization.data(withJSONObject: requestBody, options: []) + let data = try await networkManager.sendRequest(to: endpoint, type: .json, body: body, option: .none) + let response = try JSONDecoder().decode(T.self, from: data) + return response + } catch { + throw ASRepositoryErrors(type: .sendRequest, reason: error.localizedDescription, file: #file, line: #line) + } } } diff --git a/alsongDalsong/ASRepository/ASRepository/Repositories/SubmitsRepository.swift b/alsongDalsong/ASRepository/ASRepository/Repositories/SubmitsRepository.swift index e56d250..7d9f568 100644 --- a/alsongDalsong/ASRepository/ASRepository/Repositories/SubmitsRepository.swift +++ b/alsongDalsong/ASRepository/ASRepository/Repositories/SubmitsRepository.swift @@ -24,15 +24,19 @@ final class SubmitsRepository: SubmitsRepositoryProtocol { } func submitAnswer(answer: Music) async throws -> Bool { - let queryItems = [URLQueryItem(name: "userId", value: ASFirebaseAuth.myID), - URLQueryItem(name: "roomNumber", value: mainRepository.number.value)] - let endPoint = FirebaseEndpoint(path: .submitAnswer, method: .post) - .update(\.queryItems, with: queryItems) - .update(\.headers, with: ["Content-Type": "application/json"]) + do { + let queryItems = [URLQueryItem(name: "userId", value: ASFirebaseAuth.myID), + URLQueryItem(name: "roomNumber", value: mainRepository.number.value)] + let endPoint = FirebaseEndpoint(path: .submitAnswer, method: .post) + .update(\.queryItems, with: queryItems) + .update(\.headers, with: ["Content-Type": "application/json"]) - let body = try ASEncoder.encode(answer) - let response = try await networkManager.sendRequest(to: endPoint, type: .json, body: body, option: .none) - let responseDict = try ASDecoder.decode([String: String].self, from: response) - return !responseDict.isEmpty + let body = try ASEncoder.encode(answer) + let response = try await networkManager.sendRequest(to: endPoint, type: .json, body: body, option: .none) + let responseDict = try ASDecoder.decode([String: String].self, from: response) + return !responseDict.isEmpty + } catch { + throw ASRepositoryErrors(type: .submitAnswer, reason: error.localizedDescription, file: #file, line: #line) + } } } diff --git a/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Utils/ASErrors.swift b/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Utils/ASErrors.swift new file mode 100644 index 0000000..7a74a94 --- /dev/null +++ b/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Utils/ASErrors.swift @@ -0,0 +1,25 @@ +import Foundation + +struct ASErrors: LocalizedError { + let type: ErrorType + let reason: String + let file: String + let line: Int + + enum ErrorType { + case analyze, startRecording, playFull, playPartial + case leaveRoom + case submitHumming + case fetchAvatars + case gameStart, changeMode + case authorizeAppleMusic, joinRoom, createRoom + case submitRehumming + case changeRecordOrder, navigateToLobby + case submitMusic, searchMusicOnSelect, randomMusic + case searchMusicOnSubmit, submitAnswer + } + + var errorDescription: String? { + return "\n[\(URL(fileURLWithPath: file).lastPathComponent):\(line)] \(type) 에러: \n\(reason)" + } +} diff --git a/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Utils/AudioHelper.swift b/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Utils/AudioHelper.swift index a1becf9..28270dd 100644 --- a/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Utils/AudioHelper.swift +++ b/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Utils/AudioHelper.swift @@ -1,5 +1,4 @@ import ASAudioKit -import ASLogKit import Combine import Foundation @@ -69,12 +68,14 @@ actor AudioHelper { func analyze(with data: Data) async -> [CGFloat] { do { - Logger.debug("파형분석 시작") + LogHandler.handleDebug("파형분석 시작") let columns = try await ASAudioAnalyzer.analyze(data: data, samplesCount: 24) - Logger.debug("파형분석 완료") - Logger.debug(columns) + LogHandler.handleDebug("파형분석 완료") + LogHandler.handleDebug(columns) return columns } catch { + let error = ASErrors(type: .analyze, reason: error.localizedDescription, file: #file, line: #line) + LogHandler.handleError(error) return [] } } @@ -112,13 +113,22 @@ extension AudioHelper { func play(file: Data, option: PlayType) async { switch option { - case .full: await player?.startPlaying(data: file) + case .full: + do { + try await player?.startPlaying(data: file) + } catch { + let error = ASErrors(type: .playFull, reason: error.localizedDescription, file: #file, line: #line) + LogHandler.handleError(error) + } case let .partial(time): - await player?.startPlaying(data: file) do { + try await player?.startPlaying(data: file) try await Task.sleep(nanoseconds: UInt64(time * 1_000_000_000)) await stopPlaying() - } catch { Logger.error(error.localizedDescription) } + } catch { + let error = ASErrors(type: .playPartial, reason: error.localizedDescription, file: #file, line: #line) + LogHandler.handleError(error) + } @unknown default: break } } @@ -167,19 +177,23 @@ extension AudioHelper { makeRecorder() let tempURL = makeURL() recorderStateSubject.send(true) - await recorder?.startRecording(url: tempURL) - visualize() - Logger.debug("녹음 시작") do { + try await recorder?.startRecording(url: tempURL) + visualize() + LogHandler.handleDebug("녹음 시작") + try await Task.sleep(nanoseconds: 6 * 1_000_000_000) let recordedData = await stopRecording() sendDataThrough(recorderDataSubject, recordedData ?? Data()) - } catch { Logger.error(error.localizedDescription) } + } catch { + let error = ASErrors(type: .startRecording, reason: error.localizedDescription, file: #file, line: #line) + LogHandler.handleError(error) + } } private func stopRecording() async -> Data? { let recordedData = await recorder?.stopRecording() - Logger.debug("녹음 정지") + LogHandler.handleDebug("녹음 정지") recorderStateSubject.send(false) removeRecorder() return recordedData @@ -261,7 +275,7 @@ extension AudioHelper { } private func calculateAmplitude() { - Logger.debug("진폭계산 시작") + LogHandler.handleDebug("진폭계산 시작") cancellable = Timer.publish(every: 0.125, on: .main, in: .common) .autoconnect() .sink { [weak self] _ in diff --git a/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Utils/LogHandler.swift b/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Utils/LogHandler.swift new file mode 100644 index 0000000..31fb59f --- /dev/null +++ b/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Utils/LogHandler.swift @@ -0,0 +1,15 @@ +import ASLogKit + +enum LogHandler { + static func handleError(_ message: String) { + Logger.error(message) + } + + static func handleError(_ error: ASErrors) { + handleError(error.localizedDescription) + } + + static func handleDebug(_ message: Any) { + Logger.debug(message) + } +} diff --git a/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/Game/GameNavigationController.swift b/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/Game/GameNavigationController.swift index 13f9b8f..6b2fc5d 100644 --- a/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/Game/GameNavigationController.swift +++ b/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/Game/GameNavigationController.swift @@ -1,6 +1,5 @@ import ASContainer import ASEntity -import ASLogKit import ASRepositoryProtocol import Combine import UIKit @@ -287,7 +286,8 @@ final class GameNavigationController: @unchecked Sendable { do { _ = try await roomActionRepository.leaveRoom() } catch { - Logger.error(error.localizedDescription) + let error = ASErrors(type: .leaveRoom, reason: error.localizedDescription, file: #file, line: #line) + LogHandler.handleError(error) } } } diff --git a/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/Humming/HummingViewModel.swift b/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/Humming/HummingViewModel.swift index 1436454..5a84a14 100644 --- a/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/Humming/HummingViewModel.swift +++ b/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/Humming/HummingViewModel.swift @@ -1,5 +1,4 @@ import ASEntity -import ASLogKit import ASRepositoryProtocol import Combine import Foundation @@ -46,9 +45,10 @@ final class HummingViewModel: @unchecked Sendable { private func submitHumming() async { do { let result = try await recordsRepository.uploadRecording(recordedData ?? Data()) - if !result { Logger.error("Humming Did not sent") } + if !result { LogHandler.handleError("Humming Did not sent") } } catch { - Logger.error(error.localizedDescription) + let error = ASErrors(type: .submitHumming, reason: error.localizedDescription, file: #file, line: #line) + LogHandler.handleError(error) } } diff --git a/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/Loading/LoadingViewModel.swift b/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/Loading/LoadingViewModel.swift index 8f5ec17..c83ddf3 100644 --- a/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/Loading/LoadingViewModel.swift +++ b/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/Loading/LoadingViewModel.swift @@ -21,22 +21,27 @@ final class LoadingViewModel: @unchecked Sendable { func fetchAvatars() { Task { - loadingStatus = String(localized: "게임 데이터 불러오는 중") - avatars = try await avatarRepository.getAvatarUrls() - - guard let randomAvatarUrl = avatars.randomElement() else { return } - selectedAvatar = randomAvatarUrl - loadingStatus = String(localized: "아바타 이미지 다운로드 중") - - await withTaskGroup(of: Data?.self) { group in - avatars.forEach { url in - group.addTask { [weak self] in - return await self?.dataDownloadRepository.downloadData(url: url) + do { + loadingStatus = String(localized: "게임 데이터 불러오는 중") + avatars = try await avatarRepository.getAvatarUrls() + + guard let randomAvatarUrl = avatars.randomElement() else { return } + selectedAvatar = randomAvatarUrl + loadingStatus = String(localized: "아바타 이미지 다운로드 중") + + await withTaskGroup(of: Data?.self) { group in + avatars.forEach { url in + group.addTask { [weak self] in + return await self?.dataDownloadRepository.downloadData(url: url) + } } } + + avatarData = await dataDownloadRepository.downloadData(url: randomAvatarUrl) + } catch { + let error = ASErrors(type: .fetchAvatars, reason: error.localizedDescription, file: #file, line: #line) + LogHandler.handleError(error) } - - avatarData = await dataDownloadRepository.downloadData(url: randomAvatarUrl) } } } diff --git a/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/Lobby/LobbyViewController.swift b/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/Lobby/LobbyViewController.swift index a9d16d2..3049256 100644 --- a/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/Lobby/LobbyViewController.swift +++ b/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/Lobby/LobbyViewController.swift @@ -125,11 +125,7 @@ final class LobbyViewController: UIViewController { } private func gameStart() async throws { - do { - try await viewmodel.gameStart() - } catch { - throw error - } + try await viewmodel.gameStart() } } diff --git a/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/Lobby/LobbyViewModel.swift b/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/Lobby/LobbyViewModel.swift index 8f48294..cdf7af4 100644 --- a/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/Lobby/LobbyViewModel.swift +++ b/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/Lobby/LobbyViewModel.swift @@ -1,5 +1,4 @@ import ASEntity -import ASLogKit import ASRepositoryProtocol import Combine import Foundation @@ -94,6 +93,8 @@ final class LobbyViewModel: ObservableObject, @unchecked Sendable { do { _ = try await roomActionRepository.startGame(roomNumber: roomNumber) } catch { + let error = ASErrors(type: .gameStart, reason: error.localizedDescription, file: #file, line: #line) + LogHandler.handleError(error.localizedDescription) throw error } } @@ -105,7 +106,8 @@ final class LobbyViewModel: ObservableObject, @unchecked Sendable { _ = try await self.roomActionRepository.changeMode(roomNumber: roomNumber, mode: mode) } } catch { - Logger.error(error.localizedDescription) + let error = ASErrors(type: .changeMode, reason: error.localizedDescription, file: #file, line: #line) + LogHandler.handleError(error) } } } diff --git a/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/Onboarding/OnboardingViewModel.swift b/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/Onboarding/OnboardingViewModel.swift index 1470907..2f65df0 100644 --- a/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/Onboarding/OnboardingViewModel.swift +++ b/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/Onboarding/OnboardingViewModel.swift @@ -42,7 +42,12 @@ final class OnboardingViewModel: @unchecked Sendable { func authorizeAppleMusic() { let musicAPI = ASMusicAPI() Task { - let _ = try await musicAPI.search(for: "뉴진스", 1, 1) + do { + let _ = try await musicAPI.search(for: "뉴진스", 1, 1) + } catch { + let error = ASErrors(type: .authorizeAppleMusic, reason: error.localizedDescription, file: #file, line: #line) + LogHandler.handleError(error) + } } } @@ -55,6 +60,9 @@ final class OnboardingViewModel: @unchecked Sendable { return id } catch { buttonEnabled = true + + let error = ASErrors(type: .joinRoom, reason: error.localizedDescription, file: #file, line: #line) + LogHandler.handleError(error) throw error } } @@ -68,6 +76,9 @@ final class OnboardingViewModel: @unchecked Sendable { return try await joinRoom(roomNumber: roomNumber) } catch { buttonEnabled = true + + let error = ASErrors(type: .createRoom, reason: error.localizedDescription, file: #file, line: #line) + LogHandler.handleError(error) throw error } } diff --git a/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/Rehumming/RehummingViewModel.swift b/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/Rehumming/RehummingViewModel.swift index 822bfce..f159021 100644 --- a/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/Rehumming/RehummingViewModel.swift +++ b/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/Rehumming/RehummingViewModel.swift @@ -30,12 +30,9 @@ final class RehummingViewModel: @unchecked Sendable { guard let recordedData else { return } do { let result = try await recordsRepository.uploadRecording(recordedData) - if result { - // 전송됨 - } else { - // 전송 안됨, 오류 alert - } } catch { + let error = ASErrors(type: .submitHumming, reason: error.localizedDescription, file: #file, line: #line) + LogHandler.handleError(error) throw error } } @@ -48,7 +45,10 @@ final class RehummingViewModel: @unchecked Sendable { func updateRecordedData(with data: Data) { // TODO: - data가 empty일 때(녹음이 제대로 되지 않았을 때 사용자 오류처리 필요 - guard !data.isEmpty else { return } + guard !data.isEmpty else { + isRecording = false + return + } recordedData = data isRecording = false } diff --git a/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/Result/HummingResultViewModel.swift b/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/Result/HummingResultViewModel.swift index 4b2cbef..9d12077 100644 --- a/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/Result/HummingResultViewModel.swift +++ b/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/Result/HummingResultViewModel.swift @@ -1,5 +1,4 @@ import ASEntity -import ASLogKit import ASRepositoryProtocol import Combine import Foundation @@ -56,7 +55,7 @@ final class HummingResultViewModel: @unchecked Sendable { let records = await mapRecords(displayableResult.records) let submit = await mapAnswer(displayableResult.submit) result = (answer, records, submit) - Logger.debug("updateCurrentResult에서 한번") + LogHandler.handleDebug("updateCurrentResult에서 한번") updateResultPhase() } } @@ -68,21 +67,21 @@ final class HummingResultViewModel: @unchecked Sendable { Task { switch resultPhase { case .answer: - Logger.debug("Answer Play") + LogHandler.handleDebug("Answer Play") resultPhase = .record(0) await startPlaying() case let .record(count): - Logger.debug("Record \(count) Play") + LogHandler.handleDebug("Record \(count) Play") if result.records.count - 1 == count { resultPhase = .submit } else { resultPhase = .record(count + 1) } await startPlaying() case .submit: - Logger.debug("Submit Play") + LogHandler.handleDebug("Submit Play") resultPhase = .none await startPlaying() if totalResult.isEmpty { canEndGame = true } case .none: - Logger.debug("None") + LogHandler.handleDebug("None") resultPhase = .answer await startPlaying() } @@ -92,9 +91,10 @@ final class HummingResultViewModel: @unchecked Sendable { func changeRecordOrder() async { do { let succeded = try await roomActionRepository.changeRecordOrder(roomNumber: roomNumber) - if !succeded { Logger.error("Changing RecordOrder failed") } + if !succeded { LogHandler.handleError("Changing RecordOrder failed") } } catch { - Logger.error(error.localizedDescription) + let error = ASErrors(type: .changeRecordOrder, reason: error.localizedDescription, file: #file, line: #line) + LogHandler.handleError(error) } } @@ -102,9 +102,10 @@ final class HummingResultViewModel: @unchecked Sendable { do { guard totalResult.isEmpty else { return } let succeded = try await roomActionRepository.resetGame() - if !succeded { Logger.error("Game Reset failed") } + if !succeded { LogHandler.handleError("Game Reset failed") } } catch { - Logger.error("Game Reset failed") + let error = ASErrors(type: .navigateToLobby, reason: error.localizedDescription, file: #file, line: #line) + LogHandler.handleError(error) } } @@ -128,7 +129,7 @@ final class HummingResultViewModel: @unchecked Sendable { for record in records { let recordData = await getRecordData(url: record.fileUrl) let recordAmplitudes = await AudioHelper.shared.analyze(with: recordData ?? Data()) - Logger.debug(recordAmplitudes) + LogHandler.handleDebug(recordAmplitudes) let playerName = record.player?.nickname let playerAvatarData = await getAvatarData(url: record.player?.avatarUrl) mappedRecords.append(MappedRecord(recordData, recordAmplitudes, playerName, playerAvatarData)) @@ -177,7 +178,7 @@ extension HummingResultViewModel { .dropFirst() .sink { [weak self] _, recordOrder in guard let self else { return } - Logger.debug("recordOrder changed", recordOrder) + LogHandler.handleDebug("recordOrder changed \(recordOrder)") updateCurrentResult() } .store(in: &cancellables) @@ -187,7 +188,7 @@ extension HummingResultViewModel { hummingResultRepository.getResult() .receive(on: DispatchQueue.main) .map { $0.sorted { $0.answer.player?.order ?? 0 < $1.answer.player?.order ?? 1 } } - .sink(receiveCompletion: { Logger.debug($0) }, + .sink(receiveCompletion: { LogHandler.handleDebug($0) }, receiveValue: { [weak self] sortedResult in guard let self, isValidResult(sortedResult) else { return } diff --git a/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/SelectMusic/SelectMusicViewModel.swift b/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/SelectMusic/SelectMusicViewModel.swift index 9649c69..edebf47 100644 --- a/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/SelectMusic/SelectMusicViewModel.swift +++ b/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/SelectMusic/SelectMusicViewModel.swift @@ -113,17 +113,25 @@ final class SelectMusicViewModel: ObservableObject, @unchecked Sendable { do { _ = try await answersRepository.submitMusic(answer: selectedMusic) } catch { + let error = ASErrors(type: .submitMusic, reason: error.localizedDescription, file: #file, line: #line) + LogHandler.handleError(error) throw error } } } func searchMusic(text: String) async throws { - if text.isEmpty { return } - await updateIsSearching(with: true) - let searchList = try await musicAPI.search(for: text) - await updateSearchList(with: searchList) - await updateIsSearching(with: false) + do { + if text.isEmpty { return } + await updateIsSearching(with: true) + let searchList = try await musicAPI.search(for: text) + await updateSearchList(with: searchList) + await updateIsSearching(with: false) + } catch { + let error = ASErrors(type: .searchMusicOnSelect, reason: error.localizedDescription, file: #file, line: #line) + LogHandler.handleError(error) + throw error + } } @MainActor @@ -131,6 +139,8 @@ final class SelectMusicViewModel: ObservableObject, @unchecked Sendable { do { selectedMusic = try await musicAPI.randomSong(from: "pl.u-aZb00o7uPlzMZzr") } catch { + let error = ASErrors(type: .randomMusic, reason: error.localizedDescription, file: #file, line: #line) + LogHandler.handleError(error) throw error } } diff --git a/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/SubmitAnswer/SubmitAnswerViewModel.swift b/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/SubmitAnswer/SubmitAnswerViewModel.swift index ecb20c9..cc16ac4 100644 --- a/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/SubmitAnswer/SubmitAnswerViewModel.swift +++ b/alsongDalsong/alsongDalsong/alsongDalsong/Sources/Views/SubmitAnswer/SubmitAnswerViewModel.swift @@ -136,6 +136,8 @@ final class SubmitAnswerViewModel: ObservableObject, @unchecked Sendable { await updateSearchList(with: searchList) await updateIsSearching(with: false) } catch { + let error = ASErrors(type: .searchMusicOnSubmit, reason: error.localizedDescription, file: #file, line: #line) + LogHandler.handleError(error) throw error } } @@ -153,9 +155,10 @@ final class SubmitAnswerViewModel: ObservableObject, @unchecked Sendable { func submitAnswer() async throws { guard let selectedMusic else { return } do { - let response = try await submitsRepository.submitAnswer(answer: selectedMusic) - + let _ = try await submitsRepository.submitAnswer(answer: selectedMusic) } catch { + let error = ASErrors(type: .submitAnswer, reason: error.localizedDescription, file: #file, line: #line) + LogHandler.handleError(error) throw error } }