교육 (Today I Learned)/Hanaro

[Hanaro] 62일차 / REST API(CRUD), JSP

Bay Im 2024. 4. 18. 09:17

CRUD 기능 ArrayList → JPA (MySQL) 및 Model → REST API 변환하기

  • 이전 Model 사용 Controller
    • @Controller
      @RequiredArgsConstructor
      public class ProductController {
          final ProductRepository productRepository;
      
          // 전체 상품 출력
          @GetMapping("/")
          public String main(Model model) {
              List<Product> productList = productRepository.findAll();
              model.addAttribute("productList", productList);
      
              return "productList";
          }
      
          // 상품 추가 폼 출력
          @GetMapping("/add")
          public String viewAddForm() {
              return "addProductForm";
          }
      
          // 상품 추가
          @PostMapping("/add-action")
          public String addProduct(@RequestParam String name,
                                   @RequestParam int price,
                                   @RequestParam LocalDate limitDate) {
              Product product = Product.builder()
                      .name(name)
                      .price(price)
                      .limitDate(limitDate)
                      .build();
              productRepository.save(product);
      
              return "redirect:/";
          }
      
          // 상품 수정 폼 출력
          @GetMapping("/edit")
          public String viewUpdateForm(@RequestParam Long id, Model model) {
              Product product = productRepository.findById(id).orElse(null);
              model.addAttribute("product", product);
      
              return "editProductForm";
          }
      
          // 상품 수정
          @PostMapping("/edit-action")
          public String editProduct(@RequestParam Long id,
                                    @RequestParam String name,
                                    @RequestParam int price,
                                    @RequestParam LocalDate limitDate) {
              Product product = productRepository.findById(id).orElse(null);
              product.setName(name);
              product.setPrice(price);
              product.setLimitDate(limitDate);
              productRepository.save(product);
      
              return "redirect:/";
          }
      
          // 상품 삭제
          @GetMapping("/delete-action")
          public String deleteProduct(@RequestParam Long id) {
              Product product = productRepository.findById(id).orElse(null);
              productRepository.delete(product);
      
              return "redirect:/";
          }
      }


  • 폼 출력하는 ViewController 별도 생성
    • @Controller
      public class ProductViewController {
          // 메인 폼 출력
          @GetMapping("/")
          public String viewMain() {
              return "productList";
          }
      
          // 상품 추가 폼 출력
          @GetMapping("/add")
          public String viewAddForm() {
              return "addProductForm";
          }
      
          // 상품 수정 폼 출력
          @GetMapping("/edit")
          public String viewEditForm() {
              return "editProductForm";
          }
      }


  • DTO 생성
    • 엔티티 DTO 생성
      • 엔티티 → DTO로 변환하는 메소드도 함께 넣어준다.
      • 예시 (ProductDTO)
        • @Getter
          @Setter
          @AllArgsConstructor
          @NoArgsConstructor
          @Builder
          public class ProductDTO {
              private Long id;
              private String name;
              private int price;
              @DateTimeFormat(pattern = "yyyy-mm-dd")
              private LocalDate limitDate;
          
              // DTO-> Entity 변환 메소드
              public Product toProductEntity() {
                  return Product.builder()
                          .id(id)
                          .name(name)
                          .price(price)
                          .limitDate(limitDate)
                          .build();
              }
          }


    • @RequestBody 또는 @ResponseBody로 값을 받을 DTO 생성
      • html에서 보내거나 받아올 json 값을 넣어줄 DTO를 생성해준다.
      • Request, Response 값 둘 다 DTO를 생성해서 받아준다.
      • 받아올 값이 1개라면 DTO 생성없이 Map을 사용해도 괜찮다.
      • 예시 (AddReqDTO)
      • @Data
        public class AddReqDTO {
            private String name;
            private int price;
            @DateTimeFormat(pattern = "yyyy-mm-dd")
            private LocalDate limitDate;
        }
  • 엔티티 클래스에 엔티티 → DTO 변환 메소드 추가
    • 위에 DTO 클래스에서 DTO → 엔티티 변환 메소드를 만든것 처럼, 엔티티 클래스에도 엔티티 → DTO 변환 메소드를 추가해준다.
    • 예시 (Product)
      •     // Entity -> DTO 변환 메소드
            public ProductDTO toProductDTO() {
                return ProductDTO.builder()
                        .id(id)
                        .name(name)
                        .price(price)
                        .limitDate(limitDate)
                        .build();
            }


  • Repository 생성
    • JPA를 사용하기 위해 Repository 인터페이스를 생성한다.
    • @Repository 어노테이션 주입 후 JpaRepository 상속받기 (extends)
    • 예시 (ProductRepository)
      • @Repository
        public interface ProductRepository extends JpaRepository<Product, Long> {
        }


  • ApiController 생성
    • 어노테이션 주입
      • @RestController
      • @RequiredArgsConstructor
    • Repository 생성자 주입
      • final ProductRepository productRepository;
    • CRUD 기능 REST API 생성
      • 전체 조회
        •     // 상품 전체 조회
              @PostMapping("/")
              public List<Product> main() {
                  List<Product> productList = productRepository.findAll();
          
                  return productList;
              }


      • 추가
        •     // 상품 추가
              @PostMapping("/add-action")
              public List<Product> add(@RequestBody AddReqDTO addReqDTO) {
                  Product product = Product.builder()
                          .name(addReqDTO.getName())
                          .price(addReqDTO.getPrice())
                          .limitDate(addReqDTO.getLimitDate())
                          .build();
                  productRepository.save(product);
          
                  List<Product> productList = productRepository.findAll();
                  return productList;
              }


      • 상세 조회
        •     // 수정할 상품 불러오기
              @PostMapping("/edit")
              public ProductDTO viewEditForm(@RequestBody Map<String, Long> idMap) {
                  Long id = idMap.get("id");
                  Product product = productRepository.findById(id).orElse(null);
                  ProductDTO productDTO = product.toProductDTO();
          
                  return productDTO;
              }


      • 수정
        •     // 상품 수정
              @PostMapping("/edit-action")
              public List<Product> updateProduct(@RequestBody ProductDTO productDTO) {
                  Product product = productRepository.findById(productDTO.getId()).orElse(null);
                  product.updateProduct(productDTO.getName(), productDTO.getPrice(),productDTO.getLimitDate());
                  productRepository.save(product);
          
                  List<Product> productList = productRepository.findAll();
                  return productList;
              }


      • 삭제
        •     // 상품 삭제
              @PostMapping("/delete")
              public List<Product> delete(@RequestBody Map<String, Long> idMap) {
                  Long id = idMap.get("id");
                  Product product = productRepository.findById(id).orElse(null);
                  productRepository.delete(product);
          
                  List<Product> productList = productRepository.findAll();
                  return productList;
              }


  • html에서 fetch()를 사용하여 json 값 전달 및 가져오기
    • 전체 조회 (productList.html)
      • ...생략...
                <!-- 상품 목록 -->
                <div class="list-container">
                    <div id="productList" class="list"></div>
                </div>
                <!-- 총 상품 갯수 -->
                <div style="margin-bottom: 10px" class="listSizeBox bg-white rounded-start text-danger fw-bold">
                    <span id="total">▲ 총</span>
                    <span id="productCount" class="text-dark">0</span>
                    <span id="totalNum">개의 상품이 있습니다.</span>
                </div>
        ...생략...   
        <script>
            // 페이지 로드 시 초기 데이터 로드
            window.onload = function() {
                loadData();
            };
        
            // 데이터 로드 함수
            function loadData() {
                fetch("/", {
                    method: "POST",
                    headers: { "Content-Type": "application/json" }
                })
                    .then(response => response.json())
                    .then(data => {
                        renderProductList(data);
                    })
                    .catch(error => console.error('Error:', error));
            }
        
            // 상품 목록 렌더링 함수
            function renderProductList(products) {
                const productList = document.getElementById('productList');
                productList.innerHTML = '';
        
                if (products.length === 0) {
                    productList.innerHTML = `
                        <div class="row">
                            <table class="table table-bordered border-danger">
                                <tr class="text-center">
                                    <th scope="col">상품 목록이 비어있습니다.</th>
                                </tr>
                            </table>
                        </div>
                    `;
                } else {
                    productList.innerHTML = `
                        <table class="table table-bordered border-danger">
                            <tr class="text-center">
                                <th scope="col" id="listNum" class="text-danger">번호</th>
                                <th scope="col" id="listName">상품명</th>
                                <th scope="col" id="listPrice">가격</th>
                                <th scope="col" id="listDate">유통기한</th>
                                <th scope="col" id="editTitle">수정</th>
                                <th scope="col" id="removeTitle">삭제</th>
                            </tr>
                            ${products.map((product) => `
                                <tr class="text-center align-middle">
                                    <td>${product.id}</td>
                                    <td>${product.name}</td>
                                    <td>${product.price}</td>
                                    <td>${product.limitDate}</td>
                                    <td>
                                        <a href="/edit?id=${product.id}"
                                           class="btn btn-primary fw-bold editBtn"
                                           role="button">수정</a>
                                    </td>
                                    <td>
                                        <button class="btn btn-danger fw-bold removeBtn" onclick="confirmAndDelete(${product.id})">삭제</button>
                                    </td>
                                </tr>
                            `).join('')}
                        </table>
                    `;
                }
        
                // 총 상품 갯수 업데이트
                document.getElementById('productCount').textContent = products.length;
            }
    • 추가 (addProductForm.html)
      • ...생략...
                <form name="addForm" id="addForm">
                    <!-- 상품명 -->
                    <div class="form-group">
                        <label for="inputName" class="text-white">상품명:</label>
                        <input
                                type="text"
                                class="form-control"
                                id="inputName"
                                name="inputName"
                                required
                        />
                    </div>
        
                    <!-- 가격 -->
                    <div class="form-group">
                        <label for="inputPrice" class="text-white">가격:</label>
                        <input
                                type="number"
                                class="form-control"
                                id="inputPrice"
                                name="inputPrice"
                                required
                        />
                    </div>
        
                    <!-- 유통기한 -->
                    <div class="form-group">
                        <label for="inputLimitDate" class="text-white">유통기한:</label>
                        <input
                                type="text"
                                class="form-control datepicker"
                                id="inputLimitDate"
                                name="inputLimitDate"
                                required
                        />
                    </div>
        
                    <!-- 상품 추가 -->
                    <div class="addButton">
                        <button type="button" class="btn btn-dark" onclick="productAdded()">
                            상품 추가 완료!
                        </button>
                        <button
                                type="button"
                                onclick="history.back();"
                                class="btn btn-light"
                        >
                            돌아가기
                        </button>
                    </div>
        ...생략...
        <script>
            const productAdded = () => {
                const inputName = document.getElementById("inputName").value;
                const inputPrice = document.getElementById("inputPrice").value;
                const inputLimitDate = document.getElementById("inputLimitDate").value;
        
                fetch("/add-action", {
                    method: "POST",
                    headers: {
                        "Content-Type": "application/json"
                    },
                    body: JSON.stringify({
                        name: inputName,
                        price: inputPrice,
                        limitDate: inputLimitDate
                    })
                })
                    .then(response => {
                        if (response.ok) {
                            alert("상품을 추가하였습니다!");
                            window.location.href = "/";
                        } else {
                            console.error("상품 추가 실패");
                        }
                    })
                    .catch(error => console.error("Error: ", error));
            }
    • 상세 조회 (editProductForm.html)
      • ...생략...
        <form name="formUpdate" id="formUpdate">
                    <div class="form-group">
                        <!-- 인덱스 생성 -->
                        <input type="hidden" id="id" name="id"/>
        
                        <!-- 상품명 -->
                        <label for="inputName" class="text-white">상품명:</label>
                        <input
                                type="text"
                                class="form-control"
                                id="inputName"
                                name="inputName"
                        />
                    </div>
        
                    <!-- 가격 -->
                    <div class="form-group">
                        <label for="inputPrice" class="text-white">가격:</label>
                        <input
                                type="number"
                                class="form-control"
                                id="inputPrice"
                                name="inputPrice"
                        />
                    </div>
        
                    <!-- 유통기한 -->
                    <div class="form-group">
                        <label for="inputLimitDate" class="text-white">유통기한:</label>
                        <input
                                type="text"
                                class="form-control datepicker"
                                id="inputLimitDate"
                                name="inputLimitDate"
                        />
                    </div>
        ...생략...
        <script>
        		// url에서 id 값 가져오기 
            window.onload = function() {
                const queryString = window.location.search;
                const urlParams = new URLSearchParams(queryString);
                const id = urlParams.get('id');
        
                loadData(id);
            };
        
            const loadData = (id) => {
                fetch("/edit", {
                    method: "POST",
                    headers: {"Content-Type": "application/json"},
                    body: JSON.stringify({ id: id })
                })
                    .then(respnse => respnse.json())
                    .then(data => {
                        document.getElementById("id").value = data.id;
                        document.getElementById("inputName").value = data.name;
                        document.getElementById("inputPrice").value = data.price;
                        document.getElementById("inputLimitDate").value = data.limitDate;
                    })
                    .catch(error => {
                        console.error("Error: ", error);
                    })
            }
    • 수정 (editProductForm.html)
      • ...생략...
        <button
        	type="button"
        	class="btn btn-dark"
        	onclick="updateProduct()"
        	>상품 수정 완료!
        </button>
        ...생략...
        <script>    
            const updateProduct = () => {
                const id = document.getElementById("id").value;
                const inputName = document.getElementById("inputName").value;
                const inputPrice = document.getElementById("inputPrice").value;
                const inputLimitDate = document.getElementById("inputLimitDate").value;
        
                fetch("/edit-action", {
                    method: "POST",
                    headers: {
                        "Content-Type": "application/json"
                    },
                    body: JSON.stringify({
                        id: id,
                        name: inputName,
                        price: inputPrice,
                        limitDate: inputLimitDate
                    })
                })
                    .then(response => {
                        if (response.ok) {
                            alert("상품을 수정하였습니다!")
                            window.location.href = "/";
                        } else {
                            console.error("상품 수정 실패");
                        }
                    })
                    .catch(error => {
                        console.error("오류 발생: ", error);
                    })
            }
    • 삭제 (productList.html)
      • ...생략...
        <td>
        	<button class="btn btn-danger fw-bold removeBtn" onclick="confirmAndDelete(${product.id})">삭제</button>
        </td>
        ...생략...
        <script>
            // 상품 삭제 함수
            function confirmAndDelete(id) {
                var confirmation = confirm("정말로 삭제하시겠습니까?");
                if (confirmation) {
                    fetch("/delete", {
                        method: 'POST',
                        headers: {
                            'Content-Type': 'application/json'
                        },
                        body: JSON.stringify({ id: id })
                    })
                        .then(response => response.json())
                        .then(data => {
                            renderProductList(data);
                            alert("삭제하였습니다!");
                        })
                        .catch(error => console.error('Error:', error));
                }
            }
  • 기타
    • 헷갈렸던 점
      • 상세 조회와 수정이 같은 Form에서 이뤄지기 때문에 REST 구현시 조금 헤맸다. (Model 구현은 쉬웠는데..)
      • 상세 조회 폼으로 이동하면 먼저 사용자가 선택한 id에 해당하는 값들이 input 박스 안에 출력되어야 하고, 사용자는 출력된 input 박스 값을 수정하여 수정 액션이 이뤄져야 한다.
    • 해결 방법
      • 수정 Form html으로 가면 사용자가 전체 출력 Form에서 선택한 id를 가져와서 데이터를 출력한다. (localhost:8080/edit?id=1 의 형태에서 id 값만 가져오기!)
        • id 값 가져오기
          •     window.onload = function() {
                    const queryString = window.location.search;
                    const urlParams = new URLSearchParams(queryString);
                    const id = urlParams.get('id');
            
                    loadData(id);
                };
        • 데이터 출력하기
          •     const loadData = (id) => {
                    fetch("/edit", {
                        method: "POST",
                        headers: {"Content-Type": "application/json"},
                        body: JSON.stringify({ id: id })
                    })
                        .then(respnse => respnse.json())
                        .then(data => {
                            document.getElementById("id").value = data.id;
                            document.getElementById("inputName").value = data.name;
                            document.getElementById("inputPrice").value = data.price;
                            document.getElementById("inputLimitDate").value = data.limitDate;
                        })
                        .catch(error => {
                            console.error("Error: ", error);
                        })
                }
      • 이후 수정 버튼을 누르면 수정 액션이 진행되도록 한다.
        •     const updateProduct = () => {
                  const id = document.getElementById("id").value;
                  const inputName = document.getElementById("inputName").value;
                  const inputPrice = document.getElementById("inputPrice").value;
                  const inputLimitDate = document.getElementById("inputLimitDate").value;
          
                  fetch("/edit-action", {
                      method: "POST",
                      headers: {
                          "Content-Type": "application/json"
                      },
                      body: JSON.stringify({
                          id: id,
                          name: inputName,
                          price: inputPrice,
                          limitDate: inputLimitDate
                      })
                  })
                      .then(response => {
                          if (response.ok) {
                              alert("상품을 수정하였습니다!")
                              window.location.href = "/";
                          } else {
                              console.error("상품 수정 실패");
                          }
                      })
                      .catch(error => {
                          console.error("오류 발생: ", error);
                      })
              }
      • 이때 데이터 출력하기와, 수정하기 모두 document.getElementById("id명").value 형식을 사용한다. 나는 데이터 출력은 innerText를 사용하는 줄 알았지만 value를 사용하는 것이였다.

 

 

