백엔드 과제 제출로 오랜만에 처음부터 세팅해서 만져본 스프링부트 + JPA
퇴사하고 약 4달만에 코딩해보는거라 처음부터 약간 버벅거렸지만..
기록해보겠습니당.
1. - 개발 환경
- Java 21
- Spring Boot 3.4.1
- thymeleaf
- MariaDB 10.10
2. - build.gradle 의존성
<java />
implementation 'org.springframework.boot:spring-boot-starter-data-jpa' implementation 'org.springframework.boot:spring-boot-starter-web' developmentOnly 'org.springframework.boot:spring-boot-devtools' implementation 'org.springframework.boot:spring-boot-starter-jdbc' implementation 'org.projectlombok:lombok' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-security' implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6' runtimeOnly 'org.mariadb.jdbc:mariadb-java-client' implementation 'nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect' implementation 'com.googlecode.json-simple:json-simple:1.1' testImplementation 'org.springframework.security:spring-security-test' testImplementation 'org.springframework.boot:spring-boot-starter-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' implementation group: 'org.springframework', name: 'spring-jdbc', version: '6.1.12' implementation group: 'org.springframework.boot', name: 'spring-boot-starter-thymeleaf', version: '3.3.5'
3. - DB
장바구니, 장바구니 상품, 상품, 고객, 주문(결제) 테이블로 구성.

