로그인 수정, 애드몹 수정, 메인페이지까지 점검완료

This commit is contained in:
eld_master 2025-01-13 15:04:33 +09:00
parent 3069552557
commit eab0597573
34 changed files with 2742 additions and 1053 deletions

View File

@ -1,44 +1,48 @@
plugins {
id "com.android.application"
// START: FlutterFire Configuration
// (Firebase, Google Services )
id 'com.google.gms.google-services'
// END: FlutterFire Configuration
id "kotlin-android"
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id "dev.flutter.flutter-gradle-plugin"
}
android {
namespace = "com.allscore_app"
compileSdkVersion = 34
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17
}
compileSdkVersion 34
defaultConfig {
applicationId = "com.allscore_app"
applicationId "com.allscore_app"
minSdkVersion 23
targetSdkVersion 34
versionCode = 1
versionName = "1.0"
versionCode 1
versionName "1.0"
}
// ...
buildTypes {
release {
// TODO: Add your own signing config for the release build.
// Signing with the debug keys for now, so `flutter run --release` works.
signingConfig = signingConfigs.debug
signingConfig signingConfigs.debug
}
}
compileOptions {
sourceCompatibility JavaVersion.VERSION_17
targetCompatibility JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_17
}
}
flutter {
source = "../.."
}
dependencies {
// ...
implementation 'com.google.android.gms:play-services-auth:20.6.0'
}
// (Firebase Auth, Crashlytics , .)
apply plugin: 'com.google.gms.google-services'

View File

@ -0,0 +1 @@
{"installed":{"client_id":"19981745655-3dadv7n64jqcada6mtc1ao25k1m90gp3.apps.googleusercontent.com","project_id":"allscore-447406","auth_uri":"https://accounts.google.com/o/oauth2/auth","token_uri":"https://oauth2.googleapis.com/token","auth_provider_x509_cert_url":"https://www.googleapis.com/oauth2/v1/certs"}}

View File

@ -1,27 +1,45 @@
{
"project_info": {
"project_number": "70449524223",
"firebase_url": "https://allscore-344c2-default-rtdb.asia-southeast1.firebasedatabase.app",
"project_id": "allscore-344c2",
"storage_bucket": "allscore-344c2.firebasestorage.app"
"project_number": "452355332155",
"firebase_url": "https://allscore-29edf-default-rtdb.asia-southeast1.firebasedatabase.app",
"project_id": "allscore-29edf",
"storage_bucket": "allscore-29edf.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:70449524223:android:94ffb9ec98e508313e4bca",
"mobilesdk_app_id": "1:452355332155:android:152995468604d10d13e41e",
"android_client_info": {
"package_name": "com.allscore_app"
}
},
"oauth_client": [],
"oauth_client": [
{
"client_id": "452355332155-t29ceato8o62c9kq9drefe7b6hd1ka1d.apps.googleusercontent.com",
"client_type": 1,
"android_info": {
"package_name": "com.allscore_app",
"certificate_hash": "83fe36945bd0f037a7b934f9737a4fa94c47872d"
}
},
{
"client_id": "452355332155-jv26k1rs4tro38tc99mffid2e3gbra6j.apps.googleusercontent.com",
"client_type": 3
}
],
"api_key": [
{
"current_key": "AIzaSyAJEItMxO-TemHGlveSKySG-eNaTD9XJI0"
"current_key": "AIzaSyB6hil7Nrk8wslHDfRNRRyw6rQktY16tTc"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
"other_platform_oauth_client": [
{
"client_id": "452355332155-jv26k1rs4tro38tc99mffid2e3gbra6j.apps.googleusercontent.com",
"client_type": 3
}
]
}
}
}

View File

@ -2,16 +2,24 @@
package="com.allscore_app">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<uses-permission android:name="android.permission.INTERNET" />
<application
android:label="allscore_app"
android:label="올스코어"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<!-- ★ 여기에 meta-data 추가 ★ -->
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-3151339278746301~2596989191" />
android:value="ca-app-pub-3940256099942544~3347511713" />
<!-- android:value="ca-app-pub-6461991944599918~9492697896" -->
<!-- 구글 로그인 관련 -->
<meta-data
android:name="com.google.android.gms.auth.api.signin.client_id"
android:value="19981745655-3dadv7n64jqcada6mtc1ao25k1m90gp3.apps.googleusercontent.com" />
<activity
android:name=".MainActivity"

View File

@ -10,6 +10,7 @@ buildscript {
classpath "com.android.tools.build:gradle:8.2.1"
// Kotlin classpath가
// classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10"
classpath 'com.google.gms:google-services:4.4.2'
}
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 972 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.6 KiB

View File

@ -5,7 +5,7 @@
<key>CFBundleDevelopmentRegion</key>
<string>$(DEVELOPMENT_LANGUAGE)</string>
<key>CFBundleDisplayName</key>
<string>Allscore App</string>
<string>올스코어</string>
<key>CFBundleExecutable</key>
<string>$(EXECUTABLE_NAME)</string>
<key>CFBundleIdentifier</key>

4
lib/config/config.dart Normal file
View File

@ -0,0 +1,4 @@
class Config {
static const String testAdUnitId = 'ca-app-pub-3940256099942544/6300978111';
static const String realAdUnitId = 'ca-app-pub-3940256099942544/6300978111';
}

View File

@ -0,0 +1,149 @@
import 'package:flutter/material.dart';
/// "방 정보 보기" ()
class RoomSettingFinishDialog extends StatelessWidget {
final Map<String, dynamic> roomInfo;
const RoomSettingFinishDialog({
Key? key,
required this.roomInfo,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final media = MediaQuery.of(context);
final String roomTitle = (roomInfo['room_title'] ?? '') as String;
final String roomIntro = (roomInfo['room_intro'] ?? '') as String;
final String openYn = (roomInfo['open_yn'] ?? 'Y') as String;
final int maxPeople = (roomInfo['number_of_people'] ?? 0) as int;
final int runningTime = (roomInfo['running_time'] ?? 0) as int;
final String scoreOpen = (roomInfo['score_open_range'] ?? 'ALL') as String;
final String openLabel = (openYn == 'Y') ? '공개' : '비공개';
return Dialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(18)),
child: ConstrainedBox(
constraints: BoxConstraints(
maxHeight: media.size.height * 0.8,
minWidth: media.size.width * 0.8,
),
child: SingleChildScrollView(
padding: const EdgeInsets.all(20),
child: Column(
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// (A)
const Text(
'방 정보',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
const Divider(color: Colors.black54),
const SizedBox(height: 12),
// (B)
_buildLabelValue(label: '방 제목', value: roomTitle.isNotEmpty ? roomTitle : '(없음)'),
const SizedBox(height: 14),
// (C)
const Text(
'방 소개',
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
const SizedBox(height: 6),
// (width: double.infinity), 100px
Container(
width: double.infinity,
constraints: const BoxConstraints(maxHeight: 100),
decoration: BoxDecoration(
border: Border.all(color: Colors.black26),
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(8),
child: SingleChildScrollView(
child: Text(
roomIntro.isNotEmpty ? roomIntro : '소개글 없음',
style: const TextStyle(fontSize: 14, color: Colors.black),
softWrap: true,
),
),
),
const SizedBox(height: 16),
// (D)
_buildLabelValue(label: '공개 여부', value: openLabel),
const SizedBox(height: 8),
// (E)
_buildLabelValue(label: '최대 인원', value: '$maxPeople'),
const SizedBox(height: 8),
// (F)
_buildLabelValue(label: '운영 시간', value: '$runningTime'),
const SizedBox(height: 8),
// (G)
_buildLabelValue(label: '점수 공개 범위', value: scoreOpen),
const SizedBox(height: 24),
// (H) ( )
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
child: const Text('닫기'),
),
],
),
],
),
),
),
);
}
/// label + value를
Widget _buildLabelValue({
required String label,
required String value,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
label,
style: const TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
color: Colors.black87,
),
),
const SizedBox(height: 4),
Text(
value,
style: const TextStyle(fontSize: 14, color: Colors.black),
),
],
);
}
}

View File

@ -69,8 +69,11 @@ void showSettingsDialog(BuildContext context) {
//
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setString('auth_token', ''); // auth_token
Navigator.of(context).pushReplacement(
await prefs.setBool('auto_login', false); // auto_login
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => const LoginPage()), //
(route) => false,
);
},
style: ButtonStyle(

View File

@ -0,0 +1,127 @@
import 'package:flutter/material.dart';
class UserInfoFinishDialog extends StatelessWidget {
final Map<String, dynamic> userData;
const UserInfoFinishDialog({Key? key, required this.userData}) : super(key: key);
@override
Widget build(BuildContext context) {
// : { "nickname": "testuser4", "profile_img": "...", "introduce_myself": "..." }
final nickname = (userData['nickname'] ?? '').toString();
final profileImg = (userData['profile_img'] ?? '').toString();
final intro = (userData['introduce_myself'] ?? '').toString().trim();
return Dialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
// ,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// (A)
const Text(
'유저 정보',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 16),
// (B)
Container(
width: 100,
height: 100,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.black26, width: 1.5),
),
child: ClipOval(
child: (profileImg.isNotEmpty)
? Image.network(
'https://eldsoft.com:8097/images$profileImg',
fit: BoxFit.cover,
errorBuilder: (ctx, err, st) =>
const Center(child: Text('이미지\n오류', textAlign: TextAlign.center, style: TextStyle(fontSize: 12))),
)
: const Center(
child: Text(
'이미지\n없음',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 12),
),
),
),
),
const SizedBox(height: 16),
// (C)
Text(
nickname.isNotEmpty ? nickname : '유저',
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
),
),
const SizedBox(height: 20),
// (D)
const Align(
alignment: Alignment.centerLeft,
child: Text(
'소개',
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
),
),
),
const SizedBox(height: 6),
// (E) ( )
Container(
width: double.infinity,
// 60, 200
constraints: const BoxConstraints(
minHeight: 60,
maxHeight: 200,
),
decoration: BoxDecoration(
border: Border.all(color: Colors.black26),
borderRadius: BorderRadius.circular(8),
),
padding: const EdgeInsets.all(8),
child: SingleChildScrollView(
child: Text(
intro.isNotEmpty ? intro : '소개글이 없습니다.',
style: const TextStyle(fontSize: 14),
),
),
),
const SizedBox(height: 24),
// (F)
SizedBox(
width: 100,
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: const EdgeInsets.symmetric(vertical: 12),
),
child: const Text('확인', style: TextStyle(fontSize: 14)),
),
),
],
),
),
);
}
}

View File

@ -11,13 +11,17 @@ import 'firebase_options.dart';
import 'views/login/login_page.dart';
import 'views/room/main_page.dart';
//
import 'package:google_mobile_ads/google_mobile_ads.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Firebase
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform, // FirebaseOptions
);
//
await Firebase.initializeApp();
//
MobileAds.instance.initialize();
runApp(const MyApp());
}

View File

@ -6,6 +6,12 @@ import 'login_page.dart';
import 'pw_finding_page.dart';
import 'signup_page.dart';
//
import 'package:google_mobile_ads/google_mobile_ads.dart';
//
import '../../config/config.dart';
class IdFindingPage extends StatefulWidget {
const IdFindingPage({Key? key}) : super(key: key);
@ -21,7 +27,14 @@ class _IdFindingPageState extends State<IdFindingPage> {
String foundIdMessage = '';
String authId = '';
/// (1)
BannerAd? _bannerAd;
bool _isBannerReady = false; //
String adUnitId = Config.testAdUnitId;
Future<void> _findId(String nickname, String email) async {
//
showDialog(
context: context,
@ -85,6 +98,36 @@ class _IdFindingPageState extends State<IdFindingPage> {
}
}
@override
void initState() {
super.initState();
_initBannerAd();
}
void _initBannerAd() {
_bannerAd = BannerAd(
// / ID
adUnitId: adUnitId,
size: AdSize.banner,
request: const AdRequest(),
listener: BannerAdListener(
onAdLoaded: (ad) {
setState(() => _isBannerReady = true);
},
onAdFailedToLoad: (ad, error) {
ad.dispose();
},
),
);
_bannerAd?.load();
}
@override
void dispose() {
_bannerAd?.dispose();
super.dispose();
}
Future<void> _findAllId() async {
// ID
print('ID 전체 찾기 요청 $authId'); //
@ -185,7 +228,6 @@ class _IdFindingPageState extends State<IdFindingPage> {
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
@ -303,10 +345,21 @@ class _IdFindingPageState extends State<IdFindingPage> {
},
child: const Text('회원가입', style: TextStyle(color: Colors.black)),
),
],
),
),
),
// (3)
bottomNavigationBar: _isBannerReady && _bannerAd != null
? Container(
color: Colors.white,
width: _bannerAd!.size.width.toDouble(),
height: _bannerAd!.size.height.toDouble(),
child: AdWidget(ad: _bannerAd!),
)
: SizedBox.shrink(), // or
);
}
}

View File

