간단한 웹 페이지를 만들면서 알게되는 코드와 결과만 블로그에 남기겠습니다. 상세코드는 github를 통해 확인 할 수 있습니다.
상품 도메인 모델
상품 관리 기능
서비스 제공 흐름
Item - 상품 객체, ItemRepository - 상품 저장소를 개발했고 ItemRepository에는 상품 저장, 찾기, 전체 찾기, 업데이트 기능이 있다.
ItemRepositoryTest를 통해 테스트 해보겠다.
package com.example.springmvc.domain.item;
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 savedItem = itemRepository.save(item);
//then
Item findItem = itemRepository.findById(item.getId());
assertThat(findItem).isEqualTo(savedItem);
}
@Test
void findAll() {
//given
Item item1 = new Item("item1", 10000, 10);
Item item2 = new Item("item2", 20000, 20);
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);
Item findItem = itemRepository.findById(itemId);
//then
assertThat(findItem.getItemName()).isEqualTo(updateParam.getItemName());
assertThat(findItem.getPrice()).isEqualTo(updateParam.getPrice());
assertThat(findItem.getQuantity()).isEqualTo(updateParam.getQuantity());
}
}
성공
핵심 비즈니스 로직을 개발하는 동안, 웹 퍼블리셔는 HTML 마크업을 완료했다는 가정이다.
위 프로젝트는 부트스트랩을 사용하였다.
resources/static/css/bootstrap.min.css를 추가하였다.
아래에서 실행 결과 올릴 때 뷰도 확인 할 수 있기 때문에 따로 올리진 않겠다.
<!DOCTYPE HTML>
<html 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">
</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
th:href="@{/css/bootstrap.min.css}"
URL 링크 표현식 - @{...}
속성 변경 - th:onclick
th:onclick="|location.href='@{/basic/items/add}'|"
리터럴 대체 - |...|
<span th:text="'Welcome to our application, ' + ${user.name} + '!'">
<span th:text="|Welcome to our application, ${user.name}!|">
반복 출력 - th:each
변수 표현식 - ${...}
<td th:text="${item.price}">10000</td>
내용 변경 - th:text
<td th:text="${item.price}">10000</td>
- 타임리프는 순수 HTML 파일을 웹 브라우저에서 열어도 내용을 확인할 수 있고, 서버를 통해 뷰 템플릿을 거치면 동적으로 변경된 결과를 확인할 수 있다. JSP를 생각해보면, JSP 파일은 웹 브라우저에서 그냥 열면 JSP 소스코드와 HTML이 뒤죽박죽 되어서 정상적인 확인이 불가능하다. 오직 서버를 통해서 JSP를 열어야 한다.
- 이렇게 순수 HTML을 그대로 유지하면서 뷰 템플릿도 사용할 수 있는 타임리프의 특징을 네츄럴 템플릿 (natural templates)이라 한다.
package com.example.springmvc.web.item.basic;
import com.example.springmvc.domain.item.Item;
import com.example.springmvc.domain.item.ItemRepository;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.*;
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";
}
// 상품 수정 폼
@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}";
}
// @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";
// }
/**
* @ModelAttribute("item") Item item
* model.addAttribute("item", item); 자동 추가
*/
// @PostMapping("/add")
// public String addItemV2(@ModelAttribute("item") Item item, Model model) {
// itemRepository.save(item);
// //model.addAttribute("item", item); //자동 추가, 생략 가능
// return "basic/item";
// }
/**
* @ModelAttribute name 생략 가능
* model.addAttribute(item); 자동 추가, 생략 가능
* 생략시 model에 저장되는 name은 클래스명 첫글자만 소문자로 등록 Item -> item
*/
// @PostMapping("/add")
// public String addItemV3(@ModelAttribute Item item) {
// itemRepository.save(item);
// return "basic/item";
// }
/**
* @ModelAttribute 자체 생략 가능
* model.addAttribute(item) 자동 추가
*/
// 등록
@PostMapping("/add")
public String addItemV4(Item item) {
itemRepository.save(item);
return "basic/item";
}
}
상품 등록
addItemV1
addItemV2
addItemV3
addItemV4
지금 까지 했던 것 중 오류가 있다.
POST, Redirect GET
@PostMapping("/add")
public String addItemV4(Item item) {
itemRepository.save(item);
return "basic/item";
}
변경
@PostMapping("/add")
public String addItemV5(Item item) {
itemRepository.save(item);
return "redirect:/basic/items/" + item.getId();
}
컨트롤러 코드 추가
@PostMapping("/add")
public String addItemV6(Item item, RedirectAttributes redirectAttributes) {
Item savedItem = itemRepository.save(item);
redirectAttributes.addAttribute("itemId", savedItem.getId());
redirectAttributes.addAttribute("status", true);
return "redirect:/basic/items/{itemId}";
}
item.html 코드추가
<h2 th:if="${param.status}" th:text="'저장 완료!'"></h2>
상품 목록
상품 등록
상품 상세
상품 수정
최종 확인
참고
김영한: 스프링 MVC 1편 - 백엔드 웹 개발 핵심 기술(인프런)
Github - https://github.com/b2b2004/Spring_MVC