Skip to content

[Feat-T3-105] 홈 화면 UI 구현#23

Merged
choijungp merged 13 commits into
developfrom
feat/homeView
Jul 26, 2025
Merged

[Feat-T3-105] 홈 화면 UI 구현#23
choijungp merged 13 commits into
developfrom
feat/homeView

Conversation

@choijungp

@choijungp choijungp commented Jul 24, 2025

Copy link
Copy Markdown
Contributor

🌁 Background

홈 화면 UI를 구현하였습니다.
서버 통신 로직은 다음 PR에서 구현하겠습니다. !!! (+ 플로팅 버튼, 루틴 상세 뷰)


📱 Screenshot

홈 화면 (Default)

iPhone SE3 iPhone 13 mini iPhone 16 Pro

홈 화면 (EmptyView)

iPhone SE3 iPhone 13 mini iPhone 16 Pro

홈 화면 (루틴 리스트 최대 높이)

iPhone SE3 iPhone 13 mini iPhone 16 Pro

루틴 정렬 Bottom Sheet

iPhone SE3 iPhone 13 mini iPhone 16 Pro

전체 흐름

Simulator.Screen.Recording.-.iPhone.13.mini.-.2025-07-24.at.22.35.53.mp4

👩‍💻 Contents

  • 메인 루틴 UI 컴포넌트: MainRoutineView 구현
  • 서브 루틴 UI 컴포넌트: SubRoutineButton 구현
    • 서브 루틴은 피그마상 전체 영역이 터치영역으로 잡혀있어서 View가 아닌 Button으로 구현하였습니다.
  • 루틴 UI 컴포넌트 : RoutineView 구현
    • 메인 루틴 + 서브 루틴으로 조합된 RoutineView를 구현하였습니다.
  • WeekView, DateView 구현
    • DateView는 요일 + DateButton으로 구성된 날짜 뷰이고,
    • WeekView는 상단 년 + 월 Label + 주를 바꾸는 버튼 + 한 주의 DateView들로 이루어져 있습니다.
  • 루틴이 없을 때 보여지는 EmptyView: HomeEmptyView 구현
  • 감정 구슬 등록 버튼: HomeRegisterEmotionButton 구현
    • 추천 루틴의 RegisterEmotionButton과 역할은 같지만 UI가 다른 이슈로 네이밍을 살짝 다르게 했습니다.
  • 선택지 SelectableItemTableView 구현
    • 추천 루틴의 난이도 필터링, 홈 화면의 루틴 정렬, 추후 2차 기능에 포함될 반복 주기가 같은 UI 형태를 가지고 있어 이를 재사용할 수 있는 SelectableItemTableViewSelectableItemCell로 구현하였습니다.

✅ Testing

TabBarView를 진입점으로 하여 HomeView의 UI · UX만 확인했습니다.


📝 Review Note

1. SelectableItemTableView 재사용

홈 화면의 루틴 정렬 추천 루틴 화면의 루틴 난이도 필터링 루틴 등록의 반복 주기 설정

홈 화면의 루틴 정렬, 추천 루틴 화면의 루틴 난이도 필터링, 루틴 등록의 반복 주기 설정 UI가 많이 유사하다고 생각해 재사용할 수 있을 것이라 생각했습니다.
따라서 SelectableItem 프로토콜을 만들어 SelectableItemTableView에서 SelectableItem를 채택한 제네릭 타입들이 사용할 수 있도록 구현하였습니다.

protocol SelectableItem {
    var id: Int { get }
    var title: String { get }
}

따라서 기존 RoutineLevelCell, RoutineLevelView를 삭제하고 SelectableItemTableView로 사용하였습니다.
2차에 루틴 반복주기 설정 시에도 해당 뷰를 재사용하면 좋을 것 같아요 !!!


2. SelectableItemTableView 제네릭 이슈
1번째에 이어서 SelectableItemTableView 관련 이슈입니다 ..!!
재사용성을 위해 SelectableItemTableView를 제네릭 타입을 받도록 구현하였습니다.

final class SelectableItemTableView<T: SelectableItem & CaseIterable & Equatable>: UIViewController, UITableViewDelegate, UITableViewDataSource {
    // 구현 세부사항
}

근데 제네릭을 사용한 class의 extension에서 @objc 함수를 사용할 수 없다는 사실을 아셨나요 ..?
(저는 진짜 몰랐음 .. 제가 지금도 잘못 인지한 거일수도 있음 ... )

컨벤션을 맞추기 위해 SelectableItemTableView의 extension에서 UITableViewDelegate, UITableViewDataSource를 채택해주고 싶었지만 다음과 같은 에러가 발생했습니다.

extension of generic classes cannot contain @objc memebers
satisfying requirement for instance methode tableView(_: numberOfRowsInSection:) in protocol UITableViewDelegate

따라서 어쩔 수 없이 extension이 아닌 SelectableItemTableView 내부에 tableView delegate, datasource 관련 함수를 구현해주었습니다.
(MARK 주석으로 나누긴 했어요 !!!)


그리고 또한 제네릭 타입 내부에서 static 저장 프로퍼티를 사용할 수 없대유 ...

static stored properties not supported in generic types

그래서 원래 private enum Layout으로 정의했던 값들을 SelectableItemTableView에서는 어쩔 수 없이 빼서 사용했습니다.

만약 제가 해당 오류들을 잘못인지하고 있던 것이라면 알려주세요 ... 🥺 (책임 나누기 아님)


3. Delegate와 closure
HomeView에서 가지고 있는 서브 뷰들이 꽤 많은 편입니다 !!!
따라서 Delegate 혹은 Closure를 통해 터치 이벤트를 처리해주려고 했는데요 .....

HomeView -> RoutineView -> MainRoutineView 이런 식으로 이벤트가 들어가다 보니 delegate의 delegate를 써야 하는 경우가 있었습니다. (이때 다소 불편하다고 생각이 들었음)

따라서 처리해야 하는 이벤트가 하나라면 delegate가 아닌 상위 뷰에서 closure로 이벤트를 전달받게 하는 것도 좋지 않을까 ? 라는 생각을 했습니다 !!!!

그래서 DateView를 보면 didTappedDateButton가 있고, 그것을 상위 WeekView에서 dateView 사용할 때 설정해주는 식으로 구현했습니다.

// DateView 프로퍼티
var didTappedDateButton: ((Date) -> Void)?


// WeekView의 일부분 ..
let dateView = DateView(date: date,
                        isSelected: isSelected,
                        isToday: isToday)
dateView.didTappedDateButton = { [weak self] date in
    self?.selectDate(date: date)
}
dateViews[date] = dateView
dateStackView.addArrangedSubview(dateView)
dateView.snp.makeConstraints { make in
    make.height.equalTo(Layout.dateViewHeight)
}

근데 줏대 없이 이곳에서는 delegate, 어느 곳에서는 closure를 사용한 것 같아 혼돈이 올 수 있을 것 같아 리뷰 노트에 적어놓습니다....


4. RoutineView의 intrinsicContentSize
HomeView에서 routineStackView에 RoutineView들을 추가해주려고 할 때, height를 지정해줘야 Layout이 잘 잡히더라고요 ..
하지만 RoutineView는 서브 루틴의 여부, 서브 루틴의 개수에 따라 높이가 유동적으로 달라질 수 있습니다.

