From 3bd0096d24672f0b55758dc61e8dcb5a3d4429db Mon Sep 17 00:00:00 2001 From: shinhong_park Date: Mon, 16 Jan 2023 12:57:45 +0900 Subject: [PATCH] =?UTF-8?q?=EC=9D=B4=EB=A9=94=EC=9D=BC=20=EB=AF=B8?= =?UTF-8?q?=EC=9D=B8=EC=A6=9D=EC=8B=9C=20CTA=20=EA=B0=9C=EC=84=A0=20(#207)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * improve error handling logic, add confirm action when email not veritifed * Apply SwiftFormat changes * improve logout logic * Apply SwiftFormat changes Co-authored-by: shp7724 --- .../SNUTT/AppState/States/ReviewState.swift | 5 +-- .../SNUTT/AppState/States/SystemState.swift | 2 + SNUTT-2022/SNUTT/Models/ErrorCode.swift | 5 ++- SNUTT-2022/SNUTT/Services/AuthService.swift | 2 + .../SNUTT/Services/GlobalUIService.swift | 10 +++++ .../ViewModels/LectureDetailViewModel.swift | 2 + .../ViewModels/SearchSceneViewModel.swift | 14 +++++++ .../SNUTT/ViewModels/TimetableViewModel.swift | 4 -- .../Components/Search/SearchLectureCell.swift | 2 +- SNUTT-2022/SNUTT/Views/SNUTTView.swift | 38 ++++++++++++++----- .../Views/Scenes/LectureDetailScene.swift | 2 +- .../SNUTT/Views/Scenes/ReviewScene.swift | 12 +++--- .../Views/Scenes/SearchLectureScene.swift | 11 ++++++ .../SNUTT/Views/Scenes/TimetableScene.swift | 2 - 14 files changed, 83 insertions(+), 28 deletions(-) diff --git a/SNUTT-2022/SNUTT/AppState/States/ReviewState.swift b/SNUTT-2022/SNUTT/AppState/States/ReviewState.swift index 169b461d..0f031614 100644 --- a/SNUTT-2022/SNUTT/AppState/States/ReviewState.swift +++ b/SNUTT-2022/SNUTT/AppState/States/ReviewState.swift @@ -22,11 +22,8 @@ class WebViewPreloadManager { private var bag = Set() func preload(url: URL, accessToken: String) { - if eventSignal != nil && webView != nil { - return - } eventSignal = eventSignal ?? .init() - webView = webView ?? WKWebView(cookies: NetworkConfiguration.getCookiesFrom(accessToken: accessToken)) + webView = WKWebView(cookies: NetworkConfiguration.getCookiesFrom(accessToken: accessToken)) coordinator = coordinator ?? Coordinator(eventSignal: eventSignal!) webView?.scrollView.bounces = false webView?.backgroundColor = UIColor(STColor.systemBackground) diff --git a/SNUTT-2022/SNUTT/AppState/States/SystemState.swift b/SNUTT-2022/SNUTT/AppState/States/SystemState.swift index bde105eb..1be821d1 100644 --- a/SNUTT-2022/SNUTT/AppState/States/SystemState.swift +++ b/SNUTT-2022/SNUTT/AppState/States/SystemState.swift @@ -13,4 +13,6 @@ class SystemState: ObservableObject { @Published var isErrorAlertPresented = false @Published var error: STError? = nil @Published var preferredColorScheme: ColorScheme? = nil + + @Published var selectedTab: TabType = .timetable } diff --git a/SNUTT-2022/SNUTT/Models/ErrorCode.swift b/SNUTT-2022/SNUTT/Models/ErrorCode.swift index de28b849..c54402c2 100644 --- a/SNUTT-2022/SNUTT/Models/ErrorCode.swift +++ b/SNUTT-2022/SNUTT/Models/ErrorCode.swift @@ -116,9 +116,10 @@ enum ErrorCode: Int { .NO_EMAIL, .NO_PASSWORD_RESET_REQUEST, .CANT_CHANGE_OTHERS_THEME, - .EMAIL_NOT_VERIFIED, .EXPIRED_PASSWORD_RESET_CODE: return "요청 실패" + case .EMAIL_NOT_VERIFIED: + return "인증 필요" case .NO_USER_TOKEN, .WRONG_API_KEY, .WRONG_USER_TOKEN, @@ -276,7 +277,7 @@ enum ErrorCode: Int { case .WRONG_APPLE_TOKEN: return "애플 계정으로 로그인하지 못했습니다." case .EMAIL_NOT_VERIFIED: - return "인증되지 않은 이메일입니다. 강의평 탭에서 이메일 인증을 먼저 진행해주세요." + return "강의평 확인을 위해 이메일 인증이 필요합니다. 이메일 인증을 진행하시겠습니까?" case .NO_PASSWORD_RESET_REQUEST: return "비밀번호 재설정을 다시 시도해주세요." case .EXPIRED_PASSWORD_RESET_CODE: diff --git a/SNUTT-2022/SNUTT/Services/AuthService.swift b/SNUTT-2022/SNUTT/Services/AuthService.swift index 10ab2a31..f36e7d7e 100644 --- a/SNUTT-2022/SNUTT/Services/AuthService.swift +++ b/SNUTT-2022/SNUTT/Services/AuthService.swift @@ -127,7 +127,9 @@ extension UserAuthHandler { appState.user.accessToken = nil appState.user.userId = nil appState.user.current = nil + appState.timetable.current = nil } + localRepositories.userDefaultsRepository.set(TimetableDto.self, key: .currentTimetable, value: nil) localRepositories.userDefaultsRepository.set(String.self, key: .accessToken, value: nil) localRepositories.userDefaultsRepository.set(String.self, key: .userId, value: nil) localRepositories.userDefaultsRepository.set(UserDto.self, key: .userDto, value: nil) diff --git a/SNUTT-2022/SNUTT/Services/GlobalUIService.swift b/SNUTT-2022/SNUTT/Services/GlobalUIService.swift index 874bc329..f3dbbe97 100644 --- a/SNUTT-2022/SNUTT/Services/GlobalUIService.swift +++ b/SNUTT-2022/SNUTT/Services/GlobalUIService.swift @@ -12,6 +12,8 @@ protocol GlobalUIServiceProtocol { func setColorScheme(_ colorScheme: ColorScheme?) func loadColorSchemeDuringBootstrap() + func setSelectedTab(_ tab: TabType) + func setIsErrorAlertPresented(_ value: Bool) func setIsMenuOpen(_ value: Bool) func openEllipsis(for timetable: TimetableMetadata) @@ -58,6 +60,14 @@ struct GlobalUIService: GlobalUIServiceProtocol, UserAuthHandler { appState.system.preferredColorScheme = colorScheme } + func setSelectedTab(_ tab: TabType) { + appState.system.selectedTab = tab + } + + func setIsErrorAlertPresented(_ value: Bool) { + appState.system.isErrorAlertPresented = value + } + func setIsMenuOpen(_ value: Bool) { DispatchQueue.main.async { appState.menu.isOpen = value diff --git a/SNUTT-2022/SNUTT/ViewModels/LectureDetailViewModel.swift b/SNUTT-2022/SNUTT/ViewModels/LectureDetailViewModel.swift index 4e5b8e43..6329978d 100644 --- a/SNUTT-2022/SNUTT/ViewModels/LectureDetailViewModel.swift +++ b/SNUTT-2022/SNUTT/ViewModels/LectureDetailViewModel.swift @@ -101,6 +101,8 @@ extension LectureDetailScene { func fetchReviewId(of lecture: Lecture) async -> String? { do { return try await lectureService.fetchReviewId(courseNumber: lecture.courseNumber, instructor: lecture.instructor) + } catch let error as STError where error.code == .EMAIL_NOT_VERIFIED { + // noop } catch { services.globalUIService.presentErrorAlert(error: error) } diff --git a/SNUTT-2022/SNUTT/ViewModels/SearchSceneViewModel.swift b/SNUTT-2022/SNUTT/ViewModels/SearchSceneViewModel.swift index 74cee090..46065ed0 100644 --- a/SNUTT-2022/SNUTT/ViewModels/SearchSceneViewModel.swift +++ b/SNUTT-2022/SNUTT/ViewModels/SearchSceneViewModel.swift @@ -14,10 +14,15 @@ class SearchSceneViewModel: BaseViewModel, ObservableObject { @Published private var _selectedLecture: Lecture? @Published private var _searchText: String = "" @Published private var _isFilterOpen: Bool = false + @Published private var _selectedTab: TabType = .review @Published var searchResult: [Lecture]? = nil @Published var selectedTagList: [SearchTag] = [] @Published var isLoading: Bool = false @Published var isLectureOverlapped: Bool = false + + @Published var emailVerifyError: STError? = nil + @Published var presentEmailVerifyAlert = false + var errorTitle: String = "" var errorMessage: String = "" @@ -36,6 +41,11 @@ class SearchSceneViewModel: BaseViewModel, ObservableObject { set { services.searchService.setSelectedLecture(newValue) } } + var selectedTab: TabType { + get { _selectedTab } + set { services.globalUIService.setSelectedTab(newValue) } + } + var currentTimetableWithSelection: Timetable? { _currentTimetable?.withSelectedLecture(_selectedLecture) } @@ -50,6 +60,7 @@ class SearchSceneViewModel: BaseViewModel, ObservableObject { appState.timetable.$current.assign(to: &$_currentTimetable) appState.timetable.$configuration.assign(to: &$_timetableConfig) appState.search.$selectedLecture.assign(to: &$_selectedLecture) + appState.system.$selectedTab.assign(to: &$_selectedTab) appState.search.$searchText.assign(to: &$_searchText) appState.search.$isFilterOpen.assign(to: &$_isFilterOpen) appState.search.$searchResult.assign(to: &$searchResult) @@ -128,6 +139,9 @@ class SearchSceneViewModel: BaseViewModel, ObservableObject { func fetchReviewId(of lecture: Lecture) async -> String? { do { return try await services.lectureService.fetchReviewId(courseNumber: lecture.courseNumber, instructor: lecture.instructor) + } catch let error as STError where error.code == .EMAIL_NOT_VERIFIED { + emailVerifyError = error + presentEmailVerifyAlert = true } catch { services.globalUIService.presentErrorAlert(error: error) } diff --git a/SNUTT-2022/SNUTT/ViewModels/TimetableViewModel.swift b/SNUTT-2022/SNUTT/ViewModels/TimetableViewModel.swift index cb947c50..cb7247a7 100644 --- a/SNUTT-2022/SNUTT/ViewModels/TimetableViewModel.swift +++ b/SNUTT-2022/SNUTT/ViewModels/TimetableViewModel.swift @@ -108,8 +108,4 @@ class TimetableViewModel: BaseViewModel, ObservableObject { private var timetableState: TimetableState { appState.timetable } - - func preloadWebViews() { - services.globalUIService.preloadWebViews() - } } diff --git a/SNUTT-2022/SNUTT/Views/Components/Search/SearchLectureCell.swift b/SNUTT-2022/SNUTT/Views/Components/Search/SearchLectureCell.swift index 8867238a..ebfc9688 100644 --- a/SNUTT-2022/SNUTT/Views/Components/Search/SearchLectureCell.swift +++ b/SNUTT-2022/SNUTT/Views/Components/Search/SearchLectureCell.swift @@ -95,7 +95,7 @@ struct SearchLectureCell: View { /// This `sheet` modifier should be called on `HStack` to prevent animation glitch when `dismiss`ed. .sheet(isPresented: $showReviewWebView) { if let container = container { - ReviewScene(viewModel: .init(container: container), detailId: $reviewId) + ReviewScene(viewModel: .init(container: container), isMainWebView: false, detailId: $reviewId) .id(reviewId) } } diff --git a/SNUTT-2022/SNUTT/Views/SNUTTView.swift b/SNUTT-2022/SNUTT/Views/SNUTTView.swift index c4640765..40fcadf0 100644 --- a/SNUTT-2022/SNUTT/Views/SNUTTView.swift +++ b/SNUTT-2022/SNUTT/Views/SNUTTView.swift @@ -10,20 +10,19 @@ import SwiftUI struct SNUTTView: View { @ObservedObject var viewModel: ViewModel - @State private var selectedTab: TabType = .timetable /// Required to synchronize between two navigation bar heights: `TimetableScene` and `SearchLectureScene`. @State private var navigationBarHeight: CGFloat = 0 private var selected: Binding { Binding { - selectedTab + viewModel.selectedTab } set: { - [previous = selectedTab] current in + [previous = viewModel.selectedTab] current in if previous == current, current == .review { viewModel.reloadReviewWebView() } - selectedTab = current + viewModel.selectedTab = current } } @@ -48,17 +47,22 @@ struct SNUTTView: View { SearchLectureScene(viewModel: .init(container: viewModel.container), navigationBarHeight: navigationBarHeight) } TabScene(tabType: .review) { - ReviewScene(viewModel: .init(container: viewModel.container)) + ReviewScene(viewModel: .init(container: viewModel.container), isMainWebView: true) } TabScene(tabType: .settings) { SettingScene(viewModel: .init(container: viewModel.container)) } } - if selectedTab == .timetable { + .onAppear { + viewModel.selectedTab = .timetable + viewModel.preloadWebViews() + } + + if viewModel.selectedTab == .timetable { MenuSheetScene(viewModel: .init(container: viewModel.container)) LectureTimeSheetScene(viewModel: .init(container: viewModel.container)) } - if selectedTab == .search { + if viewModel.selectedTab == .search { FilterSheetScene(viewModel: .init(container: viewModel.container)) } PopupScene(viewModel: .init(container: viewModel.container)) @@ -97,11 +101,22 @@ struct SNUTTView: View { extension SNUTTView { class ViewModel: BaseViewModel, ObservableObject { - @Published var isErrorAlertPresented = false @Published var accessToken: String? = nil @Published var preferredColorScheme: ColorScheme? = nil @Published private var error: STError? = nil + @Published private var _isErrorAlertPresented = false + var isErrorAlertPresented: Bool { + get { _isErrorAlertPresented } + set { services.globalUIService.setIsErrorAlertPresented(newValue) } + } + + @Published private var _selectedTab: TabType = .review + var selectedTab: TabType { + get { _selectedTab } + set { services.globalUIService.setSelectedTab(newValue) } + } + var isAuthenticated: Bool { guard let accessToken = accessToken else { return false } return !accessToken.isEmpty @@ -110,9 +125,10 @@ extension SNUTTView { override init(container: DIContainer) { super.init(container: container) appState.system.$error.assign(to: &$error) - appState.system.$isErrorAlertPresented.assign(to: &$isErrorAlertPresented) + appState.system.$isErrorAlertPresented.assign(to: &$_isErrorAlertPresented) appState.user.$accessToken.assign(to: &$accessToken) appState.system.$preferredColorScheme.assign(to: &$preferredColorScheme) + appState.system.$selectedTab.assign(to: &$_selectedTab) } var errorTitle: String { @@ -126,6 +142,10 @@ extension SNUTTView { func reloadReviewWebView() { services.globalUIService.sendMainWebViewReloadSignal() } + + func preloadWebViews() { + services.globalUIService.preloadWebViews() + } } } diff --git a/SNUTT-2022/SNUTT/Views/Scenes/LectureDetailScene.swift b/SNUTT-2022/SNUTT/Views/Scenes/LectureDetailScene.swift index 55f6cb70..1daea9fc 100644 --- a/SNUTT-2022/SNUTT/Views/Scenes/LectureDetailScene.swift +++ b/SNUTT-2022/SNUTT/Views/Scenes/LectureDetailScene.swift @@ -207,7 +207,7 @@ struct LectureDetailScene: View { viewModel.reloadDetailWebView(detailId: reviewId) } .sheet(isPresented: $showReviewWebView) { - ReviewScene(viewModel: .init(container: viewModel.container), detailId: $reviewId) + ReviewScene(viewModel: .init(container: viewModel.container), isMainWebView: false, detailId: $reviewId) .id(colorScheme) .id(reviewId) } diff --git a/SNUTT-2022/SNUTT/Views/Scenes/ReviewScene.swift b/SNUTT-2022/SNUTT/Views/Scenes/ReviewScene.swift index e8dcde0a..e4bc4e6b 100644 --- a/SNUTT-2022/SNUTT/Views/Scenes/ReviewScene.swift +++ b/SNUTT-2022/SNUTT/Views/Scenes/ReviewScene.swift @@ -11,17 +11,19 @@ import SwiftUI struct ReviewScene: View { @ObservedObject var viewModel: ViewModel @Binding var detailId: String? + private var isMainWebView: Bool @Environment(\.colorScheme) var colorScheme @Environment(\.dismiss) var dismiss - init(viewModel: ViewModel, detailId: Binding = .constant(nil)) { + init(viewModel: ViewModel, isMainWebView: Bool, detailId: Binding = .constant(nil)) { self.viewModel = viewModel _detailId = detailId + self.isMainWebView = isMainWebView } private var eventSignal: PassthroughSubject? { - viewModel.getPreloadedWebView(detailId: detailId).eventSignal + viewModel.getPreloadedWebView(isMain: isMainWebView).eventSignal } private var reviewUrl: URL { @@ -34,7 +36,7 @@ struct ReviewScene: View { var body: some View { ZStack { - ReviewWebView(preloadedWebView: viewModel.getPreloadedWebView(detailId: detailId)) + ReviewWebView(preloadedWebView: viewModel.getPreloadedWebView(isMain: isMainWebView)) .navigationBarHidden(true) .edgesIgnoringSafeArea(.bottom) .background(STColor.systemBackground) @@ -92,8 +94,8 @@ extension ReviewScene { }.store(in: &bag) } - func getPreloadedWebView(detailId: String?) -> WebViewPreloadManager { - if detailId == nil { + func getPreloadedWebView(isMain: Bool) -> WebViewPreloadManager { + if isMain { return appState.review.preloadedMain } else { return appState.review.preloadedDetail diff --git a/SNUTT-2022/SNUTT/Views/Scenes/SearchLectureScene.swift b/SNUTT-2022/SNUTT/Views/Scenes/SearchLectureScene.swift index 4eb91d83..0297da0e 100644 --- a/SNUTT-2022/SNUTT/Views/Scenes/SearchLectureScene.swift +++ b/SNUTT-2022/SNUTT/Views/Scenes/SearchLectureScene.swift @@ -82,6 +82,17 @@ struct SearchLectureScene: View { .task { await viewModel.fetchTags() } + .alert(viewModel.emailVerifyError?.title ?? "", isPresented: $viewModel.presentEmailVerifyAlert, actions: { + Button("확인") { + viewModel.emailVerifyError = nil + viewModel.selectedTab = .review + } + Button("취소", role: .cancel) { + viewModel.emailVerifyError = nil + } + }, message: { + Text(viewModel.emailVerifyError?.content ?? "") + }) .navigationBarHidden(true) .animation(.customSpring, value: viewModel.searchResult?.count) .animation(.customSpring, value: viewModel.isLoading) diff --git a/SNUTT-2022/SNUTT/Views/Scenes/TimetableScene.swift b/SNUTT-2022/SNUTT/Views/Scenes/TimetableScene.swift index 559cebbd..6943e124 100644 --- a/SNUTT-2022/SNUTT/Views/Scenes/TimetableScene.swift +++ b/SNUTT-2022/SNUTT/Views/Scenes/TimetableScene.swift @@ -70,8 +70,6 @@ struct TimetableScene: View { ActivityViewController(activityItems: [screenshot, linkMetadata]) } .onLoad { - viewModel.preloadWebViews() - await withTaskGroup(of: Void.self, body: { group in group.addTask { await viewModel.fetchTimetableList()