diff --git a/README.md b/README.md index cafde8a2c..10c0c5c6c 100644 --- a/README.md +++ b/README.md @@ -1 +1,46 @@ -# spring-gift-product \ No newline at end of file +# spring-gift-product + +--- + +## Step1 + +**요구 사항**
+아래와 같이 http 메세지를 받도록 구현한다 + +```http request +GET /api/products HTTP/1.1 +``` + +```http request +HTTP/1.1 200 +Content-Type: application/json + +[ + { + "id": 8146027, + "name": "아이스 카페 아메리카노 T", + "price": 4500, + "imageUrl": "https://st.kakaocdn.net/product/gift/product/20231010111814_9a667f9eccc943648797925498bdd8a3.jpg" + } +] +``` + + +**필요 조건**
+상품 데이터 관리 +현재는 별도의 데이터베이스가 없으므로 적절한 컬렉션을 이용하여 메모리에 저장한다. +```java +public class ProductController { + private final Map productMap = new HashMap<>(); +} +``` +---- +## 구현 기능 +- product 조회 +- product 추가 +- product 수정 +- product 삭제 + + + + diff --git a/build.gradle b/build.gradle index df7db9334..c13eda0cf 100644 --- a/build.gradle +++ b/build.gradle @@ -19,6 +19,7 @@ repositories { dependencies { implementation 'org.springframework.boot:spring-boot-starter-jdbc' + implementation 'org.springframework.boot:spring-boot-starter-data-jdbc' implementation 'org.springframework.boot:spring-boot-starter-thymeleaf' implementation 'org.springframework.boot:spring-boot-starter-web' runtimeOnly 'com.h2database:h2' diff --git a/src/main/http/product/Product.http b/src/main/http/product/Product.http new file mode 100644 index 000000000..e5f6a467f --- /dev/null +++ b/src/main/http/product/Product.http @@ -0,0 +1,31 @@ +### get product list +GET http://localhost:8080/api/products +Content-Type: application/json + +### get product +GET http://localhost:8080/api/products/1 +Content-Type: application/json + +### create product +POST http://localhost:8080/api/products +Content-Type: application/json + +{ + "name": "아메리카노", + "price": "4500", + "imageUrl": "http://hello" +} + +### update product +PUT http://localhost:8080/api/products/1 +Content-Type: application/json + +{ + "name": "아메리카노2", + "price": "5000", + "imageUrl": "http://hello" +} + +### delete product +DELETE http://localhost:8080/api/products/1 +Content-Type: application/json \ No newline at end of file diff --git a/src/main/java/gift/global/exception/BusinessException.java b/src/main/java/gift/global/exception/BusinessException.java new file mode 100644 index 000000000..8c46ea1b5 --- /dev/null +++ b/src/main/java/gift/global/exception/BusinessException.java @@ -0,0 +1,21 @@ +package gift.global.exception; + +import gift.global.response.ErrorCode; + +public class BusinessException extends RuntimeException { + private final ErrorCode errorCode; + + public BusinessException(ErrorCode errorCode) { + super(errorCode.getMessage()); + this.errorCode = errorCode; + } + + public ErrorCode getErrorCode() { + return errorCode; + } + + @Override + public String toString() { + return errorCode.toString() + " : " + super.toString(); + } +} diff --git a/src/main/java/gift/global/handler/GlobalExceptionHandler.java b/src/main/java/gift/global/handler/GlobalExceptionHandler.java new file mode 100644 index 000000000..48486ee22 --- /dev/null +++ b/src/main/java/gift/global/handler/GlobalExceptionHandler.java @@ -0,0 +1,16 @@ +package gift.global.handler; + +import gift.global.exception.BusinessException; +import gift.global.response.ErrorResponseDto; +import gift.global.utils.ResponseHelper; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.ExceptionHandler; +import org.springframework.web.bind.annotation.RestControllerAdvice; + +@RestControllerAdvice +public class GlobalExceptionHandler { + @ExceptionHandler(BusinessException.class) + public ResponseEntity handleBusinessException(BusinessException e) { + return ResponseHelper.createErrorResponse(e.getErrorCode()); + } +} diff --git a/src/main/java/gift/global/response/ErrorCode.java b/src/main/java/gift/global/response/ErrorCode.java new file mode 100644 index 000000000..ccf8c8953 --- /dev/null +++ b/src/main/java/gift/global/response/ErrorCode.java @@ -0,0 +1,28 @@ +package gift.global.response; + +public enum ErrorCode { + // Product + PRODUCT_NOT_FOUND_ERROR(400, "EP001", "Product Not Found Error"), + ; + private final int status; + private final String code; + private final String message; + + ErrorCode(int status, String code, String message) { + this.status = status; + this.code = code; + this.message = message; + } + + public int getStatus() { + return status; + } + + public String getCode() { + return code; + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/gift/global/response/ErrorResponseDto.java b/src/main/java/gift/global/response/ErrorResponseDto.java new file mode 100644 index 000000000..1c631b9c1 --- /dev/null +++ b/src/main/java/gift/global/response/ErrorResponseDto.java @@ -0,0 +1,7 @@ +package gift.global.response; + +public record ErrorResponseDto(String code, String message) { + public ErrorResponseDto(ErrorCode errorCode) { + this(errorCode.getCode(), errorCode.getMessage()); + } +} diff --git a/src/main/java/gift/global/response/ResultCode.java b/src/main/java/gift/global/response/ResultCode.java new file mode 100644 index 000000000..bf2c46adc --- /dev/null +++ b/src/main/java/gift/global/response/ResultCode.java @@ -0,0 +1,35 @@ +package gift.global.response; + +public enum ResultCode { + // Product + GET_ALL_PRODUCTS_SUCCESS(200, "P001", "모든 제품 조회 성공"), + GET_PRODUCT_BY_ID_SUCCESS(200, "P002", "단일 제품 조회 성공"), + CREATE_PRODUCT_SUCCESS(200, "P003", "제품 추가 성공"), + UPDATE_PRODUCT_SUCCESS(200, "P002", "제품 수정 성공"), + DELETE_PRODUCT_SUCCESS(200, "P002", "제품 삭제 성공"), + + ; + + // status 를 HttpStatus 로 관리하는 것이 좋을까, 아니면 int로 관리하는 것이 좋을까? + private final int status; + private final String code; + private final String message; + + ResultCode(int status, String code, String message) { + this.status = status; + this.code = code; + this.message = message; + } + + public int getStatus() { + return status; + } + + public String getCode() { + return code; + } + + public String getMessage() { + return message; + } +} diff --git a/src/main/java/gift/global/response/ResultResponseDto.java b/src/main/java/gift/global/response/ResultResponseDto.java new file mode 100644 index 000000000..bb3aec0c1 --- /dev/null +++ b/src/main/java/gift/global/response/ResultResponseDto.java @@ -0,0 +1,8 @@ +package gift.global.response; + +public record ResultResponseDto(String code, String message, T data) { + // 자바 record 에서 생성자를 만들 경우, 반드시 canonical(표준) 생성자를 사용해야 한다!! + public ResultResponseDto(ResultCode resultCode, T data) { + this(resultCode.getCode(), resultCode.getMessage(), data); + } +} diff --git a/src/main/java/gift/global/response/SimpleResultResponseDto.java b/src/main/java/gift/global/response/SimpleResultResponseDto.java new file mode 100644 index 000000000..895a220db --- /dev/null +++ b/src/main/java/gift/global/response/SimpleResultResponseDto.java @@ -0,0 +1,7 @@ +package gift.global.response; + +public record SimpleResultResponseDto(String code, String message) { + public SimpleResultResponseDto(ResultCode resultCode) { + this(resultCode.getCode(), resultCode.getMessage()); + } +} diff --git a/src/main/java/gift/global/utils/ResponseHelper.java b/src/main/java/gift/global/utils/ResponseHelper.java new file mode 100644 index 000000000..928d30f68 --- /dev/null +++ b/src/main/java/gift/global/utils/ResponseHelper.java @@ -0,0 +1,26 @@ +package gift.global.utils; + +import gift.global.response.*; +import org.springframework.http.ResponseEntity; + +public class ResponseHelper { + private ResponseHelper() {} + + public static ResponseEntity> createResponse(ResultCode resultCode, T data) { + ResultResponseDto resultResponseDto = new ResultResponseDto<>(resultCode, data); + return org.springframework.http.ResponseEntity.status(resultCode.getStatus()) + .body(resultResponseDto); + } + + public static ResponseEntity createSimpleResponse(ResultCode resultCode) { + var resultResponseDto = new SimpleResultResponseDto(resultCode); + return ResponseEntity.status(resultCode.getStatus()) + .body(resultResponseDto); + } + + public static ResponseEntity createErrorResponse(ErrorCode errorCode) { + ErrorResponseDto errorResponseDto = new ErrorResponseDto(errorCode); + return ResponseEntity.status(errorCode.getStatus()) + .body(errorResponseDto); + } +} diff --git a/src/main/java/gift/product/controller/ProductController.java b/src/main/java/gift/product/controller/ProductController.java new file mode 100644 index 000000000..e3356b1d4 --- /dev/null +++ b/src/main/java/gift/product/controller/ProductController.java @@ -0,0 +1,53 @@ +package gift.product.controller; + +import gift.global.response.ResultCode; +import gift.global.response.ResultResponseDto; +import gift.global.response.SimpleResultResponseDto; +import gift.global.utils.ResponseHelper; +import gift.product.dto.ProductRequestDto; +import gift.product.domain.Product; +import gift.product.service.ProductService; +import org.springframework.http.ResponseEntity; +import org.springframework.web.bind.annotation.*; + +import java.util.List; + +@RestController +@RequestMapping("/api/products") +public class ProductController { + private final ProductService productService; + + public ProductController(ProductService productService) { + this.productService = productService; + } + + @GetMapping("") + public ResponseEntity>> getAllProducts() { + List products = productService.getAllProducts(); + return ResponseHelper.createResponse(ResultCode.GET_ALL_PRODUCTS_SUCCESS, products); + } + + @GetMapping("/{id}") + public ResponseEntity> getProductById(@PathVariable(name = "id") Long id) { + Product product = productService.getProductById(id); + return ResponseHelper.createResponse(ResultCode.GET_PRODUCT_BY_ID_SUCCESS, product); + } + + @PostMapping("") + public ResponseEntity createProduct(@RequestBody ProductRequestDto productRequestDto) { + productService.createProduct(productRequestDto.toServiceDto()); + return ResponseHelper.createSimpleResponse(ResultCode.CREATE_PRODUCT_SUCCESS); + } + + @PutMapping("/{id}") + public ResponseEntity updateProduct(@PathVariable(name = "id") Long id, @RequestBody ProductRequestDto productRequestDto) { + productService.updateProduct(productRequestDto.toServiceDto(id)); + return ResponseHelper.createSimpleResponse(ResultCode.UPDATE_PRODUCT_SUCCESS); + } + + @DeleteMapping("/{id}") + public ResponseEntity deleteProduct(@PathVariable(name = "id") Long id) { + productService.deleteProduct(id); + return ResponseHelper.createSimpleResponse(ResultCode.DELETE_PRODUCT_SUCCESS); + } +} \ No newline at end of file diff --git a/src/main/java/gift/product/domain/Product.java b/src/main/java/gift/product/domain/Product.java new file mode 100644 index 000000000..edc7add4b --- /dev/null +++ b/src/main/java/gift/product/domain/Product.java @@ -0,0 +1,52 @@ +package gift.product.domain; + +import gift.product.dto.ProductRequestDto; +import org.springframework.data.annotation.Id; +import org.springframework.data.relational.core.mapping.Table; + +@Table("products") +public class Product { + @Id + private Long id; + private String name; + private int price; + private String imageUrl; + + // JDBC 에서 엔티티 클래스를 인스턴스화할 때 반드시 기본 생성자와 파라미터 생성자가 필요하다 + public Product() {} + + public Product(Long id, String name, int price, String imageUrl) { + this.id = id; + this.name = name; + this.price = price; + this.imageUrl = imageUrl; + } + + public Product(Long id, ProductRequestDto productRequestDto) { + this.id = id; + this.name = productRequestDto.name(); + this.price = productRequestDto.price(); + this.imageUrl = productRequestDto.imageUrl(); + } + + + public Long getId() { + return id; + } + + public String getName() { + return name; + } + + public int getPrice() { + return price; + } + + public String getImageUrl() { + return imageUrl; + } + + public boolean checkNew() { + return id == null; + } +} diff --git a/src/main/java/gift/product/dto/ProductRequestDto.java b/src/main/java/gift/product/dto/ProductRequestDto.java new file mode 100644 index 000000000..e2dfd53ff --- /dev/null +++ b/src/main/java/gift/product/dto/ProductRequestDto.java @@ -0,0 +1,18 @@ +package gift.product.dto; + +import java.util.Objects; + +public record ProductRequestDto(String name, int price, String imageUrl) { + public ProductRequestDto { + Objects.requireNonNull(name); + Objects.requireNonNull(imageUrl); + } + + public ServiceDto toServiceDto() { + return new ServiceDto(null, this.name, this.price, this.imageUrl); + } + + public ServiceDto toServiceDto(Long id) { + return new ServiceDto(id, this.name, this.price, this.imageUrl); + } +} diff --git a/src/main/java/gift/product/dto/ServiceDto.java b/src/main/java/gift/product/dto/ServiceDto.java new file mode 100644 index 000000000..019e1842a --- /dev/null +++ b/src/main/java/gift/product/dto/ServiceDto.java @@ -0,0 +1,9 @@ +package gift.product.dto; + +import gift.product.domain.Product; + +public record ServiceDto(Long id, String name, int price, String imageUrl) { + public Product toProduct() { + return new Product(id, name, price, imageUrl); + } +} diff --git a/src/main/java/gift/product/exception/ProductNotFoundException.java b/src/main/java/gift/product/exception/ProductNotFoundException.java new file mode 100644 index 000000000..693aadee3 --- /dev/null +++ b/src/main/java/gift/product/exception/ProductNotFoundException.java @@ -0,0 +1,10 @@ +package gift.product.exception; + +import gift.global.exception.BusinessException; +import gift.global.response.ErrorCode; + +public class ProductNotFoundException extends BusinessException { + public ProductNotFoundException() { + super(ErrorCode.PRODUCT_NOT_FOUND_ERROR); + } +} diff --git a/src/main/java/gift/product/repository/ProductRepository.java b/src/main/java/gift/product/repository/ProductRepository.java new file mode 100644 index 000000000..72df257f5 --- /dev/null +++ b/src/main/java/gift/product/repository/ProductRepository.java @@ -0,0 +1,62 @@ +package gift.product.repository; + +import gift.product.domain.Product; +import org.springframework.jdbc.core.simple.JdbcClient; +import org.springframework.stereotype.Repository; +import org.springframework.util.Assert; + +import java.util.List; +import java.util.Optional; + +@Repository +public class ProductRepository { + private final JdbcClient jdbcClient; + + public ProductRepository(JdbcClient jdbcClient) { + this.jdbcClient = jdbcClient; + } + + public List findAll() { + String sql = "select * from products"; + return jdbcClient.sql(sql) + .query(Product.class) + .list(); + } + + public Optional findById(Long id) { + String sql = "select * from products where id = ?"; + return jdbcClient.sql(sql) + .param(id) + .query(Product.class) + .optional(); + } + + public void save(Product product) { + Assert.notNull(product, "Product must not be null"); + if (product.checkNew()) { + String sql = "INSERT INTO products (name, price, image_url) VALUES (?, ?, ?)"; + jdbcClient.sql(sql) + .param(product.getName()) + .param(product.getPrice()) + .param(product.getImageUrl()) + .update(); + + } + if (!product.checkNew()) { + String sql = "UPDATE products SET name = ?, price = ?, image_url = ? WHERE id = ?"; + jdbcClient.sql(sql) + .param(product.getName()) + .param(product.getPrice()) + .param(product.getImageUrl()) + .param(product.getId()) + .update(); + } + } + + public void deleteById(Long id) { + String sql = "delete from products where id = ?"; + jdbcClient.sql(sql) + .param(id) + .update(); + } +} diff --git a/src/main/java/gift/product/service/ProductService.java b/src/main/java/gift/product/service/ProductService.java new file mode 100644 index 000000000..1a2dda1f0 --- /dev/null +++ b/src/main/java/gift/product/service/ProductService.java @@ -0,0 +1,46 @@ +package gift.product.service; + +import gift.product.dto.ServiceDto; +import gift.product.domain.Product; +import gift.product.exception.ProductNotFoundException; +import gift.product.repository.ProductRepository; +import org.springframework.stereotype.Service; + +import java.util.List; + +@Service +public class ProductService { + private final ProductRepository productRepository; + + public ProductService(ProductRepository productRepository) { + this.productRepository = productRepository; + } + + public List getAllProducts() { + return productRepository.findAll(); + } + + public Product getProductById(Long id) { + return productRepository.findById(id) + .orElseThrow(ProductNotFoundException::new); + } + + public void createProduct(ServiceDto serviceDto) { + productRepository.save(serviceDto.toProduct()); + } + + public void updateProduct(ServiceDto serviceDto) { + validateProductExists(serviceDto.id()); + productRepository.save(serviceDto.toProduct()); + } + + public void deleteProduct(Long id) { + validateProductExists(id); + productRepository.deleteById(id); + } + + private void validateProductExists(Long id) { + productRepository.findById(id) + .orElseThrow(ProductNotFoundException::new); + } +} diff --git a/src/main/java/gift/view/ViewController.java b/src/main/java/gift/view/ViewController.java new file mode 100644 index 000000000..06b16be34 --- /dev/null +++ b/src/main/java/gift/view/ViewController.java @@ -0,0 +1,13 @@ +package gift.view; + +import org.springframework.stereotype.Controller; +import org.springframework.web.bind.annotation.GetMapping; + +@Controller +public class ViewController { + + @GetMapping("/") + public String index() { + return "index"; + } +} diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index 3d16b65f4..763017f2c 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1 +1,15 @@ spring.application.name=spring-gift + +spring.datasource.url=jdbc:h2:mem:testdb +spring.datasource.driverClassName=org.h2.Driver +spring.datasource.username=sa +spring.datasource.password=1234 + +# H2 Console Configuration +spring.h2.console.enabled=true +spring.h2.console.path=/h2-console + +# SQL Script Initialization +spring.sql.init.mode=always +spring.sql.init.schema-locations=classpath:schema.sql +spring.sql.init.data-locations=classpath:data.sql \ No newline at end of file diff --git a/src/main/resources/data.sql b/src/main/resources/data.sql new file mode 100644 index 000000000..594dd9348 --- /dev/null +++ b/src/main/resources/data.sql @@ -0,0 +1,2 @@ +INSERT INTO products (name, price, image_url) VALUES ('test1', 10000, 'http://'); +INSERT INTO products (name, price, image_url) VALUES ('test2', 20000, 'http://'); diff --git a/src/main/resources/schema.sql b/src/main/resources/schema.sql new file mode 100644 index 000000000..e38d50c96 --- /dev/null +++ b/src/main/resources/schema.sql @@ -0,0 +1,6 @@ +CREATE TABLE IF NOT EXISTS products ( + id BIGINT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + price BIGINT NOT NULL, + image_url VARCHAR(255) +); \ No newline at end of file diff --git a/src/main/resources/templates/index.html b/src/main/resources/templates/index.html new file mode 100644 index 000000000..72fc92ae8 --- /dev/null +++ b/src/main/resources/templates/index.html @@ -0,0 +1,255 @@ + + + + + Manage Products + + + + + + +
+
+

Manage Products

+
+ + +
+
+ +
+
+ +
+
+ +
+
+ + + + + + + + + + + + + + + + + + + + + +
NamePriceImage URLActions
+ + +
+
+ + + + + + +