@ -1,50 +1,118 @@
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'dart:convert';
import 'dart:convert' show utf8;
// import 'package:http/http.dart' as http; . Api.serverRequest()
import 'dart:convert' show utf8, jsonEncode;
import 'package:crypto/crypto.dart';
import 'package:shared_preferences/shared_preferences.dart';
// Api
import '../../plugins/api.dart';
//
import '../../dialogs/response_dialog.dart';
// () ID/PW ,
import 'id_finding_page.dart';
import 'pw_finding_page.dart';
import 'signup_page.dart';
//
import 'package:google_sign_in/google_sign_in.dart';
import 'package:firebase_auth/firebase_auth.dart';
//
import 'package:google_mobile_ads/google_mobile_ads.dart';
//
import '../room/main_page.dart';
//
import '../../config/config.dart';
//
import 'package:fluttertoast/fluttertoast.dart'; // Toast
class LoginPage extends StatefulWidget {
const LoginPage({Key? key}) : super(key: key);
@override
_LoginPageState createState() => _LoginPageState();
State<LoginPage> createState() => _LoginPageState();
}
class _LoginPageState extends State<LoginPage> {
//
// (A) ID/PW
//
final TextEditingController idController = TextEditingController();
final TextEditingController passwordController = TextEditingController();
bool autoLogin = false;
String loginErrorMessage = '';
String loginErrorMessage = ''; //
//
// (B)
//
final GoogleSignIn _googleSignIn = GoogleSignIn(
scopes: <String>['email'],
);
//
// (C)
//
BannerAd? _bannerAd;
Widget? _adWidget;
bool _isBannerReady = false;
String adUnitId = Config.testAdUnitId;
//
DateTime? _lastPressedTime;
//
bool _isLoading = false;
// : 2
static const _exitDuration = Duration(seconds: 2);
Future<bool> _onWillPop() async {
final now = DateTime.now();
if (_lastPressedTime == null ||
now.difference(_lastPressedTime!) > _exitDuration) {
// or
_lastPressedTime = now;
// (Toast )
Fluttertoast.showToast(
msg: '한 번 더 누르면 앱이 종료됩니다.',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
);
return false; // pop
}
// 2
return true; // pop (Scaffold , )
}
@override
void initState() {
super.initState();
_initBannerAd();
}
void _initBannerAd() {
_bannerAd = BannerAd(
adUnitId: "ca-app-pub-3151339278746301~1689299887",
request: const AdRequest(),
// / ID
adUnitId: adUnitId,
size: AdSize.banner,
request: const AdRequest(),
listener: BannerAdListener(
onAdLoaded: (ad) {
setState(() {
_adWidget = AdWidget(ad: ad as AdWithView);
});
setState(() => _isBannerReady = true);
print('로그인페이지 배너 광고 로드 완료');
},
onAdFailedToLoad: (ad, error) {
print('Ad failed to load: $error');
print('로그인페이지 배너 광고 로드 실패: $error');
ad.dispose();
},
),
)..load();
);
_bannerAd?.load();
}
@override
@ -53,93 +121,287 @@ class _LoginPageState extends State<LoginPage> {
super.dispose();
}
Future<void> _login() async {
String id = idController.text.trim();
String password = passwordController.text.trim();
//
// (D1) ID/PW
//
Future<void> _loginWithIdPw() async {
setState(() => _isLoading = true);
final id = idController.text.trim();
final pw = passwordController.text.trim();
// autoLogin
String autoLoginStatus = autoLogin ? 'Y' : 'N';
// PW SHA-256
final bytes = utf8.encode(pw);
final digest = sha256.convert(bytes);
final hashedPw = digest.toString();
// PW를 sha256으로
var bytes = utf8.encode(password);
var digest = sha256.convert(bytes);
final requestBody = {
"user_id": id,
"user_pw": hashedPw,
};
try {
final response = await http.post(
Uri.parse('https://eldsoft.com:8097/user/login'),
headers: {
'Content-Type': 'application/json',
'auth_token': '',
},
body: jsonEncode({
'user_id': id,
'user_pw': digest.toString(),
}),
).timeout(const Duration(seconds: 10));
// (1) /user/login
final response = await Api.serverRequest(uri: '/user/login', body: requestBody);
//
String responseBody = utf8.decode(response.bodyBytes);
if (response.statusCode == 200) {
final Map<String, dynamic> jsonResponse = jsonDecode(responseBody);
print('jsonResponse: $jsonResponse');
if (jsonResponse['result'] == 'OK') {
if (response['result'] == 'OK') {
//
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
//
final authData = jsonResponse['auth'] ?? {};
final token = authData['token'] ?? '';
final userSeq = authData['user_seq'] ?? 0; //
print('ID/PW 로그인 성공: $resp');
SharedPreferences prefs = await SharedPreferences.getInstance();
// autoLogin
await prefs.setString('auth_token', token);
await prefs.setBool('auto_login', autoLogin);
// (New) user_seq
await prefs.setInt('my_user_seq', userSeq);
// (a) google_user_yn = N
final prefs = await SharedPreferences.getInstance();
await prefs.setString('oauth_type', 'idpw');
await prefs.setBool('auto_login', true);
await prefs.setString('jwt_token', resp['auth']['token'].toString());
await prefs.setString('user_seq', resp['auth']['user_seq'].toString());
//
//
if (!mounted) return;
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const MainPage()),
MaterialPageRoute(builder: (_) => const MainPage()),
);
} else if (jsonResponse['response_info']['msg_title'] == '로그인 실패') {
//
setState(() {
loginErrorMessage = '회원정보를 다시 확인해주세요.';
});
} else {
// result != OK ,
_showDialog('로그인 실패', '서버에서 로그인에 실패했습니다.\n관리자에게 문의해주세요.');
showResponseDialog(context, resp['response_info']['msg_title'], resp['response_info']['msg_content']);
}
} else {
_showDialog('오류', '로그인에 실패했습니다. 관리자에게 문의해주세요.');
// FAIL
showResponseDialog(context, '오류', '로그인 요청 실패');
}
} catch (e) {
print('로그인 요청 중 오류: $e');
_showDialog('오류', '로그인 요청이 실패했습니다. 관리자에게 문의해주세요.\n$e');
showResponseDialog(context, '오류', '로그인 요청 중 예외 발생.\n$e');
} finally {
setState(() => _isLoading = false);
}
}
void _showDialog(String title, String content) {
showDialog(
//
// (D2)
//
Future<void> _googleLogin() async {
setState(() => _isLoading = true);
try {
// 1)
final GoogleSignInAccount? googleUser = await _googleSignIn.signIn();
if (googleUser == null) {
//
return;
}
// 2)
final GoogleSignInAuthentication googleAuth = await googleUser.authentication;
// 3) FirebaseAuth Credential
final AuthCredential credential = GoogleAuthProvider.credential(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
// 4) FirebaseAuth로
final UserCredential userCredential =
await FirebaseAuth.instance.signInWithCredential(credential);
final User? user = userCredential.user;
if (user == null) {
print('구글 로그인 실패: Firebase User가 null');
_showAlert('로그인 오류', 'Firebase User가 null입니다.');
return;
}
final idToken = await user.getIdToken();
//
final requestBody = {
'id_token': idToken,
};
final response = await Api.serverRequest(uri: '/user/google/login', body: requestBody);
if (response['result'] == 'OK') {
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('oauth_type', 'google');
await prefs.setBool('auto_login', true);
await prefs.setString('jwt_token', resp['auth']['token'].toString());
await prefs.setString('user_seq', resp['auth']['user_seq'].toString());
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const MainPage()),
);
} else {
showResponseDialog(context, resp['response_info']['msg_title'], resp['response_info']['msg_content']);
}
} else {
showResponseDialog(context, '오류', '구글 로그인 요청 실패');
}
// () SharedPreferences에 google_user_yn = 'Y'
final prefs = await SharedPreferences.getInstance();
await prefs.setString('google_user_yn', 'Y');
} catch (e) {
_showAlert('오류', '구글 로그인 중 오류가 발생했습니다.\n$e');
} finally {
setState(() => _isLoading = false);
}
}
//
// (D3)
//
Future<void> _googleSignUp() async {
final agreed = await _showTermsModal();
if (agreed != true) {
return;
}
try {
// (2)
final GoogleSignInAccount? googleUser = await _googleSignIn.signIn();
if (googleUser == null) {
//
return;
}
// (3)
final GoogleSignInAuthentication googleAuth = await googleUser.authentication;
// (4) FirebaseAuth Credential
final AuthCredential credential = GoogleAuthProvider.credential(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
// (5) FirebaseAuth로 ( )
// "회원가입" , Firebase signInWithCredential()
final UserCredential userCredential =
await FirebaseAuth.instance.signInWithCredential(credential);
final User? user = userCredential.user;
if (user == null) {
showResponseDialog(context, '오류', '구글계정 인증에 실패했습니다.');
return;
}
// (6) idToken ,
final idToken = await user.getIdToken();
final requestBody = {
'id_token': idToken,
};
// '/user/google/signup' API ()
final response = await Api.serverRequest(uri: '/user/google/signup', body: requestBody);
if (response['result'] == 'OK') {
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
//
showResponseDialog(context, '회원가입 완료', '구글 회원가입이 완료되었습니다.');
} else {
// OK가 ,
final msgTitle = resp['response_info']?['msg_title'] ?? '오류';
final msgContent = resp['response_info']?['msg_content'] ?? '회원가입에 실패했습니다.';
showResponseDialog(context, msgTitle, msgContent);
}
} else {
// FAIL ( result != OK)
showResponseDialog(context, '오류', '구글 회원가입 요청 실패');
}
} catch (e) {
showResponseDialog(context, '오류', '구글 회원가입 중 오류가 발생했습니다.\n$e');
}
}
//
// (E)
//
Future<bool?> _showTermsModal() async {
return showDialog<bool>(
context: context,
builder: (BuildContext context) {
barrierDismissible: false,
builder: (ctx) {
return AlertDialog(
backgroundColor: Colors.white,
title: Text(title, style: const TextStyle(color: Colors.black)),
content: Text(content, style: const TextStyle(color: Colors.black)),
actions: <Widget>[
Center(
child: TextButton(
style: TextButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
title: const Text(
'개인정보 수집 및 이용 동의서',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
content: SingleChildScrollView(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
'''올스코어(이하 "회사"라 합니다)는 이용자의 개인정보를 중요시하며, 「개인정보 보호법」 등 관련 법령을 준수하고 있습니다. 회사는 개인정보 수집 및 이용에 관한 사항을 아래와 같이 안내드리오니, 내용을 충분히 숙지하신 후 동의하여 주시기 바랍니다.
1.
: (ID), (PW), ( ),
: ,
2.
3.
: .
: .
: 5
: 5
: 3
4.
.
:
:
5.
, , , .
, "회원 탈퇴" .
6.
.
.
7.
: eld_yeojh@naver.com
8.
, .
''',
style: TextStyle(fontSize: 14),
),
child: const Text('확인'),
onPressed: () {
Navigator.of(context).pop();
},
],
),
),
actions: [
TextButton(
style: TextButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
),
onPressed: () => Navigator.pop(ctx, false),
child: Text('거부'),
),
TextButton(
style: TextButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
),
onPressed: () => Navigator.pop(ctx, true),
child: Text('동의'),
),
],
);
@ -147,124 +409,287 @@ class _LoginPageState extends State<LoginPage> {
);
}
//
// (F) Alert
//
void _showAlert(String title, String message) {
showDialog(
context: context,
builder: (_) => AlertDialog(
backgroundColor: Colors.white,
title: Text(title, style: const TextStyle(color: Colors.black)),
content: Text(message, style: const TextStyle(color: Colors.black)),
actions: [
TextButton(
style: TextButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
),
onPressed: () => Navigator.pop(context),
child: const Text('확인'),
),
],
),
);
}
//
// (G)
//
@override
Widget build(BuildContext context) {
return Scaffold(
return WillPopScope(
onWillPop: _onWillPop,
child: Stack(
children: [
Scaffold(
backgroundColor: Colors.white,
// AppBar
appBar: AppBar(
title: const Text('ALL SCORE', style: TextStyle(color: Colors.white)),
backgroundColor: Colors.black,
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'로그인',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
const SizedBox(height: 32),
TextField(
controller: idController,
decoration: InputDecoration(
labelText: 'ID',
labelStyle: const TextStyle(color: Colors.black),
border: const OutlineInputBorder(),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2.0),
//
body: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// (1) UI
Expanded(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
//
// 1. (ID/PW)
//
const Text(
'올스코어 로그인',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black),
),
const SizedBox(height: 16),
// (A)
SizedBox(
width: 300,
child: TextField(
controller: idController,
decoration: InputDecoration(
labelText: 'ID',
border: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.black),
borderRadius: BorderRadius.circular(8),
),
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.black),
borderRadius: BorderRadius.circular(8),
),
),
),
),
const SizedBox(height: 12),
// (B)
SizedBox(
width: 300,
child: TextField(
controller: passwordController,
obscureText: true,
decoration: InputDecoration(
labelText: 'PW',
border: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.black),
borderRadius: BorderRadius.circular(8),
),
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.black),
borderRadius: BorderRadius.circular(8),
),
),
),
),
// (C)
if (loginErrorMessage.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
loginErrorMessage,
style: const TextStyle(color: Colors.red),
),
),
const SizedBox(height: 8),
// (D)
SizedBox(
width: 300,
child: Row(
children: [
Checkbox(
value: autoLogin,
onChanged: (val) {
setState(() {
autoLogin = val ?? false;
});
},
),
const Text('자동로그인', style: TextStyle(color: Colors.black)),
],
),
),
// (E)
SizedBox(
width: 300,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
side: const BorderSide(color: Colors.black),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
onPressed: _loginWithIdPw,
child: const Text('로그인'),
),
),
// (F) ID/PW ,
const SizedBox(height: 8),
TextButton(
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (_) => const IdFindingPage()));
},
child: const Text('ID 찾기', style: TextStyle(color: Colors.black)),
),
TextButton(
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (_) => const PwFindingPage()));
},
child: const Text('PW 찾기', style: TextStyle(color: Colors.black)),
),
TextButton(
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (_) => const SignUpPage()));
},
child: const Text('회원가입', style: TextStyle(color: Colors.black)),
),
const SizedBox(height: 24),
const Divider(height: 1, color: Colors.black),
const SizedBox(height: 24),
//
// 2. /
//
const Text(
'구글 계정',
style: TextStyle(fontSize: 20, color: Colors.black, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
// (a)
SizedBox(
width: 300,
child: ElevatedButton.icon(
icon: Container(
width: 24,
height: 24,
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/icons8-google-logo-48.png'),
fit: BoxFit.contain,
),
),
),
label: const Text(
'Google 로그인',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
side: const BorderSide(color: Colors.black),
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
onPressed: _googleLogin,
),
),
const SizedBox(height: 12),
// (b)
SizedBox(
width: 300,
child: ElevatedButton.icon(
icon: Container(
width: 24,
height: 24,
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/icons8-google-logo-48.png'),
fit: BoxFit.contain,
),
),
),
label: const Text(
'Google 회원가입',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
side: const BorderSide(color: Colors.black),
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
onPressed: _googleSignUp,
),
),
],
),
),
),
const SizedBox(height: 16),
TextField(
controller: passwordController,
obscureText: true,
decoration: InputDecoration(
labelText: 'PW',
labelStyle: const TextStyle(color: Colors.black),
border: const OutlineInputBorder(),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2.0),
),
),
),
if (loginErrorMessage.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
loginErrorMessage,
style: const TextStyle(color: Colors.red),
),
),
Row(
mainAxisAlignment: MainAxisAlignment.start,
children: [
Checkbox(
value: autoLogin,
onChanged: (bool? value) {
setState(() {
autoLogin = value ?? false;
});
},
),
const Text('자동로그인', style: TextStyle(color: Colors.black)),
],
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _login,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
),
child: const Text('로그인'),
),
const SizedBox(height: 16),
TextButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const IdFindingPage()),
);
},
child: const Text('ID 찾기', style: TextStyle(color: Colors.black)),
),
TextButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const PwFindingPage()),
);
},
child: const Text('PW 찾기', style: TextStyle(color: Colors.black)),
),
TextButton(
onPressed: () {
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const SignUpPage()),
);
},
child: const Text('회원가입', style: TextStyle(color: Colors.black)),
),
const SizedBox(height: 16),
//
),
// (2)
if (_isBannerReady && _bannerAd != null)
Container(
width: _bannerAd!.size.width.toDouble(),
height: _bannerAd!.size.height.toDouble(),
alignment: Alignment.center,
child: AdWidget(ad: _bannerAd!),
)
else
Container(
width: 300,
height: 50,
color: Colors.grey[300],
child: const Center(child: Text('광고 영역', style: TextStyle(color: Colors.black))),
color: Colors.grey.shade400,
alignment: Alignment.center,
child: const Text(
'광고 로딩중',
style: TextStyle(color: Colors.black),
),
),
],
],
),
),
// (2)
if (_isLoading)
Container(
width: double.infinity,
height: double.infinity,
color: Colors.black54, //
alignment: Alignment.center,
child: const CircularProgressIndicator(color: Colors.white),
),
]
)
);
}
}

View File

@ -6,6 +6,12 @@ import 'login_page.dart'; // 로그인 페이지 임포트 추가
import 'signup_page.dart'; //
import 'id_finding_page.dart'; // ID
//
import 'package:google_mobile_ads/google_mobile_ads.dart';
//
import '../../config/config.dart';
class PwFindingPage extends StatefulWidget {
const PwFindingPage({Key? key}) : super(key: key);
@ -19,6 +25,11 @@ class _PwFindingPageState extends State<PwFindingPage> {
String emailErrorMessage = ''; //
String idErrorMessage = ''; // ID
/// (1)
BannerAd? _bannerAd;
bool _isBannerReady = false; //
String adUnitId = Config.testAdUnitId;
Future<void> _findPassword(String id, String email) async {
// PW
print('PW 찾기 요청: ID: $id, 이메일: $email'); //
@ -113,6 +124,36 @@ class _PwFindingPageState extends State<PwFindingPage> {
);
}
@override
void initState() {
super.initState();
_initBannerAd();
}
void _initBannerAd() {
_bannerAd = BannerAd(
// / ID
adUnitId: adUnitId,
size: AdSize.banner,
request: const AdRequest(),
listener: BannerAdListener(
onAdLoaded: (ad) {
setState(() => _isBannerReady = true);
},
onAdFailedToLoad: (ad, error) {
ad.dispose();
},
),
);
_bannerAd?.load();
}
@override
void dispose() {
_bannerAd?.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
@ -213,6 +254,16 @@ class _PwFindingPageState extends State<PwFindingPage> {
],
),
),
// (3)
bottomNavigationBar: _isBannerReady && _bannerAd != null
? Container(
color: Colors.white,
width: _bannerAd!.size.width.toDouble(),
height: _bannerAd!.size.height.toDouble(),
child: AdWidget(ad: _bannerAd!),
)
: SizedBox.shrink(), // or
);
}
}

