diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..549e00a --- /dev/null +++ b/.gitignore @@ -0,0 +1,33 @@ +HELP.md +target/ +!.mvn/wrapper/maven-wrapper.jar +!**/src/main/**/target/ +!**/src/test/**/target/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ +build/ +!**/src/main/**/build/ +!**/src/test/**/build/ + +### VS Code ### +.vscode/ diff --git a/.mvn/wrapper/MavenWrapperDownloader.java b/.mvn/wrapper/MavenWrapperDownloader.java new file mode 100644 index 0000000..a45eb6b --- /dev/null +++ b/.mvn/wrapper/MavenWrapperDownloader.java @@ -0,0 +1,118 @@ +/* + * Copyright 2007-present the original author or 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. + */ + +import java.net.*; +import java.io.*; +import java.nio.channels.*; +import java.util.Properties; + +public class MavenWrapperDownloader { + + private static final String WRAPPER_VERSION = "0.5.6"; + /** + * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided. + */ + private static final String DEFAULT_DOWNLOAD_URL = "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/" + + WRAPPER_VERSION + "/maven-wrapper-" + WRAPPER_VERSION + ".jar"; + + /** + * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to + * use instead of the default one. + */ + private static final String MAVEN_WRAPPER_PROPERTIES_PATH = + ".mvn/wrapper/maven-wrapper.properties"; + + /** + * Path where the maven-wrapper.jar will be saved to. + */ + private static final String MAVEN_WRAPPER_JAR_PATH = + ".mvn/wrapper/maven-wrapper.jar"; + + /** + * Name of the property which should be used to override the default download url for the wrapper. + */ + private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl"; + + public static void main(String args[]) { + System.out.println("- Downloader started"); + File baseDirectory = new File(args[0]); + System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath()); + + // If the maven-wrapper.properties exists, read it and check if it contains a custom + // wrapperUrl parameter. + File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH); + String url = DEFAULT_DOWNLOAD_URL; + if (mavenWrapperPropertyFile.exists()) { + FileInputStream mavenWrapperPropertyFileInputStream = null; + try { + mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile); + Properties mavenWrapperProperties = new Properties(); + mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream); + url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url); + } catch (IOException e) { + System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'"); + } finally { + try { + if (mavenWrapperPropertyFileInputStream != null) { + mavenWrapperPropertyFileInputStream.close(); + } + } catch (IOException e) { + // Ignore ... + } + } + } + System.out.println("- Downloading from: " + url); + + File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH); + if (!outputFile.getParentFile().exists()) { + if (!outputFile.getParentFile().mkdirs()) { + System.out.println( + "- ERROR creating output directory '" + outputFile.getParentFile().getAbsolutePath() + "'"); + } + } + System.out.println("- Downloading to: " + outputFile.getAbsolutePath()); + try { + downloadFileFromURL(url, outputFile); + System.out.println("Done"); + System.exit(0); + } catch (Throwable e) { + System.out.println("- Error downloading"); + e.printStackTrace(); + System.exit(1); + } + } + + private static void downloadFileFromURL(String urlString, File destination) throws Exception { + if (System.getenv("MVNW_USERNAME") != null && System.getenv("MVNW_PASSWORD") != null) { + String username = System.getenv("MVNW_USERNAME"); + char[] password = System.getenv("MVNW_PASSWORD").toCharArray(); + Authenticator.setDefault(new Authenticator() { + @Override + protected PasswordAuthentication getPasswordAuthentication() { + return new PasswordAuthentication(username, password); + } + }); + } + URL website = new URL(urlString); + ReadableByteChannel rbc; + rbc = Channels.newChannel(website.openStream()); + FileOutputStream fos = new FileOutputStream(destination); + fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE); + fos.close(); + rbc.close(); + } + +} diff --git a/.mvn/wrapper/maven-wrapper.jar b/.mvn/wrapper/maven-wrapper.jar new file mode 100644 index 0000000..2cc7d4a Binary files /dev/null and b/.mvn/wrapper/maven-wrapper.jar differ diff --git a/.mvn/wrapper/maven-wrapper.properties b/.mvn/wrapper/maven-wrapper.properties new file mode 100644 index 0000000..a9f1ef8 --- /dev/null +++ b/.mvn/wrapper/maven-wrapper.properties @@ -0,0 +1,2 @@ +distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.3/apache-maven-3.8.3-bin.zip +wrapperUrl=https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar diff --git a/docker.md b/docker.md new file mode 100644 index 0000000..0f86bfe --- /dev/null +++ b/docker.md @@ -0,0 +1,7 @@ +``` +docker create --name mysql8 -e MYSQL_ROOT_PASSWORD=root -p 3306:3306 -v c:\work\mysqldata:/var/lib/mysql mysql:8.0.27 +``` + +docker start mysql8 + +docker stop mysql8 \ No newline at end of file diff --git a/mvnw b/mvnw new file mode 100644 index 0000000..a16b543 --- /dev/null +++ b/mvnw @@ -0,0 +1,310 @@ +#!/bin/sh +# ---------------------------------------------------------------------------- +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you 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. +# ---------------------------------------------------------------------------- + +# ---------------------------------------------------------------------------- +# Maven Start Up Batch script +# +# Required ENV vars: +# ------------------ +# JAVA_HOME - location of a JDK home dir +# +# Optional ENV vars +# ----------------- +# M2_HOME - location of maven2's installed home dir +# MAVEN_OPTS - parameters passed to the Java VM when running Maven +# e.g. to debug Maven itself, use +# set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +# MAVEN_SKIP_RC - flag to disable loading of mavenrc files +# ---------------------------------------------------------------------------- + +if [ -z "$MAVEN_SKIP_RC" ] ; then + + if [ -f /etc/mavenrc ] ; then + . /etc/mavenrc + fi + + if [ -f "$HOME/.mavenrc" ] ; then + . "$HOME/.mavenrc" + fi + +fi + +# OS specific support. $var _must_ be set to either true or false. +cygwin=false; +darwin=false; +mingw=false +case "`uname`" in + CYGWIN*) cygwin=true ;; + MINGW*) mingw=true;; + Darwin*) darwin=true + # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home + # See https://developer.apple.com/library/mac/qa/qa1170/_index.html + if [ -z "$JAVA_HOME" ]; then + if [ -x "/usr/libexec/java_home" ]; then + export JAVA_HOME="`/usr/libexec/java_home`" + else + export JAVA_HOME="/Library/Java/Home" + fi + fi + ;; +esac + +if [ -z "$JAVA_HOME" ] ; then + if [ -r /etc/gentoo-release ] ; then + JAVA_HOME=`java-config --jre-home` + fi +fi + +if [ -z "$M2_HOME" ] ; then + ## resolve links - $0 may be a link to maven's home + PRG="$0" + + # need this for relative symlinks + while [ -h "$PRG" ] ; do + ls=`ls -ld "$PRG"` + link=`expr "$ls" : '.*-> \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG="`dirname "$PRG"`/$link" + fi + done + + saveddir=`pwd` + + M2_HOME=`dirname "$PRG"`/.. + + # make it fully qualified + M2_HOME=`cd "$M2_HOME" && pwd` + + cd "$saveddir" + # echo Using m2 at $M2_HOME +fi + +# For Cygwin, ensure paths are in UNIX format before anything is touched +if $cygwin ; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --unix "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --unix "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --unix "$CLASSPATH"` +fi + +# For Mingw, ensure paths are in UNIX format before anything is touched +if $mingw ; then + [ -n "$M2_HOME" ] && + M2_HOME="`(cd "$M2_HOME"; pwd)`" + [ -n "$JAVA_HOME" ] && + JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`" +fi + +if [ -z "$JAVA_HOME" ]; then + javaExecutable="`which javac`" + if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then + # readlink(1) is not available as standard on Solaris 10. + readLink=`which readlink` + if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then + if $darwin ; then + javaHome="`dirname \"$javaExecutable\"`" + javaExecutable="`cd \"$javaHome\" && pwd -P`/javac" + else + javaExecutable="`readlink -f \"$javaExecutable\"`" + fi + javaHome="`dirname \"$javaExecutable\"`" + javaHome=`expr "$javaHome" : '\(.*\)/bin'` + JAVA_HOME="$javaHome" + export JAVA_HOME + fi + fi +fi + +if [ -z "$JAVACMD" ] ; then + 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 + else + JAVACMD="`which java`" + fi +fi + +if [ ! -x "$JAVACMD" ] ; then + echo "Error: JAVA_HOME is not defined correctly." >&2 + echo " We cannot execute $JAVACMD" >&2 + exit 1 +fi + +if [ -z "$JAVA_HOME" ] ; then + echo "Warning: JAVA_HOME environment variable is not set." +fi + +CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher + +# traverses directory structure from process work directory to filesystem root +# first directory with .mvn subdirectory is considered project base directory +find_maven_basedir() { + + if [ -z "$1" ] + then + echo "Path not specified to find_maven_basedir" + return 1 + fi + + basedir="$1" + wdir="$1" + while [ "$wdir" != '/' ] ; do + if [ -d "$wdir"/.mvn ] ; then + basedir=$wdir + break + fi + # workaround for JBEAP-8937 (on Solaris 10/Sparc) + if [ -d "${wdir}" ]; then + wdir=`cd "$wdir/.."; pwd` + fi + # end of workaround + done + echo "${basedir}" +} + +# concatenates all lines of a file +concat_lines() { + if [ -f "$1" ]; then + echo "$(tr -s '\n' ' ' < "$1")" + fi +} + +BASE_DIR=`find_maven_basedir "$(pwd)"` +if [ -z "$BASE_DIR" ]; then + exit 1; +fi + +########################################################################################## +# Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +# This allows using the maven wrapper in projects that prohibit checking in binary data. +########################################################################################## +if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found .mvn/wrapper/maven-wrapper.jar" + fi +else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..." + fi + if [ -n "$MVNW_REPOURL" ]; then + jarUrl="$MVNW_REPOURL/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + else + jarUrl="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + fi + while IFS="=" read key value; do + case "$key" in (wrapperUrl) jarUrl="$value"; break ;; + esac + done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties" + if [ "$MVNW_VERBOSE" = true ]; then + echo "Downloading from: $jarUrl" + fi + wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" + if $cygwin; then + wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"` + fi + + if command -v wget > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found wget ... using wget" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + wget "$jarUrl" -O "$wrapperJarPath" + else + wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" + fi + elif command -v curl > /dev/null; then + if [ "$MVNW_VERBOSE" = true ]; then + echo "Found curl ... using curl" + fi + if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then + curl -o "$wrapperJarPath" "$jarUrl" -f + else + curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f + fi + + else + if [ "$MVNW_VERBOSE" = true ]; then + echo "Falling back to using Java to download" + fi + javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java" + # For Cygwin, switch paths to Windows format before running javac + if $cygwin; then + javaClass=`cygpath --path --windows "$javaClass"` + fi + if [ -e "$javaClass" ]; then + if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Compiling MavenWrapperDownloader.java ..." + fi + # Compiling the Java class + ("$JAVA_HOME/bin/javac" "$javaClass") + fi + if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then + # Running the downloader + if [ "$MVNW_VERBOSE" = true ]; then + echo " - Running MavenWrapperDownloader.java ..." + fi + ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR") + fi + fi + fi +fi +########################################################################################## +# End of extension +########################################################################################## + +export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"} +if [ "$MVNW_VERBOSE" = true ]; then + echo $MAVEN_PROJECTBASEDIR +fi +MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS" + +# For Cygwin, switch paths to Windows format before running java +if $cygwin; then + [ -n "$M2_HOME" ] && + M2_HOME=`cygpath --path --windows "$M2_HOME"` + [ -n "$JAVA_HOME" ] && + JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"` + [ -n "$CLASSPATH" ] && + CLASSPATH=`cygpath --path --windows "$CLASSPATH"` + [ -n "$MAVEN_PROJECTBASEDIR" ] && + MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"` +fi + +# Provide a "standardized" way to retrieve the CLI args that will +# work with both Windows and non-Windows executions. +MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@" +export MAVEN_CMD_LINE_ARGS + +WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +exec "$JAVACMD" \ + $MAVEN_OPTS \ + -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \ + "-Dmaven.home=${M2_HOME}" "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \ + ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@" diff --git a/mvnw.cmd b/mvnw.cmd new file mode 100644 index 0000000..c8d4337 --- /dev/null +++ b/mvnw.cmd @@ -0,0 +1,182 @@ +@REM ---------------------------------------------------------------------------- +@REM Licensed to the Apache Software Foundation (ASF) under one +@REM or more contributor license agreements. See the NOTICE file +@REM distributed with this work for additional information +@REM regarding copyright ownership. The ASF licenses this file +@REM to you under the Apache License, Version 2.0 (the +@REM "License"); you may not use this file except in compliance +@REM with the License. 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, +@REM software distributed under the License is distributed on an +@REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +@REM KIND, either express or implied. See the License for the +@REM specific language governing permissions and limitations +@REM under the License. +@REM ---------------------------------------------------------------------------- + +@REM ---------------------------------------------------------------------------- +@REM Maven Start Up Batch script +@REM +@REM Required ENV vars: +@REM JAVA_HOME - location of a JDK home dir +@REM +@REM Optional ENV vars +@REM M2_HOME - location of maven2's installed home dir +@REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands +@REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending +@REM MAVEN_OPTS - parameters passed to the Java VM when running Maven +@REM e.g. to debug Maven itself, use +@REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000 +@REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files +@REM ---------------------------------------------------------------------------- + +@REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on' +@echo off +@REM set title of command window +title %0 +@REM enable echoing by setting MAVEN_BATCH_ECHO to 'on' +@if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO% + +@REM set %HOME% to equivalent of $HOME +if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%") + +@REM Execute a user defined script before this one +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre +@REM check for pre script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_pre.bat" call "%HOME%\mavenrc_pre.bat" +if exist "%HOME%\mavenrc_pre.cmd" call "%HOME%\mavenrc_pre.cmd" +:skipRcPre + +@setlocal + +set ERROR_CODE=0 + +@REM To isolate internal variables from possible post scripts, we use another setlocal +@setlocal + +@REM ==== START VALIDATION ==== +if not "%JAVA_HOME%" == "" goto OkJHome + +echo. +echo Error: JAVA_HOME not found in your environment. >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +:OkJHome +if exist "%JAVA_HOME%\bin\java.exe" goto init + +echo. +echo Error: JAVA_HOME is set to an invalid directory. >&2 +echo JAVA_HOME = "%JAVA_HOME%" >&2 +echo Please set the JAVA_HOME variable in your environment to match the >&2 +echo location of your Java installation. >&2 +echo. +goto error + +@REM ==== END VALIDATION ==== + +:init + +@REM Find the project base dir, i.e. the directory that contains the folder ".mvn". +@REM Fallback to current working directory if not found. + +set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR% +IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir + +set EXEC_DIR=%CD% +set WDIR=%EXEC_DIR% +:findBaseDir +IF EXIST "%WDIR%"\.mvn goto baseDirFound +cd .. +IF "%WDIR%"=="%CD%" goto baseDirNotFound +set WDIR=%CD% +goto findBaseDir + +:baseDirFound +set MAVEN_PROJECTBASEDIR=%WDIR% +cd "%EXEC_DIR%" +goto endDetectBaseDir + +:baseDirNotFound +set MAVEN_PROJECTBASEDIR=%EXEC_DIR% +cd "%EXEC_DIR%" + +:endDetectBaseDir + +IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig + +@setlocal EnableExtensions EnableDelayedExpansion +for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a +@endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS% + +:endReadAdditionalConfig + +SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe" +set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar" +set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain + +set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + +FOR /F "tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO ( + IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B +) + +@REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central +@REM This allows using the maven wrapper in projects that prohibit checking in binary data. +if exist %WRAPPER_JAR% ( + if "%MVNW_VERBOSE%" == "true" ( + echo Found %WRAPPER_JAR% + ) +) else ( + if not "%MVNW_REPOURL%" == "" ( + SET DOWNLOAD_URL="%MVNW_REPOURL%/io/takari/maven-wrapper/0.5.6/maven-wrapper-0.5.6.jar" + ) + if "%MVNW_VERBOSE%" == "true" ( + echo Couldn't find %WRAPPER_JAR%, downloading it ... + echo Downloading from: %DOWNLOAD_URL% + ) + + powershell -Command "&{"^ + "$webclient = new-object System.Net.WebClient;"^ + "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^ + "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^ + "}"^ + "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^ + "}" + if "%MVNW_VERBOSE%" == "true" ( + echo Finished downloading %WRAPPER_JAR% + ) +) +@REM End of extension + +@REM Provide a "standardized" way to retrieve the CLI args that will +@REM work with both Windows and non-Windows executions. +set MAVEN_CMD_LINE_ARGS=%* + +%MAVEN_JAVA_EXE% %JVM_CONFIG_MAVEN_PROPS% %MAVEN_OPTS% %MAVEN_DEBUG_OPTS% -classpath %WRAPPER_JAR% "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %* +if ERRORLEVEL 1 goto error +goto end + +:error +set ERROR_CODE=1 + +:end +@endlocal & set ERROR_CODE=%ERROR_CODE% + +if not "%MAVEN_SKIP_RC%" == "" goto skipRcPost +@REM check for post script, once with legacy .bat ending and once with .cmd ending +if exist "%HOME%\mavenrc_post.bat" call "%HOME%\mavenrc_post.bat" +if exist "%HOME%\mavenrc_post.cmd" call "%HOME%\mavenrc_post.cmd" +:skipRcPost + +@REM pause the script if MAVEN_BATCH_PAUSE is set to 'on' +if "%MAVEN_BATCH_PAUSE%" == "on" pause + +if "%MAVEN_TERMINATE_CMD%" == "on" exit %ERROR_CODE% + +exit /B %ERROR_CODE% diff --git a/pom.xml b/pom.xml new file mode 100644 index 0000000..64863a6 --- /dev/null +++ b/pom.xml @@ -0,0 +1,84 @@ + + + 4.0.0 + + org.springframework.boot + spring-boot-starter-parent + 2.6.1 + + + ddd-start + ddd-start2 + 2.0 + ddd-start2 + ddd-start2 + + 17 + + + + org.springframework.boot + spring-boot-devtools + + + org.springframework.boot + spring-boot-starter-data-jpa + + + org.springframework.boot + spring-boot-starter-mail + + + org.springframework.boot + spring-boot-starter-thymeleaf + + + org.thymeleaf.extras + thymeleaf-extras-springsecurity5 + + + org.springframework.boot + spring-boot-starter-validation + + + org.springframework.boot + spring-boot-starter-web + + + org.springframework.boot + spring-boot-starter-security + + + org.mybatis.spring.boot + mybatis-spring-boot-starter + 2.2.0 + + + + com.h2database + h2 + runtime + + + mysql + mysql-connector-java + runtime + + + org.springframework.boot + spring-boot-starter-test + test + + + + + + + org.springframework.boot + spring-boot-maven-plugin + + + + + diff --git a/src/main/java/com/myshop/ShopApplication.java b/src/main/java/com/myshop/ShopApplication.java new file mode 100644 index 0000000..ec98fde --- /dev/null +++ b/src/main/java/com/myshop/ShopApplication.java @@ -0,0 +1,18 @@ +package com.myshop; + +import com.myshop.common.jpa.RangeableRepositoryImpl; +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; +import org.springframework.data.jpa.repository.config.EnableJpaRepositories; +import org.springframework.scheduling.annotation.EnableAsync; + +@SpringBootApplication +@EnableAsync +@EnableJpaRepositories(repositoryBaseClass = RangeableRepositoryImpl.class) +public class ShopApplication { + + public static void main(String[] args) { + SpringApplication.run(ShopApplication.class, args); + } + +} diff --git a/src/main/java/com/myshop/admin/ui/AdminOrderController.java b/src/main/java/com/myshop/admin/ui/AdminOrderController.java new file mode 100644 index 0000000..edd1465 --- /dev/null +++ b/src/main/java/com/myshop/admin/ui/AdminOrderController.java @@ -0,0 +1,72 @@ +package com.myshop.admin.ui; + +import com.myshop.common.ui.Pagination; +import com.myshop.order.command.application.StartShippingRequest; +import com.myshop.order.command.application.StartShippingService; +import com.myshop.order.query.application.ListRequest; +import com.myshop.order.query.application.OrderDetail; +import com.myshop.order.query.application.OrderDetailService; +import com.myshop.order.query.application.OrderViewListService; +import com.myshop.order.query.dto.OrderSummary; +import org.springframework.dao.OptimisticLockingFailureException; +import org.springframework.data.domain.Page; +import org.springframework.stereotype.Controller; +import org.springframework.ui.ModelMap; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; + +import java.util.Optional; + +@Controller +public class AdminOrderController { + + private OrderViewListService orderViewListService; + private OrderDetailService orderDetailService; + private StartShippingService startShippingService; + + public AdminOrderController(OrderViewListService orderViewListService, OrderDetailService orderDetailService, StartShippingService startShippingService) { + this.orderViewListService = orderViewListService; + this.orderDetailService = orderDetailService; + this.startShippingService = startShippingService; + } + + @RequestMapping("/admin/orders") + public String orders(@RequestParam(name = "p", defaultValue = "1") int page, + ModelMap modelMap) { + int size = 20; + ListRequest listRequest = new ListRequest(page - 1, size); + Page orderPage = orderViewListService.getList(listRequest); + modelMap.addAttribute("orderPage", orderPage); + modelMap.addAttribute("pagination", + new Pagination( + orderPage.getNumber() + 1, + orderPage.getTotalPages(), + 5 + )); + return "admin/adminOrders"; + } + + @RequestMapping("/admin/orders/{orderNo}") + public String orderDetail(@PathVariable("orderNo") String orderNo, ModelMap modelMap) { + Optional orderDetail = orderDetailService.getOrderDetail(orderNo); + if (orderDetail.isPresent()) { + modelMap.addAttribute("order", orderDetail.get()); + return "admin/adminOrderDetail"; + } else { + return "admin/noOrder"; + } + } + + @RequestMapping(value = "/admin/orders/{orderNo}/shipping", method = RequestMethod.POST) + public String startShippingOrder(@PathVariable("orderNo") String orderNo, + @RequestParam("version") long version) { + try { + startShippingService.startShipping(new StartShippingRequest(orderNo, version)); + return "admin/adminOrderShipped"; + } catch (OptimisticLockingFailureException e) { + return "admin/adminOrderLockFail"; + } + } +} diff --git a/src/main/java/com/myshop/board/domain/Article.java b/src/main/java/com/myshop/board/domain/Article.java new file mode 100644 index 0000000..38d6431 --- /dev/null +++ b/src/main/java/com/myshop/board/domain/Article.java @@ -0,0 +1,48 @@ +package com.myshop.board.domain; + +import javax.persistence.*; + +@Entity +@Table(name = "article") +@SecondaryTable( + name = "article_content", + pkJoinColumns = @PrimaryKeyJoinColumn(name = "id") +) +public class Article { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + private Long id; + + private String title; + + @AttributeOverrides({ + @AttributeOverride( + name = "content", + column = @Column(table = "article_content", name = "content")), + @AttributeOverride( + name = "contentType", + column = @Column(table = "article_content", name = "content_type")) + }) + @Embedded + private ArticleContent content; + + protected Article() { + } + + public Article(String title, ArticleContent content) { + this.title = title; + this.content = content; + } + + public Long getId() { + return id; + } + + public String getTitle() { + return title; + } + + public ArticleContent getContent() { + return content; + } +} diff --git a/src/main/java/com/myshop/board/domain/ArticleContent.java b/src/main/java/com/myshop/board/domain/ArticleContent.java new file mode 100644 index 0000000..991f563 --- /dev/null +++ b/src/main/java/com/myshop/board/domain/ArticleContent.java @@ -0,0 +1,28 @@ +package com.myshop.board.domain; + +import javax.persistence.Access; +import javax.persistence.AccessType; +import javax.persistence.Embeddable; + +@Embeddable +@Access(AccessType.FIELD) +public class ArticleContent { + private String content; + private String contentType; + + protected ArticleContent() { + } + + public ArticleContent(String content, String contentType) { + this.content = content; + this.contentType = contentType; + } + + public String getContent() { + return content; + } + + public String getContentType() { + return contentType; + } +} diff --git a/src/main/java/com/myshop/board/domain/ArticleRepository.java b/src/main/java/com/myshop/board/domain/ArticleRepository.java new file mode 100644 index 0000000..4b3831d --- /dev/null +++ b/src/main/java/com/myshop/board/domain/ArticleRepository.java @@ -0,0 +1,11 @@ +package com.myshop.board.domain; + +import org.springframework.data.repository.Repository; + +import java.util.Optional; + +public interface ArticleRepository extends Repository { + void save(Article article); + + Optional
findById(Long id); +} diff --git a/src/main/java/com/myshop/catalog/NoCategoryException.java b/src/main/java/com/myshop/catalog/NoCategoryException.java new file mode 100644 index 0000000..c0d9d2d --- /dev/null +++ b/src/main/java/com/myshop/catalog/NoCategoryException.java @@ -0,0 +1,4 @@ +package com.myshop.catalog; + +public class NoCategoryException extends RuntimeException { +} diff --git a/src/main/java/com/myshop/catalog/command/domain/category/Category.java b/src/main/java/com/myshop/catalog/command/domain/category/Category.java new file mode 100644 index 0000000..e7f0a65 --- /dev/null +++ b/src/main/java/com/myshop/catalog/command/domain/category/Category.java @@ -0,0 +1,32 @@ +package com.myshop.catalog.command.domain.category; + +import javax.persistence.Column; +import javax.persistence.EmbeddedId; +import javax.persistence.Entity; +import javax.persistence.Table; + +@Entity +@Table(name = "category") +public class Category { + @EmbeddedId + private CategoryId id; + + @Column(name = "name") + private String name; + + protected Category() { + } + + public Category(CategoryId id, String name) { + this.id = id; + this.name = name; + } + + public CategoryId getId() { + return id; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/com/myshop/catalog/command/domain/category/CategoryId.java b/src/main/java/com/myshop/catalog/command/domain/category/CategoryId.java new file mode 100644 index 0000000..a60530f --- /dev/null +++ b/src/main/java/com/myshop/catalog/command/domain/category/CategoryId.java @@ -0,0 +1,43 @@ +package com.myshop.catalog.command.domain.category; + +import javax.persistence.Access; +import javax.persistence.AccessType; +import javax.persistence.Column; +import javax.persistence.Embeddable; +import java.io.Serializable; +import java.util.Objects; + +@Embeddable +@Access(AccessType.FIELD) +public class CategoryId implements Serializable { + @Column(name = "category_id") + private Long value; + + protected CategoryId() { + } + + public CategoryId(Long value) { + this.value = value; + } + + public Long getValue() { + return value; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + CategoryId that = (CategoryId) o; + return Objects.equals(value, that.value); + } + + @Override + public int hashCode() { + return value != null ? value.hashCode() : 0; + } + + public static CategoryId of(Long value) { + return new CategoryId(value); + } +} diff --git a/src/main/java/com/myshop/catalog/command/domain/category/CategoryRepository.java b/src/main/java/com/myshop/catalog/command/domain/category/CategoryRepository.java new file mode 100644 index 0000000..05b411f --- /dev/null +++ b/src/main/java/com/myshop/catalog/command/domain/category/CategoryRepository.java @@ -0,0 +1,12 @@ +package com.myshop.catalog.command.domain.category; + +import org.springframework.data.repository.Repository; + +import java.util.List; +import java.util.Optional; + +public interface CategoryRepository extends Repository { + Optional findById(CategoryId id); + + List findAll(); +} diff --git a/src/main/java/com/myshop/catalog/command/domain/product/ExternalImage.java b/src/main/java/com/myshop/catalog/command/domain/product/ExternalImage.java new file mode 100644 index 0000000..68046cf --- /dev/null +++ b/src/main/java/com/myshop/catalog/command/domain/product/ExternalImage.java @@ -0,0 +1,30 @@ +package com.myshop.catalog.command.domain.product; + +import javax.persistence.DiscriminatorValue; +import javax.persistence.Entity; + +@Entity +@DiscriminatorValue("EI") +public class ExternalImage extends Image { + protected ExternalImage() { + } + + public ExternalImage(String path) { + super(path); + } + + @Override + public String getUrl() { + return getPath(); + } + + @Override + public boolean hasThumbnail() { + return false; + } + + @Override + public String getThumbnailUrl() { + return null; + } +} diff --git a/src/main/java/com/myshop/catalog/command/domain/product/Image.java b/src/main/java/com/myshop/catalog/command/domain/product/Image.java new file mode 100644 index 0000000..23d7f43 --- /dev/null +++ b/src/main/java/com/myshop/catalog/command/domain/product/Image.java @@ -0,0 +1,45 @@ +package com.myshop.catalog.command.domain.product; + +import javax.persistence.*; +import java.time.LocalDateTime; +import java.util.Date; + +@Entity +@Inheritance(strategy = InheritanceType.SINGLE_TABLE) +@DiscriminatorColumn(name = "image_type") +@Table(name = "image") +public abstract class Image { + @Id + @GeneratedValue(strategy = GenerationType.IDENTITY) + @Column(name = "image_id") + private Long id; + + @Column(name = "image_path") + private String path; + + @Column(name = "upload_time") + private LocalDateTime uploadTime; + + protected Image() { + } + + public Image(String path) { + this.path = path; + this.uploadTime = LocalDateTime.now(); + } + + protected String getPath() { + return path; + } + + public LocalDateTime getUploadTime() { + return uploadTime; + } + + public abstract String getUrl(); + + public abstract boolean hasThumbnail(); + + public abstract String getThumbnailUrl(); + +} diff --git a/src/main/java/com/myshop/catalog/command/domain/product/InternalImage.java b/src/main/java/com/myshop/catalog/command/domain/product/InternalImage.java new file mode 100644 index 0000000..0c416f0 --- /dev/null +++ b/src/main/java/com/myshop/catalog/command/domain/product/InternalImage.java @@ -0,0 +1,30 @@ +package com.myshop.catalog.command.domain.product; + +import javax.persistence.DiscriminatorValue; +import javax.persistence.Entity; + +@Entity +@DiscriminatorValue("II") +public class InternalImage extends Image { + protected InternalImage() { + } + + public InternalImage(String path) { + super(path); + } + + @Override + public String getUrl() { + return "/images/original/" + getPath(); + } + + @Override + public boolean hasThumbnail() { + return true; + } + + @Override + public String getThumbnailUrl() { + return "/images/thumbnail/" + getPath(); + } +} diff --git a/src/main/java/com/myshop/catalog/command/domain/product/Option.java b/src/main/java/com/myshop/catalog/command/domain/product/Option.java new file mode 100644 index 0000000..41d591e --- /dev/null +++ b/src/main/java/com/myshop/catalog/command/domain/product/Option.java @@ -0,0 +1,28 @@ +package com.myshop.catalog.command.domain.product; + +import javax.persistence.Column; +import javax.persistence.Embeddable; + +@Embeddable +public class Option { + @Column(name = "option_value") + private String value; + @Column(name = "option_title") + private String title; + + private Option() { + } + + public Option(String value, String title) { + this.value = value; + this.title = title; + } + + public String getValue() { + return value; + } + + public String getTitle() { + return title; + } +} diff --git a/src/main/java/com/myshop/catalog/command/domain/product/Product.java b/src/main/java/com/myshop/catalog/command/domain/product/Product.java new file mode 100644 index 0000000..077d5d7 --- /dev/null +++ b/src/main/java/com/myshop/catalog/command/domain/product/Product.java @@ -0,0 +1,77 @@ +package com.myshop.catalog.command.domain.product; + +import com.myshop.catalog.command.domain.category.CategoryId; +import com.myshop.common.jpa.MoneyConverter; +import com.myshop.common.model.Money; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +@Entity +@Table(name = "product") +public class Product { + @EmbeddedId + private ProductId id; + + @ElementCollection(fetch = FetchType.LAZY) + @CollectionTable(name = "product_category", + joinColumns = @JoinColumn(name = "product_id")) + private Set categoryIds; + + private String name; + + @Convert(converter = MoneyConverter.class) + private Money price; + + private String detail; + + @OneToMany(cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, + orphanRemoval = true, fetch = FetchType.LAZY) + @JoinColumn(name = "product_id") + @OrderColumn(name = "list_idx") + private List images = new ArrayList<>(); + + protected Product() { + } + + public Product(ProductId id, String name, Money price, String detail, List images) { + this.id = id; + this.name = name; + this.price = price; + this.detail = detail; + this.images.addAll(images); + } + + public ProductId getId() { + return id; + } + + public String getName() { + return name; + } + + public Money getPrice() { + return price; + } + + public String getDetail() { + return detail; + } + + public List getImages() { + return Collections.unmodifiableList(images); + } + + public void changeImages(List newImages) { + images.clear(); + images.addAll(newImages); + } + + public String getFirstIamgeThumbnailPath() { + if (images == null || images.isEmpty()) return null; + return images.get(0).getThumbnailUrl(); + } +} diff --git a/src/main/java/com/myshop/catalog/command/domain/product/ProductId.java b/src/main/java/com/myshop/catalog/command/domain/product/ProductId.java new file mode 100644 index 0000000..82b7559 --- /dev/null +++ b/src/main/java/com/myshop/catalog/command/domain/product/ProductId.java @@ -0,0 +1,43 @@ +package com.myshop.catalog.command.domain.product; + +import javax.persistence.Access; +import javax.persistence.AccessType; +import javax.persistence.Column; +import javax.persistence.Embeddable; +import java.io.Serializable; +import java.util.Objects; + +@Embeddable +@Access(AccessType.FIELD) +public class ProductId implements Serializable { + @Column(name = "product_id") + private String id; + + protected ProductId() { + } + + public ProductId(String id) { + this.id = id; + } + + public String getId() { + return id; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + ProductId productId = (ProductId) o; + return Objects.equals(id, productId.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + public static ProductId of(String id) { + return new ProductId(id); + } +} diff --git a/src/main/java/com/myshop/catalog/command/domain/product/ProductRepository.java b/src/main/java/com/myshop/catalog/command/domain/product/ProductRepository.java new file mode 100644 index 0000000..0bf9c58 --- /dev/null +++ b/src/main/java/com/myshop/catalog/command/domain/product/ProductRepository.java @@ -0,0 +1,14 @@ +package com.myshop.catalog.command.domain.product; + +import org.springframework.data.repository.Repository; + +import java.util.Optional; + +public interface ProductRepository extends Repository { + void save(Product product); + + Optional findById(ProductId id); + + void flush(); +} + diff --git a/src/main/java/com/myshop/catalog/query/category/CategoryData.java b/src/main/java/com/myshop/catalog/query/category/CategoryData.java new file mode 100644 index 0000000..fe38cab --- /dev/null +++ b/src/main/java/com/myshop/catalog/query/category/CategoryData.java @@ -0,0 +1,34 @@ +package com.myshop.catalog.query.category; + +import com.myshop.catalog.command.domain.category.CategoryId; + +import javax.persistence.Column; +import javax.persistence.EmbeddedId; +import javax.persistence.Entity; +import javax.persistence.Table; + +@Entity +@Table(name = "category") +public class CategoryData { + @EmbeddedId + private CategoryId id; + + @Column(name = "name") + private String name; + + protected CategoryData() { + } + + public CategoryData(CategoryId id, String name) { + this.id = id; + this.name = name; + } + + public CategoryId getId() { + return id; + } + + public String getName() { + return name; + } +} diff --git a/src/main/java/com/myshop/catalog/query/category/CategoryDataDao.java b/src/main/java/com/myshop/catalog/query/category/CategoryDataDao.java new file mode 100644 index 0000000..99ad1be --- /dev/null +++ b/src/main/java/com/myshop/catalog/query/category/CategoryDataDao.java @@ -0,0 +1,13 @@ +package com.myshop.catalog.query.category; + +import com.myshop.catalog.command.domain.category.CategoryId; +import org.springframework.data.repository.Repository; + +import java.util.List; +import java.util.Optional; + +public interface CategoryDataDao extends Repository { + Optional findById(CategoryId id); + + List findAll(); +} diff --git a/src/main/java/com/myshop/catalog/query/product/CategoryProduct.java b/src/main/java/com/myshop/catalog/query/product/CategoryProduct.java new file mode 100644 index 0000000..8c64985 --- /dev/null +++ b/src/main/java/com/myshop/catalog/query/product/CategoryProduct.java @@ -0,0 +1,53 @@ +package com.myshop.catalog.query.product; + +import com.myshop.catalog.query.category.CategoryData; + +import java.util.List; + +public class CategoryProduct { + private CategoryData category; + + private List items; + private int page; + private int size; + private long totalCount; + private int totalPages; + + public CategoryProduct(CategoryData category, + List items, + int page, + int size, + long totalCount, + int totalPages) { + this.category = category; + this.items = items; + this.page = page; + this.size = size; + this.totalCount = totalCount; + this.totalPages = totalPages; + } + + public CategoryData getCategory() { + return category; + } + + public List getItems() { + return items; + } + + public int getPage() { + return page; + } + + public int getSize() { + return size; + } + + public long getTotalCount() { + return totalCount; + } + + public int getTotalPages() { + return totalPages; + } +} diff --git a/src/main/java/com/myshop/catalog/query/product/ImageData.java b/src/main/java/com/myshop/catalog/query/product/ImageData.java new file mode 100644 index 0000000..4b1b6c9 --- /dev/null +++ b/src/main/java/com/myshop/catalog/query/product/ImageData.java @@ -0,0 +1,12 @@ +package com.myshop.catalog.query.product; + +import javax.persistence.Column; +import java.time.LocalDateTime; + +public class ImageData { + @Column(name = "image_path") + private String path; + + private LocalDateTime uploadTime; + +} diff --git a/src/main/java/com/myshop/catalog/query/product/ProductData.java b/src/main/java/com/myshop/catalog/query/product/ProductData.java new file mode 100644 index 0000000..9f85e98 --- /dev/null +++ b/src/main/java/com/myshop/catalog/query/product/ProductData.java @@ -0,0 +1,76 @@ +package com.myshop.catalog.query.product; + +import com.myshop.catalog.command.domain.category.CategoryId; +import com.myshop.catalog.command.domain.product.Image; +import com.myshop.catalog.command.domain.product.ProductId; +import com.myshop.common.jpa.MoneyConverter; +import com.myshop.common.model.Money; + +import javax.persistence.*; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.Set; + +@Entity +@Table(name = "product") +public class ProductData { + @EmbeddedId + private ProductId id; + + @ElementCollection + @CollectionTable(name = "product_category", + joinColumns = @JoinColumn(name = "product_id")) + private Set categoryIds; + + private String name; + + @Convert(converter = MoneyConverter.class) + private Money price; + + private String detail; + + // TODO 목록에서 사용할 것 + + @OneToMany(cascade = {CascadeType.PERSIST, CascadeType.REMOVE}, + orphanRemoval = true, fetch = FetchType.EAGER) + @JoinColumn(name = "product_id") + @OrderColumn(name = "list_idx") + private List images = new ArrayList<>(); + + protected ProductData() { + } + + public ProductData(ProductId id, String name, Money price, String detail, List images) { + this.id = id; + this.name = name; + this.price = price; + this.detail = detail; + this.images.addAll(images); + } + + public ProductId getId() { + return id; + } + + public String getName() { + return name; + } + + public Money getPrice() { + return price; + } + + public String getDetail() { + return detail; + } + + public List getImages() { + return Collections.unmodifiableList(images); + } + + public String getFirstIamgeThumbnailPath() { + if (images == null || images.isEmpty()) return null; + return images.get(0).getThumbnailUrl(); + } +} diff --git a/src/main/java/com/myshop/catalog/query/product/ProductDataDao.java b/src/main/java/com/myshop/catalog/query/product/ProductDataDao.java new file mode 100644 index 0000000..385aee3 --- /dev/null +++ b/src/main/java/com/myshop/catalog/query/product/ProductDataDao.java @@ -0,0 +1,15 @@ +package com.myshop.catalog.query.product; + +import com.myshop.catalog.command.domain.category.CategoryId; +import com.myshop.catalog.command.domain.product.ProductId; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.repository.Repository; + +import java.util.Optional; + +public interface ProductDataDao extends Repository { + Optional findById(ProductId id); + + Page findByCategoryIdsContains(CategoryId id, Pageable pageable); +} diff --git a/src/main/java/com/myshop/catalog/query/product/ProductQueryService.java b/src/main/java/com/myshop/catalog/query/product/ProductQueryService.java new file mode 100644 index 0000000..2b6e458 --- /dev/null +++ b/src/main/java/com/myshop/catalog/query/product/ProductQueryService.java @@ -0,0 +1,55 @@ +package com.myshop.catalog.query.product; + +import com.myshop.catalog.NoCategoryException; +import com.myshop.catalog.command.domain.category.CategoryId; +import com.myshop.catalog.command.domain.product.ProductId; +import com.myshop.catalog.query.category.CategoryData; +import com.myshop.catalog.query.category.CategoryDataDao; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; + +import static java.util.stream.Collectors.toList; + +@Service +public class ProductQueryService { + private ProductDataDao productDataDao; + private CategoryDataDao categoryDataDao; + + public ProductQueryService(ProductDataDao productDataDao, + CategoryDataDao categoryDataDao) { + this.productDataDao = productDataDao; + this.categoryDataDao = categoryDataDao; + } + + @Transactional + public CategoryProduct getProductInCategory(Long categoryId, int page, int size) { + CategoryData category = categoryDataDao.findById(new CategoryId(categoryId)) + .orElseThrow(() -> new NoCategoryException()); + + Page productPage = productDataDao.findByCategoryIdsContains(category.getId(), Pageable.ofSize(size).withPage(page - 1)); + return new CategoryProduct(category, + toSummary(productPage.getContent()), + page, + productPage.getSize(), + productPage.getTotalElements(), + productPage.getTotalPages()); + } + + private List toSummary(List products) { + return products.stream().map( + prod -> new ProductSummary( + prod.getId().getId(), + prod.getName(), + prod.getPrice().getValue(), + prod.getFirstIamgeThumbnailPath())).collect(toList()); + } + + public Optional getProduct(String productId) { + return productDataDao.findById(new ProductId(productId)); + } +} diff --git a/src/main/java/com/myshop/catalog/query/product/ProductSummary.java b/src/main/java/com/myshop/catalog/query/product/ProductSummary.java new file mode 100644 index 0000000..2d79382 --- /dev/null +++ b/src/main/java/com/myshop/catalog/query/product/ProductSummary.java @@ -0,0 +1,31 @@ +package com.myshop.catalog.query.product; + +public class ProductSummary { + private String id; + private String name; + private int price; + private String image; + + public ProductSummary(String productId, String name, int price, String image) { + this.id = productId; + this.name = name; + this.price = price; + this.image = image; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public int getPrice() { + return price; + } + + public String getImage() { + return image; + } +} diff --git a/src/main/java/com/myshop/catalog/ui/ProductController.java b/src/main/java/com/myshop/catalog/ui/ProductController.java new file mode 100644 index 0000000..89fef78 --- /dev/null +++ b/src/main/java/com/myshop/catalog/ui/ProductController.java @@ -0,0 +1,59 @@ +package com.myshop.catalog.ui; + +import com.myshop.catalog.query.category.CategoryData; +import com.myshop.catalog.query.category.CategoryDataDao; +import com.myshop.catalog.query.product.CategoryProduct; +import com.myshop.catalog.query.product.ProductData; +import com.myshop.catalog.query.product.ProductQueryService; +import org.springframework.stereotype.Controller; +import org.springframework.ui.ModelMap; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestParam; + +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.util.List; +import java.util.Optional; + +@Controller +public class ProductController { + private CategoryDataDao categoryDataDao; + private ProductQueryService productQueryService; + + public ProductController(CategoryDataDao categoryDataDao, + ProductQueryService productQueryService) { + this.categoryDataDao = categoryDataDao; + this.productQueryService = productQueryService; + } + + @RequestMapping("/categories") + public String categories(ModelMap model) { + List categories = categoryDataDao.findAll(); + model.addAttribute("categories", categories); + return "category/categoryList"; + } + + @RequestMapping("/categories/{categoryId}") + public String list(@PathVariable("categoryId") Long categoryId, + @RequestParam(name = "page", required = false, defaultValue = "1") int page, + ModelMap model) { + CategoryProduct productInCategory = productQueryService.getProductInCategory(categoryId, page, 10); + model.addAttribute("productInCategory", productInCategory); + return "category/productList"; + } + + @RequestMapping("/products/{productId}") + public String detail(@PathVariable("productId") String productId, + ModelMap model, + HttpServletResponse response) throws IOException { + Optional product = productQueryService.getProduct(productId); + if (product.isPresent()) { + model.addAttribute("product", product.get()); + return "category/productDetail"; + } else { + response.sendError(HttpServletResponse.SC_NOT_FOUND); + return null; + } + } +} diff --git a/src/main/java/com/myshop/common/ValidationError.java b/src/main/java/com/myshop/common/ValidationError.java new file mode 100644 index 0000000..627a728 --- /dev/null +++ b/src/main/java/com/myshop/common/ValidationError.java @@ -0,0 +1,31 @@ +package com.myshop.common; + +public class ValidationError { + private String name; + private String code; + + public ValidationError(String name, String code) { + this.name = name; + this.code = code; + } + + public String getName() { + return name; + } + + public String getCode() { + return code; + } + + public boolean hasName() { + return name != null; + } + + public static ValidationError of(String code) { + return new ValidationError(null, code); + } + + public static ValidationError of(String name, String code) { + return new ValidationError(name, code); + } +} diff --git a/src/main/java/com/myshop/common/ValidationErrorException.java b/src/main/java/com/myshop/common/ValidationErrorException.java new file mode 100644 index 0000000..da781e1 --- /dev/null +++ b/src/main/java/com/myshop/common/ValidationErrorException.java @@ -0,0 +1,15 @@ +package com.myshop.common; + +import java.util.List; + +public class ValidationErrorException extends RuntimeException { + private List errors; + + public ValidationErrorException(List errors) { + this.errors = errors; + } + + public List getErrors() { + return errors; + } +} diff --git a/src/main/java/com/myshop/common/VersionConflictException.java b/src/main/java/com/myshop/common/VersionConflictException.java new file mode 100644 index 0000000..20fdb1f --- /dev/null +++ b/src/main/java/com/myshop/common/VersionConflictException.java @@ -0,0 +1,4 @@ +package com.myshop.common; + +public class VersionConflictException extends RuntimeException { +} diff --git a/src/main/java/com/myshop/common/event/Event.java b/src/main/java/com/myshop/common/event/Event.java new file mode 100644 index 0000000..f0739fc --- /dev/null +++ b/src/main/java/com/myshop/common/event/Event.java @@ -0,0 +1,14 @@ +package com.myshop.common.event; + +public abstract class Event { + private long timestamp; + + public Event() { + this.timestamp = System.currentTimeMillis(); + } + + public long getTimestamp() { + return timestamp; + } + +} diff --git a/src/main/java/com/myshop/common/event/EventStoreHandler.java b/src/main/java/com/myshop/common/event/EventStoreHandler.java new file mode 100644 index 0000000..8b41834 --- /dev/null +++ b/src/main/java/com/myshop/common/event/EventStoreHandler.java @@ -0,0 +1,19 @@ +package com.myshop.common.event; + +import com.myshop.eventstore.api.EventStore; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +@Component +public class EventStoreHandler { + private EventStore eventStore; + + public EventStoreHandler(EventStore eventStore) { + this.eventStore = eventStore; + } + + @EventListener(Event.class) + public void handle(Event event) { + eventStore.save(event); + } +} diff --git a/src/main/java/com/myshop/common/event/Events.java b/src/main/java/com/myshop/common/event/Events.java new file mode 100644 index 0000000..3b18c86 --- /dev/null +++ b/src/main/java/com/myshop/common/event/Events.java @@ -0,0 +1,17 @@ +package com.myshop.common.event; + +import org.springframework.context.ApplicationEventPublisher; + +public class Events { + private static ApplicationEventPublisher publisher; + + static void setPublisher(ApplicationEventPublisher publisher) { + Events.publisher = publisher; + } + + public static void raise(Object event) { + if (publisher != null) { + publisher.publishEvent(event); + } + } +} diff --git a/src/main/java/com/myshop/common/event/EventsConfiguration.java b/src/main/java/com/myshop/common/event/EventsConfiguration.java new file mode 100644 index 0000000..1d48729 --- /dev/null +++ b/src/main/java/com/myshop/common/event/EventsConfiguration.java @@ -0,0 +1,18 @@ +package com.myshop.common.event; + +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.ApplicationContext; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class EventsConfiguration { + @Autowired + private ApplicationContext applicationContext; + + @Bean + public InitializingBean eventsInitializer() { + return () -> Events.setPublisher(applicationContext); + } +} diff --git a/src/main/java/com/myshop/common/jpa/EmailSetConverter.java b/src/main/java/com/myshop/common/jpa/EmailSetConverter.java new file mode 100644 index 0000000..c099369 --- /dev/null +++ b/src/main/java/com/myshop/common/jpa/EmailSetConverter.java @@ -0,0 +1,31 @@ +package com.myshop.common.jpa; + +import com.myshop.common.model.Email; +import com.myshop.common.model.EmailSet; + +import javax.persistence.AttributeConverter; +import java.util.Arrays; +import java.util.Set; +import java.util.stream.Collectors; + +import static java.util.stream.Collectors.toSet; + +public class EmailSetConverter implements AttributeConverter { + @Override + public String convertToDatabaseColumn(EmailSet attribute) { + if (attribute == null) return null; + return attribute.getEmails().stream() + .map(email -> email.getAddress()) + .collect(Collectors.joining(",")); + } + + @Override + public EmailSet convertToEntityAttribute(String dbData) { + if (dbData == null) return null; + String[] emails = dbData.split(","); + Set emailSet = Arrays.stream(emails) + .map(value -> new Email(value)) + .collect(toSet()); + return new EmailSet(emailSet); + } +} diff --git a/src/main/java/com/myshop/common/jpa/MoneyConverter.java b/src/main/java/com/myshop/common/jpa/MoneyConverter.java new file mode 100644 index 0000000..4eb38fe --- /dev/null +++ b/src/main/java/com/myshop/common/jpa/MoneyConverter.java @@ -0,0 +1,18 @@ +package com.myshop.common.jpa; + +import com.myshop.common.model.Money; + +import javax.persistence.AttributeConverter; + +public class MoneyConverter implements AttributeConverter { + + @Override + public Integer convertToDatabaseColumn(Money money) { + return money == null ? null : money.getValue(); + } + + @Override + public Money convertToEntityAttribute(Integer value) { + return value == null ? null : new Money(value); + } +} diff --git a/src/main/java/com/myshop/common/jpa/Rangeable.java b/src/main/java/com/myshop/common/jpa/Rangeable.java new file mode 100644 index 0000000..41edfac --- /dev/null +++ b/src/main/java/com/myshop/common/jpa/Rangeable.java @@ -0,0 +1,31 @@ +package com.myshop.common.jpa; + +import org.springframework.data.domain.Sort; + +public class Rangeable { + private int start; + private int limit; + private Sort sort; + + public Rangeable(int start, int limit, Sort sort) { + this.start = start; + this.limit = limit; + this.sort = sort; + } + + public int getStart() { + return start; + } + + public int getLimit() { + return limit; + } + + public Sort getSort() { + return sort; + } + + public static Rangeable of(int start, int limit) { + return new Rangeable(start, limit, Sort.unsorted()); + } +} \ No newline at end of file diff --git a/src/main/java/com/myshop/common/jpa/RangeableExecutor.java b/src/main/java/com/myshop/common/jpa/RangeableExecutor.java new file mode 100644 index 0000000..f9424ec --- /dev/null +++ b/src/main/java/com/myshop/common/jpa/RangeableExecutor.java @@ -0,0 +1,9 @@ +package com.myshop.common.jpa; + +import org.springframework.data.jpa.domain.Specification; + +import java.util.List; + +public interface RangeableExecutor { + List getRange(Specification spec, Rangeable rangeable); +} \ No newline at end of file diff --git a/src/main/java/com/myshop/common/jpa/RangeableRepository.java b/src/main/java/com/myshop/common/jpa/RangeableRepository.java new file mode 100644 index 0000000..fe487f8 --- /dev/null +++ b/src/main/java/com/myshop/common/jpa/RangeableRepository.java @@ -0,0 +1,11 @@ +package com.myshop.common.jpa; + +import org.springframework.data.repository.NoRepositoryBean; +import org.springframework.data.repository.Repository; + +import java.io.Serializable; + +@NoRepositoryBean +public interface RangeableRepository + extends Repository, RangeableExecutor { +} \ No newline at end of file diff --git a/src/main/java/com/myshop/common/jpa/RangeableRepositoryImpl.java b/src/main/java/com/myshop/common/jpa/RangeableRepositoryImpl.java new file mode 100644 index 0000000..bd88198 --- /dev/null +++ b/src/main/java/com/myshop/common/jpa/RangeableRepositoryImpl.java @@ -0,0 +1,32 @@ +package com.myshop.common.jpa; + +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.repository.support.JpaEntityInformation; +import org.springframework.data.jpa.repository.support.SimpleJpaRepository; + +import javax.persistence.EntityManager; +import javax.persistence.TypedQuery; +import java.io.Serializable; +import java.util.List; + +public class RangeableRepositoryImpl + extends SimpleJpaRepository + implements RangeableRepository { + + public RangeableRepositoryImpl( + JpaEntityInformation entityInformation, + EntityManager entityManager) { + super(entityInformation, entityManager); + } + + @Override + public List getRange(Specification spec, Rangeable rangeable) { + TypedQuery query = getQuery( + spec, getDomainClass(), rangeable.getSort()); + + query.setFirstResult(rangeable.getStart()); + query.setMaxResults(rangeable.getLimit()); + + return query.getResultList(); + } +} \ No newline at end of file diff --git a/src/main/java/com/myshop/common/jpa/SpecBuilder.java b/src/main/java/com/myshop/common/jpa/SpecBuilder.java new file mode 100644 index 0000000..e514263 --- /dev/null +++ b/src/main/java/com/myshop/common/jpa/SpecBuilder.java @@ -0,0 +1,48 @@ +package com.myshop.common.jpa; + +import org.springframework.data.jpa.domain.Specification; +import org.springframework.util.StringUtils; + +import java.util.ArrayList; +import java.util.List; +import java.util.function.Function; +import java.util.function.Supplier; + +public class SpecBuilder { + public static Builder builder(Class type) { + return new Builder(); + } + + public static class Builder { + private List> specs = new ArrayList<>(); + + public Builder and(Specification spec) { + specs.add(spec); + return this; + } + + public Builder ifHasText(String str, + Function> specSupplier) { + if (StringUtils.hasText(str)) { + specs.add(specSupplier.apply(str)); + } + return this; + } + + public Builder ifTrue(Boolean cond, + Supplier> specSupplier) { + if (cond != null && cond.booleanValue()) { + specs.add(specSupplier.get()); + } + return this; + } + + public Specification toSpec() { + Specification spec = Specification.where(null); + for (Specification s : specs) { + spec = spec.and(s); + } + return spec; + } + } +} diff --git a/src/main/java/com/myshop/common/model/Address.java b/src/main/java/com/myshop/common/model/Address.java new file mode 100644 index 0000000..69df2f7 --- /dev/null +++ b/src/main/java/com/myshop/common/model/Address.java @@ -0,0 +1,37 @@ +package com.myshop.common.model; + +import javax.persistence.Column; +import javax.persistence.Embeddable; + +@Embeddable +public class Address { + @Column(name = "zip_code") + private String zipCode; + + @Column(name = "address1") + private String address1; + + @Column(name = "address2") + private String address2; + + public Address() { + } + + public Address(String zipCode, String address1, String address2) { + this.zipCode = zipCode; + this.address1 = address1; + this.address2 = address2; + } + + public String getZipCode() { + return zipCode; + } + + public String getAddress1() { + return address1; + } + + public String getAddress2() { + return address2; + } +} diff --git a/src/main/java/com/myshop/common/model/Email.java b/src/main/java/com/myshop/common/model/Email.java new file mode 100644 index 0000000..5f01166 --- /dev/null +++ b/src/main/java/com/myshop/common/model/Email.java @@ -0,0 +1,32 @@ +package com.myshop.common.model; + +import java.util.Objects; + +public class Email { + private String address; + + public Email(String address) { + this.address = address; + } + + public String getAddress() { + return address; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Email email = (Email) o; + return Objects.equals(address, email.address); + } + + @Override + public int hashCode() { + return Objects.hash(address); + } + + public static Email of(String address) { + return new Email(address); + } +} diff --git a/src/main/java/com/myshop/common/model/EmailSet.java b/src/main/java/com/myshop/common/model/EmailSet.java new file mode 100644 index 0000000..7b720b8 --- /dev/null +++ b/src/main/java/com/myshop/common/model/EmailSet.java @@ -0,0 +1,18 @@ +package com.myshop.common.model; + +import java.util.Collections; +import java.util.HashSet; +import java.util.Set; + +public class EmailSet { + private Set emails = new HashSet<>(); + + public EmailSet(Set emails) { + this.emails.addAll(emails); + } + + public Set getEmails() { + return Collections.unmodifiableSet(emails); + } + +} diff --git a/src/main/java/com/myshop/common/model/Money.java b/src/main/java/com/myshop/common/model/Money.java new file mode 100644 index 0000000..da91b02 --- /dev/null +++ b/src/main/java/com/myshop/common/model/Money.java @@ -0,0 +1,38 @@ +package com.myshop.common.model; + +import java.util.Objects; + +public class Money { + + private int value; + + public Money(int value) { + this.value = value; + } + + public int getValue() { + return value; + } + + public Money multiply(int multiplier) { + return new Money(value * multiplier); + } + + @Override + public String toString() { + return Integer.toString(value); + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + Money money = (Money) o; + return value == money.value; + } + + @Override + public int hashCode() { + return Objects.hash(value); + } +} diff --git a/src/main/java/com/myshop/common/ui/Pagination.java b/src/main/java/com/myshop/common/ui/Pagination.java new file mode 100644 index 0000000..6f50608 --- /dev/null +++ b/src/main/java/com/myshop/common/ui/Pagination.java @@ -0,0 +1,65 @@ +package com.myshop.common.ui; + +public class Pagination { + private int current; + private int beginPage; + private int endPage; + private int numberOfPage; + private int totalPages; + + public Pagination(int current, int totalPages, int numberOfPage) { + this.current = current; + this.totalPages = totalPages; + this.numberOfPage = numberOfPage; + int pageMod = current % 5; + beginPage = pageMod == 0 ? current - 5 + 1 : current - pageMod + 1; + endPage = beginPage + 5 - 1; + if (endPage > totalPages) { + endPage = totalPages; + } + } + + public int getCurrent() { + return current; + } + + public int getTotalPages() { + return totalPages; + } + + public int getBeginPage() { + return beginPage; + } + + public int getEndPage() { + return endPage; + } + + public int getNumberOfPage() { + return numberOfPage; + } + + public boolean isHasPrevious() { + return beginPage > 1; + } + + public int getPreviousBeginPage() { + return beginPage - numberOfPage; + } + + public int getNextBeginPage() { + return beginPage + numberOfPage; + } + + public boolean isHasNext() { + return endPage < totalPages; + } + + public int[] getPageNos() { + int[] result = new int[endPage - beginPage + 1]; + for (int i = beginPage ; i <= endPage ; i++) { + result[i - beginPage] = i; + } + return result; + } +} diff --git a/src/main/java/com/myshop/eventstore/api/EventEntry.java b/src/main/java/com/myshop/eventstore/api/EventEntry.java new file mode 100644 index 0000000..3fef163 --- /dev/null +++ b/src/main/java/com/myshop/eventstore/api/EventEntry.java @@ -0,0 +1,45 @@ +package com.myshop.eventstore.api; + +public class EventEntry { + private Long id; + private String type; + private String contentType; + private String payload; + private long timestamp; + + public EventEntry(String type, String contentType, String payload) { + this.type = type; + this.contentType = contentType; + this.payload = payload; + this.timestamp = System.currentTimeMillis(); + } + + public EventEntry(Long id, String type, String contentType, String payload, + long timestamp) { + this.id = id; + this.type = type; + this.contentType = contentType; + this.payload = payload; + this.timestamp = timestamp; + } + + public Long getId() { + return id; + } + + public String getType() { + return type; + } + + public String getContentType() { + return contentType; + } + + public String getPayload() { + return payload; + } + + public long getTimestamp() { + return timestamp; + } +} diff --git a/src/main/java/com/myshop/eventstore/api/EventStore.java b/src/main/java/com/myshop/eventstore/api/EventStore.java new file mode 100644 index 0000000..8c02e6d --- /dev/null +++ b/src/main/java/com/myshop/eventstore/api/EventStore.java @@ -0,0 +1,9 @@ +package com.myshop.eventstore.api; + +import java.util.List; + +public interface EventStore { + void save(Object event); + + List get(long offset, long limit); +} diff --git a/src/main/java/com/myshop/eventstore/api/PayloadConvertException.java b/src/main/java/com/myshop/eventstore/api/PayloadConvertException.java new file mode 100644 index 0000000..0a89f8f --- /dev/null +++ b/src/main/java/com/myshop/eventstore/api/PayloadConvertException.java @@ -0,0 +1,7 @@ +package com.myshop.eventstore.api; + +public class PayloadConvertException extends RuntimeException { + public PayloadConvertException(Exception e) { + super(e); + } +} diff --git a/src/main/java/com/myshop/eventstore/infra/JdbcEventStore.java b/src/main/java/com/myshop/eventstore/infra/JdbcEventStore.java new file mode 100644 index 0000000..85ce292 --- /dev/null +++ b/src/main/java/com/myshop/eventstore/infra/JdbcEventStore.java @@ -0,0 +1,66 @@ +package com.myshop.eventstore.infra; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.myshop.eventstore.api.EventEntry; +import com.myshop.eventstore.api.EventStore; +import com.myshop.eventstore.api.PayloadConvertException; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; + +import java.sql.Timestamp; +import java.util.List; + +@Component +public class JdbcEventStore implements EventStore { + private ObjectMapper objectMapper; + private JdbcTemplate jdbcTemplate; + + public JdbcEventStore(ObjectMapper objectMapper, JdbcTemplate jdbcTemplate) { + this.objectMapper = objectMapper; + this.jdbcTemplate = jdbcTemplate; + } + + @Override + public void save(Object event) { + EventEntry entry = new EventEntry(event.getClass().getName(), + "application/json", toJson(event)); + jdbcTemplate.update( + "insert into evententry " + + "(type, content_type, payload, timestamp) " + + "values (?, ?, ?, ?)", + ps -> { + ps.setString(1, entry.getType()); + ps.setString(2, entry.getContentType()); + ps.setString(3, entry.getPayload()); + ps.setTimestamp(4, new Timestamp(entry.getTimestamp())); + }); + } + + private String toJson(Object event) { + try { + return objectMapper.writeValueAsString(event); + } catch (JsonProcessingException e) { + throw new PayloadConvertException(e); + } + } + + @Override + public List get(long offset, long limit) { + return jdbcTemplate.query( + "select * from evententry order by id asc limit ?, ?", + ps -> { + ps.setLong(1, offset); + ps.setLong(2, limit); + }, + (rs, rowNum) -> { + return new EventEntry( + rs.getLong("id"), + rs.getString("type"), + rs.getString("content_type"), + rs.getString("payload"), + rs.getTimestamp("timestamp").getTime()); + }); + } +} \ No newline at end of file diff --git a/src/main/java/com/myshop/eventstore/ui/EventApi.java b/src/main/java/com/myshop/eventstore/ui/EventApi.java new file mode 100644 index 0000000..726888a --- /dev/null +++ b/src/main/java/com/myshop/eventstore/ui/EventApi.java @@ -0,0 +1,26 @@ +package com.myshop.eventstore.ui; + +import com.myshop.eventstore.api.EventEntry; +import com.myshop.eventstore.api.EventStore; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RequestMethod; +import org.springframework.web.bind.annotation.RequestParam; +import org.springframework.web.bind.annotation.RestController; + +import java.util.List; + +@RestController +public class EventApi { + private EventStore eventStore; + + public EventApi(EventStore eventStore) { + this.eventStore = eventStore; + } + + @RequestMapping(value = "/api/events", method = RequestMethod.GET) + public List list( + @RequestParam("offset") Long offset, + @RequestParam("limit") Long limit) { + return eventStore.get(offset, limit); + } +} diff --git a/src/main/java/com/myshop/integration/EventForwarder.java b/src/main/java/com/myshop/integration/EventForwarder.java new file mode 100644 index 0000000..7cc2f9b --- /dev/null +++ b/src/main/java/com/myshop/integration/EventForwarder.java @@ -0,0 +1,60 @@ +package com.myshop.integration; + +import com.myshop.eventstore.api.EventEntry; +import com.myshop.eventstore.api.EventStore; +import org.springframework.scheduling.annotation.Scheduled; +import org.springframework.stereotype.Component; + +import java.util.List; + +@Component +public class EventForwarder { + private static final int DEFAULT_LIMIT_SIZE = 100; + + private EventStore eventStore; + private OffsetStore offsetStore; + private EventSender eventSender; + private int limitSize = DEFAULT_LIMIT_SIZE; + + public EventForwarder(EventStore eventStore, + OffsetStore offsetStore, + EventSender eventSender) { + this.eventStore = eventStore; + this.offsetStore = offsetStore; + this.eventSender = eventSender; + } + + @Scheduled(initialDelay = 1000L, fixedDelay = 1000L) + public void getAndSend() { + long nextOffset = getNextOffset(); + List events = eventStore.get(nextOffset, limitSize); + if (!events.isEmpty()) { + int processedCount = sendEvent(events); + if (processedCount > 0) { + saveNextOffset(nextOffset + processedCount); + } + } + } + + private long getNextOffset() { + return offsetStore.get(); + } + + private int sendEvent(List events) { + int processedCount = 0; + try { + for (EventEntry entry : events) { + eventSender.send(entry); + processedCount++; + } + } catch(Exception ex) { + // 로깅 처리 + } + return processedCount; + } + + private void saveNextOffset(long nextOffset) { + offsetStore.update(nextOffset); + } + +} diff --git a/src/main/java/com/myshop/integration/EventSender.java b/src/main/java/com/myshop/integration/EventSender.java new file mode 100644 index 0000000..1da310f --- /dev/null +++ b/src/main/java/com/myshop/integration/EventSender.java @@ -0,0 +1,7 @@ +package com.myshop.integration; + +import com.myshop.eventstore.api.EventEntry; + +public interface EventSender { + void send(EventEntry event); +} diff --git a/src/main/java/com/myshop/integration/OffsetStore.java b/src/main/java/com/myshop/integration/OffsetStore.java new file mode 100644 index 0000000..6a58691 --- /dev/null +++ b/src/main/java/com/myshop/integration/OffsetStore.java @@ -0,0 +1,6 @@ +package com.myshop.integration; + +public interface OffsetStore { + long get(); + void update(long nextOffset); +} diff --git a/src/main/java/com/myshop/integration/infra/MemoryOffsetStore.java b/src/main/java/com/myshop/integration/infra/MemoryOffsetStore.java new file mode 100644 index 0000000..4641999 --- /dev/null +++ b/src/main/java/com/myshop/integration/infra/MemoryOffsetStore.java @@ -0,0 +1,19 @@ +package com.myshop.integration.infra; + +import com.myshop.integration.OffsetStore; +import org.springframework.stereotype.Component; + +@Component +public class MemoryOffsetStore implements OffsetStore { + private long nextOffset = 0; + + @Override + public long get() { + return nextOffset; + } + + @Override + public void update(long nextOffset) { + this.nextOffset = nextOffset; + } +} diff --git a/src/main/java/com/myshop/integration/infra/SysoutEventSender.java b/src/main/java/com/myshop/integration/infra/SysoutEventSender.java new file mode 100644 index 0000000..eb11e3f --- /dev/null +++ b/src/main/java/com/myshop/integration/infra/SysoutEventSender.java @@ -0,0 +1,13 @@ +package com.myshop.integration.infra; + +import com.myshop.eventstore.api.EventEntry; +import com.myshop.integration.EventSender; +import org.springframework.stereotype.Component; + +@Component +public class SysoutEventSender implements EventSender { + @Override + public void send(EventEntry event) { + System.out.println("EventSender send event : " + event); + } +} diff --git a/src/main/java/com/myshop/lock/AlreadyLockedException.java b/src/main/java/com/myshop/lock/AlreadyLockedException.java new file mode 100644 index 0000000..808777d --- /dev/null +++ b/src/main/java/com/myshop/lock/AlreadyLockedException.java @@ -0,0 +1,4 @@ +package com.myshop.lock; + +public class AlreadyLockedException extends LockException { +} diff --git a/src/main/java/com/myshop/lock/LockData.java b/src/main/java/com/myshop/lock/LockData.java new file mode 100644 index 0000000..b293a97 --- /dev/null +++ b/src/main/java/com/myshop/lock/LockData.java @@ -0,0 +1,35 @@ +package com.myshop.lock; + +public class LockData { + private String type; + private String id; + private String lockId; + private long timestamp; + + public LockData(String type, String id, String lockId, long timestamp) { + this.type = type; + this.id = id; + this.lockId = lockId; + this.timestamp = timestamp; + } + + public String getType() { + return type; + } + + public String getId() { + return id; + } + + public String getLockId() { + return lockId; + } + + public long getTimestamp() { + return timestamp; + } + + public boolean isExpired() { + return timestamp < System.currentTimeMillis(); + } +} \ No newline at end of file diff --git a/src/main/java/com/myshop/lock/LockException.java b/src/main/java/com/myshop/lock/LockException.java new file mode 100644 index 0000000..ec042ab --- /dev/null +++ b/src/main/java/com/myshop/lock/LockException.java @@ -0,0 +1,14 @@ +package com.myshop.lock; + +public class LockException extends RuntimeException { + public LockException() { + } + + public LockException(String message) { + super(message); + } + + public LockException(Throwable cause) { + super(cause); + } +} diff --git a/src/main/java/com/myshop/lock/LockId.java b/src/main/java/com/myshop/lock/LockId.java new file mode 100644 index 0000000..8cace86 --- /dev/null +++ b/src/main/java/com/myshop/lock/LockId.java @@ -0,0 +1,13 @@ +package com.myshop.lock; + +public class LockId { + private String value; + + public LockId(String value) { + this.value = value; + } + + public String getValue() { + return value; + } +} diff --git a/src/main/java/com/myshop/lock/LockManager.java b/src/main/java/com/myshop/lock/LockManager.java new file mode 100644 index 0000000..b148d99 --- /dev/null +++ b/src/main/java/com/myshop/lock/LockManager.java @@ -0,0 +1,11 @@ +package com.myshop.lock; + +public interface LockManager { + LockId tryLock(String type, String id) throws LockException; + + void checkLock(LockId lockId) throws LockException; + + void releaseLock(LockId lockId) throws LockException; + + void extendLockExpiration(LockId lockId, long inc) throws LockException; +} diff --git a/src/main/java/com/myshop/lock/LockManagerException.java b/src/main/java/com/myshop/lock/LockManagerException.java new file mode 100644 index 0000000..79fccd5 --- /dev/null +++ b/src/main/java/com/myshop/lock/LockManagerException.java @@ -0,0 +1,7 @@ +package com.myshop.lock; + +public class LockManagerException extends RuntimeException { + public LockManagerException(Exception cause) { + super(cause); + } +} diff --git a/src/main/java/com/myshop/lock/LockingFailException.java b/src/main/java/com/myshop/lock/LockingFailException.java new file mode 100644 index 0000000..377af4b --- /dev/null +++ b/src/main/java/com/myshop/lock/LockingFailException.java @@ -0,0 +1,10 @@ +package com.myshop.lock; + +public class LockingFailException extends LockException { + public LockingFailException() { + } + + public LockingFailException(Exception cause) { + super(cause); + } +} diff --git a/src/main/java/com/myshop/lock/NoLockException.java b/src/main/java/com/myshop/lock/NoLockException.java new file mode 100644 index 0000000..112651d --- /dev/null +++ b/src/main/java/com/myshop/lock/NoLockException.java @@ -0,0 +1,4 @@ +package com.myshop.lock; + +public class NoLockException extends RuntimeException { +} diff --git a/src/main/java/com/myshop/lock/SpringLockManager.java b/src/main/java/com/myshop/lock/SpringLockManager.java new file mode 100644 index 0000000..f8da1a0 --- /dev/null +++ b/src/main/java/com/myshop/lock/SpringLockManager.java @@ -0,0 +1,108 @@ +package com.myshop.lock; + +import org.springframework.dao.DuplicateKeyException; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Propagation; +import org.springframework.transaction.annotation.Transactional; + +import java.sql.Timestamp; +import java.util.List; +import java.util.Optional; +import java.util.UUID; + +@Component +public class SpringLockManager implements LockManager { + private int lockTimeout = 5 * 60 * 1000; + private JdbcTemplate jdbcTemplate; + + private RowMapper lockDataRowMapper = (rs, rowNum) -> + new LockData(rs.getString(1), rs.getString(2), + rs.getString(3), rs.getTimestamp(4).getTime()); + + public SpringLockManager(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + @Override + public LockId tryLock(String type, String id) throws LockException { + checkAlreadyLocked(type, id); + LockId lockId = new LockId(UUID.randomUUID().toString()); + locking(type, id, lockId); + return lockId; + } + + private void checkAlreadyLocked(String type, String id) { + List locks = jdbcTemplate.query( + "select * from locks where type = ? and id = ?", + lockDataRowMapper, type, id); + Optional lockData = handleExpiration(locks); + if (lockData.isPresent()) throw new AlreadyLockedException(); + } + + private Optional handleExpiration(List locks) { + if (locks.isEmpty()) return Optional.empty(); + LockData lockData = locks.get(0); + if (lockData.isExpired()) { + jdbcTemplate.update( + "delete from locks where type = ? and id = ?", + lockData.getType(), lockData.getId()); + return Optional.empty(); + } else { + return Optional.of(lockData); + } + } + + private void locking(String type, String id, LockId lockId) { + try { + int updatedCount = jdbcTemplate.update( + "insert into locks values (?, ?, ?, ?)", + type, id, lockId.getValue(), new Timestamp(getExpirationTime())); + if (updatedCount == 0) throw new LockingFailException(); + } catch (DuplicateKeyException e) { + throw new LockingFailException(e); + } + } + + private long getExpirationTime() { + return System.currentTimeMillis() + lockTimeout; + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + @Override + public void checkLock(LockId lockId) throws LockException { + Optional lockData = getLockData(lockId); + if (!lockData.isPresent()) throw new NoLockException(); + } + + private Optional getLockData(LockId lockId) { + List locks = jdbcTemplate.query( + "select * from locks where lockid = ?", + lockDataRowMapper, lockId.getValue()); + return handleExpiration(locks); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + @Override + public void extendLockExpiration(LockId lockId, long inc) throws LockException { + Optional lockDataOpt = getLockData(lockId); + LockData lockData = + lockDataOpt.orElseThrow(() -> new NoLockException()); + jdbcTemplate.update( + "update locks set expiration_time = ? where type = ? AND id = ?", + new Timestamp(lockData.getTimestamp() + inc), + lockData.getType(), lockData.getId()); + } + + @Transactional(propagation = Propagation.REQUIRES_NEW) + @Override + public void releaseLock(LockId lockId) throws LockException { + jdbcTemplate.update("delete from locks where lockid = ?", lockId.getValue()); + } + + public void setLockTimeout(int lockTimeout) { + this.lockTimeout = lockTimeout; + } +} \ No newline at end of file diff --git a/src/main/java/com/myshop/member/command/application/BlockMemberService.java b/src/main/java/com/myshop/member/command/application/BlockMemberService.java new file mode 100644 index 0000000..c88eef3 --- /dev/null +++ b/src/main/java/com/myshop/member/command/application/BlockMemberService.java @@ -0,0 +1,28 @@ +package com.myshop.member.command.application; + +import com.myshop.member.command.domain.Member; +import com.myshop.member.command.domain.MemberId; +import com.myshop.member.command.domain.MemberRepository; +import org.springframework.security.access.prepost.PreAuthorize; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class BlockMemberService { + + private MemberRepository memberRepository; + + public BlockMemberService(MemberRepository memberRepository) { + this.memberRepository = memberRepository; + } + + @PreAuthorize("hasRole('ADMIN')") + @Transactional + public void block(String memberId) { + Member member = memberRepository.findById(new MemberId(memberId)) + .orElseThrow(() -> new NoMemberException()); + + member.block(); + } + +} diff --git a/src/main/java/com/myshop/member/command/application/NoMemberException.java b/src/main/java/com/myshop/member/command/application/NoMemberException.java new file mode 100644 index 0000000..84a8eea --- /dev/null +++ b/src/main/java/com/myshop/member/command/application/NoMemberException.java @@ -0,0 +1,4 @@ +package com.myshop.member.command.application; + +public class NoMemberException extends RuntimeException { +} diff --git a/src/main/java/com/myshop/member/command/domain/IdPasswordNotMatchingException.java b/src/main/java/com/myshop/member/command/domain/IdPasswordNotMatchingException.java new file mode 100644 index 0000000..908b2c2 --- /dev/null +++ b/src/main/java/com/myshop/member/command/domain/IdPasswordNotMatchingException.java @@ -0,0 +1,4 @@ +package com.myshop.member.command.domain; + +public class IdPasswordNotMatchingException extends RuntimeException { +} diff --git a/src/main/java/com/myshop/member/command/domain/Member.java b/src/main/java/com/myshop/member/command/domain/Member.java new file mode 100644 index 0000000..6356133 --- /dev/null +++ b/src/main/java/com/myshop/member/command/domain/Member.java @@ -0,0 +1,85 @@ +package com.myshop.member.command.domain; + +import com.myshop.common.event.Events; +import com.myshop.common.jpa.EmailSetConverter; +import com.myshop.common.model.Email; +import com.myshop.common.model.EmailSet; + +import javax.persistence.*; +import java.util.Random; +import java.util.Set; + +@Entity +@Table(name = "member") +public class Member { + @EmbeddedId + private MemberId id; + + private String name; + @Embedded + private Password password; + + private boolean blocked; + + @Column(name = "emails") + @Convert(converter = EmailSetConverter.class) + private EmailSet emails; + + protected Member() { + } + + public Member(MemberId id, String name) { + this.id = id; + this.name = name; + } + + public MemberId getId() { + return id; + } + + public String getName() { + return name; + } + + public void initializePassword() { + String newPassword = generateRandomPassword(); + this.password = new Password(newPassword); + Events.raise(new PasswordChangedEvent(id.getId(), newPassword)); + } + + private String generateRandomPassword() { + Random random = new Random(); + int number = random.nextInt(); + return Integer.toHexString(number); + } + + public void changeEmails(Set emails) { + this.emails = new EmailSet(emails); + } + + public void block() { + this.blocked = true; + Events.raise(new MemberBlockedEvent(id.getId())); + } + + public void unblock() { + this.blocked = false; + Events.raise(new MemberUnblockedEvent(id.getId())); + } + + public void changePassword(String oldPw, String newPw) { + if (!password.match(oldPw)) { + throw new IdPasswordNotMatchingException(); + } + this.password = new Password(newPw); + Events.raise(new PasswordChangedEvent(id.getId(), newPw)); + } + + public boolean isBlocked() { + return blocked; + } + + public EmailSet getEmails() { + return emails; + } +} diff --git a/src/main/java/com/myshop/member/command/domain/MemberBlockedEvent.java b/src/main/java/com/myshop/member/command/domain/MemberBlockedEvent.java new file mode 100644 index 0000000..833898c --- /dev/null +++ b/src/main/java/com/myshop/member/command/domain/MemberBlockedEvent.java @@ -0,0 +1,15 @@ +package com.myshop.member.command.domain; + +import com.myshop.common.event.Event; + +public class MemberBlockedEvent extends Event { + private String memberId; + + public MemberBlockedEvent(String memberId) { + this.memberId = memberId; + } + + public String getMemberId() { + return memberId; + } +} diff --git a/src/main/java/com/myshop/member/command/domain/MemberId.java b/src/main/java/com/myshop/member/command/domain/MemberId.java new file mode 100644 index 0000000..4f6559b --- /dev/null +++ b/src/main/java/com/myshop/member/command/domain/MemberId.java @@ -0,0 +1,40 @@ +package com.myshop.member.command.domain; + +import javax.persistence.Column; +import javax.persistence.Embeddable; +import java.io.Serializable; +import java.util.Objects; + +@Embeddable +public class MemberId implements Serializable { + @Column(name = "member_id") + private String id; + + protected MemberId() { + } + + public MemberId(String id) { + this.id = id; + } + + public String getId() { + return id; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + MemberId memberId = (MemberId) o; + return Objects.equals(id, memberId.id); + } + + @Override + public int hashCode() { + return Objects.hash(id); + } + + public static MemberId of(String id) { + return new MemberId(id); + } +} diff --git a/src/main/java/com/myshop/member/command/domain/MemberRepository.java b/src/main/java/com/myshop/member/command/domain/MemberRepository.java new file mode 100644 index 0000000..77e3491 --- /dev/null +++ b/src/main/java/com/myshop/member/command/domain/MemberRepository.java @@ -0,0 +1,25 @@ +package com.myshop.member.command.domain; + +import org.springframework.data.jpa.repository.Lock; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.jpa.repository.QueryHints; +import org.springframework.data.repository.Repository; +import org.springframework.data.repository.query.Param; + +import javax.persistence.LockModeType; +import javax.persistence.QueryHint; +import java.util.Optional; + +public interface MemberRepository extends Repository { + Optional findById(MemberId memberId); + + @Lock(LockModeType.PESSIMISTIC_WRITE) + @QueryHints({ + @QueryHint(name = "javax.persistence.lock.timeout", value = "3000") + }) + @Query("select m from Member m where m.id = :id") + Optional findByIdForUpdate(@Param("id") MemberId memberId); + + void save(Member member); + +} diff --git a/src/main/java/com/myshop/member/command/domain/MemberUnblockedEvent.java b/src/main/java/com/myshop/member/command/domain/MemberUnblockedEvent.java new file mode 100644 index 0000000..3c2122f --- /dev/null +++ b/src/main/java/com/myshop/member/command/domain/MemberUnblockedEvent.java @@ -0,0 +1,13 @@ +package com.myshop.member.command.domain; + +public class MemberUnblockedEvent { + private String memberId; + + public MemberUnblockedEvent(String memberId) { + this.memberId = memberId; + } + + public String getMemberId() { + return memberId; + } +} diff --git a/src/main/java/com/myshop/member/command/domain/Password.java b/src/main/java/com/myshop/member/command/domain/Password.java new file mode 100644 index 0000000..6ac84c1 --- /dev/null +++ b/src/main/java/com/myshop/member/command/domain/Password.java @@ -0,0 +1,21 @@ +package com.myshop.member.command.domain; + +import javax.persistence.Column; +import javax.persistence.Embeddable; + +@Embeddable +public class Password { + @Column(name = "password") + private String value; + + protected Password() { + } + + public Password(String value) { + this.value = value; + } + + public boolean match(String password) { + return this.value.equals(password); + } +} diff --git a/src/main/java/com/myshop/member/command/domain/PasswordChangedEvent.java b/src/main/java/com/myshop/member/command/domain/PasswordChangedEvent.java new file mode 100644 index 0000000..93f1d4a --- /dev/null +++ b/src/main/java/com/myshop/member/command/domain/PasswordChangedEvent.java @@ -0,0 +1,19 @@ +package com.myshop.member.command.domain; + +public class PasswordChangedEvent { + private String id; + private String newPassword; + + public PasswordChangedEvent(String id, String newPassword) { + this.id = id; + this.newPassword = newPassword; + } + + public String getId() { + return id; + } + + public String getNewPassword() { + return newPassword; + } +} diff --git a/src/main/java/com/myshop/member/infra/PasswordChangedEventHandler.java b/src/main/java/com/myshop/member/infra/PasswordChangedEventHandler.java new file mode 100644 index 0000000..5cac0b5 --- /dev/null +++ b/src/main/java/com/myshop/member/infra/PasswordChangedEventHandler.java @@ -0,0 +1,13 @@ +package com.myshop.member.infra; + +import com.myshop.member.command.domain.PasswordChangedEvent; +import org.springframework.context.event.EventListener; +import org.springframework.stereotype.Component; + +@Component +public class PasswordChangedEventHandler { + @EventListener(PasswordChangedEvent.class) + public void handle(PasswordChangedEvent event) { + // 이메일 발송 코드 + } +} diff --git a/src/main/java/com/myshop/member/query/MemberData.java b/src/main/java/com/myshop/member/query/MemberData.java new file mode 100644 index 0000000..c48f29b --- /dev/null +++ b/src/main/java/com/myshop/member/query/MemberData.java @@ -0,0 +1,39 @@ +package com.myshop.member.query; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import javax.persistence.Table; + +@Entity +@Table(name = "member") +public class MemberData { + @Id + @Column(name = "member_id") + private String id; + @Column(name = "name") + private String name; + @Column(name = "blocked") + private boolean blocked; + + protected MemberData() { + } + + public MemberData(String id, String name, boolean blocked) { + this.id = id; + this.name = name; + this.blocked = blocked; + } + + public String getId() { + return id; + } + + public String getName() { + return name; + } + + public boolean isBlocked() { + return blocked; + } +} diff --git a/src/main/java/com/myshop/member/query/MemberDataDao.java b/src/main/java/com/myshop/member/query/MemberDataDao.java new file mode 100644 index 0000000..8007144 --- /dev/null +++ b/src/main/java/com/myshop/member/query/MemberDataDao.java @@ -0,0 +1,26 @@ +package com.myshop.member.query; + +import com.myshop.common.jpa.Rangeable; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.repository.Repository; + +import java.util.List; +import java.util.Optional; + +public interface MemberDataDao extends Repository { + + MemberData findById(String memberId); + + Page findByBlocked(boolean blocked, Pageable pageable); + List findByNameLike(String name, Pageable pageable); + + List findAll(Specification spec, Pageable pageable); + + List getRange(Specification spec, Rangeable rangeable); + + List findFirst3ByNameLikeOrderByName(String name); + Optional findFirstByNameLikeOrderByName(String name); + MemberData findFirstByBlockedOrderById(boolean blocked); +} diff --git a/src/main/java/com/myshop/member/query/MemberDataSpecs.java b/src/main/java/com/myshop/member/query/MemberDataSpecs.java new file mode 100644 index 0000000..db7204f --- /dev/null +++ b/src/main/java/com/myshop/member/query/MemberDataSpecs.java @@ -0,0 +1,14 @@ +package com.myshop.member.query; + +import org.springframework.data.jpa.domain.Specification; + +public class MemberDataSpecs { + + public static Specification nonBlocked() { + return (root, query, cb) -> cb.equal(root.get("blocked"), false); + } + + public static Specification nameLike(String keyword) { + return (root, query, cb) -> cb.like(root.get("name"), keyword + "%"); + } +} diff --git a/src/main/java/com/myshop/member/query/MemberQueryService.java b/src/main/java/com/myshop/member/query/MemberQueryService.java new file mode 100644 index 0000000..8411071 --- /dev/null +++ b/src/main/java/com/myshop/member/query/MemberQueryService.java @@ -0,0 +1,21 @@ +package com.myshop.member.query; + +import com.myshop.member.command.application.NoMemberException; +import org.springframework.stereotype.Service; + +@Service +public class MemberQueryService { + private MemberDataDao memberDataDao; + + public MemberQueryService(MemberDataDao memberDataDao) { + this.memberDataDao = memberDataDao; + } + + public MemberData getMemberData(String memberId) { + MemberData memberData = memberDataDao.findById(memberId); + if (memberData == null) { + throw new NoMemberException(); + } + return memberData; + } +} diff --git a/src/main/java/com/myshop/order/NoOrderException.java b/src/main/java/com/myshop/order/NoOrderException.java new file mode 100644 index 0000000..08046fe --- /dev/null +++ b/src/main/java/com/myshop/order/NoOrderException.java @@ -0,0 +1,4 @@ +package com.myshop.order; + +public class NoOrderException extends RuntimeException { +} diff --git a/src/main/java/com/myshop/order/command/application/CancelOrderService.java b/src/main/java/com/myshop/order/command/application/CancelOrderService.java new file mode 100644 index 0000000..d604a8d --- /dev/null +++ b/src/main/java/com/myshop/order/command/application/CancelOrderService.java @@ -0,0 +1,29 @@ +package com.myshop.order.command.application; + +import com.myshop.order.NoOrderException; +import com.myshop.order.command.domain.*; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +public class CancelOrderService { + private OrderRepository orderRepository; + private CancelPolicy cancelPolicy; + + public CancelOrderService(OrderRepository orderRepository, + CancelPolicy cancelPolicy) { + this.orderRepository = orderRepository; + this.cancelPolicy = cancelPolicy; + } + + @Transactional + public void cancel(OrderNo orderNo, Canceller canceller) { + Order order = orderRepository.findById(orderNo) + .orElseThrow(() -> new NoOrderException()); + if (!cancelPolicy.hasCancellationPermission(order, canceller)) { + throw new NoCancellablePermission(); + } + order.cancel(); + } + +} diff --git a/src/main/java/com/myshop/order/command/application/ChangeShippingRequest.java b/src/main/java/com/myshop/order/command/application/ChangeShippingRequest.java new file mode 100644 index 0000000..8f80bdd --- /dev/null +++ b/src/main/java/com/myshop/order/command/application/ChangeShippingRequest.java @@ -0,0 +1,21 @@ +package com.myshop.order.command.application; + +import com.myshop.order.command.domain.ShippingInfo; + +public class ChangeShippingRequest { + private String number; + private ShippingInfo shippingInfo; + + public ChangeShippingRequest(String number, ShippingInfo shippingInfo) { + this.number = number; + this.shippingInfo = shippingInfo; + } + + public String getNumber() { + return number; + } + + public ShippingInfo getShippingInfo() { + return shippingInfo; + } +} diff --git a/src/main/java/com/myshop/order/command/application/ChangeShippingService.java b/src/main/java/com/myshop/order/command/application/ChangeShippingService.java new file mode 100644 index 0000000..d940039 --- /dev/null +++ b/src/main/java/com/myshop/order/command/application/ChangeShippingService.java @@ -0,0 +1,27 @@ +package com.myshop.order.command.application; + +import com.myshop.order.NoOrderException; +import com.myshop.order.command.domain.Order; +import com.myshop.order.command.domain.OrderNo; +import com.myshop.order.command.domain.OrderRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@Service +public class ChangeShippingService { + private OrderRepository orderRepository; + + public ChangeShippingService(OrderRepository orderRepository) { + this.orderRepository = orderRepository; + } + + @Transactional + public void changeShipping(ChangeShippingRequest changeReq) { + Optional orderOpt = orderRepository.findById(new OrderNo(changeReq.getNumber())); + Order order = orderOpt.orElseThrow(() -> new NoOrderException()); + order.changeShippingInfo(changeReq.getShippingInfo()); + } + +} diff --git a/src/main/java/com/myshop/order/command/application/NoCancellablePermission.java b/src/main/java/com/myshop/order/command/application/NoCancellablePermission.java new file mode 100644 index 0000000..648ecd6 --- /dev/null +++ b/src/main/java/com/myshop/order/command/application/NoCancellablePermission.java @@ -0,0 +1,4 @@ +package com.myshop.order.command.application; + +public class NoCancellablePermission extends RuntimeException { +} diff --git a/src/main/java/com/myshop/order/command/application/NoOrderProductException.java b/src/main/java/com/myshop/order/command/application/NoOrderProductException.java new file mode 100644 index 0000000..bf2ff2c --- /dev/null +++ b/src/main/java/com/myshop/order/command/application/NoOrderProductException.java @@ -0,0 +1,13 @@ +package com.myshop.order.command.application; + +public class NoOrderProductException extends RuntimeException { + private String productId; + + public NoOrderProductException(String productId) { + this.productId = productId; + } + + public String getProductId() { + return productId; + } +} diff --git a/src/main/java/com/myshop/order/command/application/OrderProduct.java b/src/main/java/com/myshop/order/command/application/OrderProduct.java new file mode 100644 index 0000000..d5d9b83 --- /dev/null +++ b/src/main/java/com/myshop/order/command/application/OrderProduct.java @@ -0,0 +1,22 @@ +package com.myshop.order.command.application; + +public class OrderProduct { + private String productId; + private int quantity; + + public OrderProduct() { + } + + public OrderProduct(String productId, int quantity) { + this.productId = productId; + this.quantity = quantity; + } + + public String getProductId() { + return productId; + } + + public int getQuantity() { + return quantity; + } +} diff --git a/src/main/java/com/myshop/order/command/application/OrderRequest.java b/src/main/java/com/myshop/order/command/application/OrderRequest.java new file mode 100644 index 0000000..3f09d5b --- /dev/null +++ b/src/main/java/com/myshop/order/command/application/OrderRequest.java @@ -0,0 +1,36 @@ +package com.myshop.order.command.application; + +import com.myshop.member.command.domain.MemberId; +import com.myshop.order.command.domain.ShippingInfo; + +import java.util.List; + +public class OrderRequest { + private List orderProducts; + private MemberId ordererMemberId; + private ShippingInfo shippingInfo; + + public List getOrderProducts() { + return orderProducts; + } + + public void setOrderProducts(List orderProducts) { + this.orderProducts = orderProducts; + } + + public MemberId getOrdererMemberId() { + return ordererMemberId; + } + + public void setOrdererMemberId(MemberId ordererMemberId) { + this.ordererMemberId = ordererMemberId; + } + + public ShippingInfo getShippingInfo() { + return shippingInfo; + } + + public void setShippingInfo(ShippingInfo shippingInfo) { + this.shippingInfo = shippingInfo; + } +} diff --git a/src/main/java/com/myshop/order/command/application/OrderRequestValidator.java b/src/main/java/com/myshop/order/command/application/OrderRequestValidator.java new file mode 100644 index 0000000..4f81b71 --- /dev/null +++ b/src/main/java/com/myshop/order/command/application/OrderRequestValidator.java @@ -0,0 +1,52 @@ +package com.myshop.order.command.application; + +import com.myshop.common.ValidationError; +import org.springframework.util.StringUtils; + +import java.util.ArrayList; +import java.util.List; + +public class OrderRequestValidator { + public List validate(OrderRequest orderRequest) { + List errors = new ArrayList<>(); + if (orderRequest == null) { + errors.add(ValidationError.of("required")); + } else { + if (orderRequest.getOrdererMemberId() == null) + errors.add(ValidationError.of("ordererMemberId", "required")); + if (orderRequest.getOrderProducts() == null) + errors.add(ValidationError.of("orderProducts", "required")); + if (orderRequest.getOrderProducts().isEmpty()) + errors.add(ValidationError.of("orderProducts", "required")); + + if (orderRequest.getShippingInfo() == null) { + errors.add(ValidationError.of("shippingInfo", "required")); + } else { + if (orderRequest.getShippingInfo().getReceiver() == null) { + errors.add(ValidationError.of("shippingInfo.receiver", "required")); + } else { + if (!StringUtils.hasText(orderRequest.getShippingInfo().getReceiver().getName())) { + errors.add(ValidationError.of("shippingInfo.receiver.name", "required")); + } + if (!StringUtils.hasText(orderRequest.getShippingInfo().getReceiver().getPhone())) { + errors.add(ValidationError.of("shippingInfo.receiver.phone", "required")); + } + if (orderRequest.getShippingInfo().getAddress() == null) { + errors.add(ValidationError.of("shippingInfo.address", "required")); + } else { + if (!StringUtils.hasText(orderRequest.getShippingInfo().getAddress().getZipCode())) { + errors.add(ValidationError.of("shippingInfo.address.zipCode", "required")); + } + if (!StringUtils.hasText(orderRequest.getShippingInfo().getAddress().getAddress1())) { + errors.add(ValidationError.of("shippingInfo.address.address1", "required")); + } + if (!StringUtils.hasText(orderRequest.getShippingInfo().getAddress().getAddress2())) { + errors.add(ValidationError.of("shippingInfo.address.address2", "required")); + } + } + } + } + } + return errors; + } +} diff --git a/src/main/java/com/myshop/order/command/application/PlaceOrderService.java b/src/main/java/com/myshop/order/command/application/PlaceOrderService.java new file mode 100644 index 0000000..ce74f9f --- /dev/null +++ b/src/main/java/com/myshop/order/command/application/PlaceOrderService.java @@ -0,0 +1,53 @@ +package com.myshop.order.command.application; + +import com.myshop.catalog.command.domain.product.Product; +import com.myshop.catalog.command.domain.product.ProductId; +import com.myshop.catalog.command.domain.product.ProductRepository; +import com.myshop.common.ValidationError; +import com.myshop.common.ValidationErrorException; +import com.myshop.order.command.domain.*; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Service +public class PlaceOrderService { + private ProductRepository productRepository; + private OrderRepository orderRepository; + private OrdererService ordererService; + + public PlaceOrderService(ProductRepository productRepository, + OrderRepository orderRepository, + OrdererService ordererService) { + this.productRepository = productRepository; + this.orderRepository = orderRepository; + this.ordererService = ordererService; + } + + @Transactional + public OrderNo placeOrder(OrderRequest orderRequest) { + List errors = validateOrderRequest(orderRequest); + if (!errors.isEmpty()) throw new ValidationErrorException(errors); + + List orderLines = new ArrayList<>(); + for (OrderProduct op : orderRequest.getOrderProducts()) { + Optional productOpt = productRepository.findById(new ProductId(op.getProductId())); + Product product = productOpt.orElseThrow(() -> new NoOrderProductException(op.getProductId())); + orderLines.add(new OrderLine(product.getId(), product.getPrice(), op.getQuantity())); + } + OrderNo orderNo = orderRepository.nextOrderNo(); + Orderer orderer = ordererService.createOrderer(orderRequest.getOrdererMemberId()); + + Order order = new Order(orderNo, orderer, orderLines, orderRequest.getShippingInfo(), OrderState.PAYMENT_WAITING); + orderRepository.save(order); + return orderNo; + } + + private List validateOrderRequest(OrderRequest orderRequest) { + return new OrderRequestValidator().validate(orderRequest); + } + +} diff --git a/src/main/java/com/myshop/order/command/application/RefundService.java b/src/main/java/com/myshop/order/command/application/RefundService.java new file mode 100644 index 0000000..e12be1b --- /dev/null +++ b/src/main/java/com/myshop/order/command/application/RefundService.java @@ -0,0 +1,5 @@ +package com.myshop.order.command.application; + +public interface RefundService { + void refund(String orderNumber); +} diff --git a/src/main/java/com/myshop/order/command/application/StartShippingRequest.java b/src/main/java/com/myshop/order/command/application/StartShippingRequest.java new file mode 100644 index 0000000..8d9c401 --- /dev/null +++ b/src/main/java/com/myshop/order/command/application/StartShippingRequest.java @@ -0,0 +1,22 @@ +package com.myshop.order.command.application; + +public class StartShippingRequest { + private String orderNumber; + private long version; + + protected StartShippingRequest() { + } + + public StartShippingRequest(String orderNumber, long version) { + this.orderNumber = orderNumber; + this.version = version; + } + + public String getOrderNumber() { + return orderNumber; + } + + public long getVersion() { + return version; + } +} diff --git a/src/main/java/com/myshop/order/command/application/StartShippingService.java b/src/main/java/com/myshop/order/command/application/StartShippingService.java new file mode 100644 index 0000000..1f446a1 --- /dev/null +++ b/src/main/java/com/myshop/order/command/application/StartShippingService.java @@ -0,0 +1,30 @@ +package com.myshop.order.command.application; + +import com.myshop.common.VersionConflictException; +import com.myshop.order.NoOrderException; +import com.myshop.order.command.domain.Order; +import com.myshop.order.command.domain.OrderNo; +import com.myshop.order.command.domain.OrderRepository; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +import java.util.Optional; + +@Service +public class StartShippingService { + private OrderRepository orderRepository; + + public StartShippingService(OrderRepository orderRepository) { + this.orderRepository = orderRepository; + } + + @Transactional + public void startShipping(StartShippingRequest req) { + Optional orderOpt = orderRepository.findById(new OrderNo(req.getOrderNumber())); + Order order = orderOpt.orElseThrow(() -> new NoOrderException()); + if (order.matchVersion(req.getVersion())) { + throw new VersionConflictException(); + } + order.startShipping(); + } +} diff --git a/src/main/java/com/myshop/order/command/domain/AlreadyShippedException.java b/src/main/java/com/myshop/order/command/domain/AlreadyShippedException.java new file mode 100644 index 0000000..488cc5f --- /dev/null +++ b/src/main/java/com/myshop/order/command/domain/AlreadyShippedException.java @@ -0,0 +1,4 @@ +package com.myshop.order.command.domain; + +public class AlreadyShippedException extends RuntimeException { +} diff --git a/src/main/java/com/myshop/order/command/domain/CancelPolicy.java b/src/main/java/com/myshop/order/command/domain/CancelPolicy.java new file mode 100644 index 0000000..10aad7c --- /dev/null +++ b/src/main/java/com/myshop/order/command/domain/CancelPolicy.java @@ -0,0 +1,5 @@ +package com.myshop.order.command.domain; + +public interface CancelPolicy { + boolean hasCancellationPermission(Order order, Canceller canceller); +} diff --git a/src/main/java/com/myshop/order/command/domain/Canceller.java b/src/main/java/com/myshop/order/command/domain/Canceller.java new file mode 100644 index 0000000..e1943a0 --- /dev/null +++ b/src/main/java/com/myshop/order/command/domain/Canceller.java @@ -0,0 +1,17 @@ +package com.myshop.order.command.domain; + +public class Canceller { + private String memberId; + + public Canceller(String memberId) { + this.memberId = memberId; + } + + public String getMemberId() { + return memberId; + } + + public static Canceller of(String memberId) { + return new Canceller(memberId); + } +} diff --git a/src/main/java/com/myshop/order/command/domain/Order.java b/src/main/java/com/myshop/order/command/domain/Order.java new file mode 100644 index 0000000..8907aae --- /dev/null +++ b/src/main/java/com/myshop/order/command/domain/Order.java @@ -0,0 +1,158 @@ +package com.myshop.order.command.domain; + +import com.myshop.common.event.Events; +import com.myshop.common.jpa.MoneyConverter; +import com.myshop.common.model.Money; + +import javax.persistence.*; +import java.time.LocalDateTime; +import java.util.List; + +@Entity +@Table(name = "purchase_order") +@Access(AccessType.FIELD) +public class Order { + @EmbeddedId + private OrderNo number; + + @Version + private long version; + + @Embedded + private Orderer orderer; + + @ElementCollection(fetch = FetchType.LAZY) + @CollectionTable(name = "order_line", joinColumns = @JoinColumn(name = "order_number")) + @OrderColumn(name = "line_idx") + private List orderLines; + + @Convert(converter = MoneyConverter.class) + @Column(name = "total_amounts") + private Money totalAmounts; + + @Embedded + private ShippingInfo shippingInfo; + + @Column(name = "state") + @Enumerated(EnumType.STRING) + private OrderState state; + + @Column(name = "order_date") + private LocalDateTime orderDate; + + protected Order() { + } + + public Order(OrderNo number, Orderer orderer, List orderLines, + ShippingInfo shippingInfo, OrderState state) { + setNumber(number); + setOrderer(orderer); + setOrderLines(orderLines); + setShippingInfo(shippingInfo); + this.state = state; + this.orderDate = LocalDateTime.now(); + Events.raise(new OrderPlacedEvent(number.getNumber(), orderer, orderLines, orderDate)); + } + + private void setNumber(OrderNo number) { + if (number == null) throw new IllegalArgumentException("no number"); + this.number = number; + } + + private void setOrderer(Orderer orderer) { + if (orderer == null) throw new IllegalArgumentException("no orderer"); + this.orderer = orderer; + } + + private void setOrderLines(List orderLines) { + verifyAtLeastOneOrMoreOrderLines(orderLines); + this.orderLines = orderLines; + calculateTotalAmounts(); + } + + private void verifyAtLeastOneOrMoreOrderLines(List orderLines) { + if (orderLines == null || orderLines.isEmpty()) { + throw new IllegalArgumentException("no OrderLine"); + } + } + + private void calculateTotalAmounts() { + this.totalAmounts = new Money(orderLines.stream() + .mapToInt(x -> x.getAmounts().getValue()).sum()); + } + + private void setShippingInfo(ShippingInfo shippingInfo) { + if (shippingInfo == null) throw new IllegalArgumentException("no shipping info"); + this.shippingInfo = shippingInfo; + } + + public OrderNo getNumber() { + return number; + } + + public long getVersion() { + return version; + } + + public Orderer getOrderer() { + return orderer; + } + + public Money getTotalAmounts() { + return totalAmounts; + } + + public ShippingInfo getShippingInfo() { + return shippingInfo; + } + + public OrderState getState() { + return state; + } + + public void changeShippingInfo(ShippingInfo newShippingInfo) { + verifyNotYetShipped(); + setShippingInfo(newShippingInfo); + Events.raise(new ShippingInfoChangedEvent(number, newShippingInfo)); + } + + public void cancel() { + verifyNotYetShipped(); + this.state = OrderState.CANCELED; + Events.raise(new OrderCanceledEvent(number.getNumber())); + } + + private void verifyNotYetShipped() { + if (!isNotYetShipped()) + throw new AlreadyShippedException(); + } + + public boolean isNotYetShipped() { + return state == OrderState.PAYMENT_WAITING || state == OrderState.PREPARING; + } + + public List getOrderLines() { + return orderLines; + } + + public boolean matchVersion(long version) { + return this.version == version; + } + + public void startShipping() { + verifyShippableState(); + this.state = OrderState.SHIPPED; + Events.raise(new ShippingStartedEvent(number.getNumber())); + } + + private void verifyShippableState() { + verifyNotYetShipped(); + verifyNotCanceled(); + } + + private void verifyNotCanceled() { + if (state == OrderState.CANCELED) { + throw new OrderAlreadyCanceledException(); + } + } +} diff --git a/src/main/java/com/myshop/order/command/domain/OrderAlreadyCanceledException.java b/src/main/java/com/myshop/order/command/domain/OrderAlreadyCanceledException.java new file mode 100644 index 0000000..74019dc --- /dev/null +++ b/src/main/java/com/myshop/order/command/domain/OrderAlreadyCanceledException.java @@ -0,0 +1,4 @@ +package com.myshop.order.command.domain; + +public class OrderAlreadyCanceledException extends RuntimeException { +} diff --git a/src/main/java/com/myshop/order/command/domain/OrderCanceledEvent.java b/src/main/java/com/myshop/order/command/domain/OrderCanceledEvent.java new file mode 100644 index 0000000..726ec57 --- /dev/null +++ b/src/main/java/com/myshop/order/command/domain/OrderCanceledEvent.java @@ -0,0 +1,16 @@ +package com.myshop.order.command.domain; + +import com.myshop.common.event.Event; + +public class OrderCanceledEvent extends Event { + private String orderNumber; + + public OrderCanceledEvent(String number) { + super(); + this.orderNumber = number; + } + + public String getOrderNumber() { + return orderNumber; + } +} diff --git a/src/main/java/com/myshop/order/command/domain/OrderLine.java b/src/main/java/com/myshop/order/command/domain/OrderLine.java new file mode 100644 index 0000000..d3cf379 --- /dev/null +++ b/src/main/java/com/myshop/order/command/domain/OrderLine.java @@ -0,0 +1,57 @@ +package com.myshop.order.command.domain; + +import com.myshop.catalog.command.domain.product.ProductId; +import com.myshop.common.jpa.MoneyConverter; +import com.myshop.common.model.Money; + +import javax.persistence.Column; +import javax.persistence.Convert; +import javax.persistence.Embeddable; +import javax.persistence.Embedded; + +@Embeddable +public class OrderLine { + @Embedded + private ProductId productId; + + @Convert(converter = MoneyConverter.class) + @Column(name = "price") + private Money price; + + @Column(name = "quantity") + private int quantity; + + @Convert(converter = MoneyConverter.class) + @Column(name = "amounts") + private Money amounts; + + protected OrderLine() { + } + + public OrderLine(ProductId productId, Money price, int quantity) { + this.productId = productId; + this.price = price; + this.quantity = quantity; + this.amounts = calculateAmounts(); + } + + private Money calculateAmounts() { + return price.multiply(quantity); + } + + public ProductId getProductId() { + return productId; + } + + public Money getPrice() { + return price; + } + + public int getQuantity() { + return quantity; + } + + public Money getAmounts() { + return amounts; + } +} diff --git a/src/main/java/com/myshop/order/command/domain/OrderNo.java b/src/main/java/com/myshop/order/command/domain/OrderNo.java new file mode 100644 index 0000000..6bc4ec8 --- /dev/null +++ b/src/main/java/com/myshop/order/command/domain/OrderNo.java @@ -0,0 +1,40 @@ +package com.myshop.order.command.domain; + +import javax.persistence.Column; +import javax.persistence.Embeddable; +import java.io.Serializable; +import java.util.Objects; + +@Embeddable +public class OrderNo implements Serializable { + @Column(name = "order_number") + private String number; + + protected OrderNo() { + } + + public OrderNo(String number) { + this.number = number; + } + + public String getNumber() { + return number; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + OrderNo orderNo = (OrderNo) o; + return Objects.equals(number, orderNo.number); + } + + @Override + public int hashCode() { + return Objects.hash(number); + } + + public static OrderNo of(String number) { + return new OrderNo(number); + } +} diff --git a/src/main/java/com/myshop/order/command/domain/OrderPlacedEvent.java b/src/main/java/com/myshop/order/command/domain/OrderPlacedEvent.java new file mode 100644 index 0000000..450466d --- /dev/null +++ b/src/main/java/com/myshop/order/command/domain/OrderPlacedEvent.java @@ -0,0 +1,38 @@ +package com.myshop.order.command.domain; + +import java.time.LocalDateTime; +import java.util.Date; +import java.util.List; + +public class OrderPlacedEvent { + private String number; + private Orderer orderer; + private List orderLines; + private LocalDateTime orderDate; + + private OrderPlacedEvent() { + } + + public OrderPlacedEvent(String number, Orderer orderer, List orderLines, LocalDateTime orderDate) { + this.number = number; + this.orderer = orderer; + this.orderLines = orderLines; + this.orderDate = orderDate; + } + + public String getNumber() { + return number; + } + + public Orderer getOrderer() { + return orderer; + } + + public List getOrderLines() { + return orderLines; + } + + public LocalDateTime getOrderDate() { + return orderDate; + } +} diff --git a/src/main/java/com/myshop/order/command/domain/OrderRepository.java b/src/main/java/com/myshop/order/command/domain/OrderRepository.java new file mode 100644 index 0000000..2173548 --- /dev/null +++ b/src/main/java/com/myshop/order/command/domain/OrderRepository.java @@ -0,0 +1,19 @@ +package com.myshop.order.command.domain; + +import org.springframework.data.repository.Repository; + +import java.util.Date; +import java.util.Optional; +import java.util.concurrent.ThreadLocalRandom; + +public interface OrderRepository extends Repository { + Optional findById(OrderNo id); + + void save(Order order); + + default OrderNo nextOrderNo() { + int randomNo = ThreadLocalRandom.current().nextInt(900000) + 100000; + String number = String.format("%tY% authorities = authentication.getAuthorities(); + if (authorities == null) return false; + return authorities.stream().anyMatch(authority -> authority.getAuthority().equals("ROLE_ADMIN")); + } +} diff --git a/src/main/java/com/myshop/order/infra/paygate/ExternalRefundService.java b/src/main/java/com/myshop/order/infra/paygate/ExternalRefundService.java new file mode 100644 index 0000000..5cd1751 --- /dev/null +++ b/src/main/java/com/myshop/order/infra/paygate/ExternalRefundService.java @@ -0,0 +1,16 @@ +package com.myshop.order.infra.paygate; + +import com.myshop.order.command.application.RefundService; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.stereotype.Component; + +@Component +public class ExternalRefundService implements RefundService { + private Logger logger = LoggerFactory.getLogger(getClass()); + + @Override + public void refund(String orderNumber) { + logger.info("refund order[{}]", orderNumber); + } +} diff --git a/src/main/java/com/myshop/order/query/application/ListRequest.java b/src/main/java/com/myshop/order/query/application/ListRequest.java new file mode 100644 index 0000000..a9456be --- /dev/null +++ b/src/main/java/com/myshop/order/query/application/ListRequest.java @@ -0,0 +1,19 @@ +package com.myshop.order.query.application; + +public class ListRequest { + private int page; + private int size; + + public ListRequest(int page, int size) { + this.page = page; + this.size = size; + } + + public int getPage() { + return page; + } + + public int getSize() { + return size; + } +} diff --git a/src/main/java/com/myshop/order/query/application/OrderDetail.java b/src/main/java/com/myshop/order/query/application/OrderDetail.java new file mode 100644 index 0000000..e3dfa81 --- /dev/null +++ b/src/main/java/com/myshop/order/query/application/OrderDetail.java @@ -0,0 +1,63 @@ +package com.myshop.order.query.application; + +import com.myshop.order.command.domain.Order; +import com.myshop.order.command.domain.OrderState; +import com.myshop.order.command.domain.Orderer; +import com.myshop.order.command.domain.ShippingInfo; + +import java.util.List; + +public class OrderDetail { + + private final String number; + private long version; + private final Orderer orderer; + private final ShippingInfo shippingInfo; + private final OrderState state; + private final int totalAmounts; + private List orderLines; + private final boolean notYetShipped; + + public OrderDetail(Order order, List orderLines) { + this.orderLines = orderLines; + number = order.getNumber().getNumber(); + version = order.getVersion(); + orderer = order.getOrderer(); + shippingInfo = order.getShippingInfo(); + state = order.getState(); + notYetShipped = order.isNotYetShipped(); + totalAmounts = order.getTotalAmounts().getValue(); + } + + public String getNumber() { + return number; + } + + public long getVersion() { + return version; + } + + public Orderer getOrderer() { + return orderer; + } + + public ShippingInfo getShippingInfo() { + return shippingInfo; + } + + public OrderState getState() { + return state; + } + + public int getTotalAmounts() { + return totalAmounts; + } + + public List getOrderLines() { + return orderLines; + } + + public boolean isNotYetShipped() { + return notYetShipped; + } +} diff --git a/src/main/java/com/myshop/order/query/application/OrderDetailService.java b/src/main/java/com/myshop/order/query/application/OrderDetailService.java new file mode 100644 index 0000000..7d66414 --- /dev/null +++ b/src/main/java/com/myshop/order/query/application/OrderDetailService.java @@ -0,0 +1,39 @@ +package com.myshop.order.query.application; + +import com.myshop.catalog.query.product.ProductData; +import com.myshop.catalog.query.product.ProductQueryService; +import com.myshop.order.command.domain.Order; +import com.myshop.order.command.domain.OrderNo; +import com.myshop.order.command.domain.OrderRepository; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +import java.util.List; +import java.util.Optional; +import java.util.stream.Collectors; + +@Component +public class OrderDetailService { + private OrderRepository orderRepository; + private ProductQueryService productQueryService; + + public OrderDetailService(OrderRepository orderRepository, + ProductQueryService productQueryService) { + this.orderRepository = orderRepository; + this.productQueryService = productQueryService; + } + + @Transactional + public Optional getOrderDetail(String orderNumber) { + Optional orderOpt = orderRepository.findById(new OrderNo(orderNumber)); + return orderOpt.map(order -> { + List orderLines = order.getOrderLines().stream() + .map(orderLine -> { + Optional productOpt = + productQueryService.getProduct(orderLine.getProductId().getId()); + return new OrderLineDetail(orderLine, productOpt.orElse(null)); + }).collect(Collectors.toList()); + return new OrderDetail(order, orderLines); + }); + } +} diff --git a/src/main/java/com/myshop/order/query/application/OrderLineDetail.java b/src/main/java/com/myshop/order/query/application/OrderLineDetail.java new file mode 100644 index 0000000..d5a82e7 --- /dev/null +++ b/src/main/java/com/myshop/order/query/application/OrderLineDetail.java @@ -0,0 +1,47 @@ +package com.myshop.order.query.application; + +import com.myshop.catalog.query.product.ProductData; +import com.myshop.order.command.domain.OrderLine; + +public class OrderLineDetail { + + private final String productId; + private final int price; + private final int quantity; + private final int amounts; + private final String productName; + private final String productImagePath; + + public OrderLineDetail(OrderLine orderLine, ProductData product) { + productId = orderLine.getProductId().getId(); + price = orderLine.getPrice().getValue(); + quantity = orderLine.getQuantity(); + amounts = orderLine.getAmounts().getValue(); + productName = product.getName(); + productImagePath = product.getFirstIamgeThumbnailPath(); + } + + public String getProductId() { + return productId; + } + + public int getPrice() { + return price; + } + + public int getQuantity() { + return quantity; + } + + public int getAmounts() { + return amounts; + } + + public String getProductName() { + return productName; + } + + public String getProductImagePath() { + return productImagePath; + } +} diff --git a/src/main/java/com/myshop/order/query/application/OrderViewListService.java b/src/main/java/com/myshop/order/query/application/OrderViewListService.java new file mode 100644 index 0000000..21622a2 --- /dev/null +++ b/src/main/java/com/myshop/order/query/application/OrderViewListService.java @@ -0,0 +1,27 @@ +package com.myshop.order.query.application; + +import com.myshop.order.query.dao.OrderSummaryDao; +import com.myshop.order.query.dto.OrderSummary; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.stereotype.Component; +import org.springframework.transaction.annotation.Transactional; + +@Component +public class OrderViewListService { + private OrderSummaryDao orderSummaryDao; + + public OrderViewListService(OrderSummaryDao orderSummaryDao) { + this.orderSummaryDao = orderSummaryDao; + } + + @Transactional + public Page getList(ListRequest listReq) { + PageRequest pageable = PageRequest.of( + listReq.getPage(), + listReq.getSize(), + Sort.by(Sort.Direction.DESC, "number")); + return orderSummaryDao.findAll(pageable); + } +} diff --git a/src/main/java/com/myshop/order/query/dao/OrderSummaryDao.java b/src/main/java/com/myshop/order/query/dao/OrderSummaryDao.java new file mode 100644 index 0000000..85cf7d7 --- /dev/null +++ b/src/main/java/com/myshop/order/query/dao/OrderSummaryDao.java @@ -0,0 +1,38 @@ +package com.myshop.order.query.dao; + +import com.myshop.order.query.dto.OrderSummary; +import com.myshop.order.query.dto.OrderView; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.data.jpa.repository.Query; +import org.springframework.data.repository.Repository; + +import java.util.List; + +public interface OrderSummaryDao extends Repository { + List findByOrdererId(String ordererId); + List findByOrdererId(String ordererId, Sort sort); + List findByOrdererId(String ordererId, Pageable pageable); + List findByOrdererIdOrderByNumberDesc(String ordererId); + + List findAll(Specification spec); + List findAll(Specification spec, Sort sort); + List findAll(Specification spec, Pageable pageable); + + Page findAll(Pageable pageable); + + @Query(""" + select new com.myshop.order.query.dto.OrderView( + o.number, o.state, m.name, m.id, p.name + ) + from Order o join o.orderLines ol, Member m, Product p + where o.orderer.memberId.id = :ordererId + and o.orderer.memberId.id = m.id + and index(ol) = 0 + and ol.productId.id = p.id + order by o.number.number desc + """) + List findOrderView(String ordererId); +} diff --git a/src/main/java/com/myshop/order/query/dao/OrderSummarySpecs.java b/src/main/java/com/myshop/order/query/dao/OrderSummarySpecs.java new file mode 100644 index 0000000..5066b8d --- /dev/null +++ b/src/main/java/com/myshop/order/query/dao/OrderSummarySpecs.java @@ -0,0 +1,24 @@ +package com.myshop.order.query.dao; + +import com.myshop.order.query.dto.OrderSummary; +import com.myshop.order.query.dto.OrderSummary_; +import org.springframework.data.jpa.domain.Specification; + +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Order; +import javax.persistence.criteria.Root; +import java.time.LocalDateTime; + +public class OrderSummarySpecs { + public static Specification ordererId(String ordererId) { + return (Root root, CriteriaQuery query, CriteriaBuilder cb) -> + cb.equal(root.get("ordererId"), ordererId); + } + + public static Specification orderDateBetween( + LocalDateTime from, LocalDateTime to) { + return (Root root, CriteriaQuery query, CriteriaBuilder cb) -> + cb.between(root.get(OrderSummary_.orderDate), from, to); + } +} diff --git a/src/main/java/com/myshop/order/query/dao/OrdererIdSpec.java b/src/main/java/com/myshop/order/query/dao/OrdererIdSpec.java new file mode 100644 index 0000000..7b5bda9 --- /dev/null +++ b/src/main/java/com/myshop/order/query/dao/OrdererIdSpec.java @@ -0,0 +1,26 @@ +package com.myshop.order.query.dao; + +import com.myshop.order.query.dto.OrderSummary; +import com.myshop.order.query.dto.OrderSummary_; +import org.springframework.data.jpa.domain.Specification; + +import javax.persistence.criteria.CriteriaBuilder; +import javax.persistence.criteria.CriteriaQuery; +import javax.persistence.criteria.Predicate; +import javax.persistence.criteria.Root; + +public class OrdererIdSpec implements Specification { + + private String ordererId; + + public OrdererIdSpec(String ordererId) { + this.ordererId = ordererId; + } + + @Override + public Predicate toPredicate(Root root, + CriteriaQuery query, + CriteriaBuilder cb) { + return cb.equal(root.get(OrderSummary_.ordererId), ordererId); + } +} diff --git a/src/main/java/com/myshop/order/query/dto/OrderSummary.java b/src/main/java/com/myshop/order/query/dto/OrderSummary.java new file mode 100644 index 0000000..ff5b44b --- /dev/null +++ b/src/main/java/com/myshop/order/query/dto/OrderSummary.java @@ -0,0 +1,96 @@ +package com.myshop.order.query.dto; + +import org.hibernate.annotations.Immutable; +import org.hibernate.annotations.Subselect; +import org.hibernate.annotations.Synchronize; + +import javax.persistence.Column; +import javax.persistence.Entity; +import javax.persistence.Id; +import java.time.LocalDateTime; + +@Entity +@Immutable +@Subselect( + """ + select o.order_number as number, + o.version, + o.orderer_id, + o.orderer_name, + o.total_amounts, + o.receiver_name, + o.state, + o.order_date, + p.product_id, + p.name as product_name + from purchase_order o inner join order_line ol + on o.order_number = ol.order_number + cross join product p + where + ol.line_idx = 0 + and ol.product_id = p.product_id""" +) +@Synchronize({"purchase_order", "order_line", "product"}) +public class OrderSummary { + @Id + private String number; + private long version; + @Column(name = "orderer_id") + private String ordererId; + @Column(name = "orderer_name") + private String ordererName; + @Column(name = "total_amounts") + private int totalAmounts; + @Column(name = "receiver_name") + private String receiverName; + private String state; + @Column(name = "order_date") + private LocalDateTime orderDate; + @Column(name = "product_id") + private String productId; + @Column(name = "product_name") + private String productName; + + protected OrderSummary() { + } + + public String getNumber() { + return number; + } + + public long getVersion() { + return version; + } + + public String getOrdererId() { + return ordererId; + } + + public String getOrdererName() { + return ordererName; + } + + public int getTotalAmounts() { + return totalAmounts; + } + + public String getReceiverName() { + return receiverName; + } + + public String getState() { + return state; + } + + public LocalDateTime getOrderDate() { + return orderDate; + } + + public String getProductId() { + return productId; + } + + public String getProductName() { + return productName; + } +} diff --git a/src/main/java/com/myshop/order/query/dto/OrderSummary_.java b/src/main/java/com/myshop/order/query/dto/OrderSummary_.java new file mode 100644 index 0000000..2f23872 --- /dev/null +++ b/src/main/java/com/myshop/order/query/dto/OrderSummary_.java @@ -0,0 +1,19 @@ +package com.myshop.order.query.dto; + +import javax.persistence.metamodel.SingularAttribute; +import javax.persistence.metamodel.StaticMetamodel; +import java.time.LocalDateTime; + +@StaticMetamodel(OrderSummary.class) +public class OrderSummary_ { + public static volatile SingularAttribute number; + public static volatile SingularAttribute version; + public static volatile SingularAttribute ordererId; + public static volatile SingularAttribute ordererName; + public static volatile SingularAttribute totalAmounts; + public static volatile SingularAttribute receiverName; + public static volatile SingularAttribute state; + public static volatile SingularAttribute orderDate; + public static volatile SingularAttribute productId; + public static volatile SingularAttribute productName; +} diff --git a/src/main/java/com/myshop/order/query/dto/OrderView.java b/src/main/java/com/myshop/order/query/dto/OrderView.java new file mode 100644 index 0000000..f6944af --- /dev/null +++ b/src/main/java/com/myshop/order/query/dto/OrderView.java @@ -0,0 +1,42 @@ +package com.myshop.order.query.dto; + +import com.myshop.member.command.domain.MemberId; +import com.myshop.order.command.domain.OrderNo; +import com.myshop.order.command.domain.OrderState; + +public class OrderView { + + private final String number; + private final OrderState state; + private final String memberName; + private final String memberId; + private final String productName; + + public OrderView(OrderNo number, OrderState state, String memberName, MemberId memberId, String productName) { + this.number = number.getNumber(); + this.state = state; + this.memberName = memberName; + this.memberId = memberId.getId(); + this.productName = productName; + } + + public String getNumber() { + return number; + } + + public OrderState getState() { + return state; + } + + public String getMemberName() { + return memberName; + } + + public String getMemberId() { + return memberId; + } + + public String getProductName() { + return productName; + } +} diff --git a/src/main/java/com/myshop/order/ui/CancelOrderController.java b/src/main/java/com/myshop/order/ui/CancelOrderController.java new file mode 100644 index 0000000..b21482e --- /dev/null +++ b/src/main/java/com/myshop/order/ui/CancelOrderController.java @@ -0,0 +1,27 @@ +package com.myshop.order.ui; + +import com.myshop.order.command.application.CancelOrderService; +import com.myshop.order.command.domain.Canceller; +import com.myshop.order.command.domain.OrderNo; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; +import org.springframework.stereotype.Controller; +import org.springframework.ui.ModelMap; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; + +@Controller +public class CancelOrderController { + private CancelOrderService cancelOrderService; + + public CancelOrderController(CancelOrderService cancelOrderService) { + this.cancelOrderService = cancelOrderService; + } + + @RequestMapping("/my/orders/{orderNo}/cancel") + public String orderDetail(@PathVariable("orderNo") String orderNo, ModelMap modelMap) { + User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + cancelOrderService.cancel(new OrderNo(orderNo), new Canceller(user.getUsername())); + return "my/orderCanceled"; + } +} diff --git a/src/main/java/com/myshop/order/ui/MyOrderController.java b/src/main/java/com/myshop/order/ui/MyOrderController.java new file mode 100644 index 0000000..d292c66 --- /dev/null +++ b/src/main/java/com/myshop/order/ui/MyOrderController.java @@ -0,0 +1,48 @@ +package com.myshop.order.ui; + +import com.myshop.order.query.application.OrderDetail; +import com.myshop.order.query.application.OrderDetailService; +import com.myshop.order.query.dao.OrderSummaryDao; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; +import org.springframework.stereotype.Controller; +import org.springframework.ui.ModelMap; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.RequestMapping; + +import java.util.Optional; + +@Controller +public class MyOrderController { + private OrderDetailService orderDetailService; + private OrderSummaryDao orderSummaryDao; + + public MyOrderController(OrderDetailService orderDetailService, + OrderSummaryDao orderSummaryDao) { + this.orderDetailService = orderDetailService; + this.orderSummaryDao = orderSummaryDao; + } + + @RequestMapping("/my/orders") + public String orders(ModelMap modelMap) { + User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + modelMap.addAttribute("orders", orderSummaryDao.findByOrdererId(user.getUsername())); + return "my/orders"; + } + + @RequestMapping("/my/orders/{orderNo}") + public String orderDetail(@PathVariable("orderNo") String orderNo, ModelMap modelMap) { + User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + Optional orderDetail = orderDetailService.getOrderDetail(orderNo); + if (orderDetail.isPresent()) { + if (orderDetail.get().getOrderer().getMemberId().getId().equals(user.getUsername())) { + modelMap.addAttribute("order", orderDetail.get()); + return "my/orderDetail"; + } else { + return "my/notYourOrder"; + } + } else { + return "my/noOrder"; + } + } +} diff --git a/src/main/java/com/myshop/order/ui/OrderController.java b/src/main/java/com/myshop/order/ui/OrderController.java new file mode 100644 index 0000000..82e0b10 --- /dev/null +++ b/src/main/java/com/myshop/order/ui/OrderController.java @@ -0,0 +1,106 @@ +package com.myshop.order.ui; + +import com.myshop.catalog.query.product.ProductData; +import com.myshop.catalog.query.product.ProductQueryService; +import com.myshop.common.ValidationErrorException; +import com.myshop.member.command.domain.MemberId; +import com.myshop.order.command.application.NoOrderProductException; +import com.myshop.order.command.application.OrderProduct; +import com.myshop.order.command.application.OrderRequest; +import com.myshop.order.command.application.PlaceOrderService; +import com.myshop.order.command.domain.OrderNo; +import com.myshop.order.command.domain.OrdererService; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.User; +import org.springframework.stereotype.Controller; +import org.springframework.ui.ModelMap; +import org.springframework.validation.BindingResult; +import org.springframework.web.bind.WebDataBinder; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.InitBinder; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; + +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +@Controller +public class OrderController { + private ProductQueryService productQueryService; + private PlaceOrderService placeOrderService; + private OrdererService ordererService; + + public OrderController(ProductQueryService productQueryService, + PlaceOrderService placeOrderService, + OrdererService ordererService) { + this.productQueryService = productQueryService; + this.placeOrderService = placeOrderService; + this.ordererService = ordererService; + } + + @PostMapping("/orders/orderConfirm") + public String orderConfirm(@ModelAttribute("orderReq") OrderRequest orderRequest, + ModelMap modelMap) { + User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + orderRequest.setOrdererMemberId(MemberId.of(user.getUsername())); + populateProductsAndTotalAmountsModel(orderRequest, modelMap); + return "order/confirm"; + } + + private void populateProductsAndTotalAmountsModel(OrderRequest orderRequest, ModelMap modelMap) { + List products = getProducts(orderRequest.getOrderProducts()); + modelMap.addAttribute("products", products); + int totalAmounts = 0; + for (int i = 0 ; i < orderRequest.getOrderProducts().size() ; i++) { + OrderProduct op = orderRequest.getOrderProducts().get(i); + ProductData prod = products.get(i); + totalAmounts += op.getQuantity() * prod.getPrice().getValue(); + } + modelMap.addAttribute("totalAmounts", totalAmounts); + } + + private List getProducts(List orderProducts) { + List results = new ArrayList<>(); + for (OrderProduct op : orderProducts) { + Optional productOpt = productQueryService.getProduct(op.getProductId()); + ProductData product = productOpt.orElseThrow(() -> new NoOrderProductException(op.getProductId())); + results.add(product); + } + return results; + } + + @PostMapping("/orders/order") + public String order(@ModelAttribute("orderReq") OrderRequest orderRequest, + BindingResult bindingResult, + ModelMap modelMap) { + User user = (User) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + orderRequest.setOrdererMemberId(MemberId.of(user.getUsername())); + try { + OrderNo orderNo = placeOrderService.placeOrder(orderRequest); + modelMap.addAttribute("orderNo", orderNo.getNumber()); + return "order/orderComplete"; + } catch (ValidationErrorException e) { + e.getErrors().forEach(err -> { + if (err.hasName()) { + bindingResult.rejectValue(err.getName(), err.getCode()); + } else { + bindingResult.reject(err.getCode()); + } + }); + populateProductsAndTotalAmountsModel(orderRequest, modelMap); + return "order/confirm"; + } + } + + @ExceptionHandler(NoOrderProductException.class) + public String handleNoOrderProduct() { + return "order/noProduct"; + } + + @InitBinder + public void init(WebDataBinder binder) { + binder.initDirectFieldAccess(); + } + +} diff --git a/src/main/java/com/myshop/order/ui/OrderRequestValidator4Spring.java b/src/main/java/com/myshop/order/ui/OrderRequestValidator4Spring.java new file mode 100644 index 0000000..43e7057 --- /dev/null +++ b/src/main/java/com/myshop/order/ui/OrderRequestValidator4Spring.java @@ -0,0 +1,37 @@ +package com.myshop.order.ui; + +import com.myshop.order.command.application.OrderProduct; +import com.myshop.order.command.application.OrderRequest; +import org.springframework.validation.Errors; +import org.springframework.validation.ValidationUtils; +import org.springframework.validation.Validator; + +public class OrderRequestValidator4Spring implements Validator { + @Override + public boolean supports(Class aClass) { + return OrderRequest.class.isAssignableFrom(aClass); + } + + @Override + public void validate(Object o, Errors errors) { + OrderRequest orderReq = (OrderRequest) o; + if (orderReq.getOrderProducts() == null || orderReq.getOrderProducts().isEmpty()) { + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "orderProducts", "required"); + } else { + for (int i = 0; i < orderReq.getOrderProducts().size(); i++) { + OrderProduct orderProduct = orderReq.getOrderProducts().get(i); + if (orderProduct.getProductId() == null || orderProduct.getProductId().trim().isEmpty()) { + errors.rejectValue("orderProducts[" + i + "].productId", "required"); + } + if (orderProduct.getQuantity() <= 0) { + errors.rejectValue("orderProducts[" + i + "].quantity", "nonPositive"); + } + } + } + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "shippingInfo.receiver.name", "required"); + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "shippingInfo.receiver.phone", "required"); + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "shippingInfo.address.zipCode", "required"); + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "shippingInfo.address.address1", "required"); + ValidationUtils.rejectIfEmptyOrWhitespace(errors, "shippingInfo.address.address2", "required"); + } +} diff --git a/src/main/java/com/myshop/springconfig/security/CookieSecurityContextRepository.java b/src/main/java/com/myshop/springconfig/security/CookieSecurityContextRepository.java new file mode 100644 index 0000000..4a64f9a --- /dev/null +++ b/src/main/java/com/myshop/springconfig/security/CookieSecurityContextRepository.java @@ -0,0 +1,79 @@ +package com.myshop.springconfig.security; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.context.SecurityContext; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.core.userdetails.UsernameNotFoundException; +import org.springframework.security.web.context.HttpRequestResponseHolder; +import org.springframework.security.web.context.SecurityContextRepository; + +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.net.URLDecoder; + +import static com.myshop.springconfig.security.WebSecurityConfig.AUTHCOOKIENAME; + +public class CookieSecurityContextRepository implements SecurityContextRepository { + private Logger logger = LoggerFactory.getLogger(getClass()); + + private UserDetailsService userDetailsService; + + public CookieSecurityContextRepository(UserDetailsService userDetailsService) { + this.userDetailsService = userDetailsService; + } + + @Override + public SecurityContext loadContext(HttpRequestResponseHolder requestResponseHolder) { + SecurityContext sc = SecurityContextHolder.createEmptyContext(); + Cookie cookie = findAuthCookie(requestResponseHolder.getRequest()); + if (cookie != null) { + String id = getUserId(cookie); + if (id != null) { + populateAuthentication(sc, id); + } + } + return sc; + } + + private void populateAuthentication(SecurityContext sc, String id) { + try { + UserDetails userDetails = userDetailsService.loadUserByUsername(id); + sc.setAuthentication(new UsernamePasswordAuthenticationToken(userDetails, "", userDetails.getAuthorities())); + } catch (UsernameNotFoundException e) { + logger.debug("user name not found: " + id, e); + } + } + + private String getUserId(Cookie cookie) { + try { + return URLDecoder.decode(cookie.getValue(), "utf-8"); + } catch (Exception ex) { + return null; + } + } + + private Cookie findAuthCookie(HttpServletRequest request) { + Cookie[] cookies = request.getCookies(); + if (cookies == null || cookies.length == 0) return null; + for (Cookie c : cookies) { + if (c.getName().equals(AUTHCOOKIENAME)) { + return c; + } + } + return null; + } + + @Override + public void saveContext(SecurityContext context, HttpServletRequest request, HttpServletResponse response) { + } + + @Override + public boolean containsContext(HttpServletRequest request) { + return false; + } +} diff --git a/src/main/java/com/myshop/springconfig/security/CustomAuthSuccessHandler.java b/src/main/java/com/myshop/springconfig/security/CustomAuthSuccessHandler.java new file mode 100644 index 0000000..b9b2347 --- /dev/null +++ b/src/main/java/com/myshop/springconfig/security/CustomAuthSuccessHandler.java @@ -0,0 +1,35 @@ +package com.myshop.springconfig.security; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; + +import javax.servlet.ServletException; +import javax.servlet.http.Cookie; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.io.UnsupportedEncodingException; +import java.net.URLEncoder; + +import static com.myshop.springconfig.security.WebSecurityConfig.AUTHCOOKIENAME; + +public class CustomAuthSuccessHandler implements AuthenticationSuccessHandler { + + @Override + public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { + UserDetails user = (UserDetails) authentication.getPrincipal(); + try { + Cookie authCookie = new Cookie(AUTHCOOKIENAME, URLEncoder.encode(encryptId(user), "UTF-8")); + authCookie.setPath("/"); + response.addCookie(authCookie); + } catch (UnsupportedEncodingException ex) { + throw new RuntimeException(ex); + } + response.sendRedirect("/home"); + } + + private String encryptId(UserDetails user) { + return user.getUsername(); + } +} diff --git a/src/main/java/com/myshop/springconfig/security/WebSecurityConfig.java b/src/main/java/com/myshop/springconfig/security/WebSecurityConfig.java new file mode 100644 index 0000000..5728802 --- /dev/null +++ b/src/main/java/com/myshop/springconfig/security/WebSecurityConfig.java @@ -0,0 +1,79 @@ +package com.myshop.springconfig.security; + +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; +import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; +import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; +import org.springframework.security.config.annotation.web.builders.HttpSecurity; +import org.springframework.security.config.annotation.web.builders.WebSecurity; +import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; +import org.springframework.security.core.userdetails.UserDetailsService; +import org.springframework.security.crypto.password.NoOpPasswordEncoder; +import org.springframework.security.web.context.SecurityContextRepository; +import org.springframework.security.web.savedrequest.NullRequestCache; + +import javax.sql.DataSource; + +@EnableWebSecurity +@EnableGlobalMethodSecurity(prePostEnabled = true) +@Configuration +public class WebSecurityConfig extends WebSecurityConfigurerAdapter { + public static final String AUTHCOOKIENAME = "AUTH"; + + @Autowired + private DataSource dataSource; + + @Override + protected void configure(AuthenticationManagerBuilder auth) throws Exception { + auth + .jdbcAuthentication() + .dataSource(dataSource) + .usersByUsernameQuery("select member_id, password, 'true' from member where member_id = ?") + .authoritiesByUsernameQuery("select member_id, authority from member_authorities where member_id = ?") + .passwordEncoder(NoOpPasswordEncoder.getInstance()) + ; + } + + @Override + public void configure(WebSecurity web) throws Exception { + web.ignoring().antMatchers( + "/vendor/**", + "/api/**", + "/images/**", + "/favicon.ico"); + } + + @Override + protected void configure(HttpSecurity http) throws Exception { + http.securityContext().securityContextRepository(new CookieSecurityContextRepository(userDetailsService())); + http.requestCache().requestCache(new NullRequestCache()); + + http + .authorizeRequests() + .antMatchers("/", "/home", "/categories/**", "/products/**").permitAll() + .antMatchers("/admin/**").hasRole("ADMIN") + .anyRequest().authenticated() + .and() + .formLogin() // login + .loginPage("/login") + .permitAll() + .successHandler(new CustomAuthSuccessHandler()) + .and() + .logout() // /login?logout + .logoutUrl("/logout") + .logoutSuccessUrl("/loggedOut") + .deleteCookies(AUTHCOOKIENAME) + .permitAll() + .and() + .csrf().disable() + ; + } + + @Bean + @Override + public UserDetailsService userDetailsServiceBean() { + return super.userDetailsService(); + } +} diff --git a/src/main/java/com/myshop/springconfig/web/WebMvcConfig.java b/src/main/java/com/myshop/springconfig/web/WebMvcConfig.java new file mode 100644 index 0000000..f1ed49b --- /dev/null +++ b/src/main/java/com/myshop/springconfig/web/WebMvcConfig.java @@ -0,0 +1,22 @@ +package com.myshop.springconfig.web; + +import org.springframework.context.annotation.Configuration; +import org.springframework.web.servlet.config.annotation.ViewControllerRegistry; +import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; + +@Configuration +public class WebMvcConfig implements WebMvcConfigurer { + + @Override + public void addViewControllers(ViewControllerRegistry registry) { + registry.addRedirectViewController("/", "/home"); + registry.addViewController("/home").setViewName("home"); + registry.addViewController("/login").setViewName("login"); + registry.addViewController("/error/forbidden").setViewName("error/forbidden"); + registry.addViewController("/error/notFound").setViewName("error/notFound"); + registry.addViewController("/my/main").setViewName("my/myMain"); + registry.addViewController("/admin/main").setViewName("admin/adminMain"); + registry.addViewController("/loggedOut").setViewName("loggedOut"); + } + +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties new file mode 100644 index 0000000..f8e2a48 --- /dev/null +++ b/src/main/resources/application.properties @@ -0,0 +1,15 @@ +spring.datasource.url=jdbc:mysql://localhost/shop?characterEncoding=utf8 +spring.datasource.username=shopuser +spring.datasource.password=shoppass +spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver +spring.datasource.hikari.maximum-pool-size=10 + +spring.jpa.database=mysql +spring.jpa.show-sql=true +spring.jpa.hibernate.naming.physical-strategy=org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl + +spring.jpa.open-in-view=false + +logging.level.root=INFO +logging.level.com.myshop=DEBUG +logging.level.org.springframework.security=DEBUG \ No newline at end of file diff --git a/src/main/resources/messages.properties b/src/main/resources/messages.properties new file mode 100644 index 0000000..44553d5 --- /dev/null +++ b/src/main/resources/messages.properties @@ -0,0 +1,7 @@ +required=필수항목입니다. +PAYMENT_WAITING=결제 대기 +PREPARING=상품 준비 +SHIPPED=상품 발송 +DELIVERING=배송 중 +DELIVERY_COMPLETED=배송 완료 +CANCELED=취소 diff --git a/src/main/resources/static/favicon.ico b/src/main/resources/static/favicon.ico new file mode 100644 index 0000000..cf919e4 Binary files /dev/null and b/src/main/resources/static/favicon.ico differ diff --git a/src/main/resources/static/images/original/pen.jpg b/src/main/resources/static/images/original/pen.jpg new file mode 100644 index 0000000..508bc81 Binary files /dev/null and b/src/main/resources/static/images/original/pen.jpg differ diff --git a/src/main/resources/static/images/original/pen2.jpg b/src/main/resources/static/images/original/pen2.jpg new file mode 100644 index 0000000..666c88d Binary files /dev/null and b/src/main/resources/static/images/original/pen2.jpg differ diff --git a/src/main/resources/static/images/original/rpi3.jpg b/src/main/resources/static/images/original/rpi3.jpg new file mode 100644 index 0000000..264b0ad Binary files /dev/null and b/src/main/resources/static/images/original/rpi3.jpg differ diff --git a/src/main/resources/static/images/original/wbp.png b/src/main/resources/static/images/original/wbp.png new file mode 100644 index 0000000..50c2f15 Binary files /dev/null and b/src/main/resources/static/images/original/wbp.png differ diff --git a/src/main/resources/static/images/thumbnail/pen.jpg b/src/main/resources/static/images/thumbnail/pen.jpg new file mode 100644 index 0000000..5315ccb Binary files /dev/null and b/src/main/resources/static/images/thumbnail/pen.jpg differ diff --git a/src/main/resources/static/images/thumbnail/pen2.jpg b/src/main/resources/static/images/thumbnail/pen2.jpg new file mode 100644 index 0000000..9fff378 Binary files /dev/null and b/src/main/resources/static/images/thumbnail/pen2.jpg differ diff --git a/src/main/resources/static/images/thumbnail/rpi3.jpg b/src/main/resources/static/images/thumbnail/rpi3.jpg new file mode 100644 index 0000000..a2c542e Binary files /dev/null and b/src/main/resources/static/images/thumbnail/rpi3.jpg differ diff --git a/src/main/resources/static/images/thumbnail/wbp.png b/src/main/resources/static/images/thumbnail/wbp.png new file mode 100644 index 0000000..d2bf511 Binary files /dev/null and b/src/main/resources/static/images/thumbnail/wbp.png differ diff --git a/src/main/resources/templates/admin/adminMain.html b/src/main/resources/templates/admin/adminMain.html new file mode 100644 index 0000000..2ebf959 --- /dev/null +++ b/src/main/resources/templates/admin/adminMain.html @@ -0,0 +1,22 @@ + + + + + 관리자 페이지 + + + + +
+ +
+

관리자

+ +
+ +
+ + + diff --git a/src/main/resources/templates/admin/adminOrderDetail.html b/src/main/resources/templates/admin/adminOrderDetail.html new file mode 100644 index 0000000..4a66c84 --- /dev/null +++ b/src/main/resources/templates/admin/adminOrderDetail.html @@ -0,0 +1,103 @@ + + + + + + + + + +
+ +
+ + +

주문 상세

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
주문번호
상태
주문자이름
받는사람이름
연락처
주소 + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
상품가격개수
총합총합
+ + +
+ +
+ +
+ +
+ + + + \ No newline at end of file diff --git a/src/main/resources/templates/admin/adminOrderLockFail.html b/src/main/resources/templates/admin/adminOrderLockFail.html new file mode 100644 index 0000000..feec52b --- /dev/null +++ b/src/main/resources/templates/admin/adminOrderLockFail.html @@ -0,0 +1,33 @@ +<%@ page contentType="text/html; charset=utf-8" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> + + + + + 관리자 주문 배송 처리 실패 : ${order.number} + + + + + + +
+ + + + + 주문 내역 보기 +
+ + + + + \ No newline at end of file diff --git a/src/main/resources/templates/admin/adminOrderShipped.html b/src/main/resources/templates/admin/adminOrderShipped.html new file mode 100644 index 0000000..93cc694 --- /dev/null +++ b/src/main/resources/templates/admin/adminOrderShipped.html @@ -0,0 +1,33 @@ +<%@ page contentType="text/html; charset=utf-8" %> +<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %> +<%@ taglib prefix="form" uri="http://www.springframework.org/tags/form" %> +<%@ taglib prefix="spring" uri="http://www.springframework.org/tags" %> + + + + + 관리자 주문 배송 처리 : ${order.number} + + + + + + +
+ + + + + 주문 내역 보기 +
+ + + + + \ No newline at end of file diff --git a/src/main/resources/templates/admin/adminOrders.html b/src/main/resources/templates/admin/adminOrders.html new file mode 100644 index 0000000..899e404 --- /dev/null +++ b/src/main/resources/templates/admin/adminOrders.html @@ -0,0 +1,64 @@ + + + + + 관리자 : 주문 목록 + + + + +
+ +
+ + +

주문 목록

+ + + + + + + + + + + + + + + + + + + +
주문번호금액상태상품
주문번호구매금액상태[[${order.productName}]] 등
+ + +
+ +
+ + + \ No newline at end of file diff --git a/src/main/resources/templates/category/categoryList.html b/src/main/resources/templates/category/categoryList.html new file mode 100644 index 0000000..b6309d6 --- /dev/null +++ b/src/main/resources/templates/category/categoryList.html @@ -0,0 +1,24 @@ + + + + + 카테고리 목록 + + + + +
+ +
+

카테고리 목록

+
    +
  • + +
  • +
+
+ +
+ + + diff --git a/src/main/resources/templates/category/productDetail.html b/src/main/resources/templates/category/productDetail.html new file mode 100644 index 0000000..c441da2 --- /dev/null +++ b/src/main/resources/templates/category/productDetail.html @@ -0,0 +1,57 @@ + + + + + 제품 상세 + + + + +
+ +
+
+
+
+
+
+
제품명
+

+ 가격 : +
+ 상세 : +

+ +
+
+
+
+ + +
+ + + + + +
+ + +
+ + \ No newline at end of file diff --git a/src/main/resources/templates/category/productList.html b/src/main/resources/templates/category/productList.html new file mode 100644 index 0000000..15cbaa4 --- /dev/null +++ b/src/main/resources/templates/category/productList.html @@ -0,0 +1,38 @@ + + + + + 제품 목록 + + + + +
+ +
+

카테고리

+ + 상품이 없습니다. + +
+
+
+ + + +
+
+
+
이름
+

가격 : [[${product.price}]]

+
+
+
+
+
+ +
+ + + diff --git a/src/main/resources/templates/fragments/layout.html b/src/main/resources/templates/fragments/layout.html new file mode 100644 index 0000000..c95bcc4 --- /dev/null +++ b/src/main/resources/templates/fragments/layout.html @@ -0,0 +1,42 @@ + + + + + Title + + + + + + + + + + +
+

도메인 구현 시작하기, 문의: madvirus@madvirus.net

+
+ + + \ No newline at end of file diff --git a/src/main/resources/templates/home.html b/src/main/resources/templates/home.html new file mode 100644 index 0000000..64d1b15 --- /dev/null +++ b/src/main/resources/templates/home.html @@ -0,0 +1,22 @@ + + + + + 마이샵 + + + + +
+ +
+
+

오늘의 특가

+

라즈베리파이 3

+ 상품 보기 +
+
+ +
+ + \ No newline at end of file diff --git a/src/main/resources/templates/loggedOut.html b/src/main/resources/templates/loggedOut.html new file mode 100644 index 0000000..7af7b10 --- /dev/null +++ b/src/main/resources/templates/loggedOut.html @@ -0,0 +1,20 @@ + + + + + 마이샵 + + + + +
+ +
+ +
+ +
+ + \ No newline at end of file diff --git a/src/main/resources/templates/login.html b/src/main/resources/templates/login.html new file mode 100644 index 0000000..2603ffd --- /dev/null +++ b/src/main/resources/templates/login.html @@ -0,0 +1,29 @@ + + + + + 로그인 + + + + +
+ +
+
+
+ + +
+
+ + +
+ +
+
+ +
+
+ + \ No newline at end of file diff --git a/src/main/resources/templates/my/myMain.html b/src/main/resources/templates/my/myMain.html new file mode 100644 index 0000000..eb2141c --- /dev/null +++ b/src/main/resources/templates/my/myMain.html @@ -0,0 +1,22 @@ + + + + + 마이샵 + + + + +
+ +
+

user

+ +
+ +
+ + + \ No newline at end of file diff --git a/src/main/resources/templates/my/noOrder.html b/src/main/resources/templates/my/noOrder.html new file mode 100644 index 0000000..5a45b22 --- /dev/null +++ b/src/main/resources/templates/my/noOrder.html @@ -0,0 +1,21 @@ + + + + + 내 주문 + + + + +
+ +
+ +
+ +
+ + + diff --git a/src/main/resources/templates/my/notYourOrder.html b/src/main/resources/templates/my/notYourOrder.html new file mode 100644 index 0000000..7127bc8 --- /dev/null +++ b/src/main/resources/templates/my/notYourOrder.html @@ -0,0 +1,21 @@ + + + + + 내 주문 + + + + +
+ +
+ +
+ +
+ + + diff --git a/src/main/resources/templates/my/orderCanceled.html b/src/main/resources/templates/my/orderCanceled.html new file mode 100644 index 0000000..6efd018 --- /dev/null +++ b/src/main/resources/templates/my/orderCanceled.html @@ -0,0 +1,25 @@ + + + + + 마이샵 + + + + +
+ +
+ + + 주문 내역 보기 + 주문 목록 보기 + 첫 화면으로 이동하기 +
+ +
+ + + diff --git a/src/main/resources/templates/my/orderDetail.html b/src/main/resources/templates/my/orderDetail.html new file mode 100644 index 0000000..8a23134 --- /dev/null +++ b/src/main/resources/templates/my/orderDetail.html @@ -0,0 +1,99 @@ + + + + + 주문 상세 + + + + +
+ +
+

주문 상세

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
주문번호
상태
주문자이름
받는사람이름
연락처
주소 + + + +
+ + + + + + + + + + + + + + + + + + + + + + + + +
상품가격개수
총합총합
+ + + + + 주문 목록 보기 + 첫 화면으로 이동하기 +
+ +
+ +
+ + + + + \ No newline at end of file diff --git a/src/main/resources/templates/my/orders.html b/src/main/resources/templates/my/orders.html new file mode 100644 index 0000000..645a18b --- /dev/null +++ b/src/main/resources/templates/my/orders.html @@ -0,0 +1,42 @@ + + + + + 마이샵 + + + + +
+ +
+

주문 목록

+ + + + + + + + + + + + + + + + + + + + +
주문번호금액상태상품
주문번호금액상태제품명
+
+ +
+ + + \ No newline at end of file diff --git a/src/main/resources/templates/order/confirm.html b/src/main/resources/templates/order/confirm.html new file mode 100644 index 0000000..2359074 --- /dev/null +++ b/src/main/resources/templates/order/confirm.html @@ -0,0 +1,138 @@ + + + + + 주문 확인 + + + + +
+ +
+
+
+
+
주문자
+
+ +
+ +
+
+
+
+ +
+
+
주문 상품
+ + + + + + + + + + + + + + + + + + + + + + + +
상품가격개수
제품명10001 + + + 금액
총합총합
+
+
+ +
+
+
받는 사람
+
+ +
+ +
+ 입력 오류 메시지 +
+
+
+
+ +
+ +
+ 입력 오류 메시지 +
+
+
+
+ +
+ +
+ 입력 오류 메시지 +
+
+
+
+ +
+ +
+ 입력 오류 메시지 +
+
+
+
+ +
+ +
+ 입력 오류 메시지 +
+
+
+
+
+ + +
+
+ +
+ + + \ No newline at end of file diff --git a/src/main/resources/templates/order/noProduct.html b/src/main/resources/templates/order/noProduct.html new file mode 100644 index 0000000..ab020b8 --- /dev/null +++ b/src/main/resources/templates/order/noProduct.html @@ -0,0 +1,21 @@ + + + + + 마이샵 + + + + +
+ +
+ +
+ +
+ + + \ No newline at end of file diff --git a/src/main/resources/templates/order/orderComplete.html b/src/main/resources/templates/order/orderComplete.html new file mode 100644 index 0000000..da225d0 --- /dev/null +++ b/src/main/resources/templates/order/orderComplete.html @@ -0,0 +1,24 @@ + + + + + 마이샵 + + + + +
+ +
+ + + 주문 내역 보기 + 첫 화면으로 이동하기 +
+ +
+ + + \ No newline at end of file diff --git a/src/sql/ddl.sql b/src/sql/ddl.sql new file mode 100644 index 0000000..986ea1e --- /dev/null +++ b/src/sql/ddl.sql @@ -0,0 +1,106 @@ +create database shop character set utf8mb4 collate utf8mb4_general_ci; + +CREATE USER 'shopuser'@'localhost' IDENTIFIED BY 'shoppass'; +CREATE USER 'shopuser'@'%' IDENTIFIED BY 'shoppass'; + +GRANT ALL PRIVILEGES ON shop.* TO 'shopuser'@'localhost'; +GRANT ALL PRIVILEGES ON shop.* TO 'shopuser'@'%'; + +use shop; + +create table shop.purchase_order ( + order_number varchar(50) not null primary key, + version bigint, + orderer_id varchar(50), + orderer_name varchar(50), + total_amounts int, + shipping_zip_code varchar(6), + shipping_addr1 varchar(100), + shipping_addr2 varchar(100), + shipping_message varchar(200), + receiver_name varchar(50), + receiver_phone varchar(50), + state varchar(20), + order_date datetime +) character set utf8mb4; + +create table shop.order_line ( + order_number varchar(50) not null, + line_idx int not null, + product_id varchar(50) not null, + price int, + quantity int, + amounts int +) character set utf8mb4; + +create index order_line_idx ON order_line (order_number, line_idx); + +create table shop.category ( + category_id bigint not null primary key, + name varchar(100) +) character set utf8mb4; + +create table shop.product ( + product_id varchar(50) not null primary key, + name varchar(100), + price int, + detail text +) character set utf8mb4; + +create table shop.product_category ( + product_id varchar(50) not null, + category_id bigint not null, + constraint primary key (product_id, category_id) +) character set utf8mb4; + +create table shop.image ( + image_id int not null auto_increment primary key, + product_id varchar(50), + list_idx int, + image_type varchar(10), + image_path varchar(255), + upload_time datetime +) character set utf8mb4; + +create table shop.member ( + member_id varchar(50) not null primary key, + name varchar(50), + password varchar(255), + blocked boolean, + emails varchar(200) +) character set utf8mb4; + +create table shop.member_authorities ( + member_id varchar(50) not null, + authority varchar(50) not null, + primary key (member_id, authority) +) character set utf8mb4; + +create table shop.article ( + id int not null auto_increment primary key, + title varchar(255) +) character set utf8mb4; + +create table shop.article_content ( + id int not null primary key, + content varchar(255), + content_type varchar(255) +) character set utf8mb4; + +create table shop.evententry ( + id int not null AUTO_INCREMENT PRIMARY KEY, + `type` varchar(255), + `content_type` varchar(255), + payload MEDIUMTEXT, + `timestamp` datetime +) character set utf8mb4; + +create table shop.locks ( + `type` varchar(255), + id varchar(255), + lockid varchar(255), + expiration_time datetime, + primary key (`type`, id) +) character set utf8mb4; + +create unique index locks_idx ON shop.locks (lockid); \ No newline at end of file diff --git a/src/sql/init.sql b/src/sql/init.sql new file mode 100644 index 0000000..a7ad61b --- /dev/null +++ b/src/sql/init.sql @@ -0,0 +1,46 @@ +use shop; + +truncate table purchase_order; +truncate table order_line; +truncate table category; +truncate table product_category; +truncate table product; +truncate table image; +truncate table member; +truncate table member_authorities; +truncate table article; +truncate table article_content; +truncate table evententry; + +insert into member (member_id, name, password, blocked) values ('user1', '사용자1', '1234', false); +insert into member (member_id, name, password, blocked) values ('user2', '사용자2', '5678', false); +insert into member (member_id, name, password, blocked) values ('admin', '운영자', 'admin1234', false); +insert into member_authorities values ('user1', 'ROLE_USER'); +insert into member_authorities values ('user2', 'ROLE_USER'); +insert into member_authorities values ('admin', 'ROLE_ADMIN'); + +insert into category values (1001, '전자제품'); +insert into category values (2001, '필기구'); + +insert into product values ('prod-001', '라즈베리파이3 모델B', 56000, '모델B'); +insert into image (product_id, list_idx, image_type, image_path, upload_time) values + ('prod-001', 0, 'II', 'rpi3.jpg', now()); +insert into image (product_id, list_idx, image_type, image_path, upload_time) values + ('prod-001', 1, 'EI', 'http://external/image/path', now()); + +insert into product_category values ('prod-001', 1001); + +insert into product values ('prod-002', '어프로치 휴대용 화이트보드 세트', 11920, '화이트보드'); +insert into image (product_id, list_idx, image_type, image_path, upload_time) values + ('prod-002', 0, 'II', 'wbp.png', now()); + +insert into product_category values ('prod-002', 2001); + +insert into product values ('prod-003', '볼펜 겸용 터치펜', 9000, '볼펜과 터치펜을 하나로!'); +insert into image (product_id, list_idx, image_type, image_path, upload_time) values + ('prod-003', 0, 'II', 'pen.jpg', now()); +insert into image (product_id, list_idx, image_type, image_path, upload_time) values + ('prod-003', 1, 'II', 'pen2.jpg', now()); + +insert into product_category values ('prod-003', 1001); +insert into product_category values ('prod-003', 2001); \ No newline at end of file diff --git a/src/test/java/com/myshop/ShopApplicationTests.java b/src/test/java/com/myshop/ShopApplicationTests.java new file mode 100644 index 0000000..7f537a2 --- /dev/null +++ b/src/test/java/com/myshop/ShopApplicationTests.java @@ -0,0 +1,13 @@ +package com.myshop; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ShopApplicationTests { + + @Test + void contextLoads() { + } + +} diff --git a/src/test/java/com/myshop/board/domain/ArticleRepositoryIT.java b/src/test/java/com/myshop/board/domain/ArticleRepositoryIT.java new file mode 100644 index 0000000..ca1108f --- /dev/null +++ b/src/test/java/com/myshop/board/domain/ArticleRepositoryIT.java @@ -0,0 +1,70 @@ +package com.myshop.board.domain; + +import com.myshop.helper.DbHelper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.support.rowset.SqlRowSet; + +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +public class ArticleRepositoryIT { + @Autowired + private ArticleRepository articleRepository; + @Autowired + private JdbcTemplate jdbcTemplate; + + private DbHelper dbHelper; + + @BeforeEach + void setUp() { + dbHelper = new DbHelper(jdbcTemplate); + dbHelper.clear(); + } + + @Test + void save() { + Article aritcle = new Article("title", + new ArticleContent("content", "type") + ); + articleRepository.save(aritcle); + + SqlRowSet rsArticle = jdbcTemplate.queryForRowSet( + "select * from article where id = ?", + aritcle.getId()); + assertThat(rsArticle.next()).isTrue(); + assertThat(rsArticle.getString("title")).isEqualTo("title"); + + SqlRowSet rsContent = jdbcTemplate.queryForRowSet( + "select * from article_content where id = ?", + aritcle.getId()); + assertThat(rsContent.next()).isTrue(); + assertThat(rsContent.getString("content")).isEqualTo("content"); + assertThat(rsContent.getString("content_type")).isEqualTo("type"); + } + + @Test + void findByIdNoData() { + assertThat(articleRepository.findById(0L)).isEmpty(); + } + + @Test + void findById() { + jdbcTemplate.update("insert into article values (100, 'title')"); + jdbcTemplate.update("insert into article_content values (100, 'content', 'type')"); + + Optional
articleOpt = articleRepository.findById(100L); + assertThat(articleOpt).isPresent(); + Article article = articleOpt.get(); + assertThat(article.getTitle()).isEqualTo("title"); + assertThat(article.getContent().getContent()).isEqualTo("content"); + assertThat(article.getContent().getContentType()).isEqualTo("type"); + } +} diff --git a/src/test/java/com/myshop/catalog/command/domain/product/ProductRepositoryIT.java b/src/test/java/com/myshop/catalog/command/domain/product/ProductRepositoryIT.java new file mode 100644 index 0000000..bace421 --- /dev/null +++ b/src/test/java/com/myshop/catalog/command/domain/product/ProductRepositoryIT.java @@ -0,0 +1,72 @@ +package com.myshop.catalog.command.domain.product; + +import com.myshop.common.model.Money; +import com.myshop.helper.DbHelper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.support.rowset.SqlRowSet; +import org.springframework.transaction.annotation.Transactional; + +import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +class ProductRepositoryIT { + @Autowired + private ProductRepository productRepository; + @Autowired + private JdbcTemplate jdbcTemplate; + @Autowired + private DbHelper dbHelper; + + @BeforeEach + void setUp() { + dbHelper.clear(); + } + + @Test + void save() { + List images = new ArrayList<>(); + images.add(new ExternalImage("https://extern.image.com/some-img.png")); + images.add(new InternalImage("internal-img.png")); + Product product = new Product( + ProductId.of("PRD-01"), + "제품-01", + new Money(9000), + "상세 내용", + images); + productRepository.save(product); + + SqlRowSet rsProd = jdbcTemplate.queryForRowSet("select * from product where product_id = ?", "PRD-01"); + assertThat(rsProd.next()).isTrue(); + assertThat(rsProd.getInt("price")).isEqualTo(9000); + + SqlRowSet rsImg = jdbcTemplate.queryForRowSet("select * from image where product_id = ? order by list_idx", "PRD-01"); + assertThat(rsImg.next()).isTrue(); + assertThat(rsImg.getString("image_type")).isEqualTo("EI"); + } + + @Transactional + @Test + void updateImages() { + jdbcTemplate.update("insert into product values (?,?,?,?)", "PROD-02", "PRD 2", 10000, "상세"); + jdbcTemplate.update("insert into image values (?,?,?,?,?,?)", + 1, "PROD-02", 0, "EI", "http://images.img/img.01.png", LocalDateTime.now()); + jdbcTemplate.update("insert into image values (?,?,?,?,?,?)", + 2, "PROD-02", 1, "EI", "http://images.img/img.02.png", LocalDateTime.now()); + + Product product = productRepository.findById(ProductId.of("PROD-02")).get(); + product.changeImages(List.of( + new InternalImage("/path01.png"), + new InternalImage("/path02.png") + )); + productRepository.flush(); + } +} \ No newline at end of file diff --git a/src/test/java/com/myshop/catalog/domain/category/CategoryRepositoryIT.java b/src/test/java/com/myshop/catalog/domain/category/CategoryRepositoryIT.java new file mode 100644 index 0000000..2729180 --- /dev/null +++ b/src/test/java/com/myshop/catalog/domain/category/CategoryRepositoryIT.java @@ -0,0 +1,45 @@ +package com.myshop.catalog.domain.category; + +import com.myshop.catalog.command.domain.category.Category; +import com.myshop.catalog.command.domain.category.CategoryId; +import com.myshop.catalog.command.domain.category.CategoryRepository; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.AutoConfigureTestDatabase; +import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +@DataJpaTest +@AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE) +class CategoryRepositoryIT { + @Autowired + private CategoryRepository categoryRepository; + @Autowired + private JdbcTemplate jdbcTemplate; + + @BeforeEach + void setUp() { + jdbcTemplate.update("truncate table category"); + jdbcTemplate.update("insert into category values (1, 'cat1')"); + jdbcTemplate.update("insert into category values (2, 'cat2')"); + } + + @Test + void findAll() { + List categories = categoryRepository.findAll(); + assertThat(categories).hasSize(2); + } + + @Test + void findById() { + Optional catOpt = categoryRepository.findById(CategoryId.of(2L)); + assertThat(catOpt).isPresent(); + assertThat(catOpt.get().getName()).isEqualTo("cat2"); + } +} \ No newline at end of file diff --git a/src/test/java/com/myshop/catalog/domain/product/ProductRepositoryIT.java b/src/test/java/com/myshop/catalog/domain/product/ProductRepositoryIT.java new file mode 100644 index 0000000..a152818 --- /dev/null +++ b/src/test/java/com/myshop/catalog/domain/product/ProductRepositoryIT.java @@ -0,0 +1,13 @@ +package com.myshop.catalog.domain.product; + +import com.myshop.catalog.command.domain.product.ProductRepository; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class ProductRepositoryIT { + @Autowired + private ProductRepository productRepository; + + +} \ No newline at end of file diff --git a/src/test/java/com/myshop/catalog/query/product/ProductDataDaoIT.java b/src/test/java/com/myshop/catalog/query/product/ProductDataDaoIT.java new file mode 100644 index 0000000..4b6d5ae --- /dev/null +++ b/src/test/java/com/myshop/catalog/query/product/ProductDataDaoIT.java @@ -0,0 +1,60 @@ +package com.myshop.catalog.query.product; + +import com.myshop.catalog.command.domain.category.CategoryId; +import com.myshop.catalog.command.domain.product.ProductId; +import com.myshop.helper.DbHelper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.time.LocalDateTime; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +class ProductDataDaoIT { + @Autowired + private ProductDataDao productDataDao; + @Autowired + private JdbcTemplate jdbcTemplate; + @Autowired + private DbHelper dbHelper; + + @BeforeEach + void setUp() { + dbHelper.clear(); + } + + @Test + void findByCategoryIds() { + jdbcTemplate.update("insert into product values (?,?,?,?)", "PROD-01", "PRD 1", 10000, "상세"); + jdbcTemplate.update("insert into image values (?,?,?,?,?,?)", + 1, "PROD-02", 0, "EI", "http://images.img/img.01.png", LocalDateTime.now()); + jdbcTemplate.update("insert into product_category values (?, ?)", "PROD-01", 1); + jdbcTemplate.update("insert into product_category values (?, ?)", "PROD-01", 2); + + jdbcTemplate.update("insert into product values (?,?,?,?)", "PROD-02", "PRD 2", 10000, "상세"); + jdbcTemplate.update("insert into product_category values (?, ?)", "PROD-02", 2); + jdbcTemplate.update("insert into product_category values (?, ?)", "PROD-02", 3); + + jdbcTemplate.update("insert into product values (?,?,?,?)", "PROD-03", "PRD 3", 10000, "상세"); + jdbcTemplate.update("insert into product_category values (?, ?)", "PROD-03", 2); + jdbcTemplate.update("insert into product_category values (?, ?)", "PROD-03", 3); + + jdbcTemplate.update("insert into product values (?,?,?,?)", "PROD-04", "PRD 4", 10000, "상세"); + jdbcTemplate.update("insert into product_category values (?, ?)", "PROD-04", 1); + jdbcTemplate.update("insert into product_category values (?, ?)", "PROD-04", 3); + + PageRequest pageRequest = PageRequest.of(0, 2, Sort.by(Sort.Direction.DESC, "id")); + Page products = productDataDao.findByCategoryIdsContains(CategoryId.of(2L), pageRequest); + assertThat(products.getNumberOfElements()).isEqualTo(2); + assertThat(products.getContent()).hasSize(2); + assertThat(products.getContent().get(0).getId()).isEqualTo(ProductId.of("PROD-03")); + assertThat(products.getContent().get(1).getId()).isEqualTo(ProductId.of("PROD-02")); + } +} \ No newline at end of file diff --git a/src/test/java/com/myshop/helper/DbHelper.java b/src/test/java/com/myshop/helper/DbHelper.java new file mode 100644 index 0000000..f77ed62 --- /dev/null +++ b/src/test/java/com/myshop/helper/DbHelper.java @@ -0,0 +1,25 @@ +package com.myshop.helper; + +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.stereotype.Component; + +@Component +public class DbHelper { + private JdbcTemplate jdbcTemplate; + + public DbHelper(JdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + public void clear() { + jdbcTemplate.update("truncate table member"); + jdbcTemplate.update("truncate table article"); + jdbcTemplate.update("truncate table article_content"); + jdbcTemplate.update("truncate table purchase_order"); + jdbcTemplate.update("truncate table order_line"); + jdbcTemplate.update("truncate table product"); + jdbcTemplate.update("truncate table image"); + jdbcTemplate.update("truncate table category"); + jdbcTemplate.update("truncate table product_category"); + } +} diff --git a/src/test/java/com/myshop/member/command/domain/MemberRepositoryIT.java b/src/test/java/com/myshop/member/command/domain/MemberRepositoryIT.java new file mode 100644 index 0000000..2d95df1 --- /dev/null +++ b/src/test/java/com/myshop/member/command/domain/MemberRepositoryIT.java @@ -0,0 +1,71 @@ +package com.myshop.member.command.domain; + +import com.myshop.common.model.Email; +import com.myshop.helper.DbHelper; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; +import org.springframework.jdbc.support.rowset.SqlRowSet; +import org.springframework.transaction.annotation.Transactional; + +import javax.persistence.LockModeType; +import java.util.Optional; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +class MemberRepositoryIT { + @Autowired + private MemberRepository memberRepository; + @Autowired + private DbHelper dbHelper; + @Autowired + private JdbcTemplate jdbcTemplate; + + @BeforeEach + void setUp() { + dbHelper.clear(); + } + + @Test + void findById() { + jdbcTemplate.update( + """ + insert into member values (?, ?, ?, ?, ?) + """, + "member1", "이름", "p1", false, "email1@email.com,email2@email.com" + ); + Optional memOpt = memberRepository.findById(MemberId.of("member1")); + assertThat(memOpt).isPresent(); + Member member = memOpt.get(); + assertThat(member.getEmails().getEmails()).contains( + Email.of("email1@email.com"), Email.of("email2@email.com") + ); + } + + @Test + void save() { + Member member = new Member(MemberId.of("id1"), "이름2"); + member.changeEmails(Set.of(Email.of("mail1@mail.com"), Email.of("mail2@mail.com"))); + memberRepository.save(member); + + SqlRowSet rs = jdbcTemplate.queryForRowSet("select * from member where member_id = ?", "id1"); + assertThat(rs.next()).isTrue(); + assertThat(rs.getString("emails")).contains("mail1@mail.com", "mail2@mail.com"); + } + + @Transactional + @Test + void findByIdForUpdate() { + jdbcTemplate.update( + """ + insert into member values (?, ?, ?, ?, ?) + """, + "member1", "이름", "p1", false, "email1@email.com,email2@email.com" + ); + Optional member1 = memberRepository.findByIdForUpdate(MemberId.of("member1")); + } +} \ No newline at end of file diff --git a/src/test/java/com/myshop/member/query/MemberDataDaoIT.java b/src/test/java/com/myshop/member/query/MemberDataDaoIT.java new file mode 100644 index 0000000..b03f5d2 --- /dev/null +++ b/src/test/java/com/myshop/member/query/MemberDataDaoIT.java @@ -0,0 +1,106 @@ +package com.myshop.member.query; + +import com.myshop.common.jpa.Rangeable; +import com.myshop.common.jpa.SpecBuilder; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.Page; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.test.context.jdbc.Sql; +import org.springframework.util.StringUtils; + +import java.util.List; +import java.util.Optional; + +@SpringBootTest +@Sql("classpath:shop-init-test.sql") +class MemberDataDaoIT { + private Logger logger = LoggerFactory.getLogger(getClass()); + + @Autowired + private MemberDataDao memberDataDao; + + @Test + void findByBlocked() { + Page page = memberDataDao.findByBlocked(false, PageRequest.of(2, 3)); + logger.info("blocked result: {}", page.getContent().size()); + List content = page.getContent(); + long totalElements = page.getTotalElements(); + int totalPages = page.getTotalPages(); + int number = page.getNumber(); + int numberOfElements = page.getNumberOfElements(); + int size = page.getSize(); + logger.info("content.size()={}, totalElements={}, totalPages={}, number={}, numberOfElements={}", + content.size(), totalElements, totalPages, number, numberOfElements); + } + + @Test + void findByNameLike() { + Sort sort = Sort.by("name").descending(); + PageRequest pageReq = PageRequest.of(1, 2, sort); + List user = memberDataDao.findByNameLike("사용자%", pageReq); + logger.info("name llike result: {}", user.size()); + } + + @Test + void findAll() { + Specification spec = MemberDataSpecs.nonBlocked(); + List result = memberDataDao.findAll(spec, PageRequest.of(1, 2)); + logger.info("spec result: {}", result.size()); + } + + @Test + void getRange() { + Specification spec = MemberDataSpecs.nonBlocked(); + List result = memberDataDao.getRange(spec, Rangeable.of(2, 4)); + logger.info("spec result: {}", result.size()); + } + + @Test + void findFirst() { + List result = memberDataDao.findFirst3ByNameLikeOrderByName("사용자%"); + logger.info("result: {}", result.size()); + + Optional member = memberDataDao.findFirstByNameLikeOrderByName("없음"); + logger.info("result: {}", member); + MemberData data = memberDataDao.findFirstByBlockedOrderById(false); + logger.info("result: {}", data); + } + + + @Test + void compositeSpec() { + SearchRequest searchRequest = new SearchRequest(); + Specification spec = Specification.where(null); + if (searchRequest.isOnlyNotBlocked()) { + spec = spec.and(MemberDataSpecs.nonBlocked()); + } + if (StringUtils.hasText(searchRequest.getName())) { + spec = spec.and(MemberDataSpecs.nameLike(searchRequest.getName())); + } + List result = memberDataDao.findAll(spec, PageRequest.of(0, 5)); + logger.info("result: {}", result.size()); + } + + @Test + void specBuilder() { + SearchRequest searchRequest = new SearchRequest(); + searchRequest.setOnlyNotBlocked(true); + Specification spec = SpecBuilder.builder(MemberData.class) + .ifTrue( + searchRequest.isOnlyNotBlocked(), + () -> MemberDataSpecs.nonBlocked()) + .ifHasText( + searchRequest.getName(), + name -> MemberDataSpecs.nameLike(searchRequest.getName())) + .toSpec(); + List result = memberDataDao.findAll(spec, PageRequest.of(0, 5)); + logger.info("result: {}", result.size()); + } + +} \ No newline at end of file diff --git a/src/test/java/com/myshop/member/query/SearchRequest.java b/src/test/java/com/myshop/member/query/SearchRequest.java new file mode 100644 index 0000000..45436de --- /dev/null +++ b/src/test/java/com/myshop/member/query/SearchRequest.java @@ -0,0 +1,22 @@ +package com.myshop.member.query; + +public class SearchRequest { + private boolean onlyNotBlocked; + private String name; + + public boolean isOnlyNotBlocked() { + return onlyNotBlocked; + } + + public void setOnlyNotBlocked(boolean onlyNotBlocked) { + this.onlyNotBlocked = onlyNotBlocked; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } +} diff --git a/src/test/java/com/myshop/order/command/application/CancelOrderServiceIT.java b/src/test/java/com/myshop/order/command/application/CancelOrderServiceIT.java new file mode 100644 index 0000000..f18ff87 --- /dev/null +++ b/src/test/java/com/myshop/order/command/application/CancelOrderServiceIT.java @@ -0,0 +1,20 @@ +package com.myshop.order.command.application; + +import com.myshop.order.command.domain.Canceller; +import com.myshop.order.command.domain.OrderNo; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.test.context.jdbc.Sql; + +@SpringBootTest +@Sql("classpath:shop-init-test.sql") +class CancelOrderServiceIT { + @Autowired + private CancelOrderService cancelOrderService; + + @Test + void cancel() { + cancelOrderService.cancel(OrderNo.of("ORDER-001"), Canceller.of("user1")); + } +} \ No newline at end of file diff --git a/src/test/java/com/myshop/order/command/domain/OrderRepositoryIT.java b/src/test/java/com/myshop/order/command/domain/OrderRepositoryIT.java new file mode 100644 index 0000000..d78bd2d --- /dev/null +++ b/src/test/java/com/myshop/order/command/domain/OrderRepositoryIT.java @@ -0,0 +1,80 @@ +package com.myshop.order.command.domain; + +import com.myshop.catalog.command.domain.product.ProductId; +import com.myshop.common.model.Address; +import com.myshop.common.model.Money; +import com.myshop.helper.DbHelper; +import com.myshop.member.command.domain.MemberId; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.jdbc.core.JdbcTemplate; + +import java.time.LocalDateTime; +import java.util.List; +import java.util.Optional; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +class OrderRepositoryIT { + private Logger logger = LoggerFactory.getLogger(getClass()); + + @Autowired + private OrderRepository orderRepository; + @Autowired + private JdbcTemplate jdbcTemplate; + @Autowired + private DbHelper dbHelper; + + @BeforeEach + void setUp() { + dbHelper.clear(); + } + + @Test + void save() { + orderRepository.save(new Order( + OrderNo.of("ORD1234"), + new Orderer(MemberId.of("member"), "name"), + List.of(new OrderLine(ProductId.of("PRDID"), new Money(1000), 2)), + new ShippingInfo(new Address("12345", "addr1", "addr2"), "집앞", + new Receiver("아무개", "010-0000-0000")), + OrderState.PAYMENT_WAITING + )); + } + + @Test + void findById() { + jdbcTemplate.update( + """ + insert into purchase_order + values (?,?,?,?,?,?,?,?,?,?,?,?,?) + """, + "ORD1234", 1, "user1", "사용자1", 5000, + "11122", "주소1", "주소2", "1층", + "받는사람", "010-1234-5689", + OrderState.PAYMENT_WAITING.name(), LocalDateTime.now() + ); + jdbcTemplate.update( + """ + insert into order_line + values (?,?,?,?,?,?) + """, + "ORD1234", 0, "PROD1", 1000, 5, 5000 + ); + Optional ordOpt = orderRepository.findById(OrderNo.of("ORD1234")); + assertThat(ordOpt).isPresent(); + Order order = ordOpt.get(); + assertThat(order.getOrderLines()).hasSize(1); + } + + @Test + void nextOrderNo() { + OrderNo orderNo = orderRepository.nextOrderNo(); + logger.info("nextOrderNo: {}", orderNo.getNumber()); + } +} \ No newline at end of file diff --git a/src/test/java/com/myshop/order/query/dao/OrderSummaryDaoIT.java b/src/test/java/com/myshop/order/query/dao/OrderSummaryDaoIT.java new file mode 100644 index 0000000..ac83689 --- /dev/null +++ b/src/test/java/com/myshop/order/query/dao/OrderSummaryDaoIT.java @@ -0,0 +1,90 @@ +package com.myshop.order.query.dao; + +import com.myshop.order.query.dto.OrderSummary; +import com.myshop.order.query.dto.OrderView; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.context.SpringBootTest; +import org.springframework.data.domain.PageRequest; +import org.springframework.data.domain.Pageable; +import org.springframework.data.domain.Sort; +import org.springframework.data.jpa.domain.Specification; +import org.springframework.test.context.jdbc.Sql; + +import java.time.LocalDateTime; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +@SpringBootTest +@Sql("classpath:shop-init-test.sql") +class OrderSummaryDaoIT { + @Autowired + private OrderSummaryDao orderSummaryDao; + + @Test + void findAllSpec() { + LocalDateTime from = LocalDateTime.of(2022, 1, 1, 0, 0, 0); + LocalDateTime to = LocalDateTime.of(2022, 1, 2, 0, 0, 0); + + Specification spec = OrderSummarySpecs.ordererId("user1") + .and(OrderSummarySpecs.orderDateBetween(from, to)); + + List results = orderSummaryDao.findAll(spec); + assertThat(results).hasSize(1); + } + + @Test + void findAllSpecSort() { + LocalDateTime from = LocalDateTime.of(2022, 1, 1, 0, 0, 0); + LocalDateTime to = LocalDateTime.of(2022, 1, 2, 0, 0, 0); + + Specification spec = OrderSummarySpecs.ordererId("user1") + .and(OrderSummarySpecs.orderDateBetween(from, to)); + + Sort sort = Sort.by("number").ascending().and(Sort.by("orderDate").descending()); + + List results = orderSummaryDao.findAll(spec, sort); + assertThat(results).hasSize(1); + } + + @Test + void findAllSpecPageable() { + LocalDateTime from = LocalDateTime.of(2022, 1, 1, 0, 0, 0); + LocalDateTime to = LocalDateTime.of(2023, 1, 2, 0, 0, 0); + + Specification spec = OrderSummarySpecs.orderDateBetween(from, to); + + Pageable pageable = PageRequest.of(1, 1); + List results = orderSummaryDao.findAll(spec, pageable); + assertThat(results).hasSize(2); + } + + @Test + void findByOrdererIdOrderByNumberDesc() { + List results = orderSummaryDao.findByOrdererIdOrderByNumberDesc("user1"); + assertThat(results.get(0).getNumber()).isEqualTo("ORDER-002"); + assertThat(results.get(1).getNumber()).isEqualTo("ORDER-001"); + } + + @Test + void findByOrdererIdSort() { + Sort sort = Sort.by("number").ascending(); + List results = orderSummaryDao.findByOrdererId("user1", sort); + assertThat(results.get(0).getNumber()).isEqualTo("ORDER-001"); + assertThat(results.get(1).getNumber()).isEqualTo("ORDER-002"); + } + + @Test + void findByOrdererIdPageable() { + Pageable pageable = PageRequest.of(1, 1); + List results = orderSummaryDao.findByOrdererId("user1", pageable); + assertThat(results.get(0).getNumber()).isEqualTo("ORDER-001"); + assertThat(results.get(1).getNumber()).isEqualTo("ORDER-002"); + } + + @Test + void findOrderView() { + List result = orderSummaryDao.findOrderView("user1"); + } +} \ No newline at end of file diff --git a/src/test/resources/shop-init-test.sql b/src/test/resources/shop-init-test.sql new file mode 100644 index 0000000..7a80c99 --- /dev/null +++ b/src/test/resources/shop-init-test.sql @@ -0,0 +1,85 @@ +truncate table purchase_order; +truncate table order_line; +truncate table category; +truncate table product_category; +truncate table product; +truncate table image; +truncate table member; +truncate table member_authorities; +truncate table article; +truncate table article_content; +truncate table evententry; + +insert into member (member_id, name, password, blocked) values ('user1', '사용자1', '1234', false); +insert into member (member_id, name, password, blocked) values ('user2', '사용자2', '5678', false); +insert into member (member_id, name, password, blocked) values ('user3', '사용자3', '5678', true); +insert into member (member_id, name, password, blocked) values ('user4', '사용자4', '5678', true); +insert into member (member_id, name, password, blocked) values ('user5', '사용자5', '5678', false); +insert into member (member_id, name, password, blocked) values ('user6', '사용자6', '5678', false); +insert into member (member_id, name, password, blocked) values ('user7', '사용자7', '5678', false); +insert into member (member_id, name, password, blocked) values ('user8', '사용자8', '5678', false); +insert into member (member_id, name, password, blocked) values ('admin', '운영자', 'admin1234', false); +insert into member_authorities values ('user1', 'ROLE_USER'); +insert into member_authorities values ('user2', 'ROLE_USER'); +insert into member_authorities values ('admin', 'ROLE_ADMIN'); + +insert into category values (1001, '전자제품'); +insert into category values (2001, '필기구'); + +insert into product values ('prod-001', '라즈베리파이3 모델B', 56000, '모델B'); +insert into image (product_id, list_idx, image_type, image_path, upload_time) values + ('prod-001', 0, 'II', 'rpi3.jpg', now()); +insert into image (product_id, list_idx, image_type, image_path, upload_time) values + ('prod-001', 1, 'EI', 'http://external/image/path', now()); + +insert into product_category values ('prod-001', 1001); + +insert into product values ('prod-002', '어프로치 휴대용 화이트보드 세트', 11920, '화이트보드'); +insert into image (product_id, list_idx, image_type, image_path, upload_time) values + ('prod-002', 0, 'II', 'wbp.png', now()); + +insert into product_category values ('prod-002', 2001); + +insert into product values ('prod-003', '볼펜 겸용 터치펜', 9000, '볼펜과 터치펜을 하나로!'); +insert into image (product_id, list_idx, image_type, image_path, upload_time) values + ('prod-003', 0, 'II', 'pen.jpg', now()); +insert into image (product_id, list_idx, image_type, image_path, upload_time) values + ('prod-003', 1, 'II', 'pen2.jpg', now()); + +insert into product_category values ('prod-003', 1001); +insert into product_category values ('prod-003', 2001); + +insert into purchase_order values ( +'ORDER-001', 1, 'user1', '사용자1', 4000, +'123456', '서울시', '관악구', '메시지', +'사용자1', '010-1234-5678', 'PREPARING', '2022-01-01 15:30:00' +); + +insert into order_line values ('ORDER-001', 0, 'prod-001', 1000, 2, 2000); +insert into order_line values ('ORDER-001', 1, 'prod-002', 2000, 1, 2000); + +insert into purchase_order values ( +'ORDER-002', 2, 'user1', '사용자1', 5000, +'123456', '서울시', '관악구', '메시지', +'사용자1', '010-1234-5678', 'PREPARING', '2022-01-02 09:18:21' +); +insert into order_line values ('ORDER-002', 0, 'prod-001', 1000, 5, 5000); + +insert into purchase_order values ( +'ORDER-003', 3, 'user2', '사용자2', 5000, +'123456', '서울시', '관악구', '메시지', +'사용자1', '010-1234-5678', 'SHIPPED', '2016-01-03 09:00:00' +); +insert into order_line values ('ORDER-003', 0, 'prod-001', 1000, 5, 5000); + +insert into article (title) values ('제목'); +insert into article_content values (1, 'content', 'type'); + +insert into evententry (type, content_type, payload, timestamp) values + ('com.myshop.eventstore.infra.SampleEvent', 'application/json', '{"name": "name1", "value": 11}', now()); +insert into evententry (type, content_type, payload, timestamp) values + ('com.myshop.eventstore.infra.SampleEvent', 'application/json', '{"name": "name2", "value": 12}', now()); +insert into evententry (type, content_type, payload, timestamp) values + ('com.myshop.eventstore.infra.SampleEvent', 'application/json', '{"name": "name3", "value": 13}', now()); +insert into evententry (type, content_type, payload, timestamp) values + ('com.myshop.eventstore.infra.SampleEvent', 'application/json', '{"name": "name4", "value": 14}', now());