스프링 mvc - 프로젝트

이주인·2023년 1월 25일
0

스프링 공부

목록 보기
1/11

요구사항 분석

상품 도메인 모델

  • 상품 id
  • 상품명(itemName)
  • 가격(price)
  • 수량(quantity)

상품관리 기능

  • 상품 목록(items)
  • 상품 상세(item)
  • 상품 등록(addForm, addItem)
  • 상품 수성(editForm, edit)
  • 상품 삭제(deleteItem)

상품 객체

package hello.itemService2.domain.item;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
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;
    }


}

상품 저장소


package hello.itemService2.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 {

	//상품의 id와 상품 속성을 저장하는 HashMap
    private static final Map<Long, Item> store = new HashMap<>();
    //id 번호를 자동으로 하나씩 증가
    private static long sequence = 0L;

	//상품 저장기능
    public Item save(Item item){
        item.setId(++sequence);		//id 번호 지정
        store.put(item.getId(), item);	//id 번호와 객체 저장
        return item;
    }

    public Item findById(Long id){
        
        //id번호를 통해 HashMap에서 객체를 찾음
        return store.get(id);	
    }

    public List<Item> findAll(){
    	
        //HashMap의 모든 데이터를 전송
        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 deleteItem(Long itemId){
    	//객체 삭제
        store.remove(itemId);
    }
	
    //테스트를 위한 HAshMap 초기화
    public void clearStore(){
        store.clear();
    }

}

컨트롤러

//기본

@Controller		//컨트롤러임을 알려주는 어노테이션
@RequestMapping("/basic/items")		//모든 컨트롤러 앞에 이 링크가 맵핑된다
//final이 붙은 맴버변수만 사용하여 생성자를 자동 생성
@RequiredArgsConstructor	
public class BasicItemController {

	/*
    @RequiredArgsConstructor 덕분에 생성자를 생성할 필요가 없음.
    대신 final이 필수.
 	생성자가 하나만 있을 경우 @Autowired로 의존관계를 주입
    */
    private final ItemRepository itemRepository;
 
 /**
 * 테스트용 데이터 추가
 */
 	//모든 빈의 의존관계가 모두 주입되고 나면 초기화 용도로 호출
 	@PostConstruct	
 	public void init() {
    itemRepository.save(new Item("testA", 10000, 10));
 	itemRepository.save(new Item("testB", 20000, 20));
 	}
}

상품 목록

상품 목록 html


<!- 상품 목록 타임리프 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">
        	<!- 클릭시 addForm으로 넘어가는 버튼 ->
            <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}">
            
            	<!- 상품상세로 넘어가는 버튼 ->
                <!- 상품id를 url에 넘긴다. ->
                <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}">테스트 상품1</a>
                </td>
                <td th:text="${item.price}">10000</td>
                <td th:text="${item.quantity}">10</td>
            </tr>

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

링크표현식 - @{...}

<td><a href="item.html" th:href="@{/basic/items/{itemId}(itemId=${item.id})}" 
th:text="${item.id}">회원 id</a></td>
  • 경로 변수 {...}를 사용하여 경로를 템플릿처럼 편리하게 사용할 수 있다.
  • 이 경우 상품의 id를 가져와서 itemId 변수에 넣는다.

상품 목록 컨트롤러

  1. 모든 상품을 조회한 후 model에 담는다.
  2. 뷰 템플릿을 호출한다.

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


상품 상세

상품 상세 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">
    <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>

	<!- 모델에 있는 item 정보를 획득하고 프로퍼티 접근법으로 출력한다. ( item.getId() ) ->
    <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 class="col">
            <button class="w-100 btn btn-secondary btn-lg"
                    onclick="location.href='items.html'"
                    th:onclick="|location.href='@{/basic/items/{itemId}/delete(itemId=${item.id})}'|"
                    type="button">삭제
            </button>
        </div>
    </div>
</div> <!-- /container -->
</body>
</html>

속성 변경

th:value="${item.id}"
  • 모델의 item 정보를 획득한 후 프로퍼티 접근법으로 출력한다.
  • ex) (item.getId())

상품 상세 컨트롤러

 	@GetMapping("/{itemId}")
    // 1.
    public String item(@PathVariable long itemId, Model model) {
    	// id를 통해 item을 찾은후 model에 담아 반환
        Item item = itemRepository.findById(itemId);
        model.addAttribute("item", item);
        // 2.
        return "basic/item";
    }
  1. @PathVariable로 넘어온 상품 ID로 상품을 조회하고 모델에 담아준다.
  2. 뷰 템플릿을 호출

@PathVariable

  • url 변수(이 경유 @GetMapping("/{itemId}"))에 들어있는 변수값을 long itemId에 담아 값을 넘겨준다.


상품 삭제

  • 삭제 버튼 HTML
<div class="col">
	<button class="w-100 btn btn-secondary btn-lg"
			onclick="location.href='items.html'"
 			th:onclick="|location.href='@{/basic/items/{itemId}/delete(itemId=${item.id})}'|"
			type="button">삭제
	</button>
</div>

- 상품 삭제 Controller
// 1.
@GetMapping("/{itemId}/delete")
public String deleteItem(@PathVariable Long itemId) {
	itemRepository.deleteItem(itemId);
	// 2.
    return "redirect:/basic/items";
}
  1. get 방식으로 삭제 로직을 호출
  2. 값을 삭제한 뒤 리다이렉트를 통해 상품 목록 페이지로 돌아옴


상품 등록