4. 상품 목록 조회
1. 프론트는 부트스트랩 쇼핑몰 템플릿을 사용했습니다.
https://themewagon.com/themes/foodmart/
FoodMart - Free Bootstrap 5 eCommerce Website Template
FoodMart is a free Bootstrap 5 eCommerce website template for grocery store websites. The theme is responsive, search engine optimized, and user-friendly.
themewagon.com
2. 상품 Entity
<java />
@Entity @Getter @Setter @ToString @NoArgsConstructor @AllArgsConstructor @Builder public class Product extends BaseEntity{ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "product_id") private Long id; //상품 ID @Column(nullable = false, length = 255) private String name; //상품 이름 @Column(length = 1000) private String description; //상품 설명 @Column(nullable = false) private Long price; //상품 가격 @Column(nullable = false) private int stock; //재고 수량 @Column(nullable = false) private String imgUrl; //이미지 경로 public static Product toProduct(ProductDto productDto) { Product product = new Product(); product.setId(productDto.getId()); product.setName(productDto.getName()); product.setDescription(productDto.getDescription()); product.setPrice(productDto.getPrice()); product.setStock(productDto.getStock()); product.setImgUrl(productDto.getImgUrl()); return product; } public static Product uptProduct(ProductDto productDto) { // 생략 } }
3. 상품 Dto
<java />
@Getter @Setter @NoArgsConstructor @ToString public class ProductDto { private Long id; private String name; private String description; private Long price; private int stock; private String imgUrl; public ProductDto(Long id, String name, String description, Long price, int stock, String imgUrl) { this.id = id; this.name = name; this.description = description; this.price = price; this.stock = stock; this.imgUrl = imgUrl; } public static ProductDto toProductDto(Product product) { ProductDto productDto = new ProductDto(); productDto.setId(product.getId()); productDto.setName(product.getName()); productDto.setDescription(product.getDescription()); productDto.setPrice(product.getPrice()); productDto.setStock(product.getStock()); productDto.setImgUrl(product.getImgUrl()); return productDto; } }
4. 전체 상품 조회
Repository
<java />
public interface ProductRepository extends JpaRepository<Product, Long>{ Product findProductById(Long id); }
Service
<java />
/* * DTO -> Entity * Entity -> DTO */ @Service @RequiredArgsConstructor public class ProductService { private final ProductRepository productRepository; public List<ProductDto> getAllProduct() { List<Product> productList = productRepository.findAll(); List<ProductDto> dtoList = new ArrayList<>(); for (Product product : productList) { dtoList.add(ProductDto.toProductDto(product)); } return dtoList; } public ProductDto findByProductId(Long id) { // 생략 } }
5. 전체 상품 조회 후 메인 화면에 전달
Controller
<java />
@Controller @RequiredArgsConstructor public class MainController { private final ProductService productService; @GetMapping("/") public String main(Model model) { List<ProductDto> products = productService.getAllProduct(); model.addAttribute("products", products); return "index"; } }
6. 메인 화면 index.html
<html />
<html lang="ko" xmlns:th="http://www.thymeleaf.org"> <!DOCTYPE html> <html lang="ko" xmlns:th="http://www.thymeleaf.org" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/layout}"> <th:block layout:fragment="script"> <script th:inline="javascript"> // 생략 </script> </th:block> <th:block layout:fragment="content"> <section class="py-5"> <div class="container-fluid"> <div class="row"> <div class="col-md-12"> <div class="bootstrap-tabs product-tabs"> <div class="tabs-header d-flex justify-content-between border-bottom my-5"> <h3>상품목록</h3> <nav> <div class="nav nav-tabs" id="nav-tab" role="tablist"> <a href="#" class="nav-link text-uppercase fs-6 active" id="nav-all-tab" data-bs-toggle="tab" data-bs-target="#nav-all">All</a> <!-- <a href="#" class="nav-link text-uppercase fs-6" id="nav-fruits-tab" data-bs-toggle="tab" data-bs-target="#nav-fruits">Fruits & Veges</a> <a href="#" class="nav-link text-uppercase fs-6" id="nav-juices-tab" data-bs-toggle="tab" data-bs-target="#nav-juices">Juices</a> --> </div> </nav> </div> <div class="tab-content" id="nav-tabContent"> <div class="tab-pane fade show active" id="nav-all" role="tabpanel" aria-labelledby="nav-all-tab"> <div class="product-grid row row-cols-1 row-cols-sm-2 row-cols-md-3 row-cols-lg-4 row-cols-xl-5"> <th:block th:each="products : ${products}"> <input type="hidden" id="productId" th:value="${products.id}"> <div class="col"> <div class="product-item"> <figure> <a href="index.html" title="Product Title"> <img th:src="${products.imgUrl}" class="tab-image"> </a> </figure> <h3>[[${products.name}]]</h3> <!-- <span class="qty" id="stock">재고 : [[${products.stock}]]개</span> --> <span class="price">[[${products.price}]]</span> <div class="d-flex align-items-center justify-content-between"> <div class="input-group product-qty"> <span class="input-group-btn"> <button type="button" class="quantity-left-minus btn btn-danger btn-number" data-type="minus"> <!-- <svg width="16" height="16"><use xlink:href="#minus"></use></svg> --> <i class="fa-solid fa-minus"></i> </button> </span> <input type="text" id="quantity" name="quantity" class="form-control input-number" value="1"> <span class="input-group-btn"> <button type="button" class="quantity-right-plus btn btn-success btn-number" data-type="plus"> <!-- <svg width="16" height="16"><use xlink:href="#plus"></use></svg> --> <i class="fa-solid fa-plus"></i> </button> </span> </div> <button type="button" class="btn btn-light border border-primary btn-lg btn-cart-font" th:attr="onclick=|addCart(${products.id})|">장바구니 담기 <i class="fa-solid fa-cart-arrow-down"></i></button> </div> </div> </div> </th:block> </div> <!-- / product-grid --> </div> </div> </div> </div> </div> </div> </section> </th:block> </html>

5. 장바구니 담기 기능
1. 장바구니, 장바구니 상품 Entity
회원 1명당 1개의 장바구니를 갖는다.
<java />
@Entity @Getter @Setter @ToString public class Cart extends BaseEntity{ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "cart_id") private Long id; private int count; // 카트 상품 개수 @OneToOne(fetch = FetchType.EAGER) @JoinColumn(name="customer_id") private Customer customer; @OneToMany(mappedBy = "cart", cascade = CascadeType.PERSIST) private List<CartProduct> cartList = new ArrayList<>(); public static Cart createCart(Customer customer) { Cart cart = new Cart(); cart.setCount(0); cart.setCustomer(customer); return cart; } }
<java />
@Entity @Getter @Setter public class CartProduct extends BaseEntity{ @Id @GeneratedValue(strategy = GenerationType.IDENTITY) @Column(name = "cart_product_id") private Long id; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name="cart_id") private Cart cart; @ManyToOne(fetch = FetchType.LAZY) @JoinColumn(name="product_id") private Product product; private int count; // 카트 상품 개수 public static CartProduct createCartProduct(Cart cart, Product product, int count) { // 생략 } public void addCount(int count) { this.count += count; } }
2. Dto
<java />
@Getter @Setter public class CartProductDto { @NotNull private Long productId; @Min(value = 1, message = "최소 1개 이상 담아주세요.") private int count; }
<java />
@Getter @Setter public class CartDetailDto { private Long cartProductId; private String name; private Long price; private int count; private String imgUrl; public CartDetailDto(Long cartProductId, String name, Long price, int count, String imgUrl) { this.cartProductId = cartProductId; this.name = name; this.price = price; this.count = count; this.imgUrl = imgUrl; } }
3. 장바구니 담기
Service
<java />
@Service @RequiredArgsConstructor @Transactional public class CartService { private final CartRepository cartRepository; private final ProductRepository productRepository; private final CartProductRepository cartProductRepository; private final CustomerRepository customerRepository; public Long addCart(CartProductDto dto, String email) { // 회원의 장바구니 조회 Product product = productRepository.findById(dto.getProductId()) .orElseThrow(EntityNotFoundException::new); Customer customer = customerRepository.findByEmail(email); Cart cart = cartRepository.findByCustomerId(customer.getId()); // 장바구니에 상품을 처음 담을 경우 장바구니 엔티티 생성 if (cart == null) { cart = Cart.createCart(customer); cartRepository.save(cart); } // 현재 상품이 이미 장바구니에 들어가 있는지 조회 CartProduct savedProduct = cartProductRepository.findByCartIdAndProductId(cart.getId(), product.getId()); // 상품이 장바구니에 없다면 카트 상품 생성 후 추가 if (savedProduct != null) { savedProduct.addCount(dto.getCount()); return savedProduct.getId(); } else { CartProduct cartProduct = CartProduct.createCartProduct(cart, product, dto.getCount()); cartProductRepository.save(cartProduct); return cartProduct.getId(); } } @Transactional(readOnly = true) public List<CartDetailDto> getCartList(@Param("email") String email) { // 생략 } @Transactional(readOnly = true) public boolean validateCart(Long cartProductId, String email) { // 생략 } public void updateCartCount(Long cartProductId, int count) { // 생략 } public void deleteCart(Long cartProductId) { // 생략 } }
Controller
<java />
@Controller @RequiredArgsConstructor public class CartController { private final CartService cartService; private final CustomerService customerService; @PostMapping(value = "/cart") public @ResponseBody ResponseEntity addCart(@RequestBody @Valid CartProductDto dto, BindingResult bindingResult, Principal principal){ if (bindingResult.hasErrors()) { StringBuilder sb = new StringBuilder(); List<FieldError> fieldErrors = bindingResult.getFieldErrors(); for (FieldError fieldError : fieldErrors) { sb.append(fieldError.getDefaultMessage()); } return new ResponseEntity<String>(sb.toString(), HttpStatus.BAD_REQUEST); } String email = principal.getName(); Long cartProductId; try { cartProductId = cartService.addCart(dto, email); } catch(Exception e){ return new ResponseEntity<String>(e.getMessage(), HttpStatus.BAD_REQUEST); } return new ResponseEntity<Long>(cartProductId, HttpStatus.OK); } @GetMapping("/cart") public String getCartList(Principal principal, Model model) { // 생략 } @DeleteMapping(value = "/cart/{cartProductId}") public @ResponseBody ResponseEntity deleteCart(@PathVariable("cartProductId") Long cartProductId, Principal principal) { // 생략 } }
4. 프론트단
<javascript />
function addCart(productId) { var url = "/cart"; var paramData = { productId :productId, count : $("#quantity").val(), }; var param = JSON.stringify(paramData); $.ajax({ url: url, type : "POST", contentType : "application/json", data: param, dataType: "json", cache: false, success: function(status) { alert("장바구니에 상품을 담았습니다."); location.href="/"; }, error: function(jqXHR, status, error) { if (jqXHR.status == '401') { alert('로그인 후 이용해주세요.'); location.href="/customer/login"; } else { } } }); }

6. 장바구니 리스트, 장바구니 삭제 기능
1. 장바구니 리스트 조회, 삭제
Repository
<java />
public interface CartProductRepository extends JpaRepository<CartProduct, Long>{ CartProduct findByCartIdAndProductId(Long cartId, Long productId); @Query(""" select cp from CartProduct cp join fetch cp.product p where cp.cart.id = :cartId order by cp.createdAt desc """) List<CartProduct> findCartDetailDtoList(@Param("cartId") Long cartId); }
<java />
public interface CartRepository extends JpaRepository<Cart, Long>{ Cart findByCustomerId(Long customerId); }
Service
<java />
@Transactional(readOnly = true) public List<CartDetailDto> getCartList(@Param("email") String email) { List<CartDetailDto> cartDetailDtoList = new ArrayList<>(); Customer customer = customerRepository.findByEmail(email); Cart cart = cartRepository.findByCustomerId(customer.getId()); if (cart == null) { return cartDetailDtoList; } List<CartProduct> cartProductList = cartProductRepository.findCartDetailDtoList(cart.getId()); cartDetailDtoList = cartProductList.stream() .map(m -> new CartDetailDto(m.getId(), m.getProduct().getName(), m.getProduct().getPrice(), m.getCount(), m.getProduct().getImgUrl())) .collect(Collectors.toList()); return cartDetailDtoList; } public void deleteCart(Long cartProductId) { CartProduct cartProduct = cartProductRepository.findById(cartProductId) .orElseThrow(EntityNotFoundException::new); cartProductRepository.delete(cartProduct); }
Controller
<java />
@GetMapping("/cart") public String getCartList(Principal principal, Model model) { // 현재 로그인한 사용자의 이메일 정보를 이용하여 장바구니에 담겨있는 상품 조회 List<CartDetailDto> cartList = cartService.getCartList(principal.getName()); Customer customer = customerService.getCustomerInfo(principal.getName()); model.addAttribute("cartList", cartList); model.addAttribute("customerName", customer.getName()); model.addAttribute("customerEmail", customer.getEmail()); return "/product/cart"; } @DeleteMapping(value = "/cart/{cartProductId}") public @ResponseBody ResponseEntity deleteCart(@PathVariable("cartProductId") Long cartProductId, Principal principal) { if (!cartService.validateCart(cartProductId, principal.getName())) { return new ResponseEntity<String>("삭제 권한이 없습니다.", HttpStatus.FORBIDDEN); } cartService.deleteCart(cartProductId); return new ResponseEntity<Long>(cartProductId, HttpStatus.OK); }
2. 프론트단 cartList.html
<html />
<!DOCTYPE html> <html lang="ko" xmlns:th="http://www.thymeleaf.org" xmlns:sec="http://www.thymeleaf.org/extras/spring-security" xmlns:layout="http://www.ultraq.net.nz/thymeleaf/layout" layout:decorate="~{layout/layout}"> <th:block layout:fragment="script"> // 생략 </th:block> <th:block layout:fragment="css"> <style> // 생략 </style> </th:block> <th:block layout:fragment="content"> <h3>장바구니 목록</h3> <div> <table class="table"> <colgroup> <col width="15%"/> <col width="70%"/> <col width="15%"/> </colgroup> <thead> <tr class="text-center"> <td><input type="checkbox" id="checkall" onclick="chkAll()"/>전체 선택</td> <td>상품명</td> <td>금액</td> </tr> </thead> <tbody> <tr th:each="cartList : ${cartList}"> <td> <input type="checkbox" name="cartChkbox" th:value="${cartList.cartProductId}" > </td> <td> <div class="proImgDiv imgItem"> <img th:src="${cartList.imgUrl}" class = "rounded proImg" th:alt="${cartList.name}"> </div> <div class="imgItem"> <span th:text="${cartList.name}" id="productName" class="fs17 font-weight-bold"></span> <div class="fs17 font-weight-light"> <span class="input-group mt-2"> <span th:id="'price_' + ${cartList.cartProductId}" th:data-price="${cartList.price}" th:text="${cartList.price} + '원'" class="align-self-center mr-2"> </span> <input type="number" name="count" th:id="'count_' + ${cartList.cartProductId}" th:value="${cartList.count}" min="1" onchange="changeCount(this)" class="form-control mr-2" > <button type="button" class="close" aria-label="Close"> <span aria-hidden="true" th:data-id="${cartList.cartProductId}" onclick="deleteCart(this)">×</span> </button> </span> </div> </div> </td> <td class="text-center align-middle"> <span th:id="'totalPrice_' + ${cartList.cartProductId}" name="totalPrice" th:text="${cartList.price * cartList.count} + '원'"> </span> </td> </tr> </tbody> </table> <h2 class="text-center"> 총 주문 금액 : <span id="orderTotal" class="text-danger">0원</span> </h2> <div class="text-center"> <button type="button" class="btn btn-primary btn-lg" onclick="requestPayment()">토스로 결제하기</button> </div> </div> </th:block> </html>
deleteCart
<html />
<script th:inline="javascript"> $(document).ready(function() { // 생략 }); function deleteCart(obj){ var cartProductId = obj.dataset.id; var url = "/cart/" + cartProductId; $.ajax({ url : url, type : "DELETE", dataType : "json", cache : false, success : function(result, status){ location.href='/cart'; }, error : function(jqXHR, status, error){ if(jqXHR.status == '401'){ alert('로그인 후 이용해주세요'); location.href='/customer/login'; } else{ alert('시스템 오류입니다.'); } } }); } </script>

'💻 my code archive > 🏷️JAVA & Spring(Boot)' 카테고리의 다른 글
Spring Boot 스프링부트 application.yml 설정값 가져오기 (0) | 2025.02.03 |
---|---|
스프링부트 SpringBoot 쇼핑몰 토스 Toss Payment API 연동 결제 구현하기 (1) | 2025.01.13 |
프로젝트 #1 스프링부트 SpringBoot 3x Swagger 적용법 (1) | 2024.04.20 |
SheetJS 테이블 내용 엑셀 다운로드, 스타일 적용 방법 (0) | 2023.08.18 |
[스프링부트 블로그 만들기] 댓글 기능, 댓글 목록, 삭제까지 구현하기 (0) | 2022.03.25 |