jae_coding

[Spring Project] 웹계층 개발 본문

Spring, java/Spring_Project

[Spring Project] 웹계층 개발

재코딩 2022. 8. 24. 17:17
반응형

목차

  • 홈 화면 레이아웃 구성
  • 회원 등록 및 조회
  • 상품 등록, 수정 및 목록 조회
  • 상품 주문, 취소 및 목록 조회
  • 결과 페이지

1. 홈 화면 레이아웃 구성

view 리소스 코드

 

 

Bootstrap

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

getbootstrap.com

에 들어가셔서 본인의 버전에 맞도록 다운로드 후 resources에 등록하시면 됩니다.

그리고 css 파일을 추가해주시면 됩니다.

jumbotron-narrow.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. 회원 등록 및 조회

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.validation.BindingResult;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;

import javax.persistence.EntityManager;
import javax.validation.Valid;
import java.util.List;

@Controller
@RequiredArgsConstructor
public class MemberController {

    private final MemberService memberService;

    //데이터를 열어보기
    @GetMapping("/members/new")
    public String createForm(Model model){
        model.addAttribute("memberForm", new MemberForm());
        return "members/createMemberForm";
    }

    //데이터를 실제로 등록하기
    @PostMapping("/members/new")
    //valid를 이용하여 필수빈칸 만들기, bindingResult를 이용하여 오류코드 실행
    public String create(@Valid MemberForm form, BindingResult result){

        //회원명이 없을 경우 에러메세지를 출력해준다. (MemberForm에 만들어 놓은 NotEmpty)
        if(result.hasErrors()){
            return "members/createMemberForm";
        }

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

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

}

MemberForm

package jpabook.jpashop.controller;

import lombok.Getter;
import lombok.Setter;

import javax.validation.constraints.NotEmpty;

@Getter
@Setter
public class MemberForm {

    @NotEmpty(message = "회원 이름은 필수입니다.")
    private String name;

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

}

CreateMemberForm

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

MemberList

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

 

여기서 주의할 점은 thymeleaf문을 이용하여 사용한다는 점이고 fragments 디렉토리 세팅을 해주어야합니다.

 

fragments

bodyHeader

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

footer

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<div class="footer" th:fragment="footer">
  <p>&copy; Hello Shop V2</p>
</div>

header

<!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, shrink-
  to-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>
  • 상품 등록, 수정 및 목록 조회

 

 

3. 상품 등록, 수정 및 목록 조회

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.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.GetMapping;
import org.springframework.web.bind.annotation.ModelAttribute;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;

import java.util.List;

@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){
        /** JPA가 관리를 하지 않는 준영속 엔티티!!
         * 준영속 엔티티: 영속성 콘텍스트가 더는 관리하지 않는 엔티티
         * 여기서 Book은 이미 DB에 한번 저장되어 식별자가 이미 존재한다.
         * 이렇게 임의로 만들어낸 엔티티도 기존 식별자를 가지고 있으면 준영속 엔티티로 볼 수 있다.
         * ** 준영속 엔티티 수정방법 **
         * 1. 변경 감지 기능 사용 (dirty checking)
         * 2. 병합("merge") 사용
         */
        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:/items";
    }

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

    //Id를 사용하게 되면 보안에 대한 취약점이 있으므로 권한을 부여하는 방법으로 하는 것도 좋다.
    @GetMapping("/items/{itemId}/edit")
    public String updateItemForm(@PathVariable("itemId") Long itemId, Model model){
        Book item = (Book) itemService.findOne(itemId); //타입캐스팅을 사용했지만 좋은 방법은 아니다.

        BookForm form = new BookForm();
        form.setId(item.getId());
        form.setName(item.getName());
        form.setPrice(item.getPrice());
        form.setStockQuantity(item.getStockQuantity());
        form.setAuthor(item.getAuthor());
        form.setIsbn(item.getIsbn());

        model.addAttribute("form", form);

        return "items/updateItemForm";
    }

    @PostMapping("/items/{itemId}/edit")
    public String updateItem(@PathVariable Long itemId, @ModelAttribute("form")BookForm form){
        /** dirty checking으로 변경하기
        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);
        */

        itemService.updateItem(itemId, form.getName(), form.getPrice(),
                form.getStockQuantity());
        return "redirect:/items";

    }
}

createItemForm

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

itemList

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

updateItemForm

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

 

 

 

4. 상품 주문, 취소 및 목록 조회

OrderController

package jpabook.jpashop.controller;

