최신화

This commit is contained in:
eld_master 2025-01-07 22:38:36 +09:00
parent 0f450c884f
commit 3069552557
59 changed files with 7544 additions and 49 deletions

12
.gitattributes vendored Normal file
View File

@ -0,0 +1,12 @@
#
# https://help.github.com/articles/dealing-with-line-endings/
#
# Linux start script should use lf
/gradlew text eol=lf
# These are Windows script files and should use crlf
*.bat text eol=crlf
# Binary files should be left untouched
*.jar binary

6
.gitignore vendored
View File

@ -43,3 +43,9 @@ app.*.map.json
/android/app/debug
/android/app/profile
/android/app/release
# Ignore Gradle project-specific cache directory
.gradle
# Ignore Gradle build output directory
build

View File

@ -1,33 +1,33 @@
plugins {
id "com.android.application"
// START: FlutterFire Configuration
id 'com.google.gms.google-services'
// END: FlutterFire Configuration
id "kotlin-android"
// The Flutter Gradle Plugin must be applied after the Android and Kotlin Gradle plugins.
id "dev.flutter.flutter-gradle-plugin"
}
android {
namespace = "com.example.allscore_app"
compileSdk = flutter.compileSdkVersion
namespace = "com.allscore_app"
compileSdkVersion = 34
ndkVersion = flutter.ndkVersion
compileOptions {
sourceCompatibility = JavaVersion.VERSION_1_8
targetCompatibility = JavaVersion.VERSION_1_8
sourceCompatibility = JavaVersion.VERSION_17
targetCompatibility = JavaVersion.VERSION_17
}
kotlinOptions {
jvmTarget = JavaVersion.VERSION_1_8
jvmTarget = JavaVersion.VERSION_17
}
defaultConfig {
// TODO: Specify your own unique Application ID (https://developer.android.com/studio/build/application-id.html).
applicationId = "com.example.allscore_app"
// You can update the following values to match your application needs.
// For more information, see: https://flutter.dev/to/review-gradle-config.
minSdk = flutter.minSdkVersion
targetSdk = flutter.targetSdkVersion
versionCode = flutter.versionCode
versionName = flutter.versionName
applicationId = "com.allscore_app"
minSdkVersion 23
targetSdkVersion 34
versionCode = 1
versionName = "1.0"
}
buildTypes {

View File

@ -0,0 +1,30 @@
{
"project_info": {
"project_number": "70449524223",
"firebase_url": "https://allscore-344c2-default-rtdb.asia-southeast1.firebasedatabase.app",
"project_id": "allscore-344c2",
"storage_bucket": "allscore-344c2.firebasestorage.app"
},
"client": [
{
"client_info": {
"mobilesdk_app_id": "1:70449524223:android:94ffb9ec98e508313e4bca",
"android_client_info": {
"package_name": "com.allscore_app"
}
},
"oauth_client": [],
"api_key": [
{
"current_key": "AIzaSyAJEItMxO-TemHGlveSKySG-eNaTD9XJI0"
}
],
"services": {
"appinvite_service": {
"other_platform_oauth_client": []
}
}
}
],
"configuration_version": "1"
}

View File

@ -1,8 +1,18 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android">
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.allscore_app">
<uses-permission android:name="android.permission.READ_EXTERNAL_STORAGE"/>
<application
android:label="allscore_app"
android:name="${applicationName}"
android:icon="@mipmap/ic_launcher">
<!-- ★ 여기에 meta-data 추가 ★ -->
<meta-data
android:name="com.google.android.gms.ads.APPLICATION_ID"
android:value="ca-app-pub-3151339278746301~2596989191" />
<activity
android:name=".MainActivity"
android:exported="true"
@ -18,19 +28,21 @@
to determine the Window background behind the Flutter UI. -->
<meta-data
android:name="io.flutter.embedding.android.NormalTheme"
android:resource="@style/NormalTheme"
/>
android:resource="@style/NormalTheme" />
<intent-filter>
<action android:name="android.intent.action.MAIN"/>
<category android:name="android.intent.category.LAUNCHER"/>
</intent-filter>
</activity>
<!-- Don't delete the meta-data below.
This is used by the Flutter tool to generate GeneratedPluginRegistrant.java -->
<meta-data
android:name="flutterEmbedding"
android:value="2" />
</application>
<!-- Required to query activities that can process text, see:
https://developer.android.com/training/package-visibility and
https://developer.android.com/reference/android/content/Intent#ACTION_PROCESS_TEXT.
@ -42,4 +54,5 @@
<data android:mimeType="text/plain"/>
</intent>
</queries>
</manifest>

View File

@ -1,4 +1,4 @@
package com.example.allscore_app
package com.allscore_app
import io.flutter.embedding.android.FlutterActivity

View File

@ -1,3 +1,19 @@
// build.gradle ( android/build.gradle)
buildscript {
repositories {
google()
mavenCentral()
}
dependencies {
// Android Gradle Plugin
classpath "com.android.tools.build:gradle:8.2.1"
// Kotlin classpath가
// classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.10"
}
}
//
allprojects {
repositories {
google()
@ -6,6 +22,7 @@ allprojects {
}
rootProject.buildDir = "../build"
subprojects {
project.buildDir = "${rootProject.buildDir}/${project.name}"
}

View File

@ -1,3 +1,4 @@
org.gradle.jvmargs=-Xmx4G -XX:MaxMetaspaceSize=2G -XX:+HeapDumpOnOutOfMemoryError
android.useAndroidX=true
android.enableJetifier=true
org.gradle.java.home=C:\\Program Files\\Java\\jdk-17

View File

@ -2,4 +2,5 @@ distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip

View File

@ -18,8 +18,11 @@ pluginManagement {
plugins {
id "dev.flutter.flutter-plugin-loader" version "1.0.0"
id "com.android.application" version "8.1.0" apply false
id "org.jetbrains.kotlin.android" version "1.8.22" apply false
id "com.android.application" version "8.2.1" apply false
// START: FlutterFire Configuration
id "com.google.gms.google-services" version "4.3.15" apply false
// END: FlutterFire Configuration
id "org.jetbrains.kotlin.android" version "1.8.10" apply false
}
include ":app"

43
app/build.gradle.kts Normal file
View File

@ -0,0 +1,43 @@
/*
* This file was generated by the Gradle 'init' task.
*
* This generated file contains a sample Java application project to get you started.
* For more details on building Java & JVM projects, please refer to https://docs.gradle.org/8.12/userguide/building_java_projects.html in the Gradle documentation.
*/
plugins {
// Apply the application plugin to add support for building a CLI application in Java.
application
}
repositories {
// Use Maven Central for resolving dependencies.
mavenCentral()
}
dependencies {
// Use JUnit Jupiter for testing.
testImplementation(libs.junit.jupiter)
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
// This dependency is used by the application.
implementation(libs.guava)
}
// Apply a specific Java toolchain to ease working on different environments.
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
}
}
application {
// Define the main class for the application.
mainClass = "org.example.App"
}
tasks.named<Test>("test") {
// Use JUnit Platform for unit tests.
useJUnitPlatform()
}

View File

@ -0,0 +1,14 @@
/*
* This source file was generated by the Gradle 'init' task
*/
package org.example;
public class App {
public String getGreeting() {
return "Hello World!";
}
public static void main(String[] args) {
System.out.println(new App().getGreeting());
}
}

View File

@ -0,0 +1,14 @@
/*
* This source file was generated by the Gradle 'init' task
*/
package org.example;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
class AppTest {
@Test void appHasAGreeting() {
App classUnderTest = new App();
assertNotNull(classUnderTest.getGreeting(), "app should have a greeting");
}
}

1
firebase.json Normal file
View File

@ -0,0 +1 @@
{"flutter":{"platforms":{"android":{"default":{"projectId":"allscore-344c2","appId":"1:70449524223:android:94ffb9ec98e508313e4bca","fileOutput":"android/app/google-services.json"}},"dart":{"lib/firebase_options.dart":{"projectId":"allscore-344c2","configurations":{"android":"1:70449524223:android:94ffb9ec98e508313e4bca","ios":"1:70449524223:ios:98ebdbaa616a807f3e4bca","macos":"1:70449524223:ios:98ebdbaa616a807f3e4bca","web":"1:70449524223:web:e9c27da6646d655f3e4bca","windows":"1:70449524223:web:479dd789b837f54c3e4bca"}}}}}}

5
gradle.properties Normal file
View File

@ -0,0 +1,5 @@
# This file was generated by the Gradle 'init' task.
# https://docs.gradle.org/current/userguide/build_environment.html#sec:gradle_configuration_properties
org.gradle.configuration-cache=true

10
gradle/libs.versions.toml Normal file
View File

@ -0,0 +1,10 @@
# This file was generated by the Gradle 'init' task.
# https://docs.gradle.org/current/userguide/platforms.html#sub::toml-dependencies-format
[versions]
guava = "33.3.1-jre"
junit-jupiter = "5.11.1"
[libraries]
guava = { module = "com.google.guava:guava", version.ref = "guava" }
junit-jupiter = { module = "org.junit.jupiter:junit-jupiter", version.ref = "junit-jupiter" }

BIN
gradle/wrapper/gradle-wrapper.jar vendored Normal file

Binary file not shown.

View File

@ -0,0 +1,7 @@
distributionBase=GRADLE_USER_HOME
distributionPath=wrapper/dists
distributionUrl=https\://services.gradle.org/distributions/gradle-8.12-bin.zip
networkTimeout=10000
validateDistributionUrl=true
zipStoreBase=GRADLE_USER_HOME
zipStorePath=wrapper/dists

251
gradlew vendored Normal file
View File

@ -0,0 +1,251 @@
#!/bin/sh
#
# Copyright © 2015-2021 the original authors.
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# https://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
#
# SPDX-License-Identifier: Apache-2.0
#
##############################################################################
#
# Gradle start up script for POSIX generated by Gradle.
#
# Important for running:
#
# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
# noncompliant, but you have some other compliant shell such as ksh or
# bash, then to run this script, type that shell name before the whole
# command line, like:
#
# ksh Gradle
#
# Busybox and similar reduced shells will NOT work, because this script
# requires all of these POSIX shell features:
# * functions;
# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
# * compound commands having a testable exit status, especially «case»;
# * various built-in commands including «command», «set», and «ulimit».
#
# Important for patching:
#
# (2) This script targets any POSIX shell, so it avoids extensions provided
# by Bash, Ksh, etc; in particular arrays are avoided.
#
# The "traditional" practice of packing multiple parameters into a
# space-separated string is a well documented source of bugs and security
# problems, so this is (mostly) avoided, by progressively accumulating
# options in "$@", and eventually passing that to Java.
#
# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
# see the in-line comments for details.
#
# There are tweaks for specific operating systems such as AIX, CygWin,
# Darwin, MinGW, and NonStop.
#
# (3) This script is generated from the Groovy template
# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
# within the Gradle project.
#
# You can find Gradle at https://github.com/gradle/gradle/.
#
##############################################################################
# Attempt to set APP_HOME
# Resolve links: $0 may be a link
app_path=$0
# Need this for daisy-chained symlinks.
while
APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
[ -h "$app_path" ]
do
ls=$( ls -ld "$app_path" )
link=${ls#*' -> '}
case $link in #(
/*) app_path=$link ;; #(
*) app_path=$APP_HOME$link ;;
esac
done
# This is normally unused
# shellcheck disable=SC2034
APP_BASE_NAME=${0##*/}
# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036)
APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit
# Use the maximum available, or set MAX_FD != -1 to use that value.
MAX_FD=maximum
warn () {
echo "$*"
} >&2
die () {
echo
echo "$*"
echo
exit 1
} >&2
# OS specific support (must be 'true' or 'false').
cygwin=false
msys=false
darwin=false
nonstop=false
case "$( uname )" in #(
CYGWIN* ) cygwin=true ;; #(
Darwin* ) darwin=true ;; #(
MSYS* | MINGW* ) msys=true ;; #(
NONSTOP* ) nonstop=true ;;
esac
CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
# Determine the Java command to use to start the JVM.
if [ -n "$JAVA_HOME" ] ; then
if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
# IBM's JDK on AIX uses strange locations for the executables
JAVACMD=$JAVA_HOME/jre/sh/java
else
JAVACMD=$JAVA_HOME/bin/java
fi
if [ ! -x "$JAVACMD" ] ; then
die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
else
JAVACMD=java
if ! command -v java >/dev/null 2>&1
then
die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
Please set the JAVA_HOME variable in your environment to match the
location of your Java installation."
fi
fi
# Increase the maximum file descriptors if we can.
if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
case $MAX_FD in #(
max*)
# In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
MAX_FD=$( ulimit -H -n ) ||
warn "Could not query maximum file descriptor limit"
esac
case $MAX_FD in #(
'' | soft) :;; #(
*)
# In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked.
# shellcheck disable=SC2039,SC3045
ulimit -n "$MAX_FD" ||
warn "Could not set maximum file descriptor limit to $MAX_FD"
esac
fi
# Collect all arguments for the java command, stacking in reverse order:
# * args from the command line
# * the main class name
# * -classpath
# * -D...appname settings
# * --module-path (only if needed)
# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
# For Cygwin or MSYS, switch paths to Windows format before running java
if "$cygwin" || "$msys" ; then
APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
JAVACMD=$( cygpath --unix "$JAVACMD" )
# Now convert the arguments - kludge to limit ourselves to /bin/sh
for arg do
if
case $arg in #(
-*) false ;; # don't mess with options #(
/?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
[ -e "$t" ] ;; #(
*) false ;;
esac
then
arg=$( cygpath --path --ignore --mixed "$arg" )
fi
# Roll the args list around exactly as many times as the number of
# args, so each arg winds up back in the position where it started, but
# possibly modified.
#
# NB: a `for` loop captures its iteration list before it begins, so
# changing the positional parameters here affects neither the number of
# iterations, nor the values presented in `arg`.
shift # remove old arg
set -- "$@" "$arg" # push replacement arg
done
fi
# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
# Collect all arguments for the java command:
# * DEFAULT_JVM_OPTS, JAVA_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments,
# and any embedded shellness will be escaped.
# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be
# treated as '${Hostname}' itself on the command line.
set -- \
"-Dorg.gradle.appname=$APP_BASE_NAME" \
-classpath "$CLASSPATH" \
org.gradle.wrapper.GradleWrapperMain \
"$@"
# Stop when "xargs" is not available.
if ! command -v xargs >/dev/null 2>&1
then
die "xargs is not available"
fi
# Use "xargs" to parse quoted args.
#
# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
#
# In Bash we could simply go:
#
# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
# set -- "${ARGS[@]}" "$@"
#
# but POSIX shell has neither arrays nor command substitution, so instead we
# post-process each arg (as a line of input to sed) to backslash-escape any
# character that might be a shell metacharacter, then use eval to reverse
# that process (while maintaining the separation between arguments), and wrap
# the whole thing up as a single "set" statement.
#
# This will of course break if any of these variables contains a newline or
# an unmatched quote.
#
eval "set -- $(
printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
xargs -n1 |
sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
tr '\n' ' '
)" '"$@"'
exec "$JAVACMD" "$@"

94
gradlew.bat vendored Normal file
View File

@ -0,0 +1,94 @@
@rem
@rem Copyright 2015 the original author or authors.
@rem
@rem Licensed under the Apache License, Version 2.0 (the "License");
@rem you may not use this file except in compliance with the License.
@rem You may obtain a copy of the License at
@rem
@rem https://www.apache.org/licenses/LICENSE-2.0
@rem
@rem Unless required by applicable law or agreed to in writing, software
@rem distributed under the License is distributed on an "AS IS" BASIS,
@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
@rem See the License for the specific language governing permissions and
@rem limitations under the License.
@rem
@rem SPDX-License-Identifier: Apache-2.0
@rem
@if "%DEBUG%"=="" @echo off
@rem ##########################################################################
@rem
@rem Gradle startup script for Windows
@rem
@rem ##########################################################################
@rem Set local scope for the variables with windows NT shell
if "%OS%"=="Windows_NT" setlocal
set DIRNAME=%~dp0
if "%DIRNAME%"=="" set DIRNAME=.
@rem This is normally unused
set APP_BASE_NAME=%~n0
set APP_HOME=%DIRNAME%
@rem Resolve any "." and ".." in APP_HOME to make it shorter.
for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
@rem Find java.exe
if defined JAVA_HOME goto findJavaFromJavaHome
set JAVA_EXE=java.exe
%JAVA_EXE% -version >NUL 2>&1
if %ERRORLEVEL% equ 0 goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:findJavaFromJavaHome
set JAVA_HOME=%JAVA_HOME:"=%
set JAVA_EXE=%JAVA_HOME%/bin/java.exe
if exist "%JAVA_EXE%" goto execute
echo. 1>&2
echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
echo. 1>&2
echo Please set the JAVA_HOME variable in your environment to match the 1>&2
echo location of your Java installation. 1>&2
goto fail
:execute
@rem Setup the command line
set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
@rem Execute Gradle
"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
:end
@rem End local scope for the variables with windows NT shell
if %ERRORLEVEL% equ 0 goto mainEnd
:fail
rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
rem the _cmd.exe /c_ return code!
set EXIT_CODE=%ERRORLEVEL%
if %EXIT_CODE% equ 0 set EXIT_CODE=1
if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
exit /b %EXIT_CODE%
:mainEnd
if "%OS%"=="Windows_NT" endlocal
:omega

View File

@ -45,5 +45,9 @@
<true/>
<key>UIApplicationSupportsIndirectInputEvents</key>
<true/>
<key>GADApplicationIdentifier</key>
<string>ca-app-pub-3151339278746301~1689299887</string> <!-- AdMob 앱 ID 추가 -->
<key>NSPhotoLibraryUsageDescription</key>
<string>이 앱이 사진 라이브러리에 접근할 수 있도록 허용합니다.</string>
</dict>
</plist>

View File

@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
/// void였던 Future<void>
Future<void> showResponseDialog(BuildContext context, String title, String message) {
return showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor: Colors.white,
title: Center(
child: Text(
title,
style: const TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
),
content: Text(
message,
style: const TextStyle(
fontSize: 16,
color: Colors.black,
),
),
actions: <Widget>[
Center(
child: TextButton(
onPressed: () {
Navigator.of(context).pop(); // Dialog를
},
style: ButtonStyle(
backgroundColor: MaterialStateProperty.all(Colors.black),
foregroundColor: MaterialStateProperty.all(Colors.white),
padding: MaterialStateProperty.all(
const EdgeInsets.symmetric(horizontal: 20, vertical: 10),
),
),
child: const Text('확인'),
),
),
],
);
},
);
}

View File

@ -0,0 +1,267 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../plugins/api.dart'; // (Api.serverRequest)
import '../../dialogs/response_dialog.dart'; // /
import '../../views/room/waiting_room_team_page.dart'; // : import
import '../../views/room/waiting_room_private_page.dart'; // : import
///
class RoomDetailDialog extends StatefulWidget {
final Map<String, dynamic> roomData;
const RoomDetailDialog({Key? key, required this.roomData}) : super(key: key);
@override
State<RoomDetailDialog> createState() => _RoomDetailDialogState();
}
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 int roomSeq;
late String roomType; // "private" "team"
/// ( + )
final TextEditingController _pwController = TextEditingController();
@override
void initState() {
super.initState();
// roomData에서
roomTitle = widget.roomData['room_title'] ?? '(방제목 없음)';
roomIntro = widget.roomData['room_intro'] ?? '';
roomStatus = widget.roomData['room_status'] ?? '대기중';
openYn = widget.roomData['open_yn'] ?? '공개';
//
roomSeq = widget.roomData['room_seq'] ?? 0;
roomType = widget.roomData['room_type'] ?? 'private';
// "TEAM"/"team"
// '비공개' true
isPrivate = (openYn == '비공개');
// Flag
isWait = (roomStatus == '대기중');
isRunning = (roomStatus == '진행중');
isFinish = (roomStatus == '종료');
}
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
// +
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 24),
child: Column(
mainAxisSize: MainAxisSize.min, //
crossAxisAlignment: CrossAxisAlignment.start,
children: [
// (A) ( )
Center(
child: Text(
roomTitle,
style: const TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
),
const SizedBox(height: 20),
// (B) "방 소개"
const Text(
'방 소개',
style: TextStyle(
fontWeight: FontWeight.bold,
color: Colors.black,
fontSize: 14,
),
),
const SizedBox(height: 6),
// (C)
Container(
height: 80,
padding: const EdgeInsets.all(8),
child: SingleChildScrollView(
child: Text(
roomIntro.isNotEmpty ? roomIntro : '소개글이 없습니다.',
style: const TextStyle(color: Colors.black),
),
),
),
const SizedBox(height: 16),
// (D) /
Text(
isPrivate ? '비공개방' : '공개방',
style: const TextStyle(
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
// (D-1) ( + )
if (isPrivate && isWait) ...[
const SizedBox(height: 16),
TextField(
controller: _pwController,
obscureText: true,
decoration: const InputDecoration(
labelText: '비밀번호',
labelStyle: TextStyle(color: Colors.black),
border: OutlineInputBorder(),
),
),
],
const SizedBox(height: 24),
// (E)
_buildBottomButton(),
],
),
),
);
}
///
Widget _buildBottomButton() {
if (isWait) {
// (A) -> "입장" + "닫기"
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
_buildBlackButton(
label: '입장',
onTap: _onEnterRoom, //
),
_buildBlackButton(
label: '닫기',
onTap: () => Navigator.pop(context),
),
],
);
} else if (isRunning) {
// (B) -> "확인" ( )
return Center(
child: _buildBlackButton(
label: '확인',
onTap: () => Navigator.pop(context),
),
);
} else {
// (C) -> "결과보기", "확인" ( )
return Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
SizedBox(
width: 100,
child: _buildBlackButton(
label: '결과보기',
onTap: () {
// TODO:
Navigator.pop(context);
},
),
),
SizedBox(
width: 100,
child: _buildBlackButton(
label: '확인',
onTap: () => Navigator.pop(context),
),
),
],
);
}
}
/// "입장"
Future<void> _onEnterRoom() async {
final pw = _pwController.text.trim();
// API
final requestBody = {
"room_seq": "$roomSeq",
"room_type": roomType,
"room_pw": pw, // or
};
try {
final response = await Api.serverRequest(
uri: '/room/score/enter/room',
body: requestBody,
);
if (response == null || response['result'] != 'OK') {
//
showResponseDialog(context, '오류', '방 입장 실패. 서버 통신 오류.');
return;
}
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
// ->
Navigator.pop(context); //
// room_type에 /
if (roomType.toLowerCase() == 'team') {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => WaitingRoomTeamPage(
roomSeq: roomSeq,
roomType: 'team',
),
),
);
} else {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => WaitingRoomPrivatePage(
roomSeq: roomSeq,
roomType: 'private',
),
),
);
}
} else {
//
final msgTitle = resp['response_info']?['msg_title'] ?? '방 입장 실패';
final msgContent = resp['response_info']?['msg_content'] ?? '오류가 발생했습니다.';
showResponseDialog(context, msgTitle, msgContent);
}
} catch (e) {
showResponseDialog(context, '오류', '서버 요청 중 예외 발생: $e');
}
}
///
Widget _buildBlackButton({
required String label,
required VoidCallback onTap,
}) {
return ElevatedButton(
onPressed: onTap,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 24, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: Text(label, style: const TextStyle(fontSize: 14)),
);
}
}

