feat: #30 - 회원가입, 로그인 api 연결#31
Conversation
Walkthrough인증 시스템을 리팩토링하여 Result 기반 에러 처리에서 async throws 패턴으로 전환했습니다. AuthRepository와 TokenRepository를 의존성으로 주입받고, 앱 실행 시 회원가입 또는 로그인 플로우를 자동으로 수행하도록 변경했습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant RootInteractor
participant AuthRepository
participant TokenRepository
participant AuthService
participant Network as Network API
RootInteractor->>RootInteractor: activate()
RootInteractor->>RootInteractor: performAuthFlow()
alt UUID가 존재하는 경우
RootInteractor->>AuthRepository: login(uuid: String)
AuthRepository->>AuthService: login(request: LoginRequest)
AuthService->>Network: POST /api/v1/auth/login
Network-->>AuthService: LoginResponse
AuthService-->>AuthRepository: LoginResponse
AuthRepository-->>RootInteractor: LoginResult
RootInteractor->>TokenRepository: save(.accessToken)
else UUID가 없는 경우
RootInteractor->>AuthRepository: signup(info: SignupInfo)
AuthRepository->>AuthService: signup(request: SignupRequest)
AuthService->>Network: POST /api/v1/auth/signup
Network-->>AuthService: SignupResponse
AuthService-->>AuthRepository: SignupResponse
AuthRepository-->>RootInteractor: SignupResult
RootInteractor->>TokenRepository: save(.uuid)
RootInteractor->>TokenRepository: save(.accessToken)
RootInteractor->>AuthRepository: login(uuid: String)
AuthRepository->>AuthService: login(request: LoginRequest)
AuthService->>Network: POST /api/v1/auth/login
Network-->>AuthService: LoginResponse
AuthService-->>AuthRepository: LoginResponse
AuthRepository-->>RootInteractor: LoginResult
RootInteractor->>TokenRepository: save(.accessToken)
end
RootInteractor->>RootInteractor: attachMain()
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. ✨ Finishing touches
🧪 Generate unit tests (beta)
Tip Issue Planner is now in beta. Read the docs and try it out! Share your feedback on Discord. Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (6)
Projects/Domain/Sources/Interface/Auth/AuthServiceProtocol.swift (1)
11-14: 파일명과 프로토콜 이름 불일치.파일 경로는
AuthServiceProtocol.swift이지만, 프로토콜 이름은AuthRepositoryInterface로 변경되었습니다. 파일명도AuthRepositoryInterface.swift로 변경하여 일관성을 유지하는 것이 좋습니다. (헤더 주석 Line 2는 이미AuthRepositoryInterface.swift로 업데이트된 상태입니다.)🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Domain/Sources/Interface/Auth/AuthServiceProtocol.swift` around lines 11 - 14, The protocol name and file name are inconsistent: the file is AuthServiceProtocol.swift but the protocol is declared as AuthRepositoryInterface; rename the protocol to match the file or rename the file to match the protocol—pick one consistent name (e.g., rename the file to AuthRepositoryInterface.swift or rename the protocol to AuthServiceProtocol) and update all references/imports/usages accordingly (search for AuthRepositoryInterface and AuthServiceProtocol across the codebase and update call sites, conforming types, and the header comment that already mentions AuthRepositoryInterface.swift). Ensure the symbols SignupInfo, SignupResult, and LoginResult remain referenced correctly after the rename.Projects/Data/Sources/Transform/AuthTransform.swift (1)
12-30:SignupResult와LoginResult의 동일한 구조 참고.현재
SignupResult와LoginResult가 동일한 필드(uuid,accessToken,nickname)를 가지며, 변환 로직도 동일합니다. 향후 두 타입이 분기할 가능성이 있다면 현 구조가 적합하지만, 그렇지 않다면 하나의AuthResult타입으로 통합하는 것도 고려할 수 있습니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Data/Sources/Transform/AuthTransform.swift` around lines 12 - 30, SignupResponse.toDomain() and LoginResponse.toDomain() produce identical objects (uuid, accessToken, nickname); consolidate by introducing a single AuthResult (or protocol AuthResultProtocol) and update both conversion methods to return/match AuthResult instead of separate SignupResult/LoginResult, or remove duplicate types by replacing SignupResult and LoginResult with AuthResult and adjust all usages to the new type; change references in SignupResponse.toDomain, LoginResponse.toDomain, and any code expecting SignupResult/LoginResult to use AuthResult (or conform to the new protocol) to remove duplication.Projects/Modules/Networks/Sources/TargetType/AuthAPI.swift (1)
38-44:task프로퍼티의 중복 케이스를 하나로 합칠 수 있습니다.
signup과login모두 동일한.requestJSONEncodable(request)패턴을 사용하고 있으므로,method처럼 하나의 case로 합칠 수 있습니다. 현재 구조도 괜찮지만, 향후 case가 늘어날 때 유지보수에 도움이 됩니다.♻️ 선택적 리팩토링 제안
public var task: Moya.Task { switch self { - case .signup(let request): - return .requestJSONEncodable(request) - case .login(let request): + case .signup(let request), .login(let request): return .requestJSONEncodable(request) } }🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Modules/Networks/Sources/TargetType/AuthAPI.swift` around lines 38 - 44, task 프로퍼티에서 signup과 login 케이스가 동일한 처리를 하고 있으니 중복을 제거하세요: AuthAPI의 public var task 구현에서 switch 문을 수정해 .signup(let request), .login(let request)처럼 여러 케이스를 한 줄로 묶고 하나의 반환(.requestJSONEncodable(request))만 사용하도록 변경하면 됩니다; 관련 심볼: task, .signup, .login, .requestJSONEncodable.Projects/Features/RootFeature/Sources/RootInteractor.swift (1)
61-63: SwiftLintunneeded_override경고:willResignActive()가super호출만 하고 있습니다.Task 취소 로직을 추가하지 않는다면 이 override는 제거해도 됩니다. Task 취소를 추가한다면 유지하면서 취소 로직을 넣으세요.
🤖 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 61 - 63, The override of willResignActive() in RootInteractor only calls super and triggers a SwiftLint unneeded_override; either remove the willResignActive() override entirely from the RootInteractor class, or keep it and implement task cancellation logic (cancel any active Task, child Task handles, Combine subscriptions, or async work) and then call super.willResignActive(); refer to the willResignActive() method in RootInteractor to apply the change.Projects/Modules/Networks/Sources/Service/AuthService.swift (2)
17-17:@unchecked Sendable사용에 대한 안전성 확인이 필요합니다.
AuthService가@unchecked Sendable로 선언되어 있습니다. 내부의MoyaProvider는 Alamofire의Session을 사용하며, AlamofireSession은 자체적으로 thread-safe하게 설계되어 있어 현재로는 문제가 없을 가능성이 높습니다. 다만,@unchecked는 컴파일러의 동시성 검증을 우회하므로, 향후 프로퍼티가 추가될 때 주의가 필요합니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Modules/Networks/Sources/Service/AuthService.swift` at line 17, AuthService is marked `@unchecked` Sendable which bypasses Swift's concurrency checks—inspect the class (AuthService) and its stored properties (notably the MoyaProvider instance) and either remove `@unchecked` Sendable or make the safety explicit: ensure all stored properties are immutable (use let) and are Sendable types, or wrap any non-Sendable mutable state (e.g., MoyaProvider/Alamofire Session) behind an actor or synchronize access; then add a succinct comment near the AuthService declaration documenting why it is safe (reference MoyaProvider/Alamofire Session thread-safety) if you keep `@unchecked` Sendable.
25-25:asyncThowsRequest메서드명에 오타가 있습니다 (Thows→Throws).이 메서드는
MoyaProvider+Async.swift의 111번 줄에 정의되어 있으며, 현재 5개의 서비스 파일에서 12번 사용 중입니다 (AuthService, TravelProgramService, UserTravelService, PlaceService, TravelTemplateService). 현재 PR과 무관한 기존 오타이지만, 별도 PR에서 수정하면 코드 가독성이 개선됩니다.🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed. In `@Projects/Modules/Networks/Sources/Service/AuthService.swift` at line 25, 메서드명 오타 asyncThowsRequest을 올바른 asyncThrowsRequest로 수정하세요: MoyaProvider+Async.swift에 정의된 메서 asyncThowsRequest의 선언과 모든 호출부(AuthService의 signup 호출을 비롯해 TravelProgramService, UserTravelService, PlaceService, TravelTemplateService 등 총 12곳)를 동일하게 리네임해 컴파일 오류를 방지하고 코드 가독성을 개선합니다; 선언부 시그니처(비동기 throws 표기)와 익스텐션/프로토콜 구현 위치의 이름을 모두 일관되게 변경하세요.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.
Inline comments:
In `@Projects/Features/RootFeature/Sources/RootInteractor.swift`:
- Around line 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.
- Around line 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.
In `@Projects/Modules/Networks/Sources/DTO/Auth/LoginDTO.swift`:
- Around line 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.
---
Nitpick comments:
In `@Projects/Data/Sources/Transform/AuthTransform.swift`:
- Around line 12-30: SignupResponse.toDomain() and LoginResponse.toDomain()
produce identical objects (uuid, accessToken, nickname); consolidate by
introducing a single AuthResult (or protocol AuthResultProtocol) and update both
conversion methods to return/match AuthResult instead of separate
SignupResult/LoginResult, or remove duplicate types by replacing SignupResult
and LoginResult with AuthResult and adjust all usages to the new type; change
references in SignupResponse.toDomain, LoginResponse.toDomain, and any code
expecting SignupResult/LoginResult to use AuthResult (or conform to the new
protocol) to remove duplication.
In `@Projects/Domain/Sources/Interface/Auth/AuthServiceProtocol.swift`:
- Around line 11-14: The protocol name and file name are inconsistent: the file
is AuthServiceProtocol.swift but the protocol is declared as
AuthRepositoryInterface; rename the protocol to match the file or rename the
file to match the protocol—pick one consistent name (e.g., rename the file to
AuthRepositoryInterface.swift or rename the protocol to AuthServiceProtocol) and
update all references/imports/usages accordingly (search for
AuthRepositoryInterface and AuthServiceProtocol across the codebase and update
call sites, conforming types, and the header comment that already mentions
AuthRepositoryInterface.swift). Ensure the symbols SignupInfo, SignupResult, and
LoginResult remain referenced correctly after the rename.
In `@Projects/Features/RootFeature/Sources/RootInteractor.swift`:
- Around line 61-63: The override of willResignActive() in RootInteractor only
calls super and triggers a SwiftLint unneeded_override; either remove the
willResignActive() override entirely from the RootInteractor class, or keep it
and implement task cancellation logic (cancel any active Task, child Task
handles, Combine subscriptions, or async work) and then call
super.willResignActive(); refer to the willResignActive() method in
RootInteractor to apply the change.
In `@Projects/Modules/Networks/Sources/Service/AuthService.swift`:
- Line 17: AuthService is marked `@unchecked` Sendable which bypasses Swift's
concurrency checks—inspect the class (AuthService) and its stored properties
(notably the MoyaProvider instance) and either remove `@unchecked` Sendable or
make the safety explicit: ensure all stored properties are immutable (use let)
and are Sendable types, or wrap any non-Sendable mutable state (e.g.,
MoyaProvider/Alamofire Session) behind an actor or synchronize access; then add
a succinct comment near the AuthService declaration documenting why it is safe
(reference MoyaProvider/Alamofire Session thread-safety) if you keep `@unchecked`
Sendable.
- Line 25: 메서드명 오타 asyncThowsRequest을 올바른 asyncThrowsRequest로 수정하세요:
MoyaProvider+Async.swift에 정의된 메서 asyncThowsRequest의 선언과 모든 호출부(AuthService의
signup 호출을 비롯해 TravelProgramService, UserTravelService, PlaceService,
TravelTemplateService 등 총 12곳)를 동일하게 리네임해 컴파일 오류를 방지하고 코드 가독성을 개선합니다; 선언부
시그니처(비동기 throws 표기)와 익스텐션/프로토콜 구현 위치의 이름을 모두 일관되게 변경하세요.
In `@Projects/Modules/Networks/Sources/TargetType/AuthAPI.swift`:
- Around line 38-44: task 프로퍼티에서 signup과 login 케이스가 동일한 처리를 하고 있으니 중복을 제거하세요:
AuthAPI의 public var task 구현에서 switch 문을 수정해 .signup(let request), .login(let
request)처럼 여러 케이스를 한 줄로 묶고 하나의 반환(.requestJSONEncodable(request))만 사용하도록 변경하면
됩니다; 관련 심볼: task, .signup, .login, .requestJSONEncodable.
| 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)") | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
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.
| } catch { | ||
| print("[RootInteractor] Auth flow failed: \(error)") | ||
| } |
There was a problem hiding this comment.
인증 실패 시 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.
| public struct LoginResponse: Decodable, Sendable { | ||
| public let uuid: String | ||
| public let accessToken: String | ||
| public let nickname: String | ||
| } |
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
rg -n "keyDecodingStrategy|convertFromSnakeCase|CodingKeys" --type swift -C 3Repository: 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 2Repository: YAPP-Github/27th-App-Team-1-iOS
Length of output: 2511
accessToken 프로퍼티의 JSON 키 매핑 문제를 해결하세요.
현재 코드는 JSON 디코딩 시 keyDecodingStrategy를 설정하지 않았으며, CodingKeys도 정의되어 있지 않습니다. 서버가 access_token(snake_case)을 반환하는 경우 디코딩이 실패합니다.
다음 중 하나를 선택하세요:
CodingKeysenum을 추가하여 명시적으로 매핑:case accessToken = "access_token"- 또는
JSONDecoder의keyDecodingStrategy를.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.
연결된 이슈
작업 내용
인증 플로우
참고
fcmToken으로 전달하고 있습니다. Firebase 연동 후 실제 FCM 토큰으로 교체 필요합니다.
💻 주요 코드 설명
TokenProviderAdapterRootInteractordidBecomeActive()에서 인증 플로우 실행, 성공 시에만attachMain()호출기타 더 이야기해볼 점
Firebase(GoogleService-Info.plist) 연동 후 실제 FCM 토큰 저장 로직 추가 필요
인증 실패 시 재시도 또는 에러 화면 처리 추후 논의 필요
Summary by CodeRabbit
릴리즈 노트
새로운 기능
개선사항