요구사항 분석
- 상품 도메인 모델
상품 ID
상품명
가격
수량
- 상품 관리 기능
상품 목록
상품 상세
상품 등록
상품 수정
- 서비스 제공 흐름
백엔드 개발자: 디자이너, 웹 퍼블리셔를 통해서 HTML 화면이 나오기 전까지 시스템을 설계하고, 핵심
비즈니스 모델 개발
이후 HTML이 나오면 뷰 템플릿으로 동적 변환, 웹 화면 흐름 제어
※
웹 프론트엔드 개발자가 별도로 있으면, 웹 프론트엔드 개발자가 웹 퍼블리셔 역할까지 포함해서 하는 경우도 있음
웹 프론트엔드 개발자 => HTML을 동적으로 만들고 웹 화면의 흐름을 담당
백엔드 개발자 => HTTP API를 통해 웹 클라이언트가 필요로 하는 데이터와 기능을 제공
상품 도메인 개발
- Item
package hello.itemservice.domain.item;
import lombok.Data;
//@Getter @Setter
@Data //Getter, Setter, RequiredArgsConstructor, ToString, EqualsAndHashCode, Value
public class Item {
private Long id;
private String itemName;
private Integer price; //null 허용 위해 Integer 사용
private Integer quantity;
public Item() {
}
public Item(String itemName, Integer price, Integer quantity) {
this.itemName = itemName;
this.price = price;
this.quantity = quantity;
}
}
- 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 {
private static final Map<Long, Item> store = new HashMap<>(); //static
// 멀티스레드에서는 ConcurrentHashMap
private static long sequence = 0L; //static
// 멀티스레드에서는 AtomicLong
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 itemFound = findById(itemId);
itemFound.setItemName(updateParam.getItemName());
itemFound.setPrice(updateParam.getPrice());
itemFound.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;
import static org.junit.jupiter.api.Assertions.*;
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 itemFound = itemRepository.findById(savedItem.getId());
assertThat(itemFound).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 update() {
//given
Item item = new Item("item", 20000, 20);
Item savedItem = itemRepository.save(item);
Long id = savedItem.getId();
//when
Item updateParam = new Item("item2", 23000, 10);
itemRepository.update(id, updateParam);
//then
Item foundItem = itemRepository.findById(id);
assertThat(foundItem.getItemName()).isEqualTo(updateParam.getItemName());
assertThat(foundItem.getPrice()).isEqualTo(updateParam.getPrice());
assertThat(foundItem.getQuantity()).isEqualTo(updateParam.getQuantity());
}
}
상품 목록 (타임리프)
부트 스트랩 활용 ( https://getbootstrap.com/docs/5.0/getting-started/download/ )
resources/static/css/bootstrap.min.css
※ /resources/static => 전체 공개되는 정적 리소스 (HTML)
※ /resources/templates => 뷰 템플릿 (타임리프)
- 타임리프
th:xxx 가 붙은 부분은 서버사이드에서 렌더링되어 기존 HTML 속성 대체
HTML을 파일로 직접 열었을 때 th:xxx 있어도 웹 브라우저는 th: 속성 알지 못해 무시함
∴ 순수 HTML을 유지하면서 뷰 템플릿도 사용 가능 => 내추럴 템플릿
- BasicItemController
@Controller
@RequestMapping("/basic/items")
@RequiredArgsConstructor // final 붙은 변수를 사용해 생성자 자동 생성
public class BasicItemController {
private final ItemRepository itemRepository;
/* @RequiredArgsConstructor로 생략
@Autowired // 필드 하나면 생략 가능
public BasicItemController(ItemRepository itemRepository) {
this.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));
}
}
- items.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"> <!--뷰 템플릿 거치면 th:href 값이 href로 대체되어 동적 변경-->
</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> <!--url 링크-->
<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>
상품 상세
- BasicItemController
@GetMapping("/{itemId}")
public String item(@PathVariable long itemId, Model model){
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "basic/item";
}
- 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>
<!---->
<h2 th:if="${param.status}" th:text="'저장 완료'"></h2> <!--요청 파라미터 값-->
<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
@GetMapping("/add")
public String addForm(){
return "basic/addForm";
}
- addForm.html
<!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">
<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"><!--th:action 값 없으면 현재 URL에 데이터 전송--->
<div>
<label for="itemName">상품명</label>
<input type="text" id="itemName" name="itemName" class="form-control" 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="form-control" 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>
상품 등록 처리 - @ModelAttribute
- POST - HTML Form
content-type: application/x-www-form-urlencoded
메시지 바디에 쿼리 파리미터 형식으로 전달 ex) itemName=itemA&price=10000&quantity=10
- 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";
}
//@PostMapping("/add")
public String addItemV2(@ModelAttribute("item") Item item){ //item이라는 key 값으로 model에 자동 추가
itemRepository.save(item);
//model.addAttribute("item", item);
return "basic/item";
}
//@PostMapping("/add")
public String addItemV3(@ModelAttribute Item item){ //괄호 생략 시 클래스명 첫글자만 소문자로 바꿔 model 저장
itemRepository.save(item);
return "basic/item";
}
//@PostMapping("/add")
public String addItemV4(Item item){ // String, int 외 타입은 @ModelAttribute로 인식
itemRepository.save(item);
return "basic/item"; //폼 새로고침 시 계속 POST 호출 -> 계속 등록됨 => PRG(post redirect get) 필요
}
상품 수정
- BasicItemController
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @ModelAttribute Item item){
itemRepository.update(itemId, item);
return "redirect:/basic/items/{itemId}"; //Pathvariable 값 활용
}
※HTML form 전송은 PUT, PATCH 지원 X
Post / Redirect / Get
- BasicItemController
//@PostMapping("/add")
public String addItemV4(Item item){ // String, int 외 타입은 @ModelAttribute로 인식
itemRepository.save(item);
return "basic/item"; //등록 후 새로고침 시 계속 POST 호출 -> 계속 등록됨 => PRG(post redirect get) 필요
}
//@PostMapping("/add")
public String addItemV5(Item item){
itemRepository.save(item);
return "redirect:/basic/items/" + item.getId(); //상품 상세 페이지로 redirect //getId() => 인코딩 안돼 위험
}
RedirectAttributes
: URL 인코딩, Pathvariable 쿼리 파라미터 처리
- BasicItemController
@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}"; //getId
}
'Programming > 스프링' 카테고리의 다른 글
[김영한 스프링 MVC 2] 타임리프 - 스프링 통합과 폼 (0) | 2023.12.02 |
---|---|
[김영한 스프링 MVC 2] 타임리프 - 기본 기능 (0) | 2023.11.26 |
[김영한 스프링 MVC 1] 스프링 MVC - 기본 기능 (0) | 2023.10.29 |
[김영한 스프링 MVC 1] 스프링 MVC - 구조 이해 (0) | 2023.10.22 |
[김영한 스프링 MVC 1] MVC 프레임워크 만들기 (0) | 2023.09.24 |