Skip to content

[Feat] 루틴/등록 수정 페이지 user interaction 추가#50

Merged
taipaise merged 1 commit intodevelopfrom
feat/routine-creation-interaction
Aug 17, 2025
Merged

[Feat] 루틴/등록 수정 페이지 user interaction 추가#50
taipaise merged 1 commit intodevelopfrom
feat/routine-creation-interaction

Conversation

@taipaise
Copy link
Copy Markdown
Collaborator

@taipaise taipaise commented Aug 16, 2025

🌁 Background

👩‍💻 Contents

  • 각 루틴 등록/추가 카드 뷰 user interaction 추가

📝 Review Note

분명히 2~300 줄 수준인줄 알았는데,, 송구하옵니다 ㅜㅜ

인터렉션 처리 흐름 View -> ViewModel (RoutineNameContentView를 예시로 들어서)

  • RoutineNameContentViewRoutineCreationExpandable를 채택한 UI 컴포넌트입니다.
  • 이 뷰 내부에는 사용자 동작을 외부로 전달하기 위한 enum Action이 정의되어 있습니다.
  • action이라는 클로저 프로퍼티를 가지고 있으며, **해당 클로저의 파라미터 타입은 위에서 정의한 Action**입니다.
final class RoutineNameContentView: UIView, RoutineCreationExpandable {
    enum Action {
        case subroutineChanged(index: Int, text: String)
        case deleteAllSubroutines
    }

    var action: ((Action) -> Void)?
}
  • viewModel과 유사한데? 라고 생각하셨다면 맞습니다! 해당 구조를 차용해서 만든 구조입니다!
    • 뷰에서 발생하는 이벤트를 Action 타입으로 정의
    • 정의한 Action을 클로저(action)로 외부에 전달
    • 이를 통해 뷰 → 외부로의 데이터 전달 흐름을 단순하고 명확하게 관리하고자 했습니다.
  • 아래는 사용 예시입니다.
    @objc private func textFieldEditingChanged(_ sender: UITextField) {
        action?(.subroutineChanged(index: sender.tag, text: sender.text ?? ""))
    }
  • ContentView가 방출한 Action은 CardView를 거쳐 ViewController로 전달됩니다. VC는 이 액션을 받아 ViewModel의 Input으로 변환하여 전달합니다.
final class RoutineCreationViewController {
    // 생략
    private func bindCreationCardViews() {
        subRoutineNameView.onAction = { [weak self] action in
            switch action {
            case .subroutineChanged(let index, let text):
                self?.viewModel.action(input: .configureSubRoutine(name: text, index: index))
            case .deleteAllSubroutines:
                self?.viewModel.action(input: .deleteAllSubRoutines)
            }
        }
        // 생략
    }
}