View File

@ -21,7 +21,7 @@ class _SignUpPageState extends State<SignUpPage> {
//
bool _isUsernameValid(String username) => RegExp(r'^(?![0-9])[A-Za-z0-9]{6,20}$').hasMatch(username);
bool _isPasswordValidPattern(String password) => RegExp(r'^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d]{8,20}$').hasMatch(password);
bool _isPasswordValidPattern(String password) => RegExp(r"""^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d!@#$%^&*()_\-+=~`{}\[\]|\\:;\"'<>,.?/]{8,20}$""").hasMatch(password);
bool _isEmailValid(String email) => RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(email);
bool _isNicknameValid(String nickname) => RegExp(r'^[A-Za-z가-힣0-9]{2,20}$').hasMatch(nickname);

View File

@ -1,48 +1,325 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'main_page.dart';
import '../../plugins/api.dart';
import '../../dialogs/response_dialog.dart';
import '../../dialogs/room_setting_finish_dialog.dart';
import '../../dialogs/user_info_finish_dialog.dart';
class FinishPrivatePage extends StatelessWidget {
class FinishPrivatePage extends StatefulWidget {
final int roomSeq;
final bool fromPlayingPage; // / =>
const FinishPrivatePage({
Key? key,
required this.roomSeq,
this.fromPlayingPage = false,
}) : super(key: key);
@override
Widget build(BuildContext context) {
//
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text('게임 종료 (개인전)', style: TextStyle(color: Colors.white)),
backgroundColor: Colors.black,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
onPressed: () {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
},
),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
State<FinishPrivatePage> createState() => _FinishPrivatePageState();
}
class _FinishPrivatePageState extends State<FinishPrivatePage> {
bool _isLoading = true;
Map<String, dynamic> _roomInfo = {}; // room_info
// user_info Map
// userSeq { user_seq, nickname, participant_type, score, ... }
Map<String, dynamic> _userMap = {};
// ( ) ( )
List<Map<String, dynamic>> _playerList = [];
// (ADMIN) ( 1 )
List<Map<String, dynamic>> _adminList = [];
String _roomTitle = '';
DateTime? _startDt;
DateTime? _endDt;
int _masterUserSeq = 0; // user_seq (ADMIN과는 )
@override
void initState() {
super.initState();
_fetchFinishRoomInfo();
}
/// (A)
Future<void> _fetchFinishRoomInfo() async {
setState(() => _isLoading = true);
try {
final body = {"room_seq": "${widget.roomSeq}"};
final response = await Api.serverRequest(
uri: '/room/score/get/finish/room/info',
body: body,
);
if (response['result'] == 'OK') {
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
final data = resp['data'] ?? {};
final rInfo = data['room_info'] ?? {};
final uInfo = data['user_info'] ?? {};
final rTitle = (rInfo['room_title'] ?? '') as String;
final mSeq = (rInfo['master_user_seq'] ?? 0) as int;
final sdt = rInfo['start_dt'];
final edt = rInfo['end_dt'];
setState(() {
_roomInfo = rInfo;
_userMap = uInfo;
_roomTitle = rTitle.isNotEmpty ? rTitle : '종료된 방(개인전)';
_masterUserSeq = mSeq;
if (sdt != null && sdt is String && sdt.contains('T')) {
_startDt = DateTime.tryParse(sdt);
}
if (edt != null && edt is String && edt.contains('T')) {
_endDt = DateTime.tryParse(edt);
}
});
// userInfo -> List
final List<Map<String, dynamic>> tempList = [];
uInfo.forEach((_, val) {
// val: { user_seq, participant_type, nickname, score, ... }
tempList.add(Map<String, dynamic>.from(val));
});
// (1) (ADMIN)
final adminList = tempList.where((u) {
final pType = (u['participant_type'] ?? '').toString().toUpperCase();
return pType == 'ADMIN';
}).toList();
// (2) (ADMIN ) &
final playerList = tempList.where((u) {
final pType = (u['participant_type'] ?? '').toString().toUpperCase();
return pType != 'ADMIN';
}).toList();
//
playerList.sort((a, b) {
final sa = (a['score'] ?? 0) as int;
final sb = (b['score'] ?? 0) as int;
return sb.compareTo(sa); //
});
setState(() {
_adminList = adminList;
_playerList = playerList;
_isLoading = false;
});
} else {
final msgTitle = resp['response_info']?['msg_title'] ?? '오류';
final msgContent = resp['response_info']?['msg_content'] ?? '데이터 조회 실패';
showResponseDialog(context, msgTitle, msgContent);
}
} else {
showResponseDialog(context, '실패', '서버 통신 오류');
}
} catch (e) {
showResponseDialog(context, '오류', '$e');
} finally {
setState(() => _isLoading = false);
}
}
/// (B)
Future<bool> _onWillPop() async {
if (widget.fromPlayingPage) {
// / =>
Navigator.popUntil(context, (route) => route.isFirst);
} else {
// => pop
Navigator.pop(context);
}
return false;
}
/// (C) ( )
Future<void> _openRoomSettingDialog() async {
if (_roomInfo.isEmpty) return;
await showDialog(
context: context,
barrierDismissible: false,
builder: (_) => RoomSettingFinishDialog(roomInfo: _roomInfo),
);
}
/// (D)
Widget _buildGameTimeWidget() {
if (_startDt == null || _endDt == null) {
return const Text('게임 진행 시간: 00:00', style: TextStyle(fontSize: 14));
}
final dur = _endDt!.difference(_startDt!);
final hh = dur.inHours.toString().padLeft(2, '0');
final mm = dur.inMinutes.remainder(60).toString().padLeft(2, '0');
return Text('게임 진행 시간: $hh:$mm', style: const TextStyle(fontSize: 14));
}
/// (E) ( )
/// - 1/2/3 //
Widget _buildPlayerItem(Map<String, dynamic> user, int index) {
final score = (user['score'] ?? 0) as int;
final nickname = user['nickname'] ?? '유저';
final profileImg = user['profile_img'] ?? '';
final userSeq = user['user_seq'] ?? 0;
Widget medal = const SizedBox();
if (index == 0) {
medal = const Text('🥇 ', style: TextStyle(fontSize: 16));
} else if (index == 1) {
medal = const Text('🥈 ', style: TextStyle(fontSize: 16));
} else if (index == 2) {
medal = const Text('🥉 ', style: TextStyle(fontSize: 16));
}
return GestureDetector(
onTap: () => _onTapUser(user),
child: Container(
margin: const EdgeInsets.symmetric(vertical: 4),
child: Row(
children: [
const Text('게임이 종료되었습니다.', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
//
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
},
style: ElevatedButton.styleFrom(backgroundColor: Colors.black),
child: const Text('메인으로', style: TextStyle(color: Colors.white)),
medal,
//
Container(
width: 36, height: 36,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.black54),
),
child: ClipOval(
child: (profileImg.isNotEmpty)
? Image.network(
'https://eldsoft.com:8097/images$profileImg',
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => const Center(child: Text('ERR')),
)
: const Center(child: Text('No\nImg', textAlign: TextAlign.center, style: TextStyle(fontSize: 10))),
),
),
const SizedBox(width: 8),
Expanded(
child: Text(nickname, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
),
Text('$score', style: const TextStyle(fontSize: 14)),
],
),
),
);
}
/// ( 1 )
Widget _buildAdminItem(Map<String, dynamic> admin) {
final nickname = admin['nickname'] ?? '사회자';
final profileImg = admin['profile_img'] ?? '';
return GestureDetector(
onTap: () => _onTapUser(admin),
child: Row(
children: [
Container(
width: 36, height: 36,
margin: const EdgeInsets.only(right: 8),
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.deepPurple),
),
child: ClipOval(
child: (profileImg.isNotEmpty)
? Image.network(
'https://eldsoft.com:8097/images$profileImg',
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => const Center(child: Text('ERR')),
)
: const Center(child: Text('No\nImg', textAlign: TextAlign.center, style: TextStyle(fontSize: 10))),
),
),
Text(nickname, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.deepPurple)),
],
),
);
}
/// ->
Future<void> _onTapUser(Map<String, dynamic> userData) async {
// user_info_finish_dialog.dart ( )
await showDialog(
context: context,
barrierDismissible: false,
builder: (_) => UserInfoFinishDialog(userData: userData),
);
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: _onWillPop,
child: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.black,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
onPressed: _onWillPop,
),
title: Text(_roomTitle, style: const TextStyle(color: Colors.white)),
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// (A) : [ ] +
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
//
ElevatedButton(
onPressed: _openRoomSettingDialog,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
side: const BorderSide(color: Colors.black, width: 1),
),
child: const Text('방 정보 보기', style: TextStyle(color: Colors.black)),
),
_buildGameTimeWidget(),
],
),
const SizedBox(height: 16),
// (B)
if (_adminList.isNotEmpty) ...[
Container(
width: double.infinity,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
border: Border.all(color: Colors.deepPurple),
borderRadius: BorderRadius.circular(8),
),
child: Row(
children: [
const Text('사회자: ', style: TextStyle(fontSize: 14, color: Colors.deepPurple)),
..._adminList.map(_buildAdminItem),
],
),
),
const SizedBox(height: 16),
],
// (C)
ListView.builder(
primary: false,
shrinkWrap: true,
itemCount: _playerList.length,
itemBuilder: (ctx, i) => _buildPlayerItem(_playerList[i], i),
),
],
),
),
),
);
}
}

View File

@ -1,47 +1,399 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'main_page.dart';
class FinishTeamPage extends StatelessWidget {
import '../../plugins/api.dart';
import '../../dialogs/response_dialog.dart';
import '../../dialogs/room_setting_finish_dialog.dart';
import '../../dialogs/user_info_finish_dialog.dart';
class FinishTeamPage extends StatefulWidget {
final int roomSeq;
final bool fromPlayingPage; //
const FinishTeamPage({
Key? key,
required this.roomSeq,
this.fromPlayingPage = false,
}) : super(key: key);
@override
Widget build(BuildContext context) {
//
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text('게임 종료 (팀전)', style: TextStyle(color: Colors.white)),
backgroundColor: Colors.black,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
onPressed: () {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
},
),
State<FinishTeamPage> createState() => _FinishTeamPageState();
}
class _FinishTeamPageState extends State<FinishTeamPage> {
bool _isLoading = true;
Map<String, dynamic> _roomInfo = {};
Map<String, dynamic> _userMap = {};
List<Map<String, dynamic>> _userList = [];
String _roomTitle = '';
int _masterUserSeq = 0;
DateTime? _startDt;
DateTime? _endDt;
// [ { user }, { user } ... ]
Map<String, List<Map<String, dynamic>>> _teamMap = {};
//
Map<String, int> _teamScoreMap = {};
//
List<Map<String, dynamic>> _adminList = [];
@override
void initState() {
super.initState();
_fetchFinishRoomInfo();
}
Future<void> _fetchFinishRoomInfo() async {
setState(() => _isLoading = true);
try {
final body = {"room_seq": "${widget.roomSeq}"};
final response = await Api.serverRequest(
uri: '/room/score/get/finish/room/info',
body: body,
);
if (response['result'] == 'OK') {
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
final data = resp['data'] ?? {};
final rInfo = data['room_info'] ?? {};
final uInfo = data['user_info'] ?? {};
final rTitle = (rInfo['room_title'] ?? '') as String;
final mSeq = (rInfo['master_user_seq'] ?? 0) as int;
final sdt = rInfo['start_dt'];
final edt = rInfo['end_dt'];
setState(() {
_roomInfo = rInfo;
_userMap = uInfo;
_roomTitle = rTitle.isNotEmpty ? rTitle : '종료된 팀전';
_masterUserSeq = mSeq;
if (sdt != null && sdt is String && sdt.contains('T')) {
_startDt = DateTime.tryParse(sdt);
}
if (edt != null && edt is String && edt.contains('T')) {
_endDt = DateTime.tryParse(edt);
}
});
// userList
final List<Map<String, dynamic>> tempList = [];
uInfo.forEach((_, val) {
tempList.add(Map<String, dynamic>.from(val));
});
// (1) (ADMIN)
final adminList = tempList.where((u) {
final pType = (u['participant_type'] ?? '').toString().toUpperCase();
return pType == 'ADMIN';
}).toList();
// (2)
final players = tempList.where((u) {
final pType = (u['participant_type'] ?? '').toString().toUpperCase();
return pType != 'ADMIN';
}).toList();
// (3) +
final Map<String, List<Map<String, dynamic>>> tMap = {};
final Map<String, int> tScoreMap = {};
for (var user in players) {
final tName = (user['team_name'] ?? 'WAIT').toString().toUpperCase();
if (tName == 'WAIT') continue; //
tMap.putIfAbsent(tName, () => []);
tMap[tName]!.add(user);
}
//
tMap.forEach((team, mems) {
int sumScore = 0;
// mems
mems.sort((a, b) {
final sa = (a['score'] ?? 0) as int;
final sb = (b['score'] ?? 0) as int;
return sb.compareTo(sa);
});
for (var m in mems) {
sumScore += (m['score'] ?? 0) as int;
}
tScoreMap[team] = sumScore;
});
// (4)
final sortedTeams = tScoreMap.keys.toList();
sortedTeams.sort((a, b) => tScoreMap[b]!.compareTo(tScoreMap[a]!));
//
final Map<String, List<Map<String, dynamic>>> finalTeamMap = {};
final Map<String, int> finalScoreMap = {};
for (var t in sortedTeams) {
finalTeamMap[t] = tMap[t]!;
finalScoreMap[t] = tScoreMap[t]!;
}
setState(() {
_adminList = adminList;
_userList = tempList; //
_teamMap = finalTeamMap;
_teamScoreMap = finalScoreMap;
_isLoading = false;
});
} else {
final msgTitle = resp['response_info']?['msg_title'] ?? '오류';
final msgContent = resp['response_info']?['msg_content'] ?? '데이터 조회 실패';
showResponseDialog(context, msgTitle, msgContent);
}
} else {
showResponseDialog(context, '실패', '서버 통신 오류');
}
} catch (e) {
showResponseDialog(context, '오류', '$e');
} finally {
setState(() => _isLoading = false);
}
}
Future<bool> _onWillPop() async {
if (widget.fromPlayingPage) {
Navigator.popUntil(context, (route) => route.isFirst);
} else {
Navigator.pop(context);
}
return false;
}
Future<void> _openRoomSettingDialog() async {
if (_roomInfo.isEmpty) return;
await showDialog(
context: context,
barrierDismissible: false,
builder: (_) => RoomSettingFinishDialog(roomInfo: _roomInfo),
);
}
Widget _buildGameTimeWidget() {
if (_startDt == null || _endDt == null) {
return const Text('게임 진행 시간: 00:00', style: TextStyle(fontSize: 14));
}
final dur = _endDt!.difference(_startDt!);
final hh = dur.inHours.toString().padLeft(2, '0');
final mm = dur.inMinutes.remainder(60).toString().padLeft(2, '0');
return Text('게임 진행 시간: $hh:$mm', style: const TextStyle(fontSize: 14));
}
/// ( + 123 // )
Widget _buildTeamBox(String teamName, int index) {
final members = _teamMap[teamName] ?? [];
final tScore = _teamScoreMap[teamName] ?? 0;
// 1/2/3 ->
Color bgColor = Colors.white;
if (index == 0) {
bgColor = const Color(0xFFFFF9C4); // : amber.shade100
} else if (index == 1) {
bgColor = const Color(0xFFE0E0E0); // ()
} else if (index == 2) {
bgColor = const Color(0xFFFFE0B2); // (~ )
}
return Container(
margin: const EdgeInsets.symmetric(vertical: 8),
decoration: BoxDecoration(
color: bgColor,
border: Border.all(color: Colors.black),
borderRadius: BorderRadius.circular(8),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('게임이 종료되었습니다.', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
//
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
},
style: ElevatedButton.styleFrom(backgroundColor: Colors.black),
child: const Text('메인으로', style: TextStyle(color: Colors.white)),
child: Column(
children: [
//
Container(
color: Colors.black,
width: double.infinity,
padding: const EdgeInsets.all(8),
child: Center(
child: Text(
'$teamName 팀 (점수: $tScore)',
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
),
),
//
Container(
padding: const EdgeInsets.all(8),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(
children: members.map((m) => _buildTeamMember(m)).toList(),
),
),
),
],
),
);
}
///
Widget _buildTeamMember(Map<String, dynamic> user) {
final score = (user['score'] ?? 0) as int;
final nickname = user['nickname'] ?? '유저';
final profileImg = user['profile_img'] ?? '';
return GestureDetector(
onTap: () => _onTapUser(user),
child: Container(
width: 60,
margin: const EdgeInsets.only(right: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('$score', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 2),
Container(
width: 30, height: 30,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.black),
),
child: ClipOval(
child: Image.network(
'https://eldsoft.com:8097/images$profileImg',
fit: BoxFit.cover,
errorBuilder: (ctx, err, st) => const Center(
child: Text('ERR', style: TextStyle(fontSize: 8)),
),
),
),
),
const SizedBox(height: 2),
Text(nickname, style: const TextStyle(fontSize: 11), overflow: TextOverflow.ellipsis),
],
),
),
);
}
/// (ADMIN)
Widget _buildAdminList() {
final adminList = _adminList;
if (adminList.isEmpty) return const SizedBox();
return Container(
width: double.infinity,
margin: const EdgeInsets.only(top: 16),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.deepPurple),
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
Container(
color: Colors.black,
width: double.infinity,
padding: const EdgeInsets.all(8),
child: const Center(
child: Text('사회자', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
),
),
ListView.builder(
shrinkWrap: true,
primary: false,
itemCount: adminList.length,
itemBuilder: (ctx, i) {
final user = adminList[i];
return _buildAdminItem(user);
},
),
],
),
);
}
Widget _buildAdminItem(Map<String, dynamic> user) {
final nickname = user['nickname'] ?? '사회자';
final profileImg = user['profile_img'] ?? '';
return ListTile(
onTap: () => _onTapUser(user),
leading: CircleAvatar(
backgroundColor: Colors.white,
backgroundImage: (profileImg.isNotEmpty)
? NetworkImage('https://eldsoft.com:8097/images$profileImg')
: null,
child: (profileImg.isEmpty)
? const Text('NoImg', style: TextStyle(fontSize: 10))
: null,
),
title: Text(nickname),
subtitle: const Text('사회자'),
);
}
Future<void> _onTapUser(Map<String, dynamic> userData) async {
//
await showDialog(
context: context,
barrierDismissible: false,
builder: (_) => UserInfoFinishDialog(userData: userData),
);
}
@override
Widget build(BuildContext context) {
final teamNames = _teamMap.keys.toList();
//
return WillPopScope(
onWillPop: _onWillPop,
child: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.black,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
onPressed: _onWillPop,
),
title: Text(_roomTitle, style: const TextStyle(color: Colors.white)),
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
children: [
// +
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
ElevatedButton(
onPressed: _openRoomSettingDialog,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
side: const BorderSide(color: Colors.black),
),
child: const Text('방 정보 보기', style: TextStyle(color: Colors.black)),
),
_buildGameTimeWidget(),
],
),
const SizedBox(height: 16),
//
for (int i = 0; i < teamNames.length; i++)
_buildTeamBox(teamNames[i], i),
//
_buildAdminList(),
],
),
),
),
);
}
}

