[SB] 11. 스프링부트 상점 v2

최재원's avatar
Mar 31, 2025
[SB] 11. 스프링부트 상점 v2

1. 상점 그림 그리기

그림 그리는 방법
  1. home.html 파일 생성
  1. Bootstrap link 추가
    1. link
      <!-- Latest compiled and minified CSS --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet"> <!-- Latest compiled JavaScript --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script>
  1. home 화면을 먼저 만들어보기
    1. 인텔리J를 사용 중이라면
      1. 화면에 크롬아이콘을 클릭해 html 화면을 확인 하면서 만들기
    2. cursor을 사용 중이라면
      1. live server을 켜서 html 화면을 확인 하면서 만들기
  1. home 화면을 복사 붙이기 하면서 다른 화면도 만들기
  1. 화면을 다 만들었다면, templates/layout 폴더를 생성
    1. nav 부분을 header.html 파일로 옮기기
    2. footer 부분을 footer.html 파일로 옮기기
  1. 모든 html 파일을 mustache 파일로 변경

1. 인증 관련

1-1. home

notion image
  • 프로그램의 메인 화면
  • 로그인 유무에 따라 다음 화면을 만들어야함
로그인 상태라면
  • 회원가입 ❌
  • 로그인 ❌
  • 로그아웃 ✅
  • 가운데 이름 표시 ✅
로그아웃 상태라면
  • 회원가입 ✅
  • 로그인 ✅
  • 로그아웃 ❌
  • 가운데 이름 표시 ❌

1-2. user/join-form

notion image
회원가입 화면
  • form 태그로 만들기
  • form 태그에 action 에 주소 잘 확인하기
  • input 태그에 required 를 추가해 무조건 input 값을 받도록 만들기
  • input태그에 name 속성 유무 확인하기
  • post 요청 주소에는 맨 끝에 save를 붙여서 사용하기

1-3. user/login-form

notion image
로그인 화면
  • 데이터를 저장하지 않는 유일한 post 요청
  • form 태그로 만들기
  • form 태그에 action 에 주소 잘 확인하기
  • input 태그에 required 를 추가해 무조건 input 값을 받도록 만들기
  • input 태그에 name 속성 유무 확인하기

2. 상점 관련

2-1. store/list

notion image
상품목록 화면
  • table 로 만들기
  • a 태그를 사용해 상세보기 링크를 만들기

2-2. store/detail

notion image
상품상세 화면
  • a태그를 사용해 수정, 삭제 버튼을 만들기
아래 구매화면은 form 태그로 만들기 form 태그에 action 주소 잘 확인하기 input 태그에 required 를 추가해 무조건 input 값을 받도록 만들기 input 태그에 name 속성 유무 확인하기

2-3. store/update-form

notion image
상품수정 화면
  • form 태그로 만들기
  • form 태그에 action 에 주소 잘 확인하기
  • input 태그에 required 를 추가해 무조건 input 값을 받도록 만들기
  • input 태그에 name 속성 유무 확인하기
  • post 요청 주소에는 맨 끝에 update를 붙여서 사용하기

2-3. store/save-form

notion image
상품등록 화면
  • form 태그로 만들기
  • form 태그에 action 에 주소 잘 확인하기
  • input 태그에 required 를 추가해 무조건 input 값을 받도록 만들기
  • input 태그에 name 속성 유무 확인하기
  • post 요청 주소에는 맨 끝에 save를 붙여서 사용하기

3. 구매 관련

3-1. log/list

notion image
구매목록 화면
  • table 태그로 만들기

4. 모든 html → mustache로 변경