따라서 RoutineView의 intrinsicContentSize를 override해서 RoutineView의 고유 사이즈를 계산해주었습니다. !!!

override var intrinsicContentSize: CGSize {
    let baseHeight = Layout.timeLabelHeight + Layout.mainRoutineViewTopSpacing + Layout.mainRoutineViewHeight

    if routine.subRoutines.isEmpty {
        return CGSize(width: UIView.noIntrinsicMetric, height: baseHeight)
    } else {
        let subRoutineHeight = Layout.subRoutineLabelTopSpacing + Layout.subRoutineLabelHeight + Layout.subRoutineStackViewTopSpacing + (CGFloat(routine.subRoutines.count) * Layout.subRoutineViewHeight)
        return CGSize(width: UIView.noIntrinsicMetric, height: baseHeight + subRoutineHeight)
    }
}

그리고.. .. 이렇게 크게 가져가고 싶지 않었는데 ... UI가 복잡하다 보니 PR이 커진 것 같아요 ㅠ.ㅠ
정말 죄송합니다 ... 어떻게든 끊어보고 싶긴 했는데 ..... 천천히 봐주셔요 ... 🥺 감사합니띵 !!


📣 Related Issue

  • close #T3-105

Summary by CodeRabbit

  • 신규 기능

    • 홈 화면 및 루틴 관리 기능이 대폭 추가되었습니다. 주간 캘린더, 루틴 리스트, 루틴 정렬, 감정구슬 등록 버튼, 루틴 및 서브루틴 완료 체크, 루틴 상세/추가 액션 등 다양한 UI 컴포넌트가 도입되었습니다.
    • 날짜 포맷 변환, 선택형 아이템 테이블, 루틴 정렬 방식 선택 등 공통 컴포넌트가 추가되었습니다.
    • 루틴 및 서브루틴 데이터 모델이 도입되었습니다.
    • 신규 아이콘(ellipsis, information, sort)이 추가되었습니다.
  • 기능 개선

    • 홈 화면이 뷰 컨트롤러에서 뷰 기반 구조로 전환되었습니다.
    • 루틴 난이도 선택 뷰가 제네릭 선택 테이블로 교체되어 재사용성이 향상되었습니다.
    • ViewModel에 Combine 기반 반응형 데이터 바인딩이 적용되어 닉네임 및 루틴 리스트가 실시간으로 갱신됩니다.
  • 버그 수정

    • 루틴 난이도 관련 텍스트 속성명이 일관성 있게 변경되었습니다.
  • 기타

    • 불필요한 파일(RoutineLevelView, HomeViewController 등)이 삭제되었습니다.

choijungp added 12 commits July 23, 2025 20:07
- epllipsis 아이콘 에셋 추가 및 BitnagilIcon 프로퍼티 추가
- MainRoutineView 구현
- Layout enum 추가
- 불필요한 do 문법 제거
- MainRoutine 구조체 폴더 위치 변경
- informationIcon, sortIcon HomeView에서 필요한 이미지 에셋 추가
- SelectableItem protocol 정의 (정렬, 필터링 시 선택지)
- SelectableItemCell UI 구현
- SelectableItemTableView UI 구현
- RoutineLevelView, RoutineLevelCell 삭제 -> SelectableItemTableView 재사용
- RoutineSortType 정의
@choijungp
choijungp requested a review from taipaise July 24, 2025 14:15
@choijungp choijungp self-assigned this Jul 24, 2025
@coderabbitai

coderabbitai Bot commented Jul 24, 2025

Copy link
Copy Markdown

"""

Walkthrough

이번 변경은 홈 화면의 주요 UI 및 데이터 구조를 대대적으로 리팩터링하고, 공통 선택 리스트, 루틴 및 서브루틴 뷰 컴포넌트, 날짜 및 주간 선택 기능, 디자인 시스템 아이콘 추가, 모델 및 뷰모델 확장 등 대규모 기능 개발이 포함됩니다. 기존 홈 컨트롤러와 난이도 선택 뷰는 제거되고, 새로운 구조로 대체되었습니다.

Changes

파일/경로 그룹 변경 요약
.../Images.xcassets/*_icon.imageset/Contents.json ellipsis, information, sort 아이콘 이미지셋 JSON 파일 신규 추가
.../BitnagilIcon.swift ellipsisIcon, informationIcon, sortIcon 정적 프로퍼티 추가
.../SelectableItem.swift SelectableItem 프로토콜 신설 (id, title 속성)
.../SelectableItemCell.swift, .../SelectableItemTableView.swift 셀 클래스 및 테이블 뷰: 난이도 선택 → 제네릭 선택 아이템 구조로 리팩터링, SelectableItem 기반
.../Date+.swift Date 확장: 다양한 포맷 문자열 변환 메소드 및 enum 추가
.../TabBarView.swift DI 컨테이너에서 뷰모델 주입, 홈/추천/마이페이지 탭만 유지, Report 탭 제거
.../Home/Model/*.swift MainRoutine, SubRoutine, RoutineSortType 등 홈 루틴 관련 모델 신규 추가
.../Home/View/Component/*.swift DateView, WeekView, HomeEmptyView, HomeRegisterEmotionButton, MainRoutineView, RoutineView, SubRoutineButton 등 홈 화면용 UI 컴포넌트 대거 추가
.../Home/View/HomeView.swift 홈 메인 화면 신규 구현: 닉네임, 감정구슬, 주간/일간 루틴, 정렬, bottom sheet, 제스처 등 포함
.../Home/ViewModel/HomeViewModel.swift 뷰모델: Combine 기반 Input/Output, 닉네임/루틴 reactive publisher, 샘플 데이터, 일간 루틴 fetch 등 구현
.../Onboarding/View/OnboardingRecommendedRoutineView.swift 온보딩 완료 후 홈 이동 시 HomeViewControllerHomeView로 변경
.../RecommendedRoutine/Model/RoutineLevelType.swift SelectableItem 프로토콜 채택, levelTitletitle로 명칭 변경
.../RecommendedRoutine/View/Component/RoutineLevelButton.swift levelTitle 참조 → title로 변경
.../RecommendedRoutine/View/RecommendedRoutineView.swift 난이도 선택 뷰: 커스텀 뷰 → 제네릭 선택 테이블 뷰로 대체, delegate 메소드 변경
.../RecommendedRoutine/View/Component/RoutineLevelView.swift 기존 난이도 선택 뷰 및 delegate 프로토콜 파일 삭제
.../Home/View/HomeViewController.swift 기존 홈 뷰컨트롤러 파일 삭제

Sequence Diagram(s)

sequenceDiagram
    participant User
    participant HomeView
    participant HomeViewModel
    participant RoutineView
    participant MainRoutineView
    participant SubRoutineButton

    User->>HomeView: 앱 실행/화면 진입
    HomeView->>HomeViewModel: action(.loadNickname)
    HomeView->>HomeViewModel: action(.fetchRoutines)
    HomeViewModel-->>HomeView: nicknamePublisher, routinesPublisher emit
    HomeView->>RoutineView: 루틴 리스트 렌더링
    User->>WeekView: 날짜/주 이동
    WeekView->>HomeView: didSelectDate
    HomeView->>HomeViewModel: action(.fetchDailyRoutines(date))
    HomeViewModel-->>HomeView: routinesPublisher emit
    HomeView->>RoutineView: 루틴 리스트 갱신
    User->>RoutineView: 메인/서브루틴 체크/해제
    RoutineView->>HomeView: delegate 호출 (체크/해제)
    HomeView->>HomeViewModel: (TODO) 서버 통신 예정
Loading
sequenceDiagram
    participant User
    participant HomeView
    participant SelectableItemTableView
    participant RoutineSortType

    User->>HomeView: 정렬 버튼 터치
    HomeView->>SelectableItemTableView: present bottom sheet
    User->>SelectableItemTableView: 정렬 옵션 선택
    SelectableItemTableView->>HomeView: delegate(didSelectItem)
    HomeView->>HomeViewModel: (TODO) 정렬 적용
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45분

  • 신규 파일, 모델, UI 컴포넌트, 뷰모델, delegate 패턴, 제네릭, Combine, 제스처, 샘플 데이터, 기존 코드 삭제 등 다양한 영역에 걸친 중간~고난도 변경이므로 상당한 리뷰 시간이 필요합니다.

Possibly related PRs

Poem

🐇
새로운 홈에 햇살이 비추네,
루틴과 감정, 한 주의 길 따라
토끼는 탭하고, 날짜를 넘기며
체크와 정렬, 셀렉트도 척척!
코드밭에 새싹이 솟아나듯
오늘도 빛나길,
리뷰도 화이팅!
🌱✨
"""

