diff --git a/yummygyudon/code/java-baseball/.gitignore b/yummygyudon/code/java-baseball/.gitignore new file mode 100644 index 0000000..6c01878 --- /dev/null +++ b/yummygyudon/code/java-baseball/.gitignore @@ -0,0 +1,32 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/** +!**/src/test/** + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/yummygyudon/code/java-baseball/build.gradle b/yummygyudon/code/java-baseball/build.gradle new file mode 100644 index 0000000..080ed8c --- /dev/null +++ b/yummygyudon/code/java-baseball/build.gradle @@ -0,0 +1,25 @@ +plugins { + id 'java' +} + +group 'woowacourse-projects' +version '1.0-SNAPSHOT' + +repositories { + mavenCentral() + maven { url 'https://jitpack.io' } +} + +dependencies { + implementation 'com.github.woowacourse-projects:mission-utils:1.1.0' +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +test { + useJUnitPlatform() +} diff --git a/yummygyudon/code/java-baseball/gradle/wrapper/gradle-wrapper.jar b/yummygyudon/code/java-baseball/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/yummygyudon/code/java-baseball/gradle/wrapper/gradle-wrapper.jar differ diff --git a/yummygyudon/code/java-baseball/gradle/wrapper/gradle-wrapper.properties b/yummygyudon/code/java-baseball/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ae04661 --- /dev/null +++ b/yummygyudon/code/java-baseball/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/yummygyudon/code/java-baseball/gradlew b/yummygyudon/code/java-baseball/gradlew new file mode 100755 index 0000000..a69d9cb --- /dev/null +++ b/yummygyudon/code/java-baseball/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/yummygyudon/code/java-baseball/gradlew.bat b/yummygyudon/code/java-baseball/gradlew.bat new file mode 100644 index 0000000..f127cfd --- /dev/null +++ b/yummygyudon/code/java-baseball/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/yummygyudon/code/java-baseball/settings.gradle b/yummygyudon/code/java-baseball/settings.gradle new file mode 100644 index 0000000..3a4a665 --- /dev/null +++ b/yummygyudon/code/java-baseball/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'java-baseball' + diff --git a/yummygyudon/code/java-baseball/src/main/java/baseball/Application.java b/yummygyudon/code/java-baseball/src/main/java/baseball/Application.java new file mode 100644 index 0000000..f5245af --- /dev/null +++ b/yummygyudon/code/java-baseball/src/main/java/baseball/Application.java @@ -0,0 +1,20 @@ +package baseball; + +import baseball.application.GameLauncher; +import baseball.domain.Computer; +import baseball.util.channel.GameReader; +import baseball.util.channel.Printer; +import baseball.util.enums.Command; +import baseball.util.enums.GameFlag; + +public class Application { + public static void main(String[] args) { + // TODO: 프로그램 구현 + do { + Computer computer = new Computer(); + GameLauncher launcher = new GameLauncher(computer); + Printer.print(Command.START_GAME.getCommand()); + launcher.execute(); + } while (GameReader.read(Command.ASK_RESUME).equals(GameFlag.RETRY)); + } +} diff --git a/yummygyudon/code/java-baseball/src/main/java/baseball/application/GameLauncher.java b/yummygyudon/code/java-baseball/src/main/java/baseball/application/GameLauncher.java new file mode 100644 index 0000000..6102d4d --- /dev/null +++ b/yummygyudon/code/java-baseball/src/main/java/baseball/application/GameLauncher.java @@ -0,0 +1,65 @@ +package baseball.application; + +import baseball.domain.Computer; +import baseball.domain.Judgement; +import baseball.domain.Pitch; +import baseball.util.JudgementConverter; +import baseball.util.channel.GameReader; +import baseball.util.channel.Printer; +import baseball.util.enums.Command; + +import java.util.Arrays; +import java.util.List; + +public class GameLauncher { + + private final Computer computer; + + public GameLauncher(Computer computer) { + this.computer = computer; + } + + /** + * 한 게임을 실행하는 유일한 public 메서드 + * @see baseball.Application + */ + public void execute() { + boolean finishFlag; + do { + Pitch pitch = createPitch(GameReader.read(Command.PITCHING)); + Judgement judgement = judge(pitch); + + Printer.println(JudgementConverter.of(judgement)); + finishFlag = judgement.isThreeStrike(); + } while (!finishFlag); + Printer.print(Command.END_GAME.getCommand()); + } + + /** + * 입력값을 한 글자씩 분할하여 Pitch 객체로 변환하는 메서드 + * + * @param input 사용자의 입력값 + * @return 입력값에 대한 Pitch 변환 객체 + * @see Pitch + */ + private Pitch createPitch(String input) { + List pitches = Arrays.stream(input.split("")) + .map(Integer::parseInt) + .toList(); + return new Pitch(pitches); + } + + /** + * Pitch 에 대해 본 게임의 Computer 와 비교하여 구종을 판별하는 메서드 + * @param pitch 사용자가 입력한 구종 + * @return 판정 결과 + * @see Judgement + */ + private Judgement judge(Pitch pitch) { + return new Judgement( + computer.countBall(pitch.getPitches()), + computer.countStrike(pitch.getPitches()) + ); + } + +} diff --git a/yummygyudon/code/java-baseball/src/main/java/baseball/domain/Computer.java b/yummygyudon/code/java-baseball/src/main/java/baseball/domain/Computer.java new file mode 100644 index 0000000..90ecdc6 --- /dev/null +++ b/yummygyudon/code/java-baseball/src/main/java/baseball/domain/Computer.java @@ -0,0 +1,68 @@ +package baseball.domain; + +import baseball.util.RandomNumberGenerator; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class Computer extends DefaultData { + + /** + * 한 게임에서 사용될 컴퓨터 역할의 객체 + * @see DefaultData + */ + public Computer() { + super(); + setNumberList(generateComputerNumber()); + } + + /** + * 진행 게임의 기준값이 될 서로 다른 숫자 3개의 리스트를 생성하여 반환합니다. + * @return RandomNumber List + * @see RandomNumberGenerator + */ + private List generateComputerNumber() { + List numberList = new ArrayList<>(); + while (numberList.size() < MAX_LENGTH) { + int randomNumber = RandomNumberGenerator.generate(); + if (!numberList.contains(randomNumber)) { + numberList.add(randomNumber); + } + } + return numberList; + } + + /** + * 사용자(Pitch)와 비교한 후, Strike 의 갯수를 연산하여 반환합니다. + * @param userNumber 사용자가 입력한 숫자 List + * @return Strike 갯수 + * @see baseball.application.GameLauncher#judge(Pitch) + */ + public int countStrike(List userNumber) { + int strikeCount = 0; + for (int index = 0; index < MAX_LENGTH; index++) { + if (Objects.equals(userNumber.get(index), numberList.get(index))) { + strikeCount += 1; + } + } + return strikeCount; + } + + /** + * 사용자(Pitch)와 비교한 후, Ball 의 갯수를 연산하여 반환합니다. + * @param userNumber - 사용자가 입력한 숫자 List + * @return Ball 갯수 + * @see baseball.application.GameLauncher#judge(Pitch) + */ + public int countBall(List userNumber) { + int ballCount = 0; + for (Integer number : userNumber) { + if (numberList.contains(number)) { + ballCount += 1; + } + } + return ballCount - countStrike(userNumber); + } + +} diff --git a/yummygyudon/code/java-baseball/src/main/java/baseball/domain/DefaultData.java b/yummygyudon/code/java-baseball/src/main/java/baseball/domain/DefaultData.java new file mode 100644 index 0000000..711a2bf --- /dev/null +++ b/yummygyudon/code/java-baseball/src/main/java/baseball/domain/DefaultData.java @@ -0,0 +1,13 @@ +package baseball.domain; + +import java.util.List; + +public abstract class DefaultData { + protected final int MAX_LENGTH = 3; + protected List numberList; + + public void setNumberList(List numberList) { + this.numberList = numberList; + } + +} diff --git a/yummygyudon/code/java-baseball/src/main/java/baseball/domain/Judgement.java b/yummygyudon/code/java-baseball/src/main/java/baseball/domain/Judgement.java new file mode 100644 index 0000000..f3cf9a7 --- /dev/null +++ b/yummygyudon/code/java-baseball/src/main/java/baseball/domain/Judgement.java @@ -0,0 +1,54 @@ +package baseball.domain; + +import baseball.application.GameLauncher; + +public class Judgement { + + private final int ball; + private final int strike; + + /** + * 각 Pitch 들의 Computer 와의 비교 판정 값을 저장하는 객체 + * @param ball Ball 판정 값 + * @param strike Strike 판정 값 + * @see GameLauncher#execute() + */ + public Judgement(int ball, int strike) { + this.ball = ball; + this.strike = strike; + } + + /** + * @return 본 Judgement 의 Ball 수 + * @see GameLauncher#judge(Pitch) + */ + public int getBall() { + return ball; + } + + /** + * @return 본 Judgement 의 Strike 수 + * @see GameLauncher#judge(Pitch) + */ + public int getStrike() { + return strike; + } + + /** + * + * @return 낫싱 여부 + * @see baseball.util.JudgementConverter + */ + public boolean isNothing() { + return ball == 0 && strike == 0; + } + + /** + * @return 3 스트라이크 여부 + * @see baseball.util.JudgementConverter + */ + public boolean isThreeStrike() { + return strike == 3; + } + +} diff --git a/yummygyudon/code/java-baseball/src/main/java/baseball/domain/Pitch.java b/yummygyudon/code/java-baseball/src/main/java/baseball/domain/Pitch.java new file mode 100644 index 0000000..caf9b8c --- /dev/null +++ b/yummygyudon/code/java-baseball/src/main/java/baseball/domain/Pitch.java @@ -0,0 +1,31 @@ +package baseball.domain; + +import java.util.List; + +public class Pitch extends DefaultData{ + /** + * 사용자 입력 값을 기반으로 사용자의 투구 정보를 담는 객체 + * @see DefaultData + * @throws IllegalArgumentException : MAX_LENGTH 가 아닌 리스트가 들어올 경우 + */ + public Pitch(List inputNumberList) { + super(); + if (!(inputNumberList.size() == MAX_LENGTH)) { + throw new IllegalArgumentException(); + } + validateNumbers(inputNumberList); + setNumberList(inputNumberList); + } + + private void validateNumbers(List numbers) { + long uniqueCount = numbers.stream() + .distinct() + .count(); + if (uniqueCount != MAX_LENGTH) { + throw new IllegalArgumentException(); + } + } + public List getPitches() { + return numberList; + } +} diff --git a/yummygyudon/code/java-baseball/src/main/java/baseball/util/JudgementConverter.java b/yummygyudon/code/java-baseball/src/main/java/baseball/util/JudgementConverter.java new file mode 100644 index 0000000..fbefb1d --- /dev/null +++ b/yummygyudon/code/java-baseball/src/main/java/baseball/util/JudgementConverter.java @@ -0,0 +1,45 @@ +package baseball.util; + +import baseball.application.GameLauncher; +import baseball.domain.Judgement; +import baseball.util.enums.PitchType; + +public class JudgementConverter { + + /** + * 판정 결과 값인 Judgement 값에 대해 결과 값으로 변환해줍니다. + * @param judgement 결과 변환 대상 Judgement + * @return Judgement to String 변환 값 + * @see Judgement + * @see GameLauncher#execute() + */ + public static String of(Judgement judgement) { + if (judgement.isNothing()) { + return PitchType.NOTHING.getType(); + } + if (judgement.isThreeStrike()) { + return judgement.getStrike() + PitchType.STRIKE.getType(); + } + return makeJudgementResult(judgement.getBall(), judgement.getStrike()); + } + + /** + * "낫싱" 혹은 "3 스트라이크"가 아닌 경우에 대한 별도의 결과 변환 메서드입니다. + * @param ballCount 변환 대상 Judgement 의 Ball 수 + * @param strikeCount 변환 대상 Judgement 의 Strike 수 + * @return Judgement to String 변환 값 + * @see #of(Judgement) + * @see PitchType + */ + private static String makeJudgementResult(int ballCount, int strikeCount) { + String judgementResult = ""; + if (ballCount > 0) { + judgementResult += ballCount + PitchType.BALL.getType(); + } + if (strikeCount > 0) { + judgementResult += strikeCount + PitchType.STRIKE.getType(); + } + return judgementResult.trim(); + } + +} diff --git a/yummygyudon/code/java-baseball/src/main/java/baseball/util/RandomNumberGenerator.java b/yummygyudon/code/java-baseball/src/main/java/baseball/util/RandomNumberGenerator.java new file mode 100644 index 0000000..145ea0e --- /dev/null +++ b/yummygyudon/code/java-baseball/src/main/java/baseball/util/RandomNumberGenerator.java @@ -0,0 +1,12 @@ +package baseball.util; + +import camp.nextstep.edu.missionutils.Randoms; +public class RandomNumberGenerator { + + private static final int MIN_LIMIT = 1; + private static final int MAX_LIMIT = 9; + + public static int generate() { + return Randoms.pickNumberInRange(MIN_LIMIT, MAX_LIMIT); + } +} diff --git a/yummygyudon/code/java-baseball/src/main/java/baseball/util/channel/GameReader.java b/yummygyudon/code/java-baseball/src/main/java/baseball/util/channel/GameReader.java new file mode 100644 index 0000000..df134ac --- /dev/null +++ b/yummygyudon/code/java-baseball/src/main/java/baseball/util/channel/GameReader.java @@ -0,0 +1,33 @@ +package baseball.util.channel; + + +import baseball.util.enums.Command; +import camp.nextstep.edu.missionutils.Console; + +public abstract class GameReader { + public static String read(Command command) { + Printer.print(command.getCommand()); + String input = Console.readLine().trim(); + validate(input); + + // 사용자의 Pitching 입력 값을 요청하는 명령인데 입력한 숫자가 3 자리가 아닐 경우 + if (command.equals(Command.PITCHING) && !(input.length() == 3)) { + throw new IllegalArgumentException(); + } + return input; + } + + /** + * @param input 사용자의 입력 값 + * @exception IllegalArgumentException 빈 값을 입력한 경우 & 슷지가 아닌 값을 입력한 경우 + */ + private static void validate(String input) { + if (input.isBlank()) { + throw new IllegalArgumentException(); + } + if (!input.matches("[1-9]+")) { + throw new IllegalArgumentException(); + } + } + +} diff --git a/yummygyudon/code/java-baseball/src/main/java/baseball/util/channel/Printer.java b/yummygyudon/code/java-baseball/src/main/java/baseball/util/channel/Printer.java new file mode 100644 index 0000000..769b2c3 --- /dev/null +++ b/yummygyudon/code/java-baseball/src/main/java/baseball/util/channel/Printer.java @@ -0,0 +1,10 @@ +package baseball.util.channel; + +public abstract class Printer { + public static void print(String message) { + System.out.print(message); + } + public static void println(String message) { + System.out.println(message); + } +} diff --git a/yummygyudon/code/java-baseball/src/main/java/baseball/util/enums/Command.java b/yummygyudon/code/java-baseball/src/main/java/baseball/util/enums/Command.java new file mode 100644 index 0000000..4e5db6c --- /dev/null +++ b/yummygyudon/code/java-baseball/src/main/java/baseball/util/enums/Command.java @@ -0,0 +1,18 @@ +package baseball.util.enums; + +public enum Command { + START_GAME("숫자 야구 게임을 시작합니다.\n"), + PITCHING("숫자를 입력해주세요 : "), + END_GAME("3개의 숫자를 모두 맞히셨습니다! 게임 종료\n"), + ASK_RESUME("게임을 새로 시작하려면 1, 종료하려면 2를 입력하세요.\n") + ; + private final String command; + + Command(String command) { + this.command = command; + } + + public String getCommand() { + return command; + } +} diff --git a/yummygyudon/code/java-baseball/src/main/java/baseball/util/enums/GameFlag.java b/yummygyudon/code/java-baseball/src/main/java/baseball/util/enums/GameFlag.java new file mode 100644 index 0000000..ae2d7b1 --- /dev/null +++ b/yummygyudon/code/java-baseball/src/main/java/baseball/util/enums/GameFlag.java @@ -0,0 +1,5 @@ +package baseball.util.enums; + +public abstract class GameFlag { + public static String RETRY = "1"; +} diff --git a/yummygyudon/code/java-baseball/src/main/java/baseball/util/enums/PitchType.java b/yummygyudon/code/java-baseball/src/main/java/baseball/util/enums/PitchType.java new file mode 100644 index 0000000..80f24ea --- /dev/null +++ b/yummygyudon/code/java-baseball/src/main/java/baseball/util/enums/PitchType.java @@ -0,0 +1,15 @@ +package baseball.util.enums; + +public enum PitchType { + NOTHING("낫싱"), STRIKE("스트라이크"), BALL("볼 ") + ; + private final String type; + + PitchType(String value) { + this.type = value; + } + + public String getType() { + return type; + } +} diff --git a/yummygyudon/code/java-baseball/src/test/java/baseball/ApplicationTest.java b/yummygyudon/code/java-baseball/src/test/java/baseball/ApplicationTest.java new file mode 100644 index 0000000..3fa29fa --- /dev/null +++ b/yummygyudon/code/java-baseball/src/test/java/baseball/ApplicationTest.java @@ -0,0 +1,35 @@ +package baseball; + +import camp.nextstep.edu.missionutils.test.NsTest; +import org.junit.jupiter.api.Test; + +import static camp.nextstep.edu.missionutils.test.Assertions.assertRandomNumberInRangeTest; +import static camp.nextstep.edu.missionutils.test.Assertions.assertSimpleTest; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ApplicationTest extends NsTest { + @Test + void 게임종료_후_재시작() { + assertRandomNumberInRangeTest( + () -> { + run("246", "135", "1", "597", "589", "2"); + assertThat(output()).contains("낫싱", "3스트라이크", "1볼 1스트라이크", "3스트라이크", "게임 종료"); + }, + 1, 3, 5, 5, 8, 9 + ); + } + + @Test + void 예외_테스트() { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException("1234")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @Override + public void runMain() { + Application.main(new String[]{}); + } +} diff --git a/yummygyudon/code/java-baseball/src/test/java/baseball/domain/ComputerTest.java b/yummygyudon/code/java-baseball/src/test/java/baseball/domain/ComputerTest.java new file mode 100644 index 0000000..2041626 --- /dev/null +++ b/yummygyudon/code/java-baseball/src/test/java/baseball/domain/ComputerTest.java @@ -0,0 +1,37 @@ +package baseball.domain; + +import org.junit.jupiter.api.*; + +import java.util.ArrayList; +import java.util.List; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +@DisplayName("Computer 도메인 테스트") +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class ComputerTest { + + @Test + @Order(0) + @DisplayName("랜덤 숫자 생성기 정상 작동 여부") + public void Case_1_No_Duplicate_Random_Number() { + // when + List targetComputers = new ArrayList<>(); + + // given + for (int i = 0; i < 100; i++) { + targetComputers.add(new Computer()); + } + + // then + for (Computer computer : targetComputers) { + assertTrue(() -> { + long uniqueCount = computer.numberList.stream() + .distinct() + .count(); + return uniqueCount == 3; + }); + } + } + +} diff --git a/yummygyudon/code/java-baseball/src/test/java/baseball/domain/JudgementTest.java b/yummygyudon/code/java-baseball/src/test/java/baseball/domain/JudgementTest.java new file mode 100644 index 0000000..6cb68f2 --- /dev/null +++ b/yummygyudon/code/java-baseball/src/test/java/baseball/domain/JudgementTest.java @@ -0,0 +1,100 @@ +package baseball.domain; + +import baseball.util.JudgementConverter; +import baseball.util.RandomNumberGenerator; +import org.junit.jupiter.api.*; + +import java.util.ArrayList; +import java.util.List; + +import static camp.nextstep.edu.missionutils.test.Assertions.assertSimpleTest; +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("판정(Judgement) 도메인 테스트") +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class JudgementTest { + + @Test + @Order(0) + @DisplayName("올바른 판정 판정 여부") + public void Case_1_Correct_Judgement() { + // when + Computer computer = new Computer(); + computer.setNumberList(List.of(1, 7, 8)); + + // given + List pitches = List.of( + new Pitch(List.of(2, 5, 6)), + new Pitch(List.of(2, 1, 6)), + new Pitch(List.of(2, 1, 7)), + new Pitch(List.of(8, 1, 7)), + new Pitch(List.of(1, 5, 6)), + new Pitch(List.of(1, 7, 6)), + new Pitch(List.of(1, 7, 8)), + new Pitch(List.of(1, 4, 7)), + new Pitch(List.of(8, 7, 1)) + ); + + // then + for (Pitch pitch : pitches) { + assertSimpleTest( + () -> { + Judgement judgement = new Judgement( + computer.countBall(pitch.getPitches()), + computer.countStrike(pitch.getPitches()) + ); + assertThat(JudgementConverter.of(judgement)).isIn("낫싱", "1볼", "2볼", "3볼", "1스트라이크", "2스트라이크", "1볼 1스트라이크", "2볼 1스트라이크", "3스트라이크"); + } + ); + } + } + @Test + @Order(1) + @DisplayName("볼/스트라이크 판정 한계값 이탈 여부") + public void Case_2_Not_Over_Valid() { + Computer computer = new Computer(); + + // given + for (int i = 0; i < 100; i++) { + List numberList = new ArrayList<>(); + while (numberList.size() < 3) { + int randomNumber = RandomNumberGenerator.generate(); + if (!numberList.contains(randomNumber)) { + numberList.add(randomNumber); + } + } + Pitch pitch = new Pitch(numberList); + assertThat(computer.countStrike(pitch.getPitches())).isGreaterThanOrEqualTo(0); + assertThat(computer.countStrike(pitch.getPitches())).isLessThanOrEqualTo(3); + assertThat(computer.countBall(pitch.getPitches())).isGreaterThanOrEqualTo(0); + assertThat(computer.countBall(pitch.getPitches())).isLessThanOrEqualTo(3); + + } + } + + @Test + @Order(2) + @DisplayName("Judgement 반환값 정확도 여부") + public void Case_3_Judgement_Result_Covert() { + // when + Computer computer = new Computer(); + + // given & then + for (int i = 0; i < 100; i++) { + List numberList = new ArrayList<>(); + while (numberList.size() < 3) { + int randomNumber = RandomNumberGenerator.generate(); + if (!numberList.contains(randomNumber)) { + numberList.add(randomNumber); + } + } + Pitch pitch = new Pitch(numberList); + Judgement judgement = new Judgement( + computer.countStrike(pitch.getPitches()), + computer.countBall(pitch.getPitches()) + ); + assertThat(JudgementConverter.of(judgement)).isNotIn("4볼", "-1볼", "0볼 0스트라이크", "0볼", "0스트라이크", "4스트라이크", "1볼 2스트라이크", "2볼 2스트라이크"); + } + } + +} diff --git a/yummygyudon/code/java-baseball/src/test/java/baseball/domain/PitchTest.java b/yummygyudon/code/java-baseball/src/test/java/baseball/domain/PitchTest.java new file mode 100644 index 0000000..465e5b2 --- /dev/null +++ b/yummygyudon/code/java-baseball/src/test/java/baseball/domain/PitchTest.java @@ -0,0 +1,126 @@ +package baseball.domain; + +import baseball.Application; +import camp.nextstep.edu.missionutils.test.NsTest; +import org.junit.jupiter.api.*; + +import java.util.List; + +import static camp.nextstep.edu.missionutils.test.Assertions.assertSimpleTest; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayName("Pitch 도메인 테스트") +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +public class PitchTest extends NsTest { + + @DisplayName("빈 값을 입력한 경우") + @Test + @Order(0) + public void Case_1_Empty_Input() { + // when & given + List inputs = List.of( + "\n", " ", " "," ", "1 2 3" + ); + + // then + for (String input : inputs) { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException(input)) + .isInstanceOf(IllegalArgumentException.class) + ); + } + } + + @DisplayName("3자리를 초과해서 입력한 경우") + @Test + @Order(1) + public void Case_2_Over_Length_Input() { + List inputs = List.of( + "12345", "3295834859", "1234" + ); + + // then + for (String input : inputs) { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException(input)) + .isInstanceOf(IllegalArgumentException.class) + ); + } + } + + @DisplayName("3자리 미만해서 입력한 경우") + @Test + @Order(2) + public void Case_3_Less_Length_Input() { + List inputs = List.of( + "1", "34" + ); + + // then + for (String input : inputs) { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException(input)) + .isInstanceOf(IllegalArgumentException.class) + ); + } + } + + @DisplayName("0을 입력한 경우") + @Test + @Order(3) + public void Case_4_Zero_Input() { + // when & given + List inputs = List.of( + "109", "048", "000","012", "106" + ); + + // then + for (String input : inputs) { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException(input)) + .isInstanceOf(IllegalArgumentException.class) + ); + } + } + + @DisplayName("중복된 숫자를 입력한 경우") + @Test + @Order(4) + public void Case_5_Duplicate_Input() { + List inputs = List.of( + "111", "121", "323" + ); + + // then + for (String input : inputs) { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException(input)) + .isInstanceOf(IllegalArgumentException.class) + ); + } + } + + @DisplayName("영어, 한글, 특수 문자를 입력한 경우") + @Test + @Order(5) + public void Case_6_Not_Number_Input() { + List inputs = List.of( + "하이요", "Hii", "Who","인in", "*&*" + ); + + // then + for (String input : inputs) { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException(input)) + .isInstanceOf(IllegalArgumentException.class) + ); + } + } + + @Override + protected void runMain() { + Application.main(new String[]{}); + } + + +} diff --git a/yummygyudon/code/java-christmas/.gitignore b/yummygyudon/code/java-christmas/.gitignore new file mode 100644 index 0000000..5dca701 --- /dev/null +++ b/yummygyudon/code/java-christmas/.gitignore @@ -0,0 +1,35 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/** +!**/src/test/** + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ + +### Mac OS ### +.DS_Store diff --git a/yummygyudon/code/java-christmas/build.gradle b/yummygyudon/code/java-christmas/build.gradle new file mode 100644 index 0000000..b11abb5 --- /dev/null +++ b/yummygyudon/code/java-christmas/build.gradle @@ -0,0 +1,22 @@ +plugins { + id 'java' +} + +repositories { + mavenCentral() + maven { url 'https://jitpack.io' } +} + +dependencies { + implementation 'com.github.woowacourse-projects:mission-utils:1.1.0' +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +test { + useJUnitPlatform() +} diff --git a/yummygyudon/code/java-christmas/gradle/wrapper/gradle-wrapper.jar b/yummygyudon/code/java-christmas/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/yummygyudon/code/java-christmas/gradle/wrapper/gradle-wrapper.jar differ diff --git a/yummygyudon/code/java-christmas/gradle/wrapper/gradle-wrapper.properties b/yummygyudon/code/java-christmas/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ae04661 --- /dev/null +++ b/yummygyudon/code/java-christmas/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/yummygyudon/code/java-christmas/gradlew b/yummygyudon/code/java-christmas/gradlew new file mode 100755 index 0000000..a69d9cb --- /dev/null +++ b/yummygyudon/code/java-christmas/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/yummygyudon/code/java-christmas/gradlew.bat b/yummygyudon/code/java-christmas/gradlew.bat new file mode 100644 index 0000000..f127cfd --- /dev/null +++ b/yummygyudon/code/java-christmas/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/yummygyudon/code/java-christmas/settings.gradle b/yummygyudon/code/java-christmas/settings.gradle new file mode 100644 index 0000000..e061963 --- /dev/null +++ b/yummygyudon/code/java-christmas/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'java-christmas' + diff --git a/yummygyudon/code/java-christmas/src/main/java/christmas/Application.java b/yummygyudon/code/java-christmas/src/main/java/christmas/Application.java new file mode 100644 index 0000000..bd06519 --- /dev/null +++ b/yummygyudon/code/java-christmas/src/main/java/christmas/Application.java @@ -0,0 +1,11 @@ +package christmas; + +import christmas.presentation.ChristmasPlanner; + +public class Application { + public static void main(String[] args) { + // TODO: 프로그램 구현 + ChristmasPlanner christmasPlanner = new ChristmasPlanner(); + christmasPlanner.run(); + } +} diff --git a/yummygyudon/code/java-christmas/src/main/java/christmas/application/ChristmasPromotion.java b/yummygyudon/code/java-christmas/src/main/java/christmas/application/ChristmasPromotion.java new file mode 100644 index 0000000..30e336e --- /dev/null +++ b/yummygyudon/code/java-christmas/src/main/java/christmas/application/ChristmasPromotion.java @@ -0,0 +1,170 @@ +package christmas.application; + +import christmas.application.info.EventResultInfo; +import christmas.domain.enums.EventDiscountType; +import christmas.domain.enums.MenuType; +import christmas.global.constant.Standard; +import christmas.presentation.view.InputView; + +import java.util.List; + +public class ChristmasPromotion { + + private static final String GIVEAWAY_EVENT_NAME = "증정 이벤트"; + + private final int planningDate; + private final GiftService giftService; + private final DiscountService discountService; + private final OrderService orderService; + + public ChristmasPromotion(int date) { + this.planningDate = date; + this.giftService = new GiftService(); + this.discountService = new DiscountService(); + this.orderService = new OrderService(); + } + + /** 주문합니다.*/ + public void order(List menuInputs) { + orderService.registerOrders(menuInputs); + } + + /** 플래너의 정보를 반환합니다.*/ + public EventResultInfo.DateInfo viewPlannerInfo() { + return new EventResultInfo.DateInfo(Standard.MONTH_OF_DECEMBER, planningDate); + } + + /** 주문 메뉴를 반환합니다. */ + public List viewOrderedMenusHistory() { + return orderService.getOrdersHistory(); + } + + + /** 할인 전 총주문 금액을 반환합니다. */ + public EventResultInfo.Amount viewTotalAmountPaidBeforeDiscount() { + return new EventResultInfo.Amount(orderService.getTotalAmount()); + } + + /** 증정 메뉴를 반환합니다. */ + public EventResultInfo.ReceiveMenu viewGiveawayMenu() { + if (!giftService.isGiveaway(orderService.getTotalAmount())) { + return null; + } + String giveawayMenuName = giftService.getGiveawayMenuName(); + int providedQuantity = giftService.getGiveawayMenuQuantity(); + return new EventResultInfo.ReceiveMenu(giveawayMenuName, providedQuantity); + } + + /** 크리스마스 디데이 혜택 내역을 반환합니다. */ + public EventResultInfo.Benefit viewChristmasPeriodEventBenefit() { + if (orderService.getTotalAmount() < Standard.MINIMUM_AMOUNT_FOR_ALL_EVENT) { + return null; + } + if (!discountService.isInChristmasDiscountEventPeriod(planningDate)) { + return null; + } + int totalChristmasEventDiscountAmount = discountService.getTotalChristmasEventDiscountAmount(planningDate); + return makeBenefitInfo(EventDiscountType.CHRISTMAS.getName(), totalChristmasEventDiscountAmount); + } + + /** 주일/주말 혜택 내역을 반환합니다. */ + public EventResultInfo.Benefit viewWeekEventBenefit() { + if (orderService.getTotalAmount() < Standard.MINIMUM_AMOUNT_FOR_ALL_EVENT) { + return null; + } + return getWeekEventBenefit(); + } + + private EventResultInfo.Benefit getWeekEventBenefit() { + EventResultInfo.Benefit result = null; + if (discountService.isInWeekdays(planningDate) && orderService.isExistAnyMenuTypeInOrders(MenuType.DESSERT)) { + int dessertMenuCount = orderService.getOrdersCountByMenuType(MenuType.DESSERT); + int totalWeekdayDiscountAmount = discountService.getTotalWeekDiscountAmount(EventDiscountType.WEEKDAY, dessertMenuCount); + result = makeBenefitInfo(EventDiscountType.WEEKDAY.getName(), totalWeekdayDiscountAmount); + } + if (discountService.isInWeekends(planningDate) && orderService.isExistAnyMenuTypeInOrders(MenuType.MAIN)) { + int mainMenuCount = orderService.getOrdersCountByMenuType(MenuType.MAIN); + int totalWeekendDiscountAmount = discountService.getTotalWeekDiscountAmount(EventDiscountType.WEEKEND, mainMenuCount); + result = makeBenefitInfo(EventDiscountType.WEEKEND.getName(), totalWeekendDiscountAmount); + } + return result; + } + + /** 특별 혜택 내역을 반환합니다. */ + public EventResultInfo.Benefit viewSpecialDiscountEventBenefit() { + if (orderService.getTotalAmount() < Standard.MINIMUM_AMOUNT_FOR_ALL_EVENT) { + return null; + } + if (!discountService.isInSpecialDiscountEventDays(planningDate)) { + return null; + } + int totalSpecialDiscountAmount = discountService.getTotalSpecialDiscountAmount(); + return makeBenefitInfo(EventDiscountType.SPECIAL.getName(), totalSpecialDiscountAmount); + } + + /** 증정 혜택 내역을 반환합니다. */ + public EventResultInfo.Benefit viewGiveawayBenefit() { + if (!giftService.isGiveaway(orderService.getTotalAmount())) { + return null; + } + int giveawayMenuPrice = giftService.getGiveawayMenuPrice(); + return makeBenefitInfo(GIVEAWAY_EVENT_NAME, giveawayMenuPrice); + } + + // Benefit DTO로 만들어주는 생성 메서드 + private EventResultInfo.Benefit makeBenefitInfo(String benefitName, int benefitAmount) { + return new EventResultInfo.Benefit(benefitName, benefitAmount); + } + + + /** 총 혜택 금액을 반환합니다. */ + public EventResultInfo.Amount viewTotalBenefitAmount() { + if (orderService.getTotalAmount() < Standard.MINIMUM_AMOUNT_FOR_ALL_EVENT) { + return null; + } + int totalBenefitAmount = combineAllDiscountAmount(); + if (giftService.isGiveaway(orderService.getTotalAmount())) { + totalBenefitAmount += giftService.getGiveawayMenuPrice(); + } + return new EventResultInfo.Amount(totalBenefitAmount); + } + + + /** 할인 후 예상 결제 금액을 반환합니다. */ + public EventResultInfo.Amount viewTotalAmountPaidAfterDiscount() { + if (orderService.getTotalAmount() < Standard.MINIMUM_AMOUNT_FOR_ALL_EVENT) { + return new EventResultInfo.Amount(orderService.getTotalAmount()); + } + return new EventResultInfo.Amount(orderService.getTotalAmount() - combineAllDiscountAmount()); + } + + // 전체 혜택 금액을 연산합니다. + private int combineAllDiscountAmount() { + int totalDiscountAmount = 0; + if (discountService.isInChristmasDiscountEventPeriod(planningDate)) { + totalDiscountAmount += discountService.getTotalChristmasEventDiscountAmount(planningDate); + } + if (discountService.isInWeekdays(planningDate)) { + int dessertMenuCount = orderService.getOrdersCountByMenuType(MenuType.DESSERT); + totalDiscountAmount += discountService.getTotalWeekDiscountAmount(EventDiscountType.WEEKDAY, dessertMenuCount); + } + if (discountService.isInWeekends(planningDate)) { + int mainMenuCount = orderService.getOrdersCountByMenuType(MenuType.MAIN); + totalDiscountAmount += discountService.getTotalWeekDiscountAmount(EventDiscountType.WEEKEND, mainMenuCount); + } + if (discountService.isInSpecialDiscountEventDays(planningDate)) { + totalDiscountAmount += discountService.getTotalSpecialDiscountAmount(); + } + return totalDiscountAmount; + } + + /** 이벤트 뱃지를 반환합니다. */ + public EventResultInfo.Badge viewBadge() { + int totalDiscountAmount = combineAllDiscountAmount(); + if (!giftService.isBadge(totalDiscountAmount)) { + return null; + } + String badgeName = giftService.receiveBadgeName(totalDiscountAmount); + return new EventResultInfo.Badge(badgeName); + } +} diff --git a/yummygyudon/code/java-christmas/src/main/java/christmas/application/DiscountService.java b/yummygyudon/code/java-christmas/src/main/java/christmas/application/DiscountService.java new file mode 100644 index 0000000..7578efd --- /dev/null +++ b/yummygyudon/code/java-christmas/src/main/java/christmas/application/DiscountService.java @@ -0,0 +1,43 @@ +package christmas.application; + +import christmas.domain.enums.EventDiscountType; +import christmas.global.constant.Standard; + + +class DiscountService { + + /** 크리스마스 D-Day 할인 관련 기능 */ + boolean isInChristmasDiscountEventPeriod(int date) { + return date <= Standard.DATE_OF_CHRISTMAS; + } + + int getTotalChristmasEventDiscountAmount(int date) { + int dateDifference = date - Standard.FIRST_DATE_OF_DECEMBER; + int increaseDiscount = Standard.D_DAY_DISCOUNT_INCREASE_AMOUNT * dateDifference; + return EventDiscountType.CHRISTMAS.getDiscountAmount() + increaseDiscount; + } + + /** 주중-주말 할인 관련 기능 */ + boolean isInWeekdays(int date) { + return Standard.WEEKDAY_OF_DECEMBER.contains(date); + } + + boolean isInWeekends(int date) { + return Standard.WEEKEND_OF_DECEMBER.contains(date); + } + + int getTotalWeekDiscountAmount(EventDiscountType discountType, int targetMenuCount) { + return discountType.getDiscountAmount() * targetMenuCount; + } + + + /** 특별 할인 관련 기능 */ + boolean isInSpecialDiscountEventDays(int date) { + return Standard.SPECIAL_DATE_OF_DECEMBER.contains(date); + } + + int getTotalSpecialDiscountAmount() { + return EventDiscountType.SPECIAL.getDiscountAmount(); + } + +} diff --git a/yummygyudon/code/java-christmas/src/main/java/christmas/application/GiftService.java b/yummygyudon/code/java-christmas/src/main/java/christmas/application/GiftService.java new file mode 100644 index 0000000..8a4420a --- /dev/null +++ b/yummygyudon/code/java-christmas/src/main/java/christmas/application/GiftService.java @@ -0,0 +1,52 @@ +package christmas.application; + +import christmas.domain.entity.Menu; +import christmas.domain.enums.Badge; +import christmas.domain.repository.MenuRepository; +import christmas.global.constant.Standard; + + +class GiftService { + private static final int GIVEAWAY_PROVIDE_QUANTITY = 1; + + GiftService() { + } + + /** 증정 Menu(Giveaway) 관련 기능 */ + boolean isGiveaway(int totalAmount) { + return totalAmount >= Standard.MINIMUM_AMOUNT_FOR_GIVEAWAY_EVENT; + } + + String getGiveawayMenuName() { + Menu giveawayMenu = MenuRepository.findMenuByName(Standard.GIVEAWAY_MENU_NAME); + return giveawayMenu.getName(); + } + + int getGiveawayMenuPrice() { + Menu giveawayMenu = MenuRepository.findMenuByName(Standard.GIVEAWAY_MENU_NAME); + return giveawayMenu.getPrice(); + } + + int getGiveawayMenuQuantity() { + return GIVEAWAY_PROVIDE_QUANTITY; + } + + + /** 증정 Badge 관련 기능 */ + boolean isBadge(int discountAmount) { + return discountAmount >= Standard.MINIMUM_AMOUNT_FOR_BADGE_EVENT; + } + + String receiveBadgeName(int discountAmount) { + if (discountAmount >= Badge.SANTA.getPriceCriteria()) { + return Badge.SANTA.getName(); + } + if (discountAmount >= Badge.TREE.getPriceCriteria()) { + return Badge.TREE.getName(); + } + if (discountAmount >= Badge.STAR.getPriceCriteria()) { + return Badge.STAR.getName(); + } + return Badge.NONE.getName(); + } +} diff --git a/yummygyudon/code/java-christmas/src/main/java/christmas/application/OrderService.java b/yummygyudon/code/java-christmas/src/main/java/christmas/application/OrderService.java new file mode 100644 index 0000000..22f95a3 --- /dev/null +++ b/yummygyudon/code/java-christmas/src/main/java/christmas/application/OrderService.java @@ -0,0 +1,68 @@ +package christmas.application; + +import christmas.application.info.EventResultInfo; +import christmas.domain.entity.Menu; +import christmas.domain.entity.Order; +import christmas.domain.enums.MenuType; +import christmas.domain.repository.MenuRepository; +import christmas.domain.repository.OrderRepository; +import christmas.presentation.view.InputView; + +import java.util.List; +import java.util.stream.Collectors; + +class OrderService { + private final OrderRepository orderRepository; + + OrderService() { + this.orderRepository = new OrderRepository(); + } + + /** Order 등록 */ + void registerOrders(List menuInputs) { + List orders = menuInputs.stream() + .map(menuInput -> { + Menu menu = MenuRepository.findMenuByName(menuInput.name()); + return new Order(menuInput.quantity(), menu); + }) + .toList(); + orderRepository.insert(orders); + } + + + + /** Order 연산 기능 */ + // 해당 MenuType 에 해당하는 메뉴들이 존재하는지 확인 + boolean isExistAnyMenuTypeInOrders(MenuType menuType) { + return orderRepository.findAll().stream() + .anyMatch(order -> order.getMenu().getType().equals(menuType)); + } + + // 전체 금액 연산 + int getTotalAmount() { + int totalAmount = 0; + for (Order eachOrder : orderRepository.findAll()) { + totalAmount += eachOrder.calculateTotalPrice(); + } + return totalAmount; + } + + // 해당 MenuType 에 해당하는 메뉴들을 몇 개나 시켰는지 연산 + int getOrdersCountByMenuType(MenuType menuType) { + long totalOrderCount = orderRepository.findAll().stream() + .filter(order -> order.getMenu().getType().equals(menuType)) + .mapToInt(Order::getQuantity) + .sum(); + return (int) totalOrderCount; + } + + // 주문 정보 반환 (직접적인 Menu 객체를 노출시키지 않고 DTO로 반환) + List getOrdersHistory() { + return orderRepository.findAll().stream() + .map(order -> new EventResultInfo.ReceiveMenu( + order.getMenu().getName(), order.getQuantity() + )) + .collect(Collectors.toList()); + } + +} diff --git a/yummygyudon/code/java-christmas/src/main/java/christmas/application/info/EventResultInfo.java b/yummygyudon/code/java-christmas/src/main/java/christmas/application/info/EventResultInfo.java new file mode 100644 index 0000000..6379b76 --- /dev/null +++ b/yummygyudon/code/java-christmas/src/main/java/christmas/application/info/EventResultInfo.java @@ -0,0 +1,39 @@ +package christmas.application.info; + + +public abstract class EventResultInfo { + + public record DateInfo( + int month, + int date + ) { + } + + + public record ReceiveMenu( + String name, + Integer quantity + ) { + } + + public record Amount( + int amount + ) { + } + + public record Benefit( + + String name, + Integer discountAmount + ) { + } + + + public record Badge( + String badgeName + ) { + } + +} + + diff --git a/yummygyudon/code/java-christmas/src/main/java/christmas/domain/entity/Menu.java b/yummygyudon/code/java-christmas/src/main/java/christmas/domain/entity/Menu.java new file mode 100644 index 0000000..957fbd3 --- /dev/null +++ b/yummygyudon/code/java-christmas/src/main/java/christmas/domain/entity/Menu.java @@ -0,0 +1,29 @@ +package christmas.domain.entity; + +import christmas.domain.enums.MenuType; + +public class Menu { + + private final String name; + private final int price; + private final MenuType type; + + public Menu(String name, int price, MenuType type) { + this.name = name; + this.price = price; + this.type = type; + } + + + public String getName() { + return name; + } + + public int getPrice() { + return price; + } + + public MenuType getType() { + return type; + } +} diff --git a/yummygyudon/code/java-christmas/src/main/java/christmas/domain/entity/Order.java b/yummygyudon/code/java-christmas/src/main/java/christmas/domain/entity/Order.java new file mode 100644 index 0000000..6924c3f --- /dev/null +++ b/yummygyudon/code/java-christmas/src/main/java/christmas/domain/entity/Order.java @@ -0,0 +1,41 @@ +package christmas.domain.entity; + +import christmas.domain.exception.OrderException; +import christmas.domain.exception.error.OrderError; +import christmas.global.constant.Standard; + +public class Order { + + private final int quantity; + private final Menu menu; + + public Order(int quantity, Menu menu) { + validateQuantity(quantity); + this.quantity = quantity; + this.menu = menu; + } + + private void validateQuantity(int quantity) { + // 최대 갯수 검사 + if (quantity > Standard.MAXIMUM_QUANTITY_FOR_ORDER) { + throw new OrderException(OrderError.TOO_MANY_SINGLE_MENU_QUANTITY); + } + // 0개 검사 + if (quantity < Standard.MINIMUM_QUANTITY_FOR_ORDER) { + throw new OrderException(OrderError.TOO_LITTLE_SINGLE_MENU_QUANTITY); + } + } + + public int getQuantity() { + return quantity; + } + + public Menu getMenu() { + return menu; + } + + public int calculateTotalPrice() { + return quantity * menu.getPrice(); + } + +} diff --git a/yummygyudon/code/java-christmas/src/main/java/christmas/domain/enums/Badge.java b/yummygyudon/code/java-christmas/src/main/java/christmas/domain/enums/Badge.java new file mode 100644 index 0000000..f6c4b28 --- /dev/null +++ b/yummygyudon/code/java-christmas/src/main/java/christmas/domain/enums/Badge.java @@ -0,0 +1,24 @@ +package christmas.domain.enums; + +public enum Badge { + NONE(null, 0), + STAR("별", 5_000), + TREE("트리", 10_000), + SANTA("산타", 20_000); + + private final String name; + private final int priceCriteria; + + Badge(String name, int priceCriteria) { + this.name = name; + this.priceCriteria = priceCriteria; + } + + public String getName() { + return name; + } + + public int getPriceCriteria() { + return priceCriteria; + } +} diff --git a/yummygyudon/code/java-christmas/src/main/java/christmas/domain/enums/EventDiscountType.java b/yummygyudon/code/java-christmas/src/main/java/christmas/domain/enums/EventDiscountType.java new file mode 100644 index 0000000..d09d2eb --- /dev/null +++ b/yummygyudon/code/java-christmas/src/main/java/christmas/domain/enums/EventDiscountType.java @@ -0,0 +1,26 @@ +package christmas.domain.enums; + +public enum EventDiscountType { + WEEKDAY("평일 할인", 2_023), + WEEKEND("주말 할인", 2_023), + SPECIAL("특별 할인", 1_000), + CHRISTMAS("크리스마스 디데이 할인", 1_000), + ; + + private final String name; + private final int discountAmount; + + + EventDiscountType(String name, int discountAmount) { + this.name = name; + this.discountAmount = discountAmount; + } + + public String getName() { + return name; + } + + public int getDiscountAmount() { + return discountAmount; + } +} diff --git a/yummygyudon/code/java-christmas/src/main/java/christmas/domain/enums/MenuType.java b/yummygyudon/code/java-christmas/src/main/java/christmas/domain/enums/MenuType.java new file mode 100644 index 0000000..227d837 --- /dev/null +++ b/yummygyudon/code/java-christmas/src/main/java/christmas/domain/enums/MenuType.java @@ -0,0 +1,5 @@ +package christmas.domain.enums; + +public enum MenuType { + APPETIZER, MAIN, DESSERT, BEVERAGE +} diff --git a/yummygyudon/code/java-christmas/src/main/java/christmas/domain/exception/MenuException.java b/yummygyudon/code/java-christmas/src/main/java/christmas/domain/exception/MenuException.java new file mode 100644 index 0000000..7bdcf74 --- /dev/null +++ b/yummygyudon/code/java-christmas/src/main/java/christmas/domain/exception/MenuException.java @@ -0,0 +1,13 @@ +package christmas.domain.exception; + +import christmas.domain.exception.error.MenuError; +import christmas.global.exception.base.ChristmasPlannerException; + +/** + * Menu 도메인 관련 에러만 발생시키는 예외 + */ +public class MenuException extends ChristmasPlannerException { + public MenuException(MenuError error) { + super(error); + } +} diff --git a/yummygyudon/code/java-christmas/src/main/java/christmas/domain/exception/OrderException.java b/yummygyudon/code/java-christmas/src/main/java/christmas/domain/exception/OrderException.java new file mode 100644 index 0000000..9846927 --- /dev/null +++ b/yummygyudon/code/java-christmas/src/main/java/christmas/domain/exception/OrderException.java @@ -0,0 +1,13 @@ +package christmas.domain.exception; + +import christmas.domain.exception.error.OrderError; +import christmas.global.exception.base.ChristmasPlannerException; + +/** + * Order 도메인 관련 에러만 발생시키는 예외 + */ +public class OrderException extends ChristmasPlannerException { + public OrderException(OrderError error) { + super(error); + } +} diff --git a/yummygyudon/code/java-christmas/src/main/java/christmas/domain/exception/error/MenuError.java b/yummygyudon/code/java-christmas/src/main/java/christmas/domain/exception/error/MenuError.java new file mode 100644 index 0000000..bab967c --- /dev/null +++ b/yummygyudon/code/java-christmas/src/main/java/christmas/domain/exception/error/MenuError.java @@ -0,0 +1,21 @@ +package christmas.domain.exception.error; + +import christmas.global.exception.base.ChristmasPlannerError; + +public enum MenuError implements ChristmasPlannerError { + + // 탐색하는 이름의 메뉴가 존재하지 않을 경우 + MENU_NOT_FOUND("유효하지 않은 주문입니다. 다시 입력해 주세요."), + ; + + private final String errorMessage; + + MenuError(String errorMessage) { + this.errorMessage = errorMessage; + } + + @Override + public String getErrorMessage() { + return errorMessage; + } +} diff --git a/yummygyudon/code/java-christmas/src/main/java/christmas/domain/exception/error/OrderError.java b/yummygyudon/code/java-christmas/src/main/java/christmas/domain/exception/error/OrderError.java new file mode 100644 index 0000000..8ea827c --- /dev/null +++ b/yummygyudon/code/java-christmas/src/main/java/christmas/domain/exception/error/OrderError.java @@ -0,0 +1,34 @@ +package christmas.domain.exception.error; + +import christmas.global.exception.base.ChristmasPlannerError; + +public enum OrderError implements ChristmasPlannerError { + + // 단일 메뉴 주문 갯수가 20개 초과일 경우 + TOO_MANY_SINGLE_MENU_QUANTITY("유효하지 않은 주문입니다. 다시 입력해 주세요."), + + // 단일 메뉴 주문 갯수가 1개 미만일 경우 + TOO_LITTLE_SINGLE_MENU_QUANTITY("유효하지 않은 주문입니다. 다시 입력해 주세요."), + + // 전체 메뉴 주문 갯수가 20개 초과할 경우 + TOO_MANY_MENU_QUANTITY("유효하지 않은 주문입니다. 다시 입력해 주세요."), + + // 음료 메뉴만 주문한 경우 + ONLY_BEVERAGE("유효하지 않은 주문입니다. 다시 입력해 주세요."), + + // 중복된 메뉴에 대한 주문이 존재할 경우 + DUPLICATED_ORDER_EXIST("유효하지 않은 주문입니다. 다시 입력해 주세요."), + ; + + private final String errorMessage; + + OrderError(String errorMessage) { + this.errorMessage = errorMessage; + } + + @Override + + public String getErrorMessage() { + return errorMessage; + } +} diff --git a/yummygyudon/code/java-christmas/src/main/java/christmas/domain/repository/MenuRepository.java b/yummygyudon/code/java-christmas/src/main/java/christmas/domain/repository/MenuRepository.java new file mode 100644 index 0000000..8d84713 --- /dev/null +++ b/yummygyudon/code/java-christmas/src/main/java/christmas/domain/repository/MenuRepository.java @@ -0,0 +1,54 @@ +package christmas.domain.repository; + +import christmas.domain.entity.Menu; +import christmas.domain.enums.MenuType; +import christmas.domain.exception.MenuException; +import christmas.domain.exception.error.MenuError; + +import java.util.HashMap; +import java.util.Map; + +/** + * 절댓값인 메뉴 데이터만 들어있는 클래스이기에 생성자를 막아놓습니다. + */ +public abstract class MenuRepository { + private static final Map menus; + + static { + menus = new HashMap<>(); + initAppetizer(); + initMain(); + initDessert(); + initBeverage(); + } + + // init + private static void initAppetizer() { + menus.put("양송이수프", new Menu("양송이수프", 6_000, MenuType.APPETIZER)); + menus.put("타파스", new Menu("타파스", 5_500, MenuType.APPETIZER)); + menus.put("시저샐러드", new Menu("시저샐러드", 8_000, MenuType.APPETIZER)); + } + private static void initMain() { + menus.put("티본스테이크", new Menu("티본스테이크", 55_000, MenuType.MAIN)); + menus.put("바비큐립", new Menu("바비큐립", 54_000, MenuType.MAIN)); + menus.put("해산물파스타", new Menu("해산물파스타", 35_000, MenuType.MAIN)); + menus.put("크리스마스파스타", new Menu("크리스마스파스타", 25_000, MenuType.MAIN)); + } + private static void initDessert() { + menus.put("초코케이크", new Menu("초코케이크", 15_000, MenuType.DESSERT)); + menus.put("아이스크림", new Menu("아이스크림", 5_000, MenuType.DESSERT)); + } + private static void initBeverage() { + menus.put("제로콜라", new Menu("제로콜라", 3_000, MenuType.BEVERAGE)); + menus.put("레드와인", new Menu("레드와인", 60_000, MenuType.BEVERAGE)); + menus.put("샴페인", new Menu("샴페인", 25_000, MenuType.BEVERAGE)); + } + + public static Menu findMenuByName(String name) { + if (!menus.containsKey(name)) { + throw new MenuException(MenuError.MENU_NOT_FOUND); + } + return menus.get(name); + } + +} diff --git a/yummygyudon/code/java-christmas/src/main/java/christmas/domain/repository/OrderRepository.java b/yummygyudon/code/java-christmas/src/main/java/christmas/domain/repository/OrderRepository.java new file mode 100644 index 0000000..4ed482d --- /dev/null +++ b/yummygyudon/code/java-christmas/src/main/java/christmas/domain/repository/OrderRepository.java @@ -0,0 +1,66 @@ +package christmas.domain.repository; + +import christmas.domain.entity.Order; +import christmas.domain.enums.MenuType; +import christmas.domain.exception.OrderException; +import christmas.domain.exception.error.OrderError; +import christmas.global.constant.Standard; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + +public class OrderRepository { + private final List orders ; + + public OrderRepository() { + orders = new ArrayList<>(); + } + + + // insert + public void insert(List orders) { + validate(orders); + this.orders.addAll(orders); + } + + private void validate(List orders) { + checkOnlyBeverage(orders); + checkDuplicatedMenuOrder(orders); + checkTotalOrderMenuQuantity(orders); + } + public void checkOnlyBeverage(List orders) { + long beverageCount = orders.stream() + .filter(order -> order.getMenu().getType().equals(MenuType.BEVERAGE)) + .count(); + if (orders.size() == beverageCount) { + throw new OrderException(OrderError.ONLY_BEVERAGE); + } + } + + public void checkDuplicatedMenuOrder(List orders) { + long uniqueOrderCount = orders.stream() + .map(order -> order.getMenu().getName()) + .distinct() + .count(); + if (orders.size() != uniqueOrderCount) { + throw new OrderException(OrderError.DUPLICATED_ORDER_EXIST); + } + } + + public void checkTotalOrderMenuQuantity(List orders) { + int totalOrderQuantity = orders.stream() + .mapToInt(Order::getQuantity) + .sum(); + if (totalOrderQuantity > Standard.MAXIMUM_QUANTITY_FOR_ORDER) { + throw new OrderException(OrderError.TOO_MANY_SINGLE_MENU_QUANTITY); + } + } + + + // select + public List findAll() { + return Collections.unmodifiableList(orders); + } + +} diff --git a/yummygyudon/code/java-christmas/src/main/java/christmas/global/channel/Printer.java b/yummygyudon/code/java-christmas/src/main/java/christmas/global/channel/Printer.java new file mode 100644 index 0000000..98a76ae --- /dev/null +++ b/yummygyudon/code/java-christmas/src/main/java/christmas/global/channel/Printer.java @@ -0,0 +1,13 @@ +package christmas.global.channel; + +public abstract class Printer { + + public static void print(String message) { + System.out.println(message); + } + + public static void printBlankLine() { + System.out.println(); + } + +} diff --git a/yummygyudon/code/java-christmas/src/main/java/christmas/global/channel/Reader.java b/yummygyudon/code/java-christmas/src/main/java/christmas/global/channel/Reader.java new file mode 100644 index 0000000..0df2ba4 --- /dev/null +++ b/yummygyudon/code/java-christmas/src/main/java/christmas/global/channel/Reader.java @@ -0,0 +1,10 @@ +package christmas.global.channel; + +import camp.nextstep.edu.missionutils.Console; + +public abstract class Reader { + + public static String read() { + return Console.readLine(); + } +} diff --git a/yummygyudon/code/java-christmas/src/main/java/christmas/global/constant/Regex.java b/yummygyudon/code/java-christmas/src/main/java/christmas/global/constant/Regex.java new file mode 100644 index 0000000..c4dff82 --- /dev/null +++ b/yummygyudon/code/java-christmas/src/main/java/christmas/global/constant/Regex.java @@ -0,0 +1,12 @@ +package christmas.global.constant; + +public abstract class Regex { + + /** 입력 형식*/ + // 날짜 입력 + public static String REGEX_PATTERN_FOR_DATE = "\\d{0,2}"; + + // 메뉴 및 수량 입력 + public static String REGEX_PATTERN_FOR_MENU_INPUT = "^(([\\s]*[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]+[\\s]*)\\-([\\s]*[\\d]{0,2}[\\s]*))(,(([\\s]*[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]+[\\s]*)\\-([\\s]*[\\d]{0,2}[\\s]*)))*(,([\\s]*[ㄱ-ㅎ|ㅏ-ㅣ|가-힣]+[\\s]*)\\-([\\s]*[\\d]{0,2}[\\s]*))$"; + +} diff --git a/yummygyudon/code/java-christmas/src/main/java/christmas/global/constant/Standard.java b/yummygyudon/code/java-christmas/src/main/java/christmas/global/constant/Standard.java new file mode 100644 index 0000000..a79f460 --- /dev/null +++ b/yummygyudon/code/java-christmas/src/main/java/christmas/global/constant/Standard.java @@ -0,0 +1,49 @@ +package christmas.global.constant; + +import java.util.List; + +public abstract class Standard { + + // 입력 관련 기준 + public static final String MENU_INPUT_SEPARATOR_FOR_ORDER = ","; + public static final String MENU_INPUT_SEPARATOR_FOR_NAME_AND_QUANTITY = "-"; + + // 날짜 관련 기준 + public static final int MONTH_OF_DECEMBER = 12; + public static final int FIRST_DATE_OF_DECEMBER = 1; + public static final int LAST_DATE_OF_DECEMBER = 31; + public static final int DATE_OF_CHRISTMAS = 25; + + // 메뉴 관련 기준 + public static final int MINIMUM_QUANTITY_FOR_ORDER = 1; + public static final int MAXIMUM_QUANTITY_FOR_ORDER = 20; + public static final String GIVEAWAY_MENU_NAME = "샴페인"; + + // 금액 관련 기준 + public static final int D_DAY_DISCOUNT_INCREASE_AMOUNT = 100; + public static final int MINIMUM_AMOUNT_FOR_ALL_EVENT = 10_000; + public static final int MINIMUM_AMOUNT_FOR_GIVEAWAY_EVENT = 120_000; + public static final int MINIMUM_AMOUNT_FOR_BADGE_EVENT = 5_000; + + // 기간 관련 기준 + public static final List WEEKDAY_OF_DECEMBER = List.of( + 3, 4, 5, 6, 7, + 10, 11, 12, 13, 14, + 17, 18, 19, 20, 21, + 24, 25, 26, 27, 28, + 31 + ); + + public static final List WEEKEND_OF_DECEMBER = List.of( + 1, 2, + 8, 9, + 15, 16, + 22, 23, + 29, 30 + ); + + public static final List SPECIAL_DATE_OF_DECEMBER = List.of( + 3, 10, 17, 24, 25, 31 + ); + +} diff --git a/yummygyudon/code/java-christmas/src/main/java/christmas/global/exception/GlobalError.java b/yummygyudon/code/java-christmas/src/main/java/christmas/global/exception/GlobalError.java new file mode 100644 index 0000000..37d5a4c --- /dev/null +++ b/yummygyudon/code/java-christmas/src/main/java/christmas/global/exception/GlobalError.java @@ -0,0 +1,26 @@ +package christmas.global.exception; + +import christmas.global.exception.base.ChristmasPlannerError; + +public enum GlobalError implements ChristmasPlannerError { + + /** 입력 에러 */ + // 잘못된 날짜 형식으로 입력했을 경우 + NOT_AVAILABLE_DATE_INPUT_FORMAT("유효하지 않은 날짜입니다. 다시 입력해 주세요."), + + // 잘못된 패턴으로 메뉴와 주문 갯수를 입력했을 경우 + NOT_AVAILABLE_MENU_INPUT_FORMAT("유효하지 않은 주문입니다. 다시 입력해 주세요."), + ; + + private final String errorMessage; + + GlobalError(String errorMessage) { + this.errorMessage = errorMessage; + } + + @Override + public String getErrorMessage() { + return this.errorMessage; + } + +} diff --git a/yummygyudon/code/java-christmas/src/main/java/christmas/global/exception/GlobalException.java b/yummygyudon/code/java-christmas/src/main/java/christmas/global/exception/GlobalException.java new file mode 100644 index 0000000..df65e4a --- /dev/null +++ b/yummygyudon/code/java-christmas/src/main/java/christmas/global/exception/GlobalException.java @@ -0,0 +1,13 @@ +package christmas.global.exception; + +import christmas.global.exception.base.ChristmasPlannerException; + +/** + * 전역적 혹은 시스템 전체 범위에서 발생하는 예외 + */ +public class GlobalException extends ChristmasPlannerException { + + public GlobalException(GlobalError error) { + super(error); + } +} diff --git a/yummygyudon/code/java-christmas/src/main/java/christmas/global/exception/base/ChristmasPlannerError.java b/yummygyudon/code/java-christmas/src/main/java/christmas/global/exception/base/ChristmasPlannerError.java new file mode 100644 index 0000000..f570141 --- /dev/null +++ b/yummygyudon/code/java-christmas/src/main/java/christmas/global/exception/base/ChristmasPlannerError.java @@ -0,0 +1,5 @@ +package christmas.global.exception.base; + + +public interface ChristmasPlannerError extends ErrorBase { +} diff --git a/yummygyudon/code/java-christmas/src/main/java/christmas/global/exception/base/ChristmasPlannerException.java b/yummygyudon/code/java-christmas/src/main/java/christmas/global/exception/base/ChristmasPlannerException.java new file mode 100644 index 0000000..20b2125 --- /dev/null +++ b/yummygyudon/code/java-christmas/src/main/java/christmas/global/exception/base/ChristmasPlannerException.java @@ -0,0 +1,10 @@ +package christmas.global.exception.base; + +public class ChristmasPlannerException extends IllegalArgumentException{ + private static final String ERROR_MESSAGE_HEADER = "[ERROR] "; + + public ChristmasPlannerException(ErrorBase error) { + super(ERROR_MESSAGE_HEADER + error.getErrorMessage()); + } + +} diff --git a/yummygyudon/code/java-christmas/src/main/java/christmas/global/exception/base/ErrorBase.java b/yummygyudon/code/java-christmas/src/main/java/christmas/global/exception/base/ErrorBase.java new file mode 100644 index 0000000..c799944 --- /dev/null +++ b/yummygyudon/code/java-christmas/src/main/java/christmas/global/exception/base/ErrorBase.java @@ -0,0 +1,7 @@ +package christmas.global.exception.base; + +public interface ErrorBase { + + String getErrorMessage(); + +} diff --git a/yummygyudon/code/java-christmas/src/main/java/christmas/global/message/Ask.java b/yummygyudon/code/java-christmas/src/main/java/christmas/global/message/Ask.java new file mode 100644 index 0000000..2ff455f --- /dev/null +++ b/yummygyudon/code/java-christmas/src/main/java/christmas/global/message/Ask.java @@ -0,0 +1,8 @@ +package christmas.global.message; + +public abstract class Ask { + + public static final String VISIT_DATE = "12월 중 식당 예상 방문 날짜는 언제인가요? (숫자만 입력해 주세요!)"; + public static final String MENU = "주문하실 메뉴를 메뉴와 개수를 알려 주세요. (e.g. 해산물파스타-2,레드와인-1,초코케이크-1)"; + +} \ No newline at end of file diff --git a/yummygyudon/code/java-christmas/src/main/java/christmas/global/message/Notice.java b/yummygyudon/code/java-christmas/src/main/java/christmas/global/message/Notice.java new file mode 100644 index 0000000..d6cf7dd --- /dev/null +++ b/yummygyudon/code/java-christmas/src/main/java/christmas/global/message/Notice.java @@ -0,0 +1,14 @@ +package christmas.global.message; + +public abstract class Notice { + + public static final String INFO = "안녕하세요! 우테코 식당 12월 이벤트 플래너입니다."; + public static final String ORDERED_MENU_LIST = "<주문 메뉴>"; + public static final String TOTAL_AMOUNT_BEFORE_DISCOUNT = "<할인 전 총주문 금액>"; + public static final String GIVEAWAY_MENU = "<증정 메뉴>"; + public static final String BENEFIT_LIST = "<혜택 내역>"; + public static final String TOTAL_BENEFIT_AMOUNT = "<총혜택 금액>"; + public static final String TOTAL_AMOUNT_AFTER_DISCOUNT = "<할인 후 예상 결제 금액>"; + public static final String EVENT_BADGE = "<12월 이벤트 배지>"; + +} diff --git a/yummygyudon/code/java-christmas/src/main/java/christmas/global/message/Unit.java b/yummygyudon/code/java-christmas/src/main/java/christmas/global/message/Unit.java new file mode 100644 index 0000000..0eaf7b0 --- /dev/null +++ b/yummygyudon/code/java-christmas/src/main/java/christmas/global/message/Unit.java @@ -0,0 +1,8 @@ +package christmas.global.message; + +public abstract class Unit { + + public static final String CURRENCY = "원"; + public static final String MENU_QUANTITY = "개"; + +} diff --git a/yummygyudon/code/java-christmas/src/main/java/christmas/presentation/ChristmasPlanner.java b/yummygyudon/code/java-christmas/src/main/java/christmas/presentation/ChristmasPlanner.java new file mode 100644 index 0000000..072ce23 --- /dev/null +++ b/yummygyudon/code/java-christmas/src/main/java/christmas/presentation/ChristmasPlanner.java @@ -0,0 +1,122 @@ +package christmas.presentation; + +import christmas.application.ChristmasPromotion; +import christmas.application.info.EventResultInfo; +import christmas.global.exception.base.ChristmasPlannerException; +import christmas.presentation.view.InputView; +import christmas.presentation.view.OutputView; + +import java.util.ArrayList; +import java.util.List; + +public class ChristmasPlanner { + + private final ChristmasPromotion eventService; + private final InputView inputView; + private final OutputView outputView; + + + public ChristmasPlanner() { + this.inputView = new InputView(); + this.outputView = new OutputView(); + this.eventService = new ChristmasPromotion(registerDate()); + } + + + /** + * 플래너 객체 자체가 등록일자 기반으로 행동해야 하기 때문에
+ * 생성 시점에 입력을 받아 플래너를 생성합니다. + * + * @return 예약일 + * @see ChristmasPlanner#ChristmasPlanner() + */ + private int registerDate() { + boolean isRegistered = false; + int date = 0; + outputView.printGreeting(); + while (!isRegistered) { + try { + date = inputView.readDate(); + isRegistered = true; + } catch (ChristmasPlannerException exception) { + outputView.printException(exception); + } + } + return date; + } + + /** + * 해당 구현체의 유일한 public 메서드로서
+ * 해당 메서드 호출에 따라 전체 기능을 수행합니다. + */ + public void run() { + order(); + + showPlannerHeader(); + + showPlanSummary(); + } + + /** 주문 처리를 수행합니다. */ + private void order() { + boolean isOrdered = false; + while (!isOrdered) { + try { + List menuInputs = inputView.readMenuAndQuantity(); + eventService.order(menuInputs); + isOrdered = true; + } catch (ChristmasPlannerException exception) { + outputView.printException(exception); + } + } + } + + /** 플래너 상단 헤더를 출력합니다. */ + private void showPlannerHeader() { + EventResultInfo.DateInfo dateInfo = eventService.viewPlannerInfo(); + + outputView.printSummaryHeader(dateInfo); + } + + /** 플래너 결과를 정리하여 출력합니다. */ + private void showPlanSummary() { + showOrderDetails(); + showBenefits(); + showAppliedResult(); + } + + // 사용자의 주문 내역 및 결제 금액을 출력합니다. + private void showOrderDetails() { + List orderedMenus = eventService.viewOrderedMenusHistory(); + outputView.printOrderedMenus(orderedMenus); + + EventResultInfo.Amount totalAmountBeforeDiscount = eventService.viewTotalAmountPaidBeforeDiscount(); + outputView.printPaidBeforeDiscount(totalAmountBeforeDiscount); + } + + // 각종 혜택 정보를 출력합니다. + private void showBenefits() { + EventResultInfo.ReceiveMenu giveawayMenu = eventService.viewGiveawayMenu(); + outputView.printGiveawayMenu(giveawayMenu); + + List benefits = new ArrayList<>(); + benefits.add(eventService.viewChristmasPeriodEventBenefit()); + benefits.add(eventService.viewWeekEventBenefit()); + benefits.add(eventService.viewSpecialDiscountEventBenefit()); + benefits.add(eventService.viewGiveawayBenefit()); + + outputView.printBenefits(benefits); + } + + // 총 혜택 금액과 최종 결제 금액 및 배지 정보를 출력합니다. + private void showAppliedResult() { + EventResultInfo.Amount totalDiscountAmount = eventService.viewTotalBenefitAmount(); + outputView.printTotalDiscountAmount(totalDiscountAmount); + + EventResultInfo.Amount totalAmountPaid = eventService.viewTotalAmountPaidAfterDiscount(); + outputView.printPaidAfterDiscount(totalAmountPaid); + + EventResultInfo.Badge badge = eventService.viewBadge(); + outputView.printBadge(badge); + } +} diff --git a/yummygyudon/code/java-christmas/src/main/java/christmas/presentation/view/InputView.java b/yummygyudon/code/java-christmas/src/main/java/christmas/presentation/view/InputView.java new file mode 100644 index 0000000..a6b25dc --- /dev/null +++ b/yummygyudon/code/java-christmas/src/main/java/christmas/presentation/view/InputView.java @@ -0,0 +1,80 @@ +package christmas.presentation.view; + +import christmas.global.channel.Printer; +import christmas.global.channel.Reader; +import christmas.global.constant.Regex; +import christmas.global.constant.Standard; +import christmas.global.exception.GlobalError; +import christmas.global.exception.GlobalException; +import christmas.global.message.Ask; + +import java.util.ArrayList; +import java.util.List; + +public class InputView { + + /** 방문 날짜를 입력받습니다. */ + public int readDate() { + Printer.print(Ask.VISIT_DATE); + + String input = Reader.read(); + validateDateInput(input); + + int date = Integer.parseInt(input); + validateDate(date); + return date; + } + + private void validateDateInput(String dateInput) { + if (dateInput.isBlank() || dateInput.isEmpty()) { + throw new GlobalException(GlobalError.NOT_AVAILABLE_DATE_INPUT_FORMAT); + } + if (!dateInput.matches(Regex.REGEX_PATTERN_FOR_DATE)) { + throw new GlobalException(GlobalError.NOT_AVAILABLE_DATE_INPUT_FORMAT); + } + + } + + private void validateDate(int date) { + if (date < Standard.FIRST_DATE_OF_DECEMBER || date > Standard.LAST_DATE_OF_DECEMBER) { + throw new GlobalException(GlobalError.NOT_AVAILABLE_DATE_INPUT_FORMAT); + } + } + + /** 주문 메뉴와 수량을 입력받습니다. */ + public List readMenuAndQuantity() { + Printer.print(Ask.MENU); + + String input = Reader.read(); + validateMenuInput(input); + + return splitMenuInput(input); + } + + private void validateMenuInput(String menuInput) { + if (menuInput.isBlank() || menuInput.isEmpty()) { + throw new GlobalException(GlobalError.NOT_AVAILABLE_MENU_INPUT_FORMAT); + } + if (!menuInput.matches(Regex.REGEX_PATTERN_FOR_MENU_INPUT)) { + throw new GlobalException(GlobalError.NOT_AVAILABLE_MENU_INPUT_FORMAT); + } + } + + private List splitMenuInput(String menuInput) { + String[] inputMenus = menuInput.split(Standard.MENU_INPUT_SEPARATOR_FOR_ORDER); + List result = new ArrayList<>(); + + for (String eachMenu : inputMenus) { + eachMenu = eachMenu.trim(); + String[] nameAndQuantity = eachMenu.split(Standard.MENU_INPUT_SEPARATOR_FOR_NAME_AND_QUANTITY); + + String name = nameAndQuantity[0].trim(); + int quantity = Integer.parseInt(nameAndQuantity[1].trim()); + result.add(new MenuInput(name, quantity)); + } + return result; + } + + public record MenuInput(String name, int quantity) { } + +} diff --git a/yummygyudon/code/java-christmas/src/main/java/christmas/presentation/view/OutputView.java b/yummygyudon/code/java-christmas/src/main/java/christmas/presentation/view/OutputView.java new file mode 100644 index 0000000..6860073 --- /dev/null +++ b/yummygyudon/code/java-christmas/src/main/java/christmas/presentation/view/OutputView.java @@ -0,0 +1,136 @@ +package christmas.presentation.view; + +import christmas.application.info.EventResultInfo; +import christmas.global.channel.Printer; +import christmas.global.exception.base.ChristmasPlannerException; +import christmas.global.message.Notice; +import christmas.global.message.Unit; + +import java.text.DecimalFormat; +import java.util.List; +import java.util.Objects; + +public class OutputView { + + private static final String SUMMARY_HEADER_FORMAT = "%d월 %d일에 우테코 식당에서 받을 이벤트 혜택 미리 보기!"; + private static final String BENEFIT_FORMAT = "%s: %s"; + private static final String AMOUNT_FORMAT = "%s" + Unit.CURRENCY; + private static final String DISCOUNT_AMOUNT_FORMAT = "-%s" + Unit.CURRENCY; + private static final String MENU_FORMAT = "%s %d" + Unit.MENU_QUANTITY; + + private static final String NO_BENEFIT = "없음"; + + /** 예외 출력 기능 */ + public void printException(ChristmasPlannerException exception) { + Printer.print(exception.getMessage()); + } + + /** 결과 출력 기능 */ + // 플래너 애플리케이션 시작말 출력 + public void printGreeting() { + Printer.print(Notice.INFO); + } + + // 혜택 미리보기 시작말 출력 + public void printSummaryHeader(EventResultInfo.DateInfo dateInfo) { + Printer.print(String.format(SUMMARY_HEADER_FORMAT, dateInfo.month(), dateInfo.date())); + + Printer.printBlankLine(); + } + + // 주문 메뉴 출력 + public void printOrderedMenus(List menus) { + Printer.print(Notice.ORDERED_MENU_LIST); + + for (EventResultInfo.ReceiveMenu menu : menus) { + Printer.print(String.format(MENU_FORMAT, menu.name(), menu.quantity())); + } + + Printer.printBlankLine(); + } + + // 할인 전 총주문 금액 출력 + public void printPaidBeforeDiscount(EventResultInfo.Amount amountBeforeDiscount) { + Printer.print(Notice.TOTAL_AMOUNT_BEFORE_DISCOUNT); + + Printer.print(String.format(AMOUNT_FORMAT, convertToAmountFormat(amountBeforeDiscount.amount()))); + + Printer.printBlankLine(); + } + + // 증정 메뉴 출력 + public void printGiveawayMenu(EventResultInfo.ReceiveMenu menu) { + Printer.print(Notice.GIVEAWAY_MENU); + + if (Objects.isNull(menu)) { + Printer.print(NO_BENEFIT); + Printer.printBlankLine(); + return; + } + Printer.print(String.format(MENU_FORMAT, menu.name(), menu.quantity())); + + Printer.printBlankLine(); + } + + // 혜택 내역 출력 + public void printBenefits(List benefits) { + Printer.print(Notice.BENEFIT_LIST); + + if (benefits.stream().allMatch(Objects::isNull)) { + // 모든 데이터가 Null 일 경우 + Printer.print(NO_BENEFIT); + Printer.printBlankLine(); + return; + } + + for (EventResultInfo.Benefit benefit : benefits) { + if (!Objects.isNull(benefit)){ + String discountResult = String.format(DISCOUNT_AMOUNT_FORMAT, convertToAmountFormat(benefit.discountAmount())); + Printer.print(String.format(BENEFIT_FORMAT, benefit.name(), discountResult)); + } + } + Printer.printBlankLine(); + } + + // 총혜택 금액 출력 + public void printTotalDiscountAmount(EventResultInfo.Amount totalDiscountAmount) { + Printer.print(Notice.TOTAL_BENEFIT_AMOUNT); + + if (Objects.isNull(totalDiscountAmount)) { + Printer.print(String.format(AMOUNT_FORMAT, 0)); + Printer.printBlankLine(); + return; + } + Printer.print(String.format(DISCOUNT_AMOUNT_FORMAT, convertToAmountFormat(totalDiscountAmount.amount()))); + + Printer.printBlankLine(); + } + + // 할인 후 예상 결제 금액 출력 + public void printPaidAfterDiscount(EventResultInfo.Amount amountAfterDiscount) { + Printer.print(Notice.TOTAL_AMOUNT_AFTER_DISCOUNT); + + Printer.print(String.format(AMOUNT_FORMAT, convertToAmountFormat(amountAfterDiscount.amount()))); + + Printer.printBlankLine(); + } + + // 12월 이벤트 배지 출력 + public void printBadge(EventResultInfo.Badge badge) { + Printer.print(Notice.EVENT_BADGE); + + if (Objects.isNull(badge)) { + Printer.print(NO_BENEFIT); + return; + } + + Printer.print(badge.badgeName()); + } + + /** 천 원 단위 쉼표(,) 처리 기능 */ + private String convertToAmountFormat(int amount) { + DecimalFormat decimalFormat = new DecimalFormat("###,###"); + return decimalFormat.format(amount); + } + +} diff --git a/yummygyudon/code/java-christmas/src/test/java/christmas/ApplicationTest.java b/yummygyudon/code/java-christmas/src/test/java/christmas/ApplicationTest.java new file mode 100644 index 0000000..1e5ae7a --- /dev/null +++ b/yummygyudon/code/java-christmas/src/test/java/christmas/ApplicationTest.java @@ -0,0 +1,56 @@ +package christmas; + +import camp.nextstep.edu.missionutils.test.NsTest; +import org.junit.jupiter.api.Test; + +import static camp.nextstep.edu.missionutils.test.Assertions.assertSimpleTest; +import static org.assertj.core.api.Assertions.assertThat; + +class ApplicationTest extends NsTest { + private static final String LINE_SEPARATOR = System.lineSeparator(); + + @Test + void 모든_타이틀_출력() { + assertSimpleTest(() -> { + run("3", "티본스테이크-1,바비큐립-1,초코케이크-2,제로콜라-1"); + assertThat(output()).contains( + "<주문 메뉴>", + "<할인 전 총주문 금액>", + "<증정 메뉴>", + "<혜택 내역>", + "<총혜택 금액>", + "<할인 후 예상 결제 금액>", + "<12월 이벤트 배지>" + ); + }); + } + + @Test + void 혜택_내역_없음_출력() { + assertSimpleTest(() -> { + run("26", "타파스-1,제로콜라-1"); + assertThat(output()).contains("<혜택 내역>" + LINE_SEPARATOR + "없음"); + }); + } + + @Test + void 날짜_예외_테스트() { + assertSimpleTest(() -> { + runException("a"); + assertThat(output()).contains("[ERROR] 유효하지 않은 날짜입니다. 다시 입력해 주세요."); + }); + } + + @Test + void 주문_예외_테스트() { + assertSimpleTest(() -> { + runException("3", "제로콜라-a"); + assertThat(output()).contains("[ERROR] 유효하지 않은 주문입니다. 다시 입력해 주세요."); + }); + } + + @Override + protected void runMain() { + Application.main(new String[]{}); + } +} diff --git a/yummygyudon/code/java-christmas/src/test/java/christmas/application/DiscountServiceTest.java b/yummygyudon/code/java-christmas/src/test/java/christmas/application/DiscountServiceTest.java new file mode 100644 index 0000000..16f92e5 --- /dev/null +++ b/yummygyudon/code/java-christmas/src/test/java/christmas/application/DiscountServiceTest.java @@ -0,0 +1,223 @@ +package christmas.application; + +import christmas.domain.enums.EventDiscountType; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +class DiscountServiceTest { + + private final DiscountService discountService = new DiscountService(); + + @DisplayName("날짜에 따른 올바른 크리스마스 디데이 할인 적용 일자 소속 여부를 반환한다.") + @ParameterizedTest(name = "{index} 번째 확인 일자 = 12월 {0}일, 기댓값 = {1}") + @MethodSource("argumentsForCheckDateContainsInChristmasEventDays") + void isInChristmasDiscountEventPeriod(int date, boolean expectedResult) { + // when + boolean isInChristmasDiscountEventPeriod = discountService.isInChristmasDiscountEventPeriod(date); + + // then + assertThat(isInChristmasDiscountEventPeriod).isEqualTo(expectedResult); + } + private static Stream argumentsForCheckDateContainsInChristmasEventDays() { + return Stream.of( + Arguments.of(1, true), + Arguments.of(8, true), + Arguments.of(15, true), + Arguments.of(22, true), + Arguments.of(23, true), + Arguments.of(24, true), + Arguments.of(25, true), + Arguments.of(26, false), + Arguments.of(27, false), + Arguments.of(28, false), + Arguments.of(29, false), + Arguments.of(30, false), + Arguments.of(31, false) + ); + } + + @DisplayName("올바른 크리스마스 디데이 할인 적용 금액을 반환한다.") + @ParameterizedTest(name = "{index} 번째 적용 할인 타입 = 크리스마스 디데이 할인 | 일자 = 12월 {0}일, 기대 할인가 = {1}원") + @MethodSource("argumentsForChristmasEventDiscount") + void getTotalChristmasEventDiscountAmount(int date, int expectedDiscountAmount) { + // when + int totalChristmasEventDiscountAmount = discountService.getTotalChristmasEventDiscountAmount(date); + + // then + assertThat(totalChristmasEventDiscountAmount).isEqualTo(expectedDiscountAmount); + } + private static Stream argumentsForChristmasEventDiscount() { + return Stream.of( + Arguments.of(1, 1_000), + Arguments.of(2, 1_100), + Arguments.of(3, 1_200), + Arguments.of(4, 1_300), + Arguments.of(5, 1_400), + Arguments.of(6, 1_500), + Arguments.of(7, 1_600), + Arguments.of(14, 2_300), + Arguments.of(21, 3_000), + Arguments.of(22, 3_100), + Arguments.of(23, 3_200), + Arguments.of(24, 3_300), + Arguments.of(25, 3_400) + ); + } + + @DisplayName("날짜에 따른 올바른 주일 소속 여부를 반환한다.") + @ParameterizedTest(name = "{index} 번째 확인 일자 = 12월 {0}일, 기댓값 = {1}") + @MethodSource("argumentsForCheckDateContainsInWeekday") + void isInWeekdays(int date, boolean expectedResult) { + // when + boolean isInWeekdays = discountService.isInWeekdays(date); + + // then + assertThat(isInWeekdays).isEqualTo(expectedResult); + } + private static Stream argumentsForCheckDateContainsInWeekday() { + return Stream.of( + Arguments.of(1, false), + Arguments.of(2, false), + Arguments.of(3, true), + Arguments.of(7, true), + Arguments.of(8, false), + Arguments.of(9, false), + Arguments.of(10, true), + Arguments.of(14, true), + Arguments.of(15, false), + Arguments.of(16, false), + Arguments.of(17, true), + Arguments.of(21, true), + Arguments.of(22, false), + Arguments.of(23, false), + Arguments.of(24, true), + Arguments.of(28, true), + Arguments.of(29, false), + Arguments.of(30, false), + Arguments.of(31, true) + ); + } + + @DisplayName("날짜에 따른 올바른 주말 소속 여부를 반환한다.") + @ParameterizedTest(name = "{index} 번째 확인 일자 = 12월 {0}일, 기댓값 = {1}") + @MethodSource("argumentsForCheckDateContainsInWeekend") + void isInWeekends(int date, boolean expectedResult) { + // when + boolean isInWeekends = discountService.isInWeekends(date); + + // then + assertThat(isInWeekends).isEqualTo(expectedResult); + } + private static Stream argumentsForCheckDateContainsInWeekend() { + return Stream.of( + Arguments.of(1, true), + Arguments.of(2, true), + Arguments.of(3, false), + Arguments.of(7, false), + Arguments.of(8, true), + Arguments.of(9, true), + Arguments.of(10, false), + Arguments.of(14, false), + Arguments.of(15, true), + Arguments.of(16, true), + Arguments.of(17, false), + Arguments.of(21, false), + Arguments.of(22, true), + Arguments.of(23, true), + Arguments.of(24, false), + Arguments.of(28, false), + Arguments.of(29, true), + Arguments.of(30, true), + Arguments.of(31, false) + ); + } + + @DisplayName("올바른 주 할인 적용 금액을 반환한다.") + @ParameterizedTest(name = "{index} 번째 적용 할인 타입 = {0} | 적용 주문 메뉴 수 = {1}, 기대 할인가 = {2}원") + @MethodSource("argumentsForWeekDiscount") + void getTotalWeekDiscountAmount(EventDiscountType discountType, int targetMenuOrdersCount, int expectedDiscountAmount) { + // when + int totalWeekDiscountAmount = discountService.getTotalWeekDiscountAmount(discountType, targetMenuOrdersCount); + + // then + assertThat(totalWeekDiscountAmount).isEqualTo(expectedDiscountAmount); + } + private static Stream argumentsForWeekDiscount() { + return Stream.of( + Arguments.of(EventDiscountType.WEEKDAY, 1, 2_023), + Arguments.of(EventDiscountType.WEEKDAY, 2, 4_046), + Arguments.of(EventDiscountType.WEEKDAY, 3, 6_069), + Arguments.of(EventDiscountType.WEEKDAY, 4, 8_092), + Arguments.of(EventDiscountType.WEEKDAY, 5, 10_115), + Arguments.of(EventDiscountType.WEEKDAY, 10, 20_230), + Arguments.of(EventDiscountType.WEEKDAY, 15, 30_345), + Arguments.of(EventDiscountType.WEEKDAY, 16, 32_368), + Arguments.of(EventDiscountType.WEEKDAY, 17, 34_391), + Arguments.of(EventDiscountType.WEEKDAY, 18, 36_414), + Arguments.of(EventDiscountType.WEEKDAY, 19, 38_437), + Arguments.of(EventDiscountType.WEEKDAY, 20, 40_460), + Arguments.of(EventDiscountType.WEEKEND, 1, 2_023), + Arguments.of(EventDiscountType.WEEKEND, 2, 4_046), + Arguments.of(EventDiscountType.WEEKEND, 3, 6_069), + Arguments.of(EventDiscountType.WEEKEND, 4, 8_092), + Arguments.of(EventDiscountType.WEEKEND, 5, 10_115), + Arguments.of(EventDiscountType.WEEKEND, 10, 20_230), + Arguments.of(EventDiscountType.WEEKEND, 15, 30_345), + Arguments.of(EventDiscountType.WEEKEND, 16, 32_368), + Arguments.of(EventDiscountType.WEEKEND, 17, 34_391), + Arguments.of(EventDiscountType.WEEKEND, 18, 36_414), + Arguments.of(EventDiscountType.WEEKEND, 19, 38_437), + Arguments.of(EventDiscountType.WEEKEND, 20, 40_460) + ); + } + + @DisplayName("날짜에 따른 올바른 특별 할인 적용 일자 소속 여부를 반환한다.") + @ParameterizedTest(name = "{index} 번째 확인 일자 = 12월 {0}일, 기댓값 = {1}") + @MethodSource("argumentsForCheckDateContainsInSpecialDiscountEventDays") + void isInSpecialDiscountEventDays(int date, boolean expectedResult) { + // when + boolean isInSpecialDiscountEventDays = discountService.isInSpecialDiscountEventDays(date); + + // then + assertThat(isInSpecialDiscountEventDays).isEqualTo(expectedResult); + } + private static Stream argumentsForCheckDateContainsInSpecialDiscountEventDays() { + return Stream.of( + Arguments.of(2, false), + Arguments.of(3, true), + Arguments.of(4, false), + Arguments.of(9, false), + Arguments.of(10, true), + Arguments.of(11, false), + Arguments.of(16, false), + Arguments.of(17, true), + Arguments.of(18, false), + Arguments.of(23, false), + Arguments.of(24, true), + Arguments.of(25, true), + Arguments.of(26, false), + Arguments.of(30, false), + Arguments.of(31, true) + ); + } + + @DisplayName("올바른 특별 할인 적용 금액을 반환한다.") + @Test + void getTotalSpecialDiscountAmount() { + // given + int officialSpecialDiscountAmount = EventDiscountType.SPECIAL.getDiscountAmount(); + + // when + int totalSpecialDiscountAmount = discountService.getTotalSpecialDiscountAmount(); + + // then + assertThat(totalSpecialDiscountAmount).isEqualTo(officialSpecialDiscountAmount); + } +} \ No newline at end of file diff --git a/yummygyudon/code/java-christmas/src/test/java/christmas/application/GiftServiceTest.java b/yummygyudon/code/java-christmas/src/test/java/christmas/application/GiftServiceTest.java new file mode 100644 index 0000000..3dc8b14 --- /dev/null +++ b/yummygyudon/code/java-christmas/src/test/java/christmas/application/GiftServiceTest.java @@ -0,0 +1,114 @@ +package christmas.application; + +import christmas.domain.entity.Menu; +import christmas.domain.enums.Badge; +import christmas.domain.repository.MenuRepository; +import christmas.global.constant.Standard; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + + +class GiftServiceTest { + private final GiftService giftService = new GiftService(); + private final Menu officialGiveawayMenu = MenuRepository.findMenuByName(Standard.GIVEAWAY_MENU_NAME); + + @DisplayName("결제 금액에 따른 올바른 증정 메뉴 증정 여부를 반환한다.") + @ParameterizedTest(name = "{index} 번째 확인 대상 총 금액 = {0}원, 기댓값 = {1}") + @MethodSource("argumentsForCheckGiveaway") + void isGiveaway(int totalAmount, boolean expectedResult) { + // when + boolean isGiveaway = giftService.isGiveaway(totalAmount); + + // then + assertThat(isGiveaway).isEqualTo(expectedResult); + } + private static Stream argumentsForCheckGiveaway() { + return Stream.of( + Arguments.of(119_000, false), + Arguments.of(120_001, true), + Arguments.of(120_000, true), + Arguments.of(121_000, true), + Arguments.of(119_999, false) + ); + } + + @DisplayName("올바른 증정 메뉴 이름을 반환한다.") + @Test + void getGiveawayMenuName() { + // when + String giveawayMenuName = giftService.getGiveawayMenuName(); + + // then + assertThat(giveawayMenuName).isEqualTo(officialGiveawayMenu.getName()); + } + + @DisplayName("올바른 증정 메뉴 증정 수량을 반환한다.") + @Test + void getGiveawayMenuPrice() { + // when + int giveawayMenuQuantity = giftService.getGiveawayMenuQuantity(); + + // then + assertThat(giveawayMenuQuantity).isEqualTo(1); + } + + @DisplayName("올바른 증정 메뉴 가격을 반환한다.") + @Test + void getGiveawayMenuQuantity() { + // when + int giveawayMenuPrice = giftService.getGiveawayMenuPrice(); + + // then + assertThat(giveawayMenuPrice).isEqualTo(officialGiveawayMenu.getPrice()); + } + + @DisplayName("혜택 금액에 따른 올바른 뱃지 증정 여부를 반환한다.") + @ParameterizedTest(name = "{index} 번째 할인 금액 = {0}원, 기댓값 = {1}") + @MethodSource("argumentsForCheckProvidingBadge") + void isBadge(int discountAmount, boolean expectedResult) { + // when + boolean isBadgeProvided = giftService.isBadge(discountAmount); + + // then + assertThat(isBadgeProvided).isEqualTo(expectedResult); + } + private static Stream argumentsForCheckProvidingBadge() { + return Stream.of( + Arguments.of(1_000, false), + Arguments.of(4_900, false), + Arguments.of(4_999, false), + Arguments.of(5_000, true), + Arguments.of(5_001, true) + ); + } + + @DisplayName("올바른 증정 뱃지의 이름을 반환한다.") + @ParameterizedTest(name = "{index} 번째 할인 금액 = {0}원, 기대 수령 뱃지 이름 = {1}") + @MethodSource("argumentsForReceiveBadge") + void receiveBadgeName(int discountAmount, String expectedBadgeName) { + // when + String badgeName = giftService.receiveBadgeName(discountAmount); + + // then + assertThat(badgeName).isEqualTo(expectedBadgeName); + } + private static Stream argumentsForReceiveBadge() { + return Stream.of( + Arguments.of(4_999, Badge.NONE.getName()), + Arguments.of(5_001, Badge.STAR.getName()), + Arguments.of(9_999, Badge.STAR.getName()), + Arguments.of(10_001, Badge.TREE.getName()), + Arguments.of(19_999, Badge.TREE.getName()), + Arguments.of(20_001, Badge.SANTA.getName()), + Arguments.of(100_000, Badge.SANTA.getName()), + Arguments.of(1_000_000, Badge.SANTA.getName()) + ); + } +} \ No newline at end of file diff --git a/yummygyudon/code/java-christmas/src/test/java/christmas/application/OrderServiceTest.java b/yummygyudon/code/java-christmas/src/test/java/christmas/application/OrderServiceTest.java new file mode 100644 index 0000000..1f964c2 --- /dev/null +++ b/yummygyudon/code/java-christmas/src/test/java/christmas/application/OrderServiceTest.java @@ -0,0 +1,369 @@ +package christmas.application; + +import christmas.application.info.EventResultInfo; +import christmas.domain.enums.MenuType; +import christmas.domain.exception.MenuException; +import christmas.domain.exception.OrderException; +import christmas.domain.exception.error.MenuError; +import christmas.domain.exception.error.OrderError; +import christmas.presentation.view.InputView; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.List; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.*; + +class OrderServiceTest { + + private OrderService orderService; + + @BeforeEach + void initRepository() { + orderService = new OrderService(); + } + + + @DisplayName("잘못된 입력 주문들에 대해 저장할 경우, 예외가 발생한다.") + @ParameterizedTest(name = "{index} 번째 입력 주문 = {0}, 기대 예외 = {1}") + @MethodSource("argumentsForWrongMenuInputs") + void registerWrongOrders(List wrongMenuInputs, Exception expectedException) { + assertThatThrownBy(() -> orderService.registerOrders(wrongMenuInputs)) + .usingRecursiveComparison() + .isEqualTo(expectedException); + } + private static Stream argumentsForWrongMenuInputs() { + return Stream.of( + // 존재하지 않은 메뉴를 등록하려고 할 경우 + Arguments.of( + List.of( + new InputView.MenuInput("타파스",5), + new InputView.MenuInput("계란샐러드",5), + new InputView.MenuInput("해산물파스타",5), + new InputView.MenuInput("아이스크림",5) + ), + new MenuException(MenuError.MENU_NOT_FOUND) + ), + // 단일 메뉴 중 주문 갯수가 20개를 초과할 경우 + Arguments.of( + List.of( + new InputView.MenuInput("타파스",5), + new InputView.MenuInput("시저샐러드",21), + new InputView.MenuInput("해산물파스타",5), + new InputView.MenuInput("아이스크림",5) + ), + new OrderException(OrderError.TOO_MANY_SINGLE_MENU_QUANTITY) + ), + // 단일 메뉴 중 주문 갯수가 1개 미만일 경우 + Arguments.of( + List.of( + new InputView.MenuInput("양송이수프",5), + new InputView.MenuInput("크리스마스파스타",0), + new InputView.MenuInput("초코케이크",5), + new InputView.MenuInput("레드와인",5) + ), + new OrderException(OrderError.TOO_LITTLE_SINGLE_MENU_QUANTITY) + ), + // 총 메뉴 갯수가 20개를 초과할 경우 + Arguments.of( + List.of( + new InputView.MenuInput("양송이수프",6), + new InputView.MenuInput("크리스마스파스타",5), + new InputView.MenuInput("초코케이크",5), + new InputView.MenuInput("레드와인",5) + ), + new OrderException(OrderError.TOO_MANY_MENU_QUANTITY) + ), + // 주문 메뉴가 모두 음료일 경우 + Arguments.of( + List.of( + new InputView.MenuInput("제로콜라",7), + new InputView.MenuInput("레드와인",7), + new InputView.MenuInput("샴페인",6) + ), + new OrderException(OrderError.ONLY_BEVERAGE) + ), + // 중복 메뉴 주문이 있을 경우 + Arguments.of( + List.of( + new InputView.MenuInput("타파스",5), + new InputView.MenuInput("바비큐립",3), + new InputView.MenuInput("바비큐립",7), + new InputView.MenuInput("제로콜라",5) + ), + new OrderException(OrderError.DUPLICATED_ORDER_EXIST) + ) + ); + } + @DisplayName("올바른 입력 주문들에 대해 저장할 경우, 성공한다.") + @ParameterizedTest(name = "{index} 번째 탐색 메뉴값 = {0}") + @MethodSource("argumentsForAvailableMenuInputs") + void registerAvailableOrders(List availableMenuInputs) { + assertThatCode(() -> orderService.registerOrders(availableMenuInputs)) + .doesNotThrowAnyException(); + } + private static Stream argumentsForAvailableMenuInputs() { + return Stream.of( + Arguments.of( + List.of( + new InputView.MenuInput("타파스",5), + new InputView.MenuInput("해산물파스타",5), + new InputView.MenuInput("아이스크림",5) + ) + ), + Arguments.of( + List.of( + new InputView.MenuInput("타파스",5), + new InputView.MenuInput("시저샐러드",5), + new InputView.MenuInput("해산물파스타",5), + new InputView.MenuInput("아이스크림",5) + ) + ), + Arguments.of( + List.of( + new InputView.MenuInput("양송이수프",5), + new InputView.MenuInput("제로콜라",5), + new InputView.MenuInput("레드와인",5) + ) + ), + Arguments.of( + List.of( + new InputView.MenuInput("양송이수프",5), + new InputView.MenuInput("크리스마스파스타",5), + new InputView.MenuInput("초코케이크",5), + new InputView.MenuInput("레드와인",5) + ) + ), + Arguments.of( + List.of( + new InputView.MenuInput("제로콜라",7), + new InputView.MenuInput("레드와인",7), + new InputView.MenuInput("아이스크림",6) + ) + ), + Arguments.of( + List.of( + new InputView.MenuInput("타파스",5), + new InputView.MenuInput("바비큐립",3), + new InputView.MenuInput("티본스테이크",7), + new InputView.MenuInput("제로콜라",5) + ) + ) + ); + } + + /** 연산 기능에 대한 테스트 */ + @DisplayName("지정 타입의 메뉴에 대한 주문 존재 여부를 정확하게 반환한다.") + @ParameterizedTest(name = "{index} 번째 탐색 메뉴값 = {0}") + @MethodSource("argumentsForCheckSpecifiedTypeExist") + void isExistAnyMenuTypeInOrders(List menuInputs, MenuType targetMenuType, boolean expectedResult) { + // given + orderService.registerOrders(menuInputs); + + // when + boolean existAnyMenuTypeInOrders = orderService.isExistAnyMenuTypeInOrders(targetMenuType); + + // then + assertThat(existAnyMenuTypeInOrders).isEqualTo(expectedResult); + } + private static Stream argumentsForCheckSpecifiedTypeExist() { + return Stream.of( + Arguments.of( + List.of( + new InputView.MenuInput("타파스",5), + new InputView.MenuInput("해산물파스타",5), + new InputView.MenuInput("아이스크림",5) + ), + MenuType.BEVERAGE, + false + ), + Arguments.of( + List.of( + new InputView.MenuInput("타파스",5), + new InputView.MenuInput("시저샐러드",5), + new InputView.MenuInput("해산물파스타",5), + new InputView.MenuInput("아이스크림",5) + ), + MenuType.MAIN, + true + ), + Arguments.of( + List.of( + new InputView.MenuInput("양송이수프",5), + new InputView.MenuInput("제로콜라",5), + new InputView.MenuInput("레드와인",5) + ), + MenuType.MAIN, + false + ), + Arguments.of( + List.of( + new InputView.MenuInput("양송이수프",5), + new InputView.MenuInput("제로콜라",5), + new InputView.MenuInput("레드와인",5) + ), + MenuType.APPETIZER, + true + ) + ); + } + + @DisplayName("주문들의 총합 결제금액을 정확하게 반환한다.") + @ParameterizedTest(name = "{index} 번째 탐색 메뉴값 = {0}") + @MethodSource("argumentsForTotalAmount") + void getTotalAmount(List menuInputs, int expectedTotalAmount) { + // given + orderService.registerOrders(menuInputs); + + // when + int totalAmount = orderService.getTotalAmount(); + + // then + assertThat(totalAmount).isEqualTo(expectedTotalAmount); + } + private static Stream argumentsForTotalAmount() { + return Stream.of( + Arguments.of( + List.of( + new InputView.MenuInput("타파스",5), // 27_500 + new InputView.MenuInput("해산물파스타",5), // 175_000 + new InputView.MenuInput("아이스크림",5) // 25_000 + ), + 227_500 + ), + Arguments.of( + List.of( + new InputView.MenuInput("타파스",5), // 27_500 + new InputView.MenuInput("시저샐러드",5), // 40_000 + new InputView.MenuInput("해산물파스타",5), // 175_000 + new InputView.MenuInput("아이스크림",5) // 25_000 + ), + 267_500 + ), + Arguments.of( + List.of( + new InputView.MenuInput("양송이수프",5), // 30_000 + new InputView.MenuInput("제로콜라",5), // 15_000 + new InputView.MenuInput("레드와인",5) // 300_000 + ), + 345_000 + ) + ); + } + + @DisplayName("지정 타입의 메뉴에 대한 총 주문 수량을 정확하게 반환한다.") + @ParameterizedTest(name = "{index} 번째 입력 주문 = {0}, 탐색 대상 타입 : {1} | 기대 주문 총량 = {2}") + @MethodSource("argumentsForTotalAmountOfSpecifiedType") + void getOrdersCountByMenuType(List menuInputs, MenuType targetType, int expectedOrdersCount) { + // given + orderService.registerOrders(menuInputs); + + // when + int ordersCountByMenuType = orderService.getOrdersCountByMenuType(targetType); + + // then + assertThat(ordersCountByMenuType).isEqualTo(expectedOrdersCount); + } + private static Stream argumentsForTotalAmountOfSpecifiedType() { + return Stream.of( + Arguments.of( + List.of( + new InputView.MenuInput("타파스",5), + new InputView.MenuInput("해산물파스타",5), + new InputView.MenuInput("아이스크림",5) + ), + MenuType.BEVERAGE, + 0 + ), + Arguments.of( + List.of( + new InputView.MenuInput("타파스",5), + new InputView.MenuInput("해산물파스타",5), + new InputView.MenuInput("아이스크림",5) + ), + MenuType.APPETIZER, + 5 + ), + Arguments.of( + List.of( + new InputView.MenuInput("타파스",5), + new InputView.MenuInput("시저샐러드",5), + new InputView.MenuInput("해산물파스타",5), + new InputView.MenuInput("아이스크림",5) + ), + MenuType.APPETIZER, + 10 + ), + Arguments.of( + List.of( + new InputView.MenuInput("양송이수프",5), + new InputView.MenuInput("바비큐립",5), + new InputView.MenuInput("티본스테이크",5) + ), + MenuType.MAIN, + 10 + ) + ); + } + + @DisplayName("주문 내역 정보를 정확하게 반환한다.") + @ParameterizedTest(name = "{index} 번째 입력 주문 = {0}, 기대 주문 내역 = {1}") + @MethodSource("argumentsForOrderHistoryInfo") + void getOrdersHistory(List menuInputs, List expectedMenuHistoryInfos) { + // given + orderService.registerOrders(menuInputs); + + // when + List ordersHistory = orderService.getOrdersHistory(); + + // then + assertThat(ordersHistory).usingRecursiveComparison().isEqualTo(expectedMenuHistoryInfos); + } + private static Stream argumentsForOrderHistoryInfo() { + return Stream.of( + Arguments.of( + List.of( + new InputView.MenuInput("타파스",5), + new InputView.MenuInput("해산물파스타",5), + new InputView.MenuInput("아이스크림",5) + ), + List.of( + new EventResultInfo.ReceiveMenu("타파스",5), + new EventResultInfo.ReceiveMenu("해산물파스타",5), + new EventResultInfo.ReceiveMenu("아이스크림",5) + ) + ), + Arguments.of( + List.of( + new InputView.MenuInput("타파스",5), + new InputView.MenuInput("시저샐러드",5), + new InputView.MenuInput("해산물파스타",5), + new InputView.MenuInput("아이스크림",5) + ), + List.of( + new EventResultInfo.ReceiveMenu("타파스",5), + new EventResultInfo.ReceiveMenu("시저샐러드",5), + new EventResultInfo.ReceiveMenu("해산물파스타",5), + new EventResultInfo.ReceiveMenu("아이스크림",5) + ) + ), + Arguments.of( + List.of( + new InputView.MenuInput("양송이수프",5), + new InputView.MenuInput("제로콜라",5), + new InputView.MenuInput("레드와인",5) + ), + List.of( + new EventResultInfo.ReceiveMenu("양송이수프",5), + new EventResultInfo.ReceiveMenu("제로콜라",5), + new EventResultInfo.ReceiveMenu("레드와인",5) + ) + ) + ); + } + +} diff --git a/yummygyudon/code/java-christmas/src/test/java/christmas/domain/MenuTest.java b/yummygyudon/code/java-christmas/src/test/java/christmas/domain/MenuTest.java new file mode 100644 index 0000000..043c718 --- /dev/null +++ b/yummygyudon/code/java-christmas/src/test/java/christmas/domain/MenuTest.java @@ -0,0 +1,55 @@ +package christmas.domain; + +import christmas.domain.entity.Menu; +import christmas.domain.enums.MenuType; +import christmas.domain.repository.MenuRepository; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayName("Menu 도메인 테스트") +class MenuTest { + + @DisplayName("존재하지 않는 메뉴를 찾으면 예외가 발생한다.") + @ParameterizedTest(name = "{index} 번째 탐색 메뉴값 = {0}") + @ValueSource(strings = { + "우니동", "소갈비살", "탕후루", "사이다" + }) + void findMenuNotExist(String notExistMenu) { + assertThatThrownBy(() -> MenuRepository.findMenuByName(notExistMenu)) + .isInstanceOf(IllegalArgumentException.class); + } + + @DisplayName("존재하는 메뉴를 찾으면 정확하게 반환한다.") + @ParameterizedTest(name = "{index} 번째 탐색 메뉴값 = {0}") + @MethodSource("argumentsForExistMenu") + void findMenuExist(String searchMenu, Menu expectedMenu) { + assertThat(MenuRepository.findMenuByName(searchMenu)) + .usingRecursiveComparison().isEqualTo(expectedMenu); + } + private static Stream argumentsForExistMenu() { + return Stream.of( + Arguments.of("양송이수프", new Menu("양송이수프", 6_000, MenuType.APPETIZER)), + Arguments.of("타파스", new Menu("타파스", 5_500, MenuType.APPETIZER)), + Arguments.of("시저샐러드", new Menu("시저샐러드", 8_000, MenuType.APPETIZER)), + Arguments.of("티본스테이크", new Menu("티본스테이크", 55_000, MenuType.MAIN)), + Arguments.of("바비큐립", new Menu("바비큐립", 54_000, MenuType.MAIN)), + Arguments.of("해산물파스타", new Menu("해산물파스타", 35_000, MenuType.MAIN)), + Arguments.of("크리스마스파스타", new Menu("크리스마스파스타", 25_000, MenuType.MAIN)), + Arguments.of("초코케이크", new Menu("초코케이크", 15_000, MenuType.DESSERT)), + Arguments.of("아이스크림", new Menu("아이스크림", 5_000, MenuType.DESSERT)), + Arguments.of("제로콜라", new Menu("제로콜라", 3_000, MenuType.BEVERAGE)), + Arguments.of("레드와인", new Menu("레드와인", 60_000, MenuType.BEVERAGE)), + Arguments.of("샴페인", new Menu("샴페인", 25_000, MenuType.BEVERAGE)) + ); + } + + +} \ No newline at end of file diff --git a/yummygyudon/code/java-christmas/src/test/java/christmas/domain/OrderTest.java b/yummygyudon/code/java-christmas/src/test/java/christmas/domain/OrderTest.java new file mode 100644 index 0000000..7ae8b4c --- /dev/null +++ b/yummygyudon/code/java-christmas/src/test/java/christmas/domain/OrderTest.java @@ -0,0 +1,235 @@ +package christmas.domain; + +import christmas.domain.entity.Menu; +import christmas.domain.entity.Order; +import christmas.domain.enums.MenuType; +import christmas.domain.exception.OrderException; +import christmas.domain.exception.error.OrderError; +import christmas.domain.repository.MenuRepository; +import christmas.domain.repository.OrderRepository; +import org.junit.jupiter.api.*; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.List; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + + +@DisplayName("Order 도메인 테스트") +class OrderTest { + + + /** Order 테스트 */ + @Nested + @DisplayName("Order Object Test") + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + class OrderObjectTest { + private static final Menu availableMenu = MenuRepository.findMenuByName("초코케이크"); + @DisplayName("1보다 작은 수량으로 Order 객체를 생성할 경우, 예외가 발생한다.") + @ParameterizedTest(name = "{index} 번째 유효하지 않은 수량값 = {0}") + @ValueSource(ints = { + -1, 0, -9999 + }) + void createOrderWithTooSmallQuantity(int invalidQuantity) { + assertThatThrownBy(() -> new Order(invalidQuantity, availableMenu)) + .isInstanceOf(IllegalArgumentException.class) + .isInstanceOf(OrderException.class) + .hasMessageContaining(OrderError.TOO_LITTLE_SINGLE_MENU_QUANTITY.getErrorMessage()); + } + + @DisplayName("20보다 큰 수량으로 Order 객체를 생성할 경우, 예외가 발생한다.") + @ParameterizedTest(name = "{index} 번째 유효하지 않은 수량값 = {0}") + @ValueSource(ints = { + 21, 100, 1000, 9999 + }) + void createOrderWithTooBigQuantity(int invalidQuantity) { + assertThatThrownBy(() -> new Order(invalidQuantity, availableMenu)) + .isInstanceOf(IllegalArgumentException.class) + .isInstanceOf(OrderException.class) + .hasMessageContaining(OrderError.TOO_MANY_SINGLE_MENU_QUANTITY.getErrorMessage()); + } + + @DisplayName("해당 주문에 대한 결제금액을 반환한다.") + @ParameterizedTest(name = "{index} 번째 주문 = {1} : {0}개 / 예상 총 금액 : {2}") + @MethodSource("argumentsForAvailableOrder") + void calculateTotalPrice(int quantity, Menu menu, int expectedTotalPrice) { + // when + Order availableOrder = new Order(quantity, menu); + + // given + int totalPrice = availableOrder.calculateTotalPrice(); + + // then + assertThat(totalPrice).isEqualTo(expectedTotalPrice); + } + private static Stream argumentsForAvailableOrder() { + return Stream.of( + Arguments.of(3, new Menu("양송이수프", 6_000, MenuType.APPETIZER), 18_000), + Arguments.of(2, new Menu("타파스", 5_500, MenuType.APPETIZER), 11_000), + Arguments.of(5, new Menu("시저샐러드", 8_000, MenuType.APPETIZER), 40_000), + Arguments.of(10, new Menu("티본스테이크", 55_000, MenuType.MAIN), 550_000), + Arguments.of(13, new Menu("바비큐립", 54_000, MenuType.MAIN), 702_000), + Arguments.of(7, new Menu("해산물파스타", 35_000, MenuType.MAIN), 245_000), + Arguments.of(4, new Menu("크리스마스파스타", 25_000, MenuType.MAIN), 100_000), + Arguments.of(9, new Menu("초코케이크", 15_000, MenuType.DESSERT), 135_000), + Arguments.of(20, new Menu("아이스크림", 5_000, MenuType.DESSERT), 100_000), + Arguments.of(1, new Menu("제로콜라", 3_000, MenuType.BEVERAGE), 3_000), + Arguments.of(11, new Menu("레드와인", 60_000, MenuType.BEVERAGE), 660_000), + Arguments.of(19, new Menu("샴페인", 25_000, MenuType.BEVERAGE), 475_000) + ); + } + + } + + /** Order Repository 테스트 */ + @Nested + @DisplayName("Order Repository Test") + @TestInstance(TestInstance.Lifecycle.PER_METHOD) + class OrderRepoTest { + private OrderRepository orderRepository; + + @BeforeEach + void setOrderRepository() { + orderRepository = new OrderRepository(); + } + + @DisplayName("음료 메뉴에 대한 Order 만 존재할 경우, 예외가 발생한다.") + @ParameterizedTest(name = "{index} 번째 음료 메뉴만 주문 = {0}") + @MethodSource("argumentsForOnlyBeverage") + void checkOnlyBeverage(List orders) { + assertThatThrownBy(() -> orderRepository.insert(orders)) + .isInstanceOf(IllegalArgumentException.class) + .isInstanceOf(OrderException.class) + .hasMessageContaining(OrderError.ONLY_BEVERAGE.getErrorMessage()); + } + private static Stream argumentsForOnlyBeverage() { + return Stream.of( + Arguments.of( + Named.of( + "[제로콜라:1]", + List.of( + new Order(1, new Menu("제로콜라", 3_000, MenuType.BEVERAGE)) + ) + ) + ), + Arguments.of( + Named.of( + "[레드와인:5, 제로콜라:10, 샴페인:5]", + List.of( + new Order(5, new Menu("레드와인", 60_000, MenuType.BEVERAGE)), + new Order(10, new Menu("제로콜라", 3_000, MenuType.BEVERAGE)), + new Order(5, new Menu("샴페인", 25_000, MenuType.BEVERAGE)) + ) + ) + ), + Arguments.of( + Named.of( + "[레드와인:5, 샴페인:5]", + List.of( + new Order(5, new Menu("레드와인", 60_000, MenuType.BEVERAGE)), + new Order(5, new Menu("샴페인", 25_000, MenuType.BEVERAGE)) + ) + ) + ) + ); + } + + @DisplayName("동일 메뉴에 대한 중복 Order 가 존재할 경우, 예외가 발생한다.") + @ParameterizedTest(name = "{index} 번째 중복 존재 주문 = {0}") + @MethodSource("argumentsForDuplicatedOrders") + void checkDuplicatedMenuOrder(List orders) { + assertThatThrownBy(() -> orderRepository.insert(orders)) + .isInstanceOf(IllegalArgumentException.class) + .isInstanceOf(OrderException.class) + .hasMessageContaining(OrderError.DUPLICATED_ORDER_EXIST.getErrorMessage()); + } + private static Stream argumentsForDuplicatedOrders() { + return Stream.of( + Arguments.of( + Named.of( + "[양송이수프:1, 양송이수프:2, 타파스:1]", + List.of( + new Order(1, new Menu("양송이수프", 6_000, MenuType.APPETIZER)), + new Order(2, new Menu("양송이수프", 6_000, MenuType.APPETIZER)), + new Order(1, new Menu("타파스", 5_500, MenuType.APPETIZER)) + ) + ) + + ), + Arguments.of( + Named.of( + "[티본스테이크:1, 크리스마스파스타:2, 아이스크림:2, 티본스테이크:1]", + List.of( + new Order(1, new Menu("티본스테이크", 55_000, MenuType.MAIN)), + new Order(2, new Menu("크리스마스파스타", 25_000, MenuType.MAIN)), + new Order(2, new Menu("아이스크림", 5_000, MenuType.DESSERT)), + new Order(1, new Menu("티본스테이크", 55_000, MenuType.MAIN)) + ) + ) + ), + Arguments.of( + Named.of( + "[양송이수프:3, 시저샐러드:6, 티본스테이크:2, 티본스테이크:4, 레드와인:5]", + List.of( + new Order(3, new Menu("양송이수프", 6_000, MenuType.APPETIZER)), + new Order(6, new Menu("시저샐러드", 8_000, MenuType.APPETIZER)), + new Order(2, new Menu("티본스테이크", 55_000, MenuType.MAIN)), + new Order(4, new Menu("티본스테이크", 55_000, MenuType.MAIN)), + new Order(5, new Menu("레드와인", 60_000, MenuType.BEVERAGE)) + ) + ) + ) + ); + } + + @DisplayName("전체 Order 된 Menu 들의 주문 수량이 20개를 넘어갈 경우, 예외가 발생한다.") + @ParameterizedTest(name = "{index} 번째 총 주문 갯수 한계치 초과 주문 = {0}") + @MethodSource("argumentsForOverTotalQuantity") + void checkTotalOrderMenuQuantity(List orders) { + assertThatThrownBy(() -> orderRepository.insert(orders)) + .isInstanceOf(IllegalArgumentException.class) + .isInstanceOf(OrderException.class) + .hasMessageContaining(OrderError.TOO_MANY_MENU_QUANTITY.getErrorMessage()); + } + private static Stream argumentsForOverTotalQuantity() { + return Stream.of( + Arguments.of( + Named.of( + "[해산물파스타:19, 제로콜라:2]", + List.of( + new Order(19, new Menu("해산물파스타", 35_000, MenuType.MAIN)), + new Order(2, new Menu("제로콜라", 3_000, MenuType.BEVERAGE)) + ) + ) + ), + Arguments.of( + Named.of( + "[티본스테이크:5, 제로콜라:5, 레드와인:5, 샴페인:6]", + List.of( + new Order(5, new Menu("티본스테이크", 55_000, MenuType.MAIN)), + new Order(5, new Menu("제로콜라", 3_000, MenuType.BEVERAGE)), + new Order(5, new Menu("레드와인", 60_000, MenuType.BEVERAGE)), + new Order(6, new Menu("샴페인", 25_000, MenuType.BEVERAGE)) + ) + ) + ), + Arguments.of( + Named.of( + "[시저샐러드:8, 바비큐립:5, 크리스마스파스타:4, 제로콜라:10]", + List.of( + new Order(8, new Menu("시저샐러드", 8_000, MenuType.APPETIZER)), + new Order(5, new Menu("바비큐립", 54_000, MenuType.MAIN)), + new Order(4, new Menu("크리스마스파스타", 25_000, MenuType.MAIN)), + new Order(10, new Menu("제로콜라", 3_000, MenuType.BEVERAGE))) + ) + ) + ); + } + } + +} \ No newline at end of file diff --git a/yummygyudon/code/java-lotto/.gitignore b/yummygyudon/code/java-lotto/.gitignore new file mode 100644 index 0000000..6c01878 --- /dev/null +++ b/yummygyudon/code/java-lotto/.gitignore @@ -0,0 +1,32 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/** +!**/src/test/** + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/yummygyudon/code/java-lotto/build.gradle b/yummygyudon/code/java-lotto/build.gradle new file mode 100644 index 0000000..b11abb5 --- /dev/null +++ b/yummygyudon/code/java-lotto/build.gradle @@ -0,0 +1,22 @@ +plugins { + id 'java' +} + +repositories { + mavenCentral() + maven { url 'https://jitpack.io' } +} + +dependencies { + implementation 'com.github.woowacourse-projects:mission-utils:1.1.0' +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +test { + useJUnitPlatform() +} diff --git a/yummygyudon/code/java-lotto/gradle/wrapper/gradle-wrapper.jar b/yummygyudon/code/java-lotto/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/yummygyudon/code/java-lotto/gradle/wrapper/gradle-wrapper.jar differ diff --git a/yummygyudon/code/java-lotto/gradle/wrapper/gradle-wrapper.properties b/yummygyudon/code/java-lotto/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ae04661 --- /dev/null +++ b/yummygyudon/code/java-lotto/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/yummygyudon/code/java-lotto/gradlew b/yummygyudon/code/java-lotto/gradlew new file mode 100755 index 0000000..a69d9cb --- /dev/null +++ b/yummygyudon/code/java-lotto/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/yummygyudon/code/java-lotto/gradlew.bat b/yummygyudon/code/java-lotto/gradlew.bat new file mode 100644 index 0000000..f127cfd --- /dev/null +++ b/yummygyudon/code/java-lotto/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/yummygyudon/code/java-lotto/settings.gradle b/yummygyudon/code/java-lotto/settings.gradle new file mode 100644 index 0000000..1daa39f --- /dev/null +++ b/yummygyudon/code/java-lotto/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'java-lotto' + diff --git a/yummygyudon/code/java-lotto/src/main/java/lotto/Application.java b/yummygyudon/code/java-lotto/src/main/java/lotto/Application.java new file mode 100644 index 0000000..2cd8def --- /dev/null +++ b/yummygyudon/code/java-lotto/src/main/java/lotto/Application.java @@ -0,0 +1,10 @@ +package lotto; + +import lotto.presentation.LottoGameRunner; + +public class Application { + public static void main(String[] args) { + // TODO: 프로그램 구현 + LottoGameRunner.getRunner().run(); + } +} diff --git a/yummygyudon/code/java-lotto/src/main/java/lotto/application/LottoGame.java b/yummygyudon/code/java-lotto/src/main/java/lotto/application/LottoGame.java new file mode 100644 index 0000000..76ae269 --- /dev/null +++ b/yummygyudon/code/java-lotto/src/main/java/lotto/application/LottoGame.java @@ -0,0 +1,144 @@ +package lotto.application; + +import lotto.domain.Lotto; +import lotto.domain.exception.LottoException; +import lotto.domain.repository.LottoRepository; +import lotto.global.constant.enums.MatchResultType; +import lotto.global.exception.GlobalError; +import lotto.global.exception.GlobalException; + +import java.util.*; + +public class LottoGame { + private static final int LOTTO_PRICE = 1_000; + private final LottoRepository lottoRepository; + + private Lotto winningLotto; + private Integer winningBonus; + + private Map resultBoard; + + /** + *

+ * 사용자가 입력한 구매 금액에 따라 자동으로 로또를 발행한 후, 발행된 로또들을 본 게임 로또 저장소에 저장합니다. + *

+ * + * @param purchasedLottos 본 게임에 대해 자동 발행된 로또들을 입력 받습니다. + * @see LottoRepository + */ + private LottoGame(List purchasedLottos) { + initBoard(); + this.lottoRepository = LottoRepository.createRepository(purchasedLottos); + } + + /** + * 본 게임을 생성합니다. + * + * @param paymentAmount 사용자 로또 구매 금액 + * @return 입력 금액 기반으로 생성된 LottoGame 객체를 외부로 반환합니다. + */ + public static LottoGame createLottoGame(long paymentAmount) { + long quantity = calculateLottoQuantity(paymentAmount); + + List lottos = new ArrayList<>(); + // 로또들을 통해 본 게임 객체를 생성합니다. + for (long purchase = 0; purchase < quantity; purchase++) { + lottos.add(LottoMachine.issueAutomaticLotto()); + } + return new LottoGame(lottos); + } + + private static long calculateLottoQuantity(long paymentAmount) { + return paymentAmount / LOTTO_PRICE; + } + + /** 본 게임의 당첨 번호를 등록합니다. */ + public void registerWinningNumbers(List winningNumbers) throws LottoException{ + this.winningLotto = LottoMachine.issueManualLotto(winningNumbers); + } + + /** 본 게임의 보너스 번호를 등록합니다. */ + public void registerWinningBonus(int winningBonus) { + if (winningLotto.isExist(winningBonus)) { + throw new GlobalException(GlobalError.ALREADY_EXIST_IN_WINNING_NUMBERS); + } + this.winningBonus = winningBonus; + } + + /** + * 구매 현황에 대해 반환 합니다. + * @return 구매 금액에 따라 자동 발행된 로또 정보를 반환합니다. + * @see LottoGameInfo.PurchaseStatus + */ + public LottoGameInfo.PurchaseStatus checkPurchaseStatus() { + return new LottoGameInfo.PurchaseStatus( + lottoRepository.count(), + lottoRepository.lottos() + ); + } + + /** + *

+ * 당첨 번호 혹은 보너스 번호가 입력되기 전에 본 메서드를 호출할 경우,
+ * 그에 상응하는 에러를 발생시킵니다. + *

+ * @return 본 로또 게임의 통계 결과를 반환합니다. + * @see LottoGameInfo.MatchResult + * @exception GlobalException : Lotto 도메인의 에러가 아닌 전체적인 LottoGame 규모의 에러 + * @see GlobalError#NOT_REGISTER_WINNING_YET + * @see GlobalError#NOT_REGISTER_BONUS_YET + */ + public LottoGameInfo.MatchResult checkMatchResult() throws GlobalException { + if (Objects.isNull(winningLotto)) { + throw new GlobalException(GlobalError.NOT_REGISTER_WINNING_YET); + } + if (Objects.isNull(winningBonus)) { + throw new GlobalException(GlobalError.NOT_REGISTER_BONUS_YET); + } + matchWinning(); + return new LottoGameInfo.MatchResult( + resultBoard + ); + } + + /** + * 수익률 관련 정보를 반환합니다. + * @return 본 계임의 수익률 계산에 필요한 정보들을 반환합니다. + * @see LottoGameInfo.RevenueInfo + */ + public LottoGameInfo.RevenueInfo checkRevenue() { + long totalReward = 0L; + for (Map.Entry eachResult: resultBoard.entrySet()) { + MatchResultType matchType = eachResult.getKey(); + totalReward += matchType.calculateReward(eachResult.getValue()); + } + return new LottoGameInfo.RevenueInfo( + lottoRepository.count() * LOTTO_PRICE, + totalReward + ); + } + + private void matchWinning() { + // 이전에 수행되었을 수도 있는 당첨 통계 결과를 중복 연산을 방지하고 초기화합니다. + initBoard(); + for (Lotto lotto : lottoRepository.lottos()) { + MatchResultType matchType = LottoMachine.match(lotto, winningLotto, winningBonus); + if (!matchType.equals(MatchResultType.BOOM)) { + Long currentPoint = resultBoard.get(matchType); + resultBoard.put(matchType, currentPoint + 1); + } + } + } + + /** 본 게임의 점수판을 초기화합니다. */ + private void initBoard() { + Map board = Map.of( + MatchResultType.MATCH_THREE, 0L, + MatchResultType.MATCH_FOUR, 0L, + MatchResultType.MATCH_FIVE, 0L, + MatchResultType.MATCH_FIVE_WITH_BONUS, 0L, + MatchResultType.MATCH_SIX, 0L + ); + this.resultBoard = new HashMap<>(board); + } +} diff --git a/yummygyudon/code/java-lotto/src/main/java/lotto/application/LottoGameInfo.java b/yummygyudon/code/java-lotto/src/main/java/lotto/application/LottoGameInfo.java new file mode 100644 index 0000000..11933e1 --- /dev/null +++ b/yummygyudon/code/java-lotto/src/main/java/lotto/application/LottoGameInfo.java @@ -0,0 +1,38 @@ +package lotto.application; + +import lotto.domain.Lotto; +import lotto.global.constant.enums.MatchResultType; + +import java.util.List; +import java.util.Map; + +public class LottoGameInfo { + + /** + * @param quantity 구매 수량 + * @param lottos 발행 로또 + */ + public record PurchaseStatus( + long quantity, + List lottos + ) {} + + /** + * @param resultBoard 로또 게임 점수 보드 + */ + public record MatchResult( + Map resultBoard + ) { + } + + /** + * @param inputAmount 투입 금액 + * @param totalReward 전체 상금액 + */ + public record RevenueInfo( + long inputAmount, + long totalReward + ) { + } + +} diff --git a/yummygyudon/code/java-lotto/src/main/java/lotto/application/LottoMachine.java b/yummygyudon/code/java-lotto/src/main/java/lotto/application/LottoMachine.java new file mode 100644 index 0000000..f47cd85 --- /dev/null +++ b/yummygyudon/code/java-lotto/src/main/java/lotto/application/LottoMachine.java @@ -0,0 +1,83 @@ +package lotto.application; + +import camp.nextstep.edu.missionutils.Randoms; +import lotto.domain.Lotto; +import lotto.global.constant.enums.MatchResultType; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +/** + * Application 계층의 LottoGame 객체에서만 사용할 수 있도록 최대 공개 범위는 default 입니다. + */ +public abstract class LottoMachine { + private static final int LOWER_LIMIT_NUMBER = 1; + private static final int UPPER_LIMIT_NUMBER = 45; + + private static final int MATCH_THREE = 3; + private static final int MATCH_FOUR = 4; + private static final int MATCH_FIVE = 5; + + private static final int NUMBER_QUANTITY = 6; + + /** 자동 발행 로또를 반환합니다. */ + static Lotto issueAutomaticLotto() { + List randomNumbers = Randoms.pickUniqueNumbersInRange( + LOWER_LIMIT_NUMBER, UPPER_LIMIT_NUMBER, NUMBER_QUANTITY + ); + List lottoNumbers = new ArrayList<>(randomNumbers); + lottoNumbers.sort(Integer::compareTo); + return new Lotto(lottoNumbers); + } + + /** 수동 발행 로또를 발행합니다. */ + static Lotto issueManualLotto(List manualNumbers) { + List lottoNumbers = new ArrayList<>(manualNumbers); + lottoNumbers.sort(Integer::compareTo); + return new Lotto(lottoNumbers); + } + + /** 로또 비교 결과를 반환합니다. */ + static MatchResultType match(Lotto lotto, Lotto winningLotto, int bonusNumber) { + if (isAllMatch(lotto, winningLotto)) { + return MatchResultType.MATCH_SIX; + } + int matchedNumberQuantity = countMatchedNumber(lotto, winningLotto); + boolean bonusIn = isBonusIn(lotto, bonusNumber); + MatchResultType matchResultType = decideMatchType(matchedNumberQuantity); + + if (Objects.equals(matchResultType, MatchResultType.MATCH_FIVE) && bonusIn) { + return MatchResultType.MATCH_FIVE_WITH_BONUS; + } + return matchResultType; + } + + private static boolean isAllMatch(Lotto lotto, Lotto winningLotto) { + return lotto.isSameWith(winningLotto); + } + + private static boolean isBonusIn(Lotto lotto, int bonusNumber) { + return lotto.isExist(bonusNumber); + } + + private static int countMatchedNumber(Lotto lotto, Lotto winningLotto) { + return (int) lotto.getLottoNumbers().stream() + .filter(number -> winningLotto.getLottoNumbers().contains(number)) + .count(); + } + + private static MatchResultType decideMatchType(int matchCount) { + if (matchCount == MATCH_THREE) { + return MatchResultType.MATCH_THREE; + } + if (matchCount == MATCH_FOUR) { + return MatchResultType.MATCH_FOUR; + } + if (matchCount == MATCH_FIVE) { + return MatchResultType.MATCH_FIVE; + } + return MatchResultType.BOOM; + } + +} diff --git a/yummygyudon/code/java-lotto/src/main/java/lotto/domain/Lotto.java b/yummygyudon/code/java-lotto/src/main/java/lotto/domain/Lotto.java new file mode 100644 index 0000000..f94984b --- /dev/null +++ b/yummygyudon/code/java-lotto/src/main/java/lotto/domain/Lotto.java @@ -0,0 +1,78 @@ +package lotto.domain; + +import lotto.domain.exception.LottoError; +import lotto.domain.exception.LottoException; +import lotto.global.constant.ConstValue; + +import java.util.Collections; +import java.util.List; + +public class Lotto { + private final List lottoNumbers; + + public Lotto(List lottoNumbers) { + validate(lottoNumbers); + this.lottoNumbers = lottoNumbers; + } + + private void validate(List lottoNumbers) { + checkSize(lottoNumbers); + checkRange(lottoNumbers); + checkDuplicate(lottoNumbers); + } + + // TODO: 추가 기능 구현 + /** 숫자의 갯수를 검사한다. */ + private void checkSize(List lottoNumbers) { + if (lottoNumbers.size() != ConstValue.LOTTO_NUMBER_QUANTITY) { + throw new LottoException(LottoError.UNAVAILABLE_NUMBER_QUANTITY); + } + } + + /** 숫자들의 범위가 유효한지 검사한다. */ + private void checkRange(List lottoNumbers) { + for (Integer number : lottoNumbers) { + if (number < ConstValue.LOTTO_NUMBER_LOWER_LIMIT || number > ConstValue.LOTTO_NUMBER_UPPER_LIMIT) { + throw new LottoException(LottoError.UNAVAILABLE_NUMBER_RANGE); + } + } + } + + /** 숫자 중복이 없는지 검사한다. */ + private void checkDuplicate(List lottoNumbers) { + long countUnique = lottoNumbers.stream().distinct().count(); + if (countUnique < lottoNumbers.size()) { + throw new LottoException(LottoError.DUPLICATED_NUMBER_EXIST); + } + } + + public List getLottoNumbers() { + return Collections.unmodifiableList(lottoNumbers); + } + + /** + * 1등 당첨 여부를 확인할 때 사용한다. + * + * @param otherLotto 비교 대상 로또 객체 + * @return 비교 대상 로또의 번호와 모두 일치하는지 + */ + public boolean isSameWith(Lotto otherLotto) { + for (Integer number : otherLotto.getLottoNumbers()) { + if (!isExist(number)) { + return false; + } + } + return true; + } + + /** + * 번호 포함 여부를 확인할 때 사용한다. + * + * @param number 번호 + * @return 번호가 본 로또의 번호속에 존재하는지 + * @see Lotto#isSameWith(Lotto) + */ + public boolean isExist(Integer number) { + return lottoNumbers.contains(number); + } +} diff --git a/yummygyudon/code/java-lotto/src/main/java/lotto/domain/exception/LottoError.java b/yummygyudon/code/java-lotto/src/main/java/lotto/domain/exception/LottoError.java new file mode 100644 index 0000000..46961f7 --- /dev/null +++ b/yummygyudon/code/java-lotto/src/main/java/lotto/domain/exception/LottoError.java @@ -0,0 +1,23 @@ +package lotto.domain.exception; + +import lotto.global.exception.base.LottoGameError; + +public enum LottoError implements LottoGameError { + + UNAVAILABLE_NUMBER_RANGE("유효한 범위가 아닙니다. 1부터 45사이의 정수를 입력해주세요."), + UNAVAILABLE_NUMBER_QUANTITY("숫자가 너무 많거나 적습니다. 6개의 정수만 입력해주세요."), + DUPLICATED_NUMBER_EXIST("중복되는 숫자가 숫자가 존재합니다. 서로 다른 정수 6개만 입력해주세요."), + ; + + private final String errorMessage; + + LottoError(String errorMessage) { + this.errorMessage = errorMessage; + } + + @Override + public String getErrorMessage() { + return errorMessage; + } + +} diff --git a/yummygyudon/code/java-lotto/src/main/java/lotto/domain/exception/LottoException.java b/yummygyudon/code/java-lotto/src/main/java/lotto/domain/exception/LottoException.java new file mode 100644 index 0000000..a5eb297 --- /dev/null +++ b/yummygyudon/code/java-lotto/src/main/java/lotto/domain/exception/LottoException.java @@ -0,0 +1,9 @@ +package lotto.domain.exception; + +import lotto.global.exception.base.LottoGameException; + +public class LottoException extends LottoGameException { + public LottoException(LottoError error) { + super(error); + } +} diff --git a/yummygyudon/code/java-lotto/src/main/java/lotto/domain/repository/LottoRepository.java b/yummygyudon/code/java-lotto/src/main/java/lotto/domain/repository/LottoRepository.java new file mode 100644 index 0000000..e8b068b --- /dev/null +++ b/yummygyudon/code/java-lotto/src/main/java/lotto/domain/repository/LottoRepository.java @@ -0,0 +1,29 @@ +package lotto.domain.repository; + +import lotto.domain.Lotto; + +import java.util.Collections; +import java.util.List; + +public class LottoRepository { + private final List lottos ; + + private LottoRepository(List lottos) { + this.lottos = lottos; + } + + public static LottoRepository createRepository(List lottos) { + return new LottoRepository(lottos); + } + + // Query - SELECT + public List lottos() { + return Collections.unmodifiableList(lottos); + } + + // Query - COUNT(*) + public long count() { + return lottos.size(); + } + +} diff --git a/yummygyudon/code/java-lotto/src/main/java/lotto/global/constant/ConstValue.java b/yummygyudon/code/java-lotto/src/main/java/lotto/global/constant/ConstValue.java new file mode 100644 index 0000000..0b030a5 --- /dev/null +++ b/yummygyudon/code/java-lotto/src/main/java/lotto/global/constant/ConstValue.java @@ -0,0 +1,8 @@ +package lotto.global.constant; + +public abstract class ConstValue { + public static final int LOTTO_NUMBER_QUANTITY = 6; + public static final int LOTTO_NUMBER_LOWER_LIMIT = 1; + public static final int LOTTO_NUMBER_UPPER_LIMIT = 45; + public static final int LOTTO_PRICE = 1_000; +} diff --git a/yummygyudon/code/java-lotto/src/main/java/lotto/global/constant/enums/MatchResultType.java b/yummygyudon/code/java-lotto/src/main/java/lotto/global/constant/enums/MatchResultType.java new file mode 100644 index 0000000..fefb169 --- /dev/null +++ b/yummygyudon/code/java-lotto/src/main/java/lotto/global/constant/enums/MatchResultType.java @@ -0,0 +1,31 @@ +package lotto.global.constant.enums; + +import java.util.function.Function; + +public enum MatchResultType { + BOOM("", value -> value * 0L), // 임의의 꽝 데이터 + MATCH_THREE("3개 일치 (5,000원) - %d개\n", value -> value * 5_000), + MATCH_FOUR("4개 일치 (50,000원) - %d개\n", value -> value * 50_000), + MATCH_FIVE("5개 일치 (1,500,000원) - %d개\n", value -> value * 1_500_000), + MATCH_FIVE_WITH_BONUS("5개 일치, 보너스 볼 일치 (30,000,000원) - %d개\n", value -> value * 30_000_000), + MATCH_SIX("6개 일치 (2,000,000,000원) - %d개", value -> value * 2_000_000_000); + + private final String format; + + private final Function expression; + + MatchResultType(String format, Function expression) { + this.format = format; + this.expression = expression; + } + + public String getFormat() { + return this.format; + } + + public long calculateReward(long count) { + return expression.apply(count); + } + + +} diff --git a/yummygyudon/code/java-lotto/src/main/java/lotto/global/exception/GlobalError.java b/yummygyudon/code/java-lotto/src/main/java/lotto/global/exception/GlobalError.java new file mode 100644 index 0000000..2fbd43b --- /dev/null +++ b/yummygyudon/code/java-lotto/src/main/java/lotto/global/exception/GlobalError.java @@ -0,0 +1,39 @@ +package lotto.global.exception; + +import lotto.global.exception.base.LottoGameError; + +public enum GlobalError implements LottoGameError { + + /** 입력값 요구 사항 에러 */ + // 잘못된 형식의 입력값 + BLANK_INPUT_ERROR("빈 값을 입력하셨습니다."), + NOT_ENOUGH_MONEY("1,000 원 이상을 입력해주세요."), + NOT_AVAILABLE_INTEGER("인식할 수 없는 정수 값입니다. 다시 입력해주세요."), + + // 유효하지 않은 입력값 + NOT_AVAILABLE_AMOUNT("유효한 구매 금액이 아닙니다. 1000원으로 나누어 떨어지는 1000원 이상의 금액을 입력해주세요."), + NOT_AVAILABLE_LOTTO_NUMBER("유효한 로또 숫자가 아닙니다. 1부터 45 사이의 정수만 입력해주세요."), + NOT_AVAILABLE_LOTTO_NUMBERS_PATTERN("잘못된 입력 형식입니다. 다시 입력해주세요."), + + + /** 잘못된 애플리케이션 메서드 호출 및 값 입력 */ + NOT_REGISTER_WINNING_YET("당첨 번호를 먼저 입력하세요."), + NOT_REGISTER_BONUS_YET("보너스 번호를 먼저 입력하세요."), + REGISTER_WINNING_YET("당첨 번호를 먼저 지불하세요."), + TOO_MANY_LOTTOS("너무 많은 로또를 구매하셨습니다. 구매 금액을 줄여주세요."), + ALREADY_REGISTER_WINNING("이미 당첨 번호와 보너스 번호가 등록되어 있습니다."), + ALREADY_EXIST_IN_WINNING_NUMBERS("이미 당첨 번호에 포함되어 있는 숫자입니다. 다른 숫자를 입력해주세요."), + ; + + private final String errorMessage; + + GlobalError(String errorMessage) { + this.errorMessage = errorMessage; + } + + @Override + public String getErrorMessage() { + return this.errorMessage; + } + +} diff --git a/yummygyudon/code/java-lotto/src/main/java/lotto/global/exception/GlobalException.java b/yummygyudon/code/java-lotto/src/main/java/lotto/global/exception/GlobalException.java new file mode 100644 index 0000000..2f4c400 --- /dev/null +++ b/yummygyudon/code/java-lotto/src/main/java/lotto/global/exception/GlobalException.java @@ -0,0 +1,10 @@ +package lotto.global.exception; + +import lotto.global.exception.base.LottoGameException; + +public class GlobalException extends LottoGameException { + + public GlobalException(GlobalError error) { + super(error); + } +} diff --git a/yummygyudon/code/java-lotto/src/main/java/lotto/global/exception/base/ErrorBase.java b/yummygyudon/code/java-lotto/src/main/java/lotto/global/exception/base/ErrorBase.java new file mode 100644 index 0000000..b587248 --- /dev/null +++ b/yummygyudon/code/java-lotto/src/main/java/lotto/global/exception/base/ErrorBase.java @@ -0,0 +1,7 @@ +package lotto.global.exception.base; + +public interface ErrorBase { + + String getErrorMessage(); + +} diff --git a/yummygyudon/code/java-lotto/src/main/java/lotto/global/exception/base/LottoGameError.java b/yummygyudon/code/java-lotto/src/main/java/lotto/global/exception/base/LottoGameError.java new file mode 100644 index 0000000..09afea0 --- /dev/null +++ b/yummygyudon/code/java-lotto/src/main/java/lotto/global/exception/base/LottoGameError.java @@ -0,0 +1,4 @@ +package lotto.global.exception.base; + +public interface LottoGameError extends ErrorBase{ +} diff --git a/yummygyudon/code/java-lotto/src/main/java/lotto/global/exception/base/LottoGameException.java b/yummygyudon/code/java-lotto/src/main/java/lotto/global/exception/base/LottoGameException.java new file mode 100644 index 0000000..26e2ce6 --- /dev/null +++ b/yummygyudon/code/java-lotto/src/main/java/lotto/global/exception/base/LottoGameException.java @@ -0,0 +1,10 @@ +package lotto.global.exception.base; + +public class LottoGameException extends IllegalArgumentException{ + private static final String ERROR_MESSAGE_HEADER = "[ERROR] "; + + public LottoGameException(ErrorBase error) { + super(ERROR_MESSAGE_HEADER + error.getErrorMessage()); + } + +} diff --git a/yummygyudon/code/java-lotto/src/main/java/lotto/global/util/channel/base/DefaultReader.java b/yummygyudon/code/java-lotto/src/main/java/lotto/global/util/channel/base/DefaultReader.java new file mode 100644 index 0000000..b790559 --- /dev/null +++ b/yummygyudon/code/java-lotto/src/main/java/lotto/global/util/channel/base/DefaultReader.java @@ -0,0 +1,7 @@ +package lotto.global.util.channel.base; + +public interface DefaultReader { + + String read(); + +} diff --git a/yummygyudon/code/java-lotto/src/main/java/lotto/global/util/channel/base/DefaultValidator.java b/yummygyudon/code/java-lotto/src/main/java/lotto/global/util/channel/base/DefaultValidator.java new file mode 100644 index 0000000..6eb102e --- /dev/null +++ b/yummygyudon/code/java-lotto/src/main/java/lotto/global/util/channel/base/DefaultValidator.java @@ -0,0 +1,15 @@ +package lotto.global.util.channel.base; + +import lotto.global.exception.GlobalError; +import lotto.global.exception.GlobalException; + +public abstract class DefaultValidator { + protected abstract boolean validate(String input); + + protected void checkIsBlank(String input) { + if (input.isBlank() || input.isEmpty()) { + throw new GlobalException(GlobalError.BLANK_INPUT_ERROR); + } + } + +} diff --git a/yummygyudon/code/java-lotto/src/main/java/lotto/global/util/channel/print/Printer.java b/yummygyudon/code/java-lotto/src/main/java/lotto/global/util/channel/print/Printer.java new file mode 100644 index 0000000..776c543 --- /dev/null +++ b/yummygyudon/code/java-lotto/src/main/java/lotto/global/util/channel/print/Printer.java @@ -0,0 +1,9 @@ +package lotto.global.util.channel.print; + +public abstract class Printer { + + public static void print(String message) { + System.out.println(message); + } + +} diff --git a/yummygyudon/code/java-lotto/src/main/java/lotto/global/util/channel/read/ReaderForBonus.java b/yummygyudon/code/java-lotto/src/main/java/lotto/global/util/channel/read/ReaderForBonus.java new file mode 100644 index 0000000..e8d6e9a --- /dev/null +++ b/yummygyudon/code/java-lotto/src/main/java/lotto/global/util/channel/read/ReaderForBonus.java @@ -0,0 +1,35 @@ +package lotto.global.util.channel.read; + +import camp.nextstep.edu.missionutils.Console; +import lotto.global.exception.GlobalException; +import lotto.global.util.channel.base.DefaultReader; +import lotto.global.util.channel.print.Printer; +import lotto.global.util.channel.validator.LottoNumberValidator; +import lotto.presentation.message.Ask; + +public class ReaderForBonus extends LottoNumberValidator implements DefaultReader { + + + @Override + public String read() { + String input; + do { + Printer.print(Ask.WINNING_BONUS_NUMBER); + input = Console.readLine(); + } while (!validate(input.trim())); + return input; + } + + @Override + protected boolean validate(String input) { + try { + checkIsBlank(input); + checkIsNumber(input); + checkIsAvailableLottoNumber(input); + return true; + } catch (GlobalException exception) { + Printer.print(exception.getMessage()); + return false; + } + } +} diff --git a/yummygyudon/code/java-lotto/src/main/java/lotto/global/util/channel/read/ReaderForPurchase.java b/yummygyudon/code/java-lotto/src/main/java/lotto/global/util/channel/read/ReaderForPurchase.java new file mode 100644 index 0000000..9dbffdb --- /dev/null +++ b/yummygyudon/code/java-lotto/src/main/java/lotto/global/util/channel/read/ReaderForPurchase.java @@ -0,0 +1,55 @@ +package lotto.global.util.channel.read; + +import camp.nextstep.edu.missionutils.Console; +import lotto.global.constant.ConstValue; +import lotto.global.exception.GlobalError; +import lotto.global.exception.GlobalException; +import lotto.global.util.channel.base.DefaultReader; +import lotto.global.util.channel.print.Printer; +import lotto.global.util.channel.validator.NumberValidator; +import lotto.presentation.message.Ask; + +public class ReaderForPurchase extends NumberValidator implements DefaultReader { + + + @Override + public String read() { + String input; + do { + Printer.print(Ask.PAYMENT_AMOUNT); + input = Console.readLine(); + } while (!validate(input)); + return input; + } + + @Override + protected boolean validate(String input) { + try { + checkIsBlank(input); + checkIsNumber(input); + checkIsEnoughMoney(input); + checkIsDivided(input); + return true; + } catch (GlobalException exception) { + Printer.print(exception.getMessage()); + return false; + } + } + + /** 1,000원 이하인지 확인한다. */ + private void checkIsEnoughMoney(String input) { + long amount = Long.parseLong(input); + if (amount < ConstValue.LOTTO_PRICE) { + throw new GlobalException(GlobalError.NOT_AVAILABLE_AMOUNT); + } + } + + + /** 1,000원으로 나누어 떨어지는지 확인한다. */ + private void checkIsDivided(String input) { + long amount = Long.parseLong(input); + if (!(amount % ConstValue.LOTTO_PRICE == 0)) { + throw new GlobalException(GlobalError.NOT_AVAILABLE_AMOUNT); + } + } +} diff --git a/yummygyudon/code/java-lotto/src/main/java/lotto/global/util/channel/read/ReaderForWinningLotto.java b/yummygyudon/code/java-lotto/src/main/java/lotto/global/util/channel/read/ReaderForWinningLotto.java new file mode 100644 index 0000000..113e700 --- /dev/null +++ b/yummygyudon/code/java-lotto/src/main/java/lotto/global/util/channel/read/ReaderForWinningLotto.java @@ -0,0 +1,58 @@ +package lotto.global.util.channel.read; + +import camp.nextstep.edu.missionutils.Console; +import lotto.global.exception.GlobalError; +import lotto.global.exception.GlobalException; +import lotto.global.util.channel.base.DefaultReader; +import lotto.global.util.channel.print.Printer; +import lotto.global.util.channel.validator.LottoNumberValidator; +import lotto.presentation.message.Ask; + +public class ReaderForWinningLotto extends LottoNumberValidator implements DefaultReader { + + private static final String AVAILABLE_SEPARATOR = ","; + // 숫자와 쉼표들 사이에는 공백은 허용되도록 패턴을 정의했습니다. + private static final String AVAILABLE_PATTERN = "^(\\d{0,2}[\\s]*,)([\\s]*\\d{0,2}[\\s]*,)+([\\s]*\\d{0,2}[\\s]*)"; + + + @Override + public String read() { + String input; + do { + Printer.print(Ask.WINNING_NUMBERS); + input = Console.readLine(); + } while (!validate(input)); + return input; + } + + @Override + protected boolean validate(String input) { + try { + checkIsBlank(input); + checkIsPatternAvailable(input); + checkIsEachNumberAvailable(input); + return true; + } catch (GlobalException exception) { + Printer.print(exception.getMessage()); + return false; + } + } + + /** 요구사항의 입력 패턴과 동일한지 검사합니다. */ + protected void checkIsPatternAvailable(String input) { + if (!input.matches(AVAILABLE_PATTERN)) { + throw new GlobalException(GlobalError.NOT_AVAILABLE_LOTTO_NUMBERS_PATTERN); + } + } + + /** 쉼표로 분리한 각각의 숫자들이 유효한지 검사합니다. */ + protected void checkIsEachNumberAvailable(String input) { + for (String numberInput : input.split(AVAILABLE_SEPARATOR)) { + // 순수 이름값에 대해서만 길이를 재기 위해 String.trim() 을 수행합니다. + numberInput = numberInput.trim(); + checkIsBlank(numberInput); + checkIsAvailableLottoNumber(numberInput); + } + + } +} diff --git a/yummygyudon/code/java-lotto/src/main/java/lotto/global/util/channel/validator/LottoNumberValidator.java b/yummygyudon/code/java-lotto/src/main/java/lotto/global/util/channel/validator/LottoNumberValidator.java new file mode 100644 index 0000000..7e00981 --- /dev/null +++ b/yummygyudon/code/java-lotto/src/main/java/lotto/global/util/channel/validator/LottoNumberValidator.java @@ -0,0 +1,27 @@ +package lotto.global.util.channel.validator; + +import lotto.global.constant.ConstValue; +import lotto.global.exception.GlobalError; +import lotto.global.exception.GlobalException; + +/** + * @see NumberValidator + */ +public abstract class LottoNumberValidator extends NumberValidator{ + + private static final String REGEX_NUMBER_QUANTITY = "^([1-9]{0})|(\\d{0,2})?";//"\\d{0,2}"; + + + /** 로또 번호 요구사항 준수여부를 검사합니다. */ + protected void checkIsAvailableLottoNumber(String numberInput) { + numberInput = numberInput.trim(); + checkIsNumber(numberInput); + if (!numberInput.matches(REGEX_NUMBER_QUANTITY)){ + throw new GlobalException(GlobalError.NOT_AVAILABLE_LOTTO_NUMBER); + } + int number = Integer.parseInt(numberInput.trim()); + if (number < ConstValue.LOTTO_NUMBER_LOWER_LIMIT || number > ConstValue.LOTTO_NUMBER_UPPER_LIMIT) { + throw new GlobalException(GlobalError.NOT_AVAILABLE_LOTTO_NUMBER); + } + } +} diff --git a/yummygyudon/code/java-lotto/src/main/java/lotto/global/util/channel/validator/NumberValidator.java b/yummygyudon/code/java-lotto/src/main/java/lotto/global/util/channel/validator/NumberValidator.java new file mode 100644 index 0000000..b2f98b9 --- /dev/null +++ b/yummygyudon/code/java-lotto/src/main/java/lotto/global/util/channel/validator/NumberValidator.java @@ -0,0 +1,34 @@ +package lotto.global.util.channel.validator; + +import lotto.global.exception.GlobalError; +import lotto.global.exception.GlobalException; +import lotto.global.util.channel.base.DefaultValidator; + +/** + * @see DefaultValidator + */ +public abstract class NumberValidator extends DefaultValidator { + + protected void checkIsNumber(String input) { + checkIsAvailableLong(input); + checkIsAvailableInteger(input); + } + + /** 지나치게 긴 숫자를 입력했을 경우를 대비하여 Long 형변환을 시도하여 검사합니다. */ + private void checkIsAvailableLong(String input) { + try { + Long.parseLong(input); + } catch (NumberFormatException e) { + throw new GlobalException(GlobalError.NOT_AVAILABLE_INTEGER); + } + } + + /** 올바른 형식의 정수를 입력했는지 검사합니다. */ + private void checkIsAvailableInteger(String input) { + try { + Integer.parseInt(input); + } catch (NumberFormatException e) { + throw new GlobalException(GlobalError.NOT_AVAILABLE_INTEGER); + } + } +} diff --git a/yummygyudon/code/java-lotto/src/main/java/lotto/presentation/LottoGameRunner.java b/yummygyudon/code/java-lotto/src/main/java/lotto/presentation/LottoGameRunner.java new file mode 100644 index 0000000..9ad594f --- /dev/null +++ b/yummygyudon/code/java-lotto/src/main/java/lotto/presentation/LottoGameRunner.java @@ -0,0 +1,101 @@ +package lotto.presentation; + +import lotto.application.LottoGame; +import lotto.application.LottoGameInfo; +import lotto.domain.exception.LottoException; +import lotto.global.exception.GlobalException; +import lotto.global.util.channel.print.Printer; +import lotto.global.util.channel.read.ReaderForBonus; +import lotto.global.util.channel.read.ReaderForPurchase; +import lotto.global.util.channel.read.ReaderForWinningLotto; +import lotto.presentation.mapper.GameResult; +import lotto.presentation.mapper.GameResultMapper; +import lotto.presentation.message.Notice; + +import java.util.Arrays; +import java.util.List; +import java.util.Objects; + +public class LottoGameRunner { + + private static LottoGameRunner RUNNER; + + private final ReaderForPurchase purchaseReader = new ReaderForPurchase(); + private final ReaderForWinningLotto winningLottoReader = new ReaderForWinningLotto(); + private final ReaderForBonus bonusReader = new ReaderForBonus(); + + private LottoGame lottoGame; + + private LottoGameRunner() { + } + + public static LottoGameRunner getRunner() { + if (Objects.isNull(RUNNER)) { + RUNNER = new LottoGameRunner(); + } + return RUNNER; + } + + public void run() { + purchase(); + + generateWinningLotto(); + + viewResult(); + } + + + private void purchase() { + // 입력한 구입금액에 따라 로또를 발행한다. + lottoGame = LottoGame.createLottoGame(receivePaymentAmount()); + LottoGameInfo.PurchaseStatus purchaseStatus = lottoGame.checkPurchaseStatus(); + + // 발행된 로또 결과를 출력한다. + GameResult.IssuedLottos issuedLottos = GameResultMapper.ofLottos(purchaseStatus); + Printer.print(issuedLottos.toResult()); + } + + private void generateWinningLotto() { + boolean isRegistered = false; + while (!isRegistered) { + try { + List winningNumbers = receiveWinningNumbers(); + lottoGame.registerWinningNumbers(winningNumbers); + int winningBonus = receiveWinningBonus(); + lottoGame.registerWinningBonus(winningBonus); + isRegistered = true; + } catch (GlobalException | LottoException exception) { + Printer.print(exception.getMessage()); + } + } + } + + private void viewResult() { + Printer.print(Notice.WINNING_STATISTICS); + + GameResult.WinningStatistic winningStatistic = GameResultMapper.ofGame(lottoGame.checkMatchResult()); + Printer.print(winningStatistic.toResult()); + + GameResult.RevenueRate revenueRate = GameResultMapper.ofGameResult(lottoGame.checkRevenue()); + Printer.print(revenueRate.toResult()); + } + + + private long receivePaymentAmount() { + String inputAmount = purchaseReader.read(); + return Long.parseLong(inputAmount); + } + + private List receiveWinningNumbers() { + String inputNumbers = winningLottoReader.read(); + return Arrays.stream(inputNumbers.split(",")). + map(inputNumber -> Integer.parseInt(inputNumber.trim())) + .toList(); + } + + private int receiveWinningBonus() { + String inputNumber = bonusReader.read(); + return Integer.parseInt(inputNumber); + } + +} diff --git a/yummygyudon/code/java-lotto/src/main/java/lotto/presentation/mapper/GameResult.java b/yummygyudon/code/java-lotto/src/main/java/lotto/presentation/mapper/GameResult.java new file mode 100644 index 0000000..7e91a0f --- /dev/null +++ b/yummygyudon/code/java-lotto/src/main/java/lotto/presentation/mapper/GameResult.java @@ -0,0 +1,114 @@ +package lotto.presentation.mapper; + +import lotto.application.LottoGameInfo; +import lotto.domain.Lotto; +import lotto.global.constant.enums.MatchResultType; + +import java.util.List; +import java.util.Map; +import java.util.StringJoiner; + +public abstract class GameResult { + + /** + * 출력 요구사항에 맞는 구매 현황 데이터로 변환합니다. + * @see Resultable + */ + public static class IssuedLottos implements Resultable { + private static final String PREFIX = "["; + private static final String SUFFIX = "]\n"; + private static final String DELIMITER = ", "; + private static final String PURCHASE_FORMAT = "%d개를 구매했습니다.\n"; + + private final LottoGameInfo.PurchaseStatus purchaseStatus; + + private IssuedLottos(LottoGameInfo.PurchaseStatus purchaseStatus) { + this.purchaseStatus = purchaseStatus; + } + public static IssuedLottos from(LottoGameInfo.PurchaseStatus purchaseStatus) { + return new IssuedLottos(purchaseStatus); + } + + @Override + public String toResult() { + StringBuilder resultBuilder = new StringBuilder(); + resultBuilder.append(String.format(PURCHASE_FORMAT, purchaseStatus.quantity())); + + for (Lotto lotto : purchaseStatus.lottos()) { + String eachLottoResult = convertToResult(lotto.getLottoNumbers()); + resultBuilder.append(eachLottoResult); + } + return resultBuilder.toString(); + } + + private String convertToResult(List numbers) { + StringJoiner numberJoiner = new StringJoiner(DELIMITER, PREFIX, SUFFIX); + for (Integer number : numbers) { + numberJoiner.add(String.valueOf(number)); + } + return numberJoiner.toString(); + } + } + + + /** + * 출력 요구사항에 맞는 당첨 통계 데이터로 변환합니다. + * @see Resultable + */ + public static class WinningStatistic implements Resultable { + + private final Map resultBoard; + + private WinningStatistic(Map resultBoard) { + this.resultBoard = resultBoard; + } + + public static WinningStatistic from(LottoGameInfo.MatchResult matchResult) { + return new WinningStatistic(matchResult.resultBoard()); + } + + @Override + public String toResult() { + StringBuilder builder = new StringBuilder(); + for (MatchResultType matchResultType : MatchResultType.values()) { + String result = String.format( matchResultType.getFormat(), resultBoard.get(matchResultType)); + builder.append(result); + } + return builder.toString(); + } + } + + + /** + * 출력 요구사항에 맞는 수익률 데이터로 변환합니다. + * @see Resultable + */ + public static class RevenueRate implements Resultable { + private static final String REVENUE_RATE_FORMAT = "총 수익률은 %.1f"; + private static final String RESULT_SUFFIX = "%입니다."; + + private final long inputAmount; + private final long totalReward; + + private RevenueRate(long inputAmount, long totalReward) { + this.inputAmount = inputAmount; + this.totalReward = totalReward; + } + + public static RevenueRate from(LottoGameInfo.RevenueInfo revenueInfo) { + return new RevenueRate(revenueInfo.inputAmount(), revenueInfo.totalReward()); + } + + + @Override + public String toResult() { + return String.format(REVENUE_RATE_FORMAT, calculateRate()) + RESULT_SUFFIX; + } + + private double calculateRate() { + double rate = (double) totalReward / inputAmount; + return Math.round(rate * 1000) / 10.0; + } + + } +} diff --git a/yummygyudon/code/java-lotto/src/main/java/lotto/presentation/mapper/GameResultMapper.java b/yummygyudon/code/java-lotto/src/main/java/lotto/presentation/mapper/GameResultMapper.java new file mode 100644 index 0000000..85a1e0f --- /dev/null +++ b/yummygyudon/code/java-lotto/src/main/java/lotto/presentation/mapper/GameResultMapper.java @@ -0,0 +1,18 @@ +package lotto.presentation.mapper; + +import lotto.application.LottoGameInfo; + +public abstract class GameResultMapper { + + public static GameResult.IssuedLottos ofLottos(LottoGameInfo.PurchaseStatus purchaseStatus) { + return GameResult.IssuedLottos.from(purchaseStatus); + } + + public static GameResult.WinningStatistic ofGame(LottoGameInfo.MatchResult matchResult) { + return GameResult.WinningStatistic.from(matchResult); + } + + public static GameResult.RevenueRate ofGameResult(LottoGameInfo.RevenueInfo revenueInfo) { + return GameResult.RevenueRate.from(revenueInfo); + } +} diff --git a/yummygyudon/code/java-lotto/src/main/java/lotto/presentation/mapper/Resultable.java b/yummygyudon/code/java-lotto/src/main/java/lotto/presentation/mapper/Resultable.java new file mode 100644 index 0000000..12e96a7 --- /dev/null +++ b/yummygyudon/code/java-lotto/src/main/java/lotto/presentation/mapper/Resultable.java @@ -0,0 +1,5 @@ +package lotto.presentation.mapper; + +public interface Resultable { + String toResult(); +} diff --git a/yummygyudon/code/java-lotto/src/main/java/lotto/presentation/message/Ask.java b/yummygyudon/code/java-lotto/src/main/java/lotto/presentation/message/Ask.java new file mode 100644 index 0000000..ae1c377 --- /dev/null +++ b/yummygyudon/code/java-lotto/src/main/java/lotto/presentation/message/Ask.java @@ -0,0 +1,10 @@ +package lotto.presentation.message; + +public abstract class Ask { + + public static final String PAYMENT_AMOUNT = "구입금액을 입력해 주세요."; + + public static final String WINNING_NUMBERS = "당청 번호를 입력해 주세요."; + + public static final String WINNING_BONUS_NUMBER = "보너스 번호를 입력해 주세요."; +} diff --git a/yummygyudon/code/java-lotto/src/main/java/lotto/presentation/message/Notice.java b/yummygyudon/code/java-lotto/src/main/java/lotto/presentation/message/Notice.java new file mode 100644 index 0000000..0f41cee --- /dev/null +++ b/yummygyudon/code/java-lotto/src/main/java/lotto/presentation/message/Notice.java @@ -0,0 +1,6 @@ +package lotto.presentation.message; + +public abstract class Notice { + public static final String WINNING_STATISTICS = "당첨 통계\n---"; + +} diff --git a/yummygyudon/code/java-lotto/src/test/java/lotto/ApplicationTest.java b/yummygyudon/code/java-lotto/src/test/java/lotto/ApplicationTest.java new file mode 100644 index 0000000..a15c7d1 --- /dev/null +++ b/yummygyudon/code/java-lotto/src/test/java/lotto/ApplicationTest.java @@ -0,0 +1,61 @@ +package lotto; + +import camp.nextstep.edu.missionutils.test.NsTest; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static camp.nextstep.edu.missionutils.test.Assertions.assertRandomUniqueNumbersInRangeTest; +import static camp.nextstep.edu.missionutils.test.Assertions.assertSimpleTest; +import static org.assertj.core.api.Assertions.assertThat; + +class ApplicationTest extends NsTest { + private static final String ERROR_MESSAGE = "[ERROR]"; + + @Test + void 기능_테스트() { + assertRandomUniqueNumbersInRangeTest( + () -> { + run("8000", "1,2,3,4,5,6", "7"); + assertThat(output()).contains( + "8개를 구매했습니다.", + "[8, 21, 23, 41, 42, 43]", + "[3, 5, 11, 16, 32, 38]", + "[7, 11, 16, 35, 36, 44]", + "[1, 8, 11, 31, 41, 42]", + "[13, 14, 16, 38, 42, 45]", + "[7, 11, 30, 40, 42, 43]", + "[2, 13, 22, 32, 38, 45]", + "[1, 3, 5, 14, 22, 45]", + "3개 일치 (5,000원) - 1개", + "4개 일치 (50,000원) - 0개", + "5개 일치 (1,500,000원) - 0개", + "5개 일치, 보너스 볼 일치 (30,000,000원) - 0개", + "6개 일치 (2,000,000,000원) - 0개", + "총 수익률은 62.5%입니다." + ); + }, + List.of(8, 21, 23, 41, 42, 43), + List.of(3, 5, 11, 16, 32, 38), + List.of(7, 11, 16, 35, 36, 44), + List.of(1, 8, 11, 31, 41, 42), + List.of(13, 14, 16, 38, 42, 45), + List.of(7, 11, 30, 40, 42, 43), + List.of(2, 13, 22, 32, 38, 45), + List.of(1, 3, 5, 14, 22, 45) + ); + } + + @Test + void 예외_테스트() { + assertSimpleTest(() -> { + runException("1000j"); + assertThat(output()).contains(ERROR_MESSAGE); + }); + } + + @Override + public void runMain() { + Application.main(new String[]{}); + } +} diff --git a/yummygyudon/code/java-lotto/src/test/java/lotto/LottoTest.java b/yummygyudon/code/java-lotto/src/test/java/lotto/LottoTest.java new file mode 100644 index 0000000..ce92ef4 --- /dev/null +++ b/yummygyudon/code/java-lotto/src/test/java/lotto/LottoTest.java @@ -0,0 +1,41 @@ +package lotto; + +import lotto.domain.Lotto; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class LottoTest { + @DisplayName("로또 번호의 개수가 6개가 넘어가면 예외가 발생한다.") + @Test + void createLottoByOverSize() { + assertThatThrownBy(() -> new Lotto(List.of(1, 2, 3, 4, 5, 6, 7))) + .isInstanceOf(IllegalArgumentException.class); + } + + @DisplayName("로또 번호에 중복된 숫자가 있으면 예외가 발생한다.") + @Test + void createLottoByDuplicatedNumber() { + // TODO: 이 테스트가 통과할 수 있게 구현 코드 작성 + assertThatThrownBy(() -> new Lotto(List.of(1, 2, 3, 4, 5, 5))) + .isInstanceOf(IllegalArgumentException.class); + } + + // 아래에 추가 테스트 작성 가능 + @DisplayName("로또 번호에 1보다 작은 숫자가 있으면 예외가 발생한다.") + @Test + void createLottoByBiggerThanLowerLimit() { + assertThatThrownBy(() -> new Lotto(List.of(0, 2, 3, 4, 5, 6))) + .isInstanceOf(IllegalArgumentException.class); + } + @DisplayName("로또 번호에 45보다 큰 숫자가 있으면 예외가 발생한다.") + @Test + void createLottoBySmallThanUpperLimit() { + assertThatThrownBy(() -> new Lotto(List.of(1, 2, 3, 4, 5, 46))) + .isInstanceOf(IllegalArgumentException.class); + } + +} \ No newline at end of file diff --git a/yummygyudon/code/java-lotto/src/test/java/lotto/application/LottoMachineTest.java b/yummygyudon/code/java-lotto/src/test/java/lotto/application/LottoMachineTest.java new file mode 100644 index 0000000..45b3426 --- /dev/null +++ b/yummygyudon/code/java-lotto/src/test/java/lotto/application/LottoMachineTest.java @@ -0,0 +1,126 @@ +package lotto.application; + +import lotto.domain.Lotto; +import lotto.global.constant.enums.MatchResultType; +import org.assertj.core.api.Assertions; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.RepeatedTest; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.ArrayList; +import java.util.List; +import java.util.stream.Stream; + + +@DisplayName("LottoMachine 테스트") +class LottoMachineTest { + + @DisplayName("자동 로또 발행 테스트") + @RepeatedTest( + value = 1_000, + name = "{displayName}, {currentRepetition}/{totalRepetitions}" + ) + public void Case_Auto_Lotto() { + Lotto autoLotto = LottoMachine.issueAutomaticLotto(); + Assertions.assertThat(autoLotto).isInstanceOf(Lotto.class); + } + + @DisplayName("수동 로또 발행 테스트") + @ParameterizedTest(name = "{index} : 입략 로또 번호 = {0}") + @MethodSource("argumentsForManualLotto") + public void Case_Manual_Lotto(List lottoNumbers) { + // 무작위 정렬 숫자 리스트를 통해 수동 발행한 로또 생성 + Lotto autoLotto = LottoMachine.issueManualLotto(lottoNumbers); + + // 같은 번호 구성으로 정렬하여 새로운 로또를 생성 + List sameNumbers = new ArrayList<>(lottoNumbers); + sameNumbers.sort(Integer::compareTo); + Lotto sameLotto = new Lotto(sameNumbers); + + Assertions.assertThat(autoLotto).isInstanceOf(Lotto.class); + // 무작위 정렬 숫자를 넣더라도 잘 정렬되어 로또가 생성된 것인지 확인 + Assertions.assertThat(sameLotto).usingRecursiveComparison().isEqualTo(autoLotto); + } + private static Stream argumentsForManualLotto() { + return Stream.of( + Arguments.of(List.of(1, 2, 3, 4, 5, 6)), + Arguments.of(List.of(40, 41, 42, 43, 44, 45)), + Arguments.of(List.of(11, 22, 33, 44, 1, 2)), + Arguments.of(List.of(45, 1, 14, 29, 23, 17)) + ); + } + + @DisplayName("로또 비교 테스트") + @ParameterizedTest(name = "{index} : Lotto_A Numbers = {0} & Lotto_B Numbers = {1}, Expected = {2}") + @MethodSource("argumentsForComparingLottos") + public void Case_Compare_And_Matching_Lotto( + List numbersOfLottoA, + List numbersOfLottoB, + int bonusNumber, + MatchResultType expectedMatchType + ) { + Lotto lottaA = new Lotto(numbersOfLottoA); + Lotto lottoB = new Lotto(numbersOfLottoB); + + Assertions.assertThat(LottoMachine.match(lottaA, lottoB, bonusNumber)) + .isEqualTo(expectedMatchType); + } + private static Stream argumentsForComparingLottos() { + return Stream.of( + Arguments.of( + List.of(1, 2, 3, 4, 5, 6), + List.of(1, 2, 3, 7, 8, 9), + 45, + MatchResultType.MATCH_THREE + ), + Arguments.of( + List.of(1, 2, 3, 4, 5, 6), + List.of(1, 2, 3, 4, 7, 8), + 45, + MatchResultType.MATCH_FOUR + ), + Arguments.of( + List.of(1, 2, 3, 4, 5, 6), + List.of(1, 2, 3, 4, 5, 7), + 45, + MatchResultType.MATCH_FIVE + ), + Arguments.of( + List.of(1, 2, 3, 4, 5, 6), + List.of(1, 2, 3, 4, 5, 7), + 6, + MatchResultType.MATCH_FIVE_WITH_BONUS + ), + Arguments.of( + List.of(1, 2, 3, 4, 5, 6), + List.of(1, 2, 3, 4, 5, 6), + 45, + MatchResultType.MATCH_SIX + ), + // 0개가 일치한 경우 + Arguments.of( + List.of(1, 2, 3, 4, 5, 6), + List.of(7, 8, 9, 10, 11, 12), + 45, + MatchResultType.BOOM + ), + // 1개가 일치한 경우 + Arguments.of( + List.of(1, 2, 3, 4, 5, 6), + List.of(1, 7, 8, 9, 10, 11), + 45, + MatchResultType.BOOM + ), + // 2개가 일치한 경우 + Arguments.of( + List.of(1, 2, 3, 4, 5, 6), + List.of(1, 2, 7, 8, 9, 10), + 45, + MatchResultType.BOOM + ) + ); + } + +} \ No newline at end of file diff --git a/yummygyudon/code/java-lotto/src/test/java/lotto/global/util/channel/read/ReaderForBonusTest.java b/yummygyudon/code/java-lotto/src/test/java/lotto/global/util/channel/read/ReaderForBonusTest.java new file mode 100644 index 0000000..be6b729 --- /dev/null +++ b/yummygyudon/code/java-lotto/src/test/java/lotto/global/util/channel/read/ReaderForBonusTest.java @@ -0,0 +1,104 @@ +package lotto.global.util.channel.read; + +import camp.nextstep.edu.missionutils.test.NsTest; +import lotto.Application; +import lotto.global.exception.GlobalError; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static camp.nextstep.edu.missionutils.test.Assertions.assertSimpleTest; +import static org.assertj.core.api.Assertions.*; + +@DisplayName("보너스 번호 입력기 테스트") +class ReaderForBonusTest extends NsTest { + + private static final String AVAILABLE_AMOUNT_INPUT = "8000"; + private static final String AVAILABLE_WINNING_LOTTO_NUMBERS_INPUT = "1,2,3,4,5,6"; + + @DisplayName("빈 값을 입력한 경우") + @ParameterizedTest(name = "{index} 번째 검사값 = {0}") + @ValueSource(strings = { + "\n", " ", " ", " " + }) + void Is_Expected_Validation_About_Blank(String invalidBonusNumberInput) { + assertSimpleTest(() -> { + runException( + AVAILABLE_AMOUNT_INPUT, + AVAILABLE_WINNING_LOTTO_NUMBERS_INPUT, + invalidBonusNumberInput + ); + assertThat(output()).contains(GlobalError.BLANK_INPUT_ERROR.getErrorMessage()); + }); + } + + @DisplayName("한글/영어/특수기호를 포함하고 있는 경우") + @ParameterizedTest(name = "{index} 번째 검사값 = {0}") + @ValueSource(strings = { + "ㄱㄴㄷ가나다", "abcABC", ".,/%$#^", "1234567890ㅑㅑ" + }) + void Is_Expected_Validation_About_Not_Numeric(String invalidBonusNumberInput) { + assertSimpleTest(() -> { + runException( + AVAILABLE_AMOUNT_INPUT, + AVAILABLE_WINNING_LOTTO_NUMBERS_INPUT, + invalidBonusNumberInput + ); + assertThat(output()).contains(GlobalError.NOT_AVAILABLE_INTEGER.getErrorMessage()); + }); + } + + @DisplayName("0만 입력하거나 0으로 시작되는 잘못된 형식의 숫자를 입력 경우") + @ParameterizedTest(name = "{index} 번째 검사값 = {0}") + @ValueSource(strings = { + "00700", "0", "00", "01234567890" + }) + void Is_Expected_Validation_About_Invalid_Number(String invalidBonusNumberInput) { + assertSimpleTest(() -> { + runException( + AVAILABLE_AMOUNT_INPUT, + AVAILABLE_WINNING_LOTTO_NUMBERS_INPUT, + invalidBonusNumberInput + ); + assertThat(output()).contains(GlobalError.NOT_AVAILABLE_LOTTO_NUMBER.getErrorMessage()); + }); + } + + @DisplayName("유효하지 않은 로또 번호를 입력한 경우") + @ParameterizedTest(name = "{index} 번째 검사값 = {0}") + @ValueSource(strings = { + "46", "-1", "99", "1234567890" + }) + void Is_Expected_Validation_About_Invalid_Lotto_Number(String invalidBonusNumberInput) { + assertSimpleTest(() -> { + runException( + AVAILABLE_AMOUNT_INPUT, + AVAILABLE_WINNING_LOTTO_NUMBERS_INPUT, + invalidBonusNumberInput + ); + assertThat(output()).contains(GlobalError.NOT_AVAILABLE_LOTTO_NUMBER.getErrorMessage()); + }); + } + + @DisplayName("당첨 로또 번호 중 하나와 동일한 번호를 입력한 경우") + @ParameterizedTest(name = "{index} 번째 검사값 = {0}") + @ValueSource(strings = { + "1", "2", "3", "4", "5", "6" + }) + void Is_Expected_Validation_About_Alredy_Exist_In_Winning_Number(String invalidBonusNumberInput) { + assertSimpleTest(() -> { + runException( + AVAILABLE_AMOUNT_INPUT, + AVAILABLE_WINNING_LOTTO_NUMBERS_INPUT, + invalidBonusNumberInput + ); + assertThat(output()).contains(GlobalError.ALREADY_EXIST_IN_WINNING_NUMBERS.getErrorMessage()); + }); + } + + @Override + protected void runMain() { + Application.main(new String[]{}); + } + +} \ No newline at end of file diff --git a/yummygyudon/code/java-lotto/src/test/java/lotto/global/util/channel/read/ReaderForPurchaseTest.java b/yummygyudon/code/java-lotto/src/test/java/lotto/global/util/channel/read/ReaderForPurchaseTest.java new file mode 100644 index 0000000..00b2c25 --- /dev/null +++ b/yummygyudon/code/java-lotto/src/test/java/lotto/global/util/channel/read/ReaderForPurchaseTest.java @@ -0,0 +1,126 @@ +package lotto.global.util.channel.read; + +import camp.nextstep.edu.missionutils.test.NsTest; +import lotto.Application; +import lotto.global.exception.GlobalError; +import lotto.presentation.message.Ask; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static camp.nextstep.edu.missionutils.test.Assertions.assertSimpleTest; +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("구매 금액 입력기 테스트") +class ReaderForPurchaseTest extends NsTest { + private static final String PROCESS_STOP = "END"; + + @DisplayName("빈 값을 입력한 경우") + @ParameterizedTest(name = "{index} 번째 검사값 = {0}") + @ValueSource(strings = { + "\n", " ", " ", " " + }) + void Is_Expected_Validation_About_Blank(String invalidAmountInput) { + assertSimpleTest(() -> { + runException( + invalidAmountInput + ); + assertThat(output()).contains(GlobalError.BLANK_INPUT_ERROR.getErrorMessage()); + }); + } + + @DisplayName("0만 입력하거나 0으로 시작되는 잘못된 형식의 숫자를 입력 경우") + @ParameterizedTest(name = "{index} 번째 검사값 = {0}") + @ValueSource(strings = { + "00700", "0", "00", "01234567890" + }) + void Is_Expected_Validation_About_Invalid_Number(String invalidAmountInput) { + assertSimpleTest(() -> { + runException( + invalidAmountInput + ); + assertThat(output()).contains(GlobalError.NOT_AVAILABLE_AMOUNT.getErrorMessage()); + }); + } + @DisplayName("1000보다 작은 숫자를 입력한 경우") + @ParameterizedTest(name = "{index} 번째 검사값 = {0}") + @ValueSource(strings = { + "900", "999", "1", "100" + }) + void Is_Expected_Validation_About_Not_Enough_Money(String invalidAmountInput) { + assertSimpleTest(() -> { + runException( + invalidAmountInput + ); + assertThat(output()).contains(GlobalError.NOT_AVAILABLE_AMOUNT.getErrorMessage()); + }); + } + + @DisplayName("1000으로 나누어 떨어지지 않는 숫자를 입력한 경우") + @ParameterizedTest(name = "{index} 번째 검사값 = {0}") + @ValueSource(strings = { + "1234", "1000001", "999999", "1234500" + }) + void Is_Expected_Validation_About_Impossible_Divide(String invalidAmountInput) { + assertSimpleTest(() -> { + runException( + invalidAmountInput + ); + assertThat(output()).contains(GlobalError.NOT_AVAILABLE_AMOUNT.getErrorMessage()); + }); + } + + @DisplayName("한글/영어/특수기호를 포함하고 있는 경우") + @ParameterizedTest(name = "{index} 번째 검사값 = {0}") + @ValueSource(strings = { + "ㄱㄴㄷ가나다", "abcABC", ".,/%$#^", "1234567890ㅑㅑ" + }) + void Is_Expected_Validation_About_Not_Numeric(String invalidAmountInput) { + assertSimpleTest(() -> { + runException( + invalidAmountInput + ); + assertThat(output()).contains(GlobalError.NOT_AVAILABLE_INTEGER.getErrorMessage()); + }); + } + @DisplayName("너무 큰 정수값을 입력한 경우") + @ParameterizedTest(name = "{index} 번째 검사값 = {0}") + @ValueSource( + strings = { + "10000000000000000000", + "9999999999999999999999999" + } + ) + void Is_Expected_Validation_About_Long(String invalidAmountInput) { + assertSimpleTest(() -> { + runException( + invalidAmountInput + ); + assertThat(output()).contains( + GlobalError.NOT_AVAILABLE_INTEGER.getErrorMessage() + ); + }); + } + + @DisplayName("올바른 입력값을 입력한 경우") + @ParameterizedTest(name = "{index} 번째 검사값 = {0}") + @ValueSource(strings = { + "1000", "20000", "300000", "100000" + }) + void Is_Expected_Validation_About_Available(String availableAmountInput) { + assertSimpleTest(() -> { + runException( + availableAmountInput, + PROCESS_STOP + ); + // 다음 단계로 넘어갔는지 확인하는 단계 + assertThat(output()).contains(Ask.WINNING_NUMBERS); + }); + } + + + @Override + protected void runMain() { + Application.main(new String[]{}); + } +} \ No newline at end of file diff --git a/yummygyudon/code/java-lotto/src/test/java/lotto/global/util/channel/read/ReaderForWinningLottoTest.java b/yummygyudon/code/java-lotto/src/test/java/lotto/global/util/channel/read/ReaderForWinningLottoTest.java new file mode 100644 index 0000000..bf93835 --- /dev/null +++ b/yummygyudon/code/java-lotto/src/test/java/lotto/global/util/channel/read/ReaderForWinningLottoTest.java @@ -0,0 +1,115 @@ +package lotto.global.util.channel.read; + +import camp.nextstep.edu.missionutils.test.NsTest; +import lotto.Application; +import lotto.global.exception.GlobalError; +import lotto.presentation.message.Ask; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +import static camp.nextstep.edu.missionutils.test.Assertions.assertSimpleTest; +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("로또 번호 입력기 테스트") +class ReaderForWinningLottoTest extends NsTest { + + private static final String AVAILABLE_AMOUNT_INPUT = "8000"; + private static final String PROCESS_STOP = "END"; + + @DisplayName("빈 값을 입력한 경우") + @ParameterizedTest(name = "{index} 번째 검사값 = {0}") + @ValueSource(strings = { + "\n", " ", " ", " " + }) + void Is_Expected_Validation_About_Blank(String invalidNumbersInput) { + assertSimpleTest(() -> { + runException( + AVAILABLE_AMOUNT_INPUT, + invalidNumbersInput + ); + assertThat(output()).contains(GlobalError.BLANK_INPUT_ERROR.getErrorMessage()); + }); + } + + @DisplayName("한글/영어/특수기호를 포함하고 있는 경우") + @ParameterizedTest(name = "{index} 번째 검사값 = {0}") + @ValueSource(strings = { + "a,b,c,d,e,f", "abcABC", ".,/%$#^", "1234567890ㅑㅑ" + }) + void Is_Expected_Validation_About_Not_Numeric(String invalidNumbersInput) { + assertSimpleTest(() -> { + runException( + AVAILABLE_AMOUNT_INPUT, + invalidNumbersInput + ); + assertThat(output()).contains(GlobalError.NOT_AVAILABLE_LOTTO_NUMBERS_PATTERN.getErrorMessage()); + }); + } + + @DisplayName("구분자를 쓰지 않은 경우") + @ParameterizedTest(name = "{index} 번째 검사값 = {0}") + @ValueSource(strings = { + "00700", "0", "00", "01234567890", "8000", "123456" + }) + void Is_Expected_Validation_About_Invalid_Number(String invalidNumbersInput) { + assertSimpleTest(() -> { + runException( + AVAILABLE_AMOUNT_INPUT, + invalidNumbersInput + ); + assertThat(output()).contains(GlobalError.NOT_AVAILABLE_LOTTO_NUMBERS_PATTERN.getErrorMessage()); + }); + } + @DisplayName("유효한 구분자가 아닌 기호를 쓰는 경우") + @ParameterizedTest(name = "{index} 번째 검사값 = {0}") + @ValueSource(strings = { + "1*2(3)4^5%6", "1/2/3/4/5/6", "1+2+3+4+5+6", "1-2-3-4-5-6", "1&2&3&4&5&6" + }) + void Is_Expected_Validation_About_Invalid_Separator(String invalidNumbersInput) { + assertSimpleTest(() -> { + runException( + AVAILABLE_AMOUNT_INPUT, + invalidNumbersInput + ); + assertThat(output()).contains(GlobalError.NOT_AVAILABLE_LOTTO_NUMBERS_PATTERN.getErrorMessage()); + }); + } + + @DisplayName("유효하지 않은 숫자들 가진 경우") + @ParameterizedTest(name = "{index} 번째 검사값 = {0}") + @ValueSource(strings = { + "1, 2, 3, 4, 5, 66", "11, 22, 33, 44, 55, 66", "0, 1, 2, 3, 4, 5" + }) + void Is_Expected_Validation_About_Invalid_Name(String invalidNumbersInput) { + assertSimpleTest(() -> { + runException( + AVAILABLE_AMOUNT_INPUT, + invalidNumbersInput + ); + assertThat(output()).contains(GlobalError.NOT_AVAILABLE_LOTTO_NUMBER.getErrorMessage()); + }); + } + + @DisplayName("올바른 입력값을 입력한 경우") + @ParameterizedTest(name = "{index} 번째 검사값 = {0}") + @ValueSource(strings = { + "1,2,3,4,5,6", "1, 11, 13, 15, 34, 43", "11,22, 26 ,33,44 ,45" + }) + void Is_Expected_Validation_About_Available(String availableAmountInput) { + assertSimpleTest(() -> { + runException( + AVAILABLE_AMOUNT_INPUT, + availableAmountInput, + PROCESS_STOP + ); + // 다음 단계로 넘어갔는지 확인하는 단계 + assertThat(output()).contains(Ask.WINNING_BONUS_NUMBER); + }); + } + + @Override + protected void runMain() { + Application.main(new String[]{}); + } +} \ No newline at end of file diff --git a/yummygyudon/code/java-menu/.gitignore b/yummygyudon/code/java-menu/.gitignore new file mode 100644 index 0000000..6c01878 --- /dev/null +++ b/yummygyudon/code/java-menu/.gitignore @@ -0,0 +1,32 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/** +!**/src/test/** + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/yummygyudon/code/java-menu/build.gradle b/yummygyudon/code/java-menu/build.gradle new file mode 100644 index 0000000..15f3352 --- /dev/null +++ b/yummygyudon/code/java-menu/build.gradle @@ -0,0 +1,25 @@ +plugins { + id 'java' +} + +group 'camp.nextstep.edu' +version '1.0-SNAPSHOT' + +repositories { + mavenCentral() + maven { url 'https://jitpack.io' } +} + +dependencies { + implementation 'com.github.woowacourse-projects:mission-utils:1.1.0' +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +test { + useJUnitPlatform() +} diff --git a/yummygyudon/code/java-menu/gradle/wrapper/gradle-wrapper.jar b/yummygyudon/code/java-menu/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/yummygyudon/code/java-menu/gradle/wrapper/gradle-wrapper.jar differ diff --git a/yummygyudon/code/java-menu/gradle/wrapper/gradle-wrapper.properties b/yummygyudon/code/java-menu/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ae04661 --- /dev/null +++ b/yummygyudon/code/java-menu/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/yummygyudon/code/java-menu/gradlew b/yummygyudon/code/java-menu/gradlew new file mode 100755 index 0000000..a69d9cb --- /dev/null +++ b/yummygyudon/code/java-menu/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/yummygyudon/code/java-menu/gradlew.bat b/yummygyudon/code/java-menu/gradlew.bat new file mode 100644 index 0000000..f127cfd --- /dev/null +++ b/yummygyudon/code/java-menu/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/yummygyudon/code/java-menu/settings.gradle b/yummygyudon/code/java-menu/settings.gradle new file mode 100644 index 0000000..d4f1bed --- /dev/null +++ b/yummygyudon/code/java-menu/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'java-menu' + diff --git a/yummygyudon/code/java-menu/src/main/java/menu/Application.java b/yummygyudon/code/java-menu/src/main/java/menu/Application.java new file mode 100644 index 0000000..f00f456 --- /dev/null +++ b/yummygyudon/code/java-menu/src/main/java/menu/Application.java @@ -0,0 +1,11 @@ +package menu; + +import menu.presentation.MenuPlanner; + +public class Application { + public static void main(String[] args) { + // TODO: 프로그램 구현 + MenuPlanner menuPlanner = new MenuPlanner(); + menuPlanner.startPlan(); + } +} diff --git a/yummygyudon/code/java-menu/src/main/java/menu/application/CategoryService.java b/yummygyudon/code/java-menu/src/main/java/menu/application/CategoryService.java new file mode 100644 index 0000000..a78df5f --- /dev/null +++ b/yummygyudon/code/java-menu/src/main/java/menu/application/CategoryService.java @@ -0,0 +1,14 @@ +package menu.application; + +import camp.nextstep.edu.missionutils.Randoms; +import menu.domain.repository.CategoryRepository; + +import java.util.Map; + +class CategoryService { + + public String getRandomCategory() { + Map categories = CategoryRepository.categories(); + return categories.get(Randoms.pickNumberInRange(1, 5)); + } +} diff --git a/yummygyudon/code/java-menu/src/main/java/menu/application/MenuPlanService.java b/yummygyudon/code/java-menu/src/main/java/menu/application/MenuPlanService.java new file mode 100644 index 0000000..00d043d --- /dev/null +++ b/yummygyudon/code/java-menu/src/main/java/menu/application/MenuPlanService.java @@ -0,0 +1,90 @@ +package menu.application; + +import menu.domain.Coach; +import menu.domain.repository.CoachRepository; +import menu.global.constant.Standard; + +import java.util.ArrayList; +import java.util.List; + +public class MenuPlanService { + private final MenuService menuService; + private final CategoryService categoryService; + + public MenuPlanService(List coachNames) { + CoachRepository.init(coachNames); + this.menuService = new MenuService(); + this.categoryService = new CategoryService(); + } + + public List getCoachNames() { + return CoachRepository.coaches().stream() + .map(Coach::getName) + .toList(); + } + + public void registerImpossibleMenusOf(String coachName, List menus) { + menuService.checkIsMenuExist(menus); + for (Coach coach : CoachRepository.coaches()) { + if (coach.getName().equals(coachName)) { + coach.changeImpossibleMenus(menus); + break; + } + } + } + + public PlannerInfo.WeeklyCategory planCategoryOfWeek() { + List randomCategories = new ArrayList<>(); + while (randomCategories.size() < Standard.DATE_LIMIT_FOR_PLAN) { + String randomCategory = categoryService.getRandomCategory(); + if (isTooMuchCategoryDuplicate(randomCategory, randomCategories)) { + randomCategories.add(randomCategory); + } + } + return PlannerInfo.WeeklyCategory.of(randomCategories); + } + private boolean isTooMuchCategoryDuplicate(String targetCategory, List existCategories) { + long numberOfSameCategory = existCategories.stream() + .filter(category -> category.equals(targetCategory)) + .count(); + return numberOfSameCategory < 2; + } + + public void planMenuForAllCoachOfWeek(PlannerInfo.WeeklyCategory weeklyCategory) { + for (String category : weeklyCategory.getCategories()) { + for (Coach coach : CoachRepository.coaches()) { + planCoachMenuOfDay(coach, category); + } + } + } + + + + + private void planCoachMenuOfDay(Coach coach, String categoryOfDay) { + String menu; + do { + menu = planCoachMenu(coach, categoryOfDay); + } while (isMenuDuplicate(menu, coach.getMenus())); + coach.addMenu(menu); + } + private String planCoachMenu(Coach coach, String category) { + String randomMenu; + do { + randomMenu = menuService.pickRandomMenuOf(category); + } while (!coach.isPossibleToEat(randomMenu)); + return randomMenu; + } + private boolean isMenuDuplicate(String targetMenu, List existMenus) { + return existMenus.contains(targetMenu); + } + + public List makeWeeklyMenuBoard() { + List planResult = new ArrayList<>(); + for (Coach coach : CoachRepository.coaches()) { + PlannerInfo.CoachMenu coachMenu = PlannerInfo.CoachMenu.of(coach.getName(), coach.getMenus()); + planResult.add(coachMenu); + } + return planResult; + } +} diff --git a/yummygyudon/code/java-menu/src/main/java/menu/application/MenuService.java b/yummygyudon/code/java-menu/src/main/java/menu/application/MenuService.java new file mode 100644 index 0000000..e6f14c1 --- /dev/null +++ b/yummygyudon/code/java-menu/src/main/java/menu/application/MenuService.java @@ -0,0 +1,24 @@ +package menu.application; + +import camp.nextstep.edu.missionutils.Randoms; +import menu.domain.exception.MenuError; +import menu.domain.exception.MenuException; +import menu.domain.repository.MenuRepository; + +import java.util.List; + +class MenuService { + + public void checkIsMenuExist(List menus) { + for (String menuName : menus) { + boolean exist = MenuRepository.isExist(menuName); + if (!exist) { + throw new MenuException(MenuError.MENU_NOT_FOUND); + } + } + } + public String pickRandomMenuOf(String category) { + List menusOfCategory = MenuRepository.getMenusOf(category); + return Randoms.shuffle(menusOfCategory).get(0); + } +} diff --git a/yummygyudon/code/java-menu/src/main/java/menu/application/PlannerInfo.java b/yummygyudon/code/java-menu/src/main/java/menu/application/PlannerInfo.java new file mode 100644 index 0000000..9456513 --- /dev/null +++ b/yummygyudon/code/java-menu/src/main/java/menu/application/PlannerInfo.java @@ -0,0 +1,50 @@ +package menu.application; + +import java.util.List; + +/** + * 시험 땐 Record로 써야할 듯... + * 오해할 수도.. + */ +public abstract class PlannerInfo { + + public static class WeeklyCategory { + private final List categories; + + public List getCategories() { + return categories; + } + + private WeeklyCategory(List categories) { + this.categories = categories; + } + + public static WeeklyCategory of(List categories) { + return new WeeklyCategory(categories); + } + } + + public static class CoachMenu { + private final String coachName; + private final List menuNames; + + private CoachMenu(String coachName, List menuNames) { + this.coachName = coachName; + this.menuNames = menuNames; + } + + public String getCoachName() { + return coachName; + } + + public List getMenuNames() { + return menuNames; + } + + public static CoachMenu of( + String coachName, List menuNames + ) { + return new CoachMenu(coachName, menuNames); + } + } +} diff --git a/yummygyudon/code/java-menu/src/main/java/menu/domain/Coach.java b/yummygyudon/code/java-menu/src/main/java/menu/domain/Coach.java new file mode 100644 index 0000000..577f3b3 --- /dev/null +++ b/yummygyudon/code/java-menu/src/main/java/menu/domain/Coach.java @@ -0,0 +1,50 @@ +package menu.domain; + +import menu.domain.exception.CoachError; +import menu.domain.exception.CoachException; +import menu.global.constant.Standard; + +import java.util.ArrayList; +import java.util.List; + +public class Coach { + + private final String name; + private List impossibleMenus; + private final List menus; + + public Coach(String name) { + if (name.length() < Standard.MINIMUM_LENGTH_OF_COACH_NAME) { + throw new CoachException(CoachError.TOO_SHORT_NAME); + } + if (name.length() > Standard.MAXIMUM_LENGTH_OF_COACH_NAME) { + throw new CoachException(CoachError.TOO_LONG_NAME); + } + this.name = name; + this.menus = new ArrayList<>(); + this.impossibleMenus = new ArrayList<>(); + } + + public String getName() { + return name; + } + + public List getMenus() { + return menus; + } + + public void changeImpossibleMenus(List impossibleMenus) { + if (impossibleMenus.size() > Standard.MAXIMUM_NUMBER_OF_IMPOSSIBLE_MENU) { + throw new CoachException(CoachError.TOO_MANY_IMPOSSIBLE_MENUS_OF_COACH); + } + this.impossibleMenus = impossibleMenus; + } + + public boolean isPossibleToEat(String menuName) { + return !this.impossibleMenus.contains(menuName); + } + + public void addMenu(String menu) { + menus.add(menu); + } +} diff --git a/yummygyudon/code/java-menu/src/main/java/menu/domain/Menu.java b/yummygyudon/code/java-menu/src/main/java/menu/domain/Menu.java new file mode 100644 index 0000000..4063d96 --- /dev/null +++ b/yummygyudon/code/java-menu/src/main/java/menu/domain/Menu.java @@ -0,0 +1,19 @@ +package menu.domain; + +public class Menu { + private final String name; + private final String category; + + public Menu(String name, String category) { + this.name = name; + this.category = category; + } + + public String getName() { + return name; + } + + public String getCategory() { + return category; + } +} diff --git a/yummygyudon/code/java-menu/src/main/java/menu/domain/exception/CoachError.java b/yummygyudon/code/java-menu/src/main/java/menu/domain/exception/CoachError.java new file mode 100644 index 0000000..7e25ab0 --- /dev/null +++ b/yummygyudon/code/java-menu/src/main/java/menu/domain/exception/CoachError.java @@ -0,0 +1,23 @@ +package menu.domain.exception; + +import menu.global.exception.base.MenuPlannerError; + +public enum CoachError implements MenuPlannerError { + TOO_LONG_NAME("이름이 너무 깁니다."), + TOO_SHORT_NAME("이름이 너무 짧습니다."), + TOO_MANY_COACH("코치의 인원이 너무 많습니다."), + TOO_FEW_COACH("코치의 인원이 너무 적습니다."), + TOO_MANY_IMPOSSIBLE_MENUS_OF_COACH("못먹는 메뉴가 너무 많습니다."), + ; + + private final String errorMessage; + + CoachError(String errorMessage) { + this.errorMessage = errorMessage; + } + + @Override + public String getErrorMessage() { + return this.errorMessage; + } +} diff --git a/yummygyudon/code/java-menu/src/main/java/menu/domain/exception/CoachException.java b/yummygyudon/code/java-menu/src/main/java/menu/domain/exception/CoachException.java new file mode 100644 index 0000000..fdd61b0 --- /dev/null +++ b/yummygyudon/code/java-menu/src/main/java/menu/domain/exception/CoachException.java @@ -0,0 +1,9 @@ +package menu.domain.exception; + +import menu.global.exception.base.MenuPlannerException; + +public class CoachException extends MenuPlannerException { + public CoachException(CoachError error) { + super(error); + } +} diff --git a/yummygyudon/code/java-menu/src/main/java/menu/domain/exception/MenuError.java b/yummygyudon/code/java-menu/src/main/java/menu/domain/exception/MenuError.java new file mode 100644 index 0000000..405c30a --- /dev/null +++ b/yummygyudon/code/java-menu/src/main/java/menu/domain/exception/MenuError.java @@ -0,0 +1,19 @@ +package menu.domain.exception; + +import menu.global.exception.base.MenuPlannerError; + +public enum MenuError implements MenuPlannerError { + MENU_NOT_FOUND("존재하지 않는 메뉴입니다."), + ; + + private final String errorMessage; + + MenuError(String errorMessage) { + this.errorMessage = errorMessage; + } + + @Override + public String getErrorMessage() { + return this.errorMessage; + } +} diff --git a/yummygyudon/code/java-menu/src/main/java/menu/domain/exception/MenuException.java b/yummygyudon/code/java-menu/src/main/java/menu/domain/exception/MenuException.java new file mode 100644 index 0000000..3bd1403 --- /dev/null +++ b/yummygyudon/code/java-menu/src/main/java/menu/domain/exception/MenuException.java @@ -0,0 +1,9 @@ +package menu.domain.exception; + +import menu.global.exception.base.MenuPlannerException; + +public class MenuException extends MenuPlannerException { + public MenuException(MenuError error) { + super(error); + } +} diff --git a/yummygyudon/code/java-menu/src/main/java/menu/domain/repository/CategoryRepository.java b/yummygyudon/code/java-menu/src/main/java/menu/domain/repository/CategoryRepository.java new file mode 100644 index 0000000..12bcac9 --- /dev/null +++ b/yummygyudon/code/java-menu/src/main/java/menu/domain/repository/CategoryRepository.java @@ -0,0 +1,21 @@ +package menu.domain.repository; + + +import java.util.Map; + +public class CategoryRepository { + + private static final Map categories = Map.of( + 1, "일식", + 2, "한식", + 3, "중식", + 4, "아시안", + 5, "양식" + ); + + // read-only + public static Map categories() { + return categories; + } + +} diff --git a/yummygyudon/code/java-menu/src/main/java/menu/domain/repository/CoachRepository.java b/yummygyudon/code/java-menu/src/main/java/menu/domain/repository/CoachRepository.java new file mode 100644 index 0000000..a462ac5 --- /dev/null +++ b/yummygyudon/code/java-menu/src/main/java/menu/domain/repository/CoachRepository.java @@ -0,0 +1,33 @@ +package menu.domain.repository; + +import menu.domain.Coach; +import menu.domain.exception.CoachError; +import menu.domain.exception.CoachException; +import menu.global.constant.Standard; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Collectors; + +public class CoachRepository { + + private static List coaches = new ArrayList<>(); + + + public static void init(List coachNames) { + if (coachNames.size() < Standard.MINIMUM_NUMBER_OF_COACH) { + throw new CoachException(CoachError.TOO_FEW_COACH); + } + if (coachNames.size() > Standard.MAXIMUM_NUMBER_OF_COACH) { + throw new CoachException(CoachError.TOO_MANY_COACH); + } + coaches = coachNames.stream() + .map(Coach::new) + .collect(Collectors.toList()); + } + + public static List coaches() { + return Collections.unmodifiableList(coaches); + } +} diff --git a/yummygyudon/code/java-menu/src/main/java/menu/domain/repository/MenuRepository.java b/yummygyudon/code/java-menu/src/main/java/menu/domain/repository/MenuRepository.java new file mode 100644 index 0000000..929add6 --- /dev/null +++ b/yummygyudon/code/java-menu/src/main/java/menu/domain/repository/MenuRepository.java @@ -0,0 +1,37 @@ +package menu.domain.repository; + +import menu.domain.Menu; + +import java.util.ArrayList; +import java.util.List; + +public class MenuRepository { + private static final List menus = new ArrayList<>(); + + static { + addMenusOf("한식","김밥", "김치찌개", "쌈밥", "된장찌개", "비빔밥", "칼국수", "불고기", "떡볶이", "제육볶음"); + addMenusOf("일식", "규동", "우동", "미소시루", "스시", "가츠동", "오니기리", "하이라이스", "라멘", "오코노미야끼"); + addMenusOf("중식", "깐풍기", "볶음면", "동파육", "짜장면", "짬뽕", "마파두부", "탕수육", "토마토 달걀볶음", "고추잡채"); + addMenusOf("아시안", "팟타이", "카오 팟", "나시고렝", "파인애플 볶음밥", "쌀국수", "똠얌꿍", "반미", "월남쌈", "분짜"); + addMenusOf("양식", "라자냐", "그라탱", "뇨끼", "끼슈", "프렌치 토스트", "바게트", "스파게티", "피자", "파니니"); + } + + private static void addMenusOf(String category, String... menuNames) { + for (String menuName : menuNames) { + menus.add(new Menu(menuName, category)); + } + } + + public static boolean isExist(String menuName) { + return menus.stream() + .anyMatch(menu -> menu.getName().equals(menuName)); + } + + public static List getMenusOf(String category) { + return menus.stream() + .filter(menu -> menu.getCategory().equals(category)) + .map(Menu::getName) + .toList(); + } + +} diff --git a/yummygyudon/code/java-menu/src/main/java/menu/global/channel/Printer.java b/yummygyudon/code/java-menu/src/main/java/menu/global/channel/Printer.java new file mode 100644 index 0000000..6f3d45d --- /dev/null +++ b/yummygyudon/code/java-menu/src/main/java/menu/global/channel/Printer.java @@ -0,0 +1,11 @@ +package menu.global.channel; + +public abstract class Printer { + public static void print(String message) { + System.out.println(message); + } + + public static void printBlankLine() { + System.out.println(); + } +} diff --git a/yummygyudon/code/java-menu/src/main/java/menu/global/channel/Reader.java b/yummygyudon/code/java-menu/src/main/java/menu/global/channel/Reader.java new file mode 100644 index 0000000..aeb2490 --- /dev/null +++ b/yummygyudon/code/java-menu/src/main/java/menu/global/channel/Reader.java @@ -0,0 +1,9 @@ +package menu.global.channel; + +import camp.nextstep.edu.missionutils.Console; + +public abstract class Reader { + public static String read() { + return Console.readLine(); + } +} diff --git a/yummygyudon/code/java-menu/src/main/java/menu/global/constant/Regex.java b/yummygyudon/code/java-menu/src/main/java/menu/global/constant/Regex.java new file mode 100644 index 0000000..141f7f8 --- /dev/null +++ b/yummygyudon/code/java-menu/src/main/java/menu/global/constant/Regex.java @@ -0,0 +1,5 @@ +package menu.global.constant; + +public abstract class Regex { + public static final String REGEX_PATTERN_FOR_COACH_NAME_INPUT = "^([\\s]*[a-z|A-Z|ㄱ-ㅎ|ㅏ-ㅣ|가-힣]+[\\s]*)(,(([\\s]*[a-z|A-Z|ㄱ-ㅎ|ㅏ-ㅣ|가-힣]+[\\s]*)))*(([\\s]*[a-z|A-Z|ㄱ-ㅎ|ㅏ-ㅣ|가-힣]+[\\s]*))$"; +} diff --git a/yummygyudon/code/java-menu/src/main/java/menu/global/constant/Standard.java b/yummygyudon/code/java-menu/src/main/java/menu/global/constant/Standard.java new file mode 100644 index 0000000..96bc2af --- /dev/null +++ b/yummygyudon/code/java-menu/src/main/java/menu/global/constant/Standard.java @@ -0,0 +1,19 @@ +package menu.global.constant; + +public abstract class Standard { + + // 표준 기호 + public static final String NAME_INPUT_SEPARATOR = ","; + public static final String RESULT_RECOMMEND_PREFIX = "[ "; + public static final String RESULT_RECOMMEND_SUFFIX = " ]"; + public static final String RESULT_RECOMMEND_SEPARATOR = " | "; + + + // 표준 제한값 + public static int DATE_LIMIT_FOR_PLAN = 5; + public static int MINIMUM_NUMBER_OF_COACH = 2; + public static int MAXIMUM_NUMBER_OF_COACH = 5; + public static int MINIMUM_LENGTH_OF_COACH_NAME = 2; + public static int MAXIMUM_LENGTH_OF_COACH_NAME = 4; + public static int MAXIMUM_NUMBER_OF_IMPOSSIBLE_MENU = 2; +} diff --git a/yummygyudon/code/java-menu/src/main/java/menu/global/constant/Variable.java b/yummygyudon/code/java-menu/src/main/java/menu/global/constant/Variable.java new file mode 100644 index 0000000..c139d5c --- /dev/null +++ b/yummygyudon/code/java-menu/src/main/java/menu/global/constant/Variable.java @@ -0,0 +1,6 @@ +package menu.global.constant; + +public abstract class Variable { + public static final String KOR_CATEGORY = "카테고리"; + +} diff --git a/yummygyudon/code/java-menu/src/main/java/menu/global/exception/GlobalError.java b/yummygyudon/code/java-menu/src/main/java/menu/global/exception/GlobalError.java new file mode 100644 index 0000000..5765dc4 --- /dev/null +++ b/yummygyudon/code/java-menu/src/main/java/menu/global/exception/GlobalError.java @@ -0,0 +1,23 @@ +package menu.global.exception; + +import menu.global.exception.base.MenuPlannerError; + +public enum GlobalError implements MenuPlannerError { + + /** 입력 에러 */ + // 잘못된 패턴으로 메뉴와 주문 갯수를 입력했을 경우 + NOT_AVAILABLE_INPUT_PATTERN("잘못된 형식의 입력값입니다. 다시 입력해 주세요."), + + ; + + private final String errorMessage; + + GlobalError(String errorMessage) { + this.errorMessage = errorMessage; + } + + @Override + public String getErrorMessage() { + return this.errorMessage; + } +} diff --git a/yummygyudon/code/java-menu/src/main/java/menu/global/exception/GlobalException.java b/yummygyudon/code/java-menu/src/main/java/menu/global/exception/GlobalException.java new file mode 100644 index 0000000..15b61fa --- /dev/null +++ b/yummygyudon/code/java-menu/src/main/java/menu/global/exception/GlobalException.java @@ -0,0 +1,9 @@ +package menu.global.exception; + +import menu.global.exception.base.MenuPlannerException; + +public class GlobalException extends MenuPlannerException { + public GlobalException(GlobalError error) { + super(error); + } +} diff --git a/yummygyudon/code/java-menu/src/main/java/menu/global/exception/base/ErrorBase.java b/yummygyudon/code/java-menu/src/main/java/menu/global/exception/base/ErrorBase.java new file mode 100644 index 0000000..b8efc77 --- /dev/null +++ b/yummygyudon/code/java-menu/src/main/java/menu/global/exception/base/ErrorBase.java @@ -0,0 +1,7 @@ +package menu.global.exception.base; + +public interface ErrorBase { + + String getErrorMessage(); + +} diff --git a/yummygyudon/code/java-menu/src/main/java/menu/global/exception/base/MenuPlannerError.java b/yummygyudon/code/java-menu/src/main/java/menu/global/exception/base/MenuPlannerError.java new file mode 100644 index 0000000..82b931c --- /dev/null +++ b/yummygyudon/code/java-menu/src/main/java/menu/global/exception/base/MenuPlannerError.java @@ -0,0 +1,5 @@ +package menu.global.exception.base; + + +public interface MenuPlannerError extends ErrorBase { +} diff --git a/yummygyudon/code/java-menu/src/main/java/menu/global/exception/base/MenuPlannerException.java b/yummygyudon/code/java-menu/src/main/java/menu/global/exception/base/MenuPlannerException.java new file mode 100644 index 0000000..dd0f9ec --- /dev/null +++ b/yummygyudon/code/java-menu/src/main/java/menu/global/exception/base/MenuPlannerException.java @@ -0,0 +1,10 @@ +package menu.global.exception.base; + +public class MenuPlannerException extends IllegalArgumentException{ + private static final String ERROR_MESSAGE_HEADER = "[ERROR] "; + + public MenuPlannerException(MenuPlannerError error) { + super(ERROR_MESSAGE_HEADER + error.getErrorMessage()); + } + +} diff --git a/yummygyudon/code/java-menu/src/main/java/menu/global/message/Ask.java b/yummygyudon/code/java-menu/src/main/java/menu/global/message/Ask.java new file mode 100644 index 0000000..ac68f10 --- /dev/null +++ b/yummygyudon/code/java-menu/src/main/java/menu/global/message/Ask.java @@ -0,0 +1,7 @@ +package menu.global.message; + +public abstract class Ask { + + public static final String COACH_NAMES = "코치의 이름을 입력해 주세요. (, 로 구분)"; + public static final String IMPOSSIBLE_MENUS = "(이)가 못 먹는 메뉴를 입력해 주세요."; +} diff --git a/yummygyudon/code/java-menu/src/main/java/menu/global/message/Notice.java b/yummygyudon/code/java-menu/src/main/java/menu/global/message/Notice.java new file mode 100644 index 0000000..75ab7ba --- /dev/null +++ b/yummygyudon/code/java-menu/src/main/java/menu/global/message/Notice.java @@ -0,0 +1,9 @@ +package menu.global.message; + +public abstract class Notice { + + public static final String START_RECOMMEND = "점심 메뉴 추천을 시작합니다."; + public static final String RESULT_RECOMMEND = "메뉴 추천 결과입니다."; + public static final String RESULT_RECOMMEND_HEADLINE = "[ 구분 | 월요일 | 화요일 | 수요일 | 목요일 | 금요일 ]"; + public static final String FINISH_RECOMMEND = "추천을 완료했습니다."; +} diff --git a/yummygyudon/code/java-menu/src/main/java/menu/presentation/MenuPlanner.java b/yummygyudon/code/java-menu/src/main/java/menu/presentation/MenuPlanner.java new file mode 100644 index 0000000..56660a4 --- /dev/null +++ b/yummygyudon/code/java-menu/src/main/java/menu/presentation/MenuPlanner.java @@ -0,0 +1,60 @@ +package menu.presentation; + + +import menu.application.MenuPlanService; +import menu.application.PlannerInfo; +import menu.global.exception.base.MenuPlannerException; +import menu.presentation.view.InputView; +import menu.presentation.view.OutputView; + +import java.util.List; + +public class MenuPlanner { + + private final InputView inputView; + private final OutputView outputView; + private final MenuPlanService menuPlanService; + + public MenuPlanner() { + this.inputView = new InputView(); + this.outputView = new OutputView(); + List coachNames = inputView.askCoachNames(); + this.menuPlanService = new MenuPlanService(coachNames); + } + + public void startPlan() { + List coachNames = menuPlanService.getCoachNames(); + for (String coachName : coachNames) { + boolean isSuccessToRegister; + do { + isSuccessToRegister = registerImpossibleMenus(coachName); + } while (!isSuccessToRegister); + } + viewResultOfPlan(); + } + + private boolean registerImpossibleMenus(String coachName) { + try { + List impossibleMenus = inputView.askImpossibleMenusOf(coachName); + menuPlanService.registerImpossibleMenusOf(coachName, impossibleMenus); + return true; + } catch (MenuPlannerException plannerException) { + outputView.printException(plannerException); + return false; + } + } + private void viewResultOfPlan() { + outputView.printResultHead(); + PlannerInfo.WeeklyCategory categories = menuPlanService.planCategoryOfWeek(); + outputView.printCategoryOfWeek(categories); + + menuPlanService.planMenuForAllCoachOfWeek(categories); + List coachMenus = menuPlanService.makeWeeklyMenuBoard(); + for (PlannerInfo.CoachMenu coachMenu : coachMenus) { + outputView.printCoachMenuOfWeek(coachMenu); + } + + outputView.printFinishing(); + } + +} diff --git a/yummygyudon/code/java-menu/src/main/java/menu/presentation/view/InputView.java b/yummygyudon/code/java-menu/src/main/java/menu/presentation/view/InputView.java new file mode 100644 index 0000000..af0d632 --- /dev/null +++ b/yummygyudon/code/java-menu/src/main/java/menu/presentation/view/InputView.java @@ -0,0 +1,52 @@ +package menu.presentation.view; + +import menu.global.channel.Printer; +import menu.global.channel.Reader; +import menu.global.constant.Regex; +import menu.global.constant.Standard; +import menu.global.exception.GlobalError; +import menu.global.exception.GlobalException; +import menu.global.message.Ask; +import menu.global.message.Notice; + +import java.util.ArrayList; +import java.util.List; + +public class InputView { + + public List askCoachNames() { + Printer.print(Notice.START_RECOMMEND); + Printer.printBlankLine(); + Printer.print(Ask.COACH_NAMES); + + String inputValue = Reader.read().trim(); + validateInputPattern(inputValue); + Printer.printBlankLine(); + return splitInputValue(inputValue); + } + + public List askImpossibleMenusOf(String coachName) { + Printer.print(coachName + Ask.IMPOSSIBLE_MENUS); + + String inputValue = Reader.read().trim(); + validateInputPattern(inputValue); + Printer.printBlankLine(); + return splitInputValue(inputValue); + } + + private void validateInputPattern(String input) { + if (!input.matches(Regex.REGEX_PATTERN_FOR_COACH_NAME_INPUT)) { + throw new GlobalException(GlobalError.NOT_AVAILABLE_INPUT_PATTERN); + } + } + + private List splitInputValue(String inputValue) { + String[] values = inputValue.split(Standard.NAME_INPUT_SEPARATOR); + List result = new ArrayList<>(); + for (String value : values) { + result.add(value.trim()); + } + return result; + } + +} diff --git a/yummygyudon/code/java-menu/src/main/java/menu/presentation/view/OutputView.java b/yummygyudon/code/java-menu/src/main/java/menu/presentation/view/OutputView.java new file mode 100644 index 0000000..480fea1 --- /dev/null +++ b/yummygyudon/code/java-menu/src/main/java/menu/presentation/view/OutputView.java @@ -0,0 +1,51 @@ +package menu.presentation.view; + +import menu.application.PlannerInfo; +import menu.global.channel.Printer; +import menu.global.constant.Standard; +import menu.global.constant.Variable; +import menu.global.exception.base.MenuPlannerException; +import menu.global.message.Notice; + +import java.util.StringJoiner; + +public class OutputView { + + /** 예외 출력 기능 */ + public void printException(MenuPlannerException exception) { + Printer.print(exception.getMessage()); + } + + /** 결과 출력 기능 */ + public void printResultHead() { + Printer.print(Notice.RESULT_RECOMMEND); + Printer.print(Notice.RESULT_RECOMMEND_HEADLINE); + } + + + public void printCategoryOfWeek(PlannerInfo.WeeklyCategory weeklyCategory) { + StringJoiner joiner = new StringJoiner( + Standard.RESULT_RECOMMEND_SEPARATOR, Standard.RESULT_RECOMMEND_PREFIX, Standard.RESULT_RECOMMEND_SUFFIX + ); + joiner.add(Variable.KOR_CATEGORY); + for (String category : weeklyCategory.getCategories()) { + joiner.add(category); + } + Printer.print(joiner.toString()); + } + public void printCoachMenuOfWeek(PlannerInfo.CoachMenu coachMenu) { + StringJoiner joiner = new StringJoiner( + Standard.RESULT_RECOMMEND_SEPARATOR, Standard.RESULT_RECOMMEND_PREFIX, Standard.RESULT_RECOMMEND_SUFFIX + ); + joiner.add(coachMenu.getCoachName()); + for (String menuName : coachMenu.getMenuNames()) { + joiner.add(menuName); + } + Printer.print(joiner.toString()); + } + public void printFinishing() { + Printer.printBlankLine(); + Printer.print(Notice.FINISH_RECOMMEND); + } + +} diff --git a/yummygyudon/code/java-menu/src/test/java/menu/ApplicationTest.java b/yummygyudon/code/java-menu/src/test/java/menu/ApplicationTest.java new file mode 100644 index 0000000..aa03d02 --- /dev/null +++ b/yummygyudon/code/java-menu/src/test/java/menu/ApplicationTest.java @@ -0,0 +1,130 @@ +package menu; + +import camp.nextstep.edu.missionutils.Randoms; +import camp.nextstep.edu.missionutils.test.NsTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.function.Executable; +import org.mockito.MockedStatic; + +import java.time.Duration; +import java.util.Arrays; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertTimeoutPreemptively; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyList; +import static org.mockito.Mockito.mockStatic; + +public class ApplicationTest extends NsTest { + + private static final Duration RANDOM_TEST_TIMEOUT = Duration.ofSeconds(10L); + + @DisplayName("전체 기능 테스트") + @Nested + class AllFeatureTest { + + @Test + void 기능_테스트() { + assertTimeoutPreemptively(RANDOM_TEST_TIMEOUT, () -> { + final Executable executable = () -> { + runException("구구,제임스", "김밥", "떡볶이"); + + assertThat(output()).contains( + "점심 메뉴 추천을 시작합니다.", + "코치의 이름을 입력해 주세요. (, 로 구분)", + "구구(이)가 못 먹는 메뉴를 입력해 주세요.", + "제임스(이)가 못 먹는 메뉴를 입력해 주세요.", + "메뉴 추천 결과입니다.", + "[ 구분 | 월요일 | 화요일 | 수요일 | 목요일 | 금요일 ]", + "[ 카테고리 | 한식 | 양식 | 일식 | 중식 | 아시안 ]", + "[ 구구 | 김치찌개 | 스파게티 | 규동 | 짜장면 | 카오 팟 ]", + "[ 제임스 | 제육볶음 | 라자냐 | 가츠동 | 짬뽕 | 파인애플 볶음밥 ]", + "추천을 완료했습니다." + ); + }; + + assertRandomTest(executable, + Mocking.ofRandomNumberInRange(2, 5, 1, 3, 4), // 숫자는 카테고리 번호를 나타낸다. + Mocking.ofShuffle( + // 월요일 + List.of("김치찌개", "김밥", "쌈밥", "된장찌개", "비빔밥", "칼국수", "불고기", "떡볶이", "제육볶음"), // 구구 + List.of("제육볶음", "김밥", "김치찌개", "쌈밥", "된장찌개", "비빔밥", "칼국수", "불고기", "떡볶이"), // 제임스 + + // 화요일 + List.of("스파게티", "라자냐", "그라탱", "뇨끼", "끼슈", "프렌치 토스트", "바게트", "피자", "파니니"), // 구구 + List.of("라자냐", "그라탱", "뇨끼", "끼슈", "프렌치 토스트", "바게트", "스파게티", "피자", "파니니"), // 제임스 + + // 수요일 + List.of("규동", "우동", "미소시루", "스시", "가츠동", "오니기리", "하이라이스", "라멘", "오코노미야끼"), // 구구 + List.of("가츠동", "규동", "우동", "미소시루", "스시", "오니기리", "하이라이스", "라멘", "오코노미야끼"), // 제임스 + + // 목요일 + List.of("짜장면", "깐풍기", "볶음면", "동파육", "짬뽕", "마파두부", "탕수육", "토마토 달걀볶음", "고추잡채"), // 구구 + List.of("짬뽕", "깐풍기", "볶음면", "동파육", "짜장면", "마파두부", "탕수육", "토마토 달걀볶음", "고추잡채"), // 제임스 + + // 금요일 + List.of("카오 팟", "팟타이", "나시고렝", "파인애플 볶음밥", "쌀국수", "똠얌꿍", "반미", "월남쌈", "분짜"), // 구구 + List.of("파인애플 볶음밥", "팟타이", "카오 팟", "나시고렝", "쌀국수", "똠얌꿍", "반미", "월남쌈", "분짜") // 제임스 + ) + ); + }); + } + } + + @Override + protected void runMain() { + Application.main(new String[]{}); + } + + private static void assertRandomTest( + final Executable executable, + final Mocking... mockings + ) { + assertTimeoutPreemptively(RANDOM_TEST_TIMEOUT, () -> { + try (final MockedStatic mock = mockStatic(Randoms.class)) { + Arrays.stream(mockings).forEach(mocking -> mocking.stub(mock)); + executable.execute(); + } + }); + } + + public static class Mocking { + + /** + * stubbing lambda verification 예시) () -> Randoms.pickNumberInList(anyList()) + */ + private final MockedStatic.Verification verification; + + // 반환할 첫 번째 값 + private final T value; + + /** + * 첫 번째 값을 반환하고 나서 다음에 반환할 값들. 예를 들면, verification을 처음 실행하면 value를 반환하고 두 번째 실행하면 values[0]을 + * 반환한다. + */ + private final T[] values; + + private Mocking(final MockedStatic.Verification verification, + final T value, + final T... values) { + this.verification = verification; + this.value = value; + this.values = values; + } + + public static Mocking ofRandomNumberInRange(final Integer value, final Integer... values) { + return new Mocking(() -> Randoms.pickNumberInRange(anyInt(), anyInt()), value, values); + } + + public static Mocking ofShuffle(final List value, final List... values) { + return new Mocking(() -> Randoms.shuffle(anyList()), value, values); + } + + public void stub(final MockedStatic mock) { + mock.when(verification).thenReturn(value, Arrays.stream(values).toArray()); + } + } +} diff --git a/yummygyudon/code/java-racingcar/.gitignore b/yummygyudon/code/java-racingcar/.gitignore new file mode 100644 index 0000000..6c01878 --- /dev/null +++ b/yummygyudon/code/java-racingcar/.gitignore @@ -0,0 +1,32 @@ +HELP.md +.gradle +build/ +!gradle/wrapper/gradle-wrapper.jar +!**/src/main/** +!**/src/test/** + +### STS ### +.apt_generated +.classpath +.factorypath +.project +.settings +.springBeans +.sts4-cache + +### IntelliJ IDEA ### +.idea +*.iws +*.iml +*.ipr +out/ + +### NetBeans ### +/nbproject/private/ +/nbbuild/ +/dist/ +/nbdist/ +/.nb-gradle/ + +### VS Code ### +.vscode/ diff --git a/yummygyudon/code/java-racingcar/build.gradle b/yummygyudon/code/java-racingcar/build.gradle new file mode 100644 index 0000000..15f3352 --- /dev/null +++ b/yummygyudon/code/java-racingcar/build.gradle @@ -0,0 +1,25 @@ +plugins { + id 'java' +} + +group 'camp.nextstep.edu' +version '1.0-SNAPSHOT' + +repositories { + mavenCentral() + maven { url 'https://jitpack.io' } +} + +dependencies { + implementation 'com.github.woowacourse-projects:mission-utils:1.1.0' +} + +java { + toolchain { + languageVersion = JavaLanguageVersion.of(17) + } +} + +test { + useJUnitPlatform() +} diff --git a/yummygyudon/code/java-racingcar/gradle/wrapper/gradle-wrapper.jar b/yummygyudon/code/java-racingcar/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000..249e583 Binary files /dev/null and b/yummygyudon/code/java-racingcar/gradle/wrapper/gradle-wrapper.jar differ diff --git a/yummygyudon/code/java-racingcar/gradle/wrapper/gradle-wrapper.properties b/yummygyudon/code/java-racingcar/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..ae04661 --- /dev/null +++ b/yummygyudon/code/java-racingcar/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,5 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-7.5.1-bin.zip +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists diff --git a/yummygyudon/code/java-racingcar/gradlew b/yummygyudon/code/java-racingcar/gradlew new file mode 100755 index 0000000..a69d9cb --- /dev/null +++ b/yummygyudon/code/java-racingcar/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/yummygyudon/code/java-racingcar/gradlew.bat b/yummygyudon/code/java-racingcar/gradlew.bat new file mode 100644 index 0000000..f127cfd --- /dev/null +++ b/yummygyudon/code/java-racingcar/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/yummygyudon/code/java-racingcar/settings.gradle b/yummygyudon/code/java-racingcar/settings.gradle new file mode 100644 index 0000000..4b94a0e --- /dev/null +++ b/yummygyudon/code/java-racingcar/settings.gradle @@ -0,0 +1,2 @@ +rootProject.name = 'java-racingcar' + diff --git a/yummygyudon/code/java-racingcar/src/main/java/racingcar/Application.java b/yummygyudon/code/java-racingcar/src/main/java/racingcar/Application.java new file mode 100644 index 0000000..0b2d214 --- /dev/null +++ b/yummygyudon/code/java-racingcar/src/main/java/racingcar/Application.java @@ -0,0 +1,11 @@ +package racingcar; + +import racingcar.presentation.RacingGameExecutor; + +public class Application { + public static void main(String[] args) { + // TODO: 프로그램 구현 + RacingGameExecutor gameExecutor = new RacingGameExecutor(); + gameExecutor.execute(); + } +} diff --git a/yummygyudon/code/java-racingcar/src/main/java/racingcar/application/RacingGame.java b/yummygyudon/code/java-racingcar/src/main/java/racingcar/application/RacingGame.java new file mode 100644 index 0000000..c397133 --- /dev/null +++ b/yummygyudon/code/java-racingcar/src/main/java/racingcar/application/RacingGame.java @@ -0,0 +1,60 @@ +package racingcar.application; + +import racingcar.domain.Race; +import racingcar.domain.RacingCar; +import racingcar.util.channel.Printer; +import racingcar.util.constant.ConstVariable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Objects; + +public class RacingGame { + private final Race race; + private final Long trial; + + public RacingGame(Long trial, List carNames) { + this.trial = trial; + this.race = new Race(carNames); + } + + public void start() { + Printer.printLine(ConstVariable.RESULT_START_MESSAGE); + + for (int round = 1; round <= trial; round++) { + race.circuit(); + + viewCircuitResult(race); + Printer.printBlankLine(); + } + viewWinners(race); + } + + /** 각 서킷의 결과를 출력합니다. */ + private void viewCircuitResult(Race race) { + for (RacingCar car : race.getRacingCars()) { + // 거리만큼 "-"로 변환 + String distanceStatus = ConstVariable.RESULT_MOVEMENT.repeat(car.getDistance().intValue()); + String circuitResult = String.join(ConstVariable.RESULT_SEPARATOR, car.getName(), distanceStatus); + + Printer.printLine(circuitResult); + } + } + + /** 최종 레이스의 결과를 출력합니다. */ + private void viewWinners(Race race) { + Long farthestDistance = race.getMostFarthestDistance(); + List winners = new ArrayList<>(); + + for (RacingCar car : race.getRacingCars()) { + // 본 레이스의 가장 긴 거리 기록과 동일한 차량의 이름을 구합니다. + if (Objects.equals(car.getDistance(), farthestDistance)) { + winners.add(car.getName()); + } + } + + String winnersResult = String.join(ConstVariable.RESULT_WINNER_SEPARATOR, winners); + Printer.printLine(ConstVariable.RESULT_WINNER_MESSAGE + winnersResult); + } + +} diff --git a/yummygyudon/code/java-racingcar/src/main/java/racingcar/domain/Race.java b/yummygyudon/code/java-racingcar/src/main/java/racingcar/domain/Race.java new file mode 100644 index 0000000..ffe7cf8 --- /dev/null +++ b/yummygyudon/code/java-racingcar/src/main/java/racingcar/domain/Race.java @@ -0,0 +1,51 @@ +package racingcar.domain; + +import racingcar.util.RandomNumberGenerator; +import racingcar.util.constant.ConstStandard; + +import java.util.ArrayList; +import java.util.List; + +public class Race { + + private final List racingCars; + + public Race(List carNames) { + this.racingCars = new ArrayList<>(); + + // 입력된 이름들로 RacingCar 갹체들을 만들어 주입합니다. + for (String carName : carNames) { + this.racingCars.add(new RacingCar(carName)); + } + } + + public List getRacingCars() { + return racingCars; + } + + public Long getMostFarthestDistance() { + Long farthestDistance = 0L; + for (RacingCar car : racingCars) { + if (isFarthest(car.getDistance(), farthestDistance)) { + farthestDistance = car.getDistance(); + } + } + return farthestDistance; + } + + public void circuit() { + for (RacingCar car : racingCars) { + if (isRacingCarMove()) { + car.move(); + } + } + } + + private boolean isRacingCarMove() { + int movingValue = RandomNumberGenerator.generate(); + return movingValue >= ConstStandard.CRITERIA_MOVE; + } + private boolean isFarthest(long targetDistance, long elderFarthestDistance) { + return targetDistance > elderFarthestDistance; + } +} diff --git a/yummygyudon/code/java-racingcar/src/main/java/racingcar/domain/RacingCar.java b/yummygyudon/code/java-racingcar/src/main/java/racingcar/domain/RacingCar.java new file mode 100644 index 0000000..f7c2137 --- /dev/null +++ b/yummygyudon/code/java-racingcar/src/main/java/racingcar/domain/RacingCar.java @@ -0,0 +1,23 @@ +package racingcar.domain; + +public class RacingCar { + private final String name; + private Long distance; + + RacingCar(String name) { + this.name = name; + this.distance = 0L; + } + + public String getName() { + return name; + } + + public Long getDistance() { + return distance; + } + + void move() { + this.distance += 1; + } +} diff --git a/yummygyudon/code/java-racingcar/src/main/java/racingcar/presentation/RacingGameExecutor.java b/yummygyudon/code/java-racingcar/src/main/java/racingcar/presentation/RacingGameExecutor.java new file mode 100644 index 0000000..0f289e4 --- /dev/null +++ b/yummygyudon/code/java-racingcar/src/main/java/racingcar/presentation/RacingGameExecutor.java @@ -0,0 +1,49 @@ +package racingcar.presentation; + +import racingcar.application.RacingGame; +import racingcar.util.channel.Printer; +import racingcar.util.channel.ReaderForRacingCars; +import racingcar.util.channel.ReaderForTrial; +import racingcar.util.constant.ConstStandard; +import racingcar.util.enums.Command; + +import java.util.Arrays; +import java.util.List; + +public class RacingGameExecutor { + private final ReaderForRacingCars racingCarsReader; + private final ReaderForTrial trialReader; + + public RacingGameExecutor() { + this.racingCarsReader = new ReaderForRacingCars(); + this.trialReader = new ReaderForTrial(); + } + + public void execute() { + List racingCarsNames = receiveRacingCarNames(); + Long trial = receiveTrial(); + + RacingGame racingGame = new RacingGame(trial, racingCarsNames); + + racingGame.start(); + } + + private List receiveRacingCarNames() { + Printer.printLine(Command.ASK_CAR_NAMES.getMessage()); + + String carNamesInput = racingCarsReader.read(); + List carNames = Arrays.asList(carNamesInput.split(ConstStandard.CRITERIA_NAMES_INPUT_SEPARATOR)); + + return carNames.stream() + .map(String::trim) + .toList(); + } + + private Long receiveTrial() { + Printer.printLine(Command.ASK_TRIAL.getMessage()); + + String trialInput = trialReader.read(); + + return Long.valueOf(trialInput); + } +} diff --git a/yummygyudon/code/java-racingcar/src/main/java/racingcar/util/RandomNumberGenerator.java b/yummygyudon/code/java-racingcar/src/main/java/racingcar/util/RandomNumberGenerator.java new file mode 100644 index 0000000..360c21c --- /dev/null +++ b/yummygyudon/code/java-racingcar/src/main/java/racingcar/util/RandomNumberGenerator.java @@ -0,0 +1,12 @@ +package racingcar.util; + +import camp.nextstep.edu.missionutils.Randoms; + +public abstract class RandomNumberGenerator { + private static final int LOWER_LIMIT = 0; + private static final int UPPER_LIMIT = 9; + + public static int generate() { + return Randoms.pickNumberInRange(LOWER_LIMIT, UPPER_LIMIT); + } +} diff --git a/yummygyudon/code/java-racingcar/src/main/java/racingcar/util/channel/Printer.java b/yummygyudon/code/java-racingcar/src/main/java/racingcar/util/channel/Printer.java new file mode 100644 index 0000000..637928b --- /dev/null +++ b/yummygyudon/code/java-racingcar/src/main/java/racingcar/util/channel/Printer.java @@ -0,0 +1,12 @@ +package racingcar.util.channel; + +public abstract class Printer { + + public static void printLine(String message) { + System.out.println(message); + } + + public static void printBlankLine() { + System.out.println(); + } +} diff --git a/yummygyudon/code/java-racingcar/src/main/java/racingcar/util/channel/Reader.java b/yummygyudon/code/java-racingcar/src/main/java/racingcar/util/channel/Reader.java new file mode 100644 index 0000000..34d4205 --- /dev/null +++ b/yummygyudon/code/java-racingcar/src/main/java/racingcar/util/channel/Reader.java @@ -0,0 +1,11 @@ +package racingcar.util.channel; + +public abstract class Reader { + + protected void validateBlank(String input) { + if (input.isBlank()) { + throw new IllegalArgumentException(); + } + } + public abstract String read(); +} diff --git a/yummygyudon/code/java-racingcar/src/main/java/racingcar/util/channel/ReaderForRacingCars.java b/yummygyudon/code/java-racingcar/src/main/java/racingcar/util/channel/ReaderForRacingCars.java new file mode 100644 index 0000000..c16fec4 --- /dev/null +++ b/yummygyudon/code/java-racingcar/src/main/java/racingcar/util/channel/ReaderForRacingCars.java @@ -0,0 +1,43 @@ +package racingcar.util.channel; + +import camp.nextstep.edu.missionutils.Console; +import racingcar.util.constant.ConstStandard; + +public class ReaderForRacingCars extends Reader { + + private static final String SPLIT_SEPARATOR = ""; + + public ReaderForRacingCars() { + super(); + } + + @Override + public String read() { + String carsInput = Console.readLine().trim(); + + validateBlank(carsInput); + validateSeparator(carsInput); + validateNameLength(carsInput); + + return carsInput; + } + + private void validateSeparator(String carsInput) { + for (String str : carsInput.split(SPLIT_SEPARATOR)) { + if (ConstStandard.INVALID_NAMES_INPUT_SEPARATORS.contains(str)) { + throw new IllegalArgumentException(); + } + } + } + private void validateNameLength(String carsInput) { + for (String carName : carsInput.split(ConstStandard.CRITERIA_NAMES_INPUT_SEPARATOR)) { + // 순수 이름값에 대해서만 길이를 재기 위해 String.trim() 을 수행합니다. + carName = carName.trim(); + if (carName.length() > ConstStandard.CRITERIA_NAME_LENGTH_UPPER_LIMIT) { + throw new IllegalArgumentException(); + } + validateBlank(carName); + } + } + +} diff --git a/yummygyudon/code/java-racingcar/src/main/java/racingcar/util/channel/ReaderForTrial.java b/yummygyudon/code/java-racingcar/src/main/java/racingcar/util/channel/ReaderForTrial.java new file mode 100644 index 0000000..64c5eb6 --- /dev/null +++ b/yummygyudon/code/java-racingcar/src/main/java/racingcar/util/channel/ReaderForTrial.java @@ -0,0 +1,36 @@ +package racingcar.util.channel; + +import camp.nextstep.edu.missionutils.Console; + +public class ReaderForTrial extends Reader { + + private static final String REGEX_CHECK_AVAILABLE = "^[1-9][0-9]*$"; + + private static final String REGEX_CHECK_NUMERIC = "[^0-9]"; + + public ReaderForTrial() { + super(); + } + + @Override + public String read() { + String trialInput = Console.readLine().trim(); + + validateBlank(trialInput); + validateNumeric(trialInput); + validateAvailable(trialInput); + + return trialInput; + } + private void validateNumeric(String input) { + if (input.matches(REGEX_CHECK_NUMERIC)) { + throw new IllegalArgumentException(); + } + } + + private void validateAvailable(String input) { + if (!input.matches(REGEX_CHECK_AVAILABLE)) { + throw new IllegalArgumentException(); + } + } +} diff --git a/yummygyudon/code/java-racingcar/src/main/java/racingcar/util/constant/ConstStandard.java b/yummygyudon/code/java-racingcar/src/main/java/racingcar/util/constant/ConstStandard.java new file mode 100644 index 0000000..c76165a --- /dev/null +++ b/yummygyudon/code/java-racingcar/src/main/java/racingcar/util/constant/ConstStandard.java @@ -0,0 +1,13 @@ +package racingcar.util.constant; + +public abstract class ConstStandard { + + public static final int CRITERIA_MOVE = 4; + + public static final int CRITERIA_NAME_LENGTH_UPPER_LIMIT = 5; + + public static final String CRITERIA_NAMES_INPUT_SEPARATOR = ","; + + public static final String INVALID_NAMES_INPUT_SEPARATORS = "<>\"'[]{}~!@#$%^&*()\\-_+=:;."; + +} diff --git a/yummygyudon/code/java-racingcar/src/main/java/racingcar/util/constant/ConstVariable.java b/yummygyudon/code/java-racingcar/src/main/java/racingcar/util/constant/ConstVariable.java new file mode 100644 index 0000000..1a8cecd --- /dev/null +++ b/yummygyudon/code/java-racingcar/src/main/java/racingcar/util/constant/ConstVariable.java @@ -0,0 +1,9 @@ +package racingcar.util.constant; + +public abstract class ConstVariable { + public static final String RESULT_SEPARATOR = " : "; + public static final String RESULT_START_MESSAGE = "실행 결과"; + public static final String RESULT_MOVEMENT = "-"; + public static final String RESULT_WINNER_MESSAGE = "최종 우승자 : "; + public static final String RESULT_WINNER_SEPARATOR = ", "; +} diff --git a/yummygyudon/code/java-racingcar/src/main/java/racingcar/util/enums/Command.java b/yummygyudon/code/java-racingcar/src/main/java/racingcar/util/enums/Command.java new file mode 100644 index 0000000..257f4eb --- /dev/null +++ b/yummygyudon/code/java-racingcar/src/main/java/racingcar/util/enums/Command.java @@ -0,0 +1,17 @@ +package racingcar.util.enums; + +public enum Command { + ASK_CAR_NAMES("경주할 자동차 이름을 입력하세요.(이름은 쉼표(,) 기쥰으로 구분"), + ASK_TRIAL("시도할 회수는 몇회인가요?"), + ; + + private final String message; + + Command(String message) { + this.message = message; + } + + public String getMessage() { + return message; + } +} diff --git a/yummygyudon/code/java-racingcar/src/test/java/racingcar/ApplicationTest.java b/yummygyudon/code/java-racingcar/src/test/java/racingcar/ApplicationTest.java new file mode 100644 index 0000000..764ba4c --- /dev/null +++ b/yummygyudon/code/java-racingcar/src/test/java/racingcar/ApplicationTest.java @@ -0,0 +1,38 @@ +package racingcar; + +import camp.nextstep.edu.missionutils.test.NsTest; +import org.junit.jupiter.api.Test; + +import static camp.nextstep.edu.missionutils.test.Assertions.assertRandomNumberInRangeTest; +import static camp.nextstep.edu.missionutils.test.Assertions.assertSimpleTest; +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class ApplicationTest extends NsTest { + private static final int MOVING_FORWARD = 4; + private static final int STOP = 3; + + @Test + void 전진_정지() { + assertRandomNumberInRangeTest( + () -> { + run("pobi,woni", "1"); + assertThat(output()).contains("pobi : -", "woni : ", "최종 우승자 : pobi"); + }, + MOVING_FORWARD, STOP + ); + } + + @Test + void 이름에_대한_예외_처리() { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException("pobi,javaji", "1")) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @Override + public void runMain() { + Application.main(new String[]{}); + } +} diff --git a/yummygyudon/code/java-racingcar/src/test/java/racingcar/domain/RaceTest.java b/yummygyudon/code/java-racingcar/src/test/java/racingcar/domain/RaceTest.java new file mode 100644 index 0000000..922c96e --- /dev/null +++ b/yummygyudon/code/java-racingcar/src/test/java/racingcar/domain/RaceTest.java @@ -0,0 +1,99 @@ +package racingcar.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; +import org.junit.jupiter.params.provider.ValueSource; + +import java.util.List; +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("Race 도메인 테스트") +class RaceTest { + + private final Race testRace = new Race(List.of("pobi", "woni", "jun")); + private final int numberOfTestRacingCars = testRace.getRacingCars().size(); + + + @Nested + @DisplayName("조회 테스트") + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + class Getter { + + @Test + @DisplayName("올바른 RacingCar 리스트를 가져오는가") + void Case_Is_Expected_RacingCars() { + // when + List expectedRacingCars = List.of( + new RacingCar("pobi"), + new RacingCar("woni"), + new RacingCar("jun") + ); + + // then + assertThat(testRace.getRacingCars()).usingRecursiveComparison().isEqualTo(expectedRacingCars); + } + + @ParameterizedTest(name = "{index} : 이동 여부 = [pobi | woni | jun] - {0} , 기댓값 : {1}") + @DisplayName("올바른 Farthest Distance를 가져오는가") + @MethodSource("argumentsForGettingFarthestDistanceTest") + void Case_Is_Expected_Farthest_Distance(List givenMoveValues, Long expectedFarthestDistance) { + // when + List racingCars = testRace.getRacingCars(); + + // given + for (int index = 0; index < numberOfTestRacingCars; index++) { + RacingCar racingCar = racingCars.get(index); + Boolean isCarMove = givenMoveValues.get(index); + if (isCarMove) { + racingCar.move(); + } + } + + // then + assertThat(testRace.getMostFarthestDistance()).isEqualTo(expectedFarthestDistance); + } + private static Stream argumentsForGettingFarthestDistanceTest() { + return Stream.of( + Arguments.of(List.of(true, true, false), 1L), + Arguments.of(List.of(false, true, false), 2L), + Arguments.of(List.of(true, false, false), 2L), + Arguments.of(List.of(true, false, false), 3L), + Arguments.of(List.of(true, false, false), 4L) + ); + } + + } + + @Nested + @DisplayName("서킷 테스트") + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + class Circuit { + + private static final int MINIMUM_FARTHEST_DISTANCE = 0; + + @ParameterizedTest(name = "{index} : 해당 시나리오에서 발생할 수 있는 최대 거리 값 = {0}") + @DisplayName("올바른 방식으로 서킷이 진행되는가") + @ValueSource(longs = { + 1L, 2L, 3L, 4L, 5L, 6L, 7L, 8L, 9L, 10L + }) + void Case_Is_Expected_Circuit_Result(Long maxDistanceInScenario) { + // when + testRace.circuit(); + + // then + assertThat(testRace.getMostFarthestDistance()) + .isLessThanOrEqualTo(maxDistanceInScenario); // 초과하는 최장거리가 존재할 수 없음 + + assertThat(testRace.getMostFarthestDistance()) + .isGreaterThanOrEqualTo(MINIMUM_FARTHEST_DISTANCE); // 초과하는 최장거리가 존재할 수 없음 + } + } + +} \ No newline at end of file diff --git a/yummygyudon/code/java-racingcar/src/test/java/racingcar/domain/RacingCarTest.java b/yummygyudon/code/java-racingcar/src/test/java/racingcar/domain/RacingCarTest.java new file mode 100644 index 0000000..eb38938 --- /dev/null +++ b/yummygyudon/code/java-racingcar/src/test/java/racingcar/domain/RacingCarTest.java @@ -0,0 +1,83 @@ +package racingcar.domain; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.TestInstance; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.Arguments; +import org.junit.jupiter.params.provider.MethodSource; + +import java.util.stream.Stream; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("RacingCar 도메인 테스트") +class RacingCarTest { + private final RacingCar testRacingCar = new RacingCar("test"); + + @Nested + @DisplayName("조회 테스트") + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + class Getter { + + @Test + @DisplayName("올바른 Name 값을 가져오는가") + void Is_Getting_Expected_Name() { + assertThat(testRacingCar.getName()).isEqualTo("test"); + } + + @DisplayName("올바른 Distance 값을 가져오는가") + @ParameterizedTest(name = "{index} : 이동여부 = {0}, 기댓값 = {1}") + @MethodSource("argumentsForGettingDistanceTest") + void Is_Getting_Expected_Distance(boolean isMove, long expectedMovementResult) { + // when + if (isMove) { + testRacingCar.move(); + } + + // then + assertThat(testRacingCar.getDistance()).isEqualTo(expectedMovementResult); + } + private static Stream argumentsForGettingDistanceTest() { + return Stream.of( + Arguments.of(false, 0L), + Arguments.of(true, 1L), + Arguments.of(false, 1L), + Arguments.of(true, 2L) + ); + } + } + + @Nested + @DisplayName("이동 테스트") + @TestInstance(TestInstance.Lifecycle.PER_CLASS) + class Movement { + + @DisplayName("정상적으로 이동하는가") + @ParameterizedTest(name = "{index} : 이동여부 = {0}, 기댓값 = {1}") + @MethodSource("argumentsForMovementTest") + void Is_Expected_Movement(boolean isMove, long expectedMovementResult) { + // when + if (isMove) { + testRacingCar.move(); + } + + // then + assertThat(testRacingCar.getDistance()).isEqualTo(expectedMovementResult); + } + private static Stream argumentsForMovementTest() { + return Stream.of( + Arguments.of(false, 0L), + Arguments.of(true, 1L), + Arguments.of(false, 1L), + Arguments.of(true, 2L) + ); + } + + } + + + + +} \ No newline at end of file diff --git a/yummygyudon/code/java-racingcar/src/test/java/racingcar/util/RandomNumberGeneratorTest.java b/yummygyudon/code/java-racingcar/src/test/java/racingcar/util/RandomNumberGeneratorTest.java new file mode 100644 index 0000000..b0f3dca --- /dev/null +++ b/yummygyudon/code/java-racingcar/src/test/java/racingcar/util/RandomNumberGeneratorTest.java @@ -0,0 +1,26 @@ +package racingcar.util; + +import org.junit.jupiter.api.*; + +import static org.assertj.core.api.Assertions.assertThat; + +@DisplayName("랜덤 숫자 생성기 테스트") +@TestMethodOrder(MethodOrderer.OrderAnnotation.class) +class RandomNumberGeneratorTest { + + @Order(1) + @DisplayName("Case1. 정수만을 생성하는가") + @RepeatedTest(1_000) + void Case_1_Is_Generated_Integer() { + assertThat(RandomNumberGenerator.generate()) + .isInstanceOf(Integer.class); + } + + @Order(2) + @DisplayName("Case2. 요구되는 유효 범위내의 값만을 생성하는가") + @RepeatedTest(1_000) + void Case_2_Is_Generated_In_Available_Range() { + assertThat(RandomNumberGenerator.generate()) + .isIn(0, 1, 2, 3, 4, 5, 6, 7, 8, 9); + } +} \ No newline at end of file diff --git a/yummygyudon/code/java-racingcar/src/test/java/racingcar/util/channel/ReaderForRacingCarsTest.java b/yummygyudon/code/java-racingcar/src/test/java/racingcar/util/channel/ReaderForRacingCarsTest.java new file mode 100644 index 0000000..6d09ca5 --- /dev/null +++ b/yummygyudon/code/java-racingcar/src/test/java/racingcar/util/channel/ReaderForRacingCarsTest.java @@ -0,0 +1,55 @@ +package racingcar.util.channel; + +import camp.nextstep.edu.missionutils.test.NsTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import racingcar.Application; + +import static camp.nextstep.edu.missionutils.test.Assertions.assertSimpleTest; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayName("시도 횟수 입력기 테스트") +class ReaderForRacingCarsTest extends NsTest { + + @DisplayName("빈 값을 입력한 경우") + @ParameterizedTest(name = "{index} 번째 검사값 = {0}") + @ValueSource(strings = { + "\n", " ", " ", " " + }) + void Is_Expected_Validation_About_Blank(String invalidTrialInput) { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException(invalidTrialInput)) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @DisplayName("유효한 구분자가 아닌 기호를 쓰는 경우") + @ParameterizedTest(name = "{index} 번째 검사값 = {0}") + @ValueSource(strings = { + "pobi, woni. jun", "pobi/woni/jun", "pobi & woni & jun", "pobi + woni - jun", "PO_, #ㅅ#, @ㅇ@" + }) + void Is_Expected_Validation_About_Invalid_Separator(String invalidTrialInput) { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException(invalidTrialInput)) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @DisplayName("유효하지 않은 이름을 가진 경우") + @ParameterizedTest(name = "{index} 번째 검사값 = {0}") + @ValueSource(strings = { + " , , ", " , woni, jun", " , woni, " + }) + void Is_Expected_Validation_About_Invalid_Name(String invalidTrialInput) { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException(invalidTrialInput)) + .isInstanceOf(IllegalArgumentException.class) + ); + } + + @Override + protected void runMain() { + Application.main(new String[]{}); + } +} \ No newline at end of file diff --git a/yummygyudon/code/java-racingcar/src/test/java/racingcar/util/channel/ReaderForTrialTest.java b/yummygyudon/code/java-racingcar/src/test/java/racingcar/util/channel/ReaderForTrialTest.java new file mode 100644 index 0000000..e2d725f --- /dev/null +++ b/yummygyudon/code/java-racingcar/src/test/java/racingcar/util/channel/ReaderForTrialTest.java @@ -0,0 +1,61 @@ +package racingcar.util.channel; + +import camp.nextstep.edu.missionutils.test.NsTest; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; +import racingcar.Application; + +import static camp.nextstep.edu.missionutils.test.Assertions.assertSimpleTest; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +@DisplayName("시도 횟수 입력기 테스트") +class ReaderForTrialTest extends NsTest { + private static final String VALID_RACING_CAR_NAMES_INPUT = "pobi, woni, jun"; + + @DisplayName("빈 값을 입력한 경우") + @ParameterizedTest(name = "{index} 번째 검사값 = {0}") + @ValueSource(strings = { + "\n", " ", " ", " " + }) + void Is_Expected_Validation_About_Blank(String invalidTrialInput) { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException( + VALID_RACING_CAR_NAMES_INPUT, + invalidTrialInput) + ).isInstanceOf(IllegalArgumentException.class) + ); + } + @DisplayName("한글/영어/특수기호를 포함하고 있는 경우") + @ParameterizedTest(name = "{index} 번째 검사값 = {0}") + @ValueSource(strings = { + "ㄱㄴㄷ가나다", "abcABC", ".,/%$#^", "1234567890ㅑㅑ" + }) + void Is_Expected_Validation_About_Not_Numeric(String invalidTrialInput) { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException( + VALID_RACING_CAR_NAMES_INPUT, + invalidTrialInput) + ).isInstanceOf(IllegalArgumentException.class) + ); + } + + @DisplayName("0만 입력하거나 0으로 시작되는 잘못된 형식의 숫자를 입력 경우") + @ParameterizedTest(name = "{index} 번째 검사값 = {0}") + @ValueSource(strings = { + "00700", "0", "00", "01234567890" + }) + void Is_Expected_Validation_About_Invalid_Number(String invalidTrialInput) { + assertSimpleTest(() -> + assertThatThrownBy(() -> runException( + VALID_RACING_CAR_NAMES_INPUT, + invalidTrialInput) + ).isInstanceOf(IllegalArgumentException.class) + ); + } + + @Override + protected void runMain() { + Application.main(new String[]{}); + } +} \ No newline at end of file