1 분 소요


상품 수정(Get)

BasicItemController

@GetMapping("/{itemId}/edit")
public String editForm(@PathVariable Long itemId, Model model) {
    Item item = itemRepository.findById(itemId);
    model.addAttribute("item", item);
    return "basic/editForm";
}


상품 수정(html)

/resources/templates/basic/editForm.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

<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" 
            th:value="${item.price}">
    </div>

    <div>
        <label for="quantity">수량</label>
        <input type="text" id="quantity" name="quantity" class="form-control" 
            th:value="${item.quantity}">
    </div>

    <!-- 코드 생략.. -->
    <button class="w-100 btn btn-primary btn-lg" type="submit">저장 </button>

    <!-- 코드 생략.. -->
    <button class="w-100 btn btn-secondary btn-lg" 
            onclick="location.href='item.html'" 
            th:onclick="|location.href='@{/basic/items/{itemId}(itemId=${item.id})}'|" 
            type="button">취소</button>
</form>


상품 수정(Post)

BasicItemController

@PostMapping("/{itemId}/edit")
public String edit(@PathVariable Long itemId, @ModelAttribute Item item) {
    itemRepository.update(itemId, item);
    return "redirect:/basic/items/{itemId}";
}
  • GET /items/{itemId}/edit : 상품 수정 폼
  • POST /items/{itemId}/edit : 상품 수정 처리


Redirect

상품 수정 마지막에 뷰 템플릿을 호출하는 대신에 상품 상세 화면으로 이동하도록 리다이렉트를 호출.

  • 스프링은 redirect:/...로 편리하게 리다이렉트를 지원.
  • redirect:/basic/items/{itemId}"
    • 컨트롤러에 매핑된 @PathVariable의 값은 redirect 에도 사용 할 수 있다.


PRG

Post/Redirect/Get
상품 등록 처리 컨트롤러(addItemV1 ~ v4)는 등록을 완료하고 웹 브라우저의 새로고침을 하면 중복 등록되는 문제가 있다.

전체 흐름
PRG1

POST 등록 후 새로 고침

PRG2
웹 브라우저의 새로 고침은 서버에 전송한 마지막 데이터를 다시 전송한다.
따라서, POST /add + 상품 데이터를 서버로 전송한 후 새로고침을 하게 되면 다시 전송하게 된다.

POST, Redirect GET

PRG3
새로고침 문제를 해결하기 위해 저장 후 뷰 템플릿으로 이동하는 것이 아닌, 상품 상세 화면으로 리다이렉트를 호출해주면 된다.
리다이렉트의 영향으로 상품 저장 후 실제 상품 상세 화면으로 다시 이동한다. 따라서, 마지막에 호출한 내용은 GET/items/{id}가 된다.

BasicItemController에 추가

@PostMapping("/add")
public String addItemV5(Item item) {
        itemRepository.save(item);
        return "redirect:/basic/items/" + item.getId();
}

[주의]
"redirect:/basic/items/" + item.getId()
redirect에서 +item.getId()처럼 URL에 변수를 더해서 사용하는 것은 URL인코딩이 안되기 때문에 좋지않은 방법이다. (RedirectAttributes 사용 권장)


RedirectAttributes

상품 등록(Post)

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}";
}

실행 시 리다이렉트 결과
http://localhost:8080/basic/items/3?status=true

RedirectAttributes
RedirectAttributes를 사용하면 URL인코딩도 해주고, pathVariable, 쿼리 파라미터까지 처리해준다.

  • redirect:/basic/items/{itemId}
    • pathVariable 바인딩 : {itemId}
    • 나머지는 쿼리 파라미터로 처리 : ?status=true

상품 등록(html)

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 : 해당 조건이 참이면 실행
  • ${param.status} : 타임리프에서 쿼리 파라미터를 편리하게 조회하는 기능.
    • 원래는 컨트롤러에서 모델에 직접 담고 값을 꺼내야 하지만 쿼리 파라미터는 자주 사용해서 타임리프에서 직접 지원한다.


<출처 : 인프런 - 스프링 MVC 1편 : 백엔드 웹 개발 핵심 기술(김영한)>

댓글남기기