diff --git a/ComfortableMove/ComfortableMove/Core/Manager/AlertManager.swift b/ComfortableMove/ComfortableMove/Core/Manager/AlertManager.swift index 2ce2772..a408438 100644 --- a/ComfortableMove/ComfortableMove/Core/Manager/AlertManager.swift +++ b/ComfortableMove/ComfortableMove/Core/Manager/AlertManager.swift @@ -7,6 +7,7 @@ import Foundation import SwiftUI +import Network // MARK: - Alert Types enum AlertType: Identifiable { @@ -18,7 +19,9 @@ enum AlertType: Identifiable { case bluetoothConfirm(busName: String, onConfirm: () -> Void, onCancel: () -> Void) case bluetoothSuccess case bluetoothFailure - case busDeviceNotFound // 추가 + case busDeviceNotFound + case externalLink(title: String, url: URL, onConfirm: () -> Void) + case networkUnavailable var id: String { switch self { @@ -31,16 +34,20 @@ enum AlertType: Identifiable { case .bluetoothSuccess: return "bluetoothSuccess" case .bluetoothFailure: return "bluetoothFailure" case .busDeviceNotFound: return "busDeviceNotFound" + case .externalLink: return "externalLink" + case .networkUnavailable: return "networkUnavailable" } } var priority: Int { switch self { + case .networkUnavailable: return 4 // 가장 높은 우선순위 case .locationUnauthorized: return 3 case .bluetoothUnsupported, .bluetoothUnauthorized: return 2 case .bluetoothConfirm, .bluetoothSuccess, .bluetoothFailure, .busDeviceNotFound: return 1 case .noBusInfo: return 1 case .apiError: return 0 + case .externalLink: return 1 } } @@ -64,6 +71,10 @@ enum AlertType: Identifiable { return "버스 배려석 알림 전송에 실패하였습니다." case .busDeviceNotFound: return "알림 기기를 찾을 수 없음" + case .externalLink: + return "외부 링크 이동" + case .networkUnavailable: + return "네트워크 연결 필요" } } @@ -87,17 +98,23 @@ enum AlertType: Identifiable { return "다시 한번 시도해주세요." case .busDeviceNotFound: return "해당 버스에 알림 기기가 설치되지 않았거나,\n현재 신호가 약하여 연결할 수 없습니다." + case .externalLink(let pageName, _, _): + return "'\(pageName)' 페이지로 이동하시겠습니까?\n앱을 벗어나 브라우저가 실행됩니다." + case .networkUnavailable: + return "인터넷에 연결되어 있지 않습니다.\nWi-Fi 또는 셀룰러 데이터를 켜주세요." } } var shouldBlockApp: Bool { switch self { - case .bluetoothUnsupported, .bluetoothUnauthorized, .locationUnauthorized: + case .bluetoothUnsupported, .bluetoothUnauthorized, .locationUnauthorized, .networkUnavailable: return true case .noBusInfo, .apiError, .bluetoothConfirm, .bluetoothSuccess, .bluetoothFailure: return false case .busDeviceNotFound: return false + case .externalLink: + return false } } @@ -106,25 +123,71 @@ enum AlertType: Identifiable { } var isConfirmAlert: Bool { - if case .bluetoothConfirm = self { + switch self { + case .bluetoothConfirm, .externalLink: return true + default: + return false } - return false } } // MARK: - Alert Manager class AlertManager: ObservableObject { @Published var currentAlert: AlertType? + + private let monitor = NWPathMonitor() + private let queue = DispatchQueue(label: "NetworkMonitor") + private var isNetworkConnected: Bool = true // 현재 네트워크 상태 저장 + + init() { + startMonitoring() + } + + deinit { + monitor.cancel() + } + + private func startMonitoring() { + monitor.pathUpdateHandler = { [weak self] path in + DispatchQueue.main.async { + let isConnected = path.status == .satisfied + self?.isNetworkConnected = isConnected + + if isConnected { + // 네트워크가 연결되면 네트워크 관련 알림만 닫음 + if case .networkUnavailable = self?.currentAlert { + self?.currentAlert = nil + } + } else { + // 네트워크 끊김 -> 알림 표시 + self?.showAlert(.networkUnavailable) + } + } + } + monitor.start(queue: queue) + } func showAlert(_ type: AlertType) { + var typeToDisplay = type + + // 네트워크가 끊겨있는데 API 관련 에러가 들어오면, 네트워크 에러 알림으로 교체 + if !isNetworkConnected { + switch type { + case .apiError, .noBusInfo: + typeToDisplay = .networkUnavailable + default: + break + } + } + // 현재 alert가 없거나, 새로운 alert의 우선순위가 더 높거나 같은 경우 표시 if let current = currentAlert { - if type.priority >= current.priority { - currentAlert = type + if typeToDisplay.priority >= current.priority { + currentAlert = typeToDisplay } } else { - currentAlert = type + currentAlert = typeToDisplay } } diff --git a/ComfortableMove/ComfortableMove/Core/Manager/Bluetooth/BluetoothConfig.swift b/ComfortableMove/ComfortableMove/Core/Manager/Bluetooth/BluetoothConfig.swift index 72fb22d..bef5e00 100644 --- a/ComfortableMove/ComfortableMove/Core/Manager/Bluetooth/BluetoothConfig.swift +++ b/ComfortableMove/ComfortableMove/Core/Manager/Bluetooth/BluetoothConfig.swift @@ -49,7 +49,7 @@ struct BluetoothConfig { return withSound ? "DEFAULT" : "SILENT" } - static let scanTimeout: TimeInterval = 10 + static let scanTimeout: TimeInterval = 5 static func busNumber(from deviceName: String) -> String? { guard deviceName.hasPrefix(deviceNamePrefix) else { diff --git a/ComfortableMove/ComfortableMove/Core/Presentation/Help/InfoView.swift b/ComfortableMove/ComfortableMove/Core/Presentation/Help/InfoView.swift index 2ca4332..459ca4e 100644 --- a/ComfortableMove/ComfortableMove/Core/Presentation/Help/InfoView.swift +++ b/ComfortableMove/ComfortableMove/Core/Presentation/Help/InfoView.swift @@ -10,10 +10,11 @@ import SwiftUI struct InfoView: View { private let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0" @AppStorage("isSoundEnabled") private var isSoundEnabled: Bool = true + @StateObject private var alertManager = AlertManager() @Environment(\.presentationMode) var presentationMode: Binding - var backButton : some View { // <-- 👀 커스텀 버튼 + var backButton : some View { // <-- 커스텀 버튼 Button{ HapticManager.shared.impact(style: .light) // 햅틱 추가 self.presentationMode.wrappedValue.dismiss() @@ -51,6 +52,7 @@ struct InfoView: View { Text("알림음을 꺼도 불빛과 전광판 알림은 유지됩니다.") .moveFont(.caption) .foregroundColor(.gray.opacity(0.7)) + .fixedSize(horizontal: false, vertical: true) // 줄바꿈 허용 } .accessibleGroup(combine: true) // 텍스트 그룹화 @@ -58,7 +60,7 @@ struct InfoView: View { Toggle("", isOn: $isSoundEnabled) .labelsHidden() - .padding(12) // 터치 영역 확보 + .padding(.leading, 12) .onChange(of: isSoundEnabled) { _, _ in HapticManager.shared.impact(style: .light) // 토글 시 햅틱 } @@ -91,9 +93,13 @@ struct InfoView: View { // 앱 문의 Button(action: { - HapticManager.shared.impact(style: .light) // 햅틱 추가 + HapticManager.shared.impact(style: .light) if let url = URL(string: "https://forms.gle/rnSD44sUEuy1nLaH6") { - UIApplication.shared.open(url) + alertManager.showAlert(.externalLink( + title: "앱 문의", + url: url, + onConfirm: { UIApplication.shared.open(url) } + )) } }) { HStack { @@ -116,9 +122,13 @@ struct InfoView: View { // 개인정보 처리 방침 및 이용약관 Button(action: { - HapticManager.shared.impact(style: .light) // 햅틱 추가 + HapticManager.shared.impact(style: .light) if let url = URL(string: "https://important-hisser-903.notion.site/10-22-ver-29a65f12c44480b6b591e726c5c80f89?source=copy_link") { - UIApplication.shared.open(url) + alertManager.showAlert(.externalLink( + title: "개인정보 처리 방침 및 이용약관", + url: url, + onConfirm: { UIApplication.shared.open(url) } + )) } }) { HStack { @@ -157,9 +167,37 @@ struct InfoView: View { .toolbarBackground(Color("BFPrimaryColor"), for: .navigationBar) .toolbarBackground(.visible, for: .navigationBar) .toolbarColorScheme(.dark, for: .navigationBar) + .alert(item: $alertManager.currentAlert) { alertType in + createAlert(for: alertType) + } + } + + // MARK: - Create Alert + private func createAlert(for alertType: AlertType) -> Alert { + if case .externalLink(_, _, let onConfirm) = alertType { + return Alert( + title: Text(alertType.title), + message: Text(alertType.message), + primaryButton: .default(Text("이동")) { + alertManager.dismissAlert() + onConfirm() + }, + secondaryButton: .cancel(Text("취소")) { + alertManager.dismissAlert() + } + ) + } + + return Alert( + title: Text(alertType.title), + message: Text(alertType.message), + dismissButton: .default(Text("확인")) { + alertManager.dismissAlert() + } + ) } } #Preview { InfoView() -} \ No newline at end of file +} diff --git a/ComfortableMove/ComfortableMove/Core/Presentation/Home/HomeView.swift b/ComfortableMove/ComfortableMove/Core/Presentation/Home/HomeView.swift index eddbd89..d448e8c 100644 --- a/ComfortableMove/ComfortableMove/Core/Presentation/Home/HomeView.swift +++ b/ComfortableMove/ComfortableMove/Core/Presentation/Home/HomeView.swift @@ -25,6 +25,9 @@ struct HomeView: View { @State private var isButtonTapped = false @AppStorage("isSoundEnabled") private var isSoundEnabled: Bool = true + // 자동 새로고침 타이머 (1분마다) + private let autoRefreshTimer = Timer.publish(every: 60, on: .main, in: .common).autoconnect() + var body: some View { GeometryReader { geometry in @@ -302,6 +305,12 @@ struct HomeView: View { .alert(item: $alertManager.currentAlert) { alertType in createAlert(for: alertType) } + .onReceive(autoRefreshTimer) { _ in + // 1분마다 버스 도착 정보 자동 새로고침 + if nearestStation != nil { + refreshBusArrivals() + } + } .navigationBarHidden(true) } }