웹 계층 개발

유병익·2022년 11월 29일
0
post-thumbnail

1. 홈 화면과 레이아웃


1.1 HomeController


  • HomeController.java
    package jpabook.jpashop.controller;
    
    import lombok.extern.slf4j.Slf4j;
    import org.springframework.stereotype.Controller;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    @Controller
    @Slf4j
    public class HomeController {
    
        @RequestMapping("/")
        public String home(){
            log.info("Home Contoller");
            return "home";
        }
    }



1.2 Thymeleaf Template 등록


  • home.html
    <!DOCTYPE HTML>
    <html xmlns:th="http://www.thymeleaf.org">
    <head th:replace="fragments/header :: header">
        <title>Hello</title>
        <meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
    </head>
    <body>
    <div class="container">
        <div th:replace="fragments/bodyHeader :: bodyHeader"/>
        <div class="jumbotron">
            <h1>HELLO SHOP</h1>
            <p class="lead">회원 기능</p>
            <p>
                <a class="btn btn-lg btn-secondary" href="/members/new">회원 가입</a>
                <a class="btn btn-lg btn-secondary" href="/members">회원 목록</a>
            </p>
            <p class="lead">상품 기능</p>
            <p>
                <a class="btn btn-lg btn-dark" href="/items/new">상품 등록</a>
                <a class="btn btn-lg btn-dark" href="/items">상품 목록</a>
            </p>
            <p class="lead">주문 기능</p>
            <p>
                <a class="btn btn-lg btn-info" href="/order">상품 주문</a>
                <a class="btn btn-lg btn-info" href="/orders">주문 내역</a>
            </p>
        </div>
        <div th:replace="fragments/footer :: footer"/>
    </div> <!-- /container --></body>
    </html>
  • fragments/header.html
    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org">
    <head th:fragment="header">
        <!-- Required meta tags -->
        <meta charset="utf-8">
        <meta name="viewport" content="width=device-width, initial-scale=1, shrinkto-fit=no">
        <!-- Bootstrap CSS -->
        <link rel="stylesheet" href="/css/bootstrap.min.css" integrity="sha384-
    ggOyR0iXCbMQv3Xipma34MD+dH/1fQ784/j6cY/iJTQUOhcWr7x9JvoRxT2MZw1T"
              crossorigin="anonymous">
        <!-- Custom styles for this template -->
        <link href="/css/jumbotron-narrow.css" rel="stylesheet">
        <title>Hello, world!</title>
    </head>
  • fragments/bodyHeader.html
    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org">
    <div class="header" th:fragment="bodyHeader">
      <ul class="nav nav-pills pull-right">
        <li><a href="/">Home</a></li>
      </ul>
      <a href="/"><h3 class="text-muted">HELLO SHOP</h3></a>
    </div>
  • fragments/footer.html
    <!DOCTYPE html>
    <html xmlns:th="http://www.thymeleaf.org">
    <div class="footer" th:fragment="footer">
      <p>&copy; Hello Shop V2</p>
    </div>
  • Spring Boot의 Thymeleaf 기본 설정
spring:
	thymeleaf:
		prefix: classpath:/templates/
		suffix: .html
📌 즉, View를 resources/template/ 부터 찾는다.



1.3 View 리소스 등록


부트스트랩(v4.3.1) 사용 https://getbootstrap.com

  • jumbotron-narrow.css
```css
/* Space out content a bit */
body {
    padding-top: 20px;
    padding-bottom: 20px;
}
/* Everything but the jumbotron gets side spacing for mobile first views */
.header,
.marketing,
.footer {
    padding-left: 15px;
    padding-right: 15px;
}
/* Custom page header */
.header {
    border-bottom: 1px solid #e5e5e5;
}
/* Make the masthead heading the same height as the navigation */
.header h3 {
    margin-top: 0;
    margin-bottom: 0;
    line-height: 40px;
    padding-bottom: 19px;
}
/* Custom page footer */
.footer {
    padding-top: 19px;
    color: #777; border-top: 1px solid #e5e5e5;
}
/* Customize container */
@media (min-width: 768px) {
    .container {
        max-width: 730px;
    }
}
.container-narrow > hr {
    margin: 30px 0;
}
/* Main marketing message and sign up button */
.jumbotron {
    text-align: center;
    border-bottom: 1px solid #e5e5e5;
}
.jumbotron .btn {
    font-size: 21px;
    padding: 14px 24px;
}
/* Supporting marketing content */
.marketing {
    margin: 40px 0;
}
.marketing p + h4 {
    margin-top: 28px;
}
/* Responsive: Portrait tablets and up */
@media screen and (min-width: 768px) {
    /* Remove the padding we set earlier */
    .header,
    .marketing,
    .footer {
        padding-left: 0;
        padding-right: 0; }
    /* Space out the masthead */
    .header {
        margin-bottom: 30px;
    }
    /* Remove the bottom border on the jumbotron for visual effect */
    .jumbotron {
        border-bottom: 0;
    }
}
```



