Skip to content

[Feat-T3-183] 온보딩 · 목표 재설정 api v2 연동#59

Merged
choijungp merged 5 commits intodevelopfrom
feat/onboarding
Sep 4, 2025
Merged

[Feat-T3-183] 온보딩 · 목표 재설정 api v2 연동#59
choijungp merged 5 commits intodevelopfrom
feat/onboarding

Conversation

@choijungp
Copy link
Copy Markdown
Contributor

@choijungp choijungp commented Sep 2, 2025

🌁 Background

온보딩 및 목표 재설정 api를 v2와 연결하고, 약간의 UI를 수정했어요 ~

📱 Screenshot

Simulator.Screen.Recording.-.iPhone.13.mini.-.2025-09-03.at.01.14.17.mp4

👩‍💻 Contents

  • 온보딩 · 목표 재설정 api v2 연동
  • 약간의 UI 수정 (UI 흐름 및 텍스트 등 ···)
  • 토스트 뷰 (NoticiationCenter 썼으묘)

📝 Review Note

1. OnboardingDTO vs. OnboardingResponseDTO

struct OnboardingDTO: Encodable {
    let timeSlot: String
    let emotionType: [String]
    let realOutingFrequency: String?
    let targetOutingFrequency: String

    func toOnboardingEntity() -> OnboardingEntity {
        return OnboardingEntity(
            time: timeSlot,
            feeling: emotionType,
            outdoor: targetOutingFrequency)
    }
}
struct OnboardingResponseDTO: Decodable {
    let timeSlot: String
    let emotionTypes: [String]
    let targetOutingFrequency: String

    func toOnboardingEntity() -> OnboardingEntity {
        return OnboardingEntity(
            time: timeSlot,
            feeling: emotionTypes,
            outdoor: targetOutingFrequency)
    }
}

처음에는 realOutingFrequency 값이 옵셔널이니까 둘을 분리할 필요가 없을 것이라 생각했지만 ...
유저 정보를 조회할 때(OnboardingResponseDTO)에는 감정 변수명이 emotionTypes로 오고,
온보딩 값을 등록할 때(OnboardingDTO)에는 감정 변수명이 emotionType로 와서 분리할 수 밖에 없었습니다 ㅠㅠ !!!!

안드는 이미 수정해서 v2로 업로드 되어 있어서 바꿔달라고 말할 수 없었어요 ㅠ.ㅠ

2. ToastView

띵이 이전에 ToastMessageView를 너무너무 잘 만들어주셨는데요 !!!
디자인 v2로 바꾸면서 2줄짜리 토스트 뷰가 필요 없어졌고, 그로 인해 showToast() 함수에서 width, height 등을 보내주는 것이 불필요해졌다고 생각이 들었습니다 !!!

그래서 딩이 했던 코드를 기반으로 ToastView를 만들었는데 기존 코드를 맘대로 바꾸는 것 같아 죄송하고 조심스러워서
ToastMessageView를 삭제하는 대신 리뷰 노트에 의견 남겨봅니다 ㅠㅠ !!!

📣 Related Issue

  • close #T3-183

Summary by CodeRabbit

  • New Features
    • 맞춤 추천 루틴 변경 시 하단 토스트 알림이 표시됩니다.
    • 마이페이지에서 이전에 설정한 온보딩 결과를 불러와 확인할 수 있습니다.
  • Enhancements
    • 온보딩 결과 화면이 진입 경로(온보딩/마이페이지)에 맞춰 동작 및 문구가 개선되었습니다.
    • 약관 동의 후 시작 흐름이 인트로 화면으로 단순화되었습니다.
    • 추천 루틴 결과 화면의 문구와 레이아웃이 가독성 있게 정비되었습니다.
  • Style
    • 토스트 배경색 및 일부 버튼의 기본 색상이 일관된 회색 톤으로 변경되었습니다.

@choijungp choijungp requested a review from taipaise September 2, 2025 16:34
@choijungp choijungp self-assigned this Sep 2, 2025
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Sep 2, 2025

Walkthrough

온보딩 데이터 흐름을 전면 개편: Domain에 OnboardingEntity 추가, 선택 배열 기반 인터페이스를 엔터티 기반으로 교체. DataSource에 요청/응답 DTO 및 엔드포인트 추가·변경, Repository/UseCase/Protocol 동기화. Presentation 레이어는 온보딩 결과 화면 분기 구조(EntryPoint) 도입, 토스트 노티/뷰 추가, 관련 VC/VM 및 네비게이션 경로 업데이트.

Changes

