diff --git a/lib/theme/app_palette.dart b/lib/theme/app_palette.dart index 1262bdb..07e4e67 100644 --- a/lib/theme/app_palette.dart +++ b/lib/theme/app_palette.dart @@ -1,16 +1,36 @@ // ๐ŸŽจ ์•ฑ ์ „์—ญ ๋ฌด์ฑ„์ƒ‰ ํŒ”๋ ˆํŠธ (coolors.co: 1c1c1c-daddd8-ecebe4-eef0f2-fafaff). // ๋ฐฐ๊ฒฝ/์นด๋“œ/๋ฐฐ๋„ˆ ๊ฐ™์€ ๊ตฌ์กฐ์  ์ƒ‰์ƒ์€ ์ „๋ถ€ ์ด 5์ƒ‰์œผ๋กœ ํ†ต์ผํ•œ๋‹ค. // ์‚ญ์ œ/์œ„๋ฐ˜/๊ฑฐ์ ˆ์ฒ˜๋Ÿผ ์‚ฌ์šฉ์ž๊ฐ€ ์ฆ‰์‹œ ์•Œ์•„์ฑ„์•ผ ํ•˜๋Š” ๊ฒฝ๊ณ ์„ฑ ์ƒ‰์ƒ(๋นจ๊ฐ• ๋“ฑ)๋งŒ ์˜ˆ์™ธ๋กœ ์œ ์ง€ํ•œ๋‹ค. +// +// ๐Ÿ›๏ธ mist/paper/linen/sage(๊ตฌ์กฐ์ƒ‰ 4์ข…)๋Š” ํ”„๋กœํ•„ ๊พธ๋ฏธ๊ธฐ ํ…œํ”Œ๋ฆฟ์„ ์žฅ์ฐฉํ•˜๋ฉด ํ™”๋ฉด +// ์ „์ฒด์— ๊ทธ ํ…Œ๋งˆ๊ฐ€ ๋ฒˆ์ง€๋„๋ก const๋ฅผ ๋นผ๊ณ  ๋ฐ”๊ฟ”์น˜๊ธฐ ๊ฐ€๋Šฅํ•˜๊ฒŒ ํ–ˆ๋‹ค. ink๋Š” ํ…์ŠคํŠธ/ +// ๋ฒ„ํŠผ ๋“ฑ ๋„ˆ๋ฌด ๋งŽ์€ ๊ณณ์—์„œ ์“ฐ์—ฌ์„œ ๋ฐ”๊พธ๋ฉด ๊ฐ€๋…์„ฑ์ด ๊นจ์งˆ ์ˆ˜ ์žˆ์–ด ๊ทธ๋Œ€๋กœ ๊ณ ์ • ๋‘”๋‹ค. +// applyTemplateTheme()๋กœ๋งŒ ๋ฐ”๊พธ๊ณ , ์ง์ ‘ ๋Œ€์ž…ํ•˜์ง€ ์•Š๋Š”๋‹ค. import 'package:flutter/material.dart'; +import 'profile_themes.dart'; class AppPalette { AppPalette._(); static const Color ink = Color(0xFF1C1C1C); // ๊ฐ€์žฅ ์–ด๋‘์šด ํ†ค โ€” ๋ฐฐ๋„ˆ/์ฃผ์š” ํ…์ŠคํŠธ/๋ฒ„ํŠผ - static const Color sage = Color(0xFFDADDD8); // ๋ณด๋”/๊ตฌ๋ถ„์„ /๋ณด์กฐ ํ‘œ๋ฉด - static const Color linen = Color(0xFFECEBE4); // ์€์€ํ•œ ์นด๋“œ/๋ฐฐ์ง€ ๋ฐฐ๊ฒฝ - static const Color mist = Color(0xFFEEF0F2); // ํŽ˜์ด์ง€ ๋ฐฐ๊ฒฝ - static const Color paper = Color(0xFFFAFAFF); // ์นด๋“œ ํ‘œ๋ฉด(๊ฑฐ์˜ ํฐ์ƒ‰) + + static const Color _defaultSage = Color(0xFFDADDD8); + static const Color _defaultLinen = Color(0xFFECEBE4); + static const Color _defaultMist = Color(0xFFEEF0F2); + static const Color _defaultPaper = Color(0xFFFAFAFF); + + static Color sage = _defaultSage; // ๋ณด๋”/๊ตฌ๋ถ„์„ /๋ณด์กฐ ํ‘œ๋ฉด + static Color linen = _defaultLinen; // ์€์€ํ•œ ์นด๋“œ/๋ฐฐ์ง€ ๋ฐฐ๊ฒฝ + static Color mist = _defaultMist; // ํŽ˜์ด์ง€ ๋ฐฐ๊ฒฝ + static Color paper = _defaultPaper; // ์นด๋“œ ํ‘œ๋ฉด(๊ฑฐ์˜ ํฐ์ƒ‰) + + /// ๐Ÿ›๏ธ ์žฅ์ฐฉํ•œ ํ…œํ”Œ๋ฆฟ์— ๋งž์ถฐ ๊ตฌ์กฐ์ƒ‰ 4์ข…์„ ํ•œ ๋ฒˆ์— ๋ฐ”๊พผ๋‹ค. null์„ ์ฃผ๋ฉด ๊ธฐ๋ณธ๊ฐ’์œผ๋กœ ๋ณต์›. + static void applyTemplateTheme(ProfileTheme? theme) { + mist = theme?.background ?? _defaultMist; + paper = theme?.surface ?? _defaultPaper; + linen = theme?.surfaceAlt ?? _defaultLinen; + sage = theme?.border ?? _defaultSage; + } // ๐Ÿšจ ๊ฒฝ๊ณ ์„ฑ ์ƒ‰์ƒ 3์ข… - ์˜ˆ์ „์—” ํ™”๋ฉด๋งˆ๋‹ค Colors.red/red[400]/red[700]/redAccent์ฒ˜๋Ÿผ // ์ œ๊ฐ๊ฐ ์จ์„œ ๊ฐ™์€ ์˜๋ฏธ์ธ๋ฐ ์ƒ‰์ด ๋ฏธ๋ฌ˜ํ•˜๊ฒŒ ๋‹ฌ๋ž๋‹ค. ์ด์ œ ์˜๋ฏธ๋ณ„๋กœ ๋”ฑ ํ•˜๋‚˜์”ฉ๋งŒ ์“ด๋‹ค. diff --git a/lib/theme/theme_sync.dart b/lib/theme/theme_sync.dart new file mode 100644 index 0000000..21b1911 --- /dev/null +++ b/lib/theme/theme_sync.dart @@ -0,0 +1,15 @@ +// ๐ŸŒ ์ง€๊ธˆ ์žฅ์ฐฉ๋œ ํ”„๋กœํ•„ ํ…Œ๋งˆ๋ฅผ ์•ฑ ์ „์ฒด์— ํผ๋œจ๋ฆฌ๋Š” ํ†ต๋กœ. +// AppPalette์˜ ๊ตฌ์กฐ์ƒ‰(mist/paper/linen/sage)์€ ์ „์—ญ static ๊ฐ’์ด๋ผ ๋ฐ”๊พธ๋ฉด ๋˜์ง€๋งŒ, +// TitlePill์ฒ˜๋Ÿผ "์‹ค์‹œ๊ฐ„์œผ๋กœ" ๋‹ค์‹œ ๊ทธ๋ ค์ ธ์•ผ ํ•˜๋Š” ๊ณต์œ  ์œ„์ ฏ์€ ๊ตฌ๋…ํ•  ๋Œ€์ƒ์ด +// ๋”ฐ๋กœ ํ•„์š”ํ•ด์„œ ValueNotifier๋ฅผ ํ•˜๋‚˜ ๋” ๋‘”๋‹ค. ํ…œํ”Œ๋ฆฟ์„ ์žฅ์ฐฉ/ํ•ด์ œํ•  ๋•Œ๋Š” +// AppPalette๋ฅผ ์ง์ ‘ ๊ฑด๋“œ๋ฆฌ์ง€ ๋ง๊ณ  ๊ผญ ์ด ํ•จ์ˆ˜๋ฅผ ํ†ตํ•ด์„œ๋งŒ ๋ฐ”๊พผ๋‹ค. +import 'package:flutter/foundation.dart'; +import 'app_palette.dart'; +import 'profile_themes.dart'; + +final ValueNotifier globalProfileTheme = ValueNotifier(null); + +void applyGlobalProfileTheme(ProfileTheme? theme) { + AppPalette.applyTemplateTheme(theme); + globalProfileTheme.value = theme; +} diff --git a/lib/ui/app_notice.dart b/lib/ui/app_notice.dart index f51a4e8..16bf55c 100644 --- a/lib/ui/app_notice.dart +++ b/lib/ui/app_notice.dart @@ -131,7 +131,7 @@ class _NoticeBannerState extends State<_NoticeBanner> Flexible( child: Text( widget.message, - style: const TextStyle( + style: TextStyle( color: AppPalette.paper, fontSize: 13.5, fontWeight: FontWeight.w600, diff --git a/lib/ui/device_checkout_ledger_page.dart b/lib/ui/device_checkout_ledger_page.dart index b762e85..f8f18cf 100644 --- a/lib/ui/device_checkout_ledger_page.dart +++ b/lib/ui/device_checkout_ledger_page.dart @@ -352,7 +352,7 @@ class _DeviceCheckoutLedgerPageState extends State { ? const BorderSide( color: AppPalette.warning, ) - : const BorderSide( + : BorderSide( color: AppPalette.sage, )), ), diff --git a/lib/ui/group_detail_screen.dart b/lib/ui/group_detail_screen.dart index 7af9daa..b4b14a1 100644 --- a/lib/ui/group_detail_screen.dart +++ b/lib/ui/group_detail_screen.dart @@ -325,7 +325,7 @@ class _GroupDetailScreenState extends State { label: const Text('๋ฐฑํ’ˆํƒ€ ๋ฐ”๋กœ๊ฐ€๊ธฐ'), style: OutlinedButton.styleFrom( foregroundColor: AppPalette.ink, - side: const BorderSide(color: AppPalette.sage), + side: BorderSide(color: AppPalette.sage), padding: const EdgeInsets.symmetric(vertical: 14), shape: RoundedRectangleBorder( borderRadius: BorderRadius.circular(16), diff --git a/lib/ui/main_dashboard.dart b/lib/ui/main_dashboard.dart index 5a036f9..604e7e8 100644 --- a/lib/ui/main_dashboard.dart +++ b/lib/ui/main_dashboard.dart @@ -13,6 +13,7 @@ import '../function/student_dashboard_controller.dart'; import '../models/dashboard_tile_layout.dart'; import '../theme/app_palette.dart'; import '../theme/profile_themes.dart'; +import '../theme/theme_sync.dart'; import 'admin_dashboard.dart'; import 'app_notice.dart'; import 'board_home_screen.dart'; @@ -94,6 +95,7 @@ class _MainDashboardState extends State { Future _loadEquippedTheme() async { final theme = await ProfileShopController.fetchEquippedTheme(_displayId); + applyGlobalProfileTheme(theme); if (!mounted) return; setState(() { _equippedTheme = theme; @@ -808,8 +810,8 @@ class _MainDashboardState extends State { ), ), if (isEditing) - const Padding( - padding: EdgeInsets.only(top: 8, left: 24, right: 24), + Padding( + padding: const EdgeInsets.only(top: 8, left: 24, right: 24), child: Text( 'ํƒ€์ผ์„ ๋“œ๋ž˜๊ทธํ•ด์„œ ์˜ฎ๊ธฐ๊ณ , ์˜ค๋ฅธ์ชฝ ์•„๋ž˜ ์†์žก์ด๋กœ ํฌ๊ธฐ๋ฅผ ๋ฐ”๊ฟ”๋ณด์„ธ์š”.', textAlign: TextAlign.center, @@ -969,7 +971,7 @@ class _MainDashboardState extends State { width: 2, ), ), - child: const Icon( + child: Icon( Icons.open_in_full_rounded, size: 14, color: AppPalette.paper, @@ -1131,7 +1133,7 @@ class _DashboardMenuCard extends StatelessWidget { label: '๋กœ๊ทธ์•„์›ƒ', onTap: onLogout, ), - const Divider(height: 1, color: AppPalette.sage), + Divider(height: 1, color: AppPalette.sage), _MenuRow( icon: Icons.settings_outlined, label: '์„ค์ •', diff --git a/lib/ui/profile_customize_screen.dart b/lib/ui/profile_customize_screen.dart index e43de89..2931030 100644 --- a/lib/ui/profile_customize_screen.dart +++ b/lib/ui/profile_customize_screen.dart @@ -4,6 +4,7 @@ import 'package:flutter/material.dart'; import '../function/profile_shop_controller.dart'; import '../theme/app_palette.dart'; import '../theme/profile_themes.dart'; +import '../theme/theme_sync.dart'; import 'app_notice.dart'; import 'title_pill.dart'; @@ -35,7 +36,21 @@ class _ProfileCustomizeScreenState extends State { @override void initState() { super.initState(); - _controller.loadCatalog(widget.studentId); + _initialize(); + } + + /// ์ƒ์  ํ™”๋ฉด์„ ์—ด ๋•Œ๋งˆ๋‹ค ์„œ๋ฒ„์˜ ์‹ค์ œ ์žฅ์ฐฉ ์ƒํƒœ๋กœ ์ „์—ญ ํ…Œ๋งˆ๋ฅผ ๋‹ค์‹œ ๋งž์ถฐ๋‘”๋‹ค + /// (๋”ด ๋ฐ์„œ ์žฅ์ฐฉ ์ƒํƒœ๊ฐ€ ๋ฐ”๋€Œ์—ˆ์„ ์ˆ˜๋„ ์žˆ๋Š” ์—ฃ์ง€ ์ผ€์ด์Šค ๋ณดํ˜ธ์šฉ). + Future _initialize() async { + await _controller.loadCatalog(widget.studentId); + if (!mounted) return; + for (final t in _controller.templates) { + if (t.equipped) { + _syncGlobalTheme(t.id); + return; + } + } + _syncGlobalTheme(null); } @override @@ -88,6 +103,7 @@ class _ProfileCustomizeScreenState extends State { widget.studentId, templateId, ); + if (success) _syncGlobalTheme(templateId); if (!mounted) return; AppNotice.show( context, @@ -96,6 +112,21 @@ class _ProfileCustomizeScreenState extends State { ); } + /// ๐Ÿ’ก ์ปค์Šคํ…€์ฐฝ ์•ˆ์—์„œ์˜ "๋ฏธ๋ฆฌ๋ณด๊ธฐ"์™€ ๋‹ฌ๋ฆฌ, ์‹ค์ œ๋กœ ์žฅ์ฐฉ์ด ๋๋‚œ ๋’ค์—๋Š” ์•ฑ ์ „์ฒด + /// (TitlePill, ๋‹ค๋ฅธ ํ™”๋ฉด์˜ ๋ฐฐ๊ฒฝ/์นด๋“œ์ƒ‰)๋„ ๊ฐ™์ด ๋ฐ”๋€Œ๋„๋ก ์ „์—ญ์— ๋ฐ˜์˜ํ•œ๋‹ค. + void _syncGlobalTheme(int? equippedTemplateId) { + if (equippedTemplateId == null) { + applyGlobalProfileTheme(null); + return; + } + for (final t in _controller.templates) { + if (t.id == equippedTemplateId) { + applyGlobalProfileTheme(profileThemes[t.code]); + return; + } + } + } + @override Widget build(BuildContext context) { return Scaffold( diff --git a/lib/ui/study_timer_screen.dart b/lib/ui/study_timer_screen.dart index 4918f5e..2551101 100644 --- a/lib/ui/study_timer_screen.dart +++ b/lib/ui/study_timer_screen.dart @@ -6,6 +6,7 @@ import 'package:flutter/services.dart'; import '../function/browser_notification.dart'; import '../function/study_timer_controller.dart'; import '../theme/app_palette.dart'; +import '../theme/theme_sync.dart'; import 'app_notice.dart'; import 'launchpad_transition.dart'; import 'study_preset_screen.dart'; @@ -117,6 +118,10 @@ class _StudyTimerScreenState extends State } } + // ๐Ÿ›๏ธ ์žฅ์ฐฉํ•œ ํ…œํ”Œ๋ฆฟ์˜ ๊ฐ•์กฐ์ƒ‰ - ์‹œ์ž‘/์žฌ๊ฐœ์ฒ˜๋Ÿผ "์ค‘๋ฆฝ" ์ƒํƒœ์˜ ๋ฒ„ํŠผ/ํ…์ŠคํŠธ์—๋งŒ ์“ด๋‹ค. + // ์ผ์‹œ์ •์ง€(warning)/์ข…๋ฃŒ(danger) ๊ฐ™์€ ๊ฒฝ๊ณ ์„ฑ ์ƒ‰์€ ์˜๋ฏธ๊ฐ€ ์žˆ์–ด์„œ ๊ทธ๋Œ€๋กœ ๋‘”๋‹ค. + Color get _accentColor => globalProfileTheme.value?.accent ?? AppPalette.ink; + String _formatHMS(int totalSeconds) { final h = totalSeconds ~/ 3600; final m = (totalSeconds % 3600) ~/ 60; @@ -430,7 +435,7 @@ class _StudyTimerScreenState extends State style: TextStyle( fontSize: 52, fontWeight: FontWeight.bold, - color: isPaused ? Colors.grey : AppPalette.ink, + color: isPaused ? Colors.grey : _accentColor, ), ), const SizedBox(height: 36), @@ -471,7 +476,7 @@ class _StudyTimerScreenState extends State size: 140, icon: Icons.play_arrow_rounded, label: _controller.pomodoroEnabled ? '๋ฝ€๋ชจ๋„๋กœ ์‹œ์ž‘' : '๊ณต๋ถ€ ์‹œ์ž‘', - color: AppPalette.ink, + color: _accentColor, onTap: () { requestNotificationPermission(); _runAction(_controller.start); @@ -495,7 +500,7 @@ class _StudyTimerScreenState extends State size: 110, icon: isPaused ? Icons.play_arrow_rounded : Icons.pause_rounded, label: isPaused ? '์žฌ๊ฐœ' : '์ผ์‹œ์ •์ง€', - color: isPaused ? AppPalette.ink : AppPalette.warning, + color: isPaused ? _accentColor : AppPalette.warning, onTap: () => _runAction(isPaused ? _controller.resume : _controller.pause), ), diff --git a/lib/ui/teacher_attendance_page.dart b/lib/ui/teacher_attendance_page.dart index c8311bf..ea36879 100644 --- a/lib/ui/teacher_attendance_page.dart +++ b/lib/ui/teacher_attendance_page.dart @@ -921,25 +921,25 @@ class _TimeSettingsMenuCard extends StatelessWidget { label: '์ƒˆ๋กœ๊ณ ์นจ', onTap: () => onPick('refresh'), ), - const Divider(height: 1, color: AppPalette.sage), + Divider(height: 1, color: AppPalette.sage), _TimeMenuRow( icon: Icons.access_time_rounded, label: attendanceTimeLabel, onTap: () => onPick('attendance_time'), ), - const Divider(height: 1, color: AppPalette.sage), + Divider(height: 1, color: AppPalette.sage), _TimeMenuRow( icon: Icons.timer_outlined, label: '๋ฐ˜์ถœ ํ—ˆ์šฉ ์‹œ๊ฐ„ ์„ค์ •', onTap: () => onPick('permission_window'), ), - const Divider(height: 1, color: AppPalette.sage), + Divider(height: 1, color: AppPalette.sage), _TimeMenuRow( icon: Icons.school_rounded, label: 'ํ•˜๊ต ์ฒ˜๋ฆฌ', onTap: () => onPick('dismissal'), ), - const Divider(height: 1, color: AppPalette.sage), + Divider(height: 1, color: AppPalette.sage), _TimeMenuRow( icon: Icons.bug_report_outlined, label: 'ํ…Œ์ŠคํŠธ์šฉ: ํ—ˆ์šฉ์‹œ๊ฐ„ ์ดˆ๊ธฐํ™”', diff --git a/lib/ui/title_pill.dart b/lib/ui/title_pill.dart index 9fbafff..80df533 100644 --- a/lib/ui/title_pill.dart +++ b/lib/ui/title_pill.dart @@ -1,7 +1,10 @@ // ๐Ÿชถ ๊ฒ€์ • AppBar ๋Œ€์‹  ์“ฐ๋Š” ๋‘ฅ๊ทผ ์ œ๋ชฉ ์•Œ์•ฝ. ๋ชจ๋“  ํ™”๋ฉด์ด ๋˜‘๊ฐ™์€ ํฐํŠธ/ํฌ๊ธฐ๋ฅผ ์“ฐ๊ณ , // ํ™”๋ฉด ๋งจ ์œ„์— ๋”ฑ ๋ถ™์–ด ๋ณด์ด์ง€ ์•Š๋„๋ก ์œ„์ชฝ์— ์‚ด์ง ์—ฌ๋ฐฑ์„ ๋‘”๋‹ค. +// ๐Ÿ›๏ธ ํ”„๋กœํ•„ ๊พธ๋ฏธ๊ธฐ ํ…œํ”Œ๋ฆฟ์„ ์žฅ์ฐฉํ•˜๋ฉด ์ด ์•Œ์•ฝ ๋ฐฐ๊ฒฝ๋„ ๊ฐ™์ด ๋ฐ”๋€๋‹ค - ์•ฑ ์ „์ฒด๊ฐ€ +// ๊ณต์œ ํ•˜๋Š” ์œ„์ ฏ์ด๋ผ ์—ฌ๊ธฐ ํ•˜๋‚˜๋งŒ ๊ณ ์น˜๋ฉด ๋ชจ๋“  ํ™”๋ฉด์˜ ์ œ๋ชฉ ๋ฐฐ๊ฒฝ์ด ํ•œ๋ฒˆ์— ๋ฐ”๋€๋‹ค. import 'package:flutter/material.dart'; import '../theme/app_palette.dart'; +import '../theme/theme_sync.dart'; class TitlePill extends StatelessWidget { final String text; @@ -10,23 +13,28 @@ class TitlePill extends StatelessWidget { @override Widget build(BuildContext context) { - return Padding( - padding: const EdgeInsets.only(top: 8), - child: Container( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), - decoration: BoxDecoration( - color: AppPalette.ink, - borderRadius: BorderRadius.circular(999), - ), - child: Text( - text, - style: const TextStyle( - color: Colors.white, - fontWeight: FontWeight.bold, - fontSize: 15, + return ValueListenableBuilder( + valueListenable: globalProfileTheme, + builder: (context, theme, _) { + return Padding( + padding: const EdgeInsets.only(top: 8), + child: Container( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + decoration: BoxDecoration( + color: theme?.secondary ?? AppPalette.ink, + borderRadius: BorderRadius.circular(999), + ), + child: Text( + text, + style: const TextStyle( + color: Colors.white, + fontWeight: FontWeight.bold, + fontSize: 15, + ), + ), ), - ), - ), + ); + }, ); } }