[FEAT] 로그인 구현#12
Conversation
WalkthroughSwinject 기반 싱글톤 DI를 제거하고 AppDependencies로 의존성 그래프를 전환했으며, RootView 기반 인증 흐름과 Auth 도메인(DTO/API/Repository/UseCase/UI)을 추가했습니다. 네트워크 기본 URL, 커스텀 폰트·색상·이미지 자산, 공통 UI 컴포넌트 변경 및 Toast Swift 패키지 참조가 통합되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant App as SampoomManagementApp
participant Dep as AppDependencies
participant Root as RootView
participant Pref as AuthPreferences
participant LVm as LoginViewModel
participant SVm as SignUpViewModel
participant LUC as LoginUseCase
participant SUC as SignUpUseCase
participant Repo as AuthRepositoryImpl
participant API as AuthAPI
participant Net as NetworkManager
App->>Dep: init AppDependencies()
App->>Root: RootView(dependencies: Dep)
Root->>Pref: hasToken()
alt 토큰 존재
Root-->User: Show ContentView
else 토큰 없음
Root-->User: Show LoginView
end
User->>LVm: submit(email,password)
LVm->>LUC: execute(email,password)
LUC->>Repo: signIn(...)
Repo->>API: login(...)
API->>Net: request("auth/login")
Net-->>API: response(LoginResponseDTO)
API-->>Repo: LoginResponseDTO
Repo->>Pref: saveToken(access, refresh)
Repo-->>LUC: User
LUC-->>LVm: User
LVm-->>Root: success -> Show ContentView
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60-90 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)
📜 Recent review detailsConfiguration used: CodeRabbit UI Review profile: CHILL Plan: Pro 📒 Files selected for processing (2)
🧰 Additional context used🧬 Code graph analysis (2)SampoomManagement/Features/Auth/Data/Repository/AuthRepositoryImpl.swift (3)
SampoomManagement/Features/Auth/Data/Local/Preferences/AuthPreferences.swift (2)
🔇 Additional comments (10)
Comment |
There was a problem hiding this comment.
Actionable comments posted: 3
🧹 Nitpick comments (18)
SampoomManagement/Features/Auth/UI/LoginUiState.swift (1)
47-65: 이중 옵셔널 패턴이 올바르게 구현되었습니다.
copy메서드의 이중 옵셔널(String??) 패턴이 SignUpUiState와 일관되게 사용되어 "값 변경 없음"과 "nil로 설정" 을 구별할 수 있습니다. 구현은 기술적으로 정확하지만, 향후 유지보수성을 위해 더 명시적인 방식(예: 열거형 사용)을 고려해볼 수 있습니다.SampoomManagement/App/RootView.swift (1)
24-71: 하드코딩된 색상 값을 Color asset으로 교체하세요.Line 47에서 하드코딩된 RGB 값
Color(red: 0.5, green: 0.2, blue: 0.8)이 사용되고 있습니다. 일관된 디자인 시스템 유지를 위해 Color asset(예:Color(.accent))을 사용하는 것이 좋습니다.다음과 같이 수정할 수 있습니다:
- .foregroundColor(Color(red: 0.5, green: 0.2, blue: 0.8)) + .foregroundColor(Color(.accent))SampoomManagement/Features/Auth/UI/LoginViewModel.swift (1)
57-66: 에러 상태 관리를 개선하는 것을 고려하세요.Line 65의 타임스탬프 추가 방식은
onChange를 강제로 트리거하기 위한 workaround입니다. 이는 동작하지만 코드 냄새가 있으며, 에러 메시지에 타임스탬프가 포함되어 다른 곳에서 파싱이 필요합니다(SignUpView.swift Line 168 참조).더 깔끔한 대안으로
@Published var errorId = UUID()와 같은 별도의 식별자를 사용하거나, Combine의removeDuplicates()를 활용하는 방법을 고려하세요.SampoomManagement/Features/Auth/Domain/Models/User.swift (1)
10-17: 도메인 모델과 인증 토큰의 분리를 고려하세요.현재
User모델이 사용자 정보(id,name,role)와 인증 토큰(accessToken,refreshToken,expiresIn)을 함께 포함하고 있습니다.현재 구조로도 동작하지만, 토큰은 별도의 생명주기와 관리 방식(갱신, 만료 처리)이 필요할 수 있습니다. 향후 확장성을 위해 토큰을 별도의 타입으로 분리하는 것을 고려하세요.
SampoomManagement/Features/Auth/Domain/ValidationResult.swift (1)
10-27: LGTM! 선택적 개선 사항을 고려하세요.ValidationResult 구현이 명확하고 사용하기 쉽습니다.
선택적 개선 사항:
- Swift의 표준
Result<Void, String>타입 사용을 고려할 수 있습니다.isSuccess구현을 더 간결하게 할 수 있습니다:var isSuccess: Bool { switch self { case .success: return true case .error: return false } }현재 구현도 충분히 명확하므로 선택사항입니다.
SampoomManagement/Features/Auth/UI/SignUpView.swift (3)
11-24: 사용하지 않는 KeyboardObserver를 제거하세요.Line 13의
keyboardObserver가 선언되었지만 이 뷰에서 사용되지 않습니다. 불필요한 메모리 할당과 관찰을 방지하기 위해 제거하세요.- @StateObject private var keyboardObserver = KeyboardObserver()
25-152: LGTM! 선택적으로 반복 코드 추출을 고려하세요.폼 구조가 일관되고 명확합니다. 각 필드가 적절한 validation과 error handling을 가지고 있습니다.
선택적 개선: 레이블 + 텍스트 필드 패턴이 반복되므로, 재사용 가능한 헬퍼 View를 추출하면 코드 중복을 줄일 수 있습니다. 하지만 현재 구조도 충분히 명확하고 유지보수 가능합니다.
177-179: 코드 중복을 제거하세요.이
hideKeyboard메서드는LoginView.swiftLine 114-116에도 동일하게 구현되어 있습니다.공통 extension이나 helper로 추출하여 중복을 제거하세요.
예를 들어 View extension으로 추출할 수 있습니다:
// View+Extension.swift extension View { func hideKeyboard() { UIApplication.shared.sendAction(#selector(UIResponder.resignFirstResponder), to: nil, from: nil, for: nil) } }그 후 뷰에서
.onTapGesture { hideKeyboard() }형태로 사용할 수 있습니다.SampoomManagement/Core/UI/Extensions/Font+Extension.swift (1)
93-97: UIFont 초기화 실패 처리를 고려하세요.UIFont 초기화가 옵셔널을 반환하므로, 폰트가 등록되지 않은 경우 nil이 반환됩니다. 호출하는 쪽에서 적절한 fallback 처리가 필요합니다.
필요시 기본 폰트로 fallback하는 로직을 추가할 수 있습니다:
extension UIFont { static func gmarketSans(size: CGFloat, weight: Font.GmarketSansWeight) -> UIFont? { - return UIFont(name: weight.name, size: size) + return UIFont(name: weight.name, size: size) ?? .systemFont(ofSize: size) } }SampoomManagement/Features/Auth/UI/LoginView.swift (2)
13-13: 사용되지 않는 KeyboardObserver를 제거하세요.
KeyboardObserver가 선언되었지만 사용되지 않고 있습니다(93번 라인의 offset이 주석 처리됨).키보드 높이 조정이 필요하지 않다면 해당 프로퍼티를 제거하세요:
- @StateObject private var keyboardObserver = KeyboardObserver()
93-93: 주석 처리된 코드를 정리하세요.키보드 높이에 따른 offset 조정 코드가 주석 처리되어 있습니다. 향후 사용 계획이 없다면 제거하는 것이 좋습니다.
SampoomManagement/Features/Auth/Data/Remote/API/AuthAPI.swift (2)
19-26: 불필요한 중간 DTO 객체 생성 제거 고려
LoginRequestDTO를 생성한 후 즉시 파라미터 딕셔너리로 변환하고 있어 불필요한 객체 할당이 발생합니다. 파라미터를 직접 생성하면 더 간결하고 효율적입니다.다음 diff를 적용하여 리팩토링할 수 있습니다:
func login(email: String, password: String) async throws -> APIResponse<LoginResponseDTO> { return try await withCheckedThrowingContinuation { continuation in - let requestDTO = LoginRequestDTO(email: email, password: password) - let parameters: [String: Any] = [ - "email": requestDTO.email, - "password": requestDTO.password + "email": email, + "password": password ]
53-70: 불필요한 중간 DTO 객체 생성 제거 고려
SignupRequestDTO를 생성한 후 즉시 파라미터 딕셔너리로 변환하고 있어 불필요한 객체 할당이 발생합니다. 로그인 메서드와 동일한 패턴입니다.다음 diff를 적용하여 리팩토링할 수 있습니다:
) async throws -> APIResponse<SignupResponseDTO> { return try await withCheckedThrowingContinuation { continuation in - let requestDTO = SignupRequestDTO( - userName: userName, - workspace: workspace, - branch: branch, - position: position, - email: email, - password: password - ) - let parameters: [String: Any] = [ - "email": requestDTO.email, - "password": requestDTO.password, - "workspace": requestDTO.workspace, - "branch": requestDTO.branch, - "userName": requestDTO.userName, - "position": requestDTO.position + "email": email, + "password": password, + "workspace": workspace, + "branch": branch, + "userName": userName, + "position": position ]SampoomManagement/Features/Auth/Domain/AuthValidator.swift (1)
38-56: 비밀번호 최대 길이 검증 추가 권장현재 최소 길이(8자)와 영문/숫자 포함 여부만 검증하고 있습니다. 매우 긴 비밀번호는 해싱 시 서버 리소스를 과도하게 사용할 수 있어 DoS 공격 벡터가 될 수 있습니다. 일반적으로 최대 길이(예: 128자)를 설정하는 것이 좋습니다.
다음과 같이 최대 길이 검증을 추가할 수 있습니다:
static func validatePassword(_ password: String) -> ValidationResult { if password.isEmpty { return .error(StringResources.Auth.passwordRequired) } if password.count < 8 { return .error(StringResources.Auth.passwordTooShort) } + + if password.count > 128 { + return .error(StringResources.Auth.passwordTooLong) + } // 영문, 숫자 포함 여부 확인 let hasLetter = password.rangeOfCharacter(from: .letters) != nilNote:
StringResources.Auth.passwordTooLong문자열 리소스도 추가해야 합니다.SampoomManagement/Features/Auth/Data/Local/Preferences/KeychainManager.swift (1)
24-43: 강제 언래핑 대신 안전한 처리 권장Line 25에서
data(using: .utf8)!의 강제 언래핑을 사용하고 있습니다. Swift의String은 항상 유효한 UTF-8이므로 실제로는 안전하지만, 강제 언래핑은 코드를 읽는 사람에게 잠재적 크래시 위험을 암시합니다. 명시적으로 안전성을 표현하는 것이 좋습니다.다음과 같이 guard let을 사용할 수 있습니다:
func save(_ value: String, for key: String) throws { - let data = value.data(using: .utf8)! + guard let data = value.data(using: .utf8) else { + throw KeychainError.unknown(errSecParam) + } let query: [String: Any] = [SampoomManagement/Features/Auth/UI/SignUpViewModel.swift (1)
105-108: onChange 트리거를 위한 타임스탬프 사용동일한 에러 메시지가 연속으로 발생할 때 SwiftUI의
onChange가 트리거되지 않는 문제를 해결하기 위해 타임스탬프를 추가하는 방식을 사용하고 있습니다. 이는 알려진 우회 방법이지만, 에러 메시지에 타임스탬프가 포함되어 문자열이 오염됩니다.더 깔끔한 대안으로 별도의 에러 ID나 카운터를 사용하는 방법을 고려해보세요. 예를 들어,
errorId: UUID?를 추가하고 에러 발생 시마다 새 UUID를 생성하는 방식입니다.예시:
// UiState에 추가 struct SignUpUiState { // ... var errorId: UUID? var error: String? } // ViewModel에서 사용 private func showError(_ message: String) { uiState = uiState.copy(error: message, errorId: UUID()) } // View에서 .onChange(of: viewModel.uiState.errorId) { _, newId in if let newId = newId, let error = viewModel.uiState.error { // Show toast } }SampoomManagement/Features/Auth/Domain/UseCase/SignUpUseCase.swift (2)
10-15: 구조는 좋지만 몇 가지 Swift 모범 사례를 적용하면 더 좋습니다.클래스 구조와 의존성 주입 패턴이 잘 구현되어 있고 LoginUseCase와 일관성 있게 설계되었습니다. 다음 개선 사항을 고려해보세요:
final키워드 추가 - 상속을 방지하고 성능을 개선합니다- 명시적인 접근 제어자 추가 - 의도를 명확히 합니다
다음 diff를 적용하여 개선할 수 있습니다:
-class SignUpUseCase { +final class SignUpUseCase { private let repository: AuthRepository - init(repository: AuthRepository) { + internal init(repository: AuthRepository) { self.repository = repository }
17-33: 로직은 정확하나 문서화를 추가하면 좋습니다.메서드 구현이 올바르고 repository layer로의 위임이 적절합니다. AuthRepositoryImpl의 구현을 보면 회원가입 후 자동 로그인이 수행되는데, 이러한 중요한 동작을 문서화하는 것을 권장합니다.
다음과 같이 문서화 및 접근 제어자를 추가할 수 있습니다:
+ /// 회원가입을 수행하고 자동으로 로그인합니다. + /// - Parameters: + /// - userName: 사용자 이름 + /// - workspace: 작업 공간 + /// - branch: 지점 + /// - position: 직책 + /// - email: 이메일 주소 + /// - password: 비밀번호 + /// - Returns: 로그인된 사용자 정보 + /// - Throws: 회원가입 또는 로그인 중 발생한 오류 - func execute( + internal func execute( userName: String, workspace: String, branch: String, position: String, email: String, password: String ) async throws -> User {
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (9)
SampoomManagement/Resources/Assets.xcassets/oneline_logo.imageset/oneline_logo 1.svgis excluded by!**/*.svgSampoomManagement/Resources/Assets.xcassets/oneline_logo.imageset/oneline_logo.svgis excluded by!**/*.svgSampoomManagement/Resources/Assets.xcassets/oneline_logo.imageset/oneline_logo_dark.svgis excluded by!**/*.svgSampoomManagement/Resources/Assets.xcassets/square_logo.imageset/square_logo_dark.svgis excluded by!**/*.svgSampoomManagement/Resources/Assets.xcassets/square_logo.imageset/square_logo_light 1.svgis excluded by!**/*.svgSampoomManagement/Resources/Assets.xcassets/square_logo.imageset/square_logo_light.svgis excluded by!**/*.svgSampoomManagement/Resources/Fonts/GmarketSansBold.otfis excluded by!**/*.otfSampoomManagement/Resources/Fonts/GmarketSansLight.otfis excluded by!**/*.otfSampoomManagement/Resources/Fonts/GmarketSansMedium.otfis excluded by!**/*.otf
📒 Files selected for processing (42)
SampoomManagement.xcodeproj/project.pbxproj(9 hunks)SampoomManagement/App/ContentView.swift(1 hunks)SampoomManagement/App/RootView.swift(1 hunks)SampoomManagement/App/SampoomManagementApp.swift(1 hunks)SampoomManagement/Core/DI/AppDependencies.swift(1 hunks)SampoomManagement/Core/DI/CoreDIModule.swift(0 hunks)SampoomManagement/Core/DI/DIContainer.swift(0 hunks)SampoomManagement/Core/Network/NetworkManager.swift(1 hunks)SampoomManagement/Core/Resources/StringResources.swift(1 hunks)SampoomManagement/Core/UI/Components/CommonButton.swift(3 hunks)SampoomManagement/Core/UI/Components/CommonTextField.swift(4 hunks)SampoomManagement/Core/UI/Extensions/Font+Extension.swift(1 hunks)SampoomManagement/Core/Utilities/KeyboardObserver.swift(1 hunks)SampoomManagement/Features/Auth/Data/Local/Preferences/AuthPreferences.swift(1 hunks)SampoomManagement/Features/Auth/Data/Local/Preferences/KeychainManager.swift(1 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/LoginRequestDTO.swift(1 hunks)SampoomManagement/Features/Auth/Data/Remote/DTO/LoginResponseDTO.swift(1 hunks)SampoomManagement/Features/Auth/Data/Remote/DTO/SignupRequestDTO.swift(1 hunks)SampoomManagement/Features/Auth/Data/Remote/DTO/SignupResponseDTO.swift(1 hunks)SampoomManagement/Features/Auth/Data/Repository/AuthRepositoryImpl.swift(1 hunks)SampoomManagement/Features/Auth/Domain/AuthValidator.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/LoginUseCase.swift(1 hunks)SampoomManagement/Features/Auth/Domain/UseCase/SignUpUseCase.swift(1 hunks)SampoomManagement/Features/Auth/Domain/ValidationResult.swift(1 hunks)SampoomManagement/Features/Auth/UI/LoginUiEvent.swift(1 hunks)SampoomManagement/Features/Auth/UI/LoginUiState.swift(1 hunks)SampoomManagement/Features/Auth/UI/LoginView.swift(1 hunks)SampoomManagement/Features/Auth/UI/LoginViewModel.swift(1 hunks)SampoomManagement/Features/Auth/UI/SignUpUiEvent.swift(1 hunks)SampoomManagement/Features/Auth/UI/SignUpUiState.swift(1 hunks)SampoomManagement/Features/Auth/UI/SignUpView.swift(1 hunks)SampoomManagement/Features/Auth/UI/SignUpViewModel.swift(1 hunks)SampoomManagement/Features/Part/DI/PartDIModule.swift(0 hunks)SampoomManagement/Info.plist(1 hunks)SampoomManagement/Resources/Assets.xcassets/Disable.colorset/Contents.json(1 hunks)SampoomManagement/Resources/Assets.xcassets/TextSecondary.colorset/Contents.json(1 hunks)SampoomManagement/Resources/Assets.xcassets/oneline_logo.imageset/Contents.json(1 hunks)SampoomManagement/Resources/Assets.xcassets/square_logo.imageset/Contents.json(1 hunks)
💤 Files with no reviewable changes (3)
- SampoomManagement/Features/Part/DI/PartDIModule.swift
- SampoomManagement/Core/DI/DIContainer.swift
- SampoomManagement/Core/DI/CoreDIModule.swift
🧰 Additional context used
🧬 Code graph analysis (18)
SampoomManagement/Features/Auth/Domain/UseCase/SignUpUseCase.swift (2)
SampoomManagement/Features/Auth/Domain/UseCase/LoginUseCase.swift (1)
execute(17-19)SampoomManagement/Features/Auth/Data/Repository/AuthRepositoryImpl.swift (1)
signUp(20-39)
SampoomManagement/Features/Auth/UI/LoginUiState.swift (1)
SampoomManagement/Features/Auth/UI/SignUpUiState.swift (1)
copy(82-118)
SampoomManagement/App/ContentView.swift (1)
SampoomManagement/Core/DI/AppDependencies.swift (1)
makePartViewModel(58-60)
SampoomManagement/Features/Auth/UI/SignUpUiState.swift (1)
SampoomManagement/Features/Auth/UI/LoginUiState.swift (1)
copy(47-65)
SampoomManagement/Features/Auth/UI/SignUpViewModel.swift (3)
SampoomManagement/Features/Auth/UI/SignUpUiState.swift (1)
copy(82-118)SampoomManagement/Features/Auth/Domain/AuthValidator.swift (4)
validateEmail(20-35)validatePassword(38-56)validatePasswordCheck(59-69)validateNotEmpty(12-17)SampoomManagement/Features/Auth/Domain/UseCase/SignUpUseCase.swift (1)
execute(17-33)
SampoomManagement/Features/Auth/UI/LoginUiEvent.swift (2)
SampoomManagement/Features/Auth/UI/LoginViewModel.swift (1)
submit(35-55)SampoomManagement/Features/Auth/UI/SignUpViewModel.swift (1)
submit(62-97)
SampoomManagement/App/SampoomManagementApp.swift (1)
SampoomManagement/Core/UI/Extensions/Font+Extension.swift (2)
gmarketSans(28-30)gmarketSans(94-96)
SampoomManagement/Features/Auth/Domain/Repository/AuthRepository.swift (1)
SampoomManagement/Features/Auth/Data/Repository/AuthRepositoryImpl.swift (4)
signUp(20-39)signIn(41-51)signOut(53-55)isSignedIn(57-59)
SampoomManagement/App/RootView.swift (2)
SampoomManagement/Core/DI/AppDependencies.swift (2)
makeLoginViewModel(50-52)makeSignUpViewModel(54-56)SampoomManagement/Features/Auth/Data/Local/Preferences/AuthPreferences.swift (1)
hasToken(31-33)
SampoomManagement/Features/Auth/UI/SignUpView.swift (3)
SampoomManagement/Features/Auth/UI/SignUpViewModel.swift (8)
updateName(23-26)updateBranch(29-32)updatePosition(35-38)updateEmail(41-44)updatePassword(47-53)updatePasswordCheck(56-59)submit(62-97)consumeError(100-102)SampoomManagement/Features/Auth/UI/LoginViewModel.swift (4)
updateEmail(23-26)updatePassword(29-32)submit(35-55)consumeError(58-60)SampoomManagement/Features/Auth/UI/LoginView.swift (1)
hideKeyboard(115-117)
SampoomManagement/Features/Auth/UI/LoginView.swift (1)
SampoomManagement/Features/Auth/UI/LoginViewModel.swift (4)
updateEmail(23-26)updatePassword(29-32)submit(35-55)consumeError(58-60)
SampoomManagement/Features/Auth/Data/Local/Preferences/AuthPreferences.swift (1)
SampoomManagement/Features/Auth/Data/Local/Preferences/KeychainManager.swift (3)
save(24-43)get(45-71)delete(73-85)
SampoomManagement/Features/Auth/UI/SignUpUiEvent.swift (1)
SampoomManagement/Features/Auth/UI/SignUpViewModel.swift (1)
submit(62-97)
SampoomManagement/Features/Auth/Data/Repository/AuthRepositoryImpl.swift (3)
SampoomManagement/Features/Auth/Data/Remote/API/AuthAPI.swift (2)
signup(45-86)login(19-42)SampoomManagement/Features/Auth/Data/Local/Preferences/AuthPreferences.swift (3)
saveToken(18-21)clear(35-38)hasToken(31-33)SampoomManagement/Features/Auth/Data/Mappers/AuthMappers.swift (1)
toModel(11-20)
SampoomManagement/Features/Auth/Domain/UseCase/LoginUseCase.swift (2)
SampoomManagement/Features/Auth/Domain/UseCase/SignUpUseCase.swift (1)
execute(17-33)SampoomManagement/Features/Auth/Data/Repository/AuthRepositoryImpl.swift (1)
signIn(41-51)
SampoomManagement/Features/Auth/UI/LoginViewModel.swift (3)
SampoomManagement/Features/Auth/UI/LoginUiState.swift (1)
copy(47-65)SampoomManagement/Features/Auth/Domain/AuthValidator.swift (1)
validateNotEmpty(12-17)SampoomManagement/Features/Auth/Domain/UseCase/LoginUseCase.swift (1)
execute(17-19)
SampoomManagement/Features/Auth/Domain/AuthValidator.swift (3)
SampoomManagement/Core/Resources/StringResources.swift (1)
fieldRequired(101-103)SampoomManagement/Features/Auth/UI/LoginViewModel.swift (2)
validateEmail(70-76)validatePassword(78-84)SampoomManagement/Features/Auth/UI/SignUpViewModel.swift (3)
validateEmail(136-139)validatePassword(141-144)validatePasswordCheck(146-152)
SampoomManagement/Features/Auth/Data/Remote/API/AuthAPI.swift (1)
SampoomManagement/Core/Network/NetworkManager.swift (1)
request(18-50)
🪛 SwiftLint (0.57.0)
SampoomManagement/Core/UI/Components/CommonTextField.swift
[Warning] 129-129: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
🔇 Additional comments (50)
SampoomManagement/Resources/Assets.xcassets/oneline_logo.imageset/Contents.json (1)
1-32: 💡 에셋 구성 확인 완료기본/라이트/다크 변형이 모두 정의되어 있어 다크 모드 대응에 문제 없겠습니다.
SampoomManagement/Resources/Assets.xcassets/square_logo.imageset/Contents.json (1)
1-32: 💡 에셋 구성 확인 완료사각 로고 역시 라이트/다크 변형이 적절히 분리되어 있어 UI 일관성이 유지될 것입니다.
SampoomManagement/Info.plist (1)
5-10: 폰트 리소스 번들 포함 여부 확인 필요Info.plist에 UIAppFonts 항목을 추가하셨는데, 해당 OTF 파일들이 타깃 번들에 실제로 포함되어 있는지 반드시 확인해 주세요. 빠져 있으면 폰트 적용이 되지 않고 기본 폰트로 표시됩니다.
SampoomManagement/Features/Auth/UI/LoginUiState.swift (1)
10-45: LGTM!UI 상태 구조체가 잘 정의되어 있습니다.
isValid계산 프로퍼티는 이메일과 비밀번호가 비어있지 않고 에러가 없는지 적절하게 검증합니다.SampoomManagement.xcodeproj/project.pbxproj (1)
12-12: LGTM!Toast Swift 패키지 통합이 표준 절차에 따라 올바르게 수행되었습니다. 프레임워크 참조, 패키지 의존성, Info.plist 설정이 모두 적절하게 구성되어 있습니다.
Also applies to: 45-45, 92-92, 125-125, 289-289, 324-324, 389-396, 410-414
SampoomManagement/Features/Auth/Data/Remote/DTO/LoginResponseDTO.swift (1)
10-17: LGTM!로그인 응답 DTO가 명확하게 정의되어 있으며, API 응답 매핑에 필요한 모든 필드를 포함하고 있습니다.
SampoomManagement/Features/Auth/UI/LoginUiEvent.swift (1)
10-14: LGTM!로그인 UI 이벤트가 명확하게 정의되어 있으며, 이메일/비밀번호 변경 및 제출 이벤트를 적절하게 표현합니다.
SampoomManagement/Features/Auth/Data/Mappers/AuthMappers.swift (1)
10-21: LGTM!LoginResponseDTO에서 User 모델로의 매핑이 명확하고 직관적입니다. 모든 필드가 적절하게 변환되고 있습니다.
SampoomManagement/Features/Auth/Data/Remote/DTO/SignupResponseDTO.swift (1)
10-14: LGTM!회원가입 응답 DTO가 명확하게 정의되어 있으며, API 응답 매핑에 필요한 필드를 포함하고 있습니다.
SampoomManagement/Features/Auth/Data/Local/Preferences/AuthPreferences.swift (2)
31-33: 토큰 존재 여부 검증 로직을 확인하세요.두 토큰이 모두 존재할 때만 true를 반환하는데, 이는 올바른 접근입니다. 그러나
getAccessToken()과getRefreshToken()이 에러를 무시하므로, 키체인 읽기 실패 시 false를 반환하여 사용자가 이미 로그인되어 있어도 로그아웃된 것처럼 보일 수 있습니다.
35-38: 토큰 삭제 실패를 확인하세요.
try?로 삭제 에러를 무시하므로, 키체인 삭제가 실패해도 앱은 사용자가 로그아웃되었다고 가정할 수 있습니다. 최소한 실패를 로깅하거나, 삭제 실패 시 재시도 로직을 추가하는 것을 권장합니다.SampoomManagement/Features/Auth/Data/Repository/AuthRepositoryImpl.swift (2)
20-39: 회원가입 후 자동 로그인 실패 시나리오를 고려하세요.회원가입이 성공한 후 자동으로 로그인을 시도하는데(Line 38), 만약 로그인이 실패하면 사용자는 회원가입은 되었지만 로그인되지 않은 상태가 됩니다. 이 경우 사용자에게 명확한 에러 메시지를 제공하거나, 회원가입 성공 후 수동 로그인을 유도하는 것이 더 나을 수 있습니다.
또한 회원가입 응답(
SignupResponseDTO)을 사용하지 않고 버리고 있는데(Line 28의_), 필요한 정보가 있다면 활용하는 것을 고려해보세요.
41-51: 토큰 저장 실패를 확인하세요.
preferences.saveToken()이 에러를 조용히 무시하므로(AuthPreferences에서try?사용), 토큰 저장에 실패해도 User 객체를 반환합니다. 이는 메모리상에서는 로그인되었지만 재시작 후에는 로그인이 풀리는 불일치 상태를 만들 수 있습니다.AuthPreferences가 에러를 throw하도록 수정하고, 여기서 적절히 처리하는 것을 권장합니다.
SampoomManagement/App/SampoomManagementApp.swift (4)
12-13: LGTM!SwiftUI Environment 기반 DI 컨테이너를 앱 레벨에서 초기화하는 접근 방식이 적절합니다.
15-18: LGTM!앱 초기화 시점에 전역 폰트를 설정하는 것이 적절합니다.
20-24: LGTM!RootView에 의존성을 주입하는 패턴이 적절하며, 인증 플로우 조율을 위한 구조가 잘 설계되었습니다.
28-35: 전역 폰트 설정의 영향 범위를 검토하세요.UIKit의
appearance()프록시를 사용한 전역 폰트 설정은 모든 UILabel, UITextField, UITextView에 영향을 미칩니다. 특정 컴포넌트나 서드파티 라이브러리에서 다른 폰트가 필요한 경우 의도하지 않은 동작이 발생할 수 있습니다.필요시 개별 컴포넌트에서 명시적으로 폰트를 재설정할 수 있는지 확인하세요.
SampoomManagement/App/RootView.swift (2)
10-22: StateObject 초기화 방식을 확인하세요.
init내에서_loginViewModel과_signUpViewModel을StateObject(wrappedValue:)로 초기화하는 방식은 일반적으로 안전하지만, SwiftUI의 뷰 재생성 시 상태 손실 가능성이 있습니다.현재 구현은 올바르게 보이지만, 뷰 모델의 상태가 예상치 않게 초기화되는 경우 이 부분을 점검하세요.
73-75: LGTM!인증 상태 확인 로직이 명확하고 적절하게 추상화되어 있습니다.
SampoomManagement/Features/Auth/UI/LoginViewModel.swift (3)
12-20: LGTM!
@MainActor를 사용하여 UI 업데이트를 메인 스레드에서 처리하는 것이 적절하며, 의존성 주입 패턴도 올바르게 구현되어 있습니다.
22-32: LGTM!입력 값 변경 시 즉시 유효성 검사를 수행하는 패턴이 사용자 경험 측면에서 적절합니다.
34-55: LGTM!로그인 제출 로직이 잘 구현되어 있습니다:
- 유효성 검사 후 제출
- 비동기 호출 전 값 캡처
- 적절한 로딩 상태 관리
- 명확한 에러 처리
SampoomManagement/Core/UI/Components/CommonButton.swift (2)
85-85: LGTM!폰트를 Gmarket Sans로 변경하고 corner radius를 증가시킨 것이 디자인 시스템 통일성 측면에서 적절합니다.
padding(4)추가도 버튼 내부 여백 개선에 도움이 됩니다.Also applies to: 94-94, 98-101
110-110: LGTM!하드코딩된 색상 값을 시맨틱 컬러(
.disable,.accent,.textSecondary)로 교체한 것이 유지보수성과 디자인 일관성 측면에서 우수합니다.Also applies to: 119-119, 127-127
SampoomManagement/Features/Auth/Domain/UseCase/LoginUseCase.swift (1)
10-20: LGTM!LoginUseCase가 명확한 단일 책임을 가지고 있으며, repository를 적절히 추상화하고 있습니다. 테스트 가능한 구조입니다.
SampoomManagement/Features/Auth/UI/SignUpView.swift (1)
156-172: LGTM! 에러 메시지 처리가 LoginViewModel의 workaround에 의존합니다.키보드 해제 및 성공/에러 상태 처리가 적절합니다.
참고: Line 168의 타임스탬프 제거 로직은
LoginViewModel.showError의 workaround에 의존합니다(LoginViewModel.swift Line 65). 해당 workaround가 개선되면 이 부분도 함께 단순화할 수 있습니다.SampoomManagement/Features/Auth/UI/SignUpUiEvent.swift (1)
10-18: 구현이 적절합니다.회원가입 UI 이벤트를 명확하게 정의한 enum입니다. 각 케이스가 직관적이며 Clean Architecture 패턴을 잘 따르고 있습니다.
SampoomManagement/Core/UI/Extensions/Font+Extension.swift (2)
34-89: 시맨틱 폰트 토큰이 잘 정의되어 있습니다.일관된 타이포그래피를 위한 시맨틱 폰트 속성들이 명확하게 정의되어 있습니다. 크기와 용도가 주석으로 잘 설명되어 있습니다.
13-30: 검증 완료: Info.plist에 GmarketSansLight, GmarketSansMedium, GmarketSansBold 폰트가 등록되어 있습니다. 커스텀 폰트 구현이 적절합니다.SampoomManagement/Core/DI/AppDependencies.swift (2)
12-46: 의존성 주입 컨테이너 구현이 적절합니다.새로운 SwiftUI 기반 DI 컨테이너가 명확하게 구현되어 있습니다. 초기화 시점에 모든 의존성을 수동으로 연결하는 방식이 단순하고 이해하기 쉽습니다.
48-61: ViewModel 팩토리 메서드가 잘 정의되어 있습니다.각 화면별 ViewModel을 생성하는 팩토리 메서드가 깔끔하게 구현되어 있습니다.
SampoomManagement/Features/Auth/UI/LoginView.swift (2)
11-18: 로그인 뷰 구조가 적절합니다.ViewModel과 콜백을 통한 상태 관리 및 네비게이션 처리가 깔끔합니다.
104-109: 에러 메시지 처리 로직을 확인하세요.타임스탬프를 제거하기 위해
"_"기준으로 메시지를 분리하고 있습니다. 이 로직이 서버에서 전달되는 에러 메시지 형식과 일치하는지 확인해 주세요.에러 메시지 형식이 문서화되어 있거나 테스트로 검증되었는지 확인하세요. 예상하지 못한 형식의 에러 메시지가 오면 의도한 대로 동작하지 않을 수 있습니다.
SampoomManagement/App/ContentView.swift (1)
15-22: 의존성 주입 방식 전환이 적절합니다.새로운
AppDependencies를 사용한 의존성 주입으로 깔끔하게 전환되었습니다.StateObject를 통한 ViewModel 초기화도 올바르게 구현되어 있습니다.SampoomManagement/Core/Utilities/KeyboardObserver.swift (1)
11-37: 키보드 옵저버 구현이 적절합니다.Combine을 활용한 키보드 높이 추적이 깔끔하게 구현되어 있습니다. 메모리 누수 방지를 위한
weak self사용과 애니메이션 처리도 적절합니다.SampoomManagement/Features/Auth/UI/SignUpUiState.swift (3)
10-65: UI State 구조가 적절합니다.회원가입에 필요한 모든 필드와 에러 상태를 명확하게 정의하고 있습니다. 기본값으로
workspace를 "대리점"으로 설정한 것도 비즈니스 요구사항을 반영한 것으로 보입니다.
67-80: 유효성 검사 로직이 적절합니다.모든 필드가 비어있지 않고 에러가 없는지 확인하는 간결한 구현입니다.
82-118: 불변 업데이트를 위한 copy 메서드가 잘 구현되어 있습니다.옵셔널 래핑(
String??)을 사용하여 "업데이트 안 함"과 "nil로 설정"을 구분하는 패턴이 적절하게 적용되어 있습니다. LoginUiState의 copy 메서드와 일관된 구현입니다.SampoomManagement/Features/Auth/Domain/AuthValidator.swift (3)
12-17: LGTM!빈 값 검증 로직이 명확하고 올바릅니다. 공백 처리도 적절합니다.
20-35: 이메일 검증 로직 확인 완료정규식 패턴을 사용한 이메일 형식 검증이 적절하게 구현되어 있습니다. 일반적인 이메일 형식을 처리하기에 충분하지만, RFC 5322의 모든 경우를 커버하지는 않습니다(예: 특수 문자, 인용 부호 등). 현재 애플리케이션의 요구사항에는 충분해 보입니다.
59-69: LGTM!비밀번호 확인 검증 로직이 올바르게 구현되어 있습니다.
SampoomManagement/Core/Resources/StringResources.swift (1)
74-111: LGTM!인증 관련 문자열 리소스가 잘 구성되어 있습니다. 로그인과 회원가입에 필요한 레이블, 플레이스홀더, 버튼 텍스트, 검증 메시지가 모두 포함되어 있습니다.
fieldRequired함수를 통한 동적 메시지 생성도 적절합니다.SampoomManagement/Core/UI/Components/CommonTextField.swift (2)
54-79: LGTM!내부 상태에서
@Binding으로 전환한 것은 폼 필드 관리에 적합한 접근 방식입니다. 에러 상태(isError,errorMessage)를 추가하여 유효성 검증 피드백을 제공하는 것도 좋습니다.
171-179: LGTM!테두리 색상 우선순위 로직(에러 > 포커스 > 기본)이 명확하고 사용자 경험에 적합합니다.
SampoomManagement/Features/Auth/Data/Local/Preferences/KeychainManager.swift (3)
45-71: LGTM!키체인에서 값을 가져오는 로직이 올바르게 구현되어 있습니다. 항목이 없을 때 nil 반환, 다른 에러 발생 시 throw, 디코딩 실패 시 nil 반환하는 처리가 적절합니다.
73-85: LGTM!삭제 메서드가 멱등성(idempotent)을 보장하도록 구현되어 있어 좋습니다. 항목이 없는 경우도 성공으로 처리하는 것이 적절합니다.
87-99: LGTM!서비스 범위 내 모든 항목을 삭제하는 로직이 올바릅니다.
SampoomManagement/Features/Auth/UI/SignUpViewModel.swift (3)
23-59: LGTM!각 필드 업데이트 메서드가 즉시 검증을 수행하여 실시간 피드백을 제공하는 것이 좋습니다. Lines 50-52에서 비밀번호 변경 시 비밀번호 확인 필드도 재검증하는 로직이 적절합니다.
62-97: LGTM!회원가입 제출 로직이 올바르게 구현되어 있습니다. 모든 필드 검증 후 진행, 값 캡처, 로딩 상태 관리, 에러 처리가 적절합니다.
112-152: LGTM!모든 검증 메서드가 일관된 패턴을 따르고 있어 코드 가독성과 유지보수성이 좋습니다.
AuthValidator를 활용한 검증 로직이 적절합니다.
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
SampoomManagement/Core/UI/Components/CommonTextField.swift (1)
147-157: 사용되지 않는 computed property 제거
buttonBackgroundColor가 정의되어 있지만body에서 사용되지 않습니다.다음과 같이 사용되지 않는 코드를 제거하세요:
- private var buttonBackgroundColor: Color { - // 다크모드 고려한 기본 배경색 - switch colorScheme { - case .dark: - return Color(red: 0.1, green: 0.1, blue: 0.1) // 매우 어두운 회색 - case .light: - return .white - @unknown default: - return Color(.systemBackground) - } - } -
♻️ Duplicate comments (1)
SampoomManagement/Core/UI/Components/CommonTextField.swift (1)
129-131: 이전 리뷰의 제안이 적용되었습니다
onChange핸들러에서 사용되지 않는oldValue파라미터를_로 변경하여 SwiftLint 경고가 해결되었습니다.
🧹 Nitpick comments (1)
SampoomManagement/Core/Network/NetworkError.swift (1)
33-48: 네트워크 에러 케이스 중복을 개선하세요.
AuthError.networkError와NetworkError.networkError케이스가 중복됩니다. 이는 다음과 같은 문제를 야기할 수 있습니다:
- 어떤 에러 타입을 사용해야 할지 혼란 발생
- 에러 처리 로직의 일관성 저하
- 코드 중복으로 인한 유지보수성 감소
다음 방법들을 고려해보세요:
- 인증 관련 네트워크 에러는
NetworkError를 재사용하고AuthError는 순수 인증 로직 에러만 처리AuthError.networkError를AuthError.authNetworkError로 명확하게 네이밍예시: NetworkError 재사용 방식
enum AuthError: Error, LocalizedError { case tokenSaveFailed(Error) case invalidCredentials - case networkError(Error) var errorDescription: String? { switch self { case .tokenSaveFailed(let error): return "토큰 저장 실패: \(error.localizedDescription)" case .invalidCredentials: return "잘못된 인증 정보입니다" - case .networkError(let error): - return "네트워크 오류: \(error.localizedDescription)" } } }그리고 네트워크 관련 에러는
NetworkError를 직접 사용하도록 호출 코드를 조정하세요.
📜 Review details
Configuration used: CodeRabbit UI
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (5)
SampoomManagement/Core/Network/NetworkError.swift(1 hunks)SampoomManagement/Core/UI/Components/CommonTextField.swift(4 hunks)SampoomManagement/Features/Auth/Data/Local/Preferences/AuthPreferences.swift(1 hunks)SampoomManagement/Features/Auth/Data/Repository/AuthRepositoryImpl.swift(1 hunks)SampoomManagement/Features/Auth/UI/LoginViewModel.swift(1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
- SampoomManagement/Features/Auth/UI/LoginViewModel.swift
🧰 Additional context used
🧠 Learnings (1)
📚 Learning: 2025-10-11T12:58:56.474Z
Learnt from: Sangyoon98
PR: 33-Auto/Sampoom-Management-iOS#7
File: SampoomManagement/Core/UI/Components/CommonTextField.swift:117-119
Timestamp: 2025-10-11T12:58:56.474Z
Learning: In Swift/SwiftUI iOS 17.0+, the onChange(of:perform:) modifier expects a two-parameter closure (oldValue, newValue) or zero-parameter closure, not a single-parameter closure. The older single-parameter version was deprecated in iOS 17.0.
Applied to files:
SampoomManagement/Core/UI/Components/CommonTextField.swift
🧬 Code graph analysis (2)
SampoomManagement/Features/Auth/Data/Repository/AuthRepositoryImpl.swift (3)
SampoomManagement/Features/Auth/Data/Remote/API/AuthAPI.swift (2)
signup(45-86)login(19-42)SampoomManagement/Features/Auth/Data/Local/Preferences/AuthPreferences.swift (3)
saveToken(18-28)clear(42-50)hasToken(38-40)SampoomManagement/Features/Auth/Data/Mappers/AuthMappers.swift (1)
toModel(11-20)
SampoomManagement/Features/Auth/Data/Local/Preferences/AuthPreferences.swift (1)
SampoomManagement/Features/Auth/Data/Local/Preferences/KeychainManager.swift (3)
save(24-43)delete(73-85)get(45-71)
🔇 Additional comments (10)
SampoomManagement/Core/UI/Components/CommonTextField.swift (3)
53-79: 외부 바인딩 및 에러 상태 처리 잘 구현됨
@Binding을 통한 외부 상태 관리와@FocusState를 활용한 포커스 제어, 그리고 에러 상태 처리가 적절하게 추가되었습니다. SwiftUI 모범 사례를 잘 따르고 있습니다.
114-115: 중복 패딩 사용 확인 필요
size.padding을 적용한 후 추가로.padding(4)를 적용하고 있습니다. 이는 모든 방향에 4포인트의 추가 패딩을 더하는데, 의도된 디자인인지 확인이 필요합니다..padding(size.padding) // EdgeInsets with per-side values .padding(4) // Uniform 4pt padding on all sides만약 border와 내부 콘텐츠 사이에 추가 여백을 두려는 의도라면 명확한 주석을 추가하거나, 더 명시적인 방식으로 구현하는 것을 고려해보세요.
227-273: Preview 구현이 다양한 사용 사례를 잘 보여줍니다외부 바인딩을 사용한 프리뷰 코드가 이메일, 비밀번호, 에러 상태 등 다양한 케이스를 명확하게 시연하고 있습니다.
SampoomManagement/Features/Auth/Data/Local/Preferences/AuthPreferences.swift (3)
18-28: 이전 리뷰 지적 사항이 올바르게 수정되었습니다.토큰 저장 실패 시 롤백 로직이 추가되어 부분 저장으로 인한 불일치 상태를 방지합니다. 에러를 호출자에게 전파하여 적절한 처리가 가능합니다.
38-40: getter 메서드가 수정되면 이 메서드도 업데이트 필요합니다.
getAccessToken()과getRefreshToken()이 에러를 throw하도록 변경되면, 이 메서드도 그에 맞춰 에러 처리 로직을 추가해야 합니다. 키체인 접근 실패 시 false를 반환하는 것이 적절한지, 아니면 에러를 전파해야 하는지 결정해야 합니다.예시 개선안:
func hasToken() -> Bool { do { return try getAccessToken() != nil && try getRefreshToken() != nil } catch { // 키체인 접근 오류 시 로깅 print("AuthPreferences - 키체인 접근 실패: \(error)") return false // 또는 에러를 throw } }
42-50: 로그아웃 시나리오에 적합한 에러 처리입니다.삭제 실패 시에도 로그아웃 상태로 간주하는 것이 합리적이며, 에러 로깅을 통해 디버깅이 가능합니다.
SampoomManagement/Features/Auth/Data/Repository/AuthRepositoryImpl.swift (4)
14-17: 깔끔한 의존성 주입 구조입니다.
41-57: 토큰 저장 실패에 대한 에러 처리가 잘 구현되어 있습니다.키체인 저장 실패 시 로깅 후
AuthError.tokenSaveFailed로 감싸서 전파하는 것이 적절합니다. 호출자가 명확한 에러 타입을 받을 수 있습니다.
63-65:AuthPreferences.hasToken()의 에러 처리 개선이 필요합니다.이 메서드는
preferences.hasToken()에 의존하는데, 해당 메서드가 키체인 접근 오류와 토큰 부재를 구분하지 못합니다.AuthPreferences.swift의 getter 메서드들이 수정되면 여기도 함께 업데이트되어야 합니다.
28-38: 회원가입 API가 토큰을 반환하지 않으므로 회원가입 후 별도 로그인 호출이 필요합니다.Likely an incorrect or invalid review comment.
📝 Summary
로그인 기능 구현
🙏 Question & PR point
📬 Reference
Summary by CodeRabbit
New Features
Style
Chores