View File

@ -0,0 +1,490 @@
import 'package:flutter/material.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../plugins/api.dart'; // API
import 'response_dialog.dart'; //
class RoomSettingModal extends StatefulWidget {
final Map<String, dynamic> roomInfo;
// : {
// "room_seq": "13",
// "room_master_yn": "Y",
// "room_title": "...",
// "room_type": "private" or "team"
// ...
// }
const RoomSettingModal({Key? key, required this.roomInfo}) : super(key: key);
@override
State<RoomSettingModal> createState() => _RoomSettingModalState();
}
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)
// FRD
late DatabaseReference _roomRef;
bool _isLoading = true;
// ( )
late bool isPrivateType; // true이면 , false이면
@override
void initState() {
super.initState();
// (1) room_seq
roomSeq = int.tryParse('${widget.roomInfo['room_seq'] ?? '0'}') ?? 0;
// (2)
final roomTypeStr = (widget.roomInfo['room_type'] ?? 'private').toString().toLowerCase();
// room_type "private" , (: "team")
isPrivateType = (roomTypeStr == 'private');
// (3) firebase ref
final roomKey = 'korea-$roomSeq';
_roomRef = FirebaseDatabase.instance.ref('rooms/$roomKey/roomInfo');
// (4) user_seq와 user_seq + FRD에서 roomInfo
_checkMasterAndFetchData();
}
/// my_user_seq를 ,
/// FRD에서 roomInfo를 state
Future<void> _checkMasterAndFetchData() async {
final prefs = await SharedPreferences.getInstance();
final myUserSeq = prefs.getInt('my_user_seq') ?? 0;
final snapshot = await _roomRef.get();
if (!snapshot.exists) {
//
setState(() {
_isLoading = false;
isMaster = false;
roomTitle = '방 정보 없음';
});
return;
}
final data = snapshot.value as Map<dynamic, dynamic>? ?? {};
// master_user_seq, open_yn, etc
final masterSeq = data['master_user_seq'] ?? 0;
setState(() {
isMaster = (masterSeq.toString() == myUserSeq.toString());
//
roomTitle = data['room_title']?.toString() ?? '';
roomIntro = data['room_intro']?.toString() ?? '';
openYn = data['open_yn']?.toString() ?? 'Y';
roomPw = data['room_pw']?.toString() ?? '';
runningTime = _toInt(data['running_time'], 1);
numberOfPeople = _toInt(data['number_of_people'], 10);
scoreOpenRange = data['score_open_range']?.toString() ?? 'PRIVATE';
_isLoading = false;
});
}
/// int
int _toInt(dynamic val, int defaultVal) {
if (val == null) return defaultVal;
if (val is int) return val;
if (val is String) {
return int.tryParse(val) ?? defaultVal;
}
return defaultVal;
}
/// ()
Future<void> _onUpdate() async {
// API로
final requestBody = {
'room_seq': '$roomSeq',
'room_status': 'WAIT',
'room_title': roomTitle,
'room_intro': roomIntro,
'open_yn': openYn,
'room_pw': roomPw,
'running_time': '$runningTime',
// widget.roomInfo['room_type']
'room_type': widget.roomInfo['room_type'] ?? 'private',
'number_of_people': '$numberOfPeople',
'score_open_range': scoreOpenRange,
};
// ( number_of_teams )
if (!isPrivateType) {
// : 4
// FRD에서 roomInfo['number_of_teams']
requestBody['number_of_teams'] = '4';
}
try {
final response = await Api.serverRequest(
uri: '/room/score/update/room/setting/info',
body: requestBody,
);
if (response['result'] == 'OK') {
final resp = response['response'] ?? {};
final serverResult = resp['result'] ?? 'FAIL';
if (serverResult == 'OK') {
await showResponseDialog(
context,
'성공',
'방 설정이 성공적으로 수정되었습니다.',
);
Navigator.pop(context, 'refresh');
} else {
//
final msgTitle = resp['response_info']?['msg_title'] ?? '수정 실패';
final msgContent = resp['response_info']?['msg_content'] ?? '오류가 발생했습니다.';
showResponseDialog(context, msgTitle, msgContent);
}
} else {
showResponseDialog(context, '실패', '서버 통신에 실패했습니다.');
}
} catch (e) {
showResponseDialog(context, '오류 발생', '서버 요청 중 오류가 발생했습니다.\n$e');
}
}
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: _isLoading
? const SizedBox(
height: 100,
child: Center(child: CircularProgressIndicator()),
)
: SingleChildScrollView(
padding: const EdgeInsets.symmetric(horizontal: 16, vertical: 12),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
//
Text(
'방 설정 정보',
style: TextStyle(
fontSize: 18,
fontWeight: FontWeight.bold,
color: Colors.black.withOpacity(0.9),
),
),
const SizedBox(height: 12),
// (1)
_buildTitle('방 제목'),
TextField(
readOnly: !isMaster,
controller: TextEditingController(text: roomTitle),
onChanged: (value) => roomTitle = value,
style: const TextStyle(color: Colors.black),
decoration: InputDecoration(
border: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black.withOpacity(0.8)),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
isDense: true,
),
),
const SizedBox(height: 12),
// (2)
_buildTitle('방 소개'),
SizedBox(
height: 60,
child: TextField(
readOnly: !isMaster,
controller: TextEditingController(text: roomIntro),
onChanged: (value) => roomIntro = value,
maxLines: null,
expands: true,
style: const TextStyle(color: Colors.black),
textAlignVertical: TextAlignVertical.top,
decoration: InputDecoration(
border: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black.withOpacity(0.8)),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
isDense: true,
),
),
),
const SizedBox(height: 12),
// (3) (/)
_buildTitle('비밀번호 설정'),
Row(
children: [
Radio<String>(
value: 'Y',
groupValue: openYn,
activeColor: Colors.black,
onChanged: isMaster
? (val) {
setState(() {
openYn = val ?? 'Y';
});
}
: null,
),
const Text('공개', style: TextStyle(color: Colors.black)),
const SizedBox(width: 8),
Radio<String>(
value: 'N',
groupValue: openYn,
activeColor: Colors.black,
onChanged: isMaster
? (val) {
setState(() {
openYn = val ?? 'N';
});
}
: null,
),
const Text('비공개', style: TextStyle(color: Colors.black)),
],
),
if (openYn == 'N') ...[
SizedBox(
height: 40,
child: TextField(
readOnly: !isMaster,
obscureText: true,
onChanged: (value) => roomPw = value,
style: const TextStyle(color: Colors.black),
decoration: InputDecoration(
border: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black.withOpacity(0.8)),
),
hintText: '비밀번호 입력',
hintStyle: TextStyle(color: Colors.black.withOpacity(0.4)),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
isDense: true,
),
),
),
const SizedBox(height: 12),
],
// (4)
_buildTitle('운영시간'),
Row(
children: [
DropdownButton<int>(
value: runningTime,
dropdownColor: Colors.white,
style: const TextStyle(color: Colors.black),
underline: Container(
height: 1,
color: Colors.black.withOpacity(0.8),
),
items: [1, 2, 3, 4, 5, 6]
.map((e) => DropdownMenuItem<int>(
value: e,
child: Text('$e', style: const TextStyle(color: Colors.black)),
))
.toList(),
onChanged: isMaster
? (val) {
if (val == null) return;
setState(() {
runningTime = val;
});
}
: null,
),
const SizedBox(width: 8),
const Text('시간', style: TextStyle(color: Colors.black)),
],
),
const SizedBox(height: 12),
// (5)
_buildTitle('최대 인원수'),
Row(
children: [
SizedBox(
width: 80,
height: 40,
child: TextField(
readOnly: !isMaster,
controller: TextEditingController(text: '$numberOfPeople'),
keyboardType: TextInputType.number,
onChanged: (value) {
setState(() {
numberOfPeople = int.tryParse(value) ?? numberOfPeople;
});
},
style: const TextStyle(color: Colors.black),
decoration: InputDecoration(
border: OutlineInputBorder(
borderSide: BorderSide(color: Colors.black.withOpacity(0.8)),
),
contentPadding: const EdgeInsets.symmetric(horizontal: 12, vertical: 8),
isDense: true,
),
),
),
const SizedBox(width: 8),
const Text('', style: TextStyle(color: Colors.black)),
],
),
const SizedBox(height: 12),
// (6)
_buildTitle('점수 공개 범위'),
// PRIVATE, ALL
// PRIVATE, TEAM, ALL
if (isPrivateType)
Column(
children: [
// PRIVATE
RadioListTile<String>(
value: 'PRIVATE',
groupValue: scoreOpenRange,
activeColor: Colors.black,
title: const Text('개인', style: TextStyle(color: Colors.black)),
onChanged: isMaster
? (val) {
if (val != null) {
setState(() => scoreOpenRange = val);
}
}
: null,
),
// ALL
RadioListTile<String>(
value: 'ALL',
groupValue: scoreOpenRange,
activeColor: Colors.black,
title: const Text('전체', style: TextStyle(color: Colors.black)),
onChanged: isMaster
? (val) {
if (val != null) {
setState(() => scoreOpenRange = val);
}
}
: null,
),
],
)
else
// PRIVATE / TEAM / ALL
Column(
children: [
RadioListTile<String>(
value: 'PRIVATE',
groupValue: scoreOpenRange,
activeColor: Colors.black,
title: const Text('개인', style: TextStyle(color: Colors.black)),
onChanged: isMaster
? (val) {
if (val != null) {
setState(() => scoreOpenRange = val);
}
}
: null,
),
RadioListTile<String>(
value: 'TEAM',
groupValue: scoreOpenRange,
activeColor: Colors.black,
title: const Text('', style: TextStyle(color: Colors.black)),
onChanged: isMaster
? (val) {
if (val != null) {
setState(() => scoreOpenRange = val);
}
}
: null,
),
RadioListTile<String>(
value: 'ALL',
groupValue: scoreOpenRange,
activeColor: Colors.black,
title: const Text('전체', style: TextStyle(color: Colors.black)),
onChanged: isMaster
? (val) {
if (val != null) {
setState(() => scoreOpenRange = val);
}
}
: null,
),
],
),
const SizedBox(height: 20),
// (7)
if (isMaster)
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: _onUpdate,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.black,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
child: const Text('수정', style: TextStyle(color: Colors.white)),
),
ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.black,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
),
child: const Text('확인', style: TextStyle(color: Colors.white)),
),
],
)
else
Center(
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.black,
padding: const EdgeInsets.symmetric(horizontal: 40, vertical: 12),
),
child: const Text('확인', style: TextStyle(color: Colors.white)),
),
),
],
),
),
);
}
/// ()
Widget _buildTitle(String label) {
return Align(
alignment: Alignment.centerLeft,
child: Text(
label,
style: TextStyle(
fontWeight: FontWeight.bold,
fontSize: 14,
color: Colors.black.withOpacity(0.9),
),
),
);
}
}

View File

@ -0,0 +1,172 @@
import 'package:flutter/material.dart';
import '../plugins/api.dart'; //
import 'response_dialog.dart'; // etc
class ScoreEditDialog extends StatefulWidget {
final int roomSeq;
final String roomType; // "PRIVATE" or "TEAM"
final Map<String, dynamic> userData;
const ScoreEditDialog({
Key? key,
required this.roomSeq,
required this.roomType,
required this.userData,
}) : super(key: key);
@override
State<ScoreEditDialog> createState() => _ScoreEditDialogState();
}
class _ScoreEditDialogState extends State<ScoreEditDialog> {
late int currentScore; //
late int newScore; //
@override
void initState() {
super.initState();
currentScore = (widget.userData['score'] ?? 0) as int;
newScore = currentScore;
}
Future<void> _onApplyScore() async {
final reqBody = {
"room_seq": "${widget.roomSeq}",
"room_type": widget.roomType,
"target_user_seq": "${widget.userData['user_seq']}",
"after_score": "$newScore",
};
try {
final response = await Api.serverRequest(
uri: '/room/score/update/score',
body: reqBody,
);
if (response['result'] == 'OK') {
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
//
await showResponseDialog(context, '성공', '점수가 업데이트되었습니다.');
Navigator.pop(context, 'refresh');
} else {
final msgTitle = resp['response_info']?['msg_title'] ?? '오류';
final msgContent = resp['response_info']?['msg_content'] ?? '점수 업데이트 실패';
showResponseDialog(context, msgTitle, msgContent);
}
} else {
showResponseDialog(context, '실패', '서버 통신 실패');
}
} catch (e) {
showResponseDialog(context, '오류 발생', '$e');
}
}
void _onDelta(int delta) {
setState(() {
newScore += delta;
if (newScore < 0) newScore = 0; // 0
if (newScore > 999999) newScore = 999999; //
});
}
@override
Widget build(BuildContext context) {
final userName = widget.userData['nickname'] ?? '유저';
final department = widget.userData['department'] ?? '';
final introduce = widget.userData['introduce_myself'] ?? '';
return Dialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Text('유저 정보 보기', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
// &
Text(userName, style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
Text(department, style: TextStyle(fontSize: 14, color: Colors.grey)),
const SizedBox(height: 12),
//
const Align(
alignment: Alignment.centerLeft,
child: Text('소개글', style: TextStyle(fontWeight: FontWeight.bold)),
),
const SizedBox(height: 4),
Container(
width: double.infinity,
height: 80,
decoration: BoxDecoration(
border: Border.all(color: Colors.black),
borderRadius: BorderRadius.circular(8),
),
child: SingleChildScrollView(
padding: const EdgeInsets.all(8),
child: Text(introduce.isNotEmpty ? introduce : '소개글이 없습니다.'),
),
),
const SizedBox(height: 12),
//
Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Text('$currentScore',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const Text(''),
Text('$newScore',
style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
],
),
const SizedBox(height: 8),
Wrap(
spacing: 4,
runSpacing: 4,
children: [
_buildDeltaButton(-100),
_buildDeltaButton(-10),
_buildDeltaButton(-1),
_buildDeltaButton(1),
_buildDeltaButton(10),
_buildDeltaButton(100),
],
),
const SizedBox(height: 12),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
ElevatedButton(
onPressed: _onApplyScore,
style: ElevatedButton.styleFrom(backgroundColor: Colors.black),
child: const Text('적용', style: TextStyle(color: Colors.white)),
),
ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(backgroundColor: Colors.black),
child: const Text('닫기', style: TextStyle(color: Colors.white)),
),
],
),
],
),
),
);
}
Widget _buildDeltaButton(int delta) {
final label = (delta >= 0) ? '+$delta' : '$delta';
return ElevatedButton(
onPressed: () => _onDelta(delta),
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
side: BorderSide(color: Colors.black),
),
child: Text(label),
);
}
}

View File

@ -0,0 +1,88 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart'; // SharedPreferences
import '../views/login/login_page.dart'; // ( )
import '../views/user/my_page.dart'; // ( )
void showSettingsDialog(BuildContext context) {
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor: Colors.white,
title: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
Container(
width: MediaQuery.of(context).size.width * 0.2,
child: const Text(
'',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
),
Container(
width: MediaQuery.of(context).size.width * 0.2,
child: const Text(
'설정',
style: TextStyle(
fontSize: 24,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
),
IconButton(
icon: const Icon(Icons.close, color: Colors.black),
onPressed: () {
Navigator.of(context).pop();
},
),
],
),
content: Column(
mainAxisSize: MainAxisSize.min,
children: [
const SizedBox(height: 10),
SizedBox(
width: double.infinity,
child: TextButton(
onPressed: () {
Navigator.of(context).push(
MaterialPageRoute(builder: (context) => const MyPage()), //
);
},
style: ButtonStyle(
side: MaterialStateProperty.all(const BorderSide(color: Colors.black)),
foregroundColor: MaterialStateProperty.all(Colors.black),
),
child: const Text('내 정보 관리'),
),
),
const SizedBox(height: 10),
SizedBox(
width: double.infinity,
child: TextButton(
onPressed: () async {
//
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setString('auth_token', ''); // auth_token
Navigator.of(context).pushReplacement(
MaterialPageRoute(builder: (context) => const LoginPage()), //
);
},
style: ButtonStyle(
side: MaterialStateProperty.all(const BorderSide(color: Colors.black)),
foregroundColor: MaterialStateProperty.all(Colors.black),
),
child: const Text('로그아웃'),
),
),
],
),
);
},
);
}

View File

@ -0,0 +1,148 @@
import 'package:flutter/material.dart';
import 'response_dialog.dart';
import '../../plugins/api.dart';
class TeamNameEditModal extends StatefulWidget {
final int roomSeq; //
final String roomTypeName; // "TEAM"
final String beforeTeamName; //
final List<String> existingTeamNames; // (ex: ["A","B","C","WAIT"...]
const TeamNameEditModal({
Key? key,
required this.roomSeq,
required this.roomTypeName,
required this.beforeTeamName,
required this.existingTeamNames,
}) : super(key: key);
@override
State<TeamNameEditModal> createState() => _TeamNameEditModalState();
}
class _TeamNameEditModalState extends State<TeamNameEditModal> {
String afterTeamName = '';
String _errorMsg = ''; //
Future<void> _onUpdateTeamName() async {
//
final newName = afterTeamName.trim().toUpperCase();
if (newName.isEmpty) {
setState(() {
_errorMsg = '새 팀명을 입력해주세요.';
});
return;
}
// (, teamName과 OK)
// : beforeTeamName= "A", user가 "B" existingTeamNames=["A","B","C"]
final existingNames = widget.existingTeamNames.map((e) => e.toUpperCase()).toList();
if (newName != widget.beforeTeamName.toUpperCase() && existingNames.contains(newName)) {
setState(() {
_errorMsg = '이미 존재하는 팀명입니다.';
});
return;
}
// body
// {
// "room_seq": "9",
// "room_type_name": "TEAM",
// "before_team_name": "A",
// "after_team_name": "B"
// }
final reqBody = {
'room_seq': '${widget.roomSeq}',
'room_type_name': widget.roomTypeName, // "TEAM"
'before_team_name': widget.beforeTeamName,
'after_team_name': newName,
};
try {
final response = await Api.serverRequest(
uri: '/room/score/update/team/name',
body: reqBody,
);
if (response['result'] == 'OK') {
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
//
await showResponseDialog(context, '성공', '팀명이 성공적으로 수정되었습니다.');
Navigator.pop(context, 'refresh');
} else {
final msgTitle = resp['response_info']?['msg_title'] ?? '오류';
final msgContent = resp['response_info']?['msg_content'] ?? '수정 실패';
showResponseDialog(context, msgTitle, msgContent);
}
} else {
showResponseDialog(context, '실패', '서버 통신 실패');
}
} catch (e) {
showResponseDialog(context, '오류 발생', '$e');
}
}
@override
Widget build(BuildContext context) {
return Dialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('팀명 수정',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
Text('기존 팀명: ${widget.beforeTeamName}'),
const SizedBox(height: 8),
TextField(
decoration: const InputDecoration(
border: OutlineInputBorder(),
labelText: '새 팀명',
),
onChanged: (val) {
setState(() {
afterTeamName = val;
_errorMsg = ''; //
});
},
),
//
if (_errorMsg.isNotEmpty) ...[
const SizedBox(height: 8),
Text(
_errorMsg,
style: const TextStyle(color: Colors.red),
),
],
const SizedBox(height: 20),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
SizedBox(
width: 100,
child: ElevatedButton(
onPressed: _onUpdateTeamName,
style: ElevatedButton.styleFrom(backgroundColor: Colors.black),
child:
const Text('수정', style: TextStyle(color: Colors.white)),
),
),
SizedBox(
width: 100,
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(backgroundColor: Colors.black),
child:
const Text('취소', style: TextStyle(color: Colors.white)),
),
),
],
),
],
),
),
);
}
}

View File

@ -0,0 +1,64 @@
/// user_info_basic_dialog.dart
import 'package:flutter/material.dart';
class UserInfoBasicDialog extends StatelessWidget {
final Map<String, dynamic> userData;
const UserInfoBasicDialog({
Key? key,
required this.userData,
}) : super(key: key);
@override
Widget build(BuildContext context) {
final userName = userData['nickname'] ?? '유저';
final department = userData['department'] ?? '';
final introduce = userData['introduce_myself'] ?? '';
return Dialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('유저 정보 (진행중-개인전)',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
Text(userName, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Text(department, style: const TextStyle(fontSize: 14, color: Colors.grey)),
const SizedBox(height: 12),
const Align(
alignment: Alignment.centerLeft,
child: Text('소개글', style: TextStyle(fontWeight: FontWeight.bold)),
),
const SizedBox(height: 4),
Container(
width: double.infinity,
height: 80,
decoration: BoxDecoration(
border: Border.all(color: Colors.black),
borderRadius: BorderRadius.circular(8),
),
child: SingleChildScrollView(
padding: const EdgeInsets.all(8),
child: Text(introduce.isNotEmpty ? introduce : '소개글이 없습니다.'),
),
),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(backgroundColor: Colors.black),
child: const Text('확인', style: TextStyle(color: Colors.white)),
),
],
),
),
);
}
}

View File

