1. view 세팅
1-1. 🧔templates 폴더 세팅 (그림 그리기)

🧔layout/header.mustache
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>blog</title> </head> <body> <nav> <ul> <li> <a href="#">상품목록</a> </li> <li> <a href="#">상품등록</a> </li> <li> <a href="#">구매목록</a> </li> </ul> </nav> <hr>
🧔log/list.mustache
{{>layout/header}} <section> <table border="1"> <tr> <th>주문번호</th> <th>상품명(조인)</th> <th>구매개수</th> <th>총가격</th> <th>구매자이름</th> </tr> <tr> <td>1</td> <td>바나나</td> <td>5개</td> <td>15000원</td> <td>ssar</td> </tr> <tr> <td>2</td> <td>바나나</td> <td>5개</td> <td>15000원</td> <td>ssar</td> </tr> <tr> <td>3</td> <td>딸기</td> <td>5개</td> <td>10000원</td> <td>cos</td> </tr> </table> </section> </body> </html>
🧔store/detail.mustache
{{> layout/header}} <section> <a href="#">수정화면가기</a> <form action="#"> <button type="submit">삭제</button> </form> <div> 번호 : 1 <br> 상품명 : 바나나 <br> 상품가격 : 3000원 <br> 상품재고 : 100개 <br> </div> <form action="#"> <input type="hidden" value="1"> <input type="text" placeholder="당신은 누구인가요?"> <input type="text" placeholder="Enter 개수"> <button type="submit">구매</button> </form> </section> </body> </html>
🧔store/list.mustache
{{>layout/header}} <section> <table border="1"> <tr> <th>번호</th> <th>상품명</th> <th></th> </tr> <tr> <td>1</td> <td>바나나</td> <td><a href="#">상세보기</a></td> </tr> <tr> <td>2</td> <td>딸기</td> <td><a href="#">상세보기</a></td> </tr> </table> </section> </body> </html>
🧔store/save-form.mustache
{{> layout/header}} <section> <form action="#"> <input type="text" placeholder="상품명"><br> <input type="text" placeholder="수량"><br> <input type="text" placeholder="가격"><br> <button type="submit">상품등록</button> </form> </section> </body> </html>
🧔store/update-form.mustache
{{> layout/header}} <section> <form action="#"> <input type="text" value="바나나"><br> <input type="text" value="100"><br> <input type="text" value="3000"><br> <button type="submit">상품수정</button> </form> </section> </body> </html>
1-2. 🔗라우팅 테스트

☕HelloController.java
package com.metacoding.storev1; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; @Controller public class HelloController { @GetMapping("/log/list") public String t1() { return "log/list"; } @GetMapping("/store/list") public String t2() { return "store/list"; } @GetMapping("/store/detail") public String t3() { return "store/detail"; } @GetMapping("/store/save-form") public String t4() { return "store/save-form"; } @GetMapping("/store/update-form") public String t5() { return "store/update-form"; } }
1-3. 실행 결과 화면
🔗/log/list 페이지

🔗/store/list 페이지

🔗/store/detail 페이지

🔗/store/save-form 페이지

🔗/store/update-form 페이지

1-4. ☕java 파일 세팅

- Controller → 요청 받고 응답해주고
- Service → 서비스 로직 작성
- Repository → db에 데이터 넣고 받고
- Entity → Log.java, Store.java
1-5. 테이블 세팅

