반응형

# 스프링 MVC 웹 페이지 만들기

## 프로젝트 생성.

  • Dependencies : Spring Web, Thymeleaf, Lombok
  • GENERATE 클릭 후 압축 해제 진행. 인텔리제이에서 File > open 클릭 후 압축해제한 폴더 내 build.gradle 선택하여 파일 열기.
  • Lombok : File > Settings > annotation processors 입력 후 Enable annotation processing 체크.
  • Gradle : File > Settings > Gradle 검색 후 Gradle에서 아래와 같이 설정.
Build and run using : IntelliJ IDEA
Run tests using : IntelliJ IDEA
  • 전체적인 설정 완료 후 main 실행해서 정상 작동하는지 확인.
  • Port 충돌날 경우 Run > Edit Configurations > Environment variables 에 아래와 같이 입력하여 Port 변경.
server.port=8082
 
  • 웰컴 페이지 추가 : resources > static에 index.html 추가.
<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<ul>
    <li>상품 관리
        <ul>
            <li><a href="/basic/items">상품 관리 - 기본</a></li>
        </ul>
    </li>
</ul>
</body>
</html>

## 요구사항 분석

  • 상품 도메인 모델 : 상품 ID, 상품명, 가격, 수량,
  • 상품 관리 기능 : 상품 목록, 상품 상세, 상품 등록, 상품 수정
  • 필요 화면 : 상품 목록, 상품 상세, 상품 등록 폼, 상품 수정 폼

서비스 제공 흐름

## 상품 도메인 개발

item 상품 객체 생성.

package hello.itemservice.domain.item;

import lombok.Data;
import lombok.Getter;
import lombok.Setter;

//@Getter @Setter
@Data
public class Item {

    private Long id;
    private String itemName;
    private Integer price;
    private Integer quantity;

    public Item() {
    }

    public Item(String itemName, Integer price, Integer quantity) {
        this.itemName = itemName;
        this.price = price;
        this.quantity = quantity;
    }
}
  • @Data 의 경우 주의해서 사용 가능하면 @Getter, @Setter 사용.

ItemRepository 상품 저장소.

package hello.itemservice.domain.item;

import org.springframework.stereotype.Repository;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Repository
public class ItemRepository {

    // 실무에서는 동시 접근과 관련하여 HashMap 보다는 ConcurrentHashMap사용.
    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();
    }
}

ItemRepositoryTest.

package hello.itemservice.domain.item;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.AfterEach;
import org.junit.jupiter.api.Test;

import java.util.List;

import static org.assertj.core.api.Assertions.assertThat;

class ItemRepositoryTest {

    ItemRepository itemRepository = new ItemRepository();

    @AfterEach
    void afterEach() {
        itemRepository.clearStore();
    }

    @Test
    void save() {
        // given
        Item item = new Item("itemA", 10000, 10);

        // when
        Item saveItem = itemRepository.save(item);

        // then
        Item findItem = itemRepository.findById(item.getId());
        assertThat(findItem).isEqualTo(saveItem);
    }

    @Test
    void findAll() {
        // given
        Item item1 = new Item("item1", 10000, 10);
        Item item2 = new Item("item2", 20000, 50);
        itemRepository.save(item1);
        itemRepository.save(item2);

        // when
        List<Item> result = itemRepository.findAll();

        // then
        assertThat(result.size()).isEqualTo(2);
        assertThat(result).contains(item1, item2);
    }

    @Test
    void updateItem() {
        // given
        Item item = new Item("item1", 10000, 10);

        Item savedItem = itemRepository.save(item);
        Long itemId = savedItem.getId();

        // when
        Item updateParam = new Item("item2", 20000, 30);
        itemRepository.update(itemId, updateParam);

        // then
        Item findItem = itemRepository.findById(itemId);
        assertThat(findItem.getItemName()).isEqualTo(updateParam.getItemName());
        assertThat(findItem.getPrice()).isEqualTo(updateParam.getPrice());
        assertThat(findItem.getQuantity()).isEqualTo(updateParam.getQuantity());
    }
}

## 상품 서비스 HTML

부트스트랩 

 

Bootstrap

The most popular HTML, CSS, and JS library in the world.

getbootstrap.com

 

Download

Download Bootstrap to get the compiled CSS and JavaScript, source code, or include it with your favorite package managers like npm, RubyGems, and more.