상품 등록 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">
    <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>
    
    <!- 액션을 따로 지정하지 않아 get방식과 post방식 둘다 호출 가능 ->
    <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">
            
            	<!- 이 버튼이 눌릴 시 post방식으로 /add 가 호출된다. ->
                <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

  • action에 값이 없을 경우 현재 url에 데이터를 전송.
  • 상품 등록 폼의 url과 실제 상품을 등록하는 url을 똑같이 맞추어 HTTP 메소드로 두 기능을 구분.

POST - HTML Form

  • 메시지 바디에 쿼이 파라미터 형식으로 값을 전달
    ex) itemName=itemA&price=10000&quantity=10

상품 등록 Controller

	// 1. 
	@GetMapping("/add")
	public String addForm() {
		return "basic/addForm";
	}
	
    // 2. 
    @PostMapping("/add")
    public String addItemV6(@ModelAttribute Item item, RedirectAttributes redirectAttributes) {
        // 클래스 첫 글자를 소문자로 변환해주는 것이 값에 들어감
		
        // 3, 4
        Item savedItem = itemRepository.save(item);
        redirectAttributes.addAttribute("itemId", savedItem.getId());
        redirectAttributes.addAttribute("status", true);
		
        //상품 상세 페이지로 이동
        return "redirect:/basic/items/{itemId}";
    }
  1. 뷰 템플릿을 호출
  2. 실제 상품을 등록하는 url
  3. 입력한 상품값을 저장한 후 저장된 상품의 id값을 반환
  4. 잘 입력됬다는 상태값을 반환

@ModelAttribute

  • 객체와 객체의 생성자까지 자동 생성(요첨 파라미터 처리)
  • 모델 객체 생성 및 뷰에 넣어줌(model 추가)

RedirectAttributes

  • URL 인코딩도 해주고, pathVarible , 쿼리 파라미터까지 처리해준다
  • th:if : 해당 조건이 참이면 실행
    ${param.status} : 타임리프에서 쿼리 파라미터를 편리하게 조회하는 기능


상품 등록 이전 컨트롤러

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

    //@ModelAttribute 또한 생략 가능
    //@PostMapping("/add")
    public String addItemV3(@ModelAttribute Item item) {
        // 클래스 첫 글자를 소문자로 변환해주는 것이 값에 들어감
        itemRepository.save(item);

        return "basic/item";
    }

    //리다이렉트를 이용하여 get방식으로 다시 호출
    //PRG 패턴
    //@PostMapping("/add")
    public String addItemV5(@ModelAttribute Item item) {
        // 클래스 첫 글자를 소문자로 변환해주는 것이 값에 들어감
        itemRepository.save(item);

        return "redirect:/basic/items/" + item.getId();
    }

v1

  • @RequestParam을 이용하여 객체의 속성값을 일일히 받아온 후, 저장

v2

  • @ModelAttribute를 이용하여 파라미터를 한번에 처리
  • 모델에 데이터를 담을때 이름이 필요하며, 어노테이션에 지정한 name(value) 속성을 사용한다.
  • 이때, @ModelAttribute("value")의 이름과 model.addAttribute("item", item)의 속성 값이 같아야 한다.("item" <- 이거)
  • model.addAttribute("item", item)가 없어도 잘 동작함 -> 객체를 자동으로 생성 후 값을 넣어주기 때문

v3

  • @@ModelAttribute의 이름은 생략이 가능
  • 이때 모델에 저장할 때 클래스명을 사용한다.(클래스의 첫글짜만 소문자로 변경해서 등록)
  • ex) @ModelAttribute Item item 일 경우 item으로 등록

v4(생략됨)

  • @ModelAttribute은 생략할 수 있다.


상품수정

상품 수정 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">
    <style>
        .container {
            max-width: 560px;
        }
    </style>
</head>
<body>
<div class="container">
    <div class="py-5 text-center">
        <h2>상품 수정 폼</h2>
    </div>
	
    <!- th:actiondp 값이 지정되어 있지 않다. ->
    <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>

상품 수정 Controller

	// 1.
    @GetMapping("/{itemId}/edit")
    public String editForm(@PathVariable Long itemId, Model model) {

        Item item = itemRepository.findById(itemId);
        model.addAttribute("item", item);
        return "basic/editForm";
    }

	// 2.
    @PostMapping("/{itemId}/edit")
    public String edit(@PathVariable Long itemId, @ModelAttribute Item item) {
        itemRepository.update(itemId, item);
        
        // 3.
        return "redirect:/basic/items/{itemId}";
    }
  1. 수정에 필요한 정보를 조회하고, 수정용 폼 뷰를 호출한다
  2. 메시지 바디에 쿼리 파라미터 형식으로 값을 전달
  3. 리다이렉트를 사용하여 뷰템플릿을 호출하는 대신 상품 상세 화면으로 이동


PRG Post/Redirect/Get

웹 브라우저의 새로 고침은 마지막에 서버에 전송한 데이터를 다시 전송한다.
상품 등록폼에세 데이터를 입력한 후 저장한 뒤, 새로고침을 할 경우 id만 다른 상품 데이터가 계속 싸우게 된다.

이때 상품 저장후 뷰 템플릿으로 이동하는 것이 아닌 상품 상세 화면으로 리다이렉트 화면을 호출해주면 새로고침 문제를 해결할 수 있다.

PRG패턴 : POST 처리 이후에 뷰 템플릿이 아니라 특정 화면으로 리다이렉트 하도록 코드를 작성하는 것.

profile
컴공

0개의 댓글