[SPM-244] 유저 로그인, 회원 가입, 토큰 갱신, 자동 로그인, 로그아웃, Authorization 전역 헤더 구현#17
[SPM-244] 유저 로그인, 회원 가입, 토큰 갱신, 자동 로그인, 로그아웃, Authorization 전역 헤더 구현#17Sangyoon98 merged 3 commits intodevfrom
Conversation
Walkthrough인증(토큰 갱신·인터셉터)과 네트워크 세션을 도입하고 AuthViewModel로 인증 상태를 중앙화했으며, TabView 기반의 앱 UI를 재구성하고 로컬라이제이션 키 및 일부 뷰 의존성 주입을 추가했습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant App
participant AuthVM as AuthViewModel
participant UseCase as CheckLoginStateUC
participant Repo as AuthRepository
participant Prefs as AuthPreferences
participant NetMgr as NetworkManager
participant Interc as AuthRequestInterceptor
participant TokenSvc as TokenRefreshService
participant API as AuthAPI
App->>AuthVM: updateLoginState()
AuthVM->>UseCase: execute()
UseCase->>Repo: isSignedIn()
Repo->>Prefs: getStoredUser()/토큰 확인
Prefs-->>Repo: 저장값 반환
Repo-->>UseCase: 로그인 상태 반환
UseCase-->>AuthVM: isLoggedIn 업데이트
AuthVM-->>App: UI 전환 결정
Note over App,Interc: API 요청 흐름 (요약)
App->>NetMgr: request(...)
NetMgr->>Interc: adapt() - 헤더 주입
Interc->>Prefs: getAccessToken()
Prefs-->>Interc: accessToken
Interc-->>NetMgr: 요청 전송(토큰 포함)
NetMgr->>API: 원격호출
alt 401 응답
NetMgr->>Interc: retry() -> TokenRefresh 시도
Interc->>TokenSvc: refreshToken() via Repo/API
TokenSvc->>API: refresh(...)
API-->>TokenSvc: 새 토큰 반환
TokenSvc->>Prefs: saveUser(updatedUser)
TokenSvc-->>Interc: 성공 -> 재시도 허용
Interc-->>NetMgr: 요청 재전송
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested labels
Suggested reviewers
Poem
Pre-merge checks and finishing touches❌ Failed checks (1 warning)
✅ Passed checks (2 passed)
✨ Finishing touches
🧪 Generate unit tests (beta)
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: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
SampoomManagement/Features/Order/UI/OrderListViewModel.swift (1)
33-52: MainActor 컨텍스트 누락으로 인한 데이터 레이스 위험코드를 검증한 결과, 이 변경은 안전하지 않습니다.
GetOrderUseCase는@MainActor로 표시되지 않았으며,execute()메서드도 메인 액터 격리되지 않았습니다. Swift 동시성 규칙에 따르면:
- Line 35의 첫 상태 업데이트는 메인 스레드에서 실행되지만
- Line 38의
try await getOrderUseCase.execute()이후, 실행 컨텍스트가 메인 액터에서 벗어날 수 있음- Lines 39-43, 45-48의 상태 업데이트가 백그라운드 스레드에서 실행되면
@Published속성 수정으로 인한 데이터 레이스가 발생OrderDetailViewModel도 동일한 패턴을 사용하므로 같은 문제를 갖고 있습니다.
필수 수정: 상태 업데이트를
MainActor.run으로 감싸거나,Task { @MainActor in ... }형태로 명시적으로 메인 액터 격리를 선언해야 합니다.private func loadOrderList() { Task { uiState = uiState.copy(orderLoading: true, orderError: nil) do { let orderList = try await getOrderUseCase.execute() await MainActor.run { uiState = uiState.copy( orderList: orderList.items, orderLoading: false, orderError: nil ) } } catch { await MainActor.run { uiState = uiState.copy( orderLoading: false, orderError: error.localizedDescription ) } } print("OrderListViewModel - loadOrderList: \(uiState)") } }또는 더 간단하게:
private func loadOrderList() { Task { @MainActor in uiState = uiState.copy(orderLoading: true, orderError: nil) do { let orderList = try await getOrderUseCase.execute() uiState = uiState.copy( orderList: orderList.items, orderLoading: false, orderError: nil ) } catch { uiState = uiState.copy( orderLoading: false, orderError: error.localizedDescription ) } } }SampoomManagement/Core/Network/NetworkManager.swift (3)
31-36: HTTP 상태코드 검증 누락으로 4xx/5xx가 성공으로 처리될 수 있습니다
.validate()가 없어 전송 계층만 성공이면 4xx/5xx도.success로 들어와 잘못된 디코딩/에러 처리로 이어질 수 있습니다. 상태코드를 2xx로 한정해 주세요.적용 예:
- let dataRequest = session.request( + let dataRequest = session.request( url, method: method, parameters: parameters, encoding: method == .get ? URLEncoding.default : JSONEncoding.default - ) + ).validate()
29-58: Task가 취소돼도 요청이 취소되지 않습니다
withTaskCancellationHandler의onCancel에서DataRequest.cancel()을 호출하지 않아 화면 전환/뒤로가기 시 네트워크가 계속 진행됩니다. 요청 참조를 잡아 취소를 전달하세요.적용 예:
return try await withTaskCancellationHandler(operation: { - try await withCheckedThrowingContinuation { continuation in - let dataRequest = session.request( + var runningRequest: DataRequest? + try await withCheckedThrowingContinuation { continuation in + let req = session.request( url, method: method, parameters: parameters, encoding: method == .get ? URLEncoding.default : JSONEncoding.default - ) + ).validate() + runningRequest = req - dataRequest.responseData { response in + req.responseData { response in switch response.result { ... } } } }, onCancel: { - }) + runningRequest?.cancel() + })
42-53: 민감 정보/토큰이 로그로 노출될 수 있습니다원문 응답과 디코드 객체를 그대로
#if DEBUG), 민감 필드를 마스킹하거나 OSLog 사용을 권장합니다.적용 예:
- print("NetworkManager - Raw response data: \(String(data: data, encoding: .utf8) ?? "Unable to decode")") + #if DEBUG + print("NetworkManager - Raw response data (truncated): \(String(data: data.prefix(512), encoding: .utf8) ?? "Unable to decode")") + #endif ... - print("NetworkManager - Decoded response: \(apiResponse)") + #if DEBUG + print("NetworkManager - Decoded response: \(String(describing: type(of: apiResponse)))") + #endif ... - print("NetworkManager - Decoding error: \(error)") + #if DEBUG + print("NetworkManager - Decoding error: \(error)") + #endif
🧹 Nitpick comments (16)
SampoomManagement/Core/Network/NetworkManager.swift (1)
21-26:Codable→Decodable로 제약 축소 및 불필요 파라미터 제거 제안응답은 디코딩만 필요합니다. 제네릭을
T: Decodable로 완화하고, 타입 추론이 가능하면responseType파라미터는 제거해 깔끔하게 유지하세요. 기존 호출부 영향이 있으면 유지해도 무방합니다.적용 예:
- func request<T: Codable>( + func request<T: Decodable>( endpoint: String, method: HTTPMethod = .get, parameters: Parameters? = nil, - responseType: T.Type + responseType: T.Type ) async throws -> APIResponse<T> {또는 호출부에서 타입이 명확하면
responseType제거를 고려.SampoomManagement/Features/Auth/UI/AuthViewModel.swift (1)
40-46:에러 메시지를
OSLog.Logger로 레벨 구분하고, 운영 빌드에서 과도한 정보가 남지 않도록 조정해 주세요.예시:
import OSLog private let logger = Logger(subsystem: "SampoomManagement", category: "Auth") do { try await signOutUseCase.execute() } catch { logger.error("Sign out failed: \(String(describing: error))") }Also applies to: 52-62
SampoomManagement/App/RootView.swift (3)
15-16:@ObservedObject초기화 스타일 일관화초기화에서 래퍼 형식(
_authViewModel = ObservedObject(wrappedValue:))을 쓰면 SwiftUI 경고를 피하고 일관성이 좋아집니다. 기능은 동일합니다.적용 예:
- @ObservedObject private var authViewModel: AuthViewModel + @ObservedObject private var authViewModel: AuthViewModel ... - self.authViewModel = dependencies.authViewModel + _authViewModel = ObservedObject(wrappedValue: dependencies.authViewModel)Also applies to: 22-23
74-79:onChange(of:_:_)는 iOS 17+ 전용 API입니다 — 최소 타겟 확인 필요프로젝트 최소 타겟이 iOS 16이면 빌드가 실패합니다. 타겟을 확인하거나 iOS 16 호환 시그니처로 대체하세요.
iOS 16 호환 예:
- .onChange(of: authViewModel.shouldNavigateToLogin) { _, shouldNavigate in + .onChange(of: authViewModel.shouldNavigateToLogin) { shouldNavigate in if shouldNavigate { showSignUp = false authViewModel.resetNavigationState() } }
41-45: 성공 시 UI 상태도 함께 정리로그인/회원가입 성공 시
updateLoginState()만으로도 화면 전환되지만, 잔여 상태를 정리하면 이후 플로우가 명확해집니다.showSignUp = false를 함께 설정하는 것을 권장합니다.적용 예:
- authViewModel.updateLoginState() + authViewModel.updateLoginState() + showSignUp = false ... - authViewModel.updateLoginState() + authViewModel.updateLoginState() + showSignUp = falseAlso applies to: 61-68
SampoomManagement/Features/Auth/Data/Repository/AuthRepositoryImpl.swift (1)
106-108:clearTokens()에 불필요한async throws내부가 동기·비예외 동작이면 시그니처를 단순화하세요. 현재 프로토콜 제약 때문에 유지해야 한다면 주석으로 의도를 남겨 혼란을 줄이세요.
예:
// NOTE: 현재는 동기/비예외지만 프로토콜 정합성 및 향후 비동기/에러 전파 대비해 async throws 유지 func clearTokens() async throws { preferences.clear() }SampoomManagement/Features/Auth/Domain/Repository/AuthRepository.swift (1)
22-24:clearTokens()시그니처 정합성 재검토구현이 동기·비예외라면
async throws는 과합니다. 향후 확장을 의도한 것이면 주석으로 명시하거나, 지금은func clearTokens()로 단순화 후 필요 시 승격하는 방식을 권장합니다. 호출부/유즈케이스 영향 범위를 함께 검토해 주세요.SampoomManagement/Features/Auth/Data/Local/Preferences/AuthPreferences.swift (3)
16-21: 키 네임스페이스 상수화 제안
"auth.*"접두사를 상수로 분리하면 오타/변경 리스크를 줄일 수 있습니다. 예:private static let ns = "auth."; static let userId = ns + "userId"등.
22-40: 부분 실패 롤백 처리 좋습니다.토큰/유저 필드 동시 저장 시 롤백으로 일관성 보장한 점 좋습니다. 추후 일관성 강화를 위해 단일 JSON(직렬화된 User)을 한 키에 저장하는 옵션도 고려해볼 수 있습니다.
54-79: 파싱 견고성 및 타입 폭 제안
expiresIn이 큰 값일 수 있으므로Int64(또는TimeInterval) 사용 검토 바랍니다.- 일부 키만 존재하는 부분 장애 상태가 반복되면
return nil대신clear()로 정리하거나, 최소한 경고 로그를 한 번만 남기도록 스로틀링을 고려하세요.SampoomManagement/App/Screens/DashboardScreen.swift (1)
27-36: 로그아웃 확인 다이얼로그/파괴적 역할 추가 권장실수 방지를 위해 확인 시트/알럿을 두고, 가능한 경우 버튼
role: .destructive를 적용하세요. 커스텀 CommonButton이 role 미지원이면 상위에서.alert로 감싸는 방식 권장.로그아웃 시 네비게이션 스택(
ordersNavigationPath,partsNavigationPath) 초기화가 상위(Root)에서 보장되는지 확인 부탁드립니다.SampoomManagement/Core/Network/TokenRefreshService.swift (2)
22-41: 하드코딩된 URL과 타임아웃/헤더 보완
- Base URL/경로를 구성에서 주입하세요(.plist/DI).
- 네트워크 지연 대비 타임아웃 설정을 권장합니다.
- 서버가 JSON만 반환한다면
Accept: application/json추가.예시:
- let url = URL(string: "https://sampoom.store/api/auth/refresh")! + let url = URL(string: baseURL.appendingPathComponent("/api/auth/refresh"))! @@ - request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("application/json", forHTTPHeaderField: "Content-Type") + request.setValue("application/json", forHTTPHeaderField: "Accept") + request.timeoutInterval = 15
45-61: 저장 경로 일관성 및 에러 맵핑
getStoredUser()부재로tokenRefreshFailed를 던지는 것은 합리적입니다. 추가로 저장 실패 시AuthError.tokenSaveFailed로 래핑하여 상위에서 원인 구분이 가능하도록 하는 것도 고려해보세요. 현재 인터셉터와 레포에서의 에러 정책을 일치시켜 주세요.레포의
AuthRepositoryImpl.refreshToken()과 본 서비스의 동작이 중복됩니다. 하나로 통합하거나 호출 계층을 명확히 해 중복 갱신을 피하는지 확인 부탁드립니다.SampoomManagement/App/ContentView.swift (1)
35-43: Tab(value:) 및 role 사용의 iOS 타겟 확인
Tab(value:)/role: .search는 최신 SwiftUI API입니다. 프로젝트 최소 iOS 버전에서 컴파일/동작 가능한지 확인하세요. 필요 시 기존.tabItem {}+.tag(...)패턴으로 폴백을 준비하세요.다음도 함께 점검 바랍니다:
- 로그아웃 시
selectedTab초기화 필요 여부(예: .dashboard).- 로그아웃 후
ordersNavigationPath/partsNavigationPathreset 타이밍.Also applies to: 50-64, 66-83, 85-110, 112-140, 141-143
SampoomManagement/Core/DI/AppDependencies.swift (2)
25-25: AuthViewModel 생성 패턴 통일 + 지연 초기화 제안현재 DI에서 대부분의 VM은 factory 메서드로 생성하지만, AuthViewModel만 상수 프로퍼티로 즉시 생성되어 패턴이 혼재합니다. 앱 시작 시 불필요한 초기화를 줄이고 일관성을 위해 lazy 또는 factory로 통일하는 것을 권장합니다. 또한 AuthViewModel이 @mainactor 컨텍스트를 요구한다면 메인 스레드에서 초기화되는지 확인해주세요.
적용 예시(lazy로 전환):
- let authViewModel: AuthViewModel + lazy var authViewModel: AuthViewModel = { + AuthViewModel( + checkLoginStateUseCase: checkLoginStateUseCase, + signOutUseCase: signOutUseCase, + clearTokensUseCase: clearTokensUseCase + ) + }()init 내 즉시 생성 제거:
- // Auth ViewModel - authViewModel = AuthViewModel( - checkLoginStateUseCase: checkLoginStateUseCase, - signOutUseCase: signOutUseCase, - clearTokensUseCase: clearTokensUseCase - )또는 다른 VM과 동일하게 makeAuthViewModel() 팩토리로 노출하는 방식을 선택해도 좋습니다.
Also applies to: 93-98
89-91: SignOutUseCase vs ClearTokensUseCase 역할 경계 명확화두 유스케이스의 책임이 겹쳐 보일 수 있습니다.
- 예: 사용자 요청 로그아웃(SignOut)과 세션 만료 등 비자발적 로그아웃(ClearTokens) 분리라면, 간단한 문서화/주석으로 의도를 명확히 해두는 것을 권장합니다. 필요 시 네이밍 보완도 고려해주세요(예: PurgeAuthStateUseCase).
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (28)
SampoomManagement/App/ContentView.swift(1 hunks)SampoomManagement/App/RootView.swift(1 hunks)SampoomManagement/App/SampoomManagementApp.swift(2 hunks)SampoomManagement/App/Screens/DashboardScreen.swift(1 hunks)SampoomManagement/Core/DI/AppDependencies.swift(2 hunks)SampoomManagement/Core/Network/AuthRequestInterceptor.swift(1 hunks)SampoomManagement/Core/Network/NetworkError.swift(4 hunks)SampoomManagement/Core/Network/NetworkManager.swift(2 hunks)SampoomManagement/Core/Network/TokenRefreshService.swift(1 hunks)SampoomManagement/Core/Resources/StringResources.swift(2 hunks)SampoomManagement/Features/Auth/Data/Local/Preferences/AuthPreferences.swift(3 hunks)SampoomManagement/Features/Auth/Data/Mappers/AuthMappers.swift(1 hunks)SampoomManagement/Features/Auth/Data/Remote/API/AuthAPI.swift(1 hunks)SampoomManagement/Features/Auth/Data/Remote/DTO/RefreshRequestDTO.swift(1 hunks)SampoomManagement/Features/Auth/Data/Remote/DTO/RefreshResponseDTO.swift(1 hunks)SampoomManagement/Features/Auth/Data/Repository/AuthRepositoryImpl.swift(1 hunks)SampoomManagement/Features/Auth/Domain/Models/User.swift(1 hunks)SampoomManagement/Features/Auth/Domain/Repository/AuthRepository.swift(1 hunks)SampoomManagement/Features/Auth/Domain/UseCase/CheckLoginStateUseCase.swift(1 hunks)SampoomManagement/Features/Auth/Domain/UseCase/ClearTokensUseCase.swift(1 hunks)SampoomManagement/Features/Auth/Domain/UseCase/SignOutUseCase.swift(1 hunks)SampoomManagement/Features/Auth/UI/AuthViewModel.swift(1 hunks)SampoomManagement/Features/Auth/UI/LoginView.swift(2 hunks)SampoomManagement/Features/Cart/UI/CartListView.swift(2 hunks)SampoomManagement/Features/Order/UI/OrderListViewModel.swift(1 hunks)SampoomManagement/Features/Part/UI/PartDetailBottomSheetView.swift(1 hunks)SampoomManagement/Features/Part/UI/PartListView.swift(1 hunks)SampoomManagement/Features/Part/UI/PartView.swift(3 hunks)
🧰 Additional context used
🧬 Code graph analysis (20)
SampoomManagement/Features/Auth/Domain/Models/User.swift (2)
SampoomManagement/Core/Network/TokenRefreshService.swift (1)
refreshToken(17-62)SampoomManagement/Features/Auth/Data/Repository/AuthRepositoryImpl.swift (1)
refreshToken(71-104)
SampoomManagement/Features/Auth/Data/Mappers/AuthMappers.swift (2)
SampoomManagement/Core/Network/TokenRefreshService.swift (1)
refreshToken(17-62)SampoomManagement/Features/Auth/Data/Repository/AuthRepositoryImpl.swift (1)
refreshToken(71-104)
SampoomManagement/Core/Resources/StringResources.swift (1)
SampoomManagement/Features/Part/UI/PartViewModel.swift (1)
selectCategory(62-67)
SampoomManagement/Features/Auth/Domain/Repository/AuthRepository.swift (3)
SampoomManagement/Core/Network/TokenRefreshService.swift (1)
refreshToken(17-62)SampoomManagement/Features/Auth/Data/Repository/AuthRepositoryImpl.swift (5)
refreshToken(71-104)clearTokens(106-108)isSignedIn(110-112)getAccessToken(115-117)getRefreshToken(119-121)SampoomManagement/Features/Auth/Data/Local/Preferences/AuthPreferences.swift (2)
getAccessToken(81-83)getRefreshToken(85-87)
SampoomManagement/Features/Cart/UI/CartListView.swift (1)
SampoomManagement/Core/DI/AppDependencies.swift (1)
makeOrderDetailViewModel(193-200)
SampoomManagement/Features/Auth/Domain/UseCase/ClearTokensUseCase.swift (3)
SampoomManagement/Features/Auth/Domain/UseCase/CheckLoginStateUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Auth/Domain/UseCase/SignOutUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Auth/Data/Repository/AuthRepositoryImpl.swift (1)
clearTokens(106-108)
SampoomManagement/Features/Auth/Data/Remote/DTO/RefreshResponseDTO.swift (2)
SampoomManagement/Core/Network/TokenRefreshService.swift (1)
refreshToken(17-62)SampoomManagement/Features/Auth/Data/Repository/AuthRepositoryImpl.swift (1)
refreshToken(71-104)
SampoomManagement/Features/Order/UI/OrderListViewModel.swift (2)
SampoomManagement/Features/Order/UI/OrderListUiState.swift (1)
copy(25-35)SampoomManagement/Features/Order/Domain/UseCase/GetOrderUseCase.swift (1)
execute(17-19)
SampoomManagement/App/Screens/DashboardScreen.swift (1)
SampoomManagement/Features/Auth/UI/AuthViewModel.swift (1)
signOut(40-50)
SampoomManagement/Features/Auth/Data/Local/Preferences/AuthPreferences.swift (3)
SampoomManagement/Features/Auth/Data/Local/Preferences/KeychainManager.swift (3)
save(24-43)delete(73-85)get(45-71)SampoomManagement/Core/Network/TokenRefreshService.swift (1)
refreshToken(17-62)SampoomManagement/Features/Auth/Data/Repository/AuthRepositoryImpl.swift (1)
refreshToken(71-104)
SampoomManagement/Features/Auth/Data/Remote/API/AuthAPI.swift (3)
SampoomManagement/Core/Network/NetworkManager.swift (1)
request(21-59)SampoomManagement/Core/Network/TokenRefreshService.swift (1)
refreshToken(17-62)SampoomManagement/Features/Auth/Data/Repository/AuthRepositoryImpl.swift (1)
refreshToken(71-104)
SampoomManagement/Features/Auth/Domain/UseCase/SignOutUseCase.swift (4)
SampoomManagement/Features/Auth/Domain/UseCase/CheckLoginStateUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Auth/Domain/UseCase/ClearTokensUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Auth/UI/AuthViewModel.swift (1)
signOut(40-50)SampoomManagement/Features/Auth/Data/Repository/AuthRepositoryImpl.swift (1)
signOut(59-69)
SampoomManagement/Features/Auth/Data/Remote/DTO/RefreshRequestDTO.swift (2)
SampoomManagement/Core/Network/TokenRefreshService.swift (1)
refreshToken(17-62)SampoomManagement/Features/Auth/Data/Repository/AuthRepositoryImpl.swift (1)
refreshToken(71-104)
SampoomManagement/App/ContentView.swift (1)
SampoomManagement/Core/DI/AppDependencies.swift (7)
makePartViewModel(147-152)makeOutboundListViewModel(169-177)makeCartListViewModel(179-187)makeOrderListViewModel(189-191)makeOrderDetailViewModel(193-200)makeSearchViewModel(164-167)makePartListViewModel(154-158)
SampoomManagement/Features/Auth/UI/AuthViewModel.swift (4)
SampoomManagement/Features/Auth/Domain/UseCase/CheckLoginStateUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Auth/Domain/UseCase/ClearTokensUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Auth/Domain/UseCase/SignOutUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Auth/Data/Repository/AuthRepositoryImpl.swift (1)
signOut(59-69)
SampoomManagement/Features/Auth/Data/Repository/AuthRepositoryImpl.swift (5)
SampoomManagement/Features/Auth/Data/Mappers/AuthMappers.swift (1)
toModel(11-20)SampoomManagement/Features/Auth/Data/Local/Preferences/AuthPreferences.swift (4)
saveUser(22-40)clear(108-120)getRefreshToken(85-87)getStoredUser(54-79)SampoomManagement/Features/Auth/UI/AuthViewModel.swift (1)
signOut(40-50)SampoomManagement/Features/Auth/Data/Remote/API/AuthAPI.swift (2)
logout(71-78)refresh(81-94)SampoomManagement/Core/Network/TokenRefreshService.swift (1)
refreshToken(17-62)
SampoomManagement/Core/Network/AuthRequestInterceptor.swift (2)
SampoomManagement/Features/Auth/Data/Local/Preferences/AuthPreferences.swift (2)
getAccessToken(81-83)clear(108-120)SampoomManagement/Core/Network/TokenRefreshService.swift (1)
refreshToken(17-62)
SampoomManagement/Features/Auth/Domain/UseCase/CheckLoginStateUseCase.swift (1)
SampoomManagement/Features/Auth/Data/Repository/AuthRepositoryImpl.swift (1)
isSignedIn(110-112)
SampoomManagement/App/RootView.swift (1)
SampoomManagement/Features/Auth/UI/AuthViewModel.swift (2)
updateLoginState(36-38)resetNavigationState(65-67)
SampoomManagement/Core/Network/TokenRefreshService.swift (2)
SampoomManagement/Features/Auth/Data/Repository/AuthRepositoryImpl.swift (2)
refreshToken(71-104)getRefreshToken(119-121)SampoomManagement/Features/Auth/Data/Local/Preferences/AuthPreferences.swift (3)
getRefreshToken(85-87)getStoredUser(54-79)saveUser(22-40)
🔇 Additional comments (25)
SampoomManagement/Features/Order/UI/OrderListViewModel.swift (1)
14-14: 코드 구성이 개선되었습니다.MARK 주석을 추가하여 코드의 가독성과 탐색성이 향상되었습니다.
Also applies to: 19-19, 24-24, 32-32
SampoomManagement/Features/Auth/UI/LoginView.swift (2)
20-24: LGTM! 의존성 주입 패턴이 올바르게 적용되었습니다.커스텀 이니셜라이저를 통해 뷰모델과 성공/회원가입 네비게이션 콜백을 주입받는 구조가 적절합니다.
112-116: LGTM! 로그인 성공 핸들러가 올바르게 구현되었습니다.onChange 핸들러의 순서 변경은 기능에 영향을 주지 않으며, 성공 시 콜백 실행 로직이 적절합니다.
SampoomManagement/Features/Auth/Data/Remote/DTO/RefreshResponseDTO.swift (1)
10-13: LGTM! 토큰 갱신 응답 DTO가 올바르게 정의되었습니다.필드 타입과 네이밍이 적절하며, TokenRefreshService와 AuthRepositoryImpl에서 사용되는 구조와 일치합니다.
SampoomManagement/Features/Part/UI/PartDetailBottomSheetView.swift (1)
173-173: LGTM! 지역화 리소스 적용이 적절합니다.하드코딩된 문자열을 StringResources로 변경하여 일관성 있는 지역화 패턴을 따르고 있습니다.
SampoomManagement/Features/Cart/UI/CartListView.swift (2)
13-13: LGTM! 의존성 주입 구조가 올바르게 적용되었습니다.AppDependencies를 통한 중앙화된 DI 패턴이 적절하게 구현되었습니다.
54-54: LGTM! 뷰모델 생성이 DI 컨테이너로 이동되었습니다.OrderDetailViewModel을 dependencies.makeOrderDetailViewModel을 통해 생성하도록 변경하여 일관된 의존성 관리를 구현했습니다.
SampoomManagement/Features/Part/UI/PartListView.swift (1)
68-68: LGTM! 네비게이션 타이틀 지역화가 올바르게 적용되었습니다.하드코딩된 문자열을 StringResources로 교체하여 지역화 일관성을 개선했습니다.
SampoomManagement/Features/Auth/Data/Remote/DTO/RefreshRequestDTO.swift (1)
10-11: LGTM! 토큰 갱신 요청 DTO가 올바르게 정의되었습니다.단순하고 명확한 구조로 토큰 갱신 API 요청에 적절합니다.
SampoomManagement/Core/Network/NetworkError.swift (2)
17-17: LGTM! unauthorized 에러 케이스가 적절하게 추가되었습니다.인증 실패 처리를 위한 에러 케이스와 설명이 올바르게 정의되었습니다.
Also applies to: 33-34
44-45: LGTM! 인증 관련 에러 케이스가 적절하게 추가되었습니다.tokenRefreshFailed와 unauthorized 케이스가 토큰 갱신 플로우를 지원하도록 올바르게 구현되었습니다.
Also applies to: 57-60
SampoomManagement/App/SampoomManagementApp.swift (2)
19-19: LGTM! 전역 백그라운드 설정이 초기화에 추가되었습니다.앱 전체의 일관된 UI 설정을 위해 적절한 위치에서 호출되고 있습니다.
39-51: "Background" 컬러 애셋 존재 확인 완료 - 우려사항 없음검증 결과, "Background" 컬러 애셋이
SampoomManagement/Resources/Assets.xcassets/Background.colorset/에 정상적으로 존재합니다. 코드는 if-let 바인딩으로 안전하게 처리하고 있으며, 백그라운드 컬러가 정상적으로 적용됩니다.SampoomManagement/Features/Auth/Domain/Models/User.swift (1)
14-15: 토큰 필드 추가가 적절합니다.User 모델에
accessToken과refreshToken필드를 추가하여 토큰 관리 기능을 지원합니다. 관련 파일들(AuthMappers, TokenRefreshService, AuthRepositoryImpl)에서 일관되게 사용되고 있으며, AuthPreferences를 통해 키체인에 안전하게 저장됩니다.SampoomManagement/Features/Auth/Domain/UseCase/CheckLoginStateUseCase.swift (1)
10-20: Clean Architecture 패턴을 잘 따르고 있습니다.로그인 상태 확인 로직을 use case로 캡슐화하여 책임을 명확히 분리하였으며, 의존성 주입을 통해 테스트 가능한 구조를 갖추었습니다.
SampoomManagement/Features/Auth/Domain/UseCase/SignOutUseCase.swift (1)
10-20: 로그아웃 로직이 올바르게 캡슐화되었습니다.Use case 패턴을 통해 로그아웃 로직을 도메인 레이어에서 명확히 정의하였으며, repository에 위임하여 책임을 분리하였습니다.
SampoomManagement/Features/Part/UI/PartView.swift (1)
32-32: 로컬라이제이션 개선이 우수합니다.하드코딩된 문자열을
StringResources로 교체하여 유지보수성과 향후 다국어 지원 가능성을 높였습니다.Also applies to: 58-60, 129-129, 137-137
SampoomManagement/Features/Auth/Data/Mappers/AuthMappers.swift (1)
16-17: 매핑 로직이 User 모델 변경과 일관됩니다.
LoginResponseDTO에서User로 변환 시 새로 추가된 토큰 필드들을 올바르게 매핑하고 있습니다.SampoomManagement/Features/Auth/Domain/UseCase/ClearTokensUseCase.swift (1)
10-20: 토큰 초기화 로직이 명확히 분리되었습니다.토큰 만료 시 토큰을 초기화하는 책임을 use case로 캡슐화하여 도메인 로직을 명확히 정의하였습니다.
SampoomManagement/Core/Resources/StringResources.swift (1)
134-137: 문자열 리소스가 적절히 추가되었습니다.Part 및 Auth 기능에 필요한 새로운 문자열 리소스를 일관된 방식으로 추가하였습니다.
Also applies to: 174-174
SampoomManagement/Features/Auth/Data/Remote/API/AuthAPI.swift (2)
70-78: 로그아웃 API 메서드가 올바르게 구현되었습니다.로그아웃 엔드포인트 호출이 적절하며,
NetworkManager의 interceptor를 통해 Authorization 헤더가 자동으로 추가됩니다.
81-94: 토큰 갱신 API 메서드가 올바르게 구현되었습니다.
refreshToken을 파라미터로 받아 API를 호출하는 로직이 명확합니다.NetworkManager를 사용하는 이 메서드는 일반적인 API 호출 경로이며,TokenRefreshService는 interceptor 순환 참조를 피하기 위해 URLSession을 직접 사용하는 별도 경로입니다.SampoomManagement/Features/Auth/Data/Repository/AuthRepositoryImpl.swift (1)
71-104: API 새로고침 요청의 인터셉터 이중 경로 문제 및 clearTokens 시그니처 불일치코드 검토 결과, TokenRefreshService가
URLSession.shared를 사용해 인터셉터를 회피하므로 무한 재귀 위험은 완화되어 있습니다. 그러나 두 가지 실질적인 문제가 있습니다:
토큰 갱신 경로의 불일치 (AuthRepositoryImpl 71-104줄):
AuthRepositoryImpl.refreshToken()→api.refresh()→ NetworkManager (인터셉터 포함)AuthRequestInterceptor.retry()→TokenRefreshService.refreshToken()→ URLSession.shared (인터셉터 없음)직접 호출과 재시도 경로가 다릅니다. 일관성을 위해 AuthRepositoryImpl.refreshToken()도 TokenRefreshService를 직접 사용하는 것을 권장합니다.
clearTokens() 함수 시그니처 오류 (AuthRepositoryImpl 106-108줄):
async throws로 선언했으나 실제로는 동기이고 예외를 발생하지 않음preferences.clear()는 동기 메서드이며 오류 처리 없음- 시그니처를
func clearTokens()로 수정하거나, 실제로 비동기 처리가 필요한 경우 구현 변경 필요권장사항: AuthRepositoryImpl.refreshToken()을 TokenRefreshService 위임으로 단순화하고, clearTokens() 시그니처 정정
SampoomManagement/Features/Auth/Data/Local/Preferences/AuthPreferences.swift (1)
108-116: 로그아웃 클리어 범위 적절합니다.추가된 키까지 모두 삭제해 잔존 상태가 남지 않습니다. LGTM.
SampoomManagement/Core/DI/AppDependencies.swift (1)
68-80: 초기화 순서 적절. Authorization 부착 조건만 재확인 부탁AuthPreferences → TokenRefreshService → AuthRequestInterceptor → NetworkManager 순으로 초기화된 점은 합리적입니다. 로그인/회원가입 요청 시 토큰이 없을 수 있으므로, Interceptor가 액세스 토큰 존재 시에만 Authorization 헤더를 부착하는지, 그리고 NetworkManager가 헤더를 캐싱하지 않고 매 요청마다 Interceptor를 통해 최신 토큰을 반영하는지 확인 부탁드립니다.
There was a problem hiding this comment.
Actionable comments posted: 0
♻️ Duplicate comments (4)
SampoomManagement/Core/DI/AppDependencies.swift (1)
28-29: 가시성 축소 적용 LGTMrefresh 서비스와 인터셉터를 private로 제한한 선택 좋습니다. 이전 피드백 반영 확인됩니다.
SampoomManagement/Core/Network/AuthRequestInterceptor.swift (3)
11-26: 동시 갱신 병합(coalescing) 구현 적절단일 in-flight Task로 병합하고 완료 후 해제하는 패턴이 명확합니다. 이전 제안이 잘 반영되었습니다.
38-53: Authorization 항상 최신 토큰으로 덮어쓰기 — OK매 요청마다 AccessToken을 조회해 헤더를 갱신/제거하는 흐름이 적절합니다. 👍
추가로, 방어적 코딩 차원에서 인증 엔드포인트(예: /api/auth/)에는 헤더를 생략하는 조건을 둘 수도 있습니다. (미래에 실수로 refresh를 AF 경로로 태울 경우 대비)
func adapt(_ urlRequest: URLRequest, for session: Session, completion: @escaping (Result<URLRequest, Error>) -> Void) { var adaptedRequest = urlRequest - do { + // 인증 관련 경로는 헤더 부착 생략 (방어적) + if let url = adaptedRequest.url, url.path.contains("/api/auth/") { + adaptedRequest.setValue(nil, forHTTPHeaderField: "Authorization") + completion(.success(adaptedRequest)) + return + } + do { if let accessToken = try authPreferences.getAccessToken() { adaptedRequest.setValue("Bearer \(accessToken)", forHTTPHeaderField: "Authorization") } else { adaptedRequest.setValue(nil, forHTTPHeaderField: "Authorization") } } catch { print("AuthRequestInterceptor - 토큰 조회 실패: \(error)") } completion(.success(adaptedRequest)) }
55-80: 컴파일 오류:await authPreferences.clear()제거 필요
AuthPreferences.clear()는 동기 메서드입니다.await사용 시 컴파일 실패합니다.다음 수정 제안:
- // 토큰 재발급 실패 시 로그아웃 처리 - await authPreferences.clear() + // 토큰 재발급 실패 시 로그아웃 처리 + authPreferences.clear() completion(.doNotRetry)보완 제안:
- 인증/로그인/리프레시 경로에서는 리프레시를 시도하지 않도록 우회 조건을 추가해 불필요한 재시도를 줄이세요.
빠른 점검 스크립트:
#!/bin/bash # await 사용 여부 점검 rg -nP --type=swift '\bawait\s+authPreferences\.clear\s*\(' -C1 # 인증 경로 우회 필요 여부 (retry에서 URL 확인) rg -nP --type=swift 'func\s+retry\(' -n -C3
🧹 Nitpick comments (2)
SampoomManagement/Core/DI/AppDependencies.swift (1)
89-99: AuthViewModel 수명/스레드 문맥 확인 권장앱 전역 단일 인스턴스로 보입니다. SwiftUI에서 메인 액터 상호작용이 많다면 생성/접근이 MainActor에서 이뤄지는지 확인해 주세요. 필요 시 @mainactor 주석 또는 생성 시점 보장을 고려하세요.
SampoomManagement/Core/Network/AuthRequestInterceptor.swift (1)
28-36: @unchecked Sendable 사용 근거 확인현재 상태로도 안전해 보이나, @unchecked Sendable는 컴파일러 보증을 우회합니다. 내부가 불변(immutable)이고 동시성 보호(Actor 등)가 충분한지 주석으로 근거를 남기거나, 불필요하면 제거를 고려하세요.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (2)
SampoomManagement/Core/DI/AppDependencies.swift(2 hunks)SampoomManagement/Core/Network/AuthRequestInterceptor.swift(1 hunks)
🧰 Additional context used
🧬 Code graph analysis (1)
SampoomManagement/Core/Network/AuthRequestInterceptor.swift (2)
SampoomManagement/Core/Network/TokenRefreshService.swift (1)
refreshToken(17-62)SampoomManagement/Features/Auth/Data/Local/Preferences/AuthPreferences.swift (2)
getAccessToken(81-83)clear(108-120)
🔇 Additional comments (2)
SampoomManagement/Core/DI/AppDependencies.swift (2)
68-80: 네트워크 인증 배선 순서 적절AuthPreferences → TokenRefreshService → AuthRequestInterceptor → NetworkManager 순으로 초기화하여 인터셉터-리프레시 루프를 방지합니다. 👍
22-26: authViewModel은 외부에서 직접 사용되므로 노출 유지 필수
authViewModel은RootView.swift와DashboardScreen.swift에서 상태 바인딩 및 메서드 호출(updateLoginState, signOut)에 직접 활용되고 있습니다. 따라서 private으로 축소할 수 없습니다.반면
checkLoginStateUseCase,signOutUseCase,clearTokensUseCase는 외부에서 직접 참조되지 않으므로, 만약 DI 내부 배선 전용이라면 private으로 제한하는 것이 캡슐화 관점에서 더 안전할 수 있습니다. 그러나 현재 구조(AuthViewModel 내부에서만 사용)에서는 노출 여부의 실질적 영향이 제한적이므로, 의도된 설계인지 검토하기 바랍니다.
CHOOSLA
left a comment
There was a problem hiding this comment.
SSL Pinning을 구현할 때가 됐군요 추후에 소통해요 ^^
네 교수님^^ |
📝 Summary
🙏 Question & PR point
📬 Reference
Summary by CodeRabbit
New Features
Improvements