diff --git a/HANE24.xcodeproj/project.pbxproj b/HANE24.xcodeproj/project.pbxproj index cef2964..0927a92 100644 --- a/HANE24.xcodeproj/project.pbxproj +++ b/HANE24.xcodeproj/project.pbxproj @@ -25,6 +25,8 @@ 0E9868252B2B6C0B00E127DC /* CalendarHeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E9868242B2B6C0B00E127DC /* CalendarHeaderView.swift */; }; 0E9868272B2B76BE00E127DC /* CalendarBodyView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0E9868262B2B76BE00E127DC /* CalendarBodyView.swift */; }; 0EBBF14E29B08D580076AAB9 /* ErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EBBF14D29B08D580076AAB9 /* ErrorView.swift */; }; + 0ECF97A62BEDE7CB00C37095 /* Error.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECF97A52BEDE7CB00C37095 /* Error.swift */; }; + 0ECF97A82BEDE8EA00C37095 /* ErrorHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ECF97A72BEDE8EA00C37095 /* ErrorHandler.swift */; }; 0ED6E3E32B354D320026E69D /* WidgetKit.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0ED6E3E22B354D310026E69D /* WidgetKit.framework */; }; 0ED6E3E52B354D320026E69D /* SwiftUI.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 0ED6E3E42B354D320026E69D /* SwiftUI.framework */; }; 0ED6E3E82B354D320026E69D /* HANE24WidgetBundle.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED6E3E72B354D320026E69D /* HANE24WidgetBundle.swift */; }; @@ -134,6 +136,8 @@ 0E9868242B2B6C0B00E127DC /* CalendarHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarHeaderView.swift; sourceTree = ""; }; 0E9868262B2B76BE00E127DC /* CalendarBodyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarBodyView.swift; sourceTree = ""; }; 0EBBF14D29B08D580076AAB9 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = ""; }; + 0ECF97A52BEDE7CB00C37095 /* Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = ""; }; + 0ECF97A72BEDE8EA00C37095 /* ErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandler.swift; sourceTree = ""; }; 0ED6E3E12B354D310026E69D /* HANE24WidgetExtension.appex */ = {isa = PBXFileReference; explicitFileType = "wrapper.app-extension"; includeInIndex = 0; path = HANE24WidgetExtension.appex; sourceTree = BUILT_PRODUCTS_DIR; }; 0ED6E3E22B354D310026E69D /* WidgetKit.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = WidgetKit.framework; path = System/Library/Frameworks/WidgetKit.framework; sourceTree = SDKROOT; }; 0ED6E3E42B354D320026E69D /* SwiftUI.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = SwiftUI.framework; path = System/Library/Frameworks/SwiftUI.framework; sourceTree = SDKROOT; }; @@ -360,6 +364,7 @@ isa = PBXGroup; children = ( 9AF730F5299E074800AF2E53 /* Structs.swift */, + 0ECF97A52BEDE7CB00C37095 /* Error.swift */, 9AF730FC299F602D00AF2E53 /* JSONs.swift */, D68CC3E72BCC1764008D62E2 /* ReissueModel.swift */, D6E780C82BCCE84300FB547D /* CalendarModel.swift */, @@ -375,6 +380,7 @@ D6C528F72BBECE0700F51A06 /* ReissueVM.swift */, D6185AE02BB3C71800E6944A /* CalendarVM.swift */, 0E3ED8A72BB13727001B0BAE /* NetworkManager.swift */, + 0ECF97A72BEDE8EA00C37095 /* ErrorHandler.swift */, 0EE06CBC2BB2819500B4988C /* HomeVM.swift */, 0E6B608E29AC850D009D8BC4 /* NetworkMonitoringManager.swift */, ); @@ -628,6 +634,7 @@ D6E780CE2BCD030200FB547D /* Tapbar.swift in Sources */, D68CC3F32BCC1F2A008D62E2 /* ListItemButton.swift in Sources */, 9AF730FD299F602D00AF2E53 /* JSONs.swift in Sources */, + 0ECF97A82BEDE8EA00C37095 /* ErrorHandler.swift in Sources */, D6E780D42BCD04AB00FB547D /* ReissueButton.swift in Sources */, D6AECA262BECCBF5009A8018 /* AccTimeCardsView.swift in Sources */, 0EE58193299CC24000EE3351 /* MoreView.swift in Sources */, @@ -670,6 +677,7 @@ D6C528F82BBECE0700F51A06 /* ReissueVM.swift in Sources */, D6E19CF32BEB427B005DF8C3 /* Error.swift in Sources */, 9AF730F3299D599300AF2E53 /* TagLogView.swift in Sources */, + 0ECF97A62BEDE7CB00C37095 /* Error.swift in Sources */, 0E0FE5F32BB3E6110050498E /* CircularProgressBar.swift in Sources */, ); runOnlyForDeploymentPostprocessing = 0; diff --git a/HANE24/ContentView.swift b/HANE24/ContentView.swift index 475d06e..67cf5fd 100644 --- a/HANE24/ContentView.swift +++ b/HANE24/ContentView.swift @@ -34,7 +34,6 @@ struct ContentView: View { UserDefaults.standard.setValue(0, forKey: "MonthlySelectionOption") UserDefaults.standard.set(true, forKey: "isFirst") } - do { try hane.isSignIn = await hane.isLogin() ? true : false self.signInChecked = true diff --git a/HANE24/Model/Error.swift b/HANE24/Model/Error.swift index 3a05196..047c60f 100644 --- a/HANE24/Model/Error.swift +++ b/HANE24/Model/Error.swift @@ -7,6 +7,71 @@ import Foundation +enum CustomError: Error { + case tokenExpired + case wrongQueryType + case networkDisconnected + case unAuthorized + case internalServer + case responseBodyEmpty + case decodeFailed + case invalidURL + case unknownError(String) + case none +} + +extension CustomError: LocalizedError { + public var errorDescription: String? { + switch self { + case .tokenExpired: + return "사용자 토큰이 만료되었습니다" + case .wrongQueryType: + return "잘못된 요청입니다" + case .networkDisconnected: + return "네트워크 상태가 원활하지 않습니다" + case .unAuthorized: + return "알 수 없는 사용자입니다" + case .internalServer: + return "서버 에러 발생" + case .responseBodyEmpty: + return "내부 에러 발생" + case .decodeFailed: + return "내부 에러 발생" + case .invalidURL: + return "잘못된 접근입니다" + case .unknownError: + return "원인을 알 수 없는 에러 발생" + case .none: + return nil + } + } + + public var recoverySuggestion: String? { + switch self { + case .tokenExpired: + return "다시 로그인해주세요" + case .wrongQueryType: + return "다시 시도해주세요" + case .networkDisconnected: + return "Wi-Fi 혹은 데이터 확인 후 다시 시도해주세요" + case .unAuthorized: + return "로그인 정보를 다시 확인해주세요" + case .internalServer: + return "개발팀에게 문의해주세요" + case .responseBodyEmpty: + return "개발팀에게 문의해주세요" + case .decodeFailed: + return "개발팀에게 문의해주세요" + case .invalidURL: + return "개발팀에게 문의해주세요" + case .unknownError: + return "개발팀에게 문의해주세요" + case .none: + return nil + } + } +} + // API 처리에 대한 실패 enum ReissueError: Error { case tokenExpired(String) diff --git a/HANE24/View/MainView.swift b/HANE24/View/MainView.swift index 14a97e0..5aa61ed 100644 --- a/HANE24/View/MainView.swift +++ b/HANE24/View/MainView.swift @@ -10,6 +10,7 @@ import SwiftUI struct MainView: View { @EnvironmentObject var hane: Hane @StateObject var homeVM = HomeVM() + @ObservedObject var errorHandler = ErrorHandler.shared @State var selection = 1 @Environment(\.colorScheme) var colorScheme @@ -41,6 +42,7 @@ struct MainView: View { try await homeVM.refresh() } catch { print("error on MainView \(error.localizedDescription)") + print("error: ", error) } } @@ -50,6 +52,15 @@ struct MainView: View { NoticeView(showNotice: $isNoticedTagLatencyInfo, notice: hane.tagLatencyNotice) } } + .alert( + "에러가 발생했어요", + isPresented: $errorHandler.showAlert) { + Button("확인") { + errorHandler.errorType = CustomError.none + } + } message: { + Text(errorHandler.errorType.recoverySuggestion ?? "개발팀에 문의해주세요") + } } } diff --git a/HANE24/ViewModel/ErrorHandler.swift b/HANE24/ViewModel/ErrorHandler.swift new file mode 100644 index 0000000..d2b472b --- /dev/null +++ b/HANE24/ViewModel/ErrorHandler.swift @@ -0,0 +1,71 @@ +// +// ErrorHandler.swift +// 24HANE +// +// Created by Katherine JANG on 5/10/24. +// + +import Foundation +import SwiftUI + + +class ErrorHandler: ObservableObject { + + static let shared = ErrorHandler() + + var errorType: CustomError = .none + @Published var showAlert: Bool = false + @Published var signInRequired: Bool = false + + private init() { } + + func errorFromHttpRequest(_ statusCode: Int?) throws { + switch statusCode { + case 400: + throw CustomError.wrongQueryType + case 401: + throw CustomError.unAuthorized + case 500: + throw CustomError.internalServer + default: + throw CustomError.unknownError("\(statusCode)") + } + } + + @MainActor + func updateErrorView() { + switch self.errorType { + case .tokenExpired, .unAuthorized: + self.signInRequired = true + case .wrongQueryType, .networkDisconnected, .internalServer, .responseBodyEmpty, + .decodeFailed, .unknownError, .invalidURL: + self.showAlert = true + case .none: + break + } + } + + func verifyError(_ error: Error) async { + switch error { + case DecodingError.dataCorrupted: + self.errorType = .decodeFailed + case URLError.timedOut: + self.errorType = .networkDisconnected + case URLError.networkConnectionLost: + self.errorType = .networkDisconnected + case is CustomError: + self.errorType = error as? CustomError ?? .none + default: + self.errorType = .unknownError(error.localizedDescription.description) + } + } + + @MainActor + func handleError(_ error: Error) { + Task { + await self.verifyError(error) + self.updateErrorView() + } + } + + } diff --git a/HANE24/ViewModel/HomeVM.swift b/HANE24/ViewModel/HomeVM.swift index 4cc8b79..912db1c 100644 --- a/HANE24/ViewModel/HomeVM.swift +++ b/HANE24/ViewModel/HomeVM.swift @@ -49,29 +49,38 @@ class HomeVM: ObservableObject { } @MainActor - func updateAccumulationTimes() async throws { - guard let accTimes = try await NetworkManager.shared.apiRequest("/v3/tag-log/accumulationTimes", .get, type: AccumulationTimes.self) else { + func updateAccumulationTimes() async { + do { + guard let accTimes = try await NetworkManager.shared.apiRequest("/v3/tag-log/accumulationTimes", .get, type: AccumulationTimes.self) else { throw MyError.tokenExpired("") + } + self.accumulationTimes = accTimes + self.dailyAccumulationTime = accTimes.todayAccumulationTime + } catch { + ErrorHandler.shared.handleError(error) } - self.accumulationTimes = accTimes - self.dailyAccumulationTime = accTimes.todayAccumulationTime } //TODO: 요청한 데이터가 nil일 경우 에러 핸들링 @MainActor - func updateMainInfo() async throws { - guard let mainInfo = try await NetworkManager.shared.apiRequest("/v3/tag-log/maininfo", .get, type: MainInfo.self) else { - throw MyError.tokenExpired("") - } - self.mainInfo = mainInfo - self.isInCluster = mainInfo.inoutState == "IN" - self.fundInfoNotice = mainInfo.infoMessages.fundInfoNotice - self.tagLatencyNotice = mainInfo.infoMessages.tagLatencyNotice + func updateMainInfo() async { + do { + guard let mainInfo = try await NetworkManager.shared.apiRequest("/v3/tag-log/maininfo", .get, type: MainInfo.self) else { + throw MyError.tokenExpired("") + } + self.mainInfo = mainInfo + self.isInCluster = mainInfo.inoutState == "IN" + self.fundInfoNotice = mainInfo.infoMessages.fundInfoNotice + self.tagLatencyNotice = mainInfo.infoMessages.tagLatencyNotice + } catch { + print("error caught") + ErrorHandler.shared.handleError(error) + } } @MainActor func refresh() async throws { - try await self.updateMainInfo() - try await self.updateAccumulationTimes() + await self.updateMainInfo() + await self.updateAccumulationTimes() } }