Conversation
- DateView, WeekView 수정된 디자인 반영 - WeekView 로직 HomeViewModel로 이동
Walkthrough감정 API 단일 조회가 fetchEmotion→loadEmotion로 명칭 변경되며 v2 엔드포인트를 사용합니다. 감정 모델/DTO/엔터티에 optional 메시지 필드가 추가되었습니다. 홈 화면 전반 UI가 헤더 중심으로 재구성되고 주간 선택/네비게이션 로직이 단순화되었습니다. 아이콘/이미지 에셋과 디자인 토큰이 대거 갱신되었습니다. Changes
Sequence Diagram(s)sequenceDiagram
participant View as HomeView
participant VM as HomeViewModel
participant UC as EmotionUseCase
participant Repo as EmotionRepository
participant EP as EmotionEndpoint
participant API as Server
Note over View,VM: onAppear or 날짜 변경
View->>VM: action(.loadInitial / selectedDate)
VM->>UC: loadEmotion(date: Date)
UC->>Repo: loadEmotion(date: "yyyy-MM-dd")
Repo->>EP: .loadEmotion(date)
EP->>API: GET /api/v2/emotion-marbles/{date}
API-->>Repo: EmotionResponseDTO (message?)
Repo-->>UC: EmotionEntity(message?)
UC-->>VM: EmotionEntity?
VM-->>View: state update (emoji/message or empty)
sequenceDiagram
participant User
participant Week as WeekView
participant Home as HomeView
participant VM as HomeViewModel
User->>Week: 날짜 탭
Week->>Home: didSelectDate(date)
Home->>VM: selectedDateSubject.send(date)
VM-->>Home: selectedDatePublisher(date)
Home->>Week: updateWeekDateViews(date)
Note over VM: Input.moveWeek(week: ±1)
Home->>VM: action(.moveWeek(week))
VM-->>Home: selectedDatePublisher(newDate)
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 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
|
Docstrings generation was requested by @taipaise. * #47 (comment) The following files were modified: * `Projects/DataSource/Sources/DTO/EmotionResponseDTO.swift` * `Projects/DataSource/Sources/Repository/EmotionRepository.swift` * `Projects/Domain/Sources/UseCase/Emotion/EmotionUseCase.swift` * `Projects/Presentation/Sources/Common/Component/FloatingButton.swift` * `Projects/Presentation/Sources/Common/Component/FloatingMenuView.swift` * `Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift` * `Projects/Presentation/Sources/Common/View/TabBarView.swift` * `Projects/Presentation/Sources/EmotionRegister/Model/Emotion.swift` * `Projects/Presentation/Sources/Home/View/Component/DateView.swift` * `Projects/Presentation/Sources/Home/View/Component/HomeEmptyView.swift` * `Projects/Presentation/Sources/Home/View/Component/HomeRegisterEmotionButton.swift` * `Projects/Presentation/Sources/Home/View/Component/RoutineView.swift` * `Projects/Presentation/Sources/Home/View/Component/WeekView.swift` * `Projects/Presentation/Sources/Home/View/HomeView.swift` * `Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift` * `Projects/Presentation/Sources/RecommendedRoutine/ViewModel/RecommendedRoutineViewModel.swift`
|
Note Generated docstrings for this pull request at #48 |
There was a problem hiding this comment.
Actionable comments posted: 10
🔭 Outside diff range comments (3)
Projects/Presentation/Sources/Home/View/Component/DateView.swift (1)
76-86: 제약 충돌 가능성: dayLabel ↔ dateButton 폭 동기화와 고정 사이즈가 상충현재 제약은 다음과 같이 과제약(over-constrained)입니다.
- dayLabel: leading/trailing = superview 고정 + width = dateButton.width
- dateButton: horizontalEdges = dayLabel + size(= 30) ⇒ width 고정
즉 dateButton.width는 30으로, 동시에 dayLabel과 동일 폭이어야 하고, dayLabel은 superview 양 끝에 붙어 있어 superview 폭과 같아야 합니다. 이로 인해 Auto Layout 충돌이 발생할 수 있습니다.
중앙 정렬로 의도를 유지하면서 충돌을 제거하는 제안을 드립니다.
dayLabel.snp.makeConstraints { make in make.top.leading.trailing.equalToSuperview() make.height.equalTo(Layout.dayLabelHeight) - make.width.equalTo(dateButton.snp.width) } dateButton.snp.makeConstraints { make in make.top.equalTo(dayLabel.snp.bottom).offset(Layout.dateButtonTopSpacing) - make.horizontalEdges.equalTo(dayLabel) + make.centerX.equalTo(dayLabel) make.size.equalTo(Layout.dateButtonSize) }Projects/Presentation/Sources/RecommendedRoutine/ViewModel/RecommendedRoutineViewModel.swift (1)
121-124: 에러 핸들링 누락: 로깅 및 기본 상태 전파 필요오류를 삼키면 사용자에게 상태가 갱신되지 않아 혼란을 줄 수 있습니다. 위의 fetchRecommendedRoutines와 동일하게 로깅하고, 기본값(false) 전파를 권장합니다.
} catch { - + BitnagilLogger.log(logType: .error, message: "loadEmotion 실패: \(error.localizedDescription)") + emotionExistSubject.send(false) }Projects/DataSource/Sources/Repository/EmotionRepository.swift (1)
22-29: 수정 필요: loadEmotion의 Optional 반환 의미에 맞게 response가 없을 땐 nil을 반환하세요간단 설명: Presentation 레이어(RecommendedRoutineViewModel, HomeViewModel)가 nil 여부로 UI를 분기하고 있으므로, Repository에서 response가 없는 정상 흐름(예: 204/404)은 예외가 아니라 nil로 반환해야 합니다. 현재 구현은 response == nil일 때 throw 하고 있어 UI가 의도치 않게 catch로 빠질 수 있습니다.
필요한 위치(확인됨):
- Projects/DataSource/Sources/Repository/EmotionRepository.swift — func loadEmotion(date: String) (현재 22–29행)
- 호출부(참고):
- Projects/Presentation/Sources/RecommendedRoutine/ViewModel/RecommendedRoutineViewModel.swift (loadEmotion에서 emotionEntity == nil로 분기함)
- Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift (emotionEntity?.toEmotion() 사용)
- 도메인/프로토콜: Projects/Domain/.../EmotionUseCaseProtocol.swift, EmotionUseCase.swift — 반환 타입이 EmotionEntity? 으로 명시되어 있음
권장 변경(diff):
- guard let response = try await networkService.request(endpoint: endpoint, type: EmotionResponseDTO.self) - else { throw NetworkError.unknown(description: "Emotion Reponse를 받아오지 못했습니다.") } + guard let response = try await networkService.request(endpoint: endpoint, type: EmotionResponseDTO.self) + else { return nil } // 204/404 등: 감정 미등록 -> nil 반환추가:
- 예외(네트워크 실패/서버 오류/디코딩 에러)는 그대로 throw 하되, "데이터 없음"과 구분되도록 NetworkService가 상태코드(204/404)와 디코딩 실패를 명확히 구분하는 것이 좋습니다.
- 오타 수정: "Reponse" → "Response"
🧹 Nitpick comments (39)
Projects/Presentation/Resources/Images.xcassets/Home/unchecked_circle_icon.imageset/Contents.json (1)
1-24: 틴트 적용 아이콘이면 template-rendering-intent 설정 권장unchecked 상태 아이콘이 단색으로 틴트를 통해 색상을 바꿔 쓰는 용도라면 템플릿 렌더링을 명시해두는 것이 좋습니다. Interface Builder/코드에서의 실수(원본 색상 렌더링) 방지에 유용합니다.
다음과 같이 properties 블록을 추가해 주세요:
{ "images" : [ { "filename" : "unchecked_circle_icon.png", "idiom" : "universal", "scale" : "1x" }, { "filename" : "unchecked_circle_icon@2x.png", "idiom" : "universal", "scale" : "2x" }, { "filename" : "unchecked_circle_icon@3x.png", "idiom" : "universal", "scale" : "3x" } ], "info" : { "author" : "xcode", "version" : 1 - } + }, + "properties": { + "template-rendering-intent": "template" + } }추가로, 만약 이 아이콘이 벡터(PDF) 리소스로 관리 가능하다면 PNG 1x/2x/3x 대신 단일 PDF로의 전환을 고려해 보세요(앱 용량 및 유지보수 측면에서 유리).
Projects/Presentation/Sources/Common/Component/PrimaryButton.swift (1)
64-67: 비활성(.disabled) 타이틀 색상 명시 설정 제안현재 setTitleColor는 .normal만 지정합니다. UIKit 기본 동작으로 .disabled는 .normal을 fallback 하긴 하지만, 알파 감소 등 시스템 디밍과 충돌 여지를 줄이기 위해 .disabled 상태에도 명시 지정하는 것을 권장합니다.
다음 수정안을 고려해 주세요:
private func updateButtonUI() { - setTitleColor(buttonState.buttonTextColor, for: .normal) + setTitleColor(buttonState.buttonTextColor, for: .normal) + setTitleColor(buttonState.buttonTextColor, for: .disabled) backgroundColor = buttonState.buttonColor }Projects/Presentation/Resources/Images.xcassets/Home/chevron_right_icon.imageset/Contents.json (1)
1-23: 탬플릿 아이콘이면 template-rendering-intent 속성 추가를 권장합니다chevron 아이콘은 보통 틴트 컬러로 사용됩니다. 에셋 카탈로그에 template 속성을 지정해두면 매 사용처에서 renderingMode를 일일이 설정하지 않아도 일관된 틴팅이 가능합니다.
아래와 같이 properties 블록을 추가해 주세요:
], + "properties" : { + "template-rendering-intent" : "template" + }, "info" : {Projects/Presentation/Resources/Images.xcassets/Home/alarm_badge_icon.imageset/Contents.json (1)
4-16: 이 아이콘이 단색 틴트 대상인지 확인 필요알람 배지 아이콘이 단색으로 시스템/디자인 토큰 컬러에 맞춰 틴트되는 용도라면 template-rendering-intent를 추가해 두는 편이 좋습니다. 만약 멀티컬러 그래픽이라면 현 상태 유지가 맞습니다.
단색 틴트 대상일 경우 적용 diff 예시:
], + "properties" : { + "template-rendering-intent" : "template" + }, "info" : {사용 의도(단색/멀티컬러)를 확인해 주시면 필요 시 위 변경 반영하겠습니다.
Projects/Presentation/Sources/Common/DesignSystem/BitnagilGraphic.swift (1)
11-13: 리소스 번들 참조를 UI 클래스 의존 대신 토큰 클래스로 교체 권장IntroViewController에 번들 의존하면 모듈 분리/이동 시 간접 결합이 생깁니다. 파일 내부 토큰 클래스를 통해 번들을 얻도록 하면 안전합니다. SPM 사용 시 .module로도 대비해 두면 좋습니다.
- private static var bundle: Bundle { - return Bundle(for: IntroViewController.self) - } + private final class ResourceBundleToken {} + private static let bundle: Bundle = { + #if SWIFT_PACKAGE + return .module + #else + return Bundle(for: ResourceBundleToken.self) + #endif + }()Projects/Presentation/Resources/Images.xcassets/Home/unchecked_circle_small_icon.imageset/Contents.json (1)
1-23: 체크/언체크 원형 아이콘도 템플릿 속성 적용을 권장합니다상태 아이콘들은 보통 틴트 컬러로 재사용됩니다. 에셋에 template-rendering-intent를 지정해 일관된 렌더링을 보장하세요.
], + "properties" : { + "template-rendering-intent" : "template" + }, "info" : {Projects/Shared/Sources/Extension/Date+.swift (2)
10-17: DateFormatter 매 호출 생성 → 빈번 호출 시 캐싱/재사용 고려현재는 호출마다 DateFormatter를 생성합니다. 성능 이슈가 체감되지는 않더라도, UI 바인딩 등에서 포맷 호출이 잦다면 타입별 캐시를 두는 편이 효율적입니다(NSCache는 스레드 세이프). 선택 사항입니다.
예시(선택 적용):
extension Date { private static let localeKR = Locale(identifier: "ko_KR") private static let formatterCache = NSCache<NSString, DateFormatter>() private static func formatter(for type: DateType) -> DateFormatter { let key = type.formatString as NSString if let f = formatterCache.object(forKey: key) { return f } let f = DateFormatter() f.locale = localeKR f.dateFormat = type.formatString formatterCache.setObject(f, forKey: key) return f } public func convertToString(dateType: DateType) -> String { Date.formatter(for: dateType).string(from: self) } public static func convertToDate(from string: String, dateType: DateType) -> Date? { Date.formatter(for: dateType).date(from: string) } }Also applies to: 19-25
37-46: AM/PM 표기에서 24시간 토큰(HH) 혼용 가능성 확인 요청ko_KR 로케일에서 AM/PM 표기를 사용할 때는 보통 12시간 시각 토큰(h/ hh)을 함께 사용합니다. 현재
.amPmTime이"a HH:mm"으로 24시간 토큰과 혼용되어 있어, 의도와 다르게 표시될 수 있습니다. 길이감 차이를 주고 싶다면.amPmTime은 두 자리 시(hh),.amPmTimeShort는 한 자리 시(h)로 구분하는 방향을 권장합니다.아래와 같이 변경하는 것이 일반적입니다. (의도적으로 HH를 사용하신 것이라면 이 코멘트는 무시하셔도 됩니다.)
- case .amPmTime: "a HH:mm" + case .amPmTime: "a hh:mm"Projects/Presentation/Sources/Common/Component/FloatingMenuView.swift (1)
17-21: 탭(터치) 영역 44pt 권장현재 버튼 높이 24pt는 iOS HIG 권장 최소 탭 영역(44pt)에 미달합니다. 실제 컨테이너가 충분히 커서 보완되더라도 접근성/사용성 측면에서 버튼 자체 높이를 44로 올리는 것을 권장합니다.
아래처럼 높이만 상향하면 레이아웃 영향 최소화로 접근성 기준을 만족시킬 수 있습니다.
- static let registerRoutineButtonHeight: CGFloat = 24 + static let registerRoutineButtonHeight: CGFloat = 44Projects/Domain/Sources/Entity/EmotionEntity.swift (1)
14-26: 초기화 시그니처 변경에 대한 소스 호환성 완화 제안init에 emotionMessage가 추가되면서 기존 호출부가 모두 업데이트되어야 합니다. PR 전반에서 대응하신 것으로 보이나, 후속 작업/외부 모듈 호환성을 고려해 파라미터 기본값을 두면 소스 변경 부담이 줄어듭니다.
public init( emotionType: String, emotionName: String, - emotionImageUrl: URL?, - emotionMessage: String? + emotionImageUrl: URL? = nil, + emotionMessage: String? = nil ) {Projects/Presentation/Sources/Home/View/Component/HomeEmptyView.swift (2)
75-80: 중복 제약 제거로 간결화 제안emptySubLabel 에 대해 horizontalEdges와 centerX를 동시에 지정하고 있습니다. centerX는 불필요하므로 제거하여 제약을 단순화할 수 있습니다. 동작 변화는 없습니다.
emptySubLabel.snp.makeConstraints { make in make.horizontalEdges.equalToSuperview() make.top.equalTo(emptyMainLabel.snp.bottom).offset(Layout.emptySubLabelTopSpacing) - make.centerX.equalToSuperview() make.height.equalTo(Layout.emptySubLabelHeight) }
16-20: 접근성 측면에서 버튼 높이 44pt 권장현재 버튼 높이 36pt는 권장 최소 탭 영역(44pt)에 못 미칩니다. 시각적 디자인을 유지하면서도 터치 영역을 확장할 수 있다면 사용자 경험이 개선됩니다.
- static let registerRoutineButtonHeight: CGFloat = 36 + static let registerRoutineButtonHeight: CGFloat = 44Projects/Presentation/Sources/Home/View/Component/HomeRegisterEmotionButton.swift (5)
16-22: 오탈자(Backgroud) 및 불필요한 Optional 제거 제안프로퍼티명이
buttonBackgroudColor로 오탈자이며, 항상 색상을 반환하므로 Optional이 아닐 가능성이 큽니다. 명확성과 안전성을 위해 수정하는 것을 권장합니다.다음 변경으로 명명/타입을 개선할 수 있습니다:
- var buttonBackgroudColor: UIColor? { + var buttonBackgroundColor: UIColor { switch self { - case .default: BitnagilColor.orange500 - case .tap: BitnagilColor.orange600 - case .disabled: BitnagilColor.gray30 + case .default: BitnagilColor.orange500 + case .tap: BitnagilColor.orange600 + case .disabled: BitnagilColor.gray30 } } @@ - backgroundColor = buttonState.buttonBackgroudColor + backgroundColor = buttonState.buttonBackgroundColor @@ - backgroundColor = buttonState.buttonBackgroudColor + backgroundColor = buttonState.buttonBackgroundColorAlso applies to: 59-61, 72-74
24-37: 중복 문자열 제거: ButtonState의 buttonTitle/buttonTextColor 활용
buttonTitle과buttonTextColor를 이미 계산해두었지만 실제로는 하드코딩 문자열/색상을 반복 설정하고 있습니다. 상태 소스 오브 트루스를 단일화하면 유지보수성이 좋아집니다.아래와 같이 초기 설정과 UI 업데이트를 단순화할 수 있습니다:
- setTitle("오늘 감정 등록하기", for: .normal) - setTitleColor(ButtonState.default.buttonTextColor, for: .normal) + setTitle(buttonState.buttonTitle, for: .normal) + setTitleColor(buttonState.buttonTextColor, for: .normal) @@ - setTitle("오늘 감정 등록하기", for: .highlighted) + setTitle(ButtonState.tap.buttonTitle, for: .highlighted) setTitleColor(ButtonState.tap.buttonTextColor, for: .highlighted) @@ - setTitle("오늘 감정 등록완료", for: .disabled) + setTitle(ButtonState.disabled.buttonTitle, for: .disabled) setTitleColor(ButtonState.disabled.buttonTextColor, for: .disabled) @@ - backgroundColor = buttonState.buttonBackgroudColor + backgroundColor = buttonState.buttonBackgroundColor + // 정상 상태 텍스트/타이틀도 상태에 맞게 갱신 + setTitle(buttonState.buttonTitle, for: .normal) + setTitleColor(buttonState.buttonTextColor, for: .normal)참고:
.disabled/.highlighted용 설정은 그대로 두고,.normal에 대해서만 상태 변화 시 갱신하면 일관성을 유지할 수 있습니다.Also applies to: 62-70, 72-74
76-79: 상태 업데이트 순서 및 하이라이트 응답 처리 제안
- 현재 순서(상태 변경 → isEnabled 변경)도 동작하나, isEnabled의 부수효과(컨트롤 상태) 반영 후 색/타이틀을 갱신하려면 순서를 바꾸는 편이 자연스럽습니다.
- 또한
.tap상태는 현재 배경색 변화에 연결되어 있지 않습니다. 하이라이트 시 배경도 변경되도록 훅을 추가하면 UX가 개선됩니다.순서 조정:
- self.buttonState = buttonState - self.isEnabled = buttonState != .disabled + self.isEnabled = buttonState != .disabled + self.buttonState = buttonState하이라이트 연동(파일 내 적절한 위치에 추가):
// 강조(터치 다운/업)에 따라 상태 연동 override var isHighlighted: Bool { didSet { guard isEnabled else { return } let nextState: ButtonState = isHighlighted ? .tap : .default if buttonState != nextState { buttonState = nextState } } }
55-61: 코너 반경/마스킹 컨벤션 확인 (DesignSystem 일관성)
PrimaryButton에서는 cornerRadius 12(참고: Projects/Presentation/Sources/Common/Component/PrimaryButton.swift)인데, 여기서는 8입니다. 의도된 디자인 차이라면 OK이고, 아니면 디자인 토큰에 맞춰 정렬하는 것을 권장합니다. 또한layer.masksToBounds = true는 필요할 때만 사용하세요(그림자 등 효과에 영향).
62-69: 로컬라이제이션 키로 전환 권장한글 하드코딩 문자열은 Localizable.strings 키로 분리하는 것이 좋습니다. 예: "오늘 감정 등록하기", "오늘 감정 등록완료".
원하시면 로컬라이즈 키/파일 추가와 호출부 변경까지 패치 제안 드리겠습니다.
Projects/Presentation/Resources/Images.xcassets/Home/chevron_left_icon.imageset/Contents.json (1)
1-23: 새 좌측 체브론 에셋 추가 — 사용처 확인, RTL 처리 검토 필요에셋 추가 자체는 문제 없습니다. 사용처 확인 결과:
- Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift:37 — static let chevronLeftIcon = UIImage(named: "chevron_left_icon", in: bundle, with: nil)
- Projects/Presentation/Sources/Home/View/HomeView.swift:147 — previousWeekButton.setImage(BitnagilIcon.chevronLeftIcon, for: .normal)
요청사항: 이 아이콘이 내비게이션 백 버튼 등 방향성 있는 UI에 쓰인다면 RTL 환경에서 적절히 반전(flipping) 처리했는지 확인하거나, 대안으로 SF Symbol(chevron.left) 사용을 검토해주세요.
Projects/Presentation/Resources/Images.xcassets/Home/checked_circle_small_icon.imageset/Contents.json (1)
19-22: Tint 적용 예정이면 Template 렌더링 설정 권장해당 아이콘을 UIImageView.tintColor로 색상을 바꿔 사용할 계획이라면, 에셋의 Render As를 Template로 지정하는 편이 안전합니다. JSON에 아래 속성을 추가하면 일관된 틴팅이 보장됩니다.
"images" : [ ... ], + "properties" : { + "template-rendering-intent" : "template" + }, "info" : { "author" : "xcode", "version" : 1 }Projects/Presentation/Sources/RecommendedRoutine/ViewModel/RecommendedRoutineViewModel.swift (2)
116-120: 불리언 전파 로직 단순화 제안nil 여부만 필요하므로 한 줄로 축약 가능합니다. 가독성과 유지보수성이 개선됩니다.
- if emotionEntity == nil { - emotionExistSubject.send(false) - } else { - emotionExistSubject.send(true) - } + let exists = emotionEntity != nil + emotionExistSubject.send(exists)
112-116: Presentation에서 Repository 직접 호출 대신 UseCase 사용 고려현재 ViewModel에서 날짜 문자열 포맷팅을 중복 수행하고 Repository를 직접 호출합니다. Domain의 EmotionUseCaseProtocol(loadEmotion(date: Date))을 의존하면 포맷팅 로직 중복을 제거하고 계층 분리를 더 명확히 할 수 있습니다.
예시 변경안(핵심 부분만):
// 프로퍼티 private let emotionUseCase: EmotionUseCaseProtocol // 초기화 init(recommendedRoutineUseCase: RecommendedRoutineUseCaseProtocol, emotionUseCase: EmotionUseCaseProtocol) { self.recommendedRoutineUseCase = recommendedRoutineUseCase self.emotionUseCase = emotionUseCase ... } // 사용처 let exists = (try await emotionUseCase.loadEmotion(date: Date())) != nil emotionExistSubject.send(exists)Projects/Presentation/Sources/Common/Component/FloatingButton.swift (1)
31-37: tintColor 적용 보장을 위한 템플릿 렌더링 권장UIImageView의 tintColor가 적용되려면 이미지가 템플릿 렌더링이어야 합니다. 에셋에서 Render As: Template로 설정했는지 확인하거나 코드에서 강제 설정을 권장합니다.
- plusIcon.image = BitnagilIcon.plusIcon + plusIcon.image = BitnagilIcon.plusIcon.withRenderingMode(.alwaysTemplate)에셋에서 처리한다면 Xcode Asset Catalog에서 해당 이미지의 Render As를 Template로 지정해도 됩니다.
Projects/Domain/Sources/Protocol/Repository/EmotionRepositoryProtocol.swift (2)
17-17: loadEmotion 네이밍 변경 타당. Optional 반환 의미를 주석에 명시 부탁이제 단일 조회가 v2로 전환되며
loadEmotion(date:) -> EmotionEntity?가 되었습니다. 반환값이 Optional인 만큼, "해당 날짜에 등록된 감정이 없으면 nil을 반환, 그 외 네트워크/디코딩 오류는 throw"와 같은 동작 기준을 주석에 분명히 남겨 주세요. 현재 DataSource 구현(EmotionRepository.loadEmotion)은 response가 없을 때 throw하고 있어 Optional 반환 의미와 어긋날 소지가 있습니다(아래 DataSource 파일 코멘트 참조).
12-13: 메서드 네이밍 일관성: fetchEmotions vs loadEmotion복수 조회는
fetchEmotions, 단일 조회는loadEmotion로 혼재합니다. 의도된 구분이라면 OK지만, 통일(예:loadEmotions/loadEmotion혹은fetchEmotions/fetchEmotion)을 고려하면 API 탐색성이 좋아집니다.Also applies to: 17-17
Projects/Presentation/Sources/Common/View/TabBarView.swift (2)
20-21: 라이트 테마 강제 여부 확인 필요 (.white 사용)배경을
.systemBackground에서.white로 고정하면 다크 모드 지원이 깨질 수 있습니다. 이번 디자인 개편이 라이트 테마 고정이라면 OK, 아니라면 디자인 시스템의 동적 색상 또는 시스템 백그라운드를 고려해 주세요.
23-36: 색상 옵셔널 처리 일관성 및 폰트/아이콘 상태 적용 범위 확장 제안
- Line 28, 35의
iconColor는 UIColor?를 받으므로 현재도 동작하나, Line 24, 31처럼 안전한 기본값(예:.systemGray)을 coalesce하여 일관성 있게 처리하면 안정성이 높아집니다.- iPad 등에서 사용할 수 있는 inline/compactInline appearance도 동일 속성으로 맞춰두면 상태 일관성이 개선됩니다.
적용 예:
-appearance.stackedLayoutAppearance.normal.iconColor = BitnagilColor.gray90 +appearance.stackedLayoutAppearance.normal.iconColor = BitnagilColor.gray90 ?? .systemGray -appearance.stackedLayoutAppearance.selected.iconColor = BitnagilColor.gray10 +appearance.stackedLayoutAppearance.selected.iconColor = BitnagilColor.gray10 ?? .label + +appearance.inlineLayoutAppearance = appearance.stackedLayoutAppearance +appearance.compactInlineLayoutAppearance = appearance.stackedLayoutAppearanceProjects/DataSource/Sources/Repository/EmotionRepository.swift (1)
10-12: 테스트 용이성: NetworkService 의존성 주입 고려
NetworkService.shared싱글턴 직접 의존은 테스트 대역 주입이 어렵습니다. 생성자 주입으로 바꾸면 단위 테스트가 쉬워집니다. 범위가 큰 변경이라면 추후 작업으로도 충분합니다.Projects/DataSource/Sources/Endpoint/EmotionEndpoint.swift (2)
24-33: path가 baseURL 전체를 반환하는 형태인지 확인 필요 (중복 결합 위험)현재
path가 케이스에 따라baseURL(절대 경로)을 다시 반환합니다. Endpoint 빌더가baseURL + path로 결합하는 구조라면 최종 URL이 이중으로 결합될 수 있습니다. 반대로path만 사용하는 구조라면 현 구현이 맞습니다.
- 확인 포인트:
- Network 레이어에서 최종 URL을 어떻게 조립하는지 (예:
URL(string: endpoint.baseURL + endpoint.path)형태인지,path단독 사용인지)만약 결합 방식이라면 아래처럼
path는 상대 경로만 반환하도록 조정하는 것을 권장합니다.다음 diff는 결합 방식일 때의 제안입니다:
- var path: String { - switch self { - case .fetchEmotions: - return baseURL - case .loadEmotion(let date): - return baseURL + "/\(date)" - case .registerEmotion: - return baseURL - } - } + var path: String { + switch self { + case .fetchEmotions, .registerEmotion: + return "" + case .loadEmotion(let date): + return "/\(date)" + } + }
28-31: 경로에 날짜 문자열 포함 시 퍼센트 인코딩 권장
date값은 현재 형식상 안전해 보이나(yyyy-MM-dd), 일반화 관점에서 URL path에 넣을 값은urlPathAllowed로 인코딩하는 편이 안전합니다.- case .loadEmotion(let date): - return baseURL + "/\(date)" + case .loadEmotion(let date): + let encoded = date.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? date + return baseURL + "/\(encoded)"Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift (1)
139-145: 변수명(weekStartDate) 오해 소지 — 실제론 “일주일 이동 후 날짜”입니다현재 구현은 선택일자에 대해
.weekOfYear만큼 더한 “새 날짜”를 계산합니다. 변수명이weekStartDate라 월요일(주 시작일)로 착각할 수 있습니다. 의도에 맞게 변수명을 정정하거나, 정말 월요일 정렬이 의도라면 주 시작일로 스냅하는 로직을 추가하는 것을 권장합니다.다음은 단순 변수명 정정 예시입니다:
- private func moveWeek(by week: Int) { - let currentDate = selectedDateSubject.value - guard let weekStartDate = calendar.date(byAdding: .weekOfYear, value: week, to: currentDate) - else { return } - selectedDateSubject.send(weekStartDate) - } + private func moveWeek(by week: Int) { + let currentDate = selectedDateSubject.value + guard let newDate = calendar.date(byAdding: .weekOfYear, value: week, to: currentDate) + else { return } + selectedDateSubject.send(newDate) + }주 시작일(월요일) 기준으로 맞추고 싶다면, WeekView의 주 계산 규칙과 동일하게
calculateWeekStartDate와 동일한 로직(또는 유틸)을 적용해 동일한 기준을 공유하는 것도 고려 부탁드립니다.Projects/DataSource/Sources/DTO/EmotionResponseDTO.swift (1)
33-38: imageUrl → URL 변환 시 유효성 보장 여부 확인
URL(string:)결과가 nil일 수 있습니다. 서버에서 절대경로를 항상 보장한다면 OK지만, 상대경로 또는 빈 문자열 가능성이 있다면 API 스펙 확인이 필요합니다. 필요 시 베이스 URL과 조합하거나, 실패 시 로깅을 남기는 것도 고려해 주세요.Projects/Presentation/Sources/Home/View/Component/WeekView.swift (1)
74-79: 주차 전환 시 선택 날짜가 월요일로 강제 초기화될 수 있습니다
updateWeekDateViews(date:)에서 전달된date와 현재selectedDate가 같은 “날짜”가 아니면selectedDate를 주 시작일(월요일)로 바꿉니다. 이로 인해:
- 외부에서 “같은 요일로 일주일 이동”을 의도해 넘겨도, WeekView는 월요일을 선택해버릴 수 있습니다.
- UX적으로 “이전 선택 요일 유지”를 기대한다면 동작이 어긋납니다.
같은 “주”에 속하면 기존 선택을 유지하고, 다른 주로 넘어갈 때만 선택일을 갱신하는 식으로 완화하는 것을 권장합니다.
- let weekStartDate = calculateWeekStartDate(for: date) - let isSelectedDay = calendar.isDate(selectedDate, equalTo: date, toGranularity: .day) - if !isSelectedDay { - selectedDate = weekStartDate - } + let weekStartDate = calculateWeekStartDate(for: date) + // 같은 주면 기존 선택 유지, 다른 주로 바뀔 때만 선택일 갱신 + let isSameWeek = calendar.isDate(selectedDate, equalTo: date, toGranularity: .weekOfYear) + if !isSameWeek { + selectedDate = date + }필요 시 “항상 주 시작일 선택”이 의도라면, ViewModel의
moveWeek(by:)도 동일 규칙으로 정렬하도록 맞춰주는 것을 추천드립니다.Projects/Presentation/Sources/Home/View/Component/RoutineView.swift (4)
124-129: 메인 루틴 라벨 고정 폭(211) — 다양한 화면/로캘에서 오버플로우 가능성고정 폭 대신 체크 버튼과의 관계로 우측 제약을 느슨하게 두는 쪽이 유연합니다. 텍스트 길이(로캘/다이내믹 타입)에 따른 잘림을 방지할 수 있습니다.
- mainRoutineLabel.snp.makeConstraints { make in - make.centerY.equalToSuperview() - make.leading.equalToSuperview() - make.width.equalTo(211) - } + mainRoutineLabel.snp.makeConstraints { make in + make.centerY.equalToSuperview() + make.leading.equalToSuperview() + make.trailing.lessThanOrEqualTo(mainRoutineCheckButton.snp.leading).offset(-10) + }추가로, 텍스트 우선 배치를 원하시면 다음 우선순위 조정도 고려해 주세요:
mainRoutineLabel.setContentCompressionResistancePriority(.defaultLow, for: .horizontal) mainRoutineLabel.setContentHuggingPriority(.defaultHigh, for: .horizontal)
107-110: 매직 넘버 대신 선언된 상수 사용 권장
Layout.timeLabelHeight를 이미 선언해 두셨으니 동일하게 사용하면 가독성과 변경 용이성이 좋아집니다.- timeLabel.snp.makeConstraints { make in - make.top.leading.equalToSuperview() - make.height.equalTo(20) - } + timeLabel.snp.makeConstraints { make in + make.top.leading.equalToSuperview() + make.height.equalTo(Layout.timeLabelHeight) + }
56-63: intrinsicContentSize 계산식, 피그마와의 정합성 확인 필요동적 높이 산식이
base 56, 서브 루틴이 있으면100 + 34*(n-1)인데, 디자인 요구사항(마진/라인/스택 간격 포함치)과 정확히 일치하는지 확인을 권장합니다. 현재 레이아웃 제약에서는 컨테이너 하단 앵커가 내용물에 물려있지 않아, 상위 레이아웃에서intrinsicContentSize의존도가 높습니다.필요하시면 실측 기반으로 보정치 산출을 도와드릴 수 있습니다.
49-55: isLayoutConfigured 플래그는 현재 무의미
layoutSubviews에서 한 번만 true로 바꾸지만, 실제 레이아웃 구성은init에서 이미 끝납니다. 추후 확장 의도가 없다면 제거해도 됩니다.Projects/Presentation/Sources/Home/View/HomeView.swift (3)
59-59: 빈 문자열로 초기화된 nickname 프로퍼티
nickname이 빈 문자열로 초기화되어 있어, 실제 닉네임을 받기 전에 UI에 빈 값이 표시될 수 있습니다.- private var nickname: String = "" + private var nickname: String = "사용자"
121-126: 불필요한 줄바꿈 문자홈 라벨 텍스트에 불필요한 줄바꿈 문자(
\n)가 포함되어 있습니다. 이는 UI 레이아웃에 의도하지 않은 공백을 만들 수 있습니다.- let homeLabelText = "\(nickname)\n" + let homeLabelText = nickname
554-556: 중복된 의존성 해결 코드
emotionRegisterViewModel과routineCreationViewModel의존성 해결 로직이 여러 곳에서 중복되어 있습니다.의존성 해결 로직을 별도의 헬퍼 메서드로 추출하는 것을 고려해보세요:
private func resolveViewModel<T>(type: T.Type) -> T? { guard let viewModel = DIContainer.shared.resolve(type: type) else { BitnagilLogger.log(logType: .error, message: "\(type) 의존성이 등록되지 않았습니다.") return nil } return viewModel }Also applies to: 597-599
| /// - Parameter date: 조회하고 싶은 날짜 | ||
| /// - Returns: 감정 구슬 | ||
| func fetchEmotion(date: Date) async throws -> EmotionEntity? | ||
| func loadEmotion(date: Date) async throws -> EmotionEntity? |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
레거시 참조 잔존 여부 점검 제안
프로토콜/구현/호출부 전반에서 fetchEmotion 호출이 남아있지 않은지 확인 부탁드립니다. 아래 스크립트는 잔존 호출을 탐지합니다(결과가 없으면 정상).
🏁 Script executed:
#!/bin/bash
# 레거시 메서드 호출 탐지
rg -nP --type=swift -C2 '\bfetchEmotion\s*\('
# 레거시 엔드포인트/레포지토리 케이스 명도 함께 점검
rg -nP --type=swift -C2 'EmotionEndpoint\.\s*fetchEmotion\b|emotionRepository\.fetchEmotion\b'Length of output: 1010
레거시 fetchEmotion 호출이 남아있음 — 수정 필요
rg 결과 ViewModel에 fetchEmotion 호출/정의가 남아있습니다. EmotionEndpoint/Repository 호출은 발견되지 않았습니다.
- Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift:82 — case .loadEmotion: 호출 fetchEmotion()
- Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift:127 — private func fetchEmotion() 정의
조치 제안:
- 호출부(fetchEmotion())를 제거하거나 새로운 API인 loadEmotion(date:)로 대체(적절한 Date 인자 전달).
- private func fetchEmotion() 구현을 삭제하거나 내부에서 useCase.loadEmotion(date:)를 호출하도록 업데이트.
- 수정 후 동일한 검색(rg 명령)으로 잔여 참조가 없는지 재확인하세요.
🤖 Prompt for AI Agents
In Projects/Domain/Sources/Protocol/UseCase/EmotionUseCaseProtocol.swift around
line 18 and related Presentation files (HomeViewModel.swift lines ~82 and ~127),
there are leftover calls to the legacy fetchEmotion API; replace or remove them:
update call sites that invoke fetchEmotion() (e.g., the .loadEmotion case) to
call the new useCase.loadEmotion(date:) with an appropriate Date argument (e.g.,
the target date or Date()), and either remove the private func fetchEmotion() or
modify its implementation to call useCase.loadEmotion(date:) and forward/handle
results/errors accordingly; after changes, run the same rg search to confirm no
remaining fetchEmotion references.
|
|
||
| public func fetchEmotion(date: Date) async throws -> EmotionEntity? { | ||
| public func loadEmotion(date: Date) async throws -> EmotionEntity? { | ||
| let dateString = date.convertToString(dateType: .yearMonthDate) |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
날짜 포맷의 타임존 민감도 점검 권장
Date().convertToString(.yearMonthDate)가 로케일은 지정하지만 타임존 지정은 보이지 않습니다. 자정 경계/타임존(특히 KST 기준) 이슈가 UX에 민감하다면 DateFormatter에 명시적 timeZone 설정 또는 서버 기준 타임존으로의 변환을 고려해 주세요.
서버가 어떤 타임존을 기준으로 날짜를 해석하는지 확인 부탁드립니다. 필요 시 다음 검색으로 repo 내 관련 사용처를 확인해 주세요:
🏁 Script executed:
#!/bin/bash
rg -n -C2 --type=swift 'convertToString\s*\(\s*dateType:\s*\.yearMonthDate'Length of output: 5098
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "Searching for convertToString definitions/usages..."
rg -n --type=swift "convertToString" -C3 || true
echo
echo "Searching for Date extension implementations that might define convertToString..."
rg -n --type=swift "extension\s+Date" -C1 || true
echo
echo "Searching for DateFormatter/timeZone usages in the repo to check for global settings..."
rg -n --type=swift "timeZone\s*=" -C2 || trueLength of output: 13683
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Projects/Shared/Sources/Extension/Date+.swift ==="
sed -n '1,240p' Projects/Shared/Sources/Extension/Date+.swift || true
echo
echo "=== Searching for DateType definition ==="
rg -n --type=swift 'enum\s+DateType\b' -C3 || true
rg -n --type=swift 'struct\s+DateType\b' -C3 || true
echo
echo "=== Files mentioning formatString ==="
rg -n --type=swift 'formatString' -C3 || true
echo
echo "=== Searching for EmotionRepository protocol/implementations ==="
rg -n --type=swift 'EmotionRepository' -C3 || true
echo
echo "=== Searching for loadEmotion usages/definitions ==="
rg -n --type=swift "loadEmotion\s*\(" -C3 || trueLength of output: 20186
타임존 미지정 — 서버 기준 타임존 확인 후 Date 포맷에 timeZone을 명시해 주세요
간단 요약: 현재 convertToString/convertToDate는 locale(ko_KR)만 설정하고 timeZone을 지정하지 않습니다(기본 TimeZone.current 사용). yyyy‑MM‑dd 같이 날짜만 사용하는 포맷은 자정 경계에서 서버 해석 기준과 달라져 하루 차이가 발생할 수 있습니다. 서버가 어느 타임존을 기준으로 날짜를 해석하는지 확인해 주세요.
주의할 위치
- 수정 대상(우선): Projects/Shared/Sources/Extension/Date+.swift — convertToString / convertToDate (formatter.timeZone 추가)
- 주요 사용처(확인 필요):
- Projects/Domain/Sources/UseCase/Emotion/EmotionUseCase.swift (loadEmotion → API 날짜 파라미터)
- Projects/Domain/Sources/UseCase/Routine/RoutineUseCase.swift (fetchRoutines start/end)
- Projects/Presentation/Sources/RecommendedRoutine/ViewModel/RecommendedRoutineViewModel.swift
- Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift (여러 곳에서 .yearMonthDate 사용)
권장 변경 예시 (서버가 KST를 기준으로 할 경우)
- Date+.swift 내부 포맷터에 timeZone 명시 추가:
formatter.locale = Locale(identifier: "ko_KR")
formatter.timeZone = TimeZone(identifier: "Asia/Seoul") // 또는 서버 기준 TZ(예: "UTC")
formatter.dateFormat = dateType.formatString
조치 요청
- 백엔드가 yyyy‑MM‑dd를 어떤 타임존으로 해석하는지 확인해 주세요.
- 확인 결과에 따라 Projects/Shared/Sources/Extension/Date+.swift에서 formatter.timeZone을 서버 기준으로 명시하거나, 필요하면 convertToString에 timeZone 파라미터(기본값: 서버 TZ)를 추가하는 방식으로 수정해 주세요.
🤖 Prompt for AI Agents
In Projects/Domain/Sources/UseCase/Emotion/EmotionUseCase.swift around line 24,
the call date.convertToString(dateType: .yearMonthDate) relies on a Date
formatter that currently doesn't set a timeZone, which can cause off‑by‑one-day
errors at midnight relative to the server; confirm with backend which timezone
yyyy‑MM‑dd is interpreted in (e.g., "Asia/Seoul" or "UTC") and then update
Projects/Shared/Sources/Extension/Date+.swift: add formatter.timeZone =
TimeZone(identifier: "<SERVER_TZ>") inside convertToString and convertToDate (or
add a timeZone parameter with default set to the server TZ) so all callers
(including EmotionUseCase, RoutineUseCase, RecommendedRoutineViewModel,
HomeViewModel) use the server's timezone for date-only formats.
| static let homeIcon = UIImage(named: "home_fill_icon", in: bundle, with: nil) | ||
| static let recommendIcon = UIImage(named: "recommend_fill_icon",in: bundle, with: nil) |
There was a problem hiding this comment.
잘못된 이미지 파일 참조
homeIcon과 recommendIcon이 여전히 이전 fill 아이콘 이미지 파일을 참조하고 있습니다. 통합된 아이콘 구조로 변경했다면, 새로운 이미지 파일명을 사용해야 합니다.
- static let homeIcon = UIImage(named: "home_fill_icon", in: bundle, with: nil)
- static let recommendIcon = UIImage(named: "recommend_fill_icon",in: bundle, with: nil)
+ static let homeIcon = UIImage(named: "home_icon", in: bundle, with: nil)
+ static let recommendIcon = UIImage(named: "recommend_icon", in: bundle, with: nil)📝 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.
| static let homeIcon = UIImage(named: "home_fill_icon", in: bundle, with: nil) | |
| static let recommendIcon = UIImage(named: "recommend_fill_icon",in: bundle, with: nil) | |
| static let homeIcon = UIImage(named: "home_icon", in: bundle, with: nil) | |
| static let recommendIcon = UIImage(named: "recommend_icon", in: bundle, with: nil) |
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift around
lines 53–54, the UIImage initializers still reference the old "*_fill_icon"
asset names; update those two lines to use the new unified asset names from the
asset catalog (replace "home_fill_icon" and "recommend_fill_icon" with the
current unified filenames for home and recommend), keep the same bundle and
scale parameters, and verify the new asset names exist in the bundle so the
UIImage(named:in:with:) calls return non-nil images.
| static let homeIcon = UIImage(named: "home_fill_icon", in: bundle, with: nil) | ||
| static let recommendIcon = UIImage(named: "recommend_fill_icon",in: bundle, with: nil) | ||
| static let reportFillIcon = UIImage(named: "report_fill_icon", in: bundle, with: nil)?.withRenderingMode(.alwaysOriginal) | ||
| static let reportEmptyIcon = UIImage(named: "report_empty_icon", in: bundle, with: nil)?.withRenderingMode(.alwaysOriginal) | ||
|
|
||
| static let mypageFillIcon = UIImage(named: "mypage_fill_icon", in: bundle, with: nil) | ||
| static let mypageEmptyIcon = UIImage(named: "mypage_empty_icon", in: bundle, with: nil) | ||
| static let mypageIcon = UIImage(named: "mypage_fill_icon", in: bundle, with: nil) |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
탭바 아이콘 통합 변경 확인 필요
탭바 아이콘이 fill/empty 변형에서 단일 아이콘으로 통합되었습니다. 이 변경사항이 탭바의 선택/미선택 상태 표시에 영향을 줄 수 있습니다.
🏁 Script executed:
#!/bin/bash
# TabBarView에서 새로운 단일 아이콘 사용 방식 확인
rg -n -C3 'homeIcon|recommendIcon|mypageIcon' --type swiftLength of output: 3322
탭바 아이콘: 선택/비선택 아이콘이 동일하게 설정되어 있어 시각적 구분이 되지 않습니다 — 수정 필요
BitnagilIcon에 fill 아이콘만 정의되어 있고 TabBarView에서 image와 selectedImage에 동일한 아이콘을 할당하고 있습니다(아래 위치 확인). 의도된 변경인지 확인하거나, UI 상태 구분을 위해 수정해 주세요.
문제 위치
- Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift (대략 53–57)
- static let homeIcon = UIImage(named: "home_fill_icon", in: bundle, with: nil)
- static let recommendIcon = UIImage(named: "recommend_fill_icon", in: bundle, with: nil)
- static let mypageIcon = UIImage(named: "mypage_fill_icon", in: bundle, with: nil)
- Projects/Presentation/Sources/Common/View/TabBarView.swift (대략 57–68)
- homeView.tabBarItem = UITabBarItem(title: "홈", image: BitnagilIcon.homeIcon, selectedImage: BitnagilIcon.homeIcon)
- recommendView/tabBarItem 및 mypageView/tabBarItem도 동일 사용
권장 조치 (중 하나 선택)
- 선택/비선택 각각의 asset(예: home_empty_icon, home_fill_icon)을 추가하여 image와 selectedImage에 분리 할당.
- 또는 단일 아이콘을 유지하려면 selectedImage를 비우고 템플릿 렌더링(.alwaysTemplate) + tabBar.tintColor / unselectedItemTintColor로 상태를 구분.
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift around
lines 53–57, the tab bar icons are all assigned the "fill" assets so image and
selectedImage are identical causing no visual state change; fix by either (A)
adding proper empty/default assets (e.g., home_empty_icon, recommend_empty_icon,
mypage_empty_icon) and set static lets for both filled and empty variants then
use image = emptyIcon and selectedImage = fillIcon in TabBarView, or (B) keep a
single asset but return it as a template (.withRenderingMode(.alwaysTemplate))
and remove selectedImage (or keep nil) and rely on tabBar.tintColor /
unselectedItemTintColor to show selection — update BitnagilIcon.swift to expose
the correct variants and update TabBarView.tabBarItem assignments accordingly.
|
|
||
| static let mypageFillIcon = UIImage(named: "mypage_fill_icon", in: bundle, with: nil) | ||
| static let mypageEmptyIcon = UIImage(named: "mypage_empty_icon", in: bundle, with: nil) | ||
| static let mypageIcon = UIImage(named: "mypage_fill_icon", in: bundle, with: nil) |
There was a problem hiding this comment.
일관성 없는 아이콘 명명
mypageIcon도 여전히 mypage_fill_icon을 참조하고 있어 다른 통합 아이콘들과 일관성이 없습니다.
- static let mypageIcon = UIImage(named: "mypage_fill_icon", in: bundle, with: nil)
+ static let mypageIcon = UIImage(named: "mypage_icon", in: bundle, with: nil)📝 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.
| static let mypageIcon = UIImage(named: "mypage_fill_icon", in: bundle, with: nil) | |
| static let mypageIcon = UIImage(named: "mypage_icon", in: bundle, with: nil) |
🤖 Prompt for AI Agents
Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift around line
57: the property mypageIcon is referencing the asset named "mypage_fill_icon"
which is inconsistent with the naming used for other unified icons; change the
reference so the property and asset names match the established convention
(either rename the asset to "mypage_icon" or update the property to
mypageFillIcon), and ensure the chosen asset name exists in the bundle and
update any usages to the new property name.
| homeView.tabBarItem = UITabBarItem( | ||
| title: "홈", | ||
| image: BitnagilIcon.homeEmptyIcon, | ||
| selectedImage: BitnagilIcon.homeFillIcon) | ||
| image: BitnagilIcon.homeIcon, | ||
| selectedImage: BitnagilIcon.homeIcon) | ||
|
|
||
| recommendView.tabBarItem = UITabBarItem( | ||
| title: "추천루틴", | ||
| image: BitnagilIcon.recommendEmptyIcon, | ||
| selectedImage: BitnagilIcon.recommendFillIcon) | ||
| image: BitnagilIcon.recommendIcon, | ||
| selectedImage: BitnagilIcon.recommendIcon) | ||
|
|
||
| mypageView.tabBarItem = UITabBarItem( | ||
| title: "마이페이지", | ||
| image: BitnagilIcon.mypageEmptyIcon, | ||
| selectedImage: BitnagilIcon.mypageFillIcon) | ||
| image: BitnagilIcon.mypageIcon, | ||
| selectedImage: BitnagilIcon.mypageIcon) | ||
|
|
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
선택/비선택에 동일 아이콘 사용 시, 렌더링 모드 템플릿 보장 필요
선택/비선택 모두 동일 이미지로 설정했기 때문에 색 변화는 tint에 전적으로 의존합니다. 에셋의 Render As가 Template가 아니거나 코드에서 .alwaysTemplate로 강제하지 않으면 색 변화가 적용되지 않을 수 있습니다. 아래처럼 템플릿 모드를 강제하는 것을 권장합니다.
- homeView.tabBarItem = UITabBarItem(
- title: "홈",
- image: BitnagilIcon.homeIcon,
- selectedImage: BitnagilIcon.homeIcon)
+ homeView.tabBarItem = UITabBarItem(
+ title: "홈",
+ image: BitnagilIcon.homeIcon?.withRenderingMode(.alwaysTemplate),
+ selectedImage: BitnagilIcon.homeIcon?.withRenderingMode(.alwaysTemplate))
- recommendView.tabBarItem = UITabBarItem(
- title: "추천루틴",
- image: BitnagilIcon.recommendIcon,
- selectedImage: BitnagilIcon.recommendIcon)
+ recommendView.tabBarItem = UITabBarItem(
+ title: "추천루틴",
+ image: BitnagilIcon.recommendIcon?.withRenderingMode(.alwaysTemplate),
+ selectedImage: BitnagilIcon.recommendIcon?.withRenderingMode(.alwaysTemplate))
- mypageView.tabBarItem = UITabBarItem(
- title: "마이페이지",
- image: BitnagilIcon.mypageIcon,
- selectedImage: BitnagilIcon.mypageIcon)
+ mypageView.tabBarItem = UITabBarItem(
+ title: "마이페이지",
+ image: BitnagilIcon.mypageIcon?.withRenderingMode(.alwaysTemplate),
+ selectedImage: BitnagilIcon.mypageIcon?.withRenderingMode(.alwaysTemplate))또는 에셋 Contents.json에 template-rendering-intent를 설정해도 됩니다(아래 에셋 파일 코멘트 참고).
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Searching for TabBarView.swift ==="
rg -n --hidden -S "TabBarView.swift" || true
echo
echo "=== Showing TabBarView.swift (if found) ==="
FILEPATH=$(rg -n --hidden -S --files | rg "TabBarView.swift" || true)
if [ -n "$FILEPATH" ]; then
echo "Found: $FILEPATH"
echo "----- START FILE -----"
sed -n '1,240p' "$FILEPATH" || true
echo "----- END FILE -----"
else
echo "TabBarView.swift not found by filename search. Searching by symbol 'TabBarView'..."
rg -n --hidden -S "class TabBarView|struct TabBarView" || true
fi
echo
echo "=== Searching for BitnagilIcon and icon names ==="
rg -n --hidden -S "BitnagilIcon" || true
rg -n --hidden -S "homeIcon" || true
rg -n --hidden -S "recommendIcon" || true
rg -n --hidden -S "mypageIcon" || true
echo
echo "=== Searching for withRenderingMode usage and template-rendering-intent in assets ==="
rg -n --hidden -S "withRenderingMode" || true
rg -n --hidden -S "template-rendering-intent" || true
rg -n --hidden -S "\"rendering-intent\"" || true
echo
echo "=== Searching for .xcassets and Contents.json references (paths) ==="
rg -n --hidden -S --glob '!**/Pods/**' --glob '!**/node_modules/**' "Contents.json" || true
rg -n --hidden -S --glob '!**/Pods/**' --glob '!**/node_modules/**' "imageset" || true
echo
echo "=== List nearby asset directories that may contain icons (xcassets) ==="
rg -n --hidden -S --files | rg "\.xcassets" || trueLength of output: 44414
🏁 Script executed:
#!/bin/bash
set -euo pipefail
echo "=== Show BitnagilIcon.swift ==="
sed -n '1,240p' Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift || true
echo
echo "=== Show home_fill_icon Contents.json ==="
sed -n '1,240p' Projects/Presentation/Resources/Images.xcassets/TabBarIcon/home_fill_icon.imageset/Contents.json || true
echo
echo "=== Show recommend_fill_icon Contents.json ==="
sed -n '1,240p' Projects/Presentation/Resources/Images.xcassets/TabBarIcon/recommend_fill_icon.imageset/Contents.json || true
echo
echo "=== Show mypage_fill_icon Contents.json ==="
sed -n '1,240p' Projects/Presentation/Resources/Images.xcassets/TabBarIcon/mypage_fill_icon.imageset/Contents.json || trueLength of output: 6704
선택/비선택 동일 아이콘 사용 시 템플릿 렌더링 강제 필요
검증 결과: TabBarView에서 사용하는 home/recommend/mypage 아이콘은 BitnagilIcon에서 UIImage(named:)로 반환되고 있고, 해당 에셋(Contents.json)에도 템플릿 렌더링 설정이 없습니다. 따라서 tint가 적용되지 않아 의도한 색 변화가 동작하지 않을 수 있습니다. 아래 중 하나로 수정하세요.
수정 대상(예):
- Projects/Presentation/Sources/Common/View/TabBarView.swift (tabBarItem 생성부)
- Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift (Tab Bar Icons: homeIcon, recommendIcon, mypageIcon)
- Projects/Presentation/Resources/Images.xcassets/TabBarIcon/{home_fill_icon, recommend_fill_icon, mypage_fill_icon}.imageset/Contents.json
권장 수정안(두 가지 중 택1):
- 빠른 수정 — TabBarView에서 렌더링 모드 강제
- homeView.tabBarItem = UITabBarItem(
- title: "홈",
- image: BitnagilIcon.homeIcon,
- selectedImage: BitnagilIcon.homeIcon)
+ homeView.tabBarItem = UITabBarItem(
+ title: "홈",
+ image: BitnagilIcon.homeIcon?.withRenderingMode(.alwaysTemplate),
+ selectedImage: BitnagilIcon.homeIcon?.withRenderingMode(.alwaysTemplate))
# (recommendView / mypageView도 동일 적용)- 권장(중앙관리) — BitnagilIcon에서 템플릿 모드로 반환
- static let homeIcon = UIImage(named: "home_fill_icon", in: bundle, with: nil)
- static let recommendIcon = UIImage(named: "recommend_fill_icon",in: bundle, with: nil)
- static let mypageIcon = UIImage(named: "mypage_fill_icon", in: bundle, with: nil)
+ static let homeIcon = UIImage(named: "home_fill_icon", in: bundle, with: nil)?.withRenderingMode(.alwaysTemplate)
+ static let recommendIcon = UIImage(named: "recommend_fill_icon", in: bundle, with: nil)?.withRenderingMode(.alwaysTemplate)
+ static let mypageIcon = UIImage(named: "mypage_fill_icon", in: bundle, with: nil)?.withRenderingMode(.alwaysTemplate)대체 옵션) 에셋에 템플릿 렌더링 인텐트 설정
- 각 imageset의 Contents.json에 properties.template-rendering-intent: "template" 추가(또는 Xcode에서 Image Set → Render As: Template 설정).
주의: reportFill/reportEmpty처럼 의도적으로 .alwaysOriginal을 사용한 아이콘은 변경하지 마세요.
📝 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.
| homeView.tabBarItem = UITabBarItem( | |
| title: "홈", | |
| image: BitnagilIcon.homeEmptyIcon, | |
| selectedImage: BitnagilIcon.homeFillIcon) | |
| image: BitnagilIcon.homeIcon, | |
| selectedImage: BitnagilIcon.homeIcon) | |
| recommendView.tabBarItem = UITabBarItem( | |
| title: "추천루틴", | |
| image: BitnagilIcon.recommendEmptyIcon, | |
| selectedImage: BitnagilIcon.recommendFillIcon) | |
| image: BitnagilIcon.recommendIcon, | |
| selectedImage: BitnagilIcon.recommendIcon) | |
| mypageView.tabBarItem = UITabBarItem( | |
| title: "마이페이지", | |
| image: BitnagilIcon.mypageEmptyIcon, | |
| selectedImage: BitnagilIcon.mypageFillIcon) | |
| image: BitnagilIcon.mypageIcon, | |
| selectedImage: BitnagilIcon.mypageIcon) | |
| homeView.tabBarItem = UITabBarItem( | |
| title: "홈", | |
| image: BitnagilIcon.homeIcon?.withRenderingMode(.alwaysTemplate), | |
| selectedImage: BitnagilIcon.homeIcon?.withRenderingMode(.alwaysTemplate)) | |
| recommendView.tabBarItem = UITabBarItem( | |
| title: "추천루틴", | |
| image: BitnagilIcon.recommendIcon?.withRenderingMode(.alwaysTemplate), | |
| selectedImage: BitnagilIcon.recommendIcon?.withRenderingMode(.alwaysTemplate)) | |
| mypageView.tabBarItem = UITabBarItem( | |
| title: "마이페이지", | |
| image: BitnagilIcon.mypageIcon?.withRenderingMode(.alwaysTemplate), | |
| selectedImage: BitnagilIcon.mypageIcon?.withRenderingMode(.alwaysTemplate)) |
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Common/View/TabBarView.swift around lines 55–69
the tab bar icons (home/recommend/mypage) are using UIImages that are not set to
template rendering so tint color may not apply; fix by either (quick) forcing
template mode when creating the tabBarItem images (call
.withRenderingMode(.alwaysTemplate) on the UIImages used for
image/selectedImage), (recommended) change the BitnagilIcon getters for these
tab icons to return images with .withRenderingMode(.alwaysTemplate) centrally,
or (alternative) set properties.template-rendering-intent: "template" in each
imageset Contents.json; do not change icons that intentionally use
.alwaysOriginal (e.g., reportFill/reportEmpty).
| mainRoutineCheckButton.snp.makeConstraints { make in | ||
| make.leading.equalTo(mainRoutineLabel.snp.trailing).offset(10) | ||
| mainRoutineHeightConstraint = make.height.equalTo(40).constraint | ||
| make.size.equalTo(40) | ||
| } | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
mainRoutineCheckButton 제약: 수직 위치 제약 누락 및 중복 높이 제약
- 현재 버튼은
leading/size/height만 있고centerY/top/bottom수직 제약이 없습니다. 오토레이아웃 경고 또는 의도치 않은 배치가 발생할 수 있습니다. height와size에 모두 40을 설정해 높이 제약이 중복됩니다. 또한mainRoutineHeightConstraint는 이후 사용되지 않습니다.
아래처럼 수직 정렬을 추가하고, 중복 제약 및 불필요한 프로퍼티를 제거하는 것을 권장합니다.
- mainRoutineCheckButton.snp.makeConstraints { make in
- make.leading.equalTo(mainRoutineLabel.snp.trailing).offset(10)
- mainRoutineHeightConstraint = make.height.equalTo(40).constraint
- make.size.equalTo(40)
- }
+ mainRoutineCheckButton.snp.makeConstraints { make in
+ make.leading.equalTo(mainRoutineLabel.snp.trailing).offset(10)
+ make.centerY.equalToSuperview()
+ make.size.equalTo(40)
+ }또한, 사용되지 않는 프로퍼티는 제거해 주세요.
- private var mainRoutineHeightConstraint: Constraint?📝 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.
| mainRoutineCheckButton.snp.makeConstraints { make in | |
| make.leading.equalTo(mainRoutineLabel.snp.trailing).offset(10) | |
| mainRoutineHeightConstraint = make.height.equalTo(40).constraint | |
| make.size.equalTo(40) | |
| } | |
| mainRoutineCheckButton.snp.makeConstraints { make in | |
| make.leading.equalTo(mainRoutineLabel.snp.trailing).offset(10) | |
| make.centerY.equalToSuperview() | |
| make.size.equalTo(40) | |
| } |
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Home/View/Component/RoutineView.swift around
lines 130–135, the mainRoutineCheckButton constraints lack any vertical
positioning (no centerY/top/bottom) and redundantly set both height and size
while mainRoutineHeightConstraint is unused; fix by replacing the duplicate
height constraint with a single vertical alignment (e.g.,
make.centerY.equalTo(mainRoutineLabel) or constrain top/bottom), set either size
or explicit width/height (not both) so height isn’t duplicated, remove the
mainRoutineHeightConstraint property if it’s not used elsewhere, and ensure only
the necessary constraints remain to avoid Auto Layout warnings.
| let checkedIcon = BitnagilIcon.checkedCircleSmallIcon | ||
| let uncheckedIcon = BitnagilIcon.uncheckedCircleSmallIcon | ||
| checkButton.setImage(subRoutine.isDone ? checkedIcon : uncheckedIcon, for: .normal) | ||
|
|
||
| checkButton.snp.makeConstraints { make in | ||
| make.top.leading.equalToSuperview() | ||
| make.size.equalTo(24) | ||
| } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
서브 루틴 체크 버튼에 액션 미연결 — 기능 회귀
RoutineViewDelegate에는 didTapSubRoutineCheckButton이 정의되어 있지만, 실제 checkButton에는 액션이 연결되어 있지 않습니다. 현 상태에선 서브 루틴 체크 토글이 불가능합니다(중대한 회귀).
버튼 탭 시 델리게이트 호출 및 낙관적 UI 갱신을 추가해 주세요.
let checkedIcon = BitnagilIcon.checkedCircleSmallIcon
let uncheckedIcon = BitnagilIcon.uncheckedCircleSmallIcon
checkButton.setImage(subRoutine.isDone ? checkedIcon : uncheckedIcon, for: .normal)
+ checkButton.addAction(
+ UIAction { [weak self] _ in
+ guard let self else { return }
+ var toggled = subRoutine
+ toggled.isDone.toggle()
+ // 낙관적 UI 업데이트
+ let nextImage = toggled.isDone ? checkedIcon : uncheckedIcon
+ checkButton.setImage(nextImage, for: .normal)
+ // 델리게이트 알림
+ self.delegate?.routineView(self, didTapSubRoutineCheckButton: toggled)
+ },
+ for: .touchUpInside
+ )| guard let routineCreationViewModel = DIContainer.shared.resolve(type: RoutineCreationViewModel.self) | ||
| else { fatalError("routineCreationViewModel 의존성이 등록되지 않았습니다.") } |
There was a problem hiding this comment.
🛠️ Refactor suggestion
의존성 해결 실패 시 크래시 발생
DIContainer에서 의존성 해결 실패 시 fatalError로 앱이 크래시됩니다. 사용자 경험을 위해 더 우아한 에러 처리가 필요합니다.
- 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
+ }Committable suggestion skipped: line range outside the PR's diff.
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Home/View/HomeView.swift around lines 166–167,
remove the fatalError crash-on-missing-dependency and instead handle a resolve
failure gracefully: attempt to resolve RoutineCreationViewModel, and if it
returns nil, log the error, provide a safe fallback (e.g., create a default or
mock RoutineCreationViewModel or set an optional state), and update the UI to
show an error state or alert to the user rather than crashing; ensure dependency
resolution remains optional and UI code branches on the nil case to avoid
force-unwrapping.
| self?.nickname = nickname | ||
| } | ||
| .store(in: &cancellables) |
There was a problem hiding this comment.
닉네임 업데이트 후 UI 갱신 누락
닉네임이 업데이트되어도 homeLabel의 텍스트가 자동으로 갱신되지 않습니다.
.sink { [weak self] nickname in
self?.nickname = nickname
+ let homeLabelText = "\(nickname)님, 오셨군요!\n오늘 기분은 어떤가요?"
+ self?.homeLabel.attributedText = BitnagilFont(
+ family: .cafe24Ssurround,
+ style: .cafe24Title1,
+ weight: .light).attributedString(text: homeLabelText)
}📝 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.
| self?.nickname = nickname | |
| } | |
| .store(in: &cancellables) | |
| .sink { [weak self] nickname in | |
| self?.nickname = nickname | |
| let homeLabelText = "\(nickname)님, 오셨군요!\n오늘 기분은 어떤가요?" | |
| self?.homeLabel.attributedText = BitnagilFont( | |
| family: .cafe24Ssurround, | |
| style: .cafe24Title1, | |
| weight: .light | |
| ).attributedString(text: homeLabelText) | |
| } | |
| .store(in: &cancellables) |
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Home/View/HomeView.swift around lines 387 to
389, the nickname property is updated but the UI label (homeLabel) is not
refreshed; after assigning self?.nickname = nickname, update the label on the
main thread (e.g., DispatchQueue.main.async) to set homeLabel.text to the new
nickname or wire the view to the published property so the label is
automatically bound and refreshed on the main queue.
taipaise
left a comment
There was a problem hiding this comment.
오늘도 고생하셨습니다. RoutineView를 구현하시면서 많이 고민하신 흔적이 보이는거 같습니다!! 어푸루브 드립니다~~
There was a problem hiding this comment.
리뷰 노트에서 언급해주신 것처럼 덕분에 재활용하기 훨씬 좋을 것 같습니다 bb
| var baseHeight: CGFloat = 56 | ||
| if !routine.subRoutines.isEmpty { | ||
| baseHeight = 100 | ||
| baseHeight += (34.0 * CGFloat(routine.subRoutines.count - 1)) |
There was a problem hiding this comment.
구현해준 방식으로 intrinsicContentSize를 구현하는 방법도 좋지만, subRoutineStackView를 사용해주시는 만큼 오토레이아웃으로 해결하는 방법도 고려해볼 수 있을 것 같습니다!
RoutineView 안에서 자체적으로 제약조건을 통해서 높이가 자동으로 결정되게하고, RoutineView를 사용하는 외부에서는 RoutineView의 layout을 잡을 때 height 조건을 설정하지 않는 방법입니다. 다만 이 방법을 사용하기 위해서는 RoutineView 내부의 subview들의 top, bottom, height 조건 설정에 주의해야합니다. 서브뷰들의 위 아래 제약조건이 활성화되어 있는 상태에서 높이가 정해진다면 자연스럽게 RoutineView의 높이가 결정될테니까요.
현재 코드에서는 subroutineStackView의 bottom이 superview인 containerView에 붙어있지 않은데, 이런 제약조건들을 추가하면 자연스럽게 직접 계산해주지 않아도 높이를 구할 수 있을 거에요.
다만 RoutineView에 항상 subroutine이 등록된 것은 아니기 때문에, 서브루틴이 있는지 없는지에 따라 분기처리가 들어가 코드가 많이 복잡해질 수 있을 것 같습니다.. 구분선이나 서브루틴 스택뷰를 숨겨야할 수도 있고, main routine의 bottom 제약 조건도 이에 따라 유동적으로 조절해야할 테니까요!!
오토레이아웃으로 해결하는 방법은, 정말 유동적으로 subView들의 높이 변화에 대응할 수 있다는 점에서 큰 장점이 있다 생각하지만 복잡성이 굉장히 올라간다 생각합니다. 그래서 레이아웃 디버깅이 힘들것 같다면, 지금처럼 직접 계산해주는 방법이 훨씬 직관적이고 좋은 방법일수도 있지 않을까? 하는 생각이 드네요. 아직 pr을 올리지는 않았지만, 루틴 등록/수정에서도 이렇게 동적으로 높이를 조절해야하는 부분이 있는데요(서브루틴을 등록하는 부분에서), 해당 코드를 보고 어떤 방법을 선택하는 것이 좋을지 이야기 나누어봐도 좋겠습니다!
There was a problem hiding this comment.
(고민중 ································)
같이 고민해주셔서 감사해요 ㅠㅠ !!!!!!!!! 추천 루틴 뷰 만들면서 딩의 코멘트를 반영해볼게유 !!!!!!! 그리고 다시 돌아와서 홈 UI를 완벽 수정하겠습니다
🥺👍🏻
🌁 Background
홈 화면 수정된 디자인을 반영했어요 ~
📱 Screenshot
1. 홈 화면
2. 홈 Empty 화면
👩💻 Contents
📝 Review Note
1. WeekView 수정
기존에는 WeekView에 1주일 날짜 + 2025년 08월 Label + 주를 옮기는 버튼들을 포함하고 있었고,
계산 로직이 좀 많이 포함되어 있었다고 생각합니다.
하지만 루틴 리스트 뷰가 새로 생기면서 WeekView는 1주일 날짜만 있는 것이 재사용 가능하다고 생각하여 분리를 했어요 !!!
아직 루틴 리스트에 대한 역할 분담을 하진 않았지만 .. 만약 루틴 리스트 뷰를 만들게 된다면 WeekView를 재사용 할 수 있을 것 같아요 ㅎㅎ (제발 ...)
2. RoutineView
서버 v2를 아직 다 적용하진 않았지만 ..
루틴 조회, 완료 시 MainRoutine - SubRoutine을 나누지 않는 것 같습니다. ㅜㅜ
그래서 기존 MainRoutineView, SubRoutineView, RoutineView로 나뉘어져 있다면
이번엔 그냥 RoutineView로만 만들었습니다.
다만 피그마에 있는 RoutineView의 요구사항은 다음과 같아요 ㅠㅠ

Layout을 잡기 위해 Height를 계산해야 하는데 .. 그럼 줄 수에 따른 Height를 어떻게 계산해야 할 지 모르겠어요 ㅠ.ㅠ ...
일단 실마리를 얻으면 서버 v2 작업과 함께 다시 수정해보겠습니다 ..
📣 Related Issue
Summary by CodeRabbit
신기능
UI/스타일
기타