Spring MVC 패턴 예제로 공부하기

tabi·2023년 6월 20일
0

Spring

목록 보기
14/15
post-thumbnail

예제를 통해 Spring MVC 처리 과정에 대해 알아보자.

1. HomeController의 return 주소

  • HomeController : return "home"; 은 /WEB-INF/views/home.jsp 으로 요청하는 것과 같다.
  • 이유: servlet-context.xml 에서 다음과 같이 설정되어 있기 때문!
		<beans:property name="prefix" value="/WEB-INF/views/" />
		<beans:property name="suffix" value=".jsp" />

2. deptDTO로 실습하기

2-1. dept 정보 화면에 출력하기

  1. 조건
  • 요청 url은 다음과 같이 설정하고
http://localhost/scott/dept 요청
  • dept를 Select 해서 가져와보자.
Select* dept
  • dept.jsp 를 만들어 화면에 띄워보자
  • DeptDTO.java 는 다음과 같다.
package org.doit.ik.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor //디폴트 생성자
@NoArgsConstructor //모든 필드에 대한 생성자
public class DeptDTO {
	
		private int deptno;
		private String dname;
		private String loc;
}
  1. webapp - scott 폴더 생성 - 그 안에 dept 파일 생성(이 파일은 요청용 파일이다.)
/scott/dept 요청 URL
  1. DeptHandler 역할을 하는 핸들러를 만들자.
  • 요청에 의한 처리를 해주는 역할
  • HomeController 밑에 ScottController 생성
  • 기존에는 게시판을 만들 때 글쓰기, 수정하기 등 각각의 기능마다 핸들러를 만들었지만, Spring에서는 Controller 하나만 만들어주면 게시판 하나를 만들 수 있다.
  1. ScottController.java
package org.doit.ik;

import java.text.DateFormat;
import java.util.Date;
import java.util.Locale;

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import lombok.extern.log4j.Log4j;

@Controller
@Log4j //이것과 동일: private static final Logger logger = LoggerFactory.getLogger(HomeController.class);
public class ScottController {
	
@RequestMapping(value = "/scott/dept", method = RequestMethod.GET)
// 이것을 컨트롤러 메소드 라고 한다
// -> 따로 properties 파일이 필요없다. 요청이 들어오면 아래 함수를 처리하겠다는 의미
	public void dept(Locale locale, Model model) {
		log.info(">/scott/dept get 요청됨");
	}
}
  1. WEB-INF views 폴더에 scott 파일 생성 후, 그 안에 dept.jsp 파일 생성

  2. 이제 일반 dept 파일에서 실행시켜보면 이렇게 실행이 된다.

  3. src/main/java 하위의 org.doit.ik.mapper 패키지 안에 .scott으로 패키지를 생성한 뒤 그 안에 DeptMapper.java 파일을 만들자(인터페이스)

package org.doit.ik.mapper.scott;
import java.util.List;
import org.doit.ik.domain.deptDTO;

public interface DeptMapper {
	List<deptDTO> selectDept(); //부서정보를 Select 해 List에 담는 인터페이스	
}
  1. src/main/resources 하위의 org.doit.ik.mapper 패키지 안에 .scott으로 폴더를 만든 뒤 그 안에 DeptMapper.xml 파일을 만들자
  • 이것이 DAO와 같은 역할이다. (=Mapper.xml에 들어가는 쿼리에 따라 나오는 결과가 다르다.)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "https://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="org.doit.ik.mapper.scott.DeptMapper">
<!-- name은 반드시 인터페이스명, 경로와 동일하게 맞추어준다. -->

	<!-- List<deptDTO> selectDept(); -->
	<select id="selectDept" resultType="org.doit.ik.domain.DeptDTO">
	<!-- id 속성에는 반드시 인터페이스의 메소드명이 들어가야 한다. -->
	<!-- resultType 속성: select 태그에는 반드시 resultType(return타입)이 들어가야 한다. 담기는 객체의 타입이 들어감 -->
	SELECT *
	FROM dept
	ORDER BY deptno ASC <!-- 세미콜론(;) 찍으면 안 된다! -->
	</select>
	
</mapper>

즉, 만약 가져오고자 하는 정보값이 늘어나 DTO를 수정해야 한다면, Mapper.xml의 쿼리를 수정 -> DeptDTO.java에서 원하는 변수 선언 추가 -> dept.jsp에 ${추가된 변수}를 추가해주면 원하는 정보를 추가적으로 가지고 올 수 있다.

  • 추가된 DeptMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "https://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="org.doit.ik.mapper.scott.DeptMapper">
<!-- name은 반드시 인터페이스명, 경로와 동일하게 맞추어준다. -->

	<!-- List<deptDTO> selectDept(); -->
	<select id="selectDept" resultType="org.doit.ik.domain.DeptDTO">
	<!-- id 속성에는 반드시 인터페이스의 메소드명이 들어가야 한다. -->
	<!-- resultType 속성: select 태그에는 반드시 resultType(return타입)이 들어가야 한다. 담기는 객체의 타입이 들어감 -->
	SELECT d.deptno, dname, loc, COUNT(e.empno) numberOfEmps    
    FROM dept d FULL JOIN emp e ON d.deptno = e.deptno
    GROUP BY d.deptno, dname, loc
    ORDER BY deptno ASC
	</select>
	
</mapper>
  • 추가된 DeptDTO.java
package org.doit.ik.domain;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor //디폴트 생성자
@NoArgsConstructor //모든 필드에 대한 생성자
public class DeptDTO {
	
		private int deptno;
		private String dname;
		private String loc;
		
