Compare commits
2
Commits
49ab432fe9
...
77ef666e16
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
77ef666e16 | ||
|
|
c8ed8f2fd2 |
+1
-1
@@ -1 +1 @@
|
||||
{"flutter":{"platforms":{"android":{"default":{"projectId":"school-display-ff28f","appId":"1:137322214849:android:2595999513206bdeb16b40","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"school-display-ff28f","configurations":{"android":"1:137322214849:android:2595999513206bdeb16b40","web":"1:137322214849:web:0423e5c788d95761b16b40"}}}}}}
|
||||
{"flutter":{"platforms":{"android":{"default":{"projectId":"school-display-ff28f","appId":"1:137322214849:android:2595999513206bdeb16b40","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"school-display-ff28f","configurations":{"android":"1:137322214849:android:2595999513206bdeb16b40","web":"1:137322214849:web:0423e5c788d95761b16b40"}}}}},"hosting":{"public":"build/web","ignore":["firebase.json","**/.*"],"rewrites":[{"source":"**","destination":"/index.html"}]}}
|
||||
@@ -23,6 +23,7 @@ final RegExp _logNamePattern = RegExp(r'^(.*?)\s*\(([^)]*)\)$');
|
||||
class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
|
||||
List<dynamic> _roster = []; // 전체 학생 명단 (/api/users)
|
||||
List<dynamic> _logs = []; // 출석 로그 (/api/logs)
|
||||
Map<String, dynamic> _activeViolationsByStudentId = {}; // 무단반출 중인 학생 (/api/violations/active)
|
||||
Timer? _timer;
|
||||
bool _isLoading = true;
|
||||
String _filterType = "ALL"; // "ALL", "CHECKED_IN", "ABSENT"
|
||||
@@ -45,17 +46,29 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
|
||||
final results = await Future.wait([
|
||||
http.get(Uri.parse('$baseUrl/api/users')),
|
||||
http.get(Uri.parse('$baseUrl/api/logs')),
|
||||
http.get(Uri.parse('$baseUrl/api/violations/active')),
|
||||
]);
|
||||
|
||||
final usersRes = results[0];
|
||||
final logsRes = results[1];
|
||||
final violationsRes = results[2];
|
||||
|
||||
if (usersRes.statusCode == 200 && logsRes.statusCode == 200) {
|
||||
final usersData = jsonDecode(utf8.decode(usersRes.bodyBytes));
|
||||
final logsData = jsonDecode(utf8.decode(logsRes.bodyBytes));
|
||||
|
||||
Map<String, dynamic> activeViolations = {};
|
||||
if (violationsRes.statusCode == 200) {
|
||||
final violationsData = jsonDecode(utf8.decode(violationsRes.bodyBytes));
|
||||
for (final v in (violationsData['violations'] ?? [])) {
|
||||
activeViolations[v['student_id'].toString()] = v;
|
||||
}
|
||||
}
|
||||
|
||||
setState(() {
|
||||
_roster = usersData['users'] ?? [];
|
||||
_logs = logsData['logs'] ?? [];
|
||||
_activeViolationsByStudentId = activeViolations;
|
||||
_isLoading = false;
|
||||
});
|
||||
} else {
|
||||
@@ -66,6 +79,125 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
|
||||
}
|
||||
}
|
||||
|
||||
/// 🚨 선생님이 특정 학생에게 지금부터 N분간 반출을 허용한다.
|
||||
Future<void> _allowRemoval(String studentId, int minutes) async {
|
||||
try {
|
||||
final response = await http.post(
|
||||
Uri.parse('$baseUrl/api/violations/allow'),
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: jsonEncode({"studentId": studentId, "minutes": minutes}),
|
||||
);
|
||||
final result = jsonDecode(utf8.decode(response.bodyBytes));
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(result['message'] ?? '처리되었습니다.')),
|
||||
);
|
||||
_fetchAll();
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('❌ 반출 허용 실패: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// ⏰ 지금부터 N분간 전체 학생의 반출을 자동으로 허용한다 (쉬는시간 등).
|
||||
Future<void> _setPermissionWindow(int minutes) async {
|
||||
try {
|
||||
final response = await http.post(
|
||||
Uri.parse('$baseUrl/api/permissions/window'),
|
||||
headers: {"Content-Type": "application/json"},
|
||||
body: jsonEncode({"minutes": minutes}),
|
||||
);
|
||||
final result = jsonDecode(utf8.decode(response.bodyBytes));
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text(result['message'] ?? '처리되었습니다.')),
|
||||
);
|
||||
_fetchAll();
|
||||
} catch (e) {
|
||||
if (!mounted) return;
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(content: Text('❌ 설정 실패: $e')),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _showAllowDialog(String studentId, String studentName) {
|
||||
final controller = TextEditingController(text: "5");
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: Text('$studentName 학생 반출 허용'),
|
||||
content: TextField(
|
||||
controller: controller,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '허용 시간 (분)',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('취소'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
final minutes = int.tryParse(controller.text.trim()) ?? 5;
|
||||
Navigator.pop(context);
|
||||
_allowRemoval(studentId, minutes);
|
||||
},
|
||||
child: const Text('허용하기'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
void _showPermissionWindowDialog() {
|
||||
final controller = TextEditingController(text: "10");
|
||||
showDialog(
|
||||
context: context,
|
||||
builder: (context) => AlertDialog(
|
||||
title: const Text('⏰ 전체 학생 반출 허용 시간 설정'),
|
||||
content: Column(
|
||||
mainAxisSize: MainAxisSize.min,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
const Text(
|
||||
'쉬는시간처럼 지금부터 일정 시간 동안 모든 학생의 반출을 자동으로 허용합니다.',
|
||||
style: TextStyle(fontSize: 13, color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 16),
|
||||
TextField(
|
||||
controller: controller,
|
||||
keyboardType: TextInputType.number,
|
||||
decoration: const InputDecoration(
|
||||
labelText: '지금부터 허용 시간 (분)',
|
||||
border: OutlineInputBorder(),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
actions: [
|
||||
TextButton(
|
||||
onPressed: () => Navigator.pop(context),
|
||||
child: const Text('취소'),
|
||||
),
|
||||
ElevatedButton(
|
||||
onPressed: () {
|
||||
final minutes = int.tryParse(controller.text.trim()) ?? 10;
|
||||
Navigator.pop(context);
|
||||
_setPermissionWindow(minutes);
|
||||
},
|
||||
child: const Text('설정하기'),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// 오늘 날짜 기준으로 학번별 "가장 최근 출석 기록"만 남긴 맵을 만든다.
|
||||
Map<String, Map<String, String>> get _todaysCheckInsByStudentId {
|
||||
final String todayPrefix = DateTime.now().toIso8601String().substring(
|
||||
@@ -100,12 +232,16 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
|
||||
return _roster.map((u) {
|
||||
final String id = u['id']?.toString() ?? '';
|
||||
final checkIn = checkIns[id];
|
||||
final violation = _activeViolationsByStudentId[id];
|
||||
return {
|
||||
'studentId': id,
|
||||
'studentName': u['name']?.toString() ?? '',
|
||||
'isCheckedIn': checkIn != null,
|
||||
'pocketNumber': checkIn?['pocketNumber'],
|
||||
'checkInTime': checkIn?['time'],
|
||||
'hasActiveViolation': violation != null,
|
||||
'violationPocket': violation?['pocket_number'],
|
||||
'violationTime': violation?['time'],
|
||||
};
|
||||
}).toList();
|
||||
}
|
||||
@@ -137,12 +273,38 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
|
||||
backgroundColor: Colors.blue,
|
||||
foregroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
actions: [
|
||||
TextButton.icon(
|
||||
onPressed: _showPermissionWindowDialog,
|
||||
icon: const Icon(Icons.timer_outlined, color: Colors.white),
|
||||
label: const Text(
|
||||
'반출 허용 시간 설정',
|
||||
style: TextStyle(color: Colors.white),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
],
|
||||
),
|
||||
body: _isLoading
|
||||
? const Center(child: CircularProgressIndicator())
|
||||
: Column(
|
||||
: LayoutBuilder(
|
||||
builder: (context, constraints) {
|
||||
final bool isWide = constraints.maxWidth >= 800;
|
||||
return isWide
|
||||
? _buildDesktopBody(totalCount, checkedInCount, absentCount)
|
||||
: _buildMobileBody(totalCount, checkedInCount, absentCount);
|
||||
},
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 📱 모바일 레이아웃 (기존 카드 리스트)
|
||||
// -----------------------------------------------------------------------
|
||||
Widget _buildMobileBody(int total, int checkedIn, int absent) {
|
||||
return Column(
|
||||
children: [
|
||||
_buildSummaryCards(totalCount, checkedInCount, absentCount),
|
||||
_buildSummaryCards(total, checkedIn, absent),
|
||||
_buildFilterChips(),
|
||||
Expanded(
|
||||
child: _filteredStudents.isEmpty
|
||||
@@ -158,6 +320,7 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
|
||||
itemBuilder: (context, index) {
|
||||
final student = _filteredStudents[index];
|
||||
final bool isCheckedIn = student['isCheckedIn'];
|
||||
final bool hasViolation = student['hasActiveViolation'] == true;
|
||||
return Container(
|
||||
margin: const EdgeInsets.symmetric(
|
||||
horizontal: 24,
|
||||
@@ -165,8 +328,11 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
|
||||
),
|
||||
padding: const EdgeInsets.all(18),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
color: hasViolation ? Colors.red[50] : Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: hasViolation
|
||||
? Border.all(color: Colors.red[300]!, width: 1.5)
|
||||
: null,
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.03),
|
||||
@@ -175,32 +341,36 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(10),
|
||||
decoration: BoxDecoration(
|
||||
color: (isCheckedIn
|
||||
? Colors.blue
|
||||
: Colors.red)
|
||||
color: (hasViolation
|
||||
? Colors.red
|
||||
: (isCheckedIn ? Colors.blue : Colors.red))
|
||||
.withValues(alpha: 0.1),
|
||||
shape: BoxShape.circle,
|
||||
),
|
||||
child: Icon(
|
||||
isCheckedIn
|
||||
hasViolation
|
||||
? Icons.warning_amber_rounded
|
||||
: (isCheckedIn
|
||||
? Icons.check_circle_rounded
|
||||
: Icons.error_rounded,
|
||||
color: isCheckedIn
|
||||
? Colors.blue
|
||||
: Colors.red,
|
||||
: Icons.error_rounded),
|
||||
color: hasViolation
|
||||
? Colors.red
|
||||
: (isCheckedIn ? Colors.blue : Colors.red),
|
||||
size: 24,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: Column(
|
||||
crossAxisAlignment:
|
||||
CrossAxisAlignment.start,
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
'${student['studentName']} 학생',
|
||||
@@ -211,21 +381,27 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
|
||||
),
|
||||
const SizedBox(height: 4),
|
||||
Text(
|
||||
isCheckedIn
|
||||
hasViolation
|
||||
? '🚨 무단반출 감지! (${student['violationTime']})'
|
||||
: (isCheckedIn
|
||||
? '학번: ${student['studentId']} | 제출시간: ${student['checkInTime']}'
|
||||
: '학번: ${student['studentId']} | 미제출',
|
||||
: '학번: ${student['studentId']} | 미제출'),
|
||||
style: TextStyle(
|
||||
color: isCheckedIn
|
||||
color: hasViolation
|
||||
? Colors.red[700]
|
||||
: (isCheckedIn
|
||||
? Colors.grey[600]
|
||||
: Colors.red[400],
|
||||
: Colors.red[400]),
|
||||
fontSize: 12,
|
||||
fontWeight: hasViolation
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
if (isCheckedIn &&
|
||||
student['pocketNumber'] != null)
|
||||
if (isCheckedIn && student['pocketNumber'] != null)
|
||||
Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
@@ -234,9 +410,8 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: Colors.blue.shade200,
|
||||
),
|
||||
border:
|
||||
Border.all(color: Colors.blue.shade200),
|
||||
),
|
||||
child: Text(
|
||||
student['pocketNumber'],
|
||||
@@ -249,11 +424,273 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
if (hasViolation)
|
||||
Padding(
|
||||
padding: const EdgeInsets.only(top: 12),
|
||||
child: SizedBox(
|
||||
width: double.infinity,
|
||||
child: ElevatedButton.icon(
|
||||
onPressed: () => _showAllowDialog(
|
||||
student['studentId'],
|
||||
student['studentName'],
|
||||
),
|
||||
icon: const Icon(Icons.check, size: 18),
|
||||
label: const Text('반출 허용'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red[600],
|
||||
foregroundColor: Colors.white,
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------
|
||||
// 🖥️ 데스크톱 레이아웃 (선생님이 교실 컴퓨터 브라우저로 접속했을 때)
|
||||
// -----------------------------------------------------------------------
|
||||
Widget _buildDesktopBody(int total, int checkedIn, int absent) {
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 1100),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.all(24.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Row(
|
||||
children: [
|
||||
Expanded(
|
||||
child: _statTile(
|
||||
"전체 학생",
|
||||
"$total명",
|
||||
Icons.groups_rounded,
|
||||
Colors.blueGrey,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: _statTile(
|
||||
"출석 완료",
|
||||
"$checkedIn명",
|
||||
Icons.check_circle_rounded,
|
||||
Colors.green,
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 16),
|
||||
Expanded(
|
||||
child: _statTile(
|
||||
"미출석",
|
||||
"$absent명",
|
||||
Icons.error_rounded,
|
||||
Colors.red,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
const SizedBox(height: 20),
|
||||
_buildFilterChips(),
|
||||
const SizedBox(height: 12),
|
||||
Expanded(
|
||||
child: Container(
|
||||
width: double.infinity,
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.04),
|
||||
blurRadius: 16,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: _filteredStudents.isEmpty
|
||||
? const Center(
|
||||
child: Text(
|
||||
'해당하는 학생이 없습니다.',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
)
|
||||
: SingleChildScrollView(
|
||||
child: DataTable(
|
||||
headingRowColor: WidgetStateProperty.all(
|
||||
Colors.grey[50],
|
||||
),
|
||||
columns: const [
|
||||
DataColumn(label: Text('상태')),
|
||||
DataColumn(label: Text('학번')),
|
||||
DataColumn(label: Text('이름')),
|
||||
DataColumn(label: Text('제출 시간')),
|
||||
DataColumn(label: Text('주머니 번호')),
|
||||
DataColumn(label: Text('작업')),
|
||||
],
|
||||
rows: _filteredStudents.map((student) {
|
||||
final bool isCheckedIn = student['isCheckedIn'];
|
||||
final bool hasViolation =
|
||||
student['hasActiveViolation'] == true;
|
||||
return DataRow(
|
||||
color: hasViolation
|
||||
? WidgetStateProperty.all(Colors.red[50])
|
||||
: null,
|
||||
cells: [
|
||||
DataCell(
|
||||
Icon(
|
||||
hasViolation
|
||||
? Icons.warning_amber_rounded
|
||||
: (isCheckedIn
|
||||
? Icons.check_circle_rounded
|
||||
: Icons.error_rounded),
|
||||
color: hasViolation
|
||||
? Colors.red
|
||||
: (isCheckedIn
|
||||
? Colors.blue
|
||||
: Colors.red),
|
||||
size: 20,
|
||||
),
|
||||
),
|
||||
DataCell(Text('${student['studentId']}')),
|
||||
DataCell(
|
||||
Text(
|
||||
'${student['studentName']}',
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
Text(
|
||||
hasViolation
|
||||
? '🚨 무단반출 (${student['violationTime']})'
|
||||
: (isCheckedIn
|
||||
? '${student['checkInTime']}'
|
||||
: '미제출'),
|
||||
style: TextStyle(
|
||||
color: hasViolation
|
||||
? Colors.red[700]
|
||||
: (isCheckedIn
|
||||
? Colors.grey[700]
|
||||
: Colors.red[400]),
|
||||
fontWeight: hasViolation
|
||||
? FontWeight.bold
|
||||
: FontWeight.normal,
|
||||
),
|
||||
),
|
||||
),
|
||||
DataCell(
|
||||
isCheckedIn && student['pocketNumber'] != null
|
||||
? Container(
|
||||
padding: const EdgeInsets.symmetric(
|
||||
horizontal: 10,
|
||||
vertical: 4,
|
||||
),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.blue.shade50,
|
||||
borderRadius:
|
||||
BorderRadius.circular(20),
|
||||
border: Border.all(
|
||||
color: Colors.blue.shade200,
|
||||
),
|
||||
),
|
||||
child: Text(
|
||||
student['pocketNumber'],
|
||||
style: const TextStyle(
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.blueAccent,
|
||||
fontSize: 12,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const Text('-'),
|
||||
),
|
||||
DataCell(
|
||||
hasViolation
|
||||
? ElevatedButton.icon(
|
||||
onPressed: () => _showAllowDialog(
|
||||
student['studentId'],
|
||||
student['studentName'],
|
||||
),
|
||||
icon: const Icon(
|
||||
Icons.check,
|
||||
size: 16,
|
||||
),
|
||||
label: const Text('반출 허용'),
|
||||
style: ElevatedButton.styleFrom(
|
||||
backgroundColor: Colors.red[600],
|
||||
foregroundColor: Colors.white,
|
||||
padding:
|
||||
const EdgeInsets.symmetric(
|
||||
horizontal: 12,
|
||||
),
|
||||
),
|
||||
)
|
||||
: const Text('-'),
|
||||
),
|
||||
],
|
||||
);
|
||||
}).toList(),
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
Widget _statTile(String label, String value, IconData icon, Color color) {
|
||||
return Container(
|
||||
padding: const EdgeInsets.all(20),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(16),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.black.withValues(alpha: 0.04),
|
||||
blurRadius: 12,
|
||||
offset: const Offset(0, 4),
|
||||
),
|
||||
],
|
||||
),
|
||||
child: Row(
|
||||
children: [
|
||||
Container(
|
||||
padding: const EdgeInsets.all(12),
|
||||
decoration: BoxDecoration(
|
||||
color: color.withValues(alpha: 0.1),
|
||||
borderRadius: BorderRadius.circular(12),
|
||||
),
|
||||
child: Icon(icon, color: color, size: 26),
|
||||
),
|
||||
const SizedBox(width: 14),
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.start,
|
||||
children: [
|
||||
Text(
|
||||
label,
|
||||
style: TextStyle(color: Colors.grey[600], fontSize: 13),
|
||||
),
|
||||
const SizedBox(height: 2),
|
||||
Text(
|
||||
value,
|
||||
style: TextStyle(
|
||||
color: color,
|
||||
fontSize: 22,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -135,7 +135,11 @@ class _TeacherDashboardState extends State<TeacherDashboard> {
|
||||
const SizedBox(height: 16),
|
||||
|
||||
// 📊 [그리드 레이아웃 메뉴] 시후의 카드 컴포넌트 스타일 적용
|
||||
Padding(
|
||||
// 🖥️ 데스크톱 브라우저에서 카드가 지나치게 커지지 않도록 최대 너비를 제한한다.
|
||||
Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 700),
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||
child: GridView.count(
|
||||
shrinkWrap: true,
|
||||
@@ -173,6 +177,8 @@ class _TeacherDashboardState extends State<TeacherDashboard> {
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
],
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user