선생님 대시보드 학년별 카테고리 분류 + 학생 계정 관리 버그 수정
- 실시간 출석 현황을 학년별 섹션으로 묶어서 표시 (백엔드에 grade 필드 추가)
- 학생 계정 추가 폼에 학년 선택 드롭다운 추가
- 학생 계정 생성/삭제가 실제로는 존재하지 않는 엔드포인트를 호출하던
버그 수정 (/api/users/register-student, /api/users/delete/{id} →
/api/users/create, /api/users/delete)
- 학생 대시보드(개발자 모드) 카드 그리드가 넓은 화면에서 과도하게
커지던 레이아웃 버그 수정, 웹은 5열/폰은 2열로 반응형 처리
- 버전 1.2.0+6
Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
@@ -230,7 +230,10 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
|
||||
),
|
||||
TextButton.icon(
|
||||
onPressed: _showAttendanceTimeDialog,
|
||||
icon: const Icon(Icons.access_time_rounded, color: Colors.white),
|
||||
icon: const Icon(
|
||||
Icons.access_time_rounded,
|
||||
color: Colors.white,
|
||||
),
|
||||
label: Text(
|
||||
_controller.attendanceTime != null
|
||||
? '출석시간 ${_controller.attendanceTime}'
|
||||
@@ -289,38 +292,90 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
|
||||
// 📱 모바일 레이아웃 (기존 카드 리스트)
|
||||
// -----------------------------------------------------------------------
|
||||
Widget _buildMobileBody(int total, int checkedIn, int absent) {
|
||||
final filteredStudents = _controller.filteredStudents;
|
||||
final groups = _controller.studentsByGrade;
|
||||
return Column(
|
||||
children: [
|
||||
_buildSummaryCards(total, checkedIn, absent),
|
||||
_buildPermissionBanner(),
|
||||
_buildFilterChips(),
|
||||
Expanded(
|
||||
child: filteredStudents.isEmpty
|
||||
child: groups.isEmpty
|
||||
? const Center(
|
||||
child: Text(
|
||||
'해당하는 학생이 없습니다.',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
)
|
||||
: GridView.builder(
|
||||
: _buildGradeGroupedList(
|
||||
groups,
|
||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 220,
|
||||
mainAxisSpacing: 12,
|
||||
crossAxisSpacing: 12,
|
||||
mainAxisExtent: 148,
|
||||
),
|
||||
itemCount: filteredStudents.length,
|
||||
itemBuilder: (context, index) =>
|
||||
_buildStudentTile(filteredStudents[index]),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
/// 🏫 학년별로 섹션 제목을 붙여 그리드를 이어붙인 목록. 모바일/데스크톱 공용.
|
||||
Widget _buildGradeGroupedList(
|
||||
List<MapEntry<String, List<Map<String, dynamic>>>> 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: Colors.blue,
|
||||
borderRadius: BorderRadius.circular(2),
|
||||
),
|
||||
),
|
||||
const SizedBox(width: 8),
|
||||
Text(
|
||||
group.key,
|
||||
style: const TextStyle(
|
||||
fontSize: 16,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87,
|
||||
),
|
||||
),
|
||||
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<String, dynamic> student) {
|
||||
final bool isCheckedIn = student['isCheckedIn'];
|
||||
@@ -454,7 +509,7 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
|
||||
// 🖥️ 데스크톱 레이아웃 (선생님이 교실 컴퓨터 브라우저로 접속했을 때)
|
||||
// -----------------------------------------------------------------------
|
||||
Widget _buildDesktopBody(int total, int checkedIn, int absent) {
|
||||
final filteredStudents = _controller.filteredStudents;
|
||||
final groups = _controller.studentsByGrade;
|
||||
return Center(
|
||||
child: ConstrainedBox(
|
||||
constraints: const BoxConstraints(maxWidth: 1100),
|
||||
@@ -511,25 +566,16 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
|
||||
),
|
||||
],
|
||||
),
|
||||
child: filteredStudents.isEmpty
|
||||
child: groups.isEmpty
|
||||
? const Center(
|
||||
child: Text(
|
||||
'해당하는 학생이 없습니다.',
|
||||
style: TextStyle(color: Colors.grey),
|
||||
),
|
||||
)
|
||||
: GridView.builder(
|
||||
: _buildGradeGroupedList(
|
||||
groups,
|
||||
padding: const EdgeInsets.all(20),
|
||||
gridDelegate:
|
||||
const SliverGridDelegateWithMaxCrossAxisExtent(
|
||||
maxCrossAxisExtent: 220,
|
||||
mainAxisSpacing: 16,
|
||||
crossAxisSpacing: 16,
|
||||
mainAxisExtent: 148,
|
||||
),
|
||||
itemCount: filteredStudents.length,
|
||||
itemBuilder: (context, index) =>
|
||||
_buildStudentTile(filteredStudents[index]),
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user