Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
41 commits
Select commit Hold shift + click to select a range
fcac45f
[FEAT] 비로그인 기능 Flow 추가
woolnd Feb 25, 2026
1983d73
[FEAT] 게스트모드 전용 route 수정
woolnd Feb 25, 2026
1bbd142
[FEAT] 비로그인 기능 차단 알람 추가
woolnd Feb 25, 2026
8f1d196
[REFACTOR] 인트로 Flow 수정
woolnd Mar 3, 2026
77214c3
[REFACTOR] 로그인 Flow 수정
woolnd Mar 3, 2026
c15a7e1
[REFACTOR] 로그아웃 및 회원탈퇴 Flow 수정
woolnd Mar 4, 2026
0eba6be
[REFACTOR] 로그인 시트 애니메이션 수정
woolnd Mar 4, 2026
35681c2
[FEAT] 온보딩 집주소 등록 토스트 추가
woolnd Mar 4, 2026
1dea4d8
[REFACTOR] 지도 회전 수정
woolnd Mar 4, 2026
98daf5f
[REFACTOR] 알림 권한 Flow 수정
woolnd Mar 4, 2026
b039014
[REFACTOR] 위치 권한 수정
woolnd Mar 4, 2026
9b733a9
[FEAT] 알람 설정 시트 추가 구현
woolnd Mar 4, 2026
cd8b5a2
[REFACTOR] 알람 설정 시트 타이밍 수정
woolnd Mar 4, 2026
2a629a3
[REFACTOR] 로그인 시트 디자인 수정
woolnd Mar 4, 2026
3c74d12
[REFACTOR] 알람 설정 시트 디자인 수정
woolnd Mar 4, 2026
58465d5
[REFACTOR] 인트로 이미지 및 타이틀 수정
woolnd Mar 4, 2026
b3bd93e
[REFACTOR] 비로그인 Flow 수정
woolnd Mar 4, 2026
a7c3e63
[REFACTOR] 지도 줌 수치 조정
woolnd Mar 4, 2026
6d18ed4
Merge pull request #294 from Atcha-Project/feat/#293
woolnd Mar 4, 2026
16ca478
[REFACTOR] 버튼 label 수정
woolnd Mar 4, 2026
26a8d90
[REFACTOR] 메인화면 지도 회전 기능 수정
woolnd Mar 5, 2026
dffb285
[REFACTOR] 메인화면 회전 기능 플래그 수정
woolnd Mar 5, 2026
021172e
[REFACTOR] 위치 업데이트 로직 수정
woolnd Mar 5, 2026
195059e
[REFACTOR] 비로그인 시 택시비 풍선 로직 수정
woolnd Mar 5, 2026
a83f144
[REFACTOR] 출발지 설정 풍선 텍스트 수정
woolnd Mar 5, 2026
6dbfb94
[REFACTOR] 소셜로그인 버튼 버튼 수정
woolnd Mar 5, 2026
fad5a6a
[REFACTOR] 알람 기본 값 진동으로 수정
woolnd Mar 5, 2026
c3ee38a
[REFACTOR] 출발전 상세경로 애니메이션 방지 코드 추가
woolnd Mar 5, 2026
d470403
[REFACTOR] 출발전 상세경로 대중교통 시간 노출 막기 코드 추가
woolnd Mar 5, 2026
b0fe2a7
[REFACTOR] 알람 울릴 시 바로 따라가기 기능 실행으로 수정
woolnd Mar 6, 2026
01a53f7
[REFACTOR] 알람 설정 슬라이더 두께, 색상 수정
woolnd Mar 6, 2026
edf5b67
[REFACTOR] 알람 초시 설정 슬라이더 추가
woolnd Mar 6, 2026
2466cff
[REFACTOR] 게스트 상태 관리 ViewModel 일원화
woolnd Mar 6, 2026
804fb2b
[REFACTOR] 로그아웃 시 게스트모드 전환 수정
woolnd Mar 6, 2026
0204338
[REFACTOR] 로그인 시 지도 위치 유지
woolnd Mar 6, 2026
b438b42
[REFACTOR] 알람 발생 시 지도 카메라 튐 현상 수정 및 초기 마커 오류 해결
woolnd Mar 6, 2026
12fa5ae
Merge pull request #296 from Atcha-Project/refactor/#295
woolnd Mar 6, 2026
999894a
Merge pull request #297 from Atcha-Project/env/dev
woolnd Mar 7, 2026
24cd5a2
[BUGFIX] 온보딩 후 게스트 모드 전환 코드 수정
woolnd Mar 7, 2026
644dc68
Merge pull request #299 from Atcha-Project/env/dev
woolnd Mar 7, 2026
cd94823
Merge branch 'env/live' into env/stage
woolnd Mar 7, 2026
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
72 changes: 64 additions & 8 deletions Atcha-iOS.xcodeproj/project.pbxproj