View File

@ -1,12 +1,24 @@
import 'package:flutter/material.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart'; // AdMob
// import...
import '../../dialogs/settings_dialog.dart';
import 'create_room_page.dart';
//
import 'room_search_home_page.dart';
//
import 'playing_private_page.dart';
import 'playing_team_page.dart';
// : API &
import '../../plugins/api.dart';
import '../../dialogs/response_dialog.dart';
//
import 'package:fluttertoast/fluttertoast.dart'; // Toast
class MainPage extends StatefulWidget {
const MainPage({Key? key}) : super(key: key);
@ -15,25 +27,151 @@ class MainPage extends StatefulWidget {
}
class _MainPageState extends State<MainPage> {
bool _isBackButtonVisible = false; //
/// (1)
BannerAd? _bannerAd;
bool _isBannerReady = false; //
//
DateTime? _lastPressedTime;
// : 2
static const _exitDuration = Duration(seconds: 2);
@override
void initState() {
super.initState();
_isBackButtonVisible = false;
// (A) FRD
FirebaseDatabase.instance.goOffline();
// (B)
WidgetsBinding.instance.addPostFrameCallback((_) {
_checkForcedExitStatus();
});
// (C)
_initBannerAd();
}
///
void _initBannerAd() {
_bannerAd = BannerAd(
size: AdSize.banner, //
// adUnitId: 'ca-app-pub-3940256099942544/6300978111' ()
adUnitId: 'ca-app-pub-3940256099942544/6300978111', // : ID
listener: BannerAdListener(
onAdLoaded: (Ad ad) {
setState(() => _isBannerReady = true);
debugPrint('배너 광고 로드 완료');
},
onAdFailedToLoad: (Ad ad, LoadAdError err) {
debugPrint('배너 광고 로드 실패: $err');
ad.dispose();
},
),
request: const AdRequest(),
);
// load()
_bannerAd?.load();
}
@override
void dispose() {
_bannerAd?.dispose(); //
super.dispose();
}
Future<bool> _onWillPop() async {
final now = DateTime.now();
if (_lastPressedTime == null ||
now.difference(_lastPressedTime!) > _exitDuration) {
// or
_lastPressedTime = now;
// (Toast )
Fluttertoast.showToast(
msg: '한 번 더 누르면 앱이 종료됩니다.',
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
);
return false; // pop
}
// 2
return true; // pop (Scaffold , )
}
/// (B) "강제 종료 여부"
Future<void> _checkForcedExitStatus() async {
try {
final Map<String, dynamic> requestBody = {};
final response = await Api.serverRequest(
uri: '/room/score/enter/running/room',
body: requestBody,
);
if (response['result'] == 'OK') {
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
final data = resp['data'] ?? {};
final forceExitYn = (data['force_exit_yn'] ?? 'N').toString().toUpperCase();
if (forceExitYn == 'Y') {
final int roomSeq = data['room_seq'] ?? 0;
final String roomType = (data['room_type_name'] ?? '').toString().toUpperCase();
final String roomTitle = (data['room_title'] ?? '').toString();
// (1) FRD
FirebaseDatabase.instance.goOnline();
showResponseDialog(context, '게임 재입장', '강제 종료 된 게임에 재입장 합니다.');
// (2) pushReplacement
if (roomType == 'TEAM') {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => PlayingTeamPage(
roomSeq: roomSeq,
roomTitle: roomTitle.isNotEmpty ? roomTitle : '재입장 (팀전)',
),
),
);
} else {
//
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => PlayingPrivatePage(
roomSeq: roomSeq,
roomTitle: roomTitle.isNotEmpty ? roomTitle : '재입장 (개인전)',
),
),
);
}
}
} else {
final msgTitle = resp['response_info']?['msg_title'] ?? '오류';
final msgContent = resp['response_info']?['msg_content'] ?? '강제 종료 여부 확인 실패';
showResponseDialog(context, msgTitle, msgContent);
}
} else {
showResponseDialog(context, '서버 오류', '강제 종료 여부 확인에 실패했습니다.');
}
} catch (e) {
showResponseDialog(context, '서버 오류', '강제 종료 여부 확인에 실패했습니다.');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
// (A) /
return WillPopScope(
onWillPop: _onWillPop,
child: Scaffold(
backgroundColor: Colors.white,
// (B) AppBar: ,
appBar: AppBar(
backgroundColor: Colors.black,
elevation: 0,
automaticallyImplyLeading: false, //
automaticallyImplyLeading: false, // X
title: const Text(
'ALLSCORE',
style: TextStyle(color: Colors.white),
@ -42,17 +180,16 @@ class _MainPageState extends State<MainPage> {
IconButton(
icon: const Icon(Icons.settings, color: Colors.white),
onPressed: () {
showSettingsDialog(context); //
showSettingsDialog(context);
},
),
],
),
// (C) :
body: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// ( / )
//
Expanded(
child: Padding(
padding: const EdgeInsets.all(16.0),
@ -60,7 +197,6 @@ class _MainPageState extends State<MainPage> {
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// (C1)
_buildBlackWhiteButton(
label: '방만들기',
onTap: () {
@ -71,10 +207,9 @@ class _MainPageState extends State<MainPage> {
},
),
const SizedBox(width: 16),
// (C2) => RoomSearchHomePage로
_buildBlackWhiteButton(
label: '참여하기',
onTap: () async {
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const RoomSearchHomePage()),
@ -87,42 +222,38 @@ class _MainPageState extends State<MainPage> {
),
),
// (D)
Container(
color: Colors.white,
padding: const EdgeInsets.only(bottom: 24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
height: 50,
width: 300,
color: Colors.grey.shade400,
child: const Center(
child: Text(
'구글 광고',
style: TextStyle(color: Colors.black),
),
),
),
],
//
// () Container(...) _bannerAd
if (_isBannerReady && _bannerAd != null)
Container(
width: _bannerAd!.size.width.toDouble(),
height: _bannerAd!.size.height.toDouble(),
alignment: Alignment.center,
child: AdWidget(ad: _bannerAd!),
)
else
//
Container(
width: 300,
height: 50,
color: Colors.grey.shade400,
alignment: Alignment.center,
child: const Text(
'광고 로딩중',
style: TextStyle(color: Colors.black),
),
),
),
// (E) :
//
Center(
child: OutlinedButton(
onPressed: () {
// (15 )
// debugging용
// ( )
// TODO
},
style: OutlinedButton.styleFrom(
backgroundColor: Colors.white,
side: const BorderSide(color: Colors.black54, width: 1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(40)),
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 60),
),
child: const Text(
@ -131,13 +262,13 @@ class _MainPageState extends State<MainPage> {
),
),
),
const SizedBox(height: 16),
],
const SizedBox(height: 16),
],
),
),
);
}
/// +
Widget _buildBlackWhiteButton({
required String label,
required VoidCallback onTap,
@ -149,9 +280,7 @@ class _MainPageState extends State<MainPage> {
foregroundColor: Colors.black,
side: const BorderSide(color: Colors.black, width: 1),
padding: const EdgeInsets.symmetric(vertical: 36, horizontal: 32),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(20)),
),
child: Text(label, style: const TextStyle(color: Colors.black)),
);

View File

@ -3,14 +3,11 @@ import 'package:firebase_database/firebase_database.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'main_page.dart';
import 'finish_private_page.dart'; // ()
import 'finish_private_page.dart'; //
import '../../plugins/api.dart';
import '../../dialogs/response_dialog.dart';
//
import '../../dialogs/score_edit_dialog.dart';
// (/X)
import '../../dialogs/user_info_basic_dialog.dart';
import '../../dialogs/score_edit_dialog.dart'; //
import '../../dialogs/user_info_basic_dialog.dart'; //
class PlayingPrivatePage extends StatefulWidget {
final int roomSeq;
@ -27,7 +24,6 @@ class PlayingPrivatePage extends StatefulWidget {
}
class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
// FRD
late DatabaseReference _roomRef;
Stream<DatabaseEvent>? _roomStream;
@ -35,21 +31,20 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
String roomTitle = '';
int myScore = 0;
// (ADMIN )
List<Map<String, dynamic>> _scoreList = [];
bool _isLoading = true;
// user_seq
String mySeq = '0';
// userListMap
// userListMap: { userSeq: true/false }
Map<String, bool> _userListMap = {};
@override
void initState() {
super.initState();
// (1) FRD
FirebaseDatabase.instance.goOnline();
roomTitle = widget.roomTitle;
_initFirebase();
}
@ -79,18 +74,14 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
}
final data = snapshot.value as Map<dynamic, dynamic>? ?? {};
final roomInfoData = data['roomInfo'] as Map<dynamic, dynamic>? ?? {};
final userInfoData = data['userInfo'] as Map<dynamic, dynamic>? ?? {};
final userListData = data['userList'] as Map<dynamic, dynamic>?;
//
final roomStatus = (roomInfoData['room_status'] ?? 'WAIT').toString().toUpperCase();
// FINISH라면 =>
if (roomStatus == 'FINISH') {
// ->
// ( )
//
if (mounted) {
Navigator.pushReplacement(
context,
@ -117,7 +108,7 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
});
}
//
//
final List<Map<String, dynamic>> rawList = [];
userInfoData.forEach((uSeq, uData) {
rawList.add({
@ -133,10 +124,10 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
});
//
int tempMyScore = 0;
for (var u in rawList) {
if ((u['is_my_score'] ?? 'N') == 'Y') {
tempMyScore = u['score'] ?? 0;
int tmpMyScore = 0;
for (var user in rawList) {
if ((user['is_my_score'] ?? 'N') == 'Y') {
tmpMyScore = user['score'] ?? 0;
}
}
@ -149,8 +140,9 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
return scoreB.compareTo(scoreA);
});
myScore = tempMyScore;
myScore = tmpMyScore;
_scoreList = playerList;
_isLoading = false;
});
}, onError: (err) {
@ -161,11 +153,104 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
});
}
/// (A) WillPopScope + AppBar leading
Future<bool> _onBackPressed() async {
// ? => API
/// Finish API
Future<void> _requestFinish() async {
final reqBody = {
"room_seq": "${widget.roomSeq}",
"room_type": "PRIVATE",
};
try {
await Api.serverRequest(uri: '/room/score/game/finish', body: reqBody);
} catch (e) {
// ignore
}
}
///
Future<bool> _onWillPop() async {
if (roomMasterYn == 'Y') {
// ->
final confirm = await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (_) {
return AlertDialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(color: Colors.black, width: 1),
),
title: const Text('나가기', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
content: const Text(
'방장이 나가면 게임이 종료됩니다.\n정말 나가시겠습니까?',
style: TextStyle(fontSize: 14),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
style: TextButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
),
child: const Text('취소'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
style: TextButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
),
child: const Text('종료'),
),
],
);
},
);
if (confirm != true) return false;
// Finish API
await _requestFinish();
} else {
//
final confirm = await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (_) {
return AlertDialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(color: Colors.black, width: 1),
),
title: const Text('나가기', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
content: const Text(
'진행중인 방에서 나가면 재입장이 불가능합니다.\n정말 나가시겠습니까?',
style: TextStyle(fontSize: 14),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
style: TextButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
),
child: const Text('취소'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
style: TextButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
),
child: const Text('나가기'),
),
],
);
},
);
if (confirm != true) return false;
}
// userList => false
@ -177,36 +262,17 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
return false;
}
/// (B) "게임 종료"
Future<void> _requestFinish() async {
final reqBody = {
"room_seq": "${widget.roomSeq}",
"room_type": "PRIVATE",
};
try {
final resp = await Api.serverRequest(
uri: '/room/score/game/finish',
body: reqBody,
);
// OK / FAIL
// room_status = FINISH => FRD에서 ->
} catch (e) {
//
print('게임 종료 API 에러: $e');
}
}
/// (C)
///
Widget _buildScoreItem(Map<String, dynamic> user) {
final userSeq = user['user_seq'].toString();
final score = user['score'] ?? 0;
final score = user['score'] ?? 0;
final nickname = user['nickname'] ?? '유저';
final bool isActive = _userListMap[userSeq] ?? true;
final hasExited = !isActive;
return GestureDetector(
onTap: () => _onUserTapped(user),
onTap: () => _onTapUser(user),
child: Container(
width: 60,
margin: const EdgeInsets.all(4),
@ -214,49 +280,43 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
mainAxisSize: MainAxisSize.min,
children: [
hasExited
? Text('X', style: TextStyle(fontSize: 20, color: Colors.redAccent, fontWeight: FontWeight.bold))
: Text('$score', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black)),
? Text('X', style: const TextStyle(fontSize: 20, color: Colors.redAccent, fontWeight: FontWeight.bold))
: Text('$score', style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 2),
Container(
width: 30,
height: 30,
width: 30, height: 30,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: hasExited ? Colors.redAccent : Colors.black),
),
child: hasExited
? Center(
? const Center(
child: Text('X', style: TextStyle(fontSize: 14, color: Colors.redAccent, fontWeight: FontWeight.bold)),
)
: ClipOval(
child: Image.network(
'https://eldsoft.com:8097/images${user['profile_img']}',
fit: BoxFit.cover,
errorBuilder: (ctx, err, st) => const Center(
child: Text('ERR', style: TextStyle(fontSize: 8, color: Colors.black)),
),
errorBuilder: (_, __, ___) => const Center(child: Text('ERR', style: TextStyle(fontSize: 8))),
),
),
),
const SizedBox(height: 2),
Text(
nickname,
style: TextStyle(fontSize: 11, color: hasExited ? Colors.redAccent : Colors.black),
overflow: TextOverflow.ellipsis,
),
Text(nickname,
style: TextStyle(fontSize: 11, color: hasExited ? Colors.redAccent : Colors.black),
overflow: TextOverflow.ellipsis),
],
),
),
);
}
Future<void> _onUserTapped(Map<String, dynamic> userData) async {
Future<void> _onTapUser(Map<String, dynamic> userData) async {
final pType = (userData['participant_type'] ?? '').toString().toUpperCase();
if (pType == 'ADMIN') {
//
//
await showDialog(
context: context,
barrierDismissible: false,
builder: (_) => ScoreEditDialog(
roomSeq: widget.roomSeq,
roomType: 'PRIVATE',
@ -264,10 +324,9 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
),
);
} else if (roomMasterYn == 'Y') {
// (PLAYER)
// (PLAYER)
await showDialog(
context: context,
barrierDismissible: false,
builder: (_) => ScoreEditDialog(
roomSeq: widget.roomSeq,
roomType: 'PRIVATE',
@ -275,10 +334,9 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
),
);
} else {
//
//
await showDialog(
context: context,
barrierDismissible: false,
builder: (_) => UserInfoBasicDialog(userData: userData),
);
}
@ -287,7 +345,7 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: _onBackPressed,
onWillPop: _onWillPop,
child: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
@ -295,7 +353,7 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
onPressed: _onBackPressed,
onPressed: () => _onWillPop(),
),
title: Text(
roomTitle.isNotEmpty ? roomTitle : '진행중 (개인전)',
@ -304,10 +362,7 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
actions: [
if (roomMasterYn == 'Y')
TextButton(
onPressed: () async {
//
await _requestFinish();
},
onPressed: _requestFinish,
child: const Text('게임종료', style: TextStyle(color: Colors.white)),
),
],
@ -319,31 +374,26 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
//
Container(
width: double.infinity,
color: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
children: [
const Text('내 점수', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black)),
const Text('내 점수', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Text('$myScore', style: const TextStyle(fontSize: 36, fontWeight: FontWeight.bold, color: Colors.black)),
Text('$myScore', style: const TextStyle(fontSize: 36, fontWeight: FontWeight.bold)),
],
),
),
const Divider(height: 1, color: Colors.black),
Expanded(
child: Container(
child: SingleChildScrollView(
padding: const EdgeInsets.all(8),
child: SingleChildScrollView(
child: Wrap(
spacing: 8,
runSpacing: 8,
children: _scoreList.map(_buildScoreItem).toList(),
),
child: Wrap(
spacing: 8,
runSpacing: 8,
children: _scoreList.map(_buildScoreItem).toList(),
),
),
),
Container(
height: 50,
decoration: BoxDecoration(
@ -351,7 +401,7 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
border: Border.all(color: Colors.black, width: 1),
),
child: const Center(
child: Text('구글 광고', style: TextStyle(color: Colors.black)),
child: Text('구글 광고'),
),
),
],

View File

@ -3,7 +3,7 @@ import 'package:firebase_database/firebase_database.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'main_page.dart';
import 'finish_team_page.dart'; // ()
import 'finish_team_page.dart'; //
import '../../plugins/api.dart';
import '../../dialogs/response_dialog.dart';
import '../../dialogs/score_edit_dialog.dart';
@ -37,15 +37,17 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
Map<String, List<Map<String, dynamic>>> _teamMap = {};
bool _isLoading = true;
String mySeq = '0';
// userListMap
// userListMap: { seq: true/false }
Map<String, bool> _userListMap = {};
@override
void initState() {
super.initState();
// (1) FRD
FirebaseDatabase.instance.goOnline();
roomTitle = widget.roomTitle;
_initFirebase();
}
@ -77,16 +79,13 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
}
final data = snapshot.value as Map<dynamic, dynamic>? ?? {};
final roomInfoData = data['roomInfo'] as Map<dynamic, dynamic>? ?? {};
final userInfoData = data['userInfo'] as Map<dynamic, dynamic>? ?? {};
final userListData = data['userList'] as Map<dynamic, dynamic>?;
// room_status
final roomStatus = (roomInfoData['room_status'] ?? 'WAIT').toString().toUpperCase();
// FINISH ->
if (roomStatus == 'FINISH') {
//
if (mounted) {
Navigator.pushReplacement(
context,
@ -97,14 +96,12 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
}
setState(() {
//
final masterSeq = roomInfoData['master_user_seq'];
roomMasterYn = (masterSeq != null && masterSeq.toString() == mySeq) ? 'Y' : 'N';
final newTitle = (roomInfoData['room_title'] ?? '') as String;
if (newTitle.isNotEmpty) roomTitle = newTitle;
// userListMap
_userListMap.clear();
if (userListData != null) {
userListData.forEach((k, v) {
@ -124,37 +121,36 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
});
});
// /
// &
int tmpMyScore = 0;
int tmpMyTeamScore = 0;
String myTeam = 'WAIT';
for (var user in rawList) {
final uSeq = user['user_seq'].toString();
final sc = (user['score'] ?? 0) as int;
final tName = user['team_name'] ?? 'WAIT';
final sc = (user['score'] ?? 0) as int;
final tName= (user['team_name'] ?? 'WAIT');
if (uSeq == mySeq) {
tmpMyScore = sc;
myTeam = tName;
}
}
//
for (var user in rawList) {
final tName = user['team_name'] ?? 'WAIT';
final sc = (user['score'] ?? 0) as int;
if (tName == myTeam && tName != 'WAIT') {
final sc = (user['score'] ?? 0) as int;
if (tName == myTeam && myTeam != 'WAIT') {
tmpMyTeamScore += sc;
}
}
// (ADMIN/WAIT )
// ADMIN, WAIT
final Map<String, List<Map<String, dynamic>>> tMap = {};
final Map<String, int> tScoreMap = {};
for (var user in rawList) {
final pType = user['participant_type'];
final tName = user['team_name'] ?? 'WAIT';
final tName = (user['team_name'] ?? 'WAIT');
if (pType == 'ADMIN') continue;
if (tName == 'WAIT') continue;
@ -162,7 +158,6 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
tMap[tName]!.add(user);
}
//
tMap.forEach((k, members) {
int sumScore = 0;
for (var m in members) {
@ -175,6 +170,7 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
myTeamScore = tmpMyTeamScore;
_teamMap = tMap;
_teamScoreMap = tScoreMap;
_isLoading = false;
});
}, onError: (err) {
@ -185,11 +181,102 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
});
}
/// (A) -> ? => Finish API
Future<bool> _onBackPressed() async {
if (roomMasterYn == 'Y') {
await _requestFinish();
///
Future<void> _requestFinish() async {
final body = {
"room_seq": "${widget.roomSeq}",
"room_type": "TEAM",
};
try {
await Api.serverRequest(uri: '/room/score/game/finish', body: body);
} catch (e) {
// ignore
}
}
Future<bool> _onWillPop() async {
if (roomMasterYn == 'Y') {
//
final confirm = await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (_) {
return AlertDialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(color: Colors.black, width: 1),
),
title: const Text('나가기', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
content: const Text(
'방장이 나가면 게임이 종료됩니다.\n정말 나가시겠습니까?',
style: TextStyle(fontSize: 14),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
style: TextButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
),
child: const Text('취소'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
style: TextButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
),
child: const Text('종료'),
),
],
);
},
);
if (confirm != true) return false;
await _requestFinish();
} else {
//
final confirm = await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (_) {
return AlertDialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(color: Colors.black, width: 1),
),
title: const Text('나가기', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
content: const Text(
'진행중인 방에서 나가면 재입장이 불가능합니다.\n정말 나가시겠습니까?',
style: TextStyle(fontSize: 14),
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
style: TextButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
),
child: const Text('취소'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
style: TextButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
),
child: const Text('나가기'),
),
],
);
},
);
if (confirm != true) return false;
}
// userList => false
final userRef = _roomRef.child('userList').child(mySeq);
await userRef.set(false);
@ -199,26 +286,10 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
return false;
}
Future<void> _requestFinish() async {
final body = {
"room_seq": "${widget.roomSeq}",
"room_type": "TEAM",
};
try {
final resp = await Api.serverRequest(
uri: '/room/score/game/finish',
body: body,
);
// result ...
} catch (e) {
print('finish API error: $e');
}
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: _onBackPressed,
onWillPop: _onWillPop,
child: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
@ -230,7 +301,7 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
onPressed: _onBackPressed,
onPressed: () => _onWillPop(),
),
actions: [
if (roomMasterYn == 'Y')
@ -244,7 +315,7 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
? const Center(child: CircularProgressIndicator())
: Column(
children: [
// /
// (A) /
Container(
color: Colors.white,
padding: const EdgeInsets.only(top: 16, bottom: 16),
@ -253,17 +324,17 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
children: [
Column(
children: [
const Text('내 점수', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black)),
const Text('내 점수', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Text('$myScore', style: const TextStyle(fontSize: 36, fontWeight: FontWeight.bold, color: Colors.black)),
Text('$myScore', style: const TextStyle(fontSize: 36, fontWeight: FontWeight.bold)),
],
),
Container(width: 1, height: 60, color: Colors.black),
Column(
children: [
const Text('우리 팀 점수', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black)),
const Text('우리 팀 점수', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Text('$myTeamScore', style: const TextStyle(fontSize: 36, fontWeight: FontWeight.bold, color: Colors.black)),
Text('$myTeamScore', style: const TextStyle(fontSize: 36, fontWeight: FontWeight.bold)),
],
),
],
@ -271,6 +342,7 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
),
const Divider(height: 1, color: Colors.black),
// (B)
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(8),
@ -280,6 +352,7 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
),
),
),
Container(
height: 50,
decoration: BoxDecoration(
@ -316,7 +389,8 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 8),
child: Center(
child: Text('$teamName (팀점수 $teamScore)', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
child: Text('$teamName (팀점수 $teamScore)',
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
),
),
Container(
@ -333,14 +407,14 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
Widget _buildTeamMemberItem(Map<String, dynamic> userData) {
final userSeq = userData['user_seq'].toString();
final score = userData['score'] ?? 0;
final nickname = userData['nickname'] ?? '유저';
final score = userData['score'] ?? 0;
final nickname= userData['nickname'] ?? '유저';
final bool isActive = _userListMap[userSeq] ?? true;
final hasExited = !isActive;
return GestureDetector(
onTap: () => _onUserTapped(userData),
onTap: () => _onTapUser(userData),
child: Container(
width: 60,
margin: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
@ -348,8 +422,8 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
mainAxisSize: MainAxisSize.min,
children: [
hasExited
? Text('X', style: TextStyle(fontSize: 18, color: Colors.redAccent, fontWeight: FontWeight.bold))
: Text('$score', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black)),
? Text('X', style: const TextStyle(fontSize: 18, color: Colors.redAccent, fontWeight: FontWeight.bold))
: Text('$score', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 2),
Container(
width: 30,
@ -359,12 +433,18 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
border: Border.all(color: hasExited ? Colors.redAccent : Colors.black),
),
child: hasExited
? Center(child: Text('X', style: TextStyle(fontSize: 14, color: Colors.redAccent, fontWeight: FontWeight.bold)))
? const Center(
child: Text(
'X',
style: TextStyle(fontSize: 14, color: Colors.redAccent, fontWeight: FontWeight.bold),
),
)
: ClipOval(
child: Image.network(
'https://eldsoft.com:8097/images${userData['profile_img']}',
fit: BoxFit.cover,
errorBuilder: (ctx, err, st) => const Center(child: Text('ERR', style: TextStyle(fontSize: 8, color: Colors.black))),
errorBuilder: (ctx, err, st) =>
const Center(child: Text('ERR', style: TextStyle(fontSize: 8))),
),
),
),
@ -380,13 +460,11 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
);
}
Future<void> _onUserTapped(Map<String, dynamic> userData) async {
Future<void> _onTapUser(Map<String, dynamic> userData) async {
final pType = (userData['participant_type'] ?? '').toString().toUpperCase();
if (pType == 'ADMIN') {
//
await showDialog(
context: context,
barrierDismissible: false,
builder: (_) => ScoreEditDialog(
roomSeq: widget.roomSeq,
roomType: 'TEAM',
@ -396,7 +474,6 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
} else if (roomMasterYn == 'Y') {
await showDialog(
context: context,
barrierDismissible: false,
builder: (_) => ScoreEditDialog(
roomSeq: widget.roomSeq,
roomType: 'TEAM',
@ -406,7 +483,6 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
} else {
await showDialog(
context: context,
barrierDismissible: false,
builder: (_) => UserInfoBasicDialog(userData: userData),
);
}

View File

@ -1,14 +1,15 @@
import 'package:flutter/material.dart';
import 'dart:async';
import '../../plugins/api.dart'; // (: Api.serverRequest)
import '../../dialogs/response_dialog.dart'; //
import '../../dialogs/room_detail_dialog.dart'; // import
import '../../plugins/api.dart';
import '../../dialogs/response_dialog.dart';
import '../../dialogs/room_detail_dialog.dart';
// ( import)
//
import '../room/finish_private_page.dart';
import '../room/finish_team_page.dart';
/// /
/// - roomStatus: "WAIT"/"RUNNING"/"FINISH"
/// - 1 10 ,
/// - room_title
class RoomSearchListPage extends StatefulWidget {
final String roomStatus; // WAIT / RUNNING / FINISH
@ -21,7 +22,6 @@ class RoomSearchListPage extends StatefulWidget {
class _RoomSearchListPageState extends State<RoomSearchListPage> {
final TextEditingController _searchController = TextEditingController();
//
List<Map<String, dynamic>> _roomList = [];
bool _isLoading = false;
@ -47,7 +47,6 @@ class _RoomSearchListPageState extends State<RoomSearchListPage> {
super.dispose();
}
///
void _onScroll() {
if (!_scrollController.hasClients) return;
final thresholdPixels = 200;
@ -59,7 +58,6 @@ class _RoomSearchListPageState extends State<RoomSearchListPage> {
}
}
/// (1)
Future<void> _fetchRoomList({required bool isRefresh}) async {
if (_isLoading) return;
if (!isRefresh && !_hasMore) return;
@ -72,7 +70,6 @@ class _RoomSearchListPageState extends State<RoomSearchListPage> {
_roomList.clear();
}
// API WAIT/RUNNING/FINISH ()
final String searchType = widget.roomStatus.toUpperCase();
final String searchValue = _searchController.text.trim();
final String searchPage = _currentPage.toString();
@ -84,14 +81,8 @@ class _RoomSearchListPageState extends State<RoomSearchListPage> {
};
try {
final response = await Api.serverRequest(
uri: '/room/score/room/list',
body: requestBody,
);
final response = await Api.serverRequest(uri: '/room/score/room/list', body: requestBody);
print('🔵 response: $response');
// () : { result: OK, response: {...}, ... }
if (response == null || response['result'] != 'OK') {
showResponseDialog(context, '오류', '방 목록을 불러오지 못했습니다.');
} else {
@ -105,13 +96,14 @@ class _RoomSearchListPageState extends State<RoomSearchListPage> {
_hasMore = false;
} else {
for (var item in respData) {
print('🔵 item: $item');
final parsedItem = {
'room_seq': item['room_seq'] ?? 0,
'nickname': item['nickname'] ?? '사용자',
// WAIT/RUNNING/FINISH ->
'room_status': _statusToKr(item['room_status'] ?? ''),
'raw_room_status': (item['room_status'] ?? '').toString().toUpperCase(),
'open_yn': (item['open_yn'] == 'Y') ? '공개' : '비공개',
'room_type': item['room_type_name'] ?? 'private',
'room_type': (item['room_type_name'] ?? 'PRIVATE').toString().toLowerCase(),
'room_title': item['room_title'] ?? '(방제목 없음)',
'room_intro': item['room_intro'] ?? '',
'now_people': item['now_number_of_people']?.toString() ?? '0',
@ -137,7 +129,6 @@ class _RoomSearchListPageState extends State<RoomSearchListPage> {
}
}
/// WAIT->'대기중', RUNNING->'진행중', FINISH->'종료'
String _statusToKr(String status) {
switch (status.toUpperCase()) {
case 'WAIT':
@ -156,12 +147,42 @@ class _RoomSearchListPageState extends State<RoomSearchListPage> {
}
void _onRoomItemTap(Map<String, dynamic> item) {
//
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => RoomDetailDialog(roomData: item),
);
// room_status() , raw_room_status( WAIT/RUNNING/FINISH)
final rawStatus = (item['raw_room_status'] ?? '').toString().toUpperCase();
if (rawStatus == 'FINISH') {
// => FinishPrivatePage or FinishTeamPage
final roomType = (item['room_type'] ?? 'private').toString().toLowerCase();
final roomSeq = (item['room_seq'] ?? 0) as int;
final roomTitle = (item['room_title'] ?? '(종료된 방)') as String;
if (roomType == 'private') {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => FinishPrivatePage(
roomSeq: roomSeq,
fromPlayingPage: false, // false
),
),
);
} else {
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => FinishTeamPage(
roomSeq: roomSeq,
fromPlayingPage: false,
),
),
);
}
} else {
// or => , : RoomDetailDialog
showDialog(
context: context,
builder: (_) => RoomDetailDialog(roomData: item),
);
}
}
@override
@ -180,7 +201,7 @@ class _RoomSearchListPageState extends State<RoomSearchListPage> {
),
body: Column(
children: [
// (A)
//
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
@ -206,14 +227,12 @@ class _RoomSearchListPageState extends State<RoomSearchListPage> {
),
),
// (B) or
Expanded(
child: _isLoading && _roomList.isEmpty
? const Center(child: CircularProgressIndicator())
: _buildRoomListView(),
),
// (C)
Container(
height: 60,
color: Colors.white,
@ -234,16 +253,11 @@ class _RoomSearchListPageState extends State<RoomSearchListPage> {
}
Widget _buildRoomListView() {
print('🔵 _roomList: $_roomList');
if (_roomList.isEmpty) {
return const Center(
child: Text(
'검색 결과가 없습니다.',
style: TextStyle(color: Colors.black),
),
child: Text('검색 결과가 없습니다.', style: TextStyle(color: Colors.black)),
);
}
return ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),

View File

@ -4,9 +4,9 @@ import 'package:firebase_database/firebase_database.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'main_page.dart';
import '../../plugins/api.dart'; // API
import '../../plugins/api.dart';
import '../../dialogs/response_dialog.dart';
import '../../dialogs/yes_no_dialog.dart'; // /
import '../../dialogs/yes_no_dialog.dart';
import '../../dialogs/room_setting_dialog.dart';
import '../../dialogs/user_info_private_dialog.dart';
import 'playing_private_page.dart';
@ -38,46 +38,57 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
int numberOfPeople = 10;
String scoreOpenRange = 'PRIVATE';
// FRD
late DatabaseReference _roomRef;
Stream<DatabaseEvent>? _roomStream;
StreamSubscription<DatabaseEvent>? _roomStreamSubscription;
//
List<Map<String, dynamic>> _userList = [];
bool _isLoading = true;
// FRD
late DatabaseReference _roomRef;
Stream<DatabaseEvent>? _roomStream;
//
bool _movedToRunningPage = false;
//
bool _kickedOut = false;
// FRD
StreamSubscription<DatabaseEvent>? _roomStreamSubscription;
// user_seq
String mySeq = '0';
// () user_seq
String mySeq = '0'; // '6'
//
// 1
//
Timer? _countdownTimer;
Duration _remaining = const Duration(hours: 1); // 1
DateTime? _createDt; // FRD의 roomInfo.create_dt
@override
void initState() {
super.initState();
_loadMySeq();
// FRD
FirebaseDatabase.instance.goOnline();
_initRoomRef();
}
/// (A) my_user_seq ->
Future<void> _loadMySeq() async {
Future<void> _initRoomRef() async {
final prefs = await SharedPreferences.getInstance();
mySeq = prefs.getInt('my_user_seq')?.toString() ?? '0';
final roomKey = 'korea-${widget.roomSeq}';
_roomRef = FirebaseDatabase.instance.ref('rooms/$roomKey');
// onDisconnect + connect_yn='Y'
final myUserRef = _roomRef.child('userInfo').child(mySeq);
myUserRef.onDisconnect().update({'connect_yn': 'N'});
await myUserRef.update({'connect_yn': 'Y'});
_listenRoomData();
}
void _listenRoomData() {
_roomStream = _roomRef.onValue;
_roomStream?.listen((event) {
_roomStreamSubscription = _roomStream?.listen((event) {
final snapshot = event.snapshot;
if (!snapshot.exists) {
setState(() {
@ -94,12 +105,13 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
final roomStatus = (roomInfoData['room_status'] ?? 'WAIT').toString().toUpperCase();
// (A) roomInfo
setState(() {
roomTitle = (roomInfoData['room_title'] ?? '') as String;
roomIntro = (roomInfoData['room_intro'] ?? '') as String;
openYn = (roomInfoData['open_yn'] ?? 'Y') as String;
roomPw = (roomInfoData['room_pw'] ?? '') as String;
runningTime = _toInt(roomInfoData['running_time'], 1);
roomTitle = (roomInfoData['room_title'] ?? '') as String;
roomIntro = (roomInfoData['room_intro'] ?? '') as String;
openYn = (roomInfoData['open_yn'] ?? 'Y') as String;
roomPw = (roomInfoData['room_pw'] ?? '') as String;
runningTime = _toInt(roomInfoData['running_time'], 1);
numberOfPeople = _toInt(roomInfoData['number_of_people'], 10);
scoreOpenRange = (roomInfoData['score_open_range'] ?? 'PRIVATE') as String;
@ -110,7 +122,7 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
roomMasterYn = 'Y';
}
//
// userList
final tempList = <Map<String, dynamic>>[];
userInfoData.forEach((userSeq, userMap) {
tempList.add({
@ -119,16 +131,16 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
'nickname': userMap['nickname'] ?? '유저',
'score': userMap['score'] ?? 0,
'profile_img': userMap['profile_img'] ?? '',
'department': userMap['department'] ?? '',
'introduce_myself': userMap['introduce_myself'] ?? '',
'ready_yn': (userMap['ready_yn'] ?? 'N').toString().toUpperCase(),
'connect_yn': (userMap['connect_yn'] ?? 'Y').toString().toUpperCase(),
});
});
_userList = tempList;
_isLoading = false;
});
// ->
// (B) =>
if (roomStatus == 'RUNNING' && !_movedToRunningPage) {
_movedToRunningPage = true;
Navigator.pushReplacement(
@ -143,20 +155,21 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
return;
}
// (2) "내 user_seq가 목록에 있는지"
// (C) 1 create_dt
// : "2025-01-07T06:38:10.123456"
final createDtStr = (roomInfoData['create_dt'] ?? '') as String;
if (createDtStr.isNotEmpty && createDtStr.contains('T')) {
final dt = DateTime.tryParse(createDtStr);
if (dt != null) {
_createDt = dt;
_startCountdownTimer();
}
}
// (D) =>
final amIStillHere = _userList.any((u) => u['user_seq'].toString() == mySeq);
// (3) ,
// (_kickedOut == false),
// (roomMasterYn != 'Y'),
// / => "강퇴"
if (!amIStillHere && !_kickedOut && roomMasterYn != 'Y') {
// RUNNING이면 ,
// DELETE
_kickedOut = true; //
// () +
_kickedOut = true;
Future.delayed(Duration.zero, () async {
await showResponseDialog(context, '안내', '강퇴되었습니다.');
Navigator.pushReplacement(
@ -166,7 +179,6 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
});
}
}, onError: (error) {
print('FRD onError: $error');
setState(() {
_isLoading = false;
roomTitle = '오류 발생';
@ -174,13 +186,64 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
});
}
// 1
void _startCountdownTimer() {
if (_countdownTimer != null && _countdownTimer!.isActive) {
return; //
}
if (_createDt == null) return;
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
// : createDt + 1
final endTime = _createDt!.add(const Duration(hours: 1));
final now = DateTime.now();
final diff = endTime.difference(now);
if (diff.isNegative) {
// ->
timer.cancel();
_remaining = const Duration(seconds: 0);
_onAutoTimeout();
} else {
setState(() {
_remaining = diff;
});
}
});
}
// 1
void _onAutoTimeout() {
// => (leave API)
// =>
if (roomMasterYn == 'Y') {
_requestLeaveRoom();
} else {
_requestLeaveRoom();
}
}
Future<void> _requestLeaveRoom() async {
try {
final reqBody = {"room_seq": "${widget.roomSeq}"};
final response = await Api.serverRequest(uri: '/room/score/game/leave', body: reqBody);
// result ok ->
} catch (e) {
//
}
if (mounted) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
}
}
@override
void dispose() {
_roomStreamSubscription?.cancel(); //
_countdownTimer?.cancel();
_roomStreamSubscription?.cancel();
super.dispose();
}
/// (B) ->
///
Future<void> _onLeaveRoom() async {
if (roomMasterYn == 'Y') {
//
@ -195,15 +258,9 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
side: const BorderSide(color: Colors.black, width: 2),
),
title: const Center(
child: Text(
'방 나가기',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black),
),
),
content: const Text(
'방장이 나가면 방은 삭제됩니다.\n정말 나가시겠습니까?',
style: TextStyle(fontSize: 14, color: Colors.black),
child: Text('방 나가기', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
),
content: const Text('방장이 나가면 방은 삭제됩니다.\n정말 나가시겠습니까?', style: TextStyle(fontSize: 14)),
actionsAlignment: MainAxisAlignment.spaceEvenly,
actions: [
TextButton(
@ -211,17 +268,19 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
style: TextButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('취소'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
onPressed: () {
final myUserRef = _roomRef.child('userInfo').child(mySeq);
myUserRef.onDisconnect().cancel();
Navigator.pop(context, true);
},
style: TextButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('확인'),
@ -233,75 +292,10 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
if (confirm != true) return;
// leave API
try {
final reqBody = {"room_seq": "${widget.roomSeq}"};
final response = await Api.serverRequest(uri: '/room/score/game/leave', body: reqBody);
if (response['result'] == 'OK') {
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
} else {
final msg = resp['response_info']?['msg_content'] ?? '방 나가기 실패';
final again = await showYesNoDialog(
context: context,
title: '오류',
message: '$msg\n그래도 나가시겠습니까?',
yesNo: true,
);
if (again == true) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
}
}
} else {
final again = await showYesNoDialog(
context: context,
title: '오류',
message: '서버오류\n그래도 나가시겠습니까?',
yesNo: true,
);
if (again == true) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
}
}
} catch (e) {
final again = await showYesNoDialog(
context: context,
title: '오류',
message: '$e\n그래도 나가시겠습니까?',
yesNo: true,
);
if (again == true) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
}
}
await _requestLeaveRoom();
} else {
//
try {
final reqBody = {"room_seq": "${widget.roomSeq}"};
final response = await Api.serverRequest(uri: '/room/score/game/leave', body: reqBody);
if (response['result'] == 'OK') {
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
} else {
final msg = resp['response_info']?['msg_content'] ?? '나가기 실패';
final again = await showYesNoDialog(context: context, title: '오류', message: msg, yesNo: true);
if (again == true) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
}
}
} else {
final again = await showYesNoDialog(context: context, title: '오류', message: '서버 통신 오류', yesNo: true);
if (again == true) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
}
}
} catch (e) {
final again = await showYesNoDialog(context: context, title: '오류', message: '$e', yesNo: true);
if (again == true) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
}
}
await _requestLeaveRoom();
}
}
@ -314,17 +308,14 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
return defaultVal;
}
/// (=3, =2)
///
Widget _buildTopButtons() {
if (_isLoading) return const SizedBox();
final me = _userList.firstWhere(
(u) => (u['user_seq'] ?? '0') == mySeq,
orElse: () => {},
);
final me = _userList.firstWhere((u) => (u['user_seq'] ?? '0') == mySeq, orElse: () => {});
final myReadyYn = (me['ready_yn'] ?? 'N').toString().toUpperCase();
final bool isReady = (myReadyYn == 'Y');
final String readyLabel = isReady ? '준비완료' : '준비';
final readyLabel = isReady ? '준비완료' : '준비';
final btnStyle = ElevatedButton.styleFrom(
backgroundColor: Colors.white,
@ -335,7 +326,6 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
if (roomMasterYn == 'Y') {
// => 3
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Container(
@ -372,7 +362,6 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
} else {
// => 2
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Container(
@ -399,15 +388,11 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
}
}
/// READY
Future<void> _onToggleReady() async {
try {
final me = _userList.firstWhere(
(u) => (u['user_seq'] ?? '0') == mySeq,
orElse: () => {},
);
final me = _userList.firstWhere((u) => (u['user_seq'] ?? '0') == mySeq, orElse: () => {});
final myReadyYn = (me['ready_yn'] ?? 'N').toString().toUpperCase();
final bool isReady = (myReadyYn == 'Y');
final isReady = (myReadyYn == 'Y');
final newYn = isReady ? 'N' : 'Y';
final userRef = _roomRef.child('userInfo').child(mySeq);
@ -417,7 +402,6 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
}
}
///
Future<void> _onOpenRoomSetting() async {
final roomInfo = {
"room_seq": "${widget.roomSeq}",
@ -442,12 +426,8 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
}
}
///
Future<void> _onGameStart() async {
final notReady = _userList.any((u) {
final ry = (u['ready_yn'] ?? 'N').toString().toUpperCase();
return (ry != 'Y');
});
final notReady = _userList.any((u) => (u['ready_yn'] ?? 'N').toString().toUpperCase() != 'Y');
if (notReady) {
showResponseDialog(context, '안내', 'READY되지 않은 참가자가 있습니다(방장 포함).');
return;
@ -457,37 +437,45 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
"room_seq": "${widget.roomSeq}",
"room_type": "PRIVATE",
};
try {
final response = await Api.serverRequest(
uri: '/room/score/game/start',
body: requestBody,
);
final response = await Api.serverRequest(uri: '/room/score/game/start', body: requestBody);
if (response['result'] == 'OK') {
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
print('게임 시작 요청 성공(개인전)');
} else {
final msgTitle = resp['response_info']?['msg_title'] ?? '오류';
final msgContent = resp['response_info']?['msg_content'] ?? '게임 시작 실패';
showResponseDialog(context, msgTitle, msgContent);
// ...
}
} else {
showResponseDialog(context, '실패', '서버 통신 오류');
// ...
}
} catch (e) {
showResponseDialog(context, '오류', '$e');
// ...
}
}
// ()
String _formatDuration(Duration d) {
final mm = d.inMinutes.remainder(60).toString().padLeft(2, '0');
final ss = d.inSeconds.remainder(60).toString().padLeft(2, '0');
return '$mm:$ss';
}
@override
Widget build(BuildContext context) {
// (: 60:00 ~ 0:00)
final countdownStr = _formatDuration(_remaining);
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.black,
elevation: 0,
title: const Text('대기 방 (개인전)', style: TextStyle(color: Colors.white)),
// +
title: Text(
(roomTitle.isNotEmpty ? roomTitle : '방 제목') + ' [$countdownStr]',
style: const TextStyle(color: Colors.white),
),
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
onPressed: _onLeaveRoom,
@ -510,24 +498,16 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Text(
roomTitle.isNotEmpty ? roomTitle : '방 제목',
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 8),
//
_buildTopButtons(),
const SizedBox(height: 20),
const Text('사회자', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black)),
const Text('사회자', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
_buildAdminSection(),
const SizedBox(height: 20),
const Text('참가자', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black)),
const Text('참가자', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
_buildPlayerSection(),
],
@ -536,7 +516,6 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
);
}
//
Widget _buildAdminSection() {
final adminList = _userList.where((u) {
final t = (u['participant_type'] ?? '').toString().toUpperCase();
@ -544,7 +523,6 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
}).toList();
return Container(
width: double.infinity,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white,
@ -552,7 +530,7 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
borderRadius: BorderRadius.circular(8),
),
child: adminList.isEmpty
? const Text('사회자가 없습니다.', style: TextStyle(color: Colors.black))
? const Text('사회자가 없습니다.')
: Wrap(
spacing: 16,
runSpacing: 8,
@ -561,7 +539,6 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
);
}
//
Widget _buildPlayerSection() {
final playerList = _userList.where((u) {
final t = (u['participant_type'] ?? '').toString().toUpperCase();
@ -569,7 +546,6 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
}).toList();
return Container(
width: double.infinity,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white,
@ -577,7 +553,7 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
borderRadius: BorderRadius.circular(8),
),
child: playerList.isEmpty
? const Text('참가자가 없습니다.', style: TextStyle(color: Colors.black))
? const Text('참가자가 없습니다.')
: SingleChildScrollView(
child: Wrap(
spacing: 16,
@ -589,13 +565,14 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
);
}
// Seat
Widget _buildSeat(Map<String, dynamic> userData) {
final userName = userData['nickname'] ?? '유저';
final profileImg = userData['profile_img'] ?? '';
final readyYn = userData['ready_yn'] ?? 'N';
final isReady = (readyYn == 'Y');
final isMaster = (roomMasterYn == 'Y');
final readyYn = (userData['ready_yn'] ?? 'N').toString().toUpperCase();
final connectYn = (userData['connect_yn'] ?? 'Y').toString().toUpperCase();
final bool isReady = (readyYn == 'Y');
final bool isDisconnected = (connectYn == 'N');
final bool isMaster = (roomMasterYn == 'Y');
return GestureDetector(
onTap: () async {
@ -616,7 +593,6 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
child: Container(
margin: const EdgeInsets.only(right: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 50,
@ -624,40 +600,55 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(
color: isReady ? Colors.red : Colors.black,
width: isReady ? 2 : 1,
color: isDisconnected
? Colors.orange
: (isReady ? Colors.red : Colors.black),
width: isDisconnected ? 2 : (isReady ? 2 : 1),
),
borderRadius: BorderRadius.circular(20),
boxShadow: isReady
? [
BoxShadow(
color: Colors.redAccent.withOpacity(0.6),
blurRadius: 8,
spreadRadius: 2,
offset: const Offset(0, 0),
)
]
: [],
boxShadow: [
if (isReady)
BoxShadow(
color: Colors.redAccent.withOpacity(0.6),
blurRadius: 8,
spreadRadius: 2,
),
if (isDisconnected)
BoxShadow(
color: Colors.orangeAccent.withOpacity(0.6),
blurRadius: 8,
spreadRadius: 2,
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Image.network(
'https://eldsoft.com:8097/images$profileImg',
fit: BoxFit.cover,
errorBuilder: (ctx, err, st) {
return const Center(
child: Text(
'이미지\n불가',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 10),
child: isDisconnected
? const Center(
child: Text(
'!',
style: TextStyle(
fontSize: 20,
color: Colors.orange,
fontWeight: FontWeight.bold,
),
),
)
: Image.network(
'https://eldsoft.com:8097/images$profileImg',
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => const Center(
child: Text(
'이미지\n불가',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 10),
),
),
),
);
},
),
),
),
const SizedBox(height: 4),
Text(userName, style: const TextStyle(fontSize: 12, color: Colors.black)),
Text(userName, style: const TextStyle(fontSize: 12)),
],
),
),

View File

@ -4,9 +4,9 @@ import 'package:firebase_database/firebase_database.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'main_page.dart';
import '../../plugins/api.dart'; // API
import '../../dialogs/response_dialog.dart'; //
import '../../dialogs/yes_no_dialog.dart'; // /
import '../../plugins/api.dart';
import '../../dialogs/response_dialog.dart';
import '../../dialogs/yes_no_dialog.dart';
import '../../dialogs/room_setting_dialog.dart';
import '../../dialogs/user_info_team_dialog.dart';
import '../../dialogs/team_name_edit_dialog.dart';
@ -27,9 +27,7 @@ class WaitingRoomTeamPage extends StatefulWidget {
}
class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
//
//
//
String roomMasterYn = 'N';
String roomTitle = '';
String roomIntro = '';
@ -40,57 +38,57 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
String scoreOpenRange = 'PRIVATE';
int numberOfTeams = 1;
//
List<String> _teamNameList = [];
//
List<Map<String, dynamic>> _userList = [];
bool _isLoading = true;
// FRD
late DatabaseReference _roomRef;
Stream<DatabaseEvent>? _roomStream;
//
bool _movedToRunningPage = false;
//
bool _kickedOut = false;
// FRD
StreamSubscription<DatabaseEvent>? _roomStreamSubscription;
// user_seq
String mySeq = '0'; // '6'
bool _movedToRunningPage = false;
bool _kickedOut = false;
String mySeq = '0';
// () 1
Timer? _countdownTimer;
Duration _remaining = const Duration(hours: 1);
DateTime? _createDt;
@override
void initState() {
super.initState();
_loadMySeq();
FirebaseDatabase.instance.goOnline();
_initRoomRef();
}
/// (A) user_seq를 +
Future<void> _loadMySeq() async {
Future<void> _initRoomRef() async {
final prefs = await SharedPreferences.getInstance();
// : getString or getInt
mySeq = prefs.getInt('my_user_seq')?.toString() ?? '0';
// roomKey / FRD
final roomKey = 'korea-${widget.roomSeq}';
_roomRef = FirebaseDatabase.instance.ref('rooms/$roomKey');
//
// onDisconnect + connect_yn='Y'
final myUserRef = _roomRef.child('userInfo').child(mySeq);
myUserRef.onDisconnect().update({'connect_yn': 'N'});
await myUserRef.update({'connect_yn': 'Y'});
_listenRoomData();
}
@override
void dispose() {
_roomStreamSubscription?.cancel(); //
_countdownTimer?.cancel();
_roomStreamSubscription?.cancel();
super.dispose();
}
void _listenRoomData() {
_roomStream = _roomRef.onValue;
_roomStream?.listen((event) async {
_roomStreamSubscription = _roomStream?.listen((event) {
final snapshot = event.snapshot;
if (!snapshot.exists) {
setState(() {
@ -105,20 +103,19 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
final roomInfoData = data['roomInfo'] as Map<dynamic, dynamic>? ?? {};
final userInfoData = data['userInfo'] as Map<dynamic, dynamic>? ?? {};
//
final roomStatus = (roomInfoData['room_status'] ?? 'WAIT').toString().toUpperCase();
setState(() {
roomTitle = (roomInfoData['room_title'] ?? '') as String;
roomIntro = (roomInfoData['room_intro'] ?? '') as String;
openYn = (roomInfoData['open_yn'] ?? 'Y') as String;
roomPw = (roomInfoData['room_pw'] ?? '') as String;
runningTime = _toInt(roomInfoData['running_time'], 1);
roomTitle = (roomInfoData['room_title'] ?? '') as String;
roomIntro = (roomInfoData['room_intro'] ?? '') as String;
openYn = (roomInfoData['open_yn'] ?? 'Y') as String;
roomPw = (roomInfoData['room_pw'] ?? '') as String;
runningTime = _toInt(roomInfoData['running_time'], 1);
numberOfPeople = _toInt(roomInfoData['number_of_people'], 10);
scoreOpenRange = (roomInfoData['score_open_range'] ?? 'PRIVATE') as String;
numberOfTeams = _toInt(roomInfoData['number_of_teams'], 1);
scoreOpenRange = (roomInfoData['score_open_range'] ?? 'PRIVATE') as String;
numberOfTeams = _toInt(roomInfoData['number_of_teams'], 1);
//
//
final tStr = (roomInfoData['team_name_list'] ?? '') as String;
if (tStr.isNotEmpty) {
_teamNameList = tStr.split(',').map((e) => e.trim().toUpperCase()).toList();
@ -126,14 +123,14 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
_teamNameList = List.generate(numberOfTeams, (i) => String.fromCharCode(65 + i));
}
//
//
roomMasterYn = 'N';
final masterSeq = roomInfoData['master_user_seq'];
if (masterSeq != null && masterSeq.toString() == mySeq) {
roomMasterYn = 'Y';
}
//
// userList
final tempList = <Map<String, dynamic>>[];
userInfoData.forEach((userSeq, userMap) {
tempList.add({
@ -143,16 +140,16 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
'team_name': userMap['team_name'] ?? '',
'score': userMap['score'] ?? 0,
'profile_img': userMap['profile_img'] ?? '',
'department': userMap['department'] ?? '',
'introduce_myself': userMap['introduce_myself'] ?? '',
'ready_yn': (userMap['ready_yn'] ?? 'N').toString().toUpperCase(),
'connect_yn': (userMap['connect_yn'] ?? 'Y').toString().toUpperCase(),
});
});
_userList = tempList;
_isLoading = false;
});
// RUNNING이면
// ->
if (roomStatus == 'RUNNING' && !_movedToRunningPage) {
_movedToRunningPage = true;
Navigator.pushReplacement(
@ -167,20 +164,20 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
return;
}
// (2) "내 user_seq가 목록에 있는지"
// (C) create_dt -> 1
final createDtStr = (roomInfoData['create_dt'] ?? '') as String;
if (createDtStr.isNotEmpty && createDtStr.contains('T')) {
final dt = DateTime.tryParse(createDtStr);
if (dt != null) {
_createDt = dt;
_startCountdownTimer();
}
}
//
final amIStillHere = _userList.any((u) => u['user_seq'].toString() == mySeq);
// (3) ,
// (_kickedOut == false),
// (roomMasterYn != 'Y'),
// / => "강퇴"
if (!amIStillHere && !_kickedOut && roomMasterYn != 'Y') {
// RUNNING이면 ,
// DELETE
_kickedOut = true; //
// () +
_kickedOut = true;
Future.delayed(Duration.zero, () async {
await showResponseDialog(context, '안내', '강퇴되었습니다.');
Navigator.pushReplacement(
@ -190,7 +187,6 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
});
}
}, onError: (error) {
print('FRD onError: $error');
setState(() {
_isLoading = false;
roomTitle = '오류 발생';
@ -198,12 +194,51 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
});
}
//
// [] ->
//
//
void _startCountdownTimer() {
if (_countdownTimer != null && _countdownTimer!.isActive) {
return;
}
if (_createDt == null) return;
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
final endTime = _createDt!.add(const Duration(hours: 1));
final now = DateTime.now();
final diff = endTime.difference(now);
if (diff.isNegative) {
timer.cancel();
_remaining = const Duration(seconds: 0);
_onAutoTimeout();
} else {
setState(() {
_remaining = diff;
});
}
});
}
void _onAutoTimeout() {
// -> =(), =
_requestLeaveRoom();
}
Future<void> _requestLeaveRoom() async {
try {
final reqBody = {"room_seq": "${widget.roomSeq}"};
final response = await Api.serverRequest(uri: '/room/score/game/leave', body: reqBody);
// ...
} catch (e) {
// ...
}
if (mounted) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
}
}
// ->
Future<void> _onLeaveRoom() async {
if (roomMasterYn == 'Y') {
// ->
final confirm = await showDialog<bool>(
context: context,
barrierDismissible: false,
@ -215,15 +250,9 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
side: const BorderSide(color: Colors.black, width: 2),
),
title: const Center(
child: Text(
'방 나가기',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black),
),
),
content: const Text(
'방장이 나가면 방은 삭제됩니다.\n정말 나가시겠습니까?',
style: TextStyle(fontSize: 14, color: Colors.black),
child: Text('방 나가기', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black)),
),
content: const Text('방장이 나가면 방은 삭제됩니다.\n정말 나가시겠습니까?', style: TextStyle(fontSize: 14)),
actionsAlignment: MainAxisAlignment.spaceEvenly,
actions: [
TextButton(
@ -231,17 +260,19 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
style: TextButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('취소'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
onPressed: () {
final myUserRef = _roomRef.child('userInfo').child(mySeq);
myUserRef.onDisconnect().cancel();
Navigator.pop(context, true);
},
style: TextButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('확인'),
@ -252,75 +283,9 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
);
if (confirm != true) return;
try {
final reqBody = {"room_seq": "${widget.roomSeq}"};
final response = await Api.serverRequest(uri: '/room/score/game/leave', body: reqBody);
if (response['result'] == 'OK') {
final resp = response['response'];
if (resp != null && resp['result'] == 'OK') {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
} else {
final msg = resp?['response_info']?['msg_content'] ?? '방 나가기 실패';
final again = await showYesNoDialog(
context: context,
title: '오류',
message: '$msg\n그래도 나가시겠습니까?',
yesNo: true,
);
if (again == true) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
}
}
} else {
final again = await showYesNoDialog(
context: context,
title: '오류',
message: '서버오류\n그래도 나가시겠습니까?',
yesNo: true,
);
if (again == true) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
}
}
} catch (e) {
final again = await showYesNoDialog(
context: context,
title: '오류',
message: '$e\n그래도 나가시겠습니까?',
yesNo: true,
);
if (again == true) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
}
}
await _requestLeaveRoom();
} else {
//
try {
final reqBody = {"room_seq": "${widget.roomSeq}"};
final response = await Api.serverRequest(uri: '/room/score/game/leave', body: reqBody);
if (response['result'] == 'OK') {
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
} else {
final msg = resp['response_info']?['msg_content'] ?? '나가기 실패';
final again = await showYesNoDialog(context: context, title: '오류', message: msg, yesNo: true);
if (again == true) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
}
}
} else {
final again = await showYesNoDialog(context: context, title: '오류', message: '서버 통신 오류', yesNo: true);
if (again == true) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
}
}
} catch (e) {
final again = await showYesNoDialog(context: context, title: '오류', message: '$e', yesNo: true);
if (again == true) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
}
}
await _requestLeaveRoom();
}
}
@ -333,22 +298,14 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
return defaultVal;
}
//
// : = 3, = 2
// READY "준비"/"준비완료"
// : READY=Y
//
//
Widget _buildTopButtons() {
if (_isLoading) return const SizedBox();
// READY
final me = _userList.firstWhere(
(u) => (u['user_seq'] ?? '0') == mySeq,
orElse: () => {},
);
final me = _userList.firstWhere((u) => (u['user_seq'] ?? '0') == mySeq, orElse: () => {});
final myReadyYn = (me['ready_yn'] ?? 'N').toString().toUpperCase();
final bool isReady = (myReadyYn == 'Y');
final String readyLabel = isReady ? '준비완료' : '준비';
final readyLabel = isReady ? '준비완료' : '준비';
final btnStyle = ElevatedButton.styleFrom(
backgroundColor: Colors.white,
@ -357,9 +314,7 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
);
if (roomMasterYn == 'Y') {
// -> [ ], [/], [ ]
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Container(
@ -394,9 +349,7 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
],
);
} else {
// -> [ ], [/]
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Container(
@ -423,16 +376,11 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
}
}
/// READY
Future<void> _onToggleReady() async {
try {
//
final me = _userList.firstWhere(
(u) => (u['user_seq'] ?? '') == mySeq,
orElse: () => {},
);
final me = _userList.firstWhere((u) => (u['user_seq'] ?? '') == mySeq, orElse: () => {});
final myReadyYn = (me['ready_yn'] ?? 'N').toString().toUpperCase();
final bool isReady = (myReadyYn == 'Y');
final isReady = (myReadyYn == 'Y');
final newYn = isReady ? 'N' : 'Y';
final userRef = _roomRef.child('userInfo').child(mySeq);
@ -442,7 +390,6 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
}
}
///
Future<void> _onOpenRoomSetting() async {
final roomInfo = {
"room_seq": "${widget.roomSeq}",
@ -465,16 +412,12 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
builder: (_) => RoomSettingModal(roomInfo: roomInfo),
);
if (result == 'refresh') {
// do something
// ...
}
}
/// ( READY=Y )
Future<void> _onGameStart() async {
final notReady = _userList.any((u) {
final ry = (u['ready_yn'] ?? 'N').toString().toUpperCase();
return (ry != 'Y');
});
final notReady = _userList.any((u) => (u['ready_yn'] ?? 'N').toString().toUpperCase() != 'Y');
if (notReady) {
showResponseDialog(context, '안내', 'READY되지 않은 참가자가 있습니다(방장 포함).');
return;
@ -485,30 +428,85 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
"room_type": "TEAM",
};
try {
final response = await Api.serverRequest(
uri: '/room/score/game/start',
body: requestBody,
);
final response = await Api.serverRequest(uri: '/room/score/game/start', body: requestBody);
if (response['result'] == 'OK') {
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
print('게임 시작 요청 성공(팀전)');
} else {
final msgTitle = resp['response_info']?['msg_title'] ?? '오류';
final msgContent = resp['response_info']?['msg_content'] ?? '게임 시작 실패';
showResponseDialog(context, msgTitle, msgContent);
// ...
}
} else {
showResponseDialog(context, '실패', '서버 통신 오류');
// ...
}
} catch (e) {
showResponseDialog(context, '오류', '$e');
// ...
}
}
//
// / / / Seat
//
// ()
String _formatDuration(Duration d) {
final mm = d.inMinutes.remainder(60).toString().padLeft(2, '0');
final ss = d.inSeconds.remainder(60).toString().padLeft(2, '0');
return '$mm:$ss';
}
@override
Widget build(BuildContext context) {
final countdownStr = _formatDuration(_remaining);
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.black,
elevation: 0,
// +
title: Text(
(roomTitle.isNotEmpty ? roomTitle : '방 제목') + ' [$countdownStr]',
style: const TextStyle(color: Colors.white),
),
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
onPressed: _onLeaveRoom,
),
),
bottomNavigationBar: Container(
height: 50,
decoration: BoxDecoration(
color: Colors.grey.shade300,
border: Border.all(color: Colors.black, width: 1),
),
child: const Center(
child: Text('구글 광고', style: TextStyle(color: Colors.black)),
),
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTopButtons(),
const SizedBox(height: 20),
const Text('사회자', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black)),
const SizedBox(height: 8),
_buildAdminSection(),
const SizedBox(height: 20),
const Text('팀별 참가자', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black)),
const SizedBox(height: 8),
_buildTeamSection(),
const SizedBox(height: 20),
_buildWaitSection(),
],
),
),
);
}
Widget _buildAdminSection() {
final adminList = _userList.where((u) {
final pType = (u['participant_type'] ?? '').toString().toUpperCase();
@ -516,7 +514,6 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
}).toList();
return Container(
width: double.infinity,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white,
@ -553,7 +550,6 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
if (teamMap.isEmpty) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white,
@ -660,13 +656,14 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
Widget _buildSeat(Map<String, dynamic> user) {
final userName = user['nickname'] ?? '유저';
final profileImg = user['profile_img'] ?? '';
final readyYn = user['ready_yn'] ?? 'N';
final isReady = (readyYn == 'Y');
final isMaster = (roomMasterYn == 'Y');
final readyYn = (user['ready_yn'] ?? 'N').toString().toUpperCase();
final connectYn = (user['connect_yn'] ?? 'Y').toString().toUpperCase();
final bool isReady = (readyYn == 'Y');
final bool isDisconnected = (connectYn == 'N');
final bool isMaster = (roomMasterYn == 'Y');
return GestureDetector(
onTap: () async {
//
final result = await showDialog(
context: context,
barrierDismissible: false,
@ -674,7 +671,7 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
userData: user,
isRoomMaster: isMaster,
roomSeq: widget.roomSeq,
roomTypeName: widget.roomType.toUpperCase(), // "TEAM"
roomTypeName: widget.roomType.toUpperCase(),
teamNameList: _teamNameList,
),
);
@ -686,7 +683,6 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
width: 60,
margin: const EdgeInsets.only(right: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 50,
@ -694,36 +690,51 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(
color: isReady ? Colors.red : Colors.black,
width: isReady ? 2 : 1,
color: isDisconnected
? Colors.orange
: (isReady ? Colors.red : Colors.black),
width: isDisconnected ? 2 : (isReady ? 2 : 1),
),
borderRadius: BorderRadius.circular(20),
boxShadow: isReady
? [
BoxShadow(
color: Colors.redAccent.withOpacity(0.6),
blurRadius: 8,
spreadRadius: 2,
offset: const Offset(0, 0),
)
]
: [],
boxShadow: [
if (isReady)
BoxShadow(
color: Colors.redAccent.withOpacity(0.6),
blurRadius: 8,
spreadRadius: 2,
),
if (isDisconnected)
BoxShadow(
color: Colors.orangeAccent.withOpacity(0.6),
blurRadius: 8,
spreadRadius: 2,
),
],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Image.network(
'https://eldsoft.com:8097/images$profileImg',
fit: BoxFit.cover,
errorBuilder: (ctx, err, st) {
return const Center(
child: Text(
'이미지\n불가',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 10),
child: isDisconnected
? const Center(
child: Text(
'!',
style: TextStyle(
fontSize: 20,
color: Colors.orange,
fontWeight: FontWeight.bold,
),
),
)
: Image.network(
'https://eldsoft.com:8097/images$profileImg',
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => const Center(
child: Text(
'이미지\n불가',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 10),
),
),
),
);
},
),
),
),
const SizedBox(height: 2),
@ -733,63 +744,4 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.black,
elevation: 0,
title: const Text('대기 방 (팀전)', style: TextStyle(color: Colors.white)),
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
onPressed: _onLeaveRoom, //
),
),
bottomNavigationBar: Container(
height: 50,
decoration: BoxDecoration(
color: Colors.grey.shade300,
border: Border.all(color: Colors.black, width: 1),
),
child: const Center(
child: Text('구글 광고', style: TextStyle(color: Colors.black)),
),
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Text(
roomTitle.isNotEmpty ? roomTitle : '방 제목',
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 8),
_buildTopButtons(),
const SizedBox(height: 20),
const Text('사회자', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black)),
const SizedBox(height: 8),
_buildAdminSection(),
const SizedBox(height: 20),
const Text('팀별 참가자', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black)),
const SizedBox(height: 8),
_buildTeamSection(),
const SizedBox(height: 20),
_buildWaitSection(),
],
),
),
);
}
}

View File

@ -9,6 +9,7 @@ import file_selector_macos
import firebase_auth
import firebase_core
import firebase_database
import google_sign_in_ios
import shared_preferences_foundation
import webview_flutter_wkwebview
@ -17,6 +18,7 @@ func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
FLTFirebaseDatabasePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseDatabasePlugin"))
FLTGoogleSignInPlugin.register(with: registry.registrar(forPlugin: "FLTGoogleSignInPlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
FLTWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "FLTWebViewFlutterPlugin"))
}