@ -0,0 +1,272 @@
import 'package:flutter/material.dart';
import 'response_dialog.dart';
import '../../plugins/api.dart';
class UserInfoPrivateDialog extends StatefulWidget {
final Map<String, dynamic> userData;
final bool isRoomMaster; //
final int roomSeq;
final String roomTypeName; // "PRIVATE"
const UserInfoPrivateDialog({
Key? key,
required this.userData,
required this.isRoomMaster,
required this.roomSeq,
required this.roomTypeName,
}) : super(key: key);
@override
State<UserInfoPrivateDialog> createState() => _UserInfoPrivateDialogState();
}
class _UserInfoPrivateDialogState extends State<UserInfoPrivateDialog> {
late String participantType; // 'ADMIN' or 'PLAYER'
late String introduceMyself;
@override
void initState() {
super.initState();
final rawType = (widget.userData['participant_type'] ?? 'PLAYER').toString().toUpperCase();
participantType = (rawType == 'ADMIN') ? 'ADMIN' : 'PLAYER';
introduceMyself = widget.userData['introduce_myself'] ?? '';
}
/// API ( )
Future<void> _onUpdateUserInfo() async {
//
if (!widget.isRoomMaster) return;
final requestBody = {
'room_seq': '${widget.roomSeq}',
'room_type_name': widget.roomTypeName, // "PRIVATE"
'target_user_seq': '${widget.userData['user_seq']}',
'participant_type': participantType,
};
try {
final response = await Api.serverRequest(
uri: '/room/score/update/user/role',
body: requestBody,
);
if (response['result'] == 'OK') {
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
await showResponseDialog(context, '성공', '역할이 성공적으로 수정되었습니다.');
Navigator.pop(context, 'refresh');
} else {
final msgTitle = resp['response_info']?['msg_title'] ?? '오류';
final msgContent = resp['response_info']?['msg_content'] ?? '수정 실패';
showResponseDialog(context, msgTitle, msgContent);
}
} else {
showResponseDialog(context, '실패', '서버 통신에 실패했습니다.');
}
} catch (e) {
showResponseDialog(context, '오류 발생', '$e');
}
}
/// : "추방하기" ( )
Future<void> _onKickParticipant() async {
// -> return
if (!widget.isRoomMaster) return;
final reqBody = {
"room_seq": "${widget.roomSeq}",
"target_user_seq": "${widget.userData['user_seq']}",
};
try {
final response = await Api.serverRequest(
uri: '/room/score/kick/participant',
body: reqBody,
);
if (response['result'] == 'OK') {
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
//
await showResponseDialog(context, '성공', '해당 유저가 강퇴되었습니다.');
Navigator.pop(context, 'refresh');
} else {
final msgTitle = resp['response_info']?['msg_title'] ?? '오류';
final msgContent = resp['response_info']?['msg_content'] ?? '강퇴 실패';
showResponseDialog(context, msgTitle, msgContent);
}
} else {
showResponseDialog(context, '실패', '서버 통신 실패');
}
} catch (e) {
showResponseDialog(context, '오류 발생', '$e');
}
}
@override
Widget build(BuildContext context) {
final userName = widget.userData['nickname'] ?? '유저';
final department = widget.userData['department'] ?? '소속정보없음';
final profileImg = widget.userData['profile_img'] ?? '';
return Dialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'유저 정보 (개인전)',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 12),
// (A)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Container(
width: 80, height: 80,
decoration: BoxDecoration(
border: Border.all(color: Colors.black),
borderRadius: BorderRadius.circular(16),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: (profileImg.isNotEmpty)
? Image.network(
'https://eldsoft.com:8097/images$profileImg',
fit: BoxFit.cover,
errorBuilder: (ctx, err, st) => const Center(
child: Text('이미지\n불가', textAlign: TextAlign.center),
),
)
: const Center(child: Text('이미지\n없음', textAlign: TextAlign.center)),
),
),
const SizedBox(width: 16),
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(
userName,
style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold),
),
const SizedBox(height: 6),
Text(
department,
style: const TextStyle(fontSize: 14),
),
],
),
),
],
),
const SizedBox(height: 16),
// (B)
if (widget.isRoomMaster) ...[
Row(
children: [
const Text('역할: ', style: TextStyle(fontWeight: FontWeight.bold)),
DropdownButton<String>(
value: participantType,
items: const [
DropdownMenuItem(value: 'ADMIN', child: Text('사회자')),
DropdownMenuItem(value: 'PLAYER', child: Text('참가자')),
],
onChanged: (val) {
if (val == null) return;
setState(() {
participantType = val;
});
},
),
],
),
const SizedBox(height: 12),
] else ...[
Text('역할: $participantType', style: const TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
],
// (C)
const Align(
alignment: Alignment.centerLeft,
child: Text('소개', style: TextStyle(fontWeight: FontWeight.bold)),
),
const SizedBox(height: 4),
Container(
width: double.infinity,
height: 100,
decoration: BoxDecoration(
border: Border.all(color: Colors.black),
borderRadius: BorderRadius.circular(8),
),
child: SingleChildScrollView(
padding: const EdgeInsets.all(8),
child: Text(
introduceMyself.isNotEmpty ? introduceMyself : '소개글이 없습니다.',
style: const TextStyle(fontSize: 14),
softWrap: true,
maxLines: 100,
overflow: TextOverflow.clip,
),
),
),
const SizedBox(height: 16),
// (D)
if (widget.isRoomMaster) ...[
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
// (D-1)
SizedBox(
width: 90,
child: ElevatedButton(
onPressed: _onUpdateUserInfo,
style: ElevatedButton.styleFrom(backgroundColor: Colors.black),
child: FittedBox(
child: Text('수정하기', style: TextStyle(color: Colors.white)),
),
),
),
// (D-2)
SizedBox(
width: 90,
child: ElevatedButton(
onPressed: _onKickParticipant,
style: ElevatedButton.styleFrom(backgroundColor: Colors.black),
child: FittedBox(
child: Text('추방하기', style: TextStyle(color: Colors.white)),
),
),
),
// (D-3)
SizedBox(
width: 90,
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(backgroundColor: Colors.black),
child: FittedBox(
child: Text('확인', style: TextStyle(color: Colors.white)),
),
),
),
],
),
] else ...[
ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(backgroundColor: Colors.black),
child: const Text('확인', style: TextStyle(color: Colors.white)),
),
],
],
),
),
);
}
}

View File

@ -0,0 +1,316 @@
import 'package:flutter/material.dart';
import 'response_dialog.dart';
import '../../plugins/api.dart';
class UserInfoTeamDialog extends StatefulWidget {
final Map<String, dynamic> userData;
final bool isRoomMaster; // "현재 로그인 유저"
final int roomSeq;
final String roomTypeName; // "TEAM"
final List<String> teamNameList;
const UserInfoTeamDialog({
Key? key,
required this.userData,
required this.isRoomMaster,
required this.roomSeq,
required this.roomTypeName,
required this.teamNameList,
}) : super(key: key);
@override
State<UserInfoTeamDialog> createState() => _UserInfoTeamDialogState();
}
class _UserInfoTeamDialogState extends State<UserInfoTeamDialog> {
late String participantType; // 'ADMIN' / 'PLAYER'
late String teamName; // 'A'/'B'/'WAIT'
late String introduceMyself; //
@override
void initState() {
super.initState();
final rawType = (widget.userData['participant_type'] ?? 'PLAYER').toString().toUpperCase();
participantType = (rawType == 'ADMIN') ? 'ADMIN' : 'PLAYER';
final rawTeam = (widget.userData['team_name'] ?? '').toString().trim().toUpperCase();
teamName = (rawTeam.isEmpty || rawTeam == 'WAIT') ? 'WAIT' : rawTeam;
introduceMyself = widget.userData['introduce_myself'] ?? '';
// teamNameList에 WAIT ( )
final hasWait = widget.teamNameList.map((e) => e.toUpperCase()).contains('WAIT');
if (!hasWait) {
widget.teamNameList.add('WAIT');
}
}
// (1) / API ( )
Future<void> _onUpdateUserInfo() async {
// -> return
if (!widget.isRoomMaster) return;
final reqBody = {
'room_seq': '${widget.roomSeq}',
'room_type_name': widget.roomTypeName, // "TEAM"
'target_user_seq': '${widget.userData['user_seq']}',
'participant_type': participantType,
'team_name': teamName,
};
try {
final response = await Api.serverRequest(
uri: '/room/score/update/user/role',
body: reqBody,
);
if (response['result'] == 'OK') {
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
await showResponseDialog(context, '성공', '역할/팀이 성공적으로 수정되었습니다.');
Navigator.pop(context, 'refresh');
} else {
final msgTitle = resp['response_info']?['msg_title'] ?? '오류';
final msgContent = resp['response_info']?['msg_content'] ?? '수정 실패';
showResponseDialog(context, msgTitle, msgContent);
}
} else {
showResponseDialog(context, '실패', '서버 통신 실패');
}
} catch (e) {
showResponseDialog(context, '오류 발생', '$e');
}
}
// (2) : "추방하기" API
Future<void> _onKickParticipant() async {
//
if (!widget.isRoomMaster) return;
final reqBody = {
"room_seq": "${widget.roomSeq}",
"target_user_seq": "${widget.userData['user_seq']}",
};
try {
final response = await Api.serverRequest(
uri: '/room/score/kick/participant',
body: reqBody,
);
if (response['result'] == 'OK') {
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
//
await showResponseDialog(context, '성공', '해당 유저가 강퇴되었습니다.');
Navigator.pop(context, 'refresh');
} else {
final msgTitle = resp['response_info']?['msg_title'] ?? '오류';
final msgContent = resp['response_info']?['msg_content'] ?? '강퇴 실패';
showResponseDialog(context, msgTitle, msgContent);
}
} else {
showResponseDialog(context, '실패', '서버 통신 실패');
}
} catch (e) {
showResponseDialog(context, '오류 발생', '$e');
}
}
@override
Widget build(BuildContext context) {
final userName = widget.userData['nickname'] ?? '유저';
final profileImg = widget.userData['profile_img'] ?? '';
final department = widget.userData['department'] ?? '소속정보 없음';
return Dialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(16)),
child: Padding(
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text('유저 정보 (팀전)', style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 12),
// (A)
Row(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
//
Container(
width: 80, height: 80,
decoration: BoxDecoration(
border: Border.all(color: Colors.black),
borderRadius: BorderRadius.circular(16),
),
child: ClipRRect(
borderRadius: BorderRadius.circular(16),
child: (profileImg.isNotEmpty)
? Image.network(
'https://eldsoft.com:8097/images$profileImg',
fit: BoxFit.cover,
errorBuilder: (ctx, err, st) =>
const Center(child: Text('이미지\n불가', textAlign: TextAlign.center)),
)
: const Center(child: Text('이미지\n없음', textAlign: TextAlign.center)),
),
),
const SizedBox(width: 16),
// +
Expanded(
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(userName, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 6),
Text(department, style: const TextStyle(fontSize: 14)),
],
),
),
],
),
const SizedBox(height: 16),
// (B) /
if (widget.isRoomMaster) ...[
//
Row(
children: [
const Text('역할: ', style: TextStyle(fontWeight: FontWeight.bold)),
DropdownButton<String>(
value: participantType,
items: const [
DropdownMenuItem(value: 'ADMIN', child: Text('사회자')),
DropdownMenuItem(value: 'PLAYER', child: Text('참가자')),
],
onChanged: (val) {
if (val == null) return;
setState(() {
participantType = val;
});
},
),
],
),
const SizedBox(height: 12),
//
Row(
children: [
const Text('팀명: ', style: TextStyle(fontWeight: FontWeight.bold)),
DropdownButton<String>(
value: widget.teamNameList
.map((e) => e.toUpperCase())
.contains(teamName) ? teamName : 'WAIT',
items: widget.teamNameList.map((t) => DropdownMenuItem(
value: t.toUpperCase(),
child: Text(t),
)).toList(),
onChanged: (val) {
if (val == null) return;
setState(() {
teamName = val;
});
},
),
],
),
const SizedBox(height: 16),
] else ...[
// (B') 일반유저 -> 그냥 정보만 표시
Text('역할: $participantType', style: const TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 6),
Text('팀명: $teamName', style: const TextStyle(fontWeight: FontWeight.bold)),
const SizedBox(height: 16),
],
// (C)
const Align(
alignment: Alignment.centerLeft,
child: Text('소개', style: TextStyle(fontWeight: FontWeight.bold)),
),
const SizedBox(height: 4),
Container(
width: double.infinity,
height: 100,
decoration: BoxDecoration(
border: Border.all(color: Colors.black),
borderRadius: BorderRadius.circular(8),
),
child: SingleChildScrollView(
padding: const EdgeInsets.all(8),
child: Text(
introduceMyself.isNotEmpty ? introduceMyself : '소개글이 없습니다.',
style: const TextStyle(fontSize: 14),
softWrap: true, //
maxLines: 100, //
overflow: TextOverflow.clip, // clip
),
),
),
const SizedBox(height: 16),
// (D)
if (widget.isRoomMaster) ...[
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
//
SizedBox(
width: 90,
child: ElevatedButton(
onPressed: _onUpdateUserInfo,
style: ElevatedButton.styleFrom(backgroundColor: Colors.black),
// () FittedBox
child: FittedBox(
child: Text(
'수정하기',
style: TextStyle(color: Colors.white),
),
),
),
),
//
SizedBox(
width: 90,
child: ElevatedButton(
onPressed: _onKickParticipant,
style: ElevatedButton.styleFrom(backgroundColor: Colors.black),
child: FittedBox(
child: Text(
'추방하기',
style: TextStyle(color: Colors.white),
),
),
),
),
//
SizedBox(
width: 90,
child: ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(backgroundColor: Colors.black),
child: FittedBox(
child: Text(
'확인',
style: TextStyle(color: Colors.white),
),
),
),
),
],
),
] else ...[
// "확인"
ElevatedButton(
onPressed: () => Navigator.pop(context),
style: ElevatedButton.styleFrom(backgroundColor: Colors.black),
child: const Text('확인', style: TextStyle(color: Colors.white)),
),
],
],
),
),
);
}
}

View File

@ -0,0 +1,67 @@
import 'package:flutter/material.dart';
/// :
/// final bool? result = await showYesNoDialog(
/// context: context,
/// title: '확인',
/// message: '정말 진행하시겠습니까?',
/// );
///
/// if (result == true) {
/// // YES
/// } else {
/// // NO or
/// }
Future<bool?> showYesNoDialog({
required BuildContext context,
required String title,
required String message,
bool yesNo = true,
}) {
return showDialog<bool>(
context: context,
barrierDismissible: false, // dialog
builder: (BuildContext ctx) {
return AlertDialog(
backgroundColor: Colors.white,
title: Center(
child: Text(
title,
style: const TextStyle(
fontSize: 20,
fontWeight: FontWeight.bold,
color: Colors.black,
),
),
),
content: Text(
message,
style: const TextStyle(
fontSize: 16,
color: Colors.black,
),
),
actions: <Widget>[
TextButton(
onPressed: () => Navigator.of(ctx).pop(false), // NO
style: TextButton.styleFrom(
backgroundColor: Colors.black12,
foregroundColor: Colors.black,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
),
child: const Text('아니오'),
),
TextButton(
onPressed: () => Navigator.of(ctx).pop(true), // YES
style: TextButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 8),
),
child: const Text(''),
),
],
);
},
);
}

94
lib/firebase_options.dart Normal file
View File

@ -0,0 +1,94 @@
// File generated by FlutterFire CLI.
// ignore_for_file: type=lint
import 'package:firebase_core/firebase_core.dart' show FirebaseOptions;
import 'package:flutter/foundation.dart'
show defaultTargetPlatform, kIsWeb, TargetPlatform;
/// Default [FirebaseOptions] for use with your Firebase apps.
///
/// Example:
/// ```dart
/// import 'firebase_options.dart';
/// // ...
/// await Firebase.initializeApp(
/// options: DefaultFirebaseOptions.currentPlatform,
/// );
/// ```
class DefaultFirebaseOptions {
static FirebaseOptions get currentPlatform {
if (kIsWeb) {
return web;
}
switch (defaultTargetPlatform) {
case TargetPlatform.android:
return android;
case TargetPlatform.iOS:
return ios;
case TargetPlatform.macOS:
return macos;
case TargetPlatform.windows:
return windows;
case TargetPlatform.linux:
throw UnsupportedError(
'DefaultFirebaseOptions have not been configured for linux - '
'you can reconfigure this by running the FlutterFire CLI again.',
);
default:
throw UnsupportedError(
'DefaultFirebaseOptions are not supported for this platform.',
);
}
}
static const FirebaseOptions web = FirebaseOptions(
apiKey: 'AIzaSyCnZuHtj5oUe_YS9nv3nlQIKWYCCfYFysU',
appId: '1:70449524223:web:e9c27da6646d655f3e4bca',
messagingSenderId: '70449524223',
projectId: 'allscore-344c2',
authDomain: 'allscore-344c2.firebaseapp.com',
databaseURL: 'https://allscore-344c2-default-rtdb.asia-southeast1.firebasedatabase.app',
storageBucket: 'allscore-344c2.firebasestorage.app',
measurementId: 'G-50Q1W265RY',
);
static const FirebaseOptions android = FirebaseOptions(
apiKey: 'AIzaSyAJEItMxO-TemHGlveSKySG-eNaTD9XJI0',
appId: '1:70449524223:android:94ffb9ec98e508313e4bca',
messagingSenderId: '70449524223',
projectId: 'allscore-344c2',
databaseURL: 'https://allscore-344c2-default-rtdb.asia-southeast1.firebasedatabase.app',
storageBucket: 'allscore-344c2.firebasestorage.app',
);
static const FirebaseOptions ios = FirebaseOptions(
apiKey: 'AIzaSyDq2y-BRlthl6BHs4B7FByiUnpyOfPPZQk',
appId: '1:70449524223:ios:98ebdbaa616a807f3e4bca',
messagingSenderId: '70449524223',
projectId: 'allscore-344c2',
databaseURL: 'https://allscore-344c2-default-rtdb.asia-southeast1.firebasedatabase.app',
storageBucket: 'allscore-344c2.firebasestorage.app',
iosBundleId: 'com.example.allscoreApp',
);
static const FirebaseOptions macos = FirebaseOptions(
apiKey: 'AIzaSyDq2y-BRlthl6BHs4B7FByiUnpyOfPPZQk',
appId: '1:70449524223:ios:98ebdbaa616a807f3e4bca',
messagingSenderId: '70449524223',
projectId: 'allscore-344c2',
databaseURL: 'https://allscore-344c2-default-rtdb.asia-southeast1.firebasedatabase.app',
storageBucket: 'allscore-344c2.firebasestorage.app',
iosBundleId: 'com.example.allscoreApp',
);
static const FirebaseOptions windows = FirebaseOptions(
apiKey: 'AIzaSyCnZuHtj5oUe_YS9nv3nlQIKWYCCfYFysU',
appId: '1:70449524223:web:479dd789b837f54c3e4bca',
messagingSenderId: '70449524223',
projectId: 'allscore-344c2',
authDomain: 'allscore-344c2.firebaseapp.com',
databaseURL: 'https://allscore-344c2-default-rtdb.asia-southeast1.firebasedatabase.app',
storageBucket: 'allscore-344c2.firebasestorage.app',
measurementId: 'G-S9J5WDYJZM',
);
}

View File

@ -1,10 +1,24 @@
import 'package:flutter/material.dart';
import 'login_page.dart';
import 'id_finding_page.dart';
import 'pw_finding_page.dart';
import 'signup_page.dart';
import 'package:shared_preferences/shared_preferences.dart';
// Firebase Core
import 'package:firebase_core/firebase_core.dart';
// Firebase Database ()
import 'package:firebase_database/firebase_database.dart';
// firebase_options.dart import
import 'firebase_options.dart';
import 'views/login/login_page.dart';
import 'views/room/main_page.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// Firebase
await Firebase.initializeApp(
options: DefaultFirebaseOptions.currentPlatform, // FirebaseOptions
);
void main() {
runApp(const MyApp());
}
@ -14,12 +28,31 @@ class MyApp extends StatelessWidget {
@override
Widget build(BuildContext context) {
return MaterialApp(
title: 'Flutter Demo',
title: 'ALLSCORE',
theme: ThemeData(
colorScheme: ColorScheme.fromSeed(seedColor: Colors.deepPurple),
useMaterial3: true,
primarySwatch: Colors.blue,
),
home: FutureBuilder<bool>(
future: _checkLoginStatus(),
builder: (context, snapshot) {
if (snapshot.connectionState == ConnectionState.waiting) {
return const Scaffold(
body: Center(child: CircularProgressIndicator()),
);
} else if (snapshot.hasData && snapshot.data == true) {
return const MainPage();
} else {
return const LoginPage();
}
},
),
home: const LoginPage(),
);
}
Future<bool> _checkLoginStatus() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
bool autoLogin = prefs.getBool('auto_login') ?? false;
String authToken = prefs.getString('auth_token') ?? '';
return autoLogin && authToken.isNotEmpty;
}
}

109
lib/plugins/api.dart Normal file
View File

@ -0,0 +1,109 @@
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
import 'package:image_picker/image_picker.dart';
class Api {
static const String baseUrl = 'https://eldsoft.com:8097';
//
static Future<Map<String, dynamic>> serverRequest({
required String uri,
required Map<String, dynamic> body,
}) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? authToken = prefs.getString('auth_token');
final url = '$baseUrl$uri'; // URL
final headers = { //
'Content-Type': 'application/json',
'auth-token': authToken ?? '',
};
final response = await http.post(
Uri.parse(url),
headers: headers,
body: json.encode(body),
);
// res
Map<String, dynamic> res;
if (response.statusCode == 200) {
String responseBody = utf8.decode(response.bodyBytes);
final Map<String, dynamic> jsonResponse = jsonDecode(responseBody);
print('응답: $jsonResponse');
print('응답[result]: ${jsonResponse['result']}');
await prefs.setString('auth_token', jsonResponse['auth']['token']);
res = {
'result': "OK",
'response': jsonResponse,
};
} else {
res = {
'result': "FAIL",
'response': '',
}; //
}
return res; // res
}
static Future<Map<String, dynamic>> uploadProfileImage(XFile image, {Map<String, dynamic>? body}) async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? authToken = prefs.getString('auth_token');
final uri = Uri.parse('https://eldsoft.com:8097/user/update/profile/img');
final headers = { //
'auth-token': authToken ?? '',
};
final request = http.MultipartRequest('POST', uri)
..headers.addAll(headers); //
// MultipartFile로
final file = await http.MultipartFile.fromPath('file', image.path);
request.files.add(file);
// body가 null이
if (body != null) {
request.fields['body'] = json.encode(body); // JSON body
}
Map<String, dynamic> res;
try {
//
final response = await request.send();
if (response.statusCode == 200) {
// UTF-8
final responseData = await response.stream.toBytes();
final responseString = utf8.decode(responseData); // UTF-8
final jsonResponse = json.decode(responseString);
print('응답: $jsonResponse');
print('응답[result]: ${jsonResponse['result']}');
await prefs.setString('auth_token', jsonResponse['auth']['token']);
res = {
'result': "OK",
'response': jsonResponse,
};
} else {
res = {
'result': "FAIL",
'response': '',
}; //
}
} catch (e) {
print('업로드 중 오류 발생: $e');
res = {
'result': "FAIL",
'response': '',
};
}
return res; // Map<String, dynamic>
}
}

11
lib/plugins/utils.dart Normal file
View File

@ -0,0 +1,11 @@
import 'dart:convert';
import 'package:crypto/crypto.dart';
class Utils {
// SHA-256
static String hashPassword(String password) {
final bytes = utf8.encode(password); // UTF-8
final digest = sha256.convert(bytes); // SHA-256
return digest.toString(); //
}
}

View File