☕Store.java
package com.metacoding.storev1.store; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; import lombok.Getter; import lombok.NoArgsConstructor; @Getter // mustache에서 필드값에 접근하기 위해 @NoArgsConstructor // JPA가 ObjectMapping을 위해 new할 때 사용 // 리플렉션할 때 초반에 클래스를 생성해야하는데 기본생성자가 없으면 생성 불가하기 때문. @Table(name = "store_tb") // 테이블명 설정 @Entity // 이 클래스가 DB 테이블과 매핑된 엔티티임을 나타냄 public class Store { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String name; // 상품 이름 private Integer stock; // 상품 개수 private Integer price; // 상품 가격 }
☕Log.java
package com.metacoding.storev1.log; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; import lombok.Getter; import lombok.NoArgsConstructor; @Getter // mustache에서 필드값에 접근하기 위해 @NoArgsConstructor // JPA가 ObjectMapping을 위해 new할 때 사용 // 리플렉션할 때 초반에 클래스를 생성해야하는데 기본생성자가 없으면 생성 불가하기 때문. @Table(name = "log_tb") // 테이블명 설정 @Entity // 이 클래스가 DB 테이블과 매핑된 엔티티임을 나타냄 -> JPA에게 알려줌 public class Log { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private Integer storeId; // 상품 ID (FK) private String qty; // 구매 개수 private Integer totalPrice; // qty * store(price) private String buyer; // 구매자 이름 }
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 | classpath->resources 폴더 # 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 # sql formatter spring.jpa.properties.hibernate.format_sql=true
테이블 생성 쿼리문
# Table Setting ## store_tb ```sql create table store_tb ( id integer generated by default as identity, price integer, stock integer, name varchar(255), primary key (id) ); ``` ## log_tb ```sql create table log_tb ( id integer generated by default as identity, store_id integer, total_price integer, buyer varchar(255), qty varchar(255), primary key (id) ) ```
- 빨간줄 → 파일을 읽고 쿼리를 실행함
- 아직 파일도 없고 쿼리도 작성하지 않았으니 주석 처리
table
create table store_tb ( id integer generated by default as identity, price integer, stock integer, name varchar(255), primary key (id) ); create table log_tb ( id integer generated by default as identity, qty integer(255), store_id integer, buyer varchar(255), total_price integer, primary key (id) );
resources/db/data.sql (더미 데이터 생성)
- 더미는 일관성 있게
- 테이블의 수량은 총 계산으로 맞추자
- 총 50개면
- 저장소에 40개
- 구매자에 10개
insert into store_tb (name, price, stock) values ('바나나', 3000, 40); insert into store_tb (name, price, stock) values ('딸기', 2000, 45); insert into log_tb (store_id, total_price, buyer, qty) values (1, 15000, 'ssar', 5); insert into log_tb (store_id, total_price, buyer, qty) values (1, 15000, 'ssar', 5); insert into log_tb (store_id, total_price, buyer, qty) values (2, 10000, 'cos', 5);
- 쿼리문을 작성했으니 위의 프로퍼티 코드에서 빨간줄을 주석해제 하자


1-6. 화면 연결
- mustache 파일과 Controller를 연결하자
🧔templates/layout/header.mustache
<!doctype html> <html lang="en"> <head> <meta charset="UTF-8"> <meta name="viewport" content="width=device-width, initial-scale=1"> <title>blog</title> </head> <body> <nav> <ul> <li> <a href="/">상품목록</a> </li> <li> <a href="/store/save-form">상품등록</a> </li> <li> <a href="/log">구매목록</a> </li> </ul> </nav> <hr>
☕StoreController
@GetMapping("/") public String list() { return "store/list"; } @GetMapping("/store/save-form") public String saveForm() { return "store/save-form"; }
☕LogController
@GetMapping("/log") public String list() { return "log/list"; }
🧔templates/store/detail.mustache
{{> layout/header}} <section> <a href="/store/1/update-form">수정화면가기</a> <form action="/store/1/delete" method="post"> <button type="submit">삭제</button> </form> <div> 번호 : 1 <br> 상품명 : 바나나 <br> 상품가격 : 3000원 <br> 상품재고 : 100개 <br> </div> <form action="#" method="post"> <input type="hidden" value="1"> <input type="text" placeholder="당신은 누구인가요?"> <input type="text" placeholder="Enter 개수"> <button type="submit">구매</button> </form> </section> </body> </html>
☕StoreController
@GetMapping("/store/{id}/update-form") public String updateForm(@PathVariable("id") int id) { return "store/update-form"; } @PostMapping("/store/{id}/delete") public String delete(@PathVariable("id") int id) { return "redirect:/"; }
🧔templates/store/list.mustache
{{>layout/header}} <section> <table border="1"> <tr> <th>번호</th> <th>상품명</th> <th></th> </tr> <tr> <td>1</td> <td>바나나</td> <td><a href="/store/1">상세보기</a></td> </tr> <tr> <td>2</td> <td>딸기</td> <td><a href="/store/2">상세보기</a></td> </tr> </table> </section> </body> </html>
☕StoreController
@GetMapping("/store/{id}") public String detail(@PathVariable("id") int id) { return "store/detail"; }
🧔templates/store/save-form.mustache
{{> layout/header}} <section> <form action="/store/save" method="post"> <input type="text" placeholder="상품명"><br> <input type="text" placeholder="수량"><br> <input type="text" placeholder="가격"><br> <button type="submit">상품등록</button> </form> </section> </body> </html>
☕StoreController
@PostMapping("/store/save") public String save() { return "redirect:/store/1"; }
🧔templates/store/update-form.mustache
{{> layout/header}} <section> <form action="/store/1/update" method="post"> <input type="text" value="바나나"><br> <input type="text" value="100"><br> <input type="text" value="3000"><br> <button type="submit">상품수정</button> </form> </section> </body> </html>
☕StoreController
@PostMapping("/store/{id}/update") public String update(@PathVariable("id") int id) { return "redirect:/store/1"; }
git add . git commit -m "view complete" git push origin master
2. store 로직 추가
2-1. 상품 등록