		private int numberOfEmps; //사원수
}
  • 변경된 dept.jsp
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Insert title here</title>
<link rel="shortcut icon" type="image/x-icon" href="../images/SiSt.ico">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.0/jquery.min.js"></script> 
<link rel="stylesheet" href="/resources/cdn-main/example.css">
<script src="/resources/cdn-main/example.js"></script>
</head>
<body class="wide">
<div class="message" title="Spring MVC 처리 과정">
	<ul>
		<li> pom.xml - 스프링 5.0.7 </li>
	</ul>
</div>
<h3><span class="material-symbols-outlined">lists</span> Dept Info</h3>

<form action= "/scott/emp" method="post">
	<table id="tbl-dept">
		<caption></caption>
		<thead>
         <tr>
           <th></th>
           <th>DeptNo</th>
           <th>DName</th>
           <th>Loc</th>
           <th>Edit</th>
         </tr>
      </thead>
      <tbody>
      <c:forEach items= "${list}" var="dto">
      <tr>
      <td><input type="checkbox" data-deptno="${ dto.deptno }" value="${ dto.deptno }" name="deptno"></td>
        <td>${ dto.deptno }</td>
        <td>${ dto.dname }<span class="badge right red">${dto.numberOfEmps}</span></td>
        <td>${ dto.loc }</td>
        <td align="center"><span class="material-symbols-outlined delete" data-deptno="${ dto.deptno }">close</span></td>
      </tr>
      
      </c:forEach>
      </tbody>
      <tfoot>
      <tr>
        <td colspan="5">
          <button id="search" class="search" >search</button> 
          <button id="add" type="button" class="add">부서추가</button> 
        </td>
      </tr>
      </tfoot>
    </table>
  </form>

</body>
</html>

  1. ScottController.java 에서 dto를 가져와 출력한다고 추가 코딩
package org.doit.ik;

import java.text.DateFormat;
import java.util.Date;
import java.util.List;
import java.util.Locale;

import org.doit.ik.domain.DeptDTO;
import org.doit.ik.mapper.scott.DeptMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import lombok.Setter;
import lombok.extern.log4j.Log4j;

@Controller
@Log4j //이것과 동일: private static final Logger logger = LoggerFactory.getLogger(HomeController.class);
public class ScottController {
	
	@Setter(onMethod=@__({@Autowired})) //@Autowired 해도 된다.
	private DeptMapper deptMapper;
	
	@RequestMapping(value = "/scott/dept", method = RequestMethod.GET)
    //컨트롤러 메소드 -> 따로 properties 파일이 필요없다. 요청이 들어오면 아래 함수를 처리하겠다는 의미
	
	public void dept(Locale locale, Model model) {
		log.info(">/scott/dept get 요청됨");
		List<DeptDTO> list = this.deptMapper.selectDept();
		//list.forEach(dto -> log.info(dto)); //dto를 가져와서 하나씩 출력하는 람다식
        model.addAttribute("list", list); //request.setAttribute와 동일
	}
}

의존 관계 자동 주입 4가지 @Autowired @Setter @Inject @Resource

  • DI(의존성 종속, Dependency Injection): 클래스간의 의존관계를 스프링 컨테이너가 자동으로 연결해주는 것
  • Dependency: 객체가 다른 객체와 상호작용하는 것
  • Ex) 클래스 A가 클래스 B,C와 상호작용한다면 객체 A는 객체B,C와 의존관계
  • @Primary : 같은 Type의 빈에 대해 주입시 가장 높은 우선순위를 갖는다.
    • @Autowired로 주입시 @Primary가 붙은 Bean이 주입된다는 것
    • Bean만 바꿔서 테스트할때 편하게 사용할 수 있다.
    • 같은 Type의 빈이 여러개 있을 때 @Primary가 붙은 빈 1개만 주입 받을 수 있다.
  • @Qualifier
    - 같은 Type의 빈이 여러개 있을 때 동시에 빈을 여러개 주입받아야 되는 상황에 사용(Application에서 여러개의 디비와 connection을 맺어야 할 경우 Multi DataSource가 필요)
  1. 실행
  • 이렇게 잘 불러와진다!
  1. 정적인 파일(HTML, CSS 등)은 resources에 넣는다.
  • 이런식으로!
  • head 태그 안에 이렇게 적용해주면 resources 파일 안에 있는 양식을 불러서 적용 된다.
<link rel="stylesheet" href="/resources/cdn-main/example.css">
<script src="/resources/cdn-main/example.js"></script>

2-2. 누르고 선택했을 때 부서 검색해 넘어가는 페이지 만들기

  1. emp 파일들 생성

  2. EmpMapper.java 생성(org.doit.ik.mapper.scott 안에)

package org.doit.ik.mapper.scott;

import java.util.ArrayList;
import java.util.List;

import org.doit.ik.domain.DeptDTO;
import org.doit.ik.domain.EmpDTO;

public interface EmpMapper {
	List<EmpDTO> selectEmp(ArrayList<Integer> deptnos); //체크한 부서번호를 매개변수로 넣어준다.
	
}
  1. EmpDTO.java 생성(org.doit.ik.domain 안에)
package org.doit.ik.domain;

import java.sql.Date;

import org.springframework.format.annotation.DateTimeFormat;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class EmpDTO {
    
   private int empno;
   private String ename;
   private String job;
   private int mgr;
   @DateTimeFormat(pattern = "yyyy/MM/dd")
   private Date hiredate;
   private int sal;
   private double comm;
   private int deptno;
}
  1. EmpMapper.xml 생성(org.doit.ik.mapper.scott 안에)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "https://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="org.doit.ik.mapper.scott.EmpMapper">
