Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FEAT: 상황에 따른 에러 핸들링 #16

Merged
merged 10 commits into from
Jun 7, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 10 additions & 2 deletions HANE24.xcodeproj/project.pbxproj
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,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 */; };
Expand All @@ -40,9 +42,9 @@
0ED6E3FC2B354DFC0026E69D /* HANE24WidgetConstant.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED6E3FA2B354DFC0026E69D /* HANE24WidgetConstant.swift */; };
0ED6E3FF2B354E670026E69D /* AccumulationTimeStruct.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED6E3FE2B354E670026E69D /* AccumulationTimeStruct.swift */; };
0ED6E4002B354E670026E69D /* AccumulationTimeStruct.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0ED6E3FE2B354E670026E69D /* AccumulationTimeStruct.swift */; };
0EE06CBD2BB2819500B4988C /* HomeVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE06CBC2BB2819500B4988C /* HomeVM.swift */; };
0EE06CBF2BB2AD4C00B4988C /* HeaderView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE06CBE2BB2AD4C00B4988C /* HeaderView.swift */; };
0EE06CC12BB2B2EB00B4988C /* PullToRefresh.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE06CC02BB2B2EB00B4988C /* PullToRefresh.swift */; };
0EE06CBD2BB2819500B4988C /* HomeVM.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE06CBC2BB2819500B4988C /* HomeVM.swift */; };
0EE58193299CC24000EE3351 /* MoreView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE58192299CC24000EE3351 /* MoreView.swift */; };
0EE58198299CC74C00EE3351 /* ReissuanceView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EE58197299CC74C00EE3351 /* ReissuanceView.swift */; };
0EEB0E2429AE2AF700FEB700 /* CardProgressView.swift in Sources */ = {isa = PBXBuildFile; fileRef = 0EEB0E2329AE2AF700FEB700 /* CardProgressView.swift */; };
Expand Down Expand Up @@ -118,6 +120,8 @@
0E9868242B2B6C0B00E127DC /* CalendarHeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarHeaderView.swift; sourceTree = "<group>"; };
0E9868262B2B76BE00E127DC /* CalendarBodyView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CalendarBodyView.swift; sourceTree = "<group>"; };
0EBBF14D29B08D580076AAB9 /* ErrorView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorView.swift; sourceTree = "<group>"; };
0ECF97A52BEDE7CB00C37095 /* Error.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = Error.swift; sourceTree = "<group>"; };
0ECF97A72BEDE8EA00C37095 /* ErrorHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ErrorHandler.swift; sourceTree = "<group>"; };
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; };
Expand All @@ -130,9 +134,9 @@
0ED6E3FE2B354E670026E69D /* AccumulationTimeStruct.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AccumulationTimeStruct.swift; sourceTree = "<group>"; };
0ED6E4032B3559B80026E69D /* 24HANE.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = 24HANE.entitlements; sourceTree = "<group>"; };
0ED6E4042B3559DF0026E69D /* HANE24WidgetExtension.entitlements */ = {isa = PBXFileReference; lastKnownFileType = text.plist.entitlements; path = HANE24WidgetExtension.entitlements; sourceTree = "<group>"; };
0EE06CBC2BB2819500B4988C /* HomeVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeVM.swift; sourceTree = "<group>"; };
0EE06CBE2BB2AD4C00B4988C /* HeaderView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HeaderView.swift; sourceTree = "<group>"; };
0EE06CC02BB2B2EB00B4988C /* PullToRefresh.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PullToRefresh.swift; sourceTree = "<group>"; };
0EE06CBC2BB2819500B4988C /* HomeVM.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeVM.swift; sourceTree = "<group>"; };
0EE58192299CC24000EE3351 /* MoreView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MoreView.swift; sourceTree = "<group>"; };
0EE58197299CC74C00EE3351 /* ReissuanceView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReissuanceView.swift; sourceTree = "<group>"; };
0EEB0E2329AE2AF700FEB700 /* CardProgressView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CardProgressView.swift; sourceTree = "<group>"; };
Expand Down Expand Up @@ -324,6 +328,7 @@
isa = PBXGroup;
children = (
9AF730F5299E074800AF2E53 /* Structs.swift */,
0ECF97A52BEDE7CB00C37095 /* Error.swift */,
9AF730FC299F602D00AF2E53 /* JSONs.swift */,
);
path = Model;
Expand All @@ -335,6 +340,7 @@
9AF730FA299F53C700AF2E53 /* HaneVM.swift */,
D6185AE02BB3C71800E6944A /* CalendarVM.swift */,
0E3ED8A72BB13727001B0BAE /* NetworkManager.swift */,
0ECF97A72BEDE8EA00C37095 /* ErrorHandler.swift */,
0EE06CBC2BB2819500B4988C /* HomeVM.swift */,
0E6B608E29AC850D009D8BC4 /* NetworkMonitoringManager.swift */,
);
Expand Down Expand Up @@ -552,6 +558,7 @@
9AF730F1299D58F100AF2E53 /* AccTimeCardForCalendarView.swift in Sources */,
2B1326112B2821E600301A8B /* ThisMonthAccTimeCardView.swift in Sources */,
9AF730FD299F602D00AF2E53 /* JSONs.swift in Sources */,
0ECF97A82BEDE8EA00C37095 /* ErrorHandler.swift in Sources */,
0EE58193299CC24000EE3351 /* MoreView.swift in Sources */,
0E16550E299A5887001E5EED /* MainView.swift in Sources */,
0EFAF5BE29A4D97800125948 /* LoadingView.swift in Sources */,
Expand Down Expand Up @@ -584,6 +591,7 @@
0E1654F3299A285B001E5EED /* HANE24App.swift in Sources */,
0EEB0E2429AE2AF700FEB700 /* CardProgressView.swift in Sources */,
9AF730F3299D599300AF2E53 /* TagLogView.swift in Sources */,
0ECF97A62BEDE7CB00C37095 /* Error.swift in Sources */,
0E0FE5F32BB3E6110050498E /* CircularProgressBar.swift in Sources */,
);
runOnlyForDeploymentPostprocessing = 0;
Expand Down
1 change: 0 additions & 1 deletion HANE24/ContentView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
74 changes: 74 additions & 0 deletions HANE24/Model/Error.swift
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

