-
스프링 MVC - 웹 페이지 만들기Web/Spring 2023. 10. 15. 16:15
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}: 타임리프 쿼리 파라미터 간편 조회 기능
원래는 모델에 담고 값을 꺼내야하는데, 쿼리파라미터는 자주 사용해서 타임리프에서 지원해줌
'Web > Spring' 카테고리의 다른 글
SpringMVC 2 - 검증 (0) 2023.10.24 Spring MVC 2 - 국제화/메시지 (0) 2023.10.24 Spring MVC (6) - 스프링 MVC 기본기능 (2) : Http Body (0) 2023.10.14 Spring MVC (6) - 스프링 MVC 기본 기능 (0) 2023.10.09 SpringMVC (5) - 스프링 MVC 구조 이해 (0) 2023.10.02