Skip to content

[Feat-T3-159] 루틴 리스트 화면 구현 및 서버 v2 연동#54

Merged
choijungp merged 6 commits intodevelopfrom
feat/routine-list-view
Aug 21, 2025
Merged

[Feat-T3-159] 루틴 리스트 화면 구현 및 서버 v2 연동#54
choijungp merged 6 commits intodevelopfrom
feat/routine-list-view

Conversation

@choijungp
Copy link
Copy Markdown
Contributor

@choijungp choijungp commented Aug 20, 2025

🌁 Background

루틴 리스트 화면을 구현했어요 ~~
그리고 서버 연동이 필요해서 대망의 루틴 조회를 v2와 연동했어요 ~~

📱 Screenshot

1. 루틴 리스트 화면

iPhone SE3 iPhone 13 mini iPhone 16 Pro
Simulator Screenshot - iPhone SE (3rd generation) - 2025-08-20 at 17 24 16 Simulator Screenshot - iPhone 13 mini - 2025-08-20 at 17 27 48 Simulator Screenshot - iPhone 16 Pro - 2025-08-20 at 17 18 47

2. 루틴 리스트 Empty 화면

iPhone SE3 iPhone 13 mini iPhone 16 Pro
Simulator Screenshot - iPhone SE (3rd generation) - 2025-08-20 at 17 24 22 Simulator Screenshot - iPhone 13 mini - 2025-08-20 at 17 27 52 Simulator Screenshot - iPhone 16 Pro - 2025-08-20 at 17 18 51

3 - (1). 루틴 삭제 화면

iPhone SE3 iPhone 13 mini iPhone 16 Pro
Simulator Screenshot - iPhone SE (3rd generation) - 2025-08-21 at 17 24 04 Simulator Screenshot - iPhone 13 mini - 2025-08-21 at 17 25 54 Simulator Screenshot - iPhone 16 Pro - 2025-08-21 at 17 29 28

3 - (2). 루틴 삭제 Alert 화면

iPhone SE3 iPhone 13 mini iPhone 16 Pro
Simulator Screenshot - iPhone SE (3rd generation) - 2025-08-21 at 17 24 07 Simulator Screenshot - iPhone 13 mini - 2025-08-21 at 17 25 43 Simulator Screenshot - iPhone 16 Pro - 2025-08-21 at 17 29 30

4. 루틴 수정 날짜 적용 화면

iPhone SE3 iPhone 13 mini iPhone 16 Pro
Simulator Screenshot - iPhone SE (3rd generation) - 2025-08-21 at 17 24 11 Simulator Screenshot - iPhone 13 mini - 2025-08-21 at 17 25 50 Simulator Screenshot - iPhone 16 Pro - 2025-08-21 at 17 29 25

👩‍💻 Contents

  • RoutineListViewController 구현
  • RoutineCardView 확장
  • MainRoutine, SubRoutine, Routine(protocol) 삭제
  • Routine, RoutineProtocol 구현
  • fetchRoutines 서버 v2 연동

📝 Review Note

1. 루틴 조회 서버 v2 연동

루틴 리스트를 위해서는 루틴 조회 api를 v2로 연동해야 했습니다 !!
그러므로 인해 RoutineDTO, RoutineEntity의 변화가 많이 있었습니다. 또한 View에서 사용하는 모델에서도 큰 변화가 있었습니다 ㅜㅜ

일단 RoutineDTO, RoutineEntity는 서버에서 보내주는대로 변경을 하였고,
View에서 사용하는 모델은 기존 MainRoutine, SubRoutine, Routine(<- protocol이었음) 이 있었습니다 !!

하지만 SubRoutine에 대한 타입을 따로 갖고 있을 필요가 없어서 .. 일단 기존 녀석들을 다 제거하고,
Routine 구조체를 새로 생성했습니다 !!

