Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
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
2 changes: 1 addition & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -87,7 +87,7 @@ AtchaV2(앱, 조합 루트: 어댑터·스플래시·AlarmSyncService) ─► Ho
### 미완 상태 (작업 시 참고)

- **인증은 게스트 계정 기반(2026-09-24 전환)** — 앱 시작 시 토큰이 없으면 `AppCoordinator.signInAsGuest()`가 `POST /auth/guest` `{deviceId, fcmToken?}`로 JWT를 받아 세션에 채택하고 곧장 홈으로 간다(`DefaultSignInAsGuestUseCase`, plain client 전용 — 토큰이 없는 것이 정상이라 `publicPathSuffixes`에 등록). **로그인 화면 단계가 없다.** `deviceId`는 게스트 계정의 유일한 신원이므로 `KeychainDeviceIdentifierAdapter`가 IDFV를 **키체인에 고정**한다 — IDFV는 앱 전체 삭제 후 재설치 시 값이 바뀌어 기존 계정에 영영 못 돌아가기 때문(키체인 항목은 앱 삭제 후 잔존). 토큰 만료도 로그인 화면이 아니라 조용한 재인증으로 끝난다: `GET /auth/reissue` 실패 → `AppCoordinator.handleSessionExpiry` → 같은 deviceId로 `/auth/guest` 재호출 → 서버가 같은 계정을 돌려준다.
- **소셜 로그인(카카오·애플) 코드는 보존돼 있으나 호출처가 없다** — `AuthFeature`·`SignInUseCase`·`SocialLoginAdapter`·`AppCoordinator.showLogin()`(`@available(deprecated)` 표시)은 그대로 남겨뒀다. 계정 승격이 도입되면 그 경로가 진입점이 된다. 관련 API(`GET /auth/check`·`/auth/login`·`POST /auth/sign-up`, 소셜 토큰을 Bearer로)와 미가입 시 **최소 가입**(현재 위치+역지오코딩 주소, 실패 시 폴백 `""`·(0,0), 알림 빈도 [1,10]) 로직도 유지된다. 로그아웃(`POST /auth/logout` — refresh 토큰을 Bearer로 싣는 특례, CoreAuth `AuthSessionManager.signOut()`)·회원탈퇴(`DELETE /members/me`)·유저 정보(`GET /members/me`, 홈주소/알림빈도 PATCH — `UserRepository`)은 `SettingsFeature`(홈 상단 톱니바퀴 진입: 우리집 설정·약관·피드백·버전·로그아웃·계정 탈퇴)로 연결됐다. 알림 빈도는 레거시처럼 미노출. 로그아웃·탈퇴·강제 만료는 전부 `AppCoordinator.handleSessionExpiry`에 합류하고 거기서 `AlarmSessionTeardown`이 로컬 알람·LA·스냅샷·동기화 상태를 비운다(로그아웃은 토큰이 살아 있을 때 서버 알람 취소를 먼저 시도). 홈주소 변경은 `GET /locations/is-service-region` 통과 후 PATCH. 앱 버전은 `GET /app/version`(무토큰, 1.5초 예산·실패 시 통과)으로 권장 업데이트 팝업만. 카카오 키는 `TUIST_KAKAO_APP_KEY` env로 주입(미주입 시 카카오 버튼만 런타임 비활성). 잔여: 카카오/Apple 콘솔에 V2 번들 등록, 서버 audience 허용 확인. `DevDemoFallbacks`(실패 은폐 데모 폴백)와 "DEV 건너뛰기"는 제거됨 — DEV에 남은 것은 막차 변경 주입용 `DevChangeSimulator`(refresh 가로채기 전용, 에러 은폐 없음)뿐이라 서버 실패가 DEV에서도 그대로 표면화된다.
- **소셜 로그인은 V2에서 완전히 제거됐다(2026-09-24)** — `Projects/Feature/Auth` 모듈 전체, `SignInUseCase`, `SocialLoginAdapter`, `KakaoConfig`, 카카오 SDK 의존성(`KakaoSDK*`, `Tuist/Package.swift`의 kakao-ios-sdk), Apple Sign In entitlement, 카카오 URL 스킴·`LSApplicationQueriesSchemes`·`TUIST_KAKAO_APP_KEY`가 모두 사라졌다. 관련 서버 API(`/auth/check`·`/auth/login`·`/auth/sign-up`)와 Domain 계약(`SocialCredential`, `SocialLoginService`, `SignUpForm`)도 함께 제거. `Projects/Core/Auth`는 **세션·토큰 관리라 그대로 유지**된다(게스트 인증이 쓴다). 회원탈퇴(`DELETE /members/me`)·유저 정보(`GET /members/me`, 홈주소/알림빈도 PATCH — `UserRepository`)는 `SettingsFeature`(홈 상단 톱니바퀴 진입: 우리집 설정·약관·피드백·버전·로그아웃·계정 탈퇴)로 연결돼 있다. 알림 빈도는 레거시처럼 미노출. 로그아웃(`POST /auth/logout`, CoreAuth `AuthSessionManager.signOut()`)·탈퇴·강제 만료는 전부 `AppCoordinator.handleSessionExpiry`에 합류하고 거기서 `AlarmSessionTeardown`이 로컬 알람·LA·스냅샷·동기화 상태를 비운다. **다만 게스트 모델에서 로그아웃/탈퇴 직후 같은 deviceId로 재인증되므로 UX가 재검토 대상이다(미해결).** 홈주소 변경은 `GET /locations/is-service-region` 통과 후 PATCH. 앱 버전은 `GET /app/version`(무토큰, 1.5초 예산·실패 시 통과)으로 권장 업데이트 팝업만. `DevDemoFallbacks`와 "DEV 건너뛰기"는 제거됨 — DEV에 남은 것은 막차 변경 주입용 `DevChangeSimulator`(refresh 가로채기 전용, 에러 은폐 없음)뿐이라 서버 실패가 DEV에서도 그대로 표면화된다.
- `AppEnvironment.apiBaseURL`은 **세 환경 모두 `https://atcha.kro.kr/api` 단일값**(2026-09-24 확정). 이전 호스트 둘은 모두 죽었다 — `atcha.online`은 NXDOMAIN, `atcha.p-e.kr`은 443 연결 불가. **`/api` 접두어는 base URL에서만 붙인다** — 각 `Endpoint.path`는 접두어를 모르므로(`/auth/guest`, `/routes/user-routes` …) 서버가 접두어 체계를 바꾸면 이 프로퍼티 한 줄만 고친다. 환경별 호스트가 다시 생기면 `switch`로 되돌리면 된다.
- **네트워크 진단**: `URLSessionNetworkClient`가 DEBUG에서 `com.atcha.network` 서브시스템에 `메서드 · URL · 상태코드/에러 · 소요시간`을 남긴다(헤더·본문은 토큰이 실리므로 절대 로깅 금지). `NetworkError.debugDescription`이 URLError 코드를 노출해 타임아웃(-1001)·연결 불가(-1004)·TLS 실패(-1200)를 가른다 — 이전에는 전부 `.transport`로 뭉개져 원인 구분이 불가능했다. 부트스트랩 실패는 `BootstrapFailureMessage`를 거쳐 스플래시의 `showRetry`로 표면화된다(게스트 전환으로 부트스트랩이 비동기가 되면서 비로소 쓰이는 경로).
- `Projects/App/Resources/GoogleService-Info.plist`는 **레거시 번들 ID(`com.atcha.iOS`)용 파일**이라 존재 가드만 통과할 뿐 V2(`com.atcha.iOS.v2`)로의 사일런트 푸시가 성립하지 않는다 — V2용 재발급·교체 필요. FCM 토큰은 로그인/가입 파라미터로만 서버에 가고, 갱신 전달은 `SyncPushTokenUseCase`까지 배선됐지만 서버 API 미확정이라 `UnconfirmedPushTokenRepository`(no-op)가 주입돼 있다. 그래서 갱신 채널은 현재 폴링(앱 시작·포그라운드 복귀)과 홈 pull-to-refresh(수동)뿐.
Expand Down
26 changes: 1 addition & 25 deletions Projects/App/Project.swift
Original file line number Diff line number Diff line change
@@ -1,12 +1,7 @@
import ProjectDescription
import ProjectDescriptionHelpers