2. 회원 등록


2.1 회원 등록 Form


  • MemberForm.java
    package jpabook.jpashop.controller;
    
    import javax.validation.constraints.NotEmpty;
    
    import lombok.Getter;
    import lombok.Setter;
    
    @Getter
    @Setter
    public class MemberForm {
    
        @NotEmpty(message = "회원 이름은 필수입니다.")
        private String name;
    
        private String city;
        private String street;
        private String zipcode;
    
    }
  • 회원 등록 Form 객체를 사용해 데이터를 전달받는다.

참고

  • 요구사항이 정말 단순할 때는 엔티티를 직접 등록과 수정 화면에서 사용해도 된다.
  • 하지만 화면 요구사항이 복잡하면, 엔티티에 화면을 처리하기 위한 기능이 점점 증가한다.
    • 엔티티는 점점 화면에 종속적으로 변함 → 지저분해진 엔티티는 유지보수하기 어려움
  • 엔티티는 핵심 비즈니스 로직만 가져야 함
    • 화면을 위한 로직은 없어야 함
  • 화면이나 API에 맞는 폼 객체나 DTO를 사용하자.
    • API 요구사항을 Form 또는 DTO로 처리
    • 엔티티는 최대한 순수하게 유지



2.2 Member Controller


  • MemberController.java
    package jpabook.jpashop.controller;
    
    import jpabook.jpashop.domain.Address;
    import jpabook.jpashop.domain.Member;
    import jpabook.jpashop.domain.item.Book;
    import jpabook.jpashop.domain.item.Item;
    import jpabook.jpashop.service.MemberService;
    import lombok.RequiredArgsConstructor;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.validation.BindingResult;
    import org.springframework.web.bind.annotation.GetMapping;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    
    import javax.validation.Valid;
    import java.util.List;
    
    @Controller
    @RequestMapping("/members")
    @RequiredArgsConstructor
    public class MemberController {
    
        private final MemberService memberService;
    
        @GetMapping("/new")
        public String createForm(Model model){
            model.addAttribute("memberForm",new MemberForm());
            return "members/createMemberForm";
        }
    
        @PostMapping("/new")
        public String create(@Valid MemberForm memberForm, BindingResult result){
    
            if(result.hasErrors()){
                return "members/createMemberForm";
            }
    
            Address address = new Address(memberForm.getCity(), memberForm.getStreet(), memberForm.getZipcode());
            Member member = new Member();
            member.setName(memberForm.getName());
            member.setAddress(address);
            memberService.join(member);
            return "redirect:/";
        }
    }



2.3 회원 등록 View


  • 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>
                <input type="text" th:field="*{name}" class="form-control"
                       placeholder="이름을 입력하세요"
                       th:class="${#fields.hasErrors('name')}? 'form-control fieldError' : '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>



3. 회원 목록 조회


3.1 MemberController - 추가


    @GetMapping("")
    public String list(Model model){
        List<Member> members = memberService.findMembers();
        model.addAttribute("members",members);
        Item item = new Book();
        return "members/memberList";
    }
  • 회원 조회 기능을 위해, MemberController에 위 메서드 추가


3.2 회원 목록 View


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



4. 상품 등록


4.1 상품 등록 Form


  • BookForm.java
    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;
    
    }



