Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion Projects/Presentation/Sources/Home/View/HomeView.swift
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,13 @@ final class HomeView: BaseViewController<HomeViewModel> {
configureGradientBackground()
showIndicatorView()
viewModel.action(input: .loadNickname)
viewModel.action(input: .fetchRoutines)
}

override func viewWillAppear(_ animated: Bool) {
super.viewWillAppear(animated)
configureNavigationBar(navigationStyle: .hidden)
viewModel.action(input: .loadEmotion)
viewModel.action(input: .fetchRoutines)

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

fetchRoutines를 viewWillAppear로 이동한 것은 타이밍 관점에서 적절합니다

뷰 재진입 시 최신 상태를 보장할 수 있습니다. 다만 ViewModel.selectDate 내부의 fetchRoutines 중복 호출은 제거해야 루프/과호출을 근본적으로 줄일 수 있습니다(위 ViewModel 코멘트의 diff 참고).

🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Home/View/HomeView.swift at line 115, the call
to viewModel.action(input: .fetchRoutines) should be removed to avoid redundant
fetchRoutines calls. This is because fetchRoutines is already triggered inside
ViewModel.selectDate, so removing this direct call here will prevent duplicate
fetches and reduce unnecessary loops or over-calling.

}

override func viewDidLayoutSubviews() {
Expand Down
72 changes: 41 additions & 31 deletions Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ final class HomeViewModel: ViewModel {
updateRoutineCompletion(updatedRoutine: updatedRoutine)

case .refreshSelectedDateRoutine:
fetchRoutines(for: selectedDateSubject.value)
fetchDailyRoutine(for: selectedDateSubject.value)

case .selectRoutineSortType(let routineSortType):
sortRoutine(routineSortType: routineSortType)
Expand Down Expand Up @@ -135,17 +135,51 @@ final class HomeViewModel: ViewModel {
// MARK: - 루틴
// 루틴들을 불러옵니다. (처음에는 +-1 주, 그 이후에는 1주씩)
private func fetchRoutines() {
var startDate = oldestDate
var endDate = latestDate

if routines.isEmpty {
startDate = calendar.date(byAdding: .weekOfYear, value: -1, to: today) ?? today
endDate = calendar.date(byAdding: .weekOfYear, value: 1, to: today) ?? today
oldestDate = calendar.date(byAdding: .weekOfYear, value: -1, to: today) ?? today
latestDate = calendar.date(byAdding: .weekOfYear, value: 1, to: today) ?? today
}
fetchRoutines(startDate: oldestDate, endDate: latestDate)
}

// 날짜를 선택하고 그 날에 해당하는 루틴을 불러옵니다.
private func selectDate(date: Date) {
selectedDateSubject.send(date)
fetchRoutines()
fetchDailyRoutine(for: date)
}

Comment on lines +145 to +151

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

selectDate에서 fetchRoutines 중복 호출 제거 권장 (루프·중복 네트워크 호출 방지)

selectDate 내부의 fetchRoutines는 fetchDailyRoutine가 캐시 확장까지 담당하는 현재 구조에서 중복 호출이며, HomeView.viewWillAppear에서도 fetch가 실행되어 과호출/루프의 촉매가 됩니다. 아래처럼 제거해 주세요.

 private func selectDate(date: Date) {
     selectedDateSubject.send(date)
-    fetchRoutines()
     fetchDailyRoutine(for: date)
 }
📝 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 func selectDate(date: Date) {
selectedDateSubject.send(date)
fetchRoutines()
fetchDailyRoutine(for: date)
}
// 날짜를 선택하고 그 날에 해당하는 루틴을 불러옵니다.
private func selectDate(date: Date) {
selectedDateSubject.send(date)
fetchDailyRoutine(for: date)
}
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift around lines
145 to 151, the selectDate function calls fetchRoutines redundantly, causing
duplicate network requests and potential loops since fetchDailyRoutine already
handles cache extension and fetchRoutines is also called in
HomeView.viewWillAppear. Remove the fetchRoutines call from selectDate to
prevent these redundant calls and improve efficiency.

private func fetchDailyRoutine(for date: Date) {
if let dailyRoutines = routines[date.convertToString(dateType: .yearMonthDate)] {
routinesSubject.send(dailyRoutines)
return
}

var startDate: Date = Date()
var endDate: Date = Date()
if date < oldestDate {
// 캐싱된 데이터보다 이전의 날짜 조회 시,
startDate = calendar.date(byAdding: .weekOfYear, value: -1, to: oldestDate) ?? oldestDate
endDate = calendar.date(byAdding: .day, value: -1, to: oldestDate) ?? oldestDate
oldestDate = startDate
} else if date > latestDate {
// 캐싱된 데이터보다 이후의 날짜 조회 시,
startDate = calendar.date(byAdding: .day, value: 1, to: latestDate) ?? latestDate
endDate = calendar.date(byAdding: .weekOfYear, value: 1, to: latestDate) ?? latestDate
latestDate = endDate
}
fetchRoutines(startDate: startDate, endDate: endDate)

if let dailyRoutines = routines[date.convertToString(dateType: .yearMonthDate)] {
routinesSubject.send(dailyRoutines)
return
} else {
routinesSubject.send([])
}
}
Comment on lines +152 to +179

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

⚠️ Potential issue

무한 루프 및 깜빡임(flicker) 유발 지점: 캐시 내부 날짜에도 불필요한 fetch 및 즉시 빈 배열 방출

문제점

  • date가 캐시 [oldestDate, latestDate] 안에 있지만 해당 날짜 키가 비어있을 때도 start/end를 오늘(Date())로 둔 채 fetchRoutines가 호출됩니다.
  • 비동기 fetchRoutines 호출 직후 즉시 빈 배열을 routinesSubject로 내보내 UI가 빈 상태로 깜빡이고, fetch 완료 → refreshSelectedDateRoutine → 다시 fetchDailyRoutine의 루프가 이어질 수 있습니다.

해결

  • 캐시 범위 밖인 경우에만 네트워크 fetch를 수행하고, 수행했다면 즉시 반환(return)하여 빈 배열을 방출하지 않도록 합니다.
  • 캐시 범위 안인데 데이터가 없는 경우에만 빈 배열을 단 1회 방출합니다.

아래 패치 적용을 제안합니다.

 private func fetchDailyRoutine(for date: Date) {
     if let dailyRoutines = routines[date.convertToString(dateType: .yearMonthDate)] {
         routinesSubject.send(dailyRoutines)
         return
     }

-    var startDate: Date = Date()
-    var endDate: Date = Date()
+    var startDateToFetch: Date?
+    var endDateToFetch: Date?

     if date < oldestDate {
         // 캐싱된 데이터보다 이전의 날짜 조회 시,
-        startDate = calendar.date(byAdding: .weekOfYear, value: -1, to: oldestDate) ?? oldestDate
-        endDate = calendar.date(byAdding: .day, value: -1, to: oldestDate) ?? oldestDate
-        oldestDate = startDate
+        let s = calendar.date(byAdding: .weekOfYear, value: -1, to: oldestDate) ?? oldestDate
+        let e = calendar.date(byAdding: .day, value: -1, to: oldestDate) ?? oldestDate
+        startDateToFetch = s
+        endDateToFetch = e
+        oldestDate = s
     } else if date > latestDate {
         // 캐싱된 데이터보다 이후의 날짜 조회 시,
-        startDate = calendar.date(byAdding: .day, value: 1, to: latestDate) ?? latestDate
-        endDate = calendar.date(byAdding: .weekOfYear, value: 1, to: latestDate) ?? latestDate
-        latestDate = endDate
+        let s = calendar.date(byAdding: .day, value: 1, to: latestDate) ?? latestDate
+        let e = calendar.date(byAdding: .weekOfYear, value: 1, to: latestDate) ?? latestDate
+        startDateToFetch = s
+        endDateToFetch = e
+        latestDate = e
     }
-    fetchRoutines(startDate: startDate, endDate: endDate)
-
-    if let dailyRoutines = routines[date.convertToString(dateType: .yearMonthDate)] {
-        routinesSubject.send(dailyRoutines)
-        return
-    } else {
-        routinesSubject.send([])
-    }
+    if let s = startDateToFetch, let e = endDateToFetch {
+        // 범위 확장 fetch를 수행했다면, 완료 후 .refreshSelectedDateRoutine에서 다시 들어와 데이터 방출하도록 즉시 반환
+        fetchRoutines(startDate: s, endDate: e)
+        return
+    }
+    // 캐시 범위 안이지만 데이터가 없을 때만 빈 배열 방출 (중복 네트워크 호출/루프 방지)
+    routinesSubject.send([])
 }

이 변경으로

  • 캐시 확대가 필요한 경우에만 네트워크 호출
  • 확대 fetch 직후에는 빈 배열 방출을 피해서 UI 깜빡임↓
  • fetch 완료 → refreshSelectedDateRoutine → 재평가 흐름으로 유한 반복 보장
📝 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 func fetchDailyRoutine(for date: Date) {
if let dailyRoutines = routines[date.convertToString(dateType: .yearMonthDate)] {
routinesSubject.send(dailyRoutines)
return
}
var startDate: Date = Date()
var endDate: Date = Date()
if date < oldestDate {
// 캐싱된 데이터보다 이전의 날짜 조회 시,
startDate = calendar.date(byAdding: .weekOfYear, value: -1, to: oldestDate) ?? oldestDate
endDate = calendar.date(byAdding: .day, value: -1, to: oldestDate) ?? oldestDate
oldestDate = startDate
} else if date > latestDate {
// 캐싱된 데이터보다 이후의 날짜 조회 시,
startDate = calendar.date(byAdding: .day, value: 1, to: latestDate) ?? latestDate
endDate = calendar.date(byAdding: .weekOfYear, value: 1, to: latestDate) ?? latestDate
latestDate = endDate
}
fetchRoutines(startDate: startDate, endDate: endDate)
if let dailyRoutines = routines[date.convertToString(dateType: .yearMonthDate)] {
routinesSubject.send(dailyRoutines)
return
} else {
routinesSubject.send([])
}
}
private func fetchDailyRoutine(for date: Date) {
if let dailyRoutines = routines[date.convertToString(dateType: .yearMonthDate)] {
routinesSubject.send(dailyRoutines)
return
}
var startDateToFetch: Date?
var endDateToFetch: Date?
if date < oldestDate {
// 캐싱된 데이터보다 이전의 날짜 조회 시,
let s = calendar.date(byAdding: .weekOfYear, value: -1, to: oldestDate) ?? oldestDate
let e = calendar.date(byAdding: .day, value: -1, to: oldestDate) ?? oldestDate
startDateToFetch = s
endDateToFetch = e
oldestDate = s
} else if date > latestDate {
// 캐싱된 데이터보다 이후의 날짜 조회 시,
let s = calendar.date(byAdding: .day, value: 1, to: latestDate) ?? latestDate
let e = calendar.date(byAdding: .weekOfYear, value: 1, to: latestDate) ?? latestDate
startDateToFetch = s
endDateToFetch = e
latestDate = e
}
if let s = startDateToFetch, let e = endDateToFetch {
// 범위 확장 fetch를 수행했다면, 완료 후 .refreshSelectedDateRoutine에서 다시 들어와 데이터 방출하도록 즉시 반환
fetchRoutines(startDate: s, endDate: e)
return
}
// 캐시 범위 안이지만 데이터가 없을 때만 빈 배열 방출 (중복 네트워크 호출/루프 방지)
routinesSubject.send([])
}
🤖 Prompt for AI Agents
In Projects/Presentation/Sources/Home/ViewModel/HomeViewModel.swift around lines
152 to 179, the fetchDailyRoutine function calls fetchRoutines unnecessarily
when the requested date is within the cached range but the data is missing,
causing flickering and potential infinite loops. Fix this by modifying the logic
to only call fetchRoutines when the date is outside the cached range, and
immediately return after fetching to avoid emitting an empty array prematurely.
If the date is within the cached range but data is missing, emit an empty array
only once without triggering a fetch. This prevents unnecessary network calls
and UI flicker.


// 서버로부터 루틴들을 불러옵니다.. (루틴 조회 시작 날짜 ~ 루틴 조회 종료 날짜)
private func fetchRoutines(startDate: Date, endDate: Date) {
Task {
do {
let entities = try await routineUseCase.fetchRoutines(startDate: startDate, endDate: endDate)
Expand All @@ -154,35 +188,11 @@ final class HomeViewModel: ViewModel {
}
fetchRoutineResultSubject.send(true)
} catch {

// TODO: 에러 처리
}
}
}

// 날짜를 선택하고 그 날에 해당하는 루틴을 불러옵니다.
private func selectDate(date: Date) {
selectedDateSubject.send(date)
fetchRoutines(for: date)
}

// 선택한 날의 루틴을 필터링하여 보여줍니다. (oldestDate, latestDate 업데이트)
private func fetchRoutines(for date: Date) {
if date <= oldestDate {
oldestDate = calendar.date(byAdding: .weekOfYear, value: -1, to: date) ?? date
latestDate = calendar.date(byAdding: .day, value: -1, to: date) ?? date
} else if date >= latestDate {
oldestDate = calendar.date(byAdding: .day, value: 1, to: date) ?? date
latestDate = calendar.date(byAdding: .weekOfYear, value: 1, to: date) ?? date
}

let dateKey = date.convertToString(dateType: .yearMonthDate)
guard let dailyRoutines = routines[dateKey] else {
fetchRoutines()
return
}
routinesSubject.send(dailyRoutines)
}

// 반복 루틴을 삭제합니다.
private func deleteAllRoutine() {
guard let routineId = selectedRoutineSubject.value?.id
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,7 @@ extension RecommendedRoutineView: FloatingMenuViewDelegate {
fatalError("routineCreationViewModel 의존성이 등록되지 않았습니다.")
}
let routineCreationView = RoutineCreationView(viewModel: routineCreationViewModel)
routineCreationView.hidesBottomBarWhenPushed = true
self.navigationController?.pushViewController(routineCreationView, animated: true)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -368,6 +368,7 @@ final class ResultRecommendedRoutineView: BaseViewController<ResultRecommendedRo
guard let routineCreationViewModel = DIContainer.shared.resolve(type: RoutineCreationViewModel.self)
else { fatalError("routineCreationViewModel 의존성이 등록되지 않았습니다.") }
let routineCreationView = RoutineCreationView(viewModel: routineCreationViewModel, recommendRoutineId: routineId)
routineCreationView.hidesBottomBarWhenPushed = true
self.navigationController?.pushViewController(routineCreationView, animated: true)
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,8 @@ final class RoutineCreationInputView: UIView {
textField.delegate = self
textField.textColor = .black
textField.font = BitnagilFont.init(style: .body2, weight: .semiBold).font
textField.returnKeyType = .done
textField.addTarget(self, action: #selector(textFieldEditingChanged(_:)), for: .editingChanged)

deleteButton.setImage(BitnagilIcon.deleteIcon, for: .normal)
deleteButton.addAction(
Expand Down Expand Up @@ -93,13 +95,15 @@ final class RoutineCreationInputView: UIView {
func configure(title: String) {
textField.text = title
}

@objc private func textFieldEditingChanged(_ sender: UITextField) {
delegate?.routineCreationInputView(self, didChangeText: sender.text ?? "")
}
}

extension RoutineCreationInputView: UITextFieldDelegate {
func textField(_ textField: UITextField, shouldChangeCharactersIn range: NSRange, replacementString string: String) -> Bool {
if let text = textField.text {
delegate?.routineCreationInputView(self, didChangeText: text)
}
func textFieldShouldReturn(_ textField: UITextField) -> Bool {
textField.resignFirstResponder()
return true
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -621,6 +621,8 @@ final class RoutineCreationView: BaseViewController<RoutineCreationViewModel> {
repeatInfoButton.isSelected = false
repeatToolTipView.hideTooltip()
}

view.endEditing(true)
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -272,12 +272,7 @@ final class RoutineCreationViewModel: ViewModel {
guard
let name = nameSubject.value,
!name.isEmpty,
executionTimeSubject.value != .none,
weekDaySubject.value.count > 0,
subRoutinesSubject
.value
.map({$0.subRoutineName})
.allSatisfy({$0?.isEmpty == false })
executionTimeSubject.value != .none
else {
checkRoutinePublisher.send(false)
return
Expand Down
2 changes: 2 additions & 0 deletions SupportingFiles/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -62,5 +62,7 @@
</dict>
<key>UILaunchStoryboardName</key>
<string>LaunchScreen</string>
<key>UIRequiresFullScreen</key>
<true/>
</dict>
</plist>
Loading