스프링 MVC - 웹 페이지 만들기
1. 프로젝트 생성
스프링 부트 2.7, packaging Jar, dependencies: Spring Web, Thymeleaf,Lombok
1.1 /resources/static/index.html -> 웰컴페이지 추가함
2. 요구사항 분석
상품을 관리할 수 있는 서비스 만들기
상품 도메인 : 상품 ID, 상품명, 가격, 수량
상품 관리 기능 : 상품목록, 상품상세, 상품등록,상품 수정
3. 상품 도메인 개발
@Data
public class Item {
private Long id;
private String itemName;
private Integer price;
private Integer quantity;
public Item() {
}
//..
}
> price와 quantity는 Integer로 선언하여, 0이거나 null일 때도 값을 처리할 수 있도록 함
- 상품저장소
@repository
public class ItemRepository{
private static final Map<Long, Item< store = new HashMap<>();
private static final sequence = 0L;
public Item save(Item item){
item.setId(++sequence);
stroe.put(item.getId(), item);
return item;
}
//..
public void update(Long itemId, Item updateParam){
Item findItem = findById(itemId);
findItem.setItemName(updateParam.getItemName());
//..
}
}
- update 부분에서 item 파라미터 DTO같은 것 따로 만들어서 넣는 게 좋다.
그냥 item으로 받으면, 수정할 정보 외에도 너무 많은 걸 담고 있다. (명확성이 떨어짐)
* 실무에서는 hashmap 쓰면 안된다. (싱글톤 + 멀티스레드)
* 싱글톤으로 관리되기 때문에 사실 static 안써도 상관 없긴함!
다만 따로 생성해서 테스트 하거나 할때 확인하려면, static으로 해야함
* Long도 그냥 쓰면 안되고 아토믹 롱? 같은 것 써야함
4. 상품서비스 HTML
-> HTML파일 따로 다운받음 (CSS 포함)
-> 정적 파일은 직접 열어도 잘 열림
5. 상품목록 - 타임리프
BasicItemController
@Controller
@RequestMapping("/basic/items")
@RequiredArgsConstructor // final붙은애들 사용해서 생성자 만들어준다.
public class BasicItemController {
private final ItemRepository itemRepository; //생성자가 하나면, Autowired생략해도 됨
@GetMapping
public String item(Model model){
List<Item> items = itemRepository.findAll();
model.addAttribute("items",items);
return "basic/items";
}
/**
* 테스트용 데이터 추가
*/
@PostConstruct
public void init() {
itemRepository.save(new Item("testA", 10000, 10));
itemRepository.save(new Item("testB", 20000, 20));
}
}
- 컨트롤러 로직은 모든 상품 조회해서 뷰 템플릿에 담음
- items.html 정적 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>
<!-- -->
<button class="btn btn-primary float-end" onclick="location.href='addForm.html'"
th:onclick="|location.href='@{/basic/items/add}'|"
<!-- ||는 문자열을 이어주는 타임리프 문법 -->
type="button">상품 등록</button>
<!-- -->
<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>
- 타임리프의 장점은 html태그에 코드를 때려박아도, 기존 html을 최대한 유지한다는 것
추후에 마크업 작업을 해도 html코드가 깨지지 않는다.
> 타임리프 사용 선언
<html xmlns:th="http://www.thymeleaf.org">
> 속성 변경 - th:href 등
타임리프 뷰 템플릿을 거치게 되면, 원래 값을 변경함 (값이 없다면 새로 생성)
HTML을 그대로 볼 때는 href속성이 사용되고, 뷰 템플릿을 거치면, th:href의 값으로 치환
대부분의 HTML 속성을 th:xxx로 변경할 수 있음
> 타임리프 핵심
핵심은 th:xxx가 붙은 부분은 서버사이드 랜더링, 나머지는 기존 코드 사용
HTML 파일을 직접 열면 th:xxx가 있어도 속성을 알지 못하므로 무시함
> URL 링크 표현식 @(..)
타임리프의 URL링크 표현식이다. (서블릿 컨텍스트를 자동으로 포함한다)
> 리터럴 대체 |...|
타임리프는 문자와 표현식을 나누어 관리하므로, 더해서 사용해야한다.
<span th:text="'Welcome to our application, ' + ${user.name} + '!'">
이때 리터럴 대체문법을 사용하면, 더하기 없이 편리하게 사용할 수 있다.
> 반복 출력: -th:each
<tr th:each="item : ${items}">
items의 데이터가 item 변수에 담기고, 반복문 안에서 사용할 수 있다.
> 변수 표현식 ${}
프러퍼티 접근법을 사용하여, 변수에 접근가능
${} -> 변수.getxxx와 동일
> 내용 변경 - th:text
HTML 태그 사이 내용을 대체한다.
> 링크 표현식 2
th:href="@{/basic/items/{itemId}(itemId=${item.id})}"
URL 링크 표현식으로 경로를 템플릿 처럼 편리하게 사용가능
경로변수 + 쿼리 파라미터도 생성 가능하다.
th:href="@{/basic/items/{itemId}(itemId=${item.id}, query='test')}"
*정적페이지에서 css경로를 절대경로로 바꾸고 파일을 열면
/css > C:css로 먹음 !! 상대경로로 잘 쓰자.. 웹 프로젝트의 경우
root가 보통 프로젝트 최상위 (webapp / 혹은 resources 임)
[웹 프로젝트는 웹 페이지를 보여주는게 목적!]
6. 상품 상세
@GetMapping("/{itemId}")
public String item(@PathVariable Long itemId, Model mode){
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "basic/item";
- PathVariable로 넘어온 상품 ID 조회하고 모델에 담은 후 뷰 템플릿 호출
6. 상품 등록
@GetMapping("/add")
public String addForm(){
return "basic/addForm";
}
- 단순히 From으로 이동
- th:action HTML form에서 action에 값이 없으면, 현재 URL에 데이터를 전송
- 상품 등록 처리 : POST / basic/items/add
7. 상품 등록 처리 - @ModelAttribute
- 이제 상품 등록 폼에서 전달된 데이터를 실제 상품 등록을 처리해보자
- 상품 등록 폼은 다음 방식으로 서버에 데이터를 전달함
POST - HTML Form
content-type : application/x-www-form-urlencoded
메시지 바디에 쿼리 파라미터 형식으로 전달
요청 파라미터를 처리해야하므로, @RequestParam을 사용
@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, Model model) {
itemRepository.save(item);
//model.addAttribute("item", item);
return "basic/item";
@PostMapping("/add")
public String addItemV3(Item item) {
itemRepository.save(item);
//model.addAttribute("item", item);
return "basic/item";
}
- V1 요청 파라미터로 하나씩 다 받은 후에 데이터를 토대로 Item객체를 생성
- V2 @ModelAttribute로 요청 파라미터 처리 (프로퍼티 접근법 사용 setXXX로 생성해줌)
Model추가 : 모델에 @ModelAttribute로 지정한 객체 자동으로 넣어줌
모델에 데이터를 담을 때는 이름이 필요함, 이름을 지정하지 않으면 클래스명 사용
- V3 ModelAttribute생략 (요청 파라미터의 경우 기본 자료형 아니면, 자동으로 붙여줌)
*어차피 Model은 컨트롤러 밖에서 생성되어 주입되는 것!! (어뎁터 + Argument Resolver)
8. 상품 수정
@GetMappint("/{itemId}/edit")
public String editForm(@PathVariable Long itemId, Model model){
Item item = itemRepository.findById(itemId);
model.addAttribute("item", item);
return "basic/editForm";
}
> 상품 수정 폼 뷰 (등록폼과 유사)
> Get /item/{itemId}/edit : 상품 수정 폼
> Post /item/{itemId}/edit : 상품 수정 처리
- 리다이렉트
상품 수정은 마지막에 뷰 템플릿을 호출하는 대신에 상품 상세 화면으로 이동하도록 리다이렉트 호출한다.
스프링은 redirect:/..로 지원
> 컨트롤러에 매핑된 @PathVariable은 redirect에도 사용할 수 있다.
(redirect: /basic/items/{itemId} -> @PathVariable Long itemId 그대로 사용
9 PRG: Post/Redirect/Get
- POST 등록 후 새로고침
웹 브라우저의 새로 고침은 마지막에 서버에 전송한 데이터를 다시 전송
상품 등록 폼에서 데이터 입력하고 저장하면, POST/add + 상품데이터 서버 전송
새로 고침하면, 또 마지막에 전송한 POST/add가 전송되게 됨
- 새로 고침 문제를 해결하기 위해 상품 저장 후 뷰 템플릿으로 이동하는 것이 아니라
상품 상세 화면으로 리다이렉트 호출! (컨트롤러 호출 -> 뷰템플릿은 포워딩처리해서 url변화 없음)
리다이렉트는 GET 방식으로 컨트롤러 호출함 물리적 URL이 변화 -> 새로고침해도 상세화면만 나온다.
@PostMapping("/add")
public String addItemV5(Item item){
itemRepository.save(item);
return "redirect:/basic/item/"+ item.getId(); //URL 인코딩 문제 있음 사용x
}
10 RedirectAttributes
리다이렉트 후 -> 고객입장에서 저장이 잘 된건지 확인받고 싶음
저장되었다는 메시지 보여달라는 요구사항이 들어옴
@PostMapping("/add")
public String addItemV6(Item item, RedirectAttributes rd){
Item savedItem = itemRepository.save(item);
rd.addAttribute("itemId", savedItem.getId());
rd.addAttribute("status",true);
return "redirect:/basic/items/{itemId}";
}
- RedirectAttriute를 사용하면, URL인코딩, pathVariable,쿼리 파라미터 처리 다해준다.
- 위 객체에 값을 add시
- redirect를 확인해서 {} < 바인딩 가능한건 꺼내서 바인딩하고, 나머지는 전부 쿼리 스트링 처리
<div class="container">
<div class="py-5 text-center">
<h2>상품 상세</h2>
</div>
<!-- 추가 -->
<h2 th:if="${param.status}" th:text="'저장 완료!'"></h2>
- ${param.status}: 타임리프 쿼리 파라미터 간편 조회 기능
원래는 모델에 담고 값을 꺼내야하는데, 쿼리파라미터는 자주 사용해서 타임리프에서 지원해줌