getbootstrap.com

이동: https://getbootstrap.com/docs/5.0/getting-started/download/
Compiled CSS and JS 항목을 다운로드.

압축 출고 bootstrap.min.css 를 복사해 아래 폴더에 추가.
resources/static/css/bootstrap.min.css

HTML, CSS

  • /resources/static/css/bootstrap.min.css 부트스트랩 다운로드해서 추가.
  • /resources/static/html/items.html
<!DOCTYPE HTML>
<html>
<head>
    <meta charset="utf-8">
    <link 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'" 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>
                <td><a href="item.html">1</a></td>
                <td><a href="item.html">테스트 상품1</a></td>
                <td>10000</td>
                <td>10</td>
            </tr>
            <tr>
                <td><a href="item.html">2</a></td>
                <td><a href="item.html">테스트 상품2</a></td>
                <td>20000</td>
                <td>20</td>
            </tr>
            </tbody>
        </table>
    </div>
</div> <!-- /container -->
</body>
</html>
  • /resources/static/html/item.html
<!DOCTYPE HTML>
<html>
<head>
    <meta charset="utf-8">
    <link 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" readonly>
    </div>
    <div>
        <label for="itemName">상품명</label>
        <input type="text" id="itemName" name="itemName" class="form-control"
               value="상품A" readonly>
    </div>
    <div>
        <label for="price">가격</label>
        <input type="text" id="price" name="price" class="form-control"
               value="10000" readonly>
    </div>
    <div>
        <label for="quantity">수량</label>
        <input type="text" id="quantity" name="quantity" class="form-control"
               value="10" 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'" type="button">상품 수정</button>
        </div>
        <div class="col">
            <button class="w-100 btn btn-secondary btn-lg"
                    onclick="location.href='items.html'" type="button">목록으로</button>
        </div>
    </div>
</div> <!-- /container -->
</body>
</html>
  • /resources/static/html/addForm.html
<!DOCTYPE HTML>
<html>
<head>
    <meta charset="utf-8">
    <link 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" method="post">
        <div>
            <label for="itemName">상품명</label>
            <input type="text" id="itemName" name="itemName" class="formcontrol" 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="formcontrol" 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'" type="button">취소</button>
            </div>
        </div>
    </form>
</div> <!-- /container -->
</body>
</html>
  • /resources/static/html/editForm.html
<!DOCTYPE HTML>
<html>
<head>
    <meta charset="utf-8">
    <link 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" method="post">
        <div>
            <label for="id">상품 ID</label>
            <input type="text" id="id" name="id" class="form-control" value="1"
                   readonly>
        </div>
        <div>
            <label for="itemName">상품명</label>
            <input type="text" id="itemName" name="itemName" class="formcontrol" value="상품A">
        </div>
        <div>
            <label for="price">가격</label>
            <input type="text" id="price" name="price" class="form-control"
                   value="10000">
        </div>
        <div>
            <label for="quantity">수량</label>
            <input type="text" id="quantity" name="quantity" class="formcontrol" value="10">
        </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'" type="button">취소</button>
            </div>
        </div>
    </form>
</div> <!-- /container -->
</body>
</html>

## 상품목록_타임리프 사용

  • 컨트롤러와 뷰 템플릿 개발.

BasicItemController

package hello.itemservice.web.basic;

import hello.itemservice.domain.item.Item;
import hello.itemservice.domain.item.ItemRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.annotation.PostConstruct;
import java.util.List;

@Controller
@RequestMapping("/basic/items")
@RequiredArgsConstructor
public class BasicItemController {

    private final ItemRepository itemRepository;

    @GetMapping
    public String items(Model model) {
        List<Item> items = itemRepository.findAll();
        model.addAttribute("items", items);
        return "basic/items";
    }

    /**
     * 테스트용 데이터 추가.
     * */
    @PostConstruct
    public void init() {
        itemRepository.save(new Item("itemA", 10000, 10));
        itemRepository.save(new Item("itemB", 20000, 20));
    }
}
  • @RequiredArgsConstructor : final 붙은 멤버변수만 사용, 생성자를 자동으로 만들어준다.

items.html

  • 아래 경로에 추가.
/resources/templates/basic/items.html
  • 속성 변경 : 기존 css href를 th:href로 변경.