// 카카오 네이티브 앱 키 — xcconfig 금지 규약(AtchaV2) 하에서 env(TUIST_KAKAO_APP_KEY)로
// 주입한다(mise [env] 또는 CI secret). 미주입이면 빈 문자열: generate/빌드는 통과하고
// 카카오 로그인만 런타임 비활성(providerUnavailable → 실패 토스트)이다.
let kakaoAppKey = Environment.kakaoAppKey.getString(default: "")

var appInfoPlist: [String: Plist.Value] = [
let appInfoPlist: [String: Plist.Value] = [
"CFBundleDisplayName": "앗차",
"UILaunchStoryboardName": "LaunchScreen",
"NSAlarmKitUsageDescription": "막차 시간에 맞춰 알람을 울리기 위해 권한이 필요합니다.",
Expand All @@ -27,17 +22,7 @@ var appInfoPlist: [String: Plist.Value] = [
],
"UISupportedInterfaceOrientations": ["UIInterfaceOrientationPortrait"],
"ITSAppUsesNonExemptEncryption": false,
// 카카오톡 로그인 앱 전환 조회용(레거시 Info.plist 실측).
"LSApplicationQueriesSchemes": ["kakaokompassauth", "kakaolink"],
// 런타임(KakaoConfig)이 읽는 키 — 빈 값이면 KakaoSDK.initSDK를 건너뛴다.
"KAKAO_APP_KEY": .string(kakaoAppKey),
]
// 키 미주입 시 무의미한 `kakao` 빈 스킴이 등록되지 않도록 조건 분기.
if !kakaoAppKey.isEmpty {
appInfoPlist["CFBundleURLTypes"] = [
["CFBundleURLSchemes": ["kakao\(kakaoAppKey)"]],
]
}

let appTarget = Target.target(
name: "AtchaV2",
Expand All @@ -54,13 +39,9 @@ let appTarget = Target.target(
"aps-environment": "development",
// 폴백 노티의 .timeSensitive interruptionLevel용(Phase 15) — 집중 모드 관통.
"com.apple.developer.usernotifications.time-sensitive": true,
// 소셜 로그인(Apple) — App ID capability 활성 + 프로비저닝 갱신 필요(콘솔 작업).
"com.apple.developer.applesignin": ["Default"],
]),
dependencies: [
.target(name: "AtchaWidget"),
.project(target: "AuthFeature", path: "../Feature/Auth"),
.project(target: "AuthFeatureInterface", path: "../Feature/Auth"),
.project(target: "HomeFeature", path: "../Feature/Home"),
.project(target: "HomeFeatureInterface", path: "../Feature/Home"),
.project(target: "SearchFeature", path: "../Feature/Search"),
Expand All @@ -79,11 +60,6 @@ let appTarget = Target.target(
.external(name: "FirebaseCore"),
.external(name: "FirebaseCrashlytics"),
.external(name: "FirebaseMessaging"),
// 소셜 SDK는 앱 타겟에서만 링크(내부 모듈 static framework 중복 심볼 방지 규약)
// — import 지점은 SocialLoginAdapter·AppDelegate·SceneDelegate뿐.
.external(name: "KakaoSDKCommon"),
.external(name: "KakaoSDKAuth"),
.external(name: "KakaoSDKUser"),
],
settings: .atchaV2(base: [
// Firebase static libraries under XcodeProj-based integration need
Expand Down
127 changes: 0 additions & 127 deletions Projects/App/Sources/Adapters/SocialLoginAdapter.swift

This file was deleted.

38 changes: 3 additions & 35 deletions Projects/App/Sources/AppCoordinator.swift
Original file line number Diff line number Diff line change
Expand Up @@ -14,8 +14,6 @@ final class AppCoordinator: Coordinator, CoordinatorFinishDelegate {
/// 게스트 부트스트랩 in-flight — 만료 스트림의 중복 yield나 재시도 연타에
/// /auth/guest를 겹쳐 쏘지 않는다.
private var bootstrapTask: Task<Void, Never>?
/// 로그인 플로우 표시 중 가드 — 만료 스트림의 중복 yield에 로그인을 겹치지 않는다.
private var isShowingLogin = false
/// 세션 만료 경유 재인증이면 성공 직후 수동 동기화 1회 — activate()의 시작
/// 동기화는 최초 1회 가드라 재인증 경로에선 돌지 않기 때문.
private var needsSyncAfterLogin = false
Expand Down Expand Up @@ -111,38 +109,8 @@ final class AppCoordinator: Coordinator, CoordinatorFinishDelegate {
}
}

/// 소셜 로그인 시트 — 스플래시를 root로 유지한 채 present한다
/// (스플래시 배경 위 바텀시트 = 레거시와 같은 시각 결과).
///
/// 게스트 인증 전환(2026-09-24) 이후 **호출처가 없다.** 서버가 게스트 계정으로
/// 부트스트랩을 처리하므로 강제 로그인 단계 자체가 사라졌다. 소셜 계정 승격이
/// 도입되면 이 경로가 그대로 진입점이 되므로 AuthFeature와 함께 남겨둔다.
@available(*, deprecated, message: "게스트 부트스트랩으로 대체됨. 소셜 계정 승격 도입 시 재사용.")
private func showLogin() {
guard !isShowingLogin else { return }
isShowingLogin = true
let loginCoordinator = container.makeAuthDIContainer().makeLoginCoordinator(
navigationController: navigationController,
onAuthenticated: { [weak self] in
guard let self else { return }
self.isShowingLogin = false
self.startHome()
self.container.alarmSyncService.activate()
self.syncPushTokenAfterLogin()
if self.needsSyncAfterLogin {
self.needsSyncAfterLogin = false
let syncService = self.container.alarmSyncService
Task { await syncService.syncNow() }
}
}
)
loginCoordinator.finishDelegate = self
addChild(loginCoordinator)
loginCoordinator.start()
}

/// refresh 확정 사망(AuthSessionManager.sessionExpired) 관찰 — 홈을 접고 스플래시
/// 위 로그인으로 되돌린다. 스트림은 단일 소비자(이 코디네이터) 전제.
/// refresh 확정 사망(AuthSessionManager.sessionExpired) 관찰 — 홈을 접고 스플래시로
/// 되돌린 뒤 게스트 재인증한다. 스트림은 단일 소비자(이 코디네이터) 전제.
private func observeSessionExpiry() {
sessionExpiryTask = Task { [weak self] in
guard let stream = self?.container.authSessionManager.sessionExpired else { return }
Expand All @@ -154,7 +122,7 @@ final class AppCoordinator: Coordinator, CoordinatorFinishDelegate {
}

private func handleSessionExpiry() {
guard bootstrapTask == nil, !isShowingLogin else { return }
guard bootstrapTask == nil else { return }
needsSyncAfterLogin = true
// 로그아웃·탈퇴·강제 만료 공통 합류점 — 이전 계정의 알람이 로그인 화면에서 울리지 않게
// 로컬 정리를 여기서 한 번 더 보장한다(로그아웃 경로의 선행 정리와 겹쳐도 멱등).
Expand Down
19 changes: 0 additions & 19 deletions Projects/App/Sources/AppDIContainer.swift
Original file line number Diff line number Diff line change
@@ -1,6 +1,4 @@
import AtchaData
import AuthFeature
import AuthFeatureInterface
import CoreAlarm
import CoreAuth
import CoreNetwork
Expand Down Expand Up @@ -157,23 +155,6 @@ final class AppDIContainer {
)
}

func makeAuthDIContainer() -> any AuthCoordinatorBuildable {
AuthDIContainer(
signInUseCase: DefaultSignInUseCase(
socialLoginService: SocialLoginAdapter(),
// plain client — 소셜 Bearer 보존 + 401이 세션 복구를 촉발하지 않게.
authRepository: AuthRepositoryImpl(networkClient: plainNetworkClient),
sessionStore: AuthSessionStoreAdapter(sessionManager: authSessionManager),
pushTokenProvider: FCMPushTokenAdapter(),
// 최소 가입 폼 재료 — 신규 계정일 때만 쓰인다(현재 위치 + 역지오코딩 주소).
getCurrentLocationUseCase: DefaultGetCurrentLocationUseCase(
locationService: CoreLocationServiceAdapter()
),
reverseGeocodeUseCase: DefaultReverseGeocodeUseCase(repository: placeRepository)
)
)
}

// MARK: - 계정 스택 (설정 화면 + DEV 디버그 메뉴의 로그아웃)

private var userRepository: any UserRepository {
Expand Down
Loading
Loading