Conversation
Walkthrough온보딩 데이터 흐름을 전면 개편: Domain에 OnboardingEntity 추가, 선택 배열 기반 인터페이스를 엔터티 기반으로 교체. DataSource에 요청/응답 DTO 및 엔드포인트 추가·변경, Repository/UseCase/Protocol 동기화. Presentation 레이어는 온보딩 결과 화면 분기 구조(EntryPoint) 도입, 토스트 노티/뷰 추가, 관련 VC/VM 및 네비게이션 경로 업데이트. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant VC as OnboardingResultViewController
participant VM as OnboardingViewModel
participant Repo as OnboardingRepository
participant API as OnboardingEndpoint
rect rgb(236,248,255)
note over VC,VM: EntryPoint = onboarding
User->>VC: 다음 버튼 탭
VC->>VM: makeOnboardingChoices
VM->>VM: OnboardingEntity 구성
VM-->>VC: onboardingEntity 발행
VC->>Repo: registerOnboarding(onboardingEntity)
Repo->>API: POST /api/v2/onboardings
API-->>Repo: [RecommendedRoutineDTO]
Repo-->>VC: [RecommendedRoutineEntity]
VC->>VC: ResultRecommendedRoutineViewController 푸시
end
sequenceDiagram
autonumber
actor User
participant VC as OnboardingResultViewController
participant VM as OnboardingViewModel
participant Repo as OnboardingRepository
participant API as OnboardingEndpoint
rect rgb(245,240,255)
note over VC,VM: EntryPoint = myPagePrevious
User->>VC: 화면 진입
VC->>VM: loadOnboardingResult
VM->>Repo: loadOnboardingResult()
Repo->>API: GET /api/v2/onboardings
API-->>Repo: OnboardingResponseDTO
Repo-->>VM: OnboardingEntity
VM-->>VC: 결과 문자열 업데이트
end
sequenceDiagram
autonumber
participant VM as ResultRecommendedRoutineViewModel
participant NC as NotificationCenter
participant RVC as RecommendedRoutineViewController
participant TV as ToastView
VM->>NC: post(.showRecommendedRoutineToast)
NC-->>RVC: notification
RVC->>TV: showToastMessageView()
TV->>TV: fade-in(0.35s) 후 자동 사라짐
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Poem
✨ 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. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
Status, Documentation and Community
|
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (4)
Projects/Domain/Sources/Entity/Enum/OnboardingChoiceType.swift (1)
28-49: 컴파일 오류: switch 분기에서 반환 누락
onboardingType는 값을 반환해야 하는 계산 프로퍼티입니다. 각 case에return이 빠져 컴파일되지 않습니다.- public var onboardingType: OnboardingType { - switch self { - case .morningTime: .time - case .eveningTime: .time - case .allTime: .time - - case .never: .frequency - case .rarely: .frequency - case .sometimes: .frequency - case .often: .frequency - - case .stability: .feeling - case .connection: .feeling - case .growth: .feeling - case .vitality: .feeling - - case .once: .outdoor - case .twoToThree: .outdoor - case .fourOrMore: .outdoor - case .notSure: .outdoor - } - } + public var onboardingType: OnboardingType { + switch self { + case .morningTime, .eveningTime, .allTime: + return .time + case .never, .rarely, .sometimes, .often: + return .frequency + case .stability, .connection, .growth, .vitality: + return .feeling + case .once, .twoToThree, .fourOrMore, .notSure: + return .outdoor + } + }Projects/Presentation/Sources/Onboarding/View/OnboardingResultViewController.swift (1)
232-235: UIStackView의 arrangedSubviews 제거 방식 수정 필요removeFromSuperview만 호출하면 UIStackView의 arrangedSubviews 목록에 남을 수 있습니다.
다음처럼 교체해 주세요:
- resultStackView.arrangedSubviews.forEach { - $0.removeFromSuperview() - } + resultStackView.arrangedSubviews.forEach { subview in + resultStackView.removeArrangedSubview(subview) + subview.removeFromSuperview() + }Projects/Presentation/Sources/Onboarding/View/OnboardingViewController.swift (1)
115-120: UIButton + UIAction 클로저에서 강한 순환 참조 발생 가능
UIAction클로저가self를 강하게 캡처해 버튼(View) → 액션 → VC로 순환 참조가 생길 수 있습니다. 선택지 버튼에서는[weak self]를 이미 사용 중이니 동일하게 적용해 주세요.아래처럼 수정 제안드립니다.
- nextButton.addAction( - UIAction { _ in - self.goNextStep() - }, - for: .touchUpInside) + nextButton.addAction( + UIAction { [weak self] _ in + self?.goNextStep() + }, + for: .touchUpInside)Projects/Presentation/Sources/ResultRecommendedRoutine/View/ResultRecommendedRoutineViewController.swift (1)
353-359: DI 실패 시 fatalError 제거 (프로덕션 크래시 위험)의존성 누락으로 앱이 즉시 크래시합니다. 사용자 피드백 후 안전 종료가 바람직합니다.
- guard let routineCreationViewModel = DIContainer.shared.resolve(type: RoutineCreationViewModel.self) - else { fatalError("routineCreationViewModel 의존성이 등록되지 않았습니다.") } + guard let routineCreationViewModel = DIContainer.shared.resolve(type: RoutineCreationViewModel.self) else { + BitnagilLogger.log(logType: .error, message: "RoutineCreationViewModel Resolve Fail: 의존성이 등록되지 않았습니다.") + assertionFailure("RoutineCreationViewModel not registered") + return + }
🧹 Nitpick comments (25)
Projects/Presentation/Sources/Onboarding/Model/RecommendedRoutine.swift (1)
16-16: routineType 기본값 처리와 Optional 불일치
- 현재 엔티티의
type이 nil일 때.recommendation으로 고정됩니다. 엔티티의category가 유효하면 우선 사용하고, 없을 때만.recommendation으로 폴백하는 편이 자연스럽습니다.- 모델 프로퍼티(
routineType: RoutineCategoryType?)는 Optional인데, 이니셜라이저 파라미터는 Non-Optional입니다. 결과적으로 항상 값이 들어오므로 Optional이 불필요해 보입니다. 일관성을 위해 Non-Optional로 변경을 제안합니다.아래처럼 수정을 제안드립니다.
- let routineType: RoutineCategoryType? + let routineType: RoutineCategoryType @@ - routineType: RoutineCategoryType, + routineType: RoutineCategoryType, @@ - routineType: type ?? .recommendation, + routineType: type ?? (category ?? .recommendation),Also applies to: 25-26, 46-46
Projects/Domain/Sources/Entity/OnboardingEntity.swift (1)
8-25: 동시성·비교 가능성 확보를 위한 프로토콜 채택 권장도메인 엔티티가 async 경계로 오가므로
Sendable채택을 권장합니다. 테스트와 변경 감지를 위해Equatable도 함께 유용합니다. 현재 필드 타입은 모두 자동Sendable조건을 만족합니다.-public struct OnboardingEntity { +public struct OnboardingEntity: Sendable, Equatable {Projects/Domain/Sources/Protocol/Repository/OnboardingRepositoryProtocol.swift (1)
9-12: LGTM — 결과 로드 API 추가 적절서명과 도메인 모델 정합성이 좋습니다. 에러 케이스(미등록 시) 명세는 구현부와 맞춰 문서화만 보완하면 됩니다.
Projects/Presentation/Sources/Onboarding/View/OnboardingResultViewController.swift (3)
51-51: 버튼 인스턴스 재생성 대신 타이틀만 갱신하세요불필요한 객체 재생성을 줄이고 스타일/타깃/제약 누락 위험을 제거할 수 있습니다.
다음처럼 한 개 인스턴스를 유지하는 편이 안전합니다:
- private var nextButton = PrimaryButton(buttonState: .default, buttonTitle: "다음") + private let nextButton = PrimaryButton(buttonState: .default, buttonTitle: "다음") @@ - if entryPoint == .myPagePrevious { - nextButton = PrimaryButton(buttonState: .default, buttonTitle: "변경하기") - } + if entryPoint == .myPagePrevious { + nextButton.setTitle("변경하기", for: .normal) + }Also applies to: 123-126
262-270: SnapKit 제약은 superview 추가 후 설정(팀 러닝 반영)time/outdoor 뷰는 resultStackView에 추가되기 전에 height 제약이 걸립니다. 이전 리뷰 러닝에 따라 순서를 바꾸는 것이 안전합니다.
- [timeResultView, feelingResultView, outdoorResultView].forEach { - $0.snp.makeConstraints { make in - make.height.equalTo(Layout.resultLabelHeight) - } - } - - [timeResultView, feelingResultStackView, outdoorResultView].forEach { - resultStackView.addArrangedSubview($0) - } + [timeResultView, feelingResultStackView, outdoorResultView].forEach { + resultStackView.addArrangedSubview($0) + } + [timeResultView, feelingResultView, outdoorResultView].forEach { + $0.snp.makeConstraints { make in + make.height.equalTo(Layout.resultLabelHeight) + } + }
273-296: DI resolve 중복 제거 및 실패 처리 완화 제안동일한 resolve/설정/생성 로직이 중복됩니다. 작은 팩토리로 정리하면 가독성과 안전성이 올라갑니다. fatalError 대신 assertionFailure+return nil도 고려해 보세요(릴리즈 크래시 완화).
- var nextView: UIViewController? - switch entryPoint { + var nextView: UIViewController? + let makeVC: (ResultRecommendedRoutineViewModelType, ResultRecommendedRoutineViewController.EntryPoint) -> UIViewController? = { type, entry in + guard let vm = DIContainer.shared.resolve(type: ResultRecommendedRoutineViewModel.self) else { + assertionFailure("ResultRecommendedRoutineViewModel 의존성이 등록되지 않았습니다.") + return nil + } + vm.configure(viewModelType: type) + return ResultRecommendedRoutineViewController(entryPoint: entry, viewModel: vm) + } + switch entryPoint { case .onboarding: - guard let resultRecommendedRoutineViewModel = DIContainer.shared.resolve(type: ResultRecommendedRoutineViewModel.self) - else { fatalError("resultRecommendedRoutineViewModel 의존성이 등록되지 않았습니다.") } - - resultRecommendedRoutineViewModel.configure(viewModelType: .onboarding(onboardingEntity: onboardingEntity)) - nextView = ResultRecommendedRoutineViewController(entryPoint: .onboarding, viewModel: resultRecommendedRoutineViewModel) + nextView = makeVC(.onboarding(onboardingEntity: onboardingEntity), .onboarding) case .myPagePrevious: break case .myPageResult: - guard let resultRecommendedRoutineViewModel = DIContainer.shared.resolve(type: ResultRecommendedRoutineViewModel.self) - else{ fatalError("resultRecommendedRoutineViewModel 의존성이 등록되지 않았습니다.") } - - resultRecommendedRoutineViewModel.configure(viewModelType: .mypage(onboardingEntity: onboardingEntity)) - nextView = ResultRecommendedRoutineViewController(entryPoint: .mypage, viewModel: resultRecommendedRoutineViewModel) + nextView = makeVC(.mypage(onboardingEntity: onboardingEntity), .mypage) }원하시면 제가 위 리팩터를 반영한 PR용 패치를 더 만들어 드리겠습니다.
Projects/Presentation/Sources/Login/View/Component/TotalAgreementButton.swift (1)
14-14: 타입 정밀도 통일: CFloat → CGFloat 권장UIKit/SnapKit과의 일관성을 위해 CGFloat 사용이 더 적합합니다.
- static let checkIconSize: CFloat = 24 + static let checkIconSize: CGFloat = 24Projects/Presentation/Sources/EmotionRegister/View/EmotionRegisterView.swift (1)
130-131: 변수명 개선 제안
초기화 시그니처(entryPoint: .emotion, viewModel:)가 정확히 일치합니다.
resultRecommendedRoutineView→resultRecommendedRoutineVC로 변경해 가독성을 높여 주세요.Projects/Presentation/Sources/Onboarding/View/OnboardingViewController.swift (1)
242-246: EntryPoint 분기 적용 깔끔합니다.마이페이지 유입 여부에 따라
.myPageResultvs.onboarding로 명확히 라우팅되는 점 좋습니다. 한 줄로 단순화하면 가독성이 조금 더 좋아집니다.아래처럼 축약 가능합니다.
- if isFromMypage { - nextView = OnboardingResultViewController(viewModel: viewModel, entryPoint: .myPageResult) - } else { - nextView = OnboardingResultViewController(viewModel: viewModel, entryPoint: .onboarding) - } + let entryPoint: OnboardingResultViewController.EntryPoint = isFromMypage ? .myPageResult : .onboarding + nextView = OnboardingResultViewController(viewModel: viewModel, entryPoint: entryPoint)Projects/Presentation/Sources/Common/Component/ToastView.swift (2)
65-74: 연속 호출 시 애니메이션 중첩 방지빠르게 여러 번 호출되면 페이드 인/아웃 애니메이션이 중첩될 수 있습니다. 시작 전에 기존 애니메이션 제거를 권장합니다.
func showToastMessageView() { - alpha = 0 + layer.removeAllAnimations() + alpha = 0 isHidden = false UIView.animate(withDuration: 0.35, delay: 0.01) { self.alpha = 1 } completion: { [weak self] _ in self?.hideToastMessageView() } }
76-82: hide 시에도 애니메이션 정리동일 이유로 hide 전에도 정리가 있으면 더 안전합니다.
private func hideToastMessageView() { - UIView.animate(withDuration: 0.35, delay: 2.0) { + layer.removeAllAnimations() + UIView.animate(withDuration: 0.35, delay: 2.0) { self.alpha = 0 } completion: { _ in self.isHidden = true } }Projects/Presentation/Sources/ResultRecommendedRoutine/View/ResultRecommendedRoutineViewController.swift (6)
65-71: 고정 높이(label height) 제거 또는 최소 높이로 완화하여 잘림/접근성 이슈 방지메인/서브 라벨을 고정 높이(30/24)로 제약하면 기기/언어/다이내믹 타입에 따라 텍스트가 잘릴 수 있습니다. 최소 높이 제약과 numberOfLines 설정으로 유연하게 표시하는 편이 안전합니다.
아래처럼 수정 제안드립니다.
@@ override func configureAttribute() { let mainLabelText = entryPoint.mainLabelText mainLabel.attributedText = BitnagilFont(style: .title2, weight: .bold).attributedString(text: mainLabelText) mainLabel.textColor = BitnagilColor.gray10 mainLabel.textAlignment = .left + mainLabel.numberOfLines = 0 let subLabelText = entryPoint.subLabelText subLabel.attributedText = BitnagilFont(style: .body1, weight: .medium).attributedString(text: subLabelText) subLabel.textColor = BitnagilColor.gray50 subLabel.textAlignment = .left + subLabel.numberOfLines = 0 @@ mainLabel.snp.makeConstraints { make in make.leading.equalTo(safeArea).offset(Layout.horizontalMargin) make.trailing.equalTo(safeArea).inset(Layout.horizontalMargin) mainLabelTopConstraint = make.top.equalTo(safeArea).offset(Layout.mainLabelMinTopSpacing).constraint - make.height.equalTo(Layout.mainLabelHeight) + make.height.greaterThanOrEqualTo(Layout.mainLabelHeight) } @@ subLabel.snp.makeConstraints { make in make.leading.equalTo(safeArea).offset(Layout.horizontalMargin) make.trailing.equalTo(safeArea).inset(Layout.horizontalMargin) - make.top.equalTo(mainLabel.snp.bottom).offset(Layout.subLabelTopSpacing) - make.height.equalTo(Layout.subLabelHeight) + make.top.equalTo(mainLabel.snp.bottom).offset(Layout.subLabelTopSpacing) + make.height.greaterThanOrEqualTo(Layout.subLabelHeight) }Also applies to: 170-175, 177-182, 129-138
112-126: 회전/사이즈 클래스 변경 시 탑 스페이싱 재계산 필요isLayoutConfigured로 1회만 계산하면 회전(가로/세로) 시 레이아웃이 어색해질 수 있습니다. traitCollectionDidChange 또는 viewSafeAreaInsetsDidChange에서 재계산을 트리거하는 것을 권장합니다.
161-162: 온보딩 외 진입점에서 진행바 숨김마이페이지/감정 경로에서도 진행바가 생성됩니다. 온보딩 진입에서만 진행바를 설정하도록 가드하는 편이 UX에 적합합니다.
- configureCustomNavigationBar(navigationBarStyle: .withProgressBar(step: OnboardingType.allCases.count + 1)) + if case .onboarding = entryPoint { + configureCustomNavigationBar(navigationBarStyle: .withProgressBar(step: OnboardingType.allCases.count + 1)) + }
246-257: 등록 실패 시 사용자 피드백 제공 필요실패 시 로그만 남고 사용자 안내가 없습니다. 토스트/얼럿 등 즉시 피드백을 노출해 주세요.
313-328: .emotion 스킵 경로의 popToViewController 인덱스 접근은 취약스택 구조 변경 시 index 기반(-3) pop은 쉽게 깨집니다. 특정 타입의 VC를 탐색해 pop하거나, 라우터/코디네이터로 되돌림 지점을 명시하세요.
107-110: 결과 재조회 시점 검토 (뷰 재진입 시 동기화)viewDidLoad 1회 호출만으로는 이전 화면에서 선택 변경 후 돌아오는 케이스를 커버하기 어렵습니다. 과거 온보딩 추천 화면과 동일하게 viewWillAppear에서 재조회 트리거하는 패턴을 고려해 주세요.
Projects/Presentation/Sources/MyPage/View/MypageView.swift (1)
128-131: DIContainer 네임스페이스 사용 일관화같은 파일에서 Shared.DIContainer와 DIContainer를 혼용합니다. 한 가지 방식으로 통일해 가독성을 높여 주세요.
Also applies to: 170-174
Projects/DataSource/Sources/Endpoint/OnboardingEndpoint.swift (2)
37-43: Accept 헤더 구체화 제안
"*/*"대신"application/json"명시가 서버/캐시 계층에서 더 예측 가능합니다.- "accept": "*/*" + "accept": "application/json"
49-57: GET 케이스의 기본 바디 파라미터 처리 확인현재 default로 빈 딕셔너리를 반환합니다. 네트워크 레이어가 빈 바디를 자동 생략하지 않는다면 GET에 Body가 붙을 수 있으니, GET일 때는 nil 처리 또는 직렬화 스킵을 확인해 주세요.
Projects/DataSource/Sources/DTO/OnboardingDTO.swift (1)
10-15: CodingKeys를 명시해 키 변경 리스크 축소현재는 프로퍼티명이 API 키와 우연히 일치해 동작합니다. 추후 리팩터링(프로퍼티명 변경 등) 시 안전하게 유지되도록 CodingKeys를 명시하는 것을 권장합니다.
struct OnboardingDTO: Encodable { let timeSlot: String let emotionType: [String] let realOutingFrequency: String? let targetOutingFrequency: String + + private enum CodingKeys: String, CodingKey { + case timeSlot + case emotionType + case realOutingFrequency + case targetOutingFrequency + }Projects/Presentation/Sources/ResultRecommendedRoutine/ViewModel/ResultRecommendedRoutineViewModel.swift (2)
161-168: 토스트 노출 0.1초 지연은 플리키(flicker)/레이스 유발 소지 — 즉시 메인 스레드에서 발행 권장불필요한 지연을 제거하고 메인 큐에서 바로 Notification을 보내세요.
- private func showRecommendedRoutineToastMessageView() { - DispatchQueue.main.asyncAfter(deadline: .now() + 0.1) { - NotificationCenter.default.post( - name: .showRecommendedRoutineToast, - object: nil, - userInfo: nil) - } - } + private func showRecommendedRoutineToastMessageView() { + DispatchQueue.main.async { + NotificationCenter.default.post(name: .showRecommendedRoutineToast, object: nil) + } + }Also applies to: 10-10
106-111: 결과 발행 전 빈 배열 재발행은 UI 깜빡임 가능스켈레톤/플레이스홀더를 별도로 운용하지 않는다면
send([])를 생략해 불필요한 레이아웃 재계산을 줄이는 것이 좋습니다.- resultRecommendedRoutinesSubject.send([]) resultRecommendedRoutinesSubject.send(recommendedRoutines)Projects/DataSource/Sources/Repository/OnboardingRepository.swift (1)
23-31: 빈 본문(nil) 응답을 빈 배열로 삼키면 장애 탐지 어려움 — API 스펙에 맞게 분리 처리 권장서버가 204/빈 페이로드를 의도적으로 반환하는지 확인하고, 의도치 않은 nil이라면 throw로 상위에 전달해 관측 가능하게 해주세요. 최소한 로그를 남겨 원인 분석 가능성을 확보하는 게 좋습니다.
- guard let response = try await networkService.request(endpoint: endpoint, type: RecommendedRoutineListResponseDTO.self) - else { return [] } + guard let response = try await networkService.request(endpoint: endpoint, type: RecommendedRoutineListResponseDTO.self) else { + BitnagilLogger.log(logType: .error, message: "registerOnboarding 응답이 비어 있습니다.") + // API가 204를 의도한다면 [] 반환 유지, 아니라면 명시적으로 throw를 고려하세요. + return [] + }API 문서 기준으로 204/빈 배열 여부를 확인해 주세요. 필요 시 에러 타입(UserError.onboardingRegisterFailed 등)을 정의해 throw로 바꾸는 것을 제안합니다.
Projects/Presentation/Sources/Onboarding/ViewModel/OnboardingViewModel.swift (1)
175-197: TODO 제거: 로드 실패 시 사용자 피드백 경로 추가SwiftLint 경고(TODO) 해소 및 UX 측면에서 실패 시 토스트/알럿 혹은 에러 퍼블리셔를 통해 화면에 반영되도록 처리해 주세요.
- } catch { - // TODO: 에러 처리 - } + } catch { + BitnagilLogger.log(logType: .error, message: "온보딩 결과 로드 실패: \(error.localizedDescription)") + // 예: 에러 퍼블리셔 추가 또는 토스트 노출 트리거 + // errorSubject.send("온보딩 결과를 불러오지 못했어요. 잠시 후 다시 시도해 주세요.") + }
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (26)
Projects/DataSource/Sources/DTO/OnboardingDTO.swift(1 hunks)Projects/DataSource/Sources/DTO/OnboardingResponseDTO.swift(1 hunks)Projects/DataSource/Sources/DTO/RecommendedRoutineDTO.swift(3 hunks)Projects/DataSource/Sources/Endpoint/OnboardingEndpoint.swift(2 hunks)Projects/DataSource/Sources/Repository/OnboardingRepository.swift(1 hunks)Projects/Domain/Sources/Entity/Enum/OnboardingChoiceType.swift(1 hunks)Projects/Domain/Sources/Entity/OnboardingEntity.swift(1 hunks)Projects/Domain/Sources/Entity/RecommendedRoutineEntity.swift(2 hunks)Projects/Domain/Sources/Protocol/Repository/OnboardingRepositoryProtocol.swift(1 hunks)Projects/Domain/Sources/Protocol/UseCase/ResultRecommendedRoutineUseCaseProtocol.swift(1 hunks)Projects/Domain/Sources/UseCase/ResultRecommendedRoutine/ResultRecommendedRoutineUseCase.swift(1 hunks)Projects/Presentation/Sources/Common/Component/ToastMessageView.swift(1 hunks)Projects/Presentation/Sources/Common/Component/ToastView.swift(1 hunks)Projects/Presentation/Sources/Common/Extension/Notification+.swift(1 hunks)Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift(1 hunks)Projects/Presentation/Sources/EmotionRegister/View/EmotionRegisterView.swift(1 hunks)Projects/Presentation/Sources/Login/View/Component/TotalAgreementButton.swift(1 hunks)Projects/Presentation/Sources/Login/View/TermsAgreementViewController.swift(1 hunks)Projects/Presentation/Sources/MyPage/View/MypageView.swift(1 hunks)Projects/Presentation/Sources/Onboarding/Model/RecommendedRoutine.swift(1 hunks)Projects/Presentation/Sources/Onboarding/View/OnboardingResultViewController.swift(8 hunks)Projects/Presentation/Sources/Onboarding/View/OnboardingViewController.swift(1 hunks)Projects/Presentation/Sources/Onboarding/ViewModel/OnboardingViewModel.swift(6 hunks)Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineViewController.swift(5 hunks)Projects/Presentation/Sources/ResultRecommendedRoutine/View/ResultRecommendedRoutineViewController.swift(6 hunks)Projects/Presentation/Sources/ResultRecommendedRoutine/ViewModel/ResultRecommendedRoutineViewModel.swift(6 hunks)
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-07-16T09:21:15.038Z
Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#19
File: Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift:57-59
Timestamp: 2025-07-16T09:21:15.038Z
Learning: OnboardingRecommendedRoutineView에서 viewWillAppear에 registerOnboarding 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.
Applied to files:
Projects/DataSource/Sources/Repository/OnboardingRepository.swiftProjects/Domain/Sources/Protocol/Repository/OnboardingRepositoryProtocol.swiftProjects/Presentation/Sources/Login/View/TermsAgreementViewController.swiftProjects/Presentation/Sources/Common/PresentationDependencyAssembler.swiftProjects/Presentation/Sources/ResultRecommendedRoutine/View/ResultRecommendedRoutineViewController.swiftProjects/Presentation/Sources/Onboarding/View/OnboardingViewController.swiftProjects/Presentation/Sources/EmotionRegister/View/EmotionRegisterView.swiftProjects/Presentation/Sources/ResultRecommendedRoutine/ViewModel/ResultRecommendedRoutineViewModel.swiftProjects/Presentation/Sources/MyPage/View/MypageView.swiftProjects/Presentation/Sources/Onboarding/View/OnboardingResultViewController.swift
📚 Learning: 2025-08-13T10:59:57.531Z
Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#46
File: Projects/Presentation/Sources/Onboarding/View/OnboardingResultViewController.swift:139-144
Timestamp: 2025-08-13T10:59:57.531Z
Learning: OnboardingResultViewController에서 rectangleImageView가 resultGraphicView와 약간 겹치도록 의도된 레이아웃임. 겹침을 위해서는 inset 대신 offset에 음수 값을 사용하는 것이 더 의미상 적절함.
Applied to files:
Projects/Presentation/Sources/Onboarding/View/OnboardingResultViewController.swift
📚 Learning: 2025-08-17T13:30:29.342Z
Learnt from: taipaise
PR: YAPP-Github/Bitnagil-iOS#51
File: Projects/Presentation/Sources/Common/Component/RoutineCardView.swift:76-80
Timestamp: 2025-08-17T13:30:29.342Z
Learning: In SnapKit, even height constraints (intrinsic constraints) should be applied after adding the view to its superview hierarchy. While simple height constraints might work before adding to superview, it's not guaranteed and goes against best practices. The recommended approach is to call addArrangedSubview first, then apply constraints.
Applied to files:
Projects/Presentation/Sources/Onboarding/View/OnboardingResultViewController.swift
🧬 Code graph analysis (16)
Projects/DataSource/Sources/DTO/OnboardingResponseDTO.swift (1)
Projects/DataSource/Sources/DTO/OnboardingDTO.swift (1)
toOnboardingEntity(16-21)
Projects/Domain/Sources/Protocol/UseCase/ResultRecommendedRoutineUseCaseProtocol.swift (2)
Projects/Presentation/Sources/ResultRecommendedRoutine/ViewModel/ResultRecommendedRoutineViewModel.swift (3)
fetchResultRecommendedRoutines(82-103)fetchResultRecommendedRoutines(106-111)fetchResultRecommendedRoutines(114-119)Projects/Domain/Sources/UseCase/ResultRecommendedRoutine/ResultRecommendedRoutineUseCase.swift (2)
fetchResultRecommendedRoutines(18-21)fetchResultRecommendedRoutines(23-26)
Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineViewController.swift (1)
Projects/Presentation/Sources/Common/Component/ToastView.swift (1)
showToastMessageView(65-74)
Projects/DataSource/Sources/DTO/OnboardingDTO.swift (1)
Projects/DataSource/Sources/DTO/OnboardingResponseDTO.swift (1)
toOnboardingEntity(15-20)
Projects/DataSource/Sources/Repository/OnboardingRepository.swift (4)
Projects/Presentation/Sources/Onboarding/ViewModel/OnboardingViewModel.swift (1)
loadOnboardingResult(176-197)Projects/DataSource/Sources/NetworkService/NetworkService.swift (1)
request(25-48)Projects/DataSource/Sources/DTO/OnboardingDTO.swift (1)
toOnboardingEntity(16-21)Projects/DataSource/Sources/DTO/OnboardingResponseDTO.swift (1)
toOnboardingEntity(15-20)
Projects/Presentation/Sources/Common/Component/ToastView.swift (2)
Projects/Presentation/Sources/Common/Component/ToastMessageView.swift (2)
configureAttribute(38-53)configureLayout(55-85)Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineViewController.swift (2)
configureAttribute(74-111)configureLayout(113-205)
Projects/Domain/Sources/Protocol/Repository/OnboardingRepositoryProtocol.swift (2)
Projects/Presentation/Sources/Onboarding/ViewModel/OnboardingViewModel.swift (1)
loadOnboardingResult(176-197)Projects/DataSource/Sources/Repository/OnboardingRepository.swift (2)
loadOnboardingResult(14-21)registerOnboarding(23-35)
Projects/Presentation/Sources/Login/View/TermsAgreementViewController.swift (1)
Projects/Shared/Sources/DIContainer/DIContainer.swift (1)
resolve(18-25)
Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift (1)
Projects/Shared/Sources/DIContainer/DIContainer.swift (1)
resolve(18-25)
Projects/Presentation/Sources/ResultRecommendedRoutine/View/ResultRecommendedRoutineViewController.swift (1)
Projects/Presentation/Sources/ResultRecommendedRoutine/ViewModel/ResultRecommendedRoutineViewModel.swift (2)
action(56-74)showRecommendedRoutineToastMessageView(161-168)
Projects/DataSource/Sources/Endpoint/OnboardingEndpoint.swift (2)
Projects/Presentation/Sources/Onboarding/ViewModel/OnboardingViewModel.swift (1)
loadOnboardingResult(176-197)Projects/DataSource/Sources/Repository/OnboardingRepository.swift (2)
loadOnboardingResult(14-21)registerOnboarding(23-35)
Projects/Presentation/Sources/ResultRecommendedRoutine/ViewModel/ResultRecommendedRoutineViewModel.swift (1)
Projects/Domain/Sources/UseCase/ResultRecommendedRoutine/ResultRecommendedRoutineUseCase.swift (2)
fetchResultRecommendedRoutines(18-21)fetchResultRecommendedRoutines(23-26)
Projects/Domain/Sources/UseCase/ResultRecommendedRoutine/ResultRecommendedRoutineUseCase.swift (2)
Projects/Presentation/Sources/ResultRecommendedRoutine/ViewModel/ResultRecommendedRoutineViewModel.swift (3)
fetchResultRecommendedRoutines(82-103)fetchResultRecommendedRoutines(106-111)fetchResultRecommendedRoutines(114-119)Projects/DataSource/Sources/Repository/OnboardingRepository.swift (1)
registerOnboarding(23-35)
Projects/Presentation/Sources/MyPage/View/MypageView.swift (1)
Projects/Shared/Sources/DIContainer/DIContainer.swift (1)
resolve(18-25)
Projects/Presentation/Sources/Onboarding/View/OnboardingResultViewController.swift (4)
Projects/Presentation/Sources/ResultRecommendedRoutine/ViewModel/ResultRecommendedRoutineViewModel.swift (2)
action(56-74)configure(77-79)Projects/Presentation/Sources/Onboarding/ViewModel/OnboardingViewModel.swift (3)
action(60-80)makeOnboardingResult(200-215)loadOnboardingResult(176-197)Projects/Presentation/Sources/Common/Extension/UIViewController+.swift (1)
configureCustomNavigationBar(19-29)Projects/Shared/Sources/DIContainer/DIContainer.swift (1)
resolve(18-25)
Projects/Presentation/Sources/Onboarding/ViewModel/OnboardingViewModel.swift (1)
Projects/DataSource/Sources/Repository/OnboardingRepository.swift (1)
loadOnboardingResult(14-21)
🪛 SwiftLint (0.57.0)
Projects/Presentation/Sources/Onboarding/ViewModel/OnboardingViewModel.swift
[Warning] 194-194: TODOs should be resolved (에러 처리)
(todo)
🔇 Additional comments (27)
Projects/Presentation/Sources/Common/Extension/Notification+.swift (1)
10-12: LGTM — 명명·스코프 적절토스트 트리거용 노티피케이션 네임 분리 좋습니다. 현 모듈 내 사용 범위라면 접근제어 현행 유지로 충분합니다.
Projects/Domain/Sources/Protocol/Repository/OnboardingRepositoryProtocol.swift (1)
14-16: registerOnboarding 호출부 모두 새 시그니처(onboardingEntity:)로 업데이트됨
rg 스크립트 실행 결과onboardingChoices파라미터 사용 흔적이 없고, 모든 호출이onboardingEntity:로 올바르게 변경된 것을 확인했습니다.Projects/Domain/Sources/Entity/Enum/OnboardingChoiceType.swift (1)
23-26: fourToMore 레거시 식별자 참조 미발견 — 수동 검증 필요
rg 검색 결과 코드 내에fourToMore참조가 없으나, JSON 디코딩이나 문자열 기반 매핑 등에서 남은 동적 참조 여부를 최종 수동 확인해 주세요.Projects/Presentation/Sources/Onboarding/View/OnboardingResultViewController.swift (4)
15-20: EntryPoint 도입으로 플로우 분기가 명확해졌습니다 — LGTM온보딩/마이페이지(FLOW 이전/결과) 분리 의도가 코드에 잘 드러납니다.
74-80: EntryPoint별 결과 로딩/생성 분기 적절 — LGTM온보딩/마이페이지 결과 흐름 차이가 로직으로 잘 반영되었습니다.
176-181: 의도된 겹침 오프셋 유지 — LGTMrectangleImageView를 음수 offset으로 겹치게 한 구현이 이전 합의사항(override inset 대신 offset)과 일치합니다.
60-64: 검증 완료: 모든 OnboardingResultViewController 호출부에서 entryPoint를 명시적으로 전달
MyPageView.swift(173), OnboardingViewController.swift(243/245)에서 각각.myPagePrevious,.myPageResult,.onboarding을 직접 넘기고 있어 기본값(.onboarding)에 의존하지 않습니다.Projects/Presentation/Sources/Common/Component/ToastMessageView.swift (1)
42-42: 토스트 배경색 그레이 톤으로 변경 — LGTM새 토스트 디자인 가이드와 일관됩니다. 다크모드에서 BitnagilColor.gray30 대비만 한번 확인해 주세요.
Projects/Presentation/Sources/Login/View/Component/TotalAgreementButton.swift (1)
49-49: 체크 아이콘 초기 tint를 gray90으로 통일 — LGTM새 그레이스케일 테마와 잘 맞습니다.
Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift (1)
53-57: fatalError 메시지 오타 수정 (Line 54)
Line 54의 fatalError 메시지를"onboardingRepository 의존성이 등록되지 않았습니다."로 변경하세요. DI 등록부와OnboardingViewModel초기화 시그니처(userDataRepository, onboardingRepository)가 일치합니다.- else { fatalError("userDataRepository 의존성이 등록되지 않았습니다.") } + else { fatalError("onboardingRepository 의존성이 등록되지 않았습니다.") }Projects/Domain/Sources/Entity/RecommendedRoutineEntity.swift (1)
13-23:typeOptional 변경 영향 범위 수동 검증 필요
자동화 스크립트로는.type!강제 언래핑이나 switch 분기 누락을 발견하지 못했습니다. UI 바인딩, switch 분기, 필터링 로직 등에서type이nil일 때의 처리를 수동으로 검증하고, 필요 시 기본값(fallback) 전략을 합의·적용해 주세요.Projects/DataSource/Sources/DTO/RecommendedRoutineDTO.swift (1)
15-16: routineType 파싱 로직 간결화 및 nil 처리 검증 필요
var+if let대신let type = routineType.flatMap(RoutineCategoryType.init(rawValue:))적용 권장- 서버 미문서화 값/오타 수신 시 상위 레이어에서 모니터링/로그 수집 권장
- 호출부·표시부에서
type == nil케이스가 정상 처리되는지 수동 검증 필요Projects/Domain/Sources/Protocol/UseCase/ResultRecommendedRoutineUseCaseProtocol.swift (1)
12-15:onboardingEntity파라미터명 변경 반영 확인
구onboardingChoices호출 흔적이 없으며, 구현체와 호출부 모두onboardingEntity로 올바르게 업데이트되어 있어 추가 조치 불필요합니다.Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineViewController.swift (5)
35-36: 토스트 레이아웃 상수 추가 적절함고정 높이/바텀 간격을 분리해 가독성과 재사용성을 높였습니다.
56-56: 토스트 뷰 주입 방향 OK전역 속성으로 보관해 반복 표시에도 재생성 비용이 없어 성능상 이점이 있습니다.
133-134: 뷰 계층 추가 위치 적절
dimmedView이후에 추가되어 오버레이 위에 자연스럽게 노출됩니다.
199-204: 오토레이아웃 제약 적합세이프에어리어 기준 좌우 마진, 고정 높이, 바텀 인셋 모두 의도에 부합합니다.
234-239: NotificationCenter 연동 안전메인 스레드 수신 + AnyCancellable로 수명 관리가 잘 되어 있습니다.
Projects/DataSource/Sources/DTO/OnboardingResponseDTO.swift (1)
10-21: DTO → 도메인 매핑 적합
timeSlot/emotionTypes/targetOutingFrequency가OnboardingEntity(time/feeling/outdoor)로 일관되게 매핑됩니다.백엔드 응답에서 세 필드가 항상 존재(Non-optional)하는지 확인 부탁드립니다. 누락 가능성이 있으면 디코딩 실패를 막기 위해 옵셔널 처리 또는 기본값 방어 로직이 필요합니다.
Projects/Domain/Sources/UseCase/ResultRecommendedRoutine/ResultRecommendedRoutineUseCase.swift (1)
18-20: 도메인 모델 중심의 유스케이스로 개선 👍
OnboardingEntity를 직접 받아 Repository로 위임하는 구조가 단순·명확합니다.Projects/Presentation/Sources/ResultRecommendedRoutine/View/ResultRecommendedRoutineViewController.swift (1)
330-347: 마이페이지 토스트 노출 타이밍 확인 요청rootViewController 전환/탭 전환 이후 토스트 노티를 발행합니다. 수신자가 이미 등록된 상태인지(특히 초기 탭 VC의 viewDidLoad 시점) 확인이 필요합니다. 레이스가 의심되면 post 시점을 약간 지연하거나(현재 0.1s) 수신자 쪽에서 replay 처리 검토가 필요합니다.
Projects/DataSource/Sources/Endpoint/OnboardingEndpoint.swift (1)
16-25: URL 구성 로직 확인 필요: baseURL과 path 중복 결합 여부 점검 후 역할 분리 권장
NetworkService에서endpoint.baseURL과endpoint.path를 어떻게 조합해urlString을 생성하는지 확인하세요. 중복 결합이 발생할 경우,baseURL은 호스트만,path는 리소스 경로만 반환하도록 분리해야 합니다.Projects/Presentation/Sources/Login/View/TermsAgreementViewController.swift (1)
124-128: IntroViewModel DI 등록이 확인되었습니다 — fatalError 유지
PresentationDependencyAssembler.swift에서IntroViewModel이 정상 등록되고 있기 때문에(라인 103) 런타임 의존성 누락은 개발자 설정 오류로 보고 즉시 추적 가능한fatalError가 적절합니다.Likely an incorrect or invalid review comment.
Projects/Presentation/Sources/ResultRecommendedRoutine/ViewModel/ResultRecommendedRoutineViewModel.swift (1)
15-17: 온보딩/마이페이지 enum 호출부 시그니처 일괄 확인 완료
ResultRecommendedRoutineViewModelType.onboarding및.mypage호출부가 Projects 내에서 검색되지 않아(스크립트 결과 0건), 모두 새OnboardingEntity기반 시그니처로 마이그레이션된 상태입니다. 추가 수정 불필요합니다.Projects/DataSource/Sources/Repository/OnboardingRepository.swift (1)
14-21: v2 응답 → Domain 매핑 경로 명확하고 단순 — LGTM성공/실패 분기와 매핑이 명확합니다. 에러 케이스도 도메인 에러로 변환되어 호출 측 처리에 용이합니다.
Projects/Presentation/Sources/Onboarding/ViewModel/OnboardingViewModel.swift (2)
227-234: OnboardingEntity 구성 로직 명확 — LGTM선택값 검증 후 Entity로 일관되게 합성하는 흐름이 좋습니다.
29-41: 구독부 및 DI 등록 정상 확인 완료 구독부(예: OnboardingResultViewController의 onboardingChoicesPublisher sink)와 OnboardingRepositoryProtocol 등록·해상이 DataSource→Domain→Presentation 전반에 걸쳐 올바르게 적용되었습니다.
| private func configureAttribute() { | ||
| alpha = 0 | ||
| isHidden = true | ||
|
|
||
| layer.masksToBounds = true | ||
| layer.cornerRadius = 12 | ||
| backgroundColor = BitnagilColor.gray30 | ||
|
|
||
| checkIcon.image = BitnagilIcon.orangeCheckedCircleIcon | ||
|
|
||
| messageLabel.text = message | ||
| messageLabel.font = BitnagilFont(style: .body2, weight: .medium).font | ||
| messageLabel.textColor = .white | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
토스트가 터치 이벤트를 가로채는 문제 가능성
UIView 기본값이 isUserInteractionEnabled = true라 표시 중 하단 UI 터치가 차단될 수 있습니다. 토스트는 안내용이므로 상호작용 비활성화를 권장합니다. 아이콘 왜곡 방지를 위해 contentMode도 지정하세요.
아래 패치 제안:
private func configureAttribute() {
alpha = 0
isHidden = true
+ isUserInteractionEnabled = false
layer.masksToBounds = true
layer.cornerRadius = 12
backgroundColor = BitnagilColor.gray30
- checkIcon.image = BitnagilIcon.orangeCheckedCircleIcon
+ checkIcon.image = BitnagilIcon.orangeCheckedCircleIcon
+ checkIcon.contentMode = .scaleAspectFit
messageLabel.text = message
messageLabel.font = BitnagilFont(style: .body2, weight: .medium).font
messageLabel.textColor = .white
+ messageLabel.numberOfLines = 1
+ messageLabel.lineBreakMode = .byTruncatingTail
}📝 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.
| private func configureAttribute() { | |
| alpha = 0 | |
| isHidden = true | |
| layer.masksToBounds = true | |
| layer.cornerRadius = 12 | |
| backgroundColor = BitnagilColor.gray30 | |
| checkIcon.image = BitnagilIcon.orangeCheckedCircleIcon | |
| messageLabel.text = message | |
| messageLabel.font = BitnagilFont(style: .body2, weight: .medium).font | |
| messageLabel.textColor = .white | |
| } | |
| private func configureAttribute() { | |
| alpha = 0 | |
| isHidden = true | |
| isUserInteractionEnabled = false | |
| layer.masksToBounds = true | |
| layer.cornerRadius = 12 | |
| backgroundColor = BitnagilColor.gray30 | |
| checkIcon.image = BitnagilIcon.orangeCheckedCircleIcon | |
| checkIcon.contentMode = .scaleAspectFit | |
| messageLabel.text = message | |
| messageLabel.font = BitnagilFont(style: .body2, weight: .medium).font | |
| messageLabel.textColor = .white | |
| messageLabel.numberOfLines = 1 | |
| messageLabel.lineBreakMode = .byTruncatingTail | |
| } |
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Common/Component/ToastView.swift around lines
33 to 46, the toast view currently leaves user interaction enabled which can
block touches to underlying UI; disable interaction on the toast by setting
isUserInteractionEnabled = false (and also disable interaction on subviews if
needed) and set checkIcon.contentMode = .scaleAspectFit to prevent icon
distortion; update configureAttribute() to apply these property changes so the
toast is non-interactive and the icon preserves its aspect ratio.
| guard let onboardingViewModel = DIContainer.shared.resolve(type: OnboardingViewModel.self) | ||
| else { fatalError("onboardingViewModel 의존성이 등록되지 않았습니다.") } | ||
|
|
||
| let onboardingView = OnboardingViewController( | ||
| viewModel: onboardingViewModel, | ||
| onboarding: .time, | ||
| isFromMypage: true) | ||
| let onboardingView = OnboardingResultViewController(viewModel: onboardingViewModel, entryPoint: .myPagePrevious) | ||
| onboardingView.hidesBottomBarWhenPushed = true | ||
| navigationController?.pushViewController(onboardingView, animated: true) | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
DI 실패 시 fatalError 제거 (프로덕션 크래시 위험)
설정 화면에서는 안전하게 빠져나가는데, 여기서는 fatalError로 크래시합니다. 일관되게 안전 처리해 주세요.
- guard let onboardingViewModel = DIContainer.shared.resolve(type: OnboardingViewModel.self)
- else { fatalError("onboardingViewModel 의존성이 등록되지 않았습니다.") }
+ guard let onboardingViewModel = DIContainer.shared.resolve(type: OnboardingViewModel.self) else {
+ BitnagilLogger.log(logType: .error, message: "OnboardingViewModel Resolve Fail: 의존성이 등록되지 않았습니다.")
+ assertionFailure("OnboardingViewModel not registered")
+ return
+ }📝 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.
| guard let onboardingViewModel = DIContainer.shared.resolve(type: OnboardingViewModel.self) | |
| else { fatalError("onboardingViewModel 의존성이 등록되지 않았습니다.") } | |
| let onboardingView = OnboardingViewController( | |
| viewModel: onboardingViewModel, | |
| onboarding: .time, | |
| isFromMypage: true) | |
| let onboardingView = OnboardingResultViewController(viewModel: onboardingViewModel, entryPoint: .myPagePrevious) | |
| onboardingView.hidesBottomBarWhenPushed = true | |
| navigationController?.pushViewController(onboardingView, animated: true) | |
| } | |
| guard let onboardingViewModel = DIContainer.shared.resolve(type: OnboardingViewModel.self) else { | |
| BitnagilLogger.log(logType: .error, message: "OnboardingViewModel Resolve Fail: 의존성이 등록되지 않았습니다.") | |
| assertionFailure("OnboardingViewModel not registered") | |
| return | |
| } | |
| let onboardingView = OnboardingResultViewController(viewModel: onboardingViewModel, entryPoint: .myPagePrevious) | |
| onboardingView.hidesBottomBarWhenPushed = true | |
| navigationController?.pushViewController(onboardingView, animated: true) | |
| } |
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/MyPage/View/MypageView.swift around lines
170–176, remove the fatalError on DI resolve failure and handle the missing
OnboardingViewModel safely: attempt to resolve, and if nil log the error (or
send to analytics), present a user-friendly alert (or toast) indicating the
action cannot be completed, and simply return or pop to the previous screen
instead of crashing; only create and push OnboardingResultViewController when
the viewModel is non-nil (set hidesBottomBarWhenPushed after creation).
There was a problem hiding this comment.
- 어거지일 수도 있지만, 아래와 같은 구조도 가능은.. 할거 같습니다!
struct OnboardingDTO: Codable {
let timeSlot: String
let realOutingFrequency: String?
let targetOutingFrequency: String
var emotionTypes: [String]? {
return emotionType1 == nil ? emotionType2 : emotionType1
}
private let emotionType1: [String]?
private let emotionType2: [String]?
init(
timeSlot: String,
realOutingFrequency: String?,
targetOutingFrequency: String,
emotionTypes: [String]?
) {
self.timeSlot = timeSlot
self.realOutingFrequency = realOutingFrequency
self.targetOutingFrequency = targetOutingFrequency
emotionType1 = emotionTypes
emotionType2 = nil
}
private enum CodingKeys: String, CodingKey {
case timeSlot
case emotionType1 = "emotionTypes"
case emotionType2 = "emotionType"
case realOutingFrequency
case targetOutingFrequency
}
}코딩키를 커스텀 해서 일단 응답을 받고, 둘 중에 nil이 아닌 값을 emotionTypes으로 사용하는 방법을 생각해보았습니다
외부에서는 emotionTypes가 계산 속성이기 때문에 let과 같이 변경 불가합니다!
물론 현상유지해서 파일 두 개 유지하는 것도 괜찮다고 생각합니다!
- 코드를 수정하거나 삭제하는 것은 불가피하고, 너무나 당연한 일이라고 생각합니다!! 수정해서 더 좋은 구조를 갖게 되거나 사용성이 올라가거나, 요구사항에 맞으면 오히려 좋아입니다!! 노 죄송!!!!
수정할 코드가 있는데, 당장 제가 하는 작업에 영향이 있다면 말씀만 살짝 해주시면 좋을거 같아요!!! (지금은 없지만요) 그렇지 않다면 수정 이후에 pr에서 확인하는 것도 좋습니다! 저도 조이가 작업하신 코드 중에 수정할 내용이 있으면 그렇게 하겠습니다~!!!
이번 PR도 완고입니다~!~! 😇😇
770d6de to
78fe2e5
Compare
There was a problem hiding this comment.
Actionable comments posted: 3
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
Projects/Presentation/Sources/Onboarding/View/OnboardingViewController.swift (1)
132-139: mainLabel 고정 높이로 인한 텍스트 클리핑 위험Dynamic Type/다국어 대응을 위해 고정 height → 최소 height로 완화하세요.
패치:
mainLabel.snp.makeConstraints { make in make.leading.equalTo(safeArea).offset(Layout.horizontalMargin) make.trailing.equalTo(safeArea).inset(Layout.horizontalMargin) mainLabelTopConstraint = make.top.equalTo(safeArea).offset(Layout.mainLabelMaxTopSpacing).constraint - make.height.equalTo(onboarding == .feeling ? Layout.mainLabelMinHeight : Layout.mainLabelHeight) + make.height.greaterThanOrEqualTo(onboarding == .feeling ? Layout.mainLabelMinHeight : Layout.mainLabelHeight) }추가(선택):
mainLabel.adjustsFontForContentSizeCategory = true
♻️ Duplicate comments (1)
Projects/Presentation/Sources/ResultRecommendedRoutine/View/ResultRecommendedRoutineViewController.swift (1)
42-43: 사용자 문구 띄어쓰기 수정 반영 확인"맞춤 추천 루틴 보러 가기"로 정상 반영되어 있습니다. 이전 코멘트와 일치합니다.
🧹 Nitpick comments (15)
Projects/Presentation/Sources/ResultRecommendedRoutine/View/ResultRecommendedRoutineViewController.swift (6)
112-120: 상단 간격 1회만 계산—회전/환경 변화 시 레이아웃 불일치 가능isLayoutConfigured로 최초 1회만 갱신하면 회전·Split View·Status Bar 변화 시 상단 간격이 부정확할 수 있습니다.
대안:
- viewSafeAreaInsetsDidChange 또는 viewWillTransition(to:)에서 updateMainLabelTopSpacing() 재호출
- isLayoutConfigured 제거 후 idempotent 업데이트 유지
Also applies to: 121-126
128-136: 네비게이션 바 설정 위치 일관성본 파일은 configureAttribute에서 배경/네비 설정을 수행하지만, OnboardingViewController는 configureLayout에서 처리합니다. 컨벤션을 통일해 추후 유지보수 비용을 줄이는 것을 권장합니다.
339-348: 토스트 노출 타이밍 안정화(탭 전환/Pop 동기화)탭 전환·pop 직후 0.1초 지연은 취약합니다. 수신자 화면의 viewDidAppear에서 토스트를 트리거하거나, TabBar/Coordinator에서 이벤트를 받아 노출시키는 구조로 이동을 권장합니다(뷰모델에서 NotificationCenter 직접 post 지양).
간단 대안:
- TabBarView 또는 대상 탭 루트 VC에서 .showRecommendedRoutineToast 구독을 담당하고, viewDidAppear에서 큐잉된 이벤트만 처리
20-45: 하드코딩된 사용자 문구의 지역화 필요노출 문자열은 Localizable 리소스로 분리해 주세요(예: "당신만의 추천 루틴이 생성되었어요!", "건너뛰기" 등).
Also applies to: 154-161
65-72: Layout 상수 네이밍/의도 정합성mainLabelHeight, subLabelHeight가 고정 높이처럼 보이지만 최소 높이로 쓰일 예정이라면 의미를 드러내도록 mainLabelMinHeight, subLabelMinHeight 등으로 변경을 검토해 주세요.
315-324: Skip 버튼의 인덱스 기반 pop 취약성—타입 기반 pop으로 전환 권장
count - 3에 의존하면 네비게이션 경로가 바뀔 때 쉽게 깨집니다. 뷰 컨트롤러 스택에서 직접 원하는 타입을 찾아 pop 하세요.예시 패치:
- if viewControllers.count >= 3 { - navigationController.popToViewController(viewControllers[viewControllers.count - 3], animated: false) - } + if let target = viewControllers.first(where: { $0 is EmotionRegisterView }) { + navigationController.popToViewController(target, animated: false) + }타입명을 확인하려면 아래 스크립트를 사용하세요:
#!/bin/bash rg -nP -C2 --type=swift 'class\s+EmotionRegisterView\b'Projects/Presentation/Sources/Onboarding/View/OnboardingViewController.swift (3)
115-120: UIAction 클로저에서 self 약한 캡처로 일관성 유지다른 곳은 [weak self]를 사용하지만 여기만 강한 캡처입니다. 일관성 및 잠재적 순환참조 예방 차원에서 weak 사용을 권장합니다.
패치:
- nextButton.addAction( - UIAction { _ in - self.goNextStep() - }, - for: .touchUpInside) + nextButton.addAction( + UIAction { [weak self] _ in + self?.goNextStep() + }, + for: .touchUpInside)
61-68: 상단 간격 1회만 계산—환경 변화 대응 보완Result 화면과 동일하게, 회전/윈도우 크기 변경 시 간격 재계산이 필요할 수 있습니다.
대안:
- viewSafeAreaInsetsDidChange에서 updateMainLabelTopSpacing() 호출
Also applies to: 70-74
124-127: 네비게이션/배경 설정 컨벤션 통일 제안본 파일은 configureLayout에서 네비 설정, Result 화면은 configureAttribute에서 수행합니다. 공통 컨벤션을 정해 통일하는 것이 좋습니다.
Projects/Domain/Sources/Protocol/Repository/OnboardingRepositoryProtocol.swift (3)
14-16: 엔터티 기반 등록 시그니처로의 전환 👍 + 예외 전파 일관성 확인OnboardingEntity 기반은 명확합니다. 다만 DataSource 구현이 응답 nil 시 빈 배열([])을 반환하는 것으로 보입니다(참고: Repository 구현 스니펫). 실패는 throw로 올리는 쪽이 상위 레이어 처리에 일관적입니다.
권장 패치(Repository 구현 측):
guard let response = try await networkService.request(endpoint: endpoint, type: RecommendedRoutineListResponseDTO.self) else { throw UserError.onboardingRegisterFailed }
13-16: 문서화 오탈자(띄어쓰기) 정정"추천루틴" → "추천 루틴"으로 통일해 주세요.
패치:
-/// - Returns: 온보딩 결과를 바탕으로 받은 추천루틴 목록 +/// - Returns: 온보딩 결과를 바탕으로 받은 추천 루틴 목록
18-21: 파라미터 명확성 개선 제안selectedRoutines는 의미가 넓습니다. recommendedRoutineIds 등으로 구체화하면 호출부 가독성이 좋아집니다(바이너리 호환에 영향 없음).
Projects/Presentation/Sources/Onboarding/ViewModel/OnboardingViewModel.swift (1)
181-182:feeling배열의 compactMap 처리가 중복되어 있습니다.Line 181에서
compactMap으로 nil을 필터링한 후, Line 182에서 다시compactMap을 사용하고 있습니다. 한 번의 compactMap으로 최적화할 수 있습니다.-let feelingOnboardingChoices = onboardingEntity.feeling.compactMap({ OnboardingChoiceType(rawValue: $0 )}) -let feelingResult = feelingOnboardingChoices.compactMap({ $0.resultTitle }).joined(separator: ", ") +let feelingResult = onboardingEntity.feeling + .compactMap { OnboardingChoiceType(rawValue: $0)?.resultTitle } + .joined(separator: ", ")Projects/Presentation/Sources/Onboarding/View/OnboardingResultViewController.swift (2)
15-19:EntryPointenum에 대한 문서화가 필요합니다.각 케이스가 어떤 상황을 나타내는지 명확하지 않습니다. 특히
myPagePrevious와myPageResult의 차이점이 불분명합니다.enum EntryPoint { + /// 온보딩 플로우에서 진입 case onboarding + /// 마이페이지에서 이전 목표 확인 후 변경 case myPagePrevious + /// 마이페이지에서 목표 재설정 결과 확인 case myPageResult }
278-278: 의존성 주입 실패 시 fatalError 대신 더 안전한 에러 처리를 고려해보세요.프로덕션 환경에서 크래시를 방지하기 위해 옵셔널 체이닝이나 에러 로깅 후 graceful한 처리를 권장합니다.
-guard let resultRecommendedRoutineViewModel = DIContainer.shared.resolve(type: ResultRecommendedRoutineViewModel.self) -else { fatalError("resultRecommendedRoutineViewModel 의존성이 등록되지 않았습니다.") } +guard let resultRecommendedRoutineViewModel = DIContainer.shared.resolve(type: ResultRecommendedRoutineViewModel.self) else { + BitnagilLogger.log(logType: .error, message: "resultRecommendedRoutineViewModel 의존성이 등록되지 않았습니다.") + // 사용자에게 에러 토스트 표시 또는 이전 화면으로 복귀 + return +}Also applies to: 288-288
📜 Review details
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
📒 Files selected for processing (26)
Projects/DataSource/Sources/DTO/OnboardingDTO.swift(1 hunks)Projects/DataSource/Sources/DTO/OnboardingResponseDTO.swift(1 hunks)Projects/DataSource/Sources/DTO/RecommendedRoutineDTO.swift(3 hunks)Projects/DataSource/Sources/Endpoint/OnboardingEndpoint.swift(2 hunks)Projects/DataSource/Sources/Repository/OnboardingRepository.swift(1 hunks)Projects/Domain/Sources/Entity/Enum/OnboardingChoiceType.swift(1 hunks)Projects/Domain/Sources/Entity/OnboardingEntity.swift(1 hunks)Projects/Domain/Sources/Entity/RecommendedRoutineEntity.swift(2 hunks)Projects/Domain/Sources/Protocol/Repository/OnboardingRepositoryProtocol.swift(1 hunks)Projects/Domain/Sources/Protocol/UseCase/ResultRecommendedRoutineUseCaseProtocol.swift(1 hunks)Projects/Domain/Sources/UseCase/ResultRecommendedRoutine/ResultRecommendedRoutineUseCase.swift(1 hunks)Projects/Presentation/Sources/Common/Component/ToastMessageView.swift(1 hunks)Projects/Presentation/Sources/Common/Component/ToastView.swift(1 hunks)Projects/Presentation/Sources/Common/Extension/Notification+.swift(1 hunks)Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift(1 hunks)Projects/Presentation/Sources/EmotionRegister/View/EmotionRegisterView.swift(1 hunks)Projects/Presentation/Sources/Login/View/Component/TotalAgreementButton.swift(1 hunks)Projects/Presentation/Sources/Login/View/TermsAgreementViewController.swift(1 hunks)Projects/Presentation/Sources/MyPage/View/MypageView.swift(1 hunks)Projects/Presentation/Sources/Onboarding/Model/RecommendedRoutine.swift(1 hunks)Projects/Presentation/Sources/Onboarding/View/OnboardingResultViewController.swift(8 hunks)Projects/Presentation/Sources/Onboarding/View/OnboardingViewController.swift(1 hunks)Projects/Presentation/Sources/Onboarding/ViewModel/OnboardingViewModel.swift(6 hunks)Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineViewController.swift(5 hunks)Projects/Presentation/Sources/ResultRecommendedRoutine/View/ResultRecommendedRoutineViewController.swift(7 hunks)Projects/Presentation/Sources/ResultRecommendedRoutine/ViewModel/ResultRecommendedRoutineViewModel.swift(6 hunks)
🚧 Files skipped from review as they are similar to previous changes (20)
- Projects/Domain/Sources/Entity/OnboardingEntity.swift
- Projects/Presentation/Sources/Common/Component/ToastMessageView.swift
- Projects/DataSource/Sources/DTO/OnboardingResponseDTO.swift
- Projects/DataSource/Sources/DTO/OnboardingDTO.swift
- Projects/Presentation/Sources/Login/View/Component/TotalAgreementButton.swift
- Projects/Presentation/Sources/Login/View/TermsAgreementViewController.swift
- Projects/Presentation/Sources/Common/Extension/Notification+.swift
- Projects/Presentation/Sources/ResultRecommendedRoutine/ViewModel/ResultRecommendedRoutineViewModel.swift
- Projects/Presentation/Sources/Common/Component/ToastView.swift
- Projects/DataSource/Sources/DTO/RecommendedRoutineDTO.swift
- Projects/Presentation/Sources/EmotionRegister/View/EmotionRegisterView.swift
- Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineViewController.swift
- Projects/Domain/Sources/Protocol/UseCase/ResultRecommendedRoutineUseCaseProtocol.swift
- Projects/Presentation/Sources/Onboarding/Model/RecommendedRoutine.swift
- Projects/Domain/Sources/Entity/Enum/OnboardingChoiceType.swift
- Projects/DataSource/Sources/Endpoint/OnboardingEndpoint.swift
- Projects/Domain/Sources/UseCase/ResultRecommendedRoutine/ResultRecommendedRoutineUseCase.swift
- Projects/Presentation/Sources/MyPage/View/MypageView.swift
- Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift
- Projects/Domain/Sources/Entity/RecommendedRoutineEntity.swift
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-07-16T09:21:15.038Z
Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#19
File: Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift:57-59
Timestamp: 2025-07-16T09:21:15.038Z
Learning: OnboardingRecommendedRoutineView에서 viewWillAppear에 registerOnboarding 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.
Applied to files:
Projects/Domain/Sources/Protocol/Repository/OnboardingRepositoryProtocol.swiftProjects/DataSource/Sources/Repository/OnboardingRepository.swiftProjects/Presentation/Sources/Onboarding/View/OnboardingViewController.swiftProjects/Presentation/Sources/ResultRecommendedRoutine/View/ResultRecommendedRoutineViewController.swiftProjects/Presentation/Sources/Onboarding/View/OnboardingResultViewController.swift
📚 Learning: 2025-08-17T13:30:29.342Z
Learnt from: taipaise
PR: YAPP-Github/Bitnagil-iOS#51
File: Projects/Presentation/Sources/Common/Component/RoutineCardView.swift:76-80
Timestamp: 2025-08-17T13:30:29.342Z
Learning: In SnapKit, even height constraints (intrinsic constraints) should be applied after adding the view to its superview hierarchy. While simple height constraints might work before adding to superview, it's not guaranteed and goes against best practices. The recommended approach is to call addArrangedSubview first, then apply constraints.
Applied to files:
Projects/Presentation/Sources/ResultRecommendedRoutine/View/ResultRecommendedRoutineViewController.swiftProjects/Presentation/Sources/Onboarding/View/OnboardingResultViewController.swift
📚 Learning: 2025-08-13T10:59:57.531Z
Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#46
File: Projects/Presentation/Sources/Onboarding/View/OnboardingResultViewController.swift:139-144
Timestamp: 2025-08-13T10:59:57.531Z
Learning: OnboardingResultViewController에서 rectangleImageView가 resultGraphicView와 약간 겹치도록 의도된 레이아웃임. 겹침을 위해서는 inset 대신 offset에 음수 값을 사용하는 것이 더 의미상 적절함.
Applied to files:
Projects/Presentation/Sources/Onboarding/View/OnboardingResultViewController.swift
🧬 Code graph analysis (5)
Projects/Domain/Sources/Protocol/Repository/OnboardingRepositoryProtocol.swift (2)
Projects/Presentation/Sources/Onboarding/ViewModel/OnboardingViewModel.swift (1)
loadOnboardingResult(176-197)Projects/DataSource/Sources/Repository/OnboardingRepository.swift (2)
loadOnboardingResult(14-21)registerOnboarding(23-35)
Projects/DataSource/Sources/Repository/OnboardingRepository.swift (3)
Projects/Presentation/Sources/Onboarding/ViewModel/OnboardingViewModel.swift (1)
loadOnboardingResult(176-197)Projects/DataSource/Sources/NetworkService/NetworkService.swift (1)
request(25-48)Projects/DataSource/Sources/DTO/OnboardingResponseDTO.swift (1)
toOnboardingEntity(15-20)
Projects/Presentation/Sources/Onboarding/ViewModel/OnboardingViewModel.swift (1)
Projects/DataSource/Sources/Repository/OnboardingRepository.swift (1)
loadOnboardingResult(14-21)
Projects/Presentation/Sources/ResultRecommendedRoutine/View/ResultRecommendedRoutineViewController.swift (2)
Projects/Presentation/Sources/Common/Extension/UIViewController+.swift (1)
configureCustomNavigationBar(19-29)Projects/Presentation/Sources/ResultRecommendedRoutine/ViewModel/ResultRecommendedRoutineViewModel.swift (2)
action(56-74)showRecommendedRoutineToastMessageView(161-168)
Projects/Presentation/Sources/Onboarding/View/OnboardingResultViewController.swift (5)
Projects/Presentation/Sources/ResultRecommendedRoutine/ViewModel/ResultRecommendedRoutineViewModel.swift (2)
action(56-74)configure(77-79)Projects/Presentation/Sources/Onboarding/ViewModel/OnboardingViewModel.swift (3)
action(60-80)makeOnboardingResult(200-215)loadOnboardingResult(176-197)Projects/Presentation/Sources/ResultRecommendedRoutine/View/ResultRecommendedRoutineViewController.swift (1)
configureLayout(166-212)Projects/Presentation/Sources/Common/Extension/UIViewController+.swift (1)
configureCustomNavigationBar(19-29)Projects/Shared/Sources/DIContainer/DIContainer.swift (1)
resolve(18-25)
🪛 SwiftLint (0.57.0)
Projects/Presentation/Sources/Onboarding/ViewModel/OnboardingViewModel.swift
[Warning] 194-194: TODOs should be resolved (에러 처리)
(todo)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (4)
Projects/Presentation/Sources/Onboarding/View/OnboardingViewController.swift (1)
242-246: 검증 완료:OnboardingResultViewController의init(viewModel:entryPoint:)시그니처가EntryPointenum의.onboarding및.myPageResult케이스를 모두 지원하므로 호출이 유효합니다.Projects/Domain/Sources/Protocol/Repository/OnboardingRepositoryProtocol.swift (1)
9-12: loadOnboardingResult 도입 적절도메인에서 과거 온보딩 결과를 직접 조회하도록 분리한 점 좋습니다. UseCase 구성 간결해집니다.
Projects/DataSource/Sources/Repository/OnboardingRepository.swift (2)
14-21: 변경 사항이 잘 구현되었습니다!새로운
loadOnboardingResult()메서드가 적절한 에러 처리와 함께 깔끔하게 구현되었습니다.
23-35: DTO 변환 로직이 명확하게 구현되었습니다.
OnboardingEntity를OnboardingDTO로 변환하는 과정이 명시적이고 이해하기 쉽게 작성되었습니다.
| private func goToResultRecommendedRoutineView(onboardingEntity: OnboardingEntity) { | ||
| var nextView: UIViewController? | ||
| switch entryPoint { | ||
| case .onboarding: | ||
| guard let resultRecommendedRoutineViewModel = DIContainer.shared.resolve(type: ResultRecommendedRoutineViewModel.self) | ||
| else { fatalError("resultRecommendedRoutineViewModel 의존성이 등록되지 않았습니다.") } | ||
|
|
||
| resultRecommendedRoutineViewModel.configure(viewModelType: .onboarding(onboardingEntity: onboardingEntity)) | ||
| nextView = ResultRecommendedRoutineViewController(entryPoint: .onboarding, viewModel: resultRecommendedRoutineViewModel) | ||
|
|
||
| var resultRecommendedView: ResultRecommendedRoutineView | ||
| if isFromMypage { | ||
| resultRecommendedRoutineViewModel.configure(viewModelType: .mypage(onboardingChoices: onboardingChoices)) | ||
| resultRecommendedView = ResultRecommendedRoutineView(entryPoint: .mypage, viewModel: resultRecommendedRoutineViewModel) | ||
| } else { | ||
| resultRecommendedRoutineViewModel.configure(viewModelType: .onboarding(onboardingChoices: onboardingChoices)) | ||
| resultRecommendedView = ResultRecommendedRoutineView(entryPoint: .onboarding, viewModel: resultRecommendedRoutineViewModel) | ||
| case .myPagePrevious: | ||
| break | ||
|
|
||
| case .myPageResult: | ||
| guard let resultRecommendedRoutineViewModel = DIContainer.shared.resolve(type: ResultRecommendedRoutineViewModel.self) | ||
| else{ fatalError("resultRecommendedRoutineViewModel 의존성이 등록되지 않았습니다.") } | ||
|
|
||
| resultRecommendedRoutineViewModel.configure(viewModelType: .mypage(onboardingEntity: onboardingEntity)) | ||
| nextView = ResultRecommendedRoutineViewController(entryPoint: .mypage, viewModel: resultRecommendedRoutineViewModel) | ||
| } | ||
| self.navigationController?.pushViewController(resultRecommendedView, animated: true) | ||
|
|
||
| guard let nextView else { return } | ||
| self.navigationController?.pushViewController(nextView, animated: true) | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
goToResultRecommendedRoutineView 메서드의 중복 코드를 제거해주세요.
onboarding과 myPageResult 케이스에서 거의 동일한 로직이 반복되고 있습니다.
private func goToResultRecommendedRoutineView(onboardingEntity: OnboardingEntity) {
- var nextView: UIViewController?
- switch entryPoint {
- case .onboarding:
- guard let resultRecommendedRoutineViewModel = DIContainer.shared.resolve(type: ResultRecommendedRoutineViewModel.self)
- else { fatalError("resultRecommendedRoutineViewModel 의존성이 등록되지 않았습니다.") }
-
- resultRecommendedRoutineViewModel.configure(viewModelType: .onboarding(onboardingEntity: onboardingEntity))
- nextView = ResultRecommendedRoutineViewController(entryPoint: .onboarding, viewModel: resultRecommendedRoutineViewModel)
-
- case .myPagePrevious:
- break
-
- case .myPageResult:
- guard let resultRecommendedRoutineViewModel = DIContainer.shared.resolve(type: ResultRecommendedRoutineViewModel.self)
- else{ fatalError("resultRecommendedRoutineViewModel 의존성이 등록되지 않았습니다.") }
-
- resultRecommendedRoutineViewModel.configure(viewModelType: .mypage(onboardingEntity: onboardingEntity))
- nextView = ResultRecommendedRoutineViewController(entryPoint: .mypage, viewModel: resultRecommendedRoutineViewModel)
- }
-
- guard let nextView else { return }
- self.navigationController?.pushViewController(nextView, animated: true)
+ guard entryPoint != .myPagePrevious else { return }
+
+ guard let resultRecommendedRoutineViewModel = DIContainer.shared.resolve(type: ResultRecommendedRoutineViewModel.self)
+ else { fatalError("resultRecommendedRoutineViewModel 의존성이 등록되지 않았습니다.") }
+
+ let viewModelType: ResultRecommendedRoutineViewModelType
+ let nextEntryPoint: ResultRecommendedRoutineViewController.EntryPoint
+
+ switch entryPoint {
+ case .onboarding:
+ viewModelType = .onboarding(onboardingEntity: onboardingEntity)
+ nextEntryPoint = .onboarding
+ case .myPageResult:
+ viewModelType = .mypage(onboardingEntity: onboardingEntity)
+ nextEntryPoint = .mypage
+ case .myPagePrevious:
+ return // 이미 위에서 처리됨
+ }
+
+ resultRecommendedRoutineViewModel.configure(viewModelType: viewModelType)
+ let nextView = ResultRecommendedRoutineViewController(entryPoint: nextEntryPoint, viewModel: resultRecommendedRoutineViewModel)
+ self.navigationController?.pushViewController(nextView, animated: true)
}📝 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.
| private func goToResultRecommendedRoutineView(onboardingEntity: OnboardingEntity) { | |
| var nextView: UIViewController? | |
| switch entryPoint { | |
| case .onboarding: | |
| guard let resultRecommendedRoutineViewModel = DIContainer.shared.resolve(type: ResultRecommendedRoutineViewModel.self) | |
| else { fatalError("resultRecommendedRoutineViewModel 의존성이 등록되지 않았습니다.") } | |
| resultRecommendedRoutineViewModel.configure(viewModelType: .onboarding(onboardingEntity: onboardingEntity)) | |
| nextView = ResultRecommendedRoutineViewController(entryPoint: .onboarding, viewModel: resultRecommendedRoutineViewModel) | |
| var resultRecommendedView: ResultRecommendedRoutineView | |
| if isFromMypage { | |
| resultRecommendedRoutineViewModel.configure(viewModelType: .mypage(onboardingChoices: onboardingChoices)) | |
| resultRecommendedView = ResultRecommendedRoutineView(entryPoint: .mypage, viewModel: resultRecommendedRoutineViewModel) | |
| } else { | |
| resultRecommendedRoutineViewModel.configure(viewModelType: .onboarding(onboardingChoices: onboardingChoices)) | |
| resultRecommendedView = ResultRecommendedRoutineView(entryPoint: .onboarding, viewModel: resultRecommendedRoutineViewModel) | |
| case .myPagePrevious: | |
| break | |
| case .myPageResult: | |
| guard let resultRecommendedRoutineViewModel = DIContainer.shared.resolve(type: ResultRecommendedRoutineViewModel.self) | |
| else{ fatalError("resultRecommendedRoutineViewModel 의존성이 등록되지 않았습니다.") } | |
| resultRecommendedRoutineViewModel.configure(viewModelType: .mypage(onboardingEntity: onboardingEntity)) | |
| nextView = ResultRecommendedRoutineViewController(entryPoint: .mypage, viewModel: resultRecommendedRoutineViewModel) | |
| } | |
| self.navigationController?.pushViewController(resultRecommendedView, animated: true) | |
| guard let nextView else { return } | |
| self.navigationController?.pushViewController(nextView, animated: true) | |
| } | |
| private func goToResultRecommendedRoutineView(onboardingEntity: OnboardingEntity) { | |
| // Early-exit if coming from the “previous” MyPage entry point | |
| guard entryPoint != .myPagePrevious else { return } | |
| // Resolve the shared view model dependency once | |
| guard let resultRecommendedRoutineViewModel = DIContainer.shared.resolve(type: ResultRecommendedRoutineViewModel.self) | |
| else { fatalError("resultRecommendedRoutineViewModel 의존성이 등록되지 않았습니다.") } | |
| // Determine the per-entryPoint configuration | |
| let viewModelType: ResultRecommendedRoutineViewModelType | |
| let nextEntryPoint: ResultRecommendedRoutineViewController.EntryPoint | |
| switch entryPoint { | |
| case .onboarding: | |
| viewModelType = .onboarding(onboardingEntity: onboardingEntity) | |
| nextEntryPoint = .onboarding | |
| case .myPageResult: | |
| viewModelType = .mypage(onboardingEntity: onboardingEntity) | |
| nextEntryPoint = .mypage | |
| case .myPagePrevious: | |
| return // already handled above | |
| } | |
| // Apply configuration and push the next view controller | |
| resultRecommendedRoutineViewModel.configure(viewModelType: viewModelType) | |
| let nextView = ResultRecommendedRoutineViewController( | |
| entryPoint: nextEntryPoint, | |
| viewModel: resultRecommendedRoutineViewModel | |
| ) | |
| self.navigationController?.pushViewController(nextView, animated: true) | |
| } |
| // 이전 온보딩 선택 결과를 불러옵니다. | ||
| private func loadOnboardingResult() { | ||
| Task { | ||
| do { | ||
| let onboardingEntity = try await onboardingRepository.loadOnboardingResult() | ||
|
|
||
| let feelingOnboardingChoices = onboardingEntity.feeling.compactMap({ OnboardingChoiceType(rawValue: $0 )}) | ||
| let feelingResult = feelingOnboardingChoices.compactMap({ $0.resultTitle }).joined(separator: ", ") | ||
|
|
||
| guard | ||
| let timeOnboardingChoice = OnboardingChoiceType(rawValue: onboardingEntity.time), | ||
| let outdoorOnboardingChoice = OnboardingChoiceType(rawValue: onboardingEntity.outdoor), | ||
| let timeResult = timeOnboardingChoice.resultTitle, | ||
| let outdoorResult = outdoorOnboardingChoice.resultTitle | ||
| else { return } | ||
|
|
||
| let result = [timeResult, feelingResult, outdoorResult] | ||
| onboardingResultSubject.send(result) | ||
| } catch { | ||
| // TODO: 에러 처리 | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
loadOnboardingResult() 메서드의 에러 처리를 구현해주세요.
현재 에러가 발생하면 아무런 처리 없이 무시되고 있습니다. 사용자에게 적절한 피드백을 제공하거나 기본값을 설정하는 등의 처리가 필요합니다.
에러 처리 구현을 도와드릴까요? 다음과 같은 접근 방법을 제안드립니다:
private func loadOnboardingResult() {
Task {
do {
let onboardingEntity = try await onboardingRepository.loadOnboardingResult()
let feelingOnboardingChoices = onboardingEntity.feeling.compactMap({ OnboardingChoiceType(rawValue: $0 )})
let feelingResult = feelingOnboardingChoices.compactMap({ $0.resultTitle }).joined(separator: ", ")
guard
let timeOnboardingChoice = OnboardingChoiceType(rawValue: onboardingEntity.time),
let outdoorOnboardingChoice = OnboardingChoiceType(rawValue: onboardingEntity.outdoor),
let timeResult = timeOnboardingChoice.resultTitle,
let outdoorResult = outdoorOnboardingChoice.resultTitle
else { return }
let result = [timeResult, feelingResult, outdoorResult]
onboardingResultSubject.send(result)
} catch {
- // TODO: 에러 처리
+ // 에러 로깅
+ BitnagilLogger.log(logType: .error, message: "온보딩 결과 로드 실패: \(error)")
+ // 기본값 설정 또는 에러 상태 전달
+ onboardingResultSubject.send([])
}
}
}📝 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.
| // 이전 온보딩 선택 결과를 불러옵니다. | |
| private func loadOnboardingResult() { | |
| Task { | |
| do { | |
| let onboardingEntity = try await onboardingRepository.loadOnboardingResult() | |
| let feelingOnboardingChoices = onboardingEntity.feeling.compactMap({ OnboardingChoiceType(rawValue: $0 )}) | |
| let feelingResult = feelingOnboardingChoices.compactMap({ $0.resultTitle }).joined(separator: ", ") | |
| guard | |
| let timeOnboardingChoice = OnboardingChoiceType(rawValue: onboardingEntity.time), | |
| let outdoorOnboardingChoice = OnboardingChoiceType(rawValue: onboardingEntity.outdoor), | |
| let timeResult = timeOnboardingChoice.resultTitle, | |
| let outdoorResult = outdoorOnboardingChoice.resultTitle | |
| else { return } | |
| let result = [timeResult, feelingResult, outdoorResult] | |
| onboardingResultSubject.send(result) | |
| } catch { | |
| // TODO: 에러 처리 | |
| } | |
| } | |
| } | |
| // 이전 온보딩 선택 결과를 불러옵니다. | |
| private func loadOnboardingResult() { | |
| Task { | |
| do { | |
| let onboardingEntity = try await onboardingRepository.loadOnboardingResult() | |
| let feelingOnboardingChoices = onboardingEntity.feeling.compactMap({ OnboardingChoiceType(rawValue: $0 )}) | |
| let feelingResult = feelingOnboardingChoices.compactMap({ $0.resultTitle }).joined(separator: ", ") | |
| guard | |
| let timeOnboardingChoice = OnboardingChoiceType(rawValue: onboardingEntity.time), | |
| let outdoorOnboardingChoice = OnboardingChoiceType(rawValue: onboardingEntity.outdoor), | |
| let timeResult = timeOnboardingChoice.resultTitle, | |
| let outdoorResult = outdoorOnboardingChoice.resultTitle | |
| else { return } | |
| let result = [timeResult, feelingResult, outdoorResult] | |
| onboardingResultSubject.send(result) | |
| } catch { | |
| // 에러 로깅 | |
| BitnagilLogger.log(logType: .error, message: "온보딩 결과 로드 실패: \(error)") | |
| // 기본값 설정 또는 에러 상태 전달 | |
| onboardingResultSubject.send([]) | |
| } | |
| } | |
| } |
🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 194-194: TODOs should be resolved (에러 처리)
(todo)
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Onboarding/ViewModel/OnboardingViewModel.swift
around lines 175 to 197 the catch block in loadOnboardingResult() swallows
errors; implement proper error handling by (1) logging or reporting the error
(e.g., via a logger or analytics/telemetry), (2) updating
onboardingResultSubject with a safe fallback (e.g., a localized default result
array or an empty array) so subscribers get a deterministic state, and (3)
exposing a user-facing error state (e.g., send an error through a dedicated
errorSubject or call a delegate/publisher so the UI can show an alert). Ensure
the catch converts the error into a user-friendly message or default values and
avoids silently ignoring failures.
| mainLabel.snp.makeConstraints { make in | ||
| make.leading.equalTo(safeArea).offset(Layout.horizontalMargin) | ||
| make.trailing.equalTo(safeArea).inset(Layout.horizontalMargin) | ||
| make.horizontalEdges.equalTo(safeArea).inset(Layout.horizontalMargin) | ||
| mainLabelTopConstraint = make.top.equalTo(safeArea).offset(Layout.mainLabelMinTopSpacing).constraint | ||
| make.height.equalTo(entryPoint == .onboarding ? Layout.mainLabelMinHeight : Layout.mainLabelHeight) | ||
| make.height.equalTo(Layout.mainLabelHeight) | ||
| } | ||
|
|
||
| subLabel.snp.makeConstraints { make in | ||
| make.leading.equalTo(safeArea).offset(Layout.horizontalMargin) | ||
| make.trailing.equalTo(safeArea).inset(Layout.horizontalMargin) | ||
| make.top.equalTo(mainLabel.snp.bottom).offset(entryPoint == .onboarding ? 5 : Layout.subLabelTopSpacing) | ||
| make.height.equalTo(entryPoint == .onboarding ? Layout.subLabelMinHeight : Layout.subLabelHeight) | ||
| make.horizontalEdges.equalTo(safeArea).inset(Layout.horizontalMargin) | ||
| make.top.equalTo(mainLabel.snp.bottom).offset(Layout.subLabelTopSpacing) | ||
| make.height.equalTo(Layout.subLabelHeight) | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
고정 높이(Label)로 인한 클리핑/접근성 이슈 제거
mainLabel/subLabel에 고정 height를 주면 Dynamic Type·다국어에서 잘림 위험이 큽니다. 최소 높이 또는 intrinsic 기반으로 변경해 주세요.
적용 패치:
mainLabel.snp.makeConstraints { make in
- make.horizontalEdges.equalTo(safeArea).inset(Layout.horizontalMargin)
- mainLabelTopConstraint = make.top.equalTo(safeArea).offset(Layout.mainLabelMinTopSpacing).constraint
- make.height.equalTo(Layout.mainLabelHeight)
+ make.horizontalEdges.equalTo(safeArea).inset(Layout.horizontalMargin)
+ mainLabelTopConstraint = make.top.equalTo(safeArea).offset(Layout.mainLabelMinTopSpacing).constraint
+ make.height.greaterThanOrEqualTo(Layout.mainLabelHeight) // 최소 높이로
}
subLabel.snp.makeConstraints { make in
- make.horizontalEdges.equalTo(safeArea).inset(Layout.horizontalMargin)
- make.top.equalTo(mainLabel.snp.bottom).offset(Layout.subLabelTopSpacing)
- make.height.equalTo(Layout.subLabelHeight)
+ make.horizontalEdges.equalTo(safeArea).inset(Layout.horizontalMargin)
+ make.top.equalTo(mainLabel.snp.bottom).offset(Layout.subLabelTopSpacing)
+ make.height.greaterThanOrEqualTo(Layout.subLabelHeight) // 최소 높이로
}추가로 속성 설정(선택):
// configureAttribute() 내
mainLabel.numberOfLines = 0
subLabel.numberOfLines = 0Also applies to: 182-186
🤖 Prompt for AI Agents
In
Projects/Presentation/Sources/ResultRecommendedRoutine/View/ResultRecommendedRoutineViewController.swift
around lines 176-186 (also applies to 182-186), the mainLabel and subLabel use
fixed height constraints which can cause clipping with Dynamic Type and
different locales; change those fixed height constraints to be minimum heights
(use make.height.greaterThanOrEqualTo(Layout.mainLabelHeight) and
make.height.greaterThanOrEqualTo(Layout.subLabelHeight) or remove the explicit
height constraint to rely on intrinsicContentSize), and ensure both labels are
allowed to wrap by setting mainLabel.numberOfLines = 0 and
subLabel.numberOfLines = 0 in configureAttribute().
🌁 Background
온보딩 및 목표 재설정 api를 v2와 연결하고, 약간의 UI를 수정했어요 ~
📱 Screenshot
Simulator.Screen.Recording.-.iPhone.13.mini.-.2025-09-03.at.01.14.17.mp4
👩💻 Contents
NoticiationCenter썼으묘)📝 Review Note
1. OnboardingDTO vs. OnboardingResponseDTO
처음에는
realOutingFrequency값이 옵셔널이니까 둘을 분리할 필요가 없을 것이라 생각했지만 ...유저 정보를 조회할 때(
OnboardingResponseDTO)에는 감정 변수명이emotionTypes로 오고,온보딩 값을 등록할 때(
OnboardingDTO)에는 감정 변수명이emotionType로 와서 분리할 수 밖에 없었습니다 ㅠㅠ !!!!안드는 이미 수정해서 v2로 업로드 되어 있어서 바꿔달라고 말할 수 없었어요 ㅠ.ㅠ
2. ToastView
띵이 이전에
ToastMessageView를 너무너무 잘 만들어주셨는데요 !!!디자인 v2로 바꾸면서 2줄짜리 토스트 뷰가 필요 없어졌고, 그로 인해
showToast()함수에서 width, height 등을 보내주는 것이 불필요해졌다고 생각이 들었습니다 !!!그래서 딩이 했던 코드를 기반으로
ToastView를 만들었는데 기존 코드를 맘대로 바꾸는 것 같아 죄송하고 조심스러워서ToastMessageView를 삭제하는 대신 리뷰 노트에 의견 남겨봅니다 ㅠㅠ !!!📣 Related Issue
Summary by CodeRabbit