또한 RoutineCardView에서 RecommendedRoutine인지 Routine인지에 따라 UI가 조금 수정이 되어야 해서
RoutineProtocol이라는 프로토콜을 만들어서 RecommendedRoutineRoutine가 채택하도록 하였습니다 !!!
(네이밍 좀 구린가요 ㅠㅠ ?)

RoutineCreationViewModel 쪽에서도 루틴 조회할 때 RoutineEntity를 사용했었떠라고요 !!
그래서 진짜 조금 수정했는데 .. 서브 루틴이 등록이 안되는 느낌 ? ㅠㅠ 이여요 !!!

제 잘못인지 원래 안되었는지 모르겠어요 ㅠㅠ
왜냐면 .. 아예 최초 루틴 등록에서도 서브 루틴이 등록 안되는 느낌이라 .. 한번 요 부분만 확인 부탁드립니다 !!!

2. RoutineListViewController의 selectedDate

루틴 리스트 화면에서 WeekView를 사용하잖어요 ..
그럼 HomeView에서 selectedDate에 대한 값을 받아서 RoutineListViewController에서 사용해야 하는데,

기존 HomeViewModel의 selectedDatePublisher는 날짜를 선택한 경우를 뜻한거라 루틴 더보기 버튼을 눌렀을 때의 날짜를 방출해주는 publisher를 또 만들어줘야 한다고 생각햇어요 ㅠㅠ !! View에서 subject 값에 접근하지 못하니까 !!

그래서 routineListDatePublisher를 하나 더 만들어줬습니다 !!! .......
좋은 구조인지는 모루겟어요

3. RoutineDeleteAlertViewController

루틴 삭제의 경우 흐름이 2가지가 있는데요 !!

  • RoutineListViewController > RoutineDeleteViewController > RoutineDeleteAlertViewController
  • RoutineListViewController > RoutineDeleteAlertViewController

일케 2가지 입니다 !!!

만약 해당 루틴이 반복 설정이 되어있다면 1번 흐름, 당일 루틴으로 등록되어 있다면 2번의 흐름인데요 !!!

그래서 RoutineDeleteAlertViewController의 dismiss에서 만약 이전 뷰가 있다면 .. 그 뷰까지 dismiss하도록 했는데 ..
그 동작이 약간 어색하고, dimmedView 터치를 활용해서 dismiss가 되는 경우 적용이 되지 않어요 ㅠㅠ

일단 급해서 알고만 있고.. 추후 고쳐볼게요 ㅠㅠ

📣 Related Issue

  • close #T3-159

Summary by CodeRabbit

  • New Features

    • 루틴 목록 화면 추가(주간 날짜 선택, 일자별 루틴 조회, 빈 상태에서 생성 이동)
    • 루틴 편집/삭제 플로우 추가(시트/확인 알럿: 오늘만/모든 날짜)
    • RoutineList 화면 및 관련 뷰모델/알럿/삭제 UI 추가
  • Refactor

    • 단일 Routine 모델 통합 및 카드·상세 정보 표시 변경
    • 홈/사용사례/레포지토리 인터페이스와 반환 타입(일자별 allCompleted 포함) 변경
    • 카드뷰에 편집·삭제 버튼 및 새로운 델리게이트 추가
  • Chores

    • 아이콘/에셋 정리 및 교체(닫기 아이콘 등), 날짜 포맷 yy.MM.dd 추가

@choijungp choijungp requested a review from taipaise August 20, 2025 08:38
@choijungp choijungp self-assigned this Aug 20, 2025
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Aug 20, 2025

Walkthrough

루틴 관련 데이터 모델과 API/프레젠테이션 흐름을 전면 개편했습니다. DTO와 Entity를 재구성하고 Repository/UseCase/Protocol 시그니처를 변경했으며, MainRoutine/SubRoutine과 상세 화면을 제거하고 Routine/RoutineList 흐름(VC/VM/삭제·편집 시트)을 새로 추가했습니다. 아이콘·애셋 및 온보딩 텍스트 키를 정리했습니다.