home
{{>layout/header}} <div class="container mt-5 "> <div class="p-5 bg-dark text-white rounded"> <h1>메타 상점에 오신것을 환영합니다</h1> <p>안녕하세요 쌀님</p> </div> </div> {{>layout/footer}}
layout/header
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <!-- Latest compiled and minified CSS --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" /> <!-- Latest compiled JavaScript --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> <title>Document</title> </head> <body> <nav class="navbar navbar-expand-sm bg-dark navbar-dark"> <div class="container-fluid"> <ul class="navbar-nav"> <li class="nav-item"> <a class="nav-link active" href="#">Home</a> </li> <li class="nav-item"> <a class="nav-link" href="#">상품목록</a> </li> <li class="nav-item"> <a class="nav-link" href="#">상품등록</a> </li> <li class="nav-item"> <a class="nav-link" href="#">구매목록</a> </li> <li class="nav-item"> <a class="nav-link" href="#">회원가입</a> </li> <li class="nav-item"> <a class="nav-link" href="#">로그인</a> </li> <li class="nav-item"> <a class="nav-link" href="#">로그아웃</a> </li> </ul> </div> </nav> <!-- 네브바 종료 -->
layout/footer
<!-- 푸터 시작 --> <footer class="mt-5 p-4 bg-dark text-white text-center d-flex justify-content-around align-items-center"> <div> <p>Created by Cos</p> <p>🚩 겟인데어</p> </div> <div> <p>🏴 부산 수영구 XX동</p> <p>📞 010-2222-7777</p> </div> </footer> </body> </html>
user/join-form
{{>layout/header}} <div class="container bg-light text-white mt-5 p-5 rounded"> <h1 class="text-dark">회원가입</h1> <form action="#"> <div class="mb-3 mt-3"> <input type="text" class="form-control" placeholder="Username 입력" name="username" required /> </div> <div class="mb-3"> <input type="text" class="form-control" placeholder="Fullname 입력" name="fullname" required /> </div> <div class="mb-3"> <input type="password" class="form-control" placeholder="Password 입력" name="password" required /> </div> <button class="btn btn-primary">회원가입</button> </form> </div> {{>layout/footer}}
user/login-form
{{>layout/header}} <div class="container bg-light text-white mt-5 p-5 rounded"> <h1 class="text-dark">로그인</h1> <form action="#"> <div class="mb-3 mt-3"> <input type="text" class="form-control" placeholder="Username 입력" name="username" required /> </div> <div class="mb-3"> <input type="password" class="form-control" placeholder="Password 입력" name="password" required /> </div> <button class="btn btn-primary">로그인</button> </form> </div> {{>layout/footer}}
store/list
{{>layout/header}} <div class="container text-white mt-5 p-5 rounded"> <h1 class="text-dark">상품목록</h1> <table class="table table-bordered table-hover"> <thead> <tr> <th>번호</th> <th>상품명</th> <th></th> </tr> </thead> <tbody> <tr> <td>1</td> <td>바나나</td> <td><a href="#">상세보기</a></td> </tr> <tr> <td>2</td> <td>딸기</td> <td><a href="#">상세보기</a></td> </tr> </tbody> </table> </div> {{>layout/footer}}
store/detail
{{>layout/header}} <div class="container bg-light text-dark mt-5 p-5 rounded"> <h1 class="text-dark">상품상세</h1> <div class="mb-5"> <div> 번호 : 1 <br /> 상품명 : 바나나 <br /> 상품가격 : 3000원 <br /> 상품재고 : 100개 <br /> </div> <br /> <a href="#" class="btn btn-primary">수정</a> <a href="#" class="btn btn-danger">삭제</a> </div> <form action="#"> <div class="row"> <div class="col"> <input type="hidden" value="1" /> <input type="number" class="form-control" placeholder="수량 입력" name="qty" /> </div> <button class="col btn btn-dark">구매</button> </div> </form> </div> {{>layout/footer}}
store/update-form
{{>layout/header}} <div class="container bg-light text-dark mt-5 p-5 rounded"> <h1 class="text-dark">상품수정</h1> <form action="#"> <div class="mb-3 mt-3"> <input type="text" class="form-control" placeholder="상품명" name="name" value="바나나" required /> </div> <div class="mb-3"> <input type="number" class="form-control" placeholder="재고" name="stock" value="100" required /> </div> <div class="mb-3"> <input type="number" class="form-control" placeholder="가격" name="price" value="3000" required /> </div> <button class="btn btn-primary">수정</button> </form> </div> {{>layout/footer}}
store/save-form
{{>layout/header}} <div class="container bg-light text-dark mt-5 p-5 rounded"> <h1 class="text-dark">상품등록</h1> <form action="#"> <div class="mb-3 mt-3"> <input type="text" class="form-control" placeholder="상품명" name="name" required /> </div> <div class="mb-3"> <input type="number" class="form-control" placeholder="재고" name="stock" required /> </div> <div class="mb-3"> <input type="number" class="form-control" placeholder="가격" name="price" required /> </div> <button class="btn btn-primary">등록</button> </form> </div> {{>layout/footer}}
log/list
{{>layout/header}} <div class="container text-white mt-5 p-5 rounded"> <h1 class="text-dark">구매목록</h1> <table class="table table-bordered table-hover"> <thead> <tr> <th>주문번호</th> <th>상품명</th> <th>구매 수량</th> <th>총 가격</th> </tr> </thead> <tbody> <tr> <td>1</td> <td>바나나</td> <td>5개</td> <td>15000원</td> </tr> <tr> <td>2</td> <td>바나나</td> <td>5개</td> <td>15000원</td> </tr> </tbody> </table> </div> {{>layout/footer}}

2. Component & 테이블 만들기

notion image

1. Component