BIN
my_release_key.jks Normal file

Binary file not shown.

View File

@ -210,10 +210,10 @@ packages:
dependency: "direct dev"
description:
name: flutter_lints
sha256: "5398f14efa795ffb7a33e9b6a08798b26a180edac4ad7db3f231e40f82ce11e1"
sha256: a25a15ebbdfc33ab1cd26c63a6ee519df92338a9c10f122adda92938253bef04
url: "https://pub.dev"
source: hosted
version: "5.0.0"
version: "2.0.3"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
@ -232,6 +232,14 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
fluttertoast:
dependency: "direct main"
description:
name: fluttertoast
sha256: "24467dc20bbe49fd63e57d8e190798c4d22cbbdac30e54209d153a15273721d1"
url: "https://pub.dev"
source: hosted
version: "8.2.10"
google_mobile_ads:
dependency: "direct main"
description:
@ -240,6 +248,46 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.2.0"
google_sign_in:
dependency: "direct main"
description:
name: google_sign_in
sha256: "821f354c053d51a2d417b02d42532a19a6ea8057d2f9ebb8863c07d81c98aaf9"
url: "https://pub.dev"
source: hosted
version: "5.4.4"
google_sign_in_android:
dependency: transitive
description:
name: google_sign_in_android
sha256: "3b96f9b6cf61915f73cbe1218a192623e296a9b8b31965702503649477761e36"
url: "https://pub.dev"
source: hosted
version: "6.1.34"
google_sign_in_ios:
dependency: transitive
description:
name: google_sign_in_ios
sha256: "83f015169102df1ab2905cf8abd8934e28f87db9ace7a5fa676998842fed228a"
url: "https://pub.dev"
source: hosted
version: "5.7.8"
google_sign_in_platform_interface:
dependency: transitive
description:
name: google_sign_in_platform_interface
sha256: "1f6e5787d7a120cc0359ddf315c92309069171306242e181c09472d1b00a2971"
url: "https://pub.dev"
source: hosted
version: "2.4.5"
google_sign_in_web:
dependency: transitive
description:
name: google_sign_in_web
sha256: "75cc41ebc53b1756320ee14d9c3018ad3e6cea298147dbcd86e9d0c8d6720b40"
url: "https://pub.dev"
source: hosted
version: "0.10.2+1"
http:
dependency: "direct main"
description:
@ -320,6 +368,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "0.2.1+1"
js:
dependency: transitive
description:
name: js
sha256: f2c445dce49627136094980615a031419f7f3eb393237e4ecd97ac15dea343f3
url: "https://pub.dev"
source: hosted
version: "0.6.7"
leak_tracker:
dependency: transitive
description:
@ -348,10 +404,10 @@ packages:
dependency: transitive
description:
name: lints
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
sha256: "0a217c6c989d21039f1498c3ed9f3ed71b354e69873f13a8dfc3c9fe76f1b452"
url: "https://pub.dev"
source: hosted
version: "5.1.1"
version: "2.1.1"
matcher:
dependency: transitive
description:

