- 학생 계정 관리 화면에 드래그앤드롭/클릭 업로드 영역 추가 (excel, desktop_drop, file_picker 패키지 도입) - .xlsx 파싱 → 미리보기 팝업 → 확인 후 한 명씩 순서대로 계정 생성, 결과(성공/실패)를 요약해서 보여줌. 초기 비밀번호는 1234 고정 - file_picker의 웹 구현체가 pickFile(s)를 아직 구현하지 않아서 (UnimplementedError) 웹에서는 package:web으로 직접 <input type=file>을 다뤄서 우회 (lib/function/excel_file_picker_web.dart), 다른 플랫폼은 기존 file_picker 사용 (excel_file_picker_io.dart) Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
224 lines
7.1 KiB
Dart
224 lines
7.1 KiB
Dart
// 👥 교사용 학생 계정 관리 화면의 기능(서버 통신/상태/엑셀 일괄등록) 담당 컨트롤러.
|
|
import 'dart:convert';
|
|
import 'package:excel/excel.dart';
|
|
import 'package:flutter/foundation.dart';
|
|
import 'package:http/http.dart' as http;
|
|
import '../config.dart';
|
|
|
|
/// 📄 엑셀 한 줄에서 뽑아낸 학생 정보 (아직 서버에 보내기 전, 미리보기용).
|
|
class BulkStudentRow {
|
|
final String studentId;
|
|
final String name;
|
|
final int? grade;
|
|
|
|
const BulkStudentRow({
|
|
required this.studentId,
|
|
required this.name,
|
|
this.grade,
|
|
});
|
|
}
|
|
|
|
/// 📊 일괄 등록 결과. [failures]는 "학번 이름: 실패사유" 형태의 문자열 목록.
|
|
class BulkImportResult {
|
|
final int successCount;
|
|
final List<String> failures;
|
|
|
|
const BulkImportResult({required this.successCount, required this.failures});
|
|
}
|
|
|
|
class TeacherStudentManagementController extends ChangeNotifier {
|
|
bool _isWorking = false;
|
|
bool get isWorking => _isWorking;
|
|
|
|
int _bulkTotal = 0;
|
|
int _bulkDone = 0;
|
|
int get bulkTotal => _bulkTotal;
|
|
int get bulkDone => _bulkDone;
|
|
|
|
/// 📄 엑셀 파일 바이트를 파싱한다. 1행은 머리글로 보고 건너뛰며,
|
|
/// A열=학번, B열=이름, C열=학년(선택, 숫자) 순서를 기대한다.
|
|
/// 학번이나 이름이 비어있는 줄은 조용히 무시한다.
|
|
List<BulkStudentRow> parseExcelBytes(Uint8List bytes) {
|
|
final excel = Excel.decodeBytes(bytes);
|
|
if (excel.tables.isEmpty) return [];
|
|
|
|
final sheet = excel.tables.values.first;
|
|
final rows = <BulkStudentRow>[];
|
|
|
|
for (final row in sheet.rows.skip(1)) {
|
|
final studentId = _cellText(row.isNotEmpty ? row[0]?.value : null);
|
|
final name = _cellText(row.length > 1 ? row[1]?.value : null);
|
|
if (studentId == null || name == null) continue;
|
|
|
|
final grade = _cellToGrade(row.length > 2 ? row[2]?.value : null);
|
|
rows.add(BulkStudentRow(studentId: studentId, name: name, grade: grade));
|
|
}
|
|
return rows;
|
|
}
|
|
|
|
String? _cellText(CellValue? value) {
|
|
if (value == null) return null;
|
|
String text;
|
|
if (value is IntCellValue) {
|
|
text = value.value.toString();
|
|
} else if (value is DoubleCellValue) {
|
|
text = value.value == value.value.roundToDouble()
|
|
? value.value.toInt().toString()
|
|
: value.value.toString();
|
|
} else {
|
|
text = value.toString().trim();
|
|
}
|
|
return text.isEmpty ? null : text;
|
|
}
|
|
|
|
int? _cellToGrade(CellValue? value) {
|
|
if (value == null) return null;
|
|
if (value is IntCellValue) return value.value;
|
|
if (value is DoubleCellValue) return value.value.round();
|
|
return int.tryParse(value.toString().trim());
|
|
}
|
|
|
|
/// ➕ 엑셀에서 뽑아낸 학생 목록을 한 명씩 순서대로 서버에 등록한다 (기본 비밀번호 1234).
|
|
/// 한 명 실패해도 나머지는 계속 진행하고, 마지막에 성공/실패를 모아서 돌려준다.
|
|
Future<BulkImportResult> bulkAddStudents(List<BulkStudentRow> rows) async {
|
|
_isWorking = true;
|
|
_bulkTotal = rows.length;
|
|
_bulkDone = 0;
|
|
notifyListeners();
|
|
|
|
int successCount = 0;
|
|
final failures = <String>[];
|
|
|
|
for (final row in rows) {
|
|
final (success, message) = await _createStudentRequest(
|
|
studentId: row.studentId,
|
|
name: row.name,
|
|
password: "1234",
|
|
grade: row.grade,
|
|
);
|
|
if (success) {
|
|
successCount++;
|
|
} else {
|
|
failures.add('${row.studentId} ${row.name}: $message');
|
|
}
|
|
_bulkDone++;
|
|
notifyListeners();
|
|
}
|
|
|
|
_isWorking = false;
|
|
notifyListeners();
|
|
return BulkImportResult(successCount: successCount, failures: failures);
|
|
}
|
|
|
|
/// ➕ 학생 계정 추가. (성공여부, 메시지)를 반환한다 — UI가 성공했을 때만 입력칸을 비운다.
|
|
Future<(bool success, String message)> addStudentAccount({
|
|
required String studentId,
|
|
required String name,
|
|
required String password,
|
|
int? grade,
|
|
}) async {
|
|
_isWorking = true;
|
|
notifyListeners();
|
|
try {
|
|
return await _createStudentRequest(
|
|
studentId: studentId,
|
|
name: name,
|
|
password: password,
|
|
grade: grade,
|
|
);
|
|
} finally {
|
|
_isWorking = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
/// 실제 계정 생성 HTTP 요청. isWorking 상태는 건드리지 않는다 —
|
|
/// 단건 등록(addStudentAccount)과 일괄 등록(bulkAddStudents) 양쪽에서
|
|
/// 각자 알맞은 시점에 isWorking을 관리하기 위해 분리했다.
|
|
Future<(bool success, String message)> _createStudentRequest({
|
|
required String studentId,
|
|
required String name,
|
|
required String password,
|
|
int? grade,
|
|
}) async {
|
|
try {
|
|
final url = Uri.parse('$baseUrl/api/users/create');
|
|
final response = await http.post(
|
|
url,
|
|
headers: {"Content-Type": "application/json"},
|
|
body: jsonEncode({
|
|
"studentId": studentId,
|
|
"name": name,
|
|
"password": password,
|
|
"grade": grade,
|
|
}),
|
|
);
|
|
final result = jsonDecode(utf8.decode(response.bodyBytes));
|
|
|
|
if (response.statusCode == 200 && result['status'] == 'success') {
|
|
return (true, '✅ ${result['message'] ?? '계정이 생성되었습니다.'}');
|
|
} else {
|
|
return (false, '❌ 생성 실패: ${result['message'] ?? '오류 발생'}');
|
|
}
|
|
} catch (e) {
|
|
return (false, '🚨 네트워크 에러: $e');
|
|
}
|
|
}
|
|
|
|
/// 🏫 이미 만들어진 학생 계정의 학년을 지정/변경한다. grade가 null이면 "미배정"으로 되돌린다.
|
|
Future<(bool success, String message)> updateStudentGrade({
|
|
required String studentId,
|
|
int? grade,
|
|
}) async {
|
|
_isWorking = true;
|
|
notifyListeners();
|
|
try {
|
|
final url = Uri.parse('$baseUrl/api/users/update-grade');
|
|
final response = await http.post(
|
|
url,
|
|
headers: {"Content-Type": "application/json"},
|
|
body: jsonEncode({"studentId": studentId, "grade": grade}),
|
|
);
|
|
final result = jsonDecode(utf8.decode(response.bodyBytes));
|
|
|
|
if (response.statusCode == 200 && result['status'] == 'success') {
|
|
return (true, '✅ ${result['message'] ?? '학년이 지정되었습니다.'}');
|
|
} else {
|
|
return (false, '❌ 지정 실패: ${result['message'] ?? '오류 발생'}');
|
|
}
|
|
} catch (e) {
|
|
return (false, '🚨 네트워크 에러: $e');
|
|
} finally {
|
|
_isWorking = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
|
|
/// ❌ 학생 계정 삭제.
|
|
Future<(bool success, String message)> deleteStudentAccount(
|
|
String studentId,
|
|
) async {
|
|
_isWorking = true;
|
|
notifyListeners();
|
|
final url = Uri.parse('$baseUrl/api/users/delete');
|
|
try {
|
|
final response = await http.delete(
|
|
url,
|
|
headers: {"Content-Type": "application/json"},
|
|
body: jsonEncode({"studentId": studentId}),
|
|
);
|
|
if (response.statusCode == 200) {
|
|
final resData = jsonDecode(utf8.decode(response.bodyBytes));
|
|
return (true, '✅ ${resData['message']}');
|
|
} else {
|
|
return (false, '❌ 삭제 실패하였습니다.');
|
|
}
|
|
} catch (e) {
|
|
return (false, '❌ 서버 에러 발생');
|
|
} finally {
|
|
_isWorking = false;
|
|
notifyListeners();
|
|
}
|
|
}
|
|
}
|