release 적용 및 코드 전체 영어로 바꾸기 완료

This commit is contained in:
eld_master 2025-01-25 17:19:02 +09:00
parent 7278c6e06e
commit e7f95cacf2
34 changed files with 3529 additions and 2363 deletions

View File

@ -6,7 +6,7 @@
<application
android:label="올스코어"
android:label="ALLSCORE"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">

View File

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

View File

@ -10,4 +10,109 @@ class Config {
static const String baseUrl = 'https://eldsoft.com:8097';
//
static const String uploadImageUrl = '$baseUrl/user/update/profile/img';
}
//
static const String termsOfService = '''
AllScore (hereinafter referred to as the "Company") places great importance on users personal information and complies with the Personal Information Protection Act and other relevant laws. The Company provides the following information regarding the collection and use of personal information. Please read this policy carefully before providing your consent.
1. Personal Information We Collect
Required Items: ID, Password, Nickname (not your real name), Email Address
Optional Items: Affiliation, Self-introduction
2. Purpose of Collecting and Using Personal Information
- Member Management
- Identifying and authenticating members
- Preventing fraudulent or unauthorized use
- Handling inquiries related to service use
- Service Provision
- Basic services such as creating and joining game rooms
- Additional services such as providing statistics and rankings
- Customer Support and Announcements
- Delivering important notices related to the service
- Handling user inquiries and complaints
3. Retention and Use Period of Personal Information
- Upon Membership Withdrawal: All personal information collected is destroyed immediately upon withdrawal.
- Retention in Accordance with Relevant Laws: If certain information must be retained under laws such as the Act on the Consumer Protection in Electronic Commerce, etc., it will be kept for the period specified by those laws.
- Records of contracts or subscription withdrawals: 5 years
- Records of payment and supply of goods: 5 years
- Records of customer complaints or dispute resolution: 3 years
4. Procedure and Method for Destroying Personal Information
- Destruction Procedure
- The information is destroyed without delay after a member requests withdrawal or once the purpose of collection and use has been fulfilled.
- Destruction Method
- Electronic Files: Permanently deleted using methods that prevent any recovery or restoration
- Paper Documents: Shredded or incinerated
5. User Rights and How to Exercise Them
- Users may request to view, correct, delete, or suspend the processing of their personal information at any time.
- If you wish to withdraw your membership, you may do so via the "Membership Withdrawal" feature within the service or by contacting customer support.
6. Right to Refuse Consent and Possible Disadvantages
- Users have the right to refuse consent to the collection and use of personal information.
- However, if you refuse to consent to the required items, you may be restricted from using certain services.
7. Personal Information Protection Officer
Contact: eldyeojh@gmail.com
8. Measures to Ensure the Security of Personal Information
The Company takes technical and administrative measures to protect personal information, including:
- Encryption of personal information
- Countermeasures against hacking and similar threats
- Installation and operation of access control systems
''';
}
//
/*
'''올스코어(이하 "회사"라 합니다)는 이용자의 개인정보를 중요시하며, 「개인정보 보호법」 등 관련 법령을 준수하고 있습니다. 회사는 개인정보 수집 및 이용에 관한 사항을 아래와 같이 안내드리오니, 내용을 충분히 숙지하신 후 동의하여 주시기 바랍니다.
1.
: (ID), (PW), ( ),
: ,
2.
3.
: .
: .
: 5
: 5
: 3
4.
.
:
:
5.
, , , .
, "회원 탈퇴" .
6.
.
.
7.
: eldyeojh@gmail.com
8.
, .
''',
*/

View File

@ -37,7 +37,7 @@ Future<void> showResponseDialog(BuildContext context, String title, String messa
const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
),
),
child: const Text('확인'),
child: const Text('OK'),
),
),
],

View File