@ -7,6 +7,8 @@ import 'package:shared_preferences/shared_preferences.dart';
import 'id_finding_page.dart';
import 'pw_finding_page.dart';
import 'signup_page.dart';
import 'package:google_mobile_ads/google_mobile_ads.dart';
import '../room/main_page.dart';
class LoginPage extends StatefulWidget {
const LoginPage({Key? key}) : super(key: key);
@ -21,11 +23,44 @@ class _LoginPageState extends State<LoginPage> {
bool autoLogin = false;
String loginErrorMessage = '';
BannerAd? _bannerAd;
Widget? _adWidget;
@override
void initState() {
super.initState();
_bannerAd = BannerAd(
adUnitId: "ca-app-pub-3151339278746301~1689299887",
request: const AdRequest(),
size: AdSize.banner,
listener: BannerAdListener(
onAdLoaded: (ad) {
setState(() {
_adWidget = AdWidget(ad: ad as AdWithView);
});
},
onAdFailedToLoad: (ad, error) {
print('Ad failed to load: $error');
ad.dispose();
},
),
)..load();
}
@override
void dispose() {
_bannerAd?.dispose();
super.dispose();
}
Future<void> _login() async {
String id = idController.text;
String password = passwordController.text;
String id = idController.text.trim();
String password = passwordController.text.trim();
// autoLogin
String autoLoginStatus = autoLogin ? 'Y' : 'N';
// PW를 sha256으로
var bytes = utf8.encode(password);
var digest = sha256.convert(bytes);
@ -34,6 +69,7 @@ class _LoginPageState extends State<LoginPage> {
Uri.parse('https://eldsoft.com:8097/user/login'),
headers: {
'Content-Type': 'application/json',
'auth_token': '',
},
body: jsonEncode({
'user_id': id,
@ -41,26 +77,46 @@ class _LoginPageState extends State<LoginPage> {
}),
).timeout(const Duration(seconds: 10));
//
String responseBody = utf8.decode(response.bodyBytes);
if (response.statusCode == 200) {
final Map<String, dynamic> jsonResponse = jsonDecode(responseBody);
print('jsonResponse: $jsonResponse');
if (jsonResponse['result'] == 'OK') {
print('로그인 성공');
//
final authData = jsonResponse['auth'] ?? {};
final token = authData['token'] ?? '';
final userSeq = authData['user_seq'] ?? 0; //
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setString('auth_token', jsonResponse['auth']['token']);
// autoLogin
await prefs.setString('auth_token', token);
await prefs.setBool('auto_login', autoLogin);
// (New) user_seq
await prefs.setInt('my_user_seq', userSeq);
//
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const MainPage()),
);
} else if (jsonResponse['response_info']['msg_title'] == '로그인 실패') {
//
setState(() {
loginErrorMessage = '회원정보를 다시 확인해주세요.';
});
} else {
// result != OK ,
_showDialog('로그인 실패', '서버에서 로그인에 실패했습니다.\n관리자에게 문의해주세요.');
}
} else {
_showDialog('오류', '로그인에 실패했습니다. 관리자에게 문의해주세요.');
}
} catch (e) {
_showDialog('오류', '요청이 실패했습니다. 관리자에게 문의해주세요.');
print('로그인 요청 중 오류: $e');
_showDialog('오류', '로그인 요청이 실패했습니다. 관리자에게 문의해주세요.\n$e');
}
}
@ -118,9 +174,9 @@ class _LoginPageState extends State<LoginPage> {
decoration: InputDecoration(
labelText: 'ID',
labelStyle: const TextStyle(color: Colors.black),
border: OutlineInputBorder(),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.black, width: 2.0),
border: const OutlineInputBorder(),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2.0),
),
),
),
@ -131,9 +187,9 @@ class _LoginPageState extends State<LoginPage> {
decoration: InputDecoration(
labelText: 'PW',
labelStyle: const TextStyle(color: Colors.black),
border: OutlineInputBorder(),
focusedBorder: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.black, width: 2.0),
border: const OutlineInputBorder(),
focusedBorder: const OutlineInputBorder(
borderSide: BorderSide(color: Colors.black, width: 2.0),
),
),
),
@ -196,9 +252,19 @@ class _LoginPageState extends State<LoginPage> {
},
child: const Text('회원가입', style: TextStyle(color: Colors.black)),
),
const SizedBox(height: 16),
//
Container(
height: 50,
color: Colors.grey[300],
child: const Center(child: Text('광고 영역', style: TextStyle(color: Colors.black))),
),
],
),
),
);
}
}
}

View File

@ -0,0 +1,489 @@
import 'package:flutter/material.dart';
import 'package:flutter/services.dart'; //
import '../../plugins/api.dart';
import '../../dialogs/response_dialog.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'waiting_room_team_page.dart';
import 'waiting_room_private_page.dart';
class CreateRoomPage extends StatefulWidget {
const CreateRoomPage({Key? key}) : super(key: key);
@override
_CreateRoomPageState createState() => _CreateRoomPageState();
}
class _CreateRoomPageState extends State<CreateRoomPage> {
final TextEditingController _roomNameController = TextEditingController();
final TextEditingController _roomDescriptionController = TextEditingController();
/// (open_yn: 'Y'/'N')
bool _isPrivate = false;
final TextEditingController _passwordController = TextEditingController();
/// (1~6)
int _selectedHour = 1;
/// : /
bool _isTeamGame = false;
/// ( 2)
int _selectedTeamCount = 2;
///
final TextEditingController _maxParticipantsController = TextEditingController(text: '1');
///
/// - : 'ALL' / 'PRIVATE'
/// - : 'ALL' / 'TEAM' / 'PRIVATE'
String _selectedScoreOpenRange = 'ALL';
@override
void dispose() {
_roomNameController.dispose();
_roomDescriptionController.dispose();
_passwordController.dispose();
_maxParticipantsController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return Scaffold(
//
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text(
'방 만들기',
style: TextStyle(color: Colors.white),
),
backgroundColor: Colors.black,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
),
body: SingleChildScrollView(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
/// (A)
const Text('방 제목',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 6),
_buildWhiteBorderTextField(
controller: _roomNameController,
hintText: '방 제목을 입력하세요',
),
const SizedBox(height: 16),
/// (B)
const Text('방 소개',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
const SizedBox(height: 6),
_buildMultilineBox(
controller: _roomDescriptionController,
hintText: '방 소개를 입력하세요',
),
const SizedBox(height: 16),
/// (C) ( / )
const Text('비밀번호 설정',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
Row(
children: [
Checkbox(
value: !_isPrivate,
activeColor: Colors.black,
checkColor: Colors.white,
onChanged: (value) {
setState(() {
// == !
_isPrivate = !value!;
});
},
),
const Text('공개'),
const SizedBox(width: 10),
Checkbox(
value: _isPrivate,
activeColor: Colors.black,
checkColor: Colors.white,
onChanged: (value) {
setState(() {
_isPrivate = value!;
});
},
),
const Text('비공개'),
],
),
if (_isPrivate)
_buildWhiteBorderTextField(
controller: _passwordController,
hintText: '비밀번호를 입력하세요',
obscureText: true,
),
const SizedBox(height: 16),
/// (D) (1~6)
const Text('운영시간 설정',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
Row(
children: [
DropdownButton<int>(
value: _selectedHour,
dropdownColor: Colors.white,
style: const TextStyle(color: Colors.black),
items: List.generate(6, (index) => index + 1)
.map((value) => DropdownMenuItem<int>(
value: value,
child: Text(value.toString()),
))
.toList(),
onChanged: (value) {
setState(() {
_selectedHour = value!;
});
},
),
const SizedBox(width: 8),
const Text('시간'),
],
),
const SizedBox(height: 16),
/// (E) (/)
const Text('게임 유형',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
Row(
children: [
Checkbox(
value: !_isTeamGame,
activeColor: Colors.black,
checkColor: Colors.white,
onChanged: (value) {
setState(() {
_isTeamGame = !value!;
//
_selectedScoreOpenRange = 'ALL';
});
},
),
const Text('개인전'),
const SizedBox(width: 10),
Checkbox(
value: _isTeamGame,
activeColor: Colors.black,
checkColor: Colors.white,
onChanged: (value) {
setState(() {
_isTeamGame = value!;
//
_selectedScoreOpenRange = 'ALL';
});
},
),
const Text('팀전'),
const SizedBox(width: 16),
if (_isTeamGame) ...[
const Text('팀수: '),
const SizedBox(width: 8),
// 2
DropdownButton<int>(
value: _selectedTeamCount,
dropdownColor: Colors.white,
style: const TextStyle(color: Colors.black),
items: List.generate(9, (index) => index + 2) // 2 ~ 10
.map((value) => DropdownMenuItem<int>(
value: value,
child: Text(value.toString()),
))
.toList(),
onChanged: (value) {
setState(() {
_selectedTeamCount = value!;
});
},
),
],
],
),
const SizedBox(height: 16),
/// (F)
const Text('최대 인원수',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold)),
Row(
children: [
SizedBox(
width: 80,
child: TextField(
controller: _maxParticipantsController,
keyboardType: TextInputType.number,
textAlign: TextAlign.center,
style: const TextStyle(fontSize: 16),
decoration: const InputDecoration(
border: OutlineInputBorder(),
contentPadding: EdgeInsets.symmetric(vertical: 8),
),
inputFormatters: [
FilteringTextInputFormatter.digitsOnly,
LengthLimitingTextInputFormatter(3),
],
onChanged: (value) {
if (value.isNotEmpty) {
final number = int.tryParse(value);
if (number == null || number < 1 || number > 100) {
_maxParticipantsController.text = '1';
_maxParticipantsController.selection =
TextSelection.fromPosition(
TextPosition(
offset: _maxParticipantsController.text.length,
),
);
}
}
},
),
),
const SizedBox(width: 8),
const Text('', style: TextStyle(fontSize: 16)),
],
),
const SizedBox(height: 16),
/// (G)
/// : 'ALL', 'PRIVATE'
/// : 'ALL', 'TEAM', 'PRIVATE'
const Text(
'점수 공개 범위 설정',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
const SizedBox(height: 8),
if (!_isTeamGame)
// ALL, PRIVATE
Column(
children: [
RadioListTile<String>(
title: const Text('전체'),
value: 'ALL',
groupValue: _selectedScoreOpenRange,
activeColor: Colors.black,
onChanged: (value) {
setState(() {
_selectedScoreOpenRange = value!;
});
},
),
RadioListTile<String>(
title: const Text('개인'),
value: 'PRIVATE',
groupValue: _selectedScoreOpenRange,
activeColor: Colors.black,
onChanged: (value) {
setState(() {
_selectedScoreOpenRange = value!;
});
},
),
],
)
else
// ALL, TEAM, PRIVATE
Column(
children: [
RadioListTile<String>(
title: const Text('전체'),
value: 'ALL',
groupValue: _selectedScoreOpenRange,
activeColor: Colors.black,
onChanged: (value) {
setState(() {
_selectedScoreOpenRange = value!;
});
},
),
RadioListTile<String>(
title: const Text(''),
value: 'TEAM',
groupValue: _selectedScoreOpenRange,
activeColor: Colors.black,
onChanged: (value) {
setState(() {
_selectedScoreOpenRange = value!;
});
},
),
RadioListTile<String>(
title: const Text('개인'),
value: 'PRIVATE',
groupValue: _selectedScoreOpenRange,
activeColor: Colors.black,
onChanged: (value) {
setState(() {
_selectedScoreOpenRange = value!;
});
},
),
],
),
const SizedBox(height: 24),
/// (H) "방 생성하기"
Center(
child: ElevatedButton(
onPressed: () async {
try {
final serverResponse = await createRoom();
if (serverResponse['result'] == 'OK') {
final serverResponse1 = serverResponse['response'];
if (serverResponse1['result'] == 'OK') {
// roomSeq
final roomSeq = serverResponse1['data']['room_seq'];
//
if (_isTeamGame) {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => WaitingRoomTeamPage(
roomSeq: roomSeq,
roomType: 'team',
),
),
);
} else {
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => WaitingRoomPrivatePage(
roomSeq: roomSeq,
roomType: 'private',
),
),
);
}
} else {
showResponseDialog(
context,
'${serverResponse1['response_info']['msg_title']}',
'${serverResponse1['response_info']['msg_content']}',
);
}
} else {
showResponseDialog(
context,
'방 생성 실패',
'서버에 문제가 있습니다. 관리자에게 문의해주세요.',
);
}
} catch (e) {
showResponseDialog(context, '방 생성 실패', e.toString());
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 40),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40),
),
),
child: const Text('방 생성하기', style: TextStyle(fontSize: 16)),
),
),
const SizedBox(height: 16),
],
),
),
);
}
///
Future<Map<String, dynamic>> createRoom() async {
// requestBody
final requestBody = {
'room_title': _roomNameController.text,
'room_intro': _roomDescriptionController.text,
'open_yn': _isPrivate ? 'N' : 'Y',
'room_pw': _isPrivate ? _passwordController.text : '',
'running_time': _selectedHour.toString(),
'room_type': _isTeamGame ? 'team' : 'private',
'number_of_teams': _selectedTeamCount.toString(),
'number_of_people': _maxParticipantsController.text,
// :
// : 'ALL', 'PRIVATE'
// : 'ALL', 'TEAM', 'PRIVATE'
'score_open_range': _selectedScoreOpenRange,
'room_status': 'WAIT',
};
try {
final serverResponse =
await Api.serverRequest(uri: '/room/score/create/room', body: requestBody);
if (serverResponse == null) {
throw Exception('서버 응답이 null입니다.');
}
if (serverResponse['result'] == 'OK') {
return serverResponse;
} else {
return {'result': 'FAIL'};
}
} catch (e) {
print('serverResponse 오류: $e');
return {'result': 'FAIL'};
}
}
/// + TextField
Widget _buildWhiteBorderTextField({
required TextEditingController controller,
String hintText = '',
bool obscureText = false,
}) {
return TextField(
controller: controller,
obscureText: obscureText,
style: const TextStyle(color: Colors.black),
decoration: InputDecoration(
hintText: hintText,
hintStyle: TextStyle(color: Colors.grey.shade400),
border: const OutlineInputBorder(),
filled: true,
fillColor: Colors.white,
),
);
}
/// ( )
Widget _buildMultilineBox({
required TextEditingController controller,
String hintText = '',
}) {
return Container(
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.black),
borderRadius: BorderRadius.circular(8),
),
child: TextField(
controller: controller,
maxLines: 4,
style: const TextStyle(color: Colors.black),
decoration: InputDecoration(
border: InputBorder.none,
hintText: hintText,
hintStyle: TextStyle(color: Colors.grey.shade400),
),
),
);
}
}

View File

@ -0,0 +1,48 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'main_page.dart';
class FinishPrivatePage extends StatelessWidget {
final int roomSeq;
const FinishPrivatePage({
Key? key,
required this.roomSeq,
}) : super(key: key);
@override
Widget build(BuildContext context) {
//
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text('게임 종료 (개인전)', style: TextStyle(color: Colors.white)),
backgroundColor: Colors.black,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
onPressed: () {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
},
),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('게임이 종료되었습니다.', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
//
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
},
style: ElevatedButton.styleFrom(backgroundColor: Colors.black),
child: const Text('메인으로', style: TextStyle(color: Colors.white)),
),
],
),
),
);
}
}

View File

@ -0,0 +1,47 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'main_page.dart';
class FinishTeamPage extends StatelessWidget {
final int roomSeq;
const FinishTeamPage({
Key? key,
required this.roomSeq,
}) : super(key: key);
@override
Widget build(BuildContext context) {
//
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text('게임 종료 (팀전)', style: TextStyle(color: Colors.white)),
backgroundColor: Colors.black,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
onPressed: () {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
},
),
),
body: Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text('게임이 종료되었습니다.', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold)),
const SizedBox(height: 16),
ElevatedButton(
onPressed: () {
//
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
},
style: ElevatedButton.styleFrom(backgroundColor: Colors.black),
child: const Text('메인으로', style: TextStyle(color: Colors.white)),
),
],
),
),
);
}
}

View File

@ -0,0 +1,159 @@
import 'package:flutter/material.dart';
import 'package:shared_preferences/shared_preferences.dart';
import '../../dialogs/settings_dialog.dart';
import 'create_room_page.dart';
//
import 'room_search_home_page.dart';
class MainPage extends StatefulWidget {
const MainPage({Key? key}) : super(key: key);
@override
_MainPageState createState() => _MainPageState();
}
class _MainPageState extends State<MainPage> {
bool _isBackButtonVisible = false; //
@override
void initState() {
super.initState();
_isBackButtonVisible = false;
}
@override
Widget build(BuildContext context) {
return Scaffold(
// (A) /
backgroundColor: Colors.white,
// (B) AppBar: ,
appBar: AppBar(
backgroundColor: Colors.black,
elevation: 0,
automaticallyImplyLeading: false, //
title: const Text(
'ALLSCORE',
style: TextStyle(color: Colors.white),
),
actions: [
IconButton(
icon: const Icon(Icons.settings, color: Colors.white),
onPressed: () {
showSettingsDialog(context); //
},
),
],
),
// (C) :
body: Column(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
// ( / )
Expanded(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
// (C1)
_buildBlackWhiteButton(
label: '방만들기',
onTap: () {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const CreateRoomPage()),
);
},
),
const SizedBox(width: 16),
// (C2) => RoomSearchHomePage로
_buildBlackWhiteButton(
label: '참여하기',
onTap: () async {
Navigator.push(
context,
MaterialPageRoute(builder: (_) => const RoomSearchHomePage()),
);
},
),
],
),
),
),
),
// (D)
Container(
color: Colors.white,
padding: const EdgeInsets.only(bottom: 24),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
height: 50,
width: 300,
color: Colors.grey.shade400,
child: const Center(
child: Text(
'구글 광고',
style: TextStyle(color: Colors.black),
),
),
),
],
),
),
// (E) :
Center(
child: OutlinedButton(
onPressed: () {
// (15 )
// debugging용
// ( )
},
style: OutlinedButton.styleFrom(
backgroundColor: Colors.white,
side: const BorderSide(color: Colors.black54, width: 1),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40),
),
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 60),
),
child: const Text(
'방 생성 완료 이동(임시)',
style: TextStyle(color: Colors.black, fontSize: 16),
),
),
),
const SizedBox(height: 16),
],
),
);
}
/// +
Widget _buildBlackWhiteButton({
required String label,
required VoidCallback onTap,
}) {
return ElevatedButton(
onPressed: onTap,
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
side: const BorderSide(color: Colors.black, width: 1),
padding: const EdgeInsets.symmetric(vertical: 36, horizontal: 32),
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(20),
),
),
child: Text(label, style: const TextStyle(color: Colors.black)),
);
}
}

View File

@ -0,0 +1,362 @@
import 'package:flutter/material.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'main_page.dart';
import 'finish_private_page.dart'; // ()
import '../../plugins/api.dart';
import '../../dialogs/response_dialog.dart';
//
import '../../dialogs/score_edit_dialog.dart';
// (/X)
import '../../dialogs/user_info_basic_dialog.dart';
class PlayingPrivatePage extends StatefulWidget {
final int roomSeq;
final String roomTitle;
const PlayingPrivatePage({
Key? key,
required this.roomSeq,
required this.roomTitle,
}) : super(key: key);
@override
State<PlayingPrivatePage> createState() => _PlayingPrivatePageState();
}
class _PlayingPrivatePageState extends State<PlayingPrivatePage> {
// FRD
late DatabaseReference _roomRef;
Stream<DatabaseEvent>? _roomStream;
String roomMasterYn = 'N';
String roomTitle = '';
int myScore = 0;
// (ADMIN )
List<Map<String, dynamic>> _scoreList = [];
bool _isLoading = true;
// user_seq
String mySeq = '0';
// userListMap
Map<String, bool> _userListMap = {};
@override
void initState() {
super.initState();
roomTitle = widget.roomTitle;
_initFirebase();
}
Future<void> _initFirebase() async {
final prefs = await SharedPreferences.getInstance();
mySeq = prefs.getInt('my_user_seq')?.toString() ?? '0';
final roomKey = 'korea-${widget.roomSeq}';
_roomRef = FirebaseDatabase.instance.ref('rooms/$roomKey');
_listenRoomData();
}
void _listenRoomData() {
_roomStream = _roomRef.onValue;
_roomStream?.listen((event) {
final snapshot = event.snapshot;
if (!snapshot.exists) {
setState(() {
_isLoading = false;
roomTitle = '방 정보 없음';
_scoreList = [];
myScore = 0;
});
return;
}
final data = snapshot.value as Map<dynamic, dynamic>? ?? {};
final roomInfoData = data['roomInfo'] as Map<dynamic, dynamic>? ?? {};
final userInfoData = data['userInfo'] as Map<dynamic, dynamic>? ?? {};
final userListData = data['userList'] as Map<dynamic, dynamic>?;
//
final roomStatus = (roomInfoData['room_status'] ?? 'WAIT').toString().toUpperCase();
// FINISH라면 =>
if (roomStatus == 'FINISH') {
// ->
// ( )
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => FinishPrivatePage(roomSeq: widget.roomSeq)),
);
}
return;
}
setState(() {
//
final masterSeq = roomInfoData['master_user_seq'];
roomMasterYn = (masterSeq != null && masterSeq.toString() == mySeq) ? 'Y' : 'N';
//
final newTitle = (roomInfoData['room_title'] ?? '') as String;
if (newTitle.isNotEmpty) roomTitle = newTitle;
// userListMap
_userListMap.clear();
if (userListData != null) {
userListData.forEach((k, v) {
_userListMap[k.toString()] = (v == true);
});
}
//
final List<Map<String, dynamic>> rawList = [];
userInfoData.forEach((uSeq, uData) {
rawList.add({
'user_seq': uSeq,
'participant_type': (uData['participant_type'] ?? '').toString().toUpperCase(),
'nickname': uData['nickname'] ?? '유저',
'score': uData['score'] ?? 0,
'profile_img': uData['profile_img'] ?? '',
'department': uData['department'] ?? '',
'introduce_myself': uData['introduce_myself'] ?? '',
'is_my_score': (uSeq.toString() == mySeq) ? 'Y' : 'N',
});
});
//
int tempMyScore = 0;
for (var u in rawList) {
if ((u['is_my_score'] ?? 'N') == 'Y') {
tempMyScore = u['score'] ?? 0;
}
}
// ADMIN
final playerList = rawList.where((u) => u['participant_type'] != 'ADMIN').toList();
//
playerList.sort((a, b) {
final scoreA = a['score'] ?? 0;
final scoreB = b['score'] ?? 0;
return scoreB.compareTo(scoreA);
});
myScore = tempMyScore;
_scoreList = playerList;
_isLoading = false;
});
}, onError: (err) {
setState(() {
_isLoading = false;
roomTitle = '오류 발생';
});
});
}
/// (A) WillPopScope + AppBar leading
Future<bool> _onBackPressed() async {
// ? => API
if (roomMasterYn == 'Y') {
await _requestFinish();
}
// userList => false
final userRef = _roomRef.child('userList').child(mySeq);
await userRef.set(false);
if (!mounted) return false;
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
return false;
}
/// (B) "게임 종료"
Future<void> _requestFinish() async {
final reqBody = {
"room_seq": "${widget.roomSeq}",
"room_type": "PRIVATE",
};
try {
final resp = await Api.serverRequest(
uri: '/room/score/game/finish',
body: reqBody,
);
// OK / FAIL
// room_status = FINISH => FRD에서 ->
} catch (e) {
//
print('게임 종료 API 에러: $e');
}
}
/// (C)
Widget _buildScoreItem(Map<String, dynamic> user) {
final userSeq = user['user_seq'].toString();
final score = user['score'] ?? 0;
final nickname = user['nickname'] ?? '유저';
final bool isActive = _userListMap[userSeq] ?? true;
final hasExited = !isActive;
return GestureDetector(
onTap: () => _onUserTapped(user),
child: Container(
width: 60,
margin: const EdgeInsets.all(4),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
hasExited
? Text('X', style: TextStyle(fontSize: 20, color: Colors.redAccent, fontWeight: FontWeight.bold))
: Text('$score', style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold, color: Colors.black)),
const SizedBox(height: 2),
Container(
width: 30,
height: 30,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: hasExited ? Colors.redAccent : Colors.black),
),
child: hasExited
? Center(
child: Text('X', style: TextStyle(fontSize: 14, color: Colors.redAccent, fontWeight: FontWeight.bold)),
)
: ClipOval(
child: Image.network(
'https://eldsoft.com:8097/images${user['profile_img']}',
fit: BoxFit.cover,
errorBuilder: (ctx, err, st) => const Center(
child: Text('ERR', style: TextStyle(fontSize: 8, color: Colors.black)),
),
),
),
),
const SizedBox(height: 2),
Text(
nickname,
style: TextStyle(fontSize: 11, color: hasExited ? Colors.redAccent : Colors.black),
overflow: TextOverflow.ellipsis,
),
],
),
),
);
}
Future<void> _onUserTapped(Map<String, dynamic> userData) async {
final pType = (userData['participant_type'] ?? '').toString().toUpperCase();
if (pType == 'ADMIN') {
//
await showDialog(
context: context,
barrierDismissible: false,
builder: (_) => ScoreEditDialog(
roomSeq: widget.roomSeq,
roomType: 'PRIVATE',
userData: userData,
),
);
} else if (roomMasterYn == 'Y') {
// (PLAYER)
await showDialog(
context: context,
barrierDismissible: false,
builder: (_) => ScoreEditDialog(
roomSeq: widget.roomSeq,
roomType: 'PRIVATE',
userData: userData,
),
);
} else {
//
await showDialog(
context: context,
barrierDismissible: false,
builder: (_) => UserInfoBasicDialog(userData: userData),
);
}
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: _onBackPressed,
child: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.black,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
onPressed: _onBackPressed,
),
title: Text(
roomTitle.isNotEmpty ? roomTitle : '진행중 (개인전)',
style: const TextStyle(color: Colors.white),
),
actions: [
if (roomMasterYn == 'Y')
TextButton(
onPressed: () async {
//
await _requestFinish();
},
child: const Text('게임종료', style: TextStyle(color: Colors.white)),
),
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: Column(
children: [
//
Container(
width: double.infinity,
color: Colors.white,
padding: const EdgeInsets.symmetric(vertical: 16),
child: Column(
children: [
const Text('내 점수', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black)),
const SizedBox(height: 4),
Text('$myScore', style: const TextStyle(fontSize: 36, fontWeight: FontWeight.bold, color: Colors.black)),
],
),
),
const Divider(height: 1, color: Colors.black),
Expanded(
child: Container(
padding: const EdgeInsets.all(8),
child: SingleChildScrollView(
child: Wrap(
spacing: 8,
runSpacing: 8,
children: _scoreList.map(_buildScoreItem).toList(),
),
),
),
),
Container(
height: 50,
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.black, width: 1),
),
child: const Center(
child: Text('구글 광고', style: TextStyle(color: Colors.black)),
),
),
],
),
),
);
}
}

