Skip to content

Commit 3e20bd9

Browse files
committed
[Fix] QA Review
- HomeView Auto Update - Bluetooth Timeout 10->5 - InfoView - NetworkError Fix
1 parent 9061db4 commit 3e20bd9

4 files changed

Lines changed: 125 additions & 15 deletions

File tree

ComfortableMove/ComfortableMove/Core/Manager/AlertManager.swift

Lines changed: 70 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77

88
import Foundation
99
import SwiftUI
10+
import Network
1011

1112
// MARK: - Alert Types
1213
enum AlertType: Identifiable {
@@ -18,7 +19,9 @@ enum AlertType: Identifiable {
1819
case bluetoothConfirm(busName: String, onConfirm: () -> Void, onCancel: () -> Void)
1920
case bluetoothSuccess
2021
case bluetoothFailure
21-
case busDeviceNotFound // 추가
22+
case busDeviceNotFound
23+
case externalLink(title: String, url: URL, onConfirm: () -> Void)
24+
case networkUnavailable
2225

2326
var id: String {
2427
switch self {
@@ -31,16 +34,20 @@ enum AlertType: Identifiable {
3134
case .bluetoothSuccess: return "bluetoothSuccess"
3235
case .bluetoothFailure: return "bluetoothFailure"
3336
case .busDeviceNotFound: return "busDeviceNotFound"
37+
case .externalLink: return "externalLink"
38+
case .networkUnavailable: return "networkUnavailable"
3439
}
3540
}
3641

3742
var priority: Int {
3843
switch self {
44+
case .networkUnavailable: return 4 // 가장 높은 우선순위
3945
case .locationUnauthorized: return 3
4046
case .bluetoothUnsupported, .bluetoothUnauthorized: return 2
4147
case .bluetoothConfirm, .bluetoothSuccess, .bluetoothFailure, .busDeviceNotFound: return 1
4248
case .noBusInfo: return 1
4349
case .apiError: return 0
50+
case .externalLink: return 1
4451
}
4552
}
4653

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

@@ -87,17 +98,23 @@ enum AlertType: Identifiable {
8798
return "다시 한번 시도해주세요."
8899
case .busDeviceNotFound:
89100
return "해당 버스에 알림 기기가 설치되지 않았거나,\n현재 신호가 약하여 연결할 수 없습니다."
101+
case .externalLink(let pageName, _, _):
102+
return "'\(pageName)' 페이지로 이동하시겠습니까?\n앱을 벗어나 브라우저가 실행됩니다."
103+
case .networkUnavailable:
104+
return "인터넷에 연결되어 있지 않습니다.\nWi-Fi 또는 셀룰러 데이터를 켜주세요."
90105
}
91106
}
92107

93108
var shouldBlockApp: Bool {
94109
switch self {
95-
case .bluetoothUnsupported, .bluetoothUnauthorized, .locationUnauthorized:
110+
case .bluetoothUnsupported, .bluetoothUnauthorized, .locationUnauthorized, .networkUnavailable:
96111
return true
97112
case .noBusInfo, .apiError, .bluetoothConfirm, .bluetoothSuccess, .bluetoothFailure:
98113
return false
99114
case .busDeviceNotFound:
100115
return false
116+
case .externalLink:
117+
return false
101118
}
102119
}
103120

@@ -106,25 +123,71 @@ enum AlertType: Identifiable {
106123
}
107124

108125
var isConfirmAlert: Bool {
109-
if case .bluetoothConfirm = self {
126+
switch self {
127+
case .bluetoothConfirm, .externalLink:
110128
return true
129+
default:
130+
return false
111131
}
112-
return false
113132
}
114133
}
115134

116135
// MARK: - Alert Manager
117136
class AlertManager: ObservableObject {
118137
@Published var currentAlert: AlertType?
138+
139+
private let monitor = NWPathMonitor()
140+
private let queue = DispatchQueue(label: "NetworkMonitor")
141+
private var isNetworkConnected: Bool = true // 현재 네트워크 상태 저장
142+
143+
init() {
144+
startMonitoring()
145+
}
146+
147+
deinit {
148+
monitor.cancel()
149+
}
150+
151+
private func startMonitoring() {
152+
monitor.pathUpdateHandler = { [weak self] path in
153+
DispatchQueue.main.async {
154+
let isConnected = path.status == .satisfied
155+
self?.isNetworkConnected = isConnected
156+
157+
if isConnected {
158+
// 네트워크가 연결되면 네트워크 관련 알림만 닫음
159+
if case .networkUnavailable = self?.currentAlert {
160+
self?.currentAlert = nil
161+
}
162+
} else {
163+
// 네트워크 끊김 -> 알림 표시
164+
self?.showAlert(.networkUnavailable)
165+
}
166+
}
167+
}
168+
monitor.start(queue: queue)
169+
}
119170

120171
func showAlert(_ type: AlertType) {
172+
var typeToDisplay = type
173+
174+
// 네트워크가 끊겨있는데 API 관련 에러가 들어오면, 네트워크 에러 알림으로 교체
175+
if !isNetworkConnected {
176+
switch type {
177+
case .apiError, .noBusInfo:
178+
typeToDisplay = .networkUnavailable
179+
default:
180+
break
181+
}
182+
}
183+
121184
// 현재 alert가 없거나, 새로운 alert의 우선순위가 더 높거나 같은 경우 표시
122185
if let current = currentAlert {
123-
if type.priority >= current.priority {
124-
currentAlert = type
186+
if typeToDisplay.priority >= current.priority {
187+
currentAlert = typeToDisplay
125188
}
126189
} else {
127-
currentAlert = type
190+
currentAlert = typeToDisplay
128191
}
129192
}
130193

ComfortableMove/ComfortableMove/Core/Manager/Bluetooth/BluetoothConfig.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -49,7 +49,7 @@ struct BluetoothConfig {
4949
return withSound ? "DEFAULT" : "SILENT"
5050
}
5151

52-
static let scanTimeout: TimeInterval = 10
52+
static let scanTimeout: TimeInterval = 5
5353

5454
static func busNumber(from deviceName: String) -> String? {
5555
guard deviceName.hasPrefix(deviceNamePrefix) else {

ComfortableMove/ComfortableMove/Core/Presentation/Help/InfoView.swift

Lines changed: 45 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,11 @@ import SwiftUI
1010
struct InfoView: View {
1111
private let appVersion = Bundle.main.infoDictionary?["CFBundleShortVersionString"] as? String ?? "1.0.0"
1212
@AppStorage("isSoundEnabled") private var isSoundEnabled: Bool = true
13+
@StateObject private var alertManager = AlertManager()
1314

1415
@Environment(\.presentationMode) var presentationMode: Binding<PresentationMode>
1516

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

5759
Spacer()
5860

5961
Toggle("", isOn: $isSoundEnabled)
6062
.labelsHidden()
61-
.padding(12) // 터치 영역 확보
63+
.padding(.leading, 12)
6264
.onChange(of: isSoundEnabled) { _, _ in
6365
HapticManager.shared.impact(style: .light) // 토글 시 햅틱
6466
}
@@ -91,9 +93,13 @@ struct InfoView: View {
9193

9294
// 앱 문의
9395
Button(action: {
94-
HapticManager.shared.impact(style: .light) // 햅틱 추가
96+
HapticManager.shared.impact(style: .light)
9597
if let url = URL(string: "https://forms.gle/rnSD44sUEuy1nLaH6") {
96-
UIApplication.shared.open(url)
98+
alertManager.showAlert(.externalLink(
99+
title: "앱 문의",
100+
url: url,
101+
onConfirm: { UIApplication.shared.open(url) }
102+
))
97103
}
98104
}) {
99105
HStack {
@@ -116,9 +122,13 @@ struct InfoView: View {
116122

117123
// 개인정보 처리 방침 및 이용약관
118124
Button(action: {
119-
HapticManager.shared.impact(style: .light) // 햅틱 추가
125+
HapticManager.shared.impact(style: .light)
120126
if let url = URL(string: "https://important-hisser-903.notion.site/10-22-ver-29a65f12c44480b6b591e726c5c80f89?source=copy_link") {
121-
UIApplication.shared.open(url)
127+
alertManager.showAlert(.externalLink(
128+
title: "개인정보 처리 방침 및 이용약관",
129+
url: url,
130+
onConfirm: { UIApplication.shared.open(url) }
131+
))
122132
}
123133
}) {
124134
HStack {
@@ -157,9 +167,37 @@ struct InfoView: View {
157167
.toolbarBackground(Color("BFPrimaryColor"), for: .navigationBar)
158168
.toolbarBackground(.visible, for: .navigationBar)
159169
.toolbarColorScheme(.dark, for: .navigationBar)
170+
.alert(item: $alertManager.currentAlert) { alertType in
171+
createAlert(for: alertType)
172+
}
173+
}
174+
175+
// MARK: - Create Alert
176+
private func createAlert(for alertType: AlertType) -> Alert {
177+
if case .externalLink(_, _, let onConfirm) = alertType {
178+
return Alert(
179+
title: Text(alertType.title),
180+
message: Text(alertType.message),
181+
primaryButton: .default(Text("이동")) {
182+
alertManager.dismissAlert()
183+
onConfirm()
184+
},
185+
secondaryButton: .cancel(Text("취소")) {
186+
alertManager.dismissAlert()
187+
}
188+
)
189+
}
190+
191+
return Alert(
192+
title: Text(alertType.title),
193+
message: Text(alertType.message),
194+
dismissButton: .default(Text("확인")) {
195+
alertManager.dismissAlert()
196+
}
197+
)
160198
}
161199
}
162200

163201
#Preview {
164202
InfoView()
165-
}
203+
}

ComfortableMove/ComfortableMove/Core/Presentation/Home/HomeView.swift

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,9 @@ struct HomeView: View {
2525
@State private var isButtonTapped = false
2626
@AppStorage("isSoundEnabled") private var isSoundEnabled: Bool = true
2727

28+
// 자동 새로고침 타이머 (1분마다)
29+
private let autoRefreshTimer = Timer.publish(every: 60, on: .main, in: .common).autoconnect()
30+
2831

2932
var body: some View {
3033
GeometryReader { geometry in
@@ -302,6 +305,12 @@ struct HomeView: View {
302305
.alert(item: $alertManager.currentAlert) { alertType in
303306
createAlert(for: alertType)
304307
}
308+
.onReceive(autoRefreshTimer) { _ in
309+
// 1분마다 버스 도착 정보 자동 새로고침
310+
if nearestStation != nil {
311+
refreshBusArrivals()
312+
}
313+
}
305314
.navigationBarHidden(true)
306315
}
307316
}

0 commit comments

Comments
 (0)