diff --git a/.gitignore b/.gitignore
new file mode 100644
index 000000000..00eec072e
--- /dev/null
+++ b/.gitignore
@@ -0,0 +1,13 @@
+#For mac
+*.DS_Store
+
+#For idea
+.idea/
+
+#For Gradle
+*.gradle
+
+frontend/node_modules/.cache/
+*.mv.db
+*/*.json
+*/build
diff --git a/HELP.md b/HELP.md
new file mode 100644
index 000000000..6eb3147f1
--- /dev/null
+++ b/HELP.md
@@ -0,0 +1,32 @@
+# Read Me First
+The following was discovered as part of building this project:
+
+* The original package name 'com.example.gc-coffee' is invalid and this project uses 'com.example.gccoffee' instead.
+
+# Getting Started
+
+### Reference Documentation
+For further reference, please consider the following sections:
+
+* [Official Gradle documentation](https://docs.gradle.org)
+* [Spring Boot Gradle Plugin Reference Guide](https://docs.spring.io/spring-boot/docs/2.7.4/gradle-plugin/reference/html/)
+* [Create an OCI image](https://docs.spring.io/spring-boot/docs/2.7.4/gradle-plugin/reference/html/#build-image)
+* [Spring Web](https://docs.spring.io/spring-boot/docs/2.7.4/reference/htmlsingle/#web)
+* [Spring Data JPA](https://docs.spring.io/spring-boot/docs/2.7.4/reference/htmlsingle/#data.sql.jpa-and-spring-data)
+* [JDBC API](https://docs.spring.io/spring-boot/docs/2.7.4/reference/htmlsingle/#data.sql)
+
+### Guides
+The following guides illustrate how to use some features concretely:
+
+* [Building a RESTful Web Service](https://spring.io/guides/gs/rest-service/)
+* [Serving Web Content with Spring MVC](https://spring.io/guides/gs/serving-web-content/)
+* [Building REST services with Spring](https://spring.io/guides/tutorials/rest/)
+* [Accessing Data with JPA](https://spring.io/guides/gs/accessing-data-jpa/)
+* [Accessing Relational Data using JDBC with Spring](https://spring.io/guides/gs/relational-data-access/)
+* [Managing Transactions](https://spring.io/guides/gs/managing-transactions/)
+
+### Additional Links
+These additional references should also help you:
+
+* [Gradle Build Scans – insights for your project's build](https://scans.gradle.com#gradle)
+
diff --git a/README.md b/README.md
index cf5104cc0..8cbb22b0f 100644
--- a/README.md
+++ b/README.md
@@ -2,9 +2,10 @@
## 프로젝트 소개 😎
React로 만들어진 Front End가 정해져있는 상황에서,
백엔드 개발자가 Spring Boot로 상품관리 API를 구현하여 A-Z 최종 서비스를 완성시켜봅니다.
-클로닝 외에도 "본인만의 아이디어"를 추가하여 더 발전시켜 완성해봅니다.
+클로닝 외에도 "본인만의 아이디어"를 추가하여 더 발전시켜 완성해봅니다.
## 이곳은 공개 Repo입니다.
1. 이 repo를 fork한 뒤
2. 여러분의 개인 Repo에서 상품관리 API를 A-Z까지 작업하여
3. 개발이 끝나면 이 Repo에 PR을 보내어 제출을 완료해주세요.
+
diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar
new file mode 100644
index 000000000..249e5832f
Binary files /dev/null and b/gradle/wrapper/gradle-wrapper.jar differ
diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties
new file mode 100644
index 000000000..8049c684f
--- /dev/null
+++ b/gradle/wrapper/gradle-wrapper.properties
@@ -0,0 +1,5 @@
+distributionBase=GRADLE_USER_HOME
+distributionPath=wrapper/dists
+distributionUrl=https\://services.gradle.org/distributions/gradle-7.5-bin.zip
+zipStoreBase=GRADLE_USER_HOME
+zipStorePath=wrapper/dists
diff --git a/gradlew b/gradlew
new file mode 100755
index 000000000..a69d9cb6c
--- /dev/null
+++ b/gradlew
@@ -0,0 +1,240 @@
+#!/bin/sh
+
+#
+# Copyright © 2015-2021 the original authors.
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+# https://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+##############################################################################
+#
+# Gradle start up script for POSIX generated by Gradle.
+#
+# Important for running:
+#
+# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is
+# noncompliant, but you have some other compliant shell such as ksh or
+# bash, then to run this script, type that shell name before the whole
+# command line, like:
+#
+# ksh Gradle
+#
+# Busybox and similar reduced shells will NOT work, because this script
+# requires all of these POSIX shell features:
+# * functions;
+# * expansions «$var», «${var}», «${var:-default}», «${var+SET}»,
+# «${var#prefix}», «${var%suffix}», and «$( cmd )»;
+# * compound commands having a testable exit status, especially «case»;
+# * various built-in commands including «command», «set», and «ulimit».
+#
+# Important for patching:
+#
+# (2) This script targets any POSIX shell, so it avoids extensions provided
+# by Bash, Ksh, etc; in particular arrays are avoided.
+#
+# The "traditional" practice of packing multiple parameters into a
+# space-separated string is a well documented source of bugs and security
+# problems, so this is (mostly) avoided, by progressively accumulating
+# options in "$@", and eventually passing that to Java.
+#
+# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS,
+# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly;
+# see the in-line comments for details.
+#
+# There are tweaks for specific operating systems such as AIX, CygWin,
+# Darwin, MinGW, and NonStop.
+#
+# (3) This script is generated from the Groovy template
+# https://github.com/gradle/gradle/blob/master/subprojects/plugins/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt
+# within the Gradle project.
+#
+# You can find Gradle at https://github.com/gradle/gradle/.
+#
+##############################################################################
+
+# Attempt to set APP_HOME
+
+# Resolve links: $0 may be a link
+app_path=$0
+
+# Need this for daisy-chained symlinks.
+while
+ APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path
+ [ -h "$app_path" ]
+do
+ ls=$( ls -ld "$app_path" )
+ link=${ls#*' -> '}
+ case $link in #(
+ /*) app_path=$link ;; #(
+ *) app_path=$APP_HOME$link ;;
+ esac
+done
+
+APP_HOME=$( cd "${APP_HOME:-./}" && pwd -P ) || exit
+
+APP_NAME="Gradle"
+APP_BASE_NAME=${0##*/}
+
+# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"'
+
+# Use the maximum available, or set MAX_FD != -1 to use that value.
+MAX_FD=maximum
+
+warn () {
+ echo "$*"
+} >&2
+
+die () {
+ echo
+ echo "$*"
+ echo
+ exit 1
+} >&2
+
+# OS specific support (must be 'true' or 'false').
+cygwin=false
+msys=false
+darwin=false
+nonstop=false
+case "$( uname )" in #(
+ CYGWIN* ) cygwin=true ;; #(
+ Darwin* ) darwin=true ;; #(
+ MSYS* | MINGW* ) msys=true ;; #(
+ NONSTOP* ) nonstop=true ;;
+esac
+
+CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar
+
+
+# Determine the Java command to use to start the JVM.
+if [ -n "$JAVA_HOME" ] ; then
+ if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
+ # IBM's JDK on AIX uses strange locations for the executables
+ JAVACMD=$JAVA_HOME/jre/sh/java
+ else
+ JAVACMD=$JAVA_HOME/bin/java
+ fi
+ if [ ! -x "$JAVACMD" ] ; then
+ die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+ fi
+else
+ JAVACMD=java
+ which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+
+Please set the JAVA_HOME variable in your environment to match the
+location of your Java installation."
+fi
+
+# Increase the maximum file descriptors if we can.
+if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then
+ case $MAX_FD in #(
+ max*)
+ MAX_FD=$( ulimit -H -n ) ||
+ warn "Could not query maximum file descriptor limit"
+ esac
+ case $MAX_FD in #(
+ '' | soft) :;; #(
+ *)
+ ulimit -n "$MAX_FD" ||
+ warn "Could not set maximum file descriptor limit to $MAX_FD"
+ esac
+fi
+
+# Collect all arguments for the java command, stacking in reverse order:
+# * args from the command line
+# * the main class name
+# * -classpath
+# * -D...appname settings
+# * --module-path (only if needed)
+# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables.
+
+# For Cygwin or MSYS, switch paths to Windows format before running java
+if "$cygwin" || "$msys" ; then
+ APP_HOME=$( cygpath --path --mixed "$APP_HOME" )
+ CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" )
+
+ JAVACMD=$( cygpath --unix "$JAVACMD" )
+
+ # Now convert the arguments - kludge to limit ourselves to /bin/sh
+ for arg do
+ if
+ case $arg in #(
+ -*) false ;; # don't mess with options #(
+ /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath
+ [ -e "$t" ] ;; #(
+ *) false ;;
+ esac
+ then
+ arg=$( cygpath --path --ignore --mixed "$arg" )
+ fi
+ # Roll the args list around exactly as many times as the number of
+ # args, so each arg winds up back in the position where it started, but
+ # possibly modified.
+ #
+ # NB: a `for` loop captures its iteration list before it begins, so
+ # changing the positional parameters here affects neither the number of
+ # iterations, nor the values presented in `arg`.
+ shift # remove old arg
+ set -- "$@" "$arg" # push replacement arg
+ done
+fi
+
+# Collect all arguments for the java command;
+# * $DEFAULT_JVM_OPTS, $JAVA_OPTS, and $GRADLE_OPTS can contain fragments of
+# shell script including quotes and variable substitutions, so put them in
+# double quotes to make sure that they get re-expanded; and
+# * put everything else in single quotes, so that it's not re-expanded.
+
+set -- \
+ "-Dorg.gradle.appname=$APP_BASE_NAME" \
+ -classpath "$CLASSPATH" \
+ org.gradle.wrapper.GradleWrapperMain \
+ "$@"
+
+# Stop when "xargs" is not available.
+if ! command -v xargs >/dev/null 2>&1
+then
+ die "xargs is not available"
+fi
+
+# Use "xargs" to parse quoted args.
+#
+# With -n1 it outputs one arg per line, with the quotes and backslashes removed.
+#
+# In Bash we could simply go:
+#
+# readarray ARGS < <( xargs -n1 <<<"$var" ) &&
+# set -- "${ARGS[@]}" "$@"
+#
+# but POSIX shell has neither arrays nor command substitution, so instead we
+# post-process each arg (as a line of input to sed) to backslash-escape any
+# character that might be a shell metacharacter, then use eval to reverse
+# that process (while maintaining the separation between arguments), and wrap
+# the whole thing up as a single "set" statement.
+#
+# This will of course break if any of these variables contains a newline or
+# an unmatched quote.
+#
+
+eval "set -- $(
+ printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" |
+ xargs -n1 |
+ sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' |
+ tr '\n' ' '
+ )" '"$@"'
+
+exec "$JAVACMD" "$@"
diff --git a/gradlew.bat b/gradlew.bat
new file mode 100644
index 000000000..53a6b238d
--- /dev/null
+++ b/gradlew.bat
@@ -0,0 +1,91 @@
+@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo.
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH.
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo.
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME%
+echo.
+echo Please set the JAVA_HOME variable in your environment to match the
+echo location of your Java installation.
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/kdt-react-order-ui b/kdt-react-order-ui
new file mode 160000
index 000000000..95b44145c
--- /dev/null
+++ b/kdt-react-order-ui
@@ -0,0 +1 @@
+Subproject commit 95b44145cb6934843b9b2496820de134c2e9839f
diff --git a/sample.html b/sample.html
deleted file mode 100644
index 2db14018b..000000000
--- a/sample.html
+++ /dev/null
@@ -1,149 +0,0 @@
-
-
-
-
-
-
-
-
-
-
- Hello, world!
-
-
-
-
Grids & Circle
-
-
-
-
-
상품 목록
-
- -
-
-
-
커피콩
-
Columbia Nariñó
-
- 5000원
-
-
- -
-
-
-
커피콩
-
Columbia Nariñó
-
- 5000원
-
-
- -
-
-
-
커피콩
-
Columbia Nariñó
-
- 5000원
-
-
-
-
-
-
-
Summary
-
-
-
-
Columbia Nariñó 2개
-
-
-
Brazil Serra Do Caparaó 2개
-
-
-
Columbia Nariñó 2개
-
-
-
-
총금액
- 15000원
-
-
-
-
-
-
-
\ No newline at end of file
diff --git a/src/main/java/com/example/gccoffee/GcCoffeeApplication.java b/src/main/java/com/example/gccoffee/GcCoffeeApplication.java
new file mode 100644
index 000000000..a28ec83ba
--- /dev/null
+++ b/src/main/java/com/example/gccoffee/GcCoffeeApplication.java
@@ -0,0 +1,13 @@
+package com.example.gccoffee;
+
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@SpringBootApplication
+public class GcCoffeeApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(GcCoffeeApplication.class, args);
+ }
+
+}
diff --git a/src/main/java/com/example/gccoffee/JdbcUtils.java b/src/main/java/com/example/gccoffee/JdbcUtils.java
new file mode 100644
index 000000000..eb3bfdff0
--- /dev/null
+++ b/src/main/java/com/example/gccoffee/JdbcUtils.java
@@ -0,0 +1,19 @@
+package com.example.gccoffee;
+
+import java.nio.ByteBuffer;
+import java.sql.Timestamp;
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+public class JdbcUtils {
+
+ public static UUID toUUID(byte[] bytes) {
+ var byteBuffer = ByteBuffer.wrap(bytes);
+ return new UUID(byteBuffer.getLong(), byteBuffer.getLong());
+ }
+
+ public static LocalDateTime toLocalDateTime(Timestamp timestamp) {
+ return timestamp != null ? timestamp.toLocalDateTime() : null;
+ }
+
+}
diff --git a/src/main/java/com/example/gccoffee/configuration/MvcConfiguration.java b/src/main/java/com/example/gccoffee/configuration/MvcConfiguration.java
new file mode 100644
index 000000000..0341956c4
--- /dev/null
+++ b/src/main/java/com/example/gccoffee/configuration/MvcConfiguration.java
@@ -0,0 +1,13 @@
+package com.example.gccoffee.configuration;
+
+import org.springframework.context.annotation.Configuration;
+import org.springframework.web.servlet.config.annotation.CorsRegistry;
+import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
+
+@Configuration
+public class MvcConfiguration implements WebMvcConfigurer {
+ @Override
+ public void addCorsMappings(CorsRegistry registry) {
+ registry.addMapping("/api/**").allowedOrigins("*");
+ }
+}
diff --git a/src/main/java/com/example/gccoffee/controller/CreateOrderRequest.java b/src/main/java/com/example/gccoffee/controller/CreateOrderRequest.java
new file mode 100644
index 000000000..841e0e3ad
--- /dev/null
+++ b/src/main/java/com/example/gccoffee/controller/CreateOrderRequest.java
@@ -0,0 +1,10 @@
+package com.example.gccoffee.controller;
+
+import com.example.gccoffee.model.OrderItem;
+
+import java.util.List;
+
+public record CreateOrderRequest(
+ String email, String address, String postcode, List orderItems
+) {
+}
diff --git a/src/main/java/com/example/gccoffee/controller/CreateProductRequest.java b/src/main/java/com/example/gccoffee/controller/CreateProductRequest.java
new file mode 100644
index 000000000..4c838eeaf
--- /dev/null
+++ b/src/main/java/com/example/gccoffee/controller/CreateProductRequest.java
@@ -0,0 +1,6 @@
+package com.example.gccoffee.controller;
+
+import com.example.gccoffee.model.Category;
+
+public record CreateProductRequest(String productName, Category category, long price, String description) {
+}
diff --git a/src/main/java/com/example/gccoffee/controller/ProductController.java b/src/main/java/com/example/gccoffee/controller/ProductController.java
new file mode 100644
index 000000000..f9f79a152
--- /dev/null
+++ b/src/main/java/com/example/gccoffee/controller/ProductController.java
@@ -0,0 +1,40 @@
+package com.example.gccoffee.controller;
+
+import com.example.gccoffee.service.ProductService;
+import org.springframework.stereotype.Controller;
+import org.springframework.ui.Model;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+
+@Controller
+public class ProductController {
+
+ private final ProductService productService;
+
+ public ProductController(ProductService productService) {
+ this.productService = productService;
+ }
+
+ @GetMapping("/products")
+ public String productsPage(Model model) {
+ var products = productService.getAllProducts();
+ model.addAttribute("products", products);
+ return "product-list";
+ }
+
+ @GetMapping("new-product")
+ public String newProductPage() {
+ return "new-product";
+ }
+
+ @PostMapping("/products")
+ public String newProduct(CreateProductRequest createProductRequest) {
+ productService.createProduct(
+ createProductRequest.productName(),
+ createProductRequest.category(),
+ createProductRequest.price(),
+ createProductRequest.description());
+ return "redirect:/products";
+ }
+
+}
diff --git a/src/main/java/com/example/gccoffee/controller/api/OrderRestController.java b/src/main/java/com/example/gccoffee/controller/api/OrderRestController.java
new file mode 100644
index 000000000..618cd0bb7
--- /dev/null
+++ b/src/main/java/com/example/gccoffee/controller/api/OrderRestController.java
@@ -0,0 +1,29 @@
+package com.example.gccoffee.controller.api;
+
+import com.example.gccoffee.controller.CreateOrderRequest;
+import com.example.gccoffee.model.Email;
+import com.example.gccoffee.model.Order;
+import com.example.gccoffee.service.OrderService;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RestController;
+
+@RestController
+public class OrderRestController {
+
+ private final OrderService orderService;
+
+ public OrderRestController(OrderService orderService) {
+ this.orderService = orderService;
+ }
+
+ @PostMapping("/api/v1/orders")
+ public Order createOrder(@RequestBody CreateOrderRequest orderRequest) {
+ return orderService.createOrder(
+ new Email(orderRequest.email()),
+ orderRequest.address(),
+ orderRequest.postcode(),
+ orderRequest.orderItems()
+ );
+ }
+}
diff --git a/src/main/java/com/example/gccoffee/controller/api/ProductRestController.java b/src/main/java/com/example/gccoffee/controller/api/ProductRestController.java
new file mode 100644
index 000000000..901035f61
--- /dev/null
+++ b/src/main/java/com/example/gccoffee/controller/api/ProductRestController.java
@@ -0,0 +1,29 @@
+package com.example.gccoffee.controller.api;
+
+import com.example.gccoffee.model.Category;
+import com.example.gccoffee.model.Product;
+import com.example.gccoffee.service.ProductService;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RequestParam;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.List;
+import java.util.Optional;
+
+@RestController
+public class ProductRestController {
+
+ private final ProductService productService;
+
+ public ProductRestController(ProductService productService) {
+ this.productService = productService;
+ }
+
+ @GetMapping("/api/v1/products")
+ public List productList(@RequestParam Optional category) {
+ return category
+ .map(productService::getProductsByCategory)
+ .orElse(productService.getAllProducts());
+ }
+
+}
diff --git a/src/main/java/com/example/gccoffee/model/Category.java b/src/main/java/com/example/gccoffee/model/Category.java
new file mode 100644
index 000000000..8369f5058
--- /dev/null
+++ b/src/main/java/com/example/gccoffee/model/Category.java
@@ -0,0 +1,5 @@
+package com.example.gccoffee.model;
+
+public enum Category {
+ COFFEE_BEAN_PACKAGE
+}
diff --git a/src/main/java/com/example/gccoffee/model/Email.java b/src/main/java/com/example/gccoffee/model/Email.java
new file mode 100644
index 000000000..d2f2b264d
--- /dev/null
+++ b/src/main/java/com/example/gccoffee/model/Email.java
@@ -0,0 +1,49 @@
+package com.example.gccoffee.model;
+
+import org.springframework.util.Assert;
+
+import java.util.Objects;
+import java.util.regex.Pattern;
+
+public class Email {
+
+ private final String address;
+
+ public Email(String address) {
+ Assert.notNull(address, "address should not be null");
+ Assert.isTrue(address.length() >= 4 && address.length() <= 50, "address length must be between 4 and 50 characters.");
+ Assert.isTrue(checkAddress(address), "Invalid email address");
+ this.address = address;
+ }
+
+ private static boolean checkAddress(String address) {
+ return Pattern.matches("\\b[\\w\\.-]+@[\\w\\.-]+\\.\\w{2,4}\\b", address);
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (o == null || getClass() != o.getClass()) return false;
+ Email email = (Email) o;
+ return Objects.equals(address, email.address);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(address);
+ }
+
+ @Override
+ public String toString() {
+ final StringBuilder sb = new StringBuilder("Email{");
+ sb.append("address='").append(address).append('\'');
+ sb.append('}');
+ return sb.toString();
+ }
+
+ public String getAddress() {
+ return address;
+ }
+
+}
+
diff --git a/src/main/java/com/example/gccoffee/model/Order.java b/src/main/java/com/example/gccoffee/model/Order.java
new file mode 100644
index 000000000..c2e0b2a00
--- /dev/null
+++ b/src/main/java/com/example/gccoffee/model/Order.java
@@ -0,0 +1,78 @@
+package com.example.gccoffee.model;
+
+import lombok.Getter;
+import lombok.RequiredArgsConstructor;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.UUID;
+
+@Getter
+public class Order {
+ private final UUID orderId;
+ private final Email email;
+ private String address;
+ private String postcode;
+ private final List orderItems;
+ private OrderStatus orderStatus;
+ private final LocalDateTime createdAt;
+ private LocalDateTime updatedAt;
+
+ public Order(UUID orderId, Email email, String address, String postcode, List orderItems, OrderStatus orderStatus, LocalDateTime createdAt, LocalDateTime updatedAt) {
+ this.orderId = orderId;
+ this.email = email;
+ this.address = address;
+ this.postcode = postcode;
+ this.orderItems = orderItems;
+ this.orderStatus = orderStatus;
+ this.createdAt = createdAt;
+ this.updatedAt = updatedAt;
+ }
+
+ public UUID getOrderId() {
+ return orderId;
+ }
+
+ public Email getEmail() {
+ return email;
+ }
+
+ public String getAddress() {
+ return address;
+ }
+
+ public String getPostcode() {
+ return postcode;
+ }
+
+ public List getOrderItems() {
+ return orderItems;
+ }
+
+ public OrderStatus getOrderStatus() {
+ return orderStatus;
+ }
+
+ public LocalDateTime getCreatedAt() {
+ return createdAt;
+ }
+
+ public LocalDateTime getUpdatedAt() {
+ return updatedAt;
+ }
+
+ public void setAddress(String address) {
+ this.address = address;
+ this.updatedAt = LocalDateTime.now();
+ }
+
+ public void setPostcode(String postcode) {
+ this.postcode = postcode;
+ this.updatedAt = LocalDateTime.now();
+ }
+
+ public void setOrderStatus(OrderStatus orderStatus) {
+ this.orderStatus = orderStatus;
+ this.updatedAt = LocalDateTime.now();
+ }
+}
diff --git a/src/main/java/com/example/gccoffee/model/OrderItem.java b/src/main/java/com/example/gccoffee/model/OrderItem.java
new file mode 100644
index 000000000..fbabc841a
--- /dev/null
+++ b/src/main/java/com/example/gccoffee/model/OrderItem.java
@@ -0,0 +1,6 @@
+package com.example.gccoffee.model;
+
+import java.util.UUID;
+
+public record OrderItem(UUID productId, Category category, long price, int quantity) {
+}
\ No newline at end of file
diff --git a/src/main/java/com/example/gccoffee/model/OrderStatus.java b/src/main/java/com/example/gccoffee/model/OrderStatus.java
new file mode 100644
index 000000000..d3869ef4a
--- /dev/null
+++ b/src/main/java/com/example/gccoffee/model/OrderStatus.java
@@ -0,0 +1,10 @@
+package com.example.gccoffee.model;
+
+public enum OrderStatus {
+ ACCEPTED,
+ PAYMENT_CONFIRMED,
+ READY_FOR_DELIVERY,
+ SHIPPED,
+ SETTLED,
+ CANCELLED
+}
diff --git a/src/main/java/com/example/gccoffee/model/Product.java b/src/main/java/com/example/gccoffee/model/Product.java
new file mode 100644
index 000000000..04eba876f
--- /dev/null
+++ b/src/main/java/com/example/gccoffee/model/Product.java
@@ -0,0 +1,82 @@
+package com.example.gccoffee.model;
+
+import java.time.LocalDateTime;
+import java.util.UUID;
+
+public class Product {
+ private final UUID productId;
+ private String productName;
+ private Category category;
+ private long price;
+ private String description;
+ private final LocalDateTime createdAt;
+ private LocalDateTime updatedAt;
+
+ public Product(UUID productId, String productName, Category category, long price) {
+ this.productId = productId;
+ this.productName = productName;
+ this.category = category;
+ this.price = price;
+ this.createdAt = LocalDateTime.now();
+ this.updatedAt = LocalDateTime.now();
+ }
+
+ public Product(UUID productId, String productName, Category category, long price, String description, LocalDateTime createdAt, LocalDateTime updatedAt) {
+ this.productId = productId;
+ this.productName = productName;
+ this.category = category;
+ this.price = price;
+ this.description = description;
+ this.createdAt = createdAt;
+ this.updatedAt = updatedAt;
+ }
+
+ public UUID getProductId() {
+ return productId;
+ }
+
+ public String getProductName() {
+ return productName;
+ }
+
+ public Category getCategory() {
+ return category;
+ }
+
+ public long getPrice() {
+ return price;
+ }
+
+ public String getDescription() {
+ return description;
+ }
+
+ public LocalDateTime getCreatedAt() {
+ return createdAt;
+ }
+
+ public LocalDateTime getUpdatedAt() {
+ return updatedAt;
+ }
+
+ public void setPrice(long price) {
+ this.price = price;
+ this.updatedAt = LocalDateTime.now();
+ }
+
+ public void setCategory(Category category) {
+ this.category = category;
+ this.updatedAt = LocalDateTime.now();
+ }
+
+ public void setProductName(String productName) {
+ this.productName = productName;
+ this.updatedAt = LocalDateTime.now();
+ }
+
+ public void setDescription(String description) {
+ this.description = description;
+ this.updatedAt = LocalDateTime.now();
+ }
+
+}
diff --git a/src/main/java/com/example/gccoffee/repository/OrderJdbcRepository.java b/src/main/java/com/example/gccoffee/repository/OrderJdbcRepository.java
new file mode 100644
index 000000000..118f51895
--- /dev/null
+++ b/src/main/java/com/example/gccoffee/repository/OrderJdbcRepository.java
@@ -0,0 +1,60 @@
+package com.example.gccoffee.repository;
+
+import com.example.gccoffee.model.Order;
+import com.example.gccoffee.model.OrderItem;
+import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
+import org.springframework.stereotype.Repository;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.time.LocalDateTime;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+@Repository
+public class OrderJdbcRepository implements OrderRepository {
+
+ private final NamedParameterJdbcTemplate jdbcTemplate;
+
+ public OrderJdbcRepository(NamedParameterJdbcTemplate jdbcTemplate) {
+ this.jdbcTemplate = jdbcTemplate;
+ }
+
+ @Override
+ @Transactional
+ public Order insert(Order order) {
+ jdbcTemplate.update("INSERT INTO orders(order_id, email, address, postcode, order_status, created_at, updated_at) " +
+ "VALUES (UUID_TO_BIN(:orderId), :email, :address, :postcode, :orderStatus, :createdAt, :updatedAt)",
+ toOrderParamMap(order));
+ order.getOrderItems()
+ .forEach(item ->
+ jdbcTemplate.update("INSERT INTO order_items(order_id, product_id, category, price, quantity, created_at, updated_at) " +
+ "VALUES (UUID_TO_BIN(:orderId), UUID_TO_BIN(:productId), :category, :price, :quantity, :createdAt, :updatedAt)",
+ toOrderItemParamMap(order.getOrderId(), order.getCreatedAt(), order.getUpdatedAt(), item)));
+ return order;
+ }
+
+ private Map toOrderParamMap(Order order) {
+ var paramMap = new HashMap();
+ paramMap.put("orderId", order.getOrderId().toString().getBytes());
+ paramMap.put("email", order.getEmail().getAddress());
+ paramMap.put("address", order.getAddress());
+ paramMap.put("postcode", order.getPostcode());
+ paramMap.put("orderStatus", order.getOrderStatus().toString());
+ paramMap.put("createdAt", order.getCreatedAt());
+ paramMap.put("updatedAt", order.getUpdatedAt());
+ return paramMap;
+ }
+
+ private Map toOrderItemParamMap(UUID orderId, LocalDateTime createdAt, LocalDateTime updatedAt, OrderItem item) {
+ var paramMap = new HashMap();
+ paramMap.put("orderId", orderId.toString().getBytes());
+ paramMap.put("productId", item.productId().toString().getBytes());
+ paramMap.put("category", item.category().toString());
+ paramMap.put("price", item.price());
+ paramMap.put("quantity", item.quantity());
+ paramMap.put("createdAt", createdAt);
+ paramMap.put("updatedAt", updatedAt);
+ return paramMap;
+ }
+}
diff --git a/src/main/java/com/example/gccoffee/repository/OrderRepository.java b/src/main/java/com/example/gccoffee/repository/OrderRepository.java
new file mode 100644
index 000000000..096ecae80
--- /dev/null
+++ b/src/main/java/com/example/gccoffee/repository/OrderRepository.java
@@ -0,0 +1,9 @@
+package com.example.gccoffee.repository;
+
+import com.example.gccoffee.model.Order;
+
+public interface OrderRepository {
+
+ Order insert(Order order);
+
+}
diff --git a/src/main/java/com/example/gccoffee/repository/ProductJdbcRepository.java b/src/main/java/com/example/gccoffee/repository/ProductJdbcRepository.java
new file mode 100644
index 000000000..551a545a5
--- /dev/null
+++ b/src/main/java/com/example/gccoffee/repository/ProductJdbcRepository.java
@@ -0,0 +1,113 @@
+package com.example.gccoffee.repository;
+
+import com.example.gccoffee.model.Category;
+import com.example.gccoffee.model.Product;
+import org.springframework.dao.EmptyResultDataAccessException;
+import org.springframework.jdbc.core.RowMapper;
+import org.springframework.jdbc.core.namedparam.NamedParameterJdbcTemplate;
+import org.springframework.stereotype.Repository;
+
+import java.util.*;
+
+import static com.example.gccoffee.JdbcUtils.toLocalDateTime;
+import static com.example.gccoffee.JdbcUtils.toUUID;
+
+@Repository
+public class ProductJdbcRepository implements ProductRepository {
+
+ private final NamedParameterJdbcTemplate jdbcTemplate;
+
+ public ProductJdbcRepository(NamedParameterJdbcTemplate jdbcTemplate) {
+ this.jdbcTemplate = jdbcTemplate;
+ }
+
+ @Override
+ public List findAll() {
+ return jdbcTemplate.query("select * from products", productRowMapper);
+ }
+
+ @Override
+ public Product insert(Product product) {
+ var update = jdbcTemplate.update("INSERT INTO products(product_id, product_name, category, price, description, created_at, updated_at)" +
+ " VALUES (UUID_TO_BIN(:productId), :productName, :category, :price, :description, :createdAt, :updatedAt)", toParamMap(product));
+ if (update != 1) {
+ throw new RuntimeException("Noting was inserted");
+ }
+ return product;
+ }
+
+ @Override
+ public Product update(Product product) {
+ var update = jdbcTemplate.update(
+ "UPDATE products SET product_name = :productName, category = :category, price = :price, description = :description, created_at = :createdAt, updated_at = :updatedAt" +
+ " WHERE product_id = UUID_TO_BIN(:productId)",
+ toParamMap(product)
+ );
+ if (update != 1) {
+ throw new RuntimeException("Nothing was updated");
+ }
+ return product;
+ }
+
+ @Override
+ public Optional findById(UUID productId) {
+ try {
+ return Optional.ofNullable(
+ jdbcTemplate.queryForObject("SELECT * FROM products WHERE product_id = UUID_TO_BIN(:productId)",
+ Collections.singletonMap("productId", productId.toString().getBytes()), productRowMapper)
+ );
+ } catch (EmptyResultDataAccessException e) {
+ return Optional.empty();
+ }
+ }
+
+ @Override
+ public Optional findByName(String productName) {
+ try {
+ return Optional.ofNullable(
+ jdbcTemplate.queryForObject("SELECT * FROM products WHERE product_name = :productName",
+ Collections.singletonMap("productName", productName), productRowMapper)
+ );
+ } catch (EmptyResultDataAccessException e) {
+ return Optional.empty();
+ }
+ }
+
+ @Override
+ public List findByCategory(Category category) {
+ return jdbcTemplate.query(
+ "SELECT * FROM products WHERE category = :category",
+ Collections.singletonMap("category", category.toString()),
+ productRowMapper
+ );
+ }
+
+ @Override
+ public void deleteAll() {
+ jdbcTemplate.update("DELETE FROM products", Collections.emptyMap());
+ }
+
+ private static final RowMapper productRowMapper = (resultSet, i) -> {
+ var productId = toUUID(resultSet.getBytes("product_id"));
+ var productName = resultSet.getString("product_name");
+ var category = Category.valueOf(resultSet.getString("category"));
+ var price = resultSet.getLong("price");
+ var description = resultSet.getString("description");
+ var createdAt = toLocalDateTime(resultSet.getTimestamp("created_at"));
+ var updatedAt = toLocalDateTime(resultSet.getTimestamp("updated_at"));
+ return new Product(productId, productName, category, price, description, createdAt, updatedAt);
+ };
+
+ private Map toParamMap(Product product) {
+ var paramMap = new HashMap();
+ paramMap.put("productId", product.getProductId().toString().getBytes());
+ paramMap.put("productName", product.getProductName());
+ paramMap.put("category", product.getCategory().toString());
+ paramMap.put("price", product.getPrice());
+ paramMap.put("description", product.getDescription());
+ paramMap.put("createdAt", product.getCreatedAt());
+ paramMap.put("updatedAt", product.getUpdatedAt());
+ return paramMap;
+ }
+
+}
diff --git a/src/main/java/com/example/gccoffee/repository/ProductRepository.java b/src/main/java/com/example/gccoffee/repository/ProductRepository.java
new file mode 100644
index 000000000..293c4442e
--- /dev/null
+++ b/src/main/java/com/example/gccoffee/repository/ProductRepository.java
@@ -0,0 +1,26 @@
+package com.example.gccoffee.repository;
+
+import com.example.gccoffee.model.Category;
+import com.example.gccoffee.model.Product;
+
+import java.util.List;
+import java.util.Optional;
+import java.util.UUID;
+
+public interface ProductRepository {
+
+ List findAll();
+
+ Product insert(Product product);
+
+ Product update(Product product);
+
+ Optional findById(UUID productId);
+
+ Optional findByName(String productName);
+
+ List findByCategory(Category category);
+
+ void deleteAll();
+
+}
diff --git a/src/main/java/com/example/gccoffee/service/DefaultOrderService.java b/src/main/java/com/example/gccoffee/service/DefaultOrderService.java
new file mode 100644
index 000000000..743f9f3fc
--- /dev/null
+++ b/src/main/java/com/example/gccoffee/service/DefaultOrderService.java
@@ -0,0 +1,37 @@
+package com.example.gccoffee.service;
+
+import com.example.gccoffee.model.Email;
+import com.example.gccoffee.model.Order;
+import com.example.gccoffee.model.OrderItem;
+import com.example.gccoffee.model.OrderStatus;
+import com.example.gccoffee.repository.OrderRepository;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.UUID;
+
+@Service
+public class DefaultOrderService implements OrderService {
+
+ private final OrderRepository orderRepository;
+
+ public DefaultOrderService(OrderRepository orderRepository) {
+ this.orderRepository = orderRepository;
+ }
+
+ @Override
+ public Order createOrder(Email email, String address, String postcode, List orderItems) {
+ Order order = new Order(
+ UUID.randomUUID(),
+ email,
+ address,
+ postcode,
+ orderItems,
+ OrderStatus.ACCEPTED,
+ LocalDateTime.now(),
+ LocalDateTime.now());
+ return orderRepository.insert(order);
+ }
+
+}
diff --git a/src/main/java/com/example/gccoffee/service/DefaultProductService.java b/src/main/java/com/example/gccoffee/service/DefaultProductService.java
new file mode 100644
index 000000000..42317918e
--- /dev/null
+++ b/src/main/java/com/example/gccoffee/service/DefaultProductService.java
@@ -0,0 +1,43 @@
+package com.example.gccoffee.service;
+
+import com.example.gccoffee.model.Category;
+import com.example.gccoffee.model.Product;
+import com.example.gccoffee.repository.ProductRepository;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDateTime;
+import java.util.List;
+import java.util.UUID;
+
+@Service
+public class DefaultProductService implements ProductService {
+
+ private final ProductRepository productRepository;
+
+ public DefaultProductService(ProductRepository productRepository) {
+ this.productRepository = productRepository;
+ }
+
+ @Override
+ public List getProductsByCategory(Category category) {
+ return productRepository.findByCategory(category);
+ }
+
+ @Override
+ public List getAllProducts() {
+ return productRepository.findAll();
+ }
+
+ @Override
+ public Product createProduct(String productName, Category category, long price) {
+ var product = new Product(UUID.randomUUID(), productName, category, price);
+ return productRepository.insert(product);
+ }
+
+ @Override
+ public Product createProduct(String productName, Category category, long price, String description) {
+ var product = new Product(UUID.randomUUID(), productName, category, price, description, LocalDateTime.now(), LocalDateTime.now());
+ return productRepository.insert(product);
+ }
+
+}
diff --git a/src/main/java/com/example/gccoffee/service/OrderService.java b/src/main/java/com/example/gccoffee/service/OrderService.java
new file mode 100644
index 000000000..2d000efd2
--- /dev/null
+++ b/src/main/java/com/example/gccoffee/service/OrderService.java
@@ -0,0 +1,11 @@
+package com.example.gccoffee.service;
+
+import com.example.gccoffee.model.Email;
+import com.example.gccoffee.model.Order;
+import com.example.gccoffee.model.OrderItem;
+
+import java.util.List;
+
+public interface OrderService {
+ Order createOrder(Email email, String address, String postcode, List orderItems);
+}
diff --git a/src/main/java/com/example/gccoffee/service/ProductService.java b/src/main/java/com/example/gccoffee/service/ProductService.java
new file mode 100644
index 000000000..2a716e52a
--- /dev/null
+++ b/src/main/java/com/example/gccoffee/service/ProductService.java
@@ -0,0 +1,18 @@
+package com.example.gccoffee.service;
+
+import com.example.gccoffee.model.Category;
+import com.example.gccoffee.model.Product;
+
+import java.util.List;
+
+public interface ProductService {
+
+ List getProductsByCategory(Category category);
+
+ List getAllProducts();
+
+ Product createProduct(String productName, Category category, long price);
+
+ Product createProduct(String productName, Category category, long price, String description);
+
+}
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
new file mode 100644
index 000000000..c27c22fb8
--- /dev/null
+++ b/src/main/resources/application.yml
@@ -0,0 +1,5 @@
+spring:
+ datasource:
+ url: jdbc:mysql://localhost/order_mgmt
+ username: root
+ password: root1234!
diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql
new file mode 100644
index 000000000..206753965
--- /dev/null
+++ b/src/main/resources/schema.sql
@@ -0,0 +1,36 @@
+CREATE TABLE products
+(
+ product_id BINARY(16) PRIMARY KEY,
+ product_name VARCHAR(20) NOT NULL,
+ category VARCHAR(50) NOT NULL,
+ price bigint NOT NULL,
+ description VARCHAR(500) DEFAULT NULL,
+ created_at datetime(6) NOT NULL,
+ updated_at datetime(6) DEFAULT NULL
+);
+
+CREATE TABLE orders
+(
+ order_id binary(16) PRIMARY KEY,
+ email VARCHAR(50) NOT NULL,
+ address VARCHAR(200) NOT NULL,
+ postcode VARCHAR(200) NOT NULL,
+ order_status VARCHAR(50) NOT NULL,
+ created_at datetime(6) NOT NULL,
+ updated_at datetime(6) DEFAULT NULL
+);
+
+CREATE TABLE order_items
+(
+ seq bigint NOT NULL PRIMARY KEY AUTO_INCREMENT,
+ order_id binary(16) NOT NULL,
+ product_id binary(16) NOT NULL,
+ category VARCHAR(50) NOT NULL,
+ price bigint NOT NULL,
+ quantity int NOT NULL,
+ created_at datetime(6) NOT NULL,
+ updated_at datetime(6) DEFAULT NULL,
+ INDEX (order_id),
+ CONSTRAINT fk_order_items_to_order FOREIGN KEY (order_id) REFERENCES orders (order_id) ON DELETE CASCADE,
+ CONSTRAINT fk_order_items_to_product FOREIGN KEY (product_id) REFERENCES products (product_id)
+);
\ No newline at end of file
diff --git a/src/main/resources/templates/new-product.html b/src/main/resources/templates/new-product.html
new file mode 100644
index 000000000..17a3abff5
--- /dev/null
+++ b/src/main/resources/templates/new-product.html
@@ -0,0 +1,40 @@
+
+
+
+
+
+
+
+
+
+
+ Grids & Circles
+
+
+Grids & Circles
+Production Management
+
+
+
\ No newline at end of file
diff --git a/src/main/resources/templates/product-list.html b/src/main/resources/templates/product-list.html
new file mode 100644
index 000000000..990678f99
--- /dev/null
+++ b/src/main/resources/templates/product-list.html
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+ Grids & Circles
+
+
+Grids & Circles
+Production Management
+
+
+
+
+ | Id |
+ Name |
+ Category |
+ Price |
+ Description |
+ CreatedAt |
+ UpdatedAt |
+
+
+
+
+ |
+ |
+ |
+ |
+ |
+ |
+ |
+
+
+
+
+
\ No newline at end of file
diff --git a/src/test/java/com/example/gccoffee/EmailTest.java b/src/test/java/com/example/gccoffee/EmailTest.java
new file mode 100644
index 000000000..8cec2c77b
--- /dev/null
+++ b/src/test/java/com/example/gccoffee/EmailTest.java
@@ -0,0 +1,31 @@
+package com.example.gccoffee;
+
+import com.example.gccoffee.model.Email;
+import org.junit.jupiter.api.Test;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+class EmailTest {
+
+ @Test
+ public void testInvalidEmail() {
+ assertThrows(IllegalArgumentException.class, () -> {
+ var email = new Email("accccc");
+ });
+ }
+
+ @Test
+ public void testValidEmail() {
+ var email = new Email("hello@gmail.com");
+ assertEquals("hello@gmail.com", email.getAddress());
+ }
+
+ @Test
+ public void testEqEmail() {
+ var email = new Email("hello@gmail.com");
+ var email2 = new Email("hello@gmail.com");
+ assertEquals(email, email2);
+ }
+
+}
\ No newline at end of file
diff --git a/src/test/java/com/example/gccoffee/GcCoffeeApplicationTests.java b/src/test/java/com/example/gccoffee/GcCoffeeApplicationTests.java
new file mode 100644
index 000000000..c7cb2517a
--- /dev/null
+++ b/src/test/java/com/example/gccoffee/GcCoffeeApplicationTests.java
@@ -0,0 +1,13 @@
+package com.example.gccoffee;
+
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.test.context.SpringBootTest;
+
+@SpringBootTest
+class GcCoffeeApplicationTests {
+
+ @Test
+ void contextLoads() {
+ }
+
+}
diff --git a/src/test/java/com/example/gccoffee/ProductJdbcRepositoryTest.java b/src/test/java/com/example/gccoffee/ProductJdbcRepositoryTest.java
new file mode 100644
index 000000000..e0df7c9d1
--- /dev/null
+++ b/src/test/java/com/example/gccoffee/ProductJdbcRepositoryTest.java
@@ -0,0 +1,109 @@
+package com.example.gccoffee;
+
+import com.example.gccoffee.model.Category;
+import com.example.gccoffee.model.Product;
+import com.example.gccoffee.repository.ProductRepository;
+import com.wix.mysql.EmbeddedMysql;
+import com.wix.mysql.ScriptResolver;
+import com.wix.mysql.config.Charset;
+import org.junit.jupiter.api.*;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.boot.test.context.SpringBootTest;
+import org.springframework.test.context.ActiveProfiles;
+
+import java.util.UUID;
+
+import static com.wix.mysql.EmbeddedMysql.anEmbeddedMysql;
+import static com.wix.mysql.config.MysqldConfig.aMysqldConfig;
+import static com.wix.mysql.distribution.Version.v8_0_11;
+import static org.hamcrest.MatcherAssert.assertThat;
+import static org.hamcrest.Matchers.is;
+import static org.hamcrest.Matchers.samePropertyValuesAs;
+
+@SpringBootTest
+@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
+@ActiveProfiles("test")
+@TestInstance(TestInstance.Lifecycle.PER_CLASS)
+class ProductJdbcRepositoryTest {
+
+ static EmbeddedMysql embeddedMysql;
+ //맥 m1 사용으로 인한 embedded mysql 사용 불가
+
+ @BeforeAll
+ static void setup() {
+ var config = aMysqldConfig(v8_0_11)
+ .withCharset(Charset.UTF8)
+ .withPort(2215)
+ .withUser("test", "test1234!")
+ .withTimeZone("Asia/Seoul")
+ .build();
+ embeddedMysql = anEmbeddedMysql(config)
+ .addSchema("test-order_mgmt", ScriptResolver.classPathScript("schema.sql"))
+ .start();
+ }
+
+ @AfterAll
+ static void cleanup() {
+ embeddedMysql.stop();
+ }
+
+ @Autowired
+ ProductRepository repository;
+
+ private final Product newProduct = new Product(UUID.randomUUID(), "new-product", Category.COFFEE_BEAN_PACKAGE, 1000L);
+
+ @Test
+ @Order(1)
+ @DisplayName("상품을 추가할 수 있다.")
+ void testInsert() {
+ repository.insert(newProduct);
+ var all = repository.findAll();
+ assertThat(all.isEmpty(), is(false));
+ }
+
+ @Test
+ @Order(2)
+ @DisplayName("상품을 이름으로 조회할 수 있다.")
+ void testFindByName() {
+ var product = repository.findByName(newProduct.getProductName());
+ assertThat(product.isEmpty(), is(false));
+ }
+
+ @Test
+ @Order(3)
+ @DisplayName("상품을아이디로 조회할 수 있다.")
+ void testFindById() {
+ var product = repository.findById(newProduct.getProductId());
+ assertThat(product.isEmpty(), is(false));
+ }
+
+ @Test
+ @Order(4)
+ @DisplayName("상품들을 카테고리로 조회할 수 있다.")
+ void testFindByCategory() {
+ var product = repository.findByCategory(Category.COFFEE_BEAN_PACKAGE);
+ assertThat(product.isEmpty(), is(false));
+ }
+
+ @Test
+ @Order(5)
+ @DisplayName("상품을 수정할 수 있다.")
+ void testUpdate() {
+ newProduct.setProductName("updated-product");
+ repository.update(newProduct);
+
+ var product = repository.findById(newProduct.getProductId());
+ assertThat(product.isEmpty(), is(false));
+ assertThat(product.get(), samePropertyValuesAs(newProduct));
+ }
+
+ @Test
+ @Order(6)
+ @DisplayName("상품을 전체 삭제한다.")
+ void testDeleteAll() {
+ repository.deleteAll();
+ var all = repository.findAll();
+ assertThat(all.isEmpty(), is(true));
+ }
+
+}
\ No newline at end of file