From 2d8cfc4f413808e2c7e9ce6ee7408c740ea5c4ca Mon Sep 17 00:00:00 2001 From: Yoorim Choi Date: Sun, 29 Sep 2024 16:58:55 +0900 Subject: [PATCH] Add `NoticeView` (#313) * Add `NoticeView` * Apply SwiftFormat changes * `notices` -> `notice` --------- Co-authored-by: peng-u-0807 --- SNUTT-2022/SNUTT.xcodeproj/project.pbxproj | 4 ++ .../SNUTT/AppState/AppEnvironment.swift | 4 +- .../SNUTT/AppState/States/SystemState.swift | 2 + .../SNUTT/Repositories/Dto/ConfigDto.swift | 7 +++ .../SNUTT/Services/GlobalUIService.swift | 55 ++++++++++++++++++- .../SNUTT/Views/Components/NoticeView.swift | 53 ++++++++++++++++++ SNUTT-2022/SNUTT/Views/SNUTTView.swift | 45 ++++++++++++--- 7 files changed, 158 insertions(+), 12 deletions(-) create mode 100644 SNUTT-2022/SNUTT/Views/Components/NoticeView.swift diff --git a/SNUTT-2022/SNUTT.xcodeproj/project.pbxproj b/SNUTT-2022/SNUTT.xcodeproj/project.pbxproj index ab910297..31d07805 100644 --- a/SNUTT-2022/SNUTT.xcodeproj/project.pbxproj +++ b/SNUTT-2022/SNUTT.xcodeproj/project.pbxproj @@ -73,6 +73,7 @@ B8B22D1629311F6200AB88F3 /* EmptyLectureList.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8B22D1529311F6200AB88F3 /* EmptyLectureList.swift */; }; B8B7501D2B6274A5004F6272 /* KakaoMapsSDK_SPM in Frameworks */ = {isa = PBXBuildFile; productRef = B8B7501C2B6274A5004F6272 /* KakaoMapsSDK_SPM */; }; B8BC0C9028BE02D2007A1CA8 /* ReviewRepository.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8BC0C8F28BE02D2007A1CA8 /* ReviewRepository.swift */; }; + B8C4F3352CA8F48E006B6BAD /* NoticeView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8C4F3342CA8F48E006B6BAD /* NoticeView.swift */; }; B8E51E6628B5EC500065248E /* NetworkConfiguration.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8E51E6528B5EC500065248E /* NetworkConfiguration.swift */; }; B8E51E6828B615140065248E /* WebErrorView.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8E51E6728B615140065248E /* WebErrorView.swift */; }; B8E51E6B28B769680065248E /* ReviewRouter.swift in Sources */ = {isa = PBXBuildFile; fileRef = B8E51E6A28B769680065248E /* ReviewRouter.swift */; }; @@ -404,6 +405,7 @@ B8AF8D3D28C72A880056DE62 /* ValidationUtils.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ValidationUtils.swift; sourceTree = ""; }; B8B22D1529311F6200AB88F3 /* EmptyLectureList.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = EmptyLectureList.swift; sourceTree = ""; }; B8BC0C8F28BE02D2007A1CA8 /* ReviewRepository.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ReviewRepository.swift; sourceTree = ""; }; + B8C4F3342CA8F48E006B6BAD /* NoticeView.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NoticeView.swift; sourceTree = ""; }; B8E51E5C28B546770065248E /* DebugConfig.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = DebugConfig.xcconfig; sourceTree = ""; }; B8E51E5E28B547000065248E /* ReleaseConfig.xcconfig */ = {isa = PBXFileReference; lastKnownFileType = text.xcconfig; path = ReleaseConfig.xcconfig; sourceTree = ""; }; B8E51E6528B5EC500065248E /* NetworkConfiguration.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = NetworkConfiguration.swift; sourceTree = ""; }; @@ -1140,6 +1142,7 @@ B82EA54B2B62C8490029FDF3 /* LectureMapView.swift */, B82EA54D2B6393190029FDF3 /* KakaoMapView.swift */, B8E65A732B4D4E3900BE4930 /* TimeRangeSelectionSheet.swift */, + B8C4F3342CA8F48E006B6BAD /* NoticeView.swift */, ); path = Components; sourceTree = ""; @@ -1521,6 +1524,7 @@ BED04D2F28EA6FA600937E4C /* SettingsMenuItem.swift in Sources */, 738406FF2B5718C600007E62 /* ThemeDetailViewModel.swift in Sources */, B8B22D1629311F6200AB88F3 /* EmptyLectureList.swift in Sources */, + B8C4F3352CA8F48E006B6BAD /* NoticeView.swift in Sources */, BE28036228E884F400B2B1AB /* ReviewWebView.swift in Sources */, BEB3B6A528CDE1FD00E56062 /* TimeUtils.swift in Sources */, BE2CB3632959C0CC00FCF0F0 /* ReviewState.swift in Sources */, diff --git a/SNUTT-2022/SNUTT/AppState/AppEnvironment.swift b/SNUTT-2022/SNUTT/AppState/AppEnvironment.swift index f901af75..3dc2bdbd 100644 --- a/SNUTT-2022/SNUTT/AppState/AppEnvironment.swift +++ b/SNUTT-2022/SNUTT/AppState/AppEnvironment.swift @@ -168,12 +168,12 @@ extension EnvironmentValues { #if DEBUG extension AppEnvironment.Services { - @MainActor static func preview(appState: AppState) -> Self { + @MainActor static func preview(appState _: AppState) -> Self { .init(timetableService: FakeTimetableService(), userService: FakeUserService(), lectureService: FakeLectureService(), searchService: FakeSearchService(), - globalUIService: GlobalUIService(appState: appState, localRepositories: .init(userDefaultsRepository: UserDefaultsRepository(storage: .preview)), webRepositories: nil), + globalUIService: FakeGlobalUIService(), courseBookService: FakeCourseBookService(), authService: FakeAuthService(), notificationService: FakeNotificationService(), diff --git a/SNUTT-2022/SNUTT/AppState/States/SystemState.swift b/SNUTT-2022/SNUTT/AppState/States/SystemState.swift index 2e2d8164..26d3b817 100644 --- a/SNUTT-2022/SNUTT/AppState/States/SystemState.swift +++ b/SNUTT-2022/SNUTT/AppState/States/SystemState.swift @@ -20,6 +20,8 @@ class SystemState { @Published var selectedTab: TabType = .timetable + @Published var noticeViewInfo: ConfigsDto.NoticeViewInfoDto? + var isMapViewExpanded: Bool? var configs: ConfigsDto? } diff --git a/SNUTT-2022/SNUTT/Repositories/Dto/ConfigDto.swift b/SNUTT-2022/SNUTT/Repositories/Dto/ConfigDto.swift index fbad59c3..1dbcb1d9 100644 --- a/SNUTT-2022/SNUTT/Repositories/Dto/ConfigDto.swift +++ b/SNUTT-2022/SNUTT/Repositories/Dto/ConfigDto.swift @@ -12,6 +12,7 @@ struct ConfigsDto: Codable { let vacancySugangSnuUrl: VacancySugangSnuUrlDto? let settingsBadge: SettingsBadgeDto? let reactNativeBundleFriends: ReactNativeBundleFriendsDto? + let notice: NoticeViewInfoDto? let disableMapFeature: Bool? } @@ -35,4 +36,10 @@ extension ConfigsDto { src["ios"] } } + + struct NoticeViewInfoDto: Codable { + let title: String + let content: String + let visible: Bool + } } diff --git a/SNUTT-2022/SNUTT/Services/GlobalUIService.swift b/SNUTT-2022/SNUTT/Services/GlobalUIService.swift index 959405d0..2cc02a3f 100644 --- a/SNUTT-2022/SNUTT/Services/GlobalUIService.swift +++ b/SNUTT-2022/SNUTT/Services/GlobalUIService.swift @@ -45,15 +45,17 @@ protocol GlobalUIServiceProtocol: Sendable { func setRoutingState(_ key: WritableKeyPath, value: V) func hasNewBadge(settingName: String) -> Bool + + func showNoticeViewIfNeeded() async throws } -struct GlobalUIService: GlobalUIServiceProtocol, UserAuthHandler { +struct GlobalUIService: GlobalUIServiceProtocol, UserAuthHandler, ConfigsProvidable { var appState: AppState var localRepositories: AppEnvironment.LocalRepositories - var webRepositories: AppEnvironment.WebRepositories? + var webRepositories: AppEnvironment.WebRepositories var configRepository: ConfigRepositoryProtocol? { - webRepositories?.configRepository + webRepositories.configRepository } func setColorScheme(_ colorScheme: ColorScheme?) { @@ -163,6 +165,13 @@ struct GlobalUIService: GlobalUIServiceProtocol, UserAuthHandler { appState.menu.isLectureTimeSheetOpen = value } + // MARK: Show Notice View + + func showNoticeViewIfNeeded() async throws { + let configs = try await configRepository?.fetchConfigs() + appState.system.noticeViewInfo = configs?.notice + } + // MARK: Error Handling func presentErrorAlert(error: Error) { @@ -195,3 +204,43 @@ extension GlobalUIService { appState.routing[keyPath: key] = value } } + +class FakeGlobalUIService: GlobalUIServiceProtocol { + func setColorScheme(_: ColorScheme?) {} + func loadColorSchemeDuringBootstrap() {} + + func setSelectedTab(_: TabType) {} + func setIsErrorAlertPresented(_: Bool) {} + func setIsMenuOpen(_: Bool) {} + + func openEllipsis(for _: TimetableMetadata) {} + func closeEllipsis() {} + + func openThemeSheet() {} + func closeThemeSheet() {} + + func openRenameSheet() {} + func closeRenameSheet() {} + + func openCreateSheet(withPicker _: Bool) {} + func closeCreateSheet() {} + + func setRenameTitle(_: String) {} + func setCreateTitle(_: String) {} + func setCreateQuarter(_: Quarter?) {} + + func setIsLectureTimeSheetOpen(_: Bool, modifying _: TimePlace?, action _: ((TimePlace) -> Void)?) {} + + func presentErrorAlert(error _: STError?) {} + func presentErrorAlert(error _: ErrorCode) {} + func presentErrorAlert(error _: Error) {} + + func preloadWebViews() {} + func sendMainWebViewReloadSignal() {} + func sendDetailWebViewReloadSignal(url _: URL) {} + + func setRoutingState(_: WritableKeyPath, value _: V) {} + func hasNewBadge(settingName _: String) -> Bool { return true } + + func showNoticeViewIfNeeded() async throws {} +} diff --git a/SNUTT-2022/SNUTT/Views/Components/NoticeView.swift b/SNUTT-2022/SNUTT/Views/Components/NoticeView.swift new file mode 100644 index 00000000..022f056b --- /dev/null +++ b/SNUTT-2022/SNUTT/Views/Components/NoticeView.swift @@ -0,0 +1,53 @@ +// +// NoticeView.swift +// SNUTT +// +// Created by 최유림 on 9/29/24. +// + +import SwiftUI + +struct NoticeView: View { + let title: String? + let content: String? + let sendFeedback: (String, String) async -> Bool + + @State private var pushToFeedBackView = false + + var body: some View { + VStack(spacing: 32) { + Image("warning.cat") + VStack(spacing: 8) { + if let title = title { + Text(title) + .font(STFont.title.font) + } + if let content = content { + Text(content) + .font(STFont.detailLabel.font) + .multilineTextAlignment(.center) + } + Spacer().frame(height: 4) + Button { + pushToFeedBackView = true + } label: { + Text("문의사항 보내기") + .foregroundColor(.white) + .font(.system(size: 14, weight: .semibold)) + .padding(.horizontal, 16) + .padding(.vertical, 8) + .background( + STColor.cyan + ) + .clipShape(.capsule) + } + } + } + .padding(.horizontal, 48) + .background( + NavigationLink("", isActive: $pushToFeedBackView) { + UserSupportView(email: nil, sendFeedback: sendFeedback) + } + ) + } +} diff --git a/SNUTT-2022/SNUTT/Views/SNUTTView.swift b/SNUTT-2022/SNUTT/Views/SNUTTView.swift index b5e1bd94..8239969c 100644 --- a/SNUTT-2022/SNUTT/Views/SNUTTView.swift +++ b/SNUTT-2022/SNUTT/Views/SNUTTView.swift @@ -33,7 +33,13 @@ struct SNUTTView: View, Sendable { var body: some View { ZStack { - if viewModel.isAuthenticated && pushToTimetableScene { + if let noticeViewInfo = viewModel.noticeViewInfo, noticeViewInfo.visible { + NavigationView { + NoticeView(title: noticeViewInfo.title, + content: noticeViewInfo.content, + sendFeedback: viewModel.sendFeedback) + } + } else if viewModel.isAuthenticated && pushToTimetableScene { TabView(selection: selected) { TabScene(tabType: .timetable) { TimetableScene(viewModel: .init(container: viewModel.container)) @@ -112,6 +118,9 @@ struct SNUTTView: View, Sendable { setTabBarStyle() setNavBarStyle() } + .onLoad { + await viewModel.showNoticeViewIfNeeded() + } let _ = debugChanges() } @@ -137,6 +146,7 @@ extension SNUTTView { @Published var accessToken: String? = nil @Published var preferredColorScheme: ColorScheme? = nil @Published private var error: STError? = nil + @Published var noticeViewInfo: ConfigsDto.NoticeViewInfoDto? @Published private var _isErrorAlertPresented = false var isErrorAlertPresented: Bool { @@ -162,6 +172,7 @@ extension SNUTTView { appState.user.$accessToken.assign(to: &$accessToken) appState.system.$preferredColorScheme.assign(to: &$preferredColorScheme) appState.system.$selectedTab.assign(to: &$_selectedTab) + appState.system.$noticeViewInfo.assign(to: &$noticeViewInfo) } var errorTitle: String { @@ -181,12 +192,10 @@ extension SNUTTView { } func getThemeList() async { - func getThemeList() async { - do { - try await services.themeService.getThemeList() - } catch { - services.globalUIService.presentErrorAlert(error: error) - } + do { + try await services.themeService.getThemeList() + } catch { + services.globalUIService.presentErrorAlert(error: error) } } @@ -214,6 +223,14 @@ extension SNUTTView { } } + func showNoticeViewIfNeeded() async { + do { + try await services.globalUIService.showNoticeViewIfNeeded() + } catch { + services.globalUIService.presentErrorAlert(error: error) + } + } + func showVacancyBannerIfNeeded() async { do { try await services.vacancyService.showVacancyBannerIfNeeded() @@ -257,6 +274,20 @@ extension SNUTTView { services.globalUIService.presentErrorAlert(error: error) } } + + func sendFeedback(email: String, message: String) async -> Bool { + if !Validation.check(email: email) { + services.globalUIService.presentErrorAlert(error: .INVALID_EMAIL) + return false + } + do { + try await services.etcService.sendFeedback(email: email, message: message) + return true + } catch { + services.globalUIService.presentErrorAlert(error: error) + return false + } + } } }