Skip to content

Commit 0d714d0

Browse files
authored
Merge pull request #300 from Atcha-Project/env/stage
[Merge] Live 환경 배포 및 업데이트 심사 (v1.7)
2 parents 375e500 + cd94823 commit 0d714d0

87 files changed

Lines changed: 2201 additions & 1049 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Atcha-iOS.xcodeproj/project.pbxproj

Lines changed: 64 additions & 8 deletions
Large diffs are not rendered by default.

Atcha-iOS/App/AppFlowCoordinator.swift

Lines changed: 64 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -12,11 +12,13 @@ class AppFlowCoordinator {
1212
private let container: AppDIContainer
1313
private let window: UIWindow
1414

15+
// 메모리 유지를 위한 Coordinator 참조
1516
private var splashCoordinator: SplashCoordinator?
1617
private var mainCoordinator: MainCoordinator?
17-
private var loginCoordinator: LoginCoordinator?
1818
private var onboardingCoordinator: OnboardingCoordinator?
1919
private var lockScreenCoordinator: LockScreenCoordinator?
20+
private var introCoordinator: IntroCoordinator?
21+
2022

2123
init(window: UIWindow, container: AppDIContainer) {
2224
self.window = window
@@ -28,28 +30,27 @@ class AppFlowCoordinator {
2830
window.rootViewController = navigationController
2931
window.makeKeyAndVisible()
3032

33+
// 세션 만료 시: 모든 뷰를 엎고 다시 앱 시작
3134
SessionController.shared.routeToLogin = { [weak self] in
32-
self?.showLoginFlow()
35+
DispatchQueue.main.async {
36+
AppDIContainer.shared.tokenStorage.clearAllTokens()
37+
UserDefaultsWrapper.shared.set(false, forKey: UserDefaultsWrapper.Key.hasSeenIntro.rawValue)
38+
self?.startApp()
39+
}
3340
}
3441

3542
let splashCoordinator = container.makeSplashCoordinator(navigationController: navigationController)
3643
splashCoordinator.routerHandler = { [weak self] router in
37-
guard let self else { return }
44+
guard let self = self else { return }
3845
switch router {
39-
case .login:
40-
showLoginFlow()
46+
case .intro:
47+
showIntroFlow()
4148
case .main:
4249
showMainFlow()
43-
case .onboarding:
44-
showOnboardingFlow()
4550
case .alarm(let info, let address):
4651
showMainFlow(info: info, address: address, bottomType: .departure)
4752
case .lockScreen(let info, let address):
4853
showLockScreenFlow(info: info, address: address)
49-
// case .realTime(let info, let address):
50-
// showMainFlow(info: info, address: address, bottomType: .realTime)
51-
// case .finishTime(let info, let address):
52-
// showMainFlow(info: info, address: address, bottomType: .finish)
5354
case .detailRoute(let lat, let lon, let address):
5455
print("lat : \(lat), lon : \(lon), address : \(address)")
5556
}
@@ -61,26 +62,63 @@ class AppFlowCoordinator {
6162
private func showMainFlow(info: LegInfo? = nil,
6263
address: String? = nil,
6364
bottomType: MapBottomType = .search) {
65+
6466
let navigationController = UINavigationController()
6567
window.rootViewController = navigationController
66-
mainCoordinator = container.makeMainCoordinator(navigationController: navigationController)
67-
mainCoordinator?.signoutFinish = { [weak self] in
68+
69+
let mainCoordinator = container.makeMainCoordinator(navigationController: navigationController)
70+
self.mainCoordinator = mainCoordinator
71+
72+
// [신규 유저 온보딩 흐름]
73+
// Main 화면(지도)을 깔아둔 상태에서, 그 위(Navigation 스택)에 온보딩을 얹습니다.
74+
mainCoordinator.routeToOnboarding = { [weak self] in
75+
guard let self = self else { return }
6876
DispatchQueue.main.async {
69-
self?.showLoginFlow()
77+
let onboardingCoordinator = self.container.makeOnboardingCoordinator(navigationController: navigationController)
78+
79+
// 온보딩(회원가입)이 성공적으로 끝났을 때
80+
onboardingCoordinator.onFinish = { [weak self] success in
81+
DispatchQueue.main.async {
82+
// 위에 쌓여있던 온보딩 화면들을 싹 치우고 밑에 깔려있던 Main(지도)으로 복귀!
83+
navigationController.popToRootViewController(animated: true)
84+
85+
// 집 주소가 등록되었으니, MainVC를 찔러서 현위치/마커를 새로고침하게 합니다.
86+
if let mainVC = navigationController.viewControllers.first as? MainViewController {
87+
mainVC.viewModel.setupLocation()
88+
mainVC.shouldShowWelcomeToast = true
89+
90+
mainVC.viewModel.isGuest = false
91+
}
92+
93+
self?.onboardingCoordinator = nil
94+
}
95+
}
96+
97+
onboardingCoordinator.start()
98+
self.onboardingCoordinator = onboardingCoordinator // 메모리 유지
7099
}
71100
}
72-
mainCoordinator?.lockScreenConfrim = { [weak self] info, address in
101+
102+
// [락스크린 확인 흐름]
103+
mainCoordinator.lockScreenConfrim = { [weak self] info, address in
73104
DispatchQueue.main.async {
74105
if let info, let address {
75-
// self?.showMainFlow(info: info, address: address, bottomType: .realTime)
76106
self?.showMainFlow(info: info, address: address, bottomType: .detail)
77-
// TODO: 상세화면 연동 로직 적용하기
78107
} else {
79108
self?.showMainFlow()
80109
}
81110
}
82111
}
83-
mainCoordinator?.start(info: info, address: address, bottomType: bottomType)
112+
113+
// [회원 탈퇴 흐름]
114+
mainCoordinator.withdrawFinish = { [weak self] in
115+
DispatchQueue.main.async {
116+
// 앱 데이터를 다 지웠으니, 스플래시부터 앱을 아예 새로 시작(리부팅)합니다!
117+
self?.startApp()
118+
}
119+
}
120+
121+
mainCoordinator.start(info: info, address: address, bottomType: bottomType)
84122
}
85123

86124
private func showLockScreenFlow(info: LegInfo? = nil,
@@ -94,7 +132,6 @@ class AppFlowCoordinator {
94132
switch router {
95133
case .lockScreen(let info, let address):
96134
if let info, let address {
97-
// TODO: 상세화면 연동하기 로직
98135
self?.showMainFlow(info: info, address: address, bottomType: .departure)
99136
} else {
100137
self?.showMainFlow()
@@ -107,32 +144,21 @@ class AppFlowCoordinator {
107144
self.lockScreenCoordinator = lockScreenCoordinator
108145
}
109146

110-
private func showLoginFlow() {
147+
private func showIntroFlow() {
111148
let navigationController = UINavigationController()
112149
window.rootViewController = navigationController
113150

114-
let loginCoordinator = container.makeLoginCoordinator(navigationController: navigationController)
115-
loginCoordinator.onFinishWithExistUser = { [weak self] isExist in
116-
DispatchQueue.main.async {
117-
isExist ? self?.showMainFlow() : self?.showOnboardingFlow()
118-
}
119-
}
120-
loginCoordinator.start()
121-
self.loginCoordinator = loginCoordinator
122-
}
123-
124-
private func showOnboardingFlow() {
125-
let navigationController = UINavigationController()
126-
window.rootViewController = navigationController
151+
let introCoordinator = container.makeIntroCoordinator(navigationController: navigationController)
127152

128-
let onboardingCoordinator = container.makeOnboardingCoordinator(navigationController: navigationController)
129-
onboardingCoordinator.onFinish = { [weak self] success in
153+
// 인트로에서 "게스트 모드로 시작하기" 등을 눌렀을 때 지도(Main)로 넘어갑니다.
154+
introCoordinator.onFinishWithGuest = { [weak self] in
130155
DispatchQueue.main.async {
131-
success ? self?.showMainFlow() : self?.showLoginFlow()
156+
UserDefaultsWrapper.shared.set(true, forKey: UserDefaultsWrapper.Key.hasSeenIntro.rawValue)
157+
self?.showMainFlow()
132158
}
133159
}
134160

135-
onboardingCoordinator.start()
136-
self.onboardingCoordinator = onboardingCoordinator
161+
introCoordinator.start()
162+
self.introCoordinator = introCoordinator
137163
}
138164
}

Atcha-iOS/App/DIContainer/AppCompositionRoot.swift

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ final class AppCompositionRoot {
2525
let onboardingDIContainer: OnboardingDIContainer
2626
let mainDIContainer: MainDIContainer
2727
let lockScreenDIContainer: LockScreenDIContainer
28+
let introDIContainer: IntroDIContainer
2829

2930
// MARK: - Init
3031
init() {
@@ -43,6 +44,7 @@ final class AppCompositionRoot {
4344
self.mainDIContainer = MainDIContainer(apiService: apiService,
4445
locationStateHolder: locationStateHolder)
4546
self.lockScreenDIContainer = LockScreenDIContainer(apiService: apiService)
47+
self.introDIContainer = IntroDIContainer()
4648
}
4749
}
4850

@@ -51,7 +53,8 @@ extension AppCompositionRoot: SplashCoordinatorFactory,
5153
LoginCoordinatorFactory,
5254
OnboardingCoordinatorFactory,
5355
MainCoordinatorFactory,
54-
LockScreenCoordinatorFactory {
56+
LockScreenCoordinatorFactory,
57+
IntroCoordinatorFactory {
5558
func makeSplashCoordinator(navigationController: UINavigationController) -> SplashCoordinator {
5659
return splashDIContainer.makeSplashCoordinator(navigationController: navigationController)
5760
}
@@ -71,4 +74,8 @@ extension AppCompositionRoot: SplashCoordinatorFactory,
7174
func makeLockScreenCoordinator(navigationController: UINavigationController) -> LockScreenCoordinator {
7275
return lockScreenDIContainer.makeLockScreenCoordinator(navigationController: navigationController)
7376
}
77+
78+
func makeIntroCoordinator(navigationController: UINavigationController) -> IntroCoordinator {
79+
return introDIContainer.makeIntroCoordinator(navigationController: navigationController)
80+
}
7481
}

Atcha-iOS/App/DIContainer/AppDIContainer.swift

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ final class AppDIContainer {
2121
let mainDIContainer: MainDIContainer
2222
let onboardingDIContainer: OnboardingDIContainer
2323
let lockScreenDIContainer: LockScreenDIContainer
24+
let introDIContainer: IntroDIContainer
2425

2526
let locationStateHolder: LocationStateHolder
2627

@@ -38,7 +39,8 @@ final class AppDIContainer {
3839
self.onboardingDIContainer = compositionRoot.onboardingDIContainer
3940
self.mainDIContainer = compositionRoot.mainDIContainer
4041
self.lockScreenDIContainer = compositionRoot.lockScreenDIContainer
41-
42+
self.introDIContainer = compositionRoot.introDIContainer
43+
4244
// Shared state holders
4345
self.locationStateHolder = compositionRoot.locationStateHolder
4446
}

Atcha-iOS/App/DIContainer/CoordinatorFactory.swift

Lines changed: 11 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,14 +32,19 @@ protocol LockScreenCoordinatorFactory {
3232
func makeLockScreenCoordinator(navigationController: UINavigationController) -> LockScreenCoordinator
3333
}
3434

35+
protocol IntroCoordinatorFactory {
36+
func makeIntroCoordinator(navigationController: UINavigationController) -> IntroCoordinator
37+
}
38+
3539
/// A convenience alias that groups all coordinator factory protocols used to bootstrap flows.
36-
typealias AppCoordinatorFactory = SplashCoordinatorFactory & LoginCoordinatorFactory & OnboardingCoordinatorFactory & MainCoordinatorFactory & LockScreenCoordinatorFactory
40+
typealias AppCoordinatorFactory = SplashCoordinatorFactory & LoginCoordinatorFactory & OnboardingCoordinatorFactory & MainCoordinatorFactory & LockScreenCoordinatorFactory & IntroCoordinatorFactory
3741

3842
extension AppDIContainer: SplashCoordinatorFactory,
3943
LoginCoordinatorFactory,
4044
OnboardingCoordinatorFactory,
4145
MainCoordinatorFactory,
42-
LockScreenCoordinatorFactory {
46+
LockScreenCoordinatorFactory,
47+
IntroCoordinatorFactory {
4348
func makeSplashCoordinator(navigationController: UINavigationController) -> SplashCoordinator {
4449
return splashDIContainer.makeSplashCoordinator(navigationController: navigationController)
4550
}
@@ -59,5 +64,9 @@ extension AppDIContainer: SplashCoordinatorFactory,
5964
func makeLockScreenCoordinator(navigationController: UINavigationController) -> LockScreenCoordinator {
6065
return lockScreenDIContainer.makeLockScreenCoordinator(navigationController: navigationController)
6166
}
67+
68+
func makeIntroCoordinator(navigationController: UINavigationController) -> IntroCoordinator {
69+
return introDIContainer.makeIntroCoordinator(navigationController: navigationController)
70+
}
6271
}
6372

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
//
2+
// LoginDIContainer.swift
3+
// Atcha-iOS
4+
//
5+
// Created by geonhui Yu on 6/23/25.
6+
//
7+
8+
import UIKit
9+
import Foundation
10+
11+
final class IntroDIContainer {
12+
func makeIntroViewModel() -> IntroViewModel {
13+
IntroViewModel()
14+
}
15+
16+
func makeIntroCoordinator(navigationController: UINavigationController) -> IntroCoordinator {
17+
IntroCoordinator(navigationController: navigationController,
18+
diContainer: self)
19+
}
20+
}

Atcha-iOS/App/DIContainer/Main/MainDIContainer.swift

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,10 @@ final class MainDIContainer {
4444
LockScreenDIContainer(apiService: apiService)
4545
}()
4646

47+
private lazy var loginDI: LoginDIContainer = {
48+
LoginDIContainer(apiService: apiService)
49+
}()
50+
4751
init(apiService: APIService, locationStateHolder: LocationStateHolder) {
4852
self.apiService = apiService
4953
self.locationStateHolder = locationStateHolder
@@ -116,3 +120,11 @@ extension MainDIContainer{
116120
return proximityDI
117121
}
118122
}
123+
124+
// MARK: - Login
125+
extension MainDIContainer{
126+
func makeLoginDIContainer() -> LoginDIContainer {
127+
return loginDI
128+
}
129+
}
130+

Atcha-iOS/App/DIContainer/User/Home/HomeRegisterDIContainer.swift

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ final class HomeRegisterDIContainer {
2323
private lazy var searchAddressUseCase: SearchAddressUseCase = SearchAddressUseCaseImpl(repository: addressRepository)
2424
private lazy var homePatchUseCase: HomePatchUseCase = HomePatchUseCaseImpl(repository: userRepository)
2525
private lazy var streamUseCase: ObserveLocationStreamUseCase = ObserLocationStreamUseCaseImpl(repository: LocationStreamRepositoryImpl())
26+
private lazy var signUpUseCase: SignUpUseCase = SignUpUseCaseImpl(repository: userRepository)
2627

2728
func makeHomeRegisterViewModel(context: HomeRegisterContext) -> HomeRegisterViewModel {
2829
return HomeRegisterViewModel(context: context,
@@ -40,7 +41,7 @@ final class HomeRegisterDIContainer {
4041
let viewModel = HomeFindViewModel(context: context,
4142
searchAddressUseCase: searchAddressUseCase,
4243
homePatchUseCase: homePatchUseCase,
43-
locationStateHolder: locationStateHolder, streamUseCase: streamUseCase)
44+
locationStateHolder: locationStateHolder, streamUseCase: streamUseCase, signUpUseCase: signUpUseCase)
4445
return viewModel
4546
}
4647

Atcha-iOS/Core/Manager/AlarmManager.swift

Lines changed: 18 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,7 @@ final class AlarmManager {
2626
// MARK: - State
2727
private var alarmVolume: Float = 1.0
2828
private var currentSoundFile: String?
29-
var selectedOption: PushAlarmOption = .onlySound
29+
var selectedOption: PushAlarmOption = .onlyVibration
3030

3131
private var interruptionObserver: NSObjectProtocol?
3232
private var silenceHintObserver: NSObjectProtocol?
@@ -40,6 +40,7 @@ final class AlarmManager {
4040
loadStoredAlarmOption()
4141
setupAudioSession()
4242
startObservingAudioSession()
43+
preloadPreviewSound()
4344
}
4445

4546
// MARK: - Public: Volume / Option
@@ -148,6 +149,16 @@ final class AlarmManager {
148149
}
149150
}
150151
}
152+
153+
private func preloadPreviewSound() {
154+
guard let url = Bundle.main.url(forResource: "siren", withExtension: "mp3") else { return }
155+
do {
156+
audioPlayer = try AVAudioPlayer(contentsOf: url)
157+
audioPlayer?.prepareToPlay()
158+
} catch {
159+
print("알람음 프리로드 실패")
160+
}
161+
}
151162
}
152163

153164
// MARK: - Private: Session / Storage
@@ -166,8 +177,7 @@ extension AlarmManager {
166177
selectedOption = option
167178
print("알람 타입 불러오기: \(option)")
168179
} else {
169-
selectedOption = .onlySound
170-
print("알람 타입 기본값 사용: onlySound")
180+
selectedOption = .onlyVibration
171181
}
172182
}
173183

@@ -532,6 +542,8 @@ extension AlarmManager {
532542
isPreviewing = true
533543
alarmVolume = volume
534544

545+
stopRepeatingVibration()
546+
535547
// 햅틱 엔진이 중단되어 있으면 재시작
536548
if selectedOption == .onlyVibration || selectedOption == .both {
537549
restartHapticEngine()
@@ -548,6 +560,9 @@ extension AlarmManager {
548560
}
549561

550562
case .onlyVibration:
563+
audioPlayer?.stop()
564+
audioPlayer = nil
565+
currentSoundFile = nil
551566
startRepeatingVibration()
552567
print("진동 미리듣기")
553568

Atcha-iOS/Core/Network/Token/TokenInterceptor.swift

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,11 @@ final class TokenInterceptor: RequestInterceptor, @unchecked Sendable {
3232
// completion(.success(request)); return
3333
// }
3434

35+
if path.contains("/auth/check") || path.contains("/auth/login") {
36+
completion(.success(request))
37+
return
38+
}
39+
3540
if path.contains("/auth/logout") {
3641
if let refreshToken = tokenStorage.refreshToken {
3742
request.setValue("Bearer \(refreshToken)", forHTTPHeaderField: "Authorization")

0 commit comments

Comments
 (0)