From c1abd913d6754b3e5b9b28a859f14f3e356eb592 Mon Sep 17 00:00:00 2001 From: kimnahun Date: Wed, 18 Feb 2026 04:55:02 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20#30=20-=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85,=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20api=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Application/AppComponent.swift | 10 +++- .../Adapter/TokenProviderAdapter.swift | 3 +- .../Data/Sources/DI/AuthServiceFactory.swift | 5 ++ .../Repository/Auth/AuthRepository.swift | 38 +++++++++++++++ .../Sources/Transform/AuthTransform.swift | 30 ++++++++++++ .../Interface/Auth/AuthServiceProtocol.swift | 7 +-- .../Sources/Model/Auth/LoginResult.swift | 21 +++++++++ .../Sources/Model/Auth/SignupError.swift | 26 ----------- .../RootFeature/Sources/RootBuilder.swift | 8 +++- .../RootFeature/Sources/RootInteractor.swift | 43 ++++++++++++++++- .../RootFeature/Sources/RootRouter.swift | 1 - .../Networks/Sources/DTO/Auth/LoginDTO.swift | 23 ++++++++++ .../ErrorMapping/SignupError+Mapping.swift | 46 ------------------- .../Sources/Service/AuthService.swift | 33 ++++--------- .../Networks/Sources/TargetType/AuthAPI.swift | 8 +++- 15 files changed, 196 insertions(+), 106 deletions(-) create mode 100644 Projects/Data/Sources/Repository/Auth/AuthRepository.swift create mode 100644 Projects/Data/Sources/Transform/AuthTransform.swift create mode 100644 Projects/Domain/Sources/Model/Auth/LoginResult.swift delete mode 100644 Projects/Domain/Sources/Model/Auth/SignupError.swift create mode 100644 Projects/Modules/Networks/Sources/DTO/Auth/LoginDTO.swift delete mode 100644 Projects/Modules/Networks/Sources/ErrorMapping/SignupError+Mapping.swift diff --git a/Projects/App/Sources/Application/AppComponent.swift b/Projects/App/Sources/Application/AppComponent.swift index 4bdb273..f131497 100644 --- a/Projects/App/Sources/Application/AppComponent.swift +++ b/Projects/App/Sources/Application/AppComponent.swift @@ -61,8 +61,16 @@ final class AppComponent: Component, RootDependency { } } + var authRepository: AuthRepositoryInterface { + shared { makeAuthRepository() } + } + + var tokenRepository: TokenRepositoryProtocol { + shared { TokenRepositoryFactory.make() } + } + var tokenProvider: TokenProviding { - shared { TokenRepositoryFactory.makeTokenProvider() } + shared { TokenRepositoryFactory.makeTokenProvider(with: tokenRepository) } } init() { diff --git a/Projects/Data/Sources/Adapter/TokenProviderAdapter.swift b/Projects/Data/Sources/Adapter/TokenProviderAdapter.swift index eb5fa23..c440c32 100644 --- a/Projects/Data/Sources/Adapter/TokenProviderAdapter.swift +++ b/Projects/Data/Sources/Adapter/TokenProviderAdapter.swift @@ -18,7 +18,6 @@ public final class TokenProviderAdapter: TokenProviding, @unchecked Sendable { } public func accessToken() -> String? { -// tokenRepository.get(.accessToken) - "eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJiYmE3ODIwYS0wMDUzLTQxZDctODdhYi00Zjk2ZWM3ZDI1MTMiLCJpYXQiOjE3NzEyMzU1MDUsImV4cCI6MTc3MTMyMTkwNX0.SfuVfF9FFpnFcUkGM7zC7mlE7-f8zo3NgG5mm86xekLnurFhGgnTIhwpew7FinguOex0smsnx--EHsMaED8D5A" + tokenRepository.get(.accessToken) } } diff --git a/Projects/Data/Sources/DI/AuthServiceFactory.swift b/Projects/Data/Sources/DI/AuthServiceFactory.swift index 4db457f..486b15e 100644 --- a/Projects/Data/Sources/DI/AuthServiceFactory.swift +++ b/Projects/Data/Sources/DI/AuthServiceFactory.swift @@ -13,3 +13,8 @@ import Networks public func makeAuthService() -> AuthServiceProtocol { AuthService() } + +public func makeAuthRepository() -> AuthRepositoryInterface { + let service = makeAuthService() + return AuthRepository(service: service) +} diff --git a/Projects/Data/Sources/Repository/Auth/AuthRepository.swift b/Projects/Data/Sources/Repository/Auth/AuthRepository.swift new file mode 100644 index 0000000..ca8ff30 --- /dev/null +++ b/Projects/Data/Sources/Repository/Auth/AuthRepository.swift @@ -0,0 +1,38 @@ +// +// AuthRepository.swift +// Data +// +// Created by kimnahun on 2026-02-18. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +import Domain +import Networks + +public final class AuthRepository: AuthRepositoryInterface { + private let service: AuthServiceProtocol + + public init(service: AuthServiceProtocol) { + self.service = service + } + + public func signup(info: SignupInfo) async throws -> SignupResult { + do { + let request = SignupRequest(fcmToken: info.fcmToken) + return try await service.signup(request: request).toDomain() + } catch { + throw error.toNDGLError() + } + } + + public func login(uuid: String) async throws -> LoginResult { + do { + let request = LoginRequest(uuid: uuid) + return try await service.login(request: request).toDomain() + } catch { + throw error.toNDGLError() + } + } +} diff --git a/Projects/Data/Sources/Transform/AuthTransform.swift b/Projects/Data/Sources/Transform/AuthTransform.swift new file mode 100644 index 0000000..b2027e5 --- /dev/null +++ b/Projects/Data/Sources/Transform/AuthTransform.swift @@ -0,0 +1,30 @@ +// +// AuthTransform.swift +// Data +// +// Created by kimnahun on 2026-02-18. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Domain +import Networks + +extension SignupResponse { + func toDomain() -> SignupResult { + SignupResult( + uuid: uuid, + accessToken: accessToken, + nickname: nickname + ) + } +} + +extension LoginResponse { + func toDomain() -> LoginResult { + LoginResult( + uuid: uuid, + accessToken: accessToken, + nickname: nickname + ) + } +} diff --git a/Projects/Domain/Sources/Interface/Auth/AuthServiceProtocol.swift b/Projects/Domain/Sources/Interface/Auth/AuthServiceProtocol.swift index cca8550..b84814a 100644 --- a/Projects/Domain/Sources/Interface/Auth/AuthServiceProtocol.swift +++ b/Projects/Domain/Sources/Interface/Auth/AuthServiceProtocol.swift @@ -1,5 +1,5 @@ // -// AuthServiceProtocol.swift +// AuthRepositoryInterface.swift // Domain // // Created by NDGL on 2026-02-06. @@ -8,6 +8,7 @@ import Foundation -public protocol AuthServiceProtocol: Sendable { - func signup(info: SignupInfo) async -> Result +public protocol AuthRepositoryInterface { + func signup(info: SignupInfo) async throws -> SignupResult + func login(uuid: String) async throws -> LoginResult } diff --git a/Projects/Domain/Sources/Model/Auth/LoginResult.swift b/Projects/Domain/Sources/Model/Auth/LoginResult.swift new file mode 100644 index 0000000..a3f9efe --- /dev/null +++ b/Projects/Domain/Sources/Model/Auth/LoginResult.swift @@ -0,0 +1,21 @@ +// +// LoginResult.swift +// Domain +// +// Created by kimnahun on 2026-02-18. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +public struct LoginResult: Sendable { + public let uuid: String + public let accessToken: String + public let nickname: String + + public init(uuid: String, accessToken: String, nickname: String) { + self.uuid = uuid + self.accessToken = accessToken + self.nickname = nickname + } +} diff --git a/Projects/Domain/Sources/Model/Auth/SignupError.swift b/Projects/Domain/Sources/Model/Auth/SignupError.swift deleted file mode 100644 index c0dcd9b..0000000 --- a/Projects/Domain/Sources/Model/Auth/SignupError.swift +++ /dev/null @@ -1,26 +0,0 @@ -// -// SignupError.swift -// Domain -// -// Created by kimnahun on 1/21/26. -// Copyright © 2026 NDGL-iOS. All rights reserved. -// - -import Foundation - -// MARK: - SignupField - -public enum SignupField: Sendable, Equatable { - case fcmToken - case email - case unknown(String) -} - -// MARK: - SignupError - -public enum SignupError: Error, Sendable { - case validationFailed(field: SignupField, message: String) - case serverError(message: String?) - case networkError(message: String) - case unknown(code: String, message: String) -} diff --git a/Projects/Features/RootFeature/Sources/RootBuilder.swift b/Projects/Features/RootFeature/Sources/RootBuilder.swift index 08379aa..c40282f 100644 --- a/Projects/Features/RootFeature/Sources/RootBuilder.swift +++ b/Projects/Features/RootFeature/Sources/RootBuilder.swift @@ -16,6 +16,8 @@ import RIBs public protocol RootDependency: Dependency { var homeUsecase: HomeUsecaseProtocol { get } var followDetailUsecase: FollowDetailUsecaseProtocol { get } + var authRepository: AuthRepositoryInterface { get } + var tokenRepository: TokenRepositoryProtocol { get } } // MARK: - RootComponent @@ -47,7 +49,11 @@ public final class RootBuilder: Builder, RootBuildable { public func build() -> LaunchRouting { let component = RootComponent(dependency: dependency) let viewController = RootViewController() - let interactor = RootInteractor(presenter: viewController) + let interactor = RootInteractor( + presenter: viewController, + authRepository: dependency.authRepository, + tokenRepository: dependency.tokenRepository + ) let mainBuilder = MainBuilder(dependency: component) diff --git a/Projects/Features/RootFeature/Sources/RootInteractor.swift b/Projects/Features/RootFeature/Sources/RootInteractor.swift index 94d061e..a45d8d5 100644 --- a/Projects/Features/RootFeature/Sources/RootInteractor.swift +++ b/Projects/Features/RootFeature/Sources/RootInteractor.swift @@ -6,6 +6,9 @@ // Copyright © 2026 NDGL-iOS. All rights reserved. // +import Foundation + +import Domain import RIBs import RxSwift @@ -35,20 +38,58 @@ final class RootInteractor: PresentableInteractor, RootInteract weak var router: RootRouting? weak var listener: RootListener? + private let authRepository: AuthRepositoryInterface + private let tokenRepository: TokenRepositoryProtocol private let disposeBag = DisposeBag() - override init(presenter: RootPresentable) { + init( + presenter: RootPresentable, + authRepository: AuthRepositoryInterface, + tokenRepository: TokenRepositoryProtocol + ) { + self.authRepository = authRepository + self.tokenRepository = tokenRepository super.init(presenter: presenter) presenter.listener = self } override func didBecomeActive() { super.didBecomeActive() + performAuthFlow() } override func willResignActive() { super.willResignActive() } + + private func performAuthFlow() { + Task { [weak self] in + guard let self else { return } + + do { + if let uuid = self.tokenRepository.get(.uuid) { + let loginResult = try await self.authRepository.login(uuid: uuid) + self.tokenRepository.save(loginResult.accessToken, for: .accessToken) + } else { + let fcmToken = self.tokenRepository.get(.fcmToken) ?? UUID().uuidString + let signupResult = try await self.authRepository.signup( + info: SignupInfo(fcmToken: fcmToken) + ) + self.tokenRepository.save(signupResult.uuid, for: .uuid) + self.tokenRepository.save(signupResult.accessToken, for: .accessToken) + + let loginResult = try await self.authRepository.login(uuid: signupResult.uuid) + self.tokenRepository.save(loginResult.accessToken, for: .accessToken) + } + + await MainActor.run { + self.router?.attachMain() + } + } catch { + print("[RootInteractor] Auth flow failed: \(error)") + } + } + } } // MARK: - RootPresentableListener diff --git a/Projects/Features/RootFeature/Sources/RootRouter.swift b/Projects/Features/RootFeature/Sources/RootRouter.swift index d1dca91..8950131 100644 --- a/Projects/Features/RootFeature/Sources/RootRouter.swift +++ b/Projects/Features/RootFeature/Sources/RootRouter.swift @@ -43,7 +43,6 @@ final class RootRouter: LaunchRouter, Ro override func didLoad() { super.didLoad() - attachMain() } // MARK: - RootRouting diff --git a/Projects/Modules/Networks/Sources/DTO/Auth/LoginDTO.swift b/Projects/Modules/Networks/Sources/DTO/Auth/LoginDTO.swift new file mode 100644 index 0000000..4cda947 --- /dev/null +++ b/Projects/Modules/Networks/Sources/DTO/Auth/LoginDTO.swift @@ -0,0 +1,23 @@ +// +// LoginDTO.swift +// Networks +// +// Created by kimnahun on 2026-02-18. +// Copyright © 2026 NDGL-iOS. All rights reserved. +// + +import Foundation + +public struct LoginRequest: Encodable, Sendable { + public let uuid: String + + public init(uuid: String) { + self.uuid = uuid + } +} + +public struct LoginResponse: Decodable, Sendable { + public let uuid: String + public let accessToken: String + public let nickname: String +} diff --git a/Projects/Modules/Networks/Sources/ErrorMapping/SignupError+Mapping.swift b/Projects/Modules/Networks/Sources/ErrorMapping/SignupError+Mapping.swift deleted file mode 100644 index 2bd5200..0000000 --- a/Projects/Modules/Networks/Sources/ErrorMapping/SignupError+Mapping.swift +++ /dev/null @@ -1,46 +0,0 @@ -// -// SignupError+Mapping.swift -// Networks -// -// Created by kimnahun on 1/21/26. -// Copyright © 2026 NDGL-iOS. All rights reserved. -// - -import Domain -import Foundation - -extension SignupError { - public init(code: String, message: String, errors: [ErrorResponse.ErrorDetail]) { - let fieldMessage = errors.first?.message ?? message - - let field: SignupField = { - guard let fieldString = errors.first?.field else { - return .unknown("unknown") - } - switch fieldString { - case "fcmToken": return .fcmToken - case "email": return .email - default: return .unknown(fieldString) - } - }() - - switch code { - case "COMM-01-005": - self = .validationFailed(field: field, message: fieldMessage) - case "COMM-08-001": - self = .serverError(message: message) - default: - self = .unknown(code: code, message: message) - } - } -} - -extension SignupField { - public init(rawValue: String) { - switch rawValue { - case "fcmToken": self = .fcmToken - case "email": self = .email - default: self = .unknown(rawValue) - } - } -} diff --git a/Projects/Modules/Networks/Sources/Service/AuthService.swift b/Projects/Modules/Networks/Sources/Service/AuthService.swift index 8edfef0..d29409e 100644 --- a/Projects/Modules/Networks/Sources/Service/AuthService.swift +++ b/Projects/Modules/Networks/Sources/Service/AuthService.swift @@ -6,10 +6,14 @@ // Copyright © 2026 NDGL-iOS. All rights reserved. // -import Domain import Foundation import Moya +public protocol AuthServiceProtocol: Sendable { + func signup(request: SignupRequest) async throws -> SignupResponse + func login(request: LoginRequest) async throws -> LoginResponse +} + public final class AuthService: AuthServiceProtocol, @unchecked Sendable { private let provider: MoyaProvider @@ -17,28 +21,11 @@ public final class AuthService: AuthServiceProtocol, @unchecked Sendable { self.provider = provider } - public func signup(info: SignupInfo) async -> Result { - // Domain → DTO 변환 - let request = SignupRequest(fcmToken: info.fcmToken) - - let result: NetworkResult = await provider.request( - .signup(request: request), - errorMapper: SignupError.init - ) + public func signup(request: SignupRequest) async throws -> SignupResponse { + try await provider.asyncThowsRequest(.signup(request: request)) + } - // NetworkResult → Result 변환 + DTO → Domain 변환 - switch result { - case .success(let response): - let signupResult = SignupResult( - uuid: response.uuid, - accessToken: response.accessToken, - nickname: response.nickname - ) - return .success(signupResult) - case .failure(let error): - return .failure(error) - case .networkFailure(let error): - return .failure(.networkError(message: error.message)) - } + public func login(request: LoginRequest) async throws -> LoginResponse { + try await provider.asyncThowsRequest(.login(request: request)) } } diff --git a/Projects/Modules/Networks/Sources/TargetType/AuthAPI.swift b/Projects/Modules/Networks/Sources/TargetType/AuthAPI.swift index 6589767..9866431 100644 --- a/Projects/Modules/Networks/Sources/TargetType/AuthAPI.swift +++ b/Projects/Modules/Networks/Sources/TargetType/AuthAPI.swift @@ -6,12 +6,12 @@ // Copyright © 2026 NDGL-iOS. All rights reserved. // -import Alamofire import Foundation import Moya public enum AuthAPI { case signup(request: SignupRequest) + case login(request: LoginRequest) } extension AuthAPI: TargetType { @@ -23,12 +23,14 @@ extension AuthAPI: TargetType { switch self { case .signup: return "/api/v1/auth/users" + case .login: + return "/api/v1/auth/login" } } public var method: Moya.Method { switch self { - case .signup: + case .signup, .login: return .post } } @@ -37,6 +39,8 @@ extension AuthAPI: TargetType { switch self { case .signup(let request): return .requestJSONEncodable(request) + case .login(let request): + return .requestJSONEncodable(request) } }