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
Original file line number Diff line number Diff line change
Expand Up @@ -124,7 +124,7 @@ final class DateView: UIView {
self.isSelected = isSelected
}

func updateAllCompleted() {
allCompletedIcon.isHidden.toggle()
func updateAllCompleted(isCompleted: Bool) {
allCompletedIcon.isHidden = !isCompleted
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@ final class WeekView: UIView {

let isAllCompleted = allCompletedDates.contains(date)
if isAllCompleted {
dateView.updateAllCompleted()
dateView.updateAllCompleted(isCompleted: true)
}
dateView.didTapDateButton = { [weak self] date in
self?.selectDate(date: date)
Expand All @@ -109,7 +109,9 @@ final class WeekView: UIView {
self.allCompletedDates = allCompletedDates
for dateView in dateViews {
if allCompletedDates.contains(dateView.key) {
dateView.value.updateAllCompleted()
dateView.value.updateAllCompleted(isCompleted: true)
} else {
dateView.value.updateAllCompleted(isCompleted: false)
}
}
}
Expand Down
12 changes: 12 additions & 0 deletions Projects/Presentation/Sources/Home/View/HomeViewController.swift
Original file line number Diff line number Diff line change
Expand Up @@ -74,6 +74,7 @@ final class HomeViewController: BaseViewController<HomeViewModel> {

// routineView
private let emptyView = HomeEmptyView()
private let refreshControl = UIRefreshControl()
private let routineHeaderView = UIView()
private let routineListLabel = UILabel()
private let routineListButton = UIButton()
Expand Down Expand Up @@ -181,6 +182,12 @@ final class HomeViewController: BaseViewController<HomeViewModel> {
self.navigationController?.pushViewController(routineCreationView, animated: true)
}

routineScrollView.refreshControl = refreshControl
refreshControl.addTarget(
self,
action: #selector(pullToRoutineRefresh),
for: .valueChanged)

routineListLabel.text = "루틴 리스트"
routineListLabel.font = BitnagilFont(style: .body2, weight: .semiBold).font
routineListLabel.textColor = BitnagilColor.gray60
Expand Down Expand Up @@ -433,6 +440,7 @@ final class HomeViewController: BaseViewController<HomeViewModel> {
.receive(on: DispatchQueue.main)
.sink { [weak self] routines in
self?.updateRoutineView(routines: routines)
self?.routineScrollView.refreshControl?.endRefreshing()
self?.hideIndicatorView()
Comment on lines +443 to 444
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

네트워크 오류 시 refreshControl이 종료되지 않아 스피너가 무한히 표시됨

endRefreshing()routinesPublisher 구독 내에서만 호출됩니다. refreshDailyRoutine 액션이 네트워크 오류로 실패하면:

  1. networkErrorPublisher로 에러 알럿이 표시됨
  2. routinesPublisher는 방출되지 않음
  3. endRefreshing()이 호출되지 않아 리프레시 스피너가 무한히 계속 돌아감

networkErrorPublisher를 처리하는 경로에서도 endRefreshing()을 호출해야 합니다.

🐛 수정 방향 제안

bind() 내에서 networkErrorPublisher에 추가 구독을 연결하거나, bindNetworkError 호출 전에 별도 처리:

+        viewModel.output.networkErrorPublisher
+            .receive(on: DispatchQueue.main)
+            .sink { [weak self] _ in
+                self?.routineScrollView.refreshControl?.endRefreshing()
+            }
+            .store(in: &cancellables)
+
         bindNetworkError(from: viewModel.output.networkErrorPublisher)

또는 bindNetworkError 구현 내부에서 콜백으로 처리하는 방식도 가능합니다.

📝 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
self?.routineScrollView.refreshControl?.endRefreshing()
self?.hideIndicatorView()
viewModel.output.networkErrorPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] _ in
self?.routineScrollView.refreshControl?.endRefreshing()
}
.store(in: &cancellables)
bindNetworkError(from: viewModel.output.networkErrorPublisher)
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@Projects/Presentation/Sources/Home/View/HomeViewController.swift` around
lines 443 - 444, `routinesPublisher`가 방출되지 않을 때(네트워크 실패)
`routineScrollView.refreshControl?.endRefreshing()`가 호출되지 않아 스피너가 멈추지 않으므로,
`bind()` 내부에서 `networkErrorPublisher`에 추가 구독을 만들어 네트워크 오류 수신 시
`routineScrollView.refreshControl?.endRefreshing()`와 `hideIndicatorView()`를
호출하도록 처리하세요; 또는 `bindNetworkError`의 구현을 확장해 오류 콜백에 위 두 호출을 위임하도록 변경하여
`refreshDailyRoutine` 실패 경로에서도 항상 리프레시가 종료되게 만드세요 (참조:
routineScrollView.refreshControl?.endRefreshing(), routinesPublisher,
networkErrorPublisher, bind(), bindNetworkError, refreshDailyRoutine).

}
.store(in: &cancellables)
Expand Down Expand Up @@ -635,6 +643,10 @@ final class HomeViewController: BaseViewController<HomeViewModel> {
emotionRegistrationViewController.hidesBottomBarWhenPushed = true
navigationController?.pushViewController(emotionRegistrationViewController, animated: true)
}

@objc private func pullToRoutineRefresh() {
viewModel.action(input: .refreshDailyRoutine)
}
}

// MARK: WeekViewDelegate
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -146,19 +146,6 @@ final class RoutineCreationViewController: BaseViewController<RoutineCreationVie
UIAction { [weak self] _ in
guard let self else { return }
self.viewModel.action(input: .registerRoutine)
if self.isFromMypage {
if
let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first(where: { $0.isKeyWindow }),
let tabBarView = window.rootViewController as? TabBarView {
self.navigationController?.popToRootViewController(animated: false)
tabBarView.selectedIndex = 1
viewModel.action(input: .showRecommendedRoutineToastMessageView)
}
} else {
viewModel.action(input: .showUpdateRoutineToastMessageView)
self.navigationController?.popViewController(animated: true)
}
},
for: .touchUpInside)
bindCreationCardViews()
Expand Down Expand Up @@ -314,6 +301,28 @@ final class RoutineCreationViewController: BaseViewController<RoutineCreationVie
}
}
.store(in: &cancellables)

viewModel.output.routineCreationResultPublisher
.receive(on: DispatchQueue.main)
.sink { [weak self] creationResult in
guard let self else { return }
if creationResult {
if self.isFromMypage {
if
let windowScene = UIApplication.shared.connectedScenes.first as? UIWindowScene,
let window = windowScene.windows.first(where: { $0.isKeyWindow }),
let tabBarView = window.rootViewController as? TabBarView {
self.navigationController?.popToRootViewController(animated: false)
tabBarView.selectedIndex = 1
viewModel.action(input: .showRecommendedRoutineToastMessageView)
}
} else {
viewModel.action(input: .showUpdateRoutineToastMessageView)
self.navigationController?.popViewController(animated: true)
}
Comment on lines +319 to +322
Copy link

Choose a reason for hiding this comment

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

⚠️ Potential issue | 🟠 Major

🧩 Analysis chain

🏁 Script executed:

# Search for showUpdatedRoutineToast usage and related message display
rg -n "showUpdatedRoutineToast" --type swift -A 5

Repository: YAPP-Github/Bitnagil-iOS

Length of output: 2898


🏁 Script executed:

# Search for Korean text related to routine update/creation toast message
rg -n "수정|생성|등록" --type swift | grep -i toast

Repository: YAPP-Github/Bitnagil-iOS

Length of output: 212


🏁 Script executed:

# Search for ToastMessage or toast-related views
rg -n "ToastMessage|ToastView" --type swift

Repository: YAPP-Github/Bitnagil-iOS

Length of output: 4523


🏁 Script executed:

# Check RoutineCreationViewController to see if there's different handling for new vs update
cat -n Projects/Presentation/Sources/RoutineCreation/View/RoutineCreationViewController.swift | sed -n '305,330p'

Repository: YAPP-Github/Bitnagil-iOS

Length of output: 1546


🏁 Script executed:

# Check if there's a separate toast message for routine creation (vs update)
rg -n "루틴.*등록|루틴.*생성|routine.*creat" --type swift -i | head -20

Repository: YAPP-Github/Bitnagil-iOS

Length of output: 3256


루틴 신규 생성 시 토스트 메시지가 "수정" 관련 문구로 표시되는 문제

isFromMypage == false 분기(라인 320)에서 호출하는 showUpdateRoutineToastMessageView는 RoutineListViewController에서 "루틴 수정이 완료되었습니다." 토스트를 표시합니다. 그런데 이 분기는 신규 루틴 생성(updateInfo == nil)도 포함하므로, 신규 등록 시에 수정 관련 문구가 표시되는 것이 부적절합니다.

신규 생성과 수정을 구분하여 각각 적절한 토스트 메시지("루틴 생성이 완료되었습니다." vs "루틴 수정이 완료되었습니다.")를 표시하도록 수정이 필요합니다.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In
`@Projects/Presentation/Sources/RoutineCreation/View/RoutineCreationViewController.swift`
around lines 319 - 322, The toast shown on completion uses
viewModel.action(input: .showUpdateRoutineToastMessageView) regardless of
whether a routine was created or updated; change the branch in
RoutineCreationViewController (the isFromMypage == false path) to check
updateInfo == nil and call a distinct action for creation (e.g.,
.showCreateRoutineToastMessageView) when new, and only call
.showUpdateRoutineToastMessageView when updateInfo != nil; if the
ToastAction/Action enum or viewModel lacks a create case, add
.showCreateRoutineToastMessageView and implement handling where Toast actions
are consumed so the correct "생성" vs "수정" message is displayed, then keep the
existing popViewController(animated: true) behavior.

}
}
.store(in: &cancellables)
}

private func bindCreationCardViews() {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,7 @@ final class RoutineCreationViewModel: ViewModel {
let periodPublisher: AnyPublisher<(Date?, Date?), Never>
let executionTimePublisher: AnyPublisher<Date?, Never>
let isRoutineValid: AnyPublisher<Bool, Never>
let routineCreationResultPublisher: AnyPublisher<Bool, Never>
let networkErrorPublisher: AnyPublisher<(() -> Void)?, Never>
}

Expand All @@ -55,6 +56,7 @@ final class RoutineCreationViewModel: ViewModel {
private let periodEndSubject = CurrentValueSubject<Date?, Never>(nil)
private let executionTimeSubject = CurrentValueSubject<ExecutionTime, Never>(.init(startAt: nil))
private let checkRoutinePublisher = CurrentValueSubject<Bool, Never>(false)
private let routineCreationResultSubject = PassthroughSubject<Bool, Never>()
private let routineUseCase: RoutineUseCaseProtocol
private let networkRetryHandler: NetworkRetryHandler
private let recommenededRoutineUseCase: RecommendedRoutineUseCaseProtocol
Expand Down Expand Up @@ -82,6 +84,7 @@ final class RoutineCreationViewModel: ViewModel {
.map { $0.startAt }
.eraseToAnyPublisher(),
isRoutineValid: checkRoutinePublisher.eraseToAnyPublisher(),
routineCreationResultPublisher: routineCreationResultSubject.eraseToAnyPublisher(),
networkErrorPublisher: networkRetryHandler.networkErrorActionSubject.eraseToAnyPublisher())

updateIsRoutineValid()
Expand Down Expand Up @@ -302,8 +305,10 @@ final class RoutineCreationViewModel: ViewModel {

try await routineUseCase.saveRoutine(routine: routine)

routineCreationResultSubject.send(true)
networkRetryHandler.clearRetryState()
} catch {
routineCreationResultSubject.send(false)
networkRetryHandler.handleNetworkError(error) { [weak self] in
self?.registerRoutine()
}
Expand Down