- 화면마다 위젯/스타일만 담당하는 UI 파일과, 서버통신/상태/파생로직만 담당하는 ChangeNotifier 컨트롤러 파일로 1:1 분리 (lib/screens/ -> lib/ui/ + lib/function/) - UI는 ListenableBuilder로 컨트롤러를 구독해서 재렌더링, 버튼은 컨트롤러 메서드만 호출 - 다이얼로그/스낵바 등 위젯 코드는 전부 UI 파일에 남기고, 컨트롤러는 결과값(성공여부+메시지) 또는 콜백으로만 UI와 통신 (BuildContext/Widget 의존성 없음) - 디자인/레이아웃은 기존과 완전히 동일하게 유지 (순수 코드 재배치) - change_password_screen.dart, debug_pocket_main.dart(미사용 파일)는 깨지지 않게 import 경로만 갱신하고 리팩터링은 보류
212 lines
6.7 KiB
Dart
212 lines
6.7 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');
|
|
}
|
|
}
|
|
}
|