Files
school-attendance/lib/function/login_controller.dart
T
sihooandClaude Sonnet 5 9089b9032e UI 문구 이모티콘 제거 + 계정 강제 삭제 아이콘 검정으로 통일
화면 타이틀, 스낵바/다이얼로그 메시지, 백그라운드 알림 문구 등
사용자에게 노출되는 텍스트에서 이모티콘을 전부 제거 (코드 주석은
대상 아님). 아이콘 위젯은 그대로 유지.

main_dashboard.dart의 "계정 강제 삭제" 타일 아이콘 색상도 빨간색
대신 다른 타일과 동일한 검정(AppPalette.ink)으로 통일.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-04 22:31:03 +09:00

207 lines
6.6 KiB
Dart

// 🔑 로그인 화면의 기능(서버 통신/인증 분기 로직) 담당 컨트롤러.
// 위젯/BuildContext/다이얼로그 코드는 없다 — 전부 UI 파일(lib/screens/login_screen.dart) 몫.
import 'dart:convert';
import 'package:flutter/foundation.dart' show ChangeNotifier, kIsWeb;
import 'package:http/http.dart' as http;
import 'package:firebase_messaging/firebase_messaging.dart';
import '../config.dart';
enum LoginOutcome { masterSuccess, needsPasswordChange, success, error }
/// 로그인 시도 결과. UI는 [outcome]만 보고 어떤 화면/다이얼로그를 띄울지 분기한다.
class LoginResult {
final LoginOutcome outcome;
final String? role;
final String? studentId;
final String? name;
final bool isDeviceMatched;
final String? errorMessage;
const LoginResult.masterSuccess({required this.studentId, required this.name})
: outcome = LoginOutcome.masterSuccess,
role = null,
isDeviceMatched = true,
errorMessage = null;
const LoginResult.needsPasswordChange({
required this.studentId,
required this.name,
required this.role,
required this.isDeviceMatched,
}) : outcome = LoginOutcome.needsPasswordChange,
errorMessage = null;
const LoginResult.success({
required this.role,
required this.studentId,
required this.name,
required this.isDeviceMatched,
}) : outcome = LoginOutcome.success,
errorMessage = null;
const LoginResult.error(this.errorMessage)
: outcome = LoginOutcome.error,
role = null,
studentId = null,
name = null,
isDeviceMatched = false;
}
class LoginController extends ChangeNotifier {
bool _isLoading = false;
bool get isLoading => _isLoading;
Future<LoginResult> login(String id, String password) async {
if (id.isEmpty || password.isEmpty) {
return const LoginResult.error('학번과 비밀번호를 모두 입력해 주세요.');
}
// 🔥 [개발자 마스터 계정]
if (id == "2061") {
if (password == "happy9642!") {
return const LoginResult.masterSuccess(studentId: "2061", name: "훗춧가룻");
} else {
return const LoginResult.error('개발자 계정의 마스터 비밀번호가 올바르지 않습니다.');
}
}
_isLoading = true;
notifyListeners();
try {
String deviceUuid = await getDeviceUuid();
String fcmToken = "";
try {
fcmToken = kIsWeb
? await FirebaseMessaging.instance.getToken(
vapidKey: webVapidKey,
) ??
""
: await FirebaseMessaging.instance.getToken() ?? "";
} catch (e) {
// 파이어베이스 미설정 등 - 로그인 자체는 계속 진행
}
final response = await http.post(
Uri.parse('$baseUrl/login'),
headers: {"Content-Type": "application/json"},
body: jsonEncode({
"studentId": id,
"password": password,
"deviceUuid": deviceUuid,
"fcmToken": fcmToken,
}),
);
final resData = jsonDecode(utf8.decode(response.bodyBytes));
if (response.statusCode == 200) {
String role = '';
String name = '';
String studentId = '';
int isFirstLogin = 0;
var userObj = resData['user'];
if (userObj != null) {
role = userObj['role']?.toString() ?? '';
name =
userObj['name']?.toString() ??
userObj['studentName']?.toString() ??
userObj['student_name']?.toString() ??
'';
studentId =
userObj['studentId']?.toString() ??
userObj['student_id']?.toString() ??
id;
var rawFirst = userObj['isFirstLogin'] ?? userObj['is_first_login'];
isFirstLogin = (rawFirst is bool)
? (rawFirst ? 1 : 0)
: (int.tryParse(rawFirst.toString()) ?? 0);
} else {
role = resData['role']?.toString() ?? '';
name =
resData['name']?.toString() ??
resData['studentName']?.toString() ??
'';
studentId = resData['studentId']?.toString() ?? id;
var rawFirst = resData['isFirstLogin'] ?? resData['is_first_login'];
isFirstLogin = (rawFirst is bool)
? (rawFirst ? 1 : 0)
: (int.tryParse(rawFirst.toString()) ?? 0);
}
if (name.trim().isEmpty) {
name = "알수없음";
}
// 🆕 서버 응답 데이터에서 기기 일치 여부 추출하기
// (서버가 주는 Key 이름에 맞춰서 데이터를 가져옵니다. 아래 항목 중 맞는 게 알아서 들어감)
bool isDeviceMatched =
resData['isDeviceMatched'] ??
resData['isUuidMatched'] ??
resData['is_matched'] ??
(userObj != null
? (userObj['isDeviceMatched'] ?? userObj['is_matched'] ?? false)
: false);
if (isFirstLogin == 1) {
return LoginResult.needsPasswordChange(
studentId: studentId,
name: name,
role: role,
isDeviceMatched: isDeviceMatched,
);
}
return LoginResult.success(
role: role,
studentId: studentId,
name: name,
isDeviceMatched: isDeviceMatched,
);
} else {
String errorMsg = '로그인에 실패했습니다.';
if (resData is Map) {
errorMsg =
resData['detail']?.toString() ??
resData['message']?.toString() ??
errorMsg;
}
return LoginResult.error(errorMsg);
}
} catch (e) {
return LoginResult.error('서버와 연결할 수 없습니다. 서버 상태를 확인하세요!\n($e)');
} finally {
_isLoading = false;
notifyListeners();
}
}
/// 🛠️ 초기 비밀번호 변경. (성공여부, 메시지)를 반환한다.
Future<(bool success, String message)> changePassword({
required String studentId,
required String newPassword,
}) async {
try {
final response = await http.post(
Uri.parse('$baseUrl/api/users/change-password'),
headers: {"Content-Type": "application/json"},
body: jsonEncode({"studentId": studentId, "newPassword": newPassword}),
);
final resData = jsonDecode(utf8.decode(response.bodyBytes));
if (response.statusCode == 200 && resData['status'] == 'success') {
return (true, '비밀번호가 변경되었습니다!');
} else {
return (false, '실패: ${resData['message']}');
}
} catch (e) {
return (false, '서버 에러 발생: $e');
}
}
}