diff --git a/.gitignore b/.gitignore
index 311a75b..6f77bea 100644
--- a/.gitignore
+++ b/.gitignore
@@ -23,7 +23,8 @@ graph.dot
*.log
# Local Xcode derived data for CLI builds
-.derivedData/
+# .derivedData, .derivedData-pr3, .derivedData-keycheck 등 CLI 임시 경로 전부 무시
+.derivedData*/
# macOS junk
.DS_Store
diff --git a/.package.resolved b/.package.resolved
index be21391..eada9da 100644
--- a/.package.resolved
+++ b/.package.resolved
@@ -1,6 +1,15 @@
{
- "originHash" : "a233e486feeee6851825f048f7a7b0b0f1dc2fd69a98db3f78b3507baa03d09a",
+ "originHash" : "04f9c7beed1c00c645a6d4cc85b6dc9f57eb3b4efe2300fbb9aba9ab4b820bc7",
"pins" : [
+ {
+ "identity" : "alamofire",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/Alamofire/Alamofire.git",
+ "state" : {
+ "revision" : "7595cbcf59809f9977c5f6378500de2ad73b7ddb",
+ "version" : "5.12.0"
+ }
+ },
{
"identity" : "combine-schedulers",
"kind" : "remoteSourceControl",
@@ -10,6 +19,15 @@
"version" : "1.2.0"
}
},
+ {
+ "identity" : "kakao-ios-sdk",
+ "kind" : "remoteSourceControl",
+ "location" : "https://github.com/kakao/kakao-ios-sdk",
+ "state" : {
+ "revision" : "2a68ca01e2d7900a1559b31d0d59843837f130f2",
+ "version" : "2.28.0"
+ }
+ },
{
"identity" : "nuke",
"kind" : "remoteSourceControl",
diff --git a/.swiftlint.yml b/.swiftlint.yml
index ab13113..75c9fcd 100644
--- a/.swiftlint.yml
+++ b/.swiftlint.yml
@@ -106,6 +106,17 @@ excluded:
- Derived
- DerivedData
- .derivedData
+ - .derivedData-app
+ - .derivedData-app2
+ - .derivedData-coresocialauth
+ - .derivedData-data
+ - .derivedData-data2
+ - .derivedData-feature
+ - .derivedData-feature2
+ - .derivedData-feature3
+ - .derivedData-networklog
+ - .derivedData-rename
+ - .derivedData-rename-app
- .git
- Mozi.xcworkspace
- "**/*.xcodeproj"
diff --git a/AGENTS.md b/AGENTS.md
index 5b26023..831edb5 100644
--- a/AGENTS.md
+++ b/AGENTS.md
@@ -27,7 +27,7 @@ Projects/
Shared/{Util,DesignSystem,Logger}
ThirdParty/{ThirdParty,ThirdPartyUI,ThirdPartyCore}
Domain/
- Core/{Network,Storage}
+ Core/{Network,Storage,SocialAuth}
Data/
Feature/
App/
diff --git a/CLAUDE.md b/CLAUDE.md
index ae7907d..37714f2 100644
--- a/CLAUDE.md
+++ b/CLAUDE.md
@@ -27,7 +27,7 @@ Projects/
Shared/{Util,DesignSystem,Logger}
ThirdParty/{ThirdParty,ThirdPartyUI,ThirdPartyCore}
Domain/
- Core/{Network,Storage}
+ Core/{Network,Storage,SocialAuth}
Data/
Feature/
App/
diff --git a/Config/Example.xcconfig b/Config/Example.xcconfig
index dccd412..53543b1 100644
--- a/Config/Example.xcconfig
+++ b/Config/Example.xcconfig
@@ -1,3 +1,4 @@
// Debug.xcconfig / Release.xcconfig 로 복사해서 사용
// https:/$()/... 형태는 // 가 주석으로 파싱되지 않게 하기 위함
API_BASE_URL =
+KAKAO_NATIVE_APP_KEY =
diff --git a/Projects/App/Mozi.entitlements b/Projects/App/Mozi.entitlements
new file mode 100644
index 0000000..a812db5
--- /dev/null
+++ b/Projects/App/Mozi.entitlements
@@ -0,0 +1,10 @@
+
+
+
+
+ com.apple.developer.applesignin
+
+ Default
+
+
+
diff --git a/Projects/App/Project.swift b/Projects/App/Project.swift
index 1d785c7..b753290 100644
--- a/Projects/App/Project.swift
+++ b/Projects/App/Project.swift
@@ -8,11 +8,13 @@ let project = ProjectFactory.app(
.domain,
.coreNetwork,
.coreStorage,
+ .coreSocialAuth,
.sharedLogger,
.sharedUtils,
.sharedDesignSystem,
.thirdParty,
.thirdPartyUI,
.thirdPartyCore,
- ]
+ ],
+ entitlements: .file(path: "Mozi.entitlements")
)
diff --git a/Projects/App/Sources/DI/AppBootstrap.swift b/Projects/App/Sources/DI/AppBootstrap.swift
index af778cf..3a388e7 100644
--- a/Projects/App/Sources/DI/AppBootstrap.swift
+++ b/Projects/App/Sources/DI/AppBootstrap.swift
@@ -1,3 +1,4 @@
+import CoreSocialAuth
import Foundation
import SharedDesignSystem
import SharedLogger
@@ -8,9 +9,33 @@ enum AppBootstrap {
static func run() {
_ = DesignSystemFontRegistration.registerIfNeeded()
let infra = InfraContainer.live()
+ let kakaoAppKey = requireKakaoNativeAppKeyIfNeeded(
+ infra.configuration.kakaoNativeAppKey
+ )
+ let socialConfig = SocialAuthConfiguration(
+ kakaoAppKey: kakaoAppKey
+ )
+ KakaoAuthBootstrap.initializeIfNeeded(appKey: socialConfig.kakaoAppKey)
prepareDependencies {
- Dependencies.register(&$0, infra: infra)
+ Dependencies.register(&$0, infra: infra, socialConfig: socialConfig)
}
Logger.shared.info("App bootstrap completed", category: .general)
}
+
+ /// Debug 에서는 카카오 키 누락을 부트 시점에 즉시 실패시킨다.
+ private static func requireKakaoNativeAppKeyIfNeeded(_ key: String?) -> String? {
+ #if DEBUG
+ guard let key, key.isEmpty == false else {
+ preconditionFailure(
+ """
+ Missing KAKAO_NATIVE_APP_KEY.
+ Copy Config/Example.xcconfig to Config/Debug.xcconfig and set the native app key for this scheme.
+ """
+ )
+ }
+ return key
+ #else
+ return key
+ #endif
+ }
}
diff --git a/Projects/App/Sources/DI/AppConfiguration.swift b/Projects/App/Sources/DI/AppConfiguration.swift
index f4beb2f..b86f0ea 100644
--- a/Projects/App/Sources/DI/AppConfiguration.swift
+++ b/Projects/App/Sources/DI/AppConfiguration.swift
@@ -5,12 +5,14 @@ struct AppConfiguration: Sendable {
let baseURL: URL
let bundleID: String
let displayName: String
+ let kakaoNativeAppKey: String?
static func make() -> AppConfiguration {
AppConfiguration(
baseURL: AppInfo.apiBaseURL,
bundleID: AppInfo.bundleID,
- displayName: AppInfo.displayName
+ displayName: AppInfo.displayName,
+ kakaoNativeAppKey: AppInfo.kakaoNativeAppKey
)
}
}
diff --git a/Projects/App/Sources/DI/Dependencies.swift b/Projects/App/Sources/DI/Dependencies.swift
index 493c737..40b2aa1 100644
--- a/Projects/App/Sources/DI/Dependencies.swift
+++ b/Projects/App/Sources/DI/Dependencies.swift
@@ -1,16 +1,22 @@
+import CoreSocialAuth
import Data
import Domain
import ThirdParty
enum Dependencies {
+ @MainActor
static func register(
_ values: inout DependencyValues,
- infra: InfraContainer
+ infra: InfraContainer,
+ socialConfig: SocialAuthConfiguration
) {
+ let socialAuthServices = SocialAuthServiceFactory().make(
+ configuration: socialConfig
+ )
values.authClient = .live(
baseURL: infra.configuration.baseURL,
keychain: infra.keychain,
- oauthServices: OAuthServiceFactory.makeStub()
+ socialAuthServices: socialAuthServices
)
}
}
diff --git a/Projects/App/Sources/DI/InfraContainer.swift b/Projects/App/Sources/DI/InfraContainer.swift
index b180f08..917b310 100644
--- a/Projects/App/Sources/DI/InfraContainer.swift
+++ b/Projects/App/Sources/DI/InfraContainer.swift
@@ -14,9 +14,7 @@ extension InfraContainer {
let configuration = AppConfiguration.make()
return InfraContainer(
configuration: configuration,
- userDefaults: DefaultUserDefaultsStorage(
- suiteName: configuration.bundleID
- ),
+ userDefaults: DefaultUserDefaultsStorage(),
keychain: DefaultKeychainStorage(
service: configuration.bundleID
)
diff --git a/Projects/App/Sources/MoziApp.swift b/Projects/App/Sources/MoziApp.swift
index 4ea8591..8ccdc81 100644
--- a/Projects/App/Sources/MoziApp.swift
+++ b/Projects/App/Sources/MoziApp.swift
@@ -1,3 +1,4 @@
+import CoreSocialAuth
import Feature
import SwiftUI
import ThirdParty
@@ -14,6 +15,12 @@ struct MoziApp: App {
WindowGroup {
CompositionRoot.rootView(store: store)
.preferredColorScheme(.dark)
+ .onOpenURL { url in
+ if KakaoAuthRedirectHandler.handle(url: url) {
+ return
+ }
+ store.send(.appCoordinator(.deepLinkReceived(url)))
+ }
}
}
}
diff --git a/Projects/Core/Network/Sources/Logging/NetworkLog.swift b/Projects/Core/Network/Sources/Logging/NetworkLog.swift
index aef3675..3fb6697 100644
--- a/Projects/Core/Network/Sources/Logging/NetworkLog.swift
+++ b/Projects/Core/Network/Sources/Logging/NetworkLog.swift
@@ -9,11 +9,11 @@ enum NetworkLog {
#if DEBUG
if let body = request.httpBody, let bodyText = String(data: body, encoding: .utf8) {
- message += "\nBody: \(redact(bodyText))"
+ message += "\nBody:\n\(formattedBody(bodyText))"
}
#endif
- Logger.shared.info(redact(message), category: .network)
+ Logger.shared.info(message, category: .network)
}
static func response(
@@ -27,7 +27,7 @@ enum NetworkLog {
#if DEBUG
if let bodyText = String(data: data, encoding: .utf8), !bodyText.isEmpty {
- message += "\nBody: \(redact(bodyText))"
+ message += "\nBody:\n\(formattedBody(bodyText))"
}
#endif
@@ -53,23 +53,58 @@ enum NetworkLog {
return components.string ?? url.absoluteString
}
+ /// JSON body 는 여러 줄로 정리한 뒤 민감 값만 가린다.
+ static func formattedBody(_ text: String) -> String {
+ let pretty = prettyPrintedJSON(text) ?? text
+ return redact(pretty)
+ }
+
+ static func prettyPrintedJSON(_ text: String) -> String? {
+ guard let data = text.data(using: .utf8),
+ let object = try? JSONSerialization.jsonObject(with: data),
+ let prettyData = try? JSONSerialization.data(
+ withJSONObject: object,
+ options: [.prettyPrinted, .sortedKeys]
+ ),
+ let pretty = String(data: prettyData, encoding: .utf8) else {
+ return nil
+ }
+ return pretty
+ }
+
+ /// 민감 값만 가리고 필드명은 남긴다.
static func redact(_ text: String) -> String {
var output = text
- let patterns = [
- #"Bearer\s+[A-Za-z0-9\-._~+/]+=*"#,
- #"(accessToken|refreshToken|Authorization)"\s*:\s*"[^"]+""#,
- ]
- for pattern in patterns {
- if let regex = try? NSRegularExpression(pattern: pattern, options: [.caseInsensitive]) {
- let range = NSRange(output.startIndex..
+ if let bearerRegex = try? NSRegularExpression(
+ pattern: #"Bearer\s+[A-Za-z0-9\-._~+/]+=*"#,
+ options: [.caseInsensitive]
+ ) {
+ let range = NSRange(output.startIndex..?
+
+ override public init() {
+ super.init()
+ }
+
+ public nonisolated func login() async throws -> String {
+ try await loginOnMainActor()
+ }
+
+ private func loginOnMainActor() async throws -> String {
+ guard continuation == nil else {
+ throw SocialAuthError.cancelled
+ }
+
+ return try await withCheckedThrowingContinuation { continuation in
+ self.continuation = continuation
+
+ let request = ASAuthorizationAppleIDProvider().createRequest()
+ request.requestedScopes = [.fullName, .email]
+
+ let controller = ASAuthorizationController(authorizationRequests: [request])
+ controller.delegate = self
+ controller.presentationContextProvider = self
+ controller.performRequests()
+ }
+ }
+
+ private func finish(_ result: Result) {
+ guard let continuation else { return }
+ self.continuation = nil
+
+ switch result {
+ case let .success(token):
+ continuation.resume(returning: token)
+ case let .failure(error):
+ continuation.resume(throwing: error)
+ }
+ }
+
+ private static func resolvePresentationAnchor() -> ASPresentationAnchor {
+ let scenes = UIApplication.shared.connectedScenes.compactMap { $0 as? UIWindowScene }
+ if let keyWindow = scenes.flatMap(\.windows).first(where: \.isKeyWindow) {
+ return keyWindow
+ }
+ if let firstWindow = scenes.flatMap(\.windows).first {
+ return firstWindow
+ }
+ return ASPresentationAnchor()
+ }
+}
+
+extension AppleSocialAuthService: ASAuthorizationControllerDelegate {
+ public nonisolated func authorizationController(
+ controller: ASAuthorizationController,
+ didCompleteWithAuthorization authorization: ASAuthorization
+ ) {
+ Task { @MainActor in
+ guard let credential = authorization.credential as? ASAuthorizationAppleIDCredential else {
+ self.finish(.failure(SocialAuthError.failed))
+ return
+ }
+
+ guard let tokenData = credential.identityToken,
+ let identityToken = String(data: tokenData, encoding: .utf8),
+ identityToken.isEmpty == false else {
+ self.finish(.failure(SocialAuthError.failed))
+ return
+ }
+
+ self.finish(.success(identityToken))
+ }
+ }
+
+ public nonisolated func authorizationController(
+ controller: ASAuthorizationController,
+ didCompleteWithError error: Error
+ ) {
+ Task { @MainActor in
+ if let authError = error as? ASAuthorizationError, authError.code == .canceled {
+ self.finish(.failure(SocialAuthError.cancelled))
+ return
+ }
+ self.finish(.failure(SocialAuthError.failed))
+ }
+ }
+}
+
+extension AppleSocialAuthService: ASAuthorizationControllerPresentationContextProviding {
+ public nonisolated func presentationAnchor(
+ for controller: ASAuthorizationController
+ ) -> ASPresentationAnchor {
+ if Thread.isMainThread {
+ return MainActor.assumeIsolated {
+ AppleSocialAuthService.resolvePresentationAnchor()
+ }
+ }
+ return DispatchQueue.main.sync {
+ MainActor.assumeIsolated {
+ AppleSocialAuthService.resolvePresentationAnchor()
+ }
+ }
+ }
+}
diff --git a/Projects/Core/SocialAuth/Sources/Kakao/KakaoAuthBootstrap.swift b/Projects/Core/SocialAuth/Sources/Kakao/KakaoAuthBootstrap.swift
new file mode 100644
index 0000000..b64be5a
--- /dev/null
+++ b/Projects/Core/SocialAuth/Sources/Kakao/KakaoAuthBootstrap.swift
@@ -0,0 +1,17 @@
+import Foundation
+import SharedLogger
+import ThirdPartyCore
+
+public enum KakaoAuthBootstrap {
+ @MainActor
+ public static func initializeIfNeeded(appKey: String?) {
+ guard let appKey, appKey.isEmpty == false else {
+ Logger.shared.info(
+ "Kakao SDK skipped: KAKAO_NATIVE_APP_KEY is empty",
+ category: .general
+ )
+ return
+ }
+ KakaoSDK.initSDK(appKey: appKey)
+ }
+}
diff --git a/Projects/Core/SocialAuth/Sources/Kakao/KakaoAuthRedirectHandler.swift b/Projects/Core/SocialAuth/Sources/Kakao/KakaoAuthRedirectHandler.swift
new file mode 100644
index 0000000..7c6b13b
--- /dev/null
+++ b/Projects/Core/SocialAuth/Sources/Kakao/KakaoAuthRedirectHandler.swift
@@ -0,0 +1,13 @@
+import Foundation
+import ThirdPartyCore
+
+public enum KakaoAuthRedirectHandler {
+ @MainActor
+ @discardableResult
+ public static func handle(url: URL) -> Bool {
+ if AuthApi.isKakaoTalkLoginUrl(url) {
+ return AuthController.handleOpenUrl(url: url)
+ }
+ return false
+ }
+}
diff --git a/Projects/Core/SocialAuth/Sources/Kakao/KakaoSocialAuthService.swift b/Projects/Core/SocialAuth/Sources/Kakao/KakaoSocialAuthService.swift
new file mode 100644
index 0000000..1ffc2aa
--- /dev/null
+++ b/Projects/Core/SocialAuth/Sources/Kakao/KakaoSocialAuthService.swift
@@ -0,0 +1,43 @@
+import Foundation
+import ThirdPartyCore
+
+/// 카카오 SDK 로그인 후 access token 문자열을 반환한다.
+public struct KakaoSocialAuthService: SocialAuthService {
+ public init() {}
+
+ public func login() async throws -> String {
+ try await withCheckedThrowingContinuation { continuation in
+ Task { @MainActor in
+ let handler: (OAuthToken?, Error?) -> Void = { token, error in
+ if let error {
+ continuation.resume(throwing: Self.mapError(error))
+ return
+ }
+
+ guard let accessToken = token?.accessToken,
+ accessToken.isEmpty == false else {
+ continuation.resume(throwing: SocialAuthError.failed)
+ return
+ }
+
+ continuation.resume(returning: accessToken)
+ }
+
+ if UserApi.isKakaoTalkLoginAvailable() {
+ UserApi.shared.loginWithKakaoTalk(completion: handler)
+ } else {
+ UserApi.shared.loginWithKakaoAccount(completion: handler)
+ }
+ }
+ }
+ }
+
+ private static func mapError(_ error: Error) -> SocialAuthError {
+ if let sdkError = error as? SdkError,
+ sdkError.isClientFailed,
+ sdkError.getClientError().reason == .Cancelled {
+ return .cancelled
+ }
+ return .failed
+ }
+}
diff --git a/Projects/Core/SocialAuth/Sources/NotConfiguredSocialAuthService.swift b/Projects/Core/SocialAuth/Sources/NotConfiguredSocialAuthService.swift
new file mode 100644
index 0000000..89fe743
--- /dev/null
+++ b/Projects/Core/SocialAuth/Sources/NotConfiguredSocialAuthService.swift
@@ -0,0 +1,13 @@
+import Foundation
+
+public struct NotConfiguredSocialAuthService: SocialAuthService {
+ private let message: String
+
+ public init(message: String) {
+ self.message = message
+ }
+
+ public func login() async throws -> String {
+ throw SocialAuthError.notConfigured(message: message)
+ }
+}
diff --git a/Projects/Core/SocialAuth/Sources/SocialAuthConfiguration.swift b/Projects/Core/SocialAuth/Sources/SocialAuthConfiguration.swift
new file mode 100644
index 0000000..c80416f
--- /dev/null
+++ b/Projects/Core/SocialAuth/Sources/SocialAuthConfiguration.swift
@@ -0,0 +1,9 @@
+import Foundation
+
+public struct SocialAuthConfiguration: Sendable {
+ public let kakaoAppKey: String?
+
+ public init(kakaoAppKey: String?) {
+ self.kakaoAppKey = kakaoAppKey
+ }
+}
diff --git a/Projects/Core/SocialAuth/Sources/SocialAuthError.swift b/Projects/Core/SocialAuth/Sources/SocialAuthError.swift
new file mode 100644
index 0000000..3de3bbf
--- /dev/null
+++ b/Projects/Core/SocialAuth/Sources/SocialAuthError.swift
@@ -0,0 +1,7 @@
+import Foundation
+
+public enum SocialAuthError: Error, Sendable, Equatable {
+ case cancelled
+ case failed
+ case notConfigured(message: String)
+}
diff --git a/Projects/Data/Sources/Auth/Service/OAuth/OAuthService.swift b/Projects/Core/SocialAuth/Sources/SocialAuthService.swift
similarity index 52%
rename from Projects/Data/Sources/Auth/Service/OAuth/OAuthService.swift
rename to Projects/Core/SocialAuth/Sources/SocialAuthService.swift
index 5d9b1d3..e2e379f 100644
--- a/Projects/Data/Sources/Auth/Service/OAuth/OAuthService.swift
+++ b/Projects/Core/SocialAuth/Sources/SocialAuthService.swift
@@ -1,6 +1,5 @@
-import Domain
import Foundation
-public protocol OAuthService: Sendable {
+public protocol SocialAuthService: Sendable {
func login() async throws -> String
}
diff --git a/Projects/Core/SocialAuth/Sources/SocialAuthServiceFactory.swift b/Projects/Core/SocialAuth/Sources/SocialAuthServiceFactory.swift
new file mode 100644
index 0000000..2b17476
--- /dev/null
+++ b/Projects/Core/SocialAuth/Sources/SocialAuthServiceFactory.swift
@@ -0,0 +1,22 @@
+import Foundation
+
+public struct SocialAuthServiceFactory: Sendable {
+ public init() {}
+
+ @MainActor
+ public func make(configuration: SocialAuthConfiguration) -> SocialAuthServices {
+ let kakao: any SocialAuthService
+ if let key = configuration.kakaoAppKey, key.isEmpty == false {
+ kakao = KakaoSocialAuthService()
+ } else {
+ kakao = NotConfiguredSocialAuthService(
+ message: "Kakao login is not configured yet"
+ )
+ }
+
+ return SocialAuthServices(
+ kakao: kakao,
+ apple: AppleSocialAuthService()
+ )
+ }
+}
diff --git a/Projects/Core/SocialAuth/Sources/SocialAuthServices.swift b/Projects/Core/SocialAuth/Sources/SocialAuthServices.swift
new file mode 100644
index 0000000..ce22709
--- /dev/null
+++ b/Projects/Core/SocialAuth/Sources/SocialAuthServices.swift
@@ -0,0 +1,11 @@
+import Foundation
+
+public struct SocialAuthServices: Sendable {
+ public let kakao: any SocialAuthService
+ public let apple: any SocialAuthService
+
+ public init(kakao: any SocialAuthService, apple: any SocialAuthService) {
+ self.kakao = kakao
+ self.apple = apple
+ }
+}
diff --git a/Projects/Core/SocialAuth/Tests/SocialAuthServiceFactoryTests.swift b/Projects/Core/SocialAuth/Tests/SocialAuthServiceFactoryTests.swift
new file mode 100644
index 0000000..c1afec8
--- /dev/null
+++ b/Projects/Core/SocialAuth/Tests/SocialAuthServiceFactoryTests.swift
@@ -0,0 +1,42 @@
+@testable import CoreSocialAuth
+import XCTest
+
+final class SocialAuthServiceFactoryTests: XCTestCase {
+ @MainActor
+ func test_카카오키없으면_카카오는_notConfigured() async {
+ let services = SocialAuthServiceFactory().make(
+ configuration: SocialAuthConfiguration(kakaoAppKey: nil)
+ )
+
+ do {
+ _ = try await services.kakao.login()
+ XCTFail("expected notConfigured")
+ } catch let error as SocialAuthError {
+ XCTAssertEqual(
+ error,
+ .notConfigured(message: "Kakao login is not configured yet")
+ )
+ } catch {
+ XCTFail("unexpected \(error)")
+ }
+ }
+
+ @MainActor
+ func test_빈_카카오키면_카카오는_notConfigured() async {
+ let services = SocialAuthServiceFactory().make(
+ configuration: SocialAuthConfiguration(kakaoAppKey: "")
+ )
+
+ do {
+ _ = try await services.kakao.login()
+ XCTFail("expected notConfigured")
+ } catch let error as SocialAuthError {
+ XCTAssertEqual(
+ error,
+ .notConfigured(message: "Kakao login is not configured yet")
+ )
+ } catch {
+ XCTFail("unexpected \(error)")
+ }
+ }
+}
diff --git a/Projects/Data/Project.swift b/Projects/Data/Project.swift
index 4be2406..95ac16f 100644
--- a/Projects/Data/Project.swift
+++ b/Projects/Data/Project.swift
@@ -7,6 +7,7 @@ let project = ProjectFactory.framework(
.domain,
.coreNetwork,
.coreStorage,
+ .coreSocialAuth,
.sharedLogger,
.sharedUtils,
],
@@ -15,5 +16,6 @@ let project = ProjectFactory.framework(
.domain,
.coreNetwork,
.coreStorage,
+ .coreSocialAuth,
]
)
diff --git a/Projects/Data/README.md b/Projects/Data/README.md
index 5b7599c..849d9ca 100644
--- a/Projects/Data/README.md
+++ b/Projects/Data/README.md
@@ -1,17 +1,17 @@
# Data
## 책임
-- DTO, Datasource, `*RepositoryImpl`, `*ClientFactory`, live 조립
+- DTO, Datasource, `*RepositoryImpl`, `*ClientFactory`, Domain 오케스트레이션(`*Client.live`)
## 현재 상태
-- Auth 구현 추가
+- Auth 구현
- DTO/Endpoint/Datasource
- `AuthLocalDatasource`
- `AuthTokenRefresher`
- `AuthRepositoryImpl`
- `AuthClientFactory` (repository → Domain client adapter)
- `AuthClient.live` (plain/refresher/authed 풀 조립)
- - `OAuthService` / `OAuthServiceFactory` (PR1 stub)
+ - `SocialAuthCredentialProvider` (`CoreSocialAuth` credential → Domain `AuthError` 매핑)
## 이후 패턴
- `Data//{Client,DTO,Datasource,Repository,Service}`
@@ -23,13 +23,23 @@
- 금지: Feature
## 내부 규칙
-- Domain `*Client` 의 live 구현(`*Client.live`)은 Data 에서 제공
-- App 은 pure infra 준비 + `prepareDependencies` 등록만 담당
+- Domain `*Client` 의 live 오케스트레이션(`*Client.live`)은 Data 에서 제공
+- 기술 구현(SDK wrapper 등)은 Core/* 에 둔다
+- App 은 config/factory 조립 + `prepareDependencies` 등록만 담당
- Feature 가 Data 를 직접 import 하지 않음
- refresh 는 plain client + `AuthTokenRefresher` 경로
- refresh 실패 시 unauthorized 만 로컬 세션 삭제, badRequest/일시 네트워크 오류는 세션 유지
- request body encode 는 Data remote 에서 수행하고 실패 시 throw
-- OAuth credential 은 `OAuthService` 가 담당 (Souzip 스타일)
+- 소셜 credential 은 `CoreSocialAuth` 가 담당하고, Data 는 provider 선택과 `AuthError` 매핑만 한다
+
+
+## RemoteDatasource 규칙
+- 순수 서버 통신만 담당
+- request body encode 는 Remote 책임
+- encoder 는 Remote 프로퍼티 (`NetworkJSONCoding.makeEncoder()` 기본값)
+- Endpoint 는 path/method/raw body only
+- response 는 공통 envelope 없이 payload DTO 직접 decode
+- OAuth/SDK, 로컬 저장, Domain 매핑은 Remote 밖
## 주요 진입점
- `Sources/Auth/Client/AuthClient+Live.swift`
@@ -38,8 +48,7 @@
- `Sources/Auth/Datasource/AuthRemoteDatasource.swift`
- `Sources/Auth/Datasource/AuthLocalDatasource.swift`
- `Sources/Auth/Service/AuthTokenRefresher.swift`
-- `Sources/Auth/Service/OAuth/OAuthService.swift`
-- `Sources/Auth/Service/OAuth/OAuthServiceFactory.swift`
+- `Sources/Auth/Service/SocialAuth/SocialAuthCredentialProvider.swift`
- `Sources/Auth/Endpoint/AuthEndpoint.swift`
- `Sources/Auth/DTO/*`
@@ -50,7 +59,8 @@
- token refresher rotation 교체 저장
- logout 시 로컬 세션 삭제
- factory credential → repository 연결
-- OAuth stub notConfigured
+- SocialAuth provider 선택 / 에러 매핑
+- CoreSocialAuth live 구현은 수동 검증 (SDK/UI 의존)
## 관련 문서
- [ARCHITECTURE.md](../../docs/ARCHITECTURE.md)
diff --git a/Projects/Data/Sources/Auth/Client/AuthClient+Live.swift b/Projects/Data/Sources/Auth/Client/AuthClient+Live.swift
index 177fd02..addf950 100644
--- a/Projects/Data/Sources/Auth/Client/AuthClient+Live.swift
+++ b/Projects/Data/Sources/Auth/Client/AuthClient+Live.swift
@@ -1,4 +1,5 @@
import CoreNetwork
+import CoreSocialAuth
import CoreStorage
import Domain
import Foundation
@@ -8,7 +9,7 @@ public extension AuthClient {
static func live(
baseURL: URL,
keychain: any KeychainStorage,
- oauthServices: OAuthServices
+ socialAuthServices: SocialAuthServices
) -> AuthClient {
let networkConfiguration = NetworkConfiguration(baseURL: baseURL)
let plainNetworkClient = DefaultNetworkClient.plain(
@@ -42,7 +43,10 @@ public extension AuthClient {
return AuthClientFactory.make(
repository: repository,
credentialProvider: { provider in
- try await oauthServices.service(for: provider).login()
+ try await SocialAuthCredentialProvider.credential(
+ for: provider,
+ services: socialAuthServices
+ )
}
)
}
diff --git a/Projects/Data/Sources/Auth/Datasource/AuthRemoteDatasource.swift b/Projects/Data/Sources/Auth/Datasource/AuthRemoteDatasource.swift
index 42a4e52..aad4170 100644
--- a/Projects/Data/Sources/Auth/Datasource/AuthRemoteDatasource.swift
+++ b/Projects/Data/Sources/Auth/Datasource/AuthRemoteDatasource.swift
@@ -4,40 +4,39 @@ import Foundation
public struct AuthRemoteDatasource: Sendable {
private let plainClient: any NetworkClient
private let authedClient: any NetworkClient
+ private let encoder: JSONEncoder
public init(
plainClient: any NetworkClient,
- authedClient: any NetworkClient
+ authedClient: any NetworkClient,
+ encoder: JSONEncoder = NetworkJSONCoding.makeEncoder()
) {
self.plainClient = plainClient
self.authedClient = authedClient
+ self.encoder = encoder
}
public func loginWithKakao(accessToken: String) async throws -> LoginResponseDTO {
- let body = try makeEncoder().encode(KakaoLoginRequestDTO(accessToken: accessToken))
+ let body = try encoder.encode(KakaoLoginRequestDTO(accessToken: accessToken))
return try await plainClient.request(AuthEndpoint.loginKakao(body))
}
public func loginWithApple(identityToken: String) async throws -> LoginResponseDTO {
- let body = try makeEncoder().encode(AppleLoginRequestDTO(identityToken: identityToken))
+ let body = try encoder.encode(AppleLoginRequestDTO(identityToken: identityToken))
return try await plainClient.request(AuthEndpoint.loginApple(body))
}
public func loginWithDev() async throws -> LoginResponseDTO {
- let body = try makeEncoder().encode(DevLoginRequestDTO())
+ let body = try encoder.encode(DevLoginRequestDTO())
return try await plainClient.request(AuthEndpoint.loginDev(body))
}
public func refresh(refreshToken: String) async throws -> TokenResponseDTO {
- let body = try makeEncoder().encode(RefreshRequestDTO(refreshToken: refreshToken))
+ let body = try encoder.encode(RefreshRequestDTO(refreshToken: refreshToken))
return try await plainClient.request(AuthEndpoint.refresh(body))
}
public func logout() async throws {
try await authedClient.request(AuthEndpoint.logout)
}
-
- private func makeEncoder() -> JSONEncoder {
- NetworkJSONCoding.makeEncoder()
- }
}
diff --git a/Projects/Data/Sources/Auth/Service/OAuth/OAuthServiceFactory.swift b/Projects/Data/Sources/Auth/Service/OAuth/OAuthServiceFactory.swift
deleted file mode 100644
index d5d92b7..0000000
--- a/Projects/Data/Sources/Auth/Service/OAuth/OAuthServiceFactory.swift
+++ /dev/null
@@ -1,48 +0,0 @@
-import Domain
-import Foundation
-
-public struct OAuthServices: Sendable {
- public let kakao: any OAuthService
- public let apple: any OAuthService
-
- public init(kakao: any OAuthService, apple: any OAuthService) {
- self.kakao = kakao
- self.apple = apple
- }
-
- public func service(for provider: AuthProvider) -> any OAuthService {
- switch provider {
- case .kakao:
- return kakao
- case .apple:
- return apple
- }
- }
-}
-
-public enum OAuthServiceFactory {
- /// PR1 stub. 실제 SDK 연동은 후속 구현으로 교체한다.
- public static func makeStub() -> OAuthServices {
- OAuthServices(
- kakao: NotConfiguredOAuthService(provider: .kakao),
- apple: NotConfiguredOAuthService(provider: .apple)
- )
- }
-}
-
-struct NotConfiguredOAuthService: OAuthService {
- let provider: AuthProvider
-
- func login() async throws -> String {
- switch provider {
- case .kakao:
- throw AuthError.notConfigured(
- message: "Kakao login is not configured yet"
- )
- case .apple:
- throw AuthError.notConfigured(
- message: "Apple login is not configured yet"
- )
- }
- }
-}
diff --git a/Projects/Data/Sources/Auth/Service/SocialAuth/SocialAuthCredentialProvider.swift b/Projects/Data/Sources/Auth/Service/SocialAuth/SocialAuthCredentialProvider.swift
new file mode 100644
index 0000000..bff691f
--- /dev/null
+++ b/Projects/Data/Sources/Auth/Service/SocialAuth/SocialAuthCredentialProvider.swift
@@ -0,0 +1,34 @@
+import CoreSocialAuth
+import Domain
+import Foundation
+
+enum SocialAuthCredentialProvider {
+ static func credential(
+ for provider: AuthProvider,
+ services: SocialAuthServices
+ ) async throws -> String {
+ do {
+ switch provider {
+ case .kakao:
+ return try await services.kakao.login()
+ case .apple:
+ return try await services.apple.login()
+ }
+ } catch let error as SocialAuthError {
+ throw map(error)
+ } catch {
+ throw AuthError.loginFailed
+ }
+ }
+
+ private static func map(_ error: SocialAuthError) -> AuthError {
+ switch error {
+ case .cancelled:
+ return .cancelled
+ case .failed:
+ return .loginFailed
+ case let .notConfigured(message):
+ return .notConfigured(message: message)
+ }
+ }
+}
diff --git a/Projects/Data/Tests/Auth/OAuthServiceFactoryTests.swift b/Projects/Data/Tests/Auth/OAuthServiceFactoryTests.swift
deleted file mode 100644
index 255c6f1..0000000
--- a/Projects/Data/Tests/Auth/OAuthServiceFactoryTests.swift
+++ /dev/null
@@ -1,37 +0,0 @@
-@testable import Data
-import Domain
-import XCTest
-
-final class OAuthServiceFactoryTests: XCTestCase {
- func test_카카오_stub_로그인이면_notConfigured() async {
- let services = OAuthServiceFactory.makeStub()
-
- do {
- _ = try await services.service(for: .kakao).login()
- XCTFail("expected notConfigured")
- } catch let error as AuthError {
- XCTAssertEqual(
- error,
- .notConfigured(message: "Kakao login is not configured yet")
- )
- } catch {
- XCTFail("unexpected \(error)")
- }
- }
-
- func test_애플_stub_로그인이면_notConfigured() async {
- let services = OAuthServiceFactory.makeStub()
-
- do {
- _ = try await services.service(for: .apple).login()
- XCTFail("expected notConfigured")
- } catch let error as AuthError {
- XCTAssertEqual(
- error,
- .notConfigured(message: "Apple login is not configured yet")
- )
- } catch {
- XCTFail("unexpected \(error)")
- }
- }
-}
diff --git a/Projects/Data/Tests/Auth/SocialAuthCredentialProviderTests.swift b/Projects/Data/Tests/Auth/SocialAuthCredentialProviderTests.swift
new file mode 100644
index 0000000..2c5d9e8
--- /dev/null
+++ b/Projects/Data/Tests/Auth/SocialAuthCredentialProviderTests.swift
@@ -0,0 +1,122 @@
+@testable import Data
+import CoreSocialAuth
+import Domain
+import XCTest
+
+final class SocialAuthCredentialProviderTests: XCTestCase {
+ func test_카카오_provider면_kakao_service를_호출() async throws {
+ let kakao = StubSocialAuthService(token: "kakao-token")
+ let apple = StubSocialAuthService(token: "apple-token")
+ let services = SocialAuthServices(kakao: kakao, apple: apple)
+
+ let token = try await SocialAuthCredentialProvider.credential(
+ for: .kakao,
+ services: services
+ )
+
+ XCTAssertEqual(token, "kakao-token")
+ let kakaoCalls = await kakao.callCount
+ let appleCalls = await apple.callCount
+ XCTAssertEqual(kakaoCalls, 1)
+ XCTAssertEqual(appleCalls, 0)
+ }
+
+ func test_애플_provider면_apple_service를_호출() async throws {
+ let kakao = StubSocialAuthService(token: "kakao-token")
+ let apple = StubSocialAuthService(token: "apple-token")
+ let services = SocialAuthServices(kakao: kakao, apple: apple)
+
+ let token = try await SocialAuthCredentialProvider.credential(
+ for: .apple,
+ services: services
+ )
+
+ XCTAssertEqual(token, "apple-token")
+ }
+
+ func test_cancelled는_AuthError_cancelled로_매핑() async {
+ let services = SocialAuthServices(
+ kakao: StubSocialAuthService(error: SocialAuthError.cancelled),
+ apple: StubSocialAuthService(token: "unused")
+ )
+
+ do {
+ _ = try await SocialAuthCredentialProvider.credential(
+ for: .kakao,
+ services: services
+ )
+ XCTFail("expected cancelled")
+ } catch let error as AuthError {
+ XCTAssertEqual(error, .cancelled)
+ } catch {
+ XCTFail("unexpected \(error)")
+ }
+ }
+
+ func test_notConfigured는_AuthError_notConfigured로_매핑() async {
+ let services = SocialAuthServices(
+ kakao: StubSocialAuthService(
+ error: .notConfigured(message: "Kakao login is not configured yet")
+ ),
+ apple: StubSocialAuthService(token: "unused")
+ )
+
+ do {
+ _ = try await SocialAuthCredentialProvider.credential(
+ for: .kakao,
+ services: services
+ )
+ XCTFail("expected notConfigured")
+ } catch let error as AuthError {
+ XCTAssertEqual(
+ error,
+ .notConfigured(message: "Kakao login is not configured yet")
+ )
+ } catch {
+ XCTFail("unexpected \(error)")
+ }
+ }
+
+ func test_failed는_AuthError_loginFailed로_매핑() async {
+ let services = SocialAuthServices(
+ kakao: StubSocialAuthService(error: .failed),
+ apple: StubSocialAuthService(token: "unused")
+ )
+
+ do {
+ _ = try await SocialAuthCredentialProvider.credential(
+ for: .kakao,
+ services: services
+ )
+ XCTFail("expected loginFailed")
+ } catch let error as AuthError {
+ XCTAssertEqual(error, .loginFailed)
+ } catch {
+ XCTFail("unexpected \(error)")
+ }
+ }
+}
+
+private actor StubSocialAuthService: SocialAuthService {
+ private let token: String?
+ private let error: SocialAuthError?
+ private(set) var callCount = 0
+
+ init(token: String) {
+ self.token = token
+ self.error = nil
+ }
+
+ init(error: SocialAuthError) {
+ self.token = nil
+ self.error = error
+ }
+
+ func login() async throws -> String {
+ callCount += 1
+ if let error {
+ throw error
+ }
+ return token ?? ""
+ }
+}
diff --git a/Projects/Feature/Sources/AppCoordinator/AppCoordinatorView.swift b/Projects/Feature/Sources/AppCoordinator/AppCoordinatorView.swift
index ce08f65..43a84ba 100644
--- a/Projects/Feature/Sources/AppCoordinator/AppCoordinatorView.swift
+++ b/Projects/Feature/Sources/AppCoordinator/AppCoordinatorView.swift
@@ -34,8 +34,5 @@ public struct AppCoordinatorView: View {
.task {
store.send(.onAppear)
}
- .onOpenURL { url in
- store.send(.deepLinkReceived(url))
- }
}
}
diff --git a/Projects/Shared/Util/Sources/Bundle/AppInfo.swift b/Projects/Shared/Util/Sources/Bundle/AppInfo.swift
index 46a6fab..9b29ac2 100644
--- a/Projects/Shared/Util/Sources/Bundle/AppInfo.swift
+++ b/Projects/Shared/Util/Sources/Bundle/AppInfo.swift
@@ -49,6 +49,11 @@ public enum AppInfo {
return url
}
+ /// 카카오 네이티브 앱 키. 비어 있거나 없으면 nil.
+ public static var kakaoNativeAppKey: String? {
+ string(.kakaoNativeAppKey)
+ }
+
public static func string(_ key: InfoPlistKey) -> String? {
nonEmptyString(fromInfoDictionaryKey: key.rawValue)
}
diff --git a/Projects/Shared/Util/Sources/Bundle/InfoPlistKey.swift b/Projects/Shared/Util/Sources/Bundle/InfoPlistKey.swift
index 9bf8c3f..d9b0caa 100644
--- a/Projects/Shared/Util/Sources/Bundle/InfoPlistKey.swift
+++ b/Projects/Shared/Util/Sources/Bundle/InfoPlistKey.swift
@@ -2,6 +2,7 @@ import Foundation
public enum InfoPlistKey: String, Sendable {
case apiBaseURL = "API_BASE_URL"
+ case kakaoNativeAppKey = "KAKAO_NATIVE_APP_KEY"
case appVersion = "CFBundleShortVersionString"
case buildNumber = "CFBundleVersion"
}
diff --git a/Projects/ThirdParty/ThirdPartyCore/Project.swift b/Projects/ThirdParty/ThirdPartyCore/Project.swift
index 6ed34aa..2c00d5d 100644
--- a/Projects/ThirdParty/ThirdPartyCore/Project.swift
+++ b/Projects/ThirdParty/ThirdPartyCore/Project.swift
@@ -1,7 +1,14 @@
import ProjectDescription
import ProjectDescriptionHelpers
-let project = ProjectFactory.framework(
+let project = ProjectFactory.thirdParty(
.thirdPartyCore,
- dependencies: []
+ packages: [
+ .package(url: "https://github.com/kakao/kakao-ios-sdk", .exact("2.28.0")),
+ ],
+ productDependencies: [
+ .package(product: "KakaoSDKCommon"),
+ .package(product: "KakaoSDKAuth"),
+ .package(product: "KakaoSDKUser"),
+ ]
)
diff --git a/Projects/ThirdParty/ThirdPartyCore/Sources/Exports.swift b/Projects/ThirdParty/ThirdPartyCore/Sources/Exports.swift
index 67d54cb..0413478 100644
--- a/Projects/ThirdParty/ThirdPartyCore/Sources/Exports.swift
+++ b/Projects/ThirdParty/ThirdPartyCore/Sources/Exports.swift
@@ -1,2 +1,3 @@
-/// UI 가 아닌 외부 패키지 re-export 진입점
-public enum ThirdPartyCorePlaceholder {}
+@_exported import KakaoSDKAuth
+@_exported import KakaoSDKCommon
+@_exported import KakaoSDKUser
diff --git a/Tuist/ProjectDescriptionHelpers/DefaultInfoPlist.swift b/Tuist/ProjectDescriptionHelpers/DefaultInfoPlist.swift
index dcb6323..3420edf 100644
--- a/Tuist/ProjectDescriptionHelpers/DefaultInfoPlist.swift
+++ b/Tuist/ProjectDescriptionHelpers/DefaultInfoPlist.swift
@@ -7,6 +7,7 @@ public enum DefaultInfoPlist {
"CFBundleVersion": .string(ProjectEnvironment.appBuildNumber),
"API_BASE_URL": "$(API_BASE_URL)",
+ "KAKAO_NATIVE_APP_KEY": "$(KAKAO_NATIVE_APP_KEY)",
"UILaunchStoryboardName": "LaunchScreen",
"UIUserInterfaceStyle": "Dark",
@@ -22,13 +23,26 @@ public enum DefaultInfoPlist {
"UISceneConfigurations": [:],
],
+ // 카카오톡 로그인 가능 여부 조회용 스킴
+ "LSApplicationQueriesSchemes": [
+ "kakaokompassauth",
+ "kakaolink",
+ "kakaoplus",
+ ],
+
// 딥링크용 커스텀 스킴: mozi://home
+ // 카카오 로그인 콜백: kakao{NATIVE_APP_KEY}://oauth
"CFBundleURLTypes": [
[
"CFBundleTypeRole": "Editor",
"CFBundleURLName": "$(PRODUCT_BUNDLE_IDENTIFIER)",
"CFBundleURLSchemes": ["mozi"],
- ]
+ ],
+ [
+ "CFBundleTypeRole": "Editor",
+ "CFBundleURLName": "kakao-$(PRODUCT_BUNDLE_IDENTIFIER)",
+ "CFBundleURLSchemes": ["kakao$(KAKAO_NATIVE_APP_KEY)"],
+ ],
],
])
diff --git a/Tuist/ProjectDescriptionHelpers/Module.swift b/Tuist/ProjectDescriptionHelpers/Module.swift
index 0c7970f..774c7e3 100644
--- a/Tuist/ProjectDescriptionHelpers/Module.swift
+++ b/Tuist/ProjectDescriptionHelpers/Module.swift
@@ -15,6 +15,7 @@ public enum Module: String, CaseIterable {
case app
case coreNetwork
case coreStorage
+ case coreSocialAuth
public var targetName: String {
switch self {
@@ -30,6 +31,7 @@ public enum Module: String, CaseIterable {
case .app: return "App"
case .coreNetwork: return "CoreNetwork"
case .coreStorage: return "CoreStorage"
+ case .coreSocialAuth: return "CoreSocialAuth"
}
}
@@ -47,6 +49,7 @@ public enum Module: String, CaseIterable {
case .app: return "Projects/App"
case .coreNetwork: return "Projects/Core/Network"
case .coreStorage: return "Projects/Core/Storage"
+ case .coreSocialAuth: return "Projects/Core/SocialAuth"
}
}
@@ -68,6 +71,7 @@ public enum Module: String, CaseIterable {
case .app: return "app"
case .coreNetwork: return "core.network"
case .coreStorage: return "core.storage"
+ case .coreSocialAuth: return "core.socialauth"
}
}
@@ -93,4 +97,5 @@ public extension TargetDependency {
static var feature: TargetDependency { Module.feature.dependency }
static var coreNetwork: TargetDependency { Module.coreNetwork.dependency }
static var coreStorage: TargetDependency { Module.coreStorage.dependency }
+ static var coreSocialAuth: TargetDependency { Module.coreSocialAuth.dependency }
}
diff --git a/Tuist/ProjectDescriptionHelpers/ProjectFactory.swift b/Tuist/ProjectDescriptionHelpers/ProjectFactory.swift
index fba8a44..72046f8 100644
--- a/Tuist/ProjectDescriptionHelpers/ProjectFactory.swift
+++ b/Tuist/ProjectDescriptionHelpers/ProjectFactory.swift
@@ -168,7 +168,8 @@ public enum ProjectFactory {
dependencies: [TargetDependency],
infoPlist: InfoPlist = DefaultInfoPlist.app,
sources: SourceFilesList = ["Sources/**"],
- resources: ResourceFileElements = ["Resources/**"]
+ resources: ResourceFileElements = ["Resources/**"],
+ entitlements: Entitlements? = nil
) -> Project {
let target = Target.target(
name: name,
@@ -179,6 +180,7 @@ public enum ProjectFactory {
infoPlist: infoPlist,
sources: sources,
resources: resources,
+ entitlements: entitlements,
dependencies: dependencies,
settings: ProjectSettings.app()
)
diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md
index f741c17..81d68b2 100644
--- a/docs/ARCHITECTURE.md
+++ b/docs/ARCHITECTURE.md
@@ -6,7 +6,7 @@
세로 계층 = 모듈
가로 Feature = 폴더
외부 의존성 = ThirdParty*
-live 구현 = Data / live 등록 = App only
+기술 구현 = Core/* / 도메인 오케스트레이션 = Data / live 등록 = App only
```
---
@@ -18,7 +18,7 @@ Projects/
Shared/{Util,DesignSystem,Logger}
ThirdParty/{ThirdParty,ThirdPartyUI,ThirdPartyCore}
Domain/
- Core/{Network,Storage}
+ Core/{Network,Storage,SocialAuth}
Data/
Feature/
App/
@@ -31,8 +31,8 @@ Projects/
| SharedLogger | 전역 `Logger.shared` OSLog facade |
| ThirdParty* | 외부 패키지 진입점 |
| Domain | Entity, `*Client`, Error |
-| Core/* | Network/Storage |
-| Data | DTO, Datasource, `*RepositoryImpl`, `*ClientFactory`, `*Client.live` |
+| Core/* | Network/Storage/SocialAuth 등 기술 구현. Domain 모름 |
+| Data | DTO, Datasource, `*RepositoryImpl`, `*ClientFactory`, `*Client.live` (Remote는 순수 서버 통신) |
| Feature | Root, AppCoordinator, Scene |
| App | bootstrap, live 주입, root store |
@@ -53,6 +53,7 @@ App → 조립
Feature → Data / Core* / ThirdPartyCore
Domain → Data / Core* / Feature
Data → Feature
+Core/* → Domain / Data / Feature / App
```
---
@@ -80,7 +81,8 @@ bootstrapping
profileCompleted == true → main(Placeholder)
```
-Auth 인프라와 로그인 게이트 UI 는 존재한다. 소셜 SDK 실연동과 MainTab 은 후속이다.
+Auth 인프라 + 로그인 게이트 + CoreSocialAuth 기반 카카오/애플 연동이 존재한다.
+App 은 SocialAuth factory 조립과 bootstrap/redirect 호출만 담당한다. MainTab 은 후속이다.
---
@@ -91,7 +93,7 @@ Auth 인프라와 로그인 게이트 UI 는 존재한다. 소셜 SDK 실연동
| `Mozi-Debug` | Debug | `com.teamMozi.debug` |
| `Mozi` | Release | `com.teamMozi.app` |
-storage namespace 는 Bundle ID 재사용.
+Keychain service 는 Bundle ID 를 사용하고, UserDefaults 는 standard 를 사용한다.
---
@@ -120,6 +122,7 @@ storage namespace 는 Bundle ID 재사용.
- [Domain](../Projects/Domain/README.md)
- [Data](../Projects/Data/README.md)
- [CoreNetwork](../Projects/Core/Network/README.md)
+- [CoreSocialAuth](../Projects/Core/SocialAuth/README.md)
---