대시보드 레이아웃을 목업처럼 재구성 (작은 프로필 카드 + 매이슨리 타일 그리드)
- 화면 전체를 차지하던 색상 배너를 작은 둥근 프로필 카드로 교체하고 가운데에 배치 - 앱바는 ">" 메뉴 버튼만 올라가는 투명한 얇은 바로 변경 - 균일한 그리드 대신 flutter_staggered_grid_view로 타일마다 크기가 다른 매이슨리 레이아웃 적용 (NFC 태그는 세로로 길게, 실시간 출석/서버 DB 제어는 가로로 넓게) - 화면 폭에 따라 열 개수가 자동으로 바뀌어 폰에서도 안 깨짐 - 가로로 넓지만 낮은 타일에서 내용이 넘치지 않도록 카드 배율 계산을 가로/세로 중 작은 쪽 기준으로 변경 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
+124
-83
@@ -4,6 +4,7 @@
|
||||
// (lib/function/dashboard_layout_controller.dart). 캔버스는 가로 6칸×세로 4칸,
|
||||
// 타일 크기는 1x1~4x4.
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||
import '../function/dashboard_layout_controller.dart';
|
||||
import '../function/session_store.dart';
|
||||
import '../function/student_dashboard_controller.dart';
|
||||
@@ -449,17 +450,11 @@ class _MainDashboardState extends State<MainDashboard> {
|
||||
|
||||
return Scaffold(
|
||||
backgroundColor: AppPalette.mist,
|
||||
// 🪶 큰 색상 배너 대신 작은 프로필 카드를 본문에 두므로, 앱바는 ">" 메뉴
|
||||
// 버튼(또는 편집 중 "완료" 버튼)만 올려두는 투명한 얇은 바로 둔다.
|
||||
appBar: AppBar(
|
||||
title: Text(
|
||||
theme.title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
),
|
||||
),
|
||||
centerTitle: true,
|
||||
backgroundColor: theme.color,
|
||||
foregroundColor: Colors.white,
|
||||
backgroundColor: Colors.transparent,
|
||||
foregroundColor: AppPalette.ink,
|
||||
elevation: 0,
|
||||
actions: isEditing
|
||||
? [
|
||||
@@ -469,19 +464,10 @@ class _MainDashboardState extends State<MainDashboard> {
|
||||
? const SizedBox(
|
||||
width: 16,
|
||||
height: 16,
|
||||
child: CircularProgressIndicator(
|
||||
strokeWidth: 2,
|
||||
color: Colors.white,
|
||||
),
|
||||
child: CircularProgressIndicator(strokeWidth: 2),
|
||||
)
|
||||
: const Icon(
|
||||
Icons.check_rounded,
|
||||
color: Colors.white,
|
||||
),
|
||||
label: const Text(
|
||||
'완료',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
: const Icon(Icons.check_rounded),
|
||||
label: const Text('완료'),
|
||||
),
|
||||
]
|
||||
: [
|
||||
@@ -499,56 +485,79 @@ class _MainDashboardState extends State<MainDashboard> {
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
vertical: 28,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.color,
|
||||
borderRadius: const BorderRadius.only(
|
||||
bottomLeft: Radius.circular(32),
|
||||
bottomRight: Radius.circular(32),
|
||||
const SizedBox(height: 8),
|
||||
// 🪪 작은 프로필 카드로 배치 — 화면 전체를 차지하던 배너 대신
|
||||
// 가운데에 떠 있는 카드 하나만 둔다.
|
||||
Center(
|
||||
child: Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 28,
|
||||
vertical: 18,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: theme.color,
|
||||
borderRadius: BorderRadius.circular(24),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: theme.color.withValues(alpha: 0.25),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 6),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
children: [
|
||||
Text(
|
||||
theme.title,
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
letterSpacing: 1.2,
|
||||
color: Colors.white,
|
||||
fontSize: 13,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: CircleAvatar(
|
||||
radius: 22,
|
||||
backgroundColor: AppPalette.linen,
|
||||
child: Icon(
|
||||
theme.icon,
|
||||
size: 22,
|
||||
color: theme.color,
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 10),
|
||||
Text(
|
||||
'$_displayName 님',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 17,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 3),
|
||||
Text(
|
||||
theme.subtitle,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.8),
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
child: Column(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(4),
|
||||
decoration: const BoxDecoration(
|
||||
color: Colors.white,
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: CircleAvatar(
|
||||
radius: 30,
|
||||
backgroundColor: AppPalette.linen,
|
||||
child: Icon(theme.icon, size: 32, color: theme.color),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 14),
|
||||
Text(
|
||||
'$_displayName 님',
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.white,
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
theme.subtitle,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
color: Colors.white.withValues(alpha: 0.8),
|
||||
fontSize: 14,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
const SizedBox(height: 28),
|
||||
Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
child: Row(
|
||||
@@ -603,22 +612,47 @@ class _MainDashboardState extends State<MainDashboard> {
|
||||
);
|
||||
}
|
||||
|
||||
// 📱 [기능 보류] kTileEditingEnabled가 꺼져 있을 때 쓰는 기본 그리드.
|
||||
// 화면 폭에 맞춰 열 개수가 자동으로 줄어들기 때문에 폰에서도 안 깨진다.
|
||||
// 🧱 타일마다 크기(가로/세로 칸 수)가 다른 매이슨리(벽돌쌓기) 그리드.
|
||||
// 화면 폭에 맞춰 열 개수가 자동으로 바뀌기 때문에 폰에서도 안 깨진다.
|
||||
// id는 서버 레이아웃 저장용 키와 같지만, 여기서는 그냥 "이 타일이 몇 칸짜리인지" 찾는 데만 쓴다.
|
||||
static const Map<String, (int w, int h)> _tileSpans = {
|
||||
'nfc_tag': (1, 2),
|
||||
'attendance_check': (2, 1),
|
||||
'admin_db': (2, 1),
|
||||
};
|
||||
|
||||
(int, int) _spanFor(String id) => _tileSpans[id] ?? (1, 1);
|
||||
|
||||
Widget _buildSimpleGrid(List<({String id, Widget child})> tiles) {
|
||||
return Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
child: GridView.builder(
|
||||
shrinkWrap: true,
|
||||
physics: const NeverScrollableScrollPhysics(),
|
||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 160,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
childAspectRatio: 1,
|
||||
),
|
||||
itemCount: tiles.length,
|
||||
itemBuilder: (context, index) => tiles[index].child,
|
||||
child: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final double width = constraints.maxWidth;
|
||||
final int crossAxisCount = width < 420
|
||||
? 2
|
||||
: width < 700
|
||||
? 3
|
||||
: width < 1000
|
||||
? 4
|
||||
: 6;
|
||||
|
||||
return StaggeredGrid.count(
|
||||
crossAxisCount: crossAxisCount,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
children: [
|
||||
for (final tile in tiles)
|
||||
StaggeredGridTile.count(
|
||||
crossAxisCellCount: _spanFor(
|
||||
tile.id,
|
||||
).$1.clamp(1, crossAxisCount),
|
||||
mainAxisCellCount: _spanFor(tile.id).$2,
|
||||
child: tile.child,
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
@@ -732,7 +766,14 @@ class _MainDashboardState extends State<MainDashboard> {
|
||||
return LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
// 📏 타일의 실제 픽셀 크기를 기준으로 배율을 계산 (1x1 기준 ≈ 1.0).
|
||||
final double scale = (constraints.maxWidth / 110).clamp(1.0, 3.4);
|
||||
// 가로/세로 중 더 작은 쪽을 기준으로 삼아야, 가로로 넓지만 낮은 타일에서
|
||||
// 배율이 과하게 커져 내용이 넘치는 걸 막을 수 있다.
|
||||
final double referenceSize = constraints.hasBoundedHeight
|
||||
? constraints.maxWidth < constraints.maxHeight
|
||||
? constraints.maxWidth
|
||||
: constraints.maxHeight
|
||||
: constraints.maxWidth;
|
||||
final double scale = (referenceSize / 110).clamp(1.0, 3.4);
|
||||
final double iconBoxPadding = 6 + 6 * (scale - 1);
|
||||
final double iconSize = 16 + 10 * (scale - 1);
|
||||
final double iconRadius = 10 + 6 * (scale - 1);
|
||||
|
||||
@@ -350,6 +350,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.1.1"
|
||||
flutter_staggered_grid_view:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: flutter_staggered_grid_view
|
||||
sha256: "19e7abb550c96fbfeb546b23f3ff356ee7c59a019a651f8f102a4ba9b7349395"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.7.0"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
|
||||
@@ -57,6 +57,7 @@ dependencies:
|
||||
file_picker: ^12.1.2
|
||||
desktop_drop: ^0.8.2
|
||||
web: ^1.1.1
|
||||
flutter_staggered_grid_view: ^0.7.0
|
||||
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
|
||||
Reference in New Issue
Block a user