🧔templates/store/save-form.mustache
{{> layout/header}} {{! value 값 나중에 삭제 }} <section> <form action="/store/save" method="post"> <input type="text" placeholder="상품명" name="name" value="오렌지"><br> <input type="text" placeholder="수량" name="stock" value="30"><br> <input type="text" placeholder="가격" name="price" value="1500"><br> <button type="submit">상품등록</button> </form> </section> </body> </html>
☕StoreController
package com.metacoding.storev1.store; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; import org.springframework.web.bind.annotation.RequestParam; @Controller // IoC (제어의 역전)컨테이너에 등록 => HashSet public class StoreController { private StoreService storeService; public StoreController(StoreService storeService) { this.storeService = storeService; } @GetMapping("/store/save-form") public String saveForm() { return "store/save-form"; } @PostMapping("/store/save") public String save(@RequestParam("name") String name, @RequestParam("stock") int stock, @RequestParam("price") int price) { System.out.println("name : " + name); System.out.println("stock : " + stock); System.out.println("price : " + price); storeService.상품등록(name, stock, price); return "redirect:/"; } }
☕StoreService
package com.metacoding.storev1.store; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @Service public class StoreService { private StoreRepository storeRepository; public StoreService(StoreRepository storeRepository) { this.storeRepository = storeRepository; } @Transactional public void 상품등록(String name, int stock, int price) { storeRepository.save(name, stock, price); } }
☕StoreRepository
package com.metacoding.storev1.store; import org.springframework.stereotype.Repository; import jakarta.persistence.EntityManager; import jakarta.persistence.Query; // @RequiredArgsConstructor // final 이 붙은 필드로만 생성자 만들어 @Repository public class StoreRepository { private final EntityManager em; public StoreRepository(EntityManager em) { this.em = em; } public void save(String name, int stock, int price) { Query query = em.createNativeQuery("insert into store_tb (name, price, stock) values (?, ?, ?)"); query.setParameter(1, name); query.setParameter(2, stock); query.setParameter(3, price); query.executeUpdate(); } }


git add . git commit -m "store save" git push origin master
2-2. 상품 목록