View File

@ -0,0 +1,414 @@
import 'package:flutter/material.dart';
import 'package:firebase_database/firebase_database.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'main_page.dart';
import 'finish_team_page.dart'; // ()
import '../../plugins/api.dart';
import '../../dialogs/response_dialog.dart';
import '../../dialogs/score_edit_dialog.dart';
import '../../dialogs/user_info_basic_dialog.dart';
class PlayingTeamPage extends StatefulWidget {
final int roomSeq;
final String roomTitle;
const PlayingTeamPage({
Key? key,
required this.roomSeq,
required this.roomTitle,
}) : super(key: key);
@override
State<PlayingTeamPage> createState() => _PlayingTeamPageState();
}
class _PlayingTeamPageState extends State<PlayingTeamPage> {
late DatabaseReference _roomRef;
Stream<DatabaseEvent>? _roomStream;
String roomMasterYn = 'N';
String roomTitle = '';
int myScore = 0;
int myTeamScore = 0;
Map<String, int> _teamScoreMap = {};
Map<String, List<Map<String, dynamic>>> _teamMap = {};
bool _isLoading = true;
String mySeq = '0';
// userListMap
Map<String, bool> _userListMap = {};
@override
void initState() {
super.initState();
roomTitle = widget.roomTitle;
_initFirebase();
}
Future<void> _initFirebase() async {
final prefs = await SharedPreferences.getInstance();
mySeq = prefs.getInt('my_user_seq')?.toString() ?? '0';
final roomKey = 'korea-${widget.roomSeq}';
_roomRef = FirebaseDatabase.instance.ref('rooms/$roomKey');
_listenRoomData();
}
void _listenRoomData() {
_roomStream = _roomRef.onValue;
_roomStream?.listen((event) {
final snapshot = event.snapshot;
if (!snapshot.exists) {
setState(() {
_isLoading = false;
roomTitle = '방 정보 없음';
myScore = 0;
myTeamScore = 0;
_teamScoreMap = {};
_teamMap = {};
});
return;
}
final data = snapshot.value as Map<dynamic, dynamic>? ?? {};
final roomInfoData = data['roomInfo'] as Map<dynamic, dynamic>? ?? {};
final userInfoData = data['userInfo'] as Map<dynamic, dynamic>? ?? {};
final userListData = data['userList'] as Map<dynamic, dynamic>?;
// room_status
final roomStatus = (roomInfoData['room_status'] ?? 'WAIT').toString().toUpperCase();
// FINISH ->
if (roomStatus == 'FINISH') {
if (mounted) {
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => FinishTeamPage(roomSeq: widget.roomSeq)),
);
}
return;
}
setState(() {
//
final masterSeq = roomInfoData['master_user_seq'];
roomMasterYn = (masterSeq != null && masterSeq.toString() == mySeq) ? 'Y' : 'N';
final newTitle = (roomInfoData['room_title'] ?? '') as String;
if (newTitle.isNotEmpty) roomTitle = newTitle;
// userListMap
_userListMap.clear();
if (userListData != null) {
userListData.forEach((k, v) {
_userListMap[k.toString()] = (v == true);
});
}
//
final List<Map<String, dynamic>> rawList = [];
userInfoData.forEach((uSeq, uData) {
rawList.add({
'user_seq': uSeq,
'participant_type': (uData['participant_type'] ?? '').toString().toUpperCase(),
'nickname': uData['nickname'] ?? '유저',
'team_name': (uData['team_name'] ?? '').toString().toUpperCase(),
'score': uData['score'] ?? 0,
});
});
// /
int tmpMyScore = 0;
int tmpMyTeamScore = 0;
String myTeam = 'WAIT';
for (var user in rawList) {
final uSeq = user['user_seq'].toString();
final sc = (user['score'] ?? 0) as int;
final tName = user['team_name'] ?? 'WAIT';
if (uSeq == mySeq) {
tmpMyScore = sc;
myTeam = tName;
}
}
//
for (var user in rawList) {
final tName = user['team_name'] ?? 'WAIT';
final sc = (user['score'] ?? 0) as int;
if (tName == myTeam && tName != 'WAIT') {
tmpMyTeamScore += sc;
}
}
// (ADMIN/WAIT )
final Map<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);
}
//
tMap.forEach((k, members) {
int sumScore = 0;
for (var m in members) {
sumScore += (m['score'] ?? 0) as int;
}
tScoreMap[k] = sumScore;
});
myScore = tmpMyScore;
myTeamScore = tmpMyTeamScore;
_teamMap = tMap;
_teamScoreMap = tScoreMap;
_isLoading = false;
});
}, onError: (err) {
setState(() {
_isLoading = false;
roomTitle = '오류 발생';
});
});
}
/// (A) -> ? => Finish API
Future<bool> _onBackPressed() async {
if (roomMasterYn == 'Y') {
await _requestFinish();
}
// userList => false
final userRef = _roomRef.child('userList').child(mySeq);
await userRef.set(false);
if (!mounted) return false;
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
return false;
}
Future<void> _requestFinish() async {
final body = {
"room_seq": "${widget.roomSeq}",
"room_type": "TEAM",
};
try {
final resp = await Api.serverRequest(
uri: '/room/score/game/finish',
body: body,
);
// result ...
} catch (e) {
print('finish API error: $e');
}
}
@override
Widget build(BuildContext context) {
return WillPopScope(
onWillPop: _onBackPressed,
child: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: Text(
roomTitle.isNotEmpty ? roomTitle : '진행중 (팀전)',
style: const TextStyle(color: Colors.white),
),
backgroundColor: Colors.black,
elevation: 0,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
onPressed: _onBackPressed,
),
actions: [
if (roomMasterYn == 'Y')
TextButton(
onPressed: _requestFinish,
child: const Text('게임종료', style: TextStyle(color: Colors.white)),
),
],
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: Column(
children: [
// /
Container(
color: Colors.white,
padding: const EdgeInsets.only(top: 16, bottom: 16),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Column(
children: [
const Text('내 점수', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black)),
const SizedBox(height: 4),
Text('$myScore', style: const TextStyle(fontSize: 36, fontWeight: FontWeight.bold, color: Colors.black)),
],
),
Container(width: 1, height: 60, color: Colors.black),
Column(
children: [
const Text('우리 팀 점수', style: TextStyle(fontSize: 14, fontWeight: FontWeight.bold, color: Colors.black)),
const SizedBox(height: 4),
Text('$myTeamScore', style: const TextStyle(fontSize: 36, fontWeight: FontWeight.bold, color: Colors.black)),
],
),
],
),
),
const Divider(height: 1, color: Colors.black),
Expanded(
child: SingleChildScrollView(
padding: const EdgeInsets.all(8),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _teamMap.keys.map(_buildTeamSection).toList(),
),
),
),
Container(
height: 50,
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.black, width: 1),
),
child: const Center(
child: Text('구글 광고', style: TextStyle(color: Colors.black)),
),
),
],
),
),
);
}
Widget _buildTeamSection(String teamName) {
final upperName = teamName.toUpperCase();
final members = _teamMap[upperName] ?? [];
final teamScore = _teamScoreMap[upperName] ?? 0;
return Container(
width: double.infinity,
margin: const EdgeInsets.only(bottom: 8),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.black),
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
Container(
color: Colors.black,
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 8),
child: Center(
child: Text('$teamName (팀점수 $teamScore)', style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
),
),
Container(
padding: const EdgeInsets.all(8),
child: SingleChildScrollView(
scrollDirection: Axis.horizontal,
child: Row(children: members.map(_buildTeamMemberItem).toList()),
),
),
],
),
);
}
Widget _buildTeamMemberItem(Map<String, dynamic> userData) {
final userSeq = userData['user_seq'].toString();
final score = userData['score'] ?? 0;
final nickname = userData['nickname'] ?? '유저';
final bool isActive = _userListMap[userSeq] ?? true;
final hasExited = !isActive;
return GestureDetector(
onTap: () => _onUserTapped(userData),
child: Container(
width: 60,
margin: const EdgeInsets.symmetric(horizontal: 6, vertical: 4),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
hasExited
? Text('X', style: TextStyle(fontSize: 18, color: Colors.redAccent, fontWeight: FontWeight.bold))
: Text('$score', style: const TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black)),
const SizedBox(height: 2),
Container(
width: 30,
height: 30,
decoration: BoxDecoration(
shape: BoxShape.circle,
border: Border.all(color: hasExited ? Colors.redAccent : Colors.black),
),
child: hasExited
? Center(child: Text('X', style: TextStyle(fontSize: 14, color: Colors.redAccent, fontWeight: FontWeight.bold)))
: ClipOval(
child: Image.network(
'https://eldsoft.com:8097/images${userData['profile_img']}',
fit: BoxFit.cover,
errorBuilder: (ctx, err, st) => const Center(child: Text('ERR', style: TextStyle(fontSize: 8, color: Colors.black))),
),
),
),
const SizedBox(height: 2),
Text(
nickname,
style: TextStyle(fontSize: 11, color: hasExited ? Colors.redAccent : Colors.black),
overflow: TextOverflow.ellipsis,
),
],
),
),
);
}
Future<void> _onUserTapped(Map<String, dynamic> userData) async {
final pType = (userData['participant_type'] ?? '').toString().toUpperCase();
if (pType == 'ADMIN') {
//
await showDialog(
context: context,
barrierDismissible: false,
builder: (_) => ScoreEditDialog(
roomSeq: widget.roomSeq,
roomType: 'TEAM',
userData: userData,
),
);
} else if (roomMasterYn == 'Y') {
await showDialog(
context: context,
barrierDismissible: false,
builder: (_) => ScoreEditDialog(
roomSeq: widget.roomSeq,
roomType: 'TEAM',
userData: userData,
),
);
} else {
await showDialog(
context: context,
barrierDismissible: false,
builder: (_) => UserInfoBasicDialog(userData: userData),
);
}
}
}

View File

@ -0,0 +1,80 @@
import 'package:flutter/material.dart';
import 'room_search_list_page.dart';
///
/// - "대기중/진행중/종료" 3
/// - RoomSearchListPage로 +
class RoomSearchHomePage extends StatelessWidget {
const RoomSearchHomePage({Key? key}) : super(key: key);
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.black,
elevation: 0,
title: const Text('방 검색', style: TextStyle(color: Colors.white)),
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
),
//
bottomNavigationBar: Container(
height: 50,
color: Colors.grey.shade400,
child: const Center(
child: Text('구글 광고', style: TextStyle(color: Colors.black)),
),
),
// : 3 ( / / )
body: Center(
child: Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
_buildSearchStatusButton(context, label: '대기중', status: 'WAIT'),
const SizedBox(width: 16),
_buildSearchStatusButton(context, label: '진행중', status: 'RUNNING'),
const SizedBox(width: 16),
_buildSearchStatusButton(context, label: '종료', status: 'FINISH'),
],
),
),
);
}
///
Widget _buildSearchStatusButton(
BuildContext context, {
required String label,
required String status,
}) {
return SizedBox(
width: 100,
height: 100,
child: ElevatedButton(
onPressed: () {
// RoomSearchListPage로 , roomStatus
Navigator.push(
context,
MaterialPageRoute(
builder: (_) => RoomSearchListPage(roomStatus: status),
),
);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
side: const BorderSide(color: Colors.black, width: 1),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(12)),
padding: EdgeInsets.zero,
),
child: Text(label, style: const TextStyle(color: Colors.black)),
),
);
}
}

View File

@ -0,0 +1,289 @@
import 'package:flutter/material.dart';
import 'dart:async';
import '../../plugins/api.dart'; // (: Api.serverRequest)
import '../../dialogs/response_dialog.dart'; //
import '../../dialogs/room_detail_dialog.dart'; // import
/// /
/// - roomStatus: "WAIT"/"RUNNING"/"FINISH"
/// - 1 10 ,
/// - room_title
class RoomSearchListPage extends StatefulWidget {
final String roomStatus; // WAIT / RUNNING / FINISH
const RoomSearchListPage({Key? key, required this.roomStatus}) : super(key: key);
@override
State<RoomSearchListPage> createState() => _RoomSearchListPageState();
}
class _RoomSearchListPageState extends State<RoomSearchListPage> {
final TextEditingController _searchController = TextEditingController();
//
List<Map<String, dynamic>> _roomList = [];
bool _isLoading = false;
bool _hasMore = true;
int _currentPage = 1;
final int _pageSize = 10;
late ScrollController _scrollController;
@override
void initState() {
super.initState();
_scrollController = ScrollController()..addListener(_onScroll);
_fetchRoomList(isRefresh: true);
}
@override
void dispose() {
_scrollController.removeListener(_onScroll);
_scrollController.dispose();
_searchController.dispose();
super.dispose();
}
///
void _onScroll() {
if (!_scrollController.hasClients) return;
final thresholdPixels = 200;
final maxScroll = _scrollController.position.maxScrollExtent;
final currentScroll = _scrollController.position.pixels;
if (maxScroll - currentScroll <= thresholdPixels) {
_fetchRoomList(isRefresh: false);
}
}
/// (1)
Future<void> _fetchRoomList({required bool isRefresh}) async {
if (_isLoading) return;
if (!isRefresh && !_hasMore) return;
setState(() => _isLoading = true);
if (isRefresh) {
_currentPage = 1;
_hasMore = true;
_roomList.clear();
}
// API WAIT/RUNNING/FINISH ()
final String searchType = widget.roomStatus.toUpperCase();
final String searchValue = _searchController.text.trim();
final String searchPage = _currentPage.toString();
final requestBody = {
"search_type": searchType,
"search_value": searchValue,
"search_page": searchPage,
};
try {
final response = await Api.serverRequest(
uri: '/room/score/room/list',
body: requestBody,
);
print('🔵 response: $response');
// () : { result: OK, response: {...}, ... }
if (response == null || response['result'] != 'OK') {
showResponseDialog(context, '오류', '방 목록을 불러오지 못했습니다.');
} else {
final innerResp = response['response'];
if (innerResp == null || innerResp['result'] != 'OK') {
showResponseDialog(context, '오류', '내부 응답이 잘못되었습니다.');
} else {
final respData = innerResp['data'];
if (respData is List) {
if (respData.isEmpty) {
_hasMore = false;
} else {
for (var item in respData) {
print('🔵 item: $item');
final parsedItem = {
'room_seq': item['room_seq'] ?? 0,
'nickname': item['nickname'] ?? '사용자',
'room_status': _statusToKr(item['room_status'] ?? ''),
'open_yn': (item['open_yn'] == 'Y') ? '공개' : '비공개',
'room_type': item['room_type_name'] ?? 'private',
'room_title': item['room_title'] ?? '(방제목 없음)',
'room_intro': item['room_intro'] ?? '',
'now_people': item['now_number_of_people']?.toString() ?? '0',
'max_people': item['number_of_people']?.toString() ?? '0',
'start_dt': item['start_dt'],
'end_dt': item['end_dt'],
};
_roomList.add(parsedItem);
}
if (respData.length < _pageSize) {
_hasMore = false;
}
_currentPage++;
}
}
}
}
} catch (e) {
showResponseDialog(context, '오류', '서버 요청 중 예외 발생: $e');
} finally {
setState(() => _isLoading = false);
}
}
/// WAIT->'대기중', RUNNING->'진행중', FINISH->'종료'
String _statusToKr(String status) {
switch (status.toUpperCase()) {
case 'WAIT':
return '대기중';
case 'RUNNING':
return '진행중';
case 'FINISH':
return '종료';
default:
return status;
}
}
void _onSearch() {
_fetchRoomList(isRefresh: true);
}
void _onRoomItemTap(Map<String, dynamic> item) {
//
showDialog(
context: context,
barrierDismissible: false,
builder: (_) => RoomDetailDialog(roomData: item),
);
}
@override
Widget build(BuildContext context) {
final statusKr = _statusToKr(widget.roomStatus);
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.black,
title: Text('$statusKr 방 검색', style: const TextStyle(color: Colors.white)),
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
onPressed: () => Navigator.pop(context),
),
),
body: Column(
children: [
// (A)
Padding(
padding: const EdgeInsets.all(8.0),
child: TextField(
controller: _searchController,
onSubmitted: (_) => _onSearch(),
style: const TextStyle(color: Colors.black),
decoration: InputDecoration(
hintText: '방 제목 입력',
hintStyle: TextStyle(color: Colors.grey.shade400),
prefixIcon: const Icon(Icons.search, color: Colors.black54),
suffixIcon: IconButton(
icon: const Icon(Icons.close, color: Colors.black54),
onPressed: () {
_searchController.clear();
},
),
border: OutlineInputBorder(
borderSide: const BorderSide(color: Colors.black),
borderRadius: BorderRadius.circular(20),
),
contentPadding: const EdgeInsets.symmetric(vertical: 8, horizontal: 12),
),
),
),
// (B) or
Expanded(
child: _isLoading && _roomList.isEmpty
? const Center(child: CircularProgressIndicator())
: _buildRoomListView(),
),
// (C)
Container(
height: 60,
color: Colors.white,
child: Center(
child: Container(
height: 50,
width: 300,
color: Colors.grey.shade400,
child: const Center(
child: Text('구글 광고', style: TextStyle(color: Colors.black)),
),
),
),
),
],
),
);
}
Widget _buildRoomListView() {
print('🔵 _roomList: $_roomList');
if (_roomList.isEmpty) {
return const Center(
child: Text(
'검색 결과가 없습니다.',
style: TextStyle(color: Colors.black),
),
);
}
return ListView.builder(
controller: _scrollController,
padding: const EdgeInsets.symmetric(vertical: 8, horizontal: 16),
itemCount: _roomList.length,
itemBuilder: (context, index) {
final item = _roomList[index];
return _buildRoomItem(item);
},
);
}
Widget _buildRoomItem(Map<String, dynamic> item) {
final roomTitle = item['room_title'] ?? '(방제목 없음)';
final nickname = item['nickname'] ?? '유저';
final roomStatus = item['room_status'] ?? '대기중';
final openYn = item['open_yn'] ?? '공개';
final nowPeople = item['now_people'] ?? '0';
final maxPeople = item['max_people'] ?? '0';
final roomIntro = item['room_intro'] ?? '';
return GestureDetector(
onTap: () => _onRoomItemTap(item),
child: Container(
margin: const EdgeInsets.only(bottom: 12),
padding: const EdgeInsets.all(10),
decoration: BoxDecoration(
border: Border.all(color: Colors.black54),
borderRadius: BorderRadius.circular(8),
),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Text(roomTitle, style: const TextStyle(fontSize: 16, fontWeight: FontWeight.bold)),
const SizedBox(height: 4),
Text('$nickname / $roomStatus / $openYn / $nowPeople/$maxPeople명'),
const SizedBox(height: 4),
Text(roomIntro, style: const TextStyle(fontSize: 12)),
],
),
),
);
}
}

View File

