From f416094820c609989834ace3d6f0311a997bbf95 Mon Sep 17 00:00:00 2001 From: eld_master Date: Thu, 16 Jan 2025 01:34:11 +0900 Subject: [PATCH] =?UTF-8?q?=EC=A7=84=ED=96=89=EC=A4=91=20=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=EA=B9=8C=EC=A7=80=20=EC=99=84=EB=A3=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/views/room/main_page.dart | 31 +- lib/views/room/playing_private_page.dart | 187 ++++++++++-- lib/views/room/playing_team_page.dart | 193 ++++++++++-- lib/views/room/waiting_room_private_page.dart | 276 +++++++++++++----- lib/views/room/waiting_room_team_page.dart | 115 +++++--- 5 files changed, 620 insertions(+), 182 deletions(-) diff --git a/lib/views/room/main_page.dart b/lib/views/room/main_page.dart index 96885ba..63acaa5 100644 --- a/lib/views/room/main_page.dart +++ b/lib/views/room/main_page.dart @@ -189,6 +189,15 @@ class _MainPageState extends State { ), ], ), + + 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 원하는 위젯 body: Column( mainAxisAlignment: MainAxisAlignment.spaceBetween, @@ -225,28 +234,6 @@ class _MainPageState extends State { ), ), ), - - // 광고 영역 교체 - // (기존) 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), - ), - ), ], ), ), diff --git a/lib/views/room/playing_private_page.dart b/lib/views/room/playing_private_page.dart index 3ed262b..13bd00e 100644 --- a/lib/views/room/playing_private_page.dart +++ b/lib/views/room/playing_private_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'dart:async'; import 'package:firebase_database/firebase_database.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -9,6 +10,9 @@ import '../../dialogs/response_dialog.dart'; import '../../dialogs/score_edit_dialog.dart'; // 점수 수정 모달 import '../../dialogs/user_info_basic_dialog.dart'; // 일반 유저 정보 모달 +import 'package:google_mobile_ads/google_mobile_ads.dart'; +import '../../config/config.dart'; + class PlayingPrivatePage extends StatefulWidget { final int roomSeq; final String roomTitle; @@ -24,8 +28,20 @@ class PlayingPrivatePage extends StatefulWidget { } class _PlayingPrivatePageState extends State { + + // FRD late DatabaseReference _roomRef; Stream? _roomStream; + StreamSubscription? _roomStreamSubscription; + // ───────────────────────────────────────── + // 운영시간 카운트다운 + // ───────────────────────────────────────── + Timer? _countdownTimer; + Duration _remaining = const Duration(hours: 1); // 기본 1시간 + DateTime? _roomStartDt; // FRD의 roomInfo.room_start_dt + String _roomRunningTime = '0'; // 운영시간 + bool _roomTimeOut = false; + String _roomExitYn = 'N'; String roomMasterYn = 'N'; String roomTitle = ''; @@ -34,11 +50,22 @@ class _PlayingPrivatePageState extends State { List> _scoreList = []; bool _isLoading = true; + // 종료 페이지 이동 중복 방지 + bool _movedToFinishPage = false; + + // 점수 공개 범위 + String scoreOpenRange = 'ALL'; + String mySeq = '0'; // userListMap: { userSeq: true/false } Map _userListMap = {}; + /// (1) 광고 배너 관련 변수 + BannerAd? _bannerAd; + bool _isBannerReady = false; // 광고 로드 완료 여부 + String adUnitId = Config.testAdUnitId; + @override void initState() { super.initState(); @@ -46,9 +73,40 @@ class _PlayingPrivatePageState extends State { FirebaseDatabase.instance.goOnline(); roomTitle = widget.roomTitle; + // (C) 배너 광고 초기화 + _initBannerAd(); + // (D) 방 정보 초기화 _initFirebase(); } + @override + void dispose() { + _countdownTimer?.cancel(); + _roomStreamSubscription?.cancel(); + super.dispose(); + } + + /// 배너 광고 초기화 + void _initBannerAd() { + _bannerAd = BannerAd( + size: AdSize.banner, // 일반 배너 사이즈 + // adUnitId: 'ca-app-pub-3940256099942544/6300978111' (테스트용) + adUnitId: adUnitId, // 실제/테스트 배너 광고 단위 ID + listener: BannerAdListener( + onAdLoaded: (Ad ad) { + setState(() => _isBannerReady = true); + }, + onAdFailedToLoad: (Ad ad, LoadAdError err) { + ad.dispose(); + }, + ), + request: const AdRequest(), + ); + + // load() 호출로 광고 요청 + _bannerAd?.load(); + } + Future _initFirebase() async { final prefs = await SharedPreferences.getInstance(); mySeq = prefs.getInt('my_user_seq')?.toString() ?? '0'; @@ -83,15 +141,20 @@ class _PlayingPrivatePageState extends State { if (roomStatus == 'FINISH') { // 종료 페이지 if (mounted) { - Navigator.pushReplacement( + Navigator.pushAndRemoveUntil( context, MaterialPageRoute(builder: (_) => FinishPrivatePage(roomSeq: widget.roomSeq)), + (route) => false, ); } return; } setState(() { + // 운영시간 입력 + _roomRunningTime = roomInfoData['running_time'] ?? '0'; + // 점수 공개 범위 + scoreOpenRange = roomInfoData['score_open_range'] ?? 'ALL'; // 방장 여부 final masterSeq = roomInfoData['master_user_seq']; roomMasterYn = (masterSeq != null && masterSeq.toString() == mySeq) ? 'Y' : 'N'; @@ -111,6 +174,10 @@ class _PlayingPrivatePageState extends State { // 전체 유저 목록 final List> rawList = []; userInfoData.forEach((uSeq, uData) { + // 방장 표시 + if (uSeq.toString() == roomInfoData['master_user_seq'].toString()) uData['nickname'] = '★' + (uData['nickname'] ?? '유저'); + // 관리자 표시 + if ((uData['participant_type'] ?? '').toString().toUpperCase() == 'ADMIN') uData['nickname'] = '☆' + (uData['nickname'] ?? '유저'); rawList.add({ 'user_seq': uSeq, 'participant_type': (uData['participant_type'] ?? '').toString().toUpperCase(), @@ -132,7 +199,7 @@ class _PlayingPrivatePageState extends State { } // ADMIN 제외 - final playerList = rawList.where((u) => u['participant_type'] != 'ADMIN').toList(); + final playerList = rawList.toList(); // 점수 내림차순 playerList.sort((a, b) { final scoreA = a['score'] ?? 0; @@ -145,6 +212,33 @@ class _PlayingPrivatePageState extends State { _isLoading = false; }); + + + // 종료 => 이동 + if (roomStatus == 'FINISH' && !_movedToFinishPage) { + _movedToFinishPage = true; + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + builder: (_) => FinishPrivatePage( + roomSeq: widget.roomSeq, + ), + ), + (route) => false, + ); + return; + } + // 운영시간 카운트다운 위한 room_start_dt 파싱 + final roomStartDtStr = (roomInfoData['start_dt'] ?? '') as String; + if (roomStartDtStr.isNotEmpty) { + final dt = DateTime.tryParse(roomStartDtStr); + if (dt != null) { + setState(() { + _roomStartDt = dt; + }); + _startCountdownTimer(); + } + } }, onError: (err) { setState(() { _isLoading = false; @@ -153,6 +247,43 @@ class _PlayingPrivatePageState extends State { }); } + // 운영시간 카운트다운 타이머 + void _startCountdownTimer() { + if (_countdownTimer != null && _countdownTimer!.isActive) { + return; // 이미 실행중이면 중복 실행 방지 + } + if (_roomStartDt == null) return; + + _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + // 목표시각: roomStartDt + 운영시간 + final endTime = _roomStartDt!.add(Duration(hours: int.parse(_roomRunningTime))); + 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() { + // 방장 => 방 삭제 (leave API) + // 일반 => 그냥 나가기 + setState(() { + _roomTimeOut = true; + _roomExitYn = 'Y'; + }); + _requestFinish(); + } + /// 방장이면 Finish API Future _requestFinish() async { final reqBody = { @@ -258,14 +389,15 @@ class _PlayingPrivatePageState extends State { await userRef.set(false); if (!mounted) return false; - Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage())); + Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (_) => const MainPage()), (route) => false); return false; } /// 참가자 카드 Widget _buildScoreItem(Map user) { final userSeq = user['user_seq'].toString(); - final score = user['score'] ?? 0; + final tempScore = user['score'] ?? 0; + final score = (scoreOpenRange == 'ALL') ? tempScore : '-'; final nickname = user['nickname'] ?? '유저'; final bool isActive = _userListMap[userSeq] ?? true; @@ -342,8 +474,16 @@ class _PlayingPrivatePageState extends State { } } + // (★) 카운트다운 표시용 + 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 WillPopScope( onWillPop: _onWillPop, child: Scaffold( @@ -355,10 +495,21 @@ class _PlayingPrivatePageState extends State { icon: const Icon(Icons.arrow_back_ios, color: Colors.white), onPressed: () => _onWillPop(), ), - title: Text( - roomTitle.isNotEmpty ? roomTitle : '진행중 (개인전)', - style: const TextStyle(color: Colors.white), - ), + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // 왼쪽: 방 제목 + Text( + roomTitle.isNotEmpty ? roomTitle : '방 제목', + style: const TextStyle(color: Colors.white), + ), + // 오른쪽: 남은시간 + Text( + countdownStr, // 예: "10:23" + style: const TextStyle(color: Colors.white), + ), + ], + ), actions: [ if (roomMasterYn == 'Y') TextButton( @@ -394,19 +545,17 @@ class _PlayingPrivatePageState extends State { ), ), ), - Container( - height: 50, - decoration: BoxDecoration( - color: Colors.white, - border: Border.all(color: Colors.black, width: 1), - ), - child: const Center( - child: Text('구글 광고'), - ), - ), ], ), - ), + 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 원하는 위젯 + ), ); } } diff --git a/lib/views/room/playing_team_page.dart b/lib/views/room/playing_team_page.dart index 0d211c4..6129842 100644 --- a/lib/views/room/playing_team_page.dart +++ b/lib/views/room/playing_team_page.dart @@ -1,4 +1,5 @@ import 'package:flutter/material.dart'; +import 'dart:async'; import 'package:firebase_database/firebase_database.dart'; import 'package:shared_preferences/shared_preferences.dart'; @@ -9,6 +10,9 @@ import '../../dialogs/response_dialog.dart'; import '../../dialogs/score_edit_dialog.dart'; import '../../dialogs/user_info_basic_dialog.dart'; +import 'package:google_mobile_ads/google_mobile_ads.dart'; +import '../../config/config.dart'; + class PlayingTeamPage extends StatefulWidget { final int roomSeq; final String roomTitle; @@ -24,8 +28,19 @@ class PlayingTeamPage extends StatefulWidget { } class _PlayingTeamPageState extends State { + // FRD late DatabaseReference _roomRef; Stream? _roomStream; + StreamSubscription? _roomStreamSubscription; + // ───────────────────────────────────────── + // 운영시간 카운트다운 + // ───────────────────────────────────────── + Timer? _countdownTimer; + Duration _remaining = const Duration(hours: 1); // 기본 1시간 + DateTime? _roomStartDt; // FRD의 roomInfo.room_start_dt + String _roomRunningTime = '0'; // FRD의 roomInfo.running_time + bool _roomTimeOut = false; + String _roomExitYn = 'N'; String roomMasterYn = 'N'; String roomTitle = ''; @@ -37,11 +52,22 @@ class _PlayingTeamPageState extends State { Map>> _teamMap = {}; bool _isLoading = true; + // 종료 페이지 이동 중복 방지 + bool _movedToFinishPage = false; + String mySeq = '0'; // userListMap: { seq: true/false } Map _userListMap = {}; + // 점수 공개 범위 + String scoreOpenRange = 'ALL'; + + /// (1) 광고 배너 관련 변수 + BannerAd? _bannerAd; + bool _isBannerReady = false; // 광고 로드 완료 여부 + String adUnitId = Config.testAdUnitId; + @override void initState() { super.initState(); @@ -49,9 +75,40 @@ class _PlayingTeamPageState extends State { FirebaseDatabase.instance.goOnline(); roomTitle = widget.roomTitle; + // (C) 배너 광고 초기화 + _initBannerAd(); + // (D) 방 정보 초기화 _initFirebase(); } + @override + void dispose() { + _countdownTimer?.cancel(); + _roomStreamSubscription?.cancel(); + super.dispose(); + } + + /// 배너 광고 초기화 + void _initBannerAd() { + _bannerAd = BannerAd( + size: AdSize.banner, // 일반 배너 사이즈 + // adUnitId: 'ca-app-pub-3940256099942544/6300978111' (테스트용) + adUnitId: adUnitId, // 실제/테스트 배너 광고 단위 ID + listener: BannerAdListener( + onAdLoaded: (Ad ad) { + setState(() => _isBannerReady = true); + }, + onAdFailedToLoad: (Ad ad, LoadAdError err) { + ad.dispose(); + }, + ), + request: const AdRequest(), + ); + + // load() 호출로 광고 요청 + _bannerAd?.load(); + } + Future _initFirebase() async { final prefs = await SharedPreferences.getInstance(); mySeq = prefs.getInt('my_user_seq')?.toString() ?? '0'; @@ -87,15 +144,22 @@ class _PlayingTeamPageState extends State { if (roomStatus == 'FINISH') { // 종료화면 if (mounted) { - Navigator.pushReplacement( + Navigator.pushAndRemoveUntil( context, MaterialPageRoute(builder: (_) => FinishTeamPage(roomSeq: widget.roomSeq)), + (route) => false, ); } return; } setState(() { + // 운영시간 입력 + _roomRunningTime = roomInfoData['running_time'] ?? '0'; + + // 점수 공개 범위 + scoreOpenRange = roomInfoData['score_open_range'] ?? 'ALL'; + final masterSeq = roomInfoData['master_user_seq']; roomMasterYn = (masterSeq != null && masterSeq.toString() == mySeq) ? 'Y' : 'N'; @@ -112,6 +176,13 @@ class _PlayingTeamPageState extends State { // 전체 유저 final List> rawList = []; userInfoData.forEach((uSeq, uData) { + // 방장 표시 + if (uSeq.toString() == roomInfoData['master_user_seq'].toString()) { + uData['nickname'] = '★' + (uData['nickname'] ?? '유저'); + } else if ((uData['participant_type'] ?? '').toString().toUpperCase() == 'ADMIN') { + // 관리자 표시 + uData['nickname'] = '☆' + (uData['nickname'] ?? '유저'); + } rawList.add({ 'user_seq': uSeq, 'participant_type': (uData['participant_type'] ?? '').toString().toUpperCase(), @@ -151,7 +222,7 @@ class _PlayingTeamPageState extends State { for (var user in rawList) { final pType = user['participant_type']; final tName = (user['team_name'] ?? 'WAIT'); - if (pType == 'ADMIN') continue; + // if (pType == 'ADMIN') continue; if (tName == 'WAIT') continue; tMap.putIfAbsent(tName, () => []); @@ -173,6 +244,33 @@ class _PlayingTeamPageState extends State { _isLoading = false; }); + + // 종료 => 이동 + if (roomStatus == 'FINISH' && !_movedToFinishPage) { + _movedToFinishPage = true; + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute( + builder: (_) => FinishTeamPage( + roomSeq: widget.roomSeq, + ), + ), + (route) => false, + ); + return; + } + + // 운영시간 카운트다운 위한 room_start_dt 파싱 + final roomStartDtStr = (roomInfoData['start_dt'] ?? '') as String; + if (roomStartDtStr.isNotEmpty) { + final dt = DateTime.tryParse(roomStartDtStr); + if (dt != null) { + setState(() { + _roomStartDt = dt; + }); + _startCountdownTimer(); + } + } }, onError: (err) { setState(() { _isLoading = false; @@ -181,6 +279,43 @@ class _PlayingTeamPageState extends State { }); } + // 운영시간 카운트다운 타이머 + void _startCountdownTimer() { + if (_countdownTimer != null && _countdownTimer!.isActive) { + return; // 이미 실행중이면 중복 실행 방지 + } + if (_roomStartDt == null) return; + + _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + // 목표시각: roomStartDt + 운영시간 + final endTime = _roomStartDt!.add(Duration(hours: int.parse(_roomRunningTime))); + 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() { + // 방장 => 방 삭제 (leave API) + // 일반 => 그냥 나가기 + setState(() { + _roomTimeOut = true; + _roomExitYn = 'Y'; + }); + _requestFinish(); + } + /// 게임종료 Future _requestFinish() async { final body = { @@ -282,21 +417,40 @@ class _PlayingTeamPageState extends State { await userRef.set(false); if (!mounted) return false; - Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage())); + Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (_) => const MainPage()), (route) => false); return false; } + // (★) 카운트다운 표시용 + 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 WillPopScope( onWillPop: _onWillPop, child: Scaffold( backgroundColor: Colors.white, appBar: AppBar( - title: Text( - roomTitle.isNotEmpty ? roomTitle : '진행중 (팀전)', - style: const TextStyle(color: Colors.white), - ), + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // 왼쪽: 방 제목 + Text( + roomTitle.isNotEmpty ? roomTitle : '방 제목', + style: const TextStyle(color: Colors.white), + ), + // 오른쪽: 남은시간 + Text( + countdownStr, // 예: "10:23" + style: const TextStyle(color: Colors.white), + ), + ], + ), backgroundColor: Colors.black, elevation: 0, leading: IconButton( @@ -352,19 +506,16 @@ class _PlayingTeamPageState extends State { ), ), ), - - Container( - height: 50, - decoration: BoxDecoration( - color: Colors.white, - border: Border.all(color: Colors.black, width: 1), - ), - child: const Center( - child: Text('구글 광고', style: TextStyle(color: Colors.black)), - ), - ), ], ), + 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 원하는 위젯 ), ); } @@ -372,7 +523,8 @@ class _PlayingTeamPageState extends State { Widget _buildTeamSection(String teamName) { final upperName = teamName.toUpperCase(); final members = _teamMap[upperName] ?? []; - final teamScore = _teamScoreMap[upperName] ?? 0; + final tempTeamScore = _teamScoreMap[upperName] ?? 0; + final teamScore = (scoreOpenRange == 'TEAM' || scoreOpenRange == 'ALL') ? tempTeamScore : '-'; return Container( width: double.infinity, @@ -407,7 +559,8 @@ class _PlayingTeamPageState extends State { Widget _buildTeamMemberItem(Map userData) { final userSeq = userData['user_seq'].toString(); - final score = userData['score'] ?? 0; + final tempScore = userData['score'] ?? 0; + final score = (scoreOpenRange == 'ALL') ? tempScore : '-'; final nickname= userData['nickname'] ?? '유저'; final bool isActive = _userListMap[userSeq] ?? true; diff --git a/lib/views/room/waiting_room_private_page.dart b/lib/views/room/waiting_room_private_page.dart index e92d214..1eacda7 100644 --- a/lib/views/room/waiting_room_private_page.dart +++ b/lib/views/room/waiting_room_private_page.dart @@ -11,6 +11,12 @@ import '../../dialogs/room_setting_dialog.dart'; import '../../dialogs/user_info_private_dialog.dart'; import 'playing_private_page.dart'; +// 광고 +import 'package:google_mobile_ads/google_mobile_ads.dart'; + +// 설정 +import '../../config/config.dart'; + class WaitingRoomPrivatePage extends StatefulWidget { final int roomSeq; final String roomType; // "private" @@ -62,13 +68,54 @@ class _WaitingRoomPrivatePageState extends State { Timer? _countdownTimer; Duration _remaining = const Duration(hours: 1); // 기본 1시간 DateTime? _createDt; // FRD의 roomInfo.create_dt + bool _roomTimeOut = false; + String _roomExitYn = 'N'; + + /// (1) 광고 배너 관련 변수 + BannerAd? _bannerAd; + bool _isBannerReady = false; // 광고 로드 완료 여부 + String adUnitId = Config.testAdUnitId; + + // 방장 SEQ 저장 + String _masterSeqString = ''; @override void initState() { super.initState(); // FRD 연결 복원 FirebaseDatabase.instance.goOnline(); + // (B) 방 정보 초기화 _initRoomRef(); + // (C) 배너 광고 초기화 + _initBannerAd(); + } + + @override + void dispose() { + _countdownTimer?.cancel(); + _roomStreamSubscription?.cancel(); + super.dispose(); + } + + /// 배너 광고 초기화 + void _initBannerAd() { + _bannerAd = BannerAd( + size: AdSize.banner, // 일반 배너 사이즈 + // adUnitId: 'ca-app-pub-3940256099942544/6300978111' (테스트용) + adUnitId: adUnitId, // 실제/테스트 배너 광고 단위 ID + listener: BannerAdListener( + onAdLoaded: (Ad ad) { + setState(() => _isBannerReady = true); + }, + onAdFailedToLoad: (Ad ad, LoadAdError err) { + ad.dispose(); + }, + ), + request: const AdRequest(), + ); + + // load() 호출로 광고 요청 + _bannerAd?.load(); } Future _initRoomRef() async { @@ -80,7 +127,9 @@ class _WaitingRoomPrivatePageState extends State { // onDisconnect + connect_yn='Y' final myUserRef = _roomRef.child('userInfo').child(mySeq); - myUserRef.onDisconnect().update({'connect_yn': 'N'}); + if (_roomRef.child('userList').child(mySeq) == true) { + myUserRef.onDisconnect().update({'connect_yn': 'N'}); + } await myUserRef.update({'connect_yn': 'Y'}); _listenRoomData(); @@ -96,14 +145,38 @@ class _WaitingRoomPrivatePageState extends State { roomTitle = '방 정보 없음'; _userList = []; }); + _roomMasterLeave(); return; } final data = snapshot.value as Map? ?? {}; final roomInfoData = data['roomInfo'] as Map? ?? {}; - final userInfoData = data['userInfo'] as Map? ?? {}; + final userInfoDynamic = data['userInfo']; // 이건 List일 수 있음 final roomStatus = (roomInfoData['room_status'] ?? 'WAIT').toString().toUpperCase(); + + final tempList = >[]; + // userList + if (userInfoDynamic is Map) { + userInfoDynamic.forEach((key, val) { + if (val is Map) { + final tempUserSeq = val['user_seq'] ?? '0'; + if (tempUserSeq != '0') { + tempList.add({ + 'user_seq': tempUserSeq, + 'participant_type': val['participant_type'] ?? '', + 'nickname': val['nickname'] ?? '유저', + 'team_name': val['team_name'] ?? '', + 'score': val['score'] ?? 0, + 'profile_img': val['profile_img'] ?? '', + 'introduce_myself': val['introduce_myself'] ?? '', + 'ready_yn': (val['ready_yn'] ?? 'N').toString().toUpperCase(), + 'connect_yn': (val['connect_yn'] ?? 'Y').toString().toUpperCase(), + }); + } + } + }); + } // (A) roomInfo 갱신 setState(() { @@ -121,21 +194,13 @@ class _WaitingRoomPrivatePageState extends State { if (masterSeq != null && masterSeq.toString() == mySeq) { roomMasterYn = 'Y'; } - - // userList - final tempList = >[]; - userInfoData.forEach((userSeq, userMap) { - tempList.add({ - 'user_seq': userSeq, - 'participant_type': userMap['participant_type'] ?? '', - 'nickname': userMap['nickname'] ?? '유저', - 'score': userMap['score'] ?? 0, - 'profile_img': userMap['profile_img'] ?? '', - 'introduce_myself': userMap['introduce_myself'] ?? '', - 'ready_yn': (userMap['ready_yn'] ?? 'N').toString().toUpperCase(), - 'connect_yn': (userMap['connect_yn'] ?? 'Y').toString().toUpperCase(), - }); - }); + + if (masterSeq != null) { + _masterSeqString = masterSeq.toString(); + } else { + _masterSeqString = ''; + } + _userList = tempList; _isLoading = false; }); @@ -143,7 +208,7 @@ class _WaitingRoomPrivatePageState extends State { // (B) 진행중 => 이동 if (roomStatus == 'RUNNING' && !_movedToRunningPage) { _movedToRunningPage = true; - Navigator.pushReplacement( + Navigator.pushAndRemoveUntil( context, MaterialPageRoute( builder: (_) => PlayingPrivatePage( @@ -151,6 +216,7 @@ class _WaitingRoomPrivatePageState extends State { roomTitle: roomTitle, ), ), + (route) => false, ); return; } @@ -158,11 +224,15 @@ class _WaitingRoomPrivatePageState extends State { // (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 (createDtStr.isNotEmpty) { + final dotIndex = createDtStr.indexOf('.'); + final isoStr = createDtStr.substring(0, dotIndex); + final dt = DateTime.tryParse(isoStr); if (dt != null) { - _createDt = dt; - _startCountdownTimer(); + setState(() { + _createDt = dt; + }); + _startCountdownTimer(); } } @@ -170,11 +240,25 @@ class _WaitingRoomPrivatePageState extends State { final amIStillHere = _userList.any((u) => u['user_seq'].toString() == mySeq); if (!amIStillHere && !_kickedOut && roomMasterYn != 'Y') { _kickedOut = true; + if (_roomExitYn == 'N') { + showResponseDialog(context, '안내', '강퇴되었습니다.'); + } Future.delayed(Duration.zero, () async { - await showResponseDialog(context, '안내', '강퇴되었습니다.'); - Navigator.pushReplacement( + Navigator.pushAndRemoveUntil( context, MaterialPageRoute(builder: (_) => const MainPage()), + (route) => false, + ); + }); + } + // (D) 방 제한시간 종료 + if (_roomTimeOut) { + showResponseDialog(context, '안내', '방 제한시간이 종료되었습니다.'); + Future.delayed(Duration.zero, () async { + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (_) => const MainPage()), + (route) => false, ); }); } @@ -186,6 +270,18 @@ class _WaitingRoomPrivatePageState extends State { }); } + // 방장이 나갔을 때 + void _roomMasterLeave() { + Future.delayed(Duration.zero, () async { + await showResponseDialog(context, '안내', '방장이 나갔습니다.'); + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (_) => const MainPage()), + (route) => false, + ); + }); + } + // 1시간 카운트다운 타이머 void _startCountdownTimer() { if (_countdownTimer != null && _countdownTimer!.isActive) { @@ -216,11 +312,11 @@ class _WaitingRoomPrivatePageState extends State { void _onAutoTimeout() { // 방장 => 방 삭제 (leave API) // 일반 => 그냥 나가기 - if (roomMasterYn == 'Y') { - _requestLeaveRoom(); - } else { - _requestLeaveRoom(); - } + setState(() { + _roomTimeOut = true; + _roomExitYn = 'Y'; + }); + _requestLeaveRoom(); } Future _requestLeaveRoom() async { @@ -232,17 +328,10 @@ class _WaitingRoomPrivatePageState extends State { // } if (mounted) { - Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage())); + Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (_) => const MainPage()), (route) => false); } } - @override - void dispose() { - _countdownTimer?.cancel(); - _roomStreamSubscription?.cancel(); - super.dispose(); - } - /// 뒤로가기 → 방 나가기 Future _onLeaveRoom() async { if (roomMasterYn == 'Y') { @@ -292,9 +381,11 @@ class _WaitingRoomPrivatePageState extends State { if (confirm != true) return; // leave API + setState(() {_roomExitYn = 'Y';}); await _requestLeaveRoom(); } else { // 일반 + setState(() {_roomExitYn = 'Y';}); await _requestLeaveRoom(); } } @@ -472,25 +563,34 @@ class _WaitingRoomPrivatePageState extends State { backgroundColor: Colors.black, elevation: 0, // 방 제목 + 남은시간 표시 - title: Text( - (roomTitle.isNotEmpty ? roomTitle : '방 제목') + ' [$countdownStr]', - style: const TextStyle(color: Colors.white), + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // 왼쪽: 방 제목 + Text( + roomTitle.isNotEmpty ? roomTitle : '방 제목', + style: const TextStyle(color: Colors.white), + ), + // 오른쪽: 남은시간 + Text( + countdownStr, // 예: "10:23" + 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)), - ), - ), + 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 원하는 위젯 body: _isLoading ? const Center(child: CircularProgressIndicator()) : SingleChildScrollView( @@ -502,10 +602,10 @@ class _WaitingRoomPrivatePageState extends State { _buildTopButtons(), const SizedBox(height: 20), - 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)), + // const SizedBox(height: 8), + // _buildAdminSection(), + // const SizedBox(height: 20), const Text('참가자', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), const SizedBox(height: 8), @@ -516,36 +616,38 @@ class _WaitingRoomPrivatePageState extends State { ); } - Widget _buildAdminSection() { - final adminList = _userList.where((u) { - final t = (u['participant_type'] ?? '').toString().toUpperCase(); - return t == 'ADMIN'; - }).toList(); + // Widget _buildAdminSection() { + // final adminList = _userList.where((u) { + // final t = (u['participant_type'] ?? '').toString().toUpperCase(); + // return t == 'ADMIN'; + // }).toList(); - return Container( - padding: const EdgeInsets.all(8), - decoration: BoxDecoration( - color: Colors.white, - border: Border.all(color: Colors.black), - borderRadius: BorderRadius.circular(8), - ), - child: adminList.isEmpty - ? const Text('사회자가 없습니다.') - : Wrap( - spacing: 16, - runSpacing: 8, - children: adminList.map(_buildSeat).toList(), - ), - ); - } + // return Container( + // padding: const EdgeInsets.all(8), + // decoration: BoxDecoration( + // color: Colors.white, + // border: Border.all(color: Colors.black), + // borderRadius: BorderRadius.circular(8), + // ), + // child: adminList.isEmpty + // ? const Text('사회자가 없습니다.') + // : Wrap( + // spacing: 16, + // runSpacing: 8, + // children: adminList.map(_buildSeat).toList(), + // ), + // ); + // } Widget _buildPlayerSection() { final playerList = _userList.where((u) { - final t = (u['participant_type'] ?? '').toString().toUpperCase(); - return t != 'ADMIN'; + final t = (u['user_seq'] ?? null); + return t != null; }).toList(); + // final playerList = _userList; return Container( + width: double.infinity, padding: const EdgeInsets.all(8), decoration: BoxDecoration( color: Colors.white, @@ -574,6 +676,24 @@ class _WaitingRoomPrivatePageState extends State { final bool isDisconnected = (connectYn == 'N'); final bool isMaster = (roomMasterYn == 'Y'); + // user가 방장인지 확인 + final isRoomMasterUser = (userData['user_seq']?.toString() ?? '') == _masterSeqString; + // user가 사회자인지 확인 + final participantType = (userData['participant_type'] ?? '').toString().toUpperCase(); + final isAdmin = (participantType == 'ADMIN'); + + // 아이콘 붙이기 + String roleIcon = ''; + if (isRoomMasterUser) { + // 방장 + roleIcon = '★ '; + } else if (isAdmin) { + // 사회자 + roleIcon = '☆ '; + } + + final displayName = '$roleIcon$userName'; + return GestureDetector( onTap: () async { final result = await showDialog( @@ -648,7 +768,7 @@ class _WaitingRoomPrivatePageState extends State { ), ), const SizedBox(height: 4), - Text(userName, style: const TextStyle(fontSize: 12)), + Text(displayName, style: const TextStyle(fontSize: 12, color: Colors.black)), ], ), ), diff --git a/lib/views/room/waiting_room_team_page.dart b/lib/views/room/waiting_room_team_page.dart index dfffed8..b9ff47e 100644 --- a/lib/views/room/waiting_room_team_page.dart +++ b/lib/views/room/waiting_room_team_page.dart @@ -63,8 +63,6 @@ class _WaitingRoomTeamPageState extends State { bool _movedToRunningPage = false; bool _kickedOut = false; - bool _roomTimeOut = false; - String _roomTimeOutMsg = ''; String mySeq = '0'; @@ -72,6 +70,8 @@ class _WaitingRoomTeamPageState extends State { Timer? _countdownTimer; Duration _remaining = const Duration(hours: 1); DateTime? _createDt; + bool _roomTimeOut = false; + String _roomExitYn = 'N'; /// (1) 광고 배너 관련 변수 BannerAd? _bannerAd; @@ -100,7 +100,9 @@ class _WaitingRoomTeamPageState extends State { // onDisconnect + connect_yn='Y' final myUserRef = _roomRef.child('userInfo').child(mySeq); - myUserRef.onDisconnect().update({'connect_yn': 'N'}); + if (_roomRef.child('userList').child(mySeq) == true) { + myUserRef.onDisconnect().update({'connect_yn': 'N'}); + } await myUserRef.update({'connect_yn': 'Y'}); _listenRoomData(); @@ -146,6 +148,7 @@ class _WaitingRoomTeamPageState extends State { roomTitle = '방 정보 없음'; _userList = []; }); + _roomMasterLeave(); return; } @@ -153,29 +156,32 @@ class _WaitingRoomTeamPageState extends State { final roomInfoData = data['roomInfo'] as Map? ?? {}; // final userInfoData = data['userInfo'] as Map? ?? {}; final userInfoDynamic = data['userInfo']; // 이건 List일 수 있음 + + final roomStatus = (roomInfoData['room_status'] ?? 'WAIT').toString().toUpperCase(); final tempList = >[]; // userList if (userInfoDynamic is Map) { userInfoDynamic.forEach((key, val) { - if (val is Map) { - tempList.add({ - 'user_seq': val['user_seq'].toString() ?? '0', - 'participant_type': val['participant_type'] ?? '', - 'nickname': val['nickname'] ?? '유저', - 'team_name': val['team_name'] ?? '', - 'score': val['score'] ?? 0, - 'profile_img': val['profile_img'] ?? '', - 'introduce_myself': val['introduce_myself'] ?? '', - 'ready_yn': (val['ready_yn'] ?? 'N').toString().toUpperCase(), - 'connect_yn': (val['connect_yn'] ?? 'Y').toString().toUpperCase(), - }); + final tempUserSeq = val['user_seq'] ?? '0'; + if (tempUserSeq != '0') { + if (val is Map) { + tempList.add({ + 'user_seq': val['user_seq'].toString() ?? '0', + 'participant_type': val['participant_type'] ?? '', + 'nickname': val['nickname'] ?? '유저', + 'team_name': val['team_name'] ?? '', + 'score': val['score'] ?? 0, + 'profile_img': val['profile_img'] ?? '', + 'introduce_myself': val['introduce_myself'] ?? '', + 'ready_yn': (val['ready_yn'] ?? 'N').toString().toUpperCase(), + 'connect_yn': (val['connect_yn'] ?? 'Y').toString().toUpperCase(), + }); + } } }); } - final roomStatus = (roomInfoData['room_status'] ?? 'WAIT').toString().toUpperCase(); - setState(() { roomTitle = (roomInfoData['room_title'] ?? '') as String; roomIntro = (roomInfoData['room_intro'] ?? '') as String; @@ -232,14 +238,7 @@ class _WaitingRoomTeamPageState extends State { if (createDtStr.isNotEmpty) { final dotIndex = createDtStr.indexOf('.'); final isoStr = createDtStr.substring(0, dotIndex); - print('isoStr: $isoStr'); - - final dt1 = DateTime.tryParse(createDtStr); - print('dt1: $dt1'); final dt = DateTime.tryParse(isoStr); - print('dt: $dt'); - final dt2 = DateTime.parse(isoStr); - print('dt2: $dt2'); if (dt != null) { setState(() { _createDt = dt; @@ -248,17 +247,25 @@ class _WaitingRoomTeamPageState extends State { } } - // 강퇴판별 + // (D) 내가 목록에서 사라졌는지 => 강퇴 판별 final amIStillHere = _userList.any((u) => u['user_seq'].toString() == mySeq); if (!amIStillHere && !_kickedOut && roomMasterYn != 'Y') { _kickedOut = true; - if (_roomTimeOut) { - _roomTimeOutMsg = '방장이 나갔습니다.'; - } else if (_kickedOut) { - _roomTimeOutMsg = '강퇴되었습니다.'; + if (_roomExitYn == 'N') { + showResponseDialog(context, '안내', '강퇴되었습니다.'); } Future.delayed(Duration.zero, () async { - await showResponseDialog(context, '안내', _roomTimeOutMsg); + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (_) => const MainPage()), + (route) => false, + ); + }); + } + // (D) 방 제한시간 종료 + if (_roomTimeOut) { + showResponseDialog(context, '안내', '방 제한시간이 종료되었습니다.'); + Future.delayed(Duration.zero, () async { Navigator.pushAndRemoveUntil( context, MaterialPageRoute(builder: (_) => const MainPage()), @@ -274,6 +281,18 @@ class _WaitingRoomTeamPageState extends State { }); } + // 방장이 나갔을 때 + void _roomMasterLeave() { + Future.delayed(Duration.zero, () async { + await showResponseDialog(context, '안내', '방장이 나갔습니다.'); + Navigator.pushAndRemoveUntil( + context, + MaterialPageRoute(builder: (_) => const MainPage()), + (route) => false, + ); + }); + } + // 카운트다운 void _startCountdownTimer() { if (_countdownTimer != null && _countdownTimer!.isActive) { @@ -282,8 +301,7 @@ class _WaitingRoomTeamPageState extends State { if (_createDt == null) return; _countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { - // final endTime = _createDt!.add(const Duration(hours: 1)); - final endTime = _createDt!.add(const Duration(minutes: 1)); + final endTime = _createDt!.add(const Duration(hours: 1)); final now = DateTime.now(); final diff = endTime.difference(now); @@ -303,6 +321,7 @@ class _WaitingRoomTeamPageState extends State { // 자동 종료 -> 방장=나가기(방삭제), 일반=나가기 setState(() { _roomTimeOut = true; + _roomExitYn = 'Y'; }); _requestLeaveRoom(); } @@ -371,8 +390,11 @@ class _WaitingRoomTeamPageState extends State { ); if (confirm != true) return; + // leave API + setState(() {_roomExitYn = 'Y';}); await _requestLeaveRoom(); } else { + setState(() {_roomExitYn = 'Y';}); await _requestLeaveRoom(); } } @@ -514,6 +536,13 @@ class _WaitingRoomTeamPageState extends State { setState(() => _isServerRequestLoading = false); return; } + // 팀 배정 안되어있는 사람 있는지 확인 + final notTeam = _userList.any((u) => (u['team_name'] ?? '').toString().toUpperCase() == 'WAIT'); + if (notTeam) { + showResponseDialog(context, '안내', '팀 배정이 안된 참가자가 있습니다.'); + setState(() => _isServerRequestLoading = false); + return; + } final requestBody = { "room_seq": "${widget.roomSeq}", @@ -527,11 +556,11 @@ class _WaitingRoomTeamPageState extends State { print('게임 시작 요청 성공(팀전)'); } else { // 게임 시작 요청 실패 - showResponseDialog(context, '오류', '게임 시작 요청에 실패했습니다.'); + showResponseDialog(context, response['response_info']['msg_title'], response['response_info']['msg_content']); } } else { // 게임 시작 요청 실패 - showResponseDialog(context, '오류', '게임 시작 요청에 실패했습니다.'); + showResponseDialog(context, response['response_info']['msg_title'], response['response_info']['msg_content']); } } catch (e) { // 게임 시작 요청 실패 @@ -637,14 +666,15 @@ class _WaitingRoomTeamPageState extends State { // } Widget _buildTeamSection() { - // final players = _userList.where((u) { - // final pType = (u['participant_type'] ?? '').toString().toUpperCase(); - // return (pType != 'ADMIN'); - // }).toList(); - final players = _userList.toList(); + final players = _userList.where((u) { + final t = (u['user_seq'] ?? null); + return t != null; + }).toList(); + // final players = _userList.toList(); final Map>> teamMap = {}; for (final tName in _teamNameList) { + if (tName == 'WAIT') continue; teamMap[tName] = []; } @@ -670,6 +700,7 @@ class _WaitingRoomTeamPageState extends State { return Column( crossAxisAlignment: CrossAxisAlignment.start, children: _teamNameList.map((teamName) { + if (teamName == 'WAIT') return const SizedBox(); final members = teamMap[teamName]!; return Container( @@ -725,12 +756,10 @@ class _WaitingRoomTeamPageState extends State { Widget _buildWaitSection() { final waitList = _userList.where((u) { - final pType = (u['participant_type'] ?? '').toString().toUpperCase(); - // if (pType == 'ADMIN') return false; final tName = (u['team_name'] ?? '').toString().toUpperCase(); - return (tName.isEmpty || tName == 'WAIT'); + return tName == 'WAIT'; }).toList(); - + if (waitList.isEmpty) return const SizedBox(); return Container(