diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000000..0c64355dfc --- /dev/null +++ b/.gitignore @@ -0,0 +1,195 @@ +logs + +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/**/build/ +!**/src/test/**/build/ + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache +bin/ +!**/src/main/**/bin/ +!**/src/test/**/bin/ + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ +!**/src/main/**/out/ +!**/src/test/**/out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ +### Java template +# Compiled class file +*.class + +# Log file +*.log + +# BlueJ files +*.ctxt + +# Mobile Tools for Java (J2ME) +.mtj.tmp/ + +# Package Files # +*.jar +*.war +*.nar +*.ear +*.zip +*.tar.gz +*.rar + +# virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml +hs_err_pid* + +### Maven template +target/ +pom.xml.tag +pom.xml.releaseBackup +pom.xml.versionsBackup +pom.xml.next +release.properties +dependency-reduced-pom.xml +buildNumber.properties +.mvn/timing.properties +# https://github.com/takari/maven-wrapper#usage-without-binary-jar +.mvn/wrapper/maven-wrapper.jar + +### JetBrains template +# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio, WebStorm and Rider +# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839 + +# User-specific stuff +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf + +# Generated files +.idea/**/contentModel.xml + +# Sensitive or high-churn files +.idea/**/dataSources/ +.idea/**/dataSources.ids +.idea/**/dataSources.local.xml +.idea/**/sqlDataSources.xml +.idea/**/dynamic.xml +.idea/**/uiDesigner.xml +.idea/**/dbnavigator.xml + +# Gradle +.idea/**/gradle.xml +.idea/**/libraries + +# Gradle and Maven with auto-import +# When using Gradle or Maven with auto-import, you should exclude module files, +# since they will be recreated, and may cause churn. Uncomment if using +# auto-import. +# .idea/artifacts +# .idea/compiler.xml +# .idea/jarRepositories.xml +# .idea/modules.xml +# .idea/*.iml +# .idea/modules +# *.iml +# *.ipr + +# CMake +cmake-build-*/ + +# Mongo Explorer plugin +.idea/**/mongoSettings.xml + +# File-based project format +*.iws + +# IntelliJ +out/ + +# mpeltonen/sbt-idea plugin +.idea_modules/ + +# JIRA plugin +atlassian-ide-plugin.xml + +# Cursive Clojure plugin +.idea/replstate.xml + +# Crashlytics plugin (for Android Studio and IntelliJ) +com_crashlytics_export_strings.xml +crashlytics.properties +crashlytics-build.properties +fabric.properties + +# Editor-based Rest Client +.idea/httpRequests + +# Android studio 3.1+ serialized cache file +.idea/caches/build_file_checksums.ser + +### Gradle template +.gradle +**/build/ +!src/**/build/ + +# Ignore Gradle GUI config +gradle-app.setting + +# Avoid ignoring Gradle wrapper jar file (.jar files are usually ignored) +!gradle-wrapper.jar + +# Cache of project +.gradletasknamecache + +# # Work around https://youtrack.jetbrains.com/issue/IDEA-116898 +# gradle/wrapper/gradle-wrapper.properties + +### macOS template +# General +.DS_Store +.AppleDouble +.LSOverride + +# Icon must end with two \r +Icon + +# Thumbnails +._* + +# Files that might appear in the root of a volume +.DocumentRevisions-V100 +.fseventsd +.Spotlight-V100 +.TemporaryItems +.Trashes +.VolumeIcon.icns +.com.apple.timemachine.donotpresent + +# Directories potentially created on remote AFP share +.AppleDB +.AppleDesktop +Network Trash Folder +Temporary Items +.apdisk + diff --git a/README.md b/README.md index 43762768b5..76f8a12cc7 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,2 @@ # SpringBoot Basic Weekly Mission 스프링부트 basic 위클리미션을 코드리뷰하는 Repository입니다. - -주차별 과제는 노션에서 확인하세요! -[노션에서 미션 확인가기](https://www.notion.so/backend-devcourse/Part1-3-38f57acca0dd490db11393701417943a) diff --git a/build.gradle b/build.gradle new file mode 100644 index 0000000000..aa7bc54e4a --- /dev/null +++ b/build.gradle @@ -0,0 +1,32 @@ +plugins { + id 'java' + id 'org.springframework.boot' version '3.1.1' + id 'io.spring.dependency-management' version '1.0.15.RELEASE' +} + +group = 'com.programmers' +version = '0.0.1-SNAPSHOT' + +java { + sourceCompatibility = '17' +} + +repositories { + mavenCentral() +} + +dependencies { + implementation 'org.springframework.boot:spring-boot-starter' + implementation 'org.springframework.boot:spring-boot-starter-jdbc' + implementation 'org.springframework.boot:spring-boot-starter-web' + implementation 'org.springframework.boot:spring-boot-starter-validation' + implementation 'org.projectlombok:lombok:1.18.26' + runtimeOnly 'com.mysql:mysql-connector-j' + + testImplementation 'org.springframework.boot:spring-boot-starter-test' + testImplementation 'com.h2database:h2' +} + +tasks.named('test') { + useJUnitPlatform() +} diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000..249e5832f0 Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000000..774fae8767 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.6.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/gradlew b/gradlew new file mode 100755 index 0000000000..a69d9cb6c2 --- /dev/null +++ b/gradlew @@ -0,0 +1,240 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit + +APP_NAME="Gradle" +APP_BASE_NAME=${0##*/} + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + +# Collect all arguments for the java command; +# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of +# shell script including quotes and variable substitutions, so put them in +# double quotes to make sure that they get re-expanded; and +# * put everything else in single quotes, so that it's not re-expanded. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + org.gradle.wrapper.GradleWrapperMain \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000000..53a6b238d4 --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,91 @@ +@rem +@rem Copyright 2015 the original author or authors. +@rem +@rem Licensed under the Apache License, Version 2.0 (the "License"); +@rem you may not use this file except in compliance with the License. +@rem You may obtain a copy of the License at +@rem +@rem https://www.apache.org/licenses/LICENSE-2.0 +@rem +@rem Unless required by applicable law or agreed to in writing, software +@rem distributed under the License is distributed on an "AS IS" BASIS, +@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +@rem See the License for the specific language governing permissions and +@rem limitations under the License. +@rem + +@if "%DEBUG%"=="" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%"=="" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Resolve any "." and ".." in APP_HOME to make it shorter. +for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m" + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if %ERRORLEVEL% equ 0 goto execute + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto execute + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %* + +:end +@rem End local scope for the variables with windows NT shell +if %ERRORLEVEL% equ 0 goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +set EXIT_CODE=%ERRORLEVEL% +if %EXIT_CODE% equ 0 set EXIT_CODE=1 +if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE% +exit /b %EXIT_CODE% + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/settings.gradle b/settings.gradle new file mode 100644 index 0000000000..20365b4d8f --- /dev/null +++ b/settings.gradle @@ -0,0 +1 @@ +rootProject.name = 'springboot-basic' diff --git a/src/main/java/com/programmers/springbootbasic/SpringBootConsoleApplication.java b/src/main/java/com/programmers/springbootbasic/SpringBootConsoleApplication.java new file mode 100644 index 0000000000..e538e11bf8 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/SpringBootConsoleApplication.java @@ -0,0 +1,66 @@ +package com.programmers.springbootbasic; + +import com.programmers.springbootbasic.presentation.Command; +import com.programmers.springbootbasic.presentation.controller.VoucherController; +import com.programmers.springbootbasic.presentation.view.ConsoleApplicationView; +import com.programmers.springbootbasic.service.dto.Voucher.VoucherResponses; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.boot.CommandLineRunner; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.UUID; + +@Profile("console") +@Component +public class SpringBootConsoleApplication implements CommandLineRunner { + private final VoucherController voucherController; + private final ConsoleApplicationView applicationView; + private final Logger logger = LoggerFactory.getLogger(SpringBootConsoleApplication.class); + + public SpringBootConsoleApplication(VoucherController voucherController, ConsoleApplicationView applicationView) { + this.voucherController = voucherController; + this.applicationView = applicationView; + } + + @Override + public void run(String... args) throws Exception { + while (true) { + try { + Command command = applicationView.selectCommand(); + switch (command) { + case CREATE -> create(); + case LIST -> list(); + case EXIT -> { + exit(); + return; + } + } + } catch (IllegalArgumentException | IllegalStateException e) { + logger.error(e.getMessage(), e); + applicationView.printErrorMessage(e.getMessage()); + } + } + } + + private void create() throws IOException { + applicationView.startCreation(); + + String voucherType = applicationView.inputType(); + int amountOrPercent = applicationView.inputAmountOrPercent(); + + UUID voucherId = voucherController.createVoucher(voucherType, amountOrPercent); + applicationView.printCreatedVoucherId(voucherId); + } + + private void list() throws IOException { + VoucherResponses voucherResponses = voucherController.listVouchers(); + applicationView.listVouchers(voucherResponses); + } + + private void exit() throws IOException { + applicationView.exit(); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/SpringbootBasicApplication.java b/src/main/java/com/programmers/springbootbasic/SpringbootBasicApplication.java new file mode 100644 index 0000000000..869fb8c7e7 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/SpringbootBasicApplication.java @@ -0,0 +1,13 @@ +package com.programmers.springbootbasic; + +import org.springframework.boot.SpringApplication; +import org.springframework.boot.autoconfigure.SpringBootApplication; + +@SpringBootApplication +public class SpringbootBasicApplication { + + public static void main(String[] args) { + SpringApplication.run(SpringbootBasicApplication.class, args); + } + +} diff --git a/src/main/java/com/programmers/springbootbasic/common/domain/Page.java b/src/main/java/com/programmers/springbootbasic/common/domain/Page.java new file mode 100644 index 0000000000..b7c03725fa --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/common/domain/Page.java @@ -0,0 +1,55 @@ +package com.programmers.springbootbasic.common.domain; + +import java.util.List; +import java.util.function.Function; + +public class Page { + private final List contents; + private final int numberOfElements; + private final Pageable pageable; + private final int totalPage; + private final long totalElements; + + public Page(List contents, Pageable pageable, long totalElements) { + this.contents = contents; + this.numberOfElements = contents.size(); + this.pageable = pageable; + this.totalElements = totalElements; + this.totalPage = calculateTotalPage(); + } + + private int calculateTotalPage() { + return (int) ((totalElements / pageable.size()) + (totalElements % pageable.size() == 0 ? 0 : 1)); + } + + public Page map(Function converter) { + List converted = this.contents.stream() + .map(converter) + .toList(); + return new Page<>(converted, this.pageable, this.totalElements); + } + + public List getContents() { + return contents; + } + + public int getNumberOfElements() { + return numberOfElements; + } + + public Pageable getPageable() { + return pageable; + } + + public int getTotalPage() { + return totalPage; + } + + public long getTotalElements() { + return totalElements; + } + + public boolean isLast() { + return pageable.page() + 1 >= totalPage; + } +} diff --git a/src/main/java/com/programmers/springbootbasic/common/domain/Pageable.java b/src/main/java/com/programmers/springbootbasic/common/domain/Pageable.java new file mode 100644 index 0000000000..1f296b1d11 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/common/domain/Pageable.java @@ -0,0 +1,16 @@ +package com.programmers.springbootbasic.common.domain; + +import jakarta.validation.constraints.Min; +import jakarta.validation.constraints.NotNull; + +public record Pageable( + @NotNull @Min(value = 0, message = INVALID_PAGE) int page, + @NotNull @Min(value = 1, message = INVALID_SIZE) int size +) { + private static final String INVALID_PAGE = "페이지는 0보다 커야합니다."; + private static final String INVALID_SIZE = "사이즈는 0보다 커야합니다."; + + public long getOffSet() { + return (long) page * (long) size; + } +} diff --git a/src/main/java/com/programmers/springbootbasic/common/exception/ErrorResponse.java b/src/main/java/com/programmers/springbootbasic/common/exception/ErrorResponse.java new file mode 100644 index 0000000000..b1cc907cf6 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/common/exception/ErrorResponse.java @@ -0,0 +1,23 @@ +package com.programmers.springbootbasic.common.exception; + +public class ErrorResponse { + private static final String BAD_REQUEST_MESSAGE = "잘못된 입력값입니다. 입력값을 확인하세요"; + private static final String INTERNAL_SERVER_ERROR_MESSAGE = "서버 내부 에러입니다."; + private final String message; + + public ErrorResponse(String message) { + this.message = message; + } + + public static ErrorResponse internalServerError() { + return new ErrorResponse(INTERNAL_SERVER_ERROR_MESSAGE); + } + + public static ErrorResponse badRequest() { + return new ErrorResponse(BAD_REQUEST_MESSAGE); + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/com/programmers/springbootbasic/common/exception/GlobalExceptionHandler.java b/src/main/java/com/programmers/springbootbasic/common/exception/GlobalExceptionHandler.java new file mode 100644 index 0000000000..97a3f91f8b --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/common/exception/GlobalExceptionHandler.java @@ -0,0 +1,48 @@ +package com.programmers.springbootbasic.common.exception; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.http.HttpStatus; +import org.springframework.http.converter.HttpMessageNotReadableException; +import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + Logger log = LoggerFactory.getLogger("exception"); + + @ExceptionHandler({UnsupportedOperationException.class, IllegalStateException.class, IllegalArgumentException.class}) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse handleThrowingException(RuntimeException e) { + log.warn(e.getMessage(), e); + return new ErrorResponse(e.getMessage()); + } + + @ExceptionHandler(MethodArgumentNotValidException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse handleMethodArgumentNotValidException(MethodArgumentNotValidException e) { + log.warn(e.getMessage(), e); + return new ErrorResponse(e + .getBindingResult() + .getFieldErrors() + .get(0) + .getDefaultMessage() + ); + } + + @ExceptionHandler(HttpMessageNotReadableException.class) + @ResponseStatus(HttpStatus.BAD_REQUEST) + public ErrorResponse handleHttpMessageNotReadableException(HttpMessageNotReadableException e) { + log.warn(e.getMessage(), e); + return ErrorResponse.badRequest(); + } + + @ExceptionHandler(Exception.class) + @ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) + public ErrorResponse handleException(Exception e) { + log.error(e.getMessage(), e); + return ErrorResponse.internalServerError(); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/common/util/NumberParser.java b/src/main/java/com/programmers/springbootbasic/common/util/NumberParser.java new file mode 100644 index 0000000000..6e3d63e121 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/common/util/NumberParser.java @@ -0,0 +1,16 @@ +package com.programmers.springbootbasic.common.util; + +public final class NumberParser { + static final String INVALID_NUMBER = "잘못된 수입니다. 현재 입력 값: "; + + private NumberParser() { + } + + public static int parseToInt(String input) { + try { + return Integer.parseInt(input); + } catch (NumberFormatException e) { + throw new IllegalArgumentException(INVALID_NUMBER + input); + } + } +} diff --git a/src/main/java/com/programmers/springbootbasic/common/util/PatternUtils.java b/src/main/java/com/programmers/springbootbasic/common/util/PatternUtils.java new file mode 100644 index 0000000000..7adc9bfec1 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/common/util/PatternUtils.java @@ -0,0 +1,19 @@ +package com.programmers.springbootbasic.common.util; + +import java.util.regex.Pattern; + +public final class PatternUtils { + private static final Pattern DATE = Pattern.compile("^\\d{4}-\\d{2}-\\d{2}$"); + private static final Pattern EMAIL = Pattern.compile("\\b[\\w\\.-]+@[\\w\\.-]+\\.\\w{2,4}\\b"); + + private PatternUtils() { + } + + public static boolean isDate(String input) { + return DATE.matcher(input).matches(); + } + + public static boolean isEmail(String input) { + return EMAIL.matcher(input).matches(); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/common/util/Validator.java b/src/main/java/com/programmers/springbootbasic/common/util/Validator.java new file mode 100644 index 0000000000..25bda08661 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/common/util/Validator.java @@ -0,0 +1,38 @@ +package com.programmers.springbootbasic.common.util; + +import java.util.UUID; + +public final class Validator { + private static final String ID_IS_NULL = "빈 값이나 공백을 아이디로 사용할 수 없습니다. 현재 값: "; + private static final String AMOUNT_OR_DISCOUNT_IS_NULL = "빈 값이나 공백을 할인률/할인액으로 지정할 수 없습니다. 현재 값: "; + private static final String NULL_OR_BLANK = "빈 값이나 공백을 사용할 수 없습니다. 현재 값: "; + private static final String EMAIL_IS_WRONG_OR_NULL = "빈 값이나 유효하지 않은 이메일입니다. 현재 값: "; + + + private Validator() { + } + + public static void checkNullUuid(UUID uuid) { + if (uuid == null) { + throw new IllegalArgumentException(ID_IS_NULL + uuid); + } + } + + public static void checkNullOrBlank(String input) { + if (input == null || input.isBlank()) { + throw new IllegalArgumentException(NULL_OR_BLANK + input); + } + } + + public static void checkNullNumber(Number number) { + if (number == null) { + throw new IllegalArgumentException(AMOUNT_OR_DISCOUNT_IS_NULL + number); + } + } + + public static void checkNullOrWrongEmail(String input) { + if (input == null || !PatternUtils.isEmail(input)) { + throw new IllegalArgumentException(EMAIL_IS_WRONG_OR_NULL + input); + } + } +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/customer/Customer.java b/src/main/java/com/programmers/springbootbasic/domain/customer/Customer.java new file mode 100644 index 0000000000..9010e9a050 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/customer/Customer.java @@ -0,0 +1,37 @@ +package com.programmers.springbootbasic.domain.customer; + +import com.programmers.springbootbasic.common.util.Validator; + +import java.util.UUID; + +public class Customer { + private final UUID customerId; + private final String email; + + private String name; + + public Customer(UUID customerId, String email, String name) { + checkInvalidValue(customerId, email, name); + this.customerId = customerId; + this.email = email; + this.name = name; + } + + public UUID getCustomerId() { + return this.customerId; + } + + public String getEmail() { + return this.email; + } + + public String getName() { + return this.name; + } + + private void checkInvalidValue(UUID customerId, String email, String name) { + Validator.checkNullUuid(customerId); + Validator.checkNullOrBlank(name); + Validator.checkNullOrWrongEmail(email); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/customer/CustomerUpdateDto.java b/src/main/java/com/programmers/springbootbasic/domain/customer/CustomerUpdateDto.java new file mode 100644 index 0000000000..c872f6fc37 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/customer/CustomerUpdateDto.java @@ -0,0 +1,10 @@ +package com.programmers.springbootbasic.domain.customer; + +import java.util.UUID; + +public record CustomerUpdateDto( + UUID id, + String name, + String email +) { +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/customer/Repository/CustomerRepository.java b/src/main/java/com/programmers/springbootbasic/domain/customer/Repository/CustomerRepository.java new file mode 100644 index 0000000000..9f5ce1499a --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/customer/Repository/CustomerRepository.java @@ -0,0 +1,17 @@ +package com.programmers.springbootbasic.domain.customer.Repository; + +import com.programmers.springbootbasic.domain.customer.Customer; +import com.programmers.springbootbasic.domain.customer.CustomerUpdateDto; + +import java.util.Optional; +import java.util.UUID; + +public interface CustomerRepository { + + UUID save(Customer customer); + + void update(CustomerUpdateDto customer); + + Optional findByEmail(String email); +} + diff --git a/src/main/java/com/programmers/springbootbasic/domain/customer/Repository/JdbcCustomerRepository.java b/src/main/java/com/programmers/springbootbasic/domain/customer/Repository/JdbcCustomerRepository.java new file mode 100644 index 0000000000..f34d4ff859 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/customer/Repository/JdbcCustomerRepository.java @@ -0,0 +1,86 @@ +package com.programmers.springbootbasic.domain.customer.Repository; + +import com.programmers.springbootbasic.domain.customer.Customer; +import com.programmers.springbootbasic.domain.customer.CustomerUpdateDto; +import org.springframework.dao.DataAccessException; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Repository; + +import java.util.Collections; +import java.util.Map; +import java.util.Optional; +import java.util.UUID; + +@Repository +public class JdbcCustomerRepository implements CustomerRepository { + private static final String FAIL_TO_CREATE = "유저 생성에 실패했습니다. 입력값을 확인해주세요"; + private static final String FAIL_TO_UPDATE = "유저 업데이트에 실패했습니다. 입력값을 확인해주세요"; + private static final String DATABASE_CUSTOMER_ID = "customer_id"; + private static final String CUSTOMER_ID = "customerId"; + private static final String NAME = "name"; + private static final String EMAIL = "email"; + private final NamedParameterJdbcTemplate jdbcTemplate; + private final RowMapper rowMapper = (rs, rowNum) -> { + UUID customerId = UUID.fromString(rs.getString(DATABASE_CUSTOMER_ID)); + String email = rs.getString(EMAIL); + String name = rs.getString(NAME); + return new Customer(customerId, email, name); + }; + + public JdbcCustomerRepository(NamedParameterJdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Override + public UUID save(Customer customer) { + int affectedRow = jdbcTemplate.update( + "INSERT INTO customer(customer_id, name, email) VALUES(:customerId, :name, :email)", + toParamMap(customer)); + if (affectedRow != 1) { + throw new IllegalArgumentException(FAIL_TO_CREATE); + } + return customer.getCustomerId(); + } + + private Map toParamMap(Customer customer) { + return Map.of( + CUSTOMER_ID, customer.getCustomerId().toString(), + NAME, customer.getName(), + EMAIL, customer.getEmail() + ); + } + + @Override + public void update(CustomerUpdateDto updateDto) { + int affectedRow = jdbcTemplate.update( + "UPDATE customer SET name = :name, email = :email WHERE customer_id = :customerId", + toParamMap(updateDto) + ); + if (affectedRow != 1) { + throw new IllegalArgumentException(FAIL_TO_UPDATE); + } + } + + private Map toParamMap(CustomerUpdateDto forUpdate) { + return Map.of( + CUSTOMER_ID, forUpdate.id().toString(), + NAME, forUpdate.name(), + EMAIL, forUpdate.email() + ); + } + + @Override + public Optional findByEmail(String email) { + try { + Customer customer = jdbcTemplate.queryForObject( + "SELECT * FROM customer WHERE email = :email", + Collections.singletonMap(EMAIL, email), + rowMapper + ); + return Optional.ofNullable(customer); + } catch (DataAccessException e) { + return Optional.empty(); + } + } +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/FixedAmountVoucher.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/FixedAmountVoucher.java new file mode 100644 index 0000000000..d134d77f04 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/FixedAmountVoucher.java @@ -0,0 +1,39 @@ +package com.programmers.springbootbasic.domain.voucher; + +import java.util.UUID; + +public class FixedAmountVoucher extends Voucher { + private static final long ZERO = 0L; + private static final int MIN_AMOUNT = 10; + private static final int MAX_AMOUNT = 10_000_000; + private static final String INVALID_AMOUNT = String.format( + "잘못된 할인 금액입니다. 할인 금액은 최소 %d원부터 최대 %d원까지 설정 가능합니다. 현재 입력 금액: ", MIN_AMOUNT, MAX_AMOUNT + ); + + private final int amount; + + public FixedAmountVoucher(UUID voucherId, int amount) { + super(voucherId, VoucherType.FIX); + if (isInvalidAmount(amount)) { + throw new IllegalArgumentException(INVALID_AMOUNT + String.format("%d원", amount)); + } + this.amount = amount; + } + + @Override + public Long getDiscountPrice(Long priceBeforeDiscount) { + if (priceBeforeDiscount <= amount) { + return ZERO; + } + return priceBeforeDiscount - amount; + } + + @Override + public int getAmountOrPercent() { + return amount; + } + + private boolean isInvalidAmount(int amount) { + return amount < MIN_AMOUNT || MAX_AMOUNT < amount; + } +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/PercentDiscountVoucher.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/PercentDiscountVoucher.java new file mode 100644 index 0000000000..b229d2ae88 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/PercentDiscountVoucher.java @@ -0,0 +1,40 @@ +package com.programmers.springbootbasic.domain.voucher; + + +import java.util.UUID; + +public class PercentDiscountVoucher extends Voucher { + private static final int MIN_PERCENT = 1; + private static final int MAX_PERCENT = 100; + private static final String INVALID_PERCENT = String.format( + "잘못된 할인율입니다. 할인율은 최소 %d부터 최대 %d까지 입니다. 현재 입력 퍼센트: ", MIN_PERCENT, MAX_PERCENT + ); + + private final int percent; + + public PercentDiscountVoucher(UUID voucherId, int percent) { + super(voucherId, VoucherType.PERCENT); + if (isInvalidPercent(percent)) { + throw new IllegalArgumentException(INVALID_PERCENT + String.format("%d%%", percent)); + } + this.percent = percent; + } + + @Override + public Long getDiscountPrice(Long priceBeforeDiscount) { + int tenForRound = 10; + double discountAmount = priceBeforeDiscount * percent / (double) MAX_PERCENT; + double discountedPrice = priceBeforeDiscount - discountAmount; + return Math.round((discountedPrice) / tenForRound) * tenForRound; + } + + @Override + public int getAmountOrPercent() { + return this.percent; + } + + + private boolean isInvalidPercent(int percent) { + return percent < MIN_PERCENT || MAX_PERCENT < percent; + } +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/Repository/JdbcVoucherRepository.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/Repository/JdbcVoucherRepository.java new file mode 100644 index 0000000000..01d773df76 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/Repository/JdbcVoucherRepository.java @@ -0,0 +1,81 @@ +package com.programmers.springbootbasic.domain.voucher.Repository; + +import com.programmers.springbootbasic.common.domain.Page; +import com.programmers.springbootbasic.common.domain.Pageable; +import com.programmers.springbootbasic.domain.voucher.Voucher; +import com.programmers.springbootbasic.domain.voucher.VoucherType; +import org.springframework.context.annotation.Primary; +import org.springframework.jdbc.core.RowMapper; +import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate; +import org.springframework.stereotype.Repository; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.UUID; + +@Repository +@Primary +public class JdbcVoucherRepository implements VoucherRepository { + private static final String FAIL_TO_CREATE = "바우처 생성에 실패했습니다. 입력값을 확인해주세요"; + private static final String DATABASE_VOUCHER_ID = "voucher_id"; + private static final String VOUCHER_ID = "voucherId"; + private static final String DATABASE_VOUCHER_TYPE = "voucher_type"; + private static final String VOUCHER_TYPE = "voucherType"; + private static final String DATABASE_AMOUNT_OR_PERCENT = "amount_or_percent"; + private static final String AMOUNT_OR_PERCENT = "amountOrPercent"; + private final RowMapper rowMapper = (rs, rowNum) -> { + UUID voucherId = UUID.fromString(rs.getString(DATABASE_VOUCHER_ID)); + VoucherType voucherType = VoucherType.valueOf(rs.getString(DATABASE_VOUCHER_TYPE)); + int amountOrPercent = rs.getInt(DATABASE_AMOUNT_OR_PERCENT); + return Voucher.createVoucher(voucherId, voucherType, amountOrPercent); + }; + private final NamedParameterJdbcTemplate jdbcTemplate; + + public JdbcVoucherRepository(NamedParameterJdbcTemplate jdbcTemplate) { + this.jdbcTemplate = jdbcTemplate; + } + + @Override + public UUID save(Voucher voucher) { + int affectedRow = jdbcTemplate.update("INSERT INTO voucher(voucher_id, voucher_type, amount_or_percent) VALUES (:voucherId, :voucherType, :amountOrPercent)" + , toParamMap(voucher)); + if (affectedRow != 1) { + throw new IllegalArgumentException(FAIL_TO_CREATE); + } + return voucher.getVoucherId(); + } + + private Map toParamMap(Voucher voucher) { + return Map.of( + VOUCHER_ID, voucher.getVoucherId().toString(), + VOUCHER_TYPE, voucher.getVoucherType().name(), + AMOUNT_OR_PERCENT, voucher.getAmountOrPercent() + ); + } + + @Override + public List findAll() { + return jdbcTemplate.query( + "SELECT * FROM voucher", + rowMapper); + } + + @Override + public long countTotalVoucher() { + return jdbcTemplate.queryForObject( + "SELECT COUNT(*) FROM voucher", + Collections.emptyMap(), + Long.class); + } + + @Override + public Page findAllWithPagination(Pageable pageable) { + long voucherCount = countTotalVoucher(); + List vouchers = jdbcTemplate.query( + "SELECT * FROM voucher LIMIT :size OFFSET :offset", + Map.of("size", pageable.size(), "offset", pageable.getOffSet()) + , rowMapper); + return new Page<>(vouchers, pageable, voucherCount); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/Repository/MemoryVoucherRepository.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/Repository/MemoryVoucherRepository.java new file mode 100644 index 0000000000..494752242b --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/Repository/MemoryVoucherRepository.java @@ -0,0 +1,34 @@ +package com.programmers.springbootbasic.domain.voucher.Repository; + +import com.programmers.springbootbasic.domain.voucher.Voucher; +import org.springframework.stereotype.Repository; + +import java.util.List; +import java.util.Map; +import java.util.UUID; +import java.util.concurrent.ConcurrentHashMap; + +@Repository +public class MemoryVoucherRepository implements VoucherRepository { + private static final String ALREADY_EXISTS = "이미 해당 아이디의 바우처가 존재합니다."; + private final Map storage = new ConcurrentHashMap<>(); + + @Override + public UUID save(Voucher voucher) { + if (storage.containsKey(voucher.getVoucherId())) { + throw new IllegalStateException(ALREADY_EXISTS); + } + storage.put(voucher.getVoucherId(), voucher); + return voucher.getVoucherId(); + } + + @Override + public List findAll() { + return List.copyOf(storage.values()); + } + + @Override + public long countTotalVoucher() { + return storage.size(); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/Repository/VoucherRepository.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/Repository/VoucherRepository.java new file mode 100644 index 0000000000..3f5570ad6b --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/Repository/VoucherRepository.java @@ -0,0 +1,22 @@ +package com.programmers.springbootbasic.domain.voucher.Repository; + +import com.programmers.springbootbasic.common.domain.Page; +import com.programmers.springbootbasic.common.domain.Pageable; +import com.programmers.springbootbasic.domain.voucher.Voucher; + +import java.util.List; +import java.util.UUID; + +public interface VoucherRepository { + String NOT_SUPPORT = "현재 저장소에서는 해당 기능을 지원하지 않습니다."; + + UUID save(Voucher voucher); + + List findAll(); + + long countTotalVoucher(); + + default Page findAllWithPagination(Pageable pageable) { + throw new UnsupportedOperationException(NOT_SUPPORT); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/Voucher.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/Voucher.java new file mode 100644 index 0000000000..2aead227d3 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/Voucher.java @@ -0,0 +1,35 @@ +package com.programmers.springbootbasic.domain.voucher; + +import com.programmers.springbootbasic.common.util.Validator; + +import java.util.UUID; + +public abstract class Voucher { + private final UUID voucherId; + private final VoucherType voucherType; + + protected Voucher(UUID voucherId, VoucherType voucherType) { + Validator.checkNullUuid(voucherId); + this.voucherId = voucherId; + this.voucherType = voucherType; + } + + public static Voucher createVoucher(UUID voucherId, VoucherType voucherType, int amountOrPercent) { + return switch (voucherType) { + case FIX -> new FixedAmountVoucher(voucherId, amountOrPercent); + case PERCENT -> new PercentDiscountVoucher(voucherId, amountOrPercent); + }; + } + + public UUID getVoucherId() { + return voucherId; + } + + public VoucherType getVoucherType() { + return voucherType; + } + + public abstract Long getDiscountPrice(Long priceBeforeDiscount); + + public abstract int getAmountOrPercent(); +} diff --git a/src/main/java/com/programmers/springbootbasic/domain/voucher/VoucherType.java b/src/main/java/com/programmers/springbootbasic/domain/voucher/VoucherType.java new file mode 100644 index 0000000000..aa06fb0496 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/domain/voucher/VoucherType.java @@ -0,0 +1,28 @@ +package com.programmers.springbootbasic.domain.voucher; + +import java.util.Arrays; + +public enum VoucherType { + PERCENT("정률 할인"), + FIX("정액 할인"); + + private static final String INVALID_VOUCHER_TYPE = "잘못된 바우처 유형입니다. 현재 입력 값: "; + + private final String description; + + VoucherType(String description) { + this.description = description; + } + + public static VoucherType from(String typeDescription) { + return Arrays.stream(values()) + .filter(enumeration -> typeDescription.equals(enumeration.description)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException(INVALID_VOUCHER_TYPE + typeDescription)); + } + + public String getDescription() { + return description; + } + +} diff --git a/src/main/java/com/programmers/springbootbasic/presentation/Command.java b/src/main/java/com/programmers/springbootbasic/presentation/Command.java new file mode 100644 index 0000000000..dced707a47 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/presentation/Command.java @@ -0,0 +1,27 @@ +package com.programmers.springbootbasic.presentation; + +import java.util.Arrays; + +public enum Command { + CREATE("생성"), + LIST("조회"), + EXIT("나가기"); + private static final String INVALID_COMMAND = "잘못된 명령어 입니다. 현재 입력 값: "; + + private final String description; + + Command(String description) { + this.description = description; + } + + public static Command from(String inputCommand) { + return Arrays.stream(values()) + .filter(enumerate -> inputCommand.equals(enumerate.getDescription())) + .findAny() + .orElseThrow(() -> new IllegalArgumentException(INVALID_COMMAND + inputCommand)); + } + + public String getDescription() { + return description; + } +} diff --git a/src/main/java/com/programmers/springbootbasic/presentation/controller/CustomerRestController.java b/src/main/java/com/programmers/springbootbasic/presentation/controller/CustomerRestController.java new file mode 100644 index 0000000000..fb1e6fd9fa --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/presentation/controller/CustomerRestController.java @@ -0,0 +1,47 @@ +package com.programmers.springbootbasic.presentation.controller; + +import com.programmers.springbootbasic.presentation.controller.dto.Customer.CustomerCreationRequest; +import com.programmers.springbootbasic.presentation.controller.dto.Customer.CustomerUpdateRequest; +import com.programmers.springbootbasic.service.CustomerService; +import com.programmers.springbootbasic.service.dto.Customer.CustomerResponse; +import jakarta.validation.Valid; +import jakarta.validation.constraints.Email; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PatchMapping; +import org.springframework.web.bind.annotation.PathVariable; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import java.util.UUID; + +@RestController +@RequestMapping("/api/customer") +public class CustomerRestController { + private final CustomerService customerService; + + public CustomerRestController(CustomerService customerService) { + this.customerService = customerService; + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public UUID createCustomer(@RequestBody @Valid CustomerCreationRequest request) { + return customerService.createCustomer(request); + } + + @GetMapping("/{email}") + @ResponseStatus(HttpStatus.OK) + public CustomerResponse findCustomer(@PathVariable @Valid @Email String email) { + return customerService.findByEmail(email); + } + + @PatchMapping("/{id}") + @ResponseStatus(HttpStatus.OK) + public void updateCustomer(@PathVariable UUID id, @RequestBody @Valid CustomerUpdateRequest request) { + customerService.updateCustomer(id, request); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/presentation/controller/VoucherController.java b/src/main/java/com/programmers/springbootbasic/presentation/controller/VoucherController.java new file mode 100644 index 0000000000..372df2041c --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/presentation/controller/VoucherController.java @@ -0,0 +1,34 @@ +package com.programmers.springbootbasic.presentation.controller; + +import com.programmers.springbootbasic.common.util.Validator; +import com.programmers.springbootbasic.domain.voucher.VoucherType; +import com.programmers.springbootbasic.presentation.controller.dto.Voucher.VoucherCreationRequest; +import com.programmers.springbootbasic.service.VoucherService; +import com.programmers.springbootbasic.service.dto.Voucher.VoucherResponses; +import org.springframework.stereotype.Controller; + +import java.util.UUID; + +@Controller +public class VoucherController { + private final VoucherService voucherService; + + public VoucherController(VoucherService voucherService) { + this.voucherService = voucherService; + } + + public UUID createVoucher(String voucherType, Integer amountOrPercent) { + checkValid(voucherType, amountOrPercent); + VoucherCreationRequest request = new VoucherCreationRequest(VoucherType.from(voucherType), amountOrPercent); + return voucherService.createVoucher(request); + } + + private void checkValid(String voucherType, Integer amountOrPercent) { + Validator.checkNullOrBlank(voucherType); + Validator.checkNullNumber(amountOrPercent); + } + + public VoucherResponses listVouchers() { + return voucherService.list(); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/presentation/controller/VoucherRestController.java b/src/main/java/com/programmers/springbootbasic/presentation/controller/VoucherRestController.java new file mode 100644 index 0000000000..e700f0edb2 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/presentation/controller/VoucherRestController.java @@ -0,0 +1,47 @@ +package com.programmers.springbootbasic.presentation.controller; + +import com.programmers.springbootbasic.common.domain.Page; +import com.programmers.springbootbasic.common.domain.Pageable; +import com.programmers.springbootbasic.presentation.controller.dto.Voucher.VoucherCreationRequest; +import com.programmers.springbootbasic.service.VoucherService; +import com.programmers.springbootbasic.service.dto.Voucher.VoucherResponse; +import com.programmers.springbootbasic.service.dto.Voucher.VoucherResponses; +import jakarta.validation.Valid; +import org.springframework.http.HttpStatus; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.ModelAttribute; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.ResponseStatus; +import org.springframework.web.bind.annotation.RestController; + +import java.util.UUID; + +@RestController +@RequestMapping("/api/voucher") +public class VoucherRestController { + private final VoucherService voucherService; + + public VoucherRestController(VoucherService voucherService) { + this.voucherService = voucherService; + } + + @PostMapping + @ResponseStatus(HttpStatus.CREATED) + public UUID createVoucher(@RequestBody @Valid VoucherCreationRequest request) { + return voucherService.createVoucher(request); + } + + @GetMapping("/vouchers") + @ResponseStatus(HttpStatus.OK) + public Page findVouchers(@ModelAttribute Pageable pageable) { + return voucherService.findVoucherByPage(pageable); + } + + @GetMapping("/all") + @ResponseStatus(HttpStatus.OK) + public VoucherResponses listVouchers() { + return voucherService.list(); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/presentation/controller/dto/Customer/CustomerCreationRequest.java b/src/main/java/com/programmers/springbootbasic/presentation/controller/dto/Customer/CustomerCreationRequest.java new file mode 100644 index 0000000000..e7ea6339ea --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/presentation/controller/dto/Customer/CustomerCreationRequest.java @@ -0,0 +1,17 @@ +package com.programmers.springbootbasic.presentation.controller.dto.Customer; + + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +public record CustomerCreationRequest( + @NotBlank + @Size(min = 1, max = 20, message = "이름은 1자 이상 20자 이하여야 합니다.") + String name, + @NotNull + @Email(message = "이메일 형식이 아닙니다.") + String email +) { +} diff --git a/src/main/java/com/programmers/springbootbasic/presentation/controller/dto/Customer/CustomerUpdateRequest.java b/src/main/java/com/programmers/springbootbasic/presentation/controller/dto/Customer/CustomerUpdateRequest.java new file mode 100644 index 0000000000..5e39b4d821 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/presentation/controller/dto/Customer/CustomerUpdateRequest.java @@ -0,0 +1,16 @@ +package com.programmers.springbootbasic.presentation.controller.dto.Customer; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; +import jakarta.validation.constraints.NotNull; +import jakarta.validation.constraints.Size; + +public record CustomerUpdateRequest( + @NotBlank + @Size(min = 1, max = 20, message = "이름은 1자 이상 20자 이하여야 합니다.") + String name, + @NotNull + @Email(message = "이메일 형식이 아닙니다.") + String email +) { +} diff --git a/src/main/java/com/programmers/springbootbasic/presentation/controller/dto/Voucher/VoucherCreationRequest.java b/src/main/java/com/programmers/springbootbasic/presentation/controller/dto/Voucher/VoucherCreationRequest.java new file mode 100644 index 0000000000..1b0c06b7ba --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/presentation/controller/dto/Voucher/VoucherCreationRequest.java @@ -0,0 +1,12 @@ +package com.programmers.springbootbasic.presentation.controller.dto.Voucher; + +import com.programmers.springbootbasic.domain.voucher.VoucherType; +import jakarta.validation.constraints.NotNull; + + +public record VoucherCreationRequest( + @NotNull + VoucherType voucherType, + int amountOrPercent +) { +} diff --git a/src/main/java/com/programmers/springbootbasic/presentation/view/Console.java b/src/main/java/com/programmers/springbootbasic/presentation/view/Console.java new file mode 100644 index 0000000000..a4814532b9 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/presentation/view/Console.java @@ -0,0 +1,31 @@ +package com.programmers.springbootbasic.presentation.view; + +import org.springframework.stereotype.Component; + +import java.io.*; + +@Component +public class Console implements Input, Output { + private static final String NEW_LINE = "\n"; + + private final BufferedReader br = new BufferedReader(new InputStreamReader(System.in)); + private final BufferedWriter bw = new BufferedWriter(new OutputStreamWriter(System.out)); + + @Override + public String inputLine() throws IOException { + return br.readLine(); + } + + @Override + public void print(String input) throws IOException { + bw.write(input); + bw.flush(); + } + + @Override + public void printLine(String input) throws IOException { + bw.write(input); + bw.write(NEW_LINE); + bw.flush(); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/presentation/view/ConsoleApplicationView.java b/src/main/java/com/programmers/springbootbasic/presentation/view/ConsoleApplicationView.java new file mode 100644 index 0000000000..157d56e9ac --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/presentation/view/ConsoleApplicationView.java @@ -0,0 +1,121 @@ +package com.programmers.springbootbasic.presentation.view; + +import com.programmers.springbootbasic.common.util.NumberParser; +import com.programmers.springbootbasic.common.util.Validator; +import com.programmers.springbootbasic.presentation.Command; +import com.programmers.springbootbasic.service.dto.Voucher.VoucherResponse; +import com.programmers.springbootbasic.service.dto.Voucher.VoucherResponses; +import org.springframework.stereotype.Component; + +import java.io.IOException; +import java.util.UUID; + +@Component +public class ConsoleApplicationView { + private static final String BLANK = ""; + private static final String PERCENT = "정률 할인"; + private static final String FIX = "정액 할인"; + private static final String OPENING_MESSAGE = """ + === 바우처 프로그램 === + 프로그램을 종료하려면 "나가기" 를 입력하세요. + 새로운 바우처을 생성하려면 "생성" 를 입력하세요. + 모든 바우처을 나열하려면 "조회" 를 입력하세요. + + 입력:\s"""; + private static final String CREATE_MESSAGE = "바우처을 생성합니다."; + private static final String LIST_MESSAGE = "바우처들을 조회합니다."; + private static final String EXIT_MESSAGE = "어플리케이션을 종료합니다."; + private static final String INPUT_TYPE = """ + 바우처의 타입을 입력해주세요. + 정률 할인 쿠폰을 생성하려면 "%s" 을 입력하세요. + 정액 할인 쿠폰을 생성하려면 "%s" 을 입력하세요. + + 입력:\s"""; + + private static final String INPUT_AMOUNT_OR_PERCENT = """ + 할인 금액을 아래 한도에 맞춰 입력하세요. + 정률 할인 시: 1 ~ 100% (퍼센트 제외) + 정액 할인 시: 10 ~ 10,000,000원 (원 제외) + + 입력:\s"""; + private static final String CREATED_VOUCHER_INFO = "=== 바우처 생성완료 ==="; + private static final String INVALID_VOUCHER_TYPE = "잘못된 바우처 유형입니다. 현재 입력 값: "; + + private final Console console; + + public ConsoleApplicationView(Console consoleView) { + this.console = consoleView; + } + + public Command selectCommand() throws IOException { + console.print(OPENING_MESSAGE); + String inputCommand = console.inputLine(); + printNewLine(); + return Command.from(inputCommand); + } + + public void printNewLine() throws IOException { + console.printLine(BLANK); + } + + public void startCreation() throws IOException { + console.printLine(CREATE_MESSAGE); + printNewLine(); + } + + public String inputType() throws IOException { + // 타입 입력 + console.print(String.format(INPUT_TYPE, PERCENT, FIX)); + String voucherType = console.inputLine(); + printNewLine(); + Validator.checkNullOrBlank(voucherType); + checkInvalidType(voucherType); + return voucherType; + } + + private void checkInvalidType(String input) { + if (input.equals(PERCENT) || input.equals(FIX)) { + return; + } + throw new IllegalArgumentException(INVALID_VOUCHER_TYPE + input); + } + + public int inputAmountOrPercent() throws IOException { + // 할인액 or 할인율 입력 + console.print(INPUT_AMOUNT_OR_PERCENT); + String amountOrPercentInput = console.inputLine(); + printNewLine(); + return NumberParser.parseToInt(amountOrPercentInput); + } + + public void printCreatedVoucherId(UUID voucherId) throws IOException { + console.printLine(CREATED_VOUCHER_INFO); + printVoucherId(voucherId); + printNewLine(); + } + + private void printVoucherId(UUID voucherId) throws IOException { + console.printLine(String.valueOf(voucherId)); + } + + public void listVouchers(VoucherResponses responses) throws IOException { + console.printLine(LIST_MESSAGE); + for (VoucherResponse response : responses.voucherResponses()) { + printVoucherResponse(response); + } + printNewLine(); + } + + private void printVoucherResponse(VoucherResponse response) throws IOException { + console.printLine(response.toString()); + } + + public void exit() throws IOException { + console.printLine(EXIT_MESSAGE); + } + + public void printErrorMessage(String message) throws IOException { + console.printLine(message); + printNewLine(); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/presentation/view/Input.java b/src/main/java/com/programmers/springbootbasic/presentation/view/Input.java new file mode 100644 index 0000000000..ba0882d1cc --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/presentation/view/Input.java @@ -0,0 +1,8 @@ +package com.programmers.springbootbasic.presentation.view; + +import java.io.IOException; + +public interface Input { + String inputLine() throws IOException; + +} diff --git a/src/main/java/com/programmers/springbootbasic/presentation/view/Output.java b/src/main/java/com/programmers/springbootbasic/presentation/view/Output.java new file mode 100644 index 0000000000..6d24feec09 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/presentation/view/Output.java @@ -0,0 +1,10 @@ +package com.programmers.springbootbasic.presentation.view; + +import java.io.IOException; + +public interface Output { + void print(String input) throws IOException; + + void printLine(String input) throws IOException; + +} diff --git a/src/main/java/com/programmers/springbootbasic/service/CustomerMapper.java b/src/main/java/com/programmers/springbootbasic/service/CustomerMapper.java new file mode 100644 index 0000000000..d6c7c70687 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/service/CustomerMapper.java @@ -0,0 +1,30 @@ +package com.programmers.springbootbasic.service; + +import com.programmers.springbootbasic.domain.customer.Customer; +import com.programmers.springbootbasic.domain.customer.CustomerUpdateDto; +import com.programmers.springbootbasic.presentation.controller.dto.Customer.CustomerCreationRequest; +import com.programmers.springbootbasic.presentation.controller.dto.Customer.CustomerUpdateRequest; +import com.programmers.springbootbasic.service.dto.Customer.CustomerResponse; + +import java.util.UUID; + +public final class CustomerMapper { + private CustomerMapper() { + } + + public static Customer toCustomer(CustomerCreationRequest request) { + return new Customer(UUID.randomUUID(), request.email(), request.name()); + } + + public static Customer toCustomer(CustomerUpdateRequest request) { + return new Customer(UUID.randomUUID(), request.email(), request.name()); + } + + public static CustomerResponse toCustomerResponse(Customer customer) { + return new CustomerResponse(customer.getCustomerId(), customer.getName(), customer.getEmail()); + } + + public static CustomerUpdateDto toCustomerUpdateDto(UUID id, CustomerUpdateRequest request) { + return new CustomerUpdateDto(id, request.name(), request.email()); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/service/CustomerService.java b/src/main/java/com/programmers/springbootbasic/service/CustomerService.java new file mode 100644 index 0000000000..93dfef470a --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/service/CustomerService.java @@ -0,0 +1,37 @@ +package com.programmers.springbootbasic.service; + +import com.programmers.springbootbasic.domain.customer.Customer; +import com.programmers.springbootbasic.domain.customer.CustomerUpdateDto; +import com.programmers.springbootbasic.domain.customer.Repository.CustomerRepository; +import com.programmers.springbootbasic.presentation.controller.dto.Customer.CustomerCreationRequest; +import com.programmers.springbootbasic.presentation.controller.dto.Customer.CustomerUpdateRequest; +import com.programmers.springbootbasic.service.dto.Customer.CustomerResponse; +import org.springframework.stereotype.Service; + +import java.util.UUID; + +@Service +public class CustomerService { + private static final String NO_CUSTOMER_MATCHES_EMAIL = "해당하는 이메일의 고객이 존재하지 않습니다."; + private final CustomerRepository customerRepository; + + public CustomerService(CustomerRepository customerRepository) { + this.customerRepository = customerRepository; + } + + public UUID createCustomer(CustomerCreationRequest request) { + Customer customer = CustomerMapper.toCustomer(request); + return customerRepository.save(customer); + } + + public void updateCustomer(UUID id, CustomerUpdateRequest request) { + CustomerUpdateDto customerUpdateDto = CustomerMapper.toCustomerUpdateDto(id, request); + customerRepository.update(customerUpdateDto); + } + + public CustomerResponse findByEmail(String email) { + return customerRepository.findByEmail(email) + .map(CustomerMapper::toCustomerResponse) + .orElseThrow(() -> new IllegalArgumentException(NO_CUSTOMER_MATCHES_EMAIL)); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/service/VoucherMapper.java b/src/main/java/com/programmers/springbootbasic/service/VoucherMapper.java new file mode 100644 index 0000000000..dc87068288 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/service/VoucherMapper.java @@ -0,0 +1,38 @@ +package com.programmers.springbootbasic.service; + +import com.programmers.springbootbasic.domain.voucher.Voucher; +import com.programmers.springbootbasic.domain.voucher.VoucherType; +import com.programmers.springbootbasic.presentation.controller.dto.Voucher.VoucherCreationRequest; +import com.programmers.springbootbasic.service.dto.Voucher.VoucherResponse; +import com.programmers.springbootbasic.service.dto.Voucher.VoucherResponses; + +import java.util.List; +import java.util.UUID; + +public final class VoucherMapper { + private VoucherMapper() { + + } + + public static Voucher toVoucher(VoucherCreationRequest request) { + VoucherType voucherType = request.voucherType(); + return Voucher.createVoucher(UUID.randomUUID(), voucherType, request.amountOrPercent()); + } + + public static VoucherResponses toVoucherResponseList(List vouchers) { + List voucherResponses = vouchers.stream() + .map(VoucherMapper::toVoucherResponse) + .toList(); + return new VoucherResponses(voucherResponses); + } + + public static VoucherResponse toVoucherResponse(Voucher voucher) { + voucher.getAmountOrPercent(); + return new VoucherResponse( + voucher.getVoucherId(), + voucher.getVoucherType(), + voucher.getAmountOrPercent() + ); + } + +} diff --git a/src/main/java/com/programmers/springbootbasic/service/VoucherService.java b/src/main/java/com/programmers/springbootbasic/service/VoucherService.java new file mode 100644 index 0000000000..6d0c7ff84c --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/service/VoucherService.java @@ -0,0 +1,35 @@ +package com.programmers.springbootbasic.service; + +import com.programmers.springbootbasic.common.domain.Page; +import com.programmers.springbootbasic.common.domain.Pageable; +import com.programmers.springbootbasic.domain.voucher.Repository.VoucherRepository; +import com.programmers.springbootbasic.domain.voucher.Voucher; +import com.programmers.springbootbasic.presentation.controller.dto.Voucher.VoucherCreationRequest; +import com.programmers.springbootbasic.service.dto.Voucher.VoucherResponse; +import com.programmers.springbootbasic.service.dto.Voucher.VoucherResponses; +import org.springframework.stereotype.Service; + +import java.util.UUID; + +@Service +public class VoucherService { + private final VoucherRepository voucherRepository; + + public VoucherService(VoucherRepository voucherRepository) { + this.voucherRepository = voucherRepository; + } + + public UUID createVoucher(VoucherCreationRequest request) { + Voucher voucher = VoucherMapper.toVoucher(request); + return voucherRepository.save(voucher); + } + + public VoucherResponses list() { + return VoucherMapper.toVoucherResponseList(voucherRepository.findAll()); + } + + public Page findVoucherByPage(Pageable pageable) { + return voucherRepository.findAllWithPagination(pageable) + .map(VoucherMapper::toVoucherResponse); + } +} diff --git a/src/main/java/com/programmers/springbootbasic/service/dto/Customer/CustomerResponse.java b/src/main/java/com/programmers/springbootbasic/service/dto/Customer/CustomerResponse.java new file mode 100644 index 0000000000..8607672bf4 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/service/dto/Customer/CustomerResponse.java @@ -0,0 +1,10 @@ +package com.programmers.springbootbasic.service.dto.Customer; + +import java.util.UUID; + +public record CustomerResponse( + UUID customerId, + String name, + String email +) { +} diff --git a/src/main/java/com/programmers/springbootbasic/service/dto/Voucher/VoucherResponse.java b/src/main/java/com/programmers/springbootbasic/service/dto/Voucher/VoucherResponse.java new file mode 100644 index 0000000000..615887efc3 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/service/dto/Voucher/VoucherResponse.java @@ -0,0 +1,14 @@ +package com.programmers.springbootbasic.service.dto.Voucher; + + +import com.programmers.springbootbasic.domain.voucher.VoucherType; + +import java.util.UUID; + +public record VoucherResponse( + UUID voucherId, + VoucherType voucherType, + int amountOrPercent +) { + +} diff --git a/src/main/java/com/programmers/springbootbasic/service/dto/Voucher/VoucherResponses.java b/src/main/java/com/programmers/springbootbasic/service/dto/Voucher/VoucherResponses.java new file mode 100644 index 0000000000..89d7226501 --- /dev/null +++ b/src/main/java/com/programmers/springbootbasic/service/dto/Voucher/VoucherResponses.java @@ -0,0 +1,8 @@ +package com.programmers.springbootbasic.service.dto.Voucher; + +import java.util.List; + +public record VoucherResponses( + List voucherResponses +) { +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml new file mode 100644 index 0000000000..05d7b67678 --- /dev/null +++ b/src/main/resources/application.yml @@ -0,0 +1,23 @@ +spring: + datasource: + url: jdbc:mysql://localhost:3306/spring_basic + username: root + password: + driver-class-name: com.mysql.cj.jdbc.Driver + sql: + init: + mode: always + +--- +spring: + config: + activate: + on-profile: console + main: + web-application-type: none + +--- +spring: + config: + activate: + on-profile: web diff --git a/src/main/resources/logback.xml b/src/main/resources/logback.xml new file mode 100644 index 0000000000..8a2cc572d8 --- /dev/null +++ b/src/main/resources/logback.xml @@ -0,0 +1,27 @@ + + + + + logs/myApp-%d{yyyy-MM-dd}.log + + + + %date %level [%thread] %logger{10} [%file:%line] -%kvp- %msg%n + + + + + + %kvp %msg%n + + + + + + + + + + + + diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 0000000000..a3726e28be --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,18 @@ +drop table if exists customer; +drop table if exists voucher; + +CREATE TABLE customer +( + customer_id char(36) PRIMARY KEY, + name varchar(20) NOT NULL, + email varchar(50) NOT NULL, + CONSTRAINT unq_user_email UNIQUE (email) +); + + +CREATE TABLE voucher +( + voucher_id char(36) PRIMARY KEY, + voucher_type varchar(20) NOT NULL, + amount_or_percent int NOT NULL +); diff --git a/src/test/java/com/programmers/springbootbasic/SpringbootBasicApplicationTests.java b/src/test/java/com/programmers/springbootbasic/SpringbootBasicApplicationTests.java new file mode 100644 index 0000000000..99ebcf1c61 --- /dev/null +++ b/src/test/java/com/programmers/springbootbasic/SpringbootBasicApplicationTests.java @@ -0,0 +1,9 @@ +package com.programmers.springbootbasic; + +import org.junit.jupiter.api.Test; +import org.springframework.boot.test.context.SpringBootTest; + +@SpringBootTest +class SpringbootBasicApplicationTests { + +} diff --git a/src/test/java/com/programmers/springbootbasic/common/util/PatternUtilsTest.java b/src/test/java/com/programmers/springbootbasic/common/util/PatternUtilsTest.java new file mode 100644 index 0000000000..79543bcdbb --- /dev/null +++ b/src/test/java/com/programmers/springbootbasic/common/util/PatternUtilsTest.java @@ -0,0 +1,39 @@ +package com.programmers.springbootbasic.common.util; + +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static org.assertj.core.api.Assertions.assertThat; + +class PatternUtilsTest { + + @ParameterizedTest + @ValueSource(strings = {"20221130", "2022.05.31", "9999-9999-9999"}) + void given_비정상날짜포맷_when_날짜패턴매칭_then_불일치(String wrongDate) { + // given + // when + boolean result = PatternUtils.isDate(wrongDate); + // then + assertThat(result).isFalse(); + } + + @ParameterizedTest + @ValueSource(strings = {"2022-11-30 12:12", "2022-11-30 121212"}) + void given_비정상날짜시간포맷_when_날짜패턴매칭_then_불일치(String wrongDate) { + // given + // when + boolean result = PatternUtils.isDate(wrongDate); + // then + assertThat(result).isFalse(); + } + + @ParameterizedTest + @ValueSource(strings = {"!@#$›$!‹$!@$!", "sangmin@kkk.12121", "dasdasgdfgs", "sasa@1.1"}) + void given_비정상이메일_when_이메일패턴매칭_then_불일치(String wrongEmail) { + // given + // when + boolean result = PatternUtils.isEmail(wrongEmail); + // then + assertThat(result).isFalse(); + } +} diff --git a/src/test/java/com/programmers/springbootbasic/domain/customer/JdbcCustomerRepositoryTest.java b/src/test/java/com/programmers/springbootbasic/domain/customer/JdbcCustomerRepositoryTest.java new file mode 100644 index 0000000000..0794cef112 --- /dev/null +++ b/src/test/java/com/programmers/springbootbasic/domain/customer/JdbcCustomerRepositoryTest.java @@ -0,0 +1,57 @@ +package com.programmers.springbootbasic.domain.customer; + +import com.programmers.springbootbasic.domain.customer.Repository.JdbcCustomerRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.context.annotation.Import; + +import java.util.Optional; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +@JdbcTest +@Import(JdbcCustomerRepository.class) +class JdbcCustomerRepositoryTest { + @Autowired + private JdbcCustomerRepository repository; + + @Test + @DisplayName("고객을 저장 할 수 있다") + void save() { + // given + Customer customer = new Customer(UUID.randomUUID(), "test@gmail.com", "test"); + // when + UUID id = repository.save(customer); + // then + assertThat(id).isNotNull(); + } + + @Test + @DisplayName("고객을 조회 할 수 있다") + void findByEmail() { + // given + Customer customer = new Customer(UUID.randomUUID(), "test@gmail.com", "test"); + repository.save(customer); + // when + Optional result = repository.findByEmail(customer.getEmail()); + // then + assertThat(result).isPresent(); + } + + @Test + @DisplayName("고객을 수정 할 수 있다") + void update() { + // given + Customer customer = new Customer(UUID.randomUUID(), "test@gmail.com", "test"); + repository.save(customer); + Customer forUpdate = new Customer(customer.getCustomerId(), "update@gmail.com", "update"); + // when + repository.update(forUpdate); + // then + Optional updated = repository.findByEmail(forUpdate.getEmail()); + assertThat(updated).get().usingRecursiveComparison().isEqualTo(forUpdate); + } +} diff --git a/src/test/java/com/programmers/springbootbasic/domain/voucher/FixedAmountVoucherTest.java b/src/test/java/com/programmers/springbootbasic/domain/voucher/FixedAmountVoucherTest.java new file mode 100644 index 0000000000..cf1a498169 --- /dev/null +++ b/src/test/java/com/programmers/springbootbasic/domain/voucher/FixedAmountVoucherTest.java @@ -0,0 +1,51 @@ +package com.programmers.springbootbasic.domain.voucher; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class FixedAmountVoucherTest { + @Test + void 정상입력값_바우처생성_성공() { + // given + UUID voucherId = UUID.randomUUID(); + int amount = 5_000; + + // when + Voucher fixedAmountVoucher = new FixedAmountVoucher(voucherId, amount); + + // then + assertThat(fixedAmountVoucher).isNotNull(); + } + + @Test + void 잘못된할인금액_바우처생성_예외발생() { + // given + UUID voucherId = UUID.randomUUID(); + int amount = 1_000_000_000; + + // when && then + assertThatThrownBy(() -> new FixedAmountVoucher(voucherId, amount)) + .isInstanceOf(IllegalArgumentException.class); + } + + @ParameterizedTest + @CsvSource(value = {"10000,5000,5000", "300,50000,0"}) + void 물건금액할인금액_할인_할인된금액(Long price, int amount, Long expectedPrice) { + // given + FixedAmountVoucher voucher = new FixedAmountVoucher( + UUID.randomUUID(), + amount); + + // when + Long discountedPrice = voucher.getDiscountPrice(price); + + // then + assertThat(discountedPrice).isEqualTo(expectedPrice); + } +} diff --git a/src/test/java/com/programmers/springbootbasic/domain/voucher/JdbcVoucherRepositoryTest.java b/src/test/java/com/programmers/springbootbasic/domain/voucher/JdbcVoucherRepositoryTest.java new file mode 100644 index 0000000000..0213100dda --- /dev/null +++ b/src/test/java/com/programmers/springbootbasic/domain/voucher/JdbcVoucherRepositoryTest.java @@ -0,0 +1,48 @@ +package com.programmers.springbootbasic.domain.voucher; + +import com.programmers.springbootbasic.domain.voucher.Repository.JdbcVoucherRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.boot.test.autoconfigure.jdbc.JdbcTest; +import org.springframework.context.annotation.Import; + +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +@JdbcTest +@Import(JdbcVoucherRepository.class) +class JdbcVoucherRepositoryTest { + @Autowired + private JdbcVoucherRepository repository; + + @ParameterizedTest + @CsvSource(value = {"정률 할인, 10", "정액 할인, 10000"}) + @DisplayName("바우처를 저장할 수 있다") + void save(String voucherType, int amountOrPercent) { + // given + Voucher voucher = Voucher.createVoucher(UUID.randomUUID(), VoucherType.from(voucherType), amountOrPercent); + // when + UUID id = repository.save(voucher); + // then + assertThat(id).isNotNull(); + } + + @Test + @DisplayName("바우처를 모두 조회할 수 있다") + void findAll() { + // given + Voucher fix = Voucher.createVoucher(UUID.randomUUID(), VoucherType.FIX, 10000); + Voucher percent = Voucher.createVoucher(UUID.randomUUID(), VoucherType.PERCENT, 10); + repository.save(fix); + repository.save(percent); + // when + List all = repository.findAll(); + // then + assertThat(all).hasSize(2); + } +} diff --git a/src/test/java/com/programmers/springbootbasic/domain/voucher/MemoryVoucherRepositoryTest.java b/src/test/java/com/programmers/springbootbasic/domain/voucher/MemoryVoucherRepositoryTest.java new file mode 100644 index 0000000000..ae34becb05 --- /dev/null +++ b/src/test/java/com/programmers/springbootbasic/domain/voucher/MemoryVoucherRepositoryTest.java @@ -0,0 +1,53 @@ +package com.programmers.springbootbasic.domain.voucher; + +import com.programmers.springbootbasic.domain.voucher.Repository.MemoryVoucherRepository; +import com.programmers.springbootbasic.domain.voucher.Repository.VoucherRepository; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.EnumSource; + +import java.util.List; +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; + +class MemoryVoucherRepositoryTest { + private final VoucherRepository voucherRepository = new MemoryVoucherRepository(); + + @ParameterizedTest + @EnumSource(VoucherType.class) + void 바우처생성_바우처저장_성공(VoucherType voucherType) { + // given + UUID voucherId = UUID.randomUUID(); + // when + Voucher voucher = switch (voucherType) { + case FIX -> { + int amount = 5_000; + yield new FixedAmountVoucher(voucherId, amount); + } + case PERCENT -> { + int percent = 30; + yield new PercentDiscountVoucher(voucherId, percent); + } + }; + UUID id = voucherRepository.save(voucher); + // then + assertThat(id).isNotNull(); + } + + @Test + void 바우처생성및저장_모든바우처가져오기_성공() { + // given + UUID voucherId = UUID.randomUUID(); + int amount = 5_000; + + Voucher voucher = new FixedAmountVoucher(voucherId, amount); + voucherRepository.save(voucher); + + // when + List all = voucherRepository.findAll(); + + // then + assertThat(all).isNotNull(); + } +} diff --git a/src/test/java/com/programmers/springbootbasic/domain/voucher/PercentDiscountVoucherTest.java b/src/test/java/com/programmers/springbootbasic/domain/voucher/PercentDiscountVoucherTest.java new file mode 100644 index 0000000000..948030495d --- /dev/null +++ b/src/test/java/com/programmers/springbootbasic/domain/voucher/PercentDiscountVoucherTest.java @@ -0,0 +1,52 @@ +package com.programmers.springbootbasic.domain.voucher; + +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class PercentDiscountVoucherTest { + @Test + void 정상입력값_바우처생성_성공() { + // given + UUID voucherId = UUID.randomUUID(); + int percent = 30; + + // when + Voucher percentDiscountVoucher = new PercentDiscountVoucher(voucherId, percent); + + // then + assertThat(percentDiscountVoucher).isNotNull(); + } + + @Test + void 잘못된퍼센트_바우처생성_예외발생() { + // given + UUID voucherId = UUID.randomUUID(); + int percent = 300; + + // when && then + assertThatThrownBy(() -> new PercentDiscountVoucher(voucherId, percent)) + .isInstanceOf(IllegalArgumentException.class); + } + + @ParameterizedTest + @CsvSource(value = {"4550,33,3050", "5500,10,4950"}) + void 물건금액할인퍼센트_할인_할인된금액(Long price, int percent, Long expectedPrice) { + // given + PercentDiscountVoucher voucher = new PercentDiscountVoucher( + UUID.randomUUID(), + percent + ); + + // when + Long discountedPrice = voucher.getDiscountPrice(price); + + // then + assertThat(discountedPrice).isEqualTo(expectedPrice); + } +} diff --git a/src/test/java/com/programmers/springbootbasic/domain/voucher/VoucherTest.java b/src/test/java/com/programmers/springbootbasic/domain/voucher/VoucherTest.java new file mode 100644 index 0000000000..1629be791b --- /dev/null +++ b/src/test/java/com/programmers/springbootbasic/domain/voucher/VoucherTest.java @@ -0,0 +1,35 @@ +package com.programmers.springbootbasic.domain.voucher; + +import org.junit.jupiter.api.Test; + +import java.util.UUID; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class VoucherTest { + + @Test + void 정상입력값_바우처생성_성공() { + //given + UUID voucherId = UUID.randomUUID(); + int amount = 10_000; + + // when + Voucher fixedAmountVoucher = new FixedAmountVoucher(voucherId, amount); + + // then + assertThat(fixedAmountVoucher).isNotNull(); + } + + @Test + void 잘못된바우처아이디_바우처생성_예외발생() { + // given + UUID voucherId = null; + int amount = 10_000; + + // when && then + assertThatThrownBy(() -> new FixedAmountVoucher(voucherId, amount)) + .isInstanceOf(IllegalArgumentException.class); + } +} diff --git a/src/test/java/com/programmers/springbootbasic/domain/voucher/VoucherTypeTest.java b/src/test/java/com/programmers/springbootbasic/domain/voucher/VoucherTypeTest.java new file mode 100644 index 0000000000..ab14590737 --- /dev/null +++ b/src/test/java/com/programmers/springbootbasic/domain/voucher/VoucherTypeTest.java @@ -0,0 +1,42 @@ +package com.programmers.springbootbasic.domain.voucher; + +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class VoucherTypeTest { + @Test + void 정률할인_바우처타입으로변환_성공() { + // given + String description = VoucherType.PERCENT.getDescription(); + + // when + VoucherType voucherType = VoucherType.from(description); + + // then + assertThat(voucherType).isEqualTo(VoucherType.PERCENT); + } + + @Test + void 정액할인_바우처타입으로변환_성공() { + // given + String description = VoucherType.FIX.getDescription(); + + // when + VoucherType voucherType = VoucherType.from(description); + + // then + assertThat(voucherType).isEqualTo(VoucherType.FIX); + } + + @Test + void 잘못된값_바우처타입으로변환_에외발생() { + // given + String description = "뭔지 모름"; + + // when && then + assertThatThrownBy(() -> VoucherType.from(description)) + .isInstanceOf(IllegalArgumentException.class); + } +}