From f6be60ef46086162350eb745147179523f40c31b Mon Sep 17 00:00:00 2001 From: sihoo Date: Thu, 1 Oct 2026 07:09:06 +0000 Subject: [PATCH] =?UTF-8?q?=ED=85=9C=ED=94=8C=EB=A6=BF=20=ED=85=8C?= =?UTF-8?q?=EB=A7=88=EB=A5=BC=20=EC=95=B1=20=EC=A0=84=EC=B2=B4=EB=A1=9C=20?= =?UTF-8?q?=ED=99=95=EC=9E=A5=20-=20=EC=A0=9C=EB=AA=A9=20=EC=95=8C?= =?UTF-8?q?=EC=95=BD,=20=EB=B0=B1=ED=92=88=ED=83=80,=20=EB=AA=A8=EB=93=A0?= =?UTF-8?q?=20=ED=99=94=EB=A9=B4=20=EB=B0=B0=EA=B2=BD/=EC=B9=B4=EB=93=9C?= =?UTF-8?q?=EC=83=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 구조색 4종(mist/paper/linen/sage)을 const에서 뺀 뒤 AppPalette.applyTemplateTheme() 으로 템플릿 장착/해제 시 한 번에 바꿔치기하게 했다. 이 4개는 전부 "배경/카드/ 구분선" 용도로만 쓰이던 색이라, 장착 즉시 거의 모든 화면(백품타 포함, AppPalette.mist/ paper/sage를 쓰는 화면이면 코드 수정 없이 자동 반영)의 배경·카드·테두리가 테마를 따라간다. ink는 텍스트/버튼 등 너무 광범위하게 쓰여서 그대로 고정. 제목 알약(TitlePill, 24개 화면이 공유)은 ink를 직접 바꾸면 안 되므로 별도로 globalProfileTheme(ValueNotifier) 하나를 구독해서 배경을 theme.secondary로 바꾸게 했다 - 이 위젯 하나만 고쳐서 모든 화면의 제목 배경에 한 번에 적용. 백품타(study_timer_screen)는 시작/재개 버튼과 큰 타이머 숫자처럼 "중립" 요소만 테마 강조색(accent)으로 바꾸고, 일시정지(warning)/종료(danger) 같은 경고성 색은 의미가 있어서 그대로 뒀다. main_dashboard/profile_customize_screen은 실제로 장착이 바뀔 때마다 applyGlobalProfileTheme()을 호출해 위 두 전역 상태를 동기화한다. 구조색을 const에서 빼면서 기존에 const로 선언돼 있던 지점 몇 곳 (app_notice, device_checkout_ledger_page, group_detail_screen, main_dashboard, teacher_attendance_page)에서 const 컴파일 에러가 나서 해당 const 키워드만 제거해 수정했다. --- lib/theme/app_palette.dart | 28 ++++++++++++++--- lib/theme/theme_sync.dart | 15 ++++++++++ lib/ui/app_notice.dart | 2 +- lib/ui/device_checkout_ledger_page.dart | 2 +- lib/ui/group_detail_screen.dart | 2 +- lib/ui/main_dashboard.dart | 10 ++++--- lib/ui/profile_customize_screen.dart | 33 +++++++++++++++++++- lib/ui/study_timer_screen.dart | 11 +++++-- lib/ui/teacher_attendance_page.dart | 8 ++--- lib/ui/title_pill.dart | 40 +++++++++++++++---------- 10 files changed, 116 insertions(+), 35 deletions(-) create mode 100644 lib/theme/theme_sync.dart 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, + ), + ), ), - ), - ), + ); + }, ); } }