일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | |||||
3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | 14 | 15 | 16 |
17 | 18 | 19 | 20 | 21 | 22 | 23 |
24 | 25 | 26 | 27 | 28 | 29 | 30 |
- 기본문법
- 타임리프
- 값 타입 컬렉션
- 예제 도메인 모델
- 실무활용
- JPA 활용2
- Bean Validation
- 페이징
- 검증 애노테이션
- 트위터
- 일론머스크
- 스프링
- JPA 활용 2
- JPQL
- 로그인
- 스프링MVC
- JPA
- jpa 활용
- QueryDSL
- 타임리프 문법
- API 개발 고급
- 프로젝트 환경설정
- Spring Data JPA
- 스프링 데이터 JPA
- 컬렉션 조회 최적화
- 불변 객체
- 임베디드 타입
- 스프링 mvc
- 벌크 연산
- 김영한
- Today
- Total
RE-Heat 개발자 일지
스프링 MVC 1편 - [7] 웹 페이지 만들기 [하편] 본문
출처 : https://www.inflearn.com/course/%EC%8A%A4%ED%94%84%EB%A7%81-mvc-1
인프런 김영한님의 스프링 MVC 1편 강의를 듣고 정리한 내용입니다.
[6] 상품 상세
상품 상세 컨트롤러와 뷰 개발.
BasicItemController - item()
@GetMapping("/{itemId}")
public String item(@PathVariable long itemId, Model model) {
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "basic/item";
}
참고] @RequestParam vs @PathVariable
https://velog.io/@dmchoi224/Rest-API-RequestParam-%EA%B3%BC-PathVariable
http://restapi.com?userId=test&memo=테스트 [쿼리 스트링 방식]
http://restapi.com/test/테스트 [RESTful 방식]
@RequestParam은 쿼리 스트링에서 사용한다.
@PathVariable은 값을 하나만 받아올 수 있으므로 Restful방식에서 활용
/resources/templates/basic/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"
th:onclick="|location.href='@{/basic/items}'|"
onclick="location.href='items.html'"
type="button">목록으로</button>
</div>
</div>
</div> <!-- /container -->
</body>
</html>
=>items.html의 상품ID 혹은 상품명을 클릭하면 localhost:8081/basic/items/1로 넘어감
[7] 상품 등록 폼
BasicItemController : addForm 추가
@GetMapping("/add")
public String addForm() {
return "basic/addForm";
}
/resources/templates/basic/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="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"
th:onclick="|location.href='@{/basic/items}'|"
onclick="location.href='items.html'"
type="button">취소
</button>
</div>
</div>
</form>
</div> <!-- /container -->
</body>
</html>
① th:action : HTML form에서 action에 값이 없으면 현재 URL을 데이터에 전송한다.
② 취소 : 취소 시 상품 목록으로 이동 th:onclick="|location.href='@{/basic/items}'|"
[8] 상품 등록 처리 - @ModelAttribute
상품 등록 폼에 전달된 데이터를 바탕으로 실제 상품을 등록 처리하자!
BasicItemController : 상품등록
//@PostMapping("/add")
public String addItemV1(@RequestParam String itemName,
@RequestParam Integer 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) {
itemRepository.save(item);
//model.addAttribute("item", item);
return "basic/item";
}
//@PostMapping("/add")
public String addItemV3(@ModelAttribute Item item) {
//@ModelAttribute에 이름을 넣지 않으면 클래스의 첫번째 대문자를 소문자로 바꿔서 이름을 넣어 줌.
//ex) Item -> item ==> model.addAttribute("item", item);
//ex) HelloData -> helloData => model.addAttribute("helloData", helloData)
itemRepository.save(item);
//model.addAttribute("item", item);
return "basic/item";
}
//@PostMapping("/add")
public String addItemV4(Item item) {
//@ModelAttribute 생략도 가능. but 비추천
itemRepository.save(item);
//model.addAttribute("item", item);
return "basic/item";
}
■ 상품등록 V1
① 요청파라미터 형식이므로 @RequestParam를 사용.
② Item 객체를 생성하고 ItemRepository를 통해 저장
③ 저장된 item을 모델에 담은 후 뷰에 전달
■ 상품등록 V2
① @ModelAttribute로 변수를 한 번에 받음.
@ModelAttribute는 Item객체를 생성하고 모델에 데이터를 담는 역할까지 해줌.
■ 상품등록 V3
① @ModelAttribute의 이름("item")을 생략하면 클래스명의 첫 글자만 소문자로 변경해 등록한다.
ex) Item -> item ==> model.addAttribute("item", item);
HelloData -> helloData => model.addAttribute("helloData", helloData)
■ 상품등록 V4
① @ModelAttribute 자체도 생략 가능.
다만 @ModelAttribute까지 생략하면 가독성에 좋지 않을 수 있어 영한님은 비추천하심.
[9] 상품 수정
등록한 상품을 수정해 보자!
상품 수정은 상품 등록과 전체 프로세스가 유사하다.
- GET /items/{itemId}/edit: 상품수정폼
- POST /items/{itemId}/edit : 상품 수정 처리
BasicItemController : 상품수정폼
@GetMapping("/{itemId}/edit")
public String editForm(@PathVariable Long itemId, Model model){
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "basic/editForm";
}
/resources/templates/basic/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="form-control" 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="form-control" 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"
th:onclick="|location.href='@{/basic/items/{itemId}(itemId=${item.id})}'|"
onclick="location.href='item.html'" type="button">취소</button>
</div>
</div>
</form>
</div> <!-- /container -->
</body>
</html>
BasicItemController : 상품수정처리
@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @ModelAttribute Item item){
itemRepository.update(itemId, item);
return "redirect:/basic/items/{itemId}";
}
① 리다이렉트
상품수정처리에선 뷰 템플릿을 호출하는 대신 상품 상세 화면으로 이동하도록 리다이렉트를 호출한다.
redirect:/basic/items/{itemId}에서 {itemId}는 @PathVatriable Long itemId 값을 그대로 사용함.
참고] HTML Form 전송은 GET, POST만 사용할 수 있음. PUT, PATCH는 지원하지 않는다.
[10] PRG : Post/Redirect/Get
상품 등록에선 Redirect를 안 썼는데, 상품 수정에선 redirect를 쓴 이유는 무엇일까?
상품 등록 페이지에서 등록 버튼을 누르고 새로고침을 하면 계속 등록되는 문제점이 발생한다.
웹브라우저에 새로 고침은 마지막 행동을 재반복(마지막 서버에 전송한 데이터 다시 전송)하는 것이다. 그러면 POST/add가 다시 호출돼 내용은 같고 ID만 다른 상품 데이터가 쌓이게 된다. 이런 문제에 관한 해법은 바로 Redirect다.
Redirect는 웹 브라우저에게 리다이렉트하라는 명령을 하는 것이고 그러면 웹 브라우저는 GET 방식으로 상품 상세 페이지를 연다. 마지막에 한 행위가 GET방식으로 상품 상세페이지를 여는 것이었으므로 새로고침을 눌러도 상품 상세 화면으로 이동한다.
이런 패턴을 POST -> Redirect -> GET의 앞 글자를 따 PRG 패턴이라고 부른다.
BasicItemController : 상품등록 V5 (PRG 패턴 적용)
//@PostMapping("/add")
public String addItemV5(Item item) {
itemRepository.save(item);
//새로고침을 해도 재등록되지 않도록 redirect처리.
return "redirect:/basic/items/" + item.getId();
}
문제점 : item.getId()를 쓰면 'URL 인코딩'이 되지 않아 위험. 이를 RedirectAttributes를 사용해 해결할 수 있다.
[11] RedirectAttributes
저장 후 바로 상세화면으로 가면 클라이언트 입장에선 제대로 저장이 된 것인지 제대로 판단이 서지 않는다. 이럴 때 RedirectAttributes를 쓰면 편리하게 해결할 수 있다. 또 URL에 한글이나 띄어쓰기 등이 있어 '인코딩'이 필요할 때도 유용하다.
BasicItemController : 상품등록 V6 (RedirectAttributes 적용)
@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}";
}
① redirectAttributes 객체에 itemId를 추가하면서 인코딩도 동시에 해결
② 쿼리 파라미터에 status=true를 추가하면 뷰 템플릿에 조건문을 활용해 (th:if로) '저장되었습니다'를 띄울 수 있음.
실행하면 다음과 같은 리다이렉트 결과가 나온다.
http://localhost:8080/basic/items/3?status=true
■ RedirectAttributes
redirect:/basic/items/{itemId}
URL 인코딩 처리 + PathVariable + 쿼리 파라미터까지 추가
redirect:/basic/items/{itemId}
- pathVariable 바인딩: {itemId}
- 나머지는 쿼리 파라미터로 처리: ?status=true
resources/templates/basic/item.html
<div class="container">
<div class="py-5 text-center">
<h2>상품 상세</h2>
</div>
<!-- -->
<h2 th:if="${param.status}" th:text="'저장 완료'"></h2>
- th:if 해당 조건이 참이면 실행. 여기선 status=true가 쿼리파라미터로 넘어오면 실행
- ${param.status} 타임리프에서 쿼리 파라미터를 편리하게 조회하는 기능.
실행 결과
'백엔드 > 스프링' 카테고리의 다른 글
스프링 MVC 2편 - [1] 타임리프 - 기본기능(하편) (0) | 2023.07.09 |
---|---|
스프링 MVC 2편 - [1] 타임리프 - 기본기능(상편) (0) | 2023.07.08 |
스프링 MVC 1편 - [7] 웹 페이지 만들기 [상편] (0) | 2023.07.05 |
스프링 MVC 1편 - [6] 기본 기능(하편) (0) | 2023.07.02 |
스프링 MVC 1편 - [6] 기본 기능(상편) (0) | 2023.06.30 |