Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 9 additions & 1 deletion Projects/App/Sources/Application/AppComponent.swift
Original file line number Diff line number Diff line change
Expand Up @@ -61,8 +61,16 @@ final class AppComponent: Component<EmptyDependency>, RootDependency {
}
}

var authRepository: AuthRepositoryInterface {
shared { makeAuthRepository() }
}

var tokenRepository: TokenRepositoryProtocol {
shared { TokenRepositoryFactory.make() }
}

var tokenProvider: TokenProviding {
shared { TokenRepositoryFactory.makeTokenProvider() }
shared { TokenRepositoryFactory.makeTokenProvider(with: tokenRepository) }
}

init() {
Expand Down
3 changes: 1 addition & 2 deletions Projects/Data/Sources/Adapter/TokenProviderAdapter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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)
}
}
5 changes: 5 additions & 0 deletions Projects/Data/Sources/DI/AuthServiceFactory.swift
Original file line number Diff line number Diff line change
Expand Up @@ -13,3 +13,8 @@ import Networks
public func makeAuthService() -> AuthServiceProtocol {
AuthService()
}

public func makeAuthRepository() -> AuthRepositoryInterface {
let service = makeAuthService()
return AuthRepository(service: service)
}
38 changes: 38 additions & 0 deletions Projects/Data/Sources/Repository/Auth/AuthRepository.swift
Original file line number Diff line number Diff line change
@@ -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()
}
}
}
30 changes: 30 additions & 0 deletions Projects/Data/Sources/Transform/AuthTransform.swift
Original file line number Diff line number Diff line change
@@ -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
)
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
//
// AuthServiceProtocol.swift
// AuthRepositoryInterface.swift
// Domain
//
// Created by NDGL on 2026-02-06.
Expand All @@ -8,6 +8,7 @@

import Foundation

public protocol AuthServiceProtocol: Sendable {
func signup(info: SignupInfo) async -> Result<SignupResult, SignupError>
public protocol AuthRepositoryInterface {
func signup(info: SignupInfo) async throws -> SignupResult
func login(uuid: String) async throws -> LoginResult
}
21 changes: 21 additions & 0 deletions Projects/Domain/Sources/Model/Auth/LoginResult.swift
Original file line number Diff line number Diff line change
@@ -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
}
}
26 changes: 0 additions & 26 deletions Projects/Domain/Sources/Model/Auth/SignupError.swift

This file was deleted.

8 changes: 7 additions & 1 deletion Projects/Features/RootFeature/Sources/RootBuilder.swift
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -47,7 +49,11 @@ public final class RootBuilder: Builder<RootDependency>, 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)

Expand Down
43 changes: 42 additions & 1 deletion Projects/Features/RootFeature/Sources/RootInteractor.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,9 @@
// Copyright © 2026 NDGL-iOS. All rights reserved.
//

import Foundation

import Domain
import RIBs
import RxSwift

Expand Down Expand Up @@ -35,20 +38,58 @@ final class RootInteractor: PresentableInteractor<RootPresentable>, 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)")
}
Comment on lines +88 to +90

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

인증 실패 시 print만 하고 있어 사용자에게 어떤 피드백도 제공되지 않습니다.

PR 목표에서 에러 처리가 미구현이라고 언급되어 있지만, 현재 상태로는 인증 실패 시 앱이 빈 화면에 머물게 됩니다. 최소한 재시도 로직이나 에러 화면 전환을 위한 TODO를 명시해두면 추후 추적에 도움이 됩니다.

             } catch {
-                print("[RootInteractor] Auth flow failed: \(error)")
+                // TODO: 인증 실패 시 재시도 또는 에러 화면 처리 필요 (추후 구현)
+                print("[RootInteractor] Auth flow failed: \(error)")
             }

