템플릿마다 메인 대시보드 전체 테마(배경/타일/아이콘 색)가 같이 바뀌게 추가
용어 정리: 템플릿 = 상점에서 사는 색/애니메이션 상품, 테마 = 그 템플릿이 몰고 오는 화면 전체 디자인(배경색/타일색/아이콘색). 1단계로 메인 대시보드부터 적용 - 프로필 카드 그라데이션뿐 아니라 페이지 배경, 타일 카드 배경/테두리, 아이콘 동그라미 배경, 타일 아이콘 강조색, 제목/부제목 텍스트 색까지 장착한 템플릿에 맞춰 통째로 바뀐다. 잠긴/비활성 타일(NFC 미인증 등)은 기존처럼 중립 회색(sage)을 그대로 써서 상태 구분은 유지. 6개 기존 템플릿마다 직접 디자인한 테마(lib/theme/profile_themes.dart)를 새로 만들었고, 최고가 "갤럭시" 템플릿은 다른 5개와 달리 통째로 다크 테마로 디자인해 프리미엄 아이템 느낌을 살렸다. 백엔드 /api/shop/equipped가 템플릿 code를 같이 내려주도록 수정해서 프론트엔드가 code로 디자인해둔 테마를 찾아 쓸 수 있게 했다.
This commit is contained in:
@@ -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<ProfileTheme?> 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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String, ProfileTheme> 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,
|
||||
),
|
||||
};
|
||||
+44
-37
@@ -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,
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user