1. 상점 그림 그리기
그림 그리는 방법
- home.html 파일 생성
- Bootstrap link 추가
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>
- home 화면을 먼저 만들어보기
- 인텔리J를 사용 중이라면
- 화면에 크롬아이콘을 클릭해 html 화면을 확인 하면서 만들기
- cursor을 사용 중이라면
- live server을 켜서 html 화면을 확인 하면서 만들기
- home 화면을 복사 붙이기 하면서 다른 화면도 만들기
- 화면을 다 만들었다면, templates/layout 폴더를 생성
- nav 부분을 header.html 파일로 옮기기
- footer 부분을 footer.html 파일로 옮기기
- 모든 html 파일을 mustache 파일로 변경
1. 인증 관련
1-1. home

- 프로그램의 메인 화면
- 로그인 유무에 따라 다음 화면을 만들어야함
로그인 상태라면
- 회원가입 ❌
- 로그인 ❌
- 로그아웃 ✅
- 가운데 이름 표시 ✅
로그아웃 상태라면
- 회원가입 ✅
- 로그인 ✅
- 로그아웃 ❌
- 가운데 이름 표시 ❌
1-2. user/join-form

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

로그인 화면
- 데이터를 저장하지 않는 유일한 post 요청
form
태그로 만들기
form
태그에action
에 주소 잘 확인하기
input
태그에required
를 추가해 무조건input
값을 받도록 만들기
input
태그에name
속성 유무 확인하기
2. 상점 관련
2-1. store/list

상품목록 화면
table
로 만들기
a
태그를 사용해 상세보기 링크를 만들기
2-2. store/detail

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

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

상품등록 화면
form
태그로 만들기
form
태그에action
에 주소 잘 확인하기
input
태그에required
를 추가해 무조건input
값을 받도록 만들기
input
태그에name
속성 유무 확인하기
- post 요청 주소에는 맨 끝에 save를 붙여서 사용하기
3. 구매 관련
3-1. log/list

구매목록 화면
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 & 테이블 만들기

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. 회원가입

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. 로그인 & 로그아웃


로그인 성공하면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. 상품목록

- 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. 상품상세

- 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. 상품수정

- 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. 상품등록

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. 구매목록

- 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. 구매하기

- 로그인 한 유저만 구매 할 수 있다
- 이 데이터에 대한 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