[Feat-T3-158] 루틴 등록/수정 화면 수정된 디자인 반영#49
Conversation
- 루틴 생성/수정에서 접었다 폈다 할 수 있는 카드 뷰들의 contentView가 채택하는 프로토콜입니다.
Walkthrough도메인에 RepeatType 추가 및 WeekType을 Week로 개명하고 해당 참조를 전반 업데이트. 루틴 엔티티들의 repeatDay 타입을 [Week]로 변경. 프레젠테이션 계층은 RoutineCreation 화면을 ViewController 기반으로 전면 교체하고, 다수의 컴포넌트(시간/기간/반복/이름) 추가 및 기존 컴포넌트/뷰 삭제. FSCalendar 도입과 아이콘/이미지 에셋 추가. 공용 Date 확장에 isMidnight 추가. Changes
Sequence Diagram(s)sequenceDiagram
participant User as User
participant Home as Home/Recommended/Result Views
participant RC as RoutineCreationViewController
participant VM as RoutineCreationViewModel
participant TP as TimePickerView
participant Cal as BitnagilCalendarView
User->>Home: Routine Creation 진입
Home->>RC: init(viewModel:..., [routineId|recommendId])
RC->>VM: 초기 액션(데이터 fetch)
User->>RC: 시간 설정 탭
RC->>TP: present bottom sheet
TP-->>RC: timePickerView(_:didSelectTime:)
RC->>VM: 실행 시간 업데이트
RC->>RC: timeView UI 갱신
User->>RC: 기간 설정 탭
RC->>Cal: present bottom sheet
Cal-->>RC: bitnagilCalendarView(_:didSelectDate:)
RC->>VM: 시작/종료일 업데이트
RC->>RC: periodView UI 갱신
Estimated code review effort🎯 4 (Complex) | ⏱️ ~75 minutes Poem
Tip 🔌 Remote MCP (Model Context Protocol) integration is now available!Pro plan users can now connect to remote MCP servers from the Integrations page. Connect with popular remote MCPs such as Notion and Linear to add more context to your reviews and chats. ✨ Finishing Touches
🧪 Generate unit tests
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. 🪧 TipsChatThere are 3 ways to chat with CodeRabbit:
SupportNeed help? Create a ticket on our support page for assistance with any issues or questions. CodeRabbit Commands (Invoked using PR/Issue comments)Type Other keywords and placeholders
Status, Documentation and Community
|
There was a problem hiding this comment.
Actionable comments posted: 15
🔭 Outside diff range comments (2)
Projects/Presentation/Sources/Home/View/HomeView.swift (1)
165-172: retain cycle 가능성 제거 및 변수명 일관성 개선emptyView가 클로저를 강하게 보유하면 self를 강하게 캡쳐해 순환 참조가 발생할 수 있습니다. 또한 실제 타입은 VC인데 지역 변수명이 View로 되어 있어 오독 여지가 있습니다.
다음과 같이 수정을 권장합니다:
- emptyView.didTapRegisterRoutineButton = { - guard let routineCreationViewModel = DIContainer.shared.resolve(type: RoutineCreationViewModel.self) - else { fatalError("routineCreationViewModel 의존성이 등록되지 않았습니다.") } - - let routineCreationView = RoutineCreationViewController(viewModel: routineCreationViewModel) - routineCreationView.hidesBottomBarWhenPushed = true - self.navigationController?.pushViewController(routineCreationView, animated: true) - } + emptyView.didTapRegisterRoutineButton = { [weak self] in + guard let self = self else { return } + guard let routineCreationViewModel = DIContainer.shared.resolve(type: RoutineCreationViewModel.self) + else { fatalError("routineCreationViewModel 의존성이 등록되지 않았습니다.") } + + let routineCreationViewController = RoutineCreationViewController(viewModel: routineCreationViewModel) + routineCreationViewController.hidesBottomBarWhenPushed = true + self.navigationController?.pushViewController(routineCreationViewController, animated: true) + }Projects/Presentation/Sources/RoutineCreation/ViewModel/RoutineCreationViewModel.swift (1)
130-139: 반복 요일 재매핑 불필요 + 실행 시간 파싱 포맷 불일치 (버그)
- routine.repeatDay는 이미 [Week]이므로 rawValue를 통해 다시 Week로 매핑할 필요가 없습니다.
- 저장 시 .time 포맷(HH:mm:ss)으로 직렬화하는데, 읽을 때 .amPmTimeShort로 파싱하고 있어 실패 시 현재 시각으로 대체되는 버그가 발생합니다.
아래와 같이 수정해 주세요.
- let weekDay = routine.repeatDay.compactMap { Week(rawValue: $0.rawValue) } - let repeatType: RepeatType = weekDay.count == Week.allCases.count ? .daily : .weekly + let weekDay = routine.repeatDay + let repeatType: RepeatType = weekDay.count == Week.allCases.count ? .daily : .weekly let executionType: ExecutionType @@ - } else { - let time = Date.convertToDate(from: routine.executionTime, dateType: .amPmTimeShort) - executionType = .time(startAt: time ?? Date()) - } + } else { + let time = Date.convertToDate(from: routine.executionTime, dateType: .time) + executionType = .time(startAt: time ?? Date()) + }
♻️ Duplicate comments (1)
Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_repeat_icon.imageset/Contents.json (1)
19-23: 템플릿 렌더링 코멘트(중복) — 필요 시 동일 적용 검토앞서 clear_icon, list_icon 파일에 남긴
template-rendering-intent: template관련 제안과 동일합니다. 단색 아이콘이라면 적용을, 복합 색상이라면 현 상태 유지가 적절합니다.
🧹 Nitpick comments (39)
Projects/Presentation/Resources/Colors.xcassets/Skyblue.colorset/Contents.json (1)
19-22: 색상 에셋에 localizable 추가 — 실제 필요 여부와 Xcode/툴체인 호환성 확인 권장"properties.localizable": true는 이 색상 에셋에 로케일별 변형(Localized Variants)을 허용합니다. 보통 색상은 로케일에 따라 달라지지 않으므로(브랜딩/시맨틱 컬러 일관성), 명확한 의도가 없다면 혼선을 줄이기 위해 비활성화를 권장합니다. 또한 이 설정은 Xcode 버전 의존성이 있으니(구버전 경고/무시 가능) CI/로컬 툴체인이 이를 지원하는지 확인해 주세요.
- 실제로 로케일별 다른 색상을 제공할 계획이 없다면 제거하는 편이 안전합니다.
- 만약 현지화가 목적이라면, 색상명을 Hue 기반(Skyblue) 대신 역할 기반(예: BrandPrimary/Accent) 시맨틱 네이밍으로 정리하는 것을 추천합니다.
불필요 시 아래처럼 되돌릴 수 있습니다.
"info" : { "author" : "xcode", "version" : 1 - }, - "properties" : { - "localizable" : true - } + }Tuist/Package.swift (1)
10-11: 장기적으로는 UICalendarView로의 전환도 고려iOS 16+ 대상이라면 시스템 제공 UICalendarView 채택 시 유지보수/접근성/성능 이점이 있습니다. 현재 디자인/요구사항 제약으로 FSCalendar를 쓰는 것은 이해하나, 기술 부채로 추적해두면 좋겠습니다.
필요하시면 FSCalendar → UICalendarView 전환 영향도 및 마이그레이션 계획 초안 드리겠습니다.
Projects/Presentation/Resources/Images.xcassets/clear_icon.imageset/Contents.json (1)
19-23: 아이콘을 틴트 컬러로 사용하는 경우 템플릿 렌더링 의도(properties) 추가 권장단색 아이콘이라면
template-rendering-intent: template를 지정해 시스템 틴트 컬러를 안정적으로 적용할 수 있습니다. (다색/풀컬러 이미지라면 스킵)다음 변경으로 속성을 추가할 수 있습니다:
"info" : { "author" : "xcode", "version" : 1 - } + }, + "properties" : { + "template-rendering-intent" : "template" + } }Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_list_icon.imageset/Contents.json (1)
19-23: 이 아이콘도 템플릿 렌더링이 필요하면 properties 추가UI에서 틴트 컬러로 재사용한다면
template-rendering-intent: template를 고려하세요. 다색이라면 현 상태 유지가 맞습니다.적용 예시:
"info" : { "author" : "xcode", "version" : 1 - } + }, + "properties" : { + "template-rendering-intent" : "template" + } }Projects/Domain/Sources/Entity/Enum/RepeatType.swift (1)
8-11: 도메인 RepeatType 추가는 명확합니다. 직렬화/열거/동시성 안전을 위해 프로토콜 채택을 권장합니다.
UI/네트워크/영속계층 전반에서 쓰일 수 있는 타입이므로 Codable/CaseIterable/Sendable 채택 시 이점이 큽니다. 원시값을 String으로 두면 직렬화 스키마가 더 안정적입니다.아래처럼 확장하는 것을 제안합니다:
-public enum RepeatType { - case daily - case weekly -} +public enum RepeatType: String, Codable, CaseIterable, Sendable { + case daily + case weekly +}만약 외부 스키마와의 호환성을 위해 원시값을 커스터마이징해야 한다면 다음처럼 명시도 가능합니다:
public enum RepeatType: String, Codable, CaseIterable, Sendable { case daily = "daily" case weekly = "weekly" }Projects/Shared/Sources/Extension/Date+.swift (1)
11-15: isMidnight가 초(second)를 무시합니다. 00:00:30도 자정으로 판정될 수 있습니다.
자정 여부가 UI 표시에 직결되므로 더 견고한 판정이 필요합니다. DST/타임존 변동에도 안전한 startOfDay 비교를 권장합니다.간결하고 안전한 대안:
- let components = Calendar.current.dateComponents([.hour, .minute, .second], from: self) - return components.hour == 0 && components.minute == 0 + let cal = Calendar.current + return cal.startOfDay(for: self) == self만약 구성요소 비교를 유지하고 싶다면 초까지 체크하세요:
- return components.hour == 0 && components.minute == 0 + return components.hour == 0 && components.minute == 0 && components.second == 0추가로, 사용 환경에 따라 Calendar.autoupdatingCurrent 사용을 고려하면 사용자가 시스템 설정(타임존)을 바꿨을 때도 정확도를 보장할 수 있습니다.
Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_two_icon.imageset/Contents.json (1)
1-23: 아이콘 템플릿 렌더링 의도 확인 및 메타데이터 보강 제안아이콘이 틴트 컬러로 사용될 예정이라면 template-rendering-intent를 명시하는 편이 안전합니다. 또한 iOS 15+ 타겟이라면 1x 이미지는 용량 절감을 위해 제외하는 것도 고려 가능합니다.
원하시는 경우 아래처럼 properties 필드를 추가할 수 있습니다(틴트 적용 아이콘일 때만):
"images" : [ ... ], "info" : { "author" : "xcode", "version" : 1 - } + }, + "properties" : { + "template-rendering-intent" : "template" + }추가 확인:
- 이 아이콘이 단색/틴트 사용 목적이라면 위 옵션 권장
- 만약 원본 컬러 유지가 목적이라면 현 상태가 적절
- iOS 15 이상만 타겟이라면 1x 에셋 제거로 앱 사이즈 최적화 가능(선택 사항)
Projects/Domain/Sources/Entity/Enum/WeekType.swift (1)
1-6: 파일명/주석명 불일치 정리 제안파일 경로는 WeekType.swift인데, 헤더 주석은 Week.swift로 표기되어 있습니다. 타입/파일명/주석명을 Week로 통일하면 추후 탐색성이 좋아집니다.
Projects/Presentation/Project.swift (1)
5-7: -ObjC 링크 플래그 추가 합리적(LGTM) + 범위 축소(타겟 레벨) 권장FSCalendar(ObjC 카테고리 포함) 사용 시 -ObjC가 필요할 수 있어 합리적인 설정입니다. 다만 프로젝트 전체보다는 해당 타겟에만 설정해 링크 영향 범위를 줄이는 것을 권장합니다.
아래와 같이 프로젝트 레벨 설정을 제거하고 타겟 레벨에 추가하는 방식을 제안합니다.
프로젝트 레벨 설정 제거(현 위치):
- settings: .settings( - base: ["OTHER_LDFLAGS": "$(inherited) -ObjC"] - ),타겟 레벨 설정 추가(같은 파일 내 Presentation 타겟 정의에 settings 필드 추가):
.target( name: "Presentation", destinations: .iOS, product: .framework, bundleId: "com.bitnagil.presentation", deploymentTargets: .iOS("15.0"), sources: ["Sources/**"], resources: ["Resources/**"], settings: .settings( base: ["OTHER_LDFLAGS": "$(inherited) -ObjC"] ), dependencies: [ .external(name: "SnapKit"), .external(name: "Kingfisher"), .external(name: "Lottie"), .external(name: "FSCalendar"), .project(target: "Domain", path: "../Domain"), .project(target: "Shared", path: "../Shared") ] )Projects/Presentation/Sources/RoutineCreation/Model/Week+.swift (3)
22-40: 표현 문자열의 하드코딩 로컬라이제이션 고려"koreanValue"는 프레젠테이션에 적절하지만, 다국어 확장 가능성을 염두에 둔다면 Localizable.strings 기반 혹은 공용 모듈(Shared)의 로케일 유틸로 이전을 고려해 주세요.
원하시면 지역화 키/strings 파일 골격을 생성해 드릴 수 있습니다.
9-21: 속성명 명확성: id → weekdayIndex(선택 사항)id는 Identifiable.id와 혼동될 여지가 있습니다. 컬렉션 정렬/매핑 용도라면 weekdayIndex 등 더 구체적인 이름이 가독성에 유리할 수 있습니다.
9-21: Swift 5.9+ switch 표현식 호환성 확인 필요현재
Week+.swift의var id·var koreanValue는 Swift 5.9 이상에서만 지원되는 switch 표현식(switch as expression) 문법을 사용하고 있습니다.
레포에SWIFT_VERSION설정이 없어 기본 Xcode 버전을 따르고 있으니,
- CI 및 로컬 빌드에서 사용 중인 Xcode 버전(=Swift 언어 버전)이 5.9 이상인지 확인하거나
- 하위 호환을 위해 각 case에 명시적
return을 추가하세요.검토 대상 위치:
- 파일: Projects/Presentation/Sources/RoutineCreation/Model/Week+.swift
- Lines: 9–21, 22–40
하위 호환 대안 예시:
extension Week { - var id: Int { - switch self { - case .monday: 0 - case .tuesday: 1 - … - } - } + var id: Int { + switch self { + case .monday: return 0 + case .tuesday: return 1 + case .wednesday: return 2 + case .thursday: return 3 + case .friday: return 4 + case .saturday: return 5 + case .sunday: return 6 + } + } - var koreanValue: String { - switch self { - case .monday: - "월" - … - } - } + var koreanValue: String { + switch self { + case .monday: return "월" + case .tuesday: return "화" + case .wednesday: return "수" + case .thursday: return "목" + case .friday: return "금" + case .saturday: return "토" + case .sunday: return "일" + } + } }
- CI 워크플로우(
.github/workflows등)에서 사용하는 Xcode 버전을 확인해주세요.- Swift 5.9 미만을 지원해야 한다면 위와 같이 명시적
return형태로 수정하는 것이 안전합니다.Tuist/Package.resolved (1)
12-20: FSCalendar 추가 적절, 최소 iOS 16 지원 여부 확인 및 UICalendarView 검토 제안
- 현 요구사항(선택된 날짜 서체 변경) 상 FSCalendar 채택은 합리적입니다.
- 다만 최소 지원 OS가 iOS 16.0 이상이라면 Apple 공식 UICalendarView로 전환 시 유지보수 비용을 줄일 수 있습니다.
- 현재
Tuist/Dependencies.swift에platforms: [.iOS]만 선언되어 있어 실제 최소 iOS 버전이 명시되어 있지 않습니다. 프로젝트 설정에서 반드시 확인해주세요.Tuist/Package.resolved는 잠금 파일로서 올바르게 커밋된 상태이며, 패키지 버전 업그레이드 정책(예: 2.8.x 유지/업)을 팀 내 합의해두면 좋겠습니다.프로젝트의 실제 최소 iOS 버전을 확인하려면 아래 스크립트를 실행하거나 Xcode 프로젝트 설정을 검토해주세요.
# Tuist 설정에서 iOS 최소 버전(deploymentTarget) 탐색 rg -nP --type=swift '\.iOS\(\.v[0-9]+' -n -C2 # Tuist 설정에서 플랫폼 선언 확인 rg -nP --type=swift 'platforms\s*:\s*\[.*?\.iOS' -n -C2Projects/Presentation/Sources/RoutineCreation/View/Protocol/RoutineCreationExpandable.swift (2)
17-17: 프로토콜에서 SnapKit 타입 노출 최소화 권장Presentation 내부에서만 쓰이더라도, 프로토콜에 제3자 라이브러리 타입(Constraint)을 노출하면 결합도가 올라가고 교체 비용이 커집니다. 가능하다면 UIKit의 NSLayoutConstraint로 추상화하거나, 높이 자체를 의도값으로 노출하고(예: preferredCollapsedHeight), 제약은 컨테이너가 관리하도록 역할을 분리해보세요.
아래와 같이 교체하는 방안을 고려해볼 수 있습니다.
- var heightConstraint: Constraint? { get set } + var heightConstraint: NSLayoutConstraint? { get set }또는 제약 자체를 노출하지 않고 다음과 같이 의미 기반 API로 전환:
- var preferredCollapsedHeight: CGFloat? { get }
- 컨테이너(CardView)가 해당 값으로 자체 제약을 갱신
19-20: 네이밍/인터페이스 미세 개선 제안
- setExpanded(expanded:) 보다는 setExpanded(_ isExpanded:)처럼 불리언 의도를 드러내는 네이밍이 읽기 좋습니다.
- 토글 성격이라면 메서드 대신 var isExpanded: Bool { get set } 속성화도 고려할 수 있습니다(데이터-UI 동기화를 단순화).
아래처럼 변경을 제안드립니다.
- func setExpanded(expanded: Bool) + func setExpanded(_ isExpanded: Bool)Projects/Presentation/Sources/ResultRecommendedRoutine/View/ResultRecommendedRoutineView.swift (1)
367-372: RoutineCreationViewController 전환 시 DI 실패 처리 개선 제안전체 프로젝트에서 더 이상
RoutineCreationView(구현체)가 참조되지 않음을 확인했습니다. DIContainer.resolve 실패 시fatalError대신assertionFailure후 안전 반환하도록 수정하면, 디버그 빌드에서는 문제를 즉시 파악하고 릴리즈 빌드에서는 앱 크래시를 방지할 수 있습니다.
- 파일: Projects/Presentation/Sources/ResultRecommendedRoutine/View/ResultRecommendedRoutineView.swift
- 메서드:
private func goToRoutineCreationView(routineId:)- guard let routineCreationViewModel = DIContainer.shared.resolve(type: RoutineCreationViewModel.self) - else { fatalError("routineCreationViewModel 의존성이 등록되지 않았습니다.") } + guard let routineCreationViewModel = DIContainer.shared.resolve(type: RoutineCreationViewModel.self) else { + assertionFailure("routineCreationViewModel 의존성이 등록되지 않았습니다.") + return + }[optional_refactors_recommended]
Projects/Domain/Sources/Entity/RoutineSummaryEntity.swift (1)
20-25: 로컬 변수명 정리 제안: weekType → weeks이제 타입이 Week이므로 변수명도 의미에 맞게 정리하면 가독성이 좋아집니다.
다음과 같이 변경을 제안합니다:
- let weekType: [Week] = repeatDay?.compactMap(Week.init(rawValue:)) ?? [] + let weeks: [Week] = repeatDay?.compactMap(Week.init(rawValue:)) ?? [] self.routineId = routineId self.routineName = routineName - self.repeatDay = weekType + self.repeatDay = weeks self.executionTime = executionTimeProjects/Domain/Sources/Entity/RoutineEntity.swift (1)
32-38: 로컬 변수명 정리 제안: weekType → weeks실제 타입이 Week이므로 변수명을 단수/복수 의미에 맞게 정리하면 이해가 쉬워집니다.
- let weekType: [Week] = repeatDay?.compactMap(Week.init(rawValue:)) ?? [] + let weeks: [Week] = repeatDay?.compactMap(Week.init(rawValue:)) ?? [] self.routineId = routineId self.historySeq = historySeq self.routineName = routineName - self.repeatDay = weekType + self.repeatDay = weeks self.executionTime = executionTimeProjects/Presentation/Sources/RoutineCreation/View/Component/RoutinePeriodContentView.swift (3)
49-52: 불필요한 임시 초기화 제거 (SwiftLint TODO 경고 해소)초기값 세팅은 상위에서 configure(dependencies:)로 주입하는 것이 일관됩니다. TODO와 임시 값 설정을 제거해 주세요.
- // TODO: - 추후 삭제 - configureButon(buttonType: .start, date: Date()) - configureButon(buttonType: .end, date: Date()) + // 초기 값은 상위에서 configure(dependencies:)로 주입됩니다.
68-74: 중복 로직 제거: helper 재사용중복된 문자열 변환/세팅 대신 helper를 재사용하면 간결해집니다.
- let startString = dependencies.start.convertToString(dateType: .yearMonthDate) - let endString = dependencies.end.convertToString(dateType: .yearMonthDate) - - startButton.setTitle(startString, for: .normal) - endButton.setTitle(endString, for: .normal) + configureButton(buttonType: .start, date: dependencies.start) + configureButton(buttonType: .end, date: dependencies.end)
146-151: 오타 및 접근 제어자 정정: configureButon → configureButton, private 지정메서드명이 오타이며, 외부 노출 필요가 없어 보입니다.
- func configureButon(buttonType: ButtonType, date: Date) { + private func configureButton(buttonType: ButtonType, date: Date) { let button = buttonType == .start ? startButton : endButton let dateString = date.convertToString(dateType: .yearMonthDate) button.setTitle(dateString, for: .normal) }init 내 호출부도 함께 변경해 주세요:
- configureButon(buttonType: .start, date: Date()) - configureButon(buttonType: .end, date: Date()) + configureButton(buttonType: .start, date: Date()) + configureButton(buttonType: .end, date: Date())Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineView.swift (2)
281-285: fade-out 애니메이션이 보이지 않는 문제: isHidden을 애니메이션 완료 후에 적용하세요지금은 숨김 전환 시
isHidden = true가 먼저 설정되어 alpha 애니메이션이 시각적으로 보이지 않습니다. 아래처럼 show/hide 분기하여 hidden/alpha를 처리하면 페이드 인/아웃이 모두 자연스럽게 동작합니다.private func toggleFloatingButton() { floatingButton.toggle() isShowingFloatingMenu.toggle() - floatingMenu.isHidden = !isShowingFloatingMenu - dimmedView.isHidden = !isShowingFloatingMenu - - UIView.animate(withDuration: 0.2, delay: 0, options: [.curveEaseOut]) { - self.dimmedView.alpha = self.isShowingFloatingMenu ? 1 : 0 - self.floatingMenu.alpha = self.isShowingFloatingMenu ? 1 : 0 - } + if isShowingFloatingMenu { + [floatingMenu, dimmedView].forEach { + $0.isHidden = false + $0.alpha = 0 + } + UIView.animate(withDuration: 0.2, delay: 0, options: [.curveEaseOut]) { + self.dimmedView.alpha = 1 + self.floatingMenu.alpha = 1 + } + } else { + UIView.animate(withDuration: 0.2, delay: 0, options: [.curveEaseOut], animations: { + self.dimmedView.alpha = 0 + self.floatingMenu.alpha = 0 + }, completion: { _ in + self.floatingMenu.isHidden = true + self.dimmedView.isHidden = true + }) + } }
83-86: UIAction 클로저에서 self 강한 캡처로 인한 순환 참조 가능성
levelButton,registerEmotionButton의 액션 클로저가self를 강하게 캡처합니다. 버튼이 뷰 컨트롤러를 소유하고 클로저가 다시 self를 소유할 수 있어 릭 위험이 있습니다.[weak self]로 캡처해주세요.- levelButton.addAction(UIAction { _ in - self.showBottomSheet() - }, for: .touchUpInside) + levelButton.addAction(UIAction { [weak self] _ in + self?.showBottomSheet() + }, for: .touchUpInside) - registerEmotionButton.addAction(UIAction { _ in - guard let emotionRegisterViewModel = DIContainer.shared.resolve(type: EmotionRegisterViewModel.self) else { - fatalError("emotionRegisterViewModel 의존성이 등록되지 않았습니다.") - } - let emotionRegisterView = EmotionRegisterView(viewModel: emotionRegisterViewModel) - emotionRegisterView.hidesBottomBarWhenPushed = true - self.navigationController?.pushViewController(emotionRegisterView, animated: true) - }, for: .touchUpInside) + registerEmotionButton.addAction(UIAction { [weak self] _ in + guard + let self, + let emotionRegisterViewModel = DIContainer.shared.resolve(type: EmotionRegisterViewModel.self) + else { + assertionFailure("emotionRegisterViewModel 의존성이 등록되지 않았습니다.") + return + } + let emotionRegisterView = EmotionRegisterView(viewModel: emotionRegisterViewModel) + emotionRegisterView.hidesBottomBarWhenPushed = true + self.navigationController?.pushViewController(emotionRegisterView, animated: true) + }, for: .touchUpInside)Also applies to: 94-101
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineCreationCardView.swift (3)
11-29: SwiftLint: fileprivate 대신 private 사용 권장Layout은 파일 외부에서 접근될 필요가 없어 보입니다. SwiftLint 경고를 해소하세요.
-fileprivate enum Layout { +private enum Layout {
100-101: 중복된 withRenderingMode 호출 제거
BitnagilIcon.chevronIcon(direction:)자체가.alwaysTemplate을 반환합니다. 추가 호출은 중복입니다.- chevronImageView.image = BitnagilIcon.chevronIcon(direction: .down)?.withRenderingMode(.alwaysTemplate) + chevronImageView.image = BitnagilIcon.chevronIcon(direction: .down)
31-31: UIGestureRecognizerDelegate 채택 불필요현재 delegate 메서드를 구현하지 않아 프로토콜 채택이 불필요합니다. 제거로 간결성을 높일 수 있습니다.
Projects/Presentation/Sources/RoutineCreation/View/Component/TimePickerView.swift (3)
37-37: 로케일 제거로 24시간제 노출 회귀 가능 — AM/PM 유지가 의도였다면 로케일을 복원하세요이전 학습 맥락상(한국어 앱이지만 AM/PM 노출을 위한 의도적인 en_US 설정) 로케일 제거는 UX 회귀일 수 있습니다. iOS 기본 로케일(ko_KR)에서는 24시간제가 기본입니다.
private func configureAttribute() { datePicker.preferredDatePickerStyle = .wheels datePicker.datePickerMode = .time + // AM/PM 표기를 위한 의도적 로케일 설정 (한국어 앱) + datePicker.locale = Locale(identifier: "en_US")
66-71: 오버컨스트레인트 가능성: 상/하 + 고정 높이 동시 지정registerButton에 top/bottom/height를 모두 required로 주면 환경에 따라 제약 충돌 로그가 날 수 있습니다. height의 우선순위를 낮추어 유연성을 확보하는 것을 추천합니다.
- make.height.equalTo(Layout.registerButtonHeight) + make.height.equalTo(Layout.registerButtonHeight).priority(.high)
2-2: 파일 헤더 주석의 파일명이 실제 파일과 불일치
DatePickerViewController.swift→TimePickerView.swift로 수정 권장.Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift (1)
51-52: 구분선은 이미지 대신 UIView로 그리는 방법 고려
divideLineIcon사용 대신UIView().backgroundColor로 1px 라인을 그리면 해상도 적응/메모리 비용/테마 대응이 수월합니다. 특히 다크모드/동적 색상 변경 시 유지보수가 용이합니다.Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineNameContentView.swift (1)
51-52: 사용되지 않는 상태 변수 제거
isExpanded는 어디에서도 사용되지 않습니다. 혼란을 줄이기 위해 제거하세요.- private var isExpanded = true // 초기 상태 추적Projects/Presentation/Sources/RoutineCreation/View/Component/BitnagilCalendarView.swift (2)
116-129: 초기 선택 상태 동기화 누락 — selectedDate와 초기 UI 동기화 필요초기 선택을 calendarView.select(Date())로 설정했지만, 내부 상태(selectedDate) 및 라벨 갱신이 보장되지 않습니다.
아래처럼 초기 선택 상태 및 라벨을 동기화하세요.
private func configureCalendar() { calendarView.delegate = self calendarView.headerHeight = Layout.calendarHeaderHeight calendarView.appearance.weekdayTextColor = BitnagilColor.gray50 calendarView.appearance.weekdayFont = BitnagilFont.init(style: .body2, weight: .semiBold).font calendarView.appearance.titleFont = BitnagilFont.init(style: .subtitle1, weight: .regular).font calendarView.appearance.todayColor = .clear calendarView.appearance.titleTodayColor = .black calendarView.appearance.titleSelectionColor = BitnagilColor.orange500 calendarView.appearance.selectionColor = BitnagilColor.orange50 calendarView.appearance.borderRadius = 0.5 - calendarView.select(Date()) + let today = Date() + calendarView.select(today) + selectedDate = today }추가로 dateLabel은 현재 month 표기를 위해 currentPage 기반으로 갱신하는 것이 안전합니다(아래 코멘트 참고).
148-152: 이벤트 중복 가능성: 날짜 선택 시에도 delegate 호출됨현재 didSelect에서 delegate를 즉시 호출하고, 확인 버튼에서도 다시 호출합니다. TimePickerView는 확인 버튼에서 단일 콜백을 주는 패턴을 사용하고 있어 UX 일관성이 떨어질 수 있습니다.
- 옵션 A(권장): didSelect에서는 내부 상태 및 UI만 갱신하고 delegate 호출은 확인 버튼에서만 수행.
- 옵션 B: 지금 방식 유지 시, 이중 콜백 수신에 따른 상위 로직의 중복 반영을 방지하는 처리 필요.
상태 갱신 예시는 아래처럼 보완하세요.
extension BitnagilCalendarView: FSCalendarDelegate { func calendar(_ calendar: FSCalendar, didSelect date: Date, at monthPosition: FSCalendarMonthPosition) { - delegate?.bitnagilCalendarView(self, didSelectDate: date) + selectedDate = date } }또한, 월 변경 시 라벨이 자동 갱신되도록 아래 델리게이트도 추가하세요.
extension BitnagilCalendarView: FSCalendarDelegate { + func calendarCurrentPageDidChange(_ calendar: FSCalendar) { + dateLabel.text = calendar.currentPage.convertToString(dateType: .yearMonth) + } }Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineRepeatContentView.swift (2)
221-227: weekly 미선택 시 bottom 오프셋이 0으로 설정되어 여백이 사라집니다초기에는
-edgeSpacing여백을 주었지만, weekly 해제 시-foldedHeight(=0)로 업데이트 하여 하단 여백이 없어집니다. UX 일관성을 위해 edgeSpacing을 유지하세요.} else { weekStackView.isHidden = true - repeatButtonBottomConstraint?.update(offset: -Layout.foldedHeight) + repeatButtonBottomConstraint?.update(offset: -Layout.edgeSpacing) }
13-26: 레이아웃 상수 오타 및 불필요한 상수 정리 제안
- 오타:
repeaetButtonBetweenSpacing→repeatButtonBetweenSpacing- 사용되지 않는 상수:
checkButtonImageTopSpacing,checkButtonImageSize,checkButtonLabelHeight,checkButtonLabelTrailingSpacing아래처럼 정리하세요.
private enum Layout { static let edgeSpacing: CGFloat = 19 static let repeatButtonTopSpacing: CGFloat = 14 static let repeatButtonHeight: CGFloat = 39 - static let repeaetButtonBetweenSpacing: CGFloat = 13 + static let repeatButtonBetweenSpacing: CGFloat = 13 static let weekStackViewTopSpacing: CGFloat = 15 static let weekStackViewHeight: CGFloat = 38 static let weekStackViewSpacing: CGFloat = 5 - static let checkButtonImageTopSpacing: CGFloat = 24 - static let checkButtonImageSize: CGFloat = 18 - static let checkButtonLabelHeight: CGFloat = 20 - static let checkButtonLabelTrailingSpacing: CGFloat = 6 static let foldedHeight: CGFloat = 0 }그리고 사용처도 함께 변경하세요.
- make.leading.equalTo(dailyButton.snp.trailing).offset(Layout.repeaetButtonBetweenSpacing) + make.leading.equalTo(dailyButton.snp.trailing).offset(Layout.repeatButtonBetweenSpacing)Projects/Presentation/Sources/RoutineCreation/View/RoutineCreationViewController.swift (2)
114-116: 하드코딩된 더미 의존성 제거 권장
subRoutineNameView에 하드코딩된 더미 데이터가 남아있습니다. ViewModel 바인딩 또는 실제 데이터 적용 시점까지 제거하는 것이 안전합니다.- subRoutineNameView.configure(dependencies: .init(subRoutines: ["ㄴㄴㄴ", "ㅋㅋㅋ"])) - bindCreationCardViews() + bindCreationCardViews()
193-200: SwiftLint 지적사항: 사용하지 않는 클로저 파라미터(_)로 변경
periodView.onAction의 파라미터를 사용하지 않아 SwiftLint 경고가 발생합니다. 파라미터명을_로 바꾸세요.- periodView.onAction = { [weak self] action in + periodView.onAction = { [weak self] _ in let datePickerView = BitnagilCalendarView() datePickerView.delegate = self self?.presentCustomBottomSheet(contentViewController: datePickerView, maxHeight: Layout.calendarBottomSheetHeight) }Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineTimeContentView.swift (2)
8-12: 불필요한 모듈 임포트 제거 제안본 파일에서는 Domain 타입을 사용하지 않습니다. 불필요한 import를 제거하여 의존성을 줄이세요.
-import Domain import SnapKit import UIKit
65-75: UI 상태 갱신 로직 단일화 제안
configure(dependencies:)와configureTime(time:)가 유사한 역할을 합니다. 하나로 통합하거나,configureTime에서configure(dependencies:)를 재사용하면 중복을 줄일 수 있습니다.예:
configureTime(time:)에서configure(dependencies: .init(startTime: time))호출로 대체.
📜 Review details
Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
💡 Knowledge Base configuration:
- MCP integration is disabled by default for public repositories
- Jira integration is disabled by default for public repositories
- Linear integration is disabled by default for public repositories
You can enable these sources in your CodeRabbit configuration.
⛔ Files ignored due to path filters (30)
Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_date.imageset/routine_creation_date@1x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_date.imageset/routine_creation_date@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_date.imageset/routine_creation_date@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_list_icon.imageset/routine_creation_list_icon@1x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_list_icon.imageset/routine_creation_list_icon@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_list_icon.imageset/routine_creation_list_icon@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_one_icon.imageset/routine_creation_one_icon@1x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_one_icon.imageset/routine_creation_one_icon@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_one_icon.imageset/routine_creation_one_icon@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_repeat_icon.imageset/routine_creation_repeat_icon@1x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_repeat_icon.imageset/routine_creation_repeat_icon@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_repeat_icon.imageset/routine_creation_repeat_icon@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_three_icon.imageset/routine_creation_three_icon@1x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_three_icon.imageset/routine_creation_three_icon@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_three_icon.imageset/routine_creation_three_icon@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_time_icon.imageset/routine_creation_time_icon@1x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_time_icon.imageset/routine_creation_time_icon@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_time_icon.imageset/routine_creation_time_icon@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_two_icon.imageset/routine_creation_two_icon@1x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_two_icon.imageset/routine_creation_two_icon@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_two_icon.imageset/routine_creation_two_icon@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/asterisk_icon.imageset/asterisk_icon@1x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/asterisk_icon.imageset/asterisk_icon@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/asterisk_icon.imageset/asterisk_icon@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/clear_icon.imageset/clear_icon@1x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/clear_icon.imageset/clear_icon@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/clear_icon.imageset/clear_icon@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/divide_line_icon.imageset/divide_line_icon@1x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/divide_line_icon.imageset/divide_line_icon@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/divide_line_icon.imageset/divide_line_icon@3x.pngis excluded by!**/*.png
📒 Files selected for processing (39)
Projects/Domain/Sources/Entity/Enum/RepeatType.swift(1 hunks)Projects/Domain/Sources/Entity/Enum/WeekType.swift(1 hunks)Projects/Domain/Sources/Entity/RoutineEntity.swift(2 hunks)Projects/Domain/Sources/Entity/RoutineSummaryEntity.swift(2 hunks)Projects/Presentation/Project.swift(2 hunks)Projects/Presentation/Resources/Colors.xcassets/Skyblue.colorset/Contents.json(1 hunks)Projects/Presentation/Resources/Images.xcassets/RoutineCreation/Contents.json(1 hunks)Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_date.imageset/Contents.json(1 hunks)Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_list_icon.imageset/Contents.json(1 hunks)Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_one_icon.imageset/Contents.json(1 hunks)Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_repeat_icon.imageset/Contents.json(1 hunks)Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_three_icon.imageset/Contents.json(1 hunks)Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_time_icon.imageset/Contents.json(1 hunks)Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_two_icon.imageset/Contents.json(1 hunks)Projects/Presentation/Resources/Images.xcassets/clear_icon.imageset/Contents.json(1 hunks)Projects/Presentation/Resources/Images.xcassets/divide_line_icon.imageset/Contents.json(1 hunks)Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift(2 hunks)Projects/Presentation/Sources/Home/View/HomeView.swift(2 hunks)Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineView.swift(2 hunks)Projects/Presentation/Sources/ResultRecommendedRoutine/View/ResultRecommendedRoutineView.swift(1 hunks)Projects/Presentation/Sources/RoutineCreation/Model/Week+.swift(1 hunks)Projects/Presentation/Sources/RoutineCreation/View/Component/BitnagilCalendarView.swift(1 hunks)Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineCreationCardView.swift(1 hunks)Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineCreationInputView.swift(0 hunks)Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineNameContentView.swift(1 hunks)Projects/Presentation/Sources/RoutineCreation/View/Component/RoutinePeriodContentView.swift(1 hunks)Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineRepeatContentView.swift(1 hunks)Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineTimeContentView.swift(1 hunks)Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineTimePickerButton.swift(0 hunks)Projects/Presentation/Sources/RoutineCreation/View/Component/SubroutineAdditionButton.swift(0 hunks)Projects/Presentation/Sources/RoutineCreation/View/Component/TimePickerView.swift(3 hunks)Projects/Presentation/Sources/RoutineCreation/View/Protocol/RoutineCreationExpandable.swift(1 hunks)Projects/Presentation/Sources/RoutineCreation/View/RoutineCreationView.swift(0 hunks)Projects/Presentation/Sources/RoutineCreation/View/RoutineCreationViewController.swift(1 hunks)Projects/Presentation/Sources/RoutineCreation/ViewModel/RoutineCreationViewModel.swift(3 hunks)Projects/Shared/Sources/Extension/Date+.swift(1 hunks)Tuist/Dependencies.swift(1 hunks)Tuist/Package.resolved(1 hunks)Tuist/Package.swift(1 hunks)
💤 Files with no reviewable changes (4)
- Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineTimePickerButton.swift
- Projects/Presentation/Sources/RoutineCreation/View/Component/SubroutineAdditionButton.swift
- Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineCreationInputView.swift
- Projects/Presentation/Sources/RoutineCreation/View/RoutineCreationView.swift
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-07-16T09:21:15.038Z
Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#19
File: Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift:57-59
Timestamp: 2025-07-16T09:21:15.038Z
Learning: OnboardingRecommendedRoutineView에서 viewWillAppear에 registerOnboarding 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.
Applied to files:
Projects/Presentation/Sources/RoutineCreation/View/RoutineCreationViewController.swiftProjects/Presentation/Sources/Home/View/HomeView.swiftProjects/Presentation/Sources/ResultRecommendedRoutine/View/ResultRecommendedRoutineView.swiftProjects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineView.swift
📚 Learning: 2025-07-27T13:07:40.469Z
Learnt from: taipaise
PR: YAPP-Github/Bitnagil-iOS#26
File: Projects/Presentation/Sources/DatePicker/View/DatePickerView.swift:33-53
Timestamp: 2025-07-27T13:07:40.469Z
Learning: DatePickerView에서 locale을 "en_US"로 설정하는 것은 한국어 앱이지만 시간을 AM/PM 형식으로 표시하기 위한 의도적인 UX 설계 결정이다. 한국 로케일은 기본적으로 24시간 형식을 사용하기 때문이다.
Applied to files:
Projects/Presentation/Sources/RoutineCreation/View/Component/TimePickerView.swift
🧬 Code Graph Analysis (8)
Projects/Presentation/Sources/RoutineCreation/View/RoutineCreationViewController.swift (5)
Projects/Presentation/Sources/RoutineCreation/ViewModel/RoutineCreationViewModel.swift (3)
action(89-116)fetchRoutine(118-153)fetchRecommendedRoutine(155-174)Projects/Presentation/Sources/Common/Extension/UIViewController+.swift (2)
configureCustomNavigationBar(19-28)presentCustomBottomSheet(97-100)Projects/Presentation/Sources/RoutineCreation/View/Component/RoutinePeriodContentView.swift (3)
configureAttribute(76-106)configure(68-74)configureLayout(108-144)Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineCreationCardView.swift (4)
configureAttribute(78-109)configure(69-76)configure(182-184)configureLayout(111-180)Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineTimeContentView.swift (3)
configureAttribute(77-108)configure(65-75)configureLayout(110-151)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineCreationCardView.swift (3)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutinePeriodContentView.swift (2)
configure(68-74)setExpanded(58-66)Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineRepeatContentView.swift (2)
configure(70-90)setExpanded(56-68)Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift (1)
chevronIcon(45-47)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutinePeriodContentView.swift (4)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineRepeatContentView.swift (4)
configureAttribute(92-120)configureLayout(122-156)setExpanded(56-68)configure(70-90)Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineTimeContentView.swift (4)
configureAttribute(77-108)configureLayout(110-151)setExpanded(55-63)configure(65-75)Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineNameContentView.swift (4)
configureAttribute(88-124)configureLayout(126-218)setExpanded(63-71)configure(73-86)Projects/Shared/Sources/Extension/Date+.swift (1)
convertToString(16-22)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineTimeContentView.swift (2)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutinePeriodContentView.swift (4)
configureAttribute(76-106)configureLayout(108-144)setExpanded(58-66)configure(68-74)Projects/Shared/Sources/Extension/Date+.swift (1)
convertToString(16-22)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineRepeatContentView.swift (3)
Projects/Presentation/Sources/RoutineCreation/ViewModel/RoutineCreationViewModel.swift (1)
action(89-116)Projects/Presentation/Sources/RoutineCreation/View/Component/RoutinePeriodContentView.swift (2)
setExpanded(58-66)configure(68-74)Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineTimeContentView.swift (2)
setExpanded(55-63)configure(65-75)
Projects/Presentation/Sources/RoutineCreation/View/Component/TimePickerView.swift (1)
Projects/Presentation/Sources/RoutineCreation/View/RoutineCreationViewController.swift (1)
timePickerView(215-218)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineNameContentView.swift (3)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutinePeriodContentView.swift (4)
configureAttribute(76-106)configureLayout(108-144)setExpanded(58-66)configure(68-74)Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineCreationCardView.swift (4)
configureAttribute(78-109)configureLayout(111-180)configure(69-76)configure(182-184)Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineTimeContentView.swift (4)
configureAttribute(77-108)configureLayout(110-151)setExpanded(55-63)configure(65-75)
Projects/Presentation/Sources/RoutineCreation/View/Component/BitnagilCalendarView.swift (4)
Projects/Presentation/Sources/RoutineCreation/View/RoutineCreationViewController.swift (1)
bitnagilCalendarView(222-224)Projects/Presentation/Sources/RoutineCreation/View/Component/TimePickerView.swift (3)
viewDidLoad(28-32)configureAttribute(34-54)configureLayout(56-73)Projects/Shared/Sources/Extension/Date+.swift (1)
convertToString(16-22)Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift (1)
chevronIcon(45-47)
🪛 SwiftLint (0.57.0)
Projects/Presentation/Sources/RoutineCreation/View/RoutineCreationViewController.swift
[Warning] 194-194: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineCreationCardView.swift
[Warning] 11-11: Prefer private over fileprivate declarations
(private_over_fileprivate)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutinePeriodContentView.swift
[Warning] 49-49: 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 (26)
Tuist/Package.swift (1)
10-11: FSCalendar 의존성 추가 자체는 적절합니다루틴 생성 화면에서 달력 UI 요구사항을 충족하기 위한 선택으로 타당해 보입니다. 버전 범위도 명확(from: "2.8.4").
Projects/Presentation/Resources/Images.xcassets/clear_icon.imageset/Contents.json (1)
1-17: clear_icon.imageset: 이미지 파일 존재 여부 확인 완료
Projects/Presentation/Resources/Images.xcassets/clear_icon.imageset/Contents.json에 명시된
• clear_icon@1x.png
• clear_icon@2x.png
• clear_icon@3x.png
세 파일이 모두 실제로 존재함을 확인했습니다.향후 전체 Images.xcassets 폴더에 대해 동일한 검증 스크립트를 CI 파이프라인에 통합하여
파일 누락을 자동으로 점검하도록 설정할 것을 권장합니다.
- 검증 대상 경로
- Projects/Presentation/Resources/Images.xcassets/clear_icon.imageset/Contents.json
- 검증 결과
- OK – clear_icon@1x.png, clear_icon@2x.png, clear_icon@3x.png 모두 존재
[optional_refactors_recommended]
Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_list_icon.imageset/Contents.json (1)
1-17: 매핑된 이미지명 일치 확인 완료
BitnagilIcon.routineListIcon에서 사용한"routine_creation_list_icon"이 Assets 내 파일명과 정확히 일치함을 확인했습니다. 런타임에 nil 반환될 가능성은 없습니다.Projects/Presentation/Resources/Images.xcassets/RoutineCreation/Contents.json (1)
1-6: 루트 Contents.json 구성 정상Xcode가 생성한 표준 메타데이터 형태이며 추가 조치 불필요합니다.
Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_repeat_icon.imageset/Contents.json (1)
1-17: 반복 아이콘 파일 존재/명명 확인DesignSystem에서
routineRepeatIcon으로 노출된다면 이미지셋 파일명(routine_creation_repeat_icon)과의 매핑이 정확한지 확인해 주세요. 이전 코멘트의 점검 스크립트에서도 함께 검증 가능합니다.Projects/Domain/Sources/Entity/Enum/RepeatType.swift (1)
8-11: 중복 RepeatType 정의 및 참조 확인 완료
Projects/Domain/Sources/Entity/Enum/RepeatType.swift에 정의된RepeatType은 한 곳뿐입니다.RoutineCreationViewModel.swift(9행)와RoutineRepeatContentView.swift(8행)에import Domain이 선언되어 있어, Presentation 레이어의RepeatType참조는 모두Domain.RepeatType입니다.도메인 타입 이관이 올바르게 반영되었으므로, 더 이상 중복 선언이나 잘못된 모듈 참조는 없습니다.
Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_one_icon.imageset/Contents.json (2)
1-23: 에셋 매니페스트 구성 정상입니다.
1x/2x/3x 스케일과 idiom, info 메타데이터가 적절합니다.
1-23: RoutineCreation 이미지셋 내 PNG 파일 존재 확인 완료Projects/Presentation/Resources/Images.xcassets/RoutineCreation 하위 모든
.imageset폴더의 Contents.json에 선언된 PNG 파일이 리포지토리에 정상적으로 존재함을 확인했습니다. 런타임 이미지 로딩 실패 우려가 없습니다.Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_time_icon.imageset/Contents.json (1)
1-23: 구성 적합합니다.
스케일/idiom/info 필드가 표준 형식을 따릅니다. 디자인 시스템 바인딩만 맞춰주면 런타임 이슈 없겠습니다.Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_date.imageset/Contents.json (1)
1-23: 에셋 정의 이상 없습니다.
이미지 파일 네이밍과 스케일 구성이 일관적입니다.Tuist/Dependencies.swift (1)
7-7: 사소한 포맷 정리(LGTM)트레일링 콤마 제거는 기능적 영향이 없습니다. 그대로 진행해도 무방합니다.
Projects/Domain/Sources/Entity/Enum/WeekType.swift (1)
8-16: WeekType → Week 리네이밍 잔여 참조 없음 확인
스크립트 실행 결과WeekType에 대한 잔여 참조가 모두 제거된 것을 확인했습니다. 변경 사항 승인합니다.Projects/Presentation/Project.swift (1)
21-21: FSCalendar 의존성 선언 및 버전 핀 확인 완료
- Tuist/Package.swift:
.package(url: "https://github.com/WenchaoD/FSCalendar.git", from: "2.8.4")- Tuist/Package.resolved:
"location" : "https://github.com/WenchaoD/FSCalendar.git"및 커밋0fbdec5172fccb90f707472eeaea4ffe095278f6문제 없으므로 승인합니다.
Projects/Presentation/Resources/Images.xcassets/RoutineCreation/routine_creation_three_icon.imageset/Contents.json (1)
1-23: 아이콘 에셋 추가 승인
- 1x/2x/3x 스케일 및 메타데이터가 모두 올바릅니다.
- BitnagilIcon.swift (
static let threeIcon = UIImage(named: "routine_creation_three_icon", …)) 의 로딩 키가 Assets 키 이름과 일치합니다.Projects/Presentation/Resources/Images.xcassets/divide_line_icon.imageset/Contents.json (1)
1-23: 디바이더 라인 아이콘 에셋 추가 문제 없습니다스케일별 파일 지정 및 메타데이터가 정상입니다. BitnagilIcon 등에서의 키 매핑만 일치 확인 부탁드립니다.
Projects/Domain/Sources/Entity/RoutineSummaryEntity.swift (1)
11-11: [Week]로 타입 일원화, 잘 반영되었습니다도메인 전반의 Week로의 변경과 일치하며, 퍼블릭 API 의미도 명확합니다.
Projects/Domain/Sources/Entity/RoutineEntity.swift (1)
12-12: repeatDay의 [Week] 전환 LGTM도메인 모델 통일성 측면에서 적절합니다.
Projects/Presentation/Sources/Home/View/HomeView.swift (1)
600-604: VC 전환 처리 LGTM플로팅 메뉴 액션에서의 화면 전환 로직과 탭바 숨김 처리 일관성 좋습니다.
Projects/Presentation/Sources/RoutineCreation/ViewModel/RoutineCreationViewModel.swift (1)
227-239: .weekly로의 스위칭 및 상태 동기화 로직 적절.daily 선택 시 전체 요일 선택, .weekly 전환 시 기존 .daily였다면 초기화 처리 로직이 명확합니다.
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutinePeriodContentView.swift (1)
95-106: 액션 바인딩 및 스타일링 일관성 양호버튼 액션을 onAction 클로저로 전달하는 패턴이 재사용성과 구성요소 간 결합도 측면에서 적절합니다.
Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineView.swift (2)
306-309: RoutineCreationViewController로의 마이그레이션 LGTM추천 루틴 카드 탭 시 VC 기반 생성 화면으로의 전환이 일관되게 반영되었습니다. 추천 루틴 id를 넘기는 이니셜라이저 사용도 적절합니다.
329-332: 플로팅 메뉴 > 루틴 등록 버튼 흐름도 VC 기반으로 올바르게 교체됨새로운 컨트롤러 생성자 사용과 네비게이션 푸시가 문제없이 동작할 것으로 보입니다.
Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift (1)
51-52: 아이콘 리소스 추가 LGTM새로운 에셋 바인딩이 일관된 패턴으로 잘 추가되었습니다. 참조 명도 에셋 카탈로그와 맞습니다.
Also applies to: 77-83
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineNameContentView.swift (1)
63-71: folding 로직(setExpanded)의 구현 패턴이 다른 컴포넌트와 일관적입니다heightConstraint 활성화와 서브뷰 hidden/alpha 토글이 잘 정리되어 있습니다.
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineRepeatContentView.swift (1)
82-90: Week.id와 StackView 인덱스의 정렬 가정 확인 필요
index = $0.id를 사용해 버튼을 찾아 선택하고 있습니다.Week.allCases의 순서와id의 매핑이 1:1로 보장되지 않으면 오동작합니다.도메인 정의(Week)의
id,rawValue,allCases순서 일관성을 확인해주세요. 불확실하다면 버튼 생성 시button.tag = index로 직접 인덱스를 부여하고, 그 값을 사용해 매핑하는 방식을 권장합니다.Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineTimeContentView.swift (1)
97-108: 액션 전달 방식은 적절합니다버튼 액션을 클로저로 노출하고, [weak self] 캡처로 순환 참조를 방지한 점 좋습니다. 디자인 시스템 리소스 사용도 일관적입니다.
| private var calendarView = FSCalendar() | ||
| private let confirmButton = UIButton() | ||
|
|
||
| var delegate: BitnagilCalendarViewDelegate? | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
선택된 날짜 상태 보관 프로퍼티 추가 제안
확정(확인 버튼) 시 전달할 선택 날짜를 명확히 유지하기 위해 상태 프로퍼티가 필요합니다.
아래 프로퍼티를 추가하세요.
private let rightChevronImageView = UIImageView()
private var calendarView = FSCalendar()
private let confirmButton = UIButton()
+ // 선택된 날짜 상태 (기본값: 오늘)
+ private var selectedDate: Date = Date()
+
var delegate: BitnagilCalendarViewDelegate?📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| private var calendarView = FSCalendar() | |
| private let confirmButton = UIButton() | |
| var delegate: BitnagilCalendarViewDelegate? | |
| private let rightChevronImageView = UIImageView() | |
| private var calendarView = FSCalendar() | |
| private let confirmButton = UIButton() | |
| // 선택된 날짜 상태 (기본값: 오늘) | |
| private var selectedDate: Date = Date() | |
| var delegate: BitnagilCalendarViewDelegate? |
🤖 Prompt for AI Agents
In
Projects/Presentation/Sources/RoutineCreation/View/Component/BitnagilCalendarView.swift
around lines 35–39, add a stored property to keep the currently selected date
(e.g. private var selectedDate: Date?) so the view can reliably provide the
selected date when the confirm button is tapped; set this property in the
FSCalendar delegate (didSelect/didDeselect) handlers and read it in the
confirmButton action to pass to the delegate/handler.
| prevButton.backgroundColor = .clear | ||
| nextButton.backgroundColor = .clear | ||
| } |
There was a problem hiding this comment.
이전/다음 달 이동 버튼에 액션이 없습니다 — 실사용 불가 상태
좌/우 이동 버튼(prevButton/nextButton)에 달 이동 액션이 연결되어 있지 않아 헤더를 숨긴 대체 내비게이션이 동작하지 않습니다.
아래와 같이 버튼 액션을 추가하고, 라벨도 함께 업데이트하세요.
prevButton.backgroundColor = .clear
nextButton.backgroundColor = .clear
+
+ prevButton.addAction(
+ UIAction { [weak self] _ in
+ guard let self else { return }
+ let current = self.calendarView.currentPage
+ guard let newDate = Calendar.current.date(byAdding: .month, value: -1, to: current) else { return }
+ self.calendarView.setCurrentPage(newDate, animated: true)
+ self.dateLabel.text = newDate.convertToString(dateType: .yearMonth)
+ },
+ for: .touchUpInside
+ )
+
+ nextButton.addAction(
+ UIAction { [weak self] _ in
+ guard let self else { return }
+ let current = self.calendarView.currentPage
+ guard let newDate = Calendar.current.date(byAdding: .month, value: 1, to: current) else { return }
+ self.calendarView.setCurrentPage(newDate, animated: true)
+ self.dateLabel.text = newDate.convertToString(dateType: .yearMonth)
+ },
+ 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.
| prevButton.backgroundColor = .clear | |
| nextButton.backgroundColor = .clear | |
| } | |
| prevButton.backgroundColor = .clear | |
| nextButton.backgroundColor = .clear | |
| prevButton.addAction( | |
| UIAction { [weak self] _ in | |
| guard let self else { return } | |
| let current = self.calendarView.currentPage | |
| guard let newDate = Calendar.current.date(byAdding: .month, value: -1, to: current) else { return } | |
| self.calendarView.setCurrentPage(newDate, animated: true) | |
| self.dateLabel.text = newDate.convertToString(dateType: .yearMonth) | |
| }, | |
| for: .touchUpInside | |
| ) | |
| nextButton.addAction( | |
| UIAction { [weak self] _ in | |
| guard let self else { return } | |
| let current = self.calendarView.currentPage | |
| guard let newDate = Calendar.current.date(byAdding: .month, value: 1, to: current) else { return } | |
| self.calendarView.setCurrentPage(newDate, animated: true) | |
| self.dateLabel.text = newDate.convertToString(dateType: .yearMonth) | |
| }, | |
| for: .touchUpInside | |
| ) | |
| } |
🤖 Prompt for AI Agents
In
Projects/Presentation/Sources/RoutineCreation/View/Component/BitnagilCalendarView.swift
around lines 61–63, the prevButton/nextButton only set backgroundColor and lack
actions, so header-hidden navigation doesn't work; add touchUpInside targets to
prevButton and nextButton (e.g., addTarget(self, action:
#selector(didTapPrevMonth), for: .touchUpInside) and addTarget(self, action:
#selector(didTapNextMonth), for: .touchUpInside)), implement the corresponding
selector methods to decrement/increment the current month state, refresh/redraw
the calendar view, and update the header label text (or currentMonthLabel)
accordingly so the UI and internal state stay in sync.
| UIAction { [weak self] _ in | ||
| guard let self else { return } | ||
| self.delegate?.bitnagilCalendarView(self, didSelectDate: Date()) | ||
| dismiss(animated: true) | ||
| }, | ||
| for: .touchUpInside) | ||
| } |
There was a problem hiding this comment.
확인 버튼이 항상 현재 시각(Date())를 전달합니다 — 선택된 날짜를 전달하도록 수정 필요
사용자가 달력에서 선택한 날짜가 아닌, 현재 시각을 delegate로 전달하고 있어 UX/기능 불일치가 발생합니다. FSCalendar의 선택값(또는 별도 상태)을 사용해야 합니다.
아래처럼 선택 상태를 보관하고, 확인 시 해당 값을 전달하도록 변경을 제안합니다.
- confirmButton.addAction(
- UIAction { [weak self] _ in
- guard let self else { return }
- self.delegate?.bitnagilCalendarView(self, didSelectDate: Date())
- dismiss(animated: true)
- },
- for: .touchUpInside)
+ confirmButton.addAction(
+ UIAction { [weak self] _ in
+ guard let self else { return }
+ let date = self.calendarView.selectedDate ?? self.selectedDate
+ self.delegate?.bitnagilCalendarView(self, didSelectDate: date)
+ self.dismiss(animated: true)
+ },
+ for: .touchUpInside)추가로, 클래스에 선택 날짜 상태를 보관하는 프로퍼티를 도입해야 합니다. 아래 별도 코멘트의 diff를 함께 적용해주세요.
Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In
Projects/Presentation/Sources/RoutineCreation/View/Component/BitnagilCalendarView.swift
around lines 139–145, the confirm button handler currently sends Date() (now) to
the delegate instead of the calendar’s selected date; change this to send the
stored selected date. Add a private var selectedDate: Date? to the class, update
the FSCalendar delegate/didSelect method to set selectedDate when the user picks
a date, and then in the UIAction guard unwrap selectedDate and call
delegate?.bitnagilCalendarView(self, didSelectDate: selectedDate) (or handle nil
appropriately) before dismissing. Ensure weak self handling remains and remove
the use of Date() in the confirm action.
| public var onAction: ((ContentView.Action) -> Void)? { | ||
| didSet { contentView.action = onAction } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
contentView.action 연결 방식이 중복/상충됩니다 (didSet vs 브릿지 클로저)
onAction의 didSet에서 contentView.action을 직접 치환하면서, 동시에 configureAttribute에서 브릿지 클로저로 onAction을 전달하고 있습니다. 하나의 방식만 유지해야 혼동과 버그를 줄일 수 있습니다. 브릿지 클로저 패턴만 남기는 것을 권장합니다.
- public var onAction: ((ContentView.Action) -> Void)? {
- didSet { contentView.action = onAction }
- }
+ public var onAction: ((ContentView.Action) -> Void)?Also applies to: 105-109
🤖 Prompt for AI Agents
In
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineCreationCardView.swift
around lines 42-44 and 105-109, the code assigns contentView.action both in the
onAction didSet and again via a bridge closure in configureAttribute, causing
duplication/conflict; remove the didSet assignment and let the bridge closure in
configureAttribute be the single source of truth for wiring onAction to
contentView.action (or vice versa if you prefer the didSet approach: remove the
bridge closure instead), ensuring only one wiring path remains and updating any
documentation/comments accordingly.
| func configure( | ||
| image: UIImage, | ||
| title: String, | ||
| contents: [String] | ||
| ) { | ||
| titleImageView.image = image | ||
| titleLabel.text = title | ||
| } |
There was a problem hiding this comment.
configure(image:title:contents:)에서 contents 파라미터가 사용되지 않습니다
헤더의 내용 라벨 업데이트 로직이 누락되어 기능적으로 불완전합니다. configureContents(contents)를 호출하도록 보완하세요.
func configure(
image: UIImage,
title: String,
contents: [String]
) {
titleImageView.image = image
titleLabel.text = title
+ configureContents(contents: contents)
}🤖 Prompt for AI Agents
In
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineCreationCardView.swift
around lines 69–76, the configure(image:title:contents:) method accepts a
contents parameter but never uses it; update the method to call
configureContents(contents) after setting titleImageView.image and
titleLabel.text so the header's content label is populated (ensure
configureContents is invoked with the passed contents array).
| import SnapKit | ||
| import UIKit | ||
|
|
||
| protocol RoutineCreationExpandable: UIView { |
There was a problem hiding this comment.
컴파일 오류: 프로토콜이 UIView를 상속하고 있습니다
Swift에서는 프로토콜이 구체 타입(예: UIView)을 상속할 수 없습니다. 클래스 제약과 구체 타입 제약은 아래처럼 표현해야 합니다.
아래 diff로 수정해 주세요.
-protocol RoutineCreationExpandable: UIView {
+protocol RoutineCreationExpandable: AnyObject where Self: UIView {📝 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.
| protocol RoutineCreationExpandable: UIView { | |
| protocol RoutineCreationExpandable: AnyObject where Self: UIView { |
🤖 Prompt for AI Agents
In
Projects/Presentation/Sources/RoutineCreation/View/Protocol/RoutineCreationExpandable.swift
around line 11, the protocol incorrectly inherits from the concrete type UIView;
change the declaration to constrain conforming types to UIView by replacing the
inheritance with a Self constraint (protocol RoutineCreationExpandable where
Self: UIView) so the protocol enforces that only UIView subclasses can conform.
There was a problem hiding this comment.
요 RoutineCreationExpandable 프로토콜이 UIView를 채택해야 하는 이유가 궁굼함니다 ~~
There was a problem hiding this comment.
🧩 Analysis chain
@choijungp UIView 제약이 필요한 이유를 확인해보겠습니다!
🏁 Script executed:
#!/bin/bash
# RoutineCreationExpandable 프로토콜이 어떻게 사용되는지 확인
echo "=== RoutineCreationExpandable 프로토콜 사용처 검색 ==="
rg -n "RoutineCreationExpandable" --type=swift -A3 -B3
echo -e "\n=== RoutineCreationCardView에서 어떻게 사용되는지 확인 ==="
ast-grep --pattern $'class RoutineCreationCardView$_ {
$$$
}'
echo -e "\n=== ContentView의 제네릭 제약 확인 ==="
rg -n "ContentView.*RoutineCreationExpandable" --type=swift -C2
Length of output: 10009
🏁 Script executed:
#!/bin/bash
# RoutineCreationCardView의 구체적인 ContentView 사용법 확인
echo "=== RoutineCreationCardView에서 ContentView를 어떻게 사용하는지 확인 ==="
rg -n -A10 -B5 "contentView" Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineCreationCardView.swift
echo -e "\n=== addSubview, layout 관련 사용법 확인 ==="
rg -n "addSubview\|constraint\|layout" Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineCreationCardView.swift -A2 -B2
Length of output: 4694
@choijungp 좋은 질문입니다! UIView 제약이 필요한 이유를 코드를 통해 확인해보니 명확해졌습니다.
UIView 제약이 필요한 이유
RoutineCreationCardView에서 ContentView를 다음과 같이 두 가지 역할로 동시에 사용하고 있기 때문입니다:
1. UIView로서의 기능
// Line 40: ContentView 인스턴스 생성
private let contentView = ContentView()
// Line 116: 서브뷰로 추가 (UIView 메서드)
addSubview(contentView)
// Lines 173-177: Auto Layout 제약 설정 (UIView 프로퍼티)
contentView.snp.makeConstraints { make in
make.top.equalTo(divideLine.snp.bottom)
make.horizontalEdges.equalToSuperview()
make.bottom.equalToSuperview().offset(-Layout.edgeSpacing)
}
// Line 234/240: 가시성 제어 (UIView 프로퍼티)
contentView.isHidden = false/true2. RoutineCreationExpandable로서의 기능
// Line 183: 프로토콜 메서드 호출
contentView.configure(dependencies: dependencies)
// Line 245: 프로토콜 메서드 호출
contentView.setExpanded(expanded: nextExpandedState)현재 구현의 장점
RoutineCreationCardView<ContentView: UIView & RoutineCreationExpandable>대신- 간단히
RoutineCreationCardView<ContentView: RoutineCreationExpandable>로 쓸 수 있음
하지만 문법적으로는 수정 필요
제가 처음에 지적했듯이, Swift에서 프로토콜이 구체 타입을 상속할 수 없으므로 다음과 같이 수정해야 합니다:
protocol RoutineCreationExpandable: AnyObject where Self: UIView {
// ...
}이렇게 하면 동일한 효과를 얻으면서도 Swift 문법에 맞게 됩니다! 🐰
| registerButton.layer.cornerRadius = 12 | ||
| registerButton.layer.masksToBounds = true | ||
| registerButton.titleLabel?.font = BitnagilFont.init(style: .body1, weight: .semiBold).font | ||
| registerButton.backgroundColor = BitnagilColor.gray95 | ||
| registerButton.setTitle("등록하기", for: .normal) | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
동적 타이틀 값(registerButtonTitle) 미적용
초기화 시 계산한 registerButtonTitle을 실제 버튼 타이틀에 적용하지 않고 하드코딩("등록하기") 되어 있습니다.
registerButton.layer.cornerRadius = 12
registerButton.layer.masksToBounds = true
registerButton.titleLabel?.font = BitnagilFont.init(style: .body1, weight: .semiBold).font
registerButton.backgroundColor = BitnagilColor.gray95
- registerButton.setTitle("등록하기", for: .normal)
+ registerButton.setTitle(registerButtonTitle, for: .normal)
+
+ registerButton.addAction(
+ UIAction { [weak self] _ in
+ self?.viewModel.action(input: .registerRoutine)
+ },
+ 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.
| registerButton.layer.cornerRadius = 12 | |
| registerButton.layer.masksToBounds = true | |
| registerButton.titleLabel?.font = BitnagilFont.init(style: .body1, weight: .semiBold).font | |
| registerButton.backgroundColor = BitnagilColor.gray95 | |
| registerButton.setTitle("등록하기", for: .normal) | |
| registerButton.layer.cornerRadius = 12 | |
| registerButton.layer.masksToBounds = true | |
| registerButton.titleLabel?.font = BitnagilFont.init(style: .body1, weight: .semiBold).font | |
| registerButton.backgroundColor = BitnagilColor.gray95 | |
| registerButton.setTitle(registerButtonTitle, for: .normal) | |
| registerButton.addAction( | |
| UIAction { [weak self] _ in | |
| self?.viewModel.action(input: .registerRoutine) | |
| }, | |
| for: .touchUpInside | |
| ) |
🤖 Prompt for AI Agents
In
Projects/Presentation/Sources/RoutineCreation/View/RoutineCreationViewController.swift
around lines 108 to 113, the computed/initialized registerButtonTitle is not
applied to the button — the title is hardcoded as "등록하기"; replace the hardcoded
string with the registerButtonTitle when calling setTitle(for: .normal). Also
ensure registerButtonTitle is accessible at this point (compute it before this
setup), and if the title can change later, assign it through a setter/observer
or update the button title wherever registerButtonTitle is modified.
| private func bindCreationCardViews() { | ||
| periodView.onAction = { [weak self] action in | ||
| let datePickerView = BitnagilCalendarView() | ||
| datePickerView.delegate = self | ||
|
|
||
| self?.presentCustomBottomSheet(contentViewController: datePickerView, maxHeight: Layout.calendarBottomSheetHeight) | ||
| } | ||
|
|
||
| timeView.onAction = { [weak self] action in | ||
| switch action { | ||
| case .allDayTapped: | ||
| print() | ||
| case .timeSetTapped: | ||
| let datePickerView = TimePickerView() | ||
| datePickerView.delegate = self | ||
| self?.presentCustomBottomSheet(contentViewController: datePickerView, maxHeight: Layout.timePickerBottomSheetHeight) | ||
| } | ||
| } | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
반복(요일/매일) 영역의 onAction 바인딩 누락
RoutineRepeatContentView로부터 전달되는 액션을 처리하지 않아 ViewModel과 동기화되지 않습니다.
아래처럼 바인딩을 추가하세요. (Action 설계가 setWeeks 전체 갱신 기반이라 ViewModel의 .toggleRepeatDay와 직접 매칭이 어렵습니다. VC에서 상태를 기억해 diff를 계산하거나, ViewModel에 일괄 설정용 Input 추가를 검토하세요.)
private func bindCreationCardViews() {
+ repeatView.onAction = { [weak self] action in
+ guard let self else { return }
+ switch action {
+ case .repeatDaily:
+ self.viewModel.action(input: .configureRepeatType(selectedType: .daily))
+ case .repeatWeekly:
+ self.viewModel.action(input: .configureRepeatType(selectedType: .weekly))
+ case .setWeeks(let weeks):
+ // TODO: - 현재 ViewModel은 토글 단위 입력만 지원합니다.
+ // VC에서 이전 상태와 비교해 diff만큼 toggleRepeatDay를 호출하거나,
+ // ViewModel에 'configureWeeks(Set<Week>)' 같은 일괄 설정 인풋 추가를 권장합니다.
+ weeks.forEach { week in
+ self.viewModel.action(input: .toggleRepeatDay(weekDay: week))
+ }
+ }
+ }
periodView.onAction = { [weak self] _ in
let datePickerView = BitnagilCalendarView()
datePickerView.delegate = self
self?.presentCustomBottomSheet(contentViewController: datePickerView, maxHeight: Layout.calendarBottomSheetHeight)
}반복 요일 설정은 토글 기반보다 “선택 집합을 일괄 반영” API가 더 견고합니다. ViewModel에 configureWeeks(Set<Week>) 추가를 고려해보세요.
Committable suggestion skipped: line range outside the PR's diff.
🧰 Tools
🪛 SwiftLint (0.57.0)
[Warning] 194-194: Unused parameter in a closure should be replaced with _
(unused_closure_parameter)
🤖 Prompt for AI Agents
In
Projects/Presentation/Sources/RoutineCreation/View/RoutineCreationViewController.swift
around lines 193–211 the onAction for the repeat (요일/매일) area from
RoutineRepeatContentView is not handled so the ViewModel is never updated; add a
binding similar to periodView/timeView that captures the repeat actions, keeps a
local selected-weeks Set<Week> state (or call a new ViewModel input like
configureWeeks(_:)) and on each action compute the new set (toggle or replace
depending on action) then send the full updated set to the ViewModel so UI and
VM remain synchronized; if you prefer minimal change, implement VC-side state +
diff to produce a Set<Week> and call existing VM input, else add a ViewModel
method configureWeeks(Set<Week>) and call that from the new onAction handler.
| timeView.onAction = { [weak self] action in | ||
| switch action { | ||
| case .allDayTapped: | ||
| print() | ||
| case .timeSetTapped: | ||
| let datePickerView = TimePickerView() | ||
| datePickerView.delegate = self | ||
| self?.presentCustomBottomSheet(contentViewController: datePickerView, maxHeight: Layout.timePickerBottomSheetHeight) | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
하루종일 토글 미구현 및 시간 설정과의 일관성 부족
.allDayTapped에서 print()만 호출되어 기능이 동작하지 않습니다. TimePicker처럼 ViewModel에 전달하고 UI를 갱신하세요.
timeView.onAction = { [weak self] action in
switch action {
case .allDayTapped:
- print()
+ self?.viewModel.action(input: .toggleRepeatAllDay)
+ // 즉시 UI 반영 (바인딩 구현 전 임시 처리)
+ if let self {
+ let midnight = Calendar.current.startOfDay(for: Date())
+ self.timeView.configure(dependencies: .init(startTime: midnight))
+ }
case .timeSetTapped:
let datePickerView = TimePickerView()
datePickerView.delegate = self
self?.presentCustomBottomSheet(contentViewController: datePickerView, maxHeight: Layout.timePickerBottomSheetHeight)
}
}향후 bind() 구현에서 ViewModel의 상태를 구독하여 UI를 일관되게 갱신하는 방향을 권장합니다.
Committable suggestion skipped: line range outside the PR's diff.
| .package(url: "https://github.com/airbnb/lottie-ios", from: "4.0.0"), | ||
| .package(url: "https://github.com/WenchaoD/FSCalendar.git", from: "2.8.4") |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
Tuist 타깃에 FSCalendar 제품 연결 여부 확인 필요
패키지 선언만으로는 링크되지 않습니다. Tuist의 Project.swift(혹은 해당 모듈 설정)에서 .product(name: "FSCalendar", package: "FSCalendar") 혹은 .external(name: "FSCalendar")로 타깃 의존성에 추가되었는지, 그리고 소스 코드에서 import FSCalendar가 실제로 사용되는지 점검 바랍니다.
다음 스크립트로 레포 전반에서 연결 및 사용 여부를 점검할 수 있습니다:
🏁 Script executed:
#!/usr/bin/env bash
set -euo pipefail
echo "== FSCalendar 임포트 사용처 검색 =="
rg -nP -C2 --type=swift '\bimport\s+FSCalendar\b' || echo "임포트 사용처가 없습니다."
echo
echo "== Tuist 설정에서 FSCalendar 연결 여부 검색(Project.swift, Helpers 등) =="
rg -nP -C3 '(FSCalendar|\.product\(\s*name:\s*"FSCalendar")' Tuist || echo "Tuist 설정에서 FSCalendar 연결이 보이지 않습니다."
echo
echo "== 패키지 이름/제품명 오기 여부 점검 힌트 =="
echo "• package url: https://github.com/WenchaoD/FSCalendar.git"
echo "• product/module: FSCalendar"Length of output: 2005
FSCalendar 종속성 누락 – Tuist 타깃에 FSCalendar 제품 링크 추가 필요
검사 결과, 코드에서는 Projects/Presentation/.../BitnagilCalendarView.swift:8에서 import FSCalendar를 사용하고 있으나, Tuist 설정(Tuist/Project.swift)에 해당 패키지의 제품 연결이 없습니다. Tuist/Package.swift에 선언만 되어 있을 뿐, 타깃 의존성에 추가되지 않아 빌드 시 링크되지 않습니다.
– import 사용처
• Projects/Presentation/Sources/RoutineCreation/View/Component/BitnagilCalendarView.swift:8
– Tuist 설정(의존성 미확인)
• Tuist/Package.swift: 패키지 선언만 존재
• Tuist/Project.swift: .product(name: "FSCalendar", package: "FSCalendar") 또는 .external(name: "FSCalendar") 미등록
해당 Presentation 타깃의 dependencies 목록에 FSCalendar를 추가해 주세요:
targets: [
.target(
name: "Presentation",
dependencies: [
+ .product(name: "FSCalendar", package: "FSCalendar"),
// …기존 의존성
]
),
…
]Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In Tuist/Package.swift around lines 10 to 11 there is a package declaration for
FSCalendar but the Presentation target is not linking the FSCalendar product;
update Tuist/Project.swift to add the FSCalendar dependency to the Presentation
target by adding .product(name: "FSCalendar", package: "FSCalendar") (or
.external(name: "FSCalendar") depending on your project conventions) into that
target's dependencies array so BitnagilCalendarView.swift can import and link
FSCalendar.
choijungp
left a comment
There was a problem hiding this comment.
띵 ............ 띵의 깊은 설계를 0.01 % 밖에 이해하지 못하것어요 ........
하지만 대단하다는 것 .. 루등 뷰가 너무너무 어렵다는 것은 알겟슴니다 ...
전혀 미흡한 부분이 없어유 ...
다만 api 붙이면서 조금씩 다듬으면 ㄱㅊ지 않으까요 ???
너무 고생하셧습니다 !! 띵푸루부 ~~~~~~~ 짱푸루부 !!
| private func configureConfirmButton() { | ||
| confirmButton.layer.cornerRadius = 12 | ||
| confirmButton.layer.masksToBounds = true | ||
| confirmButton.backgroundColor = BitnagilColor.gray10 | ||
| confirmButton.titleLabel?.font = BitnagilFont.init(style: .body1, weight: .semiBold).font | ||
| confirmButton.setTitle("확인", for: .normal) | ||
| confirmButton.setTitleColor(.white, for: .normal) | ||
| confirmButton.addAction( | ||
| UIAction { [weak self] _ in | ||
| guard let self else { return } | ||
| self.delegate?.bitnagilCalendarView(self, didSelectDate: Date()) | ||
| dismiss(animated: true) | ||
| }, | ||
| for: .touchUpInside) | ||
| } |
There was a problem hiding this comment.
요 버튼은 PrimaryButton으로 할 수 있었을 것 같으묘 !!!!
추후에 primaryButton와 같은 녀석들이면 재사용하는 것이 어떠신지 !!?? (급한 건 아님 ..)
There was a problem hiding this comment.
싹 다!!!! 이런 버튼 짱 많은데 싹 다 갈아치워버리죠!!!
| title: String, | ||
| placeHolder: String, | ||
| titleImage: UIImage?, | ||
| withInfoImage: Bool, |
There was a problem hiding this comment.
궁구만게 있는데요 !! 요 withInfoImage는 무슨 용도일까요 ???
피그마를 봤는데 사실상 이 버튼이 있는 이유를 잘 모르겟어요 ㅠㅠ
기존 v1 디자인에서는 툴팁을 보여줬는데 그것도 아닌 것 같고 ..
어쩔때는 닫았을때 보이고 안보이고 ....... 디자이너에게 확실히 물어보는게 좋을 것 같아요 !!!!
아님 딩이 알고계시다면 무슨 용도인지 궁굼증을 해결해주세요 ㅠㅠ !!
There was a problem hiding this comment.
저도 사실 일단 보이길래 만들었습니다.. 확실하게 물어보고 가는게 좋겠네요!!
내일 물어보고 수정하겠습니다!!
| case subroutineChanged(index: Int, text: String) | ||
| case deleteAllSubroutines |
There was a problem hiding this comment.
요 액션들은 아직 not yet 반영인것이지요 ??? ??
| import SnapKit | ||
| import UIKit | ||
|
|
||
| protocol RoutineCreationExpandable: UIView { |
There was a problem hiding this comment.
요 RoutineCreationExpandable 프로토콜이 UIView를 채택해야 하는 이유가 궁굼함니다 ~~
| private enum Layout { | ||
| static let horizontalSpacing: CGFloat = 20 | ||
| static let textFieldHeight: CGFloat = 24 | ||
| static let textFieldTopSpacing: CGFloat = 51 |
There was a problem hiding this comment.
요 51 값이 textField랑 navigationTitle 사이의 값이죵 ?? ???
근데 네비바를 hidden 처리해놔서 .. 잡는 영역이 약간 다른 느낌 ?!?!? ㅠㅠ
그래서 피그마보단 조금 더 가까워 보여요 !! 혹시 90으로 수정하는건 어떠신지 ??!?
There was a problem hiding this comment.
RoutineCreationExpandable 가 추가되는 RoutineCreationCardView에서, RoutineCreationExpandable의 isHidden값을 조절할 수 있게 하기 위함입니다!
프로토콜 타입만 있으면 해당 프로퍼티 접근이 힘들더라구요 ㅜㅜ
|
@choijungp 하.. api를 붙이기 전에 일단 화면 단에서 유저의 interaction 처리까지 구현한 상태인데요, 정말 수정할수록 개똥망인것 같습니다.. |
🌁 Background
📱 Screenshot
카드 접힘
카드 열림
달력
📝 Review Note
코드 양이 너무 많고, 저도 처음 시도해보는 구조들이기 때문에, 필요하다면 PR 설명회를 진행하는 것도 좋을 것 같습니다~
열고 접을 수 있는 카드 뷰의 구조
루틴 등록 수정은 루틴의 정보를 담고 있는 4개의 뷰로 이뤄져있습니다. 이제부터 이것을 카드 뷰라고 하겠습니다! 카드뷰의 구조는 아래와 같습니다.

루틴 등록/수정 화면은 총 4개의 카드 뷰로 구성되어 있으며, 각 카드 뷰는 루틴의 세부 정보를 담고 있습니다.
카드 뷰는 공통적으로 접기/펼치기 기능을 지원하며, 구조는 다음과 같습니다.
CardView
ContentView
RoutineCreationExpandable 프로토콜
카드 뷰 재사용을 용이하게 하기 위해, delegate 대신 클로저로 액션을 전달하는 방법을 선택했습니다. 각 카드 뷰는 onAction: ((Action) -> Void)? 형태의 콜백만 노출하고, VC는 이 클로저를 설정해 이벤트를 받습니다.
미흠한 부분들
📣 Related Issue
📬 Reference
Summary by CodeRabbit
신규 기능
리팩터
작업