4.2 Item Controller


  • ItemController.java
    package jpabook.jpashop.controller;
    
    import jpabook.jpashop.domain.item.Book;
    import jpabook.jpashop.domain.item.Item;
    import jpabook.jpashop.service.ItemService;
    import lombok.RequiredArgsConstructor;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.*;
    
    import java.util.List;
    
    @Controller
    @RequiredArgsConstructor
    @RequestMapping("/items")
    public class ItemController {
    
        private final ItemService itemService;
    
        @GetMapping("/new")
        public String createForm(Model model) {
            model.addAttribute("form", new BookForm());
            return "items/createItemForm";
        }
    
        @PostMapping("/new")
        public String create(BookForm bookForm) {
            Book book = Book.createBook(bookForm.getName(), bookForm.getPrice(), bookForm.getStockQuantity(), bookForm.getAuthor(), bookForm.getIsbn());
            itemService.save(book);
            return "redirect:/";
        }
    }



4.3 상품 등록 View


  • 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. 상품 목록


5.1 Item Controller - 추가


@GetMapping("")
    public String list(Model model) {
        List<Item> items = itemService.findItems();
        model.addAttribute("items", items);
        return "items/itemList";
    }
  • 상품 조회 기능을 위해, 위 메서드를 ItemController에 위 메서드 추가


5.2 상품 목록 View


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



6. 상품 수정


6.1 Item Controller - 추가


		@GetMapping("/{itemId}/edit")
    public String updateItemForm(@PathVariable Long itemId, Model model) {
        Book item = (Book) itemService.findOne(itemId);
        BookForm bookForm = new BookForm();
        bookForm.setId(item.getId());
        bookForm.setName(item.getName());
        bookForm.setPrice(item.getPrice());
        bookForm.setStockQuantity(item.getStockQuantity());
        bookForm.setAuthor(item.getAuthor());
        bookForm.setIsbn(item.getIsbn());
        model.addAttribute("form", bookForm);
        return "/items/updateItemForm";
    }

    @PostMapping("/{itemId}/edit")
    public String updateItem(@PathVariable Long itemId, @ModelAttribute("form") BookForm bookForm) {
         Book book = new Book();
				 book.setId(form.getId());
				 book.setName(form.getName());
				 book.setPrice(form.getPrice()); book.setStockQuantity(form.getStockQuantity());
				 book.setAuthor(form.getAuthor());
				 book.setIsbn(form.getIsbn());
				 itemService.saveItem(book);
    }
  • 상품 정보 수정 폼으로 이동 및 실제 수정 기능을 위해 ItemController 에 위 메서드들 추가

  • 상품 수정 폼 이동

    1. Home 화면에서 수정 버튼을 선택 → /items/{itemId}/edit URL을 GET 방식으로 요청
    2. updateItemForm() 실행
      • itemService.findOne(itemId) 호출
      • 수정할 상품을 조회
    3. 조회 결과를 모델 객체에 담아서 items/updateItemForm(View)에 전달
  • 상품 수정 실행

    📌 상품 수정 폼 HTML에는 상품의 id(hidden), 상품명, 가격, 수량 정보 있음 1. 상품 수정 폼에서 정보를 수정하고 Submit 버튼을 선택 2. /items/{itemId}/edit URL을 POST 방식으로 요청 3. updateItem() 실행

현재 컨트롤러에서 Entity 객체를 새로 생성해 파라미터로 서비스로 전달하는데, 해당 item 엔티티 인스턴스는 현재 준영속 상태이다. 따라서 영속성 컨텍스트의 지원을 받을 수 없고 데이터를 수정해도 변경 감지 기능은 동작하지 않는다.



6.2 상품 수정 View


  • updateItemForm.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:object="${form}" method="post">
            <!-- id -->
            <input type="hidden" th:field="*{id}"/>
            <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>
        <div th:replace="fragments/footer :: footer"/>
    </div> <!-- /container -->
    </body>
    </html>



6.3 Item Update 로직 수정


  • ItemController.java
		@PostMapping("/{itemId}/edit")
    public String updateItem(@PathVariable Long itemId, @ModelAttribute("form") BookForm bookForm) {
        itemService.updateItem(itemId, bookForm.getName(), bookForm.getPrice(), bookForm.getStockQuantity());
        return "redirect:/items";
    }
  • ItemService.java
		@Transactional
    public void updateItem(Long itemId, String name, int price, int stockQuantity) {
        Item findItem = itemRepository.findOne(itemId);
        findItem.setName(name);
			  findItem.setPrice(price);
    }

