[Feat] 모듈 구조 변경 및 토큰 로직 #24
Conversation
- Endpoint에 isAuthorized 값 추가 (인증이 필요한지 여부) - 그렇다면 Endpoint 만들 때 header에 TokenManager에서 accessToken 빼와서 저장
- 카카오 로그인 시 nickname, profileImageUrl을 KakaoSDK에서 받아와 저장하도록 로직 추가 - 카카오, 애플 로그인 시 소셜 로그인 타입 저장하도록 로직 추가
- 온보딩 서버 Request Enum 값 변경 - 온보딩 UI 수정 (SubTitle 여부)
- TokenResponseDTO 구현 - reissue response 타입 교체
|
""" Walkthrough이 변경 사항은 네트워크 및 영속성 서비스의 모듈 분리를 제거하고, 토큰 및 사용자 데이터 관리를 싱글톤 기반의 내부 클래스로 통합하며, DI(의존성 주입) 관련 코드와 프로토콜, 외부 패키지(Then) 의존성을 대폭 정리합니다. 엔드포인트에서 토큰 전달 방식이 변경되고, 저장소 및 유즈케이스의 인터페이스가 간소화되었습니다. Changes
Changes Table (요약)
Sequence Diagram(s)sequenceDiagram
participant SceneDelegate
participant UserDataRepository
participant TokenManager
participant NetworkService
participant TabBarView
participant IntroView
SceneDelegate->>UserDataRepository: resolve(UserDataRepositoryProtocol)
Note right of SceneDelegate: scene(_:willConnectTo:options:)
SceneDelegate->>SceneDelegate: set rootViewController = SplashView
SceneDelegate->>UserDataRepository: reissueToken()
UserDataRepository->>TokenManager: loadToken(.refreshToken)
UserDataRepository->>NetworkService: request(reissueEndpoint)
NetworkService-->>UserDataRepository: TokenResponseDTO
UserDataRepository->>TokenManager: saveToken(newTokens)
UserDataRepository-->>SceneDelegate: true
alt 로그인 성공
SceneDelegate->>TabBarView: set as rootViewController
else 로그인 실패
SceneDelegate->>IntroView: set as rootViewController
end
sequenceDiagram
participant AuthRepository
participant KakaoSDK
participant UserDefaultsStorage
participant NetworkService
participant TokenManager
AuthRepository->>KakaoSDK: login()
KakaoSDK-->>AuthRepository: nickname, profileImageUrl
AuthRepository->>UserDefaultsStorage: save(nickname, profileImageUrl, socialLoginType)
AuthRepository->>NetworkService: request(loginEndpoint)
NetworkService-->>AuthRepository: TokenResponseDTO
AuthRepository->>TokenManager: saveToken(accessToken, refreshToken)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~40–60 minutes Possibly related PRs
Suggested reviewers
Poem
Note ⚡️ Unit Test Generation is now available in beta!Learn more here, or try it out under "Finishing Touches" below. 📜 Recent review detailsConfiguration used: .coderabbit.yaml 📒 Files selected for processing (1)
🚧 Files skipped from review as they are similar to previous changes (1)
✨ Finishing Touches
🧪 Generate unit tests
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. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
Documentation and Community
|
There was a problem hiding this comment.
Actionable comments posted: 11
🔭 Outside diff range comments (3)
Projects/Presentation/Sources/Onboarding/View/Component/ProgressBarView.swift (2)
8-8: SnapKit import 누락으로 인한 컴파일 오류 발생
이 파일에서snp.makeConstraints를 사용하지만import SnapKit문이 없습니다. Swift 파일마다 모듈을 명시적으로 임포트해야 하므로, 현재 상태로는 빌드 시 컴파일 오류가 발생합니다.import UIKit +import SnapKit
48-49: stepCount가 0일 때 0으로 나누기(runtime crash) 가능성
CGFloat(step) / CGFloat(stepCount)연산에서stepCount가 0이면 런타임에서EXC_BAD_INSTRUCTION가 발생합니다. 방어 코드로 0 여부를 체크하거나, 0인 경우 0으로 고정하는 로직을 추가하세요.- let progressRatio = min(max(CGFloat(step) / CGFloat(stepCount), 0.0), 1.0) + guard stepCount > 0 else { + backgroundView.snp.makeConstraints { make in + make.edges.equalToSuperview() + } + progressView.snp.makeConstraints { make in + make.leading.top.bottom.equalTo(backgroundView) + make.width.equalTo(0) + } + return + } + let progressRatio = min(max(CGFloat(step) / CGFloat(stepCount), 0.0), 1.0)Projects/DataSource/Sources/Repository/AuthRepository.swift (1)
34-50: Apple 로그인의 닉네임 처리 로직에 문제가 있을 수 있습니다.신규 사용자의 경우
loadNickname()이 실패하여 전체 로그인이 실패할 수 있으며, Kakao와 달리 프로필 이미지를 저장하지 않는 불일치가 있습니다.다음과 같은 개선을 제안합니다:
func appleLogin(nickname: String?, authToken: String) async throws -> UserEntity { - var savedNickname: String = "" + let savedNickname: String? if let nickname { try saveNickname(nickname: nickname) savedNickname = nickname } else { - savedNickname = try loadNickname() + savedNickname = try? loadNickname() } let user = try await requestServerLogin( socialType: .apple, - nickname: savedNickname, + nickname: savedNickname ?? nickname, token: authToken) try saveSocialLoginType(socialLoginType: .apple) + // Apple 프로필 이미지 URL 처리 추가 필요 return user }
🧹 Nitpick comments (35)
Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineView.swift (1)
59-62: 문자열 하드코딩 대신 Localizable.strings 사용 제안텍스트 리소스를 코드에 직접 작성하면 다국어 지원·문구 변경 시 유지보수가 어려워집니다.
NSLocalizedString으로 분리해 두면 추후 번역이나 A/B 테스트에도 유연합니다.- routineLabel.text = "루틴 목록" + routineLabel.text = NSLocalizedString("routine_list_title", + comment: "추천 루틴 화면 헤더 타이틀")Projects/Presentation/Sources/RecommendedRoutine/View/Component/RegisterEmotionButton.swift (3)
13-20:borderWith오타 – 상수명을borderWidth로 교정하는 것이 가독성에 더 좋습니다
오타가 반복적으로 사용되면 추후 검색·리팩터링 시 놓칠 수 있습니다.- static let borderWith: CGFloat = 1 + static let borderWidth: CGFloat = 1
40-44: 아이콘의contentMode값을.scaleAspectFit으로 변경을 고려해 보세요
현재 20 × 20 뷰에 8 × 8 이미지를 중앙에 배치하고 있습니다.
콘텐츠가 확장될 가능성이 있거나 다양한 크기의 아이콘을 재사용할 경우.scaleAspectFit이 더 안전한 선택입니다.- plusIcon.contentMode = .center + plusIcon.contentMode = .scaleAspectFit
46-49: 하드코딩된 버튼 텍스트를 로컬라이즈된 문자열로 분리하세요
UI 텍스트를 코드에 직접 작성하면 다국어 지원 및 문자열 관리가 어려워집니다.
Localizable.strings에 키를 정의하고, 여기서는 키를 참조하는 방식이 권장됩니다.- buttonLabel.text = "오늘의 감정 루틴 추천받기" + buttonLabel.text = NSLocalizedString("register_emotion_button_title", + comment: "오늘의 감정 루틴 추천받기 버튼 타이틀")Projects/Presentation/Sources/Onboarding/View/Component/ProgressBarView.swift (1)
34-39: 불필요한 frame 설정 제거로 미세 최적화 가능
Auto Layout 제약을 곧바로 적용하기 때문에backgroundView.frame = bounds코드는 실제 레이아웃 계산에 영향을 주지 않습니다. 불필요한 직접 frame 설정을 제거하면 가독성과 성능 모두 약간 개선됩니다.Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineCategoryButton.swift (2)
11-16: UIButton의 기본titleLabel활용을 고려해보세요
UIButton은 이미titleLabel(및 상태별 타이틀/컬러 변경 API)을 내장하고 있습니다. 별도의UILabel을 서브뷰로 추가하면
① 접근성·상태별 스타일(선택/비-선택) 처리가 이중화되고,
② 터치 영역 계산 시 불필요한 레이어가 늘어납니다.가능하다면 다음과 같이 UIButton 자체 프로퍼티를 활용해 복잡도를 줄여보세요.
-final class RoutineCategoryButton: UIButton { +final class RoutineCategoryButton: UIButton { - private let categoryLabel = UILabel()// configureAttribute 내부 setTitle(routineCategory.title, for: .normal) titleLabel?.font = BitnagilFont(style: .caption1, weight: .regular).font setTitleColor(BitnagilColor.navy100, for: .normal)
37-40:configureAttribute()의 일회성 설정은 프로퍼티 초기화 클로저로 옮기면 간결해집니다
categoryLabel의 폰트·컬러 설정은 객체 생성 시 한 번만 실행됩니다.
아래와 같이 프로퍼티 선언부에서 클로저를 이용해 초기화하면,
- 초기화 로직을 한 곳에 모을 수 있고
configureAttribute()의 책임이 줄어듭니다.-private let categoryLabel = UILabel() +private let categoryLabel: UILabel = { + let label = UILabel() + label.font = BitnagilFont(style: .caption1, weight: .regular).font + label.textColor = BitnagilColor.navy100 + return label +}()그 후
configureAttribute()에서는 텍스트만 설정하면 됩니다.- categoryLabel.font = ... - categoryLabel.textColor = ... + categoryLabel.text = routineCategory.titleProjects/Presentation/Sources/Onboarding/View/OnboardingView.swift (3)
64-68: 동적 글꼴 대응‧접근성 속성 추가 권장
mainLabel이 고정 높이(60pt)로 제약돼 있어 Dynamic Type 크기 변경 시 잘릴 가능성이 있습니다.
adjustsFontForContentSizeCategory및accessibilityLabel을 지정해 접근성과 가변 폰트 대응을 강화해 보시는 것을 권장드립니다.mainLabel.attributedText = BitnagilFont(style: .title2, weight: .bold).attributedString(text: onboarding.mainTitle) mainLabel.textColor = BitnagilColor.navy500 mainLabel.numberOfLines = 2 mainLabel.textAlignment = .left +mainLabel.adjustsFontForContentSizeCategory = true +mainLabel.accessibilityLabel = onboarding.mainTitle
70-76:subLabel변수 섀도잉으로 인한 가독성 저하
subLabel = UILabel()후 바로if let subLabel { … }로 다시 바인딩하면서 동일 이름이 중첩돼 읽기 흐름이 끊깁니다.
지역 상수를 별도로 두거나guard let label = subLabel패턴으로 명확히 분리하면 이해하기 쉬워집니다.- subLabel = UILabel() - if let subLabel { - subLabel.attributedText = BitnagilFont(style: .body2, weight: .medium).attributedString(text: subTitle) - subLabel.textColor = BitnagilColor.gray50 - subLabel.numberOfLines = 2 - subLabel.textAlignment = .left - } + let label = UILabel() + label.attributedText = BitnagilFont(style: .body2, weight: .medium).attributedString(text: subTitle) + label.textColor = BitnagilColor.gray50 + label.numberOfLines = 2 + label.textAlignment = .left + subLabel = label
79-81:UIStackView기본 속성 보강 제안
choiceStackView에alignment/distribution값을 명시하지 않아 기본값(.fill,.fill)에 의존하고 있습니다.
명시적으로 설정하면 의도를 표현할 수 있고, 추후 레이아웃 변경 시 오해를 줄일 수 있습니다.choiceStackView.axis = .vertical choiceStackView.spacing = Layout.choiceStackViewSpacing +choiceStackView.alignment = .fill +choiceStackView.distribution = .fillProjects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelButton.swift (3)
40-43: UIStackView 정렬을 명시적으로 설정하여 레이아웃 흔들림 방지
stackView.alignment이 기본값.fill그대로라서 레이블 높이·아이콘 크기 차이에 따라 버튼 내부 요소가 수직으로 어긋날 수 있습니다.
alignment = .center를 명시해 두면 모든 서브뷰가 동일한 중심선에 배치되어 일관된 UI를 보장합니다.stackView.isUserInteractionEnabled = false stackView.axis = .horizontal stackView.spacing = Layout.stackViewSpacing +stackView.alignment = .center
44-47: 하드코딩된 기본 문구를 DesignSystem 상수로 추출 고려문자열
"난이도 선택"이 여러 곳에서 재사용될 가능성이 있다면 DesignSystem(예:BitnagilStrings)에 상수로 정의해 두면 다국어 지원·일관성 유지에 유리합니다.
현재는 단순하지만 추후 로컬라이즈 시 이 부분을 다시 찾아야 하는 수고를 줄일 수 있습니다.
48-54: 아이콘 생성 비용을 줄이기 위한 캐싱 또는 static 프로퍼티 활용 제안
chevronIcon.image를 초기화할 때마다rotate → resize → renderingMode과정을 거치면서 새로운UIImage인스턴스가 생성됩니다. 동일한 리소스를 여러 버튼에서 공유한다면 아래와 같이 정적 프로퍼티(또는 이미지 캐시)로 한 번만 만들어 재사용하면 메모리·CPU 사용을 절감할 수 있습니다.private static let downChevronImage: UIImage? = { BitnagilIcon .chevronIcon(direction: .down)? .resizeAspectFit(to: CGSize(width: Layout.chevronImageSize, height: Layout.chevronImageSize))? .withRenderingMode(.alwaysTemplate) }() ... chevronIcon.image = Self.downChevronImageProjects/Presentation/Sources/Common/Component/CustomBottomSheet.swift (1)
57-68: 색상·알파 값을 상수로 분리해 재사용성 강화 제안
0.7알파·systemBackground·systemGray4등 하드코딩된 값을Layout(또는DesignSystem) 내부 상수로 옮기면 추후 디자인 변경 시 유지보수 비용이 줄어듭니다.+ private enum Layout { + ... + static let dimmedAlpha: CGFloat = 0.7 + static let dimmedColor: UIColor = .black + static let dragHandleColor: UIColor = .systemGray4 + } - dimmedView.backgroundColor = UIColor.black.withAlphaComponent(0.7) + dimmedView.backgroundColor = Layout.dimmedColor.withAlphaComponent(Layout.dimmedAlpha)Projects/Presentation/Sources/Login/View/IntroView.swift (1)
39-46: 노출 문자열을 Localizable.strings 로 이전 권장한글 하드코딩은 다국어 지원·A/B 테스트 때 장애물이 됩니다.
NSLocalizedString(또는Stringsenum)으로 추출해 주세요.- let text = "당신의 하루 리듬을 이해하고,\n작은 변화를 함께 시작해볼게요." + let text = NSLocalizedString( + "intro_message", + value: "당신의 하루 리듬을 이해하고,\n작은 변화를 함께 시작해볼게요.", + comment: "IntroView 메시지")Projects/Presentation/Sources/Login/View/Component/SocialLoginButton.swift (1)
13-18:iconSize타입을CGFloat로 변경하여 불필요한 타입 변환 제거
CFloat→CGFloat로 바꾸면 CGFloat <-> Float 변환 비용과 경고 가능성을 없앨 수 있습니다.- static let iconSize: CFloat = 24 + static let iconSize: CGFloat = 24Projects/Presentation/Sources/Login/View/Component/TotalAgreementButton.swift (2)
44-52: checkButton 이미지에.alwaysTemplate렌더링 모드 지정 권장
checkButton에 틴트 컬러를 주고 있으나, 에셋이 template 이미지로 설정되지 않았을 경우 색상이 적용되지 않습니다. 코드에서 확실히 보장하려면 렌더링 모드를 강제 지정해 두는 편이 안전합니다.- checkButton.image = BitnagilIcon.checkIcon + checkButton.image = BitnagilIcon.checkIcon.withRenderingMode(.alwaysTemplate)
53-56: 하드코드된 텍스트는 Localizable.strings로 분리 필요
"전체동의"문자열이 코드에 직접 박혀 있어 다국어 지원이 어렵습니다.
NSLocalizedString사용 후.strings파일로 분리해 주세요.Projects/Presentation/Sources/Login/View/Component/TermsAgreementItemView.swift (2)
26-29:agreementLable→agreementLabel오타 수정 권장라벨 변수명이 오타입니다. 일관된 네이밍은 가독성과 자동 완성 정확도에 영향을 줍니다.
- private let agreementLable = UILabel() + private let agreementLabel = UILabel()
58-63: 고정 문자열 “더보기”의 로컬라이즈 처리 필요UI 텍스트
"더보기"역시 코드에 직접 작성되어 있습니다.
다국어 대응 및 문자열 관리 차원에서NSLocalizedString("see_more", comment: "")등으로 추출해 주세요.Projects/Presentation/Sources/Onboarding/View/OnboardingResultView.swift (3)
74-83:mainLabel‧subLabel텍스트 설정 보강 제안
- 다국어 지원을 위해 하드-코딩된 한국어 문자열을
Localizable.strings로 이전하면 유지보수성이 향상됩니다.mainLabel.numberOfLines를 2로 고정하면 Dynamic Type 확대 시 텍스트가 잘릴 수 있습니다. 0으로 두어 줄 수를 자동으로 조정하도록 하는 편이 안전합니다.- mainLabel.numberOfLines = 2 + // 길이·폰트 변경에 대응할 수 있도록 0(무제한) 권장 + mainLabel.numberOfLines = 0
84-86:UIStackView정렬 옵션 지정으로 좌측 정렬 보장
axis = .vertical만 설정하면 기본alignment가.fill이 되어 라벨 폭이 StackView 폭 전체를 차지합니다. 라벨 길이가 짧을 때 왼쪽 정렬이 어색하게 보일 수 있으므로alignment = .leading추가를 권장합니다.resultStackView.axis = .vertical + resultStackView.alignment = .leading resultStackView.spacing = Layout.resultStackViewSpacing
174-185: 결과 라벨 갱신 로직 중복 최소화
updateTime/Feeling/OutdoorResultLabel세 메서드가 유사한 처리를 반복하고 있습니다. 공통 연산(베이스 텍스트 생성·highlight 적용)을 하나의 유틸 메서드로 추출하면 코드 양이 줄고 변동 시 유지보수가 쉬워집니다. 예시:private func setResult(label: UILabel, template: String, highlight: String) { let base = String(format: template, highlight) label.attributedText = NSAttributedString.highlighted(text: base, highlightText: highlight) }이후 각 메서드는 한 줄로 대체 가능:
setResult(label: timeResultLabel, template: "• %@을 만들고 싶고", highlight: timeResult)Projects/Presentation/Sources/RecommendedRoutine/View/Component/RecommendedRoutineCardView.swift (2)
49-51: 스택뷰 정렬을 명시적으로 지정해 예상치 못한 레이아웃 변화 방지
UIStackView의 기본alignment가.fill이므로, 내부 라벨 높이가 변동될 때 폭이 늘어나는 현상이 발생할 수 있습니다..leading(또는 요구사항에 맞는 값)을 명시해 두면 향후 폰트/콘텐츠 변경 시 레이아웃 흔들림을 줄일 수 있습니다.labelStackView.axis = .vertical labelStackView.spacing = Layout.stackViewSpacing +labelStackView.alignment = .leading
52-59: 다이내믹 타입 대응 및 접근성 개선 권장
라벨에adjustsFontForContentSizeCategory = true를 설정하면 사용자가 iOS 설정에서 글꼴 크기를 변경했을 때 UI가 자동으로 확장되어 접근성을 향상시킵니다. 두 라벨 모두에 적용하는 것을 권장합니다.mainLabel.text = recommendedRoutine.mainTitle mainLabel.font = BitnagilFont(style: .body1, weight: .semiBold).font +mainLabel.adjustsFontForContentSizeCategory = true ... subLabel.text = recommendedRoutine.subTitle subLabel.font = BitnagilFont(style: .body2, weight: .regular).font +subLabel.adjustsFontForContentSizeCategory = trueProjects/DataSource/Sources/Common/Enum/UserDefaultsKey.swift (1)
10-11: 새로운 사용자 데이터 키 추가 승인
socialLoginType과profileImageUrl케이스 추가가 적절합니다. 소셜 로그인 정보 저장은 향후 활용도가 높을 것으로 보입니다.다만
profileImageUrl을 UserDefaults에 저장하는 것에 대해 몇 가지 고려사항이 있습니다:
- URL 길이가 길 경우 UserDefaults 성능에 영향을 줄 수 있음
- 프로필 이미지 URL은 변경 가능성이 높아 캐싱 전략 필요
향후 이미지 캐싱 라이브러리(예: Kingfisher) 도입 시 URL 저장 방식 재검토를 권장합니다.
Projects/DataSource/Sources/DTO/TokenResponseDTO.swift (1)
8-11: 토큰 응답 DTO 구현 승인
TokenResponseDTO구조체는 토큰 응답 파싱을 위한 기본 구현이 적절합니다.향후 토큰 관리 개선을 위해 다음 사항들을 고려해보세요:
- 토큰 만료 시간(
expiresIn) 필드 추가- 토큰 타입(
tokenType) 필드 추가- 서버 응답 키가 snake_case인 경우
CodingKeys활용struct TokenResponseDTO: Decodable { let accessToken: String let refreshToken: String let expiresIn: Int? let tokenType: String? enum CodingKeys: String, CodingKey { case accessToken = "access_token" case refreshToken = "refresh_token" case expiresIn = "expires_in" case tokenType = "token_type" } }Projects/App/Sources/DependencyInjection.swift (1)
16-16: 의존성 주입 간소화 승인 및 테스트 가능성 고려 필요모듈 통합으로 인한 의존성 주입 코드 간소화는 유지보수 측면에서 긍정적입니다.
하지만 NetworkService와 Persistence가 싱글톤으로 변경되면서 다음 사항들을 고려해야 합니다:
- 단위 테스트 시 모킹이 어려워질 수 있음
- 테스트 간 상태 격리 문제 발생 가능성
- 프로토콜 기반 의존성 주입의 장점 상실
향후 테스트 가능성을 위해 프로토콜 인터페이스는 유지하되, 기본 구현을 싱글톤으로 제공하는 방식을 고려해보세요. 또는 테스트용 팩터리 메서드를 통해 의존성을 주입할 수 있는 방법을 마련하는 것이 좋겠습니다.
Projects/DataSource/Sources/Common/TokenManager.swift (1)
8-31: 토큰 관리를 위한 싱글톤 구현이 잘 되어 있습니다.TokenManager가 토큰 작업을 중앙화하고 명시적인 에러 처리를 제공하는 구조가 적절합니다. removeToken에서 access/refresh 토큰을 모두 제거하는 것도 로그아웃 시나리오에 적합합니다.
다만 PR 목표에서 언급된 Endpoint에서의 강제 언래핑 이슈를 고려해 보세요. computed property에서 throwing 함수를 사용할 수 없는 제약사항에 대한 대안을 검토해보는 것이 좋겠습니다.
Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift (1)
219-223: 루트 뷰 컨트롤러 직접 변경 방식에 대한 검토가 필요합니다.navigation controller를 통한 전환 대신 키 윈도우의 rootViewController를 직접 변경하는 방식은 매우 급작스러운 전환을 만들 수 있습니다. 사용자 경험 관점에서 부드러운 전환 애니메이션이나 다른 방식을 고려해보는 것이 좋겠습니다.
아래와 같은 애니메이션 전환을 고려해보세요:
private func goToHomeView() { if let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene, let window = windowScene.windows.first(where: { $0.isKeyWindow }) { let newRootViewController = TabBarView() UIView.transition(with: window, duration: 0.3, options: .transitionCrossDissolve, animations: { window.rootViewController = newRootViewController }) } }Projects/DataSource/Sources/Common/DataSourceDependencyAssembler.swift (1)
17-17: 테스트 가능성에 대한 고려 필요리포지토리들이 이제 의존성 주입 없이 싱글톤 인스턴스를 직접 사용하도록 변경되었습니다. 구조가 단순해졌지만, 단위 테스트에서 의존성을 모킹하기 어려워질 수 있습니다.
향후 테스트 작성 시 의존성 모킹을 위한 대안을 고려해보시기 바랍니다:
// 테스트를 위한 초기화자 추가 고려 DIContainer.shared.register(type: AuthRepositoryProtocol.self) { _ in #if DEBUG return AuthRepository(networkService: NetworkService.shared, tokenManager: TokenManager.shared) #else return AuthRepository() #endif }Also applies to: 21-21, 25-25
Projects/DataSource/Sources/Persistence/UserDefaultsStorage.swift (1)
21-23: 제네릭 타입 캐스팅의 안전성 검토제네릭 메서드를 사용한 타입 캐스팅이 편리하지만, 잘못된 타입으로 호출 시 런타임에
nil을 반환할 수 있습니다. 이는 예상치 못한 동작을 야기할 수 있습니다.타입 안전성을 위해 다음과 같은 접근을 고려해보세요:
func load<T>(forKey key: String, as type: T.Type) -> T? { return userDefaults.object(forKey: key) as? T } // 또는 일반적인 타입별 메서드 제공 func loadString(forKey key: String) -> String? { return userDefaults.string(forKey: key) }Projects/App/Sources/SceneDelegate.swift (1)
26-37: 비동기 로그인 상태 확인 로직 개선 제안토큰 재발급을 통한 로그인 상태 확인 로직이 적절하게 구현되었습니다. 하지만 사용자 경험 개선을 위한 몇 가지 제안이 있습니다.
다음과 같은 개선을 고려해보세요:
Task { // 로딩 인디케이터 표시 let loadingViewController = LoadingViewController() window.rootViewController = loadingViewController window.makeKeyAndVisible() let isLogined = await userDataRepository.reissueToken() // 메인 스레드에서 UI 업데이트 보장 await MainActor.run { if isLogined { window.rootViewController = TabBarView() } else { let introView = IntroView() let navigationController = UINavigationController(rootViewController: introView) window.rootViewController = navigationController } } self.window = window }Projects/DataSource/Sources/Repository/UserDataRepository.swift (1)
18-18: TODO 주석 처리 필요서버에서 닉네임을 제공한다는 TODO 주석이 있습니다. 이를 이슈로 추적하거나 구현 일정을 명확히 해주세요.
이 TODO를 별도 이슈로 생성하여 추적하시겠습니까?
Projects/DataSource/Sources/Repository/AuthRepository.swift (1)
165-170: 프로필 이미지 URL 저장 시 유효성 검증이 필요합니다.URL을 문자열로 변환하여 저장하는 것은 적절하지만, 빈 문자열이나 잘못된 URL이 저장될 가능성이 있습니다.
private func saveUserProfileImageUrl(profileImageUrl: URL) throws { - guard userDefaultsStorage.save(profileImageUrl.absoluteString, forKey: UserDefaultsKey.profileImageUrl.rawValue) else { + let urlString = profileImageUrl.absoluteString + guard !urlString.isEmpty, + userDefaultsStorage.save(urlString, forKey: UserDefaultsKey.profileImageUrl.rawValue) else { throw UserError.profileImageUrlSaveFailed } }또한 URL을 다시 로드하는 메서드도 구현하면 좋겠습니다:
private func loadUserProfileImageUrl() throws -> URL { let urlString: String? = userDefaultsStorage.load(forKey: UserDefaultsKey.profileImageUrl.rawValue) guard let urlString, let url = URL(string: urlString) else { throw UserError.profileImageUrlLoadFailed } return url }
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
📒 Files selected for processing (57)
Projects/App/Project.swift(0 hunks)Projects/App/Sources/DependencyInjection.swift(1 hunks)Projects/App/Sources/SceneDelegate.swift(2 hunks)Projects/DataSource/Sources/Common/DataSourceDependencyAssembler.swift(1 hunks)Projects/DataSource/Sources/Common/Enum/Endpoint.swift(1 hunks)Projects/DataSource/Sources/Common/Enum/UserDefaultsKey.swift(1 hunks)Projects/DataSource/Sources/Common/Error/TokenError.swift(1 hunks)Projects/DataSource/Sources/Common/Error/UserError.swift(1 hunks)Projects/DataSource/Sources/Common/TokenManager.swift(1 hunks)Projects/DataSource/Sources/DTO/TokenResponseDTO.swift(1 hunks)Projects/DataSource/Sources/Endpoint/AuthEndpoint.swift(4 hunks)Projects/DataSource/Sources/Endpoint/OnboardingEndpoint.swift(3 hunks)Projects/DataSource/Sources/NetworkService/Extension/Endpoint+.swift(0 hunks)Projects/DataSource/Sources/NetworkService/NetworkError.swift(1 hunks)Projects/DataSource/Sources/NetworkService/NetworkService.swift(1 hunks)Projects/DataSource/Sources/Persistence/KeychainStorage.swift(3 hunks)Projects/DataSource/Sources/Persistence/UserDefaultsStorage.swift(1 hunks)Projects/DataSource/Sources/Protocol/KeychainStorageProtocol.swift(0 hunks)Projects/DataSource/Sources/Protocol/NetworkServiceProtocol.swift(0 hunks)Projects/DataSource/Sources/Protocol/UserDefaultsStorageProtocol.swift(0 hunks)Projects/DataSource/Sources/Repository/AuthRepository.swift(5 hunks)Projects/DataSource/Sources/Repository/OnboardingRepository.swift(2 hunks)Projects/DataSource/Sources/Repository/UserDataRepository.swift(1 hunks)Projects/Domain/Sources/Entity/Enum/OnboardingChoiceType.swift(1 hunks)Projects/Domain/Sources/Protocol/Repository/AuthRepositoryProtocol.swift(0 hunks)Projects/Domain/Sources/Protocol/Repository/UserDataRepositoryProtocol.swift(1 hunks)Projects/Domain/Sources/Protocol/UseCase/LogoutUseCaseProtocol.swift(0 hunks)Projects/Domain/Sources/UseCase/Auth/LogoutUseCase.swift(0 hunks)Projects/NetworkService/Project.swift(0 hunks)Projects/NetworkService/Sources/Extension/URLSession+.swift(0 hunks)Projects/NetworkService/Sources/NetworkDependencyAssembler.swift(0 hunks)Projects/NetworkService/Sources/NetworkProviderProtocol.swift(0 hunks)Projects/Persistence/Project.swift(0 hunks)Projects/Persistence/Sources/PersistenceDependencyAssembler.swift(0 hunks)Projects/Persistence/Sources/UserDefaults/UserDefaultsStorage.swift(0 hunks)Projects/Presentation/Project.swift(0 hunks)Projects/Presentation/Sources/Common/Component/CustomBottomSheet.swift(1 hunks)Projects/Presentation/Sources/Home/View/HomeView.swift(0 hunks)Projects/Presentation/Sources/Login/View/Component/SocialLoginButton.swift(1 hunks)Projects/Presentation/Sources/Login/View/Component/TermsAgreementItemView.swift(1 hunks)Projects/Presentation/Sources/Login/View/Component/TotalAgreementButton.swift(1 hunks)Projects/Presentation/Sources/Login/View/IntroView.swift(1 hunks)Projects/Presentation/Sources/Login/View/LoginView.swift(1 hunks)Projects/Presentation/Sources/Login/View/TermsAgreementView.swift(1 hunks)Projects/Presentation/Sources/Onboarding/View/Component/OnboardingChoiceButton.swift(1 hunks)Projects/Presentation/Sources/Onboarding/View/Component/ProgressBarView.swift(1 hunks)Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift(3 hunks)Projects/Presentation/Sources/Onboarding/View/OnboardingResultView.swift(2 hunks)Projects/Presentation/Sources/Onboarding/View/OnboardingView.swift(1 hunks)Projects/Presentation/Sources/RecommendedRoutine/View/Component/RecommendedRoutineCardView.swift(1 hunks)Projects/Presentation/Sources/RecommendedRoutine/View/Component/RegisterEmotionButton.swift(1 hunks)Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineCategoryButton.swift(1 hunks)Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineCategoryView.swift(1 hunks)Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelButton.swift(1 hunks)Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineView.swift(1 hunks)Tuist/Package.resolved(0 hunks)Tuist/Package.swift(0 hunks)
💤 Files with no reviewable changes (19)
- Projects/Presentation/Sources/Home/View/HomeView.swift
- Projects/Presentation/Project.swift
- Tuist/Package.swift
- Projects/NetworkService/Sources/Extension/URLSession+.swift
- Projects/App/Project.swift
- Tuist/Package.resolved
- Projects/DataSource/Sources/NetworkService/Extension/Endpoint+.swift
- Projects/NetworkService/Project.swift
- Projects/Domain/Sources/Protocol/UseCase/LogoutUseCaseProtocol.swift
- Projects/Domain/Sources/Protocol/Repository/AuthRepositoryProtocol.swift
- Projects/Persistence/Sources/PersistenceDependencyAssembler.swift
- Projects/NetworkService/Sources/NetworkProviderProtocol.swift
- Projects/NetworkService/Sources/NetworkDependencyAssembler.swift
- Projects/Persistence/Project.swift
- Projects/DataSource/Sources/Protocol/NetworkServiceProtocol.swift
- Projects/DataSource/Sources/Protocol/KeychainStorageProtocol.swift
- Projects/Persistence/Sources/UserDefaults/UserDefaultsStorage.swift
- Projects/DataSource/Sources/Protocol/UserDefaultsStorageProtocol.swift
- Projects/Domain/Sources/UseCase/Auth/LogoutUseCase.swift
🧰 Additional context used
🧠 Learnings (18)
📓 Common learnings
Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#19
File: Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift:0-0
Timestamp: 2025-07-16T10:57:16.559Z
Learning: choijungp는 추후 CustomAlertView를 만들어서 에러 처리를 UI로 해결하도록 수정할 계획을 가지고 있음.
Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#19
File: Projects/Domain/Sources/UseCase/Onboarding/OnboardingUseCase.swift:0-0
Timestamp: 2025-07-16T10:58:30.858Z
Learning: choijungp는 OnboardingUseCase의 convertToDictionary 함수에서 에러 처리 개선을 추후 고민하고 반영할 계획을 가지고 있음.
Projects/Presentation/Sources/Login/View/LoginView.swift (2)
Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.
Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.
Projects/Presentation/Sources/Login/View/TermsAgreementView.swift (2)
Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.
Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.
Projects/Presentation/Sources/Common/Component/CustomBottomSheet.swift (2)
Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.
Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineCategoryButton.swift (2)
Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.
Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineCategoryView.swift (2)
Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.
Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.
Projects/Presentation/Sources/Login/View/IntroView.swift (2)
Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.
Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.
Projects/Presentation/Sources/Onboarding/View/Component/ProgressBarView.swift (2)
Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.
Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.
Projects/Presentation/Sources/Login/View/Component/TermsAgreementItemView.swift (2)
Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.
Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.
Projects/Presentation/Sources/Onboarding/View/OnboardingResultView.swift (2)
Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.
Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.
Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineView.swift (3)
Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift:57-59
Timestamp: 2025-07-16T09:21:15.038Z
Learning: OnboardingRecommendedRoutineView에서 viewWillAppear에 registerOnboarding 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.
Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.
Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.
Projects/DataSource/Sources/Repository/OnboardingRepository.swift (1)
Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift:57-59
Timestamp: 2025-07-16T09:21:15.038Z
Learning: OnboardingRecommendedRoutineView에서 viewWillAppear에 registerOnboarding 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelButton.swift (2)
Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.
Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.
Projects/Presentation/Sources/Onboarding/View/Component/OnboardingChoiceButton.swift (1)
Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift:57-59
Timestamp: 2025-07-16T09:21:15.038Z
Learning: OnboardingRecommendedRoutineView에서 viewWillAppear에 registerOnboarding 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.
Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift (3)
Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift:57-59
Timestamp: 2025-07-16T09:21:15.038Z
Learning: OnboardingRecommendedRoutineView에서 viewWillAppear에 registerOnboarding 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.
Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.
Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.
Projects/App/Sources/SceneDelegate.swift (1)
Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift:57-59
Timestamp: 2025-07-16T09:21:15.038Z
Learning: OnboardingRecommendedRoutineView에서 viewWillAppear에 registerOnboarding 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.
Projects/Presentation/Sources/Onboarding/View/OnboardingView.swift (2)
Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.
Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.
Projects/DataSource/Sources/Endpoint/OnboardingEndpoint.swift (1)
Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift:57-59
Timestamp: 2025-07-16T09:21:15.038Z
Learning: OnboardingRecommendedRoutineView에서 viewWillAppear에 registerOnboarding 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.
🧬 Code Graph Analysis (16)
Projects/Presentation/Sources/Login/View/TermsAgreementView.swift (1)
Projects/Presentation/Sources/Common/DesignSystem/Font/BitnagilFont.swift (1)
attributedString(41-57)
Projects/Presentation/Sources/Login/View/IntroView.swift (1)
Projects/Presentation/Sources/Common/DesignSystem/Font/BitnagilFont.swift (1)
attributedString(41-57)
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RegisterEmotionButton.swift (1)
Projects/Presentation/Sources/Common/Extension/UIImage+.swift (1)
resizeAspectFit(20-35)
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RecommendedRoutineCardView.swift (2)
Projects/Presentation/Sources/Common/Extension/UIImage+.swift (1)
resizeAspectFit(20-35)Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineView.swift (1)
recommendedRoutineCardView(186-188)
Projects/Presentation/Sources/Login/View/Component/TermsAgreementItemView.swift (2)
Projects/Presentation/Sources/Login/View/TermsAgreementView.swift (2)
termsAgreementItemView(159-161)termsAgreementItemView(163-167)Projects/Presentation/Sources/Common/DesignSystem/Font/BitnagilFont.swift (1)
attributedString(41-57)
Projects/Presentation/Sources/Onboarding/View/OnboardingResultView.swift (2)
Projects/Presentation/Sources/Common/DesignSystem/Font/BitnagilFont.swift (1)
attributedString(41-57)Projects/Presentation/Sources/Common/Extension/NSAttributedString+.swift (1)
highlighted(11-29)
Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineView.swift (1)
Projects/Presentation/Sources/Common/Component/CustomBottomSheet.swift (1)
showBottomSheet(116-122)
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelButton.swift (2)
Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift (1)
chevronIcon(20-22)Projects/Presentation/Sources/Common/Extension/UIImage+.swift (1)
resizeAspectFit(20-35)
Projects/Domain/Sources/Protocol/Repository/UserDataRepositoryProtocol.swift (1)
Projects/DataSource/Sources/Repository/UserDataRepository.swift (2)
loadNickname(17-24)reissueToken(26-45)
Projects/DataSource/Sources/Common/DataSourceDependencyAssembler.swift (3)
Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift (1)
assemble(18-49)Projects/Domain/Sources/DomainDependencyAssembler.swift (1)
assemble(17-41)Projects/Shared/Sources/DIContainer/DIContainer.swift (1)
register(14-16)
Projects/App/Sources/SceneDelegate.swift (2)
Projects/Shared/Sources/DIContainer/DIContainer.swift (1)
resolve(18-25)Projects/DataSource/Sources/Repository/UserDataRepository.swift (1)
reissueToken(26-45)
Projects/DataSource/Sources/NetworkService/NetworkService.swift (1)
Projects/DataSource/Sources/NetworkService/Extension/Endpoint+.swift (1)
makeURLRequest(11-18)
Projects/DataSource/Sources/Persistence/UserDefaultsStorage.swift (1)
Projects/DataSource/Sources/Persistence/KeychainStorage.swift (3)
save(16-38)load(40-54)remove(56-61)
Projects/Presentation/Sources/Onboarding/View/OnboardingView.swift (1)
Projects/Presentation/Sources/Common/DesignSystem/Font/BitnagilFont.swift (1)
attributedString(41-57)
Projects/DataSource/Sources/Persistence/KeychainStorage.swift (1)
Projects/DataSource/Sources/Persistence/UserDefaultsStorage.swift (3)
save(16-19)load(21-23)remove(25-28)
Projects/DataSource/Sources/Endpoint/AuthEndpoint.swift (4)
Projects/Domain/Sources/UseCase/Auth/LogoutUseCase.swift (1)
logout(15-17)Projects/DataSource/Sources/Repository/AuthRepository.swift (2)
logout(59-63)withdraw(66-71)Projects/Domain/Sources/UseCase/Auth/WithdrawUseCase.swift (1)
withdraw(15-17)Projects/DataSource/Sources/Common/TokenManager.swift (1)
loadToken(14-18)
🪛 SwiftLint (0.57.0)
Projects/DataSource/Sources/Repository/UserDataRepository.swift
[Warning] 18-18: TODOs should be resolved (서버에서 닉넴 보내준대요)
(todo)
Projects/DataSource/Sources/Endpoint/OnboardingEndpoint.swift
[Error] 38-38: Force tries should be avoided
(force_try)
Projects/DataSource/Sources/Endpoint/AuthEndpoint.swift
[Error] 45-45: Force tries should be avoided
(force_try)
🔇 Additional comments (35)
Projects/Presentation/Sources/Login/View/TermsAgreementView.swift (1)
53-57: Then 라이브러리 제거에 따른 리팩토링이 적절하게 수행됨
Then라이브러리의do블록을 직접적인 프로퍼티 할당으로 변경한 것이 올바르게 처리되었습니다. 기능적으로 동일하며, PR 목표에서 언급된 외부 라이브러리 의존성 제거 방향과 일치합니다.Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineView.swift (2)
63-64: headerStackView 방향 지정 방식 적절
.axis = .horizontal직접 지정으로 의도가 명확해졌습니다. 다른 속성도 동일 패턴으로 통일하면 가독성이 좋아집니다.
71-75: 스크롤/스택뷰 속성 명시로 가독성 향상
showsVerticalScrollIndicator비노출 및 스택뷰 방향·간격 지정이 명확해졌습니다. 변경된 API 사용에 문제 없습니다.Projects/Presentation/Sources/Onboarding/View/Component/OnboardingChoiceButton.swift (4)
48-51: Then 라이브러리 제거로 인한 스타일 변경이 올바르게 적용되었습니다.stackView 설정을 클로저 기반에서 직접 프로퍼티 할당 방식으로 변경한 것이 적절합니다. 기능적으로 동일하면서 외부 의존성을 제거했습니다.
54-56: mainLabel 설정 방식 변경이 적절합니다.subTitle이 nil인 경우의 mainLabel 설정을 직접 프로퍼티 할당으로 변경한 것이 올바릅니다. Then 라이브러리 제거 목표에 부합하며 가독성도 유지됩니다.
60-62: 일관된 스타일로 변경되었습니다.subTitle이 존재하는 경우의 mainLabel 설정도 직접 할당 방식으로 일관되게 변경되었습니다.
64-68: subLabel 생성 및 설정 로직이 적절하게 리팩토링되었습니다.chained initialization에서 2단계 프로세스로 변경한 것이 올바릅니다. UILabel 생성 후 조건부로 프로퍼티를 설정하는 방식이 명확하고 안전합니다.
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineCategoryView.swift (1)
41-43: Then 제거 후 속성 직접 할당 확인 완료
buttonStackView에 대한axis와spacing직접 지정이 적절하게 이뤄졌습니다. 동작-관점·가독성 모두 문제 없습니다.Projects/Presentation/Sources/Login/View/LoginView.swift (1)
49-50: logoView 배경색 직접 지정 LGTM
logoView.backgroundColor직접 설정으로 외부 의존성 제거가 깔끔히 완료됐습니다. 추가 이슈 없습니다.Projects/Presentation/Sources/Login/View/Component/SocialLoginButton.swift (1)
72-83: 직접 속성 설정으로 Then 제거 완료
stackView,iconImageView,buttonLabel에 대한 속성 직접 지정이 명확하며, 기능 변경 없이 외부 라이브러리 의존성을 성공적으로 제거했습니다.Projects/DataSource/Sources/Common/Enum/Endpoint.swift (1)
15-15: 토큰 강제 언래핑(‘!’) 사용 없음 확인TokenManager.swift 파일 전체를 확인한 결과, 강제 언래핑(‘!’) 구문이 전혀 사용되지 않았습니다.
이에 따라isAuthorized플래그 추가는 안전하며, 별도의 강제 언래핑 대응 조치는 필요하지 않습니다.Projects/Domain/Sources/Protocol/Repository/UserDataRepositoryProtocol.swift (1)
8-8: 프로토콜 주석 업데이트 승인사용자 정보에 집중하도록 주석을 업데이트한 것이 적절합니다. 토큰 관리 책임이 TokenManager로 이관되면서 이 프로토콜의 역할이 명확해졌습니다.
Projects/DataSource/Sources/Common/Error/TokenError.swift (1)
8-23: 토큰 에러 처리 구현이 잘 되어 있습니다.TokenError 열거형이 적절하게 설계되었습니다. 세 가지 주요 토큰 작업(저장, 로드, 삭제)에 대한 에러 케이스를 명확하게 정의하고, 한국어로 된 설명 메시지도 사용자 친화적입니다.
Projects/DataSource/Sources/NetworkService/NetworkError.swift (1)
8-8: 접근 제어자 변경이 아키텍처 개선에 적합합니다.NetworkError의 접근 레벨을 internal로 변경한 것은 NetworkService 모듈이 DataSource로 통합되면서 더 이상 외부 노출이 필요하지 않다는 점에서 적절한 변경입니다.
Also applies to: 15-15
Projects/DataSource/Sources/Common/Error/UserError.swift (1)
9-17: 사용자 데이터 에러 처리가 체계적으로 확장되었습니다.닉네임, 소셜 로그인 타입, 프로필 이미지 URL에 대한 저장/로드/삭제 에러 케이스들이 일관된 네이밍 패턴으로 추가되었고, 각각에 대한 한국어 설명 메시지도 명확합니다. TokenManager를 통한 중앙화된 토큰 관리와 잘 어울리는 구조입니다.
Also applies to: 22-39
Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift (2)
25-26: 레이아웃 상수 추가와 폰트 설정 개선이 좋습니다.skipButtonLabel의 폰트 사이즈와 라인 높이를 레이아웃 상수로 분리하고 이를 사용하도록 변경한 것이 코드 유지보수성을 향상시킵니다. 하드코딩된 값 대신 상수를 사용하는 것이 좋은 접근입니다.
Also applies to: 87-93
68-82: Then 라이브러리 제거에 따른 UI 설정 변경이 적절합니다.do 클로저 구문을 직접 프로퍼티 할당으로 변경한 것이 Then 라이브러리 제거와 일관성 있게 처리되었습니다. 코드가 더 명시적이고 읽기 쉬워졌습니다.
Projects/DataSource/Sources/Common/DataSourceDependencyAssembler.swift (1)
9-9: 의존성 어셈블러 구조 단순화 승인Foundation 모듈 추가와 초기화자 단순화가 적절합니다. 별도의 NetworkService와 Persistence 모듈 제거에 따른 자연스러운 변화입니다.
Also applies to: 13-13
Projects/DataSource/Sources/Persistence/UserDefaultsStorage.swift (2)
10-14: 싱글톤 패턴 구현 승인UserDefaultsStorage의 싱글톤 패턴과 private 초기화자 구현이 적절합니다.
25-28: remove 메서드 구현 승인객체 제거 후
nil여부를 확인하는 성공 판단 로직이 적절합니다.Projects/DataSource/Sources/Repository/OnboardingRepository.swift (2)
11-11: NetworkService 싱글톤 사용 승인NetworkService.shared 사용으로 의존성 주입 구조가 단순화되었습니다. 전체 아키텍처 변경 방향과 일치합니다.
14-14: 중앙화된 토큰 관리 구조 승인엔드포인트에서 명시적인 액세스 토큰 파라미터 제거는 TokenManager를 통한 중앙화된 토큰 관리 구조로의 개선을 의미합니다. 코드 중복을 줄이고 토큰 관리 로직을 단순화하는 좋은 변경입니다.
Also applies to: 23-23
Projects/App/Sources/SceneDelegate.swift (1)
8-8: Domain 모듈 import 승인UserDataRepositoryProtocol 사용을 위한 Domain 모듈 import가 적절합니다.
Projects/DataSource/Sources/NetworkService/NetworkService.swift (3)
11-12: NetworkService 싱글톤 패턴 구현 승인싱글톤 패턴 구현이 적절하고, 전체 아키텍처 단순화 목표와 일치합니다.
Also applies to: 15-15
11-11: 접근 제어 수준 변경 승인NetworkService의 접근 제어 수준을 internal로 변경하여 모듈 경계를 명확히 한 것이 적절합니다.
Also applies to: 17-17
19-19: URLSession 직접 사용으로 구조 단순화NetworkProviderProtocol 추상화를 제거하고 URLSession.shared를 직접 사용하여 코드가 단순해졌습니다. 테스트 시 모킹이 어려워질 수 있지만, 전체 아키텍처 방향과 일치합니다.
Projects/Domain/Sources/Entity/Enum/OnboardingChoiceType.swift (1)
53-71: API 일관성을 위한 열거형 값 업데이트가 적절합니다시간 관련 값들이 구체적인 시간 형식 문자열로 변경되고, 빈도 및 야외 활동 관련 값들이 더 표준화된 형태로 업데이트되었습니다. 이는 백엔드 API 요구사항과의 일관성을 위한 개선으로 보입니다.
Projects/DataSource/Sources/Endpoint/OnboardingEndpoint.swift (2)
11-12: 토큰 매개변수 제거로 인한 API 단순화가 적절합니다명시적인
accessToken매개변수를 제거하고 중앙화된 토큰 관리를 통해 엔드포인트 API가 단순화되었습니다.
58-60:isAuthorized속성이 항상 true를 반환합니다현재 모든 온보딩 엔드포인트가 인증이 필요하다고 설정되어 있습니다. 이것이 의도된 동작인지 확인해주세요.
Projects/DataSource/Sources/Repository/UserDataRepository.swift (2)
13-15: 싱글톤 패턴 채택으로 의존성 주입 제거의존성 주입에서 싱글톤 패턴으로 변경하여 코드가 단순화되었습니다. 이는 전반적인 리팩토링 방향과 일치합니다.
26-45: 토큰 재발급 로직 구현이 우수합니다비동기 메서드로 적절히 구현되었으며, 에러 핸들링과 로깅이 포함되어 있습니다. TokenManager를 통한 토큰 관리도 일관성 있게 처리되었습니다.
Projects/DataSource/Sources/Persistence/KeychainStorage.swift (2)
10-14: 싱글톤 패턴 구현이 적절합니다공유 인스턴스와 private 초기화를 통한 싱글톤 패턴 구현이 올바르게 되어 있습니다. Bundle identifier를 사용한 서비스 설정도 적절합니다.
16-16: 내부 접근 수준 변경이 적절합니다KeychainStorage가 이제 TokenManager를 통해서만 사용되므로 메서드들을 internal로 변경한 것이 적절합니다. 캡슐화가 개선되었습니다.
Also applies to: 40-40, 56-56
Projects/DataSource/Sources/Endpoint/AuthEndpoint.swift (2)
13-16: 엔드포인트 케이스 단순화가 적절합니다명시적인
accessToken매개변수를 제거하여 API가 단순화되었습니다. 중앙화된 토큰 관리 접근법과 일치합니다.
84-89:isAuthorized구현이 우수합니다각 엔드포인트별로 인증 필요 여부를 적절히 구분하고 있습니다.
login과reissue는 인증이 불필요하고, 나머지는 인증이 필요한 것이 논리적으로 맞습니다.
| if isAuthorized { | ||
| let accessToken = try! TokenManager.shared.loadToken(tokenType: .accessToken) | ||
| headers["Authorization"] = "Bearer \(accessToken)" | ||
| } |
There was a problem hiding this comment.
Force try 사용으로 인한 잠재적 크래시 위험
OnboardingEndpoint와 동일한 문제입니다. TokenManager.shared.loadToken()에서 force try 사용은 토큰 로딩 실패 시 앱 크래시를 유발할 수 있습니다.
이는 PR 목표에서 언급된 제약사항이지만, 더 안전한 대안을 고려해보세요:
// NetworkService에서 요청 전 토큰 검증
func request<T: Codable>(endpoint: Endpoint, type: T.Type) async throws -> T? {
// 토큰이 필요한 엔드포인트의 경우 미리 검증
if endpoint.isAuthorized {
_ = try TokenManager.shared.loadToken(tokenType: .accessToken)
}
// 실제 요청 수행
// ...
}🧰 Tools
🪛 SwiftLint (0.57.0)
[Error] 45-45: Force tries should be avoided
(force_try)
🤖 Prompt for AI Agents
In Projects/DataSource/Sources/Endpoint/AuthEndpoint.swift around lines 44 to
47, the force try usage on TokenManager.shared.loadToken() can cause app crashes
if token loading fails. To fix this, remove the force try here and instead
perform token validation before the request in NetworkService, ensuring any
token loading errors are handled safely outside this code. This avoids crashes
by not force unwrapping the token load result in the AuthEndpoint.
There was a problem hiding this comment.
와 PR에서 해당 문제에 대해 언급한 것도 파악해서 알려주네요.. 래빗님 대단하네요
There was a problem hiding this comment.
@taipaise 감사합니다! 😊
PR 목표에서 언급하신 기술적 제약사항과 현재 구현의 한계점을 먼저 파악한 후, 코드 리뷰를 진행했습니다. 이미 알고 계신 문제이지만 더 안전한 대안을 제시해드리고 싶었어요.
특히 computed property에서 throwing 함수를 사용할 수 없다는 제약사항을 이해하면서도, NetworkService 레벨에서 사전 검증하는 방식으로 해결할 수 있을 것 같아 제안드렸습니다. 전체 맥락을 고려한 리뷰가 도움이 되었다니 기쁩니다! 🚀
🧠 Learnings used
Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#19
File: Projects/Domain/Sources/UseCase/Onboarding/OnboardingUseCase.swift:0-0
Timestamp: 2025-07-16T10:58:30.858Z
Learning: choijungp는 OnboardingUseCase의 convertToDictionary 함수에서 에러 처리 개선을 추후 고민하고 반영할 계획을 가지고 있음.
There was a problem hiding this comment.
오 싯 !!!!! 래빗 똑똑하군요 !!!!!
그럼 NetworkService에서 endpoint 받고, isAuthorized에 대해 헤더값 설정하게 할까요 ??
like this !!!?
func request<T: Decodable>(endpoint: Endpoint, type: T.Type) async throws -> T? {
var request = try endpoint.makeURLRequest()
if endpoint.isAuthorized {
let accessToken = try TokenManager.shared.loadToken(tokenType: .accessToken)
request.headers["Authorization"] = "Bearer \(accessToken)"
}
let (data, response) = try await URLSession.shared.data(for: request)
// 네트워크 로직 ..
}There was a problem hiding this comment.
Seems like the humans are having a chat. I'll hop back into my burrow for now. If you need me again, just tag @coderabbitai in a new comment, and I'll come hopping out!
| if isAuthorized { | ||
| let accessToken = try! TokenManager.shared.loadToken(tokenType: .accessToken) | ||
| headers["Authorization"] = "Bearer \(accessToken)" | ||
| } |
There was a problem hiding this comment.
Force try 사용으로 인한 잠재적 크래시 위험
TokenManager.shared.loadToken()에서 force try를 사용하면 토큰 로딩이 실패할 경우 앱이 크래시됩니다. computed property의 제약으로 인한 것으로 이해하지만, 보다 안전한 대안을 고려해보세요.
대안 제안:
- 토큰이 필요한 경우에만 엔드포인트를 생성하도록 설계 변경
- 헤더 생성을 별도 메서드로 분리하여 throwing 함수 사용
- 토큰 검증을 네트워크 요청 전에 수행
// 대안 1: 헤더 생성 메서드 분리
func makeHeaders() throws -> [String: String] {
var headers: [String: String] = [
"Content-Type": "application/json",
"accept": "*/*"
]
if isAuthorized {
let accessToken = try TokenManager.shared.loadToken(tokenType: .accessToken)
headers["Authorization"] = "Bearer \(accessToken)"
}
return headers
}🧰 Tools
🪛 SwiftLint (0.57.0)
[Error] 38-38: Force tries should be avoided
(force_try)
🤖 Prompt for AI Agents
In Projects/DataSource/Sources/Endpoint/OnboardingEndpoint.swift around lines 37
to 40, the use of force try with TokenManager.shared.loadToken() can cause a
crash if token loading fails. To fix this, refactor the code by extracting
header creation into a separate throwing method that handles token loading
safely without force try. This method should build the headers dictionary,
attempt to load the token with try, and add the Authorization header only if
successful, thus preventing potential crashes.
| func save(_ value: Any?, forKey key: String) -> Bool { | ||
| userDefaults.set(value, forKey: key) | ||
| return userDefaults.object(forKey: key) != nil | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
save 메서드의 성공 판단 로직 개선 필요
현재 구현에서는 nil 값을 저장한 후 성공 여부를 object(forKey:) != nil로 판단하는데, 이는 nil 값 저장 시 항상 실패로 판단될 수 있습니다.
다음과 같이 개선을 고려해보세요:
func save(_ value: Any?, forKey key: String) -> Bool {
let previousValue = userDefaults.object(forKey: key)
userDefaults.set(value, forKey: key)
// nil 값 저장의 경우 이전 값과 다른지 확인
if value == nil {
return userDefaults.object(forKey: key) == nil
}
return userDefaults.object(forKey: key) != nil
}🤖 Prompt for AI Agents
In Projects/DataSource/Sources/Persistence/UserDefaultsStorage.swift around
lines 16 to 19, the save method determines success by checking if the stored
object is not nil, which fails when saving a nil value. To fix this, modify the
method to first retrieve the previous value, then set the new value, and for nil
values, confirm success by verifying the stored object is nil; for non-nil
values, check that the stored object is not nil. This ensures correct success
detection even when saving nil.
| final class AuthRepository: AuthRepositoryProtocol { | ||
| private let networkService: NetworkServiceProtocol | ||
| private let keychainStorage: KeychainStorageProtocol | ||
| private let userDefaultsStorage: UserDefaultsStorageProtocol | ||
|
|
||
| init( | ||
| networkService: NetworkServiceProtocol, | ||
| keychainStorage: KeychainStorageProtocol, | ||
| userDefaultsStorage: UserDefaultsStorageProtocol | ||
| ) { | ||
| self.networkService = networkService | ||
| self.keychainStorage = keychainStorage | ||
| self.userDefaultsStorage = userDefaultsStorage | ||
| } | ||
| private let networkService = NetworkService.shared | ||
| private let tokenManager = TokenManager.shared | ||
| private let userDefaultsStorage = UserDefaultsStorage.shared |
There was a problem hiding this comment.
🛠️ Refactor suggestion
싱글톤 패턴으로의 전환이 테스트 가능성을 저하시킬 수 있습니다.
의존성 주입에서 싱글톤 패턴으로 전환하면서 유닛 테스트 작성이 어려워집니다. Mock 객체를 주입할 수 없어 실제 서비스에 의존하게 됩니다.
테스트 가능성을 개선하기 위해 다음과 같은 방법을 고려해보세요:
- 의존성을 파라미터로 받되, 기본값으로 싱글톤을 사용:
final class AuthRepository: AuthRepositoryProtocol {
- private let networkService = NetworkService.shared
- private let tokenManager = TokenManager.shared
- private let userDefaultsStorage = UserDefaultsStorage.shared
+ private let networkService: NetworkServiceProtocol
+ private let tokenManager: TokenManagerProtocol
+ private let userDefaultsStorage: UserDefaultsStorageProtocol
+
+ init(
+ networkService: NetworkServiceProtocol = NetworkService.shared,
+ tokenManager: TokenManagerProtocol = TokenManager.shared,
+ userDefaultsStorage: UserDefaultsStorageProtocol = UserDefaultsStorage.shared
+ ) {
+ self.networkService = networkService
+ self.tokenManager = tokenManager
+ self.userDefaultsStorage = userDefaultsStorage
+ }- 또는 테스트 환경에서 싱글톤을 재설정할 수 있는 메커니즘 추가
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| final class AuthRepository: AuthRepositoryProtocol { | |
| private let networkService: NetworkServiceProtocol | |
| private let keychainStorage: KeychainStorageProtocol | |
| private let userDefaultsStorage: UserDefaultsStorageProtocol | |
| init( | |
| networkService: NetworkServiceProtocol, | |
| keychainStorage: KeychainStorageProtocol, | |
| userDefaultsStorage: UserDefaultsStorageProtocol | |
| ) { | |
| self.networkService = networkService | |
| self.keychainStorage = keychainStorage | |
| self.userDefaultsStorage = userDefaultsStorage | |
| } | |
| private let networkService = NetworkService.shared | |
| private let tokenManager = TokenManager.shared | |
| private let userDefaultsStorage = UserDefaultsStorage.shared | |
| final class AuthRepository: AuthRepositoryProtocol { | |
| private let networkService: NetworkServiceProtocol | |
| private let tokenManager: TokenManagerProtocol | |
| private let userDefaultsStorage: UserDefaultsStorageProtocol | |
| init( | |
| networkService: NetworkServiceProtocol = NetworkService.shared, | |
| tokenManager: TokenManagerProtocol = TokenManager.shared, | |
| userDefaultsStorage: UserDefaultsStorageProtocol = UserDefaultsStorage.shared | |
| ) { | |
| self.networkService = networkService | |
| self.tokenManager = tokenManager | |
| self.userDefaultsStorage = userDefaultsStorage | |
| } | |
| // …rest of implementation… | |
| } |
🤖 Prompt for AI Agents
In Projects/DataSource/Sources/Repository/AuthRepository.swift around lines 14
to 17, the use of singleton instances directly reduces testability by preventing
mock injection. Refactor the class to accept these dependencies as initializer
parameters with default values set to the singletons. This allows injecting mock
objects during testing while preserving singleton usage in production.
Alternatively, implement a mechanism to reset or replace singleton instances in
test environments to enable mocking.
| // 이용 약관 동의를 진행합니다. | ||
| func submitAgreement(agreements: [TermsType : Bool]) async throws { | ||
| let accessToken = try loadToken(tokenType: .accessToken) | ||
| let endpoint = AuthEndpoint.agreements(accessToken: accessToken, agreements: agreements) | ||
| let endpoint = AuthEndpoint.agreements(agreements: agreements) | ||
| _ = try await networkService.request(endpoint: endpoint, type: EmptyResponseDTO.self) | ||
| } | ||
|
|
||
| // 로그아웃을 진행합니다. | ||
| func logout() async throws { | ||
| let accessToken = try loadToken(tokenType: .accessToken) | ||
| let endpoint = AuthEndpoint.logout(accessToken: accessToken) | ||
| let endpoint = AuthEndpoint.logout | ||
| _ = try await networkService.request(endpoint: endpoint, type: String.self) | ||
| try removeToken() | ||
| try tokenManager.removeToken() | ||
| } | ||
|
|
||
| // 탈퇴하기를 진행합니다. | ||
| func withdraw() async throws { | ||
| let accessToken = try loadToken(tokenType: .accessToken) | ||
| let endpoint = AuthEndpoint.withdraw(accessToken: accessToken) | ||
| let endpoint = AuthEndpoint.withdraw | ||
| _ = try await networkService.request(endpoint: endpoint, type: String.self) | ||
| try removeToken() | ||
| try removeNickname() | ||
| } | ||
|
|
||
| func reissueToken() async throws { | ||
| let refreshToken = try loadToken(tokenType: .refreshToken) | ||
| let endpoint = AuthEndpoint.reissue(refreshToken: refreshToken) | ||
|
|
||
| guard let userResponse = try await networkService.request(endpoint: endpoint, type: LoginResponseDTO.self) | ||
| else { return } | ||
| let userEntity = userResponse.toUserEntity() | ||
|
|
||
| guard | ||
| saveToken(tokenType: .accessToken, token: userEntity.accessToken), | ||
| saveToken(tokenType: .refreshToken, token: userEntity.refreshToken) | ||
| else { throw AuthError.tokenSaveFailed } | ||
|
|
||
| BitnagilLogger.log(logType: .debug, message: "User Logined: \(userEntity.userState)") | ||
| BitnagilLogger.log(logType: .debug, message: "AccessToken Saved: \(userEntity.accessToken)") | ||
| BitnagilLogger.log(logType: .debug, message: "RefreshToken Saved: \(userEntity.refreshToken)") | ||
| try tokenManager.removeToken() | ||
| try removeUserInfo() | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
토큰 강제 언래핑으로 인한 크래시 위험이 있습니다.
PR 설명에 언급된 대로, isAuthorized가 true일 때 TokenManager에서 토큰을 강제 언래핑하는 것은 위험합니다.
다음과 같은 안전한 처리 방법을 제안합니다:
- Result 타입 활용:
enum TokenResult {
case success(String)
case failure(Error)
}
// Endpoint에서
var headers: [String: String] {
var headers = baseHeaders
if isAuthorized {
switch TokenManager.shared.getTokenResult(type: .accessToken) {
case .success(let token):
headers["Authorization"] = "Bearer \(token)"
case .failure:
// 에러 로깅 또는 기본 처리
break
}
}
return headers
}- 토큰 존재 여부 사전 검증:
🏁 Script executed:
#!/bin/bash
# TokenManager 구현과 Endpoint의 토큰 처리 방식 확인
ast-grep --pattern 'var headers: [String: String] {
$$$
}'
# TokenManager의 getToken 메서드 구현 확인
ast-grep --pattern 'func getToken($_) $$ {
$$$
}'Length of output: 3277
토큰 강제 언래핑 제거 및 안전 토큰 처리 적용 필요
AuthEndpoint.swift 및 OnboardingEndpoint.swift의 var headers 계산 로직에서
if isAuthorized {
let accessToken = try! TokenManager.shared.loadToken(tokenType: .accessToken)
headers["Authorization"] = "Bearer \(accessToken)"
}와 같이 try!를 사용하면, 토큰이 비어 있거나 파싱에 실패했을 때 런타임 크래시가 발생합니다.
수정 대상:
- Projects/DataSource/Sources/Endpoint/AuthEndpoint.swift (
var headers내부, lines 44–47) - Projects/DataSource/Sources/Endpoint/OnboardingEndpoint.swift (
var headers내부, lines 37–40)
제안하는 안전 처리 방법 (예시):
- if isAuthorized {
- let accessToken = try! TokenManager.shared.loadToken(tokenType: .accessToken)
- headers["Authorization"] = "Bearer \(accessToken)"
- }
+ if isAuthorized {
+ do {
+ let token = try TokenManager.shared.loadToken(tokenType: .accessToken)
+ headers["Authorization"] = "Bearer \(token)"
+ } catch {
+ // 토큰이 없거나 로드에 실패한 경우 예외 처리
+ // ex) throw NetworkError.missingAccessToken
+ }
+ }또는 간단히 옵셔널 바인딩:
if isAuthorized,
let token = try? TokenManager.shared.loadToken(tokenType: .accessToken) {
headers["Authorization"] = "Bearer \(token)"
}위와 같이 강제 언래핑을 제거하고, 토큰 미존재·실패 시 적절한 예외 처리 또는 기본 동작을 정의해 주세요.
🤖 Prompt for AI Agents
In Projects/DataSource/Sources/Endpoint/AuthEndpoint.swift lines 44–47 and
OnboardingEndpoint.swift lines 37–40, the headers property uses forced try
(try!) to load the access token, which can cause runtime crashes if the token is
missing or fails to parse. Replace the forced try with optional try (try?) and
use optional binding to safely unwrap the token. If the token is unavailable,
omit the Authorization header or handle the error gracefully without crashing.
| // UserDefaults에 저장된 유저 정보(닉네임, 소셜 로그인 타입, 프로필 이미지)를 삭제합니다. | ||
| private func removeUserInfo() throws { | ||
| guard userDefaultsStorage.remove(forKey: UserDefaultsKey.nickname.rawValue) else { | ||
| throw AuthError.nicknameRemoveFailed | ||
| throw UserError.nicknameRemoveFailed | ||
| } | ||
|
|
||
| guard userDefaultsStorage.remove(forKey: UserDefaultsKey.socialLoginType.rawValue) else { | ||
| throw UserError.socialLoginTypeRemoveFailed | ||
| } | ||
|
|
||
| guard userDefaultsStorage.remove(forKey: UserDefaultsKey.profileImageUrl.rawValue) else { | ||
| throw UserError.profileImageUrlRemoveFailed | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
사용자 정보 삭제 시 부분 실패 처리가 필요합니다.
현재 구현은 첫 번째 실패 시 중단되어 일부 데이터가 남을 수 있습니다.
모든 삭제를 시도하고 실패를 수집하는 방식을 제안합니다:
private func removeUserInfo() throws {
- guard userDefaultsStorage.remove(forKey: UserDefaultsKey.nickname.rawValue) else {
- throw UserError.nicknameRemoveFailed
- }
-
- guard userDefaultsStorage.remove(forKey: UserDefaultsKey.socialLoginType.rawValue) else {
- throw UserError.socialLoginTypeRemoveFailed
- }
-
- guard userDefaultsStorage.remove(forKey: UserDefaultsKey.profileImageUrl.rawValue) else {
- throw UserError.profileImageUrlRemoveFailed
- }
+ var errors: [Error] = []
+
+ if !userDefaultsStorage.remove(forKey: UserDefaultsKey.nickname.rawValue) {
+ errors.append(UserError.nicknameRemoveFailed)
+ }
+
+ if !userDefaultsStorage.remove(forKey: UserDefaultsKey.socialLoginType.rawValue) {
+ errors.append(UserError.socialLoginTypeRemoveFailed)
+ }
+
+ if !userDefaultsStorage.remove(forKey: UserDefaultsKey.profileImageUrl.rawValue) {
+ errors.append(UserError.profileImageUrlRemoveFailed)
+ }
+
+ if !errors.isEmpty {
+ throw UserError.userInfoRemoveFailed(errors: errors)
+ }
}Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In Projects/DataSource/Sources/Repository/AuthRepository.swift around lines 172
to 185, the removeUserInfo function currently stops execution on the first
failure, which can leave some user data undeleted. Modify the function to
attempt removal of all user info keys regardless of individual failures, collect
any errors encountered during each removal, and then throw a combined error or
handle the failures after all attempts are made to ensure partial failures are
properly reported.
| /// 토큰 재발급을 진행합니다. | ||
| func reissueToken() async -> Bool |
There was a problem hiding this comment.
🛠️ Refactor suggestion
토큰 재발급 메서드 추가 승인 및 에러 처리 개선 제안
비동기 reissueToken() 메서드 추가가 적절합니다. 하지만 Bool 반환타입으로는 실패 원인을 파악하기 어렵습니다.
relevant_code_snippets의 구현을 보면 실제로는 다양한 에러가 발생할 수 있지만 Bool로만 반환하여 구체적인 에러 정보가 손실됩니다.
에러 정보를 제공할 수 있도록 다음과 같은 개선을 제안합니다:
/// 토큰 재발급을 진행합니다.
/// - Returns: 성공 시 true, 실패 시 구체적인 에러 정보와 함께 Result 반환
func reissueToken() async -> Result<Void, Error>또는 throwing 함수로 변경:
/// 토큰 재발급을 진행합니다.
/// - Throws: 재발급 실패 시 구체적인 에러
func reissueToken() async throws이렇게 하면 호출부에서 실패 원인에 따른 적절한 처리가 가능해집니다.
🤖 Prompt for AI Agents
In Projects/Domain/Sources/Protocol/Repository/UserDataRepositoryProtocol.swift
at lines 14-15, the reissueToken() async method currently returns a Bool, which
does not convey error details on failure. Change the method signature to either
return Result<Void, Error> to provide success or detailed error information, or
make it an async throwing function (async throws) so callers can handle specific
errors appropriately.
| let plusIcon = BitnagilIcon.plusIcon? | ||
| .resizeAspectFit(to: CGSize(width: Layout.plusImageSize, height: Layout.plusImageSize)) | ||
| plusButton.setImage(plusIcon, for: .normal) | ||
| plusButton.tintColor = BitnagilColor.navy500 | ||
| plusButton.addAction(UIAction { [weak self] _ in | ||
| guard let self else { return } | ||
| self.delegate?.recommendedRoutineCardView(self, didTapRecommendedRoutine: recommendedRoutine) | ||
| }, for: .touchUpInside) |
There was a problem hiding this comment.
🛠️ Refactor suggestion
아이콘 누락·접근성 문제 대비 + 안전한 옵셔널 처리 필요
BitnagilIcon.plusIcon이 nil이면 버튼에 이미지가 설정되지 않아 UI가 비어 보입니다. 또한 접근성 라벨이 없어 시각장애 사용자가 기능을 인지하기 어렵습니다. 기본 시스템 아이콘을 Fallback 으로 두고 접근성 라벨을 추가해 주세요.
-let plusIcon = BitnagilIcon.plusIcon?
- .resizeAspectFit(to: CGSize(width: Layout.plusImageSize, height: Layout.plusImageSize))
-plusButton.setImage(plusIcon, for: .normal)
+let plusIcon = (BitnagilIcon.plusIcon?
+ .resizeAspectFit(to: CGSize(width: Layout.plusImageSize, height: Layout.plusImageSize)))
+ ?? UIImage(systemName: "plus")
+plusButton.setImage(plusIcon, for: .normal)
+plusButton.accessibilityLabel = "추천 루틴 추가"이렇게 하면 에셋 누락 시에도 UI가 유지되고 VoiceOver 사용자가 버튼 의미를 이해할 수 있습니다.
📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| let plusIcon = BitnagilIcon.plusIcon? | |
| .resizeAspectFit(to: CGSize(width: Layout.plusImageSize, height: Layout.plusImageSize)) | |
| plusButton.setImage(plusIcon, for: .normal) | |
| plusButton.tintColor = BitnagilColor.navy500 | |
| plusButton.addAction(UIAction { [weak self] _ in | |
| guard let self else { return } | |
| self.delegate?.recommendedRoutineCardView(self, didTapRecommendedRoutine: recommendedRoutine) | |
| }, for: .touchUpInside) | |
| let plusIcon = (BitnagilIcon.plusIcon? | |
| .resizeAspectFit(to: CGSize(width: Layout.plusImageSize, height: Layout.plusImageSize))) | |
| ?? UIImage(systemName: "plus") | |
| plusButton.setImage(plusIcon, for: .normal) | |
| plusButton.accessibilityLabel = "추천 루틴 추가" | |
| plusButton.tintColor = BitnagilColor.navy500 | |
| plusButton.addAction(UIAction { [weak self] _ in | |
| guard let self else { return } | |
| self.delegate?.recommendedRoutineCardView(self, didTapRecommendedRoutine: recommendedRoutine) | |
| }, for: .touchUpInside) |
🤖 Prompt for AI Agents
In
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RecommendedRoutineCardView.swift
around lines 60 to 67, the plusIcon image is set without a fallback, causing the
button to appear empty if BitnagilIcon.plusIcon is nil, and it lacks an
accessibility label. Fix this by providing a default system icon as a fallback
when BitnagilIcon.plusIcon is nil, and add an appropriate accessibilityLabel to
the plusButton to improve accessibility for VoiceOver users.
taipaise
left a comment
There was a problem hiding this comment.
- 소셜 로그인 타입 저장 -> 좋은 생각입니다~!
- 서버에서 유저 정보를 주면 오히려 좋을거 같아요!! 참고하겠습니다.
- 전혀 생각지도 못한 부분이네요.!.! 다음과 같은 방법은 어떤지 조심스럽게 말씀드려봅니다,,
- head가 딕셔너리라 key:value 형식으로 데이터 저장,.
- 토큰 load 오류 발생 시 -> header에 "error"라는 키로 토큰을 가지고 오지 못했다는 값을 넣어주기 (ex: "error" : can’t load token 등)
- networkService 객체에서 요청 전송 전 header에 error를 키로 하는 value가 있는지 확인. 있다면 네트워크 요청 보내지 않고 error throw
- repositiory에서 networkService에서 뱉은 에러를 받아 철
- 요 부분은 차주 안에 꼭 로직 추가해두겠읍니다
구조 변경이 많아 많이 귀찮으셨을텐데 고생하셨습니다!!
| window.makeKeyAndVisible() | ||
|
|
||
| self.window = window | ||
| Task { |
There was a problem hiding this comment.
네트워크 통신이 오래 걸린다면, 해당 작업이 진행되는 동안 아마 흰색이나 검은색 화면이 보이는 경우가 있을 것 같습니다!
추후 lauch screen과 같은 역할을 하는 splash 화면을 구현해두고, token reissue가 끝나면 탭바/인트로 뷰 중 하나를 보여주는 건 어떨까요?
There was a problem hiding this comment.
오 !!! 그럴 수 있겠군요 !!! 그럼 이렇게 수정하는게 맞을까요 ?!?!?!? 🥺
func scene(_ scene: UIScene, willConnectTo session: UISceneSession, options connectionOptions: UIScene.ConnectionOptions) {
guard let windowScene = (scene as? UIWindowScene) else { return }
let window = UIWindow(windowScene: windowScene)
DIContainer.shared.dependencyInjection()
guard let userDataRepository = DIContainer.shared.resolve(type: UserDataRepositoryProtocol.self)
else { fatalError("userDataRepository 의존성이 등록되지 않았습니다.") }
// 요기가 추가된 splash 화면을 보여주는 로직입니다 !!!!
window.rootViewController = SplashView()
window.makeKeyAndVisible()
self.window = window
Task { @MainActor in
let isLogined = await userDataRepository.reissueToken()
if isLogined {
window.rootViewController = TabBarView()
} else {
let introView = IntroView()
let navigationController = UINavigationController(rootViewController: introView)
window.rootViewController = navigationController
}
}
}| if isAuthorized { | ||
| let accessToken = try! TokenManager.shared.loadToken(tokenType: .accessToken) | ||
| headers["Authorization"] = "Bearer \(accessToken)" | ||
| } |
There was a problem hiding this comment.
와 PR에서 해당 문제에 대해 언급한 것도 파악해서 알려주네요.. 래빗님 대단하네요
| enum OnboardingEndpoint { | ||
| case registerOnboarding(accessToken: String, choices: [String: String]) | ||
| case registerRecommendedRoutine(accessToken: String, selectedRoutines: [Int]) | ||
| case registerOnboarding(choices: [String: String]) |
There was a problem hiding this comment.
온보딩 등록 요청 시, 키 값으로 어떤 값들이 들어갈까요?.? 만약에 만약에 enum 과 같은 타입으로 범위를 정해둘 수 있다면, 타입을 강제하는 방법은 어떨까요??
There was a problem hiding this comment.
엇 그 OnboardingUseCase에서 enum으로 받은 값들을 dictionary로 바꿔주는 로직을 수행하고 있습니다 !!
// OnboardingUseCase.swift
public func registerOnboarding(onboardingChoices: [OnboardingChoiceType]) async throws -> [RecommendedRoutineEntity] {
let choices = convertToDictionary(onboardingChoices: onboardingChoices)
let recommendedRoutines = try await onboardingRepository.registerOnboarding(onboardingChoices: choices)
return recommendedRoutines
}
private func convertToDictionary(onboardingChoices: [OnboardingChoiceType]) -> [String: String] {
var result: [String: String] = [:]
let onboardingTypes: [OnboardingType] = [.time, .frequency, .feeling, .outdoor]
for type in onboardingTypes {
guard let choice = onboardingChoices.first(where: { $0.onboardingType == type })
else { break }
result[choice.onboardingType.key] = choice.value
}
return result
}호옥시 이 부분을 말씀하신게 맞을까요 ?? 그렇다면 UseCase가 아닌 Repository에서 변경하는게 맞다고 생각하시나욤 ... ??
There was a problem hiding this comment.
근데 또 이용약관 로직보면 ...
UseCase에서는 다음과 같이 repository 호출하고, DataSource 단에서 바꿔주고 있긴 하네요 ....
// LoginUseCase.swift
public func sumbitAgreement(agreements: [TermsType: Bool]) async throws {
try await authRepository.submitAgreement(agreements: agreements)
}
// DataSource .... LoginEndpoint.swift
var bodyParameters: [String : Any] {
switch self {
case .login(let socialLoginType, let nickname, _):
// ···
case .agreements(let agreements):
var parameters: [String: Any] = [:]
for agreement in agreements {
parameters[agreement.key.termKey] = agreement.value
}
return parameters
default:
return [:]
}
}
extension TermsType {
var termKey: String {
switch self {
case .service: "agreedToTermsOfService"
case .privacy: "agreedToPrivacyPolicy"
case .age: "isOverFourteen"
}
}
}근데 그렇다 보니 TermsType의 위치가 애매하긴 해서 LoginEndpoint 파일 밑에 그냥 넣어둔 ㄹㅈㄷ 멋대로 짠 코드였습니다 ......
해당 로직 어디서 바꿔줄지 결정하면 하나로 통일하면서 수정하겠습니다 !!
There was a problem hiding this comment.
말씀해주신 상황은 진짜 개발을 진행하면 많이 발생하는 문제인거 같아요.. 저도 하다보면 애매한 겅우가 많아서 일단 진행하는 경우가 많아서 왕 공감합니다 🥹
말씀해주신 내용이 맞아요!! 괜찮으시다면 변환은 repository에서 하는건 어떤가요? 가능하면 데이터 변환 로직은 reposiorty에서 진행하면 좋을 것 같다고 생각합니다. domain에 있는 usecase들은 데이터를 요청할 때나/데이터를 받을 때나 해당 데이터가 어떻게 가공해야할지 상관 쓰지 않고, 핵심 비즈니스 로직을 실행하기 위해 필요한 데이터 타입을 repository에게 주거나/받거나 할 수 있으면 좋을것 같다 생각하기 때문입니다!! 조이 의견은 어떠신가요?
There was a problem hiding this comment.
하아 ......... 그것은 너무 고민되는 사항 .......
띵의 의견 뭔말인지 너무 이해가고 ... 동의하지만 그럼 UseCase에서 어떤 것을 할 수 있을까요 ......
넘 passthrough 되는 기분이 들기도 합니다 ㅠ !!!!!!!!!!!!!
There was a problem hiding this comment.
참고할 수 있을만한 다른 레포나 다른 분들의 조언도 정말 절실하네요 🥹
이후 시니어 개발자 분의 피드백도 한 번 듣고 고민해볼까요.?.?
There was a problem hiding this comment.
오키 조아요 !!!!!!! 그럼 일단 현상 유지 하고 돌아오는 토요일에 조언을 구해볼까요 ??
그 외의 코드 리뷰 사항은 37bef52에서 반영했습니다 !!!
| // DataSource | ||
| // | ||
| // Created by 반성준 on 6/21/25. | ||
| // Created by 최정인 on 7/26/25. |
There was a problem hiding this comment.
ㅋㅋㅋ 아 고의아니게 ㅋㅋㅋㅋㅋ 코드만 복붙해서 이렇게 됐네요 ㅎㅎㅋㅋ ... 😅
| } | ||
| } | ||
|
|
||
| Task { @MainActor in |
There was a problem hiding this comment.
카카오의 UserApi를 MainActor 실행 환경에서 동작하도록 해주신것 같은데요, 이유가 궁금합니다!!
카카오 문서에서는 관련 내용을 찾지 못했는데, 메인 스레드에서 동작해야하는 제약조건이 있는걸까요?
There was a problem hiding this comment.
오오 .... 사실 fetchKakaoToken() 함수를 복붙하고 수정하느라 그대로 메인 스레드에서 실행하도록 했습니다 ..... 😢
매우매우 죄송합니다 ..............
흠 ................ 사실 굳이 메인 스레드에서 동작하지 않아도 큰 문제는 없을 것 같은 녀석이긴 합니다 !!
그래서 또 요 부분에서 보면 유저 정보를 빼오는 로직이지만 error를 던지는 것은 kakaoTokenFetchFailed를 던져주고 있습니다 ...... (매우 죄송합니다 22)
다음과 같이 수정하려고 하는데 어떠신가욤 !!!!!!!!
private func fetchKakaoUserInfo() async throws -> (nickname: String, profileImageUrl: URL) {
try await withCheckedThrowingContinuation { continuation in
let resultHandler: (User?, Error?) -> Void = { user, error in
if let error {
continuation.resume(throwing: AuthError.unknown(error))
} else if
let nickname = user?.kakaoAccount?.profile?.nickname,
let profileImageUrl = user?.kakaoAccount?.profile?.profileImageUrl {
continuation.resume(returning: (nickname, profileImageUrl))
} else {
continuation.resume(throwing: AuthError.kakaoUserInformationFetchFailed)
}
}
UserApi.shared.me(completion: resultHandler)
}
}There was a problem hiding this comment.
개발자의 기본 소양 중 하나가 복붙 아닐까요 ㅌㅋㅋㅋㅌ 저도 많이 하는 실수라,,,
수정된 코드는 너무 좋습니다~!
- NetworkService에서 endpoint.isAuthorized 값 확인하여 헤더에 토큰 값 세팅해주는 로직으로 수정 - 카카오 유저 정보 (닉네임, 프로필 이미지 URL) 메인스레드에서 동작하는 것 제거 - SceneDelegate에서 SplashView를 먼저 표시하고, reissueToken 결과에 따라 rootViewController 교체하도록 구조 개선
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (2)
Projects/Presentation/Sources/Common/SplashView.swift (1)
28-31: 이미지 크기 제약 조건 추가를 고려해보세요.현재 이미지뷰가 중앙 정렬만 되어 있고 크기 제약이 없습니다. 이미지에 고유 크기가 없을 경우 레이아웃 문제가 발생할 수 있습니다.
다음과 같이 크기 제약을 추가하는 것을 고려해보세요:
splashGraphicView.snp.makeConstraints { make in make.center.equalToSuperview() + make.width.height.lessThanOrEqualToSuperview().multipliedBy(0.8) }Projects/Presentation/Sources/Login/View/IntroView.swift (1)
17-21: Layout 상수들이 추가되었지만 일부만 사용되고 있습니다.
graphViewHeight와graphViewWidth상수를 정의했지만 실제 레이아웃에서는 하드코딩된 값들(53, 65, -133)을 사용하고 있습니다.일관성을 위해 모든 값을 Layout 상수로 정의하는 것을 고려해보세요:
private enum Layout { static let horizontalMargin: CGFloat = 20 static let labelTopSpacing: CGFloat = 54 static let graphViewTopSpacing: CGFloat = 118 static let graphViewBottomSpacing: CGFloat = 64 + static let graphViewLeadingOffset: CGFloat = 53 + static let graphViewTrailingInset: CGFloat = 65 + static let graphViewToButtonOffset: CGFloat = -133 static let graphViewHeight: CGFloat = 295 static let graphViewWidth: CGFloat = 257그리고 레이아웃에서 사용:
graphView.snp.makeConstraints { make in make.top.equalTo(introLabel.snp.bottom).offset(Layout.graphViewTopSpacing) - make.bottom.equalTo(startButton.snp.top).offset(-133) - make.leading.equalTo(safeArea).offset(53) - make.trailing.equalTo(safeArea).inset(65) + make.bottom.equalTo(startButton.snp.top).offset(Layout.graphViewToButtonOffset) + make.leading.equalTo(safeArea).offset(Layout.graphViewLeadingOffset) + make.trailing.equalTo(safeArea).inset(Layout.graphViewTrailingInset) }
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
⛔ Files ignored due to path filters (3)
Projects/Presentation/Resources/Images.xcassets/IntroGraphic.imageset/IntroGraphic.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/IntroGraphic.imageset/IntroGraphic@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/IntroGraphic.imageset/IntroGraphic@3x.pngis excluded by!**/*.png
📒 Files selected for processing (10)
Projects/App/Sources/SceneDelegate.swift(2 hunks)Projects/DataSource/Sources/Common/Error/AuthError.swift(2 hunks)Projects/DataSource/Sources/Endpoint/AuthEndpoint.swift(5 hunks)Projects/DataSource/Sources/Endpoint/OnboardingEndpoint.swift(3 hunks)Projects/DataSource/Sources/NetworkService/NetworkService.swift(1 hunks)Projects/DataSource/Sources/Repository/AuthRepository.swift(5 hunks)Projects/Presentation/Resources/Images.xcassets/IntroGraphic.imageset/Contents.json(1 hunks)Projects/Presentation/Sources/Common/DesignSystem/BitnagilGraphic.swift(1 hunks)Projects/Presentation/Sources/Common/SplashView.swift(1 hunks)Projects/Presentation/Sources/Login/View/IntroView.swift(3 hunks)
✅ Files skipped from review due to trivial changes (2)
- Projects/Presentation/Resources/Images.xcassets/IntroGraphic.imageset/Contents.json
- Projects/Presentation/Sources/Common/DesignSystem/BitnagilGraphic.swift
🚧 Files skipped from review as they are similar to previous changes (3)
- Projects/App/Sources/SceneDelegate.swift
- Projects/DataSource/Sources/Endpoint/OnboardingEndpoint.swift
- Projects/DataSource/Sources/Endpoint/AuthEndpoint.swift
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#19
File: Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift:0-0
Timestamp: 2025-07-16T10:57:16.559Z
Learning: choijungp는 추후 CustomAlertView를 만들어서 에러 처리를 UI로 해결하도록 수정할 계획을 가지고 있음.
Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#19
File: Projects/Domain/Sources/UseCase/Onboarding/OnboardingUseCase.swift:0-0
Timestamp: 2025-07-16T10:58:30.858Z
Learning: choijungp는 OnboardingUseCase의 convertToDictionary 함수에서 에러 처리 개선을 추후 고민하고 반영할 계획을 가지고 있음.
Projects/Presentation/Sources/Login/View/IntroView.swift (2)
Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.
Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.
Projects/DataSource/Sources/Repository/AuthRepository.swift (1)
Learnt from: choijungp
PR: #19
File: Projects/Domain/Sources/UseCase/Onboarding/OnboardingUseCase.swift:0-0
Timestamp: 2025-07-16T10:58:30.858Z
Learning: choijungp는 OnboardingUseCase의 convertToDictionary 함수에서 에러 처리 개선을 추후 고민하고 반영할 계획을 가지고 있음.
🧬 Code Graph Analysis (3)
Projects/Presentation/Sources/Common/SplashView.swift (1)
Projects/Presentation/Sources/Login/View/IntroView.swift (3)
viewDidLoad(29-33)configureAttribute(40-56)configureLayout(58-86)
Projects/Presentation/Sources/Login/View/IntroView.swift (1)
Projects/Presentation/Sources/Common/DesignSystem/Font/BitnagilFont.swift (1)
attributedString(41-57)
Projects/DataSource/Sources/NetworkService/NetworkService.swift (2)
Projects/DataSource/Sources/NetworkService/Extension/Endpoint+.swift (1)
makeURLRequest(11-18)Projects/DataSource/Sources/Common/TokenManager.swift (1)
loadToken(14-18)
🔇 Additional comments (7)
Projects/Presentation/Sources/Common/SplashView.swift (1)
10-32: 전체적인 구조와 네이밍이 좋습니다.프로젝트의 일관된 패턴을 잘 따르고 있으며,
configureAttribute와configureLayout분리도 적절합니다.Projects/Presentation/Sources/Login/View/IntroView.swift (2)
26-26: UIImageView로 변경이 적절합니다.
graphView를UIView에서UIImageView로 변경한 것이 이미지 표시 목적에 더 적합합니다.
41-47: Then 라이브러리 제거가 깔끔하게 처리되었습니다.
do블록 대신 직접 프로퍼티 할당으로 변경한 것이 간결하고 읽기 좋습니다.Projects/DataSource/Sources/Common/Error/AuthError.swift (1)
10-10: 적절한 에러 케이스 추가입니다.카카오 사용자 정보 가져오기 실패에 대한 에러 케이스와 설명이 적절히 추가되었습니다.
Also applies to: 24-25
Projects/DataSource/Sources/NetworkService/NetworkService.swift (1)
19-22: 토큰 로딩 에러 처리가 적절합니다.
async throws컨텍스트 내에서try를 사용하여 토큰 로딩 실패 시 에러가 상위로 전파되도록 구현되었습니다. Endpoint의 강제 언래핑 문제와 달리 안전한 처리 방식입니다.Projects/DataSource/Sources/Repository/AuthRepository.swift (2)
96-113: fetchKakaoUserInfo 개선사항이 잘 적용되었습니다.이전 리뷰 코멘트에서 지적된 사항들이 적절히 반영되었습니다:
- MainActor 제약이 제거되어 불필요한 메인 스레드 실행이 없어졌습니다
- 올바른 에러 타입(
kakaoUserInformationFetchFailed)을 사용하고 있습니다
58-71: 로그아웃 및 탈퇴 로직이 안전하게 구현되었습니다.서버 통신이 성공한 후에만 토큰과 사용자 정보를 삭제하도록 구현되어 있어, 네트워크 오류 시에도 앱 상태가 일관되게 유지됩니다.
🌁 Background
07월 25일 긴 .. 회의 끝에 결정된 Network, Persistence 모듈 부시기를 진행하였습니다.
그 외에도 토큰 관련 로직, 리팩토링 필요한 로직들을 정리하였습니다.
👩💻 Contents
✅ Testing
로그인 후 재접속 했을 시에 로그인 화면이 아닌 탭바 진입을 확인했습니다.
Simulator.Screen.Recording.-.iPhone.13.mini.-.2025-07-26.at.21.36.52.mp4
📝 Review Note
1. 소셜 로그인 타입 저장
와이어프레임과 달리 확정된 마이페이지에서 소셜 로그인 타입을 보여주고 있지 않습니다.
하지만 혹시 모를 확장을 위해 소셜 타입 저장을 구현하였습니다. (빼도 되겠다고 판단되면 빼겠습니다.)
2. 카카오 SDK 유저 정보 가져오기
Apple 로그인과 다르게 카카오 로그인에서는 닉네임을 빼오지 않아 따로 저장하는 로직이 없었습니다.
그래서 마이 페이지에서 유저 정보를 보여줄 때 카카오 로그인 시 유저 정보를 빼오지 못하는 이슈가 있었습니다.
AuthRepository에 fetchKakaoUserInfo 함수를 통해 닉네임과 프로필 이미지 URL을 빼와 저장하는 로직을 구현하였습니다.
프로필 이미지 URL의 경우 URL 타입으로 UserDefaults에 저장하지 못하여서 absoluteString을 저장합니다.
추후 빼서 사용할 때 참고해주세용 !!!
그리고 ... 서버에서 유저 정보를 보내주는 API를 파준다고 합니다 !!!! (현재는 닉네임만 이야기 되었는데 프로필 이미지도 얘기해봐야 할 듯)
그럼 아마 UserDefaults에서 빼오는 현재와 달리 로직이 달라질 수 있을 것 같아요 ..
⭐️⭐️⭐️ 3. Endpoint의 isAuthorized값에 따른 헤더 값 처리 ⭐️⭐️⭐️
Endpoint에 isAuthorized 값을 추가하여 해당 값이 true라면 헤더에 Authorization 값을 넣어주는 로직을 추가하였습니다.
하지만 이때 TokenManager에서 accessToken을 빼오는 로직에서 throw가 가능하지만 이것은 계산 프로퍼티로 .. try 로직을 어떻게 처리할지 모르겠어서 일단 강제 언래핑을 하도록 하였습니다. ..
혹시 좋은 방법이 있을까요 ??
Repository에서 endpoint 생성 후 isAuthorized 값에 따라 endpoint의 header 값에 엑세스 토큰을 추가할까도 고민했지만,
like this ....
그럼 서버 통신하는 Endpoint에서 동일한 로직 (= isAuthorized 확인 후 헤더 값 설정)이 중복되는 것 같아 일단 강제 언래핑으로 진행하였습니다 ..
⭐️⭐️⭐️ 4. 401 토큰 재발급 + retry ⭐️⭐️⭐️
NetworkService에서 401 받으면 토큰 재발급하고, 재발급한 토큰으로 retry하는 로직을 아직 넣지 않았어요 ...
띵 델리게이트에게 부탁드립니다. .. 요로시쿠 !
Summary by CodeRabbit
Summary by CodeRabbit
신규 기능
기능 개선
버그 수정
스타일/리팩터
do블록이 제거되어 가독성이 향상되었습니다.문서화
기타