월루를 꿈꾸는 대학생
[MVC-1] 스프링 MVC -웹 페이지 본문
요구사항 분석
상품 도메인 모델
상품 ID
상품명
가격
수량
상품 관리 기능
상품 목록
상품 상세
상품 등록
상품 수정
흰색 - 컨트롤러
검은색 -뷰
https://javacan.tistory.com/entry/what-is-a-domain-model
@Data는 데이터모델에 쓰기엔 예측하지 못한 동작을 할 수 있기에 게터 ,세터 정도만 쓰는 것이 좋음
쓸려면 확인하고 쓰자 !
레포지토리 작업
@Repository // 안에 @Component가 있으니 컴포넌트 스캔
public class ItemRepository {
private static final Map<Long, Item> store = new HashMap<>(); //static 사용
private static long sequence = 0L; //static 사용
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 findItem = findById(itemId);
findItem.setItemName(updateParam.getItemName());
findItem.setPrice(updateParam.getPrice());
findItem.setQuantity(updateParam.getQuantity());
}
public void clearStore() {
store.clear();
}
}
상품서비스 html
부트스트랩
- 웹 사이트 쉽게 만들어주는 html ,css, js프레임워크
정적 리소스가 공개되는 /resources/static 폴더에 html 넣어두면 공개가 되어버리니 주의가 필요함
상품목록 - 타임리프
컨트롤러
@Controller
@RequestMapping("/basic/items")
public class BasicItemController {
private final ItemRepository itemRepository;
@Autowired
public BasicItemController(ItemRepository itemRepository){
this.itemRepository = itemRepository;
}
@GetMapping
public String items(Model model){
List<Item> items = itemRepository.findAll();
model.addAttribute("itmes",items);
return "basic/items";
}
@PostConstruct // 만들어지기 전에 먼저 호출되는
public void init() {
itemRepository.save(new Item("testA", 10000, 10));
itemRepository.save(new Item("testB", 20000, 20));
}
}
RequiredArgsConstructor
- final 붙은 멤버변수만 사용해서 생성자 자동 만들어줌
- 생성자가 딱 한 개 있으면 스프링이 @Autowired로 의존관계 주입해줌
@PostConstruct
- 해당 빈의 의존관계 주입이 모두 끝나고 초기화 용도로 호출
타임리프는 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">
</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>
속성 변경 - th
<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>
- 타임리프 뷰 템플릿을 거치면 원래 태그를 th:로 시작하는 태그로 덮어씌워서 대체
- th:xxx 가 붙은 부분은 서버사이드에서 랜더링 되고 기존 것을 대체 th:xxx가 없으면 기존 xxx속성 사용
링크 표현식
<link href="../css/bootstrap.min.css"
th:href="@{/css/bootstrap.min.css}" rel="stylesheet">
- @{} : 타임리프에서 url 사용시 씀
리터럴 대체 문법
<td><a href="item.html" th:href="@{|/basic/items/${item.id}|}" th:text="${item.itemName}">상품명</a></td>
- | | : 파이프 안에 문자와 표현식을 사용
- 그냥 큰 따옴표 같은 느낌 원래 그냥 쓰면 다 + 로 붙여야하는데 간편하게 쓰기 가능
반복 출력 th:each
<tr th:each="item : ${items}">
- 컨트롤러에서 받은 items를 반복문에 넣어서 처리
- 변수는 ${} 로 사용 - 플러터랑 같네
내용변경 th:text
<td th:text="${item.price}">10000</td>
- th:text에 있는 값이 표안에 들어가도록 10000의 값을 대체해서 드감
상품 상세
@GetMapping("/{itemId}")
public String item(@PathVariable long itemId, Model model){
Item item = itemRepository.findById(itemId);
model.addAttribute("item",item);
return "basic/item";
}
속성 변경
- th:value="{item.id}"
- 모델에 있는 item 정보를 받고 프로퍼티 접근법으로 값 가져옴 item.getId()
링크
- 리터럴 표현을 사용
th:onclick="|location.href='@{/basic/items/{itemId}/edit(itemId=${item.id})}'|"
상품 등록 폼
@GetMapping("/add")
public String addForm() {
return "basic/addForm";
}
<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>
- th:action
- action에 값이 없으면 현재 url에 데이터 전송함
상품 등록 처리 - @ModelAttribute
상품등록폼의 전송 방식
POST- HTML Form
- content-type: application/x-www-form-urlencoded
- 메시지 바디에 쿼리 파리미터 형식으로 전달
- ex )회원 가입, 상품 주문 등등
실제 넘어가지는 값
html에 있는 name을 키값으로 값을 넘김
level1
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";
}
- html에서 전달한 값들을 @RequestParam으로 다 받은 다음 Item객체 만들어서 저장
level2
public String addItemV2(@ModelAttribute("item") Item item, Model model) {
itemRepository.save(item);
//model.addAttribute("item", item); //자동 추가, 생략 가능
return "basic/item";
}
- @ModelAttribute는 Item객체를 생성하고 요청 파라미터 값을 프로퍼티 접근법 setXXX으로 넣어서 만들어줌
- 파라미터로 받은 Model에 @ModelAttribute가 만든 객체를 자동으로 넣어줌 그래서model.addAttribute("item", item); 생략 가능
- 모델에 넣을 때 이름은 애노테이션에 지정된 이름으로 저장
level3
public String addItemV3(@ModelAttribute Item item) {
// 이름 지정 안 하면 클래스 첫글자 소문자로 바꾼 거로 모델에 담김
itemRepository.save(item);
return "basic/item";
}
- 이름 생략시 클래스의 앞글자를 소문자로 한 이름을 자동으로 등록
- Model 객체가 없지만 Model은 전체 구조에서 사용하는 것이고 RequestMappingHandlerAdapter에서 생성해서주니까 없어도 괜찮은 듯
https://www.inflearn.com/questions/287855
level4
@PostMapping("/add")
public String addItemV4(Item item) {
itemRepository.save(item);
return "basic/item";
}
- @ModelAttribute 자체도 생략 가능
- 대상 객체는 모델에서 자동 등록 됨 지난 시간에 배움
상품 수정
@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}";
}
리다이렉트
- 마지막 리턴시 return "redirect:/basic/items/{itemId}"; 문구로 상품 상세 화면으로 이동이 됨
PRG POST/Redirect/Get
- 상품 등록한 상태로 새로고침하면 같은 값이 계속계속 들어가버림
get으로 상품저장 화면에서 가서 상품 정보 입력후 버튼을 누르면 post방식으로 상품저장을 하게 되고 여기서 새로고침을 누르면 아까 했던 post방식의 상품저장이 되돌이표 되는 것 = > id만 다르고 데이터값은 같은 데이터가 쌓임
새로고침 : 웹 브라우저에 마지막으로 서버에 전송한 데이터를 다시 전송 // 내가 마지막에 했던 행위를 다시 하는 것
문제 해결을 하기 위해 리다이렉트를 함
POST, Redirect GET
상품 저장 후에 리다이렉트로 상품 상세화면으로 넘어가면 주소자체도 바뀌고 마지막 행위가 3번이니까 새로고침해도 get /items/id 주소로 get방식으로 요청하는 걸 반복함
즉 리다이렉트로 url 이랑 마지막 남은 방식을 바꿔서 새로고침의 문제를 해결 가능
@PostMapping("/add") //PRG
public String addItemV5(Item item) {
itemRepository.save(item);
return "redirect:/basic/items/" + item.getId();
}
- 리다이렉트 할 때 주소를 item.getId() 이거는 숫자니까 괜찮은데 한글 , 띄어쓰기 등등 나오면 에러가 남 -> 다음장에서 RedirectAttributes로 해결 가능
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}"; // 리다이렉트에 넣은 itemId가 리턴값 문자열에 들어감
// state는 ?status=true로 들어감
}
컨트롤러에서 리턴하게 되면
다음과 같은 url로 리다이렉트 됨
해당 url은 담당하는 컨트롤러는 얘임
@GetMapping("/{itemId}")
public String item(@PathVariable long itemId, Model model){
Item item = itemRepository.findById(itemId);
model.addAttribute("item",item);
return "basic/item";
}
그럼 리턴값에 해당하는 basic/item html로 가서 처리를 해야겠지!
RedirectAttributes
- url 인코딩 및 pathVarible , 쿼리 파라미터까지 처리 해줌
<h2 th:if="${param.status}" th:text="'저장 완료!'"></h2>
- ${param.status}
- 타임리프에서 쿼리 파라미터 조회하는 기능
- 쿼리 파라미터에 있던 status값을 가져온 다음 참이면 뒤에 text 화면에 표시
'Programing > Spring Boot' 카테고리의 다른 글
[MVC-2] 타임리프 (0) | 2023.08.08 |
---|---|
[MVC-1] 스프링 MVC - 기본 기능 (0) | 2023.08.01 |
[MVC-1] 스프링 MVC - 구조 이해 (0) | 2023.08.01 |
[MVC-1] MVC 프레임 워크 만들기 (0) | 2023.08.01 |
[MVC-1] 서블릿 , JSP , MVC패턴 (0) | 2023.07.25 |