에러 처리 로직(재시도/에러 화면) 구현을 위한 이슈를 생성해 드릴까요?

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Projects/Features/RootFeature/Sources/RootInteractor.swift` around lines 88 -
90, The catch in RootInteractor that currently calls print("[RootInteractor]
Auth flow failed: \(error)") should be replaced with real error handling: log
the error with context, call a new method (e.g., handleAuthError(_:) or
presentAuthError(_:)) on RootInteractor to trigger UI feedback or navigation to
an error screen, and/or enqueue a retry via a retryAuth() helper; also add a
clear TODO comment referencing the new method so a follow-up issue can be
created to implement retry/backoff UX if you don't implement it now.

}
}
Comment on lines +65 to +92

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

Task가 저장·취소되지 않아 Interactor 비활성화 이후에도 인증 플로우가 계속 실행될 수 있습니다.

performAuthFlow()에서 생성된 Task가 프로퍼티에 저장되지 않아, willResignActive() 시점에 취소할 수 없습니다. Interactor가 deactivate된 후에도 네트워크 요청이 완료되면 tokenRepository.save()가 호출되어 의도치 않은 토큰 저장이 발생할 수 있습니다.

🛡️ Task 저장 및 취소 처리 제안
 final class RootInteractor: PresentableInteractor<RootPresentable>, RootInteractable {
 
     weak var router: RootRouting?
     weak var listener: RootListener?
 
     private let authRepository: AuthRepositoryInterface
     private let tokenRepository: TokenRepositoryProtocol
     private let disposeBag = DisposeBag()
+    private var authTask: Task<Void, Never>?
 
     // ...
 
-    override func willResignActive() {
-        super.willResignActive()
-    }
+    override func willResignActive() {
+        super.willResignActive()
+        authTask?.cancel()
+        authTask = nil
+    }
 
     private func performAuthFlow() {
-        Task { [weak self] in
+        authTask = Task { [weak self] in
             guard let self else { return }
             // ...
         }
     }
 }

추가로, Task 내부에서 try Task.checkCancellation()을 주요 await 지점 사이에 넣으면 취소 시 조기 종료가 가능합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Projects/Features/RootFeature/Sources/RootInteractor.swift` around lines 65 -
92, The Task created in performAuthFlow() is not retained or cancelled, so
authentication continues after the Interactor is deactivated; fix by storing the
returned Task in a property (e.g., add a private var authTask: Task<Void,
Never>?), assign authTask = Task { ... } inside performAuthFlow(), and cancel it
in willResignActive() by calling authTask?.cancel(); also sprinkle try
Task.checkCancellation() between major await points (before/after
authRepository.signup/login and before tokenRepository.save) to exit early when
cancelled and avoid unintended tokenRepository.save(...) calls; ensure
router?.attachMain() remains invoked on MainActor only if not cancelled.

}

// MARK: - RootPresentableListener
Expand Down
1 change: 0 additions & 1 deletion Projects/Features/RootFeature/Sources/RootRouter.swift
Original file line number Diff line number Diff line change
Expand Up @@ -43,7 +43,6 @@ final class RootRouter: LaunchRouter<RootInteractable, RootViewControllable>, Ro

override func didLoad() {
super.didLoad()
attachMain()
}

// MARK: - RootRouting
Expand Down
23 changes: 23 additions & 0 deletions Projects/Modules/Networks/Sources/DTO/Auth/LoginDTO.swift
Original file line number Diff line number Diff line change
@@ -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
}
Comment on lines +19 to +23

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

rg -n "keyDecodingStrategy|convertFromSnakeCase|CodingKeys" --type swift -C 3

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 57


🏁 Script executed:

cat -n "Projects/Modules/Networks/Sources/DTO/Auth/LoginDTO.swift"

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 680


🏁 Script executed:

rg -n "JSONDecoder" --type swift -C 2

Repository: YAPP-Github/27th-App-Team-1-iOS

Length of output: 2511


accessToken 프로퍼티의 JSON 키 매핑 문제를 해결하세요.

현재 코드는 JSON 디코딩 시 keyDecodingStrategy를 설정하지 않았으며, CodingKeys도 정의되어 있지 않습니다. 서버가 access_token(snake_case)을 반환하는 경우 디코딩이 실패합니다.

다음 중 하나를 선택하세요:

  • CodingKeys enum을 추가하여 명시적으로 매핑: case accessToken = "access_token"
  • 또는 JSONDecoderkeyDecodingStrategy.convertFromSnakeCase로 설정 (MoyaProvider+Async.swift의 모든 JSONDecoder 인스턴스에 적용 필요)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Projects/Modules/Networks/Sources/DTO/Auth/LoginDTO.swift` around lines 19 -
23, The LoginResponse struct's accessToken will fail to decode if the server
returns snake_case ("access_token"); update the model or decoder: either add a
CodingKeys enum to LoginResponse mapping case accessToken = "access_token"
(refer to LoginResponse and accessToken) or configure
JSONDecoder.keyDecodingStrategy = .convertFromSnakeCase for all decoders created
(update decoder creation in MoyaProvider+Async.swift where JSONDecoder instances
are constructed) so snake_case keys map to camelCase properties.

This file was deleted.

33 changes: 10 additions & 23 deletions Projects/Modules/Networks/Sources/Service/AuthService.swift
Original file line number Diff line number Diff line change
Expand Up @@ -6,39 +6,26 @@
// 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<AuthAPI>

public init(provider: MoyaProvider<AuthAPI> = MoyaProvider<AuthAPI>()) {
self.provider = provider
}

public func signup(info: SignupInfo) async -> Result<SignupResult, SignupError> {
// Domain → DTO 변환
let request = SignupRequest(fcmToken: info.fcmToken)

let result: NetworkResult<SignupResponse, SignupError> = 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))
}
}
Loading