Changes

Cohort / File(s) Change Summary
Auth DTO rename
Projects/DataSource/Sources/DTO/LoginResponseDTO.swift
파일명 주석 갱신만(기능 동일).
Routine DTO 신규/교체
Projects/DataSource/Sources/DTO/RoutineDTO.swift, .../RoutineResponseDTO.swift
새로운 RoutineDictionaryDTO/RoutineDateDTO/RoutineDTO 추가 및 toRoutineEntity 매핑 추가. 기존 RoutineResponseDTO 계열 파일/매핑 삭제.
Endpoint 스펙 변경
Projects/DataSource/Sources/Endpoint/RoutineEndpoint.swift
deleteDailyRoutine 시그니처를 routineId: String으로 변경, 경로를 /day/{routineId}로 수정, 요청 바디 제거, 일부 baseURL 분기(v1/v2) 추가.
Repository 적응
Projects/DataSource/Sources/Repository/RoutineRepository.swift
응답 디코딩을 RoutineDTO 계열로 변경. fetchRoutines 반환 타입을 날짜→(routines, allCompleted) 튜플로 변경. deleteDailyRoutine 인자를 routineId: String으로 변경.
Domain Entity 리디자인
Projects/Domain/Sources/Entity/RoutineEntity.swift
routineId 필수화, repeatDay[String]으로 변경, 레거시 필드 제거 및 routineCompleteYn, subRoutineNames, subRoutineCompleteYn, recommendedRoutineType, routineDeletedYn, routineStartDate, routineEndDate 등 신규 필드 추가. 이니셜라이저 갱신.
Domain Protocol 업데이트
Projects/Domain/Sources/Protocol/...
Repository/UseCase 프로토콜들의 fetchRoutines 반환형을 날짜→(routines, allCompleted)로 변경. deleteDailyRoutine 파라미터를 routineId: String으로 변경.
UseCase 적응
Projects/Domain/Sources/UseCase/Routine/RoutineUseCase.swift
위 프로토콜 변경에 맞춰 시그니처/반환 타입 업데이트 및 호출 경로 수정.
삭제 엔티티 제거
Projects/Domain/Sources/Entity/DeleteRoutineEntity.swift
DeleteRoutineEntity, DeleteSubRoutineEntity 파일 삭제.
공유 확장
Projects/Shared/Sources/Extension/Date+.swift
DateType에 yearMonthDateShort 추가(포맷 "yy.MM.dd").
프레젠테이션 모델 통합/정리
Projects/Presentation/Sources/Home/Model/Routine.swift, .../MainRoutine.swift, .../SubRoutine.swift
MainRoutine/SubRoutine 제거. 새로운 Routine(RoutineProtocol 채택) 추가 및 RoutineEntityRoutine 매핑 추가.
Home 화면 적응
.../Home/View/Component/RoutineView.swift, .../Home/View/HomeView.swift, .../Home/ViewModel/HomeViewModel.swift, .../Home/View/RoutineDetailView.swift
RoutineView가 Routine 모델 사용 및 delegate 제거. HomeView에서 RoutineList 진입 퍼블리셔 추가 및 RoutineDetailView 삭제. HomeViewModel은 Routine으로 출력 타입 전환 및 루틴 관련 입력/출력 갱신(예: selectRoutineListDate 추가).
RoutineList 신설
Projects/Presentation/Sources/RoutineList/**
RoutineListViewModel(입력/출력/캐시/삭제 플로우) 추가. RoutineListViewController, RoutineDeleteViewController, RoutineDeleteAlertViewController, RoutineEditAlertViewController 등 UI/시트 추가 및 날짜 선택·렌더링·삭제 흐름 구현.
DI 등록
Projects/Presentation/Sources/Common/PresentationDependencyAssembler.swift
RoutineListViewModel DI 등록 추가(등록 실패 시 fatalError 메시지 포함).
RoutineCardView 확장
Projects/Presentation/Sources/Common/Component/RoutineCardView.swift
RoutineProtocol 도입, 편집/삭제 델리게이트 추가(루틴 전달), 상세 정보 섹션 및 레이아웃 재구성, 버튼 동작 조정.
Design System 아이콘 정리
Projects/Presentation/Sources/Common/DesignSystem/BitnagilIcon.swift
ellipsisIcon 및 일부 디테일 아이콘 제거. Routine List용 editIcon, trashIcon, closeIcon 추가. withRenderingMode(.alwaysOriginal) 제거.
애셋 변경
Projects/Presentation/Resources/Images.xcassets/...
RoutineList 카탈로그 메타데이터 추가. ellipsis_icon, routine_icon, subRoutine_icon 이미지셋 메타 삭제. close_icon 파일명으로 교체.
RoutineCreation 적응
Projects/Presentation/Sources/RoutineCreation/ViewModel/RoutineCreationViewModel.swift
서브루틴/반복 일자 매핑을 신규 Routine 필드형식(subRoutineNames, repeatDay 문자열 등)에 맞춰 변경.
Onboarding / RecommendedRoutine 정리
Projects/Presentation/Sources/.../Onboarding
OnboardingChoiceProtocolmainTitletitle로 명칭 변경. RecommendedRoutineRoutineProtocol 채택, mainTitletitle로 필드명 변경 및 routineType을 옵셔널로 변경. 관련 버튼 바인딩 갱신.
RecommendedRoutine VC 델리게이트 시그니처 적응
Projects/Presentation/Sources/RecommendedRoutine/View/RecommendedRoutineViewController.swift
RoutineCardViewDelegate의 편집/삭제 콜백(빈 구현) 추가.

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
Loading
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
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~70 minutes

Possibly related PRs

Suggested reviewers

  • taipaise

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 Docstrings
🧪 Generate unit tests
  • Create PR with unit tests
  • Post copyable unit tests in a comment
  • Commit unit tests in branch feat/routine-list-view

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.

❤️ Share
🪧 Tips

Chat

There are 3 ways to chat with CodeRabbit:

  • Review comments: Directly reply to a review comment made by CodeRabbit. Example:
    • I pushed a fix in commit <commit_id>, please review it.
    • Open a follow-up GitHub issue for this discussion.
  • Files and specific lines of code (under the "Files changed" tab): Tag @coderabbitai in a new review comment at the desired location with your query.
  • PR comments: Tag @coderabbitai in a new PR comment to ask questions about the PR branch. For the best results, please provide a very specific query, as very limited context is provided in this mode. Examples:
    • @coderabbitai gather interesting stats about this repository and render them as a table. Additionally, render a pie chart showing the language distribution in the codebase.
    • @coderabbitai read the files in the src/scheduler package and generate a class diagram using mermaid and a README in the markdown format.

Support

Need help? Create a ticket on our support page for assistance with any issues or questions.

CodeRabbit Commands (Invoked using PR/Issue comments)

Type @coderabbitai help to get the list of available commands.

Other keywords and placeholders

  • Add @coderabbitai ignore anywhere in the PR description to prevent this PR from being reviewed.
  • Add @coderabbitai summary or @coderabbitai 요약 to generate the high-level summary at a specific location in the PR description.
  • Add @coderabbitai anywhere in the PR title to generate the title automatically.

Status, Documentation and Community

  • Visit our Status Page to check the current availability of CodeRabbit.
  • Visit our Documentation for detailed information on how to use CodeRabbit.
  • Join our Discord Community to get help, request features, and share feedback.
  • Follow us on X/Twitter for updates and announcements.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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:)))

검증·리팩토링 제안
• 서버 v2 repeatDay 예시 값(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, trashIconroutineListEditIcon, 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, routineEndDateString이면 날짜 계산/정렬에서 비용이 큽니다. 도메인에서는 Date로 보유하고, DTO 매핑 레이어에서만 문자열 변환을 권장합니다.

해당 변경은 광범위합니다. 이후 뷰/유스케이스/매핑 레이어 영향도를 고려해 단계적 적용을 추천합니다. 필요 시 리팩터링 플랜/PR 분리도 도와드릴 수 있습니다.

Also applies to: 21-33, 36-44


33-45: 서브루틴 이름/완료여부 배열 길이 불일치 방지 가드 추가 제안

subRoutineNamessubRoutineCompleteYn 길이가 불일치하면 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회 외출" → "주 23회 외출" (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.didTapRegisterRoutineButtonself를 강하게 캡처합니다. 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.

📥 Commits

Reviewing files that changed from the base of the PR and between b02122b and 84818be.

⛔ Files ignored due to path filters (24)
  • Projects/Presentation/Resources/Images.xcassets/RoutineList/edit_icon.imageset/edit_icon.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/RoutineList/edit_icon.imageset/edit_icon@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/RoutineList/edit_icon.imageset/edit_icon@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/RoutineList/trash_icon.imageset/trash_icon.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/RoutineList/trash_icon.imageset/trash_icon@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/RoutineList/trash_icon.imageset/trash_icon@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/edit_icon.imageset/edit_icon.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/edit_icon.imageset/edit_icon@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/edit_icon.imageset/edit_icon@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/ellipsis_icon.imageset/ellipsis_icon.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/ellipsis_icon.imageset/ellipsis_icon@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/ellipsis_icon.imageset/ellipsis_icon@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/repeat_icon.imageset/repeat_icon.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/repeat_icon.imageset/repeat_icon@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/repeat_icon.imageset/repeat_icon@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/routine_icon.imageset/routine_icon.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/routine_icon.imageset/routine_icon@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/routine_icon.imageset/routine_icon@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/subRoutine_icon.imageset/subRoutine_icon.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/subRoutine_icon.imageset/subRoutine_icon@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/subRoutine_icon.imageset/subRoutine_icon@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/trash_icon.imageset/trash_icon.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/trash_icon.imageset/trash_icon@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/trash_icon.imageset/trash_icon@3x.png is 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.swift
  • Projects/Presentation/Sources/RoutineCreation/ViewModel/RoutineCreationViewModel.swift
  • Projects/Presentation/Sources/RoutineList/ViewModel/RoutineListViewModel.swift
  • Projects/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)

mainTitletitle 변경에 맞춘 바인딩이 적절합니다. 다른 사용처에서도 동일한 리네이밍이 누락되지 않았는지만 한 번 더 확인 부탁드립니다.

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.subroutinessubRoutineNames: 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: 주석 처리된 코드 블록

deleteDailyRoutineupdateRoutineCompletion 메서드의 구현이 주석 처리되어 있습니다. 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: 주의 시작일 계산 로직

월요일을 주의 시작으로 계산하는 로직이 올바르게 구현되었습니다.

Comment on lines +115 to +119
subRoutineTitleLabel.textColor = BitnagilColor.gray40
subRoutineTitleLabel.snp.makeConstraints { make in
make.height.equalTo(Layout.subLabelHeight)
}
subRoutineStackView.addArrangedSubview(subRoutineTitleLabel)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Comment on lines +27 to 35
private var routine: Routine {
didSet {
updateRoutineState()
}
}
weak var delegate: RoutineViewDelegate?
init(routine: MainRoutine) {

init(routine: Routine) {
self.routine = routine
super.init(frame: .zero)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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=swift

Length of output: 52614


RoutineView.swift: 루틴 완료 토글 이벤트 콜백으로 상위에 전달하세요

현재 mainRoutineCheckButton 액션에서 routine.isDone 토글 후 View 내부 상태만 갱신되므로, 변경된 상태가 ViewModel/UseCase로 전달되지 않습니다. 아래와 같이 콜백(closure) 프로퍼티를 추가하고, UIAction 내에서 호출해 상위 계층에서 처리하도록 구현해주세요.

  • 파일: Projects/Presentation/Sources/Home/View/Component/RoutineView.swift
  • 추가: 클로저 프로퍼티 선언
  • 수정: mainRoutineCheckButton UIAction 클로저에 콜백 호출 삽입
  • 사용처: 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.

Suggested change
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.

Comment on lines +438 to +450
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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Suggested change
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.

Comment on lines +9 to 11
var title: String { get }
var subTitle: String? { get }
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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: Stringtitle: String
    • 내부 할당: self.title = mainTitleself.title = title
    • 팩토리 호출: mainTitle: titletitle: title
  • Projects/Presentation/Sources/Onboarding/Model/OnboardingType.swift:21
    기존) var mainTitle: String { … } → 변경) var title: String { … }

Comment on lines +16 to 17
let routineType: RoutineCategoryType?
let routineLevel: RoutineLevelType
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

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.

Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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 throws
Projects/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 = .fillEqually

Also 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.gray40
Projects/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.

📥 Commits

Reviewing files that changed from the base of the PR and between 84818be and cd7d032.

⛔ Files ignored due to path filters (6)
  • Projects/Presentation/Resources/Images.xcassets/Graphic/default_emotion_graphic.imageset/default_emotion_graphic.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/default_emotion_graphic.imageset/default_emotion_graphic@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/Graphic/default_emotion_graphic.imageset/default_emotion_graphic@3x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/RoutineList/close_icon.imageset/close_icon.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/RoutineList/close_icon.imageset/close_icon@2x.png is excluded by !**/*.png
  • Projects/Presentation/Resources/Images.xcassets/RoutineList/close_icon.imageset/close_icon@3x.png is 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: 단건 조회 디코딩 타입 전환 적절

