Skip to content

Commit 7357ad6

Browse files
authored
Merge pull request #302 from Atcha-Project/bugfix/#301
[BUGFIX] QA 반영 (v1.7)
2 parents 24cd5a2 + 043992e commit 7357ad6

16 files changed

Lines changed: 837 additions & 447 deletions

Atcha-iOS.xcodeproj/project.pbxproj

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -151,6 +151,8 @@
151151
6DC3BF5E2E07123F00831470 /* IntroCell.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DC3BF5D2E07123F00831470 /* IntroCell.swift */; };
152152
6DC3BF602E071F0900831470 /* LoginUseCase.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DC3BF5F2E071F0900831470 /* LoginUseCase.swift */; };
153153
6DC3BF682E0721F300831470 /* LoginDTO.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DC3BF672E0721F300831470 /* LoginDTO.swift */; };
154+
6DC617A72F60EB1A002DD641 /* LocationSmoother.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DC617A62F60EB1A002DD641 /* LocationSmoother.swift */; };
155+
6DC617A92F610238002DD641 /* HomeArrivalManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DC617A82F610238002DD641 /* HomeArrivalManager.swift */; };
154156
6DD632B12E4F8A9F00C6A66E /* CheckServiceRegionRequest.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DD632B02E4F8A9F00C6A66E /* CheckServiceRegionRequest.swift */; };
155157
6DD632B62E52E23A00C6A66E /* ProximityManager.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DD632B52E52E23A00C6A66E /* ProximityManager.swift */; };
156158
6DD632B92E52E8E300C6A66E /* ProximityViewController.swift in Sources */ = {isa = PBXBuildFile; fileRef = 6DD632B82E52E8E300C6A66E /* ProximityViewController.swift */; };
@@ -472,6 +474,8 @@
472474
6DC3BF5D2E07123F00831470 /* IntroCell.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = IntroCell.swift; sourceTree = "<group>"; };
473475
6DC3BF5F2E071F0900831470 /* LoginUseCase.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginUseCase.swift; sourceTree = "<group>"; };
474476
6DC3BF672E0721F300831470 /* LoginDTO.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LoginDTO.swift; sourceTree = "<group>"; };
477+
6DC617A62F60EB1A002DD641 /* LocationSmoother.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = LocationSmoother.swift; sourceTree = "<group>"; };
478+
6DC617A82F610238002DD641 /* HomeArrivalManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = HomeArrivalManager.swift; sourceTree = "<group>"; };
475479
6DD632B02E4F8A9F00C6A66E /* CheckServiceRegionRequest.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = CheckServiceRegionRequest.swift; sourceTree = "<group>"; };
476480
6DD632B52E52E23A00C6A66E /* ProximityManager.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProximityManager.swift; sourceTree = "<group>"; };
477481
6DD632B82E52E8E300C6A66E /* ProximityViewController.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ProximityViewController.swift; sourceTree = "<group>"; };
@@ -1181,6 +1185,8 @@
11811185
6DADA5952EA09B9500CA9BE2 /* Amplitude */,
11821186
B61C448D2E3F57B600285A4B /* AlarmManager.swift */,
11831187
6DD632B72E52E8D000C6A66E /* Proximity */,
1188+
6DC617A62F60EB1A002DD641 /* LocationSmoother.swift */,
1189+
6DC617A82F610238002DD641 /* HomeArrivalManager.swift */,
11841190
);
11851191
path = Manager;
11861192
sourceTree = "<group>";
@@ -2199,6 +2205,7 @@
21992205
B664018B2E2277A900A397AE /* PushAlarmOption.swift in Sources */,
22002206
6D26E0072F3C197F005097A4 /* SubwayInfoRepository.swift in Sources */,
22012207
B65C13082E057C590016D2F0 /* APIService.swift in Sources */,
2208+
6DC617A92F610238002DD641 /* HomeArrivalManager.swift in Sources */,
22022209
6D6879D02E4211B800E59C55 /* HomePatchRequest.swift in Sources */,
22032210
6D9283742E3AFF6A0090889B /* BusRouteCell.swift in Sources */,
22042211
6D73EB5F2E16120200F8DF8B /* CourseSearchViewModel.swift in Sources */,
@@ -2281,6 +2288,7 @@
22812288
6DADA5972EA09BA400CA9BE2 /* AmplitudeManager.swift in Sources */,
22822289
6DD632C22E544DB500C6A66E /* PushAlarmBottomView.swift in Sources */,
22832290
6D5E03D02E28853E0065AFBE /* CourseSearchResponse.swift in Sources */,
2291+
6DC617A72F60EB1A002DD641 /* LocationSmoother.swift in Sources */,
22842292
6D91A8E62E29F5BC0081BAFC /* CourseSettingViewModel.swift in Sources */,
22852293
6D26E0002F3C17C3005097A4 /* SubwayRealTimeInfoRequest.swift in Sources */,
22862294
B673C4912E0424FD00EE4AD0 /* SplashViewModel.swift in Sources */,

