release 적용 및 코드 전체 영어로 바꾸기 완료
This commit is contained in:
parent
7278c6e06e
commit
e7f95cacf2
@ -6,7 +6,7 @@
|
||||
|
||||
|
||||
<application
|
||||
android:label="올스코어"
|
||||
android:label="ALLSCORE"
|
||||
android:name="${applicationName}"
|
||||
android:icon="@mipmap/ic_launcher">
|
||||
|
||||
|
@ -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>
|
||||
|
@ -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. 개인정보의 안전성 확보 조치
|
||||
회사는 개인정보의 안전한 처리를 위하여 기술적, 관리적 보호조치를 시행하고 있습니다.
|
||||
개인정보의 암호화
|
||||
해킹 등에 대비한 대책
|
||||
접근 통제 장치의 설치 및 운영
|
||||
''',
|
||||
*/
|
@ -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'),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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,
|
||||
|
@ -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),
|
||||
);
|
||||
|
@ -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) {
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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 app’s 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),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
|
@ -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),
|
||||
),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -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),
|
||||
),
|
||||
),
|
||||
],
|
||||
|
@ -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'),
|
||||
),
|
||||
],
|
||||
);
|
||||
|
@ -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
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
),
|
||||
),
|
||||
|
@ -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) 하단 광고 영역 */,
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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),
|
||||
),
|
||||
]
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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) 하단 광고 영역 */
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 6–20 characters, letters and digits, and cannot start with a digit.';
|
||||
} else if (label == 'Password') { // '비밀번호'
|
||||
_passwordError = _isPasswordValidPattern(_password)
|
||||
? null
|
||||
: 'Password must be 8–20 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 2–20 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> {
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -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 = '',
|
||||
|
@ -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,
|
||||
|
@ -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));
|
||||
}
|
||||
|
||||
/// 팀 박스 (팀별 점수 표시 + 1등2등3등 금/은/동 배경)
|
||||
/// 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),
|
||||
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -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)),
|
||||
|
@ -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(),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -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(
|
||||
|
@ -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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
@ -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)),
|
||||
],
|
||||
|
@ -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),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
|
@ -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
@ -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.'
|
||||
// '서버에 문제가 있습니다. 관리자에게 문의해주세요.'
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
Loading…
Reference in New Issue
Block a user