Conversation
Walkthrough자산(이미지/컬러) 추가와 디자인 토큰 확장, 공통 컴포넌트/프로토콜 업데이트, 홈 화면 정렬 기능 제거, 추천 루틴 화면의 VC로 전환 및 레이아웃/빈 상태/이모션 등록 플로우 개편이 포함됩니다. TabBar에서 새로운 VC를 사용하도록 변경되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant User
participant RegisterEmotionButtonView
participant RecommendedRoutineViewController
participant EmotionRegisterViewController
User->>RegisterEmotionButtonView: 버튼 탭
RegisterEmotionButtonView-->>RecommendedRoutineViewController: registerEmotionButtonViewDidTapRegisterButton(...)
RecommendedRoutineViewController->>EmotionRegisterViewController: push 생성/네비게이션
sequenceDiagram
participant ViewModel
participant RecommendedRoutineViewController
participant EmptyView
participant ListView
ViewModel-->>RecommendedRoutineViewController: 추천 루틴 목록 업데이트
alt 목록 비어있음
RecommendedRoutineViewController->>EmptyView: 표시
RecommendedRoutineViewController->>ListView: 숨김/축소
else 목록 존재
RecommendedRoutineViewController->>EmptyView: 숨김
RecommendedRoutineViewController->>ListView: 표시
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~60 minutes Possibly related PRs
Suggested reviewers
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
|
11fc2fe to
6bc3032
Compare
There was a problem hiding this comment.
Actionable comments posted: 5
🔭 Outside diff range comments (2)
Projects/Presentation/Sources/Common/Extension/UIViewController+.swift (1)
86-95: 컴파일 오류: switch 분기에서 반환 누락
progressImage(step:)는UIImage?를 반환하지만 각 case에return이 없어 컴파일되지 않습니다. 아래처럼 명시적 반환이 필요합니다.수정 예:
private func progressImage(step: Int) -> UIImage? { switch step { - case 1: BitnagilGraphic.progressStep1 - case 2: BitnagilGraphic.progressStep2 - case 3: BitnagilGraphic.progressStep3 - case 4: BitnagilGraphic.progressStep4 - case 5: BitnagilGraphic.progressStep5 - default: nil + case 1: return BitnagilGraphic.progressStep1 + case 2: return BitnagilGraphic.progressStep2 + case 3: return BitnagilGraphic.progressStep3 + case 4: return BitnagilGraphic.progressStep4 + case 5: return BitnagilGraphic.progressStep5 + default: return nil } }Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineViewController.swift (1)
82-85: UIAction 클로저에서 self를 강하게 캡처하여 VC가 해제되지 않는 순환 참조가 발생할 수 있습니다.floatingButton은 [weak self]를 사용하지만 levelButton은 그렇지 않습니다. 일관되게 약한 캡처로 수정하세요.
- levelButton.addAction(UIAction { _ in - self.showBottomSheet() - }, for: .touchUpInside) + levelButton.addAction(UIAction { [weak self] _ in + self?.showBottomSheet() + }, for: .touchUpInside)
🧹 Nitpick comments (30)
Projects/Presentation/Resources/Images.xcassets/RoutineCategory/rest_icon.imageset/Contents.json (1)
1-23: 아이콘 틴트 적용 의도 확인 및 Template 설정 제안디자인 시스템에서 카테고리 아이콘에 일관된 틴트 색상을 적용한다면, 이미지셋 properties에 template-rendering-intent를 명시해 주는 것이 안전합니다. 현재는 기본값(default)이라 뷰 단에서의 렌더링 모드에 따라 동작이 달라질 수 있습니다.
다음과 같이 properties를 추가하는 것을 제안합니다:
{ "images" : [ ... ], "info" : { "author" : "xcode", "version" : 1 - } + }, + "properties" : { + "template-rendering-intent" : "template" + } }추가 확인:
- 해당 아이콘을 다크 모드에서 별도 색상으로 제공할 필요가 있다면, appearances(dark) 변형을 이미지셋에 함께 추가하는 것도 고려해 주세요.
- 장기적으로 동일 형태의 단색 벡터 아이콘이라면 PDF 벡터 1종으로 전환하여 용량 최적화를 도모할 수 있습니다.
Projects/Presentation/Resources/Images.xcassets/RoutineCategory/wakeup_icon.imageset/Contents.json (1)
1-23: 아이콘 렌더링 모드 명시로 사용 일관성 확보 제안추천 루틴 카드/아이콘이 공통 색상으로 틴팅된다면 template-rendering-intent를 명시하면 안전합니다. 뷰 레벨에서 alwaysTemplate를 강제하지 않는 경우에도 동작이 일관적입니다.
다음과 같이 properties 추가를 제안합니다:
"info" : { "author" : "xcode", "version" : 1 - } + }, + "properties" : { + "template-rendering-intent" : "template" + }틀별(Original) 렌더링이 의도라면(다색 아이콘 등) 이 제안은 생략하셔도 됩니다. 의도 확인 부탁드립니다.
Projects/Presentation/Resources/Images.xcassets/RoutineCategory/grow_icon.imageset/Contents.json (1)
1-23: 일관된 에셋 속성 유지 권장 (Template 또는 Original 의도 명시)동일 계열 아이콘들 간 렌더링 속성을 통일하는 것이 유지보수에 유리합니다. wakeup/rest/shine 등과 동일하게 template-rendering-intent를 명시하는 것을 권장합니다.
"info" : { "author" : "xcode", "version" : 1 - } + }, + "properties" : { + "template-rendering-intent" : "template" + }Projects/Presentation/Resources/Images.xcassets/RoutineCategory/shine_icon.imageset/Contents.json (1)
1-23: 다크 모드 대응 및 템플릿 렌더링 고려현재는 단일(universal) 1x/2x/3x만 제공됩니다. 다크 모드에서 아이콘 컬러 대비가 충분한지 확인 부탁드립니다. 템플릿 이미지로 운용한다면 색상 대비는 틴트 컬러로 제어 가능하므로 properties에 template-rendering-intent 추가를 권장합니다.
"info" : { "author" : "xcode", "version" : 1 - } + }, + "properties" : { + "template-rendering-intent" : "template" + }추가 제안:
- 다크 모드에서 원본 색상을 사용해야 한다면 appearances: "luminosity": "dark" 변형 자산을 함께 두는 방식을 고려해 주세요.
Projects/Presentation/Resources/Images.xcassets/RoutineCategory/outside_icon.imageset/Contents.json (1)
19-23: (옵션) 템플릿 렌더링 의도 명시로 예기치 않은 틴팅 방지아이콘이 틴트되지 않고 원본 색상으로 사용되어야 한다면
template-rendering-intent를 명시하는 것을 권장합니다. 현재도 기본 동작으로 문제없을 수 있으나, 전역 tint 적용 시 예기치 않은 색상 변화 가능성이 줄어듭니다.아래와 같이 properties 블록 추가를 제안합니다:
"info" : { "author" : "xcode", "version" : 1 - } + }, + "properties" : { + "template-rendering-intent" : "original" + } }Projects/Presentation/Sources/Common/Protocol/SelectableItem.swift (2)
10-11: 프로퍼티 명description은 혼동 유발 — 충돌/의미 오해 가능
description은 Swift에서CustomStringConvertible/NSObject와 연상되거나 충돌 소지가 있어 가독성·유지보수성 측면에서 불리합니다. UI 문맥에 맞는subtitle/detail/body등으로 변경을 권장합니다. 변경 시 기존 사용처 일괄 치환 필요.가능한 리팩터 예:
protocol SelectableItem { var id: Int { get } var displayName: String? { get } - var description: String { get } + var detail: String { get } }SelectableItemCell 및 관련 뷰모델/익스텐션의 참조(
.description)도 함께 업데이트해야 합니다.
10-11: 기대 역할 주석 추가로 재사용성/의도 명확화
displayName(옵셔널)과description(또는detail)의 역할/폴백 규칙을 문서화하면, 구현체/셀 양쪽에서 일관된 처리에 도움이 됩니다.예시:
/// 선택지 표기용 모델 /// - displayName: 강조 표기(없으면 detail을 타이틀로 사용) /// - detail: 전체 문구(설명/부가 정보 포함) protocol SelectableItem { ... }Projects/Presentation/Resources/Colors.xcassets/RoutineCategory/Yellow10.colorset/Contents.json (1)
1-20: 케이스 정합성 확인 완료
코드 내UIColor(named: "Yellow10")호출이 에셋 이름 "Yellow10"과 정확히 일치함을 확인했습니다.
- 다크 모드 전용 스펙이 있다면 Appearance(다크) 변형 추가를 고려해주세요.
Projects/Presentation/Sources/Common/Extension/UIViewController+.swift (1)
19-24: 기본 배경색 .white 대신 시스템 적응형(.systemBackground) 권장 + 배경 설정 단일화디폴트 배경을
.white로 두면 다크 모드에서 부조화가 생길 수 있습니다..systemBackground로 기본값을 둬 테마 적응성을 확보하고, 배경 설정을 한 곳에서만 수행해 소스 오브 트루스를 단일화하는 걸 제안합니다.아래와 같이 변경하면 테마 대응과 중복 설정을 줄일 수 있습니다:
- func configureCustomNavigationBar(navigationBarStyle: NavigationBarStyle, backgroundColor: UIColor? = .white) { + func configureCustomNavigationBar(navigationBarStyle: NavigationBarStyle, backgroundColor: UIColor? = .systemBackground) { let safeArea = self.view.safeAreaLayoutGuide let navigationBar: UIView = customNavigationBar(navigationBarStyle: navigationBarStyle) navigationBar.backgroundColor = backgroundColor self.view.addSubview(navigationBar)추가로, 배경색을 생성 시점으로 이관하려면
customNavigationBar에 배경색 인자를 전달하고(시그니처 확장), 내부navigationBar.backgroundColor설정을 그 인자로만 수행하도록 정리하는 것도 방법입니다. 이를 적용하려면 현재 64라인의 기본 배경 설정도 정리해 주세요.// customNavigationBar(navigationBarStyle:backgroundColor:)로 확장하고 // 64라인의 .systemBackground 하드는 제거Projects/Presentation/Sources/RecommendedRoutine/View/Component/RecommendedRoutineEmptyView.swift (2)
51-57: 라벨 고정 높이 제약은 Dynamic Type/현지화에 취약합니다. 제거를 권장합니다.
28,20고정 높이는 글자 크기 확대(접근성)나 문자열 길이 증가(현지화) 시 잘림 위험이 큽니다. 고정 높이를 제거하고 라벨이 내용에 맞게 크기 결정되도록 하는 편이 안전합니다.다음 diff로 높이 제약을 제거해 주세요:
- mainLabel.snp.makeConstraints { make in - make.height.equalTo(28) - } - - subLabel.snp.makeConstraints { make in - make.height.equalTo(20) - } + // 고정 높이 제거: 콘텐츠에 맞게 자동 결정그리고 라벨에 줄바꿈 허용을 추가해 주세요:
// configureAttribute() mainLabel.numberOfLines = 0 subLabel.numberOfLines = 0
27-29: StackView 정렬을 center로 두면 빈 상태 메시지가 더 자연스럽습니다.텍스트는 중앙 정렬이지만, 스택뷰 alignment 기본값은 fill입니다. 시맨틱하게 중앙 배치를 원하시면 alignment를
.center로 조정하는 것을 권장합니다.다음 한 줄을 추가해 주세요:
stackView.alignment = .centerProjects/Presentation/Sources/RecommendedRoutine/View/Component/RegisterEmotionButtonView.swift (2)
71-91: 좁은 화면에서 라벨/버튼 겹침 방지: 압축/허깅 우선순위 설정을 권장합니다.현재 라벨은 leading만, 버튼은 trailing 고정/고정폭입니다. 화면이 좁을 때 레이아웃 충돌을 피하려면 라벨의 가로 압축 저항을 낮추고 버튼의 허깅/저항을 높여 버튼 우선 배치를 보장하는 것이 안전합니다.
다음 설정을
configureAttribute()말미에 추가해 주세요:registerEmotionLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) registerEmotionLabel.setContentHuggingPriority(.defaultLow, for: .horizontal) registerEmotionButton.setContentCompressionResistancePriority(.required, for: .horizontal) registerEmotionButton.setContentHuggingPriority(.required, for: .horizontal) registerEmotionLabel.lineBreakMode = .byTruncatingTail
58-64: 접근성: 버튼의 접근성 라벨/특성 지정 권장시각적 타이틀은 설정되어 있으나, VoiceOver 사용자에게 명확한 동작 설명을 제공하려면 접근성 라벨/힌트를 명시하는 것이 좋습니다.
다음 설정을 추가해 주세요:
registerEmotionButton.isAccessibilityElement = true registerEmotionButton.accessibilityLabel = "추천받기" registerEmotionButton.accessibilityHint = "내 기분에 맞는 루틴을 추천받는 화면으로 이동"Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift (1)
54-61: 아이콘 에셋명/번들 탑재 확인 및 렌더링 모드 일관성 검토
- 에셋(
wakeup_icon,shine_icon,grow_icon,outside_icon,connect_icon,rest_icon)이 실제 xcassets에 모두 존재하는지 확인 부탁드립니다.- 해당 아이콘을 틴트할 계획이면
.withRenderingMode(.alwaysTemplate)적용을 고려하세요. 원본 색상 유지가 목적이라면 현재 상태가 적절합니다.필요 시 다음과 같이 템플릿 렌더링을 적용할 수 있습니다:
- static let wakeupIcon = UIImage(named: "wakeup_icon", in: bundle, with: nil) + static let wakeupIcon = UIImage(named: "wakeup_icon", in: bundle, with: nil)?.withRenderingMode(.alwaysTemplate)Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelButton.swift (1)
66-68: 고정 높이 20은 Dynamic Type에서 충돌/잘림 소지라벨 높이를 고정하면 접근성 글꼴 크기에서 제약 충돌이나 텍스트 잘림이 발생할 수 있습니다. 고정 높이를 제거하고, 필요 시
>=제약 또는 콘텐츠 기반 레이아웃으로 전환을 권장합니다.다음과 같이 수정해 주세요:
- buttonLabel.snp.makeConstraints { make in - make.height.equalTo(Layout.buttonLabelHeight) - } + // 고정 높이 제거: 콘텐츠 기반으로 자동 결정추가로:
buttonLabel.numberOfLines = 1 buttonLabel.lineBreakMode = .byTruncatingTail buttonLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal)Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineCategoryButton.swift (1)
56-59: 비선택 상태의 시각적 경계(경계선/배경 대비) 검토 권장비선택 상태가 흰 배경 + gray60 텍스트라 동일한 흰색 배경 컨텍스트(예: 카드 내부/스크롤 배경)에서는 칩의 경계가 희미해질 수 있습니다. 디자인과 협의하여 다음 중 하나를 고려해 주세요:
- 얇은 보더 추가(예: gray 계열 라이트 톤)
- 살짝 톤 분리된 배경(예: gray99/gray98 등)
- 그림자(아웃셋 매우 약하게)
또한 UIKit 의미론을 활용해
isChecked대신isSelected상태를 사용하면 상태 관리가 단순해집니다(옵션).실기기에서 추천 루틴 목록 배경(특히 흰색 배경) 위에서 비선택 칩의 가독성을 확인해 주세요. 필요시 보더/배경 톤 적용 여부를 알려주시면 패치안 제안 드리겠습니다.
Projects/Presentation/Sources/RecommendedRoutine/Model/RoutineLevelType+.swift (2)
2-2: 파일 헤더의 파일명 주석이 실제 파일명과 불일치합니다헤더 주석의 파일명을
RoutineLevelType+.swift로 맞춰 주세요. 추적성과 일관성을 위해 권장됩니다.-// RoutineLevelType.swift +// RoutineLevelType+.swift
19-33: 하드코딩 문자열의 지역화(로컬라이제이션) 고려
displayName/description의 한글 문자열을 Localizable.strings로 이동하면 확장성과 테스트 용이성이 개선됩니다. Domain/Presentation 간 확장에서 문자열 리소스는 보통 Presentation에서 관리합니다.원하시면 Localizable 키 설계와 적용 패치를 함께 제안드리겠습니다.
Projects/Presentation/Sources/Common/Component/RoutineCardView.swift (5)
63-67: tintColor가 적용되지 않을 수 있습니다. 템플릿 렌더링 모드로 변경하세요.현재 setImage 후 tintColor만 설정되어 있어, 원본 이미지가 템플릿이 아니라면 색상이 적용되지 않습니다.
아래처럼 렌더링 모드를 지정해 주세요.
- let plusImage = BitnagilIcon.plusIcon? - .resizeAspectFit(to: CGSize(width: Layout.plusImageSize, height: Layout.plusImageSize)) + let plusImage = BitnagilIcon.plusIcon? + .resizeAspectFit(to: CGSize(width: Layout.plusImageSize, height: Layout.plusImageSize))? + .withRenderingMode(.alwaysTemplate) plusButton.setImage(plusImage, for: .normal) plusButton.tintColor = BitnagilColor.gray10
31-39: 미사용 프로퍼티(editButton, deleteButton)가 남아 있습니다.현재 레이아웃/액션에 사용되지 않는 버튼들은 제거하거나, 사용할 계획이라면 delegate/target-action을 추가해 주세요.
- private let editButton = UIButton() - private let deleteButton = UIButton()
59-62: 하드코딩된 타이틀과 외부 주입 API 부재공통 컴포넌트로 재사용할 계획이면 모델/파라미터를 통한 설정 API를 제공하세요. 예: configure(title:category:subRoutines:isPlusHidden:)
예시 API:
struct RoutineCardContent { let category: RoutineCategoryType let title: String let subRoutines: [String] let showsPlus: Bool } func configure(with content: RoutineCardContent) { titleLabel.text = content.title (categoryIconView as? RoutineCategoryIcon)?.update(category: content.category) subRoutineStackView.arrangedSubviews.dropFirst().forEach { $0.removeFromSuperview() } content.subRoutines.forEach { /* 라벨 생성 및 추가 */ } plusButton.isHidden = !content.showsPlus }
133-166: 접근 제어: fileprivate 대신 private 권장 (SwiftLint 경고 반영)해당 타입은 파일 외부에서 사용되지 않습니다. private로 범위를 좁혀주세요.
-fileprivate class RoutineCategoryIcon: UIView { +private class RoutineCategoryIcon: UIView {또한 update(category:) 같은 변경 API가 필요하면 메서드를 추가해 주세요.
152-157: fallback 컬러 고려category 배경/아이콘이 nil일 경우 BitnagilColor.yellow10/BitnagilIcon.shineIcon 역시 nil 가능성(UIColor(named:) 반환값)이 있어 투명하게 보일 수 있습니다. 명확한 기본값을 두는 편이 안전합니다.
- backgroundColor = routineCategory.iconBackgroundColor ?? BitnagilColor.yellow10 - routineCategoryIcon.image = routineCategory.iconImage ?? BitnagilIcon.shineIcon + backgroundColor = routineCategory.iconBackgroundColor + ?? BitnagilColor.yellow10 + ?? UIColor.systemYellow.withAlphaComponent(0.2) + routineCategoryIcon.image = routineCategory.iconImage + ?? BitnagilIcon.shineIcon + ?? UIImage(systemName: "sparkles")Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineViewController.swift (7)
139-145: 감정 등록 버튼 숨김 시 레이아웃 겹침 가능성 최소화isHidden만 변경하면 뷰는 보이지 않지만 고정 높이 제약은 유지됩니다. 현재는 headerStackView top을 재설정하여 대부분의 케이스에서 문제 없겠지만, 안전하게 버튼 높이를 0으로 업데이트하는 접근을 권장합니다.
아래처럼 height 제약을 저장/업데이트해 보세요.
+ private var registerEmotionButtonHeightConstraint: Constraint? ... registerEmotionButton.snp.makeConstraints { make in make.leading.equalToSuperview().offset(Layout.horizontalMargin) make.trailing.equalToSuperview().inset(Layout.horizontalMargin) make.top.equalTo(categoryView.snp.bottom).offset(Layout.registerEmotionButtonTopSpacing) - make.height.equalTo(Layout.registerEmotionButtonHeight) + self.registerEmotionButtonHeightConstraint = make.height.equalTo(Layout.registerEmotionButtonHeight).constraint }showEmotionButton 내부:
- registerEmotionButton.isHidden = !showingEmotionButton + registerEmotionButton.isHidden = !showingEmotionButton + registerEmotionButtonHeightConstraint?.update(offset: showingEmotionButton ? Layout.registerEmotionButtonHeight : 0)
224-233: 불필요한 예외 처리 로직 정리registerEmotionButton은 recommendedRoutineStackView의 arrangedSubview가 아니므로 비교 조건은 항상 참입니다. 깔끔하게 모두 제거하세요.
- recommendedRoutineStackView.arrangedSubviews.forEach { view in - if view != registerEmotionButton { - recommendedRoutineStackView.removeArrangedSubview(view) - view.removeFromSuperview() - } - } + recommendedRoutineStackView.arrangedSubviews.forEach { view in + recommendedRoutineStackView.removeArrangedSubview(view) + view.removeFromSuperview() + }
256-273: 헤더 상단 제약 재설정 로직은 적절합니다. 다만 애니메이션이 필요하면 layoutIfNeeded 포함을 고려현재는 제약만 재작성(remake)하고 있어 화면 전환 없이 스냅으로 변동됩니다. 사용자 경험상 부드러운 전환이 필요하면 UIView.animate 블록 내에서 layoutIfNeeded 호출을 검토해 주세요.
275-287: isHidden을 즉시 토글하면 페이드 애니메이션이 보장되지 않습니다. 애니메이션 완료 후 isHidden을 갱신하세요.현재는 숨길 때도 isHidden을 먼저 true로 만들어 애니메이션이 적용되지 않습니다. 보이게 할 때는 alpha 초기값 0 설정도 필요합니다.
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.alpha = 0 + floatingMenu.isHidden = false + dimmedView.isHidden = false + } + + UIView.animate(withDuration: 0.2, delay: 0, options: [.curveEaseOut]) { + self.dimmedView.alpha = self.isShowingFloatingMenu ? 1 : 0 + self.floatingMenu.alpha = self.isShowingFloatingMenu ? 1 : 0 + } completion: { _ in + self.floatingMenu.isHidden = !self.isShowingFloatingMenu + self.dimmedView.isHidden = !self.isShowingFloatingMenu + } }
315-320: 제네릭 캐스팅/옵셔널 처리 단순화현재 guard let with
as? RoutineLevelType?는 타입 캐스팅 검증 용도로만 사용되어 항상 통과할 가능성이 높고 가독성이 떨어집니다. 옵셔널 선택(해제 가능)을 그대로 전달하는 편이 명확합니다.- func selectableItemTableView<T: SelectableItem & CaseIterable & Equatable>(_ sender: SelectableItemTableView<T>, didSelectItem: T?) { - guard let didSelectLevel = didSelectItem as? RoutineLevelType? - else { return } - viewModel.action(input: .selectLevel(selectedLevel: didSelectLevel)) - levelButton.updateButton(level: didSelectLevel) - } + func selectableItemTableView<T: SelectableItem & CaseIterable & Equatable>(_ sender: SelectableItemTableView<T>, didSelectItem: T?) { + let selectedLevel = didSelectItem as? RoutineLevelType + viewModel.action(input: .selectLevel(selectedLevel: selectedLevel)) + levelButton.updateButton(level: selectedLevel) + }
303-306: DI 해제 실패 시 fatalError는 사용자 크래시를 유발합니다. 런타임 오류 처리로 전환을 권장합니다.실 서비스 환경에서 의존성 미등록은 치명적이지만, 사용자 경험을 위해 경고 로그 + 안전한 return으로 처리하는 편이 낫습니다. Alert 노출도 고려해 주세요.
- guard let routineCreationViewModel = DIContainer.shared.resolve(type: RoutineCreationViewModel.self) - else { fatalError("routineCreationViewModel 의존성이 등록되지 않았습니다.") } + guard let routineCreationViewModel = DIContainer.shared.resolve(type: RoutineCreationViewModel.self) else { + BitnagilLogger.log(logType: .error, message: "routineCreationViewModel 의존성 해제 실패") + return + }- guard let emotionRegisterViewModel = DIContainer.shared.resolve(type: EmotionRegisterViewModel.self) - else { fatalError("emotionRegisterViewModel 의존성이 등록되지 않았습니다.") } + guard let emotionRegisterViewModel = DIContainer.shared.resolve(type: EmotionRegisterViewModel.self) else { + BitnagilLogger.log(logType: .error, message: "emotionRegisterViewModel 의존성 해제 실패") + return + }Also applies to: 327-329, 338-340
76-79: 카피 라이팅 사소한 제안"추천 루틴리스트" 보다는 "추천 루틴 리스트"처럼 띄어쓰기를 반영한 표기가 자연스럽습니다. 디자인/기획과 합의된 용어가 있다면 무시하셔도 됩니다.
- routineLabel.text = "추천 루틴리스트" + routineLabel.text = "추천 루틴 리스트"
📜 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 (18)
Projects/Presentation/Resources/Images.xcassets/RoutineCategory/connect_icon.imageset/connect_icon.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCategory/connect_icon.imageset/connect_icon@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCategory/connect_icon.imageset/connect_icon@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCategory/grow_icon.imageset/grow_icon.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCategory/grow_icon.imageset/grow_icon@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCategory/grow_icon.imageset/grow_icon@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCategory/outside_icon.imageset/outside_icon.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCategory/outside_icon.imageset/outside_icon@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCategory/outside_icon.imageset/outside_icon@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCategory/rest_icon.imageset/rest_icon.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCategory/rest_icon.imageset/rest_icon@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCategory/rest_icon.imageset/rest_icon@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCategory/shine_icon.imageset/shine_icon.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCategory/shine_icon.imageset/shine_icon@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCategory/shine_icon.imageset/shine_icon@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCategory/wakeup_icon.imageset/wakeup_icon.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCategory/wakeup_icon.imageset/wakeup_icon@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineCategory/wakeup_icon.imageset/wakeup_icon@3x.pngis excluded by!**/*.png
📒 Files selected for processing (28)
Projects/Presentation/Resources/Colors.xcassets/RoutineCategory/Contents.json(1 hunks)Projects/Presentation/Resources/Colors.xcassets/RoutineCategory/Yellow10.colorset/Contents.json(1 hunks)Projects/Presentation/Resources/Images.xcassets/RoutineCategory/Contents.json(1 hunks)Projects/Presentation/Resources/Images.xcassets/RoutineCategory/connect_icon.imageset/Contents.json(1 hunks)Projects/Presentation/Resources/Images.xcassets/RoutineCategory/grow_icon.imageset/Contents.json(1 hunks)Projects/Presentation/Resources/Images.xcassets/RoutineCategory/outside_icon.imageset/Contents.json(1 hunks)Projects/Presentation/Resources/Images.xcassets/RoutineCategory/rest_icon.imageset/Contents.json(1 hunks)Projects/Presentation/Resources/Images.xcassets/RoutineCategory/shine_icon.imageset/Contents.json(1 hunks)Projects/Presentation/Resources/Images.xcassets/RoutineCategory/wakeup_icon.imageset/Contents.json(1 hunks)Projects/Presentation/Sources/Common/Component/RoutineCardView.swift(1 hunks)Projects/Presentation/Sources/Common/Component/SelectableItemCell.swift(3 hunks)Projects/Presentation/Sources/Common/DesignSystem/BitnagilColor.swift(1 hunks)Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift(2 hunks)Projects/Presentation/Sources/Common/Extension/UIViewController+.swift(1 hunks)Projects/Presentation/Sources/Common/Protocol/SelectableItem.swift(1 hunks)Projects/Presentation/Sources/Common/View/TabBarView.swift(1 hunks)Projects/Presentation/Sources/Home/Model/RoutineSortType.swift(0 hunks)Projects/Presentation/Sources/Home/View/HomeView.swift(0 hunks)Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift(0 hunks)Projects/Presentation/Sources/RecommendedRoutine/Model/RoutineCategoryType+.swift(2 hunks)Projects/Presentation/Sources/RecommendedRoutine/Model/RoutineLevelType+.swift(1 hunks)Projects/Presentation/Sources/RecommendedRoutine/Model/RoutineLevelType.swift(0 hunks)Projects/Presentation/Sources/RecommendedRoutine/View/Component/RecommendedRoutineEmptyView.swift(1 hunks)Projects/Presentation/Sources/RecommendedRoutine/View/Component/RegisterEmotionButton.swift(0 hunks)Projects/Presentation/Sources/RecommendedRoutine/View/Component/RegisterEmotionButtonView.swift(1 hunks)Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineCategoryButton.swift(2 hunks)Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelButton.swift(2 hunks)Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineViewController.swift(14 hunks)
💤 Files with no reviewable changes (5)
- Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift
- Projects/Presentation/Sources/RecommendedRoutine/Model/RoutineLevelType.swift
- Projects/Presentation/Sources/Home/Model/RoutineSortType.swift
- Projects/Presentation/Sources/RecommendedRoutine/View/Component/RegisterEmotionButton.swift
- Projects/Presentation/Sources/Home/View/HomeView.swift
🧰 Additional context used
🧠 Learnings (1)
📚 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/Common/View/TabBarView.swiftProjects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineViewController.swift
🧬 Code Graph Analysis (6)
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RecommendedRoutineEmptyView.swift (1)
Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineViewController.swift (2)
configureAttribute(73-110)configureLayout(112-195)
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelButton.swift (2)
Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift (1)
chevronIcon(45-47)Projects/Presentation/Sources/Common/Extension/UIImage+.swift (1)
resizeAspectFit(20-35)
Projects/Presentation/Sources/Common/Component/SelectableItemCell.swift (1)
Projects/Presentation/Sources/Common/DesignSystem/Font/BitnagilFont.swift (1)
attributedString(50-66)
Projects/Presentation/Sources/Common/Component/RoutineCardView.swift (2)
Projects/Presentation/Sources/Common/Component/SelectableItemCell.swift (2)
configureAttribute(30-39)configureLayout(41-55)Projects/Presentation/Sources/Common/Extension/UIImage+.swift (1)
resizeAspectFit(20-35)
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RegisterEmotionButtonView.swift (1)
Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineViewController.swift (1)
registerEmotionButtonViewDidTapRegisterButton(337-344)
Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineViewController.swift (4)
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RecommendedRoutineEmptyView.swift (1)
configureLayout(41-58)Projects/Presentation/Sources/Common/Extension/UIViewController+.swift (1)
configureCustomNavigationBar(19-29)Projects/Presentation/Sources/RecommendedRoutine/ViewModel/RecommendedRoutineViewModel.swift (1)
selectCategory(82-90)Projects/Shared/Sources/DIContainer/DIContainer.swift (1)
resolve(18-25)
🪛 SwiftLint (0.57.0)
Projects/Presentation/Sources/Common/Component/RoutineCardView.swift
[Warning] 133-133: Prefer private over fileprivate declarations
(private_over_fileprivate)
🔇 Additional comments (13)
Projects/Presentation/Resources/Images.xcassets/RoutineCategory/Contents.json (1)
1-6: 상위 카탈로그 메타 구성 적절Images.xcassets 내 RoutineCategory 그룹의 Contents.json이 표준 구조(info.author/version)로 잘 생성되어 있습니다. 추가 조치는 없어 보입니다.
Projects/Presentation/Resources/Images.xcassets/RoutineCategory/connect_icon.imageset/Contents.json (2)
1-23: 에셋 정의 적절 — 1x/2x/3x 구성 및 메타 정보 OK파일명과 스케일이 일치하고 idiom/universal 설정도 정상입니다. 디자인 시스템에서
connect_icon을 참조하는 로직과도 일관성이 있어 보입니다.
1-23: 사용처와 에셋명 일치 확인 완료
RoutineCategoryType+의.connection케이스에서BitnagilIcon.connectIcon사용BitnagilIcon.connectIcon내부에서UIImage(named: "connect_icon")호출두 경로 모두
connect_icon에셋을 정확히 참조하므로 런타임 로딩 실패 우려가 없습니다.Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift (1)
72-72: 형식 변경으로 보이며 기능 영향 없음이 줄의 변경은 의미적 변경이 없어 보입니다. 추가 조치는 필요 없습니다.
Projects/Presentation/Resources/Colors.xcassets/RoutineCategory/Contents.json (1)
1-6: LGTM: 표준 xcassets 메타데이터정상적인 Contents.json이며 추가 조치 필요 없습니다.
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineLevelButton.swift (2)
44-47: displayName 적용 및 톤 조정 변경, 적절합니다
title→displayName전환과gray40색상 조정 모두 일관된 UX를 위해 적절해 보입니다.Also applies to: 53-53
75-77: update 로직의 displayName 반영, 적절합니다상태 변경 시 레이블이 즉시 반영되도록 잘 처리되었습니다.
Projects/Presentation/Sources/Common/View/TabBarView.swift (2)
52-52: 뷰 → 뷰컨트롤러 전환 반영 적절합니다
RecommendedRoutineViewController로 변경이 일관되고, UINavigationController 루트로도 적합합니다. DI로 주입되는RecommendedRoutineViewModel시그니처도 그대로여서 문제 없겠습니다.
52-52: 레거시RecommendedRoutineView참조 검증 완료전역 검색 결과(원래 제안한
rg -nP '\bRecommendedRoutineView\b'스크립트) Swift/스토리보드(.storyboard)/XIB(.xib) 내에서 정확히 일치하는RecommendedRoutineView잔여 참조를 찾지 못했습니다. 컴파일 및 인터페이스 빌드 단계에서도 문제가 없으므로 해당 이슈는 해결된 것으로 판단됩니다.Projects/Presentation/Sources/Common/Component/SelectableItemCell.swift (1)
8-8: SnapKit 도입 OK오토레이아웃 일관성 측면에서 SnapKit 사용 추가 문제 없습니다.
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RoutineCategoryButton.swift (1)
34-40: 기본 스타일 변경(UI 토큰 반영) 적절합니다배경색을 흰색, 라벨을 medium/gray60으로 교체한 부분이 새 디자인 가이드와 일치해 보입니다.
Projects/Presentation/Sources/RecommendedRoutine/Model/RoutineCategoryType+.swift (2)
36-56: 아이콘/배경 컬러 매핑 추가 LGTM카테고리별 아이콘과 배경 컬러 토큰 매핑이 일관되고,
.outdoor/.outdoorReport묶음 처리도 적절합니다.RoutineCardView에서의 사용과도 자연스럽게 연결됩니다.
36-56: nil 반환 케이스 안전 처리 확인됨
- Projects/Presentation/Sources/Common/Component/RoutineCardView.swift (155–156)
•routineCategory.iconBackgroundColor ?? BitnagilColor.yellow10
•routineCategory.iconImage ?? BitnagilIcon.shineIcon- Projects/Presentation/Sources/Login/View/Component/SocialLoginButton.swift (77)
•UIImageView.image에 optional 할당(값이 nil이면 단순히 이미지 없음)두 소비 지점 모두 강제 언래핑 없이 nil을 안전하게 처리하고 있어 UI 깨짐이 발생하지 않습니다.
| subRoutineLabel.snp.makeConstraints { make in | ||
| make.height.equalTo(Layout.subRoutineLabelHeight) | ||
| } | ||
| subRoutineStackView.addArrangedSubview(subRoutineLabel) | ||
|
|
There was a problem hiding this comment.
UIStackView에 추가되기 전에 제약을 설치하고 있어 런타임 크래시 위험이 있습니다.
SnapKit 제약은 공통 슈퍼뷰가 있어야 설치 가능합니다. subRoutineLabel을 arrangedSubview로 추가한 뒤 제약을 설정하세요.
아래와 같이 순서를 교체해 주세요.
- subRoutineLabel.snp.makeConstraints { make in
- make.height.equalTo(Layout.subRoutineLabelHeight)
- }
- subRoutineStackView.addArrangedSubview(subRoutineLabel)
+ subRoutineStackView.addArrangedSubview(subRoutineLabel)
+ subRoutineLabel.snp.makeConstraints { make in
+ make.height.equalTo(Layout.subRoutineLabelHeight)
+ }📝 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.
| subRoutineLabel.snp.makeConstraints { make in | |
| make.height.equalTo(Layout.subRoutineLabelHeight) | |
| } | |
| subRoutineStackView.addArrangedSubview(subRoutineLabel) | |
| subRoutineStackView.addArrangedSubview(subRoutineLabel) | |
| subRoutineLabel.snp.makeConstraints { make in | |
| make.height.equalTo(Layout.subRoutineLabelHeight) | |
| } |
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Common/Component/RoutineCardView.swift around
lines 76–80, the code installs SnapKit constraints on subRoutineLabel before it
is added to the UIStackView which can cause a runtime crash because constraints
require a common superview; move
subRoutineStackView.addArrangedSubview(subRoutineLabel) to come before
subRoutineLabel.snp.makeConstraints { ... } so the label is in the stack view
(common superview) when constraints are activated.
There was a problem hiding this comment.
superView에 접근하는 것은 아니라 괜찮을 것 같은데, 문제가 될까요?
There was a problem hiding this comment.
🧩 Analysis chain
🏁 Script executed:
#!/bin/bash
# Let's examine the SnapKit behavior and look for similar patterns in the codebase
echo "=== Searching for similar SnapKit constraint patterns in the codebase ==="
rg -n "\.snp\.makeConstraints" -A 3 -B 1 | head -20
echo -e "\n=== Looking for UIStackView addArrangedSubview patterns ==="
rg -n "addArrangedSubview" -A 2 -B 2 | head -15
echo -e "\n=== Checking for height constraint patterns ==="
rg -n "make\.height\.equalTo" -A 1 -B 1 | head -15
Length of output: 6313
🌐 Web query:
SnapKit iOS constraints view hierarchy superview requirement UIStackView addArrangedSubview order
💡 Result:
SnapKit requires that constraints are made between views that share a common superview in the hierarchy, and for views within a UIStackView, constraints should not typically reference the stackView itself as superview; instead, you should constrain subviews within the stack to their own content, or use container views if more control is needed[3][4][7][1]. When using UIStackView’s addArrangedSubview method, the order you add subviews determines their display order, and each addArrangedSubview also makes the view a subview of the stack view—there’s no extra step needed[5][2][8].
Context and Details:
-
SnapKit Superview Requirement:
SnapKit creates constraints between views, usually to their superview, usingmakeConstraintsorequalToSuperview()syntax[4][7][1].
Example:box.snp.makeConstraints { make in make.edges.equalToSuperview().inset(20) }
The constrained view must already be in its superview's hierarchy before constraints are defined; otherwise, layout errors may occur[4][7][10].
-
UIStackView and Constraints:
UIStackView manages its arrangedSubviews’ layout automatically. If you directly constrain an arrangedSubview’s edges to the stackView (e.g.make.left.right.equalToSuperview()inside the stackView’s arrangedSubview), only certain constraints will work; others may be ignored or conflict, because the stackView itself only manages the placement of arrangedSubviews and not their internal layout[3][5].- If you need more control (e.g. pin left/right), the recommended approach is:
- Add a container UIView as an arrangedSubview to the stackView.
- Add your custom view inside the container.
- Apply SnapKit constraints between your custom view and the container[3].
Example (from [3]):
let containerView = UIView() let customView = UIView() containerView.addSubview(customView) customView.snp.makeConstraints { make in make.left.right.equalToSuperview().inset(15) make.height.equalTo(44) } stackView.addArrangedSubview(containerView)
- If you need more control (e.g. pin left/right), the recommended approach is:
-
arrangedSubviews and Hierarchy:
When you useaddArrangedSubview(view)orinsertArrangedSubview(view, at:), UIKit automatically adds the view as a subview of the stackView (i.e., you do not need to calladdSubviewyourself)[5].
The order in which you calladdArrangedSubvieworinsertArrangedSubviewdetermines how the views appear in the stack (top-to-bottom or left-to-right depending on stack axis)[2][5][8]. -
Removal:
- To remove from the arranged subview list:
stackView.removeArrangedSubview(view)(removes layout involvement, but not from the view hierarchy)[5]. - To remove the view completely:
view.removeFromSuperview()[5].
- To remove from the arranged subview list:
-
Constraint Pitfalls:
- Directly pinning edges of a view inside a UIStackView to the stackView (e.g.
make.left.right.equalToSuperview()) may not always work as expected, due to how UIStackView manages the frames of its arrangedSubviews[3]. - Use container views for complex layout needs, or rely on StackView’s alignment/distribution/content hugging and compression resistance properties for simpler cases[3][2][5].
- Directly pinning edges of a view inside a UIStackView to the stackView (e.g.
Summary Table: SnapKit + UIStackView Behavior
| Normal UIView Constraints | UIStackView arrangedSubview Constraints | |
|---|---|---|
| Superview requirement | Must be in superview for SnapKit | ArrangedSubview is auto-added as subview |
| addArrangedSubview | N/A | Adds view as subview and as arrangedSubview |
| Constraint location | To direct superview or siblings | NOT directly to UIStackView; use containers |
| Order of appearance | addSubview order is z-index | addArrangedSubview order is display order |
In summary:
- Use SnapKit’s constraints only once views are in appropriate superview hierarchy[4][7][10].
- With UIStackView, add views via
addArrangedSubviewfor display order[5][2]. - For custom constraints inside stack views, add a container view as arrangedSubview, and constrain your actual view inside that container[3].
- Direct constraint of arrangedSubview's edges to the stackView is not supported by UIKit; rely on stackView’s properties or containers for customization[3][2][5].
Citations:
- 1: https://iosapptemplates.com/blog/ios-development/snapkit-tutorial-ios
- 2: https://www.mikegopsill.com/posts/stackviews/
- 3: Make right and left constraints in UIStackView SnapKit/SnapKit#603
- 4: https://snapkit.github.io/SnapKit/docs/
- 5: https://nshipster.com/uistackview/
- 6: https://betterprogramming.pub/how-to-use-snapkit-in-your-ios-apps-cbe38c893b23
- 7: https://www.kodeco.com/3225401-snapkit-for-ios-constraints-in-a-snap
- 8: https://uynguyen.github.io/2020/07/18/iOS-Introducing-Stack-Views/
- 9: https://developer.apple.com/documentation/uikit/uistackview?changes=_5&language=objc
- 10: https://en.proft.me/2020/07/14/snapkit-tutorial-easy-auto-layout-constraints/
@taipaise 좋은 질문입니다! 기술적으로 살펴보면 다음과 같습니다:
현재 코드가 당장 크래시를 일으키지 않을 수 있는 이유:
make.height.equalTo(Layout.subRoutineLabelHeight)는 다른 뷰를 참조하지 않는 intrinsic constraint입니다- 단순한 높이 제약이므로 상대적으로 안전할 수 있습니다
하지만 여전히 문제가 될 수 있는 이유:
- SnapKit의 내부 동작: SnapKit은 제약을 설치할 때 뷰 계층에서 적절한 위치를 찾아야 합니다
- 일관성 부족: 코드베이스의 다른 부분들을 보면 대부분 뷰를 슈퍼뷰에 추가한 후 제약을 설정하고 있습니다
- iOS 버전별 차이: iOS 버전이나 SnapKit 버전에 따라 동작이 달라질 수 있습니다
권장하는 순서:
// 안전하고 일관된 방법
subRoutineStackView.addArrangedSubview(subRoutineLabel)
subRoutineLabel.snp.makeConstraints { make in
make.height.equalTo(Layout.subRoutineLabelHeight)
}현재 코드가 동작할 가능성이 높지만, 베스트 프랙티스를 따르는 것이 더 안전하고 유지보수에 좋습니다. 어떻게 생각하시나요?
✏️ Learnings added
Learnt from: taipaise
PR: YAPP-Github/Bitnagil-iOS#51
File: Projects/Presentation/Sources/Common/Component/RoutineCardView.swift:76-80
Timestamp: 2025-08-17T13:30:29.321Z
Learning: In SnapKit, even height constraints (intrinsic constraints) should be applied after adding the view to its superview hierarchy. While simple height constraints might work before adding to superview, it's not guaranteed and goes against best practices. The recommended approach is to call addArrangedSubview first, then apply constraints.
Note: Learnings are effective only in the context of similar code segments. To apply general coding guidelines, please configure review instructions. You can manage existing learnings in the app.
| ["물 마시기", "물 마시기", "물 마시기"].forEach { | ||
| let subRoutineTitleLabel = UILabel() | ||
| subRoutineTitleLabel.text = "• \($0)" | ||
| subRoutineTitleLabel.font = BitnagilFont(style: .body2, weight: .medium).font | ||
| subRoutineTitleLabel.textColor = BitnagilColor.gray40 | ||
| subRoutineTitleLabel.snp.makeConstraints { make in | ||
| make.height.equalTo(Layout.subRoutineLabelHeight) | ||
| } | ||
| subRoutineStackView.addArrangedSubview(subRoutineTitleLabel) | ||
| } |
There was a problem hiding this comment.
반복 구간에서도 동일한 제약 설치 순서 문제 발생
각 subRoutineTitleLabel도 UIStackView에 추가하기 전에 제약을 설치하고 있습니다. 순서를 바꾸세요. 또한 더미 텍스트는 다음 PR에서 실제 데이터 바인딩으로 교체 예정이므로 TODO 표기 권장합니다.
수정 예시는 다음과 같습니다.
- ["물 마시기", "물 마시기", "물 마시기"].forEach {
+ // TODO(T3-167-followup): 실제 서버 v2 연동 시 바인딩된 세부 루틴 목록으로 교체
+ ["물 마시기", "물 마시기", "물 마시기"].forEach {
let subRoutineTitleLabel = UILabel()
subRoutineTitleLabel.text = "• \($0)"
subRoutineTitleLabel.font = BitnagilFont(style: .body2, weight: .medium).font
subRoutineTitleLabel.textColor = BitnagilColor.gray40
- subRoutineTitleLabel.snp.makeConstraints { make in
- make.height.equalTo(Layout.subRoutineLabelHeight)
- }
- subRoutineStackView.addArrangedSubview(subRoutineTitleLabel)
+ subRoutineStackView.addArrangedSubview(subRoutineTitleLabel)
+ subRoutineTitleLabel.snp.makeConstraints { make in
+ make.height.equalTo(Layout.subRoutineLabelHeight)
+ }
}📝 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.
| ["물 마시기", "물 마시기", "물 마시기"].forEach { | |
| let subRoutineTitleLabel = UILabel() | |
| subRoutineTitleLabel.text = "• \($0)" | |
| subRoutineTitleLabel.font = BitnagilFont(style: .body2, weight: .medium).font | |
| subRoutineTitleLabel.textColor = BitnagilColor.gray40 | |
| subRoutineTitleLabel.snp.makeConstraints { make in | |
| make.height.equalTo(Layout.subRoutineLabelHeight) | |
| } | |
| subRoutineStackView.addArrangedSubview(subRoutineTitleLabel) | |
| } | |
| // TODO(T3-167-followup): 실제 서버 v2 연동 시 바인딩된 세부 루틴 목록으로 교체 | |
| ["물 마시기", "물 마시기", "물 마시기"].forEach { | |
| let subRoutineTitleLabel = UILabel() | |
| subRoutineTitleLabel.text = "• \($0)" | |
| subRoutineTitleLabel.font = BitnagilFont(style: .body2, weight: .medium).font | |
| subRoutineTitleLabel.textColor = BitnagilColor.gray40 | |
| subRoutineStackView.addArrangedSubview(subRoutineTitleLabel) | |
| subRoutineTitleLabel.snp.makeConstraints { make in | |
| make.height.equalTo(Layout.subRoutineLabelHeight) | |
| } | |
| } |
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Common/Component/RoutineCardView.swift around
lines 81 to 90, each subRoutineTitleLabel currently has constraints applied
before being added to the UIStackView; move the constraint installation to after
you add the label to subRoutineStackView. Create the label, set its properties
(text should be a TODO placeholder comment instead of real dummy text), add it
with subRoutineStackView.addArrangedSubview(subRoutineTitleLabel), then call
subRoutineTitleLabel.snp.makeConstraints { make in
make.height.equalTo(Layout.subRoutineLabelHeight) } so constraints are applied
after insertion into the stack; also replace the duplicate hard-coded text with
a TODO marker to indicate future data binding.
| guard let displayName = item.displayName else { | ||
| titleLabel.text = item.description | ||
| return | ||
| } | ||
| let attributedString = NSMutableAttributedString(string: item.description) | ||
| attributedString.addAttribute( | ||
| .font, | ||
| value: BitnagilFont(style: .body1, weight: .regular).font, | ||
| range: NSRange(location: 0, length: item.description.count)) | ||
|
|
||
| if let range = item.description.range(of: displayName) { | ||
| let nsRange = NSRange(range, in: item.description) | ||
| attributedString.addAttributes([ | ||
| .font: BitnagilFont(style: .body1, weight: .semiBold).font | ||
| ], range: nsRange) | ||
| } | ||
| titleLabel.attributedText = attributedString | ||
| } |
There was a problem hiding this comment.
String.count로 NSRange 구성 → 크래시 가능성, 그리고 타이포그래피 속성 누락
NSRange(location: 0, length: item.description.count)는 Swift String(문자 수)과 NSAttributedString(UTF-16 코드 유닛 수) 차이로 인해 이모지/조합형 문자 포함 시 크래시가 발생할 수 있습니다.- 또한 기본 폰트만 지정되어 BitnagilFont가 제공하는 lineHeight/letterSpacing(kerning) 등 타이포그래피 속성이 반영되지 않습니다. 제공된
BitnagilFont(style:weight:).attributedString(text:)를 활용하면 설계된 토큰을 일관되게 적용할 수 있습니다.
아래와 같이 수정하면 안전한 범위 계산과 디자인 토큰 일관 적용을 동시에 해결할 수 있습니다.
- guard let displayName = item.displayName else {
- titleLabel.text = item.description
- return
- }
- let attributedString = NSMutableAttributedString(string: item.description)
- attributedString.addAttribute(
- .font,
- value: BitnagilFont(style: .body1, weight: .regular).font,
- range: NSRange(location: 0, length: item.description.count))
-
- if let range = item.description.range(of: displayName) {
- let nsRange = NSRange(range, in: item.description)
- attributedString.addAttributes([
- .font: BitnagilFont(style: .body1, weight: .semiBold).font
- ], range: nsRange)
- }
- titleLabel.attributedText = attributedString
+ // 기본 타이포그래피(라인하이트/커닝 포함)를 BitnagilFont 토큰으로 구성
+ let baseFont = BitnagilFont(style: .body1, weight: .regular)
+ let attributedString = NSMutableAttributedString(
+ attributedString: baseFont.attributedString(text: item.description)
+ )
+
+ // displayName이 존재하면 해당 구간만 강조(세미볼드)
+ if let displayName = item.displayName,
+ let range = item.description.range(of: displayName) {
+ let nsRange = NSRange(range, in: item.description) // UTF-16 안전 변환
+ attributedString.addAttributes([
+ .font: BitnagilFont(style: .body1, weight: .semiBold).font
+ ], range: nsRange)
+ }
+ titleLabel.attributedText = attributedString📝 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.
| guard let displayName = item.displayName else { | |
| titleLabel.text = item.description | |
| return | |
| } | |
| let attributedString = NSMutableAttributedString(string: item.description) | |
| attributedString.addAttribute( | |
| .font, | |
| value: BitnagilFont(style: .body1, weight: .regular).font, | |
| range: NSRange(location: 0, length: item.description.count)) | |
| if let range = item.description.range(of: displayName) { | |
| let nsRange = NSRange(range, in: item.description) | |
| attributedString.addAttributes([ | |
| .font: BitnagilFont(style: .body1, weight: .semiBold).font | |
| ], range: nsRange) | |
| } | |
| titleLabel.attributedText = attributedString | |
| } | |
| // 기본 타이포그래피(라인하이트/커닝 포함)를 BitnagilFont 토큰으로 구성 | |
| let baseFont = BitnagilFont(style: .body1, weight: .regular) | |
| let attributedString = NSMutableAttributedString( | |
| attributedString: baseFont.attributedString(text: item.description) | |
| ) | |
| // displayName이 존재하면 해당 구간만 강조(세미볼드) | |
| if let displayName = item.displayName, | |
| let range = item.description.range(of: displayName) { | |
| let nsRange = NSRange(range, in: item.description) // UTF-16 안전 변환 | |
| attributedString.addAttributes([ | |
| .font: BitnagilFont(style: .body1, weight: .semiBold).font | |
| ], range: nsRange) | |
| } | |
| titleLabel.attributedText = attributedString | |
| } |
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Common/Component/SelectableItemCell.swift
around lines 60 to 77, the code builds an NSRange using item.description.count
(unsafe for extended grapheme clusters) and constructs the attributed string by
manually setting only the font which misses BitnagilFont's typography tokens;
fix by creating the base attributed string with BitnagilFont(style: .body1,
weight: .regular).attributedString(text: item.description) to inherit
lineHeight/kerning, compute the highlight range using NSString (let nsString =
item.description as NSString; let nsRange = nsString.range(of: displayName)) so
ranges are UTF-16 safe, and then apply BitnagilFont(style: .body1, weight:
.semiBold).font (or its attributed variant) to that nsRange before assigning to
titleLabel.attributedText.
| // MARK: - Category Color | ||
| static let green10 = UIColor(named: "Green10", in: bundle, compatibleWith: nil) | ||
| static let pink10 = UIColor(named: "Pink10", in: bundle, compatibleWith: nil) | ||
| static let purple10 = UIColor(named: "Purple10", in: bundle, compatibleWith: nil) | ||
| static let skyblue10 = UIColor(named: "Skyblue10", in: bundle, compatibleWith: nil) | ||
| static let yellow10 = UIColor(named: "Yellow10", in: bundle, compatibleWith: nil) |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
새 카테고리 컬러 토큰 추가 잘 적용되었습니다. 다만 에셋 존재 여부 점검을 권장합니다.
UIColor(named:)는 런타임 시 에셋 미존재/오탈자일 경우 nil을 반환합니다. 신규 토큰들이 실제 .xcassets에 모두 존재하는지 빠르게 검증해 주세요. 특히 "Skyblue10" 대소문자 표기 일관성도 확인이 필요합니다.
아래 스크립트로 에셋 존재 여부를 확인할 수 있습니다.
추가로, nil 안전성을 높이려면 기본값을 두는 접근(예: BitnagilColor.gray98 등)을 도입하는 것도 고려해 주세요.
[suggest_optional_refactor]
🏁 Script executed:
#!/bin/bash
set -euo pipefail
# 신규 컬러 토큰과 에셋 이름의 일치 여부 확인
declare -a COLORS=("Green10" "Pink10" "Purple10" "Skyblue10" "Yellow10")
missing=0
for c in "${COLORS[@]}"; do
path=$(fd -t f "$c.colorset/Contents.json" | head -n1 || true)
if [[ -z "$path" ]]; then
echo "MISSING: $c.colorset"
missing=1
else
echo "FOUND: $path"
fi
done
exit $missingLength of output: 1008
다음 Color Set 에셋이 .xcassets에 존재하지 않아 런타임에 nil을 반환합니다:
- Green10.colorset
- Pink10.colorset
- Purple10.colorset
- Skyblue10.colorset
- Yellow10.colorset
각 색상 세트의 이름(대소문자 포함)이 .xcassets 내에 정확히 일치하도록 추가하거나 수정해주세요.
추가로, UIColor(named:)가 nil을 반환할 경우를 대비해 기본값(BitnagilColor.gray98 등)을 함께 적용하는 nil-안전성 보강도 권장드립니다.
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Common/DesignSystem/BitnagilColor.swift around
lines 37 to 42, the named color lookups return nil because the Color Sets
(Green10, Pink10, Purple10, Skyblue10, Yellow10) are missing or misnamed in the
.xcassets; add or rename those .colorset entries in the asset catalog to match
the exact names (including capitalization), and update the code to be nil-safe
by providing a fallback color (e.g., BitnagilColor.gray98) when
UIColor(named:in:compatibleWith:) returns nil so the app won’t crash or render
unexpectedly.
| stackView.snp.makeConstraints { make in | ||
| make.edges.equalToSuperview() | ||
| } |
There was a problem hiding this comment.
빈 상태 뷰가 모호한 레이아웃 제약 상태입니다 (width/height 미정).
VC에서 recommendedRoutineEmptyView에 centerX, centerY만 주고 있어(참고: RecommendedRoutineViewController.swift Line 140-149) 이 뷰는 고유 크기(intrinsic size)가 없기 때문에 Auto Layout 상 모호합니다. 현재 내부 stackView를 superview에 edge로 붙이고 있어도, 상위 뷰에 폭/높이 제약이 없어서 경고가 발생할 수 있습니다.
- 권장: VC에서
leading/trailing까지 함께 잡아 폭을 결정해 주세요.
다음과 같이 VC 제약을 보완해 주세요:
// RecommendedRoutineViewController.configureLayout 내
recommendedRoutineEmptyView.snp.makeConstraints { make in
make.centerY.equalToSuperview().offset(Layout.headerStackViewHeight)
make.leading.equalTo(safeArea).offset(Layout.horizontalMargin)
make.trailing.equalTo(safeArea).inset(Layout.horizontalMargin)
// centerX는 leading/trailing로 대체되므로 생략 가능
}🤖 Prompt for AI Agents
In
Projects/Presentation/Sources/RecommendedRoutine/View/Component/RecommendedRoutineEmptyView.swift
around lines 47 to 49, the empty view is ambiguous in Auto Layout because it has
no intrinsic content size and its internal stackView is only pinned to the view
edges; update the view controller constraints instead to give the empty view a
concrete width (and position) by adding leading and trailing constraints in
RecommendedRoutineViewController.configureLayout (use safeArea +
Layout.horizontalMargin) and keep the vertical placement via centerY (or offset)
— remove or replace centerX with the leading/trailing constraints so the empty
view is no longer ambiguous.
| subRoutineLabel.snp.makeConstraints { make in | ||
| make.height.equalTo(Layout.subRoutineLabelHeight) | ||
| } | ||
| subRoutineStackView.addArrangedSubview(subRoutineLabel) | ||
|
|
There was a problem hiding this comment.
superView에 접근하는 것은 아니라 괜찮을 것 같은데, 문제가 될까요?
🌁 Background
추천 루틴 화면 수정된 디자인을 반영했어요 ~
📱 Screenshot
1. 감정 구슬 등록 버튼이 있는 추천 루틴 리스트 화면
2. 감정 구슬 등록 버튼이 없는 추천 루틴 리스트 화면
3. 추천 루틴 리스트 난이도 설정 화면
4. 추천 루틴 리스트 Empty View
👩💻 Contents
📝 Review Note
1. RoutineCardView
RoutineCardView를 루틴 리스트, 추천 루틴 리스트에서 재사용할 수 있을 것이라 생각해서
Common/Component에 위치시켰습니당 !!!하지만 현재는 추천 루틴에 대한 UI만 만들었고 ~ 루틴 리스트 만들때 이어 붙이고 분기처리 하겟씀니다 !!!
2. 서버 v2 연동 작업은 not yet !!!!!!
아직 서버 v2 연동 작업에 들어가지 않아 그냥 Dummy RoutineCardView를 StackView 상단에 넣어보는 것으로 테스트 해봤습니다 !!
다음 PR에 서버 연동해서 기존 RecommendedRoutineCardView를 삭제하고 RoutineCardView로 대체하겠습니다 !
3. SelectableItemTableView, SelectableItemCell
이전 홈 v1 디자인 PR에서 선택 테이블 뷰가 분명 재사용이 가능하여서 ..
재사용할 수 있도록 프로토콜 만들고, 제네릭으로 한 것인데요 .. ㅠㅠ
추천 루틴에서 난이도 선택하는 뷰 말고는 다 사라진거 있죠 ㅠㅠ !!!!!!
아직 미래에 추가될 가능성(저희 얍 끝나고도 .. 다른 기능을 붙일 가능성이 높으니까 !!!)도 있고 .... 시간두 엄청 많지 않어서
일단 SelectableItemTableView, SelectableItemCell 요 녀석들은 유지한 채, 추천 루틴 난이도 선택에서 사용하는 용도로만 했어유 ..... ㅠㅠ
만약 기능 추가되도 비슷한 뷰가 안만들어지면 ... 얘만을 위한 아이로 바꾸겠슴니다 !!!!
📣 Related Issue
Summary by CodeRabbit
신기능
변경
제거