-
Notifications
You must be signed in to change notification settings - Fork 0
[#134] 사용자 설정으로 시간을 바꿀 때 확인 버튼을 탭하면 서버에 변동사항이 저장되도록 개선한다 #139
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
291cc1d
23090a2
ecc66fb
6947bc6
4c0e7d7
2106322
6a0c067
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -10,7 +10,8 @@ import Foundation | |||||||||||||||||
| final class PushNotificationSettingsViewModel: Store { | ||||||||||||||||||
| struct State { | ||||||||||||||||||
| var pushNotificationEnable: Bool = false | ||||||||||||||||||
| var pushNotificationTime: Date = .init() | ||||||||||||||||||
| var viewPushNotificationTime: Date = .init() | ||||||||||||||||||
| var sheetPushNotificationTime: Date = .init() | ||||||||||||||||||
| var showTimePicker: Bool = false | ||||||||||||||||||
| var isLoading: Bool = false | ||||||||||||||||||
| var sheetHeight: CGFloat = .pi | ||||||||||||||||||
|
|
@@ -19,31 +20,33 @@ final class PushNotificationSettingsViewModel: Store { | |||||||||||||||||
| var alertTitle: String = "" | ||||||||||||||||||
| var alertMessage: String = "" | ||||||||||||||||||
| var pushNotificationHour: Int { | ||||||||||||||||||
| Calendar.current.component(.hour, from: pushNotificationTime) | ||||||||||||||||||
| Calendar.current.component(.hour, from: viewPushNotificationTime) | ||||||||||||||||||
| } | ||||||||||||||||||
| var pushNotificationMinute: Int { | ||||||||||||||||||
| Calendar.current.component(.minute, from: pushNotificationTime) | ||||||||||||||||||
| Calendar.current.component(.minute, from: viewPushNotificationTime) | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| enum Action { | ||||||||||||||||||
| case onAppear | ||||||||||||||||||
| case fetchSettings | ||||||||||||||||||
| case setAlert(Bool) | ||||||||||||||||||
| case setLoading(Bool) | ||||||||||||||||||
| case setPushNotificationEnable(Bool) | ||||||||||||||||||
| case setPushNotificationHour(Int) | ||||||||||||||||||
| case setPushNotificationTime(Date) | ||||||||||||||||||
| case setPushNotificationTime(view: Date? = nil, sheet: Date? = nil) | ||||||||||||||||||
| case setShowTimePicker(Bool) | ||||||||||||||||||
| case setSheetHeight(CGFloat) | ||||||||||||||||||
| case selectPresetTime(Date) | ||||||||||||||||||
| case confirmUpdate | ||||||||||||||||||
| case rollbackUpdate | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| enum SideEffect { | ||||||||||||||||||
| case fetchPushNotificationSettings | ||||||||||||||||||
| case updatePushNotificationSettings | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| private let calendar = Calendar.current | ||||||||||||||||||
| @Published private(set) var state: State = .init() | ||||||||||||||||||
| private let calendar = Calendar.current | ||||||||||||||||||
| private let fetchPushSettingsUseCase: FetchPushSettingsUseCase | ||||||||||||||||||
| private let updatePushSettingsUseCase: UpdatePushSettingsUseCase | ||||||||||||||||||
|
|
||||||||||||||||||
|
|
@@ -57,36 +60,45 @@ final class PushNotificationSettingsViewModel: Store { | |||||||||||||||||
|
|
||||||||||||||||||
| func reduce(with action: Action) -> [SideEffect] { | ||||||||||||||||||
| var state = self.state | ||||||||||||||||||
| var effects: [SideEffect] = [] | ||||||||||||||||||
| switch action { | ||||||||||||||||||
| case .onAppear: | ||||||||||||||||||
| return [.fetchPushNotificationSettings] | ||||||||||||||||||
| case .fetchSettings: | ||||||||||||||||||
| effects = [.fetchPushNotificationSettings] | ||||||||||||||||||
| case .setAlert(let isPresented): | ||||||||||||||||||
| setAlert(&state, isPresented: isPresented) | ||||||||||||||||||
| case .setLoading(let value): | ||||||||||||||||||
| state.isLoading = value | ||||||||||||||||||
| case .setPushNotificationEnable(let value): | ||||||||||||||||||
| self.state.pushNotificationEnable = value | ||||||||||||||||||
| return [.updatePushNotificationSettings] | ||||||||||||||||||
| case .setPushNotificationHour(let value): | ||||||||||||||||||
| // 시간만 변경 | ||||||||||||||||||
| if let newDate = calendar.date( | ||||||||||||||||||
| bySettingHour: value, | ||||||||||||||||||
| minute: 0, second: 0, | ||||||||||||||||||
| of: state.pushNotificationTime | ||||||||||||||||||
| ) { | ||||||||||||||||||
| self.state.pushNotificationTime = newDate | ||||||||||||||||||
| return [.updatePushNotificationSettings] | ||||||||||||||||||
| state.pushNotificationEnable = value | ||||||||||||||||||
| effects = [.updatePushNotificationSettings] | ||||||||||||||||||
| case .setPushNotificationTime(let view, let sheet): | ||||||||||||||||||
| if let value = view { | ||||||||||||||||||
| state.viewPushNotificationTime = value | ||||||||||||||||||
| } | ||||||||||||||||||
| if let value = sheet { | ||||||||||||||||||
| state.sheetPushNotificationTime = value | ||||||||||||||||||
| } | ||||||||||||||||||
| case .setPushNotificationTime(let value): | ||||||||||||||||||
| self.state.pushNotificationTime = value | ||||||||||||||||||
| return [.updatePushNotificationSettings] | ||||||||||||||||||
| case .setShowTimePicker(let value): | ||||||||||||||||||
| state.showTimePicker = value | ||||||||||||||||||
| if !value { | ||||||||||||||||||
| state.sheetPushNotificationTime = state.viewPushNotificationTime | ||||||||||||||||||
| } | ||||||||||||||||||
| case .setSheetHeight(let value): | ||||||||||||||||||
| state.sheetHeight = value | ||||||||||||||||||
| case .selectPresetTime(let date): | ||||||||||||||||||
| state.viewPushNotificationTime = date | ||||||||||||||||||
| state.sheetPushNotificationTime = date | ||||||||||||||||||
| effects = [.updatePushNotificationSettings] | ||||||||||||||||||
| case .confirmUpdate: | ||||||||||||||||||
| state.showTimePicker = false | ||||||||||||||||||
| state.viewPushNotificationTime = state.sheetPushNotificationTime | ||||||||||||||||||
| effects = [.updatePushNotificationSettings] | ||||||||||||||||||
| case .rollbackUpdate: | ||||||||||||||||||
| state.showTimePicker = false | ||||||||||||||||||
| state.sheetPushNotificationTime = state.viewPushNotificationTime | ||||||||||||||||||
| } | ||||||||||||||||||
| self.state = state | ||||||||||||||||||
| return [] | ||||||||||||||||||
| return effects | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| func run(_ effect: SideEffect) { | ||||||||||||||||||
|
|
@@ -101,7 +113,7 @@ final class PushNotificationSettingsViewModel: Store { | |||||||||||||||||
| if let hour = settings.scheduledTime.hour, | ||||||||||||||||||
| let minute = settings.scheduledTime.minute, | ||||||||||||||||||
| let date = calendar.date(bySettingHour: hour, minute: minute, second: 0, of: Date()) { | ||||||||||||||||||
| self.send(.setPushNotificationTime(date)) | ||||||||||||||||||
| self.send(.setPushNotificationTime(view: date, sheet: date)) | ||||||||||||||||||
| } | ||||||||||||||||||
| } catch { | ||||||||||||||||||
| send(.setAlert(true)) | ||||||||||||||||||
|
|
@@ -112,14 +124,18 @@ final class PushNotificationSettingsViewModel: Store { | |||||||||||||||||
| do { | ||||||||||||||||||
| defer { send(.setLoading(false)) } | ||||||||||||||||||
| send(.setLoading(true)) | ||||||||||||||||||
| let dateComponents = calendar.dateComponents([.hour, .minute], from: state.pushNotificationTime) | ||||||||||||||||||
| let dateComponents = calendar.dateComponents( | ||||||||||||||||||
| [.hour, .minute], | ||||||||||||||||||
| from: state.sheetPushNotificationTime | ||||||||||||||||||
| ) | ||||||||||||||||||
|
Comment on lines
+127
to
+130
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
|
||||||||||||||||||
| let settings = PushNotificationSettings( | ||||||||||||||||||
| isEnabled: state.pushNotificationEnable, | ||||||||||||||||||
| scheduledTime: dateComponents | ||||||||||||||||||
| ) | ||||||||||||||||||
| try await updatePushSettingsUseCase.execute(settings) | ||||||||||||||||||
| } catch { | ||||||||||||||||||
| send(.setAlert(true)) | ||||||||||||||||||
| send(.fetchSettings) | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
| } | ||||||||||||||||||
|
|
||||||||||||||||||
| Original file line number | Diff line number | Diff line change | ||||
|---|---|---|---|---|---|---|
|
|
@@ -37,14 +37,14 @@ struct PushNotificationSettingsView: View { | |||||
| } | ||||||
| .contentShape(Rectangle()) | ||||||
| .onTapGesture { | ||||||
| viewModel.send(.setPushNotificationHour(hour)) | ||||||
| viewModel.send(.selectPresetTime(date)) | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| HStack { | ||||||
| Text("사용자 설정") | ||||||
| Spacer() | ||||||
| Text(formattedTimeString(viewModel.state.pushNotificationTime)) | ||||||
| Text(formattedTimeString(viewModel.state.viewPushNotificationTime)) | ||||||
| .foregroundStyle(.secondary) | ||||||
| if viewModel.state.pushNotificationMinute != 0 { | ||||||
| Image(systemName: "checkmark") | ||||||
|
|
@@ -67,37 +67,70 @@ struct PushNotificationSettingsView: View { | |||||
| } | ||||||
| } | ||||||
| .onAppear { | ||||||
| viewModel.send(.onAppear) | ||||||
| viewModel.send(.fetchSettings) | ||||||
| } | ||||||
| .sheet(isPresented: Binding( | ||||||
| get: { viewModel.state.showTimePicker }, | ||||||
| set: { _ in viewModel.send(.setShowTimePicker(false)) } | ||||||
| set: { viewModel.send(.setShowTimePicker($0)) } | ||||||
| )) { | ||||||
| DatePicker( | ||||||
| "", | ||||||
| selection: Binding( | ||||||
| get: { viewModel.state.pushNotificationTime }, | ||||||
| set: { viewModel.send(.setPushNotificationTime($0)) } | ||||||
| ), | ||||||
| displayedComponents: .hourAndMinute | ||||||
| ) | ||||||
| .datePickerStyle(.wheel) | ||||||
| .labelsHidden() | ||||||
| .presentationDragIndicator(.hidden) | ||||||
| .presentationDetents([.height(viewModel.state.sheetHeight)]) | ||||||
| .onAppear { | ||||||
| UIDatePicker.appearance().minuteInterval = 5 | ||||||
| NavigationStack { | ||||||
| DatePicker( | ||||||
| "", | ||||||
| selection: Binding( | ||||||
| get: { viewModel.state.sheetPushNotificationTime }, | ||||||
| set: { viewModel.send(.setPushNotificationTime(sheet: $0)) } | ||||||
| ), | ||||||
| displayedComponents: .hourAndMinute | ||||||
| ) | ||||||
| .datePickerStyle(.wheel) | ||||||
| .labelsHidden() | ||||||
| .presentationDragIndicator(.hidden) | ||||||
| .presentationDetents([.height(viewModel.state.sheetHeight)]) | ||||||
| .onAppear { UIDatePicker.appearance().minuteInterval = 5 } | ||||||
| .onDisappear { UIDatePicker.appearance().minuteInterval = 1 /* 기본값으로 복원 */ } | ||||||
| .toolbar { toolbar } | ||||||
| .background( | ||||||
| GeometryReader { geometry in | ||||||
| Color.clear.onAppear { | ||||||
| viewModel.send(.setSheetHeight(geometry.size.height)) | ||||||
| } | ||||||
| } | ||||||
| ) | ||||||
| } | ||||||
| .onDisappear { | ||||||
| UIDatePicker.appearance().minuteInterval = 1 // 기본값으로 복원 | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
| @ToolbarContentBuilder | ||||||
| private var toolbar: some ToolbarContent { | ||||||
| if #available(iOS 26.0, *) { | ||||||
|
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. iOS 버전 분기 처리에
Suggested change
|
||||||
| ToolbarItem(placement: .topBarLeading) { | ||||||
| Button(role: .cancel) { | ||||||
| viewModel.send(.rollbackUpdate) | ||||||
| } | ||||||
| } | ||||||
| .background( | ||||||
| GeometryReader { geometry in | ||||||
| Color.clear.onAppear { | ||||||
| viewModel.send(.setSheetHeight(geometry.size.height)) | ||||||
| } | ||||||
|
|
||||||
| ToolbarItem(placement: .topBarTrailing) { | ||||||
| Button(role: .confirm) { | ||||||
| viewModel.send(.confirmUpdate) | ||||||
| } | ||||||
| } | ||||||
| } else { | ||||||
| ToolbarItem(placement: .topBarLeading) { | ||||||
| Button { | ||||||
| viewModel.send(.rollbackUpdate) | ||||||
| } label: { | ||||||
| Text("취소") | ||||||
| } | ||||||
| ) | ||||||
| } | ||||||
|
|
||||||
| ToolbarItem(placement: .topBarTrailing) { | ||||||
| Button { | ||||||
| viewModel.send(.confirmUpdate) | ||||||
| } label: { | ||||||
| Text("확인") | ||||||
| .bold() | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
| } | ||||||
|
|
||||||
|
|
||||||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
setPushNotificationEnable액션이 더 이상updatePushNotificationSettings사이드 이펙트를 발생시키지 않습니다. 이로 인해 사용자가 푸시 알림 활성화 토글을 눌러도 변경사항이 서버에 즉시 저장되지 않는 문제가 발생합니다. 토글 스위치는 즉시 반응하여 서버에 저장되는 것이 일반적인 사용자 경험에 부합하므로, 이전과 같이 사이드 이펙트를 발생시켜야 합니다.