☕StoreRepository
// List<Store> -> 모델 public List<Store> findAll() { // 조건 : 오브젝트 매핑은 @Entity가 붙어야지만 가능하다(디폴트생성자 호출) -> Store.class Query query = em.createNativeQuery("select * from store_tb order by id desc", Store.class); return query.getResultList(); }
☕StoreController
@GetMapping("/") public String list(HttpServletRequest request) { List<Store> storeList = storeService.상품목록(); request.setAttribute("models", storeList); return "store/list"; }
☕StoreService
public List<Store> 상품목록() { List<Store> storeList = storeRepository.findAll(); return storeList; }
🧔templates/store/list.mustache
{{>layout/header}} <section> <table border="1"> <tr> <th>번호</th> <th>상품명</th> <th></th> </tr> {{# models}} <tr> <td>{{id}}</td> <td>{{name}}</td> <td><a href="/store/{{id}}">상세보기</a></td> </tr> {{/models}} </table> </section> </body> </html>


git add . git commit -m "store list" git push origin master
2-3. 상품 상세보기

☕StoreRepository
public Store findById(int id) { // 조건 : 오브젝트 매핑은 @Entity가 붙어야지만 가능하다(디폴트생성자 호출) Query query = em.createNativeQuery("select * from store_tb where id = ?", Store.class); query.setParameter(1, id); return (Store) query.getSingleResult(); }
☕StoreController
@GetMapping("/store/{id}") public String detail(@PathVariable("id") int id, HttpServletRequest request) { Store store = storeService.상품상세보기(id); request.setAttribute("model", store); return "store/detail"; }
☕StoreService
public Store 상품상세보기(int id) { try { return storeRepository.findById(id); } catch (Exception e) { throw new RuntimeException("해당 상품이 없어 상세보기가 불가합니다"); } }
🧔templates/store/list.mustache
{{> layout/header}} <section> <a href="/store/{{id}}/update-form">수정화면가기</a> <form action="/store/{{id}}/delete" method="post"> <button type="submit">삭제</button> </form> <div> 번호 : {{model.id}} <br> 상품명 : {{model.name}} <br> 상품가격 : {{model.price}} <br> 상품재고 : {{model.stock}}개 <br> </div> <form action="#" method="post"> <input type="hidden" value="{{id}}"> <input type="text" placeholder="당신은 누구인가요?"> <input type="text" placeholder="Enter 개수"> <button type="submit">구매</button> </form> </section> </body> </html>

git add . git commit -m "store detail" git push origin master
2-4. 상품 삭제

☕StoreRepository
// TODO #1 삭제 쿼리문 작성 public void deleteById(int id) { Query query = em.createNativeQuery("delete from store_tb where id = ?"); query.setParameter(1, id); query.executeUpdate(); }
☕StoreController
// TODO #2 요청데이터에서 무엇을 사용해서 삭제를 할까 @PostMapping("/store/{id}/delete") public String delete(@PathVariable("id") int id) { storeService.상품삭제(id); return "redirect:/"; }
☕StoreService
// TODO #3 싱픔삭제 로직 작성 @Transactional public void 상품삭제(int id) { try { storeRepository.findById(id); storeRepository.deleteById(id); } catch (Exception e) { throw new RuntimeException("해당 상품이 없어 삭제 불가합니다"); } }


git add . git commit -m "store delete" git push origin master
2-5. 상품 업데이트

☕StoreRepository
// TODO #1 id를 기준으로 업데이트 쿼리 작성 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(); }
☕StoreController
// TODO #2 id, name, stock, price를 받아 업데이트 요청 @PostMapping("/store/{id}/update") public String update(@PathVariable("id") int id, @RequestParam("name") String name, @RequestParam("stock") int stock, @RequestParam("price") int price) { storeService.상품업데이트(id, name, stock, price); return "redirect:/store/" + id; }
☕StoreService
// TODO #3 상품데이터 업데이트 로직 @Transactional public void 상품업데이트(int id, String name, int stock, int price) { try { storeRepository.findById(id); storeRepository.updateById(id, name, stock, price); } catch (Exception e) { throw new RuntimeException("해당 상품이 없어 업데이트가 불가합니다"); } }
🧔templates/store/update-form.mustache
{{> layout/header}} <section> <form action="/store/{{model.id}}/update" method="post"> <input type="text" name="name" value={{model.name}}><br> <input type="text" name="stock" value={{model.stock}}><br> <input type="text" name="price" value={{model.price}}><br> <button type="submit">상품수정</button> </form> </section> </body> </html>
☕StoreController
// TODO #4 업데이트 화면을 요청, id에 해당하는 데이터 출력 @GetMapping("/store/{id}/update-form") public String updateForm(@PathVariable("id") int id, HttpServletRequest request) { Store store = storeService.상품상세보기(id); request.setAttribute("model", store); return "store/update-form"; }


git add . git commit -m "store update" git push origin master
3. log 로직 추가
9. 스프링부트 상점 v1 TEST ← log 쿼리를 테스트
3-0. 쿼리 테이블 확인
결과 테이블

두 테이블을 inner join

이 중에서 보여줄 테이블만 select

3-1. join한 테이블에 담을 DTO 생성
DTO → 화면에 필요한 데이터만 있는 오브젝트
package com.metacoding.storev1.log; import lombok.Data; // DTO : Data Transfer Object -> 화면에 필요한 데이터만 있는 오브젝트 public class LogResponse { @Data // getter, setter, toString public static class ListPage { private int id; private String name; private int qty; private int totalPrice; private String buyer; public ListPage(int id, String name, int qty, int totalPrice, String buyer) { this.id = id; this.name = name; this.qty = qty; this.totalPrice = totalPrice; this.buyer = buyer; } } }
@Data
→ getter, setter, toString을 만들어 주는 어노테이션
3-2. 구매 목록
☕LogRepository
package com.metacoding.storev1.log; import java.util.ArrayList; import java.util.List; import org.springframework.stereotype.Repository; import jakarta.persistence.EntityManager; import jakarta.persistence.Query; @Repository public class LogRepository { private EntityManager em; public LogRepository(EntityManager em) { this.em = em; } public List<LogResponse.ListPage> findAllJoinStore() { List<LogResponse.ListPage> logList = new ArrayList<>(); String q = "SELECT lt.id, st.name, lt.qty, lt.total_price, lt.buyer FROM log_tb lt INNER JOIN store_tb st ON lt.store_id = st.id ORDER BY lt.id DESC"; Query query = em.createNativeQuery(q); List<Object[]> obsList = (List<Object[]>) query.getResultList(); // Object[] -> ROW // ObjectMapping for (Object[] obs : obsList) { LogResponse.ListPage log = new LogResponse.ListPage( (int) obs[0], (String) obs[1], (int) obs[2], (int) obs[3], (String) obs[4]); logList.add(log); } return logList; } }
- 수작업으로 오브젝트 맵핑을 해보았다
☕LogController
package com.metacoding.storev1.log; import java.util.List; import org.springframework.stereotype.Controller; import org.springframework.web.bind.annotation.GetMapping; import jakarta.servlet.http.HttpServletRequest; @Controller public class LogController { private LogService logService; public LogController(LogService logService) { this.logService = logService; } @GetMapping("/log") public String list(HttpServletRequest request) { List<LogResponse.ListPage> listPage = logService.구매목록(); request.setAttribute("models", listPage); return "log/list"; } }
☕LogService
package com.metacoding.storev1.log; import java.util.List; import org.springframework.stereotype.Service; import com.metacoding.storev1.log.LogResponse.ListPage; @Service public class LogService { private LogRepository logRepository; public LogService(LogRepository logRepository) { this.logRepository = logRepository; } public List<ListPage> 구매목록() { return logRepository.findAllJoinStore(); } }
🧔log/list.mustache
{{>layout/header}} <section> <table border="1"> <tr> <th>주문번호</th> <th>상품명(조인)</th> <th>구매개수</th> <th>총가격</th> <th>구매자이름</th> </tr> {{#models}} <tr> <td>{{id}}</td> <td>{{name}}</td> <td>{{qty}}개</td> <td>{{totalPrice}}원</td> <td>{{buyer}}</td> </tr> {{/models}} </table> </section> </body> </html>

3-3. 구매 하기
🧔store/detail.mustache
{{> layout/header}} <section> <a href="/store/{{id}}/update-form">수정화면가기</a> <form action="/store/{{id}}/delete" method="post"> <button type="submit">삭제</button> </form> <div> 번호 : {{model.id}} <br> 상품명 : {{model.name}} <br> 상품가격 : {{model.price}} <br> 상품재고 : {{model.stock}}개 <br> </div> <form action="/log/save" method="post"> <input type="hidden" value="{{model.id}}" name="storeId"> <input type="text" placeholder="당신은 누구인가요?" name="buyer"> <input type="text" placeholder="Enter 개수" name="qty"> <button type="submit">구매</button> </form> </section> </body> </html>
☕LogController
@PostMapping("/log/save") public String save( @RequestParam("storeId") int storeId, @RequestParam("buyer") String buyer, @RequestParam("qty") int qty) { logService.구매하기(storeId, buyer, qty); return "redirect:/log"; }
☕LogService
@Transactional public void 구매하기(int storeId, String buyer, int qty) { // 1. 상품 재고 업데이트 (조회, 업데이트) // 1-1. 조회 Store store = storeRepository.findById(storeId); // 1-2. 재고 상태 변경 store.재고감소(qty); // 1-3. 재고 업데이트 storeRepository.updateById(store.getId(), store.getName(), store.getStock(), store.getPrice()); // 2. 구매 기록 하기 logRepository.save(storeId, qty, qty * store.getPrice(), buyer); }
☕Store
package com.metacoding.storev1.store; import jakarta.persistence.Entity; import jakarta.persistence.GeneratedValue; import jakarta.persistence.GenerationType; import jakarta.persistence.Id; import jakarta.persistence.Table; import lombok.Getter; import lombok.NoArgsConstructor; @Getter // JPA가 필드 값에 접근하기 위해 필요함 @NoArgsConstructor // JPA가 ObjectMapping을 위해 new할 때 사용 // 리플렉션할 때 초반에 클래스를 생성해야하는데 기본생성자가 없으면 생성 불가하기 때문. @Table(name = "store_tb") // 테이블명 설정 @Entity // 이 클래스가 DB 테이블과 매핑된 엔티티임을 나타냄 -> JPA에게 알려줌 public class Store { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Integer id; private String name; // 상품 이름 private Integer stock; // 상품 개수 private Integer price; // 상품 가격 public void 재고감소(int qty) { this.stock = this.stock - qty; } }
☕LogRepository
public void save(int storeId, int qty, int totalPrice, String buyer) { Query query = em.createNativeQuery("insert into log_tb (store_id, qty ,total_price, buyer) values (?,?,?,?)"); query.setParameter(1, storeId); query.setParameter(2, qty); query.setParameter(3, totalPrice); query.setParameter(4, buyer); query.executeUpdate(); }
Share article