반응형
# 스프링 MVC 웹 페이지 만들기
## 프로젝트 생성.
- Dependencies : Spring Web, Thymeleaf, Lombok
- GENERATE 클릭 후 압축 해제 진행. 인텔리제이에서 File > open 클릭 후 압축해제한 폴더 내 build.gradle 선택하여 파일 열기.
- Lombok : File > Settings > annotation processors 입력 후 Enable annotation processing 체크.
- Gradle : File > Settings > Gradle 검색 후 Gradle에서 아래와 같이 설정.
Build and run using : IntelliJ IDEA
Run tests using : IntelliJ IDEA
- 전체적인 설정 완료 후 main 실행해서 정상 작동하는지 확인.
- Port 충돌날 경우 Run > Edit Configurations > Environment variables 에 아래와 같이 입력하여 Port 변경.
server.port=8082
- 웰컴 페이지 추가 : resources > static에 index.html 추가.
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Title</title>
</head>
<body>
<ul>
<li>상품 관리
<ul>
<li><a href="/basic/items">상품 관리 - 기본</a></li>
</ul>
</li>
</ul>
</body>
</html>
## 요구사항 분석
- 상품 도메인 모델 : 상품 ID, 상품명, 가격, 수량,
- 상품 관리 기능 : 상품 목록, 상품 상세, 상품 등록, 상품 수정
- 필요 화면 : 상품 목록, 상품 상세, 상품 등록 폼, 상품 수정 폼
서비스 제공 흐름
## 상품 도메인 개발
item 상품 객체 생성.
package hello.itemservice.domain.item;
import lombok.Data;
import lombok.Getter;
import lombok.Setter;
//@Getter @Setter
@Data
public class Item {
private Long id;
private String itemName;
private Integer price;
private Integer quantity;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
- @Data 의 경우 주의해서 사용 가능하면 @Getter, @Setter 사용.
ItemRepository 상품 저장소.
package hello.itemservice.domain.item;
import org.springframework.stereotype.Repository;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Repository
public class ItemRepository {
// 실무에서는 동시 접근과 관련하여 HashMap 보다는 ConcurrentHashMap사용.
private static final Map<Long, Item> store = new HashMap<>(); // static
private static long sequence = 0L; // static
public Item save(Item item) {
item.setId(++sequence);
store.put(item.getId(), item);
return item;
}
public Item findById(Long id) {
return store.get(id);
}
public List<Item> findAll() {
return new ArrayList<>(store.values());
}
public void update(Long itemId, Item updateParam) {
Item findItem = findById(itemId);
findItem.setItemName(updateParam.getItemName());
findItem.setPrice(updateParam.getPrice());
findItem.setQuantity(updateParam.getQuantity());
}
public void clearStore() {
store.clear();
}
}
ItemRepositoryTest.
package hello.itemservice.domain.item;
import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.assertj.core.api.Assertions.assertThat;
class ItemRepositoryTest {
ItemRepository itemRepository = new ItemRepository();
@AfterEach
void afterEach() {
itemRepository.clearStore();
}
@Test
void save() {
// given
Item item = new Item("itemA", 10000, 10);
// when
Item saveItem = itemRepository.save(item);
// then
Item findItem = itemRepository.findById(item.getId());
assertThat(findItem).isEqualTo(saveItem);
}
@Test
void findAll() {
// given
Item item1 = new Item("item1", 10000, 10);
Item item2 = new Item("item2", 20000, 50);
itemRepository.save(item1);
itemRepository.save(item2);
// when
List<Item> result = itemRepository.findAll();
// then
assertThat(result.size()).isEqualTo(2);
assertThat(result).contains(item1, item2);
}
@Test
void updateItem() {
// given
Item item = new Item("item1", 10000, 10);
Item savedItem = itemRepository.save(item);
Long itemId = savedItem.getId();
// when
Item updateParam = new Item("item2", 20000, 30);
itemRepository.update(itemId, updateParam);
// then
Item findItem = itemRepository.findById(itemId);
assertThat(findItem.getItemName()).isEqualTo(updateParam.getItemName());
assertThat(findItem.getPrice()).isEqualTo(updateParam.getPrice());
assertThat(findItem.getQuantity()).isEqualTo(updateParam.getQuantity());
}
}
## 상품 서비스 HTML
부트스트랩
- HTML을 편리하게 개발하기 위해 부트스트랩 사용
- https://getbootstrap.com/
- 해당 사이트에서 다운로드 받아서 진행 : https://getbootstrap.com/docs/5.0/getting-started/download/
이동: https://getbootstrap.com/docs/5.0/getting-started/download/
Compiled CSS and JS 항목을 다운로드.
압축 출고 bootstrap.min.css 를 복사해 아래 폴더에 추가.
resources/static/css/bootstrap.min.css
HTML, CSS
- /resources/static/css/bootstrap.min.css 부트스트랩 다운로드해서 추가.
- /resources/static/html/items.html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link href="../css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container" style="max-width: 600px">
<div class="py-5 text-center">
<h2>상품 목록</h2>
</div>
<div class="row">
<div class="col">
<button class="btn btn-primary float-end"
onclick="location.href='addForm.html'" type="button">상품
등록</button>
</div>
</div>
<hr class="my-4">
<div>
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>상품명</th>
<th>가격</th>
<th>수량</th>
</tr>
</thead>
<tbody>
<tr>
<td><a href="item.html">1</a></td>
<td><a href="item.html">테스트 상품1</a></td>
<td>10000</td>
<td>10</td>
</tr>
<tr>
<td><a href="item.html">2</a></td>
<td><a href="item.html">테스트 상품2</a></td>
<td>20000</td>
<td>20</td>
</tr>
</tbody>
</table>
</div>
</div> <!-- /container -->
</body>
</html>
- /resources/static/html/item.html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link href="../css/bootstrap.min.css" rel="stylesheet">
<style>
.container {
max-width: 560px;
}
</style>
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2>상품 상세</h2>
</div>
<div>
<label for="itemId">상품 ID</label>
<input type="text" id="itemId" name="itemId" class="form-control"
value="1" readonly>
</div>
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" name="itemName" class="form-control"
value="상품A" readonly>
</div>
<div>
<label for="price">가격</label>
<input type="text" id="price" name="price" class="form-control"
value="10000" readonly>
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" name="quantity" class="form-control"
value="10" readonly>
</div>
<hr class="my-4">
<div class="row">
<div class="col">
<button class="w-100 btn btn-primary btn-lg"
onclick="location.href='editForm.html'" type="button">상품 수정</button>
</div>
<div class="col">
<button class="w-100 btn btn-secondary btn-lg"
onclick="location.href='items.html'" type="button">목록으로</button>
</div>
</div>
</div> <!-- /container -->
</body>
</html>
- /resources/static/html/addForm.html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link href="../css/bootstrap.min.css" rel="stylesheet">
<style>
.container {
max-width: 560px;
}
</style>
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2>상품 등록 폼</h2>
</div>
<h4 class="mb-3">상품 입력</h4>
<form action="item.html" method="post">
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" name="itemName" class="formcontrol" placeholder="이름을 입력하세요">
</div>
<div>
<label for="price">가격</label>
<input type="text" id="price" name="price" class="form-control"
placeholder="가격을 입력하세요">
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" name="quantity" class="formcontrol" placeholder="수량을 입력하세요">
</div>
<hr class="my-4">
<div class="row">
<div class="col">
<button class="w-100 btn btn-primary btn-lg" type="submit">상품
등록</button>
</div>
<div class="col">
<button class="w-100 btn btn-secondary btn-lg"
onclick="location.href='items.html'" type="button">취소</button>
</div>
</div>
</form>
</div> <!-- /container -->
</body>
</html>
- /resources/static/html/editForm.html
<!DOCTYPE HTML>
<html>
<head>
<meta charset="utf-8">
<link href="../css/bootstrap.min.css" rel="stylesheet">
<style>
.container {
max-width: 560px;
}
</style>
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2>상품 수정 폼</h2>
</div>
<form action="item.html" method="post">
<div>
<label for="id">상품 ID</label>
<input type="text" id="id" name="id" class="form-control" value="1"
readonly>
</div>
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" name="itemName" class="formcontrol" value="상품A">
</div>
<div>
<label for="price">가격</label>
<input type="text" id="price" name="price" class="form-control"
value="10000">
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" name="quantity" class="formcontrol" value="10">
</div>
<hr class="my-4">
<div class="row">
<div class="col">
<button class="w-100 btn btn-primary btn-lg" type="submit">저장
</button>
</div>
<div class="col">
<button class="w-100 btn btn-secondary btn-lg"
onclick="location.href='item.html'" type="button">취소</button>
</div>
</div>
</form>
</div> <!-- /container -->
</body>
</html>
## 상품목록_타임리프 사용
- 컨트롤러와 뷰 템플릿 개발.
BasicItemController
package hello.itemservice.web.basic;
import hello.itemservice.domain.item.Item;
import hello.itemservice.domain.item.ItemRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.annotation.PostConstruct;
import java.util.List;
@Controller
@RequestMapping("/basic/items")
@RequiredArgsConstructor
public class BasicItemController {
private final ItemRepository itemRepository;
@GetMapping
public String items(Model model) {
List<Item> items = itemRepository.findAll();
model.addAttribute("items", items);
return "basic/items";
}
/**
* 테스트용 데이터 추가.
* */
@PostConstruct
public void init() {
itemRepository.save(new Item("itemA", 10000, 10));
itemRepository.save(new Item("itemB", 20000, 20));
}
}
- @RequiredArgsConstructor : final 붙은 멤버변수만 사용, 생성자를 자동으로 만들어준다.
items.html
- 아래 경로에 추가.
/resources/templates/basic/items.html
- 속성 변경 : 기존 css href를 th:href로 변경.
<link th:href="@{/css/bootstrap.min.css}"
href="../css/bootstrap.min.css" rel="stylesheet">
- 속성변경 : 기존 상품 등록 폼 이동 onclick 변경
<button class="btn btn-primary float-end"
onclick="location.href='addForm.html'"
th:onclick="|location.href='@{/basic/items/add}'|"
type="button">상품
등록</button>
- 반복 출력 : th:each 사용.
<tr th:each="item : ${items}">
<td><a href="item.html" th:href="@{/basic/items/{itemId}(itemId=${item.id})}" th:text="${item.id}">회원ID</a></td>
<td><a href="item.html" th:href="@{/basic/items/{itemId}(itemId=${item.id})}" th:text="${item.itemName}">상품명</a></td>
<td th:text="${item.price}">10000</td>
<td th:text="${item.quantity}">10</td>
</tr>
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link th:href="@{/css/bootstrap.min.css}"
href="../css/bootstrap.min.css" rel="stylesheet">
</head>
<body>
<div class="container" style="max-width: 600px">
<div class="py-5 text-center">
<h2>상품 목록</h2>
</div>
<div class="row">
<div class="col">
<button class="btn btn-primary float-end"
onclick="location.href='addForm.html'"
th:onclick="|location.href='@{/basic/items/add}'|"
type="button">상품
등록</button>
</div>
</div>
<hr class="my-4">
<div>
<table class="table">
<thead>
<tr>
<th>ID</th>
<th>상품명</th>
<th>가격</th>
<th>수량</th>
</tr>
</thead>
<tbody>
<tr th:each="item : ${items}">
<td><a href="item.html" th:href="@{/basic/items/{itemId}(itemId=${item.id})}" th:text="${item.id}">회원ID</a></td>
<td><a href="item.html" th:href="@{|/basic/items/${item.id}|}" th:text="${item.itemName}">상품명</a></td>
<td th:text="${item.price}">10000</td>
<td th:text="${item.quantity}">10</td>
</tr>
</tbody>
</table>
</div>
</div> <!-- /container -->
</body>
</html>
타임리프
- 타임리프 사용 선언 : 타임리프 사용을 위해 아래와 같이 선언.
<html xmlns:th="http://www.thymeleaf.org">
속성 변경 : th:href
- href="value1"을 th:href="value2"의 값으로 변경.
- 타임리프 뷰 템플릿을 거치면서 원래 값을 th:xxx 로 변경한다. 만약 값이 없으면 새로 생성한다.
<link th:href="@{/css/bootstrap.min.css}"
href="../css/bootstrap.min.css" rel="stylesheet">
속성 변경 : th:onclick
<button class="btn btn-primary float-end"
onclick="location.href='addForm.html'"
th:onclick="|location.href='@{/basic/items/add}'|"
type="button">상품
등록</button>
타임리프의 핵심
- th:xxx가 붙은 부분은 서버사이드에서랜더링 되고, 기존 것을 대체한다. th:xxx가 없으면 기존 html의 xxx 속성이 그대로 사용된다.
- HTML파일을 직접 열었을 때, th:xxx가 있어도 웹 브라우저는 th: 속성을 알지 못하므로 무시. (HTML 파일 보기를 유지하면서도 템플릿 기능도 할 수 있다.)
URL 링크 표현식 : @{ ... }
- @{ } : 타임리프는 URL 링크를 사용하는 경우 @{ } 를 사용. (=URL 링크 표현식)
- URL 링크 표현식을 사용하면 서블릿 컨텍스트를 자동으로 포함.
th:href="@{/css/bootstrap.min.css}"
URL 링크 표현식2 : @ { ... }
- 상품 id 선택 시 링크.
<td><a href="item.html" th:href="@{/basic/items/{itemId}(itemId=${item.id})}" th:text="${item.id}">회원ID</a></td>
- URL 경로 표현식을 이용하여 경로를 템플릿처럼 편리하게 사용.
- 경로 변수 ( {item.id} ) 뿐만 아니라 쿼리 파라미터도 생성한다.
th:href="@{/basic/items/{itemId}(itemId=${item.id}, query='test')}" th:text="${item.id}"
=>
생성링크 : http://localhost:8080/basic/items/1?query=test
리터럴 대체 : | ... |
- 타임리프에서 문자와 표현식 등은 분리되어 있기 때문에 더해서 사용해야 함.
<span th:text="'Welcome to our apllication, ' + ${user.name} + '!'">
- 아래와 같이 리터럴 대체 문법 사용 시 더하기 없이 편리하게 사용 가능.
<span th:text="|Welcome to our apllication, ${user.name} !|">
반복 출력 : th:each
<tr th:each="item : ${items}">
- 반복은 th:each 사용, 이렇게 하면 모델에 포함된 items 컬렉션 데이터가 item 변수에 하나씩 포함, 반복문 안에서 item 변수 사용 가능.
- 컬렉션의 수 만큼 <tr> .. <tr>이 하위 태그를 포함해서 생성.
<tr th:each="item : ${items}">
<td><a href="item.html" th:href="@{/basic/items/{itemId}(itemId=${item.id})}" th:text="${item.id}">회원ID</a></td>
<td><a href="item.html" th:href="@{/basic/items/{itemId}(itemId=${item.id})}" th:text="${item.itemName}">상품명</a></td>
<td th:text="${item.price}">10000</td>
<td th:text="${item.quantity}">10</td>
</tr>
변수 표현식 : ${ ... }
<td th:text="${item.price}">10000</td>
- 모델에 포함된 값이나, 타임리프 변수로 선언한 값을 조회.
- 프로퍼티 접근법을 사용 ( item.getPrice() )
내용 변경 : th:text
<td th:text="${item.price}">10000</td>
- 내용의 값을 th:text의 값으로 변경. 위 코드에서는 10000을 ${item.price} 값으로 변경.
## 상품 상세
BasicItemController : item 추가.
package hello.itemservice.web.basic;
import hello.itemservice.domain.item.Item;
import hello.itemservice.domain.item.ItemRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.annotation.PostConstruct;
import java.util.List;
@Controller
@RequestMapping("/basic/items")
@RequiredArgsConstructor
public class BasicItemController {
private final ItemRepository itemRepository;
@GetMapping
public String items(Model model) {
List<Item> items = itemRepository.findAll();
model.addAttribute("items", items);
return "basic/items";
}
@GetMapping("/{itemId}")
public String item(@PathVariable long itemId, Model model) {
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "basic/item";
}
/**
* 테스트용 데이터 추가.
* */
@PostConstruct
public void init() {
itemRepository.save(new Item("itemA", 10000, 10));
itemRepository.save(new Item("itemB", 20000, 20));
}
}
item.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link th:href="@{/css/bootstrap.min.css}"
href="../css/bootstrap.min.css" rel="stylesheet">
<style>
.container {
max-width: 560px;
}
</style>
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2>상품 상세</h2>
</div>
<div>
<label for="itemId">상품 ID</label>
<input type="text" id="itemId" name="itemId" class="form-control" value="1" th:value="${item.id}" readonly>
</div>
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" name="itemName" class="form-control" value="상품A" th:value="${item.itemName}" readonly>
</div>
<div>
<label for="price">가격</label>
<input type="text" id="price" name="price" class="form-control" value="10000" th:value="${item.price}" readonly>
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" name="quantity" class="form-control" value="10" th:value="${item.quantity}" readonly>
</div>
<hr class="my-4">
<div class="row">
<div class="col">
<button class="w-100 btn btn-primary btn-lg"
onclick="location.href='editForm.html'"
th:onclick="|location.href='@{/basic/items/{itemId}/edit(itemId=${item.id})}'|"
type="button">상품 수정</button>
</div>
<div class="col">
<button class="w-100 btn btn-secondary btn-lg"
onclick="location.href='items.html'"
th:onclick="|location.href='@{/basic/items}'|"
type="button">목록으로</button>
</div>
</div>
</div> <!-- /container -->
</body>
</html>
## 상품 등록 폼
BasicItemController : addForm 추가
package hello.itemservice.web.basic;
import hello.itemservice.domain.item.Item;
import hello.itemservice.domain.item.ItemRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import javax.annotation.PostConstruct;
import java.util.List;
@Controller
@RequestMapping("/basic/items")
@RequiredArgsConstructor
public class BasicItemController {
private final ItemRepository itemRepository;
@GetMapping
public String items(Model model) {
List<Item> items = itemRepository.findAll();
model.addAttribute("items", items);
return "basic/items";
}
@GetMapping("/{itemId}")
public String item(@PathVariable long itemId, Model model) {
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "basic/item";
}
@GetMapping("/add")
public String addForm() {
return "basic/addForm";
}
/**
* 테스트용 데이터 추가.
* */
@PostConstruct
public void init() {
itemRepository.save(new Item("itemA", 10000, 10));
itemRepository.save(new Item("itemB", 20000, 20));
}
}
addForm.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link th:href="@{/css/bootstrap.min.css}"
href="../css/bootstrap.min.css" rel="stylesheet">
<style>
.container {
max-width: 560px;
}
</style>
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2>상품 등록 폼</h2>
</div>
<h4 class="mb-3">상품 입력</h4>
<form action="item.html" th:action method="post">
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" name="itemName" class="formcontrol" placeholder="이름을 입력하세요">
</div>
<div>
<label for="price">가격</label>
<input type="text" id="price" name="price" class="form-control"
placeholder="가격을 입력하세요">
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" name="quantity" class="formcontrol" placeholder="수량을 입력하세요">
</div>
<hr class="my-4">
<div class="row">
<div class="col">
<button class="w-100 btn btn-primary btn-lg"
type="submit">상품등록</button>
</div>
<div class="col">
<button class="w-100 btn btn-secondary btn-lg"
onclick="location.href='items.html'"
th:onclick="|location.href='@{/basic/items}'|"
type="button">취소</button>
</div>
</div>
</form>
</div> <!-- /container -->
</body>
</html>
속성 변경 : th:action
- HTML form에서 action 에 값이 없으면 현재 URL에 데이터를 전송한다.
- 상품 등록 폼의 URL과 실제 상품 등록을 처리하는 URL을 동일하게 맞추고 HTTP 메서드로 두 기능을 구분.
상품 등록 폼 : GET /basic/items/add
상품 등록 처리 : POST /basic/items/add
- 이렇게 하면 하나의 URL로 등록 폼과, 등록 처리를 깔끔하게 처리할 수 있다.
## 상품 등록 처리_@ModelAttribute
- POST - HTML Form
content-type: application/x-www-form-urlencoded
메시지 바디에 쿼리 파리미터 형식으로 전달 itemName=itemA&price=10000&quantity=10
예) 회원 가입, 상품 주문, HTML Form 사용
- 요청 파라미터 형식을 처리해야 하므로 @RequestParam 사용.
상품 등록 처리_@RequestParam
addItemV1 - BasicItemController에 추가
@PostMapping("/add")
public String addItemV1(@RequestParam String itemName,
@RequestParam int price,
@RequestParam Integer quantity,
Model model) {
Item item = new Item();
item.setItemName(itemName);
item.setPrice(price);
item.setQuantity(quantity);
itemRepository.save(item);
model.addAttribute("item", item);
return "basic/item";
}
addItemV2 - BasicItemController에 추가
@PostMapping("/add")
public String addItemV2(@ModelAttribute("item") Item item) {
itemRepository.save(item);
// model.addAttribute("item", item); // 자동 추가, 생략 가능
return "basic/item";
}
@ModelAttribute 중요 기능
1. 요청 파라미터 처리
- @ModelAttribute 는 Item 객체를 생성, 요청 파라미터의 값을 프로퍼티 접근법(setXxx)으로 입력해준다.
2. Model 추가
- 모델(Model)에 @ModelAttribute 로 지정한 객체를 자동으로 넣어준다.
- 위 코드에서 model.addAttribute("item", item) 가 주석처리 되어 있어도 잘 동작.
- 모델에 데이터를 담을 때는 이름이 필요. 이름은 @ModelAttribute 에 지정한 name(value) 속성을 사용.
- 만약 다음과 같이 @ModelAttribute 의 이름을 다르게 지정하면 다른 이름으로 모델에 포함된다.
- @ModelAttribute("hello") Item item -> 이름을 hello 로 지정
- model.addAttribute("hello", item); -> 모델에 hello 이름으로 저장
addItemV3 - BasicItemController에 추가
@PostMapping("/add")
public String addItemV3(@ModelAttribute Item item) {
// @ModelAttribute("item")에서 ("item") 생략 시 Item -> item이 modelAttribute 담기게 됨.
itemRepository.save(item);
// model.addAttribute("item", item); // 자동 추가, 생략 가능
return "basic/item";
}
- @ModelAttribute 의 이름을 생략할 수 있다. (@ModelAttribute 의 이름을 생략 시 모델에 저장될 때 클래스명을 사용. 이때 클래스의 첫글자만 소문자로 변경해서 등록한다.)
@ModelAttribute 클래스명 모델에 자동 추가되는 이름
Item -> item
HelloWorld -> helloWorld
addItemV4 - BasicItemController에 추가
@PostMapping("/add")
public String addItemV4(Item item) {
itemRepository.save(item);
// model.addAttribute("item", item); // 자동 추가, 생략 가능
return "basic/item";
}
- @ModelAttribute 자체도 생략가능.
- 대상 객체는 모델에 자동 등록.
- 나머지 사항은 기존과 동일하다.
## 상품 수정.
BasicItemController : editForm 추가.
@GetMapping("/{itemId}/edit")
public String editForm(@PathVariable Long itemId, Model model) {
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "basic/editForm";
}
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @ModelAttribute Item item) {
itemRepository.update(itemId, item);
return "redirect:/basic/items/{itemId}";
}
- 상품 수정은 상품 등록과 전체 프로세스가 유사.
GET /items/{itemId}/edit : 상품 수정 폼
POST /items/{itemId}/edit : 상품 수정 처리
리다이렉트
- 상품 수정은 마지막에 뷰 템플릿을 호출하는 대신 상품 상세 화면으로 이동하도록 리다이렉트를 호출.
- 스프링은 redirect:/... 으로 편리하게 리다이렉트를 지원.
redirect:/basic/items/{itemId}" 컨트롤러에 매핑된 @PathVariable 의 값은 redirect 에서도 사용 가능.
redirect:/basic/items/{itemId} {itemId} 는 @PathVariable Long itemId 의 값을 그대로 사용.
editForm.html
<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="utf-8">
<link th:href="@{/css/bootstrap.min.css}"
href="../css/bootstrap.min.css" rel="stylesheet">
<style>
.container {
max-width: 560px;
}
</style>
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2>상품 수정 폼</h2>
</div>
<form action="item.html" th:action method="post">
<div>
<label for="id">상품 ID</label>
<input type="text" id="id" name="id" class="form-control" value="1" th:value="${item.id}" readonly>
</div>
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" name="itemName" class="formcontrol" value="상품A" th:value="${item.itemName}">
</div>
<div>
<label for="price">가격</label>
<input type="text" id="price" name="price" class="form-control" value="10000" th:value="${item.price}">
</div>
<div>
<label for="quantity">수량</label>
<input type="text" id="quantity" name="quantity" class="formcontrol" value="10" th:value="${item.quantity}">
</div>
<hr class="my-4">
<div class="row">
<div class="col">
<button class="w-100 btn btn-primary btn-lg" type="submit">저장</button>
</div>
<div class="col">
<button class="w-100 btn btn-secondary btn-lg"
onclick="location.href='item.html'"
th:onclick="|location.href='@{/basic/items/{itemId}(itemId=${item.id})}'|"
type="button">취소</button>
</div>
</div>
</form>
</div> <!-- /container -->
</body>
</html>
## PRG : Post / Redirect / Get
- 상품 등록 처리 컨트롤러는 심각한 문제 존재. (addItemV1 ~ addItemV4, 상품 등록을 완료하고 웹 브라우저의 새로고침 버튼을 클릭 시 상품이 계속해서 중복 등록.)
- 웹 브라우저 새로고침 : 마지막에 서버에 전송한 데이터를 다시 전송 (마지막 행위를 다시 실행(데이터 까지 포함하여))
- 새로고침 해결방법 : 리다이렉트
BasicItemController : addItemV5 추가.
@PostMapping("/add")
public String addItemV5(Item item) {
itemRepository.save(item);
// model.addAttribute("item", item); // 자동 추가, 생략 가능
return "redirect:/basic/items/" + item.getId();
}
주의할점
- "redirect:/basic/items/" + item.getId() redirect에서 +item.getId() 처럼 URL에 변수를 더해서 사용하는 것은 URL 인코딩이 안되기 때문에 위험. RedirectAttributes 를 사용.
## RedirectAttributes
- 상품 저장이 잘 되었으면 상품 상세 화면에 "저장되었습니다" 라는 메시지를 보여달라는 요구사항.
BasicItemController : addItemV6 추가
@PostMapping("/add")
public String addItemV6(Item item, RedirectAttributes redirectAttributes) {
Item saveItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", saveItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/basic/items/{itemId}";
}
item.html 수정
<!-- 추가 -->
<h2 th:if="${param.status}" th:text="'저장 완료'"></h2>
- th:if : 해당 조건이 참이면 실행
- ${param.status} : 타임리프에서 쿼리 파라미터를 편리하게 조회하는 기능, 원래는 컨트롤러에서 모델에 직접 담고 값을 꺼내야 하는데, 쿼리 파라미터는 자주 사용해서 타임리프에서 직접 지원.
## 정리
- 프로젝트 생성.
- 타임리프 사용.
- @ModelAttribute
- 리다이렉트.
- PRG Post/ Redirect / Get 패턴 : 등록 시 리다이렉트 이용하여 중복등록 방지.
- RedirectAttributes
반응형
'인프런 강의 학습 > 스프링 MVC 1' 카테고리의 다른 글
스프링 MVC 10일차_스프링 MVC_기본 기능_3 (0) | 2022.03.01 |
---|---|
스프링 MVC 9일차_스프링 MVC_기본 기능_2 (0) | 2022.02.28 |
스프링 MVC 8일차_스프링 MVC_기본 기능_1 (0) | 2022.02.27 |
스프링 MVC 7일차_스프링 MVC 구조이해. (0) | 2022.02.26 |
스프링 MVC 6일차_MVC 프레임워크 만들기_2 (0) | 2022.02.25 |