html에서 JSP로 변환 실습

  • build.gradle 코드 추가
    • dependencies {
          // JSP
          implementation 'jakarta.servlet:jakarta.servlet-api'
          implementation 'jakarta.servlet.jsp.jstl:jakarta.servlet.jsp.jstl-api'
          implementation 'org.apache.tomcat.embed:tomcat-embed-jasper'
          implementation 'org.glassfish.web:jakarta.servlet.jsp.jstl'
  • application.properties 코드 추가
    • # JSP
      spring.mvc.view.prefix=/WEB-INF/views/
      spring.mvc.view.suffix=.jsp
  • views 디렉토리에 .jsp 파일 생성
    • jsp 파일 상단에 아래 코드를 넣어준다.
      • <%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8" %>
        <%@ page trimDirectiveWhitespaces="true" %>
        <%@ taglib uri="http://java.sun.com/jsp/jstl/core" prefix="c" %>
    • 이후 구현된 html 코드 전체를 아래에 넣어주고, 타임리프 코드(xmlns:th="http://www.thymeleaf.org")는 지워준다.
    • 타임리프로 값을 가져오던 코드는 jsp 문법으로 변경해준다.
  • dependencies에 타임리프 관련 코드와 기존에 있던 html 파일은 모두 삭제
  • 변경 예시
    • 전제 조회
      • 타임리프
        •  <tr th:each="product: ${productList}" class="text-center align-middle">
                                  <th th:text="${product.id}"></th>
                                  <td th:text="${product.name}" class="text-center align-middle"></td>
                                  <td th:text="${product.price}" class="text-center align-middle"></td>
                                  <td th:text="${product.limitDate}" class="text-center align-middle"></td>
                                  <td class="text-center align-middle">
                                      <a
                                              th:href="@{/edit(id=${product.id})}"
                                              class="btn btn-primary fw-bold editBtn"
                                              role="button"
                                      >수정</a
                                      >
                                  </td>
                                  <td class="text-center align-middle">
                                      <a
                                              th:href="@{/delete-action(id=${product.id})}"
                                              class="btn btn-danger fw-bold removeBtn"
                                              role="button"
                                              onclick="return confirmAndNotify(this)"
                                      >삭제</a
                                      >
                                  </td>
                              </tr>
      • JSP
        •                         <c:forEach var="product" items="${productList}" varStatus="status">
                                  <tr class="text-center align-middle">
                                      <th><span>${product.id+1}</span></th>
                                      <td class="text-center align-middle"><span>${product.name}</span></td>
                                      <td class="text-center align-middle"><span>${product.price}</span></td>
                                      <td class="text-center align-middle"><span>${product.limitDate}</span></td>
                                      <td class="text-center align-middle">
                                          <a
                                                  href="/edit?id=${product.id}"
                                                  class="btn btn-primary fw-bold editBtn"
                                                  role="button"
                                          >수정</a
                                          >
                                      </td>
                                      <td class="text-center align-middle">
                                          <a
                                                  href="/delete-action?id=${product.id}"
                                                  class="btn btn-danger fw-bold removeBtn"
                                                  role="button"
                                                  onclick="return confirmAndNotify(this)"
                                          >삭제</a
                                          >
                                      </td>
                                  </tr>
                                  </c:forEach>
728x90