Skip to content

[Feat-T3-70] 추천 루틴 UI 구현#21

Merged
choijungp merged 15 commits into
developfrom
feat/recommend-view
Jul 17, 2025
Merged

[Feat-T3-70] 추천 루틴 UI 구현#21
choijungp merged 15 commits into
developfrom
feat/recommend-view

Conversation

@choijungp

@choijungp choijungp commented Jul 17, 2025

Copy link
Copy Markdown
Contributor

🌁 Background

추천 루틴 UI를 구현하였습니다.
아직 서버 API가 미배포된 상황으로 현재는 UI와 사용자 인터랙션만 확인해주시면 감사하겠습니다.

또한 CustomBottomSheet를 구현하였습니다.
CustomBottomSheet에 원하는 UIViewController를 넣어주시면 됩니다.


📱 Screenshot

추천 루틴 목록 확인 (맞춤 추천)

iPhone SE3 iPhone 13 mini iPhone 16 Pro

추천 루틴 목록 확인 (맞춤 추천이 아닌 카테고리)

iPhone SE3 iPhone 13 mini iPhone 16 Pro

추천 루틴 난이도 선택

iPhone SE3 iPhone 13 mini iPhone 16 Pro

👩‍💻 Contents

  • 추천 루틴 관련 UI

    • RoutineCategoryButton : 루틴 카테고리 버튼
    • RoutineCategoryView : 전체 루틴 카테고리 버튼들을 보여주는 수평 스크롤 뷰
    • RoutineLevelButton : 루틴 난이도 선택 버튼
    • RoutineLevelCell : RoutineLevelView에 보일 tableView Cell
    • RoutineLevelView : 루틴 난이도 선택 뷰
    • RecommendedRoutineCardView : 추천 루틴 카드 뷰
    • RegisterEmotionButton : 오늘 감정 루틴 추천 받기 버튼
    • RecommendedRoutineView : 추천 루틴 뷰
  • CustomBottomSheet : 커스텀 바텀 시트 (재사용성 높음)


✅ Testing

SceneDelegate의 진입점을 TabBarView()로 바꾸어 추천 루틴 탭의 UI를 테스트 해보았습니다.

  • 카테고리 선택 시 추천 루틴이 바뀜
    • 맞춤 추천 카테고리에만 감정 루틴 추천 받기 버튼이 보임
  • 난이도 선택

📝 Review Note

  • RecommendedRoutine 타입에 난이도가 아직 없어 난이도 필터링은 구현하지 않았습니다.
    추후 서버 통신 로직과 붙일 때 마저 구현하겠습니다.
  • EmptyView 또한 디자인적으로 확정되지 않아 통신 로직 붙일 때 마저 구현하겠습니다.

📣 Related Issue

  • close #T3-70

Summary by CodeRabbit

  • 신규 기능

    • 추천 루틴을 카테고리별 및 난이도별로 확인할 수 있는 추천 루틴 화면이 추가되었습니다.
    • 추천 루틴 카드, 카테고리 버튼, 난이도 버튼, 감정 등록 버튼 등 다양한 UI 컴포넌트가 도입되었습니다.
    • 바텀시트 UI 컴포넌트가 새롭게 추가되어 하단 시트 형태의 모달을 지원합니다.
    • 이미지 리사이즈 및 회전 기능이 확장되었습니다.
    • 플러스 및 체브론 아이콘이 신규 추가되고, 방향에 따른 체브론 회전 기능이 도입되었습니다.
    • 추천 루틴 관련 뷰모델과 의존성 주입 설정이 추가되었습니다.
  • 버그 수정

    • 기존 RecommendView가 제거되고 새로운 추천 루틴 뷰가 적용되었습니다.

@choijungp
choijungp requested a review from taipaise July 17, 2025 13:18
@choijungp choijungp self-assigned this Jul 17, 2025
@coderabbitai

coderabbitai Bot commented Jul 17, 2025

Copy link
Copy Markdown

"""

Walkthrough

이번 변경에서는 추천 루틴(RecommendedRoutine) 관련 신규 UI 컴포넌트, 뷰, 뷰모델, 모델(enum 등), 이미지 에셋 및 유틸리티 확장(extension)들이 대거 추가되었습니다. 또한, DI 컨테이너 등록, 탭바 뷰의 추천 탭 교체, 커스텀 바텀시트 구현 및 뷰컨트롤러 확장 등도 포함되어 있습니다.

Changes

파일/경로 요약 변경 요약
.../Images.xcassets/chevron_icon.imageset/Contents.json
.../plus_icon.imageset/Contents.json
chevron, plus 아이콘 에셋 JSON 추가 (1x, 2x, 3x 스케일)
.../Common/Component/CustomBottomSheet.swift
.../Extension/UIViewController+.swift
커스텀 바텀시트 UIViewController 및 presentCustomBottomSheet 메서드 추가
.../Common/DesignSystem/BitnagilIcon.swift
.../Common/Extension/UIImage+.swift
아이콘(static 이미지) 및 UIImage 리사이즈/회전 확장 메서드 추가
.../Common/TabBarView.swift RecommendView 제거, DI로 RecommendedRoutineView 및 ViewModel 연결
.../Onboarding/Model/RecommendedRoutine.swift routineCategory 프로퍼티 및 초기화자 추가
.../RecommendedRoutine/Model/RoutineCategoryType.swift
.../RoutineLevelType.swift
루틴 카테고리/레벨 enum 및 id, title 등 프로퍼티 추가
.../RecommendedRoutine/View/Component/RecommendedRoutineCardView.swift 추천 루틴 카드 뷰 및 델리게이트 프로토콜 추가
.../RecommendedRoutine/View/Component/RegisterEmotionButton.swift 감정 등록 버튼 컴포넌트 추가
.../RecommendedRoutine/View/Component/RoutineCategoryButton.swift 루틴 카테고리 버튼 컴포넌트 추가
.../RecommendedRoutine/View/Component/RoutineCategoryView.swift 카테고리 버튼 리스트 뷰 및 델리게이트 프로토콜 추가
.../RecommendedRoutine/View/Component/RoutineLevelButton.swift 루틴 난이도 버튼 컴포넌트 추가
.../RecommendedRoutine/View/Component/RoutineLevelCell.swift 루틴 난이도 셀(체크마크 포함) 추가
.../RecommendedRoutine/View/Component/RoutineLevelView.swift 루틴 난이도 선택 뷰컨트롤러 및 델리게이트 프로토콜 추가
.../RecommendedRoutine/View/RecommendedRoutineView.swift 추천 루틴 메인 뷰컨트롤러(카테고리, 카드, 난이도, 감정등록 포함) 추가
.../RecommendedRoutine/ViewModel/RecommendedRoutineViewModel.swift 추천 루틴 뷰모델, Input/Output, 더미 데이터 등 전체 로직 추가
.../Common/PresentationDependencyAssembler.swift RecommendedRoutineViewModel DI 컨테이너 등록

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant TabBarView
    participant DIContainer
    participant RecommendedRoutineView
    participant RecommendedRoutineViewModel
    participant RoutineLevelView

    User->>TabBarView: 추천 탭 선택
    TabBarView->>DIContainer: RecommendedRoutineViewModel resolve
    TabBarView->>RecommendedRoutineView: 생성 및 표시
    RecommendedRoutineView->>RecommendedRoutineViewModel: fetchRecommendedRoutines(category)
    RecommendedRoutineViewModel-->>RecommendedRoutineView: recommendedRoutinePublisher
    User->>RecommendedRoutineView: 카테고리/난이도/카드 선택
    RecommendedRoutineView->>RecommendedRoutineViewModel: action(input)
    RecommendedRoutineViewModel-->>RecommendedRoutineView: 상태 변경(Publisher)
    User->>RecommendedRoutineView: 난이도 버튼 탭
    RecommendedRoutineView->>RoutineLevelView: 바텀시트로 표시
    RoutineLevelView-->>RecommendedRoutineView: 난이도 선택 델리게이트 콜백
    RecommendedRoutineView->>RecommendedRoutineViewModel: action(selectLevel)
Loading

Poem

🐇
새로 온 루틴, 카드와 뷰
카테고리, 난이도, 감정까지 두루두루!
바텀시트 쏙, 아이콘이 반짝
토끼는 깡총, 코드를 찬양
추천 루틴, 오늘도 성장!

"""


