[Feat-T3-105] 홈 화면 UI 구현#23
Conversation
- epllipsis 아이콘 에셋 추가 및 BitnagilIcon 프로퍼티 추가 - MainRoutineView 구현
- Layout enum 추가 - 불필요한 do 문법 제거 - MainRoutine 구조체 폴더 위치 변경
- informationIcon, sortIcon HomeView에서 필요한 이미지 에셋 추가
- SelectableItem protocol 정의 (정렬, 필터링 시 선택지) - SelectableItemCell UI 구현 - SelectableItemTableView UI 구현 - RoutineLevelView, RoutineLevelCell 삭제 -> SelectableItemTableView 재사용 - RoutineSortType 정의
|
""" Walkthrough이번 변경은 홈 화면의 주요 UI 및 데이터 구조를 대대적으로 리팩터링하고, 공통 선택 리스트, 루틴 및 서브루틴 뷰 컴포넌트, 날짜 및 주간 선택 기능, 디자인 시스템 아이콘 추가, 모델 및 뷰모델 확장 등 대규모 기능 개발이 포함됩니다. 기존 홈 컨트롤러와 난이도 선택 뷰는 제거되고, 새로운 구조로 대체되었습니다. Changes
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) 서버 통신 예정
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) 정렬 적용
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45분
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 detailsConfiguration used: .coderabbit.yaml 📒 Files selected for processing (1)
🧰 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)
✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. Note: Be mindful of the bot's finite context window. It's strongly recommended to break down tasks such as reading entire modules into smaller chunks. For a focused discussion, use review comments to chat about specific files and their changes, instead of using the PR comments. CodeRabbit Commands (Invoked using PR comments)
Other keywords and placeholders
Documentation and Community
|
There was a problem hiding this comment.
Actionable comments posted: 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.swiftProjects/Presentation/Sources/Home/View/Component/MainRoutineView.swift (1)
105-110: 고정된 버튼 너비로 인한 텍스트 잘림 가능성
checkButton의 너비가 165로 고정되어 있어 긴 루틴 제목의 경우 텍스트가 잘릴 수 있습니다.다음 중 하나를 고려해보세요:
- 동적 너비 사용 (최소/최대 너비 제약)
mainLabel에 줄임표(...) 처리 추가:mainLabel.text = mainRoutine.title mainLabel.font = BitnagilFont(style: .subtitle1, weight: .semiBold).font mainLabel.textColor = BitnagilColor.navy500 +mainLabel.lineBreakMode = .byTruncatingTailProjects/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
⛔ Files ignored due to path filters (9)
Projects/Presentation/Resources/Images.xcassets/ellipsis_icon.imageset/ellipsis_icon.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/ellipsis_icon.imageset/ellipsis_icon@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/ellipsis_icon.imageset/ellipsis_icon@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/information_icon.imageset/information_icon.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/information_icon.imageset/information_icon@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/information_icon.imageset/information_icon@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/sort_icon.imageset/sort_icon.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/sort_icon.imageset/sort_icon@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/sort_icon.imageset/sort_icon@3x.pngis 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실제로 이미 기본값을 사용하고 있으므로 현재 구현이 안전합니다.
| 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) | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| checkButton.addAction(UIAction { [weak self] _ in | ||
| guard let self else { return } | ||
| self.delegate?.mainRoutineView(self, didTapCheckButton: mainRoutine) | ||
| }, for: .touchUpInside) |
There was a problem hiding this comment.
클로저에서 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.
| 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) | ||
| } | ||
| } |
There was a problem hiding this comment.
서브루틴 버튼 클로저 캡처 문제
클로저에서 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.
| 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.
| 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) | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ 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.
| func updateState(isDone: Bool) { | ||
| subRoutine.isDone = isDone | ||
| } |
There was a problem hiding this comment.
💡 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.swiftLength of output: 2644
SubRoutine이 값 타입(struct)이라 내부 복사본에만 변경이 적용됩니다.
SubRoutineButton의 subRoutine 프로퍼티는 값 타입인 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.
taipaise
left a comment
There was a problem hiding this comment.
오늘도 정말 고생하셨습니다~! 꼭! 짚고 넘어갈 부분은 없는 것 같아서 어푸루브드립니다~
There was a problem hiding this comment.
범용적으로 사용할 수 있는 테이블 뷰가 있는 VC를 구현해주셨군요! 해당 VC를 구현해주신 이유가 궁금합니다.
홈 화면 이외에 비슷한 레이아웃으로 특정 항목을 선택할 수 있는 VC가 있는 걸까요?
추가로 이미 구현되어 있는 BaseViewController와 다르게 ViewModel을 가지고 있지 않은 것 같습니다. 어떠한 경우에 이 VC를 사용하게 될까요?
There was a problem hiding this comment.
앗 !! 리뷰 노트에 1. SelectableItemTableView 재사용 내용을 참고하시면 좋을 것 같아요 !!
홈 화면의 루틴 정렬, 추천 루틴 화면의 루틴 난이도 필터링, 루틴 등록의 반복 주기 설정에서 동일한 UI에 선택 화면이 비슷하여 재사용할 수 있을 것이라 생각하여
기존 추천 루틴 화면의 루틴 난이도 필터링에서 사용했던 RoutineLevelCell, RoutineLevelView를 삭제하고 SelectableItemCell, SelectableItemTableView를 구현했습니다 !!!
그래서 현재 홈 화면의 루틴 정렬, 추천 루틴 화면의 루틴 난이도 필터링에서 재사용해서 사용하고 있습니다 !!!
There was a problem hiding this comment.
어제 너무 급해서 리뷰 노트를 보는 것도 깜빡하고 넘어갔습니다 고생해서 쓰셨을텐데 진짜 죄송합니다ㅜㅜㅜ
2차 까지 생각하시고 구현하신 것이군요!! 말씀해주신 오류들은 저도 덕분에 이번에 처음 알았어요!
답변 감사합니다~!
There was a problem hiding this comment.
다이죠부네 ~~~~ 띵 백번 이해합니다 !!
There was a problem hiding this comment.
빡빡한 스케줄임에도 제스쳐까지..고생하셨습니다 ㅜㅜ
| updateAttribute() | ||
| } | ||
| } | ||
| var didTappedDateButton: ((Date) -> Void)? |
There was a problem hiding this comment.
외부에서 직접 해당 메서드를 접근해서 초기화할 수 있게 하신 이유가 있을까요? delegate가 아니라 이런 방법을 사용하신 이유가 궁금합니다!
추가적으로 이후에 해당 메서드가 실행될 때, DateView를 프로퍼티로 가지고 있는 WeekView의 monthLabel을 갱신하는 작업이 일어나는 것 같습니다.
DateView의 내부에서 WeekView의 monthLabel을 갱신하고 있는 구조가 아주 조금은 어색한거 같아 리뷰 남깁니다!!
제가 잘못 이해한 것일 수 있으니 편하게 말씀해주시면 감사하겠습니다! 🥹
There was a problem hiding this comment.
요거슨 !! 리뷰 노트 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을 구현해서 로직을 좀 옮길 수 있다면 옮겨보도록 할게유 .....
There was a problem hiding this comment.
작년 두번째 스프린트에서 View도 추상화의 대상이 될 수 있다라는 피드백을 기억하시나요? 당시에 전화기의 키패드를 예시로 들어서 설명해주셨었는데요,
1. 전화기 키패드의 동작 방법을 모두 외부에서 구현 (번호 구성하는 로직이 외부에서 이뤄짐)
2. 내부적으로 번화번호를 구성하는 방법을 구현. 나중에 전화걸기 버튼을 누르면 구성한 번호를 전달
요 피드백과 의미가 통하는 부분이 있다고 생각합니다!
말씀해주신 것처럼 불필요한 delegate가 생겨날 수 있는 구조라는 말씀에 공감합니다. 또 결국 DateView가 WeekView안의 서브 뷰로 존재하게 될텐데, DateView를 외부에서 알 수 있는 구조도 아니라 괜찮은 것 같네요! 정리하자면 외부에서는 내부적으로 어떻게 동작할 지 신경쓰지 않고 WeekView의 delegate만 알고 사용하면되는 구조라 좋다고 생각합니다.
다만 View의 추상화를 통해 View가 View자신의 동작 로직을 일부 가져가게 된다면, 이것을 어떻게 처리해야할 지는 같이 좀 더 고민해보면 좋을거 같아요..View가 멍청해질 수록 명확하게 역할분리를 했다는 것이니까요!! 정말 할 수록 선택하고 고민해야하는 부분이 많아지는 것 같습니다~!
다시 한 번 리뷰 노트 사과드립니다 ㅠ 다음번부터 꼼꼼히 읽어볼게요,.,
There was a problem hiding this comment.
네네네네 덕분에 같이 고민해볼 수 있어서 오히려 좋아 입니다 ~ ㅎ.ㅎ 일단 머지 고고 하고 더 고민 깊생 해보겠습니다 !!!
|
다시 봐도 홈 뷰가 레전드네요.. 고생하셨습니다 정말 |
🌁 Background
홈 화면 UI를 구현하였습니다.
서버 통신 로직은 다음 PR에서 구현하겠습니다. !!! (+ 플로팅 버튼, 루틴 상세 뷰)
📱 Screenshot
홈 화면 (Default)
홈 화면 (EmptyView)
홈 화면 (루틴 리스트 최대 높이)
루틴 정렬 Bottom Sheet
전체 흐름
Simulator.Screen.Recording.-.iPhone.13.mini.-.2025-07-24.at.22.35.53.mp4
👩💻 Contents
MainRoutineView구현SubRoutineButton구현RoutineView구현WeekView,DateView구현DateView는 요일 + DateButton으로 구성된 날짜 뷰이고,WeekView는 상단년 + 월 Label+ 주를 바꾸는 버튼 + 한 주의 DateView들로 이루어져 있습니다.HomeEmptyView구현HomeRegisterEmotionButton구현RegisterEmotionButton과 역할은 같지만 UI가 다른 이슈로 네이밍을 살짝 다르게 했습니다.SelectableItemTableView구현SelectableItemTableView와SelectableItemCell로 구현하였습니다.✅ Testing
TabBarView를 진입점으로 하여 HomeView의 UI · UX만 확인했습니다.
📝 Review Note
1.
SelectableItemTableView재사용홈 화면의 루틴 정렬, 추천 루틴 화면의 루틴 난이도 필터링, 루틴 등록의 반복 주기 설정 UI가 많이 유사하다고 생각해 재사용할 수 있을 것이라 생각했습니다.
따라서 SelectableItem 프로토콜을 만들어 SelectableItemTableView에서 SelectableItem를 채택한 제네릭 타입들이 사용할 수 있도록 구현하였습니다.
따라서 기존 RoutineLevelCell, RoutineLevelView를 삭제하고 SelectableItemTableView로 사용하였습니다.
2차에 루틴 반복주기 설정 시에도 해당 뷰를 재사용하면 좋을 것 같아요 !!!
2.
SelectableItemTableView제네릭 이슈1번째에 이어서
SelectableItemTableView관련 이슈입니다 ..!!재사용성을 위해
SelectableItemTableView를 제네릭 타입을 받도록 구현하였습니다.근데 제네릭을 사용한 class의 extension에서 @objc 함수를 사용할 수 없다는 사실을 아셨나요 ..?
(저는 진짜 몰랐음 .. 제가 지금도 잘못 인지한 거일수도 있음 ... )
컨벤션을 맞추기 위해 SelectableItemTableView의 extension에서 UITableViewDelegate, UITableViewDataSource를 채택해주고 싶었지만 다음과 같은 에러가 발생했습니다.
따라서 어쩔 수 없이 extension이 아닌 SelectableItemTableView 내부에 tableView delegate, datasource 관련 함수를 구현해주었습니다.
(MARK 주석으로 나누긴 했어요 !!!)
그리고 또한 제네릭 타입 내부에서 static 저장 프로퍼티를 사용할 수 없대유 ...
그래서 원래 private enum Layout으로 정의했던 값들을 SelectableItemTableView에서는 어쩔 수 없이 빼서 사용했습니다.
만약 제가 해당 오류들을 잘못인지하고 있던 것이라면 알려주세요 ... 🥺 (책임 나누기 아님)
3. Delegate와 closure
HomeView에서 가지고 있는 서브 뷰들이 꽤 많은 편입니다 !!!
따라서 Delegate 혹은 Closure를 통해 터치 이벤트를 처리해주려고 했는데요 .....
HomeView -> RoutineView -> MainRoutineView 이런 식으로 이벤트가 들어가다 보니 delegate의 delegate를 써야 하는 경우가 있었습니다. (이때 다소 불편하다고 생각이 들었음)
따라서 처리해야 하는 이벤트가 하나라면 delegate가 아닌 상위 뷰에서 closure로 이벤트를 전달받게 하는 것도 좋지 않을까 ? 라는 생각을 했습니다 !!!!
그래서 DateView를 보면 didTappedDateButton가 있고, 그것을 상위 WeekView에서 dateView 사용할 때 설정해주는 식으로 구현했습니다.
근데 줏대 없이 이곳에서는 delegate, 어느 곳에서는 closure를 사용한 것 같아 혼돈이 올 수 있을 것 같아 리뷰 노트에 적어놓습니다....
4. RoutineView의 intrinsicContentSize
HomeView에서 routineStackView에 RoutineView들을 추가해주려고 할 때, height를 지정해줘야 Layout이 잘 잡히더라고요 ..
하지만 RoutineView는 서브 루틴의 여부, 서브 루틴의 개수에 따라 높이가 유동적으로 달라질 수 있습니다.
따라서 RoutineView의 intrinsicContentSize를 override해서 RoutineView의 고유 사이즈를 계산해주었습니다. !!!
그리고.. .. 이렇게 크게 가져가고 싶지 않었는데 ... UI가 복잡하다 보니 PR이 커진 것 같아요 ㅠ.ㅠ
정말 죄송합니다 ... 어떻게든 끊어보고 싶긴 했는데 ..... 천천히 봐주셔요 ... 🥺 감사합니띵 !!
📣 Related Issue
Summary by CodeRabbit
신규 기능
기능 개선
버그 수정
기타