최신화
This commit is contained in:
parent
0f450c884f
commit
3069552557
12
.gitattributes
vendored
Normal file
12
.gitattributes
vendored
Normal 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
6
.gitignore
vendored
@ -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
|
||||
|
@ -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 {
|
||||
|
30
android/app/google-services.json
Normal file
30
android/app/google-services.json
Normal 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"
|
||||
}
|
@ -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>
|
||||
|
@ -1,4 +1,4 @@
|
||||
package com.example.allscore_app
|
||||
package com.allscore_app
|
||||
|
||||
import io.flutter.embedding.android.FlutterActivity
|
||||
|
||||
|
@ -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}"
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
||||
|
@ -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
43
app/build.gradle.kts
Normal 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()
|
||||
}
|
14
app/src/main/java/org/example/App.java
Normal file
14
app/src/main/java/org/example/App.java
Normal 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());
|
||||
}
|
||||
}
|
14
app/src/test/java/org/example/AppTest.java
Normal file
14
app/src/test/java/org/example/AppTest.java
Normal 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
1
firebase.json
Normal 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
5
gradle.properties
Normal 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
10
gradle/libs.versions.toml
Normal 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
BIN
gradle/wrapper/gradle-wrapper.jar
vendored
Normal file
Binary file not shown.
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal file
7
gradle/wrapper/gradle-wrapper.properties
vendored
Normal 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
251
gradlew
vendored
Normal 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
94
gradlew.bat
vendored
Normal 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
|
@ -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>
|
||||
|
47
lib/dialogs/response_dialog.dart
Normal file
47
lib/dialogs/response_dialog.dart
Normal 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('확인'),
|
||||
),
|
||||
),
|
||||
],
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
267
lib/dialogs/room_detail_dialog.dart
Normal file
267
lib/dialogs/room_detail_dialog.dart
Normal 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)),
|
||||
);
|
||||
}
|
||||
}
|
490
lib/dialogs/room_setting_dialog.dart
Normal file
490
lib/dialogs/room_setting_dialog.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
172
lib/dialogs/score_edit_dialog.dart
Normal file
172
lib/dialogs/score_edit_dialog.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
88
lib/dialogs/settings_dialog.dart
Normal file
88
lib/dialogs/settings_dialog.dart
Normal 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('로그아웃'),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
);
|
||||
},
|
||||
);
|
||||
}
|
148
lib/dialogs/team_name_edit_dialog.dart
Normal file
148
lib/dialogs/team_name_edit_dialog.dart
Normal 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)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
64
lib/dialogs/user_info_basic_dialog.dart
Normal file
64
lib/dialogs/user_info_basic_dialog.dart
Normal 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)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
272
lib/dialogs/user_info_private_dialog.dart
Normal file
272
lib/dialogs/user_info_private_dialog.dart
Normal 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)),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
316
lib/dialogs/user_info_team_dialog.dart
Normal file
316
lib/dialogs/user_info_team_dialog.dart
Normal 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)),
|
||||
),
|
||||
],
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
67
lib/dialogs/yes_no_dialog.dart
Normal file
67
lib/dialogs/yes_no_dialog.dart
Normal 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
94
lib/firebase_options.dart
Normal 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',
|
||||
);
|
||||
|
||||
}
|
@ -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
109
lib/plugins/api.dart
Normal 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
11
lib/plugins/utils.dart
Normal 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(); // 해시 값을 문자열로 반환
|
||||
}
|
||||
}
|
@ -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))),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
489
lib/views/room/create_room_page.dart
Normal file
489
lib/views/room/create_room_page.dart
Normal 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),
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
48
lib/views/room/finish_private_page.dart
Normal file
48
lib/views/room/finish_private_page.dart
Normal 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)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
47
lib/views/room/finish_team_page.dart
Normal file
47
lib/views/room/finish_team_page.dart
Normal 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)),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
159
lib/views/room/main_page.dart
Normal file
159
lib/views/room/main_page.dart
Normal 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)),
|
||||
);
|
||||
}
|
||||
}
|
362
lib/views/room/playing_private_page.dart
Normal file
362
lib/views/room/playing_private_page.dart
Normal 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)),
|
||||
),
|
||||
),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
414
lib/views/room/playing_team_page.dart
Normal file
414
lib/views/room/playing_team_page.dart
Normal 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),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
80
lib/views/room/room_search_home_page.dart
Normal file
80
lib/views/room/room_search_home_page.dart
Normal 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)),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
289
lib/views/room/room_search_list_page.dart
Normal file
289
lib/views/room/room_search_list_page.dart
Normal 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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
666
lib/views/room/waiting_room_private_page.dart
Normal file
666
lib/views/room/waiting_room_private_page.dart
Normal 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)),
|
||||
],
|
||||
),
|
||||
),
|
||||
);
|
||||
}
|
||||
}
|
795
lib/views/room/waiting_room_team_page.dart
Normal file
795
lib/views/room/waiting_room_team_page.dart
Normal 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
927
lib/views/user/my_page.dart
Normal 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, '업로드 실패', '서버에 문제가 있습니다. 관리자에게 문의해주세요.');
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
155
lib/views/user/withdrawal_page.dart
Normal file
155
lib/views/user/withdrawal_page.dart
Normal 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, '회원탈퇴 실패', '서버에 문제가 있습니다. 관리자에게 문의해주세요.');
|
||||
}
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
|
@ -3,6 +3,7 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_linux
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
@ -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"))
|
||||
}
|
||||
|
254
pubspec.lock
254
pubspec.lock
@ -1,6 +1,14 @@
|
||||
# Generated by pub
|
||||
# See https://dart.dev/tools/pub/glossary#lockfile
|
||||
packages:
|
||||
_flutterfire_internals:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: _flutterfire_internals
|
||||
sha256: daa1d780fdecf8af925680c06c86563cdd445deea995d5c9176f1302a2b10bbe
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.3.48"
|
||||
async:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -41,8 +49,16 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.19.0"
|
||||
cross_file:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: cross_file
|
||||
sha256: "7caf6a750a0c04effbb52a676dce9a4a592e10ad35c34d6d2d0e4811160d5670"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.3.4+2"
|
||||
crypto:
|
||||
dependency: "direct dev"
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: crypto
|
||||
sha256: "1e445881f28f22d6140f181e07737b22f1e099a5e1ff94b0af2f9e4a463f4855"
|
||||
@ -81,6 +97,110 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.0.1"
|
||||
file_selector_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_linux
|
||||
sha256: "54cbbd957e1156d29548c7d9b9ec0c0ebb6de0a90452198683a7d23aed617a33"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.3+2"
|
||||
file_selector_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_macos
|
||||
sha256: "271ab9986df0c135d45c3cdb6bd0faa5db6f4976d3e4b437cf7d0f258d941bfc"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.4+2"
|
||||
file_selector_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_platform_interface
|
||||
sha256: a3994c26f10378a039faa11de174d7b78eb8f79e4dd0af2a451410c1a5c3f66b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.6.2"
|
||||
file_selector_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: file_selector_windows
|
||||
sha256: "8f5d2f6590d51ecd9179ba39c64f722edc15226cc93dcc8698466ad36a4a85a4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.9.3+3"
|
||||
firebase_auth:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_auth
|
||||
sha256: "03483af6e67b7c4b696ca9386989a6cd5593569e1ac5af6907ea5f7fd9c16d8b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.3.4"
|
||||
firebase_auth_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_auth_platform_interface
|
||||
sha256: "3e1409f48c48930635705b1237ebbdee8c54c19106a0a4fb321dbb4b642820c4"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "7.4.10"
|
||||
firebase_auth_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_auth_web
|
||||
sha256: d83fe95c44d73c9c29b006ac7df3aa5e1b8ce92b62edc44e8f86250951fe2cd0
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.13.5"
|
||||
firebase_core:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_core
|
||||
sha256: "15d761b95dfa2906dfcc31b7fc6fe293188533d1a3ffe78389ba9e69bd7fdbde"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.9.0"
|
||||
firebase_core_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_platform_interface
|
||||
sha256: d7253d255ff10f85cfd2adaba9ac17bae878fa3ba577462451163bd9f1d1f0bf
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.4.0"
|
||||
firebase_core_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_core_web
|
||||
sha256: fbc008cf390d909b823763064b63afefe9f02d8afdb13eb3f485b871afee956b
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.19.0"
|
||||
firebase_database:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: firebase_database
|
||||
sha256: "473c25413683c1c4c8d80918efdc1a232722624bad3b6edfed9fae52b8d927c1"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "11.2.0"
|
||||
firebase_database_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_database_platform_interface
|
||||
sha256: e83241bcbe4e1bcfcbfd12d0e2ef7706af009663d291efa96bc965adb9ded25d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.5+47"
|
||||
firebase_database_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: firebase_database_web
|
||||
sha256: "9f4048132a3645f1ad528c4839a7c15ad3ff922ee7761821ea9526ffd52735b7"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.6+5"
|
||||
flutter:
|
||||
dependency: "direct main"
|
||||
description: flutter
|
||||
@ -94,6 +214,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.0.0"
|
||||
flutter_plugin_android_lifecycle:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: flutter_plugin_android_lifecycle
|
||||
sha256: "615a505aef59b151b46bbeef55b36ce2b6ed299d160c51d84281946f0aa0ce0e"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.0.24"
|
||||
flutter_test:
|
||||
dependency: "direct dev"
|
||||
description: flutter
|
||||
@ -104,8 +232,16 @@ packages:
|
||||
description: flutter
|
||||
source: sdk
|
||||
version: "0.0.0"
|
||||
google_mobile_ads:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: google_mobile_ads
|
||||
sha256: "4775006383a27a5d86d46f8fb452bfcb17794fc0a46c732979e49a8eb1c8963f"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.2.0"
|
||||
http:
|
||||
dependency: "direct dev"
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: http
|
||||
sha256: b9c29a161230ee03d3ccf545097fccd9b87a5264228c5d348202e0f0c28f9010
|
||||
@ -120,6 +256,70 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.1.1"
|
||||
image_picker:
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: image_picker
|
||||
sha256: b6951e25b795d053a6ba03af5f710069c99349de9341af95155d52665cb4607c
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.9"
|
||||
image_picker_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_android
|
||||
sha256: fa8141602fde3f7e2f81dbf043613eb44dfa325fa0bcf93c0f142c9f7a2c193e
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.12+18"
|
||||
image_picker_for_web:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_for_web
|
||||
sha256: "869fe8a64771b7afbc99fc433a5f7be2fea4d1cb3d7c11a48b6b579eb9c797f0"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.2.0"
|
||||
image_picker_ios:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_ios
|
||||
sha256: "4f0568120c6fcc0aaa04511cb9f9f4d29fc3d0139884b1d06be88dcec7641d6b"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.8.12+1"
|
||||
image_picker_linux:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_linux
|
||||
sha256: "4ed1d9bb36f7cd60aa6e6cd479779cc56a4cb4e4de8f49d487b1aaad831300fa"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1+1"
|
||||
image_picker_macos:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_macos
|
||||
sha256: "3f5ad1e8112a9a6111c46d0b57a7be2286a9a07fc6e1976fdf5be2bd31d4ff62"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1+1"
|
||||
image_picker_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_platform_interface
|
||||
sha256: "9ec26d410ff46f483c5519c29c02ef0e02e13a543f882b152d4bfd2f06802f80"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.10.0"
|
||||
image_picker_windows:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: image_picker_windows
|
||||
sha256: "6ad07afc4eb1bc25f3a01084d28520496c4a3bb0cb13685435838167c9dcedeb"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "0.2.1+1"
|
||||
leak_tracker:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -148,10 +348,10 @@ packages:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: lints
|
||||
sha256: "4a16b3f03741e1252fda5de3ce712666d010ba2122f8e912c94f9f7b90e1a4c3"
|
||||
sha256: c35bb79562d980e9a453fc715854e1ed39e24e7d0297a880ef54e17f9874a9d7
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "5.1.0"
|
||||
version: "5.1.1"
|
||||
matcher:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -176,6 +376,14 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.15.0"
|
||||
mime:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: mime
|
||||
sha256: "801fd0b26f14a4a58ccb09d5892c3fbdeff209594300a542492cf13fba9d247a"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.0.6"
|
||||
path:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -225,13 +433,13 @@ packages:
|
||||
source: hosted
|
||||
version: "2.1.8"
|
||||
shared_preferences:
|
||||
dependency: "direct dev"
|
||||
dependency: "direct main"
|
||||
description:
|
||||
name: shared_preferences
|
||||
sha256: "95f9997ca1fb9799d494d0cb2a780fd7be075818d59f00c43832ed112b158a82"
|
||||
sha256: "3c7e73920c694a436afaf65ab60ce3453d91f84208d761fbd83fc21182134d93"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.3.3"
|
||||
version: "2.3.4"
|
||||
shared_preferences_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
@ -365,6 +573,38 @@ packages:
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "1.1.0"
|
||||
webview_flutter:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter
|
||||
sha256: ec81f57aa1611f8ebecf1d2259da4ef052281cb5ad624131c93546c79ccc7736
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "4.9.0"
|
||||
webview_flutter_android:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_android
|
||||
sha256: "47a8da40d02befda5b151a26dba71f47df471cddd91dfdb7802d0a87c5442558"
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.16.9"
|
||||
webview_flutter_platform_interface:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_platform_interface
|
||||
sha256: d937581d6e558908d7ae3dc1989c4f87b786891ab47bb9df7de548a151779d8d
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "2.10.0"
|
||||
webview_flutter_wkwebview:
|
||||
dependency: transitive
|
||||
description:
|
||||
name: webview_flutter_wkwebview
|
||||
sha256: b7e92f129482460951d96ef9a46b49db34bd2e1621685de26e9eaafd9674e7eb
|
||||
url: "https://pub.dev"
|
||||
source: hosted
|
||||
version: "3.16.3"
|
||||
xdg_directories:
|
||||
dependency: transitive
|
||||
description:
|
||||
|
13
pubspec.yaml
13
pubspec.yaml
@ -30,6 +30,14 @@ environment:
|
||||
dependencies:
|
||||
flutter:
|
||||
sdk: flutter
|
||||
google_mobile_ads: ^5.2.0
|
||||
http: ^1.2.2
|
||||
crypto: ^3.0.1
|
||||
shared_preferences: ^2.0.6
|
||||
image_picker: ^0.8.4+4
|
||||
firebase_core: ^3.9.0
|
||||
firebase_auth: ^5.3.4
|
||||
firebase_database: ^11.2.0
|
||||
|
||||
# The following adds the Cupertino Icons font to your application.
|
||||
# Use with the CupertinoIcons class for iOS style icons.
|
||||
@ -38,9 +46,14 @@ dependencies:
|
||||
dev_dependencies:
|
||||
flutter_test:
|
||||
sdk: flutter
|
||||
google_mobile_ads: ^5.2.0
|
||||
http: ^1.2.2
|
||||
crypto: ^3.0.1
|
||||
shared_preferences: ^2.0.6
|
||||
image_picker: ^0.8.4+4
|
||||
firebase_core: ^3.9.0
|
||||
firebase_auth: ^5.3.4
|
||||
firebase_database: ^11.2.0
|
||||
|
||||
# The "flutter_lints" package below contains a set of recommended lints to
|
||||
# encourage good coding practices. The lint set provided by the package is
|
||||
|
14
settings.gradle.kts
Normal file
14
settings.gradle.kts
Normal 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")
|
@ -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"));
|
||||
}
|
||||
|
@ -3,6 +3,9 @@
|
||||
#
|
||||
|
||||
list(APPEND FLUTTER_PLUGIN_LIST
|
||||
file_selector_windows
|
||||
firebase_auth
|
||||
firebase_core
|
||||
)
|
||||
|
||||
list(APPEND FLUTTER_FFI_PLUGIN_LIST
|
||||
|
Loading…
Reference in New Issue
Block a user