📜 Recent review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 66526c9 and 6bf9281.

📒 Files selected for processing (1)
  • Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelView.swift (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelView.swift
✨ 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/recommend-view

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.
    • Explain this complex logic.
    • 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. Examples:
    • @coderabbitai explain this code block.
    • @coderabbitai modularize this function.
  • 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 src/utils.ts and explain its main purpose.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.
    • @coderabbitai help me debug CodeRabbit configuration file.

Support

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

Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments.

CodeRabbit Commands (Invoked using PR comments)

  • @coderabbitai pause to pause the reviews on a PR.
  • @coderabbitai resume to resume the paused reviews.
  • @coderabbitai review to trigger an incremental review. This is useful when automatic reviews are disabled for the repository.
  • @coderabbitai full review to do a full review from scratch and review all the files again.
  • @coderabbitai summary to regenerate the summary of the PR.
  • @coderabbitai generate docstrings to generate docstrings for this PR.
  • @coderabbitai generate sequence diagram to generate a sequence diagram of the changes in this PR.
  • @coderabbitai auto-generate unit tests to generate unit tests for this PR.
  • @coderabbitai resolve resolve all the CodeRabbit review comments.
  • @coderabbitai configuration to show the current CodeRabbit configuration for the repository.
  • @coderabbitai help to get help.

Other keywords and placeholders

  • Add @coderabbitai 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.

Documentation and Community

  • 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.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 4

🔭 Outside diff range comments (1)
Projects/Presentation/Sources/Onboarding/Model/RecommendedRoutine.swift (1)

29-36: extension 메서드에서 새로운 속성을 처리하지 않는 문제가 있습니다.

toRecommendedRoutine() 메서드가 새로 추가된 routineCategory 속성을 설정하지 않아서, 이 메서드로 생성된 인스턴스는 기본값도 갖지 않게 됩니다.

 extension RecommendedRoutineEntity {
     func toRecommendedRoutine() -> RecommendedRoutine {
         return RecommendedRoutine(
             id: id,
             mainTitle: title,
-            subTitle: description)
+            subTitle: description,
+            routineCategory: .recommendation)
     }
 }
🧹 Nitpick comments (10)
Projects/Presentation/Sources/Common/Extension/UIImage+.swift (1)

10-72: 이미지 변환 유틸리티가 잘 구현되어 있지만 에지 케이스 처리를 개선해주세요.

UIGraphicsImageRenderer 사용으로 성능이 좋고, 각 메서드의 목적이 명확합니다. 하지만 몇 가지 개선점이 있습니다:

  1. division by zero 방지 필요: resizeAspectFitresizeAspectFill에서 높이가 0일 때 크래시 가능성
  2. 입력 검증 추가 권장: 음수나 0 크기 입력에 대한 처리
  3. 메모리 최적화: 큰 이미지 처리 시 메모리 사용량 고려

다음과 같은 개선을 고려해보세요:

func resizeAspectFit(to size: CGSize) -> UIImage? {
+    guard self.size.width > 0, self.size.height > 0, size.width > 0, size.height > 0 else { return nil }
    let aspectRatio = self.size.width / self.size.height
    let targetRatio = size.width / size.height
    // ... 기존 코드
}
Projects/Presentation/Sources/Common/Extension/UIViewController+.swift (1)

79-82: 바텀시트 프레젠테이션 API가 깔끔하게 구현되어 있습니다.

CustomBottomSheet 프레젠테이션을 위한 편리한 API를 제공하고 있으며, 메서드 시그니처와 구현이 적절합니다. 향후 필요에 따라 completion handler나 애니메이션 옵션 추가를 고려할 수 있겠습니다.

추가 개선사항을 고려해보세요:

