diff --git a/.gitattributes b/.gitattributes new file mode 100644 index 0000000..f91f646 --- /dev/null +++ b/.gitattributes @@ -0,0 +1,12 @@ +# +# https://help.github.com/articles/dealing-with-line-endings/ +# +# Linux start script should use lf +/gradlew text eol=lf + +# These are Windows script files and should use crlf +*.bat text eol=crlf + +# Binary files should be left untouched +*.jar binary + diff --git a/.gitignore b/.gitignore index 79c113f..18eb7a0 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,9 @@ app.*.map.json /android/app/debug /android/app/profile /android/app/release + +# Ignore Gradle project-specific cache directory +.gradle + +# Ignore Gradle build output directory +build diff --git a/android/app/build.gradle b/android/app/build.gradle index 8b7bb63..e907d79 100644 --- a/android/app/build.gradle +++ b/android/app/build.gradle @@ -1,33 +1,33 @@ plugins { id "com.android.application" + // START: FlutterFire Configuration + 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.example.allscore_app" - compileSdk = flutter.compileSdkVersion + namespace = "com.allscore_app" + compileSdkVersion = 34 ndkVersion = flutter.ndkVersion compileOptions { - sourceCompatibility = JavaVersion.VERSION_1_8 - targetCompatibility = JavaVersion.VERSION_1_8 + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 } kotlinOptions { - jvmTarget = JavaVersion.VERSION_1_8 + jvmTarget = JavaVersion.VERSION_17 } defaultConfig { - // TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html). - applicationId = "com.example.allscore_app" - // You can update the following values to match your application needs. - // For more information, see: https://flutter.dev/to/review-gradle-config. - minSdk = flutter.minSdkVersion - targetSdk = flutter.targetSdkVersion - versionCode = flutter.versionCode - versionName = flutter.versionName + applicationId = "com.allscore_app" + minSdkVersion 23 + targetSdkVersion 34 + versionCode = 1 + versionName = "1.0" } buildTypes { diff --git a/android/app/google-services.json b/android/app/google-services.json new file mode 100644 index 0000000..e0156d4 --- /dev/null +++ b/android/app/google-services.json @@ -0,0 +1,30 @@ +{ + "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" + }, + "client": [ + { + "client_info": { + "mobilesdk_app_id": "1:70449524223:android:94ffb9ec98e508313e4bca", + "android_client_info": { + "package_name": "com.allscore_app" + } + }, + "oauth_client": [], + "api_key": [ + { + "current_key": "AIzaSyAJEItMxO-TemHGlveSKySG-eNaTD9XJI0" + } + ], + "services": { + "appinvite_service": { + "other_platform_oauth_client": [] + } + } + } + ], + "configuration_version": "1" +} \ No newline at end of file diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index af73524..0a3ecc1 100644 --- a/android/app/src/main/AndroidManifest.xml +++ b/android/app/src/main/AndroidManifest.xml @@ -1,8 +1,18 @@ - + + + + + + + + + android:resource="@style/NormalTheme" /> + + + + NSPhotoLibraryUsageDescription + 이 앱이 사진 라이브러리에 접근할 수 있도록 허용합니다. diff --git a/lib/dialogs/response_dialog.dart b/lib/dialogs/response_dialog.dart new file mode 100644 index 0000000..6d8aabf --- /dev/null +++ b/lib/dialogs/response_dialog.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; + +/// 원래는 void였던 것을 Future로 변경 +Future showResponseDialog(BuildContext context, String title, String message) { + return showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + backgroundColor: Colors.white, + title: Center( + child: Text( + title, + style: const TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + ), + content: Text( + message, + style: const TextStyle( + fontSize: 16, + color: Colors.black, + ), + ), + actions: [ + Center( + child: TextButton( + onPressed: () { + Navigator.of(context).pop(); // 이 Dialog를 닫음 + }, + style: ButtonStyle( + backgroundColor: MaterialStateProperty.all(Colors.black), + foregroundColor: MaterialStateProperty.all(Colors.white), + padding: MaterialStateProperty.all( + const EdgeInsets.symmetric(horizontal: 20, vertical: 10), + ), + ), + child: const Text('확인'), + ), + ), + ], + ); + }, + ); +} \ No newline at end of file diff --git a/lib/dialogs/room_detail_dialog.dart b/lib/dialogs/room_detail_dialog.dart new file mode 100644 index 0000000..5079a26 --- /dev/null +++ b/lib/dialogs/room_detail_dialog.dart @@ -0,0 +1,267 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../../plugins/api.dart'; // 서버 요청용 (Api.serverRequest) +import '../../dialogs/response_dialog.dart'; // 오류/알림 모달 +import '../../views/room/waiting_room_team_page.dart'; // 팀전 대기방 예: import 경로 +import '../../views/room/waiting_room_private_page.dart'; // 개인전 대기방 예: import 경로 + +/// 분리된 방 상세 모달 +class RoomDetailDialog extends StatefulWidget { + final Map roomData; + + const RoomDetailDialog({Key? key, required this.roomData}) : super(key: key); + + @override + State createState() => _RoomDetailDialogState(); +} + +class _RoomDetailDialogState extends State { + late String roomTitle; + late String roomIntro; + late String roomStatus; // '대기중'/'진행중'/'종료' + late String openYn; // '공개'/'비공개' + late bool isPrivate; + late bool isWait; + late bool isRunning; + late bool isFinish; + + /// 서버에 전달할 방 번호 / 방 타입 + late int roomSeq; + late String roomType; // "private" 또는 "team" 등 + + /// 비밀번호 입력 컨트롤러 (비공개 + 대기중일 때만 표시) + final TextEditingController _pwController = TextEditingController(); + + @override + void initState() { + super.initState(); + // roomData에서 필요한 필드 추출 + roomTitle = widget.roomData['room_title'] ?? '(방제목 없음)'; + roomIntro = widget.roomData['room_intro'] ?? ''; + roomStatus = widget.roomData['room_status'] ?? '대기중'; + openYn = widget.roomData['open_yn'] ?? '공개'; + + // 서버에 전송할 정보 + roomSeq = widget.roomData['room_seq'] ?? 0; + roomType = widget.roomData['room_type'] ?? 'private'; + // ※ 서버에 "TEAM"/"team" 식으로 넘길지 확인 필요 + + // '비공개'이면 true + isPrivate = (openYn == '비공개'); + // 상태별 Flag + isWait = (roomStatus == '대기중'); + isRunning = (roomStatus == '진행중'); + isFinish = (roomStatus == '종료'); + } + + @override + Widget build(BuildContext context) { + 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, // 내용만큼 높이 + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // (A) 상단에 방 제목 (가운데 정렬) + Center( + child: Text( + roomTitle, + style: const TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + ), + const SizedBox(height: 20), + + // (B) "방 소개" 라벨 + const Text( + '방 소개', + style: TextStyle( + fontWeight: FontWeight.bold, + color: Colors.black, + fontSize: 14, + ), + ), + const SizedBox(height: 6), + + // (C) 방 소개 영역 + Container( + height: 80, + padding: const EdgeInsets.all(8), + child: SingleChildScrollView( + child: Text( + roomIntro.isNotEmpty ? roomIntro : '소개글이 없습니다.', + style: const TextStyle(color: Colors.black), + ), + ), + ), + const SizedBox(height: 16), + + // (D) 공개/비공개 표시 + Text( + isPrivate ? '비공개방' : '공개방', + style: const TextStyle( + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + // (D-1) 비밀번호 필드 (비공개 + 대기중) + if (isPrivate && isWait) ...[ + const SizedBox(height: 16), + TextField( + controller: _pwController, + obscureText: true, + decoration: const InputDecoration( + labelText: '비밀번호', + labelStyle: TextStyle(color: Colors.black), + border: OutlineInputBorder(), + ), + ), + ], + const SizedBox(height: 24), + + // (E) 하단 버튼 + _buildBottomButton(), + ], + ), + ), + ); + } + + /// 하단 버튼 구역 + Widget _buildBottomButton() { + if (isWait) { + // (A) 대기중 -> "입장" + "닫기" + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + _buildBlackButton( + label: '입장', + onTap: _onEnterRoom, // 실제 입장 로직 + ), + _buildBlackButton( + label: '닫기', + onTap: () => Navigator.pop(context), + ), + ], + ); + } else if (isRunning) { + // (B) 진행중 -> "확인" (중앙 정렬) + return Center( + child: _buildBlackButton( + label: '확인', + onTap: () => Navigator.pop(context), + ), + ); + } else { + // (C) 종료 -> "결과보기", "확인" (두 버튼 크기 동일) + return Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + SizedBox( + width: 100, + child: _buildBlackButton( + label: '결과보기', + onTap: () { + // TODO: 결과보기 로직 + Navigator.pop(context); + }, + ), + ), + SizedBox( + width: 100, + child: _buildBlackButton( + label: '확인', + onTap: () => Navigator.pop(context), + ), + ), + ], + ); + } + } + + /// "입장" 버튼 로직 + Future _onEnterRoom() async { + final pw = _pwController.text.trim(); + + // 서버 API 요청 바디 + final requestBody = { + "room_seq": "$roomSeq", + "room_type": roomType, + "room_pw": pw, // 비공개 방이면 비번 or 공백 + }; + + try { + final response = await Api.serverRequest( + uri: '/room/score/enter/room', + body: requestBody, + ); + + if (response == null || response['result'] != 'OK') { + // 통신 자체 실패 + showResponseDialog(context, '오류', '방 입장 실패. 서버 통신 오류.'); + return; + } + + final resp = response['response'] ?? {}; + if (resp['result'] == 'OK') { + // 성공 -> 모달 닫고 대기방 페이지 이동 + Navigator.pop(context); // 현재 모달 닫기 + + // room_type에 따라 개인전/팀전 대기방 이동 + if (roomType.toLowerCase() == 'team') { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (_) => WaitingRoomTeamPage( + roomSeq: roomSeq, + roomType: 'team', + ), + ), + ); + } else { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (_) => WaitingRoomPrivatePage( + roomSeq: roomSeq, + roomType: 'private', + ), + ), + ); + } + } else { + // 내부 실패 + final msgTitle = resp['response_info']?['msg_title'] ?? '방 입장 실패'; + final msgContent = resp['response_info']?['msg_content'] ?? '오류가 발생했습니다.'; + showResponseDialog(context, msgTitle, msgContent); + } + } catch (e) { + showResponseDialog(context, '오류', '서버 요청 중 예외 발생: $e'); + } + } + + /// 공통 버튼 스타일 + Widget _buildBlackButton({ + required String label, + required VoidCallback onTap, + }) { + return ElevatedButton( + onPressed: onTap, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.black, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)), + ), + child: Text(label, style: const TextStyle(fontSize: 14)), + ); + } +} diff --git a/lib/dialogs/room_setting_dialog.dart b/lib/dialogs/room_setting_dialog.dart new file mode 100644 index 0000000..5d2200c --- /dev/null +++ b/lib/dialogs/room_setting_dialog.dart @@ -0,0 +1,490 @@ +import 'package:flutter/material.dart'; +import 'package:firebase_database/firebase_database.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../../plugins/api.dart'; // 서버 API 요청 +import 'response_dialog.dart'; // 요청 결과 모달창 + +class RoomSettingModal extends StatefulWidget { + final Map roomInfo; + // 예: { + // "room_seq": "13", + // "room_master_yn": "Y", + // "room_title": "...", + // "room_type": "private" or "team" + // ... + // } + + const RoomSettingModal({Key? key, required this.roomInfo}) : super(key: key); + + @override + State createState() => _RoomSettingModalState(); +} + +class _RoomSettingModalState extends State { + // ───────────────────────────────────────────── + // 로컬 상태 + // ───────────────────────────────────────────── + late bool isMaster; // 방장 여부 + String openYn = 'Y'; // 공개/비공개 + String roomPw = ''; // 비공개 시 비번 + late int roomSeq; // 방 번호 + String roomTitle = ''; // 방 제목 + String roomIntro = ''; // 방 소개 + int runningTime = 1; // 운영 시간 + int numberOfPeople = 10; // 최대 인원 + String scoreOpenRange = 'PRIVATE'; // 점수 공개 범위 (PRIVATE / TEAM / ALL) + + // FRD 관련 + late DatabaseReference _roomRef; + bool _isLoading = true; + + // 방이 개인전인지 팀전인지 구분(편의를 위해 로컬 변수 사용) + late bool isPrivateType; // true이면 개인전, false이면 팀전 + + @override + void initState() { + super.initState(); + + // (1) room_seq + roomSeq = int.tryParse('${widget.roomInfo['room_seq'] ?? '0'}') ?? 0; + + // (2) 방 타입 + final roomTypeStr = (widget.roomInfo['room_type'] ?? 'private').toString().toLowerCase(); + // room_type 이 "private"면 개인전, 아니면 팀전으로 처리 (예: "team") + isPrivateType = (roomTypeStr == 'private'); + + // (3) firebase ref + final roomKey = 'korea-$roomSeq'; + _roomRef = FirebaseDatabase.instance.ref('rooms/$roomKey/roomInfo'); + + // (4) 내 user_seq와 방장 user_seq 비교 + FRD에서 roomInfo 조회 + _checkMasterAndFetchData(); + } + + /// 로컬스토리지에서 my_user_seq를 불러오고, + /// FRD에서 roomInfo를 읽어서 state 업데이트 + Future _checkMasterAndFetchData() async { + final prefs = await SharedPreferences.getInstance(); + final myUserSeq = prefs.getInt('my_user_seq') ?? 0; + + final snapshot = await _roomRef.get(); + if (!snapshot.exists) { + // 방 정보 없음 + setState(() { + _isLoading = false; + isMaster = false; + roomTitle = '방 정보 없음'; + }); + return; + } + + final data = snapshot.value as Map? ?? {}; + // master_user_seq, open_yn, etc + final masterSeq = data['master_user_seq'] ?? 0; + + setState(() { + isMaster = (masterSeq.toString() == myUserSeq.toString()); + + // 각 필드들 + roomTitle = data['room_title']?.toString() ?? ''; + roomIntro = data['room_intro']?.toString() ?? ''; + openYn = data['open_yn']?.toString() ?? 'Y'; + roomPw = data['room_pw']?.toString() ?? ''; + runningTime = _toInt(data['running_time'], 1); + numberOfPeople = _toInt(data['number_of_people'], 10); + scoreOpenRange = data['score_open_range']?.toString() ?? 'PRIVATE'; + + _isLoading = false; + }); + } + + /// 단순 int 변환 유틸 + int _toInt(dynamic val, int defaultVal) { + if (val == null) return defaultVal; + if (val is int) return val; + if (val is String) { + return int.tryParse(val) ?? defaultVal; + } + return defaultVal; + } + + /// (수정) 버튼 클릭 + Future _onUpdate() async { + // 서버 API로 방 설정 수정 + final requestBody = { + 'room_seq': '$roomSeq', + 'room_status': 'WAIT', + 'room_title': roomTitle, + 'room_intro': roomIntro, + 'open_yn': openYn, + 'room_pw': roomPw, + 'running_time': '$runningTime', + // widget.roomInfo['room_type']가 있다면 그대로 전송 + 'room_type': widget.roomInfo['room_type'] ?? 'private', + 'number_of_people': '$numberOfPeople', + 'score_open_range': scoreOpenRange, + }; + + // (팀전인 경우에만 number_of_teams 추가) + if (!isPrivateType) { + // 예: 임의로 4 라고 가정 + // 실제로는 FRD에서 roomInfo['number_of_teams'] 를 가져올 수도 있음 + requestBody['number_of_teams'] = '4'; + } + + try { + final response = await Api.serverRequest( + uri: '/room/score/update/room/setting/info', + body: requestBody, + ); + + if (response['result'] == 'OK') { + final resp = response['response'] ?? {}; + final serverResult = resp['result'] ?? 'FAIL'; + if (serverResult == 'OK') { + await showResponseDialog( + context, + '성공', + '방 설정이 성공적으로 수정되었습니다.', + ); + Navigator.pop(context, 'refresh'); + } 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, '오류 발생', '서버 요청 중 오류가 발생했습니다.\n$e'); + } + } + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: _isLoading + ? const SizedBox( + height: 100, + child: Center(child: CircularProgressIndicator()), + ) + : SingleChildScrollView( + padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + // 모달 상단 제목 + Text( + '방 설정 정보', + style: TextStyle( + fontSize: 18, + fontWeight: FontWeight.bold, + color: Colors.black.withOpacity(0.9), + ), + ), + const SizedBox(height: 12), + + // (1) 방 제목 + _buildTitle('방 제목'), + TextField( + readOnly: !isMaster, + controller: TextEditingController(text: roomTitle), + onChanged: (value) => roomTitle = value, + style: const TextStyle(color: Colors.black), + decoration: InputDecoration( + border: OutlineInputBorder( + borderSide: BorderSide(color: Colors.black.withOpacity(0.8)), + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + isDense: true, + ), + ), + const SizedBox(height: 12), + + // (2) 방 소개 + _buildTitle('방 소개'), + SizedBox( + height: 60, + child: TextField( + readOnly: !isMaster, + controller: TextEditingController(text: roomIntro), + onChanged: (value) => roomIntro = value, + maxLines: null, + expands: true, + style: const TextStyle(color: Colors.black), + textAlignVertical: TextAlignVertical.top, + decoration: InputDecoration( + border: OutlineInputBorder( + borderSide: BorderSide(color: Colors.black.withOpacity(0.8)), + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + isDense: true, + ), + ), + ), + const SizedBox(height: 12), + + // (3) 비밀번호 설정 (공개/비공개) + _buildTitle('비밀번호 설정'), + Row( + children: [ + Radio( + value: 'Y', + groupValue: openYn, + activeColor: Colors.black, + onChanged: isMaster + ? (val) { + setState(() { + openYn = val ?? 'Y'; + }); + } + : null, + ), + const Text('공개', style: TextStyle(color: Colors.black)), + const SizedBox(width: 8), + Radio( + value: 'N', + groupValue: openYn, + activeColor: Colors.black, + onChanged: isMaster + ? (val) { + setState(() { + openYn = val ?? 'N'; + }); + } + : null, + ), + const Text('비공개', style: TextStyle(color: Colors.black)), + ], + ), + if (openYn == 'N') ...[ + SizedBox( + height: 40, + child: TextField( + readOnly: !isMaster, + obscureText: true, + onChanged: (value) => roomPw = value, + style: const TextStyle(color: Colors.black), + decoration: InputDecoration( + border: OutlineInputBorder( + borderSide: BorderSide(color: Colors.black.withOpacity(0.8)), + ), + hintText: '비밀번호 입력', + hintStyle: TextStyle(color: Colors.black.withOpacity(0.4)), + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + isDense: true, + ), + ), + ), + const SizedBox(height: 12), + ], + + // (4) 운영시간 + _buildTitle('운영시간'), + Row( + children: [ + DropdownButton( + value: runningTime, + dropdownColor: Colors.white, + style: const TextStyle(color: Colors.black), + underline: Container( + height: 1, + color: Colors.black.withOpacity(0.8), + ), + items: [1, 2, 3, 4, 5, 6] + .map((e) => DropdownMenuItem( + value: e, + child: Text('$e', style: const TextStyle(color: Colors.black)), + )) + .toList(), + onChanged: isMaster + ? (val) { + if (val == null) return; + setState(() { + runningTime = val; + }); + } + : null, + ), + const SizedBox(width: 8), + const Text('시간', style: TextStyle(color: Colors.black)), + ], + ), + const SizedBox(height: 12), + + // (5) 최대 인원수 + _buildTitle('최대 인원수'), + Row( + children: [ + SizedBox( + width: 80, + height: 40, + child: TextField( + readOnly: !isMaster, + controller: TextEditingController(text: '$numberOfPeople'), + keyboardType: TextInputType.number, + onChanged: (value) { + setState(() { + numberOfPeople = int.tryParse(value) ?? numberOfPeople; + }); + }, + style: const TextStyle(color: Colors.black), + decoration: InputDecoration( + border: OutlineInputBorder( + borderSide: BorderSide(color: Colors.black.withOpacity(0.8)), + ), + contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8), + isDense: true, + ), + ), + ), + const SizedBox(width: 8), + const Text('명', style: TextStyle(color: Colors.black)), + ], + ), + const SizedBox(height: 12), + + // (6) 점수 공개 범위 + _buildTitle('점수 공개 범위'), + // 개인전이면 PRIVATE, ALL 두 가지만 노출 + // 팀전이면 PRIVATE, TEAM, ALL 세 가지 노출 + if (isPrivateType) + Column( + children: [ + // PRIVATE + RadioListTile( + value: 'PRIVATE', + groupValue: scoreOpenRange, + activeColor: Colors.black, + title: const Text('개인', style: TextStyle(color: Colors.black)), + onChanged: isMaster + ? (val) { + if (val != null) { + setState(() => scoreOpenRange = val); + } + } + : null, + ), + // ALL + RadioListTile( + value: 'ALL', + groupValue: scoreOpenRange, + activeColor: Colors.black, + title: const Text('전체', style: TextStyle(color: Colors.black)), + onChanged: isMaster + ? (val) { + if (val != null) { + setState(() => scoreOpenRange = val); + } + } + : null, + ), + ], + ) + else + // 팀전이면 PRIVATE / TEAM / ALL + Column( + children: [ + RadioListTile( + value: 'PRIVATE', + groupValue: scoreOpenRange, + activeColor: Colors.black, + title: const Text('개인', style: TextStyle(color: Colors.black)), + onChanged: isMaster + ? (val) { + if (val != null) { + setState(() => scoreOpenRange = val); + } + } + : null, + ), + RadioListTile( + value: 'TEAM', + groupValue: scoreOpenRange, + activeColor: Colors.black, + title: const Text('팀', style: TextStyle(color: Colors.black)), + onChanged: isMaster + ? (val) { + if (val != null) { + setState(() => scoreOpenRange = val); + } + } + : null, + ), + RadioListTile( + value: 'ALL', + groupValue: scoreOpenRange, + activeColor: Colors.black, + title: const Text('전체', style: TextStyle(color: Colors.black)), + onChanged: isMaster + ? (val) { + if (val != null) { + setState(() => scoreOpenRange = val); + } + } + : null, + ), + ], + ), + const SizedBox(height: 20), + + // (7) 하단 버튼 + if (isMaster) + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton( + onPressed: _onUpdate, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.black, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + ), + child: const Text('수정', style: TextStyle(color: Colors.white)), + ), + ElevatedButton( + onPressed: () => Navigator.pop(context), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.black, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12), + ), + child: const Text('확인', style: TextStyle(color: Colors.white)), + ), + ], + ) + else + Center( + child: ElevatedButton( + onPressed: () => Navigator.pop(context), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.black, + padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 12), + ), + child: const Text('확인', style: TextStyle(color: Colors.white)), + ), + ), + ], + ), + ), + ); + } + + /// 블랙 앤 화이트 컨셉의 타이틀(레이블) 텍스트 + Widget _buildTitle(String label) { + return Align( + alignment: Alignment.centerLeft, + child: Text( + label, + style: TextStyle( + fontWeight: FontWeight.bold, + fontSize: 14, + color: Colors.black.withOpacity(0.9), + ), + ), + ); + } +} diff --git a/lib/dialogs/score_edit_dialog.dart b/lib/dialogs/score_edit_dialog.dart new file mode 100644 index 0000000..8437d1e --- /dev/null +++ b/lib/dialogs/score_edit_dialog.dart @@ -0,0 +1,172 @@ +import 'package:flutter/material.dart'; +import '../plugins/api.dart'; // 서버 요청 +import 'response_dialog.dart'; // 응답 모달 etc + +class ScoreEditDialog extends StatefulWidget { + final int roomSeq; + final String roomType; // "PRIVATE" or "TEAM" + final Map userData; + + const ScoreEditDialog({ + Key? key, + required this.roomSeq, + required this.roomType, + required this.userData, + }) : super(key: key); + + @override + State createState() => _ScoreEditDialogState(); +} + +class _ScoreEditDialogState extends State { + late int currentScore; // 현재 점수 + late int newScore; // 수정 후 점수 + + @override + void initState() { + super.initState(); + currentScore = (widget.userData['score'] ?? 0) as int; + newScore = currentScore; + } + + Future _onApplyScore() async { + final reqBody = { + "room_seq": "${widget.roomSeq}", + "room_type": widget.roomType, + "target_user_seq": "${widget.userData['user_seq']}", + "after_score": "$newScore", + }; + + try { + final response = await Api.serverRequest( + uri: '/room/score/update/score', + body: reqBody, + ); + if (response['result'] == 'OK') { + final resp = response['response'] ?? {}; + if (resp['result'] == 'OK') { + // 성공 + await showResponseDialog(context, '성공', '점수가 업데이트되었습니다.'); + Navigator.pop(context, 'refresh'); + } 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'); + } + } + + void _onDelta(int delta) { + setState(() { + newScore += delta; + if (newScore < 0) newScore = 0; // 최소 0점이라고 가정 + if (newScore > 999999) newScore = 999999; // 임의 최대치 + }); + } + + @override + Widget build(BuildContext context) { + final userName = widget.userData['nickname'] ?? '유저'; + final department = widget.userData['department'] ?? ''; + final introduce = widget.userData['introduce_myself'] ?? ''; + + return Dialog( + backgroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Text('유저 정보 보기', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const SizedBox(height: 12), + // 닉네임 & 소속 + Text(userName, style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + Text(department, style: TextStyle(fontSize: 14, color: Colors.grey)), + + const SizedBox(height: 12), + // 소개 + const Align( + alignment: Alignment.centerLeft, + child: Text('소개글', style: TextStyle(fontWeight: FontWeight.bold)), + ), + const SizedBox(height: 4), + Container( + width: double.infinity, + height: 80, + decoration: BoxDecoration( + border: Border.all(color: Colors.black), + borderRadius: BorderRadius.circular(8), + ), + child: SingleChildScrollView( + padding: const EdgeInsets.all(8), + child: Text(introduce.isNotEmpty ? introduce : '소개글이 없습니다.'), + ), + ), + + const SizedBox(height: 12), + // 점수 수정 영역 + Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Text('$currentScore', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + const Text(' → '), + Text('$newScore', + style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + ], + ), + const SizedBox(height: 8), + Wrap( + spacing: 4, + runSpacing: 4, + children: [ + _buildDeltaButton(-100), + _buildDeltaButton(-10), + _buildDeltaButton(-1), + _buildDeltaButton(1), + _buildDeltaButton(10), + _buildDeltaButton(100), + ], + ), + + const SizedBox(height: 12), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + ElevatedButton( + onPressed: _onApplyScore, + style: ElevatedButton.styleFrom(backgroundColor: Colors.black), + child: const Text('적용', style: TextStyle(color: Colors.white)), + ), + ElevatedButton( + onPressed: () => Navigator.pop(context), + style: ElevatedButton.styleFrom(backgroundColor: Colors.black), + child: const Text('닫기', style: TextStyle(color: Colors.white)), + ), + ], + ), + ], + ), + ), + ); + } + + Widget _buildDeltaButton(int delta) { + final label = (delta >= 0) ? '+$delta' : '$delta'; + return ElevatedButton( + onPressed: () => _onDelta(delta), + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.black, + side: BorderSide(color: Colors.black), + ), + child: Text(label), + ); + } +} diff --git a/lib/dialogs/settings_dialog.dart b/lib/dialogs/settings_dialog.dart new file mode 100644 index 0000000..831cf8f --- /dev/null +++ b/lib/dialogs/settings_dialog.dart @@ -0,0 +1,88 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; // SharedPreferences 임포트 +import '../views/login/login_page.dart'; // 로그인 페이지 임포트 (상위 디렉토리로 이동) +import '../views/user/my_page.dart'; // 마이페이지 임포트 (상위 디렉토리로 이동) + +void showSettingsDialog(BuildContext context) { + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + backgroundColor: Colors.white, + title: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + Container( + width: MediaQuery.of(context).size.width * 0.2, + child: const Text( + '', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + ), + Container( + width: MediaQuery.of(context).size.width * 0.2, + child: const Text( + '설정', + style: TextStyle( + fontSize: 24, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + ), + IconButton( + icon: const Icon(Icons.close, color: Colors.black), + onPressed: () { + Navigator.of(context).pop(); + }, + ), + ], + ), + content: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const SizedBox(height: 10), + SizedBox( + width: double.infinity, + child: TextButton( + onPressed: () { + Navigator.of(context).push( + MaterialPageRoute(builder: (context) => const MyPage()), // 마이페이지로 이동 + ); + }, + style: ButtonStyle( + side: MaterialStateProperty.all(const BorderSide(color: Colors.black)), + foregroundColor: MaterialStateProperty.all(Colors.black), + ), + child: const Text('내 정보 관리'), + ), + ), + const SizedBox(height: 10), + SizedBox( + width: double.infinity, + child: TextButton( + onPressed: () async { + // 로그아웃 클릭 시 동작 + SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.setString('auth_token', ''); // auth_token 초기화 + Navigator.of(context).pushReplacement( + MaterialPageRoute(builder: (context) => const LoginPage()), // 로그인 페이지로 이동 + ); + }, + style: ButtonStyle( + side: MaterialStateProperty.all(const BorderSide(color: Colors.black)), + foregroundColor: MaterialStateProperty.all(Colors.black), + ), + child: const Text('로그아웃'), + ), + ), + ], + ), + ); + }, + ); +} \ No newline at end of file diff --git a/lib/dialogs/team_name_edit_dialog.dart b/lib/dialogs/team_name_edit_dialog.dart new file mode 100644 index 0000000..9db08b9 --- /dev/null +++ b/lib/dialogs/team_name_edit_dialog.dart @@ -0,0 +1,148 @@ +import 'package:flutter/material.dart'; +import 'response_dialog.dart'; +import '../../plugins/api.dart'; + +class TeamNameEditModal extends StatefulWidget { + final int roomSeq; // 방 번호 + final String roomTypeName; // "TEAM" + final String beforeTeamName; // 현재 팀명 + final List existingTeamNames; // 이미 존재하는 팀명들 (ex: ["A","B","C","WAIT"...] + + const TeamNameEditModal({ + Key? key, + required this.roomSeq, + required this.roomTypeName, + required this.beforeTeamName, + required this.existingTeamNames, + }) : super(key: key); + + @override + State createState() => _TeamNameEditModalState(); +} + +class _TeamNameEditModalState extends State { + String afterTeamName = ''; + String _errorMsg = ''; // 팀명 중복 에러 등 표시 + + Future _onUpdateTeamName() async { + // 새 팀명이 비었거나 기존 팀명과 같으면 처리 + final newName = afterTeamName.trim().toUpperCase(); + if (newName.isEmpty) { + setState(() { + _errorMsg = '새 팀명을 입력해주세요.'; + }); + return; + } + // 중복 검사 (단, 기존 teamName과 동일하면 OK) + // 예: beforeTeamName= "A", user가 "B" → existingTeamNames=["A","B","C"] 이면 중복 + final existingNames = widget.existingTeamNames.map((e) => e.toUpperCase()).toList(); + if (newName != widget.beforeTeamName.toUpperCase() && existingNames.contains(newName)) { + setState(() { + _errorMsg = '이미 존재하는 팀명입니다.'; + }); + return; + } + + // 서버 요청 body + // { + // "room_seq": "9", + // "room_type_name": "TEAM", + // "before_team_name": "A", + // "after_team_name": "B" + // } + final reqBody = { + 'room_seq': '${widget.roomSeq}', + 'room_type_name': widget.roomTypeName, // "TEAM" + 'before_team_name': widget.beforeTeamName, + 'after_team_name': newName, + }; + + try { + final response = await Api.serverRequest( + uri: '/room/score/update/team/name', + body: reqBody, + ); + if (response['result'] == 'OK') { + final resp = response['response'] ?? {}; + if (resp['result'] == 'OK') { + // 성공 + await showResponseDialog(context, '성공', '팀명이 성공적으로 수정되었습니다.'); + Navigator.pop(context, 'refresh'); + } 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'); + } + } + + @override + Widget build(BuildContext context) { + return Dialog( + backgroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('팀명 수정', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const SizedBox(height: 12), + Text('기존 팀명: ${widget.beforeTeamName}'), + const SizedBox(height: 8), + TextField( + decoration: const InputDecoration( + border: OutlineInputBorder(), + labelText: '새 팀명', + ), + onChanged: (val) { + setState(() { + afterTeamName = val; + _errorMsg = ''; // 에러 메시지 초기화 + }); + }, + ), + // 에러 메시지 + if (_errorMsg.isNotEmpty) ...[ + const SizedBox(height: 8), + Text( + _errorMsg, + style: const TextStyle(color: Colors.red), + ), + ], + const SizedBox(height: 20), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + SizedBox( + width: 100, + child: ElevatedButton( + onPressed: _onUpdateTeamName, + style: ElevatedButton.styleFrom(backgroundColor: Colors.black), + child: + const Text('수정', style: TextStyle(color: Colors.white)), + ), + ), + SizedBox( + width: 100, + child: ElevatedButton( + onPressed: () => Navigator.pop(context), + style: ElevatedButton.styleFrom(backgroundColor: Colors.black), + child: + const Text('취소', style: TextStyle(color: Colors.white)), + ), + ), + ], + ), + ], + ), + ), + ); + } +} diff --git a/lib/dialogs/user_info_basic_dialog.dart b/lib/dialogs/user_info_basic_dialog.dart new file mode 100644 index 0000000..90ee8cf --- /dev/null +++ b/lib/dialogs/user_info_basic_dialog.dart @@ -0,0 +1,64 @@ +/// user_info_basic_dialog.dart +import 'package:flutter/material.dart'; + +class UserInfoBasicDialog extends StatelessWidget { + final Map userData; + + const UserInfoBasicDialog({ + Key? key, + required this.userData, + }) : super(key: key); + + @override + Widget build(BuildContext context) { + final userName = userData['nickname'] ?? '유저'; + final department = userData['department'] ?? ''; + final introduce = userData['introduce_myself'] ?? ''; + + return Dialog( + backgroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('유저 정보 (진행중-개인전)', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const SizedBox(height: 12), + + Text(userName, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + const SizedBox(height: 4), + Text(department, style: const TextStyle(fontSize: 14, color: Colors.grey)), + + const SizedBox(height: 12), + const Align( + alignment: Alignment.centerLeft, + child: Text('소개글', style: TextStyle(fontWeight: FontWeight.bold)), + ), + const SizedBox(height: 4), + Container( + width: double.infinity, + height: 80, + decoration: BoxDecoration( + border: Border.all(color: Colors.black), + borderRadius: BorderRadius.circular(8), + ), + child: SingleChildScrollView( + padding: const EdgeInsets.all(8), + child: Text(introduce.isNotEmpty ? introduce : '소개글이 없습니다.'), + ), + ), + + const SizedBox(height: 16), + ElevatedButton( + onPressed: () => Navigator.pop(context), + style: ElevatedButton.styleFrom(backgroundColor: Colors.black), + child: const Text('확인', style: TextStyle(color: Colors.white)), + ), + ], + ), + ), + ); + } +} diff --git a/lib/dialogs/user_info_private_dialog.dart b/lib/dialogs/user_info_private_dialog.dart new file mode 100644 index 0000000..5a73474 --- /dev/null +++ b/lib/dialogs/user_info_private_dialog.dart @@ -0,0 +1,272 @@ +import 'package:flutter/material.dart'; +import 'response_dialog.dart'; +import '../../plugins/api.dart'; + +class UserInfoPrivateDialog extends StatefulWidget { + final Map userData; + final bool isRoomMaster; // 현재 로그인 유저가 방장인지 + final int roomSeq; + final String roomTypeName; // "PRIVATE" + + const UserInfoPrivateDialog({ + Key? key, + required this.userData, + required this.isRoomMaster, + required this.roomSeq, + required this.roomTypeName, + }) : super(key: key); + + @override + State createState() => _UserInfoPrivateDialogState(); +} + +class _UserInfoPrivateDialogState extends State { + late String participantType; // 'ADMIN' or 'PLAYER' + late String introduceMyself; + + @override + void initState() { + super.initState(); + final rawType = (widget.userData['participant_type'] ?? 'PLAYER').toString().toUpperCase(); + participantType = (rawType == 'ADMIN') ? 'ADMIN' : 'PLAYER'; + introduceMyself = widget.userData['introduce_myself'] ?? ''; + } + + /// 역할 변경 API (방장 전용) + Future _onUpdateUserInfo() async { + // 방장만 수정하기 가능 + if (!widget.isRoomMaster) return; + + final requestBody = { + 'room_seq': '${widget.roomSeq}', + 'room_type_name': widget.roomTypeName, // "PRIVATE" + 'target_user_seq': '${widget.userData['user_seq']}', + 'participant_type': participantType, + }; + + try { + final response = await Api.serverRequest( + uri: '/room/score/update/user/role', + body: requestBody, + ); + if (response['result'] == 'OK') { + final resp = response['response'] ?? {}; + if (resp['result'] == 'OK') { + await showResponseDialog(context, '성공', '역할이 성공적으로 수정되었습니다.'); + Navigator.pop(context, 'refresh'); + } 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'); + } + } + + /// 새로 추가: "추방하기" (방장 전용) + Future _onKickParticipant() async { + // 방장이 아닌데 추방 시도 -> 그냥 return + if (!widget.isRoomMaster) return; + + final reqBody = { + "room_seq": "${widget.roomSeq}", + "target_user_seq": "${widget.userData['user_seq']}", + }; + + try { + final response = await Api.serverRequest( + uri: '/room/score/kick/participant', + body: reqBody, + ); + if (response['result'] == 'OK') { + final resp = response['response'] ?? {}; + if (resp['result'] == 'OK') { + // 성공 + await showResponseDialog(context, '성공', '해당 유저가 강퇴되었습니다.'); + Navigator.pop(context, 'refresh'); + } 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'); + } + } + + @override + Widget build(BuildContext context) { + final userName = widget.userData['nickname'] ?? '유저'; + final department = widget.userData['department'] ?? '소속정보없음'; + final profileImg = widget.userData['profile_img'] ?? ''; + + return Dialog( + backgroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + '유저 정보 (개인전)', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 12), + + // (A) 프로필 영역 + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Container( + width: 80, height: 80, + decoration: BoxDecoration( + border: Border.all(color: Colors.black), + borderRadius: BorderRadius.circular(16), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + 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), + ), + ) + : const Center(child: Text('이미지\n없음', textAlign: TextAlign.center)), + ), + ), + const SizedBox(width: 16), + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text( + userName, + style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 6), + Text( + department, + style: const TextStyle(fontSize: 14), + ), + ], + ), + ), + ], + ), + const SizedBox(height: 16), + + // (B) 방장이면 역할 수정 가능 + if (widget.isRoomMaster) ...[ + Row( + children: [ + const Text('역할: ', style: TextStyle(fontWeight: FontWeight.bold)), + DropdownButton( + value: participantType, + items: const [ + DropdownMenuItem(value: 'ADMIN', child: Text('사회자')), + DropdownMenuItem(value: 'PLAYER', child: Text('참가자')), + ], + onChanged: (val) { + if (val == null) return; + setState(() { + participantType = val; + }); + }, + ), + ], + ), + const SizedBox(height: 12), + ] else ...[ + Text('역할: $participantType', style: const TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 12), + ], + + // (C) 소개 + const Align( + alignment: Alignment.centerLeft, + child: Text('소개', style: TextStyle(fontWeight: FontWeight.bold)), + ), + const SizedBox(height: 4), + Container( + width: double.infinity, + height: 100, + decoration: BoxDecoration( + border: Border.all(color: Colors.black), + borderRadius: BorderRadius.circular(8), + ), + child: SingleChildScrollView( + padding: const EdgeInsets.all(8), + child: Text( + introduceMyself.isNotEmpty ? introduceMyself : '소개글이 없습니다.', + style: const TextStyle(fontSize: 14), + softWrap: true, + maxLines: 100, + overflow: TextOverflow.clip, + ), + ), + ), + const SizedBox(height: 16), + + // (D) 하단 버튼 + if (widget.isRoomMaster) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + // (D-1) 역할 수정하기 + SizedBox( + width: 90, + child: ElevatedButton( + onPressed: _onUpdateUserInfo, + style: ElevatedButton.styleFrom(backgroundColor: Colors.black), + child: FittedBox( + child: Text('수정하기', style: TextStyle(color: Colors.white)), + ), + ), + ), + // (D-2) 추방하기 + SizedBox( + width: 90, + child: ElevatedButton( + onPressed: _onKickParticipant, + style: ElevatedButton.styleFrom(backgroundColor: Colors.black), + child: FittedBox( + child: Text('추방하기', style: TextStyle(color: Colors.white)), + ), + ), + ), + // (D-3) 확인 + SizedBox( + width: 90, + child: ElevatedButton( + onPressed: () => Navigator.pop(context), + style: ElevatedButton.styleFrom(backgroundColor: Colors.black), + child: FittedBox( + child: Text('확인', style: TextStyle(color: Colors.white)), + ), + ), + ), + ], + ), + ] else ...[ + ElevatedButton( + onPressed: () => Navigator.pop(context), + style: ElevatedButton.styleFrom(backgroundColor: Colors.black), + child: const Text('확인', style: TextStyle(color: Colors.white)), + ), + ], + ], + ), + ), + ); + } +} diff --git a/lib/dialogs/user_info_team_dialog.dart b/lib/dialogs/user_info_team_dialog.dart new file mode 100644 index 0000000..c65d197 --- /dev/null +++ b/lib/dialogs/user_info_team_dialog.dart @@ -0,0 +1,316 @@ +import 'package:flutter/material.dart'; +import 'response_dialog.dart'; +import '../../plugins/api.dart'; + +class UserInfoTeamDialog extends StatefulWidget { + final Map userData; + final bool isRoomMaster; // 이 모달을 연 "현재 로그인 유저"가 방장인지 + final int roomSeq; + final String roomTypeName; // "TEAM" + final List teamNameList; + + const UserInfoTeamDialog({ + Key? key, + required this.userData, + required this.isRoomMaster, + required this.roomSeq, + required this.roomTypeName, + required this.teamNameList, + }) : super(key: key); + + @override + State createState() => _UserInfoTeamDialogState(); +} + +class _UserInfoTeamDialogState extends State { + late String participantType; // 'ADMIN' / 'PLAYER' + late String teamName; // 'A'/'B'/'WAIT' + late String introduceMyself; // 유저 소개 + + @override + void initState() { + super.initState(); + final rawType = (widget.userData['participant_type'] ?? 'PLAYER').toString().toUpperCase(); + participantType = (rawType == 'ADMIN') ? 'ADMIN' : 'PLAYER'; + + final rawTeam = (widget.userData['team_name'] ?? '').toString().trim().toUpperCase(); + teamName = (rawTeam.isEmpty || rawTeam == 'WAIT') ? 'WAIT' : rawTeam; + + introduceMyself = widget.userData['introduce_myself'] ?? ''; + + // teamNameList에 WAIT 없으면 추가 (기존 코드) + final hasWait = widget.teamNameList.map((e) => e.toUpperCase()).contains('WAIT'); + if (!hasWait) { + widget.teamNameList.add('WAIT'); + } + } + + // (1) 역할/팀 수정 API (기존 코드) + Future _onUpdateUserInfo() async { + // 방장이 아닌데 수정 시도 -> 그냥 return + if (!widget.isRoomMaster) return; + + final reqBody = { + 'room_seq': '${widget.roomSeq}', + 'room_type_name': widget.roomTypeName, // "TEAM" + 'target_user_seq': '${widget.userData['user_seq']}', + 'participant_type': participantType, + 'team_name': teamName, + }; + + try { + final response = await Api.serverRequest( + uri: '/room/score/update/user/role', + body: reqBody, + ); + if (response['result'] == 'OK') { + final resp = response['response'] ?? {}; + if (resp['result'] == 'OK') { + await showResponseDialog(context, '성공', '역할/팀이 성공적으로 수정되었습니다.'); + Navigator.pop(context, 'refresh'); + } 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'); + } + } + + // (2) 새로 추가: "추방하기" API 호출 함수 + Future _onKickParticipant() async { + // 방장이 아니면 리턴 + if (!widget.isRoomMaster) return; + + final reqBody = { + "room_seq": "${widget.roomSeq}", + "target_user_seq": "${widget.userData['user_seq']}", + }; + + try { + final response = await Api.serverRequest( + uri: '/room/score/kick/participant', + body: reqBody, + ); + if (response['result'] == 'OK') { + final resp = response['response'] ?? {}; + if (resp['result'] == 'OK') { + // 성공 + await showResponseDialog(context, '성공', '해당 유저가 강퇴되었습니다.'); + Navigator.pop(context, 'refresh'); + } 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'); + } + } + + @override + Widget build(BuildContext context) { + final userName = widget.userData['nickname'] ?? '유저'; + final profileImg = widget.userData['profile_img'] ?? ''; + final department = widget.userData['department'] ?? '소속정보 없음'; + + return Dialog( + backgroundColor: Colors.white, + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)), + child: Padding( + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text('유저 정보 (팀전)', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const SizedBox(height: 12), + + // (A) 프로필 영역 + Row( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + // 프로필 이미지 + Container( + width: 80, height: 80, + decoration: BoxDecoration( + border: Border.all(color: Colors.black), + borderRadius: BorderRadius.circular(16), + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(16), + 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)), + ) + : const Center(child: Text('이미지\n없음', textAlign: TextAlign.center)), + ), + ), + const SizedBox(width: 16), + // 닉네임 + 소속 + Expanded( + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(userName, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + const SizedBox(height: 6), + Text(department, style: const TextStyle(fontSize: 14)), + ], + ), + ), + ], + ), + const SizedBox(height: 16), + + // (B) 방장만 역할/팀 수정 가능 + if (widget.isRoomMaster) ...[ + // 역할 드롭다운 + Row( + children: [ + const Text('역할: ', style: TextStyle(fontWeight: FontWeight.bold)), + DropdownButton( + value: participantType, + items: const [ + DropdownMenuItem(value: 'ADMIN', child: Text('사회자')), + DropdownMenuItem(value: 'PLAYER', child: Text('참가자')), + ], + onChanged: (val) { + if (val == null) return; + setState(() { + participantType = val; + }); + }, + ), + ], + ), + const SizedBox(height: 12), + + // 팀명 드롭다운 + Row( + children: [ + const Text('팀명: ', style: TextStyle(fontWeight: FontWeight.bold)), + DropdownButton( + value: widget.teamNameList + .map((e) => e.toUpperCase()) + .contains(teamName) ? teamName : 'WAIT', + items: widget.teamNameList.map((t) => DropdownMenuItem( + value: t.toUpperCase(), + child: Text(t), + )).toList(), + onChanged: (val) { + if (val == null) return; + setState(() { + teamName = val; + }); + }, + ), + ], + ), + const SizedBox(height: 16), + ] else ...[ + // (B') 일반유저 -> 그냥 정보만 표시 + Text('역할: $participantType', style: const TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 6), + Text('팀명: $teamName', style: const TextStyle(fontWeight: FontWeight.bold)), + const SizedBox(height: 16), + ], + + // (C) 소개 + const Align( + alignment: Alignment.centerLeft, + child: Text('소개', style: TextStyle(fontWeight: FontWeight.bold)), + ), + const SizedBox(height: 4), + Container( + width: double.infinity, + height: 100, + decoration: BoxDecoration( + border: Border.all(color: Colors.black), + borderRadius: BorderRadius.circular(8), + ), + child: SingleChildScrollView( + padding: const EdgeInsets.all(8), + child: Text( + introduceMyself.isNotEmpty ? introduceMyself : '소개글이 없습니다.', + style: const TextStyle(fontSize: 14), + softWrap: true, // 줄바꿈 허용 + maxLines: 100, // 임의의 큰 수 + overflow: TextOverflow.clip, // 필요하면 clip 처리 + ), + ), + ), + const SizedBox(height: 16), + + // (D) 하단 버튼 + if (widget.isRoomMaster) ...[ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + // 수정하기 + SizedBox( + width: 90, + child: ElevatedButton( + onPressed: _onUpdateUserInfo, + style: ElevatedButton.styleFrom(backgroundColor: Colors.black), + // (★) FittedBox 감싸기 + child: FittedBox( + child: Text( + '수정하기', + style: TextStyle(color: Colors.white), + ), + ), + ), + ), + // 추방하기 + SizedBox( + width: 90, + child: ElevatedButton( + onPressed: _onKickParticipant, + style: ElevatedButton.styleFrom(backgroundColor: Colors.black), + child: FittedBox( + child: Text( + '추방하기', + style: TextStyle(color: Colors.white), + ), + ), + ), + ), + // 확인 + SizedBox( + width: 90, + child: ElevatedButton( + onPressed: () => Navigator.pop(context), + style: ElevatedButton.styleFrom(backgroundColor: Colors.black), + child: FittedBox( + child: Text( + '확인', + style: TextStyle(color: Colors.white), + ), + ), + ), + ), + ], + ), + ] else ...[ + // 일반 유저는 "확인"만 + ElevatedButton( + onPressed: () => Navigator.pop(context), + style: ElevatedButton.styleFrom(backgroundColor: Colors.black), + child: const Text('확인', style: TextStyle(color: Colors.white)), + ), + ], + ], + ), + ), + ); + } +} diff --git a/lib/dialogs/yes_no_dialog.dart b/lib/dialogs/yes_no_dialog.dart new file mode 100644 index 0000000..5582c1b --- /dev/null +++ b/lib/dialogs/yes_no_dialog.dart @@ -0,0 +1,67 @@ +import 'package:flutter/material.dart'; + +/// 사용 예시: +/// final bool? result = await showYesNoDialog( +/// context: context, +/// title: '확인', +/// message: '정말 진행하시겠습니까?', +/// ); +/// +/// if (result == true) { +/// // YES +/// } else { +/// // NO or 닫힘 +/// } +Future showYesNoDialog({ + required BuildContext context, + required String title, + required String message, + bool yesNo = true, +}) { + return showDialog( + context: context, + barrierDismissible: false, // dialog 밖 터치로 닫기 방지 + builder: (BuildContext ctx) { + return AlertDialog( + backgroundColor: Colors.white, + title: Center( + child: Text( + title, + style: const TextStyle( + fontSize: 20, + fontWeight: FontWeight.bold, + color: Colors.black, + ), + ), + ), + content: Text( + message, + style: const TextStyle( + fontSize: 16, + color: Colors.black, + ), + ), + actions: [ + TextButton( + onPressed: () => Navigator.of(ctx).pop(false), // NO + style: TextButton.styleFrom( + backgroundColor: Colors.black12, + foregroundColor: Colors.black, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), + ), + child: const Text('아니오'), + ), + TextButton( + onPressed: () => Navigator.of(ctx).pop(true), // YES + style: TextButton.styleFrom( + backgroundColor: Colors.black, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8), + ), + child: const Text('예'), + ), + ], + ); + }, + ); +} diff --git a/lib/firebase_options.dart b/lib/firebase_options.dart new file mode 100644 index 0000000..5f25ea8 --- /dev/null +++ b/lib/firebase_options.dart @@ -0,0 +1,94 @@ +// File generated by FlutterFire CLI. +// ignore_for_file: type=lint +import 'package:firebase_core/firebase_core.dart' show FirebaseOptions; +import 'package:flutter/foundation.dart' + show defaultTargetPlatform, kIsWeb, TargetPlatform; + +/// Default [FirebaseOptions] for use with your Firebase apps. +/// +/// Example: +/// ```dart +/// import 'firebase_options.dart'; +/// // ... +/// await Firebase.initializeApp( +/// options: DefaultFirebaseOptions.currentPlatform, +/// ); +/// ``` +class DefaultFirebaseOptions { + static FirebaseOptions get currentPlatform { + if (kIsWeb) { + return web; + } + switch (defaultTargetPlatform) { + case TargetPlatform.android: + return android; + case TargetPlatform.iOS: + return ios; + case TargetPlatform.macOS: + return macos; + case TargetPlatform.windows: + return windows; + case TargetPlatform.linux: + throw UnsupportedError( + 'DefaultFirebaseOptions have not been configured for linux - ' + 'you can reconfigure this by running the FlutterFire CLI again.', + ); + default: + throw UnsupportedError( + 'DefaultFirebaseOptions are not supported for this platform.', + ); + } + } + + static const FirebaseOptions web = FirebaseOptions( + apiKey: 'AIzaSyCnZuHtj5oUe_YS9nv3nlQIKWYCCfYFysU', + appId: '1:70449524223:web:e9c27da6646d655f3e4bca', + messagingSenderId: '70449524223', + projectId: 'allscore-344c2', + authDomain: 'allscore-344c2.firebaseapp.com', + databaseURL: 'https://allscore-344c2-default-rtdb.asia-southeast1.firebasedatabase.app', + storageBucket: 'allscore-344c2.firebasestorage.app', + measurementId: 'G-50Q1W265RY', + ); + + static const FirebaseOptions android = FirebaseOptions( + apiKey: 'AIzaSyAJEItMxO-TemHGlveSKySG-eNaTD9XJI0', + appId: '1:70449524223:android:94ffb9ec98e508313e4bca', + messagingSenderId: '70449524223', + projectId: 'allscore-344c2', + databaseURL: 'https://allscore-344c2-default-rtdb.asia-southeast1.firebasedatabase.app', + storageBucket: 'allscore-344c2.firebasestorage.app', + ); + + static const FirebaseOptions ios = FirebaseOptions( + apiKey: 'AIzaSyDq2y-BRlthl6BHs4B7FByiUnpyOfPPZQk', + appId: '1:70449524223:ios:98ebdbaa616a807f3e4bca', + messagingSenderId: '70449524223', + projectId: 'allscore-344c2', + databaseURL: 'https://allscore-344c2-default-rtdb.asia-southeast1.firebasedatabase.app', + storageBucket: 'allscore-344c2.firebasestorage.app', + iosBundleId: 'com.example.allscoreApp', + ); + + static const FirebaseOptions macos = FirebaseOptions( + apiKey: 'AIzaSyDq2y-BRlthl6BHs4B7FByiUnpyOfPPZQk', + appId: '1:70449524223:ios:98ebdbaa616a807f3e4bca', + messagingSenderId: '70449524223', + projectId: 'allscore-344c2', + databaseURL: 'https://allscore-344c2-default-rtdb.asia-southeast1.firebasedatabase.app', + storageBucket: 'allscore-344c2.firebasestorage.app', + iosBundleId: 'com.example.allscoreApp', + ); + + static const FirebaseOptions windows = FirebaseOptions( + apiKey: 'AIzaSyCnZuHtj5oUe_YS9nv3nlQIKWYCCfYFysU', + appId: '1:70449524223:web:479dd789b837f54c3e4bca', + messagingSenderId: '70449524223', + projectId: 'allscore-344c2', + authDomain: 'allscore-344c2.firebaseapp.com', + databaseURL: 'https://allscore-344c2-default-rtdb.asia-southeast1.firebasedatabase.app', + storageBucket: 'allscore-344c2.firebasestorage.app', + measurementId: 'G-S9J5WDYJZM', + ); + +} \ No newline at end of file diff --git a/lib/main.dart b/lib/main.dart index aeff99d..3eda742 100644 --- a/lib/main.dart +++ b/lib/main.dart @@ -1,10 +1,24 @@ import 'package:flutter/material.dart'; -import 'login_page.dart'; -import 'id_finding_page.dart'; -import 'pw_finding_page.dart'; -import 'signup_page.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +// Firebase Core +import 'package:firebase_core/firebase_core.dart'; +// Firebase Database (필요하다면) +import 'package:firebase_database/firebase_database.dart'; + +// firebase_options.dart 파일을 import +import 'firebase_options.dart'; + +import 'views/login/login_page.dart'; +import 'views/room/main_page.dart'; + +void main() async { + WidgetsFlutterBinding.ensureInitialized(); + + // Firebase 초기화 + await Firebase.initializeApp( + options: DefaultFirebaseOptions.currentPlatform, // FirebaseOptions 사용 + ); -void main() { runApp(const MyApp()); } @@ -14,12 +28,31 @@ class MyApp extends StatelessWidget { @override Widget build(BuildContext context) { return MaterialApp( - title: 'Flutter Demo', + title: 'ALLSCORE', theme: ThemeData( - colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple), - useMaterial3: true, + primarySwatch: Colors.blue, + ), + home: FutureBuilder( + future: _checkLoginStatus(), + builder: (context, snapshot) { + if (snapshot.connectionState == ConnectionState.waiting) { + return const Scaffold( + body: Center(child: CircularProgressIndicator()), + ); + } else if (snapshot.hasData && snapshot.data == true) { + return const MainPage(); + } else { + return const LoginPage(); + } + }, ), - home: const LoginPage(), ); } + + Future _checkLoginStatus() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + bool autoLogin = prefs.getBool('auto_login') ?? false; + String authToken = prefs.getString('auth_token') ?? ''; + return autoLogin && authToken.isNotEmpty; + } } diff --git a/lib/plugins/api.dart b/lib/plugins/api.dart new file mode 100644 index 0000000..0a31c48 --- /dev/null +++ b/lib/plugins/api.dart @@ -0,0 +1,109 @@ +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; +import 'dart:convert'; +import 'package:image_picker/image_picker.dart'; + +class Api { + static const String baseUrl = 'https://eldsoft.com:8097'; + + // 사용자 정보를 업데이트하는 메서드 + static Future> serverRequest({ + required String uri, + required Map body, + }) async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + String? authToken = prefs.getString('auth_token'); + + final url = '$baseUrl$uri'; // URL 생성 + final headers = { // 헤더 설정 + 'Content-Type': 'application/json', + 'auth-token': authToken ?? '', + }; + + final response = await http.post( + Uri.parse(url), + headers: headers, + body: json.encode(body), + ); + + // 변경 가능한 변수 res 선언 + Map res; + + if (response.statusCode == 200) { + String responseBody = utf8.decode(response.bodyBytes); + final Map jsonResponse = jsonDecode(responseBody); + print('응답: $jsonResponse'); + print('응답[result]: ${jsonResponse['result']}'); + + await prefs.setString('auth_token', jsonResponse['auth']['token']); + res = { + 'result': "OK", + 'response': jsonResponse, + }; + } else { + res = { + 'result': "FAIL", + 'response': '', + }; // 요청 실패 시 응답 반환 + } + + return res; // res 반환 + } + + static Future> uploadProfileImage(XFile image, {Map? body}) async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + String? authToken = prefs.getString('auth_token'); + + final uri = Uri.parse('https://eldsoft.com:8097/user/update/profile/img'); + final headers = { // 헤더 설정 + 'auth-token': authToken ?? '', + }; + + final request = http.MultipartRequest('POST', uri) + ..headers.addAll(headers); // 헤더 추가 + + // 이미지 파일을 MultipartFile로 변환 + final file = await http.MultipartFile.fromPath('file', image.path); + request.files.add(file); + + // body가 null이 아닐 경우 추가 + if (body != null) { + request.fields['body'] = json.encode(body); // JSON 형식으로 body 추가 + } + + Map res; + + try { + // 서버에 요청 전송 + final response = await request.send(); + + if (response.statusCode == 200) { + // 응답을 바이트로 읽고 UTF-8로 디코딩 + final responseData = await response.stream.toBytes(); + final responseString = utf8.decode(responseData); // UTF-8로 디코딩 + final jsonResponse = json.decode(responseString); + print('응답: $jsonResponse'); + print('응답[result]: ${jsonResponse['result']}'); + + await prefs.setString('auth_token', jsonResponse['auth']['token']); + res = { + 'result': "OK", + 'response': jsonResponse, + }; + } else { + res = { + 'result': "FAIL", + 'response': '', + }; // 요청 실패 시 응답 반환 + } + } catch (e) { + print('업로드 중 오류 발생: $e'); + res = { + 'result': "FAIL", + 'response': '', + }; + } + + return res; // Map 반환 + } +} \ No newline at end of file diff --git a/lib/plugins/utils.dart b/lib/plugins/utils.dart new file mode 100644 index 0000000..ad4b9eb --- /dev/null +++ b/lib/plugins/utils.dart @@ -0,0 +1,11 @@ +import 'dart:convert'; +import 'package:crypto/crypto.dart'; + +class Utils { + // 비밀번호를 SHA-256으로 인코딩하는 메서드 추가 + static String hashPassword(String password) { + final bytes = utf8.encode(password); // 비밀번호를 UTF-8로 인코딩 + final digest = sha256.convert(bytes); // SHA-256 해시 생성 + return digest.toString(); // 해시 값을 문자열로 반환 + } +} diff --git a/lib/id_finding_page.dart b/lib/views/login/id_finding_page.dart similarity index 100% rename from lib/id_finding_page.dart rename to lib/views/login/id_finding_page.dart diff --git a/lib/login_page.dart b/lib/views/login/login_page.dart similarity index 70% rename from lib/login_page.dart rename to lib/views/login/login_page.dart index 199e43f..c552136 100644 --- a/lib/login_page.dart +++ b/lib/views/login/login_page.dart @@ -7,6 +7,8 @@ import 'package:shared_preferences/shared_preferences.dart'; import 'id_finding_page.dart'; import 'pw_finding_page.dart'; import 'signup_page.dart'; +import 'package:google_mobile_ads/google_mobile_ads.dart'; +import '../room/main_page.dart'; class LoginPage extends StatefulWidget { const LoginPage({Key? key}) : super(key: key); @@ -21,11 +23,44 @@ class _LoginPageState extends State { bool autoLogin = false; String loginErrorMessage = ''; + BannerAd? _bannerAd; + Widget? _adWidget; + + @override + void initState() { + super.initState(); + _bannerAd = BannerAd( + adUnitId: "ca-app-pub-3151339278746301~1689299887", + request: const AdRequest(), + size: AdSize.banner, + listener: BannerAdListener( + onAdLoaded: (ad) { + setState(() { + _adWidget = AdWidget(ad: ad as AdWithView); + }); + }, + onAdFailedToLoad: (ad, error) { + print('Ad failed to load: $error'); + ad.dispose(); + }, + ), + )..load(); + } + + @override + void dispose() { + _bannerAd?.dispose(); + super.dispose(); + } + Future _login() async { - String id = idController.text; - String password = passwordController.text; + String id = idController.text.trim(); + String password = passwordController.text.trim(); + + // autoLogin 체크여부 String autoLoginStatus = autoLogin ? 'Y' : 'N'; + // PW를 sha256으로 해시 var bytes = utf8.encode(password); var digest = sha256.convert(bytes); @@ -34,6 +69,7 @@ class _LoginPageState extends State { Uri.parse('https://eldsoft.com:8097/user/login'), headers: { 'Content-Type': 'application/json', + 'auth_token': '', }, body: jsonEncode({ 'user_id': id, @@ -41,26 +77,46 @@ class _LoginPageState extends State { }), ).timeout(const Duration(seconds: 10)); + // 응답 바디 디코딩 String responseBody = utf8.decode(response.bodyBytes); if (response.statusCode == 200) { final Map jsonResponse = jsonDecode(responseBody); - + print('jsonResponse: $jsonResponse'); + if (jsonResponse['result'] == 'OK') { - print('로그인 성공'); + // 로그인 성공 + final authData = jsonResponse['auth'] ?? {}; + final token = authData['token'] ?? ''; + final userSeq = authData['user_seq'] ?? 0; // 새로 추가 + SharedPreferences prefs = await SharedPreferences.getInstance(); - await prefs.setString('auth_token', jsonResponse['auth']['token']); + // 토큰 및 autoLogin 여부 저장 + await prefs.setString('auth_token', token); await prefs.setBool('auto_login', autoLogin); + // (New) 내 user_seq 저장 + await prefs.setInt('my_user_seq', userSeq); + + // 메인 페이지로 이동 + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (context) => const MainPage()), + ); } else if (jsonResponse['response_info']['msg_title'] == '로그인 실패') { + // 로그인 실패 메시지 setState(() { loginErrorMessage = '회원정보를 다시 확인해주세요.'; }); + } else { + // result != OK 이지만, 다른 이유 + _showDialog('로그인 실패', '서버에서 로그인에 실패했습니다.\n관리자에게 문의해주세요.'); } } else { _showDialog('오류', '로그인에 실패했습니다. 관리자에게 문의해주세요.'); } } catch (e) { - _showDialog('오류', '요청이 실패했습니다. 관리자에게 문의해주세요.'); + print('로그인 요청 중 오류: $e'); + _showDialog('오류', '로그인 요청이 실패했습니다. 관리자에게 문의해주세요.\n$e'); } } @@ -118,9 +174,9 @@ class _LoginPageState extends State { decoration: InputDecoration( labelText: 'ID', labelStyle: const TextStyle(color: Colors.black), - border: OutlineInputBorder(), - focusedBorder: OutlineInputBorder( - borderSide: const BorderSide(color: Colors.black, width: 2.0), + border: const OutlineInputBorder(), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.black, width: 2.0), ), ), ), @@ -131,9 +187,9 @@ class _LoginPageState extends State { decoration: InputDecoration( labelText: 'PW', labelStyle: const TextStyle(color: Colors.black), - border: OutlineInputBorder(), - focusedBorder: OutlineInputBorder( - borderSide: const BorderSide(color: Colors.black, width: 2.0), + border: const OutlineInputBorder(), + focusedBorder: const OutlineInputBorder( + borderSide: BorderSide(color: Colors.black, width: 2.0), ), ), ), @@ -196,9 +252,19 @@ class _LoginPageState extends State { }, child: const Text('회원가입', style: TextStyle(color: Colors.black)), ), + const SizedBox(height: 16), + // 광고 영역 + Container( + height: 50, + color: Colors.grey[300], + child: const Center(child: Text('광고 영역', style: TextStyle(color: Colors.black))), + ), ], ), ), ); } -} \ No newline at end of file +} + + + diff --git a/lib/pw_finding_page.dart b/lib/views/login/pw_finding_page.dart similarity index 100% rename from lib/pw_finding_page.dart rename to lib/views/login/pw_finding_page.dart diff --git a/lib/signup_page.dart b/lib/views/login/signup_page.dart similarity index 100% rename from lib/signup_page.dart rename to lib/views/login/signup_page.dart diff --git a/lib/views/room/create_room_page.dart b/lib/views/room/create_room_page.dart new file mode 100644 index 0000000..646ed1b --- /dev/null +++ b/lib/views/room/create_room_page.dart @@ -0,0 +1,489 @@ +import 'package:flutter/material.dart'; +import 'package:flutter/services.dart'; // 숫자 입력 제한 +import '../../plugins/api.dart'; +import '../../dialogs/response_dialog.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'waiting_room_team_page.dart'; +import 'waiting_room_private_page.dart'; + +class CreateRoomPage extends StatefulWidget { + const CreateRoomPage({Key? key}) : super(key: key); + + @override + _CreateRoomPageState createState() => _CreateRoomPageState(); +} + +class _CreateRoomPageState extends State { + final TextEditingController _roomNameController = TextEditingController(); + final TextEditingController _roomDescriptionController = TextEditingController(); + + /// 공개 여부 (open_yn: 'Y'/'N') + bool _isPrivate = false; + final TextEditingController _passwordController = TextEditingController(); + + /// 운영시간 (1~6) + int _selectedHour = 1; + + /// 게임 유형: 개인전 / 팀전 + bool _isTeamGame = false; + /// 팀 수 (팀전이면 최소 2팀부터) + int _selectedTeamCount = 2; + + /// 최대 인원 + final TextEditingController _maxParticipantsController = TextEditingController(text: '1'); + + /// 점수 공개 범위 + /// - 개인전: 'ALL' / 'PRIVATE' + /// - 팀전: 'ALL' / 'TEAM' / 'PRIVATE' + String _selectedScoreOpenRange = 'ALL'; + + @override + void dispose() { + _roomNameController.dispose(); + _roomDescriptionController.dispose(); + _passwordController.dispose(); + _maxParticipantsController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return Scaffold( + // 전체 흰 배경 + backgroundColor: Colors.white, + + appBar: AppBar( + title: const Text( + '방 만들기', + style: TextStyle(color: Colors.white), + ), + backgroundColor: Colors.black, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios, color: Colors.white), + onPressed: () => Navigator.pop(context), + ), + ), + + body: SingleChildScrollView( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + /// (A) 방 제목 + const Text('방 제목', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const SizedBox(height: 6), + _buildWhiteBorderTextField( + controller: _roomNameController, + hintText: '방 제목을 입력하세요', + ), + const SizedBox(height: 16), + + /// (B) 방 소개 + const Text('방 소개', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + const SizedBox(height: 6), + _buildMultilineBox( + controller: _roomDescriptionController, + hintText: '방 소개를 입력하세요', + ), + const SizedBox(height: 16), + + /// (C) 비밀번호 설정 (공개 / 비공개) + const Text('비밀번호 설정', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + Row( + children: [ + Checkbox( + value: !_isPrivate, + activeColor: Colors.black, + checkColor: Colors.white, + onChanged: (value) { + setState(() { + // 공개 == !비공개 + _isPrivate = !value!; + }); + }, + ), + const Text('공개'), + const SizedBox(width: 10), + Checkbox( + value: _isPrivate, + activeColor: Colors.black, + checkColor: Colors.white, + onChanged: (value) { + setState(() { + _isPrivate = value!; + }); + }, + ), + const Text('비공개'), + ], + ), + if (_isPrivate) + _buildWhiteBorderTextField( + controller: _passwordController, + hintText: '비밀번호를 입력하세요', + obscureText: true, + ), + const SizedBox(height: 16), + + /// (D) 운영시간 설정 (1~6시간) + const Text('운영시간 설정', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + Row( + children: [ + DropdownButton( + value: _selectedHour, + dropdownColor: Colors.white, + style: const TextStyle(color: Colors.black), + items: List.generate(6, (index) => index + 1) + .map((value) => DropdownMenuItem( + value: value, + child: Text(value.toString()), + )) + .toList(), + onChanged: (value) { + setState(() { + _selectedHour = value!; + }); + }, + ), + const SizedBox(width: 8), + const Text('시간'), + ], + ), + const SizedBox(height: 16), + + /// (E) 게임 유형 (개인전/팀전) + const Text('게임 유형', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + Row( + children: [ + Checkbox( + value: !_isTeamGame, + activeColor: Colors.black, + checkColor: Colors.white, + onChanged: (value) { + setState(() { + _isTeamGame = !value!; + // 점수 공개 범위 초기화 + _selectedScoreOpenRange = 'ALL'; + }); + }, + ), + const Text('개인전'), + const SizedBox(width: 10), + Checkbox( + value: _isTeamGame, + activeColor: Colors.black, + checkColor: Colors.white, + onChanged: (value) { + setState(() { + _isTeamGame = value!; + // 점수 공개 범위 초기화 + _selectedScoreOpenRange = 'ALL'; + }); + }, + ), + const Text('팀전'), + const SizedBox(width: 16), + if (_isTeamGame) ...[ + const Text('팀수: '), + const SizedBox(width: 8), + // 팀은 최소 2팀부터 + DropdownButton( + value: _selectedTeamCount, + dropdownColor: Colors.white, + style: const TextStyle(color: Colors.black), + items: List.generate(9, (index) => index + 2) // 2 ~ 10 + .map((value) => DropdownMenuItem( + value: value, + child: Text(value.toString()), + )) + .toList(), + onChanged: (value) { + setState(() { + _selectedTeamCount = value!; + }); + }, + ), + ], + ], + ), + const SizedBox(height: 16), + + /// (F) 최대 인원수 + const Text('최대 인원수', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)), + Row( + children: [ + SizedBox( + width: 80, + child: TextField( + controller: _maxParticipantsController, + keyboardType: TextInputType.number, + textAlign: TextAlign.center, + style: const TextStyle(fontSize: 16), + decoration: const InputDecoration( + border: OutlineInputBorder(), + contentPadding: EdgeInsets.symmetric(vertical: 8), + ), + inputFormatters: [ + FilteringTextInputFormatter.digitsOnly, + LengthLimitingTextInputFormatter(3), + ], + onChanged: (value) { + if (value.isNotEmpty) { + final number = int.tryParse(value); + if (number == null || number < 1 || number > 100) { + _maxParticipantsController.text = '1'; + _maxParticipantsController.selection = + TextSelection.fromPosition( + TextPosition( + offset: _maxParticipantsController.text.length, + ), + ); + } + } + }, + ), + ), + const SizedBox(width: 8), + const Text('명', style: TextStyle(fontSize: 16)), + ], + ), + const SizedBox(height: 16), + + /// (G) 점수 공개 범위 + /// 개인전: 'ALL', 'PRIVATE' + /// 팀전: 'ALL', 'TEAM', 'PRIVATE' + const Text( + '점수 공개 범위 설정', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 8), + + if (!_isTeamGame) + // 개인전 → ALL, PRIVATE + Column( + children: [ + RadioListTile( + title: const Text('전체'), + value: 'ALL', + groupValue: _selectedScoreOpenRange, + activeColor: Colors.black, + onChanged: (value) { + setState(() { + _selectedScoreOpenRange = value!; + }); + }, + ), + RadioListTile( + title: const Text('개인'), + value: 'PRIVATE', + groupValue: _selectedScoreOpenRange, + activeColor: Colors.black, + onChanged: (value) { + setState(() { + _selectedScoreOpenRange = value!; + }); + }, + ), + ], + ) + else + // 팀전 → ALL, TEAM, PRIVATE + Column( + children: [ + RadioListTile( + title: const Text('전체'), + value: 'ALL', + groupValue: _selectedScoreOpenRange, + activeColor: Colors.black, + onChanged: (value) { + setState(() { + _selectedScoreOpenRange = value!; + }); + }, + ), + RadioListTile( + title: const Text('팀'), + value: 'TEAM', + groupValue: _selectedScoreOpenRange, + activeColor: Colors.black, + onChanged: (value) { + setState(() { + _selectedScoreOpenRange = value!; + }); + }, + ), + RadioListTile( + title: const Text('개인'), + value: 'PRIVATE', + groupValue: _selectedScoreOpenRange, + activeColor: Colors.black, + onChanged: (value) { + setState(() { + _selectedScoreOpenRange = value!; + }); + }, + ), + ], + ), + const SizedBox(height: 24), + + /// (H) "방 생성하기" 버튼 + Center( + child: ElevatedButton( + onPressed: () async { + try { + final serverResponse = await createRoom(); + if (serverResponse['result'] == 'OK') { + final serverResponse1 = serverResponse['response']; + if (serverResponse1['result'] == 'OK') { + // 성공 응답시 roomSeq 저장 + final roomSeq = serverResponse1['data']['room_seq']; + + // 방 생성 성공 → 대기 방으로 이동 + if (_isTeamGame) { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (_) => WaitingRoomTeamPage( + roomSeq: roomSeq, + roomType: 'team', + ), + ), + ); + } else { + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (_) => WaitingRoomPrivatePage( + roomSeq: roomSeq, + roomType: 'private', + ), + ), + ); + } + } else { + showResponseDialog( + context, + '${serverResponse1['response_info']['msg_title']}', + '${serverResponse1['response_info']['msg_content']}', + ); + } + } else { + showResponseDialog( + context, + '방 생성 실패', + '서버에 문제가 있습니다. 관리자에게 문의해주세요.', + ); + } + } catch (e) { + showResponseDialog(context, '방 생성 실패', e.toString()); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.black, + foregroundColor: Colors.white, + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 40), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(40), + ), + ), + child: const Text('방 생성하기', style: TextStyle(fontSize: 16)), + ), + ), + const SizedBox(height: 16), + ], + ), + ), + ); + } + + /// “방 생성” 로직 + Future> createRoom() async { + // requestBody 생성 + final requestBody = { + 'room_title': _roomNameController.text, + 'room_intro': _roomDescriptionController.text, + 'open_yn': _isPrivate ? 'N' : 'Y', + 'room_pw': _isPrivate ? _passwordController.text : '', + 'running_time': _selectedHour.toString(), + 'room_type': _isTeamGame ? 'team' : 'private', + 'number_of_teams': _selectedTeamCount.toString(), + 'number_of_people': _maxParticipantsController.text, + // 점수공개범위: + // 개인전: 'ALL', 'PRIVATE' + // 팀전: 'ALL', 'TEAM', 'PRIVATE' + 'score_open_range': _selectedScoreOpenRange, + + 'room_status': 'WAIT', + }; + + try { + final serverResponse = + await Api.serverRequest(uri: '/room/score/create/room', body: requestBody); + + if (serverResponse == null) { + throw Exception('서버 응답이 null입니다.'); + } + if (serverResponse['result'] == 'OK') { + return serverResponse; + } else { + return {'result': 'FAIL'}; + } + } catch (e) { + print('serverResponse 오류: $e'); + return {'result': 'FAIL'}; + } + } + + /// 테두리 + 흰 배경 TextField + Widget _buildWhiteBorderTextField({ + required TextEditingController controller, + String hintText = '', + bool obscureText = false, + }) { + return TextField( + controller: controller, + obscureText: obscureText, + style: const TextStyle(color: Colors.black), + decoration: InputDecoration( + hintText: hintText, + hintStyle: TextStyle(color: Colors.grey.shade400), + border: const OutlineInputBorder(), + filled: true, + fillColor: Colors.white, + ), + ); + } + + /// 다중라인 텍스트박스 (방 소개 등) + Widget _buildMultilineBox({ + required TextEditingController controller, + String hintText = '', + }) { + return Container( + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.black), + borderRadius: BorderRadius.circular(8), + ), + child: TextField( + controller: controller, + maxLines: 4, + style: const TextStyle(color: Colors.black), + decoration: InputDecoration( + border: InputBorder.none, + hintText: hintText, + hintStyle: TextStyle(color: Colors.grey.shade400), + ), + ), + ); + } +} diff --git a/lib/views/room/finish_private_page.dart b/lib/views/room/finish_private_page.dart new file mode 100644 index 0000000..e3e8089 --- /dev/null +++ b/lib/views/room/finish_private_page.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'main_page.dart'; + +class FinishPrivatePage extends StatelessWidget { + final int roomSeq; + + const FinishPrivatePage({ + Key? key, + required this.roomSeq, + }) : 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, + 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)), + ), + ], + ), + ), + ); + } +} diff --git a/lib/views/room/finish_team_page.dart b/lib/views/room/finish_team_page.dart new file mode 100644 index 0000000..243b37d --- /dev/null +++ b/lib/views/room/finish_team_page.dart @@ -0,0 +1,47 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; +import 'main_page.dart'; + +class FinishTeamPage extends StatelessWidget { + final int roomSeq; + + const FinishTeamPage({ + Key? key, + required this.roomSeq, + }) : 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, + 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)), + ), + ], + ), + ), + ); + } +} diff --git a/lib/views/room/main_page.dart b/lib/views/room/main_page.dart new file mode 100644 index 0000000..d87c2bd --- /dev/null +++ b/lib/views/room/main_page.dart @@ -0,0 +1,159 @@ +import 'package:flutter/material.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import '../../dialogs/settings_dialog.dart'; +import 'create_room_page.dart'; + +// 새로 추가할 페이지들 +import 'room_search_home_page.dart'; + +class MainPage extends StatefulWidget { + const MainPage({Key? key}) : super(key: key); + + @override + _MainPageState createState() => _MainPageState(); +} + +class _MainPageState extends State { + bool _isBackButtonVisible = false; // 뒤로가기 버튼 상태 + + @override + void initState() { + super.initState(); + _isBackButtonVisible = false; + } + + @override + Widget build(BuildContext context) { + return Scaffold( + // (A) 전체 배경 흰색 → 텍스트/버튼은 블랙 위주 + backgroundColor: Colors.white, + + // (B) 상단 AppBar: 블랙 배경, 흰색 아이콘 + appBar: AppBar( + backgroundColor: Colors.black, + elevation: 0, + automaticallyImplyLeading: false, // 뒤로가기 버튼 자동생성 비활성 + title: const Text( + 'ALLSCORE', + style: TextStyle(color: Colors.white), + ), + actions: [ + IconButton( + icon: const Icon(Icons.settings, color: Colors.white), + onPressed: () { + showSettingsDialog(context); // 설정 모달 호출 + }, + ), + ], + ), + + // (C) 본문: 위아래 공간 분배 + body: Column( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + // 중간 영역(“방 만들기” / “참여하기”) + Expanded( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + // (C1) 방 만들기 버튼 + _buildBlackWhiteButton( + label: '방만들기', + onTap: () { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const CreateRoomPage()), + ); + }, + ), + const SizedBox(width: 16), + // (C2) 참여하기 버튼 => RoomSearchHomePage로 이동 + _buildBlackWhiteButton( + label: '참여하기', + onTap: () async { + Navigator.push( + context, + MaterialPageRoute(builder: (_) => const RoomSearchHomePage()), + ); + }, + ), + ], + ), + ), + ), + ), + + // (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), + ), + ), + ), + ], + ), + ), + + // (E) 임시 버튼: 방 생성 완료 이동 + Center( + child: OutlinedButton( + onPressed: () { + // 예시로 팀전 대기방(15번 방) 이동 + // 실무에서는 제외하거나 debugging용 + // (아직 남겨두고 싶다면 유지) + }, + style: OutlinedButton.styleFrom( + backgroundColor: Colors.white, + side: const BorderSide(color: Colors.black54, width: 1), + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(40), + ), + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 60), + ), + child: const Text( + '방 생성 완료 이동(임시)', + style: TextStyle(color: Colors.black, fontSize: 16), + ), + ), + ), + const SizedBox(height: 16), + ], + ), + ); + } + + /// 블랙 라인 + 흰 배경 스타일의 버튼 + Widget _buildBlackWhiteButton({ + required String label, + required VoidCallback onTap, + }) { + return ElevatedButton( + onPressed: onTap, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + 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), + ), + ), + child: Text(label, style: const TextStyle(color: Colors.black)), + ); + } +} diff --git a/lib/views/room/playing_private_page.dart b/lib/views/room/playing_private_page.dart new file mode 100644 index 0000000..7b467f8 --- /dev/null +++ b/lib/views/room/playing_private_page.dart @@ -0,0 +1,362 @@ +import 'package:flutter/material.dart'; +import 'package:firebase_database/firebase_database.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'main_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'; + +class PlayingPrivatePage extends StatefulWidget { + final int roomSeq; + final String roomTitle; + + const PlayingPrivatePage({ + Key? key, + required this.roomSeq, + required this.roomTitle, + }) : super(key: key); + + @override + State createState() => _PlayingPrivatePageState(); +} + +class _PlayingPrivatePageState extends State { + // FRD + late DatabaseReference _roomRef; + Stream? _roomStream; + + String roomMasterYn = 'N'; + String roomTitle = ''; + + int myScore = 0; + + // (ADMIN 제외) 플레이어 목록 + List> _scoreList = []; + + bool _isLoading = true; + + // 내 user_seq + String mySeq = '0'; + + // userListMap + Map _userListMap = {}; + + @override + void initState() { + super.initState(); + roomTitle = widget.roomTitle; + _initFirebase(); + } + + Future _initFirebase() 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'); + + _listenRoomData(); + } + + void _listenRoomData() { + _roomStream = _roomRef.onValue; + _roomStream?.listen((event) { + final snapshot = event.snapshot; + if (!snapshot.exists) { + setState(() { + _isLoading = false; + roomTitle = '방 정보 없음'; + _scoreList = []; + myScore = 0; + }); + return; + } + + final data = snapshot.value as Map? ?? {}; + + final roomInfoData = data['roomInfo'] as Map? ?? {}; + final userInfoData = data['userInfo'] as Map? ?? {}; + final userListData = data['userList'] as Map?; + + // 방 상태 체크 + final roomStatus = (roomInfoData['room_status'] ?? 'WAIT').toString().toUpperCase(); + + // 만약 FINISH라면 => 종료 페이지 이동 + if (roomStatus == 'FINISH') { + // 모든 유저 -> 종료 페이지 + // (중복 이동 방지) + if (mounted) { + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => FinishPrivatePage(roomSeq: widget.roomSeq)), + ); + } + return; + } + + 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) { + _userListMap[k.toString()] = (v == true); + }); + } + + // 전체 참가자 + final List> rawList = []; + userInfoData.forEach((uSeq, uData) { + rawList.add({ + 'user_seq': uSeq, + 'participant_type': (uData['participant_type'] ?? '').toString().toUpperCase(), + 'nickname': uData['nickname'] ?? '유저', + 'score': uData['score'] ?? 0, + 'profile_img': uData['profile_img'] ?? '', + 'department': uData['department'] ?? '', + 'introduce_myself': uData['introduce_myself'] ?? '', + 'is_my_score': (uSeq.toString() == mySeq) ? 'Y' : 'N', + }); + }); + + // 내 점수 + int tempMyScore = 0; + for (var u in rawList) { + if ((u['is_my_score'] ?? 'N') == 'Y') { + tempMyScore = u['score'] ?? 0; + } + } + + // ADMIN 제외 + final playerList = rawList.where((u) => u['participant_type'] != 'ADMIN').toList(); + // 점수 내림차순 + playerList.sort((a, b) { + final scoreA = a['score'] ?? 0; + final scoreB = b['score'] ?? 0; + return scoreB.compareTo(scoreA); + }); + + myScore = tempMyScore; + _scoreList = playerList; + _isLoading = false; + }); + }, onError: (err) { + setState(() { + _isLoading = false; + roomTitle = '오류 발생'; + }); + }); + } + + /// (A) WillPopScope + AppBar leading + Future _onBackPressed() async { + // 방장? => 게임 종료 API + if (roomMasterYn == 'Y') { + await _requestFinish(); + } + + // userList => false + final userRef = _roomRef.child('userList').child(mySeq); + await userRef.set(false); + + if (!mounted) return false; + Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage())); + return false; + } + + /// (B) 서버에 "게임 종료" 요청 + Future _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 user) { + final userSeq = user['user_seq'].toString(); + final score = user['score'] ?? 0; + final nickname = user['nickname'] ?? '유저'; + + final bool isActive = _userListMap[userSeq] ?? true; + final hasExited = !isActive; + + return GestureDetector( + onTap: () => _onUserTapped(user), + child: Container( + width: 60, + margin: const EdgeInsets.all(4), + child: Column( + 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)), + const SizedBox(height: 2), + Container( + width: 30, + height: 30, + decoration: BoxDecoration( + shape: BoxShape.circle, + 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)), + ) + : 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)), + ), + ), + ), + ), + const SizedBox(height: 2), + Text( + nickname, + style: TextStyle(fontSize: 11, color: hasExited ? Colors.redAccent : Colors.black), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ); + } + + Future _onUserTapped(Map 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', + userData: userData, + ), + ); + } else if (roomMasterYn == 'Y') { + // 방장(PLAYER)도 점수 수정 + await showDialog( + context: context, + barrierDismissible: false, + builder: (_) => ScoreEditDialog( + roomSeq: widget.roomSeq, + roomType: 'PRIVATE', + userData: userData, + ), + ); + } else { + // 일반 모달 + await showDialog( + context: context, + barrierDismissible: false, + builder: (_) => UserInfoBasicDialog(userData: userData), + ); + } + } + + @override + Widget build(BuildContext context) { + return WillPopScope( + onWillPop: _onBackPressed, + child: Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: Colors.black, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios, color: Colors.white), + onPressed: _onBackPressed, + ), + title: Text( + roomTitle.isNotEmpty ? roomTitle : '진행중 (개인전)', + style: const TextStyle(color: Colors.white), + ), + actions: [ + if (roomMasterYn == 'Y') + TextButton( + onPressed: () async { + // 방장 수동 종료버튼 + await _requestFinish(); + }, + child: const Text('게임종료', style: TextStyle(color: Colors.white)), + ), + ], + ), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : Column( + children: [ + // 내 점수 + 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 SizedBox(height: 4), + Text('$myScore', style: const TextStyle(fontSize: 36, fontWeight: FontWeight.bold, color: Colors.black)), + ], + ), + ), + const Divider(height: 1, color: Colors.black), + + Expanded( + child: Container( + padding: const EdgeInsets.all(8), + child: SingleChildScrollView( + child: Wrap( + spacing: 8, + runSpacing: 8, + children: _scoreList.map(_buildScoreItem).toList(), + ), + ), + ), + ), + + 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)), + ), + ), + ], + ), + ), + ); + } +} diff --git a/lib/views/room/playing_team_page.dart b/lib/views/room/playing_team_page.dart new file mode 100644 index 0000000..99ddce5 --- /dev/null +++ b/lib/views/room/playing_team_page.dart @@ -0,0 +1,414 @@ +import 'package:flutter/material.dart'; +import 'package:firebase_database/firebase_database.dart'; +import 'package:shared_preferences/shared_preferences.dart'; + +import 'main_page.dart'; +import 'finish_team_page.dart'; // (★) 팀전 종료화면 +import '../../plugins/api.dart'; +import '../../dialogs/response_dialog.dart'; +import '../../dialogs/score_edit_dialog.dart'; +import '../../dialogs/user_info_basic_dialog.dart'; + +class PlayingTeamPage extends StatefulWidget { + final int roomSeq; + final String roomTitle; + + const PlayingTeamPage({ + Key? key, + required this.roomSeq, + required this.roomTitle, + }) : super(key: key); + + @override + State createState() => _PlayingTeamPageState(); +} + +class _PlayingTeamPageState extends State { + late DatabaseReference _roomRef; + Stream? _roomStream; + + String roomMasterYn = 'N'; + String roomTitle = ''; + + int myScore = 0; + int myTeamScore = 0; + + Map _teamScoreMap = {}; + Map>> _teamMap = {}; + + bool _isLoading = true; + + String mySeq = '0'; + + // userListMap + Map _userListMap = {}; + + @override + void initState() { + super.initState(); + roomTitle = widget.roomTitle; + _initFirebase(); + } + + Future _initFirebase() 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'); + + _listenRoomData(); + } + + void _listenRoomData() { + _roomStream = _roomRef.onValue; + _roomStream?.listen((event) { + final snapshot = event.snapshot; + if (!snapshot.exists) { + setState(() { + _isLoading = false; + roomTitle = '방 정보 없음'; + myScore = 0; + myTeamScore = 0; + _teamScoreMap = {}; + _teamMap = {}; + }); + return; + } + + final data = snapshot.value as Map? ?? {}; + + final roomInfoData = data['roomInfo'] as Map? ?? {}; + final userInfoData = data['userInfo'] as Map? ?? {}; + final userListData = data['userList'] as Map?; + + // room_status + final roomStatus = (roomInfoData['room_status'] ?? 'WAIT').toString().toUpperCase(); + + // FINISH -> 종료화면 + if (roomStatus == 'FINISH') { + if (mounted) { + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => FinishTeamPage(roomSeq: widget.roomSeq)), + ); + } + return; + } + + 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) { + _userListMap[k.toString()] = (v == true); + }); + } + + // 전체 유저 + final List> rawList = []; + userInfoData.forEach((uSeq, uData) { + rawList.add({ + 'user_seq': uSeq, + 'participant_type': (uData['participant_type'] ?? '').toString().toUpperCase(), + 'nickname': uData['nickname'] ?? '유저', + 'team_name': (uData['team_name'] ?? '').toString().toUpperCase(), + 'score': uData['score'] ?? 0, + }); + }); + + // 내 점수/팀점수 + 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'; + 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') { + tmpMyTeamScore += sc; + } + } + + // 팀별 분류 (ADMIN/WAIT 제외) + final Map>> tMap = {}; + final Map tScoreMap = {}; + + for (var user in rawList) { + final pType = user['participant_type']; + final tName = user['team_name'] ?? 'WAIT'; + if (pType == 'ADMIN') continue; + if (tName == 'WAIT') continue; + + tMap.putIfAbsent(tName, () => []); + tMap[tName]!.add(user); + } + + // 팀 점수 합 + tMap.forEach((k, members) { + int sumScore = 0; + for (var m in members) { + sumScore += (m['score'] ?? 0) as int; + } + tScoreMap[k] = sumScore; + }); + + myScore = tmpMyScore; + myTeamScore = tmpMyTeamScore; + _teamMap = tMap; + _teamScoreMap = tScoreMap; + _isLoading = false; + }); + }, onError: (err) { + setState(() { + _isLoading = false; + roomTitle = '오류 발생'; + }); + }); + } + + /// (A) 뒤로가기 -> 방장? => Finish API + Future _onBackPressed() async { + if (roomMasterYn == 'Y') { + await _requestFinish(); + } + // userList => false + final userRef = _roomRef.child('userList').child(mySeq); + await userRef.set(false); + + if (!mounted) return false; + Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage())); + return false; + } + + Future _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, + child: Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + title: Text( + roomTitle.isNotEmpty ? roomTitle : '진행중 (팀전)', + style: const TextStyle(color: Colors.white), + ), + backgroundColor: Colors.black, + elevation: 0, + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios, color: Colors.white), + onPressed: _onBackPressed, + ), + actions: [ + if (roomMasterYn == 'Y') + TextButton( + onPressed: _requestFinish, + child: const Text('게임종료', style: TextStyle(color: Colors.white)), + ), + ], + ), + body: _isLoading + ? const Center(child: CircularProgressIndicator()) + : Column( + children: [ + // 내 점수 / 팀 점수 + Container( + color: Colors.white, + padding: const EdgeInsets.only(top: 16, bottom: 16), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Column( + children: [ + const Text('내 점수', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black)), + const SizedBox(height: 4), + Text('$myScore', style: const TextStyle(fontSize: 36, fontWeight: FontWeight.bold, color: Colors.black)), + ], + ), + Container(width: 1, height: 60, color: Colors.black), + Column( + children: [ + const Text('우리 팀 점수', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black)), + const SizedBox(height: 4), + Text('$myTeamScore', style: const TextStyle(fontSize: 36, fontWeight: FontWeight.bold, color: Colors.black)), + ], + ), + ], + ), + ), + const Divider(height: 1, color: Colors.black), + + Expanded( + child: SingleChildScrollView( + padding: const EdgeInsets.all(8), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: _teamMap.keys.map(_buildTeamSection).toList(), + ), + ), + ), + 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)), + ), + ), + ], + ), + ), + ); + } + + Widget _buildTeamSection(String teamName) { + final upperName = teamName.toUpperCase(); + final members = _teamMap[upperName] ?? []; + final teamScore = _teamScoreMap[upperName] ?? 0; + + return Container( + width: double.infinity, + margin: const EdgeInsets.only(bottom: 8), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.black), + borderRadius: BorderRadius.circular(8), + ), + child: Column( + children: [ + Container( + color: Colors.black, + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 8), + child: Center( + child: Text('$teamName (팀점수 $teamScore)', 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(_buildTeamMemberItem).toList()), + ), + ), + ], + ), + ); + } + + Widget _buildTeamMemberItem(Map userData) { + final userSeq = userData['user_seq'].toString(); + final score = userData['score'] ?? 0; + final nickname = userData['nickname'] ?? '유저'; + + final bool isActive = _userListMap[userSeq] ?? true; + final hasExited = !isActive; + + return GestureDetector( + onTap: () => _onUserTapped(userData), + child: Container( + width: 60, + margin: const EdgeInsets.symmetric(horizontal: 6, vertical: 4), + child: Column( + 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)), + const SizedBox(height: 2), + Container( + width: 30, + height: 30, + decoration: BoxDecoration( + shape: BoxShape.circle, + 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))) + : 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))), + ), + ), + ), + const SizedBox(height: 2), + Text( + nickname, + style: TextStyle(fontSize: 11, color: hasExited ? Colors.redAccent : Colors.black), + overflow: TextOverflow.ellipsis, + ), + ], + ), + ), + ); + } + + Future _onUserTapped(Map 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', + userData: userData, + ), + ); + } else if (roomMasterYn == 'Y') { + await showDialog( + context: context, + barrierDismissible: false, + builder: (_) => ScoreEditDialog( + roomSeq: widget.roomSeq, + roomType: 'TEAM', + userData: userData, + ), + ); + } else { + await showDialog( + context: context, + barrierDismissible: false, + builder: (_) => UserInfoBasicDialog(userData: userData), + ); + } + } +} diff --git a/lib/views/room/room_search_home_page.dart b/lib/views/room/room_search_home_page.dart new file mode 100644 index 0000000..a73fdc1 --- /dev/null +++ b/lib/views/room/room_search_home_page.dart @@ -0,0 +1,80 @@ +import 'package:flutter/material.dart'; +import 'room_search_list_page.dart'; + +/// 방 검색 홈 화면 +/// - "대기중/진행중/종료" 버튼 3개로 구분 +/// - 버튼 누르면 RoomSearchListPage로 이동 + 상태값 전달 +class RoomSearchHomePage extends StatelessWidget { + const RoomSearchHomePage({Key? key}) : super(key: key); + + @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: () => Navigator.pop(context), + ), + ), + + // 화면 하단 광고 영역 + bottomNavigationBar: Container( + height: 50, + color: Colors.grey.shade400, + child: const Center( + child: Text('구글 광고', style: TextStyle(color: Colors.black)), + ), + ), + + // 본문: 중앙에 3개 버튼 (대기중 / 진행중 / 종료) + body: Center( + child: Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + _buildSearchStatusButton(context, label: '대기중', status: 'WAIT'), + const SizedBox(width: 16), + _buildSearchStatusButton(context, label: '진행중', status: 'RUNNING'), + const SizedBox(width: 16), + _buildSearchStatusButton(context, label: '종료', status: 'FINISH'), + ], + ), + ), + ); + } + + /// 동일한 크기로 버튼 + Widget _buildSearchStatusButton( + BuildContext context, { + required String label, + required String status, + }) { + return SizedBox( + width: 100, + height: 100, + child: ElevatedButton( + onPressed: () { + // RoomSearchListPage로 이동하며, roomStatus 전달 + Navigator.push( + context, + MaterialPageRoute( + builder: (_) => RoomSearchListPage(roomStatus: status), + ), + ); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.black, + side: const BorderSide(color: Colors.black, width: 1), + shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)), + padding: EdgeInsets.zero, + ), + child: Text(label, style: const TextStyle(color: Colors.black)), + ), + ); + } +} diff --git a/lib/views/room/room_search_list_page.dart b/lib/views/room/room_search_list_page.dart new file mode 100644 index 0000000..cf7c7bb --- /dev/null +++ b/lib/views/room/room_search_list_page.dart @@ -0,0 +1,289 @@ +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 + +/// 서버로부터 방 리스트를 검색/조회하는 페이지 +/// - roomStatus: "WAIT"/"RUNNING"/"FINISH" +/// - 1페이지당 10개씩 로드, 스크롤 최하단 도달 시 다음 페이지 자동 로드 +/// - 검색창을 통해 room_title 필터링 +class RoomSearchListPage extends StatefulWidget { + final String roomStatus; // WAIT / RUNNING / FINISH + + const RoomSearchListPage({Key? key, required this.roomStatus}) : super(key: key); + + @override + State createState() => _RoomSearchListPageState(); +} + +class _RoomSearchListPageState extends State { + final TextEditingController _searchController = TextEditingController(); + + // 방 목록 + List> _roomList = []; + + bool _isLoading = false; + bool _hasMore = true; + int _currentPage = 1; + final int _pageSize = 10; + + late ScrollController _scrollController; + + @override + void initState() { + super.initState(); + + _scrollController = ScrollController()..addListener(_onScroll); + _fetchRoomList(isRefresh: true); + } + + @override + void dispose() { + _scrollController.removeListener(_onScroll); + _scrollController.dispose(); + _searchController.dispose(); + super.dispose(); + } + + /// 스크롤이 최하단 근처 도달 시 다음 페이지 로드 + void _onScroll() { + if (!_scrollController.hasClients) return; + final thresholdPixels = 200; + final maxScroll = _scrollController.position.maxScrollExtent; + final currentScroll = _scrollController.position.pixels; + + if (maxScroll - currentScroll <= thresholdPixels) { + _fetchRoomList(isRefresh: false); + } + } + + /// (1) 서버에서 방 리스트 가져오기 + Future _fetchRoomList({required bool isRefresh}) async { + if (_isLoading) return; + if (!isRefresh && !_hasMore) return; + + setState(() => _isLoading = true); + + if (isRefresh) { + _currentPage = 1; + _hasMore = true; + _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_value": searchValue, + "search_page": searchPage, + }; + + try { + 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 { + final innerResp = response['response']; + if (innerResp == null || innerResp['result'] != 'OK') { + showResponseDialog(context, '오류', '내부 응답이 잘못되었습니다.'); + } else { + final respData = innerResp['data']; + if (respData is List) { + if (respData.isEmpty) { + _hasMore = false; + } else { + for (var item in respData) { + print('🔵 item: $item'); + final parsedItem = { + 'room_seq': item['room_seq'] ?? 0, + 'nickname': item['nickname'] ?? '사용자', + 'room_status': _statusToKr(item['room_status'] ?? ''), + 'open_yn': (item['open_yn'] == 'Y') ? '공개' : '비공개', + 'room_type': item['room_type_name'] ?? 'private', + 'room_title': item['room_title'] ?? '(방제목 없음)', + 'room_intro': item['room_intro'] ?? '', + 'now_people': item['now_number_of_people']?.toString() ?? '0', + 'max_people': item['number_of_people']?.toString() ?? '0', + 'start_dt': item['start_dt'], + 'end_dt': item['end_dt'], + }; + _roomList.add(parsedItem); + } + + if (respData.length < _pageSize) { + _hasMore = false; + } + _currentPage++; + } + } + } + } + } catch (e) { + showResponseDialog(context, '오류', '서버 요청 중 예외 발생: $e'); + } finally { + setState(() => _isLoading = false); + } + } + + /// WAIT->'대기중', RUNNING->'진행중', FINISH->'종료' + String _statusToKr(String status) { + switch (status.toUpperCase()) { + case 'WAIT': + return '대기중'; + case 'RUNNING': + return '진행중'; + case 'FINISH': + return '종료'; + default: + return status; + } + } + + void _onSearch() { + _fetchRoomList(isRefresh: true); + } + + void _onRoomItemTap(Map item) { + // 여기서 분리된 모달 호출 + showDialog( + context: context, + barrierDismissible: false, + builder: (_) => RoomDetailDialog(roomData: item), + ); + } + + @override + Widget build(BuildContext context) { + final statusKr = _statusToKr(widget.roomStatus); + + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + backgroundColor: Colors.black, + title: Text('$statusKr 방 검색', style: const TextStyle(color: Colors.white)), + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios, color: Colors.white), + onPressed: () => Navigator.pop(context), + ), + ), + body: Column( + children: [ + // (A) 검색창 + Padding( + padding: const EdgeInsets.all(8.0), + child: TextField( + controller: _searchController, + onSubmitted: (_) => _onSearch(), + style: const TextStyle(color: Colors.black), + decoration: InputDecoration( + hintText: '방 제목 입력', + hintStyle: TextStyle(color: Colors.grey.shade400), + prefixIcon: const Icon(Icons.search, color: Colors.black54), + suffixIcon: IconButton( + icon: const Icon(Icons.close, color: Colors.black54), + onPressed: () { + _searchController.clear(); + }, + ), + border: OutlineInputBorder( + borderSide: const BorderSide(color: Colors.black), + borderRadius: BorderRadius.circular(20), + ), + contentPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12), + ), + ), + ), + + // (B) 로딩 표시 or 리스트 + Expanded( + child: _isLoading && _roomList.isEmpty + ? const Center(child: CircularProgressIndicator()) + : _buildRoomListView(), + ), + + // (C) 하단 광고 + Container( + height: 60, + color: Colors.white, + child: Center( + child: Container( + height: 50, + width: 300, + color: Colors.grey.shade400, + child: const Center( + child: Text('구글 광고', style: TextStyle(color: Colors.black)), + ), + ), + ), + ), + ], + ), + ); + } + + Widget _buildRoomListView() { + print('🔵 _roomList: $_roomList'); + if (_roomList.isEmpty) { + return const Center( + child: Text( + '검색 결과가 없습니다.', + style: TextStyle(color: Colors.black), + ), + ); + } + + return ListView.builder( + controller: _scrollController, + padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16), + itemCount: _roomList.length, + itemBuilder: (context, index) { + final item = _roomList[index]; + return _buildRoomItem(item); + }, + ); + } + + Widget _buildRoomItem(Map item) { + final roomTitle = item['room_title'] ?? '(방제목 없음)'; + final nickname = item['nickname'] ?? '유저'; + final roomStatus = item['room_status'] ?? '대기중'; + final openYn = item['open_yn'] ?? '공개'; + final nowPeople = item['now_people'] ?? '0'; + final maxPeople = item['max_people'] ?? '0'; + final roomIntro = item['room_intro'] ?? ''; + + return GestureDetector( + onTap: () => _onRoomItemTap(item), + child: Container( + margin: const EdgeInsets.only(bottom: 12), + padding: const EdgeInsets.all(10), + decoration: BoxDecoration( + border: Border.all(color: Colors.black54), + borderRadius: BorderRadius.circular(8), + ), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Text(roomTitle, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)), + const SizedBox(height: 4), + Text('$nickname / $roomStatus / $openYn / $nowPeople/$maxPeople명'), + const SizedBox(height: 4), + Text(roomIntro, style: const TextStyle(fontSize: 12)), + ], + ), + ), + ); + } +} diff --git a/lib/views/room/waiting_room_private_page.dart b/lib/views/room/waiting_room_private_page.dart new file mode 100644 index 0000000..82acf6e --- /dev/null +++ b/lib/views/room/waiting_room_private_page.dart @@ -0,0 +1,666 @@ +import 'package:flutter/material.dart'; +import 'dart:async'; +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 '../../dialogs/room_setting_dialog.dart'; +import '../../dialogs/user_info_private_dialog.dart'; +import 'playing_private_page.dart'; + +class WaitingRoomPrivatePage extends StatefulWidget { + final int roomSeq; + final String roomType; // "private" + + const WaitingRoomPrivatePage({ + Key? key, + required this.roomSeq, + required this.roomType, + }) : super(key: key); + + @override + State createState() => _WaitingRoomPrivatePageState(); +} + +class _WaitingRoomPrivatePageState extends State { + // ───────────────────────────────────────── + // 방 설정 + // ───────────────────────────────────────── + String roomMasterYn = 'N'; + String roomTitle = ''; + String roomIntro = ''; + String openYn = 'Y'; + String roomPw = ''; + int runningTime = 1; + int numberOfPeople = 10; + String scoreOpenRange = 'PRIVATE'; + + // 유저 목록 + List> _userList = []; + + bool _isLoading = true; + + // FRD + late DatabaseReference _roomRef; + Stream? _roomStream; + + // 진행중 화면 이동 중복 방지 + bool _movedToRunningPage = false; + + // 강퇴 안내 중복 방지 + bool _kickedOut = false; + + // FRD 구독 해제 + StreamSubscription? _roomStreamSubscription; + + // (예) 내 user_seq + String mySeq = '0'; // 원래 '6' 고정이었던 부분 제거 + + @override + void initState() { + super.initState(); + _loadMySeq(); + } + + /// (A) my_user_seq 로드 -> 리스너 + Future _loadMySeq() 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'); + _listenRoomData(); + } + + void _listenRoomData() { + _roomStream = _roomRef.onValue; + _roomStream?.listen((event) { + final snapshot = event.snapshot; + if (!snapshot.exists) { + setState(() { + _isLoading = false; + roomTitle = '방 정보 없음'; + _userList = []; + }); + return; + } + + final data = snapshot.value as Map? ?? {}; + final roomInfoData = data['roomInfo'] as Map? ?? {}; + final userInfoData = data['userInfo'] as Map? ?? {}; + + 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); + numberOfPeople = _toInt(roomInfoData['number_of_people'], 10); + scoreOpenRange = (roomInfoData['score_open_range'] ?? 'PRIVATE') as String; + + // 방장 여부 + roomMasterYn = 'N'; + final masterSeq = roomInfoData['master_user_seq']; + if (masterSeq != null && masterSeq.toString() == mySeq) { + roomMasterYn = 'Y'; + } + + // 유저 목록 + 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'] ?? '', + 'department': userMap['department'] ?? '', + 'introduce_myself': userMap['introduce_myself'] ?? '', + 'ready_yn': (userMap['ready_yn'] ?? 'N').toString().toUpperCase(), + }); + }); + _userList = tempList; + _isLoading = false; + }); + + // 진행중 -> 화면 이동 + if (roomStatus == 'RUNNING' && !_movedToRunningPage) { + _movedToRunningPage = true; + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (_) => PlayingPrivatePage( + roomSeq: widget.roomSeq, + roomTitle: roomTitle, + ), + ), + ); + return; + } + + // (2) 여기서 "내 user_seq가 목록에 있는지" 검사 + final amIStillHere = _userList.any((u) => u['user_seq'].toString() == mySeq); + + // (3) 만약 내가 목록에서 사라졌고, + // 아직 안내하지 않았으며(_kickedOut == false), + // 내가 방장도 아니고(roomMasterYn != 'Y'), + // 방이 진행중/종료가 아닌 상태면 => "강퇴"로 간주 + if (!amIStillHere && !_kickedOut && roomMasterYn != 'Y') { + // ※ 방이 이미 RUNNING이면 그냥 게임중에 퇴장인지, + // 방이 DELETE 상태인지 등 필요 시 조건 보강 + + _kickedOut = true; // 중복 안내 막기 + + // (★) 강퇴 안내 + 메인으로 이동 + Future.delayed(Duration.zero, () async { + await showResponseDialog(context, '안내', '강퇴되었습니다.'); + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => const MainPage()), + ); + }); + } + }, onError: (error) { + print('FRD onError: $error'); + setState(() { + _isLoading = false; + roomTitle = '오류 발생'; + }); + }); + } + + @override + void dispose() { + _roomStreamSubscription?.cancel(); // ← 구독 해제 + super.dispose(); + } + + /// (B) 뒤로가기 -> 방 나가기 + Future _onLeaveRoom() async { + if (roomMasterYn == 'Y') { + // 방장 + final confirm = await showDialog( + context: context, + barrierDismissible: false, + builder: (_) { + return AlertDialog( + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + 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), + ), + actionsAlignment: MainAxisAlignment.spaceEvenly, + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + 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), + 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('확인'), + ), + ], + ); + }, + ); + 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())); + } + } + } 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())); + } + } + } + } + + int _toInt(dynamic val, int defaultVal) { + if (val == null) return defaultVal; + if (val is int) return val; + if (val is String) { + return int.tryParse(val) ?? defaultVal; + } + return defaultVal; + } + + /// 상단 버튼 (방장=3개, 일반=2개) + Widget _buildTopButtons() { + if (_isLoading) return const SizedBox(); + + 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 btnStyle = ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.black, + side: const BorderSide(color: Colors.black, width: 1), + ); + + if (roomMasterYn == 'Y') { + // 방장 => 3개 + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 4), + child: ElevatedButton( + style: btnStyle, + onPressed: _onOpenRoomSetting, + child: const Text('방 설정'), + ), + ), + ), + Expanded( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 4), + child: ElevatedButton( + style: btnStyle, + onPressed: _onToggleReady, + child: Text(readyLabel), + ), + ), + ), + Expanded( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 4), + child: ElevatedButton( + style: btnStyle, + onPressed: _onGameStart, + child: const Text('게임 시작'), + ), + ), + ), + ], + ); + } else { + // 일반 => 2개 + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 8), + child: ElevatedButton( + style: btnStyle, + onPressed: _onOpenRoomSetting, + child: const Text('방 설정'), + ), + ), + ), + Expanded( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 8), + child: ElevatedButton( + style: btnStyle, + onPressed: _onToggleReady, + child: Text(readyLabel), + ), + ), + ), + ], + ); + } + } + + /// READY 토글 + Future _onToggleReady() async { + try { + 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 newYn = isReady ? 'N' : 'Y'; + + final userRef = _roomRef.child('userInfo').child(mySeq); + await userRef.update({"ready_yn": newYn}); + } catch (e) { + print('READY 설정 실패: $e'); + } + } + + /// 방 설정 + Future _onOpenRoomSetting() async { + final roomInfo = { + "room_seq": "${widget.roomSeq}", + "room_master_yn": roomMasterYn, + "room_title": roomTitle, + "room_intro": roomIntro, + "open_yn": openYn, + "room_pw": roomPw, + "running_time": runningTime, + "room_type": widget.roomType, + "number_of_people": numberOfPeople, + "score_open_range": scoreOpenRange, + }; + + final result = await showDialog( + context: context, + barrierDismissible: false, + builder: (_) => RoomSettingModal(roomInfo: roomInfo), + ); + if (result == 'refresh') { + // ... + } + } + + /// 게임 시작 + Future _onGameStart() async { + final notReady = _userList.any((u) { + final ry = (u['ready_yn'] ?? 'N').toString().toUpperCase(); + return (ry != 'Y'); + }); + if (notReady) { + showResponseDialog(context, '안내', 'READY되지 않은 참가자가 있습니다(방장 포함).'); + return; + } + + final requestBody = { + "room_seq": "${widget.roomSeq}", + "room_type": "PRIVATE", + }; + + try { + 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'); + } + } + + @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: 20, 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), + _buildPlayerSection(), + ], + ), + ), + ); + } + + // 사회자 + Widget _buildAdminSection() { + final adminList = _userList.where((u) { + final t = (u['participant_type'] ?? '').toString().toUpperCase(); + return t == 'ADMIN'; + }).toList(); + + return Container( + width: double.infinity, + 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('사회자가 없습니다.', style: TextStyle(color: Colors.black)) + : 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'; + }).toList(); + + return Container( + width: double.infinity, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.black), + borderRadius: BorderRadius.circular(8), + ), + child: playerList.isEmpty + ? const Text('참가자가 없습니다.', style: TextStyle(color: Colors.black)) + : SingleChildScrollView( + child: Wrap( + spacing: 16, + runSpacing: 16, + alignment: WrapAlignment.center, + children: playerList.map(_buildSeat).toList(), + ), + ), + ); + } + + // Seat + Widget _buildSeat(Map userData) { + final userName = userData['nickname'] ?? '유저'; + final profileImg = userData['profile_img'] ?? ''; + final readyYn = userData['ready_yn'] ?? 'N'; + final isReady = (readyYn == 'Y'); + final isMaster = (roomMasterYn == 'Y'); + + return GestureDetector( + onTap: () async { + final result = await showDialog( + context: context, + barrierDismissible: false, + builder: (_) => UserInfoPrivateDialog( + userData: userData, + isRoomMaster: isMaster, + roomSeq: widget.roomSeq, + roomTypeName: 'PRIVATE', + ), + ); + if (result == 'refresh') { + // ... + } + }, + child: Container( + margin: const EdgeInsets.only(right: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all( + color: isReady ? Colors.red : Colors.black, + width: 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), + ) + ] + : [], + ), + 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), + ), + ); + }, + ), + ), + ), + const SizedBox(height: 4), + Text(userName, 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 new file mode 100644 index 0000000..8a500a7 --- /dev/null +++ b/lib/views/room/waiting_room_team_page.dart @@ -0,0 +1,795 @@ +import 'package:flutter/material.dart'; +import 'dart:async'; +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 '../../dialogs/room_setting_dialog.dart'; +import '../../dialogs/user_info_team_dialog.dart'; +import '../../dialogs/team_name_edit_dialog.dart'; +import 'playing_team_page.dart'; + +class WaitingRoomTeamPage extends StatefulWidget { + final int roomSeq; + final String roomType; // "team" + + const WaitingRoomTeamPage({ + Key? key, + required this.roomSeq, + required this.roomType, + }) : super(key: key); + + @override + State createState() => _WaitingRoomTeamPageState(); +} + +class _WaitingRoomTeamPageState extends State { + // ───────────────────────────────────────── + // 방 설정 + // ───────────────────────────────────────── + String roomMasterYn = 'N'; + String roomTitle = ''; + String roomIntro = ''; + String openYn = 'Y'; + String roomPw = ''; + int runningTime = 1; + int numberOfPeople = 10; + String scoreOpenRange = 'PRIVATE'; + int numberOfTeams = 1; + + // 팀명 리스트 + List _teamNameList = []; + + // 유저 목록 + List> _userList = []; + + bool _isLoading = true; + + // FRD + late DatabaseReference _roomRef; + Stream? _roomStream; + + // 진행중 화면 중복 이동 방지 + bool _movedToRunningPage = false; + + // 강퇴 안내 중복 방지 + bool _kickedOut = false; + + // FRD 구독 해제 + StreamSubscription? _roomStreamSubscription; + + // 로컬스토리지에서 가져올 user_seq + String mySeq = '0'; // 원래 '6'을 하드코딩 했던 부분을 제거 + + @override + void initState() { + super.initState(); + _loadMySeq(); + } + + /// (A) 내 user_seq를 로드하고 나서 방 레퍼런스 설정 + 리스너 등록 + Future _loadMySeq() 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'); + // 리스너 시작 + _listenRoomData(); + } + + @override + void dispose() { + _roomStreamSubscription?.cancel(); // ← 구독 해제 + super.dispose(); + } + + void _listenRoomData() { + _roomStream = _roomRef.onValue; + _roomStream?.listen((event) async { + final snapshot = event.snapshot; + if (!snapshot.exists) { + setState(() { + _isLoading = false; + roomTitle = '방 정보 없음'; + _userList = []; + }); + return; + } + + final data = snapshot.value as Map? ?? {}; + final roomInfoData = data['roomInfo'] as Map? ?? {}; + final userInfoData = data['userInfo'] as Map? ?? {}; + + // 현재 방 상태 + 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); + numberOfPeople = _toInt(roomInfoData['number_of_people'], 10); + 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(); + } else { + _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'; + } + + // 유저 목록 + final tempList = >[]; + userInfoData.forEach((userSeq, userMap) { + tempList.add({ + 'user_seq': userSeq, + 'participant_type': userMap['participant_type'] ?? '', + 'nickname': userMap['nickname'] ?? '유저', + '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(), + }); + }); + _userList = tempList; + _isLoading = false; + }); + + // 상태가 RUNNING이면 진행중 화면으로 + if (roomStatus == 'RUNNING' && !_movedToRunningPage) { + _movedToRunningPage = true; + Navigator.pushReplacement( + context, + MaterialPageRoute( + builder: (_) => PlayingTeamPage( + roomSeq: widget.roomSeq, + roomTitle: roomTitle, + ), + ), + ); + return; + } + + // (2) 여기서 "내 user_seq가 목록에 있는지" 검사 + final amIStillHere = _userList.any((u) => u['user_seq'].toString() == mySeq); + + // (3) 만약 내가 목록에서 사라졌고, + // 아직 안내하지 않았으며(_kickedOut == false), + // 내가 방장도 아니고(roomMasterYn != 'Y'), + // 방이 진행중/종료가 아닌 상태면 => "강퇴"로 간주 + if (!amIStillHere && !_kickedOut && roomMasterYn != 'Y') { + // ※ 방이 이미 RUNNING이면 그냥 게임중에 퇴장인지, + // 방이 DELETE 상태인지 등 필요 시 조건 보강 + + _kickedOut = true; // 중복 안내 막기 + + // (★) 강퇴 안내 + 메인으로 이동 + Future.delayed(Duration.zero, () async { + await showResponseDialog(context, '안내', '강퇴되었습니다.'); + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (_) => const MainPage()), + ); + }); + } + }, onError: (error) { + print('FRD onError: $error'); + setState(() { + _isLoading = false; + roomTitle = '오류 발생'; + }); + }); + } + + // ───────────────────────────────────────── + // [추가] 뒤로가기 -> 방 나가기 + // ───────────────────────────────────────── + Future _onLeaveRoom() async { + if (roomMasterYn == 'Y') { + // 방장 -> 경고 모달 + final confirm = await showDialog( + context: context, + barrierDismissible: false, + builder: (_) { + return AlertDialog( + backgroundColor: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(16), + 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), + ), + actionsAlignment: MainAxisAlignment.spaceEvenly, + actions: [ + TextButton( + onPressed: () => Navigator.pop(context, false), + 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), + 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('확인'), + ), + ], + ); + }, + ); + 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())); + } + } + } 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())); + } + } + } + } + + int _toInt(dynamic val, int defaultVal) { + if (val == null) return defaultVal; + if (val is int) return val; + if (val is String) { + return int.tryParse(val) ?? defaultVal; + } + 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 myReadyYn = (me['ready_yn'] ?? 'N').toString().toUpperCase(); + final bool isReady = (myReadyYn == 'Y'); + final String readyLabel = isReady ? '준비완료' : '준비'; + + final btnStyle = ElevatedButton.styleFrom( + backgroundColor: Colors.white, + foregroundColor: Colors.black, + side: const BorderSide(color: Colors.black, width: 1), + ); + + if (roomMasterYn == 'Y') { + // 방장 -> [방 설정], [준비/준비완료], [게임 시작] + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 4), + child: ElevatedButton( + style: btnStyle, + onPressed: _onOpenRoomSetting, + child: const Text('방 설정'), + ), + ), + ), + Expanded( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 4), + child: ElevatedButton( + style: btnStyle, + onPressed: _onToggleReady, + child: Text(readyLabel), + ), + ), + ), + Expanded( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 4), + child: ElevatedButton( + style: btnStyle, + onPressed: _onGameStart, + child: const Text('게임 시작'), + ), + ), + ), + ], + ); + } else { + // 일반 -> [방 설정], [준비/준비완료] + return Row( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + Expanded( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 8), + child: ElevatedButton( + style: btnStyle, + onPressed: _onOpenRoomSetting, + child: const Text('방 설정'), + ), + ), + ), + Expanded( + child: Container( + margin: const EdgeInsets.symmetric(horizontal: 8), + child: ElevatedButton( + style: btnStyle, + onPressed: _onToggleReady, + child: Text(readyLabel), + ), + ), + ), + ], + ); + } + } + + /// READY 토글 + Future _onToggleReady() async { + try { + // 내 데이터 + final me = _userList.firstWhere( + (u) => (u['user_seq'] ?? '') == mySeq, + orElse: () => {}, + ); + final myReadyYn = (me['ready_yn'] ?? 'N').toString().toUpperCase(); + final bool isReady = (myReadyYn == 'Y'); + final newYn = isReady ? 'N' : 'Y'; + + final userRef = _roomRef.child('userInfo').child(mySeq); + await userRef.update({"ready_yn": newYn}); + } catch (e) { + print('READY 설정 실패: $e'); + } + } + + /// 방 설정 열기 + Future _onOpenRoomSetting() async { + final roomInfo = { + "room_seq": "${widget.roomSeq}", + "room_master_yn": roomMasterYn, + "room_title": roomTitle, + "room_intro": roomIntro, + "open_yn": openYn, + "room_pw": roomPw, + "running_time": runningTime, + "room_type": widget.roomType, + "number_of_people": numberOfPeople, + "score_open_range": scoreOpenRange, + "number_of_teams": numberOfTeams, + "team_name_list": _teamNameList.join(','), + }; + + final result = await showDialog( + context: context, + barrierDismissible: false, + builder: (_) => RoomSettingModal(roomInfo: roomInfo), + ); + if (result == 'refresh') { + // do something + } + } + + /// 게임 시작 (전체 READY=Y 필요) + Future _onGameStart() async { + final notReady = _userList.any((u) { + final ry = (u['ready_yn'] ?? 'N').toString().toUpperCase(); + return (ry != 'Y'); + }); + if (notReady) { + showResponseDialog(context, '안내', 'READY되지 않은 참가자가 있습니다(방장 포함).'); + return; + } + + final requestBody = { + "room_seq": "${widget.roomSeq}", + "room_type": "TEAM", + }; + try { + 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 + // ───────────────────────────────────────── + Widget _buildAdminSection() { + final adminList = _userList.where((u) { + final pType = (u['participant_type'] ?? '').toString().toUpperCase(); + return pType == 'ADMIN'; + }).toList(); + + return Container( + width: double.infinity, + 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('사회자가 없습니다.', style: TextStyle(color: Colors.black)) + : Wrap( + spacing: 16, + runSpacing: 8, + children: adminList.map(_buildSeat).toList(), + ), + ); + } + + Widget _buildTeamSection() { + final players = _userList.where((u) { + final pType = (u['participant_type'] ?? '').toString().toUpperCase(); + return (pType != 'ADMIN'); + }).toList(); + + final Map>> teamMap = {}; + for (final tName in _teamNameList) { + teamMap[tName] = []; + } + + for (var user in players) { + final tName = (user['team_name'] ?? '').toString().trim().toUpperCase(); + if (tName.isNotEmpty && tName != 'WAIT' && teamMap.containsKey(tName)) { + teamMap[tName]!.add(user); + } + } + + if (teamMap.isEmpty) { + return Container( + width: double.infinity, + padding: const EdgeInsets.all(8), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.black), + borderRadius: BorderRadius.circular(8), + ), + child: const Text('팀에 배정된 참가자가 없습니다.', style: TextStyle(color: Colors.black)), + ); + } + + return Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: _teamNameList.map((teamName) { + final members = teamMap[teamName]!; + + return Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.black), + borderRadius: BorderRadius.circular(8), + ), + child: Column( + children: [ + GestureDetector( + onTap: () async { + if (roomMasterYn == 'Y') { + final result = await showDialog( + context: context, + barrierDismissible: false, + builder: (_) => TeamNameEditModal( + roomSeq: widget.roomSeq, + roomTypeName: 'TEAM', + beforeTeamName: teamName, + existingTeamNames: _teamNameList, + ), + ); + if (result == 'refresh') { + // ... + } + } + }, + child: Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 8), + color: Colors.black, + child: Center( + child: Text( + '팀 $teamName', + style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold), + ), + ), + ), + ), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.all(8), + child: Row(children: members.map(_buildSeat).toList()), + ), + ], + ), + ); + }).toList(), + ); + } + + 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'); + }).toList(); + + if (waitList.isEmpty) return const SizedBox(); + + return Container( + margin: const EdgeInsets.only(bottom: 16), + decoration: BoxDecoration( + color: Colors.white, + border: Border.all(color: Colors.black), + borderRadius: BorderRadius.circular(8), + ), + child: Column( + children: [ + Container( + width: double.infinity, + padding: const EdgeInsets.symmetric(vertical: 8), + color: Colors.black, + child: const Center( + child: Text('대기중', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)), + ), + ), + SingleChildScrollView( + scrollDirection: Axis.horizontal, + padding: const EdgeInsets.all(8), + child: Row(children: waitList.map(_buildSeat).toList()), + ), + ], + ), + ); + } + + Widget _buildSeat(Map user) { + final userName = user['nickname'] ?? '유저'; + final profileImg = user['profile_img'] ?? ''; + final readyYn = user['ready_yn'] ?? 'N'; + final isReady = (readyYn == 'Y'); + final isMaster = (roomMasterYn == 'Y'); + + return GestureDetector( + onTap: () async { + // 유저 정보 모달 + final result = await showDialog( + context: context, + barrierDismissible: false, + builder: (_) => UserInfoTeamDialog( + userData: user, + isRoomMaster: isMaster, + roomSeq: widget.roomSeq, + roomTypeName: widget.roomType.toUpperCase(), // "TEAM" + teamNameList: _teamNameList, + ), + ); + if (result == 'refresh') { + // ... + } + }, + child: Container( + width: 60, + margin: const EdgeInsets.only(right: 8), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + Container( + width: 50, + height: 50, + decoration: BoxDecoration( + color: Colors.white, + border: Border.all( + color: isReady ? Colors.red : Colors.black, + width: 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), + ) + ] + : [], + ), + 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), + ), + ); + }, + ), + ), + ), + const SizedBox(height: 2), + Text(userName, style: const TextStyle(fontSize: 12, color: Colors.black)), + ], + ), + ), + ); + } + + @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(), + ], + ), + ), + ); + } +} diff --git a/lib/views/user/my_page.dart b/lib/views/user/my_page.dart new file mode 100644 index 0000000..c72203d --- /dev/null +++ b/lib/views/user/my_page.dart @@ -0,0 +1,927 @@ +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; +import 'dart:convert'; +import '../../dialogs/response_dialog.dart'; +import 'package:crypto/crypto.dart'; +import '../../plugins/api.dart'; +import '../../plugins/utils.dart'; +import 'withdrawal_page.dart'; +import 'package:image_picker/image_picker.dart'; +import 'dart:io'; +// import 'package:path/path.dart'; + +class MyPage extends StatefulWidget { + const MyPage({Key? key}) : super(key: key); + + @override + _MyPageState createState() => _MyPageState(); +} + +class _MyPageState extends State { + String user_nickname = ''; + String user_pw = ''; + String new_user_pw = '**********'; + String user_email = ''; + String user_department = ''; + String user_introduce_myself = ''; + String user_profile_image = ''; + bool isEditingNickname = false; + bool isEditingPassword = false; + bool isConfirmingPassword = false; + String confirmPassword = ''; + final TextEditingController _nicknameController = TextEditingController(); + final TextEditingController _passwordController = TextEditingController(); + final TextEditingController _confirmPasswordController = TextEditingController(); + final FocusNode _nicknameFocusNode = FocusNode(); + final FocusNode _passwordFocusNode = FocusNode(); + final FocusNode _confirmPasswordFocusNode = FocusNode(); + String? _passwordError; + String? _emailError; + final TextEditingController _emailController = TextEditingController(); + final FocusNode _emailFocusNode = FocusNode(); + bool isEditingEmail = false; + final TextEditingController _departmentController = TextEditingController(); + final FocusNode _departmentFocusNode = FocusNode(); + bool isEditingDepartment = false; + final TextEditingController _introduceController = TextEditingController(); + String? _nicknameError; + XFile? _image; // 선택된 이미지 파일 + + @override + void initState() { + super.initState(); + _fetchUserInfo(); + } + + @override + void dispose() { + _nicknameController.dispose(); + _passwordController.dispose(); + _confirmPasswordController.dispose(); + _nicknameFocusNode.dispose(); + _passwordFocusNode.dispose(); + _confirmPasswordFocusNode.dispose(); + _emailController.dispose(); + _emailFocusNode.dispose(); + _departmentController.dispose(); + _departmentFocusNode.dispose(); + _introduceController.dispose(); + super.dispose(); + } + + @override + Widget build(BuildContext context) { + return GestureDetector( + onTap: () { + if (isEditingNickname) { + setState(() { + user_nickname = _nicknameController.text; + isEditingNickname = false; + _nicknameFocusNode.unfocus(); + }); + } + if (isEditingPassword) { + setState(() { + new_user_pw = _passwordController.text; + isEditingPassword = false; + _passwordFocusNode.unfocus(); + }); + } + if (isConfirmingPassword) { + setState(() { + isConfirmingPassword = false; + _confirmPasswordFocusNode.unfocus(); + }); + } + if (isEditingEmail) { + setState(() { + user_email = _emailController.text; + isEditingEmail = false; + _emailFocusNode.unfocus(); + }); + } + if (isEditingDepartment) { + setState(() { + user_department = _departmentController.text; + isEditingDepartment = false; + _departmentFocusNode.unfocus(); + }); + } + }, + child: Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + title: const Text('MY PAGE', style: TextStyle(color: Colors.black)), + backgroundColor: Colors.transparent, + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios, color: Colors.black), + onPressed: () { + Navigator.pop(context); + Navigator.pop(context); + }, + ), + ), + body: SingleChildScrollView( + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + crossAxisAlignment: CrossAxisAlignment.start, + children: [ + Card( + elevation: 4, + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + '닉네임:', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + Expanded( + child: GestureDetector( + onTap: () { + setState(() { + isEditingNickname = true; + _nicknameController.text = user_nickname; + _nicknameFocusNode.requestFocus(); + }); + }, + child: isEditingNickname + ? TextField( + controller: _nicknameController, + focusNode: _nicknameFocusNode, + onChanged: (newNickname) { + setState(() { + user_nickname = newNickname; + // 닉네임 패턴 검증 + if (!_isNicknameValidPattern(user_nickname)) { + // 패턴이 일치하지 않을 경우 안내 문구 표시 + _nicknameError = '닉네임은 2~20자 영문, 한글, 숫자만 사용할 수 있습니다.'; + } else { + _nicknameError = null; // 패턴이 일치하면 오류 메시지 초기화 + } + }); + }, + onSubmitted: (newNickname) { + setState(() { + user_nickname = newNickname; + isEditingNickname = false; + _nicknameFocusNode.unfocus(); + }); + }, + decoration: InputDecoration( + hintText: '닉네임을 입력하세요', + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 8.0), + ), + ) + : Container( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + user_nickname, + style: const TextStyle(fontSize: 18, color: Colors.black54), + ), + ), + ), + ), + ], + ), + ), + ), + // 닉네임 오류 메시지 추가 + if (_nicknameError != null) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + _nicknameError!, + style: const TextStyle(color: Colors.red, fontSize: 12), + ), + ), + const SizedBox(height: 16), + Card( + elevation: 4, + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + '비밀번호:', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + Expanded( + child: GestureDetector( + onTap: () { + setState(() { + isEditingPassword = true; + _passwordController.text = new_user_pw.replaceAll(RegExp(r'.'), '*'); + _passwordFocusNode.requestFocus(); + }); + }, + child: isEditingPassword + ? TextField( + controller: _passwordController, + focusNode: _passwordFocusNode, + obscureText: true, + onChanged: (newPassword) { + setState(() { + new_user_pw = newPassword; + _passwordError = _isPasswordValidPattern(new_user_pw) ? null : '비밀번호는 8~20자 영문과 숫자가 반드시 포함되어야 합니다.'; + }); + }, + onSubmitted: (newPassword) { + setState(() { + isEditingPassword = false; + _passwordFocusNode.unfocus(); + }); + }, + decoration: InputDecoration( + hintText: '비밀번호를 입력하세요', + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 8.0), + ), + ) + : Container( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + '**********', + style: const TextStyle(fontSize: 18, color: Colors.black54), + ), + ), + ), + ), + ], + ), + ), + ), + // 비밀번호 오류 메시지 추가 + if (_passwordError != null) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + _passwordError!, + style: const TextStyle(color: Colors.red, fontSize: 12), + ), + ), + const SizedBox(height: 16), + Card( + elevation: 4, + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + '비밀번호 확인:', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + Expanded( + child: GestureDetector( + onTap: () { + setState(() { + isConfirmingPassword = true; + _confirmPasswordController.text = ''; + _confirmPasswordFocusNode.requestFocus(); + }); + }, + child: isConfirmingPassword + ? TextField( + controller: _confirmPasswordController, + focusNode: _confirmPasswordFocusNode, + obscureText: true, + onChanged: (value) { + setState(() { + confirmPassword = value; + }); + }, + onSubmitted: (newPassword) { + setState(() { + isConfirmingPassword = false; + _confirmPasswordFocusNode.unfocus(); + }); + }, + decoration: InputDecoration( + hintText: '비밀번호를 다시 입력해주세요', + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 8.0), + ), + ) + : Container( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + '*' * confirmPassword.length, + style: const TextStyle(fontSize: 18, color: Colors.black54), + ), + ), + ), + ), + ], + ), + ), + ), + // 비밀번호 일치 여부 안내 텍스트 추가 + const SizedBox(height: 8), + Text( + (new_user_pw == confirmPassword) + ? '비밀번호가 일치합니다.' + : (confirmPassword.isNotEmpty ? '비밀번호가 일치하지 않습니다.' : ''), + style: TextStyle( + fontSize: 16, + color: (new_user_pw == confirmPassword) ? Colors.green : Colors.red, + ), + ), + Card( + elevation: 4, + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + '이메일:', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + Expanded( + child: GestureDetector( + onTap: () { + setState(() { + isEditingEmail = true; + _emailController.text = user_email; + _emailFocusNode.requestFocus(); + }); + }, + child: isEditingEmail + ? TextField( + controller: _emailController, + focusNode: _emailFocusNode, + onChanged: (value) { + setState(() { + user_email = value; + _emailError = _isEmailValid(user_email) ? null : '올바른 이메일 형식을 입력해주세요.'; + }); + }, + onSubmitted: (newEmail) { + setState(() { + user_email = newEmail; + isEditingEmail = false; + _emailFocusNode.unfocus(); + }); + }, + decoration: InputDecoration( + hintText: '이메일을 입력하세요', + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 8.0), + ), + ) + : Container( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + user_email, + style: const TextStyle(fontSize: 18, color: Colors.black54), + ), + ), + ), + ), + ], + ), + ), + ), + // 이메일 오류 메시지 추가 + if (_emailError != null) + Padding( + padding: const EdgeInsets.only(top: 8.0), + child: Text( + _emailError!, + style: const TextStyle(color: Colors.red, fontSize: 12), + ), + ), + const SizedBox(height: 8), + Card( + elevation: 4, + color: Colors.white, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), + ), + child: Padding( + padding: const EdgeInsets.all(16.0), + child: Row( + mainAxisAlignment: MainAxisAlignment.spaceBetween, + children: [ + const Text( + '소속:', + style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold), + ), + Expanded( + child: GestureDetector( + onTap: () { + setState(() { + isEditingDepartment = true; + _departmentController.text = user_department; + _departmentFocusNode.requestFocus(); + }); + }, + child: isEditingDepartment + ? TextField( + controller: _departmentController, + focusNode: _departmentFocusNode, + onChanged: (value) { + setState(() { + user_department = value; + }); + }, + onSubmitted: (newDepartment) { + setState(() { + user_department = newDepartment; + isEditingDepartment = false; + _departmentFocusNode.unfocus(); + }); + }, + decoration: InputDecoration( + hintText: '소속을 입력하세요', + border: InputBorder.none, + contentPadding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 8.0), + ), + ) + : Container( + padding: const EdgeInsets.symmetric(horizontal: 8.0), + child: Text( + user_department, + style: const TextStyle(fontSize: 18, color: Colors.black54), + ), + ), + ), + ), + ], + ), + ), + ), + const SizedBox(height: 8), + // 소속 박스 아래에 텍스트 추가 + const SizedBox(height: 8), + const Text( + '프로필 이미지를 설정해주세요.', + style: TextStyle(fontSize: 16, color: Colors.black), + ), + const SizedBox(height: 16), // 여백 추가 + Center( + child: Column( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + GestureDetector( + onTap: () { + _selectImage(); + }, + child: Container( + width: 100, // 정사각형 가로 길이 + height: 100, // 정사각형 세로 길이 + decoration: BoxDecoration( + color: Colors.white, // 내부 색상 + border: Border.all(color: Colors.black), // 테두리 색상 + borderRadius: BorderRadius.circular(20), // 모서리 둥글게 + ), + child: ClipRRect( + borderRadius: BorderRadius.circular(20), // 모서리 둥글게 + child: Image.network( + 'https://eldsoft.com:8097/images${user_profile_image}', // 프로필 이미지 URL + fit: BoxFit.cover, // 이미지 크기 조정 + errorBuilder: (context, error, stackTrace) { + return const Center(child: Text('이미지를 불러올 수 없습니다.')); // 오류 처리 + }, + ), + ), + ), + ), + const SizedBox(height: 8), + const Text( + '이미지를 클릭하시면 새로 등록할 수 있습니다.', + textAlign: TextAlign.center, // 가운데 정렬 + style: TextStyle(fontSize: 12, color: Colors.blueGrey), // 색상 변경 + ), + ], + ), + ), + const SizedBox(height: 8), + const Text( + '다른 유저에게 자신을 소개해주세요.', + style: TextStyle(fontSize: 16, color: Colors.black), + ), + const SizedBox(height: 8), // 여백 추가 + TextField( + controller: _introduceController..text = user_introduce_myself, // 기본값 설정 + maxLines: 5, // 여러 줄 입력 가능 + decoration: InputDecoration( + hintText: '자신을 소개하는 내용을 입력하세요...', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), // 모서리 둥글게 + borderSide: const BorderSide(color: Colors.black), // 테두리 색상 + ), + contentPadding: const EdgeInsets.all(10), // 패딩 추가 + ), + onChanged: (value) { + setState(() { + user_introduce_myself = value; // 자기소개 내용 업데이트 + }); + }, + ), + const SizedBox(height: 30), // 여백 추가 + Center( + child: OutlinedButton( + onPressed: () { + // 수정하기 버튼 클릭 시 모달창 열기 + _showEditDialog(); // 모달창을 여는 메서드 호출 + }, + style: OutlinedButton.styleFrom( + backgroundColor: Colors.white, // 버튼 배경색 + side: const BorderSide(color: Colors.black54, width: 1), // 테두리 색상 및 두께 + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(40), // 모서리 둥글게 조정 + ), + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 60), // 버튼 패딩 + ), + child: const Text( + '수정하기', + style: TextStyle(color: Colors.black, fontSize: 16), // 텍스트 색상 및 크기 + ), + ), + ), + const SizedBox(height: 10), // 여백 추가 + Center( + child: OutlinedButton( + onPressed: () { + // 회원탈퇴 버튼 클릭 시 WithdrawalPage로 이동 + Navigator.push( + context, + MaterialPageRoute(builder: (context) => const WithdrawalPage()), + ); + }, + style: OutlinedButton.styleFrom( + backgroundColor: Colors.white, // 버튼 배경색 + side: const BorderSide(color: Colors.black54, width: 1), // 테두리 색상 및 두께 + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(40), // 모서리 둥글게 조정 + ), + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 60), // 버튼 패딩 + ), + child: const Text( + '회원탈퇴', + style: TextStyle(color: Colors.black, fontSize: 16), // 텍스트 색상 및 크기 + ), + ), + ), + const SizedBox(height: 30), // 여백 추가 + ], + ), + ), + ), + ), + ); + } + + Future _fetchUserInfo() async { + SharedPreferences prefs = await SharedPreferences.getInstance(); + String? authToken = prefs.getString('auth_token'); + + final response = await Api.serverRequest( + uri: '/user/myinfo', + body: {}, + ); + + if (response['result'] == 'OK') { + final jsonResponse = response['response']; + print('응답: $jsonResponse'); + if (jsonResponse['result'] == 'OK') { + SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.setString('auth_token', jsonResponse['auth']['token']); + + setState(() { + user_nickname = jsonResponse['data']['nickname']; + user_email = jsonResponse['data']['user_email']; + user_department = jsonResponse['data']['department']; + user_introduce_myself = jsonResponse['data']['introduce_myself']; + user_profile_image = jsonResponse['data']['profile_img']; + }); + } else { + showResponseDialog(context, '${jsonResponse['response_info']['msg_title']}', '${jsonResponse['response_info']['msg_content']}'); + } + } else { + showResponseDialog(context, '요청 실패', '서버에 문제가 있습니다. 관리자에게 문의해주세요.'); + } + } + + bool _isPasswordValidPattern(String password) { + // 비밀번호는 최소 8자 이상, 최대 20자 이하의 영문과 숫자가 반드시 포함되어야 하며, 다른 문자도 허용 + return password.length >= 8 && password.length <= 20 && + RegExp(r'^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d\S]{8,20}$').hasMatch(password); + } + + bool _isEmailValid(String email) { + return RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(email); // 이메일 패턴 검증 + } + + Future _selectImage() async { + // 이미지 선택 다이얼로그를 표시하는 로직을 추가합니다. + showDialog( + context: context, + builder: (BuildContext context) { + return Dialog( + backgroundColor: Colors.transparent, // 배경을 투명하게 설정 + child: Container( + decoration: BoxDecoration( + color: Colors.white, // 배경색을 흰색으로 설정 + borderRadius: BorderRadius.circular(16), // 모서리 둥글게 조정 + ), + padding: const EdgeInsets.all(16), + child: Column( + mainAxisSize: MainAxisSize.min, + children: [ + const Text( + '프로필 이미지 선택', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), + ), + const SizedBox(height: 16), + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, + children: [ + Expanded( + child: ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); // 다이얼로그 닫기 + _pickImage(); // 갤러리에서 이미지 선택 + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, // 버튼 배경색 + side: const BorderSide(color: Colors.black), // 테두리 색상 + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), // 모서리 둥글게 + ), + ), + child: const Text( + '갤러리', + style: TextStyle(color: Colors.black), // 텍스트 색상 + ), + ), + ), + const SizedBox(width: 8), // 버튼 간격 + Expanded( + child: ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); // 취소 + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, // 버튼 배경색 + side: const BorderSide(color: Colors.black), // 테두리 색상 + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(10), // 모서리 둥글게 + ), + ), + child: const Text( + '취소', + style: TextStyle(color: Colors.black), // 텍스트 색상 + ), + ), + ), + ], + ), + ], + ), + ), + ); + }, + ).then((pickedFile) { + if (pickedFile != null) { + // 선택된 이미지에 따라 처리하는 로직을 추가합니다. + if (pickedFile == 'gallery') { + _pickImage(); // 갤러리에서 이미지 선택 + } + } + }); + } + + // 모달창을 여는 메서드 추가 + void _showEditDialog() { + // 현재 비밀번호와 confirmPassword 비교 + if (new_user_pw != '**********') { + if (!_isPasswordValidPattern(new_user_pw)) { + showResponseDialog(context, '수정하기 실패', '비밀번호 패턴을 확인해주세요.'); + return; + } + if (new_user_pw != confirmPassword) { + showResponseDialog(context, '수정하기 실패', '비밀번호가 일치하지 않습니다.'); + return; // 비밀번호가 일치하지 않으면 모달창을 열지 않음 + } + } + if (!_isEmailValid(user_email)) { + showResponseDialog(context, '수정하기 실패', '이메일 형식을 확인해주세요.'); + return; + } + if (!_isNicknameValidPattern(user_nickname)) { + showResponseDialog(context, '수정하기 실패', '닉네임 패턴을 확인해주세요.'); + return; + } + + showDialog( + context: context, + builder: (BuildContext context) { + return AlertDialog( + backgroundColor: Colors.white, // 모달창 배경색을 흰색으로 설정 + title: const Center( // 제목을 가운데 정렬 + child: Text( + '회원정보 수정', + style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), // 제목 스타일 + ), + ), + content: Column( + mainAxisSize: MainAxisSize.min, // 내용의 크기를 최소화 + children: [ + const Center( // 문구를 가운데 정렬 + child: Text( + '회원정보를 수정합니다.', + style: TextStyle(fontSize: 16), // 문구 스타일 + ), + ), + const SizedBox(height: 8), // 여백 추가 + const Center( // 문구를 가운데 정렬 + child: Text( + '현재 비밀번호를 입력해주세요.', + style: TextStyle(fontSize: 16), // 문구 스타일 + ), + ), + const SizedBox(height: 8), // 여백 추가 + TextField( + obscureText: true, // 비밀번호 입력 시 텍스트 숨기기 + decoration: InputDecoration( + hintText: '현재 비밀번호', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), // 모서리 둥글게 + borderSide: const BorderSide(color: Colors.black), // 테두리 색상 + ), + contentPadding: const EdgeInsets.all(10), // 패딩 추가 + ), + onChanged: (value) { + user_pw = value; // 입력한 비밀번호를 user_pw에 저장 + }, + ), + ], + ), + actions: [ + Row( + mainAxisAlignment: MainAxisAlignment.spaceEvenly, // 버튼을 양쪽 끝으로 배치 + children: [ + SizedBox( + width: 100, // 버튼의 너비를 고정 + child: ElevatedButton( + onPressed: () async { + // 수정하기 버튼 클릭 시 서버에 요청 + try { + final serverResponse = await _updateUserInfo(); // 현재 비밀번호를 전달 + + if (serverResponse['result'] == 'OK') { + final serverResponse1 = serverResponse['response']; + if (serverResponse1['result'] == 'OK') { + showResponseDialog(context, '수정하기 성공', '회원정보가 성공적으로 수정되었습니다.'); + Navigator.of(context).pop(); // 모달창 닫기 + Navigator.of(context).pop(); // 모달창 닫기 + } else { + showResponseDialog(context, '${serverResponse1['response_info']['msg_title']}', '${serverResponse1['response_info']['msg_content']}'); + } + } else { + showResponseDialog(context, '수정하기 실패', '서버에 문제가 있습니다. 관리자에게 문의해주세요.'); + } + } catch (e) { + showResponseDialog(context, '수정하기 실패', e.toString()); + } + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, // 버튼 배경색 + side: const BorderSide(color: Colors.black54, width: 1), // 테두리 색상 및 두께 + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(40), // 모서리 둥글게 조정 + ), + ), + child: const Text( + '수정하기', + style: TextStyle(color: Colors.black, fontSize: 14), // 텍스트 색상 및 크기 + ), + ), + ), + SizedBox( + width: 100, // 버튼의 너비를 고정 + child: ElevatedButton( + onPressed: () { + Navigator.of(context).pop(); // 모달창 닫기 + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.white, // 버튼 배경색 + side: const BorderSide(color: Colors.black54, width: 1), // 테두리 색상 및 두께 + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(40), // 모서리 둥글게 조정 + ), + ), + child: const Text( + '취소', + style: TextStyle(color: Colors.black, fontSize: 14), // 텍스트 색상 및 크기 + ), + ), + ), + ], + ), + ], + ); + }, + ); + } + + // 사용자 정보를 업데이트하는 메서드 추가 + Future> _updateUserInfo() async { + try { + final serverResponse = await Api.serverRequest( + uri: '/user/update/user/info', + body: { + "user_pw": Utils.hashPassword(user_pw), // 서버에서 받아온 비밀번호 + "new_user_pw": Utils.hashPassword(new_user_pw), // 사용자가 입력한 새로운 비밀번호 + "user_pw_change_yn": new_user_pw != '**********' ? 'Y' : 'N', // 비밀번호 변경 여부 + "nickname": user_nickname, // 닉네임 + "user_email": user_email, // 이메일 + "department": user_department, // 소속 + "profile_img": user_profile_image, // 프로필 이미지 + "introduce_myself": user_introduce_myself, // 자기소개 + }, + ); + + print('serverResponse 응답: $serverResponse'); + + // 응답이 null인지 확인 + if (serverResponse == null) { + throw Exception('서버 응답이 null입니다.'); + } + + // 응답 구조 확인 + if (serverResponse['result'] == 'OK') { + return serverResponse; // 성공 시 응답 반환 + } else { + return { + 'result': 'FAIL', + }; + } + } catch (e) { + print('serverResponse 오류: $e'); + return { + 'result': 'FAIL', + }; + } + } + + // 닉네임 패턴 검증 메서드 추가 + bool _isNicknameValidPattern(String nickname) { + // 닉네임은 3~15자 사이의 영문자와 숫자만 포함 + return RegExp(r'^[A-Za-z가-힣0-9]{2,20}$').hasMatch(nickname); + } + + Future _pickImage() async { + final ImagePicker picker = ImagePicker(); + // 갤러리에서 이미지 선택 + final XFile? selectedImage = await picker.pickImage(source: ImageSource.gallery); + + if (selectedImage != null) { + // 선택된 이미지가 있을 경우 서버에 업로드 + final serverResponse = await Api.uploadProfileImage(selectedImage, body: { + 'test': 'test', // 필요한 데이터 추가 + }); + + if (serverResponse['result'] == 'OK') { + final serverResponse1 = serverResponse['response']; + print('응답: $serverResponse1'); + if (serverResponse1['result'] == 'OK') { + showResponseDialog(context, '업로드 성공', '프로필 이미지가 성공적으로 업로드되었습니다.'); + + // user_profile_image 값을 업데이트 (앞의 '/images' 제거) + setState(() { + user_profile_image = serverResponse1['data']['img_src'].replaceFirst('/images', ''); // 앞의 '/images' 제거 + }); + } else { + showResponseDialog(context, '${serverResponse1['response_info']['msg_title']}', '${serverResponse1['response_info']['msg_content']}'); + } + } else { + showResponseDialog(context, '업로드 실패', '서버에 문제가 있습니다. 관리자에게 문의해주세요.'); + } + } + } +} \ No newline at end of file diff --git a/lib/views/user/withdrawal_page.dart b/lib/views/user/withdrawal_page.dart new file mode 100644 index 0000000..270a8dd --- /dev/null +++ b/lib/views/user/withdrawal_page.dart @@ -0,0 +1,155 @@ +import 'package:flutter/material.dart'; +import 'package:http/http.dart' as http; +import 'package:shared_preferences/shared_preferences.dart'; +import 'dart:convert'; +import '../../plugins/utils.dart'; +import '../../plugins/api.dart'; +import '../../dialogs/response_dialog.dart'; +import '../login/login_page.dart'; // 로그인 페이지 임포트 추가 + +class WithdrawalPage extends StatefulWidget { + const WithdrawalPage({Key? key}) : super(key: key); + + @override + _WithdrawalPageState createState() => _WithdrawalPageState(); +} + +class _WithdrawalPageState extends State { + bool _isAgreed = false; // 체크박스 상태를 관리하는 변수 + final TextEditingController _passwordController = TextEditingController(); // 비밀번호 입력 컨트롤러 + + @override + Widget build(BuildContext context) { + return Scaffold( + backgroundColor: Colors.white, + appBar: AppBar( + title: const Text('회원탈퇴', style: TextStyle(color: Colors.black)), + backgroundColor: Colors.transparent, + leading: IconButton( + icon: const Icon(Icons.arrow_back_ios, color: Colors.black), + onPressed: () => Navigator.pop(context), + ), + ), + body: Padding( + padding: const EdgeInsets.all(16.0), + child: Column( + mainAxisAlignment: MainAxisAlignment.center, + children: [ + const Text( + '회원탈퇴를 진행합니다.\n현재 비밀번호를 입력해주세요.', + textAlign: TextAlign.center, + style: TextStyle(fontSize: 18), + ), + const SizedBox(height: 20), + TextField( + controller: _passwordController, + obscureText: true, + decoration: InputDecoration( + hintText: '비밀번호 입력', + border: OutlineInputBorder( + borderRadius: BorderRadius.circular(10), + borderSide: const BorderSide(color: Colors.black), + ), + contentPadding: const EdgeInsets.all(10), + ), + ), + const SizedBox(height: 20), + Container( + decoration: BoxDecoration( + border: Border.all(color: Colors.black54, width: 1), // 테두리 설정 + borderRadius: BorderRadius.circular(10), // 모서리 둥글게 + ), + padding: const EdgeInsets.all(16.0), // 내부 여백 + child: const Text( + '[회원 탈퇴 안내]\n' + '회원 탈퇴를 진행하시겠습니까?\n' + ' - 회원 탈퇴 시 등록하신 모든 개인정보(ID, 비밀번호, 닉네임, 이메일 주소, 소속, 자기소개 등)는 즉시 삭제되며 복구가 불가능합니다.\n' + ' - 탈퇴 후 동일한 아이디로 재가입이 불가능할 수 있습니다.\n' + ' - 관련 법령에 따라 일정 기간 보관이 필요한 경우 해당 기간 동안 법령이 허용하는 범위 내에서만 보관됩니다.\n' + '탈퇴를 원하시면 아래의 "동의" 버튼을 눌러주시기 바랍니다.', + textAlign: TextAlign.left, + style: TextStyle(fontSize: 12), + ), + ), + const SizedBox(height: 20), + Row( + children: [ + Checkbox( + value: _isAgreed, // 체크박스 상태 + activeColor: Colors.black, // 체크박스가 활성화될 때의 색상 + checkColor: Colors.white, // 체크 표시 색상 + onChanged: (value) { + setState(() { + _isAgreed = value ?? false; // 체크박스 상태 변경 + }); + }, + ), + const Expanded( + child: Text( + '회원탈퇴에 동의합니다.', + style: TextStyle(fontSize: 16), + ), + ), + ], + ), + const SizedBox(height: 20), + ElevatedButton( + onPressed: () { + // 탈퇴하기 버튼 클릭 시의 동작을 여기에 추가 + _requestWithdrawal(_passwordController.text, _isAgreed); + }, + style: ElevatedButton.styleFrom( + backgroundColor: Colors.black54, // 버튼 배경색 + padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 60), // 버튼 패딩 + ), + child: const Text( + '탈퇴하기', + style: TextStyle(color: Colors.white, fontSize: 16), // 텍스트 색상 및 크기 + ), + ), + ], + ), + ), + ); + } + + Future _requestWithdrawal(String password, bool isAgreed) async { + if (!isAgreed) { + // 체크박스가 체크되지 않은 경우 + showResponseDialog(context, '회원탈퇴 동의 확인', '회원탈퇴 동의 체크가 필요합니다.'); + return; + } + + if (password.isEmpty) { + // 비밀번호가 입력되지 않은 경우 + showResponseDialog(context, '비밀번호 확인', '비밀번호를 입력해야 합니다.'); + return; + } + + final response = await Api.serverRequest( + uri: '/user/withdraw/user', + body: { + 'user_pw': Utils.hashPassword(password), // 입력한 비밀번호 + }, + ); + + if (response['result'] == 'OK') { + final serverResponse = response['response']; + if (serverResponse['result'] == 'OK') { + // 회원탈퇴 완료 후 로그인 페이지로 이동 + SharedPreferences prefs = await SharedPreferences.getInstance(); + await prefs.remove('auth_token'); // 토큰 초기화 + + showResponseDialog(context, '회원탈퇴 완료', '회원탈퇴가 완료되었습니다.'); + Navigator.pushReplacement( + context, + MaterialPageRoute(builder: (context) => const LoginPage()), // 로그인 페이지로 이동 + ); + } else { + showResponseDialog(context, serverResponse['response_info']['msg_title'], serverResponse['response_info']['msg_content']); + } + } else { + showResponseDialog(context, '회원탈퇴 실패', '서버에 문제가 있습니다. 관리자에게 문의해주세요.'); + } + } +} \ No newline at end of file diff --git a/linux/flutter/generated_plugin_registrant.cc b/linux/flutter/generated_plugin_registrant.cc index e71a16d..64a0ece 100644 --- a/linux/flutter/generated_plugin_registrant.cc +++ b/linux/flutter/generated_plugin_registrant.cc @@ -6,6 +6,10 @@ #include "generated_plugin_registrant.h" +#include void fl_register_plugins(FlPluginRegistry* registry) { + g_autoptr(FlPluginRegistrar) file_selector_linux_registrar = + fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin"); + file_selector_plugin_register_with_registrar(file_selector_linux_registrar); } diff --git a/linux/flutter/generated_plugins.cmake b/linux/flutter/generated_plugins.cmake index 2e1de87..2db3c22 100644 --- a/linux/flutter/generated_plugins.cmake +++ b/linux/flutter/generated_plugins.cmake @@ -3,6 +3,7 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_linux ) list(APPEND FLUTTER_FFI_PLUGIN_LIST diff --git a/macos/Flutter/GeneratedPluginRegistrant.swift b/macos/Flutter/GeneratedPluginRegistrant.swift index 724bb2a..f3cb340 100644 --- a/macos/Flutter/GeneratedPluginRegistrant.swift +++ b/macos/Flutter/GeneratedPluginRegistrant.swift @@ -5,8 +5,18 @@ import FlutterMacOS import Foundation +import file_selector_macos +import firebase_auth +import firebase_core +import firebase_database import shared_preferences_foundation +import webview_flutter_wkwebview func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) { + FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin")) + FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin")) + FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin")) + FLTFirebaseDatabasePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseDatabasePlugin")) SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin")) + FLTWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "FLTWebViewFlutterPlugin")) } diff --git a/pubspec.lock b/pubspec.lock index 5bd61fa..653b66e 100644 --- a/pubspec.lock +++ b/pubspec.lock @@ -1,6 +1,14 @@ # Generated by pub # See https://dart.dev/tools/pub/glossary#lockfile packages: + _flutterfire_internals: + dependency: transitive + description: + name: _flutterfire_internals + sha256: daa1d780fdecf8af925680c06c86563cdd445deea995d5c9176f1302a2b10bbe + url: "https://pub.dev" + source: hosted + version: "1.3.48" async: dependency: transitive description: @@ -41,8 +49,16 @@ packages: url: "https://pub.dev" source: hosted version: "1.19.0" + cross_file: + dependency: transitive + description: + name: cross_file + sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670" + url: "https://pub.dev" + source: hosted + version: "0.3.4+2" crypto: - dependency: "direct dev" + dependency: "direct main" description: name: crypto sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855" @@ -81,6 +97,110 @@ packages: url: "https://pub.dev" source: hosted version: "7.0.1" + file_selector_linux: + dependency: transitive + description: + name: file_selector_linux + sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33" + url: "https://pub.dev" + source: hosted + version: "0.9.3+2" + file_selector_macos: + dependency: transitive + description: + name: file_selector_macos + sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc" + url: "https://pub.dev" + source: hosted + version: "0.9.4+2" + file_selector_platform_interface: + dependency: transitive + description: + name: file_selector_platform_interface + sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b + url: "https://pub.dev" + source: hosted + version: "2.6.2" + file_selector_windows: + dependency: transitive + description: + name: file_selector_windows + sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4" + url: "https://pub.dev" + source: hosted + version: "0.9.3+3" + firebase_auth: + dependency: "direct main" + description: + name: firebase_auth + sha256: "03483af6e67b7c4b696ca9386989a6cd5593569e1ac5af6907ea5f7fd9c16d8b" + url: "https://pub.dev" + source: hosted + version: "5.3.4" + firebase_auth_platform_interface: + dependency: transitive + description: + name: firebase_auth_platform_interface + sha256: "3e1409f48c48930635705b1237ebbdee8c54c19106a0a4fb321dbb4b642820c4" + url: "https://pub.dev" + source: hosted + version: "7.4.10" + firebase_auth_web: + dependency: transitive + description: + name: firebase_auth_web + sha256: d83fe95c44d73c9c29b006ac7df3aa5e1b8ce92b62edc44e8f86250951fe2cd0 + url: "https://pub.dev" + source: hosted + version: "5.13.5" + firebase_core: + dependency: "direct main" + description: + name: firebase_core + sha256: "15d761b95dfa2906dfcc31b7fc6fe293188533d1a3ffe78389ba9e69bd7fdbde" + url: "https://pub.dev" + source: hosted + version: "3.9.0" + firebase_core_platform_interface: + dependency: transitive + description: + name: firebase_core_platform_interface + sha256: d7253d255ff10f85cfd2adaba9ac17bae878fa3ba577462451163bd9f1d1f0bf + url: "https://pub.dev" + source: hosted + version: "5.4.0" + firebase_core_web: + dependency: transitive + description: + name: firebase_core_web + sha256: fbc008cf390d909b823763064b63afefe9f02d8afdb13eb3f485b871afee956b + url: "https://pub.dev" + source: hosted + version: "2.19.0" + firebase_database: + dependency: "direct main" + description: + name: firebase_database + sha256: "473c25413683c1c4c8d80918efdc1a232722624bad3b6edfed9fae52b8d927c1" + url: "https://pub.dev" + source: hosted + version: "11.2.0" + firebase_database_platform_interface: + dependency: transitive + description: + name: firebase_database_platform_interface + sha256: e83241bcbe4e1bcfcbfd12d0e2ef7706af009663d291efa96bc965adb9ded25d + url: "https://pub.dev" + source: hosted + version: "0.2.5+47" + firebase_database_web: + dependency: transitive + description: + name: firebase_database_web + sha256: "9f4048132a3645f1ad528c4839a7c15ad3ff922ee7761821ea9526ffd52735b7" + url: "https://pub.dev" + source: hosted + version: "0.2.6+5" flutter: dependency: "direct main" description: flutter @@ -94,6 +214,14 @@ packages: url: "https://pub.dev" source: hosted version: "5.0.0" + flutter_plugin_android_lifecycle: + dependency: transitive + description: + name: flutter_plugin_android_lifecycle + sha256: "615a505aef59b151b46bbeef55b36ce2b6ed299d160c51d84281946f0aa0ce0e" + url: "https://pub.dev" + source: hosted + version: "2.0.24" flutter_test: dependency: "direct dev" description: flutter @@ -104,8 +232,16 @@ packages: description: flutter source: sdk version: "0.0.0" + google_mobile_ads: + dependency: "direct main" + description: + name: google_mobile_ads + sha256: "4775006383a27a5d86d46f8fb452bfcb17794fc0a46c732979e49a8eb1c8963f" + url: "https://pub.dev" + source: hosted + version: "5.2.0" http: - dependency: "direct dev" + dependency: "direct main" description: name: http sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010 @@ -120,6 +256,70 @@ packages: url: "https://pub.dev" source: hosted version: "4.1.1" + image_picker: + dependency: "direct main" + description: + name: image_picker + sha256: b6951e25b795d053a6ba03af5f710069c99349de9341af95155d52665cb4607c + url: "https://pub.dev" + source: hosted + version: "0.8.9" + image_picker_android: + dependency: transitive + description: + name: image_picker_android + sha256: fa8141602fde3f7e2f81dbf043613eb44dfa325fa0bcf93c0f142c9f7a2c193e + url: "https://pub.dev" + source: hosted + version: "0.8.12+18" + image_picker_for_web: + dependency: transitive + description: + name: image_picker_for_web + sha256: "869fe8a64771b7afbc99fc433a5f7be2fea4d1cb3d7c11a48b6b579eb9c797f0" + url: "https://pub.dev" + source: hosted + version: "2.2.0" + image_picker_ios: + dependency: transitive + description: + name: image_picker_ios + sha256: "4f0568120c6fcc0aaa04511cb9f9f4d29fc3d0139884b1d06be88dcec7641d6b" + url: "https://pub.dev" + source: hosted + version: "0.8.12+1" + image_picker_linux: + dependency: transitive + description: + name: image_picker_linux + sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_macos: + dependency: transitive + description: + name: image_picker_macos + sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" + image_picker_platform_interface: + dependency: transitive + description: + name: image_picker_platform_interface + sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80" + url: "https://pub.dev" + source: hosted + version: "2.10.0" + image_picker_windows: + dependency: transitive + description: + name: image_picker_windows + sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb" + url: "https://pub.dev" + source: hosted + version: "0.2.1+1" leak_tracker: dependency: transitive description: @@ -148,10 +348,10 @@ packages: dependency: transitive description: name: lints - sha256: "4a16b3f03741e1252fda5de3ce712666d010ba2122f8e912c94f9f7b90e1a4c3" + sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7 url: "https://pub.dev" source: hosted - version: "5.1.0" + version: "5.1.1" matcher: dependency: transitive description: @@ -176,6 +376,14 @@ packages: url: "https://pub.dev" source: hosted version: "1.15.0" + mime: + dependency: transitive + description: + name: mime + sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a" + url: "https://pub.dev" + source: hosted + version: "1.0.6" path: dependency: transitive description: @@ -225,13 +433,13 @@ packages: source: hosted version: "2.1.8" shared_preferences: - dependency: "direct dev" + dependency: "direct main" description: name: shared_preferences - sha256: "95f9997ca1fb9799d494d0cb2a780fd7be075818d59f00c43832ed112b158a82" + sha256: "3c7e73920c694a436afaf65ab60ce3453d91f84208d761fbd83fc21182134d93" url: "https://pub.dev" source: hosted - version: "2.3.3" + version: "2.3.4" shared_preferences_android: dependency: transitive description: @@ -365,6 +573,38 @@ packages: url: "https://pub.dev" source: hosted version: "1.1.0" + webview_flutter: + dependency: transitive + description: + name: webview_flutter + sha256: ec81f57aa1611f8ebecf1d2259da4ef052281cb5ad624131c93546c79ccc7736 + url: "https://pub.dev" + source: hosted + version: "4.9.0" + webview_flutter_android: + dependency: transitive + description: + name: webview_flutter_android + sha256: "47a8da40d02befda5b151a26dba71f47df471cddd91dfdb7802d0a87c5442558" + url: "https://pub.dev" + source: hosted + version: "3.16.9" + webview_flutter_platform_interface: + dependency: transitive + description: + name: webview_flutter_platform_interface + sha256: d937581d6e558908d7ae3dc1989c4f87b786891ab47bb9df7de548a151779d8d + url: "https://pub.dev" + source: hosted + version: "2.10.0" + webview_flutter_wkwebview: + dependency: transitive + description: + name: webview_flutter_wkwebview + sha256: b7e92f129482460951d96ef9a46b49db34bd2e1621685de26e9eaafd9674e7eb + url: "https://pub.dev" + source: hosted + version: "3.16.3" xdg_directories: dependency: transitive description: diff --git a/pubspec.yaml b/pubspec.yaml index 73f06c3..d261cfe 100644 --- a/pubspec.yaml +++ b/pubspec.yaml @@ -30,6 +30,14 @@ environment: dependencies: flutter: 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 # The following adds the Cupertino Icons font to your application. # Use with the CupertinoIcons class for iOS style icons. @@ -38,9 +46,14 @@ dependencies: 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 # The "flutter_lints" package below contains a set of recommended lints to # encourage good coding practices. The lint set provided by the package is diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..23061cd --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,14 @@ +/* + * This file was generated by the Gradle 'init' task. + * + * The settings file is used to specify which projects to include in your build. + * For more detailed information on multi-project builds, please refer to https://docs.gradle.org/8.12/userguide/multi_project_builds.html in the Gradle documentation. + */ + +plugins { + // Apply the foojay-resolver plugin to allow automatic download of JDKs + id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0" +} + +rootProject.name = "AllSCORE" +include("app") diff --git a/windows/flutter/generated_plugin_registrant.cc b/windows/flutter/generated_plugin_registrant.cc index 8b6d468..5861e0f 100644 --- a/windows/flutter/generated_plugin_registrant.cc +++ b/windows/flutter/generated_plugin_registrant.cc @@ -6,6 +6,15 @@ #include "generated_plugin_registrant.h" +#include +#include +#include void RegisterPlugins(flutter::PluginRegistry* registry) { + FileSelectorWindowsRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FileSelectorWindows")); + FirebaseAuthPluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi")); + FirebaseCorePluginCApiRegisterWithRegistrar( + registry->GetRegistrarForPlugin("FirebaseCorePluginCApi")); } diff --git a/windows/flutter/generated_plugins.cmake b/windows/flutter/generated_plugins.cmake index b93c4c3..ce851e9 100644 --- a/windows/flutter/generated_plugins.cmake +++ b/windows/flutter/generated_plugins.cmake @@ -3,6 +3,9 @@ # list(APPEND FLUTTER_PLUGIN_LIST + file_selector_windows + firebase_auth + firebase_core ) list(APPEND FLUTTER_FFI_PLUGIN_LIST