This commit is contained in:
2025-12-15 18:18:17 +09:00
parent 03a7ed2ef2
commit 4c2c98de8a
216 changed files with 9831 additions and 725 deletions
@@ -0,0 +1,246 @@
import 'package:flutter/material.dart';
import 'package:provider/provider.dart';
import 'package:service_api/service_api.dart';
import 'package:feature_common/feature_common.dart';
// --- [Import] 각 게임별 패키지 ---
// 1. 스도쿠
import 'package:feature_game_sudoku/feature_game_sudoku.dart' as sudoku_pkg;
import 'package:feature_game_sudoku/models/game_level.dart' as sudoku_model;
import 'package:feature_game_sudoku/screens/game_screen.dart' as sudoku_screen;
// 2. 수학 퀴즈
import 'package:feature_game_mathquiz/feature_game_mathquiz.dart' as math_pkg;
import 'package:feature_game_mathquiz/models/math_quiz_difficulty.dart' as math_model;
import 'package:feature_game_mathquiz/screens/math_quiz_screen.dart' as math_screen;
import 'package:feature_game_mathquiz/controllers/math_quiz_controller.dart';
// 3. 순서 기억
import 'package:feature_game_sequence/feature_game_sequence.dart' as sequence_pkg;
import 'package:feature_game_sequence/models/sequence_models.dart' as sequence_model;
import 'package:feature_game_sequence/screens/sequence_game_screen.dart' as sequence_screen;
import 'package:feature_game_sequence/controllers/sequence_game_controller.dart';
// 4. 카드 뒤집기
import 'package:feature_game_cardflip/feature_game_cardflip.dart' as cardflip_pkg;
import 'package:feature_game_cardflip/models/cardflip_models.dart' as cardflip_model;
import 'package:feature_game_cardflip/screens/cardflip_game_screen.dart' as cardflip_screen;
import 'package:feature_game_cardflip/controllers/cardflip_controller.dart';
// 5. 색상 매칭
import 'package:feature_game_colormatch/feature_game_colormatch.dart' as colormatch_pkg;
import 'package:feature_game_colormatch/models/colormatch_models.dart' as colormatch_model;
import 'package:feature_game_colormatch/screens/colormatch_game_screen.dart' as colormatch_screen;
import 'package:feature_game_colormatch/controllers/colormatch_controller.dart';
// 6. 다른 그림 찾기
import 'package:feature_game_finddiff/feature_game_finddiff.dart' as finddiff_pkg;
import 'package:feature_game_finddiff/models/finddiff_models.dart' as finddiff_model;
import 'package:feature_game_finddiff/screens/finddiff_game_screen.dart' as finddiff_screen;
import 'package:feature_game_finddiff/controllers/finddiff_controller.dart';
import 'package:feature_game_schulte/feature_game_schulte.dart' as schulte_pkg; // ✅ 추가
// 8. [신규] 따라 그리기
import 'package:feature_game_tracing/feature_game_tracing.dart' as tracing_pkg;
import 'package:feature_game_read_aloud/feature_game_read_aloud.dart' as readAloud;
import 'package:feature_game_dictation/feature_game_dictation.dart' as dictation_pkg;
class DailyCourseController {
final BuildContext context;
final List<BrainGameType> course;
int _currentIndex = 0;
DailyCourseController(this.context, this.course);
// 1. 코스 시작
void start() {
if (course.isEmpty) return;
_currentIndex = 0;
_launchCurrentGame();
}
// 2. 다음 게임으로 이동
void _next() {
_currentIndex++;
if (_currentIndex < course.length) {
Future.delayed(const Duration(milliseconds: 300), () {
_launchCurrentGame();
});
} else {
_showCourseCompletedDialog();
}
}
// 3. 게임 실행 로직
void _launchCurrentGame() async {
final gameType = course[_currentIndex];
final session = context.read<SessionNotifier>().session;
final String userId = session?.userId ?? 'guest';
final String? userName = session?.userName;
Widget? gameScreen;
const int defaultLevelIndex = 1;
try {
switch (gameType) {
// --- 1. 스도쿠 ---
case BrainGameType.sudoku:
final level = sudoku_model.AppLevels.getLevel(defaultLevelIndex);
final puzzleService = PuzzleService();
final gameData = await puzzleService.startGame(level.levelIndex.toString());
gameScreen = sudoku_screen.GameScreen(
gameData: gameData,
themeName: "랜덤",
userId: userId,
userName: userName,
levelIndex: level.levelIndex,
onNextGame: _next,
);
break;
// --- 2. 수학 퀴즈 ---
case BrainGameType.mathQuiz:
final level = math_model.MathQuizDifficulties.getLevel(defaultLevelIndex);
final ctrl = MathQuizController();
ctrl.setUserInfo(userId, userName);
ctrl.startNewGame(level);
gameScreen = ChangeNotifierProvider<MathQuizController>.value(
value: ctrl,
child: math_screen.MathQuizScreen(
// onNextGame: _next, // MathQuizScreen에 onNextGame 추가 필요
),
);
break;
// --- 3. 순서 기억 ---
case BrainGameType.sequence:
final level = sequence_model.SequenceDifficulties.getLevel(defaultLevelIndex);
final ctrl = SequenceGameController();
ctrl.setUserInfo(userId, userName);
ctrl.startNewGame(level);
gameScreen = ChangeNotifierProvider<SequenceGameController>.value(
value: ctrl,
child: sequence_screen.SequenceGameScreen(
// onNextGame: _next,
),
);
break;
// --- 4. 카드 뒤집기 ---
case BrainGameType.cardFlip:
final level = cardflip_model.CardFlipDifficulties.getLevel(defaultLevelIndex);
final ctrl = CardFlipController();
ctrl.setUserInfo(userId, userName);
ctrl.startNewGame(level);
gameScreen = ChangeNotifierProvider<CardFlipController>.value(
value: ctrl,
child: cardflip_screen.CardFlipGameScreen(
// onNextGame: _next,
),
);
break;
// --- 5. 색상 매칭 ---
case BrainGameType.colorMatch:
final level = colormatch_model.ColorMatchDifficulties.getLevel(defaultLevelIndex);
final ctrl = ColorMatchController();
ctrl.setUserInfo(userId, userName);
ctrl.startNewGame(level);
gameScreen = ChangeNotifierProvider<ColorMatchController>.value(
value: ctrl,
child: colormatch_screen.ColorMatchGameScreen(
// onNextGame: _next,
),
);
break;
// --- 6. 다른 그림 찾기 ---
case BrainGameType.findDiff:
final level = finddiff_model.FindDiffDifficulties.getLevel(defaultLevelIndex);
final ctrl = FindDiffController();
ctrl.setUserInfo(userId, userName);
ctrl.startNewGame(level);
gameScreen = ChangeNotifierProvider<FindDiffController>.value(
value: ctrl,
child: finddiff_screen.FindDiffGameScreen(
// onNextGame: _next,
),
);
break;
// --- 7. 스파이더 ---
case BrainGameType.schulte:
// Lv1~3: 3x3, Lv4~6: 4x4 ... (화면 내부에서 처리)
gameScreen = schulte_pkg.SchulteGameScreen(
levelIndex: defaultLevelIndex,
onNextGame: _next,
);
break;
// --- 8. [신규] 따라 그리기 ---
case BrainGameType.tracing:
// TracingGame은 별도 컨트롤러 없이 로컬 상태로 구현됨
gameScreen = tracing_pkg.TracingGameScreen(
levelIndex: defaultLevelIndex, // 필요한 레벨 전달 (예: 1~6)
onNextGame: _next,
);
break;
// --- 9. [신규] 소리 내어 읽기 (추후 구현 시 추가) ---
case BrainGameType.readAloud:
gameScreen = readAloud.ReadAloudGameScreen(
levelIndex: defaultLevelIndex, // 필요한 레벨 전달 (예: 1~6)
onNextGame: _next,
);
break;
case BrainGameType.dictation:
gameScreen = dictation_pkg.DictationGameScreen(
levelIndex: 1, // 또는 사용자 레벨
onNextGame: _next,
);
break;
}
if (gameScreen != null) {
final route = MaterialPageRoute(builder: (_) => gameScreen!);
await Navigator.push(context, route);
} else {
debugPrint("게임 화면 생성 실패: $gameType");
_next();
}
} catch (e) {
debugPrint("게임 실행 중 오류 발생 ($gameType): $e");
_next();
}
}
void _showCourseCompletedDialog() {
showDialog(
context: context,
builder: (ctx) => AlertDialog(
title: const Text("🎉 훈련 완료!"),
content: const Text("오늘의 추천 코스를 모두 완수했습니다.\n두뇌가 한층 더 건강해졌습니다!"),
actions: [
ElevatedButton(
onPressed: () => Navigator.pop(ctx),
child: const Text("확인"),
)
],
),
);
}
}
@@ -1,5 +1,6 @@
/// A Calculator.
class Calculator {
/// Returns [value] plus 1.
int addOne(int value) => value + 1;
}
library feature_brain_trainer;
export 'screens/brain_training_home.dart';
export 'screens/assessment_screen.dart';
export 'screens/assessment_history_screen.dart';
export 'controllers/daily_course_controller.dart';
@@ -1,21 +0,0 @@
enum CognitiveArea {
memory, // 기억력
calculation, // 계산/논리력
attention, // 주의집중력
perception, // 시지각/공간지각력
}
// 게임별 훈련 영역 매핑
enum BrainGameType {
sequence(CognitiveArea.memory, '순서 기억'), // feature_game_sequence
cardFlip(CognitiveArea.memory, '카드 뒤집기'), // feature_game_cardflip
mathQuiz(CognitiveArea.calculation, '암산 퀴즈'), // feature_game_mathquiz
sudoku(CognitiveArea.calculation, '스도쿠'), // feature_game_sudoku
colorMatch(CognitiveArea.attention, '색상 매칭'), // feature_game_colormatch
findDiff(CognitiveArea.perception, '다른 그림 찾기'), // feature_game_finddiff
spider(CognitiveArea.perception, '스파이더 카드'); // feature_game_spider
final CognitiveArea area;
final String label;
const BrainGameType(this.area, this.label);
}
@@ -0,0 +1,310 @@
import 'package:flutter/material.dart';
import 'package:service_api/service_api.dart';
import 'package:intl/intl.dart';
import 'assessment_screen.dart';
class AssessmentHistoryScreen extends StatefulWidget {
const AssessmentHistoryScreen({super.key});
@override
State<AssessmentHistoryScreen> createState() => _AssessmentHistoryScreenState();
}
class _AssessmentHistoryScreenState extends State<AssessmentHistoryScreen> {
List<AssessmentRecord> _history = [];
bool _isLoading = true;
@override
void initState() {
super.initState();
_loadHistory();
}
Future<void> _loadHistory() async {
final identityService = IdentityService();
final list = await identityService.getAssessmentHistory();
list.sort((a, b) => a.date.compareTo(b.date));
if (mounted) {
setState(() {
_history = list;
_isLoading = false;
});
}
}
void _startNewAssessment() async {
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (_) => const AssessmentScreen()),
);
if (result == true) {
_loadHistory();
}
}
(String, Color) _getAreaStatus(int score) {
// 8점 만점 기준 (8문항)
// 6점 이상: 위험, 4~5점: 주의, 3점 이하: 양호
if (score >= 6) return ("위험", Colors.redAccent);
if (score >= 4) return ("주의", Colors.orange);
return ("양호", Colors.green);
}
String _getAreaName(CognitiveArea area) {
switch (area) {
case CognitiveArea.memory: return "기억력";
case CognitiveArea.perception: return "시지각/지남력";
case CognitiveArea.calculation: return "계산력/판단력";
case CognitiveArea.attention: return "주의력/집행기능";
case CognitiveArea.language: return "언어/구성 능력";
}
}
@override
Widget build(BuildContext context) {
final reversedList = _history.reversed.toList();
return Scaffold(
appBar: AppBar(
title: const Text('나의 두뇌 건강 변화'),
// 🔽 [수정] 컬러 충돌 방지를 위해 FilledButton.tonal 또는 IconButton 사용
actions: [
Padding(
padding: const EdgeInsets.only(right: 16.0),
child: FilledButton.tonalIcon(
onPressed: _startNewAssessment,
icon: const Icon(Icons.add_task),
label: const Text('새 진단'),
style: FilledButton.styleFrom(
// 테마의 Secondary 컬러를 사용하여 자연스럽게 어우러짐
// 글자색/아이콘색은 자동으로 맞춰짐
),
),
),
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: _history.isEmpty
? Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Icon(Icons.note_alt_outlined, size: 64, color: Colors.grey),
const SizedBox(height: 16),
const Text("아직 진단 기록이 없습니다.", style: TextStyle(fontSize: 18, color: Colors.grey)),
const SizedBox(height: 20),
ElevatedButton(
onPressed: _startNewAssessment,
child: const Text("첫 진단 시작하기"),
),
],
),
)
: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
children: [
// --- 1. 그래프 영역 ---
Card(
elevation: 4,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
color: Colors.white,
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
const Text(
"종합 위험도 변화 추이",
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
const Text(
"그래프가 낮을수록(점수가 낮을수록) 건강한 상태입니다.",
style: TextStyle(fontSize: 12, color: Colors.grey),
),
const SizedBox(height: 20),
SizedBox(
height: 200,
width: double.infinity,
child: CustomPaint(
painter: AssessmentGraphPainter(_history),
),
),
],
),
),
),
const SizedBox(height: 24),
const Align(
alignment: Alignment.centerLeft,
child: Text("상세 기록 (터치하여 상세 보기)", style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
),
const SizedBox(height: 10),
// --- 2. 리스트 영역 ---
Expanded(
child: ListView.builder(
itemCount: reversedList.length,
itemBuilder: (context, index) {
final record = reversedList[index];
final totalScore = record.scores.values.fold(0, (sum, val) => sum + val);
// 종합 판정 (32점 만점) -> 12점 이상 주의
final bool isRisky = totalScore >= 12;
final Color statusColor = isRisky ? Colors.redAccent : Colors.green;
final String statusText = isRisky ? "종합 주의" : "종합 양호";
return Card(
elevation: 2,
margin: const EdgeInsets.only(bottom: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: ExpansionTile(
leading: CircleAvatar(
backgroundColor: statusColor.withOpacity(0.1),
child: Icon(
isRisky ? Icons.priority_high : Icons.check,
color: statusColor
),
),
title: Text(
DateFormat('yyyy년 MM월 dd일 (E) HH:mm', 'ko_KR').format(record.date),
style: const TextStyle(fontWeight: FontWeight.bold),
),
subtitle: Row(
children: [
Text(statusText, style: TextStyle(color: statusColor, fontWeight: FontWeight.bold)),
const SizedBox(width: 8),
Text("(위험점수: $totalScore점)", style: const TextStyle(color: Colors.grey)),
],
),
children: [
const Divider(height: 1),
Padding(
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 16),
child: Column(
children: CognitiveArea.values.map((area) {
final int score = record.scores[area] ?? 0;
final (String label, Color color) = _getAreaStatus(score);
return Padding(
padding: const EdgeInsets.symmetric(vertical: 6),
child: Row(
children: [
SizedBox(
width: 120,
child: Text(_getAreaName(area), style: const TextStyle(fontSize: 15)),
),
Expanded(
child: ClipRRect(
borderRadius: BorderRadius.circular(4),
child: LinearProgressIndicator(
value: score / 8.0, // 8점 만점 기준
backgroundColor: Colors.grey[200],
valueColor: AlwaysStoppedAnimation<Color>(color),
minHeight: 8,
),
),
),
const SizedBox(width: 12),
SizedBox(
width: 50,
child: Text(
label,
style: TextStyle(
color: color,
fontWeight: FontWeight.bold,
fontSize: 14
),
textAlign: TextAlign.end,
),
),
],
),
);
}).toList(),
),
),
],
),
);
},
),
),
],
),
),
);
}
}
class AssessmentGraphPainter extends CustomPainter {
final List<AssessmentRecord> history;
AssessmentGraphPainter(this.history);
@override
void paint(Canvas canvas, Size size) {
if (history.isEmpty) return;
const double padding = 10.0;
final double graphWidth = size.width - padding * 2;
final double graphHeight = size.height - padding * 2;
final data = history.length > 7 ? history.sublist(history.length - 7) : history;
final paintLine = Paint()
..color = Colors.blueAccent
..strokeWidth = 3
..style = PaintingStyle.stroke
..strokeCap = StrokeCap.round;
final paintDot = Paint()
..color = Colors.redAccent
..style = PaintingStyle.fill;
final paintGrid = Paint()
..color = Colors.grey.withOpacity(0.2)
..strokeWidth = 1;
// Y축 가이드라인 (최대 32점 기준)
const double maxY = 32.0;
for (int i = 0; i <= 4; i++) {
double y = padding + graphHeight - (i * 8 / maxY) * graphHeight; // 8점 단위
canvas.drawLine(Offset(padding, y), Offset(padding + graphWidth, y), paintGrid);
}
final double stepX = data.length > 1 ? graphWidth / (data.length - 1) : graphWidth / 2;
Path path = Path();
List<Offset> points = [];
for (int i = 0; i < data.length; i++) {
int totalScore = data[i].scores.values.fold(0, (sum, val) => sum + val);
double x = padding + (i * stepX);
double y = padding + graphHeight - (totalScore / maxY) * graphHeight;
if (data.length == 1) x = size.width / 2;
points.add(Offset(x, y));
if (i == 0) {
path.moveTo(x, y);
} else {
path.lineTo(x, y);
}
}
if (data.length > 1) canvas.drawPath(path, paintLine);
for (var point in points) {
canvas.drawCircle(point, 5, paintDot);
}
}
@override
bool shouldRepaint(covariant CustomPainter oldDelegate) => true;
}
@@ -0,0 +1,228 @@
import 'dart:math';
import 'package:flutter/material.dart';
import 'package:service_api/service_api.dart';
import 'package:service_api/models/assessment_data.dart';
class AssessmentScreen extends StatefulWidget {
const AssessmentScreen({super.key});
@override
State<AssessmentScreen> createState() => _AssessmentScreenState();
}
class _AssessmentScreenState extends State<AssessmentScreen> {
final Map<String, bool> _answers = {};
final PageController _pageController = PageController();
int _currentPage = 0;
late final Map<CognitiveArea, List<AssessmentQuestion>> _dailyQuestions;
late final List<CognitiveArea> _areaKeys;
@override
void initState() {
super.initState();
_dailyQuestions = _generateDailyQuestions();
_areaKeys = _dailyQuestions.keys.toList();
}
/// [핵심 로직] 전체 문제 풀에서 영역별로 8개씩 랜덤 추출 (총 32문항)
Map<CognitiveArea, List<AssessmentQuestion>> _generateDailyQuestions() {
final random = Random();
Map<CognitiveArea, List<AssessmentQuestion>> grouped = {};
// 1. 전체 질문 분류
Map<CognitiveArea, List<AssessmentQuestion>> pool = {};
for (var q in rawAssessmentQuestions) {
if (!pool.containsKey(q.area)) pool[q.area] = [];
pool[q.area]!.add(q);
}
// 2. 각 영역에서 랜덤하게 8문제씩 추출 (총 32문제)
// (데이터가 부족할 경우를 대비해 take 사용)
pool.forEach((area, questions) {
var shuffled = List<AssessmentQuestion>.from(questions)..shuffle(random);
grouped[area] = shuffled.take(8).toList(); // 👈 8개로 설정
});
return grouped;
}
void _submitResult() async {
// 1. 점수 계산
Map<CognitiveArea, int> scores = {};
int totalYesCount = 0;
_dailyQuestions.forEach((area, questions) {
int areaScore = 0;
for (var q in questions) {
if (_answers[q.id] == true) {
areaScore++;
totalYesCount++;
}
}
scores[area] = areaScore;
});
// 2. 히스토리 저장
final identityService = IdentityService();
await identityService.saveAssessmentResult(scores);
if (!mounted) return;
// 3. 결과 알림 (기준: 32문항 중 12개 이상이면 주의)
String message = "진단 결과가 기록되었습니다.";
if (totalYesCount >= 12) {
message += "\n\n주의가 필요한 항목이 다수 확인되었습니다.\n($totalYesCount/32개 해당)\n꾸준한 훈련을 권장합니다.";
} else {
message += "\n\n현재 양호한 상태입니다.\n이 상태를 유지하기 위해 매일 훈련하세요!";
}
showDialog(
context: context,
barrierDismissible: false,
builder: (ctx) => AlertDialog(
title: const Text('진단 완료'),
content: Text(message),
actions: [
TextButton(
onPressed: () {
Navigator.pop(ctx);
Navigator.pop(context, true);
},
child: const Text('확인'),
),
],
),
);
}
String _getAreaTitle(CognitiveArea area) {
switch (area) {
case CognitiveArea.memory: return "1. 기억력 체크";
case CognitiveArea.perception: return "2. 시공간/지남력 체크";
case CognitiveArea.calculation: return "3. 계산력/판단력 체크";
case CognitiveArea.attention: return "4. 주의력/집행기능 체크";
case CognitiveArea.language: return "5. 언어/구성 능력";
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(
title: const Text('자가 진단 체크리스트'),
centerTitle: true,
bottom: PreferredSize(
preferredSize: const Size.fromHeight(4.0),
child: LinearProgressIndicator(
value: (_currentPage + 1) / _areaKeys.length,
backgroundColor: Colors.grey[200],
valueColor: AlwaysStoppedAnimation<Color>(Theme.of(context).primaryColor),
),
),
),
body: Column(
children: [
Padding(
padding: const EdgeInsets.all(16.0),
child: Text(
_getAreaTitle(_areaKeys[_currentPage]),
style: const TextStyle(fontSize: 22, fontWeight: FontWeight.bold),
),
),
Expanded(
child: PageView.builder(
controller: _pageController,
physics: const NeverScrollableScrollPhysics(),
itemCount: _areaKeys.length,
onPageChanged: (idx) {
setState(() { _currentPage = idx; });
},
itemBuilder: (context, index) {
final area = _areaKeys[index];
final questions = _dailyQuestions[area]!;
return ListView.separated(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 8),
itemCount: questions.length,
separatorBuilder: (c, i) => const Divider(height: 1),
itemBuilder: (context, qIndex) {
final q = questions[qIndex];
final bool isChecked = _answers[q.id] ?? false;
return CheckboxListTile(
title: Text(
q.text,
style: TextStyle(
fontSize: 16,
color: isChecked ? Colors.black : Colors.grey[800],
fontWeight: isChecked ? FontWeight.w600 : FontWeight.normal,
),
),
value: isChecked,
activeColor: Colors.redAccent,
onChanged: (val) {
setState(() { _answers[q.id] = val!; });
},
);
},
);
},
),
),
Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
if (_currentPage > 0)
ElevatedButton.icon(
onPressed: () => _pageController.previousPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut
),
icon: const Icon(Icons.arrow_back),
label: const Text('이전'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
)
else
const SizedBox(width: 90),
Text(
"${_currentPage + 1} / ${_areaKeys.length}",
style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16),
),
if (_currentPage < _areaKeys.length - 1)
ElevatedButton.icon(
onPressed: () => _pageController.nextPage(
duration: const Duration(milliseconds: 300),
curve: Curves.easeInOut
),
icon: const Icon(Icons.arrow_forward),
label: const Text('다음'),
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
)
else
ElevatedButton.icon(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.blueAccent,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
onPressed: _submitResult,
icon: const Icon(Icons.check),
label: const Text('결과 제출'),
),
],
),
),
],
),
);
}
}
@@ -3,18 +3,120 @@ import 'package:provider/provider.dart';
import 'package:feature_common/feature_common.dart';
import 'package:service_api/service_api.dart';
// 각 게임 패키지 import
import 'assessment_screen.dart';
import 'assessment_history_screen.dart';
import '../controllers/daily_course_controller.dart';
// 각 게임 패키지 로비 import
import 'package:feature_game_sudoku/feature_game_sudoku.dart';
import 'package:feature_game_mathquiz/feature_game_mathquiz.dart';
// ... (나머지 게임들)
import 'package:feature_game_sequence/feature_game_sequence.dart';
import 'package:feature_game_cardflip/feature_game_cardflip.dart';
import 'package:feature_game_colormatch/feature_game_colormatch.dart';
import 'package:feature_game_finddiff/feature_game_finddiff.dart';
import 'package:feature_game_schulte/feature_game_schulte.dart';
import 'package:feature_game_read_aloud/feature_game_read_aloud.dart';
import 'package:feature_game_dictation/feature_game_dictation.dart';
class BrainTrainingHome extends StatelessWidget {
// 🔽 [신규] 따라 그리기 패키지 import
import 'package:feature_game_tracing/feature_game_tracing.dart';
class BrainTrainingHome extends StatefulWidget {
const BrainTrainingHome({super.key});
@override
State<BrainTrainingHome> createState() => _BrainTrainingHomeState();
}
class _BrainTrainingHomeState extends State<BrainTrainingHome> {
bool _hasAssessment = false;
@override
void initState() {
super.initState();
_checkAssessmentStatus();
}
Future<void> _checkAssessmentStatus() async {
final identityService = context.read<IdentityService>();
final scores = await identityService.getCognitiveScores();
if (mounted) {
setState(() {
_hasAssessment = (scores != null && scores.isNotEmpty);
});
}
}
void _handleCheckupTap() {
if (_hasAssessment) {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const AssessmentHistoryScreen()),
).then((_) => _checkAssessmentStatus());
} else {
_openNewAssessment();
}
}
void _openNewAssessment() async {
final result = await Navigator.push(
context,
MaterialPageRoute(builder: (_) => const AssessmentScreen()),
);
if (result == true) {
_checkAssessmentStatus();
}
}
void _startDailyCourse() async {
final identityService = context.read<IdentityService>();
final scores = await identityService.getCognitiveScores();
if (scores == null || scores.isEmpty) {
if (!mounted) return;
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('먼저 두뇌 건강 체크를 진행해 주세요!')),
);
_openNewAssessment();
return;
}
final trainer = BrainTrainingService();
final recommendedCourse = trainer.recommendGames(scores);
if (!mounted) return;
final controller = DailyCourseController(context, recommendedCourse);
controller.start();
}
@override
Widget build(BuildContext context) {
return Scaffold(
appBar: AppBar(title: const Text('두뇌 건강 지킴이')),
appBar: AppBar(
title: const Text('두뇌 건강 지킴이'),
centerTitle: false,
actions: [
IconButton(
icon: const Icon(Icons.show_chart_rounded),
tooltip: '진단 기록 보기',
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const AssessmentHistoryScreen())
).then((_) => _checkAssessmentStatus());
},
),
IconButton(
icon: const Icon(Icons.settings),
onPressed: () => Navigator.push(
context,
MaterialPageRoute(builder: (_) => const SettingsScreen())
),
)
],
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
@@ -25,14 +127,14 @@ class BrainTrainingHome extends StatelessWidget {
const SizedBox(height: 24),
// 2. 오늘의 추천 코스 (자동 믹스 게임)
// 2. 오늘의 추천 코스
const Text('오늘의 추천 훈련', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 10),
_buildDailyTrainingButton(context),
const SizedBox(height: 24),
// 3. 게임 아케이드 (자유 선택)
// 3. 게임 아케이드
const Text('전체 게임', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 10),
_buildGameGrid(context),
@@ -45,32 +147,54 @@ class BrainTrainingHome extends StatelessWidget {
Widget _buildCheckupCard(BuildContext context) {
return Card(
color: Colors.indigo.shade50,
elevation: 0,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: BorderSide(color: Colors.indigo.shade100),
),
child: ListTile(
leading: const Icon(Icons.health_and_safety, size: 40, color: Colors.indigo),
title: const Text('나의 두뇌 건강 체크'),
subtitle: const Text('간단한 질문으로 현재 상태를 확인하고\n맞춤형 훈련을 추천받으세요.'),
trailing: const Icon(Icons.arrow_forward_ios),
onTap: () {
// TODO: 체크리스트 화면으로 이동
// Navigator.push(context, MaterialPageRoute(builder: (_) => AssessmentScreen()));
},
contentPadding: const EdgeInsets.all(16),
leading: Container(
padding: const EdgeInsets.all(12),
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(12),
),
child: const Icon(Icons.health_and_safety, size: 32, color: Colors.indigo),
),
title: Text(_hasAssessment ? '나의 두뇌 건강 리포트' : '나의 두뇌 건강 체크', style: const TextStyle(fontWeight: FontWeight.bold)),
subtitle: Text(_hasAssessment
? '최근 진단 결과를 확인하고 변화를 추적하세요.'
: '간단한 질문으로 상태를 확인하고\n맞춤형 훈련을 추천받으세요.'
),
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
onTap: _handleCheckupTap,
),
);
}
Widget _buildDailyTrainingButton(BuildContext context) {
return ElevatedButton.icon(
return ElevatedButton(
style: ElevatedButton.styleFrom(
padding: const EdgeInsets.all(20),
padding: const EdgeInsets.symmetric(vertical: 24),
backgroundColor: Colors.orange,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
elevation: 4,
),
onPressed: _startDailyCourse,
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: const [
Icon(Icons.play_circle_fill, size: 40),
SizedBox(width: 16),
Text(
'오늘의 코스 시작하기\n(맞춤형 3종 세트)',
textAlign: TextAlign.start,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)
),
],
),
icon: const Icon(Icons.play_circle_fill, size: 32),
label: const Text('오늘의 코스 시작하기\n(맞춤형 3종 세트)', textAlign: TextAlign.center, style: TextStyle(fontSize: 18)),
onPressed: () {
// TODO: Assessment 결과에 따라 선정된 게임들을 순차적으로 실행하는 로직
// 예: Sequence -> Math -> ColorMatch 순서로 실행되는 별도 Flow 화면으로 이동
},
);
}
@@ -80,7 +204,13 @@ class BrainTrainingHome extends StatelessWidget {
{'name': '스도쿠', 'icon': Icons.grid_3x3, 'dest': const SudokuLobbyScreen()},
{'name': '계산 퀴즈', 'icon': Icons.calculate, 'dest': const MathQuizLobbyScreen()},
{'name': '순서 기억', 'icon': Icons.onetwothree, 'dest': const SequenceLobbyScreen()},
// ... 나머지 게임 추가
{'name': '카드 뒤집기', 'icon': Icons.flip, 'dest': const CardFlipLobbyScreen()},
{'name': '색상 매칭', 'icon': Icons.palette, 'dest': const ColorMatchLobbyScreen()},
{'name': '다른 그림', 'icon': Icons.image_search, 'dest': const FindDiffLobbyScreen()},
{'name': '숫자 찾기', 'icon': Icons.looks_one, 'dest': const SchulteLobbyScreen()},
{'name': '따라 그리기', 'icon': Icons.gesture, 'dest': const TracingLobbyScreen()},
{'name': '읽고 말하기', 'icon': Icons.record_voice_over, 'dest': const ReadAloudLobbyScreen()},
{'name': '받아쓰기', 'icon': Icons.keyboard, 'dest': const DictationLobbyScreen()},
];
return GridView.builder(
@@ -88,9 +218,9 @@ class BrainTrainingHome extends StatelessWidget {
physics: const NeverScrollableScrollPhysics(),
gridDelegate: const SliverGridDelegateWithFixedCrossAxisCount(
crossAxisCount: 2,
childAspectRatio: 1.5,
crossAxisSpacing: 10,
mainAxisSpacing: 10,
childAspectRatio: 1.3,
crossAxisSpacing: 12,
mainAxisSpacing: 12,
),
itemCount: games.length,
itemBuilder: (context, index) {
@@ -98,14 +228,21 @@ class BrainTrainingHome extends StatelessWidget {
onTap: () {
Navigator.push(context, MaterialPageRoute(builder: (_) => games[index]['dest'] as Widget));
},
child: Card(
elevation: 2,
child: Container(
decoration: BoxDecoration(
color: Colors.white,
borderRadius: BorderRadius.circular(16),
border: Border.all(color: Colors.grey.shade200),
boxShadow: [
BoxShadow(color: Colors.grey.shade100, blurRadius: 4, offset: const Offset(0, 2))
],
),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Icon(games[index]['icon'] as IconData, size: 36, color: Colors.blueGrey),
const SizedBox(height: 8),
Text(games[index]['name'] as String, style: const TextStyle(fontWeight: FontWeight.bold)),
Icon(games[index]['icon'] as IconData, size: 40, color: Colors.blueGrey),
const SizedBox(height: 12),
Text(games[index]['name'] as String, style: const TextStyle(fontWeight: FontWeight.bold, fontSize: 16)),
],
),
),
+42 -42
View File
@@ -1,54 +1,54 @@
name: feature_brain_trainer
description: "A new Flutter package project."
description: Brain training features including assessment, recommendation logic, and daily course management.
version: 0.0.1
homepage:
publish_to: 'none'
resolution: workspace
environment:
sdk: ^3.9.2
flutter: ">=1.17.0"
sdk: '^3.9.2'
flutter: '>=3.10.0'
dependencies:
flutter:
sdk: flutter
# 1. 상태 관리
provider: ^6.0.0
# 2. 공통 서비스 (IdentityService, BrainTrainingService, 모델 등)
service_api:
path: ../service_api
# 3. 공통 UI (CommonGameShell, 버튼 스타일 등)
feature_common:
path: ../feature_common
feature_game_sudoku:
path: ../feature_game_sudoku
feature_game_cardflip:
path: ../feature_game_cardflip
feature_game_colormatch:
path: ../feature_game_colormatch
feature_game_schulte:
path: ../feature_game_schulte
feature_game_sequence:
path: ../feature_game_sequence
feature_game_finddiff:
path: ../feature_game_finddiff
feature_game_mathquiz:
path: ../feature_game_mathquiz
# ... 나머지 게임 패키지들도 추가
feature_game_tracing:
path: ../feature_game_tracing
feature_game_read_aloud:
path: ../feature_game_read_aloud
feature_game_dictation:
path: ../feature_game_dictation
# (선택) 날짜 포맷팅 등이 필요하면 추가
intl: ^0.18.0
dev_dependencies:
flutter_test:
sdk: flutter
flutter_lints: ^5.0.0
# For information on the generic Dart part of this file, see the
# following page: https://dart.dev/tools/pub/pubspec
# The following section is specific to Flutter packages.
flutter:
# To add assets to your package, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
#
# For details regarding assets in packages, see
# https://flutter.dev/to/asset-from-package
#
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# To add custom fonts to your package, add a fonts section here,
# in this "flutter" section. Each entry in this list should have a
# "family" key with the font family name, and a "fonts" key with a
# list giving the asset and other descriptors for the font. For
# example:
# fonts:
# - family: Schyler
# fonts:
# - asset: fonts/Schyler-Regular.ttf
# - asset: fonts/Schyler-Italic.ttf
# style: italic
# - family: Trajan Pro
# fonts:
# - asset: fonts/TrajanPro.ttf
# - asset: fonts/TrajanPro_Bold.ttf
# weight: 700
#
# For details regarding fonts in packages, see
# https://flutter.dev/to/font-from-package
lints: ^3.0.0