[Auth] #20 OAuth 웹 로그인과 토큰 갱신 흐름 정리#29
Conversation
- Tools/TokenGenerator.swift: Mode 1.tokens.json 을 Swift 토큰으로 변환하는 단일 Swift 스크립트 - ShapeStyle+: brand(primary/secondary/beige/neutral 50~900 + primary alpha 8) / semantic(text/border/surface/bg) / status(error/warning + alpha) 총 69개 컬러 자동 생성 - CGFloat+Radius+: .none / .default / .full - CGFloat+Spacing+: .s0 ~ .s96 (12종) - Color+: init(hex:alpha:) 시그니처에 alpha 인자 추가 - Tools/README.md: 사용법 + 디자이너 핸드오프 흐름
- splashLogo.gif 추가 (Splash 화면용) - Service 레이어 Base.swift 제거, Common/Encodable+.swift 로 분리
- TimeSpot → Picke 네이밍 정리 (PickeCustomButtonConfig) - enableBackgroundColor: navy900 → primary500 (브랜드 CTA) - enableFontColor: gray100 → bgDefault - disableFontColor: gray900 → neutral500 - disableBackgroundColor: enableColor → neutral100 - cornerRadius 30 → .full (capsule) - typography pretendardFont(.SemiBold, 20) → pretendardCustomFont(.headingLarge) 추후 Figma CTA 디자인(node 3888:4287) 확인 후 hover/pressed 상태 정밀 조정 예정.
- TokenGenerator에 Component 트리 재귀 워커 + semantic alias 확장 + hex 인덱스 fallback 도입 - Component 토큰이 inline hex로 export 돼도 aliasData 또는 brand hex 매칭으로 brand var 참조 자동화 - UI/Token/ComponentToken.swift 자동 생성 (button/input/bedge 색·radius)
- Sources/Extension/UI/{Button,Navigaion}/ → Sources/UI/{Button,Navigaion}/ 이동
- CTAButtonVariant이 ComponentToken.Button.Primary.{Background,Text} 참조하도록 변경
- pressed 상태가 디자인 토큰의 primary800 색을 사용하도록 (opacity 변경 제거)
- TokenGenerator 입력/출력 파일과 Component 토큰 해석 우선순위 명시 - DesignSystem Sources 폴더 구조 (Color/Extension/UI/Token)와 새 파일 추가 시 tuist 재생성 절차 정리 - CTA 버튼 두 API 병행 정책과 pressed 상태 토큰 사용 규칙 문서화
- SplashFeature에 loading 상태와 onAppear ViewAction을 추가하고 continuousClock으로 0.3초 지연 - SplashView가 SDWebImageSwiftUI의 AnimatedImage로 splashLogo.gif를 250x250으로 표시
- DependencyPlugin Modules.swift에 Auth 케이스 추가 - Presentation 통합 모듈이 Auth를 의존성으로 묶고 @_exported import로 노출 - Projects/Presentation/Auth/ 신설 — AuthCoordinator/LoginFeature/LoginView 초기 스캐폴드와 테스트 타겟
- AppReducer 상태에 auth(AuthCoordinator.State)를 추가하고 splash.onAppear → presentAuth 트리거 연결 - completeAuthTransition이 state를 auth(.init())로 전환하도록 활성화 - App Project가 Shared(Shared) 의존성을 명시하고 AppView 배경을 primary50으로 갱신
- SWYP-Find/design-tokens 의 Mode 1.tokens.json 을 가져와 TokenGenerator를 돌리고 변경이 있으면 develop 대상 PR 생성 - workflow_dispatch · 매일 10:00 KST 스케줄 · design-tokens 측 repository_dispatch 트리거 지원 - 동작에 secrets.DESIGN_TOKENS_TOKEN (private repo 읽기 권한 PAT) 필요
- 일일 스케줄 제거하고 workflow_dispatch 와 repository_dispatch(design-tokens-updated) 만 유지 - 디자인 토큰 레포의 push 알림이 도착할 때 실 변경분만 PR로 올라가도록 정리
- Tools/README.md: 단일 소스(design-tokens repo) 표기, 자동화 트리거 흐름과 수동 명령 예시, ComponentToken 출력 추가 - AGENTS.md: 디자이너 핸드오프를 push → notify → sync 자동 체인으로 갱신
루트에 생성되는 Picke-*.png 그래프와 .tuist-spider/ 도구 산출물을 gitignore 에 추가해 워크트리 노이즈를 줄이고 PR 변경에서 제외한다. docs/graph/ 경로도 같이 무시해 그래프 PNG 보관용 디렉터리가 추적되지 않도록 한다.
…thTokens + UserSession + LoginEntity) #20 OAuth 플로우에서 Provider 간 공유할 페이로드/세션 모델을 Entity 계층에 정리한다. - AppleOAuthPayload/GoogleOAuthPayload/KakaoOAuthPayload: 각 Provider 별 응답 - AuthError: 인증 단계별 표준 에러 - AuthTokens / LoginEntity: 백엔드 로그인 응답 모델 - UserSession: 메모리 공유 세션 상태 - SocialType: 통합 소셜 타입 enum (.apple/.google/.kakao/.none)
DomainInterface 에 Provider 별 Repository 프로토콜과 Provider 프로토콜, WeaveDI DependencyKey, DependencyValues 확장, 그리고 단위 테스트/Preview 용 Mock 을 추가한다. - Apple: AppleOAuthInterface / AppleAuthRequestInterface / AppleOAuthProviderInterface + Mock - Google: GoogleOAuthInterface / GoogleOAuthProviderInterface + Mock - Kakao: KakaoOAuthInterface / KakaoOAuthProviderInterface + Mock (signInWithToken 시그니처로 통일) - DefaultAppleAuthRequestImpl: ASAuthorizationAppleIDRequest 기본 빌더
ASAuthorizationController(Apple) / GoogleSignIn SDK(Google) / ASWebAuthenticationSession + PKCE 서버 콜백(Kakao) 기반의 Repository 구현을 추가한다. - AppleLoginRepositoryImpl / AppleOAuthRepositoryImpl: AppleID credential + nonce 기반 로그인 - GoogleOAuthRepositoryImpl + GoogleOAuthConfiguration + GoogleLoginManager: 클라이언트 ID 구성/세션 관리 - KakaoOAuthRepository + KakaoAuthCodeStore: 카카오톡 앱/웹 authorize 분기, 서버 콜백 딥링크 수신 - 기존 Repository/Base.swift 더미 진입점 제거
Repository 를 직접 호출하지 않고 Provider 계층을 통해 도메인 정책을 캡슐화한다. - UnifiedOAuthUseCase: 소셜 타입별 분기(socialLogin/processOAuthFlow) + Apple/Google/Kakao 토큰 가드 통일 - AppleOAuthProvider: credential + nonce 기반 로그인 - GoogleOAuthProvider: signInWithToken 으로 토큰 진입점 통일 - KakaoOAuthProvider: signInWithToken(token:) 시그니처로 Google 패턴과 통일, UserSession 갱신 - Dependencies+OAuth: Provider 별 liveValue 등록
…AO_REST_API_KEY) #20 ASWebAuthenticationSession + 카카오톡 앱 분기를 위해 Info.plist 에 다음을 등록한다. - LSApplicationQueriesSchemes: kakaokompassauth, kakaolink — UIApplication.canOpenURL 허용 - KAKAO_REST_API_KEY: KakaoOAuthRepository 가 Bundle 에서 읽는 REST API 키 - KakaoSDK 미사용 구조라 kakao{NATIVE_APP_KEY} URL 스킴은 등록하지 않음
…sion Presentation Provider 추가 #20 ASWebAuthenticationSession 이 요구하는 presentationAnchor 를 단일 위치에서 공급하고, AppDIManager 에서 Apple/Google/Kakao Repository 와 Provider 를 한 번에 등록한다. - AppPresentationContextProvider: keyWindow → 첫 윈도우 → fallback 으로 anchor 결정 - DiRegister: KakaoOAuthRepository 는 @mainactor 격리 초기화라 MainActor.assumeIsolated 로 감싸 등록 - Provider 계층(AppleOAuthProvider/GoogleOAuthProvider/KakaoOAuthProvider) 등록 추가
스플래시 로고 애니메이션을 별도 컴포넌트로 분리하고, 인증 분기 후 LoginView 가 보이도록 진입 플로우를 정리한다. - AppReducer/ContentView: Auth Coordinator 분기 및 초기 라우팅 정리 - SplashView + SplashLogoAnimatedImageView: 로고 등장 애니메이션 분리 - LoginView: 소셜 로그인 진입 화면 (Apple/Google/Kakao 버튼 배치) - SocialCircleButtonView: 원형 소셜 버튼 공용 컴포넌트
로그인 UI 에서 사용할 자산과 ImageAsset enum 확장을 추가한다. - ImageAssets.xcassets/logo/loginLogo: PicK SVG 추가 - ImageAssets.xcassets/socialLogin: google.svg / kakao.svg 추가 - ImageAsset: loginLogo / google / kakao 케이스 추가 - ShapeStyle+: 로그인 화면에서 사용할 색상 헬퍼 보강
GoogleOAuthRepositoryImpl 가 GoogleSignIn SDK 를 사용하므로 Repository 모듈에 의존성을 명시한다. - Project.swift: .SPM.googleSignIn 추가 - GoogleOAuthRepositoryImpl: 들여쓰기를 프로젝트 컨벤션(2 space) 으로 정렬
KakaoOAuthRepositoryProtocol / KakaoAuthCodeStore 가 4-space 로 들어와 있어 다른 파일들과 스타일이 어긋났다. 동작 변경 없이 들여쓰기만 2-space 로 통일한다.
- LoginEntity 에 userTag/status 필드 추가, SocialType 에 redirectUri 컴퓨티드 추가 (kakao/google: picke.store) - AuthExitEntity / WithdrawEntity 신규 - AuthInterface(login(authorizationCode, redirectUri)/refresh/withdraw/logout/updateSessionCredential) + DefaultAuthRepositoryImpl + MockAuthRepository - KakaoOAuthRepositoryDependencyKey.liveValue 를 UnifiedDI 폴백 패턴으로 변경 (fatalError 제거) - DomainInterface 모듈에 composableArchitecture 의존성 추가
- 공통 DTO: BaseResponseDTO<T>(statusCode/data/error 봉투), APIErrorDTO, BaseDataDTO 마커 프로토콜 - Auth Login: LoginDataDTO (access_token/refresh_token/user_tag/status/new_user) + LoginResponseDTO typealias + toDomain(provider:) - Auth Token: TokenDTO + RefreshResponseDTO typealias + toDomain - Auth Logout: LogOutDTO + toDomain → AuthExitEntity - Auth Withdraw: WithdrawDTO + toDomain(isSuccess:) → WithdrawEntity
- PieckeDomain enum (auth/profile) 신규 — AsyncMoya DomainType 채택, baseURL 분기 (기존 Base.swift placeholder 제거)
- BaseAPI 환경별 baseURL 노출, AuthAPI(login/refresh/withDraw/logout) enum
- AuthService: BaseTargetType 채택, login(provider:body:) → /api/v1/auth/login/{provider} 동적 경로, withdraw 는 .profile 도메인 사용
- OAuthLoginRequest(authorizationCode, redirectUri) 요청 바디
- Encodable+.toDictionary 가 JSONEncoder 슬래시 이스케이프(\/) 비활성 — 로그 가독성 개선
- Service Project.swift 에 Foundations / Model 의존성 추가 (APIHeader, DTO 접근)
- AuthRepositoryImpl: BaseResponseDTO 봉투를 풀어 LoginEntity 로 매핑, refresh/logout/withdraw 도 동일 패턴 - AccessTokenCredential: JWT exp 디코딩 기반 만료 시점 관리 + refreshLeadTime 5분 - AuthSessionManager / OptimizedSessionManager: Keychain 부터 credential 부트스트랩, 메모리 경고 시 정리 - AuthInterceptor: 요청 adapt 시 만료 임박이면 자동 refresh, 401 retry 시도 + RefreshTokenExpired 알림 발송 - TokenRefreshManager (actor): 동시 refresh 중복 방지 + 401 감지 시 자동 로그아웃 - MoyaProviderPool: TargetType 단위 Provider 재사용 캐시 (default/authorized) - MoyaProvider+Auth (.authorized 정적 빌더, OptimizedSessionManager 부착) + MoyaProvider+Response(requestResponse async) - Repository Project.swift: Service/Model/Foundations + asyncMoya/composableArchitecture/weaveDI/logMarco 의존성 명시
- AuthUseCaseImpl: authRepository 를 위임받아 keychain 저장 + UserSession 동기화 + session credential 갱신 흐름 통합 - UnifiedOAuthUseCase: apple/google/kakao 분기에서 authRepository.login(provider, authorizationCode, redirectUri) 호출 활성화 (기존 주석 처리 코드 제거) - 각 social 별 SocialType.redirectUri 자동 적용 (kakao 는 payload.redirectUri 우선)
- GoogleOAuthProvider.signInWithToken: idToken 이 아닌 payload.authorizationCode(serverAuthCode) 반환 → 백엔드 /api/v1/auth/login/google 의 authorizationCode 필드와 의미 일치, code 누락 시 invalidCredential 으로 조기 실패 - KakaoOAuthRepository: serverRedirectUri 를 https://picke.store/oauth/kakao 로 변경 (백엔드/카카오 콘솔 등록 URI 와 일치), appRedirectUri picke:// 로 통일 - KakaoAuthCodeStore: Entity import 추가
- .login(socialType:) 액션에서 socialType 에 맞춰 googleToken/kakaoToken 을 분기해 전달 (이전: kakaoToken 항상 nil → invalidCredential 으로 즉시 실패) - CancelID 매핑을 switch 로 명시 (apple/google/kakao) - 에러 토스트에 .kakao 케이스 추가
- AuthCoordinator/LoginView/SocialCircleButtonView 갱신 - 공용 Toast 시스템: ToastManager, ToastType(success/error/info), ToastView 추가 - DesignSystem ImageAsset 에 Auth 카테고리(checkBlue, errorXmark) 추가
- AppReducer.body 에 .ifCaseLet(\.auth) → AuthCoordinator 추가 - DiRegister 에서 주석 처리되어 있던 AuthRepositoryImpl as AuthInterface 등록 라인 활성화
- swift-package-manager-google-mobile-ads (12.0.0+) 신규 의존성 - GoogleMobileAds 를 staticFramework 지정 - #if TUIST 블록 들여쓰기를 프로젝트 컨벤션(2-space) 으로 정렬
- GoogleSignIn SDK 제거하고 카카오와 동일한 웹 OAuth 흐름으로 통일 - authorize URL (response_type=code, redirect_uri=https://picke.store/oauth/google) 을 ASWebAuthenticationSession 으로 열고 picke://oauth/google?code=... 콜백을 가로채 code 추출 - GoogleOAuthPayload 에 redirectUri 필드 추가, GoogleOAuthProviderInterface 반환 타입을 String → GoogleOAuthPayload 로 변경 (Kakao 와 동일) - GoogleAuthCodeStore actor 신규 (KakaoAuthCodeStore 와 동일 패턴) - GoogleOAuthRepositoryImpl 을 NSObject + ASWebAuthenticationPresentationContextProviding 주입 형태로 재작성 - DiRegister 에서 GoogleOAuthRepositoryImpl(presentationContextProvider:) 팩토리로 변경 - MockGoogleOAuthRepository / GoogleOAuthProvider mock 업데이트
- 서버가 client_secret 으로 token 교환을 담당하므로 모바일 측 PKCE(code_challenge / state / codeVerifier) 모두 제거
- KakaoOAuthRepository.buildAuthorizeURL 을 response_type/client_id/redirect_uri/prompt 4개 파라미터로 단순화
- parsePayload(from:) 가 picke:// 콜백에서 code(=ticket) 만 추출해 KakaoOAuthPayload.authorizationCode 로 반환
- KakaoOAuthPayload 에 토큰/사용자 정보 옵셔널 필드 정리, 기본 init 단순화
- UnifiedOAuthUseCase.googleLogin / kakaoLogin 이 payload.authorizationCode + payload.redirectUri 로 authRepository.login 호출 (POST /api/v1/auth/login/{provider})
- CryptoKit / Security import 제거, generatePKCE / encodeState / randomData / base64URLEncode 헬퍼 삭제
- Google/Kakao OAuth를 공용 WKWebView 바텀시트 흐름으로 통합 - Keychain service 기본값을 io.Picke.co로 변경 - 앱 아이콘 리소스와 프로젝트 템플릿 설정 변경 포함 Rejected: ASWebAuthenticationSession 유지 | Google WKWebView user-agent 요구와 모달 UX 요구를 동시에 반영하기 어려움
- attendance-ios 방식처럼 refresh token 만료 알림 이름과 인터셉터 갱신 흐름을 정리 - 로그아웃 응답의 data.logged_out 값을 AuthExitEntity로 전달 - 회원탈퇴 응답의 data.withdrawn 값을 WithdrawEntity로 전달 - OAuth 모달 드래그 dismiss 흔들림을 window 좌표 기반으로 수정 Rejected: 기존 flat logout/withdraw DTO 유지 | 실제 응답의 data 필드를 보존할 수 없음
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 2cdbb2ec10
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| case .withDraw: | ||
| return "" |
There was a problem hiding this comment.
Return withdraw endpoint path instead of empty string
AuthService.withdraw derives its route from AuthAPI.withDraw.description, but this case currently returns an empty string. That makes the DELETE request target the domain root (/api/v1/me/) rather than the withdraw endpoint, so account deletion requests will hit the wrong API path and fail or execute unintended behavior.
Useful? React with 👍 / 👎.
| guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential else { | ||
| signInContinuation?.resume(throwing: AuthError.invalidCredential("Invalid credential type")) | ||
| signInContinuation = nil | ||
| return |
There was a problem hiding this comment.
Reset Apple sign-in lock flag on all early-return failures
In didCompleteWithAuthorization, this early-failure branch resumes the continuation and returns, but it does not reset isSigningIn (or currentNonce). If any of these guard checks fail once, subsequent signIn() calls will keep hitting the isSigningIn gate and immediately fail as "already signing in," effectively wedging Apple login until app restart.
Useful? React with 👍 / 👎.
| if (200 ... 299).contains(response.statusCode) { | ||
| keychainManager.clear() | ||
| if response.data.isEmpty { return AuthExitEntity(loggedOut: true) } |
There was a problem hiding this comment.
Clear in-memory auth credentials on logout success
On successful logout, only keychainManager.clear() is called, but the in-memory credentials held by AuthSessionManager/OptimizedSessionManager are left intact. Since the auth interceptor reads AuthSessionManager.shared.credential, post-logout authorized requests can still carry the stale bearer token until process restart or refresh, causing inconsistent logged-out behavior.
Useful? React with 👍 / 👎.
- BASE_URL 기준으로 Google/Kakao OAuth redirect URI와 callback host를 구성 - 회원 탈퇴 endpoint path를 withdraw로 복구 - Apple 로그인 실패 분기와 로그아웃 성공 시 로컬 세션 정리를 보강 Rejected: 별도 OAUTH_REDIRECT_BASE_URL 추가 | 기존 BASE_URL 설정 체계를 중복시키므로 제외
- AuthAPI.withDraw description을 빈 문자열로 되돌려 profile domain root 요청을 유지 Rejected: withdraw path 명시 | 현재 서버 계약과 맞지 않아 제외
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 4f4d65360f
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
| // MARK: - 회원 탈퇴 | ||
|
|
||
| public func withDraw(token: String) async throws -> WithdrawEntity { | ||
| let response = try await provider.requestResponse(.withdraw(token: token)) |
There was a problem hiding this comment.
Route withdraw through authorized provider
withDraw sends the request via provider, which is the default MoyaProvider without the auth interceptor attached. That bypasses the refresh/retry path used for authenticated requests, so if the access token is expired the account-deletion call will fail with 401 instead of refreshing credentials first. This is especially visible for long-lived sessions where logout works (via authProvider) but withdraw does not.
Useful? React with 👍 / 👎.
|
|
||
| public func withDraw(token: String) async throws -> WithdrawEntity { | ||
| let result = try await authRepository.withDraw(token: token) | ||
| keychainManager.clear() |
There was a problem hiding this comment.
Clear local credentials only after successful withdraw
withDraw always clears the keychain even when account deletion fails. In this codebase, authRepository.withDraw returns a failure WithdrawEntity for non-2xx responses instead of throwing, so this unconditional clear logs users out even when the server rejected deletion (or a transient backend error occurred), leaving local auth state inconsistent with backend account state.
Useful? React with 👍 / 👎.
변경 내용
io.Picke.co로 맞췄습니다.statusCode / data.logged_out / error구조에 맞췄습니다.statusCode / data.withdrawn / error구조에 맞췄습니다.영향
검증
xcodebuild -workspace Picke.xcworkspace -scheme UseCase -destination 'generic/platform=iOS Simulator' buildxcodebuild -workspace Picke.xcworkspace -scheme Repository -destination 'generic/platform=iOS Simulator' buildxcodebuild -workspace Picke.xcworkspace -scheme Picke -destination 'generic/platform=iOS Simulator' build미검증