Note

⚡️ Unit Test Generation is now available in beta!

Learn more here, or try it out under "Finishing Touches" below.


📜 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 968c361 and 34d5477.

📒 Files selected for processing (1)
  • Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift (1 hunks)
🧰 Additional context used
🪛 SwiftLint (0.57.0)
Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift

[Warning] 49-49: TODOs should be resolved (Repository 혹은 UseCase와 연동해야 합니...)

(todo)


[Warning] 54-54: TODOs should be resolved (서버 통신 로직으로 교체해야 합니다.)

(todo)

🔇 Additional comments (6)
Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift (6)

12-16: Input enum 설계가 명확합니다.

각 케이스의 목적이 분명하고, associated value를 통한 날짜별 루틴 조회도 적절하게 설계되어 있습니다.


18-21: Output struct의 반응형 설계가 적절합니다.

AnyPublisher를 사용한 타입 이레이저와 Never 에러 타입 사용이 적절하며, 반응형 아키텍처에 잘 맞는 구조입니다.


23-33: 프로퍼티 설계와 초기화가 잘 되어 있습니다.

과거 리뷰에서 지적된 routinesSuject 오타가 수정되었고, CurrentValueSubject 사용과 접근 제어자 설정이 적절합니다.


35-46: action 메서드의 구조가 깔끔합니다.

switch문을 통한 각 Input 케이스 처리가 명확하고, associated value 전달도 적절하게 구현되어 있습니다.


48-51: TODO 주석이 명확합니다.

현재 UI 구현 단계에서는 하드코딩된 데이터 사용이 적절하며, Repository/UseCase 연동에 대한 TODO 주석이 명확하게 향후 작업을 안내하고 있습니다.


98-105: 날짜별 루틴 조회 로직이 적절합니다.

Date extension을 활용한 문자열 변환, guard문을 통한 nil 체크, 그리고 적절한 fallback 처리가 잘 구현되어 있습니다.

✨ 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/homeView

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 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: 7

🧹 Nitpick comments (6)
Projects/Presentation/Sources/Common/Protocol/SelectableItem.swift (1)

8-11: 프로토콜 설계가 깔끔하고 재사용성을 잘 고려했습니다.

제네릭 UI 컴포넌트에서 사용할 수 있도록 간단하고 명확한 인터페이스를 제공합니다. 다만 프로토콜에 대한 문서화 주석을 추가하는 것을 고려해보세요.

+/// 선택 가능한 아이템을 나타내는 프로토콜
+/// SelectableItemTableView 등의 제네릭 UI 컴포넌트에서 사용됩니다.
 protocol SelectableItem {
+    /// 아이템의 고유 식별자
     var id: Int { get }
+    /// 화면에 표시될 아이템의 제목
     var title: String { get }
 }
Projects/Presentation/Sources/Common/Extension/Date+.swift (1)

10-17: 날짜 포맷팅 유틸리티가 잘 구현되었지만 성능 최적화를 고려해보세요.

한국 로케일 설정과 다양한 날짜 형식 지원이 적절합니다. 다만 매번 새로운 DateFormatter 인스턴스를 생성하는 것은 성능에 영향을 줄 수 있습니다.