RoutineDTORoutineEntity 매핑으로 단순화되었습니다. 네트워크 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?()
 }

이렇게 하면

  1. dismiss 흐름은 dismissButton·cancelButton 액션, 바인딩된 isDeleteRoutine sink 내부의 dismissToRootView() 에서만 수행
  2. viewDidDisappear는 단순히 onDismiss 콜백만 처리
  3. 중복 호출로 인한 예기치 않은 동작 방지

[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() 호출을 통해 화면이 정상적으로 갱신될 것으로 판단됩니다.

Comment on lines +14 to 16
case deleteDailyRoutine(routineId: String)
case updateRoutineCompletion(routines: RoutineCompletionListDTO)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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' -C2

Length 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)

    • 관련 변수명(deleteRoutinEntitydeleteRoutineEntity) 오타도 함께 정정 필요합니다.

  • 레거시 엔티티/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.

Comment on lines +19 to 20
func deleteDailyRoutine(routineId: String) async throws

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

삭제 유스케이스 시그니처 단순화 OK — 호출처 정리 여부 확인

deleteDailyRoutine(routineId:)로 통일된 점은 👍. HomeViewModel의 옛 구현(주석 처리된 DeleteRoutineEntity 기반) 정리는 후속 PR에서라도 완료해 주세요.

