대시보드 레이아웃을 목업처럼 재구성 (작은 프로필 카드 + 매이슨리 타일 그리드)
- 화면 전체를 차지하던 색상 배너를 작은 둥근 프로필 카드로 교체하고 가운데에 배치 - 앱바는 ">" 메뉴 버튼만 올라가는 투명한 얇은 바로 변경 - 균일한 그리드 대신 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칸,
|
// (lib/function/dashboard_layout_controller.dart). 캔버스는 가로 6칸×세로 4칸,
|
||||||
// 타일 크기는 1x1~4x4.
|
// 타일 크기는 1x1~4x4.
|
||||||
import 'package:flutter/material.dart';
|
import 'package:flutter/material.dart';
|
||||||
|
import 'package:flutter_staggered_grid_view/flutter_staggered_grid_view.dart';
|
||||||
import '../function/dashboard_layout_controller.dart';
|
import '../function/dashboard_layout_controller.dart';
|
||||||
import '../function/session_store.dart';
|
import '../function/session_store.dart';
|
||||||
import '../function/student_dashboard_controller.dart';
|
import '../function/student_dashboard_controller.dart';
|
||||||
@@ -449,17 +450,11 @@ class _MainDashboardState extends State<MainDashboard> {
|
|||||||
|
|
||||||
return Scaffold(
|
return Scaffold(
|
||||||
backgroundColor: AppPalette.mist,
|
backgroundColor: AppPalette.mist,
|
||||||
|
// 🪶 큰 색상 배너 대신 작은 프로필 카드를 본문에 두므로, 앱바는 ">" 메뉴
|
||||||
|
// 버튼(또는 편집 중 "완료" 버튼)만 올려두는 투명한 얇은 바로 둔다.
|
||||||
appBar: AppBar(
|
appBar: AppBar(
|
||||||
title: Text(
|
backgroundColor: Colors.transparent,
|
||||||
theme.title,
|
foregroundColor: AppPalette.ink,
|
||||||
style: const TextStyle(
|
|
||||||
fontWeight: FontWeight.bold,
|
|
||||||
letterSpacing: 1.2,
|
|
||||||
),
|
|
||||||
),
|
|
||||||
centerTitle: true,
|
|
||||||
backgroundColor: theme.color,
|
|
||||||
foregroundColor: Colors.white,
|
|
||||||
elevation: 0,
|
elevation: 0,
|
||||||
actions: isEditing
|
actions: isEditing
|
||||||
? [
|
? [
|
||||||
@@ -469,19 +464,10 @@ class _MainDashboardState extends State<MainDashboard> {
|
|||||||
? const SizedBox(
|
? const SizedBox(
|
||||||
width: 16,
|
width: 16,
|
||||||
height: 16,
|
height: 16,
|
||||||
child: CircularProgressIndicator(
|
child: CircularProgressIndicator(strokeWidth: 2),
|
||||||
strokeWidth: 2,
|
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
)
|
)
|
||||||
: const Icon(
|
: const Icon(Icons.check_rounded),
|
||||||
Icons.check_rounded,
|
label: const Text('완료'),
|
||||||
color: Colors.white,
|
|
||||||
),
|
|
||||||
label: const Text(
|
|
||||||
'완료',
|
|
||||||
style: TextStyle(color: Colors.white),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
]
|
]
|
||||||
: [
|
: [
|
||||||
@@ -499,56 +485,79 @@ class _MainDashboardState extends State<MainDashboard> {
|
|||||||
child: Column(
|
child: Column(
|
||||||
crossAxisAlignment: CrossAxisAlignment.start,
|
crossAxisAlignment: CrossAxisAlignment.start,
|
||||||
children: [
|
children: [
|
||||||
Container(
|
const SizedBox(height: 8),
|
||||||
width: double.infinity,
|
// 🪪 작은 프로필 카드로 배치 — 화면 전체를 차지하던 배너 대신
|
||||||
padding: const EdgeInsets.symmetric(
|
// 가운데에 떠 있는 카드 하나만 둔다.
|
||||||
horizontal: 24,
|
Center(
|
||||||
vertical: 28,
|
child: Container(
|
||||||
),
|
padding: const EdgeInsets.symmetric(
|
||||||
decoration: BoxDecoration(
|
horizontal: 28,
|
||||||
color: theme.color,
|
vertical: 18,
|
||||||
borderRadius: const BorderRadius.only(
|
),
|
||||||
bottomLeft: Radius.circular(32),
|
decoration: BoxDecoration(
|
||||||
bottomRight: Radius.circular(32),
|
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(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||||
child: Row(
|
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) {
|
Widget _buildSimpleGrid(List<({String id, Widget child})> tiles) {
|
||||||
return Padding(
|
return Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||||
child: GridView.builder(
|
child: LayoutBuilder(
|
||||||
shrinkWrap: true,
|
builder: (context, constraints) {
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
final double width = constraints.maxWidth;
|
||||||
gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent(
|
final int crossAxisCount = width < 420
|
||||||
maxCrossAxisExtent: 160,
|
? 2
|
||||||
mainAxisSpacing: 12,
|
: width < 700
|
||||||
crossAxisSpacing: 12,
|
? 3
|
||||||
childAspectRatio: 1,
|
: width < 1000
|
||||||
),
|
? 4
|
||||||
itemCount: tiles.length,
|
: 6;
|
||||||
itemBuilder: (context, index) => tiles[index].child,
|
|
||||||
|
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(
|
return LayoutBuilder(
|
||||||
builder: (context, constraints) {
|
builder: (context, constraints) {
|
||||||
// 📏 타일의 실제 픽셀 크기를 기준으로 배율을 계산 (1x1 기준 ≈ 1.0).
|
// 📏 타일의 실제 픽셀 크기를 기준으로 배율을 계산 (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 iconBoxPadding = 6 + 6 * (scale - 1);
|
||||||
final double iconSize = 16 + 10 * (scale - 1);
|
final double iconSize = 16 + 10 * (scale - 1);
|
||||||
final double iconRadius = 10 + 6 * (scale - 1);
|
final double iconRadius = 10 + 6 * (scale - 1);
|
||||||
|
|||||||
@@ -350,6 +350,14 @@ packages:
|
|||||||
url: "https://pub.dev"
|
url: "https://pub.dev"
|
||||||
source: hosted
|
source: hosted
|
||||||
version: "3.1.1"
|
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:
|
flutter_test:
|
||||||
dependency: "direct dev"
|
dependency: "direct dev"
|
||||||
description: flutter
|
description: flutter
|
||||||
|
|||||||
@@ -57,6 +57,7 @@ dependencies:
|
|||||||
file_picker: ^12.1.2
|
file_picker: ^12.1.2
|
||||||
desktop_drop: ^0.8.2
|
desktop_drop: ^0.8.2
|
||||||
web: ^1.1.1
|
web: ^1.1.1
|
||||||
|
flutter_staggered_grid_view: ^0.7.0
|
||||||
|
|
||||||
dev_dependencies:
|
dev_dependencies:
|
||||||
flutter_test:
|
flutter_test:
|
||||||
|
|||||||
Reference in New Issue
Block a user