만약 좀 더 발전시킨다면 에러가 일어나면 개발진 슬랙이나 이메일, 구글 스프레드에 바로 올려버리는 것도 좋아보이네요

Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
//
// Error.swift
// 24HANE
//
// Created by Katherine JANG on 5/10/24.
//

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
}
}
}

13 changes: 12 additions & 1 deletion HANE24/View/MainView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@ struct MainView: View {
@EnvironmentObject var hane: Hane
@EnvironmentObject var calendar: CalendarVM
@StateObject var homeVM = HomeVM()
@ObservedObject var errorHandler = ErrorHandler.shared

@State var selection = 1
@Environment(\.colorScheme) var colorScheme
Expand Down Expand Up @@ -39,11 +40,12 @@ struct MainView: View {
.accentColor(Theme.toolBarIconColor(forScheme: colorScheme))
.task {
do {
try await hane.refresh()
// try await hane.refresh()
try await homeVM.refresh()
try await calendar.updateMonthlyLogs(date: .now)
} catch {
print("error on MainView \(error.localizedDescription)")
print("error: ", error)
}
}

Expand All @@ -53,6 +55,15 @@ struct MainView: View {
NoticeView(showNotice: $isNoticedTagLatencyInfo, notice: hane.tagLatencyNotice)
}
}
.alert(
"에러가 발생했어요",
isPresented: $errorHandler.showAlert) {
Button("확인") {
errorHandler.errorType = CustomError.none
}
} message: {
Text(errorHandler.errorType.recoverySuggestion ?? "개발팀에 문의해주세요")
}
}
}

Expand Down
72 changes: 72 additions & 0 deletions HANE24/ViewModel/ErrorHandler.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
//
// 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
var occurredError: Error? = nil
@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)
}
}
Comment on lines +48 to +61
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

