선생님 대시보드 학년별 카테고리 분류 + 학생 계정 관리 버그 수정
- 실시간 출석 현황을 학년별 섹션으로 묶어서 표시 (백엔드에 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:
@@ -14,10 +14,14 @@ final RegExp _logNamePattern = RegExp(r'^(.*?)\s*\(([^)]*)\)$');
|
|||||||
class TeacherAttendanceController extends ChangeNotifier {
|
class TeacherAttendanceController extends ChangeNotifier {
|
||||||
List<dynamic> _roster = []; // 전체 학생 명단 (/api/users)
|
List<dynamic> _roster = []; // 전체 학생 명단 (/api/users)
|
||||||
List<dynamic> _logs = []; // 출석 로그 (/api/logs)
|
List<dynamic> _logs = []; // 출석 로그 (/api/logs)
|
||||||
Map<String, dynamic> _activeViolationsByStudentId = {}; // 무단반출 중인 학생 (/api/violations/active)
|
Map<String, dynamic> _activeViolationsByStudentId =
|
||||||
String? _dismissedAt; // 오늘 가장 최근 하교 처리 시각 (/api/dismissal/latest). 이 시각 이후 기록만 "오늘 출석"으로 표시.
|
{}; // 무단반출 중인 학생 (/api/violations/active)
|
||||||
String? _attendanceTime; // 선생님이 지정한 자습실 출석시간 "HH:MM" (/api/settings/attendance-time). null이면 미설정.
|
String?
|
||||||
String? _globalPermissionUntil; // 하교(12시간)/반출허용시간설정으로 전체 반출이 허용된 경우 그 만료 시각 (/api/permissions/status). null이면 비활성.
|
_dismissedAt; // 오늘 가장 최근 하교 처리 시각 (/api/dismissal/latest). 이 시각 이후 기록만 "오늘 출석"으로 표시.
|
||||||
|
String?
|
||||||
|
_attendanceTime; // 선생님이 지정한 자습실 출석시간 "HH:MM" (/api/settings/attendance-time). null이면 미설정.
|
||||||
|
String?
|
||||||
|
_globalPermissionUntil; // 하교(12시간)/반출허용시간설정으로 전체 반출이 허용된 경우 그 만료 시각 (/api/permissions/status). null이면 비활성.
|
||||||
Timer? _timer;
|
Timer? _timer;
|
||||||
bool _isLoading = true;
|
bool _isLoading = true;
|
||||||
bool _isRefreshing = false; // 새로고침 버튼 클릭 시 잠깐 도는 표시용
|
bool _isRefreshing = false; // 새로고침 버튼 클릭 시 잠깐 도는 표시용
|
||||||
@@ -81,7 +85,9 @@ class TeacherAttendanceController extends ChangeNotifier {
|
|||||||
|
|
||||||
Map<String, dynamic> activeViolations = {};
|
Map<String, dynamic> activeViolations = {};
|
||||||
if (violationsRes.statusCode == 200) {
|
if (violationsRes.statusCode == 200) {
|
||||||
final violationsData = jsonDecode(utf8.decode(violationsRes.bodyBytes));
|
final violationsData = jsonDecode(
|
||||||
|
utf8.decode(violationsRes.bodyBytes),
|
||||||
|
);
|
||||||
for (final v in (violationsData['violations'] ?? [])) {
|
for (final v in (violationsData['violations'] ?? [])) {
|
||||||
activeViolations[v['student_id'].toString()] = v;
|
activeViolations[v['student_id'].toString()] = v;
|
||||||
}
|
}
|
||||||
@@ -95,15 +101,17 @@ class TeacherAttendanceController extends ChangeNotifier {
|
|||||||
|
|
||||||
String? attendanceTime;
|
String? attendanceTime;
|
||||||
if (attendanceTimeRes.statusCode == 200) {
|
if (attendanceTimeRes.statusCode == 200) {
|
||||||
final attendanceTimeData =
|
final attendanceTimeData = jsonDecode(
|
||||||
jsonDecode(utf8.decode(attendanceTimeRes.bodyBytes));
|
utf8.decode(attendanceTimeRes.bodyBytes),
|
||||||
|
);
|
||||||
attendanceTime = attendanceTimeData['attendanceTime'];
|
attendanceTime = attendanceTimeData['attendanceTime'];
|
||||||
}
|
}
|
||||||
|
|
||||||
String? globalPermissionUntil;
|
String? globalPermissionUntil;
|
||||||
if (permissionStatusRes.statusCode == 200) {
|
if (permissionStatusRes.statusCode == 200) {
|
||||||
final permissionStatusData =
|
final permissionStatusData = jsonDecode(
|
||||||
jsonDecode(utf8.decode(permissionStatusRes.bodyBytes));
|
utf8.decode(permissionStatusRes.bodyBytes),
|
||||||
|
);
|
||||||
if (permissionStatusData['active'] == true) {
|
if (permissionStatusData['active'] == true) {
|
||||||
globalPermissionUntil = permissionStatusData['permittedUntil'];
|
globalPermissionUntil = permissionStatusData['permittedUntil'];
|
||||||
}
|
}
|
||||||
@@ -272,10 +280,13 @@ class TeacherAttendanceController extends ChangeNotifier {
|
|||||||
final String id = u['id']?.toString() ?? '';
|
final String id = u['id']?.toString() ?? '';
|
||||||
final checkIn = checkIns[id];
|
final checkIn = checkIns[id];
|
||||||
final violation = _activeViolationsByStudentId[id];
|
final violation = _activeViolationsByStudentId[id];
|
||||||
final String attendanceStatus = _computeAttendanceStatus(checkIn?['time']);
|
final String attendanceStatus = _computeAttendanceStatus(
|
||||||
|
checkIn?['time'],
|
||||||
|
);
|
||||||
return {
|
return {
|
||||||
'studentId': id,
|
'studentId': id,
|
||||||
'studentName': u['name']?.toString() ?? '',
|
'studentName': u['name']?.toString() ?? '',
|
||||||
|
'grade': u['grade'] as int?,
|
||||||
'isCheckedIn': checkIn != null,
|
'isCheckedIn': checkIn != null,
|
||||||
'pocketNumber': checkIn?['pocketNumber'],
|
'pocketNumber': checkIn?['pocketNumber'],
|
||||||
'checkInTime': checkIn?['time'],
|
'checkInTime': checkIn?['time'],
|
||||||
@@ -298,4 +309,29 @@ class TeacherAttendanceController extends ChangeNotifier {
|
|||||||
}
|
}
|
||||||
return all;
|
return all;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// 🏫 [학년별 카테고리] filteredStudents를 학년(1~3학년) 순서로 묶는다.
|
||||||
|
/// 학년이 아직 지정 안 된 학생은 맨 뒤 "미배정" 그룹으로 모인다.
|
||||||
|
List<MapEntry<String, List<Map<String, dynamic>>>> get studentsByGrade {
|
||||||
|
final Map<int, List<Map<String, dynamic>>> byGrade = {};
|
||||||
|
final List<Map<String, dynamic>> ungraded = [];
|
||||||
|
|
||||||
|
for (final student in filteredStudents) {
|
||||||
|
final int? grade = student['grade'] as int?;
|
||||||
|
if (grade == null) {
|
||||||
|
ungraded.add(student);
|
||||||
|
} else {
|
||||||
|
byGrade.putIfAbsent(grade, () => []).add(student);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
final sortedGrades = byGrade.keys.toList()..sort();
|
||||||
|
final groups = <MapEntry<String, List<Map<String, dynamic>>>>[
|
||||||
|
for (final grade in sortedGrades) MapEntry('$grade학년', byGrade[grade]!),
|
||||||
|
];
|
||||||
|
if (ungraded.isNotEmpty) {
|
||||||
|
groups.add(MapEntry('미배정', ungraded));
|
||||||
|
}
|
||||||
|
return groups;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -13,11 +13,12 @@ class TeacherStudentManagementController extends ChangeNotifier {
|
|||||||
required String studentId,
|
required String studentId,
|
||||||
required String name,
|
required String name,
|
||||||
required String password,
|
required String password,
|
||||||
|
int? grade,
|
||||||
}) async {
|
}) async {
|
||||||
_isWorking = true;
|
_isWorking = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
try {
|
try {
|
||||||
final url = Uri.parse('$baseUrl/api/users/register-student');
|
final url = Uri.parse('$baseUrl/api/users/create');
|
||||||
final response = await http.post(
|
final response = await http.post(
|
||||||
url,
|
url,
|
||||||
headers: {"Content-Type": "application/json"},
|
headers: {"Content-Type": "application/json"},
|
||||||
@@ -25,6 +26,7 @@ class TeacherStudentManagementController extends ChangeNotifier {
|
|||||||
"studentId": studentId,
|
"studentId": studentId,
|
||||||
"name": name,
|
"name": name,
|
||||||
"password": password,
|
"password": password,
|
||||||
|
"grade": grade,
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
final result = jsonDecode(utf8.decode(response.bodyBytes));
|
final result = jsonDecode(utf8.decode(response.bodyBytes));
|
||||||
@@ -48,9 +50,13 @@ class TeacherStudentManagementController extends ChangeNotifier {
|
|||||||
) async {
|
) async {
|
||||||
_isWorking = true;
|
_isWorking = true;
|
||||||
notifyListeners();
|
notifyListeners();
|
||||||
final url = Uri.parse('$baseUrl/api/users/delete/$studentId');
|
final url = Uri.parse('$baseUrl/api/users/delete');
|
||||||
try {
|
try {
|
||||||
final response = await http.delete(url);
|
final response = await http.delete(
|
||||||
|
url,
|
||||||
|
headers: {"Content-Type": "application/json"},
|
||||||
|
body: jsonEncode({"studentId": studentId}),
|
||||||
|
);
|
||||||
if (response.statusCode == 200) {
|
if (response.statusCode == 200) {
|
||||||
final resData = jsonDecode(utf8.decode(response.bodyBytes));
|
final resData = jsonDecode(utf8.decode(response.bodyBytes));
|
||||||
return (true, '✅ ${resData['message']}');
|
return (true, '✅ ${resData['message']}');
|
||||||
|
|||||||
@@ -282,12 +282,21 @@ class _StudentDashboardState extends State<StudentDashboard> {
|
|||||||
),
|
),
|
||||||
),
|
),
|
||||||
const SizedBox(height: 16),
|
const SizedBox(height: 16),
|
||||||
Padding(
|
LayoutBuilder(
|
||||||
|
builder: (context, constraints) {
|
||||||
|
// 🖥️ 웹(넓은 화면)은 한 줄에 5개씩, 폰(좁은 화면)은 기존 2개 그대로.
|
||||||
|
final bool isWide = constraints.maxWidth >= 800;
|
||||||
|
return Center(
|
||||||
|
child: ConstrainedBox(
|
||||||
|
constraints: BoxConstraints(
|
||||||
|
maxWidth: isWide ? 1300 : 700,
|
||||||
|
),
|
||||||
|
child: Padding(
|
||||||
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
padding: const EdgeInsets.symmetric(horizontal: 24.0),
|
||||||
child: GridView.count(
|
child: GridView.count(
|
||||||
shrinkWrap: true,
|
shrinkWrap: true,
|
||||||
physics: const NeverScrollableScrollPhysics(),
|
physics: const NeverScrollableScrollPhysics(),
|
||||||
crossAxisCount: 2,
|
crossAxisCount: isWide ? 5 : 2,
|
||||||
crossAxisSpacing: 16,
|
crossAxisSpacing: 16,
|
||||||
mainAxisSpacing: 16,
|
mainAxisSpacing: 16,
|
||||||
childAspectRatio: 0.95,
|
childAspectRatio: 0.95,
|
||||||
@@ -308,7 +317,9 @@ class _StudentDashboardState extends State<StudentDashboard> {
|
|||||||
onTap: widget.isDeviceMatched
|
onTap: widget.isDeviceMatched
|
||||||
? () => _openPocketCheckIn(context)
|
? () => _openPocketCheckIn(context)
|
||||||
: () {
|
: () {
|
||||||
ScaffoldMessenger.of(context).showSnackBar(
|
ScaffoldMessenger.of(
|
||||||
|
context,
|
||||||
|
).showSnackBar(
|
||||||
const SnackBar(
|
const SnackBar(
|
||||||
content: Text(
|
content: Text(
|
||||||
'🚨 대리 출석 방지를 위해 등록된 본인 스마트폰에서만 출석 가능합니다.',
|
'🚨 대리 출석 방지를 위해 등록된 본인 스마트폰에서만 출석 가능합니다.',
|
||||||
@@ -358,7 +369,8 @@ class _StudentDashboardState extends State<StudentDashboard> {
|
|||||||
onTap: () => Navigator.push(
|
onTap: () => Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => const AdminDashboard(),
|
builder: (context) =>
|
||||||
|
const AdminDashboard(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
@@ -393,13 +405,18 @@ class _StudentDashboardState extends State<StudentDashboard> {
|
|||||||
onTap: () => Navigator.push(
|
onTap: () => Navigator.push(
|
||||||
context,
|
context,
|
||||||
MaterialPageRoute(
|
MaterialPageRoute(
|
||||||
builder: (context) => const NfcTagWriterScreen(),
|
builder: (context) =>
|
||||||
|
const NfcTagWriterScreen(),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
|
),
|
||||||
const SizedBox(height: 32),
|
const SizedBox(height: 32),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -230,7 +230,10 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
|
|||||||
),
|
),
|
||||||
TextButton.icon(
|
TextButton.icon(
|
||||||
onPressed: _showAttendanceTimeDialog,
|
onPressed: _showAttendanceTimeDialog,
|
||||||
icon: const Icon(Icons.access_time_rounded, color: Colors.white),
|
icon: const Icon(
|
||||||
|
Icons.access_time_rounded,
|
||||||
|
color: Colors.white,
|
||||||
|
),
|
||||||
label: Text(
|
label: Text(
|
||||||
_controller.attendanceTime != null
|
_controller.attendanceTime != null
|
||||||
? '출석시간 ${_controller.attendanceTime}'
|
? '출석시간 ${_controller.attendanceTime}'
|
||||||
@@ -289,35 +292,87 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
|
|||||||
// 📱 모바일 레이아웃 (기존 카드 리스트)
|
// 📱 모바일 레이아웃 (기존 카드 리스트)
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
Widget _buildMobileBody(int total, int checkedIn, int absent) {
|
Widget _buildMobileBody(int total, int checkedIn, int absent) {
|
||||||
final filteredStudents = _controller.filteredStudents;
|
final groups = _controller.studentsByGrade;
|
||||||
return Column(
|
return Column(
|
||||||
children: [
|
children: [
|
||||||
_buildSummaryCards(total, checkedIn, absent),
|
_buildSummaryCards(total, checkedIn, absent),
|
||||||
_buildPermissionBanner(),
|
_buildPermissionBanner(),
|
||||||
_buildFilterChips(),
|
_buildFilterChips(),
|
||||||
Expanded(
|
Expanded(
|
||||||
child: filteredStudents.isEmpty
|
child: groups.isEmpty
|
||||||
? const Center(
|
? const Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'해당하는 학생이 없습니다.',
|
'해당하는 학생이 없습니다.',
|
||||||
style: TextStyle(color: Colors.grey),
|
style: TextStyle(color: Colors.grey),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: GridView.builder(
|
: _buildGradeGroupedList(
|
||||||
|
groups,
|
||||||
padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
|
padding: const EdgeInsets.fromLTRB(16, 12, 16, 16),
|
||||||
gridDelegate:
|
),
|
||||||
const SliverGridDelegateWithMaxCrossAxisExtent(
|
),
|
||||||
|
],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// 🏫 학년별로 섹션 제목을 붙여 그리드를 이어붙인 목록. 모바일/데스크톱 공용.
|
||||||
|
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,
|
maxCrossAxisExtent: 220,
|
||||||
mainAxisSpacing: 12,
|
mainAxisSpacing: 12,
|
||||||
crossAxisSpacing: 12,
|
crossAxisSpacing: 12,
|
||||||
mainAxisExtent: 148,
|
mainAxisExtent: 148,
|
||||||
),
|
),
|
||||||
itemCount: filteredStudents.length,
|
itemCount: group.value.length,
|
||||||
itemBuilder: (context, index) =>
|
itemBuilder: (context, i) => _buildStudentTile(group.value[i]),
|
||||||
_buildStudentTile(filteredStudents[index]),
|
|
||||||
),
|
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
|
),
|
||||||
|
);
|
||||||
|
},
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -454,7 +509,7 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
|
|||||||
// 🖥️ 데스크톱 레이아웃 (선생님이 교실 컴퓨터 브라우저로 접속했을 때)
|
// 🖥️ 데스크톱 레이아웃 (선생님이 교실 컴퓨터 브라우저로 접속했을 때)
|
||||||
// -----------------------------------------------------------------------
|
// -----------------------------------------------------------------------
|
||||||
Widget _buildDesktopBody(int total, int checkedIn, int absent) {
|
Widget _buildDesktopBody(int total, int checkedIn, int absent) {
|
||||||
final filteredStudents = _controller.filteredStudents;
|
final groups = _controller.studentsByGrade;
|
||||||
return Center(
|
return Center(
|
||||||
child: ConstrainedBox(
|
child: ConstrainedBox(
|
||||||
constraints: const BoxConstraints(maxWidth: 1100),
|
constraints: const BoxConstraints(maxWidth: 1100),
|
||||||
@@ -511,25 +566,16 @@ class _TeacherAttendancePageState extends State<TeacherAttendancePage> {
|
|||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
child: filteredStudents.isEmpty
|
child: groups.isEmpty
|
||||||
? const Center(
|
? const Center(
|
||||||
child: Text(
|
child: Text(
|
||||||
'해당하는 학생이 없습니다.',
|
'해당하는 학생이 없습니다.',
|
||||||
style: TextStyle(color: Colors.grey),
|
style: TextStyle(color: Colors.grey),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
: GridView.builder(
|
: _buildGradeGroupedList(
|
||||||
|
groups,
|
||||||
padding: const EdgeInsets.all(20),
|
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]),
|
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ class _TeacherStudentManagementPageState
|
|||||||
final TextEditingController _addIdController = TextEditingController();
|
final TextEditingController _addIdController = TextEditingController();
|
||||||
final TextEditingController _addNameController = TextEditingController();
|
final TextEditingController _addNameController = TextEditingController();
|
||||||
final TextEditingController _addPwController = TextEditingController();
|
final TextEditingController _addPwController = TextEditingController();
|
||||||
|
int? _selectedGrade;
|
||||||
|
|
||||||
@override
|
@override
|
||||||
void dispose() {
|
void dispose() {
|
||||||
@@ -48,12 +49,14 @@ class _TeacherStudentManagementPageState
|
|||||||
studentId: sId,
|
studentId: sId,
|
||||||
name: sName,
|
name: sName,
|
||||||
password: sPw,
|
password: sPw,
|
||||||
|
grade: _selectedGrade,
|
||||||
);
|
);
|
||||||
if (!mounted) return;
|
if (!mounted) return;
|
||||||
if (success) {
|
if (success) {
|
||||||
_addIdController.clear();
|
_addIdController.clear();
|
||||||
_addNameController.clear();
|
_addNameController.clear();
|
||||||
_addPwController.clear();
|
_addPwController.clear();
|
||||||
|
setState(() => _selectedGrade = null);
|
||||||
}
|
}
|
||||||
ScaffoldMessenger.of(
|
ScaffoldMessenger.of(
|
||||||
context,
|
context,
|
||||||
@@ -184,7 +187,10 @@ class _TeacherStudentManagementPageState
|
|||||||
const SizedBox(width: 8),
|
const SizedBox(width: 8),
|
||||||
const Text(
|
const Text(
|
||||||
'신규 학생 계정 추가',
|
'신규 학생 계정 추가',
|
||||||
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
|
style: TextStyle(
|
||||||
|
fontSize: 18,
|
||||||
|
fontWeight: FontWeight.bold,
|
||||||
|
),
|
||||||
),
|
),
|
||||||
],
|
],
|
||||||
),
|
),
|
||||||
@@ -230,6 +236,21 @@ class _TeacherStudentManagementPageState
|
|||||||
prefixIcon: Icon(Icons.lock),
|
prefixIcon: Icon(Icons.lock),
|
||||||
),
|
),
|
||||||
),
|
),
|
||||||
|
const SizedBox(height: 12),
|
||||||
|
DropdownButtonFormField<int>(
|
||||||
|
initialValue: _selectedGrade,
|
||||||
|
decoration: const InputDecoration(
|
||||||
|
labelText: '학년 (선택)',
|
||||||
|
prefixIcon: Icon(Icons.class_),
|
||||||
|
),
|
||||||
|
items: const [
|
||||||
|
DropdownMenuItem(value: 1, child: Text('1학년')),
|
||||||
|
DropdownMenuItem(value: 2, child: Text('2학년')),
|
||||||
|
DropdownMenuItem(value: 3, child: Text('3학년')),
|
||||||
|
],
|
||||||
|
onChanged: (value) =>
|
||||||
|
setState(() => _selectedGrade = value),
|
||||||
|
),
|
||||||
const SizedBox(height: 20),
|
const SizedBox(height: 20),
|
||||||
SizedBox(
|
SizedBox(
|
||||||
width: double.infinity,
|
width: double.infinity,
|
||||||
|
|||||||
+1
-1
@@ -16,7 +16,7 @@ publish_to: 'none' # Remove this line if you wish to publish to pub.dev
|
|||||||
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
|
||||||
# In Windows, build-name is used as the major, minor, and patch parts
|
# In Windows, build-name is used as the major, minor, and patch parts
|
||||||
# of the product and file versions while build-number is used as the build suffix.
|
# of the product and file versions while build-number is used as the build suffix.
|
||||||
version: 1.1.4+5
|
version: 1.2.0+6
|
||||||
|
|
||||||
environment:
|
environment:
|
||||||
sdk: ^3.12.2
|
sdk: ^3.12.2
|
||||||
|
|||||||
Reference in New Issue
Block a user