Cohort / File(s) Summary
Domain 엔터티/열거형 업데이트
Projects/Domain/Sources/Entity/OnboardingEntity.swift, Projects/Domain/Sources/Entity/RecommendedRoutineEntity.swift, Projects/Domain/Sources/Entity/Enum/OnboardingChoiceType.swift
OnboardingEntity 신설(time, feeling, frequency?, outdoor). RecommendedRoutineEntity.type를 Optional로 변경. OnboardingChoiceType을 String rawValue 기반으로 전환하고 내부 value 프로퍼티 제거.
DTO 추가 및 수정
Projects/DataSource/Sources/DTO/OnboardingDTO.swift, Projects/DataSource/Sources/DTO/OnboardingResponseDTO.swift, Projects/DataSource/Sources/DTO/RecommendedRoutineDTO.swift
OnboardingDTO(Encodable)와 OnboardingResponseDTO(Decodable) 추가, Entity 매핑 메서드 포함. RecommendedRoutineDTO.routineType을 Optional로 변경해 매핑 시 nil 허용.
온보딩 엔드포인트 개편
Projects/DataSource/Sources/Endpoint/OnboardingEndpoint.swift
loadOnboardingResult 케이스 추가, registerOnboarding 시그니처를 OnboardingDTO 기반으로 변경. 베이스 URL을 /api/v2/onboardings로 통일, GET/POST 분리, 바디 매핑을 onboarding.dictionary로 수정.
Repository 변경
Projects/DataSource/Sources/Repository/OnboardingRepository.swift
loadOnboardingResult() 추가(응답 DTO → Entity). registerOnboarding(onboardingEntity:) 추가(Entity→DTO→네트워크→추천루틴 매핑). 기존 choices 기반 메서드와 isOnboardingDone() 제거.
도메인 프로토콜/유스케이스 시그니처 변경
Projects/Domain/Sources/Protocol/Repository/OnboardingRepositoryProtocol.swift, Projects/Domain/Sources/Protocol/UseCase/ResultRecommendedRoutineUseCaseProtocol.swift, Projects/Domain/Sources/UseCase/ResultRecommendedRoutine/ResultRecommendedRoutineUseCase.swift
Repository/UseCase 모두 OnboardingEntity 기반으로 파라미터 교체, 결과 조회 메서드 추가, choices→dictionary 변환 로직 제거.
DI 조립 변경
Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift
OnboardingViewModel 주입 시 OnboardingRepositoryProtocol 의존성 추가.
온보딩 VM/VC 개편
Projects/Presentation/Sources/Onboarding/ViewModel/OnboardingViewModel.swift, Projects/Presentation/Sources/Onboarding/View/OnboardingResultViewController.swift, Projects/Presentation/Sources/Onboarding/View/OnboardingViewController.swift
VM: 입력 loadOnboardingResult 추가, choices 퍼블리셔를 OnboardingEntity로 교체, Repository 의존성 추가. VC: EntryPoint enum 도입으로 분기(온보딩/마이페이지 이전/결과), 초기화 시그니처 변경, 흐름에 따라 makeOnboardingResult/loadOnboardingResult 호출 및 네비게이션 분기. OnboardingViewController는 EntryPoint 전달 방식으로 업데이트.
결과 추천 루틴 화면/VM 개편
Projects/Presentation/Sources/ResultRecommendedRoutine/View/ResultRecommendedRoutineViewController.swift, Projects/Presentation/Sources/ResultRecommendedRoutine/ViewModel/ResultRecommendedRoutineViewModel.swift, Projects/Presentation/Sources/Onboarding/Model/RecommendedRoutine.swift
View 클래스명을 Controller로 변경 및 문구/레이아웃 고정값 정리. VM 타입/입력에서 OnboardingEntity 사용, 토스트 노티 발송 입력 추가. 모델 매핑에서 Optional type에 기본값(.recommendation) 적용.
토스트 컴포넌트/연결
Projects/Presentation/Sources/Common/Component/ToastView.swift, Projects/Presentation/Sources/Common/Extension/Notification+.swift, Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineViewController.swift, Projects/Presentation/Sources/Common/Component/ToastMessageView.swift
새로운 ToastView 추가 및 노티 이름 showRecommendedRoutineToast 신설. 추천루틴 VC가 노티 구독해 토스트 표시. 기존 ToastMessageView 배경색 변경(네이비→그레이).
네비게이션 경로 변경
Projects/Presentation/Sources/EmotionRegister/View/EmotionRegisterView.swift, Projects/Presentation/Sources/Login/View/TermsAgreementViewController.swift, Projects/Presentation/Sources/MyPage/View/MypageView.swift
EmotionRegister가 ResultRecommendedRoutineViewController로 전환. 약관 동의 후 IntroViewController로 이동. 마이페이지 리셋 진입 시 OnboardingResultViewController로 변경 및 EntryPoint 사용.

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
Loading
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
Loading
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) 후 자동 사라짐
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60 minutes

Poem

(\/)
( •
•)⌒🥕
엔터티 한 송이 안고 토스트 빛나네,
선택은 모여 길을 바꾸었지요.
깡충깡충 코드 밭을 지나, 추천 향기 맡으러!

✨ Finishing Touches
  • 📝 Generate Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/onboarding

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore or @coderabbit ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary or @coderabbitai 요약 to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 = 24
Projects/Presentation/Sources/EmotionRegister/View/EmotionRegisterView.swift (1)

