DevLog ๐ถ
[Spring] thymeleaf, bootstrap์ ํตํด ๊ธฐ๋ณธ ์ํ ์ ์ฅ ์น ์ฌ์ดํธ ๋ง๋ค๊ธฐ ๋ณธ๋ฌธ
[Spring] thymeleaf, bootstrap์ ํตํด ๊ธฐ๋ณธ ์ํ ์ ์ฅ ์น ์ฌ์ดํธ ๋ง๋ค๊ธฐ
dolmeng2 2022. 8. 18. 12:10๊น์ํ ๋์ '์คํ๋ง MVC 1ํธ - ๋ฐฑ์๋ ์น ๊ฐ๋ฐ ํต์ฌ ๊ธฐ์ '์ ๋ณด๊ณ ์ ๋ฆฌํ ๊ธ์ ๋๋ค ๐
- ์ง๋ ํฌ์คํ ๊ณผ ์ด์ด์ง๋๋ค :D
- ์ง๋ ํฌ์คํ ์์๋ HTTP ์์ฒญ/์๋ต ํ๋ผ๋ฏธํฐ์ ArgumentResolver, ๊ทธ๋ฆฌ๊ณ HttpMessageConverter์ ๋์ ๋ฐฉ์์ ์์๋ณด์๋ค.
- ์ด๋ฒ ํฌ์คํ ์์๋ ๋ณธ๊ฒฉ์ ์ผ๋ก ์์ ์น ์ฌ์ดํธ๋ฅผ ๋ง๋ค๋ฉด์ ์ง๊ธ๊น์ง ๋ฐฐ์ด ๋ด์ฉ์ ์์ฉํด๋ณด์.
- ์คํ๋ง MVC 1์ ๋ง์ง๋ง ํฌ์คํ ์ด ๋ ์์ ์ ๋๋ค :D
| ๊ฐ๋จํ ์น ํ์ด์ง ๋ง๋ค๊ธฐ
- dependency๋ก Spring Web, Thymeleaf, Lombok์ ์ถ๊ฐํด์ฃผ์.
- ๊ฐ๋จํ ์ํ ์ฃผ๋ฌธ ์น ์ฌ์ดํธ๋ฅผ ๋ง๋ค ์์ ์ด๋ค.
[SItem.java]
- ๋์ ๊ฒฝ์ฐ ์ง๊ธ๊น์ง ํฌ์คํ ํ๋ ๋ด์ฉ์ ํ๋ก์ ํธ๊ฐ ๋ค ํ๋์ ํ๋ก์ ํธ์ฌ์ ๋๋ฉ์ธ ์ด๋ฆ์ ์ ๊ฒน์น๊ฒ ํ๊ธฐ ์ํด ์ด๋ ๊ฒ ์ค์ ํ์๋ค.
@Getter
@NoArgsConstructor
public class SItem {
private Long id;
private String itemName;
private Integer price;
private Integer quantity;
public SItem(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
public void changeId(Long id) {
this.id = id;
}
public void changeItemInfo(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
- setter๋ฅผ ์ ์ด๊ธฐ ์ํด์ ์ต๋ํ ๋ฉ์๋ ๋จ์๋ก ์ด์๋ค.
[SItemRepository.java]
@Repository
public class SItemRepository {
private static final Map<Long, SItem> store = new ConcurrentHashMap<>();
private static AtomicLong sequence = new AtomicLong(0);
public SItem save(SItem item) {
item.changeId(sequence.incrementAndGet());
store.put(item.getId(), item);
return item;
}
public SItem findById(Long id) {
return store.get(id);
}
public List<SItem> findAll() {
return new ArrayList<>(store.values());
}
public void update(Long itemId, SItem updateInfo) {
SItem findItem = store.get(itemId);
findItem.changeItemInfo(updateInfo.getItemName(), updateInfo.getPrice(), updateInfo.getQuantity());
}
public void clearStore() {
store.clear();
}
}
- ๋์์ฑ ์ด์๋ก ์ธํด์ concurrentHashMap๊ณผ AtomicLong์ ์ฌ์ฉํ์๋ค. (์ฌ์ฉํ์ง ์์๋ ๋ฌด๋ฐฉํ๋ค)
[SItemRepositoryTest.java]
class SItemRepositoryTest {
SItemRepository repository = new SItemRepository();
@AfterEach
void afterEach() {
repository.clearStore();
}
@Test
public void save() throws Exception {
// given
SItem item = new SItem("itemA", 10000, 10);
// when
SItem savedItem = repository.save(item);
// then
SItem findItem = repository.findById(savedItem.getId());
assertThat(findItem).isEqualTo(savedItem);
}
@Test
public void findAll() throws Exception {
// given
SItem item1 = new SItem("itemA", 10000, 10);
SItem item2 = new SItem("itemB", 20000, 20);
repository.save(item1);
repository.save(item2);
// when
List<SItem> findItems = repository.findAll();
// then
assertThat(findItems.size()).isEqualTo(2);
assertThat(findItems).contains(item1, item2);
}
@Test
public void updateItem() throws Exception {
// given
SItem item1 = new SItem("itemA", 10000, 10);
SItem savedItem = repository.save(item1);
Long itemId = savedItem.getId();
// when
SItem updateItem = new SItem("itemB", 20000, 20);
repository.update(itemId, updateItem);
SItem findItem = repository.findById(itemId);
// then
assertThat(findItem.getItemName()).isEqualTo(updateItem.getItemName());
assertThat(findItem.getPrice()).isEqualTo(updateItem.getPrice());
assertThat(findItem.getQuantity()).isEqualTo(updateItem.getQuantity());
}
}
- ๊ฐ ๊ธฐ๋ฅ์ ๋ํ ๊ฐ๋จํ ํ ์คํธ ์ฝ๋์ด๋ค. ๋ชจ๋ ์ ๋์๊ฐ๋ ๊ฒ์ ํ์ธํ ์ ์๋ค.
- ์ด์ , html์ ์ค์ ํ๊ธฐ ์ํด ๊ฐ๋จํ ๋ถํธ์คํธ๋ฉ์ ๋ค์ด๋ก๋ ๋ฐ์๋์.
- ์์ถ ํด์ ํ bootstrap.min.css๋ฅผ ๋ณต์ฌํด์ resources/static/css ๋ฐ์ ์ถ๊ฐํด์ฃผ์๋ค.
- ์ดํ ํ์ผ๋ค์ resources/static/basic ํด๋ ๋ฐ์๋ค ์ถ๊ฐํ์๋ค.
[items.html]
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link href="../css/bootstrap.min.css" rel="stylesheet">
<title>item List</title>
</head>
<body>
<div class="container" style="max-width: 600px">
<div class="py-5 text-center">
<h2 class="font-monospace fw-bold">Item List</h2>
<button class="btn btn-warning float-end text-white"
onclick="location.href='addForm.html'"
type="button">
โ
</button>
</div>
<div class="mt-4 font-monospace">
<table class="table table-hover table-bordered">
<thead>
<tr class="text-center">
<th>ID</th>
<th>์ํ๋ช
</th>
<th>๊ฐ๊ฒฉ</th>
<th>์๋</th>
</tr>
</thead>
<tbody class="text-center">
<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>
</body>
</html>
- ๋์ถฉ ์ด๋ฐ ๋๋์ผ๋ก ์์ดํ ๋ฆฌ์คํธ๋ฅผ ๋ํ๋ผ ์ ์๊ฒ ๋ง๋ค์๋ค.
[item.html]
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link href="../css/bootstrap.min.css" rel="stylesheet">
<title>item Detail</title>
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2 class="font-monospace fw-bold">Item Detail</h2>
</div>
<div class="row fw-bold font-monospace">
<div class="col text-center">
<label for="itemId">ID</label>
<input type="text" id="itemId" name="itemId"
class="form-control text-center"
value="1" readonly>
</div>
<div class="col text-center">
<label for="itemName">Name</label>
<input type="text" id="itemName" name="itemName"
class="form-control text-center"
value="์ํA" readonly>
</div>
</div>
<div class="row mt-2 fw-bold font-monospace">
<div class="col text-center">
<label for="price">Price</label>
<input type="text" id="price" name="price"
class="form-control text-center"
value="10000" readonly>
</div>
<div class="col text-center">
<label for="quantity">Quantity</label>
<input type="text" id="quantity" name="quantity"
class="form-control text-center"
value="10" readonly>
</div>
</div>
<hr class="my-4">
<div class="row">
<div class="col">
<button class="w-100 btn btn-warning btn-lg"
onclick="location.href='editForm.html'" type="button">
โ๐ป
</button>
</div>
<div class="col">
<button class="w-100 btn btn-info btn-lg"
onclick="location.href='items.html'" type="button">
๐
</button>
</div>
</div>
</div>
</body>
</html>
- ๊ฐ item์ ๋ํ ์ธ๋ถ ์ ๋ณด๋ฅผ ๋ํ๋ด๋ ํ๋ฉด์ด๋ค.
- ์๋ ๋ ธ๋์ ๋ฒํผ์ ์์ , ํ๋์ ๋ฒํผ์ ๋ชฉ๋ก์ผ๋ก ๋์๊ฐ๋ ๋ฒํผ์ด๋ค.
[addForm.html]
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link href="../css/bootstrap.min.css" rel="stylesheet">
<title>Item Save Form</title>
<style>
</style>
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2 class="font-monospace fw-bold">Item Save Form</h2>
</div>
<form action="item.html" method="post">
<div class="row fw-bold font-monospace">
<div class="col text-center">
<label for="itemName">Name</label>
<input type="text" id="itemName" name="itemName"
class="form-control text-center"
placeholder="Please Enter an Item Name.">
</div>
</div>
<div class="row mt-2 fw-bold font-monospace">
<div class="col text-center">
<label for="price">Price</label>
<input type="text" id="price" name="price"
class="form-control text-center"
placeholder="Please Enter an Item Price.">
</div>
<div class="col text-center">
<label for="quantity">Quantity</label>
<input type="text" id="quantity" name="quantity"
class="form-control text-center"
placeholder="Please Enter an Item Quantity.">
</div>
</div>
<hr class="my-4">
<div class="row">
<div class="col">
<button class="w-100 btn btn-warning btn-lg"
type="submit">
โญ
</button>
</div>
<div class="col">
<button class="w-100 btn btn-danger btn-lg"
onclick="location.href='items.html'" type="button">
โ
</button>
</div>
</div>
</form>
</div>
</body>
</html>
- ์์ดํ ์ ์ฅ form์ด๋ค. ์ํ ์ด๋ฆ, ๊ฐ๊ฒฉ, ์๋์ ๋ฑ๋กํ ์ ์๋ค.
[editForm.html]
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<link href="../css/bootstrap.min.css" rel="stylesheet">
<title>Item Edit Form</title>
<style>
</style>
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2 class="font-monospace fw-bold">Item Edit Form</h2>
</div>
<form action="item.html" method="post">
<div class="row fw-bold font-monospace">
<div class="col text-center">
<label for="id">Id</label>
<input type="text" id="id" name="id"
class="form-control text-center"
value="1" readonly>
</div>
<div class="col text-center">
<label for="itemName">Name</label>
<input type="text" id="itemName" name="itemName"
class="form-control text-center"
value="์ํA">
</div>
</div>
<div class="row mt-2 fw-bold font-monospace">
<div class="col text-center">
<label for="price">Price</label>
<input type="text" id="price" name="price"
class="form-control text-center"
value="10000">
</div>
<div class="col text-center">
<label for="quantity">Quantity</label>
<input type="text" id="quantity" name="quantity"
class="form-control text-center"
value="10">
</div>
</div>
<hr class="my-4">
<div class="row">
<div class="col">
<button class="w-100 btn btn-warning btn-lg"
type="submit">
โญ
</button>
</div>
<div class="col">
<button class="w-100 btn btn-danger btn-lg"
onclick="location.href='items.html'" type="button">
โ
</button>
</div>
</div>
</form>
</div>
</body>
</html>
- ์ํ ์์ ํผ๋ ๋์ผํ๊ฒ ๋ง๋ค์ด์ฃผ์๋ค.
| ์ปจํธ๋กค๋ฌ์ ๋ทฐ ํ ํ๋ฆฟ ๋ง๋ค๊ธฐ
- ๋์ ์ผ๋ก ๊ฐ์ ์์ ํ ์ ์๋๋ก ํ๊ธฐ ์ํด์. html ๊ฒฝ๋ก๋ฅผ resources/templates/basic ๋ฐ์ผ๋ก ์ฎ๊ฒจ์ฃผ์๋ค.
[SItemController.java]
@Controller
@RequestMapping("/basic/items")
@RequiredArgsConstructor
public class SItemController {
private final SItemRepository repository;
@GetMapping
public String items(Model model) {
List<SItem> items = repository.findAll();
model.addAttribute("items", items);
return "basic/items";
}
// ํ
์คํธ ๋ฐ์ดํฐ ์ถ๊ฐ (์์กด๊ด๊ณ ์ฃผ์
์ดํ ์คํ)
@PostConstruct
public void init() {
repository.save(new SItem("itemA", 10000, 10));
repository.save(new SItem("itemB", 20000, 20));
}
}
- DB๋ฅผ ์ฌ์ฉํ์ง ์๊ธฐ ๋๋ฌธ์ ์๋ฒ ์ฌ์์๋ง๋ค ๋ฐ์ดํฐ๋ฅผ ์ถ๊ฐํ ์ ์์ผ๋๊น @PostConstruct๋ฅผ ์ฌ์ฉํด์ ๋ฐ์ดํฐ๋ฅผ ๋ฃ์ด์ฃผ์๋ค.
- ์ ์ฒด ์ํ ๋ชฉ๋ก์ ์กฐํํ ์ ์๋ ์ปจํธ๋กค๋ฌ ๋ฉ์๋๋ง ์์ฑํ์๋ค.
- ์ฐ๋ฆฌ๊ฐ ๋ง๋ html์ ํ์๋ฆฌํ๋ฅผ ์ ์ฉํด๋ณด์.
[items.html] - ์์
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<link href="../css/bootstrap.min.css"
th:href="@{/css/bootstrap.min.css}"
rel="stylesheet">
<title>item List</title>
</head>
<body>
<div class="container" style="max-width: 600px">
<div class="py-5 text-center">
<h2 class="font-monospace fw-bold">Item List</h2>
<button class="btn btn-warning float-end text-white"
onclick="location.href='addForm.html'"
th:onclick="|location.href='@{/basic/items/add}'|"
type="button">
โ
</button>
</div>
<div class="mt-4 font-monospace">
<table class="table table-hover table-bordered">
<thead>
<tr class="text-center">
<th>ID</th>
<th>์ํ๋ช
</th>
<th>๊ฐ๊ฒฉ</th>
<th>์๋</th>
</tr>
</thead>
<tbody class="text-center">
<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}">ItemName</a></td>
<td th:text="${item.price}">Price</td>
<td th:text="${item.quantity}">Quantity</td>
</tr>
</tbody>
</table>
</div>
</div>
</body>
</html>
โ th:href
- ๊ธฐ์กด์ href ๊ฐ์ th:href ๊ฐ์ผ๋ก ๋ณ๊ฒฝํ๋ค. ๊ฐ์ด ์๋ค๋ฉด ์๋ก ์์ฑํ๋ค.
- HTML์ ๊ทธ๋๋ก ์ฌ์ฉํ ๋๋ href ์์ฑ์ด, ๋ทฐ ํ ํ๋ฆฟ์ ๊ฑฐ์น๋ฉด th:href ๊ฐ์ด ๋์ ์ผ๋ก ๋์ฒด๋๋ค.
- ๋๋ถ๋ถ์ HTML ์์ฑ์ ํ์๋ฆฌํ๋ฅผ ํตํด th:๋ก ๋ณ๊ฒฝ์ด ๊ฐ๋ฅํ๋ค.
- HTML์ ํ์ผ๋ก ์ด์์ ๋๋ th:๊ฐ ์์ด๋ ์น ๋ธ๋ผ์ฐ์ ๋ ์ ์ ์๊ธฐ ๋๋ฌธ์, HTML ํ์ผ ๋ณด๊ธฐ + ํ ํ๋ฆฟ ๊ธฐ๋ฅ์ ํจ๊ป ํ ์ ์๋ค.
โ URL ๋งํฌ ํํ @{...}
- URL ๋งํฌ๋ฅผ @{...}๋ก ํํํ ์ ์๋ค. ์๋ธ๋ฆฟ ์ปจํ ์คํธ๋ฅผ ์๋์ผ๋ก ํฌํจ์ํจ๋ค.
(์๋ ์ ๊ฒฝ๋ก ์ด๋ฆ์ ์ง์ ํ์ด์ผ ํ๋๋ฐ, ์ง๊ธ์ ์ ์ ์จ์ ํฌ๊ฒ ์ ๊ฒฝ์ฐ์ง ์์๋ ๋๋ค.)
th:href="@{/basic/items/{itemId}(itemId=${item.id})}"
- itemId๊ฐ item.id์ ๊ฐ์ผ๋ก ์นํ์ด ๋๊ณ , ๊ฒฐ๊ณผ์ ์ผ๋ก ๊ฒฝ๋ก ๋ณ์์ {itemId}์ ๋ค์ด๊ฐ๋ค.
- ์ถ๊ฐ์ ์ผ๋ก, ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ๋ ์์ฑํ ์ ์๋ค.
th:href="@{/basic/items/{itemId}(itemId=${item.id}, query='test')}"
--> /basic/items/1?query=test
- ๋ฆฌํฐ๋ด ๋์ฒด ๋ฌธ๋ฒ์ ์ฌ์ฉํ๋ฉด ๋ ๊ฐ๋จํ๊ฒ ํํํ ์ ์๋ค.
th:href="@{|/basic/items/${item.id}|}"
โ ์์ฑ ๋ณ๊ฒฝ th:onclick
- ์ฌ๊ธฐ์, ๋ฆฌํฐ๋ด ๋์ฒด ๋ฌธ๋ฒ์ด ์ฌ์ฉ๋์๋ค.
th:onclick="|location.href='@{/basic/items/add}'|"
|...| : ํ์๋ฆฌํ์์ ๊ธฐ์กด์๋ ๋ฌธ์, ํํ์์ด ๋ถ๋ฆฌ๋์ด ์์ด์ +๋ฅผ ํตํด ๋ํด์ ์ฌ์ฉํ์ด์ผ ํ๋๋ฐ, ||๋ฅผ ์ฌ์ฉํ๋ฉด ๊ทธ๋ด ํ์๊ฐ ์๋ค.
ex) th:text="'hi' + ${item.name} + '!'" -> th:text="|hi ${item.name}!|"
- ์ฐ๋ฆฌ๋ ๊ธฐ์กด์ด๋ผ๋ฉด ๊ฒฝ๋ก์ ๋ํด์ / ๋ผ๋ ๋ฌธ์๋ฅผ ๋ํด์คฌ์ด์ผ ํ๋๋ฐ, ๋ฆฌํฐ๋ด ๋์ฒด ๋ฌธ๋ฒ์ผ๋ก ํธํ๊ฒ ์ง์ ํด์ฃผ์๋ค.
โ ๋ฐ๋ณต ์ถ๋ ฅ th:each
th:each="item : ${items}
- 'items'๋ผ๋ ์ปฌ๋ ์ ์ ์๋ ๋ฐ์ดํฐ๋ฅผ ํ๋์ฉ ๊บผ๋ด์ item์ ๋ด์์ค๋ค. item.name ์ด๋ฐ ์์ผ๋ก ์ ๊ทผ์ด ๊ฐ๋ฅํ๋ค.
โ ๋ณ์ ํํ์ ${...}, ๋ด์ฉ ๋ณ๊ฒฝ th:text
th:text="${item.itemName}">ItemName</a></td>
- ๋ชจ๋ธ์ ํฌํจ๋ ๊ฐ์ด๋, ํ์๋ฆฌํ ๋ณ์๋ก ์ ์ธํ ๊ฐ์ ${...}์ ํตํด ์กฐํํ ์ ์๋ค.
- ๋ด์ฉ์ ๊ฐ์ th:text๋ฅผ ํตํด ๋ณ๊ฒฝ์ด ๊ฐ๋ฅํ๋ค.
- ์, ์ด์ ์ด์ด์ ๋๋จธ์ง ๋ถ๋ถ๋ ๋ง๋ค์ด๋ณด์.
[SItemController.java] - ์ถ๊ฐ
@GetMapping("/{itemId}")
public String item(@PathVariable Long itemId, Model model) {
SItem item = repository.findById(itemId);
model.addAttribute("item", item);
return "basic/item";
}
@GetMapping("/add")
public String addForm() {
return "basic/addForm";
}
- ์ํ ์์ธ์ ์ํ ๋ฑ๋ก์ ๊ดํ ํผ์ ์ถ๊ฐํ์๋ค.
[item.html] - ์์
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<link href="../css/bootstrap.min.css"
th:href="@{/css/bootstrap.min.css}"
rel="stylesheet">
<title>item Detail</title>
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2 class="font-monospace fw-bold">Item Detail</h2>
</div>
<div class="row fw-bold font-monospace">
<div class="col text-center">
<label for="itemId">ID</label>
<input type="text" id="itemId" name="itemId"
class="form-control text-center"
value="1" th:value="${item.id}" readonly>
</div>
<div class="col text-center">
<label for="itemName">Name</label>
<input type="text" id="itemName" name="itemName"
class="form-control text-center"
value="์ํA" th:value="${item.itemName}" readonly>
</div>
</div>
<div class="row mt-2 fw-bold font-monospace">
<div class="col text-center">
<label for="price">Price</label>
<input type="text" id="price" name="price"
class="form-control text-center"
value="10000" th:value="${item.price}" readonly>
</div>
<div class="col text-center">
<label for="quantity">Quantity</label>
<input type="text" id="quantity" name="quantity"
class="form-control text-center"
value="10" th:value="${item.quantity}" readonly>
</div>
</div>
<hr class="my-4">
<div class="row">
<div class="col">
<button class="w-100 btn btn-warning 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-info btn-lg"
onclick="location.href='items.html'"
th:onclick="|location.href='@{/basic/items}'|"
type="button">
๐
</button>
</div>
</div>
</div>
</body>
</html>
โ ์์ฑ ๋ณ๊ฒฝ th:value
value="10" th:value="${item.quantity}"
- th:value๋ฅผ ํตํด item์ ์๋ quantity ์ ๋ณด๋ฅผ value์ ๋ด์์ค๋ค. (๋ณ์ ํํ์ ์ฌ์ฉ)
[addForm.html]
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<link href="../css/bootstrap.min.css"
th:href="@{/css/bootstrap.min.css}"
rel="stylesheet">
<title>Item Save Form</title>
<style>
</style>
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2 class="font-monospace fw-bold">Item Save Form</h2>
</div>
<form action="item.html" th:action method="post">
<div class="row fw-bold font-monospace">
<div class="col text-center">
<label for="itemName">Name</label>
<input type="text" id="itemName" name="itemName"
class="form-control text-center"
placeholder="Please Enter an Item Name.">
</div>
</div>
<div class="row mt-2 fw-bold font-monospace">
<div class="col text-center">
<label for="price">Price</label>
<input type="text" id="price" name="price"
class="form-control text-center"
placeholder="Please Enter an Item Price.">
</div>
<div class="col text-center">
<label for="quantity">Quantity</label>
<input type="text" id="quantity" name="quantity"
class="form-control text-center"
placeholder="Please Enter an Item Quantity.">
</div>
</div>
<hr class="my-4">
<div class="row">
<div class="col">
<button class="w-100 btn btn-warning btn-lg"
type="submit">
โญ
</button>
</div>
<div class="col">
<button class="w-100 btn btn-danger btn-lg"
onclick="location.href='items.html'"
th:onclick="|location.href='@{/basic/items}'|"
type="button">
โ
</button>
</div>
</div>
</form>
</div>
</body>
</html>
โ ์์ฑ ๋ณ๊ฒฝ th:action
- HTML form์์ action์ ๊ฐ์ด ์์ผ๋ฉด ํ์ฌ URL์ ๋ฐ์ดํฐ๋ฅผ ์ ์กํด์ค๋ค.
- ์ฆ, ์ฌ๊ธฐ์ ํ์ฌ url์ด /basic/item/add์ด๊ธฐ ๋๋ฌธ์ ์ด URL๋ก POST ๋ฐฉ์์ผ๋ก ๋ฐ์ดํฐ๋ฅผ ์ ์กํ๊ฒ ๋๋ค.
- ์ฐ๋ฆฌ๋ ์ํ ๋ฑ๋ก ํผ์ /basic/item/add์์ GET์ผ๋ก, ์ค์ ๋ฑ๋ก์ /basic/item/add์ POST ์ฒ๋ฆฌ๋ฅผ ํ์๋ค.
- ๊ฐ์ URL์ด์ง๋ง HTTP ๋ฉ์๋๋ง ๋ค๋ฅด๊ฒ ํ ๊ฒ์ด๋ค. (ํ๋์ ํผ์์ ๋ฑ๋ก ์ฒ๋ฆฌ๊น์ง ์งํ)
| ์ํ ๋ฑ๋ก/์์ ์งํํ๊ธฐ
- ์ํ ๋ฑ๋ก์ ๋ฉ์์ง ๋ฐ๋์ ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ ํ์์ผ๋ก ์ ๋ฌํ๋ ๋ฐฉ์์ผ๋ก ์งํํ๋ค.
- content-type: application/x-www-form-urlencoded
[SItemController.java] - ์ถ๊ฐ
@PostMapping("/add")
public String addItemV1 (@RequestParam String itemName,
@RequestParam Integer price,
@RequestParam Integer quantity,
Model model) {
SItem item = new SItem(itemName, price, quantity);
repository.save(item);
model.addAttribute("item", item);
return "basic/item";
}
์ด ๋ฐฉ๋ฒ์ ์ ๋์ํ์ง๋ง... ์ด๋ ๊ฒ ๋ชจ๋ ํ๋ผ๋ฏธํฐ๋ฅผ @RequestParam์ ํตํด ๋ฐ๋ ๊ฑด ๋ถํธํ๋ค.
@PostMapping("/add")
public String addItemV2 (@ModelAttribute SItem item, Model model) {
repository.save(item);
model.addAttribute("item", item);
return "basic/item";
}
๊ฐ๋จํ๊ฒ ์ค์ธ ๋ฒ์ ์ด๋ค.
@ModelAttribute์ ๊ฒฝ์ฐ SItem ๊ฐ์ฒด ์์ฑ ๋ฐ ์์ฒญ ํ๋ผ๋ฏธํฐ์ ๊ฐ์ ํ๋กํผํฐ ์ ๊ทผ๋ฒ(setter ์ด์ฉ)์ผ๋ก ์ ๋ ฅํด์ค๋ค.
์ด ๊ฒฝ์ฐ์๋ SItem์ @Setter๊ฐ ์์ด์ผ ํ๋ค!! (ํน์ setXXX๋ผ๋ ์ด๋ฆ์ ๊ฐ์ง ๋ฉ์๋๋ฅผ ๋ง๋ค์ด์ฃผ์)
โญ ๊ฐ์ฒด๋ฅผ ์์ฑํ ๋ ๊ธฐ๋ณธ ์์ฑ์๊ฐ ์๋ ๋ค๋ฅธ ์์ฑ์๊ฐ ์์ผ๋ฉด ๊ทธ ์์ฑ์๋ก ํ๋ผ๋ฏธํฐ ๋ฐ์ธ๋ฉ์, ๊ธฐ๋ณธ ์์ฑ์๋ง ์์ผ๋ฉด setter๋ฅผ ํตํด ๋ฐ์ธ๋ฉ์ ์๋ํ๋ค.
๋ํ, model์๋ค๊ฐ @ModelAttribute๊ฐ ์ง์ ํ ๊ฐ์ฒด๋ฅผ ๋ฃ์ด์ฃผ๋ ์ญํ ๋ ํ๋ค!
model์ ๋ฃ์ ๋ ์ด๋ฆ์ @ModelAttribute์ ์ง์ ํ name(value) ์์ฑ์ ์ฌ์ฉํ๋ค.
@PostMapping("/add")
public String addItemV3 (SItem item) {
repository.save(sItem);
return "basic/item";
}
- ๋ ๊ฐ๋จํ๊ฒ ์ค์ธ ๋ฒ์ ์ด๋ค.
- @ModelAttribute๋ฅผ ์๋ตํ๊ณ , model.addAttribute() ๋ถ๋ถ์ ์ ๊ฑฐํ์๋ค.
- ์๋ตํ์ด๋ SItem ๊ฐ์ฒด๋ฅผ ์์ฑํ๊ณ , ์์ฒญ๋ ํ๋ผ๋ฏธํฐ์ ๊ฐ(itemName, price, quantity)๋ฅผ setXXX ๋ฉ์๋๋ฅผ ํตํด ์ฃผ์ ํ๋ค.
- โญ ๋ชจ๋ธ์ ์ ์ฅ๋ ๋ 'ํด๋์ค๋ช '์ ์ฌ์ฉํ๋ค. ์ด๋, ํด๋์ค์ ์ฒซ๊ธ์๋ง ์๋ฌธ์๋ก ๋ณ๊ฒฝํ๋ค.
- ๊ทธ๋์, ์ ์ฝ๋์ ๊ฒฝ์ฐ model.addAttribute('sItem', item')์ด ๋๋ค๊ณ ๋ณผ ์ ์๋ค.
- ๊ทธ๋ ๊ธฐ ๋๋ฌธ์, ์ด ์ฝ๋๊ฐ ๋์ํ๋ ค๋ฉด html์์ item ๋ถ๋ถ์ sItem์ผ๋ก ๋ฐ๊พธ์ด ์ค์ผ ํ๋ค.
- ๋ง์ฐฌ๊ฐ์ง๋ก ์์ ๋ ๋ฐ๊พธ์ด์ฃผ์.
[SItemController.java] - ์ถ๊ฐ
@GetMapping("/{itemId}/edit")
public String editForm(@PathVariable Long itemId, Model model) {
SItem item = repository.findById(itemId);
model.addAttribute("item", item);
return "basic/editForm";
}
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, SItem item, Model model) {
repository.update(itemId, item);
model.addAttribute("item", item);
return "redirect:/basic/items/{itemId}"
}
- ์์ ํผ๊ณผ ์ค์ ์์ ์ ์ฒ๋ฆฌํ๋ edit ๊ธฐ๋ฅ์ด๋ค.
- ์์ ํ์๋ ์ํ ์์ธ๋ฅผ ๋ณผ ์ ์๋๋ก ๋ฆฌ๋ค์ด๋ ํธ๋ฅผ ์งํํ์๋ค.
[editForm.html]
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<link href="../css/bootstrap.min.css"
th:href="@{/css/bootstrap.min.css}"
rel="stylesheet">
<title>Item Edit Form</title>
<style>
</style>
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2 class="font-monospace fw-bold">Item Edit Form</h2>
</div>
<form action="item.html" th:action method="post">
<div class="row fw-bold font-monospace">
<div class="col text-center">
<label for="id">Id</label>
<input type="text" id="id" name="id"
class="form-control text-center"
value="1" th:value="${item.id}" readonly>
</div>
<div class="col text-center">
<label for="itemName">Name</label>
<input type="text" id="itemName" name="itemName"
class="form-control text-center"
value="์ํA" th:value="${item.itemName}">
</div>
</div>
<div class="row mt-2 fw-bold font-monospace">
<div class="col text-center">
<label for="price">Price</label>
<input type="text" id="price" name="price"
class="form-control text-center"
value="10000" th:value="${item.price}">
</div>
<div class="col text-center">
<label for="quantity">Quantity</label>
<input type="text" id="quantity" name="quantity"
class="form-control text-center"
value="10" th:value="${item.quantity}">
</div>
</div>
<hr class="my-4">
<div class="row">
<div class="col">
<button class="w-100 btn btn-warning btn-lg"
type="submit">
โญ
</button>
</div>
<div class="col">
<button class="w-100 btn btn-danger btn-lg"
onclick="location.href='items.html'"
th:onclick="|location.href='@{/basic/items/{itemId}(itemId=${item.id})}'|"
type="button">
โ
</button>
</div>
</div>
</form>
</div>
</body>
</html>
- ์ ๋ง ์ ๋์ํ๋ ๊ฑธ ๋ณผ ์ ์๋ค!
| RPG, Redirect
- ํ์ฌ ์ฐ๋ฆฌ๊ฐ ๋ง๋ ์ฌ์ดํธ๋ ์ํ ๋ฑ๋ก ํ ์๋ก๊ณ ์นจ์ ํ๋ฉด ์ํ์ด ๊ณ์ ์ค๋ณตํด์ ์์ธ๋ค.
- ์๋ก๊ณ ์นจ : ๋ง์ง๋ง์ผ๋ก ์๋ฒ์ ์ ์กํ ๋ฐ์ดํฐ๋ฅผ ์ฌ์ ์ก
- ์ํ ๋ฑ๋ก ํผ์ POST /add๊ฐ ๊ณ์ ์ฌ์ ์ก๋๊ธฐ ๋๋ฌธ์ ๋์ผํ ์ํ์ด ๊ณ์ ์ถ๊ฐ๋๋ ๊ฒ.
-> ์ด๋ฅผ ๋ฐฉ์งํ๊ธฐ ์ํด์, ์ํ ์ ์ฅ ํ์ ๋ทฐ ํ ํ๋ฆฟ์ด ์๋ ์ํ ์์ธ ํ๋ฉด์ผ๋ก redirect๋ฅผ ์์ผ์ฃผ์.
--> ์ด๋ฌ๋ฉด ์๋ก๊ณ ์นจ์ ํด๋ ์ํ ์์ธ ํ๋ฉด์ ๊ณ์ ๋จธ๋ฌผ๊ฒ ๋๋ค.
[SItemController.java] - ์์
@PostMapping("/add")
public String addItemV2 (SItem item, Model model) {
repository.save(item);
model.addAttribute("item", item);
return "redirect:/basic/items/" + item.getId();
}
- ์ด๋ฐ ๋ฌธ์ ํด๊ฒฐ ๋ฐฉ๋ฒ์ด RPG - POST/REDIRECT/GET ๋ฐฉ์์ด๋ค.
- ๊ทธ๋ฌ๋, ์์ฒ๋ผ redirect๋ฌธ์ ์์ฑํ๋ ๊ฑด URL ์ธ์ฝ๋ฉ์ด ์ ๋๊ธฐ ๋๋ฌธ์ ์ข์ง ์๋ค. ๋ค์์ ๋ฐฉ๋ฒ์ ์ฌ์ฉํ์.
[SItemController.java] - ์์
@PostMapping("/add")
public String addItemV2 (SItem item, Model model, RedirectAttributes redirectAttributes) {
SItem savedItem = repository.save(item);
model.addAttribute("item", item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/basic/items/{itemId};
}
- ์ํ ์ ์ฅ ํ ์ ์ ์ฅ๋์๋ค๋ ๋ฉ์์ง๋ฅผ ์ถ๋ ฅํด์ฃผ๊ธฐ ์ํด์ status๋ผ๋ ๋ณ์๋ฅผ ์ถ๊ฐํด์ฃผ์๋ค.
- ๋ํ +๋ฅผ ํตํ redirect๋ฌธ ๋์ , addAttribute๋ก ์ถ๊ฐํด์ค itemId๋ฅผ ์ฌ์ฉํ์๋ค. ( {itemId} )
- redirectAttributes๋ฅผ ์ฌ์ฉํ๋ฉด ๊ฒฝ๋ก ๋ณ์ + ๋๋จธ์ง๋ ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ๋ก ์ฒ๋ฆฌํด์ค๋ค.
์ URL) basic/items/3?status=true
[item.html]
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
...
</head>
<body>
<div class="container">
<div class="py-5 text-center">
<h2 class="font-monospace fw-bold">Item Detail</h2>
</div>
<h2 class="font-monospace fw-bold text-center text-danger"
th:if="${param.status}" th:text="'Save Success!'"></h2>
...
</body>
</html>
โ th:if
- ํ๊ทธ์ ๋ํ ์กฐ๊ฑด์ ๊ฑธ ์ ์๋ค.
- ${param.status} -> ํ์๋ฆฌํ์์ ์ฟผ๋ฆฌ ํ๋ผ๋ฏธํฐ๋ฅผ ์กฐํํ ๋ ์ฌ์ฉํ ์ ์๋ ๊ธฐ๋ฅ์ด๋ค! param.~์ผ๋ก ์กฐํํ๋ฉด ๋๋ค.
- ์์์ง๋ ์์ง๋ง ์ ๋ฌ๋ค.
- ์ด๋ ๊ฒ ์คํ๋ง MVC1 ๊ฐ์๋ฅผ ์๊ฐํ์๋ค. ์ฝ๋ ๋ ๋ฒจ์์ ๊น๋ณด๋ฉด์ ๊ณต๋ถํ๋ ๊ฒ ๋ณต์ตํ ๋ ์ ์ผ ์ข์๋ ๊ฒ ๊ฐ๋ค.
- ๋นจ๋ฆฌ ์คํ๋ง MVC2๋ ๋ณต์ตํด์ผ๊ฒ ๋ค... ์์์์...!