[Feat-T3-159] 루틴 리스트 화면 구현 및 서버 v2 연동#54
Conversation
Walkthrough루틴 관련 데이터 모델과 API/프레젠테이션 흐름을 전면 개편했습니다. DTO와 Entity를 재구성하고 Repository/UseCase/Protocol 시그니처를 변경했으며, MainRoutine/SubRoutine과 상세 화면을 제거하고 Routine/RoutineList 흐름(VC/VM/삭제·편집 시트)을 새로 추가했습니다. 아이콘·애셋 및 온보딩 텍스트 키를 정리했습니다. Changes
Sequence Diagram(s)sequenceDiagram
autonumber
actor User
participant HomeView
participant HomeVM as HomeViewModel
participant DI as DIContainer
participant RLVC as RoutineListViewController
participant RLVM as RoutineListViewModel
participant Repo as RoutineRepositoryProtocol
participant API as DataSource(API)
User->>HomeView: 루틴 리스트 버튼 탭
HomeView->>HomeVM: action(.selectRoutineListDate)
HomeVM-->>HomeView: routineListDatePublisher(selectedDate)
HomeView->>DI: resolve RoutineListViewModel
HomeView->>RLVC: push(RoutineListViewController(viewModel, selectedDate))
RLVC->>RLVM: action(.fetchRoutineList)
RLVM->>Repo: fetchRoutines(startDate, endDate)
Repo->>API: GET /api/v2/routines?startDate&endDate
API-->>Repo: RoutineDictionaryDTO per date
Repo-->>RLVM: [date: (routines, allCompleted)]
RLVM-->>RLVC: fetchRoutinesResultPublisher(true)
RLVC->>RLVM: action(.fetchDailyRoutine)
RLVM-->>RLVC: routinesPublisher([Routine])
RLVC->>RLVC: render RoutineCardView list
sequenceDiagram
autonumber
actor User
participant RLVC as RoutineListViewController
participant RLVM as RoutineListViewModel
participant Repo as RoutineRepositoryProtocol
participant EP as RoutineEndpoint
User->>RLVC: 카드 편집/삭제 탭
RLVC->>RLVM: action(.selectRoutine(routine))
alt 편집
RLVC->>RLVC: 편집 시트 표시 -> 옵션 선택
RLVC->>RLVC: goToRoutineCreationView(...) 호출
else 삭제
RLVC->>RLVM: action(.deleteRoutine(isDeleteAllRoutines))
alt 일일 삭제
RLVM->>Repo: deleteDailyRoutine(routineId)
Repo->>EP: DELETE /api/v2/routines/day/{routineId}
else 전체 삭제
RLVM->>Repo: deleteAllRoutine(...)
Repo->>EP: DELETE /api/v1/routines...
end
RLVM-->>RLVC: deleteRoutineResultPublisher(true)
RLVC->>RLVM: action(.fetchRoutineList)
end
Estimated code review effort🎯 4 (Complex) | ⏱️ ~70 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
|
There was a problem hiding this comment.
Actionable comments posted: 8
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
Projects/Presentation/Sources/RoutineCreation/ViewModel/RoutineCreationViewModel.swift (2)
224-237: 기간 유효성 검사에 시작/종료 역전 케이스 추가 권장현재는 nil 여부만 체크합니다.
start <= end검증이 없으면 잘못된 값이 저장될 수 있습니다.적용 예:
private func updateIsRoutineValid() { guard let name = nameSubject.value, !name.isEmpty, executionTimeSubject.value.startAt != nil, - periodStartSubject.value != nil, - periodEndSubject.value != nil + let start = periodStartSubject.value, + let end = periodEndSubject.value, + start <= end else { checkRoutinePublisher.send(false) return } checkRoutinePublisher.send(true) }
263-273: 서브루틴 저장 시 공백/빈 항목 제거현재 빈 문자열(예: ["", "", ""])이 그대로 서버에 전달될 수 있습니다. 서버 400/유효성 실패 가능성이 높습니다.
아래처럼 트리밍 후 비어있는 항목을 필터링하세요.
let routine = RoutineCreationEntity( id: routineId, name: name, repeatDay: repeatDay, startDate: startDateString, endDate: endDateString, executionTime: executionTimeString, - subroutines: subRoutinesSubject.value, + subroutines: subRoutinesSubject.value + .map { $0.trimmingCharacters(in: .whitespacesAndNewlines) } + .filter { !$0.isEmpty }, recommendedRoutineType: routineType, applyDateType: updateType)Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift (1)
97-104: 컴파일 오류: computed property의 switch 문에서 return 누락현재 각 case에서 값을 반환하지 않아 컴파일 에러가 발생합니다. 아래처럼 return을 명시해 주세요.
- var rotation: Float { - switch self { - case .up: 90 - case .down: -90 - case .left: 0 - case .right: 180 - } - } + var rotation: Float { + switch self { + case .up: return 90 + case .down: return -90 + case .left: return 0 + case .right: return 180 + } + }
🧹 Nitpick comments (28)
Projects/DataSource/Sources/DTO/LoginResponseDTO.swift (2)
11-19: 서버 필드명(role) ↔ 속성명(userState) 불일치 — 혼동 방지 리네이밍 제안서버 payload 키가 "role"인데 DTO 속성명이 userState여서 코드를 처음 읽는 사람이 혼동할 수 있습니다. DTO는 서버 스키마를 최대한 1:1로 반영하는 편이 유지보수에 유리하므로, 속성명을 role로 맞추는 것을 권장합니다. 도메인 변환 시 UserState로의 매핑은 그대로 유지하면 됩니다.
아래처럼 수정하면 가독성과 일관성이 좋아집니다:
struct LoginResponseDTO: Decodable { - let accessToken: String - let refreshToken: String - let userState: String + let accessToken: String + let refreshToken: String + let role: String enum CodingKeys: String, CodingKey { case accessToken case refreshToken - case userState = "role" + case role } } extension LoginResponseDTO { func toUserEntity() -> UserEntity { return UserEntity( accessToken: accessToken, refreshToken: refreshToken, - userState: UserState(rawValue: userState) ?? .guest) + userState: UserState(rawValue: role) ?? .guest) } }Also applies to: 22-29
22-29: 알 수 없는 UserState(rawValue) 폴백 시 관측 로직 추가 검토
Projects/Domain/Sources/Entity/Enum/UserState.swift
현재UserState에는.guest,.user,.onboarding만 정의되어 있어, 서버가 이 외의 문자열을 보낼 경우toUserEntity()에서 조용히.guest로 폴백됩니다.
• 의도된 정책인지 확인이 필요합니다.Projects/DataSource/Sources/DTO/LoginResponseDTO.swift
toUserEntity()확장에는 미인식 role을 감지·기록하는 로직이 없습니다. 디버그 애서션 또는 로깅/메트릭을 추가해 추후 이상 상태를 관측하도록 개선을 권장드립니다.민감정보 로깅 현황
스캔 결과,accessToken/refreshToken이 디버그 레벨로 여러 곳에서 로그되고 있습니다. 민감 정보 노출 리스크가 있으니 로그 레벨 상향 또는 마스킹을 검토해 주세요.
• Projects/DataSource/Sources/Repository/UserDataRepository.swift (36–37)
• Projects/DataSource/Sources/Repository/AuthRepository.swift (112–113)
• Projects/DataSource/Sources/NetworkService/Plugin/RefreshTokenPlugin.swift (42–43)예시 패치:
extension LoginResponseDTO { func toUserEntity() -> UserEntity { + #if DEBUG + if UserState(rawValue: userState) == nil { + assertionFailure("Unknown role received from API: \(userState)") + } + #endif return UserEntity( accessToken: accessToken, refreshToken: refreshToken, userState: UserState(rawValue: userState) ?? .guest) } }Projects/Presentation/Sources/Onboarding/Model/OnboardingChoiceProtocol.swift (1)
9-11: 네이밍 일관성: subTitle → subtitle 제안(선택)iOS 코드베이스에서 일반적으로 ‘subtitle’ 철자를 사용합니다. 변경 영향이 크지 않다면 통일을 고려해 보세요.
적용 예:
-protocol OnboardingChoiceProtocol { - var title: String { get } - var subTitle: String? { get } -} +protocol OnboardingChoiceProtocol { + var title: String { get } + var subtitle: String? { get } +}Projects/Presentation/Sources/RoutineCreation/ViewModel/RoutineCreationViewModel.swift (5)
134-136: executionTime 포맷 파싱 실패 대비(24h/AM·PM 동시 지원)서버 v2의 시간 문자열 포맷이
"HH:mm"인지"a h:mm"인지 명확치 않습니다. 현재는 AM·PM 포맷만 시도 후 실패 시 현재 시각으로 대체되어 오동작 가능성이 큽니다.아래처럼 24시간제 → AM·PM 순으로 순차 파싱하고, 모두 실패 시 nil 유지(또는 명시적 오류 처리)를 권장합니다.
- let time = Date.convertToDate(from: routine.executionTime, dateType: .amPmTimeShort) - executionType = .init(startAt: time ?? Date()) + // 서버 v2가 "HH:mm" 또는 "a h:mm" 중 무엇을 주더라도 유연 처리 + let time24 = Date.convertToDate(from: routine.executionTime, dateType: .time24hour) + let time12 = Date.convertToDate(from: routine.executionTime, dateType: .amPmTimeShort) + let time = time24 ?? time12 + executionType = .init(startAt: time)추가로, 파싱 실패 시 현재 시각으로 대체하기보다 사용자에게 재입력을 유도하는 쪽이 데이터 정합성에 유리합니다.
137-145: fetchRoutine 시 기간(start/end) 미설정 TODO 처리 필요현재 TODO 그대로라
updateIsRoutineValid()에서 항상 false가 되어 수정/저장 플로우가 막힐 수 있습니다.
- 서버 v2 엔티티에 시작/종료 일자가 있다면 파싱하여
periodStartSubject/periodEndSubject를 채워주세요.- 형식이
"yyyy-MM-dd"라면.yearMonthDate로 파싱하면 됩니다.예시(프로퍼티명은 실제 엔티티에 맞게 수정 필요):
if let s = Date.convertToDate(from: routine.startDate, dateType: .yearMonthDate) { periodStartSubject.send(s) } if let e = Date.convertToDate(from: routine.endDate, dateType: .yearMonthDate) { periodEndSubject.send(e) }필요하시면 해당 변경까지 포함한 패치를 제안드릴게요.
57-57: 사용되지 않는 프로퍼티 정리
deletedSubroutines가 선언만 되고 사용되지 않습니다. 혼동 방지를 위해 제거를 권장합니다.- private var deletedSubroutines = Set<SubRoutineSummaryEntity>()
146-149: 에러 처리 보강 권장빈
catch는 디버깅과 사용자 피드백에 불리합니다. 최소한 로깅 또는 에러 퍼블리싱이 필요합니다.예:
} catch { // Logger.error(error) 등 checkRoutinePublisher.send(false) }같은 패턴을 다른
catch에도 일관 적용해 주세요.
120-122: 반복 요일(rawValue) 매핑 검증 및 중앙화 필요서버 v2에서 내려오는
repeatDay값이Week.rawValue(“MONDAY” 등)와 불일치할 경우compactMap이 항목을 조용히 드롭합니다.점검 결과
• Week enum 정의 (Projects/Domain/Sources/Entity/Enum/Week.swift)
– rawValue: "MONDAY", "TUESDAY", "WEDNESDAY", ...
• 매핑 로직 중복
– Presentation: RoutineCreationViewModel.swift (compactMap { Week(rawValue: $0) })
– Domain: RoutineSummaryEntity.swift (compactMap(Week.init(rawValue:)))검증·리팩토링 제안
• 서버 v2repeatDay예시 값(MON, MONDAY, 월 등) 수집 및 비교
• 매칭 누락 시 로깅/모니터링 추가
• rawValue ↔ Week 매핑 테이블 도입 또는 매핑 로직을 한 곳으로 중앙화Projects/Presentation/Sources/Common/Protocol/RoutineProtocol.swift (1)
10-14: 접근 수준 검증 결과: internal 유지해도 무방RoutineProtocol은 Presentation 모듈 내부(Projects/Presentation)에서만 참조되고 있어
internal접근 수준을 그대로 유지해도 됩니다.네이밍 제안
- UI 표시용 의도가 분명하다면
RoutinePresentable또는RoutineDisplayable같은 의미적 이름 고려routineType이 충분히 설명적이라면 충돌이 없을 때type으로 간소화 가능Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift (2)
64-65: 탭바 아이콘 렌더링 모드 변경으로 색상 회귀 가능성 점검 필요기존
.withRenderingMode(.alwaysOriginal)제거로, TabBar 혹은 아이콘 사용처에서 시스템 tint 적용(템플릿)으로 동작할 수 있습니다. 의도적으로 템플릿화를 원한 것인지(탭바 tint로 색상 일괄 관리) 혹은 기존처럼 원본 색상 유지가 맞는지 확인 부탁드립니다. 에셋의 Render As 설정(Original vs Template)에 따라 결과가 달라집니다.원하는 동작에 따라 아래 중 하나를 적용할 수 있습니다.
- 선택 1) 템플릿 아이콘으로 tint 적용
- static let reportFillIcon = UIImage(named: "report_fill_icon", in: bundle, with: nil) - static let reportEmptyIcon = UIImage(named: "report_empty_icon", in: bundle, with: nil) + static let reportFillIcon = UIImage(named: "report_fill_icon", in: bundle, with: nil)?.withRenderingMode(.alwaysTemplate) + static let reportEmptyIcon = UIImage(named: "report_empty_icon", in: bundle, with: nil)?.withRenderingMode(.alwaysTemplate)
- 선택 2) 원본 색상 유지
- static let reportFillIcon = UIImage(named: "report_fill_icon", in: bundle, with: nil) - static let reportEmptyIcon = UIImage(named: "report_empty_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)
86-88: RoutineList 아이콘 에셋 확인 완료 · 네이밍/렌더링 모드 리팩토링 제안에셋이
Images.xcassets/RoutineList/edit_icon.imageset및…/trash_icon.imageset경로에 정상적으로 존재함을 확인했습니다. 유지보수성과 일관성을 위해 아래 리팩토링을 권장드립니다:
- BitnagilIcon.swift (86–88줄)
•editIcon,trashIcon→routineListEditIcon,routineListTrashIcon등 ‘RoutineList’ 맥락을 포함한 명명으로 변경- Tint 컬러 제어가 필요한 경우
•UIImage(named:in:with:)?.withRenderingMode(.alwaysTemplate)적용- 원본 컬러 유지가 목적이라면
• xcassets 내 해당 이미지셋의 Render As를 Original로 설정Projects/Domain/Sources/Entity/RoutineEntity.swift (2)
9-19: 도메인 모델의 타입/네이밍 개선 제안(String/Yn 제거, Swift 컨벤션 정렬)
repeatDay: [String]과recommendedRoutineType: String?은 문자열-기반 오류가 잦습니다. 도메인에서는enum WeekDay: String/enum RecommendedRoutineType: String등 강타입 사용을 권장합니다.routineCompleteYn,routineDeletedYn은 API 명명 그대로지만, 도메인에서는isCompleted,isDeleted처럼 Bool의 의도 드러나는 네이밍이 좋습니다.routineStartDate,routineEndDate가String이면 날짜 계산/정렬에서 비용이 큽니다. 도메인에서는Date로 보유하고, DTO 매핑 레이어에서만 문자열 변환을 권장합니다.해당 변경은 광범위합니다. 이후 뷰/유스케이스/매핑 레이어 영향도를 고려해 단계적 적용을 추천합니다. 필요 시 리팩터링 플랜/PR 분리도 도와드릴 수 있습니다.
Also applies to: 21-33, 36-44
33-45: 서브루틴 이름/완료여부 배열 길이 불일치 방지 가드 추가 제안
subRoutineNames와subRoutineCompleteYn길이가 불일치하면 UI/로직 불일치가 발생합니다. 디버그 안전망을 추가해주세요.public init( routineId: String, routineName: String, repeatDay: [String], executionTime: String, routineCompleteYn: Bool, subRoutineNames: [String], subRoutineCompleteYn: [Bool], recommendedRoutineType: String?, routineDeletedYn: Bool, routineStartDate: String, routineEndDate: String ) { + #if DEBUG + assert(subRoutineNames.count == subRoutineCompleteYn.count, + "subRoutineNames.count(\(subRoutineNames.count)) != subRoutineCompleteYn.count(\(subRoutineCompleteYn.count))") + #endif self.routineId = routineId self.routineName = routineName self.repeatDay = repeatDay self.executionTime = executionTime self.routineCompleteYn = routineCompleteYn self.subRoutineNames = subRoutineNames self.subRoutineCompleteYn = subRoutineCompleteYn self.recommendedRoutineType = recommendedRoutineType self.routineDeletedYn = routineDeletedYn self.routineStartDate = routineStartDate self.routineEndDate = routineEndDate }Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift (1)
102-107: PresentationDependencyAssembler.swift(102–107)에서 UseCase 주입으로 계층 경계 유지 권장UseCase는 이미 DI에 등록되어 있어 바로 주입 가능합니다:
- RoutineRepositoryProtocol 등록: Projects/DataSource/Sources/Common/DataSourceDependencyAssembler.swift:36
- RoutineUseCaseProtocol 등록: Projects/Domain/Sources/DomainDependencyAssembler.swift:63
RoutineListViewModel도 Repository 대신 UseCase를 받도록 DI 등록을 수정해주세요. (이니셜라이저 시그니처 변경 선행 필수)
- DIContainer.shared.register(type: RoutineListViewModel.self) { container in - guard let routineRepository = container.resolve(type: RoutineRepositoryProtocol.self) - else { fatalError("routineRepository 의존성이 등록되지 않았습니다.") } - - return RoutineListViewModel(routineRepository: routineRepository) - } + DIContainer.shared.register(type: RoutineListViewModel.self) { container in + guard let routineUseCase = container.resolve(type: RoutineUseCaseProtocol.self) + else { fatalError("routineUseCase 의존성이 등록되지 않았습니다.") } + + return RoutineListViewModel(routineUseCase: routineUseCase) + }Projects/DataSource/Sources/Endpoint/RoutineEndpoint.swift (1)
21-25: fetchRoutines v2 이관 OK — 다른 엔드포인트 버전 혼용 의도 확인 필요
createRoutine,updateRoutine,fetchRoutines만 v2로 이동하고, 완료/삭제 관련 엔드포인트는 v1에 남아 있습니다. 서버 정책에 따른 의도된 혼용인지 확인 부탁드립니다. 일관성 차원에서updateRoutineCompletion등도 v2로 이전해야 한다면 이번 PR에서 함께 이관하는 편이 안정적입니다.Projects/Presentation/Sources/Onboarding/Model/OnboardingChoiceType.swift (1)
12-33: switch expression 사용 시 Swift 최소 버전 확인 + 카피 띄어쓰기 보정 제안현재 구현은 switch expression 문법(각 case 뒤에 값만 표기)을 사용합니다. 이는 Swift 5.9+에서 지원됩니다. 팀의 최소 Swift/Xcode 버전이 이를 보장하지 않으면 컴파일 오류가 날 수 있으니 확인 바랍니다. 하위 호환이 필요하다면 아래처럼 return을 명시하는 형태로 바꾸는 것을 권장합니다.
Swift 5.9 미만에서도 동작하도록 하는 대안:
- var title: String { - switch self { - case .morningTime: "아침을 잘 시작하고 싶어요." - case .eveningTime: "저녁을 편안하게 마무리하고 싶어요." - case .allTime: "언제든 상관 없어요." - - case .stability: "안정감" - case .connection: "연결감" - case .growth: "성장감" - case .vitality: "생동감" - - case .never: "나가지 않고 집에서만 지냈어요." - case .rarely: "잠깐 외출했어요." - case .sometimes: "가끔 나가요." - case .often: "자주 외출해요." - - case .once: "시작이 더 중요해요." - case .twoToThree: "너무 무리하지 않아도 괜찮아요." - case .fourOrMore: "충분히 활력 있는 한 주가 될거에요." - case .notSure: "아직 잘 모르겠어요." - } - } + var title: String { + switch self { + case .morningTime: + return "아침을 잘 시작하고 싶어요." + case .eveningTime: + return "저녁을 편안하게 마무리하고 싶어요." + case .allTime: + return "언제든 상관 없어요." + + case .stability: + return "안정감" + case .connection: + return "연결감" + case .growth: + return "성장감" + case .vitality: + return "생동감" + + case .never: + return "나가지 않고 집에서만 지냈어요." + case .rarely: + return "잠깐 외출했어요." + case .sometimes: + return "가끔 나가요." + case .often: + return "자주 외출해요." + + case .once: + return "시작이 더 중요해요." + case .twoToThree: + return "너무 무리하지 않아도 괜찮아요." + case .fourOrMore: + return "충분히 활력 있는 한 주가 될 거예요." + case .notSure: + return "아직 잘 모르겠어요." + } + }추가로 사용자 노출 문구의 띄어쓰기/일관성 보정 제안입니다. 변경 라인 범위를 벗어나 별도 참고만 드립니다.
- Line 44: "무기력을 이겨내고 활력을 찾고싶어요." → "무기력을 이겨내고 활력을 찾고 싶어요."
- Line 81: "주 3회 외출" → "주 2
3회 외출" (subTitle의 "일주일에 23회"와 일관)Projects/Domain/Sources/Protocol/Repository/RoutineRepositoryProtocol.swift (1)
24-24: 반환 타입 변경 OK. allCompleted 의미/키 포맷을 문서로 명확화 필요사인 변경은 합리적입니다. 다만 호출부 혼선을 줄이기 위해 다음을 제안합니다:
- allCompleted의 의미(예: 해당 날짜의 모든 루틴 완료 여부)를 주석에 명시
- Dictionary key로 사용하는 날짜 문자열 포맷(예: "yyyy-MM-dd")을 명시
또한 가독성을 위해 튜플을 typealias로 감싸는 것도 고려해보세요.
추가 제안(예시):
public typealias DailyRoutines = (routines: [RoutineEntity], allCompleted: Bool) // ... func fetchRoutines(from startDate: String, to endDate: String) async throws -> [String: DailyRoutines]도메인 레이어 관점에선 String 대신 Date를 유지하고, DataSource에서 포맷팅하도록 역할을 분리하는 것도 장기적으로 안전합니다.
Projects/DataSource/Sources/Repository/RoutineRepository.swift (1)
37-49: 라벨드 튜플로 타입 명시 및 map 사용으로 의도 명확화가독성 개선 제안입니다.
- result의 튜플 타입을 라벨드 튜플로 선언하면 이후 사용처에서 의미가 더 분명해집니다.
- toRoutineEntity()가 non-optional이라면 compactMap 대신 map이 적절합니다.
아래 수정 제안:
- var result: [String: ([RoutineEntity], Bool)] = [:] + var result: [String: (routines: [RoutineEntity], allCompleted: Bool)] = [:] for (date, routineDTO) in response.routines { - let allCompleted = routineDTO.allCompleted - let routines = routineDTO.routineList.compactMap({ $0.toRoutineEntity() }) - result[date] = (routines, allCompleted) + let allCompleted = routineDTO.allCompleted + let routines = routineDTO.routineList.map { $0.toRoutineEntity() } + result[date] = (routines: routines, allCompleted: allCompleted) }Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineViewController.swift (1)
342-345: 빈 델리게이트 메서드: 최소한의 가드/로그로 오사용 감지 필요이 화면에서는 RecommendedRoutine 전용 카드만 사용되어 편집/삭제가 호출될 가능성은 낮지만, 프로토콜 시그니처 변경으로 메서드를 비워두면 예기치 않게 호출될 때 문제 원인 파악이 어려워집니다. 최소한 assertion/log를 추가해 두세요.
아래처럼 진입을 명시적으로 막아두는 것을 권장합니다.
- func routineCardView(_ sender: RoutineCardView, didTapEditButton routine: Routine) { } - func routineCardView(_ sender: RoutineCardView, didTapDeleteButton routine: Routine) { } + func routineCardView(_ sender: RoutineCardView, didTapEditButton routine: Routine) { + assertionFailure("RecommendedRoutineViewController에서는 편집 동작이 호출되지 않아야 합니다.") + } + func routineCardView(_ sender: RoutineCardView, didTapDeleteButton routine: Routine) { + assertionFailure("RecommendedRoutineViewController에서는 삭제 동작이 호출되지 않아야 합니다.") + }Projects/Presentation/Sources/Onboarding/Model/RecommendedRoutine.swift (1)
29-29: 파라미터 명(mainTitle)과 프로퍼티 명(title) 불일치Line 29에서
self.title = mainTitle로 매핑하는 것은 동작상 문제는 없지만, 네이밍이 혼재되어 가독성과 유지보수에 불리합니다. 이니셜라이저 파라미터명을title로 변경하는 것을 권장합니다.Projects/Presentation/Sources/RoutineList/View/RoutineListViewController.swift (2)
52-59: 클로저에서 self 강한 참조로 인한 잠재적 순환 참조
emptyView.didTapRegisterRoutineButton이self를 강하게 캡처합니다.emptyView가 VC에 의해 소유되어 있고, 해당 클로저가self를 소유하면 해제가 지연될 수 있습니다.[weak self]사용을 권장합니다.- emptyView.didTapRegisterRoutineButton = { + emptyView.didTapRegisterRoutineButton = { [weak self] in guard let routineCreationViewModel = DIContainer.shared.resolve(type: RoutineCreationViewModel.self) else { fatalError("routineCreationViewModel 의존성이 등록되지 않았습니다.") } let routineCreationView = RoutineCreationViewController(viewModel: routineCreationViewModel) routineCreationView.hidesBottomBarWhenPushed = true - self.navigationController?.pushViewController(routineCreationView, animated: true) + self?.navigationController?.pushViewController(routineCreationView, animated: true) }
138-141: RoutineCardView 델리게이트 미설정 — 편집/삭제 액션 무시 가능성
RoutineCardView에 편집/삭제 버튼이 노출되는 경우(메인 루틴 카드) 델리게이트가 설정되지 않으면 사용자 액션이 무시됩니다. 리스트 화면에서 편집/삭제를 의도하지 않는다면 명시적으로 숨기거나, 의도한다면 델리게이트를 설정하고 구현하세요.for routine in routines { let routineCardView = RoutineCardView(routine: routine) routineCardViews[routine.id] = routineCardView + routineCardView.delegate = self routineStackView.addArrangedSubview(routineCardView) }추가로, 파일 하단에 다음 구현을 추가하는 것을 제안합니다(필요 시 실제 플로우로 교체):
extension RoutineListViewController: RoutineCardViewDelegate { func routineCardView(_ sender: RoutineCardView, didTapEditButton routine: Routine) { // TODO: 편집 화면 이동 혹은 바텀시트 호출 } func routineCardView(_ sender: RoutineCardView, didTapDeleteButton routine: Routine) { // TODO: 삭제 확인 후 viewModel 액션 호출 } }Projects/DataSource/Sources/DTO/RoutineDTO.swift (1)
10-12: 서버 키 매핑 명시화 필요(CodingKeys 추가)현재 Decodable은 키 전략에 의존합니다. 서버 v2가 snake_case를 사용하거나 일부 키가 camelCase와 다를 경우 디코딩 실패가 발생합니다. 명시적 CodingKeys를 추가해 디코딩 안정성을 높이세요.
예시(서버가 snake_case를 반환한다는 가정):
struct RoutineDictionaryDTO: Decodable { let routines: [String: RoutineDateDTO] + private enum CodingKeys: String, CodingKey { + case routines + } } struct RoutineDateDTO: Decodable { let routineList: [RoutineDTO] let allCompleted: Bool + private enum CodingKeys: String, CodingKey { + case routineList = "routine_list" + case allCompleted = "all_completed" + } } struct RoutineDTO: Decodable { let routineId: String let routineName: String let repeatDay: [String] let executionTime: String let routineCompleteYn: Bool let subRoutineNames: [String] let subRoutineCompleteYn: [Bool] let recommendedRoutineType: String? let routineDeletedYn: Bool let routineStartDate: String let routineEndDate: String + private enum CodingKeys: String, CodingKey { + case routineId = "routine_id" + case routineName = "routine_name" + case repeatDay = "repeat_day" + case executionTime = "execution_time" + case routineCompleteYn = "routine_complete_yn" + case subRoutineNames = "sub_routine_names" + case subRoutineCompleteYn = "sub_routine_complete_yn" + case recommendedRoutineType = "recommended_routine_type" + case routineDeletedYn = "routine_deleted_yn" + case routineStartDate = "routine_start_date" + case routineEndDate = "routine_end_date" + } }만약 실제 응답이 camelCase라면, 위 CodingKeys는 제거하고 Repository 레벨에서
JSONDecoder.keyDecodingStrategy설정 여부를 확인해 주세요.Also applies to: 14-17, 19-46
Projects/Presentation/Sources/Home/View/Component/RoutineView.swift (2)
137-144: zip 사용으로 길이 불일치 안전 처리됨 — 주석 보완 제안
zip(routine.subRoutines, routine.subRoutineCompleted)은 길이 불일치 시 짧은 쪽에 맞춰 안전하게 처리됩니다. 데이터 신뢰성이 낮은 경우를 대비해, 서버/매핑 단계에서 길이 보정을 하거나 이 동작을 주석으로 명시해 두면 유지보수에 도움이 됩니다.
124-128: 제약 중복 설정(높이 + size) 및 미사용 프로퍼티
- Line 126:
height.equalTo(40)와 Line 128:size.equalTo(40)로 높이 제약이 중복 설정되어 있습니다.mainRoutineHeightConstraint프로퍼티가 이후 어디에서도 사용되지 않습니다.불필요한 제약/프로퍼티를 제거해 주세요.
- private var mainRoutineHeightConstraint: Constraint? ... - mainRoutineHeightConstraint = make.height.equalTo(40).constraint make.size.equalTo(40)Projects/Presentation/Sources/Home/Model/Routine.swift (2)
31-32: 날짜 변환 실패 시 기본값 처리
Date.convertToDate가 실패할 경우 현재 날짜(Date())를 기본값으로 사용하고 있습니다. 이는 데이터 무결성 문제를 숨길 수 있습니다.날짜 변환 실패 시 로깅을 추가하거나, 더 명확한 기본값을 사용하는 것을 고려해보세요:
- startTime: Date.convertToDate(from: executionTime, dateType: .time) ?? Date(), + startTime: Date.convertToDate(from: executionTime, dateType: .time) ?? { + BitnagilLogger.log(logType: .warning, message: "Failed to parse executionTime: \(executionTime)") + return Date() + }(),
32-32: Week enum 변환 실패 시 기본값 처리
Week(rawValue:)가 실패할 경우.monday를 기본값으로 사용하고 있습니다. 잘못된 데이터가 있을 경우 이를 감지하기 어려울 수 있습니다.잘못된 rawValue에 대한 로깅을 추가하는 것을 고려해보세요:
- repeatDay: repeatDay.map({ Week(rawValue: $0) ?? .monday }), + repeatDay: repeatDay.compactMap({ + guard let week = Week(rawValue: $0) else { + BitnagilLogger.log(logType: .warning, message: "Invalid week rawValue: \($0)") + return nil + } + return week + }),Projects/Presentation/Sources/RoutineList/ViewModel/RoutineListViewModel.swift (1)
87-90: 데이터가 없을 때 빈 배열 반환캐시된 데이터가 없을 경우 빈 배열을 반환하는 것은 적절합니다. 하지만 데이터를 먼저 가져오는 것을 고려해볼 수 있습니다.
사용자 경험 개선을 위해 데이터가 없을 경우 자동으로
fetchRoutines()를 호출하는 것을 고려해보세요:guard let dailyRoutines = routines[dateKey] else { + if routines.isEmpty { + fetchRoutines() + } routinesSubject.send([]) return }
📜 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 (24)
Projects/Presentation/Resources/Images.xcassets/RoutineList/edit_icon.imageset/edit_icon.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineList/edit_icon.imageset/edit_icon@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineList/edit_icon.imageset/edit_icon@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineList/trash_icon.imageset/trash_icon.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineList/trash_icon.imageset/trash_icon@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineList/trash_icon.imageset/trash_icon@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/edit_icon.imageset/edit_icon.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/edit_icon.imageset/edit_icon@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/edit_icon.imageset/edit_icon@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/ellipsis_icon.imageset/ellipsis_icon.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/ellipsis_icon.imageset/ellipsis_icon@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/ellipsis_icon.imageset/ellipsis_icon@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/repeat_icon.imageset/repeat_icon.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/repeat_icon.imageset/repeat_icon@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/repeat_icon.imageset/repeat_icon@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/routine_icon.imageset/routine_icon.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/routine_icon.imageset/routine_icon@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/routine_icon.imageset/routine_icon@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/subRoutine_icon.imageset/subRoutine_icon.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/subRoutine_icon.imageset/subRoutine_icon@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/subRoutine_icon.imageset/subRoutine_icon@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/trash_icon.imageset/trash_icon.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/trash_icon.imageset/trash_icon@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/trash_icon.imageset/trash_icon@3x.pngis excluded by!**/*.png
📒 Files selected for processing (34)
Projects/DataSource/Sources/DTO/LoginResponseDTO.swift(1 hunks)Projects/DataSource/Sources/DTO/RoutineDTO.swift(1 hunks)Projects/DataSource/Sources/DTO/RoutineResponseDTO.swift(0 hunks)Projects/DataSource/Sources/Endpoint/RoutineEndpoint.swift(1 hunks)Projects/DataSource/Sources/Repository/RoutineRepository.swift(1 hunks)Projects/Domain/Sources/Entity/RoutineEntity.swift(1 hunks)Projects/Domain/Sources/Protocol/Repository/RoutineRepositoryProtocol.swift(1 hunks)Projects/Domain/Sources/Protocol/UseCase/RoutineUseCaseProtocol.swift(1 hunks)Projects/Domain/Sources/UseCase/Routine/RoutineUseCase.swift(1 hunks)Projects/Presentation/Resources/Images.xcassets/RoutineList/Contents.json(1 hunks)Projects/Presentation/Resources/Images.xcassets/ellipsis_icon.imageset/Contents.json(0 hunks)Projects/Presentation/Resources/Images.xcassets/repeat_icon.imageset/Contents.json(0 hunks)Projects/Presentation/Resources/Images.xcassets/routine_icon.imageset/Contents.json(0 hunks)Projects/Presentation/Resources/Images.xcassets/subRoutine_icon.imageset/Contents.json(0 hunks)Projects/Presentation/Sources/Common/Component/RoutineCardView.swift(8 hunks)Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift(2 hunks)Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift(1 hunks)Projects/Presentation/Sources/Common/Protocol/RoutineProtocol.swift(1 hunks)Projects/Presentation/Sources/Home/Model/MainRoutine.swift(0 hunks)Projects/Presentation/Sources/Home/Model/Routine.swift(1 hunks)Projects/Presentation/Sources/Home/Model/SubRoutine.swift(0 hunks)Projects/Presentation/Sources/Home/View/Component/RoutineView.swift(3 hunks)Projects/Presentation/Sources/Home/View/HomeView.swift(2 hunks)Projects/Presentation/Sources/Home/View/RoutineDetailView.swift(0 hunks)Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift(8 hunks)Projects/Presentation/Sources/Onboarding/Model/OnboardingChoiceProtocol.swift(1 hunks)Projects/Presentation/Sources/Onboarding/Model/OnboardingChoiceType.swift(1 hunks)Projects/Presentation/Sources/Onboarding/Model/RecommendedRoutine.swift(2 hunks)Projects/Presentation/Sources/Onboarding/View/Component/OnboardingChoiceButton.swift(1 hunks)Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineViewController.swift(1 hunks)Projects/Presentation/Sources/RoutineCreation/ViewModel/RoutineCreationViewModel.swift(1 hunks)Projects/Presentation/Sources/RoutineList/View/RoutineListViewController.swift(1 hunks)Projects/Presentation/Sources/RoutineList/ViewModel/RoutineListViewModel.swift(1 hunks)Projects/Shared/Sources/Extension/Date+.swift(2 hunks)
💤 Files with no reviewable changes (8)
- Projects/Presentation/Resources/Images.xcassets/subRoutine_icon.imageset/Contents.json
- Projects/Presentation/Resources/Images.xcassets/routine_icon.imageset/Contents.json
- Projects/DataSource/Sources/DTO/RoutineResponseDTO.swift
- Projects/Presentation/Sources/Home/Model/SubRoutine.swift
- Projects/Presentation/Resources/Images.xcassets/repeat_icon.imageset/Contents.json
- Projects/Presentation/Resources/Images.xcassets/ellipsis_icon.imageset/Contents.json
- Projects/Presentation/Sources/Home/Model/MainRoutine.swift
- Projects/Presentation/Sources/Home/View/RoutineDetailView.swift
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-08-01T06:38:18.687Z
Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#31
File: Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift:97-103
Timestamp: 2025-08-01T06:38:18.687Z
Learning: choijungp의 HomeViewModel에서 fetchRoutines(for date: Date) 메서드는 기존 캐시 범위를 확장하는 것이 아니라, 요청된 날짜 주변의 새로운 데이터 청크를 가져오기 위해 oldestDate와 latestDate를 모두 업데이트한다. 과거 날짜 요청 시에는 해당 날짜의 1주 전~1일 전, 미래 날짜 요청 시에는 해당 날짜의 1일 후~1주 후 범위로 설정한다.
Applied to files:
Projects/Domain/Sources/Protocol/Repository/RoutineRepositoryProtocol.swiftProjects/Presentation/Sources/RoutineCreation/ViewModel/RoutineCreationViewModel.swiftProjects/Presentation/Sources/RoutineList/ViewModel/RoutineListViewModel.swiftProjects/Presentation/Sources/Home/View/HomeView.swift
📚 Learning: 2025-08-17T13:30:29.342Z
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.342Z
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.
Applied to files:
Projects/Presentation/Sources/Common/Component/RoutineCardView.swift
🧬 Code Graph Analysis (12)
Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift (1)
Projects/Shared/Sources/DIContainer/DIContainer.swift (2)
register(14-16)resolve(18-25)
Projects/DataSource/Sources/Endpoint/RoutineEndpoint.swift (2)
Projects/DataSource/Sources/Repository/RoutineRepository.swift (3)
createRoutine(13-28)updateRoutine(51-65)fetchRoutines(37-49)Projects/Domain/Sources/UseCase/Routine/RoutineUseCase.swift (3)
createRoutine(50-52)updateRoutine(54-56)fetchRoutines(23-29)
Projects/Domain/Sources/Protocol/Repository/RoutineRepositoryProtocol.swift (4)
Projects/Presentation/Sources/RoutineList/ViewModel/RoutineListViewModel.swift (1)
fetchRoutines(55-74)Projects/DataSource/Sources/Repository/RoutineRepository.swift (1)
fetchRoutines(37-49)Projects/Domain/Sources/UseCase/Routine/RoutineUseCase.swift (1)
fetchRoutines(23-29)Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift (2)
fetchRoutines(152-158)fetchRoutines(197-210)
Projects/Domain/Sources/Protocol/UseCase/RoutineUseCaseProtocol.swift (4)
Projects/Presentation/Sources/RoutineList/ViewModel/RoutineListViewModel.swift (1)
fetchRoutines(55-74)Projects/DataSource/Sources/Repository/RoutineRepository.swift (1)
fetchRoutines(37-49)Projects/Domain/Sources/UseCase/Routine/RoutineUseCase.swift (1)
fetchRoutines(23-29)Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift (2)
fetchRoutines(152-158)fetchRoutines(197-210)
Projects/Domain/Sources/UseCase/Routine/RoutineUseCase.swift (3)
Projects/Presentation/Sources/RoutineList/ViewModel/RoutineListViewModel.swift (1)
fetchRoutines(55-74)Projects/DataSource/Sources/Repository/RoutineRepository.swift (1)
fetchRoutines(37-49)Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift (2)
fetchRoutines(152-158)fetchRoutines(197-210)
Projects/Presentation/Sources/RoutineList/View/RoutineListViewController.swift (5)
Projects/Presentation/Sources/Home/View/HomeView.swift (3)
weekView(582-584)configureAttribute(116-211)bind(387-451)Projects/Presentation/Sources/RoutineList/ViewModel/RoutineListViewModel.swift (2)
action(41-53)fetchDailyRoutine(83-92)Projects/Presentation/Sources/Common/Component/RoutineCardView.swift (2)
configureAttribute(64-172)configureAttribute(258-263)Projects/Shared/Sources/DIContainer/DIContainer.swift (1)
resolve(18-25)Projects/Presentation/Sources/Home/View/Component/WeekView.swift (1)
updateWeekDateViews(68-101)
Projects/Presentation/Sources/Common/Component/RoutineCardView.swift (3)
Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineViewController.swift (3)
routineCardView(334-341)routineCardView(343-343)routineCardView(344-344)Projects/Presentation/Sources/Common/Extension/UIImage+.swift (1)
resizeAspectFit(20-35)Projects/Shared/Sources/Extension/Date+.swift (1)
convertToString(16-22)
Projects/Presentation/Sources/Home/Model/Routine.swift (1)
Projects/Shared/Sources/Extension/Date+.swift (1)
convertToDate(24-30)
Projects/DataSource/Sources/Repository/RoutineRepository.swift (5)
Projects/DataSource/Sources/NetworkService/NetworkService.swift (1)
request(25-48)Projects/DataSource/Sources/DTO/RoutineDTO.swift (1)
toRoutineEntity(32-45)Projects/Presentation/Sources/RoutineList/ViewModel/RoutineListViewModel.swift (1)
fetchRoutines(55-74)Projects/Domain/Sources/UseCase/Routine/RoutineUseCase.swift (1)
fetchRoutines(23-29)Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift (2)
fetchRoutines(152-158)fetchRoutines(197-210)
Projects/Presentation/Sources/RoutineList/ViewModel/RoutineListViewModel.swift (5)
Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift (4)
fetchDailyRoutine(167-194)selectDate(161-165)fetchRoutines(152-158)fetchRoutines(197-210)Projects/DataSource/Sources/Repository/RoutineRepository.swift (1)
fetchRoutines(37-49)Projects/Domain/Sources/UseCase/Routine/RoutineUseCase.swift (1)
fetchRoutines(23-29)Projects/Shared/Sources/Extension/Date+.swift (1)
convertToString(16-22)Projects/Presentation/Sources/Home/Model/Routine.swift (1)
toRoutine(26-39)
Projects/Presentation/Sources/Home/View/HomeView.swift (3)
Projects/Presentation/Sources/RoutineList/ViewModel/RoutineListViewModel.swift (1)
action(41-53)Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift (1)
action(78-114)Projects/Shared/Sources/DIContainer/DIContainer.swift (1)
resolve(18-25)
Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift (4)
Projects/Presentation/Sources/RoutineList/ViewModel/RoutineListViewModel.swift (1)
fetchRoutines(55-74)Projects/DataSource/Sources/Repository/RoutineRepository.swift (1)
fetchRoutines(37-49)Projects/Domain/Sources/UseCase/Routine/RoutineUseCase.swift (1)
fetchRoutines(23-29)Projects/Presentation/Sources/Home/Model/Routine.swift (1)
toRoutine(26-39)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (15)
Projects/DataSource/Sources/DTO/LoginResponseDTO.swift (1)
2-2: 헤더 파일명 주석 갱신 LGTM파일명 주석이 실제 파일명과 일치하도록 정리된 점 좋습니다.
Projects/Presentation/Resources/Images.xcassets/RoutineList/Contents.json (1)
1-6: 에셋 메타데이터 추가 적절Xcode 기본 메타 포맷으로 문제 없습니다. 실제 아이콘/이미지 추가는 별도 커밋에서 이어가면 될 듯합니다.
Projects/Shared/Sources/Extension/Date+.swift (1)
34-47:.yearMonthDateShort는 현재 파싱에 사용되지 않습니다.
Date.convertToDate(from:dateType:)호출 내에.yearMonthDateShort사용 사례 없음- RoutineCardView 등에서
convertToString(dateType: .yearMonthDateShort)로만 사용따라서 파싱 모호성 우려는 없으며, 별도 조치가 필요 없습니다.
(추후.yearMonthDateShort로 파싱해야 하는 경우엔formatter.twoDigitStartDate를 명시 설정하세요.)Projects/Presentation/Sources/Onboarding/View/Component/OnboardingChoiceButton.swift (1)
56-56: 프로퍼티 리네이밍 반영(LGTM)
mainTitle→title변경에 맞춘 바인딩이 적절합니다. 다른 사용처에서도 동일한 리네이밍이 누락되지 않았는지만 한 번 더 확인 부탁드립니다.Projects/DataSource/Sources/Endpoint/RoutineEndpoint.swift (1)
64-70: 쿼리 파라미터 키/형식 검증
startDate,endDate키 사용은 적절합니다. UseCase에서.yearMonthDate(예: yyyy-MM-dd) 포맷으로 변환하고 있으므로 서버 요구사항과 일치하는지만 한 번 더 확인해 주세요.서버/문서 기준 포맷 검증이 필요하면 일시적으로 로그를 추가하거나 통합 테스트로 응답 성공 여부를 확인해 주세요.
Projects/DataSource/Sources/Repository/RoutineRepository.swift (2)
32-32: RoutineDTO로 단건 조회 디코딩 변경: 적절합니다RoutineResponseDTO → RoutineDTO로 단순화하고 toRoutineEntity() 매핑으로 반환하는 흐름이 명확합니다. 네트워크 계층의 optional 응답에 대한 guard let 처리도 적절합니다.
37-49: RoutineCreationDTO의subRoutineName필드명 불일치로 인한 요청 실패 가능성 – 수정 필수RoutineCreationDTO는
let subRoutineName: [String]으로 정의되어 있어, JSON 인코딩 시
"subRoutineName"키가 전송됩니다. 그러나 서버 스펙(및 ResponseDTO)에서는"subRoutineNames"를 사용하므로 create/update API 호출이 실패할 수 있습니다.수정할 부분:
- Projects/DataSource/Sources/DTO/RoutineCreationDTO.swift
•let subRoutineName: [String]→let subRoutineNames: [String]
• (또는) CodingKeys를 추가해subRoutineName→"subRoutineNames"매핑- Projects/DataSource/Sources/Endpoint/RoutineEndpoint.swift
• createRoutine/updateRoutine 케이스의 body 파라미터가 변경된 DTO key와 일치하는지 확인- Projects/DataSource/Sources/Repository/RoutineRepository.swift
• DTO 초기화 시subRoutineName: routine.subroutines→subRoutineNames: routine.subroutines로 업데이트추가로, API 문서와 비교해 경로(path)·HTTP 메서드·JSON 필드명이 완전히 일치하는지 재검토 부탁드립니다.
⛔ Skipped due to learnings
Learnt from: choijungp PR: YAPP-Github/Bitnagil-iOS#31 File: Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift:97-103 Timestamp: 2025-08-01T06:38:18.687Z Learning: choijungp의 HomeViewModel에서 fetchRoutines(for date: Date) 메서드는 기존 캐시 범위를 확장하는 것이 아니라, 요청된 날짜 주변의 새로운 데이터 청크를 가져오기 위해 oldestDate와 latestDate를 모두 업데이트한다. 과거 날짜 요청 시에는 해당 날짜의 1주 전~1일 전, 미래 날짜 요청 시에는 해당 날짜의 1일 후~1주 후 범위로 설정한다.Projects/Domain/Sources/Protocol/UseCase/RoutineUseCaseProtocol.swift (1)
13-14: 반환 타입 변경 정상 반영UseCase에서 per-date allCompleted 플래그를 함께 반환하도록 확장된 것은 v2 응답 구조와 일치합니다. 관련 호출부(Home/RoutineList VM)가 value.routines/value.allCompleted로 접근하도록 이미 갱신된 점도 확인되었습니다.
Projects/Domain/Sources/UseCase/Routine/RoutineUseCase.swift (1)
23-29: 패스스루 구현 합리적Date → 문자열 포맷 변환 후 Repository 호출로 위임하는 단순 패스스루 구현은 도메인 경계를 유지하며 충분합니다. 추후 가독성을 위해 반환 타입을 별도 타입(alias/struct)으로 승격하는 것도 고려 여지가 있습니다.
Projects/Presentation/Sources/Home/View/HomeView.swift (2)
182-185: 루틴 리스트 날짜 선택 액션 추가 확인루틴 리스트 버튼에 대한 액션이 올바르게 추가되었습니다.
selectRoutineListDate입력을 통해 현재 선택된 날짜가 전달됩니다.
453-473: Routine 타입으로의 마이그레이션 완료
MainRoutine에서Routine타입으로의 마이그레이션이 성공적으로 완료되었습니다.updateRoutineView메서드가 새로운 타입을 올바르게 처리하고 있습니다.Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift (2)
92-94: 루틴 리스트 날짜 전달 로직 구현 확인
selectRoutineListDate케이스가 올바르게 추가되어 현재 선택된 날짜를routineListDateSubject로 전송합니다.
231-254: 주석 처리된 코드 블록
deleteDailyRoutine과updateRoutineCompletion메서드의 구현이 주석 처리되어 있습니다. PR 설명에 따르면 서브루틴 등록이 작동하지 않을 수 있다고 언급되어 있는데, 이와 관련이 있는 것으로 보입니다.이 기능들이 의도적으로 비활성화된 것인지, 아니면 마이그레이션 과정에서 임시로 주석 처리된 것인지 확인이 필요합니다. 만약 임시 조치라면 TODO 주석을 추가하여 추후 작업이 필요함을 명시하는 것이 좋겠습니다.
private func deleteDailyRoutine() { + // TODO: Routine 모델 마이그레이션 후 재구현 필요 /*Also applies to: 258-318
Projects/Presentation/Sources/RoutineList/ViewModel/RoutineListViewModel.swift (2)
55-74: 루틴 데이터 가져오기 로직 구현 확인주 단위로 루틴 데이터를 가져와서 캐싱하는 로직이 올바르게 구현되었습니다.
HomeViewModel과 유사한 패턴을 사용하고 있어 일관성이 있습니다.
77-81: 주의 시작일 계산 로직월요일을 주의 시작으로 계산하는 로직이 올바르게 구현되었습니다.
| subRoutineTitleLabel.textColor = BitnagilColor.gray40 | ||
| subRoutineTitleLabel.snp.makeConstraints { make in | ||
| make.height.equalTo(Layout.subLabelHeight) | ||
| } | ||
| subRoutineStackView.addArrangedSubview(subRoutineTitleLabel) |
There was a problem hiding this comment.
🛠️ Refactor suggestion
반복되는 height 제약 조건 설정 패턴
서브루틴 라벨들도 동일하게 제약 조건을 먼저 설정하고 있습니다.
- subRoutineTitleLabel.snp.makeConstraints { make in
- make.height.equalTo(Layout.subLabelHeight)
- }
subRoutineStackView.addArrangedSubview(subRoutineTitleLabel)
+ subRoutineTitleLabel.snp.makeConstraints { make in
+ make.height.equalTo(Layout.subLabelHeight)
+ }🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Common/Component/RoutineCardView.swift around
lines 115–119, you’re repeating the same height constraint setup for sub-routine
labels; extract the repeated pattern into a small private helper (or
UIView/UILabel extension) like applySubLabelHeight(_ label: UILabel) that calls
label.snp.makeConstraints { make in make.height.equalTo(Layout.subLabelHeight)
}, and call that helper for each sub-routine label (preferably after adding the
label to its stack view) to remove duplication and centralize the constraint
logic.
| private var routine: Routine { | ||
| didSet { | ||
| updateRoutineState() | ||
| } | ||
| } | ||
| weak var delegate: RoutineViewDelegate? | ||
| init(routine: MainRoutine) { | ||
|
|
||
| init(routine: Routine) { | ||
| self.routine = routine | ||
| super.init(frame: .zero) |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
루틴 완료 토글이 View 내부 상태만 변경 — 상위 계층에 변경 전파 필요
현재 메인 체크 버튼은 routine.isDone만 토글하고 끝납니다. ViewModel/UseCase로 완료 상태 업데이트(서버 반영 포함)가 전달되지 않으면 홈 화면의 완료 상태가 영속화되지 않습니다. 기존 델리게이트 제거에 따른 대체 경로가 없다면, 콜백/델리게이트로 상위에 이벤트를 전파하세요.
예시(콜백 방식):
// 추가
var didTapMainCheckButton: ((Routine) -> Void)?
// 기존 액션 내에서 마지막에 호출
self.routine = updatedRoutine
self.didTapMainCheckButton?(updatedRoutine)홈 화면에서 RoutineView를 구성할 때 didTapMainCheckButton에 ViewModel 액션을 바인딩하면 흐름이 복원됩니다.
RoutineView.swift의 상태 변경 전파 로직 유무 확인을 위해 코드를 검토합니다:
🏁 Script executed:
#!/bin/bash
# 1. 클래스 정의 위치 확인
rg -nP 'class\s+RoutineView' -C3 --type=swift
# 2. 전체 구현(최초 200줄) 출력
sed -n '1,200p' Projects/Presentation/Sources/Home/View/Component/RoutineView.swift
# 3. 콜백(closure) 프로퍼티 존재 여부 확인
rg -n 'didTapMainCheckButton' -C3 --type=swift
# 4. 델리게이트 패턴 사용 여부 확인
rg -n 'delegate' -C3 --type=swift
# 5. updateRoutineState 구현부 확인
rg -n 'func updateRoutineState' -C3 --type=swiftLength of output: 52614
RoutineView.swift: 루틴 완료 토글 이벤트 콜백으로 상위에 전달하세요
현재 mainRoutineCheckButton 액션에서 routine.isDone 토글 후 View 내부 상태만 갱신되므로, 변경된 상태가 ViewModel/UseCase로 전달되지 않습니다. 아래와 같이 콜백(closure) 프로퍼티를 추가하고, UIAction 내에서 호출해 상위 계층에서 처리하도록 구현해주세요.
- 파일: Projects/Presentation/Sources/Home/View/Component/RoutineView.swift
- 추가: 클로저 프로퍼티 선언
- 수정:
mainRoutineCheckButtonUIAction 클로저에 콜백 호출 삽입 - 사용처: Home 화면(예: HomeView 또는 해당 VC)에서
didTapMainCheckButton에 ViewModel 액션 바인딩
final class RoutineView: UIView {
+ /// 루틴 완료 버튼 탭 시 상위에 변경된 Routine 전달
+ var didTapMainCheckButton: ((Routine) -> Void)?
private var routine: Routine {
didSet {
updateRoutineState()
}
}
private func configureAttribute() {
// ...
mainRoutineCheckButton.addAction(
UIAction { [weak self] _ in
- guard let self else { return }
- var updatedRoutine = routine
- updatedRoutine.isDone.toggle()
- self.routine = updatedRoutine
+ guard let self else { return }
+ var updatedRoutine = routine
+ updatedRoutine.isDone.toggle()
+ self.routine = updatedRoutine
+ // 상위에 이벤트 전파
+ self.didTapMainCheckButton?(updatedRoutine)
},
for: .touchUpInside)
// ...
}
// ...
}📝 Committable suggestion
‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.
| private var routine: Routine { | |
| didSet { | |
| updateRoutineState() | |
| } | |
| } | |
| weak var delegate: RoutineViewDelegate? | |
| init(routine: MainRoutine) { | |
| init(routine: Routine) { | |
| self.routine = routine | |
| super.init(frame: .zero) | |
| final class RoutineView: UIView { | |
| /// 루틴 완료 버튼 탭 시 상위에 변경된 Routine 전달 | |
| var didTapMainCheckButton: ((Routine) -> Void)? | |
| private var routine: Routine { | |
| didSet { | |
| updateRoutineState() | |
| } | |
| } | |
| init(routine: Routine) { | |
| self.routine = routine | |
| super.init(frame: .zero) | |
| } | |
| private func configureAttribute() { | |
| // ... | |
| mainRoutineCheckButton.addAction( | |
| UIAction { [weak self] _ in | |
| guard let self else { return } | |
| var updatedRoutine = routine | |
| updatedRoutine.isDone.toggle() | |
| self.routine = updatedRoutine | |
| // 상위에 이벤트 전파 | |
| self.didTapMainCheckButton?(updatedRoutine) | |
| }, | |
| for: .touchUpInside) | |
| // ... | |
| } | |
| // ... | |
| } |
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Home/View/Component/RoutineView.swift around
lines 27–35, add an optional closure property (e.g. didTapMainCheckButton) to
expose the "routine completed" toggle to the parent, and modify the UIAction for
mainRoutineCheckButton so that after toggling routine.isDone and before/after
calling updateRoutineState() it invokes that closure with the updated routine
(or relevant id/status tuple) so the ViewModel/UseCase can handle persistence;
finally, wire this closure from the Home screen (HomeView/VC) to call the
ViewModel action that updates the routine state.
| viewModel.output.routineListDatePublisher | ||
| .receive(on: DispatchQueue.main) | ||
| .sink { [weak self] selectedDate in | ||
| guard let self else { return } | ||
|
|
||
| guard let viewModel = DIContainer.shared.resolve(type: RoutineListViewModel.self) | ||
| else { return } | ||
|
|
||
| let routineListViewController = RoutineListViewController(viewModel: viewModel, selectedDate: selectedDate) | ||
| routineListViewController.hidesBottomBarWhenPushed = true | ||
| self.navigationController?.pushViewController(routineListViewController, animated: true) | ||
| } | ||
| .store(in: &cancellables) |
There was a problem hiding this comment.
DI 컨테이너 해결 실패 시 조용한 실패 문제
DIContainer.shared.resolve(type: RoutineListViewModel.self)이 실패할 경우 아무런 피드백 없이 조용히 실패합니다. 다른 곳에서는 fatalError를 사용하는데 일관성이 없습니다.
일관성을 위해 다음과 같이 수정하는 것을 권장합니다:
- guard let viewModel = DIContainer.shared.resolve(type: RoutineListViewModel.self)
- else { return }
+ guard let viewModel = DIContainer.shared.resolve(type: RoutineListViewModel.self)
+ else { fatalError("RoutineListViewModel 의존성이 등록되지 않았습니다.") }📝 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.
| viewModel.output.routineListDatePublisher | |
| .receive(on: DispatchQueue.main) | |
| .sink { [weak self] selectedDate in | |
| guard let self else { return } | |
| guard let viewModel = DIContainer.shared.resolve(type: RoutineListViewModel.self) | |
| else { return } | |
| let routineListViewController = RoutineListViewController(viewModel: viewModel, selectedDate: selectedDate) | |
| routineListViewController.hidesBottomBarWhenPushed = true | |
| self.navigationController?.pushViewController(routineListViewController, animated: true) | |
| } | |
| .store(in: &cancellables) | |
| viewModel.output.routineListDatePublisher | |
| .receive(on: DispatchQueue.main) | |
| .sink { [weak self] selectedDate in | |
| guard let self else { return } | |
| guard let viewModel = DIContainer.shared.resolve(type: RoutineListViewModel.self) | |
| else { fatalError("RoutineListViewModel 의존성이 등록되지 않았습니다.") } | |
| let routineListViewController = RoutineListViewController(viewModel: viewModel, selectedDate: selectedDate) | |
| routineListViewController.hidesBottomBarWhenPushed = true | |
| self.navigationController?.pushViewController(routineListViewController, animated: true) | |
| } | |
| .store(in: &cancellables) |
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Home/View/HomeView.swift around lines 438–450,
the DIContainer.resolve call silently returns nil; replace the quiet failure
with a consistent fatalError on resolve failure: unwrap the resolved
RoutineListViewModel and if nil call fatalError with a clear message (e.g.,
"Failed to resolve RoutineListViewModel from DIContainer") so the app fails
loudly and consistently with other code paths; keep the existing weak self guard
and navigation logic unchanged.
| var title: String { get } | ||
| var subTitle: String? { get } | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
프로퍼티명 변경 영향 범위 확인(mainTitle → title)
모듈 내 전역 치환이 누락된 참조가 없는지 확인 부탁드립니다.
검증 스크립트:
🏁 Script executed:
#!/bin/bash
# 남아있는 mainTitle 참조 검색
rg -n -C2 --type=swift '\bmainTitle\b'Length of output: 2706
mainTitle → title 변경 누락된 참조 확인 필요
아래 파일에서 아직 mainTitle 참조가 남아 있으므로 전역 치환을 완료해주세요.
- Projects/Presentation/Sources/Onboarding/View/OnboardingViewController.swift:77
기존)onboarding.mainTitle→ 변경)onboarding.title - Projects/Presentation/Sources/Onboarding/Model/RecommendedRoutine.swift
• init 파라미터:mainTitle: String→title: String
• 내부 할당:self.title = mainTitle→self.title = title
• 팩토리 호출:mainTitle: title→title: title - Projects/Presentation/Sources/Onboarding/Model/OnboardingType.swift:21
기존)var mainTitle: String { … }→ 변경)var title: String { … }
| let routineType: RoutineCategoryType? | ||
| let routineLevel: RoutineLevelType |
There was a problem hiding this comment.
routineType 옵셔널/파라미터 불일치로 인한 컴파일 오류 가능성
- Line 16: 프로퍼티가
RoutineCategoryType?(옵셔널)로 선언되어 있습니다. - Line 46: 매핑에서
type(아마도RoutineCategoryType?)를 이니셜라이저의routineType: RoutineCategoryType(비옵셔널) 인자로 전달하고 있습니다.
현재 상태라면 옵셔널을 비옵셔널 인자로 넘기는 부분에서 컴파일이 실패합니다. 두 가지 방향 중 하나로 정리해야 합니다.
옵션 A(권장): 이니셜라이저의 파라미터도 옵셔널로 맞춥니다.
- 시그니처 예:
init(... routineType: RoutineCategoryType?, ...) - 본 변경은 현재 파일의 변경 라인 범위를 넘어가므로, 별도 커밋에서 이니셜라이저 시그니처를 수정해 주세요.
옵션 B(빠른 완화책): 매핑 시 기본값을 주입합니다. 다만 의미 왜곡 가능성이 있으니 도메인 정의를 재확인하세요.
- routineType: type,
+ routineType: type ?? .recommendation,도메인 관점에서 routineType == nil이 의미가 있는 경우(예: 추천 유형이 비정의 상태), 옵션 A가 더 안전합니다. 어떤 의미가 맞는지 한 번 더 확인 부탁드립니다.
Also applies to: 46-46
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Onboarding/Model/RecommendedRoutine.swift
around lines 16 and 46, the property routineType is declared as
RoutineCategoryType? but is being passed to an initializer expecting a
non-optional RoutineCategoryType, causing a compile error; fix it either by (A)
changing the initializer signature to accept routineType: RoutineCategoryType?
(preferred — update the init declaration in its file in a separate commit) or
(B) supply a sensible default when mapping (e.g., map type ?? .someDefault) as a
quick mitigation after confirming domain semantics; pick A if nil has meaning,
otherwise use B.
There was a problem hiding this comment.
Actionable comments posted: 7
♻️ Duplicate comments (2)
Projects/Presentation/Sources/Common/Component/RoutineCardView.swift (2)
125-139: SnapKit 제약 설정 순서: addArrangedSubview 이후에 제약StackView에 추가하기 전에 height 제약을 거는 패턴은 경고/오동작 원인이 됩니다. 기존 학습에도 동일 내용이 있었으니 순서를 교정하세요.
- subRoutineLabel.snp.makeConstraints { make in - make.height.equalTo(Layout.subLabelHeight) - } - subRoutineStackView.addArrangedSubview(subRoutineLabel) + subRoutineStackView.addArrangedSubview(subRoutineLabel) + subRoutineLabel.snp.makeConstraints { make in + make.height.equalTo(Layout.subLabelHeight) + } ... - subRoutineTitleLabel.snp.makeConstraints { make in - make.height.equalTo(Layout.subLabelHeight) - } - subRoutineStackView.addArrangedSubview(subRoutineTitleLabel) + subRoutineStackView.addArrangedSubview(subRoutineTitleLabel) + subRoutineTitleLabel.snp.makeConstraints { make in + make.height.equalTo(Layout.subLabelHeight) + }
157-161: 반복/기간/시간 라벨도 동일 패턴 수정infoStackView에 추가하기 전에 높이 제약을 걸고 있습니다. addArrangedSubview 이후로 이동하세요.
- repeatDayLabel.snp.makeConstraints { make in - make.height.equalTo(Layout.subLabelHeight) - } - infoStackView.addArrangedSubview(repeatDayLabel) + infoStackView.addArrangedSubview(repeatDayLabel) + repeatDayLabel.snp.makeConstraints { make in + make.height.equalTo(Layout.subLabelHeight) + } ... - periodLabel.snp.makeConstraints { make in - make.height.equalTo(Layout.subLabelHeight) - } - infoStackView.addArrangedSubview(periodLabel) + infoStackView.addArrangedSubview(periodLabel) + periodLabel.snp.makeConstraints { make in + make.height.equalTo(Layout.subLabelHeight) + } ... - timeLabel.snp.makeConstraints { make in - make.height.equalTo(Layout.subLabelHeight) - } - infoStackView.addArrangedSubview(timeLabel) + infoStackView.addArrangedSubview(timeLabel) + timeLabel.snp.makeConstraints { make in + make.height.equalTo(Layout.subLabelHeight) + }Also applies to: 171-175, 183-187
🧹 Nitpick comments (18)
Projects/DataSource/Sources/Endpoint/RoutineEndpoint.swift (1)
34-35: 당일 삭제 API에서 날짜 맥락 상실 가능성 — UX/기능 리스크 점검 권장기존에는 당일 삭제 시
performedDate등 날짜 맥락을 본문으로 전달하던 흐름이었는데, 현재는/day/{routineId}로만 호출합니다. 사용자가 과거/미래 날짜 화면에서 “당일 삭제”를 시도할 때, 서버가 어떤 날짜를 대상으로 동작하는지 불명확할 수 있습니다(서버가 ‘오늘’로 간주하면 오작동). 백엔드 스펙상 날짜가 불필요하다는 확신이 없다면, 쿼리 파라미터 등으로 날짜를 명시하는 방안을 협의해 주세요.아래처럼 날짜를 함께 전달하는 설계를 제안드립니다(백엔드 합의 필요).
-enum RoutineEndpoint { - ... - case deleteDailyRoutine(routineId: String) +enum RoutineEndpoint { + ... + case deleteDailyRoutine(routineId: String, performedDate: String)var path: String { switch self { ... - case .deleteDailyRoutine(let routineId): - "\(baseURL)/day/\(routineId)" + case .deleteDailyRoutine(let routineId, _): + "\(baseURL)/day/\(routineId)"var queryParameters: [String : String] { switch self { + case .deleteDailyRoutine(_, let performedDate): + return ["performedDate": performedDate] case .fetchRoutines(let startDate, let endDate): return [ "startDate": startDate, "endDate": endDate] default: return [:] } }Projects/Domain/Sources/Protocol/Repository/RoutineRepositoryProtocol.swift (2)
24-25: 반환 타입 변경에 따른 문서(주석) 보완 필요
fetchRoutines가 이제 날짜별(routines, allCompleted)튜플을 반환합니다. 반환값 설명을 주석에 명시해 두면 호출처 구현이 수월합니다.적용 예시(주석만 변경):
/// 루틴 목록을 조회합니다. (기간) /// - Parameters: /// - startDate: 조회 시작 날짜 /// - endDate: 조회 종료 날짜 - func fetchRoutines(from startDate: String, to endDate: String) async throws -> [String: (routines: [RoutineEntity], allCompleted: Bool)] + /// - Returns: 키는 "yyyy-MM-dd" 형식의 날짜 문자열, + /// 값은 (routines: 해당 날짜의 루틴 목록, allCompleted: 해당 날짜의 전체 완료 여부) + func fetchRoutines(from startDate: String, to endDate: String) async throws -> [String: (routines: [RoutineEntity], allCompleted: Bool)]
35-38: 파라미터 명세 주석과 시그니처 불일치주석은
- Parameter routine:으로 남아 있으나, 실제 시그니처는routineId만 받습니다. 주석 정정 부탁드립니다.적용 예시(주석만 변경):
/// 당일 루틴을 삭제합니다. -/// - Parameter routine: 삭제할 루틴 정보 +/// - Parameter routineId: 삭제할 루틴의 ID func deleteDailyRoutine(routineId: String) async throwsProjects/Domain/Sources/Protocol/UseCase/RoutineUseCaseProtocol.swift (1)
13-14: 가독성 향상을 위한 반환 타입 별칭 제안튜플 반환 타입이 여러 계층에 반복됩니다.
typealias DailyRoutineBucket = (routines: [RoutineEntity], allCompleted: Bool)같은 별칭을 Domain 레벨에 두면 시그니처 가독성이 좋아집니다.추가 예시:
public typealias DailyRoutineBucket = (routines: [RoutineEntity], allCompleted: Bool) func fetchRoutines(startDate: Date, endDate: Date) async throws -> [String: DailyRoutineBucket]Projects/DataSource/Sources/Repository/RoutineRepository.swift (1)
37-49: 반환 딕셔너리 구성 시 튜플 라벨 유지 및 불필요한 compactMap 제거 제안
- 내부
result변수의 타입에서 라벨을 잃어버려 읽기성이 떨어집니다. 시그니처와 동일하게 라벨을 유지하세요.toRoutineEntity()가 non-optional을 반환한다면compactMap대신map이 맞습니다.아래 변경을 권장합니다:
- var result: [String: ([RoutineEntity], Bool)] = [:] + var result: [String: (routines: [RoutineEntity], allCompleted: Bool)] = [:] for (date, routineDTO) in response.routines { - let allCompleted = routineDTO.allCompleted - let routines = routineDTO.routineList.compactMap({ $0.toRoutineEntity() }) - result[date] = (routines, allCompleted) + let allCompleted = routineDTO.allCompleted + let routines = routineDTO.routineList.map { $0.toRoutineEntity() } + result[date] = (routines: routines, allCompleted: allCompleted) }Projects/Domain/Sources/UseCase/Routine/RoutineUseCase.swift (2)
23-29: 반환 타입 변경 반영 OK — 별칭 도입 고려UseCase 시그니처가 새 튜플 타입을 잘 반영했습니다. 여러 레이어에서 동일 튜플을 반복 사용하므로
typealias도입을 고려해 주세요.
62-64: 삭제 유스케이스 라우팅 정합성 OK + 프레젠테이션 계층 일관성 권고Repository 호출로 위임하는 흐름은 적절합니다. 한편
RoutineListViewModel이 Repository를 직접 참조하는 코드가 스니펫에 보입니다. Domain 중심 구조를 유지하려면 ViewModel → UseCase → Repository 경로로 일관화하는 것을 권장합니다.관련 스니펫: Projects/Presentation/Sources/RoutineList/ViewModel/RoutineListViewModel.swift (lines 66-85)
Projects/Presentation/Sources/RoutineList/View/RoutineEditAlertViewController.swift (1)
91-99: 고정 높이 제약 최소화 제안title 라벨에 고정 높이(24pt)를 두면 다국어/동적 글꼴에서 잘림 위험이 있습니다. 가능하면 height 제약을 제거하고 intrinsic size에 맡기세요.
mainLabel.snp.makeConstraints { make in make.top.equalToSuperview().offset(Layout.mainLabelTopSpacing) make.leading.equalToSuperview().offset(Layout.horizontalMargin) - make.height.equalTo(Layout.mainLabelHeight) }Projects/Presentation/Sources/RoutineList/View/RoutineDeleteAlertViewController.swift (3)
32-33: 오타: cancleButton → cancelButton (내부 식별자 가독성)내부 식별자 오타는 유지보수 부담을 줍니다. 일괄 변경을 권장합니다.
- private let cancleButton = UIButton() + private let cancelButton = UIButton() ... - var cancleButtonConfiguration = UIButton.Configuration.filled() - cancleButtonConfiguration.baseBackgroundColor = BitnagilColor.gray97 - cancleButtonConfiguration.background.cornerRadius = 12 - cancleButtonConfiguration.attributedTitle = AttributedString( + var cancelButtonConfiguration = UIButton.Configuration.filled() + cancelButtonConfiguration.baseBackgroundColor = BitnagilColor.gray97 + cancelButtonConfiguration.background.cornerRadius = 12 + cancelButtonConfiguration.attributedTitle = AttributedString( "취소", attributes: .init([.font: BitnagilFont(style: .body2, weight: .medium).font])) - cancleButtonConfiguration.baseForegroundColor = BitnagilColor.gray40 - cancleButton.configuration = cancleButtonConfiguration - cancleButton.addAction( + cancelButtonConfiguration.baseForegroundColor = BitnagilColor.gray40 + cancelButton.configuration = cancelButtonConfiguration + cancelButton.addAction( UIAction { [weak self] _ in self?.dismissToRootView() }, for: .touchUpInside) ... - [cancleButton, confirmButton].forEach { + [cancelButton, confirmButton].forEach { buttonStackView.addArrangedSubview($0) }Also applies to: 77-85, 114-116
74-76: 확인/취소 버튼 너비 불균형 가능성: fillEqually 권장가로 스택뷰에 두 버튼을 넣고 높이만 지정하면 intrinsic width에 따라 버튼 너비가 달라질 수 있습니다. 디자인 의도대로 동일 너비를 강제하세요.
buttonStackView.axis = .horizontal buttonStackView.spacing = Layout.buttonStackViewSpacing + buttonStackView.distribution = .fillEquallyAlso applies to: 137-142
61-66: 중복 스타일 지정 제거 제안이미 attributedText로 폰트/라인하이트를 지정했는데, 이어서 font를 다시 설정하고 있습니다. 불필요하므로 제거하세요.
subLabel.attributedText = BitnagilFont(style: .body2, weight: .medium).attributedString(text: subLabelText) subLabel.numberOfLines = 2 - subLabel.font = BitnagilFont(style: .body2, weight: .medium).font subLabel.textColor = BitnagilColor.gray40Projects/Presentation/Sources/Common/Component/RoutineCardView.swift (2)
98-106: tintColor가 적용되지 않을 수 있음 (렌더링 모드)UIGraphicsImageRenderer로 리사이즈한 이미지의 렌더링 모드는 기본적으로 original입니다. tintColor 적용 의도라면 template로 강제하세요.
- plusButton.setImage(plusImage, for: .normal) + plusButton.setImage(plusImage?.withRenderingMode(.alwaysTemplate), for: .normal) plusButton.tintColor = BitnagilColor.gray10
27-28: 고정 높이(20pt) 라벨들: 동적 글꼴 대응성 저하여러 라벨에 고정 height를 부여하고 있습니다. 가능하면 제거하고 contentCompressionResistancePriority로 단일 라인을 유지하거나, 최소/최대 줄 수만 설정하세요.
Also applies to: 125-127, 135-137, 157-159, 171-173, 183-185
Projects/Presentation/Sources/RoutineList/View/RoutineDeleteViewController.swift (2)
51-56: 중복 스타일 지정 제거 제안attributedText로 스타일을 설정한 뒤 font를 다시 설정하고 있습니다. 불필요한 중복입니다.
subLabel.attributedText = BitnagilFont(style: .body2, weight: .medium).attributedString(text: subLabelText) subLabel.numberOfLines = 2 - subLabel.font = BitnagilFont(style: .body2, weight: .medium).font subLabel.textColor = BitnagilColor.gray40
127-139: 하위 확인 시트 표시 애니메이션 일관성다른 시트들은 animated: true로 표시되는데, 여기만 false입니다. UX 일관성을 위해 true를 권장합니다(특별한 이유가 없다면).
- present(deleteAlertViewController, animated: false) + present(deleteAlertViewController, animated: true)Projects/Presentation/Sources/RoutineList/View/RoutineListViewController.swift (2)
49-52: 매직 태그(999) 기반 뷰 제거는 취약합니다외부에서 동일 태그를 쓰면 예기치 않은 뷰가 제거될 수 있습니다. dimmedView 참조를 일관되게 사용하고, 해당 라인은 제거하세요.
override func viewWillAppear(_ animated: Bool) { super.viewWillAppear(animated) - view.subviews.first(where: { $0.tag == 999 })?.removeFromSuperview() }
182-205: 디밍 레이어 해제 경로 점검 OK시트를 닫을 때 onDismiss에서 dimmedView를 해제하는 흐름이 잘 연결되어 있습니다. 단, 시스템 시트 자체의 dim과 중복되어 과도하게 어두워질 수 있으니, 필요시 시스템 dim만 사용하도록 전환하는 옵션도 고려해 보세요.
Also applies to: 238-241, 257-260
Projects/Presentation/Sources/RoutineList/ViewModel/RoutineListViewModel.swift (1)
32-32: selectedRoutine을 CurrentValueSubject 대신 단순 프로퍼티로 변경하는 것을 고려해보세요.현재
selectedRoutine이 CurrentValueSubject로 선언되어 있지만, Output에서 노출되지 않고 내부적으로만 사용되므로 단순한 옵셔널 프로퍼티로 충분할 것 같습니다.- private let selectedRoutine = CurrentValueSubject<Routine?, Never>(nil) + private var selectedRoutine: Routine?그리고 사용 부분도 수정:
- selectedRoutine.value = routine + selectedRoutine = routine- guard let routineId = selectedRoutine.value?.id else { + guard let routineId = selectedRoutine?.id else {
📜 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 (6)
Projects/Presentation/Resources/Images.xcassets/Graphic/default_emotion_graphic.imageset/default_emotion_graphic.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/default_emotion_graphic.imageset/default_emotion_graphic@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/Graphic/default_emotion_graphic.imageset/default_emotion_graphic@3x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineList/close_icon.imageset/close_icon.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineList/close_icon.imageset/close_icon@2x.pngis excluded by!**/*.pngProjects/Presentation/Resources/Images.xcassets/RoutineList/close_icon.imageset/close_icon@3x.pngis excluded by!**/*.png
📒 Files selected for processing (15)
Projects/DataSource/Sources/Endpoint/RoutineEndpoint.swift(2 hunks)Projects/DataSource/Sources/Repository/RoutineRepository.swift(2 hunks)Projects/Domain/Sources/Entity/DeleteRoutineEntity.swift(0 hunks)Projects/Domain/Sources/Protocol/Repository/RoutineRepositoryProtocol.swift(2 hunks)Projects/Domain/Sources/Protocol/UseCase/RoutineUseCaseProtocol.swift(1 hunks)Projects/Domain/Sources/UseCase/Routine/RoutineUseCase.swift(2 hunks)Projects/Presentation/Resources/Images.xcassets/RoutineList/close_icon.imageset/Contents.json(1 hunks)Projects/Presentation/Sources/Common/Component/RoutineCardView.swift(8 hunks)Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift(2 hunks)Projects/Presentation/Sources/Home/View/HomeView.swift(2 hunks)Projects/Presentation/Sources/RoutineList/View/RoutineDeleteAlertViewController.swift(1 hunks)Projects/Presentation/Sources/RoutineList/View/RoutineDeleteViewController.swift(1 hunks)Projects/Presentation/Sources/RoutineList/View/RoutineEditAlertViewController.swift(1 hunks)Projects/Presentation/Sources/RoutineList/View/RoutineListViewController.swift(1 hunks)Projects/Presentation/Sources/RoutineList/ViewModel/RoutineListViewModel.swift(1 hunks)
💤 Files with no reviewable changes (1)
- Projects/Domain/Sources/Entity/DeleteRoutineEntity.swift
✅ Files skipped from review due to trivial changes (1)
- Projects/Presentation/Resources/Images.xcassets/RoutineList/close_icon.imageset/Contents.json
🚧 Files skipped from review as they are similar to previous changes (2)
- Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift
- Projects/Presentation/Sources/Home/View/HomeView.swift
🧰 Additional context used
🧠 Learnings (3)
📚 Learning: 2025-08-01T06:38:18.687Z
Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#31
File: Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift:97-103
Timestamp: 2025-08-01T06:38:18.687Z
Learning: choijungp의 HomeViewModel에서 fetchRoutines(for date: Date) 메서드는 기존 캐시 범위를 확장하는 것이 아니라, 요청된 날짜 주변의 새로운 데이터 청크를 가져오기 위해 oldestDate와 latestDate를 모두 업데이트한다. 과거 날짜 요청 시에는 해당 날짜의 1주 전~1일 전, 미래 날짜 요청 시에는 해당 날짜의 1일 후~1주 후 범위로 설정한다.
Applied to files:
Projects/Domain/Sources/Protocol/Repository/RoutineRepositoryProtocol.swift
📚 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/RoutineList/View/RoutineListViewController.swift
📚 Learning: 2025-08-17T13:30:29.342Z
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.342Z
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.
Applied to files:
Projects/Presentation/Sources/Common/Component/RoutineCardView.swift
🧬 Code graph analysis (11)
Projects/Presentation/Sources/RoutineList/View/RoutineDeleteAlertViewController.swift (3)
Projects/Presentation/Sources/RoutineList/View/RoutineDeleteViewController.swift (1)
viewDidDisappear(38-44)Projects/Presentation/Sources/Common/DesignSystem/Font/BitnagilFont.swift (1)
attributedString(50-66)Projects/Presentation/Sources/RoutineList/ViewModel/RoutineListViewModel.swift (2)
action(47-65)deleteRoutine(106-125)
Projects/Domain/Sources/UseCase/Routine/RoutineUseCase.swift (3)
Projects/DataSource/Sources/Repository/RoutineRepository.swift (2)
fetchRoutines(37-49)deleteDailyRoutine(72-75)Projects/Presentation/Sources/RoutineList/ViewModel/RoutineListViewModel.swift (1)
fetchRoutines(67-86)Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift (3)
fetchRoutines(152-158)fetchRoutines(197-210)deleteDailyRoutine(230-255)
Projects/Presentation/Sources/RoutineList/View/RoutineDeleteViewController.swift (2)
Projects/Presentation/Sources/RoutineList/View/RoutineDeleteAlertViewController.swift (3)
viewDidDisappear(46-54)configureAttribute(56-105)configureLayout(107-143)Projects/Presentation/Sources/Common/DesignSystem/Font/BitnagilFont.swift (1)
attributedString(50-66)
Projects/Domain/Sources/Protocol/UseCase/RoutineUseCaseProtocol.swift (4)
Projects/DataSource/Sources/Repository/RoutineRepository.swift (3)
fetchRoutines(37-49)deleteAllRoutine(67-70)deleteDailyRoutine(72-75)Projects/Domain/Sources/UseCase/Routine/RoutineUseCase.swift (3)
fetchRoutines(23-29)deleteAllRoutine(58-60)deleteDailyRoutine(62-64)Projects/Presentation/Sources/RoutineList/ViewModel/RoutineListViewModel.swift (1)
fetchRoutines(67-86)Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift (4)
fetchRoutines(152-158)fetchRoutines(197-210)deleteAllRoutine(213-227)deleteDailyRoutine(230-255)
Projects/Presentation/Sources/RoutineList/View/RoutineEditAlertViewController.swift (1)
Projects/Presentation/Sources/RoutineList/View/RoutineListViewController.swift (2)
goToRoutineCreationView(151-160)configureAttribute(54-71)
Projects/DataSource/Sources/Repository/RoutineRepository.swift (5)
Projects/DataSource/Sources/NetworkService/NetworkService.swift (1)
request(25-48)Projects/DataSource/Sources/DTO/RoutineDTO.swift (1)
toRoutineEntity(32-45)Projects/Domain/Sources/UseCase/Routine/RoutineUseCase.swift (2)
fetchRoutines(23-29)deleteDailyRoutine(62-64)Projects/Presentation/Sources/RoutineList/ViewModel/RoutineListViewModel.swift (1)
fetchRoutines(67-86)Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift (3)
fetchRoutines(152-158)fetchRoutines(197-210)deleteDailyRoutine(230-255)
Projects/DataSource/Sources/Endpoint/RoutineEndpoint.swift (3)
Projects/DataSource/Sources/Repository/RoutineRepository.swift (2)
deleteDailyRoutine(72-75)deleteAllRoutine(67-70)Projects/Domain/Sources/UseCase/Routine/RoutineUseCase.swift (2)
deleteDailyRoutine(62-64)deleteAllRoutine(58-60)Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift (2)
deleteDailyRoutine(230-255)deleteAllRoutine(213-227)
Projects/Domain/Sources/Protocol/Repository/RoutineRepositoryProtocol.swift (4)
Projects/DataSource/Sources/Repository/RoutineRepository.swift (2)
fetchRoutines(37-49)deleteDailyRoutine(72-75)Projects/Domain/Sources/UseCase/Routine/RoutineUseCase.swift (2)
fetchRoutines(23-29)deleteDailyRoutine(62-64)Projects/Presentation/Sources/RoutineList/ViewModel/RoutineListViewModel.swift (1)
fetchRoutines(67-86)Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift (3)
fetchRoutines(152-158)fetchRoutines(197-210)deleteDailyRoutine(230-255)
Projects/Presentation/Sources/RoutineList/View/RoutineListViewController.swift (5)
Projects/Presentation/Sources/RoutineList/ViewModel/RoutineListViewModel.swift (2)
action(47-65)fetchDailyRoutine(95-104)Projects/Presentation/Sources/Common/Component/RoutineCardView.swift (2)
configureAttribute(64-191)configureAttribute(277-282)Projects/Shared/Sources/DIContainer/DIContainer.swift (1)
resolve(18-25)Projects/Presentation/Sources/Common/Extension/UIViewController+.swift (1)
configureCustomNavigationBar(19-29)Projects/Presentation/Sources/Home/View/Component/WeekView.swift (1)
updateWeekDateViews(68-101)
Projects/Presentation/Sources/RoutineList/ViewModel/RoutineListViewModel.swift (3)
Projects/DataSource/Sources/Repository/RoutineRepository.swift (3)
fetchRoutines(37-49)deleteAllRoutine(67-70)deleteDailyRoutine(72-75)Projects/Shared/Sources/Extension/Date+.swift (1)
convertToString(16-22)Projects/Presentation/Sources/Home/Model/Routine.swift (1)
toRoutine(26-39)
Projects/Presentation/Sources/Common/Component/RoutineCardView.swift (4)
Projects/Presentation/Sources/RoutineList/View/RoutineListViewController.swift (3)
routineCardView(172-172)routineCardView(174-212)routineCardView(214-264)Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineViewController.swift (3)
routineCardView(334-341)routineCardView(343-343)routineCardView(344-344)Projects/Presentation/Sources/Common/Extension/UIImage+.swift (1)
resizeAspectFit(20-35)Projects/Shared/Sources/Extension/Date+.swift (1)
convertToString(16-22)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (10)
Projects/DataSource/Sources/Endpoint/RoutineEndpoint.swift (1)
21-25: 엔드포인트 버전 분기 및 URL 구성 로직 검증
deleteAllRoutine만 v1(
/api/v1/routines), 그 외는 v2(/api/v2/routines)로 분기한 부분
→ 백엔드 스펙(“전체 삭제(v1)” vs “당일 삭제(v2)”)이 실제로도 이렇게 설계된 것이 맞는지 최종 확인 필요NetworkService의
makeURLRequest()호출 흐름 상에서
•Endpoint.path가 이미AppProperties.baseURL + "/api/…"형태의 절대 URL을 반환
•URLRequest(urlString:queryParameters:)초기화 시URLComponents(string: urlString)를 바로 사용
→ baseURL이 이중으로 합쳐지지 않음을 확인 완료유지보수성 관점에서
•"v1","v2"를 하드코딩하지 않고,enum이나struct상수로 관리할 것을 권장Projects/DataSource/Sources/Repository/RoutineRepository.swift (2)
30-35: 단건 조회 디코딩 타입 전환 적절
RoutineDTO→RoutineEntity매핑으로 단순화되었습니다. 네트워크 404/없음 시nil반환 가드도 합리적입니다.
72-75: 당일 삭제 엔드포인트 호출 OK — 단, 날짜 불명확성은 상위 코멘트 참조Repository 계층의 변경은 Endpoint와 일관됩니다. 다만 Endpoint 코멘트에서 언급한 바와 같이 날짜 맥락이 필요한 경우 설계 재검토가 필요할 수 있습니다(쿼리 파라미터 도입 등).
Projects/Presentation/Sources/RoutineList/View/RoutineEditAlertViewController.swift (1)
66-81: 내일 적용 플로우는 전달 값이 적절합니다“다음 날부터 적용”에서 false 전달은 의도에 부합합니다. 위 today 버튼 수정만 반영하면 일관성 있습니다.
Projects/Presentation/Sources/RoutineList/View/RoutineDeleteAlertViewController.swift (1)
46-54: 중복 dismiss 로직 제거 및 책임 일원화 제안
RoutineDeleteAlertViewController.swift의 viewDidDisappear에서 이중 dismiss 호출을 제거하고, 실제 화면 전환은 오직 dismissToRootView() 에서만 수행되도록 리팩터링을 권장합니다.• 대상 위치
– Projects/Presentation/Sources/RoutineList/View/RoutineDeleteAlertViewController.swift:46–54• 변경 전(viewDidDisappear)
override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) if isBeingDismissed { // 중복 1: dismissToRootView()에서도 이미 루트까지 dismiss 처리됨 if let routineDeleteViewController = presentingViewController?.presentingViewController { routineDeleteViewController.dismiss(animated: false) } onDismiss?() } }• 변경 후 제안
override func viewDidDisappear(_ animated: Bool) { super.viewDidDisappear(animated) - if isBeingDismissed { - if let routineDeleteViewController = presentingViewController?.presentingViewController { - routineDeleteViewController.dismiss(animated: false) - } - onDismiss?() - } + guard isBeingDismissed else { return } + onDismiss?() }이렇게 하면
- dismiss 흐름은 dismissButton·cancelButton 액션, 바인딩된 isDeleteRoutine sink 내부의 dismissToRootView() 에서만 수행
- viewDidDisappear는 단순히 onDismiss 콜백만 처리
- 중복 호출로 인한 예기치 않은 동작 방지
[dOptional_refactors_recommended]
Projects/Presentation/Sources/Common/Component/RoutineCardView.swift (1)
76-97: 이벤트 바인딩 추가 LGTM이전 커밋의 “버튼 액션 핸들러 누락” 지적이 해결되었습니다. 델리게이트 호출도 nil-safe로 처리되어 좋습니다.
Projects/Presentation/Sources/RoutineList/ViewModel/RoutineListViewModel.swift (4)
68-86: fetchRoutines 메서드의 구현이 잘 되어 있습니다.주간 단위로 루틴을 가져오고 날짜별로 캐싱하는 로직이 적절하게 구현되어 있습니다. 에러 처리도 적절히 되어 있어 안정적인 동작이 기대됩니다.
89-93: 주간 시작일 계산 로직이 정확합니다.월요일을 주의 시작으로 하는 로직이 올바르게 구현되어 있습니다. 일요일(weekday = 1)일 때 6일을 빼서 월요일로 가는 처리도 정확합니다.
70-74: API 엔드포인트의 날짜 범위 포함 여부 확인 필요현재
RoutineListViewModel.swift에서 주간 범위를 계산할 때let startDate = calculateWeekStartDate(for: selectedDateSubject.value) let endDate = calendar.date(byAdding: .weekOfYear, value: 1, to: startDate) ?? Date()로
endDate를 “다음 주의 시작일”로 설정하고 있습니다.
API 스펙 상endDate가 조회 범위에 포함(inclusive)되는지, 아니면 제외(exclusive)되는지 확인해야 합니다.
만약 inclusive가 맞다면, 실제로 조회하고자 하는 마지막 날짜에서 하루를 빼는 로직이 필요할 수 있습니다.수정 제안
- API 문서나 백엔드 담당자에게
fetchRoutines(from:to:)호출 시to날짜가 포함 범위인지 확인- inclusive일 경우:
- let endDate = calendar.date(byAdding: .weekOfYear, value: 1, to: startDate) ?? Date() + let rawEnd = calendar.date(byAdding: .weekOfYear, value: 1, to: startDate) ?? Date() + let endDate = calendar.date(byAdding: .day, value: -1, to: rawEnd) ?? rawEnd- exclusive일 경우: 현 로직 유지
파일 및 위치
- Projects/Presentation/Sources/RoutineList/ViewModel/RoutineListViewModel.swift: 70–74행
106-125: 삭제 API 구현 확인 완료 및 승인
RoutineRepositoryProtocol, RoutineRepository, RoutineUseCase 전반에 걸쳐deleteAllRoutine(routineId:)및deleteDailyRoutine(routineId:)메서드가 정의·구현되어 있습니다. 네트워크 요청은RoutineEndpoint.deleteAllRoutine·deleteDailyRoutine케이스를 통해 올바른 HTTP DELETE 메서드와 경로로 전달되며, 빈 응답을EmptyResponseDTO로 매핑합니다. 따라서 삭제 성공 시fetchRoutines()호출을 통해 화면이 정상적으로 갱신될 것으로 판단됩니다.
| case deleteDailyRoutine(routineId: String) | ||
| case updateRoutineCompletion(routines: RoutineCompletionListDTO) | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
시그니처 변경 자체는 일관성 있게 반영됨
deleteDailyRoutine(routineId:)로 단순화된 파라미터는 Repository/UseCase 계층과도 정합성이 맞습니다. 다만 하위 호출처에 기존 DeleteRoutineEntity 기반 호출이 남아 있지 않은지 확인이 필요합니다.
다음 스크립트로 이전 시그니처 사용 및 관련 엔티티 잔존 여부를 빠르게 점검해 주세요.
🏁 Script executed:
#!/bin/bash
# 1) 이전 시그니처 사용 여부
rg -nP 'deleteDailyRoutine\s*\(\s*routine\s*:' -C2
# 2) 삭제된(예정) 엔티티/DTO 흔적
rg -n 'DeleteRoutineEntity|DeleteSubRoutineEntity|DeleteRoutineDTO' -C2Length of output: 2066
HomeViewModel에서 deleteDailyRoutine 호출 시그니처와 레거시 엔티티/DTO 제거 필요
다음 사항을 반영해 주세요:
-
Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift
• 라인 247:
현재try await routineUseCase.deleteDailyRoutine(routine: deleteRoutinEntity)
수정 후
try await routineUseCase.deleteDailyRoutine(routineId: deleteRoutinEntity.routineId)
• 관련 변수명(
deleteRoutinEntity→deleteRoutineEntity) 오타도 함께 정정 필요합니다. -
레거시 엔티티/DTO 제거 또는 리팩터링
•DeleteSubRoutineEntity,DeleteRoutineEntity(라인 235–239)
•Projects/DataSource/Sources/DTO/DeleteRoutineDTO.swift이제 API 호출 계층에서
DeleteRoutineDTO를 직접 사용하거나, 별도 변환 로직 없이routineId만 넘기는 구조로 통일하시면 됩니다.
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift around lines
235–247, the HomeViewModel still calls deleteDailyRoutine with a legacy entity
and has a typo in the variable name; change the call to pass only the routineId
(try await routineUseCase.deleteDailyRoutine(routineId:
deleteRoutineEntity.routineId)) and rename the variable deleteRoutinEntity →
deleteRoutineEntity; also remove or refactor legacy types DeleteSubRoutineEntity
and DeleteRoutineEntity (lines ~235–239) and the obsolete
Projects/DataSource/Sources/DTO/DeleteRoutineDTO.swift so the code either uses
the Endpoint's deleteDailyRoutine(routineId: String) directly or accepts the
current DeleteRoutineDTO at the API layer with no intermediate entity
conversion.
| func deleteDailyRoutine(routineId: String) async throws | ||
|
|
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
삭제 유스케이스 시그니처 단순화 OK — 호출처 정리 여부 확인
deleteDailyRoutine(routineId:)로 통일된 점은 👍. HomeViewModel의 옛 구현(주석 처리된 DeleteRoutineEntity 기반) 정리는 후속 PR에서라도 완료해 주세요.
다음 스크립트로 남아있는 구 호출 패턴을 점검할 수 있습니다.
🏁 Script executed:
#!/bin/bash
rg -nP 'deleteDailyRoutine\s*\(\s*routine:' -C2
rg -n 'DeleteRoutineEntity' -C2Length of output: 1282
HomeViewModel 호출부 및 DeleteRoutineEntity 정리 필요
HomeViewModel에서 여전히 구 버전 시그니처(deleteDailyRoutine(routine:) + DeleteRoutineEntity)를 사용 중입니다. 아래 항목을 반영해 주세요.
- 파일:
Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift- line 237–239:
⇒ 더 이상 사용되지 않으므로 삭제 또는 불필요해진 생성 로직 정리
let deleteRoutinEntity = DeleteRoutineEntity( routineId: routine.id, routineCompletionId: routine.completionId, )
- line 247:
⇒ 신규 시그니처에 맞춰
try await routineUseCase.deleteDailyRoutine(routine: deleteRoutinEntity)
deleteDailyRoutine(routineId:)호출로 변경
예시:try await routineUseCase.deleteDailyRoutine(routineId: routine.id)
- line 237–239:
DeleteRoutineEntity(및 연관 서브루틴 엔티티) 정의와 임포트도 사용하지 않는다면 함께 제거 바랍니다.
이렇게 변경하면 삭제 유스케이스 시그니처 단순화 작업이 완결됩니다.
🤖 Prompt for AI Agents
In Projects/Domain/Sources/Protocol/UseCase/RoutineUseCaseProtocol.swift around
lines 19–20, the protocol already exposes deleteDailyRoutine(routineId: String)
async throws but Presentation still uses the old DeleteRoutineEntity and call
signature; update
Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift (around lines
237–247) to stop constructing DeleteRoutineEntity, remove or delete that
creation block, replace the call try await
routineUseCase.deleteDailyRoutine(routine: deleteRoutinEntity) with try await
routineUseCase.deleteDailyRoutine(routineId: routine.id), and then remove the
unused DeleteRoutineEntity type (and any related sub-entities) and their imports
from the project files where they are no longer referenced.
| editButton.snp.makeConstraints { make in | ||
| make.top.equalToSuperview().offset(Layout.plusButtonTopSpacing) | ||
| make.trailing.equalTo(deleteButton).inset(Layout.editButtonTrailingSpacing) | ||
| make.size.equalTo(Layout.plusButtonSize) | ||
| } |
There was a problem hiding this comment.
오토레이아웃 버그: editButton.trailing 대상 앵커 오류
trailing을 deleteButton의 trailing에 “inset”으로 맞추면 두 버튼이 겹치거나 의도치 않은 위치에 배치됩니다. deleteButton의 leading 기준으로 간격을 설정해야 합니다.
- make.trailing.equalTo(deleteButton).inset(Layout.editButtonTrailingSpacing)
+ make.trailing.equalTo(deleteButton.snp.leading).offset(-Layout.editButtonTrailingSpacing)📝 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.
| editButton.snp.makeConstraints { make in | |
| make.top.equalToSuperview().offset(Layout.plusButtonTopSpacing) | |
| make.trailing.equalTo(deleteButton).inset(Layout.editButtonTrailingSpacing) | |
| make.size.equalTo(Layout.plusButtonSize) | |
| } | |
| editButton.snp.makeConstraints { make in | |
| make.top.equalToSuperview().offset(Layout.plusButtonTopSpacing) | |
| make.trailing.equalTo(deleteButton.snp.leading).offset(-Layout.editButtonTrailingSpacing) | |
| make.size.equalTo(Layout.plusButtonSize) | |
| } |
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Common/Component/RoutineCardView.swift around
lines 243–247, the editButton constraint incorrectly pins trailing to
deleteButton using inset which causes overlap; change the constraint to pin
editButton.trailing to deleteButton.snp.leading (use
.equalTo(deleteButton.snp.leading).offset(...)) so spacing is based on
deleteButton's leading anchor and adjust the offset sign/value to
Layout.editButtonTrailingSpacing as needed to create the intended gap.
| func routineCardView(_ sender: RoutineCardView, didTapEditButton routine: Routine) { | ||
| viewModel.action(input: .seleteRoutine(routine: routine)) | ||
|
|
||
| guard !routine.repeatDay.isEmpty else { | ||
| goToRoutineCreationView(routineId: routine.id) | ||
| return | ||
| } | ||
|
|
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
오타 유지: .seleteRoutine
Input 케이스명이 selectRoutine이 아닌 seleteRoutine으로 사용되고 있습니다. 전역적으로 사용 중이라면 이번 PR 범위를 넘을 수 있으나, 추후 리팩토링 대상으로 남겨두는 것을 권장드립니다.
다음으로 호출/정의 위치를 한 번에 파악해 네이밍 교정 범위를 추산할 수 있습니다:
Also applies to: 214-225
🏁 Script executed:
#!/bin/bash
rg -nP '\bseleteRoutine\b' -C2Length of output: 2394
seleteRoutine 오타 수정 필요
아래 위치들에서 잘못된 케이스명을 selectRoutine으로 일괄 변경해주세요.
– ViewModel 정의부 & 처리부
• Projects/Presentation/Sources/RoutineList/ViewModel/RoutineListViewModel.swift
- enum RoutineListViewModelInput의
case seleteRoutine(routine: Routine?) → case selectRoutine(routine: Routine?)
- switch문 내
case .seleteRoutine(let routine): → case .selectRoutine(let routine)
– ViewController 호출부
• Projects/Presentation/Sources/RoutineList/View/RoutineListViewController.swift
- didTapEditButton (라인 174181):225) 역시 동일 변경
viewModel.action(input: .seleteRoutine(routine: routine)) →
viewModel.action(input: .selectRoutine(routine: routine))
- didTapDeleteButton (라인 214
전역 사용처는 아래 스크립트로 확인했습니다:
rg -nP '\bseleteRoutine\b' -C2테스트 코드나 다른 호출 지점에도 영향이 있을 수 있으니 함께 점검 부탁드립니다.
🤖 Prompt for AI Agents
In
Projects/Presentation/Sources/RoutineList/View/RoutineListViewController.swift
around lines 174-181, change the ViewModel input call from
.seleteRoutine(routine: routine) to .selectRoutine(routine: routine); also
update the symmetric occurrence in didTapDeleteButton (lines ~214-225) to
.selectRoutine. Then update the ViewModel definition and handling in
Projects/Presentation/Sources/RoutineList/ViewModel/RoutineListViewModel.swift:
rename enum case case seleteRoutine(routine: Routine?) to case
selectRoutine(routine: Routine?) and update the switch arm from case
.seleteRoutine(let routine): to case .selectRoutine(let routine). Finally run a
global search (rg -nP '\bseleteRoutine\b') and update any remaining references,
including tests, to the corrected selectRoutine spelling to keep references
consistent.
There was a problem hiding this comment.
Actionable comments posted: 4
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (3)
Projects/Presentation/Sources/Common/Component/RoutineCardView.swift (1)
206-215: 긴 제목이 우측 버튼(플러스/편집·삭제) 아래로 파고드는 오버랩 가능성 — trailing 제약 추가 필요headerInfoStackView는 top/leading만 고정되어 있어 titleLabel이 긴 경우 우측 버튼 아래로 겹칠 수 있습니다. 상황별로 headerInfoStackView.trailing을 버튼 leading에 lessThanOrEqual로 묶어 겹침을 방지해주세요.
권장 diff 1: 플러스 버튼이 보이는 경우(추천 루틴 카드)
headerInfoStackView.snp.makeConstraints { make in make.top.equalToSuperview().offset(Layout.headerInfoStackViewTopSpacing) make.leading.equalToSuperview().offset(Layout.horizontalMargin) + make.trailing.lessThanOrEqualTo(plusButton.snp.leading).offset(-Layout.buttonTrailingSpacing) }권장 diff 2: 일반 루틴 카드(편집/삭제 버튼 노출) — 버튼 추가 후에 제약 추가
if routine is Routine { addSubview(editButton) addSubview(deleteButton) editButton.snp.makeConstraints { ... } deleteButton.snp.makeConstraints { ... } + + headerInfoStackView.snp.makeConstraints { make in + make.trailing.lessThanOrEqualTo(editButton.snp.leading).offset(-Layout.buttonTrailingSpacing) + } }Also applies to: 237-252
Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift (2)
203-263: updateRoutineCompletion 주석 처리로 인한 UI 무반응 문제 확인
- HomeViewModel의 Input
.updateRoutineCompletion(updatedRoutine:)호출부 확인
(Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift:21, 94–100)updateRoutineCompletionResultPublisher를 HomeView에서 구독하여 토글 결과 처리
(Projects/Presentation/Sources/Home/View/HomeView.swift:427–430)- 현 상태:
updateRoutineCompletion(updatedRoutine:)내부 로직이 전부 주석 처리되어 퍼블리셔가 이벤트를 전혀 발행하지 않음 → UI는 토글 후 응답 없음즉시 조치(임시 안전장치): v2 API 구현 전까지 실패 이벤트를 명시적으로 발행하도록 수정
--- a/Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift +++ b/Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift @@ -201,16 +201,7 @@ private func updateRoutineCompletion(updatedRoutine: Routine) { - /* - let performedDate = selectedDateSubject.value.convertToString(dateType: .yearMonthDate) - var routineCompletionEntities: [RoutineCompletionEntity] = [] - - // ...기존 로직 생략... - - Task { - do { - try await routineUseCase.updateRoutineCompletions(routines: routineCompletionEntities) - updateRoutineCompletionResultSubject.send(true) - fetchRoutines() - } catch { - updateRoutineCompletionResultSubject.send(false) - } - } - */ + // TODO: v2 완료 토글 API 사양 확정 전 임시 처리 + updateRoutineCompletionResultSubject.send(false) }대안: 기능 비노출 전략으로 Input/Output 자체를 제거하거나, 토글 버튼(UI) 단계에서 비활성화 처리하여 사용자가 호출할 수 없도록 막는 방안도 고려해주세요.
186-199: UI 갱신 보장 및 스레드 안전성 보완, 중복 네트워크 호출 개선 필요
fetchRoutines(startDate:endDate:)는 현재routines딕셔너리만 갱신하고,routinesSubject에 발행하지 않아
선택 날짜(selectDate)에서 즉시 루틴을 표시할 수 없습니다. 이로 인해 비동기 호출이 완료되기 전 빈 배열이 먼저 방출되어 UI가 공백/깜빡임 현상을 보일 수 있습니다.- Combine Subject 발행이 Main 스레드를 보장하지 않아, View 바인딩 시 스레드 충돌 가능성이 있습니다.
- 스크립트 확인 결과 HomeViewModel 내에서
fetchRoutines()가 아래 3곳에서 호출되어 불필요한 중복 네트워크 요청이 발생합니다.
- 초기 로드 시 (빈 캐시인 경우) →
private func fetchRoutines()(라인 141)- 날짜 선택 시 →
private func selectDate(date:)내부fetchRoutines()(라인 152)- 완료 상태 업데이트 후 →
updateRoutineCompletionResultSubject.send(true)직후fetchRoutines()(라인 257)권장 패치:
- MainActor에서 상태 갱신 및 발행
private func fetchRoutines(startDate: Date, endDate: Date) {
Task {
Task { @MainActor [weak self] in guard let self = self else { return } do { let entities = try await self.routineUseCase.fetchRoutines(startDate: startDate, endDate: endDate)
for (date, values) in entities {let routineEntities = values.routinesroutines[date] = routineEntities.compactMap({ $0.toRoutine() })}fetchRoutineResultSubject.send(true)
for (date, values) in entities {self.routines[date] = values.routines.map { $0.toRoutine() }}// 현재 선택일자 즉시 발행 → 초기 공백/깜빡임 방지let key = self.selectedDateSubject.value.convertToString(dateType: .yearMonthDate)self.routinesSubject.send(self.routines[key] ?? [])self.fetchRoutineResultSubject.send(true) } catch {
// TODO: 에러 처리
}self.fetchRoutineResultSubject.send(false) } }
ViewModel 전체에 @mainactor 적용
- final class HomeViewModel: ViewModel { + @MainActor + final class HomeViewModel: ViewModel {중복 호출 최소화
selectDate또는 완료 업데이트 후 호출 시, 이미 로드된 날짜 범위 내라면 네트워크 요청을 건너뛰도록 조건 추가- 예: 선택한 날짜가
oldestDate…latestDate사이일 때는fetchRoutines()호출하지 않기날짜 키 일관성 확인
convertToString(dateType: .yearMonthDate)의 타임존이 서버(예: UTC/KST)와 일치하는지 확인- 필요 시 유틸 내부에서 고정 타임존을 적용
♻️ Duplicate comments (4)
Projects/Presentation/Sources/RoutineList/View/RoutineEditAlertViewController.swift (1)
58-64: 이전 리뷰 이슈(‘당일부터 적용’ 불리언) 정상 반영됨applyTodayButton 동작이 true로 전달되도록 수정되어 의도와 일치합니다. 굿.
Projects/Presentation/Sources/Common/Component/RoutineCardView.swift (3)
76-85: 버튼 액션 핸들러 추가 반영 확인이전 리뷰에서 누락되었던 edit/delete 액션이 정상적으로 추가되었습니다. 델리게이트 호출도 타입 안전하게 처리되어 있습니다.
Also applies to: 87-97
121-139: SnapKit 제약 순서(추가 후 제약) 개선 반영arrangedSubview로 추가한 뒤 높이 제약을 거는 패턴으로 정리되어 이전 경고 요인을 해소했습니다. 일관성 좋아요.
Also applies to: 153-160, 169-173, 181-184
241-245: 오토레이아웃 버그: editButton.trailing 기준 앵커가 잘못되어 버튼 겹침 가능trailing을 deleteButton 자체에 맞추고 offset을 음수로 주면 의도와 달리 두 버튼이 겹칠 수 있습니다. deleteButton의 leading을 기준으로 간격을 두는 것이 맞습니다.
수정 diff:
editButton.snp.makeConstraints { make in make.top.equalToSuperview().offset(Layout.plusButtonTopSpacing) - make.trailing.equalTo(deleteButton).offset(-Layout.editButtonTrailingSpacing) + make.trailing.equalTo(deleteButton.snp.leading).offset(-Layout.editButtonTrailingSpacing) make.size.equalTo(Layout.plusButtonSize) }
🧹 Nitpick comments (6)
Projects/Presentation/Sources/RoutineList/View/RoutineEditAlertViewController.swift (2)
15-17: 라벨 고정 높이 제거로 동적 글꼴/다국어 대응성 확보mainLabel에 고정 높이(24pt)를 주면 Dynamic Type/다국어 길이 변화에서 잘림이 발생합니다. 스택뷰가 아니라 일반 뷰 계층이라도 라벨은 intrinsicContentSize를 쓰는 편이 안전합니다. height 제약 제거를 제안드립니다.
적용 예시 1: mainLabel 높이 제약 제거
mainLabel.snp.makeConstraints { make in make.top.equalToSuperview().offset(Layout.mainLabelTopSpacing) make.leading.equalToSuperview().offset(Layout.horizontalMargin) - make.height.equalTo(Layout.mainLabelHeight) }적용 예시 2: 더 나아가 사용되지 않을 상수 삭제
private enum Layout { static let horizontalMargin: CGFloat = 24 static let mainLabelTopSpacing: CGFloat = 26 - static let mainLabelHeight: CGFloat = 24 static let subLabelTopSpacing: CGFloat = 10추가로 BitnagilFont가 스케일 적용을 지원한다면 adjustsFontForContentSizeCategory = true 설정도 검토해주세요.
Also applies to: 90-95
50-57: 두 버튼 설정 로직 DRY + 접근성 보강 제안
- 동일한 UIButton.Configuration 생성/스타일링 로직이 중복됩니다. 작은 헬퍼로 공통화하면 가독성과 유지보수성이 좋아집니다.
- VoiceOver 사용자를 위해 accessibilityLabel/Hint 부여를 권장합니다. 시트 제목만으로는 맥락 파악이 늦을 수 있습니다.
예시(개념 코드):
private func makeFilledConfig(title: String, bg: UIColor, fg: UIColor = .white) -> UIButton.Configuration { var c = UIButton.Configuration.filled() c.baseBackgroundColor = bg c.baseForegroundColor = fg c.background.cornerRadius = 12 c.attributedTitle = AttributedString(title, attributes: .init([.font: BitnagilFont(style: .body2, weight: .medium).font])) return c } // 사용 applyTodayButton.configuration = makeFilledConfig(title: "당일부터 적용", bg: BitnagilColor.gray10) applyTodayButton.accessibilityLabel = "당일부터 적용" applyTodayButton.accessibilityHint = "변경된 루틴을 오늘부터 적용합니다" applyTomorrowButton.configuration = makeFilledConfig(title: "다음 날부터 적용", bg: BitnagilColor.gray10) applyTomorrowButton.accessibilityLabel = "다음 날부터 적용" applyTomorrowButton.accessibilityHint = "변경된 루틴을 내일부터 적용합니다"Also applies to: 66-73
Projects/Presentation/Sources/Common/Component/RoutineCardView.swift (2)
237-238: 불필요한 바인딩 제거값을 사용하지 않으므로
if let _ = routine as? Routine대신if routine is Routine이 더 간결합니다.- if let _ = routine as? Routine { + if routine is Routine {
126-128: 라벨 고정 높이 최소화로 가독성/접근성 향상StackView 내 라벨의 고정 높이(20pt)는 Dynamic Type/다국어 시 잘림을 유발할 수 있습니다. 가능하면 제거하고 intrinsic 크기에 맡기세요. 동일 패턴을 다른 라벨에도 적용 권장.
예시(diff — subRoutineLabel):
- subRoutineLabel.snp.makeConstraints { make in - make.height.equalTo(Layout.subLabelHeight) - } + // 높이 고정 제거: 스택뷰 + intrinsicContentSize 사용동일 제안 지점: 137-138, 159-160, 172-173, 182-184
Projects/Presentation/Sources/RoutineList/View/RoutineListViewController.swift (2)
89-93: EmptyView 가로 제약 보강 제안현재 centerX/centerY/height만 있어 작은 디바이스 또는 다국어에서 레이아웃이 흔들릴 수 있습니다. 안전 영역에 가로로 붙이는 제약을 추가해 주세요.
emptyView.snp.makeConstraints { make in make.centerX.equalTo(safeArea) make.centerY.equalTo(safeArea).offset(Layout.emptyViewCenterYSpacing) make.height.equalTo(Layout.emptyViewHeight) + make.horizontalEdges.equalTo(safeArea).inset(Layout.horizontalMargin) }
172-173: 미사용 델리게이트 콜백 처리플러스 버튼은 이 화면에서 의도적으로 무동작인 것으로 보입니다. 주석으로 의도를 명시하거나 최소한의 로깅을 추가해 후속 유지보수자가 혼동하지 않도록 해주세요.
예:
func routineCardView(_ sender: RoutineCardView, didTapPlusButton routine: RecommendedRoutine) { // RoutineList에서는 추천 루틴 추가 기능을 제공하지 않음 (의도적으로 no-op) }
📜 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 selected for processing (7)
Projects/Presentation/Sources/Common/Component/RoutineCardView.swift(8 hunks)Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift(7 hunks)Projects/Presentation/Sources/Onboarding/Model/OnboardingChoiceType.swift(1 hunks)Projects/Presentation/Sources/Onboarding/Model/OnboardingType.swift(0 hunks)Projects/Presentation/Sources/RoutineList/View/RoutineEditAlertViewController.swift(1 hunks)Projects/Presentation/Sources/RoutineList/View/RoutineListViewController.swift(1 hunks)Projects/Presentation/Sources/RoutineList/ViewModel/RoutineListViewModel.swift(1 hunks)
💤 Files with no reviewable changes (1)
- Projects/Presentation/Sources/Onboarding/Model/OnboardingType.swift
🚧 Files skipped from review as they are similar to previous changes (2)
- Projects/Presentation/Sources/Onboarding/Model/OnboardingChoiceType.swift
- Projects/Presentation/Sources/RoutineList/ViewModel/RoutineListViewModel.swift
🧰 Additional context used
🧠 Learnings (2)
📚 Learning: 2025-07-16T09:21:15.038Z
Learnt from: choijungp
PR: YAPP-Github/Bitnagil-iOS#19
File: Projects/Presentation/Sources/Onboarding/View/OnboardingRecommendedRoutineView.swift:57-59
Timestamp: 2025-07-16T09:21:15.038Z
Learning: OnboardingRecommendedRoutineView에서 viewWillAppear에 registerOnboarding 호출하는 것이 적절한 이유: 사용자가 이전 페이지에서 온보딩 선택지를 변경한 후 돌아올 때 새로운 선택지로 다시 등록해야 하기 때문. 홈 뷰에서는 이 뷰로 돌아올 수 없어서 중복 호출 문제가 발생하지 않음.
Applied to files:
Projects/Presentation/Sources/RoutineList/View/RoutineListViewController.swift
📚 Learning: 2025-08-17T13:30:29.342Z
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.342Z
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.
Applied to files:
Projects/Presentation/Sources/Common/Component/RoutineCardView.swift
🧬 Code graph analysis (4)
Projects/Presentation/Sources/RoutineList/View/RoutineListViewController.swift (8)
Projects/Presentation/Sources/RoutineList/ViewModel/RoutineListViewModel.swift (2)
action(47-65)fetchDailyRoutine(95-104)Projects/Presentation/Sources/RoutineList/View/RoutineEditAlertViewController.swift (3)
viewDidLoad(30-34)configureAttribute(41-81)configureLayout(83-115)Projects/Presentation/Sources/Common/Component/RoutineCardView.swift (4)
configureAttribute(64-189)configureAttribute(275-280)configureLayout(191-253)configureLayout(282-288)Projects/Presentation/Sources/RoutineList/View/RoutineDeleteViewController.swift (2)
configureAttribute(46-84)configureLayout(86-119)Projects/Presentation/Sources/RoutineList/View/RoutineDeleteAlertViewController.swift (2)
configureAttribute(56-105)configureLayout(107-143)Projects/Shared/Sources/DIContainer/DIContainer.swift (1)
resolve(18-25)Projects/Presentation/Sources/Common/Extension/UIViewController+.swift (1)
configureCustomNavigationBar(19-29)Projects/Presentation/Sources/Home/View/Component/WeekView.swift (1)
updateWeekDateViews(68-101)
Projects/Presentation/Sources/RoutineList/View/RoutineEditAlertViewController.swift (1)
Projects/Presentation/Sources/RoutineList/View/RoutineListViewController.swift (1)
goToRoutineCreationView(151-160)
Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift (4)
Projects/Presentation/Sources/RoutineList/ViewModel/RoutineListViewModel.swift (1)
fetchRoutines(67-86)Projects/Domain/Sources/UseCase/Routine/RoutineUseCase.swift (1)
fetchRoutines(23-29)Projects/DataSource/Sources/Repository/RoutineRepository.swift (1)
fetchRoutines(37-49)Projects/Presentation/Sources/Home/Model/Routine.swift (1)
toRoutine(26-39)
Projects/Presentation/Sources/Common/Component/RoutineCardView.swift (4)
Projects/Presentation/Sources/RoutineList/View/RoutineListViewController.swift (3)
routineCardView(172-172)routineCardView(174-212)routineCardView(214-264)Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineViewController.swift (3)
routineCardView(334-341)routineCardView(343-343)routineCardView(344-344)Projects/Presentation/Sources/Common/Extension/UIImage+.swift (1)
resizeAspectFit(20-35)Projects/Shared/Sources/Extension/Date+.swift (1)
convertToString(16-22)
⏰ Context from checks skipped due to timeout of 90000ms. You can increase the timeout in your CodeRabbit configuration to a maximum of 15 minutes (900000ms). (1)
- GitHub Check: build
🔇 Additional comments (2)
Projects/Presentation/Sources/RoutineList/View/RoutineListViewController.swift (1)
33-38: init에서 selectDate 액션 호출 시점 확인 요청BaseViewController의 바인딩(setup/bind)이 super.init 이후에 이뤄질 가능성이 높습니다. 현재는 super.init 전에 액션을 보내므로 초기 이벤트가 UI에 반영되지 않을 수 있습니다. 의도적으로 미리 상태를 세팅하는 것인지 확인 바랍니다.
대안:
- super.init(viewModel:) 호출 후에 selectDate를 보내거나,
- ViewModel이 subject의 최신값을 유지(replay)하도록 설계했는지 확인.
Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift (1)
20-20: Routine 타입 전환 전반(Layer 간 일관성) 좋습니다
MainRoutine→Routine로의 전환, 관련 퍼블리셔/Subject 제네릭 정리는 인터페이스 일관성과 가독성 측면에서 적절합니다.Also applies to: 31-31, 36-36, 42-43
| case loadEmotion | ||
| case moveWeek(week: Int) | ||
| case selectDate(date: Date) | ||
| case selectRoutineListDate |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
RoutineList 화면 이동 이벤트 설계: 별도 액션/퍼블리셔 유지 OK, 네이밍/흐름은 소폭 다듬으면 더 명확합니다
- 현재 설계는 "사용자 이벤트(리스트 화면 열기) → 현재 선택일자 스냅샷 발행" 흐름을 명확히 분리합니다. 괜찮은 선택입니다.
- 다만 Input 이름
selectRoutineListDate는 “날짜 선택”으로 읽혀 이벤트 의도가 모호합니다. 이벤트 의미를 드러내도록 액션명을openRoutineList등으로 바꾸는 것을 제안합니다. 퍼블리셔/서브젝트 명(routineListDatePublisher/Subject)은 유지해도 무방합니다.
적용 예시(이 파일 내 변경만, 최소 변경):
enum Input {
- case selectRoutineListDate
+ case openRoutineList
}
...
- case .selectRoutineListDate:
+ case .openRoutineList:
let selectedDate = selectedDateSubject.value
routineListDateSubject.send(selectedDate)추가 제안(선택):
- 이벤트를 유지하되 메인 스레드 보장을 위해 본 퍼블리시를 MainActor에서 발행하시거나, downstream에서
receive(on:)을 명시하세요. - 대안 설계: 별도 Subject 없이
selectedDatePublisher를 화면 전환 트리거 시점에 단발 구독해도 됩니다. 이벤트와 상태를 분리하는 현재 구조가 선호된다면 현 상태 유지 권장.
다음 스크립트로 이 이벤트/퍼블리셔 소비 지점을 확인해 누락이 없는지 점검해 주세요.
Also applies to: 29-29, 40-40, 66-66, 87-90
🏁 Script executed:
#!/bin/bash
# RoutineList 열기 액션/퍼블리셔 사용처 확인
rg -nP -C2 '\bselectRoutineListDate\b|\bopenRoutineList\b|\broutineListDatePublisher\b' --glob '!**/Pods/**'Length of output: 3506
Input 액션 네이밍 개선 및 관련 참조 업데이트 요청
현재 selectRoutineListDate라는 이름은 “날짜 선택”으로 오해될 여지가 있어, 액션 의도를 명확히 드러내기 위해 아래와 같이 변경을 제안합니다. 이 변경은 ViewModel 뿐만 아니라 이를 사용하는 View 쪽 호출부도 함께 수정해야 합니다.
• HomeViewModel.swift
- enum Input 케이스 이름을
selectRoutineListDate→openRoutineList로 변경 - switch 분기
case .selectRoutineListDate:→case .openRoutineList:로 수정
enum Input {
- case selectRoutineListDate
+ case openRoutineList
}
…
switch input {
- case .selectRoutineListDate:
+ case .openRoutineList:
let selectedDate = selectedDateSubject.value
routineListDateSubject.send(selectedDate)• HomeView.swift
- 버튼 액션에서 호출하는 input도
selectRoutineListDate→openRoutineList로 변경
- self?.viewModel.action(input: .selectRoutineListDate)
+ self?.viewModel.action(input: .openRoutineList)• 기타
routineListDatePublisher/routineListDateSubject명칭은 그대로 유지해도 무방합니다.- Main 스레드 보장은 이미
.receive(on: DispatchQueue.main)을 통해 처리되고 있으므로 추가 조치는 선택 사항입니다.
📝 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.
| case selectRoutineListDate | |
| // HomeViewModel.swift | |
| enum Input { | |
| case openRoutineList | |
| // … other cases … | |
| } | |
| … | |
| switch input { | |
| case .openRoutineList: | |
| let selectedDate = selectedDateSubject.value | |
| routineListDateSubject.send(selectedDate) | |
| // … other branches … | |
| } |
| case selectRoutineListDate | |
| // HomeView.swift | |
| // Before: | |
| - self?.viewModel.action(input: .selectRoutineListDate) | |
| // After: | |
| self?.viewModel.action(input: .openRoutineList) |
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift around line
18, rename the enum Input case from `selectRoutineListDate` to `openRoutineList`
and update any switch branches from `case .selectRoutineListDate:` to `case
.openRoutineList:`; then update
Projects/Presentation/Sources/Home/View/HomeView.swift (and any other call
sites) to call the new input `openRoutineList` instead of
`selectRoutineListDate`. Keep
`routineListDatePublisher`/`routineListDateSubject` names unchanged and no
additional main-thread handling is required because `.receive(on:
DispatchQueue.main)` already covers it.
| override func viewWillAppear(_ animated: Bool) { | ||
| super.viewWillAppear(animated) | ||
| view.subviews.first(where: { $0.tag == 999 })?.removeFromSuperview() | ||
| } |
There was a problem hiding this comment.
💡 Verification agent
🧩 Analysis chain
매직 넘버(tag == 999) 기반 뷰 제거는 위험 — dimmedView로 일원화
이 VC에서 dimmedView로 오버레이를 관리하고 있으므로, 출처 불명의 tag 999 뷰를 지우는 코드는 다른 기능과 충돌할 수 있습니다. 제거를 권장합니다.
간단 제거 diff:
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
- view.subviews.first(where: { $0.tag == 999 })?.removeFromSuperview()
}혹은 tag 사용이 필수라면 상수 정의와 주석(출처/생성 지점)을 추가해주세요.
다음 스크립트로 tag == 999 사용 위치와 dimmedView 참조 여부를 확인해 보겠습니다:
🏁 Script executed:
#!/bin/bash
# 1. 태그 999가 설정되거나 비교되는 위치 검색
rg -nP --type=swift '\btag\s*=\s*999\b' -C3
rg -nP --type=swift '\btag\s*==\s*999\b' -C3
# 2. RoutineListViewController 내에서 dimmedView 검색
rg -nP --type=swift 'dimmedView' -C3 Projects/Presentation/Sources/RoutineList/View/RoutineListViewController.swift
# 3. 전체 코드베이스에서 dimmedView 참조 검색
rg -nP --type=swift 'dimmedView' -C3 ProjectsLength of output: 30023
매직 넘버(tag == 999) 기반 뷰 제거 코드 삭제 요청
검색 결과, tag = 999 할당 위치는 전혀 없으며 오버레이 처리 로직은 dimmedView로 일원화되어 있습니다. 해당 매직 넘버 기반 뷰 제거 코드는 동작하지 않을 뿐더러 잠재적 충돌 위험이 있어 반드시 제거해야 합니다.
- 대상 파일:
Projects/Presentation/Sources/RoutineList/View/RoutineListViewController.swift - 위치:
override func viewWillAppear(_ animated: Bool)내부
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
- view.subviews.first(where: { $0.tag == 999 })?.removeFromSuperview()
}📝 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.
| override func viewWillAppear(_ animated: Bool) { | |
| super.viewWillAppear(animated) | |
| view.subviews.first(where: { $0.tag == 999 })?.removeFromSuperview() | |
| } | |
| override func viewWillAppear(_ animated: Bool) { | |
| super.viewWillAppear(animated) | |
| } |
🤖 Prompt for AI Agents
In
Projects/Presentation/Sources/RoutineList/View/RoutineListViewController.swift
around lines 49 to 52, remove the logic that searches for and removes a subview
by tag (tag == 999); this magic-number based removal is unused, unreliable and
should be deleted so the view lifecycle relies on the centralized dimmedView
overlay logic instead—simply delete the line `view.subviews.first(where: {
$0.tag == 999 })?.removeFromSuperview()` from viewWillAppear.
| let newDimmedView = UIView() | ||
| newDimmedView.backgroundColor = .black.withAlphaComponent(0.7) | ||
| newDimmedView.frame = view.bounds | ||
| view.addSubview(newDimmedView) | ||
| dimmedView = newDimmedView | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
오버레이 뷰 Auto Layout 미적용 — 회전/사이즈 클래스 변화 시 틀어질 수 있음
frame 고정 대신 제약으로 전체 화면을 채우도록 설정하세요.
수정 diff(편집 시트 오버레이):
- let newDimmedView = UIView()
- newDimmedView.backgroundColor = .black.withAlphaComponent(0.7)
- newDimmedView.frame = view.bounds
- view.addSubview(newDimmedView)
+ let newDimmedView = UIView()
+ newDimmedView.backgroundColor = .black.withAlphaComponent(0.7)
+ newDimmedView.translatesAutoresizingMaskIntoConstraints = false
+ view.addSubview(newDimmedView)
+ NSLayoutConstraint.activate([
+ newDimmedView.topAnchor.constraint(equalTo: view.topAnchor),
+ newDimmedView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
+ newDimmedView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+ newDimmedView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+ ])
dimmedView = newDimmedView동일 패턴을 삭제 시트 오버레이(219-224)에도 적용 바랍니다.
📝 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.
| let newDimmedView = UIView() | |
| newDimmedView.backgroundColor = .black.withAlphaComponent(0.7) | |
| newDimmedView.frame = view.bounds | |
| view.addSubview(newDimmedView) | |
| dimmedView = newDimmedView | |
| let newDimmedView = UIView() | |
| newDimmedView.backgroundColor = .black.withAlphaComponent(0.7) | |
| newDimmedView.translatesAutoresizingMaskIntoConstraints = false | |
| view.addSubview(newDimmedView) | |
| NSLayoutConstraint.activate([ | |
| newDimmedView.topAnchor.constraint(equalTo: view.topAnchor), | |
| newDimmedView.bottomAnchor.constraint(equalTo: view.bottomAnchor), | |
| newDimmedView.leadingAnchor.constraint(equalTo: view.leadingAnchor), | |
| newDimmedView.trailingAnchor.constraint(equalTo: view.trailingAnchor), | |
| ]) | |
| dimmedView = newDimmedView |
🤖 Prompt for AI Agents
In
Projects/Presentation/Sources/RoutineList/View/RoutineListViewController.swift
around lines 184-189, the overlay/dimmed view is being added with a fixed frame
which will break on rotation or size-class changes; replace the frame-based
setup by disabling translatesAutoresizingMaskIntoConstraints, adding the dimmed
view to the view hierarchy, and constrain its edges to the superview (top,
bottom, leading, trailing) so it always fills the screen; apply the same change
to the delete sheet overlay at lines ~219-224 so both overlays use Auto Layout
instead of fixed frames.
| let newDimmedView = UIView() | ||
| newDimmedView.backgroundColor = .black.withAlphaComponent(0.7) | ||
| newDimmedView.frame = view.bounds | ||
| view.addSubview(newDimmedView) | ||
| dimmedView = newDimmedView | ||
|
|
There was a problem hiding this comment.
🛠️ Refactor suggestion
(중복) 삭제 플로우 오버레이도 Auto Layout 적용 필요
위와 동일한 이유로 제약 기반으로 전환하세요.
수정 diff(삭제 시트 오버레이):
- let newDimmedView = UIView()
- newDimmedView.backgroundColor = .black.withAlphaComponent(0.7)
- newDimmedView.frame = view.bounds
- view.addSubview(newDimmedView)
+ let newDimmedView = UIView()
+ newDimmedView.backgroundColor = .black.withAlphaComponent(0.7)
+ newDimmedView.translatesAutoresizingMaskIntoConstraints = false
+ view.addSubview(newDimmedView)
+ NSLayoutConstraint.activate([
+ newDimmedView.topAnchor.constraint(equalTo: view.topAnchor),
+ newDimmedView.bottomAnchor.constraint(equalTo: view.bottomAnchor),
+ newDimmedView.leadingAnchor.constraint(equalTo: view.leadingAnchor),
+ newDimmedView.trailingAnchor.constraint(equalTo: view.trailingAnchor),
+ ])
dimmedView = newDimmedView📝 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.
| let newDimmedView = UIView() | |
| newDimmedView.backgroundColor = .black.withAlphaComponent(0.7) | |
| newDimmedView.frame = view.bounds | |
| view.addSubview(newDimmedView) | |
| dimmedView = newDimmedView | |
| let newDimmedView = UIView() | |
| newDimmedView.backgroundColor = .black.withAlphaComponent(0.7) | |
| newDimmedView.translatesAutoresizingMaskIntoConstraints = false | |
| view.addSubview(newDimmedView) | |
| NSLayoutConstraint.activate([ | |
| newDimmedView.topAnchor.constraint(equalTo: view.topAnchor), | |
| newDimmedView.bottomAnchor.constraint(equalTo: view.bottomAnchor), | |
| newDimmedView.leadingAnchor.constraint(equalTo: view.leadingAnchor), | |
| newDimmedView.trailingAnchor.constraint(equalTo: view.trailingAnchor), | |
| ]) | |
| dimmedView = newDimmedView |
🤖 Prompt for AI Agents
In
Projects/Presentation/Sources/RoutineList/View/RoutineListViewController.swift
around lines 219-224, the delete-flow dimmed overlay is created using a fixed
frame which breaks on rotation and with Auto Layout; replace the frame-based
approach by setting newDimmedView.translatesAutoresizingMaskIntoConstraints =
false, add it to the view, and pin its leading/trailing/top/bottom to the
superview (or safeAreaLayoutGuide if appropriate) with constraints so it always
matches the view bounds; keep assigning dimmedView = newDimmedView and remove
the frame assignment.
🌁 Background
루틴 리스트 화면을 구현했어요 ~~
그리고 서버 연동이 필요해서 대망의 루틴 조회를 v2와 연동했어요 ~~
📱 Screenshot
1. 루틴 리스트 화면
2. 루틴 리스트 Empty 화면
3 - (1). 루틴 삭제 화면
3 - (2). 루틴 삭제 Alert 화면
4. 루틴 수정 날짜 적용 화면
👩💻 Contents
📝 Review Note
1. 루틴 조회 서버 v2 연동
루틴 리스트를 위해서는 루틴 조회 api를 v2로 연동해야 했습니다 !!
그러므로 인해
RoutineDTO,RoutineEntity의 변화가 많이 있었습니다. 또한 View에서 사용하는 모델에서도 큰 변화가 있었습니다 ㅜㅜ일단
RoutineDTO,RoutineEntity는 서버에서 보내주는대로 변경을 하였고,View에서 사용하는 모델은 기존
MainRoutine,SubRoutine,Routine(<- protocol이었음) 이 있었습니다 !!하지만 SubRoutine에 대한 타입을 따로 갖고 있을 필요가 없어서 .. 일단 기존 녀석들을 다 제거하고,
Routine 구조체를 새로 생성했습니다 !!
또한
RoutineCardView에서RecommendedRoutine인지Routine인지에 따라 UI가 조금 수정이 되어야 해서RoutineProtocol이라는 프로토콜을 만들어서RecommendedRoutine와Routine가 채택하도록 하였습니다 !!!(네이밍 좀 구린가요 ㅠㅠ ?)
RoutineCreationViewModel 쪽에서도 루틴 조회할 때 RoutineEntity를 사용했었떠라고요 !!
그래서 진짜 조금 수정했는데 .. 서브 루틴이 등록이 안되는 느낌 ? ㅠㅠ 이여요 !!!
제 잘못인지 원래 안되었는지 모르겠어요 ㅠㅠ
왜냐면 .. 아예 최초 루틴 등록에서도 서브 루틴이 등록 안되는 느낌이라 .. 한번 요 부분만 확인 부탁드립니다 !!!
2. RoutineListViewController의 selectedDate
루틴 리스트 화면에서 WeekView를 사용하잖어요 ..
그럼 HomeView에서 selectedDate에 대한 값을 받아서 RoutineListViewController에서 사용해야 하는데,
기존 HomeViewModel의
selectedDatePublisher는 날짜를 선택한 경우를 뜻한거라 루틴 더보기 버튼을 눌렀을 때의 날짜를 방출해주는 publisher를 또 만들어줘야 한다고 생각햇어요 ㅠㅠ !! View에서 subject 값에 접근하지 못하니까 !!그래서 routineListDatePublisher를 하나 더 만들어줬습니다 !!! .......
좋은 구조인지는 모루겟어요
3. RoutineDeleteAlertViewController
루틴 삭제의 경우 흐름이 2가지가 있는데요 !!
일케 2가지 입니다 !!!
만약 해당 루틴이 반복 설정이 되어있다면 1번 흐름, 당일 루틴으로 등록되어 있다면 2번의 흐름인데요 !!!
그래서 RoutineDeleteAlertViewController의 dismiss에서 만약 이전 뷰가 있다면 .. 그 뷰까지 dismiss하도록 했는데 ..
그 동작이 약간 어색하고, dimmedView 터치를 활용해서 dismiss가 되는 경우 적용이 되지 않어요 ㅠㅠ
일단 급해서 알고만 있고.. 추후 고쳐볼게요 ㅠㅠ
📣 Related Issue
Summary by CodeRabbit
New Features
Refactor
Chores