<!-- name은 반드시 인터페이스명, 경로와 동일하게 맞추어준다. -->

	<!-- List<EmpDTO> selectDept(); -->
	<select id="selectEmp" resultType="org.doit.ik.domain.EmpDTO">
	SELECT*
	FROM emp
	<where>
		<foreach item="deptno" collection="list"
		open="deptno IN(" separator=", " close=")">
		<!-- collection에는 부서번호를 가진 매개변수를 입력, Mapper.java에서 준 매개변수명 -->
		<!-- item(변수명)은 자유롭게 줘도 된다. -->
		<!-- open: 반복해서 변수를 찍기 전에 그 앞에 들어갈 문장 -->
		#{deptno}
		</foreach>
	</where>
	ORDER BY deptno ASC
	</select>
	
</mapper>

오류 주의

  • " separator=", " => separator 앞에 띄어쓰기 안 해주면 404 오류 발생한다.
  • org.springframework.beans.factory.UnsatisfiedDependencyException
  • org.springframework.beans.factory.BeanCreationException
  • java.lang.IllegalArgumentException
  • org.apache.ibatis.builder.BuilderException
  1. emp.jsp 생성(views 폴더 안에)
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Insert title here</title>
<link rel="shortcut icon" type="image/x-icon" href="../images/SiSt.ico">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.0/jquery.min.js"></script> 
<link rel="stylesheet" href="/resources/cdn-main/example.css">
<script src="/resources/cdn-main/example.js"></script>
</head>
<body class="wide">

<h3><span class="material-symbols-outlined">lists</span> Emp List</h3>

<form action= "/scott/dept" method="post">
	<table id="tbl-emp">
		<caption></caption>
		<thead>
      <tr>
        <th></th>
        <th>Empno</th>
        <th>Ename</th>
        <th>Job</th>
        <th>Mgr</th>
        <th>Hiredate</th>
        <th>Sal</th>
        <th>Comm</th>
        <th>Deptno</th>
      </tr>
    	</thead>
      <tbody>
     <c:forEach items="${ list }" var="dto">
      <tr>
        <td><input type="checkbox" value="${ dto.empno }" name="empno"></td>
        <td>${ dto.empno }</td>
        <td>${ dto.ename }</td>
        <td>${ dto.job }</td>
        <td>${ dto.mgr }</td>
        <td>${ dto.hiredate }</td>
        <td>${ dto.sal }</td>
        <td>${ dto.comm }</td>
        <td>${ dto.deptno }</td> 
      </tr>
      </c:forEach>
      </tbody>
      <tfoot>
      <tr>
        <td colspan="9">
          <button id="home" class="home">HOme</button>
        </td>
      </tr>
   	 </tfoot>
    </table>
  </form>

<script>
	$(function() {
		$("#search").on("click", function(event){
			if(!$("tbody :checkbox:checked").length){
				alert("부서를 체크하세요.");
				return;
			}//if
			$("form").submit();
		}); //click
	}); //ready
</script>
</body>
</html>
  1. ScottController.java 수정
package org.doit.ik;

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;

import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;

import org.doit.ik.domain.DeptDTO;
import org.doit.ik.domain.EmpDTO;
import org.doit.ik.mapper.scott.DeptMapper;
import org.doit.ik.mapper.scott.EmpMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;

import lombok.Setter;
import lombok.extern.log4j.Log4j;

@Controller
@Log4j //이것과 동일: private static final Logger logger = LoggerFactory.getLogger(HomeController.class);
public class ScottController {
	
	@Setter(onMethod=@__({@Autowired})) //@Autowired 라고 써도 된다.
	private DeptMapper deptMapper;
	
	@Setter(onMethod=@__({@Autowired})) //@Autowired 라고 써도 된다.
	private EmpMapper empMapper;
	
	@GetMapping("/scott/dept") //이것과 같다 @RequestMapping(value = "/scott/dept", method = RequestMethod.GET)
	//컨트롤러 메소드 -> 따로 properties 파일이 필요없다. 요청이 들어오면 아래 함수를 처리하겠다는 의미
	
	public void dept(Locale locale, Model model) {
		log.info(">/scott/dept get 요청됨");
		List<DeptDTO> list = this.deptMapper.selectDept();
		//list.forEach(dto -> log.info(dto)); //dto를 가져와서 하나씩 출력하는 람다식
		model.addAttribute("list", list); //request.setAttribute와 동일
	}//dept
	
	//Post 방식으로 요청
	@PostMapping("/scott/emp")
	// @RequestMapping(value = "/scott/dept", method = RequestMethod.POST)
	public String emp(@RequestParam(value="deptno")ArrayList<Integer> deptnos, Model model) {
	//public void emp(String[]deptnos) 혹은 public void emp(ArrayList<Integer> deptnos)로 받으면 알아서 변환까지 된다.
	//request해서 넘어오는 parameter 값은 무조건 String이기 때문
		log.info(">/scott/emp post 요청됨");
		List<EmpDTO> list = this.empMapper.selectEmp(deptnos);
		model.addAttribute("list", list);
		return "/scott/emp";
	}
	
}//class

2-3. 부서 추가하는 ajax 모달창 만들기

  • 이렇게 모달창에서 부서를 작성한 뒤 확인 버튼을 누르면
  • 이런식으로 추가되어야 한다.(성공 시 'SUCCESS' alert창 = alert(result);)
  1. dept.jsp에 부서추가하는 모달창 style 및 script 태그 추가
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Insert title here</title>
<link rel="shortcut icon" type="image/x-icon" href="../images/SiSt.ico">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.0/jquery.min.js"></script> 
<link rel="stylesheet" href="/resources/cdn-main/example.css">
<script src="/resources/cdn-main/example.js"></script>
<script src="/resources/js/dept.js"></script>
<style>
  span.material-symbols-outlined{
      vertical-align: text-bottom
  }