View File

@ -1,35 +1,18 @@
name: allscore_app
description: "A new Flutter project."
# The following line prevents the package from being accidentally published to
# pub.dev using `flutter pub publish`. This is preferred for private packages.
publish_to: 'none' # Remove this line if you wish to publish to pub.dev
# The following defines the version and build number for your application.
# A version number is three numbers separated by dots, like 1.2.43
# followed by an optional build number separated by a +.
# Both the version and the builder number may be overridden in flutter
# build by specifying --build-name and --build-number, respectively.
# In Android, build-name is used as versionName while build-number used as versionCode.
# Read more about Android versioning at https://developer.android.com/studio/publish/versioning
# In iOS, build-name is used as CFBundleShortVersionString while build-number is used as CFBundleVersion.
# Read more about iOS versioning at
# https://developer.apple.com/library/archive/documentation/General/Reference/InfoPlistKeyReference/Articles/CoreFoundationKeys.html
# In Windows, build-name is used as the major, minor, and patch parts
# of the product and file versions while build-number is used as the build suffix.
publish_to: 'none'
version: 1.0.0+1
environment:
sdk: ^3.6.0
# Dependencies specify other packages that your package needs in order to work.
# To automatically upgrade your package dependencies to the latest versions
# consider running `flutter pub upgrade --major-versions`. Alternatively,
# dependencies can be manually updated by changing the version numbers below to
# the latest version available on pub.dev. To see which dependencies have newer
# versions available, run `flutter pub outdated`.
# ───────────────────────────────────
# 의존성
# ───────────────────────────────────
dependencies:
flutter:
sdk: flutter
google_mobile_ads: ^5.2.0
http: ^1.2.2
crypto: ^3.0.1
@ -38,68 +21,27 @@ dependencies:
firebase_core: ^3.9.0
firebase_auth: ^5.3.4
firebase_database: ^11.2.0
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
google_sign_in: ^5.4.0
cupertino_icons: ^1.0.8
fluttertoast: ^8.0.9
dev_dependencies:
flutter_test:
sdk: flutter
google_mobile_ads: ^5.2.0
http: ^1.2.2
crypto: ^3.0.1
shared_preferences: ^2.0.6
image_picker: ^0.8.4+4
firebase_core: ^3.9.0
firebase_auth: ^5.3.4
firebase_database: ^11.2.0
flutter_lints: ^2.0.0
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is
# activated in the `analysis_options.yaml` file located at the root of your
# package. See that file for information about deactivating specific lint
# rules and activating additional ones.
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:
# The following line ensures that the Material Icons font is
# included with your application, so that you can use the icons in
# the material Icons class.
uses-material-design: true
# To add assets to your application, add an assets section, like this:
# assets:
# - images/a_dot_burr.jpeg
# - images/a_dot_ham.jpeg
# An image asset can refer to one or more resolution-specific "variants", see
# https://flutter.dev/to/resolution-aware-images
# For details regarding adding assets from package dependencies, see
# https://flutter.dev/to/asset-from-package
# To add custom fonts to your application, 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 from package dependencies,
# see https://flutter.dev/to/font-from-package
# 여기에 에셋 등록
assets:
- assets/images/icons8-google-logo-36.png
- assets/images/icons8-google-logo-48.png
- assets/images/icons8-google-logo-48-2.png
- assets/images/icons8-google-logo-72.png
- assets/images/icons8-google-logo-96.png
- assets/images/icons8-google-logo-144.png
- assets/images/icons8-google-logo-192.png