@ -0,0 +1,666 @@
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:firebase_database/firebase_database.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'main_page.dart';
import '../../plugins/api.dart'; // API
import '../../dialogs/response_dialog.dart';
import '../../dialogs/yes_no_dialog.dart'; // /
import '../../dialogs/room_setting_dialog.dart';
import '../../dialogs/user_info_private_dialog.dart';
import 'playing_private_page.dart';
class WaitingRoomPrivatePage extends StatefulWidget {
final int roomSeq;
final String roomType; // "private"
const WaitingRoomPrivatePage({
Key? key,
required this.roomSeq,
required this.roomType,
}) : super(key: key);
@override
State<WaitingRoomPrivatePage> createState() => _WaitingRoomPrivatePageState();
}
class _WaitingRoomPrivatePageState extends State<WaitingRoomPrivatePage> {
//
//
//
String roomMasterYn = 'N';
String roomTitle = '';
String roomIntro = '';
String openYn = 'Y';
String roomPw = '';
int runningTime = 1;
int numberOfPeople = 10;
String scoreOpenRange = 'PRIVATE';
//
List<Map<String, dynamic>> _userList = [];
bool _isLoading = true;
// FRD
late DatabaseReference _roomRef;
Stream<DatabaseEvent>? _roomStream;
//
bool _movedToRunningPage = false;
//
bool _kickedOut = false;
// FRD
StreamSubscription<DatabaseEvent>? _roomStreamSubscription;
// () user_seq
String mySeq = '0'; // '6'
@override
void initState() {
super.initState();
_loadMySeq();
}
/// (A) my_user_seq ->
Future<void> _loadMySeq() async {
final prefs = await SharedPreferences.getInstance();
mySeq = prefs.getInt('my_user_seq')?.toString() ?? '0';
final roomKey = 'korea-${widget.roomSeq}';
_roomRef = FirebaseDatabase.instance.ref('rooms/$roomKey');
_listenRoomData();
}
void _listenRoomData() {
_roomStream = _roomRef.onValue;
_roomStream?.listen((event) {
final snapshot = event.snapshot;
if (!snapshot.exists) {
setState(() {
_isLoading = false;
roomTitle = '방 정보 없음';
_userList = [];
});
return;
}
final data = snapshot.value as Map<dynamic, dynamic>? ?? {};
final roomInfoData = data['roomInfo'] as Map<dynamic, dynamic>? ?? {};
final userInfoData = data['userInfo'] as Map<dynamic, dynamic>? ?? {};
final roomStatus = (roomInfoData['room_status'] ?? 'WAIT').toString().toUpperCase();
setState(() {
roomTitle = (roomInfoData['room_title'] ?? '') as String;
roomIntro = (roomInfoData['room_intro'] ?? '') as String;
openYn = (roomInfoData['open_yn'] ?? 'Y') as String;
roomPw = (roomInfoData['room_pw'] ?? '') as String;
runningTime = _toInt(roomInfoData['running_time'], 1);
numberOfPeople = _toInt(roomInfoData['number_of_people'], 10);
scoreOpenRange = (roomInfoData['score_open_range'] ?? 'PRIVATE') as String;
//
roomMasterYn = 'N';
final masterSeq = roomInfoData['master_user_seq'];
if (masterSeq != null && masterSeq.toString() == mySeq) {
roomMasterYn = 'Y';
}
//
final tempList = <Map<String, dynamic>>[];
userInfoData.forEach((userSeq, userMap) {
tempList.add({
'user_seq': userSeq,
'participant_type': userMap['participant_type'] ?? '',
'nickname': userMap['nickname'] ?? '유저',
'score': userMap['score'] ?? 0,
'profile_img': userMap['profile_img'] ?? '',
'department': userMap['department'] ?? '',
'introduce_myself': userMap['introduce_myself'] ?? '',
'ready_yn': (userMap['ready_yn'] ?? 'N').toString().toUpperCase(),
});
});
_userList = tempList;
_isLoading = false;
});
// ->
if (roomStatus == 'RUNNING' && !_movedToRunningPage) {
_movedToRunningPage = true;
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => PlayingPrivatePage(
roomSeq: widget.roomSeq,
roomTitle: roomTitle,
),
),
);
return;
}
// (2) "내 user_seq가 목록에 있는지"
final amIStillHere = _userList.any((u) => u['user_seq'].toString() == mySeq);
// (3) ,
// (_kickedOut == false),
// (roomMasterYn != 'Y'),
// / => "강퇴"
if (!amIStillHere && !_kickedOut && roomMasterYn != 'Y') {
// RUNNING이면 ,
// DELETE
_kickedOut = true; //
// () +
Future.delayed(Duration.zero, () async {
await showResponseDialog(context, '안내', '강퇴되었습니다.');
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const MainPage()),
);
});
}
}, onError: (error) {
print('FRD onError: $error');
setState(() {
_isLoading = false;
roomTitle = '오류 발생';
});
});
}
@override
void dispose() {
_roomStreamSubscription?.cancel(); //
super.dispose();
}
/// (B) ->
Future<void> _onLeaveRoom() async {
if (roomMasterYn == 'Y') {
//
final confirm = await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (_) {
return AlertDialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(color: Colors.black, width: 2),
),
title: const Center(
child: Text(
'방 나가기',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black),
),
),
content: const Text(
'방장이 나가면 방은 삭제됩니다.\n정말 나가시겠습니까?',
style: TextStyle(fontSize: 14, color: Colors.black),
),
actionsAlignment: MainAxisAlignment.spaceEvenly,
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
style: TextButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('취소'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
style: TextButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('확인'),
),
],
);
},
);
if (confirm != true) return;
// leave API
try {
final reqBody = {"room_seq": "${widget.roomSeq}"};
final response = await Api.serverRequest(uri: '/room/score/game/leave', body: reqBody);
if (response['result'] == 'OK') {
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
} else {
final msg = resp['response_info']?['msg_content'] ?? '방 나가기 실패';
final again = await showYesNoDialog(
context: context,
title: '오류',
message: '$msg\n그래도 나가시겠습니까?',
yesNo: true,
);
if (again == true) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
}
}
} else {
final again = await showYesNoDialog(
context: context,
title: '오류',
message: '서버오류\n그래도 나가시겠습니까?',
yesNo: true,
);
if (again == true) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
}
}
} catch (e) {
final again = await showYesNoDialog(
context: context,
title: '오류',
message: '$e\n그래도 나가시겠습니까?',
yesNo: true,
);
if (again == true) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
}
}
} else {
//
try {
final reqBody = {"room_seq": "${widget.roomSeq}"};
final response = await Api.serverRequest(uri: '/room/score/game/leave', body: reqBody);
if (response['result'] == 'OK') {
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
} else {
final msg = resp['response_info']?['msg_content'] ?? '나가기 실패';
final again = await showYesNoDialog(context: context, title: '오류', message: msg, yesNo: true);
if (again == true) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
}
}
} else {
final again = await showYesNoDialog(context: context, title: '오류', message: '서버 통신 오류', yesNo: true);
if (again == true) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
}
}
} catch (e) {
final again = await showYesNoDialog(context: context, title: '오류', message: '$e', yesNo: true);
if (again == true) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
}
}
}
}
int _toInt(dynamic val, int defaultVal) {
if (val == null) return defaultVal;
if (val is int) return val;
if (val is String) {
return int.tryParse(val) ?? defaultVal;
}
return defaultVal;
}
/// (=3, =2)
Widget _buildTopButtons() {
if (_isLoading) return const SizedBox();
final me = _userList.firstWhere(
(u) => (u['user_seq'] ?? '0') == mySeq,
orElse: () => {},
);
final myReadyYn = (me['ready_yn'] ?? 'N').toString().toUpperCase();
final bool isReady = (myReadyYn == 'Y');
final String readyLabel = isReady ? '준비완료' : '준비';
final btnStyle = ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
side: const BorderSide(color: Colors.black, width: 1),
);
if (roomMasterYn == 'Y') {
// => 3
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
child: ElevatedButton(
style: btnStyle,
onPressed: _onOpenRoomSetting,
child: const Text('방 설정'),
),
),
),
Expanded(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
child: ElevatedButton(
style: btnStyle,
onPressed: _onToggleReady,
child: Text(readyLabel),
),
),
),
Expanded(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
child: ElevatedButton(
style: btnStyle,
onPressed: _onGameStart,
child: const Text('게임 시작'),
),
),
),
],
);
} else {
// => 2
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8),
child: ElevatedButton(
style: btnStyle,
onPressed: _onOpenRoomSetting,
child: const Text('방 설정'),
),
),
),
Expanded(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8),
child: ElevatedButton(
style: btnStyle,
onPressed: _onToggleReady,
child: Text(readyLabel),
),
),
),
],
);
}
}
/// READY
Future<void> _onToggleReady() async {
try {
final me = _userList.firstWhere(
(u) => (u['user_seq'] ?? '0') == mySeq,
orElse: () => {},
);
final myReadyYn = (me['ready_yn'] ?? 'N').toString().toUpperCase();
final bool isReady = (myReadyYn == 'Y');
final newYn = isReady ? 'N' : 'Y';
final userRef = _roomRef.child('userInfo').child(mySeq);
await userRef.update({"ready_yn": newYn});
} catch (e) {
print('READY 설정 실패: $e');
}
}
///
Future<void> _onOpenRoomSetting() async {
final roomInfo = {
"room_seq": "${widget.roomSeq}",
"room_master_yn": roomMasterYn,
"room_title": roomTitle,
"room_intro": roomIntro,
"open_yn": openYn,
"room_pw": roomPw,
"running_time": runningTime,
"room_type": widget.roomType,
"number_of_people": numberOfPeople,
"score_open_range": scoreOpenRange,
};
final result = await showDialog(
context: context,
barrierDismissible: false,
builder: (_) => RoomSettingModal(roomInfo: roomInfo),
);
if (result == 'refresh') {
// ...
}
}
///
Future<void> _onGameStart() async {
final notReady = _userList.any((u) {
final ry = (u['ready_yn'] ?? 'N').toString().toUpperCase();
return (ry != 'Y');
});
if (notReady) {
showResponseDialog(context, '안내', 'READY되지 않은 참가자가 있습니다(방장 포함).');
return;
}
final requestBody = {
"room_seq": "${widget.roomSeq}",
"room_type": "PRIVATE",
};
try {
final response = await Api.serverRequest(
uri: '/room/score/game/start',
body: requestBody,
);
if (response['result'] == 'OK') {
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
print('게임 시작 요청 성공(개인전)');
} else {
final msgTitle = resp['response_info']?['msg_title'] ?? '오류';
final msgContent = resp['response_info']?['msg_content'] ?? '게임 시작 실패';
showResponseDialog(context, msgTitle, msgContent);
}
} else {
showResponseDialog(context, '실패', '서버 통신 오류');
}
} catch (e) {
showResponseDialog(context, '오류', '$e');
}
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.black,
elevation: 0,
title: const Text('대기 방 (개인전)', style: TextStyle(color: Colors.white)),
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
onPressed: _onLeaveRoom,
),
),
bottomNavigationBar: Container(
height: 50,
decoration: BoxDecoration(
color: Colors.grey.shade300,
border: Border.all(color: Colors.black, width: 1),
),
child: const Center(
child: Text('구글 광고', style: TextStyle(color: Colors.black)),
),
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 20, horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Text(
roomTitle.isNotEmpty ? roomTitle : '방 제목',
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 8),
_buildTopButtons(),
const SizedBox(height: 20),
const Text('사회자', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black)),
const SizedBox(height: 8),
_buildAdminSection(),
const SizedBox(height: 20),
const Text('참가자', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black)),
const SizedBox(height: 8),
_buildPlayerSection(),
],
),
),
);
}
//
Widget _buildAdminSection() {
final adminList = _userList.where((u) {
final t = (u['participant_type'] ?? '').toString().toUpperCase();
return t == 'ADMIN';
}).toList();
return Container(
width: double.infinity,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.black),
borderRadius: BorderRadius.circular(8),
),
child: adminList.isEmpty
? const Text('사회자가 없습니다.', style: TextStyle(color: Colors.black))
: Wrap(
spacing: 16,
runSpacing: 8,
children: adminList.map(_buildSeat).toList(),
),
);
}
//
Widget _buildPlayerSection() {
final playerList = _userList.where((u) {
final t = (u['participant_type'] ?? '').toString().toUpperCase();
return t != 'ADMIN';
}).toList();
return Container(
width: double.infinity,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.black),
borderRadius: BorderRadius.circular(8),
),
child: playerList.isEmpty
? const Text('참가자가 없습니다.', style: TextStyle(color: Colors.black))
: SingleChildScrollView(
child: Wrap(
spacing: 16,
runSpacing: 16,
alignment: WrapAlignment.center,
children: playerList.map(_buildSeat).toList(),
),
),
);
}
// Seat
Widget _buildSeat(Map<String, dynamic> userData) {
final userName = userData['nickname'] ?? '유저';
final profileImg = userData['profile_img'] ?? '';
final readyYn = userData['ready_yn'] ?? 'N';
final isReady = (readyYn == 'Y');
final isMaster = (roomMasterYn == 'Y');
return GestureDetector(
onTap: () async {
final result = await showDialog(
context: context,
barrierDismissible: false,
builder: (_) => UserInfoPrivateDialog(
userData: userData,
isRoomMaster: isMaster,
roomSeq: widget.roomSeq,
roomTypeName: 'PRIVATE',
),
);
if (result == 'refresh') {
// ...
}
},
child: Container(
margin: const EdgeInsets.only(right: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(
color: isReady ? Colors.red : Colors.black,
width: isReady ? 2 : 1,
),
borderRadius: BorderRadius.circular(20),
boxShadow: isReady
? [
BoxShadow(
color: Colors.redAccent.withOpacity(0.6),
blurRadius: 8,
spreadRadius: 2,
offset: const Offset(0, 0),
)
]
: [],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Image.network(
'https://eldsoft.com:8097/images$profileImg',
fit: BoxFit.cover,
errorBuilder: (ctx, err, st) {
return const Center(
child: Text(
'이미지\n불가',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 10),
),
);
},
),
),
),
const SizedBox(height: 4),
Text(userName, style: const TextStyle(fontSize: 12, color: Colors.black)),
],
),
),
);
}
}

View File

@ -0,0 +1,795 @@
import 'package:flutter/material.dart';
import 'dart:async';
import 'package:firebase_database/firebase_database.dart';
import 'package:shared_preferences/shared_preferences.dart';
import 'main_page.dart';
import '../../plugins/api.dart'; // API
import '../../dialogs/response_dialog.dart'; //
import '../../dialogs/yes_no_dialog.dart'; // /
import '../../dialogs/room_setting_dialog.dart';
import '../../dialogs/user_info_team_dialog.dart';
import '../../dialogs/team_name_edit_dialog.dart';
import 'playing_team_page.dart';
class WaitingRoomTeamPage extends StatefulWidget {
final int roomSeq;
final String roomType; // "team"
const WaitingRoomTeamPage({
Key? key,
required this.roomSeq,
required this.roomType,
}) : super(key: key);
@override
State<WaitingRoomTeamPage> createState() => _WaitingRoomTeamPageState();
}
class _WaitingRoomTeamPageState extends State<WaitingRoomTeamPage> {
//
//
//
String roomMasterYn = 'N';
String roomTitle = '';
String roomIntro = '';
String openYn = 'Y';
String roomPw = '';
int runningTime = 1;
int numberOfPeople = 10;
String scoreOpenRange = 'PRIVATE';
int numberOfTeams = 1;
//
List<String> _teamNameList = [];
//
List<Map<String, dynamic>> _userList = [];
bool _isLoading = true;
// FRD
late DatabaseReference _roomRef;
Stream<DatabaseEvent>? _roomStream;
//
bool _movedToRunningPage = false;
//
bool _kickedOut = false;
// FRD
StreamSubscription<DatabaseEvent>? _roomStreamSubscription;
// user_seq
String mySeq = '0'; // '6'
@override
void initState() {
super.initState();
_loadMySeq();
}
/// (A) user_seq를 +
Future<void> _loadMySeq() async {
final prefs = await SharedPreferences.getInstance();
// : getString or getInt
mySeq = prefs.getInt('my_user_seq')?.toString() ?? '0';
// roomKey / FRD
final roomKey = 'korea-${widget.roomSeq}';
_roomRef = FirebaseDatabase.instance.ref('rooms/$roomKey');
//
_listenRoomData();
}
@override
void dispose() {
_roomStreamSubscription?.cancel(); //
super.dispose();
}
void _listenRoomData() {
_roomStream = _roomRef.onValue;
_roomStream?.listen((event) async {
final snapshot = event.snapshot;
if (!snapshot.exists) {
setState(() {
_isLoading = false;
roomTitle = '방 정보 없음';
_userList = [];
});
return;
}
final data = snapshot.value as Map<dynamic, dynamic>? ?? {};
final roomInfoData = data['roomInfo'] as Map<dynamic, dynamic>? ?? {};
final userInfoData = data['userInfo'] as Map<dynamic, dynamic>? ?? {};
//
final roomStatus = (roomInfoData['room_status'] ?? 'WAIT').toString().toUpperCase();
setState(() {
roomTitle = (roomInfoData['room_title'] ?? '') as String;
roomIntro = (roomInfoData['room_intro'] ?? '') as String;
openYn = (roomInfoData['open_yn'] ?? 'Y') as String;
roomPw = (roomInfoData['room_pw'] ?? '') as String;
runningTime = _toInt(roomInfoData['running_time'], 1);
numberOfPeople = _toInt(roomInfoData['number_of_people'], 10);
scoreOpenRange = (roomInfoData['score_open_range'] ?? 'PRIVATE') as String;
numberOfTeams = _toInt(roomInfoData['number_of_teams'], 1);
//
final tStr = (roomInfoData['team_name_list'] ?? '') as String;
if (tStr.isNotEmpty) {
_teamNameList = tStr.split(',').map((e) => e.trim().toUpperCase()).toList();
} else {
_teamNameList = List.generate(numberOfTeams, (i) => String.fromCharCode(65 + i));
}
//
roomMasterYn = 'N';
final masterSeq = roomInfoData['master_user_seq'];
if (masterSeq != null && masterSeq.toString() == mySeq) {
roomMasterYn = 'Y';
}
//
final tempList = <Map<String, dynamic>>[];
userInfoData.forEach((userSeq, userMap) {
tempList.add({
'user_seq': userSeq,
'participant_type': userMap['participant_type'] ?? '',
'nickname': userMap['nickname'] ?? '유저',
'team_name': userMap['team_name'] ?? '',
'score': userMap['score'] ?? 0,
'profile_img': userMap['profile_img'] ?? '',
'department': userMap['department'] ?? '',
'introduce_myself': userMap['introduce_myself'] ?? '',
'ready_yn': (userMap['ready_yn'] ?? 'N').toString().toUpperCase(),
});
});
_userList = tempList;
_isLoading = false;
});
// RUNNING이면
if (roomStatus == 'RUNNING' && !_movedToRunningPage) {
_movedToRunningPage = true;
Navigator.pushReplacement(
context,
MaterialPageRoute(
builder: (_) => PlayingTeamPage(
roomSeq: widget.roomSeq,
roomTitle: roomTitle,
),
),
);
return;
}
// (2) "내 user_seq가 목록에 있는지"
final amIStillHere = _userList.any((u) => u['user_seq'].toString() == mySeq);
// (3) ,
// (_kickedOut == false),
// (roomMasterYn != 'Y'),
// / => "강퇴"
if (!amIStillHere && !_kickedOut && roomMasterYn != 'Y') {
// RUNNING이면 ,
// DELETE
_kickedOut = true; //
// () +
Future.delayed(Duration.zero, () async {
await showResponseDialog(context, '안내', '강퇴되었습니다.');
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (_) => const MainPage()),
);
});
}
}, onError: (error) {
print('FRD onError: $error');
setState(() {
_isLoading = false;
roomTitle = '오류 발생';
});
});
}
//
// [] ->
//
Future<void> _onLeaveRoom() async {
if (roomMasterYn == 'Y') {
// ->
final confirm = await showDialog<bool>(
context: context,
barrierDismissible: false,
builder: (_) {
return AlertDialog(
backgroundColor: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(16),
side: const BorderSide(color: Colors.black, width: 2),
),
title: const Center(
child: Text(
'방 나가기',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold, color: Colors.black),
),
),
content: const Text(
'방장이 나가면 방은 삭제됩니다.\n정말 나가시겠습니까?',
style: TextStyle(fontSize: 14, color: Colors.black),
),
actionsAlignment: MainAxisAlignment.spaceEvenly,
actions: [
TextButton(
onPressed: () => Navigator.pop(context, false),
style: TextButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('취소'),
),
TextButton(
onPressed: () => Navigator.pop(context, true),
style: TextButton.styleFrom(
backgroundColor: Colors.black,
foregroundColor: Colors.white,
padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 12),
shape: RoundedRectangleBorder(borderRadius: BorderRadius.circular(8)),
),
child: const Text('확인'),
),
],
);
},
);
if (confirm != true) return;
try {
final reqBody = {"room_seq": "${widget.roomSeq}"};
final response = await Api.serverRequest(uri: '/room/score/game/leave', body: reqBody);
if (response['result'] == 'OK') {
final resp = response['response'];
if (resp != null && resp['result'] == 'OK') {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
} else {
final msg = resp?['response_info']?['msg_content'] ?? '방 나가기 실패';
final again = await showYesNoDialog(
context: context,
title: '오류',
message: '$msg\n그래도 나가시겠습니까?',
yesNo: true,
);
if (again == true) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
}
}
} else {
final again = await showYesNoDialog(
context: context,
title: '오류',
message: '서버오류\n그래도 나가시겠습니까?',
yesNo: true,
);
if (again == true) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
}
}
} catch (e) {
final again = await showYesNoDialog(
context: context,
title: '오류',
message: '$e\n그래도 나가시겠습니까?',
yesNo: true,
);
if (again == true) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
}
}
} else {
//
try {
final reqBody = {"room_seq": "${widget.roomSeq}"};
final response = await Api.serverRequest(uri: '/room/score/game/leave', body: reqBody);
if (response['result'] == 'OK') {
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
} else {
final msg = resp['response_info']?['msg_content'] ?? '나가기 실패';
final again = await showYesNoDialog(context: context, title: '오류', message: msg, yesNo: true);
if (again == true) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
}
}
} else {
final again = await showYesNoDialog(context: context, title: '오류', message: '서버 통신 오류', yesNo: true);
if (again == true) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
}
}
} catch (e) {
final again = await showYesNoDialog(context: context, title: '오류', message: '$e', yesNo: true);
if (again == true) {
Navigator.pushReplacement(context, MaterialPageRoute(builder: (_) => const MainPage()));
}
}
}
}
int _toInt(dynamic val, int defaultVal) {
if (val == null) return defaultVal;
if (val is int) return val;
if (val is String) {
return int.tryParse(val) ?? defaultVal;
}
return defaultVal;
}
//
// : = 3, = 2
// READY "준비"/"준비완료"
// : READY=Y
//
Widget _buildTopButtons() {
if (_isLoading) return const SizedBox();
// READY
final me = _userList.firstWhere(
(u) => (u['user_seq'] ?? '0') == mySeq,
orElse: () => {},
);
final myReadyYn = (me['ready_yn'] ?? 'N').toString().toUpperCase();
final bool isReady = (myReadyYn == 'Y');
final String readyLabel = isReady ? '준비완료' : '준비';
final btnStyle = ElevatedButton.styleFrom(
backgroundColor: Colors.white,
foregroundColor: Colors.black,
side: const BorderSide(color: Colors.black, width: 1),
);
if (roomMasterYn == 'Y') {
// -> [ ], [/], [ ]
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
child: ElevatedButton(
style: btnStyle,
onPressed: _onOpenRoomSetting,
child: const Text('방 설정'),
),
),
),
Expanded(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
child: ElevatedButton(
style: btnStyle,
onPressed: _onToggleReady,
child: Text(readyLabel),
),
),
),
Expanded(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 4),
child: ElevatedButton(
style: btnStyle,
onPressed: _onGameStart,
child: const Text('게임 시작'),
),
),
),
],
);
} else {
// -> [ ], [/]
return Row(
mainAxisAlignment: MainAxisAlignment.center,
children: [
Expanded(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8),
child: ElevatedButton(
style: btnStyle,
onPressed: _onOpenRoomSetting,
child: const Text('방 설정'),
),
),
),
Expanded(
child: Container(
margin: const EdgeInsets.symmetric(horizontal: 8),
child: ElevatedButton(
style: btnStyle,
onPressed: _onToggleReady,
child: Text(readyLabel),
),
),
),
],
);
}
}
/// READY
Future<void> _onToggleReady() async {
try {
//
final me = _userList.firstWhere(
(u) => (u['user_seq'] ?? '') == mySeq,
orElse: () => {},
);
final myReadyYn = (me['ready_yn'] ?? 'N').toString().toUpperCase();
final bool isReady = (myReadyYn == 'Y');
final newYn = isReady ? 'N' : 'Y';
final userRef = _roomRef.child('userInfo').child(mySeq);
await userRef.update({"ready_yn": newYn});
} catch (e) {
print('READY 설정 실패: $e');
}
}
///
Future<void> _onOpenRoomSetting() async {
final roomInfo = {
"room_seq": "${widget.roomSeq}",
"room_master_yn": roomMasterYn,
"room_title": roomTitle,
"room_intro": roomIntro,
"open_yn": openYn,
"room_pw": roomPw,
"running_time": runningTime,
"room_type": widget.roomType,
"number_of_people": numberOfPeople,
"score_open_range": scoreOpenRange,
"number_of_teams": numberOfTeams,
"team_name_list": _teamNameList.join(','),
};
final result = await showDialog(
context: context,
barrierDismissible: false,
builder: (_) => RoomSettingModal(roomInfo: roomInfo),
);
if (result == 'refresh') {
// do something
}
}
/// ( READY=Y )
Future<void> _onGameStart() async {
final notReady = _userList.any((u) {
final ry = (u['ready_yn'] ?? 'N').toString().toUpperCase();
return (ry != 'Y');
});
if (notReady) {
showResponseDialog(context, '안내', 'READY되지 않은 참가자가 있습니다(방장 포함).');
return;
}
final requestBody = {
"room_seq": "${widget.roomSeq}",
"room_type": "TEAM",
};
try {
final response = await Api.serverRequest(
uri: '/room/score/game/start',
body: requestBody,
);
if (response['result'] == 'OK') {
final resp = response['response'] ?? {};
if (resp['result'] == 'OK') {
print('게임 시작 요청 성공(팀전)');
} else {
final msgTitle = resp['response_info']?['msg_title'] ?? '오류';
final msgContent = resp['response_info']?['msg_content'] ?? '게임 시작 실패';
showResponseDialog(context, msgTitle, msgContent);
}
} else {
showResponseDialog(context, '실패', '서버 통신 오류');
}
} catch (e) {
showResponseDialog(context, '오류', '$e');
}
}
//
// / / / Seat
//
Widget _buildAdminSection() {
final adminList = _userList.where((u) {
final pType = (u['participant_type'] ?? '').toString().toUpperCase();
return pType == 'ADMIN';
}).toList();
return Container(
width: double.infinity,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.black),
borderRadius: BorderRadius.circular(8),
),
child: adminList.isEmpty
? const Text('사회자가 없습니다.', style: TextStyle(color: Colors.black))
: Wrap(
spacing: 16,
runSpacing: 8,
children: adminList.map(_buildSeat).toList(),
),
);
}
Widget _buildTeamSection() {
final players = _userList.where((u) {
final pType = (u['participant_type'] ?? '').toString().toUpperCase();
return (pType != 'ADMIN');
}).toList();
final Map<String, List<Map<String, dynamic>>> teamMap = {};
for (final tName in _teamNameList) {
teamMap[tName] = [];
}
for (var user in players) {
final tName = (user['team_name'] ?? '').toString().trim().toUpperCase();
if (tName.isNotEmpty && tName != 'WAIT' && teamMap.containsKey(tName)) {
teamMap[tName]!.add(user);
}
}
if (teamMap.isEmpty) {
return Container(
width: double.infinity,
padding: const EdgeInsets.all(8),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.black),
borderRadius: BorderRadius.circular(8),
),
child: const Text('팀에 배정된 참가자가 없습니다.', style: TextStyle(color: Colors.black)),
);
}
return Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: _teamNameList.map((teamName) {
final members = teamMap[teamName]!;
return Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.black),
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
GestureDetector(
onTap: () async {
if (roomMasterYn == 'Y') {
final result = await showDialog(
context: context,
barrierDismissible: false,
builder: (_) => TeamNameEditModal(
roomSeq: widget.roomSeq,
roomTypeName: 'TEAM',
beforeTeamName: teamName,
existingTeamNames: _teamNameList,
),
);
if (result == 'refresh') {
// ...
}
}
},
child: Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 8),
color: Colors.black,
child: Center(
child: Text(
'$teamName',
style: const TextStyle(color: Colors.white, fontWeight: FontWeight.bold),
),
),
),
),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.all(8),
child: Row(children: members.map(_buildSeat).toList()),
),
],
),
);
}).toList(),
);
}
Widget _buildWaitSection() {
final waitList = _userList.where((u) {
final pType = (u['participant_type'] ?? '').toString().toUpperCase();
if (pType == 'ADMIN') return false;
final tName = (u['team_name'] ?? '').toString().toUpperCase();
return (tName.isEmpty || tName == 'WAIT');
}).toList();
if (waitList.isEmpty) return const SizedBox();
return Container(
margin: const EdgeInsets.only(bottom: 16),
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(color: Colors.black),
borderRadius: BorderRadius.circular(8),
),
child: Column(
children: [
Container(
width: double.infinity,
padding: const EdgeInsets.symmetric(vertical: 8),
color: Colors.black,
child: const Center(
child: Text('대기중', style: TextStyle(color: Colors.white, fontWeight: FontWeight.bold)),
),
),
SingleChildScrollView(
scrollDirection: Axis.horizontal,
padding: const EdgeInsets.all(8),
child: Row(children: waitList.map(_buildSeat).toList()),
),
],
),
);
}
Widget _buildSeat(Map<String, dynamic> user) {
final userName = user['nickname'] ?? '유저';
final profileImg = user['profile_img'] ?? '';
final readyYn = user['ready_yn'] ?? 'N';
final isReady = (readyYn == 'Y');
final isMaster = (roomMasterYn == 'Y');
return GestureDetector(
onTap: () async {
//
final result = await showDialog(
context: context,
barrierDismissible: false,
builder: (_) => UserInfoTeamDialog(
userData: user,
isRoomMaster: isMaster,
roomSeq: widget.roomSeq,
roomTypeName: widget.roomType.toUpperCase(), // "TEAM"
teamNameList: _teamNameList,
),
);
if (result == 'refresh') {
// ...
}
},
child: Container(
width: 60,
margin: const EdgeInsets.only(right: 8),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
Container(
width: 50,
height: 50,
decoration: BoxDecoration(
color: Colors.white,
border: Border.all(
color: isReady ? Colors.red : Colors.black,
width: isReady ? 2 : 1,
),
borderRadius: BorderRadius.circular(20),
boxShadow: isReady
? [
BoxShadow(
color: Colors.redAccent.withOpacity(0.6),
blurRadius: 8,
spreadRadius: 2,
offset: const Offset(0, 0),
)
]
: [],
),
child: ClipRRect(
borderRadius: BorderRadius.circular(20),
child: Image.network(
'https://eldsoft.com:8097/images$profileImg',
fit: BoxFit.cover,
errorBuilder: (ctx, err, st) {
return const Center(
child: Text(
'이미지\n불가',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 10),
),
);
},
),
),
),
const SizedBox(height: 2),
Text(userName, style: const TextStyle(fontSize: 12, color: Colors.black)),
],
),
),
);
}
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
backgroundColor: Colors.black,
elevation: 0,
title: const Text('대기 방 (팀전)', style: TextStyle(color: Colors.white)),
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.white),
onPressed: _onLeaveRoom, //
),
),
bottomNavigationBar: Container(
height: 50,
decoration: BoxDecoration(
color: Colors.grey.shade300,
border: Border.all(color: Colors.black, width: 1),
),
child: const Center(
child: Text('구글 광고', style: TextStyle(color: Colors.black)),
),
),
body: _isLoading
? const Center(child: CircularProgressIndicator())
: SingleChildScrollView(
padding: const EdgeInsets.symmetric(vertical: 16, horizontal: 16),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Center(
child: Text(
roomTitle.isNotEmpty ? roomTitle : '방 제목',
style: const TextStyle(fontSize: 24, fontWeight: FontWeight.bold, color: Colors.black),
textAlign: TextAlign.center,
),
),
const SizedBox(height: 8),
_buildTopButtons(),
const SizedBox(height: 20),
const Text('사회자', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black)),
const SizedBox(height: 8),
_buildAdminSection(),
const SizedBox(height: 20),
const Text('팀별 참가자', style: TextStyle(fontSize: 16, fontWeight: FontWeight.bold, color: Colors.black)),
const SizedBox(height: 8),
_buildTeamSection(),
const SizedBox(height: 20),
_buildWaitSection(),
],
),
),
);
}
}

