Skip to content

Commit 006899f

Browse files
authored
Merge pull request #304 from Atcha-Project/bugfix/#303
[BUGFIX] QA 반영 (v1.8)
2 parents 7357ad6 + 561af40 commit 006899f

9 files changed

Lines changed: 90 additions & 65 deletions

File tree

Atcha-iOS.xcodeproj/project.pbxproj

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2468,7 +2468,7 @@
24682468
CODE_SIGN_IDENTITY = "Apple Development";
24692469
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
24702470
CODE_SIGN_STYLE = Manual;
2471-
CURRENT_PROJECT_VERSION = 15;
2471+
CURRENT_PROJECT_VERSION = 1;
24722472
DEVELOPMENT_TEAM = "";
24732473
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 23SCTLK482;
24742474
FRAMEWORK_SEARCH_PATHS = (
@@ -2491,7 +2491,7 @@
24912491
"$(inherited)",
24922492
"@executable_path/Frameworks",
24932493
);
2494-
MARKETING_VERSION = 1.7;
2494+
MARKETING_VERSION = 1.8;
24952495
PRODUCT_BUNDLE_IDENTIFIER = com.atcha.iOS;
24962496
PRODUCT_NAME = "$(TARGET_NAME)";
24972497
PROVISIONING_PROFILE_SPECIFIER = "";
@@ -2515,7 +2515,7 @@
25152515
CODE_SIGN_ENTITLEMENTS = "Atcha-iOS/Atcha-iOS.entitlements";
25162516
CODE_SIGN_IDENTITY = "Apple Development";
25172517
CODE_SIGN_STYLE = Automatic;
2518-
CURRENT_PROJECT_VERSION = 15;
2518+
CURRENT_PROJECT_VERSION = 1;
25192519
DEVELOPMENT_TEAM = 23SCTLK482;
25202520
EXCLUDED_ARCHS = "";
25212521
FRAMEWORK_SEARCH_PATHS = (
@@ -2538,7 +2538,7 @@
25382538
"$(inherited)",
25392539
"@executable_path/Frameworks",
25402540
);
2541-
MARKETING_VERSION = 1.7;
2541+
MARKETING_VERSION = 1.8;
25422542
OTHER_SWIFT_FLAGS = "";
25432543
PRODUCT_BUNDLE_IDENTIFIER = com.atcha.iOS;
25442544
PRODUCT_NAME = "$(TARGET_NAME)";
@@ -2563,7 +2563,7 @@
25632563
CODE_SIGN_IDENTITY = "Apple Development";
25642564
"CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Distribution";
25652565
CODE_SIGN_STYLE = Manual;
2566-
CURRENT_PROJECT_VERSION = 15;
2566+
CURRENT_PROJECT_VERSION = 1;
25672567
DEVELOPMENT_TEAM = "";
25682568
"DEVELOPMENT_TEAM[sdk=iphoneos*]" = 23SCTLK482;
25692569
FRAMEWORK_SEARCH_PATHS = (
@@ -2586,7 +2586,7 @@
25862586
"$(inherited)",
25872587
"@executable_path/Frameworks",
25882588
);
2589-
MARKETING_VERSION = 1.7;
2589+
MARKETING_VERSION = 1.8;
25902590
PRODUCT_BUNDLE_IDENTIFIER = com.atcha.iOS;
25912591
PRODUCT_NAME = "$(TARGET_NAME)";
25922592
PROVISIONING_PROFILE_SPECIFIER = "";

