로그인 수정, 애드몹 수정, 메인페이지까지 점검완료
@ -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'
|
||||
|
1
android/app/google-oauth-service.json
Normal 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"}}
|
@ -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
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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"
|
||||
|
@ -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'
|
||||
}
|
||||
}
|
||||
|
||||
|
BIN
assets/images/icons8-google-logo-144.png
Normal file
After Width: | Height: | Size: 3.9 KiB |
BIN
assets/images/icons8-google-logo-192.png
Normal file
After Width: | Height: | Size: 5.2 KiB |
BIN
assets/images/icons8-google-logo-36.png
Normal file
After Width: | Height: | Size: 972 B |
BIN
assets/images/icons8-google-logo-48-2.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
assets/images/icons8-google-logo-48.png
Normal file
After Width: | Height: | Size: 1.3 KiB |
BIN
assets/images/icons8-google-logo-72.png
Normal file
After Width: | Height: | Size: 1.9 KiB |
BIN
assets/images/icons8-google-logo-96.png
Normal file
After Width: | Height: | Size: 2.6 KiB |
@ -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
@ -0,0 +1,4 @@
|
||||
class Config {
|
||||
static const String testAdUnitId = 'ca-app-pub-3940256099942544/6300978111';
|
||||
static const String realAdUnitId = 'ca-app-pub-3940256099942544/6300978111';
|
||||
}
|
149
lib/dialogs/room_setting_finish_dialog.dart
Normal 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),
|
||||
),
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
@ -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(
|
||||
|
127
lib/dialogs/user_info_finish_dialog.dart
Normal 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)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
@ -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());
|
||||
}
|
||||
|
@ -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 원하는 위젯
|
||||
);
|
||||
}
|
||||
}
|
@ -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),
|
||||
),
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
@ -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 원하는 위젯
|
||||
);
|
||||
}
|
||||
}
|
@ -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);
|
||||
|
||||
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
/// 팀 박스 (팀별 점수 표시 + 1등2등3등 금/은/동 배경)
|
||||
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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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)),
|
||||
);
|
||||
|
@ -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') {
|
||||
await _requestFinish();
|
||||
// 방장 -> 모달
|
||||
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 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('구글 광고'),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -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';
|
||||
@ -27,7 +27,7 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
|
||||
late DatabaseReference _roomRef;
|
||||
Stream<DatabaseEvent>? _roomStream;
|
||||
|
||||
String roomMasterYn = 'N';
|
||||
String roomMasterYn = 'N';
|
||||
String roomTitle = '';
|
||||
|
||||
int myScore = 0;
|
||||
@ -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),
|
||||
);
|
||||
}
|
||||
|
@ -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,9 +58,8 @@ class _RoomSearchListPageState extends State<RoomSearchListPage> {
|
||||
}
|
||||
}
|
||||
|
||||
/// (1) 서버에서 방 리스트 가져오기
|
||||
Future<void> _fetchRoomList({required bool isRefresh}) async {
|
||||
if (_isLoading) return;
|
||||
if (_isLoading) return;
|
||||
if (!isRefresh && !_hasMore) return;
|
||||
|
||||
setState(() => _isLoading = true);
|
||||
@ -72,26 +70,19 @@ 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();
|
||||
|
||||
final requestBody = {
|
||||
"search_type": searchType,
|
||||
"search_type": searchType,
|
||||
"search_value": searchValue,
|
||||
"search_page": searchPage,
|
||||
};
|
||||
|
||||
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),
|
||||
|
@ -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_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,40 +437,48 @@ 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,
|
||||
onPressed: _onLeaveRoom,
|
||||
),
|
||||
),
|
||||
bottomNavigationBar: Container(
|
||||
@ -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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -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(),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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
64
pubspec.lock
@ -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:
|
||||
|
98
pubspec.yaml
@ -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
|
||||
|