@ -1,14 +1,16 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'package:shared_preferences/shared_preferences.dart'; // SharedPreferences
import '../plugins/api.dart'; // server request ( )
import 'response_dialog.dart'; // response modal (/ )
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
// For waiting rooms
import '../../views/room/waiting_room_team_page.dart'; //
import '../../views/room/waiting_room_private_page.dart'; //
///
/// A separate room detail modal ( )
class RoomDetailDialog extends StatefulWidget {
final Map<String, dynamic> roomData;
final Map<String, dynamic> roomData;
//
const RoomDetailDialog({Key? key, required this.roomData}) : super(key: key);
@ -18,41 +20,56 @@ class RoomDetailDialog extends StatefulWidget {
class _RoomDetailDialogState extends State<RoomDetailDialog> {
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 String roomStatus;
/* '대기중'/'진행중'/'종료' */
late String openYn;
/* '공개'/'비공개' */
late bool isPrivate;
/* 비공개 여부 (openYn=='비공개' => true) */
late bool isWait;
/* 대기중 여부 */
late bool isRunning;
/* 진행중 여부 */
late bool isFinish;
/* 종료 여부 */
/// /
late int roomSeq;
late String roomType; // "private" "team"
late int roomSeq;
/* 서버에 전달할 방 번호 */
late String roomType;
/* 서버에 전달할 방 타입 ("private"/"team") */
/// ( + )
final TextEditingController _pwController = TextEditingController();
// ( + )
@override
void initState() {
super.initState();
// roomData에서
roomTitle = widget.roomData['room_title'] ?? '(방제목 없음)';
// Extract fields from roomData (roomData에서 )
roomTitle = widget.roomData['room_title'] ?? '(No Title)';
// '(방제목 없음)'
roomIntro = widget.roomData['room_intro'] ?? '';
roomStatus = widget.roomData['room_status'] ?? '대기중';
openYn = widget.roomData['open_yn'] ?? '공개';
roomStatus = widget.roomData['room_status'] ?? 'Waiting';
// '대기중'
openYn = widget.roomData['open_yn'] ?? 'Open';
// '공개'
//
// Info to send to server ( )
roomSeq = widget.roomData['room_seq'] ?? 0;
roomType = widget.roomData['room_type'] ?? 'private';
// "TEAM"/"team"
// e.g. "team" vs "private"
// '비공개' true
// If openYn == '비공개', treat as private (isPrivate = true)
// '비공개' => true, else => false
isPrivate = (openYn == '비공개');
// Flag
isWait = (roomStatus == '대기중');
isRunning = (roomStatus == '진행중');
isFinish = (roomStatus == '종료');
// State flags ( flag)
// originally '대기중' => isWait, '진행중' => isRunning, '종료' => isFinish
isWait = (roomStatus == '대기중' || roomStatus.toLowerCase() == 'waiting');
isRunning = (roomStatus == '진행중' || roomStatus.toLowerCase() == 'running');
isFinish = (roomStatus == '종료' || roomStatus.toLowerCase() == 'finish');
}
@override
@ -61,13 +78,13 @@ class _RoomDetailDialogState extends State<RoomDetailDialog> {
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
// +
// White background + some padding ( + )
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 24),
child: Column(
mainAxisSize: MainAxisSize.min, //
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// (A) ( )
// (A) Title at top, centered ( , )
Center(
child: Text(
roomTitle,
@ -80,9 +97,10 @@ class _RoomDetailDialogState extends State<RoomDetailDialog> {
),
const SizedBox(height: 20),
// (B) "방 소개"
// (B) "Room Intro" label ( )
const Text(
'방 소개',
'Room Description',
// '방 소개'
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.black,
@ -91,35 +109,44 @@ class _RoomDetailDialogState extends State<RoomDetailDialog> {
),
const SizedBox(height: 6),
// (C)
// (C) roomIntro area ( )
Container(
height: 80,
padding: const EdgeInsets.all(8),
child: SingleChildScrollView(
child: Text(
roomIntro.isNotEmpty ? roomIntro : '소개글이 없습니다.',
roomIntro.isNotEmpty
? roomIntro
: 'No description provided.',
// '소개글이 없습니다.'
style: const TextStyle(color: Colors.black),
),
),
),
const SizedBox(height: 16),
// (D) /
// (D) open or private (/)
Text(
isPrivate ? '비공개방' : '공개방',
isPrivate ? 'Private Room'
// '비공개방'
: 'Open Room'
// '공개방'
,
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
// (D-1) ( + )
// (D-1) password field if private + waiting ( + )
if (isPrivate && isWait) ...[
const SizedBox(height: 16),
TextField(
controller: _pwController,
obscureText: true,
decoration: const InputDecoration(
labelText: '비밀번호',
labelText: 'Password'
// '비밀번호'
,
labelStyle: TextStyle(color: Colors.black),
border: OutlineInputBorder(),
),
@ -127,7 +154,7 @@ class _RoomDetailDialogState extends State<RoomDetailDialog> {
],
const SizedBox(height: 24),
// (E)
// (E) bottom button row ( )
_buildBottomButton(),
],
),
@ -135,42 +162,50 @@ class _RoomDetailDialogState extends State<RoomDetailDialog> {
);
}
///
/// Bottom button area ( )
Widget _buildBottomButton() {
if (isWait) {
// (A) -> "입장" + "닫기"
// (A) If waiting => "Enter" + "Close" ( -> "입장" + "닫기")
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildBlackButton(
label: '입장',
onTap: _onEnterRoom, //
label: 'Enter'
// '입장'
,
onTap: _onEnterRoom,
),
_buildBlackButton(
label: '닫기',
label: 'Close'
// '닫기'
,
onTap: () => Navigator.pop(context),
),
],
);
} else if (isRunning) {
// (B) -> "확인" ( )
// (B) If running => only "OK" in center ( -> "확인")
return Center(
child: _buildBlackButton(
label: '확인',
label: 'OK'
// '확인'
,
onTap: () => Navigator.pop(context),
),
);
} else {
// (C) -> "결과보기", "확인" ( )
// (C) If finished => "View Results" + "OK" ( -> "결과보기", "확인")
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
SizedBox(
width: 100,
child: _buildBlackButton(
label: '결과보기',
label: 'View Results'
// '결과보기'
,
onTap: () {
// TODO:
// TODO: result logic
Navigator.pop(context);
},
),
@ -178,7 +213,9 @@ class _RoomDetailDialogState extends State<RoomDetailDialog> {
SizedBox(
width: 100,
child: _buildBlackButton(
label: '확인',
label: 'OK'
// '확인'
,
onTap: () => Navigator.pop(context),
),
),
@ -187,15 +224,14 @@ class _RoomDetailDialogState extends State<RoomDetailDialog> {
}
}
/// "입장"
/// "Enter" room logic ("입장" )
Future<void> _onEnterRoom() async {
final pw = _pwController.text.trim();
// API
final requestBody = {
"room_seq": "$roomSeq",
"room_type": roomType,
"room_pw": pw, // or
"room_pw": pw,
};
try {
@ -205,17 +241,20 @@ class _RoomDetailDialogState extends State<RoomDetailDialog> {
);
if (response == null || response['result'] != 'OK') {
//
showResponseDialog(context, '오류', '방 입장 실패. 서버 통신 오류.');
// Communication error ( )
showResponseDialog(context, 'Error', 'Failed to enter the room. Server communication error.'
// '방 입장 실패. 서버 통신 오류.'
);
return;
}
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
// ->
Navigator.pop(context); //
// success ()
Navigator.pop(context); // Close this modal ( )
// room_type에 /
// If it's a team room => push to waiting_room_team_page
// else => waiting_room_private_page
if (roomType.toLowerCase() == 'team') {
Navigator.pushReplacement(
context,
@ -238,17 +277,23 @@ class _RoomDetailDialogState extends State<RoomDetailDialog> {
);
}
} else {
//
final msgTitle = resp['response_info']?['msg_title'] ?? '방 입장 실패';
final msgContent = resp['response_info']?['msg_content'] ?? '오류가 발생했습니다.';
// internal fail ( )
final msgTitle = resp['response_info']?['msg_title'] ?? 'Failed to enter room'
// '방 입장 실패'
;
final msgContent = resp['response_info']?['msg_content'] ?? 'An error occurred.'
// '오류가 발생했습니다.'
;
showResponseDialog(context, msgTitle, msgContent);
}
} catch (e) {
showResponseDialog(context, '오류', '서버 요청 중 예외 발생: $e');
showResponseDialog(context, 'Error', 'An exception occurred during server request: $e'
// '서버 요청 중 예외 발생: $e'
);
}
}
///
/// Common styled black button ( )
Widget _buildBlackButton({
required String label,
required VoidCallback onTap,

View File

@ -2,16 +2,19 @@ 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'; //
// Server request ( API )
import '../../plugins/api.dart';
// Response modal ( )
import 'response_dialog.dart';
class RoomSettingModal extends StatefulWidget {
final Map<String, dynamic> roomInfo;
// : {
// :
// {
// "room_seq": "13",
// "room_master_yn": "Y",
// "room_title": "...",
// "room_type": "private" or "team"
// "room_type": "private" or "team",
// ...
// }
@ -22,25 +25,25 @@ class RoomSettingModal extends StatefulWidget {
}
class _RoomSettingModalState extends State<RoomSettingModal> {
//
//
//
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)
//
// Local state ( )
//
late bool isMaster; // whether user is the room master ( )
String openYn = 'Y'; // open or private (/)
String roomPw = ''; // password if private ( )
late int roomSeq; // room number ( )
String roomTitle = ''; // room title ( )
String roomIntro = ''; // room description ( )
int runningTime = 1; // running time ( )
int numberOfPeople = 10; // max participants ( )
String scoreOpenRange = 'PRIVATE'; // score visibility ( ) (PRIVATE / TEAM / ALL)
// FRD
// Firebase Realtime Database reference (FRD )
late DatabaseReference _roomRef;
bool _isLoading = true;
// ( )
late bool isPrivateType; // true이면 , false이면
// Distinguish if room is "private" or "team" ( )
late bool isPrivateType;
@override
void initState() {
@ -49,44 +52,46 @@ class _RoomSettingModalState extends State<RoomSettingModal> {
// (1) room_seq
roomSeq = int.tryParse('${widget.roomInfo['room_seq'] ?? '0'}') ?? 0;
// (2)
// (2) room type
final roomTypeStr = (widget.roomInfo['room_type'] ?? 'private').toString().toLowerCase();
// room_type "private" , (: "team")
// if "private" => isPrivateType=true, else => isPrivateType=false
isPrivateType = (roomTypeStr == 'private');
// (3) firebase ref
final roomKey = 'korea-$roomSeq';
_roomRef = FirebaseDatabase.instance.ref('rooms/$roomKey/roomInfo');
// (4) user_seq와 user_seq + FRD에서 roomInfo
// (4) compare my_user_seq with master_user_seq + read roomInfo from FRD
_checkMasterAndFetchData();
}
/// my_user_seq를 ,
/// FRD에서 roomInfo를 state
/// Load my_user_seq from local storage,
/// then read roomInfo from FRD and update state
/// ( my_user_seq FRD에서 roomInfo state )
Future<void> _checkMasterAndFetchData() async {
final prefs = await SharedPreferences.getInstance();
final myUserSeq = prefs.getInt('my_user_seq') ?? 0;
final snapshot = await _roomRef.get();
final snapshot = await _roomRef.get();
if (!snapshot.exists) {
//
// No room info ( )
setState(() {
_isLoading = false;
isMaster = false;
roomTitle = '방 정보 없음';
roomTitle = 'No room info';
// '방 정보 없음'
});
return;
}
final data = snapshot.value as Map<dynamic, dynamic>? ?? {};
// master_user_seq, open_yn, etc
// e.g. master_user_seq, open_yn, etc
final masterSeq = data['master_user_seq'] ?? 0;
setState(() {
isMaster = (masterSeq.toString() == myUserSeq.toString());
//
// fill fields ( )
roomTitle = data['room_title']?.toString() ?? '';
roomIntro = data['room_intro']?.toString() ?? '';
openYn = data['open_yn']?.toString() ?? 'Y';
@ -99,7 +104,7 @@ class _RoomSettingModalState extends State<RoomSettingModal> {
});
}
/// int
/// simple int conversion ( int )
int _toInt(dynamic val, int defaultVal) {
if (val == null) return defaultVal;
if (val is int) return val;
@ -109,27 +114,25 @@ class _RoomSettingModalState extends State<RoomSettingModal> {
return defaultVal;
}
/// ()
/// "Update" button click ( )
Future<void> _onUpdate() async {
// API로
// server request to update room settings ( API로 )
final requestBody = {
'room_seq': '$roomSeq',
'room_status': 'WAIT',
'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 team game, add number_of_teams ( number_of_teams )
if (!isPrivateType) {
// : 4
// FRD에서 roomInfo['number_of_teams']
// e.g. assume '4' or fetch from FRD
requestBody['number_of_teams'] = '4';
}
@ -143,23 +146,80 @@ class _RoomSettingModalState extends State<RoomSettingModal> {
final resp = response['response'] ?? {};
final serverResult = resp['result'] ?? 'FAIL';
if (serverResult == 'OK') {
await showResponseDialog(
context,
'성공',
'방 설정이 성공적으로 수정되었습니다.',
// success ()
await showDialog(
context: context,
builder: (_) => AlertDialog(
backgroundColor: Colors.white,
content: const Text('Room settings have been updated successfully.'
// '방 설정이 성공적으로 수정되었습니다.'
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
),
);
Navigator.pop(context, 'refresh');
} else {
//
final msgTitle = resp['response_info']?['msg_title'] ?? '수정 실패';
final msgContent = resp['response_info']?['msg_content'] ?? '오류가 발생했습니다.';
showResponseDialog(context, msgTitle, msgContent);
// internal failure ( )
final msgTitle = resp['response_info']?['msg_title'] ?? 'Update Failed'
// '수정 실패'
;
final msgContent = resp['response_info']?['msg_content'] ?? 'An error occurred.'
// '오류가 발생했습니다.'
;
showDialog(
context: context,
builder: (_) => AlertDialog(
backgroundColor: Colors.white,
content: Text('$msgTitle\n$msgContent'),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
),
);
}
} else {
showResponseDialog(context, '실패', '서버 통신에 실패했습니다.');
// server communication failure ( )
showDialog(
context: context,
builder: (_) => AlertDialog(
backgroundColor: Colors.white,
content: const Text('Failed - Server communication error.'
// '실패 - 서버 통신에 실패했습니다.'
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
),
);
}
} catch (e) {
showResponseDialog(context, '오류 발생', '서버 요청 중 오류가 발생했습니다.\n$e');
// request error ( )
showDialog(
context: context,
builder: (_) => AlertDialog(
backgroundColor: Colors.white,
content: Text('Error occurred while making request.\n$e'
// '서버 요청 중 오류가 발생했습니다.\n$e'
),
actions: [
TextButton(
onPressed: () => Navigator.pop(context),
child: const Text('OK'),
),
],
),
);
}
}
@ -178,9 +238,10 @@ class _RoomSettingModalState extends State<RoomSettingModal> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
//
// Modal top title ( )
Text(
'방 설정 정보',
'Room Settings',
// '방 설정 정보'
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
@ -189,8 +250,10 @@ class _RoomSettingModalState extends State<RoomSettingModal> {
),
const SizedBox(height: 12),
// (1)
_buildTitle('방 제목'),
// (1) Room title ( )
_buildTitle('Room Title'
// '방 제목'
),
TextField(
readOnly: !isMaster,
controller: TextEditingController(text: roomTitle),
@ -206,10 +269,12 @@ class _RoomSettingModalState extends State<RoomSettingModal> {
),
const SizedBox(height: 12),
// (2)
_buildTitle('방 소개'),
// (2) Room introduction ( )
_buildTitle('Room Description'
// '방 소개'
),
SizedBox(
height: 60,
height: 60,
child: TextField(
readOnly: !isMaster,
controller: TextEditingController(text: roomIntro),
@ -229,8 +294,10 @@ class _RoomSettingModalState extends State<RoomSettingModal> {
),
const SizedBox(height: 12),
// (3) (/)
_buildTitle('비밀번호 설정'),
// (3) Password setting (/)
_buildTitle('Password Setting'
// '비밀번호 설정'
),
Row(
children: [
Radio<String>(
@ -245,7 +312,9 @@ class _RoomSettingModalState extends State<RoomSettingModal> {
}
: null,
),
const Text('공개', style: TextStyle(color: Colors.black)),
const Text('Open'
// '공개'
, style: TextStyle(color: Colors.black)),
const SizedBox(width: 8),
Radio<String>(
value: 'N',
@ -259,7 +328,9 @@ class _RoomSettingModalState extends State<RoomSettingModal> {
}
: null,
),
const Text('비공개', style: TextStyle(color: Colors.black)),
const Text('Private'
// '비공개'
, style: TextStyle(color: Colors.black)),
],
),
if (openYn == 'N') ...[
@ -274,7 +345,9 @@ class _RoomSettingModalState extends State<RoomSettingModal> {
border: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black.withOpacity(0.8)),
),
hintText: '비밀번호 입력',
hintText: 'Enter password'
// '비밀번호 입력'
,
hintStyle: TextStyle(color: Colors.black.withOpacity(0.4)),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
isDense: true,
@ -284,8 +357,10 @@ class _RoomSettingModalState extends State<RoomSettingModal> {
const SizedBox(height: 12),
],
// (4)
_buildTitle('운영시간'),
// (4) Running time ()
_buildTitle('Running Time'
// '운영시간'
),
Row(
children: [
DropdownButton<int>(
@ -312,13 +387,17 @@ class _RoomSettingModalState extends State<RoomSettingModal> {
: null,
),
const SizedBox(width: 8),
const Text('시간', style: TextStyle(color: Colors.black)),
const Text('hr'
// '시간'
, style: TextStyle(color: Colors.black)),
],
),
const SizedBox(height: 12),
// (5)
_buildTitle('최대 인원수'),
// (5) Max participants ( )
_buildTitle('Max Participants'
// '최대 인원수'
),
Row(
children: [
SizedBox(
@ -344,24 +423,32 @@ class _RoomSettingModalState extends State<RoomSettingModal> {
),
),
const SizedBox(width: 8),
const Text('', style: TextStyle(color: Colors.black)),
const Text('people',
// ''
style: TextStyle(color: Colors.black)),
],
),
const SizedBox(height: 12),
// (6)
_buildTitle('점수 공개 범위'),
// PRIVATE, ALL
// PRIVATE, TEAM, ALL
// (6) Score visibility ( )
_buildTitle('Score Visibility'
// '점수 공개 범위'
),
// If private game => PRIVATE or ALL
// If team game => PRIVATE, TEAM, ALL
if (isPrivateType)
Column(
children: [
// PRIVATE
RadioListTile<String>(
value: 'PRIVATE',
groupValue: scoreOpenRange,
activeColor: Colors.black,
title: const Text('개인', style: TextStyle(color: Colors.black)),
title: const Text(
'Private'
// '개인'
,
style: TextStyle(color: Colors.black),
),
onChanged: isMaster
? (val) {
if (val != null) {
@ -370,12 +457,16 @@ class _RoomSettingModalState extends State<RoomSettingModal> {
}
: null,
),
// ALL
RadioListTile<String>(
value: 'ALL',
groupValue: scoreOpenRange,
activeColor: Colors.black,
title: const Text('전체', style: TextStyle(color: Colors.black)),
title: const Text(
'Public'
// '전체'
,
style: TextStyle(color: Colors.black),
),
onChanged: isMaster
? (val) {
if (val != null) {
@ -387,14 +478,18 @@ class _RoomSettingModalState extends State<RoomSettingModal> {
],
)
else
// PRIVATE / TEAM / ALL
Column(
children: [
RadioListTile<String>(
value: 'PRIVATE',
groupValue: scoreOpenRange,
activeColor: Colors.black,
title: const Text('개인', style: TextStyle(color: Colors.black)),
title: const Text(
'Private'
// '개인'
,
style: TextStyle(color: Colors.black),
),
onChanged: isMaster
? (val) {
if (val != null) {
@ -407,7 +502,12 @@ class _RoomSettingModalState extends State<RoomSettingModal> {
value: 'TEAM',
groupValue: scoreOpenRange,
activeColor: Colors.black,
title: const Text('', style: TextStyle(color: Colors.black)),
title: const Text(
'Team'
// ''
,
style: TextStyle(color: Colors.black),
),
onChanged: isMaster
? (val) {
if (val != null) {
@ -420,7 +520,12 @@ class _RoomSettingModalState extends State<RoomSettingModal> {
value: 'ALL',
groupValue: scoreOpenRange,
activeColor: Colors.black,
title: const Text('전체', style: TextStyle(color: Colors.black)),
title: const Text(
'Public'
// '전체'
,
style: TextStyle(color: Colors.black),
),
onChanged: isMaster
? (val) {
if (val != null) {
@ -433,7 +538,7 @@ class _RoomSettingModalState extends State<RoomSettingModal> {
),
const SizedBox(height: 20),
// (7)
// (7) Bottom buttons ( )
if (isMaster)
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
@ -444,7 +549,11 @@ class _RoomSettingModalState extends State<RoomSettingModal> {
backgroundColor: Colors.black,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
child: const Text('수정', style: TextStyle(color: Colors.white)),
child: const Text(
'Update',
// '수정'
style: TextStyle(color: Colors.white),
),
),
ElevatedButton(
onPressed: () => Navigator.pop(context),
@ -452,7 +561,11 @@ class _RoomSettingModalState extends State<RoomSettingModal> {
backgroundColor: Colors.black,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
child: const Text('확인', style: TextStyle(color: Colors.white)),
child: const Text(
'OK'
// '확인'
, style: TextStyle(color: Colors.white),
),
),
],
)
@ -464,7 +577,11 @@ class _RoomSettingModalState extends State<RoomSettingModal> {
backgroundColor: Colors.black,
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 12),
),
child: const Text('확인', style: TextStyle(color: Colors.white)),
child: const Text(
'OK'
// '확인'
, style: TextStyle(color: Colors.white),
),
),
),
],
@ -473,7 +590,7 @@ class _RoomSettingModalState extends State<RoomSettingModal> {
);
}
/// ()
/// A black-and-white style label ( () )
Widget _buildTitle(String label) {
return Align(
alignment: Alignment.centerLeft,

View File

@ -1,8 +1,10 @@
import 'package:flutter/material.dart';
/// "방 정보 보기" ()
/// A read-only modal shown when viewing "Room Info" in a finished room
/// "방 정보 보기"
class RoomSettingFinishDialog extends StatelessWidget {
final Map<String, dynamic> roomInfo;
final Map<String, dynamic> roomInfo;
//
const RoomSettingFinishDialog({
Key? key,
@ -12,23 +14,38 @@ class RoomSettingFinishDialog extends StatelessWidget {
@override
Widget build(BuildContext context) {
final media = MediaQuery.of(context);
final String roomTitle = (roomInfo['room_title'] ?? '') as String;
final String roomIntro = (roomInfo['room_intro'] ?? '') as String;
final String openYn = (roomInfo['open_yn'] ?? 'Y') as String;
final int maxPeople = (roomInfo['number_of_people'] ?? 0) as int;
final int runningTime = (roomInfo['running_time'] ?? 0) as int;
final String tempScoreOpen = (roomInfo['score_open_range'] ?? 'ALL') as String;
var scoreOpen = '';
if (tempScoreOpen == 'ALL' || tempScoreOpen == 'all') {
scoreOpen = '전체 공개';
} else if (tempScoreOpen == 'TEAM' || tempScoreOpen == 'team') {
scoreOpen = '팀 공개';
final String roomTitle = (roomInfo['room_title'] ?? '') as String;
// '방 제목'
final String roomIntro = (roomInfo['room_intro'] ?? '') as String;
// '방 소개'
final String openYn = (roomInfo['open_yn'] ?? 'Y') as String;
// '공개 여부 (Y/N)'
final int maxPeople = (roomInfo['number_of_people'] ?? 0) as int;
// '최대 인원'
final int runningTime = (roomInfo['running_time'] ?? 0) as int;
// '운영 시간'
final String tempScoreOpen = (roomInfo['score_open_range'] ?? 'ALL') as String;
// '점수 공개 범위'
var scoreOpen = '';
// convert 'ALL'/'TEAM'/'PRIVATE' into text
// 'ALL' => '전체 공개', 'TEAM' => '팀 공개', else => '개인 공개'
if (tempScoreOpen.toUpperCase() == 'ALL') {
scoreOpen = 'Public to All';
// '전체 공개'
} else if (tempScoreOpen.toUpperCase() == 'TEAM') {
scoreOpen = 'Team Only';
// '팀 공개'
} else {
scoreOpen = '개인 공개';
scoreOpen = 'Private';
// '개인 공개'
}
final String openLabel = (openYn == 'Y') ? '공개' : '비공개';
final String openLabel = (openYn == 'Y') ? 'Public'
/* '공개' */
: 'Private'
/* '비공개' */;
return Dialog(
backgroundColor: Colors.white,
@ -44,9 +61,10 @@ class RoomSettingFinishDialog extends StatelessWidget {
mainAxisSize: MainAxisSize.min,
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// (A)
// (A) Top title ( )
const Text(
'방 정보',
'Room Info',
// '방 정보'
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
@ -56,13 +74,21 @@ class RoomSettingFinishDialog extends StatelessWidget {
const Divider(color: Colors.black54),
const SizedBox(height: 12),
// (B)
_buildLabelValue(label: '방 제목', value: roomTitle.isNotEmpty ? roomTitle : '(없음)'),
// (B) Room title ( )
_buildLabelValue(
label: 'Room Title'
// '방 제목'
,
value: roomTitle.isNotEmpty ? roomTitle : '(None)'
// '(없음)'
),
const SizedBox(height: 14),
// (C)
// (C) Room introduction ( )
const Text(
'방 소개',
'Room Description'
// '방 소개'
,
style: TextStyle(
fontSize: 15,
fontWeight: FontWeight.bold,
@ -70,11 +96,9 @@ class RoomSettingFinishDialog extends StatelessWidget {
),
),
const SizedBox(height: 6),
// (width: double.infinity), 100px
Container(
width: double.infinity,
constraints: const BoxConstraints(maxHeight: 100),
constraints: const BoxConstraints(maxHeight: 100),
decoration: BoxDecoration(
border: Border.all(color: Colors.black26),
borderRadius: BorderRadius.circular(8),
@ -82,7 +106,10 @@ class RoomSettingFinishDialog extends StatelessWidget {
padding: const EdgeInsets.all(8),
child: SingleChildScrollView(
child: Text(
roomIntro.isNotEmpty ? roomIntro : '소개글 없음',
roomIntro.isNotEmpty
? roomIntro
: 'No introduction provided.',
// '소개글 없음'
style: const TextStyle(fontSize: 14, color: Colors.black),
softWrap: true,
),
@ -90,23 +117,45 @@ class RoomSettingFinishDialog extends StatelessWidget {
),
const SizedBox(height: 16),
// (D)
_buildLabelValue(label: '공개 여부', value: openLabel),
// (D) open or private ( )
_buildLabelValue(
label: 'Open or Private'
// '공개 여부'
,
value: openLabel
),
const SizedBox(height: 8),
// (E)
_buildLabelValue(label: '최대 인원', value: '$maxPeople'),
// (E) Max participants ( )
_buildLabelValue(
label: 'Max Participants'
// '최대 인원'
,
value: '$maxPeople people'
// '$maxPeople'
),
const SizedBox(height: 8),
// (F)
_buildLabelValue(label: '운영 시간', value: '$runningTime'),
// (F) Running time ( )
_buildLabelValue(
label: 'Running Time'
// '운영 시간'
,
value: '$runningTime min'
// '$runningTime'
),
const SizedBox(height: 8),
// (G)
_buildLabelValue(label: '점수 공개 범위', value: scoreOpen),
// (G) Score open range ( )
_buildLabelValue(
label: 'Score Visibility'
// '점수 공개 범위'
,
value: scoreOpen
),
const SizedBox(height: 24),
// (H) ( )
// (H) Close button at bottom ( , )
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
@ -120,7 +169,10 @@ class RoomSettingFinishDialog extends StatelessWidget {
),
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
),
child: const Text('닫기'),
child: const Text(
'Close'
// '닫기'
),
),
],
),
@ -131,6 +183,7 @@ class RoomSettingFinishDialog extends StatelessWidget {
);
}
/// A simple widget that vertically displays a label + value
/// label + value를
Widget _buildLabelValue({
required String label,

View File

@ -1,11 +1,11 @@
import 'package:flutter/material.dart';
import '../plugins/api.dart'; //
import 'response_dialog.dart'; // etc
import '../plugins/api.dart'; // server requests ( )
import 'response_dialog.dart'; // response modal etc. ( etc)
class ScoreEditDialog extends StatefulWidget {
final int roomSeq;
final String roomType; // "PRIVATE" or "TEAM"
final Map<String, dynamic> userData;
final int roomSeq; //
final String roomType; // "PRIVATE" or "TEAM"
final Map<String, dynamic> userData; //
const ScoreEditDialog({
Key? key,
@ -30,6 +30,7 @@ class _ScoreEditDialogState extends State<ScoreEditDialog> {
}
Future<void> _onApplyScore() async {
// Apply the score changes ( )
final reqBody = {
"room_seq": "${widget.roomSeq}",
"room_type": widget.roomType,
@ -45,36 +46,55 @@ class _ScoreEditDialogState extends State<ScoreEditDialog> {
if (response['result'] == 'OK') {
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
//
await showResponseDialog(context, '성공', '점수가 업데이트되었습니다.');
// success ()
await showResponseDialog(
context,
'Success', // '성공'
'Score has been updated.' // '점수가 업데이트되었습니다.'
);
Navigator.pop(context, 'refresh');
} else {
final msgTitle = resp['response_info']?['msg_title'] ?? '오류';
final msgContent = resp['response_info']?['msg_content'] ?? '점수 업데이트 실패';
final msgTitle = resp['response_info']?['msg_title'] ?? 'Error';
// '오류'
final msgContent = resp['response_info']?['msg_content'] ?? 'Score update failed';
// '점수 업데이트 실패'
showResponseDialog(context, msgTitle, msgContent);
}
} else {
showResponseDialog(context, '실패', '서버 통신 실패');
showResponseDialog(
context,
'Failed', // '실패'
'Server communication failed.' // '서버 통신 실패'
);
}
} catch (e) {
showResponseDialog(context, '오류 발생', '$e');
showResponseDialog(
context,
'Error occurred',
// '오류 발생'
'$e'
);
}
}
void _onDelta(int delta) {
// Increase or decrease the new score ( )
setState(() {
newScore += delta;
// if (newScore < 0) newScore = 0; // 0
if (newScore > 999999) newScore = 999999; //
if (newScore < -999999) newScore = -999999; //
// Arbitrary maximum/minimum ( /)
if (newScore > 999999) newScore = 999999;
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'] ?? '';
final userName = widget.userData['nickname'] ?? 'User';
// '유저'
final department = widget.userData['department'] ?? '';
// '소속정보'
final introduce = widget.userData['introduce_myself'] ?? '';
// '소개글'
return Dialog(
backgroundColor: Colors.white,
@ -84,17 +104,33 @@ class _ScoreEditDialogState extends State<ScoreEditDialog> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('유저 정보 보기', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
Text(
'View User Info',
// '유저 정보 보기'
style: const 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)),
// Nickname & Department ( + )
Text(
userName,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
Text(
department,
style: const TextStyle(fontSize: 14, color: Colors.grey),
),
const SizedBox(height: 12),
//
// Introduction ()
const Align(
alignment: Alignment.centerLeft,
child: Text('소개글', style: TextStyle(fontWeight: FontWeight.bold)),
child: Text(
'Introduction',
// '소개글'
style: TextStyle(fontWeight: FontWeight.bold),
),
),
const SizedBox(height: 4),
Container(
@ -106,30 +142,40 @@ class _ScoreEditDialogState extends State<ScoreEditDialog> {
),
child: SingleChildScrollView(
padding: const EdgeInsets.all(8),
child: Text(introduce.isNotEmpty ? introduce : '소개글이 없습니다.'),
child: Text(
introduce.isNotEmpty
? introduce
: 'No introduction.',
// '소개글이 없습니다.'
),
),
),
const SizedBox(height: 12),
//
// Score editing area ( )
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('$currentScore',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
Text(
'$currentScore',
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const Text(''),
Text('$newScore',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
Text(
'$newScore',
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
],
),
const SizedBox(height: 8),
SizedBox(
// height: 120, //
child: GridView.count(
crossAxisCount: 2, // 3
crossAxisCount: 2,
mainAxisSpacing: 8,
crossAxisSpacing: 8,
childAspectRatio: 2.0, // :
childAspectRatio: 2.0,
shrinkWrap: true,
physics: const NeverScrollableScrollPhysics(),
children: [
@ -144,18 +190,27 @@ class _ScoreEditDialogState extends State<ScoreEditDialog> {
),
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)),
child: const Text(
'Apply',
// '적용'
style: TextStyle(color: Colors.white),
),
),
ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(backgroundColor: Colors.black),
child: const Text('닫기', style: TextStyle(color: Colors.white)),
child: const Text(
'Close',
// '닫기'
style: TextStyle(color: Colors.white),
),
),
],
),
@ -172,7 +227,7 @@ class _ScoreEditDialogState extends State<ScoreEditDialog> {
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
side: BorderSide(color: Colors.black),
side: const BorderSide(color: Colors.black),
),
child: Text(label),
);

View File

@ -1,7 +1,7 @@
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'; // ( )
import '../views/user/my_page.dart'; // ( )
import '../views/inquiry/inquiry_to_manager_page.dart'; // ( )
void showSettingsDialog(BuildContext context) {
@ -17,6 +17,7 @@ void showSettingsDialog(BuildContext context) {
width: MediaQuery.of(context).size.width * 0.2,
child: const Text(
'',
// ()
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
@ -27,7 +28,8 @@ void showSettingsDialog(BuildContext context) {
Container(
width: MediaQuery.of(context).size.width * 0.2,
child: const Text(
'설정',
'Settings',
// '설정'
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
@ -51,15 +53,20 @@ void showSettingsDialog(BuildContext context) {
width: double.infinity,
child: TextButton(
onPressed: () {
// Navigate to My Page
// '내 정보 관리'
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const MyPage()), //
MaterialPageRoute(builder: (context) => const MyPage()),
);
},
style: ButtonStyle(
side: MaterialStateProperty.all(const BorderSide(color: Colors.black)),
foregroundColor: MaterialStateProperty.all(Colors.black),
),
child: const Text('내 정보 관리'),
child: const Text(
'Manage My Info',
// '내 정보 관리'
),
),
),
const SizedBox(height: 10),
@ -67,15 +74,20 @@ void showSettingsDialog(BuildContext context) {
width: double.infinity,
child: TextButton(
onPressed: () {
// Navigate to Inquiry Page
// '문의하기'
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const InquiryToManagerPage()), //
MaterialPageRoute(builder: (context) => const InquiryToManagerPage()),
);
},
style: ButtonStyle(
side: MaterialStateProperty.all(const BorderSide(color: Colors.black)),
foregroundColor: MaterialStateProperty.all(Colors.black),
),
child: const Text('문의하기'),
child: const Text(
'Contact Us',
// '문의하기'
),
),
),
const SizedBox(height: 10),
@ -83,13 +95,18 @@ void showSettingsDialog(BuildContext context) {
width: double.infinity,
child: TextButton(
onPressed: () async {
//
// Logout
// '로그아웃'
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setString('auth_token', ''); // auth_token
await prefs.setBool('auto_login', false); // auto_login
await prefs.setString('auth_token', '');
// auth_token
await prefs.setBool('auto_login', false);
// auto_login
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (context) => const LoginPage()), //
MaterialPageRoute(builder: (context) => const LoginPage()),
//
(route) => false,
);
},
@ -97,7 +114,10 @@ void showSettingsDialog(BuildContext context) {
side: MaterialStateProperty.all(const BorderSide(color: Colors.black)),
foregroundColor: MaterialStateProperty.all(Colors.black),
),
child: const Text('로그아웃'),
child: const Text(
'Logout',
// '로그아웃'
),
),
),
],
@ -105,4 +125,4 @@ void showSettingsDialog(BuildContext context) {
);
},
);
}
}

View File

@ -2,7 +2,7 @@ import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../survey/survey_page.dart';
/// ()
/// Shows the survey modal (() )
Future<void> showSurveyDialog(BuildContext context, String nickname) async {
showDialog(
context: context,
@ -11,34 +11,44 @@ Future<void> showSurveyDialog(BuildContext context, String nickname) async {
);
}
/// AlertDialog
/// The actual AlertDialog widget ( AlertDialog )
class SurveyDialog extends StatefulWidget {
final String nickname;
const SurveyDialog({Key? key, required this.nickname}) : super(key: key);
const SurveyDialog({
Key? key,
required this.nickname,
}) : super(key: key);
@override
State<SurveyDialog> createState() => _SurveyDialogState();
}
class _SurveyDialogState extends State<SurveyDialog> {
bool _todayNotSee = false; // "오늘 하루 보지 않기"
bool _todayNotSee = false;
// "오늘 하루 보지 않기"
@override
Widget build(BuildContext context) {
return AlertDialog(
title: const Text('설문 참여 안내'),
title: const Text('Survey Participation Notice'),
// '설문 참여 안내'
content: SingleChildScrollView(
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'안녕하세요, "올스코어" 앱을 이용해주셔서 감사합니다.\n\n'
'더 나은 서비스 제공을 위해 간단한 설문조사를 준비했습니다.\n'
'설문조사에 참여해주시면 앱 발전에 큰 도움이 됩니다!\n'
'(약 1분 소요)',
'Hello, thank you for using the "ALLSCORE" app.\n\n'
'We have prepared a simple survey to provide better service.\n'
'Your participation will greatly help the apps improvement!\n'
'(It takes about 1 minute.)'
// '안녕하세요, "올스코어" 앱을 이용해주셔서 감사합니다.\n\n'
// '더 나은 서비스 제공을 위해 간단한 설문조사를 준비했습니다.\n'
// '설문조사에 참여해주시면 앱 발전에 큰 도움이 됩니다!\n'
// '(약 1분 소요)'
),
const SizedBox(height: 16),
// "오늘 하루 보지 않기"
// "Today do not see again" checkbox ("오늘 하루 보지 않기" )
Row(
children: [
Checkbox(
@ -49,44 +59,55 @@ class _SurveyDialogState extends State<SurveyDialog> {
});
},
),
const Text('오늘 하루 보지 않기'),
const Text('Do not show again today'),
// '오늘 하루 보지 않기'
],
),
],
),
),
actions: [
// "닫기"
// "Close" button ("닫기" )
TextButton(
onPressed: () async {
// , SharedPreferences
// If "Do not show again today" is checked, store in SharedPreferences
// ( , SharedPreferences )
if (_todayNotSee) {
final prefs = await SharedPreferences.getInstance();
// : survey_popup_today = 'Y'
// e.g. survey_popup_today = 'Y'
await prefs.setString('survey_popup_today', 'Y');
}
Navigator.pop(context); //
Navigator.pop(context); // close popup ( )
},
style: TextButton.styleFrom(backgroundColor: Colors.grey),
child: const Text('닫기', style: TextStyle(color: Colors.white)),
child: const Text(
'Close',
// '닫기'
style: TextStyle(color: Colors.white),
),
),
// "설문 참여"
// "Participate in Survey" button ("설문 참여" )
TextButton(
onPressed: () async {
// , SharedPreferences
// If "Do not show again today" is checked, store in SharedPreferences
// ( , SharedPreferences )
if (_todayNotSee) {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('survey_popup_today', 'Y');
}
Navigator.pop(context); //
//
Navigator.pop(context); // close popup ( )
// Navigate to the temporary survey page ( )
Navigator.push(
context,
MaterialPageRoute(builder: (_) => SurveyPage(nickname: widget.nickname)),
);
},
style: TextButton.styleFrom(backgroundColor: Colors.black),
child: const Text('설문 참여', style: TextStyle(color: Colors.white)),
child: const Text(
'Participate in Survey',
// '설문 참여'
style: TextStyle(color: Colors.white),
),
),
],
);

View File

@ -1,12 +1,16 @@
import 'package:flutter/material.dart';
import 'response_dialog.dart';
import '../../plugins/api.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<String> existingTeamNames; // (ex: ["A","B","C","WAIT"...]
final int roomSeq;
//
final String roomTypeName;
// "TEAM"
final String beforeTeamName;
//
final List<String> existingTeamNames;
// (ex: ["A","B","C","WAIT"] )
const TeamNameEditModal({
Key? key,
@ -22,28 +26,33 @@ class TeamNameEditModal extends StatefulWidget {
class _TeamNameEditModalState extends State<TeamNameEditModal> {
String afterTeamName = '';
String _errorMsg = ''; //
//
String _errorMsg = '';
// ( )
Future<void> _onUpdateTeamName() async {
//
final newName = afterTeamName.trim().toUpperCase();
// If new name is empty or same as old name ( )
if (newName.isEmpty) {
setState(() {
_errorMsg = '새 팀명을 입력해주세요.';
_errorMsg = 'Please enter a new team name.';
// '새 팀명을 입력해주세요.'
});
return;
}
// (, teamName과 OK)
// : beforeTeamName= "A", user가 "B" existingTeamNames=["A","B","C"]
// Check duplication ( )
final existingNames = widget.existingTeamNames.map((e) => e.toUpperCase()).toList();
if (newName != widget.beforeTeamName.toUpperCase() && existingNames.contains(newName)) {
setState(() {
_errorMsg = '이미 존재하는 팀명입니다.';
_errorMsg = 'Team name already exists.';
// '이미 존재하는 팀명입니다.'
});
return;
}
// body
// Request body ( )
// {
// "room_seq": "9",
// "room_type_name": "TEAM",
@ -52,7 +61,7 @@ class _TeamNameEditModalState extends State<TeamNameEditModal> {
// }
final reqBody = {
'room_seq': '${widget.roomSeq}',
'room_type_name': widget.roomTypeName, // "TEAM"
'room_type_name': widget.roomTypeName,
'before_team_name': widget.beforeTeamName,
'after_team_name': newName,
};
@ -65,19 +74,30 @@ class _TeamNameEditModalState extends State<TeamNameEditModal> {
if (response['result'] == 'OK') {
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
//
await showResponseDialog(context, '성공', '팀명이 성공적으로 수정되었습니다.');
// Success ()
await showResponseDialog(context, 'Success' /* '성공' */, 'Team name has been successfully updated.'
/* '팀명이 성공적으로 수정되었습니다.' */);
Navigator.pop(context, 'refresh');
} else {
final msgTitle = resp['response_info']?['msg_title'] ?? '오류';
final msgContent = resp['response_info']?['msg_content'] ?? '수정 실패';
final msgTitle = resp['response_info']?['msg_title'] ?? 'Error'
// '오류'
;
final msgContent = resp['response_info']?['msg_content'] ?? 'Update failed'
// '수정 실패'
;
showResponseDialog(context, msgTitle, msgContent);
}
} else {
showResponseDialog(context, '실패', '서버 통신 실패');
showResponseDialog(context, 'Failed'
// '실패'
, 'Server communication failed.'
// '서버 통신 실패'
);
}
} catch (e) {
showResponseDialog(context, '오류 발생', '$e');
showResponseDialog(context, 'Error occurred'
// '오류 발생'
, '$e');
}
}
@ -91,24 +111,32 @@ class _TeamNameEditModalState extends State<TeamNameEditModal> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('팀명 수정',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const Text(
'Edit Team Name'
// '팀명 수정'
,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
Text('기존 팀명: ${widget.beforeTeamName}'),
Text(
'Current team name: ${widget.beforeTeamName}'
// '기존 팀명: ${widget.beforeTeamName}'
),
const SizedBox(height: 8),
TextField(
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: '새 팀명',
labelText: 'New Team Name'
// '새 팀명'
),
onChanged: (val) {
setState(() {
afterTeamName = val;
_errorMsg = ''; //
afterTeamName = val;
_errorMsg = '';
});
},
),
//
// Error message ( )
if (_errorMsg.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
@ -125,8 +153,12 @@ class _TeamNameEditModalState extends State<TeamNameEditModal> {
child: ElevatedButton(
onPressed: _onUpdateTeamName,
style: ElevatedButton.styleFrom(backgroundColor: Colors.black),
child:
const Text('수정', style: TextStyle(color: Colors.white)),
child: const Text(
'Update'
// '수정'
,
style: TextStyle(color: Colors.white),
),
),
),
SizedBox(
@ -134,8 +166,12 @@ class _TeamNameEditModalState extends State<TeamNameEditModal> {
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(backgroundColor: Colors.black),
child:
const Text('취소', style: TextStyle(color: Colors.white)),
child: const Text(
'Cancel'
// '취소'
,
style: TextStyle(color: Colors.white),
),
),
),
],

View File

@ -2,7 +2,8 @@
import 'package:flutter/material.dart';
class UserInfoBasicDialog extends StatelessWidget {
final Map<String, dynamic> userData;
final Map<String, dynamic> userData;
//
const UserInfoBasicDialog({
Key? key,
@ -11,9 +12,12 @@ class UserInfoBasicDialog extends StatelessWidget {
@override
Widget build(BuildContext context) {
final userName = userData['nickname'] ?? '유저';
final userName = userData['nickname'] ?? 'User';
// '유저'
final department = userData['department'] ?? '';
// '소속정보'
final introduce = userData['introduce_myself'] ?? '';
// '소개글'
return Dialog(
backgroundColor: Colors.white,
@ -23,18 +27,31 @@ class UserInfoBasicDialog extends StatelessWidget {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('유저 정보',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const Text(
'User Info',
// '유저 정보'
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
Text(userName, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
Text(
userName,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Text(department, style: const TextStyle(fontSize: 14, color: Colors.grey)),
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)),
child: Text(
'Introduction',
// '소개글'
style: TextStyle(fontWeight: FontWeight.bold),
),
),
const SizedBox(height: 4),
Container(
@ -46,7 +63,12 @@ class UserInfoBasicDialog extends StatelessWidget {
),
child: SingleChildScrollView(
padding: const EdgeInsets.all(8),
child: Text(introduce.isNotEmpty ? introduce : '소개글이 없습니다.'),
child: Text(
introduce.isNotEmpty
? introduce
: 'No introduction.',
// '소개글이 없습니다.'
),
),
),
@ -54,7 +76,11 @@ class UserInfoBasicDialog extends StatelessWidget {
ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(backgroundColor: Colors.black),
child: const Text('확인', style: TextStyle(color: Colors.white)),
child: const Text(
'OK',
// '확인'
style: TextStyle(color: Colors.white),
),
),
],
),

View File

@ -1,29 +1,38 @@
import 'package:flutter/material.dart';
import 'response_dialog.dart'; // Not used here, just referencing
import '../../plugins/api.dart'; // Not used here, just referencing
class UserInfoFinishDialog extends StatelessWidget {
final Map<String, dynamic> userData;
final Map<String, dynamic> userData;
//
const UserInfoFinishDialog({Key? key, required this.userData}) : super(key: key);
@override
Widget build(BuildContext context) {
// : { "nickname": "testuser4", "profile_img": "...", "introduce_myself": "..." }
// e.g. { "nickname": "testuser4", "profile_img": "...", "introduce_myself": "..." }
final nickname = (userData['nickname'] ?? '').toString();
//
final profileImg = (userData['profile_img'] ?? '').toString();
//
final intro = (userData['introduce_myself'] ?? '').toString().trim();
//
return Dialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
// Give more horizontal/vertical padding
// ,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
// (A)
// (A) Top Title
//
const Text(
'유저 정보',
'User Info',
//
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
@ -31,7 +40,8 @@ class UserInfoFinishDialog extends StatelessWidget {
),
const SizedBox(height: 16),
// (B)
// (B) Profile Image
//
Container(
width: 100,
height: 100,
@ -44,12 +54,19 @@ class UserInfoFinishDialog extends StatelessWidget {
? Image.network(
'https://eldsoft.com:8097/images$profileImg',
fit: BoxFit.cover,
errorBuilder: (ctx, err, st) =>
const Center(child: Text('이미지\n오류', textAlign: TextAlign.center, style: TextStyle(fontSize: 12))),
errorBuilder: (ctx, err, st) => const Center(
child: Text(
'Image\nError',
// '이미지\n오류'
textAlign: TextAlign.center,
style: TextStyle(fontSize: 12),
),
),
)
: const Center(
child: Text(
'이미지\n없음',
'No\nImage',
// '이미지\n없음'
textAlign: TextAlign.center,
style: TextStyle(fontSize: 12),
),
@ -58,9 +75,11 @@ class UserInfoFinishDialog extends StatelessWidget {
),
const SizedBox(height: 16),
// (C)
// (C) Nickname
//
Text(
nickname.isNotEmpty ? nickname : '유저',
nickname.isNotEmpty ? nickname : 'User',
// if empty => '유저'
style: const TextStyle(
fontSize: 16,
fontWeight: FontWeight.bold,
@ -69,11 +88,13 @@ class UserInfoFinishDialog extends StatelessWidget {
const SizedBox(height: 20),
// (D)
// (D) Introduction label
//
const Align(
alignment: Alignment.centerLeft,
child: Text(
'소개',
'Introduction',
// '소개'
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
@ -82,10 +103,10 @@ class UserInfoFinishDialog extends StatelessWidget {
),
const SizedBox(height: 6),
// (E) ( )
// (E) Introduction Content
// ( )
Container(
width: double.infinity,
// 60, 200
constraints: const BoxConstraints(
minHeight: 60,
maxHeight: 200,
@ -97,7 +118,8 @@ class UserInfoFinishDialog extends StatelessWidget {
padding: const EdgeInsets.all(8),
child: SingleChildScrollView(
child: Text(
intro.isNotEmpty ? intro : '소개글이 없습니다.',
intro.isNotEmpty ? intro : 'No introduction.',
// '소개글이 없습니다.'
style: const TextStyle(fontSize: 14),
),
),
@ -105,7 +127,8 @@ class UserInfoFinishDialog extends StatelessWidget {
const SizedBox(height: 24),
// (F)
// (F) OK button
//
SizedBox(
width: 100,
child: ElevatedButton(
@ -116,7 +139,11 @@ class UserInfoFinishDialog extends StatelessWidget {
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
padding: const EdgeInsets.symmetric(vertical: 12),
),
child: const Text('확인', style: TextStyle(fontSize: 14)),
child: const Text(
'OK',
// '확인'
style: TextStyle(fontSize: 14),
),
),
),
],

View File

@ -5,10 +5,14 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'package:auto_size_text/auto_size_text.dart';
class UserInfoPrivateDialog extends StatefulWidget {
final Map<String, dynamic> userData;
final bool isRoomMaster; //
final int roomSeq;
final String roomTypeName; // "PRIVATE"
final Map<String, dynamic> userData;
/* 유저 데이터 */
final bool isRoomMaster;
/* 현재 로그인 유저가 방장인지 */
final int roomSeq;
/* 방 번호 */
final String roomTypeName;
/* "PRIVATE" */
const UserInfoPrivateDialog({
Key? key,
@ -23,12 +27,14 @@ class UserInfoPrivateDialog extends StatefulWidget {
}
class _UserInfoPrivateDialogState extends State<UserInfoPrivateDialog> {
late String participantType; // 'ADMIN' or 'PLAYER'
late String introduceMyself;
late String participantType;
/* 'ADMIN' or 'PLAYER' */
late String introduceMyself;
/* 소개글 */
//
double scaleFactor = 1.0;
double buttonScaleFactor = 1.0;
double scaleFactor = 1.0;
/* 화면 크기에 따라 폰트 크기 조절 */
double buttonScaleFactor = 1.0;
@override
void initState() {
@ -44,7 +50,7 @@ class _UserInfoPrivateDialogState extends State<UserInfoPrivateDialog> {
_updateScaleFactor();
}
//
// Adjust font size by screen width ( )
void _updateScaleFactor() {
final screenWidth = MediaQuery.of(context).size.width;
const baseWidth = 450.0;
@ -54,14 +60,15 @@ class _UserInfoPrivateDialogState extends State<UserInfoPrivateDialog> {
});
}
/// API ( )
/// API to update role (for Room Master only)
/// /* 역할 변경 API (방장 전용) */
Future<void> _onUpdateUserInfo() async {
//
if (!widget.isRoomMaster) return;
if (!widget.isRoomMaster) return;
// /* 방장만 수정하기 가능 */
final requestBody = {
'room_seq': '${widget.roomSeq}',
'room_type_name': widget.roomTypeName, // "PRIVATE"
'room_type_name': widget.roomTypeName,
'target_user_seq': '${widget.userData['user_seq']}',
'participant_type': participantType,
};
@ -74,30 +81,48 @@ class _UserInfoPrivateDialogState extends State<UserInfoPrivateDialog> {
if (response['result'] == 'OK') {
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
await showResponseDialog(context, '성공', '역할이 성공적으로 수정되었습니다.');
await showResponseDialog(
context,
'Success' /* '성공' */,
'Role has been successfully updated.'
/* '역할이 성공적으로 수정되었습니다.' */
);
Navigator.pop(context, 'refresh');
} else {
final msgTitle = resp['response_info']?['msg_title'] ?? '오류';
final msgContent = resp['response_info']?['msg_content'] ?? '수정 실패';
final msgTitle = resp['response_info']?['msg_title'] ?? 'Error'
/* '오류' */;
final msgContent = resp['response_info']?['msg_content'] ?? 'Update failed'
/* '수정 실패' */;
showResponseDialog(context, msgTitle, msgContent);
}
} else {
showResponseDialog(context, '실패', '서버 통신에 실패했습니다.');
showResponseDialog(context, 'Failed'
/* '실패' */,
'Server communication failed.'
/* '서버 통신에 실패했습니다.' */
);
}
} catch (e) {
showResponseDialog(context, '오류 발생', '$e');
showResponseDialog(context, 'Error occurred'
/* '오류 발생' */, '$e');
}
}
/// : "추방하기" ( )
/// Kick participant (Room Master only)
/// /* 새로 추가: "추방하기" (방장 전용) */
Future<void> _onKickParticipant() async {
// -> return
if (!widget.isRoomMaster) return;
//
if (!widget.isRoomMaster) return;
/* 방장이 아닌데 추방 시도 -> return */
final prefs = await SharedPreferences.getInstance();
final mySeq = prefs.getInt('my_user_seq')?.toString() ?? '0';
if (widget.userData['user_seq'] == mySeq) {
await showResponseDialog(context, '오류', '방장은 추방할 수 없습니다.');
await showResponseDialog(
context,
'Error' /* '오류' */,
'The master cannot be kicked out.'
/* '방장은 추방할 수 없습니다.' */
);
return;
}
@ -114,26 +139,39 @@ class _UserInfoPrivateDialogState extends State<UserInfoPrivateDialog> {
if (response['result'] == 'OK') {
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
//
await showResponseDialog(context, '성공', '해당 유저가 강퇴되었습니다.');
await showResponseDialog(
context,
'Success'
/* '성공' */,
'The user has been kicked out.'
/* '해당 유저가 강퇴되었습니다.' */
);
Navigator.pop(context, 'refresh');
} else {
final msgTitle = resp['response_info']?['msg_title'] ?? '오류';
final msgContent = resp['response_info']?['msg_content'] ?? '강퇴 실패';
final msgTitle = resp['response_info']?['msg_title'] ?? 'Error'
/* '오류' */;
final msgContent = resp['response_info']?['msg_content'] ?? 'Kick-out failed'
/* '강퇴 실패' */;
showResponseDialog(context, msgTitle, msgContent);
}
} else {
showResponseDialog(context, '실패', '서버 통신 실패');
showResponseDialog(context, 'Failed'
/* '실패' */,
'Server communication failed.'
/* '서버 통신 실패' */);
}
} catch (e) {
showResponseDialog(context, '오류 발생', '$e');
showResponseDialog(context, 'Error occurred'
/* '오류 발생' */, '$e');
}
}
@override
Widget build(BuildContext context) {
final userName = widget.userData['nickname'] ?? '유저';
final department = widget.userData['department'] ?? '소속정보없음';
final userName = widget.userData['nickname'] ?? 'User'
/* '유저' */;
final department = widget.userData['department'] ?? 'No Department Info'
/* '소속정보없음' */;
final profileImg = widget.userData['profile_img'] ?? '';
return Dialog(
@ -145,17 +183,19 @@ class _UserInfoPrivateDialogState extends State<UserInfoPrivateDialog> {
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'유저 정보',
'User Info'
/* '유저 정보' */,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
// (A)
// (A) Profile section ( )
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 80, height: 80,
width: 80,
height: 80,
decoration: BoxDecoration(
border: Border.all(color: Colors.black),
borderRadius: BorderRadius.circular(16),
@ -167,10 +207,20 @@ class _UserInfoPrivateDialogState extends State<UserInfoPrivateDialog> {
'https://eldsoft.com:8097/images$profileImg',
fit: BoxFit.cover,
errorBuilder: (ctx, err, st) => const Center(
child: Text('이미지\n불가', textAlign: TextAlign.center),
child: Text(
'Image\nError'
/* '이미지\n불가' */,
textAlign: TextAlign.center,
),
),
)
: const Center(child: Text('이미지\n없음', textAlign: TextAlign.center)),
: const Center(
child: Text(
'No\nImage'
/* '이미지\n없음' */,
textAlign: TextAlign.center,
),
),
),
),
const SizedBox(width: 16),
@ -194,16 +244,29 @@ class _UserInfoPrivateDialogState extends State<UserInfoPrivateDialog> {
),
const SizedBox(height: 16),
// (B)
// (B) Room master can update role
// /* 방장이면 역할 수정 가능 */
if (widget.isRoomMaster) ...[
Row(
children: [
const Text('역할: ', style: TextStyle(fontWeight: FontWeight.bold)),
const Text(
'Role: '
/* '역할: ' */,
style: TextStyle(fontWeight: FontWeight.bold),
),
DropdownButton<String>(
value: participantType,
items: const [
DropdownMenuItem(value: 'ADMIN', child: Text('사회자')),
DropdownMenuItem(value: 'PLAYER', child: Text('참가자')),
DropdownMenuItem(
value: 'ADMIN',
child: Text('Moderator'
/* '사회자' */),
),
DropdownMenuItem(
value: 'PLAYER',
child: Text('Participant'
/* '참가자' */),
),
],
onChanged: (val) {
if (val == null) return;
@ -216,14 +279,22 @@ class _UserInfoPrivateDialogState extends State<UserInfoPrivateDialog> {
),
const SizedBox(height: 12),
] else ...[
Text('역할: $participantType', style: const TextStyle(fontWeight: FontWeight.bold)),
Text(
'Role: $participantType'
/* '역할: $participantType' */,
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
],
// (C)
// (C) Introduction ()
const Align(
alignment: Alignment.centerLeft,
child: Text('소개', style: TextStyle(fontWeight: FontWeight.bold)),
child: Text(
'Introduction'
/* '소개' */,
style: TextStyle(fontWeight: FontWeight.bold),
),
),
const SizedBox(height: 4),
Container(
@ -236,57 +307,60 @@ class _UserInfoPrivateDialogState extends State<UserInfoPrivateDialog> {
child: SingleChildScrollView(
padding: const EdgeInsets.all(8),
child: Text(
introduceMyself.isNotEmpty ? introduceMyself : '소개글이 없습니다.',
introduceMyself.isNotEmpty
? introduceMyself
: 'No introduction.'
/* '소개글이 없습니다.' */,
style: const TextStyle(fontSize: 14),
softWrap: true,
maxLines: 100,
overflow: TextOverflow.clip,
maxLines: 100,
overflow: TextOverflow.clip,
),
),
),
const SizedBox(height: 16),
// (D)
// (D) Bottom buttons ( )
if (widget.isRoomMaster) ...[
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// (D-1)
SizedBox(
width: scaleFactor==0.8 ? 50 : 90,
child: ElevatedButton(
onPressed: _onUpdateUserInfo,
style: ElevatedButton.styleFrom(backgroundColor: Colors.black),
child: AutoSizeText(
scaleFactor==0.8 ? '수정' : '수정하기',
'Update'
/* '수정' */,
maxLines: 1,
style: TextStyle(color: Colors.white),
style: const TextStyle(color: Colors.white),
),
),
),
// (D-2)
SizedBox(
width: scaleFactor==0.8 ? 50 : 90,
child: ElevatedButton(
onPressed: _onKickParticipant,
style: ElevatedButton.styleFrom(backgroundColor: Colors.black),
child: AutoSizeText(
scaleFactor==0.8 ? '추방' : '추방하기',
'Kick'
/* '추방' */,
maxLines: 1,
style: TextStyle(color: Colors.white),
style: const TextStyle(color: Colors.white),
),
),
),
// (D-3)
SizedBox(
width: scaleFactor==0.8 ? 50 : 90,
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(backgroundColor: Colors.black),
child: AutoSizeText(
'확인',
'OK'
/* '확인' */,
maxLines: 1,
style: TextStyle(color: Colors.white),
style: const TextStyle(color: Colors.white),
),
),
),
@ -297,9 +371,10 @@ class _UserInfoPrivateDialogState extends State<UserInfoPrivateDialog> {
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(backgroundColor: Colors.black),
child: AutoSizeText(
'확인',
'OK'
/* '확인' */,
maxLines: 1,
style: TextStyle(color: Colors.white),
style: const TextStyle(color: Colors.white),
),
),
],

View File

@ -6,10 +6,15 @@ import 'package:auto_size_text/auto_size_text.dart';
class UserInfoTeamDialog extends StatefulWidget {
final Map<String, dynamic> userData;
final bool isRoomMaster; // "현재 로그인 유저"
final int roomSeq;
final String roomTypeName; // "TEAM"
final List<String> teamNameList;
/* 유저 데이터 */
final bool isRoomMaster;
/* 이 모달을 연 "현재 로그인 유저"가 방장인지 여부 */
final int roomSeq;
/* 방 번호 */
final String roomTypeName;
/* "TEAM" */
final List<String> teamNameList;
/* 팀명 목록 */
const UserInfoTeamDialog({
Key? key,
@ -25,12 +30,15 @@ class UserInfoTeamDialog extends StatefulWidget {
}
class _UserInfoTeamDialogState extends State<UserInfoTeamDialog> {
late String participantType; // 'ADMIN' / 'PLAYER'
late String teamName; // 'A'/'B'/'WAIT'
late String introduceMyself; //
late String participantType;
/* 'ADMIN' / 'PLAYER' */
late String teamName;
/* 'A'/'B'/'WAIT' */
late String introduceMyself;
/* 유저 소개 */
//
double scaleFactor = 1.0;
/* 화면 크기에 따라 폰트 크기 조절 */
double buttonScaleFactor = 1.0;
@override
@ -44,7 +52,8 @@ class _UserInfoTeamDialogState extends State<UserInfoTeamDialog> {
introduceMyself = widget.userData['introduce_myself'] ?? '';
// teamNameList에 WAIT ( )
// If WAIT is not in the teamNameList, add it
// /* teamNameList에 WAIT 없으면 추가 */
final hasWait = widget.teamNameList.map((e) => e.toUpperCase()).contains('WAIT');
if (!hasWait) {
widget.teamNameList.add('WAIT');
@ -57,7 +66,7 @@ class _UserInfoTeamDialogState extends State<UserInfoTeamDialog> {
_updateScaleFactor();
}
//
// Adjust font size by screen width ( )
void _updateScaleFactor() {
final screenWidth = MediaQuery.of(context).size.width;
const baseWidth = 450.0;
@ -67,10 +76,10 @@ class _UserInfoTeamDialogState extends State<UserInfoTeamDialog> {
});
}
// (1) / API ( )
/// (1) API to update role/team (/ API)
Future<void> _onUpdateUserInfo() async {
// -> return
if (!widget.isRoomMaster) return;
if (!widget.isRoomMaster) return;
// /* 방장이 아닌데 수정 시도 -> return */
final reqBody = {
'room_seq': '${widget.roomSeq}',
@ -88,30 +97,47 @@ class _UserInfoTeamDialogState extends State<UserInfoTeamDialog> {
if (response['result'] == 'OK') {
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
await showResponseDialog(context, '성공', '역할/팀이 성공적으로 수정되었습니다.');
await showResponseDialog(
context,
'Success'
/* '성공' */,
'Role/team updated successfully.'
/* '역할/팀이 성공적으로 수정되었습니다.' */
);
Navigator.pop(context, 'refresh');
} else {
final msgTitle = resp['response_info']?['msg_title'] ?? '오류';
final msgContent = resp['response_info']?['msg_content'] ?? '수정 실패';
final msgTitle = resp['response_info']?['msg_title'] ?? 'Error'
/* '오류' */;
final msgContent = resp['response_info']?['msg_content'] ?? 'Update failed'
/* '수정 실패' */;
showResponseDialog(context, msgTitle, msgContent);
}
} else {
showResponseDialog(context, '실패', '서버 통신 실패');
showResponseDialog(context, 'Failed'
/* '실패' */, 'Server communication failed.'
/* '서버 통신 실패' */);
}
} catch (e) {
showResponseDialog(context, '오류 발생', '$e');
showResponseDialog(context, 'Error occurred'
/* '오류 발생' */, '$e');
}
}
// (2) : "추방하기" API
/// (2) New: Kick participant API call ( API)
Future<void> _onKickParticipant() async {
//
if (!widget.isRoomMaster) return;
//
if (!widget.isRoomMaster) return;
// /* 방장이 아니면 리턴 */
final prefs = await SharedPreferences.getInstance();
final mySeq = prefs.getInt('my_user_seq')?.toString() ?? '0';
if (widget.userData['user_seq'] == mySeq) {
await showResponseDialog(context, '오류', '방장은 추방할 수 없습니다.');
await showResponseDialog(
context,
'Error'
/* '오류' */,
'You cannot kick yourself (the master).'
/* '방장은 추방할 수 없습니다.' */
);
return;
}
@ -128,27 +154,39 @@ class _UserInfoTeamDialogState extends State<UserInfoTeamDialog> {
if (response['result'] == 'OK') {
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
//
await showResponseDialog(context, '성공', '해당 유저가 강퇴되었습니다.');
Navigator.pop(context, 'refresh');
await showResponseDialog(
context,
'Success'
/* '성공' */,
'User has been kicked out.'
/* '해당 유저가 강퇴되었습니다.' */
);
Navigator.pop(context, 'refresh');
} else {
final msgTitle = resp['response_info']?['msg_title'] ?? '오류';
final msgContent = resp['response_info']?['msg_content'] ?? '강퇴 실패';
final msgTitle = resp['response_info']?['msg_title'] ?? 'Error'
/* '오류' */;
final msgContent = resp['response_info']?['msg_content'] ?? 'Kick-out failed'
/* '강퇴 실패' */;
showResponseDialog(context, msgTitle, msgContent);
}
} else {
showResponseDialog(context, '실패', '서버 통신 실패');
showResponseDialog(context, 'Failed'
/* '실패' */, 'Server communication failed.'
/* '서버 통신 실패' */);
}
} catch (e) {
showResponseDialog(context, '오류 발생', '$e');
showResponseDialog(context, 'Error occurred'
/* '오류 발생' */, '$e');
}
}
@override
Widget build(BuildContext context) {
final userName = widget.userData['nickname'] ?? '유저';
final userName = widget.userData['nickname'] ?? 'User'
/* '유저' */;
final profileImg = widget.userData['profile_img'] ?? '';
final department = widget.userData['department'] ?? '소속정보 없음';
final department = widget.userData['department'] ?? 'No Department Info'
/* '소속정보 없음' */;
return Dialog(
backgroundColor: Colors.white,
@ -159,16 +197,21 @@ class _UserInfoTeamDialogState extends State<UserInfoTeamDialog> {
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('유저 정보', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const Text(
'User Info'
/* '유저 정보' */,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
// (A)
// (A) Profile section ( )
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//
// Profile image ( )
Container(
width: 80, height: 80,
width: 80,
height: 80,
decoration: BoxDecoration(
border: Border.all(color: Colors.black),
borderRadius: BorderRadius.circular(16),
@ -179,21 +222,36 @@ class _UserInfoTeamDialogState extends State<UserInfoTeamDialog> {
? Image.network(
'https://eldsoft.com:8097/images$profileImg',
fit: BoxFit.cover,
errorBuilder: (ctx, err, st) =>
const Center(child: Text('이미지\n불가', textAlign: TextAlign.center)),
errorBuilder: (ctx, err, st) => const Center(
child: Text(
'Image\nError',
textAlign: TextAlign.center,
),
),
)
: const Center(child: Text('이미지\n없음', textAlign: TextAlign.center)),
: const Center(
child: Text(
'No\nImage',
textAlign: TextAlign.center,
),
),
),
),
const SizedBox(width: 16),
// +
// Nickname + Department ( + )
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(userName, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
Text(
userName,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 6),
Text(department, style: const TextStyle(fontSize: 14)),
Text(
department,
style: const TextStyle(fontSize: 14),
),
],
),
),
@ -201,17 +259,26 @@ class _UserInfoTeamDialogState extends State<UserInfoTeamDialog> {
),
const SizedBox(height: 16),
// (B) /
// (B) Only room master can update role/team ( / )
if (widget.isRoomMaster) ...[
//
// Role dropdown ( )
Row(
children: [
const Text('역할: ', style: TextStyle(fontWeight: FontWeight.bold)),
const Text(
'Role: ',
style: TextStyle(fontWeight: FontWeight.bold),
),
DropdownButton<String>(
value: participantType,
items: const [
DropdownMenuItem(value: 'ADMIN', child: Text('사회자')),
DropdownMenuItem(value: 'PLAYER', child: Text('참가자')),
DropdownMenuItem(
value: 'ADMIN',
child: Text('Moderator'),
),
DropdownMenuItem(
value: 'PLAYER',
child: Text('Participant'),
),
],
onChanged: (val) {
if (val == null) return;
@ -224,14 +291,19 @@ class _UserInfoTeamDialogState extends State<UserInfoTeamDialog> {
),
const SizedBox(height: 12),
//
// Team name dropdown ( )
Row(
children: [
const Text('팀명: ', style: TextStyle(fontWeight: FontWeight.bold)),
const Text(
'Team: ',
style: TextStyle(fontWeight: FontWeight.bold),
),
DropdownButton<String>(
value: widget.teamNameList
.map((e) => e.toUpperCase())
.contains(teamName) ? teamName : 'WAIT',
.contains(teamName)
? teamName
: 'WAIT',
items: widget.teamNameList.map((t) => DropdownMenuItem(
value: t.toUpperCase(),
child: Text(t),
@ -247,17 +319,29 @@ class _UserInfoTeamDialogState extends State<UserInfoTeamDialog> {
),
const SizedBox(height: 16),
] else ...[
// (B') 일반유저 -> 그냥 정보만 표시
Text('역할: $participantType', style: const TextStyle(fontWeight: FontWeight.bold)),
// (B') If not room master, just display info (일반유저 -> 정보만 표시)
Text(
'Role: $participantType'
/* '역할: $participantType' */,
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 6),
Text('팀명: $teamName', style: const TextStyle(fontWeight: FontWeight.bold)),
Text(
'Team: $teamName'
/* '팀명: $teamName' */,
style: const TextStyle(fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
],
// (C)
// (C) Introduction ()
const Align(
alignment: Alignment.centerLeft,
child: Text('소개', style: TextStyle(fontWeight: FontWeight.bold)),
child: Text(
'Introduction'
/* '소개' */,
style: TextStyle(fontWeight: FontWeight.bold),
),
),
const SizedBox(height: 4),
Container(
@ -270,72 +354,78 @@ class _UserInfoTeamDialogState extends State<UserInfoTeamDialog> {
child: SingleChildScrollView(
padding: const EdgeInsets.all(8),
child: Text(
introduceMyself.isNotEmpty ? introduceMyself : '소개글이 없습니다.',
introduceMyself.isNotEmpty
? introduceMyself
: 'No introduction.'
/* '소개글이 없습니다.' */,
style: const TextStyle(fontSize: 14),
softWrap: true, //
maxLines: 100, //
overflow: TextOverflow.clip, // clip
softWrap: true,
maxLines: 100,
overflow: TextOverflow.clip,
),
),
),
const SizedBox(height: 16),
// (D)
// (D) Bottom buttons ( )
if (widget.isRoomMaster) ...[
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
//
// "Update" ()
SizedBox(
width: scaleFactor==0.8 ? 75 : 90,
child: ElevatedButton(
onPressed: _onUpdateUserInfo,
style: ElevatedButton.styleFrom(backgroundColor: Colors.black),
// () FittedBox
child: AutoSizeText(
scaleFactor==0.8 ? '수정' : '수정하기',
'Update'
/* '수정' */,
maxLines: 1,
style: TextStyle(color: Colors.white),
style: const TextStyle(color: Colors.white),
),
),
),
//
// "Kick" ()
SizedBox(
width: scaleFactor==0.8 ? 75 : 90,
child: ElevatedButton(
onPressed: _onKickParticipant,
style: ElevatedButton.styleFrom(backgroundColor: Colors.black),
child: AutoSizeText(
scaleFactor==0.8 ? '추방' : '추방하기',
'Kick'
/* '추방' */,
maxLines: 1,
style: TextStyle(color: Colors.white),
style: const TextStyle(color: Colors.white),
),
),
),
//
// "OK" ()
SizedBox(
width: scaleFactor==0.8 ? 75 : 90,
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(backgroundColor: Colors.black),
child: AutoSizeText(
'확인',
'OK'
/* '확인' */,
maxLines: 1,
style: TextStyle(color: Colors.white),
style: const TextStyle(color: Colors.white),
),
),
),
],
),
] else ...[
// "확인"
// If not the master, only "OK" ( "확인")
ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(backgroundColor: Colors.black),
child: AutoSizeText(
'확인',
'OK'
/* '확인' */,
maxLines: 1,
style: TextStyle(color: Colors.white),
style: const TextStyle(color: Colors.white),
),
),
],

View File

@ -49,7 +49,7 @@ Future<bool?> showYesNoDialog({
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
),
child: const Text('아니오'),
child: const Text('No'),
),
TextButton(
onPressed: () => Navigator.of(ctx).pop(true), // YES
@ -58,7 +58,7 @@ Future<bool?> showYesNoDialog({
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
),
child: const Text(''),
child: const Text('Yes'),
),
],
);

View File

@ -1,15 +1,15 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; //
import 'package:flutter/services.dart'; // /* 숫자만 입력 위해 */
import 'package:http/http.dart' as http;
import 'dart:convert';
// API
// Server API ( API)
import '../plugins/api.dart';
//
// Main Page ( )
import '../views/room/main_page.dart';
//
// Alert modal dialog ( )
import '../dialogs/response_dialog.dart';
class SurveyPage extends StatefulWidget {
@ -25,35 +25,45 @@ class SurveyPage extends StatefulWidget {
}
class _SurveyPageState extends State<SurveyPage> {
// (0~4)
int _currentIndex = 0;
int _currentIndex = 0;
/* 현재 페이지 인덱스 (0~4) */
// (5 )
List<String> _questions = [];
/* 전체 질문 목록 (5개 예시) */
List<String> _questionsOriginal = [];
/* 질문의 한글 원본 목록 */
/// (5)
final List<String?> _answers = List.filled(5, null, growable: false);
/* 사용자가 입력한 답변(5개) */
//
final Map<int, String> _selectedRadioValue = {};
//
/* 각 페이지별 라디오 선택 값 */
final Map<int, TextEditingController> _textControllers = {};
/* 각 페이지별 텍스트필드 컨트롤러 */
@override
void initState() {
super.initState();
//
// Set questions ( )
_questions = [
"Q1. ${widget.nickname}님의 나이는 어떻게 되나요?",
"Q2. ${widget.nickname}님의 직업이 무엇인가요?",
"Q3. ${widget.nickname}님은 올스코어 앱을 어떻게 알게 됐나요?",
"Q4. ${widget.nickname}님은 올스코어 앱을 어디서 경험하셨나요?",
"Q5. 올스코어를 계속 사용할 의사가 있나요?",
"Q1. How old are you, ${widget.nickname}?",
/* "Q1. ${widget.nickname}님의 나이는 어떻게 되나요?" */
"Q2. What is your occupation, ${widget.nickname}?",
/* "Q2. ${widget.nickname}님의 직업이 무엇인가요?" */
"Q3. How did you hear about ALLSCORE, ${widget.nickname}?",
/* "Q3. ${widget.nickname}님은 올스코어 앱을 어떻게 알게 됐나요?" */
"Q4. Where have you experienced ALLSCORE, ${widget.nickname}?",
/* "Q4. ${widget.nickname}님은 올스코어 앱을 어디서 경험하셨나요?" */
"Q5. Do you plan to keep using ALLSCORE?",
/* "Q5. 올스코어를 계속 사용할 의사가 있나요?" */
];
// Original Korean questions ( )
_questionsOriginal = [
"나이는 어떻게 되나요?",
"직업이 무엇인가요?",
@ -62,7 +72,8 @@ class _SurveyPageState extends State<SurveyPage> {
"올스코어를 계속 사용할 의사가 있나요?",
];
// TextEditingController
// Initialize TextEditingController for each page
// ( TextEditingController )
for (int i = 0; i < _questions.length; i++) {
_textControllers[i] = TextEditingController();
}
@ -70,16 +81,15 @@ class _SurveyPageState extends State<SurveyPage> {
@override
void dispose() {
// TextController
// Dispose text controllers ( )
for (var ctrl in _textControllers.values) {
ctrl.dispose();
}
super.dispose();
}
/// ( )
/// Exit the survey ( )
Future<void> _onExitSurvey() async {
// "정말 나가시겠습니까?"
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (_) => const MainPage()),
@ -87,11 +97,12 @@ class _SurveyPageState extends State<SurveyPage> {
);
}
///
/// Next button ( )
void _onNext() {
if (!_validateCurrentPage()) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('값을 모두 입력해 주세요.')),
const SnackBar(content: Text('Please fill in all required fields.'
/* '값을 모두 입력해 주세요.' */)),
);
return;
}
@ -103,7 +114,7 @@ class _SurveyPageState extends State<SurveyPage> {
}
}
///
/// Previous button ( )
void _onPrev() {
if (_currentIndex > 0) {
setState(() {
@ -112,12 +123,12 @@ class _SurveyPageState extends State<SurveyPage> {
}
}
///
/// Submit ()
Future<void> _onSubmit() async {
//
if (!_validateCurrentPage()) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('값을 모두 입력해 주세요.')),
const SnackBar(content: Text('Please fill in all required fields.'
/* '값을 모두 입력해 주세요.' */)),
);
return;
}
@ -126,7 +137,9 @@ class _SurveyPageState extends State<SurveyPage> {
final List<String> qnaList = [];
for (int i = 0; i < _questionsOriginal.length; i++) {
qnaList.add(_questionsOriginal[i]);
/* 한글 질문 넣기 */
qnaList.add(_answers[i] ?? '');
/* 사용자 답변 */
}
final requestBody = {
@ -138,39 +151,48 @@ class _SurveyPageState extends State<SurveyPage> {
if (response['result'] == 'OK') {
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
//
// Survey submitted ( )
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(content: Text('설문이 제출되었습니다. 감사합니다!')),
const SnackBar(content: Text('Your survey has been submitted. Thank you!'
/* '설문이 제출되었습니다. 감사합니다!' */)),
);
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (_) => const MainPage()),
(route) => false,
);
Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (_) => const MainPage()), (route) => false);
} else {
showResponseDialog(context, '오류', '설문 제출 실패');
showResponseDialog(context, 'Error' /* 오류 */, 'Failed to submit the survey.'
/* '설문 제출 실패' */);
}
} else {
showResponseDialog(context, '오류', '설문 제출 실패');
showResponseDialog(context, 'Error' /* 오류 */, 'Failed to submit the survey.'
/* '설문 제출 실패' */);
}
} catch (e) {
showResponseDialog(context, '오류', '설문 제출 실패');
showResponseDialog(context, 'Error' /* 오류 */, 'Failed to submit the survey.'
/* '설문 제출 실패' */);
}
}
/// + _answers에
/// Validate current page input ( )
bool _validateCurrentPage() {
final index = _currentIndex;
String? answer;
switch (index) {
case 0:
// ()
// Age ()
final txt = _textControllers[index]?.text.trim() ?? '';
if (txt.isEmpty) {
return false;
}
answer = '$txt';
answer = '$txt yrs old'
/* '$txt 세' */;
break;
case 1:
//
// Occupation ()
final selected = _selectedRadioValue[index];
if (selected == null || selected.isEmpty) {
return false;
@ -179,24 +201,25 @@ class _SurveyPageState extends State<SurveyPage> {
break;
case 2:
//
// How did you hear about ALLSCORE? ( )
final selected2 = _selectedRadioValue[index];
if (selected2 == null || selected2.isEmpty) {
return false;
}
if (selected2 == '기타') {
if (selected2 == 'Others' /* '기타' */) {
final etc = _textControllers[index]?.text.trim() ?? '';
if (etc.isEmpty) {
return false;
}
answer = "기타($etc)";
answer = "Others($etc)"
/* "기타($etc)" */;
} else {
answer = selected2;
}
break;
case 3:
// ?
// Where have you experienced? ( ?)
final sel3 = _selectedRadioValue[index];
if (sel3 == null || sel3.isEmpty) {
return false;
@ -205,13 +228,14 @@ class _SurveyPageState extends State<SurveyPage> {
break;
case 4:
// ?
// Will you continue using ALLSCORE? ( ?)
final sel4 = _selectedRadioValue[index];
if (sel4 == null || sel4.isEmpty) {
return false;
}
final comment = _textControllers[index]?.text.trim() ?? '';
answer = sel4 + (comment.isNotEmpty ? " / 의견: $comment" : "");
answer = sel4 + (comment.isNotEmpty ? " / comment: $comment"
/* "/ 의견: $comment" */ : "");
break;
default:
@ -225,6 +249,7 @@ class _SurveyPageState extends State<SurveyPage> {
@override
Widget build(BuildContext context) {
final questionText = _questions[_currentIndex];
/* 현재 페이지 질문 */
final pageNumber = _currentIndex + 1;
final totalPage = _questions.length;
@ -235,16 +260,17 @@ class _SurveyPageState extends State<SurveyPage> {
},
child: Scaffold(
appBar: AppBar(
// '설문 그만하기'
leadingWidth: 120, //
leadingWidth: 120,
leading: TextButton(
onPressed: _onExitSurvey,
child: const Text(
'설문 그만하기',
'Stop Survey'
/* '설문 그만하기' */,
style: TextStyle(color: Colors.white),
),
),
title: Text('설문조사 ($pageNumber/$totalPage)'),
title: Text('Survey ($pageNumber/$totalPage)'
/* '설문조사 ($pageNumber/$totalPage)' */),
backgroundColor: Colors.black,
),
body: SingleChildScrollView(
@ -252,18 +278,17 @@ class _SurveyPageState extends State<SurveyPage> {
alignment: Alignment.center,
padding: const EdgeInsets.all(16),
child: Column(
//
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
//
// Question text ()
Text(
questionText,
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
textAlign: TextAlign.center,
),
const SizedBox(height: 24),
// UI
// Page-specific UI ( UI)
_buildSurveyPage(_currentIndex),
],
),
@ -279,7 +304,8 @@ class _SurveyPageState extends State<SurveyPage> {
child: ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.grey),
onPressed: _onPrev,
child: const Text('이전'),
child: const Text('Previous'
/* '이전' */),
),
),
if (_currentIndex > 0) const SizedBox(width: 8),
@ -288,7 +314,7 @@ class _SurveyPageState extends State<SurveyPage> {
style: ElevatedButton.styleFrom(backgroundColor: Colors.black),
onPressed: (_currentIndex < totalPage - 1) ? _onNext : _onSubmit,
child: Text(
(_currentIndex < totalPage - 1) ? '다음' : '제출하기',
(_currentIndex < totalPage - 1) ? 'Next' /* 다음 */ : 'Submit' /* 제출하기 */,
style: const TextStyle(color: Colors.white),
),
),
@ -300,23 +326,27 @@ class _SurveyPageState extends State<SurveyPage> {
);
}
/// UI
/// Build UI for each page ( UI)
Widget _buildSurveyPage(int index) {
switch (index) {
case 0:
// ( )
// Age input ( )
return Column(
children: [
const Text('(예: 나이를 숫자로 입력해 주세요.)',
textAlign: TextAlign.center),
const Text(
'(e.g. Please enter your age in digits.)'
/* '(예: 나이를 숫자로 입력해 주세요.)' */,
textAlign: TextAlign.center,
),
const SizedBox(height: 16),
TextField(
controller: _textControllers[index],
keyboardType: TextInputType.number,
inputFormatters: [FilteringTextInputFormatter.digitsOnly], //
textAlign: TextAlign.center, //
inputFormatters: [FilteringTextInputFormatter.digitsOnly],
textAlign: TextAlign.center,
decoration: const InputDecoration(
labelText: '나이',
labelText: 'Age'
/* '나이' */,
border: OutlineInputBorder(),
),
),
@ -324,8 +354,17 @@ class _SurveyPageState extends State<SurveyPage> {
);
case 1:
//
final jobs = ['학생', '회사원', '전문직', '교수/교사', '기술직', '공무원', '예술/스포츠', '기타'];
// Occupation ()
final jobs = [
'Student' /* '학생' */,
'Office Worker' /* '회사원' */,
'Professional' /* '전문직' */,
'Professor/Teacher' /* '교수/교사' */,
'Technical' /* '기술직' */,
'Government Official' /* '공무원' */,
'Art/Sports' /* '예술/스포츠' */,
'Others' /* '기타' */,
];
return Column(
mainAxisSize: MainAxisSize.min,
children: jobs.map((job) {
@ -343,8 +382,14 @@ class _SurveyPageState extends State<SurveyPage> {
);
case 2:
//
final paths = ['친구/지인 추천', '소셜 미디어', '블로그/온라인 리뷰', '학교나 직장', '기타'];
// How did you hear about ALLSCORE? ( )
final paths = [
'Friend/Acquaintance' /* '친구/지인 추천' */,
'Social Media' /* '소셜 미디어' */,
'Blog/Online Review' /* '블로그/온라인 리뷰' */,
'School/Workplace' /* '학교나 직장' */,
'Others' /* '기타' */,
];
return Column(
mainAxisSize: MainAxisSize.min,
children: [
@ -360,14 +405,16 @@ class _SurveyPageState extends State<SurveyPage> {
},
);
}).toList(),
if (_selectedRadioValue[index] == '기타')
if (_selectedRadioValue[index] == 'Others'
/* '기타' */)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: TextField(
controller: _textControllers[index],
textAlign: TextAlign.center,
decoration: const InputDecoration(
labelText: '기타 내용을 입력해 주세요.',
labelText: 'Please specify.'
/* '기타 내용을 입력해 주세요.' */,
border: OutlineInputBorder(),
),
),
@ -376,8 +423,16 @@ class _SurveyPageState extends State<SurveyPage> {
);
case 3:
// ?
final places = ['가족과 함께', '친구들과 모임', '학교 교육 목적', '직장 동호회', '카페나 공공장소', '여행 중', '기타'];
// Where have you experienced ALLSCORE? ( ?)
final places = [
'With Family' /* '가족과 함께' */,
'With Friends' /* '친구들과 모임' */,
'School (for education)' /* '학교 교육 목적' */,
'Work Club' /* '직장 동호회' */,
'Cafe or Public Space' /* '카페나 공공장소' */,
'Travel' /* '여행 중' */,
'Others' /* '기타' */,
];
return Column(
mainAxisSize: MainAxisSize.min,
children: places.map((pl) {
@ -395,13 +450,13 @@ class _SurveyPageState extends State<SurveyPage> {
);
case 4:
// ?
// Will you continue using ALLSCORE? ( ?)
return Column(
mainAxisSize: MainAxisSize.min,
children: [
RadioListTile<String>(
title: const Text('', textAlign: TextAlign.center),
value: '',
title: const Text('Yes' /* '네' */, textAlign: TextAlign.center),
value: 'Yes' /* '네' */,
groupValue: _selectedRadioValue[index],
onChanged: (val) {
setState(() {
@ -410,8 +465,8 @@ class _SurveyPageState extends State<SurveyPage> {
},
),
RadioListTile<String>(
title: const Text('아니오', textAlign: TextAlign.center),
value: '아니오',
title: const Text('No' /* '아니오' */, textAlign: TextAlign.center),
value: 'No' /* '아니오' */,
groupValue: _selectedRadioValue[index],
onChanged: (val) {
setState(() {
@ -420,14 +475,19 @@ class _SurveyPageState extends State<SurveyPage> {
},
),
const SizedBox(height: 16),
const Text('추가 의견이 있다면 자유롭게 작성해 주세요.', textAlign: TextAlign.center),
const Text(
'If you have any additional comments, feel free to write them here.'
/* '추가 의견이 있다면 자유롭게 작성해 주세요.' */,
textAlign: TextAlign.center,
),
const SizedBox(height: 8),
TextField(
controller: _textControllers[index],
maxLines: 3,
textAlign: TextAlign.center,
decoration: const InputDecoration(
hintText: 'ex) 불편사항, 개선 아이디어 등',
hintText: 'e.g. inconveniences, improvement ideas, etc.'
/* 'ex) 불편사항, 개선 아이디어 등' */,
border: OutlineInputBorder(),
),
),
@ -435,7 +495,11 @@ class _SurveyPageState extends State<SurveyPage> {
);
default:
return const Text('설문 문항 오류', textAlign: TextAlign.center);
return const Text(
'Survey question error'
/* '설문 문항 오류' */,
textAlign: TextAlign.center
);
}
}
}

View File

@ -25,40 +25,67 @@ class _InquiryToManagerPageState extends State<InquiryToManagerPage> {
final title = _titleController.text.trim();
final contents = _contentsController.text.trim();
// :
// If title or contents are empty, show message
// /* 예시: 제목이나 내용이 비어있으면 막기 */
if (title.isEmpty || contents.isEmpty) {
showResponseDialog(context, '안내', '제목과 내용을 입력해주세요.');
showResponseDialog(
context,
'Notice' /* 안내 */,
'Please enter both title and content.'
/* "제목과 내용을 입력해주세요." */
);
return;
}
//
// Submit to server ( )
try {
final requestBody = {
'title': title,
'contents': contents,
};
final serverResponse =
await Api.serverRequest(uri: '/inquiry/request', body: requestBody);
final serverResponse = await Api.serverRequest(uri: '/inquiry/request', body: requestBody);
if (serverResponse == null) {
showResponseDialog(context, '문의 전송 실패', '서버 응답이 없습니다.');
showResponseDialog(
context,
'Inquiry Failed' /* 문의 전송 실패 */,
'No response from server.' /* 서버 응답이 없습니다. */
);
}
if (serverResponse['result'] == 'OK') {
final serverResponse1 = serverResponse['response'];
if (serverResponse1['result'] == 'OK') {
await showResponseDialog(context, '문의 전송 완료', '문의가 전송되었습니다.');
await showResponseDialog(
context,
'Inquiry Sent' /* 문의 전송 완료 */,
'Your inquiry has been successfully sent.'
/* 문의가 전송되었습니다. */
);
Navigator.of(context).pushAndRemoveUntil(
MaterialPageRoute(builder: (context) => const MainPage()),
(route) => false,
);
} else {
showResponseDialog(context, '문의 전송 실패', '문의 전송에 실패했습니다.');
showResponseDialog(
context,
'Inquiry Failed' /* 문의 전송 실패 */,
'Failed to send inquiry.' /* 문의 전송에 실패했습니다. */
);
}
} else {
showResponseDialog(context, '문의 전송 실패', '문의 전송에 실패했습니다.');
showResponseDialog(
context,
'Inquiry Failed' /* 문의 전송 실패 */,
'Failed to send inquiry.' /* 문의 전송에 실패했습니다. */
);
}
} catch (e) {
showResponseDialog(context, '문의 전송 실패', '문의 전송에 실패했습니다.');
showResponseDialog(
context,
'Inquiry Failed' /* 문의 전송 실패 */,
'Failed to send inquiry.' /* 문의 전송에 실패했습니다. */
);
}
}
@ -68,7 +95,11 @@ class _InquiryToManagerPageState extends State<InquiryToManagerPage> {
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.black,
title: const Text('문의하기', style: TextStyle(color: Colors.white)),
title: const Text(
'Inquiry'
/* 문의하기 */,
style: TextStyle(color: Colors.white),
),
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
onPressed: () => Navigator.pop(context),
@ -80,35 +111,38 @@ class _InquiryToManagerPageState extends State<InquiryToManagerPage> {
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
//
// Inquiry Title input ( )
TextField(
controller: _titleController,
maxLength: 100,
decoration: const InputDecoration(
labelText: '문의 제목',
labelText: 'Inquiry Title'
/* 문의 제목 */,
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
// ( )
// Inquiry Content input ( )
TextField(
controller: _contentsController,
maxLength: 1000,
maxLines: 10, //
maxLines: 10,
decoration: const InputDecoration(
labelText: '문의 내용',
labelText: 'Inquiry Content'
/* 문의 내용 */,
border: OutlineInputBorder(),
),
),
const SizedBox(height: 16),
//
// Submit button ()
SizedBox(
width: double.infinity,
child: ElevatedButton(
style: ElevatedButton.styleFrom(backgroundColor: Colors.black),
onPressed: _handleSubmit,
child: const Text(
'제출하기',
'Submit'
/* 제출하기 */,
style: TextStyle(color: Colors.white),
),
),

View File

@ -6,10 +6,10 @@ import 'login_page.dart';
import 'pw_finding_page.dart';
import 'signup_page.dart';
//
/* 모바일 광고 */
import '../../plugins/admob.dart';
//
/* 설정 */
import '../../config/config.dart';
class IdFindingPage extends StatefulWidget {
@ -22,73 +22,98 @@ class IdFindingPage extends StatefulWidget {
class _IdFindingPageState extends State<IdFindingPage> {
final TextEditingController nicknameController = TextEditingController();
final TextEditingController emailController = TextEditingController();
String nicknameErrorMessage = '';
/* 닉네임 오류 메시지 */
String emailErrorMessage = '';
/* 이메일 오류 메시지 */
String foundIdMessage = '';
/* 찾은 ID 안내 메시지 */
String authId = '';
Future<void> _findId(String nickname, String email) async {
/* ID 찾기 요청 처리 */
//
// Show loading indicator ( )
showDialog(
context: context,
barrierDismissible: false, //
barrierDismissible: false,
builder: (BuildContext context) {
return const Center(child: CircularProgressIndicator());
},
);
try {
final response = await http.post(
Uri.parse('https://eldsoft.com:8097/user/find/id'),
headers: {
'Content-Type': 'application/json',
},
body: jsonEncode({
'nickname': nickname,
'user_email': email,
}),
).timeout(const Duration(seconds: 10)); // 10
final response = await http
.post(
Uri.parse('https://eldsoft.com:8097/user/find/id'),
headers: {
'Content-Type': 'application/json',
},
body: jsonEncode({
'nickname': nickname,
'user_email': email,
}),
)
.timeout(const Duration(seconds: 10)); // 10
String responseBody = utf8.decode(response.bodyBytes);
Navigator.of(context).pop(); //
Navigator.of(context).pop(); // Close loading indicator ( )
if (response.statusCode == 200) {
final Map<String, dynamic> jsonResponse = jsonDecode(responseBody);
//
// Reset messages ( , ID )
setState(() {
nicknameErrorMessage = '';
emailErrorMessage = '';
foundIdMessage = ''; // ID
foundIdMessage = '';
});
if (jsonResponse['response_info']['msg_title'] == '닉네임 확인') {
if (jsonResponse['response_info']['msg_title'] == '닉네임 확인') /* "닉네임 확인" */ {
setState(() {
nicknameErrorMessage = '닉네임을 다시 확인해주세요'; //
nicknameErrorMessage = 'Please check your nickname again.'
/* 닉네임을 다시 확인해주세요 */;
});
} else if (jsonResponse['response_info']['msg_title'] == '이메일 확인') {
} else if (jsonResponse['response_info']['msg_title'] == '이메일 확인') /* "이메일 확인" */ {
setState(() {
emailErrorMessage = '이메일을 다시 확인해주세요'; //
emailErrorMessage = 'Please check your email again.'
/* 이메일을 다시 확인해주세요 */;
});
} else if (jsonResponse['result'] == 'OK') {
// ID
/* ID 찾기 성공 시 */
setState(() {
foundIdMessage = '당신의 ID는 ${jsonResponse['data']['user_id']} 입니다'; // ID
authId = jsonResponse['data']['auth']; // auth_id
foundIdMessage = 'Your ID is ${jsonResponse['data']['user_id']}.'
/* 당신의 ID는 ${jsonResponse['data']['user_id']} 입니다 */;
authId = jsonResponse['data']['auth'];
});
} else {
_showErrorDialog(jsonResponse['response_info']['msg_title'], jsonResponse['response_info']['msg_content'], 'STAY');
_showErrorDialog(
jsonResponse['response_info']['msg_title'],
jsonResponse['response_info']['msg_content'],
'STAY',
);
}
} else {
//
_showErrorDialog('오류', '요청이 실패했습니다. 관리자에게 문의해주세요.', 'STAY');
//
_showErrorDialog(
'Error'
/* 오류 */,
'Request failed. Please contact the administrator.'
/* 요청이 실패했습니다. 관리자에게 문의해주세요. */,
'STAY',
);
}
} catch (e) {
Navigator.of(context).pop(); //
_showErrorDialog('오류', '요청이 실패했습니다. 관리자에게 문의해주세요.', 'STAY');
Navigator.of(context).pop();
_showErrorDialog(
'Error'
/* 오류 */,
'Request failed. Please contact the administrator.'
/* 요청이 실패했습니다. 관리자에게 문의해주세요. */,
'STAY',
);
}
}
@ -103,49 +128,67 @@ class _IdFindingPageState extends State<IdFindingPage> {
}
Future<void> _findAllId() async {
// ID
/* ID 전체 찾기 요청 처리 */
//
// Show loading indicator ( )
showDialog(
context: context,
barrierDismissible: false, //
barrierDismissible: false,
builder: (BuildContext context) {
return const Center(child: CircularProgressIndicator());
},
);
try {
final response = await http.post(
Uri.parse('https://eldsoft.com:8097/user/find/id/full'),
headers: {
'Content-Type': 'application/json',
},
body: jsonEncode({
'auth': authId, // authId
}),
).timeout(const Duration(seconds: 10)); // 10
final response = await http
.post(
Uri.parse('https://eldsoft.com:8097/user/find/id/full'),
headers: {
'Content-Type': 'application/json',
},
body: jsonEncode({'auth': authId}),
)
.timeout(const Duration(seconds: 10)); // 10
String responseBody = utf8.decode(response.bodyBytes); // UTF-8
String responseBody = utf8.decode(response.bodyBytes);
Navigator.of(context).pop(); //
Navigator.of(context).pop(); // Close loading indicator ( )
if (response.statusCode == 200) {
final Map<String, dynamic> jsonResponse = jsonDecode(responseBody);
if (jsonResponse['result'] == 'OK') {
//
_showSuccessDialog('이메일로 전체 ID를 발송했습니다.');
//
_showSuccessDialog(
'We have sent all IDs to your email.'
/* 이메일로 전체 ID를 발송했습니다. */,
);
} else {
//
_showErrorDialog(jsonResponse['response_info']['msg_title'], jsonResponse['response_info']['msg_content'], 'STAY');
_showErrorDialog(
jsonResponse['response_info']['msg_title'],
jsonResponse['response_info']['msg_content'],
'STAY',
);
}
} else {
//
_showErrorDialog('오류', '요청이 실패했습니다. 관리자에게 문의해주세요.', 'STAY');
//
_showErrorDialog(
'Error'
/* 오류 */,
'Request failed. Please contact the administrator.'
/* 요청이 실패했습니다. 관리자에게 문의해주세요. */,
'STAY',
);
}
} catch (e) {
Navigator.of(context).pop(); //
_showErrorDialog('오류', '요청이 실패했습니다. 관리자에게 문의해주세요.', 'STAY');
Navigator.of(context).pop();
_showErrorDialog(
'Error'
/* 오류 */,
'Request failed. Please contact the administrator.'
/* 요청이 실패했습니다. 관리자에게 문의해주세요. */,
'STAY',
);
}
}
@ -154,7 +197,7 @@ class _IdFindingPageState extends State<IdFindingPage> {
context: context,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor: Colors.white, //
backgroundColor: Colors.white,
title: Text(title, style: const TextStyle(color: Colors.black)),
content: Text(content, style: const TextStyle(color: Colors.black)),
actions: <Widget>[
@ -162,13 +205,15 @@ class _IdFindingPageState extends State<IdFindingPage> {
child: TextButton(
style: TextButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white, //
foregroundColor: Colors.white,
),
child: const Text('확인'),
child: const Text('OK'
/* 확인 */),
onPressed: () {
Navigator.of(context).pop(); //
Navigator.of(context).pop();
if (action == 'LOGIN') {
Navigator.of(context).pop(); //
Navigator.of(context).pop();
/* 로그인 페이지로 이동 */
}
},
),
@ -184,14 +229,17 @@ class _IdFindingPageState extends State<IdFindingPage> {
context: context,
builder: (BuildContext context) {
return AlertDialog(
title: const Text('성공'),
title: const Text('Success'
/* 성공 */),
content: Text(message),
actions: <Widget>[
TextButton(
child: const Text('확인'),
child: const Text('OK'
/* 확인 */),
onPressed: () {
Navigator.of(context).pop(); //
Navigator.of(context).pop(); //
Navigator.of(context).pop();
Navigator.of(context).pop();
/* 로그인 페이지로 이동 */
},
),
],
@ -200,11 +248,14 @@ class _IdFindingPageState extends State<IdFindingPage> {
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text('ALL SCORE', style: TextStyle(color: Colors.white)),
title: const Text('ALL SCORE'
/* ALL SCORE */,
style: TextStyle(color: Colors.white)),
backgroundColor: Colors.black,
),
body: Padding(
@ -216,7 +267,8 @@ class _IdFindingPageState extends State<IdFindingPage> {
children: [
if (foundIdMessage.isEmpty) ...[
const Text(
'ID 찾기',
'Find ID'
/* ID 찾기 */,
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
@ -227,15 +279,16 @@ class _IdFindingPageState extends State<IdFindingPage> {
TextField(
controller: nicknameController,
decoration: InputDecoration(
labelText: '닉네임',
labelText: 'Nickname'
/* 닉네임 */,
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),
),
),
),
if (nicknameErrorMessage.isNotEmpty) //
if (nicknameErrorMessage.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
@ -247,15 +300,16 @@ class _IdFindingPageState extends State<IdFindingPage> {
TextField(
controller: emailController,
decoration: InputDecoration(
labelText: '이메일',
labelText: 'Email'
/* 이메일 */,
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),
),
),
),
if (emailErrorMessage.isNotEmpty) //
if (emailErrorMessage.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
@ -272,10 +326,11 @@ class _IdFindingPageState extends State<IdFindingPage> {
backgroundColor: Colors.black,
foregroundColor: Colors.white,
),
child: const Text('ID 찾기'),
child: const Text('Find ID'
/* ID 찾기 */),
),
] else ...[
// ID
// ID
Text(
foundIdMessage,
style: const TextStyle(fontSize: 20, color: Colors.black),
@ -283,21 +338,26 @@ class _IdFindingPageState extends State<IdFindingPage> {
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
_findAllId(); // ID
_findAllId();
/* ID 전체 찾기 버튼 */
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
),
child: const Text('ID 전체 찾기'),
child: const Text('Find All IDs'
/* ID 전체 찾기 */),
),
],
const SizedBox(height: 16),
TextButton(
onPressed: () {
Navigator.pop(context);
/* 로그인 페이지로 이동 */
},
child: const Text('로그인', style: TextStyle(color: Colors.black)),
child: const Text('Login'
/* 로그인 */,
style: TextStyle(color: Colors.black)),
),
TextButton(
onPressed: () {
@ -306,7 +366,9 @@ class _IdFindingPageState extends State<IdFindingPage> {
MaterialPageRoute(builder: (context) => const PwFindingPage()),
);
},
child: const Text('PW 찾기', style: TextStyle(color: Colors.black)),
child: const Text('Find PW'
/* PW 찾기 */,
style: TextStyle(color: Colors.black)),
),
TextButton(
onPressed: () {
@ -315,16 +377,17 @@ class _IdFindingPageState extends State<IdFindingPage> {
MaterialPageRoute(builder: (context) => const SignUpPage()),
);
},
child: const Text('회원가입', style: TextStyle(color: Colors.black)),
child: const Text('Sign Up'
/* 회원가입 */,
style: TextStyle(color: Colors.black)),
),
],
),
),
),
// (3)
bottomNavigationBar: AdBannerWidget(),
bottomNavigationBar: AdBannerWidget()
/* (3) 하단 광고 영역 */,
);
}
}
}

View File

@ -1,35 +1,35 @@
import 'package:flutter/material.dart';
// import 'package:http/http.dart' as http; . Api.serverRequest()
// import 'package:http/http.dart' as http; // /* 사용안함. Api.serverRequest() 사용 */
import 'dart:convert' show utf8, jsonEncode;
import 'package:crypto/crypto.dart';
import 'package:shared_preferences/shared_preferences.dart';
// Api
/* 우리의 Api 모듈 */
import '../../plugins/api.dart';
//
/* 안내 모달창 */
import '../../dialogs/response_dialog.dart';
// () ID/PW ,
import 'id_finding_page.dart';
import 'pw_finding_page.dart';
import 'signup_page.dart';
/* (기존) ID/PW 찾기, 회원가입 페이지 */
import 'id_finding_page.dart'; /* ID 찾기 페이지 임포트 */
import 'pw_finding_page.dart'; /* PW 찾기 페이지 임포트 */
import 'signup_page.dart'; /* 회원가입 페이지 임포트 */
//
/* 구글 로그인 */
import 'package:google_sign_in/google_sign_in.dart';
import 'package:firebase_auth/firebase_auth.dart';
//
/* 광고 */
import 'package:google_mobile_ads/google_mobile_ads.dart';
//
/* 메인 페이지 */
import '../room/main_page.dart';
//
/* 설정 */
import '../../config/config.dart';
//
import 'package:fluttertoast/fluttertoast.dart'; // Toast
/* 뒤로가기 */
import 'package:fluttertoast/fluttertoast.dart'; // /* 뒤로가기 안내 문구에 Toast 등 사용 */
class LoginPage extends StatefulWidget {
const LoginPage({Key? key}) : super(key: key);
@ -40,53 +40,47 @@ class LoginPage extends StatefulWidget {
class _LoginPageState extends State<LoginPage> {
//
// (A) ID/PW
// (A) ID/PW (/)
//
final TextEditingController idController = TextEditingController();
final TextEditingController passwordController = TextEditingController();
bool autoLogin = false;
String loginErrorMessage = ''; //
bool autoLogin = false; // /* 자동로그인 체크박스 */
String loginErrorMessage = ''; // /* 로그인 실패 시 안내 */
//
// (B)
//
final GoogleSignIn _googleSignIn = GoogleSignIn(
scopes: <String>['email'],
);
final GoogleSignIn _googleSignIn = GoogleSignIn(scopes: <String>['email']);
//
// (C)
//
BannerAd? _bannerAd;
bool _isBannerReady = false;
String adUnitId = Config.adUnitId;
String adUnitId = Config.adUnitId; // /* 실제/테스트 배너 광고 단위 ID */
//
// (2 )
DateTime? _lastPressedTime;
//
bool _isLoading = false;
// : 2
static const _exitDuration = Duration(seconds: 2);
//
bool _isLoading = false;
Future<bool> _onWillPop() async {
final now = DateTime.now();
if (_lastPressedTime == null ||
now.difference(_lastPressedTime!) > _exitDuration) {
// or
if (_lastPressedTime == null || now.difference(_lastPressedTime!) > _exitDuration) {
_lastPressedTime = now;
// (Toast )
/* 안내 문구 띄우기 (Toast) */
Fluttertoast.showToast(
msg: '한 번 더 누르면 앱이 종료됩니다.',
msg: 'Press again to exit the app.'
/* '한 번 더 누르면 앱이 종료됩니다.' */,
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
);
return false; // pop
return false;
}
// 2
return true; // pop (Scaffold , )
// 2
return true;
}
@override
@ -97,7 +91,6 @@ class _LoginPageState extends State<LoginPage> {
void _initBannerAd() {
_bannerAd = BannerAd(
// / ID
adUnitId: adUnitId,
size: AdSize.banner,
request: const AdRequest(),
@ -127,7 +120,7 @@ class _LoginPageState extends State<LoginPage> {
final id = idController.text.trim();
final pw = passwordController.text.trim();
// PW SHA-256
// SHA-256
final bytes = utf8.encode(pw);
final digest = sha256.convert(bytes);
final hashedPw = digest.toString();
@ -142,33 +135,33 @@ class _LoginPageState extends State<LoginPage> {
final response = await Api.serverRequest(uri: '/user/login', body: requestBody);
if (response['result'] == 'OK') {
//
/* 내부 응답 */
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
//
// (a) google_user_yn = N
/* 로그인 성공 */
final prefs = await SharedPreferences.getInstance();
await prefs.setString('oauth_type', 'idpw');
await prefs.setString('oauth_type', 'idpw'); // /* google_user_yn = N 대신 */
await prefs.setBool('auto_login', true);
await prefs.setString('jwt_token', resp['auth']['token'].toString());
await prefs.setInt('my_user_seq', resp['auth']['user_seq']);
//
if (!mounted) return;
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const MainPage()),
);
} else {
//
showResponseDialog(context, resp['response_info']['msg_title'], resp['response_info']['msg_content']);
}
} else {
// FAIL
showResponseDialog(context, '오류', '로그인 요청 실패');
// FAIL
showResponseDialog(context, 'Error' /* 오류 */, 'Login request failed.'
/* 로그인 요청 실패 */);
}
} catch (e) {
showResponseDialog(context, '오류', '로그인 요청 중 예외 발생.\n$e');
showResponseDialog(context, 'Error' /* 오류 */, 'Exception occurred during login request.\n$e'
/* 로그인 요청 중 예외 발생.\n$e */);
} finally {
setState(() => _isLoading = false);
}
@ -183,26 +176,25 @@ class _LoginPageState extends State<LoginPage> {
// 1)
final GoogleSignInAccount? googleUser = await _googleSignIn.signIn();
if (googleUser == null) {
//
//
return;
}
// 2)
// 2)
final GoogleSignInAuthentication googleAuth = await googleUser.authentication;
// 3) FirebaseAuth Credential
// 3) FirebaseAuth Credential
final AuthCredential credential = GoogleAuthProvider.credential(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
// 4) FirebaseAuth로
final UserCredential userCredential =
await FirebaseAuth.instance.signInWithCredential(credential);
final UserCredential userCredential = await FirebaseAuth.instance.signInWithCredential(credential);
final User? user = userCredential.user;
if (user == null) {
showResponseDialog(context, '오류', '구글 로그인 오류. 관리자에게 문의해주세요.');
showResponseDialog(context, 'Error' /* 오류 */, 'Google login error. Please contact the administrator.'
/* 구글 로그인 오류. 관리자에게 문의해주세요. */);
return;
}
@ -218,31 +210,31 @@ class _LoginPageState extends State<LoginPage> {
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
final prefs = await SharedPreferences.getInstance();
await prefs.setString('oauth_type', 'google');
await prefs.setString('oauth_type', 'google'); // /* google_user_yn = Y 대체 */
await prefs.setBool('auto_login', true);
await prefs.setString('jwt_token', resp['auth']['token'].toString());
await prefs.setInt('my_user_seq', resp['auth']['user_seq']);
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const MainPage()),
);
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
} else {
showResponseDialog(context, resp['response_info']['msg_title'], resp['response_info']['msg_content']);
}
} else {
showResponseDialog(context, '오류', '구글 로그인 요청 실패');
showResponseDialog(context, 'Error' /* 오류 */, 'Google login request failed.'
/* 구글 로그인 요청 실패 */);
}
// () SharedPreferences에 google_user_yn = 'Y'
// (optional) SharedPreferences에 google_user_yn = 'Y'
final prefs = await SharedPreferences.getInstance();
await prefs.setString('google_user_yn', 'Y');
} catch (e) {
_showAlert('오류', '구글 로그인 중 오류가 발생했습니다.\n$e');
_showAlert('Error' /* 오류 */, 'An error occurred during Google login.\n$e'
/* 구글 로그인 중 오류가 발생했습니다.\n$e */);
} finally {
setState(() => _isLoading = false);
}
}
//
// (D3)
//
@ -253,64 +245,63 @@ class _LoginPageState extends State<LoginPage> {
}
try {
// (2)
final GoogleSignInAccount? googleUser = await _googleSignIn.signIn();
if (googleUser == null) {
//
return;
}
// (3)
final GoogleSignInAuthentication googleAuth = await googleUser.authentication;
// (4) FirebaseAuth Credential
final AuthCredential credential = GoogleAuthProvider.credential(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
// (5) FirebaseAuth로 ( )
// "회원가입" , Firebase signInWithCredential()
final UserCredential userCredential =
await FirebaseAuth.instance.signInWithCredential(credential);
final User? user = userCredential.user;
if (user == null) {
showResponseDialog(context, '오류', '구글계정 인증에 실패했습니다.');
return;
}
// (6) idToken ,
final idToken = await user.getIdToken();
final requestBody = {
'id_token': idToken,
};
// '/user/google/signup' API ()
final response = await Api.serverRequest(uri: '/user/google/signup', body: requestBody);
if (response['result'] == 'OK') {
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
//
showResponseDialog(context, '회원가입 완료', '구글 회원가입이 완료되었습니다.');
} else {
// OK가 ,
final msgTitle = resp['response_info']?['msg_title'] ?? '오류';
final msgContent = resp['response_info']?['msg_content'] ?? '회원가입에 실패했습니다.';
showResponseDialog(context, msgTitle, msgContent);
// (2)
final GoogleSignInAccount? googleUser = await _googleSignIn.signIn();
if (googleUser == null) {
//
return;
}
} else {
// FAIL ( result != OK)
showResponseDialog(context, '오류', '구글 회원가입 요청 실패');
}
// (3)
final GoogleSignInAuthentication googleAuth = await googleUser.authentication;
// (4) FirebaseAuth Credential
final AuthCredential credential = GoogleAuthProvider.credential(
accessToken: googleAuth.accessToken,
idToken: googleAuth.idToken,
);
// (5) FirebaseAuth로 "회원가입"
final UserCredential userCredential = await FirebaseAuth.instance.signInWithCredential(credential);
final User? user = userCredential.user;
if (user == null) {
showResponseDialog(context, 'Error' /* 오류 */, 'Google account authentication failed.'
/* 구글계정 인증에 실패했습니다. */);
return;
}
// (6) idToken ,
final idToken = await user.getIdToken();
final requestBody = {
'id_token': idToken,
};
final response = await Api.serverRequest(uri: '/user/google/signup', body: requestBody);
if (response['result'] == 'OK') {
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
//
showResponseDialog(context, 'Sign-up Complete' /* 회원가입 완료 */, 'Google sign-up has been completed.'
/* 구글 회원가입이 완료되었습니다. */);
} else {
//
final msgTitle = resp['response_info']?['msg_title'] ?? 'Error' /* 오류 */;
final msgContent = resp['response_info']?['msg_content'] ?? 'Failed to sign up.'
/* 회원가입에 실패했습니다. */;
showResponseDialog(context, msgTitle, msgContent);
}
} else {
showResponseDialog(context, 'Error' /* 오류 */, 'Google sign-up request failed.'
/* 구글 회원가입 요청 실패 */);
}
} catch (e) {
showResponseDialog(context, '오류', '구글 회원가입 중 오류가 발생했습니다.\n$e');
showResponseDialog(context, 'Error' /* 오류 */, 'An error occurred during Google sign-up.\n$e'
/* 구글 회원가입 중 오류가 발생했습니다.\n$e */);
}
}
//
// (E)
// (E) ( )
//
Future<bool?> _showTermsModal() async {
return showDialog<bool>(
@ -320,7 +311,8 @@ class _LoginPageState extends State<LoginPage> {
return AlertDialog(
backgroundColor: Colors.white,
title: const Text(
'개인정보 수집 및 이용 동의서',
'Privacy Collection and Usage Agreement'
/* 개인정보 수집 및 이용 동의서 */,
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
content: SingleChildScrollView(
@ -328,56 +320,8 @@ class _LoginPageState extends State<LoginPage> {
crossAxisAlignment: CrossAxisAlignment.start,
children: const [
Text(
'''올스코어(이하 "회사"라 합니다)는 이용자의 개인정보를 중요시하며, 「개인정보 보호법」 등 관련 법령을 준수하고 있습니다. 회사는 개인정보 수집 및 이용에 관한 사항을 아래와 같이 안내드리오니, 내용을 충분히 숙지하신 후 동의하여 주시기 바랍니다.
1.
: (ID), (PW), ( ),
: ,
2.
3.
: .
: .
: 5
: 5
: 3
4.
.
:
:
5.
, , , .
, "회원 탈퇴" .
6.
.
.
7.
: eldyeojh@gmail.com
8.
, .
''',
style: TextStyle(fontSize: 14),
Config.termsOfService,
style: const TextStyle(fontSize: 14),
),
],
),
@ -389,7 +333,8 @@ class _LoginPageState extends State<LoginPage> {
foregroundColor: Colors.white,
),
onPressed: () => Navigator.pop(ctx, false),
child: Text('거부'),
child: Text('Disagree'
/* 거부 */),
),
TextButton(
style: TextButton.styleFrom(
@ -397,7 +342,8 @@ class _LoginPageState extends State<LoginPage> {
foregroundColor: Colors.white,
),
onPressed: () => Navigator.pop(ctx, true),
child: Text('동의'),
child: Text('Agree'
/* 동의 */),
),
],
);
@ -406,7 +352,7 @@ class _LoginPageState extends State<LoginPage> {
}
//
// (F) Alert
// (F) Alert ( )
//
void _showAlert(String title, String message) {
showDialog(
@ -422,259 +368,281 @@ class _LoginPageState extends State<LoginPage> {
foregroundColor: Colors.white,
),
onPressed: () => Navigator.pop(context),
child: const Text('확인'),
child: const Text('OK' /* 확인 */),
),
],
),
);
}
//
// (G)
//
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: _onWillPop,
child: Stack(
children: [
Scaffold(
backgroundColor: Colors.white,
// AppBar
appBar: AppBar(
title: const Text('ALL SCORE', style: TextStyle(color: Colors.white)),
backgroundColor: Colors.black,
),
//
body: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// (1) UI
Expanded(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
//
// 1. (ID/PW)
//
const Text(
'올스코어 로그인',
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black),
),
const SizedBox(height: 16),
// (A)
SizedBox(
width: 300,
child: TextField(
controller: idController,
decoration: InputDecoration(
labelText: 'ID',
border: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.black),
borderRadius: BorderRadius.circular(8),
),
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.black),
borderRadius: BorderRadius.circular(8),
),
),
),
),
const SizedBox(height: 12),
// (B)
SizedBox(
width: 300,
child: TextField(
controller: passwordController,
obscureText: true,
decoration: InputDecoration(
labelText: 'PW',
border: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.black),
borderRadius: BorderRadius.circular(8),
),
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.black),
borderRadius: BorderRadius.circular(8),
),
),
),
),
// (C)
if (loginErrorMessage.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
loginErrorMessage,
style: const TextStyle(color: Colors.red),
),
),
const SizedBox(height: 8),
// (D)
SizedBox(
width: 300,
child: Row(
Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text(
'ALL SCORE'
/* ALL SCORE */,
style: TextStyle(color: Colors.white),
),
backgroundColor: Colors.black,
),
body: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// (1) UI
Expanded(
child: Center(
child: SingleChildScrollView(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
crossAxisAlignment: CrossAxisAlignment.center,
children: [
Checkbox(
value: autoLogin,
onChanged: (val) {
setState(() {
autoLogin = val ?? false;
});
},
//
// 1. (ID/PW)
//
const Text(
'ALLSCORE Login'
/* 올스코어 로그인 */,
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black),
),
const SizedBox(height: 16),
// (A)
SizedBox(
width: 300,
child: TextField(
controller: idController,
decoration: InputDecoration(
labelText: 'ID'
/* 아이디 */,
border: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.black),
borderRadius: BorderRadius.circular(8),
),
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.black),
borderRadius: BorderRadius.circular(8),
),
),
),
),
const SizedBox(height: 12),
// (B)
SizedBox(
width: 300,
child: TextField(
controller: passwordController,
obscureText: true,
decoration: InputDecoration(
labelText: 'PW'
/* 비밀번호 */,
border: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.black),
borderRadius: BorderRadius.circular(8),
),
enabledBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.black),
borderRadius: BorderRadius.circular(8),
),
),
),
),
// (C)
if (loginErrorMessage.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
loginErrorMessage,
style: const TextStyle(color: Colors.red),
),
),
const SizedBox(height: 8),
// (D)
SizedBox(
width: 300,
child: Row(
children: [
Checkbox(
value: autoLogin,
onChanged: (val) {
setState(() {
autoLogin = val ?? false;
});
},
),
const Text(
'Auto Login'
/* 자동로그인 */,
style: TextStyle(color: Colors.black),
),
],
),
),
// (E)
SizedBox(
width: 300,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
side: const BorderSide(color: Colors.black),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
onPressed: _loginWithIdPw,
child: const Text(
'Login'
/* 로그인 */,
),
),
),
// (F) ID/PW ,
const SizedBox(height: 8),
TextButton(
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (_) => const IdFindingPage()));
},
child: const Text(
'Find ID'
/* ID 찾기 */,
style: TextStyle(color: Colors.black),
),
),
TextButton(
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (_) => const PwFindingPage()));
},
child: const Text(
'Find PW'
/* PW 찾기 */,
style: TextStyle(color: Colors.black),
),
),
TextButton(
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (_) => const SignUpPage()));
},
child: const Text(
'Sign Up'
/* 회원가입 */,
style: TextStyle(color: Colors.black),
),
),
const SizedBox(height: 24),
const Divider(height: 1, color: Colors.black),
const SizedBox(height: 24),
//
// 2. /
//
const Text(
'Google Account'
/* 구글 계정 */,
style: TextStyle(fontSize: 20, color: Colors.black, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
// (a)
SizedBox(
width: 300,
child: ElevatedButton.icon(
icon: Container(
width: 24,
height: 24,
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/icons8-google-logo-48.png'),
fit: BoxFit.contain,
),
),
),
label: const Text(
'Google Login'
/* Google 로그인 */,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
side: const BorderSide(color: Colors.black),
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
onPressed: _googleLogin,
),
),
const SizedBox(height: 12),
// (b)
SizedBox(
width: 300,
child: ElevatedButton.icon(
icon: Container(
width: 24,
height: 24,
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/icons8-google-logo-48.png'),
fit: BoxFit.contain,
),
),
),
label: const Text(
'Google Sign Up'
/* Google 회원가입 */,
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
side: const BorderSide(color: Colors.black),
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
onPressed: _googleSignUp,
),
),
const Text('자동로그인', style: TextStyle(color: Colors.black)),
],
),
),
// (E)
SizedBox(
width: 300,
child: ElevatedButton(
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
side: const BorderSide(color: Colors.black),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
onPressed: _loginWithIdPw,
child: const Text('로그인'),
),
),
// (F) ID/PW ,
const SizedBox(height: 8),
TextButton(
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (_) => const IdFindingPage()));
},
child: const Text('ID 찾기', style: TextStyle(color: Colors.black)),
),
TextButton(
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (_) => const PwFindingPage()));
},
child: const Text('PW 찾기', style: TextStyle(color: Colors.black)),
),
TextButton(
onPressed: () {
Navigator.push(context, MaterialPageRoute(builder: (_) => const SignUpPage()));
},
child: const Text('회원가입', style: TextStyle(color: Colors.black)),
),
const SizedBox(height: 24),
const Divider(height: 1, color: Colors.black),
const SizedBox(height: 24),
//
// 2. /
//
const Text(
'구글 계정',
style: TextStyle(fontSize: 20, color: Colors.black, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
// (a)
SizedBox(
width: 300,
child: ElevatedButton.icon(
icon: Container(
width: 24,
height: 24,
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/icons8-google-logo-48.png'),
fit: BoxFit.contain,
),
),
),
label: const Text(
'Google 로그인',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
side: const BorderSide(color: Colors.black),
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
onPressed: _googleLogin,
),
),
const SizedBox(height: 12),
// (b)
SizedBox(
width: 300,
child: ElevatedButton.icon(
icon: Container(
width: 24,
height: 24,
decoration: const BoxDecoration(
image: DecorationImage(
image: AssetImage('assets/images/icons8-google-logo-48.png'),
fit: BoxFit.contain,
),
),
),
label: const Text(
'Google 회원가입',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
side: const BorderSide(color: Colors.black),
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 24),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
onPressed: _googleSignUp,
),
),
],
),
),
),
// (2)
if (_isBannerReady && _bannerAd != null)
Container(
width: _bannerAd!.size.width.toDouble(),
height: _bannerAd!.size.height.toDouble(),
alignment: Alignment.center,
child: AdWidget(ad: _bannerAd!),
),
],
),
),
// (2)
if (_isBannerReady && _bannerAd != null)
//
if (_isLoading)
Container(
width: _bannerAd!.size.width.toDouble(),
height: _bannerAd!.size.height.toDouble(),
width: double.infinity,
height: double.infinity,
color: Colors.black54,
alignment: Alignment.center,
child: AdWidget(ad: _bannerAd!),
)
child: const CircularProgressIndicator(color: Colors.white),
),
],
),
),
// (2)
if (_isLoading)
Container(
width: double.infinity,
height: double.infinity,
color: Colors.black54, //
alignment: Alignment.center,
child: const CircularProgressIndicator(color: Colors.white),
),
]
)
);
}
}

View File

@ -1,15 +1,15 @@
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:http/http.dart' as http; // http
import 'dart:convert';
import 'dart:convert' show utf8;
import 'login_page.dart'; //
import 'signup_page.dart'; //
import 'id_finding_page.dart'; // ID
import 'login_page.dart'; //
import 'signup_page.dart'; //
import 'id_finding_page.dart'; // ID
//
// Mobile ads ( )
import '../../plugins/admob.dart';
//
// Config ()
import '../../config/config.dart';
class PwFindingPage extends StatefulWidget {
@ -20,70 +20,96 @@ class PwFindingPage extends StatefulWidget {
}
class _PwFindingPageState extends State<PwFindingPage> {
final TextEditingController idController = TextEditingController(); // ID
final TextEditingController emailController = TextEditingController(); //
String emailErrorMessage = ''; //
String idErrorMessage = ''; // ID
final TextEditingController idController = TextEditingController();
final TextEditingController emailController = TextEditingController();
String emailErrorMessage = '';
String idErrorMessage = '';
Future<void> _findPassword(String id, String email) async {
// PW
//
/* PW 찾기 요청 처리 */
// Show loading indicator ( )
showDialog(
context: context,
barrierDismissible: false, //
barrierDismissible: false,
builder: (BuildContext context) {
return const Center(child: CircularProgressIndicator());
},
);
try {
final response = await http.post(
Uri.parse('https://eldsoft.com:8097/user/find/password'),
headers: {
'Content-Type': 'application/json',
},
body: jsonEncode({
'user_id': id,
'user_email': email,
}),
).timeout(const Duration(seconds: 10)); // 10
final response = await http
.post(
Uri.parse('https://eldsoft.com:8097/user/find/password'),
headers: {
'Content-Type': 'application/json',
},
body: jsonEncode({
'user_id': id,
'user_email': email,
}),
)
.timeout(const Duration(seconds: 10)); // 10
String responseBody = utf8.decode(response.bodyBytes); // UTF-8
String responseBody = utf8.decode(response.bodyBytes);
Navigator.of(context).pop(); //
Navigator.of(context).pop(); // Close loading indicator ( )
if (response.statusCode == 200) {
final Map<String, dynamic> jsonResponse = jsonDecode(responseBody);
//
// Reset error messages ( )
setState(() {
emailErrorMessage = '';
idErrorMessage = '';
});
if (jsonResponse['response_info']['msg_title'] == '아이디 확인') {
/* 아이디 확인 */
setState(() {
idErrorMessage = '아이디를 다시 확인해주세요'; // ID
idErrorMessage = 'Please check your ID again.'
/* 아이디를 다시 확인해주세요 */;
});
} else if (jsonResponse['response_info']['msg_title'] == '이메일 확인') {
/* 이메일 확인 */
setState(() {
emailErrorMessage = '이메일을 다시 확인해주세요'; //
emailErrorMessage = 'Please check your email again.'
/* 이메일을 다시 확인해주세요 */;
});
} else if (jsonResponse['result'] == 'OK') {
//
_showDialog('비밀번호 찾기 안내', '임시 비밀번호가 입력하신 이메일로 발송되었습니다.', 'LOGIN');
/* 성공 시 */
_showDialog(
'Password Recovery Notice'
/* 비밀번호 찾기 안내 */,
'An interim password has been sent to your email.'
/* 임시 비밀번호가 입력하신 이메일로 발송되었습니다. */,
'LOGIN',
);
} else {
//
_showDialog(jsonResponse['response_info']['msg_title'], jsonResponse['response_info']['msg_content'], 'STAY');
//
_showDialog(
jsonResponse['response_info']['msg_title'],
jsonResponse['response_info']['msg_content'],
'STAY',
);
}
} else {
//
_showDialog('오류', '요청이 실패했습니다. 관리자에게 문의해주세요.', 'STAY');
//
_showDialog(
'Error' /* 오류 */,
'Request failed. Please contact the administrator.'
/* 요청이 실패했습니다. 관리자에게 문의해주세요. */,
'STAY',
);
}
} catch (e) {
Navigator.of(context).pop(); //
_showDialog('오류', '요청이 실패했습니다. 관리자에게 문의해주세요.', 'STAY');
Navigator.of(context).pop();
_showDialog(
'Error' /* 오류 */,
'Request failed. Please contact the administrator.'
/* 요청이 실패했습니다. 관리자에게 문의해주세요. */,
'STAY',
);
}
}
@ -92,7 +118,7 @@ class _PwFindingPageState extends State<PwFindingPage> {
context: context,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor: Colors.white, //
backgroundColor: Colors.white,
title: Text(title, style: const TextStyle(color: Colors.black)),
content: Text(content, style: const TextStyle(color: Colors.black)),
actions: <Widget>[
@ -100,13 +126,14 @@ class _PwFindingPageState extends State<PwFindingPage> {
child: TextButton(
style: TextButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white, //
foregroundColor: Colors.white,
),
child: const Text('확인'),
child: const Text('OK' /* 확인 */),
onPressed: () {
Navigator.of(context).pop(); //
Navigator.of(context).pop();
if (action == 'LOGIN') {
Navigator.of(context).pop(); //
Navigator.of(context).pop();
/* 로그인 페이지로 이동 */
}
},
),
@ -132,7 +159,7 @@ class _PwFindingPageState extends State<PwFindingPage> {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text('ALL SCORE', style: TextStyle(color: Colors.white)),
title: const Text('ALL SCORE' /* ALL SCORE */, style: TextStyle(color: Colors.white)),
backgroundColor: Colors.black,
),
body: Padding(
@ -141,26 +168,24 @@ class _PwFindingPageState extends State<PwFindingPage> {
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'PW 찾기',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black,
),
'Find Password'
/* PW 찾기 */,
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black),
),
const SizedBox(height: 32),
TextField(
controller: idController, // ID
decoration: InputDecoration(
labelText: 'ID',
labelStyle: const TextStyle(color: Colors.black),
controller: idController,
decoration: const InputDecoration(
labelText: 'ID'
/* ID */,
labelStyle: TextStyle(color: Colors.black),
border: OutlineInputBorder(),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.black, width: 2.0),
borderSide: BorderSide(color: Colors.black, width: 2.0),
),
),
),
if (idErrorMessage.isNotEmpty) // ID
if (idErrorMessage.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
@ -170,17 +195,18 @@ class _PwFindingPageState extends State<PwFindingPage> {
),
const SizedBox(height: 16),
TextField(
controller: emailController, //
decoration: InputDecoration(
labelText: '이메일',
labelStyle: const TextStyle(color: Colors.black),
controller: emailController,
decoration: const InputDecoration(
labelText: 'Email'
/* 이메일 */,
labelStyle: TextStyle(color: Colors.black),
border: OutlineInputBorder(),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.black, width: 2.0),
borderSide: BorderSide(color: Colors.black, width: 2.0),
),
),
),
if (emailErrorMessage.isNotEmpty) //
if (emailErrorMessage.isNotEmpty)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
@ -191,20 +217,23 @@ class _PwFindingPageState extends State<PwFindingPage> {
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
_findPassword(idController.text, emailController.text); // PW
/* PW 찾기 요청 */
_findPassword(idController.text, emailController.text);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
),
child: const Text('PW 찾기'),
child: const Text('Find Password'
/* PW 찾기 */),
),
const SizedBox(height: 16),
TextButton(
onPressed: () {
Navigator.pop(context); //
Navigator.pop(context);
/* 로그인 페이지로 이동 */
},
child: const Text('로그인', style: TextStyle(color: Colors.black)),
child: const Text('Login' /* 로그인 */, style: TextStyle(color: Colors.black)),
),
TextButton(
onPressed: () {
@ -213,7 +242,7 @@ class _PwFindingPageState extends State<PwFindingPage> {
MaterialPageRoute(builder: (context) => const IdFindingPage()),
);
},
child: const Text('ID 찾기', style: TextStyle(color: Colors.black)),
child: const Text('Find ID' /* ID 찾기 */, style: TextStyle(color: Colors.black)),
),
TextButton(
onPressed: () {
@ -222,14 +251,13 @@ class _PwFindingPageState extends State<PwFindingPage> {
MaterialPageRoute(builder: (context) => const SignUpPage()),
);
},
child: const Text('회원가입', style: TextStyle(color: Colors.black)),
child: const Text('Sign Up' /* 회원가입 */, style: TextStyle(color: Colors.black)),
),
],
),
),
// (3)
bottomNavigationBar: AdBannerWidget(),
bottomNavigationBar: AdBannerWidget(),
/* (3) 하단 광고 영역 */
);
}
}
}

View File

@ -4,7 +4,7 @@ import 'dart:convert'; // JSON 변환을 위해 임포트
import 'login_page.dart'; //
import 'dart:convert' show utf8; // UTF-8
import 'package:crypto/crypto.dart'; // crypto
import '../../config/config.dart'; // config
class SignUpPage extends StatefulWidget {
const SignUpPage({Key? key}) : super(key: key);
@ -13,53 +13,69 @@ class SignUpPage extends StatefulWidget {
}
class _SignUpPageState extends State<SignUpPage> {
//
bool _isAgreed = false; //
// ( )
bool _isAgreed = false;
//
String _username = '', _password = '', _confirmPassword = '', _nickname = '', _email = '';
String _department = '', _introduceMyself = ''; //
String _department = '', _introduceMyself = '';
// ( )
String? _usernameError, _passwordError, _confirmPasswordError, _nicknameError, _emailError;
//
bool _isUsernameValid(String username) => RegExp(r'^(?![0-9])[A-Za-z0-9]{6,20}$').hasMatch(username);
bool _isPasswordValidPattern(String password) => RegExp(r"""^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d!@#$%^&*()_\-+=~`{}\[\]|\\:;\"'<>,.?/]{8,20}$""").hasMatch(password);
bool _isEmailValid(String email) => RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(email);
bool _isNicknameValid(String nickname) => RegExp(r'^[A-Za-z가-힣0-9]{2,20}$').hasMatch(nickname);
// (, , , )
bool _isUsernameValid(String username) =>
RegExp(r'^(?![0-9])[A-Za-z0-9]{6,20}$').hasMatch(username);
bool _isPasswordValidPattern(String password) =>
RegExp(r"""^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d!@#$%^&*()_\-+=~`{}\[\]|\\:;\"'<>,.?/]{8,20}$""")
.hasMatch(password);
bool _isEmailValid(String email) =>
RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(email);
bool _isNicknameValid(String nickname) =>
RegExp(r'^[A-Za-z가-힣0-9]{2,20}$').hasMatch(nickname);
//
// label에
void _validateInput(String label) {
setState(() {
if (label == '아이디') {
_usernameError = _isUsernameValid(_username) ? null : '아이디는 6~20자 영문 대소문자와 숫자 조합이어야 하며, 숫자로 시작할 수 없습니다.';
} else if (label == '비밀번호') {
_passwordError = _isPasswordValidPattern(_password) ? null : '비밀번호는 8~20자 영문과 숫자가 반드시 포함된 조합이어야 합니다.';
} else if (label == '비밀번호 확인') {
_confirmPasswordError = _password == _confirmPassword ? null : '비밀번호가 일치하지 않습니다.';
} else if (label == '닉네임') {
_nicknameError = _isNicknameValid(_nickname) ? null : '* 닉네임은 2~20자 영문, 한글, 숫자만 사용할 수 있습니다.';
} else if (label == '이메일') {
_emailError = _isEmailValid(_email) ? null : (_email.isNotEmpty ? '올바른 이메일 형식을 입력해주세요.' : null);
if (label == 'Username') { // '아이디'
_usernameError = _isUsernameValid(_username)
? null
: 'Username must be 620 characters, letters and digits, and cannot start with a digit.';
} else if (label == 'Password') { // '비밀번호'
_passwordError = _isPasswordValidPattern(_password)
? null
: 'Password must be 820 characters, including letters and digits.';
} else if (label == 'Confirm Password') { // '비밀번호 확인'
_confirmPasswordError = (_password == _confirmPassword)
? null
: 'Passwords do not match.';
} else if (label == 'Nickname') { // '닉네임'
_nicknameError = _isNicknameValid(_nickname)
? null
: '* Nickname must be 220 characters (letters, Korean, digits).';
} else if (label == 'Email') { // '이메일'
_emailError = _isEmailValid(_email)
? null
: (_email.isNotEmpty ? 'Please enter a valid email address.' : null);
}
});
}
//
// (Sign-up request)
Future<void> _signUp() async {
final url = 'https://eldsoft.com:8097/user/signup';
// mandatory_terms_yn
final mandatoryTermsYn = _isAgreed ? 'Y' : 'N';
// SHA-256
// (SHA-256)
final hashedPassword = _hashPassword(_password);
final body = {
"user_id": _username,
"user_pw": hashedPassword, //
"user_pw": hashedPassword,
"nickname": _nickname,
"user_email": _email,
"department": _department,
"introduce_myself": _introduceMyself,
"mandatory_terms_yn": mandatoryTermsYn //
"mandatory_terms_yn": mandatoryTermsYn
};
try {
@ -69,76 +85,72 @@ class _SignUpPageState extends State<SignUpPage> {
body: json.encode(body),
);
// body를 UTF-8
// UTF-8
final resBody = json.decode(utf8.decode(response.bodyBytes));
if (response.statusCode == 200) {
// result가 OK이어야만
if (resBody['result'] == 'OK') {
if (resBody['response_info']['msg_type'] == 'OK') {
_showDialog('회원가입 성공', '회원가입이 완료되었습니다.');
_showDialog('Sign-up Success', 'Your account has been created successfully.');
} else {
_showDialog('회원가입 실패', '${resBody['response_info']['msg_content']}');
_showDialog('Sign-up Failed', '${resBody['response_info']['msg_content']}');
}
} else {
_showDialog('회원가입 실패', '${resBody['response_info']['msg_content']}');
_showDialog('Sign-up Failed', '${resBody['response_info']['msg_content']}');
}
} else {
final errorData = json.decode(response.body);
_showDialog('회원가입 실패', errorData['message'] ?? '회원가입 실패');
_showDialog('Sign-up Failed', errorData['message'] ?? 'Sign-up failed.');
}
} catch (error) {
_showDialog('네트워크 오류', '네트워크 오류: $error');
_showDialog('Network Error', 'Network error: $error');
}
}
//
// (SHA-256)
String _hashPassword(String password) {
final bytes = utf8.encode(password); //
final digest = sha256.convert(bytes); // SHA-256
return digest.toString(); //
final bytes = utf8.encode(password);
final digest = sha256.convert(bytes);
return digest.toString();
}
//
//
void _showDialog(String title, String message) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor: Colors.white, //
backgroundColor: Colors.white,
title: Text(
title,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.black, //
color: Colors.black,
),
),
content: Column(
mainAxisSize: MainAxisSize.min, //
mainAxisSize: MainAxisSize.min,
children: [
Text(
message,
style: const TextStyle(
fontSize: 16,
color: Colors.black, //
),
style: const TextStyle(fontSize: 16, color: Colors.black),
),
const SizedBox(height: 20), //
const SizedBox(height: 20),
TextButton(
onPressed: () {
Navigator.of(context).pop(); //
if (title == '회원가입 성공') {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => LoginPage())); //
Navigator.of(context).pop();
if (title == 'Sign-up Success') {
//
Navigator.pushReplacement(context, MaterialPageRoute(builder: (context) => const LoginPage()));
}
// '회원가입 성공'
},
style: TextButton.styleFrom(
foregroundColor: Colors.white, //
backgroundColor: Colors.black, //
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10), //
foregroundColor: Colors.white,
backgroundColor: Colors.black,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
),
child: const Text('확인'),
child: const Text('OK'),
),
],
),
@ -147,22 +159,27 @@ class _SignUpPageState extends State<SignUpPage> {
);
}
//
Widget _buildTextField(String label, Function(String) onChanged, {bool obscureText = false, String? errorText}) {
// (label에 )
Widget _buildTextField(String label, Function(String) onChanged, {
bool obscureText = false,
String? errorText,
}) {
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
TextField(
onChanged: (value) {
onChanged(value);
_validateInput(label); // label에
_validateInput(label);
},
obscureText: obscureText,
decoration: InputDecoration(
labelText: label,
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),
),
errorStyle: const TextStyle(color: Colors.red, fontSize: 12),
),
),
@ -194,21 +211,34 @@ class _SignUpPageState extends State<SignUpPage> {
child: Column(
mainAxisAlignment: MainAxisAlignment.start,
children: [
const Text('회원가입', style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black)),
const Text(
'Sign Up', // '회원가입'
style: TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black),
),
const SizedBox(height: 32),
_buildTextField('아이디', (value) => _username = value, errorText: _usernameError),
_buildTextField('비밀번호', (value) => _password = value, obscureText: true, errorText: _passwordError),
_buildTextField('비밀번호 확인', (value) => _confirmPassword = value, obscureText: true, errorText: _confirmPasswordError),
_buildTextField('닉네임', (value) => _nickname = value, errorText: _nicknameError),
_buildTextField('이메일', (value) => _email = value, errorText: _emailError),
_buildTextField('소속(선택사항)', (value) => _department = value),
_buildTextField('자기소개(선택사항)', (value) => _introduceMyself = value),
_buildTextField('Username', (value) => _username = value, errorText: _usernameError),
_buildTextField('Password', (value) => _password = value, obscureText: true, errorText: _passwordError),
_buildTextField('Confirm Password', (value) => _confirmPassword = value, obscureText: true, errorText: _confirmPasswordError),
_buildTextField('Nickname', (value) => _nickname = value, errorText: _nicknameError),
_buildTextField('Email', (value) => _email = value, errorText: _emailError),
_buildTextField('Affiliation (Optional)', (value) => _department = value),
_buildTextField('Self-introduction (Optional)', (value) => _introduceMyself = value),
const SizedBox(height: 16),
const Text('개인정보 수집 및 이용 동의서', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black)),
//
const Text(
'Consent to the Collection and Use of Personal Information',
// '개인정보 수집 및 이용 동의서'
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black),
),
const SizedBox(height: 8),
Container(
height: 200,
decoration: BoxDecoration(border: Border.all(color: Colors.black.withOpacity(0.5)), borderRadius: BorderRadius.circular(8)),
decoration: BoxDecoration(
border: Border.all(color: Colors.black.withOpacity(0.5)),
borderRadius: BorderRadius.circular(8),
),
child: Scrollbar(
thickness: 5,
radius: const Radius.circular(5),
@ -216,50 +246,9 @@ class _SignUpPageState extends State<SignUpPage> {
child: const Padding(
padding: EdgeInsets.all(15.0),
child: Text(
'올스코어(이하 "회사"라 합니다)는 이용자의 개인정보를 중요시하며, '
'「개인정보 보호법」 등 관련 법령을 준수하고 있습니다. '
'회사는 개인정보 수집 및 이용에 관한 사항을 아래와 같이 안내드리오니, '
'내용을 충분히 숙지하신 후 동의하여 주시기 바랍니다.\n\n'
'1. 수집하는 개인정보 항목\n'
'필수 항목: 아이디(ID), 비밀번호(PW), 닉네임(실명 아님), 이메일 주소\n'
'선택 항목: 소속, 자기소개\n\n'
'2. 개인정보의 수집 및 이용 목적\n'
'회원 관리\n'
'회원 식별 및 인증\n'
'부정 이용 방지 및 비인가 사용 방지\n'
'서비스 이용에 따른 문의 사항 처리\n'
'서비스 제공\n'
'게임 생성 및 참여 등 기본 서비스 제공\n'
'통계 및 순위 제공 등 부가 서비스 제공\n'
'고객 지원 및 공지사항 전달\n'
'서비스 관련 중요한 공지사항 전달\n'
'이용자 문의 및 불만 처리\n\n'
'3. 개인정보의 보유 및 이용 기간\n'
'회원 탈퇴 시: 수집된 모든 개인정보는 회원 탈퇴 즉시 파기합니다.\n'
'관련 법령에 따른 보관: 전자상거래 등에서의 소비자 보호에 관한 법률 등 관계 법령의 규정에 따라 일정 기간 보관이 필요한 경우 해당 기간 동안 보관합니다.\n'
'계약 또는 청약 철회 등에 관한 기록: 5년 보관\n'
'대금 결제 및 재화 등의 공급에 관한 기록: 5년 보관\n'
'소비자의 불만 또는 분쟁 처리에 관한 기록: 3년 보관\n\n'
'4. 개인정보의 파기 절차 및 방법\n'
'파기 절차\n'
'회원 탈퇴 요청 또는 개인정보 수집 및 이용 목적이 달성된 후 지체 없이 해당 정보를 파기합니다.\n'
'파기 방법\n'
'전자적 파일 형태: 복구 및 재생이 불가능한 방법으로 영구 삭제\n'
'종이 문서 형태: 분쇄하거나 소각\n\n'
'5. 이용자의 권리 및 행사 방법\n'
'이용자는 언제든지 자신의 개인정보에 대해 열람, 수정, 삭제, 처리 정지를 요구할 수 있습니다.\n'
'회원 탈퇴를 원하시는 경우, 서비스 내의 "회원 탈퇴" 기능을 이용하시거나 고객센터를 통해 요청하실 수 있습니다.\n\n'
'6. 동의를 거부할 권리 및 거부 시 불이익\n'
'이용자는 개인정보 수집 및 이용에 대한 동의를 거부할 권리가 있습니다.\n'
'그러나 필수 항목에 대한 동의를 거부하실 경우 서비스 이용이 제한될 수 있습니다.\n\n'
'7. 개인정보 보호책임자\n'
'연락처: eldyeojh@gmail.com\n\n'
'8. 개인정보의 안전성 확보 조치\n'
'회사는 개인정보의 안전한 처리를 위하여 기술적, 관리적 보호조치를 시행하고 있습니다.\n'
'개인정보의 암호화\n'
'해킹 등에 대비한 대책\n'
'접근 통제 장치의 설치 및 운영',
Config.termsOfService,
textAlign: TextAlign.left,
style: const TextStyle(color: Colors.black),
),
),
),
@ -273,21 +262,21 @@ class _SignUpPageState extends State<SignUpPage> {
value: _isAgreed,
onChanged: (value) {
setState(() {
_isAgreed = value ?? false; //
_isAgreed = value ?? false;
});
},
),
const Text('개인정보 수집 및 이용에 동의합니다.'),
const Text('I agree to the collection and use of my personal information.'),
],
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: _signUp, //
onPressed: _signUp,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
),
child: const Text('ALL SCORE 회원가입'),
child: const Text('Sign Up for ALLSCORE'),
),
],
),
@ -295,4 +284,4 @@ class _SignUpPageState extends State<SignUpPage> {
),
);
}
}
}

View File

@ -1,5 +1,5 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; //
import 'package:flutter/services.dart'; // restrict number input ( )
import '../../plugins/api.dart';
import '../../dialogs/response_dialog.dart';
import 'package:shared_preferences/shared_preferences.dart';
@ -17,25 +17,25 @@ class _CreateRoomPageState extends State<CreateRoomPage> {
final TextEditingController _roomNameController = TextEditingController();
final TextEditingController _roomDescriptionController = TextEditingController();
/// (open_yn: 'Y'/'N')
bool _isPrivate = false;
/// Public or private (open_yn: 'Y' / 'N')
bool _isPrivate = false; // (open_yn: 'Y'/'N')
final TextEditingController _passwordController = TextEditingController();
/// (1~6)
/// Running time (1~6 hours) ()
int _selectedHour = 1;
/// : /
/// Game type: solo/team ( : /)
bool _isTeamGame = false;
/// ( 2)
/// Team count ( ) if team game, minimum 2 teams
int _selectedTeamCount = 2;
///
/// Max participants ( )
final TextEditingController _maxParticipantsController = TextEditingController(text: '1');
///
/// - : 'ALL' / 'PRIVATE'
/// - : 'ALL' / 'TEAM' / 'PRIVATE'
String _selectedScoreOpenRange = 'ALL';
/// Score open range ( )
/// - Solo: 'ALL' or 'PRIVATE'
/// - Team: 'ALL', 'TEAM', or 'PRIVATE'
String _selectedScoreOpenRange = 'ALL';
@override
void dispose() {
@ -49,12 +49,12 @@ class _CreateRoomPageState extends State<CreateRoomPage> {
@override
Widget build(BuildContext context) {
return Scaffold(
//
// White background ( )
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text(
'방 만들기',
'Create Room', // '방 만들기'
style: TextStyle(color: Colors.white),
),
backgroundColor: Colors.black,
@ -70,29 +70,35 @@ class _CreateRoomPageState extends State<CreateRoomPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// (A)
const Text('방 제목',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
/// (A) Room Title ( )
const Text(
'Room Title', // '방 제목'
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 6),
_buildWhiteBorderTextField(
controller: _roomNameController,
hintText: '방 제목을 입력하세요',
hintText: 'Enter room title', // '방 제목을 입력하세요'
),
const SizedBox(height: 16),
/// (B)
const Text('방 소개',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
/// (B) Room Description ( )
const Text(
'Room Description', // '방 소개'
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 6),
_buildMultilineBox(
controller: _roomDescriptionController,
hintText: '방 소개를 입력하세요',
hintText: 'Enter room description', // '방 소개를 입력하세요'
),
const SizedBox(height: 16),
/// (C) ( / )
const Text('비밀번호 설정',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
/// (C) Password setting (public / private) ( )
const Text(
'Password Setup', // '비밀번호 설정'
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
Row(
children: [
Checkbox(
@ -101,12 +107,12 @@ class _CreateRoomPageState extends State<CreateRoomPage> {
checkColor: Colors.white,
onChanged: (value) {
setState(() {
// == !
// public == !private ( == !)
_isPrivate = !value!;
});
},
),
const Text('공개'),
const Text('Public'), // '공개'
const SizedBox(width: 10),
Checkbox(
value: _isPrivate,
@ -118,20 +124,22 @@ class _CreateRoomPageState extends State<CreateRoomPage> {
});
},
),
const Text('비공개'),
const Text('Private'), // '비공개'
],
),
if (_isPrivate)
_buildWhiteBorderTextField(
controller: _passwordController,
hintText: '비밀번호를 입력하세요',
hintText: 'Enter password', // '비밀번호를 입력하세요'
obscureText: true,
),
const SizedBox(height: 16),
/// (D) (1~6)
const Text('운영시간 설정',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
/// (D) Running time (1~6 hours) ( )
const Text(
'Running Time', // '운영시간 설정'
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
Row(
children: [
DropdownButton<int>(
@ -151,14 +159,16 @@ class _CreateRoomPageState extends State<CreateRoomPage> {
},
),
const SizedBox(width: 8),
const Text('시간'),
const Text('Hour'), // '시간'
],
),
const SizedBox(height: 16),
/// (E) (/)
const Text('게임 유형',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
/// (E) Game type: solo/team ( : /)
const Text(
'Game Type', // '게임 유형'
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
Row(
children: [
Checkbox(
@ -168,12 +178,12 @@ class _CreateRoomPageState extends State<CreateRoomPage> {
onChanged: (value) {
setState(() {
_isTeamGame = !value!;
//
// Reset score open range ( )
_selectedScoreOpenRange = 'ALL';
});
},
),
const Text('개인전'),
const Text('Solo'), // '개인전'
const SizedBox(width: 10),
Checkbox(
value: _isTeamGame,
@ -182,17 +192,17 @@ class _CreateRoomPageState extends State<CreateRoomPage> {
onChanged: (value) {
setState(() {
_isTeamGame = value!;
//
// Reset score open range ( )
_selectedScoreOpenRange = 'ALL';
});
},
),
const Text('팀전'),
const Text('Team'), // '팀전'
const SizedBox(width: 16),
if (_isTeamGame) ...[
const Text('팀수: '),
const Text('Teams: '), // '팀수: '
const SizedBox(width: 8),
// 2
// Minimum 2 teams ( 2)
DropdownButton<int>(
value: _selectedTeamCount,
dropdownColor: Colors.white,
@ -214,9 +224,11 @@ class _CreateRoomPageState extends State<CreateRoomPage> {
),
const SizedBox(height: 16),
/// (F)
const Text('최대 인원수',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
/// (F) Max participants ( )
const Text(
'Max Participants', // '최대 인원수'
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
Row(
children: [
SizedBox(
@ -251,26 +263,26 @@ class _CreateRoomPageState extends State<CreateRoomPage> {
),
),
const SizedBox(width: 8),
const Text('', style: TextStyle(fontSize: 16)),
const Text('people', style: TextStyle(fontSize: 16)), // ''
],
),
const SizedBox(height: 16),
/// (G)
/// : 'ALL', 'PRIVATE'
/// : 'ALL', 'TEAM', 'PRIVATE'
/// (G) Score open range ( )
/// Solo: 'ALL', 'PRIVATE'
/// Team: 'ALL', 'TEAM', 'PRIVATE'
const Text(
'점수 공개 범위 설정',
'Score Visibility', // '점수 공개 범위 설정'
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
if (!_isTeamGame)
// ALL, PRIVATE
if (!_isTeamGame)
// Solo ALL, PRIVATE ()
Column(
children: [
RadioListTile<String>(
title: const Text('전체'),
title: const Text('All'), // '전체'
value: 'ALL',
groupValue: _selectedScoreOpenRange,
activeColor: Colors.black,
@ -281,7 +293,7 @@ class _CreateRoomPageState extends State<CreateRoomPage> {
},
),
RadioListTile<String>(
title: const Text('개인'),
title: const Text('Private'), // '개인'
value: 'PRIVATE',
groupValue: _selectedScoreOpenRange,
activeColor: Colors.black,
@ -294,11 +306,11 @@ class _CreateRoomPageState extends State<CreateRoomPage> {
],
)
else
// ALL, TEAM, PRIVATE
// Team ALL, TEAM, PRIVATE ()
Column(
children: [
RadioListTile<String>(
title: const Text('전체'),
title: const Text('All'), // '전체'
value: 'ALL',
groupValue: _selectedScoreOpenRange,
activeColor: Colors.black,
@ -309,7 +321,7 @@ class _CreateRoomPageState extends State<CreateRoomPage> {
},
),
RadioListTile<String>(
title: const Text(''),
title: const Text('Team'), // ''
value: 'TEAM',
groupValue: _selectedScoreOpenRange,
activeColor: Colors.black,
@ -320,7 +332,7 @@ class _CreateRoomPageState extends State<CreateRoomPage> {
},
),
RadioListTile<String>(
title: const Text('개인'),
title: const Text('Private'), // '개인'
value: 'PRIVATE',
groupValue: _selectedScoreOpenRange,
activeColor: Colors.black,
@ -334,19 +346,19 @@ class _CreateRoomPageState extends State<CreateRoomPage> {
),
const SizedBox(height: 24),
/// (H) "방 생성하기"
/// (H) "Create Room" button ( )
Center(
child: ElevatedButton(
onPressed: () async {
try {
final serverResponse = await createRoom();
final serverResponse = await createRoom(); //
if (serverResponse['result'] == 'OK') {
final serverResponse1 = serverResponse['response'];
if (serverResponse1['result'] == 'OK') {
// roomSeq
// If success, get roomSeq
final roomSeq = serverResponse1['data']['room_seq'];
//
// Success move to waiting page ( )
if (_isTeamGame) {
Navigator.pushAndRemoveUntil(
context,
@ -380,12 +392,14 @@ class _CreateRoomPageState extends State<CreateRoomPage> {
} else {
showResponseDialog(
context,
'방 생성 실패',
'서버에 문제가 있습니다. 관리자에게 문의해주세요.',
'Room Creation Failed', // '방 생성 실패'
'There is a problem with the server. Please contact the administrator.',
// '서버에 문제가 있습니다. 관리자에게 문의해주세요.'
);
}
} catch (e) {
showResponseDialog(context, '방 생성 실패', e.toString());
showResponseDialog(context, 'Room Creation Failed', e.toString());
// '방 생성 실패'
}
},
style: ElevatedButton.styleFrom(
@ -396,7 +410,10 @@ class _CreateRoomPageState extends State<CreateRoomPage> {
borderRadius: BorderRadius.circular(40),
),
),
child: const Text('방 생성하기', style: TextStyle(fontSize: 16)),
child: const Text(
'Create Room', // '방 생성하기'
style: TextStyle(fontSize: 16),
),
),
),
const SizedBox(height: 16),
@ -406,9 +423,9 @@ class _CreateRoomPageState extends State<CreateRoomPage> {
);
}
///
/// "Create Room" logic ( )
Future<Map<String, dynamic>> createRoom() async {
// requestBody
// Build request body (requestBody )
final requestBody = {
'room_title': _roomNameController.text,
'room_intro': _roomDescriptionController.text,
@ -418,9 +435,9 @@ class _CreateRoomPageState extends State<CreateRoomPage> {
'room_type': _isTeamGame ? 'team' : 'private',
'number_of_teams': _selectedTeamCount.toString(),
'number_of_people': _maxParticipantsController.text,
// :
// : 'ALL', 'PRIVATE'
// : 'ALL', 'TEAM', 'PRIVATE'
// Score open range:
// solo: 'ALL', 'PRIVATE'
// team: 'ALL', 'TEAM', 'PRIVATE'
'score_open_range': _selectedScoreOpenRange,
'room_status': 'WAIT',
@ -431,7 +448,7 @@ class _CreateRoomPageState extends State<CreateRoomPage> {
await Api.serverRequest(uri: '/room/score/create/room', body: requestBody);
if (serverResponse == null) {
throw Exception('서버 응답이 null입니다.');
throw Exception('Server response is null.');
}
if (serverResponse['result'] == 'OK') {
return serverResponse;
@ -443,7 +460,7 @@ class _CreateRoomPageState extends State<CreateRoomPage> {
}
}
/// + TextField
/// White bordered text field ( + TextField)
Widget _buildWhiteBorderTextField({
required TextEditingController controller,
String hintText = '',
@ -463,7 +480,7 @@ class _CreateRoomPageState extends State<CreateRoomPage> {
);
}
/// ( )
/// Multiline text box for introduction, etc. ( )
Widget _buildMultilineBox({
required TextEditingController controller,
String hintText = '',

View File

@ -10,7 +10,7 @@ import 'main_page.dart';
class FinishPrivatePage extends StatefulWidget {
final int roomSeq;
final String enterType; // / =>
final String enterType; // from waiting/ongoing => if back, go to MainPage (/ => )
const FinishPrivatePage({
Key? key,
@ -25,18 +25,18 @@ class FinishPrivatePage extends StatefulWidget {
class _FinishPrivatePageState extends State<FinishPrivatePage> {
bool _isLoading = true;
Map<String, dynamic> _roomInfo = {}; // room_info
// user_info Map
Map<String, dynamic> _roomInfo = {}; // room_info from server ( room_info)
// Entire user_info Map ( user_info)
// userSeq { user_seq, nickname, participant_type, score, ... }
Map<String, dynamic> _userMap = {};
// ( )
// Participant list in descending score order ( )
List<Map<String, dynamic>> _playerList = [];
String _roomTitle = '';
DateTime? _startDt;
DateTime? _endDt;
String _masterUserSeq = '0'; // user_seq (ADMIN과는 )
String _masterUserSeq = '0'; // room master user_seq ( user_seq)
@override
void initState() {
@ -44,7 +44,7 @@ class _FinishPrivatePageState extends State<FinishPrivatePage> {
_fetchFinishRoomInfo();
}
/// (A)
/// (A) Fetch finished room info from server ( )
Future<void> _fetchFinishRoomInfo() async {
setState(() => _isLoading = true);
try {
@ -69,7 +69,8 @@ class _FinishPrivatePageState extends State<FinishPrivatePage> {
setState(() {
_roomInfo = rInfo;
_userMap = uInfo;
_roomTitle = rTitle.isNotEmpty ? rTitle : '종료된 방(개인전)';
_roomTitle = rTitle.isNotEmpty ? rTitle : 'Finished Room (Solo)';
// '종료된 방(개인전)'
_masterUserSeq = mSeq.toString();
if (sdt != null && sdt is String && sdt.contains('T')) {
@ -80,7 +81,7 @@ class _FinishPrivatePageState extends State<FinishPrivatePage> {
}
});
// userInfo -> List
// Convert userInfo list (userInfo -> List )
final List<Map<String, dynamic>> tempList = [];
uInfo.forEach((_, val) {
// val: { user_seq, participant_type, nickname, score, ... }
@ -89,11 +90,11 @@ class _FinishPrivatePageState extends State<FinishPrivatePage> {
final playerList = tempList.toList();
//
// Sort by score descending ( )
playerList.sort((a, b) {
final sa = (a['score'] ?? 0) as int;
final sb = (b['score'] ?? 0) as int;
return sb.compareTo(sa); //
return sb.compareTo(sa);
});
setState(() {
@ -101,33 +102,37 @@ class _FinishPrivatePageState extends State<FinishPrivatePage> {
_isLoading = false;
});
} else {
final msgTitle = resp['response_info']?['msg_title'] ?? '오류';
final msgContent = resp['response_info']?['msg_content'] ?? '데이터 조회 실패';
final msgTitle = resp['response_info']?['msg_title'] ?? 'Error'; // '오류'
final msgContent = resp['response_info']?['msg_content'] ?? 'Failed to fetch data';
// '데이터 조회 실패'
showResponseDialog(context, msgTitle, msgContent);
}
} else {
showResponseDialog(context, '실패', '서버 통신 오류');
showResponseDialog(context, 'Failed', 'Server communication error.');
// '실패', '서버 통신 오류'
}
} catch (e) {
showResponseDialog(context, '오류', '$e');
showResponseDialog(context, 'Error', '$e');
// '오류'
} finally {
setState(() => _isLoading = false);
}
}
/// (B)
/// (B) Back press ()
Future<bool> _onWillPop() async {
if (widget.enterType == 'game') {
// =>
// If from an ongoing game go to main ( => )
Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (_) => const MainPage()), (route) => false);
} else {
// => pop
// If from search, etc. just pop ( => pop)
Navigator.pop(context);
}
return false;
}
/// (C) ( )
/// (C) Show room info modal (rooms setting finish dialog: read only)
/// ( : )
Future<void> _openRoomSettingDialog() async {
if (_roomInfo.isEmpty) return;
await showDialog(
@ -137,34 +142,42 @@ class _FinishPrivatePageState extends State<FinishPrivatePage> {
);
}
/// (D)
/// (D) Show game duration ( )
Widget _buildGameTimeWidget() {
if (_startDt == null || _endDt == null) {
return const Text('게임 진행 시간: 00:00', style: TextStyle(fontSize: 14));
return const Text(
'Game duration: 00:00', // '게임 진행 시간: 00:00'
style: TextStyle(fontSize: 14),
);
}
final dur = _endDt!.difference(_startDt!);
final hh = dur.inHours.toString().padLeft(2, '0');
final mm = dur.inMinutes.remainder(60).toString().padLeft(2, '0');
return Text('게임 진행 시간: $hh:$mm', style: const TextStyle(fontSize: 14));
return Text(
'Game duration: $hh:$mm',
// '게임 진행 시간: $hh:$mm'
style: const TextStyle(fontSize: 14),
);
}
/// (E) ( )
/// - 1/2/3 //
/// (E) Build player list item ( )
/// - 1st/2nd/3rd => gold/silver/bronze medal
Widget _buildPlayerItem(Map<String, dynamic> user, int index) {
final score = (user['score'] ?? 0) as int;
var nickname = user['nickname'] ?? '유저';
final profileImg = user['profile_img'] ?? '';
final userSeq = user['user_seq'].toString() ?? '0';
final score = (user['score'] ?? 0) as int;
var nickname = user['nickname'] ?? 'User'; // '유저'
final profileImg = user['profile_img'] ?? '';
final userSeq = user['user_seq'].toString() ?? '0';
final participantType = user['participant_type'] ?? '';
if (_masterUserSeq == userSeq) {
//
// Mark room master ( )
nickname = '' + nickname;
} else if (participantType == 'ADMIN') {
//
// Mark admin ( )
nickname = '' + nickname;
}
// Medal icons
Widget medal = const SizedBox();
if (index == 0) {
medal = const Text('🥇 ', style: TextStyle(fontSize: 16));
@ -181,9 +194,10 @@ class _FinishPrivatePageState extends State<FinishPrivatePage> {
child: Row(
children: [
medal,
//
// Profile
Container(
width: 36, height: 36,
width: 36,
height: 36,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.black54),
@ -195,23 +209,33 @@ class _FinishPrivatePageState extends State<FinishPrivatePage> {
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => const Center(child: Text('ERR')),
)
: const Center(child: Text('No\nImg', textAlign: TextAlign.center, style: TextStyle(fontSize: 10))),
: const Center(
child: Text(
'No\nImg',
// '이미지 없음'
textAlign: TextAlign.center,
style: TextStyle(fontSize: 10),
),
),
),
),
const SizedBox(width: 8),
Expanded(
child: Text(nickname, style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
child: Text(
nickname,
style: const TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
),
Text('$score', style: const TextStyle(fontSize: 14)),
Text('$score pt', // '$score'
style: const TextStyle(fontSize: 14)),
],
),
),
);
}
/// ->
/// User click user info finish dialog ( -> )
Future<void> _onTapUser(Map<String, dynamic> userData) async {
// user_info_finish_dialog.dart ( )
await showDialog(
context: context,
barrierDismissible: false,
@ -239,25 +263,27 @@ class _FinishPrivatePageState extends State<FinishPrivatePage> {
padding: const EdgeInsets.all(16),
child: Column(
children: [
// (A) : [ ] +
// (A) Top: [View Room Info] button + game duration
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
//
ElevatedButton(
onPressed: _openRoomSettingDialog,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
side: const BorderSide(color: Colors.black, width: 1),
),
child: const Text('방 정보 보기', style: TextStyle(color: Colors.black)),
child: const Text(
'View Room Info', // '방 정보 보기'
style: TextStyle(color: Colors.black),
),
),
_buildGameTimeWidget(),
],
),
const SizedBox(height: 16),
// (C)
// (C) player list ( )
ListView.builder(
primary: false,
shrinkWrap: true,

View File

@ -10,7 +10,7 @@ import 'main_page.dart';
class FinishTeamPage extends StatefulWidget {
final int roomSeq;
final String enterType; // / =>
final String enterType; // from waiting/ongoing (/) when back, go to MainPage
const FinishTeamPage({
Key? key,
@ -34,12 +34,11 @@ class _FinishTeamPageState extends State<FinishTeamPage> {
DateTime? _startDt;
DateTime? _endDt;
// [ { user }, { user } ... ]
// Teams and their members ( [ { user }, { user } ... ])
Map<String, List<Map<String, dynamic>>> _teamMap = {};
//
// Scores by team ( )
Map<String, int> _teamScoreMap = {};
@override
void initState() {
super.initState();
@ -69,7 +68,8 @@ class _FinishTeamPageState extends State<FinishTeamPage> {
setState(() {
_roomInfo = rInfo;
_userMap = uInfo;
_roomTitle = rTitle.isNotEmpty ? rTitle : '종료된 팀전';
_roomTitle = rTitle.isNotEmpty ? rTitle : 'Finished Team Game';
// '종료된 팀전'
_masterUserSeq = mSeq.toString();
if (sdt != null && sdt is String && sdt.contains('T')) {
@ -86,24 +86,23 @@ class _FinishTeamPageState extends State<FinishTeamPage> {
tempList.add(Map<String, dynamic>.from(val));
});
final players = tempList.toList();
// (3) +
// (3) Separate by team + sum scores ( + )
final Map<String, List<Map<String, dynamic>>> tMap = {};
final Map<String, int> tScoreMap = {};
for (var user in players) {
final tName = (user['team_name'] ?? 'WAIT').toString().toUpperCase();
if (tName == 'WAIT') continue; //
if (tName == 'WAIT') continue;
tMap.putIfAbsent(tName, () => []);
tMap[tName]!.add(user);
}
//
// Calculate team scores ( )
tMap.forEach((team, mems) {
int sumScore = 0;
// mems
// Sort each team's members by descending score (팀 멤버 점수 내림차순)
mems.sort((a, b) {
final sa = (a['score'] ?? 0) as int;
final sb = (b['score'] ?? 0) as int;
@ -115,11 +114,11 @@ class _FinishTeamPageState extends State<FinishTeamPage> {
tScoreMap[team] = sumScore;
});
// (4)
// (4) Sort teams by score ( )
final sortedTeams = tScoreMap.keys.toList();
sortedTeams.sort((a, b) => tScoreMap[b]!.compareTo(tScoreMap[a]!));
//
// Put sorted results into a new map ( )
final Map<String, List<Map<String, dynamic>>> finalTeamMap = {};
final Map<String, int> finalScoreMap = {};
@ -129,21 +128,24 @@ class _FinishTeamPageState extends State<FinishTeamPage> {
}
setState(() {
_userList = tempList; //
_userList = tempList; // Keep the entire list if needed
_teamMap = finalTeamMap;
_teamScoreMap = finalScoreMap;
_isLoading = false;
});
} else {
final msgTitle = resp['response_info']?['msg_title'] ?? '오류';
final msgContent = resp['response_info']?['msg_content'] ?? '데이터 조회 실패';
final msgTitle = resp['response_info']?['msg_title'] ?? 'Error'; // '오류'
final msgContent = resp['response_info']?['msg_content'] ?? 'Failed to fetch data';
// '데이터 조회 실패'
showResponseDialog(context, msgTitle, msgContent);
}
} else {
showResponseDialog(context, '실패', '서버 통신 오류');
showResponseDialog(context, 'Failed', 'Server communication error.');
// '실패', '서버 통신 오류'
}
} catch (e) {
showResponseDialog(context, '오류', '$e');
showResponseDialog(context, 'Error', '$e');
// '오류'
} finally {
setState(() => _isLoading = false);
}
@ -151,8 +153,10 @@ class _FinishTeamPageState extends State<FinishTeamPage> {
Future<bool> _onWillPop() async {
if (widget.enterType == 'game') {
// If from game, go to main (/ )
Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (_) => const MainPage()), (route) => false);
} else {
// Otherwise just pop
Navigator.pop(context);
}
return false;
@ -169,27 +173,29 @@ class _FinishTeamPageState extends State<FinishTeamPage> {
Widget _buildGameTimeWidget() {
if (_startDt == null || _endDt == null) {
return const Text('게임 진행 시간: 00:00', style: TextStyle(fontSize: 14));
return const Text('Game duration: 00:00', // '게임 진행 시간: 00:00'
style: TextStyle(fontSize: 14));
}
final dur = _endDt!.difference(_startDt!);
final hh = dur.inHours.toString().padLeft(2, '0');
final mm = dur.inMinutes.remainder(60).toString().padLeft(2, '0');
return Text('게임 진행 시간: $hh:$mm', style: const TextStyle(fontSize: 14));
return Text('Game duration: $hh:$mm', // '게임 진행 시간: $hh:$mm'
style: const TextStyle(fontSize: 14));
}
/// ( + 123 // )
/// Team box ( : e.g. 1st, 2nd, 3rd with gold/silver/bronze background)
Widget _buildTeamBox(String teamName, int index) {
final members = _teamMap[teamName] ?? [];
final tScore = _teamScoreMap[teamName] ?? 0;
// 1/2/3 ->
// 1st/2nd/3rd team color
Color bgColor = Colors.white;
if (index == 0) {
bgColor = const Color(0xFFFFF9C4); // : amber.shade100
bgColor = const Color(0xFFFFF9C4); // Goldish
} else if (index == 1) {
bgColor = const Color(0xFFE0E0E0); // ()
bgColor = const Color(0xFFE0E0E0); // Silverish
} else if (index == 2) {
bgColor = const Color(0xFFFFE0B2); // (~ )
bgColor = const Color(0xFFFFE0B2); // Bronzy
}
return Container(
@ -201,19 +207,20 @@ class _FinishTeamPageState extends State<FinishTeamPage> {
),
child: Column(
children: [
//
// Header bar ( )
Container(
color: Colors.black,
width: double.infinity,
padding: const EdgeInsets.all(8),
child: Center(
child: Text(
'$teamName 팀 (점수: $tScore)',
'$teamName Team (Score: $tScore)',
// '$teamName 팀 (점수: $tScore)'
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
),
),
//
// Team members ( )
Container(
padding: const EdgeInsets.all(8),
child: SingleChildScrollView(
@ -228,19 +235,19 @@ class _FinishTeamPageState extends State<FinishTeamPage> {
);
}
///
/// Team member display ( )
Widget _buildTeamMember(Map<String, dynamic> user) {
final score = (user['score'] ?? 0) as int;
var nickname = user['nickname'] ?? '유저';
var nickname = user['nickname'] ?? 'User'; // '유저'
final profileImg = user['profile_img'] ?? '';
final userSeq = user['user_seq'].toString() ?? '0';
final participantType = user['participant_type'] ?? '';
// Mark room master ( )
if (_masterUserSeq == userSeq) {
//
nickname = '' + nickname;
} else if (participantType == 'ADMIN') {
//
// Admin
nickname = '' + nickname;
}
@ -255,7 +262,8 @@ class _FinishTeamPageState extends State<FinishTeamPage> {
Text('$score', style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 2),
Container(
width: 30, height: 30,
width: 30,
height: 30,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: Colors.black),
@ -271,7 +279,11 @@ class _FinishTeamPageState extends State<FinishTeamPage> {
),
),
const SizedBox(height: 2),
Text(nickname, style: const TextStyle(fontSize: 11), overflow: TextOverflow.ellipsis),
Text(
nickname,
style: const TextStyle(fontSize: 11),
overflow: TextOverflow.ellipsis,
),
],
),
),
@ -279,7 +291,7 @@ class _FinishTeamPageState extends State<FinishTeamPage> {
}
Future<void> _onTapUser(Map<String, dynamic> userData) async {
//
// Show user info dialog ( )
await showDialog(
context: context,
barrierDismissible: false,
@ -289,8 +301,8 @@ class _FinishTeamPageState extends State<FinishTeamPage> {
@override
Widget build(BuildContext context) {
final teamNames = _teamMap.keys.toList();
//
final teamNames = _teamMap.keys.toList();
// Already sorted by score ( )
return WillPopScope(
onWillPop: _onWillPop,
@ -310,7 +322,7 @@ class _FinishTeamPageState extends State<FinishTeamPage> {
padding: const EdgeInsets.all(16),
child: Column(
children: [
// +
// Room info + time ( + )
Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
@ -320,17 +332,20 @@ class _FinishTeamPageState extends State<FinishTeamPage> {
backgroundColor: Colors.white,
side: const BorderSide(color: Colors.black),
),
child: const Text('방 정보 보기', style: TextStyle(color: Colors.black)),
child: const Text(
'View Room Info',
// '방 정보 보기'
style: TextStyle(color: Colors.black),
),
),
_buildGameTimeWidget(),
],
),
const SizedBox(height: 16),
//
// Team boxes ( )
for (int i = 0; i < teamNames.length; i++)
_buildTeamBox(teamNames[i], i),
],
),
),

View File

@ -1,30 +1,30 @@
import 'package:flutter/material.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:shared_preferences/shared_preferences.dart';
// import...
// Other imports...
import '../../dialogs/settings_dialog.dart';
import 'create_room_page.dart';
import 'room_search_home_page.dart';
//
// Ongoing games ( )
import 'playing_private_page.dart';
import 'playing_team_page.dart';
// : API &
// Temporary: server API & dialogs (: API & )
import '../../plugins/api.dart';
import '../../dialogs/response_dialog.dart';
//
import 'package:fluttertoast/fluttertoast.dart'; // Toast
// For back-button handling ()
import 'package:fluttertoast/fluttertoast.dart'; // Toast for back-button notice
//
// Config ()
import '../../config/config.dart';
//
// Survey ()
import '../../dialogs/survey_dialog.dart';
//
// Ads ()
import '../../plugins/admob.dart';
class MainPage extends StatefulWidget {
@ -35,20 +35,21 @@ class MainPage extends StatefulWidget {
}
class _MainPageState extends State<MainPage> {
//
// Handle back button ( )
DateTime? _lastPressedTime;
// : 2
// e.g., if a second back press occurs within 2 seconds, exit the app (2 )
static const _exitDuration = Duration(seconds: 2);
@override
void initState() {
super.initState();
// (A) FRD
// (A) When entering the main page, disconnect from Firebase Realtime Database
// ( FRD )
FirebaseDatabase.instance.goOffline();
// (B)
// (B) Check landing page info ( )
WidgetsBinding.instance.addPostFrameCallback((_) {
_checkLandingMainPageInfo();
});
@ -59,26 +60,29 @@ class _MainPageState extends State<MainPage> {
super.dispose();
}
// On back press callback ( )
Future<bool> _onWillPop() async {
final now = DateTime.now();
if (_lastPressedTime == null ||
now.difference(_lastPressedTime!) > _exitDuration) {
// or
// First back press or last press was long ago
// ( )
_lastPressedTime = now;
// (Toast )
// Show toast message ( )
Fluttertoast.showToast(
msg: '한 번 더 누르면 앱이 종료됩니다.',
msg: 'Press again to exit the app.', // '한 번 더 누르면 앱이 종료됩니다.'
toastLength: Toast.LENGTH_SHORT,
gravity: ToastGravity.BOTTOM,
);
return false; // pop
return false; // Do not pop the page
}
// 2
return true; // pop (Scaffold , )
// If second press within 2 seconds exit the app (2 )
return true;
}
/// (B) "강제 종료 여부"
/// (B) Check "force exit" from server if there's a room, re-enter
/// ( "강제 종료 여부" )
Future<void> _checkLandingMainPageInfo() async {
try {
final Map<String, dynamic> requestBody = {};
@ -92,35 +96,44 @@ class _MainPageState extends State<MainPage> {
if (resp['result'] == 'OK') {
final data = resp['data'] ?? {};
final forceExitYn = (data['force_exit_yn'] ?? 'N').toString().toUpperCase();
if (forceExitYn == 'Y') {
final int roomSeq = data['room_seq'] ?? 0;
final String roomType = (data['room_type_name'] ?? '').toString().toUpperCase();
final String roomTitle = (data['room_title'] ?? '').toString();
// (1) FRD
if (forceExitYn == 'Y') {
final int roomSeq = data['room_seq'] ?? 0;
final String roomType = (data['room_type_name'] ?? '').toString().toUpperCase();
final String roomTitle = (data['room_title'] ?? '').toString();
// (1) Restore FRD connection before re-entering ( FRD )
FirebaseDatabase.instance.goOnline();
showResponseDialog(context, '게임 재입장', '강제 종료 된 게임에 재입장 합니다.');
showResponseDialog(
context,
'Re-enter Game', // '게임 재입장'
'You will re-enter the forcibly closed game.' // '강제 종료 된 게임에 재입장 합니다.'
);
// (2) pushReplacement
// (2) Navigate based on the room type ( )
if (roomType == 'TEAM') {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => PlayingTeamPage(
roomSeq: roomSeq,
roomTitle: roomTitle.isNotEmpty ? roomTitle : '재입장 (팀전)',
roomTitle: roomTitle.isNotEmpty
? roomTitle
: 'Re-enter (Team)', // '재입장 (팀전)'
),
),
);
} else {
//
// Private
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => PlayingPrivatePage(
roomSeq: roomSeq,
roomTitle: roomTitle.isNotEmpty ? roomTitle : '재입장 (개인전)',
roomTitle: roomTitle.isNotEmpty
? roomTitle
: 'Re-enter (Private)', // '재입장 (개인전)'
),
),
);
@ -131,22 +144,34 @@ class _MainPageState extends State<MainPage> {
final prefs = await SharedPreferences.getInstance();
final surveyShownToday = prefs.getString('survey_popup_today') ?? 'N';
final nickname = (data['nickname'] ?? '').toString();
// If user hasn't taken the survey, they have joined a game before,
// and haven't shown survey today → show popup
if (surveyYn == 'N' && joinGameYn == 'Y' && surveyShownToday == 'N') {
//
Future.delayed(Duration.zero, () {
showSurveyDialog(context, nickname);
showSurveyDialog(context, nickname);
// '설문조사 팝업 표시'
});
}
} else {
final msgTitle = resp['response_info']?['msg_title'] ?? '오류';
final msgContent = resp['response_info']?['msg_content'] ?? '강제 종료 여부 확인 실패';
final msgTitle = resp['response_info']?['msg_title'] ?? 'Error'; // '오류'
final msgContent = resp['response_info']?['msg_content'] ?? 'Failed to check forced exit';
// '강제 종료 여부 확인 실패'
showResponseDialog(context, msgTitle, msgContent);
}
} else {
showResponseDialog(context, '서버 오류', '강제 종료 여부 확인에 실패했습니다.');
showResponseDialog(
context,
'Server Error', // '서버 오류'
'Failed to check forced exit.' // '강제 종료 여부 확인에 실패했습니다.'
);
}
} catch (e) {
showResponseDialog(context, '서버 오류', '강제 종료 여부 확인에 실패했습니다.');
showResponseDialog(
context,
'Server Error', // '서버 오류'
'Failed to check forced exit.' // '강제 종료 여부 확인에 실패했습니다.'
);
}
}
@ -155,62 +180,63 @@ class _MainPageState extends State<MainPage> {
return WillPopScope(
onWillPop: _onWillPop,
child: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.black,
elevation: 0,
automaticallyImplyLeading: false, // X
title: const Text(
'ALLSCORE',
style: TextStyle(color: Colors.white),
),
actions: [
IconButton(
icon: const Icon(Icons.settings, color: Colors.white),
onPressed: () {
showSettingsDialog(context);
},
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.black,
elevation: 0,
automaticallyImplyLeading: false, // Do not show back button ( X)
title: const Text(
'ALLSCORE',
style: TextStyle(color: Colors.white),
),
],
),
bottomNavigationBar: AdBannerWidget(),
actions: [
IconButton(
icon: const Icon(Icons.settings, color: Colors.white),
onPressed: () {
// Open settings dialog ( )
showSettingsDialog(context);
},
),
],
),
body: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
//
Expanded(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildBlackWhiteButton(
label: '방만들기',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const CreateRoomPage()),
);
},
),
const SizedBox(width: 16),
_buildBlackWhiteButton(
label: '참여하기',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const RoomSearchHomePage()),
);
},
),
],
bottomNavigationBar: AdBannerWidget(),
body: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Middle buttons ( )
Expanded(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildBlackWhiteButton(
label: 'Create\nRoom', // '방만들기'
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const CreateRoomPage()),
);
},
),
const SizedBox(width: 16),
_buildBlackWhiteButton(
label: 'Join', // '참여하기'
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const RoomSearchHomePage()),
);
},
),
],
),
),
),
),
),
],
),
),
@ -224,8 +250,8 @@ class _MainPageState extends State<MainPage> {
return ElevatedButton(
onPressed: onTap,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
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)),

View File

@ -10,7 +10,6 @@ import '../../dialogs/response_dialog.dart';
import '../../dialogs/score_edit_dialog.dart'; //
import '../../dialogs/user_info_basic_dialog.dart'; //
import '../../plugins/admob.dart';
import '../../config/config.dart';
class PlayingPrivatePage extends StatefulWidget {
@ -28,37 +27,36 @@ class PlayingPrivatePage extends StatefulWidget {
}
class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
// FRD
// Firebase Realtime Database (FRD)
late DatabaseReference _roomRef;
Stream<DatabaseEvent>? _roomStream;
StreamSubscription<DatabaseEvent>? _roomStreamSubscription;
//
//
// One-hour countdown ( )
//
Timer? _countdownTimer;
Duration _remaining = const Duration(hours: 1); // 1
DateTime? _roomStartDt; // FRD의 roomInfo.room_start_dt
DateTime? _roomStartDt; // roomInfo.room_start_dt
String _roomRunningTime = '0'; //
bool _roomTimeOut = false;
String _roomExitYn = 'N';
String roomMasterYn = 'N';
String roomMasterYn = 'N'; //
String roomTitle = '';
int myScore = 0;
List<Map<String, dynamic>> _scoreList = [];
bool _isLoading = true;
//
bool _movedToFinishPage = false;
bool _movedToFinishPage = false; //
//
String scoreOpenRange = 'ALL';
String mySeq = '0';
//
// My participant type ( )
String myParticipantType = 'PLAYER';
// userListMap: { userSeq: true/false }
@ -67,11 +65,11 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
@override
void initState() {
super.initState();
// (1) FRD
// (1) Restore FRD connection (FRD )
FirebaseDatabase.instance.goOnline();
roomTitle = widget.roomTitle;
// (D)
// (D) Initialize room info ( )
_initFirebase();
}
@ -99,7 +97,7 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
if (!snapshot.exists) {
setState(() {
_isLoading = false;
roomTitle = '방 정보 없음';
roomTitle = 'No Room Info'; // '방 정보 없음'
_scoreList = [];
myScore = 0;
});
@ -109,12 +107,12 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
final data = snapshot.value as Map<dynamic, dynamic>? ?? {};
final roomInfoData = data['roomInfo'] as Map<dynamic, dynamic>? ?? {};
final userInfoData = data['userInfo'] as Map<dynamic, dynamic>? ?? {};
final userListData = data['userList'] as Map<dynamic, dynamic>? ?? {};
final userListData = data['userList'] as Map<dynamic, dynamic>? ?? {};
//
// Check room status ( )
final roomStatus = (roomInfoData['room_status'] ?? 'WAIT').toString().toUpperCase();
if (roomStatus == 'FINISH') {
//
// If finished, move to finish page ( )
if (mounted) {
Navigator.pushAndRemoveUntil(
context,
@ -126,15 +124,16 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
}
setState(() {
//
// Running time ()
_roomRunningTime = roomInfoData['running_time'] ?? '0';
//
// Score open range ( )
scoreOpenRange = roomInfoData['score_open_range'] ?? 'ALL';
//
// Check if master ( )
final masterSeq = roomInfoData['master_user_seq'];
roomMasterYn = (masterSeq != null && masterSeq.toString() == mySeq) ? 'Y' : 'N';
//
// Room title ( )
final newTitle = (roomInfoData['room_title'] ?? '') as String;
if (newTitle.isNotEmpty) roomTitle = newTitle;
@ -146,34 +145,35 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
});
}
//
// Build user list ( )
final List<Map<String, dynamic>> rawList = [];
userInfoData.forEach((uSeq, uData) {
uSeq = uSeq.toString().replaceAll('_', '');
//
// Mark room master ( )
if (uSeq == roomInfoData['master_user_seq'].toString()) {
uData['nickname'] = '' + (uData['nickname'] ?? '유저');
uData['nickname'] = '' + (uData['nickname'] ?? 'User');
// '유저'
} else if ((uData['participant_type'] ?? '').toString().toUpperCase() == 'ADMIN') {
//
uData['nickname'] = '' + (uData['nickname'] ?? '유저');
// Mark admin ( )
uData['nickname'] = '' + (uData['nickname'] ?? 'User');
}
rawList.add({
'user_seq': uSeq,
'participant_type': (uData['participant_type'] ?? '').toString().toUpperCase(),
'nickname': uData['nickname'] ?? '유저',
'nickname': uData['nickname'] ?? 'User', // '유저'
'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',
});
if (uSeq.toString() == mySeq) {
myParticipantType = (uData['participant_type'] ?? '').toString().toUpperCase();
}
});
//
// Find my score ( )
int tmpMyScore = 0;
for (var user in rawList) {
if ((user['is_my_score'] ?? 'N') == 'Y') {
@ -181,9 +181,8 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
}
}
// ADMIN
// Sort by score descending ( )
final playerList = rawList.toList();
//
playerList.sort((a, b) {
final scoreA = a['score'] ?? 0;
final scoreB = b['score'] ?? 0;
@ -195,24 +194,19 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
_isLoading = false;
});
// =>
// If FINISH => move
if (roomStatus == 'FINISH' && !_movedToFinishPage) {
_movedToFinishPage = true;
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(
builder: (_) => FinishPrivatePage(
roomSeq: widget.roomSeq,
enterType: 'game',
),
),
MaterialPageRoute(builder: (_) => FinishPrivatePage(roomSeq: widget.roomSeq, enterType: 'game')),
(route) => false,
);
return;
}
// room_start_dt
// For countdown (room_start_dt) ( )
final roomStartDtStr = (roomInfoData['start_dt'] ?? '') as String;
if (roomStartDtStr.isNotEmpty) {
final dt = DateTime.tryParse(roomStartDtStr);
@ -226,29 +220,29 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
}, onError: (err) {
setState(() {
_isLoading = false;
roomTitle = '오류 발생';
roomTitle = 'Error occurred'; // '오류 발생'
});
});
}
//
// Countdown timer ( )
void _startCountdownTimer() {
if (_countdownTimer != null && _countdownTimer!.isActive) {
return; //
return;
}
if (_roomStartDt == null) return;
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
// : roomStartDt +
// End time = roomStartDt + running time (roomStartDt + )
final endTime = _roomStartDt!.add(Duration(hours: int.parse(_roomRunningTime)));
final now = DateTime.now();
final diff = endTime.difference(now);
if (diff.isNegative) {
// ->
// Time's up -> auto finish
timer.cancel();
_remaining = const Duration(seconds: 0);
_onAutoTimeout();
_onAutoTimeout();
} else {
setState(() {
_remaining = diff;
@ -257,10 +251,8 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
});
}
//
// Auto finish when time is up ( )
void _onAutoTimeout() {
// => (leave API)
// =>
setState(() {
_roomTimeOut = true;
_roomExitYn = 'Y';
@ -268,7 +260,7 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
_requestFinish();
}
/// Finish API
/// If master, call finish API ( Finish API)
Future<void> _requestFinish() async {
final reqBody = {
"room_seq": "${widget.roomSeq}",
@ -281,10 +273,10 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
}
}
///
/// On back press ()
Future<bool> _onWillPop() async {
if (roomMasterYn == 'Y') {
// ->
// If master ()
final confirm = await showDialog<bool>(
context: context,
barrierDismissible: false,
@ -295,9 +287,13 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
borderRadius: BorderRadius.circular(16),
side: const BorderSide(color: Colors.black, width: 1),
),
title: const Text('나가기', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
title: const Text(
'Leave', // '나가기'
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
content: const Text(
'방장이 나가면 게임이 종료됩니다.\n정말 나가시겠습니까?',
'If the master leaves, the game will end.\nAre you sure you want to leave?',
// '방장이 나가면 게임이 종료됩니다.\n정말 나가시겠습니까?'
style: TextStyle(fontSize: 14),
),
actions: [
@ -307,7 +303,7 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
backgroundColor: Colors.black,
foregroundColor: Colors.white,
),
child: const Text('취소'),
child: const Text('Cancel'), // '취소'
),
TextButton(
onPressed: () => Navigator.pop(context, true),
@ -315,19 +311,18 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
backgroundColor: Colors.black,
foregroundColor: Colors.white,
),
child: const Text('종료'),
child: const Text('End'), // '종료'
),
],
);
},
);
if (confirm != true) return false;
// Finish API
await _requestFinish();
} else {
//
// If not master ( )
final confirm = await showDialog<bool>(
context: context,
barrierDismissible: false,
@ -338,9 +333,13 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
borderRadius: BorderRadius.circular(16),
side: const BorderSide(color: Colors.black, width: 1),
),
title: const Text('나가기', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
title: const Text(
'Leave', // '나가기'
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
content: const Text(
'진행중인 방에서 나가면 재입장이 불가능합니다.\n정말 나가시겠습니까?',
'If you leave this ongoing game, you cannot rejoin.\nAre you sure you want to leave?',
// '진행중인 방에서 나가면 재입장이 불가능합니다.\n정말 나가시겠습니까?'
style: TextStyle(fontSize: 14),
),
actions: [
@ -350,7 +349,7 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
backgroundColor: Colors.black,
foregroundColor: Colors.white,
),
child: const Text('취소'),
child: const Text('Cancel'), // '취소'
),
TextButton(
onPressed: () => Navigator.pop(context, true),
@ -358,7 +357,7 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
backgroundColor: Colors.black,
foregroundColor: Colors.white,
),
child: const Text('나가기'),
child: const Text('Leave'), // '나가기'
),
],
);
@ -379,12 +378,12 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
return false;
}
///
/// Build participant card ( )
Widget _buildScoreItem(Map<String, dynamic> user) {
final userSeq = user['user_seq'].toString();
final tempScore = user['score'] ?? 0;
final tempScore = user['score'] ?? 0;
final score = (scoreOpenRange == 'ALL') ? tempScore : '-';
final nickname = user['nickname'] ?? '유저';
final nickname = user['nickname'] ?? 'User'; // '유저'
final bool isActive = _userListMap[userSeq] ?? true;
final hasExited = !isActive;
@ -398,31 +397,45 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
mainAxisSize: MainAxisSize.min,
children: [
hasExited
? Text('X', style: const TextStyle(fontSize: 20, color: Colors.redAccent, fontWeight: FontWeight.bold))
: Text('$score', style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
? Text(
'X',
// 'X'
style: const TextStyle(fontSize: 20, color: Colors.redAccent, fontWeight: FontWeight.bold),
)
: Text(
'$score',
style: const TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 2),
Container(
width: 30, height: 30,
width: 30,
height: 30,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: hasExited ? Colors.redAccent : Colors.black),
),
child: hasExited
? const Center(
child: Text('X', style: TextStyle(fontSize: 14, color: Colors.redAccent, fontWeight: FontWeight.bold)),
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: (_, __, ___) => const Center(child: Text('ERR', style: TextStyle(fontSize: 8))),
errorBuilder: (_, __, ___) =>
const Center(child: Text('ERR', style: TextStyle(fontSize: 8))),
),
),
),
const SizedBox(height: 2),
Text(nickname,
style: TextStyle(fontSize: 11, color: hasExited ? Colors.redAccent : Colors.black),
overflow: TextOverflow.ellipsis),
Text(
nickname,
style: TextStyle(fontSize: 11, color: hasExited ? Colors.redAccent : Colors.black),
overflow: TextOverflow.ellipsis,
),
],
),
),
@ -430,8 +443,8 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
}
Future<void> _onTapUser(Map<String, dynamic> userData) async {
if (myParticipantType == 'ADMIN') {
//
if (myParticipantType == 'ADMIN') {
// If admin ( )
await showDialog(
context: context,
builder: (_) => ScoreEditDialog(
@ -441,7 +454,7 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
),
);
} else {
//
// Normal user info ( )
await showDialog(
context: context,
builder: (_) => UserInfoBasicDialog(userData: userData),
@ -449,7 +462,7 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
}
}
// ()
// () Format countdown display ( )
String _formatDuration(Duration d) {
final mm = d.inMinutes.remainder(60).toString().padLeft(2, '0');
final ss = d.inSeconds.remainder(60).toString().padLeft(2, '0');
@ -459,6 +472,7 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
@override
Widget build(BuildContext context) {
final countdownStr = _formatDuration(_remaining);
return WillPopScope(
onWillPop: _onWillPop,
child: Scaffold(
@ -471,25 +485,32 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
onPressed: () => _onWillPop(),
),
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// :
Text(
roomTitle.isNotEmpty ? roomTitle : '방 제목',
style: const TextStyle(color: Colors.white),
),
// :
Text(
countdownStr, // : "10:23"
style: const TextStyle(color: Colors.white),
),
],
),
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Expanded(
child: Text(
roomTitle.isNotEmpty ? roomTitle : 'Room Title', // '방 제목'
style: const TextStyle(color: Colors.white),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 4),
// Right: countdown ()
Text(
countdownStr, // e.g. "10:23"
style: const TextStyle(color: Colors.white),
),
],
),
actions: [
if (roomMasterYn == 'Y')
TextButton(
onPressed: _requestFinish,
child: const Text('게임종료', style: TextStyle(color: Colors.white)),
child: const Text(
'End Game', // '게임종료'
style: TextStyle(color: Colors.white),
),
),
],
),
@ -497,15 +518,21 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
? const Center(child: CircularProgressIndicator())
: Column(
children: [
//
// My score ( )
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
children: [
const Text('내 점수', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
const Text(
'My Score', // '내 점수'
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Text('$myScore', style: const TextStyle(fontSize: 36, fontWeight: FontWeight.bold)),
Text(
'$myScore',
style: const TextStyle(fontSize: 36, fontWeight: FontWeight.bold),
),
],
),
),
@ -523,7 +550,7 @@ class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
],
),
bottomNavigationBar: AdBannerWidget(),
),
),
);
}
}

View File

@ -4,7 +4,7 @@ import 'package:firebase_database/firebase_database.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'main_page.dart';
import 'finish_team_page.dart'; //
import 'finish_team_page.dart'; //
import '../../plugins/api.dart';
import '../../dialogs/response_dialog.dart';
import '../../dialogs/score_edit_dialog.dart';
@ -27,32 +27,32 @@ class PlayingTeamPage extends StatefulWidget {
}
class _PlayingTeamPageState extends State<PlayingTeamPage> {
// FRD
// Firebase Realtime Database (FRD)
late DatabaseReference _roomRef;
Stream<DatabaseEvent>? _roomStream;
StreamSubscription<DatabaseEvent>? _roomStreamSubscription;
//
//
// One-hour countdown ( )
//
Timer? _countdownTimer;
Duration _remaining = const Duration(hours: 1); // 1
DateTime? _roomStartDt; // FRD의 roomInfo.room_start_dt
String _roomRunningTime = '0'; // FRD의 roomInfo.running_time
DateTime? _roomStartDt; // roomInfo.room_start_dt
String _roomRunningTime = '0';
bool _roomTimeOut = false;
String _roomExitYn = 'N';
String roomMasterYn = 'N';
String roomMasterYn = 'N'; //
String roomTitle = '';
int myScore = 0;
int myTeamScore = 0;
Map<String, int> _teamScoreMap = {};
Map<String, List<Map<String, dynamic>>> _teamMap = {};
Map<String, int> _teamScoreMap = {};
Map<String, List<Map<String, dynamic>>> _teamMap = {};
bool _isLoading = true;
//
bool _movedToFinishPage = false;
bool _movedToFinishPage = false; //
String mySeq = '0';
@ -68,11 +68,11 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
@override
void initState() {
super.initState();
// (1) FRD
// (1) Restore FRD connection (FRD )
FirebaseDatabase.instance.goOnline();
roomTitle = widget.roomTitle;
// (D)
// (D) Initialize room info ( )
_initFirebase();
}
@ -100,7 +100,7 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
if (!snapshot.exists) {
setState(() {
_isLoading = false;
roomTitle = '방 정보 없음';
roomTitle = 'No Room Info'; // '방 정보 없음'
myScore = 0;
myTeamScore = 0;
_teamScoreMap = {};
@ -116,7 +116,7 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
final roomStatus = (roomInfoData['room_status'] ?? 'WAIT').toString().toUpperCase();
if (roomStatus == 'FINISH') {
//
// If finished, move to finish screen ()
if (mounted) {
Navigator.pushAndRemoveUntil(
context,
@ -128,10 +128,10 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
}
setState(() {
//
// Room running time ()
_roomRunningTime = roomInfoData['running_time'] ?? '0';
//
// Score open range ( )
scoreOpenRange = roomInfoData['score_open_range'] ?? 'ALL';
final masterSeq = roomInfoData['master_user_seq'];
@ -147,21 +147,22 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
});
}
//
// Create raw user list ( )
final List<Map<String, dynamic>> rawList = [];
userInfoData.forEach((uSeq, uData) {
uSeq = uSeq.toString().replaceAll('_', '');
//
// Mark room master
if (uSeq.toString() == roomInfoData['master_user_seq'].toString()) {
uData['nickname'] = '' + (uData['nickname'] ?? '유저');
//
uData['nickname'] = '' + (uData['nickname'] ?? 'User');
} else if ((uData['participant_type'] ?? '').toString().toUpperCase() == 'ADMIN') {
//
uData['nickname'] = '' + (uData['nickname'] ?? '유저');
// ()
uData['nickname'] = '' + (uData['nickname'] ?? 'User');
}
rawList.add({
'user_seq': uSeq,
'participant_type': (uData['participant_type'] ?? '').toString().toUpperCase(),
'nickname': uData['nickname'] ?? '유저',
'nickname': uData['nickname'] ?? 'User', // '유저'
'team_name': (uData['team_name'] ?? '').toString().toUpperCase(),
'score': uData['score'] ?? 0,
});
@ -170,7 +171,7 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
}
});
// &
// My score & team score ( & )
int tmpMyScore = 0;
int tmpMyTeamScore = 0;
String myTeam = 'WAIT';
@ -186,27 +187,26 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
}
for (var user in rawList) {
final tName = user['team_name'] ?? 'WAIT';
final tName = (user['team_name'] ?? 'WAIT');
final sc = (user['score'] ?? 0) as int;
if (tName == myTeam && myTeam != 'WAIT') {
tmpMyTeamScore += sc;
}
}
// ADMIN, WAIT
// Build team map excluding ADMIN, WAIT (ADMIN, WAIT )
final Map<String, List<Map<String, dynamic>>> tMap = {};
final Map<String, int> tScoreMap = {};
for (var user in rawList) {
final pType = user['participant_type'];
final tName = (user['team_name'] ?? 'WAIT');
// if (pType == 'ADMIN') continue;
if (tName == 'WAIT') continue;
tMap.putIfAbsent(tName, () => []);
tMap[tName]!.add(user);
}
// Calculate team score ( )
tMap.forEach((k, members) {
int sumScore = 0;
for (var m in members) {
@ -222,8 +222,8 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
_isLoading = false;
});
// =>
// If FINISH => move to finish page
if (roomStatus == 'FINISH' && !_movedToFinishPage) {
_movedToFinishPage = true;
Navigator.pushAndRemoveUntil(
@ -239,7 +239,7 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
return;
}
// room_start_dt
// Parse room_start_dt for countdown ( )
final roomStartDtStr = (roomInfoData['start_dt'] ?? '') as String;
if (roomStartDtStr.isNotEmpty) {
final dt = DateTime.tryParse(roomStartDtStr);
@ -253,29 +253,29 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
}, onError: (err) {
setState(() {
_isLoading = false;
roomTitle = '오류 발생';
roomTitle = 'Error occurred'; // '오류 발생'
});
});
}
//
// Countdown timer for running time ( )
void _startCountdownTimer() {
if (_countdownTimer != null && _countdownTimer!.isActive) {
return; //
return;
}
if (_roomStartDt == null) return;
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
// : roomStartDt +
// End time = roomStartDt + running time (roomStartDt + )
final endTime = _roomStartDt!.add(Duration(hours: int.parse(_roomRunningTime)));
final now = DateTime.now();
final diff = endTime.difference(now);
if (diff.isNegative) {
// ->
// Time's up -> auto finish
timer.cancel();
_remaining = const Duration(seconds: 0);
_onAutoTimeout();
_onAutoTimeout();
} else {
setState(() {
_remaining = diff;
@ -284,10 +284,8 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
});
}
//
// Auto finish when time is up ( )
void _onAutoTimeout() {
// => (leave API)
// =>
setState(() {
_roomTimeOut = true;
_roomExitYn = 'Y';
@ -295,7 +293,7 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
_requestFinish();
}
///
/// Request finish the game ()
Future<void> _requestFinish() async {
final body = {
"room_seq": "${widget.roomSeq}",
@ -308,9 +306,10 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
}
}
// On pressing back ()
Future<bool> _onWillPop() async {
if (roomMasterYn == 'Y') {
//
// If master, prompt ( )
final confirm = await showDialog<bool>(
context: context,
barrierDismissible: false,
@ -321,9 +320,13 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
borderRadius: BorderRadius.circular(16),
side: const BorderSide(color: Colors.black, width: 1),
),
title: const Text('나가기', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
title: const Text(
'Leave', // '나가기'
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
content: const Text(
'방장이 나가면 게임이 종료됩니다.\n정말 나가시겠습니까?',
'If the master leaves, the game will end.\nAre you sure you want to leave?',
// '방장이 나가면 게임이 종료됩니다.\n정말 나가시겠습니까?'
style: TextStyle(fontSize: 14),
),
actions: [
@ -333,7 +336,7 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
backgroundColor: Colors.black,
foregroundColor: Colors.white,
),
child: const Text('취소'),
child: const Text('Cancel'), // '취소'
),
TextButton(
onPressed: () => Navigator.pop(context, true),
@ -341,7 +344,7 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
backgroundColor: Colors.black,
foregroundColor: Colors.white,
),
child: const Text('종료'),
child: const Text('End'), // '종료'
),
],
);
@ -351,7 +354,7 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
await _requestFinish();
} else {
//
// If not master ( )
final confirm = await showDialog<bool>(
context: context,
barrierDismissible: false,
@ -362,9 +365,13 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
borderRadius: BorderRadius.circular(16),
side: const BorderSide(color: Colors.black, width: 1),
),
title: const Text('나가기', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
title: const Text(
'Leave', // '나가기'
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
content: const Text(
'진행중인 방에서 나가면 재입장이 불가능합니다.\n정말 나가시겠습니까?',
'If you leave this ongoing game, you cannot rejoin.\nAre you sure you want to leave?',
// '진행중인 방에서 나가면 재입장이 불가능합니다.\n정말 나가시겠습니까?'
style: TextStyle(fontSize: 14),
),
actions: [
@ -374,7 +381,7 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
backgroundColor: Colors.black,
foregroundColor: Colors.white,
),
child: const Text('취소'),
child: const Text('Cancel'), // '취소'
),
TextButton(
onPressed: () => Navigator.pop(context, true),
@ -382,7 +389,7 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
backgroundColor: Colors.black,
foregroundColor: Colors.white,
),
child: const Text('나가기'),
child: const Text('Leave'), // '나가기'
),
],
);
@ -400,7 +407,7 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
return false;
}
// ()
// Format countdown ( )
String _formatDuration(Duration d) {
final mm = d.inMinutes.remainder(60).toString().padLeft(2, '0');
final ss = d.inSeconds.remainder(60).toString().padLeft(2, '0');
@ -410,26 +417,32 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
@override
Widget build(BuildContext context) {
final countdownStr = _formatDuration(_remaining);
return WillPopScope(
onWillPop: _onWillPop,
child: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// :
Text(
roomTitle.isNotEmpty ? roomTitle : '방 제목',
style: const TextStyle(color: Colors.white),
),
// :
Text(
countdownStr, // : "10:23"
style: const TextStyle(color: Colors.white),
),
],
),
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Left: room title ( )
Expanded(
child: Text(
roomTitle.isNotEmpty ? roomTitle : 'Room Title', // '방 제목'
style: const TextStyle(color: Colors.white),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 4),
// Right: countdown ()
Text(
countdownStr, // e.g. "10:23"
style: const TextStyle(color: Colors.white),
),
],
),
backgroundColor: Colors.black,
elevation: 0,
leading: IconButton(
@ -440,7 +453,10 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
if (roomMasterYn == 'Y')
TextButton(
onPressed: _requestFinish,
child: const Text('게임종료', style: TextStyle(color: Colors.white)),
child: const Text(
'End Game', // '게임종료'
style: TextStyle(color: Colors.white),
),
),
],
),
@ -448,7 +464,7 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
? const Center(child: CircularProgressIndicator())
: Column(
children: [
// (A) /
// (A) My score / my team's score (내 점수 / 우리 팀 점수)
Container(
color: Colors.white,
padding: const EdgeInsets.only(top: 16, bottom: 16),
@ -457,17 +473,29 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
children: [
Column(
children: [
const Text('내 점수', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
const Text(
'My Score', // '내 점수'
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Text('$myScore', style: const TextStyle(fontSize: 36, fontWeight: FontWeight.bold)),
Text(
'$myScore',
style: const TextStyle(fontSize: 36, fontWeight: FontWeight.bold),
),
],
),
Container(width: 1, height: 60, color: Colors.black),
Column(
children: [
const Text('우리 팀 점수', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold)),
const Text(
'Team Score', // '우리 팀 점수'
style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Text('$myTeamScore', style: const TextStyle(fontSize: 36, fontWeight: FontWeight.bold)),
Text(
'$myTeamScore',
style: const TextStyle(fontSize: 36, fontWeight: FontWeight.bold),
),
],
),
],
@ -475,7 +503,7 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
),
const Divider(height: 1, color: Colors.black),
// (B)
// (B) Teams section ( )
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(8),
@ -496,6 +524,8 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
final upperName = teamName.toUpperCase();
final members = _teamMap[upperName] ?? [];
final tempTeamScore = _teamScoreMap[upperName] ?? 0;
// If scoreOpenRange == 'TEAM' or 'ALL', show team score, else '-'
final teamScore = (scoreOpenRange == 'TEAM' || scoreOpenRange == 'ALL') ? tempTeamScore : '-';
return Container(
@ -513,8 +543,10 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 8),
child: Center(
child: Text('$teamName (팀점수 $teamScore)',
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
child: Text(
'$teamName (Team Score $teamScore)', // '$teamName (팀점수 $teamScore)'
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
),
),
Container(
@ -531,9 +563,9 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
Widget _buildTeamMemberItem(Map<String, dynamic> userData) {
final userSeq = userData['user_seq'].toString();
final tempScore = userData['score'] ?? 0;
final tempScore = userData['score'] ?? 0;
final score = (scoreOpenRange == 'ALL') ? tempScore : '-';
final nickname= userData['nickname'] ?? '유저';
final nickname = userData['nickname'] ?? 'User'; // '유저'
final bool isActive = _userListMap[userSeq] ?? true;
final hasExited = !isActive;
@ -547,8 +579,15 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
mainAxisSize: MainAxisSize.min,
children: [
hasExited
? Text('X', style: const TextStyle(fontSize: 18, color: Colors.redAccent, fontWeight: FontWeight.bold))
: Text('$score', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
? Text(
'X',
// 'X'
style: const TextStyle(fontSize: 18, color: Colors.redAccent, fontWeight: FontWeight.bold),
)
: Text(
'$score',
style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 2),
Container(
width: 30,
@ -568,15 +607,23 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
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))),
errorBuilder: (ctx, err, st) => const Center(
child: Text(
'ERR',
// 'ERR'
style: TextStyle(fontSize: 8),
),
),
),
),
),
const SizedBox(height: 2),
Text(
nickname,
style: TextStyle(fontSize: 11, color: hasExited ? Colors.redAccent : Colors.black),
style: TextStyle(
fontSize: 11,
color: hasExited ? Colors.redAccent : Colors.black,
),
overflow: TextOverflow.ellipsis,
),
],
@ -586,7 +633,7 @@ class _PlayingTeamPageState extends State<PlayingTeamPage> {
}
Future<void> _onTapUser(Map<String, dynamic> userData) async {
if (myParticipantType == 'ADMIN') {
if (myParticipantType == 'ADMIN') { //
await showDialog(
context: context,
builder: (_) => ScoreEditDialog(

View File

@ -1,10 +1,10 @@
import 'package:flutter/material.dart';
import 'room_search_list_page.dart';
//
// Config ()
import '../../config/config.dart';
//
// Ads ()
import '../../plugins/admob.dart';
class RoomSearchHomePage extends StatefulWidget {
@ -15,7 +15,7 @@ class RoomSearchHomePage extends StatefulWidget {
}
class _RoomSearchHomePageState extends State<RoomSearchHomePage> {
//
// Adjust font size by screen width ( )
double scaleFactor = 1.0;
@override
@ -33,8 +33,8 @@ class _RoomSearchHomePageState extends State<RoomSearchHomePage> {
super.didChangeDependencies();
_updateScaleFactor();
}
//
// Adjust font size by screen width ( )
void _updateScaleFactor() {
final screenWidth = MediaQuery.of(context).size.width;
const baseWidth = 450.0;
@ -51,33 +51,39 @@ class _RoomSearchHomePageState extends State<RoomSearchHomePage> {
appBar: AppBar(
backgroundColor: Colors.black,
elevation: 0,
title: const Text('방 검색', style: TextStyle(color: Colors.white)),
title: const Text(
'Room Search', // '방 검색'
style: TextStyle(color: Colors.white),
),
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
),
//
// Bottom ad banner ( )
bottomNavigationBar: AdBannerWidget(),
// : 3 ( / / )
// Main body: 3 buttons in the center ( / / )
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildSearchStatusButton(context, label: '대기중', status: 'WAIT'),
_buildSearchStatusButton(context, label: 'Waiting', status: 'WAIT'),
// '대기중'
const SizedBox(width: 16),
_buildSearchStatusButton(context, label: '진행중', status: 'RUNNING'),
_buildSearchStatusButton(context, label: 'Running', status: 'RUNNING'),
// '진행중'
const SizedBox(width: 16),
_buildSearchStatusButton(context, label: '종료', status: 'FINISH'),
_buildSearchStatusButton(context, label: 'Finished', status: 'FINISH'),
// '종료'
],
),
),
);
}
///
/// Build a button with same size ( )
Widget _buildSearchStatusButton(
BuildContext context, {
required String label,
@ -88,7 +94,7 @@ class _RoomSearchHomePageState extends State<RoomSearchHomePage> {
height: 100 * scaleFactor,
child: ElevatedButton(
onPressed: () {
// RoomSearchListPage로 , roomStatus
// Navigate to RoomSearchListPage with roomStatus (RoomSearchListPage로 roomStatus )
Navigator.push(
context,
MaterialPageRoute(
@ -103,7 +109,10 @@ class _RoomSearchHomePageState extends State<RoomSearchHomePage> {
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
padding: EdgeInsets.zero,
),
child: Text(label, style: const TextStyle(color: Colors.black)),
child: Text(
label,
style: const TextStyle(color: Colors.black),
),
),
);
}

View File

@ -5,15 +5,14 @@ import '../../plugins/api.dart';
import '../../dialogs/response_dialog.dart';
import '../../dialogs/room_detail_dialog.dart';
// ( import)
//
// Import for finished () rooms
import '../room/finish_private_page.dart';
import '../room/finish_team_page.dart';
//
// Config
import '../../config/config.dart';
//
// Ads
import '../../plugins/admob.dart';
class RoomSearchListPage extends StatefulWidget {
@ -30,8 +29,8 @@ class _RoomSearchListPageState extends State<RoomSearchListPage> {
List<Map<String, dynamic>> _roomList = [];
bool _isLoading = false;
bool _hasMore = true;
bool _isLoading = false; //
bool _hasMore = true; //
int _currentPage = 1;
final int _pageSize = 10;
@ -40,7 +39,6 @@ class _RoomSearchListPageState extends State<RoomSearchListPage> {
@override
void initState() {
super.initState();
_scrollController = ScrollController()..addListener(_onScroll);
_fetchRoomList(isRefresh: true);
}
@ -53,17 +51,20 @@ class _RoomSearchListPageState extends State<RoomSearchListPage> {
super.dispose();
}
// Scroll event listener ( )
void _onScroll() {
if (!_scrollController.hasClients) return;
final thresholdPixels = 200;
final maxScroll = _scrollController.position.maxScrollExtent;
final currentScroll = _scrollController.position.pixels;
// If near bottom, load more ( )
if (maxScroll - currentScroll <= thresholdPixels) {
_fetchRoomList(isRefresh: false);
}
}
// Fetch room list ( )
Future<void> _fetchRoomList({required bool isRefresh}) async {
if (_isLoading) return;
if (!isRefresh && !_hasMore) return;
@ -76,7 +77,8 @@ class _RoomSearchListPageState extends State<RoomSearchListPage> {
_roomList.clear();
}
final String searchType = widget.roomStatus.toUpperCase();
final String searchType = widget.roomStatus.toUpperCase();
// ex) WAIT, RUNNING, FINISH
final String searchValue = _searchController.text.trim();
final String searchPage = _currentPage.toString();
@ -90,11 +92,13 @@ class _RoomSearchListPageState extends State<RoomSearchListPage> {
final response = await Api.serverRequest(uri: '/room/score/room/list', body: requestBody);
if (response == null || response['result'] != 'OK') {
showResponseDialog(context, '오류', '방 목록을 불러오지 못했습니다.');
showResponseDialog(context, 'Error', 'Failed to load room list.');
// '오류', '방 목록을 불러오지 못했습니다.'
} else {
final innerResp = response['response'];
if (innerResp == null || innerResp['result'] != 'OK') {
showResponseDialog(context, '오류', '내부 응답이 잘못되었습니다.');
showResponseDialog(context, 'Error', 'Invalid internal response.');
// '오류', '내부 응답이 잘못되었습니다.'
} else {
final respData = innerResp['data'];
if (respData is List) {
@ -104,13 +108,15 @@ class _RoomSearchListPageState extends State<RoomSearchListPage> {
for (var item in respData) {
final parsedItem = {
'room_seq': item['room_seq'] ?? 0,
'nickname': item['nickname'] ?? '사용자',
// WAIT/RUNNING/FINISH ->
'room_status': _statusToKr(item['room_status'] ?? ''),
'nickname': item['nickname'] ?? 'User', // '사용자'
// Convert WAIT/RUNNING/FINISH to Korean, stored in 'room_status'
'room_status': _statusToKr(item['room_status'] ?? ''),
// Store raw English status in 'raw_room_status'
'raw_room_status': (item['room_status'] ?? '').toString().toUpperCase(),
'open_yn': (item['open_yn'] == 'Y') ? '공개' : '비공개',
'open_yn': (item['open_yn'] == 'Y') ? 'Public' : 'Private',
// '공개' '비공개'
'room_type': (item['room_type_name'] ?? 'PRIVATE').toString().toLowerCase(),
'room_title': item['room_title'] ?? '(방제목 없음)',
'room_title': item['room_title'] ?? '(No Title)', // '(방제목 없음)'
'room_intro': item['room_intro'] ?? '',
'now_people': item['now_number_of_people']?.toString() ?? '0',
'max_people': item['number_of_people']?.toString() ?? '0',
@ -129,37 +135,43 @@ class _RoomSearchListPageState extends State<RoomSearchListPage> {
}
}
} catch (e) {
showResponseDialog(context, '오류', '서버 요청 중 예외 발생: $e');
showResponseDialog(context, 'Error', 'Exception occurred while requesting server: $e');
// '오류', '서버 요청 중 예외 발생'
} finally {
setState(() => _isLoading = false);
}
}
// Convert raw status to Korean ( )
String _statusToKr(String status) {
switch (status.toUpperCase()) {
case 'WAIT':
return '대기중';
return 'Waiting'; // '대기중'
case 'RUNNING':
return '진행중';
return 'Running'; // '진행중'
case 'FINISH':
return '종료';
return 'Finished'; // '종료'
default:
return status;
}
}
// On tap search icon or submit ( )
void _onSearch() {
_fetchRoomList(isRefresh: true);
}
// When a room item is tapped ( )
void _onRoomItemTap(Map<String, dynamic> item) {
// room_status() , raw_room_status( WAIT/RUNNING/FINISH)
final rawStatus = (item['raw_room_status'] ?? '').toString().toUpperCase();
final rawStatus = (item['raw_room_status'] ?? '').toString().toUpperCase();
// WAIT / RUNNING / FINISH
if (rawStatus == 'FINISH') {
// => FinishPrivatePage or FinishTeamPage
// If the room is finished, go to FinishPrivatePage or FinishTeamPage
final roomType = (item['room_type'] ?? 'private').toString().toLowerCase();
final roomSeq = (item['room_seq'] ?? 0) as int;
final roomTitle = (item['room_title'] ?? '(종료된 방)') as String;
final roomTitle = (item['room_title'] ?? '(Finished room)') as String;
// '(종료된 방)'
if (roomType == 'private') {
Navigator.push(
@ -183,7 +195,7 @@ class _RoomSearchListPageState extends State<RoomSearchListPage> {
);
}
} else {
// or => , : RoomDetailDialog
// If the room is waiting/running, open detail dialog (/: )
showDialog(
context: context,
builder: (_) => RoomDetailDialog(roomData: item),
@ -193,24 +205,28 @@ class _RoomSearchListPageState extends State<RoomSearchListPage> {
@override
Widget build(BuildContext context) {
// Convert incoming roomStatus to Korean to show in the title ( )
final statusKr = _statusToKr(widget.roomStatus);
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.black,
title: Text('$statusKr 방 검색', style: const TextStyle(color: Colors.white)),
title: Text(
'$statusKr Rooms Search', // '$statusKr 방 검색'
style: const TextStyle(color: Colors.white),
),
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
),
//
// Bottom ad banner ( )
bottomNavigationBar: AdBannerWidget(),
body: Column(
children: [
//
// Search box ()
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
@ -218,7 +234,7 @@ class _RoomSearchListPageState extends State<RoomSearchListPage> {
onSubmitted: (_) => _onSearch(),
style: const TextStyle(color: Colors.black),
decoration: InputDecoration(
hintText: '방 제목 입력',
hintText: 'Enter room title', // '방 제목 입력'
hintStyle: TextStyle(color: Colors.grey.shade400),
prefixIcon: const Icon(Icons.search, color: Colors.black54),
suffixIcon: IconButton(
@ -249,7 +265,10 @@ class _RoomSearchListPageState extends State<RoomSearchListPage> {
Widget _buildRoomListView() {
if (_roomList.isEmpty) {
return const Center(
child: Text('검색 결과가 없습니다.', style: TextStyle(color: Colors.black)),
child: Text(
'No results found.', // '검색 결과가 없습니다.'
style: TextStyle(color: Colors.black),
),
);
}
return ListView.builder(
@ -263,11 +282,12 @@ class _RoomSearchListPageState extends State<RoomSearchListPage> {
);
}
// Each room item ( )
Widget _buildRoomItem(Map<String, dynamic> item) {
final roomTitle = item['room_title'] ?? '(방제목 없음)';
final nickname = item['nickname'] ?? '유저';
final roomStatus = item['room_status'] ?? '대기중';
final openYn = item['open_yn'] ?? '공개';
final roomTitle = item['room_title'] ?? '(No Title)'; // '(방제목 없음)'
final nickname = item['nickname'] ?? 'User'; // '유저'
final roomStatus = item['room_status'] ?? 'Waiting'; // '대기중'
final openYn = item['open_yn'] ?? 'Public'; // '공개' / '비공개'
final nowPeople = item['now_people'] ?? '0';
final maxPeople = item['max_people'] ?? '0';
final roomIntro = item['room_intro'] ?? '';
@ -284,9 +304,13 @@ class _RoomSearchListPageState extends State<RoomSearchListPage> {
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(roomTitle, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
Text(
roomTitle,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 4),
Text('$nickname / $roomStatus / $openYn / $nowPeople/$maxPeople명'),
Text('$nickname / $roomStatus / $openYn / $nowPeople/$maxPeople'),
// ex) '유저 / 대기중 / 공개 / 3/10명'
const SizedBox(height: 4),
Text(roomIntro, style: const TextStyle(fontSize: 12)),
],

View File

@ -11,12 +11,12 @@ import '../../dialogs/room_setting_dialog.dart';
import '../../dialogs/user_info_private_dialog.dart';
import 'playing_private_page.dart';
//
// Ads
import '../../plugins/admob.dart';
//
// Config
import '../../config/config.dart';
//
// Font size auto-scale
import 'package:auto_size_text/auto_size_text.dart';
class WaitingRoomPrivatePage extends StatefulWidget {
@ -34,10 +34,8 @@ class WaitingRoomPrivatePage extends StatefulWidget {
}
class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
//
//
//
String roomMasterYn = 'N';
// Room settings ( )
String roomMasterYn = 'N'; //
String roomTitle = '';
String roomIntro = '';
String openYn = 'Y';
@ -46,50 +44,43 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
int numberOfPeople = 10;
String scoreOpenRange = 'PRIVATE';
// FRD
// Firebase Realtime Database (FRD)
late DatabaseReference _roomRef;
Stream<DatabaseEvent>? _roomStream;
StreamSubscription<DatabaseEvent>? _roomStreamSubscription;
//
// User list ( )
List<Map<String, dynamic>> _userList = [];
bool _isLoading = true;
bool _isLoading = true; //
bool _movedToRunningPage = false; //
bool _kickedOut = false; //
//
bool _movedToRunningPage = false;
//
bool _kickedOut = false;
String mySeq = '0'; // user_seq
// user_seq
String mySeq = '0';
// Ready button 3-second delay ( 3 )
bool _readyButtonEnabled = true;
// 3
bool _readyButtonEnabled = true; // true: , false:
//
// 1
//
// One-hour countdown (1 )
Timer? _countdownTimer;
Duration _remaining = const Duration(hours: 1); // 1
DateTime? _createDt; // FRD의 roomInfo.create_dt
Duration _remaining = const Duration(hours: 1);
DateTime? _createDt;
bool _roomTimeOut = false;
String _roomExitYn = 'N';
// SEQ
// Save room master seq ( SEQ)
String _masterSeqString = '0';
//
// Adjust font size by screen width ( )
double scaleFactor = 1.0;
double buttonScaleFactor = 1.0;
@override
void initState() {
super.initState();
// FRD
// Restore FRD connection (FRD )
FirebaseDatabase.instance.goOnline();
// (B)
_initRoomRef();
_initRoomRef(); // (B) Initialize room info ( )
}
@override
@ -104,8 +95,8 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
super.didChangeDependencies();
_updateScaleFactor();
}
//
// Adjust font size by screen width ( )
void _updateScaleFactor() {
final screenWidth = MediaQuery.of(context).size.width;
const baseWidth = 450.0;
@ -139,10 +130,10 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
if (!snapshot.exists) {
setState(() {
_isLoading = false;
roomTitle = '방 정보 없음';
roomTitle = 'No Room Info'; // '방 정보 없음'
_userList = [];
});
_roomMasterLeave();
_roomMasterLeave(); //
return;
}
@ -151,7 +142,7 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
final userInfoDynamic = data['userInfo'] as Map<dynamic, dynamic>? ?? {};
final roomStatus = (roomInfoData['room_status'] ?? 'WAIT').toString().toUpperCase();
final tempList = <Map<String, dynamic>>[];
// userList
if (userInfoDynamic is Map) {
@ -162,7 +153,7 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
tempList.add({
'user_seq': tempUserSeq,
'participant_type': val['participant_type'] ?? '',
'nickname': val['nickname'] ?? '유저',
'nickname': val['nickname'] ?? 'User', // '유저'
'team_name': val['team_name'] ?? '',
'score': val['score'] ?? 0,
'profile_img': val['profile_img'] ?? '',
@ -175,7 +166,6 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
});
}
// (A) roomInfo
setState(() {
roomTitle = (roomInfoData['room_title'] ?? '') as String;
roomIntro = (roomInfoData['room_intro'] ?? '') as String;
@ -185,24 +175,24 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
numberOfPeople = _toInt(roomInfoData['number_of_people'], 10);
scoreOpenRange = (roomInfoData['score_open_range'] ?? 'PRIVATE') as String;
//
// Check if I'm the room master (방장 여부)
roomMasterYn = 'N';
final masterSeq = roomInfoData['master_user_seq'].toString();
if (masterSeq != null && masterSeq == mySeq) {
roomMasterYn = 'Y';
}
if (masterSeq != null) {
_masterSeqString = masterSeq;
} else {
_masterSeqString = '';
_masterSeqString = '';
}
_userList = tempList;
_isLoading = false;
});
// (B) =>
// If room status is RUNNING, move to playing page ( )
if (roomStatus == 'RUNNING' && !_movedToRunningPage) {
_movedToRunningPage = true;
Navigator.pushAndRemoveUntil(
@ -218,8 +208,7 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
return;
}
// (C) 1 create_dt
// : "2025-01-07T06:38:10.123456"
// 1-hour countdown (1 )
final createDtStr = (roomInfoData['create_dt'] ?? '') as String;
if (createDtStr.isNotEmpty) {
final dotIndex = createDtStr.indexOf('.');
@ -233,12 +222,13 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
}
}
// (D) =>
// Check if I'm kicked out (강퇴 여부)
final amIStillHere = _userList.any((u) => (u['user_seq'].toString() ?? '0') == mySeq);
if (!amIStillHere && !_kickedOut && roomMasterYn != 'Y') {
_kickedOut = true;
if (_roomExitYn == 'N') {
showResponseDialog(context, '안내', '강퇴되었습니다.');
showResponseDialog(context, 'Notice', 'You have been kicked out.');
// '안내', '강퇴되었습니다.'
}
Future.delayed(Duration.zero, () async {
Navigator.pushAndRemoveUntil(
@ -248,9 +238,11 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
);
});
}
// (D)
// Check if room time has ended ( )
if (_roomTimeOut) {
showResponseDialog(context, '안내', '방 제한시간이 종료되었습니다.');
showResponseDialog(context, 'Notice', 'Room time limit has ended.');
// '안내', '방 제한시간이 종료되었습니다.'
Future.delayed(Duration.zero, () async {
Navigator.pushAndRemoveUntil(
context,
@ -262,15 +254,16 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
}, onError: (error) {
setState(() {
_isLoading = false;
roomTitle = '오류 발생';
roomTitle = 'Error occurred'; // '오류 발생'
});
});
}
//
// When the master leaves ( )
void _roomMasterLeave() {
Future.delayed(Duration.zero, () async {
await showResponseDialog(context, '안내', '방장이 나갔습니다.');
await showResponseDialog(context, 'Notice', 'The master has left the room.');
// '안내', '방장이 나갔습니다.'
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (_) => const MainPage()),
@ -279,24 +272,22 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
});
}
// 1
// Start 1-hour countdown (1 )
void _startCountdownTimer() {
if (_countdownTimer != null && _countdownTimer!.isActive) {
return; //
return;
}
if (_createDt == null) return;
_countdownTimer = Timer.periodic(const Duration(seconds: 1), (timer) {
// : createDt + 1
final endTime = _createDt!.add(const Duration(hours: 1));
final now = DateTime.now();
final diff = endTime.difference(now);
if (diff.isNegative) {
// ->
timer.cancel();
_remaining = const Duration(seconds: 0);
_onAutoTimeout();
_onAutoTimeout();
} else {
setState(() {
_remaining = diff;
@ -305,10 +296,8 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
});
}
// 1
// Auto timeout after 1 hour (1 )
void _onAutoTimeout() {
// => (leave API)
// =>
setState(() {
_roomTimeOut = true;
_roomExitYn = 'Y';
@ -316,23 +305,28 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
_requestLeaveRoom();
}
// Request to leave the room ( )
Future<void> _requestLeaveRoom() async {
try {
final reqBody = {"room_seq": "${widget.roomSeq}"};
final response = await Api.serverRequest(uri: '/room/score/game/leave', body: reqBody);
// result ok ->
// if result ok -> move to main
} catch (e) {
await showResponseDialog(context, '오류', '방 나가기 처리 실패');
await showResponseDialog(context, 'Error', 'Failed to leave the room.');
// '오류', '방 나가기 처리 실패'
}
if (mounted) {
Navigator.pushAndRemoveUntil(context, MaterialPageRoute(builder: (_) => const MainPage()), (route) => false);
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (_) => const MainPage()),
(route) => false,
);
}
}
///
/// On back press -> leave room ( )
Future<bool> _onLeaveRoom() async {
if (roomMasterYn == 'Y') {
//
final confirm = await showDialog<bool>(
context: context,
barrierDismissible: false,
@ -344,9 +338,16 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
side: const BorderSide(color: Colors.black, width: 2),
),
title: const Center(
child: Text('방 나가기', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
child: Text(
'Leave Room', // '방 나가기'
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
),
content: const Text(
'If the master leaves, the room will be deleted.\nDo you really want to leave?',
// '방장이 나가면 방은 삭제됩니다.\n정말 나가시겠습니까?'
style: TextStyle(fontSize: 14),
),
content: const Text('방장이 나가면 방은 삭제됩니다.\n정말 나가시겠습니까?', style: TextStyle(fontSize: 14)),
actionsAlignment: MainAxisAlignment.spaceEvenly,
actions: [
TextButton(
@ -356,7 +357,7 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('취소'),
child: const Text('Cancel'), // '취소'
),
TextButton(
onPressed: () {
@ -369,7 +370,7 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('확인'),
child: const Text('OK'), // '확인'
),
],
);
@ -378,11 +379,15 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
if (confirm != true) return false;
// leave API
setState(() {_roomExitYn = 'Y';});
setState(() {
_roomExitYn = 'Y';
});
await _requestLeaveRoom();
} else {
//
setState(() {_roomExitYn = 'Y';});
//
setState(() {
_roomExitYn = 'Y';
});
await _requestLeaveRoom();
}
return false;
@ -397,14 +402,14 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
return defaultVal;
}
///
/// Top buttons ( )
Widget _buildTopButtons() {
if (_isLoading) return const SizedBox();
final me = _userList.firstWhere((u) => (u['user_seq'].toString() ?? '0') == mySeq, orElse: () => {});
final myReadyYn = (me['ready_yn'] ?? 'N').toString().toUpperCase();
final bool isReady = (myReadyYn == 'Y');
final readyLabel = '준비';
final readyLabel = 'Ready'; // '준비'
final btnStyle = ElevatedButton.styleFrom(
backgroundColor: Colors.white,
@ -413,7 +418,7 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
);
if (roomMasterYn == 'Y') {
// => 3
// Master => 3 buttons ( => 3)
return Row(
children: [
Expanded(
@ -423,7 +428,7 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
style: btnStyle,
onPressed: _onOpenRoomSetting,
child: AutoSizeText(
'방 설정',
'Settings', // '방 설정'
maxLines: 1,
style: TextStyle(fontSize: 14 * scaleFactor),
),
@ -439,7 +444,10 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
child: AutoSizeText(
readyLabel,
maxLines: 1,
style: TextStyle(fontSize: 14 * scaleFactor, color: isReady ? Colors.red : Colors.black),
style: TextStyle(
fontSize: 14 * scaleFactor,
color: isReady ? Colors.red : Colors.black,
),
),
),
),
@ -451,7 +459,8 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
style: btnStyle,
onPressed: _onGameStart,
child: AutoSizeText(
scaleFactor==0.8 ? '시작' : '게임 시작',
scaleFactor == 0.8 ? 'Start' : 'Start Game',
// '시작' '게임 시작'
maxLines: 1,
style: TextStyle(fontSize: 14 * scaleFactor),
),
@ -461,7 +470,7 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
],
);
} else {
// => 2
// Normal => 2 buttons ( => 2)
return Row(
children: [
Expanded(
@ -471,7 +480,7 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
style: btnStyle,
onPressed: _onOpenRoomSetting,
child: AutoSizeText(
'방 설정',
'Settings', // '방 설정'
maxLines: 1,
style: TextStyle(fontSize: 14 * scaleFactor),
),
@ -487,7 +496,10 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
child: AutoSizeText(
readyLabel,
maxLines: 1,
style: TextStyle(fontSize: 14 * scaleFactor, color: isReady ? Colors.red : Colors.black),
style: TextStyle(
fontSize: 14 * scaleFactor,
color: isReady ? Colors.red : Colors.black,
),
),
),
),
@ -497,13 +509,14 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
}
}
// Toggle ready ( )
Future<void> _onToggleReady() async {
// (A)
if (!_readyButtonEnabled) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('잠시 후에 다시 눌러주세요. (3초 대기)'),
content: Text('Please wait a moment before pressing again. (3s delay)'),
// '잠시 후에 다시 눌러주세요. (3초 대기)'
duration: Duration(seconds: 1),
),
);
@ -511,11 +524,11 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
return;
}
// (B)
// Disable button ( )
setState(() {
_readyButtonEnabled = false;
});
try {
final me = _userList.firstWhere((u) => (u['user_seq'].toString() ?? '0') == mySeq, orElse: () => {});
final myReadyYn = (me['ready_yn'] ?? 'N').toString().toUpperCase();
@ -525,10 +538,11 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
final userRef = _roomRef.child('userInfo').child('_${mySeq}');
await userRef.update({"ready_yn": newYn});
} catch (e) {
showResponseDialog(context, '오류', 'READY 설정 실패했습니다.');
showResponseDialog(context, 'Error', 'Failed to set READY state.');
// '오류', 'READY 설정 실패했습니다.'
}
// (C) 3
// Re-enable after 3 seconds (3 )
Future.delayed(const Duration(seconds: 3), () {
if (mounted) {
setState(() {
@ -538,6 +552,7 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
});
}
// Open room settings ( )
Future<void> _onOpenRoomSetting() async {
final roomInfo = {
"room_seq": "${widget.roomSeq}",
@ -562,10 +577,12 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
}
}
// Start game ( )
Future<void> _onGameStart() async {
final notReady = _userList.any((u) => (u['ready_yn'] ?? 'N').toString().toUpperCase() != 'Y');
if (notReady) {
showResponseDialog(context, '안내', 'READY되지 않은 참가자가 있습니다(방장 포함).');
showResponseDialog(context, 'Notice', 'Someone is not ready yet (including the master).');
// '안내', 'READY되지 않은 참가자가 있습니다(방장 포함).'
return;
}
@ -578,22 +595,20 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
if (response['result'] == 'OK') {
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
//
// Move to running room ( )
} else {
//
showResponseDialog(context, '오류', '게임 시작 실패했습니다.');
showResponseDialog(context, 'Error', 'Failed to start the game.');
// '오류', '게임 시작 실패했습니다.'
}
} else {
//
showResponseDialog(context, '오류', '게임 시작 실패했습니다.');
showResponseDialog(context, 'Error', 'Failed to start the game.');
}
} catch (e) {
//
showResponseDialog(context, '오류', '게임 시작 실패했습니다.');
showResponseDialog(context, 'Error', 'Failed to start the game.');
}
}
// ()
// Format countdown ( )
String _formatDuration(Duration d) {
final mm = d.inMinutes.remainder(60).toString().padLeft(2, '0');
final ss = d.inSeconds.remainder(60).toString().padLeft(2, '0');
@ -602,66 +617,71 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
@override
Widget build(BuildContext context) {
// (: 60:00 ~ 0:00)
final countdownStr = _formatDuration(_remaining);
final countdownStr = _formatDuration(_remaining); // : "10:23"
return WillPopScope(
onWillPop: () => _onLeaveRoom(),
child: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.black,
elevation: 0,
// +
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// :
Text(
roomTitle.isNotEmpty ? roomTitle : '방 제목',
style: const TextStyle(color: Colors.white),
),
// :
Text(
countdownStr, // : "10:23"
style: const TextStyle(color: Colors.white),
),
],
),
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
onPressed: () => _onLeaveRoom(),
),
),
bottomNavigationBar: AdBannerWidget(),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//
_buildTopButtons(),
const SizedBox(height: 20),
const Text('참가자', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 8),
_buildPlayerSection(),
],
backgroundColor: Colors.black,
elevation: 0,
// Room title + remaining time ( + )
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Left: room title ( )
Expanded(
child: Text(
roomTitle.isNotEmpty ? roomTitle : 'Room Title', // '방 제목'
style: const TextStyle(color: Colors.white),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 4),
// Right: countdown ()
Text(
countdownStr,
style: const TextStyle(color: Colors.white),
),
],
),
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
onPressed: () => _onLeaveRoom(),
),
),
bottomNavigationBar: AdBannerWidget(),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTopButtons(),
const SizedBox(height: 20),
const Text(
'Participants', // '참가자'
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
_buildPlayerSection(),
],
),
),
),
),
);
}
// Build player section ( )
Widget _buildPlayerSection() {
final playerList = _userList.where((u) {
final t = (u['user_seq'].toString() ?? null);
return t != null;
}).toList();
// final playerList = _userList;
return Container(
width: double.infinity,
@ -672,7 +692,7 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
borderRadius: BorderRadius.circular(8),
),
child: playerList.isEmpty
? const Text('참가자가 없습니다.')
? const Text('No participants.') // '참가자가 없습니다.'
: SingleChildScrollView(
child: Wrap(
spacing: 16,
@ -684,8 +704,9 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
);
}
// Build seat ( )
Widget _buildSeat(Map<String, dynamic> userData) {
final userName = userData['nickname'] ?? '유저';
final userName = userData['nickname'] ?? 'User'; // '유저'
final profileImg = userData['profile_img'] ?? '';
final readyYn = (userData['ready_yn'] ?? 'N').toString().toUpperCase();
final connectYn = (userData['connect_yn'] ?? 'Y').toString().toUpperCase();
@ -693,20 +714,18 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
final bool isDisconnected = (connectYn == 'N');
final bool isMaster = (roomMasterYn == 'Y');
// user가
// Check if user is the room master ( )
final isRoomMasterUser = (userData['user_seq']?.toString() ?? '') == _masterSeqString;
// user가
// Check if user is admin ( )
final participantType = (userData['participant_type'] ?? '').toString().toUpperCase();
final isAdmin = (participantType == 'ADMIN');
//
// Icon for room master/admin ()
String roleIcon = '';
if (isRoomMasterUser) {
//
roleIcon = '';
roleIcon = ''; //
} else if (isAdmin) {
//
roleIcon = '';
roleIcon = ''; //
}
final displayName = '$roleIcon$userName';
@ -763,7 +782,8 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
child: isDisconnected
? const Center(
child: Text(
'!',
'!',
// '!'
style: TextStyle(
fontSize: 20,
color: Colors.orange,
@ -776,7 +796,8 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => const Center(
child: Text(
'이미지\n불가',
'No\nImage',
// '이미지\n불가'
textAlign: TextAlign.center,
style: TextStyle(fontSize: 10),
),
@ -785,7 +806,10 @@ class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
),
),
const SizedBox(height: 4),
Text(displayName, style: const TextStyle(fontSize: 12, color: Colors.black)),
Text(
displayName,
style: const TextStyle(fontSize: 12, color: Colors.black),
),
],
),
),

View File

@ -3,13 +3,13 @@ import 'dart:async';
import 'package:firebase_database/firebase_database.dart';
import 'package:shared_preferences/shared_preferences.dart';
//
// Main page
import 'main_page.dart';
// API
// APIs
import '../../plugins/api.dart';
//
// Dialogs
import '../../dialogs/response_dialog.dart';
import '../../dialogs/yes_no_dialog.dart';
import '../../dialogs/room_setting_dialog.dart';
@ -17,12 +17,12 @@ import '../../dialogs/user_info_team_dialog.dart';
import '../../dialogs/team_name_edit_dialog.dart';
import 'playing_team_page.dart';
//
// Ads
import '../../plugins/admob.dart';
//
// Config
import '../../config/config.dart';
//
// Font size auto-scale
import 'package:auto_size_text/auto_size_text.dart';
class WaitingRoomTeamPage extends StatefulWidget {
@ -40,8 +40,8 @@ class WaitingRoomTeamPage extends StatefulWidget {
}
class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
//
String roomMasterYn = 'N';
// Room settings ( )
String roomMasterYn = 'N'; //
String roomTitle = '';
String roomIntro = '';
String openYn = 'Y';
@ -56,7 +56,7 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
bool _isLoading = true;
//
// Server request loading
bool _isServerRequestLoading = false;
late DatabaseReference _roomRef;
@ -68,20 +68,20 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
String mySeq = '0';
// 3
bool _readyButtonEnabled = true; // true: , false:
// Ready button 3-second delay ( 3 )
bool _readyButtonEnabled = true;
// () 1
// One-hour countdown (1 )
Timer? _countdownTimer;
Duration _remaining = const Duration(hours: 1);
DateTime? _createDt;
bool _roomTimeOut = false;
String _roomExitYn = 'N';
// SEQ
// Save room master seq ( SEQ)
String _masterSeqString = '0';
//
// Adjust font size by screen width ( )
double scaleFactor = 1.0;
double buttonScaleFactor = 1.0;
@ -89,8 +89,7 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
void initState() {
super.initState();
FirebaseDatabase.instance.goOnline();
// (D)
_initRoomRef();
_initRoomRef(); // (D) Initialize room reference
}
@override
@ -99,7 +98,7 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
_updateScaleFactor();
}
//
// Adjust font size by screen width ( )
void _updateScaleFactor() {
final screenWidth = MediaQuery.of(context).size.width;
const baseWidth = 450.0;
@ -109,16 +108,18 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
});
}
Future<void> _initRoomRef() async {
Future<void> _initRoomRef() async {
final prefs = await SharedPreferences.getInstance();
mySeq = prefs.getInt('my_user_seq')?.toString() ?? '0';
// Example: room key = "korea-123"
final roomKey = 'korea-${widget.roomSeq}';
_roomRef = FirebaseDatabase.instance.ref('rooms/$roomKey');
// onDisconnect + connect_yn='Y'
// onDisconnect + connect_yn = 'Y'
final myUserRef = _roomRef.child('userInfo').child('_${mySeq}');
if (_roomRef.child('userList').child(mySeq) == true) {
//
myUserRef.onDisconnect().update({'connect_yn': 'N'});
}
await myUserRef.update({'connect_yn': 'Y'});
@ -140,10 +141,10 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
if (!snapshot.exists) {
setState(() {
_isLoading = false;
roomTitle = '방 정보 없음';
roomTitle = 'No Room Info'; // '방 정보 없음'
_userList = [];
});
_roomMasterLeave();
_roomMasterLeave(); //
return;
}
@ -152,7 +153,7 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
final userInfoDynamic = data['userInfo'] as Map<dynamic, dynamic>? ?? {};
final roomStatus = (roomInfoData['room_status'] ?? 'WAIT').toString().toUpperCase();
final tempList = <Map<String, dynamic>>[];
// userList
if (userInfoDynamic is Map) {
@ -163,7 +164,7 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
tempList.add({
'user_seq': val['user_seq'].toString() ?? '0',
'participant_type': val['participant_type'] ?? '',
'nickname': val['nickname'] ?? '유저',
'nickname': val['nickname'] ?? 'User', // '유저'
'team_name': val['team_name'] ?? '',
'score': val['score'] ?? 0,
'profile_img': val['profile_img'] ?? '',
@ -186,32 +187,33 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
scoreOpenRange = (roomInfoData['score_open_range'] ?? 'PRIVATE') as String;
numberOfTeams = _toInt(roomInfoData['number_of_teams'], 1);
//
// Team names ()
final tStr = (roomInfoData['team_name_list'] ?? '') as String;
if (tStr.isNotEmpty) {
_teamNameList = tStr.split(',').map((e) => e.trim().toUpperCase()).toList();
} else {
// Default A, B, C... for the number of teams
_teamNameList = List.generate(numberOfTeams, (i) => String.fromCharCode(65 + i));
}
//
// Check if I'm the master (방장)
roomMasterYn = 'N';
final masterSeq = roomInfoData['master_user_seq'].toString();
if (masterSeq != null && masterSeq == mySeq) {
roomMasterYn = 'Y';
}
if (masterSeq != null) {
_masterSeqString = masterSeq;
} else {
_masterSeqString = '0';
_masterSeqString = '0';
}
_userList = tempList;
_isLoading = false;
});
// ->
// If status == RUNNING, move to running page ( )
if (roomStatus == 'RUNNING' && !_movedToRunningPage) {
_movedToRunningPage = true;
Navigator.pushAndRemoveUntil(
@ -227,7 +229,7 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
return;
}
// (C) create_dt -> 1
// Parse create_dt -> 1-hour countdown (1 )
final createDtStr = (roomInfoData['create_dt'] ?? '') as String;
if (createDtStr.isNotEmpty) {
final dotIndex = createDtStr.indexOf('.');
@ -241,12 +243,13 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
}
}
// (D) =>
// Check if I am kicked out ( )
final amIStillHere = _userList.any((u) => (u['user_seq'].toString() ?? '0') == mySeq);
if (!amIStillHere && !_kickedOut && roomMasterYn != 'Y') {
_kickedOut = true;
if (_roomExitYn == 'N') {
showResponseDialog(context, '안내', '강퇴되었습니다.');
showResponseDialog(context, 'Notice', 'You have been kicked out.');
// '안내', '강퇴되었습니다.'
}
Future.delayed(Duration.zero, () async {
Navigator.pushAndRemoveUntil(
@ -256,9 +259,11 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
);
});
}
// (D)
// Check if room time is out ( )
if (_roomTimeOut) {
showResponseDialog(context, '안내', '방 제한시간이 종료되었습니다.');
showResponseDialog(context, 'Notice', 'Room time limit has expired.');
// '안내', '방 제한시간이 종료되었습니다.'
Future.delayed(Duration.zero, () async {
Navigator.pushAndRemoveUntil(
context,
@ -270,15 +275,16 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
}, onError: (error) {
setState(() {
_isLoading = false;
roomTitle = '오류 발생';
roomTitle = 'Error occurred'; // '오류 발생'
});
});
}
//
// When the master leaves the room ( )
void _roomMasterLeave() {
Future.delayed(Duration.zero, () async {
await showResponseDialog(context, '안내', '방장이 나갔습니다.');
await showResponseDialog(context, 'Notice', 'The master has left the room.');
// '안내', '방장이 나갔습니다.'
Navigator.pushAndRemoveUntil(
context,
MaterialPageRoute(builder: (_) => const MainPage()),
@ -287,7 +293,7 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
});
}
//
// Start 1-hour countdown (1 )
void _startCountdownTimer() {
if (_countdownTimer != null && _countdownTimer!.isActive) {
return;
@ -311,15 +317,16 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
});
}
// Auto timeout ( )
void _onAutoTimeout() {
// -> =(), =
setState(() {
_roomTimeOut = true;
_roomExitYn = 'Y';
});
_requestLeaveRoom();
_requestLeaveRoom(); //
}
// Request leave room ( )
Future<void> _requestLeaveRoom() async {
try {
final reqBody = {"room_seq": "${widget.roomSeq}"};
@ -337,7 +344,7 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
}
}
// ->
// On back press -> leave the room ( -> )
Future<bool> _onLeaveRoom() async {
if (roomMasterYn == 'Y') {
final confirm = await showDialog<bool>(
@ -351,9 +358,16 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
side: const BorderSide(color: Colors.black, width: 2),
),
title: const Center(
child: Text('방 나가기', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black)),
child: Text(
'Leave Room', // '방 나가기'
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black),
),
),
content: const Text(
'If the master leaves, the room will be deleted.\nAre you sure you want to leave?',
// '방장이 나가면 방은 삭제됩니다.\n정말 나가시겠습니까?'
style: TextStyle(fontSize: 14),
),
content: const Text('방장이 나가면 방은 삭제됩니다.\n정말 나가시겠습니까?', style: TextStyle(fontSize: 14)),
actionsAlignment: MainAxisAlignment.spaceEvenly,
actions: [
TextButton(
@ -363,7 +377,7 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('취소'),
child: const Text('Cancel'), // '취소'
),
TextButton(
onPressed: () {
@ -376,7 +390,7 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
foregroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('확인'),
child: const Text('OK'), // '확인'
),
],
);
@ -385,10 +399,14 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
if (confirm != true) return false;
// leave API
setState(() {_roomExitYn = 'Y';});
setState(() {
_roomExitYn = 'Y';
});
await _requestLeaveRoom();
} else {
setState(() {_roomExitYn = 'Y';});
setState(() {
_roomExitYn = 'Y';
});
await _requestLeaveRoom();
}
return false;
@ -403,14 +421,14 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
return defaultVal;
}
//
// Top buttons ( )
Widget _buildTopButtons() {
if (_isLoading) return const SizedBox();
final me = _userList.firstWhere((u) => (u['user_seq'].toString() ?? '0') == mySeq, orElse: () => {});
final myReadyYn = (me['ready_yn'] ?? 'N').toString().toUpperCase();
final bool isReady = (myReadyYn == 'Y');
final readyLabel = '준비';
final readyLabel = 'Ready'; // '준비'
final btnStyle = ElevatedButton.styleFrom(
backgroundColor: Colors.white,
@ -428,7 +446,7 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
style: btnStyle,
onPressed: _onOpenRoomSetting,
child: AutoSizeText(
'방 설정',
'Settings', // '방 설정'
maxLines: 1,
style: TextStyle(fontSize: 14 * scaleFactor),
),
@ -444,7 +462,10 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
child: AutoSizeText(
readyLabel,
maxLines: 1,
style: TextStyle(fontSize: 14 * scaleFactor, color: isReady ? Colors.red : Colors.black),
style: TextStyle(
fontSize: 14 * scaleFactor,
color: isReady ? Colors.red : Colors.black,
),
),
),
),
@ -458,7 +479,8 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
child: _isServerRequestLoading
? const CircularProgressIndicator()
: AutoSizeText(
scaleFactor==0.8 ? '시작' : '게임 시작',
scaleFactor == 0.8 ? 'Start' : 'Start Game',
// '시작' '게임 시작'
maxLines: 1,
style: TextStyle(fontSize: 14 * scaleFactor),
),
@ -477,7 +499,7 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
style: btnStyle,
onPressed: _onOpenRoomSetting,
child: AutoSizeText(
'방 설정',
'Settings', // '방 설정'
maxLines: 1,
style: TextStyle(fontSize: 14 * scaleFactor),
),
@ -493,7 +515,10 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
child: AutoSizeText(
readyLabel,
maxLines: 1,
style: TextStyle(fontSize: 14 * scaleFactor, color: isReady ? Colors.red : Colors.black),
style: TextStyle(
fontSize: 14 * scaleFactor,
color: isReady ? Colors.red : Colors.black,
),
),
),
),
@ -503,13 +528,14 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
}
}
// Toggle ready ( )
Future<void> _onToggleReady() async {
// (A)
if (!_readyButtonEnabled) {
if (mounted) {
ScaffoldMessenger.of(context).showSnackBar(
const SnackBar(
content: Text('잠시 후에 다시 눌러주세요. (3초 대기)'),
content: Text('Please wait a moment before pressing again. (3s delay)'),
// '잠시 후에 다시 눌러주세요. (3초 대기)'
duration: Duration(seconds: 1),
),
);
@ -517,7 +543,7 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
return;
}
// (B)
// Button disable ( )
setState(() {
_readyButtonEnabled = false;
});
@ -531,10 +557,11 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
final userRef = _roomRef.child('userInfo').child('_${mySeq}');
await userRef.update({"ready_yn": newYn});
} catch (e) {
showResponseDialog(context, '오류', 'READY 설정에 실패했습니다.');
showResponseDialog(context, 'Error', 'Failed to set READY state.');
// '오류', 'READY 설정에 실패했습니다.'
}
// (C) 3
// Re-enable button after 3 seconds (3 )
Future.delayed(const Duration(seconds: 3), () {
if (mounted) {
setState(() {
@ -544,6 +571,7 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
});
}
// Open room setting ( )
Future<void> _onOpenRoomSetting() async {
final roomInfo = {
"room_seq": "${widget.roomSeq}",
@ -566,22 +594,28 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
builder: (_) => RoomSettingModal(roomInfo: roomInfo),
);
if (result == 'refresh') {
// ...
// refresh if needed
}
}
// Start game ( )
Future<void> _onGameStart() async {
setState(() => _isServerRequestLoading = true);
// Check if anyone is not ready ( )
final notReady = _userList.any((u) => (u['ready_yn'] ?? 'N').toString().toUpperCase() != 'Y');
if (notReady) {
showResponseDialog(context, '안내', '아직 준비되지 않은 참가자가 있습니다(방장 포함).');
showResponseDialog(context, 'Notice', 'Someone is not ready yet (including the master).');
// '안내', '아직 준비되지 않은 참가자가 있습니다(방장 포함).'
setState(() => _isServerRequestLoading = false);
return;
}
//
// Check if someone is not assigned to a team ( )
final notTeam = _userList.any((u) => (u['team_name'] ?? '').toString().toUpperCase() == 'WAIT');
if (notTeam) {
showResponseDialog(context, '안내', '팀 배정이 안된 참가자가 있습니다.');
showResponseDialog(context, 'Notice', 'Someone is not assigned to a team.');
// '안내', '팀 배정이 안된 참가자가 있습니다.'
setState(() => _isServerRequestLoading = false);
return;
}
@ -595,24 +629,30 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
if (response['result'] == 'OK') {
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
//
// Move to running room ( )
} else {
//
showResponseDialog(context, response['response_info']['msg_title'], response['response_info']['msg_content']);
showResponseDialog(
context,
response['response_info']['msg_title'],
response['response_info']['msg_content'],
);
}
} else {
//
showResponseDialog(context, response['response_info']['msg_title'], response['response_info']['msg_content']);
showResponseDialog(
context,
response['response_info']['msg_title'],
response['response_info']['msg_content'],
);
}
} catch (e) {
//
showResponseDialog(context, '오류', '게임 시작 요청에 실패했습니다.');
showResponseDialog(context, 'Error', 'Failed to request game start.');
// '오류', '게임 시작 요청에 실패했습니다.'
} finally {
setState(() => _isServerRequestLoading = false);
}
}
// ()
// Format countdown display ( )
String _formatDuration(Duration d) {
final mm = d.inMinutes.remainder(60).toString().padLeft(2, '0');
final ss = d.inSeconds.remainder(60).toString().padLeft(2, '0');
@ -628,60 +668,68 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
child: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.black,
elevation: 0,
// +
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// :
Text(
roomTitle.isNotEmpty ? roomTitle : '방 제목',
style: const TextStyle(color: Colors.white),
),
// :
Text(
countdownStr, // : "10:23"
style: const TextStyle(color: Colors.white),
),
],
),
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
onPressed: () => _onLeaveRoom(),
),
),
bottomNavigationBar: AdBannerWidget(),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTopButtons(),
const SizedBox(height: 20),
const Text('팀별 참가자', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black)),
const SizedBox(height: 8),
_buildTeamSection(),
const SizedBox(height: 20),
_buildWaitSection(),
],
backgroundColor: Colors.black,
elevation: 0,
// Room title + Remaining time ( + )
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// Left: room title ( )
Expanded(
child: Text(
roomTitle.isNotEmpty ? roomTitle : 'Room Title',
// '방 제목'
style: const TextStyle(color: Colors.white),
maxLines: 1,
overflow: TextOverflow.ellipsis,
),
),
const SizedBox(width: 4),
// Right: countdown ()
Text(
countdownStr, // e.g. "10:23"
style: const TextStyle(color: Colors.white),
),
],
),
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
onPressed: () => _onLeaveRoom(),
),
),
bottomNavigationBar: AdBannerWidget(),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
_buildTopButtons(),
const SizedBox(height: 20),
const Text(
'Players by Team', // '팀별 참가자'
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black),
),
const SizedBox(height: 8),
_buildTeamSection(),
const SizedBox(height: 20),
_buildWaitSection(),
],
),
),
),
),
);
}
// Build team section ( )
Widget _buildTeamSection() {
final players = _userList.where((u) {
final t = (u['user_seq'].toString() ?? null);
return t != null;
}).toList();
// final players = _userList.toList();
final Map<String, List<Map<String, dynamic>>> teamMap = {};
for (final tName in _teamNameList) {
@ -704,7 +752,10 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
border: Border.all(color: Colors.black),
borderRadius: BorderRadius.circular(8),
),
child: const Text('팀에 배정된 참가자가 없습니다.', style: TextStyle(color: Colors.black)),
child: const Text(
'No participants assigned to teams.', // '팀에 배정된 참가자가 없습니다.'
style: TextStyle(color: Colors.black),
),
);
}
@ -726,6 +777,7 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
GestureDetector(
onTap: () async {
if (roomMasterYn == 'Y') {
//
final result = await showDialog(
context: context,
barrierDismissible: false,
@ -747,7 +799,7 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
color: Colors.black,
child: Center(
child: Text(
' $teamName',
'Team $teamName', // '$teamName'
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
),
@ -765,12 +817,13 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
);
}
// Build wait section ( )
Widget _buildWaitSection() {
final waitList = _userList.where((u) {
final tName = (u['team_name'] ?? '').toString().toUpperCase();
return tName == 'WAIT';
}).toList();
if (waitList.isEmpty) return const SizedBox();
return Container(
@ -787,7 +840,10 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
padding: const EdgeInsets.symmetric(vertical: 8),
color: Colors.black,
child: const Center(
child: Text('대기중', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
child: Text(
'Waiting', // '대기중'
style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
),
),
SingleChildScrollView(
@ -800,8 +856,9 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
);
}
// Build seat ( )
Widget _buildSeat(Map<String, dynamic> user) {
final userName = user['nickname'] ?? '유저';
final userName = user['nickname'] ?? 'User'; // '유저'
final profileImg = user['profile_img'] ?? '';
final readyYn = (user['ready_yn'] ?? 'N').toString().toUpperCase();
final connectYn = (user['connect_yn'] ?? 'Y').toString().toUpperCase();
@ -809,20 +866,18 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
final bool isDisconnected = (connectYn == 'N');
final bool isMaster = (roomMasterYn == 'Y');
// user가
// Check if this user is the room master ()
final isRoomMasterUser = (user['user_seq']?.toString() ?? '') == _masterSeqString;
// user가
// Check if this user is an admin ()
final participantType = (user['participant_type'] ?? '').toString().toUpperCase();
final isAdmin = (participantType == 'ADMIN');
//
// Icon for room master or admin
String roleIcon = '';
if (isRoomMasterUser) {
//
roleIcon = '';
roleIcon = ''; //
} else if (isAdmin) {
//
roleIcon = '';
roleIcon = ''; //
}
final displayName = '$roleIcon$userName';
@ -894,7 +949,7 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
fit: BoxFit.cover,
errorBuilder: (_, __, ___) => const Center(
child: Text(
'이미지\n불가',
'No\nImage', // '이미지\n불가'
textAlign: TextAlign.center,
style: TextStyle(fontSize: 10),
),
@ -903,7 +958,10 @@ class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
),
),
const SizedBox(height: 2),
Text(displayName, style: const TextStyle(fontSize: 12, color: Colors.black)),
Text(
displayName,
style: const TextStyle(fontSize: 12, color: Colors.black),
),
],
),
),

File diff suppressed because it is too large Load Diff

View File

@ -5,7 +5,7 @@ import 'dart:convert';
import '../../plugins/utils.dart';
import '../../plugins/api.dart';
import '../../dialogs/response_dialog.dart';
import '../login/login_page.dart'; //
import '../login/login_page.dart'; //
class WithdrawalPage extends StatefulWidget {
const WithdrawalPage({Key? key}) : super(key: key);
@ -17,12 +17,12 @@ class WithdrawalPage extends StatefulWidget {
class _WithdrawalPageState extends State<WithdrawalPage> {
bool _isAgreed = false; //
final TextEditingController _passwordController = TextEditingController(); //
String _oauthType = 'idpw';
String _oauthType = 'idpw'; //
@override
void initState() {
super.initState();
_loadOAuthType(); // SharedPreferences에서 oauth_type
_loadOAuthType(); // SharedPreferences에서 oauth_type
}
/// SharedPreferences에서 oauth_type
@ -38,13 +38,16 @@ class _WithdrawalPageState extends State<WithdrawalPage> {
//
final screenWidth = MediaQuery.of(context).size.width;
// oauth_type == 'google'
// oauth_type == 'google'
final isGoogleUser = (_oauthType.toLowerCase() == 'google');
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text('회원탈퇴', style: TextStyle(color: Colors.black)),
title: const Text(
'Withdrawal', // '회원탈퇴'
style: TextStyle(color: Colors.black),
),
backgroundColor: Colors.transparent,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.black),
@ -62,7 +65,8 @@ class _WithdrawalPageState extends State<WithdrawalPage> {
const SizedBox(height: 20),
const Center(
child: Text(
'회원탈퇴를 진행합니다.\n현재 비밀번호를 입력해주세요.',
'We will proceed with account withdrawal.\nPlease enter your current password.',
// '회원탈퇴를 진행합니다.\n현재 비밀번호를 입력해주세요.'
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18),
),
@ -75,7 +79,7 @@ class _WithdrawalPageState extends State<WithdrawalPage> {
controller: _passwordController,
obscureText: true,
decoration: InputDecoration(
hintText: '비밀번호 입력',
hintText: 'Enter password', // '비밀번호 입력'
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.black),
@ -93,12 +97,16 @@ class _WithdrawalPageState extends State<WithdrawalPage> {
),
padding: const EdgeInsets.all(16.0),
child: const Text(
'[회원 탈퇴 안내]\n'
'회원 탈퇴를 진행하시겠습니까?\n'
' - 회원 탈퇴 시 등록하신 모든 개인정보(ID, 비밀번호, 닉네임, 이메일 주소, 소속, 자기소개 등)는 즉시 삭제되며 복구가 불가능합니다.\n'
' - 탈퇴 후 동일한 아이디로 재가입이 불가능할 수 있습니다.\n'
' - 관련 법령에 따라 일정 기간 보관이 필요한 경우 해당 기간 동안 법령이 허용하는 범위 내에서만 보관됩니다.\n'
'탈퇴를 원하시면 아래의 "동의" 버튼을 눌러주시기 바랍니다.',
'[Withdrawal Guide]\n' // '[회원 탈퇴 안내]'
'Do you really want to withdraw?\n' // '회원 탈퇴를 진행하시겠습니까?'
' - All personal information (ID, password, nickname, email, affiliation, self-introduction, etc.) you have registered will be deleted immediately and cannot be recovered.\n'
// ' - 회원 탈퇴 시 등록하신 모든 개인정보(...)는 즉시 삭제되며 복구가 불가능합니다.'
' - You may not be able to re-register with the same ID after withdrawal.\n'
// ' - 탈퇴 후 동일한 아이디로 재가입이 불가능할 수 있습니다.'
' - In accordance with relevant laws, if certain data must be retained for a specified period, it will be stored within the scope permitted by the law for that period only.\n'
// ' - 관련 법령에 따라 일정 기간 보관이 필요한 경우 해당 기간 동안 법령이 허용하는 범위 내에서만 보관됩니다.'
'If you wish to withdraw, please check "Agree" below.',
// '탈퇴를 원하시면 아래의 "동의" 버튼을 눌러주시기 바랍니다.'
textAlign: TextAlign.left,
style: TextStyle(fontSize: 12),
),
@ -119,7 +127,7 @@ class _WithdrawalPageState extends State<WithdrawalPage> {
),
const Expanded(
child: Text(
'회원탈퇴에 동의합니다.',
'I agree to the withdrawal.', // '회원탈퇴에 동의합니다.'
style: TextStyle(fontSize: 16),
),
),
@ -142,7 +150,7 @@ class _WithdrawalPageState extends State<WithdrawalPage> {
),
),
child: const Text(
'탈퇴하기',
'Withdraw', // '탈퇴하기'
style: TextStyle(color: Colors.white, fontSize: 16),
),
),
@ -157,17 +165,25 @@ class _WithdrawalPageState extends State<WithdrawalPage> {
Future<void> _requestWithdrawal(String password, bool isAgreed) async {
if (!isAgreed) {
showResponseDialog(context, '회원탈퇴 동의 확인', '회원탈퇴 동의 체크가 필요합니다.');
showResponseDialog(
context,
'Withdrawal Agreement Required', // '회원탈퇴 동의 확인'
'You must agree to withdraw.' // '회원탈퇴 동의 체크가 필요합니다.'
);
return;
}
//
if (_oauthType != 'google' && password.isEmpty) {
showResponseDialog(context, '비밀번호 확인', '비밀번호를 입력해야 합니다.');
showResponseDialog(
context,
'Password Required', // '비밀번호 확인'
'Please enter your password.' // '비밀번호를 입력해야 합니다.'
);
return;
}
// ''
// ''
if (password.isEmpty) {
password = '';
}
@ -186,7 +202,11 @@ class _WithdrawalPageState extends State<WithdrawalPage> {
// &
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.remove('auth_token');
await showResponseDialog(context, '회원탈퇴 완료', '회원탈퇴가 완료되었습니다.');
await showResponseDialog(
context,
'Withdrawal Completed', // '회원탈퇴 완료'
'Your account has been successfully withdrawn.' // '회원탈퇴가 완료되었습니다.'
);
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const LoginPage()),
@ -199,7 +219,12 @@ class _WithdrawalPageState extends State<WithdrawalPage> {
);
}
} else {
showResponseDialog(context, '회원탈퇴 실패', '서버에 문제가 있습니다. 관리자에게 문의해주세요.');
showResponseDialog(
context,
'Withdrawal Failed', // '회원탈퇴 실패'
'There is a problem with the server. Please contact the administrator.'
// '서버에 문제가 있습니다. 관리자에게 문의해주세요.'
);
}
}
}