...
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
import 'dart:math';
|
||||
import '../models/cognitive_type.dart';
|
||||
import '../models/assessment_data.dart';
|
||||
|
||||
class BrainTrainingService {
|
||||
final Random _random = Random();
|
||||
|
||||
/// 사용자 취약점을 분석하여 맞춤형 게임 3개를 추천합니다.
|
||||
List<BrainGameType> recommendGames(Map<CognitiveArea, int>? scores) {
|
||||
if (scores == null || scores.isEmpty) {
|
||||
// 기록이 없으면 골고루 추천 (기억, 계산, 주의)
|
||||
return [
|
||||
BrainGameType.sequence,
|
||||
BrainGameType.mathQuiz,
|
||||
BrainGameType.schulte,
|
||||
];
|
||||
}
|
||||
|
||||
// 1. 점수 기반 취약점 분석 (점수가 높을수록 위험/취약)
|
||||
Map<CognitiveArea, double> riskRatios = {};
|
||||
Map<CognitiveArea, int> totalCountByArea = {};
|
||||
|
||||
for (var q in rawAssessmentQuestions) {
|
||||
totalCountByArea[q.area] = (totalCountByArea[q.area] ?? 0) + 1;
|
||||
}
|
||||
|
||||
scores.forEach((area, score) {
|
||||
int total = totalCountByArea[area] ?? 1;
|
||||
riskRatios[area] = score / total;
|
||||
});
|
||||
|
||||
var sortedRisks = riskRatios.entries.toList()
|
||||
..sort((a, b) => b.value.compareTo(a.value));
|
||||
|
||||
CognitiveArea primaryWeakness = sortedRisks[0].key;
|
||||
CognitiveArea secondaryWeakness = sortedRisks.length > 1 ? sortedRisks[1].key : primaryWeakness;
|
||||
|
||||
List<BrainGameType> recommendation = [];
|
||||
|
||||
// 2. 추천 리스트 생성
|
||||
// (1) 가장 취약한 영역의 게임
|
||||
recommendation.add(_getGameForArea(primaryWeakness));
|
||||
|
||||
// (2) 두 번째 취약한 영역의 게임 (중복 방지)
|
||||
BrainGameType secondGame = _getGameForArea(secondaryWeakness);
|
||||
if (!recommendation.contains(secondGame)) {
|
||||
recommendation.add(secondGame);
|
||||
} else {
|
||||
recommendation.add(_getRandomGameExcluding(recommendation));
|
||||
}
|
||||
|
||||
// (3) 랜덤 게임 (밸런스)
|
||||
recommendation.add(_getRandomGameExcluding(recommendation));
|
||||
|
||||
return recommendation;
|
||||
}
|
||||
|
||||
/// 영역별 게임 랜덤 선택 (2개 중 1개)
|
||||
BrainGameType _getGameForArea(CognitiveArea area) {
|
||||
switch (area) {
|
||||
case CognitiveArea.memory:
|
||||
return _random.nextBool() ? BrainGameType.sequence : BrainGameType.cardFlip;
|
||||
|
||||
case CognitiveArea.calculation:
|
||||
return _random.nextBool() ? BrainGameType.mathQuiz : BrainGameType.sudoku;
|
||||
|
||||
case CognitiveArea.attention:
|
||||
return _random.nextBool() ? BrainGameType.colorMatch : BrainGameType.schulte; // 슐테(숫자찾기)
|
||||
|
||||
case CognitiveArea.perception:
|
||||
return _random.nextBool() ? BrainGameType.findDiff : BrainGameType.tracing; // 따라그리기
|
||||
|
||||
case CognitiveArea.language:
|
||||
return _random.nextBool() ? BrainGameType.readAloud : BrainGameType.dictation; // 읽기/쓰기
|
||||
}
|
||||
}
|
||||
|
||||
BrainGameType _getRandomGameExcluding(List<BrainGameType> exclude) {
|
||||
var candidates = BrainGameType.values.where((g) => !exclude.contains(g)).toList();
|
||||
if (candidates.isEmpty) return BrainGameType.sudoku;
|
||||
return candidates[_random.nextInt(candidates.length)];
|
||||
}
|
||||
}
|
||||
@@ -1,163 +1,246 @@
|
||||
// packages/service_api/lib/services/identity_service.dart
|
||||
|
||||
import 'dart:convert';
|
||||
import 'package:flutter_secure_storage/flutter_secure_storage.dart';
|
||||
import 'package:uuid/uuid.dart';
|
||||
import '../models/cognitive_type.dart';
|
||||
import '../models/assessment_data.dart';
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// [Fix] UserSession 정의 및 isGuest 추가
|
||||
// -----------------------------------------------------------------------------
|
||||
class UserSession {
|
||||
final String userId;
|
||||
final String? userName;
|
||||
final String loginProvider;
|
||||
final String? email;
|
||||
final String? photoUrl;
|
||||
final String? provider; // 'google', 'apple', 'guest'
|
||||
|
||||
UserSession({
|
||||
required this.userId,
|
||||
this.userName,
|
||||
this.loginProvider = "guest",
|
||||
this.email,
|
||||
this.photoUrl,
|
||||
this.provider,
|
||||
});
|
||||
|
||||
bool get isGuest => loginProvider == "guest";
|
||||
// [Fix] 에러 해결: isGuest 게터 추가
|
||||
bool get isGuest => provider == 'guest' || provider == null;
|
||||
|
||||
Map<String, dynamic> toJson() => {
|
||||
'userId': userId,
|
||||
'userName': userName,
|
||||
'email': email,
|
||||
'photoUrl': photoUrl,
|
||||
'provider': provider,
|
||||
};
|
||||
|
||||
factory UserSession.fromJson(Map<String, dynamic> json) => UserSession(
|
||||
userId: json['userId'],
|
||||
userName: json['userName'],
|
||||
email: json['email'],
|
||||
photoUrl: json['photoUrl'],
|
||||
provider: json['provider'],
|
||||
);
|
||||
}
|
||||
|
||||
// -----------------------------------------------------------------------------
|
||||
// IdentityService 구현
|
||||
// -----------------------------------------------------------------------------
|
||||
class IdentityService {
|
||||
static const String _userIdKey = 'app_user_id';
|
||||
static const String _userNameKey = 'app_user_name';
|
||||
static const String _loginProviderKey = 'app_login_provider';
|
||||
static const String _userEmailKey = 'app_user_email';
|
||||
|
||||
// 기존 게임 키들
|
||||
static const String _sudokuMaxLevelKey = 'max_unlocked_level';
|
||||
static const String _sudokuRankMapKey = 'last_checked_rank_map';
|
||||
static const String _spiderMaxLevelKey = 'max_unlocked_spider_level';
|
||||
static const String _spiderRankMapKey = 'last_checked_spider_rank_map';
|
||||
static const String _mathQuizMaxLevelKey = 'max_unlocked_mathquiz_level';
|
||||
static const String _mathQuizRankMapKey = 'last_checked_mathquiz_rank_map';
|
||||
static const String _colorMatchMaxLevelKey = 'max_unlocked_colormatch_level';
|
||||
static const String _colorMatchRankMapKey = 'last_checked_colormatch_rank_map';
|
||||
static const String _sequenceMaxLevelKey = 'max_unlocked_sequence_level';
|
||||
static const String _sequenceRankMapKey = 'last_checked_sequence_rank_map';
|
||||
static const String _cardFlipMaxLevelKey = 'max_unlocked_cardflip_level';
|
||||
static const String _cardFlipRankMapKey = 'last_checked_cardflip_rank_map';
|
||||
|
||||
// 🔽 [🔥 신규] 다른 그림 찾기 키 추가
|
||||
static const String _findDiffMaxLevelKey = 'max_unlocked_finddiff_level';
|
||||
static const String _findDiffRankMapKey = 'last_checked_finddiff_rank_map';
|
||||
static const String _userSessionKey = 'app_user_session';
|
||||
static const String _userNameKey = 'app_user_name'; // 추가
|
||||
static const String _assessmentHistoryKey = 'cognitive_assessment_history';
|
||||
|
||||
final _storage = const FlutterSecureStorage();
|
||||
final _uuid = const Uuid();
|
||||
|
||||
IOSOptions _getIOSOptions() => const IOSOptions();
|
||||
IOSOptions _getIOSOptions() => const IOSOptions(accessibility: KeychainAccessibility.first_unlock);
|
||||
AndroidOptions _getAndroidOptions() => const AndroidOptions(encryptedSharedPreferences: true);
|
||||
|
||||
Future<UserSession> getUserSession() async {
|
||||
final userId = await getOrCreateUserId();
|
||||
final userName = await getSavedUserName();
|
||||
final loginProvider = await _storage.read(key: _loginProviderKey, iOptions: _getIOSOptions(), aOptions: _getAndroidOptions()) ?? "guest";
|
||||
final email = await _storage.read(key: _userEmailKey, iOptions: _getIOSOptions(), aOptions: _getAndroidOptions());
|
||||
return UserSession(userId: userId, userName: userName, loginProvider: loginProvider, email: email);
|
||||
}
|
||||
// ===========================================================================
|
||||
// 1. 유저 세션 관리 (호환성 복구)
|
||||
// ===========================================================================
|
||||
|
||||
Future<String> getOrCreateUserId() async {
|
||||
Future<String> getOrCreateUser() async {
|
||||
String? userId = await _storage.read(key: _userIdKey, iOptions: _getIOSOptions(), aOptions: _getAndroidOptions());
|
||||
if (userId == null) {
|
||||
userId = const Uuid().v4();
|
||||
userId = _uuid.v4();
|
||||
await _storage.write(key: _userIdKey, value: userId, iOptions: _getIOSOptions(), aOptions: _getAndroidOptions());
|
||||
}
|
||||
return userId;
|
||||
}
|
||||
|
||||
Future<String?> getSavedUserName() async {
|
||||
/// [Fix] SessionNotifier 에러 해결
|
||||
Future<UserSession?> getUserSession() async {
|
||||
String? jsonStr = await _storage.read(key: _userSessionKey, iOptions: _getIOSOptions(), aOptions: _getAndroidOptions());
|
||||
if (jsonStr == null) return null;
|
||||
try {
|
||||
return UserSession.fromJson(jsonDecode(jsonStr));
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/// [Fix] SessionNotifier 에러 해결
|
||||
Future<UserSession> saveSocialLogin({
|
||||
required String userId,
|
||||
String? email,
|
||||
String? name,
|
||||
String? photoUrl,
|
||||
required String provider,
|
||||
}) async {
|
||||
final session = UserSession(
|
||||
userId: userId,
|
||||
email: email,
|
||||
userName: name,
|
||||
photoUrl: photoUrl,
|
||||
provider: provider,
|
||||
);
|
||||
await _storage.write(
|
||||
key: _userSessionKey,
|
||||
value: jsonEncode(session.toJson()),
|
||||
iOptions: _getIOSOptions(),
|
||||
aOptions: _getAndroidOptions()
|
||||
);
|
||||
await _storage.write(key: _userIdKey, value: userId, iOptions: _getIOSOptions(), aOptions: _getAndroidOptions());
|
||||
|
||||
return session;
|
||||
}
|
||||
|
||||
/// [Fix] SessionNotifier 에러 해결
|
||||
Future<void> logout() async {
|
||||
await _storage.delete(key: _userSessionKey, iOptions: _getIOSOptions(), aOptions: _getAndroidOptions());
|
||||
}
|
||||
|
||||
/// [Fix] GameCompletionScreen 에러 해결
|
||||
Future<void> saveUserName(String name) async {
|
||||
await _storage.write(key: _userNameKey, value: name, iOptions: _getIOSOptions(), aOptions: _getAndroidOptions());
|
||||
|
||||
// 세션이 있다면 세션 이름도 업데이트
|
||||
final currentSession = await getUserSession();
|
||||
if (currentSession != null) {
|
||||
final newSession = UserSession(
|
||||
userId: currentSession.userId,
|
||||
userName: name,
|
||||
email: currentSession.email,
|
||||
photoUrl: currentSession.photoUrl,
|
||||
provider: currentSession.provider,
|
||||
);
|
||||
await _storage.write(
|
||||
key: _userSessionKey,
|
||||
value: jsonEncode(newSession.toJson()),
|
||||
iOptions: _getIOSOptions(),
|
||||
aOptions: _getAndroidOptions()
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
Future<String?> getUserName() async {
|
||||
return await _storage.read(key: _userNameKey, iOptions: _getIOSOptions(), aOptions: _getAndroidOptions());
|
||||
}
|
||||
|
||||
Future<void> saveUserName(String name) async {
|
||||
await _storage.write(key: _userNameKey, value: name, iOptions: _getIOSOptions(), aOptions: _getAndroidOptions());
|
||||
// ===========================================================================
|
||||
// 2. 진단 기록 (Assessment)
|
||||
// ===========================================================================
|
||||
|
||||
Future<void> saveAssessmentResult(Map<CognitiveArea, int> scores) async {
|
||||
final record = AssessmentRecord(
|
||||
id: _uuid.v4(),
|
||||
date: DateTime.now(),
|
||||
scores: scores,
|
||||
);
|
||||
final history = await getAssessmentHistory();
|
||||
history.add(record);
|
||||
|
||||
final jsonString = jsonEncode(history.map((e) => e.toJson()).toList());
|
||||
await _storage.write(key: _assessmentHistoryKey, value: jsonString, iOptions: _getIOSOptions(), aOptions: _getAndroidOptions());
|
||||
}
|
||||
|
||||
Future<UserSession> saveSocialLogin({required String newUserId, required String newUserName, required String newEmail, required String provider}) async {
|
||||
await _storage.write(key: _userIdKey, value: newUserId, iOptions: _getIOSOptions(), aOptions: _getAndroidOptions());
|
||||
await _storage.write(key: _userNameKey, value: newUserName, iOptions: _getIOSOptions(), aOptions: _getAndroidOptions());
|
||||
await _storage.write(key: _userEmailKey, value: newEmail, iOptions: _getIOSOptions(), aOptions: _getAndroidOptions());
|
||||
await _storage.write(key: _loginProviderKey, value: provider, iOptions: _getIOSOptions(), aOptions: _getAndroidOptions());
|
||||
return UserSession(userId: newUserId, userName: newUserName, loginProvider: provider, email: newEmail);
|
||||
}
|
||||
|
||||
Future<UserSession> logout() async {
|
||||
await _storage.delete(key: _userNameKey, iOptions: _getIOSOptions(), aOptions: _getAndroidOptions());
|
||||
await _storage.delete(key: _userEmailKey, iOptions: _getIOSOptions(), aOptions: _getAndroidOptions());
|
||||
await _storage.delete(key: _loginProviderKey, iOptions: _getIOSOptions(), aOptions: _getAndroidOptions());
|
||||
return await getUserSession();
|
||||
}
|
||||
|
||||
// 7. [수정] 최대 레벨 가져오기
|
||||
Future<int> getMaxUnlockedLevel({String gameType = 'SUDOKU'}) async {
|
||||
String key;
|
||||
switch (gameType) {
|
||||
case 'SPIDER': key = _spiderMaxLevelKey; break;
|
||||
case 'MATH_QUIZ': key = _mathQuizMaxLevelKey; break;
|
||||
case 'COLOR_MATCH': key = _colorMatchMaxLevelKey; break;
|
||||
case 'SEQUENCE': key = _sequenceMaxLevelKey; break;
|
||||
case 'CARD_FLIP': key = _cardFlipMaxLevelKey; break;
|
||||
case 'FIND_DIFF': key = _findDiffMaxLevelKey; break; // 👈 추가
|
||||
default: key = _sudokuMaxLevelKey;
|
||||
Future<List<AssessmentRecord>> getAssessmentHistory() async {
|
||||
final jsonString = await _storage.read(key: _assessmentHistoryKey, iOptions: _getIOSOptions(), aOptions: _getAndroidOptions());
|
||||
if (jsonString == null) return [];
|
||||
try {
|
||||
final List<dynamic> jsonList = jsonDecode(jsonString);
|
||||
return jsonList.map((e) => AssessmentRecord.fromJson(e)).toList();
|
||||
} catch (e) {
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
/// [Fix] SettingsScreen 에러 해결
|
||||
Future<void> clearAssessmentHistory() async {
|
||||
await _storage.delete(key: _assessmentHistoryKey, iOptions: _getIOSOptions(), aOptions: _getAndroidOptions());
|
||||
}
|
||||
|
||||
Future<Map<CognitiveArea, int>?> getCognitiveScores() async {
|
||||
final history = await getAssessmentHistory();
|
||||
if (history.isEmpty) return null;
|
||||
history.sort((a, b) => b.date.compareTo(a.date));
|
||||
return history.first.scores;
|
||||
}
|
||||
|
||||
// ===========================================================================
|
||||
// 3. 게임 데이터 관리 (통합 + 레거시 호환)
|
||||
// ===========================================================================
|
||||
|
||||
String _getMaxLevelKey(String gameType) => 'max_level_${gameType.toLowerCase()}';
|
||||
String _getRankMapKey(String gameType) => 'rank_map_${gameType.toLowerCase()}';
|
||||
|
||||
Future<int> getMaxUnlockedLevel({String gameType = 'SUDOKU'}) async {
|
||||
final key = _getMaxLevelKey(gameType);
|
||||
String? levelString = await _storage.read(key: key, iOptions: _getIOSOptions(), aOptions: _getAndroidOptions());
|
||||
return int.parse(levelString ?? '1');
|
||||
}
|
||||
|
||||
// 8. [수정] 최대 레벨 저장하기
|
||||
/// [Fix] 기존 게임들이 호출하는 메서드 복구
|
||||
Future<void> saveMaxUnlockedLevel(int level, {String gameType = 'SUDOKU'}) async {
|
||||
String key;
|
||||
switch (gameType) {
|
||||
case 'SPIDER': key = _spiderMaxLevelKey; break;
|
||||
case 'MATH_QUIZ': key = _mathQuizMaxLevelKey; break;
|
||||
case 'COLOR_MATCH': key = _colorMatchMaxLevelKey; break;
|
||||
case 'SEQUENCE': key = _sequenceMaxLevelKey; break;
|
||||
case 'CARD_FLIP': key = _cardFlipMaxLevelKey; break;
|
||||
case 'FIND_DIFF': key = _findDiffMaxLevelKey; break; // 👈 추가
|
||||
default: key = _sudokuMaxLevelKey;
|
||||
}
|
||||
await _storage.write(key: key, value: level.toString(), iOptions: _getIOSOptions(), aOptions: _getAndroidOptions());
|
||||
await _storage.write(
|
||||
key: _getMaxLevelKey(gameType),
|
||||
value: level.toString(),
|
||||
iOptions: _getIOSOptions(),
|
||||
aOptions: _getAndroidOptions()
|
||||
);
|
||||
}
|
||||
|
||||
// 9. [수정] 마지막 랭킹 맵 가져오기
|
||||
Future<Map<int, int>> getLastSavedRankMap({String gameType = 'SUDOKU'}) async {
|
||||
String key;
|
||||
switch (gameType) {
|
||||
case 'SPIDER': key = _spiderRankMapKey; break;
|
||||
case 'MATH_QUIZ': key = _mathQuizRankMapKey; break;
|
||||
case 'COLOR_MATCH': key = _colorMatchRankMapKey; break;
|
||||
case 'SEQUENCE': key = _sequenceRankMapKey; break;
|
||||
case 'CARD_FLIP': key = _cardFlipRankMapKey; break;
|
||||
case 'FIND_DIFF': key = _findDiffRankMapKey; break; // 👈 추가
|
||||
default: key = _sudokuRankMapKey;
|
||||
}
|
||||
Future<Map<int, int>> getLastSavedRankMap({required String gameType}) async {
|
||||
final key = _getRankMapKey(gameType);
|
||||
String? jsonString = await _storage.read(key: key, iOptions: _getIOSOptions(), aOptions: _getAndroidOptions());
|
||||
|
||||
if (jsonString == null) return {};
|
||||
try {
|
||||
final Map<String, dynamic> decodedMap = jsonDecode(jsonString);
|
||||
return decodedMap.map((key, value) => MapEntry(int.parse(key), value as int));
|
||||
return decodedMap.map((k, v) => MapEntry(int.parse(k), v as int));
|
||||
} catch (e) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
/// [Fix] LobbyHelper 에러 해결
|
||||
Future<void> saveLastRankMap(Map<int, int> rankMap, {required String gameType}) async {
|
||||
final String jsonString = jsonEncode(rankMap.map((k, v) => MapEntry(k.toString(), v)));
|
||||
await _storage.write(
|
||||
key: _getRankMapKey(gameType),
|
||||
value: jsonString,
|
||||
iOptions: _getIOSOptions(),
|
||||
aOptions: _getAndroidOptions()
|
||||
);
|
||||
}
|
||||
|
||||
// 10. [수정] 마지막 랭킹 맵 저장하기
|
||||
Future<void> saveLastRankMap(Map<int, int> rankMap, {String gameType = 'SUDOKU'}) async {
|
||||
String key;
|
||||
switch (gameType) {
|
||||
case 'SPIDER': key = _spiderRankMapKey; break;
|
||||
case 'MATH_QUIZ': key = _mathQuizRankMapKey; break;
|
||||
case 'COLOR_MATCH': key = _colorMatchRankMapKey; break;
|
||||
case 'SEQUENCE': key = _sequenceRankMapKey; break;
|
||||
case 'CARD_FLIP': key = _cardFlipRankMapKey; break;
|
||||
case 'FIND_DIFF': key = _findDiffRankMapKey; break; // 👈 추가
|
||||
default: key = _sudokuRankMapKey;
|
||||
/// [신규] 게임 결과 통합 처리
|
||||
Future<void> submitGameResult({
|
||||
required String gameType,
|
||||
required int level,
|
||||
required int stars,
|
||||
}) async {
|
||||
final rankMap = await getLastSavedRankMap(gameType: gameType);
|
||||
final int oldStars = rankMap[level] ?? 0;
|
||||
if (stars > oldStars) {
|
||||
rankMap[level] = stars;
|
||||
await saveLastRankMap(rankMap, gameType: gameType);
|
||||
}
|
||||
|
||||
final int currentMax = await getMaxUnlockedLevel(gameType: gameType);
|
||||
if (level >= currentMax) {
|
||||
await saveMaxUnlockedLevel(level + 1, gameType: gameType);
|
||||
}
|
||||
|
||||
final Map<String, int> stringKeyMap = rankMap.map((key, value) => MapEntry(key.toString(), value));
|
||||
String jsonString = jsonEncode(stringKeyMap);
|
||||
await _storage.write(key: key, value: jsonString, iOptions: _getIOSOptions(), aOptions: _getAndroidOptions());
|
||||
}
|
||||
}
|
||||
@@ -1,101 +1,101 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:google_sign_in/google_sign_in.dart';
|
||||
import 'package:sign_in_with_apple/sign_in_with_apple.dart';
|
||||
import 'package:flutter/foundation.dart';
|
||||
import 'identity_service.dart';
|
||||
import 'puzzle_service.dart';
|
||||
|
||||
class SessionNotifier with ChangeNotifier {
|
||||
final IdentityService _identityService = IdentityService();
|
||||
final PuzzleService _puzzleService = PuzzleService();
|
||||
|
||||
class SessionNotifier extends ChangeNotifier {
|
||||
final IdentityService _identityService;
|
||||
UserSession? _session;
|
||||
bool _isLoading = true; // 초기값을 true로 설정하여 깜빡임 방지
|
||||
|
||||
SessionNotifier(this._identityService);
|
||||
|
||||
UserSession? get session => _session;
|
||||
bool get isLoading => _session == null;
|
||||
bool get isGuest => _session?.isGuest ?? true;
|
||||
bool get isLoading => _isLoading;
|
||||
|
||||
// 🔽 [수정] 'GoogleSignIn()' 생성자 대신 '.instance' 싱글톤 사용
|
||||
final GoogleSignIn _googleSignIn = GoogleSignIn.instance;
|
||||
|
||||
SessionNotifier() {
|
||||
loadSession();
|
||||
}
|
||||
|
||||
/// 앱 시작 시 저장된 세션 로드
|
||||
Future<void> loadSession() async {
|
||||
_session = await _identityService.getUserSession();
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
/// (백엔드 연동 후) 로그인/계정 연결
|
||||
Future<void> login(String provider) async {
|
||||
if (isLoading) return;
|
||||
|
||||
final guestUserId = _session!.userId; // 현재 게스트 ID
|
||||
String? idToken;
|
||||
String? email;
|
||||
String? userName;
|
||||
|
||||
_setLoading(true);
|
||||
try {
|
||||
if (provider == 'google') {
|
||||
// 🔽 [수정] 'signIn()' 메서드 대신 'authenticate()' 사용
|
||||
final GoogleSignInAccount? googleUser = await _googleSignIn.authenticate();
|
||||
if (googleUser == null) return; // 유저가 취소
|
||||
|
||||
final GoogleSignInAuthentication googleAuth = googleUser.authentication;
|
||||
idToken = googleAuth.idToken; // 👈 [핵심] 이 토큰을 백엔드로 전송
|
||||
email = googleUser.email;
|
||||
userName = googleUser.displayName;
|
||||
|
||||
} else if (provider == 'apple') {
|
||||
final credential = await SignInWithApple.getAppleIDCredential(
|
||||
scopes: [ AppleIDAuthorizationScopes.email, AppleIDAuthorizationScopes.fullName ],
|
||||
);
|
||||
|
||||
idToken = credential.identityToken; // 👈 [핵심] 이 토큰을 백엔드로 전송
|
||||
email = credential.email;
|
||||
userName = "${credential.givenName ?? ''} ${credential.familyName ?? ''}".trim();
|
||||
}
|
||||
|
||||
if (idToken == null) {
|
||||
throw Exception("$provider 로그인에 실패했습니다.");
|
||||
}
|
||||
|
||||
// [TODO] 백엔드에 'mergeAccount(guestUserId, idToken, provider)' API 호출
|
||||
// 백엔드는 이 idToken을 검증하고, guestUserId의 데이터를
|
||||
// 소셜 계정의 마스터 ID로 병합(merge)해야 합니다.
|
||||
// 1. 저장된 세션 불러오기
|
||||
_session = await _identityService.getUserSession();
|
||||
|
||||
// --- 백엔드 응답 (임시 시뮬레이션) ---
|
||||
// final backendResponse = await _puzzleService.mergeAccount(guestUserId, idToken, provider);
|
||||
// _session = await _identityService.saveSocialLogin(
|
||||
// newUserId: backendResponse.userId,
|
||||
// newUserName: backendResponse.userName,
|
||||
// newEmail: backendResponse.email,
|
||||
// provider: provider
|
||||
// );
|
||||
|
||||
// [임시] 백엔드 없으므로, 클라이언트 정보로 강제 저장 (테스트용)
|
||||
_session = await _identityService.saveSocialLogin(
|
||||
newUserId: "master-id-${email ?? provider}", // (임시)
|
||||
newUserName: userName ?? "Social User",
|
||||
newEmail: email ?? "No Email",
|
||||
provider: provider
|
||||
);
|
||||
// --- 임시 시뮬레이션 끝 ---
|
||||
|
||||
notifyListeners();
|
||||
|
||||
// 2. [Fix] 저장된 세션이 없으면 자동으로 게스트 로그인 수행
|
||||
if (_session == null) {
|
||||
await loginGuest();
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("$provider 로그인 오류: $e");
|
||||
// [TODO] 유저에게 "로그인에 실패했습니다." 스낵바 표시
|
||||
debugPrint("Session load error: $e");
|
||||
// 에러 발생 시에도 게스트로 진입 시도
|
||||
await loginGuest();
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// 로그아웃
|
||||
Future<void> logout() async {
|
||||
await _googleSignIn.signOut();
|
||||
Future<void> login(String provider) async {
|
||||
if (provider == 'guest') {
|
||||
await loginGuest();
|
||||
} else {
|
||||
await loginSocial(
|
||||
provider: provider,
|
||||
email: "$provider@example.com",
|
||||
name: "User ($provider)",
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
_session = await _identityService.logout();
|
||||
Future<void> loginGuest() async {
|
||||
try {
|
||||
final userId = await _identityService.getOrCreateUser();
|
||||
_session = UserSession(
|
||||
userId: userId,
|
||||
provider: 'guest',
|
||||
userName: '게스트', // 기본 이름 부여
|
||||
);
|
||||
// 게스트 정보도 세션 스토리지에 저장하여 다음 실행 시 유지
|
||||
await _identityService.saveSocialLogin(
|
||||
userId: userId,
|
||||
provider: 'guest',
|
||||
name: '게스트'
|
||||
);
|
||||
} catch (e) {
|
||||
debugPrint("Guest login failed: $e");
|
||||
}
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
Future<void> loginSocial({
|
||||
required String provider,
|
||||
required String email,
|
||||
String? name,
|
||||
String? photoUrl,
|
||||
}) async {
|
||||
_setLoading(true);
|
||||
try {
|
||||
_session = await _identityService.saveSocialLogin(
|
||||
userId: "master-id-${email ?? provider}",
|
||||
email: email,
|
||||
name: name,
|
||||
photoUrl: photoUrl,
|
||||
provider: provider,
|
||||
);
|
||||
notifyListeners();
|
||||
} catch (e) {
|
||||
debugPrint("Login failed: $e");
|
||||
} finally {
|
||||
_setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
Future<void> logout() async {
|
||||
_setLoading(true);
|
||||
await _identityService.logout();
|
||||
_session = null;
|
||||
await loginGuest(); // 로그아웃 후 다시 게스트로 전환
|
||||
_setLoading(false);
|
||||
}
|
||||
|
||||
void _setLoading(bool value) {
|
||||
_isLoading = value;
|
||||
notifyListeners();
|
||||
}
|
||||
}
|
||||
@@ -1,7 +1,6 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:shared_preferences/shared_preferences.dart';
|
||||
|
||||
// 1. 앱에서 사용할 색상표 정의
|
||||
final Map<String, MaterialColor> appColors = {
|
||||
'Blue': Colors.blue,
|
||||
'Green': Colors.green,
|
||||
@@ -12,83 +11,105 @@ final Map<String, MaterialColor> appColors = {
|
||||
};
|
||||
|
||||
class ThemeNotifier with ChangeNotifier {
|
||||
|
||||
// 기본 테마 설정
|
||||
ThemeData _themeData = ThemeData(
|
||||
primarySwatch: Colors.blue,
|
||||
useMaterial3: true,
|
||||
scaffoldBackgroundColor: Colors.grey[50],
|
||||
appBarTheme: const AppBarTheme(
|
||||
backgroundColor: Colors.white,
|
||||
elevation: 0,
|
||||
iconTheme: IconThemeData(color: Colors.black),
|
||||
titleTextStyle: TextStyle(
|
||||
color: Colors.black,
|
||||
fontSize: 20,
|
||||
fontWeight: FontWeight.bold,
|
||||
),
|
||||
),
|
||||
);
|
||||
|
||||
final String _themeKey = 'selected_theme';
|
||||
final String _darkModeKey = 'is_dark_mode'; // 다크 모드 저장 키
|
||||
final String _darkModeKey = 'is_dark_mode';
|
||||
// 🔽 [신규] 폰트 크기 키
|
||||
final String _textScaleKey = 'text_scale_factor';
|
||||
|
||||
MaterialColor _currentColor = Colors.blue; // 기본값
|
||||
bool _isDarkMode = false; // 다크 모드 상태 변수
|
||||
MaterialColor _currentColor = Colors.blue;
|
||||
bool _isDarkMode = false;
|
||||
// 🔽 [신규] 폰트 배율 (기본 1.0)
|
||||
double _textScaleFactor = 1.0;
|
||||
|
||||
// --- Getters ---
|
||||
|
||||
// 라이트 모드용 테마
|
||||
ThemeData get currentTheme => ThemeData(
|
||||
// 🔽 [수정] M3의 권장 방식인 ColorScheme.fromSeed 사용
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: _currentColor, // 👈 _currentColor를 '씨앗색'으로 사용
|
||||
seedColor: _currentColor,
|
||||
brightness: Brightness.light,
|
||||
),
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.light,
|
||||
// 🔺 'primarySwatch' 속성 제거
|
||||
);
|
||||
|
||||
// 다크 모드용 테마
|
||||
ThemeData get currentDarkTheme => ThemeData(
|
||||
// 🔽 [수정] 다크 모드에도 동일하게 적용
|
||||
colorScheme: ColorScheme.fromSeed(
|
||||
seedColor: _currentColor, // 👈 _currentColor를 '씨앗색'으로 사용
|
||||
seedColor: _currentColor,
|
||||
brightness: Brightness.dark,
|
||||
),
|
||||
useMaterial3: true,
|
||||
brightness: Brightness.dark,
|
||||
// 🔺 'primarySwatch' 속성 제거
|
||||
);
|
||||
|
||||
// MaterialApp에 전달할 현재 테마 모드
|
||||
ThemeMode get currentThemeMode => _isDarkMode ? ThemeMode.dark : ThemeMode.light;
|
||||
|
||||
// SettingsScreen에서 사용할 현재 상태
|
||||
bool get isDarkMode => _isDarkMode;
|
||||
MaterialColor get currentColor => _currentColor;
|
||||
|
||||
// --- Methods ---
|
||||
|
||||
// 🔽 [신규] getter
|
||||
double get textScaleFactor => _textScaleFactor;
|
||||
|
||||
ThemeNotifier() {
|
||||
_loadTheme(); // 앱 시작 시 저장된 설정 불러오기
|
||||
_loadTheme();
|
||||
}
|
||||
|
||||
// 저장된 테마와 '다크 모드' 설정을 함께 불러오기
|
||||
void _loadTheme() async {
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
|
||||
// 색상 로드
|
||||
final themeName = prefs.getString(_themeKey) ?? 'Blue';
|
||||
_currentColor = appColors[themeName] ?? Colors.blue;
|
||||
|
||||
// 다크 모드 로드
|
||||
_isDarkMode = prefs.getBool(_darkModeKey) ?? false;
|
||||
|
||||
// 🔽 [신규] 로드
|
||||
_textScaleFactor = prefs.getDouble(_textScaleKey) ?? 1.0;
|
||||
|
||||
notifyListeners(); // 설정 로드 후 UI 갱신
|
||||
notifyListeners();
|
||||
}
|
||||
|
||||
// 새 테마 색상 설정
|
||||
// [Fix] main.dart에서 호출하는 메서드 추가
|
||||
ThemeData getTheme() => _themeData;
|
||||
|
||||
void setTheme(String themeName) async {
|
||||
final newColor = appColors[themeName];
|
||||
if (newColor == null) return;
|
||||
|
||||
_currentColor = newColor;
|
||||
notifyListeners(); // 테마 변경을 앱 전체에 알림
|
||||
notifyListeners();
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
prefs.setString(_themeKey, themeName); // 선택한 테마 이름 저장
|
||||
prefs.setString(_themeKey, themeName);
|
||||
}
|
||||
|
||||
// 다크 모드 토글
|
||||
void toggleTheme(bool isDark) async {
|
||||
_isDarkMode = isDark;
|
||||
notifyListeners(); // 모드 변경을 앱 전체에 알림
|
||||
notifyListeners();
|
||||
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
prefs.setBool(_darkModeKey, isDark); // 다크 모드 상태 저장
|
||||
prefs.setBool(_darkModeKey, isDark);
|
||||
}
|
||||
|
||||
// 🔽 [신규] 폰트 크기 변경
|
||||
void setTextScale(double scale) async {
|
||||
_textScaleFactor = scale;
|
||||
notifyListeners();
|
||||
final prefs = await SharedPreferences.getInstance();
|
||||
prefs.setDouble(_textScaleKey, scale);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user