<link th:href="@{/css/bootstrap.min.css}"
        href="../css/bootstrap.min.css" rel="stylesheet">
  • 속성변경 : 기존 상품 등록 폼 이동 onclick 변경
<button class="btn btn-primary float-end"
        onclick="location.href='addForm.html'"
        th:onclick="|location.href='@{/basic/items/add}'|"
        type="button">상품
    등록</button>
  •  반복 출력 : th:each 사용. 
<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/{itemId}(itemId=${item.id})}" th:text="${item.itemName}">상품명</a></td>
    <td th:text="${item.price}">10000</td>
    <td th:text="${item.quantity}">10</td>
</tr>
<!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">
</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>

타임리프

  • 타임리프 사용 선언 : 타임리프 사용을 위해 아래와 같이 선언.
<html xmlns:th="http://www.thymeleaf.org">

속성 변경 : th:href

  • href="value1"을 th:href="value2"의 값으로 변경.
  • 타임리프 뷰 템플릿을 거치면서 원래 값을 th:xxx 로 변경한다. 만약 값이 없으면 새로 생성한다.
<link th:href="@{/css/bootstrap.min.css}"
        href="../css/bootstrap.min.css" rel="stylesheet">

속성 변경 : th:onclick

<button class="btn btn-primary float-end"
        onclick="location.href='addForm.html'"
        th:onclick="|location.href='@{/basic/items/add}'|"
        type="button">상품
    등록</button>

타임리프의 핵심

  • th:xxx가 붙은 부분은 서버사이드에서랜더링 되고, 기존 것을 대체한다. th:xxx가 없으면 기존 html의 xxx 속성이 그대로 사용된다.
  • HTML파일을 직접 열었을 때, th:xxx가 있어도 웹 브라우저는 th: 속성을 알지 못하므로 무시. (HTML 파일 보기를 유지하면서도 템플릿 기능도 할 수 있다.)

URL 링크 표현식 : @{ ... }

  • @{ } : 타임리프는 URL 링크를 사용하는 경우 @{ } 를 사용. (=URL 링크 표현식)
  • URL 링크 표현식을 사용하면 서블릿 컨텍스트를 자동으로 포함.
th:href="@{/css/bootstrap.min.css}"

URL 링크 표현식2 : @ { ... }

  • 상품 id 선택 시 링크.
<td><a href="item.html" th:href="@{/basic/items/{itemId}(itemId=${item.id})}" th:text="${item.id}">회원ID</a></td>
  • URL 경로 표현식을 이용하여 경로를 템플릿처럼 편리하게 사용.
  • 경로 변수 ( {item.id} ) 뿐만 아니라 쿼리 파라미터도 생성한다.
th:href="@{/basic/items/{itemId}(itemId=${item.id}, query='test')}" th:text="${item.id}"

=>
생성링크 : http://localhost:8080/basic/items/1?query=test

리터럴 대체 : | ... |

  • 타임리프에서 문자와 표현식 등은 분리되어 있기 때문에 더해서 사용해야 함.
<span th:text="'Welcome to our apllication, ' + ${user.name} + '!'">
  • 아래와 같이 리터럴 대체 문법 사용 시 더하기 없이 편리하게 사용 가능.
<span th:text="|Welcome to our apllication, ${user.name} !|">

반복 출력 : th:each

<tr th:each="item : ${items}">
  • 반복은 th:each 사용, 이렇게 하면 모델에 포함된 items 컬렉션 데이터가 item 변수에 하나씩 포함, 반복문 안에서 item 변수 사용 가능.
  • 컬렉션의 수 만큼 <tr> .. <tr>이 하위 태그를 포함해서 생성.
<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/{itemId}(itemId=${item.id})}" th:text="${item.itemName}">상품명</a></td>
    <td th:text="${item.price}">10000</td>
    <td th:text="${item.quantity}">10</td>
</tr>

변수 표현식 : ${ ... }

<td th:text="${item.price}">10000</td>
  • 모델에 포함된 값이나, 타임리프 변수로 선언한 값을 조회.
  • 프로퍼티 접근법을 사용 ( item.getPrice() )

내용 변경 : th:text

<td th:text="${item.price}">10000</td>
  • 내용의 값을 th:text의 값으로 변경. 위 코드에서는 10000을 ${item.price} 값으로 변경.