📌 Item Update 로직을 merge를 사용하지 않고 변경 감지를 사용하는 방법으로 수정

Controller에서 Entity 생성 X -> 필요한 값만 Service로 전달
📌 자세한 내용은 다음 장에서 다룬다.



7. 상품 주문


7.1 Order Controller


  • OrderController.java
    package jpabook.jpashop.controller;
    
    import jpabook.jpashop.domain.Member;
    import jpabook.jpashop.domain.Order;
    import jpabook.jpashop.domain.item.Item;
    import jpabook.jpashop.repository.OrderSearch;
    import jpabook.jpashop.service.ItemService;
    import jpabook.jpashop.service.MemberService;
    import jpabook.jpashop.service.OrderService;
    import lombok.RequiredArgsConstructor;
    import org.springframework.stereotype.Controller;
    import org.springframework.ui.Model;
    import org.springframework.web.bind.annotation.*;
    
    import java.util.List;
    
    @Controller
    @RequiredArgsConstructor
    
    public class OrderController {
    
        private final OrderService orderService;
        private final MemberService memberService;
        private final ItemService itemService;
    
        @GetMapping("/order")
        public String createForm(Model model) {
            List<Member> members = memberService.findMembers();
            List<Item> items = itemService.findItems();
    
            model.addAttribute("members", members);
            model.addAttribute("items", items);
            return "order/orderForm";
        }
    
        @PostMapping("/order")
        public String order(@RequestParam Long memberId, @RequestParam Long itemId, @RequestParam int count) {
            orderService.order(memberId, itemId, count);
            return "redirect:/orders";
        }
    }
  • 주문 폼 이동
    1. 메인 화면에서 상품 주문을 선택하면 /order 를 GET 방식으로 호출
    2. createForm() 메서드 실행
      • 주문할 고객정보와 상품 정보가 필요하므로 model 객체에 담아서 뷰에 넘겨줌
  • 주문 실행
    1. 주문할 회원과 상품 그리고 수량을 선택해서 Submit 버튼을 누름
    2. /order URL을 POST 방식으로 호출
    3. order() 메서드 실행
      • 고객 식별자, 주문할 상품 식별자, 수량 정보를 받아서 OrderService에 주문 요청
    4. 주문이 끝나면 상품 주문 내역이 있는 /orders URL로 리다이렉트



7.2 상품 주문 View


  • orderForm.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 role="form" action="/order" method="post">
            <div class="form-group">
                <label for="member">주문회원</label>
                <select name="memberId" id="member" class="form-control">
                    <option value="">회원선택</option>
                    <option th:each="member : ${members}"
                            th:value="${member.id}"
                            th:text="${member.name}"/>
                </select>
            </div>
            <div class="form-group">
                <label for="item">상품명</label>
                <select name="itemId" id="item" class="form-control">
                    <option value="">상품선택</option>
                    <option th:each="item : ${items}"
                            th:value="${item.id}"
                            th:text="${item.name}"/>
                </select>
            </div>
            <div class="form-group">
                <label for="count">주문수량</label>
                <input type="number" name="count" class="form-control" id="count" placeholder="주문 수량을 입력하세요">
            </div>
            <button type="submit" class="btn btn-primary">Submit</button>
        </form>
        <br/>
        <div th:replace="fragments/footer :: footer"/>
    </div> <!-- /container -->
    </body>
    </html>



8. 주문 목록 검색


8.1 Order Controller - 추가


		@GetMapping("/orders")
    public String orderList(@ModelAttribute("orderSearch") OrderSearch orderSearch, Model model) {
        List<Order> orderList = orderService.findOrderList(orderSearch);
        model.addAttribute("orders", orderList);
        return "order/orderList";
    }
  • 주문 목록 조회 기능을 위해 위 메서드를 OrderController에 추가



8.2 주문 목록 View


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



9. 주문 취소


9.1 Order Controller - 추가


	@PostMapping("/orders/{orderId}/cancel")
    public String cancelOrder(@PathVariable Long orderId) {
        orderService.cancelOrder(orderId);
        return "redirect:/orders";
    }
  • 주문 취소 기능을 위해 위 메서드를 Order Controller에 추가
profile
Backend 개발자가 되고 싶은

0개의 댓글