From 1ace7f26df7df5d3daabdcba7347a97c3d277898 Mon Sep 17 00:00:00 2001 From: Roy Date: Wed, 17 Dec 2025 10:26:16 +0900 Subject: [PATCH 1/3] =?UTF-8?q?[chore]:=20oauthusecase=20=20=EA=B0=81=20?= =?UTF-8?q?=EC=86=8C=EC=85=9C=EC=97=90=20=EB=A7=9E=EA=B2=8C=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/UseCase/OAuth/OAuthUseCase.swift | 134 +++++------------- .../Protocols/OAuthProviderProtocol.swift | 27 ++++ .../OAuth/Providers/AppleOAuthProvider.swift | 83 +++++++++++ .../OAuth/Providers/GoogleOAuthProvider.swift | 47 ++++++ .../OAuth/Providers/KakaoOAuthProvider.swift | 36 +++++ 5 files changed, 229 insertions(+), 98 deletions(-) create mode 100644 Domain/Sources/UseCase/OAuth/Protocols/OAuthProviderProtocol.swift create mode 100644 Domain/Sources/UseCase/OAuth/Providers/AppleOAuthProvider.swift create mode 100644 Domain/Sources/UseCase/OAuth/Providers/GoogleOAuthProvider.swift create mode 100644 Domain/Sources/UseCase/OAuth/Providers/KakaoOAuthProvider.swift diff --git a/Domain/Sources/UseCase/OAuth/OAuthUseCase.swift b/Domain/Sources/UseCase/OAuth/OAuthUseCase.swift index 6723525..fac61e2 100644 --- a/Domain/Sources/UseCase/OAuth/OAuthUseCase.swift +++ b/Domain/Sources/UseCase/OAuth/OAuthUseCase.swift @@ -11,119 +11,57 @@ import LogMacro import AuthenticationServices public struct OAuthUseCase: OAuthUseCaseProtocol { + // ✅ Dependencies 시스템 유지 @Dependency(\.oAuthRepository) private var repository: OAuthRepositoryProtocol @Dependency(\.googleOAuthRepository) private var googleRepository: GoogleOAuthRepositoryProtocol @Dependency(\.appleOAuthRepository) private var appleRepository: AppleOAuthRepositoryProtocol @Dependency(\.kakaoOAuthRepository) private var kakaoRepository: KakaoOAuthRepositoryProtocol - + public init() {} - + + // ✅ 기존 시그니처와 동작 완전 동일 public func signInWithApple( credential: ASAuthorizationAppleIDCredential, nonce: String ) async throws -> UserProfile { - guard let identityTokenData = credential.identityToken, - let identityToken = String(data: identityTokenData, encoding: .utf8), - let authCode = String(data: credential.authorizationCode ?? Data(), encoding: .utf8) - else { - throw AuthError.missingIDToken - } - - let displayName = formatDisplayName(credential.fullName) - Log.info("Apple sign-in credential received for \(displayName ?? "unknown user")") - - let profile = try await repository.signIn( - provider: .apple, - idToken: identityToken, + let provider = AppleOAuthProvider() + return try await provider.signInWithCredential( + credential: credential, nonce: nonce, - displayName: displayName, - authorizationCode: authCode + repository: repository ) - Log.info("Supabase sign-in with Apple succeeded") - return profile } - - // MARK: - Helper Methods - private func formatDisplayName( - _ components: PersonNameComponents? - ) -> String? { - guard let components else { return nil } - let formatter = PersonNameComponentsFormatter() - let name = formatter.string(from: components).trimmingCharacters(in: .whitespacesAndNewlines) - return name.isEmpty ? nil : name - } - + + // ✅ 기존 시그니처와 동작 완전 동일 public func signUp( with provider: SocialType ) async throws -> UserProfile { - // Kakao는 Supabase OAuth를 거치지 않고 Kakao SDK 토큰을 그대로 사용 - if provider == .kakao { - let kakaoPayload = try await kakaoRepository.signIn() - let tokens = AuthTokens( - authToken: kakaoPayload.authorizationCode ?? "", - accessToken: kakaoPayload.accessToken, - refreshToken: kakaoPayload.refreshToken ?? "", - sessionID: "" - ) - return UserProfile( - id: kakaoPayload.authorizationCode ?? UUID().uuidString, - email: nil, - displayName: kakaoPayload.displayName, - provider: .kakao, - tokens: tokens, - authCode: kakaoPayload.authorizationCode, - codeVerifier: kakaoPayload.codeVerifier - ) - } - - let payload = try await fetchPayload(for: provider) - Log.info("\(provider.rawValue) sign-in succeeded for \(payload.displayName ?? "unknown user")") - - let profile = try await repository.signIn( - provider: payload.provider, - idToken: payload.idToken, - nonce: payload.nonce, - displayName: payload.displayName, - authorizationCode: payload.authorizationCode - ) - Log.info("Supabase sign-in with \(provider.rawValue) succeeded") - - return profile - } - - private func fetchPayload( - for provider: SocialType - ) async throws -> OAuthSignInPayload { - switch provider { - case .apple: - let payload = try await appleRepository.signIn() - return OAuthSignInPayload( - provider: .apple, - idToken: payload.idToken, - nonce: payload.nonce, - displayName: payload.displayName, - authorizationCode: payload.authorizationCode - ) - case .google: - let payload = try await googleRepository.signIn() - return OAuthSignInPayload( - provider: .google, - idToken: payload.idToken, - nonce: nil, - displayName: payload.displayName, - authorizationCode: payload.authorizationCode - ) - case .kakao: - // Kakao는 SDK 토큰을 바로 사용하므로 여기서는 빈 페이로드 반환 - return OAuthSignInPayload( - provider: .kakao, - idToken: "", - nonce: nil, - displayName: nil, - authorizationCode: nil - ) - case .none: - throw AuthError.configurationMissing + Log.info("🔥 OAuthUseCase.signUp called with provider: \(provider.rawValue)") + + do { + switch provider { + case .apple: + let appleProvider = AppleOAuthProvider() + let result = try await appleProvider.signUp(repository: repository, appleRepository: appleRepository) + Log.info("✅ Apple signUp completed successfully") + return result + case .google: + let googleProvider = GoogleOAuthProvider() + let result = try await googleProvider.signUp(repository: repository, googleRepository: googleRepository) + Log.info("✅ Google signUp completed successfully") + return result + case .kakao: + let kakaoProvider = KakaoOAuthProvider() + let result = try await kakaoProvider.signUp(kakaoRepository: kakaoRepository) + Log.info("✅ Kakao signUp completed successfully") + return result + case .none: + Log.error("❌ Invalid provider: none") + throw AuthError.configurationMissing + } + } catch { + Log.error("💥 OAuthUseCase.signUp failed: \(error.localizedDescription)") + throw error } } } diff --git a/Domain/Sources/UseCase/OAuth/Protocols/OAuthProviderProtocol.swift b/Domain/Sources/UseCase/OAuth/Protocols/OAuthProviderProtocol.swift new file mode 100644 index 0000000..fe730f2 --- /dev/null +++ b/Domain/Sources/UseCase/OAuth/Protocols/OAuthProviderProtocol.swift @@ -0,0 +1,27 @@ +// +// OAuthProviderProtocol.swift +// Domain +// +// Created by Wonji Suh on 12/17/25. +// + +import Foundation +import AuthenticationServices + +public protocol OAuthProviderProtocol { + var socialType: SocialType { get } +} + +// Apple 전용 프로토콜 +public protocol AppleOAuthProviderProtocol: OAuthProviderProtocol { + func signInWithCredential( + credential: ASAuthorizationAppleIDCredential, + nonce: String, + repository: OAuthRepositoryProtocol + ) async throws -> UserProfile + + func signUp( + repository: OAuthRepositoryProtocol, + appleRepository: AppleOAuthRepositoryProtocol + ) async throws -> UserProfile +} \ No newline at end of file diff --git a/Domain/Sources/UseCase/OAuth/Providers/AppleOAuthProvider.swift b/Domain/Sources/UseCase/OAuth/Providers/AppleOAuthProvider.swift new file mode 100644 index 0000000..a4d1fee --- /dev/null +++ b/Domain/Sources/UseCase/OAuth/Providers/AppleOAuthProvider.swift @@ -0,0 +1,83 @@ +// +// AppleOAuthProvider.swift +// Domain +// +// Created by Wonji Suh on 12/17/25. +// + +import Foundation +import Dependencies +import LogMacro +import AuthenticationServices + +public class AppleOAuthProvider: AppleOAuthProviderProtocol { + public let socialType: SocialType = .apple + + public init() {} + + // ✅ 기존 signInWithApple 로직 그대로 (Dependencies 매개변수로 전달) + public func signInWithCredential( + credential: ASAuthorizationAppleIDCredential, + nonce: String, + repository: OAuthRepositoryProtocol + ) async throws -> UserProfile { + guard let identityTokenData = credential.identityToken, + let identityToken = String(data: identityTokenData, encoding: .utf8), + let authCode = String(data: credential.authorizationCode ?? Data(), encoding: .utf8) + else { + throw AuthError.missingIDToken + } + + let displayName = formatDisplayName(credential.fullName) + Log.info("Apple sign-in credential received for \(displayName ?? "unknown user")") + + let profile = try await repository.signIn( + provider: .apple, + idToken: identityToken, + nonce: nonce, + displayName: displayName, + authorizationCode: authCode + ) + Log.info("Supabase sign-in with Apple succeeded") + return profile + } + + // ✅ signUp 메소드 (Dependencies 매개변수로 전달) + public func signUp( + repository: OAuthRepositoryProtocol, + appleRepository: AppleOAuthRepositoryProtocol + ) async throws -> UserProfile { + let payload = try await fetchPayload(appleRepository: appleRepository) + Log.info("apple sign-in succeeded for \(payload.displayName ?? "unknown user")") + + let profile = try await repository.signIn( + provider: payload.provider, + idToken: payload.idToken, + nonce: payload.nonce, + displayName: payload.displayName, + authorizationCode: payload.authorizationCode + ) + Log.info("Supabase sign-in with apple succeeded") + return profile + } + + // ✅ 기존 formatDisplayName 로직 그대로 + private func formatDisplayName(_ components: PersonNameComponents?) -> String? { + guard let components else { return nil } + let formatter = PersonNameComponentsFormatter() + let name = formatter.string(from: components).trimmingCharacters(in: .whitespacesAndNewlines) + return name.isEmpty ? nil : name + } + + // ✅ 기존 fetchPayload 로직 그대로 (매개변수로 repository 전달) + private func fetchPayload(appleRepository: AppleOAuthRepositoryProtocol) async throws -> OAuthSignInPayload { + let payload = try await appleRepository.signIn() + return OAuthSignInPayload( + provider: .apple, + idToken: payload.idToken, + nonce: payload.nonce, + displayName: payload.displayName, + authorizationCode: payload.authorizationCode + ) + } +} \ No newline at end of file diff --git a/Domain/Sources/UseCase/OAuth/Providers/GoogleOAuthProvider.swift b/Domain/Sources/UseCase/OAuth/Providers/GoogleOAuthProvider.swift new file mode 100644 index 0000000..3ab1506 --- /dev/null +++ b/Domain/Sources/UseCase/OAuth/Providers/GoogleOAuthProvider.swift @@ -0,0 +1,47 @@ +// +// GoogleOAuthProvider.swift +// Domain +// +// Created by Wonji Suh on 12/17/25. +// + +import Foundation +import Dependencies +import LogMacro + +public class GoogleOAuthProvider: OAuthProviderProtocol { + public let socialType: SocialType = .google + + public init() {} + + // ✅ 기존 signUp 로직 그대로 (Dependencies 매개변수로 전달) + public func signUp( + repository: OAuthRepositoryProtocol, + googleRepository: GoogleOAuthRepositoryProtocol + ) async throws -> UserProfile { + let payload = try await fetchPayload(googleRepository: googleRepository) + Log.info("google sign-in succeeded for \(payload.displayName ?? "unknown user")") + + let profile = try await repository.signIn( + provider: payload.provider, + idToken: payload.idToken, + nonce: payload.nonce, + displayName: payload.displayName, + authorizationCode: payload.authorizationCode + ) + Log.info("Supabase sign-in with google succeeded") + return profile + } + + // ✅ 기존 fetchPayload 로직 그대로 (매개변수로 repository 전달) + private func fetchPayload(googleRepository: GoogleOAuthRepositoryProtocol) async throws -> OAuthSignInPayload { + let payload = try await googleRepository.signIn() + return OAuthSignInPayload( + provider: .google, + idToken: payload.idToken, + nonce: nil, + displayName: payload.displayName, + authorizationCode: payload.authorizationCode + ) + } +} \ No newline at end of file diff --git a/Domain/Sources/UseCase/OAuth/Providers/KakaoOAuthProvider.swift b/Domain/Sources/UseCase/OAuth/Providers/KakaoOAuthProvider.swift new file mode 100644 index 0000000..54832d6 --- /dev/null +++ b/Domain/Sources/UseCase/OAuth/Providers/KakaoOAuthProvider.swift @@ -0,0 +1,36 @@ +// +// KakaoOAuthProvider.swift +// Domain +// +// Created by Wonji Suh on 12/17/25. +// + +import Foundation +import Dependencies +import LogMacro + +public class KakaoOAuthProvider: OAuthProviderProtocol { + public let socialType: SocialType = .kakao + + public init() {} + + // ✅ 기존 Kakao 특수 로직 그대로 (Dependencies 매개변수로 전달) + public func signUp(kakaoRepository: KakaoOAuthRepositoryProtocol) async throws -> UserProfile { + let kakaoPayload = try await kakaoRepository.signIn() + let tokens = AuthTokens( + authToken: kakaoPayload.authorizationCode ?? "", + accessToken: kakaoPayload.accessToken, + refreshToken: kakaoPayload.refreshToken ?? "", + sessionID: "" + ) + return UserProfile( + id: kakaoPayload.authorizationCode ?? UUID().uuidString, + email: nil, + displayName: kakaoPayload.displayName, + provider: .kakao, + tokens: tokens, + authCode: kakaoPayload.authorizationCode, + codeVerifier: kakaoPayload.codeVerifier + ) + } +} \ No newline at end of file From e65e1090ea9cf59ca44cce6d4820c378aab80a98 Mon Sep 17 00:00:00 2001 From: Roy Date: Wed, 17 Dec 2025 11:42:08 +0900 Subject: [PATCH 2/3] =?UTF-8?q?[chore]:=20UnifiedOAuthUseCase=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * socialtype 마다 provider 로 구현 --- .../OAuth/OAuthFlow/OAuthFlowUseCase.swift | 230 ++++++++++++ .../Sources/UseCase/OAuth/OAuthUseCase.swift | 6 +- .../{ => Apple}/AppleOAuthProvider.swift | 6 +- .../{ => Google}/GoogleOAuthProvider.swift | 5 +- .../{ => Kakao}/KakaoOAuthProvider.swift | 3 +- .../Protocols/OAuthProviderProtocol.swift | 0 .../UseCase/OAuth/UnifiedOAuthUseCase.swift | 336 ++---------------- 7 files changed, 257 insertions(+), 329 deletions(-) create mode 100644 Domain/Sources/UseCase/OAuth/OAuthFlow/OAuthFlowUseCase.swift rename Domain/Sources/UseCase/OAuth/Providers/{ => Apple}/AppleOAuthProvider.swift (90%) rename Domain/Sources/UseCase/OAuth/Providers/{ => Google}/GoogleOAuthProvider.swift (89%) rename Domain/Sources/UseCase/OAuth/Providers/{ => Kakao}/KakaoOAuthProvider.swift (92%) rename Domain/Sources/UseCase/OAuth/{ => Providers}/Protocols/OAuthProviderProtocol.swift (100%) diff --git a/Domain/Sources/UseCase/OAuth/OAuthFlow/OAuthFlowUseCase.swift b/Domain/Sources/UseCase/OAuth/OAuthFlow/OAuthFlowUseCase.swift new file mode 100644 index 0000000..e29215b --- /dev/null +++ b/Domain/Sources/UseCase/OAuth/OAuthFlow/OAuthFlowUseCase.swift @@ -0,0 +1,230 @@ +// +// OAuthFlowCoordinator.swift +// Domain +// +// Created by Wonji Suh on 12/17/25. +// + +import Foundation +import ComposableArchitecture +import LogMacro +import AuthenticationServices + +public struct OAuthFlowUseCase { + @Dependency(\.oAuthUseCase) private var oAuthUseCase: any OAuthUseCaseProtocol + @Dependency(\.signUpUseCase) private var signUpUseCase + @Dependency(\.authUseCase) private var authUseCase + + public init() {} +} + + +public extension OAuthFlowUseCase { + struct FlowPayload { + public let authData: AuthData + public let immediateAuthResult: AuthResult? + } + + // 단일 엔트리 포인트: 소셜 타입에 맞춰 전체 플로우 실행 + func process( + socialType: SocialType, + appleCredential: ASAuthorizationAppleIDCredential? = nil, + nonce: String? = nil + ) async -> AuthFlowOutcome { + Log.info("🔐 Orchestrator start: \(socialType.rawValue)") + + let authDataResult = await fetchOAuthPayload( + socialType: socialType, + appleCredential: appleCredential, + nonce: nonce + ) + + guard case .success(let payload) = authDataResult else { + if case .failure(let error) = authDataResult { + return .failure(error) + } + return .failure(.unknownError("OAuth 데이터 획득 실패")) + } + + // Kakao는 finalizeKakao까지 수행했으므로 바로 로그인 성공 처리 + if let immediate = payload.immediateAuthResult { + return .loginSuccess(immediate) + } + + // 가입 여부 확인 + let checkResult = await signUpUseCase.checkUser(payload.authData) + guard case .success(let checkUser) = checkResult else { + if case .failure(let error) = checkResult { + return .failure(error) + } + return .failure(.unknownError("가입 여부 확인 실패")) + } + + if checkUser.registered { + let loginResult = await authUseCase.login(payload.authData) + switch loginResult { + case .success(let authResult): + return .loginSuccess(authResult) + case .failure(let error): + return .failure(error) + } + } + + if checkUser.needsTerms { + return .needsTermsAgreement(payload.authData) + } + + let signUpResult = await signUpUseCase.signUp(payload.authData) + switch signUpResult { + case .success(let authResult): + return .signUpSuccess(authResult) + case .failure(let error): + return .failure(error) + } + } + + // 개별 단계 노출 (UnifiedOAuthUseCase 등 파사드에서 재사용) + func fetchOAuthPayload( + socialType: SocialType, + appleCredential: ASAuthorizationAppleIDCredential? = nil, + nonce: String? = nil + ) async -> Result { + await fetchOAuthData( + socialType: socialType, + appleCredential: appleCredential, + nonce: nonce + ) + } + + func checkUserRegistrationStatus(with authData: AuthData) async -> Result { + await signUpUseCase.checkUser(authData) + } + + func login(with authData: AuthData) async -> Result { + await authUseCase.login(authData) + } + + func signUp(with authData: AuthData) async -> Result { + await signUpUseCase.signUp(authData) + } +} + + +private extension OAuthFlowUseCase { + func fetchOAuthData( + socialType: SocialType, + appleCredential: ASAuthorizationAppleIDCredential?, + nonce: String? + ) async -> Result { + do { + switch socialType { + case .apple: + guard + let credential = appleCredential, + let nonce = nonce, + !nonce.isEmpty + else { + return .failure(.invalidCredential("Apple 자격정보가 없습니다")) + } + + let profile = try await oAuthUseCase.signInWithApple( + credential: credential, + nonce: nonce + ) + + return .success( + FlowPayload( + authData: AuthData( + socialType: profile.provider, + accessToken: profile.tokens.accessToken, + authToken: profile.tokens.authToken, + displayName: profile.displayName, + authorizationCode: profile.authCode, + codeVerifier: nil, + redirectUri: nil, + refreshToken: profile.tokens.refreshToken, + sessionID: profile.tokens.sessionID, + userId: profile.id + ), + immediateAuthResult: nil + ) + ) + + case .google: + let profile = try await oAuthUseCase.signUp(with: socialType) + return .success( + FlowPayload( + authData: AuthData( + socialType: profile.provider, + accessToken: profile.tokens.accessToken, + authToken: profile.tokens.authToken, + displayName: profile.displayName, + authorizationCode: profile.authCode, + codeVerifier: nil, + redirectUri: nil, + refreshToken: profile.tokens.refreshToken, + sessionID: profile.tokens.sessionID, + userId: profile.id + ), + immediateAuthResult: nil + ) + ) + + case .kakao: + // Kakao는 ticket -> finalizeKakao를 통해 토큰 확보 + let profile = try await oAuthUseCase.signUp(with: socialType) + guard let ticket = profile.authCode else { + return .failure(.invalidCredential("Kakao ticket이 없습니다")) + } + + let finalize = await signUpUseCase.finalizeKakao(ticket: ticket) + guard case .success(let finalized) = finalize else { + if case .failure(let error) = finalize { + return .failure(error) + } + return .failure(.unknownError("Kakao finalize 실패")) + } + + let accessToken = finalized.token.accessToken + return .success( + FlowPayload( + authData: AuthData( + socialType: profile.provider, + accessToken: accessToken, + authToken: accessToken, + displayName: finalized.name, + authorizationCode: ticket, + codeVerifier: profile.codeVerifier, + redirectUri: "https://sseudam.up.railway.app/api/v1/oauth/kakao/callback", + refreshToken: finalized.token.refreshToken, + sessionID: finalized.token.sessionID, + userId: finalized.userId + ), + immediateAuthResult: finalized + ) + ) + + case .none: + return .failure(.invalidCredential("잘못된 소셜 타입")) + } + } catch { + let authError = error as? AuthError ?? .unknownError(error.localizedDescription) + return .failure(authError) + } + } +} + + +//MARK: - Dependency Registration +extension OAuthFlowUseCase: DependencyKey { + public static var liveValue: OAuthFlowUseCase = OAuthFlowUseCase() + public static var previewValue: OAuthFlowUseCase = OAuthFlowUseCase() + public static var testValue: OAuthFlowUseCase = OAuthFlowUseCase() +} + +public extension DependencyValues { + var oAuthFlowUseCase: OAuthFlowUseCase { + get { self[OAuthFlowUseCase.self] } + set { self[OAuthFlowUseCase.self] = newValue } + } +} diff --git a/Domain/Sources/UseCase/OAuth/OAuthUseCase.swift b/Domain/Sources/UseCase/OAuth/OAuthUseCase.swift index fac61e2..ede351b 100644 --- a/Domain/Sources/UseCase/OAuth/OAuthUseCase.swift +++ b/Domain/Sources/UseCase/OAuth/OAuthUseCase.swift @@ -11,7 +11,7 @@ import LogMacro import AuthenticationServices public struct OAuthUseCase: OAuthUseCaseProtocol { - // ✅ Dependencies 시스템 유지 + @Dependency(\.oAuthRepository) private var repository: OAuthRepositoryProtocol @Dependency(\.googleOAuthRepository) private var googleRepository: GoogleOAuthRepositoryProtocol @Dependency(\.appleOAuthRepository) private var appleRepository: AppleOAuthRepositoryProtocol @@ -19,7 +19,7 @@ public struct OAuthUseCase: OAuthUseCaseProtocol { public init() {} - // ✅ 기존 시그니처와 동작 완전 동일 + public func signInWithApple( credential: ASAuthorizationAppleIDCredential, nonce: String @@ -32,7 +32,7 @@ public struct OAuthUseCase: OAuthUseCaseProtocol { ) } - // ✅ 기존 시그니처와 동작 완전 동일 + public func signUp( with provider: SocialType ) async throws -> UserProfile { diff --git a/Domain/Sources/UseCase/OAuth/Providers/AppleOAuthProvider.swift b/Domain/Sources/UseCase/OAuth/Providers/Apple/AppleOAuthProvider.swift similarity index 90% rename from Domain/Sources/UseCase/OAuth/Providers/AppleOAuthProvider.swift rename to Domain/Sources/UseCase/OAuth/Providers/Apple/AppleOAuthProvider.swift index a4d1fee..a44109a 100644 --- a/Domain/Sources/UseCase/OAuth/Providers/AppleOAuthProvider.swift +++ b/Domain/Sources/UseCase/OAuth/Providers/Apple/AppleOAuthProvider.swift @@ -15,7 +15,6 @@ public class AppleOAuthProvider: AppleOAuthProviderProtocol { public init() {} - // ✅ 기존 signInWithApple 로직 그대로 (Dependencies 매개변수로 전달) public func signInWithCredential( credential: ASAuthorizationAppleIDCredential, nonce: String, @@ -42,7 +41,6 @@ public class AppleOAuthProvider: AppleOAuthProviderProtocol { return profile } - // ✅ signUp 메소드 (Dependencies 매개변수로 전달) public func signUp( repository: OAuthRepositoryProtocol, appleRepository: AppleOAuthRepositoryProtocol @@ -61,7 +59,6 @@ public class AppleOAuthProvider: AppleOAuthProviderProtocol { return profile } - // ✅ 기존 formatDisplayName 로직 그대로 private func formatDisplayName(_ components: PersonNameComponents?) -> String? { guard let components else { return nil } let formatter = PersonNameComponentsFormatter() @@ -69,7 +66,6 @@ public class AppleOAuthProvider: AppleOAuthProviderProtocol { return name.isEmpty ? nil : name } - // ✅ 기존 fetchPayload 로직 그대로 (매개변수로 repository 전달) private func fetchPayload(appleRepository: AppleOAuthRepositoryProtocol) async throws -> OAuthSignInPayload { let payload = try await appleRepository.signIn() return OAuthSignInPayload( @@ -80,4 +76,4 @@ public class AppleOAuthProvider: AppleOAuthProviderProtocol { authorizationCode: payload.authorizationCode ) } -} \ No newline at end of file +} diff --git a/Domain/Sources/UseCase/OAuth/Providers/GoogleOAuthProvider.swift b/Domain/Sources/UseCase/OAuth/Providers/Google/GoogleOAuthProvider.swift similarity index 89% rename from Domain/Sources/UseCase/OAuth/Providers/GoogleOAuthProvider.swift rename to Domain/Sources/UseCase/OAuth/Providers/Google/GoogleOAuthProvider.swift index 3ab1506..5ebff3b 100644 --- a/Domain/Sources/UseCase/OAuth/Providers/GoogleOAuthProvider.swift +++ b/Domain/Sources/UseCase/OAuth/Providers/Google/GoogleOAuthProvider.swift @@ -14,7 +14,7 @@ public class GoogleOAuthProvider: OAuthProviderProtocol { public init() {} - // ✅ 기존 signUp 로직 그대로 (Dependencies 매개변수로 전달) + public func signUp( repository: OAuthRepositoryProtocol, googleRepository: GoogleOAuthRepositoryProtocol @@ -33,7 +33,6 @@ public class GoogleOAuthProvider: OAuthProviderProtocol { return profile } - // ✅ 기존 fetchPayload 로직 그대로 (매개변수로 repository 전달) private func fetchPayload(googleRepository: GoogleOAuthRepositoryProtocol) async throws -> OAuthSignInPayload { let payload = try await googleRepository.signIn() return OAuthSignInPayload( @@ -44,4 +43,4 @@ public class GoogleOAuthProvider: OAuthProviderProtocol { authorizationCode: payload.authorizationCode ) } -} \ No newline at end of file +} diff --git a/Domain/Sources/UseCase/OAuth/Providers/KakaoOAuthProvider.swift b/Domain/Sources/UseCase/OAuth/Providers/Kakao/KakaoOAuthProvider.swift similarity index 92% rename from Domain/Sources/UseCase/OAuth/Providers/KakaoOAuthProvider.swift rename to Domain/Sources/UseCase/OAuth/Providers/Kakao/KakaoOAuthProvider.swift index 54832d6..011f36f 100644 --- a/Domain/Sources/UseCase/OAuth/Providers/KakaoOAuthProvider.swift +++ b/Domain/Sources/UseCase/OAuth/Providers/Kakao/KakaoOAuthProvider.swift @@ -14,7 +14,6 @@ public class KakaoOAuthProvider: OAuthProviderProtocol { public init() {} - // ✅ 기존 Kakao 특수 로직 그대로 (Dependencies 매개변수로 전달) public func signUp(kakaoRepository: KakaoOAuthRepositoryProtocol) async throws -> UserProfile { let kakaoPayload = try await kakaoRepository.signIn() let tokens = AuthTokens( @@ -33,4 +32,4 @@ public class KakaoOAuthProvider: OAuthProviderProtocol { codeVerifier: kakaoPayload.codeVerifier ) } -} \ No newline at end of file +} diff --git a/Domain/Sources/UseCase/OAuth/Protocols/OAuthProviderProtocol.swift b/Domain/Sources/UseCase/OAuth/Providers/Protocols/OAuthProviderProtocol.swift similarity index 100% rename from Domain/Sources/UseCase/OAuth/Protocols/OAuthProviderProtocol.swift rename to Domain/Sources/UseCase/OAuth/Providers/Protocols/OAuthProviderProtocol.swift diff --git a/Domain/Sources/UseCase/OAuth/UnifiedOAuthUseCase.swift b/Domain/Sources/UseCase/OAuth/UnifiedOAuthUseCase.swift index b664b86..5924d80 100644 --- a/Domain/Sources/UseCase/OAuth/UnifiedOAuthUseCase.swift +++ b/Domain/Sources/UseCase/OAuth/UnifiedOAuthUseCase.swift @@ -7,20 +7,13 @@ import Foundation import Dependencies -import LogMacro import AuthenticationServices /// 통합 OAuth UseCase - 로그인/회원가입 플로우를 하나로 통합 (AuthFacade 역할) public struct UnifiedOAuthUseCase { - @Dependency(\.oAuthUseCase) private var oAuthUseCase: any OAuthUseCaseProtocol - @Dependency(\.authRepository) private var authRepository: AuthRepositoryProtocol - private let sessionStoreRepository: any SessionStoreRepositoryProtocol + @Dependency(\.oAuthFlowUseCase) private var oAuthFlow: OAuthFlowUseCase - public init( - sessionStoreRepository: any SessionStoreRepositoryProtocol = SessionStoreRepository() - ) { - self.sessionStoreRepository = sessionStoreRepository - } + public init() {} } // MARK: - Public Interface @@ -33,46 +26,40 @@ public extension UnifiedOAuthUseCase { appleCredential: ASAuthorizationAppleIDCredential? = nil, nonce: String? = nil ) async -> Result { - return await getOAuthCredentials( + let payload = await oAuthFlow.fetchOAuthPayload( socialType: socialType, appleCredential: appleCredential, nonce: nonce ) + return payload.map(\.authData) } /// 회원가입 상태 확인 func checkSignUpUser( with oAuthData: AuthData ) async -> Result { - return await checkUserRegistrationStatus(with: oAuthData) + await oAuthFlow.checkUserRegistrationStatus(with: oAuthData) } /// 로그인 처리 func loginUser( with oAuthData: AuthData ) async -> Result { - let loginResult = await attemptLogin(with: oAuthData) - - if case .success(let authEntity) = loginResult { - saveTokensAndComplete(authEntity: authEntity) - } - - return loginResult + await oAuthFlow.login(with: oAuthData) } /// 회원가입 처리 func signUpUser( with oAuthData: AuthData ) async -> Result { - return await attemptSignUp(with: oAuthData) + await oAuthFlow.signUp(with: oAuthData) } /// 약관 동의 후 회원가입 처리 func signUpWithTermsAgreement( with oAuthData: AuthData ) async -> Result { - Log.info("✅ Terms agreement completed, proceeding with signup") - return await attemptSignUp(with: oAuthData) + await oAuthFlow.signUp(with: oAuthData) } /// OAuth 플로우 처리 (AuthFlowOutcome 반환) @@ -81,56 +68,11 @@ public extension UnifiedOAuthUseCase { appleCredential: ASAuthorizationAppleIDCredential? = nil, nonce: String? = nil ) async -> AuthFlowOutcome { - Log.info("🔐 Starting OAuth flow for: \(socialType.rawValue)") - - // 1단계: OAuth Provider 인증 - let oAuthData = await getOAuthCredentials( + return await oAuthFlow.process( socialType: socialType, appleCredential: appleCredential, nonce: nonce ) - guard case .success(let authData) = oAuthData else { - if case .failure(let error) = oAuthData { - return .failure(error) - } else { - return .failure(.unknownError("OAuth 인증 실패")) - } - } - - // 2단계: 사용자 등록 상태 확인 - let registrationStatus = await checkUserRegistrationStatus(with: authData) - guard case .success(let checkUser) = registrationStatus else { - if case .failure(let error) = registrationStatus { - return .failure(error) - } else { - return .failure(.unknownError("등록 상태 확인 실패")) - } - } - - // 3단계: 등록 여부에 따른 분기 처리 - if checkUser.registered { - // 이미 등록된 사용자 -> 로그인 진행 - let loginResult = await attemptLogin(with: authData) - switch loginResult { - case .success(let authResult): - saveTokensAndComplete(authEntity: authResult) - return .loginSuccess(authResult) - case .failure(let error): - return .failure(error) - } - } else if checkUser.needsTerms { - // 약관 동의 필요 - return .needsTermsAgreement(authData) - } else { - // 약관 동의 완료 -> 회원가입 진행 - let signUpResult = await attemptSignUp(with: authData) - switch signUpResult { - case .success(let authResult): - return .signUpSuccess(authResult) - case .failure(let error): - return .failure(error) - } - } } func loginOrSignUp( @@ -138,268 +80,30 @@ public extension UnifiedOAuthUseCase { appleCredential: ASAuthorizationAppleIDCredential? = nil, nonce: String? = nil ) async -> Result { - Log.info("🔐 Starting unified OAuth flow for: \(socialType.rawValue)") - - let oAuthData = await getOAuthCredentials( + let outcome = await oAuthFlow.process( socialType: socialType, appleCredential: appleCredential, nonce: nonce ) - guard case .success(let authData) = oAuthData else { - if case .failure(let error) = oAuthData { - return .failure(error) - } else { - return .failure(.unknownError("OAuth 인증 실패")) - } - } - - let registrationStatus = await checkUserRegistrationStatus(with: authData) - guard case .success(let checkUser) = registrationStatus else { - if case .failure(let error) = registrationStatus { - return .failure(error) - } else { - return .failure(.unknownError("등록 상태 확인 실패")) - } - } - - let authResult: Result - - if checkUser.registered { - authResult = await attemptLogin(with: authData) - } else { - if checkUser.needsTerms { - Log.info("📋 Terms agreement required for new user") - return .failure(.needsTermsAgreement("약관 동의가 필요합니다")) - } else { - authResult = await attemptSignUp(with: authData) - } - } - - if case .success(let authEntity) = authResult, checkUser.registered { - saveTokensAndComplete(authEntity: authEntity) - } - - return authResult - } -} - -// MARK: - Private Methods -private extension UnifiedOAuthUseCase { - - /// OAuth Provider에서 인증 정보 획득 - func getOAuthCredentials( - socialType: SocialType, - appleCredential: ASAuthorizationAppleIDCredential? = nil, - nonce: String? = nil - ) async -> Result { - do { - switch socialType { - case .apple: - guard - let credential = appleCredential, - let nonce = nonce, - !nonce.isEmpty - else { - return .failure(.invalidCredential("Apple 자격정보가 없습니다")) - } - - let profile = try await oAuthUseCase.signInWithApple( - credential: credential, - nonce: nonce - ) - - let oAuthData = AuthData( - socialType: profile.provider, - accessToken: profile.tokens.accessToken, - authToken: profile.tokens.authToken, - displayName: profile.displayName, - authorizationCode: profile.authCode, - codeVerifier: nil, - redirectUri: nil, - refreshToken: profile.tokens.refreshToken, - sessionID: profile.tokens.sessionID, - userId: profile.id - ) - - - return .success(oAuthData) - - case .google: - let profile = try await oAuthUseCase.signUp(with: socialType) - let oAuthData = AuthData( - socialType: profile.provider, - accessToken: profile.tokens.accessToken, - authToken: profile.tokens.authToken, - displayName: profile.displayName, - authorizationCode: profile.authCode, - codeVerifier: nil, - redirectUri: nil, - refreshToken: profile.tokens.refreshToken, - sessionID: profile.tokens.sessionID, - userId: profile.id - ) - return .success(oAuthData) - case .kakao: - let profile = try await oAuthUseCase.signUp(with: socialType) - guard let ticket = profile.authCode else { - return .failure(.invalidCredential("Kakao ticket이 없습니다")) - } - // 바로 finalize 호출하여 세션/토큰 확보 - let finalized = try await authRepository.finalizeKakao(ticket: ticket) - let accessToken = finalized.token.accessToken - let oAuthData = AuthData( - socialType: profile.provider, - accessToken: accessToken, - authToken: accessToken, - displayName: finalized.name, - authorizationCode: ticket, - codeVerifier: profile.codeVerifier, - redirectUri: "https://sseudam.up.railway.app/api/v1/oauth/kakao/callback", - refreshToken: finalized.token.refreshToken, - sessionID: finalized.token.sessionID, - userId: finalized.userId - ) - return .success(oAuthData) - - case .none: - return .failure(.invalidCredential("잘못된 소셜 타입")) - } - } catch { - let authError = error as? AuthError ?? .unknownError(error.localizedDescription) - return .failure(authError) - } - } - - /// 로그인 시도 - func attemptLogin( - with oAuthData: AuthData - ) async -> Result { - do { - if oAuthData.socialType == .kakao { - let tokens = AuthTokens( - authToken: oAuthData.authToken, - accessToken: oAuthData.authToken, - refreshToken: oAuthData.refreshToken, - sessionID: oAuthData.sessionID ?? "" - ) - let authEntity = AuthResult( - userId: oAuthData.userId ?? "kakao-user", - name: oAuthData.displayName ?? "", - provider: .kakao, - token: tokens - ) - Log.info("✅ Kakao finalize-only login completed") - return .success(authEntity) - } - - let input = OAuthUserInput( - accessToken: oAuthData.authToken , - socialType: oAuthData.socialType, - authorizationCode: oAuthData.authorizationCode, - codeVerifier: oAuthData.codeVerifier, - redirectUri: oAuthData.redirectUri - ) - - var authEntity = try await authRepository.login(input: input) - authEntity.token.authToken = oAuthData.authToken - Log.info("✅ Login successful for \(oAuthData.socialType.rawValue)") - return .success(authEntity) - - } catch { - Log.info("⚠️ Login failed: \(error.localizedDescription)") - return .failure(.networkError(error.localizedDescription)) - } - } - - /// 회원가입 상태 확인 - func checkUserRegistrationStatus( - with oAuthData: AuthData - ) async -> Result { - do { - if oAuthData.socialType == .kakao { - return .success(OAuthCheckUser(registered: true, needsTerms: false)) - } - - let checkInput = OAuthUserInput( - accessToken: oAuthData.authToken, - socialType: oAuthData.socialType, - authorizationCode: oAuthData.authorizationCode, - codeVerifier: oAuthData.codeVerifier, - redirectUri: oAuthData.redirectUri - ) - let result = try await authRepository.checkUser(input: checkInput) - return .success(result) - } catch { - let authError = error as? AuthError ?? .unknownError(error.localizedDescription) - return .failure(authError) + switch outcome { + case .loginSuccess(let auth): + return .success(auth) + case .signUpSuccess(let auth): + return .success(auth) + case .needsTermsAgreement: + return .failure(.needsTermsAgreement("약관 동의가 필요합니다")) + case .failure(let error): + return .failure(error) } } - - /// 회원가입 시도 - func attemptSignUp( - with oAuthData: AuthData - ) async -> Result { - do { - if oAuthData.socialType == .kakao { - let tokens = AuthTokens( - authToken: oAuthData.authToken, - accessToken: oAuthData.authToken, - refreshToken: oAuthData.refreshToken, - sessionID: oAuthData.sessionID ?? "" - ) - let authEntity = AuthResult( - userId: oAuthData.userId ?? "kakao-user", - name: oAuthData.displayName ?? "", - provider: .kakao, - token: tokens - ) - saveTokensAndComplete(authEntity: authEntity) - return .success(authEntity) - } - - let checkInput = OAuthUserInput( - accessToken: oAuthData.authToken, - socialType: oAuthData.socialType, - authorizationCode: oAuthData.authorizationCode, - codeVerifier: oAuthData.codeVerifier, - redirectUri: oAuthData.redirectUri - ) - var authEntity = try await authRepository.signUp(input: checkInput) - authEntity.token.authToken = oAuthData.authToken - saveTokensAndComplete(authEntity: authEntity) - return .success(authEntity) - } catch { - let authError = error as? AuthError ?? .unknownError(error.localizedDescription) - return .failure(authError) - } - } - - /// 토큰 저장 및 로깅 - func saveTokensAndComplete( - authEntity: AuthResult - ) { - sessionStoreRepository.save( - tokens: authEntity.token, - socialType: authEntity.provider, - userId: authEntity.userId - ) - // 완료 로깅 (저장 확인을 위한 불필요한 재로드 제거) - Log.info("💾 Tokens saved to Keychain successfully") - Log.info("🎉 OAuth flow completed for \(authEntity.provider.rawValue)") - } } - - // MARK: - Dependencies Registration extension UnifiedOAuthUseCase: DependencyKey { public static let liveValue = UnifiedOAuthUseCase() - - public static let testValue = UnifiedOAuthUseCase( - sessionStoreRepository: SessionStoreRepository() - ) + public static let testValue = UnifiedOAuthUseCase() } extension DependencyValues { From 74a6549b9d95c6c04fbdaa1f48019fba583bf855 Mon Sep 17 00:00:00 2001 From: Roy Date: Wed, 17 Dec 2025 11:45:01 +0900 Subject: [PATCH 3/3] =?UTF-8?q?[chore]:=20=20oauth=20usecase=20=EB=B6=84?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * SessionStore 구현 * 토큰 저장 및 로직 분리 --- .../SessionStoreRepository.swift | 11 +-- .../Auth/SessionStoreRepositoryProtocol.swift | 16 ---- .../MockSessionStoreRepository.swift | 32 ++++++++ .../SessionStoreRepositoryProtocol.swift | 33 ++++++++ Domain/Sources/UseCase/Auth/AuthUseCase.swift | 30 +++++++ .../UseCase/Auth/AuthUseCaseProtocol.swift | 1 + .../UseCase/SignUp/SignUpUseCase.swift | 82 +++++++++++++++++++ .../SignUp/SignUpUseCaseProtocol.swift | 14 ++++ .../UseCase/Token/TokenStorageUseCase.swift | 38 +++++++++ .../Token/TokenStorageUseCaseProtocol.swift | 12 +++ .../OAuth/UnifiedOAuthUseCaseTests.swift | 12 +-- .../Application/LiveDependencies.swift | 2 +- 12 files changed, 255 insertions(+), 28 deletions(-) rename {Domain/Sources/Repository/Auth => Data/Sources/Repository/SessionStore}/SessionStoreRepository.swift (89%) delete mode 100644 Domain/Sources/Repository/Auth/SessionStoreRepositoryProtocol.swift create mode 100644 Domain/Sources/Repository/SessionStore/MockSessionStoreRepository.swift create mode 100644 Domain/Sources/Repository/SessionStore/SessionStoreRepositoryProtocol.swift create mode 100644 Domain/Sources/UseCase/SignUp/SignUpUseCase.swift create mode 100644 Domain/Sources/UseCase/SignUp/SignUpUseCaseProtocol.swift create mode 100644 Domain/Sources/UseCase/Token/TokenStorageUseCase.swift create mode 100644 Domain/Sources/UseCase/Token/TokenStorageUseCaseProtocol.swift diff --git a/Domain/Sources/Repository/Auth/SessionStoreRepository.swift b/Data/Sources/Repository/SessionStore/SessionStoreRepository.swift similarity index 89% rename from Domain/Sources/Repository/Auth/SessionStoreRepository.swift rename to Data/Sources/Repository/SessionStore/SessionStoreRepository.swift index 90078a1..eac24c6 100644 --- a/Domain/Sources/Repository/Auth/SessionStoreRepository.swift +++ b/Data/Sources/Repository/SessionStore/SessionStoreRepository.swift @@ -6,6 +6,7 @@ // import Foundation +import Domain /// Keychain + UserDefaults를 감싼 세션 저장소 public final class SessionStoreRepository: SessionStoreRepositoryProtocol, @unchecked Sendable { @@ -17,7 +18,7 @@ public final class SessionStoreRepository: SessionStoreRepositoryProtocol, @unch public init() {} - public func save(tokens: AuthTokens, socialType: SocialType?, userId: String?) { + public func save(tokens: AuthTokens, socialType: SocialType?, userId: String?) async { KeychainManager.shared.saveTokens( accessToken: tokens.accessToken, refreshToken: tokens.refreshToken @@ -35,7 +36,7 @@ public final class SessionStoreRepository: SessionStoreRepositoryProtocol, @unch } } - public func loadTokens() -> AuthTokens? { + public func loadTokens() async -> AuthTokens? { let tokens = KeychainManager.shared.loadTokens() guard let access = tokens.accessToken, let refresh = tokens.refreshToken @@ -50,16 +51,16 @@ public final class SessionStoreRepository: SessionStoreRepositoryProtocol, @unch ) } - public func loadSocialType() -> SocialType? { + public func loadSocialType() async -> SocialType? { guard let raw = UserDefaults.standard.string(forKey: Keys.socialType) else { return nil } return SocialType(rawValue: raw) } - public func loadUserId() -> String? { + public func loadUserId() async -> String? { UserDefaults.standard.string(forKey: Keys.userId) } - public func clearAll() { + public func clearAll() async { KeychainManager.shared.clearAll() UserDefaults.standard.removeObject(forKey: Keys.sessionId) UserDefaults.standard.removeObject(forKey: Keys.socialType) diff --git a/Domain/Sources/Repository/Auth/SessionStoreRepositoryProtocol.swift b/Domain/Sources/Repository/Auth/SessionStoreRepositoryProtocol.swift deleted file mode 100644 index 5b7d48a..0000000 --- a/Domain/Sources/Repository/Auth/SessionStoreRepositoryProtocol.swift +++ /dev/null @@ -1,16 +0,0 @@ -// -// SessionStoreRepositoryProtocol.swift -// Domain -// -// Created by Wonji Suh on 12/11/25. -// - -import Foundation - -public protocol SessionStoreRepositoryProtocol { - func save(tokens: AuthTokens, socialType: SocialType?, userId: String?) - func loadTokens() -> AuthTokens? - func loadSocialType() -> SocialType? - func loadUserId() -> String? - func clearAll() -} diff --git a/Domain/Sources/Repository/SessionStore/MockSessionStoreRepository.swift b/Domain/Sources/Repository/SessionStore/MockSessionStoreRepository.swift new file mode 100644 index 0000000..9d01049 --- /dev/null +++ b/Domain/Sources/Repository/SessionStore/MockSessionStoreRepository.swift @@ -0,0 +1,32 @@ +// +// MockSessionStoreRepository.swift +// DomainTests +// +// Created by Wonji Suh on 12/17/25. +// + +import Foundation + +final class MockSessionStoreRepository: SessionStoreRepositoryProtocol, @unchecked Sendable { + private var savedTokens: AuthTokens? + private var savedSocialType: SocialType? + private var savedUserId: String? + private(set) var didClear = false + + func save(tokens: AuthTokens, socialType: SocialType?, userId: String?) async { + savedTokens = tokens + savedSocialType = socialType + savedUserId = userId + } + + func loadTokens() async -> AuthTokens? { savedTokens } + func loadSocialType() async -> SocialType? { savedSocialType } + func loadUserId() async -> String? { savedUserId } + + func clearAll() async { + didClear = true + savedTokens = nil + savedSocialType = nil + savedUserId = nil + } +} diff --git a/Domain/Sources/Repository/SessionStore/SessionStoreRepositoryProtocol.swift b/Domain/Sources/Repository/SessionStore/SessionStoreRepositoryProtocol.swift new file mode 100644 index 0000000..6af960e --- /dev/null +++ b/Domain/Sources/Repository/SessionStore/SessionStoreRepositoryProtocol.swift @@ -0,0 +1,33 @@ +// +// SessionStoreRepositoryProtocol.swift +// Domain +// +// Created by Wonji Suh on 12/11/25. +// + +import Foundation +import ComposableArchitecture + +public protocol SessionStoreRepositoryProtocol: Sendable { + func save(tokens: AuthTokens, socialType: SocialType?, userId: String?) async + func loadTokens() async -> AuthTokens? + func loadSocialType() async -> SocialType? + func loadUserId() async -> String? + func clearAll() async +} + + +public struct SessionStoreRepositoryDependency: DependencyKey { + public static var liveValue: SessionStoreRepositoryProtocol { + fatalError("AuthRepositoryDependency liveValue not implemented") + } + public static var previewValue: SessionStoreRepositoryProtocol = MockSessionStoreRepository() + public static var testValue: SessionStoreRepositoryProtocol = MockSessionStoreRepository() +} + +public extension DependencyValues { + var sessionStoreRepository: SessionStoreRepositoryProtocol { + get { self[SessionStoreRepositoryDependency.self] } + set { self[SessionStoreRepositoryDependency.self] = newValue } + } +} diff --git a/Domain/Sources/UseCase/Auth/AuthUseCase.swift b/Domain/Sources/UseCase/Auth/AuthUseCase.swift index 410cb23..4dc0eeb 100644 --- a/Domain/Sources/UseCase/Auth/AuthUseCase.swift +++ b/Domain/Sources/UseCase/Auth/AuthUseCase.swift @@ -13,8 +13,26 @@ public struct AuthUseCase: AuthUseCaseProtocol { @Shared(.appStorage("sessionId")) var sessionId: String? = "" @Dependency(\.authRepository) private var repository: AuthRepositoryProtocol + @Dependency(\.authRepository) private var authRepository: AuthRepositoryProtocol + @Dependency(\.tokenStorageUseCase) private var tokenStorageUseCase: TokenStorageUseCase + + public init() {} + // MARK: - 로그인 + public func login(_ authData: AuthData) async -> Result { + do { + let input = makeOAuthInput(from: authData) + var authResult = try await authRepository.login(input: input) + authResult.token.authToken = authData.authToken + await tokenStorageUseCase.save(auth: authResult) + return .success(authResult) + } catch { + let authError = error as? AuthError ?? .unknownError(error.localizedDescription) + return .failure(authError) + } + } + // MARK: - 로그아웃 public func logout() async throws -> LogoutStatus { let sessionId = self.sessionId ?? "" @@ -28,6 +46,18 @@ public struct AuthUseCase: AuthUseCaseProtocol { } } +private extension AuthUseCase { + func makeOAuthInput(from authData: AuthData) -> OAuthUserInput { + OAuthUserInput( + accessToken: authData.authToken, + socialType: authData.socialType, + authorizationCode: authData.authorizationCode, + codeVerifier: authData.codeVerifier, + redirectUri: authData.redirectUri + ) + } +} + extension AuthUseCase: DependencyKey { public static let liveValue: AuthUseCaseProtocol = AuthUseCase() diff --git a/Domain/Sources/UseCase/Auth/AuthUseCaseProtocol.swift b/Domain/Sources/UseCase/Auth/AuthUseCaseProtocol.swift index 90f387e..f89a957 100644 --- a/Domain/Sources/UseCase/Auth/AuthUseCaseProtocol.swift +++ b/Domain/Sources/UseCase/Auth/AuthUseCaseProtocol.swift @@ -12,4 +12,5 @@ import Foundation public protocol AuthUseCaseProtocol { func logout() async throws -> LogoutStatus func deleteUser() async throws -> AuthDeleteStatus + func login(_ authData: AuthData) async -> Result } diff --git a/Domain/Sources/UseCase/SignUp/SignUpUseCase.swift b/Domain/Sources/UseCase/SignUp/SignUpUseCase.swift new file mode 100644 index 0000000..8762a6d --- /dev/null +++ b/Domain/Sources/UseCase/SignUp/SignUpUseCase.swift @@ -0,0 +1,82 @@ +// +// SignUpUseCase.swift +// Domain +// +// Created by Wonji Suh on 12/17/25. +// + +import Foundation +import Dependencies +import LogMacro + +/// 회원가입/로그인/체크/카카오 finalize 등 인증 비즈니스 로직 전담 +public struct SignUpUseCase: SignUpUseCaseProtocol { + @Dependency(\.authRepository) private var authRepository: AuthRepositoryProtocol + @Dependency(\.tokenStorageUseCase) private var tokenStorageUseCase: TokenStorageUseCase + + public init() {} +} + +public extension SignUpUseCase { + func checkUser(_ authData: AuthData) async -> Result { + do { + let input = makeOAuthInput(from: authData) + let result = try await authRepository.checkUser(input: input) + return .success(result) + } catch { + let authError = error as? AuthError ?? .unknownError(error.localizedDescription) + return .failure(authError) + } + } + + func signUp(_ authData: AuthData) async -> Result { + do { + let input = makeOAuthInput(from: authData) + var authResult = try await authRepository.signUp(input: input) + authResult.token.authToken = authData.authToken + await tokenStorageUseCase.save(auth: authResult) + return .success(authResult) + } catch { + let authError = error as? AuthError ?? .unknownError(error.localizedDescription) + return .failure(authError) + } + } + + func finalizeKakao(ticket: String) async -> Result { + do { + let authResult = try await authRepository.finalizeKakao(ticket: ticket) + await tokenStorageUseCase.save(auth: authResult) + return .success(authResult) + } catch { + let authError = error as? AuthError ?? .unknownError(error.localizedDescription) + return .failure(authError) + } + } +} + +// MARK: - Helpers +private extension SignUpUseCase { + func makeOAuthInput(from authData: AuthData) -> OAuthUserInput { + OAuthUserInput( + accessToken: authData.authToken, + socialType: authData.socialType, + authorizationCode: authData.authorizationCode, + codeVerifier: authData.codeVerifier, + redirectUri: authData.redirectUri + ) + } +} + +// MARK: - Dependency Registration +extension SignUpUseCase: DependencyKey { + public static var liveValue: SignUpUseCase = SignUpUseCase() + public static var previewValue: SignUpUseCase = SignUpUseCase() + public static var testValue: SignUpUseCase = SignUpUseCase() +} + +public extension DependencyValues { + var signUpUseCase: SignUpUseCase { + get { self[SignUpUseCase.self] } + set { self[SignUpUseCase.self] = newValue } + } +} diff --git a/Domain/Sources/UseCase/SignUp/SignUpUseCaseProtocol.swift b/Domain/Sources/UseCase/SignUp/SignUpUseCaseProtocol.swift new file mode 100644 index 0000000..478fc44 --- /dev/null +++ b/Domain/Sources/UseCase/SignUp/SignUpUseCaseProtocol.swift @@ -0,0 +1,14 @@ +// +// SignUpUseCaseProtocol.swift +// Domain +// +// Created by Wonji Suh on 12/17/25. +// + +import Foundation + +public protocol SignUpUseCaseProtocol { + func checkUser(_ authData: AuthData) async -> Result + func signUp(_ authData: AuthData) async -> Result + func finalizeKakao(ticket: String) async -> Result +} diff --git a/Domain/Sources/UseCase/Token/TokenStorageUseCase.swift b/Domain/Sources/UseCase/Token/TokenStorageUseCase.swift new file mode 100644 index 0000000..a018832 --- /dev/null +++ b/Domain/Sources/UseCase/Token/TokenStorageUseCase.swift @@ -0,0 +1,38 @@ +// +// TokenStorageUseCase.swift +// Domain +// +// Created by Wonji Suh on 12/17/25. +// + +import LogMacro +import ComposableArchitecture + +public struct TokenStorageUseCase: TokenStorageUseCaseProtocol { + @Dependency(\.sessionStoreRepository) var sessionStore: any SessionStoreRepositoryProtocol + + + public init() {} + + public func save(auth: AuthResult) async { + await sessionStore.save( + tokens: auth.token, + socialType: auth.provider, + userId: auth.userId + ) + } +} + + +extension TokenStorageUseCase: DependencyKey { + public static var liveValue: TokenStorageUseCase = TokenStorageUseCase() + public static var testValue: TokenStorageUseCase = TokenStorageUseCase() + public static var previewValue: TokenStorageUseCase = TokenStorageUseCase() +} + +public extension DependencyValues { + var tokenStorageUseCase: TokenStorageUseCase { + get { self[TokenStorageUseCase.self] } + set { self[TokenStorageUseCase.self] = newValue } + } +} diff --git a/Domain/Sources/UseCase/Token/TokenStorageUseCaseProtocol.swift b/Domain/Sources/UseCase/Token/TokenStorageUseCaseProtocol.swift new file mode 100644 index 0000000..f934217 --- /dev/null +++ b/Domain/Sources/UseCase/Token/TokenStorageUseCaseProtocol.swift @@ -0,0 +1,12 @@ +// +// TokenStorageUseCaseProtocol.swift +// Domain +// +// Created by Wonji Suh on 12/17/25. +// + +import Foundation + +public protocol TokenStorageUseCaseProtocol { + func save(auth: AuthResult) async +} diff --git a/Domain/Tests/UseCase/OAuth/UnifiedOAuthUseCaseTests.swift b/Domain/Tests/UseCase/OAuth/UnifiedOAuthUseCaseTests.swift index cc1fcd7..e075929 100644 --- a/Domain/Tests/UseCase/OAuth/UnifiedOAuthUseCaseTests.swift +++ b/Domain/Tests/UseCase/OAuth/UnifiedOAuthUseCaseTests.swift @@ -134,21 +134,21 @@ struct UnifiedOAuthUseCaseTests { } // MARK: - Mock Session Store -private final class MockSessionStoreRepository: SessionStoreRepositoryProtocol { +private final class MockSessionStoreRepository: SessionStoreRepositoryProtocol, @unchecked Sendable { var savedTokens: AuthTokens? var savedSocialType: SocialType? var savedUserId: String? - func save(tokens: AuthTokens, socialType: SocialType?, userId: String?) { + func save(tokens: AuthTokens, socialType: SocialType?, userId: String?) async { savedTokens = tokens savedSocialType = socialType savedUserId = userId } - func loadTokens() -> AuthTokens? { savedTokens } - func loadSocialType() -> SocialType? { savedSocialType } - func loadUserId() -> String? { savedUserId } - func clearAll() { + func loadTokens() async -> AuthTokens? { savedTokens } + func loadSocialType() async -> SocialType? { savedSocialType } + func loadUserId() async -> String? { savedUserId } + func clearAll() async { savedTokens = nil savedSocialType = nil savedUserId = nil diff --git a/SseuDamApp/Sources/Application/LiveDependencies.swift b/SseuDamApp/Sources/Application/LiveDependencies.swift index 01a749a..f1744e9 100644 --- a/SseuDamApp/Sources/Application/LiveDependencies.swift +++ b/SseuDamApp/Sources/Application/LiveDependencies.swift @@ -23,7 +23,7 @@ public enum LiveDependencies { dependencies.kakaoOAuthRepository = KakaoOAuthRepository( presentationContextProvider: AppPresentationContextProvider() ) - + dependencies.sessionStoreRepository = SessionStoreRepository() // Analytics dependencies.analyticsRepository = FirebaseAnalyticsRepository()