##  상품 상세

BasicItemController : item 추가.

package hello.itemservice.web.basic;

import hello.itemservice.domain.item.Item;
import hello.itemservice.domain.item.ItemRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.annotation.PostConstruct;
import java.util.List;

@Controller
@RequestMapping("/basic/items")
@RequiredArgsConstructor
public class BasicItemController {

    private final ItemRepository itemRepository;

    @GetMapping
    public String items(Model model) {
        List<Item> items = itemRepository.findAll();
        model.addAttribute("items", items);
        return "basic/items";
    }

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

    /**
     * 테스트용 데이터 추가.
     * */
    @PostConstruct
    public void init() {
        itemRepository.save(new Item("itemA", 10000, 10));
        itemRepository.save(new Item("itemB", 20000, 20));
    }
}

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

## 상품 등록 폼

BasicItemController : addForm 추가

package hello.itemservice.web.basic;

import hello.itemservice.domain.item.Item;
import hello.itemservice.domain.item.ItemRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;

import javax.annotation.PostConstruct;
import java.util.List;

@Controller
@RequestMapping("/basic/items")
@RequiredArgsConstructor
public class BasicItemController {

    private final ItemRepository itemRepository;

    @GetMapping
    public String items(Model model) {
        List<Item> items = itemRepository.findAll();
        model.addAttribute("items", items);
        return "basic/items";
    }

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

    @GetMapping("/add")
    public String addForm() {
        return "basic/addForm";
    }

    /**
     * 테스트용 데이터 추가.
     * */
    @PostConstruct
    public void init() {
        itemRepository.save(new Item("itemA", 10000, 10));
        itemRepository.save(new Item("itemB", 20000, 20));
    }
}

addForm.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>
    <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="formcontrol" 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="formcontrol" 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>

속성 변경 : th:action

  • HTML form에서 action 에 값이 없으면 현재 URL에 데이터를 전송한다.
  • 상품 등록 폼의 URL과 실제 상품 등록을 처리하는 URL을 동일하게 맞추고 HTTP 메서드로 두 기능을 구분.
상품 등록 폼 : GET /basic/items/add

상품 등록 처리 : POST /basic/items/add
  • 이렇게 하면 하나의 URL로 등록 폼과, 등록 처리를 깔끔하게 처리할 수 있다.

## 상품 등록 처리_@ModelAttribute

  • POST - HTML Form
content-type: application/x-www-form-urlencoded

메시지 바디에 쿼리 파리미터 형식으로 전달 itemName=itemA&price=10000&quantity=10

예) 회원 가입, 상품 주문, HTML Form 사용
  • 요청 파라미터 형식을 처리해야 하므로 @RequestParam 사용.

상품 등록 처리_@RequestParam

addItemV1 - 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);

    return "basic/item";
}

addItemV2 - BasicItemController에 추가

@PostMapping("/add")
    public String addItemV2(@ModelAttribute("item") Item item) {

        itemRepository.save(item);
//        model.addAttribute("item", item);     // 자동 추가, 생략 가능

        return "basic/item";
    }

@ModelAttribute 중요 기능

1. 요청 파라미터 처리

  • @ModelAttribute 는 Item 객체를 생성, 요청 파라미터의 값을 프로퍼티 접근법(setXxx)으로 입력해준다.

2. Model 추가

  • 모델(Model)에 @ModelAttribute 로 지정한 객체를 자동으로 넣어준다.
  • 위 코드에서 model.addAttribute("item", item) 가 주석처리 되어 있어도 잘 동작.
  • 모델에 데이터를 담을 때는 이름이 필요. 이름은 @ModelAttribute 에 지정한 name(value) 속성을 사용.
  • 만약 다음과 같이 @ModelAttribute 의 이름을 다르게 지정하면 다른 이름으로 모델에 포함된다.
  • @ModelAttribute("hello") Item item -> 이름을 hello 로 지정
  • model.addAttribute("hello", item); -> 모델에 hello 이름으로 저장

addItemV3 - BasicItemController에 추가

