카테고리 없음

[스프링부트] 실전! 스프링 부트와 JPA 활용1 #7 웹 계층 개발(2)

aSpring 2023. 11. 11. 21:44
728x90
728x90
※ 본 포스팅은 김영한 강사님의 인프런 '실전! 스프링 부트와 JPA 활용1 - 웹 애플리케이션 개발' 강의를 들으며 작성한 수강일지 입니다.

 

| 웹 계층 개발

1. 홈 화면과 레이아웃
2. 회원 등록
3. 회원 목록 조회
4. 상품 등록
5. 상품 목록
6. 상품 수정
7. 변경 감지와 병합(merge)
8. 상품 주문
9. 주문 목록으로 검색, 취소
10. 다음으로

 

2. 회원 등록

- 회원 가입 클릭 시 members/new로 이동하도록 되어있음

 

문제발생!

- MemberForm class @NotEmpty annotation

2.3 이상부터는 직접 dependency를 걸어주어야 한다고 한다.

build.gradle 파일 dependencies부분에 추가해주기

implementation 'org.springframework.boot:spring-boot-starter-validation'

--> 해주어도 나는 안돼서 진행이 불가능..

--> 문의 남겨 두었으니 후에 확인 필요...

 

resources/templates/members/createMemberForm.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header" />
<style>
    .fieldError {
        border-color: #bd2130;
    }
</style>
<body>
<div class="container">
    <div th:replace="fragments/bodyHeader :: bodyHeader"/>
    <form role="form" action="/members/new" th:object="${memberForm}"
          method="post">
        <div class="form-group">
            <label th:for="name">이름</label>
            <!--  th:filed 해주면 id="name" name="name"을 안적어 주어도 됨  -->
            <input type="text" th:field="*{name}" class="form-control" placeholder="이름을 입력하세요" th:class="${#fields.hasErrors('name')}? 'form-controlfieldError' : 'form-control'">
            <p th:if="${#fields.hasErrors('name')}" th:errors="*{name}">Incorrect date</p>
        </div>
        <div class="form-group">
            <label th:for="city">도시</label>
            <input type="text" th:field="*{city}" class="form-control" placeholder="도시를 입력하세요">
        </div>
        <div class="form-group">
            <label th:for="street">거리</label>
            <input type="text" th:field="*{street}" class="form-control" placeholder="거리를 입력하세요">
        </div>
        <div class="form-group">
            <label th:for="zipcode">우편번호</label>
            <input type="text" th:field="*{zipcode}" class="form-control" placeholder="우편번호를 입력하세요">
        </div>
        <button type="submit" class="btn btn-primary">Submit</button>
    </form>
    <br/>
    <div th:replace="fragments/footer :: footer" />
</div> <!-- /container -->
</body>
</html>

 

controller/MemberForm

package jpabook.jpashop.controller;

import lombok.Getter;
import lombok.Setter;

@Getter @Setter
public class MemberForm {

//    @NotEmpty(message = "회원 이름은 필수 입니다") // Error 11.11 질문 남겨두었으니 확인 후 작업
    private String name;

    private String city;
    private String street;
    private String zipcode;
}

 

controller/MemberController

package jpabook.jpashop.controller;

import jpabook.jpashop.domain.Address;
import jpabook.jpashop.domain.Member;
import jpabook.jpashop.service.MemberService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService; // controller는 service를 갖다 씀

    @GetMapping("/members/new")
    public String createForm(Model model) {
        model.addAttribute("memberForm", new MemberForm()); // 화면에서 이 객체에 접근 할 수 있게 됨
        return "members/createMemberForm";
    }

    @PostMapping("/members/new")
    //    @Valid
    public String create(MemberForm form) {

        Address address = new Address(form.getCity(), form.getStreet(), form.getZipcode());

        Member member = new Member();
        member.setName(form.getName());
        member.setAddress(address);

        memberService.join(member); // 저장됨
        return "redirect:/"; // redirect로 home에 보냄
    }
}

 

validation 에러 처리 테스트 불가능하므로 넘어감

 

- 화면에 fit한 form data를 만들고 그걸로 data를 받는 것이 나음


3. 회원 목록 조회

MemberContoller

@GetMapping("/members")
public String list(Model model) {
    List<Member> members = memberService.findMembers();
    model.addAttribute("members", members);
    return "members/memberList";
}

 

  • 조회한 상품을 뷰에 전달하기 위해 스프링 MVC가 제공하는 모델(Model) 객체에 보관
  • 실행할 뷰 이름을 반환

 

memberList.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header" />
<body>
<div class="container">
  <div th:replace="fragments/bodyHeader :: bodyHeader" />
  <div>
    <table class="table table-striped">
      <thead>
      <tr>
        <th>#</th>
        <th>이름</th>
        <th>도시</th>
        <th>주소</th>
        <th>우편번호</th>
      </tr>
      </thead>
      <tbody>
      <tr th:each="member : ${members}">
        <td th:text="${member.id}"></td>
        <td th:text="${member.name}"></td>
        <td th:text="${member.address?.city}"></td>
        <td th:text="${member.address?.street}"></td>
        <td th:text="${member.address?.zipcode}"></td>
      </tr>
      </tbody>
    </table>
  </div>
  <div th:replace="fragments/footer :: footer" />
</div> <!-- /container -->
</body>
</html>

 