Atcha-iOS/Core/Manager/AlarmManager.swift

Lines changed: 46 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,6 +33,7 @@ final class AlarmManager {
3333
private var shouldKeepBackgroundAudio = false
3434
private var isPreviewing = false
3535
private var hapticEngine: CHHapticEngine?
36+
private var autoStopWorkItem: DispatchWorkItem?
3637

3738
// MARK: - Init
3839
private init() {
@@ -71,6 +72,8 @@ final class AlarmManager {
7172
/// 서버에서 받은 출발 시각 기준으로 1분 전에 반복 푸시/사운드/진동을 시작
7273
func startAlarm(title: String, body: String) {
7374
// 기존 알람 상태만 정리 (silent는 유지 or 다시 켜기)
75+
autoStopWorkItem?.cancel()
76+
autoStopWorkItem = nil
7477
stopAlarm(keepSilent: true)
7578
ensureBackgroundSilentRunning()
7679
applySavedVolumeForAlarmStart()
@@ -121,6 +124,7 @@ final class AlarmManager {
121124
}
122125

123126
print("알람 종료 (keepSilent = \(keepSilent))")
127+
UNUserNotificationCenter.current().removePendingNotificationRequests(withIdentifiers: [AlarmNotificationID.autoStopInfo])
124128
}
125129

126130
func alarmInit() {
@@ -220,13 +224,19 @@ extension AlarmManager {
220224

221225
self.sendImmediateLocalPush(title: title, body: body)
222226
}
227+
228+
scheduleAutoStop()
223229
}
224230

225-
func sendImmediateLocalPush(title: String, body: String) {
231+
func sendImmediateLocalPush(title: String, body: String, playSound: Bool = false) {
226232
let content = UNMutableNotificationContent()
227233
content.title = title
228234
content.body = body
229-
content.sound = nil // 사운드는 직접 재생 중
235+
if playSound {
236+
content.sound = .default
237+
} else {
238+
content.sound = nil
239+
}
230240

231241
let request = UNNotificationRequest(
232242
identifier: UUID().uuidString,
@@ -593,3 +603,37 @@ extension AlarmManager {
593603
print("미리듣기 종료 (keep=\(shouldKeepBackgroundAudio))")
594604
}
595605
}
606+
607+
extension AlarmManager {
608+
private func scheduleAutoStop() {
609+
autoStopWorkItem?.cancel()
610+
611+
let content = UNMutableNotificationContent()
612+
content.title = "출발 알람이 자동 종료되었어요"
613+
content.body = "클릭해서 경로 재탐색하기"
614+
content.sound = .default
615+
616+
let request = UNNotificationRequest(
617+
identifier: AlarmNotificationID.autoStopInfo,
618+
content: content,
619+
trigger: UNTimeIntervalNotificationTrigger(timeInterval: 120.0, repeats: false)
620+
)
621+
UNUserNotificationCenter.current().add(request, withCompletionHandler: nil)
622+
623+
// 2. 앱 내부의 정지 로직 및 팝업 신호 (포그라운드일 때 즉시, 백그라운드면 켜질 때 실행됨)
624+
let workItem = DispatchWorkItem { [weak self] in
625+
guard let self = self else { return }
626+
627+
// 음악/진동 정지
628+
self.stopAlarm(keepSilent: false) // 아예 무음까지 끄기
629+
630+
UserDefaults.standard.set(true, forKey: "isAlarmTimedOut")
631+
632+
// 메인 뷰에 타임아웃 팝업 띄우라고 신호
633+
NotificationCenter.default.post(name: NSNotification.Name("alarmDidTimeout"), object: nil)
634+
}
635+
636+
autoStopWorkItem = workItem
637+
DispatchQueue.main.asyncAfter(deadline: .now() + 120.0, execute: workItem)
638+
}
639+
}
Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
//
2+
// HomeArrivalManager.swift
3+
// Atcha-iOS
4+
//
5+
// Created by wodnd on 3/11/26.
6+
//
7+
8+
import Foundation
9+
import CoreLocation
10+
import UserNotifications
11+
import UIKit
12+
13+
final class HomeArrivalManager {
14+
static let shared = HomeArrivalManager()
15+
private init() {}
16+
17+
private var isArrivalSignalSent = false
18+
19+
func checkHomeArrival(currentCoord: CLLocationCoordinate2D) {
20+
let wrapper = UserDefaultsWrapper.shared
21+
22+
guard wrapper.bool(forKey: UserDefaultsWrapper.Key.alarmRegister.rawValue) == true else {
23+
isArrivalSignalSent = false
24+
return
25+
}
26+
27+
guard !isArrivalSignalSent else { return }
28+
29+
let homeLat = wrapper.double(forKey: UserDefaultsWrapper.Key.homeLat.rawValue) ?? 0.0
30+
let homeLon = wrapper.double(forKey: UserDefaultsWrapper.Key.homeLon.rawValue) ?? 0.0
31+
32+
guard homeLat != 0 && homeLon != 0 else { return }
33+
34+
let homeLoc = CLLocation(latitude: homeLat, longitude: homeLon)
35+
let currentLoc = CLLocation(latitude: currentCoord.latitude, longitude: currentCoord.longitude)
36+
37+
let distance = currentLoc.distance(from: homeLoc)
38+
39+
if distance <= 50 {
40+
isArrivalSignalSent = true
41+
42+
AlarmManager.shared.sendImmediateLocalPush(
43+
title: "막차 안내 종료",
44+
body: "목적지 부근에 도착했어요",
45+
playSound: true
46+
)
47+
48+
NotificationCenter.default.post(name: NSNotification.Name("userArrivedHome"), object: nil)
49+
}
50+
}
51+
52+
func reset() {
53+
isArrivalSignalSent = false
54+
}
55+
}
Lines changed: 67 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,67 @@
1+
//
2+
// LocationSmoother.swift
3+
// Atcha-iOS
4+
//
5+
// Created by wodnd on 3/11/26.
6+
//
7+
8+
import Foundation
9+
import CoreLocation
10+
import MapKit
11+
12+
final class LocationSmoother {
13+
private var buffer: [CLLocationCoordinate2D] = []
14+
private let bufferLimit: Int
15+
16+
init(limit: Int = 5) {
17+
self.bufferLimit = limit
18+
}
19+
20+
func smooth(_ next: CLLocationCoordinate2D) -> CLLocationCoordinate2D {
21+
buffer.append(next)
22+
if buffer.count > bufferLimit { buffer.removeFirst() }
23+
24+
let avgLat = buffer.map { $0.latitude }.reduce(0, +) / Double(buffer.count)
25+
let avgLon = buffer.map { $0.longitude }.reduce(0, +) / Double(buffer.count)
26+
27+
return CLLocationCoordinate2D(latitude: avgLat, longitude: avgLon)
28+
}
29+
}
30+
31+
extension LocationSmoother {
32+
/// 좌표를 경로선(Polyline) 위 가장 가까운 점으로 고정합니다.
33+
func snap(current: CLLocationCoordinate2D, polyline: [CLLocationCoordinate2D], threshold: Double = 150) -> CLLocationCoordinate2D {
34+
guard polyline.count >= 2 else { return current }
35+
36+
let p = MKMapPoint(current)
37+
var minDistance = Double.greatestFiniteMagnitude
38+
var closestPoint = p
39+
40+
for i in 0..<(polyline.count - 1) {
41+
let a = MKMapPoint(polyline[i])
42+
let b = MKMapPoint(polyline[i + 1])
43+
44+
let projected = closestPointOnSegment(p, a, b)
45+
let distance = projected.distance(to: p)
46+
47+
if distance < minDistance {
48+
minDistance = distance
49+
closestPoint = projected
50+
}
51+
}
52+
53+
// 임계값(150m) 보다 멀어지면 사용자가 경로를 이탈한 것으로 간주하여 원본 좌표 반환
54+
return minDistance < threshold ? closestPoint.coordinate : current
55+
}
56+
57+
private func closestPointOnSegment(_ p: MKMapPoint, _ a: MKMapPoint, _ b: MKMapPoint) -> MKMapPoint {
58+
let dx = b.x - a.x
59+
let dy = b.y - a.y
60+
if dx == 0 && dy == 0 { return a }
61+
62+
let t = ((p.x - a.x) * dx + (p.y - a.y) * dy) / (dx * dx + dy * dy)
63+
if t < 0 { return a }
64+
if t > 1 { return b }
65+
return MKMapPoint(x: a.x + t * dx, y: a.y + t * dy)
66+
}
67+
}

Atcha-iOS/Presentation/Course/CourseSearch/CourseSearchViewController.swift

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -362,7 +362,7 @@ extension CourseSearchViewController {
362362
let popupVC = AtchaPopupViewController(viewModel: popupVM)
363363

364364
popupVC.confirmButton.addAction(UIAction { [weak popupVC] _ in
365-
popupVC?.dismiss(animated: true)
365+
popupVC?.dismiss(animated: false)
366366

367367
self.viewModel.alarmRegister(alarmRequest)
368368
self.viewModel.getAlarmTapped?(alarmTapped.0, alarmTapped.1)
@@ -392,7 +392,7 @@ extension CourseSearchViewController {
392392
let popupVC = AtchaPopupViewController(viewModel: popupVM)
393393

394394
popupVC.confirmButton.addAction(UIAction { [weak popupVC] _ in
395-
popupVC?.dismiss(animated: true)
395+
popupVC?.dismiss(animated: false)
396396

397397
self.viewModel.alarmRegister(alarmRequest)
398398
self.viewModel.getAlarmTapped?(alarmTapped.0, alarmTapped.1)

0 commit comments

Comments
 (0)