view 업데이트 흐름

  • 사실 여기서부터 굉장히 문제가 많습니다. view에서 표시할 데이터를 viewModel에서 가공해야하지만, 상당수 많은 부분을 view에서 가공하고 있습니다. 이 부분은 꼭!!! 개선하도록 하겠습니다.
  • viewModel에서 방출된 데이터는, 다른 코드들 처럼 VC의 bind 함수에서 처리합니다. 이후 각 cardView가 가지고 있는 configure를 통해 데이터를 view에 전달합니다.
    override func bind() {
        viewModel.output.subRoutinesPublisher
            .receive(on: DispatchQueue.main)
            .sink { [weak self] subroutines in
                self?.subRoutineNameView.configure(dependencies: .init(subRoutines: subroutines))
                self?.subRoutineNameView.configure(subTitles: subroutines.filter { !$0.isEmpty })
            }
            .store(in: &cancellables)
  • 현재는 configure 함수가 두 종류입니다. 첫번째는 cardView 자체를 설정한느 함수, 두번째는 cardView내의 RoutineCreationExpandable를 설정하는 함수입니다. 일단 이렇게 이원화하는 자체가 좋은 구조는 아닌것 같습니다. 외부인 VC에서 봤을때는, 단순히 configure를 통해 CardView를 설정할 수 있구나~ 라고만 생각할 수 있는 구조가 더 좋다 생각하기 때문입니다.
  • 이 부분은 그나마 빠르게 개선할 수 있겠지만, 차차 개선해나가도록하겠습니다.
  • 한편, RoutineCreationExpandable 은 associatedType으로 모두 Dependency라는 구조체를 가지게 됩니다.
  • RoutineCreationExpandable내부의 func configure(dependency: Dependency) 를 통해, 외부에서 의존성을 주입받아 view를 재구성하는 방식입니다.
    • 원래 이름은 Dependencies로, 여러개의 데이터를 쪼개서 전달할 수 있었습니다.
    • 하지만 configure에 dependency를 전달할 때 갱신할 필요가 없는 데이터도 함께 전달해줘야하는데, 이 부분에서 문제가 발생했습니다.
    • 예를 들어 시작 날짜, 종료 날짜를 각각 쪼개어두었다면, 시작날짜가 변경되는 시점에 종료날짜에 대한 데이터도 Dependency로 묶어서 RoutineCreationExpandable에게 전달해주어야 합니다. 해당 구조로 구현이 조금 어려워서,, 현재는 Dependency는 구조체지만 내부에 하나의 데이터만 다루고 있습니다. (좀 더 잘 활용할 수 있는 방법을 고민해보겠습니다. )
    • 구조가 너무 복잡해서, 필요하시면 구두로 설명해드리는게 훨씬!! 이해하기 쉬우실 것 같습니다! 편하게 말씀해주세요!!!

Summary by CodeRabbit

  • 신규 기능
    • 주간 반복에서 요일 선택을 지원합니다.
    • 기간 선택을 위해 시작/종료 날짜 캘린더 바텀시트를 추가했습니다.
    • 실행 시간에 ‘하루 종일’ 토글과 미선택 상태 표시를 지원합니다.
    • 카드 헤더에 동적 부제목을 표시하고 상태에 따라 높이/폰트/색상을 자동 조정합니다.
  • 개선 사항
    • 루틴 이름 입력 UX 개선: 실시간 반영, 키보드 반환키로 닫기, 전체 삭제 및 체크 아이콘 상태 반영.
    • 반복 설정 UI 개선: 요일 선택 스타일 즉시 업데이트, 접힘/펼침 시 영역 자동 조절.
    • 전반적 레이아웃 간격 조정으로 가독성 향상.

@taipaise taipaise requested a review from choijungp August 16, 2025 15:30
@taipaise taipaise self-assigned this Aug 16, 2025
@coderabbitai
Copy link
Copy Markdown

coderabbitai Bot commented Aug 16, 2025

Walkthrough

도메인 RepeatType에 Equatable 및 주간 연관값(weeks) 추가. 프레젠테이션 계층 전반에서 Dependencies→Dependency로 API 일괄 변경 및 UI/레이아웃/상태 업데이트 로직 재구성. 뷰컨은 퍼블리셔 바인딩과 캘린더/타임피커 흐름 추가. 뷰모델은 입력/출력/내부 상태를 대폭 개편하고 실행시간/기간/반복 주간 처리 로직을 변경.

Changes

Cohort / File(s) Change Summary
Domain: RepeatType 확장
Projects/Domain/Sources/Entity/Enum/RepeatType.swift
enum에 Equatable 채택, case weeklycase weekly(weeks: Set<Week>)로 변경.
프로토콜 표준화
Projects/Presentation/Sources/RoutineCreation/View/Protocol/RoutineCreationExpandable.swift
associatedtype DependenciesDependency, configure(dependencies:)configure(dependency:).
카드/컨테이너 뷰 갱신
.../RoutineCreation/View/Component/RoutineCreationCardView.swift
헤더 동적 자막 지원, 고정 높이 제거, configure(subTitles:) 추가, ContentView 의존성 API 변경에 맞춘 호출 수정.
이름 입력 뷰 리팩토링
.../RoutineNameContentView.swift
체크 버튼을 이미지 기반으로 교체, 텍스트필드 델리게이트/편집 변경 이벤트 추가, DependenciesDependency, configure(dependency:)로 수정, 전체 삭제 액션 연결.
기간 선택 뷰 수정
.../RoutinePeriodContentView.swift
입력 모델을 (start,end) 튜플 포함 Dependency로 변경, configure(dependency:)에 맞춰 접근 경로 수정.
반복 설정 뷰 개편
.../RoutineRepeatContentView.swift
상수 제거, DependenciesDependency, 주차 버튼-Week 매핑 추가, 일/주간 상태 적용 헬퍼 도입, 주차 가시성/선택 상태 갱신 로직 정리, 액션 이벤트 기반 처리로 전환.
시간 설정 뷰 변경
.../RoutineTimeContentView.swift
Dependency.startTime를 Optional로 변경, nil 상태 UI 처리 추가, configure(dependency:)로 서명 변경.
뷰컨트롤러 플로우 추가
.../RoutineCreationViewController.swift
텍스트필드 바인딩/델리게이트, 클리어 버튼 액션, 퍼블리셔 바인딩들(subRoutines/repeatType/period/time), 날짜 선택 바텀시트 플로우 추가, 타임피커 결과 전달 경로 변경, 리턴키로 키보드 숨김.
뷰모델 대규모 리팩토링
.../RoutineCreation/ViewModel/RoutineCreationViewModel.swift
RoutineExampleUpdateType, ExecutionTypeExecutionTime(startAt: Date?), Input/Output 전면 수정(주/기간/시간/삭제 전용 액션 등), 내부 Subject들 분리(periodStart/End, executionTime 등), 퍼블리셔 결합/검증/등록 로직 갱신.

Sequence Diagram(s)

sequenceDiagram
  actor User
  participant VC as RoutineCreationViewController
  participant PeriodView as RoutinePeriodContentView
  participant Calendar as BitnagilCalendarView
  participant VM as RoutineCreationViewModel

  User->>PeriodView: 시작/종료 날짜 버튼 탭
  PeriodView-->>VC: onAction(.startDateTapped / .endDateTapped)
  VC->>Calendar: 바텀시트 표시
  User->>Calendar: 날짜 선택
  Calendar-->>VC: didSelectDate(date)
  VC->>VM: action(.configureStartDate/.configureEndDate)
  VM-->>VC: periodPublisher (start,end)
  VC->>PeriodView: configure(dependency: (start,end))
Loading
sequenceDiagram
  actor User
  participant VC as RoutineCreationViewController
  participant RepeatView as RoutineRepeatContentView
  participant VM as RoutineCreationViewModel

  User->>RepeatView: 일/주간 버튼/요일들 탭
  RepeatView-->>VC: onAction(.repeatDaily / .repeatWeekly / .setWeeks)
  VC->>VM: action(.configureRepeatType / .configureWeeks)
  VM-->>VC: repeatTypePublisher (nil/daily/weekly(weeks))
  VC->>RepeatView: configure(dependency: updated)
Loading
sequenceDiagram
  actor User
  participant VC as RoutineCreationViewController
  participant TimeView as RoutineTimeContentView
  participant TimePicker as TimePickerView
  participant VM as RoutineCreationViewModel

  User->>TimeView: 전체/시간 설정 탭
  TimeView-->>VC: onAction(.toggleAllDay / .timeSetTapped)
  VC->>VM: action(.toggleAllDay) note right of VC: 전체 선택 시
  VC->>TimePicker: 표시 note right of VC: 시간 설정 시
  User->>TimePicker: 시간 선택
  TimePicker-->>VC: didSelectTime(time)
  VC->>VM: action(.configureExecution(type: ExecutionTime(startAt: time)))
  VM-->>VC: executionTimePublisher(Date?)
  VC->>TimeView: configure(dependency: startTime?)
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~60–90 minutes

Possibly related PRs

Suggested reviewers

  • choijungp

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-creation-interaction

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

🧹 Nitpick comments (12)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutinePeriodContentView.swift (1)

32-34: Dependency에 튜플 대신 명시적 프로퍼티 사용 제안

튜플(dates: (start:end:))은 확장성/가독성이 떨어집니다. start/end가 별도 의미를 가지므로 명시적 프로퍼티가 더 적합합니다. 아래처럼 변경하면 호출부도 더 명확해집니다.

-    struct Dependency {
-        let dates: (start: Date, end: Date)
-    }
+    struct Dependency {
+        let startDate: Date
+        let endDate: Date
+    }
-        let startString = dependency
-            .dates
-            .start
+        let startString = dependency
+            .startDate
             .convertToString(dateType: .yearMonthDate)
-        let endString = dependency
-            .dates
-            .end
+        let endString = dependency
+            .endDate
             .convertToString(dateType: .yearMonthDate)

Also applies to: 67-75

Projects/Presentation/Sources/RoutineCreation/View/Protocol/RoutineCreationExpandable.swift (1)

11-21: 중복된 확장/축소(setExpanded) 로직 프로토콜 익스텐션으로 공통화 제안

여러 ContentView에서 동일한 토글 패턴을 반복합니다. 프로토콜 기본 구현으로 중복 제거하면 유지보수성이 좋아집니다.

다음 익스텐션을 추가하고, 특수 처리가 필요한 뷰(예: 주간 버튼 표시 갱신)는 setExpanded 호출 후 별도 메서드로 후처리하세요.

extension RoutineCreationExpandable where Self: UIView {
    func setExpanded(expanded: Bool) {
        guard heightConstraint?.isActive == expanded else { return }

        subviews.forEach { $0.alpha = 0 }
        defer { subviews.forEach { $0.alpha = 1 } }

        heightConstraint?.isActive = !expanded
        subviews.forEach { $0.isHidden = !expanded }
    }
}
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineTimeContentView.swift (1)

65-70: 체크 아이콘 상태 일관성 유지

nil(미선택)일 때는 unchecked 아이콘을 쓰면서, 특정 시간 선택 시에는 빈 이미지(UIImage())를 설정합니다. 동일한 미체크 표현(예: uncheckedIcon)으로 통일하는 편이 UI 일관성에 좋습니다. 또한 configureTime(time:)도 동일 규칙을 반영하도록 갱신을 권장합니다.

         if time.isMidnight {
             timeButton.setTitle("하루종일", for: .normal)
             checkButtonImageView.image = BitnagilIcon.checkedIcon
         } else {
             timeButton.setTitle(time.convertToString(dateType: .amPmTimeShort), for: .normal)
-            checkButtonImageView.image = UIImage()
+            checkButtonImageView.image = BitnagilIcon.uncheckedIcon
         }
-    func configureTime(time: Date) {
-        let timeString = time.convertToString(dateType: .amPmTimeShort)
-        timeButton.setTitle(timeString, for: .normal)
-    }
+    func configureTime(time: Date) {
+        if time.isMidnight {
+            timeButton.setTitle("하루종일", for: .normal)
+            checkButtonImageView.image = BitnagilIcon.checkedIcon
+        } else {
+            timeButton.setTitle(time.convertToString(dateType: .amPmTimeShort), for: .normal)
+            checkButtonImageView.image = BitnagilIcon.uncheckedIcon
+        }
+    }

Also applies to: 72-78, 157-160

Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineNameContentView.swift (1)

51-51: 미사용 프로퍼티 정리

isExpanded가 사용되지 않습니다. 불필요한 상태는 제거해 주세요.

-    private var isExpanded = true // 초기 상태 추적
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineCreationCardView.swift (2)

179-204: 서브타이틀 업데이트 로직 개선 필요

configure(subTitles:) 메서드에서 서브타이틀을 추가한 후 updateHeaderVisibility()를 호출하는데, 이미 titleLabel의 높이를 Line 198-200에서 업데이트하고 있습니다. 이로 인해 updateHeaderVisibility() 내부에서 Line 279-281에서 동일한 constraint를 다시 업데이트하게 되어 중복 작업이 발생합니다.

 func configure(subTitles: [String]) {
     labelStackView.arrangedSubviews.dropFirst(2).forEach { subview in
         labelStackView.removeArrangedSubview(subview)
         subview.removeFromSuperview()
     }

     for text in subTitles {
         let label = UILabel()
         label.text = text
         label.font = BitnagilFont(style: .body1, weight: .semiBold).font
         label.textColor = BitnagilColor.gray10
         label.numberOfLines = 1

         labelStackView.addArrangedSubview(label)
         label.snp.makeConstraints { make in
             make.height.equalTo(Layout.subTitleLabelHeight)
         }
     }

-    titleLabel.snp.updateConstraints { make in
-        make.height.equalTo(subTitles.isEmpty ? Layout.titleLabelLargeHeight : Layout.titleLabelSmallHeight)
-    }
-
     updateHeaderVisibility()
     setNeedsLayout()
 }

266-290: 폰트 설정 로직 간소화 가능

updateHeaderVisibility() 메서드의 Line 283-289에서 titleHeight 값에 따라 폰트를 설정하는 로직이 복잡합니다. 더 간결하게 표현할 수 있습니다.

 private func updateHeaderVisibility() {
     let arranged = labelStackView.arrangedSubviews
     let subtitles = arranged.dropFirst(2)
     let hasSubtitles = !subtitles.isEmpty
     let isExpanded = !contentView.isHidden
     let titleHeight: CGFloat = {
         if isExpanded { return Layout.titleLabelLargeHeight }
         return hasSubtitles ? Layout.titleLabelSmallHeight : Layout.titleLabelLargeHeight
     }()

     placeHolderLabel.isHidden = isExpanded || hasSubtitles
     subtitles.forEach { $0.isHidden = isExpanded }

     titleLabel.snp.updateConstraints { make in
         make.height.equalTo(titleHeight)
     }

-    if titleHeight == Layout.titleLabelLargeHeight {
-        titleLabel.font = isExpanded ? BitnagilFont.init(style: .body2, weight: .semiBold).font : BitnagilFont.init(style: .body1, weight: .semiBold).font
-        titleLabel.textColor = BitnagilColor.gray10
-    } else {
-        titleLabel.font = BitnagilFont.init(style: .body2, weight: .medium).font
-        titleLabel.textColor = BitnagilColor.gray50
-    }
+    let isLargeTitle = titleHeight == Layout.titleLabelLargeHeight
+    
+    if isLargeTitle {
+        let style: BitnagilFont.Style = isExpanded ? .body2 : .body1
+        titleLabel.font = BitnagilFont(style: style, weight: .semiBold).font
+        titleLabel.textColor = BitnagilColor.gray10
+    } else {
+        titleLabel.font = BitnagilFont(style: .body2, weight: .medium).font
+        titleLabel.textColor = BitnagilColor.gray50
+    }
 }
Projects/Presentation/Sources/RoutineCreation/View/RoutineCreationViewController.swift (2)

14-17: DateSelectionType enum을 더 직관적인 위치로 이동 고려

DateSelectionType enum이 ViewController 내부에 private으로 선언되어 있는데, 날짜 선택 관련 로직이 복잡해질 경우 별도 타입으로 분리하는 것을 고려해보세요.


203-208: 서브루틴 configure 중복 호출 최적화

서브루틴 업데이트 시 configure(dependencies:)configure(subTitles:)를 연속으로 호출하는데, 이는 뷰 업데이트를 두 번 트리거할 수 있습니다.

단일 configure 메서드로 통합하거나, 내부적으로 한 번의 레이아웃 패스로 처리하도록 개선하면 성능이 향상될 수 있습니다.

Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineRepeatContentView.swift (2)

169-179: 불필요한 self 캡처 간소화 가능

버튼 액션 클로저에서 self를 weak 캡처한 후 guard let으로 언래핑하는데, 더 간결하게 작성할 수 있습니다.

 button.addAction(
-    UIAction { [weak self] action in
-        guard
-            let self,
-            let sender = action.sender as? UIButton
-        else { return }
+    UIAction { [weak self] action in
+        guard let sender = action.sender as? UIButton else { return }

         sender.isSelected.toggle()
-        configureWeekButton(weekButton: sender)
-        self.emitSelectedWeeks()
+        self?.configureWeekButton(weekButton: sender)
+        self?.emitSelectedWeeks()
     },
     for: .touchUpInside
 )

249-263: emitSelectedWeeks 메서드 성능 개선 가능

주간 버튼 선택 시마다 전체 버튼을 순회하여 선택된 주를 찾는데, 이미 buttonToWeek 딕셔너리가 있으므로 더 효율적으로 처리할 수 있습니다.

 private func emitSelectedWeeks() {
     guard weeklyButton.isSelected else { return }

-    let weeks: [Week] = weekStackView
-        .arrangedSubviews
-        .compactMap { subview in
-            guard
-                let button = subview as? UIButton,
-                button.isSelected,
-                let week = buttonToWeek[button]
-            else { return nil }
-
-            return week
-        }
-    .sorted { $0.id < $1.id }
+    let weeks = buttonToWeek
+        .filter { $0.key.isSelected }
+        .compactMap { $0.value }
+        .sorted { $0.id < $1.id }

     action?(.setWeeks(weeks))
 }
Projects/Presentation/Sources/RoutineCreation/ViewModel/RoutineCreationViewModel.swift (2)

86-86: deleteAllSubRoutines 초기화 값 검토

deleteAllSubRoutines 액션이 서브루틴을 ["", "", ""]로 초기화하는데, 이는 UI에서 빈 텍스트필드 3개를 보여주기 위한 것으로 보입니다. 더 명확한 상수로 정의하면 좋겠습니다.

+private let emptySubRoutines = ["", "", ""]

 case .deleteAllSubRoutines:
-    subRoutinesSubject.send(["", "", ""])
+    subRoutinesSubject.send(emptySubRoutines)

203-214: 시간 설정 로직 복잡도 개선

configureExecutionTime 메서드에서 midnight 체크 로직이 복잡합니다. 토글 동작이 명확하게 드러나도록 개선할 수 있습니다.

 private func configureExecutionTime(time: ExecutionTime) {
-    if
-       let time = time.startAt,
-       let curTime = executionTimeSubject.value.startAt,
-       time.isMidnight,
-       curTime.isMidnight
-    {
-        executionTimeSubject.send(.init(startAt: nil))
-        return
-    }
-
-    executionTimeSubject.send(time)
+    let newTime = time.startAt
+    let currentTime = executionTimeSubject.value.startAt
+    
+    // Toggle off if both times are midnight (all-day selection)
+    let shouldClearTime = newTime?.isMidnight == true && 
+                          currentTime?.isMidnight == true
+    
+    executionTimeSubject.send(
+        shouldClearTime ? .init(startAt: nil) : time
+    )
 }
📜 Review details

Configuration used: .coderabbit.yaml
Review profile: CHILL
Plan: Pro

💡 Knowledge Base configuration:

  • MCP integration is disabled by default for public repositories
  • Jira integration is disabled by default for public repositories
  • Linear integration is disabled by default for public repositories

You can enable these sources in your CodeRabbit configuration.

📥 Commits

Reviewing files that changed from the base of the PR and between 6dc94ec and 89cb48b.

📒 Files selected for processing (9)
  • Projects/Domain/Sources/Entity/Enum/RepeatType.swift (1 hunks)
  • Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineCreationCardView.swift (6 hunks)
  • Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineNameContentView.swift (9 hunks)
  • Projects/Presentation/Sources/RoutineCreation/View/Component/RoutinePeriodContentView.swift (2 hunks)
  • Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineRepeatContentView.swift (5 hunks)
  • Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineTimeContentView.swift (2 hunks)
  • Projects/Presentation/Sources/RoutineCreation/View/Protocol/RoutineCreationExpandable.swift (1 hunks)
  • Projects/Presentation/Sources/RoutineCreation/View/RoutineCreationViewController.swift (5 hunks)
  • Projects/Presentation/Sources/RoutineCreation/ViewModel/RoutineCreationViewModel.swift (8 hunks)
🧰 Additional context used
🧬 Code Graph Analysis (7)
Projects/Presentation/Sources/RoutineCreation/View/Protocol/RoutineCreationExpandable.swift (4)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutinePeriodContentView.swift (2)
  • setExpanded (57-65)
  • configure (67-79)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineNameContentView.swift (2)
  • setExpanded (63-71)
  • configure (73-92)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineRepeatContentView.swift (2)
  • setExpanded (52-61)
  • configure (63-67)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineTimeContentView.swift (2)
  • setExpanded (55-63)
  • configure (65-79)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineTimeContentView.swift (3)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutinePeriodContentView.swift (1)
  • configure (67-79)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineNameContentView.swift (1)
  • configure (73-92)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineRepeatContentView.swift (1)
  • configure (63-67)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutinePeriodContentView.swift (5)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineNameContentView.swift (1)
  • configure (73-92)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineCreationCardView.swift (3)
  • configure (69-76)
  • configure (179-204)
  • configure (206-208)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineRepeatContentView.swift (1)
  • configure (63-67)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineTimeContentView.swift (1)
  • configure (65-79)
Projects/Shared/Sources/Extension/Date+.swift (1)
  • convertToString (16-22)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineRepeatContentView.swift (2)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineTimeContentView.swift (2)
  • configure (65-79)
  • configureAttribute (81-112)
Projects/Presentation/Sources/RoutineCreation/ViewModel/RoutineCreationViewModel.swift (1)
  • action (77-107)
Projects/Presentation/Sources/RoutineCreation/View/RoutineCreationViewController.swift (6)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineNameContentView.swift (2)
  • textFieldEditingChanged (242-244)
  • configure (73-92)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutinePeriodContentView.swift (1)
  • configure (67-79)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineRepeatContentView.swift (1)
  • configure (63-67)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineTimeContentView.swift (1)
  • configure (65-79)
Projects/Shared/Sources/Extension/Date+.swift (1)
  • convertToString (16-22)
Projects/Presentation/Sources/Common/Extension/UIViewController+.swift (1)
  • presentCustomBottomSheet (97-100)
Projects/Presentation/Sources/RoutineCreation/ViewModel/RoutineCreationViewModel.swift (5)
Projects/Domain/Sources/UseCase/Routine/RoutineUseCase.swift (1)
  • fetchRoutine (18-21)
Projects/DataSource/Sources/Repository/RoutineRepository.swift (1)
  • fetchRoutine (26-31)
Projects/DataSource/Sources/Repository/RecommendedRoutineRepository.swift (1)
  • fetchRecommendedRoutine (13-20)
Projects/Domain/Sources/UseCase/RecommendedRoutine/RecommendedRoutineUseCase.swift (1)
  • fetchRecommendedRoutine (15-18)
Projects/Shared/Sources/Extension/Date+.swift (2)
  • convertToDate (24-30)
  • convertToString (16-22)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineCreationCardView.swift (4)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutinePeriodContentView.swift (1)
  • configure (67-79)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineNameContentView.swift (1)
  • configure (73-92)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineRepeatContentView.swift (1)
  • configure (63-67)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineTimeContentView.swift (1)
  • configure (65-79)
🪛 SwiftLint (0.57.0)
Projects/Presentation/Sources/RoutineCreation/ViewModel/RoutineCreationViewModel.swift

[Warning] 256-256: TODOs should be resolved (- 수정 필요)

(todo)

🔇 Additional comments (5)
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutinePeriodContentView.swift (1)

67-79: 기간 값 유효성(시작 ≤ 종료) 보장에 대한 책임 위치 확인 필요

UI 단에서는 문자열로만 표기하므로 무방하지만, 시작일이 종료일보다 이후인 입력이 들어올 가능성(예: 사용자 선택 순서)을 ViewModel/도메인에서 방지·정규화하는지 확인이 필요합니다. 이 뷰는 그대로 표시만 하되, 상위 계층에서 보장하는 흐름이면 OK입니다.

Projects/Domain/Sources/Entity/Enum/RepeatType.swift (1)

8-11: Equatable 추가와 주간 weeks 데이터 도입 방향 👍

Set로 유일성 유지가 보장되고, Equatable 합성도 자동으로 되어 비교 로직 단순화에 도움이 됩니다.

Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineNameContentView.swift (2)

87-91: 체크 아이콘 갱신 로직은 명확하고 직관적입니다

모든 입력이 빈 문자열일 때 checked, 그 외에는 unchecked로 표현하는 규칙이 명확합니다. 위 가드 제거 후에도 정상 동작합니다.


109-127: 텍스트필드 태깅/델리게이트 설정 방식 적절

index 태그로 액션 라우팅하고, editingChanged로 상위로 이벤트 전달하는 구조가 깔끔합니다. placeholder 스타일링도 일관적입니다.

Projects/Presentation/Sources/RoutineCreation/View/RoutineCreationViewController.swift (1)

319-328: dateSelectionType 초기화 시점 개선

dateSelectionType을 날짜 선택 완료 후에 nil로 초기화하는데, 사용자가 캘린더를 취소하는 경우에도 초기화가 필요할 수 있습니다.

캘린더 뷰가 dismiss될 때 dateSelectionType이 제대로 초기화되는지 확인이 필요합니다. CustomBottomSheet이 취소되는 경우를 처리하는 delegate 메서드가 있는지 확인해보세요.

Comment on lines +8 to 11
public enum RepeatType: Equatable {
case daily
case weekly
case weekly(weeks: Set<Week>)
}
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

케이스 시그니처 변경(.weekly → .weekly(weeks:))에 따른 사용처 점검 필요

연관값 추가로 기존 .weekly 패턴 매칭/생성이 컴파일 에러가 될 수 있습니다. 아래 스크립트로 레거시 사용을 찾아 점검해 주세요.


🏁 Script executed:

#!/bin/bash
# .weekly 연관값 없이 사용된 곳 탐지
rg -nP --type=swift -C2 '\.weekly(?!\s*\()'

Length of output: 2098


.weekly 케이스 연관값 처리 누락으로 인한 컴파일 에러 수정 필요

RepeatTypeweekly(weeks: Set<Week>)가 추가되면서, 기존의 .weekly 패턴 매칭/생성 코드는 모두 컴파일 에러가 발생합니다. 아래 위치들을 확인해 패턴 매칭 시 연관값 처리를 추가하거나 와일드카드로 무시하도록 수정하세요.

  • Projects/Presentation/Sources/RoutineCreation/ViewModel/RoutineCreationViewModel.swift
    • 186줄:
  • case .weekly:
  • case .weekly(let weeks):

• 187줄:
```diff

  • if case .weekly = current {
  • if case .weekly(_) = current {

• 198줄:
```diff

  • guard case .weekly = repeatTypeSubject.value else { return }
  • guard case .weekly(_) = repeatTypeSubject.value else { return }
    
    
  • Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineRepeatContentView.swift
    • 190줄:
  • case .weekly:
  • case .weekly(_):
    
    

필요하다면 let weeks로 바인딩하여 UI 업데이트에 활용할 수 있습니다.

Comment on lines +73 to 75
func configure(dependency: Dependency) {
guard !dependency.subRoutines.isEmpty else { 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.

🛠️ Refactor suggestion

빈 배열 입력 시에도 UI가 정상 초기화되도록 가드 제거 및 나머지 필드 초기화

빈 배열이면 return 되어 체크 아이콘/텍스트필드 초기화가 누락될 수 있습니다. 가드를 제거하고, 전달된 subRoutines 수보다 많은 텍스트필드는 비워 주세요.

-    func configure(dependency: Dependency) {
-        guard !dependency.subRoutines.isEmpty else { return }
+    func configure(dependency: Dependency) {
 
         let subRoutines = dependency.subRoutines
         let subRoutineTextFields = [
             subRoutineTextField1,
             subRoutineTextField2,
             subRoutineTextField3]
         let minCount = min(subRoutines.count, subRoutineTextFields.count)
 
-        zip(subRoutines, subRoutineTextFields).prefix(minCount).forEach {
-            $1.text = $0
-        }
+        zip(subRoutines, subRoutineTextFields).prefix(minCount).forEach { $1.text = $0 }
+        // 남은 필드는 초기화
+        subRoutineTextFields.dropFirst(minCount).forEach { $0.text = "" }

Also applies to: 83-86

🤖 Prompt for AI Agents
In
Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineNameContentView.swift
around lines 73–75 (and similarly 83–86), remove the early guard that returns on
empty dependency.subRoutines so UI always initializes; always set the check-icon
state and configure each text field up to dependency.subRoutines.count with the
provided subRoutine values, and explicitly clear any remaining text fields and
reset their check-icons when dependency.subRoutines.count is less than the total
fields so no stale text or checked state remains.

Comment on lines +13 to 21
associatedtype Dependency

/// RoutineCreationExpandableContentView 에서 일어날 수 있는 action을 전달합니다.
var action: ((Action) -> Void)? { get set }
var heightConstraint: Constraint? { get set }

func setExpanded(expanded: Bool)
func configure(dependencies: Dependencies)
func configure(dependency: Dependency)
}
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

Dependencies → Dependency 리네이밍에 따른 전역 사용처 확인

프로토콜 시그니처가 바뀌었으므로 구현체/호출부에 잔존 레퍼런스가 없는지 확인해 주세요.


🏁 Script executed:

#!/bin/bash
# 남아있는 기존 표기 탐지
rg -nP --type=swift -C2 '\bassociatedtype\s+Dependencies\b|\bconfigure\s*\(\s*dependencies\s*:'

Length of output: 5069


DependenciesDependency 리네이밍에 따른 호출부 및 구현체 수정 요청

프로토콜 시그니처를 associatedtype Dependencyfunc configure(dependency:)로 변경했으므로, 아직 남아 있는 configure(dependencies:) 호출부와 구현체 시그니처를 모두 dependency 파라미터로 교체해야 합니다. 아래 위치를 확인해 주세요:

  • Projects/Presentation/Sources/RoutineCreation/View/RoutineCreationViewController.swift

    • 205행: self?.subRoutineNameView.configure(dependencies: .init(subRoutines: subroutines))
    • 214행: self?.repeatView.configure(dependencies: .init(repeatType: .none))
    • 224행: self?.repeatView.configure(dependencies: .init(repeatType: .daily))
    • 231행: self?.repeatView.configure(dependencies: .init(repeatType: .weekly(weeks: weeks)))
    • 240행: self?.periodView.configure(dependencies: .init(dates: (start, end)))
    • 247행: self?.timeView.configure(dependencies: .init(startTime: startAt))
  • Projects/Presentation/Sources/RoutineCreation/View/Component/RoutineCreationCardView.swift

    • 206행: func configure(dependencies: ContentView.Dependency)

위 모든 호출과 시그니처에서

  • 파라미터 이름 dependenciesdependency
  • 메소드 호출 configure(dependencies:)configure(dependency:)
    로 수정하고, 전달하는 인스턴스가 Dependency 타입으로 올바르게 초기화되는지 확인해 주세요.

try await routineUseCase.saveRoutine(
routineSummary: routineSummary,
subRoutineSummaries: subRoutinesSubject.value,
subRoutineSummaries: [], // TODO: - 수정 필요
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

TODO 주석 해결 필요

서브루틴 저장 로직이 빈 배열로 하드코딩되어 있습니다. 실제 서브루틴 데이터를 저장하도록 구현이 필요합니다.

 try await routineUseCase.saveRoutine(
     routineSummary: routineSummary,
-    subRoutineSummaries: [], // TODO: - 수정 필요
+    subRoutineSummaries: subRoutinesSubject.value
+        .enumerated()
+        .filter { !$0.element.isEmpty }
+        .map { index, name in
+            SubRoutineSummaryEntity(
+                subRoutineId: nil,
+                subRoutineName: name,
+                orderNumber: index
+            )
+        },
     deletedSubRoutineSummaries: Array(deletedSubroutines))

이 구현을 완성하기 위해 새로운 이슈를 생성할까요?

📝 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
subRoutineSummaries: [], // TODO: - 수정 필요
try await routineUseCase.saveRoutine(
routineSummary: routineSummary,
subRoutineSummaries: subRoutinesSubject.value
.enumerated()
.filter { !$0.element.isEmpty }
.map { index, name in
SubRoutineSummaryEntity(
subRoutineId: nil,
subRoutineName: name,
orderNumber: index
)
},
deletedSubRoutineSummaries: Array(deletedSubroutines)
)
🧰 Tools
🪛 SwiftLint (0.57.0)

[Warning] 256-256: TODOs should be resolved (- 수정 필요)

(todo)

🤖 Prompt for AI Agents
In
Projects/Presentation/Sources/RoutineCreation/ViewModel/RoutineCreationViewModel.swift
around line 256, the code currently hardcodes subRoutineSummaries: [] — replace
this placeholder with the actual conversion from your in-memory sub-routine view
models to the domain summary models (e.g., map subRoutineViewModels or
subRoutines into an array of SubRoutineSummary by pulling required fields like
id/title/duration/order, handling optionals/defaults, and preserving
order/identifiers). Ensure you call the builder/initializer used elsewhere for
summaries, remove the TODO comment, and keep validation/error handling so the
save path stores real sub-routine data instead of an empty array.

Copy link
Copy Markdown
Contributor

@choijungp choijungp left a comment

Choose a reason for hiding this comment

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

딩 .......................
너무너무너무 잘해주셔가주구 ... 어푸루부 드립니다 ㅠ ㅠ

저의 지능 이슈로 .. 혹시 이따 회의할 때 간략 브리핑 부탁드려두 될깝쇼 ?????? ?????
하지만 띵이 다 옳게 하고 있다고 생각함 ..

Comment on lines +282 to +285
case .startDateSetTapped:
self?.dateSelectionType = .start
case .endDateSetTapped:
self?.dateSelectionType = .end
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

이건 단순 궁굼한 점인데요
start <= end 여야 하는거잖아요 .. 만일 start가 end 보다 이후면 어떻게 되나요 ?? ㅠㅠ !!!!!

서버에서 이를 막아놨는지 .. 아니라면 클라라도 막아놔야한다고 생각함 .. 하지만 이것보단 빨리 구현이 우선이라구 생각해요 ㅠㅠ !!

@taipaise taipaise merged commit 5ad2739 into develop Aug 17, 2025
2 checks passed
@taipaise taipaise deleted the feat/routine-creation-interaction branch August 18, 2025 12:14
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.

2 participants