diff --git a/lib/ui/main_dashboard.dart b/lib/ui/main_dashboard.dart index 967b729..c45b618 100644 --- a/lib/ui/main_dashboard.dart +++ b/lib/ui/main_dashboard.dart @@ -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 { 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 { ? 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 { 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 { ); } - // 📱 [기능 보류] kTileEditingEnabled가 꺼져 있을 때 쓰는 기본 그리드. - // 화면 폭에 맞춰 열 개수가 자동으로 줄어들기 때문에 폰에서도 안 깨진다. + // 🧱 타일마다 크기(가로/세로 칸 수)가 다른 매이슨리(벽돌쌓기) 그리드. + // 화면 폭에 맞춰 열 개수가 자동으로 바뀌기 때문에 폰에서도 안 깨진다. + // id는 서버 레이아웃 저장용 키와 같지만, 여기서는 그냥 "이 타일이 몇 칸짜리인지" 찾는 데만 쓴다. + static const Map _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 { 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); diff --git a/pubspec.lock b/pubspec.lock index 9cd88bd..16ed42e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -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 diff --git a/pubspec.yaml b/pubspec.yaml index b9ccebe..196b37c 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -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: