This commit is contained in:
2025-11-17 18:21:49 +09:00
parent 13ed537b23
commit 86611ce092
160 changed files with 7829 additions and 452 deletions
@@ -0,0 +1,222 @@
import 'game_difficulty.dart';
/// 빈칸의 유형을 정의
enum MathQuizBlankType {
numbersOnly, // 숫자만
operatorsOnly, // 연산자만
numbersAndOperators, // 둘 다
}
/// 퍼즐의 레이아웃 형태를 정의
enum MathQuizLayout {
/// A + B = C 또는 A + B * C = D
singleLine,
/// 'ㄱ', 'ㄴ' 모양 (변수 4개)
linkedL,
/// 3x3 이상 그리드 (변수 8개 이상)
gridSquare,
}
/// 수학 퀴즈 게임의 난이도 정의
class MathQuizDifficulty extends GameDifficulty {
final int levelIndex;
final MathQuizLayout layout;
final String operators;
final int blankCount;
final MathQuizBlankType blankType;
/// [수정] 연산 복잡도 (예: 2=A+B, 3=A+B*C, 4=2x2그리드)
final int operationCount;
const MathQuizDifficulty({
required this.levelIndex,
required super.name,
required super.contextId,
required this.layout,
required this.operators,
required this.blankCount,
required this.blankType,
required this.operationCount,
});
}
/// [수정됨] 앱 전역에서 사용할 수학 퀴즈 난이도 목록 (15단계)
class MathQuizDifficulties {
static final List<MathQuizDifficulty> allDifficulties = [
// --- 패턴 1: 숫자 2개 (A op B = C) [Lv 1-3] ---
// (연산자 우선순위 없음)
const MathQuizDifficulty(
levelIndex: 1,
name: 'Lv. 1: 숫자 2개 (숫자 빈칸)',
contextId: 'MATH_L1_OP2_NUM',
layout: MathQuizLayout.singleLine,
operators: '+,-',
blankCount: 1,
blankType: MathQuizBlankType.numbersOnly,
operationCount: 2, // A, B
),
const MathQuizDifficulty(
levelIndex: 2,
name: 'Lv. 2: 숫자 2개 (연산자 빈칸)',
contextId: 'MATH_L2_OP2_OP',
layout: MathQuizLayout.singleLine,
operators: '+,-',
blankCount: 1,
blankType: MathQuizBlankType.operatorsOnly,
operationCount: 2,
),
const MathQuizDifficulty(
levelIndex: 3,
name: 'Lv. 3: 숫자 2개 (사칙연산)',
contextId: 'MATH_L3_OP2_ANY',
layout: MathQuizLayout.singleLine,
operators: '+,-,*,/',
blankCount: 2,
blankType: MathQuizBlankType.numbersAndOperators,
operationCount: 2,
),
// --- 패턴 2: 숫자 3개 (A op1 B op2 C = D) [Lv 4-6] ---
// (연산자 우선순위 *적용*)
const MathQuizDifficulty(
levelIndex: 4,
name: 'Lv. 4: 숫자 3개 (숫자 빈칸)',
contextId: 'MATH_L4_OP3_NUM',
layout: MathQuizLayout.singleLine,
operators: '+,-,*,/',
blankCount: 2,
blankType: MathQuizBlankType.numbersOnly,
operationCount: 3, // A, B, C
),
const MathQuizDifficulty(
levelIndex: 5,
name: 'Lv. 5: 숫자 3개 (연산자 빈칸)',
contextId: 'MATH_L5_OP3_OP',
layout: MathQuizLayout.singleLine,
operators: '+,-,*,/',
blankCount: 2,
blankType: MathQuizBlankType.operatorsOnly,
operationCount: 3,
),
const MathQuizDifficulty(
levelIndex: 6,
name: 'Lv. 6: 숫자 3개 (랜덤 빈칸)',
contextId: 'MATH_L6_OP3_ANY',
layout: MathQuizLayout.singleLine,
operators: '+,-,*,/',
blankCount: 3,
blankType: MathQuizBlankType.numbersAndOperators,
operationCount: 3,
),
// --- 패턴 3: 숫자 4개 (2x2 그리드 'ㄱ', 'ㄴ') [Lv 7-9] ---
const MathQuizDifficulty(
levelIndex: 7,
name: 'Lv. 7: 숫자 4개 (숫자 빈칸)',
contextId: 'MATH_L7_OP4_NUM',
layout: MathQuizLayout.linkedL,
operators: '+,-',
blankCount: 2,
blankType: MathQuizBlankType.numbersOnly,
operationCount: 4, // A, B, C, D
),
const MathQuizDifficulty(
levelIndex: 8,
name: 'Lv. 8: 숫자 4개 (연산자 빈칸)',
contextId: 'MATH_L8_OP4_OP',
layout: MathQuizLayout.linkedL,
operators: '+,-',
blankCount: 2,
blankType: MathQuizBlankType.operatorsOnly,
operationCount: 4,
),
const MathQuizDifficulty(
levelIndex: 9,
name: 'Lv. 9: 숫자 4개 (사칙연산)',
contextId: 'MATH_L9_OP4_ANY',
layout: MathQuizLayout.linkedL,
operators: '+,-,*,/',
blankCount: 3,
blankType: MathQuizBlankType.numbersAndOperators,
operationCount: 4,
),
// --- 패턴 4: 3x3 그리드 (숫자 9개) [Lv 10-12] ---
const MathQuizDifficulty(
levelIndex: 10,
name: 'Lv. 10: 3x3 그리드 (숫자)',
contextId: 'MATH_L10_OP9_NUM',
layout: MathQuizLayout.gridSquare,
operators: '+,-',
blankCount: 3,
blankType: MathQuizBlankType.numbersOnly,
operationCount: 9, // 9 Variables
),
const MathQuizDifficulty(
levelIndex: 11,
name: 'Lv. 11: 3x3 그리드 (연산자)',
contextId: 'MATH_L11_OP9_OP',
layout: MathQuizLayout.gridSquare,
operators: '+,-',
blankCount: 3,
blankType: MathQuizBlankType.operatorsOnly,
operationCount: 9,
),
const MathQuizDifficulty(
levelIndex: 12,
name: 'Lv. 12: 3x3 그리드 (사칙연산)',
contextId: 'MATH_L12_OP9_ANY',
layout: MathQuizLayout.gridSquare,
operators: '+,-,*,/',
blankCount: 4,
blankType: MathQuizBlankType.numbersAndOperators,
operationCount: 9,
),
// --- 패턴 5: 4x4 그리드 (숫자 16개) [Lv 13-15] ---
const MathQuizDifficulty(
levelIndex: 13,
name: 'Lv. 13: 4x4 그리드 (숫자)',
contextId: 'MATH_L13_OP16_NUM',
layout: MathQuizLayout.gridSquare,
operators: '+,-,*,/',
blankCount: 5,
blankType: MathQuizBlankType.numbersOnly,
operationCount: 16,
),
const MathQuizDifficulty(
levelIndex: 14,
name: 'Lv. 14: 4x4 그리드 (연산자)',
contextId: 'MATH_L14_OP16_OP',
layout: MathQuizLayout.gridSquare,
operators: '+,-,*,/',
blankCount: 5,
blankType: MathQuizBlankType.operatorsOnly,
operationCount: 16,
),
const MathQuizDifficulty(
levelIndex: 15,
name: 'Lv. 15: 4x4 그리드 (랜덤)',
contextId: 'MATH_L15_OP16_ANY',
layout: MathQuizLayout.gridSquare,
operators: '+,-,*,/',
blankCount: 6,
blankType: MathQuizBlankType.numbersAndOperators,
operationCount: 16,
),
];
/// 레벨 인덱스로 레벨 정보 찾기
static MathQuizDifficulty getLevel(int levelIndex) {
if (levelIndex < 1) levelIndex = 1;
if (levelIndex > allDifficulties.length) levelIndex = allDifficulties.length;
return allDifficulties.firstWhere((level) => level.levelIndex == levelIndex,
orElse: () => allDifficulties[0]
);
}
/// 랭킹 화면용 맵 (ContextId -> 이름)
static Map<String, String> get contextIdToNameMap {
return { for (var level in allDifficulties) level.contextId : level.name };
}
}
+3 -3
View File
@@ -1,16 +1,16 @@
// packages/service_api/lib/service_api.dart
// Models
export 'models/game_difficulty.dart'; // 👈 [추가]
export 'models/game_difficulty.dart';
export 'models/game_rank_dto.dart';
export 'models/sudoku_game_dto.dart';
export 'models/sudoku_theme.dart';
export 'models/unified_rank_dto.dart';
export 'models/validate_result_dto.dart';
// ❌ (game_level.dart는 여기서 삭제)
export 'models/math_quiz_difficulty.dart'; // 👈 [추가]
// Services
export 'services/identity_service.dart';
export 'services/puzzle_service.dart';
export 'services/theme_notifier.dart';
export 'services/session_notifier.dart';
export 'services/session_notifier.dart'; // 👈 [추가]
@@ -21,10 +21,8 @@ class UserSession {
// 앱-고유 ID와 사용자 이름, 레벨 진행 상황을 관리하는 서비스
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';
@@ -33,9 +31,12 @@ class IdentityService {
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';
final _storage = const FlutterSecureStorage();
/// 🔽 [신규] iOS 앱 간 데이터 공유를 위한 옵션
IOSOptions _getIOSOptions() => const IOSOptions(
// 🔽 [수정] Xcode 설정 전까지 'groupId'를 주석 처리하여 크래시 방지
// groupId: 'group.com.lunaticbum.mygamecenter',
@@ -45,9 +46,9 @@ class IdentityService {
encryptedSharedPreferences: true,
);
// 🔽 [신규] 1. 현재 세션 정보를 '객체'로 가져오기
// 1. 현재 세션 정보를 '객체'로 가져오기
Future<UserSession> getUserSession() async {
final userId = await getOrCreateUserId(); // 게스트 ID는 항상 보장
final userId = await getOrCreateUserId();
final userName = await getSavedUserName();
final loginProvider = await _storage.read(
key: _loginProviderKey,
@@ -107,12 +108,12 @@ class IdentityService {
);
}
// 🔽 [신규] 5. 소셜 로그인 성공 시 호출 (계정 연결)
// 5. 소셜 로그인 성공 시 호출 (계정 연결)
Future<UserSession> saveSocialLogin({
required String newUserId, // 서버가 발급한 마스터 계정 ID
required String newUserId,
required String newUserName,
required String newEmail,
required String provider, // "google" 또는 "apple"
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());
@@ -127,9 +128,8 @@ class IdentityService {
);
}
// 🔽 [신규] 6. 로그아웃 (게스트 계정으로 되돌리기)
// 6. 로그아웃 (게스트 계정으로 되돌리기)
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());
@@ -137,20 +137,40 @@ class IdentityService {
return await getUserSession();
}
// 7. 최대 레벨 가져오기
// 🔽 [수정] 7. 최대 레벨 가져오기 (gameType 분기)
Future<int> getMaxUnlockedLevel({String gameType = 'SUDOKU'}) async {
final key = gameType == 'SPIDER' ? _spiderMaxLevelKey : _sudokuMaxLevelKey;
String key;
switch (gameType) {
case 'SPIDER':
key = _spiderMaxLevelKey;
break;
case 'MATH_QUIZ': // 👈 [추가]
key = _mathQuizMaxLevelKey;
break;
default: // 'SUDOKU'
key = _sudokuMaxLevelKey;
}
String? levelString = await _storage.read(
key: key,
iOptions: _getIOSOptions(),
aOptions: _getAndroidOptions(),
);
return int.parse(levelString ?? '1'); // 기본값 1
return int.parse(levelString ?? '1');
}
// 8. 최대 레벨 저장하기
// 🔽 [수정] 8. 최대 레벨 저장하기 (gameType 분기)
Future<void> saveMaxUnlockedLevel(int level, {String gameType = 'SUDOKU'}) async {
final key = gameType == 'SPIDER' ? _spiderMaxLevelKey : _sudokuMaxLevelKey;
String key;
switch (gameType) {
case 'SPIDER':
key = _spiderMaxLevelKey;
break;
case 'MATH_QUIZ': // 👈 [추가]
key = _mathQuizMaxLevelKey;
break;
default: // 'SUDOKU'
key = _sudokuMaxLevelKey;
}
await _storage.write(
key: key,
value: level.toString(),
@@ -159,9 +179,19 @@ class IdentityService {
);
}
// 9. 마지막 랭킹 맵 가져오기
// 🔽 [수정] 9. 마지막 랭킹 맵 가져오기 (gameType 분기)
Future<Map<int, int>> getLastSavedRankMap({String gameType = 'SUDOKU'}) async {
final key = gameType == 'SPIDER' ? _spiderRankMapKey : _sudokuRankMapKey;
String key;
switch (gameType) {
case 'SPIDER':
key = _spiderRankMapKey;
break;
case 'MATH_QUIZ': // 👈 [추가]
key = _mathQuizRankMapKey;
break;
default: // 'SUDOKU'
key = _sudokuRankMapKey;
}
String? jsonString = await _storage.read(
key: key,
iOptions: _getIOSOptions(),
@@ -177,9 +207,19 @@ class IdentityService {
}
}
// 10. 마지막 랭킹 맵 저장하기
// 🔽 [수정] 10. 마지막 랭킹 맵 저장하기 (gameType 분기)
Future<void> saveLastRankMap(Map<int, int> rankMap, {String gameType = 'SUDOKU'}) async {
final key = gameType == 'SPIDER' ? _spiderRankMapKey : _sudokuRankMapKey;
String key;
switch (gameType) {
case 'SPIDER':
key = _spiderRankMapKey;
break;
case 'MATH_QUIZ': // 👈 [추가]
key = _mathQuizRankMapKey;
break;
default: // 'SUDOKU'
key = _sudokuRankMapKey;
}
final Map<String, int> stringKeyMap =
rankMap.map((key, value) => MapEntry(key.toString(), value));