- func presentCustomBottomSheet(contentViewController: UIViewController, maxHeight: CGFloat) {
+ func presentCustomBottomSheet(contentViewController: UIViewController, maxHeight: CGFloat, completion: (() -> Void)? = nil) {
    let bottomSheet = CustomBottomSheet(contentViewController: contentViewController, maxHeight: maxHeight)
-   present(bottomSheet, animated: true)
+   present(bottomSheet, animated: true, completion: completion)
}
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelButton.swift (1)

23-27: 옵셔널 변수 초기화 개선 필요

옵셔널 변수를 nil로 초기화하는 것은 불필요합니다. Swift에서 옵셔널 변수는 기본적으로 nil로 초기화됩니다.

다음과 같이 수정하세요:

-    private var level: RoutineLevelType? = nil {
+    private var level: RoutineLevelType? {
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineCategoryView.swift (1)

46-54: 중복된 정렬 로직 최적화 제안

RoutineCategoryType.allCases.sorted(by: { $0.id < $1.id })configureAttributeconfigureLayout에서 두 번 호출되고 있습니다. 성능 최적화를 위해 정렬된 결과를 프로퍼티로 캐시하는 것을 고려해보세요.

final class RoutineCategoryView: UIView {
+    private let sortedCategories = RoutineCategoryType.allCases.sorted(by: { $0.id < $1.id })
    
    private func configureAttribute() {
        // ... existing code ...
        
-        RoutineCategoryType.allCases.sorted(by: { $0.id < $1.id }).forEach { type in
+        sortedCategories.forEach { type in
            // ... existing code ...
        }
    }
}
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelView.swift (2)

14-14: 불필요한 public 접근 제한자

내부 컴포넌트에 public 접근 제한자가 필요하지 않을 것 같습니다. internal(기본값)로 충분할 것 같습니다.

- public final class RoutineLevelView: UIViewController {
+ final class RoutineLevelView: UIViewController {

59-59: 정렬 로직 최적화 제안

다른 파일과 마찬가지로 정렬된 결과를 캐시하여 성능을 개선할 수 있습니다.

+ private let sortedLevels = RoutineLevelType.allCases.sorted(by: { $0.id < $1.id })

- let level = RoutineLevelType.allCases.sorted(by: { $0.id < $1.id })[indexPath.row]
+ let level = sortedLevels[indexPath.row]
Projects/Presentation/Sources/Common/Component/CustomBottomSheet.swift (1)

148-148: 매직 넘버 상수화 제안

제스처 기반 dismiss 임계값들을 상수로 추출하면 가독성과 유지보수성이 향상됩니다.

+ private enum GestureThreshold {
+     static let dismissPositionRatio: CGFloat = 0.3
+     static let dismissVelocity: CGFloat = 1000
+ }

- let shouldDismiss = translation.y > maxHeight * 0.3 || velocity.y > 1000
+ let shouldDismiss = translation.y > maxHeight * GestureThreshold.dismissPositionRatio || velocity.y > GestureThreshold.dismissVelocity
Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineView.swift (3)

195-197: TODO 주석 해결 필요

정적 분석 도구가 지적한 대로 TODO 주석이 남아 있습니다. 루틴 등록 화면으로의 네비게이션 구현이 필요합니다.

네비게이션 구현을 위한 코드 생성을 도와드릴까요? 아니면 이를 추적하기 위한 새로운 이슈를 생성해드릴까요?


153-173: 메서드 명명 개선 제안

fetchRecommendedRoutines 메서드는 실제로 데이터를 가져오는 것이 아니라 UI를 업데이트하는 역할을 합니다. 메서드명을 updateRecommendedRoutinesUI 또는 configureRecommendedRoutines로 변경하는 것이 더 명확할 것 같습니다.

- private func fetchRecommendedRoutines(recommendedRoutines: [RecommendedRoutine]) {
+ private func updateRecommendedRoutinesUI(recommendedRoutines: [RecommendedRoutine]) {

170-172: 조건부 UI 표시 로직 개선 제안

감정 버튼을 표시하는 조건이 배열의 첫 번째 요소만 체크하고 있습니다. 더 안전한 방법을 고려해보세요.

- if !recommendedRoutines.isEmpty && recommendedRoutines[0].routineCategory == .recommendation {
+ if recommendedRoutines.allSatisfy({ $0.routineCategory == .recommendation }) && !recommendedRoutines.isEmpty {
     showEmotionButton()
 }

또는 뷰모델의 selectedCategory를 직접 체크하는 방법:

- if !recommendedRoutines.isEmpty && recommendedRoutines[0].routineCategory == .recommendation {
+ if viewModel.output.selectedCategoryPublisher.value == .recommendation && !recommendedRoutines.isEmpty {
     showEmotionButton()
 }
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 2607010 and c2b6e84.

⛔ Files ignored due to path filters (6)
  • Projects/Presentation/Resources/Images.xcassets/chevron_icon.imageset/chevron_icon.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/chevron_icon.imageset/chevron_icon@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/chevron_icon.imageset/chevron_icon@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/plus_icon.imageset/plus_icon.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/plus_icon.imageset/plus_icon@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/plus_icon.imageset/plus_icon@3x.png is excluded by !**/*.png
📒 Files selected for processing (20)
  • Projects/Presentation/Resources/Images.xcassets/chevron_icon.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Images.xcassets/plus_icon.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Sources/Common/Component/CustomBottomSheet.swift (1 hunks)
  • Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift (2 hunks)
  • Projects/Presentation/Sources/Common/Extension/UIImage+.swift (1 hunks)
  • Projects/Presentation/Sources/Common/Extension/UIViewController+.swift (2 hunks)
  • Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift (1 hunks)
  • Projects/Presentation/Sources/Common/TabBarView.swift (1 hunks)
  • Projects/Presentation/Sources/Onboarding/Model/RecommendedRoutine.swift (1 hunks)
  • Projects/Presentation/Sources/RecommendedRoutine/Model/RoutineCategoryType.swift (1 hunks)
  • Projects/Presentation/Sources/RecommendedRoutine/Model/RoutineLevelType.swift (1 hunks)
  • Projects/Presentation/Sources/RecommendedRoutine/View/Component/RecommendedRoutineCardView.swift (1 hunks)
  • Projects/Presentation/Sources/RecommendedRoutine/View/Component/RegisterEmotionButton.swift (1 hunks)
  • Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineCategoryButton.swift (1 hunks)
  • Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineCategoryView.swift (1 hunks)
  • Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelButton.swift (1 hunks)
  • Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelCell.swift (1 hunks)
  • Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelView.swift (1 hunks)
  • Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineView.swift (1 hunks)
  • Projects/Presentation/Sources/RecommendedRoutine/ViewModel/RecommendedRoutineViewModel.swift (1 hunks)
🧰 Additional context used
🧠 Learnings (8)
📓 Common learnings
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 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.
Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#19
File: Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift:0-0
Timestamp: 2025-07-16T10:57:16.559Z
Learning: choijungp는 추후 CustomAlertView를 만들어서 에러 처리를 UI로 해결하도록 수정할 계획을 가지고 있음.
Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#19
File: Projects/Domain/Sources/UseCase/Onboarding/OnboardingUseCase.swift:0-0
Timestamp: 2025-07-16T10:58:30.858Z
Learning: choijungp는 OnboardingUseCase의 convertToDictionary 함수에서 에러 처리 개선을 추후 고민하고 반영할 계획을 가지고 있음.
Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift (1)
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 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.
Projects/Presentation/Sources/Common/TabBarView.swift (1)
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 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.
Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineView.swift (3)
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 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.
Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.
Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RecommendedRoutineCardView.swift (1)
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 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.
Projects/Presentation/Sources/RecommendedRoutine/ViewModel/RecommendedRoutineViewModel.swift (1)
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 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelView.swift (1)
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 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineCategoryView.swift (1)
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 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.
🧬 Code Graph Analysis (8)
Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift (1)
Projects/Shared/Sources/DIContainer/DIContainer.swift (1)
  • register (14-16)
Projects/Presentation/Sources/Common/TabBarView.swift (1)
Projects/Shared/Sources/DIContainer/DIContainer.swift (1)
  • resolve (18-25)
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RegisterEmotionButton.swift (1)
Projects/Presentation/Sources/Common/Extension/UIImage+.swift (1)
  • resizeAspectFit (20-35)
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineCategoryButton.swift (1)
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineCategoryView.swift (2)
  • configureAttribute (38-55)
  • configureLayout (57-80)
Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift (1)
Projects/Presentation/Sources/Common/Extension/UIImage+.swift (2)
  • rotate (56-67)
  • rotate (69-71)
Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineView.swift (4)
Projects/Presentation/Sources/Common/Extension/UIViewController+.swift (1)
  • presentCustomBottomSheet (79-82)
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineCategoryView.swift (1)
  • updateSelectedCategory (82-87)
Projects/Presentation/Sources/RecommendedRoutine/ViewModel/RecommendedRoutineViewModel.swift (2)
  • selectCategory (49-55)
  • selectLevel (57-60)
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelButton.swift (1)
  • updateButton (85-87)
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RecommendedRoutineCardView.swift (2)
Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineView.swift (1)
  • recommendedRoutineCardViewDelegate (194-197)
Projects/Presentation/Sources/Common/Extension/UIImage+.swift (1)
  • resizeAspectFit (20-35)
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineCategoryView.swift (2)
Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineView.swift (1)
  • routineCategoryView (187-189)
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineCategoryButton.swift (3)
  • configureAttribute (34-42)
  • configureLayout (44-52)
  • updateButtonState (63-65)
🪛 SwiftLint (0.57.0)
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelButton.swift

[Warning] 23-23: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)

Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineView.swift

[Warning] 195-195: TODOs should be resolved (루틴 등록하기 화면으로 이동해야 함 + 추천 루틴 들고)

(todo)

Projects/Presentation/Sources/RecommendedRoutine/ViewModel/RecommendedRoutineViewModel.swift

[Warning] 59-59: TODOs should be resolved (현재 보여주고 있는 추천 루틴 난이도 필터링 해야 함)

(todo)


[Warning] 82-82: TODOs should be resolved (Dummy Data를 지우세요)

(todo)

Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelView.swift

[Warning] 21-21: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)

🔇 Additional comments (42)
Projects/Presentation/Resources/Images.xcassets/plus_icon.imageset/Contents.json (1)

1-24: 에셋 카탈로그 구조가 표준을 따르고 있습니다.

Xcode 에셋 카탈로그의 표준 JSON 구조를 올바르게 따르고 있으며, 모든 스케일 팩터(1x, 2x, 3x)와 필수 메타데이터가 적절히 정의되어 있습니다.

Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift (1)

39-41: 의존성 주입 등록이 기존 패턴을 잘 따르고 있습니다.

RecommendedRoutineViewModel의 DI 등록이 기존 패턴과 일치하며 올바르게 구현되어 있습니다. 현재는 의존성 없이 단순 인스턴스 생성만 하고 있는데, 향후 서버 통신 로직이 추가되면 UseCase 의존성 주입을 고려해볼 수 있겠습니다.

Projects/Presentation/Resources/Images.xcassets/chevron_icon.imageset/Contents.json (1)

1-24: 에셋 카탈로그 구조가 표준을 따르고 있습니다.

Xcode 에셋 카탈로그의 표준 JSON 구조를 올바르게 따르고 있으며, 회전을 통한 방향성 아이콘 구현에 적합한 기본 아이콘으로 잘 설계되어 있습니다.

Projects/Presentation/Sources/Common/TabBarView.swift (1)

45-45: RecommendedRoutineViewModel 의존성 등록이 확인되었습니다.

  • Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift
    DIContainer.shared.register(type: RecommendedRoutineViewModel.self) { … } 구문으로 정상 등록
  • Projects/Presentation/Sources/Common/TabBarView.swift
    DIContainer.shared.resolve(type: RecommendedRoutineViewModel.self) 호출부 정상 작동

이대로 머지해도 무방합니다.

Projects/Presentation/Sources/Onboarding/Model/RecommendedRoutine.swift (2)

14-14: 새로운 routineCategory 속성이 추가되었습니다.

카테고리 기능을 지원하기 위한 적절한 확장입니다.


16-26: 명시적 초기화 메서드가 잘 구현되었습니다.

기본값을 .recommendation으로 설정한 것이 합리적입니다.

Projects/Presentation/Sources/RecommendedRoutine/Model/RoutineLevelType.swift (1)

8-28: 루틴 난이도 타입이 잘 설계되었습니다.

  • CaseIterable 프로토콜 준수로 모든 케이스를 쉽게 순회할 수 있습니다
  • idlevelTitle 계산 속성이 명확하고 직관적입니다
  • 한국어 설명이 사용자 친화적으로 작성되었습니다
Projects/Presentation/Sources/RecommendedRoutine/Model/RoutineCategoryType.swift (1)

8-37: 루틴 카테고리 타입이 체계적으로 구현되었습니다.

  • 6개의 카테고리가 명확하게 정의되어 있습니다
  • CaseIterable 프로토콜로 UI에서 쉽게 활용할 수 있습니다
  • 각 카테고리의 한국어 제목이 직관적이고 이해하기 쉽습니다
  • 순차적인 ID 값으로 일관성이 유지됩니다
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RegisterEmotionButton.swift (3)

13-20: 레이아웃 상수가 체계적으로 정의되었습니다.

Layout enum을 통해 모든 레이아웃 관련 상수를 중앙 집중식으로 관리하는 것이 좋은 패턴입니다.


48-52: 디자인 시스템 일관성이 잘 유지되었습니다.

BitnagilFont와 BitnagilColor를 일관되게 사용하여 앱의 디자인 시스템을 잘 따르고 있습니다.


55-69: SnapKit을 활용한 오토레이아웃이 적절히 구현되었습니다.

아이콘과 레이블의 배치가 명확하고 제약 조건이 잘 정의되어 있습니다.

Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift (3)

18-19: 새로운 아이콘 추가 승인

plusIcon과 chevronIcon이 적절하게 추가되었습니다. plusIcon은 template 모드로 설정되어 tintColor 변경이 가능하며, chevronIcon은 기본 모드로 설정되어 있습니다.


20-22: chevronIcon 방향 메서드 구현 승인

Direction enum의 rotation 값을 사용하여 chevronIcon을 회전시키는 메서드가 잘 구현되었습니다. 회전 후 template 모드로 설정하여 tintColor 변경이 가능합니다.


38-52: Direction enum 구현 승인

Direction enum이 up, down, left, right 케이스와 각각의 회전 각도를 명확하게 정의하고 있습니다. 각 방향별 회전 각도가 직관적으로 설정되어 있습니다.

Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelCell.swift (4)

10-28: RoutineLevelCell 클래스 구조 승인

UITableViewCell을 상속한 클래스 구조가 적절하게 구현되었습니다. Layout enum으로 상수를 관리하고 있어 유지보수성이 좋습니다.


30-43: UI 속성 설정 승인

titleLabel과 checkIcon의 속성이 적절하게 설정되었습니다. 체크 아이콘의 크기 조정과 템플릿 모드 적용이 잘 구현되어 있습니다.


45-59: 레이아웃 제약 조건 승인

SnapKit을 사용한 레이아웃 제약 조건이 명확하게 설정되었습니다. titleLabel은 leading에, checkIcon은 trailing에 배치되어 적절한 UI 구조를 형성합니다.


61-64: 셀 설정 메서드 승인

configureCell 메서드가 RoutineLevelType과 선택 상태를 받아 셀을 적절히 업데이트합니다. 체크 아이콘의 표시/숨김 처리가 명확합니다.

Projects/Presentation/Sources/RecommendedRoutine/View/Component/RecommendedRoutineCardView.swift (4)

10-12: delegate 프로토콜 정의 승인

RecommendedRoutineCardViewDelegate 프로토콜이 적절하게 정의되었습니다. 델리게이트 메서드 이름이 명확하고 필요한 매개변수를 포함하고 있습니다.


14-43: 클래스 구조 및 초기화 승인

RecommendedRoutineCardView 클래스 구조가 적절하게 구현되었습니다. Layout enum으로 상수를 관리하고, 초기화 메서드에서 RecommendedRoutine을 받아 적절히 설정합니다.


45-76: UI 속성 설정 승인

UI 요소들의 속성이 적절하게 설정되었습니다. 라벨의 텍스트와 스타일 설정, plusButton의 이미지 및 액션 처리가 잘 구현되어 있습니다. weak self 사용으로 메모리 누수도 방지되었습니다.


78-103: 레이아웃 제약 조건 승인

SnapKit을 사용한 레이아웃 제약 조건이 명확하게 설정되었습니다. labelStackView와 plusButton의 배치가 적절하며, 각 요소의 크기와 위치가 잘 정의되어 있습니다.

Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineCategoryButton.swift (5)

11-32: RoutineCategoryButton 클래스 구조 승인

UIButton을 상속한 클래스 구조가 적절하게 구현되었습니다. isChecked 프로퍼티의 didSet을 통한 자동 업데이트 메커니즘이 잘 설계되어 있습니다.


34-42: UI 속성 초기 설정 승인

배경색과 카테고리 라벨의 초기 속성이 적절하게 설정되었습니다. 폰트와 색상이 디자인 시스템을 따르고 있습니다.


44-52: 레이아웃 설정 승인

코너 반지름과 라벨 중앙 정렬이 적절하게 구현되었습니다. 버튼 높이의 절반으로 설정한 코너 반지름이 캡슐 모양을 만듭니다.


54-61: 버튼 상태 업데이트 로직 승인

체크 상태에 따른 배경색, 폰트, 텍스트 색상 변경 로직이 명확하게 구현되었습니다. 선택된 상태와 비선택 상태의 스타일 차이가 적절합니다.


63-65: 외부 상태 업데이트 메서드 승인

updateButtonState 메서드를 통해 외부에서 버튼 상태를 업데이트할 수 있도록 인터페이스가 제공되어 있습니다.

Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelButton.swift (4)

10-37: RoutineLevelButton 클래스 구조 승인

UIButton을 상속한 클래스 구조가 적절하게 구현되었습니다. Layout enum으로 상수를 관리하고, level 프로퍼티의 didSet을 통한 자동 업데이트 메커니즘이 잘 설계되어 있습니다.


39-60: UI 속성 설정 승인

스택뷰, 라벨, 시브론 아이콘의 속성이 적절하게 설정되었습니다. BitnagilIcon.chevronIcon(direction: .down) 사용과 이미지 크기 조정이 잘 구현되어 있습니다.


62-79: 레이아웃 제약 조건 승인

SnapKit을 사용한 레이아웃 제약 조건이 명확하게 설정되었습니다. 스택뷰의 inset과 각 요소의 크기 설정이 적절합니다.


81-88: 버튼 업데이트 메서드 승인

updateButtonLabel과 updateButton 메서드가 적절하게 구현되었습니다. 외부에서 레벨을 업데이트할 수 있는 인터페이스가 제공되어 있습니다.

Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineCategoryView.swift (2)

26-26: 델리게이트 패턴 구현 우수

weak var delegate를 사용하여 retain cycle을 방지한 것이 좋습니다. 델리게이트 패턴 구현이 적절합니다.


82-87: UI 상태 업데이트 로직 우수

선택된 카테고리에 따라 각 버튼의 체크 상태를 업데이트하는 로직이 명확하고 효율적입니다.

Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelView.swift (1)

70-78: 선택 토글 로직 우수

동일한 레벨을 선택하면 선택 해제하는 토글 로직이 사용자 경험에 좋습니다. 선택 후 즉시 dismiss하는 것도 적절합니다.

Projects/Presentation/Sources/Common/Component/CustomBottomSheet.swift (3)

22-33: 초기화 및 모달 설정 우수

overFullScreen 모달 스타일과 crossDissolve 트랜지션이 바텀시트 UX에 적절하게 설정되어 있습니다.


107-115: 자식 뷰 컨트롤러 관리 우수

addChild, didMove(toParent:) 메서드를 사용한 적절한 뷰 컨트롤러 컨테인먼트 구현입니다.


135-162: 제스처 처리 로직 우수

팬 제스처의 velocity와 position 기반 dismiss 로직이 매우 정교하게 구현되어 있습니다. 인터랙티브한 사용자 경험을 제공합니다.

Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineView.swift (2)

137-151: Combine 바인딩 구현 우수

weak self를 사용한 메모리 안전한 바인딩과 메인 스레드에서의 UI 업데이트가 올바르게 구현되어 있습니다.


185-206: 델리게이트 패턴 구현 우수

세 가지 델리게이트 프로토콜을 통한 깔끔한 컴포넌트 간 통신이 잘 구현되어 있습니다.

Projects/Presentation/Sources/RecommendedRoutine/ViewModel/RecommendedRoutineViewModel.swift (3)

10-34: Input/Output 패턴 및 Combine 사용 우수

ViewModel 프로토콜과 Combine을 활용한 깔끔한 Input/Output 패턴이 잘 구현되어 있습니다. 반응형 프로그래밍의 좋은 예시입니다.


49-55: 카테고리 선택 로직 우수

현재 카테고리와 비교하여 불필요한 업데이트를 방지하는 로직이 효율적입니다.


59-60: 난이도 필터링 모델 미지원 안내
현재 Onboarding 및 Domain 모델(RecommendedRoutine, RecommendedRoutineEntity, RecommendedRoutineDTO)에는 난이도(difficulty/level) 관련 프로퍼티가 정의되어 있지 않습니다. 서버 API가 제공되는 시점에 해당 필드를 추가하고 ViewModel에서 필터링 로직을 구현하시면 됩니다.
이 PR 범위에서는 아래 TODO를 남겨두고, API 연동 이후 구현하는 것으로 충분해 보입니다. 전체 구조(Input/Output 패턴, Combine 사용)도 깔끔합니다.

  • TODO: 난이도 프로퍼티 추가 및 ViewModel에서 레벨 필터링 구현 (API 연동 시점)
  • TODO: 더미 데이터 제거 및 리팩토링

Comment on lines +41 to +43
guard let viewModel = DIContainer.shared.resolve(type: RecommendedRoutineViewModel.self) else {
fatalError("RecommendedViewModel 의존성이 등록되지 않았습니다.")
}

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

의존성 해결 실패 시 더 안전한 에러 처리 방식을 고려해보세요.

fatalError를 사용하면 앱이 크래시되므로, 사용자 경험에 부정적인 영향을 줄 수 있습니다. 이전 학습 내용에 따르면 추후 CustomAlertView를 통한 에러 처리를 계획하고 계신 것으로 보이니, 현재는 임시적인 처리 방식으로 이해하겠습니다.

-        guard let viewModel = DIContainer.shared.resolve(type: RecommendedRoutineViewModel.self) else {
-            fatalError("RecommendedViewModel 의존성이 등록되지 않았습니다.")
-        }
+        guard let viewModel = DIContainer.shared.resolve(type: RecommendedRoutineViewModel.self) else {
+            BitnagilLogger.log(logType: .error, message: "RecommendedRoutineViewModel 의존성이 등록되지 않았습니다.")
+            // 임시 처리: 기본 뷰 또는 에러 뷰 표시
+            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 viewModel = DIContainer.shared.resolve(type: RecommendedRoutineViewModel.self) else {
fatalError("RecommendedViewModel 의존성이 등록되지 않았습니다.")
}
guard let viewModel = DIContainer.shared.resolve(type: RecommendedRoutineViewModel.self) else {
BitnagilLogger.log(logType: .error, message: "RecommendedRoutineViewModel 의존성이 등록되지 않았습니다.")
// 임시 처리: 기본 뷰 또는 에러 뷰 표시
return
}
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Common/TabBarView.swift around lines 41 to 43,
replace the fatalError call used when dependency resolution for
RecommendedRoutineViewModel fails with a safer error handling approach. Instead
of crashing the app, implement a mechanism to show a user-friendly error
message, such as displaying a CustomAlertView or logging the error gracefully,
to improve user experience while maintaining app stability.

Comment on lines +40 to +46
plusIcon.do {
$0.contentMode = .center
$0.image = BitnagilIcon.plusIcon?
.resizeAspectFit(to: CGSize(width: Layout.plusIconImageSize, height: Layout.plusIconImageSize))?
.withRenderingMode(.alwaysTemplate)
$0.tintColor = BitnagilColor.navy400
}

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

이미지 리사이징 체인에서 잠재적인 nil 처리 문제가 있습니다.

resizeAspectFit 메서드가 nil을 반환할 수 있어서, 체인 중간에 실패하면 아이콘이 표시되지 않을 수 있습니다.

-            $0.image = BitnagilIcon.plusIcon?
-                .resizeAspectFit(to: CGSize(width: Layout.plusIconImageSize, height: Layout.plusIconImageSize))?
-                .withRenderingMode(.alwaysTemplate)
+            if let originalImage = BitnagilIcon.plusIcon,
+               let resizedImage = originalImage.resizeAspectFit(to: CGSize(width: Layout.plusIconImageSize, height: Layout.plusIconImageSize)) {
+                $0.image = resizedImage.withRenderingMode(.alwaysTemplate)
+            } else {
+                $0.image = BitnagilIcon.plusIcon?.withRenderingMode(.alwaysTemplate)
+            }
🤖 Prompt for AI Agents
In
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RegisterEmotionButton.swift
around lines 40 to 46, the image resizing chain uses optional chaining that may
result in a nil image if resizeAspectFit returns nil, causing the icon not to
display. To fix this, safely unwrap the result of resizeAspectFit before
applying withRenderingMode and setting the image, ensuring the image is only set
if resizing succeeds.

Comment on lines +82 to +123
// TODO: Dummy Data를 지우세요
private let recommendationRoutines: [RecommendedRoutine] = [
RecommendedRoutine(id: 1, mainTitle: "쓰레기 버리러 나가기", subTitle: "간단한 외출도 의미있는 변화예요."),
RecommendedRoutine(id: 2, mainTitle: "산책하며 노란 물건 찾아보기", subTitle: "가까운 공원까지만 나가도 상쾌해져요."),
RecommendedRoutine(id: 3, mainTitle: "밤 산책하며 노후 가로등 찾기", subTitle: "빛이 희미한 가로등이 있다면 제보해봐요."),
RecommendedRoutine(id: 4, mainTitle: "산책하며 고장난 표지판 찾기", subTitle: "훼손된 표지판을 제보해봐요.")
]

private let outdoorRoutines: [RecommendedRoutine] = [
RecommendedRoutine(id: 1, mainTitle: "나가봐요", subTitle: "나가라꼬!!!", routineCategory: .outdoor),
RecommendedRoutine(id: 2, mainTitle: "나가봐요", subTitle: "나가라꼬!!!", routineCategory: .outdoor),
RecommendedRoutine(id: 3, mainTitle: "나가봐요", subTitle: "나가라꼬!!!", routineCategory: .outdoor),
RecommendedRoutine(id: 4, mainTitle: "나가봐요", subTitle: "나가라꼬!!!", routineCategory: .outdoor)
]

private let wakeupRoutines: [RecommendedRoutine] = [
RecommendedRoutine(id: 1, mainTitle: "일어나요", subTitle: "일어나라꼬!!!", routineCategory: .wakeup),
RecommendedRoutine(id: 2, mainTitle: "일어나요", subTitle: "일어나라꼬!!!", routineCategory: .wakeup)
]

private let connectionRoutines: [RecommendedRoutine] = [
RecommendedRoutine(id: 1, mainTitle: "연결해요", subTitle: "연결하라꼬!!!", routineCategory: .connection)
]

private let restRoutines: [RecommendedRoutine] = [
RecommendedRoutine(id: 1, mainTitle: "쉬어가요", subTitle: "쉬어갑시다아 ~~~ 아아아 ~~", routineCategory: .rest)
]

private let growthRoutines: [RecommendedRoutine] = [
RecommendedRoutine(id: 1, mainTitle: "성장해요", subTitle: "성.장.했.다", routineCategory: .growth),
RecommendedRoutine(id: 2, mainTitle: "성장해요", subTitle: "성.장.했.다", routineCategory: .growth),
RecommendedRoutine(id: 3, mainTitle: "성장해요", subTitle: "성.장.했.다", routineCategory: .growth),
RecommendedRoutine(id: 4, mainTitle: "성장해요", subTitle: "성.장.했.다", routineCategory: .growth),
RecommendedRoutine(id: 5, mainTitle: "성장해요", subTitle: "성.장.했.다", routineCategory: .growth),
RecommendedRoutine(id: 6, mainTitle: "성장해요", subTitle: "성.장.했.다", routineCategory: .growth),
RecommendedRoutine(id: 7, mainTitle: "성장해요", subTitle: "성.장.했.다", routineCategory: .growth),
RecommendedRoutine(id: 8, mainTitle: "성장해요", subTitle: "성.장.했.다", routineCategory: .growth),
RecommendedRoutine(id: 9, mainTitle: "성장해요", subTitle: "성.장.했.다", routineCategory: .growth),
RecommendedRoutine(id: 10, mainTitle: "성장해요", subTitle: "성.장.했.다", routineCategory: .growth),
RecommendedRoutine(id: 11, mainTitle: "성장해요", subTitle: "성.장.했.다", routineCategory: .growth),
RecommendedRoutine(id: 12, mainTitle: "성장해요", subTitle: "성.장.했.다", routineCategory: .growth),
]

@coderabbitai coderabbitai Bot Jul 17, 2025

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

더미 데이터 일관성 개선 제안

더미 데이터에서 routineCategory 프로퍼티가 일부 누락되어 있습니다. 일관성을 위해 모든 더미 데이터에 추가하는 것을 고려해보세요.

 private let recommendationRoutines: [RecommendedRoutine] = [
-    RecommendedRoutine(id: 1, mainTitle: "쓰레기 버리러 나가기", subTitle: "간단한 외출도 의미있는 변화예요."),
+    RecommendedRoutine(id: 1, mainTitle: "쓰레기 버리러 나가기", subTitle: "간단한 외출도 의미있는 변화예요.", routineCategory: .recommendation),
     // ... 다른 항목들도 동일하게 수정
 ]
📝 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
// TODO: Dummy Data를 지우세요
private let recommendationRoutines: [RecommendedRoutine] = [
RecommendedRoutine(id: 1, mainTitle: "쓰레기 버리러 나가기", subTitle: "간단한 외출도 의미있는 변화예요."),
RecommendedRoutine(id: 2, mainTitle: "산책하며 노란 물건 찾아보기", subTitle: "가까운 공원까지만 나가도 상쾌해져요."),
RecommendedRoutine(id: 3, mainTitle: "밤 산책하며 노후 가로등 찾기", subTitle: "빛이 희미한 가로등이 있다면 제보해봐요."),
RecommendedRoutine(id: 4, mainTitle: "산책하며 고장난 표지판 찾기", subTitle: "훼손된 표지판을 제보해봐요.")
]
private let outdoorRoutines: [RecommendedRoutine] = [
RecommendedRoutine(id: 1, mainTitle: "나가봐요", subTitle: "나가라꼬!!!", routineCategory: .outdoor),
RecommendedRoutine(id: 2, mainTitle: "나가봐요", subTitle: "나가라꼬!!!", routineCategory: .outdoor),
RecommendedRoutine(id: 3, mainTitle: "나가봐요", subTitle: "나가라꼬!!!", routineCategory: .outdoor),
RecommendedRoutine(id: 4, mainTitle: "나가봐요", subTitle: "나가라꼬!!!", routineCategory: .outdoor)
]
private let wakeupRoutines: [RecommendedRoutine] = [
RecommendedRoutine(id: 1, mainTitle: "일어나요", subTitle: "일어나라꼬!!!", routineCategory: .wakeup),
RecommendedRoutine(id: 2, mainTitle: "일어나요", subTitle: "일어나라꼬!!!", routineCategory: .wakeup)
]
private let connectionRoutines: [RecommendedRoutine] = [
RecommendedRoutine(id: 1, mainTitle: "연결해요", subTitle: "연결하라꼬!!!", routineCategory: .connection)
]
private let restRoutines: [RecommendedRoutine] = [
RecommendedRoutine(id: 1, mainTitle: "쉬어가요", subTitle: "쉬어갑시다아 ~~~ 아아아 ~~", routineCategory: .rest)
]
private let growthRoutines: [RecommendedRoutine] = [
RecommendedRoutine(id: 1, mainTitle: "성장해요", subTitle: "성.장.했.다", routineCategory: .growth),
RecommendedRoutine(id: 2, mainTitle: "성장해요", subTitle: "성.장.했.다", routineCategory: .growth),
RecommendedRoutine(id: 3, mainTitle: "성장해요", subTitle: "성.장.했.다", routineCategory: .growth),
RecommendedRoutine(id: 4, mainTitle: "성장해요", subTitle: "성.장.했.다", routineCategory: .growth),
RecommendedRoutine(id: 5, mainTitle: "성장해요", subTitle: "성.장.했.다", routineCategory: .growth),
RecommendedRoutine(id: 6, mainTitle: "성장해요", subTitle: "성.장.했.다", routineCategory: .growth),
RecommendedRoutine(id: 7, mainTitle: "성장해요", subTitle: "성.장.했.다", routineCategory: .growth),
RecommendedRoutine(id: 8, mainTitle: "성장해요", subTitle: "성.장.했.다", routineCategory: .growth),
RecommendedRoutine(id: 9, mainTitle: "성장해요", subTitle: "성.장.했.다", routineCategory: .growth),
RecommendedRoutine(id: 10, mainTitle: "성장해요", subTitle: "성.장.했.다", routineCategory: .growth),
RecommendedRoutine(id: 11, mainTitle: "성장해요", subTitle: "성.장.했.다", routineCategory: .growth),
RecommendedRoutine(id: 12, mainTitle: "성장해요", subTitle: "성.장.했.다", routineCategory: .growth),
]
// TODO: Dummy Data를 지우세요
private let recommendationRoutines: [RecommendedRoutine] = [
RecommendedRoutine(id: 1, mainTitle: "쓰레기 버리러 나가기", subTitle: "간단한 외출도 의미있는 변화예요.", routineCategory: .recommendation),
RecommendedRoutine(id: 2, mainTitle: "산책하며 노란 물건 찾아보기", subTitle: "가까운 공원까지만 나가도 상쾌해져요.", routineCategory: .recommendation),
RecommendedRoutine(id: 3, mainTitle: "밤 산책하며 노후 가로등 찾기", subTitle: "빛이 희미한 가로등이 있다면 제보해봐요.", routineCategory: .recommendation),
RecommendedRoutine(id: 4, mainTitle: "산책하며 고장난 표지판 찾기", subTitle: "훼손된 표지판을 제보해봐요.", routineCategory: .recommendation)
]
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 82-82: TODOs should be resolved (Dummy Data를 지우세요)

(todo)

🤖 Prompt for AI Agents
In
Projects/Presentation/Sources/RecommendedRoutine/ViewModel/RecommendedRoutineViewModel.swift
between lines 82 and 123, the dummy data arrays have inconsistent usage of the
routineCategory property; some RecommendedRoutine instances lack this property.
To fix this, add the routineCategory property with the appropriate category
value to all RecommendedRoutine instances to ensure consistency across all dummy
data entries.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

RecommendedRoutine init시에 routineCategory: .recommendation를 default로 잡아주고 있어서 괜찮지 않을까요 ??
또한 더미데이터이므로 추후 서버 로직 붙이면서 삭제될 예정이라 수정할 필요 없을 것 같습니다.

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

@choijungp 맞습니다! RecommendedRoutine 초기화 시 routineCategory.recommendation으로 기본값 설정되어 있어서 명시적으로 지정하지 않아도 정상 동작하겠네요.

그리고 더미 데이터는 임시적이고 서버 로직 연동 시 삭제될 예정이니까 굳이 수정할 필요가 없다는 점도 동의합니다. 실용적인 접근이네요!

설명해주셔서 감사합니다. 🙂


🧠 Learnings used
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 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.

Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#19
File: Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift:0-0
Timestamp: 2025-07-16T10:57:16.559Z
Learning: choijungp는 추후 CustomAlertView를 만들어서 에러 처리를 UI로 해결하도록 수정할 계획을 가지고 있음.

Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#19
File: Projects/Domain/Sources/UseCase/Onboarding/OnboardingUseCase.swift:0-0
Timestamp: 2025-07-16T10:58:30.858Z
Learning: choijungp는 OnboardingUseCase의 convertToDictionary 함수에서 에러 처리 개선을 추후 고민하고 반영할 계획을 가지고 있음.

@taipaise taipaise left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

고생하셨습니다 진짜 UIKit gosu가 되셧군요.. 열심히 따라가볼게요 🥹

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

고생하셨습니다!! 혹시 sheetPresentationController를 고려해보셨을까요?
따로 커스텀 바텀 시트를 VC를 구현하신 이유가 있는지 궁금합니다!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

엇 혹시 sheetPresentationController 이것도 MaxHeight를 설정할 수 있나요 ??
디자인 요구사항에서 딱 최대 높이 이상은 못올라가게 했으면 좋겠다는 의견이 있어서 CustomBottomSheet를 구현했습니다 !! ㅠㅠ

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

제가 알기론느 detent를 이용해서 최대 높이를 정해줄 수 있는것으로 알고 있습니다! 다만 iOS 버전에 따라 조금 다를 수 있을거 같아서 확인이 필요할 것 같네요!
그런 제약 조건이 있는걸 몰랐어요.!.! 커스텀 시트 구현해주셔서 오히려 좋아입니다~

import UIKit

protocol RecommendedRoutineCardViewDelegate: AnyObject {
func recommendedRoutineCardViewDelegate(_ sender: RecommendedRoutineCardView, didTapRecommendedRoutine routine: RecommendedRoutine)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

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

프로토콜 이름에 이미 RecommendedRoutineCardView에 대한 Delegate라는 의미가 포함되어 있어서,
메서드 이름에서는 Delegate라는 단어를 생략해도 충분히 의미가 전달될 것 같습니다!

Suggested change
func recommendedRoutineCardViewDelegate(_ sender: RecommendedRoutineCardView, didTapRecommendedRoutine routine: RecommendedRoutine)
func recommendedRoutineCardView(_ sender: RecommendedRoutineCardView, didTapRecommendedRoutine routine: RecommendedRoutine)
  • 밑에서는 생략하신 것을 보니 진짜 단순 실수인거 같아요!!!

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

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

헐 !! 땡큐베리감사 !!!! 7df9948 커밋에서 수정했습니다 !!

토끼도 못잡은 것을 잡다니 ... 🥺👍🏻 감사합니다 !!!

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 0

♻️ Duplicate comments (1)
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelView.swift (1)

21-26: 중복 초기화 제거 및 성능 최적화 필요

정적 분석 도구가 지적한 대로 nil로 초기화하는 것은 중복입니다. 또한 didSet에서 reloadData()와 델리게이트 호출을 동시에 하면 성능에 영향을 줄 수 있습니다.

- private var selectedLevel: RoutineLevelType? = nil {
+ private var selectedLevel: RoutineLevelType? {
     didSet {
-        levelTableView.reloadData()
         delegate?.routineLevelView(self, didSelectLevel: selectedLevel)
     }
 }

그리고 didSelectRowAt에서 수동으로 reloadData() 호출:

 public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
     // ... existing selection logic ...
+    levelTableView.reloadData()
     dismiss(animated: true)
 }
🧹 Nitpick comments (5)
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelView.swift (2)

59-59: 중복된 정렬 로직 제거 필요

동일한 정렬 로직이 두 곳에서 반복되고 있습니다. 성능 최적화를 위해 computed property로 추출하는 것을 권장합니다.

+ private var sortedLevels: [RoutineLevelType] {
+     return RoutineLevelType.allCases.sorted(by: { $0.id < $1.id })
+ }

 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
     guard let cell = tableView.dequeueReusableCell(withIdentifier: "RoutineLevelCell", for: indexPath) as? RoutineLevelCell
     else { return UITableViewCell() }
-    let level = RoutineLevelType.allCases.sorted(by: { $0.id < $1.id })[indexPath.row]
+    let level = sortedLevels[indexPath.row]
     
     let isSelected = selectedLevel == level
     cell.configureCell(level: level, isSelected: isSelected)
     return cell
 }

 func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
-    let selectedLevel = RoutineLevelType.allCases.sorted(by: { $0.id < $1.id })[indexPath.row]
+    let selectedLevel = sortedLevels[indexPath.row]
     if self.selectedLevel == selectedLevel {
         self.selectedLevel = nil
     } else {
         self.selectedLevel = selectedLevel
     }
     dismiss(animated: true)
 }

Also applies to: 71-71


38-38: 셀 식별자 상수화 권장

하드코딩된 문자열 대신 상수로 관리하는 것이 유지보수성 측면에서 더 좋습니다.

+ private enum Constants {
+     static let cellIdentifier = "RoutineLevelCell"
+ }

 private func configureAttribute() {
     levelTableView.delegate = self
     levelTableView.dataSource = self
-    levelTableView.register(RoutineLevelCell.self, forCellReuseIdentifier: "RoutineLevelCell")
+    levelTableView.register(RoutineLevelCell.self, forCellReuseIdentifier: Constants.cellIdentifier)
 }

 func tableView(_ tableView: UITableView, cellForRowAt indexPath: IndexPath) -> UITableViewCell {
-    guard let cell = tableView.dequeueReusableCell(withIdentifier: "RoutineLevelCell", for: indexPath) as? RoutineLevelCell
+    guard let cell = tableView.dequeueReusableCell(withIdentifier: Constants.cellIdentifier, for: indexPath) as? RoutineLevelCell
     else { return UITableViewCell() }
Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineView.swift (3)

194-197: TODO 주석 해결 필요

루틴 등록 화면으로의 네비게이션 로직이 구현되지 않았습니다. 현재 임시로 print문만 사용하고 있습니다.

이 TODO를 해결하기 위한 이슈를 생성하거나 구현 코드를 제공해드릴까요?


39-43: 초기화 최적화 제안

cancellables 초기화를 더 간결하게 할 수 있습니다.

- private var cancellables: Set<AnyCancellable>
+ private var cancellables = Set<AnyCancellable>()

 public override init(viewModel: RecommendedRoutineViewModel) {
-    cancellables = []
     super.init(viewModel: viewModel)
 }

153-173: 메모리 효율성 및 가독성 개선

현재 구현에서 개선할 수 있는 부분들이 있습니다.

 private func fetchRecommendedRoutines(recommendedRoutines: [RecommendedRoutine]) {
+    clearExistingRoutines()
+    setupRoutineCards(recommendedRoutines)
+    
+    if shouldShowEmotionButton(recommendedRoutines) {
+        showEmotionButton()
+    }
+ }
+ 
+ private func clearExistingRoutines() {
     recommendedRoutineStackView.arrangedSubviews.forEach { view in
         recommendedRoutineStackView.removeArrangedSubview(view)
         view.removeFromSuperview()
     }
     recommendedRoutineCards.removeAll()
+ }
+ 
+ private func setupRoutineCards(_ recommendedRoutines: [RecommendedRoutine]) {
     for routine in recommendedRoutines {
         let routineCard = RecommendedRoutineCardView(recommendedRoutine: routine)
         recommendedRoutineCards[routine.id] = routineCard
         recommendedRoutineStackView.addArrangedSubview(routineCard)
         routineCard.delegate = self
         routineCard.snp.makeConstraints { make in
             make.height.equalTo(Layout.routineCardHeight)
         }
     }
+ }
+ 
+ private func shouldShowEmotionButton(_ recommendedRoutines: [RecommendedRoutine]) -> Bool {
+     return !recommendedRoutines.isEmpty && recommendedRoutines[0].routineCategory == .recommendation
+ }

-    if !recommendedRoutines.isEmpty && recommendedRoutines[0].routineCategory == .recommendation {
-        showEmotionButton()
-    }
 }
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between c2b6e84 and 7df9948.

📒 Files selected for processing (3)
  • Projects/Presentation/Sources/RecommendedRoutine/View/Component/RecommendedRoutineCardView.swift (1 hunks)
  • Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelView.swift (1 hunks)
  • Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineView.swift (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • Projects/Presentation/Sources/RecommendedRoutine/View/Component/RecommendedRoutineCardView.swift
🧰 Additional context used
🧠 Learnings (3)
📓 Common learnings
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 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.
Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#19
File: Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift:0-0
Timestamp: 2025-07-16T10:57:16.559Z
Learning: choijungp는 추후 CustomAlertView를 만들어서 에러 처리를 UI로 해결하도록 수정할 계획을 가지고 있음.
Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#19
File: Projects/Domain/Sources/UseCase/Onboarding/OnboardingUseCase.swift:0-0
Timestamp: 2025-07-16T10:58:30.858Z
Learning: choijungp는 OnboardingUseCase의 convertToDictionary 함수에서 에러 처리 개선을 추후 고민하고 반영할 계획을 가지고 있음.
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelView.swift (1)
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 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.
Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineView.swift (3)
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 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.
Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.
Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.
🪛 SwiftLint (0.57.0)
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelView.swift

[Warning] 21-21: Initializing an optional variable with nil is redundant

(redundant_optional_initialization)

Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineView.swift

[Warning] 195-195: 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 (2)
Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineView.swift (2)

137-151: 메모리 누수 방지 확인

Combine 구독 관리가 적절히 구현되어 있습니다. weak self를 사용하여 강한 참조 순환을 방지하고 있습니다.


85-87: 바텀시트 프레젠테이션 구현 확인

새로운 CustomBottomSheet 컴포넌트를 적절히 활용하고 있습니다. 사용자 경험 측면에서 바텀시트 높이 설정도 적절합니다.

- RoutineLevelView 테이블 뷰 셀 밑줄 간격 조정 및 마지막 셀 밑줄 생략
- CustomBottomSheet 투명도 변경

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

Actionable comments posted: 2

♻️ Duplicate comments (1)
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelView.swift (1)

21-26: 중복 초기화 제거 및 성능 최적화 필요

정적 분석 도구가 지적한 대로 nil로 초기화하는 것은 중복입니다. 또한 didSet에서 델리게이트 호출을 바로 하면 성능에 영향을 줄 수 있습니다.

- private var selectedLevel: RoutineLevelType? = nil {
+ private var selectedLevel: RoutineLevelType? {
     didSet {
-        levelTableView.reloadData()
         delegate?.routineLevelView(self, didSelectLevel: selectedLevel)
     }
 }

그리고 didSelectRowAt에서 수동으로 reloadData() 호출:

 public func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
     // ... existing selection logic ...
+    levelTableView.reloadData()
     dismiss(animated: true)
 }
🧹 Nitpick comments (2)
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelView.swift (2)

70-78: 캐시된 정렬 결과를 일관되게 사용하세요.

다른 메서드들과 일관성을 위해 캐시된 sortedLevels를 사용하세요.

 func tableView(_ tableView: UITableView, willDisplay cell: UITableViewCell, forRowAt indexPath: IndexPath) {
-    let isLastCell = indexPath.row == RoutineLevelType.allCases.count - 1
+    let isLastCell = indexPath.row == sortedLevels.count - 1

80-90: 캐시된 정렬 결과를 사용하여 일관성을 유지하세요.

다른 메서드들과 마찬가지로 캐시된 sortedLevels를 사용하세요.

 func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
-    let selectedLevel = RoutineLevelType.allCases.sorted(by: { $0.id < $1.id })[indexPath.row]
+    let selectedLevel = sortedLevels[indexPath.row]
     if self.selectedLevel == selectedLevel {
         self.selectedLevel = nil
     } else {
         self.selectedLevel = selectedLevel
     }
     levelTableView.reloadData()
     dismiss(animated: true)
 }
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between 7df9948 and 66526c9.

📒 Files selected for processing (2)
  • Projects/Presentation/Sources/Common/Component/CustomBottomSheet.swift (1 hunks)
  • Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelView.swift (1 hunks)
🚧 Files skipped from review as they are similar to previous changes (1)
  • Projects/Presentation/Sources/Common/Component/CustomBottomSheet.swift
🧰 Additional context used
🧠 Learnings (2)
📓 Common learnings
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 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.
Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#19
File: Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift:0-0
Timestamp: 2025-07-16T10:57:16.559Z
Learning: choijungp는 추후 CustomAlertView를 만들어서 에러 처리를 UI로 해결하도록 수정할 계획을 가지고 있음.
Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#19
File: Projects/Domain/Sources/UseCase/Onboarding/OnboardingUseCase.swift:0-0
Timestamp: 2025-07-16T10:58:30.858Z
Learning: choijungp는 OnboardingUseCase의 convertToDictionary 함수에서 에러 처리 개선을 추후 고민하고 반영할 계획을 가지고 있음.
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelView.swift (1)
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 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.
🔇 Additional comments (1)
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelView.swift (1)

10-12: 델리게이트 프로토콜 정의가 적절합니다.

Swift 네이밍 컨벤션을 잘 따르고 있고, 메서드 시그니처가 명확합니다.

Comment on lines +35 to +47
private func configureAttribute() {
levelTableView.delegate = self
levelTableView.dataSource = self
levelTableView.register(RoutineLevelCell.self, forCellReuseIdentifier: "RoutineLevelCell")
}

private func configureLayout() {
view.addSubview(levelTableView)

levelTableView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}

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

셀 식별자를 상수로 정의하여 안전성을 높이세요.

문자열 리터럴을 직접 사용하면 오타로 인한 런타임 에러가 발생할 수 있습니다.

+ private enum Constants {
+     static let cellIdentifier = "RoutineLevelCell"
+ }

 private func configureAttribute() {
     levelTableView.delegate = self
     levelTableView.dataSource = self
-    levelTableView.register(RoutineLevelCell.self, forCellReuseIdentifier: "RoutineLevelCell")
+    levelTableView.register(RoutineLevelCell.self, forCellReuseIdentifier: Constants.cellIdentifier)
 }
📝 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() {
levelTableView.delegate = self
levelTableView.dataSource = self
levelTableView.register(RoutineLevelCell.self, forCellReuseIdentifier: "RoutineLevelCell")
}
private func configureLayout() {
view.addSubview(levelTableView)
levelTableView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
private enum Constants {
static let cellIdentifier = "RoutineLevelCell"
}
private func configureAttribute() {
levelTableView.delegate = self
levelTableView.dataSource = self
levelTableView.register(RoutineLevelCell.self, forCellReuseIdentifier: Constants.cellIdentifier)
}
private func configureLayout() {
view.addSubview(levelTableView)
levelTableView.snp.makeConstraints { make in
make.edges.equalToSuperview()
}
}
🤖 Prompt for AI Agents
In
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelView.swift
around lines 35 to 47, the cell reuse identifier "RoutineLevelCell" is used as a
string literal, which risks runtime errors due to typos. Define this identifier
as a private constant string at the top of the class or file, then replace all
occurrences of the literal with this constant to improve safety and
maintainability.

@choijungp
choijungp merged commit 211de3a into develop Jul 17, 2025
2 checks passed
@choijungp
choijungp deleted the feat/recommend-view branch July 17, 2025 15:39
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