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,31 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
migrate_working_dir/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
# Libraries should not include pubspec.lock, per https://dart.dev/guides/libraries/private-files#pubspeclock.
/pubspec.lock
**/doc/api/
.dart_tool/
.flutter-plugins-dependencies
/build/
/coverage/
@@ -0,0 +1,10 @@
# This file tracks properties of this Flutter project.
# Used by Flutter tool to assess capabilities and perform upgrades etc.
#
# This file should be version controlled and should not be manually edited.
version:
revision: "adc901062556672b4138e18a4dc62a4be8f4b3c2"
channel: "stable"
project_type: package
@@ -0,0 +1,3 @@
## 0.0.1
* TODO: Describe initial release.
+1
View File
@@ -0,0 +1 @@
TODO: Add your license here.
@@ -0,0 +1,39 @@
<!--
This README describes the package. If you publish this package to pub.dev,
this README's contents appear on the landing page for your package.
For information about how to write a good package README, see the guide for
[writing package pages](https://dart.dev/tools/pub/writing-package-pages).
For general information about developing packages, see the Dart guide for
[creating packages](https://dart.dev/guides/libraries/create-packages)
and the Flutter guide for
[developing packages and plugins](https://flutter.dev/to/develop-packages).
-->
TODO: Put a short description of the package here that helps potential users
know whether this package might be useful for them.
## Features
TODO: List what your package can do. Maybe include images, gifs, or videos.
## Getting started
TODO: List prerequisites and provide or point to information on how to
start using the package.
## Usage
TODO: Include short and useful examples for package users. Add longer examples
to `/example` folder.
```dart
const like = 'sample';
```
## Additional information
TODO: Tell users more about the package: where to find more information, how to
contribute to the package, how to file issues, what response they can expect
from the package authors, and more.
@@ -0,0 +1,4 @@
include: package:flutter_lints/flutter.yaml
# Additional information about this file can be found at
# https://dart.dev/guides/language/analysis-options
@@ -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 "긴 문장 / 속담 읽기";
}
}
@@ -0,0 +1,27 @@
name: feature_game_read_aloud
description: Speech recognition game for language training.
version: 0.0.1
publish_to: 'none'
resolution: workspace
environment:
sdk: '^3.9.2'
flutter: '>=3.10.0'
dependencies:
flutter:
sdk: flutter
# 음성 인식 핵심 패키지
speech_to_text: ^6.6.0
permission_handler: ^11.0.0
# 공통 모듈
feature_common:
path: ../feature_common
service_api:
path: ../service_api
dev_dependencies:
flutter_test:
sdk: flutter
@@ -0,0 +1,12 @@
import 'package:flutter_test/flutter_test.dart';
import 'package:feature_game_read_aloud/feature_game_read_aloud.dart';
void main() {
test('adds one to input values', () {
final calculator = Calculator();
expect(calculator.addOne(2), 3);
expect(calculator.addOne(-7), -6);
expect(calculator.addOne(0), 1);
});
}