From 43c918d23604aaa338aae22fb94c72e7014be129 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Mon, 1 Jun 2026 17:14:43 +0900 Subject: [PATCH 1/4] =?UTF-8?q?ui:=20ProfileView=EC=97=90=EC=84=9C=20?= =?UTF-8?q?=ED=99=9C=EB=8F=99=EC=9D=84=20=EC=84=A0=ED=83=9D=ED=95=98?= =?UTF-8?q?=EB=A9=B4=20detail=20=ED=83=AD=EC=97=90=EC=84=9C=20=EB=B3=BC=20?= =?UTF-8?q?=EC=88=98=20=EC=9E=88=EB=8F=84=EB=A1=9D=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Resource/Localizable.xcstrings | 19 +- .../Sources/Main/MainView.swift | 151 ++++++----- .../Sources/Profile/ProfileView.swift | 238 +++++++++--------- 3 files changed, 215 insertions(+), 193 deletions(-) diff --git a/Application/DevLogApp/Sources/Resource/Localizable.xcstrings b/Application/DevLogApp/Sources/Resource/Localizable.xcstrings index 720e6cea..fba808af 100644 --- a/Application/DevLogApp/Sources/Resource/Localizable.xcstrings +++ b/Application/DevLogApp/Sources/Resource/Localizable.xcstrings @@ -966,6 +966,23 @@ } } }, + "profile_select_detail" : { + "extractionState" : "manual", + "localizations" : { + "en" : { + "stringUnit" : { + "state" : "translated", + "value" : "Select an activity." + } + }, + "ko" : { + "stringUnit" : { + "state" : "translated", + "value" : "활동을 선택해주세요." + } + } + } + }, "profile_select_quarter" : { "extractionState" : "manual", "localizations" : { @@ -3451,4 +3468,4 @@ } }, "version" : "1.0" -} \ No newline at end of file +} diff --git a/Application/DevLogPresentation/Sources/Main/MainView.swift b/Application/DevLogPresentation/Sources/Main/MainView.swift index fef09bd7..184c163f 100644 --- a/Application/DevLogPresentation/Sources/Main/MainView.swift +++ b/Application/DevLogPresentation/Sources/Main/MainView.swift @@ -106,65 +106,58 @@ struct MainView: View { @ViewBuilder private func sidebarView(for selectedTab: MainTab) -> some View { - switch selectedTab.mainTabSplitStyle { - case .detailOnly: + switch selectedTab { + case .home: NavigationSplitView { mainSidebar + } content: { + homeView } detail: { - selectedTabView(for: selectedTab) + homeRegularDetailView } - case .contentDetail: - switch selectedTab { - case .home: - NavigationSplitView { - mainSidebar - } content: { - homeView - } detail: { - homeRegularDetailView - } - .environment(homeViewCoordinator.router) - case .today: - NavigationSplitView { - mainSidebar - } content: { - todayView - } detail: { - todayRegularDetailView - } - case .notification: - NavigationSplitView { - mainSidebar - } content: { - PushNotificationListView( - coordinator: pushNotificationListViewCoordinator, - isCompactLayout: isCompactLayout - ) - } detail: { - Group { - if let todoId = pushNotificationListViewCoordinator.todoIdToPresent?.id { - TodoDetailView( - viewModel: pushNotificationListViewCoordinator.makeTodoDetailViewModel( - todoId: todoId - ) - ) - .id(todoId) - } else { - ContentUnavailableView( - String(localized: "push_notifications_select_detail"), - systemImage: "bell.badge" + .environment(homeViewCoordinator.router) + case .today: + NavigationSplitView { + mainSidebar + } content: { + todayView + } detail: { + todayRegularDetailView + } + case .notification: + NavigationSplitView { + mainSidebar + } content: { + PushNotificationListView( + coordinator: pushNotificationListViewCoordinator, + isCompactLayout: isCompactLayout + ) + } detail: { + Group { + if let todoId = pushNotificationListViewCoordinator.todoIdToPresent?.id { + TodoDetailView( + viewModel: pushNotificationListViewCoordinator.makeTodoDetailViewModel( + todoId: todoId ) - } + ) + .id(todoId) + } else { + ContentUnavailableView( + String(localized: "push_notifications_select_detail"), + systemImage: "bell.badge" + ) } - .background(Color(.secondarySystemBackground).ignoresSafeArea()) - } - case .profile: - NavigationSplitView { - mainSidebar - } detail: { - selectedTabView(for: selectedTab) } } + .background(Color(.secondarySystemBackground).ignoresSafeArea()) + case .profile: + NavigationSplitView { + mainSidebar + } content: { + profileView + } detail: { + profileRegularDetailView + } } } @@ -178,20 +171,6 @@ struct MainView: View { .listStyle(.sidebar) } - @ViewBuilder - private func selectedTabView(for selectedTab: MainTab) -> some View { - switch selectedTab { - case .home: - homeView - case .today: - todayView - case .notification: - notificationView - case .profile: - profileView - } - } - @ViewBuilder private func sidebarRow(_ tab: MainTab) -> some View { if tab == .notification { @@ -341,7 +320,25 @@ struct MainView: View { } private var profileView: some View { - ProfileView(coordinator: profileViewCoordinator) + ProfileView( + coordinator: profileViewCoordinator, + isCompactLayout: isCompactLayout + ) + } + + private var profileRegularDetailView: some View { + Group { + if let todoId = profileSelectedTodoId { + TodoDetailView(viewModel: profileViewCoordinator.makeTodoDetailViewModel(todoId: todoId)) + .id(todoId) + } else { + ContentUnavailableView( + String(localized: "profile_select_detail"), + systemImage: "person.crop.circle" + ) + } + } + .background(Color(.secondarySystemBackground).ignoresSafeArea()) } } @@ -396,23 +393,17 @@ private extension MainView { ) } -} - -private enum MainTabSplitStyle { - case detailOnly - case contentDetail -} - -private extension MainTab { - var mainTabSplitStyle: MainTabSplitStyle { - switch self { - case .home, .today, .notification: - .contentDetail - case .profile: - .detailOnly + var profileSelectedTodoId: String? { + for route in profileViewCoordinator.router.path.reversed() { + if case let .activity(todoId) = route { + return todoId + } } + return nil } +} +private extension MainTab { var title: String { switch self { case .home: diff --git a/Application/DevLogPresentation/Sources/Profile/ProfileView.swift b/Application/DevLogPresentation/Sources/Profile/ProfileView.swift index b19f37d4..47d912c5 100644 --- a/Application/DevLogPresentation/Sources/Profile/ProfileView.swift +++ b/Application/DevLogPresentation/Sources/Profile/ProfileView.swift @@ -11,137 +11,151 @@ import DevLogDomain struct ProfileView: View { let coordinator: ProfileViewCoordinator + let isCompactLayout: Bool @FocusState private var focused: Bool var body: some View { - NavigationStack(path: navigationPath) { - ScrollView { - LazyVStack(alignment: .leading, spacing: 16) { - HStack { - CacheableImage(url: coordinator.viewModel.state.avatarURL) { - Image(systemName: "person.crop.circle.fill") - .resizable() - .scaledToFill() - .foregroundStyle(Color(.systemGray2)) + Group { + if isCompactLayout { + NavigationStack(path: navigationPath) { + profileContentView + .toolbar { + ToolbarItem(placement: .topBarTrailing) { + Button { + coordinator.router.push(.settings) + } label: { + Image(systemName: "gearshape") + } + } } - .frame(width: 60, height: 60) - .cornerRadius(30) - .foregroundStyle(Color.gray) - - VStack(alignment: .leading) { - Text(coordinator.viewModel.state.name) - .font(.title2) - .bold() - Text(coordinator.viewModel.state.email) - .font(.caption2) - .foregroundStyle(Color.gray) + .navigationDestination(for: ProfileRoute.self) { route in + profileDestinationView(route) } + } + } else { + profileContentView + } + } + .onChange(of: focused) { _, newValue in + withAnimation { + coordinator.viewModel.send(.updateStatusTextFieldFocus(newValue)) + } + } + .alert( + "", + isPresented: Binding( + get: { coordinator.viewModel.state.showAlert }, + set: { coordinator.viewModel.send(.setAlert($0)) } + ) + ) { + Button(String(localized: "common_close"), role: .cancel) { } + } message: { + Text(coordinator.viewModel.state.alertMessage) + } + .sheet( + isPresented: Binding( + get: { coordinator.viewModel.state.showQuarterPicker }, + set: { coordinator.viewModel.send(.setQuarterPickerPresented($0)) } + ) + ) { + quarterPickerSheet + } + } + + private var profileContentView: some View { + ScrollView { + LazyVStack(alignment: .leading, spacing: 16) { + HStack { + CacheableImage(url: coordinator.viewModel.state.avatarURL) { + Image(systemName: "person.crop.circle.fill") + .resizable() + .scaledToFill() + .foregroundStyle(Color(.systemGray2)) } - let connected = coordinator.viewModel.state.isNetworkConnected - HStack { - HStack { - Image(systemName: "face.smiling") - TextField( - text: Binding( - get: { coordinator.viewModel.state.statusMessage }, - set: { coordinator.viewModel.send(.updateStatusMessage($0)) } - ) - ) { - Text(String(localized: "profile_status_placeholder")) - } - .frame(height: UIFont.preferredFont(forTextStyle: .body).lineHeight) - .focused($focused) - .disabled(!connected) + .frame(width: 60, height: 60) + .cornerRadius(30) + .foregroundStyle(Color.gray) - if !coordinator.viewModel.state.statusMessage.isEmpty, - coordinator.viewModel.state.showDoneButton { - Button(action: { - coordinator.viewModel.send(.tapResetStatusMessageButton) - }) { - Image(systemName: "xmark.circle.fill") - } - .transition(.move(edge: .trailing).combined(with: .opacity)) - } + VStack(alignment: .leading) { + Text(coordinator.viewModel.state.name) + .font(.title2) + .bold() + Text(coordinator.viewModel.state.email) + .font(.caption2) + .foregroundStyle(Color.gray) + } + } + let connected = coordinator.viewModel.state.isNetworkConnected + HStack { + HStack { + Image(systemName: "face.smiling") + TextField( + text: Binding( + get: { coordinator.viewModel.state.statusMessage }, + set: { coordinator.viewModel.send(.updateStatusMessage($0)) } + ) + ) { + Text(String(localized: "profile_status_placeholder")) } - .foregroundStyle(Color.gray) - .padding(8) - .background( - RoundedRectangle(cornerRadius: 10) - .fill(Color(.secondarySystemGroupedBackground)) - ) - if coordinator.viewModel.state.showDoneButton { + .frame(height: UIFont.preferredFont(forTextStyle: .body).lineHeight) + .focused($focused) + .disabled(!connected) + + if !coordinator.viewModel.state.statusMessage.isEmpty, + coordinator.viewModel.state.showDoneButton { Button(action: { - focused = false - coordinator.viewModel.send(.willUpdateStatusMessage) + coordinator.viewModel.send(.tapResetStatusMessageButton) }) { - Text(String(localized: "profile_done")) + Image(systemName: "xmark.circle.fill") } .transition(.move(edge: .trailing).combined(with: .opacity)) } } - .opacity(connected ? 1 : 0.7) - activityHeatmapSection - } - .padding(.horizontal, 16) - } - .refreshable { coordinator.viewModel.send(.refresh) } - .frame(maxWidth: .infinity) - .background(Color(.systemGroupedBackground)) - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - HStack(spacing: 0) { - Button { - coordinator.router.push(.settings) - } label: { - Image(systemName: "gearshape") + .foregroundStyle(Color.gray) + .padding(8) + .background( + RoundedRectangle(cornerRadius: 10) + .fill(Color(.secondarySystemGroupedBackground)) + ) + if coordinator.viewModel.state.showDoneButton { + Button(action: { + focused = false + coordinator.viewModel.send(.willUpdateStatusMessage) + }) { + Text(String(localized: "profile_done")) } + .transition(.move(edge: .trailing).combined(with: .opacity)) } } + .opacity(connected ? 1 : 0.7) + activityHeatmapSection } - .navigationDestination(for: ProfileRoute.self) { route in - switch route { - case .settings: - SettingView(viewModel: coordinator.settingViewModel) - .environment(coordinator.router) - case .activity(let todoId): - TodoDetailView(viewModel: coordinator.makeTodoDetailViewModel(todoId: todoId)) - case .theme: - ThemeView( - theme: Binding( - get: { coordinator.settingViewModel.state.theme }, - set: { coordinator.settingViewModel.send(.setTheme($0)) } - ) - ) - case .pushNotification: - PushNotificationSettingsView(viewModel: coordinator.makePushNotificationSettingsViewModel()) - case .account: - AccountView(viewModel: coordinator.makeAccountViewModel()) - } - } - .onChange(of: focused) { _, newValue in - withAnimation { - coordinator.viewModel.send(.updateStatusTextFieldFocus(newValue)) - } - } - .alert( - "", - isPresented: Binding( - get: { coordinator.viewModel.state.showAlert }, - set: { coordinator.viewModel.send(.setAlert($0)) } - ) - ) { - Button(String(localized: "common_close"), role: .cancel) { } - } message: { - Text(coordinator.viewModel.state.alertMessage) - } - .sheet( - isPresented: Binding( - get: { coordinator.viewModel.state.showQuarterPicker }, - set: { coordinator.viewModel.send(.setQuarterPickerPresented($0)) } + .padding(.horizontal, 16) + } + .refreshable { coordinator.viewModel.send(.refresh) } + .frame(maxWidth: .infinity) + .background(Color(.systemGroupedBackground)) + } + + @ViewBuilder + private func profileDestinationView(_ route: ProfileRoute) -> some View { + switch route { + case .settings: + SettingView(viewModel: coordinator.settingViewModel) + .environment(coordinator.router) + case .activity(let todoId): + TodoDetailView(viewModel: coordinator.makeTodoDetailViewModel(todoId: todoId)) + case .theme: + ThemeView( + theme: Binding( + get: { coordinator.settingViewModel.state.theme }, + set: { coordinator.settingViewModel.send(.setTheme($0)) } ) - ) { - quarterPickerSheet - } + ) + case .pushNotification: + PushNotificationSettingsView(viewModel: coordinator.makePushNotificationSettingsViewModel()) + case .account: + AccountView(viewModel: coordinator.makeAccountViewModel()) } } From 2d1ffd917adffcd9dfd252380b644a13c335b604 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Mon, 1 Jun 2026 17:23:49 +0900 Subject: [PATCH 2/4] =?UTF-8?q?fix:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=ED=8C=A8=EB=84=90=20=EB=9D=BC=EC=9A=B0?= =?UTF-8?q?=ED=8C=85=20=EC=A1=B0=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Main/MainView.swift | 53 ++++++++++++++----- .../Sources/Profile/ProfileView.swift | 31 +++++++---- 2 files changed, 62 insertions(+), 22 deletions(-) diff --git a/Application/DevLogPresentation/Sources/Main/MainView.swift b/Application/DevLogPresentation/Sources/Main/MainView.swift index 184c163f..ee58a1f1 100644 --- a/Application/DevLogPresentation/Sources/Main/MainView.swift +++ b/Application/DevLogPresentation/Sources/Main/MainView.swift @@ -328,9 +328,22 @@ struct MainView: View { private var profileRegularDetailView: some View { Group { - if let todoId = profileSelectedTodoId { - TodoDetailView(viewModel: profileViewCoordinator.makeTodoDetailViewModel(todoId: todoId)) - .id(todoId) + if let profileRoute = profileViewCoordinator.router.root { + switch profileRoute { + case .activity(let todoId): + TodoDetailView(viewModel: profileViewCoordinator.makeTodoDetailViewModel(todoId: todoId)) + .id(todoId) + case .settings, .theme, .pushNotification, .account: + NavigationStack(path: Binding( + get: { profileViewCoordinator.router.detailPath }, + set: { profileViewCoordinator.router.detailPath = $0 } + )) { + profileRegularDestinationView(profileRoute) + .navigationDestination(for: ProfileRoute.self) { route in + profileRegularDestinationView(route) + } + } + } } else { ContentUnavailableView( String(localized: "profile_select_detail"), @@ -340,6 +353,31 @@ struct MainView: View { } .background(Color(.secondarySystemBackground).ignoresSafeArea()) } + + @ViewBuilder + private func profileRegularDestinationView(_ route: ProfileRoute) -> some View { + switch route { + case .activity(let todoId): + TodoDetailView(viewModel: profileViewCoordinator.makeTodoDetailViewModel(todoId: todoId)) + .id(todoId) + case .settings: + SettingView(viewModel: profileViewCoordinator.settingViewModel) + .environment(profileViewCoordinator.router) + case .theme: + ThemeView( + theme: Binding( + get: { profileViewCoordinator.settingViewModel.state.theme }, + set: { profileViewCoordinator.settingViewModel.send(.setTheme($0)) } + ) + ) + case .pushNotification: + PushNotificationSettingsView( + viewModel: profileViewCoordinator.makePushNotificationSettingsViewModel() + ) + case .account: + AccountView(viewModel: profileViewCoordinator.makeAccountViewModel()) + } + } } private extension MainView { @@ -393,15 +431,6 @@ private extension MainView { ) } - var profileSelectedTodoId: String? { - for route in profileViewCoordinator.router.path.reversed() { - if case let .activity(todoId) = route { - return todoId - } - } - return nil - } - } private extension MainTab { var title: String { diff --git a/Application/DevLogPresentation/Sources/Profile/ProfileView.swift b/Application/DevLogPresentation/Sources/Profile/ProfileView.swift index 47d912c5..25727f13 100644 --- a/Application/DevLogPresentation/Sources/Profile/ProfileView.swift +++ b/Application/DevLogPresentation/Sources/Profile/ProfileView.swift @@ -19,15 +19,6 @@ struct ProfileView: View { if isCompactLayout { NavigationStack(path: navigationPath) { profileContentView - .toolbar { - ToolbarItem(placement: .topBarTrailing) { - Button { - coordinator.router.push(.settings) - } label: { - Image(systemName: "gearshape") - } - } - } .navigationDestination(for: ProfileRoute.self) { route in profileDestinationView(route) } @@ -36,6 +27,7 @@ struct ProfileView: View { profileContentView } } + .toolbar { profileToolbarContent } .onChange(of: focused) { _, newValue in withAnimation { coordinator.viewModel.send(.updateStatusTextFieldFocus(newValue)) @@ -137,6 +129,21 @@ struct ProfileView: View { .background(Color(.systemGroupedBackground)) } + @ToolbarContentBuilder + private var profileToolbarContent: some ToolbarContent { + ToolbarItem(placement: .topBarTrailing) { + Button { + if isCompactLayout { + coordinator.router.push(.settings) + } else { + coordinator.router.replace(with: .settings) + } + } label: { + Image(systemName: "gearshape") + } + } + } + @ViewBuilder private func profileDestinationView(_ route: ProfileRoute) -> some View { switch route { @@ -368,7 +375,11 @@ struct ProfileView: View { ForEach(activities) { activity in Button { if !activity.isDeleted { - coordinator.router.push(.activity(activity.todoId)) + if isCompactLayout { + coordinator.router.push(.activity(activity.todoId)) + } else { + coordinator.router.replace(with: .activity(activity.todoId)) + } } } label: { let item = TodoCategoryItem(from: activity.category) From 3f33c87b257a9e020347cc7a3e530841e207b60d Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Mon, 1 Jun 2026 17:42:33 +0900 Subject: [PATCH 3/4] =?UTF-8?q?ui:=20=20.navigationSplitViewColumnWidth(mi?= =?UTF-8?q?n:ideal:max)=20=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Application/DevLogPresentation/Sources/Main/MainView.swift | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/Application/DevLogPresentation/Sources/Main/MainView.swift b/Application/DevLogPresentation/Sources/Main/MainView.swift index ee58a1f1..8a1f2e47 100644 --- a/Application/DevLogPresentation/Sources/Main/MainView.swift +++ b/Application/DevLogPresentation/Sources/Main/MainView.swift @@ -112,6 +112,7 @@ struct MainView: View { mainSidebar } content: { homeView + .navigationSplitViewColumnWidth(min: 350, ideal: 450, max: nil) } detail: { homeRegularDetailView } @@ -121,6 +122,7 @@ struct MainView: View { mainSidebar } content: { todayView + .navigationSplitViewColumnWidth(min: 350, ideal: 450, max: nil) } detail: { todayRegularDetailView } @@ -132,6 +134,7 @@ struct MainView: View { coordinator: pushNotificationListViewCoordinator, isCompactLayout: isCompactLayout ) + .navigationSplitViewColumnWidth(min: 350, ideal: 450, max: nil) } detail: { Group { if let todoId = pushNotificationListViewCoordinator.todoIdToPresent?.id { @@ -155,6 +158,7 @@ struct MainView: View { mainSidebar } content: { profileView + .navigationSplitViewColumnWidth(min: 350, ideal: 450, max: nil) } detail: { profileRegularDetailView } From b9f156b9653369a140b50a4a4e2f558555e91201 Mon Sep 17 00:00:00 2001 From: opficdev <162981733+opficdev@users.noreply.github.com> Date: Mon, 1 Jun 2026 18:13:48 +0900 Subject: [PATCH 4/4] =?UTF-8?q?fix:=20=ED=94=84=EB=A1=9C=ED=95=84=20?= =?UTF-8?q?=EC=83=81=EC=84=B8=20=ED=8C=A8=EB=84=90=20=EB=82=B4=EB=B9=84?= =?UTF-8?q?=EA=B2=8C=EC=9D=B4=EC=85=98=20=EC=9D=BC=EC=9B=90=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../Sources/Main/MainView.swift | 36 ++++++++----------- 1 file changed, 15 insertions(+), 21 deletions(-) diff --git a/Application/DevLogPresentation/Sources/Main/MainView.swift b/Application/DevLogPresentation/Sources/Main/MainView.swift index 8a1f2e47..77287698 100644 --- a/Application/DevLogPresentation/Sources/Main/MainView.swift +++ b/Application/DevLogPresentation/Sources/Main/MainView.swift @@ -331,28 +331,22 @@ struct MainView: View { } private var profileRegularDetailView: some View { - Group { - if let profileRoute = profileViewCoordinator.router.root { - switch profileRoute { - case .activity(let todoId): - TodoDetailView(viewModel: profileViewCoordinator.makeTodoDetailViewModel(todoId: todoId)) - .id(todoId) - case .settings, .theme, .pushNotification, .account: - NavigationStack(path: Binding( - get: { profileViewCoordinator.router.detailPath }, - set: { profileViewCoordinator.router.detailPath = $0 } - )) { - profileRegularDestinationView(profileRoute) - .navigationDestination(for: ProfileRoute.self) { route in - profileRegularDestinationView(route) - } - } + NavigationStack(path: Binding( + get: { profileViewCoordinator.router.detailPath }, + set: { profileViewCoordinator.router.detailPath = $0 } + )) { + Group { + if let profileRoute = profileViewCoordinator.router.root { + profileRegularDestinationView(profileRoute) + } else { + ContentUnavailableView( + String(localized: "profile_select_detail"), + systemImage: "person.crop.circle" + ) } - } else { - ContentUnavailableView( - String(localized: "profile_select_detail"), - systemImage: "person.crop.circle" - ) + } + .navigationDestination(for: ProfileRoute.self) { route in + profileRegularDestinationView(route) } } .background(Color(.secondarySystemBackground).ignoresSafeArea())