다음 스크립트로 남아있는 구 호출 패턴을 점검할 수 있습니다.


🏁 Script executed:

#!/bin/bash
rg -nP 'deleteDailyRoutine\s*\(\s*routine:' -C2
rg -n 'DeleteRoutineEntity' -C2

Length 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)
  • 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.

Comment on lines +243 to +247
editButton.snp.makeConstraints { make in
make.top.equalToSuperview().offset(Layout.plusButtonTopSpacing)
make.trailing.equalTo(deleteButton).inset(Layout.editButtonTrailingSpacing)
make.size.equalTo(Layout.plusButtonSize)
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue

오토레이아웃 버그: 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.

Suggested change
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.

Comment on lines +174 to +181
func routineCardView(_ sender: RoutineCardView, didTapEditButton routine: Routine) {
viewModel.action(input: .seleteRoutine(routine: routine))

guard !routine.repeatDay.isEmpty else {
goToRoutineCreationView(routineId: routine.id)
return
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 Verification agent

🧩 Analysis chain

오타 유지: .seleteRoutine

Input 케이스명이 selectRoutine이 아닌 seleteRoutine으로 사용되고 있습니다. 전역적으로 사용 중이라면 이번 PR 범위를 넘을 수 있으나, 추후 리팩토링 대상으로 남겨두는 것을 권장드립니다.

다음으로 호출/정의 위치를 한 번에 파악해 네이밍 교정 범위를 추산할 수 있습니다:

Also applies to: 214-225


🏁 Script executed:

#!/bin/bash
rg -nP '\bseleteRoutine\b' -C2

Length 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):
viewModel.action(input: .seleteRoutine(routine: routine))
viewModel.action(input: .selectRoutine(routine: routine))
- didTapDeleteButton (라인 214
225) 역시 동일 변경

전역 사용처는 아래 스크립트로 확인했습니다:

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.

Comment thread Projects/Presentation/Sources/RoutineList/ViewModel/RoutineListViewModel.swift Outdated
Comment thread Projects/Presentation/Sources/RoutineList/ViewModel/RoutineListViewModel.swift Outdated
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)

권장 패치:

  1. 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.routines
    
  •               routines[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)
          }
      }
    
    }
  1. ViewModel 전체에 @mainactor 적용

    - final class HomeViewModel: ViewModel {
    + @MainActor
    + final class HomeViewModel: ViewModel {
  2. 중복 호출 최소화

    • selectDate 또는 완료 업데이트 후 호출 시, 이미 로드된 날짜 범위 내라면 네트워크 요청을 건너뛰도록 조건 추가
    • 예: 선택한 날짜가 oldestDate…latestDate 사이일 때는 fetchRoutines() 호출하지 않기
  3. 날짜 키 일관성 확인

    • 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.

📥 Commits

Reviewing files that changed from the base of the PR and between cd7d032 and 0b2dfa4.

📒 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 간 일관성) 좋습니다

  • MainRoutineRoutine로의 전환, 관련 퍼블리셔/Subject 제네릭 정리는 인터페이스 일관성과 가독성 측면에서 적절합니다.

Also applies to: 31-31, 36-36, 42-43

case loadEmotion
case moveWeek(week: Int)
case selectDate(date: Date)
case selectRoutineListDate
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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 케이스 이름을 selectRoutineListDateopenRoutineList로 변경
  • 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도 selectRoutineListDateopenRoutineList로 변경
-    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.

Suggested change
case selectRoutineListDate
// HomeViewModel.swift
enum Input {
case openRoutineList
// … other cases …
}
switch input {
case .openRoutineList:
let selectedDate = selectedDateSubject.value
routineListDateSubject.send(selectedDate)
// … other branches …
}
Suggested change
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.

Comment on lines +49 to +52
override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
view.subviews.first(where: { $0.tag == 999 })?.removeFromSuperview()
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

💡 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 Projects

Length 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.

Suggested change
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.

Comment on lines +184 to +189
let newDimmedView = UIView()
newDimmedView.backgroundColor = .black.withAlphaComponent(0.7)
newDimmedView.frame = view.bounds
view.addSubview(newDimmedView)
dimmedView = newDimmedView

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
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.

Comment on lines +219 to +224
let newDimmedView = UIView()
newDimmedView.backgroundColor = .black.withAlphaComponent(0.7)
newDimmedView.frame = view.bounds
view.addSubview(newDimmedView)
dimmedView = newDimmedView

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🛠️ 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.

Suggested change
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.

@choijungp choijungp merged commit 2da89e0 into develop Aug 21, 2025
2 checks passed
@choijungp choijungp deleted the feat/routine-list-view branch August 21, 2025 08:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant