...
This commit is contained in:
@@ -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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
||||
Reference in New Issue
Block a user