참고: 타임리프에서 ?를 사용하면 null 을 무시한다.
참고: 폼 객체 vs 엔티티 직접 사용
참고: 요구사항이 정말 단순할 때는 폼 객체( MemberForm ) 없이 엔티티( Member )를 직접 등록과 수정 화면 에서 사용해도 된다. 하지만 화면 요구사항이 복잡해지기 시작하면, 엔티티에 화면을 처리하기 위한 기능이 점점 증가한다. 결과적으로 엔티티는 점점 화면에 종속적으로 변하고, 이렇게 화면 기능 때문에 지저분해진 엔티티는 결국 유지보수하기 어려워진다.
실무에서 엔티티는 핵심 비즈니스 로직만 가지고 있고, 화면을 위한 로직은 없어야 한다. 화면이나 API에 맞 는 폼 객체나 DTO를 사용하자. 그래서 화면이나 API 요구사항을 이것들로 처리하고, 엔티티는 최대한 순수 하게 유지하자.

 

 

※ API를 만들 때는 절대 외부로 Entity를 반환하면 안 됨

반환 시 userpassword 등이 그대로 노출되며 API의 스펙이 변경이 되어 불안정해짐

 

template engine에서는 선택적으로 사용


4. 상품 등록

상품 등록

controller/BookForm

package jpabook.jpashop.controller;

import lombok.Getter;
import lombok.Setter;

@Getter @Setter
public class BookForm {
    
    private Long id;
    
    private String name;
    private int price;
    private int stockQuantity;
    
    private String author;
    private String isbn;
}

 

ItemController

package jpabook.jpashop.controller;

import jpabook.jpashop.domain.item.Book;
import jpabook.jpashop.service.ItemService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

@Controller
@RequiredArgsConstructor
public class ItemController {

    private final ItemService itemService;

    @GetMapping("/items/new")
    public String createForm(Model model) {
        model.addAttribute("form", new BookForm());
        return "items/createItemForm";
    }

    @PostMapping("/items/new")
    public String create(BookForm form) {
        Book book = new Book();
        book.setName(form.getName());
        book.setPrice(form.getPrice());
        book.setStockQuantity(form.getStockQuantity());
        book.setAuthor(form.getAuthor());
        book.setIsbn(form.getIsbn());

        itemService.saveItem(book);
        return "redirect:/";
    }
}

상품 등록 폼에서 데이터를 입력하고 Submit 버튼을 클릭하면 /items/new 를 POST 방식으로 요청 상품 저장이 끝나면 상품 목록 화면( redirect:/items )으로 리다이렉트 -> 현재 상품 목록 화면 없으므로 그냥 / 으로 리다이렉트

 

items/createItemForm.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header" />
<body>
<div class="container">
  <div th:replace="fragments/bodyHeader :: bodyHeader"/>
  <form th:action="@{/items/new}" th:object="${form}" method="post">
    <div class="form-group">
      <label th:for="name">상품명</label>
      <input type="text" th:field="*{name}" class="form-control"
             placeholder="이름을 입력하세요">
    </div>
    <div class="form-group">
      <label th:for="price">가격</label>
      <input type="number" th:field="*{price}" class="form-control"
             placeholder="가격을 입력하세요">
    </div>
    <div class="form-group">
      <label th:for="stockQuantity">수량</label>
      <input type="number" th:field="*{stockQuantity}" class="form-control" placeholder="수량을 입력하세요">
    </div>
    <div class="form-group">
      <label th:for="author">저자</label>
      <input type="text" th:field="*{author}" class="form-control"
             placeholder="저자를 입력하세요">
    </div>
    <div class="form-group">
      <label th:for="isbn">ISBN</label>
      <input type="text" th:field="*{isbn}" class="form-control"
             placeholder="ISBN을 입력하세요">
    </div>
    <button type="submit" class="btn btn-primary">Submit</button>
  </form>
  <br/>
  <div th:replace="fragments/footer :: footer" />
</div> <!-- /container -->
</body>
</html>


5. 상품 목록

ItemController

@GetMapping("/items")
public String list(Model model) {
    List<Item> items = itemService.findItems();
    model.addAttribute("items", items);
    return "items/itemList";
}

 

itemList.html

<!DOCTYPE HTML>
<html xmlns:th="http://www.thymeleaf.org">
<head th:replace="fragments/header :: header" />
<body>
<div class="container">
  <div th:replace="fragments/bodyHeader :: bodyHeader"/>
  <div>
    <table class="table table-striped">
      <thead>
      <tr>
        <th>#</th>
        <th>상품명</th>
        <th>가격</th>
        <th>재고수량</th>
        <th></th>
      </tr>
      </thead>
      <tbody>
      <tr th:each="item : ${items}">
        <td th:text="${item.id}"></td>
        <td th:text="${item.name}"></td>
        <td th:text="${item.price}"></td>
        <td th:text="${item.stockQuantity}"></td>
        <td>
          <a href="#" th:href="@{/items/{id}/edit (id=${item.id})}"
             class="btn btn-primary" role="button">수정</a>
        </td>
      </tr>
      </tbody>
    </table>
  </div>
  <div th:replace="fragments/footer :: footer"/>
</div> <!-- /container -->
</body>
</html>

 

model 에 담아둔 상품 목록인 items 를 꺼내서 상품 정보를 출력

- 수정 버튼 클릭 시 이동 url

728x90
728x90