@PostMapping("/add")
    public String addItemV3(@ModelAttribute Item item) {
        // @ModelAttribute("item")에서 ("item") 생략 시 Item -> item이 modelAttribute 담기게 됨.
        itemRepository.save(item);
//        model.addAttribute("item", item);     // 자동 추가, 생략 가능

        return "basic/item";
    }
  • @ModelAttribute 의 이름을 생략할 수 있다. (@ModelAttribute 의 이름을 생략 시 모델에 저장될 때 클래스명을 사용. 이때 클래스의 첫글자만 소문자로 변경해서 등록한다.)
@ModelAttribute 클래스명 모델에 자동 추가되는 이름
Item -> item
HelloWorld -> helloWorld

addItemV4 - BasicItemController에 추가

@PostMapping("/add")
    public String addItemV4(Item item) {
        itemRepository.save(item);
//        model.addAttribute("item", item);     // 자동 추가, 생략 가능
        return "basic/item";
    }
  • @ModelAttribute 자체도 생략가능.
  • 대상 객체는 모델에 자동 등록.
  • 나머지 사항은 기존과 동일하다.

## 상품 수정.

BasicItemController : editForm 추가.

@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 /items/{itemId}/edit : 상품 수정 폼
POST /items/{itemId}/edit : 상품 수정 처리

리다이렉트

  • 상품 수정은 마지막에 뷰 템플릿을 호출하는 대신 상품 상세 화면으로 이동하도록 리다이렉트를 호출.
  • 스프링은 redirect:/... 으로 편리하게 리다이렉트를 지원.
redirect:/basic/items/{itemId}" 컨트롤러에 매핑된 @PathVariable 의 값은 redirect 에서도 사용 가능.

redirect:/basic/items/{itemId} {itemId} 는 @PathVariable Long itemId 의 값을 그대로 사용.

editForm.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>
    <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="formcontrol" value="상품A" th:value="${item.itemName}">
        </div>
        <div>
            <label for="price">가격</label>
            <input type="text" id="price" name="price" class="form-control" value="10000" th:value="${item.price}">
        </div>
        <div>
            <label for="quantity">수량</label>
            <input type="text" id="quantity" name="quantity" class="formcontrol" value="10" 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>

## PRG : Post / Redirect / Get

  • 상품 등록 처리 컨트롤러는 심각한 문제 존재. (addItemV1 ~ addItemV4, 상품 등록을 완료하고 웹 브라우저의 새로고침 버튼을 클릭 시 상품이 계속해서 중복 등록.)
  • 웹 브라우저 새로고침 : 마지막에 서버에 전송한 데이터를 다시 전송 (마지막 행위를 다시 실행(데이터 까지 포함하여))
  • 새로고침 해결방법 : 리다이렉트

BasicItemController : addItemV5 추가.

    @PostMapping("/add")
    public String addItemV5(Item item) {
        itemRepository.save(item);
//        model.addAttribute("item", item);     // 자동 추가, 생략 가능
        return "redirect:/basic/items/" + item.getId();
    }

주의할점

  • "redirect:/basic/items/" + item.getId() redirect에서 +item.getId() 처럼 URL에 변수를 더해서 사용하는 것은 URL 인코딩이 안되기 때문에 위험. RedirectAttributes 를 사용.

## RedirectAttributes

  • 상품 저장이 잘 되었으면 상품 상세 화면에 "저장되었습니다" 라는 메시지를 보여달라는 요구사항.

BasicItemController : addItemV6 추가

@PostMapping("/add")
public String addItemV6(Item item, RedirectAttributes redirectAttributes) {
    Item saveItem = itemRepository.save(item);
    redirectAttributes.addAttribute("itemId", saveItem.getId());
    redirectAttributes.addAttribute("status", true);
    return "redirect:/basic/items/{itemId}";
}

item.html 수정

<!-- 추가 -->
<h2 th:if="${param.status}" th:text="'저장 완료'"></h2>
  • th:if : 해당 조건이 참이면 실행
  • ${param.status} : 타임리프에서 쿼리 파라미터를 편리하게 조회하는 기능, 원래는 컨트롤러에서 모델에 직접 담고 값을 꺼내야 하는데, 쿼리 파라미터는 자주 사용해서 타임리프에서 직접 지원.

## 정리

  1. 프로젝트 생성.
  2. 타임리프 사용.
  3. @ModelAttribute
  4. 리다이렉트.
  5. PRG Post/ Redirect / Get 패턴 : 등록 시 리다이렉트 이용하여 중복등록 방지.
  6. RedirectAttributes
반응형

+ Recent posts