Skip to content

Commit

Permalink
충남대 BE_김기웅_2주차 과제(0단계) (#83)
Browse files Browse the repository at this point in the history
* Initial commit

* feat: set up the project

* 충남대 BE_김기웅_1주차 과제(1단계, 2단계, 3단계) (#82)

* docs: README 작성

* feat(Product): 상품 클래스 생성

* feat(ProductController): 모든 상품 조회 기능

* feat(ProductController): ID로 상품 조회 기능

* feat(ProductController): 상품 추가 기능

* feat(ProductController): 상품 삭제 기능

* feat(ProductController): 상품 정보 수정 기능

* docs: README 파일에 STEP2 기능 추가

* feat(productManagement.html): 관리자 페이지 템플릿 구현

* feat(ProductViewController): admin 페이지 상품 전체 조회 기능

* feat(productForm.html): 상품 등록, 수정 폼 구현

* feat(ProductViewController): 상품 등록, 수정 기능 구현

* feat(productForm.html): 상품 등록, 수정 기능 구현

* feat(productForm.html): 상품 삭제 기능 구현

* fix(ProductViewController): 중복 ID 상품 add 시도 후 상품 수정 페이지로 리다이렉션 되는 버그 수정 (admin 메인 페이지로 리다이렉션)

* fix(productForm.html): 상품 추가 시 1 이상의 ID만 입력 가능하도록 수정

* chore: 데이터베이스 환경설정

* docs(README.md): step3 기능 목록 추가

* feat(Application): DB 초기화
- JdbcTemplate 사용
- id가 1이상이 되도록 DB레벨에서 강제

* feat(productRepository): 상품 레파지토리 클래스 생성
- JdbcTemplate 객체 생성
- SQL Query 준비 (바인딩 필요)

* feat(productRepository): 상품 레파지토리 클래스 DB CRUD 로직 추가

* feat(ProductController): 메모리를 레파지토리 객체로 대체
- ProductRepository 주입

* feat(ProductController): 핸들러메서드의 로직 중복 제거
- create, update, delete시 id로 상품을 검색해서 존재하는지 확인하는 로직을 isProductExists 메서드로 추출

* style: 자바 코드 컨벤션 적용

* style(README.md): 빈 줄 삭제

* refactor: Application에서 테이블 생성 쿼리 분리
- schema.sql 생성

* refactor: 프로젝트 구조 변경
- 역할에 따라 패키지 구분

* Initial commit

---------

Co-authored-by: 박재성(Jason) <[email protected]>
  • Loading branch information
lit2020 and wotjd243 authored Jul 4, 2024
1 parent 517cf76 commit d553953
Show file tree
Hide file tree
Showing 11 changed files with 436 additions and 2 deletions.
44 changes: 43 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
@@ -1 +1,43 @@
# spring-gift-wishlist
# spring-gift-product

# 1주차 - 상품관리 - 스프링 입문

# 🚀 1단계 - 상품 API

## 과제 진행 요구 사항
- 미션은 선물하기 상품 관리 저장소를 포크하고 클론하는 것으로 시작한다.
- 저장소에 GitHub 사용자 이름으로 브랜치가 생성되었는지 확인한다.
- 저장소를 내 계정으로 포크한다.
- 기능을 구현하기 전 README.md에 구현할 기능 목록을 정리해 추가한다.
- Git의 커밋 단위는 앞 단계에서 README.md에 정리한 기능 목록 단위로 추가한다.
- AngularJS Git Commit Message Conventions을 참고해 커밋 메시지를 작성한다.

## 기능 요구 사항
- 상품을 조회, 추가, 수정, 삭제할 수 있는 간단한 HTTP API를 구현한다.
- HTTP 요청과 응답은 JSON 형식으로 주고받는다.
- 현재는 별도의 데이터베이스가 없으므로 적절한 자바 컬렉션 프레임워크를 사용하여 메모리에 저장한다.

## 개발 할 기능
- [x] 상품을 표현할 Product 만들기
- [x] 상품 조회 API 구현
- [x] 상품 추가 API 구현
- [x] 상품 수정 API 구현
- [x] 상품 삭제 API 구현


# 🚀 2단계 - 관리자 화면

## 개발 할 기능
- [x] 상품 조회 기능
- [x] 상품 추가 기능
- [x] 상품 수정 기능
- [x] 상품 삭제 기능

# 🚀 3단계 - 데이터베이스 적용
## 개발 할 기능
- [x] DB 초기화 (앱 실행 시점)
- [x] 상품 전체 조회 (Read)
- [x] 특정 id의 상품 조회 (Read)
- [x] 상품 추가 기능 (Create)
- [x] 상품 수정 기능 (Update)
- [x] 상품 삭제 기능 (Delete)
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ version = '0.0.1-SNAPSHOT'
java {
toolchain {
languageVersion = JavaLanguageVersion.of(21)
vendor = JvmVendorSpec.AMAZON
}
}

Expand Down
3 changes: 2 additions & 1 deletion src/main/java/gift/Application.java
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class Application {
public class Application {

public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
Expand Down
48 changes: 48 additions & 0 deletions src/main/java/gift/DTO/Product.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
package gift.DTO;

public class Product {

private Long id;
private String name;
private int price;
private String imageUrl;

public Product(Long id, String name, int price, String imageUrl) {
this.id = id;
this.name = name;
this.price = price;
this.imageUrl = imageUrl;
}

public Long getId() {
return id;
}

public void setId(Long id) {
this.id = id;
}

public String getName() {
return name;
}

public void setName(String name) {
this.name = name;
}

public int getPrice() {
return price;
}

public void setPrice(int price) {
this.price = price;
}

public String getImageUrl() {
return imageUrl;
}

public void setImageUrl(String imageUrl) {
this.imageUrl = imageUrl;
}
}
96 changes: 96 additions & 0 deletions src/main/java/gift/controller/ProductController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,96 @@
package gift.controller;

import gift.DTO.Product;
import gift.repository.ProductRepository;
import java.util.List;
import java.util.Optional;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/api/products")
public class ProductController {

private final ProductRepository productRepository;

// 생성자를 사용하여 ProductRepository 초기화
public ProductController(ProductRepository productRepository) {
this.productRepository = productRepository;
}

/**
* 모든 상품 조회
*
* @return 모든 상품 목록
*/
@GetMapping
public ResponseEntity<List<Product>> getProducts() {
List<Product> products = productRepository.getAllProducts();
return new ResponseEntity<>(products, HttpStatus.OK);
}

/**
* id로 특정 상품 조회
*
* @param id 조회할 상품의 id
* @return 조회된 상품 객체와 200 OK, 해당 id가 없으면 404 NOT FOUND
*/
@GetMapping("/{id}")
public ResponseEntity<Product> getProductById(@PathVariable Long id) {
Optional<Product> product = productRepository.getProductById(id);
return product.map(value -> new ResponseEntity<>(value, HttpStatus.OK))
.orElseGet(() -> new ResponseEntity<>(HttpStatus.NOT_FOUND));
}

/**
* 새로운 상품 추가
*
* @param product 추가할 상품
* @return 같은 ID의 상품이 존재하지 않으면 201 Created, 아니면 400 Bad Request
*/
@PostMapping
public ResponseEntity<Product> addProduct(@RequestBody Product product) {
if (isProductExists(product.getId())) {
return new ResponseEntity<>(HttpStatus.BAD_REQUEST); // 400 Bad Request
}
productRepository.addProduct(product);
return new ResponseEntity<>(product, HttpStatus.CREATED); // 201 Created
}

/**
* 상품 삭제
*
* @param id 삭제할 상품의 id
* @return 삭제에 성공하면 204 NO CONTENT, 해당 ID의 상품이 없으면 404 NOT FOUND
*/
@DeleteMapping("/{id}")
public ResponseEntity<Void> deleteProduct(@PathVariable Long id) {
if (!isProductExists(id)) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND);
}
productRepository.deleteProduct(id);
return new ResponseEntity<>(HttpStatus.NO_CONTENT);
}

/**
* 상품 정보 수정
*
* @param id 수정할 상품의 id
* @param updatedProduct 새로운 상품 객체
* @return 상품 정보 수정에 성공하면 200 OK, 해당 id의 상품이 없으면 404 NOT FOUND
*/
@PutMapping("/{id}")
public ResponseEntity<Product> updateProduct(@PathVariable Long id,
@RequestBody Product updatedProduct) {
if (!isProductExists(id)) {
return new ResponseEntity<>(HttpStatus.NOT_FOUND); // 404 Not Found
}
productRepository.updateProduct(updatedProduct);
return new ResponseEntity<>(updatedProduct, HttpStatus.OK); // 200 OK
}

private boolean isProductExists(Long id) {
return productRepository.getProductById(id).isPresent();
}
}
103 changes: 103 additions & 0 deletions src/main/java/gift/controller/ProductViewController.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
package gift.controller;

import gift.DTO.Product;
import java.util.List;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.client.HttpClientErrorException;
import org.springframework.web.client.RestTemplate;

@Controller
@RequestMapping("/admin")
public class ProductViewController {

private final RestTemplate restTemplate;

public ProductViewController() {
this.restTemplate = new RestTemplate();
}

@GetMapping
public String showProductManagementPage(Model model) {
try {
List<Product> products = restTemplate.getForObject("http://localhost:8080/api/products",
List.class);
if (products == null) {
model.addAttribute("products", List.of());
return "productManagement"; // 템플릿 파일 이름
}
model.addAttribute("products", products);
} catch (Exception e) {
model.addAttribute("error", "Failed to load products: " + e.getMessage());
}
return "productManagement"; // admin 템플릿 파일 이름
}

@GetMapping("/form")
public String showProductForm(@RequestParam(required = false) Long id, Model model) {
if (id == null) {
model.addAttribute("product", new Product(0L, "", 0, ""));
return "productForm"; // 폼 템플릿 파일
}

try {
Product product = restTemplate.getForObject("http://localhost:8080/api/products/" + id,
Product.class);
model.addAttribute("product", product);
} catch (HttpClientErrorException e) {
model.addAttribute("error", "Product not found: " + e.getMessage());
return "redirect:/admin";
}

return "productForm"; // 폼 템플릿 파일
}

@PostMapping("/form")
public String saveProduct(@ModelAttribute Product product,
@RequestParam(required = false, name = "_method") String method, Model model) {
if (isUpdateMethod(method)) {
return updateProduct(product, model);
}
return createProduct(product, model);
}

private boolean isUpdateMethod(String method) {
return "put".equalsIgnoreCase(method);
}

private String updateProduct(Product product, Model model) {
try {
restTemplate.put("http://localhost:8080/api/products/" + product.getId(), product);
} catch (Exception e) {
e.printStackTrace();
model.addAttribute("error", "Failed to save product: " + e.getMessage());
return "productForm";
}
return "redirect:/admin";
}

private String createProduct(Product product, Model model) {
try {
restTemplate.postForObject("http://localhost:8080/api/products", product,
Product.class);
} catch (HttpClientErrorException.BadRequest e) {
model.addAttribute("error", "Product ID already exists: " + e.getMessage());
} catch (Exception e) {
e.printStackTrace();
model.addAttribute("error", "Failed to save product: " + e.getMessage());
}
return "redirect:/admin";
}

@GetMapping("/delete/{id}")
public String deleteProduct(@PathVariable Long id) {
restTemplate.delete("http://localhost:8080/api/products/" + id);
return "redirect:/admin";
}
}
64 changes: 64 additions & 0 deletions src/main/java/gift/repository/ProductRepository.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package gift.repository;

import gift.DTO.Product;
import java.sql.ResultSet;
import java.sql.SQLException;
import java.util.List;
import java.util.Optional;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.stereotype.Repository;

@Repository
public class ProductRepository {

@Autowired
private JdbcTemplate jdbcTemplate;

private static final String INSERT_PRODUCT_SQL = "INSERT INTO product (id, name, price, image_url) VALUES (?, ?, ?, ?)";
private static final String SELECT_ALL_PRODUCTS_SQL = "SELECT * FROM product";
private static final String SELECT_PRODUCT_BY_ID_SQL = "SELECT * FROM product WHERE id = ?";
private static final String UPDATE_PRODUCT_SQL = "UPDATE product SET name = ?, price = ?, image_url = ? WHERE id = ?";
private static final String DELETE_PRODUCT_SQL = "DELETE FROM product WHERE id = ?";

public List<Product> getAllProducts() {
return jdbcTemplate.query(SELECT_ALL_PRODUCTS_SQL, new ProductRowMapper());
}

public Optional<Product> getProductById(Long id) {
try {
return Optional.ofNullable(
jdbcTemplate.queryForObject(SELECT_PRODUCT_BY_ID_SQL, new ProductRowMapper(), id));
} catch (Exception e) {
return Optional.empty();
}
}

public void addProduct(Product product) {
jdbcTemplate.update(INSERT_PRODUCT_SQL, product.getId(), product.getName(),
product.getPrice(), product.getImageUrl());
}

public void updateProduct(Product product) {
jdbcTemplate.update(UPDATE_PRODUCT_SQL, product.getName(), product.getPrice(),
product.getImageUrl(), product.getId());
}

public void deleteProduct(Long id) {
jdbcTemplate.update(DELETE_PRODUCT_SQL, id);
}

private static class ProductRowMapper implements RowMapper<Product> {

@Override
public Product mapRow(ResultSet rs, int rowNum) throws SQLException {
return new Product(
rs.getLong("id"),
rs.getString("name"),
rs.getInt("price"),
rs.getString("image_url")
);
}
}
}
9 changes: 9 additions & 0 deletions src/main/resources/application.properties
Original file line number Diff line number Diff line change
@@ -1 +1,10 @@
spring.application.name=spring-gift
# h2-console ??? ??
spring.h2.console.enabled=true
# db url
spring.datasource.url=jdbc:h2:mem:test
spring.datasource.driver-class-name=org.h2.Driver
spring.datasource.username=sa
spring.datasource.password=
spring.datasource.initialization-mode=always
spring.jpa.hibernate.ddl-auto=update
6 changes: 6 additions & 0 deletions src/main/resources/schema.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
CREATE TABLE IF NOT EXISTS product (
id BIGINT PRIMARY KEY CHECK (id >= 1),
name VARCHAR(255) NOT NULL,
price INT NOT NULL,
image_url VARCHAR(255)
);
Loading

0 comments on commit d553953

Please sign in to comment.