</style>
<style>
  tbody tr td:last-child  span:hover{
    color:red;
    cursor: pointer;
  }
  tbody tr:hover{ background-color: rgba(240,240,240,0.5); }
</style> 
<style>
   /* The Modal (background) */
   .modal {
     display: none; /* Hidden by default */
     position: fixed; /* Stay in place */
     z-index: 1; /* Sit on top */
     padding-top: 100px; /* Location of the box */
     left: 0;
     top: 0;
     width: 100%; /* Full width */
     height: 100%; /* Full height */
     overflow: auto; /* Enable scroll if needed */
     background-color: rgb(0,0,0); /* Fallback color */
     background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
   }
   
   /* Modal Content */
   .modal-content {
     position: relative;
     background-color: #fefefe;
     margin: auto;
     padding: 0;
     border: 1px solid #888;
     width: 40%;
     box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19);
     -webkit-animation-name: animatetop;
     -webkit-animation-duration: 0.4s;
     animation-name: animatetop;
     animation-duration: 0.4s
   }
   
   /* Add Animation */
   @-webkit-keyframes animatetop {
     from {top:-300px; opacity:0} 
     to {top:0; opacity:1}
   }
   
   @keyframes animatetop {
     from {top:-300px; opacity:0}
     to {top:0; opacity:1}
   }
   
   /* The Close Button */
   .close {
     color: white;
     float: right;
     font-size: 28px;
     font-weight: bold;
   }
   
   .close:hover,
   .close:focus {
     color: #000;
     text-decoration: none;
     cursor: pointer;
   }
   
   .modal-header {
     padding: 2px 16px;
     background-color: white;
     color: black;
   }
   
   .modal-body {padding: 2px 16px;}
   
   .modal-footer {
     padding: 2px 16px;
     background-color: gray;
     color: white;
   }
</style>
</head>
<body class="wide">
<div class="message" title="Spring MVC 처리 과정">
	<ul>
		<li> pom.xml - 스프링 5.0.7 </li>
	</ul>
</div>
<h3><span class="material-symbols-outlined">lists</span> Dept Info</h3>

<form action= "/scott/emp" method="post">
	<table id="tbl-dept">
		<caption></caption>
		<thead>
         <tr>
           <th></th>
           <th>DeptNo</th>
           <th>DName</th>
           <th>Loc</th>
           <th>Edit</th>
         </tr>
      </thead>
      <tbody>
      <c:forEach items= "${list}" var="dto">
      <tr>
      <td><input type="checkbox" data-deptno="${ dto.deptno }" value="${ dto.deptno }" name="deptno"></td>
        <td>${ dto.deptno }</td>
        <td>${ dto.dname }<span class="badge right red">${dto.numberOfEmps}</span></td>
        <td>${ dto.loc }</td>
        <td align="center"><span class="material-symbols-outlined delete" data-deptno="${ dto.deptno }">close</span></td>
      </tr>
      
      </c:forEach>
      </tbody>
      <tfoot>
      <tr>
        <td colspan="5">
          <button id="search" class="search" type="button">search</button> 
          <button id="add" type="button" class="add">부서추가</button> 
        </td>
      </tr>
      </tfoot>
    </table>
  </form>
  
   <!-- 부서 추가 모달창 -->
  <!-- The Modal -->
<div id="add-modal" class="modal">

  <!-- Modal content -->
  <div class="modal-content">
    <div class="modal-header"> 
      <h2>Ajax 부서 추가</h2>
    </div>
    <div class="modal-body">
      <div class="group">
        <label>부서번호</label>
        <input type="text" class="short" name="deptno" value="50">
       </div>
       <div class="group">
           <label>부서명</label>
           <input type="text" class="short" name="dname" value="QC">
       </div>
       <div class="group">
           <label>지역명</label>
           <input type="text" class="short" name="loc" value="SEOUL">
       </div>
       <div>
           <button id="add-dept" type="button" class="ok">확인</button>
           <button type="button" class="cancel">닫기</button>
       </div>
    </div>
    <div class="modal-footer">
      <h3>Modal Footer</h3>
    </div>
  </div> 
</div>
 

<script>
	$(function() {
		$("#search").on("click", function(event){
			if(!$("tbody :checkbox:checked").length){
				alert("부서를 체크하세요.");
				return;
			}//if
			$("form").submit();
		}); //click
		
	//아래로는 부서 추가하기 위해 모달창 띄우는 코드
	     var addModal = $("#add-modal");     
	     
	     $("#add").on("click", function() {      addModal.css("display", "block");            } );
	     $(".cancel").on("click", function() {      addModal.css("display", "none");            } ); 
	     $("body").on("click", function (event){
	        if( event.currentTarget == addModal ) addModal.css("display", "none");  
	     }); //on
	     
	 //확인 버튼을 누르면 ajax로 부서 추가 + 테이블에도 추가
	    $("#add-modal #add-dept").on("click", function (){ //확인 버튼을 onclick할 때
      		//모달창 안의 텍스트박스에 입력한 값을 얻어오는 작업
	    	 let deptno = $("#add-modal :text[name=deptno]").val();
	         let dname = $("#add-modal :text[name=dname]").val();
	         let loc = $("#add-modal :text[name=loc]").val();
	         
	         //dept.js 파일에 있는 ajax 함수 이용
	         //function add(dept, callback, error) { js Object }
	         let dept={
	        		 deptno : deptno,
	        		 dname : dname,
	        		 loc : loc
	         };
	         
	         deptService.add(dept, function(result) {
				//callback 함수에 의해 결과물을 가지고 왔다면(성공 시)
				//모달창을 닫아준다
				addModal.css("display","none");
				
				//ScottRestController의 삼항연산자에 의해 insert값을 가져오는 것을 성공했다면
				if(result==='SUCCESS'){ 
					let tr = $(`
							<tr>
						      <td><input type="checkbox" data-deptno="\${ deptno }" value="\${ deptno }" name="deptno"></td>
						        <td>\${ deptno }</td> //${ dto.deptno }와 동일
						        <td>\${ dname }<span class="badge right red">0</span></td>
						        <td>\${ loc }</td>
						        <td align="center"><span class="material-symbols-outlined delete" data-deptno="\${ deptno }">close</span></td>
						      </tr>
					`);
					
					//위에서 생성한 동적인 객체를 table의 마지막 자식으로 추가
					$(tr).appendTo($("table tbody"));
					
				}//if
				
				alert(result);
			});//add
	         
	         
   }); // click
	 
	 
			
	}); //ready
</script>

</body>
</html>
  1. dept.js에 있는 함수 활용
console.log("Dept Module...");

var deptService = (function(){

  function add(dept, callback, error){
    console.log("add dept...");
    $.ajax({
      type:'post',
      url:'/scott/dept/new',
      data:JSON.stringify(dept),
      cache:false,
      contentType:"application/json; charset=utf-8",
      beforeSend:function(xhr){
          //console.log("add dept... beforeSend");
      },
      success:function(result, status, xhr){ 
        //console.log("add dept... success");
        if(callback){
          callback(result);
        }
      },
      error:function(xhr, status, er){ 
        if(error){
          error(er);
        }
      }
    });
  } // add
  
  function remove(deptno, callback, error){
    console.log("remove dept...");
    $.ajax({
      type:'delete',
      url:'/scott/dept/'+ deptno,  
      cache:false,
      success:function(deleteResult, status, xhr){ 
        if(callback){
          callback(deleteResult);
        }
      },
      error:function(xhr, status, er){
        if(error){
          error(er);
        }
      }
    });
  } // remove
  
  return {
     add       : add, <!-- 이 부분이 ajax 처리해주는 것 -->
     remove : remove
  };

})();

  1. ajax 처리하는 ScottRestController.java 만들기
package org.doit.ik;

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;

import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;

import org.doit.ik.domain.DeptDTO;
import org.doit.ik.domain.EmpDTO;
import org.doit.ik.mapper.scott.DeptMapper;
import org.doit.ik.mapper.scott.EmpMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import lombok.Setter;
import lombok.extern.log4j.Log4j;


@RestController //ajax 처리를 하는 컨트롤러라는 의미
@RequestMapping("/scott/*")
@Log4j
public class ScottRestController {
	
	@Setter(onMethod=@__({@Autowired})) //@Autowired 라고 써도 된다.
	private DeptMapper deptMapper; //DeptMapper set
	
	//scott/dept/new 라는 url로 ajax 호출하므로
	@PostMapping("/dept/new")
	public ResponseEntity<String> InsertDept(@RequestBody DeptDTO dto){//성공/실패를 String으로 응답하는 ResponseEntity
	// insert 하는 InsertDept()
	//@RequestBody DeptDTO dto: JSON 형태의 dept 객체를 JAVA의 dept dto 객체로 변환
	
		log.info("/scott/dept/now POST 요청함");
		
		//DeptMapper.java 로 이동해 코드 추가 -> DeptMapper.xml로 이동해 코드 추가
	
		//ResponseEntity: 응답데이터와 응답상태를 같이 보낼 수 있는 데이터 형태
		int insertResult = this.deptMapper.insertDept(dto); //1, 0
		return insertResult==1? new ResponseEntity<String>("SUCCESS",HttpStatus.OK): new ResponseEntity<String>(HttpStatus.INTERNAL_SERVER_ERROR);
		//삼항연산자를 이용해 성공하면 성공메세지 SUCCESS와 함께 응답상태를 같이 보내고, 실패 시 응답 상태만 보내준다.
	}
}//class
  1. DeptMapper.java로 이동해서 부서 정보 추가하는 코드 추가해 부서정보 추가하는 인터페이스로 만들어준다.
package org.doit.ik.mapper.scott;

import java.util.List;

import org.doit.ik.domain.DeptDTO;

public interface DeptMapper {
	List<DeptDTO> selectDept(); //부서정보를 Select 해 List에 담는 인터페이스
	