130-131: 변수명 개선 제안
초기화 시그니처(entryPoint: .emotion, viewModel:)가 정확히 일치합니다.
resultRecommendedRoutineViewresultRecommendedRoutineVC로 변경해 가독성을 높여 주세요.

Projects/Presentation/Sources/Onboarding/View/OnboardingViewController.swift (1)

242-246: EntryPoint 분기 적용 깔끔합니다.

마이페이지 유입 여부에 따라 .myPageResult vs .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.

📥 Commits

Reviewing files that changed from the base of the PR and between 673dc3c and 770d6de.

📒 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.swift
  • Projects/Domain/Sources/Protocol/Repository/OnboardingRepositoryProtocol.swift
  • Projects/Presentation/Sources/Login/View/TermsAgreementViewController.swift
  • Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift
  • Projects/Presentation/Sources/ResultRecommendedRoutine/View/ResultRecommendedRoutineViewController.swift
  • Projects/Presentation/Sources/Onboarding/View/OnboardingViewController.swift
  • Projects/Presentation/Sources/EmotionRegister/View/EmotionRegisterView.swift
  • Projects/Presentation/Sources/ResultRecommendedRoutine/ViewModel/ResultRecommendedRoutineViewModel.swift
  • Projects/Presentation/Sources/MyPage/View/MypageView.swift
  • Projects/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: 의도된 겹침 오프셋 유지 — LGTM

rectangleImageView를 음수 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: type Optional 변경 영향 범위 수동 검증 필요
자동화 스크립트로는 .type! 강제 언래핑이나 switch 분기 누락을 발견하지 못했습니다. UI 바인딩, switch 분기, 필터링 로직 등에서 typenil일 때의 처리를 수동으로 검증하고, 필요 시 기본값(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/targetOutingFrequencyOnboardingEntity(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.baseURLendpoint.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 전반에 걸쳐 올바르게 적용되었습니다.

Comment thread Projects/DataSource/Sources/DTO/OnboardingDTO.swift
Comment on lines +33 to +46
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
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
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.

Comment on lines +170 to 176
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)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
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).

Copy link
Copy Markdown
Collaborator

@taipaise taipaise left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

  1. 어거지일 수도 있지만, 아래와 같은 구조도 가능은.. 할거 같습니다!
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과 같이 변경 불가합니다!
물론 현상유지해서 파일 두 개 유지하는 것도 괜찮다고 생각합니다!

  1. 코드를 수정하거나 삭제하는 것은 불가피하고, 너무나 당연한 일이라고 생각합니다!! 수정해서 더 좋은 구조를 갖게 되거나 사용성이 올라가거나, 요구사항에 맞으면 오히려 좋아입니다!! 노 죄송!!!!
    수정할 코드가 있는데, 당장 제가 하는 작업에 영향이 있다면 말씀만 살짝 해주시면 좋을거 같아요!!! (지금은 없지만요) 그렇지 않다면 수정 이후에 pr에서 확인하는 것도 좋습니다! 저도 조이가 작업하신 코드 중에 수정할 내용이 있으면 그렇게 하겠습니다~!!!

이번 PR도 완고입니다~!~! 😇😇

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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: EntryPoint enum에 대한 문서화가 필요합니다.

각 케이스가 어떤 상황을 나타내는지 명확하지 않습니다. 특히 myPagePreviousmyPageResult의 차이점이 불분명합니다.

 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.

📥 Commits

Reviewing files that changed from the base of the PR and between 770d6de and 78fe2e5.

📒 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.swift
  • Projects/DataSource/Sources/Repository/OnboardingRepository.swift
  • Projects/Presentation/Sources/Onboarding/View/OnboardingViewController.swift
  • Projects/Presentation/Sources/ResultRecommendedRoutine/View/ResultRecommendedRoutineViewController.swift
  • 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/ResultRecommendedRoutine/View/ResultRecommendedRoutineViewController.swift
  • Projects/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: 검증 완료: OnboardingResultViewControllerinit(viewModel:entryPoint:) 시그니처가 EntryPoint enum의 .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 변환 로직이 명확하게 구현되었습니다.

OnboardingEntityOnboardingDTO로 변환하는 과정이 명시적이고 이해하기 쉽게 작성되었습니다.

Comment on lines +273 to 296
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)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ Refactor suggestion

goToResultRecommendedRoutineView 메서드의 중복 코드를 제거해주세요.

onboardingmyPageResult 케이스에서 거의 동일한 로직이 반복되고 있습니다.

 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.

Suggested change
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)
}

Comment on lines +175 to +197
// 이전 온보딩 선택 결과를 불러옵니다.
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: 에러 처리
}
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
// 이전 온보딩 선택 결과를 불러옵니다.
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.

Comment on lines 176 to 186
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)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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 = 0

Also 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().

@choijungp choijungp merged commit 7d827ba into develop Sep 4, 2025
2 checks passed
@choijungp choijungp deleted the feat/onboarding branch September 4, 2025 09:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants