From a29790ef2e1c1ffe195e2a0a0ab3881e9b6e0b3c Mon Sep 17 00:00:00 2001 From: MTtankkeo Date: Wed, 15 Jul 2026 14:29:31 +0900 Subject: [PATCH 1/2] =?UTF-8?q?feat:=20=EC=84=A4=EC=A0=95=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EC=97=90=20=EB=8C=80=ED=95=9C=20=EB=94=94?= =?UTF-8?q?=EC=9E=90=EC=9D=B8=20=EC=8B=9C=EC=8A=A4=ED=85=9C=20=EC=A0=81?= =?UTF-8?q?=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/app/config/app_router.dart | 72 +++++++++ lib/app/enum/theme_type.enum.dart | 17 +++ lib/app/setting/setting.dart | 9 ++ lib/app/setting/setting_binding.dart | 17 +++ lib/app/setting/setting_with_enum.dart | 20 +++ lib/app/setting/theme_setting.dart | 13 ++ lib/presentation/app.dart | 34 +++-- .../common/provider/user_auth_provider.dart | 14 ++ .../widget/navigation/grimity_drawer.dart | 11 +- .../provider/profile_edit_provider.dart | 47 +++--- .../setting/page/setting_account_page.dart | 16 ++ .../setting/page/setting_contact_page.dart | 16 ++ .../setting/page/setting_entry_page.dart | 20 +++ .../setting/page/setting_nickname_page.dart | 16 ++ .../page/setting_notification_page.dart | 16 ++ .../page/setting_profile_url_page.dart | 16 ++ .../setting/page/setting_theme_page.dart | 16 ++ lib/presentation/setting/setting_page.dart | 18 ++- lib/presentation/setting/setting_view.dart | 48 +++++- .../setting/view/setting_account_view.dart | 55 +++++++ .../setting/view/setting_body_view.dart | 32 ---- .../setting/view/setting_contact_view.dart | 33 +++++ .../setting/view/setting_entry_view.dart | 92 ++++++++++++ .../setting/view/setting_nickname_view.dart | 81 ++++++++++ .../view/setting_notification_view.dart | 140 ++++++++++++++++++ .../view/setting_profile_url_view.dart | 82 ++++++++++ .../setting/view/setting_theme_view.dart | 51 +++++++ .../setting/widget/setting_action_tile.dart | 23 --- .../setting/widget/setting_app_bar.dart | 31 ++-- .../setting_contact_options_bottom_sheet.dart | 21 --- .../widget/setting_delete_account_dialog.dart | 41 +++-- .../widget/setting_notification_section.dart | 140 ------------------ 32 files changed, 955 insertions(+), 303 deletions(-) create mode 100644 lib/app/enum/theme_type.enum.dart create mode 100644 lib/app/setting/setting.dart create mode 100644 lib/app/setting/setting_binding.dart create mode 100644 lib/app/setting/setting_with_enum.dart create mode 100644 lib/app/setting/theme_setting.dart create mode 100644 lib/presentation/setting/page/setting_account_page.dart create mode 100644 lib/presentation/setting/page/setting_contact_page.dart create mode 100644 lib/presentation/setting/page/setting_entry_page.dart create mode 100644 lib/presentation/setting/page/setting_nickname_page.dart create mode 100644 lib/presentation/setting/page/setting_notification_page.dart create mode 100644 lib/presentation/setting/page/setting_profile_url_page.dart create mode 100644 lib/presentation/setting/page/setting_theme_page.dart create mode 100644 lib/presentation/setting/view/setting_account_view.dart delete mode 100644 lib/presentation/setting/view/setting_body_view.dart create mode 100644 lib/presentation/setting/view/setting_contact_view.dart create mode 100644 lib/presentation/setting/view/setting_entry_view.dart create mode 100644 lib/presentation/setting/view/setting_nickname_view.dart create mode 100644 lib/presentation/setting/view/setting_notification_view.dart create mode 100644 lib/presentation/setting/view/setting_profile_url_view.dart create mode 100644 lib/presentation/setting/view/setting_theme_view.dart delete mode 100644 lib/presentation/setting/widget/setting_action_tile.dart delete mode 100644 lib/presentation/setting/widget/setting_contact_options_bottom_sheet.dart delete mode 100644 lib/presentation/setting/widget/setting_notification_section.dart diff --git a/lib/app/config/app_router.dart b/lib/app/config/app_router.dart index e3583dfc..b2b88052 100644 --- a/lib/app/config/app_router.dart +++ b/lib/app/config/app_router.dart @@ -34,6 +34,12 @@ import 'package:grimity/presentation/profile_edit/profile_edit_page.dart'; import 'package:grimity/presentation/ranking/ranking_page.dart'; import 'package:grimity/presentation/report/report_page.dart'; import 'package:grimity/presentation/search/search_page.dart'; +import 'package:grimity/presentation/setting/page/setting_account_page.dart'; +import 'package:grimity/presentation/setting/page/setting_contact_page.dart'; +import 'package:grimity/presentation/setting/page/setting_nickname_page.dart'; +import 'package:grimity/presentation/setting/page/setting_notification_page.dart'; +import 'package:grimity/presentation/setting/page/setting_profile_url_page.dart'; +import 'package:grimity/presentation/setting/page/setting_theme_page.dart'; import 'package:grimity/presentation/setting/setting_page.dart'; import 'package:grimity/presentation/sign_in/sign_in_page.dart'; import 'package:grimity/presentation/sign_up/sign_up_page.dart'; @@ -510,6 +516,72 @@ class SettingRoute extends GoRouteData with $SettingRoute { Widget build(BuildContext context, GoRouterState state) => SettingPage(); } +@TypedGoRoute(path: SettingAccountRoute.path, name: SettingAccountRoute.name) +class SettingAccountRoute extends GoRouteData with $SettingAccountRoute { + const SettingAccountRoute(); + + static const String path = '/setting/account'; + static const String name = 'setting/account'; + + @override + Widget build(BuildContext context, GoRouterState state) => SettingAccountPage(); +} + +@TypedGoRoute(path: SettingNicknameRoute.path, name: SettingNicknameRoute.name) +class SettingNicknameRoute extends GoRouteData with $SettingNicknameRoute { + const SettingNicknameRoute(); + + static const String path = '/setting/account/nickname'; + static const String name = 'setting/account/nickname'; + + @override + Widget build(BuildContext context, GoRouterState state) => SettingNicknamePage(); +} + +@TypedGoRoute(path: SettingProfileUrlRoute.path, name: SettingProfileUrlRoute.name) +class SettingProfileUrlRoute extends GoRouteData with $SettingProfileUrlRoute { + const SettingProfileUrlRoute(); + + static const String path = '/setting/account/profile_url'; + static const String name = 'setting/account/profile_url'; + + @override + Widget build(BuildContext context, GoRouterState state) => SettingProfileUrlPage(); +} + +@TypedGoRoute(path: SettingThemeRoute.path, name: SettingThemeRoute.name) +class SettingThemeRoute extends GoRouteData with $SettingThemeRoute { + const SettingThemeRoute(); + + static const String path = '/setting/theme'; + static const String name = 'setting/theme'; + + @override + Widget build(BuildContext context, GoRouterState state) => SettingThemePage(); +} + +@TypedGoRoute(path: SettingNotificationRoute.path, name: SettingNotificationRoute.name) +class SettingNotificationRoute extends GoRouteData with $SettingNotificationRoute { + const SettingNotificationRoute(); + + static const String path = '/setting/notification'; + static const String name = 'setting/notification'; + + @override + Widget build(BuildContext context, GoRouterState state) => SettingNotificationPage(); +} + +@TypedGoRoute(path: SettingContactRoute.path, name: SettingContactRoute.name) +class SettingContactRoute extends GoRouteData with $SettingContactRoute { + const SettingContactRoute(); + + static const String path = '/setting/contact'; + static const String name = 'setting/contact'; + + @override + Widget build(BuildContext context, GoRouterState state) => SettingContactPage(); +} + @TypedGoRoute(path: NotificationRoute.path, name: NotificationRoute.name) class NotificationRoute extends GoRouteData with $NotificationRoute { const NotificationRoute(); diff --git a/lib/app/enum/theme_type.enum.dart b/lib/app/enum/theme_type.enum.dart new file mode 100644 index 00000000..b0654ec7 --- /dev/null +++ b/lib/app/enum/theme_type.enum.dart @@ -0,0 +1,17 @@ +import 'package:flutter/foundation.dart'; + +enum ThemeType { + device, + light, + dark; + + Brightness get brightness => switch (this) { + device => _getDeviceBrightness(), + light => Brightness.light, + dark => Brightness.dark, + }; + + Brightness _getDeviceBrightness() { + return PlatformDispatcher.instance.platformBrightness; + } +} diff --git a/lib/app/setting/setting.dart b/lib/app/setting/setting.dart new file mode 100644 index 00000000..2fec159d --- /dev/null +++ b/lib/app/setting/setting.dart @@ -0,0 +1,9 @@ +import 'package:flutter/widgets.dart'; + +abstract class Setting extends ChangeNotifier { + String get key; + + T getValue(); + + Future setValue(T newValue); +} diff --git a/lib/app/setting/setting_binding.dart b/lib/app/setting/setting_binding.dart new file mode 100644 index 00000000..7b682df0 --- /dev/null +++ b/lib/app/setting/setting_binding.dart @@ -0,0 +1,17 @@ +import 'package:flutter/widgets.dart'; +import 'package:grimity/app/setting/theme_setting.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +class SettingBinding { + const SettingBinding._(); + + static Listenable get listenable => Listenable.merge([theme]); + + static late final SharedPreferences prefs; + static late final ThemeSetting theme; + + static Future setup() async { + prefs = await SharedPreferences.getInstance(); + theme = ThemeSetting(); + } +} diff --git a/lib/app/setting/setting_with_enum.dart b/lib/app/setting/setting_with_enum.dart new file mode 100644 index 00000000..26b2656b --- /dev/null +++ b/lib/app/setting/setting_with_enum.dart @@ -0,0 +1,20 @@ +import 'package:collection/collection.dart'; +import 'package:grimity/app/setting/setting.dart'; +import 'package:grimity/app/setting/setting_binding.dart'; + +abstract class SettingWithEnum extends Setting { + T get initialValue; + List get values; + + @override + T getValue() { + final value = SettingBinding.prefs.getString(key); + return values.firstWhereOrNull((e) => e.name == value) ?? initialValue; + } + + @override + Future setValue(T newValue) async { + await SettingBinding.prefs.setString(key, newValue.name); + notifyListeners(); + } +} diff --git a/lib/app/setting/theme_setting.dart b/lib/app/setting/theme_setting.dart new file mode 100644 index 00000000..1cc76455 --- /dev/null +++ b/lib/app/setting/theme_setting.dart @@ -0,0 +1,13 @@ +import 'package:grimity/app/enum/theme_type.enum.dart'; +import 'package:grimity/app/setting/setting_with_enum.dart'; + +class ThemeSetting extends SettingWithEnum { + @override + String get key => 'theme'; + + @override + ThemeType get initialValue => ThemeType.device; + + @override + List get values => ThemeType.values; +} diff --git a/lib/presentation/app.dart b/lib/presentation/app.dart index 36f9710e..6b30a06a 100644 --- a/lib/presentation/app.dart +++ b/lib/presentation/app.dart @@ -6,6 +6,7 @@ import 'package:grimity/app/config/app_theme.dart'; import 'package:grimity/app/environment/flavor.dart'; import 'package:grimity/app/linking/initialize_app_provider.dart'; import 'package:grimity/app/linking/pending_deep_link_provider.dart'; +import 'package:grimity/app/setting/setting_binding.dart'; import 'package:grimity/app/static/push_notification.dart'; import 'package:grimity/app/util/pointer_event_filter.dart'; import 'package:grimity/presentation/common/provider/user_auth_provider.dart'; @@ -22,6 +23,9 @@ void runFlavoredApp() async { // Initialize talker final talker = TalkerFlutter.init(); + // 앱 설정을 위한 초기화. + await SettingBinding.setup(); + // 포그라운드 푸시 알림 구현을 위해 관련 플러그인 초기화. await PushNotification.initializePlugin(); @@ -99,19 +103,23 @@ class _AppState extends ConsumerState with WidgetsBindingObserver { } }); - return MaterialApp.router( - localizationsDelegates: [ - GlobalMaterialLocalizations.delegate, - GlobalCupertinoLocalizations.delegate, - GlobalWidgetsLocalizations.delegate, - FlutterQuillLocalizations.delegate, - ], - routerConfig: ref.watch(routerProvider), - theme: AppTheme.appTheme.copyWith( - // TODO: 개발 중에는 앱 전체적으로 다크 모드로 고정. (시스템 다크 모드와 무관하게) - brightness: Brightness.dark, - ), - builder: routerBuilder, + return ListenableBuilder( + listenable: SettingBinding.listenable, + builder: (context, _) { + return MaterialApp.router( + localizationsDelegates: [ + GlobalMaterialLocalizations.delegate, + GlobalCupertinoLocalizations.delegate, + GlobalWidgetsLocalizations.delegate, + FlutterQuillLocalizations.delegate, + ], + routerConfig: ref.watch(routerProvider), + theme: AppTheme.appTheme.copyWith( + brightness: SettingBinding.theme.getValue().brightness, + ), + builder: routerBuilder, + ); + }, ); } } diff --git a/lib/presentation/common/provider/user_auth_provider.dart b/lib/presentation/common/provider/user_auth_provider.dart index d3ae1e00..a1da0896 100644 --- a/lib/presentation/common/provider/user_auth_provider.dart +++ b/lib/presentation/common/provider/user_auth_provider.dart @@ -1,3 +1,5 @@ +import 'package:flutter/widgets.dart'; +import 'package:grimity/app/config/app_router.dart'; import 'package:grimity/app/enum/login_provider.enum.dart'; import 'package:grimity/app/static/push_notification.dart'; import 'package:grimity/domain/entity/user.dart'; @@ -78,4 +80,16 @@ class UserAuth extends _$UserAuth { await completeLogoutProcessUseCase.execute(provider); state = null; } + + Future performSignOut(BuildContext context) async { + final user = ref.read(userAuthProvider); + if (user == null) return; + + final provider = LoginProvider.fromString(user.provider ?? ''); + await ref.read(userAuthProvider.notifier).logout(provider); + + if (context.mounted) { + SignInRoute().go(context); + } + } } diff --git a/lib/presentation/common/widget/navigation/grimity_drawer.dart b/lib/presentation/common/widget/navigation/grimity_drawer.dart index 85ef9952..ad2e465b 100644 --- a/lib/presentation/common/widget/navigation/grimity_drawer.dart +++ b/lib/presentation/common/widget/navigation/grimity_drawer.dart @@ -4,7 +4,6 @@ import 'package:gds/gds.dart'; import 'package:go_router/go_router.dart'; import 'package:grimity/app/config/app_const.dart'; import 'package:grimity/app/config/app_router.dart'; -import 'package:grimity/app/enum/login_provider.enum.dart'; import 'package:grimity/presentation/common/provider/user_auth_provider.dart'; import 'package:url_launcher/url_launcher.dart'; @@ -90,14 +89,6 @@ class GrimityDrawer extends ConsumerWidget { } Future _signOut(BuildContext context, WidgetRef ref) async { - final user = ref.read(userAuthProvider); - if (user == null) return; - - final provider = LoginProvider.fromString(user.provider ?? ''); - await ref.read(userAuthProvider.notifier).logout(provider); - - if (context.mounted) { - SignInRoute().go(context); - } + return ref.read(userAuthProvider.notifier).performSignOut(context); } } diff --git a/lib/presentation/profile_edit/provider/profile_edit_provider.dart b/lib/presentation/profile_edit/provider/profile_edit_provider.dart index f93cbafe..45aaccee 100644 --- a/lib/presentation/profile_edit/provider/profile_edit_provider.dart +++ b/lib/presentation/profile_edit/provider/profile_edit_provider.dart @@ -120,25 +120,36 @@ class ProfileEdit extends _$ProfileEdit { return false; } - final request = UpdateUserRequest( - name: state.nickname, - url: state.url, - description: state.description, - links: state.links, - ); + try { + state = state.copyWith(isLoading: true); - final result = await updateUserUseCase.execute(request); + final request = UpdateUserRequest( + name: state.nickname, + url: state.url, + description: state.description, + links: state.links, + ); - return result.fold( - onSuccess: (data) { - ToastService.showSuccess('프로필 수정이 완료되었어요'); - return true; - }, - onFailure: (error) { - ToastService.showFailure('프로필 수정에 실패했어요'); - return false; - }, - ); + final result = await updateUserUseCase.execute(request); + + // 새로운 사용자 정보로 새로고침 + if (result.isSuccess) { + await ref.read(userAuthProvider.notifier).getUser(); + } + + return result.fold( + onSuccess: (data) { + ToastService.showSuccess('프로필 수정이 완료되었어요'); + return true; + }, + onFailure: (error) { + ToastService.showFailure('프로필 수정에 실패했어요'); + return false; + }, + ); + } finally { + state = state.copyWith(isLoading: false); + } } /// 유효성 체크 @@ -272,7 +283,7 @@ abstract class ProfileEditState with _$ProfileEditState { final hasChanges = image != initialState!.image || backgroundImage != initialState!.backgroundImage || - nickname != initialState!.nickname || + (nickname.isNotEmpty && nickname != initialState!.nickname) || description != initialState!.description || url != initialState!.url || _isLinksChanged(links, initialState!.links); diff --git a/lib/presentation/setting/page/setting_account_page.dart b/lib/presentation/setting/page/setting_account_page.dart new file mode 100644 index 00000000..09c85d61 --- /dev/null +++ b/lib/presentation/setting/page/setting_account_page.dart @@ -0,0 +1,16 @@ +import 'package:flutter/widgets.dart'; +import 'package:gds/gds.dart'; +import 'package:grimity/presentation/setting/view/setting_account_view.dart'; +import 'package:grimity/presentation/setting/widget/setting_app_bar.dart'; + +class SettingAccountPage extends StatelessWidget { + const SettingAccountPage({super.key}); + + @override + Widget build(BuildContext context) { + return GdsScaffold( + appBar: SettingAppBar(title: '내 계정'), + body: SettingAccountView(), + ); + } +} diff --git a/lib/presentation/setting/page/setting_contact_page.dart b/lib/presentation/setting/page/setting_contact_page.dart new file mode 100644 index 00000000..39910144 --- /dev/null +++ b/lib/presentation/setting/page/setting_contact_page.dart @@ -0,0 +1,16 @@ +import 'package:flutter/widgets.dart'; +import 'package:gds/gds.dart'; +import 'package:grimity/presentation/setting/view/setting_contact_view.dart'; +import 'package:grimity/presentation/setting/widget/setting_app_bar.dart'; + +class SettingContactPage extends StatelessWidget { + const SettingContactPage({super.key}); + + @override + Widget build(BuildContext context) { + return GdsScaffold( + appBar: SettingAppBar(title: '문의하기'), + body: SettingContactView(), + ); + } +} diff --git a/lib/presentation/setting/page/setting_entry_page.dart b/lib/presentation/setting/page/setting_entry_page.dart new file mode 100644 index 00000000..68f2fb7b --- /dev/null +++ b/lib/presentation/setting/page/setting_entry_page.dart @@ -0,0 +1,20 @@ +import 'package:flutter/widgets.dart'; +import 'package:gds/gds.dart'; +import 'package:grimity/presentation/setting/view/setting_entry_view.dart'; +import 'package:grimity/presentation/setting/widget/setting_app_bar.dart'; + +class SettingEntryPage extends StatelessWidget { + const SettingEntryPage({super.key}); + + @override + Widget build(BuildContext context) { + return GdsScaffold( + appBar: createAppBar(), + body: SettingEntryView(), + ); + } + + static Widget createAppBar() { + return SettingAppBar(title: '설정'); + } +} diff --git a/lib/presentation/setting/page/setting_nickname_page.dart b/lib/presentation/setting/page/setting_nickname_page.dart new file mode 100644 index 00000000..6f67677e --- /dev/null +++ b/lib/presentation/setting/page/setting_nickname_page.dart @@ -0,0 +1,16 @@ +import 'package:flutter/widgets.dart'; +import 'package:gds/gds.dart'; +import 'package:grimity/presentation/setting/view/setting_nickname_view.dart'; +import 'package:grimity/presentation/setting/widget/setting_app_bar.dart'; + +class SettingNicknamePage extends StatelessWidget { + const SettingNicknamePage({super.key}); + + @override + Widget build(BuildContext context) { + return GdsScaffold( + appBar: SettingAppBar(title: '닉네임'), + body: SettingNicknameView(), + ); + } +} diff --git a/lib/presentation/setting/page/setting_notification_page.dart b/lib/presentation/setting/page/setting_notification_page.dart new file mode 100644 index 00000000..7f815c0c --- /dev/null +++ b/lib/presentation/setting/page/setting_notification_page.dart @@ -0,0 +1,16 @@ +import 'package:flutter/widgets.dart'; +import 'package:gds/gds.dart'; +import 'package:grimity/presentation/setting/view/setting_notification_view.dart'; +import 'package:grimity/presentation/setting/widget/setting_app_bar.dart'; + +class SettingNotificationPage extends StatelessWidget { + const SettingNotificationPage({super.key}); + + @override + Widget build(BuildContext context) { + return GdsScaffold( + appBar: SettingAppBar(title: '알림 설정'), + body: SettingNotificationView(), + ); + } +} diff --git a/lib/presentation/setting/page/setting_profile_url_page.dart b/lib/presentation/setting/page/setting_profile_url_page.dart new file mode 100644 index 00000000..01353961 --- /dev/null +++ b/lib/presentation/setting/page/setting_profile_url_page.dart @@ -0,0 +1,16 @@ +import 'package:flutter/widgets.dart'; +import 'package:gds/gds.dart'; +import 'package:grimity/presentation/setting/view/setting_profile_url_view.dart'; +import 'package:grimity/presentation/setting/widget/setting_app_bar.dart'; + +class SettingProfileUrlPage extends StatelessWidget { + const SettingProfileUrlPage({super.key}); + + @override + Widget build(BuildContext context) { + return GdsScaffold( + appBar: SettingAppBar(title: '프로필 URL'), + body: SettingProfileUrlView(), + ); + } +} diff --git a/lib/presentation/setting/page/setting_theme_page.dart b/lib/presentation/setting/page/setting_theme_page.dart new file mode 100644 index 00000000..8ce15c04 --- /dev/null +++ b/lib/presentation/setting/page/setting_theme_page.dart @@ -0,0 +1,16 @@ +import 'package:flutter/widgets.dart'; +import 'package:gds/gds.dart'; +import 'package:grimity/presentation/setting/view/setting_theme_view.dart'; +import 'package:grimity/presentation/setting/widget/setting_app_bar.dart'; + +class SettingThemePage extends StatelessWidget { + const SettingThemePage({super.key}); + + @override + Widget build(BuildContext context) { + return GdsScaffold( + appBar: SettingAppBar(title: '화면 테마'), + body: SettingThemeView(), + ); + } +} diff --git a/lib/presentation/setting/setting_page.dart b/lib/presentation/setting/setting_page.dart index 7b24f51b..c9b71db3 100644 --- a/lib/presentation/setting/setting_page.dart +++ b/lib/presentation/setting/setting_page.dart @@ -1,13 +1,23 @@ -import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:gds/gds.dart'; +import 'package:grimity/presentation/setting/page/setting_entry_page.dart'; import 'package:grimity/presentation/setting/setting_view.dart'; -import 'package:grimity/presentation/setting/view/setting_body_view.dart'; -import 'package:grimity/presentation/setting/widget/setting_app_bar.dart'; +import 'package:grimity/presentation/setting/view/setting_account_view.dart'; +import 'package:grimity/presentation/setting/view/setting_entry_view.dart'; class SettingPage extends StatelessWidget { const SettingPage({super.key}); @override Widget build(BuildContext context) { - return SettingView(settingAppbar: SettingAppBar(), settingBodyView: SettingBodyView()); + if (context.isMobile) { + return SettingEntryPage(); + } + + return SettingView( + appbar: SettingEntryPage.createAppBar(), + sideBar: SettingEntryView(), + initialView: SettingAccountView(), + ); } } diff --git a/lib/presentation/setting/setting_view.dart b/lib/presentation/setting/setting_view.dart index 805059f6..43e900e3 100644 --- a/lib/presentation/setting/setting_view.dart +++ b/lib/presentation/setting/setting_view.dart @@ -1,13 +1,51 @@ import 'package:flutter/material.dart'; +import 'package:flutter/widgets.dart'; +import 'package:gds/gds.dart'; -class SettingView extends StatelessWidget { - const SettingView({super.key, required this.settingAppbar, required this.settingBodyView}); +class SettingView extends StatefulWidget { + const SettingView({ + super.key, + required this.appbar, + required this.sideBar, + required this.initialView, + }); - final PreferredSizeWidget settingAppbar; - final Widget settingBodyView; + final Widget appbar; + final Widget sideBar; + final Widget initialView; + + static SettingViewState? of(BuildContext context) { + return context.findAncestorStateOfType(); + } + + @override + State createState() => SettingViewState(); +} + +class SettingViewState extends State { + late final ValueNotifier viewNotifier = ValueNotifier(widget.initialView); + + void setView(Widget newView) { + viewNotifier.value = newView; + } @override Widget build(BuildContext context) { - return Scaffold(appBar: settingAppbar, body: settingBodyView); + return GdsScaffold( + appBar: widget.appbar, + body: Row( + children: [ + SizedBox(width: 240, child: widget.sideBar), + Expanded( + child: ClipRRect( + child: ListenableBuilder( + listenable: viewNotifier, + builder: (context, _) => viewNotifier.value, + ), + ), + ), + ], + ), + ); } } diff --git a/lib/presentation/setting/view/setting_account_view.dart b/lib/presentation/setting/view/setting_account_view.dart new file mode 100644 index 00000000..98eb99ad --- /dev/null +++ b/lib/presentation/setting/view/setting_account_view.dart @@ -0,0 +1,55 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:gds/gds.dart'; +import 'package:grimity/app/config/app_router.dart'; +import 'package:grimity/presentation/common/provider/user_auth_provider.dart'; +import 'package:grimity/presentation/setting/widget/setting_delete_account_dialog.dart'; + +class SettingAccountView extends ConsumerWidget { + const SettingAccountView({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final user = ref.watch(userAuthProvider); + + return Column( + children: [ + Expanded( + child: ListView( + children: [ + GdsListItem.rightIcon( + text: '닉네임', + subText: user?.name, + state: GdsListItemState.enabled, + onTap: () => SettingNicknameRoute().push(context), + ), + GdsListItem.rightIcon( + text: '프로필 URL', + state: GdsListItemState.enabled, + onTap: () => SettingProfileUrlRoute().push(context), + ), + GdsListItem.textLarge( + text: '로그아웃', + state: GdsListItemState.enabled, + isNegative: false, + onTap: () => _signOut(context, ref), + ), + ], + ), + ), + Padding( + padding: EdgeInsets.only(bottom: GdsSpacing.spacing40), + child: GdsTextButton( + text: '탈퇴하기', + variant: GdsTextButtonVariant.assistive, + onPressed: () => showDeleteAccountDialog(context, ref), + ), + ), + ], + ); + } + + Future _signOut(BuildContext context, WidgetRef ref) async { + return ref.read(userAuthProvider.notifier).performSignOut(context); + } +} diff --git a/lib/presentation/setting/view/setting_body_view.dart b/lib/presentation/setting/view/setting_body_view.dart deleted file mode 100644 index 0e6ffe7d..00000000 --- a/lib/presentation/setting/view/setting_body_view.dart +++ /dev/null @@ -1,32 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:grimity/app/config/app_color.dart'; -import 'package:grimity/app/config/app_const.dart'; -import 'package:grimity/app/config/app_router.dart'; -import 'package:grimity/presentation/setting/widget/setting_action_tile.dart'; -import 'package:grimity/presentation/setting/widget/setting_contact_options_bottom_sheet.dart'; -import 'package:grimity/presentation/setting/widget/setting_delete_account_dialog.dart'; -import 'package:grimity/presentation/setting/widget/setting_notification_section.dart'; - -class SettingBodyView extends ConsumerWidget { - const SettingBodyView({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - return SafeArea( - child: ListView( - children: [ - SettingSubscriptionSection(), - divider, - SettingActionTile(title: '이용 안내', onTap: () => PostDetailRoute(id: AppConst.usageGuidePostId).push(context)), - divider, - SettingActionTile(title: '문의하기', onTap: () => showContactOptionsBottomSheet(context)), - divider, - SettingActionTile(title: '회원 탈퇴', onTap: () => showDeleteAccountDialog(context, ref)), - ], - ), - ); - } - - Widget get divider => Divider(height: 1, color: AppColor.gray300); -} diff --git a/lib/presentation/setting/view/setting_contact_view.dart b/lib/presentation/setting/view/setting_contact_view.dart new file mode 100644 index 00000000..2d509e45 --- /dev/null +++ b/lib/presentation/setting/view/setting_contact_view.dart @@ -0,0 +1,33 @@ +import 'package:flutter/services.dart'; +import 'package:flutter/widgets.dart'; +import 'package:gds/gds.dart'; +import 'package:grimity/app/config/app_const.dart'; +import 'package:grimity/app/service/toast_service.dart'; +import 'package:url_launcher/url_launcher.dart'; + +class SettingContactView extends StatelessWidget { + const SettingContactView({super.key}); + + @override + Widget build(BuildContext context) { + return ListView( + children: [ + GdsListItem.textLarge( + text: '오픈 카카오톡으로 이동', + state: GdsListItemState.enabled, + isNegative: false, + onTap: () async => await launchUrl(Uri.parse(AppConst.openChatUrl)), + ), + GdsListItem.textLarge( + text: '메일로 보내기', + state: GdsListItemState.enabled, + isNegative: false, + onTap: () { + Clipboard.setData(ClipboardData(text: AppConst.contactEmail)); + ToastService.showSuccess('메일을 복사했습니다'); + }, + ), + ], + ); + } +} diff --git a/lib/presentation/setting/view/setting_entry_view.dart b/lib/presentation/setting/view/setting_entry_view.dart new file mode 100644 index 00000000..584f2699 --- /dev/null +++ b/lib/presentation/setting/view/setting_entry_view.dart @@ -0,0 +1,92 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:gds/gds.dart'; +import 'package:grimity/app/config/app_const.dart'; +import 'package:grimity/app/config/app_router.dart'; +import 'package:grimity/presentation/setting/setting_view.dart'; +import 'package:grimity/presentation/setting/view/setting_account_view.dart'; +import 'package:grimity/presentation/setting/view/setting_contact_view.dart'; +import 'package:grimity/presentation/setting/view/setting_notification_view.dart'; +import 'package:grimity/presentation/setting/view/setting_theme_view.dart'; + +class SettingEntryView extends ConsumerWidget { + const SettingEntryView({super.key}); + + static GdsListItemState pressed = GdsListItemState.pressed; + static GdsListItemState enabled = GdsListItemState.enabled; + + @override + Widget build(BuildContext context, WidgetRef ref) { + final navView = SettingView.of(context); + + return ListenableBuilder( + listenable: navView?.viewNotifier ?? Listenable.merge([]), + builder: (context, _) { + final current = navView?.viewNotifier.value; + + return ListView( + children: [ + GdsListItem.icon( + text: '내 계정', + icon: GdsIcon.personOutline, + state: current is SettingAccountView ? pressed : enabled, + onTap: () { + if (context.isMobile) { + SettingAccountRoute().push(context); + } else { + assert(navView != null); + navView?.setView(SettingAccountView()); + } + }, + ), + GdsListItem.icon( + text: '화면 테마', + icon: GdsIcon.pallete, + state: current is SettingThemeView ? pressed : enabled, + onTap: () { + if (context.isMobile) { + SettingThemeRoute().push(context); + } else { + assert(navView != null); + navView?.setView(SettingThemeView()); + } + }, + ), + GdsListItem.icon( + text: '알림', + icon: GdsIcon.bellOutline, + state: current is SettingNotificationView ? pressed : enabled, + onTap: () { + if (context.isMobile) { + SettingNotificationRoute().push(context); + } else { + assert(navView != null); + navView?.setView(SettingNotificationView()); + } + }, + ), + GdsListItem.icon( + text: '이용 안내', + icon: GdsIcon.infoCircleOutline, + state: GdsListItemState.enabled, + onTap: () => PostDetailRoute(id: AppConst.usageGuidePostId).push(context), + ), + GdsListItem.icon( + text: '문의하기', + icon: GdsIcon.infoCircleOutline, + state: current is SettingContactView ? pressed : enabled, + onTap: () { + if (context.isMobile) { + SettingContactRoute().push(context); + } else { + assert(navView != null); + navView?.setView(SettingContactView()); + } + }, + ), + ], + ); + }, + ); + } +} diff --git a/lib/presentation/setting/view/setting_nickname_view.dart b/lib/presentation/setting/view/setting_nickname_view.dart new file mode 100644 index 00000000..00042866 --- /dev/null +++ b/lib/presentation/setting/view/setting_nickname_view.dart @@ -0,0 +1,81 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:gap/gap.dart'; +import 'package:gds/gds.dart'; +import 'package:go_router/go_router.dart'; +import 'package:grimity/presentation/common/provider/user_auth_provider.dart'; +import 'package:grimity/presentation/profile_edit/provider/profile_edit_provider.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class SettingNicknameView extends HookConsumerWidget { + const SettingNicknameView({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final colors = context.gdsColors; + final controller = useTextEditingController(); + final state = ref.watch(profileEditProvider); + final user = ref.watch(userAuthProvider); + final errorText = state.nicknameCheckMessage; + final isError = state.nicknameCheckMessage?.isNotEmpty ?? false; + + return Column( + children: [ + Expanded( + child: ListView( + padding: EdgeInsets.only( + top: context.isMobile ? GdsSpacing.spacing40 : GdsSpacing.spacing48, + left: context.isMobile ? GdsSpacing.spacing16 : GdsSpacing.spacing40, + right: context.isMobile ? GdsSpacing.spacing16 : GdsSpacing.spacing40, + ), + children: [ + Text( + '새로운 닉네임을 입력해주세요', + style: + context.isMobile + ? GdsTypography.title2.copyWith(color: colors.text.grayBold) + : GdsTypography.title1.copyWith(color: colors.text.grayBold), + ), + Gap(GdsSpacing.spacing24), + GdsInput.custom( + helperText: errorText, + error: isError, + child: GdsTextField.count( + placeholder: user?.name, + controller: controller, + maxLength: 12, + onChanged: ref.read(profileEditProvider.notifier).updateNickname, + onEditingComplete: () => onComplete(context, ref), + error: isError, + ), + ), + ], + ), + ), + Padding( + padding: EdgeInsets.only( + left: context.isMobile ? GdsSpacing.spacing16 : GdsSpacing.spacing40, + right: context.isMobile ? GdsSpacing.spacing16 : GdsSpacing.spacing40, + bottom: context.isMobile ? GdsSpacing.spacing24 : GdsSpacing.spacing40, + ), + child: GdsSolidButton( + size: GdsSolidButtonSize.large, + text: '저장하기', + enabled: state.isSaveable, + loading: state.isLoading, + expanded: true, + onPressed: () => onComplete(context, ref), + ), + ), + ], + ); + } + + void onComplete(BuildContext context, WidgetRef ref) async { + final isSuccess = await ref.read(profileEditProvider.notifier).updateUser(); + + if (context.mounted && isSuccess) { + context.pop(); + } + } +} diff --git a/lib/presentation/setting/view/setting_notification_view.dart b/lib/presentation/setting/view/setting_notification_view.dart new file mode 100644 index 00000000..daff05fb --- /dev/null +++ b/lib/presentation/setting/view/setting_notification_view.dart @@ -0,0 +1,140 @@ +import 'package:flutter/material.dart'; +import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:gds/gds.dart'; +import 'package:grimity/app/enum/subscription_type.enum.dart'; +import 'package:grimity/presentation/common/provider/user_subscribe_provider.dart'; + +class SettingNotificationView extends ConsumerWidget { + const SettingNotificationView({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final state = ref.watch(userSubscribeProvider); + + final List subscriptionList = state?.subscription ?? []; + final follow = subscriptionList.contains(SubscriptionType.follow); + final feedLike = subscriptionList.contains(SubscriptionType.feedLike); + final feedComment = subscriptionList.contains(SubscriptionType.feedComment); + final feedReply = subscriptionList.contains(SubscriptionType.feedReply); + final postComment = subscriptionList.contains(SubscriptionType.postComment); + final postReply = subscriptionList.contains(SubscriptionType.postReply); + final allSubscription = follow && feedLike && feedComment && feedReply && postComment && postReply; + final innerPadding = EdgeInsets.symmetric(horizontal: GdsSpacing.spacing16); + + return ListView( + children: [ + Padding( + padding: innerPadding, + child: GdsControlItem.toggle( + text: '모든 알림', + state: allSubscription ? GdsControlItemState.pressed : GdsControlItemState.enabled, + variant: GdsControlItemVariant.normal, + onTap: () => toggleAllSubscription(ref, value: !allSubscription), + ), + ), + Padding( + padding: innerPadding, + child: GdsControlItem.toggle( + text: '팔로우 알림', + state: follow ? GdsControlItemState.pressed : GdsControlItemState.enabled, + variant: GdsControlItemVariant.normal, + onTap: () => toggleSubscription(ref, value: !follow, list: subscriptionList, type: SubscriptionType.follow), + ), + ), + GdsListItem.section(text: '그림'), + Padding( + padding: innerPadding, + child: GdsControlItem.toggle( + text: '좋아요 알림', + state: feedLike ? GdsControlItemState.pressed : GdsControlItemState.enabled, + variant: GdsControlItemVariant.normal, + onTap: + () => + toggleSubscription(ref, value: !feedLike, list: subscriptionList, type: SubscriptionType.feedLike), + ), + ), + Padding( + padding: innerPadding, + child: GdsControlItem.toggle( + text: '새 댓글 알림', + state: feedComment ? GdsControlItemState.pressed : GdsControlItemState.enabled, + variant: GdsControlItemVariant.normal, + onTap: + () => toggleSubscription( + ref, + value: !feedComment, + list: subscriptionList, + type: SubscriptionType.feedComment, + ), + ), + ), + Padding( + padding: innerPadding, + child: GdsControlItem.toggle( + text: '새 답글 알림', + state: feedReply ? GdsControlItemState.pressed : GdsControlItemState.enabled, + variant: GdsControlItemVariant.normal, + onTap: + () => toggleSubscription( + ref, + value: !feedReply, + list: subscriptionList, + type: SubscriptionType.feedReply, + ), + ), + ), + GdsListItem.section(text: '자유게시판'), + Padding( + padding: innerPadding, + child: GdsControlItem.toggle( + text: '새 댓글 알림', + state: postComment ? GdsControlItemState.pressed : GdsControlItemState.enabled, + variant: GdsControlItemVariant.normal, + onTap: + () => toggleSubscription( + ref, + value: !postComment, + list: subscriptionList, + type: SubscriptionType.postComment, + ), + ), + ), + Padding( + padding: innerPadding, + child: GdsControlItem.toggle( + text: '새 답글 알림', + state: postReply ? GdsControlItemState.pressed : GdsControlItemState.enabled, + variant: GdsControlItemVariant.normal, + onTap: + () => toggleSubscription( + ref, + value: !postReply, + list: subscriptionList, + type: SubscriptionType.postReply, + ), + ), + ), + ], + ); + } + + void toggleAllSubscription(WidgetRef ref, {required bool? value}) { + final tempList = value == true ? SubscriptionType.values.toList() : []; + ref.read(userSubscribeProvider.notifier).updateSubscription(tempList); + } + + void toggleSubscription( + WidgetRef ref, { + required bool? value, + required List list, + required SubscriptionType type, + }) { + final tempList = list.toList(); + if (value == true) { + tempList.add(type); + } else { + tempList.remove(type); + } + ref.read(userSubscribeProvider.notifier).updateSubscription(tempList); + } +} diff --git a/lib/presentation/setting/view/setting_profile_url_view.dart b/lib/presentation/setting/view/setting_profile_url_view.dart new file mode 100644 index 00000000..e58a373e --- /dev/null +++ b/lib/presentation/setting/view/setting_profile_url_view.dart @@ -0,0 +1,82 @@ +import 'package:flutter/widgets.dart'; +import 'package:flutter_hooks/flutter_hooks.dart'; +import 'package:gap/gap.dart'; +import 'package:gds/gds.dart'; +import 'package:go_router/go_router.dart'; +import 'package:grimity/presentation/profile_edit/provider/profile_edit_provider.dart'; +import 'package:hooks_riverpod/hooks_riverpod.dart'; + +class SettingProfileUrlView extends HookConsumerWidget { + const SettingProfileUrlView({super.key}); + + @override + Widget build(BuildContext context, WidgetRef ref) { + final colors = context.gdsColors; + final state = ref.watch(profileEditProvider); + final controller = useTextEditingController(); + final errorText = ref.read(profileEditProvider).urlCheckMessage; + final isError = errorText?.isNotEmpty ?? false; + + return Column( + children: [ + Expanded( + child: ListView( + padding: EdgeInsets.only( + top: context.isMobile ? GdsSpacing.spacing40 : GdsSpacing.spacing48, + left: context.isMobile ? GdsSpacing.spacing16 : GdsSpacing.spacing40, + right: context.isMobile ? GdsSpacing.spacing16 : GdsSpacing.spacing40, + ), + children: [ + Text( + '프로필 URL를 정해주세요', + style: + context.isMobile + ? GdsTypography.title2.copyWith(color: colors.text.grayBold) + : GdsTypography.title1.copyWith(color: colors.text.grayBold), + ), + Gap(GdsSpacing.spacing40), + Text( + 'www.grimity.com/', + style: GdsTypography.title2.copyWith(color: colors.text.grayBold), + ), + Gap(GdsSpacing.spacing12), + GdsInput.custom( + helperText: errorText, + error: isError, + child: GdsTextField( + placeholder: '숫자, 영문(소문자), 언더바(_)', + controller: controller, + onChanged: ref.read(profileEditProvider.notifier).updateUrl, + error: isError, + ), + ), + ], + ), + ), + Padding( + padding: EdgeInsets.only( + left: context.isMobile ? GdsSpacing.spacing16 : GdsSpacing.spacing40, + right: context.isMobile ? GdsSpacing.spacing16 : GdsSpacing.spacing40, + bottom: context.isMobile ? GdsSpacing.spacing24 : GdsSpacing.spacing40, + ), + child: GdsSolidButton( + size: GdsSolidButtonSize.large, + text: '저장하기', + enabled: state.isSaveable, + loading: state.isLoading, + expanded: true, + onPressed: () => onComplete(context, ref), + ), + ), + ], + ); + } + + void onComplete(BuildContext context, WidgetRef ref) async { + final isSuccess = await ref.read(profileEditProvider.notifier).updateUser(); + + if (context.mounted && isSuccess) { + context.pop(); + } + } +} diff --git a/lib/presentation/setting/view/setting_theme_view.dart b/lib/presentation/setting/view/setting_theme_view.dart new file mode 100644 index 00000000..333b2a05 --- /dev/null +++ b/lib/presentation/setting/view/setting_theme_view.dart @@ -0,0 +1,51 @@ +import 'package:flutter/widgets.dart'; +import 'package:gds/gds.dart'; +import 'package:grimity/app/enum/theme_type.enum.dart'; +import 'package:grimity/app/setting/setting_binding.dart'; + +class SettingThemeView extends StatelessWidget { + const SettingThemeView({super.key}); + + @override + Widget build(BuildContext context) { + final setting = SettingBinding.theme; + + return ListenableBuilder( + listenable: setting, + builder: (context, child) { + final isDevice = setting.getValue() == ThemeType.device; + final isLight = setting.getValue() == ThemeType.light; + final isDark = setting.getValue() == ThemeType.dark; + + return ListView( + children: [ + GdsListItem.textLarge( + text: '시스템 설정 모드', + state: isDevice ? GdsListItemState.pressed : GdsListItemState.enabled, + isNegative: false, + onTap: () { + if (!isDevice) setting.setValue(ThemeType.device); + }, + ), + GdsListItem.textLarge( + text: '밝은 모드', + state: isLight ? GdsListItemState.pressed : GdsListItemState.enabled, + isNegative: false, + onTap: () { + if (!isLight) setting.setValue(ThemeType.light); + }, + ), + GdsListItem.textLarge( + text: '어두운 모드', + state: isDark ? GdsListItemState.pressed : GdsListItemState.enabled, + isNegative: false, + onTap: () { + if (!isDark) setting.setValue(ThemeType.dark); + }, + ), + ], + ); + }, + ); + } +} diff --git a/lib/presentation/setting/widget/setting_action_tile.dart b/lib/presentation/setting/widget/setting_action_tile.dart deleted file mode 100644 index da1e9868..00000000 --- a/lib/presentation/setting/widget/setting_action_tile.dart +++ /dev/null @@ -1,23 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:grimity/app/config/app_color.dart'; -import 'package:grimity/app/config/app_typeface.dart'; - -class SettingActionTile extends StatelessWidget { - const SettingActionTile({super.key, required this.title, required this.onTap}); - - final String title; - final VoidCallback onTap; - - @override - Widget build(BuildContext context) { - return InkWell( - onTap: onTap, - child: Container( - height: 56, - padding: EdgeInsets.symmetric(horizontal: 16), - alignment: Alignment.centerLeft, - child: Text(title, style: AppTypeface.label1.copyWith(color: AppColor.gray800)), - ), - ); - } -} diff --git a/lib/presentation/setting/widget/setting_app_bar.dart b/lib/presentation/setting/widget/setting_app_bar.dart index 76f73477..b153981a 100644 --- a/lib/presentation/setting/widget/setting_app_bar.dart +++ b/lib/presentation/setting/widget/setting_app_bar.dart @@ -1,24 +1,23 @@ import 'package:flutter/material.dart'; -import 'package:grimity/app/config/app_color.dart'; -import 'package:grimity/app/config/app_theme.dart'; -import 'package:grimity/app/config/app_typeface.dart'; +import 'package:gds/gds.dart'; +import 'package:go_router/go_router.dart'; -class SettingAppBar extends StatelessWidget implements PreferredSizeWidget { - const SettingAppBar({super.key}); +class SettingAppBar extends StatelessWidget { + const SettingAppBar({ + super.key, + required this.title, + }); + + final String title; @override Widget build(BuildContext context) { - return AppBar( - toolbarHeight: AppTheme.kToolbarHeight.height, - title: Text('설정', style: AppTypeface.subTitle3), - titleSpacing: 0, - bottom: const PreferredSize( - preferredSize: Size.fromHeight(1), - child: Divider(height: 1, color: AppColor.gray300), - ), + return GdsTopNavigation.iconButton( + title: title, + icons: [], + onIconTap: [], + showIcons: false, + onBack: context.pop, ); } - - @override - Size get preferredSize => AppTheme.kToolbarHeight; } diff --git a/lib/presentation/setting/widget/setting_contact_options_bottom_sheet.dart b/lib/presentation/setting/widget/setting_contact_options_bottom_sheet.dart deleted file mode 100644 index fa8f02d4..00000000 --- a/lib/presentation/setting/widget/setting_contact_options_bottom_sheet.dart +++ /dev/null @@ -1,21 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter/services.dart'; -import 'package:grimity/app/config/app_const.dart'; -import 'package:grimity/app/service/toast_service.dart'; -import 'package:grimity/presentation/common/widget/popup/grimity_modal_bottom_sheet.dart'; -import 'package:url_launcher/url_launcher.dart'; - -void showContactOptionsBottomSheet(BuildContext context) { - List buttons = [ - GrimityModalButtonModel(title: '카톡으로 이동', onTap: () async => await launchUrl(Uri.parse(AppConst.openChatUrl))), - GrimityModalButtonModel( - title: '메일 링크 복사', - onTap: () { - Clipboard.setData(ClipboardData(text: AppConst.contactEmail)); - ToastService.showSuccess('이메일이 복사되었습니다!'); - }, - ), - ]; - - GrimityModalBottomSheet.show(context, buttons: buttons); -} diff --git a/lib/presentation/setting/widget/setting_delete_account_dialog.dart b/lib/presentation/setting/widget/setting_delete_account_dialog.dart index ded22f8d..058950b1 100644 --- a/lib/presentation/setting/widget/setting_delete_account_dialog.dart +++ b/lib/presentation/setting/widget/setting_delete_account_dialog.dart @@ -1,32 +1,31 @@ import 'package:flutter/material.dart'; import 'package:flutter_riverpod/flutter_riverpod.dart'; +import 'package:gds/gds.dart'; import 'package:go_router/go_router.dart'; import 'package:grimity/app/config/app_router.dart'; import 'package:grimity/app/enum/login_provider.enum.dart'; import 'package:grimity/domain/usecase/me_usecases.dart'; import 'package:grimity/presentation/common/provider/user_auth_provider.dart'; -import 'package:grimity/presentation/common/widget/alert/grimity_dialog.dart'; -void showDeleteAccountDialog(BuildContext context, WidgetRef ref) { - showDialog( - context: context, - builder: - (builderContext) => GrimityDialog( - title: '정말 탈퇴하시겠어요?', - content: '계정 복구는 어려워요.', - cancelText: '취소', - confirmText: '탈퇴하기', - onCancel: () => builderContext.pop(), - onConfirm: () async { - final user = ref.read(userAuthProvider); - if (user == null) return; +Future showDeleteAccountDialog(BuildContext context, WidgetRef ref) { + final alert = GdsAlert( + size: context.isMobile ? GdsAlertSize.md : GdsAlertSize.xl, + title: '정말 탈퇴하시겠어요?', + description: '계정 복구는 어려워요.', + primaryLabel: '탈퇴하기', + onPrimaryTap: () async { + final user = ref.read(userAuthProvider); + if (user == null) return; - builderContext.pop(); - await completeDeleteUserProcessUseCase.execute(LoginProvider.fromString(user.provider ?? '')); - if (context.mounted) { - SignInRoute().go(context); - } - }, - ), + context.pop(); + await completeDeleteUserProcessUseCase.execute(LoginProvider.fromString(user.provider ?? '')); + if (context.mounted) { + SignInRoute().go(context); + } + }, + secondaryLabel: '취소', + onSecondaryTap: context.pop, ); + + return alert.open(context); } diff --git a/lib/presentation/setting/widget/setting_notification_section.dart b/lib/presentation/setting/widget/setting_notification_section.dart deleted file mode 100644 index 6260f9e6..00000000 --- a/lib/presentation/setting/widget/setting_notification_section.dart +++ /dev/null @@ -1,140 +0,0 @@ -import 'package:flutter/material.dart'; -import 'package:flutter_riverpod/flutter_riverpod.dart'; -import 'package:grimity/app/config/app_color.dart'; -import 'package:grimity/app/config/app_typeface.dart'; -import 'package:grimity/app/enum/subscription_type.enum.dart'; -import 'package:grimity/presentation/common/provider/user_subscribe_provider.dart'; -import 'package:grimity/presentation/common/widget/system/check/grimity_switch.dart'; - -class SettingSubscriptionSection extends ConsumerWidget { - const SettingSubscriptionSection({super.key}); - - @override - Widget build(BuildContext context, WidgetRef ref) { - final state = ref.watch(userSubscribeProvider); - - final List subscriptionList = state?.subscription ?? []; - final follow = subscriptionList.contains(SubscriptionType.follow); - final feedLike = subscriptionList.contains(SubscriptionType.feedLike); - final feedComment = subscriptionList.contains(SubscriptionType.feedComment); - final feedReply = subscriptionList.contains(SubscriptionType.feedReply); - final postComment = subscriptionList.contains(SubscriptionType.postComment); - final postReply = subscriptionList.contains(SubscriptionType.postReply); - final allSubscription = follow && feedLike && feedComment && feedReply && postComment && postReply; - - return ExpansionTile( - shape: Border(), - tilePadding: EdgeInsets.symmetric(horizontal: 16), - title: Text('알림 설정', style: AppTypeface.label1.copyWith(color: AppColor.gray800)), - children: [ - Divider(height: 1, color: AppColor.gray300), - _SettingSubscriptionSwitchTile( - title: '모든 알림', - value: allSubscription, - onChanged: (value) => toggleAllSubscription(ref, value: value), - ), - Divider(height: 1, color: AppColor.gray300), - _SettingSubscriptionSwitchTile( - title: '팔로우 알림', - value: follow, - onChanged: - (value) => toggleSubscription(ref, value: value, list: subscriptionList, type: SubscriptionType.follow), - ), - _SettingGroupHeader(title: '그림'), - _SettingSubscriptionSwitchTile( - title: '좋아요 알림', - value: feedLike, - onChanged: - (value) => toggleSubscription(ref, value: value, list: subscriptionList, type: SubscriptionType.feedLike), - ), - Divider(height: 1, color: AppColor.gray300), - _SettingSubscriptionSwitchTile( - title: '새 댓글 알림', - value: feedComment, - onChanged: - (value) => - toggleSubscription(ref, value: value, list: subscriptionList, type: SubscriptionType.feedComment), - ), - Divider(height: 1, color: AppColor.gray300), - _SettingSubscriptionSwitchTile( - title: '새 답글 알림', - value: feedReply, - onChanged: - (value) => - toggleSubscription(ref, value: value, list: subscriptionList, type: SubscriptionType.feedReply), - ), - _SettingGroupHeader(title: '자유게시판'), - _SettingSubscriptionSwitchTile( - title: '새 댓글 알림', - value: postComment, - onChanged: - (value) => - toggleSubscription(ref, value: value, list: subscriptionList, type: SubscriptionType.postComment), - ), - Divider(height: 1, color: AppColor.gray300), - _SettingSubscriptionSwitchTile( - title: '새 답글 알림', - value: postReply, - onChanged: - (value) => - toggleSubscription(ref, value: value, list: subscriptionList, type: SubscriptionType.postReply), - ), - ], - ); - } - - void toggleAllSubscription(WidgetRef ref, {required bool? value}) { - final tempList = value == true ? SubscriptionType.values.toList() : []; - ref.read(userSubscribeProvider.notifier).updateSubscription(tempList); - } - - void toggleSubscription( - WidgetRef ref, { - required bool? value, - required List list, - required SubscriptionType type, - }) { - final tempList = list.toList(); - if (value == true) { - tempList.add(type); - } else { - tempList.remove(type); - } - ref.read(userSubscribeProvider.notifier).updateSubscription(tempList); - } -} - -class _SettingSubscriptionSwitchTile extends StatelessWidget { - const _SettingSubscriptionSwitchTile({required this.title, this.onChanged, required this.value}); - - final String title; - final bool value; - final ValueChanged? onChanged; - - @override - Widget build(BuildContext context) { - return ListTile( - dense: true, - contentPadding: EdgeInsets.only(left: 32, right: 16), - title: Text(title, style: AppTypeface.label3.copyWith(color: AppColor.gray800)), - trailing: GrimitySwitch(value: value, onChanged: onChanged), - ); - } -} - -class _SettingGroupHeader extends StatelessWidget { - const _SettingGroupHeader({required this.title}); - - final String title; - - @override - Widget build(BuildContext context) { - return Container( - height: 36, - padding: EdgeInsets.only(left: 32), - color: AppColor.gray200, - alignment: Alignment.centerLeft, - child: Text(title, style: AppTypeface.caption3.copyWith(color: AppColor.gray600)), - ); - } -} From 8aecf716653b92c22299bbf862b4c26dc8466315 Mon Sep 17 00:00:00 2001 From: MTtankkeo Date: Wed, 15 Jul 2026 14:46:11 +0900 Subject: [PATCH 2/2] =?UTF-8?q?fix:=20=ED=85=8C=EB=A7=88=20=EB=AA=A8?= =?UTF-8?q?=EB=93=9C=20=EC=A0=81=EC=9A=A9=20=EB=B0=8F=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=ED=99=94=EB=A9=B4=20=EB=A6=AC=EC=86=8C=EC=8A=A4=20=EC=A0=95?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/app/enum/theme_type.enum.dart | 14 +++++--------- lib/presentation/app.dart | 6 +++--- .../common/provider/user_auth_provider.dart | 2 +- lib/presentation/setting/setting_view.dart | 6 ++++++ 4 files changed, 15 insertions(+), 13 deletions(-) diff --git a/lib/app/enum/theme_type.enum.dart b/lib/app/enum/theme_type.enum.dart index b0654ec7..ba59d101 100644 --- a/lib/app/enum/theme_type.enum.dart +++ b/lib/app/enum/theme_type.enum.dart @@ -1,17 +1,13 @@ -import 'package:flutter/foundation.dart'; +import 'package:flutter/material.dart'; enum ThemeType { device, light, dark; - Brightness get brightness => switch (this) { - device => _getDeviceBrightness(), - light => Brightness.light, - dark => Brightness.dark, + ThemeMode get themeMode => switch (this) { + device => ThemeMode.system, + light => ThemeMode.light, + dark => ThemeMode.dark, }; - - Brightness _getDeviceBrightness() { - return PlatformDispatcher.instance.platformBrightness; - } } diff --git a/lib/presentation/app.dart b/lib/presentation/app.dart index 6b30a06a..7a13451e 100644 --- a/lib/presentation/app.dart +++ b/lib/presentation/app.dart @@ -114,9 +114,9 @@ class _AppState extends ConsumerState with WidgetsBindingObserver { FlutterQuillLocalizations.delegate, ], routerConfig: ref.watch(routerProvider), - theme: AppTheme.appTheme.copyWith( - brightness: SettingBinding.theme.getValue().brightness, - ), + theme: AppTheme.appTheme.copyWith(brightness: Brightness.light), + darkTheme: AppTheme.appTheme.copyWith(brightness: Brightness.dark), + themeMode: SettingBinding.theme.getValue().themeMode, builder: routerBuilder, ); }, diff --git a/lib/presentation/common/provider/user_auth_provider.dart b/lib/presentation/common/provider/user_auth_provider.dart index a1da0896..45bf8155 100644 --- a/lib/presentation/common/provider/user_auth_provider.dart +++ b/lib/presentation/common/provider/user_auth_provider.dart @@ -86,7 +86,7 @@ class UserAuth extends _$UserAuth { if (user == null) return; final provider = LoginProvider.fromString(user.provider ?? ''); - await ref.read(userAuthProvider.notifier).logout(provider); + await logout(provider); if (context.mounted) { SignInRoute().go(context); diff --git a/lib/presentation/setting/setting_view.dart b/lib/presentation/setting/setting_view.dart index 43e900e3..ad15632c 100644 --- a/lib/presentation/setting/setting_view.dart +++ b/lib/presentation/setting/setting_view.dart @@ -29,6 +29,12 @@ class SettingViewState extends State { viewNotifier.value = newView; } + @override + void dispose() { + super.dispose(); + viewNotifier.dispose(); + } + @override Widget build(BuildContext context) { return GdsScaffold(