관리자 로그인 기본 기능 구현
|
|
@ -0,0 +1 @@
|
|||
/.idea/
|
||||
28
README.md
|
|
@ -0,0 +1,28 @@
|
|||
# Proof of Concept
|
||||
## [관리자](poc%2Fadmin)
|
||||
### 관리자의 기본 기능을 구현한 PoC
|
||||
#### 1. Function
|
||||
- 로그인
|
||||
- 인증/인가
|
||||
#### 2. Stack
|
||||
- Spring Security
|
||||
- Json Web Token (JWT)
|
||||
- Java Persistence Api (JPA)
|
||||
- MariaDB
|
||||
- Docker (for Database, 선택사항)
|
||||
#### 3. Description
|
||||
1. Docker DB 실행 (또는 Local DB 사용)
|
||||
- DB 환경 설정: [server.cnf](poc%2Fadmin%2Fdatabase%2Fconf.d%2Fserver.cnf)
|
||||
- JPA 설정: [application-local.yml](poc%2Fadmin%2Fsrc%2Fmain%2Fresources%2Fapplication-local.yml)
|
||||
- 계정 설정: [docker-compose.yml](poc%2Fadmin%2Fdatabase%2Fdocker-compose.yml)
|
||||
- 초기 데이터: [data.sql](poc%2Fadmin%2Fsrc%2Fmain%2Fresources%2Fdata.sql)
|
||||
```shell
|
||||
cd [경로]\bp\poc\admin\database
|
||||
|
||||
# DB 실행
|
||||
docker compose up -d # detach mode
|
||||
|
||||
# DB 종료
|
||||
docker compose -v down # 종료 (-v 옵션 줄 경우 volume 삭제)
|
||||
```
|
||||
2. 프로젝트 실행
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
HELP.md
|
||||
.gradle
|
||||
build/
|
||||
!gradle/wrapper/gradle-wrapper.jar
|
||||
!**/src/main/**/build/
|
||||
!**/src/test/**/build/
|
||||
|
||||
### STS ###
|
||||
.apt_generated
|
||||
.classpath
|
||||
.factorypath
|
||||
.project
|
||||
.settings
|
||||
.springBeans
|
||||
.sts4-cache
|
||||
bin/
|
||||
!**/src/main/**/bin/
|
||||
!**/src/test/**/bin/
|
||||
|
||||
### IntelliJ IDEA ###
|
||||
.idea
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
out/
|
||||
!**/src/main/**/out/
|
||||
!**/src/test/**/out/
|
||||
|
||||
### NetBeans ###
|
||||
/nbproject/private/
|
||||
/nbbuild/
|
||||
/dist/
|
||||
/nbdist/
|
||||
/.nb-gradle/
|
||||
|
||||
### VS Code ###
|
||||
.vscode/
|
||||
|
|
@ -0,0 +1,55 @@
|
|||
plugins {
|
||||
id 'java'
|
||||
id 'org.springframework.boot' version '3.2.5'
|
||||
id 'io.spring.dependency-management' version '1.1.4'
|
||||
}
|
||||
|
||||
group = 'com.bpgroup.poc'
|
||||
version = '0.0.1-SNAPSHOT'
|
||||
|
||||
java {
|
||||
sourceCompatibility = '17'
|
||||
}
|
||||
|
||||
repositories {
|
||||
mavenCentral()
|
||||
}
|
||||
|
||||
dependencies {
|
||||
implementation 'org.springframework.boot:spring-boot-starter-web'
|
||||
testImplementation 'org.springframework.boot:spring-boot-starter-test'
|
||||
|
||||
// thymeleaf
|
||||
implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
|
||||
implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
|
||||
|
||||
// jpa
|
||||
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
|
||||
|
||||
// security
|
||||
implementation 'org.springframework.boot:spring-boot-starter-security'
|
||||
testImplementation 'org.springframework.security:spring-security-test'
|
||||
|
||||
// dev container
|
||||
developmentOnly 'org.springframework.boot:spring-boot-devtools'
|
||||
|
||||
// db
|
||||
runtimeOnly 'org.mariadb.jdbc:mariadb-java-client'
|
||||
|
||||
// lombok
|
||||
compileOnly 'org.projectlombok:lombok'
|
||||
annotationProcessor 'org.projectlombok:lombok'
|
||||
testCompileOnly 'org.projectlombok:lombok'
|
||||
testAnnotationProcessor 'org.projectlombok:lombok'
|
||||
|
||||
// jwt
|
||||
implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
|
||||
runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
|
||||
runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'
|
||||
|
||||
|
||||
}
|
||||
|
||||
tasks.named('test') {
|
||||
useJUnitPlatform()
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
[mysqld]
|
||||
# utf8mb4: 이모지 사용 가능
|
||||
init_connect=SET collation_connection = utf8mb4_general_ci
|
||||
init_connect=SET NAMES utf8mb4
|
||||
collation-server=utf8mb4_general_ci
|
||||
character-set-server=utf8mb4
|
||||
|
||||
# table 소문자
|
||||
lower_case_table_names=1
|
||||
|
||||
# transaction 독립성 level
|
||||
transaction-isolation=READ-COMMITTED
|
||||
|
||||
# 변경 행 Base64 Encoding 후 binary log 에 저장
|
||||
binlog-format=ROW
|
||||
|
||||
# 1: 테이블 단위로 테이블 스페이스 생성
|
||||
innodb-file-per-table=1
|
||||
skip-innodb-read-only-compressed
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
version: '3.8'
|
||||
services:
|
||||
mariadb:
|
||||
container_name: mariadb
|
||||
image: mariadb:10.11.7
|
||||
restart: always
|
||||
ports:
|
||||
- 3307:3306
|
||||
volumes:
|
||||
- data:/var/lib/mysql # named volume
|
||||
- ./conf.d:/etc/mysql/conf.d # volume mount
|
||||
environment:
|
||||
- TZ=Asia/Seoul
|
||||
- MARIADB_ROOT_PASSWORD=root
|
||||
- MARIADB_DATABASE=login
|
||||
- MARIADB_USER=admin
|
||||
- MARIADB_PASSWORD=1234
|
||||
|
||||
volumes:
|
||||
data:
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
distributionBase=GRADLE_USER_HOME
|
||||
distributionPath=wrapper/dists
|
||||
distributionUrl=https\://services.gradle.org/distributions/gradle-8.7-bin.zip
|
||||
networkTimeout=10000
|
||||
validateDistributionUrl=true
|
||||
zipStoreBase=GRADLE_USER_HOME
|
||||
zipStorePath=wrapper/dists
|
||||
|
|
@ -0,0 +1,249 @@
|
|||
#!/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.
|
||||
#
|
||||
|
||||
##############################################################################
|
||||
#
|
||||
# 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/subprojects/plugins/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 "${APP_HOME:-./}" > /dev/null && pwd -P ) || 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" "$@"
|
||||
|
|
@ -0,0 +1,92 @@
|
|||
@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
|
||||
|
||||
@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
|
||||
|
|
@ -0,0 +1 @@
|
|||
rootProject.name = 'admin'
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.bpgroup.poc.admin;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;
|
||||
|
||||
@EnableJpaAuditing
|
||||
@SpringBootApplication
|
||||
public class AdminApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(AdminApplication.class, args);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.bpgroup.poc.admin.app.login;
|
||||
|
||||
import lombok.Getter;
|
||||
import lombok.ToString;
|
||||
|
||||
@Getter
|
||||
@ToString
|
||||
public class LoginResult {
|
||||
private Long id;
|
||||
private String loginId;
|
||||
private String name;
|
||||
private String email;
|
||||
|
||||
public static LoginResult of(Long id, String loginId, String name, String email) {
|
||||
LoginResult loginResult = new LoginResult();
|
||||
loginResult.id = id;
|
||||
loginResult.loginId = loginId;
|
||||
loginResult.name = name;
|
||||
loginResult.email = email;
|
||||
return loginResult;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,39 @@
|
|||
package com.bpgroup.poc.admin.app.login;
|
||||
|
||||
import com.bpgroup.poc.admin.app.login.exception.AdministratorNotFoundException;
|
||||
import com.bpgroup.poc.admin.app.login.exception.InvalidPasswordException;
|
||||
import com.bpgroup.poc.admin.domain.admin.Administrator;
|
||||
import com.bpgroup.poc.admin.domain.admin.AdministratorRepository;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
@Service
|
||||
@RequiredArgsConstructor
|
||||
public class LoginService {
|
||||
|
||||
private final AdministratorRepository loginRepository;
|
||||
private final PasswordEncoder passwordEncoder;
|
||||
|
||||
public LoginResult login(String loginId, String pwd) throws AdministratorNotFoundException, InvalidPasswordException {
|
||||
|
||||
Optional<Administrator> administrator = loginRepository.findByLoginId(loginId);
|
||||
if (administrator.isEmpty()) {
|
||||
throw new AdministratorNotFoundException(loginId);
|
||||
}
|
||||
|
||||
if (!passwordEncoder.matches(pwd, administrator.get().getPassword())) {
|
||||
throw new InvalidPasswordException(loginId);
|
||||
}
|
||||
|
||||
return LoginResult.of(
|
||||
administrator.get().getId(),
|
||||
administrator.get().getLoginId(),
|
||||
administrator.get().getName(),
|
||||
administrator.get().getLoginId()
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.bpgroup.poc.admin.app.login.exception;
|
||||
|
||||
public class AdministratorNotFoundException extends Exception {
|
||||
public AdministratorNotFoundException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
package com.bpgroup.poc.admin.app.login.exception;
|
||||
|
||||
public class InvalidPasswordException extends Exception {
|
||||
public InvalidPasswordException(String message) {
|
||||
super(message);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,29 @@
|
|||
package com.bpgroup.poc.admin.common;
|
||||
|
||||
import lombok.AccessLevel;
|
||||
import lombok.NoArgsConstructor;
|
||||
import org.slf4j.helpers.MessageFormatter;
|
||||
|
||||
@NoArgsConstructor(access = AccessLevel.PRIVATE)
|
||||
public class FormatHelper {
|
||||
/**
|
||||
* Slf4J 스타일로 문자열 포멧팅을 해주는 함수
|
||||
* <p>
|
||||
* Example:
|
||||
* <pre>
|
||||
* String str1 = FormatHelper.format("{} {} {} {}", "1", 2, 3L, true);
|
||||
* str1.equals("1 2 3 true");
|
||||
*
|
||||
* Object[] params = {"1", 2, 3L, true};
|
||||
* String str2 = FormatHelper.format("{} {} {} {}", params);
|
||||
* str2.equals("1 2 3 true");
|
||||
* </pre>
|
||||
*
|
||||
* @param pattern 파싱되서 params를 적용해 포맷팅 된 패턴
|
||||
* @param params pattern의 {}에 치환 할 값
|
||||
* @return 포맷팅 된 문자열
|
||||
*/
|
||||
public static String format(String pattern, Object... params) {
|
||||
return MessageFormatter.arrayFormat(pattern, params).getMessage();
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.bpgroup.poc.admin.domain;
|
||||
|
||||
import jakarta.persistence.Column;
|
||||
import jakarta.persistence.EntityListeners;
|
||||
import jakarta.persistence.MappedSuperclass;
|
||||
import org.springframework.data.annotation.CreatedDate;
|
||||
import org.springframework.data.annotation.LastModifiedDate;
|
||||
import org.springframework.data.jpa.domain.support.AuditingEntityListener;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
@MappedSuperclass
|
||||
@EntityListeners(AuditingEntityListener.class)
|
||||
public class BaseEntity {
|
||||
@Column(name = "create_date")
|
||||
@CreatedDate
|
||||
protected LocalDateTime createdDate;
|
||||
|
||||
@Column(name = "update_date")
|
||||
@LastModifiedDate
|
||||
protected LocalDateTime updatedDate;
|
||||
}
|
||||
|
|
@ -0,0 +1,28 @@
|
|||
package com.bpgroup.poc.admin.domain.admin;
|
||||
|
||||
import com.bpgroup.poc.admin.domain.BaseEntity;
|
||||
import jakarta.persistence.*;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
@Entity
|
||||
@Table(name = "administrator")
|
||||
public class Administrator extends BaseEntity {
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
private Long id;
|
||||
|
||||
@Column(name = "login_id")
|
||||
private String loginId;
|
||||
|
||||
@Column(name = "password")
|
||||
private String password;
|
||||
|
||||
@Column(name = "email")
|
||||
private String email;
|
||||
|
||||
@Column(name = "name")
|
||||
private String name;
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,9 @@
|
|||
package com.bpgroup.poc.admin.domain.admin;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
public interface AdministratorRepository extends JpaRepository<Administrator, Long> {
|
||||
Optional<Administrator> findByLoginId(String loginId);
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.bpgroup.poc.admin.security;
|
||||
|
||||
import jakarta.servlet.FilterChain;
|
||||
import jakarta.servlet.ServletException;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import org.springframework.security.web.csrf.CsrfToken;
|
||||
import org.springframework.web.filter.OncePerRequestFilter;
|
||||
|
||||
import java.io.IOException;
|
||||
|
||||
public class CsrfCookieFilter extends OncePerRequestFilter {
|
||||
@Override
|
||||
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
|
||||
CsrfToken csrfToken = (CsrfToken) request.getAttribute(CsrfToken.class.getName());
|
||||
if (csrfToken != null) {
|
||||
response.setHeader(csrfToken.getHeaderName(), csrfToken.getToken());
|
||||
}
|
||||
|
||||
filterChain.doFilter(request, response);
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,74 @@
|
|||
package com.bpgroup.poc.admin.security;
|
||||
|
||||
import com.bpgroup.poc.admin.common.FormatHelper;
|
||||
import com.bpgroup.poc.admin.security.authentication.AuthenticationFailException;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.context.annotation.Configuration;
|
||||
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||
import org.springframework.security.web.SecurityFilterChain;
|
||||
import org.springframework.security.web.authentication.www.BasicAuthenticationFilter;
|
||||
import org.springframework.security.web.csrf.CookieCsrfTokenRepository;
|
||||
import org.springframework.security.web.csrf.CsrfTokenRequestAttributeHandler;
|
||||
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
|
||||
|
||||
import java.util.Objects;
|
||||
|
||||
@Configuration
|
||||
public class SecurityConfig {
|
||||
|
||||
private static final String LOGIN_PATH = "/login";
|
||||
private static final String LOGOUT_PATH = "/logout";
|
||||
private static final String ERROR_PATH = "/error";
|
||||
|
||||
@Bean
|
||||
SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {
|
||||
// CSRF 설정
|
||||
CsrfTokenRequestAttributeHandler csrfTokenRequestAttributeHandler = new CsrfTokenRequestAttributeHandler();
|
||||
csrfTokenRequestAttributeHandler.setCsrfRequestAttributeName("_csrf");
|
||||
|
||||
http.csrf(t -> {
|
||||
t.csrfTokenRequestHandler(csrfTokenRequestAttributeHandler)
|
||||
.ignoringRequestMatchers("/common/modal/**")
|
||||
.ignoringRequestMatchers(LOGIN_PATH, LOGOUT_PATH, ERROR_PATH) // CSRF 무시 URL 설정
|
||||
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()); // CSRF 토큰을 쿠키에 저장 및 사용 가능
|
||||
}).addFilterAfter(new CsrfCookieFilter(), BasicAuthenticationFilter.class); // 로그인이 완료된 후 CSRF Filter 를 실행
|
||||
|
||||
// 인증 설정
|
||||
http.authorizeHttpRequests(c -> c
|
||||
.requestMatchers("/css/**", "/images/**", "/js/**").permitAll()
|
||||
.requestMatchers("/common/modal/**").permitAll()
|
||||
.requestMatchers(LOGIN_PATH, LOGOUT_PATH, ERROR_PATH).permitAll()
|
||||
.anyRequest().authenticated());
|
||||
|
||||
http.formLogin(c -> c
|
||||
.loginPage(LOGIN_PATH)
|
||||
.loginProcessingUrl(LOGIN_PATH)
|
||||
.usernameParameter("loginId")
|
||||
.passwordParameter("password")
|
||||
.defaultSuccessUrl("/main")
|
||||
.failureHandler((req, res, ex) -> {
|
||||
if (Objects.requireNonNull(ex) instanceof AuthenticationFailException authenticationFailException) {
|
||||
res.sendRedirect(FormatHelper.format(LOGIN_PATH + "?error={}", authenticationFailException.getReason()));
|
||||
} else {
|
||||
res.sendRedirect(ERROR_PATH);
|
||||
}
|
||||
}));
|
||||
|
||||
http.logout(c -> c
|
||||
.logoutRequestMatcher(new AntPathRequestMatcher(LOGOUT_PATH, "GET"))
|
||||
.logoutSuccessUrl(LOGIN_PATH)
|
||||
);
|
||||
|
||||
return http.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* Bcrypt Version, Bcrypt Strength, Salt String 설정은 생성자를 이용하여 설정 가능
|
||||
*/
|
||||
@Bean
|
||||
public BCryptPasswordEncoder passwordEncoder() {
|
||||
return new BCryptPasswordEncoder();
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
package com.bpgroup.poc.admin.security.authentication;
|
||||
|
||||
import com.bpgroup.poc.admin.app.login.LoginResult;
|
||||
import lombok.Getter;
|
||||
|
||||
@Getter
|
||||
public class AuthenticationDetail {
|
||||
|
||||
private Long id;
|
||||
private String loginId;
|
||||
private String name;
|
||||
private String email;
|
||||
|
||||
public static AuthenticationDetail from(LoginResult result) {
|
||||
AuthenticationDetail authenticationDetail = new AuthenticationDetail();
|
||||
authenticationDetail.id = result.getId();
|
||||
authenticationDetail.loginId = result.getLoginId();
|
||||
authenticationDetail.name = result.getName();
|
||||
authenticationDetail.email = result.getEmail();
|
||||
return authenticationDetail;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,15 @@
|
|||
package com.bpgroup.poc.admin.security.authentication;
|
||||
|
||||
import lombok.Getter;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
|
||||
@Getter
|
||||
public class AuthenticationFailException extends AuthenticationException {
|
||||
|
||||
private final AuthenticationFailReason reason;
|
||||
|
||||
public AuthenticationFailException(String message, AuthenticationFailReason reason) {
|
||||
super(message);
|
||||
this.reason = reason;
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
package com.bpgroup.poc.admin.security.authentication;
|
||||
|
||||
import com.bpgroup.poc.admin.app.login.exception.AdministratorNotFoundException;
|
||||
import com.bpgroup.poc.admin.app.login.exception.InvalidPasswordException;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
@Slf4j
|
||||
public enum AuthenticationFailReason {
|
||||
WRONG_LOGIN_ID,
|
||||
WRONG_PASSWORD,
|
||||
INTERNAL_ERROR;
|
||||
|
||||
public static AuthenticationFailReason from(Exception e) {
|
||||
if (e instanceof AdministratorNotFoundException || e instanceof InvalidPasswordException) {
|
||||
return WRONG_LOGIN_ID;
|
||||
} else {
|
||||
return INTERNAL_ERROR;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,45 @@
|
|||
package com.bpgroup.poc.admin.security.authentication;
|
||||
|
||||
import com.bpgroup.poc.admin.app.login.LoginResult;
|
||||
import com.bpgroup.poc.admin.app.login.LoginService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.security.authentication.AuthenticationProvider;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.AuthenticationException;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class CustomAuthenticationProvider implements AuthenticationProvider {
|
||||
|
||||
private final LoginService loginService;
|
||||
|
||||
@Override
|
||||
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
|
||||
String loginId = (String) authentication.getPrincipal();
|
||||
String password = (String) authentication.getCredentials();
|
||||
|
||||
try {
|
||||
LoginResult loginResult = loginService.login(loginId, password);
|
||||
return buildAuthenticationToken(loginResult);
|
||||
} catch (Exception e) {
|
||||
throw new AuthenticationFailException("로그인에 실패하였습니다.", AuthenticationFailReason.from(e));
|
||||
}
|
||||
}
|
||||
|
||||
private UsernamePasswordAuthenticationToken buildAuthenticationToken(LoginResult result) {
|
||||
UsernamePasswordAuthenticationToken authenticationToken = UsernamePasswordAuthenticationToken.authenticated(
|
||||
result.getLoginId(),
|
||||
null,
|
||||
null
|
||||
);
|
||||
authenticationToken.setDetails(AuthenticationDetail.from(result));
|
||||
return authenticationToken;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean supports(Class<?> authentication) {
|
||||
return (UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication));
|
||||
}
|
||||
}
|
||||
|
|
@ -0,0 +1,41 @@
|
|||
package com.bpgroup.poc.admin.web.fragment;
|
||||
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.ModelAttribute;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestMethod;
|
||||
|
||||
@Slf4j
|
||||
@Controller
|
||||
@RequestMapping("/common/modal")
|
||||
@RequiredArgsConstructor
|
||||
public class ModalController {
|
||||
|
||||
@RequestMapping(method = {RequestMethod.GET, RequestMethod.POST}, value = "alert")
|
||||
public String alert(
|
||||
@ModelAttribute("title") String title,
|
||||
@ModelAttribute("message") String message,
|
||||
Model model
|
||||
) {
|
||||
model.addAttribute("title", title);
|
||||
model.addAttribute("message", message);
|
||||
|
||||
return "fragment/modal/alert-modal";
|
||||
}
|
||||
|
||||
@RequestMapping(method = {RequestMethod.GET, RequestMethod.POST}, value = "confirm")
|
||||
public String confirm(
|
||||
@ModelAttribute("title") String title,
|
||||
@ModelAttribute("message") String message,
|
||||
Model model
|
||||
) {
|
||||
model.addAttribute("title", title);
|
||||
model.addAttribute("message", message);
|
||||
|
||||
return "fragment/modal/confirm-modal";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
package com.bpgroup.poc.admin.web.login;
|
||||
|
||||
import com.bpgroup.poc.admin.security.authentication.AuthenticationFailReason;
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.ui.Model;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
@Controller
|
||||
public class LoginController {
|
||||
|
||||
@GetMapping("/login")
|
||||
public String loginPage(
|
||||
@RequestParam(required = false) AuthenticationFailReason error,
|
||||
Model model
|
||||
) {
|
||||
if (error != null) {
|
||||
model.addAttribute("errorMessage", getMessage(error));
|
||||
}
|
||||
|
||||
return "login/login";
|
||||
}
|
||||
|
||||
private String getMessage(AuthenticationFailReason error) {
|
||||
return switch (error) {
|
||||
case WRONG_LOGIN_ID, WRONG_PASSWORD -> "아이디 또는 비밀번호가 일치하지 않습니다.";
|
||||
case INTERNAL_ERROR -> "서버에 오류가 발생했습니다.";
|
||||
};
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
package com.bpgroup.poc.admin.web.main;
|
||||
|
||||
import org.springframework.stereotype.Controller;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
|
||||
@Controller
|
||||
public class MainController {
|
||||
|
||||
@GetMapping("/main")
|
||||
public String mainPage() {
|
||||
return "main/main";
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
spring:
|
||||
sql:
|
||||
init:
|
||||
mode: always
|
||||
|
||||
datasource:
|
||||
url: jdbc:mariadb://localhost:3307/login
|
||||
username: admin
|
||||
password: 1234
|
||||
driver-class-name: org.mariadb.jdbc.Driver
|
||||
|
||||
jpa:
|
||||
show-sql: true
|
||||
properties:
|
||||
hibernate:
|
||||
format_sql: true
|
||||
hibernate:
|
||||
ddl-auto: create-drop
|
||||
defer-datasource-initialization: true
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
spring:
|
||||
application:
|
||||
name: login
|
||||
|
||||
profiles:
|
||||
default: local
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
INSERT INTO `administrator` (`login_id`, `password`, `email`, `name`, `create_date`, `update_date`)
|
||||
VALUES ('admin', '$2a$10$g6UOrQ/OS8o5r5CJk7C5juVFaItQ62U3EIn8zLPzkFplM3wVLvKZ2', 'admin@admin.com', '홍길동', CURDATE(), CURDATE());
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
|
||||
/* style reset */
|
||||
|
||||
html, body, div, span, applet, object, iframe,
|
||||
h1, h2, h3, h4, h5, h6, p, blockquote, pre, a,
|
||||
abbr, address, big, cite, code, del,
|
||||
dfn, em, img, ins, kbd, q, s, samp, small,
|
||||
strike, strong, sub, sup, tt, var, b, u, i,
|
||||
center, dl, dt, dd, ol, ul, li, fieldset, form,
|
||||
label, legend, table, caption, tbody, tfoot,
|
||||
thead, tr, th, td, article, aside, canvas,
|
||||
details, embed, figure, figcaption, footer,
|
||||
header, hgroup, menu, nav, output, ruby, section,
|
||||
summary, time, mark, audio, video, button, a, input {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
font-size: 100%;
|
||||
font: inherit;
|
||||
vertical-align: baseline;
|
||||
-webkit-tap-highlight-color : rgba(0,0,0,0);
|
||||
line-height: 150%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
/* HTML5 display-role reset for older browsers */
|
||||
|
||||
article, aside, details, figcaption, figure,
|
||||
footer, header, hgroup, menu, nav, section {
|
||||
display: block
|
||||
}
|
||||
body {
|
||||
line-height: 1
|
||||
}
|
||||
ol, ul {
|
||||
list-style: none
|
||||
}
|
||||
blockquote, q {
|
||||
quotes: none
|
||||
}
|
||||
blockquote:before, blockquote:after,
|
||||
q:before, q:after {
|
||||
content: '';
|
||||
content: none
|
||||
}
|
||||
table {
|
||||
border-collapse: collapse;
|
||||
border-spacing: 0
|
||||
}
|
||||
a {
|
||||
text-decoration: none
|
||||
}
|
||||
* {
|
||||
font-family: 'Pretendard', 'sans-serif';
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
|
@ -0,0 +1,802 @@
|
|||
@charset "utf-8";
|
||||
@import url('https://cdn.jsdelivr.net/gh/orioncactus/pretendard/dist/web/static/pretendard.css');
|
||||
@import 'reset.css';
|
||||
:root {
|
||||
|
||||
/* color */
|
||||
--color-black: #000000;
|
||||
--color-white: #ffffff;
|
||||
|
||||
/* font */
|
||||
--fs-30 : 1.875rem;
|
||||
--fs-29 : 1.813rem;
|
||||
--fs-28 : 1.75rem;
|
||||
--fs-27 : 1.688rem;
|
||||
--fs-26 : 1.625rem;
|
||||
--fs-25 : 1.563rem;
|
||||
--fs-24 : 1.5rem;
|
||||
--fs-23 : 1.438rem;
|
||||
--fs-22 : 1.375rem;
|
||||
--fs-21 : 1.3125rem;
|
||||
--fs-20 : 1.25rem;
|
||||
--fs-19 : 1.1875rem;
|
||||
--fs-18 : 1.125rem;
|
||||
--fs-17 : 1.0625rem;
|
||||
--fs-16 : 1rem;
|
||||
--fs-15 : 0.9375rem;
|
||||
--fs-14 : 0.875rem;
|
||||
--fs-13 : 0.8125rem;
|
||||
--fs-12 : 0.75rem;
|
||||
--fs-11 : 0.6875rem;
|
||||
--fs-10 : 0.625rem;
|
||||
|
||||
--fw-800 : 800;
|
||||
--fw-700 : 700;
|
||||
--fw-600 : 600;
|
||||
--fw-500 : 500;
|
||||
--fw-400 : 400;
|
||||
--fw-300 : 300;
|
||||
--fw-200 : 200;
|
||||
--fw-100 : 100;
|
||||
}
|
||||
|
||||
|
||||
/* 공통 */
|
||||
|
||||
.blind {
|
||||
visibility: hidden;
|
||||
overflow: hidden;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 0;
|
||||
height: 0;
|
||||
font-size: 0;
|
||||
line-height: 0
|
||||
}
|
||||
.wid400 {
|
||||
width: 400px;
|
||||
}
|
||||
|
||||
|
||||
/* button */
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
border-radius: 3px;
|
||||
}
|
||||
a:active, button:active {
|
||||
opacity: .75;
|
||||
}
|
||||
button:disabled {
|
||||
color: #999;
|
||||
opacity: 0.5;
|
||||
}
|
||||
.btn_normal {
|
||||
font-size: var(--fs-14);
|
||||
color: #333;
|
||||
height: 36px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #999;
|
||||
background-color: #fff;
|
||||
}
|
||||
.btn_confirm {
|
||||
font-size: var(--fs-14);
|
||||
color: #fff;
|
||||
height: 36px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #a6a6a6;
|
||||
background-color: var(--color-black);
|
||||
}
|
||||
.closeButton {
|
||||
display: none;
|
||||
position: absolute;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
background: transparent;
|
||||
}
|
||||
|
||||
/* layout */
|
||||
|
||||
html, body {
|
||||
height: 100vh;
|
||||
min-height: -webkit-fill-available;
|
||||
min-height: var(--vh-100);
|
||||
font-size: 16px;
|
||||
color: #333;
|
||||
line-height: 150%;
|
||||
font-family: 'Pretendard', 'sans-serif';
|
||||
}
|
||||
header {
|
||||
/*position: fixed;
|
||||
left: 0;
|
||||
top: 0;*/
|
||||
width: 100%;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid #c6c6c6;
|
||||
overflow: hidden;
|
||||
background-color: #ffffff;
|
||||
z-index: 100;
|
||||
}
|
||||
header a.nav_menu {
|
||||
display: none;
|
||||
}
|
||||
.wrapper {
|
||||
margin: 0 auto;
|
||||
height: 100vh;
|
||||
}
|
||||
.container {
|
||||
padding: 10px 30px 30px 30px;
|
||||
margin: 0 auto;
|
||||
min-width: 320px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
.container h2 {
|
||||
font-size: var(--fs-30);
|
||||
font-weight: var(--fw-800);
|
||||
margin: 15px 0 10px 0;
|
||||
}
|
||||
.breadcrumb a {
|
||||
font-size: 15px;
|
||||
color: #999;
|
||||
}
|
||||
.breadcrumb a::after {
|
||||
position: relative;
|
||||
content: ">";
|
||||
padding-left: 10px;
|
||||
padding-right: 5px;
|
||||
font-size: 10px;
|
||||
top: -2px
|
||||
}
|
||||
.breadcrumb a:last-child {
|
||||
color: #333;
|
||||
}
|
||||
.breadcrumb a:last-child::after {
|
||||
content: " ";
|
||||
}
|
||||
.option_wrap {
|
||||
position: relative;
|
||||
padding: 15px 12px;
|
||||
border: 1px solid #d6d6d6;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.option_wrap ul {
|
||||
overflow: hidden;
|
||||
}
|
||||
.option_wrap ul li {
|
||||
display: inline-block;
|
||||
padding: 5px 10px;
|
||||
}
|
||||
.option_wrap ul li span {
|
||||
display: inline-block;
|
||||
padding: 3px 0;
|
||||
}
|
||||
.position_right {
|
||||
position: absolute;
|
||||
right: 12px;
|
||||
bottom: 12px;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* header */
|
||||
header h1 {
|
||||
float: left;
|
||||
font-size: var(--fs-26);
|
||||
font-weight: var(--fw-800)
|
||||
}
|
||||
.menu_right {
|
||||
float: right;
|
||||
font-size: var(--fs-14);
|
||||
font-weight: var(--fw-400);
|
||||
color: #666;
|
||||
margin-top: 10px;
|
||||
}
|
||||
.menu_right a {
|
||||
display: inline-block;
|
||||
padding: 0 10px;
|
||||
}
|
||||
|
||||
|
||||
/* input */
|
||||
|
||||
[tabindex='-1'], [tabindex='0'] {
|
||||
outline: none;
|
||||
}
|
||||
input {
|
||||
-webkit-appearance : none;
|
||||
-moz-appearance:none;
|
||||
appearance:none;
|
||||
height: 36px;
|
||||
border: 1px solid #c6c6c6;
|
||||
padding: 0 10px;
|
||||
box-sizing: border-box;
|
||||
outline: none;
|
||||
font-size: var(--fs-15);
|
||||
color: #333;
|
||||
border-radius: 3px;
|
||||
}
|
||||
input:focus, input:active, input:hover {
|
||||
border: 1px solid #333;
|
||||
outline: none;
|
||||
}
|
||||
input[type=number] {
|
||||
-moz-appearance: textfield;
|
||||
}
|
||||
input[type=number]::-webkit-inner-spin-button,
|
||||
input[type=number]::-webkit-outer-spin-button {
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
input::-ms-clear {
|
||||
display: none;
|
||||
}
|
||||
input:invalid {
|
||||
-webkit-box-shadow: none;
|
||||
-moz-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
:-moz-submit-invalid {
|
||||
-moz-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
:-moz-ui-invalid {
|
||||
-moz-box-shadow: none;
|
||||
box-shadow: none;
|
||||
}
|
||||
input:focus-within {
|
||||
border: 1px solid #333;
|
||||
}
|
||||
input:disabled {
|
||||
background: #f5f5f5;
|
||||
}
|
||||
input:read-only {
|
||||
color: var(--color-black);
|
||||
background: #f5f5f5;
|
||||
}
|
||||
input::placeholder {
|
||||
color: #c6c6c6;
|
||||
font-weight: var(--fw-400);
|
||||
}
|
||||
*::placeholder {
|
||||
font-weight: var(--fw-300);
|
||||
}
|
||||
.date_select input {
|
||||
font-size: var(--fs-15);
|
||||
}
|
||||
|
||||
|
||||
/* checkbox */
|
||||
|
||||
.check_box {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
font-size: var(--fs-15);
|
||||
}
|
||||
.check_box label {
|
||||
cursor: pointer;
|
||||
}
|
||||
input[type="checkbox"] + label {
|
||||
position: relative;
|
||||
top: 5px;
|
||||
display: block;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
background: url(../images/chk_off.svg) no-repeat 0 0 / contain;
|
||||
}
|
||||
input[type='checkbox']:checked + label {
|
||||
background: url(../images/chk_on.svg) no-repeat 0 0 / contain;
|
||||
}
|
||||
input[type="checkbox"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
/* radiobox */
|
||||
|
||||
.radio_box {
|
||||
position: relative;
|
||||
padding-left: 33px;
|
||||
padding-right: 30px;
|
||||
font-size: var(--fs-16);
|
||||
margin-left: 12px;
|
||||
}
|
||||
.radio_box label {
|
||||
position: absolute;
|
||||
left: 3px;
|
||||
top: 2px;
|
||||
cursor: pointer;
|
||||
}
|
||||
input[type="radio"] + label {
|
||||
position: absolute;
|
||||
display: block;
|
||||
height: 20px;
|
||||
padding-left: 26px;
|
||||
background: url(../images/radio_off.svg) no-repeat 0 0 / contain;
|
||||
}
|
||||
input[type='radio']:checked + label {
|
||||
background: url(../images/radio_on.svg) no-repeat 0 0 / contain;
|
||||
}
|
||||
input[type="radio"] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
|
||||
/* select */
|
||||
|
||||
select {
|
||||
-webkit-appearance: none;
|
||||
-moz-appearance:none;
|
||||
appearance:none;
|
||||
height: 36px;
|
||||
padding: 0 25px 0 10px;
|
||||
border: 0;
|
||||
color: #333;
|
||||
font-size: var(--fs-14);
|
||||
font-weight: var(--fw-400);
|
||||
border-radius: 3px;
|
||||
-moz-border-radius: 3px;
|
||||
outline: 0;
|
||||
background: url('../images/ico_down_black.svg') no-repeat center right 8px, var(--color-white);
|
||||
background-size: 10px auto;
|
||||
transition: all .2s;
|
||||
border: 1px solid #c6c6c6;
|
||||
font-family: 'Pretendard', 'sans-serif';
|
||||
}
|
||||
select:disabled {
|
||||
opacity: 0.6;
|
||||
}
|
||||
select::-ms-expand {
|
||||
display:none;
|
||||
}
|
||||
select:focus-within, select:active {
|
||||
border: 1px solid #333;
|
||||
}
|
||||
select:disabled {
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
select:disabled + span {
|
||||
opacity: 0.3;
|
||||
}
|
||||
|
||||
|
||||
/* flex layout */
|
||||
|
||||
.flexbox {
|
||||
position: relative;
|
||||
display: -webkit-box;
|
||||
display: -moz-box;
|
||||
display: -ms-flexbox;
|
||||
display: flex;
|
||||
-webkit-box-align: center;
|
||||
-moz-box-align: center;
|
||||
-ms-flex-align: center;
|
||||
flex-direction: row;
|
||||
align-items: stretch
|
||||
}
|
||||
.flexitem {
|
||||
flex: 1;
|
||||
-webkit-box-flex: 1;
|
||||
-moz-box-flex: 1;
|
||||
-ms-flex: 1;
|
||||
-webkit-box-orient: horizontal;
|
||||
-webkit-box-direction: normal;
|
||||
-moz-box-orient: horizontal;
|
||||
-ms-flex-wrap: wrap;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
|
||||
/* navigation */
|
||||
|
||||
.menu_wrap {
|
||||
width: 100%;
|
||||
max-width: 250px;
|
||||
min-width: 200px;
|
||||
height: 100%;
|
||||
overflow: auto;
|
||||
background-color: #333;
|
||||
padding: 20px 0;
|
||||
}
|
||||
.menu_wrap h2 {
|
||||
display: block;
|
||||
font-size: var(--fs-20);
|
||||
font-weight: var(--fw-800);
|
||||
color: #fff;
|
||||
padding: 0 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.menu_wrap_mobile {
|
||||
display: none;
|
||||
}
|
||||
.menu_wrap_mobile h2 {
|
||||
display: block;
|
||||
font-size: var(--fs-20);
|
||||
font-weight: var(--fw-800);
|
||||
color: #fff;
|
||||
padding: 0 20px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.nav_tit {
|
||||
position: relative;
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
font-size: var(--fs-15);
|
||||
color: #fff;
|
||||
background: url('../images/ico_down.svg') no-repeat right 20px center;
|
||||
background-size: 10px;
|
||||
transition: background 0.1s ease;
|
||||
padding: 8px 20px;
|
||||
}
|
||||
.nav_tit.active {
|
||||
background: url('../images/ico_up.svg') no-repeat right 20px center, #222;
|
||||
background-size: 10px;
|
||||
}
|
||||
.nav_tit.active + .nav_section {
|
||||
display: block;
|
||||
}
|
||||
.nav_section {
|
||||
display: none;
|
||||
list-style: none;
|
||||
padding: 10px 20px;
|
||||
}
|
||||
.nav_section li {
|
||||
padding: 2px 10px;
|
||||
}
|
||||
.nav_section li a {
|
||||
display: block;
|
||||
color: #fff;
|
||||
font-size: var(--fs-14);
|
||||
}
|
||||
|
||||
|
||||
/* table */
|
||||
.table_wrap {
|
||||
position: relative;
|
||||
}
|
||||
.table_wrap.mt-20 {
|
||||
margin-top: 20px;
|
||||
}
|
||||
.table_wrap h4 {
|
||||
display: inline-block;
|
||||
font-size: 22px;
|
||||
font-weight: var(--fw-800);
|
||||
height: 36px;
|
||||
line-height: 36px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.tb_right {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 0;
|
||||
}
|
||||
table {
|
||||
width: 100%;
|
||||
border-top: 1px solid #333; table-layout:fixed;
|
||||
}
|
||||
table caption{font-size:0; text-indent:-9999px;}
|
||||
th, td {
|
||||
text-align: center;
|
||||
padding: 12px 8px;
|
||||
color: #333;
|
||||
}
|
||||
th {
|
||||
font-size: var(--fs-14);
|
||||
font-weight: var(--fw-700);
|
||||
background-color: #f9f9f9;
|
||||
border-bottom: 1px solid #c6c6c6
|
||||
}
|
||||
td {
|
||||
font-size: var(--fs-15);
|
||||
font-weight: var(--fw-400);
|
||||
border-bottom: 1px solid #e6e6e6;
|
||||
}
|
||||
|
||||
|
||||
|
||||
/* modal */
|
||||
|
||||
.modal {
|
||||
display: none;
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
z-index: 1000;
|
||||
}
|
||||
.modal-content {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
max-width: 320px;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: white;
|
||||
padding: 16px;
|
||||
}
|
||||
.modal-header {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
border-bottom: 1px solid #d6d6d6;
|
||||
}
|
||||
.modal-header h2 {
|
||||
height: 50px;
|
||||
line-height: 50px;
|
||||
font-size: 20px;
|
||||
margin: 0;
|
||||
padding: 0 15px;
|
||||
|
||||
}
|
||||
.close {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
font-size: 20px;
|
||||
cursor: pointer;
|
||||
}
|
||||
.modal-body {
|
||||
margin-top: 50px;
|
||||
font-size: var(--fs-15);
|
||||
padding: 10px 0;
|
||||
}
|
||||
.modal-body .col-box {
|
||||
padding-bottom: 16px;
|
||||
}
|
||||
p.col-box label {
|
||||
display: block;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
p.col-box input {
|
||||
width: 100%;
|
||||
}
|
||||
.btn_both {
|
||||
display: flex;
|
||||
}
|
||||
.btn_both button {
|
||||
width: 50%;
|
||||
flex: 1;
|
||||
}
|
||||
.btn_both button:nth-child(1) {
|
||||
margin-right: 3px;
|
||||
}
|
||||
.btn_both button:nth-child(2) {
|
||||
margin-left: 3px;
|
||||
}
|
||||
|
||||
|
||||
/* login */
|
||||
|
||||
.login_wrap {
|
||||
position: fixed;
|
||||
top: 0;
|
||||
left: 0;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: rgba(0, 0, 0, 0.7);
|
||||
z-index: 1;
|
||||
}
|
||||
.login_box {
|
||||
position: absolute;
|
||||
top: 50%;
|
||||
left: 50%;
|
||||
display: inline-block;
|
||||
width: 100%;
|
||||
max-width: 420px;
|
||||
transform: translate(-50%, -50%);
|
||||
background-color: white;
|
||||
padding: 30px;
|
||||
border-radius: 6px;
|
||||
}
|
||||
.login_box h1 {
|
||||
font-size: var(--fs-26);
|
||||
font-weight: var(--fw-800);
|
||||
margin-top: 5px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
.login_box p {
|
||||
padding: 6px 0;
|
||||
}
|
||||
input.login_in {
|
||||
width: 40% !important;
|
||||
margin-right: 5px;
|
||||
}
|
||||
.login_box label {
|
||||
display: inline-block;
|
||||
min-width: 75px;
|
||||
text-align: right;
|
||||
padding-right: 10px;
|
||||
}
|
||||
.login_box input {
|
||||
width: 76%;
|
||||
}
|
||||
.btn_login {
|
||||
width: 100% !important;
|
||||
margin-top: 30px;
|
||||
height: 44px;
|
||||
font-size: var(--fs-15);
|
||||
font-weight: var(--fw-800);
|
||||
}
|
||||
.login_subtit {
|
||||
position: relative;
|
||||
top: -20px;
|
||||
color: #999;
|
||||
}
|
||||
|
||||
|
||||
/* main */
|
||||
|
||||
.graph {
|
||||
margin-top: 15px;
|
||||
padding: 25px;
|
||||
background-color: #f9f9f9;
|
||||
}
|
||||
.graph canvas {
|
||||
max-height: 200px;
|
||||
}
|
||||
.main_count {
|
||||
padding: 30px 0;
|
||||
}
|
||||
.count_wrap {
|
||||
overflow: hidden;
|
||||
width: 100%;
|
||||
display: flex;
|
||||
}
|
||||
.count_wrap > li {
|
||||
flex: 1;
|
||||
font-size: var(--fs-16);
|
||||
padding: 25px;
|
||||
border: 1px solid #d6d6d6;
|
||||
border-radius: 6px;
|
||||
margin-right: 30px;
|
||||
}
|
||||
.count_wrap > li:last-child {
|
||||
margin-right: 0;
|
||||
}
|
||||
.count_wrap > li strong {
|
||||
font-weight: var(--fw-700);
|
||||
font-size: var(--fs-15);
|
||||
}
|
||||
.count_wrap > li p {
|
||||
position: relative;
|
||||
padding-left: 10px;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.count_wrap > li p em {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
.count_wrap h5 {
|
||||
font-size: var(--fs-18);
|
||||
font-weight: var(--fw-800);
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
.date {
|
||||
position: relative;
|
||||
top: -1px;
|
||||
left: 15px;
|
||||
display: inline-block;
|
||||
font-size: var(--fs-15);
|
||||
}
|
||||
|
||||
/* 모바일 */
|
||||
@media (max-width: 767px){
|
||||
header {
|
||||
padding: 10px 16px;
|
||||
}
|
||||
header h1 {
|
||||
font-size: 20px;
|
||||
}
|
||||
header a.nav_menu {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 15px;
|
||||
right: 15px;
|
||||
}
|
||||
.container {
|
||||
padding: 16px;
|
||||
height: 100%;
|
||||
overflow: inherit;
|
||||
}
|
||||
.container h2 {
|
||||
margin: 0 0 10px 0;
|
||||
}
|
||||
.menu_wrap, .menu_right, .breadcrumb {
|
||||
display: none;
|
||||
}
|
||||
.menu_wrap.active {
|
||||
position: fixed !important;
|
||||
display: block;
|
||||
top: 0 !important;
|
||||
left: 0 !important;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
max-width: inherit;
|
||||
background-color: #333;
|
||||
z-index: 1000;
|
||||
}
|
||||
.tb_right {
|
||||
position: relative;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
.closeButton {
|
||||
display: block;
|
||||
}
|
||||
.option_wrap ul li {
|
||||
width: 100%;
|
||||
}
|
||||
.position_right {
|
||||
position: inherit;
|
||||
}
|
||||
.wid400 {
|
||||
width: 100%;
|
||||
}
|
||||
.tb_wrapper {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.tb_sty01 {
|
||||
width: 1000px;
|
||||
}
|
||||
th, td {
|
||||
font-size: var(--fs-15);
|
||||
}
|
||||
.count_wrap {
|
||||
display: block;
|
||||
}
|
||||
.count_wrap li {
|
||||
flex: none;
|
||||
padding: 16px;
|
||||
margin-right: 0;
|
||||
margin-bottom: 16px;
|
||||
}
|
||||
.graph {
|
||||
padding: 16px;
|
||||
}
|
||||
.login_box label {
|
||||
display: block;
|
||||
text-align: left;
|
||||
margin-bottom: 5px;
|
||||
}
|
||||
.login_box {
|
||||
width: 92%;
|
||||
}
|
||||
.login_box input {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.login_box input.login_in {
|
||||
width: 60%;
|
||||
}
|
||||
|
||||
/* modal */
|
||||
.modal-content {
|
||||
width: 92%;
|
||||
max-width: inherit;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
/* 테블릿 세로 */
|
||||
@media (min-width: 768px) and (max-width: 991px) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* 테블릿 가로 */
|
||||
|
||||
@media (min-width: 992px) and (max-width: 1199px) {
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* 데스크탑 일반 */
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,136 @@
|
|||
|
||||
/*20240220 김보라 퍼블수정*/
|
||||
html, body {height:calc(100vh - 72px);}
|
||||
.wrapper {height:100%}
|
||||
#dayKo {margin-left: 20px;}
|
||||
.terminal_wrap {margin:20px 0 10px 0; padding:15px; background:#f5f5f5}
|
||||
.terminal_wrap .terminal_top{overflow: hidden;}
|
||||
.terminal_wrap .terminal_top .terminal_btn {float:right;}
|
||||
.terminal_wrap .terminal_list_wrap {border-top:1px solid #ddd; margin: 10px 0 0 0; padding:10px}
|
||||
.terminal_list_wrap .terminal_list_in {padding: 5px 0;}
|
||||
.terminal_list_wrap .terminal_list_in .checkbox-label {line-height: normal; padding-left: 25px; display: inline;}
|
||||
.terminal_list_wrap .terminal_list_in > p {text-align: center;}
|
||||
|
||||
.table_radio_wrap {vertical-align: middle;}
|
||||
.table_radio_wrap .radio_box {display: inline-block !important; padding-right:10px !important; padding-left: 0; margin-left:0; }
|
||||
.table_radio_wrap .radio_box input[type="radio"] + label {position:static;}
|
||||
|
||||
.btn_wrap {position: relative; text-align: right; margin-top:10px;}
|
||||
.table_select_box, .table_date_box {width: 225px;}
|
||||
|
||||
.tb_sty01.tb_sty02 {border-top:1px solid #999}
|
||||
|
||||
#batchRegisterTerminalModal .modal-content {max-width: 530px !important}
|
||||
#depositDetailModal .modal-content, #withdrawDetailModal .modal-content,
|
||||
#transferDetailModal .modal-content, #transferHistoryModal .modal-content {max-width: 750px !important}
|
||||
|
||||
.btn_normal.excelbtn {
|
||||
height: 36px;
|
||||
display: inline-block;
|
||||
vertical-align: top;
|
||||
line-height: 36px;
|
||||
}
|
||||
.tb_wrapper.tb_wrapper_x_scroll {overflow-x: auto;}
|
||||
.tb_wrapper.tb_wrapper_terminal {max-height: 170px; overflow-y: auto; overflow-x: auto;}
|
||||
.wd_auto .tbl_bert_m > th{vertical-align: middle;}
|
||||
.wd_auto input {width:100% !important;}
|
||||
|
||||
.font_bold{font-weight: bold;}
|
||||
.tooltip_prt {position:relative;}
|
||||
.tooltip_chd {
|
||||
visibility: hidden;
|
||||
background-color: black;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
border-radius: 6px;
|
||||
padding: 5px;
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
|
||||
width: max-content;
|
||||
bottom: -5%;
|
||||
left: 50%;
|
||||
transform: translate(-50%, 50%);
|
||||
}
|
||||
.tooltip_prt:hover .tooltip_chd {
|
||||
visibility: visible;
|
||||
}
|
||||
table colgroup > col { min-width:max-content !important}
|
||||
|
||||
table td > p, table td > a {
|
||||
display:block;
|
||||
overflow:hidden;
|
||||
white-space: nowrap;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.mt-05 {
|
||||
margin-top: 5px;
|
||||
}
|
||||
.mt-10 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.tb_wrapper {padding-bottom:35px}
|
||||
/*20240226 김현종 퍼블 수정*/
|
||||
.clickable_text a:link, .clickable_text a:visited {
|
||||
color: blue;
|
||||
text-decoration: underline;
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
/*20240229 김현종 퍼블 수정*/
|
||||
.title_text a {
|
||||
color: black; /* 원하는 색상으로 설정 */
|
||||
text-decoration: none; /* 밑줄 제거 */
|
||||
}
|
||||
|
||||
.title_text a:visited {
|
||||
color: black; /* 방문한 링크에 대해서도 동일한 색상 유지 */
|
||||
}
|
||||
|
||||
|
||||
/* 모바일 */
|
||||
@media (max-width: 767px){
|
||||
.mb_w50 {overflow: hidden; margin-left:-5px;}
|
||||
.mb_w50 > span {width:50%; float:left; padding:0 3px !important; box-sizing: border-box;}
|
||||
.mb_w50 > span select {width:100%}
|
||||
|
||||
table td > p, table td > a {
|
||||
overflow: inherit;
|
||||
white-space: inherit;
|
||||
word-break: break-all;
|
||||
}
|
||||
|
||||
.tb_w_auto {width:130px !important;}
|
||||
.table_wrap h4 { height: auto !important}
|
||||
.table_wrap2 .tb_right {position:absolute;}
|
||||
}
|
||||
|
||||
/* 테블릿 세로 */
|
||||
@media (min-width: 768px) and (max-width: 991px) {
|
||||
.menu_wrap {
|
||||
max-width:200px
|
||||
}
|
||||
|
||||
.tb_w_auto {width:100px}
|
||||
}
|
||||
|
||||
|
||||
/* 테블릿 가로 */
|
||||
|
||||
@media (min-width: 992px) and (max-width: 1199px) {
|
||||
.menu_wrap {
|
||||
max-width:200px
|
||||
}
|
||||
|
||||
.tb_w_auto {width:130px}
|
||||
|
||||
}
|
||||
|
||||
|
||||
/* 데스크탑 일반 */
|
||||
|
||||
@media (min-width: 1200px) {
|
||||
|
||||
}
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
|
||||
<g id="chk_off" fill="#fff" stroke="#007fa8" stroke-width="1">
|
||||
<rect width="20" height="20" stroke="none"/>
|
||||
<rect x="0.5" y="0.5" width="19" height="19" fill="none"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 275 B |
|
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" viewBox="0 0 20 20">
|
||||
<g id="chk_on" transform="translate(-1599 -1046)">
|
||||
<rect id="사각형_1369" data-name="사각형 1369" width="20" height="20" transform="translate(1599 1046)" fill="#007fa8"/>
|
||||
<path id="패스_412" data-name="패스 412" d="M2915.522-3564.191l-5.052-5.151,1.53-1.5,3.361,3.427,6.459-8.055,1.672,1.34Z" transform="translate(-1307.941 4625.084)" fill="#fff"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 467 B |
|
After Width: | Height: | Size: 4.2 KiB |
|
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="15.603" height="15.603" viewBox="0 0 15.603 15.603">
|
||||
<g id="ico_close" transform="translate(-324.162 -20.162)">
|
||||
<line id="선_19" data-name="선 19" x2="14.189" y2="14.189" transform="translate(324.869 20.869)" fill="none" stroke="#fff" stroke-width="2"/>
|
||||
<line id="선_20" data-name="선 20" x1="14.189" y2="14.189" transform="translate(324.869 20.869)" fill="none" stroke="#fff" stroke-width="2"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 471 B |
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="8" viewBox="0 0 13 8">
|
||||
<path id="패스_1291" data-name="패스 1291" d="M1321.314,229.728l-5.2-5.333-1.3,1.334,6.5,6.666h0l6.5-6.666-1.3-1.334Z" transform="translate(-1314.815 -224.395)" fill="#fff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 270 B |
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="8" viewBox="0 0 13 8">
|
||||
<path id="패스_1291" data-name="패스 1291" d="M1321.314,229.728l-5.2-5.333-1.3,1.334,6.5,6.666h0l6.5-6.666-1.3-1.334Z" transform="translate(-1314.815 -224.395)" fill="#333"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 270 B |
|
|
@ -0,0 +1,7 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="20" height="12" viewBox="0 0 20 12">
|
||||
<g id="그룹_2754" data-name="그룹 2754" transform="translate(-320 -21)">
|
||||
<rect id="사각형_1995" data-name="사각형 1995" width="20" height="2" transform="translate(320 21)" fill="#333"/>
|
||||
<rect id="사각형_1996" data-name="사각형 1996" width="20" height="2" transform="translate(320 31)" fill="#333"/>
|
||||
<rect id="사각형_1997" data-name="사각형 1997" width="20" height="2" transform="translate(320 26)" fill="#333"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 546 B |
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="13" height="8" viewBox="0 0 13 8">
|
||||
<path id="패스_330" data-name="패스 330" d="M1321.314,227.062l-5.2,5.333-1.3-1.334,6.5-6.666h0l6.5,6.666-1.3,1.334Z" transform="translate(-1314.815 -224.395)" fill="#fff"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 268 B |
|
|
@ -0,0 +1,7 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
|
||||
<g id="radio_off" transform="translate(0 -1)">
|
||||
<rect id="사각형_11" data-name="사각형 11" width="18" height="18" rx="9" transform="translate(0 1)" fill="#fff"/>
|
||||
<path id="사각형_11_-_윤곽선" data-name="사각형 11 - 윤곽선" d="M9,2a7,7,0,1,0,7,7A7.008,7.008,0,0,0,9,2M9,0A9,9,0,1,1,0,9,9,9,0,0,1,9,0Z" transform="translate(0 1)" fill="#007fa8"/>
|
||||
<circle id="타원_7" data-name="타원 7" cx="4" cy="4" r="4" transform="translate(5 6)" fill="#fff"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 578 B |
|
|
@ -0,0 +1,7 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="18" height="18" viewBox="0 0 18 18">
|
||||
<g id="radio_on" transform="translate(0 -1)">
|
||||
<rect id="사각형_11" data-name="사각형 11" width="18" height="18" rx="9" transform="translate(0 1)" fill="#fff"/>
|
||||
<path id="사각형_11_-_윤곽선" data-name="사각형 11 - 윤곽선" d="M9,2a7,7,0,1,0,7,7A7.008,7.008,0,0,0,9,2M9,0A9,9,0,1,1,0,9,9,9,0,0,1,9,0Z" transform="translate(0 1)" fill="#007fa8"/>
|
||||
<circle id="타원_7" data-name="타원 7" cx="4" cy="4" r="4" transform="translate(5 6)" fill="#007fa8"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 580 B |
|
|
@ -0,0 +1,37 @@
|
|||
const EventRouter = {
|
||||
_eventRouter: $({}),
|
||||
|
||||
/**
|
||||
* 이벤트 발생
|
||||
*
|
||||
* @param {string} eventName
|
||||
* @param {*=} data
|
||||
*/
|
||||
trigger(eventName, data) {
|
||||
this._eventRouter.trigger(eventName, data);
|
||||
},
|
||||
|
||||
/**
|
||||
* 이벤트 등록
|
||||
*
|
||||
* @param {string} eventName
|
||||
* @param {function(*)} func
|
||||
*/
|
||||
register(eventName, func) {
|
||||
if (!eventName) {
|
||||
console.warn('eventName이 비어있습니다.');
|
||||
return;
|
||||
}
|
||||
|
||||
this._eventRouter.off(eventName);
|
||||
this._eventRouter.on(eventName, func);
|
||||
},
|
||||
|
||||
/**
|
||||
* 현재 등록된 이벤트 조회 (디버깅용)
|
||||
*/
|
||||
logCurrentRegistered() {
|
||||
const events = $._data(this._eventRouter[0], "events");
|
||||
console.log(events);
|
||||
}
|
||||
};
|
||||
|
|
@ -0,0 +1,73 @@
|
|||
// mobile 100vh err issue 대응
|
||||
|
||||
const setVh = () => {
|
||||
document.documentElement.style.setProperty('--vh', `${window.innerHeight}px`)
|
||||
};
|
||||
window.addEventListener('resize', setVh);
|
||||
setVh();
|
||||
|
||||
$(document).on('touchstart', function () {
|
||||
});
|
||||
|
||||
|
||||
// 콘텐츠영역 자동 높이 조절
|
||||
$(document).ready(function () {
|
||||
// var headerHeight = $('header').outerHeight();
|
||||
// $('.wrapper').css('padding-top', headerHeight + 'px');
|
||||
});
|
||||
|
||||
$(document).ready(function () {
|
||||
setupAccordion();
|
||||
|
||||
// navigation 모바일 처리
|
||||
$('#toggleLink').click(function () {
|
||||
toggleMenu('.menu_wrap');
|
||||
});
|
||||
|
||||
$('.closeButton').click(function () {
|
||||
closeMenu('.menu_wrap');
|
||||
});
|
||||
|
||||
//modal
|
||||
$("#openModalBtn").click(function () {
|
||||
openModal();
|
||||
});
|
||||
|
||||
$(".modal .close, .modal").click(function (e) {
|
||||
if (e.target !== this) return; // 모달 내부를 클릭한 경우에는 닫히지 않도록 처리
|
||||
closeModal();
|
||||
});
|
||||
});
|
||||
|
||||
|
||||
// 아코디언
|
||||
function setupAccordion() {
|
||||
$('.nav_tit').on('click', function () {
|
||||
$(this).next('.nav_section').slideToggle(150);
|
||||
$(this).toggleClass('active');
|
||||
$('.nav_section').not($(this).next('.nav_section')).slideUp(150);
|
||||
$('.nav_tit').not($(this)).removeClass('active');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// navigation 모바일 처리
|
||||
function toggleMenu(selector) {
|
||||
$(selector).toggleClass('active');
|
||||
}
|
||||
|
||||
function closeMenu(selector) {
|
||||
$(selector).removeClass('active');
|
||||
}
|
||||
|
||||
|
||||
//modal 열기 닫기
|
||||
function openModal() {
|
||||
$(".modal").fadeIn(100);
|
||||
$("body").css("overflow", "hidden");
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
$(".modal").fadeOut(100);
|
||||
$("body").css("overflow", "auto");
|
||||
}
|
||||
|
|
@ -0,0 +1,182 @@
|
|||
const PageHelper = {
|
||||
|
||||
/**
|
||||
* 단순 확인 모달을 띄운다.
|
||||
*
|
||||
* @param message
|
||||
* @param confirmFunction
|
||||
*/
|
||||
showSimpleConfirmModal(message, confirmFunction) {
|
||||
this.showConfirmModal({
|
||||
title: '확인',
|
||||
message: message,
|
||||
}, {
|
||||
confirm: confirmFunction,
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* Confirm 모달을 띄운다.
|
||||
*
|
||||
* @param {Object} params
|
||||
* @param {string} params.title
|
||||
* @param {string} params.message
|
||||
* @param {Object=} callbacks
|
||||
* @param {function()=} callbacks.confirm
|
||||
* @param {function()=} callbacks.cancel
|
||||
*/
|
||||
showConfirmModal(params, callbacks) {
|
||||
if (!callbacks) {
|
||||
callbacks = {};
|
||||
}
|
||||
|
||||
this.loadAndApplyPage({
|
||||
url: `/common/modal/confirm`,
|
||||
method: 'POST',
|
||||
data: `title=${params.title}&message=${params.message}`,
|
||||
contentSelector: 'div[data-confirm-modal="body"]',
|
||||
appendToSelector: 'body'
|
||||
}, {
|
||||
success: function () {
|
||||
EventRouter.register('clickConfirmModalConfirmButton', callbacks.confirm);
|
||||
EventRouter.register('clickConfirmModalCancelButton', callbacks.cancel);
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 단순 오류 모달을 띄운다.
|
||||
*
|
||||
* @param message
|
||||
* @param cancelFunction
|
||||
*/
|
||||
showErrorModal(message, cancelFunction) {
|
||||
if (!cancelFunction) {
|
||||
cancelFunction = () => {}
|
||||
}
|
||||
|
||||
this.showAlertModal({
|
||||
title: '오류',
|
||||
message: message,
|
||||
}, {
|
||||
cancel: cancelFunction
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* 단순 완료 모달을 띄운다.
|
||||
*
|
||||
* @param message
|
||||
* @param cancelFunction
|
||||
*/
|
||||
showFinishModal(message, cancelFunction) {
|
||||
if (!cancelFunction) {
|
||||
cancelFunction = () => {}
|
||||
}
|
||||
|
||||
this.showAlertModal({
|
||||
title: '완료',
|
||||
message: message,
|
||||
}, {
|
||||
cancel: cancelFunction
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* alert 모달을 띄운다.
|
||||
*
|
||||
* @param {Object} params
|
||||
* @param {string} params.title
|
||||
* @param {string} params.message
|
||||
* @param callbacks=
|
||||
* @param {function()=} callbacks.cancel
|
||||
*/
|
||||
showAlertModal(params, callbacks) {
|
||||
if (!callbacks) {
|
||||
callbacks = {};
|
||||
}
|
||||
|
||||
this.loadAndApplyPage({
|
||||
url: `/common/modal/alert`,
|
||||
method: 'POST',
|
||||
data: `title=${params.title}&message=${params.message}`,
|
||||
contentSelector: 'div[data-alert-modal="body"]',
|
||||
appendToSelector: 'body'
|
||||
}, {
|
||||
success: function () {
|
||||
EventRouter.register('clickAlertModalCancelButton', callbacks.cancel);
|
||||
},
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Object} params
|
||||
* @param {string} params.url
|
||||
* @param {string} params.method
|
||||
* @param {string=} params.data
|
||||
* @param {string} params.contentSelector
|
||||
* @param {string} params.appendToSelector
|
||||
* @param {Object=} callbacks
|
||||
* @param {function(jQuery)=} callbacks.success - modal body의 jquery 객체가 파라미터로 오는 success 콜백 함수
|
||||
* @param {function(Object)=} callbacks.error - 에러 메세지가 파라미터로 오는 error 콜백 함수
|
||||
* @param {function()=} callbacks.complete
|
||||
*/
|
||||
loadAndApplyPage(params, callbacks) {
|
||||
if (!callbacks) {
|
||||
callbacks = {};
|
||||
}
|
||||
|
||||
this.loadPage({
|
||||
url: params.url,
|
||||
method: params.method,
|
||||
data: params.data,
|
||||
}, {
|
||||
success: function (res) {
|
||||
const e = $(res);
|
||||
const d = e.find(params.contentSelector);
|
||||
d.appendTo(params.appendToSelector);
|
||||
const s = e.find('script');
|
||||
s.appendTo('body');
|
||||
if (callbacks.success) callbacks.success(d);
|
||||
},
|
||||
error: function (error) {
|
||||
if (callbacks.error) callbacks.error(error);
|
||||
},
|
||||
complete: function () {
|
||||
if (callbacks.complete) callbacks.complete();
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {Object} params
|
||||
* @param {string} params.url
|
||||
* @param {string} params.method
|
||||
* @param {string=} params.data
|
||||
* @param {Object=} callbacks
|
||||
* @param {function(string)=} callbacks.success
|
||||
* @param {function(Object)=} callbacks.error
|
||||
* @param {function()=} callbacks.complete
|
||||
*/
|
||||
loadPage(params, callbacks) {
|
||||
if (!callbacks) {
|
||||
callbacks = {};
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: params.url,
|
||||
type: params.method,
|
||||
data: params.data,
|
||||
success: function (res) {
|
||||
if (callbacks.success) callbacks.success(res);
|
||||
},
|
||||
error: function (error) {
|
||||
console.error(error);
|
||||
if (callbacks.error) callbacks.error(error);
|
||||
},
|
||||
complete: function () {
|
||||
if (callbacks.complete) callbacks.complete();
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,81 @@
|
|||
const ReqHelper = {
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {jQuery} form
|
||||
* @param {Object=} callbacks
|
||||
* @param {function(Object=)=} callbacks.success
|
||||
* @param {function(Object)=} callbacks.error
|
||||
* @param {function()=} callbacks.complete
|
||||
*/
|
||||
reqByForm(form, callbacks) {
|
||||
if (!(form instanceof jQuery)) {
|
||||
throw new Error('form is not jQuery instance');
|
||||
}
|
||||
if (!callbacks) {
|
||||
callbacks = {};
|
||||
}
|
||||
|
||||
const url = form.attr('action');
|
||||
const method = form.attr('method');
|
||||
const dataObj = form.serializeArray().reduce((obj, item) => {
|
||||
obj[item.name] = item.value;
|
||||
return obj;
|
||||
}, {});
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: method,
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(dataObj),
|
||||
success: function (res) {
|
||||
if (!callbacks.success) return;
|
||||
|
||||
res.isSuccess = () => res.resultCode === "0000";
|
||||
res.isFail = () => !res.isSuccess();
|
||||
res.getMessage = () => res.resultMsg || '';
|
||||
callbacks.success(res);
|
||||
},
|
||||
error: function (error) {
|
||||
if (callbacks.error) callbacks.error(error);
|
||||
},
|
||||
complete: function () {
|
||||
if (callbacks.complete) callbacks.complete();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
*
|
||||
* @param url
|
||||
* @param sendData {Object=}
|
||||
* @param {Object=} callbacks
|
||||
* @param {function(Object=)=} callbacks.success
|
||||
* @param {function(Object)=} callbacks.error
|
||||
* @param {function()=} callbacks.complete
|
||||
*/
|
||||
reqByObj(url, sendData, callbacks) {
|
||||
if (!url) {
|
||||
throw new Error('url is empty');
|
||||
}
|
||||
if (!callbacks) {
|
||||
callbacks = {};
|
||||
}
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: "post",
|
||||
contentType: 'application/json',
|
||||
data: JSON.stringify(sendData),
|
||||
success: function (res) {
|
||||
if (callbacks.success) callbacks.success(res);
|
||||
},
|
||||
error: function (error) {
|
||||
if (callbacks.error) callbacks.error(error);
|
||||
},
|
||||
complete: function () {
|
||||
if (callbacks.complete) callbacks.complete();
|
||||
}
|
||||
});
|
||||
},
|
||||
};
|
||||
|
|
@ -0,0 +1,20 @@
|
|||
<!DOCTYPE html>
|
||||
<html xmlns:th="http://www.thymeleaf.org">
|
||||
<body>
|
||||
<th:block th:fragment="applyCsrf">
|
||||
<meta name="_csrf_header" th:content="${_csrf.headerName}"/>
|
||||
<meta name="_csrf" th:content="${_csrf.token}"/>
|
||||
<script th:inline="javascript">
|
||||
// 모든 AJAX 요청에 자동으로 CSRF 토큰이 포함
|
||||
const csrfToken = $('meta[name="_csrf"]').attr('content');
|
||||
const csrfHeader = $('meta[name="_csrf_header"]').attr('content');
|
||||
|
||||
$.ajaxSetup({
|
||||
beforeSend: function (xhr) {
|
||||
xhr.setRequestHeader(csrfHeader, csrfToken);
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</th:block>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,44 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
|
||||
<body>
|
||||
<div>
|
||||
<div class="modal" data-alert-modal="body">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<span class="close" data-alert-modal="cancelButton">×</span>
|
||||
<h2 th:text="${title}"/>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="col-box">
|
||||
<span style="white-space: pre-line" th:text="${message}"></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer btn_both">
|
||||
<button type="button" class="btn_normal" data-alert-modal="cancelButton">닫기</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script data-alert-modal="script">
|
||||
(function () {
|
||||
const modalBody = $('div[data-alert-modal="body"]');
|
||||
|
||||
modalBody.fadeIn(100);
|
||||
$("body").css("overflow", "hidden");
|
||||
|
||||
$('[data-alert-modal="cancelButton"]').on('click', function () {
|
||||
close();
|
||||
EventRouter.trigger('clickAlertModalCancelButton');
|
||||
});
|
||||
|
||||
function close() {
|
||||
modalBody.fadeOut(100);
|
||||
modalBody.remove();
|
||||
$("body").css("overflow", "auto");
|
||||
$('script[data-alert-modal="script"]').remove();
|
||||
}
|
||||
})()
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,60 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
|
||||
<body>
|
||||
<div>
|
||||
<div class="modal" data-confirm-modal="body">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<span class="close" data-confirm-modal="cancelButton">×</span>
|
||||
<h2 th:text="${title}"/>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p class="col-box">
|
||||
<span style="white-space: pre-line" th:text="${message}"></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer btn_both">
|
||||
<button type="button" class="btn_confirm" data-confirm-modal="confirmButton">확인</button>
|
||||
<button type="button" class="btn_normal" data-confirm-modal="cancelButton">취소</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script data-confirm-modal="script">
|
||||
(function () {
|
||||
const modalBody = $('div[data-confirm-modal="body"]');
|
||||
|
||||
modalBody.fadeIn(100);
|
||||
$("body").css("overflow", "hidden");
|
||||
|
||||
$('button[data-confirm-modal="confirmButton"]').on('click', function () {
|
||||
let prevented = false;
|
||||
EventRouter.trigger('clickConfirmModalConfirmButton', {preventDefault: () => prevented = true});
|
||||
if (prevented) {
|
||||
return;
|
||||
}
|
||||
|
||||
hideAndRemove();
|
||||
});
|
||||
|
||||
$('[data-confirm-modal="cancelButton"]').on('click', function () {
|
||||
let prevented = false;
|
||||
EventRouter.trigger('clickConfirmModalCancelButton', {preventDefault: () => prevented = true});
|
||||
if (prevented) {
|
||||
return;
|
||||
}
|
||||
|
||||
hideAndRemove();
|
||||
});
|
||||
|
||||
function hideAndRemove() {
|
||||
modalBody.fadeOut(100);
|
||||
modalBody.remove();
|
||||
$("body").css("overflow", "auto");
|
||||
$('script[data-confirm-modal="script"]').remove();
|
||||
}
|
||||
})()
|
||||
</script>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,46 @@
|
|||
<!-- data-page-modal="body" / 모달 본문 -->
|
||||
<!-- data-page-modal="script" / 모달 스크립트 -->
|
||||
<!-- data-page-modal="cancelButton" / 닫힘 버튼, 클릭하면 모달 및 스크립트를 삭제한다. -->
|
||||
|
||||
<!DOCTYPE html>
|
||||
<html lang="ko" xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
|
||||
<body>
|
||||
<div>
|
||||
<div class="modal" data-page-modal="body">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<span class="close" data-page-modal="cancelButton">×</span>
|
||||
<h2>
|
||||
<th:block layout:fragment="title"></th:block>
|
||||
</h2>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<th:block layout:fragment="body"></th:block>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<script data-page-modal="script">
|
||||
$('div[data-page-modal="body"]').fadeIn(100);
|
||||
$("body").css("overflow", "hidden");
|
||||
|
||||
$('[data-page-modal="cancelButton"]').on('click', cancelModal);
|
||||
|
||||
function cancelModal() {
|
||||
closeModal();
|
||||
EventRouter.trigger('cancelModal');
|
||||
}
|
||||
|
||||
function closeModal() {
|
||||
const modalBody = $('div[data-page-modal="body"]');
|
||||
modalBody.fadeOut(100);
|
||||
modalBody.remove();
|
||||
$("body").css("overflow", "auto");
|
||||
$('script[data-page-modal="script"]').remove();
|
||||
}
|
||||
</script>
|
||||
|
||||
<th:block layout:fragment="script"></th:block>
|
||||
</div>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,68 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
|
||||
<title>시스템</title>
|
||||
<script type="text/javascript" th:src="@{/js/jquery/jquery-3.7.1.min.js}"></script>
|
||||
<link rel="shortcut icon" th:href="@{/images/favicon.ico}">
|
||||
<link rel="stylesheet" th:href="@{/css/style.css}"/>
|
||||
|
||||
<script type="text/javascript" th:src="@{/js/pagehelper.js}"></script>
|
||||
<script type="text/javascript" th:src="@{/js/reqhelper.js}"></script>
|
||||
<script type="text/javascript" th:src="@{/js/eventrouter.js}"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="login_wrap">
|
||||
<div class="login_box">
|
||||
<h1>시스템</h1>
|
||||
<p class="login_subtit">환영합니다. 시스템에 로그인하세요.</p>
|
||||
<form th:action="@{/login}" method="post" id="loginForm">
|
||||
<p>
|
||||
<label>아이디</label>
|
||||
<input type="text" name="loginId" placeholder="이름 입력">
|
||||
</p>
|
||||
<p>
|
||||
<label>비밀번호</label>
|
||||
<input type="password" name="password" placeholder="암호 입력">
|
||||
</p>
|
||||
</form>
|
||||
<button type="button" id="loginButton" class="btn_confirm btn_login">로그인</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script type="text/javascript" th:inline="javascript">
|
||||
const errorMessage = /*[[${errorMessage}]]*/ '';
|
||||
|
||||
if (errorMessage) {
|
||||
PageHelper.showErrorModal(errorMessage);
|
||||
}
|
||||
|
||||
$('#loginButton').click(function () {
|
||||
|
||||
if (!validate()) {
|
||||
return;
|
||||
}
|
||||
$('#loginForm').submit();
|
||||
|
||||
function validate() {
|
||||
const loginId = $('input[name="loginId"]').val();
|
||||
const password = $('input[name="password"]').val();
|
||||
|
||||
if (!loginId) {
|
||||
PageHelper.showErrorModal('아이디를 입력해주세요.');
|
||||
return false;
|
||||
}
|
||||
|
||||
if (!password) {
|
||||
PageHelper.showErrorModal('비밀번호를 입력해주세요.');
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,14 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="ko" xmlns:th="http://www.thymeleaf.org">
|
||||
<body>
|
||||
<th:block th:fragment="gnb">
|
||||
<h1 class="title_text">
|
||||
관리자 시스템
|
||||
</h1>
|
||||
<p class="menu_right">
|
||||
<a href="#">비밀번호 변경</a>
|
||||
<a th:href="@{/logout}">로그아웃</a>
|
||||
</p>
|
||||
<a href="#none" class="nav_menu" id="toggleLink"><img th:src="@{/images/ico_menu.svg}" alt="메뉴 아이콘"></a>
|
||||
</th:block>
|
||||
</body>
|
||||
|
|
@ -0,0 +1,24 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="ko"
|
||||
xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:sec="https://www.thymeleaf.org/thymeleaf-extras-springsecurity6">
|
||||
<body>
|
||||
<th:block th:fragment="lnb">
|
||||
<nav class="menu_wrap flexitem">
|
||||
<!--/*@thymesVar id="pathInfo" type="kr.co.bpsoft.settlement_system.backend.admin.web.advice.PathInfoControllerAdvice$PathInfo"*/-->
|
||||
<h2>메뉴</h2>
|
||||
<button class="closeButton"><img th:src="@{/images/ico_close.svg}"></button>
|
||||
<th>
|
||||
<a href="#"
|
||||
class="nav_tit">
|
||||
1 Depth
|
||||
</a>
|
||||
<ul class="nav_section">
|
||||
<li>
|
||||
<a>2 Depth</a>
|
||||
</li>
|
||||
</ul>
|
||||
</th>
|
||||
</nav>
|
||||
</th:block>
|
||||
</body>
|
||||
|
|
@ -0,0 +1,37 @@
|
|||
<!DOCTYPE html>
|
||||
<html lang="ko" xmlns:th="http://www.thymeleaf.org"
|
||||
xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta http-equiv="X-UA-Compatible" content="IE=edge">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no"/>
|
||||
<title>관리자 시스템</title>
|
||||
<script type="text/javascript" th:src="@{/js/jquery/jquery-3.7.1.min.js}"></script>
|
||||
<link rel="shortcut icon" th:href="@{/images/favicon.ico}">
|
||||
<link rel="stylesheet" th:href="@{/css/style.css}"/>
|
||||
<link rel="stylesheet" th:href="@{/css/sub.css}"/>
|
||||
|
||||
<script type="text/javascript" th:src="@{/js/motion.js}"></script>
|
||||
<script type="text/javascript" th:src="@{/js/pagehelper.js}"></script>
|
||||
<script type="text/javascript" th:src="@{/js/reqhelper.js}"></script>
|
||||
<script type="text/javascript" th:src="@{/js/eventrouter.js}"></script>
|
||||
|
||||
<th:block th:replace="~{fragment/csrf/csrf :: applyCsrf}"></th:block>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<header>
|
||||
<th:block th:replace="~{main/gnb :: gnb}"></th:block>
|
||||
</header>
|
||||
<div class="wrapper flexbox">
|
||||
<th:block th:replace="~{main/lnb :: lnb}"></th:block>
|
||||
<div class="container flexitem">
|
||||
<th:block layout:fragment="contents"/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<th:block layout:fragment="script">
|
||||
|
||||
</th:block>
|
||||
</body>
|
||||
</html>
|
||||
|
|
@ -0,0 +1,13 @@
|
|||
package com.bpgroup.poc.admin;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
|
||||
@SpringBootTest
|
||||
class AdminApplicationTests {
|
||||
|
||||
@Test
|
||||
void contextLoads() {
|
||||
}
|
||||
|
||||
}
|
||||