...
This commit is contained in:
@@ -0,0 +1,4 @@
|
||||
library feature_game_read_aloud;
|
||||
|
||||
export 'screens/read_aloud_game_screen.dart';
|
||||
export 'screens/read_aloud_lobby_screen.dart'; // 추가
|
||||
@@ -0,0 +1,32 @@
|
||||
import 'dart:math';
|
||||
|
||||
class ReadAloudDifficultyRepository {
|
||||
static final Random _random = Random();
|
||||
|
||||
static final Map<int, List<String>> _levels = {
|
||||
// Lv 1: 2~3글자 단어 (기초)
|
||||
1: ['사과', '포도', '모자', '구두', '바지', '치마', '우산', '가방', '시계', '안경'],
|
||||
|
||||
// Lv 2: 4글자 이상 단어 (일상)
|
||||
2: ['텔레비전', '냉장고', '세탁기', '청소기', '선풍기', '자동차', '비행기', '자전거', '지하철'],
|
||||
|
||||
// Lv 3: 간단한 문장 (인사/안부)
|
||||
3: ['안녕하세요', '반갑습니다', '감사합니다', '잘 지내세요', '식사 하셨어요', '날씨가 좋네요'],
|
||||
|
||||
// Lv 4: 띄어쓰기가 있는 문장
|
||||
4: ['오늘 점심은 무엇인가요', '산책하기 좋은 날씨입니다', '건강이 최고입니다', '매일 운동을 합니다'],
|
||||
|
||||
// Lv 5: 속담 / 격언 (인지 훈련)
|
||||
5: ['가는 말이 고와야 오는 말이 곱다', '낮말은 새가 듣고 밤말은 쥐가 듣는다', '돌다리도 두들겨 보고 건너라', '발 없는 말이 천 리 간다'],
|
||||
|
||||
// Lv 6: 긴 문장 (뉴스/정보)
|
||||
6: ['규칙적인 식사와 운동은 건강에 좋습니다', '충분한 수분 섭취는 기억력에 도움이 됩니다', '잠을 푹 자는 것이 보약입니다'],
|
||||
};
|
||||
|
||||
static String getSentence(int level) {
|
||||
// 범위 보정 (1~6)
|
||||
int targetLevel = level.clamp(1, 6);
|
||||
final list = _levels[targetLevel]!;
|
||||
return list[_random.nextInt(list.length)];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,259 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'package:speech_to_text/speech_to_text.dart' as stt;
|
||||
import 'package:feature_common/feature_common.dart';
|
||||
import '../models/read_aloud_difficulty.dart';
|
||||
|
||||
class ReadAloudGameScreen extends BaseGameScreen {
|
||||
final int levelIndex;
|
||||
|
||||
const ReadAloudGameScreen({
|
||||
super.key,
|
||||
super.onNextGame,
|
||||
this.levelIndex = 1,
|
||||
});
|
||||
|
||||
@override
|
||||
State<ReadAloudGameScreen> createState() => _ReadAloudGameScreenState();
|
||||
}
|
||||
|
||||
class _ReadAloudGameScreenState extends BaseGameScreenState<ReadAloudGameScreen> {
|
||||
late stt.SpeechToText _speech;
|
||||
|
||||
// 상태 변수
|
||||
bool _isInitialized = false;
|
||||
bool _isListening = false;
|
||||
String _textRecognized = "";
|
||||
String _targetSentence = "";
|
||||
|
||||
// 라운드 관리
|
||||
int _currentRound = 1;
|
||||
int _totalRounds = 1;
|
||||
|
||||
@override
|
||||
void initState() {
|
||||
super.initState();
|
||||
_speech = stt.SpeechToText();
|
||||
|
||||
// 1. 라운드 설정 및 첫 문제 로드
|
||||
_calculateTotalRounds();
|
||||
_loadNewProblem();
|
||||
|
||||
// 2. STT 초기화
|
||||
_initSpeechState();
|
||||
}
|
||||
|
||||
void _calculateTotalRounds() {
|
||||
if (widget.levelIndex <= 3) {
|
||||
_totalRounds = 5; // Lv 1~3: 5문제
|
||||
} else {
|
||||
_totalRounds = 3; // Lv 4~6: 3문제
|
||||
}
|
||||
}
|
||||
|
||||
void _loadNewProblem() {
|
||||
setState(() {
|
||||
// 해당 레벨의 랜덤 문장 가져오기
|
||||
_targetSentence = ReadAloudDifficultyRepository.getSentence(widget.levelIndex);
|
||||
_textRecognized = "";
|
||||
_isListening = false;
|
||||
});
|
||||
_speech.stop(); // 이전 듣기 중단
|
||||
}
|
||||
|
||||
void _initSpeechState() async {
|
||||
try {
|
||||
bool available = await _speech.initialize(
|
||||
onStatus: (status) {
|
||||
if (status == 'done' || status == 'notListening') {
|
||||
if (mounted) setState(() => _isListening = false);
|
||||
}
|
||||
},
|
||||
onError: (errorNotification) {
|
||||
debugPrint('STT Error: $errorNotification');
|
||||
if (mounted) {
|
||||
setState(() => _isListening = false);
|
||||
// 에러 발생 시 사용자에게 알림 (선택 사항)
|
||||
}
|
||||
},
|
||||
);
|
||||
|
||||
if (mounted) {
|
||||
setState(() => _isInitialized = available);
|
||||
}
|
||||
} catch (e) {
|
||||
debugPrint("STT Initialize Failed: $e");
|
||||
}
|
||||
}
|
||||
|
||||
void _toggleListening() {
|
||||
if (!_isInitialized) {
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
const SnackBar(content: Text('마이크 권한을 확인하거나 초기화 중입니다.')),
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (_isListening) {
|
||||
_speech.stop();
|
||||
setState(() => _isListening = false);
|
||||
} else {
|
||||
setState(() {
|
||||
_isListening = true;
|
||||
_textRecognized = "";
|
||||
});
|
||||
|
||||
_speech.listen(
|
||||
onResult: (result) {
|
||||
setState(() {
|
||||
_textRecognized = result.recognizedWords;
|
||||
});
|
||||
_checkSuccess();
|
||||
},
|
||||
localeId: 'ko_KR',
|
||||
listenFor: const Duration(seconds: 15),
|
||||
pauseFor: const Duration(seconds: 3),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
void _checkSuccess() {
|
||||
final String cleanTarget = _targetSentence.replaceAll(' ', '');
|
||||
final String cleanInput = _textRecognized.replaceAll(' ', '');
|
||||
|
||||
if (cleanInput.contains(cleanTarget)) {
|
||||
_speech.stop();
|
||||
setState(() => _isListening = false);
|
||||
_handleRoundCompletion(); // 라운드 완료 처리
|
||||
}
|
||||
}
|
||||
|
||||
// 🔽 [수정] 성공 시 라운드 체크
|
||||
void _handleRoundCompletion() {
|
||||
if (_currentRound < _totalRounds) {
|
||||
// 1. 다음 라운드로 이동
|
||||
ScaffoldMessenger.of(context).showSnackBar(
|
||||
SnackBar(
|
||||
content: Text("정확합니다! 다음 문제로 넘어갑니다. (${_currentRound + 1}/$_totalRounds)"),
|
||||
duration: const Duration(milliseconds: 1000),
|
||||
behavior: SnackBarBehavior.floating,
|
||||
),
|
||||
);
|
||||
|
||||
setState(() {
|
||||
_currentRound++;
|
||||
});
|
||||
|
||||
// 자연스러운 전환을 위해 잠시 대기
|
||||
Future.delayed(const Duration(milliseconds: 1000), () {
|
||||
if (mounted) _loadNewProblem();
|
||||
});
|
||||
|
||||
} else {
|
||||
// 2. 모든 라운드 종료 -> 게임 클리어
|
||||
showCommonGameCompletion(
|
||||
GameResultArgs(
|
||||
gameType: 'READ_ALOUD',
|
||||
contextId: 'Lv${widget.levelIndex}_Final',
|
||||
primaryScore: 100,
|
||||
userId: 'user',
|
||||
scoreFormatter: (s, _) => "훈련 완료!",
|
||||
onProgressSave: (_) async {},
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(
|
||||
// 진행 상황 표시
|
||||
title: Text('소리 내어 읽기 ($_currentRound/$_totalRounds)'),
|
||||
),
|
||||
floatingActionButtonLocation: FloatingActionButtonLocation.centerFloat,
|
||||
floatingActionButton: SizedBox(
|
||||
width: 80,
|
||||
height: 80,
|
||||
child: FloatingActionButton(
|
||||
onPressed: _toggleListening,
|
||||
backgroundColor: _isListening ? Colors.redAccent : Colors.blueAccent,
|
||||
elevation: 10,
|
||||
child: Icon(
|
||||
_isListening ? Icons.mic : Icons.mic_none,
|
||||
size: 40,
|
||||
),
|
||||
),
|
||||
),
|
||||
body: SingleChildScrollView(
|
||||
child: Padding(
|
||||
padding: const EdgeInsets.symmetric(horizontal: 24.0, vertical: 40.0),
|
||||
child: Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.center,
|
||||
children: [
|
||||
const Text(
|
||||
"마이크 버튼을 누르고\n아래 글자를 큰 소리로 읽으세요.",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 18, color: Colors.grey),
|
||||
),
|
||||
const SizedBox(height: 40),
|
||||
|
||||
// 목표 텍스트 카드
|
||||
Container(
|
||||
width: double.infinity,
|
||||
padding: const EdgeInsets.all(30),
|
||||
decoration: BoxDecoration(
|
||||
color: Colors.white,
|
||||
borderRadius: BorderRadius.circular(20),
|
||||
border: Border.all(color: Colors.blueAccent.withOpacity(0.3)),
|
||||
boxShadow: [
|
||||
BoxShadow(
|
||||
color: Colors.grey.withOpacity(0.1),
|
||||
blurRadius: 10,
|
||||
offset: const Offset(0, 5),
|
||||
)
|
||||
],
|
||||
),
|
||||
child: Text(
|
||||
_targetSentence,
|
||||
textAlign: TextAlign.center,
|
||||
style: const TextStyle(
|
||||
fontSize: 32,
|
||||
fontWeight: FontWeight.bold,
|
||||
color: Colors.black87
|
||||
),
|
||||
),
|
||||
),
|
||||
|
||||
const SizedBox(height: 50),
|
||||
|
||||
if (_isListening)
|
||||
const Text("듣고 있어요...", style: TextStyle(color: Colors.redAccent, fontWeight: FontWeight.bold)),
|
||||
|
||||
const SizedBox(height: 10),
|
||||
|
||||
Text(
|
||||
_textRecognized.isEmpty ? "..." : _textRecognized,
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(
|
||||
fontSize: 24,
|
||||
color: _isListening ? Colors.black54 : Colors.blueGrey,
|
||||
fontWeight: FontWeight.w500
|
||||
),
|
||||
),
|
||||
|
||||
// 하단 안내 메시지
|
||||
const SizedBox(height: 50),
|
||||
if (_currentRound < _totalRounds)
|
||||
Text(
|
||||
"총 $_totalRounds문제 중 $_currentRound번째 문제입니다.",
|
||||
style: TextStyle(color: Colors.grey.shade400),
|
||||
),
|
||||
|
||||
const SizedBox(height: 100),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
import 'package:flutter/material.dart';
|
||||
import 'read_aloud_game_screen.dart';
|
||||
|
||||
class ReadAloudLobbyScreen extends StatelessWidget {
|
||||
const ReadAloudLobbyScreen({super.key});
|
||||
|
||||
@override
|
||||
Widget build(BuildContext context) {
|
||||
return Scaffold(
|
||||
appBar: AppBar(title: const Text('읽고 말하기')),
|
||||
body: Padding(
|
||||
padding: const EdgeInsets.all(16.0),
|
||||
child: Column(
|
||||
children: [
|
||||
const Icon(Icons.record_voice_over, size: 80, color: Colors.green),
|
||||
const SizedBox(height: 16),
|
||||
const Text(
|
||||
"화면에 나오는 단어나 문장을\n큰 소리로 읽어보세요.",
|
||||
textAlign: TextAlign.center,
|
||||
style: TextStyle(fontSize: 16),
|
||||
),
|
||||
const SizedBox(height: 32),
|
||||
Expanded(
|
||||
child: ListView.separated(
|
||||
itemCount: 6,
|
||||
separatorBuilder: (c, i) => const SizedBox(height: 12),
|
||||
itemBuilder: (context, index) {
|
||||
final level = index + 1;
|
||||
return ListTile(
|
||||
tileColor: Colors.white,
|
||||
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12), side: BorderSide(color: Colors.grey.shade300)),
|
||||
leading: CircleAvatar(child: Text("$level"), backgroundColor: Colors.green.shade100, foregroundColor: Colors.green),
|
||||
title: Text("레벨 $level"),
|
||||
subtitle: Text(_getLevelDesc(level)),
|
||||
trailing: const Icon(Icons.arrow_forward_ios, size: 16),
|
||||
onTap: () {
|
||||
Navigator.push(context, MaterialPageRoute(builder: (_) => ReadAloudGameScreen(levelIndex: level)));
|
||||
},
|
||||
);
|
||||
},
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
String _getLevelDesc(int level) {
|
||||
if (level <= 2) return "단어 읽기";
|
||||
if (level <= 4) return "짧은 문장 읽기";
|
||||
return "긴 문장 / 속담 읽기";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user