User
package com.metacoding.storev2.user; import java.sql.Timestamp; import jakarta.persistence.Column; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; import lombok.Getter; @Entity @Table(name = "user_tb") @Getter public class User { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; @Column(unique = true, nullable = false, length = 12) private String username; // 계정 아이디 private String fullname; // 계정 닉네임 @Column(nullable = false, length = 12) private String password; // 계정 비밀번호 private Timestamp createdAt; }
Controller
Service
Repository
Request
Response

Store
package com.metacoding.storev2.store; import java.sql.Timestamp; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; import lombok.Getter; @Entity @Table(name = "store_tb") @Getter public class Store { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; private String name; private int stock; // 상품 재고 private int price; // 상품 가격 private Timestamp createdAt; }
Controller
Service
Repository
Request
Response

Log
package com.metacoding.storev2.log; import java.sql.Timestamp; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; import lombok.Getter; @Entity @Table(name = "log_tb") @Getter public class Log { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private int id; private int userId; private int storeId; private int qty; // 구매 수량 private int totalPrice; // 총 가격 private Timestamp createdAt; }
Controller
Service
Repository
Request
Response

2. 테이블

application.properties
# vscode console highlight spring.output.ansi.enabled=always # utf-8 server.servlet.encoding.charset=utf-8 server.servlet.encoding.force=true # DB spring.datasource.driver-class-name=org.h2.Driver spring.datasource.url=jdbc:h2:mem:test spring.datasource.username=sa spring.datasource.password= spring.h2.console.enabled=true # JPA table create or none spring.jpa.hibernate.ddl-auto=create # query log spring.jpa.show-sql=true # dummy data spring.sql.init.data-locations=classpath:db/data.sql # create dummy data after ddl-auto create spring.jpa.defer-datasource-initialization=true # mustache request expose spring.mustache.servlet.expose-request-attributes=true # mustache session expose !!!! 머스테치에서 세션 키값으로 바로 접근하는 방법 spring.mustache.servlet.expose-session-attributes=true # sql formatter spring.jpa.properties.hibernate.format_sql=true
resources/db/data.sql
INSERT INTO user_tb (username, fullname, password, created_at) VALUES ('ssar', '쌀', '1234', now()); INSERT INTO user_tb (username, fullname, password, created_at) VALUES ('cos', '코스', '1234', now()); INSERT INTO store_tb (name, stock, price, created_at) VALUES ('바나나', 100, 3000, now()); INSERT INTO store_tb (name, stock, price, created_at) VALUES ('딸기', 50, 2000, now()); INSERT INTO log_tb (user_id, store_id, qty, total_price, created_at) VALUES (1, 1, 5, 15000, now()); INSERT INTO log_tb (user_id, store_id, qty, total_price, created_at) VALUES (1, 2, 5, 10000, now()); INSERT INTO log_tb (user_id, store_id, qty, total_price, created_at) VALUES (2, 1, 10, 30000, now()); INSERT INTO log_tb (user_id, store_id, qty, total_price, created_at) VALUES (2, 2, 10, 20000, now());

3. User 기능 만들기

1. 회원가입

notion image
Username이 중복되면 Exception을 만들어 터트리기 @Transactional을 사용해야함 insert 이기 때문
회원가입이 성공하면 user/login-form으로 리다이렉트 회원가입이 실패하면 user/join-form으로 리다이렉트
joinDTO 만들어 데이터 받기

🧔user/join-form

{{>layout/header}} <div class="container bg-light text-white mt-5 p-5 rounded"> <h1 class="text-dark">회원가입</h1> <form action="/join" method="post"> <div class="mb-3 mt-3"> <input type="text" class="form-control" placeholder="Username 입력" name="username" value="love" required /> </div> <div class="mb-3"> <input type="text" class="form-control" placeholder="Fullname 입력" name="fullname" value="러브" required /> </div> <div class="mb-3"> <input type="password" class="form-control" placeholder="Password 입력" name="password" value="1234" required /> </div> <button class="btn btn-primary">회원가입</button> </form> </div> {{>layout/footer}}

🧔layout/header

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <!-- Latest compiled and minified CSS --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" /> <!-- Latest compiled JavaScript --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> <title>Document</title> </head> <body> <nav class="navbar navbar-expand-sm bg-dark navbar-dark"> <div class="container-fluid"> <ul class="navbar-nav"> <li class="nav-item"> <a class="nav-link active" href="#">Home</a> </li> <li class="nav-item"> <a class="nav-link" href="#">상품목록</a> </li> <li class="nav-item"> <a class="nav-link" href="#">상품등록</a> </li> <li class="nav-item"> <a class="nav-link" href="#">구매목록</a> </li> <li class="nav-item"> <a class="nav-link" href="/join-form">회원가입</a> </li> <li class="nav-item"> <a class="nav-link" href="#">로그인</a> </li> <li class="nav-item"> <a class="nav-link" href="#">로그아웃</a> </li> </ul> </div> </nav> <!-- 네브바 종료 -->

☕UserController

@GetMapping("/join-form") public String joinForm() { return "/user/join-form"; } @PostMapping("/join") public String join(UserRequest.JoinDTO joinDTO) { userService.회원가입(joinDTO); return "redirect:/login-form"; }

☕UserRequest

@Data public static class JoinDTO { private String username; private String fullname; private String password; }

☕UserService

@Transactional public void 회원가입(UserRequest.JoinDTO joinDTO) { // 1. 동일 username 있는지 확인 User user = userRepository.findByUsername(joinDTO.getUsername()); if (user != null) { // 2. 있으면, exception !오류는 exception으로 처리한다 throw new RuntimeException("동일한 username이 있습니다"); } // 3. 없으면 회원가입 성공 userRepository.save(joinDTO.getUsername(), joinDTO.getFullname(), joinDTO.getPassword()); }

☕UserRepository

public void save(String username, String fullname, String password) { Query query = em.createNativeQuery( "INSERT INTO user_tb (username, fullname, password, created_at) VALUES (?, ?, ?, now())"); query.setParameter(1, username); query.setParameter(2, fullname); query.setParameter(3, password); query.executeUpdate(); } public User findByUsername(String username) { Query query = em.createNativeQuery( "select * from user_tb where username = ?"); query.setParameter(1, username); try { return (User) query.getSingleResult(); } catch (Exception e) { return null; } }

2. 로그인 & 로그아웃

notion image
notion image
로그인 성공하면 session에 유저정보 저장
로그인이 성공하면 home으로 리다이렉트 로그인이 실패하면 user/login-form으로 리다이렉트
loginDTO 만들어 데이터 받기

🧔user/login-form

{{>layout/header}} <div class="container bg-light text-white mt-5 p-5 rounded"> <h1 class="text-dark">로그인</h1> <form action="/login" method="post"> <div class="mb-3 mt-3"> <input type="text" class="form-control" placeholder="Username 입력" name="username" value="ssar" required /> </div> <div class="mb-3"> <input type="password" class="form-control" placeholder="Password 입력" name="password" value="1234" required /> </div> <button class="btn btn-primary">로그인</button> </form> </div> {{>layout/footer}}

🧔layout/header

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <!-- Latest compiled and minified CSS --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" /> <!-- Latest compiled JavaScript --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> <title>Document</title> </head> <body> <nav class="navbar navbar-expand-sm bg-dark navbar-dark"> <div class="container-fluid"> <ul class="navbar-nav"> <li class="nav-item"> <a class="nav-link active" href="#">Home</a> </li> <li class="nav-item"> <a class="nav-link" href="#">상품목록</a> </li> <li class="nav-item"> <a class="nav-link" href="#">상품등록</a> </li> {{#sessionUser}} <li class="nav-item"> <a class="nav-link" href="#">구매목록</a> </li> <li class="nav-item"> <a class="nav-link" href="/logout">로그아웃</a> </li> {{/sessionUser}} {{^sessionUser}} <li class="nav-item"> <a class="nav-link" href="/join-form">회원가입</a> </li> <li class="nav-item"> <a class="nav-link" href="/login-form">로그인</a> </li> {{/sessionUser}} </ul> </div> </nav> <!-- 네브바 종료 -->

🧔home

{{>layout/header}} <div class="container mt-5 "> <div class="p-5 bg-dark text-white rounded"> <h1>메타 상점에 오신것을 환영합니다</h1> {{#sessionUser}} <p>안녕하세요 {{sessionUser.fullname}}님</p> {{/sessionUser}} {{^sessionUser}} <p>안녕하세요</p> {{/sessionUser}} </div> </div> {{>layout/footer}}

☕UserController

@GetMapping("/login-form") public String loginForm() { return "/user/login-form"; } @PostMapping("/login") public String login(UserRequest.LoginDTO loginDTO) { User sessionUser = userService.로그인(loginDTO); session.setAttribute("sessionUser", sessionUser); // stateful 서버에 상태를 저장, 머스테치에서도 sessionUser 키값으로 데이터 접근 가능 return "redirect:/"; } @GetMapping("/") public String home() { return "/home"; } @GetMapping("/logout") public String logout() { session.invalidate(); // session에 있는 정보만 제거 return "redirect:/"; }

☕UserRequest

@Data public static class LoginDTO { private String username; private String password; }

☕UserService

public User 로그인(UserRequest.loginDTO loginDTO) { // 1. username이 있는지 확인 User user = userRepository.findByUsername(loginDTO.getUsername()); if (user == null) { // 2. 없으면, exception! 오류는 exception으로 처리한다 throw new RuntimeException("해당 아이디가 없습니다"); } // 3. password가 동일한지 확인 if (!(loginDTO.getPassword().equals(user.getPassword()))) { throw new RuntimeException("비밀번호가 틀렸습니다"); } return user; }

github에 로그인&로그아웃 까지 했다면 DTO의 이름을 첫글자를 대문자로 바꿔 줘야한다 실수했다


4. Store 기능 만들기

1. 상품목록

notion image
  • store_tb의 모든 데이터를 가져와 뿌리기
  • storeListDTO 만들어 데이터 보내기

🧔store/list

{{>layout/header}} <div class="container text-white mt-5 p-5 rounded"> <h1 class="text-dark">상품목록</h1> <table class="table table-bordered table-hover"> <thead> <tr> <th>번호</th> <th>상품명</th> <th></th> </tr> </thead> <tbody> {{#models}} <tr> <td>{{id}}</td> <td>{{name}}</td> <td><a href="/store/{{id}}">상세보기</a></td> </tr> {{/models}} </tbody> </table> </div> {{>layout/footer}}

🧔layout/header

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <!-- Latest compiled and minified CSS --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" /> <!-- Latest compiled JavaScript --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> <title>Document</title> </head> <body> <nav class="navbar navbar-expand-sm bg-dark navbar-dark"> <div class="container-fluid"> <ul class="navbar-nav"> <li class="nav-item"> <a class="nav-link active" href="#">Home</a> </li> <li class="nav-item"> <a class="nav-link" href="/store">상품목록</a> </li> <li class="nav-item"> <a class="nav-link" href="#">상품등록</a> </li> {{#sessionUser}} <li class="nav-item"> <a class="nav-link" href="#">구매목록</a> </li> <li class="nav-item"> <a class="nav-link" href="/logout">로그아웃</a> </li> {{/sessionUser}} {{^sessionUser}} <li class="nav-item"> <a class="nav-link" href="/join-form">회원가입</a> </li> <li class="nav-item"> <a class="nav-link" href="/login-form">로그인</a> </li> {{/sessionUser}} </ul> </div> </nav> <!-- 네브바 종료 -->

☕StoreController

@GetMapping("/store") public String list(HttpServletRequest request) { List<StoreResponse.StoreListItemDTO> storeListItemList = storeService.상품목록(); request.setAttribute("models", storeListItemList); return "/store/list"; }

☕StoreResponse

@Data @AllArgsConstructor public static class StoreListItemDTO { private int id; private String name; }

☕StoreService

public List<StoreResponse.StoreListItemDTO> 상품목록() { // 1. 테이블에서 store list를 가져온다 List<Store> storeList = storeRepository.findAll(); // 2. Store -> StoreDTO 변환한다 List<StoreResponse.StoreListItemDTO> storeDTOList = new ArrayList<>(); for (Store store : storeList) { storeDTOList.add(new StoreResponse.StoreListItemDTO(store.getId(), store.getName())); } return storeDTOList; }

☕StoreRepository

public List<Store> findAll() { Query query = em.createNativeQuery("select * from store_tb order by id desc", Store.class); return query.getResultList(); }

2. 상품상세

notion image
  • store_tb에 있는 1개의 데이터를 가져와 뿌리기
  • 상품이 있으면 보여주기
  • store 객체를 그대로 보내기

🧔store/detail

{{>layout/header}} <div class="container bg-light text-dark mt-5 p-5 rounded"> <h1 class="text-dark">상품상세</h1> <div class="mb-5"> <div> 번호 : {{model.id}} <br /> 상품명 : {{model.name}} <br /> 상품가격 : {{model.price}}원 <br /> 상품재고 : {{model.stock}}개 <br /> </div> <br /> <a href="#" class="btn btn-primary">수정</a> <a href="#" class="btn btn-danger">삭제</a> </div> <form action="#"> <div class="row"> <div class="col"> <input type="hidden" value="{{model.id}}" /> <input type="number" class="form-control" placeholder="수량 입력" name="qty" /> </div> <button class="col btn btn-dark">구매</button> </div> </form> </div> {{>layout/footer}}

☕StoreController

@GetMapping("/store/{id}") public String detail( HttpServletRequest request, @PathVariable("id") int id) { StoreResponse.StoreDetailDTO storeDetailDTO = storeService.상품상세(id); request.setAttribute("model", storeDetailDTO); return "/store/detail"; }

☕StoreResponse

@Data @AllArgsConstructor public static class StoreDetailDTO { private int id; private String name; private int stock; private int price; }

☕StoreService

public StoreResponse.StoreDetailDTO 상품상세(int id) { // 1. 상품 확인 Store store = storeRepository.findById(id); // 2. 상품이 없으면 예외! if (store == null) { throw new RuntimeException("해당 상품이 없습니다"); } return new StoreResponse.StoreDetailDTO(store.getId(), store.getName(), store.getStock(), store.getPrice()); }

☕StoreRepository

public Store findById(int id) { Query query = em.createNativeQuery("select * from store_tb where id = ?", Store.class); query.setParameter(1, id); try { return (Store) query.getSingleResult(); } catch (Exception e) { return null; } }

github에 상품목록 까지 했다면 DTO의 이름을 위와 같이 변경해야 한다


3. 상품삭제

  • 상품이 있으면 삭제
  • @Transactional을 사용해야함 delete 이기 때문

🧔store/detail

{{>layout/header}} <div class="container bg-light text-dark mt-5 p-5 rounded"> <h1 class="text-dark">상품상세</h1> <div class="mb-5"> <div> 번호 : {{model.id}} <br /> 상품명 : {{model.name}} <br /> 상품가격 : {{model.price}}원 <br /> 상품재고 : {{model.stock}}개 <br /> </div> <br /> <a href="#" class="btn btn-primary">수정</a> <a href="/store/{{model.id}}/delete" class="btn btn-danger">삭제</a> </div> <form action="#"> <div class="row"> <div class="col"> <input type="hidden" value="{{model.id}}" /> <input type="number" class="form-control" placeholder="수량 입력" name="qty" /> </div> <button class="col btn btn-dark">구매</button> </div> </form> </div> {{>layout/footer}}

☕StoreController

@GetMapping("/store/{id}/delete") public String delete(@PathVariable("id") int id) { storeService.상품삭제(id); return "redirect:/store"; }

☕StoreService

@Transactional public void 상품삭제(int id) { // 1. 상품 확인 Store store = storeRepository.findById(id); // 2. 상품이 없으면 예외! if (store == null) { throw new RuntimeException("해당 상품이 없습니다"); } storeRepository.deleteById(id); }

☕StoreRepository

public void deleteById(int id) { Query query = em.createNativeQuery("delete from store_tb where id = ?"); query.setParameter(1, id); query.executeUpdate(); }

4. 상품수정

notion image
  • store_tb에 있는 1개의 데이터를 가져와 뿌린다
  • store 객체를 그대로 보내기
  • storeDTO 만들어 데이터 받기
  • 상품이 있으면 수정
  • @Transactional을 사용해야함 update 이기 때문

🧔store/update-form

{{>layout/header}} <div class="container bg-light text-dark mt-5 p-5 rounded"> <h1 class="text-dark">상품수정</h1> <form action="/store/{{model.id}}/update" method="post"> <div class="mb-3 mt-3"> <input type="text" class="form-control" placeholder="상품명" name="name" value="{{model.name}}" required /> </div> <div class="mb-3"> <input type="number" class="form-control" placeholder="재고" name="stock" value="{{model.stock}}" required /> </div> <div class="mb-3"> <input type="number" class="form-control" placeholder="가격" name="price" value="{{model.price}}" required /> </div> <button class="btn btn-primary">수정</button> </form> </div> {{>layout/footer}}

🧔store/detail

{{>layout/header}} <div class="container bg-light text-dark mt-5 p-5 rounded"> <h1 class="text-dark">상품상세</h1> <div class="mb-5"> <div> 번호 : {{model.id}} <br /> 상품명 : {{model.name}} <br /> 상품가격 : {{model.price}}원 <br /> 상품재고 : {{model.stock}}개 <br /> </div> <br /> <a href="/store/{{model.id}}/update-form" class="btn btn-primary">수정</a> <a href="/store/{{model.id}}/delete" class="btn btn-danger">삭제</a> </div> <form action="#"> <div class="row"> <div class="col"> <input type="hidden" value="{{model.id}}" /> <input type="number" class="form-control" placeholder="수량 입력" name="qty" /> </div> <button class="col btn btn-dark">구매</button> </div> </form> </div> {{>layout/footer}}

☕StoreController

@GetMapping("/store/{id}/update-form") public String updateForm( HttpServletRequest request, @PathVariable("id") int id) { StoreResponse.StoreDetailDTO storeDetailDTO = storeService.상품상세(id); request.setAttribute("model", storeDetailDTO); return "/store/update-form"; } @PostMapping("/store/{id}/update") public String update( @PathVariable("id") int id, StoreRequest.StoreDTO storeDTO) { storeService.상품수정(id, storeDTO); return "redirect:/store/" + id; }

☕StoreRequest

@Data public static class StoreDTO { private String name; private int stock; private int price; }

☕StoreService

@Transactional public void 상품수정(int id, StoreRequest.StoreDTO storeDTO) { // 1. 상품 확인 Store store = storeRepository.findById(id); // 2. 상품이 없으면 예외! if (store == null) { throw new RuntimeException("해당 상품이 없습니다"); } storeRepository.updateById(id, storeDTO.getName(), storeDTO.getStock(), storeDTO.getPrice()); }

☕StoreRepository

public void updateById(int id, String name, int stock, int price) { Query query = em.createNativeQuery("update store_tb set name = ?, stock = ?, price = ? where id = ?"); query.setParameter(1, name); query.setParameter(2, stock); query.setParameter(3, price); query.setParameter(4, id); query.executeUpdate(); }

5. 상품등록

notion image
  • storeDTO 만들어 데이터 받기
  • @Transactional을 사용해야함 insert 이기 때문

🧔store/save-form

{{>layout/header}} <div class="container bg-light text-dark mt-5 p-5 rounded"> <h1 class="text-dark">상품등록</h1> <form action="/store/save" method="post"> <div class="mb-3 mt-3"> <input type="text" class="form-control" placeholder="상품명" name="name" value="오렌지" required /> </div> <div class="mb-3"> <input type="number" class="form-control" placeholder="재고" name="stock" value="40" required /> </div> <div class="mb-3"> <input type="number" class="form-control" placeholder="가격" name="price" value="4000" required /> </div> <button class="btn btn-primary">등록</button> </form> </div> {{>layout/footer}}

🧔layout/header

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <!-- Latest compiled and minified CSS --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" /> <!-- Latest compiled JavaScript --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> <title>Document</title> </head> <body> <nav class="navbar navbar-expand-sm bg-dark navbar-dark"> <div class="container-fluid"> <ul class="navbar-nav"> <li class="nav-item"> <a class="nav-link active" href="/">Home</a> </li> <li class="nav-item"> <a class="nav-link" href="/store">상품목록</a> </li> <li class="nav-item"> <a class="nav-link" href="/store/save-form">상품등록</a> </li> {{#sessionUser}} <li class="nav-item"> <a class="nav-link" href="#">구매목록</a> </li> <li class="nav-item"> <a class="nav-link" href="/logout">로그아웃</a> </li> {{/sessionUser}} {{^sessionUser}} <li class="nav-item"> <a class="nav-link" href="/join-form">회원가입</a> </li> <li class="nav-item"> <a class="nav-link" href="/login-form">로그인</a> </li> {{/sessionUser}} </ul> </div> </nav> <!-- 네브바 종료 -->

☕StoreController

@GetMapping("/store/save-form") public String saveForm() { return "/store/save-form"; } @PostMapping("/store/save") public String save(StoreRequest.StoreDTO storeDTO) { storeService.상품등록(storeDTO); return "redirect:/store"; }

☕StoreService

@Transactional public void 상품등록(StoreRequest.StoreDTO storeDTO) { storeRepository.save(storeDTO.getName(), storeDTO.getStock(), storeDTO.getPrice()); }

☕StoreRepository

public void save(String name, int stock, int price) { Query query = em .createNativeQuery("insert into store_tb (name, price, stock, created_at) values (?, ?, ?, now())"); query.setParameter(1, name); query.setParameter(2, stock); query.setParameter(3, price); query.executeUpdate(); }

5. Log 기능 만들기

1. 구매목록

notion image
  • log_tb, store_tb, user_tb 의 테이블 데이터를 join해서 가져온다 로그인 한 유저에 대한 구매목록만 가져온다
  • 이 데이터에 대한 Response DTO를 만들어 보낸다
  • 로그인한 유저만 이 화면을 볼 수 있다

🧔log/list

{{>layout/header}} <div class="container text-white mt-5 p-5 rounded"> <h1 class="text-dark">구매목록</h1> <table class="table table-bordered table-hover"> <thead> <tr> <th>주문번호</th> <th>구매자</th> <th>상품명</th> <th>구매 수량</th> <th>총 가격</th> </tr> </thead> <tbody> {{#models}} <tr> <td>{{id}}</td> <td>{{buyer}}</td> <td>{{name}}</td> <td>{{qty}}개</td> <td>{{totalPrice}}원</td> </tr> {{/models}} </tbody> </table> </div> {{>layout/footer}}

🧔layout/header

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <!-- Latest compiled and minified CSS --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" /> <!-- Latest compiled JavaScript --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> <title>Document</title> </head> <body> <nav class="navbar navbar-expand-sm bg-dark navbar-dark"> <div class="container-fluid"> <ul class="navbar-nav"> <li class="nav-item"> <a class="nav-link active" href="/">Home</a> </li> <li class="nav-item"> <a class="nav-link" href="/store">상품목록</a> </li> <li class="nav-item"> <a class="nav-link" href="/store/save-form">상품등록</a> </li> {{#sessionUser}} <li class="nav-item"> <a class="nav-link" href="/log">구매목록</a> </li> <li class="nav-item"> <a class="nav-link" href="/logout">로그아웃</a> </li> {{/sessionUser}} {{^sessionUser}} <li class="nav-item"> <a class="nav-link" href="/join-form">회원가입</a> </li> <li class="nav-item"> <a class="nav-link" href="/login-form">로그인</a> </li> {{/sessionUser}} </ul> </div> </nav> <!-- 네브바 종료 -->

☕LogController

@GetMapping("/log") public String list(HttpServletRequest request) { User user = (User) session.getAttribute("sessionUser"); // 로그인을 하지 않으면 홈으로 리다이렉트 if (user == null) return "redirect:/"; List<LogResponse.LogListItemDTO> logListItemDTOList = logService.구매목록(user); request.setAttribute("models", logListItemDTOList); return "/log/list"; }

☕LogResponse

@Data @AllArgsConstructor public static class LogListItemDTO { private int id; private String buyer; // 구매자 이름 private String name; // 구매되는 상품 이름 private int qty; // 구매 수량 private int totalPrice; // 총 가격 }

☕LogService

public List<LogListItemDTO> 구매목록(User user) { return logRepository.findAllByUserIdJoinStoreJoinUser(user.getId()); }

☕LogRepository

public List<LogResponse.LogListItemDTO> findAllByUserIdJoinStoreJoinUser(int userId) { Query query = em.createNativeQuery( """ select lt.id, ut.fullname, st.name, lt.qty, lt.total_price from LOG_TB lt inner join store_tb st on lt.store_id = st.id inner join user_tb ut on lt.user_id = ut.id where lt.user_id = ?; """); query.setParameter(1, userId); List<Object[]> objectsList = query.getResultList(); List<LogResponse.LogListItemDTO> logListItemDTOList = new ArrayList<>(); for (Object[] objects : objectsList) { logListItemDTOList.add( new LogResponse.LogListItemDTO( (int) objects[0], (String) objects[1], (String) objects[2], (int) objects[3], (int) objects[4])); } return logListItemDTOList; }

2. 구매하기

notion image
  • 로그인 한 유저만 구매 할 수 있다
  • 이 데이터에 대한 Request DTO를 만들어야 한다
  • 상품이 있으면 구매
  • @Transactional을 사용해야함 insert 이기 때문

🧔log/list

{{>layout/header}} <div class="container text-white mt-5 p-5 rounded"> <h1 class="text-dark">구매목록</h1> <table class="table table-bordered table-hover"> <thead> <tr> <th>주문번호</th> <th>구매자</th> <th>상품명</th> <th>구매 수량</th> <th>총 가격</th> </tr> </thead> <tbody> {{#models}} <tr> <td>{{id}}</td> <td>{{buyer}}</td> <td>{{name}}</td> <td>{{qty}}개</td> <td>{{totalPrice}}원</td> </tr> {{/models}} </tbody> </table> </div> {{>layout/footer}}

🧔layout/header

<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <!-- Latest compiled and minified CSS --> <link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/css/bootstrap.min.css" rel="stylesheet" /> <!-- Latest compiled JavaScript --> <script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.3/dist/js/bootstrap.bundle.min.js"></script> <title>Document</title> </head> <body> <nav class="navbar navbar-expand-sm bg-dark navbar-dark"> <div class="container-fluid"> <ul class="navbar-nav"> <li class="nav-item"> <a class="nav-link active" href="/">Home</a> </li> <li class="nav-item"> <a class="nav-link" href="/store">상품목록</a> </li> <li class="nav-item"> <a class="nav-link" href="/store/save-form">상품등록</a> </li> {{#sessionUser}} <li class="nav-item"> <a class="nav-link" href="/log">구매목록</a> </li> <li class="nav-item"> <a class="nav-link" href="/logout">로그아웃</a> </li> {{/sessionUser}} {{^sessionUser}} <li class="nav-item"> <a class="nav-link" href="/join-form">회원가입</a> </li> <li class="nav-item"> <a class="nav-link" href="/login-form">로그인</a> </li> {{/sessionUser}} </ul> </div> </nav> <!-- 네브바 종료 -->

☕LogController

@PostMapping("/log/save") public String save(LogRequest.LogDTO logDTO) { User user = (User) session.getAttribute("sessionUser"); // 로그인을 하지 않으면 상품상세로 리다이렉트 if (user == null) return "redirect:/store/" + logDTO.getStoreId(); logService.구매하기(logDTO, user); return "redirect:/log"; };

☕LogRequest

@Data public static class LogDTO{ private int storeId; private int qty; }

☕LogService

@Transactional public void 구매하기(LogRequest.LogDTO logDTO, User user) { // 1. 상품 재고 수정 (조회, 수정) // 1-1. 조회 Store store = storeRepository.findById(logDTO.getStoreId()); if(store == null) throw new RuntimeException("해당 상품이 존재하지 않습니다"); // 1-2. 재고 상태 변경 store.재고감소(logDTO.getQty()); // 1-3. 재고 수정 storeRepository.updateById(store.getId(), store.getName(), store.getStock(), store.getPrice()); // 2. 구매 기록 하기 logRepository.save(user.getId(),logDTO.getStoreId(), logDTO.getQty(), logDTO.getQty() * store.getPrice()); }

☕LogRepository

public void save(int userId, int storeId, int qty, int totalPrice) { Query query = em.createNativeQuery("insert into log_tb (user_id, store_id, qty ,total_price, created_at) values (?,?,?,?, now())"); query.setParameter(1, userId); query.setParameter(2, storeId); query.setParameter(3, qty); query.setParameter(4, totalPrice); query.executeUpdate(); }

 
 
 
 
Share article

jjack1