DateFormatter를 재사용하도록 개선해보세요:

 extension Date {
+    private static let sharedFormatter = DateFormatter()
+    
     func convertToString(dateType: DateType) -> String {
-        let formatter = DateFormatter()
-        formatter.locale = Locale(identifier: "ko_KR")
-        formatter.dateFormat = dateType.formatString
+        Date.sharedFormatter.locale = Locale(identifier: "ko_KR")
+        Date.sharedFormatter.dateFormat = dateType.formatString
 
-        return formatter.string(from: self)
+        return Date.sharedFormatter.string(from: self)
     }
Projects/Presentation/Sources/Common/TabBarView.swift (1)

41-52: 의존성 주입과 뷰 설정이 적절히 구현되었습니다.

TabBarView의 변경사항들이 좋습니다:

  • DIContainer를 통한 의존성 해결 패턴이 명확
  • HomeView로의 마이그레이션이 일관성 있게 적용
  • 뷰모델 변수명이 명확하고 구분이 잘 됨
  • 각 탭의 네비게이션 컨트롤러 래핑이 적절

의존성 등록 실패 시 fatalError 사용은 개발 단계에서는 적절하지만, 프로덕션에서는 더 graceful한 에러 처리를 고려해볼 수 있습니다.

Projects/Presentation/Sources/Home/View/Component/SubRoutineButton.swift (1)

1-7: 파일 헤더의 파일명이 잘못되었습니다.

파일명은 SubRoutineButton.swift인데 헤더에는 SubRoutineView.swift로 되어 있습니다.

-//  SubRoutineView.swift
+//  SubRoutineButton.swift
Projects/Presentation/Sources/Home/View/Component/MainRoutineView.swift (1)

105-110: 고정된 버튼 너비로 인한 텍스트 잘림 가능성

checkButton의 너비가 165로 고정되어 있어 긴 루틴 제목의 경우 텍스트가 잘릴 수 있습니다.

다음 중 하나를 고려해보세요:

  1. 동적 너비 사용 (최소/최대 너비 제약)
  2. mainLabel에 줄임표(...) 처리 추가:
 mainLabel.text = mainRoutine.title
 mainLabel.font = BitnagilFont(style: .subtitle1, weight: .semiBold).font
 mainLabel.textColor = BitnagilColor.navy500
+mainLabel.lineBreakMode = .byTruncatingTail
Projects/Presentation/Sources/Home/View/HomeView.swift (1)

92-93: TODO 항목 구현 지원

PR 설명에 따르면 서버 통신 로직은 다음 PR에서 구현 예정인 것으로 이해됩니다.

TODO 항목들의 구현이 필요하시면 도움을 드릴 수 있습니다. 특히:

  • 툴팁 뷰 구현
  • 감정 등록 화면 네비게이션
  • 서버 통신 로직
  • Bottom Sheet 구현

이슈를 생성해 추적하시겠습니까?

Also applies to: 100-101, 115-116, 315-316, 319-320, 324-325, 331-332, 338-339

📜 Review details

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

📥 Commits

Reviewing files that changed from the base of the PR and between 21db344 and 968c361.

⛔ Files ignored due to path filters (9)
  • Projects/Presentation/Resources/Images.xcassets/ellipsis_icon.imageset/ellipsis_icon.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/ellipsis_icon.imageset/ellipsis_icon@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/ellipsis_icon.imageset/ellipsis_icon@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/information_icon.imageset/information_icon.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/information_icon.imageset/information_icon@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/information_icon.imageset/information_icon@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/sort_icon.imageset/sort_icon.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/sort_icon.imageset/sort_icon@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/sort_icon.imageset/sort_icon@3x.png is excluded by !**/*.png
📒 Files selected for processing (27)
  • Projects/Presentation/Resources/Images.xcassets/ellipsis_icon.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Images.xcassets/information_icon.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Resources/Images.xcassets/sort_icon.imageset/Contents.json (1 hunks)
  • Projects/Presentation/Sources/Common/Component/SelectableItemCell.swift (3 hunks)
  • Projects/Presentation/Sources/Common/Component/SelectableItemTableView.swift (1 hunks)
  • Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift (1 hunks)
  • Projects/Presentation/Sources/Common/Extension/Date+.swift (1 hunks)
  • Projects/Presentation/Sources/Common/Protocol/SelectableItem.swift (1 hunks)
  • Projects/Presentation/Sources/Common/TabBarView.swift (1 hunks)
  • Projects/Presentation/Sources/Home/HomeViewController.swift (0 hunks)
  • Projects/Presentation/Sources/Home/Model/MainRoutine.swift (1 hunks)
  • Projects/Presentation/Sources/Home/Model/RoutineSortType.swift (1 hunks)
  • Projects/Presentation/Sources/Home/Model/SubRoutine.swift (1 hunks)
  • Projects/Presentation/Sources/Home/View/Component/DateView.swift (1 hunks)
  • Projects/Presentation/Sources/Home/View/Component/HomeEmptyView.swift (1 hunks)
  • Projects/Presentation/Sources/Home/View/Component/HomeRegisterEmotionButton.swift (1 hunks)
  • Projects/Presentation/Sources/Home/View/Component/MainRoutineView.swift (1 hunks)
  • Projects/Presentation/Sources/Home/View/Component/RoutineView.swift (1 hunks)
  • Projects/Presentation/Sources/Home/View/Component/SubRoutineButton.swift (1 hunks)
  • Projects/Presentation/Sources/Home/View/Component/WeekView.swift (1 hunks)
  • Projects/Presentation/Sources/Home/View/HomeView.swift (1 hunks)
  • Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift (1 hunks)
  • Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift (1 hunks)
  • Projects/Presentation/Sources/RecommendedRoutine/Model/RoutineLevelType.swift (2 hunks)
  • Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelButton.swift (2 hunks)
  • Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelView.swift (0 hunks)
  • Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineView.swift (3 hunks)
💤 Files with no reviewable changes (2)
  • Projects/Presentation/Sources/Home/HomeViewController.swift
  • Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelView.swift
🧰 Additional context used
🧠 Learnings (8)
Projects/Presentation/Sources/Home/View/Component/HomeEmptyView.swift (3)

Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift:57-59
Timestamp: 2025-07-16T09:21:15.038Z
Learning: OnboardingRecommendedRoutineView에서 viewWillAppear에 registerOnboarding 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.

Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.

Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.

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

Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift:57-59
Timestamp: 2025-07-16T09:21:15.038Z
Learning: OnboardingRecommendedRoutineView에서 viewWillAppear에 registerOnboarding 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.

Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.

Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.

Projects/Presentation/Sources/Home/View/Component/RoutineView.swift (1)

Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift:57-59
Timestamp: 2025-07-16T09:21:15.038Z
Learning: OnboardingRecommendedRoutineView에서 viewWillAppear에 registerOnboarding 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.

Projects/Presentation/Sources/Home/View/HomeView.swift (3)

Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift:57-59
Timestamp: 2025-07-16T09:21:15.038Z
Learning: OnboardingRecommendedRoutineView에서 viewWillAppear에 registerOnboarding 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.

Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.

Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.

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

Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift:57-59
Timestamp: 2025-07-16T09:21:15.038Z
Learning: OnboardingRecommendedRoutineView에서 viewWillAppear에 registerOnboarding 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.

Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.

Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.

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

Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift:57-59
Timestamp: 2025-07-16T09:21:15.038Z
Learning: OnboardingRecommendedRoutineView에서 viewWillAppear에 registerOnboarding 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.

Projects/Presentation/Sources/Home/View/Component/MainRoutineView.swift (1)

Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift:57-59
Timestamp: 2025-07-16T09:21:15.038Z
Learning: OnboardingRecommendedRoutineView에서 viewWillAppear에 registerOnboarding 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.

Projects/Presentation/Sources/Common/Component/SelectableItemCell.swift (2)

Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.

Learnt from: choijungp
PR: #19
File: Projects/Presentation/Sources/Login/View/TermsAgreementView.swift:44-46
Timestamp: 2025-07-16T09:09:13.869Z
Learning: BaseViewController의 viewDidLoad() 메서드에서 이미 configureAttribute(), configureLayout(), bind()를 호출하므로, 하위 클래스에서 super.viewDidLoad()를 호출하면 이 메서드들이 자동으로 호출된다. 따라서 하위 클래스에서 추가로 호출할 필요가 없다.

🧬 Code Graph Analysis (4)
Projects/Presentation/Sources/Home/View/Component/HomeRegisterEmotionButton.swift (2)
Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift (1)
  • chevronIcon (20-22)
Projects/Presentation/Sources/Common/Extension/UIImage+.swift (1)
  • resize (12-17)
Projects/Presentation/Sources/Common/TabBarView.swift (1)
Projects/Shared/Sources/DIContainer/DIContainer.swift (1)
  • resolve (18-25)
Projects/Presentation/Sources/Home/View/Component/DateView.swift (2)
Projects/Presentation/Sources/Home/View/Component/WeekView.swift (4)
  • configureAttribute (52-79)
  • configureLayout (81-111)
  • selectDate (163-169)
  • updateSelectState (155-160)
Projects/Presentation/Sources/Common/Extension/Date+.swift (1)
  • convertToString (11-17)
Projects/Presentation/Sources/Common/Component/SelectableItemCell.swift (1)
Projects/Presentation/Sources/Common/Extension/UIImage+.swift (1)
  • resizeAspectFit (20-35)
🪛 SwiftLint (0.57.0)
Projects/Presentation/Sources/Home/View/HomeView.swift

[Warning] 92-92: TODOs should be resolved (툴팁 뷰를 보여줘야 합니다.)

(todo)


[Warning] 100-100: TODOs should be resolved (감정 등록 화면으로 이동해야 합니다.)

(todo)


[Warning] 115-115: TODOs should be resolved (감정 등록 화면으로 이동해야 합니다.)

(todo)


[Warning] 315-315: TODOs should be resolved (메인 루틴 완료 버튼 체크의 서버 통신을 수행해야 합니...)

(todo)


[Warning] 319-319: TODOs should be resolved (더보기 Bottom Sheet)

(todo)


[Warning] 324-324: TODOs should be resolved (서브 루틴 완료 버튼 체크의 서버 통신을 수행해야 합니...)

(todo)


[Warning] 331-331: TODOs should be resolved (완료 · 미완료 루틴을 정렬해야 합니다.)

(todo)


[Warning] 338-338: TODOs should be resolved (그 전 주 혹은 다음 주 데이터 받아와야 합니다.)

(todo)

Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift

[Warning] 49-49: TODOs should be resolved (Repository 혹은 UseCase와 연동해야 합니...)

(todo)


[Warning] 54-54: TODOs should be resolved (서버 통신 로직으로 교체해야 합니다.)

(todo)

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

1-24: 표준 iOS 애셋 카탈로그 구조를 잘 따르고 있습니다.

JSON 구조가 Apple의 표준 애셋 카탈로그 형식을 올바르게 따르고 있으며, 1x, 2x, 3x 스케일 변형과 universal idiom 설정이 적절합니다.

Projects/Presentation/Sources/Common/Extension/Date+.swift (1)

19-36: DateType 열거형이 다양한 UI 요구사항을 잘 커버합니다.

한국어 형식(yyyy년 M월)과 표준 ISO 형식을 적절히 조합하여 UI 컴포넌트에서 필요한 모든 날짜 표현을 지원합니다. 각 케이스의 포맷 문자열이 올바르게 설정되어 있습니다.

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

47-47: SelectableItem 프로토콜 준수를 위한 프로퍼티 명 변경이 올바르게 적용되었습니다.

levelTitle에서 title로의 변경이 일관되게 적용되었으며, 기본 텍스트도 한국어로 적절히 설정되어 있습니다.

Also applies to: 82-82

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

1-24: 표준 iOS 애셋 카탈로그 구조가 일관되게 적용되었습니다.

다른 아이콘들과 동일한 구조로 올바르게 구성되어 있으며, Apple의 표준 애셋 카탈로그 형식을 따르고 있습니다.

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

23-25: 새로운 아이콘 추가가 올바르게 구현되었습니다.

기존 패턴을 잘 따르고 있습니다. 다만 일부 기존 아이콘들은 .withRenderingMode(.alwaysTemplate)을 사용하는데, 새로운 아이콘들은 사용하지 않고 있습니다. 디자인 요구사항에 따른 의도적인 선택인지 확인해보시기 바랍니다.

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

228-228: 홈 화면 리팩터링에 맞춘 깔끔한 변경입니다.

HomeViewController에서 HomeView로의 변경이 PR의 홈 화면 UI 구현 목표와 일치합니다. 기존 로직은 그대로 유지하면서 새로운 뷰 컨트롤러로 교체한 것이 적절합니다.

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

8-8: SelectableItem 프로토콜 준수로 재사용성이 향상되었습니다.

새로운 제네릭 SelectableItemTableView 인프라와 통합되어 코드 재사용성과 일관성이 크게 개선되었습니다.


21-21: 프로토콜 요구사항에 맞는 적절한 프로퍼티명 변경입니다.

levelTitle에서 title로 변경하여 SelectableItem 프로토콜의 요구사항을 충족했습니다. 기존 기능은 그대로 유지하면서 프로토콜 표준을 따른 좋은 리팩터링입니다.

Projects/Presentation/Sources/Home/Model/SubRoutine.swift (1)

8-12: 간결하고 명확한 데이터 모델 설계입니다.

서브루틴을 위한 필수 속성들만 포함하고 있으며, Hashable 프로토콜 준수로 컬렉션 작업과 UI 업데이트에 최적화되었습니다. isDone 속성이 var로 선언된 것도 완료 상태 추적에 적합합니다.

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

1-24: 표준 iOS 애셋 카탈로그 구성입니다.

1x, 2x, 3x 스케일 변형과 universal idiom을 사용한 표준적이고 올바른 구성입니다. 다양한 디바이스 해상도에서 적절히 표시될 것입니다.

Projects/Presentation/Sources/Home/Model/MainRoutine.swift (1)

10-16: 잘 설계된 데이터 모델입니다.

MainRoutine 구조체가 적절하게 설계되었습니다:

  • 불변 속성(id, startTime, title)과 가변 속성(isDone, subRoutines)의 구분이 명확함
  • Swift 네이밍 컨벤션을 잘 따르고 있음
  • 루틴의 핵심 데이터를 잘 표현하고 있음
Projects/Presentation/Sources/Home/Model/RoutineSortType.swift (1)

8-25: SelectableItem 프로토콜을 잘 구현한 열거형입니다.

RoutineSortType 열거형이 올바르게 구현되었습니다:

  • SelectableItem과 CaseIterable 프로토콜을 적절히 준수
  • 고유한 ID 값 할당 (complete: 1, incomplete: 2)
  • 명확한 한국어 제목 제공
  • 코드가 간결하고 읽기 쉬움
Projects/Presentation/Sources/Home/View/Component/HomeRegisterEmotionButton.swift (1)

10-58: 잘 구현된 커스텀 UIButton 컴포넌트입니다.

HomeRegisterEmotionButton 구현이 우수합니다:

  • Layout 열거형으로 레이아웃 상수를 잘 분리
  • SnapKit을 사용한 명확한 제약 조건 설정
  • 디자인 시스템(BitnagilIcon, BitnagilFont, BitnagilColor) 일관성 있게 사용
  • 이미지 리사이징과 템플릿 모드 적용이 적절
  • 커스텀 뷰에 적합한 초기화 방식 사용
Projects/Presentation/Sources/Common/Component/SelectableItemCell.swift (1)

10-60: 제네릭 설계로의 리팩토링이 잘 되었습니다.

SelectableItemCell로의 변경사항들이 우수합니다:

  • SelectableItem 프로토콜 사용으로 재사용성 향상
  • configureCell 메서드가 제네릭 아이템을 받도록 개선
  • 이미지 설정에서 resizeAspectFit 사용으로 종횡비 유지
  • Layout 열거형으로 상수 관리가 명확
  • 전반적으로 깔끔하고 유지보수하기 좋은 구조
Projects/Presentation/Sources/Home/View/Component/HomeEmptyView.swift (1)

10-88: 깔끔한 빈 화면 컴포넌트 구현입니다.

컴포넌트가 잘 구조화되어 있고 기존 패턴을 잘 따르고 있습니다. 메모리 관리, 레이아웃 상수 정리, 디자인 시스템 사용 등이 모두 적절합니다.

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

34-34: 제네릭 컴포넌트로의 리팩터링이 잘 되었습니다.

기존의 특화된 RoutineLevelView를 재사용 가능한 제네릭 SelectableItemTableView로 교체한 것이 코드 재사용성 측면에서 좋은 개선입니다.


199-207: 제네릭 델리게이트 메서드 구현이 안전합니다.

타입 캐스팅을 guard 문으로 안전하게 처리하고 있어 런타임 오류를 방지합니다.

Projects/Presentation/Sources/Home/View/Component/DateView.swift (1)

11-105: 날짜 표시 컴포넌트가 잘 구현되었습니다.

Date extension을 활용한 날짜 포맷팅, 선택 상태 관리, 클로저 기반 콜백 패턴 등이 모두 적절하게 구현되어 있습니다. 메모리 관리와 UI 업데이트 로직도 깔끔합니다.

Projects/Presentation/Sources/Home/View/Component/WeekView.swift (3)

114-124: 주간 시작일 계산 로직이 정확합니다.

월요일을 주간 시작일로 하는 계산이 올바르게 구현되어 있습니다. 일요일(weekday == 1) 케이스도 적절히 처리되었습니다.


127-152: DateView 동적 생성 및 메모리 관리가 적절합니다.

기존 뷰들을 제거한 후 새로운 DateView 인스턴스들을 생성하는 방식이 메모리 효율적입니다. 약한 참조를 사용한 클로저도 메모리 누수를 방지합니다.


177-188: 주간 이동 로직과 델리게이트 호출이 적절합니다.

주간 이동 시 선택된 날짜를 주간 시작일로 설정하고, 델리게이트에게 주간 이동과 날짜 선택을 모두 알리는 것이 적절한 설계입니다.

Projects/Presentation/Sources/Common/Component/SelectableItemTableView.swift (1)

31-31: 항목 정렬 구현 확인

초기화 시 항목들을 ID 기준으로 정렬하여 일관된 순서를 보장하는 좋은 구현입니다.

Projects/Presentation/Sources/Home/View/Component/RoutineView.swift (1)

56-65: 동적 높이 계산 구현 우수

intrinsicContentSize를 오버라이드하여 서브루틴 개수에 따른 동적 높이 계산을 잘 구현했습니다.

Projects/Presentation/Sources/Home/View/HomeView.swift (3)

79-82: 그라데이션 레이어 구현 우수

viewDidLayoutSubviews에서 프레임을 업데이트하여 회전 및 크기 변경에 올바르게 대응합니다.

Also applies to: 231-238


313-316: 낙관적 UI 업데이트 시 에러 처리 고려

UI를 먼저 업데이트하는 낙관적 접근은 좋지만, 서버 통신 실패 시 원복 처리가 필요합니다.

서버 통신 구현 시 실패 케이스에 대한 롤백 로직을 추가하는 것을 고려하세요:

// 예시
viewModel.updateRoutineState(mainRoutine, isDone: !mainRoutine.isDone) { [weak self] success in
    if !success {
        // 실패 시 UI 원복
        self?.sender.updateMainRoutineState(isDone: mainRoutine.isDone)
    }
}

Also applies to: 322-325


271-271: 제약 조건 접근 시 안전성 개선 필요

layoutConstraints.first에 직접 접근하는 것은 안전하지 않을 수 있습니다.

nil 체크를 추가하거나 기본값을 사용하세요:

-let currentTop = contentViewTopConstraint?.layoutConstraints.first?.constant ?? Layout.collapsedTop
+let currentTop = contentViewTopConstraint?.layoutConstraints.first?.constant ?? Layout.collapsedTop

실제로 이미 기본값을 사용하고 있으므로 현재 구현이 안전합니다.

Comment on lines +89 to +98
func tableView(_ tableView: UITableView, didSelectRowAt indexPath: IndexPath) {
let selectedItem = items[indexPath.row]
if self.selectedItem == selectedItem {
self.selectedItem = nil
} else {
self.selectedItem = selectedItem
}
itemTableView.reloadData()
dismiss(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

선택 후 자동 dismiss 동작 개선 필요

항목 선택 시 자동으로 dismiss되는 동작이 항상 원하는 동작이 아닐 수 있습니다.

delegate가 dismiss 여부를 제어할 수 있도록 개선하는 것을 고려해보세요:

protocol SelectableItemTableViewDelegate: AnyObject {
    func selectableItemTableView<T: SelectableItem>(_ sender: SelectableItemTableView<T>, didSelectItem: T?)
    func selectableItemTableViewShouldDismiss<T: SelectableItem>(_ sender: SelectableItemTableView<T>) -> Bool
}

그리고 didSelectRowAt에서:

 itemTableView.reloadData()
-dismiss(animated: true)
+if delegate?.selectableItemTableViewShouldDismiss(self) ?? true {
+    dismiss(animated: true)
+}
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Common/Component/SelectableItemTableView.swift
around lines 89 to 98, the current implementation always dismisses the view
controller after an item is selected, which may not be desired in all cases. To
fix this, introduce a delegate method selectableItemTableViewShouldDismiss that
returns a Bool to decide whether to dismiss. Modify didSelectRowAt to call this
delegate method and only call dismiss(animated: true) if it returns true,
allowing the delegate to control the dismiss behavior.

Comment on lines +63 to +66
checkButton.addAction(UIAction { [weak self] _ in
guard let self else { return }
self.delegate?.mainRoutineView(self, didTapCheckButton: mainRoutine)
}, for: .touchUpInside)

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

클로저에서 mainRoutine 캡처 문제

클로저에서 mainRoutine을 직접 캡처하고 있어, mainRoutine 프로퍼티가 업데이트되어도 클로저는 여전히 초기화 시점의 인스턴스를 참조합니다.

다음과 같이 수정하세요:

 checkButton.addAction(UIAction { [weak self] _ in
     guard let self else { return }
-    self.delegate?.mainRoutineView(self, didTapCheckButton: mainRoutine)
+    self.delegate?.mainRoutineView(self, didTapCheckButton: self.mainRoutine)
 }, for: .touchUpInside)
 moreButton.addAction(UIAction { [weak self] _ in
     guard let self else { return }
-    self.delegate?.mainRoutineView(self, didTapMoreButton: mainRoutine)
+    self.delegate?.mainRoutineView(self, didTapMoreButton: self.mainRoutine)
 }, for: .touchUpInside)

Also applies to: 81-84

🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Home/View/Component/MainRoutineView.swift
around lines 63 to 66 and also lines 81 to 84, the closure captures the
mainRoutine property directly, causing it to hold the initial instance even if
mainRoutine updates later. To fix this, capture mainRoutine as a local constant
inside the closure before using it, ensuring the closure uses the current value
when executed rather than the value at initialization.

Comment on lines +136 to +147
for subRoutine in routine.subRoutines {
let subRoutineView = SubRoutineButton(subRoutine: subRoutine)
subRoutineButtons[subRoutine.id] = subRoutineView
subRoutineView.addAction(UIAction { [weak self] _ in
guard let self else { return }
self.delegate?.routineView(self, didTapSubRoutineCheckButton: subRoutine)
}, for: .touchUpInside)
subRoutineStackView.addArrangedSubview(subRoutineView)
subRoutineView.snp.makeConstraints { make in
make.height.equalTo(Layout.subRoutineViewHeight)
}
}

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

서브루틴 버튼 클로저 캡처 문제

클로저에서 subRoutine을 직접 캡처하고 있어, 배열이 변경되어도 클로저는 초기 인스턴스를 참조합니다.

다음과 같이 수정하는 것을 고려하세요:

 subRoutineView.addAction(UIAction { [weak self] _ in
     guard let self else { return }
-    self.delegate?.routineView(self, didTapSubRoutineCheckButton: subRoutine)
+    if let currentSubRoutine = self.routine.subRoutines.first(where: { $0.id == subRoutine.id }) {
+        self.delegate?.routineView(self, didTapSubRoutineCheckButton: currentSubRoutine)
+    }
 }, for: .touchUpInside)
📝 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
for subRoutine in routine.subRoutines {
let subRoutineView = SubRoutineButton(subRoutine: subRoutine)
subRoutineButtons[subRoutine.id] = subRoutineView
subRoutineView.addAction(UIAction { [weak self] _ in
guard let self else { return }
self.delegate?.routineView(self, didTapSubRoutineCheckButton: subRoutine)
}, for: .touchUpInside)
subRoutineStackView.addArrangedSubview(subRoutineView)
subRoutineView.snp.makeConstraints { make in
make.height.equalTo(Layout.subRoutineViewHeight)
}
}
for subRoutine in routine.subRoutines {
let subRoutineView = SubRoutineButton(subRoutine: subRoutine)
subRoutineButtons[subRoutine.id] = subRoutineView
subRoutineView.addAction(UIAction { [weak self] _ in
guard let self else { return }
if let currentSubRoutine = self.routine.subRoutines.first(where: { $0.id == subRoutine.id }) {
self.delegate?.routineView(self, didTapSubRoutineCheckButton: currentSubRoutine)
}
}, for: .touchUpInside)
subRoutineStackView.addArrangedSubview(subRoutineView)
subRoutineView.snp.makeConstraints { make in
make.height.equalTo(Layout.subRoutineViewHeight)
}
}
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Home/View/Component/RoutineView.swift around
lines 136 to 147, the closure inside the loop captures the loop variable
subRoutine directly, causing it to reference the initial instance even if the
array changes. To fix this, capture subRoutine as a constant inside the loop by
assigning it to a local let constant before the closure, then use that constant
inside the closure to ensure the correct instance is referenced.

Comment on lines +159 to +168
func updateSubRoutineState(subRoutine: SubRoutine, isDone: Bool) {
guard let index = routine.subRoutines.firstIndex(where: { $0.id == subRoutine.id })
else { return }
routine.subRoutines[index].isDone.toggle()

let isDone = routine.subRoutines[index].isDone
if let subRoutineView = subRoutineButtons[subRoutine.id] {
subRoutineView.updateState(isDone: isDone)
}
}

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

서브루틴 상태 업데이트 로직 개선 필요

뷰에서 직접 모델을 변경하는 것은 좋은 패턴이 아니며, toggle 후 다시 읽는 로직도 불필요합니다.

상태 변경은 delegate를 통해 처리하고, 뷰는 표시만 담당하도록 수정:

-func updateSubRoutineState(subRoutine: SubRoutine, isDone: Bool) {
-    guard let index = routine.subRoutines.firstIndex(where: { $0.id == subRoutine.id })
-    else { return }
-    routine.subRoutines[index].isDone.toggle()
-
-    let isDone = routine.subRoutines[index].isDone
+func updateSubRoutineState(subRoutineId: Int, isDone: Bool) {
     if let subRoutineView = subRoutineButtons[subRoutineId] {
         subRoutineView.updateState(isDone: isDone)
     }
 }
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Home/View/Component/RoutineView.swift around
lines 159 to 168, the updateSubRoutineState function directly modifies the model
and toggles the isDone state, then reads it again, which is not a good pattern.
Refactor this method to remove direct model mutation and toggling; instead,
notify a delegate about the requested state change passing the subRoutine and
the desired isDone value. The view should only update its display based on the
delegate's response or external model updates, not change the model itself.

Comment on lines +68 to +70
func updateState(isDone: Bool) {
subRoutine.isDone = isDone
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

💡 Verification agent

🧩 Analysis chain

SubRoutine 모델의 가변성을 확인해주세요.

updateState 메서드에서 subRoutine.isDone을 직접 수정하고 있는데, SubRoutine이 구조체라면 이 방식이 예상대로 동작하지 않을 수 있습니다.

다음 스크립트로 SubRoutine 모델의 정의를 확인해주세요:


🏁 Script executed:

#!/bin/bash
# SubRoutine 모델의 정의 확인
ast-grep --pattern 'struct SubRoutine {
  $$$
}'

ast-grep --pattern 'class SubRoutine {
  $$$
}'

Length of output: 100


🏁 Script executed:

#!/bin/bash
# SubRoutine 타입 정의 위치를 전체 코드베이스에서 검색
rg -R "^\s*(struct|class|enum|protocol)\s+SubRoutine\b" -n .

Length of output: 467


#!/bin/bash
# SubRoutine 정의가 포함된 파일 검색
files=$(find . -type f -name "*SubRoutine*.swift")
echo "Found files:"
echo "$files"

# 각 파일에서 SubRoutine 타입 선언 확인
for f in $files; do
  echo "---- Inspecting: $f ----"
  grep -En "^\s*(struct|class|enum|protocol)\s+SubRoutine\b" "$f" || echo "No declaration found in $f"
done

#!/bin/bash
# SubRoutine 타입 정의 검색 (Swift 파일 전체 탐색)
rg "struct SubRoutine" -n .
rg "class SubRoutine" -n .
rg "enum SubRoutine" -n .
rg "protocol SubRoutine" -n .

🏁 Script executed:

#!/bin/bash
# 모든 Swift 파일에서 SubRoutine 타입 선언 검색
grep -R -E "^\s*(struct|class)\s+SubRoutine\b" -n .

Length of output: 241


#!/bin/bash
# SubRoutineButton.swift 파일의 상위 200줄을 출력하여 updateState 메서드와 subRoutine 선언 확인
sed -n '1,200p' Projects/Presentation/Sources/Home/View/Component/SubRoutineButton.swift

🏁 Script executed:

#!/bin/bash
# SubRoutineButton.swift와 SubRoutine.swift의 선언부 및 메서드 확인
echo "=== Projects/Presentation/Sources/Home/View/Component/SubRoutineButton.swift ==="
sed -n '1,200p' Projects/Presentation/Sources/Home/View/Component/SubRoutineButton.swift
echo
echo "=== Projects/Presentation/Sources/Home/Model/SubRoutine.swift ==="
sed -n '1,200p' Projects/Presentation/Sources/Home/Model/SubRoutine.swift

Length of output: 2644


SubRoutine이 값 타입(struct)이라 내부 복사본에만 변경이 적용됩니다.

SubRoutineButtonsubRoutine 프로퍼티는 값 타입인 SubRoutine의 복사본이므로,
updateState(isDone:)에서 subRoutine.isDone = isDone을 호출해도 뷰 내부에서만 상태가 변경되고 외부에는 반영되지 않습니다.

수정이 필요한 부분:

  • 외부(뷰 모델 또는 상위 컨테이너)에 변경된 상태를 통지하도록 API를 변경
  • 또는 updateState가 변경된 SubRoutine을 반환하도록 구현

예시 1) 변경된 모델 반환

func updateState(isDone: Bool) -> SubRoutine {
    var updated = subRoutine
    updated.isDone = isDone
    subRoutine = updated
    return updated
}

예시 2) delegate/closure로 알림

var onStateChanged: ((SubRoutine) -> Void)?

func updateState(isDone: Bool) {
    var updated = subRoutine
    updated.isDone = isDone
    subRoutine = updated
    onStateChanged?(updated)
}

파일: Projects/Presentation/Sources/Home/View/Component/SubRoutineButton.swift
라인: 68–70

🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Home/View/Component/SubRoutineButton.swift at
lines 68 to 70, the updateState(isDone:) method modifies a copy of the value
type SubRoutine, so changes do not propagate outside the view. To fix this,
either change updateState to return the updated SubRoutine after modifying a
local copy and assigning it back to subRoutine, or add a delegate or closure
property (e.g., onStateChanged) that is called with the updated SubRoutine to
notify external components of the change.

Comment thread Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift Outdated
Comment thread Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift

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

오늘도 정말 고생하셨습니다~! 꼭! 짚고 넘어갈 부분은 없는 것 같아서 어푸루브드립니다~

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.

범용적으로 사용할 수 있는 테이블 뷰가 있는 VC를 구현해주셨군요! 해당 VC를 구현해주신 이유가 궁금합니다.
홈 화면 이외에 비슷한 레이아웃으로 특정 항목을 선택할 수 있는 VC가 있는 걸까요?
추가로 이미 구현되어 있는 BaseViewController와 다르게 ViewModel을 가지고 있지 않은 것 같습니다. 어떠한 경우에 이 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.

앗 !! 리뷰 노트에 1. SelectableItemTableView 재사용 내용을 참고하시면 좋을 것 같아요 !!

홈 화면의 루틴 정렬, 추천 루틴 화면의 루틴 난이도 필터링, 루틴 등록의 반복 주기 설정에서 동일한 UI에 선택 화면이 비슷하여 재사용할 수 있을 것이라 생각하여
기존 추천 루틴 화면의 루틴 난이도 필터링에서 사용했던 RoutineLevelCell, RoutineLevelView를 삭제하고 SelectableItemCell, SelectableItemTableView를 구현했습니다 !!!

그래서 현재 홈 화면의 루틴 정렬, 추천 루틴 화면의 루틴 난이도 필터링에서 재사용해서 사용하고 있습니다 !!!

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.

어제 너무 급해서 리뷰 노트를 보는 것도 깜빡하고 넘어갔습니다 고생해서 쓰셨을텐데 진짜 죄송합니다ㅜㅜㅜ
2차 까지 생각하시고 구현하신 것이군요!! 말씀해주신 오류들은 저도 덕분에 이번에 처음 알았어요!
답변 감사합니다~!

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.

다이죠부네 ~~~~ 띵 백번 이해합니다 !!

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.

빡빡한 스케줄임에도 제스쳐까지..고생하셨습니다 ㅜㅜ

updateAttribute()
}
}
var didTappedDateButton: ((Date) -> Void)?

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.

외부에서 직접 해당 메서드를 접근해서 초기화할 수 있게 하신 이유가 있을까요? delegate가 아니라 이런 방법을 사용하신 이유가 궁금합니다!
추가적으로 이후에 해당 메서드가 실행될 때, DateView를 프로퍼티로 가지고 있는 WeekView의 monthLabel을 갱신하는 작업이 일어나는 것 같습니다.
DateView의 내부에서 WeekView의 monthLabel을 갱신하고 있는 구조가 아주 조금은 어색한거 같아 리뷰 남깁니다!!
제가 잘못 이해한 것일 수 있으니 편하게 말씀해주시면 감사하겠습니다! 🥹

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.

요거슨 !! 리뷰 노트 3. Delegate와 closure 고민에 적어두었는데요 !!!

간략하게 다시 말씀드리자면 .. 우선 MainRoutineView, RoutineView, HomeView를 구현하면서 ...
MainRoutineView의 delegate를 RoutineView가 채택하고 있고,
RoutineView의 delegate를 HomeView를 채택하고 있어. .. Delegate의 delegate가 반복되는 느낌이라 불편하다고 느꼈습니다 !!

따라서 차라리 뷰의 이벤트 처리가 1개라면 간단히 클로저로 받아서 처리하면 어떨까 ... 라는 생각에 DateView에서는 Closure로 받아서 이벤트 처리를 하도록 하였습니다 !!!

DateView가 눌리면 selectedDate가 바뀌고 이때 달이 바뀔 가능성이 있어서 monthLabel을 업데이트 하도록 처리하고 있습니다 ㅜ.ㅜ ...
제 생각은 didTappedDateButton을 통해서 dateButton의 이벤트 처리만 하고 있고, monthLabel의 업데이트 로직은 사실상 WeekView에서 하고 있다고 생각했는데 !!!! 혹시 그렇게 와닿지는 않으신건가요 ?? 🥺

구현에 휩싸여서 개인적 생각에 갇혀 있는 것일수도 ...

특히 WeekView에 대해서는 로직이 View에 많이 포함되어 있는 것 같아 고민이 많습니다 ㅠㅠ ............................
HomeViewModel을 구현해서 로직을 좀 옮길 수 있다면 옮겨보도록 할게유 .....

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.

작년 두번째 스프린트에서 View도 추상화의 대상이 될 수 있다라는 피드백을 기억하시나요? 당시에 전화기의 키패드를 예시로 들어서 설명해주셨었는데요,
1. 전화기 키패드의 동작 방법을 모두 외부에서 구현 (번호 구성하는 로직이 외부에서 이뤄짐)
2. 내부적으로 번화번호를 구성하는 방법을 구현. 나중에 전화걸기 버튼을 누르면 구성한 번호를 전달
요 피드백과 의미가 통하는 부분이 있다고 생각합니다!

말씀해주신 것처럼 불필요한 delegate가 생겨날 수 있는 구조라는 말씀에 공감합니다. 또 결국 DateView가 WeekView안의 서브 뷰로 존재하게 될텐데, DateView를 외부에서 알 수 있는 구조도 아니라 괜찮은 것 같네요! 정리하자면 외부에서는 내부적으로 어떻게 동작할 지 신경쓰지 않고 WeekView의 delegate만 알고 사용하면되는 구조라 좋다고 생각합니다.

다만 View의 추상화를 통해 View가 View자신의 동작 로직을 일부 가져가게 된다면, 이것을 어떻게 처리해야할 지는 같이 좀 더 고민해보면 좋을거 같아요..View가 멍청해질 수록 명확하게 역할분리를 했다는 것이니까요!! 정말 할 수록 선택하고 고민해야하는 부분이 많아지는 것 같습니다~!

다시 한 번 리뷰 노트 사과드립니다 ㅠ 다음번부터 꼼꼼히 읽어볼게요,.,

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.

네네네네 덕분에 같이 고민해볼 수 있어서 오히려 좋아 입니다 ~ ㅎ.ㅎ 일단 머지 고고 하고 더 고민 깊생 해보겠습니다 !!!

@taipaise

Copy link
Copy Markdown
Collaborator

다시 봐도 홈 뷰가 레전드네요.. 고생하셨습니다 정말

@choijungp
choijungp merged commit b28498b into develop Jul 26, 2025
2 checks passed
@choijungp
choijungp deleted the feat/homeView branch July 26, 2025 02:28
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