Atcha-iOS/Core/Manager/AlarmManager.swift

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -102,6 +102,9 @@ final class AlarmManager {
102102

103103
/// 완전 정지: 예약/타이머/진동/알림/오디오 모두 끊기
104104
func stopAlarm(keepSilent: Bool = false) {
105+
autoStopWorkItem?.cancel()
106+
autoStopWorkItem = nil
107+
105108
shouldKeepBackgroundAudio = false
106109
pendingStartWorkItem?.cancel()
107110
pendingStartWorkItem = nil

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

Lines changed: 28 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -65,6 +65,7 @@ final class CourseSearchViewController: BaseViewController<CourseSearchViewModel
6565
private let noSearchStack: UIStackView = UIStackView()
6666
private let noSearchImageView: UIImageView = UIImageView()
6767
private let noSearchLabel: UILabel = UILabel()
68+
private var previousLatestId: String?
6869

6970
override func viewDidLoad() {
7071
super.viewDidLoad()
@@ -284,13 +285,37 @@ final class CourseSearchViewController: BaseViewController<CourseSearchViewModel
284285

285286
// MARK: - Course Snapshot 갱신
286287
private func applySnapshot(courses: [CourseUIModel]) {
287-
var snapshot = Snapshot()
288+
// 1. 지금 들어온 데이터 중 1등(가장 늦은 막차)이 누구인지 확인
289+
let currentLatestId = viewModel.latestDepartureCourseId(in: courses)
288290

291+
var snapshot = Snapshot()
289292
snapshot.appendSections([.courseList])
290-
if !courses.isEmpty {
291-
snapshot.appendItems(courses, toSection: .courseList)
293+
snapshot.appendItems(courses, toSection: .courseList)
294+
295+
// 2. 만약 1등이 바뀌었다면? (예: 원래 A였는데 더 늦은 B가 들어옴)
296+
if let lastId = previousLatestId, lastId != currentLatestId {
297+
298+
// [중요] 모든 셀이 아니라, 딱 '이전 1등'과 '현재 1등'만 다시 그리라고 명령합니다.
299+
var itemsToUpdate: [CourseUIModel] = []
300+
301+
// 이전 1등이었던 셀 (이제 배지를 떼야 함)
302+
if let oldItem = courses.first(where: { $0.id == lastId }) {
303+
itemsToUpdate.append(oldItem)
304+
}
305+
// 새로운 1등인 셀 (이제 배지를 달아야 함)
306+
if let newItem = courses.first(where: { $0.id == currentLatestId }) {
307+
itemsToUpdate.append(newItem)
308+
}
309+
310+
if #available(iOS 15.0, *), !itemsToUpdate.isEmpty {
311+
snapshot.reconfigureItems(itemsToUpdate)
312+
}
292313
}
293314

315+
// 3. 상태 저장 및 스냅샷 적용
316+
previousLatestId = currentLatestId
317+
318+
// animatingDifferences를 true로 두면 1 ( ) 2 사이에 부드럽게 slide-in 됩니다.
294319
dataSource.apply(snapshot, animatingDifferences: true)
295320
}
296321

Atcha-iOS/Presentation/Location/MainViewController.swift

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -401,13 +401,13 @@ extension MainViewController {
401401
if viewModel.isGuest {
402402
presentLoginAlert()
403403
} else {
404-
AmplitudeManager.shared.track(.course_search_click)
405-
406-
guard let startCoord = viewModel.currentLocation else {
404+
guard let startCoord = mapContainerView.tMapWrapper.mapView.getCenter() else {
407405
view.showToast(message: "현재 위치를 확인 중이에요. 잠시 후 다시 시도해 주세요.")
408406
return
409407
}
410408

409+
AmplitudeManager.shared.track(.course_search_click)
410+
411411
let wrapper = UserDefaultsWrapper.shared
412412
let endLatStr = wrapper.string(forKey: UserDefaultsWrapper.Key.homeLat.rawValue) ?? "37.554722"
413413
let endLonStr = wrapper.string(forKey: UserDefaultsWrapper.Key.homeLon.rawValue) ?? "126.970833"
@@ -424,7 +424,7 @@ extension MainViewController {
424424
}
425425

426426
viewModel.handleRoute(route: .courseSearch(
427-
startLat: "", startLon: "", startAddress: ""
427+
startLat: String(startCoord.latitude), startLon: String(startCoord.longitude), startAddress: ""
428428
))
429429
}
430430
}
@@ -509,9 +509,14 @@ extension MainViewController {
509509
mapContainerView.clearMapView()
510510
mapContainerView.beforeUserMarker()
511511

512-
if let coord = viewModel.selectedLocation ?? viewModel.currentLocation {
513-
mapContainerView.setupCenter(location: coord)
512+
if let currentCoord = viewModel.currentLocation {
513+
// 현재 좌표가 있다면 즉시 이동
514+
mapContainerView.setupCenter(location: currentCoord)
515+
viewModel.selectedLocation = currentCoord // 주소 검색 결과도 현위치로 갱신
516+
shouldCenterToCurrentLocationOnce = false
514517
} else {
518+
// 아직 좌표가 없다면, 다음 위치 업데이트 시점에 이동하도록 예약
519+
shouldCenterToCurrentLocationOnce = true
515520
viewModel.setupLocation()
516521
}
517522

@@ -575,7 +580,7 @@ extension MainViewController {
575580
.compactMap { $0 }
576581
.receive(on: RunLoop.main)
577582
.sink { _ in
578-
// 👉 뷰모델에서 알아서 주소를 검색하므로 뷰컨트롤러는 카메라를 건드리지 않음!
583+
// 뷰모델에서 알아서 주소를 검색하므로 뷰컨트롤러는 카메라를 건드리지 않음!
579584
}
580585
.store(in: &cancellables)
581586
}

Atcha-iOS/Presentation/Location/MainViewModel.swift

Lines changed: 11 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -485,15 +485,18 @@ extension MainViewModel {
485485
address: lastReverseGeocode?.address,
486486
radius: lastReverseGeocode?.radius)))
487487

488-
case .courseSearch:
489-
guard let currentLocation else { return }
490-
let lat: String = "\(currentLocation.latitude)"
491-
let lon: String = "\(currentLocation.longitude)"
492-
let address: String = address ?? ""
488+
case .courseSearch(let startLat, let startLon, _):
489+
490+
if lastReverseGeocode != nil {
491+
routeHandler?(.courseSearch(startLat: "\(lastReverseGeocode?.lat ?? 0.0)",
492+
startLon: "\(lastReverseGeocode?.lon ?? 0.0)",
493+
startAddress: lastReverseGeocode?.address ?? ""))
494+
} else {
495+
routeHandler?(.courseSearch(startLat: startLat,
496+
startLon: startLon,
497+
startAddress: address ?? ""))
498+
}
493499

494-
routeHandler?(.courseSearch(startLat: lat,
495-
startLon: lon,
496-
startAddress: address))
497500
case .myPage:
498501
routeHandler?(.myPage)
499502

@@ -605,30 +608,7 @@ extension MainViewModel {
605608
}
606609
}
607610

608-
// MARK: - 2분 타임아웃
609611
extension MainViewModel {
610-
// private func startAlarmTimeoutTimer() {
611-
// alarmTimeoutCancellable?.cancel()
612-
//
613-
// let task = Task { [weak self] in
614-
// guard let self else { return }
615-
//
616-
// do {
617-
// try await Task.sleep(nanoseconds: 120 * 1_000_000_000)
618-
// } catch {
619-
// return
620-
// }
621-
//
622-
// guard !Task.isCancelled else { return }
623-
//
624-
// await MainActor.run {
625-
// self.routeHandler?(.dismissLockScreen)
626-
// }
627-
// }
628-
//
629-
// alarmTimeoutCancellable = AnyCancellable { task.cancel() }
630-
// }
631-
632612
func stopAlarmTimeoutTimer() {
633613
alarmTimeoutCancellable?.cancel()
634614
alarmTimeoutCancellable = nil

Atcha-iOS/Presentation/Location/View/TMapContainerView.swift

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ import TMapSDK
1111
import SnapKit
1212

1313
final class TMapContainerView: UIView {
14-
private var tMapWrapper: TMapWrapper!
14+
var tMapWrapper: TMapWrapper!
1515
var gestureTargetView: UIView { tMapWrapper.mapView }
1616

1717
var onUserInteraction: (() -> Void)?

Atcha-iOS/Presentation/Lock/LockViewController.swift

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -142,6 +142,7 @@ final class LockViewController: BaseViewController<LockViewModel> {
142142
@objc private func startTapped() {
143143
viewModel.cancelLockScreenTimer()
144144
AlarmManager.shared.stopAlarm()
145+
AlarmManager.shared.removeAllAlarmNotificationsExceptAutoStop()
145146

146147
let wrapper = UserDefaultsWrapper.shared
147148
let legInfo = wrapper.object(forKey: UserDefaultsWrapper.Key.legInfo.rawValue, of: LegInfo.self)

Atcha-iOS/Presentation/Main/MainCoordinator.swift

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,10 @@ final class MainCoordinator {
241241
self.navigationController.pushViewController(modifyVC, animated: true)
242242

243243
case .detailRoute(let address, let infos, let context):
244+
if navigationController.topViewController is DetailRouteViewController {
245+
print("이미 상세 경로 화면입니다. 중복 push를 방지합니다.")
246+
return
247+
}
244248
let routeDI = diContainer.makeRouteDIContainer()
245249
let vm = routeDI.makeDetailRouteViewModel(address: address, infos: infos, context: context)
246250
let vc = routeDI.makeDetailRouteViewController(viewModel: vm)
@@ -367,7 +371,7 @@ extension UINavigationController {
367371
UIView.performWithoutAnimation {
368372
if let target = viewControllers.first(where: { $0 is MainViewController }) {
369373
popToViewController(target, animated: true)
370-
} else {
374+
} else {
371375
popToRootViewController(animated: true)
372376
}
373377
}

Atcha-iOS/Presentation/Onboarding/HomeRegister/HomeRegisterViewModel.swift

Lines changed: 23 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -51,28 +51,35 @@ final class HomeRegisterViewModel: BaseViewModel {
5151
private func setupInitialState() {
5252
let defaults = UserDefaultsWrapper.shared
5353

54+
// 1. 이름과 주소가 이미 있다면 (직접 등록했던 유저) 바로 표시
5455
if let buildingName = defaults.string(forKey: UserDefaultsWrapper.Key.buildingName.rawValue),
5556
let address = defaults.string(forKey: UserDefaultsWrapper.Key.homeAddress.rawValue) {
5657
self.selectedState = .selected(name: buildingName, address: address)
57-
} else {
58-
if let lat = defaults.double(forKey: UserDefaultsWrapper.Key.homeLat.rawValue),
59-
let lon = defaults.double(forKey: UserDefaultsWrapper.Key.homeLon.rawValue) {
60-
Task {
61-
if let location = try? await fetchCurrentAddress(lat: lat, lon: lon) {
62-
let name = location.name ?? ""
63-
let address = location.address ?? ""
64-
65-
defaults.set(name, forKey: UserDefaultsWrapper.Key.buildingName.rawValue)
66-
defaults.set(address, forKey: UserDefaultsWrapper.Key.homeAddress.rawValue)
67-
68-
await MainActor.run {
69-
self.selectedState = .selected(name: name, address: address)
70-
}
58+
return
59+
}
60+
61+
// 2. 이름은 없지만 좌표는 있다면 (방금 로그인한 유저)
62+
let lat = defaults.double(forKey: UserDefaultsWrapper.Key.homeLat.rawValue)
63+
let lon = defaults.double(forKey: UserDefaultsWrapper.Key.homeLon.rawValue)
64+
65+
if let lat, let lon, lat != 0.0 && lon != 0.0 {
66+
// 이름이 없으니 좌표로 이름을 새로 따와서 채워주기
67+
Task {
68+
if let location = try? await fetchCurrentAddress(lat: lat, lon: lon) {
69+
let name = location.name ?? ""
70+
let addr = location.address ?? ""
71+
72+
// 따온 이름을 저장해두기 (다음엔 1번 조건에서 걸리도록)
73+
defaults.set(name, forKey: UserDefaultsWrapper.Key.buildingName.rawValue)
74+
defaults.set(addr, forKey: UserDefaultsWrapper.Key.homeAddress.rawValue)
75+
76+
await MainActor.run {
77+
self.selectedState = .selected(name: name, address: addr)
7178
}
7279
}
73-
} else {
74-
self.selectedState = LocationSelectionState.none
7580
}
81+
} else {
82+
self.selectedState = LocationSelectionState.none
7683
}
7784
}
7885

0 commit comments

Comments
 (0)