From 0fc80eb5c55b895a2ac135c462be25a87d7ec0ad Mon Sep 17 00:00:00 2001 From: sihoo Date: Thu, 1 Oct 2026 06:13:18 +0000 Subject: [PATCH] =?UTF-8?q?=ED=85=9C=ED=94=8C=EB=A6=BF=EB=A7=88=EB=8B=A4?= =?UTF-8?q?=20=EB=A9=94=EC=9D=B8=20=EB=8C=80=EC=8B=9C=EB=B3=B4=EB=93=9C=20?= =?UTF-8?q?=EC=A0=84=EC=B2=B4=20=ED=85=8C=EB=A7=88(=EB=B0=B0=EA=B2=BD/?= =?UTF-8?q?=ED=83=80=EC=9D=BC/=EC=95=84=EC=9D=B4=EC=BD=98=20=EC=83=89)?= =?UTF-8?q?=EA=B0=80=20=EA=B0=99=EC=9D=B4=20=EB=B0=94=EB=80=8C=EA=B2=8C=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 용어 정리: 템플릿 = 상점에서 사는 색/애니메이션 상품, 테마 = 그 템플릿이 몰고 오는 화면 전체 디자인(배경색/타일색/아이콘색). 1단계로 메인 대시보드부터 적용 - 프로필 카드 그라데이션뿐 아니라 페이지 배경, 타일 카드 배경/테두리, 아이콘 동그라미 배경, 타일 아이콘 강조색, 제목/부제목 텍스트 색까지 장착한 템플릿에 맞춰 통째로 바뀐다. 잠긴/비활성 타일(NFC 미인증 등)은 기존처럼 중립 회색(sage)을 그대로 써서 상태 구분은 유지. 6개 기존 템플릿마다 직접 디자인한 테마(lib/theme/profile_themes.dart)를 새로 만들었고, 최고가 "갤럭시" 템플릿은 다른 5개와 달리 통째로 다크 테마로 디자인해 프리미엄 아이템 느낌을 살렸다. 백엔드 /api/shop/equipped가 템플릿 code를 같이 내려주도록 수정해서 프론트엔드가 code로 디자인해둔 테마를 찾아 쓸 수 있게 했다. --- lib/function/profile_shop_controller.dart | 12 +-- lib/theme/profile_themes.dart | 98 +++++++++++++++++++++++ lib/ui/main_dashboard.dart | 81 ++++++++++--------- 3 files changed, 146 insertions(+), 45 deletions(-) create mode 100644 lib/theme/profile_themes.dart diff --git a/lib/function/profile_shop_controller.dart b/lib/function/profile_shop_controller.dart index a830788..c4114e7 100644 --- a/lib/function/profile_shop_controller.dart +++ b/lib/function/profile_shop_controller.dart @@ -5,6 +5,7 @@ import 'package:flutter/foundation.dart'; import 'package:flutter/material.dart' show Color; import 'package:http/http.dart' as http; import '../config.dart'; +import '../theme/profile_themes.dart'; class ShopTemplate { final int id; @@ -125,9 +126,7 @@ class ProfileShopController extends ChangeNotifier { /// 🏠 메인 대시보드 프로필 카드 렌더링 전용 - 상점 화면을 열지 않고도 /// 현재 장착 중인 테마 색상만 가볍게 조회한다. - static Future<(Color? primary, Color? secondary)> fetchEquippedColors( - String studentId, - ) async { + static Future fetchEquippedTheme(String studentId) async { try { final url = Uri.parse( '$baseUrl/api/shop/equipped?studentId=${Uri.encodeComponent(studentId)}', @@ -136,15 +135,12 @@ class ProfileShopController extends ChangeNotifier { if (response.statusCode == 200) { final data = jsonDecode(response.body); if (data['equipped'] == true) { - return ( - _parseColor(data['primaryColor'] as String), - _parseColor(data['secondaryColor'] as String), - ); + return profileThemes[data['code'] as String?]; } } } catch (_) { // 실패하면 그냥 기본 모습으로 보여준다. } - return (null, null); + return null; } } diff --git a/lib/theme/profile_themes.dart b/lib/theme/profile_themes.dart new file mode 100644 index 0000000..a9729b9 --- /dev/null +++ b/lib/theme/profile_themes.dart @@ -0,0 +1,98 @@ +// 🎨 프로필 꾸미기 "템플릿"을 장착하면 프로필 카드 색뿐 아니라 화면 전체의 +// 배경/타일/아이콘 색까지 통째로 바뀌는 "테마" 데이터. +// 템플릿은 상점에서 사는 색/애니메이션 상품, 테마는 그 템플릿이 몰고 오는 +// 화면 전체 디자인(배경색/타일색/아이콘색/모양)을 가리킨다 - 용어 구분은 +// 사장님이 정해준 것을 그대로 따른다. +// +// 템플릿 하나하나에 대해 직접 디자인한 고정 팔레트라서, 서버에서 내려주는 +// code(예: "lavender")로 이 맵을 찾아 쓴다. 템플릿이 늘어나면 여기도 추가해야 한다. +import 'package:flutter/material.dart'; + +class ProfileTheme { + final Color primary; // 프로필 카드 그라데이션 시작색 (템플릿 색) + final Color secondary; // 프로필 카드 그라데이션 끝색 (템플릿 색) + final Color background; // 페이지 배경 + final Color surface; // 타일/카드 배경 + final Color surfaceAlt; // 아이콘 동그라미 등 보조 표면 + final Color border; // 타일 테두리/구분선 + final Color accent; // 타일 아이콘 등 강조색 + final Color onBackground; // 배경/타일 위에 올라가는 기본 텍스트 색 + final bool isDark; + + const ProfileTheme({ + required this.primary, + required this.secondary, + required this.background, + required this.surface, + required this.surfaceAlt, + required this.border, + required this.accent, + required this.onBackground, + this.isDark = false, + }); +} + +const Map profileThemes = { + 'lavender': ProfileTheme( + primary: Color(0xFFB39DDB), + secondary: Color(0xFF7E57C2), + background: Color(0xFFF5F0FA), + surface: Color(0xFFFFFFFF), + surfaceAlt: Color(0xFFEDE4F7), + border: Color(0xFFDCCCEF), + accent: Color(0xFF7E57C2), + onBackground: Color(0xFF1C1C1C), + ), + 'mint': ProfileTheme( + primary: Color(0xFF80CBC4), + secondary: Color(0xFF00897B), + background: Color(0xFFEAF6F4), + surface: Color(0xFFFFFFFF), + surfaceAlt: Color(0xFFDBF0EC), + border: Color(0xFFBFE3DC), + accent: Color(0xFF00897B), + onBackground: Color(0xFF1C1C1C), + ), + 'sunset': ProfileTheme( + primary: Color(0xFFFFB74D), + secondary: Color(0xFFE64A19), + background: Color(0xFFFFF3E6), + surface: Color(0xFFFFFFFF), + surfaceAlt: Color(0xFFFFE5CC), + border: Color(0xFFFFD1A6), + accent: Color(0xFFE64A19), + onBackground: Color(0xFF1C1C1C), + ), + 'ocean': ProfileTheme( + primary: Color(0xFF4FC3F7), + secondary: Color(0xFF01579B), + background: Color(0xFFEAF6FC), + surface: Color(0xFFFFFFFF), + surfaceAlt: Color(0xFFD9EEF9), + border: Color(0xFFBBDDF0), + accent: Color(0xFF01579B), + onBackground: Color(0xFF1C1C1C), + ), + 'aurora': ProfileTheme( + primary: Color(0xFF81C784), + secondary: Color(0xFF1565C0), + background: Color(0xFFEDF6EF), + surface: Color(0xFFFFFFFF), + surfaceAlt: Color(0xFFDCEEE0), + border: Color(0xFFC3E2CA), + accent: Color(0xFF1565C0), + onBackground: Color(0xFF1C1C1C), + ), + // 🌌 최고가 템플릿 - 다른 5개와 달리 통째로 다크 테마라서 들어가자마자 확 다르게 느껴진다. + 'galaxy': ProfileTheme( + primary: Color(0xFF9575CD), + secondary: Color(0xFF1A237E), + background: Color(0xFF14122B), + surface: Color(0xFF211F42), + surfaceAlt: Color(0xFF2C2A52), + border: Color(0xFF3A3768), + accent: Color(0xFFB39DDB), + onBackground: Color(0xFFF5F3FF), + isDark: true, + ), +}; diff --git a/lib/ui/main_dashboard.dart b/lib/ui/main_dashboard.dart index 8a2aa3c..8b1111e 100644 --- a/lib/ui/main_dashboard.dart +++ b/lib/ui/main_dashboard.dart @@ -12,6 +12,7 @@ import '../function/session_store.dart'; import '../function/student_dashboard_controller.dart'; import '../models/dashboard_tile_layout.dart'; import '../theme/app_palette.dart'; +import '../theme/profile_themes.dart'; import 'admin_dashboard.dart'; import 'app_notice.dart'; import 'board_home_screen.dart'; @@ -85,17 +86,17 @@ class _MainDashboardState extends State { } } - // 🛍️ 프로필 꾸미기 상점에서 장착한 테마 색상 (없으면 기본 모습 그대로). - Color? _equippedPrimary; - Color? _equippedSecondary; + // 🛍️ 프로필 꾸미기 상점에서 장착한 템플릿이 몰고 오는 테마 (없으면 기본 모습 그대로). + ProfileTheme? _equippedTheme; + + // 🎨 타일 아이콘 등 강조색에 쓸 색 - 테마가 없으면 원래 쓰던 AppPalette.ink. + Color get _tileAccent => _equippedTheme?.accent ?? AppPalette.ink; Future _loadEquippedTheme() async { - final (primary, secondary) = - await ProfileShopController.fetchEquippedColors(_displayId); + final theme = await ProfileShopController.fetchEquippedTheme(_displayId); if (!mounted) return; setState(() { - _equippedPrimary = primary; - _equippedSecondary = secondary; + _equippedTheme = theme; }); } @@ -312,7 +313,7 @@ class _MainDashboardState extends State { : Icons.lock_rounded, title: 'NFC 태그', subtitle: widget.isDeviceMatched ? '출석 및 폰 수거 완료' : '본인 인증 기기 전용', - color: widget.isDeviceMatched ? AppPalette.ink : AppPalette.sage, + color: widget.isDeviceMatched ? _tileAccent : AppPalette.sage, onTap: widget.isDeviceMatched ? () => _openPocketCheckIn(context) : () => AppNotice.show( @@ -330,7 +331,7 @@ class _MainDashboardState extends State { icon: Icons.fastfood_rounded, title: '실시간 학교 상황', subtitle: '급식실 줄 & 매점 재고 확인', - color: AppPalette.ink, + color: _tileAccent, onTap: () => AppNotice.show( context, '실시간 학교 상황 페이지로 이동합니다.', @@ -344,7 +345,7 @@ class _MainDashboardState extends State { icon: Icons.tablet_mac_rounded, title: '스마트기기 반출', subtitle: '패드 사용 신청 (시간/목적)', - color: AppPalette.ink, + color: _tileAccent, onTap: () => pushLaunchpad( context, (context) => DeviceCheckoutRequestScreen( @@ -360,7 +361,7 @@ class _MainDashboardState extends State { icon: Icons.timer_rounded, title: '백품타', subtitle: '공부 타이머 | 학년별 공부 랭킹', - color: AppPalette.ink, + color: _tileAccent, onTap: () => pushLaunchpad( context, (context) => StudyTimerScreen( @@ -377,7 +378,7 @@ class _MainDashboardState extends State { icon: Icons.forum_rounded, title: '백판', subtitle: '익명 커뮤니티 게시판', - color: AppPalette.ink, + color: _tileAccent, onTap: () => pushLaunchpad( context, (context) => BoardHomeScreen(studentId: _displayId), @@ -390,7 +391,7 @@ class _MainDashboardState extends State { icon: Icons.campaign_rounded, title: '선생님 호출', subtitle: '교무실 위치 확인 및 호출', - color: AppPalette.ink, + color: _tileAccent, onTap: () => pushLaunchpad( context, (context) => TeacherCallScreen( @@ -406,7 +407,7 @@ class _MainDashboardState extends State { icon: Icons.people_alt_rounded, title: '친구', subtitle: '학번으로 친구 추가', - color: AppPalette.ink, + color: _tileAccent, onTap: () => pushLaunchpad( context, (context) => @@ -420,7 +421,7 @@ class _MainDashboardState extends State { icon: Icons.groups_rounded, title: '그룹 스터디', subtitle: '친구랑 그룹 만들어 같이 공부', - color: AppPalette.ink, + color: _tileAccent, onTap: () => pushLaunchpad( context, (context) => GroupListScreen( @@ -437,7 +438,7 @@ class _MainDashboardState extends State { icon: Icons.calendar_month_rounded, title: '출석 달력', subtitle: '연속 출석 기록 확인', - color: AppPalette.ink, + color: _tileAccent, onTap: () => pushLaunchpad( context, (context) => StudyCalendarScreen(studentId: _displayId), @@ -453,7 +454,7 @@ class _MainDashboardState extends State { icon: Icons.assignment_turned_in_rounded, title: '실시간 출석 확인', subtitle: '학생 제출 로그 모니터링', - color: AppPalette.ink, + color: _tileAccent, onTap: () => pushLaunchpad( context, (context) => const TeacherAttendancePage(), @@ -466,7 +467,7 @@ class _MainDashboardState extends State { icon: Icons.manage_accounts_rounded, title: '학생 계정 관리', subtitle: '계정 추가 및 강제 리셋', - color: AppPalette.ink, + color: _tileAccent, onTap: () => pushLaunchpad( context, (context) => const TeacherStudentManagementPage(), @@ -479,7 +480,7 @@ class _MainDashboardState extends State { icon: Icons.tablet_mac_rounded, title: '스마트기기 반출 대장', subtitle: '패드 반출 신청 승인/거절', - color: AppPalette.ink, + color: _tileAccent, onTap: () => pushLaunchpad( context, (context) => DeviceCheckoutLedgerPage( @@ -495,7 +496,7 @@ class _MainDashboardState extends State { icon: Icons.shield_outlined, title: '백판 신고 검토', subtitle: '학생 게시판 신고 처리', - color: AppPalette.ink, + color: _tileAccent, onTap: () => pushLaunchpad( context, (context) => BoardReportScreen(reviewerId: _displayId), @@ -508,7 +509,7 @@ class _MainDashboardState extends State { icon: Icons.event_available_rounded, title: '출석 달력 관리', subtitle: '날짜별 선물/이벤트 등록', - color: AppPalette.ink, + color: _tileAccent, onTap: () => pushLaunchpad( context, (context) => StudyCalendarScreen( @@ -524,7 +525,7 @@ class _MainDashboardState extends State { icon: Icons.my_location_rounded, title: '내 위치 알리기', subtitle: '학생 호출 확인 및 위치 등록', - color: AppPalette.ink, + color: _tileAccent, onTap: () => pushLaunchpad( context, (context) => TeacherLocationScreen(teacherId: _displayId), @@ -540,7 +541,7 @@ class _MainDashboardState extends State { icon: Icons.terminal_rounded, title: '서버 DB 제어', subtitle: '시스템 원격 초기화', - color: AppPalette.ink, + color: _tileAccent, onTap: () => pushLaunchpad(context, (context) => const AdminDashboard()), ), @@ -551,7 +552,7 @@ class _MainDashboardState extends State { icon: Icons.delete_sweep_rounded, title: '계정 강제 삭제', subtitle: '학생 및 교사 DB 삭제', - color: AppPalette.ink, + color: _tileAccent, onTap: () => _showDeleteUserDialog(context), ), )); @@ -561,7 +562,7 @@ class _MainDashboardState extends State { icon: Icons.edit_note_rounded, title: 'NFC 태그 쓰기', subtitle: '주머니 스티커 초기 설정', - color: AppPalette.ink, + color: _tileAccent, onTap: () => pushLaunchpad(context, (context) => const NfcTagWriterScreen()), ), @@ -623,7 +624,7 @@ class _MainDashboardState extends State { final bool isEditing = _layout.isEditing; return Scaffold( - backgroundColor: AppPalette.mist, + backgroundColor: _equippedTheme?.background ?? AppPalette.mist, // 🪶 큰 색상 배너 대신 작은 프로필 카드를 본문에 두므로, 앱바는 ">" 메뉴 // 버튼(또는 편집 중 "완료" 버튼)만 올려두는 투명한 얇은 바로 둔다. appBar: AppBar( @@ -672,21 +673,21 @@ class _MainDashboardState extends State { vertical: 18, ), decoration: BoxDecoration( - color: _equippedPrimary == null ? theme.color : null, - gradient: _equippedPrimary == null + color: _equippedTheme == null ? theme.color : null, + gradient: _equippedTheme == null ? null : LinearGradient( begin: Alignment.topLeft, end: Alignment.bottomRight, colors: [ - _equippedPrimary!, - _equippedSecondary!, + _equippedTheme!.primary, + _equippedTheme!.secondary, ], ), borderRadius: BorderRadius.circular(24), boxShadow: [ BoxShadow( - color: (_equippedPrimary ?? theme.color) + color: (_equippedTheme?.primary ?? theme.color) .withValues(alpha: 0.25), blurRadius: 16, offset: const Offset(0, 6), @@ -959,7 +960,7 @@ class _MainDashboardState extends State { width: 26, height: 26, decoration: BoxDecoration( - color: AppPalette.ink, + color: _tileAccent, borderRadius: BorderRadius.circular(8), border: Border.all( color: AppPalette.paper, @@ -1026,9 +1027,12 @@ class _MainDashboardState extends State { child: Ink( padding: EdgeInsets.all(cardPadding), decoration: BoxDecoration( - color: AppPalette.paper, + color: _equippedTheme?.surface ?? AppPalette.paper, borderRadius: BorderRadius.circular(cardRadius), - border: Border.all(color: AppPalette.sage, width: 1), + border: Border.all( + color: _equippedTheme?.border ?? AppPalette.sage, + width: 1, + ), ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, @@ -1037,7 +1041,7 @@ class _MainDashboardState extends State { Container( padding: EdgeInsets.all(iconBoxPadding), decoration: BoxDecoration( - color: AppPalette.linen, + color: _equippedTheme?.surfaceAlt ?? AppPalette.linen, borderRadius: BorderRadius.circular(iconRadius), ), child: Icon(icon, color: color, size: iconSize), @@ -1056,7 +1060,9 @@ class _MainDashboardState extends State { style: TextStyle( fontSize: titleFontSize, fontWeight: FontWeight.bold, - color: AppPalette.ink, + color: + _equippedTheme?.onBackground ?? + AppPalette.ink, ), ), ), @@ -1081,7 +1087,8 @@ class _MainDashboardState extends State { overflow: TextOverflow.ellipsis, style: TextStyle( fontSize: subtitleFontSize, - color: AppPalette.ink.withValues(alpha: 0.55), + color: (_equippedTheme?.onBackground ?? AppPalette.ink) + .withValues(alpha: 0.55), height: 1.15, ), ),