템플릿마다 메인 대시보드 전체 테마(배경/타일/아이콘 색)가 같이 바뀌게 추가

용어 정리: 템플릿 = 상점에서 사는 색/애니메이션 상품, 테마 = 그 템플릿이
몰고 오는 화면 전체 디자인(배경색/타일색/아이콘색). 1단계로 메인 대시보드부터
적용 - 프로필 카드 그라데이션뿐 아니라 페이지 배경, 타일 카드 배경/테두리,
아이콘 동그라미 배경, 타일 아이콘 강조색, 제목/부제목 텍스트 색까지 장착한
템플릿에 맞춰 통째로 바뀐다. 잠긴/비활성 타일(NFC 미인증 등)은 기존처럼
중립 회색(sage)을 그대로 써서 상태 구분은 유지.

6개 기존 템플릿마다 직접 디자인한 테마(lib/theme/profile_themes.dart)를
새로 만들었고, 최고가 "갤럭시" 템플릿은 다른 5개와 달리 통째로 다크 테마로
디자인해 프리미엄 아이템 느낌을 살렸다.

백엔드 /api/shop/equipped가 템플릿 code를 같이 내려주도록 수정해서
프론트엔드가 code로 디자인해둔 테마를 찾아 쓸 수 있게 했다.
This commit is contained in:
2026-10-01 06:13:18 +00:00
parent edf0444b71
commit 0fc80eb5c5
3 changed files with 146 additions and 45 deletions
+44 -37
View File
@@ -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<MainDashboard> {
}
}
// 🛍️ 프로필 꾸미기 상점에서 장착한 테마 색상 (없으면 기본 모습 그대로).
Color? _equippedPrimary;
Color? _equippedSecondary;
// 🛍️ 프로필 꾸미기 상점에서 장착한 템플릿이 몰고 오는 테마 (없으면 기본 모습 그대로).
ProfileTheme? _equippedTheme;
// 🎨 타일 아이콘 등 강조색에 쓸 색 - 테마가 없으면 원래 쓰던 AppPalette.ink.
Color get _tileAccent => _equippedTheme?.accent ?? AppPalette.ink;
Future<void> _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<MainDashboard> {
: 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<MainDashboard> {
icon: Icons.fastfood_rounded,
title: '실시간 학교 상황',
subtitle: '급식실 줄 & 매점 재고 확인',
color: AppPalette.ink,
color: _tileAccent,
onTap: () => AppNotice.show(
context,
'실시간 학교 상황 페이지로 이동합니다.',
@@ -344,7 +345,7 @@ class _MainDashboardState extends State<MainDashboard> {
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<MainDashboard> {
icon: Icons.timer_rounded,
title: '백품타',
subtitle: '공부 타이머 | 학년별 공부 랭킹',
color: AppPalette.ink,
color: _tileAccent,
onTap: () => pushLaunchpad(
context,
(context) => StudyTimerScreen(
@@ -377,7 +378,7 @@ class _MainDashboardState extends State<MainDashboard> {
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<MainDashboard> {
icon: Icons.campaign_rounded,
title: '선생님 호출',
subtitle: '교무실 위치 확인 및 호출',
color: AppPalette.ink,
color: _tileAccent,
onTap: () => pushLaunchpad(
context,
(context) => TeacherCallScreen(
@@ -406,7 +407,7 @@ class _MainDashboardState extends State<MainDashboard> {
icon: Icons.people_alt_rounded,
title: '친구',
subtitle: '학번으로 친구 추가',
color: AppPalette.ink,
color: _tileAccent,
onTap: () => pushLaunchpad(
context,
(context) =>
@@ -420,7 +421,7 @@ class _MainDashboardState extends State<MainDashboard> {
icon: Icons.groups_rounded,
title: '그룹 스터디',
subtitle: '친구랑 그룹 만들어 같이 공부',
color: AppPalette.ink,
color: _tileAccent,
onTap: () => pushLaunchpad(
context,
(context) => GroupListScreen(
@@ -437,7 +438,7 @@ class _MainDashboardState extends State<MainDashboard> {
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<MainDashboard> {
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<MainDashboard> {
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<MainDashboard> {
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<MainDashboard> {
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<MainDashboard> {
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<MainDashboard> {
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<MainDashboard> {
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<MainDashboard> {
icon: Icons.delete_sweep_rounded,
title: '계정 강제 삭제',
subtitle: '학생 및 교사 DB 삭제',
color: AppPalette.ink,
color: _tileAccent,
onTap: () => _showDeleteUserDialog(context),
),
));
@@ -561,7 +562,7 @@ class _MainDashboardState extends State<MainDashboard> {
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<MainDashboard> {
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<MainDashboard> {
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<MainDashboard> {
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<MainDashboard> {
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<MainDashboard> {
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<MainDashboard> {
style: TextStyle(
fontSize: titleFontSize,
fontWeight: FontWeight.bold,
color: AppPalette.ink,
color:
_equippedTheme?.onBackground ??
AppPalette.ink,
),
),
),
@@ -1081,7 +1087,8 @@ class _MainDashboardState extends State<MainDashboard> {
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,
),
),