이 코드가 클라이언트 에러를 구분하는 책임을 맡은 만큼 함수 이름에서도 errorFromHttpRequest처럼 그 부분이 보였으면 좋겠습니다.


@MainActor
func handleError(_ error: Error) {
Task {
await self.verifyError(error)
self.updateErrorView()
}
}

}
39 changes: 24 additions & 15 deletions HANE24/ViewModel/HomeVM.swift
Original file line number Diff line number Diff line change
Expand Up @@ -49,29 +49,38 @@ class HomeVM: ObservableObject {
}

@MainActor
func updateAccumulationTimes() async throws {
guard let accTimes = try await NetworkManager.shared.getRequest("/v3/tag-log/accumulationTimes", type: AccumulationTimes.self) else {
throw MyError.tokenExpired("")
func updateAccumulationTimes() async {
do {
guard let accTimes = try await NetworkManager.shared.getRequest("/v3/tag-log/accumulationTimes", type: AccumulationTimes.self) else {
throw CustomError.responseBodyEmpty
}
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.getRequest("/v3/tag-log/maininfo", 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.getRequest("/v3/tag-log/maininfo", 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()
}
}
12 changes: 6 additions & 6 deletions HANE24/ViewModel/NetworkManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -31,23 +31,23 @@ class NetworkManager: NetworkProtocol {

func getRequest<T>(_ urlPath: String, type: T.Type) async throws -> T? where T : Decodable {
guard let url = URL(string: apiRoot + urlPath) else {
/// FIXME: invalid URL의 경우 error handling
return nil
throw CustomError.invalidURL
}

guard let token = UserDefaults.standard.string(forKey: "Token") else {
/// FIXME: token invalid 경우에 signIn 상태 변경
throw MyError.tokenExpired("get new token!")
throw CustomError.tokenExpired
}

var request = URLRequest(url: url)
request.httpMethod = "GET"
request.allHTTPHeaderFields = [
"Authorization": "Bearer \(String(describing: token) )"]
let (data, response) = try await session.data(for: request)
guard (response as? HTTPURLResponse)?.statusCode == 200 else {
/// FIXME: Status Code에 따른 Error Handling
throw MyError.tokenExpired("request Failed")
let statusCode = (response as? HTTPURLResponse)?.statusCode
guard statusCode == 200 else {
try ErrorHandler.shared.errorFromHttpRequest(statusCode)
throw CustomError.unknownError("\(statusCode)")
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

요 부분이 이해가 잘 안갑니다. 코드가 200이 아닌 경우 그 상태코드를 들고 다시 errorFormHttpRequest를 이용해서 커스텀 에러에 속해있는지를 파악하는 거라 생각합니다. 이 부분에서 catch를 사용하지 않는 이유가 궁금합니다.

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

아뇨,errorFormHttpRequest를 이용해서 커스텀 에러에 속해있는지를 파악하는 것이 아닌 errorFormHttpRequest를 이용하여서 statuscode에 따라 발생한 에러를 custom 에러로 변환하는 과정입니다..! catch 후 에러를 핸들링 하는 과정을 한 곳에서 진행하는게 통일성 있을 듯 하여(요청한 vm class 내의 메서드에서) 해당 부분에서는 catch 하지 않고 다시 throw 후, 상위 메서드에서 catch 후 핸들링 하는 방식을 선택하였습니다. 만약 해당 request 메서드 내에서 catch 를 하게 되면, 상위 request를 호출한 메서드 내에서는 api request 시도에 실패하였다는 것을 알기 어려울 것 같습니다. 그렇게 될 경우, 요청 실패로인해 class내의 프로퍼티를 업데이트 하지 않아도 되는 경우에도 업데이트가 진행되는 등의 문제가 발생할 것 같습니다! 이와 같은이유로 다시 throw하게 처리하였는데 에러를 어디서 catch 하고 핸들링 하면 좋을지에 대해서는 추가적으로 고민해보겠습니다!

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

의도: 최상단으로 throw를 하기 위한 방법

의문: 책임의 소재

}
let decodedData = try JSONDecoder().decode(type.self, from: data)
return decodedData
Expand Down