import jpabook.jpashop.domain.Item.Item;
import jpabook.jpashop.domain.Member;
import jpabook.jpashop.domain.Order;
import jpabook.jpashop.repository.OrderRepository;
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("memberId") Long memberId,
                        @RequestParam("itemId") Long itemId,
                        @RequestParam("count") int count){

        //어떤 고객이 어떤 아이템을 몇개 주문하는지 콜
        orderService.order(memberId, itemId, count);
        return "redirect:/orders";
    }

    @GetMapping("/orders")
    public String orderList(@ModelAttribute("orderSearch") OrderSearch orderSearch, Model model){
        List<Order> orders = orderService.findOrders(orderSearch);
        model.addAttribute("orders", orders);
        return "order/orderList";
    }

	//주문 취소
    @PostMapping("/orders/{orderId}/cancel")
    public String cancelOrder(@PathVariable("orderId")Long orderId){
        orderService.cancelOrder(orderId);
        return "redirect:/orders";
    }
}

 

OrderForm

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

OrderList

<!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> <div>
    <form th:object="${orderSearch}" class="form-inline">
      <div class="form-group mb-2">
        <input type="text" th:field="*{memberName}" class="form- control" placeholder="회원명"/>
      </div>
      <div class="form-group mx-sm-1 mb-2">
        <select th:field="*{orderStatus}" class="form-control"> <option value="">주문상태</option>
          <option th:each=
                          "status : ${T(jpabook.jpashop.domain.OrderStatus).values()}"
                  th:value="${status}"
                  th:text="${status}">option
          </option>
        </select>
      </div>
      <button type="submit" class="btn btn-primary mb-2">검색</button> </form>
  </div>
    <table class="table table-striped">
      <thead>
      <tr>
        <th>#</th>
        <th>회원명</th> <th>대표상품 이름</th> <th>대표상품 주문가격</th>
        <th>대표상품 주문수량</th> <th>상태</th> <th>일시</th> <th></th>
      </tr>
      </thead>
      <tbody>
      <tr th:each="item : ${orders}">
        <td th:text="${item.id}"></td>
        <td th:text="${item.member.name}"></td>
        <td th:text="${item.orderItems[0].item.name}"></td>
        <td th:text="${item.orderItems[0].orderPrice}"></td>
        <td th:text="${item.orderItems[0].count}"></td>
        <td th:text="${item.status}"></td>
        <td th:text="${item.orderDate}"></td>
        <td>
          <a th:if="${item.status.name() == 'ORDER'}" href="#"
             th:href="'javascript:cancel('+${item.id}+')'"
             class="btn btn-danger">CANCEL</a>
        </td>
      </tr>
      </tbody>
    </table>
  </div>
  <div th:replace="fragments/footer :: footer"/>
</div> <!-- /container -->
</body>
<script>
    function cancel(id) {
        var form = document.createElement("form");
        form.setAttribute("method", "post");
        form.setAttribute("action", "/orders/" + id + "/cancel");
        document.body.appendChild(form);
        form.submit();
}
   </script>
</html>

 

 

 

5. 결과 페이지

 

이렇게 Spring Project를 진행해보았습니다.

김영한님의 스프링부트와 JPA활용편을 따라하면서 시스템에 대한 MVC 모델과 JPAQL과 웹계층에 대해서 정말 제대로 차근차근 공부할 수 있었습니다. 다음에는 인프런 강의를 통하여 JPA에 대하여 더욱 자세하고 기초적인 것부터 공부할 계획을 가지게 되었습니다.

 

또한 토이 프로젝트를 진행하여 올려볼 예정입니다.

 

Gitbub링크를 걸어둘 예정이니 참고하여 코딩하시면 될 것 같습니다.

 

GitHub - yongjae5717/jpa_shop_project

Contribute to yongjae5717/jpa_shop_project development by creating an account on GitHub.

github.com

그리고 강의를 듣고싶으신분이 있으면 인프런 김영한

 

김영한의 스프링 부트와 JPA 실무 완전 정복 로드맵 - 인프런 | 로드맵

[사진][사진] 현역 개발 팀장이 전해주는진짜 실무 노하우. 실무에서 스프링 부트와 JPA를 재대로 알고 사용하는 것은 매우 중요합니다. 조 단위의 거래 금액을 처리하는 주문, 결제, 정산 같은 핵

www.inflearn.com

로드맵을 참고하여보시면 좋은 실력 향상을 가져올 것으로 기대하실 수 있습니다.

감사합니다:)

 

반응형
Comments