927
lib/views/user/my_page.dart Normal file
View File

@ -0,0 +1,927 @@
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
import '../../dialogs/response_dialog.dart';
import 'package:crypto/crypto.dart';
import '../../plugins/api.dart';
import '../../plugins/utils.dart';
import 'withdrawal_page.dart';
import 'package:image_picker/image_picker.dart';
import 'dart:io';
// import 'package:path/path.dart';
class MyPage extends StatefulWidget {
const MyPage({Key? key}) : super(key: key);
@override
_MyPageState createState() => _MyPageState();
}
class _MyPageState extends State<MyPage> {
String user_nickname = '';
String user_pw = '';
String new_user_pw = '**********';
String user_email = '';
String user_department = '';
String user_introduce_myself = '';
String user_profile_image = '';
bool isEditingNickname = false;
bool isEditingPassword = false;
bool isConfirmingPassword = false;
String confirmPassword = '';
final TextEditingController _nicknameController = TextEditingController();
final TextEditingController _passwordController = TextEditingController();
final TextEditingController _confirmPasswordController = TextEditingController();
final FocusNode _nicknameFocusNode = FocusNode();
final FocusNode _passwordFocusNode = FocusNode();
final FocusNode _confirmPasswordFocusNode = FocusNode();
String? _passwordError;
String? _emailError;
final TextEditingController _emailController = TextEditingController();
final FocusNode _emailFocusNode = FocusNode();
bool isEditingEmail = false;
final TextEditingController _departmentController = TextEditingController();
final FocusNode _departmentFocusNode = FocusNode();
bool isEditingDepartment = false;
final TextEditingController _introduceController = TextEditingController();
String? _nicknameError;
XFile? _image; //
@override
void initState() {
super.initState();
_fetchUserInfo();
}
@override
void dispose() {
_nicknameController.dispose();
_passwordController.dispose();
_confirmPasswordController.dispose();
_nicknameFocusNode.dispose();
_passwordFocusNode.dispose();
_confirmPasswordFocusNode.dispose();
_emailController.dispose();
_emailFocusNode.dispose();
_departmentController.dispose();
_departmentFocusNode.dispose();
_introduceController.dispose();
super.dispose();
}
@override
Widget build(BuildContext context) {
return GestureDetector(
onTap: () {
if (isEditingNickname) {
setState(() {
user_nickname = _nicknameController.text;
isEditingNickname = false;
_nicknameFocusNode.unfocus();
});
}
if (isEditingPassword) {
setState(() {
new_user_pw = _passwordController.text;
isEditingPassword = false;
_passwordFocusNode.unfocus();
});
}
if (isConfirmingPassword) {
setState(() {
isConfirmingPassword = false;
_confirmPasswordFocusNode.unfocus();
});
}
if (isEditingEmail) {
setState(() {
user_email = _emailController.text;
isEditingEmail = false;
_emailFocusNode.unfocus();
});
}
if (isEditingDepartment) {
setState(() {
user_department = _departmentController.text;
isEditingDepartment = false;
_departmentFocusNode.unfocus();
});
}
},
child: Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text('MY PAGE', style: TextStyle(color: Colors.black)),
backgroundColor: Colors.transparent,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.black),
onPressed: () {
Navigator.pop(context);
Navigator.pop(context);
},
),
),
body: SingleChildScrollView(
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
crossAxisAlignment: CrossAxisAlignment.start,
children: [
Card(
elevation: 4,
color: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'닉네임:',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
Expanded(
child: GestureDetector(
onTap: () {
setState(() {
isEditingNickname = true;
_nicknameController.text = user_nickname;
_nicknameFocusNode.requestFocus();
});
},
child: isEditingNickname
? TextField(
controller: _nicknameController,
focusNode: _nicknameFocusNode,
onChanged: (newNickname) {
setState(() {
user_nickname = newNickname;
//
if (!_isNicknameValidPattern(user_nickname)) {
//
_nicknameError = '닉네임은 2~20자 영문, 한글, 숫자만 사용할 수 있습니다.';
} else {
_nicknameError = null; //
}
});
},
onSubmitted: (newNickname) {
setState(() {
user_nickname = newNickname;
isEditingNickname = false;
_nicknameFocusNode.unfocus();
});
},
decoration: InputDecoration(
hintText: '닉네임을 입력하세요',
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 8.0),
),
)
: Container(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
user_nickname,
style: const TextStyle(fontSize: 18, color: Colors.black54),
),
),
),
),
],
),
),
),
//
if (_nicknameError != null)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
_nicknameError!,
style: const TextStyle(color: Colors.red, fontSize: 12),
),
),
const SizedBox(height: 16),
Card(
elevation: 4,
color: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'비밀번호:',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
Expanded(
child: GestureDetector(
onTap: () {
setState(() {
isEditingPassword = true;
_passwordController.text = new_user_pw.replaceAll(RegExp(r'.'), '*');
_passwordFocusNode.requestFocus();
});
},
child: isEditingPassword
? TextField(
controller: _passwordController,
focusNode: _passwordFocusNode,
obscureText: true,
onChanged: (newPassword) {
setState(() {
new_user_pw = newPassword;
_passwordError = _isPasswordValidPattern(new_user_pw) ? null : '비밀번호는 8~20자 영문과 숫자가 반드시 포함되어야 합니다.';
});
},
onSubmitted: (newPassword) {
setState(() {
isEditingPassword = false;
_passwordFocusNode.unfocus();
});
},
decoration: InputDecoration(
hintText: '비밀번호를 입력하세요',
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 8.0),
),
)
: Container(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
'**********',
style: const TextStyle(fontSize: 18, color: Colors.black54),
),
),
),
),
],
),
),
),
//
if (_passwordError != null)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
_passwordError!,
style: const TextStyle(color: Colors.red, fontSize: 12),
),
),
const SizedBox(height: 16),
Card(
elevation: 4,
color: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'비밀번호 확인:',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
Expanded(
child: GestureDetector(
onTap: () {
setState(() {
isConfirmingPassword = true;
_confirmPasswordController.text = '';
_confirmPasswordFocusNode.requestFocus();
});
},
child: isConfirmingPassword
? TextField(
controller: _confirmPasswordController,
focusNode: _confirmPasswordFocusNode,
obscureText: true,
onChanged: (value) {
setState(() {
confirmPassword = value;
});
},
onSubmitted: (newPassword) {
setState(() {
isConfirmingPassword = false;
_confirmPasswordFocusNode.unfocus();
});
},
decoration: InputDecoration(
hintText: '비밀번호를 다시 입력해주세요',
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 8.0),
),
)
: Container(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
'*' * confirmPassword.length,
style: const TextStyle(fontSize: 18, color: Colors.black54),
),
),
),
),
],
),
),
),
//
const SizedBox(height: 8),
Text(
(new_user_pw == confirmPassword)
? '비밀번호가 일치합니다.'
: (confirmPassword.isNotEmpty ? '비밀번호가 일치하지 않습니다.' : ''),
style: TextStyle(
fontSize: 16,
color: (new_user_pw == confirmPassword) ? Colors.green : Colors.red,
),
),
Card(
elevation: 4,
color: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'이메일:',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
Expanded(
child: GestureDetector(
onTap: () {
setState(() {
isEditingEmail = true;
_emailController.text = user_email;
_emailFocusNode.requestFocus();
});
},
child: isEditingEmail
? TextField(
controller: _emailController,
focusNode: _emailFocusNode,
onChanged: (value) {
setState(() {
user_email = value;
_emailError = _isEmailValid(user_email) ? null : '올바른 이메일 형식을 입력해주세요.';
});
},
onSubmitted: (newEmail) {
setState(() {
user_email = newEmail;
isEditingEmail = false;
_emailFocusNode.unfocus();
});
},
decoration: InputDecoration(
hintText: '이메일을 입력하세요',
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 8.0),
),
)
: Container(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
user_email,
style: const TextStyle(fontSize: 18, color: Colors.black54),
),
),
),
),
],
),
),
),
//
if (_emailError != null)
Padding(
padding: const EdgeInsets.only(top: 8.0),
child: Text(
_emailError!,
style: const TextStyle(color: Colors.red, fontSize: 12),
),
),
const SizedBox(height: 8),
Card(
elevation: 4,
color: Colors.white,
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10),
),
child: Padding(
padding: const EdgeInsets.all(16.0),
child: Row(
mainAxisAlignment: MainAxisAlignment.spaceBetween,
children: [
const Text(
'소속:',
style: TextStyle(fontSize: 18, fontWeight: FontWeight.bold),
),
Expanded(
child: GestureDetector(
onTap: () {
setState(() {
isEditingDepartment = true;
_departmentController.text = user_department;
_departmentFocusNode.requestFocus();
});
},
child: isEditingDepartment
? TextField(
controller: _departmentController,
focusNode: _departmentFocusNode,
onChanged: (value) {
setState(() {
user_department = value;
});
},
onSubmitted: (newDepartment) {
setState(() {
user_department = newDepartment;
isEditingDepartment = false;
_departmentFocusNode.unfocus();
});
},
decoration: InputDecoration(
hintText: '소속을 입력하세요',
border: InputBorder.none,
contentPadding: const EdgeInsets.symmetric(vertical: 10.0, horizontal: 8.0),
),
)
: Container(
padding: const EdgeInsets.symmetric(horizontal: 8.0),
child: Text(
user_department,
style: const TextStyle(fontSize: 18, color: Colors.black54),
),
),
),
),
],
),
),
),
const SizedBox(height: 8),
//
const SizedBox(height: 8),
const Text(
'프로필 이미지를 설정해주세요.',
style: TextStyle(fontSize: 16, color: Colors.black),
),
const SizedBox(height: 16), //
Center(
child: Column(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
GestureDetector(
onTap: () {
_selectImage();
},
child: Container(
width: 100, //
height: 100, //
decoration: BoxDecoration(
color: Colors.white, //
border: Border.all(color: Colors.black), //
borderRadius: BorderRadius.circular(20), //
),
child: ClipRRect(
borderRadius: BorderRadius.circular(20), //
child: Image.network(
'https://eldsoft.com:8097/images${user_profile_image}', // URL
fit: BoxFit.cover, //
errorBuilder: (context, error, stackTrace) {
return const Center(child: Text('이미지를 불러올 수 없습니다.')); //
},
),
),
),
),
const SizedBox(height: 8),
const Text(
'이미지를 클릭하시면 새로 등록할 수 있습니다.',
textAlign: TextAlign.center, //
style: TextStyle(fontSize: 12, color: Colors.blueGrey), //
),
],
),
),
const SizedBox(height: 8),
const Text(
'다른 유저에게 자신을 소개해주세요.',
style: TextStyle(fontSize: 16, color: Colors.black),
),
const SizedBox(height: 8), //
TextField(
controller: _introduceController..text = user_introduce_myself, //
maxLines: 5, //
decoration: InputDecoration(
hintText: '자신을 소개하는 내용을 입력하세요...',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10), //
borderSide: const BorderSide(color: Colors.black), //
),
contentPadding: const EdgeInsets.all(10), //
),
onChanged: (value) {
setState(() {
user_introduce_myself = value; //
});
},
),
const SizedBox(height: 30), //
Center(
child: OutlinedButton(
onPressed: () {
//
_showEditDialog(); //
},
style: OutlinedButton.styleFrom(
backgroundColor: Colors.white, //
side: const BorderSide(color: Colors.black54, width: 1), //
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40), //
),
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 60), //
),
child: const Text(
'수정하기',
style: TextStyle(color: Colors.black, fontSize: 16), //
),
),
),
const SizedBox(height: 10), //
Center(
child: OutlinedButton(
onPressed: () {
// WithdrawalPage로
Navigator.push(
context,
MaterialPageRoute(builder: (context) => const WithdrawalPage()),
);
},
style: OutlinedButton.styleFrom(
backgroundColor: Colors.white, //
side: const BorderSide(color: Colors.black54, width: 1), //
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40), //
),
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 60), //
),
child: const Text(
'회원탈퇴',
style: TextStyle(color: Colors.black, fontSize: 16), //
),
),
),
const SizedBox(height: 30), //
],
),
),
),
),
);
}
Future<void> _fetchUserInfo() async {
SharedPreferences prefs = await SharedPreferences.getInstance();
String? authToken = prefs.getString('auth_token');
final response = await Api.serverRequest(
uri: '/user/myinfo',
body: {},
);
if (response['result'] == 'OK') {
final jsonResponse = response['response'];
print('응답: $jsonResponse');
if (jsonResponse['result'] == 'OK') {
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.setString('auth_token', jsonResponse['auth']['token']);
setState(() {
user_nickname = jsonResponse['data']['nickname'];
user_email = jsonResponse['data']['user_email'];
user_department = jsonResponse['data']['department'];
user_introduce_myself = jsonResponse['data']['introduce_myself'];
user_profile_image = jsonResponse['data']['profile_img'];
});
} else {
showResponseDialog(context, '${jsonResponse['response_info']['msg_title']}', '${jsonResponse['response_info']['msg_content']}');
}
} else {
showResponseDialog(context, '요청 실패', '서버에 문제가 있습니다. 관리자에게 문의해주세요.');
}
}
bool _isPasswordValidPattern(String password) {
// 8 , 20 ,
return password.length >= 8 && password.length <= 20 &&
RegExp(r'^(?=.*[A-Za-z])(?=.*\d)[A-Za-z\d\S]{8,20}$').hasMatch(password);
}
bool _isEmailValid(String email) {
return RegExp(r'^[\w-\.]+@([\w-]+\.)+[\w-]{2,4}$').hasMatch(email); //
}
Future<void> _selectImage() async {
// .
showDialog(
context: context,
builder: (BuildContext context) {
return Dialog(
backgroundColor: Colors.transparent, //
child: Container(
decoration: BoxDecoration(
color: Colors.white, //
borderRadius: BorderRadius.circular(16), //
),
padding: const EdgeInsets.all(16),
child: Column(
mainAxisSize: MainAxisSize.min,
children: [
const Text(
'프로필 이미지 선택',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold),
),
const SizedBox(height: 16),
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly,
children: [
Expanded(
child: ElevatedButton(
onPressed: () {
Navigator.of(context).pop(); //
_pickImage(); //
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white, //
side: const BorderSide(color: Colors.black), //
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), //
),
),
child: const Text(
'갤러리',
style: TextStyle(color: Colors.black), //
),
),
),
const SizedBox(width: 8), //
Expanded(
child: ElevatedButton(
onPressed: () {
Navigator.of(context).pop(); //
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white, //
side: const BorderSide(color: Colors.black), //
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(10), //
),
),
child: const Text(
'취소',
style: TextStyle(color: Colors.black), //
),
),
),
],
),
],
),
),
);
},
).then((pickedFile) {
if (pickedFile != null) {
// .
if (pickedFile == 'gallery') {
_pickImage(); //
}
}
});
}
//
void _showEditDialog() {
// confirmPassword
if (new_user_pw != '**********') {
if (!_isPasswordValidPattern(new_user_pw)) {
showResponseDialog(context, '수정하기 실패', '비밀번호 패턴을 확인해주세요.');
return;
}
if (new_user_pw != confirmPassword) {
showResponseDialog(context, '수정하기 실패', '비밀번호가 일치하지 않습니다.');
return; //
}
}
if (!_isEmailValid(user_email)) {
showResponseDialog(context, '수정하기 실패', '이메일 형식을 확인해주세요.');
return;
}
if (!_isNicknameValidPattern(user_nickname)) {
showResponseDialog(context, '수정하기 실패', '닉네임 패턴을 확인해주세요.');
return;
}
showDialog(
context: context,
builder: (BuildContext context) {
return AlertDialog(
backgroundColor: Colors.white, //
title: const Center( //
child: Text(
'회원정보 수정',
style: TextStyle(fontSize: 20, fontWeight: FontWeight.bold), //
),
),
content: Column(
mainAxisSize: MainAxisSize.min, //
children: [
const Center( //
child: Text(
'회원정보를 수정합니다.',
style: TextStyle(fontSize: 16), //
),
),
const SizedBox(height: 8), //
const Center( //
child: Text(
'현재 비밀번호를 입력해주세요.',
style: TextStyle(fontSize: 16), //
),
),
const SizedBox(height: 8), //
TextField(
obscureText: true, //
decoration: InputDecoration(
hintText: '현재 비밀번호',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10), //
borderSide: const BorderSide(color: Colors.black), //
),
contentPadding: const EdgeInsets.all(10), //
),
onChanged: (value) {
user_pw = value; // user_pw에
},
),
],
),
actions: [
Row(
mainAxisAlignment: MainAxisAlignment.spaceEvenly, //
children: [
SizedBox(
width: 100, //
child: ElevatedButton(
onPressed: () async {
//
try {
final serverResponse = await _updateUserInfo(); //
if (serverResponse['result'] == 'OK') {
final serverResponse1 = serverResponse['response'];
if (serverResponse1['result'] == 'OK') {
showResponseDialog(context, '수정하기 성공', '회원정보가 성공적으로 수정되었습니다.');
Navigator.of(context).pop(); //
Navigator.of(context).pop(); //
} else {
showResponseDialog(context, '${serverResponse1['response_info']['msg_title']}', '${serverResponse1['response_info']['msg_content']}');
}
} else {
showResponseDialog(context, '수정하기 실패', '서버에 문제가 있습니다. 관리자에게 문의해주세요.');
}
} catch (e) {
showResponseDialog(context, '수정하기 실패', e.toString());
}
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white, //
side: const BorderSide(color: Colors.black54, width: 1), //
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40), //
),
),
child: const Text(
'수정하기',
style: TextStyle(color: Colors.black, fontSize: 14), //
),
),
),
SizedBox(
width: 100, //
child: ElevatedButton(
onPressed: () {
Navigator.of(context).pop(); //
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.white, //
side: const BorderSide(color: Colors.black54, width: 1), //
shape: RoundedRectangleBorder(
borderRadius: BorderRadius.circular(40), //
),
),
child: const Text(
'취소',
style: TextStyle(color: Colors.black, fontSize: 14), //
),
),
),
],
),
],
);
},
);
}
//
Future<Map<String, dynamic>> _updateUserInfo() async {
try {
final serverResponse = await Api.serverRequest(
uri: '/user/update/user/info',
body: {
"user_pw": Utils.hashPassword(user_pw), //
"new_user_pw": Utils.hashPassword(new_user_pw), //
"user_pw_change_yn": new_user_pw != '**********' ? 'Y' : 'N', //
"nickname": user_nickname, //
"user_email": user_email, //
"department": user_department, //
"profile_img": user_profile_image, //
"introduce_myself": user_introduce_myself, //
},
);
print('serverResponse 응답: $serverResponse');
// null인지
if (serverResponse == null) {
throw Exception('서버 응답이 null입니다.');
}
//
if (serverResponse['result'] == 'OK') {
return serverResponse; //
} else {
return {
'result': 'FAIL',
};
}
} catch (e) {
print('serverResponse 오류: $e');
return {
'result': 'FAIL',
};
}
}
//
bool _isNicknameValidPattern(String nickname) {
// 3~15
return RegExp(r'^[A-Za-z가-힣0-9]{2,20}$').hasMatch(nickname);
}
Future<void> _pickImage() async {
final ImagePicker picker = ImagePicker();
//
final XFile? selectedImage = await picker.pickImage(source: ImageSource.gallery);
if (selectedImage != null) {
//
final serverResponse = await Api.uploadProfileImage(selectedImage, body: {
'test': 'test', //
});
if (serverResponse['result'] == 'OK') {
final serverResponse1 = serverResponse['response'];
print('응답: $serverResponse1');
if (serverResponse1['result'] == 'OK') {
showResponseDialog(context, '업로드 성공', '프로필 이미지가 성공적으로 업로드되었습니다.');
// user_profile_image ( '/images' )
setState(() {
user_profile_image = serverResponse1['data']['img_src'].replaceFirst('/images', ''); // '/images'
});
} else {
showResponseDialog(context, '${serverResponse1['response_info']['msg_title']}', '${serverResponse1['response_info']['msg_content']}');
}
} else {
showResponseDialog(context, '업로드 실패', '서버에 문제가 있습니다. 관리자에게 문의해주세요.');
}
}
}
}