	int insertDept(DeptDTO dto); //코드 추가
}
  1. DeptMapper.xml로 이동해 입력 정보를 가져오는 코딩
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "https://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="org.doit.ik.mapper.scott.DeptMapper">
<!-- name은 반드시 인터페이스명, 경로와 동일하게 맞추어준다. -->

	<!-- List<deptDTO> selectDept(); -->
	<select id="selectDept" resultType="org.doit.ik.domain.DeptDTO">
	<!-- id 속성에는 반드시 인터페이스의 메소드명이 들어가야 한다. -->
	<!-- resultType 속성: select 태그에는 반드시 resultType(return타입)이 들어가야 한다. 담기는 객체의 타입이 들어감 -->
	SELECT d.deptno, dname, loc, COUNT(e.empno) numberOfEmps    
    FROM dept d FULL JOIN emp e ON d.deptno = e.deptno
    GROUP BY d.deptno, dname, loc
    ORDER BY deptno ASC
	</select>
	
	<!-- int insertDept(DeptDTO dto); -->
	<insert id="insertDept">
		INSERT INTO dept(deptno, dname, loc)
		VALUES(#{deptno}, #{dname}, #{loc})
		<!-- #{} 해주면 DeptMapper에 객체 안의 getter에 의해서 자동으로 가져온다. -->
	</insert>
	
</mapper>

2-4. 'X' 클릭하면 원하는 부서 삭제할 수 있도록 하기

  1. dept.js의 remove 부분을 활용해 X 클릭해 원하는 부서만 삭제하는 AJAX를 만들어보자
  • url:'/scott/dept/'+ deptno 이렇게 /scott/dept/10 가져가면 10번을 삭제 한다는 것, 이런 url 패턴을 미리 만들어서 활용할 수 있다.
console.log("Dept Module...");

var deptService = (function(){

  function add(dept, callback, error){
    console.log("add dept...");
    $.ajax({
      type:'post',
      url:'/scott/dept/new',
      data:JSON.stringify(dept),
      cache:false,
      contentType:"application/json; charset=utf-8",
      beforeSend:function(xhr){
          //console.log("add dept... beforeSend");
      },
      success:function(result, status, xhr){ 
        //console.log("add dept... success");
        if(callback){
          callback(result);
        }
      },
      error:function(xhr, status, er){ 
        if(error){
          error(er);
        }
      }
    });
  } // add
  
  function remove(deptno, callback, error){
    console.log("remove dept...");
    $.ajax({
      type:'delete',
      url:'/scott/dept/'+ deptno,  
      cache:false,
      success:function(deleteResult, status, xhr){ 
        if(callback){
          callback(deleteResult);
        }
      },
      error:function(xhr, status, er){
        if(error){
          error(er);
        }
      }
    });
  } // remove
  
  return {
     add       : add, 
     remove : remove
  };

})();
  1. DeptMapper.java 에 가서 삭제하고자 하는 부서번호를 준다. (인터페이스 추가)
package org.doit.ik.mapper.scott;

import java.util.List;

import org.doit.ik.domain.DeptDTO;

public interface DeptMapper {
	List<DeptDTO> selectDept(); //부서정보를 Select 해 List에 담는 인터페이스
	
	int insertDept(DeptDTO dto);
	int deleteDept(int deptno); // 삭제하고자 하는 부서번호를 준다.
}
  1. DeptMapper.xml 에 삭제하는 코드 implement (Mapper 파일이라고 부르며 DAO의 역할)
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper
    PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
    "https://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="org.doit.ik.mapper.scott.DeptMapper">
<!-- name은 반드시 인터페이스명, 경로와 동일하게 맞추어준다. -->

	<!-- List<deptDTO> selectDept(); -->
	<select id="selectDept" resultType="org.doit.ik.domain.DeptDTO">
	<!-- id 속성에는 반드시 인터페이스의 메소드명이 들어가야 한다. -->
	<!-- resultType 속성: select 태그에는 반드시 resultType(return타입)이 들어가야 한다. 담기는 객체의 타입이 들어감 -->
	SELECT d.deptno, dname, loc, COUNT(e.empno) numberOfEmps    
    FROM dept d FULL JOIN emp e ON d.deptno = e.deptno
    GROUP BY d.deptno, dname, loc
    ORDER BY deptno ASC
	</select>
	
	<!-- int insertDept(DeptDTO dto); -->
	<insert id="insertDept">
		INSERT INTO dept(deptno, dname, loc)
		VALUES(#{deptno}, #{dname}, #{loc})
		<!-- #{} 해주면 자바 객체 안의 값을 가져오는 것 -->
		<!--  -->
	</insert>
	
	
	<!-- 삭제하는 코드 -->
	<delete id="deleteDept"> <!-- <delete>코드 안에 써주는 것</delete> -->
		DELETE FROM dept
		WHERE deptno = #{deptno}
	</delete>
	
</mapper>
  1. dept.jsp 에 가서 삭제하는 버튼에 대한 script 코드 추가
<%@ page contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>Insert title here</title>
<link rel="shortcut icon" type="image/x-icon" href="../images/SiSt.ico">
<script src="https://ajax.googleapis.com/ajax/libs/jquery/3.7.0/jquery.min.js"></script> 
<link rel="stylesheet" href="/resources/cdn-main/example.css">
<script src="/resources/cdn-main/example.js"></script>
<script src="/resources/js/dept.js"></script>
<style>
  span.material-symbols-outlined{
      vertical-align: text-bottom
  }
</style>
<style>
  tbody tr td:last-child  span:hover{
    color:red;
    cursor: pointer;
  }
  tbody tr:hover{ background-color: rgba(240,240,240,0.5); }
</style> 
<style>
   /* The Modal (background) */
   .modal {
     display: none; /* Hidden by default */
     position: fixed; /* Stay in place */
     z-index: 1; /* Sit on top */
     padding-top: 100px; /* Location of the box */
     left: 0;
     top: 0;
     width: 100%; /* Full width */
     height: 100%; /* Full height */
     overflow: auto; /* Enable scroll if needed */
     background-color: rgb(0,0,0); /* Fallback color */
     background-color: rgba(0,0,0,0.4); /* Black w/ opacity */
   }
   
   /* Modal Content */
   .modal-content {
     position: relative;
     background-color: #fefefe;
     margin: auto;
     padding: 0;
     border: 1px solid #888;
     width: 40%;
     box-shadow: 0 4px 8px 0 rgba(0,0,0,0.2),0 6px 20px 0 rgba(0,0,0,0.19);
     -webkit-animation-name: animatetop;
     -webkit-animation-duration: 0.4s;
     animation-name: animatetop;
     animation-duration: 0.4s
   }
   
   /* Add Animation */
   @-webkit-keyframes animatetop {
     from {top:-300px; opacity:0} 
     to {top:0; opacity:1}
   }
   
   @keyframes animatetop {
     from {top:-300px; opacity:0}
     to {top:0; opacity:1}
   }
   
   /* The Close Button */
   .close {
     color: white;
     float: right;
     font-size: 28px;
     font-weight: bold;
   }
   
   .close:hover,
   .close:focus {
     color: #000;
     text-decoration: none;
     cursor: pointer;
   }
   
   .modal-header {
     padding: 2px 16px;
     background-color: white;
     color: black;
   }
   
   .modal-body {padding: 2px 16px;}
   
   .modal-footer {
     padding: 2px 16px;
     background-color: gray;
     color: white;
   }
</style>
</head>
<body class="wide">
<div class="message" title="Spring MVC 처리 과정">
	<ul>
		<li> pom.xml - 스프링 5.0.7 </li>
	</ul>
</div>
<h3><span class="material-symbols-outlined">lists</span> Dept Info</h3>

<form action= "/scott/emp" method="post">
	<table id="tbl-dept">
		<caption></caption>
		<thead>
         <tr>
           <th></th>
           <th>DeptNo</th>
           <th>DName</th>
           <th>Loc</th>
           <th>Edit</th>
         </tr>
      </thead>
      <tbody>
      <c:forEach items= "${list}" var="dto">
      <tr>
      <td><input type="checkbox" data-deptno="${ dto.deptno }" value="${ dto.deptno }" name="deptno"></td>
        <td>${ dto.deptno }</td>
        <td>${ dto.dname }<span class="badge right red">${dto.numberOfEmps}</span></td>
        <td>${ dto.loc }</td>
        <td align="center"><span class="material-symbols-outlined delete" data-deptno="${ dto.deptno }">close</span></td>
      </tr>
      
      </c:forEach>
      </tbody>
      <tfoot>
      <tr>
        <td colspan="5">
          <button id="search" class="search" type="button">search</button> 
          <button id="add" type="button" class="add">부서추가</button> 
        </td>
      </tr>
      </tfoot>
    </table>
  </form>
  
   <!-- 부서 추가 모달창 -->
  <!-- The Modal -->
<div id="add-modal" class="modal">

  <!-- Modal content -->
  <div class="modal-content">
    <div class="modal-header"> 
      <h2>Ajax 부서 추가</h2>
    </div>
    <div class="modal-body">
      <div class="group">
        <label>부서번호</label>
        <input type="text" class="short" name="deptno" value="50">
       </div>
       <div class="group">
           <label>부서명</label>
           <input type="text" class="short" name="dname" value="QC">
       </div>
       <div class="group">
           <label>지역명</label>
           <input type="text" class="short" name="loc" value="SEOUL">
       </div>
       <div>
           <button id="add-dept" type="button" class="ok">확인</button>
           <button type="button" class="cancel">닫기</button>
       </div>
    </div>
    <div class="modal-footer">
      <h3>Modal Footer</h3>
    </div>
  </div> 
</div>
 

<script>
	$(function() {
		$("#search").on("click", function(event){
			if(!$("tbody :checkbox:checked").length){
				alert("부서를 체크하세요.");
				return;
			}//if
			$("form").submit();
		}); //click
		
	//아래로는 부서 추가하기 위해 모달창 띄우는 코드
	     var addModal = $("#add-modal");     
	     
	     $("#add").on("click", function() {      addModal.css("display", "block");            } );
	     $(".cancel").on("click", function() {      addModal.css("display", "none");            } ); 
	     $("body").on("click", function (event){
	        if( event.currentTarget == addModal ) addModal.css("display", "none");  
	     }); //on
	     
	 //확인 버튼을 누르면 ajax로 부서 추가 + 테이블에도 추가
	    $("#add-modal #add-dept").on("click", function (){ //확인 버튼을 onclick할 때
      		//모달창 안의 텍스트박스에 입력한 값을 얻어오는 작업
	    	 let deptno = $("#add-modal :text[name=deptno]").val();
	         let dname = $("#add-modal :text[name=dname]").val();
	         let loc = $("#add-modal :text[name=loc]").val();
	         
	         //dept.js 파일에 있는 ajax 함수 이용
	         //function add(dept, callback, error) { js Object }
	         let dept={
	        		 deptno : deptno,
	        		 dname : dname,
	        		 loc : loc
	         };
	         
	         deptService.add(dept, function(result) {
				//callback 함수에 의해 결과물을 가지고 왔다면(성공 시)
				//모달창을 닫아준다
				addModal.css("display","none");
				
				//ScottRestController의 삼항연산자에 의해 insert값을 가져오는 것을 성공했다면
				if(result==='SUCCESS'){ 
					let tr = $(`
							<tr>
						      <td><input type="checkbox" data-deptno="\${ deptno }" value="\${ deptno }" name="deptno"></td>
						        <td>\${ deptno }</td> //${ dto.deptno }와 동일
						        <td>\${ dname }<span class="badge right red">0</span></td>
						        <td>\${ loc }</td>
						        <td align="center"><span class="material-symbols-outlined delete" data-deptno="\${ deptno }">close</span></td>
						      </tr>
					`);
					
					//위에서 생성한 동적인 객체를 table의 마지막 자식으로 추가
					$(tr)
					.appendTo($("table tbody"))
					.find("span.delete") //'X'버튼을 나타내는 span 태그
						.on("click", function() {
							if (confirm("정말 삭제할까요?")) { 
									let deptno = $(this).data("deptno"); //data-deptno=50
									var spanDelete = $(this);
									deptService.remove(deptno, function(result) { 
										if(result==='SUCCESS')
											spanDelete.parents("tr").remove();
									}); //remove

							}//if
						});
				}//if
				
				alert(result);
			});//add
	               
   }); // click
	 
	 //'X'버튼 #tbl-dept > tbody > tr:nth-child(1) > td:nth-child(5) > span
	 $("#tbl-dept > tbody > tr > td > span").on("click", function(event){
		if (confirm("정말 삭제할까요?")) { //삭제 전 삭제여부 확인 위해 경고창 띄워준다.
			//<span class="material-symbols-outlined delete" data-deptno="${ dto.deptno }">close</span>
			//DB 삭제 작업
				let deptno = $(this).data("deptno"); //data-deptno=50
				deptService.remove(deptno, function(result) { //첫번째 매개변수: 삭제할 부서번호, 두번째 매개변수: callback 함수
					if(result==='SUCCESS') $(event.currentTarget).parents("tr").remove()
					//현재 이벤트를 받은 객체('X')의 부모인 'tr' 태그로 DB에서도 화면에서도 삭제된다.
				}); //remove

		}//if
	 }); //'X' 삭제 버튼 클릭
			
	}); //ready
</script>

</body>
</html>
  1. ajax 처리하는 ScottRestController.java 에 url 요청에 따라 삭제하는 deleteDept() 만들기
package org.doit.ik;

import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.model;

import java.text.DateFormat;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.Locale;

import org.doit.ik.domain.DeptDTO;
import org.doit.ik.domain.EmpDTO;
import org.doit.ik.mapper.scott.DeptMapper;
import org.doit.ik.mapper.scott.EmpMapper;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;

import lombok.Setter;
import lombok.extern.log4j.Log4j;


@RestController //ajax 처리를 하는 컨트롤러라는 의미
@RequestMapping("/scott/ * ")
@Log4j
public class ScottRestController {
	
	@Setter(onMethod=@__({@Autowired})) //@Autowired 라고 써도 된다.
	private DeptMapper deptMapper; //DeptMapper set
	
	//입력
	//scott/dept/new 라는 url로 ajax 호출하므로
	@PostMapping("/dept/new")
	public ResponseEntity<String> InsertDept(@RequestBody DeptDTO dto){ 
    //성공/실패를 String으로 응답하는 ResponseEntity
	// insert 하는 InsertDept()
	//@RequestBody DeptDTO dto: JSON 형태의 dept 객체를 JAVA의 dept dto 객체로 변환
	
		log.info("/scott/dept/now POST 요청함");
		
		//DeptMapper.java 로 이동해 코드 추가 -> DeptMapper.xml로 이동해 코드 추가
	
		//ResponseEntity: 응답데이터와 응답상태를 같이 보낼 수 있는 데이터 형태
		int insertResult = this.deptMapper.insertDept(dto); //1, 0
		return insertResult==1? new ResponseEntity<String>("SUCCESS",HttpStatus.OK): new ResponseEntity<String>(HttpStatus.INTERNAL_SERVER_ERROR);
	//삼항연산자를 이용해 성공하면 성공메세지 SUCCESS와 함께 응답상태를 같이 보내고, 실패 시 응답 상태만 보내준다.
	}//insert
	
	//삭제
	//scott/dept/10(부서번호) 라는 url로 ajax 호출
	//요청방식에는 get, post 말고도 다른 것이 많다. 이번에는 delete 요청방식을 이용해보자.
	// Restful: 요청url에 요청방식이 담겨있는 것
	@DeleteMapping(value = "/dept/{deptno}"
			, produces= {MediaType.TEXT_PLAIN_VALUE})//제공되는 데이터는 text 형태의 value 값이라는 의미
	//{변수명}을 주고 @PathVariable("deptno") int deptno하면 url 속에 있는 값을 받아서 변수 deptno에 저장하겠다는 것
	public ResponseEntity<String> DeleteDept(@PathVariable("deptno") int deptno){
	
		log.info("/scott/dept/"+deptno+ "DELETE 요청함");
		
		int deltetResult = this.deptMapper.deleteDept(deptno); //1, 0
		return deltetResult==1? new ResponseEntity<String>("SUCCESS",HttpStatus.OK): new ResponseEntity<String>(HttpStatus.INTERNAL_SERVER_ERROR);
		//삼항연산자를 이용해 성공하면 성공메세지 SUCCESS와 함께 응답상태를 같이 보내고, 실패 시 응답 상태만 보내준다.
	}
		
}//class
  • 그런데 추가 후 바로 삭제하거나, 삭제 후 바로 추가하려고 하면 오류가 발생한다. 새로고침을 눌러야만 추가하거나 삭제하는 작업을 할 수 있고 연속해서는 작업할 수 없는 문제가 발생하는 것이다.(SQLIntegrityConstraintViolationException)

java.sql.SQLIntegrityConstraintViolationException: ORA-00001: unique constraint (SCOTT.PK_DEPT) violated

  • 데이터베이스 테이블의 고유 제약 조건(PK_DEPT)이 위배되었기 때문에 발생
  • Unique Column에 대해 같은 값이 들어왔을 때 발생하는 에러
  • 먼저 참조되는 데이터를 삭제해주도록 하면 에러 해결된다.
profile
개발 공부중

0개의 댓글