Large diffs are not rendered by default.

102 changes: 64 additions & 38 deletions Atcha-iOS/App/AppFlowCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -12,11 +12,13 @@ class AppFlowCoordinator {
private let container: AppDIContainer
private let window: UIWindow

// 메모리 유지를 위한 Coordinator 참조
private var splashCoordinator: SplashCoordinator?
private var mainCoordinator: MainCoordinator?
private var loginCoordinator: LoginCoordinator?
private var onboardingCoordinator: OnboardingCoordinator?
private var lockScreenCoordinator: LockScreenCoordinator?
private var introCoordinator: IntroCoordinator?


init(window: UIWindow, container: AppDIContainer) {
self.window = window
Expand All @@ -28,28 +30,27 @@ class AppFlowCoordinator {
window.rootViewController = navigationController
window.makeKeyAndVisible()

// 세션 만료 시: 모든 뷰를 엎고 다시 앱 시작
SessionController.shared.routeToLogin = { [weak self] in
self?.showLoginFlow()
DispatchQueue.main.async {
AppDIContainer.shared.tokenStorage.clearAllTokens()
UserDefaultsWrapper.shared.set(false, forKey: UserDefaultsWrapper.Key.hasSeenIntro.rawValue)
self?.startApp()
}
}

let splashCoordinator = container.makeSplashCoordinator(navigationController: navigationController)
splashCoordinator.routerHandler = { [weak self] router in
guard let self else { return }
guard let self = self else { return }
switch router {
case .login:
showLoginFlow()
case .intro:
showIntroFlow()
case .main:
showMainFlow()
case .onboarding:
showOnboardingFlow()
case .alarm(let info, let address):
showMainFlow(info: info, address: address, bottomType: .departure)
case .lockScreen(let info, let address):
showLockScreenFlow(info: info, address: address)
// case .realTime(let info, let address):
// showMainFlow(info: info, address: address, bottomType: .realTime)
// case .finishTime(let info, let address):
// showMainFlow(info: info, address: address, bottomType: .finish)
case .detailRoute(let lat, let lon, let address):
print("lat : \(lat), lon : \(lon), address : \(address)")
}
Expand All @@ -61,26 +62,63 @@ class AppFlowCoordinator {
private func showMainFlow(info: LegInfo? = nil,
address: String? = nil,
bottomType: MapBottomType = .search) {

let navigationController = UINavigationController()
window.rootViewController = navigationController
mainCoordinator = container.makeMainCoordinator(navigationController: navigationController)
mainCoordinator?.signoutFinish = { [weak self] in

let mainCoordinator = container.makeMainCoordinator(navigationController: navigationController)
self.mainCoordinator = mainCoordinator

// [신규 유저 온보딩 흐름]
// Main 화면(지도)을 깔아둔 상태에서, 그 위(Navigation 스택)에 온보딩을 얹습니다.
mainCoordinator.routeToOnboarding = { [weak self] in
guard let self = self else { return }
DispatchQueue.main.async {
self?.showLoginFlow()
let onboardingCoordinator = self.container.makeOnboardingCoordinator(navigationController: navigationController)

// 온보딩(회원가입)이 성공적으로 끝났을 때
onboardingCoordinator.onFinish = { [weak self] success in
DispatchQueue.main.async {
// 위에 쌓여있던 온보딩 화면들을 싹 치우고 밑에 깔려있던 Main(지도)으로 복귀!
navigationController.popToRootViewController(animated: true)

// 집 주소가 등록되었으니, MainVC를 찔러서 현위치/마커를 새로고침하게 합니다.
if let mainVC = navigationController.viewControllers.first as? MainViewController {
mainVC.viewModel.setupLocation()
mainVC.shouldShowWelcomeToast = true

mainVC.viewModel.isGuest = false
}

self?.onboardingCoordinator = nil
}
}

onboardingCoordinator.start()
self.onboardingCoordinator = onboardingCoordinator // 메모리 유지
}
}
mainCoordinator?.lockScreenConfrim = { [weak self] info, address in

// [락스크린 확인 흐름]
mainCoordinator.lockScreenConfrim = { [weak self] info, address in
DispatchQueue.main.async {
if let info, let address {
// self?.showMainFlow(info: info, address: address, bottomType: .realTime)
self?.showMainFlow(info: info, address: address, bottomType: .detail)
// TODO: 상세화면 연동 로직 적용하기
} else {
self?.showMainFlow()
}
}
}
mainCoordinator?.start(info: info, address: address, bottomType: bottomType)

// [회원 탈퇴 흐름]
mainCoordinator.withdrawFinish = { [weak self] in
DispatchQueue.main.async {
// 앱 데이터를 다 지웠으니, 스플래시부터 앱을 아예 새로 시작(리부팅)합니다!
self?.startApp()
}
}

mainCoordinator.start(info: info, address: address, bottomType: bottomType)
}

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

private func showLoginFlow() {
private func showIntroFlow() {
let navigationController = UINavigationController()
window.rootViewController = navigationController

let loginCoordinator = container.makeLoginCoordinator(navigationController: navigationController)
loginCoordinator.onFinishWithExistUser = { [weak self] isExist in
DispatchQueue.main.async {
isExist ? self?.showMainFlow() : self?.showOnboardingFlow()
}
}
loginCoordinator.start()
self.loginCoordinator = loginCoordinator
}

private func showOnboardingFlow() {
let navigationController = UINavigationController()
window.rootViewController = navigationController
let introCoordinator = container.makeIntroCoordinator(navigationController: navigationController)

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

onboardingCoordinator.start()
self.onboardingCoordinator = onboardingCoordinator
introCoordinator.start()
self.introCoordinator = introCoordinator
}
}
9 changes: 8 additions & 1 deletion Atcha-iOS/App/DIContainer/AppCompositionRoot.swift
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ final class AppCompositionRoot {
let onboardingDIContainer: OnboardingDIContainer
let mainDIContainer: MainDIContainer
let lockScreenDIContainer: LockScreenDIContainer
let introDIContainer: IntroDIContainer

// MARK: - Init
init() {
Expand All @@ -43,6 +44,7 @@ final class AppCompositionRoot {
self.mainDIContainer = MainDIContainer(apiService: apiService,
locationStateHolder: locationStateHolder)
self.lockScreenDIContainer = LockScreenDIContainer(apiService: apiService)
self.introDIContainer = IntroDIContainer()
}
}

Expand All @@ -51,7 +53,8 @@ extension AppCompositionRoot: SplashCoordinatorFactory,
LoginCoordinatorFactory,
OnboardingCoordinatorFactory,
MainCoordinatorFactory,
LockScreenCoordinatorFactory {
LockScreenCoordinatorFactory,
IntroCoordinatorFactory {
func makeSplashCoordinator(navigationController: UINavigationController) -> SplashCoordinator {
return splashDIContainer.makeSplashCoordinator(navigationController: navigationController)
}
Expand All @@ -71,4 +74,8 @@ extension AppCompositionRoot: SplashCoordinatorFactory,
func makeLockScreenCoordinator(navigationController: UINavigationController) -> LockScreenCoordinator {
return lockScreenDIContainer.makeLockScreenCoordinator(navigationController: navigationController)
}

func makeIntroCoordinator(navigationController: UINavigationController) -> IntroCoordinator {
return introDIContainer.makeIntroCoordinator(navigationController: navigationController)
}
}
4 changes: 3 additions & 1 deletion Atcha-iOS/App/DIContainer/AppDIContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ final class AppDIContainer {
let mainDIContainer: MainDIContainer
let onboardingDIContainer: OnboardingDIContainer
let lockScreenDIContainer: LockScreenDIContainer
let introDIContainer: IntroDIContainer

let locationStateHolder: LocationStateHolder

Expand All @@ -38,7 +39,8 @@ final class AppDIContainer {
self.onboardingDIContainer = compositionRoot.onboardingDIContainer
self.mainDIContainer = compositionRoot.mainDIContainer
self.lockScreenDIContainer = compositionRoot.lockScreenDIContainer

self.introDIContainer = compositionRoot.introDIContainer

// Shared state holders
self.locationStateHolder = compositionRoot.locationStateHolder
}
Expand Down
13 changes: 11 additions & 2 deletions Atcha-iOS/App/DIContainer/CoordinatorFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,14 +32,19 @@ protocol LockScreenCoordinatorFactory {
func makeLockScreenCoordinator(navigationController: UINavigationController) -> LockScreenCoordinator
}

protocol IntroCoordinatorFactory {
func makeIntroCoordinator(navigationController: UINavigationController) -> IntroCoordinator
}

/// A convenience alias that groups all coordinator factory protocols used to bootstrap flows.
typealias AppCoordinatorFactory = SplashCoordinatorFactory & LoginCoordinatorFactory & OnboardingCoordinatorFactory & MainCoordinatorFactory & LockScreenCoordinatorFactory
typealias AppCoordinatorFactory = SplashCoordinatorFactory & LoginCoordinatorFactory & OnboardingCoordinatorFactory & MainCoordinatorFactory & LockScreenCoordinatorFactory & IntroCoordinatorFactory

extension AppDIContainer: SplashCoordinatorFactory,
LoginCoordinatorFactory,
OnboardingCoordinatorFactory,
MainCoordinatorFactory,
LockScreenCoordinatorFactory {
LockScreenCoordinatorFactory,
IntroCoordinatorFactory {
func makeSplashCoordinator(navigationController: UINavigationController) -> SplashCoordinator {
return splashDIContainer.makeSplashCoordinator(navigationController: navigationController)
}
Expand All @@ -59,5 +64,9 @@ extension AppDIContainer: SplashCoordinatorFactory,
func makeLockScreenCoordinator(navigationController: UINavigationController) -> LockScreenCoordinator {
return lockScreenDIContainer.makeLockScreenCoordinator(navigationController: navigationController)
}

func makeIntroCoordinator(navigationController: UINavigationController) -> IntroCoordinator {
return introDIContainer.makeIntroCoordinator(navigationController: navigationController)
}
}

20 changes: 20 additions & 0 deletions Atcha-iOS/App/DIContainer/Intro/IntroDIContainer.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
//
// LoginDIContainer.swift
// Atcha-iOS
//
// Created by geonhui Yu on 6/23/25.
//

import UIKit
import Foundation

final class IntroDIContainer {
func makeIntroViewModel() -> IntroViewModel {
IntroViewModel()
}

func makeIntroCoordinator(navigationController: UINavigationController) -> IntroCoordinator {
IntroCoordinator(navigationController: navigationController,
diContainer: self)
}
}
12 changes: 12 additions & 0 deletions Atcha-iOS/App/DIContainer/Main/MainDIContainer.swift
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ final class MainDIContainer {
LockScreenDIContainer(apiService: apiService)
}()

private lazy var loginDI: LoginDIContainer = {
LoginDIContainer(apiService: apiService)
}()

init(apiService: APIService, locationStateHolder: LocationStateHolder) {
self.apiService = apiService
self.locationStateHolder = locationStateHolder
Expand Down Expand Up @@ -116,3 +120,11 @@ extension MainDIContainer{
return proximityDI
}
}

// MARK: - Login
extension MainDIContainer{
func makeLoginDIContainer() -> LoginDIContainer {
return loginDI
}
}

Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ final class HomeRegisterDIContainer {
private lazy var searchAddressUseCase: SearchAddressUseCase = SearchAddressUseCaseImpl(repository: addressRepository)
private lazy var homePatchUseCase: HomePatchUseCase = HomePatchUseCaseImpl(repository: userRepository)
private lazy var streamUseCase: ObserveLocationStreamUseCase = ObserLocationStreamUseCaseImpl(repository: LocationStreamRepositoryImpl())
private lazy var signUpUseCase: SignUpUseCase = SignUpUseCaseImpl(repository: userRepository)

func makeHomeRegisterViewModel(context: HomeRegisterContext) -> HomeRegisterViewModel {
return HomeRegisterViewModel(context: context,
Expand All @@ -40,7 +41,7 @@ final class HomeRegisterDIContainer {
let viewModel = HomeFindViewModel(context: context,
searchAddressUseCase: searchAddressUseCase,
homePatchUseCase: homePatchUseCase,
locationStateHolder: locationStateHolder, streamUseCase: streamUseCase)
locationStateHolder: locationStateHolder, streamUseCase: streamUseCase, signUpUseCase: signUpUseCase)
return viewModel
}

Expand Down
21 changes: 18 additions & 3 deletions Atcha-iOS/Core/Manager/AlarmManager.swift
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@ final class AlarmManager {
// MARK: - State
private var alarmVolume: Float = 1.0
private var currentSoundFile: String?
var selectedOption: PushAlarmOption = .onlySound
var selectedOption: PushAlarmOption = .onlyVibration

private var interruptionObserver: NSObjectProtocol?
private var silenceHintObserver: NSObjectProtocol?
Expand All @@ -40,6 +40,7 @@ final class AlarmManager {
loadStoredAlarmOption()
setupAudioSession()
startObservingAudioSession()
preloadPreviewSound()
}

// MARK: - Public: Volume / Option
Expand Down Expand Up @@ -148,6 +149,16 @@ final class AlarmManager {
}
}
}

private func preloadPreviewSound() {
guard let url = Bundle.main.url(forResource: "siren", withExtension: "mp3") else { return }
do {
audioPlayer = try AVAudioPlayer(contentsOf: url)
audioPlayer?.prepareToPlay()
} catch {
print("알람음 프리로드 실패")
}
}
}

// MARK: - Private: Session / Storage
Expand All @@ -166,8 +177,7 @@ extension AlarmManager {
selectedOption = option
print("알람 타입 불러오기: \(option)")
} else {
selectedOption = .onlySound
print("알람 타입 기본값 사용: onlySound")
selectedOption = .onlyVibration
}
}

Expand Down Expand Up @@ -532,6 +542,8 @@ extension AlarmManager {
isPreviewing = true
alarmVolume = volume

stopRepeatingVibration()

// 햅틱 엔진이 중단되어 있으면 재시작
if selectedOption == .onlyVibration || selectedOption == .both {
restartHapticEngine()
Expand All @@ -548,6 +560,9 @@ extension AlarmManager {
}

case .onlyVibration:
audioPlayer?.stop()
audioPlayer = nil
currentSoundFile = nil
startRepeatingVibration()
print("진동 미리듣기")

Expand Down
5 changes: 5 additions & 0 deletions Atcha-iOS/Core/Network/Token/TokenInterceptor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,11 @@ final class TokenInterceptor: RequestInterceptor, @unchecked Sendable {
// completion(.success(request)); return
// }

if path.contains("/auth/check") || path.contains("/auth/login") {
completion(.success(request))
return
}

if path.contains("/auth/logout") {
if let refreshToken = tokenStorage.refreshToken {
request.setValue("Bearer \(refreshToken)", forHTTPHeaderField: "Authorization")
Expand Down
Loading