[Spring MVC] [1] 7. 스프링 MVC - 웹 페이지 만들기_2

윤경·2021년 9월 13일
0

Spring MVC

목록 보기
12/26
post-thumbnail

[6] 상품 상세

제품 상세 컨트롤러와 뷰 개발하기

✔️ BasicItemController

items 밑에 코드 추가

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

PathVariable로 넘어온 상품 ID로 상품을 조회하고 모델에 담은 후 뷰 템플릿을 호출

✔️ item

(마찬가지로 복붙해온 코드 수정)

<!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>

    <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"
                    onclick="location.href='items.html'"
                    th:onclick="|location.href='@{/basic/items}'|"
                    type="button">목록으로</button>
        </div>
    </div>

</div> <!-- /container -->

</body>
</html>

하,, 선생님 코드를 200% 믿지 말자,, 복붙했다가 왜 이대로 안 나오나 했다,, 직접 수정,,,,

[7] 상품 등록 폼

✔️ BasicItemController

코드 추가

    @GetMapping("/add")	// 데이터를 저장하는 것이 아니라 보여만 줌
    public String addForm() {
        return  "basic/addForm";
    }

✔️ addForm

복붙 후 수정

<!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"
                        onclick="location.href='items.html'"
                        th:onclick="|location.href='@{/basic/items}'|"
                        type="button">취소</button>
            </div>
        </div>

    </form>

</div> <!-- /container -->
</body>
</html>

form을 열 때는 Get, 저장시킬 때는 Post
하나의 URL로 등록 폼, 등록 처리를 수행

[8] 상품 등록 처리 - @ModelAttribute

상품 등록 폼에서 전달된 데이터로 실제 상품을 등록처리 하기.

상품 등록 폼은 POST - HTML Form 방식으로 서버에 데이터를 전달.

  • content-type: application/x-www-form-urlencoded
  • 메시지 바디에 쿼리 파라미터 형식으로 전달

요청 파라미터 형식을 처리해야 하므로 @RequestParam 사용

✔️ BasicItemController에 코드 추가

    @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);   // model에 데이터를 담음

        return  "basic/item";
    }

@RequestParam String itemName: itemName 요청 파라미터 데이터를 해당 변수에 받음
Item 객체를 생성 → itemRepository 통해 저장 → item을 모델에 담아 뷰에 전달

⭐️ 여기서는 제품 상세에 사용한 item.html 뷰 템플릿을 그대로 재활용

v2 코드 추가

    @PostMapping("/add")
    public String addItemV2(@ModelAttribute("item") Item item, Model model) {
        itemRepository.save(item);
        // model.addAttribute("item", item) // 자동 추가, 생략 가능
        return "basic/item";
    }

@RequestParam으로 변수를 하나하나 받아 Item을 생성하는 과정은 불편 ➡️ @ModelAttribute를 사용해 한 번에 처리하기

@ModelAttribute - 요청 파라미터 처리
: 아이템 생성, 요청 파라미터의 값을 프로퍼티 접근법으로 입력

@ModelAttribute - Model 추가
: @ModelAttribute바로 Model에 @ModelAttribute로 지정한 객체를 자동으로 넣어준다는 중요한 기능이 있음.
(주석 처리 해놓은 코드가 없어도 잘 동작한다는 것)

모델에 데이터를 담을 때 이름이 필요한데 @ModelAttribute에 지정한 name(value) 속성을 사용하며 @ModelAttribute의 이름을 다르게 지정하면 다른 이름으로 모델에 포함된다.

Ex.

v3 코드 추가

    @PostMapping("/add")
    public String addItemV3(@ModelAttribute Item item) {
        itemRepository.save(item);
        return "basic/item";
    }

@ModelAttribute의 이름 생략 가능!
➡️ 이렇게 이름을 생략하면 모델에 저장될 때 클래스명을 사용한다. 클래스의 첫 글자만 소문자로 바꿔 사용

v4 코드 추가

    @PostMapping("/add")
    public String addItemV4(Item item) {
        itemRepository.save(item);
        return "basic/item";
    }

@ModelAttribute 자체도 생략 가능!
대상 객체는 모델에 자동 등록되며 나머지 사항은 기존과 동일.

⭐️ 주의사항

동작시키고 싶은 버전을 제외한 @PostMapping을 주석처리 해주기 (중복 매핑으로 인한 오류 발생 방지)


[9] 상품 수정

✔️ BasicItemController에 코드 추가

: 수정에 필요한 정보를 조회, 수정용 폼 뷰를 호출

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

GET으로 부르냐, POST로 부르냐에 따라 GET-상품 수정 폼, POST-상품 수정 처리 어떤게 처리되는지가 갈린다.

📌 리다이렉트

뷰 템플릿을 호출하는 대신 상품 상세 화면으로 리다이렉트 되도록 호출.

스프링은 redirect:/... 으로 편리하게 리다이렉트 지원

✔️ basic/editForm.html

editForm 복붙해와서 코드 수정

<!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">
    <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" 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>

        <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"
                        onclick="location.href='item.html'"
                        th:onclick="|location.href='@{/basic/items/{itemId}(itemId=${item.id})}'|"
                        type="button">취소</button>
            </div>
        </div>

    </form>

</div> <!-- /container -->
</body>
</html>

📌
HTML Form 전송은 PUT, PATCH 기능 지원 X
GET, POST만 사용 가능

PUT, PATCH는 HTTP API전송시에 사용하며 히든 필드를 통해 이 두 매핑을 사용할 순 있으나 HTTP 요청상 겉보기에 어차피 POST 요청이다.


[10] PRG Post/Redirect/Get

addItemV1~4까지 상품을 등록하고 새로고침하면 계속 새로운 아이템으로 등록되는 문제가 발생한다.

전체 흐름

POST 후 새로고침

POST, Redirect GET

이와 같이 상품 저장 후 뷰 템플릿이 아닌 상품 상세 화면으로 리다이렉트를 호출하게 한다.

마지막으로 호출한 것이 POST가 아닌 GET이기 때문에 새로고침을 해도 아무 문제가 발생하지 않는다.

✔️ 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를 사용하자.


[11] RedirectAttributes

리다이렉트만 한 것은 사용자 친화적이지 못했다. (고객 입장에서 저장이 잘 된건지 알 수 없음)

이제 저장이 잘 됐으면 잘 됐다! 고 사용자에게 알려주자.

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

status=true를 추가해 이 값이 있다면 저장되었습니다를 뱉을 수 있도록 하자.

RedirectAttributes

: URL 인코딩, PathVariable, 쿼리 파라미터를 처리해준다.

✔️ 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"
                    onclick="location.href='items.html'"
                    th:onclick="|location.href='@{/basic/items}'|"
                    type="button">목록으로</button>
        </div>
    </div>

</div> <!-- /container -->

</body>
</html>
  • th:if: 해당 조건 참이면 실행
  • ${param.status}: 타임리프에서 쿼리 파라미터를 편리하게 조회하는 기능
    (원래는 컨트롤러에서 모델에 직접 담고 값을 꺼내야 하지만 쿼리 파라미터는 자주 사용하기 때문에 타임리프에서 직접 지원)

profile
개발 바보 이사 중

0개의 댓글