Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
77 changes: 70 additions & 7 deletions ComfortableMove/ComfortableMove/Core/Manager/AlertManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@

import Foundation
import SwiftUI
import Network

// MARK: - Alert Types
enum AlertType: Identifiable {
Expand All @@ -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 {
Expand All @@ -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
}
}

Expand All @@ -64,6 +71,10 @@ enum AlertType: Identifiable {
return "버스 배려석 알림 전송에 실패하였습니다."
case .busDeviceNotFound:
return "알림 기기를 찾을 수 없음"
case .externalLink:
return "외부 링크 이동"
case .networkUnavailable:
return "네트워크 연결 필요"
}
}

Expand All @@ -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
}
}

Expand All @@ -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
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<PresentationMode>

var backButton : some View { // <-- 👀 커스텀 버튼
var backButton : some View { // <-- 커스텀 버튼
Button{
HapticManager.shared.impact(style: .light) // 햅틱 추가
self.presentationMode.wrappedValue.dismiss()
Expand Down Expand Up @@ -51,14 +52,15 @@ struct InfoView: View {
Text("알림음을 꺼도 불빛과 전광판 알림은 유지됩니다.")
.moveFont(.caption)
.foregroundColor(.gray.opacity(0.7))
.fixedSize(horizontal: false, vertical: true) // 줄바꿈 허용
}
.accessibleGroup(combine: true) // 텍스트 그룹화

Spacer()

Toggle("", isOn: $isSoundEnabled)
.labelsHidden()
.padding(12) // 터치 영역 확보
.padding(.leading, 12)
.onChange(of: isSoundEnabled) { _, _ in
HapticManager.shared.impact(style: .light) // 토글 시 햅틱
}
Expand Down Expand Up @@ -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 {
Expand All @@ -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 {
Expand Down Expand Up @@ -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()
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
}
}
Expand Down