View File

@ -0,0 +1,155 @@
import 'package:flutter/material.dart';
import 'package:http/http.dart' as http;
import 'package:shared_preferences/shared_preferences.dart';
import 'dart:convert';
import '../../plugins/utils.dart';
import '../../plugins/api.dart';
import '../../dialogs/response_dialog.dart';
import '../login/login_page.dart'; //
class WithdrawalPage extends StatefulWidget {
const WithdrawalPage({Key? key}) : super(key: key);
@override
_WithdrawalPageState createState() => _WithdrawalPageState();
}
class _WithdrawalPageState extends State<WithdrawalPage> {
bool _isAgreed = false; //
final TextEditingController _passwordController = TextEditingController(); //
@override
Widget build(BuildContext context) {
return Scaffold(
backgroundColor: Colors.white,
appBar: AppBar(
title: const Text('회원탈퇴', style: TextStyle(color: Colors.black)),
backgroundColor: Colors.transparent,
leading: IconButton(
icon: const Icon(Icons.arrow_back_ios, color: Colors.black),
onPressed: () => Navigator.pop(context),
),
),
body: Padding(
padding: const EdgeInsets.all(16.0),
child: Column(
mainAxisAlignment: MainAxisAlignment.center,
children: [
const Text(
'회원탈퇴를 진행합니다.\n현재 비밀번호를 입력해주세요.',
textAlign: TextAlign.center,
style: TextStyle(fontSize: 18),
),
const SizedBox(height: 20),
TextField(
controller: _passwordController,
obscureText: true,
decoration: InputDecoration(
hintText: '비밀번호 입력',
border: OutlineInputBorder(
borderRadius: BorderRadius.circular(10),
borderSide: const BorderSide(color: Colors.black),
),
contentPadding: const EdgeInsets.all(10),
),
),
const SizedBox(height: 20),
Container(
decoration: BoxDecoration(
border: Border.all(color: Colors.black54, width: 1), //
borderRadius: BorderRadius.circular(10), //
),
padding: const EdgeInsets.all(16.0), //
child: const Text(
'[회원 탈퇴 안내]\n'
'회원 탈퇴를 진행하시겠습니까?\n'
' - 회원 탈퇴 시 등록하신 모든 개인정보(ID, 비밀번호, 닉네임, 이메일 주소, 소속, 자기소개 등)는 즉시 삭제되며 복구가 불가능합니다.\n'
' - 탈퇴 후 동일한 아이디로 재가입이 불가능할 수 있습니다.\n'
' - 관련 법령에 따라 일정 기간 보관이 필요한 경우 해당 기간 동안 법령이 허용하는 범위 내에서만 보관됩니다.\n'
'탈퇴를 원하시면 아래의 "동의" 버튼을 눌러주시기 바랍니다.',
textAlign: TextAlign.left,
style: TextStyle(fontSize: 12),
),
),
const SizedBox(height: 20),
Row(
children: [
Checkbox(
value: _isAgreed, //
activeColor: Colors.black, //
checkColor: Colors.white, //
onChanged: (value) {
setState(() {
_isAgreed = value ?? false; //
});
},
),
const Expanded(
child: Text(
'회원탈퇴에 동의합니다.',
style: TextStyle(fontSize: 16),
),
),
],
),
const SizedBox(height: 20),
ElevatedButton(
onPressed: () {
//
_requestWithdrawal(_passwordController.text, _isAgreed);
},
style: ElevatedButton.styleFrom(
backgroundColor: Colors.black54, //
padding: const EdgeInsets.symmetric(vertical: 12, horizontal: 60), //
),
child: const Text(
'탈퇴하기',
style: TextStyle(color: Colors.white, fontSize: 16), //
),
),
],
),
),
);
}
Future<void> _requestWithdrawal(String password, bool isAgreed) async {
if (!isAgreed) {
//
showResponseDialog(context, '회원탈퇴 동의 확인', '회원탈퇴 동의 체크가 필요합니다.');
return;
}
if (password.isEmpty) {
//
showResponseDialog(context, '비밀번호 확인', '비밀번호를 입력해야 합니다.');
return;
}
final response = await Api.serverRequest(
uri: '/user/withdraw/user',
body: {
'user_pw': Utils.hashPassword(password), //
},
);
if (response['result'] == 'OK') {
final serverResponse = response['response'];
if (serverResponse['result'] == 'OK') {
//
SharedPreferences prefs = await SharedPreferences.getInstance();
await prefs.remove('auth_token'); //
showResponseDialog(context, '회원탈퇴 완료', '회원탈퇴가 완료되었습니다.');
Navigator.pushReplacement(
context,
MaterialPageRoute(builder: (context) => const LoginPage()), //
);
} else {
showResponseDialog(context, serverResponse['response_info']['msg_title'], serverResponse['response_info']['msg_content']);
}
} else {
showResponseDialog(context, '회원탈퇴 실패', '서버에 문제가 있습니다. 관리자에게 문의해주세요.');
}
}
}

View File

@ -6,6 +6,10 @@
#include "generated_plugin_registrant.h"
#include <file_selector_linux/file_selector_plugin.h>
void fl_register_plugins(FlPluginRegistry* registry) {
g_autoptr(FlPluginRegistrar) file_selector_linux_registrar =
fl_plugin_registry_get_registrar_for_plugin(registry, "FileSelectorPlugin");
file_selector_plugin_register_with_registrar(file_selector_linux_registrar);
}

View File

@ -3,6 +3,7 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_linux
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST

View File

@ -5,8 +5,18 @@
import FlutterMacOS
import Foundation
import file_selector_macos
import firebase_auth
import firebase_core
import firebase_database
import shared_preferences_foundation
import webview_flutter_wkwebview
func RegisterGeneratedPlugins(registry: FlutterPluginRegistry) {
FileSelectorPlugin.register(with: registry.registrar(forPlugin: "FileSelectorPlugin"))
FLTFirebaseAuthPlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseAuthPlugin"))
FLTFirebaseCorePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseCorePlugin"))
FLTFirebaseDatabasePlugin.register(with: registry.registrar(forPlugin: "FLTFirebaseDatabasePlugin"))
SharedPreferencesPlugin.register(with: registry.registrar(forPlugin: "SharedPreferencesPlugin"))
FLTWebViewFlutterPlugin.register(with: registry.registrar(forPlugin: "FLTWebViewFlutterPlugin"))
}

View File

@ -1,6 +1,14 @@
# Generated by pub
# See https://dart.dev/tools/pub/glossary#lockfile
packages:
_flutterfire_internals:
dependency: transitive
description:
name: _flutterfire_internals
sha256: daa1d780fdecf8af925680c06c86563cdd445deea995d5c9176f1302a2b10bbe
url: "https://pub.dev"
source: hosted
version: "1.3.48"
async:
dependency: transitive
description:
@ -41,8 +49,16 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.19.0"
cross_file:
dependency: transitive
description:
name: cross_file
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
url: "https://pub.dev"
source: hosted
version: "0.3.4+2"
crypto:
dependency: "direct dev"
dependency: "direct main"
description:
name: crypto
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
@ -81,6 +97,110 @@ packages:
url: "https://pub.dev"
source: hosted
version: "7.0.1"
file_selector_linux:
dependency: transitive
description:
name: file_selector_linux
sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33"
url: "https://pub.dev"
source: hosted
version: "0.9.3+2"
file_selector_macos:
dependency: transitive
description:
name: file_selector_macos
sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc"
url: "https://pub.dev"
source: hosted
version: "0.9.4+2"
file_selector_platform_interface:
dependency: transitive
description:
name: file_selector_platform_interface
sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b
url: "https://pub.dev"
source: hosted
version: "2.6.2"
file_selector_windows:
dependency: transitive
description:
name: file_selector_windows
sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4"
url: "https://pub.dev"
source: hosted
version: "0.9.3+3"
firebase_auth:
dependency: "direct main"
description:
name: firebase_auth
sha256: "03483af6e67b7c4b696ca9386989a6cd5593569e1ac5af6907ea5f7fd9c16d8b"
url: "https://pub.dev"
source: hosted
version: "5.3.4"
firebase_auth_platform_interface:
dependency: transitive
description:
name: firebase_auth_platform_interface
sha256: "3e1409f48c48930635705b1237ebbdee8c54c19106a0a4fb321dbb4b642820c4"
url: "https://pub.dev"
source: hosted
version: "7.4.10"
firebase_auth_web:
dependency: transitive
description:
name: firebase_auth_web
sha256: d83fe95c44d73c9c29b006ac7df3aa5e1b8ce92b62edc44e8f86250951fe2cd0
url: "https://pub.dev"
source: hosted
version: "5.13.5"
firebase_core:
dependency: "direct main"
description:
name: firebase_core
sha256: "15d761b95dfa2906dfcc31b7fc6fe293188533d1a3ffe78389ba9e69bd7fdbde"
url: "https://pub.dev"
source: hosted
version: "3.9.0"
firebase_core_platform_interface:
dependency: transitive
description:
name: firebase_core_platform_interface
sha256: d7253d255ff10f85cfd2adaba9ac17bae878fa3ba577462451163bd9f1d1f0bf
url: "https://pub.dev"
source: hosted
version: "5.4.0"
firebase_core_web:
dependency: transitive
description:
name: firebase_core_web
sha256: fbc008cf390d909b823763064b63afefe9f02d8afdb13eb3f485b871afee956b
url: "https://pub.dev"
source: hosted
version: "2.19.0"
firebase_database:
dependency: "direct main"
description:
name: firebase_database
sha256: "473c25413683c1c4c8d80918efdc1a232722624bad3b6edfed9fae52b8d927c1"
url: "https://pub.dev"
source: hosted
version: "11.2.0"
firebase_database_platform_interface:
dependency: transitive
description:
name: firebase_database_platform_interface
sha256: e83241bcbe4e1bcfcbfd12d0e2ef7706af009663d291efa96bc965adb9ded25d
url: "https://pub.dev"
source: hosted
version: "0.2.5+47"
firebase_database_web:
dependency: transitive
description:
name: firebase_database_web
sha256: "9f4048132a3645f1ad528c4839a7c15ad3ff922ee7761821ea9526ffd52735b7"
url: "https://pub.dev"
source: hosted
version: "0.2.6+5"
flutter:
dependency: "direct main"
description: flutter
@ -94,6 +214,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "5.0.0"
flutter_plugin_android_lifecycle:
dependency: transitive
description:
name: flutter_plugin_android_lifecycle
sha256: "615a505aef59b151b46bbeef55b36ce2b6ed299d160c51d84281946f0aa0ce0e"
url: "https://pub.dev"
source: hosted
version: "2.0.24"
flutter_test:
dependency: "direct dev"
description: flutter
@ -104,8 +232,16 @@ packages:
description: flutter
source: sdk
version: "0.0.0"
google_mobile_ads:
dependency: "direct main"
description:
name: google_mobile_ads
sha256: "4775006383a27a5d86d46f8fb452bfcb17794fc0a46c732979e49a8eb1c8963f"
url: "https://pub.dev"
source: hosted
version: "5.2.0"
http:
dependency: "direct dev"
dependency: "direct main"
description:
name: http
sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
@ -120,6 +256,70 @@ packages:
url: "https://pub.dev"
source: hosted
version: "4.1.1"
image_picker:
dependency: "direct main"
description:
name: image_picker
sha256: b6951e25b795d053a6ba03af5f710069c99349de9341af95155d52665cb4607c
url: "https://pub.dev"
source: hosted
version: "0.8.9"
image_picker_android:
dependency: transitive
description:
name: image_picker_android
sha256: fa8141602fde3f7e2f81dbf043613eb44dfa325fa0bcf93c0f142c9f7a2c193e
url: "https://pub.dev"
source: hosted
version: "0.8.12+18"
image_picker_for_web:
dependency: transitive
description:
name: image_picker_for_web
sha256: "869fe8a64771b7afbc99fc433a5f7be2fea4d1cb3d7c11a48b6b579eb9c797f0"
url: "https://pub.dev"
source: hosted
version: "2.2.0"
image_picker_ios:
dependency: transitive
description:
name: image_picker_ios
sha256: "4f0568120c6fcc0aaa04511cb9f9f4d29fc3d0139884b1d06be88dcec7641d6b"
url: "https://pub.dev"
source: hosted
version: "0.8.12+1"
image_picker_linux:
dependency: transitive
description:
name: image_picker_linux
sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa"
url: "https://pub.dev"
source: hosted
version: "0.2.1+1"
image_picker_macos:
dependency: transitive
description:
name: image_picker_macos
sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62"
url: "https://pub.dev"
source: hosted
version: "0.2.1+1"
image_picker_platform_interface:
dependency: transitive
description:
name: image_picker_platform_interface
sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80"
url: "https://pub.dev"
source: hosted
version: "2.10.0"
image_picker_windows:
dependency: transitive
description:
name: image_picker_windows
sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb"
url: "https://pub.dev"
source: hosted
version: "0.2.1+1"
leak_tracker:
dependency: transitive
description:
@ -148,10 +348,10 @@ packages:
dependency: transitive
description:
name: lints
sha256: "4a16b3f03741e1252fda5de3ce712666d010ba2122f8e912c94f9f7b90e1a4c3"
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
url: "https://pub.dev"
source: hosted
version: "5.1.0"
version: "5.1.1"
matcher:
dependency: transitive
description:
@ -176,6 +376,14 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.15.0"
mime:
dependency: transitive
description:
name: mime
sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a"
url: "https://pub.dev"
source: hosted
version: "1.0.6"
path:
dependency: transitive
description:
@ -225,13 +433,13 @@ packages:
source: hosted
version: "2.1.8"
shared_preferences:
dependency: "direct dev"
dependency: "direct main"
description:
name: shared_preferences
sha256: "95f9997ca1fb9799d494d0cb2a780fd7be075818d59f00c43832ed112b158a82"
sha256: "3c7e73920c694a436afaf65ab60ce3453d91f84208d761fbd83fc21182134d93"
url: "https://pub.dev"
source: hosted
version: "2.3.3"
version: "2.3.4"
shared_preferences_android:
dependency: transitive
description:
@ -365,6 +573,38 @@ packages:
url: "https://pub.dev"
source: hosted
version: "1.1.0"
webview_flutter:
dependency: transitive
description:
name: webview_flutter
sha256: ec81f57aa1611f8ebecf1d2259da4ef052281cb5ad624131c93546c79ccc7736
url: "https://pub.dev"
source: hosted
version: "4.9.0"
webview_flutter_android:
dependency: transitive
description:
name: webview_flutter_android
sha256: "47a8da40d02befda5b151a26dba71f47df471cddd91dfdb7802d0a87c5442558"
url: "https://pub.dev"
source: hosted
version: "3.16.9"
webview_flutter_platform_interface:
dependency: transitive
description:
name: webview_flutter_platform_interface
sha256: d937581d6e558908d7ae3dc1989c4f87b786891ab47bb9df7de548a151779d8d
url: "https://pub.dev"
source: hosted
version: "2.10.0"
webview_flutter_wkwebview:
dependency: transitive
description:
name: webview_flutter_wkwebview
sha256: b7e92f129482460951d96ef9a46b49db34bd2e1621685de26e9eaafd9674e7eb
url: "https://pub.dev"
source: hosted
version: "3.16.3"
xdg_directories:
dependency: transitive
description:

View File

@ -30,6 +30,14 @@ environment:
dependencies:
flutter:
sdk: flutter
google_mobile_ads: ^5.2.0
http: ^1.2.2
crypto: ^3.0.1
shared_preferences: ^2.0.6
image_picker: ^0.8.4+4
firebase_core: ^3.9.0
firebase_auth: ^5.3.4
firebase_database: ^11.2.0
# The following adds the Cupertino Icons font to your application.
# Use with the CupertinoIcons class for iOS style icons.
@ -38,9 +46,14 @@ dependencies:
dev_dependencies:
flutter_test:
sdk: flutter
google_mobile_ads: ^5.2.0
http: ^1.2.2
crypto: ^3.0.1
shared_preferences: ^2.0.6
image_picker: ^0.8.4+4
firebase_core: ^3.9.0
firebase_auth: ^5.3.4
firebase_database: ^11.2.0
# The "flutter_lints" package below contains a set of recommended lints to
# encourage good coding practices. The lint set provided by the package is

14
settings.gradle.kts Normal file
View File

@ -0,0 +1,14 @@
/*
* This file was generated by the Gradle 'init' task.
*
* The settings file is used to specify which projects to include in your build.
* For more detailed information on multi-project builds, please refer to https://docs.gradle.org/8.12/userguide/multi_project_builds.html in the Gradle documentation.
*/
plugins {
// Apply the foojay-resolver plugin to allow automatic download of JDKs
id("org.gradle.toolchains.foojay-resolver-convention") version "0.8.0"
}
rootProject.name = "AllSCORE"
include("app")

View File

@ -6,6 +6,15 @@
#include "generated_plugin_registrant.h"
#include <file_selector_windows/file_selector_windows.h>
#include <firebase_auth/firebase_auth_plugin_c_api.h>
#include <firebase_core/firebase_core_plugin_c_api.h>
void RegisterPlugins(flutter::PluginRegistry* registry) {
FileSelectorWindowsRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FileSelectorWindows"));
FirebaseAuthPluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FirebaseAuthPluginCApi"));
FirebaseCorePluginCApiRegisterWithRegistrar(
registry->GetRegistrarForPlugin("FirebaseCorePluginCApi"));
}

View File

@ -3,6 +3,9 @@
#
list(APPEND FLUTTER_PLUGIN_LIST
file_selector_windows
firebase_auth
firebase_core
)
list(APPEND FLUTTER_FFI_PLUGIN_LIST