// ๐Ÿ“‹ ์‹ค์‹œ๊ฐ„ ์ถœ์„ ํ˜„ํ™ฉ ํ™”๋ฉด (UI ์ „์šฉ). ์œ„์ ฏ ๋นŒ๋“œ/๋ ˆ์ด์•„์›ƒ/์Šคํƒ€์ผ๋งŒ ๋‹ด๋‹นํ•˜๊ณ , // ์„œ๋ฒ„ ํ†ต์‹ ยท์ƒํƒœยทํŒŒ์ƒ ๋กœ์ง์€ lib/function/teacher_attendance_controller.dart๊ฐ€ ๋‹ด๋‹นํ•œ๋‹ค. import 'package:flutter/material.dart'; import '../function/teacher_attendance_controller.dart'; import '../theme/app_palette.dart'; // ----------------------------------------------------------------------------- // ๐Ÿ“… [์„œ๋ธŒ ํ™”๋ฉด 1] ์‹ค์‹œ๊ฐ„ ์ถœ์„ ํ™•์ธ ๋ž€ (StudentDashboard ์นด๋“œ ์Šคํƒ€์ผ ๋ฆฌ์ŠคํŠธํ™”) // ----------------------------------------------------------------------------- class TeacherAttendancePage extends StatefulWidget { const TeacherAttendancePage({super.key}); @override State createState() => _TeacherAttendancePageState(); } class _TeacherAttendancePageState extends State { final TeacherAttendanceController _controller = TeacherAttendanceController(); @override void initState() { super.initState(); _controller.init(); } @override void dispose() { _controller.dispose(); super.dispose(); } /// ์ปจํŠธ๋กค๋Ÿฌ ์•ก์…˜์„ ์‹คํ–‰ํ•˜๊ณ , ๊ฒฐ๊ณผ ๋ฉ”์‹œ์ง€๋ฅผ ์Šค๋‚ต๋ฐ”๋กœ ๋ณด์—ฌ์ค€๋‹ค. Future _runAction(Future Function() action) async { final message = await action(); if (!mounted) return; ScaffoldMessenger.of( context, ).showSnackBar(SnackBar(content: Text(message))); } Future _showAttendanceTimeDialog() async { final String? current = _controller.attendanceTime; final TimeOfDay initial = current != null ? TimeOfDay( hour: int.parse(current.split(':')[0]), minute: int.parse(current.split(':')[1]), ) : const TimeOfDay(hour: 19, minute: 0); final TimeOfDay? picked = await showTimePicker( context: context, initialTime: initial, helpText: '์ž์Šต์‹ค ์ถœ์„์‹œ๊ฐ„ ์ง€์ •', ); if (picked == null) return; final String formatted = '${picked.hour.toString().padLeft(2, '0')}:${picked.minute.toString().padLeft(2, '0')}'; await _runAction(() => _controller.setAttendanceTime(formatted)); } void _showTestResetConfirmDialog() { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('ํ…Œ์ŠคํŠธ์šฉ ํ—ˆ์šฉ์‹œ๊ฐ„ ์ดˆ๊ธฐํ™”'), content: const Text( 'ํ•˜๊ต ์ฒ˜๋ฆฌ๋‚˜ ๋ฐ˜์ถœ ํ—ˆ์šฉ ์‹œ๊ฐ„ ์„ค์ •์œผ๋กœ ์ผœ์ ธ ์žˆ๋Š” ๋ชจ๋“  ํ—ˆ์šฉ ์‹œ๊ฐ„๋Œ€๋ฅผ ์ง€๊ธˆ ์ฆ‰์‹œ ํ•ด์ œํ•ฉ๋‹ˆ๋‹ค.\n' '(๋ฌด๋‹จ๋ฐ˜์ถœ ๊ฐ์ง€ ํ…Œ์ŠคํŠธํ•  ๋•Œ๋งŒ ์‚ฌ์šฉํ•˜์„ธ์š”)', ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('์ทจ์†Œ'), ), ElevatedButton( onPressed: () { Navigator.pop(context); _runAction(() => _controller.resetTestPermissions()); }, style: ElevatedButton.styleFrom( backgroundColor: Colors.grey[700], foregroundColor: Colors.white, ), child: const Text('์ดˆ๊ธฐํ™”'), ), ], ), ); } 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); _runAction(() => _controller.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); _runAction(() => _controller.setPermissionWindow(minutes)); }, child: const Text('์„ค์ •ํ•˜๊ธฐ'), ), ], ), ); } /// ๐Ÿซ ํ•˜๊ต ์ฒ˜๋ฆฌ: ์‹œ๊ฐ„ ์ž…๋ ฅ ์—†์ด ๋ฐ”๋กœ ์ „์ฒด ํ•™์ƒ์˜ ๋ฐ˜์ถœ์„ (์‚ฌ์‹ค์ƒ ๋ฌด๊ธฐํ•œ) ํ—ˆ์šฉํ•œ๋‹ค. void _showDismissalConfirmDialog() { showDialog( context: context, builder: (context) => AlertDialog( title: const Text('ํ•˜๊ต ์ฒ˜๋ฆฌ'), content: const Text( '์ง€๊ธˆ๋ถ€ํ„ฐ ๋ชจ๋“  ํ•™์ƒ์˜ ๋ฐ˜์ถœ์ด ์ž๋™์œผ๋กœ ํ—ˆ์šฉ๋˜๋ฉฐ, ๋” ์ด์ƒ ๋ฌด๋‹จ๋ฐ˜์ถœ ๊ฒฝ๊ณ ๊ฐ€ ๋œจ์ง€ ์•Š์Šต๋‹ˆ๋‹ค.\nํ•˜๊ต ์ฒ˜๋ฆฌํ•˜์‹œ๊ฒ ์Šต๋‹ˆ๊นŒ?', ), actions: [ TextButton( onPressed: () => Navigator.pop(context), child: const Text('์ทจ์†Œ'), ), ElevatedButton( onPressed: () { Navigator.pop(context); _runAction(() => _controller.dismissAll()); }, child: const Text('ํ•˜๊ต ์ฒ˜๋ฆฌ'), ), ], ), ); } @override Widget build(BuildContext context) { return ListenableBuilder( listenable: _controller, builder: (context, _) { final all = _controller.combinedStudentStatus; final int totalCount = all.length; final int checkedInCount = all .where((s) => s['isAttendanceComplete'] == true) .length; final int absentCount = totalCount - checkedInCount; return Scaffold( backgroundColor: AppPalette.mist, appBar: AppBar( title: const Text( '์‹ค์‹œ๊ฐ„ ์ถœ์„ ํ˜„ํ™ฉ', style: TextStyle(fontWeight: FontWeight.bold), ), backgroundColor: AppPalette.ink, foregroundColor: Colors.white, elevation: 0, actions: [ IconButton( onPressed: (_controller.isLoading || _controller.isRefreshing) ? null : _controller.manualRefresh, icon: _controller.isRefreshing ? const SizedBox( width: 20, height: 20, child: CircularProgressIndicator( strokeWidth: 2, color: Colors.white, ), ) : const Icon(Icons.refresh_rounded), tooltip: '์ƒˆ๋กœ๊ณ ์นจ', ), TextButton.icon( onPressed: _showAttendanceTimeDialog, icon: const Icon( Icons.access_time_rounded, color: Colors.white, ), label: Text( _controller.attendanceTime != null ? '์ถœ์„์‹œ๊ฐ„ ${_controller.attendanceTime}' : '์ž์Šต์‹ค ์ถœ์„์‹œ๊ฐ„ ์„ค์ •', style: const TextStyle(color: Colors.white), ), ), TextButton.icon( onPressed: _showPermissionWindowDialog, icon: const Icon(Icons.timer_outlined, color: Colors.white), label: const Text( '๋ฐ˜์ถœ ํ—ˆ์šฉ ์‹œ๊ฐ„ ์„ค์ •', style: TextStyle(color: Colors.white), ), ), TextButton.icon( onPressed: _showDismissalConfirmDialog, icon: const Icon(Icons.school_rounded, color: Colors.white), label: const Text('ํ•˜๊ต', style: TextStyle(color: Colors.white)), ), IconButton( onPressed: _showTestResetConfirmDialog, icon: const Icon( Icons.bug_report_outlined, color: Colors.white70, ), tooltip: 'ํ…Œ์ŠคํŠธ์šฉ: ํ—ˆ์šฉ์‹œ๊ฐ„ ์ดˆ๊ธฐํ™”', ), const SizedBox(width: 8), ], ), body: _controller.isLoading ? const Center(child: CircularProgressIndicator()) : 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) { final groups = _controller.studentsByGrade; return Column( children: [ _buildSummaryCards(total, checkedIn, absent), _buildPermissionBanner(), _buildFilterChips(), Expanded( child: groups.isEmpty ? const Center( child: Text( 'ํ•ด๋‹นํ•˜๋Š” ํ•™์ƒ์ด ์—†์Šต๋‹ˆ๋‹ค.', style: TextStyle(color: Colors.grey), ), ) : _buildGradeGroupedList( groups, padding: const EdgeInsets.fromLTRB(16, 12, 16, 16), ), ), ], ); } /// ๐Ÿซ ํ•™๋…„๋ณ„๋กœ ์„น์…˜ ์ œ๋ชฉ์„ ๋ถ™์—ฌ ๊ทธ๋ฆฌ๋“œ๋ฅผ ์ด์–ด๋ถ™์ธ ๋ชฉ๋ก. ๋ชจ๋ฐ”์ผ/๋ฐ์Šคํฌํ†ฑ ๊ณต์šฉ. Widget _buildGradeGroupedList( List>>> groups, { required EdgeInsets padding, }) { return ListView.builder( padding: padding, itemCount: groups.length, itemBuilder: (context, index) { final group = groups[index]; return Padding( padding: EdgeInsets.only(bottom: index == groups.length - 1 ? 0 : 24), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( width: 4, height: 16, decoration: BoxDecoration( color: AppPalette.ink, borderRadius: BorderRadius.circular(2), ), ), const SizedBox(width: 8), Text( group.key, style: const TextStyle( fontSize: 16, fontWeight: FontWeight.bold, color: AppPalette.ink, ), ), const SizedBox(width: 6), Text( '${group.value.length}๋ช…', style: TextStyle(fontSize: 13, color: Colors.grey[500]), ), ], ), const SizedBox(height: 10), GridView.builder( shrinkWrap: true, physics: const NeverScrollableScrollPhysics(), gridDelegate: const SliverGridDelegateWithMaxCrossAxisExtent( maxCrossAxisExtent: 220, mainAxisSpacing: 12, crossAxisSpacing: 12, mainAxisExtent: 148, ), itemCount: group.value.length, itemBuilder: (context, i) => _buildStudentTile(group.value[i]), ), ], ), ); }, ); } /// ๐Ÿ€„ ํ•™์ƒ ํ•œ ๋ช…์„ "ํ•œ๋ˆˆ์— ๋ณด๊ธฐ" ํƒ€์ผ๋กœ ๊ทธ๋ฆฐ๋‹ค. ๋ชจ๋ฐ”์ผ/๋ฐ์Šคํฌํ†ฑ ๊ทธ๋ฆฌ๋“œ์—์„œ ๊ณต์šฉ์œผ๋กœ ์“ด๋‹ค. Widget _buildStudentTile(Map student) { final bool isCheckedIn = student['isCheckedIn']; final bool hasViolation = student['hasActiveViolation'] == true; final bool isPending = student['attendanceStatus'] == 'PENDING'; final bool isComplete = student['attendanceStatus'] == 'COMPLETE'; final Color statusColor = hasViolation ? Colors.red : (isPending ? Colors.orange : (isComplete ? Colors.blue : Colors.red)); final String statusLine = hasViolation ? '๋ฌด๋‹จ๋ฐ˜์ถœ (${student['violationTime']})' : (isPending ? '๋ฏธ์™„๋ฃŒ (${student['checkInTime']})' : (isComplete ? '${student['checkInTime']}' : (student['hasEverCheckedIn'] == true ? '๋ฏธ์ œ์ถœ' : '๋ฏธ๋“ฑ๋ก'))); return Container( padding: const EdgeInsets.all(14), decoration: BoxDecoration( color: hasViolation ? Colors.red[50] : AppPalette.paper, borderRadius: BorderRadius.circular(20), border: Border.all( color: hasViolation ? Colors.red[300]! : AppPalette.sage, width: hasViolation ? 1.5 : 1, ), boxShadow: [ BoxShadow( color: Colors.black.withValues(alpha: 0.03), blurRadius: 12, offset: const Offset(0, 4), ), ], ), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Row( children: [ Container( padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: statusColor.withValues(alpha: 0.1), shape: BoxShape.circle, ), child: Icon( hasViolation ? Icons.warning_amber_rounded : (isPending ? Icons.hourglass_bottom_rounded : (isComplete ? Icons.check_circle_rounded : Icons.error_rounded)), color: statusColor, size: 18, ), ), const Spacer(), if (isCheckedIn && student['pocketNumber'] != null) Container( padding: const EdgeInsets.symmetric( horizontal: 8, vertical: 3, ), decoration: BoxDecoration( color: AppPalette.linen, borderRadius: BorderRadius.circular(20), border: Border.all(color: AppPalette.sage), ), child: Text( student['pocketNumber'], style: const TextStyle( fontWeight: FontWeight.bold, color: AppPalette.ink, fontSize: 11, ), ), ), ], ), const SizedBox(height: 8), Text( '${student['studentName']}', maxLines: 1, overflow: TextOverflow.ellipsis, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 15), ), Text( 'ํ•™๋ฒˆ ${student['studentId']}', style: TextStyle(color: Colors.grey[500], fontSize: 11), ), const SizedBox(height: 4), Text( statusLine, maxLines: 2, overflow: TextOverflow.ellipsis, style: TextStyle( color: hasViolation ? Colors.red[700] : (isPending ? Colors.orange[800] : (isComplete ? Colors.grey[600] : Colors.red[400])), fontSize: 11.5, fontWeight: hasViolation ? FontWeight.bold : FontWeight.normal, ), ), if (hasViolation) ...[ const Spacer(), SizedBox( width: double.infinity, height: 30, child: ElevatedButton( onPressed: () => _showAllowDialog( student['studentId'], student['studentName'], ), style: ElevatedButton.styleFrom( backgroundColor: Colors.red[600], foregroundColor: Colors.white, padding: EdgeInsets.zero, ), child: const Text('๋ฐ˜์ถœ ํ—ˆ์šฉ', style: TextStyle(fontSize: 12)), ), ), ], ], ), ); } // ----------------------------------------------------------------------- // ๐Ÿ–ฅ๏ธ ๋ฐ์Šคํฌํ†ฑ ๋ ˆ์ด์•„์›ƒ (์„ ์ƒ๋‹˜์ด ๊ต์‹ค ์ปดํ“จํ„ฐ ๋ธŒ๋ผ์šฐ์ €๋กœ ์ ‘์†ํ–ˆ์„ ๋•Œ) // ----------------------------------------------------------------------- Widget _buildDesktopBody(int total, int checkedIn, int absent) { final groups = _controller.studentsByGrade; 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), _buildPermissionBanner(), _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: groups.isEmpty ? const Center( child: Text( 'ํ•ด๋‹นํ•˜๋Š” ํ•™์ƒ์ด ์—†์Šต๋‹ˆ๋‹ค.', style: TextStyle(color: Colors.grey), ), ) : _buildGradeGroupedList( groups, padding: const EdgeInsets.all(20), ), ), ), ], ), ), ), ); } Widget _statTile(String label, String value, IconData icon, Color color) { return Container( padding: const EdgeInsets.all(20), decoration: BoxDecoration( color: AppPalette.paper, 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, ), ), ], ), ], ), ); } /// ๐Ÿ“Š ์ƒ๋‹จ ์š”์•ฝ ์นด๋“œ ๋ทฐ (์ „์ฒด / ์ถœ์„ ์™„๋ฃŒ / ๋ฏธ์ถœ์„) Widget _buildSummaryCards(int total, int checkedIn, int absent) { return Container( width: double.infinity, padding: const EdgeInsets.all(16), color: AppPalette.ink, child: Row( mainAxisAlignment: MainAxisAlignment.spaceAround, children: [ _summaryCard("์ „์ฒด", "$total๋ช…", Colors.white70), _summaryCard("์ถœ์„ ์™„๋ฃŒ", "$checkedIn๋ช…", Colors.greenAccent), _summaryCard("๋ฏธ์ถœ์„", "$absent๋ช…", Colors.redAccent), ], ), ); } Widget _summaryCard(String title, String count, Color color) { return Column( children: [ Text(title, style: const TextStyle(color: Colors.grey, fontSize: 12)), const SizedBox(height: 4), Text( count, style: TextStyle( color: color, fontSize: 20, fontWeight: FontWeight.bold, ), ), ], ); } /// ๐Ÿ”“ ํ•˜๊ต(12์‹œ๊ฐ„)/๋ฐ˜์ถœ ํ—ˆ์šฉ ์‹œ๊ฐ„ ์„ค์ •์œผ๋กœ ์ง€๊ธˆ ์ „์ฒด ๋ฐ˜์ถœ์ด ํ—ˆ์šฉ ์ค‘์ด๋ฉด ๋ˆˆ์— ๋„๊ฒŒ ๋ฐฐ๋„ˆ๋กœ ์•Œ๋ ค์ค€๋‹ค. /// (ํ—ˆ์šฉ ์ค‘์ผ ๋• ๋ฌด๋‹จ๋ฐ˜์ถœ์„ ๊ฐ์ง€ํ•ด๋„ ๋Œ€์‹œ๋ณด๋“œ์— ๋œจ์ง€ ์•Š๊ธฐ ๋•Œ๋ฌธ์—, ์™œ ์•ˆ ๋œจ๋Š”์ง€ ํ—ท๊ฐˆ๋ฆฌ์ง€ ์•Š๊ฒŒ ํ•˜๊ธฐ ์œ„ํ•จ) Widget _buildPermissionBanner() { final String? until = _controller.globalPermissionUntil; if (until == null) return const SizedBox.shrink(); return Container( width: double.infinity, margin: const EdgeInsets.fromLTRB(16, 8, 16, 0), padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 10), decoration: BoxDecoration( color: Colors.amber[100], borderRadius: BorderRadius.circular(12), border: Border.all(color: Colors.amber[400]!), ), child: Row( children: [ Icon(Icons.lock_open_rounded, color: Colors.amber[800], size: 20), const SizedBox(width: 8), Expanded( child: Text( '์ง€๊ธˆ ์ „์ฒด ๋ฐ˜์ถœ ํ—ˆ์šฉ ์ค‘์ž…๋‹ˆ๋‹ค ($until ๊นŒ์ง€) โ€” ์ด ์‹œ๊ฐ„ ๋™์•ˆ์€ ๋ฌด๋‹จ๋ฐ˜์ถœ ๊ฒฝ๊ณ ๊ฐ€ ๋œจ์ง€ ์•Š์•„์š”.', style: TextStyle( color: Colors.amber[900], fontWeight: FontWeight.bold, fontSize: 12.5, ), ), ), ], ), ); } /// ๐Ÿ”˜ ํ•„ํ„ฐ ์นฉ๋ฒ„ํŠผ (์ „์ฒด / ์ถœ์„์ž / ๋ฏธ์ถœ์„์ž) Widget _buildFilterChips() { return Padding( padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ Wrap( spacing: 8, runSpacing: 8, children: [ FilterChip( label: const Text("์ „์ฒด"), selected: _controller.filterType == "ALL", onSelected: (_) => _controller.setFilter("ALL"), ), FilterChip( label: const Text("์ถœ์„์ž"), selected: _controller.filterType == "CHECKED_IN", onSelected: (_) => _controller.setFilter("CHECKED_IN"), ), FilterChip( label: const Text("๋ฏธ์ถœ์„์ž"), selected: _controller.filterType == "ABSENT", onSelected: (_) => _controller.setFilter("ABSENT"), ), ], ), const SizedBox(height: 8), Wrap( spacing: 8, runSpacing: 8, children: [ ChoiceChip( label: const Text("ํ•™๋…„ ์ „์ฒด"), selected: _controller.gradeFilter == "ALL", onSelected: (_) => _controller.setGradeFilter("ALL"), ), ChoiceChip( label: const Text("1ํ•™๋…„"), selected: _controller.gradeFilter == "1", onSelected: (_) => _controller.setGradeFilter("1"), ), ChoiceChip( label: const Text("2ํ•™๋…„"), selected: _controller.gradeFilter == "2", onSelected: (_) => _controller.setGradeFilter("2"), ), ChoiceChip( label: const Text("3ํ•™๋…„"), selected: _controller.gradeFilter == "3", onSelected: (_) => _controller.setGradeFilter("3"), ), ], ), ], ), ); } }