Thymeleaf (타임리프)

- 스프링 부트에서는 JSP를 이용해서 View 페이지를 개발하는 방식에서 벗어나 템플릿 기반의 화면 처리를 지원한다.

ex) FreeMarker, Mustache, Thymeleaf 를 이용해서 확장자가 html 인 페이지를 개발할 수 있다.

- 그 템플릿 기반 화면 처리 지원 중 Thymeleaf 는 기존의 JSP 에서 사용하는 JSTL과 유사한 기능(출력) 을 제공한다.

- 타임리프를 써서 View 페이지를 생성할땐 html 확장자로 저장해서 타임리프를 쓴다

 

타임리프 사용시 View 파일이 저장되는 위치

- 타임리프 사용시 VIew 가 저장될 위치가 정해져있으므로 application.propertes 에서 ViewResolver 를 설정하지 않음

- 타임리프 사용시 View 파일들이 저장될 위치는 resources/templates

+ templates 하위에 폴더를 만들어서 그 안에 저장해도 된다, 어쨌건 templates 폴더 안에 View 파일이 있어야함

- 이때, View 파일의 확장자는 html 으로 저장해야한다


Thymeleaf 프로젝트 생성 방법

- Spring Boot 프로젝트 생성시 Template Engines 카테고리의 Thymeleaf 를 체크해야한다

 

프로젝트 Thymeleaf  :  실습 준비

 

파일들 살펴보기 :  pom.xml

- pom.xml 부분

		<!-- 타임리프(thymeleaf) -->
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>

- 타임리프 라이브러리가 추가되어있다

 

파일들 살펴보기 :  application.properties

 

server.port=80

- 내장된 아파치 톰캣의 포트번호를 80 으로 설정하고 있다, 설정하지 않으면 기본 8080 으로 설정된다

타임리프 사용시 주의

1. JSP 출력시 application.properties 에서 prefix, suffix 를 설정하지만, 타임리프 사용 출력시 설정하지 않는다

2. 타임리프 사용시 View 파일은 반드시 resources/templates 폴더 안에 저장된다

- templates 하위에 폴더를 만들어서 그 안에 저장해도 된다, 어쨌건 templates 폴더 안에 View 파일이 있어야함

- webapp 안에 더이상 View 파일이 들어가지 않게된다

3. 이때, View 파일의 확장자는 html 으로 저장해야한다, 따라서 JSTL, EL 등 JSP 지원 태그 사용 불가, 타임리프 지원 태그를 배워야한다

+ 공통적으로 사용될 CSS, JS, 이미지는 resources/static 폴더 하위에 저장

 

파일들 살펴보기 :  index.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<script>
	location.href="sample1";
//	location.href="sample2";
//	location.href="sample3";
//	location.href="sample4";
//	location.href="sample5";
//	location.href="sample6";
//	location.href="sample7";
//	location.href="listTest";
</script>

</body>
</html>

- 타임리프 사용시 JSP 파일인 index.jsp 는 사용 가능

- 이 파일은 JSP 파일로 만들어도 자동 실행되어서 괜찮지만, 다른 파일들은 View 파일로 만들면 실행되지 않음

 

파일들 살펴보기 : SampleController.java

+ DB 연동하는 예제가 아니므로 Service, DAO 는 없다

- SampleController.java

package com.example.demo.controller;

import java.sql.Timestamp;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Date;
import java.util.List;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

import com.example.demo.model.Member;

@Controller
public class SampleController {

	@RequestMapping("sample1")
	public String sample1(Model model) {		
//		model.addAttribute("greeting", "Hello World");
		model.addAttribute("greeting", "안녕 하세요");		
		return "sample1";
	}
	
	@RequestMapping("sample2")
	public String sample2(Model model) {
		Member member = new Member(1,"test","1234","홍길동", new Timestamp(System.currentTimeMillis()));
		
		model.addAttribute("member", member);
		
		return "sample2";
	}
	
	@RequestMapping("sample3")
	public String sample3(Model model) {
		List<Member> list = new ArrayList<Member>();
		
		for(int i=0; i<10; i++) {
			Member member = new Member(1,"test"+i,"1234","홍길동"+i, new Timestamp(System.currentTimeMillis()));			
			list.add(member);
		}		
		model.addAttribute("list", list);
		
		return "sample3";
	}
	
	@RequestMapping("sample4")
	public String sample4(Model model) {
		List<Member> list = new ArrayList<Member>();
		
		for(int i=0; i<10; i++) {
			Member member = new Member(i,"u000"+i %3,     // 3으로 나눈 나머지 id
					                     "p000"+i %3,     // 3으로 나눈 나머지 pw 
					                     "홍길동"+i, 
					                     new Timestamp(System.currentTimeMillis()));			
			list.add(member);
		}		
		model.addAttribute("list", list);
		
		return "sample4";
	}
	
	@RequestMapping("sample5")
	public String sample5(Model model) {
		
		String result = "SUCCESS";
		
		model.addAttribute("result", result);
		
		return "sample5";
	}
	
	@RequestMapping("sample6")
	public String sample6(Model model) {	
		
		model.addAttribute("now", new Date());
		model.addAttribute("price", 123456789);
		model.addAttribute("title", "This is a just sample");
		model.addAttribute("options", Arrays.asList("AAA","BBB","CCC","DDD"));
		
		return "sample6";
	}
	
	@RequestMapping("sample7")
	public String sample7(Model model) {	
		
		return "sample7";
	}
	
}

 

파일들 살펴보기 : Member.java (DTO)

package com.example.demo.model;

import java.sql.Timestamp;

public class Member {
	private int no;
	private String id;
	private String pw;
	private String name;
	private Timestamp regdate;
	
	public Member(int no, String id, String pw, String name, Timestamp regdate) {
		this.no = no;
		this.id = id;
		this.pw = pw;
		this.name = name;
		this.regdate = regdate;
	}
	
	public int getNo() {
		return no;
	}
	public void setNo(int no) {
		this.no = no;
	}
	public String getId() {
		return id;
	}
	public void setId(String id) {
		this.id = id;
	}
	public String getPw() {
		return pw;
	}
	public void setPw(String pw) {
		this.pw = pw;
	}
	public String getName() {
		return name;
	}
	public void setName(String name) {
		this.name = name;
	}
	public Timestamp getRegdate() {
		return regdate;
	}
	public void setRegdate(Timestamp regdate) {
		this.regdate = regdate;
	}	
}

 

프로젝트 Thymeleaf : 프로젝트 실행

- 오른쪽 마우스 - Run As - Run on Server 로 실행


프로젝트 Thymeleaf : 코드 설명 1

- index.jsp 에서 "sample1" 으로 요청 (일반 변수를 타임리프로 출력하는 예제)

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<script>
	location.href="sample1";
</script>

</body>
</html>

 

- Controller 클래스 SampleController.java 에서 "sample1" 요청 부분만

	@RequestMapping("sample1")
	public String sample1(Model model) {		
//		model.addAttribute("greeting", "Hello World");
		model.addAttribute("greeting", "안녕 하세요");		
		return "sample1";
	}

- View 페이지로 값을 가져갈때 Model 객체를 사용한다

- View 파일의 prefix, suffix 를 생각할 필요 없다

- return "sample1" 을 하면 templates 폴더 안의 sample1.html 파일로 찾아간다

 

- View 파일 sample1.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>

<h1>Thymeleaf Test page</h1>

<!-- th:text : 문자열 출력   -->
<h1 th:text="${greeting}">Thymeleaf Test page</h1>

</body>
</html>

 

타임리프 라이브러리 불러오기 (sample1.html 부분)

<html xmlns:th="http://www.thymeleaf.org">

- 타임리프를 쓰기 위해 불러오는 코드

+ xmlns 는 일종의 네임스페이스(ns) 를 의미함, 즉 th 라는 네임스페이스

 

타임리프 text 태그로 출력

<!-- th:text : 문자열 출력   -->
<h1 th:text="${greeting}">Thymeleaf Test page</h1>

- 문자열을 출력하기 위해선 th 라는 네임스페이스를 써야하고,  text 라는 태그를 사용

- 그 태그 안의 ${greeting} 은 Controller 에서 View 로 오기 전에 Model 에 저장했던 네임값

- 현재는 "안녕하세요" 가 값이 되어 브라우저에 그 문자열이 출력됨

+ 그냥 EL  ${greeting} 사용시 제대로 출력되지 않는다, 반드시 타임리프로 태그 안에서 ${greeting} 사용

 

 

+ 타임리프 태그 종류

- th:with

- th:unless

- th:inline

- th:text

- 약 10개의 태그들에 익숙해지기


프로젝트 Thymeleaf : 코드 설명 2

- index.jsp 에서 "sample2" 으로 요청 (DTO를 타임리프로 출력하는 예제)

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<script>
	location.href="sample2";
</script>

</body>
</html>

 

- Controller 클래스 SampleController.java 에서 "sample2" 요청 부분만

	@RequestMapping("sample2")
	public String sample2(Model model) {
		Member member = new Member(1,"test","1234","홍길동", new Timestamp(System.currentTimeMillis()));
		
		model.addAttribute("member", member);
		
		return "sample2";
	}

- DTO Member 객체 member 를 생성하면서 값을 설정했다

- 그 객체 member 를 Model 객체에 저장해서 templates 폴더 아래의 sample2.html View 파일로 이동

- 즉 객체가 Model 에 저장되서 전달되는 경우 어떻게 출력하는지 보자

 

- View 파일 sample2.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>

<!-- th:text : DTO 객체 출력   -->
<h1 th:text="${member}">Thymeleaf Test page</h1>

<!-- th:text : 문자열(HTML태그) 출력   -->
<div th:text='${"<h3>"+member.no+"</h3>"}'></div> 

<!-- th:utext : 데이터 출력   -->
<div th:utext='${"<h3>"+member.no+"</h3>"}'></div> 
<div th:utext='${"<h3>"+member.id+"</h3>"}'></div> 
<div th:utext='${"<h3>"+member.pw+"</h3>"}'></div> 
<div th:utext='${"<h3>"+member.name+"</h3>"}'></div> 
<div th:utext='${"<h3>"+member.regdate+"</h3>"}'></div> 

</body>
</html>

- 타임리프 태그들은 모두 네임스페이스(xmls) 으로 th 를 써야한다, 즉 태그들은 th: 로 시작한다

- 넘어온 DTO 객체 member 를 th:text 태그로 출력시 해당 DTO 객체의 주소값이 출력된다

- 넘어온 DTO 객체 member 의 번호 member.no 를 th:text 태그로 출력시, member.getNo() 의 값이 출력된다

- 즉 Model 객체로 DTO가 전달되었을땐 넘어온 객체의 Model 네임값.객체 필드명 으로 그 값을 출력

 

th:text vs th:utext

th.text

- th:text 를 사용하면 <h3> 태그가 문자로 인식되어 <h3>1</h3> 로 출력된다

<!-- th:text : 문자열(HTML태그) 출력   -->
<div th:text='${"<h3>"+member.no+"</h3>"}'></div>

 

th.utext

- th:utext 를 사용하면 태그를 문자가 아닌 HTML 태그로 인식해서 1 을 출력

<!-- th:utext : 데이터 출력   -->
<div th:utext='${"<h3>"+member.no+"</h3>"}'></div>

 

 


프로젝트 Thymeleaf : 코드 설명 3

- index.jsp 에서 "sample3" 으로 요청 (리스트를 타임리프로 출력하는 예제)

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<script>
	location.href="sample3";
</script>

</body>
</html>

 

- Controller 클래스 SampleController.java 에서 "sample3" 요청 부분만

	@RequestMapping("sample3")
	public String sample3(Model model) {
		List<Member> list = new ArrayList<Member>();
		
		for(int i=0; i<10; i++) {
			Member member = new Member(1,"test"+i,"1234","홍길동"+i, new Timestamp(System.currentTimeMillis()));			
			list.add(member);
		}		
		model.addAttribute("list", list);
		
		return "sample3";
	}

- 리스트 list 를 Model 객체에 저장해서 templates 폴더 하위의 sample3.html 파일로 이동한다

 

- View 파일 sample3.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>

<table border="1" align="center" width="300">
	<tr>
		<td>ID</td>
		<td>NAME</td>
		<td>REGDATE</td>
	</tr>
	
	<!-- th:each="변수 : ${리스트}" -->
	<tr th:each="member : ${list}">
		<td th:text="${member.id}"></td>
		<td th:text="${member.name}"></td>
		<td th:text="${#dates.format(member.regdate,'yyyy-MM-dd HH:mm:ss')}"></td>
	</tr>
</table>

</body>
</html>

- 타임리프를 쓰므로 타임리프 라이브러리를 불러오고 있다

- Model 객체에 리스트가 넘어왔을때는 th:each 태그를 사용한다, 형식은 th:each="변수 : ${리스트}"

+ JSTL 의 forEach 태그 역할을, 타임리프에서는 each 태그가 수행

- th:each 태그에 "변수 : ${리스트 네임값}" 을 쓰고, 그 th:each 태그 내부의 태그들에서 ${변수.DTO필드명} 으로 출력

- th:each 에 의해 리스트 안의 요소들이 member 에 차례로 저장되고, 출력시 그 ${member.필드명} 으로 출력

 

날짜 포멧 지정 (sample3.html 부분, 변형)

	<!-- th:each="변수 : ${리스트}" -->
	<tr th:each="member : ${list}">
		<td th:text="${#dates.format(member.regdate,'yyyy-MM-dd HH:mm:ss')}"></td>
	</tr>

- th:text 태그 안에 ${#dates.format(날짜값,'포맷')} 으로 포맷을 지정해서 출력한다

+ #dates 는 함수이다, 그 함수의 format() 메소드를 사용하는 것

 

 

포맷을 제외하고 그냥 ${member.regdate} 로 출력해보자

		<td th:text="${member.regdate}"></td>

- 패턴 미지정시 DTO regdate 필드의 자료형은 Timestamp 이므로, 년월일시분초(1000분의 1초 단위) 로 출력한다

+ 자료형이 Date 이면 년월일만 출력

+ 타임리프는 View 이지만 새로고침만으로 반영되지 않고 서버를 재시작해야 반영된다


타임리프 출력 정리

1. Model 객체에 일반 자료형인 값이 저장되었을때는, 그냥 th:text=${네임값} 으로 출력

2. Model 객체에 DTO 객체가 저장되었을때는, ${네임값.필드명} 으로 출력

3. Model 객체에 리스트인 값이 저장되었을때는, th:each 태그에 "변수명 : ${리스트 네임값}" 을 써서 하나씩 출력


프로젝트 Thymeleaf : 코드 설명 4

- index.jsp 에서 "sample4" 으로 요청 (다양한 타임리프 태그 예제)

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<script>
	location.href="sample4";
</script>

</body>
</html>

 

 

- Controller 클래스 SampleController.java 에서 "sample4" 요청 부분만

	@RequestMapping("sample4")
	public String sample4(Model model) {
		List<Member> list = new ArrayList<Member>();
		
		for(int i=0; i<10; i++) {
			Member member = new Member(i,"u000"+i %3,     // 3으로 나눈 나머지 id
					                     "p000"+i %3,     // 3으로 나눈 나머지 pw 
					                     "홍길동"+i, 
					                     new Timestamp(System.currentTimeMillis()));			
			list.add(member);
		}		
		model.addAttribute("list", list);
		
		return "sample4";
	}

- List 객체 생성 후 값을 입력해서 그 리스트를 Model 객체에 저장하고 sample4.html 로 이동

 

- View 페이지 sample4.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>

<table border="1" align="center" width="300">
	<tr>
		<td>ID</td>
		<td>NAME</td>
		<td>REGDATE</td>
	</tr>
	<tr th:each="member : ${list}">
		<td th:text="${member.id}"></td>
		<td th:text="${member.name}"></td>
		<td th:text="${#dates.format(member.regdate,'yyyy-MM-dd HH:mm:ss')}"></td>
	</tr>
</table><br>

<!-- 변수 정의 -->
<!-- th:with="변수명='값' " -->
<table border="1" align="center" width="300" th:with="target='u0001'">
	<tr>
		<td>ID</td>
		<td>NAME</td>
		<td>REGDATE</td>
	</tr>
	
	<!-- member.id 값이  target이 회원만 삼항연산자를 이용해서 secret을 출력 -->
	<tr th:each="member : ${list}">  
		<td th:text="${member.id == target ? 'secret' : member.id}"></td>
		<td th:text="${member.name}"></td>
		<td th:text="${#dates.format(member.regdate,'yyyy-MM-dd HH:mm:ss')}"></td>
	</tr>
</table><br>

<!-- if조건식 -->
<!-- th:if ~ th:unless -->
<table border="1" align="center" width="300" th:with="target='u0001'">
	<tr>
		<td>ID</td>
		<td>NAME</td>
		<td>REGDATE</td>
	</tr>
	
	<!-- member.id값이 target(u0001)과 같으면 Modify를 출력 하고,
	     member.id값이 target(u0001)과 같지 않으면 View를 출력 	 -->
	<tr th:each="member : ${list}">
		<td th:if="${member.id}">
		    <a href="/modify" th:if="${member.id == target}">Modify</a>
		    <p th:unless="${member.id == target}">View</p>
		</td>
		<td th:text="${member.name}"></td>
		<td th:text="${#dates.format(member.regdate,'yyyy-MM-dd HH:mm:ss')}"></td>
	</tr>
</table><br>

</body>
</html>

- 세 부분으로 나눠져있다, 각각 each 태그로 표(목록) 를 출력하므로 실행시 3개의 표 출력됨

1) th:each 태그로 리스트 출력

2) th:with 태그로 변수 정의

3) th:if 태그로 if 조건식 사용

- 나눠서 설명

 

1) th:each 태그로 리스트 출력 부분

<table border="1" align="center" width="300">
	<tr>
		<td>ID</td>
		<td>NAME</td>
		<td>REGDATE</td>
	</tr>
	<tr th:each="member : ${list}">
		<td th:text="${member.id}"></td>
		<td th:text="${member.name}"></td>
		<td th:text="${#dates.format(member.regdate,'yyyy-MM-dd HH:mm:ss')}"></td>
	</tr>
</table><br>

- each 태그를 tr 태그에 써야한다, 그래야 루프가 돌아갈때마다 tr 태그가 생성됨, 즉 행이 생성됨

- each 태그를 어디에 쓰느냐에 따라 결과가 달라진다, 반복되서 출력되어야하는 태그 안에 th:each 를 써야한다

 

- 앞에서 리스트 list 가 Model 객체에 저장되어 넘어왔으므로 th:each 태그에 "member : ${list}" 를 작성

- th:each 를 쓴 태그 안의 태그에서 ${model.필드명} 으로 데이터 출력

2) th:with 태그로 변수 정의 부분

<!-- 변수 정의 -->
<!-- th:with="변수명='값' " -->
<table border="1" align="center" width="300" th:with="target='u0001'">
	<tr>
		<td>ID</td>
		<td>NAME</td>
		<td>REGDATE</td>
	</tr>
	
	<!-- member.id 값이  target이 회원만 삼항연산자를 이용해서 secret을 출력 -->
	<tr th:each="member : ${list}">  
		<td th:text="${member.id == target ? 'secret' : member.id}"></td>
		<td th:text="${member.name}"></td>
		<td th:text="${#dates.format(member.regdate,'yyyy-MM-dd HH:mm:ss')}"></td>
	</tr>
</table><br>

- table 태그 안에 th:with="target='u0001'" 이 있다, target 이란 변수가 생성되고 값 'u0001' 이 저장됨

- td 의 th:text 태그에서 member.id 가 target('u0001') 인 경우 'secret' 를 출력, target 이 아니면 그대로 member.id 를 출력

+ 이때 target 은 변수이므로 ' ' 로 감싸지 않고, 'secret' 은 문자데이터이므로 ' ' 로 감싸야한다

+ 타임리프 태그안에서 연산자 사용 가능, 여기선 삼항 조건 연산자를 사용하고 있다

 

th:with 태그

- 변수를 만들때 쓰는 태그

- JSTL 의 core 라이브러리 set 태그 역할

- th:with="변수명='값'" 으로 변수를 생성하고 초기화한다

 

3) th:if 태그로 if 조건식 사용 부분

<!-- if조건식 -->
<!-- th:if ~ th:unless -->
<table border="1" align="center" width="300" th:with="target='u0001'">
	<tr>
		<td>ID</td>
		<td>NAME</td>
		<td>REGDATE</td>
	</tr>
	
	<!-- member.id값이 target(u0001)과 같으면 Modify를 출력 하고,
	     member.id값이 target(u0001)과 같지 않으면 View를 출력 	 -->
	<tr th:each="member : ${list}">
		<td th:if="${member.id}">
		    <a href="/modify" th:if="${member.id == target}">Modify</a>
		    <p th:unless="${member.id == target}">View</p>
		</td>
		<td th:text="${member.name}"></td>
		<td th:text="${#dates.format(member.regdate,'yyyy-MM-dd HH:mm:ss')}"></td>
	</tr>
</table><br>

th:with 로 정의된 변수의 적용 범위

-  table 태그안에 th:with 로 target 변수가 생성되었다

- 그럼 th:with 로 정의된 변수는 table 태그 내부에서만 사용가능하다

 

+ 리스트가 Model 객체에 저장되어 넘어왔으므로 th:each 태그 사용

- td 태그에서 th:if="${member.id}" 은 'member.id 값이 있으면' 이라는 의미이다

- th:if 태그에 의해 회원의 id 값이 target('u0001') 이면 a 태그 안에서 'Modify' 를 출력시키고,

- th:unless 에 의해 target('u0001') 이 아니면 p 태그 안에서 'View' 를 출력한다

 

th:if 태그, th:unless 태그

- if 조건식 사용 가능

- if - else 조건식을 th:if=${조건식} 과 th:unless=${조건식} 로 쓰면 된다

ex) th:if="${member.id == target}" 은 'member.id 가 target 과 같으면' 실행됨

- th:unless=${조건식} 은 조건식이 아닌경우 실행된다, 즉 반대의 의미를 가진다

ex) th:unless="${member.id == target}" 은 'member.id 가 target 과 같지 않으면' 실행됨

 

 

- 전체 출력 (아래)


프로젝트 Thymeleaf : 코드 설명 5

- index.jsp 에서 "sample5" 으로 요청 (자바스크립트 안에서 타임리프를 사용하는 예제)

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<script>
	location.href="sample5";
</script>

</body>
</html>

 

- Controller 클래스 SampleController.java 에서 "sample5" 요청 부분만

	@RequestMapping("sample5")
	public String sample5(Model model) {
		
		String result = "SUCCESS";
		
		model.addAttribute("result", result);
		
		return "sample5";
	}

 

- View 페이지 sample5.html

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>

<!-- javascript안에서 Thymeleaf 사용하기 -->
<!-- script태그에  th:inline="javascript"를 추가하면 script안에서 Thymeleaf를 사용할 수 있다. -->
<script th:inline="javascript">
	var result = [[${result}]]; 	     // result = "SUCCESS" 할당됨
	document.write(result);				 // SUCCESS  출력
</script>

<script>
	var result = [[${result}]];          // result변수에 값이 할당 안됨
	document.write(result);              // 아무것도 출력 안됨
</script>

</body>
</html>

자바스크립트안에서 타임리프 사용하는 방법

- script 태그 안에 th:inline="javascript" 코드를 작성해야한다

- 자바스크립트안에서 타임리프를 사용해서 ${result} 를 가져온다, 가져와서 자바스크립트 변수 result 에 저장후 출력

+ 아래쪽은 th:inline="javascript" 코드가 없으므로 출력되지 않는다, 아래 사진의 에러는 아래쪽 코드때문


프로젝트 Thymeleaf : 코드 설명 6

- index.jsp 에서 "sample6" 으로 요청 (여러 자료형의 값을 포맷 함수 사용해서 형식 바꿔서 출력 )

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<script>
	location.href="sample6";
</script>

</body>
</html>

 

- Controller 클래스 SampleController.java 에서 "sample6" 요청 부분만

	@RequestMapping("sample6")
	public String sample6(Model model) {	
		
		model.addAttribute("now", new Date());
		model.addAttribute("price", 123456789);
		model.addAttribute("title", "This is a just sample");
		model.addAttribute("options", Arrays.asList("AAA","BBB","CCC","DDD"));
		
		return "sample6";
	}

- Date 객체, int 형 데이터, String 형 데이터, 리스트를 만들어서 Model 객체에 저장해서 sample6.html 로 이동

+ 리스트를 만드는 방법 : new 연산자로 만들기, 또는 값이 정해져있는 데이터가 있을때는 Arrays.asList() 로 리스트 생성가능

 

- View 페이지 sample6.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>

<h1 th:text="${now}"></h1>
<h1 th:text="${price}"></h1>
<h1 th:text="${title}"></h1>
<h1 th:text="${options}"></h1>

<!--1.날짜 관련 : #dates -->
<h2 th:text="${#dates.format(now, 'yyyy-MM-dd HH:mm:ss')}"></h2>

<!--2.숫자 관련 : #numbers  -->
<h2 th:text="${#numbers.formatInteger(price, 3, 'COMMA')}"></h2>

<!--3.문자 관련 : #strings  -->
<h1 th:text="${title}"></h1>

<!-- replace()함수를 이용해서 s를 진하게 출력 -->
<span th:utext="${#strings.replace(title,'s','<b>s</b>')}"></span>

<!-- listSplit()함수를 이용해서 title변수의 값을 리스트로 처리하고 출력 -->
<ul>
	<li th:each="str:${#strings.listSplit(title,' ')}">[[${str}]]</li>
</ul>

</body>
</html>

- th:text 가 h1 태그에 들어가있다, 앞에 넘어온 자료형이 모두 다른 데이터인 now, price, title, options 를 th:text 로 출력

- 다음으로는 해당 값들을 형식을 바꿔서 출력

+ JSTL 에서 국제화 라이브러리로 처리했던 내용을 타임리프에서는 # 포맷함수들 사용

 

1. 날짜 관련 형식 변경하는 방법 : #dates (sample6.html 부분)

<!--1.날짜 관련 : #dates -->
<h2 th:text="${#dates.format(now, 'yyyy-MM-dd HH:mm:ss')}"></h2>

- #dates 포맷 함수로 패턴을 지정

- th:text 태그를 사용하고, "${#dates.format(날짜데이터 네임값, '패턴지정')}" 형식을 써서 날짜를 원하는 형식으로 출력

 

2. 숫자 관련 형식 변경하는 방법 : #numbers (sample6.html 부분)

<!--2.숫자 관련 : #numbers  -->
<h2 th:text="${#numbers.formatInteger(price, 3, 'COMMA')}"></h2>

- #numbers 포맷 함수로 패턴을 지정

- th:text 태그를 사용하고 ${#numbers.formatInteger(숫자데이터 네임값, 몇자리씩 끊어서 출력할지,'끊을 문자')}" 형식을 써서 숫자를 원하는 형식으로 출력

- 여기선 price 에 저장된 숫자값을 3자리 씩 콤마(,) 로 끊어서 출력하라는 의미

 

3. 문자 관련 형식 변경하는 방법 : #strings (sample6.html 부분)

<!--3.문자 관련 : #strings  -->
<h1 th:text="${title}"></h1>

<!-- replace()함수를 이용해서 s를 진하게 출력 -->
<span th:utext="${#strings.replace(title,'s','<b>s</b>')}"></span>

<!-- listSplit()함수를 이용해서 title변수의 값을 리스트로 처리하고 출력 -->
<ul>
	<li th:each="str:${#strings.listSplit(title,' ')}">[[${str}]]</li>
</ul>

 

- #strings 포맷 함수로 패턴을 지정

- 문자를 3번 출력하고 있다 (그냥 출력, #string.replace() 함수 사용,

1) 그냥 문자를 출력

2) th:utext 태그를 사용하고, ${#strings.replace(문자데이터 네임값, '치환될문자', '치환할문자')}

- 현재는 's' 문자를 <b> 태그를 사용해서 굵은 문자를 사용

- 태그를 적용하기 위해서는 th:utext 를 사용해야한다 

3) th:each 태그를 사용하고 "반복을 위한 변수명:${#strings.listSplit(문자데이터 네임값, '파싱할 문자')}

- 현재는 title 변수에 들어간 문자데이터를 공복을 기준으로 파싱하고, 그걸 리스트로 만든다

- 리스트로 만들어진 데이터를 th:each 태그에 의해 루프를 돌며 변수 str 에 하나씩 들어가고 그 str 을 출력

- li 태그 안에 th:each 가 들어갔으므로 루프가 돌아갈때마다 li 태그가 생성됨

 

- 전체 출력


프로젝트 Thymeleaf : 코드 설명 7

- index.jsp 에서 "sample7" 으로 요청 (타임리프로 링크 걸기)

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<script>
	location.href="sample7";
</script>

</body>
</html>

 

- Controller 클래스 SampleController.java 에서 "sample7" 요청 부분만

	@RequestMapping("sample7")
	public String sample7(Model model) {	
		
		return "sample7";
	}

 

- View 파일 sample7.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>

<ul>     <!-- 링크 걸기 -->
	<li><a th:href="@{http://localhost:80/Thymeleaf/sample1}">sample1</a></li>
	<li><a th:href="@{/sample1}">sample2</a></li>
	<li><a th:href="@{~/sample1}">sample3</a></li>
	
	<!-- 링크를 통해서 값전달 하기 -->	
	<!-- http://localhost/Thymeleaf/sample1?p1=aaa&p2=bbb -->
	<li><a th:href="@{/sample1(p1='aaa', p2='bbb')}">sample4</a></li>
</ul>

</body>
</html>

- 그냥 링크를 걸면 걸리지 않는다, 타임리프 형식을 갖춰서 링크를 걸어야함

타임리프 링크 걸기 4가지 형식

1) a 태그 th:href="@{http://localhost:80/Thymleaf/sample1}" 는 url 주소로 "sample1" 으로 요청하는 형식

- Thymeleaf 는 현재 프로젝트 명, 프로젝트명은 개발시에만 써줘야한다

2) a 태그 th:href="@{/sample1}" 를 통해 "sample1" 로 요청한다

3) a 태그 th:href="@{~/sample1}" 를 통해 "sample1" 로 요청한다 (현재는 오류)

- 즉 1), 2), 3) 은 모두 "sample1" 으로 요청하므로 결과는 똑같다

4) 값을 가져갈땐 th:href="@{/sample1(p1='aaa', p2='bbb')}" 로 p1 변수에 'aaa', p2 변수에 'bbb' 를 저장해서 "sample1" 으로 요청하면서 변수들을 넘긴다

- 위의 주석과 같은 기능이다

+ 이후 Controller 클래스 "sample1" 부분에서 p1, p2 변수값을 받아주는 코드를 쓰면 된다

 

th:href 태그

- a 태그 안에 th:href="@{url}" 를 사용해서 요청을 한다(링크를 건다)

- 값을가져갈땐 th:href="@{/요청이름(변수1=값, 변수2=값)}" 으로 가져간다

 

- sample1, sample2 를 클릭했을때

 

- sample4 를 클릭했을때


프로젝트 Thymeleaf : 코드 설명 8

- index.jsp 에서 "listTest" 으로 요청 (타임리프로 링크 걸기)

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<script>
	location.href="listTest";
</script>

</body>
</html>

 

- Controller 클래스 HomeController.java

package com.example.demo.controller;

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

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class HomeController {

	@RequestMapping("/listTest")
	public void listTest(Model model) {
		
		List list = new ArrayList();
		
		for(int i=1; i<=20; i++) {
			list.add("Data:"+i);
		}
		
		model.addAttribute("name", "Sample Data");
		model.addAttribute("list", list);
	}
}

- 리스트를 생성 후 더미데이터를 넣고 그 리스트를 Model 객체에 저장

- 메소드 리턴 자료형이 Spring 이 아닌 void 이다! 아래쪽에 return 구문이 없다

- 메소드 리턴 자료형이 void 인 경우 경우 요청이름과 동일한 이름을 가진 HTML 파일로 찾아간다

ex) 여기선 요청이름값이 listTest 이므로, View 페이지인 listTest.html 로 이동한다

 

- View페이지 listTest.html

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Thymeleaf 예제</title>
</head>
<body>

	<p th:text=" 'Hello, ' + ${name} +'!' "/>
	
	<ul>
		<li th:each="data : ${list}"  th:text="${data}"></li>
	</ul>

</body>
</html>

- Model 에 저장된 일반 자료형은 th:text 태그와 ${네임값} 으로 출력

- li 태그 안에 th:each 타임리프 태그를 썼으므로 li 태그가 반복적으로 출력됨, 같은 li 태그 안에 th:text 도 사용하면서 data 를 출력 

 

- index 파일 실행하기


 

타임리프를 적용한 게시판 프로그램 : 프로젝트 ThymeleafBoard

- Spring Boot 프로젝트 sbboard 와 같은 내용, 타임리프로 출력했다는 점이 다르다

 

프로젝트 ThymeleafBoard : 실습 준비

- Spring Boot 프로젝트 sbboard 와 같은 내용, 타임리프로 출력했다는 점이 다르다

- templates 폴더 하위에 모든 View 파일들이 html 파일로 저장되어있다

 

- 또한 타임리프 사용시 기존 방식대로 링크를 걸 수 없으므로 타임리프 태그 를 사용해서 링크를 건다

- board_cont.jsp 부분

 

파일들 살펴보기 : DataAccessConfig.java

package com.example.demo.config;

import javax.sql.DataSource;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

@Configuration
@PropertySource("classpath:/application.properties")          
public class DataAccessConfig {
	
	@ConfigurationProperties(prefix = "spring.datasource")    
	public DataSource dataSource() {
		return DataSourceBuilder.create().build();
	}
	
	@Bean
	public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception{
		SqlSessionFactoryBean factoryBean=new SqlSessionFactoryBean();
		
		factoryBean.setDataSource(dataSource);
		factoryBean.setMapperLocations(
				new PathMatchingResourcePatternResolver().getResources("classpath:/mapper/*.xml")
				);
		factoryBean.setTypeAliasesPackage("com.example.demo.model"); // DTO Alias 설정
		return factoryBean.getObject();
	}
	
	@Bean
	public SqlSessionTemplate sessionTemplate(SqlSessionFactory sqlSessionFactory) {
		return new SqlSessionTemplate(sqlSessionFactory);
	}
}

- dataSource() 메소드에서 생성된 객체가 리턴되어 아래의 sqlSessionFactory() 의 매개변수로 자동으로 들어간다

- sqlSessionFactory() 메소드에서 생성된 객체가 리턴되어 아래의 sessionTemplate() 의 매개변수로 자동으로 들어간다

 

파일들 살펴보기 : board_list.html

- 타임리프로 어떻게 출력하는지 보기

- Controller 클래스에서 넘어오는 값

 

- board_list.html

<!doctype html>
<html xmlns:th="http://www.thymeleaf.org">
<html lang="ko">
<head>
	<meta charset="UTF-8">
	<title>게시판 목록</title>
    <link rel="stylesheet" href="./css/bbs.css" type="text/css">
</head>

<body>
	<!-- 게시판 리스트 -->
	<div id="bbslist_wrap">
		<h2 class="bbslist_title">게시판 목록</h2>
		<p th:text=" '글갯수:' +  ${listcount} "></p>

		<table id="bbslist_t">
			<tr align="center" valign="middle" bordercolor="#333333">
				<td style="font-family: Tahoma; font-size: 11pt;" width="8%"
					height="26">
					<div align="center">번호</div>
				</td>
				<td style="font-family: Tahoma; font-size: 11pt;" width="47%">
					<div align="center">제목</div>
				</td>
				<td style="font-family: Tahoma; font-size: 11pt;" width="14%">
					<div align="center">작성자</div>
				</td>
				<td style="font-family: Tahoma; font-size: 11pt;" width="17%">
					<div align="center">날짜</div>
				</td>
				<td style="font-family: Tahoma; font-size: 11pt;" width="14%">
					<div align="center">조회수</div>
				</td>
			</tr>

			<tr th:each="b, i : ${boardlist}"  th:with="num=${listcount-(page-1)*10}"
			    align="center" valign="middle" bordercolor="#333333"
				onmouseover="this.style.backgroundColor='F8F8F8'"
				onmouseout="this.style.backgroundColor=''">
				
				<td th:with="num=${num - i.index}"  th:text="${num}"></td>				
				
				<td>
					<div align="left">						
						
					<span th:if="${b.board_re_lev != 0}"> 					
						<span th:each="k : ${#numbers.sequence(1, b.board_re_lev)}">
							&nbsp;&nbsp;	
						</span>			
						<img src="./images/AnswerLine.gif">	
					</span>					
							
					<a th:href="@{/board_cont(board_num=${b.board_num},page=${page},state='cont')}" th:text="${b.board_subject}"> 
					</a>
					
					</div>
				</td>

				<td th:text="${b.board_name}"></td>
				
				<td th:text="${#dates.format(b.board_date, 'yyyy-MM-dd')}"></td>
				<!-- <td th:text="${b.board_date}"></td> -->
				
				<td th:text="${b.board_readcount}"></td>
			</tr>		
						
		</table>

		<div id="bbslist_paging">			
			<span th:if="${page <=1}" th:text="[이전]"></span>&nbsp;		
			
			<span th:if="${page>1}">
				<a th:href="@{/board_list(page=${page-1})}" th:text="[이전]"></a>&nbsp;
			</span>			
			
			<span th:each="a : ${#numbers.sequence(startpage, endpage)}">
				<span th:if="${a == page }" th:text="${a}"></span>
				<span th:if="${a != page }">
					<a th:href="@{/board_list(page=${a})}" th:text="${a}"></a>&nbsp;
				</span>
			</span>							
			
			<span th:if="${page >= maxpage }" th:text="[다음]"></span>&nbsp;			
			
			<span th:if="${page < maxpage }">
				<a th:href="@{/board_list(page=${page+1})}" th:text="[다음]"></a>&nbsp;
			</span>		
			
		</div>
		<div id="bbslist_w">
			<input type="button" value="글쓰기" class="input_button"
				onclick="location.href='board_write'">
		</div>
		
	</div>
</body>
</html>

th:each 에서 반복 변수와 인덱스 받기 (board_list.html 부분)

- b 에는 ${boardlist} 에서 하나씩 요소를 가져와서 저장하고, i 에는 그 리스트의 인덱스가 들어간다

 

화면 출력 번호 num 변수 (board_list.html 부분)

1. 위에서 num 변수를 선언하고, th:each 루프가 돌아갈때마다 다시 num 값이 선언된다

2. 아래에서 num - 인덱스 를 해서 루프가 돌아갈때마다 num - 0, num - 1, num - 2 로 재정의한다

+ i.index 에서 index 속성이다

 

일반 변수 출력 (board_list.html 부분)

 

댓글, 원문인 경우 구별 위해 if 태그 사용 (board_list.html 부분)

 

제목 클릭시 상세페이지로 링크걸기 (board_list.html 부분)

- GET 방식으로 "board_cont" 로 요청하며 글 번호, 페이지 번호, state 값을 가지고 간다

 

연속적인 값 출력 (board_list.html 부분)

- 연속적인 숫자값을 출력할때는 th:each 태그와 #numbers.sequence(시작, 끝) 으로 쉽게 출력 가능하다

- th:each="a : ${#numbers.sequence(startpage, endpage)}" 에 의해 startPage ~ endPage 까지 루프가 돌아가고, 그 값이 a 변수로 들어간다

 

연속적인 값 출력 2 (board_list.html 부분)

 

- 나머지 내용은 Spring Boot 프로젝트 (JSP 출력) sbboard 또는 Spring 프로젝트 springboard 와 같은 내용이므로 설명 생략

Spirng Boot 의 환경설정 파일

- pom.xml, Mapper 파일만 있다

- web.xml, servlet-context.xml, root-context.xml 이 사용되지 않는다

- MyBatis 환경설정 파일인 Configuration.xml 도 사용되지 않는다, 여기서 alias 가 설정되지 않음

- 새로 application.properties 환경 설정 파일이 생성된다

+ 새로 DB 관련 환경 설정 관련 클래스 (DataAccessConfig.java) 하나가 생성된다

 

Spring Boot 환경설정 파일 정리

1. pom.xml

2. Mapper 파일

3. application.properties

4. DataAccess 설정 파일 클래스


Spring Boot - MyBatis 연동 예제 1 : 프로젝트 MyBatis01

프로젝트 MyBatis01 : 프로젝트 생성

- java 폴더 하위에 Package 에 적힌 패키지가 생성된다

 

프로젝트 MyBatis01 : 프로젝트 실행 확인

오른쪽 마우스 - Run As - Run on Server 로 프로젝트를 실행

- 이 화면이 출력되면 실행 성공이다

 

프로젝트 MyBatis01 : 환경설정 pom.xml 파일 수정

		<!-- jsp 파일을 사용하기 위한 의존 라이브러리 -->
		<dependency>
			<groupId>org.apache.tomcat.embed</groupId>
			<artifactId>tomcat-embed-jasper</artifactId>
			<scope>provided</scope>
		</dependency>
		<!-- jstl -->
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>jstl</artifactId>
		</dependency>

- pom.xml 에 JSP 파일 사용하기 위한 의존라이브러리와 JSTL 의존 라이브러리 추가

- JSTL 을 사용하기 위해 필요한 라이브러리이다

+ ojdbc8 을 쓸 것이므로 ojdbc8 을 남기고, ojdbc6 와 ojdbc6 다운 위한 repository 인 codelds 는 주석으로 막기

 

프로젝트 MyBatis01 : 테이블 생성하기

- webapp 폴더 하위에 sql 폴더를 생성하고, sql 폴더 하위에 myboard.sql 파일 생성

- Connection profile 을 설정하고, 테이블 boardtest, 시퀀스 boardtest_seq 생성

- myboard.sql

select * from tab;
select * from seq;

create table boardtest(
no number primary key,
name varchar2(20),
subject varchar2(50),
content varchar2(1000),
register date
);

create sequence boardtest_seq;


DB 연동 준비

top-level 패키지 하위에 JAVA 파일이 저장될 폴더 생성하기

- src/main/java/com/example/demo 폴더 하위에 config, controller, service, dao, model 폴더 생성

- config 폴더엔 DB 연동에 필요한 클래스가 들어갈 것

 

DataAccess 설정 파일 생성 (간략히만 설명)

- src/main/java/com/example/demo/config – DataAccessConfig.java 생성

- DataAccessConfig.java

package com.example.demo.config;

import javax.sql.DataSource;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

@Configuration
@PropertySource("classpath:/application.properties")          
public class DataAccessConfig {
	
	@ConfigurationProperties(prefix = "spring.datasource")    
	public DataSource dataSource() {
		return DataSourceBuilder.create().build();
	}
	
	@Bean
	public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception{
		SqlSessionFactoryBean factoryBean=new SqlSessionFactoryBean();
		
		factoryBean.setDataSource(dataSource);
		factoryBean.setMapperLocations(
				new PathMatchingResourcePatternResolver().getResources("classpath:/mapper/*.xml")
				);
		factoryBean.setTypeAliasesPackage("com.example.demo.model"); // DTO Alias 설정
		return factoryBean.getObject();
	}
	
	@Bean
	public SqlSessionTemplate sessionTemplate(SqlSessionFactory sqlSessionFactory) {
		return new SqlSessionTemplate(sqlSessionFactory);
	}
}

- root-context.xml 에서 설정했던 내용을 이 클래스 DataAccessConfig 와 application.properties 파일에서 설정

 

프로젝트 MyBatis01 : 환경설정 application.properties 파일 수정

- 대부분의 환경 설정을 이 파일에서 설정

# port 
server.port=80

# view resolver
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp

# oracle
spring.datasource.driverClassName=oracle.jdbc.driver.OracleDriver
spring.datasource.url=jdbc:oracle:thin:@localhost:1521:xe
spring.datasource.username=spring
spring.datasource.password=spring123

# mybatis
mybatis.config-location=classpath:mybatis-config.xml
mybatis.mapper-locations=classpath:mapper/*.xml

1. application.properties 에서 서버 포트번호를 설정

- 기본포트 8080 에서 80 으로 수정

2. prefix 와 suffix 를 설정

- View 파일들이 저장될 최상위 경로값 설정

- 이때 prefix 에 해당하는 폴더도 webapp 폴더 하위에 생성해야함

2. DB 연동을 위해 Oracle(또는 MySql) 연동시 필요한 정보를 설정

- 기존에 root-context.xml 에서 설정하던 내용을 여기서 설정

- 또한 com/example/demo/config/DataAccessConfig.java 에도 DB 연동 관련 내용이 들어감

3. MyBatis 연동을 위해 MyBatis 연동시 필요한 정보를 설정

- 기존에 root-context.xml 에서 설정하던 내용을 여기서 설정, configuration.xml 파일, Mapper 파일을 불러옴

+ Spring Boot 에서는 configuration.xml 파일에서 alias 값을 설정할 수 없으므로, configuration.xml 파일을 삭제해도 된다, 삭제시 configuration.xml 파일을 불러오는 코드도 삭제해야함

 

DB 연동 환경설정 내용이 들어가는 곳

1. DataAccessConfig.java 파일

2. application.properties

- 이 두가지가 맞물려서 돌아간다

 

Spring Boot 에서의 MyBatis 환경설정 파일 mybatis-config.xml

- Spring Boot 에서는 MyBatis 환경설정 파일인 mybatis-config.xml 에서 DTO 클래스의 alias 값을 설정해도 적용되지 않음

- DTO class 의 alias 는 DTO 클래스 위에서 @Alias 어노테이션으로 적용시켜야한다

- 여기서 설정한 alias 값을 Mapper 파일에서 resultType, parameterType 등에 사용함

+ Controller, Service, DAO 는 Spring 과 같다, DTO 에는 @Getter, @Setter, @Alias 등 어노테이션 사용

 

프로젝트 MyBatis01 : DataAccess 설정 파일 설명

- DataAccessConfig.java

package com.example.demo.config;

import javax.sql.DataSource;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

@Configuration
@PropertySource("classpath:/application.properties")          
public class DataAccessConfig {
	
	@ConfigurationProperties(prefix = "spring.datasource")    
	public DataSource dataSource() {
		return DataSourceBuilder.create().build();
	}
	
	@Bean
	public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception{
		SqlSessionFactoryBean factoryBean=new SqlSessionFactoryBean();
		
		factoryBean.setDataSource(dataSource);
		factoryBean.setMapperLocations(
				new PathMatchingResourcePatternResolver().getResources("classpath:/mapper/*.xml")
				);
		factoryBean.setTypeAliasesPackage("com.example.demo.model"); // DTO Alias 설정
		return factoryBean.getObject();
	}
	
	@Bean
	public SqlSessionTemplate sessionTemplate(SqlSessionFactory sqlSessionFactory) {
		return new SqlSessionTemplate(sqlSessionFactory);
	}
}

- root-context.xml 에서 3가지 bean 을 만들던 내용에 해당하는 내용이 DataAccess 설정 파일에 3개의 메소드로 들어감

- DataSource, SqlSessionFactory, SqlSessionTemplate 객체들이 순차적으로 생성되고 있다

 

@Configuration
@PropertySource("classpath:/application.properties")          
public class DataAccessConfig {

- 여기서 @ProperthSource 에 application.properties 환경 설정 파일을 불러온다

- 이걸 불러와야 application.properties 와 맞물려서 bean 객체 생성 가능

 

DataAccessConfig.java 의 dataSource() 메소드 부분

	@ConfigurationProperties(prefix = "spring.datasource")    
	public DataSource dataSource() {
		return DataSourceBuilder.create().build();
	}

- @configurationProperties(prefix = "spring.datasource") 는 application.properties 에서 DB 연동에 필요한 값을 설정할때는 spring.datasource 로 시작해야한다는 의미

- 그래서 application.properties 에서 DB 연동 관련된 내용은 spring.datasource 로 시작한다

- 이때 이 DB 연동 관련 내용이 있는 곳은 위의 @PropertySource 에서 지정한 곳, 즉 application.properties 파일을 의미

 

- application.properties 부분

 

- 기존 root-context 의 이 내용이다 (아래)

 

DataAccessConfig.java 의 sqlSessionFactory() 부분

	@Bean
	public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception{
		SqlSessionFactoryBean factoryBean=new SqlSessionFactoryBean();
		
		factoryBean.setDataSource(dataSource);
		factoryBean.setMapperLocations(
				new PathMatchingResourcePatternResolver().getResources("classpath:/mapper/*.xml")
				);
		factoryBean.setTypeAliasesPackage("com.example.demo.model"); // DTO Alias 설정
		return factoryBean.getObject();
	}

- SqlSessionFactoryBean 객체 factoryBean 을 생성하면서 Setter 메소드로 프로퍼티에 값을 세팅하고 있다

1. 위에서 만든 dataSource 객체를 세팅하고

2. Mapper 파일을 불러오는 코드

3. DTO 클래스가 저장된 패키지를 여기서 경로 설정해서 불러옴, 그것에 의해서 DTO 에서 @Alias 어노테이션 사용 가능

 

- 기존 root-context 의 이 내용이다 (아래)

 

DataAccessConfig.java 의 sessionTemplate() 부분

	@Bean
	public SqlSessionTemplate sessionTemplate(SqlSessionFactory sqlSessionFactory) {
		return new SqlSessionTemplate(sqlSessionFactory);
	}

- 기존 root-context 의 이 내용이다 (아래)

 

+  Controller, Service, DAO 이나 나머지 부분은 Spring Project 때와 과 같다, DTO 만 @Getter, @Setter, @Alias 등 어노테이션 사용

 

프로젝트 MyBatis01 : DTO 클래스 보기

- Board.java

package com.example.demo.model;

import java.util.Date;

import org.apache.ibatis.type.Alias;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Alias("board")
public class Board {
	private int no;
	private String name;
	private String subject;
	private String content;
	private Date register;
}

- Getter / Setter 메소드 대신 @Getter, @Setter 어노테이션 사용

- @Alias 어노테이션으로 해당 DTO 의 alias 를 설정함, 현재는 "board" 로 설정했다

- 이때 DataAccessConfig.java 파일에서 아래의 코드로 DTO 가 저장된 패키지를 지정하는 경로를 세팅해야, DTO 클래스에서 DTO Alias 를 설정 가능하고, 그 Alias 를 Mapper 파일에서 사용 가능

- DataAccessConfig.java 에서 Mapper 파일을 불러오고, DTO 클래스 위치도 불러오므로, Mapper 파일에서 DTO Alias 를 사용 가능한 것이다.

 

Alias 설정 방법

1. DTO 클래스 위 @Alias 어노테이션으로 설정

2. DataAccessConfig.java 에서 setTypeAliasesPackage() 로 DTO 클래스가 위치한 경로를 구해서 세팅해야한다

- 위 두가지가 충족되어야 DTO 에서 Alias 를 설정하고, Mapper 파일에서 그 Alias 사용 가능

 

- 나머지 흐름, 코드는 Spring 과 같은므로 설명 생략

 

로젝트 MyBatis01 : 완성된 프로젝트 실행

 

Spring Boot 환경설정 파일 정리

1. pom.xml

2. Mapper 파일

3. application.properties

4. DataAccess 설정 파일 클래스


Spring Boot - MyBatis 연동 예제 2 : 프로젝트 sbboard

- Spring 으로 만들었던 예제 프로젝트 springboard 를 Spring Boot 으로 다시 만들기
- 프로젝트 springmember 설명 : https://laker99.tistory.com/m/150

 

프로젝트 sbboard: 실습 준비

- Spring 실습시에 생성했던 board53 테이블을 사용하므로 테이블 생성할 필요 없이 바로 실행해도 연동 된다

 

프로젝트 sbboard: 프로젝트 실행

 

파일들 살펴보기 : pom.xml

- 현재는 타임리프가 아닌 JSTL 로 출력할것이므로 JSTL 라이브러리가 추가되어있다

- ojdbc8 을 사용함

 

파일들 살펴보기 : application.properties

# port 
server.port=80

# view resolver
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp

# oracle
spring.datasource.driverClassName=oracle.jdbc.driver.OracleDriver
spring.datasource.url=jdbc:oracle:thin:@localhost:1521:xe
spring.datasource.username=spring
spring.datasource.password=spring123

# mybatis
# mybatis.config-location=classpath:mybatis-config.xml
# mybatis.mapper-locations=classpath:mapper/*.xml

# DTO Alias
# mybatis.type-aliases-package=com.example.demo.model

1. 포트 설정

- 실행방법에 따라 다른 아파치 톰캣이 동작한다

- 우리가 설정한 아파치 톰캣이 있고, Spring Boot 엔 내장된 아파치 톰캣이 있다

1) 브라우저 창을 열어서 localhost 나 ip 주소로 요청하면 application.propeties 위에서 설정한 포트가 내부포트로 동작

- 기본 포트는 8080, 우리가 application.properties 에서 80 으로 변경시킨 것이다

2) 반면 Run As - Run on Server 로 실행하면 우리가 설치한 아파치 톰캣으로 실행한다

- 우리가 아파치 톰캣 설치시 설정했던 포트번호로 실행된다

 

2. VIewResolver

+ 타임리프

- 나중에 타임리프를 쓸때는 View 파일을 저장할때 webapp 를 사용하지 않고 resources/templates 폴더 하위에 저장된다

- 나중에 타임리프를 쓸때는 JSP 파일이 아닌 HTML 파일로 View 를 만들어야한다

3. 오라클 연결

- spring.datasource 는 DataAccessConfig.java 에서 설정한 prefix 값이다

4. MyBatis 연결

- Spring Boot 에서 mybatis-config.xml 은 아무 역할을 하지 않으므로, mybatis-config.xml 을 불러오는 코드는 삭제해도 됨

- DataAccessConfig.java 파일에서 Mapper 파일을 불러오고 있으므로 application.properties 에서 Mapper 파일을 불러오는 코드는 삭제해도 됨

5. DTO Alias

- DTO Alias 는 DTO 에서 설정하고, 그 DTO 경로를 DataAccessConfig.java 에서 불러오고 있으므로 여기에서 Alias 를 설정할 필요가 없다

 

프로젝트 sbboard: DataAccess 설정 파일

- DataAccessConfig.java

package com.example.demo.config;

import javax.sql.DataSource;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

@Configuration
@PropertySource("classpath:/application.properties")          
public class DataAccessConfig {
	
	@ConfigurationProperties(prefix = "spring.datasource")    
	public DataSource dataSource() {
		return DataSourceBuilder.create().build();
	}
	
	@Bean
	public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception{
		SqlSessionFactoryBean factoryBean=new SqlSessionFactoryBean();
		
		factoryBean.setDataSource(dataSource);
		factoryBean.setMapperLocations(
				new PathMatchingResourcePatternResolver().getResources("classpath:/mapper/*.xml")
				);
		factoryBean.setTypeAliasesPackage("com.example.demo.model"); // DTO Alias 설정
		return factoryBean.getObject();
	}
	
	@Bean
	public SqlSessionTemplate sessionTemplate(SqlSessionFactory sqlSessionFactory) {
		return new SqlSessionTemplate(sqlSessionFactory);
	}
}

1. @ConfigurationProperties 어노테이션에서 DB연동과 관련된 prefix 를 "spring.datasource" 로 지정

2. 그 prefix 를 application.properties 에서 사용하고 그 DB 연동 관련 정보를 여기서 불러와서 dataSource 객체 생성

3. sqlSEssionFactory() 메소드의 코드 factoryBean.setDataSource(dataSource); 가 실제로 DB 연동하는 코드이다

4. MyBatis 환경설정 파일 Configuration.xml 은 사용되지 않으므로, 여기서도 설정하지 않음, Mapper 파일 경로만 설정한다

- 경로를 "classpath:/mapper/*.xml" 로 했으므로 Mapper 파일은 resources 폴더 하위 mapper 폴더 하위에 있어야한다

5. DTO 클래스가 저장될 패키지를 지정해야, 그 DTO 클래스에서 @Alias 로 Alias 값을 설정가능하고, 설정한 Alias 값을 Mapper 파일에서 사용 가능

6. sessionTemplate() 메소드에서 SqlSessionTemplate 객체를 생성하고 있다

 

+ application.properties 부분

 

프로젝트 sbboard: DTO 클래스

- BoardBena.java

package com.example.demo.model;

import java.util.Date;

import org.apache.ibatis.type.Alias;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Alias("board")
public class BoardBean {

	private int board_num;
	private String board_name;
	private String board_pass;
	private String board_subject; //글제목
	private String board_content; //글내용
	private int board_re_ref; //글그룹번호
	private int board_re_lev; //화면에 보이는 답변글 위치번호
	private int board_re_seq; //답변글 레벨 순서
	private int board_readcount; //조회수
	private String board_date; //글 등록날짜
	
	
}

- Lombok 라이브러리를 추가했으므로 Lombok 기능으로 Getter / Setter 메소드 대신 @Getter, @Setter 어노테이션 사용

 

static 폴더

- Spring Boot 에서는 static 폴더가 지원되고, 이 폴더 하위에 저장된 파일들은 공유된다

- 다른 폴더에서 static 폴더 하위의 폴더 순으로 경로를 설정하면 쉽게 파일을 가져올 수 있다

- 주로 static 폴더 하위에 CSS 파일, JS 파일, 이미지 등을 저장함

ex) 목록페이지 board_list.jsp 에서 CSS 파일 불러오는 부분

<head>
	<meta charset="UTF-8">
	<title>게시판 목록</title>
    <link rel="stylesheet" href="./css/bbs.css" type="text/css">
</head>

- 불러올 경로를 지정할땐 static 폴더 하위의 폴더 순으로 불러오면 쉽게 불러올 수 있다

ex) href="./css/bbs.css" 는 static 폴더 하위의, css 폴더 하위의 bbs.css 파일을 불러오라는 의미이다

+ static 폴더는 resources 폴더 하위에 있다

 

- 그 외는 Spring 프로젝트 springboard 부분을 보기

- 위의 환경설정을 제외한 나머지는 Spring 프로젝트 springboard 와 같은 내용이다

https://laker99.tistory.com/m/150

 

코딩 76 일 / 2022.10.12 / Spring MVC 회원관리 프로그램, Spring MVC 댓글 게시판 프로그램

복습 파일명 중복문제 해결 - fileupload 라이브러리 사용시 중복문제를 자동 해결해주지 않음 - UUID (java.util) 클래스는 통해 문자형태의 난수값을 만들어 준다 - 이 난수값을 String 형으로 변환하고

laker99.tistory.com


Spring Boot - MyBatis 연동 예제 3 : 프로젝트 sbmember

- Spring 으로 만들었던 예제 프로젝트 springmember 를 Spring Boot 으로 다시 만들기

- 프로젝트 springmember 설명 : https://laker99.tistory.com/m/149

- Spring Boot 에서는 인터셉터 설정, 첨부파일 업로드 방법이 달라진다, 그 부분을 보기

 

프로젝트 sbmember: 실습 준비

- Spring 실습시에 생성했던 join_member 테이블을 사용하므로 테이블 생성할 필요 없이 바로 실행해도 연동 된다

 

Spring Boot 프로젝트에서의 인터셉터(Intercepter) 설정

- servlet-context.xml 파일에서 설정했던 인터셉터 설정과 매핑을 Spring Boot 에서는 JAVA 클래스 인 WebMvcConfig. java 에서 인터셉터 설정

- JAVA 클래스인 SessionCheckInter.java 가 인터셉터 구현 클래스이다.

+ root-context.xml 파일의 내용은 JAVA 클래스인 DataAccessConfig.java 에서 설정했다

 

파일들 살펴보기 : pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.3.10.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>sbmember</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>war</packaging>
	<name>sbmember</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>1.8</java.version>
	</properties>
	<dependencies>
		<!-- jsp 파일을 사용하기 위한 의존 라이브러리-->
		<dependency>
			<groupId>org.apache.tomcat.embed</groupId>
			<artifactId>tomcat-embed-jasper</artifactId>
			<scope>provided</scope>
		</dependency>
		
		<!-- jstl -->
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>jstl</artifactId>
			<version>1.2</version>
		</dependency>	
		
		<!-- EMail -->
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-email</artifactId>
			<version>1.3.3</version>
		</dependency>
		
		<!-- fileupload -->
		<dependency>
			<groupId>commons-fileupload</groupId>
			<artifactId>commons-fileupload</artifactId>
			<version>1.2</version>
		</dependency>
	
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>2.1.4</version>
		</dependency>

		<dependency>
			<groupId>com.oracle.database.jdbc</groupId>
			<artifactId>ojdbc8</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-tomcat</artifactId>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

- pom.xml 부분

		<!-- EMail -->
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-email</artifactId>
			<version>1.3.3</version>
		</dependency>
		
		<!-- fileupload -->
		<dependency>
			<groupId>commons-fileupload</groupId>
			<artifactId>commons-fileupload</artifactId>
			<version>1.2</version>
		</dependency>

- 이메일을 보내기 위한 commons-email 과, 첨부파일 업로드를 위한 commons-fileupload 라이브러리 추가

 

파일들 살펴보기 : Mapper 파일 member.xml

- Spring 프로젝트 springboard 와 Mapper 파일 내용은 같다

 

파일들 살펴보기 : JAVA 파일들

- DataAccessConfig.java : root-context.xml 에서 bean 객체를 생성하는 내용이 이 클래스에 들어가있다


Spring Boot 의 인터셉터 처리하는 클래스

1. SessionChekInter.java : 인터셉터 구현 클래스, HandlerInterceptorAdapter 클래스를 상속

- Spring 프로젝트의 인터셉터 구현 클래스와 비슷하지만 Spring Boot 의 인터셉터 구현 클래스는 @Configuration 어노테이션이 붙는다는 점이 다르다

2. WebMvcConfig.java : servlet-context.xml 에서 인터셉터 매핑을 잡았던 내용이 이 클래스에 들어가있다

 

- SessionCheckInter.java

package com.example.demo.controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

@Configuration
public class SessionCheckInter extends HandlerInterceptorAdapter {
	
	// preHandle(request,response,handler)메소드
	// 1.Controller에서 요청(*.do)을 받기 전에  preHandle()가 호출되어 가로채는 역할로 사용
	// 2.로그인 하지않고(세션이 없으면) 요청하면 로그인 폼으로 이동 하도록 해주는 역할
	public boolean preHandle(HttpServletRequest request, 
			HttpServletResponse response, Object handler) throws Exception {
		HttpSession session = request.getSession();
		String id = (String)session.getAttribute("id");
		if (id == null || id.equals(""))  {		
			response.sendRedirect("member_login.do");	// 세션이 없으면 로그인 폼으로 이동
			return false;
		}
		return true;
	}
}

- Spring Boot 프로젝트의 인터셉터 구현 클래스는 @Configuration 어노테이션이 붙는다

- 그리고 WebMvcConfig 클래스에서 인터셉터 매핑을 잡는다

 

- WebMvcConfig.java

package com.example.demo.controller;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class WebMvcConfig implements WebMvcConfigurer {
	@Autowired
	private SessionCheckInter interceptor;

	// 인터셉터가 동작할 url 패턴 등록
	@Override
	public void addInterceptors(InterceptorRegistry registry) {
		registry.addInterceptor(interceptor)
				.addPathPatterns("/member_edit.do")
				.addPathPatterns("/member_edit_ok.do")
				.addPathPatterns("/member_del.do")
				.addPathPatterns("/member_del_ok.do")
				.addPathPatterns("/member_logout.do");
	}

}

- servlet-context.xml 에서 인터셉터 매핑을 잡았던 내용이 이 클래스에 들어가있다

1. 인터페이스 WebMvcConfigurer 를 상속받아서 구현한다

2. @Autowired 어노테이션으로 인터셉터 구현 클래스인 SessionCheckInter 객체를 주입받는다

3. addInterceptor() 메소드를 오버라이딩 함

4. addPathPatterns() 메소드로 매핑을 잡을 요청명을 추가함

- 로그인 된 경우에만 수행되어야하는 기능인 수정, 삭제, 로그아웃 에 대한 매핑을 잡아뒀다, 


Spring Boot 의 DB 연동 하는 클래스

1. DataAccessConfig.java : root-context.xml 에서 bean 객체를 생성하는 내용이 이 클래스에 들어가있다

2. application.properties : DB 연동을 위한 정보들이 있는 파일

 

- DataAccessConfig.java

package com.example.demo.config;

import javax.sql.DataSource;

import org.apache.ibatis.session.SqlSessionFactory;
import org.mybatis.spring.SqlSessionFactoryBean;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.boot.jdbc.DataSourceBuilder;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;

@Configuration
@PropertySource("classpath:/application.properties")          
public class DataAccessConfig {
	
	@ConfigurationProperties(prefix = "spring.datasource")    
	public DataSource dataSource() {
		return DataSourceBuilder.create().build();
	}
	
	@Bean
	public SqlSessionFactory sqlSessionFactory(DataSource dataSource) throws Exception{
		SqlSessionFactoryBean factoryBean=new SqlSessionFactoryBean();
		
		factoryBean.setDataSource(dataSource);
		factoryBean.setMapperLocations(
				new PathMatchingResourcePatternResolver().getResources("classpath:/mapper/*.xml")
				);
		factoryBean.setTypeAliasesPackage("com.example.demo.model"); // DTO Alias 설정
		return factoryBean.getObject();
	}
	
	@Bean
	public SqlSessionTemplate sessionTemplate(SqlSessionFactory sqlSessionFactory) {
		return new SqlSessionTemplate(sqlSessionFactory);
	}
}

- 이 클래스에서 bean 객체를 차례로 생성하면서 DB 연결을 하고 있다

- Mapper 의 Location 을 여기서 설정 했다

 

- application.properties

# port 
server.port=80

# view resolver
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp

# oracle
spring.datasource.driverClassName=oracle.jdbc.driver.OracleDriver
spring.datasource.url=jdbc:oracle:thin:@localhost:1521:xe
spring.datasource.username=spring
spring.datasource.password=spring123

# mybatis
mybatis.config-location=classpath:mybatis-config.xml
mybatis.mapper-locations=classpath:mapper/*.xml

# fileupload
spring.servlet.multipart.enabled:true
#spring.servlet.multipart.maxFileSize=50MB
#spring.servlet.multipart.maxRequestSize=50MB
spring.servlet.multipart.max-request-size:10MB
spring.servlet.multipart.max-file-size:20MB

- # mybatis 아래의 두 코드는 삭제해도 된다, Mapper 파일의 경로 설정은 이미 DataAccessConfig.java 에서 했고, mabatis-config.xml 파일은 아무 기능이 없다

- # fileupload 아래의 코드에서 업로드할 파일의 크기를 제한하고 있다, 하지만 여기서만 파일크기를 제한하면 더 큰 파일을 업로드하려할때 오류를 발생시킨다

- 파일 크기 제한은 Controller 클래스에서 해야한다

- MemberController.java 부분


Spring Boot 의 Alias 설정하는 파일

1. DataAccessConfig.java 에서 DTO 가 저장된 패키지의 경로를 설정

2. 개별 DTO 클래스 위에 @Alias 사용


Spring Boot 의 기능별 설정 파일 정리

- 인터셉터 처리 파일 : SessionCheckInter.java + WebMvcConfig.java 

- DB 연동 처리 파일: application.properties + DataAccessConfig.java

- Alias 설정 파일 : DataAccessConfig.java + 개별 DTO 클래스


static 폴더

- Spring Boot 에서 지원

- static 폴더 안에 들어가는 파일은 공유가 된다

- CSS, JS, 이미지 등 View 파일에서 사용할 파일들을 여기 저장

- 절대경로로 static 폴더 하위폴더부터 순서대로 작성해서 불러올 수 있다


프로젝트 sbmember : DTO 클래스

- MemberBean.java

package com.example.demo.model;

import org.apache.ibatis.type.Alias;

import lombok.Getter;
import lombok.Setter;

@Getter
@Setter
@Alias("member")
public class MemberBean {

	private int join_code;
	private String join_id;
	private String join_pwd;
	private String join_name;
	private String join_zip1;
	private String join_zip2;
	private String join_addr1;
	private String join_addr2;
	private String join_tel;
	private String join_phone;
	private String join_email;
	private String join_profile;
	private String join_regdate;
	private int join_state;
	private String join_delcont;
	private String join_deldate;
	
}

- @Getter, @Setter, @Alias 사용

- @Getter, @Setter 어노테이션을 쓰기 위해 pom.xml 에 lombok 라이브러리를 추가했다

- @Alias 어노테이션은 MyBatis 지원 클래스인 Alias 를 import 해야 사용가능하다

- DataAccessConfig.java 에서 DTO 경로를 설정했고, Mapper 파일도 불러오므로 여기서 설정한 @Alias("member") 를 Mapper 파일에서 사용 가능하다

 

- DataAccessConfig.java 부분


프로젝트 sbmember : 프로젝트 실행


Spring Boot 의 요청

- 요청이름값에서 확장자 (ex) .do) 는 있어도 되고 없어도 된다

- Spring Boot 에서는 Dispatcher Servlet 매핑을 하지 않으므로, 모든 요청이 Dispatcher Servlet 으로 간다

- 그래서 Spring Boot 에서는 Contorller 의 @RequestMapping 과 같은 이름값인 한 무조건 Controller 클래스까지 찾아간다


static 폴더 안의 JS 파일 불러오기

- member_join.jsp 부분

- Spring 에서는 Controller 클래스를 거치며 왔다갔다 이동하는 동안 경로가 계속해서 바뀌어서 절대경로만으로 경로를 설정했다

- Spring Boot 에서는 static 폴더에 CSS, JS, 이미지 등을 넣고, static 폴더 하위의 폴더부터 경로를 작성함으로서 쉽게 경로를 설정 가능

ex) 위 코드에서 ./js/member.js 로 static/js 하위의 member.js 파일을 불러오고 있다


- 그 외는 Spring 프로젝트 springmember 부분을 보기

- 위의 환경설정을 제외한 나머지는 Spring 프로젝트 springmember 와 같은 내용이다

https://laker99.tistory.com/m/149

 

코딩 75 일 / 2022.10.11 / Spring MVC 게시판 프로젝트, 이메일 전송하기, Spring MVC 회원관리 프로그램

게시판 직접 만들어보기 (이어서) 환경 설정 파일 작성(세팅) 순서 1) pom.xml 2) web.xml 3) servlet-context.xml 4) configuration.xml 5) board.xml 6) root-context.xml - 4), 5), 6) 은 DB 연동 관련 내용 - configuration.xml, board,x

laker99.tistory.com


 

웹 소켓 기능

- 접속해 있는 유저들끼리 채팅 가능한 기능

- Spirng 외 다른 언어로도 웹 소켓 구현 가능하다

 

웹 소켓 기능 쓰기 위해 추가해야할 환경설정 파일의 코드

1. pom.xml : 웹 소켓 라이브러리

- spring-websocket

2. servlet.xml : Handler 매핑 잡기

 

웹 소켓 예제 : 프로젝트 webSock

실습 준비

 

파일들 살펴보기 : pom.xml

- pom.xml 부분

		<!-- WebSocket -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-websocket</artifactId>
			<version>${org.springframework-version}</version>
		</dependency>

- 웹소켓 라이브러리가 추가되어야 웹 소켓으로 통신 가능

 

파일들 살펴보기 : web.xml

- *.do 로 매핑을 잡아뒀다

- 그 외는 기존 내용들과 같다

 

파일들 살펴보기 : servlet-context.xml

- 다른 내용들이 들어가있다

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:beans="http://www.springframework.org/schema/beans"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:websocket="http://www.springframework.org/schema/websocket"
	xsi:schemaLocation="http://www.springframework.org/schema/websocket http://www.springframework.org/schema/websocket/spring-websocket-4.1.xsd
		http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
	
	<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
	<!-- Enables the Spring MVC @Controller programming model -->
	<annotation-driven />
	
	<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
	<resources mapping="/resources/**" location="/resources/" />
	
	<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
	<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<beans:property name="prefix" value="/WEB-INF/views/" />
		<beans:property name="suffix" value=".jsp" />
	</beans:bean>	
	
	<context:component-scan base-package="com.ch.webSock" />
	
	<default-servlet-handler/>
	<websocket:handlers>
		<websocket:mapping handler="chatHandler" path="chat-ws.do"/>
	</websocket:handlers>
	<beans:bean id="chatHandler" class="com.ch.webSock.WebChatHandler"/>
		
</beans:beans>

- base-package 와 View 파일 저장 위치인 ViewResolver 는 기존과 같다

- base-package 로 지정된 패키지 하위에 HomeController.java, WebChatHandler.java 가 있다

- ViewResolver 로 지정된 패키지 하위에 chat.jsp, header.jsp 가 있다

 

Handler 매핑 잡기

- 어떤 요청으로 들어올때 웹 소켓을 연결시킬지 매핑을 잡아야한다

- 웹 소켓 기능 처리 위해 만든 클래스인 WebChatHandler 가 웹 소켓 연결, 처리 역할을 한다

- 이 클래스는 필요한 클래스를 상속받아 내가 만든 클래스이다

- 이 클래스로 찾아가기 위해 매핑을 잡아준다, 현재는 "chat-ws.do" 로 요청할때만 chatHandler 라는 id 값과 매핑되어 이 클래스가 동작

 

com.ch.webSock.WebChatHandler 클래스

- WebChatHandler.java

package com.ch.webSock;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

import org.springframework.web.socket.CloseStatus;
import org.springframework.web.socket.TextMessage;
import org.springframework.web.socket.WebSocketSession;
import org.springframework.web.socket.handler.TextWebSocketHandler;

public class WebChatHandler extends TextWebSocketHandler { 
	Map<String, WebSocketSession> users = new HashMap<String, WebSocketSession>();
	public void afterConnectionEstablished(WebSocketSession session) throws Exception {
		users.put(session.getId(), session);
	}
	public void afterConnectionClosed(WebSocketSession session, CloseStatus status) throws Exception {
		users.remove(session.getId());
	}
	protected void handleTextMessage(WebSocketSession session, 
			TextMessage message) throws Exception {
		String msg = message.getPayload();
		TextMessage tMsg = new TextMessage(msg.substring(4));
		Collection<WebSocketSession> list = users.values();
		for (WebSocketSession wss : list) {
			wss.sendMessage(tMsg);
		}
	}
}

- 내가 만든 클래스이다

- TextWebSocketHandler 클래스를 상속받아야한다

- afterConnectionEstablished(), afterConnectionClosed(), handleTextMessage() 메소드를 오버라이딩 해야한다

- afterConnectionEstablished() 메소드 : 연결이 된 후 어떤 일을 수행할지 작성

- afterConnectionClosed() 메소드 : 연결이 끊긴 후, 주로 연결을 사겢함

- handleTextMessage() 메소드 : 특정 유저가 전송한 메세지를 다른 유저에게 전송해주는 역할

 

코드 흐름

- HomeController 에서 요청을 받고, chat.jsp 에서 메세지 관련, 소켓 관련 처리를 한다

 

파일들 살펴보기 : root-context.xml

- DB연동을 하지 않으므로 root-context.xml 이 비어있다


흐름 설명

- index.jsp 를 실행

- 브라우저를 바꿔서 다시 실행해서 두개의 브라우저를 열기

- 서로 메세지를 주고 받을 수 있음

+ views/chat.jsp 파일에서 80 포트로 맞춰뒀으므로 80포트를 써야함


<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
    
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<script type="text/javascript">
	location.href="chat.do";
</script>
</body>
</html>

- Dispatcher Servlet -> HomeController 클래스

 

- Controller 클래스 HoemController.java

package com.ch.webSock;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class HomeController {
	@RequestMapping("chat.do")
	public String home() {
		return "chat";
	}	
}

- chat.jsp 로 이동

- 채팅하기 위한 페이지인 chat.jsp 로 이동

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ include file="header.jsp"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<script type="text/javascript">
	var websock;
	$(function() {
		$('#message').keypress(function(event) {
			var keycode = event.keyCode ? event.keyCode : event.which;
			if (keycode == 13)
				send();
			event.stopPropagation();
		});
		$('#enterBtn').click(function() {
			connect();
		});
		$('#exitBtn').click(function() {
			disconnect();
		});
		$('#sendBtn').click(function() {
			send();
		});
	});
	function send() {
		var nickName = $('#nickName').val();
		var msg = $('#message').val();
		websocket.send('msg:' + nickName + ' => ' + msg);
		$('#message').val('');
	}
	function connect() {
		websock = new WebSocket("ws://localhost:80/webSock/chat-ws.do");
		websock.onopen = onOpen;
//		websock.onclose = onClose;
		websock.onmessage = onMessage;
	}
	function disconnect() {
		websock.close();
	}
	function send() {
		var nickname = $('#nickName').val();
		var message = $('#message').val();
		websock.send('msg:' + nickname + ' : ' + message);
		$('#message').val('');
	}
	function onOpen(event) {
		appendMessage("연결되었습니다.");
	}
	function onClose(event) {
		appendMessage("연결이 종료되었니다.");
	}
	function onMessage(event) {
		var data = event.data;
		appendMessage(data);
	}
	function appendMessage(msg) {
		$('#chatMessageArea').append(msg + "<br>");
		var chatAreaheight = $('#chatArea').height();
		var maxscroll = $('#chatMessageArea').height() - chatAreaheight;
		$('#chatArea').scrollTop(maxscroll);
	}
</script>
</head>
<body>
	<div class="container">
		별명 : <input type="text" id="nickName"> 
		     <input type="button" value="입장" id="enterBtn" class="btn btn-success"> 
		     <input	type="button" value="퇴장" id="exitBtn" class="btn btn-danger">
		     
			 <h2 class="text-primary">대화영역</h2>
			 <input type="text" id="message" required="required"> 
			 <input	type="button" value="전송" id="sendBtn" class="btn btn-info">
			 <div id="chatArea">
				<div id="chatMessageArea"></div>
			 </div>
	</div>
</body>
</html>

-  가장 위에서 jQuery 함수로 이벤트 처리를 하고 있다

- send(), connect(), disconnect(), send(), onOpen(), onClose(), onMessage(), appendMessage() 메소드들

- 별명을 입력하기 위한 양식 폼이 아래에 있다

+ header.jsp 파일에서 각종 라이브러리를 불러오고 있다

- 별명 입력 양식과 대화영역 입력 양식, 버튼들의 id 값들을 자세히 보기

- id 가 "chatMessageArea" 인 div 영역에 대화내용들이 나올 것

 

위 코드 나눠서 캡처 설명

-  가장 위에서 jQuery 함수로 이벤트 처리를 하고 있다

- message 는 메세지를 입력하기 위한 양식, 엔터키를 누르면 아래 내용이 실행됨

+ keypress : 키를 눌렀을때 발생하는 이벤트

- keypress 이벤트 발생시 사용자가 누른 키 코드 keycode 를 가져온다, 13 번은 아스키코드로 "ENTER" 키이다.

- 즉 ENTER 키를 눌렀을때 send() 메소드를 호출해서 메소드를 전송시켜라는 의미

- 즉 메세지 입력 후 ENTER 키를 눌러도 send() 에 의해 메세지가 전송되고, "전송" 버튼을 눌러도 send() 에 의해 메세지가 전송된다

- enterBtn 은 "입장" 버튼, exitBtn 은 "퇴장" 버튼, sendBtn 은 "전송" 버튼, 각 버튼을 불러와서 클릭시 아래의 메소드를 실행

 

- chat.jsp 에서 connect() 메소드 부분

	function connect() {
		websock = new WebSocket("ws://localhost:80/webSock/chat-ws.do");
		websock.onopen = onOpen;
//		websock.onclose = onClose;
		websock.onmessage = onMessage;
	}

- 웹 소켓을 연결하고 있다

- WebSocket 객체를 생성하고 전역변수 websock 로 객체를 받느다

- WebSocket 객체를 생성할때 ip 주소와 포트번호까지 작성, 현재는 localhost 이고 포트번호는 80 포트로 접속

- WebSocket 객체를 생성할때 "chat-ws.do" 로 요청, 이 Handler 매핑은 servlet-context.xml 에서 잡아뒀다

- "chat-ws.do" 로 요청시 servlet-context.xml 에서 매핑이 잡혀서 WebChatHanlder 클래스가 동작한다

+ WebChatHandler 클래스에선 회원의 id 값과 WebSocketSession 세션을 put() 으로 추가해서 맵 users 에 저장함

- 아래에 정의된 onOpen() 메소드를 호출해서 appendMessage() 로 "연결되었습니다" 메세지를 출력 

	function onOpen(event) {
		appendMessage("연결되었습니다.");
	}

 

+ WebChatHandler.java 부분

 

- chat.jsp 에서 send() 메소드 부분만

	function send() {
		var nickName = $('#nickName').val();
		var msg = $('#message').val();
		websocket.send('msg:' + nickName + ' => ' + msg);
		$('#message').val('');
	}

- 사용자가 별명 입력창에 작성한 닉네임과, 대화영역에서 사용자가 입력한 대화내용을 가져옴

- 구해온 WebSocket 객체 websocket 으로 send() 메소드를 사용

- 그럼 WebChatHanlder.java 에서 handleTextMessage() 메소드가 호출되어, 목록을 가진 사용자들에게 메세지를 전송

- WebChatHanlder.java 에서 handleTextMessage() 부분만

- 메세지를 가져오고, 모든 유저를 가져와서 리스트에 저장한 후, for 문을 통해 모든 유저에게 메세지를 뿌림

 

- chat.jsp 에서 disconnect() 메소드 부분만

	function disconnect() {
		websock.close();
	}

- websocket.close() 로 연결을 끊는다

- close() 가 실행되면 WebChatHanlder 의 afterConenctionClosed() 가 호출된다

 

- WebChatHandler.java 에서 afterConnectionClosed() 부분만

- 세션을 지움

- 퇴장 버튼을 눌렀던 유저만 users 에서 빠지는 것

 

결과

- 글을 입력하고 "전송" 을 누르면 브로드캐스팅 하듯이 접속되어있는 모든 사람에게 메세지를 전송한다

- 유저가 여러명이어도 가능


Spring Boot

Spring Boot 특징

- 독립 실행이 가능한 스프링 애플리케이션 개발 가능(Tomcat, Jetty 내장)

- Tomcat 을 설치하지 않아도 자동으로 Tomcat 이 내장되어있으므로 서비스 가능
- 통합 Starter를 이용하여 프로젝트를 만든다

- 통합 Starter 를 이용하여 Maven/Gradle 로 라이브러리 관리
- 통합 Starter를 통한 자동화된 스프링 설정 제공
- 번거로운 XML 설정을 요구하지 않음, XML 파일들이 많이 빠진다

ex) web.xml, servlet-context.xml, root-context.xml 파일 이 없다

- 새로운 환경설정 파일이 하나 만들어지고, 거기에 직접 필요한 환경을 구축해야함

- Spring Actuator 제공 (애플리케이션의 모니터링과 관리를 위해서 사용)

 

Spring Boot 라이브러리 관련 환경설정 파일

- 통합 Starter 를 이용하여 Maven/Gradle 로 라이브러리 관리

- Maven 으로 환경설정시 pom.xml 파일이 생성됨, 체크해서 라이브러리 설치 가능

- Gradle 으로 환경설정시 Gradle 환경설정 파일이 생성됨

 

Spring Boot 환경 구축 방법

1. Eclipse 에 STS 3.x plug-in 추가 또는 STS 를 사용해야한다

- 현재는 STS 3 점대를 설치된 상태이므로 Spring, Spring Boot 프로젝트 모두 생성 가능

+ STS 4 점대는 Spring 프로젝트를 만들 수 있는 메뉴가 없다, Spring Boot 로 넘어가는 추세

 

2. [File] - New - Project

- Spring Starter Project 선택

- Type 에서 라이브러리 관리 방법 선택, Gradle / Maven 중 현재는 "Maven" 선택

+ Gradle 선택시 Gradle 로 라이브러리 관리하는 패키지 생성

- Packaging 에서는 압축 포맷 선택, War / Jar 중 현재는 "War" 선택

- Java Version 은 현재 사용하는 JAVA 버전인 8 을 선택

- Language 는 JAVA 선택

- Package 가 com.example.demo 로 되어있다, 이게 Spring 의 top-level 패키지와 같은 역할

- 이 패키지 com.example.demo 가 java 폴더 하위에 생성된다

- 이 패키지 하위에 Controller 등의 자바 클래스들이 오게 된다

+ demo 는 현재 프로젝트명

- Maven 을 선택했으면, Maven 환경설정 파일 (pom.xml) 안에 여기서 선택한 의존 라이브러리들이 추가됨

- Gradle 을 선택했으면, Gradle 환경설정 파일안에 여기서 선택한 의존 라이브러리들이 추가됨

- 기본적으로 선택해야할 라이브러리 : Spring Web 을 선택해야 Spring MVC 패턴으로 만들어짐

 

+ 다른 라이브러리들

- Lombok 라이브러리 : DTO 클래스 안에 정해진 필드의 Getter / Setter 메소드를 어노테이션 기반으로 쓰기 위해서 사용되는 라이브러리, 이걸 쓰면 Getter / Setter 메소드를 쓰지 않아도 된다, 나중에 사용할 라이브러리

- WebSocket 라이브러리 : 체크하면 WebSocket 라이브러리가 추가됨

- NoSql 카테고리 라이브러리들 : 체크하면 Spring Boot - NoSQL 연동

- SQL 카테고리 라이브러리들 : DB 연결시 필요한 라이브러리

- MyBatis Framework 라이브러리 : 체크하면 Spring Boot - MyBatis 연동

- Oracle Driver 라이브러리 : 오라클 연동시 Oracle Driver (오라클용 JDBC 드라이버) 체크

- [boot] 가 붙은 것은 Spring Boot 프로젝트임을 의미

- 프로젝트를 생성하며 체크했던 라이브러리들이 Maven 환경설정 파일인 pom.xml 에 들어간다

- web.xml, servlet-context.xml, root-context.xml 환경설정 파일들이 없다

 

Spring Boot 프로젝트 실행 방법

- 프로젝트를 오른쪽 마우스 클릭 -> Run As -> Spring Boot App 로 실행해야한다

- 아래와 같이 출력되면 실행 성공

 

파일들 살펴보기 : pom.xml

- Maven 을 선택했으므로 현재 프로젝트 하위에 Maven 환경설정 파일 pom.xml 이 만들어짐

- pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.7.4</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>demo</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>war</packaging>
	<name>demo</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>1.8</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-tomcat</artifactId>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

- 프로젝트 생성 시 Spring Web 을 선택했으므로 spring-boot-starter-web, spring-boot-starter-tomcat 라이브러리 등이 들어가있다

 

Spring 에는 있지만 Spring Boot 에는 없어진 환경설정 파일

- web.xml : webapp 폴더 가 비어있다, 즉 WEB-INF 폴더도 없고, 그 안에 있어야할 web.xml 파일도 없다

- web.xml 에서 불러왔던 spring 환경설정 파일 2개인 servlet-context.xml, root-context.xml 파일도 없다

- Spring Boot 프로젝트에서는 web.xml, servlet-context.xml, root-context.xml 파일이 없다

- Spring 의 6개 환경설정 파일 중에서 pom.xml, Mapper 파일(.xml) 만 Spring Boot 에 있다, Configuration.xml 파일도 필요없다

 

Spring 에는 없지만 Spring Boot 에서 새로 생긴 환경설정 파일 : application.properties
- application.properties 파일에 직접 환경설정을 해야한다
- 없어진 3개의 파일의 환경설정 내용을 여기에 작성해야함
- 프로젝트 생성 직후 application.properties 에는 아무 내용없다

 

Spring 에는 없지만 Spring Boot 에서 새로 생긴 폴더 설명

 

resources폴더 하위 static 폴더

- CSS, JS, 이미지(images) 디자인 관련 파일들을 저장함

- 공유가 되는 폴더, 이 폴더에 저장되면 쉽게 CSS, JS, 이미지들을 불러올 수 있다

- 이 폴더에 있는 파일은 다른 페이지에서 쉽게 불러올 수 있다

+ Python 의 장고 프로젝트가 Spring Boot 프로젝트와 비슷한 구조, 장고에도 static 폴더가 있다

 

resources 폴더 하위 templates 폴더

- EL, JSTL 대신하는게 타임리프, EL, JSTL 대신 타임리프 지원 태그들을 사용

- 타임리프를 쓸때 여기에 View 파일인 HTML 파일들을 저장해야한다
- Spring Boot 에서는 JSTL 을 사용하든지 타임리프를 사용하든지 선택 가능
- 타임리프 사용시 View 파일을 JSP 가 아닌 HTML 파일을 사용해야함

- 그 HTML 인 VIew 페이지들이 여기 저장되어야함
+ HTML 파일을 쓰므로 EL, JSTL 을 사용 못하게되는 것이다

 

 

Controller를 추가해서 Hello World 출력
- src/main/java/com/example/demo/controller – SampleController.java 생성
- index 파일에서 값을 요청했을때 여기서 처리해보자
- top-level 패키지 하위인 com.example.demo 아래에 controller 폴더 생성 후 SampleController.ajva 파일 생성

- 그냥 일반 클래스로 만듬

 

- SampleController.java

package com.example.demo.controller;

import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class SampleController {
	@RequestMapping("/")
	public String hello() {
		return "Hello World~!!";
	}
}

+ @RestController = @Controller + @ResponseBody ,

- 현재 프로젝트 Run As -> Run on Server 로 실행 한 후 웹브라우저에 http://localhost:8080 요청한다.

- Hello World ~!! 가 브라우저 나타나면 잘 실행된 것

- web.xml 에서 설정하던 Dispatcher Servlet 매핑을 하지 않았으므로, 그냥 여기 Controller 클래스의 @RequestMapping 만 맞으면 바로 찾아간다

- 현재는 / 이므로 아무거나 요청해도 다받음

 

- 이후 프로젝트를 Run As - Run on Server 로 실행

- View 로 가서 출력하는 대신 @ResponseBody 에 의해 return 만 하면 바로 브라우저에 출력됨

- @RestController 를 썼으므로 @ResponseBody 에 의해 return 시 요청한 브라우저로 바로 찾아가서 돌려준다, 즉 브라우저에 바로 출력된다

 

 

port 번호 설정

- Spring boot 에 내장된 tomcat 은 기본 port 가 8080, 현재 8080 은 오라클에서 쓰고 있는 포트이다

- 포트충돌 가능성, 오류 생긴다면 내장 tomcat port 번호를 80 으로 바꿔줘야한다

 - tomcat port 번호를 바꾸기 위해 application.properties 에 server.port = 80 한줄을 추가해준다

- application.properties

- 이후 다시 실행시 실행 됨
 

Spring Boot 예제 : 프로젝트 boot01

실습 준비

- 클라우드의 프로젝트 boot01 을 다운받아 압축 해제 후 import

 

Spring Boot 프로젝트 import 하는 방법

- Spring 과 같은 방법으로 import 하면 된다

- boot01 프로젝트 import

- 프로젝트 오류 발생, 현재 설정된 JAVA 가 11 버전으로 되어있고, 우리가 설치한 JAVA 는 8 점대 버전이라서 오류 발생

 

프로젝트의 JAVA 버전 설정 수정 방법

- 현재 프로젝트 오른쪽 버튼 -> Properties

- 설치된 JAVA 버전인 8 점대 (=1.8) 선택 후 적용

- 그럼 JAVA 버전이 8점대 (1.8) 로 변경되고 오류가 사라진다\

 

흐름 설명

- Run As -> Run on Server 로 프로젝트 boot1 을 실행

- 이전에 Spring 에서 했었던 예제

- 누르는 메뉴에 따라 다른 화면이 나타남

ex) '구구단' 클릭시 랜덤 단 으로 구구단 나타남


파일들 살펴보기 : pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.2.9.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>boot01</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>war</packaging>
	<name>boot01</name>
	<description>Demo project for Spring Boot</description>

	<properties>
		<java.version>1.8</java.version>
	</properties>

	<dependencies>	
		<!-- jsp 파일을 사용하기 위한 의존 라이브러리-->
		<dependency>
			<groupId>org.apache.tomcat.embed</groupId>
			<artifactId>tomcat-embed-jasper</artifactId>
			<scope>provided</scope>
		</dependency>
		
		<!-- jstl -->
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>jstl</artifactId>
		</dependency>	
	
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-tomcat</artifactId>
			<scope>provided</scope>
		</dependency>
		
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
			<exclusions>
				<exclusion>
					<groupId>org.junit.vintage</groupId>
					<artifactId>junit-vintage-engine</artifactId>
				</exclusion>
			</exclusions>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

- Maven 의 환경설정 파일, Spring Boot 프로젝트 생성시 체크했던 의존 라이브러리만 추가됨

- 원하는 라이브러리를 직접 추가할 수도 있다

- JAVA 는 1.8 버전, Spring Boot 는 2.2.9 버전 사용 중

 

pom.xml 에 새로 추가된 라이브러리 : JSTL 라이브러리

		<!-- jstl -->
		<dependency>
			<groupId>javax.servlet</groupId>
			<artifactId>jstl</artifactId>
		</dependency>

- 결과를 JSTL 로 출력하기 위해 JSTL 라이브러리 jstl 을 추가

+ 현재는 JSTL 로 출력하기, 나중에 타임리프 로 출력시엔 타임리프 라이브러리를 추가해아함


파일들 살펴보기 : application.properties

# port
server.port=80

# prefix and suffix
spring.mvc.view.prefix=/WEB-INF/views/
spring.mvc.view.suffix=.jsp

- 이 환경설정 파일 application.properties 은 resources 폴더 안에 있다

- 대부분의 환경설정을 이 파일에서 해야한다

- 서버(tomcat) 의 포트번호를 80 으로 설정하고 있다

- Spring 에서 JSTL 로 출력하기 위해 servlet-context.xml 에서 VIewResolver 를 설정헀던 것처럼, Spring Boot 에서는 prefix, suffix 를 이 파일 application.properties 안에 설정해야한다

- prefix 는 View 파일들이 저장될 최상위 폴더, suffix 는 View 파일 확장자 를 설정

- 설정된 prefix 위치에 해당하는 폴더가 webapp 폴더 하위에 생성되어있어야함, 기준은 webapp 폴더


top-level 패키지

- Spring Boot 패키지 생성시 지정한다

- 그 안에서 각각의 기능에 따른 폴더 (controller, service 등) 를 만들고 그 안에 JAVA 파일들을 넣는다, Spring 과 같다

- Sample 로 만들어진 Boot01Application.java, ServletInitializer.java 는 건드리면 서버 구동 오류 생길 수 있다, 건드리지 않기


파일들 살펴보기 : index 파일

- index.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<script src="http://code.jquery.com/jquery-latest.js"></script>
<script>
	$(document).ready(function(){
		$("#sel").change(function(){
			var sel = $("#sel").val();
			location.href=sel;
		});
	});
</script>

</head>
<body>

선택 :
<select id="sel">
	<option value="">메뉴</option>
	<option value="hi">hi</option>
	<option value="welcome">welcome</option>
	<option value="abc">abc</option>
	<option value="hello">hello</option>
	<option value="gugu">구구단</option>
</select>

</body>
</html>

- index 파일은 webapp 폴더 하위에 있어야한다

- select-option 으로 만들어져있고, select 의 id 값이 "sel" 로 되어있다, 이벤트를 발생시킨 태그인 select 태그를 가져온다

- change 이벤트가 발생, change() 함수안에서 선택된 option 의 value 값을 구해온다, 그 value 값이 요청이름값이 됨

- Dispatcher Servlet Mapping 을 설정했던 web.xml 이 없으므로, Controller 클래스의 @RequestMapping 과 이름값만 같으면 Controller 클래스로 찾아간다 

+ Controller 클래스가 여러개 있어도 요청값만 다르게 설정하면 된다

- 현재 프로젝트 선택 - Run As - Run on Server 클릭시 index 파일이 자동 실행된다


파일들 살펴보기 : Controller 클래스들

- HelloController.java

package com.example.demo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class HelloController {

	@RequestMapping("/welcome")
	public String welcome() {
		return "welcome";
	}
}

 

- SampleController.java

package com.example.demo.controller;

import java.io.IOException;
import java.util.Random;

import javax.servlet.http.HttpServletResponse;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;

//@RestController
@Controller
public class SampleController {

	@RequestMapping("/hi")
	@ResponseBody
	public void hello(HttpServletResponse response) throws IOException {
		response.getWriter().print("Hello world~!!!");
	}
	
	@RequestMapping("/abc")
	@ResponseBody
	public String abc() {
		return "hi abc";
	}
	
	@RequestMapping("/hello")
	public String hello() {
		return "hello";
	}
	
	@RequestMapping("/gugu")
	public String gugu(Model model) {
		
		Random r = new Random();
		int dan = r.nextInt(8) + 2;		// 2 ~ 9단
		
		model.addAttribute("dan", dan);
		
		return "gugu";
	}
	
}

@ResponseBody 어노테이션

- @ResponseBody 를 썼으므로 return 을 하면, 요청한 브라우저로 바로 돌려준다

- @ResponseBody 는 요청한 브라우저에 결과를 바로 돌려줘서, 브라우저에 결과가 바로 나타나게된다

- Spring 과 마찬가지로 요청을 Controller 클래스에서 받는다

ex) "hi" 로 요청이 오면 out 객체를 만들고, 브라우저에 "Hello World~!!!" 메세지를 출력, 이렇게 출력된 메세지를 "hi" 를 요청한 곳에 바로 돌려준다

- View 로 가서 출력하는 대신 @ResponseBody 에 의해 return 만 하면 바로 브라우저에 출력됨

- @ResponseBody 에 의해 return 시 요청한 브라우저로 바로 찾아가서 돌려준다, 즉 브라우저에 바로 출력된다


- @ResponseBody 가 붙은 "hi", "abc" 요청은 요청한 브라우저로 결과를 바로 돌려준다

ex) select-option 에서 "hi" 선택시

- 브라우저 창 URL 을 보면 View 로 이동하지 않고, 요청한 곳(브라우저) 인 http://localhost/boot01/hi 로 돌려줬음을 확인 가능


- @ResponseBody 가 붙지 않은 "hello", "gugu" 는 View 페이지로 돌려줘야한다

- 이땐 Spring 과 마찬가지로 prefix, suffix 를 뺀 경로를 적는다

ex) select-option 에서 "hello" 선택시

- Controller 에서 WEB-INF/views/hello.jsp 로 이동하였고 hello.jsp 인 View 파일의 내용이 출력됨


- @ResponseBody 가 붙지 않은 "hello", "gugu" 는 View 페이지로 돌려줘야한다

- 이땐 Spring 과 마찬가지로 prefix, suffix 를 뺀 경로를 적는다

ex) select-option 에서 "gugu" 선택시

- SampleController 의 @ResquestMapping("/gugu") 로 요청받음

- View 인 WEB-INF/views/gugu.jsp 로 이동하고, 값을 가져가기 위해 Model 객체에 저장

+ Random 클래스 nextInt(8) 메소드 호출 시 0 ~ 7 까지 랜덤 숫자를 발생시킴

- 실행할때마다 난수에 의해 랜덤으로 한개의 단이 나옴

 

- gugu.jsp

<%@ page language="java" 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">
<title>구구단 출력</title>
</head>
<body> 

 [ ${dan} 단 ] <br><br>
<c:forEach var="i" begin="1" end="9">
	${dan} * ${i} = ${dan * i} <br>
</c:forEach>

</body>
</html>

- JSTL forEach 태그를 사용하고 있다, JSTL 을 사용하기 위해 pom.xml 에 JSTL 라이브러리 추가했다



Spring Boot 의 Lombok 기능 예제 1

- 먼저 Lombok 기능을 쓰지 않고 해보고, Lombok 기능을 써서도 프로젝트 만들어보기

 

Lombok 기능

- DTO 클래스에서 접근 제어자가 private 인 필드들의 Getter / Setter 메소드를 쓰지 않고 어노테이션으로 자동 처리

- @Getter, @Setter 어노테이션으로 각 필드마다 Getter / Setter 메소드를 자동으로 만들어줌

 

Lomboc 기능 쓰지 않고 만드는 예제 : 프로젝트 boot02

- 프로젝트 boot02 생성

- Spring Web , Lombok , MyBatis Framework, Oracle Driver 를 추가

- 현재는 DB 연동을 하지 않는 예제이므로 Spring Web, Lombok 만 있으면 된다

- 여기서 체크를 하면 pom.xml 에 Lombok 라이브러리가 추가된다, 하지만 바로 사용가능하지 못함, 설치를 해야한다

- Finish를 하면 원격 저장소에서 로컬 저장소로 라이브러리를 다운로드

 

파일들 살펴보기 : pom.xml (JSP, JSTL 의존 라이브러리 추가 전)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
	<modelVersion>4.0.0</modelVersion>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.7.4</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.example</groupId>
	<artifactId>boot02</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<packaging>war</packaging>
	<name>boot02</name>
	<description>Demo project for Spring Boot</description>
	<properties>
		<java.version>1.8</java.version>
	</properties>
	<dependencies>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.mybatis.spring.boot</groupId>
			<artifactId>mybatis-spring-boot-starter</artifactId>
			<version>2.2.2</version>
		</dependency>

		<dependency>
			<groupId>com.oracle.database.jdbc</groupId>
			<artifactId>ojdbc8</artifactId>
			<scope>runtime</scope>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-tomcat</artifactId>
			<scope>provided</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
				<configuration>
					<excludes>
						<exclude>
							<groupId>org.projectlombok</groupId>
							<artifactId>lombok</artifactId>
						</exclude>
					</excludes>
				</configuration>
			</plugin>
		</plugins>
	</build>

</project>

- Spring Boot 프로젝트 생성 시 설정했던 내용들이 있다

- Spring Web 에 체크했으므로 spring-boot-starter-web 등 관련 라이브러리들이 추가되어있다

- Oracle Driver 에 체크했으므로 ojdbc8 라이브러리가 추가되어있고, 다운받아져있다

- Lombok 에 체크했으므려 lombok 라이브러리가 추가되어있고, 다운받아져있다

- MyBatis 에 체크했으므로 MyBatis 관련 라이브러리들이 추가되어있고, 다운받아져있다

+ 지금 프로젝트는 lombok 기능 쓰지 않을 것, 그냥 추가시켜둔 것이다

 

라이브러리 추가

- 현재 pom.xml 에 없는 의존 라이브러리를 dependencies 안에 추가

- JSP 파일을 사용하기 위한 의존 라이브러리와 JSTL 을 사용하기 위한 의존 라이브러리를 추가

 

환경 설정 파일 application.properties 수정

- 이 파일은 /main/resources 폴더 하위에 있다

- 이 파일 안에 필요한 내용 추가함

- 서버의 포트번호 설정과 prefix, suffix 설정

 

prefix 로 설정한 패키지(폴더) 만들기

- application.properties 에서 설정한 prefix 폴더들이 반드시 만들어져 있어야한다

- webapp 폴더를 기준으로 prefix 에 설정된 폴더를 생성해야한다

- webapp 하위에 WEB-INF 폴더, WEB-INF 폴더 하위에 views 폴더 생성

 

DTO 클래스 생성

- /main/java/com/example/demo/model - Member.java 생성

- com.example.demo 패키지 하위에 model 폴더 생성 후 Member.java (DTO) 파일 생성

- Member.java

package com.example.demo.model;

public class Member {
	
	private String id;
	private String passwd;
	
	public String getId() {
		return id;
	}
	public void setId(String id) {
		this.id = id;
	}
	public String getPasswd() {
		return passwd;
	}
	public void setPasswd(String passwd) {
		this.passwd = passwd;
	}
}

- 접근제어자가 private 인 필드들(원래는 테이블 컬럼명과 같은 이름으로 만듬) 생성, Getter / Setter 생성

- 현재는 Lombok 기능을 사용하지 않고 DTO 클래스를 만들었다

+ Lombok 기능 사용시 Getter / Setter 메소드를 직접 사용하지 않음

 

Controller 클래스 생성

- /main/java/com/example/demo/controller- SampleController.java 생성

- com.example.demo 패키지 하위에 controller폴더 생성 후 SampleController.java (Controller) 파일 생성

- SampleController.java (수정 전)

package com.example.demo.controller;

import org.springframework.stereotype.Controller;

@Controller
public class SampleController {

}

- @Controller 어노테이션을 붙인다

- 나중에 요청 받는 부분을 작성하고, @RequestMapping 어노테이션 사용할 것

 

index 파일 생성

- webapp 폴더 하위에 index.jsp 파일 생성

- index 파일 자동실행을 위해서는 반드시 webapp 폴더 하위에 있어야한다

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<script>
	location.href="main";
</script>
</body>
</html>

- 다시 Controller 클래스로 돌아가서 "main" 요청을 받는 코드를 작성하자


- SampleController.java (수정 후 1)

package com.example.demo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;

@Controller
public class SampleController {

	@RequestMapping("main")
	public String main() {
		return "main";
	}
}

- 이때 "main" 은 webapp/WEB-INF/views 하위에 들어가야할 JSP 파일명

 

- webapp/WEB-INF/views 하위에 main.jsp 파일 생성

- main.jsp 

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>로그인</title>
</head>
<body>

<form method="post" action="send">
	ID : <input type="text" name="id"><br>
	Password : <input type="text" name="passwd"><br>
	<input type="submit" value="가입">
</form>

</body>
</html>

- form 안에 아이디와 비번 입력 양식을 만들자, 입력 후 "가입" 클릭 시 "send" 로 요청

- 아이디 입력 양식의 name 값은 DTO 프로퍼티명과 같은 "id" 로 설정해야한다, 그래야 @ModelAttribute 로 값 전달 가능

- 비밀번호도 마찬가지

 

- 다시 이 "send" 요청을 받을 코드를 Controller 클래스에 추가

- SampleController.java (수정 후 2)

package com.example.demo.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;

import com.example.demo.model.Member;

@Controller
public class SampleController {

	@RequestMapping("main")
	public String main() {
		return "main";
	}
	
	@RequestMapping("send")
	public String send(Member member, Model model) {
		System.out.println("id:" + member.getId());
		System.out.println("passwd:" + member.getPasswd());
		
		model.addAttribute("member", member);
		return "result";
	}
}

 

- send() 메소드 매개변수에서 앞의 main.jsp 에서 넘어온 아이디, 비밀번호들을 @ModelAttribute (생략) 으로 DTO Member 객체 member 로 바로 받음

+ 값이 잘 넘어왔는지 넘어온 아이디와 비밀번호를 콘솔창에 찍어보고 있다

- Model 객체에 객체 member 를 저장해서 result.jsp 로 이동

 

- webapp/WEB-INF/views 하위에 result.jsp 파일 생성

- result.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
회원 가입 결과 : <br><br>
id : ${member.id} <br>
pass : ${member.passwd} <br>
</body>
</html>

+ JSTL 과 다르게 EL 은 라이브러리 없이 사용 가능

 

프로젝트 실행해보기

- DB 연결 안하므로 pom.xml 에서 MyBatis, Oracle 관련 라이브러리를 주석으로 막고 실행해야 오류가 발생하지 않음

- 그 후 프로젝트 실행

- 잘 실행된다

 

한글값 인코딩

+ Spring 에서는 web.xml 에서 한글값 인코딩 처리 코드를 넣었다

- Spring Boot 에서는 한글값 인코딩은 자동 처리된다


+ 서버에 돌아가는 프로젝트 Add and Remove 하는 방법

- 오류 생길때 사용



Spring Boot 의 Lombok 기능 예제 2

- 앞에서 했던 프로젝트 boot02 예제에 Lombok 기능을 사용해보기

 

Lombok 라이브러리

- java 라이브러리중 하나

- 멤버 변수에 대한 getter / setter method, toString(), Equals() 등과 생성자 코드를 불필요하게 반복적 만드는 대신 lombok 라이브러리를 사용하면 Annotation(어노테이션) 기반으로 자동으로 메소드를 생성해준다

- lombok 라이브러리를 사용하면 DTO같은 클래스에서 getter 와 setter 메소드를 자동으로 생성해 준다. 

 

Lombok 기능

- DTO 클래스에서 접근 제어자가 private 인 필드들의 Getter / Setter 메소드를 쓰지 않고 어노테이션으로 자동 처리

- @Getter, @Setter 어노테이션으로 각 필드마다 Getter / Setter 메소드를 자동으로 만들어줌

 

Lombok 라이브러리 설치

- Eclipse 나 STS 에서 Lombok 을 사용하기 위해선 Lombok 라이브러리만으로 동작하지 않고 설치를 해야 동작한다

- pom.xml 에는 아까 추가한 Lombok 라이브러리가 추가되어있다

- pom.xml 에 추가 이후 Lombok 라이브러리를 다운 받아서 Eclipse / STS 콘솔창에서 lombok 파일을 명령 프롬프트 창에서 한번 실행시켜줘야함

- 이렇게 Lombok 라이브러리를 한번은 설치해야 사용 가능

 

Eclipse / STS 에 Lombok 라이브러리 설정

- Eclipse 나 STS에서 Lombok을 이용하기 위해서 Lombok 을 설치, 설정해야한다

1. Lombok 사이트(http://projectlombok.org/all-versions) 에서 lombok-1.16.18.jar 파일을 다운로드 받는다.

- 클릭해서 다운 받으면 C:\Users\admin(내 계정)\Downloads 폴더 하위에 다운된다

2. 다운로드 받은 lombok-1.16.18.jar 파일 실행

c:\> cd C:\Users\admin\Downloads # 다운로드 받은 위치로 이동
c:\> java -jar lombok.jar # lombok 파일 실행

- 엔터시 새로운 창이 나타난다

 

- STS 에 lombok 을 설치할 것이므로 Eclipse 는 체크 해제해주고 STS 에 체크해서 Install / Update 누르기

- Install / Update 를 누르면 에 lombok 프로그램이 STS 실행파일 위치에 설치되어 저장된다

- 이 설치 작업은 한번만 수행하면 된다

- 설치가 완료되면 lombok 프로그램이 STS 실행파일 위치에 설치되어 저장된다

 

- 이후 lombok 라이브러리가 필요한 프로젝트의 pom.xml 에 lombok 라이브러리를 추가하면 lombok 을 사용 가능하다

- 프로젝트 boot02 의 pom.xml 부분

		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
			<optional>true</optional>
		</dependency>

 

Lombok 기능을 써서 DTO 작성하기

- Lombok 기능을 써서 기존 프로젝트 boot02 의 DTO 클래스를 수정하자

- Member.java (DTO, lombok 사용해서 수정)

package com.example.demo.model;

import lombok.Data;
import lombok.Getter;
import lombok.Setter;

//@Getter
//@Setter
@Data
public class Member {
	
	private String id;
	private String passwd;
	
//	public String getId() {
//		return id;
//	}
//	public void setId(String id) {
//		this.id = id;
//	}
//	public String getPasswd() {
//		return passwd;
//	}
//	public void setPasswd(String passwd) {
//		this.passwd = passwd;
//	}
}

Lombok 주요 어노테이션

- @Getter 어노테이션은 코드가 컴파일 될 때 속성들에 대해서 Getter 메소드들을 만들어준다

- @Setter 어노테이션은 코드가 컴파일 될 때 속성들에 대해서 Setter 메소드들을 만들어준다

- @ToSTring 어노테이션은 코드가 컴파일 될 때 속성들에 대해서 toString() 메소드를 생성

- @Data 어노테이션은 @Getter, @Setter, @ToString, @EqualsAndHashCode, @RequriedArgsConstructor 를 한꺼번에 설정해주는 어노테이션

+ 위의 어노테이션들은 설치했던 lombok 클래스에서 지원하는 어노테이션들이다

 

프로젝트 실행

- STS 를 Restart 시키고 다시 실행해야한다

+ STS Restart 방법 : [File] - Restart

- 위 코드처럼 DTO 클래스를 수정 후 프로젝트 boot02 를 실행해보자

 

분홍색 하이라이트 = 생소한 내용

하늘색 하이라이트 = 대비

 

Spring MVC 검색 기능 게시판 프로그램 (이어서)

상세 페이지

- 목록 페이지 list.jsp 에서 제목을 클릭하면 "view.do" 로 요청하면서, 글 번호, 페이지 번호를 전달함

<a href="view.do?num=${board.num}&pageNum=${pp.currentPage}" class="btn btn-default"> 
	<c:if test="${board.re_level >0 }">
	<img alt="" src="images/level.gif" height="2" width="${board.re_level *5 }">
	<img alt="" src="images/re.gif">
	</c:if> ${board.subject} 
	<c:if test="${board.readcount > 30 }">
	<img alt="" src="images/hot.gif">
	</c:if></a>

 

- Controller 클래스에서 "view.do" 요청 부분만

	@RequestMapping("view.do")	// 상세 페이지
	public String view(int num, String pageNum, Model model) {
		bs.selectUpdate(num);	// 조회수 1 증가
		Board board = bs.select(num); // 상세 정보 구하기
		model.addAttribute("board", board);
		model.addAttribute("pageNum", pageNum);
		
		return "view";
	}

상세 페이지로 갈때 수행할 DB작업 2가지

1. selectUpdate() 메소드 : 조회수 증가

2. select() 메소드 : 상세 정보 구하기

<돌아온 후>

- 상세 정보를 저장한 객체 board 와 페이지 번호 pageNum 을 Mdoel 객체에 저장해서 view.jsp 로 전달

 

- 관련 내용을 많이 했으므로, Service, DAO 제외하고 Mapper 파일만 보자

- Mapper 파일 Board.xml 에서 id 가 "selectUpdate" 인 SQL문 부분만

	<update id="selectUpdate" parameterType="int">
		update board set readcount = readcount+1 where num=#{num}
	</update>

- Mapper 파일 Board.xml 에서 id 가 "select" 인 SQL문 부분만

	<select id="select" parameterType="int" resultType="board">
		select * from board where num=#{num}
	</select>

 

- View 페이지 view.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ include file="header.jsp"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<script type="text/javascript">
	$(function() {
		$('#list').load('list.do?pageNum=${pageNum}');
	});
</script>
</head>
<body>
	<div class="container" align="center">
		<h2 class="text-primary">게시글 상세정보</h2>
		<table class="table table-bordered">
			<tr>
				<td>제목</td>
				<td>${board.subject}</td>
			</tr>
			<tr>
				<td>작성자</td>
				<td>${board.writer}</td>
			</tr>
			<tr>
				<td>조회수</td>
				<td>${board.readcount}</td>
			</tr>
			<tr>
				<td>아이피</td>
				<td>${board.ip}</td>
			</tr>
			<tr>
				<td>이메일</td>
				<td>${board.email}</td>
			</tr>
			<tr>
				<td>내용</td>
				<td><pre>${board.content}</pre></td>
			</tr>
		</table>
		
		<a href="list.do?pageNum=${pageNum}" class="btn btn-info">목록</a> 
		<a href="updateForm.do?num=${board.num}&pageNum=${pageNum}"
		   class="btn btn-info">수정</a> 
		<a href="deleteForm.do?num=${board.num}&pageNum=${pageNum}"
		   class="btn btn-info">삭제</a> 
		<a href="insertForm.do?nm=${board.num}&pageNum=${pageNum}"
		   class="btn btn-info">답변</a>
		<div id="list"></div>
	</div>
</body>
</html>

- Controller 에서 View 로 올때 Model 에 페이지 번호 pageNum 을 가져왔다

- 목록이 출력될 위치에 div 태그가 있고, ajax 의 load() 함수를 사용해서 "list.do" 요청하고 페이지 번호를 전달하면서 아래쪽에 리스트를 불러옴

 

 

버튼 처리

- '목록' 버튼을 누르면 페이지 번호를 전달하고 원래의 목록 페이지로 돌아감

- '답변' 버튼을 누르면 "insertForm.do" 로 요청하면서 부모글이 될 현재 글의 글 번호와 페이지 번호를 전달함

 

댓글 작성 요청 vs 원문 작성 요청

		<a href="insertForm.do?nm=${board.num}&pageNum=${pageNum}"
		   class="btn btn-info">답변</a>

- 댓글 작성을 할떄는 "insertForm.do" 로 요청하면서 nm (부모가될 글 글번호) 과 pageNum (페이지 번호) 를 가져감

			<a href="insertForm.do" class="btn btn-info">글 입력</a>

- 원문 글 작성을 할때는 아무런 값을 가져가지 않음


댓글 작성 폼

		<a href="insertForm.do?nm=${board.num}&pageNum=${pageNum}"
		   class="btn btn-info">답변</a>

- view.jsp 에서 "답변" 버튼을 누르면 "insertForm.do" 로 요청하면서 nm (부모가될 글 글번호) 과 pageNum (페이지 번호) 를 가져감

- 댓글 작성 폼으로 이동한다

 

- Controller 클래스에서 "insertForm.do" 요청 부분만

	@RequestMapping("insertForm.do")	// 글작성 폼 (원문, 답변글)
	public String insertForm(String nm, String pageNum, Model model) {
		int num = 0, ref = 0, re_level = 0, re_step = 0; // 원문
		if (nm != null) {	// 답변글
			num = Integer.parseInt(nm);
			Board board = bs.select(num);	// 부모글 정보 구해오기
			ref = board.getRef();
			re_level = board.getRe_level();
			re_step = board.getRe_step();
		}
		model.addAttribute("num", num);
		model.addAttribute("ref", ref);
		model.addAttribute("re_level", re_level);
		model.addAttribute("re_step", re_step);
		model.addAttribute("pageNum", pageNum);
		
		return "insertForm";
	}

- 답변 글인 경우 nm 이 null 이 아니므로 if 문 안의 내용을 수행함

- 부모글 번호로 부모글의 상세 정보를 구해오고, 부모글의 ref, re_level, re_step 값을 구해서 insertForm.jsp 로 이동함

- 답변글을 쓰기 전에 항상 부모글에 대한 정보를 구해와야한다, 부모글의 ref, lev, step 값을 알아야 댓글 작성 가능

- insertForm.jsp 로 갈때 Model 객체에 부모글의 글 번호 num, 부모글의 정보 ref, re_level, re_step 과 페이지 번호 pageNum 을 가져간다

+ 원문은 가져가는 num, ref, re_level, re_step 이 모두 0 으로 초기화되서 전달됨

 

- select() 메소드는 상세 정보를 구해오는 메소드, 많이 했으므로 설명 생략

 

- View 페이지 insertForm.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ include file="header.jsp"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<div class="container" align="center">
		<h2 class="text-primary">게시판 글쓰기</h2>
		<form action="insert.do" method="post">
			<input type="hidden" name="num" value="${num}"> 
			<input type="hidden" name="ref" value="${ref}"> 
			<input type="hidden" name="re_step" value="${re_step}"> 
			<input type="hidden" name="re_level" value="${re_level}"> 
			<input type="hidden" name="pageNum" value="${pageNum}">
			<table class="table table-striped">
				<tr>
					<td>제목</td>
					<td><input type="text" name="subject" required="required"></td>
				</tr>
				<tr>
					<td>작성자</td>
					<td><input type="text" name="writer" required="required"></td>
				</tr>
				<tr>
					<td>이메일</td>
					<td><input type="email" name="email" required="required"></td>
				</tr>
				<tr>
					<td>암호</td>
					<td><input type="password" name="passwd" required="required"></td>
				</tr>
				<tr>
					<td>내용</td>
					<td><textarea rows="5" cols="30" name="content"
							required="required"></textarea></td>
				</tr>
				<tr>
					<td colspan="2" align="center"><input type="submit" value="확인"></td>
				</tr>
			</table>
		</form>
	</div>
</body>
</html>

- Controller 클래스에서 넘어온 값이 부모글에 대한 5가지 정보를 hidden 으로 "insert.do" 로 재전달한다

- 댓글 작성 시에는 부모글의 num, ref, re_level, re_step, pageNum 이 넘어온다

+ 원문은 가져가는 num, ref, re_level, re_step 이 모두 0 으로 초기화되서 전달됨

 

- 댓글 작성 후 "확인" 버튼을 누르면 "insert.do" 로 요청


댓글 작성

- 댓글 작성폼에서 댓글 작성 후 "확인" 버튼을 누르면 "insert.do" 로 요청

- Controller 클래스에서 "insert.do" 요청 부분만

	@RequestMapping("insert.do")	// 글 작성
	public String insert(Board board, Model model, HttpServletRequest request) {
		int num = board.getNum();
		int number = bs.getMaxNum();
		if (num != 0) {		// 답변글
			bs.updateRe(board);
			board.setRe_level(board.getRe_level() + 1);
			board.setRe_step(board.getRe_step() + 1);
		} else				// 원문	
			board.setRef(number); // else 문 끝
            
			board.setNum(number);
			String ip = request.getRemoteAddr();
			board.setIp(ip);
			int result = bs.insert(board);
			model.addAttribute("result", result);
			
		return "insert";
	}

- 여기서 원문 작성도 하고 댓글 작성도 한다, ref, re_level, re_step 값을 다르게 설정해서 insert 하면 각각 원문, 댓글이 됨

- 댓글이므로 부모글의 num, ref, re_level, re_step, pageNum 가 hidden 으로 넘어온다

+ 원문은 num, ref, re_level, re_step 이 모두 0이고 pageNum 이 null 이다

- 이걸 @ModelAttribute (생략) 로 DTO Board 객체 board 로 바로 받음

 

흐름 설명

1. 넘어온 num 값을 가져옴, 그 num 값은 부모의 num 값이다

2. 작성할 댓글의 num 컬럼을 입력해야하므로, Service 클래스의 getMaxNum() 메소드를 사용해서 최대 num 값 + 1 된 값을 구해서 변수 number 에 저장 *아래에서 설명

3. 댓글 작성이므로 num != 0 이다

4. Service 의 updateRe() 메소드를 사용해서 부모글과 ref 가 같으면서 부모글보다 step 값이 큰 글들의 step 을 1 증가

* 아래에서 설명

5. 현재 board 의 ref, re_lvel, re_step 은 부모의 값이므로, ref 는 그대로, re_level 은 1 증가, re_step 은 1 증가시켜서 board 에 대시 세팅

6. if-else 문 뒤의 코드들을 실행, 변수 number 를 board 의 프로퍼티 num 으로 세팅한다

7. 글 작성한 사람의 ip 주소를 request.getRemoteAddr() 메소드로 구해와서 객체 board 의 프로퍼티 ip에 세팅

8. 객체 board 를 매개변수로 insert() 를 호출해서 댓글을 작성함

* 아래에서 설명

- 이때 board 의 num 은 새로 작성할 글번호, ref 는 부모와 같음, re_level, re_step 은 부모글보다 1 보다 증가된 값

<돌아온 후>

- insert() 의 결과를 변수 result 에 받아서 Model 객체에 저장해서 insert.jsp 로 이동

 

댓글 작성시 필요한 DB작업 2가지

1. getMaxNum() 메소드 : DB에서 최대 num 값 + 1을 가져옴, 컬럼 num 값을 넣기 위해 필요

2. updateRe() 메소드 : 부모글과 ref 가 같으면서 부모글보다 step 값이 큰 글들의 step 값을 1 증가

3. insert() 메소드 : 댓글 등록 (insert)


- Service, DAO 생략하고 Mapper 파일만 보기

- Mapper 파일 Board.xml 에서 id 가 "getMaxNum" 인 SQL문 부분만

	<!-- num 번호중 최대값 구하기 : 첫번째 글은 1번으로  설정 -->
	<select id="getMaxNum" resultType="int">
		select nvl(max(num),0) + 1 from board
	</select>

- DB에 아무 글도 없을때만 max(num) 이 null 이므로 nvl() 함수는 글을 처음으로 작성할때만 사용된다

- 여기서 이미 1 을 더한 뒤 돌아오므로, 이 돌아온 값이 새로 작성할 글의 num 이 될 것


- Mapper 파일 Board.xml 에서 id 가 "updateRe" 인 SQL문 부분만

	<update id="updateRe" parameterType="board">
		update board set re_step = re_step + 1
		 where ref=#{ref} and re_step > #{re_step}
	</update>

- 모글과 ref 가 같으면서 부모글보다 step 값이 큰 글들의 step 을 1 증가시킴


- Mapper 파일 Board.xml 에서 id 가 "insert" 인 SQL문 부분만

	<insert id="insert" parameterType="board">
	<!--<selectKey keyProperty="num" 
			order="BEFORE" resultType="int">
			select nvl(max(num),0) + 1 from board
		</selectKey> -->
		insert into board values (#{num},#{writer},#{subject},
			#{content},#{email},0,#{passwd},#{ref},
			#{re_step},#{re_level},#{ip},sysdate,'n')
	</insert>

 

- View 페이지 insert.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ include file="header.jsp"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<c:if test="${result > 0 }">
		<script type="text/javascript">
			alert("입력 성공");
			location.href = "list.do";
		</script>
	</c:if>
	<c:if test="${result <= 0 }">
		<script type="text/javascript">
			alert("입력 실패");
			history.go(-1);
		</script>
	</c:if>
</body>
</html>


글 수정 폼

		<a href="updateForm.do?num=${board.num}&pageNum=${pageNum}"
		   class="btn btn-info">수정</a>

- view.jsp 에서 '수정' 버튼을 누르면 "updateForm.do" 로 요청하면서 글 번호와 페이지 번호를 전달함

 

- Controller 클래스에서 "updateForm.do" 요청 부분만

	@RequestMapping("updateForm.do")	// 수정 폼
	public String updateForm(int num, String pageNum, Model model) {
		Board board = bs.select(num); // 상세 정보 구하기
		model.addAttribute("board", board);
		model.addAttribute("pageNum", pageNum);
		
		return "updateForm";
	}

- 전달받은 글 번호 num 과 페이지 번호 pageNum 을 바로 저장한다

- select() 메소드는 상세 정보를 가져오는 메소드, 많이 했으므로 설명 생략

- 가져온 상세정보 객체 board 와 페이지 번호 pageNum 을 가져간다

- 수정을 위해서는 글 번호, 페이지 번호, 비밀번호가 필요함

+ 글 번호, 비밀번호는 객체 board 안에 있다

+ 수정 위해 글 번호 필요, 수정 후 원래 페이지로 돌아가기 위해서 페이지 번호 필요

- 이전과 다르게 이번엔 수정폼에서 비번 비교를 할 것이므로 비밀번호를 가져가기

 

- View 페이지 updateForm.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ include file="header.jsp"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<script type="text/javascript">
	function chk() {
		if(frm.passwd.value != frm.passwd2.value) {
			alert("암호가 다르면 수정할 수 없습니다");
			frm.passwd2.focus();
			frm.passwd2.value = "";
			return false;
		}
	}
</script>
</head>
<body>
	<div class="container" align="center">
		<h2 class="text-primary">게시글 글수정</h2>
		<form action="update.do" method="post" name="frm"
			  onsubmit="return chk()">
			<input type="hidden" name="num" value="${board.num}"> 
			<input type="hidden" name="pageNum" value="${pageNum}"> 
			<input type="hidden" name="passwd" value="${board.passwd}">
			<table class="table table-striped">
				<tr>
					<td>번호</td>
					<td>${board.num}</td>
				</tr>
				<tr>
					<td>제목</td>
					<td><input type="text" name="subject" required="required"
								value="${board.subject}"></td>
				</tr>
				<tr>
					<td>작성자</td>
					<td><input type="text" name="writer" required="required"
								value="${board.writer}"></td>
				</tr>
				<tr>
					<td>이메일</td>
					<td><input type="email" name="email" required="required"
								value="${board.email}"></td>
				</tr>
				<tr>
					<td>암호</td>
					<td><input type="password" name="passwd2" required="required"></td>
				</tr>
				<tr>
					<td>내용</td>
					<td>
						<textarea rows="5" cols="30" name="content" required="required">${board.content}
						</textarea>
					</td>
				</tr>
				<tr>
					<td colspan="2" align="center"><input type="submit" value="확인"></td>
				</tr>
			</table>
		</form>
	</div>
</body>
</html>

- 넘어온 상세 정보 객체 board 에서 글 번호를 가져와서 hidden 객체로 "update.do" 로 전달

+ 글 번호는 출력만 하고 있으므로 hidden 으로 전달해야함

- 넘어온 페이지번호도 다시 hidden 객체로 "update.do" 로 전달

- DB와 사용자가 입력한 비번 비교를 DB 에서 하는 대신 여기서 Javascript 로 하고 있다 

 

<넘어가는 값>

- 글 번호 페이지 번호 뿐 아니라 비밀번호도 전달하고 있다

- 넘어온 상세 정보 객체 board 에서 비밀번호를 가져와서 name 값 "passwd" 변수로 저장해서 전달

비번 비교

- DB와 사용자가 입력한 비번 비교를 DB 에서 하는 대신 여기서 Javascript 로 하고 있다 

- 앞에서 넘어온 객체 board 에서 비밀번호를 가져올 수 있으므로 여기서 비번 비교를 할 수 있다

- 즉, DB의 비번은 passwd 변수에 저장되고, 사용자가 입력한 비번은 passwd2 라는 변수에 저장된다

- Controller 클래스에서 비번 비교하는 대신 이렇게 하는 방법도 있다

 

<script type="text/javascript">
	function chk() {
		if(frm.passwd.value != frm.passwd2.value) {
			alert("암호가 다르면 수정할 수 없습니다");
			frm.passwd2.focus();
			frm.passwd2.value = "";
			return false;
		}
	}
</script>

- form 태그의 name 값이 frm 이므로 frm.passwd.value 와 frm.passwd2.value 를 비교

- frm.passwd.value 는 넘어온 DB의 비밀번호, frm.passwd2.value 는 사용자가 수정 폼에 입력한 비밀번호

+ hidden 으로 넘어가는 값도 입력양식과 마찬가지로 name 값으로 값을 구할 수 있음

+ form 객체 하위객체는 name 값이다, name 값을 .(점) 으로 연결한다

 


글 수정

- 수정 폼에서 입력하고 "확인" 버튼 클릭시 "update.do" 로 요청

 

- Controller 클래스에서 "update.do" 요청 부분만

	@RequestMapping("update.do")	// 수정
	public String update(Board board, String pageNum, Model model) {
		int result = bs.update(board);
		model.addAttribute("result", result);
		model.addAttribute("pageNum", pageNum);
		
		return "update";
	}

- 비번 일치한 경우에만 여기로 넘어온다

- 수정폼에선 hidden 으로 넘어온 글 번호, 글 비밀번호, 사용자가 수정폼에서 입력한 값들을 Board 객체 board 로 받음

+ DTO 프로퍼티명과 일치한 num 이 passwd 인 DB에 저장된 비밀번호가 넘어오게 되지만, DB의 비번과 사용자가 입력한 비번이 일치한 경우만 여기로 넘어오므로 괜찮다

- 수정폼에서 hidden 으로 넘어온 페이지 번호 pageNum 은 DTO Board 프로퍼티에 없으므로 따로 받아줌

- update() 메소드를 호출하면서 매개변수로 board 를 전달한다, 그렇게 수정을 완료

- 수정 결과 result 와 페이지 번호 pageNum 을 Model 객체에 저장해서 전달

- update.jsp 에서 수정 성공 / 실패 처리를 한 후 목록 페이지로 이동할때 페이지 번호가 필요하므로 여기서 페이지 번호를 전달

 

- Service, DAO 생략

- Mapper 파일 Board.xml 에서 id 가 "update" 인 SQL문 부분만

	<update id="update" parameterType="board">
		update board set writer=#{writer},subject=#{subject},
			content=#{content},email=#{email} where num=#{num}
	</update>

- 넘어온 객체 board 에서 수정할 데이터와 글 번호를 가져와서 수정을 한다

 

- View 페이지 update.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ include file="header.jsp"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<c:if test="${result > 0 }">
		<script type="text/javascript">
			alert("수정 성공 ");
			location.href = "list.do?pageNum=${pageNum}";
		</script>
	</c:if>
	<c:if test="${result <= 0 }">
		<script type="text/javascript">
			alert("수정 실패");
			history.go(-1);
		</script>
	</c:if>
</body>
</html>

- 원래 페이지로 돌아가기 위해 페이지 번호를 받아왔다, "list.do" 로 요청하면서 받은 페이지 번호를 전달


글 삭제 폼

		<a href="deleteForm.do?num=${board.num}&pageNum=${pageNum}"
		   class="btn btn-info">삭제</a>

- view.jsp 에서 '삭제' 버튼을 누르면 "deleteForm.do" 로 요청하면서 글 번호와 페이지 번호를 전달함

 

- Controller 클래스에서 "deleteForm.do" 요청 부분만

	@RequestMapping("deleteForm.do")
	public String deleteForm(int num, String pageNum, Model model) {
		Board board = bs.select(num);
		model.addAttribute("board", board);
		model.addAttribute("pageNum", pageNum);
		
		return "deleteForm";
	}

 

- 전달받은 글 번호 num 과 페이지 번호 pageNum 을 바로 저장한다

- select() 메소드는 상세 정보를 가져오는 메소드, 많이 했으므로 설명 생략

- 가져온 상세정보 객체 board 와 페이지 번호 pageNum 을 가져간다

 

- 삭제를 위해서는 글 번호, 페이지 번호, 비밀번호가 필요함

+ 글 번호, 비밀번호는 객체 board 안에 있다

- 원래는 수정폼에 비밀번호는 가져갈 필요 없지만, 지금은 수정폼에서 비번 비교를 할 것이므로 가져감

+ 삭제 위해 글 번호 필요, 삭제 후 원래 페이지로 돌아가기 위해서 페이지 번호 필요

 

- View 페이지 deleteForm.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ include file="header.jsp"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<script type="text/javascript">
	function chk() {
		if (frm.passwd.value != frm.passwd2.value) {
			alert("암호가 다릅니다. 수정후 작업하세요");
			frm.passwd2.focus();
			frm.passwd2.value = "";
			return false;
		}
	}
</script>
</head>
<body>
	<div class="container">
		<h2 class="text-primary">게시글 삭제</h2>
		<form action="delete.do" name="frm" onsubmit="return chk()"	method="post">
			<input type="hidden" name="pageNum" value="${pageNum}"> 
			<input	type="hidden" name="passwd" value="${board.passwd}"> 
			<input type="hidden" name="num" value="${board.num}">
			<table class="table">
				<tr>
					<td>암호</td>
					<td><input type="password" name="passwd2" required="required"></td>
				</tr>
				<tr>
					<td colspan="2"><input type="submit" value="확인"></td>
				</tr>
			</table>
		</form>
	</div>
</body>
</html>

- 넘어온 상세 정보 객체 board 에서 글 번호를 가져와서 hidden 객체로 "delete.do" 로 전달

- 넘어온 상세 정보 객체 board 에서 비밀번호를 가져와서 hidden 객체로 변수 "passwd" 에 저장 후 전달

- 넘어온 페이지번호도 다시 hidden 객체로 "update.do" 로 전달

- DB와 사용자가 입력한 비번 비교를 DB 에서 하는 대신 여기서 Javascript 로 하고 있다 

 

<넘어가는 값>

- 글 번호 페이지 번호 뿐 아니라 비밀번호도 전달하고 있다

- 넘어온 상세 정보 객체 board 에서 비밀번호를 가져와서 name 값 "passwd" 변수로 저장해서 전달

- 이때 비밀번호는 넘겨줄 필요 없지만 비번 비교용으로 hidden 객체를 생성해서 변수 "passwd" 에 값을 저장하기 위해 hidden 객체를 사용했다

비번 비교

- DB와 사용자가 입력한 비번 비교를 DB 에서 하는 대신 여기서 Javascript 로 하고 있다 

- 앞에서 넘어온 객체 board 에서 비밀번호를 가져올 수 있으므로 여기서 비번 비교를 할 수 있다

- 즉, DB의 비번은 passwd 변수에 저장되고, 사용자가 입력한 비번은 passwd2 라는 변수에 저장된다

- Controller 클래스에서 비번 비교하는 대신 이렇게 하는 방법도 있다

 

<script type="text/javascript">
	function chk() {
		if (frm.passwd.value != frm.passwd2.value) {
			alert("암호가 다릅니다. 수정후 작업하세요");
			frm.passwd2.focus();
			return false;
		}
	}
</script>

- form 태그의 name 값이 frm 이므로 frm.passwd.value 와 frm.passwd2.value 를 비교

- frm.passwd.value 는 넘어온 DB의 비밀번호, frm.passwd2.value 는 사용자가 수정 폼에 입력한 비밀번호

+ hidden 으로 넘어가는 값도 입력양식과 마찬가지로 name 값으로 값을 구할 수 있음

+ form 객체 하위객체는 name 값이다, name 값을 .(점) 으로 연결한다

 

 


글 삭제

- 글 삭제 폼에서 비밀번호를 입력하고 비밀번호가 일치할때 "확인" 클릭시 "delete.do" 로 요청한다

- 요청하면서 글 번호, 페이지 번호, 비밀번호 넘어옴

+ 넘어온 값 중, 비밀번호는 필요 없다

 

- Controller 클래스에서 "delete.do" 요청 부분만

	@RequestMapping("delete.do")
	public String delete(int num, String pageNum, Model model) {
		int result = bs.delete(num);
		model.addAttribute("result", result);
		model.addAttribute("pageNum", pageNum);
		
		return "delete";
	}

- 넘어온 글 번호와 페이지 번호를 받고 Service 의 delete() 메소드 호출

<돌아온 후>

- 삭제 결과값 result 와 페이지 번호 pageNum 을 가져간다

- delete.jsp 에서 삭제 성공 / 실패 처리를 하면서 성공시 목록 페이지로 넘어갈 것, 그래서 페이지 번호를 delete.jsp 로 가져가야한다

 

- Service 는 생략

- DAO 에서 delete() 메소드 부분만

	public int delete(int num) {
		return sst.update("boardns.delete",num);
	}

- 실제 삭제 (delete) 가 아닌 del 값을 "y" 로 수정할 것이므로 SqlSession 객체 제공 메소드 중 update 메소드를 사용해야함

 

- Mapper 파일 Board.xml 에서 id 가 "delete" 인 SQL문 값만

	<update id="delete" parameterType="int">
		update board set del='y' where num=#{num}
	</update>

- 해당 글의 del 컬럼의 값을 "y" 로 설정하면 삭제된 글이 됨

- 실제로 삭제를 하진 않았다

 


Spring MVC ajax 비동기 댓글 게시판 프로그램 

- 댓글 처리시에도 페이지를 바꾸지 않고 비동기식으로 처리하는 경우가 많다

- 댓글 기능을 ajax 기능을 써서 페이지 바꾸지 않고 댓글 달기 / 수정 / 삭제를 해보자

 

실습 준비

- 클라우드의 sboard 프로젝트를 다운, 압축 해제, import

- 이 sboard 프로젝트는 부모 테이블 뿐 아니라 댓글을 위한 자식 테이블이 들어가있다, 총 테이블 2개

 

파일들 살펴보기 : web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
	<!-- 한글 입력 -->
	<filter>
		<filter-name>CharacterEncodingFilter</filter-name>
		<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
		<init-param>
			<param-name>encoding</param-name>
			<param-value>UTF-8</param-value>
		</init-param>
		<init-param>
			<param-name>forceEncoding</param-name>
			<param-value>true</param-value>
		</init-param>
	</filter>
	<filter-mapping>
		<filter-name>CharacterEncodingFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>
	<!-- The definition of the Root Spring Container shared by all Servlets and Filters -->
	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>/WEB-INF/spring/root-context.xml</param-value>
	</context-param>
	
	<!-- Creates the Spring Container shared by all Servlets and Filters -->
	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>
	<!-- Processes application requests -->
	<servlet>
		<servlet-name>appServlet</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
		</init-param>
		<load-on-startup>1</load-on-startup>
	</servlet>		
	<servlet-mapping>
		<servlet-name>appServlet</servlet-name>
		<url-pattern>/</url-pattern>
	</servlet-mapping>
</web-app>

- url-pattern 이 / 이므로 모든 요청이 Dispatcher Servlet 으로 간다

- 한글값 인코딩 처리가 되어있음

 

파일들 살펴보기 : servlet-context.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:beans="http://www.springframework.org/schema/beans"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
	<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
	<!-- Enables the Spring MVC @Controller programming model -->
	<annotation-driven />
	<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
	<resources mapping="/resources/**" location="/resources/" />
	<resources mapping="/css/**" location="/WEB-INF/css/" />
	<resources mapping="/js/**" location="/WEB-INF/js/" />
	<resources mapping="/fonts/**" location="/WEB-INF/fonts/" />
	<resources mapping="/images/**" location="/WEB-INF/images/" />
	<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
	<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<beans:property name="prefix" value="/WEB-INF/views/" />
		<beans:property name="suffix" value=".jsp" />
	</beans:bean>	
	<context:component-scan base-package="board1" />	
</beans:beans>

+ resources mapping 으로 View 파일들이 저장된 곳의 위치를 지정

- 매핑 잡는 방법 : 위의 코드처럼 폴더명을 쓰고 /** 를 쓴다 ex) "/resources/**"

- base-package 자바파일이 저장될 최상위 디렉토리는 board1 이다

- 테이블이 2개이므로 부모 테이블에 대한 클래스 1개, 자식 테이블에 대한 클래스 1개로 클래스도 2개이다

- DAO, DTO, Service 도 테이블마다 따로 있다

+ PagingPgm 클래스는 board1 프로젝트와 같은 내용

 

테이블 2개

1. 부모 테이블은 board1 으로서 이전 프로젝트 board1 에서 이미 만들었던 테이블 board을 그대로 활용

- 새로 만들 필요 없다

2. 자식 테이블은 replyBoard 이다, 댓글을 저장하는 테이블이다

 

파일들 살펴보기 : root-context.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd">
	
	<context:property-placeholder location="classpath:jdbc.properties" />
		
	<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
		destroy-method="close">
		<property name="driverClass" value="${jdbc.driverClassName}" />
		<property name="jdbcUrl" value="${jdbc.url}" />
		<property name="user" value="${jdbc.username}" />
		<property name="password" value="${jdbc.password}" />
		<property name="maxPoolSize" value="${jdbc.maxPoolSize}" />
	</bean>		
	
	<!-- 스프링 jdbc 즉 스프링으로 oracle 디비 연결 -->
	<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="dataSource" ref="dataSource" />
		<property name="configLocation" value="classpath:configuration.xml" />
		<property name="mapperLocations" value="classpath:sql/*.xml" />
	</bean>
	
	<bean id="session" class="org.mybatis.spring.SqlSessionTemplate">
		<constructor-arg index="0" ref="sqlSessionFactory" />
	</bean>
	
</beans>

- jdbc.properties 파일을 불러와서 DB 접속 정보 설정

- jdbc.properties

jdbc.driverClassName=oracle.jdbc.driver.OracleDriver
jdbc.url=jdbc:oracle:thin:@127.0.0.1:1521:xe
jdbc.username=spring
jdbc.password=spring123
jdbc.maxPoolSize=20

 

파일들 살펴보기 : configuration.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
	<typeAliases>
		<typeAlias alias="board" type="board1.model.Board" />
		<typeAlias alias="rb" type="board1.model.ReplyBoard" />
	</typeAliases>
	<!-- 
	<mappers>
		<mapper resource="Board.xml" />
		<mapper resource="ReplyBoard.xml" />
	</mappers> 
	-->
</configuration>

- alias 가 두개 잡혀 있다

- 부모 테이블에 관련된 DTO 인 Board DTO 에 대한 alias 가 "board"

- 자식 테이블에 관련된 DTO 인 ReplyBoard DTO 에 대한 alais 가 "rb"

 

- 테이블 개수가 늘어나면 DAO, DTO, Service, Controller, Alias 모두 늘어난다

 


테이블 생성

- SQL파일이 2개 있다

- board1.sql 에서 board 테이블 생성 코드 있다, 이전 프로젝트에서 생성 했으므로 생성할 필요 없음

- sboard.sql 에서 replyBoard 테이블을 생성하자

 

테이블 2개

1. 부모 테이블은 board1 으로서 이전 프로젝트 board1 에서 이미 만들었던 테이블 board을 그대로 활용

- 새로 만들 필요 없다

2. 자식 테이블은 replyBoard 이다, 댓글을 저장하는 테이블이다

- 부모 테이블 안의 글 마다 댓글을 따로 달 것이므로 자식 테이블이 필요함

 

- sboard.sql

select * from tab;
select * from board;
select * from REPLYBOARD;

-- 댓글 게시판
drop table replyBoard;
create table replyBoard (
	rno number primary key, -- 댓글 번호
	bno number not null references board(num), -- 부모키 번호
	-- on delete cascade,
	replytext varchar2(500) not null, -- 댓글 내용
	replyer varchar2(50) not null, -- 댓글 작성자
	regdate date not null, -- 댓글 작성일
	updatedate date not null -- 댓글 수정일
);
select * from REPLYBOARD;
select * from board order by num desc;
insert into REPLYBOARD values(10,262,'11','나',sysdate,sysdate);

- 테이블 replyBoard 를 생성하자

- 이 테이블 관련 DTO는 이 컬럼명과 같은 이름의 프로퍼티를 만들어아햔다

+ 테이블 board 는 생성하지 않고 이전 프로젝트의 테이블 그대로 쓰기

 

테이블 replyBoard 컬럼 설명

- rno : 댓글 번호, primary key

- 여기서도 sequence 를 쓰지 않고 있으므로 최대 댓글 번호 rno 를 구해서 1 증가시킨 값을 새 글의 rno 로 넣을것

- bno : 부모 글의 번호, foreign key 제약 조건이 설정되어있다

- replytext : 댓글 내용

- replyer : 댓글 단 사람 이름

- regdate : 댓글 작성된 날짜

- updatedate : 댓글 수정된 날짜

 

 

bno 컬럼 설명

- 부모 글의 번호, foreign key 제약 조건이 설정되어있다

- 부모 글이 같은 댓글 끼리는 bno 값이 같다

- 컬럼 bno 는 참조하는 부모 테이블 board 에서 num 컬럼을 부모키로 설정

+ 부모 키의 조건은 primary 또는 unique, board 의 num 은 primary key 이므로 부모키가 될 수 있다

+ bno 에 on delete cascade 옵션을 붙이면 부모 글을 삭제할때 달린 댓글(참조하는 자식)들도 모두 삭제됨

	bno number not null references board(num) on delete cascade, -- 부모키 번호

- bno에 on delete cascade 옵션이 없으면, 참조하는 자식이 있는 경우 부모 글이 삭제되지 않음!

+ 단 여기서는 on delete cascade 가 필요 없다, 실제 부모글을 삭제하는 코드는 없기때문에, update 로 상태값만 바꿔줌

 


흐름 설명

- 프로젝트 실행

- 이전에 달았던 댓글(실제론 글) 이 아니라 한 부모글에 대한 실제 댓글을 달 수 있다!

- 부모글마다 댓글이 따로 달릴 수 있다

- bno 값이 같은 댓글들만 출력되는 것이다

 

이전에 했던 댓글 vs 지금 하는 댓글

- 이전에 했던 댓글은 1개의 테이블에 입력됐던 사실상 글

- 지금 하는 댓글은 실제 부모글에 달리는 댓글

 


코드 설명

- index.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
	
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<script type="text/javascript">
		location.href = "list";
	</script>
</body>
</html>

- url-pattern 이 / 로 설정되어 있으므로 어떤 걸로 요청해도 Dispatcher Servlet 으로 이동한다

- "list" 로 요청했다

 

- Controller 클래스 BoardController.java 에서 "list" 요청 부분만

	@RequestMapping("/list/pageNum/{pageNum}")
	public String list(@PathVariable String pageNum, Board board, Model model) {
		final int rowPerPage = 10;
		if (pageNum == null || pageNum.equals("")) {
			pageNum = "1";
		}
		int currentPage = Integer.parseInt(pageNum);
		// int total = bs.getTotal();
		int total = bs.getTotal(board); // 검색
		int startRow = (currentPage - 1) * rowPerPage + 1;
		int endRow = startRow + rowPerPage - 1;
		PagingPgm pp = new PagingPgm(total, rowPerPage, currentPage);
		board.setStartRow(startRow);
		board.setEndRow(endRow);
		// List<Board> list = bs.list(startRow, endRow);
		int no = total - startRow + 1;
		List<Board> list = bs.list(board);
		model.addAttribute("list", list);
		model.addAttribute("no", no);
		model.addAttribute("pp", pp);
		// 검색
		model.addAttribute("search", board.getSearch());
		model.addAttribute("keyword", board.getKeyword());
		return "list";
	}

- 요청했을때 값을 전달하는 방식이 달라짐

바뀐 방식

1. @RequestMapping 로 요청이름값 "list" 를 받고, / 변수명 pageNum/ pageNum 변수에 전달될 값을 쓴다

2. 위처럼 썼을때는 매개변수쪽에 @PathVariable 어노테이션을 쓰고 pageNum 변수에 전달되는 값을 오른쪽의 변수 pageNum 이 받음

+ 지금은 index.jsp 에서 넘어왔으므로 아무 것도 가지고 오지 않았다, pageNum 값이 없음

 

기존방식 vs 바뀐 방식

기존 방식
list?pageNum=1
바뀐 방식
/list/pageNum/{pageNum}

 

- View 페이지 list.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ include file="header.jsp"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<div class="container" align="center">
		<h2 class="text-primary">게시판 목록</h2>
		<table class="table table-striped">
			<tr>
				<td>번호</td>
				<td>제목</td>
				<td>작성자</td>
				<td>작성일</td>
				<td>조회수</td>
			</tr>
			<c:if test="${empty list}">
				<tr>
					<td colspan="5">데이터가 없습니다</td>
				</tr>
			</c:if>
			<c:if test="${not empty list}">
				<c:set var="no1" value="${no }"></c:set>
				<c:forEach var="board" items="${list }">
					<tr>
						<td>${no1}</td>
						<c:if test="${board.del =='y' }">
							<td colspan="4">삭제된 데이터 입니다</td>
						</c:if>
						<c:if test="${board.del !='y' }">
							<td><a href="${path }/view/num/${board.num}/pageNum/${pp.currentPage}"
									class="btn btn-default"> 
									<c:if test="${board.re_level >0 }">
										<img alt="" src="${path }/images/level.gif" height="2" width="${board.re_level *5 }">
										<img alt="" src="${path }/images/re.gif">
									</c:if> 
									${board.subject} 
									<c:if test="${board.readcount > 30 }">
										<img alt="" src="${path }/images/hot.gif">
									</c:if></a></td>
							<td>${board.writer}</td>
							<td>${board.reg_date}</td>
							<td>${board.readcount}</td>
						</c:if>
					</tr>
					<c:set var="no1" value="${no1 - 1}"></c:set>
				</c:forEach>
			</c:if>
		</table>
		<form action="${path}/list/pageNum/1">
			<select name="search">
				<option value="subject"
					<c:if test="${search=='subject'}">selected="selected" </c:if>>제목</option>
				<option value="content"
					<c:if test="${search=='content'}">selected="selected" </c:if>>내용</option>
				<option value="writer"
					<c:if test="${search=='writer'}">selected="selected" </c:if>>작성자</option>
				<option value="subcon"
					<c:if test="${search=='subcon'}">selected="selected" </c:if>>제목+내용</option>
			</select> 
			<input type="text" name="keyword"> 
			<input type="submit" value="확인">
		</form>
		<ul class="pagination">
			<c:if test="${not empty keyword}">
				<c:if test="${pp.startPage > pp.pagePerBlk }">
					<li><a href="${path }/list/pageNum/${pp.startPage - 1}?search=${search}&keyword=${keyword}">이전</a></li>
				</c:if>
				<c:forEach var="i" begin="${pp.startPage}" end="${pp.endPage}">
					<li <c:if test="${pp.currentPage==i}">class="active"</c:if>><a
						href="${path }/list/pageNum/${i}?search=${search}&keyword=${keyword}">${i}</a></li>
				</c:forEach>
				<c:if test="${pp.endPage < pp.totalPage}">
					<li><a href="${path }/list/pageNum/${pp.endPage + 1}?search=${search}&keyword=${keyword}">다음</a></li>
				</c:if>
			</c:if>
			<c:if test="${empty keyword}">
				<c:if test="${pp.startPage > pp.pagePerBlk }">
					<li><a href="${path }/list/pageNum/${pp.startPage - 1}">이전</a></li>
				</c:if>
				<c:forEach var="i" begin="${pp.startPage}" end="${pp.endPage}">
					<li <c:if test="${pp.currentPage==i}">class="active"</c:if>><a
						href="${path }/list/pageNum/${i}">${i}</a></li>
				</c:forEach>
				<c:if test="${pp.endPage < pp.totalPage}">
					<li><a href="${path }/list/pageNum/${pp.endPage + 1}">다음</a></li>
				</c:if>
		  </c:if>
		</ul>
		<div align="center">
			<a href="${path}/insertForm" class="btn btn-info">글 입력</a>
		</div>
	</div>
</body>
</html>

 

요청하면서 값 전달하는 방법 1 (list.jsp 부분)

 

<form action="${path}/list/pageNum/1">
			<select name="search">
				<option value="subject"
					<c:if test="${search=='subject'}">selected="selected" </c:if>>제목</option>
				<option value="content"
					<c:if test="${search=='content'}">selected="selected" </c:if>>내용</option>
				<option value="writer"
					<c:if test="${search=='writer'}">selected="selected" </c:if>>작성자</option>
				<option value="subcon"
					<c:if test="${search=='subcon'}">selected="selected" </c:if>>제목+내용</option>
			</select> 
			<input type="text" name="keyword"> 
			<input type="submit" value="확인">
		</form>

- ${path} : 현재 프로젝트 명을 의미, header.jsp 안에 있다

- list : "list" 란 이름으로 요청

- pageNum : 변수명

- 1 : pageNum 에 넣을 값

- 즉, "list" 로 요청하면서 pageNum 에 1 을 저장해 전달하라는 의미

 

- 여기서 선택시 다시 "list" 로 요청한다, 이때 "list" 로 갔을때는 pageNum 에 1 이라는 값이 전달됨

 

- header.jsp 부분

<c:set var="path" value="${pageContext.request.contextPath }" />

- 현재 프로젝트명 sboard 가 변수 path 에 저장되어있다

- JSTL set 태그로 변수 선언했다, 해당 내용은 EL 태그로 출력 가능

 

요청하면서 값 전달하는 방법 2 (list.jsp 부분)

- 마찬가지로 페이지 메뉴에 있는 페이지 또는 이전, 다음을 클릭시 "list" 로 요청하면서 해당 페이지 번호르 전달한다

- 이전에는 list?pageNum=${pp.startPage-1} 형식으로 전달했지만, 현재 list/pageNum/${pp.startPage-1} 형식으로 전달

 

제목 클릭시 상세 페이지로 이동 (list.jsp 부분)

<a href="${path }/view/num/${board.num}/pageNum/${pp.currentPage}" class="btn btn-default"> 
		<c:if test="${board.re_level >0 }">
		<img alt="" src="${path }/images/level.gif" height="2" width="${board.re_level *5 }">
		<img alt="" src="${path }/images/re.gif">
		</c:if> 
		${board.subject} 
		<c:if test="${board.readcount > 30 }">
		<img alt="" src="${path }/images/hot.gif">
		</c:if></a>

- 상세 페이지로 이동하기 위해 "view" 로 요청하면서 num, pageNum 변수에 값들을 저장해서 전달

- num 변수에 ${board.num} 값이 저장되어 전달, pageNum 변수에 ${pp.currentPage} 값이 저장되어 전달

 

- Controller 클래스 BoardController.java 에서 "view" 요청 부분만

	@RequestMapping("/view/num/{num}/pageNum/{pageNum}")
	public String view(@PathVariable int num, @PathVariable String pageNum, Model model) {
		bs.selectUpdate(num);
		Board board = bs.select(num);
		model.addAttribute("board", board);
		model.addAttribute("pageNum", pageNum);
		return "view";
	}

- /view/num/{num}/pageNum/{pageNum} 으로 요청과 값을 받음

- 전달된 값을 받을때는 "요청이름값/변수/{값}/변수/{값}" 형태로 받는다

- 조회수 1 증가 + 상세 정보 구하기 작업을 한 후 view.jsp 파일로 이동, 상세 정보 board 와 페이지 번호 pageNum 전달

 

- view.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ include file="header.jsp"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<script type="text/javascript">
	/* 	window.onload=function() {
	
	 } */
	$(function() {
		$('#slist').load('${path}/slist/num/${board.num}')
//		$('#list').load('${path}/list/pageNum/${pageNum}');
		$('#repInsert').click(function() {
			if (!frm.replytext.value) {
				alert('댓글 입력후에 클릭하시오');
				frm.replytext.focus();
				return false;
			}
			var frmData = $('form').serialize();
			// var frmData = 'replyer='+frm.replyer.value+'&bno='+
			//				  frm.bno.value+'&replytext='+frm.replytext.value;				  
			$.post('${path}/sInsert', frmData, function(data) {
				$('#slist').html(data);
				frm.replytext.value = '';
			});
		});
	});
</script>
</head>
<body>
	<div class="container" align="center">
		<h2 class="text-primary">게시글 상세정보</h2>
		<table class="table table-bordered">
			<tr>
				<td>제목</td>
				<td>${board.subject}</td>
			</tr>
			<tr>
				<td>작성자</td>
				<td>${board.writer}</td>
			</tr>
			<tr>
				<td>조회수</td>
				<td>${board.readcount}</td>
			</tr>
			<tr>
				<td>아이피</td>
				<td>${board.ip}</td>
			</tr>
			<tr>
				<td>이메일</td>
				<td>${board.email}</td>
			</tr>
			<tr>
				<td>내용</td>
				<td><pre>${board.content}</pre></td>
			</tr>
		</table>
		<a href="${path}/list/pageNum/${pageNum}" class="btn btn-info">목록</a>
		<a href="${path}/updateForm/num/${board.num}/pageNum/${pageNum}"
			class="btn btn-info">수정</a> <a
			href="${path}/deleteForm/num/${board.num}/pageNum/${pageNum}"
			class="btn btn-info">삭제</a> <a
			href="${path}/insertForm/nm/${board.num}/pageNum/${pageNum}"
			class="btn btn-info">답변</a>
		<p>
		<form name="frm" id="frm">
			<input type="hidden" name="replyer" value="${board.writer}">
			<input type="hidden" name="bno" value="${board.num}"> 댓글 :
			<textarea rows="3" cols="50" name="replytext"></textarea>
			<input type="button" value="확인" id="repInsert">
		</form>
		<div id="slist"></div>
		<!-- <div id="list"></div> -->
	</div>
</body>
</html>

 

버튼 처리 (view.jsp 부분)

		<a href="${path}/list/pageNum/${pageNum}" class="btn btn-info">목록</a>
		<a href="${path}/updateForm/num/${board.num}/pageNum/${pageNum}"
			class="btn btn-info">수정</a> <a
			href="${path}/deleteForm/num/${board.num}/pageNum/${pageNum}"
			class="btn btn-info">삭제</a> <a
			href="${path}/insertForm/nm/${board.num}/pageNum/${pageNum}"
			class="btn btn-info">답변</a>

- 잘못찾아 갈땐 앞에 프로젝트 명 sboard, 즉 ${path} 를 넣어줌

- 목록으로 가기 위해 "list" 요청시에는 페이지 번호를 전달해야함

- 수정폼, 삭제폼, 답변폼 으로 가기 위한 요청에서는 글 번호, 페이지 번호 를 가져가야함


댓글

- 댓글은 댓글 입력 부분, 댓글 처리 부분이 나뉨

- 댓글 처리시에는 댓글 목록 처리, 댓글 작성 처리가 있다

 

댓글 입력 부분 (view.jsp 부분)

		<form name="frm" id="frm">
			<input type="hidden" name="replyer" value="${board.writer}">
			<input type="hidden" name="bno" value="${board.num}"> 댓글 :
			<textarea rows="3" cols="50" name="replytext"></textarea>
			<input type="button" value="확인" id="repInsert">
		</form>
		<div id="slist"></div>

- 댓글을 입력하고 " 확인을 누르면 댓글이 달린다

- 비동기적으로 처리할 것이므로 action 값이 없다

+ 동기적으로 처리할떄는 form 의 action 값으로 값을 전달함, action 값이 있어야함

- 아래의 id 가 "slist" 인 div 태그는 댓글 목록이 출력될 자리

- 처리한 댓글 목록을 콜백함수로 받아서 아래의 div 태그에 뿌릴 것

- textarea 에 내용을 입력하고 "확인" 버튼 클릭시 onClick 이벤트 발생, 그걸 jQuery 에서 처리 * 아래에서 설명

- hidden 객체로 부모글 작성자명(원래는 댓글 작성자명을 써야함) ${board.writer} 값을 변수 "replyer" 에 저장

- hidden 객체로 부모글 번호 ${board.num} 값을 변수 "bno" 에 저장

- 사용자가 입력한 댓글 내용은 변수 "replytex" 에 저장

 

- 댓글을 달면 replyBoard 테이블에 저장된다

create table replyBoard (
	rno number primary key, -- 댓글 번호
	bno number not null references board(num), 
	-- on delete cascade, -- 부모키 번호
	replytext varchar2(500) not null, -- 댓글 내용
	replyer varchar2(50) not null, -- 댓글 작성자
	regdate date not null, -- 댓글 작성일
	updatedate date not null -- 댓글 수정일
);

- 컬럼별로 어떤 값을 어디로 넣어야할지 보자

- rno 컬럼 : 댓글 번호는 최대 rno 컬럼을 구해서 1 더한 값을 넣을 것

- bno 컬럼 : 현재 view.jsp 에는 부모글 정보를 저장한 board 객체가 넘어오므로 ${board.number} 로 가져오면된다

- replytext 컬럼 : 댓글 내용, 사용자가 view.jsp 댓글 작성 부분에서 입력한 글을 의미

- replyer 컬럼 : 댓글 작성자를 저장함, 댓글 작성자를 구해와야한다

댓글 작성자 구하는 2가지 방법

1. 작성자명을 직접 입력하게 함

2. 로그인 해야만 댓글을 입력할 수 있게 하고, 세션에서 id 값을 가져와서, 그걸로 DB의 회원 이름을 구해옴

- 현재는 둘 다 하고 있지 않고 그냥 부모글 작성자의 이름이 댓글 작성자 이름 replyer 로 들어가게 된다

- regdate 컬럼 : sysdate

- updatedate 컬럼 : 처음엔 regdate 로 같은 값 삽입, 나중에 수정시엔 이 값 수정

 

댓글 처리 부분 (view.jsp 부분)

<script type="text/javascript">
	/* 	window.onload=function() {
	
	 } */
	$(function() {
		$('#slist').load('${path}/slist/num/${board.num}')
//		$('#list').load('${path}/list/pageNum/${pageNum}');
		$('#repInsert').click(function() {
			if (!frm.replytext.value) {
				alert('댓글 입력후에 클릭하시오');
				frm.replytext.focus();
				return false;
			}
			var frmData = $('form').serialize();
			// var frmData = 'replyer='+frm.replyer.value+'&bno='+
			//				  frm.bno.value+'&replytext='+frm.replytext.value;				  
			$.post('${path}/sInsert', frmData, function(data) {
				$('#slist').html(data);
				frm.replytext.value = '';
			});
		});
	});
</script>

함수 실행 시기

- load() : view.jsp 실행되자 마자 load() 함수가 실행된다

- click() : 댓글 작성 후 "확인" 버튼을 눌러야 아래의 click() 이 실행된다

- 댓글 목록 구하기 처리와 댓글 작성 처리로 나뉜다 * 아래에서 나눠서 설명

- 댓글 작성 후 다시 댓글 목록 구하기 요청을 하므로 댓글 목로 구하기 처리부터 설명


댓글 목록 구하기 처리 (view.jsp 부분 일부)

		$('#slist').load('${path}/slist/num/${board.num}')

- id 가 slist 인 태그를 불러옴, 즉 댓글 목록을 보여줄 공간인 div 태그를 가져왔다

- 그 div 태그에 load() 함수를 사용해서 "slist" 로 요청하고 부모글 번호 ${board.num} 를 전달한다

- 부모글 번호 를 전달해야하는 이유 : 댓글 중 bno 가 부모글 번호와 같은 댓글들을 리스트로 가져와서 출력해야하므로

- 그럼 그 요청으로 인해 브라우저에 출력되는 값이 돌아와서 div 태그 안에 보여지는 것이다, 출력되는 값은 가져와진 댓글 이므로, 댓글 목록이 div 태그 안에 보여짐, 

- 이 작업은 이 페이지 view.jsp 로 들어오자마자 자동으로 처리됨

+ load('${path}/slist/num/${board.num}') 는 load('slist?num=${board.num}') 와 같은 기능

 

- Controller 클래스  ReplyBoardController.java 에서 "slist" 요청 부분만

	// 댓글 목록 구하기
	@RequestMapping("/slist/num/{num}")
	public String slist(@PathVariable int num, Model model) {
		Board board = bs.select(num); // 부모 테이블의 상세 정보 구하기
		List<ReplyBoard> slist = rbs.list(num); // 댓글 목록
		model.addAttribute("slist", slist);
		model.addAttribute("board", board);
		return "slist";
	}

- 넘어오는 값은 부모글의 글 번호 num, 그걸 @PathVariable 로 옆의 변수 num 에 저장한다

- 넘어온 부모 글 번호 num 으로 BoardService 의 메소드 select() 로 부모 글의 상세 정보를 구한 후 객체 board 로 받아서 저장한다

- ReplyBoardService 의 메소드 list() 로 댓글 목록을 구한다, 이때, 넘어온 부모글 번호 num 을 전달한다 이 값으로 댓글 중 bno 가 num 인 댓글 목록을 가져올 것

- 여러개 데이터를 가져오므로 List 로 반환받는다

<돌아온 후>

- 구해온 해당 부모글의 댓글 목록 slist 와 부모 글 정보 객체 board 를 Model 객체에 저장해서 slist.jsp 로 전달

- 댓글 목록 slist 를 slist.jsp 에서 댓글 목록 출력을 하면, 그 브라우저에 출력된 내용이 load() 함수로 돌아가서 view.jsp 의 div 태그에 출력함

 

- Service 클래스 ReplyBoardServiceImpl.java 에서 list() 메소드 부분만

	public List<ReplyBoard> list(int num) {
		return rbd.list(num);
	}

- DAO 클래스 ReplyBoardDaoImpl.java 에서 list() 메소드 부분만

	public List<ReplyBoard> list(int bno) {
		return sst.selectList("rbns.list", bno);
	}

- 넘어온 부모글 번호를 매개변수 bno 로 받는다

 

- Mapper 파일 ReplyBoard.xml 에서 id 가 "list" 인 SQL문 부분만

	<select id="list" parameterType="int" resultMap="rbResult">
		select * from replyBoard where bno=#{bno} order by rno
	</select>

- 넘어온 값 #{bno} 는 부모글 번호이다

- 댓글들을 저장한 replyBoard 테이블에서 bno 컬럼의 값이 부모글 번호인 모든 댓글들을 검색해서 리스트로 돌려줌

+ bno 값이 같은 댓글들은 같은 부모 아래의 댓글들

 

- View 페이지 slist.jsp

- 이 페이지에 출력되는 내용을 view.jsp 의 load() 함수가 불러서, view.jsp 의 div 태그 안에 출력된다!

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ include file="header.jsp"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<script type="text/javascript">
	$(function() {
		$('.edit1').click(function() {
			var id  = $(this).attr('id');  // rno
			var txt = $('#td_'+id).text(); // replytext
			$('#td_'+id).html("<textarea rows='3' cols='30' id='tt_"+id+"'>"+txt
				+"</textarea>");
			$('#btn_'+id).html(
			   "<input type='button' value='확인' onclick='up("+id+")'> "
			  +"<input type='button' value='취소' onclick='lst()'>");
		});
	});
	function up(id) {
		var replytext = $('#tt_'+id).val();
		var formData = "rno="+id+'&replytext='+replytext
			+"&bno=${board.num}";
		$.post('${path}/repUpdate',formData, function(data) {
			$('#slist').html(data);
		});
	}
	function lst() {
		$('#slist').load('${path}/slist/num/${board.num}');
	}
	function del(rno,bno) {
		var formData="rno="+rno+"&bno="+bno;
		$.post("${path}/repDelete",formData, function(data) {
			$('#slist').html(data);
		});
	}
</script>
</head>
<body>
	<div class="container" align="center">
		<h2 class="text-primary">댓글</h2>
		<table class="table table-bordered">
			<tr>
				<td>작성자</td>
				<td>내용</td>
				<td>수정일</td>
				<td></td>
			</tr>
			<c:forEach var="rb" items="${slist}">
				<tr>
					<td>${rb.replyer}</td>
					<td id="td_${rb.rno}">${rb.replytext}</td>
					<td>${rb.updatedate }</td>
					<td id="btn_${rb.rno}">
						<c:if test="${rb.replyer==board.writer }">
							<input type="button" value="수정" class="edit1" id="${rb.rno}">
							<input type="button" value="삭제" onclick="del(${rb.rno},${rb.bno})">
						</c:if></td>
				</tr>
			</c:forEach>
		</table>
	</div>
</body>
</html>

- 이 페이지에 출력되는 내용을 view.jsp 의 load() 함수가 불러서, view.jsp 의 div 태그 안에 출력된다!

- 해당 부모글의 댓글 목록 slist 와 부모 글 정보 객체 board 가 넘어왔다

- 넘어온 댓글 목록 slist 를 forEach 의 items 에 넣고 하나씩 댓글을 출력

+ 댓글 수정, 삭제 기능도 있다

 


- 댓글 목록 구하기 처리를 봤고 이제 다시 view.jsp 로 돌아와서 댓글 작성을 하는 처리를 보자

댓글 작성 처리 : 댓글 쓰고 "확인" 을 눌러서 click 이벤트 발생시  (view.jsp 부분 일부)

		$('#repInsert').click(function() {
			if (!frm.replytext.value) {
				alert('댓글 입력후에 클릭하시오');
				frm.replytext.focus();
				return false;
			}
			var frmData = $('form').serialize();
			// var frmData = 'replyer='+frm.replyer.value+'&bno='+
			//				  frm.bno.value+'&replytext='+frm.replytext.value;		
			// $.post('요청이름','전달될값','콜백함수');
			$.post('${path}/sInsert', frmData, function(data) {
				$('#slist').html(data);
				frm.replytext.value = '';
			});
		});

- 댓글 작성 후 "확인" 버튼을 누르면 click 이벤트 발생해서 click() 함수가 실행됨

- 윗부분은 유효성 검사

- 아랫부분에 작성한 댓글의 replyer, bno(부모글번호), replytext 값이 넘어가는 코드를 전달해야한다, 그 전달을 이 click() 함수 내에서 처리함

넘어가는 값

- 값들을 전달하기 위해 "sInsert" 로 요청하면서 frmData 를 전달함

- 넘어가는 값들 replyer, bno(부모글번호), replytext 를 변수 frmData 에 일일히 저장해야한다 (주석 부분)

- 여기선 대신 jQuery 를 이용해서 간략하게 form 태그 $('form') 으로 구한 후 serialize() 로 form 태그의 변수들을 한꺼번에 구해서 frmData 에 저장하는 방법 사용

- post() 함수로 전달되는 값을 여러개 쓸때는 변수=값&변수=값&변수=값 형태로 쓴다

+ 앞에 ? 는 파일명이 있는 경우만 파일명?변수=값&변수=값&변수=값 형태로 쓰는 것이다

- post 방식 요청이지만 형태만 마치 get 방식으로 값이 넘어가는 것 처럼 쓰는 것

출력

- 브라우저에 출력된 결과를 콜백함수로 받을때 data 란 이름으로 받아서 id 값이 slist 인 div 영역에 출력하고 있다

- .html(data) 에 의해 sInsert 요청에 의해 브라우저에 출력된 값이 id 가 div 인 영역에 출력되게 됨

 

+ post() 함수 형식

$.post('요청이름', '전달될값', '콜백함수')

+ 아래의 form 태그에 action 이 없었다

- 비동기로 처리하므로 action 대신 ajax post() 함수 사용해서 요청함

 

- Controller 클래스 ReplyBoardController.java 에서 "sInsert" 요청 부분만

	// 댓글 작성하기
	@RequestMapping("/sInsert")
	public String sInsert(ReplyBoard rb, Model model) {
		rbs.insert(rb);
		return "redirect:slist/num/" + rb.getBno();
	}

- 넘어온 replyer, bno (부모글번호), replytext 를 ReplyBoard 객체 rb 에 바로 전달받음

- ReplyBoard DTO 에 replyer, bno, replytext 를 저장할 프로퍼티가 있다

- ReplyService 클래스의 객체 rbs 로 insert() 를 호출, 이때 객체 rb 전달

<돌아온 후>

- 댓글 삽입 후 다시 댓글 목록을 불러와야함

- 여기서 바로 redirect: 로 댓글 목록 요청하는 "slist" 로 요청, 댓글 목록을 요청할때 부모 글 번호가 필요하다

- 넘어온 데이터 rb 의 bno 프로퍼티는 부모 글 번호이므로 그걸 getter 로 구해서 전달

- 그럼 댓글 목록을 다시 구해와서 load() 의 콜백함수로 돌아간 후 id 가 slist 인 div 태그 안에 댓글이 다시 출력됨 (비동기)

 

- Service 클래스 ReplyBoardService.java 에서 insert() 메소드 부분만

	public void insert(ReplyBoard rb) {
		rbd.insert(rb);
	}

- DAO 클래스 ReplyBoardDao.java 에서 insert() 메소드 부분만

	public void insert(ReplyBoard rb) {
		sst.insert("rbns.insert", rb);
	}

- Mapper 파일 ReplyBoard.xml 에서 id 가 "insert" 인 SQL문 부분만

<insert id="insert" parameterType="rb">
		<selectKey keyProperty="rno" order="BEFORE" resultType="int">
			select nvl(max(rno),0) + 1 from replyBoard
		</selectKey>
		insert into replyBoard values (#{rno},#{bno},#{replytext},
			#{replyer},sysdate,sysdate)
</insert>

- 이전 프로젝트에서 primary key 로 설정된 컬럼을 max 함수로 최대값을 구해서, 1 을 더한 후 글 번호로 넣었었다

- 지금은 따로 만들지 않고 한꺼번에 처리 할 것

- selectKey 태그를 사용해서 SQL문으로 rno 의 최대값을 구하고 1 증가시켜 댓글 삽입까지 여기 select SQL문에서 시킨다

 

selectKey

- selectKey 안의 select SQL 문에서 rno 의 최대값을 구하고 1 증가시키고 있음

- 그 1 증가된 값은 keyProperty 속성 값 "rno" 에 반환된다

- 그 "rno" 를 아래의 insert SQL문에서 바로 사용하고 있다 (화살표)

- order="BEFORE" 의 의미는 selectKey 안의 SQL문을 아래의 DML (insert) SQL문 보다 먼저 실행하라는 의미

- select 를 먼저 하고 다른 DML SQL문을 실행할떄 주로 selectKey 를 사용한다

+ selectKey 에서 돌려주는 값이 최대 rno 에서 1 증가된 rno (댓글 번호)값이므로 resultType 은 int 이다

 


댓글 수정

- slist.jsp 에서 td 태그에 출력하고 있는 댓글의 내용을 textarea 로 바꾼다

- 수정하고 "확인" 버튼 클릭시 수정 됨

 

- slist.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ include file="header.jsp"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<script type="text/javascript">
	$(function() {
		$('.edit1').click(function() {
			var id  = $(this).attr('id');  // rno
			var txt = $('#td_'+id).text(); // replytext
			$('#td_'+id).html("<textarea rows='3' cols='30' id='tt_"+id+"'>"+txt
				+"</textarea>");
			$('#btn_'+id).html(
			   "<input type='button' value='확인' onclick='up("+id+")'> "
			  +"<input type='button' value='취소' onclick='lst()'>");
		});
	});
	function up(id) {
		var replytext = $('#tt_'+id).val();
		var formData = "rno="+id+'&replytext='+replytext
			+"&bno=${board.num}";
		$.post('${path}/repUpdate',formData, function(data) {
			$('#slist').html(data);
		});
	}
	function lst() {
		$('#slist').load('${path}/slist/num/${board.num}');
	}
	function del(rno,bno) {
		var formData="rno="+rno+"&bno="+bno;
		$.post("${path}/repDelete",formData, function(data) {
			$('#slist').html(data);
		});
	}
</script>
</head>
<body>
	<div class="container" align="center">
		<h2 class="text-primary">댓글</h2>
		<table class="table table-bordered">
			<tr>
				<td>작성자</td>
				<td>내용</td>
				<td>수정일</td>
				<td></td>
			</tr>
			<c:forEach var="rb" items="${slist}">
				<tr>
					<td>${rb.replyer}</td>
					<td id="td_${rb.rno}">${rb.replytext}</td>
					<td>${rb.updatedate }</td>
					<td id="btn_${rb.rno}">
						<c:if test="${rb.replyer==board.writer }">
							<input type="button" value="수정" class="edit1" id="${rb.rno}">
							<input type="button" value="삭제" onclick="del(${rb.rno},${rb.bno})">
						</c:if></td>
				</tr>
			</c:forEach>
		</table>
	</div>
</body>
</html>

 

forEach 태그 부분 (slist.jsp 부분)

- forEach 루프가 돌아갈때마다 td 태그의 id 값이 달라진다!

- 같은 "수정" 버튼이지만 "수정" 버튼의 id 값은 각 댓글의 댓글 번호 ${rb.rno} 이므로 댓글마다 "수정" 버튼은 다른 id 를 가지고 있게 된다

- 그래서 사용자가 어떤 댓글의 "수정" 버튼을 클릭했는지 알 수 있게 된다

- "수정" 버튼 뿐만 아니라 댓글 내용을 출력하는 두번쨰 td 태그의 id 값도 "td_${rb.rno}" 이고, 댓글마다 다르다

- 그 "수정" 을 클릭하면 두번째 td 태그에 출력되고 있는 내용이 textarea 로 바뀌어야한다

- 또한 "수정" 을 클릭하면 "수정", "삭제" 버튼 레이블이 "확인", "취소" 로 바뀜

+ "수정" 버튼의 class 값은 "edit1"

 

jQuery 부분 1 (slist.jsp 부분)

- 수정 버튼을 클릭했을때 click 이벤트 발생하면서 아래의 click() 함수 실행됨

$(function() {
		$('.edit1').click(function() {
			var id  = $(this).attr('id');  // rno
			var txt = $('#td_'+id).text(); // replytext
			$('#td_'+id).html("<textarea rows='3' cols='30' id='tt_"+id+"'>"+txt
				+"</textarea>");
			$('#btn_'+id).html(
			   "<input type='button' value='확인' onclick='up("+id+")'> "
			  +"<input type='button' value='취소' onclick='lst()'>");
		});
	});

1. 각 댓글의 수정 버튼 태그의 id 구하기

- class 가 edit1 인 태그를 $('.edit1') 으로 불러온다, 모든 수정버튼은 class 값이 edit1 으로 고정

- $(this) : 이벤트를 발생시킨 태그를 구함, 여기선 클릭된 수정 버튼 의 태그 input 태그를 구해서 id 속성 값을 가져옴

- 구해온 id 는 ${rb.rno} 로 변하는 값, 즉 각 댓글의 댓글 번호값을 구해서 변수 id 에 저장

2. 각 댓글의 내용이 출력되는 td 태그 구하기

- 그 댓글 번호인 id 를 사용해서 ${'td_'+id) 로 두번째 td 태그, 즉 내용이 출력되는 태그를 구한다

- $('td_'+id) 는 두번째 td 태그, .text() 로 그 댓글 내용을 구한 뒤 변수 txt 에 저장

3. 두번째 td 태그에서 출력되던 댓글 내용을 textarea 로 만들기

- $('#td_'+id) 로 구해온 두번째 td 태그에 .html() 함수를 사용해서 textarea 태그를 만들고 안의 내용으로 txt 를 넣음

- textarea 의 id 값은 'tt_' + id 값이다, 아래에서 textarea 에 입력한 값을 가져올때 사용할 것

4."수정", "삭제" 버튼을 "확인", "취소" 버튼으로 바꾸기

- "수정", "삭제" 버튼은 4번째 td 태그에 있다, 그 4번째 태그의 id 는 "btn_${rb.rno}" 이므로 ${'#btn_'+id) 로 구해온 뒤 html() 함수를 사용해서 "확인" "취소" 버튼을 만든다

5 "확인" 버튼 클릭시 onclick 을 통해 up() 메소드 호출, 이때 각 댓글의 글 번호값인 변수 id 를 넘겨줌

+ 이 "확인" 버튼을 누르면 실제 댓글 내용 update 가 수행됨

 

jQuery 부분 2 (slist.jsp 부분)

- "확인" 버튼 클릭시 up()함수 호출하며 DB 와 연동해서 댓글 내용을 수정

- 이때, 비동기적으로 처리한다

function up(id) { // '확인' 버튼을 클릭해서 댓글 내용을 수정
		var replytext = $('#tt_'+id).val();
		var formData = "rno="+id+'&replytext='+replytext
			+"&bno=${board.num}";
		$.post('${path}/repUpdate',formData, function(data) {
			$('#slist').html(data);
		});
	}

- 위에서 변경된 textarea 의 내용을 구해와야하므로 textarea 태그를 $('#tt_'+id) 를 써서 구해온다, val() 로 내용을 구해서 변수 replytext 에 저장

- "수정" 버튼을 눌렀을때만 rno, replytext 값이 존재하므로 serialize() 함수 사용 불가능, rno, replytext, bno(부모글번호) 를 일일히 써서 묶은 후 formData 에 저장

- post() 함수로 전달되는 값을 여러개 쓸때는 변수=값&변수=값&변수=값 형태로 쓴다

+ 앞에 ? 는 파일명이 있는 경우만 파일명?변수=값&변수=값&변수=값 형태로 쓰는 것이다

- post 방식 요청이지만 형태만 마치 get 방식으로 값이 넘어가는 것 처럼 쓰는 것

- $.post() 함수를 사용해서 "repUpdate" 요청, formData 전달

 

- Controller 클래스 ReplyBoardController.java 에서 "repUpdate" 요청 부분만

	// 댓글 내용 수정
	@RequestMapping("/repUpdate")
	public String repUpdate(ReplyBoard rb, Model model) {
		rbs.update(rb); // 댓글 내용 수정
		return "redirect:slist/num/" + rb.getBno();
	}

- 앞에서 넘어온 formData 안의 rno, replytext, bno(부모 글번호) 값을 ReplyBoard 객체 rb 로 받아서 저장

- update() 메소드를 사용해서 댓글 내용 수정, 객체 rb 전달

<돌아온 후>

- 댓글 내용 수정 후 다시 댓글 목록을 "slist" 로 요청하고 있다

- 그럼 slist.jsp 에 삭제된 댓글은 제외된 나머지 댓글 목록들이 출력된다, 이때 브라우저에 출력된 댓글 목록이 slist.jsp 에 있는 id 가 slist 인 div 태그 아래에 나타남

 

- Service, DAO 생략

- Mapper 파일 ReplyBoard.xml 에서 id 가 "update" 인 SQL문 부분만

	<update id="update" parameterType="rb">
		update replyBoard set replytext=#{replytext},
			updatedate=sysdate where rno=#{rno} 
	</update>

- 수정할 날짜를 현재 날짜로 바꿈

- 전달된 3가지 값 중 댓글 번호 rno 를 where 절에 넣어서 해당 내용 수정, replytext 로 댓글 내용 수정


댓글 삭제

jQuery 부분 3 (slist.jsp 부분)

-  slist.jsp 에서 삭제 버튼을 클릭했을때 click 이벤트 발생하면서 아래의 del() 함수 실행됨

<input type="button" value="삭제" onclick="del(${rb.rno},${rb.bno})">

- ${rb.rno} 는 해당 댓글의 댓글번호, ${rb.bno} 는 부모글의 번호

 

- slist.jsp 중 del() 함수

	function del(rno,bno) {
		var formData="rno="+rno+"&bno="+bno;
		$.post("${path}/repDelete",formData, function(data) {
			$('#slist').html(data);
		});
	}

- 받은 rno, bno 를 formData 에 묶어서 저장

- post() 함수로 전달되는 값을 여러개 쓸때는 변수=값&변수=값&변수=값 형태로 쓴다

+ 앞에 ? 는 파일명이 있는 경우만 파일명?변수=값&변수=값&변수=값 형태로 쓰는 것이다

- post 방식 요청이지만 형태만 마치 get 방식으로 값이 넘어가는 것 처럼 쓰는 것

- "repDelete" 로 요청하며 formData 전달

<콜백함수로 돌아온 후>

- id 가 slist 인 div 태그에 삭제된 댓글을 제외한 댓글 목록이 나타남

 

- Controller 클래스 ReplyBoardController.java 에서 "repDelete" 요청 부분만

	@RequestMapping("/repDelete")
	public String delete(ReplyBoard rb, Model model) {
		rbs.delete(rb.getRno());
		return "redirect:slist/num/" + rb.getBno();
	}

- 넘어온 값 들을 ReplyBoard 객체 rb 로 받고, 삭제를 위해 delete() 호출하면서 삭제할 댓글 번호 전달

<돌아온 후>

- 삭제 후 다시 댓글 목록을 가져오는 "slist" 로 요청하면서 부모글의 글 번호를 전달, 그래야 그 부모에 달린 댓글들을 가져옴

- 그럼 slist.jsp 에 삭제된 댓글은 제외된 나머지 댓글 목록들이 출력된다, 이때 브라우저에 출력된 댓글 목록이 slist.jsp 에 있는 id 가 slist 인 div 태그 아래에 나타남

 

- Service, DAO 생략

- Mapper 파일 ReplyBoard.xml 에서 id 가 "delete" 인 SQL문 부분만

	<delete id="delete" parameterType="int">
		delete from replyBoard where rno=#{rno}
	</delete>

댓글 게시판 프로그램 (이어서)

분홍색 하이라이트 = 생소한 내용

하늘색 하이라이트 = 대비

 

상세 페이지

 

제목 클릭시 상세 페이지로 이동시 넘기는 값 (board_list.jsp 부분)

		<!-- 제목 출력 부분 -->	
		<a href="board_cont.do?board_num=${b.board_num}&page=${page}&state=cont">
				${b.board_subject}
		</a>

- 상세페이지로 이동하려고 한다, "board_cont.do" 로 요청함

- 요청하면서 전달하는 값이 글 번호와 페이지 번호 외에도 state 라는 변수에 cont 라는 값을 저장해서 전달함

- 상세 페이지, 수정, 삭제 등 여러 기능을 1개의 요청으로 처리하기 위해서 state 값을 다르게 설정함

 

 

 

- Controller 클래스에서 "board_cont.do" 요청 처리 부분을 보자

	/* 게시판 내용보기,삭제폼,수정폼,답변글폼 */
	@RequestMapping(value = "/board_cont.do")
	public String board_cont(@RequestParam("board_num") int board_num,
			@RequestParam("page") String page,
			@RequestParam("state") String state, 
			Model model) throws Exception {

		if (state.equals("cont")) { // 내용보기일때만
			boardService.hit(board_num); // 조회수 증가
		}

		BoardBean board = boardService.board_cont(board_num); // 상세 정보 구하기

		model.addAttribute("bcont", board);
		model.addAttribute("page", page);

		if (state.equals("cont")) {// 내용보기일때
			String board_cont = board.getBoard_content().replace("\n","<br/>");
			model.addAttribute("board_cont", board_cont);
			
			return "board/board_cont";// 내용보기 페이지 설정
			// 글내용중 엔터키 친부분을 웹상에 보이게 할때 다음줄로 개행
		} else if (state.equals("edit")) {// 수정폼
			return "board/board_edit";
		} else if (state.equals("del")) {// 삭제폼
			return "board/board_del";
		} else if (state.equals("reply")) {// 답변달기 폼
			return "board/board_reply";
		}
		return null;
	}

- 상세 페이지, 삭제폼, 수정폼, 답변글 폼 요청을 모두 이 @RequestMapping("/board_cont.do") 에서 처리한다

- 글 번호, 페이지 번호, state 를 @RequestParam 으로 값을 바로 옆의 변수에 저장, state 값은 요청을 구분하기 위한 값

- state 값이 "cont" 인 경우, 즉 상세페이지 요청인 경우에만 조회수 값을 증가시키고 있다

-  상세 페이지, 삭제폼, 수정폼, 답변글 폼 요청은 글 번호와 페이지 번호를 전달하는 등의 같은 형식으로 되어있고 1개 글에 대한 상세 정보를 구해오는 등의 같은 기능을 수행

- 그러므로 같은 요청으로 처리하고 다른 부분은 if-else if 문으로 state 값에 따른 다른 처리를 함

- 상세 페이지로 갈떄는 hit() 메소드로 조회수를 증가시키고, board_cont() 메소드로 상세 정보를 구해옴

<돌아온 후>

- 가져온 상세정보 객체 board 와 페이지 번호 page 를 Model 객체에 저장해서 각각의 View 페이지로 이동

- 글 번호는 객체 board 안에 있다

 

<조회수 증가>

- Service 클래스 BoardServiceImpl.java 에서 hit() 메소드 부분만

	/* 조회수 증가 */
	public void hit(int board_num) throws Exception {
		boardDao.boardHit(board_num); // 조회수 증가
	}

- DAO 클래스 BoardDaoImpl.java 에서 boardHit() 메소드 부분만

	/* 게시판 조회수 증가  */
	public void boardHit(int board_num) throws Exception {
		sqlSession.update("Test.board_hit", board_num);		
	}

- Mapper 파일 board.xml 에서 id 가 "board_hit" 인 SQL문 부분만

	<!-- 게시판 조회수 증가 -->
	<update id="board_hit" parameterType="int">
		update board53 set
		board_readcount=board_readcount+1
		where board_num=#{board_num}
	</update>

 

<상세 정보 구하기>

- Service 클래스 BoardServiceImpl.java 에서 board_cont() 메소드 부분만

	/* 상세정보 */
	public BoardBean board_cont(int board_num) throws Exception {

		BoardBean board = boardDao.getBoardCont(board_num);

		return board;
	}

- DAO 클래스 BoardDaoImpl.java 에서 getBoardCont() 메소드 부분만

	/* 게시판 글내용보기  */
	public BoardBean getBoardCont(int board_num) throws Exception {
		return (BoardBean) sqlSession.selectOne("Test.board_cont",board_num);
	}

- Mapper 파일 board.xml 에서 id 가 "board_cont" 인 SQL문 부분만

	<!-- 게시판 내용보기 -->
	<select id="board_cont" resultType="board"
		parameterType="int">
		select * from board53 where board_num=#{board_num}
	</select>

- 상세 정보를 구해온다, 상세 페이지, 수정 폼, 삭제 폼, 답변달기 폼 에서 사용

 

- View 페이지를 보자

- board_cont.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>

<!doctype html>
<html lang="ko">
<head>
	<meta charset="UTF-8">
	<title>게시판 내용보기</title>
	<link rel="stylesheet" type="text/css" href="<%=request.getContextPath() %>/css/board.css" />
</head>

<body>
	<div id="boardcont_wrap">
		<h2 class="boardcont_title">게시물 내용보기</h2>
		<table id="boardcont_t">
			<tr>
				<th>제목</th>
				<td>${bcont.board_subject}</td>
			</tr>

			<tr>
				<th>글내용</th>
				<td>
					${board_cont}
					<pre>${bcont.board_content}</pre>
				</td>
			</tr>

			<tr>
				<th>조회수</th>
				<td>${bcont.board_readcount}</td>
			</tr>
		</table>

		<div id="boardcont_menu">
			<input type="button" value="수정" class="input_button"
				onclick="location='board_cont.do?board_num=${bcont.board_num}&page=${page}&state=edit'" />
			<input type="button" value="삭제" class="input_button"
				onclick="location='board_cont.do?board_num=${bcont.board_num}&page=${page}&state=del'" />
			<input type="button" value="답변" class="input_button"
				onclick="location='board_cont.do?board_num=${bcont.board_num}&page=${page}&state=reply'" />
			<input type="button" value="목록" class="input_button"
				onclick="location='board_list.do?page=${page}'" />
		</div>
	</div>
</body>
</html>

- ${bcont.필드명} 으로 상세 정보를 가져와서 출력

 

버튼 처리

-  '목록' 버튼 클릭시 "board_list.do" 로 요청하며 페이지 번호값을 가져감, 그래야 목록에서 원래 페이지로 돌아간다

- '수정', '삭제', '답변' 버튼 클릭시 동일한 요청 "board_cont.do" 로 요청한다, Controller 클래스로 가서는 state 값으로 구별

- 수정 폼, 삭제 폼, 답변작성 폼은 모두 글 번호, 페이지 번호가 필요하고, 상세정보를 돌려주므로 같은 요청으로 처리하는 것

- 답변작성 폼은 부모글에 대한 정보가 SQL문에 필요하므로 부모글의 상세정보를 가져가야 한다

ex) 부모글의 board_re_lev 보다 1 증가시킨 값이 들어가야한다

 


댓글 작성 폼

<input type="button" value="답변" class="input_button"
	onclick="location='board_cont.do?board_num=${bcont.board_num}&page=${page}&state=reply'" />

- board_cont.jsp 에서 '답변' 버튼 클릭시 "board_cont.do" 로 요청한다, state 는 reply 이다

- 댓글 작성 폼은 부모글에 대한 정보가 SQL문에 필요하므로 부모글의 상세정보를 가져가야 한다

ex) 부모글의 board_re_lev 보다 1 증가시킨 값이 들어가야한다

 

- Controller, Service, DAO 로 가서 상세 정보를 가져온다, 그 부분은 생략

- Controller 의 "board_cont.do" 요청 부분은 상세 페이지를 설명할 때 했음

 

- View 페이지 board_reply.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
    
<!doctype html>
<html lang="ko">
<head>
	<meta charset="UTF-8">
	<title>게시판 답변 달기</title>
	<link rel="stylesheet" type="text/css" href="<%=request.getContextPath() %>/css/board.css" />
	<script src="http://code.jquery.com/jquery-latest.js"></script>
	<script src="./js/board.js"></script>
</head>

<body>
 <div id="boardreply_wrap">
  <h2 class="boardreply_title">게시판 답변달기</h2>
  <form method="post" action="board_reply_ok.do">  
  <input type="hidden" name="board_num" value="${bcont.board_num}" />
  <input type="hidden" name="board_re_ref" value="${bcont.board_re_ref}" />
  <input type="hidden" name="board_re_lev" value="${bcont.board_re_lev}" />
  <input type="hidden" name="board_re_seq" value="${bcont.board_re_seq}" />
  <input type="hidden" name="page" value="${page}" />
  
   <table id="boardreply_t">
    <tr>
     <th>글쓴이</th>
     <td>
      <input name="board_name" id="board_name" size="14" class="input_box" />
     </td>
    </tr>
    
    <tr>
     <th>비밀번호</th>
     <td>
      <input type="password" name="board_pass" id="board_pass"
      size="14" class="input_box" />
     </td>
    </tr>
    
    <tr>
     <th>글제목</th>
     <td>
      <input name="board_subject" id="board_subject" size="40"
      class="input_box" value="Re:${bcont.board_subject}" />
     </td>
    </tr>
    
    <tr>
     <th>글내용</th>
     <td>
      <textarea name="board_content" id="board_content" rows="8" 
      cols="50" class="input_box" ></textarea>
     </td>
    </tr>
   </table>
   <div id="boardreply_menu">
    <input type="submit" value="답변" class="input_button" />
    <input type="reset" value="취소" class="input_button"
    onclick="$('#board_name').focus();" />
   </div>
  </form>
 </div>
</body>
</html>

- 글 번호값과 페이지 번호는 3번 전달되는데, 목록 페이지 -> 상세 페이지 -> 댓글작성폼(수정폼,삭제폼) -> 댓글작성/수정/삭제 로 넘어간다.

- hidden 으로 페이지 번호, 부모 글의 정보(글 번호, ref, seq, lev) 를 전달한다

- "board_reply_ok.do" 로 요청

 

- Controller 클래스 BoardController.java 에서 "board_reply_ok.do" 요청 부분만

	/* 게시판 답변달기 저장 */
	@RequestMapping(value = "/board_reply_ok.do", method = RequestMethod.POST)
	public String board_reply_ok(@ModelAttribute BoardBean b,
						@RequestParam("page") String page) throws Exception {

		boardService.reply_ok(b);

		return "redirect:/board_list.do?page=" + page;
	}

- 답변달기 폼에서 사용자가 입력한 값들과 hidden 으로 넘겨준 부모글의 정보는 BoardBean 객체 b 에 저장

- 페이지 번호는 DTO 프로퍼티안에 저장할 수 있는 곳이 없으므로 따로 받아서 저장

<돌아온 후>

- 댓글을 단 후 reply_ok() 에서 돌아오면 View 에 출력하는 대신 바로 목록 페이지로 이동하기 위해 "redirect:board_list.do" 요청하고 페이지 번호를 전달한다

 

- Service 클래스 BoardServiceImpl.java 에서 reply_ok() 메소드 부분만

	/* 게시판 댓글 달기 */
	public void reply_ok(BoardBean b) throws Exception {

		boardDao.refEdit(b); // 기존 댓글 board_re_seq값 1증가

		b.setBoard_re_lev(b.getBoard_re_lev() + 1); // 부모보다 1증가된 값을 저장함
		b.setBoard_re_seq(b.getBoard_re_seq() + 1);

		boardDao.boardReplyOk(b);
	}

댓글을 달때 수행하는 SQL문 2가지

1. Update SQL문으로 기존 댓글들의 board_re_seq 값을 1 증가

- 이후 부모글보다 1 증가한 board_re_lev, 부모글보다 1 증가한 board_re_seq 를 저장함

2. Insert SQL문으로 댓글 삽입

 

- DAO 클래스 BoardDaoImpl.java 에서 refEdit() 메소드 부분만

	/* 답변글 레벨 증가  */
	public void refEdit(BoardBean b) throws Exception {
		sqlSession.update("Test.board_Level", b);		
	}

- Mapper 파일 board.xml 에서 id 가 "board_Level" 인 SQL문 부분만

	<!-- 답변글 레벨 증가 -->
	<update id="board_Level" parameterType="board">
		update board53 set
		board_re_seq=board_re_seq+1
		where board_re_ref=#{board_re_ref} and
		board_re_seq > #{board_re_seq}
	</update>

- 부모글과 ref 값이 같고, 부모글보다 seq 값이 큰 글들만 board_re_seq 값을 1 증가시킴

 

<Service 클래스에서 돌아온 후>

- Service 클래스로 넘어온 DTO 객체 b는 작성할 댓글의 제목, 내용들을 저장하고 있지만, board_re_seq, board_re_ref, board_re_lev, board_num 은 부모 글의 값이다

- DTO 객체 b의 board_re_lev 컬럼의 값을 부모글의 board_re_lev 값보다 1 증가시킨 값을 넣는다

- DTO 객체 b의 board_re_seq 컬럼의 값을 부모글의 board_re_seq 값보다 1 증가시킨 값을 넣는다

 

- DAO 클래스 BoardDaoImpl.java 에서 boardReplyOkay() 메소드 부분만

	/* 답변글 저장  */
	public void boardReplyOk(BoardBean b) throws Exception {
		sqlSession.insert("Test.board_reply", b);		
	}

 

- Mapper 파일 board.xml 에서 id 가 "board_reply" 인 SQL문 부분만

	<!-- 답변글 저장 -->
	<insert id="board_reply" parameterType="board">
		insert into board53
		(board_num,board_name,board_subject,board_content,
		board_pass,board_re_ref,board_re_lev,board_re_seq,board_readcount,board_date)
		values(board53_num_seq.nextval,#{board_name},#{board_subject},#{board_content},
		#{board_pass},#{board_re_ref},#{board_re_lev},#{board_re_seq},0,SYSDATE)
	</insert>

- board_num 은 원문 글이든 댓글이든 모두 시퀀스로 입력받음

- 댓글의 ref 값은 부모글의 ref 값과 같아야하므로 그대로 #{board_re_ref} 값을 저장한다

- lev, seq 값은 이미 부모글에서 1 증가시킨 값이므로 그대로 넣음


글 수정 폼

<input type="button" value="수정" class="input_button"
	onclick="location='board_cont.do?board_num=${bcont.board_num}&page=${page}&state=edit'" />

- board_cont.jsp 에서 '수정' 버튼 클릭시 "board_cont.do" 로 요청한다, state 는 edit 이다

- 수정을 위해 글 번호 필요

- 수정 성공 후 원래 페이지로 돌아가야하므로 페이지 번호가 필요

 

- Controller, Service, DAO 로 가서 상세 정보를 가져온다, 그 부분은 생략

- Controller 의 "board_cont.do" 요청 부분은 상세 페이지를 설명할 때 했음

 

- View 페이지 board_edit.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>    

<!doctype html>
<html lang="ko">
<head>
	<meta charset="UTF-8">
	<title>게시판 수정폼</title>
	<link rel="stylesheet" type="text/css" href="<%=request.getContextPath() %>/css/bbs.css" />
	<script src="http://code.jquery.com/jquery-latest.js"></script>
	<script src="<%=request.getContextPath() %>/js/board.js"></script>
</head>

<body>
 <div id="bbswrite_wrap">
  <h2 class="bbswrite_title">게시판 수정폼</h2>
  <form method="post" action="board_edit_ok.do" onSubmit="return board_check()">
  <input type="hidden" name="board_num" value="${bcont.board_num}" />
  <input type="hidden" name="page" value="${page}" />
  
   <table id="bbswrite_t">
    <tr>
     <th>글쓴이</th>
     <td>
     <input name="board_name" id="board_name" size="14" class="input_box" 
     value="${bcont.board_name}" />
     </td>
    </tr>
    
    <tr>
     <th>비밀번호</th>
     <td>
      <input type="password" name="board_pass" id="board_pass" size="14"
      class="input_box" />
     </td>
    </tr>
    
    <tr>
     <th>글제목</th>
     <td>
      <input name="board_subject" id="board_subject" size="40" 
      class="input_box" value="${bcont.board_subject}" />
     </td>
    </tr>
    
    <tr>
     <th>글내용</th>
     <td>
      <textarea name="board_content" id="board_content" rows="8" cols="50"
      class="input_box">${bcont.board_content}</textarea>
     </td>
    </tr> 
    
   </table>
   
   <div id="bbswrite_menu">
    <input type="submit" value="수정" class="input_button" />
    <input type="reset" value="취소" class="input_button"
    onclick="$('#board_name').focus();" />
   </div>
  </form>
 </div>
</body>
</html>

 

- 앞에서 페이지 번호와 글 상세 정보가 넘어왔다

- 수정폼에 정보 입력 후 "수정" 클릭시 "board_edit_ok.do" 로 요청하며 페이지 번호, 글 번호를 hidden 객체로 전달

- value 속성으로 글 상세 정보를 수정폼에 뿌려주고 있다

- 사용자가 입력한 값 4가지도 또한 전달된다


글 수정

- 수정폼 board_edit.jsp 에 정보 입력 후 "수정" 클릭시 "board_edit_ok.do" 로 요청

- 페이지 번호, 글 번호를 hidden 객체로 전달, 사용자가 입력한 값들도 넘어온다

 

- Controller 클래스 BoardController.java 에서 "board_edit_ok.do" 요청 부분만

	/* 게시판 수정 */
	@RequestMapping(value = "/board_edit_ok.do", method = RequestMethod.POST)
	public String board_edit_ok(@ModelAttribute BoardBean b,
								@RequestParam("page") String page,
								Model model) throws Exception {

		// 수정 메서드 호출
		BoardBean board = boardService.board_cont(b.getBoard_num());
		int result = 0;
		
		if (!board.getBoard_pass().equals(b.getBoard_pass())) {// 비번 불일치
			result = 1;
			model.addAttribute("result", result);
			
			return "board/updateResult";

		} else {
			// 수정 메서드 호출
			boardService.edit(b);			
		}	
		
		return "redirect:/board_cont.do?board_num=" + b.getBoard_num()
					+ "&page=" + page + "&state=cont";
	}

- 사용자가 수정폼에 입력한 값과 hidden 으로 넘어온 값 중 글 번호를 DTO 객체 b 에 받아서 저장한다

- hidden 으로 넘어온 페이지 번호는 따로 받아서 저장한다

- 즉 객체 b 안에는 글 번호와 사용자가 수정폼에 입력한 4가지 정보가 있다

 

수정할때 수행하는 SQL문 2가지

1. Select SQL문으로 상세정보를 가져와서 비번이 일치하는지 비교

2. Update SQL문으로 글 수정

<돌아온 후>

- 수정 후 View 대신 상세페이지로 바로 가기 위해 "board_cont.do" 로 요청하며 글 번호,페이지 번호 전달

- state 값은 cont 로 설정해야 상세 페이지를 요청함

+ 각 요청마다 필요한 값들이 있음, 여기서 "board_cont.do" 요청에는 글 번호, 페이지 번호, state 가 필요

 

<상세 정보 구하기>

- Service 클래스 BoardServiceImpl.java 에서 board_cont() 메소드는 상세 정보를 구할때도 사용했으므로 설명 생략

- 비번이 일치하면 수정 메소드 edit() 를 호출

 

<글 수정>

- Service 클래스 BoardServiceImpl.java 에서 edit() 메소드 부분만

	/* 게시판 수정 */
	public void edit(BoardBean b) throws Exception {			
		boardDao.boardEdit(b);
	}

- DAO 클래스 BoardDaoImpl.java 에서 boardEdit() 메소드 부분만

	/* 게시물 수정  */
	public void boardEdit(BoardBean b) throws Exception {
		sqlSession.update("Test.board_edit", b);		
	}

- Mapper 파일 board.xml 에서 id 가 "board_edit" 인 SQL문 부분만

	<!-- 게시물 수정 -->
	<update id="board_edit" parameterType="board">
		update board53 set
		board_name=#{board_name},
		board_subject=#{board_subject},
		board_content=#{board_content}
		where board_num=#{board_num}
	</update>

 

- 수정이 끝나고 다시 상세페이지로 가게된다, 조회수도 1 증가함


글 삭제 폼

<input type="button" value="삭제" class="input_button"
	onclick="location='board_cont.do?board_num=${bcont.board_num}&page=${page}&state=del'" />

- board_cont.jsp 에서 '삭제' 버튼 클릭시 "board_cont.do" 로 요청한다, state 는 del 이다

- 삭제를 위해 글 번호 필요

- 삭제 성공 후 원래 페이지로 돌아가야하므로 페이지 번호가 필요

 

- Controller, Service, DAO 로 가서 상세 정보를 가져온다, 그 부분은 생략

- Controller 의 "board_cont.do" 요청 부분은 상세 페이지를 설명할 때 했음

 

- View 페이지 board_del.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
    
<!doctype html>
<html lang="ko">
<head>
	<meta charset="UTF-8">
	<title>게시물 삭제</title>
	<link rel="stylesheet" type="text/css" href="./css/board.css" />
	<script src="http://code.jquery.com/jquery-latest.js"></script>
	
	<script>
 	 function del_check(){
		  if($.trim($("#pwd").val())==""){
			  alert("삭제 비번을 입력하세요!");
			  $("#pwd").val("").focus();
			  return false;
	 	 }
  	}
	</script>
</head>

<body>
 <div id="boarddel_wrap">
  <h2 class="boarddel_title">게시물 삭제</h2>
  <form method="post" action="board_del_ok.do" 
  onsubmit="return del_check()">
  <input type="hidden" name="board_num" value="${bcont.board_num}" />
  <input type="hidden" name="page" value="${page}" />
   <table id="boarddel_t">
    <tr>
     <th>삭제 비밀번호</th>
     <td>
      <input type="password" name="pwd" id="pwd" size="14" 
      class="input_box" />
     </td>
    </tr>
   </table>
   <div id="boarddel_menu">
    <input type="submit" value="삭제" class="input_button" />
    <input type="reset" value="취소" class="input_button" 
    onclick="$('#pwd').focus();" />
   </div>
  </form>
 </div>
</body>
</html>

- 앞에서 페이지 번호와 글 상세 정보가 넘어왔다

- 삭제 폼에서는 글의 상세 정보에서 글 번호만 가져와서 사용함

- 삭제 폼에 정보 입력 후 "삭제" 클릭시 "board_del_ok.do" 로 요청하며 페이지 번호, 글 번호를 hidden 객체로 전달

- 사용자가 입력한 비밀번호도 전달된다


글 삭제

- 삭제폼 board_del.jsp 에 비번 입력 후 "삭제" 클릭시 "board_del_ok.do" 로 요청

- 페이지 번호, 글 번호를 hidden 객체로 전달, 사용자가 입력한 값들도 넘어온다

- 비번을 가져올때, 삭제를 할때 글 번호가 필요하고 삭제 후 원래 페이지로 돌아가기 위해 페이지 번호가 필요하다

 

- Controller 클래스 BoardController.java 에서 "board_del_ok.do" 요청 부분만

	/* 게시판 삭제 */
	@RequestMapping(value = "/board_del_ok.do", method = RequestMethod.POST)
	public String board_del_ok(@RequestParam("board_num") int board_num,
			@RequestParam("page") int page,
			@RequestParam("pwd") String board_pass,
			Model model) throws Exception {

		BoardBean board = boardService.board_cont(board_num);
		int result=0;
		
		if (!board.getBoard_pass().equals(board_pass)) { // 비번 불일치
			result = 1;
			model.addAttribute("result", result);

			return "board/deleteResult";

		} else { // 비번 일치
			boardService.del_ok(board_num);		
		}
		
		return "redirect:/board_list.do?page=" + page;
	}

- 사용자가 삭제폼에 입력한 비밀번호와 hidden 으로 넘어온 글 번호와 페이지 번호를 @RequestParam 으로 따로 받음

 

 

삭제할때 수행하는 SQL문 2가지

1. Select SQL문으로 상세정보를 가져와서 비번이 일치하는지 비교

2. Delete SQL문으로 글 삭제

<돌아온 후>

- 수정 후 목록 페이지로 이동하기 위해 "board_list.do" 로 요청하고 페이지 번호를 전달

 

<상세 정보 구하기>

- Service 클래스 BoardServiceImpl.java 에서 board_cont() 메소드는 상세 정보를 구할때도 사용했으므로 설명 생략

- 비번 비교를 하기 위해 상세 정보를 가져온다

- 비번이 일치하면 삭제 메소드 del_ok() 호출

 

<글 삭제>

- Service 클래스 BoardServiceImpl.java 에서 del_ok() 메소드 부분만

	/* 게시판 삭제 */
	public void del_ok(int board_num) throws Exception{			
		boardDao.boardDelete(board_num);		
	}

- DAO 클래스 BoardDaoImpl.java 에서 boardDelete() 메소드 부분만

	/* 게시물 삭제  */
	public void boardDelete(int board_num) throws Exception {
		sqlSession.delete("Test.board_del", board_num);				
	}

- Mapper 파일 board.xml 에서 id 가 "board_del" 인 SQL문 부분만

	<!-- 게시물 삭제 -->
	<delete id="board_del" parameterType="int">
		delete from board53 where
		board_num=#{board_num}
	</delete>

ajax 활용 댓글 게시판

실습 준비

- 클라우드의 board1 프로젝트를 STS 에 import

 

이전 프로젝트와 달라진 점

1. 원문을 작성하는 양식과 댓글을 작성하는 양식이 같이 되어있다, 하나로 처리함

- 이때는 Sequence 를 사용하지 못한다

- Sequence 를 사용하지 못하는 이유 : 원문의 컬럼 ref 는 시퀀스로 들어가야하고, 댓글의 컬럼 ref 는 시퀀스로 들어가면 안된다

2. 검색 기능 포함

- Mapper 파일을 보면 SQL문들이 동적 SQL문으로 되어있다

- SQL문의 LIKE 연산자, 와일드카드 % 를 사용해야 한다, 동적 SQL문을 써야 그걸 처리할 수 있음

- 검색된 결과의 데이터 개수를 구하는 SQL문, 검색된 결과의 리스트를 구하는 SQL문 등이 있다

- 나중에 검색 기능 설명 할 것

3. 삭제시 상태값만 변경하고 목록에서 제목 대신 "삭제된 데이터입니다" 표시

4. 동적 SQL문 사용, when 태그, if 태그 등

5. 환경설정 중 달라진 부분

- jdbc.properties 에 DB 연동 정보를 입력하고 그 파일을 root-context.xml 에서 읽어서 처리

- jdbc.properties

jdbc.driverClassName=oracle.jdbc.driver.OracleDriver
jdbc.url=jdbc:oracle:thin:@127.0.0.1:1521:xe
jdbc.username=spring
jdbc.password=spring123
jdbc.maxPoolSize=20

- root-context.xml 부분

	<context:property-placeholder location="classpath:jdbc.properties" />
	
	<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
		destroy-method="close">
		<property name="driverClass" value="${jdbc.driverClassName}" />
		<property name="jdbcUrl" value="${jdbc.url}" />
		<property name="user" value="${jdbc.username}" />
		<property name="password" value="${jdbc.password}" />
		<property name="maxPoolSize" value="${jdbc.maxPoolSize}" />
	</bean>

- resouces 폴더 안에 jdbc.properties 파일이 있으므로 classpath: 를 붙이기

- jdbc.properties 파일안의 변수값들을 불러서 사용

 

테이블 생성

- board1.sql 에서 spring 계정으로 연결 후 테이블 생성

- board1.sql

-- 게시판
select * from tab;
select * from board;

create table board (
	num number primary key, -- key
	writer varchar2(20) not null, -- 작성자
	subject varchar2(50) not null, -- 제목
	content varchar2(500) not null, -- 본문
	email varchar2(30) , -- 이메일
	readcount number default 0, -- 읽은 횟수
	passwd varchar2(12) not null, -- 암호
	ref number not null, -- 답변글끼리 그룹
	re_step number not null, -- ref내의 순서
	re_level number not null, -- 들여쓰기
	ip varchar2(20) not null, -- 작성자 ip
	reg_date date not null, -- 작성일
	del char(1)
);
update board set readcount = 51 where num = 250;

- 테이블 board 생성

- 원문을 작성하는 양식과 댓글을 작성하는 양식이 같을때는 Sequence 를 사용하지 못한다!

- 그래서 컬럼 num 에는 시퀀스로 값을 입력 가능하지만, ref 에 시퀀스로 값을 입력할 수 없음, 즉 같이 시퀀스를 사용 불가

- 컬럼 num 에는 그룹함수 max 를 사용해서 num 컬럼에 들어가있는 값 중 최대값을 구한 후 새로운 데이터를 insert 시킬땐 그 최대값에 1 을 증가한 값을 넣음

- 처음 글을 작성할떄는 컬럼 num 에 1 입력

 

테이블 board 컬럼 설명

- ip : 작성자 ip 를 저장할 것

- del : 글을 삭제하면 컬럼 del 에 "y" 란 글로 상태값을 바꿀 것

- 글을 삭제하더라도 실제로 delete SQL문으로 삭제시키지 않고, update SQL문으로 컬럼 del 을 "y" 로 수정, 그러면 목록페이지에서 글 제목에 링크가 걸리지 않게 됨

 

페이징 처리를 위해서 필요한 값

- 전체 목록을 구할때는 총 데이터 개수 ( 전체 게시물 개수 ) 가 필요

- 검색된 데이터 목록에는  페이징 처리를 위해 검색된 데이터 총 개수가 필요함

 


흐름 보기

- 프로젝트 실행

- 글을 작성해보자

- 검색 기능을 보자

- 내가 검색한 단어를 포함한 글만 목록에 출력해준다

 

- 위의 select-option 에서 어떤 컬럼을 기준으로 검색할건지 선택 가능하다

- 제목 + 내용은 OR 로 처리함

 

A가 포함된 단어를 검색하는 SQL문

select * from emp where ename like '%A%'

- 고정된 값이 아니라서 동적 SQL문이라고 한다

- 제목으로 검색한다면 제목을 가진 컬럼명이 ename 자리에 들어간다

- 내요응로 검색한다면 내용을 가진 컬럼명이 ename 자리에 들어간다

- 검색어가 A 자리에 들어간다, 사용자가 입력양식에 입력한 값을 A 자리에 넣음

- select-option 으로 이 양식을 만드는데, select 는 변수명이 되고 option 의 value 속성은 해당 컬럼(제목, 내용 등) 이 됨

		<form action="list.do">
			<input type="hidden" name="pageNum" value="1"> 
			<select	name="search">
				<option value="subject"	<c:if test="${search=='subject'}">selected="selected" </c:if>>제목</option>
				<option value="content"	<c:if test="${search=='content'}">selected="selected" </c:if>>내용</option>
				<option value="writer"	<c:if test="${search=='writer'}">selected="selected" </c:if>>작성자</option>
				<option value="subcon"	<c:if test="${search=='subcon'}">selected="selected" </c:if>>제목+내용</option>
			</select> 
			<input type="text" name="keyword"> 
			<input type="submit" value="확인">
		</form>

- 선택하는 값이 따라 value 값을 달리해서 option 의 value 에 컬럼명을 넣는다

ex) '제목' 선택시 subject 컬럼이 value 에 들어감, 즉 그게 select 의 name 인 search 의 값이 된다

- 즉 사용자가 옵션에서 선택한 값이 search 가 되고, 검색어창에 입력한 값이 keyword 가 된다

- 그럼 그 값들이 폼 -> Controller -> Service -> DAO -> Mapper 로 가서 SQL문 안에 들어감!

- DTO Board 클래스에 search 와 keyword 를 저장할 수 있는 프로퍼티를 추가했다!

+ 테이블엔 search, keyword 컬럼이 없다!

- form 태그로 감싸져 있다, 즉 이부분만 다시 list.do 로 요청해서 검색창에서 "확인" 을 누를때마다 다시 목록을 가져옴

 

- Board.java 부분 (DTO)

추가된 프로퍼티 설명

- Mapper 파일로 값을 1개만 전달 가능하므로 page 번호만 전달해서 거기서 startRow, endRow 를 계산했었다, 여기서는 DTO Board 객체의 startRow, endRow 프로퍼티에 값을 담아서 Mapper 파일에 전달할 것

- select 의 name 값이 search, 검색어 입력양식의 name 값이 keyword 이다, 이 값들을 DTO 객체에 저장해서 Mapper 파일로 전달

 

- 글의 상세페이지에서 "답변" 을 눌러 댓글을 달아보자

- 댓글 작성폼과 원문 작성폼이 같고, 내부 처리가 같다

- 원문인지 댓글인지 구별을 내부적으로 해야한다

 

- 글을 삭제해보자

- 목록 페이지 list.jsp 에서 삭제된 데이터값은 조건식으로 구별해서 제목 대신 "삭제된 데이터 입니다" 메세지 뿌림

- 링크도 걸지 않는다

 

검색 목록 출력

- 전체 목록과 검색 목록을 출력을 같은 곳에서 하고 있다

- list.jsp 에서 전체 목록의 페이징 처리와 검색을 했을 경우의 페이징 처리 를 따로 만들고 if 태그로 구분한다

 

페이징 처리

- 페이징 처리를 하는 클래스를 따로 만들어뒀다

- 기본변수와 파생변수를 여기서 정의하고 getter / setter 메소드들이 있다

- 이 클래스의 필드값들이 기본변수, 파생변수이고 이 PagingPgm 객체를 만들어서 기본변수, 파생변수를 저장 가능


- src/main/java/board1/service 하위의 PagingPgm.java 파일

- PagingPgm.java 부분

- 값 전달시에 PagingPgm 객체를 전달하며 한번에 기본변수, 파생변수들을 전달시킬수있다

- getter / setter 메소드로 값을 돌려줌

 

- Controller 클래스에서 PagingPgm 클래스를 사용하는 코드

- 일부 변수들을 생성 한 후 PaginPgm 객체를 생성하며 생성자로 넘겨주면 PagingPgm 에서 나머지 변수들의 값을 구해줌

- Model 객체에 한번에 PaginPgm 객체를 저장해서 View 페이지로 넘길수도 있다


코드 설명

글 작성 기능

- 일단 시작부터 흐름을 간략 설명

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<script type="text/javascript">
		location.href = "list.do";
		//location.href="adminMail";
	</script>
</body>
</html>

- Controller 의 "list.do" 요청 부분만

- 여기 갔다가 View 로 갔다가 list.jsp 로 이동

 

- list.jsp 부분

- "글 입력" 클릭시 "insertForm.do" 로 요청

 

- Controller 클래스에서 "insertForm.do" 요청 부분 만

	@RequestMapping("insertForm.do")	// 글작성 폼 (원문, 답변글)
	public String insertForm(String nm, String pageNum, Model model) {
		int num = 0, ref = 0, re_level = 0, re_step = 0; // 원문
		if (nm != null) {	// 답변글
			num = Integer.parseInt(nm);
			Board board = bs.select(num);	// 부모글 정보 구해오기
			ref = board.getRef();
			re_level = board.getRe_level();
			re_step = board.getRe_step();
		}
		model.addAttribute("num", num);
		model.addAttribute("ref", ref);
		model.addAttribute("re_level", re_level);
		model.addAttribute("re_step", re_step);
		model.addAttribute("pageNum", pageNum);
		
		return "insertForm";
	}

원문 글과 댓글 구별

- 원문 글 양식과 댓글 양식을 같은걸 쓰는 프로젝트이므로 여기서 원문 글 작성 양식과 댓글 작성 양식을 구분해야함

- 특정 글의 상세 페이지 하단에서 "답변" 버튼을 눌렀을때 글 번호를 변수 nm 에 저장해서 전달한다

- 그러므로 nm 값이 없다면 원문 글, nm 값이 있다면 댓글임을 구별 가능

- 댓글 작성 폼으로 갈때는 원문 글 번호를 저장한 nm 값을 넘겨준다 (아래) 

- 또한 원문 글 작성폼으로 갈때는 pageNum 이 넘어오지 않으므로 null 이 되지만 댓글 작성 폼으로 갈때는 pageNum 이 넘어온다

<댓글 작성시>

- 부모글 번호를 저장한 변수 nm 을 int 로 변환하고 select() 메소드를 호출해서 부모글의 상세 정보를 가져온다

- 부모글의 ref, level, step 등의 정보가 필요하므로 부모글의 상세 정보를 가져오는 것임

- 부모글의 ref, level, step 정보를 저장후 Model 객체에 저장해서 전달, 부모 글 번호 num 과 페이지 번호 pageNum 도 전달

<원문 작성시>

- num, ref, level, step 값을 0 으로 초기값으로 설정함, 이 값들은 원문 작성할때 필요한 값이다

<가져가는 값>

- 폼으로 가기 위해 num, ref, re_level, re_step, pageNUm 값들을 가지고 글 작성 폼인 insertForm.jsp 로 간다

- 댓글 작성시에는 원문 작성할때와 다른 num, ref, re_level, re_step 값들을 가지고 insertForm.jsp 로 간다

 

- 지금은 원문을 작성하는 중이다

 

- insertForm.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ include file="header.jsp"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<div class="container" align="center">
		<h2 class="text-primary">게시판 글쓰기</h2>
		<form action="insert.do" method="post">
			<input type="hidden" name="num" value="${num}"> 
			<input type="hidden" name="ref" value="${ref}"> 
			<input type="hidden" name="re_step" value="${re_step}"> 
			<input type="hidden" name="re_level" value="${re_level}"> 
			<input type="hidden" name="pageNum" value="${pageNum}">
			<table class="table table-striped">
				<tr>
					<td>제목</td>
					<td><input type="text" name="subject" required="required"></td>
				</tr>
				<tr>
					<td>작성자</td>
					<td><input type="text" name="writer" required="required"></td>
				</tr>
				<tr>
					<td>이메일</td>
					<td><input type="email" name="email" required="required"></td>
				</tr>
				<tr>
					<td>암호</td>
					<td><input type="password" name="passwd" required="required"></td>
				</tr>
				<tr>
					<td>내용</td>
					<td><textarea rows="5" cols="30" name="content"
							required="required"></textarea></td>
				</tr>
				<tr>
					<td colspan="2" align="center"><input type="submit" value="확인"></td>
				</tr>
			</table>
		</form>
	</div>
</body>
</html>

- 입력양식에서 4개의 값을, hideen 객체로 5개의 값(부모의 num, ref, re_step, re_level 과 pageNum) 을 "insert.do" 로 요청하며 전달

- 현재는 원문글을 작성하므로 부모가 없다, num, ref, re_step, re_level 은 0 이고 pageNum 은 null 이다

 

- Controller 클래스에서 "insert.do" 요청 부분만

	@RequestMapping("insert.do")	// 글 작성
	public String insert(Board board, Model model, HttpServletRequest request) {
		int num = board.getNum();
		int number = bs.getMaxNum();
		if (num != 0) {		// 답변글
			bs.updateRe(board);
			board.setRe_level(board.getRe_level() + 1);
			board.setRe_step(board.getRe_step() + 1);
		} else				// 원문	
			board.setRef(number); // else 문 끝
			board.setNum(number);
			String ip = request.getRemoteAddr();
			board.setIp(ip);
			int result = bs.insert(board);
			model.addAttribute("result", result);
			
		return "insert";
	}

- 넘어온 값들을 DTO Board 객체 board 로 받아서 저장한다

- 원문인 경우 board 객체의 num 값은 0 이다, 댓글인 경우 board 객체의 num, ref, re_level, re_step 값은 부모의 값이다

- 즉 num 이 0 이면 원문, num 이 0 이 아니면 댓글이다

<공통적으로 적용>

시퀀스를 쓰지 않고 컬럼 num 에 값 넣기

- 먼저 Service 클래스의 getMaxNum() 을 호출해서, 컬럼 num 중 최대값을 구한 뒤 1을 증가시켜, 변수 number 에 돌려줌

ex) 현재 DB의 데이터들 중 가장 큰 num 값을 구해와서 1 을 더함, 이게 새로 입력할 글의 num 값이 된다

* getMaxNum() 메소드는 아래에서 설명

<댓글인 경우>

- 댓글인 경우 ref 값은 부모의 ref 값과 같아야하므로, updateRe() 메소드를 호출해서 부모의 ref 와 같은 ref 이면서 부모의 re_step 보다 큰 re_step 을 가진 글들의 step 값을 1 증가시킴

- 이후 객체 board 에는 작성할 글의 정보가 들어가야하므로 부모의 re_level, re_step 에서 1 증가한 값을 Setter 메소드로 DTO 객체 board 에 저장

<원문인 경우>

- 원문인 경우 num 과 ref 값이 같은 값이 들어가야하므로 DTO 의 Setter 메소드로 글의 ref 컬럼의 값을 number 로 설정

+ else 문에 괄호가 없으므로 board.setRef(number) 한줄만 적용됨

+ 원문일때 객체 board 안의 seq, level 값이 0 이므로 그대로 둔다

<공통적으로 적용>

- 컬럼 num 의 값을 최대값보다 1 증가된 값인 number 로 설정

- 글을 작성한 사람의 IP 주소를 구하기 위해 request.getRemoteAddr() 메소드 사용하고 Setter 메소드로 객체 board 에 세팅

- Servic 클래스의 insert() 메소드로 실제 글 작성(삽입)

* insert() 메소드 아래에서 설명

- Model 객체에 받은 result 저장 후 insert.jsp 로 이동


- Service 클래스 BoardServiceImpl.java 에서 getMaxNum() 메소드 부분만

	public int getMaxNum() {
		return bd.getMaxNum();
	}

- DAO 클래스 BoardDaoImpl.java 에서 getMaxNum() 메소드 부분만

	public int getMaxNum() {
		return sst.selectOne("boardns.getMaxNum");
	}

- 그룹함수 max 는 결과가 1개이므로 selectOne() 메소드 사용

 

- Mapper 파일 Board.xml 에서 id 가 "getMaxNum" 인 SQL문 부분만

	<!-- num 번호중 최대값 구하기 : 첫번째 글은 1번으로  설정 -->
	<select id="getMaxNum" resultType="int">
		select nvl(max(num),0) + 1 from board
	</select>

- 테이블 board 에서 가장 큰 num 값을 구해온다

- 처음으로 글을 작성할땐 max(num) 은 아무 데이터 없이 null 이 나온다

- nvl() 함수를 사용해서 null 값인 경우 0 으로 바꿔준다

- 그 후 구한 컬럼 num 의 최대값에서 1 을 더해서 int 형으로 돌려준다


- Service 클래스 BoardServiceImpl.java 에서 insert() 메소드 부분만

	public int insert(Board board) {
		return bd.insert(board);
	}

- DAO 클래스 BoardDaoImpl.java 에서 insert() 메소드 부분만

	public int insert(Board board) {
		return sst.insert("boardns.insert",board);
	}

- Mapper 파일 Board.xml 에서 id 가 "insert" 인 SQL문 부분만

	<insert id="insert" parameterType="board">
	<!--<selectKey keyProperty="num" 
			order="BEFORE" resultType="int">
			select nvl(max(num),0) + 1 from board
		</selectKey> -->
		insert into board values (#{num},#{writer},#{subject},
			#{content},#{email},0,#{passwd},#{ref},
			#{re_step},#{re_level},#{ip},sysdate,'n')
	</insert>

- 나중엔 주석을 풀 것, 나중에 설명할 것

- 글 작성(삽입) SQL문이므로 컬럼 del 에는 "n" 을 넣어줌

+ 삭제 시 "y" 를 넣음

 

- View 페이지 insert.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ include file="header.jsp"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<c:if test="${result > 0 }">
		<script type="text/javascript">
			alert("입력 성공");
			location.href = "list.do";
		</script>
	</c:if>
	<c:if test="${result <= 0 }">
		<script type="text/javascript">
			alert("입력 실패");
			history.go(-1);
		</script>
	</c:if>
</body>
</html>

 - 넘어온 result 값으로 입력 성공 / 실패 처리를 한다

- 입력 성공시 목록 페이지로 이동, 이 프로젝트 전체 목록을 구할때는 페이지값을 넘겨주지 않아도 된다

- 즉, 글 작성 / 댓글 작성 성공시 "list.do" 로 요청한다


목록 페이지

- 글 작성 / 댓글 작성 성공시 "list.do" 로 요청한다

- 검색 목록을 요청할때도 "확인" 버튼 클릭시 다시 "list.do" 로 요청한다

- 이때는 hidden 객체로 pageNum 에 1 을 저장해서 넘어옴

- 그러므로 전체 목록인지 검색 목록인지를 Controller 클래스 "list.do" 요청 부분에서 구분해서 처리해야한다

 

- Controller 클래스에서 "list.do" 요청 부분만

	@RequestMapping("list.do")	// 전체 목록, 검색 목록
	public String list(String pageNum, Board board, Model model) {
		final int rowPerPage = 10;	// 화면에 출력할 데이터 갯수
		if (pageNum == null || pageNum.equals("")) {
			pageNum = "1";
		}
		int currentPage = Integer.parseInt(pageNum); // 현재 페이지 번호
		
		// int total = bs.getTotal();
		int total = bs.getTotal(board); // 검색 (데이터 갯수)
		
		int startRow = (currentPage - 1) * rowPerPage + 1;
		int endRow = startRow + rowPerPage - 1;
		
		PagingPgm pp = new PagingPgm(total, rowPerPage, currentPage);
		board.setStartRow(startRow);
		board.setEndRow(endRow);
		// List<Board> list = bs.list(startRow, endRow);
		int no = total - startRow + 1;		// 화면 출력 번호
		List<Board> list = bs.list(board);
		
		model.addAttribute("list", list);
		model.addAttribute("no", no);
		model.addAttribute("pp", pp);
		// 검색
		model.addAttribute("search", board.getSearch());
		model.addAttribute("keyword", board.getKeyword());
		
		return "list";
	}

<넘어오는 값>

- 글 작성 후 "list.do" 로 요청해서 넘어왔을때는 pageNum 이 넘어오지 않는다, null 이 된다

- 검색 창에서 "확인" 을 눌러서 "list.do" 로 요청해서 넘어왔을때는 pageNum 값이 넘어온다

- 검색을 했을때 넘어오는 search , keyword 값들이 매개변수에 선언한 DTO Board 객체 board 에 저장되게 된다

- 즉 전체 목록을 구하고자 할때는 search, keyword 값이 넘어오지 않을 것이고, 검색 목록을 구하고자 할때는 search, keyword 값이 넘어올 것이다

- search, keyword 값 (DTO Board 객체 board) 이 넘어오는지 유무 에 따라 전체 목록을 구해올지, 부분 목록을 구해올지 판별 가능

+ DTO Board 클래스 안에 search, keyword 컬럼도 만들어져 있음

<기본 변수 & 파생변수>

- 페이지 번호 pageNum 가 전달되지 않았을떄는 pageNum 을 "1" 로 설정해줌

- 기본변수 rowPerPage : 화면에 출력할 데이터 개수

- 기본변수 currentPage : 현재 페이지 번호

- 기본변수 total : 총 데이터 개수 또는 검색된 데이터 총 개수, getTotal() 메소드를 호출해서 그룹함수 count 로 구함

<getTotal() 로 총 데이터 개수 구하기, 검색된 데이터 총 개수 구하기 구별하는 법>

- 전체 데이터 개수도 이 변수 total 에 저장되고, 검색된 데이터 개수도 이 변수 total 에 저장해야한다

- 같은 위치에서 전체 데이터 목록도 구하고, 검색시엔 검색된 데이터 목록을 구하기때문에 이렇게 처리해야함

- 그러므로 전체 데이터 개수를 구하는 것과 검색된 데이터 개수를 구하는 경우가 구분되어야함

- getTotal() 메소드를 호출하면서 search, keyword 를 저장한 DTO 객체 board 를 매개변수로 전달함

- 그러면, 총 데이터 개수를 구할땐 객체 board 가 null 이고 검색한 데이터 총 개수를 구할땐 객체 board 에 keyword, search 값이 존재한다

- Mapper 파일의 데이터 개수 구하는 SQL문 에서 동적 SQL문을 쓰고 있다

* getTotal( ) 메소드 아래에서 설명

<getTotal() 에서 돌아온 후>

- startRow, endRow 변수 값을 구한다

- PagingPgm 클래스 객체 pp 를 생성하면서 기본변수 3개 total, rowPerPage, currentPage 를 생성자의 매개변수로 전달

* PaginPgm 클래스 아래에 설명

<목록 구하기>

- 목록을 잘라주기 위해 startRow, endRow 를 매개변수에서 만들어진 DTO Board 객체 board 에 Setter 메소드로 세팅

+ DTO Board 클래스 안에 startRow, endRow 컬럼도 만들어져 있음

+ 화면 출력번호를 구해서 변수 no 에 저장

- 검색을 했을때는 DTO 객체 board 안에 search, keyword, startRow, endRow 값이 저장되어있다

- 검색을 하지 않았을때는 DTO 객체 board 안에 startRow, endRow 값만 저장되어있게 된다

- 목록을 구하기 위한 Service 클래스의 list() 메소드를 호출하며 board 를 매개변수로 전달

* list() 메소드 아래에 설명

<list() 에서 돌아온 후>

- 구한 목록을 list 에 저장 후 Model 객체에 저장해서 list.jsp 에 전달

- 페이징 처리에 필요한 값들을 저장한 PaginPgm 객체 pp 를 Model 객체에 저장해서 list.jsp 에 전달, View 에서 pp의 Getter 메소드로 변수들을 불러옴

- 화면 출력번호 no 도 Model 객체에 저장해서 list.jsp에 전달

- 검색했을때 검색된 리스트에서 페이징 처리를 하려면 search 와 keyword 가 필요하므로 search, keyword 도 Model 객체에 저장해서 list.jsp 에 전달

 

+ list.jsp 로 search, keyword 를 전달해야하는 이유

- list.jsp 로 이동할때는 search, keyword 가 필요함

- 검색했을때 검색된 리스트에서 페이징 처리를 하려면 search 와 keyword 가 필요하다

- list.jsp 에서 search, keyword 를 쓰는 코드 (아래)

- ${search} 로 가져오고 있다

- 또한 list.jsp 에서 search, keyword 를 쓰는 코드 (아래)

- keyword 값이 empty 면 검색을 하지 않은 경우, keyword 값이 empty 가 아니면 검색을 한 경우 로 if 태그로 나눠서 처리

- 즉, 전체 데이터 목록도 페이징 처리를 따로 하고, 검색한 데이터도 페이징 처리를 따로 해야하므로 keyword 필요

- 페이징 처리 = [1] [2] [3] 같은 페이지 선택할 수 있는 메뉴바 만들고 원하는 페이지를 클릭할 수 있게 하는 것

- 전체 데이터 목록 출력시에는 페이지 번호만 가지고 가지만 검색된 데이터를 구할때는 "list.do" 로 요청하면서 search, keyword 를 가져감

- search, keyword 를 가져가야만 Controller 의 "list.do" 처리 부분에서, 검색된 데이터 목록 요청인지 전체 데이터 목록 요청인지 판별 가능

* 위에서 설명했음


- Service 클래스 BoardServiceImpl.java 에서 getTotal() 메소드 부분만

	public int getTotal(Board board) {
		return bd.getTotal(board);
	}

- DAO 클래스 BoardDaoImpl.java 에서 getTotal() 메소드 부분만

	public int getTotal(Board board) {
		return sst.selectOne("boardns.getTotal",board);
	}

- Mapper 파일 Board.xml 에서 id 가 "getTotal" 인 SQL문 부분만

	<select id="getTotal" parameterType="board" resultType="int">
		select count(*) from board 
		<where>
			<if test="keyword != null and search !='subcon'">
				${search} like '%'||#{keyword}||'%'
			</if>
			<if test="keyword != null and search=='subcon'">
				subject like '%'||#{keyword}||'%' or
				content like '%'||#{keyword}||'%'
			</if>
		</where>
	</select>

- 가장 위의 select 문은 count(*) 그룹함수를 사용해서 총 데이터 개수를 구하는 코드이다

- where 절 대신 where 태그를 사용해서 동적 SQL문으로 작성되었다

<전체 데이터 총 개수를 가져올때>

- 전체 데이터를 가져올때는 keyword 도 null 이고 search 도 null 이므로 where 태그 안에 만족하는 조건이 없게 된다

- select count(*) from board 만 적용되어 전체 데이터의 개수를 가져오게 된다

<where 태그, if 태그 사용>

- 동적 SQL문 이다

- where 태그와 if 태그로 특정 조건을 만족할때만 where 절을 추가하는 것과 같은 효과

<keyword 가 null 이 아니고 search 가 'subcon'(제목 + 내용) 인 경우>

- 즉 검색 대상이 제목, 내용, 작성자 인 경우

- search 에 저장된 값은 subject, content, writer 등이 될 수 있다, 이처럼 가변적인 값인 경우 #{search} 가 아닌 ${search} 로 작성해야한다

- keyword 에 저장된 값을 포함하는 데이터의 개수를 검색하게 된다

+ || 로 문자열 연결

<keyword 가 null 이 아니고 search 가 'subcon'(제목 + 내용) 인 경우>

- 즉 검색 대상이 제목 + 내용 인 경우

- or 연산자로 연결해서 subject, content 컬럼에서 특정 키워드를 포함한 데이터의 개수를 검색하게 된다

- 설명보다는 코드를 보기

+ JSTL 의 if 태그와 비슷

 

+ 동적 SQL문

- JSTL 의 태그들과 비슷하다, if, choose, when, otherwise, foreach 태그 등

- 검색기능을 구현할때 사용

- where 태그와 if 태그로 특정 조건을 만족할때만 where 절을 추가하는 것과 같은 효과

- 나중에 찾아보고 공부하기


PagingPgm 클래스

- PagingPgm.java

package board1.service;

public class PagingPgm {
	private int total;				// 데이터 갯수
	private int rowPerPage;			// 화면에 출력할 데이터 갯수
	private int pagePerBlk = 10;    // 블럭당 페이지 갯수 (1개의 블럭당 10개의 페이지)
	private int currentPage;		// 현재 페이지 번호
	private int startPage;			// 각 블럭의 시작 페이지
	private int endPage;            // 각 블럭의 끝 페이지
	private int totalPage;			// 총 페이지 수

	public PagingPgm(int total, int rowPerPage, int currentPage) {
		this.total = total;
		this.rowPerPage = rowPerPage;
		this.currentPage = currentPage;
		
		totalPage = (int) Math.ceil((double) total / rowPerPage);
		startPage = currentPage - (currentPage - 1) % pagePerBlk;	// 1,  11, 21...
		endPage = startPage + pagePerBlk - 1;				// 10, 20, 30...
		if (endPage > totalPage)
			endPage = totalPage;
	}

	public int getTotal() {
		return total;
	}

	public void setTotal(int total) {
		this.total = total;
	}

	public int getRowPerPage() {
		return rowPerPage;
	}

	public void setRowPerPage(int rowPerPage) {
		this.rowPerPage = rowPerPage;
	}

	public int getPagePerBlk() {
		return pagePerBlk;
	}

	public void setPagePerBlk(int pagePerBlk) {
		this.pagePerBlk = pagePerBlk;
	}

	public int getCurrentPage() {
		return currentPage;
	}

	public void setCurrentPage(int currentPage) {
		this.currentPage = currentPage;
	}

	public int getStartPage() {
		return startPage;
	}

	public void setStartPage(int startPage) {
		this.startPage = startPage;
	}

	public int getEndPage() {
		return endPage;
	}

	public void setEndPage(int endPage) {
		this.endPage = endPage;
	}

	public int getTotalPage() {
		return totalPage;
	}

	public void setTotalPage(int totalPage) {
		this.totalPage = totalPage;
	}
}

- 기본변수, 파생변수들이 필드이다

- 기본변수 total, rowPerPage, currentPage

- 파생변수 startPage, endPage, totalPage

- pagePerBlk : 블럭 당 페이지 개수, 즉 1 개 블럭 당 10개의 페이지로 설정했다

- 생성자 매개변수로 기본변수 3개를 전달받아서 파생 변수 값들을 구해준다

- 아래쪽엔 Getter / Setter 메소드로 만들어져 있다

- 이런식으로 따로 클래스를 만들어서 페이징 처리를 하는 경우도 많다


 

- Mapper 파일 Board.xml 에서 id 가 "list" 인 SQL문 부분만

	<!-- <select id="list" parameterType="hashMap" resultMap="boardResult"> -->
	<select id="list" parameterType="board" resultMap="boardResult">
		select * from (select a.*,rowNum rn from (
			select * from board
		<where>
			<if test="keyword != null and search!='subcon'">
				${search} like '%'||#{keyword}||'%'
			</if>
			<if test="keyword != null and search=='subcon'">
				subject like '%'||#{keyword}||'%' or
				content like '%'||#{keyword}||'%'
			</if>
		</where>			
			 order by ref desc,re_step) a )
			where rn between #{startRow} and #{endRow}
	</select>

- 첫번째 서브쿼리는 rowNum 컬럼에 대한 별칭을 rn 으로 지정하는 역할

<where 태그 시작>

<keyword 가 null 이 아니고 search 가 'subcon'(제목 + 내용) 인 경우>

- 즉 검색 대상이 제목, 내용, 작성자 인 경우

- search 에 저장된 값은 subject, content, writer 등이 될 수 있다, 이처럼 가변적인 값인 경우 #{search} 가 아닌 ${search} 로 작성해야한다

- keyword 에 저장된 값을 포함하는 데이터의 개수를 검색하게 된다

+ || 로 문자열 연결

<keyword 가 null 이 아니고 search 가 'subcon'(제목 + 내용) 인 경우>

- 즉 검색 대상이 제목 + 내용 인 경우

- or 연산자로 연결해서 subject, content 컬럼에서 특정 키워드를 포함한 데이터의 개수를 검색하게 된다

- 즉 해당 검색어를 포함한 제목이 있거나 해당 검색어를 포함한 내용이 있다면 그 데이터들만 가져옴

<where 태그 끝난 후>

- 검색을 먼저하고 정렬을 나중에 해야한다, 그러므로 where 조건 후 order by 가 와야함

- 두번째 서브쿼리에서 정렬을 해야할때, ref 로 내림차순, re_step 으로 오름차순 정렬

- 객체 board 에 저장되어 넘어온 startRow, endRow 값을 where 절에 사용

+ between A and B = A 이상 B 이하

+ 해당 SQL문에 resultMap 이 있지만 지금은 board 테이블 컬럼과 DTO Board 의 프로퍼티 명이 같으므로 쓰지 않아도 됨

- DTO Board 에 테이블 board 에는 없는 프로퍼티가 있는 경우여도 이름이 같으므로 모두 자동 매핑 되므로 resultMap 을 쓰지 않아도 된다


- View 페이지 list.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ include file="header.jsp"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<div class="container" align="center">
		<h2 class="text-primary">게시판 목록</h2>
		<table class="table table-striped">
			<tr>
				<td>번호</td>
				<td>제목</td>
				<td>작성자</td>
				<td>작성일</td>
				<td>조회수</td>
			</tr>
			
			<c:if test="${empty list}">
				<tr>
					<td colspan="5">데이터가 없습니다</td>
				</tr>
			</c:if>
			
			<c:if test="${not empty list}">
				<c:set var="no1" value="${no }"></c:set>
				<c:forEach var="board" items="${list }">
					<tr>
						<td>${no1}</td>
						<c:if test="${board.del =='y' }">
							<td colspan="4">삭제된 데이터 입니다</td>
						</c:if>
						<c:if test="${board.del !='y' }">
							<td><a href="view.do?num=${board.num}&pageNum=${pp.currentPage}"
							       class="btn btn-default"> 
								<c:if test="${board.re_level >0 }">
										<img alt="" src="images/level.gif" height="2"
											 width="${board.re_level *5 }">
										<img alt="" src="images/re.gif">
								</c:if> ${board.subject} 
								<c:if test="${board.readcount > 30 }">
										<img alt="" src="images/hot.gif">
								</c:if></a></td>
							<td>${board.writer}</td>
							<td>${board.reg_date}</td>
							<td>${board.readcount}</td>
						</c:if>
					</tr>
					<c:set var="no1" value="${no1 - 1}"/>
				</c:forEach>
			</c:if>
		</table>
		
		<form action="list.do">
			<input type="hidden" name="pageNum" value="1"> 
			<select	name="search">
				<option value="subject"	<c:if test="${search=='subject'}">selected="selected" </c:if>>제목</option>
				<option value="content"	<c:if test="${search=='content'}">selected="selected" </c:if>>내용</option>
				<option value="writer"	<c:if test="${search=='writer'}">selected="selected" </c:if>>작성자</option>
				<option value="subcon"	<c:if test="${search=='subcon'}">selected="selected" </c:if>>제목+내용</option>
			</select> 
			<input type="text" name="keyword"> 
			<input type="submit" value="확인">
		</form>
		
		<ul class="pagination">
			<!-- 검색 했을 경우의 페이징 처리 -->
			<c:if test="${not empty keyword}">
				<c:if test="${pp.startPage > pp.pagePerBlk }">
					<li><a
						href="list.do?pageNum=${pp.startPage - 1}&search=${search}&keyword=${keyword}">이전</a></li>
				</c:if>
				<c:forEach var="i" begin="${pp.startPage}" end="${pp.endPage}">
					<li <c:if test="${pp.currentPage==i}">class="active"</c:if>><a
						href="list.do?pageNum=${i}&search=${search}&keyword=${keyword}">${i}</a></li>
				</c:forEach>
				<c:if test="${pp.endPage < pp.totalPage}">
					<li><a
						href="list.do?pageNum=${pp.endPage + 1}&search=${search}&keyword=${keyword}">다음</a></li>
				</c:if>
			</c:if>
			
			<!-- 전체 목록의 페이징 처리 -->
			<c:if test="${empty keyword}">
				<c:if test="${pp.startPage > pp.pagePerBlk }">
					<li><a href="list.do?pageNum=${pp.startPage - 1}">이전</a></li>
				</c:if>
				<c:forEach var="i" begin="${pp.startPage}" end="${pp.endPage}">
					<li <c:if test="${pp.currentPage==i}">class="active"</c:if>>
						<a href="list.do?pageNum=${i}">${i}</a></li>
				</c:forEach>
				<c:if test="${pp.endPage < pp.totalPage}">
					<li><a href="list.do?pageNum=${pp.endPage + 1}">다음</a></li>
				</c:if>
			</c:if>
		</ul>
		<div align="center">
			<a href="insertForm.do" class="btn btn-info">글 입력</a>
		</div>
	</div>
</body>
</html>

- 위의 list.jsp 코드가 길므로 나눠서 캡처하면서 설명

<목록 출력>

- if 태그를 사용해서 list 가 있는 경우, list 를 출력

- 이때 Model 객체에 넘어온 화면 출력번호 no 를 가져와서 변수 no 에 저장

- 이제 list 를 forEach 의 items 에 넣어서 변수 board 로 받아서 하나씩 글을 출력, 이때 각 글의 del 컬럼이 "y" 인지 "n" 인지에 따라 나눔

- 삭제된 글은 del 컬럼이 "y" 이므로 "삭제된 데이터입니다" 를 두번째 td 자리에 출력

- 삭제된 글이 아니면 del 컬럼이 "n" 이므로 정상적으로 이미지, 제목등을 출력하고 링크를 걸어서 상세페이지로 가기 위해 "view.do" 로 요청, 이때 글 번호와 페이지 번호 가져감

- 댓글이 경우 이미지를 출력시켜 댓글임을 알림

- 조회수 값이 특정 값 이상이면 특정 이미지(hot) 를 불러와서 인기있는 글이라고 알려줌

- 현재 list 에는 검색된 목록이 있을 수도, 전체 목록이 있을수도 있다, 그러므로 그냥 list 를 출력하면 됨

- 하지만 페이징 처리는 다르다

<검색 창 부분>

- 선택한 select 와 사용자가 입력한 값을 가지고 다시 "list.do" 로 요청한다

- 이렇게 "list.do" 로 요청하면 Controller 에서는 search, keyword 값이 저장된 DTO 객체가 null 이 아니게 되므로 검색 목록요청인지 전체 목록 요청인지 구별 가능하다

- 페이징 처리는 검색된 목록, 전체 목록 나눠서 페이징 처리를 해야한다! * 이유는 아래에서 설명

 

<페이징 처리 : 검색된 목록>

- 검색 목록인 경우엔 Controller 에서 Model 로 keyword 를 전달할때 이미 keyword 값이 존재했으므로 keyword 가 null 이 아니다, 그러므로 위의 코드가 실행됨

- Controller 에서 "list.do" 요청 처리 부분 코드를 보면, 전체 데이터 목록을 원할땐 search, keyword 값이 없고, 검색된 데이터 목록을 원할땐 search, keyword 값이 넘어오도록 처리했다

- 그래서 검색한 리스트 목록을 출력하는 경우는 페이지 번호 뿐 아니라 search, keyword 도 같이 전달하며 "list.do" 로 요청해야한다

<페이징 처리 : 전체 목록>

- 전체 목록인 경우엔 Controller 에서 Model 로 keyword 를 전달할때 keyword 값이 없었으므로 keyword 가 null 이다, 그러므로 위의 코드가 실행됨

- 전체 데이터 목록을 가져올때 페이지 메뉴에서 특정 페이지 클릭시 View 에서 "list.do" 로 요청하면서 페이지 번호를 전달함

- Model 로 전달된 PagingPgm 객체 pp 로 각 변수들을 가져와서 페이징 처리를 한다

- 첫번째 블럭은 pp.startPage 가 1 이고, pp.pagePerBlk 가 10이므로 만족하지 않으므로 '이전' 메뉴가 없다

- 존재하는 페이지까지만 forEach 문을 통해 페이지 번호를 출력하고 있다

- 이떄 페이지 번호가 현재 페이지와 같을때는 class="active" 로 부트스트랩을 적용해서 디자인 적용

- 전체 목록 페이지 처리이므로 "list.do" 로 요청하면서 search, keyword 를 전달하지 않음, 그래야 Controller 에서 전체 목록 페이지 처리로 인식한다

 

페이징 처리를 전체 목록, 검색 목록 따로 처리하는 이유 2가지

1. Controller 클래스에서 글의 전체 개수를 저장한 total 값이 다르기 때문에

2. 클릭하면 가지고 가는 값도 다르기 때문에

ex) 전체 목록 페이징 처리에서 이전, 다음 같은 메뉴를 누르면 "list.do" 로 요청하면서 페이지 번호만 가지고 감

ex) 검색 목록 페이징 처리에서 이전, 다음 같은 메뉴를 누르면 "list.do" 로 요청하면서 페이지 번호 뿐 아니라 search, keyword 도 가져가야만함

복습

파일명 중복문제 해결

- fileupload 라이브러리 사용시 중복문제를 자동 해결해주지 않음
- UUID (java.util) 클래스는 통해 문자형태의 난수값을 만들어 준다

- 이 난수값을 String 형으로 변환하고 확장자를 결합해서 중복문제 해결

- 이 방법 외에도 다른 방법들도 있다

 

- 난수를 발생시키는 방법을 정리한 예제 RandomFile.java

import java.util.UUID;

public class RandomFile {

	public static void main(String[] args) {
		// TODO Auto-generated method stub

		String filename = "clock.jpg";
		String extension = filename.substring(filename.lastIndexOf("."), filename.length());
		System.out.println("extension:"+extension);
		
		UUID uuid = UUID.randomUUID();
		System.out.println("uuid:"+uuid);
		
		String newfilename = uuid.toString() + extension;
		System.out.println("newfilename:"+newfilename);
		
	}

}

1. clock.jpg 라는 첨부파일을 저장한다고 하면,  확장자를 subsgring() 을 사용해서 분리한 후 extension 에 저장

2. UUID 클래스로 난수를 발생시킨다

3. 그 난수를 String 형으로 변환시킨 후 확장자를 붙이면 파일명을 문자형태 난수로 만들 수 있다

- 난수가 중복될 확률은 현저히 낮다

 

첨부파일명 컬럼 크기 주의

- join_profile 컬럼에 난수화된 파일명이 들어가 있다

- 문자형 난수값이 들어가기때문에 첨부파일명 저장되는 컬럼 크기를 50Byte 이상으로 해두기

 


첨부파일 입력양식 name 값 주의

- member_join.jsp 부분

    <tr>
     <th>프로필사진</th>
     <td>
      <input type="file" name="join_profile1" />
     </td>
    </tr>

- DTO 에서 첨부파일명이 저장되는 컬럼과 폼에서 첨부파일 입력양식의 name 을 같은 이름으로 써선 안된다!

- DTO 에 저장하는건 첨부파일"명"이고 첨부파일 입력양식에서 입력하는건 파일이다

- 그 join_profile1 은 여기 @RequestParam("") 에 들어가는 값이다

- 그리고 의도한대로 DB 에 저장할 파일명 프로퍼티명과 폼의 name 이 일치하지 않아으므로 DTO 객체에 자동으로 매핑되어 저장되지 않게 됨

- 이후 따로 중복문제를 처리한 파일명을 구해서 Setter 메소드로 세팅해야한다

- newfile 은 중복문제 해결 후의 파일명

 

INSERT SQL 문 작성시 주의

    <!-- 회원저장 -->
    <insert id="member_join" parameterType="member">
     insert into join_member (join_code,join_id,join_pwd,join_name,
     join_zip1,join_addr1,join_addr2,join_tel,join_phone,join_email,join_profile,
     join_regdate,join_state) values(join_member_joincode_seq.nextval,
     #{join_id},#{join_pwd},#{join_name},
     #{join_zip1},#{join_addr1},#{join_addr2},#{join_tel},
     #{join_phone},#{join_email},#{join_profile, jdbcType=VARCHAR},sysdate,1)
    </insert>
출처: https://laker99.tistory.com/149?category=1087168 [레이커 갓생일기:티스토리]

- 사용자가 첨부파일을 선택할 수도 있고 선택하지 않을 수도 있다

- MyBatis 는 null 값을 허용하지 않기때문에 null 값이 들어가면 오류가 생김

- jdbcType=VARCHAR 속성을 넣어서 null 값을 허용시켜야 한다

 

Controller 클래스에서 바로 다시 요청하기

		return "redirect:member_login.do";

- Controller 클래스에서 위 문장을 작성하면, 다시 "member_login.do" 로 요청하며 로그인 폼으로 가게 됨

- 이전까지는 Controller 클래스에서 View 페이지로만 이동했다

- 만약 return 하며 다시 요청을 할때는 "redirect:" 를 붙여야한다

 

 

회원 관리 프로그램 (이어서)

id 중복검사

- member.jsp 부분

	//아이디 중복확인
    $.ajax({
        type:"POST",
        url:"member_idcheck.do",
        data: {"memid":memid},        
        success: function (data) { 
        	alert("return success="+data);
      	  if(data==1){	//중복 ID
      		var newtext='<font color="red">중복 아이디입니다.</font>';
      			$("#idcheck").text('');
        		$("#idcheck").show();
        		$("#idcheck").append(newtext);
          		$("#join_id").val('').focus();
          		return false;
	     
      	  }else{	//사용 가능한 ID
      		var newtext='<font color="blue">사용가능한 아이디입니다.</font>';
      		$("#idcheck").text('');
      		$("#idcheck").show();
      		$("#idcheck").append(newtext);
      		$("#join_pwd1").focus();
      	  }  	    	  
        }
        ,
    	  error:function(e){
    		  alert("data error"+e);
    	  }
      });//$.ajax

- ajax() 메소드를 사용했고 url 로 "member_idcheck.do" 로 요청한다, 전달할 데이터는 json 형태로 전달

- 여기서 전달한 값은 받는 쪽에서 request.getParameter() 또는 @RequestParam("") 으로 받음

- 콜백함수로 1 또는 -1 을 돌려받도록 했다, 그것에 맞춰서 처리를 함

 

- Controller 클래스 MemberAction.java 에서 "member_idcheck.do" 요청 처리 부분만

	// ID중복검사 ajax함수로 처리부분
	@RequestMapping(value = "/member_idcheck.do", method = RequestMethod.POST)
	public String member_idcheck(@RequestParam("memid") String id, Model model) throws Exception {
		System.out.println("id:"+id);
		
		int result = memberService.checkMemberId(id);
		model.addAttribute("result", result);

		return "member/idcheckResult";
	}

- 요청을 받아서 @RequestParam("memid") 로 전달된 값 memid 를 받아온다

- 이 memid 는 사용자가 입력했던 아이디이다

- checkMemberId() 메소드를 호출하며 중복검사를 한다 * 아래에 설명

- 그 결과 1 또는 -1 을 result 변수로 받고 그걸 Model 객체에 저장 후 jsp/idcheckResult.jsp 로 이동

 

- Service 클래스 MemberServiceImpl.java 에서 checkMemberId() 메소드 부분만

	public int checkMemberId(String id) throws Exception{
		return memberDao.checkMemberId(id);
	}

- DAO 클래스 MemberDaoImpl.java 에서 checkMemberId() 메소드 부분만

	/***** 아이디 중복 체크 *****/
//	@Transactional
	public int checkMemberId(String id) throws Exception {
		int re = -1;	// 사용 가능한 ID
		MemberBean mb = sqlSession.selectOne("login_check", id);
		if (mb != null)
			re = 1; 	// 중복id
		return re;
	}

- 검색된 결과가 있으면 중복 id 이므로 -1 이 리턴되고, 검색된 결과가 없으면 중복이 아니므로 1 을 리턴한다

 

- Mapper 파일 member.xml 에서 id 가 "login_check" 인 SQL문 부분만

    <!-- 로그인 인증 -->
    <select id="login_check" parameterType="String" resultType="member">
     select * from join_member where join_id=#{id} and join_state=1
    </select>

- 이제는 검색할때 where 조건문에 join_state = 1 이라는 조건을 추가해서 현재 가입된 회원중에서만 검색해야한다

- 해당 id 의 회원 상세정보를 DTO 객체에 저장해서 돌려준다

- 해당 SQL문은 id 중복검사시에도 사용하고 로그인 시에도 사용함

 

- 다시 돌아가며 DAO -> Service -> Controller -> View 페이지로 넘어왔을때 

- idCheckResult.jsp

${result}

- 이 값이 출력되므로 member.js 의 콜백함수로 반환되는 값임, 성공시 1 이 반환, 실패시 -1 이 반환됨


로그인 기능

- 가입이 끝나고 로그인 부분을 보자

		return "redirect:member_login.do";

- 회원가입이 끝나면 Conroller 클래스에서 return redirect: 에 의해 "member_login.do" 로 요청한다

 

- Controller 클래스 MemberAction.java 에서 "member_login.do" 요청 부분만

	/* 로그인 폼 뷰 */
	@RequestMapping(value = "/member_login.do")
	public String member_login() {
		return "member/member_login";
		// member 폴더의 member_login.jsp 뷰 페이지 실행
	}

- 로그인 폼 member_login.jsp 로 가게 됨

 

- member_login.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
    
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>로그인</title>
<link rel="stylesheet" type="text/css" href="<%=request.getContextPath()%>/css/admin.css" />
<link rel="stylesheet" type="text/css" href="<%=request.getContextPath()%>/css/member.css" />
<!-- <script src="./js/jquery.js"></script> -->
<script src="http://code.jquery.com/jquery-latest.js"></script>

<script>
 function check(){
	 if($.trim($("#id").val())==""){
		 alert("로그인 아이디를 입력하세요!");
		 $("#id").val("").focus();
		 return false;
	 }
	 if($.trim($("#pwd").val())==""){
		 alert("비밀번호를 입력하세요!");
		 $("#pwd").val("").focus();
		 return false;
	 }
 }
 
 /*비번찾기 공지창*/
 function pwd_find(){
	 window.open("pwd_find.do","비번찾기","width=450,height=500");
	 //자바 스크립트에서 window객체의 open("공지창경로와 파일명","공지창이름","공지창속성")
	 //메서드로 새로운 공지창을 만듬.폭이 400,높이가 400인 새로운 공지창을 만듬.단위는 픽셀
 }
</script>
</head>
<body>
 <div id="login_wrap">
  <h2 class="login_title">로그인</h2>
  <form method="post" action="member_login_ok.do" onsubmit="return check()">
   <table id="login_t">
    <tr>
     <th>아이디</th>
     <td>
      <input name="id" id="id" size="20" class="input_box" />
     </td>
    </tr>
    
    <tr>
     <th>비밀번호</th>
     <td>
     <input type="password" name="pwd" id="pwd" size="20" class="input_box"/>
     </td>
    </tr>
   </table>
    <div id="login_menu">
    <input type="submit" value="로그인" class="input_button" />
    <input type="reset" value="취소" class="input_button"
    		onclick="$('#id').focus();" />
    <input type="button" value="회원가입" class="input_button"
    		onclick="location='member_join.do'" />
    <input type="button" value="비번찾기" class="input_button"
    		onclick="pwd_find()" />
    </div>
  </form>
 </div>
</body>
</html>

+ submit 버튼은 하나여야 한다

- 유효성 검사, 비밀번호 찾기 팝업창을 띄워주는 함수 정의, 입력양식 으로 구성되어 있음

- 아이디, 비밀번호를 입력 후 "로그인" 버튼 클릭시 "member_login_ok.do" 로 요청, 그 요청에서 아이디, 비번이 일치하는지 DB에서 확인한다

 

- Controller 클래스 MeberAction.java 에서 "member_login_ok.do" 요청 부분만

	/* 로그인 인증 */
	@RequestMapping(value = "/member_login_ok.do", method = RequestMethod.POST)
	public String member_login_ok(@RequestParam("id") String id, 
			                      @RequestParam("pwd") String pwd,
			                      HttpSession session, 
			                      Model model) throws Exception {
		int result=0;		
		MemberBean m = memberService.userCheck(id);

		if (m == null) {	// 등록되지 않은 회원일때
			
			result = 1;
			model.addAttribute("result", result);
			
			return "member/loginResult";
			
		} else {			// 등록된 회원일때
			if (m.getJoin_pwd().equals(pwd)) {// 비번이 같을때
				session.setAttribute("id", id);

				String join_name = m.getJoin_name();
				String join_profile = m.getJoin_profile();

				model.addAttribute("join_name", join_name);
				model.addAttribute("join_profile", join_profile);

				return "member/main";
				
			} else {// 비번이 다를때
				result = 2;
				model.addAttribute("result", result);
				
				return "member/loginResult";				
			}
		}

	}

- 사용자가 입력한 아이디 , 비밀번호를 @RequestParam 으로 받음

- 회원인증 성공시 SESSION 으로 공유설정해야하므로 session 객체를 매개변수에 선언해서 받는다, 자동으로 생성됨

- Service 클래스의 userCheck() 메소드를 호출해서 회원 인증을 한다, 이때 id 를 전달

- id 만으로 DB에서 해당 id를 가진 데이터가 있는지 확인 후 그 데이터를 가져와서 객체 m 에 저장

- 데이터가 있는 경우에는 그 데이터에서 DB 비밀번호를 m.getJoin_pwd() 로 가져와서 사용자가 입력한 비밀번호가 맞는지 확인하고 있다

- 아이디에 해당하는 데이터가 존재하고, 비밀번호도 일치시 session 객체로 id 를 공유설정함

- 로그인이 되어있는 한 계속 SESSION 값 id 가 공유됨, 사용 가능

- id 가 틀렸을땐 변수 result 에 1을 저장, 비번 틀렸을떈 변수 result 에 2 를 저장

- 이후로그인 성공시 main.jsp 로 이동, 로그인 실패시 loginResult.jsp 로 이동해서 처리

- main.jsp 로 이동할때 사용자의 이름, 프로필 첨부파일명을 Model 객체에 저장해서 전달

 

- Service 클래스 MemberServiceImpl.java 에서 userCheck() 메소드 부분만

	public MemberBean userCheck(String id) throws Exception{
		return memberDao.userCheck(id);		
	}

- DAO 클래스 MemberDaoImpl.java 에서 userCheck() 메소드 부분만

	/* 로그인 인증 체크 */
//	@Transactional
	public MemberBean userCheck(String id) throws Exception {
		return sqlSession.selectOne("login_check", id);
	}

- Mapper 파일 member.xml 에서 id 가 "login_check" 인 SQL문 부분만

    <!-- 로그인 인증 -->
    <select id="login_check" parameterType="String" resultType="member">
     select * from join_member where join_id=#{id} and join_state=1
    </select>

- id 중복체크 시에도 해당 SQL문을 사용했었음

 

- 로그인 실패시엔 loginResult.jsp 로 이동

- loginResult.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>


<c:if test="${result == 1}">
	<script>
		alert("등록되지 않는 회원 입니다.");
		history.go(-1);
	</script>
</c:if>   

<c:if test="${result == 2}">
	<script>
		alert("회원정보가 틀렸습니다.");
		history.go(-1);
	</script>
</c:if>

 

- 로그인 성공시 일종의 마이페이지인 main.jsp 로 이동

- main.jsp

<%@ page language="java" 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">
<title>사용자 메인화면</title>
<link rel="stylesheet" type="text/css" href="./css/main.css" />
<link rel="stylesheet" type="text/css" href="./css/admin.css" />
</head>
<body>

<c:if test="${sessionScope.id == null }"> 
  <script>
   alert("다시 로그인 해주세요!");
   location.href="<%=request.getContextPath()%>/member_login.do";
  </script>
</c:if>

<c:if test="${sessionScope.id != null }">  
 <div id="main_wrap">
   <h2 class="main_title">사용자 메인화면</h2>  
   <form method="post" action="member_logout.do"> 
   <table id="main_t">
    <tr>
     <th colspan="2">
     <input type="button" value="정보수정" class="input_button"
     		onclick="location='member_edit.do'" />
     <input type="button" value="회원탈퇴" class="input_button"
     		onclick="location='member_del.do'" />
     <input type="submit" value="로그아웃" class="input_button" />     
     </th>
    </tr>
    
    <tr>
     <th>회원이름</th>
     <td>${join_name}님 로그인을 환영합니다</td>
    </tr>
    
    <tr>
     <th>프로필사진</th>
     <td>
       <c:if test="${empty join_profile}">
       &nbsp;
       </c:if>
       <c:if test="${!empty join_profile}">
       <img src="<%=request.getContextPath() %>/upload/${join_profile}" height="100" width="100" />
       </c:if>
     </td>
    </tr>
   </table>   
   </form>
 </div>
</c:if>

</body>
</html>

- 전체를 if 태그로 감싸서 세션이 있는 경우에만 해당 내용이 출력됨

- 로그인 성공 후, 또는 수정 성공 후 여기로 이동하므로, "main.do" 는 인터셉터 매핑을 잡아두지 않았고, 여기서 세션값이 없을땐 로그인 폼으로 보내주는 처리를 한다

* 프로필 이미지 출력은 나중에 정보 수정 후 main.jsp 로 돌아왔을때 수정


+ 인터셉터 설정

- servlet-context.xml 부분

	<!-- 인터셉터 설정 -->	
	<beans:bean id="sessionChk" class="myspring.controller.SessionCheckInter"/>	
	 <interceptors>
		<interceptor>
			<mapping path="/member_edit.do"/>
			<mapping path="/member_edit_ok.do"/>			
			<mapping path="/member_del.do"/>
			<mapping path="/member_del_ok.do"/>
			<mapping path="/member_logout.do"/>
			<beans:ref bean="sessionChk"/>
		</interceptor>
	</interceptors>

- 로그인해야만 쓸 수 있는 메뉴 요청에 대해 매핑을 잡아둠

- 인터셉터 사용 이유 : 비정상적인 접근(세션이 없는 경우) 을 막기 위해서



로그아웃 기능

 

버튼 처리 : "로그아웃" 버튼 클릭시

   <form method="post" action="member_logout.do">
     <input type="button" value="정보수정" class="input_button"
     		onclick="location='member_edit.do'" />

- main.jsp 의 "로그아웃" 버튼은 submit 버튼이므로 action 에 있는 "member_logout.do" 로 요청한다

- 해당 요청은 인터셉터에 등록되었으므로 세션이 있을때만 Controler 의 member_logout.do 로 감

- 세션이 없으면 로그인 폼으로 이동

 

- Controller 클래스 MemberAction.java "member_logout.do" 요청 부분만

	// 로그아웃
	@RequestMapping("member_logout.do")
	public String logout(HttpSession session) {
		session.invalidate();

		return "member/member_logout";
	}

- 인터셉터를 통과해야 즉, 세션값이 있을때만 여기에 도착함

- 로그아웃 시 해야할 일 2가지 : 세션 삭제 , 로그아웃되었다는 메세지 뿌리고 로그인 폼으로 이동

 

- member_logout.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
  
<script>
  alert("로그아웃 되었습니다!");
  location="member_login.do";
</script>

정보 수정폼 기능

 

버튼 처리 : "정보수정" 버튼 클릭시

     <input type="button" value="정보수정" class="input_button"
     		onclick="location='member_edit.do'" />

- main.jsp 에서 "정보수정" 클릭시 "member_edit.do" 로 요청

- 해당 요청은 인터셉터에 등록되었으므로 인터셉터를 거쳐 세션이 있을때만 Controler 의 member_edit.do 로 감

- 세션이 없으면 로그인 폼으로 이동

- "member_edit.do" 로 가면서 아무 값을 가져가지 않음, 세션값을 활용한다

 

- Controller 클래스 MemberAction.java "member_edit.do" 요청 부분만

	/* 회원정보 수정 폼 */
	@RequestMapping(value = "/member_edit.do")
	public String member_edit(HttpSession session, Model m) throws Exception {

		String id = (String) session.getAttribute("id");

		MemberBean editm = memberService.userCheck(id);

		String join_tel = editm.getJoin_tel();
		StringTokenizer st01 = new StringTokenizer(join_tel, "-");
		// java.util 패키지의 StringTokenizer 클래스는 첫번째 전달인자를
		// 두번째 -를 기준으로 문자열을 파싱해준다.
		String join_tel1 = st01.nextToken();// 첫번째 전화번호 저장
		String join_tel2 = st01.nextToken(); // 두번째 전번 저장
		String join_tel3 = st01.nextToken();// 세번째 전번 저장

		String join_phone = editm.getJoin_phone();
		StringTokenizer st02 = new StringTokenizer(join_phone, "-");
		// java.util 패키지의 StringTokenizer 클래스는 첫번째 전달인자를
		// 두번째 -를 기준으로 문자열을 파싱해준다.
		String join_phone1 = st02.nextToken();// 첫번째 전화번호 저장
		String join_phone2 = st02.nextToken(); // 두번째 전번 저장
		String join_phone3 = st02.nextToken();// 세번째 전번 저장

		String join_email = editm.getJoin_email();
		StringTokenizer st03 = new StringTokenizer(join_email, "@");
		// java.util 패키지의 StringTokenizer 클래스는 첫번째 전달인자를
		// 두번째 @를 기준으로 문자열을 파싱해준다.
		String join_mailid = st03.nextToken();// 첫번째 전화번호 저장
		String join_maildomain = st03.nextToken(); // 두번째 전번 저장

		m.addAttribute("editm", editm);
		m.addAttribute("join_tel1", join_tel1);
		m.addAttribute("join_tel2", join_tel2);
		m.addAttribute("join_tel3", join_tel3);
		m.addAttribute("join_phone1", join_phone1);
		m.addAttribute("join_phone2", join_phone2);
		m.addAttribute("join_phone3", join_phone3);
		m.addAttribute("join_mailid", join_mailid);
		m.addAttribute("join_maildomain", join_maildomain);

		return "member/member_edit";
	}

- 수정폼에 상세정보를 뿌려줘야 한다

- 세션에 공유된 id 를 session.getAttribuate("id") 로 받아서 그 id 를 매개변수로 userCheck() 호출하여 상세 정보를 구해옴

- 구해온 상세정보를 DTO 객체 editm 으로 받는다, 그 editm 을 Model 객체에 저장해서 member_edit.jsp 로 전달

- 그 DB에 저장된 정보인 editm 에서 결합된 전화번호, 휴대폰번호, 이메일을 잘라서 따로 저장하고 Model 객체에 저장해서 member_edit.jsp 로 전달

 

- userCheck() 메소드는 이전에도 설명 했으므로 생략, 바로 View 를 보자

 

- member_edit.jsp

<%@ page language="java" 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">
<title>회원정보 수정</title>
<link rel="stylesheet" type="text/css" href="./css/admin.css" />
<link rel="stylesheet" type="text/css" href="./css/member.css" />
<script src="./js/jquery.js"></script>
<script src="./js/member.js"></script>
<script src="http://dmaps.daum.net/map_js_init/postcode.v2.js"></script>
<script>
//우편번호, 주소 Daum API
function openDaumPostcode() {
	new daum.Postcode({
		oncomplete : function(data) {				
			// 팝업에서 검색결과 항목을 클릭했을때 실행할 코드를 작성하는 부분.
			// 우편번호와 주소 정보를 해당 필드에 넣고, 커서를 상세주소 필드로 이동한다.
			document.getElementById('join_zip1').value = data.zonecode;
			document.getElementById('join_addr1').value = data.address;				
		}
	}).open();
}
</script>
</head>
<body>
  <div id="join_wrap">
  <h2 class="join_title">회원수정</h2>
  <form name="f" method="post" action="member_edit_ok.do"
  		onsubmit="return edit_check()" enctype="multipart/form-data">
   <!-- 이진파일을 업로드 할려면 enctype 속성을 지정 -->
   <table id="join_t">
    <tr>
     <th>회원아이디</th>
     <td>
      ${id}
     </td>
    </tr>
    
    <tr>
     <th>회원비번</th>
     <td>
      <input type="password" name="join_pwd" id="join_pwd1" size="14"
      class="input_box" />
     </td>
    </tr>
    
    <tr>
     <th>회원비번확인</th>
     <td>
      <input type="password" name="join_pwd2" id="join_pwd2" size="14"
      class="input_box" />
     </td>
    </tr>
    
    <tr>
     <th>회원이름</th>
     <td>
      <input name="join_name" id="join_name" size="14" class="input_box"
      value="${editm.join_name}" />
     </td>
    </tr>
    
    <tr>
     <th>우편번호</th>
     <td>
      <input name="join_zip1" id="join_zip1" size="5" class="input_box"
      		readonly onclick="post_search()" value="${editm.join_zip1}"/>
      <%-- -<input name="join_zip2"  id="join_zip2" size="3" class="input_box" readonly 
      		onclick="post_search()" value="${editm.join_zip2}"/> --%>
      <input type="button" value="우편번호검색" class="input_button"
      		onclick="openDaumPostcode()" />
     </td>
    </tr>
    
    <tr>
     <th>주소</th>
     <td>
      <input name="join_addr1" id="join_addr1" size="50" class="input_box"
      readonly value="${editm.join_addr1}" onclick="post_search()" />
     </td>
    </tr>
    
    <tr>
     <th>나머지 주소</th>
     <td>
      <input name="join_addr2" id="join_addr2" size="37" 
      value="${editm.join_addr2}" class="input_box" />
     </td>
    </tr>
    
    <tr>
     <th>집전화번호</th>
     <td>
     <%@ include file="../../jsp/include/tel_number.jsp"%>
      <select name="join_tel1" >      
      <c:forEach var="t" items="${tel}" begin="0" end="16">
      	<option value="${t}" <c:if test="${join_tel1 == t}">${'selected'}
          </c:if>>${t}
        </option>
      </c:forEach>        
      </select>-<input name="join_tel2" id="join_tel2" size="4"
      maxlength="4" class="input_box" value="${join_tel2}"/>-<input  name="join_tel3"
      id="join_tel3" size="4" maxlength="4" class="input_box" 
      value="${join_tel3}"/>
     </td>
    </tr>
    
    <tr>
     <th>휴대전화번호</th>
     <td>
     <%@ include file="../../jsp/include/phone_number.jsp" %>
     <select name="join_phone1">
      <c:forEach var="p" items="${phone}" begin="0" end="5">
       <option value="${p}" <c:if test="${join_phone1 == p}">${'selected'}
          </c:if>>${p}
        </option>
      </c:forEach>
     </select>-<input name="join_phone2" id="join_phone2" size="4"
     maxlength="4" class="input_box" value="${join_phone2}"/>-<input name="join_phone3"
     id="join_phone3" size="4" maxlength="4" class="input_box" 
     value="${join_phone3}"/>
     </td>
    </tr>
    
    <tr>
     <th>전자우편</th>
     <td>
      <input name="join_mailid" id="join_mailid" size="10" 
      class="input_box" value="${join_mailid}"/>@<input name="join_maildomain" 
      id="join_maildomain" size="20" class="input_box" readonly
      value="${join_maildomain}" />
      
      <!--readonly는 단지 쓰기,수정이 불가능하고 읽기만 가능하다 //-->
      <select name="mail_list" onchange="domain_list()">
      <option value="">=이메일선택=</option>
      <option value="daum.net" 
      		<c:if test="${join_maildomain == 'daum.net'}">${'selected'}
            </c:if>>daum.net</option>
      <option value="nate.com" 
      		<c:if test="${join_maildomain == 'nate.com'}">${'selected'}
            </c:if>>nate.com</option>
      <option value="naver.com" 
      		<c:if test="${join_maildomain == 'naver.com'}">${'selected'}
            </c:if>>naver.com</option>
      <option value="hotmail.com" 
            <c:if test="${join_maildomain == 'hotmail.com'}">${'selected'}
            </c:if>>hotmail.com</option>
      <option value="gmail.com" 
            <c:if test="${join_maildomain == 'gmail.com'}">${'selected'}
            </c:if>>gmail.com</option>
      <option value="0">직접입력</option>
     </select> 
     </td>
    </tr>
    
    <tr>
     <th>프로필사진</th>
     <td>
      <input type="file" name="join_profile1" />
     </td>
    </tr>
   </table>
   
   <div id="join_menu">
    <input type="submit" value="회원수정" class="input_button" />
    <input type="reset" value="수정취소" class="input_button" 
    onclick="$('#join_pwd1').focus();" />
   </div>
  </form>
 </div>
</body>
</html>

- value 속성에 상세정보를 뿌려준다

+ 주솟값이 저장되는 DB 테이블의 컬럼이 두개이다, 주소와 상세정보

- 현재 수정폼에서는 DB와 연동해서 비밀번호가 맞는지 확인하고 있지 않다

- 수정폼의 비밀번호와 비밀번호확인이 같다면 문제없이 "member_edit_ok.do" 로 요청

- 요청하면서 id 값을 가져가지 않는다, 여기선 세션으로 해결하지만 hidden 으로 전달하는 편이 나음

 

회원 아이디 출력 (member_edit.jsp)

     <th>회원아이디</th>
     <td>
      ${id}
     </td>

- 넘어온 객체 editm에도 id 값이 들어있고 Session 값에도 id 가 들어있다

- 현재는 Session 에 공유설정된 id 를 가져오고 있다, 앞에 SessionScope 가 생략된것

- ${editm.id} 로 출력해도 된다

 

집전화번호 앞자리에 값뿌리기

    <tr>
     <th>집전화번호</th>
     <td>
     <%@ include file="../../jsp/include/tel_number.jsp"%>
      <select name="join_tel1" >      
      <c:forEach var="t" items="${tel}" begin="0" end="16">
      	<option value="${t}" <c:if test="${join_tel1 == t}">${'selected'}
          </c:if>>${t}
        </option>
      </c:forEach>        
      </select>-<input name="join_tel2" id="join_tel2" size="4"
      maxlength="4" class="input_box" value="${join_tel2}"/>-<input  name="join_tel3"
      id="join_tel3" size="4" maxlength="4" class="input_box" 
      value="${join_tel3}"/>
     </td>
    </tr>

+ tel_number.jsp 에서 배열 tel 에 지역번호들을 저장하고, REQUEST 공유설정했다

- 여기 member_edit 에서 ${tel} 을 forEach 의 items 에 넣고 Model 에서 넘어온 전화번호 앞자리와 일치하는지 확인해서 일치시 selected 를 추가함

- 휴대폰 번호 앞자리도 마찬가지로 처리

- 이외에도 jQuery 로 간략하게 처리할 수도 있다

 

이메일 도메인 select-option 처리

    <tr>
     <th>전자우편</th>
     <td>
      <input name="join_mailid" id="join_mailid" size="10" 
      class="input_box" value="${join_mailid}"/>@<input name="join_maildomain" 
      id="join_maildomain" size="20" class="input_box" readonly
      value="${join_maildomain}" />
      
      <!--readonly는 단지 쓰기,수정이 불가능하고 읽기만 가능하다 //-->
      <select name="mail_list" onchange="domain_list()">
      <option value="">=이메일선택=</option>
      <option value="daum.net" 
      		<c:if test="${join_maildomain == 'daum.net'}">${'selected'}
            </c:if>>daum.net</option>
      <option value="nate.com" 
      		<c:if test="${join_maildomain == 'nate.com'}">${'selected'}
            </c:if>>nate.com</option>
      <option value="naver.com" 
      		<c:if test="${join_maildomain == 'naver.com'}">${'selected'}
            </c:if>>naver.com</option>
      <option value="hotmail.com" 
            <c:if test="${join_maildomain == 'hotmail.com'}">${'selected'}
            </c:if>>hotmail.com</option>
      <option value="gmail.com" 
            <c:if test="${join_maildomain == 'gmail.com'}">${'selected'}
            </c:if>>gmail.com</option>
      <option value="0">직접입력</option>
     </select> 
     </td>
    </tr>

- if 태그를 option 태그에 일일히 사용함

 

+ readonly vs disabled

- readonly 는 form의 action 으로 값이 넘어가고 disabled 는 값이 넘어가지 않음!

 


정보 수정

- 수정폼 member_edit.jsp 에서 값을 입력 후 "회원수정" 클릭시 "member_edit_ok.do" 로 요청함

- id 값은 넘어오지 않았다

 

- Controller 클래스 MemberAction.java 에서 "member_edit_ok.do" 요청 부분만

	/* 회원정보 수정(fileupload) */
	@RequestMapping(value = "/member_edit_ok.do", method = RequestMethod.POST)
	public String member_edit_ok(@RequestParam("join_profile1") MultipartFile mf, 
								 MemberBean member,
								 HttpServletRequest request, 
								 HttpSession session, 
								 Model model) throws Exception {

		String filename = mf.getOriginalFilename();
		int size = (int) mf.getSize();		
		
		String path = request.getRealPath("upload");
		System.out.println("path:"+path);
		
		int result=0;		
		String file[] = new String[2];
//		file = filename.split(".");
//		System.out.println(file.length);
//		System.out.println("file0="+file[0]);
//		System.out.println("file1="+file[1]);
		
		String newfilename = "";
		
	if(filename != ""){	 // 첨부파일이 전송된 경우		
		
		// 파일 중복문제 해결
		String extension = filename.substring(filename.lastIndexOf("."), filename.length());
		System.out.println("extension:"+extension);
				
		UUID uuid = UUID.randomUUID();
				
		newfilename = uuid.toString() + extension;
		System.out.println("newfilename:"+newfilename);			
		
		StringTokenizer st = new StringTokenizer(filename, ".");
		file[0] = st.nextToken();		// 파일명
		file[1] = st.nextToken();		// 확장자	
		
		if(size > 100000){
			result=1;
			model.addAttribute("result", result);
			
			return "member/uploadResult";
			
		}else if(!file[1].equals("jpg") &&
				 !file[1].equals("gif") &&
				 !file[1].equals("png") ){
			
			result=2;
			model.addAttribute("result", result);
			
			return "member/uploadResult";
		}	
		
	}
		
		if (size > 0) { // 첨부파일이 전송된 경우

			mf.transferTo(new File(path + "/" + newfilename));

		}		
		

		String id = (String) session.getAttribute("id");

		String join_tel1 = request.getParameter("join_tel1").trim();
		String join_tel2 = request.getParameter("join_tel2").trim();
		String join_tel3 = request.getParameter("join_tel3").trim();
		String join_tel = join_tel1 + "-" + join_tel2 + "-" + join_tel3;
		String join_phone1 = request.getParameter("join_phone1").trim();
		String join_phone2 = request.getParameter("join_phone2").trim();
		String join_phone3 = request.getParameter("join_phone3").trim();
		String join_phone = join_phone1 + "-" + join_phone2 + "-" + join_phone3;
		String join_mailid = request.getParameter("join_mailid").trim();
		String join_maildomain = request.getParameter("join_maildomain").trim();
		String join_email = join_mailid + "@" + join_maildomain;

		MemberBean editm = this.memberService.userCheck(id);		
		
		if (size > 0 ) { 			// 첨부 파일이 수정되면
			member.setJoin_profile(newfilename);			
		} else { 					// 첨부파일이 수정되지 않으면
			member.setJoin_profile(editm.getJoin_profile());
		}

		member.setJoin_id(id);
		member.setJoin_tel(join_tel);
		member.setJoin_phone(join_phone);
		member.setJoin_email(join_email);

		memberService.updateMember(member);// 수정 메서드 호출

		model.addAttribute("join_name", member.getJoin_name());
		model.addAttribute("join_profile", member.getJoin_profile());

		return "member/main";
	}

1. 수정폼에선 넘어온 첨부파일은 @RequestParam("join_profile1") 으로 받아서 MultipartFile 객체 mf 에 저장

2. 나머지 수정폼에서 넘어온 값들은 DTO 객체 member 에 저장해서 받음

3. mf.getOriginalFilename() 으로 파일명 filename 을 구한다

4. mf.getSize() 메소드는 return 타입이 LONG 형이지만 int 형으로 강제형변환시켜서 변수 size 에 저장

5. 확장자 처리를 위해 String 배열 file 을 만들어둠

6. 확장자를 분리시켜 변수 extension 에 저장, UUID.randomUUID() 로 난수 발생시켜서 extension 과 붙여서 새로운 파일명 newfilename 을 구함

6. 파일 사이즈 제한에 대한 설정은 root-context.xml 에서 했지만, 그 코드는 더 큰 파일 첨부시 실행을 멈추고 오류를 발생시킴, 그러므로 여기서 if 문을 통해 더 큰 파일 첨부시 return uploadResult 로 보내면서 업로드를 불가능하게 만듬

7. filename 에서 확장자를 빼서 file[1] 에 저장후 file[1] 이 특정 확장자가 아닌 경우 return uploadResult 로 보내면서 업로드를 불가능하게 만듬

8. 첨부파일 업로드를 mf.transferTo() 메소드로 먼저 한다, 그 후 첨부파일을 제외한 다른 수정폼에 입력한 값들을 따로 update 시킨다

9. 수정폼의 전화번호, 휴대폰 번호, 이메일은 분리되어 값이 넘어온 상태이므로 @ModelAttribute 에 의해 DTO 객체 member 에 저장되지 않은 상태이다

10. 수정폼의 분리되어 넘어온 전화번호, 휴대폰 번호, 이메일을 결합해서 Setter 메소드로 객체 member 에 세팅해준다

11. 그리고 그 객체 member 를 매개변수로 updateMember() 를 호출해서 실제 수정을 한다 * 아래에서 설명

+ 현재는 비번 비교를 하지 않음

12. 수정 후 돌아와서 마이페이지 main.jsp 로 이동할 것이므로 회원 이름과 프로필 파일명을 Model 객체에 저장해서 전달해야만한다

- main.jsp 에서는 이 회원 이름과 프로필 파일을 출력하고 있으므로 main.jsp 로 가려면 반드시 여기서 회원 이름과 프로필 파일명을 전달해야함

 

첨부파일을 수정할때와 수정하지 않았을때 ( MemberAction.java "member_edit_ok.do" 요청 부분 일부)

		if (size > 0 ) { 			// 첨부 파일이 수정되면
			member.setJoin_profile(newfilename);			
		} else { 					// 첨부파일이 수정되지 않으면
			member.setJoin_profile(editm.getJoin_profile());
		}

- 사용자가 첨부파일을 수정할때와 수정하지 않을떄의 경우를 나눠서 처리해야한다

- 수정시에는 수정된 파일명이 컬럼에 저장되어야하고 수정되지 않았을땐 기존 파일명이 컬럼에 저장되어야한다

- size 는 사용자가 첨부한 파일을 받은 객체 mf 에서 mf.getSize() 로 구해온 값을 저장하고 있는 변수이다, 즉 사용자가 첨부를 했으면 size 는 0 보다 크고, 첨부를 하지 않았으면 0 이다

- 첨부파일을 수정하면 새 파일의 난수화된 파일명을 객체 member 에 Setter 메소드로 세팅

- 첨부파일을 수정하지 않으면 기존에 DB에 저장된 파일명을 그대로 다시 객체 member 에 Setter 메소드로 세팅

 

- Service 클래스 MemberServiceImpl.java 에서 updateMember() 메소드 부분만

	public void updateMember(MemberBean member) throws Exception{
		memberDao.updateMember(member);
	}

- DAO클래스 MemberDaoImpl.java 에서 updateMember() 메소드 부분만

	/* 회원수정 */
//	@Transactional
	public void updateMember(MemberBean member) throws Exception {
		sqlSession.update("member_edit", member);
	}

- Mapper 파일 member.xml 에서 id 가 "member_edit" 인 SQL문 부분만

    <!-- 회원수정 -->
    <update id="member_edit" parameterType="member">
     update join_member set join_pwd=#{join_pwd},join_name=#{join_name},
     join_zip1=#{join_zip1},join_addr1=#{join_addr1},
     join_addr2=#{join_addr2},join_tel=#{join_tel},join_phone=#{join_phone},
     join_email=#{join_email},join_profile=#{join_profile,jdbcType=VARCHAR} 
     where join_id=#{join_id}
    </update>

- update 문을 사용해서 해당 회원의 정보룰 수정

- 이때도 join_profile 컬럼에 null 이 저장될 수 있다, 기존에도 파일이 없었는데 수정시에도 파일을 업로드 하지 않으면 join_profile 에는 null 이 들어간다

- 그러므로 null 을 허용하기 위해 jdbcType=VARCHAR 를 추가해야한다

 

- 정보 수정 후 main.jsp 로 돌아옴

<%@ page language="java" 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">
<title>사용자 메인화면</title>
<link rel="stylesheet" type="text/css" href="./css/main.css" />
<link rel="stylesheet" type="text/css" href="./css/admin.css" />
</head>
<body>

<c:if test="${sessionScope.id == null }"> 
  <script>
   alert("다시 로그인 해주세요!");
   location.href="<%=request.getContextPath()%>/member_login.do";
  </script>
</c:if>

<c:if test="${sessionScope.id != null }">  
 <div id="main_wrap">
   <h2 class="main_title">사용자 메인화면</h2>  
   <form method="post" action="member_logout.do"> 
   <table id="main_t">
    <tr>
     <th colspan="2">
     <input type="button" value="정보수정" class="input_button"
     		onclick="location='member_edit.do'" />
     <input type="button" value="회원탈퇴" class="input_button"
     		onclick="location='member_del.do'" />
     <input type="submit" value="로그아웃" class="input_button" />     
     </th>
    </tr>
    
    <tr>
     <th>회원이름</th>
     <td>${join_name}님 로그인을 환영합니다</td>
    </tr>
    
    <tr>
     <th>프로필사진</th>
     <td>
       <c:if test="${empty join_profile}">
       &nbsp;
       </c:if>
       <c:if test="${!empty join_profile}">
       <img src="<%=request.getContextPath() %>/upload/${join_profile}" height="100" width="100" />
       </c:if>
     </td>
    </tr>
   </table>   
   </form>
 </div>
</c:if>

</body>
</html>

-  ${join_profile} 을 사용해 경로를 잡아서 img 태그로 이미지를 불러옴

 


회원 탈퇴폼 기능

버튼 처리 : "회원탈퇴" 버튼 클릭시

     <input type="button" value="회원탈퇴" class="input_button"
     		onclick="location='member_del.do'" />

- main.jsp 에서 "회원탈퇴" 클릭시 "member_del.do" 로 요청

- 해당 요청은 인터셉터에 등록되었으므로 인터셉터를 거쳐 세션이 있을때만 Controler 의 member_del.do 로 감

- 세션이 없으면 로그인 폼으로 이동

- "member_del.do" 로 가면서 아무 값을 가져가지 않음, 세션값을 활용한다

- 사용자가 회원 탈퇴를 해도 고객 정보를 실제로 DB에서 삭제하지 않고, 상태값을 변경시켜서 탈퇴 회원 처리를 함

- 비밀번호가 맞아야 탈퇴 가능

- 테이블 join_member 에 탈퇴 사유와 탈퇴 날짜를 저장할 수 있는 컬럼 또한 있다

 

join_member 테이블에서 탈퇴 관련 컬럼 3가지

1. 회원 상태 (가입 또는 탈퇴)

2. 탈퇴 사유

3. 탈퇴 날짜

 

- 먼저 삭제폼으로 이동

- Controller 클래스 MemberAction.java "member_del.do" 요청 부분만

	/* 회원정보 삭제 폼 */
	@RequestMapping(value = "/member_del.do")
	public String member_del(HttpSession session, Model dm) throws Exception {

		String id = (String) session.getAttribute("id");
		MemberBean deleteM = memberService.userCheck(id);
		dm.addAttribute("d_id", id);
		dm.addAttribute("d_name", deleteM.getJoin_name());

		return "member/member_del";
	}

- 비밀번호 비교를 하기 위해, 세션에서 id 값을 가져와서 그 id 를 매개변수로 userCheck() 를 호출하여 해당 id 회원의 상세정보를 구해옴

- 탈퇴폼에서 회원 아이디, 이름을 뿌릴 것이므로 회원 아이디와 이름을 Model 객체에 저장하고 탈퇴폼 member_del.jsp 로 이동

 

- userCheck() 메소드는 이전에도 설명했으므로 설명 생략

 

- member_del.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
    
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>회원탈퇴</title>
<link rel="stylesheet" type="text/css" href="./css/admin.css" />
<link rel="stylesheet" type="text/css" href="./css/member.css" />
<script src="./js/jquery.js"></script>
<script>
 function check(){
	 if($.trim($("#pwd").val())==""){
		 alert("비밀번호를 입력하세요!");
		 $("#pwd").val("").focus();
		 return false;
	 }
	 if($.trim($("#del_cont").val())==""){
		 alert("탈퇴사유를 입력하세요!");
		 $("#del_cont").val("").focus();
		 return false;
	 }
 }
</script>
</head>
<body>
 <div id="del_wrap">
  <h2 class="del_title">회원탈퇴</h2>
  <form method="post" action="member_del_ok.do" onsubmit="return check()">
    <table id="del_t">
     <tr>
      <th>회원아이디</th>
      <td>
      ${d_id}
      </td>
     </tr>
     
     <tr>
      <th>회원이름</th>
      <td>${d_name}</td>
     </tr>
     
     <tr>
      <th>비밀번호</th>
      <td>
      <input type="password" name="pwd" id="pwd" size="14" 
      			class="input_box" />
      </td>
     </tr>
     
     <tr>
      <th>탈퇴사유</th>
      <td>
      <textarea name="del_cont" id="del_cont" rows="7" 
      			cols="30" class="input_box"></textarea>
      </td>
     </tr>
    </table>
    
    <div id="del_menu">
     <input type="submit" value="탈퇴" class="input_button" />
     <input type="reset" value="취소" class="input_button"
     	onclick="$('#pwd').focus();" />
    </div>
  </form>
 </div>
</body>
</html>

- 회원 아이디, 회원 이름을 출력함

- 비밀번호와 탈퇴 사유를 입력하고 "탈퇴" 버튼 클릭시 "member_del_ok.do" 로 요청한다

- 이때 아이디와 회원 이름은 넘어가지 않음


회원 탈퇴

- 회원 탈퇴폼 member_del.jsp 에서 비밀번호와 탈퇴 사유 입력 후 "탈퇴" 클릭시 "member_del_ok.do" 로 요청함

 

- Controller 클래스 MemberAction.java 에서 "mebmer_del_ok.do" 요청 부분만

	/* 회원정보 삭제 완료 */
	@RequestMapping(value = "/member_del_ok.do", method = RequestMethod.POST)
	public String member_del_ok(@RequestParam("pwd") String pass, 
							    @RequestParam("del_cont") String del_cont,
							    HttpSession session) throws Exception {

		String id = (String) session.getAttribute("id");
		MemberBean member = this.memberService.userCheck(id);

		if (!member.getJoin_pwd().equals(pass)) {

			return "member/deleteResult";
			
		} else {				// 비번이 같은 경우
			
			String up = session.getServletContext().getRealPath("upload");
			String fname = member.getJoin_profile();
			System.out.println("up:"+up);
			
			// 디비에 저장된 기존 이진파일명을 가져옴
			if (fname != null) {// 기존 이진파일이 존재하면
				File delFile = new File(up +"/"+fname);
				delFile.delete();// 기존 이진파일을 삭제
			}
			MemberBean delm = new MemberBean();
			delm.setJoin_id(id);
			delm.setJoin_delcont(del_cont);

			memberService.deleteMember(delm);// 삭제 메서드 호출

			session.invalidate();	// 세션만료

			return "redirect:member_login.do";
		}
	}

- 넘어온 비밀번호와 탈퇴사유를 @RequestParam 으로 각각 변수 pass, del_cont 에 받는다

- 회원 아이디는 세션에서 구해온다, 그 id 를 매개변수로, 비밀번호가 맞는지 확인하기 위해 userCheck() 메소드 호출

- DB의 비번이 사용자가 탈퇴폼에서 입력한 비밀번호와 일치하지 않으면 deleteResult.jsp 로 이동, 거기서 탈퇴 실패 처리

- 비번이 일치하면, 첨부파일이 있는 경우 첨부파일을 삭제해야한다

 

첨부파일 삭제 (Controller 클래스 MemberAction.java 에서 "mebmer_del_ok.do" 요청 부분 일부)

			String up = session.getServletContext().getRealPath("upload");
			String fname = member.getJoin_profile();
			System.out.println("up:"+up);
			
			// 디비에 저장된 기존 이진파일명을 가져옴
			if (fname != null) {// 기존 이진파일이 존재하면
				File delFile = new File(up +"/"+fname);
				delFile.delete();// 기존 이진파일을 삭제
			}

- 업로드 폴더 경로를 구해온다, 현재는 session 객체로 절대 경로를 구해서 변수 up 에 저장

+ request 객체로 getReaulPath() 해서 경로를 구할 수도 있다

- DB에 저장된 프로필 파일명을 fname 파일명에 저장

- up 과 filename 을 사용해서 File 객체를 생성 하고 그 파일 객체 delFile 을 통해 delte() 로 해당 파일 삭제

 

탈퇴 SQL문에 전달하기 위해 DTO객체 생성 후 두가지 값 세팅  (Controller 클래스 MemberAction.java 에서 "mebmer_del_ok.do" 요청 부분 일부)

			MemberBean delm = new MemberBean();
			delm.setJoin_id(id);
			delm.setJoin_delcont(del_cont);

			memberService.deleteMember(delm);// 삭제 메서드 호출

- DTO 객체 delm을 생성해서 id 값과 탈퇴 사유를 delm 저장함

- 탈퇴 처리를 위해 id 도 필요하고, 탈퇴 사유도 필요하다. id 는 where 조건절에 들어갈 것

- Mapper 파일에 값을 전달할때 1개의 값만 전달가능하므로 DTO 객체 안에 두가지 값을 저장해서 DTO 객체를 넘길 것

 

탈퇴 메소드 호출 (Controller 클래스 MemberAction.java 에서 "mebmer_del_ok.do" 요청 부분 일부)

			memberService.deleteMember(delm);// 삭제 메서드 호출

- Service 클래스의 메소드 deleteMember() 호출 * 아래에서 설명

 

세션 끊고  (Controller 클래스 MemberAction.java 에서 "mebmer_del_ok.do" 요청 부분 일부)

			session.invalidate();	// 세션만료

			return "redirect:member_login.do";

- DB에서 탈퇴 처리한 후 세션을 삭제해줌

- 이후 "member_login.do" 로 요청하면서 로그인 폼으로 이동


- Service 클래스 MemberServiceImpl.java 에서 deleteMember() 메소드 부분만

	public void deleteMember(MemberBean member) throws Exception{
		memberDao.deleteMember(member);
	}

- DAO클래스 MemberDaoImpl.java 에서 deleteMember() 메소드 부분만

	/* 회원삭제 */
//	@Transactional
	public void deleteMember(MemberBean delm) throws Exception {
		sqlSession.update("member_delete", delm);
	}

- 값을 하나만 전달가능하지만 탈퇴 SQL문에서 id 도 필요하고 탈퇴사유도 필요하므로 두개를 저장한 DTO 객체 delm 을 넘김

 

- Mapper 파일 member.xml 에서 id 가 "member_delete" 인 SQL문 부분만

    <!-- 회원삭제 -->
    <update id="member_delete" parameterType="member">
      update join_member set join_delcont=#{join_delcont}, join_state=2,
      join_deldate=sysdate where join_id=#{join_id}
    </update>

- where 조건절에 DTO 로 넘어온 id 값이 들어가서 해당 회원의 데이터를 변경킴

- delete 가 아닌 update 문으로 join_state 를 2로 변경시켜서 탈퇴를 처리함

- 이때 DTO 로 넘어온 탈퇴 사유를 세팅하고, 탈퇴 날짜를 sysdate 로 세팅


비번 찾기 기능 실행

- 실행해서 비번 찾기를 해보자

 

- 회원가입시 등록했던 이메일 주소의 받은 메일함에서 확인

- 아래에서 비번 찾기 기능 코드 설명할 것

 

+ 임시비번 기능 활용

- 이메일로 임시비번을 저장하고 DB에도 그 임시비번으로 비밀번호를 업데이트 시켜둬야한다

- 그래야 그 임시비번으로 로그인 가능

 


비번 찾기 기능

- 로그인 폼 member_login.jsp 에서 버튼 "비번찾기" 클릭시 pwd_find() 메소드 호출함

- 비번 찾기도 비번 찾기 폼과 비번 찾기가 있음, 여기선 한번에 설명

- pwd_find() 메소드는 팝업창을 띄운다

 

- member_login.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
    
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>로그인</title>
<link rel="stylesheet" type="text/css" href="<%=request.getContextPath()%>/css/admin.css" />
<link rel="stylesheet" type="text/css" href="<%=request.getContextPath()%>/css/member.css" />
<!-- <script src="./js/jquery.js"></script> -->
<script src="http://code.jquery.com/jquery-latest.js"></script>

<script>
 function check(){
	 if($.trim($("#id").val())==""){
		 alert("로그인 아이디를 입력하세요!");
		 $("#id").val("").focus();
		 return false;
	 }
	 if($.trim($("#pwd").val())==""){
		 alert("비밀번호를 입력하세요!");
		 $("#pwd").val("").focus();
		 return false;
	 }
 }
 
 /*비번찾기 공지창*/
 function pwd_find(){
	 window.open("pwd_find.do","비번찾기","width=450,height=500");
	 //자바 스크립트에서 window객체의 open("공지창경로와 파일명","공지창이름","공지창속성")
	 //메서드로 새로운 공지창을 만듬.폭이 400,높이가 400인 새로운 공지창을 만듬.단위는 픽셀
 }
</script>
</head>
<body>
 <div id="login_wrap">
  <h2 class="login_title">로그인</h2>
  <form method="post" action="member_login_ok.do" onsubmit="return check()">
   <table id="login_t">
    <tr>
     <th>아이디</th>
     <td>
      <input name="id" id="id" size="20" class="input_box" />
     </td>
    </tr>
    
    <tr>
     <th>비밀번호</th>
     <td>
     <input type="password" name="pwd" id="pwd" size="20" class="input_box"/>
     </td>
    </tr>
   </table>
    <div id="login_menu">
    <input type="submit" value="로그인" class="input_button" />
    <input type="reset" value="취소" class="input_button"
    		onclick="$('#id').focus();" />
    <input type="button" value="회원가입" class="input_button"
    		onclick="location='member_join.do'" />
    <input type="button" value="비번찾기" class="input_button"
    		onclick="pwd_find()" />
    </div>
  </form>
 </div>
</body>
</html>

 

팝업창 띄우기 (member_login.jsp 부분)

 /*비번찾기 공지창*/
 function pwd_find(){
	 window.open("pwd_find.do","비번찾기","width=450,height=500");
	 //자바 스크립트에서 window객체의 open("공지창경로와 파일명","공지창이름","공지창속성")
	 //메서드로 새로운 공지창을 만듬.폭이 400,높이가 400인 새로운 공지창을 만듬.단위는 픽셀
 }

- 자바 스크립트에서 window객체의 open("공지창경로와 파일명","공지창이름","공지창속성") 을 써서 팝업창 띄움

- Model 1 에서는 jsp 파일을 첫번째 매개변수에 썼지만 지금은 Model 2 기반이므로 Controller 를 거쳐야한다, 그러므로 요청명을 쓴다

 

- 즉 사용자가 "비번찾기" 클릭시 pwd_find() 메소드가 호출되어 "pwd_find.do" 로 요청한다

- Controller 클래스 MemberAction.java 에서 "pwd_find.do" 요청 부분만

	/* 비번찾기 폼 */
	@RequestMapping(value = "/pwd_find.do")
	public String pwd_find() {
		return "member/pwd_find";
		// member 폴더의 pwd_find.jsp 뷰 페이지 실행
	}

- Controller 클래스를 거쳐 pwd_find.jsp 로 간다, 그 pwd_find.jsp 안의 내용이 바로 팝업창에 출력될 내용임

 

- pwd_find.jsp

<%@ page language="java" 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">
<title>비번찾기</title>
<link rel="stylesheet" type="text/css" href="./css/admin.css" />
<link rel="stylesheet" type="text/css" href="./css/member.css" />
<script src="./js/jquery.js"></script>
<script>
 function check(){
	 if($.trim($("#id").val())==""){
		 alert("아이디를 입력하세요!");
		 $("#id").val("").focus();
		 return false;
	 }
	 if($.trim($("#name").val())==""){
		 alert("회원이름을 입력하세요!");
		 $("#name").val("").focus();
		 return false;
	 }
 }
</script>
</head>
<body>
 <div id="pwd_wrap">
 
 <c:if test="${empty pwdok}"> 
  <h2 class="pwd_title">비번찾기</h2>
  <form method="post" action="pwd_find_ok.do" onsubmit="return check()">  
   <table id="pwd_t">
    <tr>
     <th>아이디</th>
     <td><input name="join_id" id="id" size="14" class="input_box" /></td>
    </tr>
    
    <tr>
     <th>회원이름</th>
     <td><input name="join_name" id="name" size="14" class="input_box" /></td>
    </tr>
   </table>
   <div id="pwd_menu">
    <input type="submit" value="찾기" class="input_button" />
    <input type="reset" value="취소" class="input_button" 
    onclick="$('#id').focus();"/>
   </div>
   <div id="pwd_close">
    <input type="button" value="닫기" class="input_button"
    onclick="self.close();" />
    <!-- close()메서드로 공지창을 닫는다. self.close()는 자바스크립트이다. -->
   </div>
  </form>
  </c:if>
  
  
  <c:if test="${!empty pwdok}">
    <h2 class="pwd_title2">비번찾기 결과</h2>
    <table id="pwd_t2">
     <tr>
      <th>검색한 비번:</th>
      <td>${pwdok}</td>
     </tr>
    </table>
    <div id="pwd_close2">
    <input type="button" value="닫기" class="input_button"
    onclick="self.close();" />
    <!-- close()메서드로 공지창을 닫는다. self.close()는 자바스크립트이다. -->
    </div>
  </c:if> 
  
 </div>
</body>
</html>

- 이 파일 pwd_find.jsp 안의 내용이 팝업창에 나타난다

- 조건식을 써서 pwdok 가 empty 인 경우와 empty 가 아닌경우로 나누어서 다른 내용을 실행함

1. 처음 이 파일에 올때는 pwdok 이 empty 인 상태이다,

2. 사용자가 아이디, 비번을 쓰고 "찾기" 클릭시 "pwd_find_ok.do" 로 요청하며 폼에 작성된 아이디, 비번값을 전송한다

* 아래에서 설명

3. DB에서 아이디, 비번이 일치되면 비밀번호를 메일을 보낸 후 Controller 클래스에서 pwdok 에서 pwdok 에 메세지를 저장한 뒤 다시 return 으로 pwd_find.jsp 로 온다

4. 다시 돌아오면 pwdok 가 empty 가 아니게 되므로 아래의 if 태그를 실행, 즉 아래 화면이 출력됨

 

 

- Controller 클래스 MemberAction.java 에서 "pwd_find_ok.do" 요청 부분만

	/* 비번찾기 완료 */
	@RequestMapping(value = "/pwd_find_ok.do", method = RequestMethod.POST)
	public String pwd_find_ok(@ModelAttribute MemberBean mem, HttpServletResponse response, Model model)
			throws Exception {
		response.setContentType("text/html;charset=UTF-8");
		PrintWriter out = response.getWriter();

		MemberBean member = memberService.findpwd(mem);

		if (member == null) {// 값이 없는 경우

			return "member/pwdResult";

		} else {

			// Mail Server 설정
			String charSet = "utf-8";
			String hostSMTP = "smtp.naver.com";
			String hostSMTPid = "아이디@naver.com";
			String hostSMTPpwd = "비밀번호"; // 비밀번호 입력해야함

			// 보내는 사람 EMail, 제목, 내용
			String fromEmail = "아이디@naver.com";
			String fromName = "관리자";
			String subject = "비밀번호 찾기";

			// 받는 사람 E-Mail 주소
			String mail = member.getJoin_email();

			try {
				HtmlEmail email = new HtmlEmail();
				email.setDebug(true);
				email.setCharset(charSet);
				email.setSSL(true);
				email.setHostName(hostSMTP);
				email.setSmtpPort(587);

				email.setAuthentication(hostSMTPid, hostSMTPpwd);
				email.setTLS(true);
				email.addTo(mail, charSet);
				email.setFrom(fromEmail, fromName, charSet);
				email.setSubject(subject);
				email.setHtmlMsg("<p align = 'center'>비밀번호 찾기</p><br>" + "<div align='center'> 비밀번호 : "
						+ member.getJoin_pwd() + "</div>");
				email.send();
			} catch (Exception e) {
				System.out.println(e);
			}

			model.addAttribute("pwdok", "등록된 email을 확인 하세요~!!");
			return "member/pwd_find";

		}

	}

- 넘어온 아이디와 회원이름은 @ModelAttribute 로 DTO MemberBean 객체 mem 에 바로 받아서 저장한다

- 그 객체 mem 을 사용해서 Service 클래스의 findpwd() 메소드를 호출해서 회원 상세정보를 받아옴

+ SQL문에 값 1개만 전달 가능하므로 id 와 이름을 DTO 객체에 저장해서 통째로 넘김

+ findpwd() 를 따라가보자 * 아래에서 설명

- findpwd() 에서 돌아온 후, 돌아온 회원의 상세정보를 객체 member 에 저장

- member 값이 없다면 없는 회원이므로 pwdResult.jsp 로 이동, 값이 있다면 메일을 보내기위한 설정을 한다

- 메일을 보낸 후 Model 객체를 통해 "pwdok" 에 메세지를 저장하고 다시 pwd_find.jsp 로 이동

 

이메일 보내는 설정 및 보내는 코드 (Controller 클래스 MemberAction.java 에서 "pwd_find_ok.do" 요청 부분 일부)

			// Mail Server 설정
			String charSet = "utf-8";
			String hostSMTP = "smtp.naver.com";
			String hostSMTPid = "아이디@naver.com";
			String hostSMTPpwd = "비밀번호"; // 비밀번호 입력해야함

			// 보내는 사람 EMail, 제목, 내용
			String fromEmail = "아이디@naver.com";
			String fromName = "관리자";
			String subject = "비밀번호 찾기";

			// 받는 사람 E-Mail 주소
			String mail = member.getJoin_email();

			try {
				HtmlEmail email = new HtmlEmail();
				email.setDebug(true);
				email.setCharset(charSet);
				email.setSSL(true);
				email.setHostName(hostSMTP);
				email.setSmtpPort(587);

				email.setAuthentication(hostSMTPid, hostSMTPpwd);
				email.setTLS(true);
				email.addTo(mail, charSet);
				email.setFrom(fromEmail, fromName, charSet);
				email.setSubject(subject);
				email.setHtmlMsg("<p align = 'center'>비밀번호 찾기</p><br>" + "<div align='center'> 비밀번호 : "
						+ member.getJoin_pwd() + "</div>");
				email.send();
			} catch (Exception e) {
				System.out.println(e);
			}

- 받는 사람의 이메일 주소로는 객체 member 에서 member.getJoin_email() 로 회원의 이메일 주소를 가져와서 설정

- HtmlEmail 객체 email 를 생성, setSubject() 로 제목 설정, setHtmlMsg() 로 내용 설정하며 검색된 비밀번호를 보냄


- Service 클래스 MemberServiceImpl.java 에서 findpwd() 메소드 부분만

	public MemberBean findpwd(MemberBean m)throws Exception {
		return memberDao.findpwd(m);
	}

- DAO 클래스 MemberDaoImpl.java 에서 findpwd() 메소드 부분만

	/* 비번 검색 */
//	@Transactional
	public MemberBean findpwd(MemberBean pm) throws Exception {
		return sqlSession.selectOne("pwd_find", pm);
	}

- Mapper 파일 member.xml 에서 id 가 "pwd_find" 인 SQL문 부분만

    <!-- 비번 검색 -->
    <select id="pwd_find" resultType="member" parameterType="member">
     select *  from join_member where join_id=#{join_id} and join_name=#{join_name}
    </select>

- 사용자가 비밀번호 찾기 폼에 입력한 id 와 이름이 모두 일치하는 데이터를 검색한다



댓글 게시판 프로그램

실습 준비

- 클라우드의 springboard 프로젝트 다운, 압축 해제, import 하기

 

파일들 살펴보기 : pom.xml

- inject 라이브러리 : @inject 어노테이션을 쓰기 위한 라이브러리, @Autowired 와 같은 역할

- 메일 보내기 위한 라이브러리 등 라이브러리가 들어가있다

 

파일들 살펴보기 : web.xml

- 반드시 WEB-INF 폴더 하위에 있어야함

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">

	<filter>
		<filter-name>CharacterEncodingFilter</filter-name>
		<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
		<init-param>
			<param-name>encoding</param-name>
			<param-value>UTF-8</param-value>
		</init-param>
		<init-param>
			<param-name>forceEncoding</param-name>
			<param-value>true</param-value>
		</init-param>
	</filter>
	<filter-mapping>
		<filter-name>CharacterEncodingFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>


	<!-- The definition of the Root Spring Container shared by all Servlets 
		and Filters -->
	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>/WEB-INF/spring/root-context.xml</param-value>
	</context-param>

	<!-- Creates the Spring Container shared by all Servlets and Filters -->
	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>

	<!-- Processes application requests -->
	<servlet>
		<servlet-name>appServlet</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
		</init-param>
		<load-on-startup>1</load-on-startup>
	</servlet>

	<servlet-mapping>
		<servlet-name>appServlet</servlet-name>
		<url-pattern>*.do</url-pattern>
	</servlet-mapping>

</web-app>

- url-patteron 을 *.do 로 설정했다

- servlet-context.xml 과 root-context.xml 을 불러옴

- filter 와 filter-mapping 을 통해 한글값 인코딩 처리를 함

 

파일들 살펴보기 : servlet-context.xml

- 이름과 위치가 지정되어있지 않다

- resources 폴더 하위에 있을 수도 있음

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:beans="http://www.springframework.org/schema/beans"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">

	<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
	
	<!-- Enables the Spring MVC @Controller programming model -->
	<annotation-driven />

	<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
	<resources mapping="/resources/**" location="/resources/" />

	<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
	<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<beans:property name="prefix" value="/jsp/" />
		<beans:property name="suffix" value=".jsp" />
	</beans:bean>
	
	<context:component-scan base-package="myspring" />
	
</beans:beans>

- base-package 를 지정했다 = 어노테이션 기반으로 쓰겠다는 의미

- base-package 를 "myspring" 으로 지정, 이게 JAVA 파일이 저장될 최상위 디렉토리이다

- webapp 가 기준이므로 prefix 로 설정된 /jsp/ 에서 jsp 폴더는 webapp 폴더 하위에 있어야한다

- 폴더 jsp 는 View 파일이 저장될 최상위 디렉토리이다.

 

파일들 살펴보기 : mybatis-config.xml

- MyBatis 환경설정 파일이다

- MyBatis 환경설정 파일 mybatis-config.xml (이름이 다를 수도 있음) 과 Mapper 파일은 주로 resources 폴더에 있음

- resources 폴더 하위에 있을때는 classpath: 를 붙여서 경로를 구함

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
  PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
  "http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>

	<typeAliases>
		<typeAlias type="myspring.model.BoardBean" alias="board"></typeAlias>
	</typeAliases>

</configuration>

- alias 로 DTO BoardBean 의 별칭을 "board" 로 설정

 

파일들 살펴보기 : board.xml

- Mapper 파일이다

- MyBatis 환경설정 파일 mybatis-config.xml (이름이 다를 수도 있음) 과 Mapper 파일은 주로 resources 폴더에 있음

- resources 폴더 하위에 있을때는 classpath: 를 붙여서 경로를 구함

<?xml version="1.0" encoding="UTF-8" ?>
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="Test">

	<!-- 게시판 저장 -->
	<insert id="board_insert" parameterType="board">
		insert into board53
		(board_num,board_name,board_pass,board_subject,
		board_content,board_re_ref,board_re_lev,board_re_seq,board_readcount,board_date)
		values
		(board53_num_seq.nextval,#{board_name},#{board_pass},#{board_subject},
		#{board_content},board53_num_seq.nextval,0,0,0,SYSDATE)
	</insert>

	<!-- 게시판 총게시물 수 -->
	<select id="board_count" resultType="int">
		select count(board_num) from board53
	</select>

	<!-- 게시판 목록 (page번호를 전달받아서 startRow와 endRow를 구함) -->
	<select id="board_list" parameterType="int" resultType="board">
	    <![CDATA[
		select * from 
		 (select rownum rnum,BOARD_NUM,BOARD_NAME,BOARD_SUBJECT,BOARD_CONTENT,
		 BOARD_RE_REF,BOARD_RE_LEV,BOARD_RE_SEQ,BOARD_READCOUNT,
	 	 BOARD_DATE from  
	  	 (select * from board53 order by BOARD_RE_REF desc,BOARD_RE_SEQ asc)) 
	   		 where rnum >= ((#{page}-1) * 10+1)  and rnum <= (#{page} * 10)
		]]>
	</select>

	<!-- 게시판 내용보기 -->
	<select id="board_cont" resultType="board"
		parameterType="int">
		select * from board53 where board_num=#{board_num}
	</select>

	<!-- 게시판 조회수 증가 -->
	<update id="board_hit" parameterType="int">
		update board53 set
		board_readcount=board_readcount+1
		where board_num=#{board_num}
	</update>

	<!-- 게시물 수정 -->
	<update id="board_edit" parameterType="board">
		update board53 set
		board_name=#{board_name},
		board_subject=#{board_subject},
		board_content=#{board_content}
		where board_num=#{board_num}
	</update>

	<!-- 게시물 삭제 -->
	<delete id="board_del" parameterType="int">
		delete from board53 where
		board_num=#{board_num}
	</delete>

	<!-- 답변글 레벨 증가 -->
	<update id="board_Level" parameterType="board">
		update board53 set
		board_re_seq=board_re_seq+1
		where board_re_ref=#{board_re_ref} and
		board_re_seq > #{board_re_seq}
	</update>

	<!-- 답변글 저장 -->
	<insert id="board_reply" parameterType="board">
		insert into board53
		(board_num,board_name,board_subject,board_content,
		board_pass,board_re_ref,board_re_lev,board_re_seq,board_readcount,board_date)
		values(board53_num_seq.nextval,#{board_name},#{board_subject},#{board_content},
		#{board_pass},#{board_re_ref},#{board_re_lev},#{board_re_seq},0,SYSDATE)
	</insert>

</mapper>

 

파일들 살펴보기 : root-context.xml

- 이름과 위치가 지정되어있지 않다, resources 폴더 하위에 있을 수도 있음

- root-context.xml 에서 mybatis-config.xml 과 Mapper 파일을 불러오기 때문에 root-context.xml 은 마지막에 설정해야한다

<?xml version="1.0" encoding="UTF-8"?>

<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd">

	<!-- Data Source -->
	<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
		<property name="driverClass" value="oracle.jdbc.driver.OracleDriver" />
		<property name="url" value="jdbc:oracle:thin:@localhost:1521:xe" />
		<property name="username" value="spring" />
		<property name="password" value="spring123" />
	</bean>
	
	<!-- <bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
		<property name="driverClassName" value="oracle.jdbc.driver.OracleDriver" />
		<property name="url" value="jdbc:oracle:thin:@localhost:1521:xe" />
		<property name="username" value="spring" />
		<property name="password" value="spring123" />
	</bean> -->
	
	
	<!-- 스프링으로 oracle 디비 연결 -->
	<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="dataSource" ref="dataSource" />
		<property name="configLocation" value="classpath:util/mybatis-config.xml" />
		<property name="mapperLocations" value="classpath:sql/*.xml" />
	</bean>

	<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
		<constructor-arg index="0" ref="sqlSessionFactory" />
	</bean>

</beans>

- 이 파일을 세팅해야만 DAO 클래스에서 @Autowired 어노테이션으로 SqlSession 주입 가능

- 여기서 Constructor DI 로 생성된 SqlSession 객체 sqlSessionFactory 를 어노테이션 기반으로 DAO 클래스의 @Autowried 쪽으로 주입한다

+ SqlSession 클래스는 MyBatis 지원 인터페이스, SqlSessionTemplate 는 SqlSession 인터페이스의 구현 클래스

- root-context.xml 에 대한 자세한 설명은 이전에 했으므로 생략

- root-context.xml 코드 자세한 설명 (검색어 : 파일들 살펴보기 : root-context.xml ) : https://laker99.tistory.com/147

 

Data Source Bean 생성 중 url 부분

		<property name="url" value="jdbc:oracle:thin:@localhost:1521:xe" />

- 이후 프로젝트할땐 localhost 대신 AWS 에 오라클을 설치하고, 받은 IP 를 여기 작성한다


댓글 게시판 테이블 생성

- root-context.xml 에서 지정했던 계정 (spring 계정)을 활성화 시키기

- Connection Profile 도 설정하기

- board53.sql

--board53.sql
select * from tab;
select * from board53; 

create table board53(
    board_num number(38) primary key
  , board_name varchar2(50) not null
  , board_pass varchar2(30) not null
  , board_subject varchar2(100) not null
  , board_content varchar2(4000) not null
  , board_re_ref number 
  , board_re_lev number 
  , board_re_seq number 
  , board_readcount number 
  , board_date date 
);

create sequence board53_num_seq
                increment by 1 start with 1 nocache;

- 실행해서 테이블 board53, 시퀀스 board53_num_seq 를 생성

 

테이블 board53 컬럼 설명

- board_num : 글 번호, board53_num_seq 값을 넣음

- board_pass : 글 비밀번호, 비밀번호를 알야아 수정 / 삭제 가능

- board_re_ref : 댓글 관련 컬럼 1, 원문은 board_num 과 값 같음, 댓글은 부모의 boadr_re_ref 와 값 같음

- board_re_lev : 댓글 관련 컬럼 2, 댓글의 깊이 저장, 원문은 0, 댓글은 1, 대댓글은 2

- board_re_seq : 댓글 관련 컬럼 3, 댓글의 출력 순서 저장, 원문은 0, 댓글들은 바뀜

- 값의 변화가 빈번하게 일어나는 것은 number 타입, 아닌 것은 varchar2 타입으로 설정

- 조회수는 자주 변화하므로 board_readcount 는 number 타입으로 설정


댓글 게시판 프로그램 흐름 보기

- 프로젝트 실행시 webapp 폴더 안의 index 파일을 실행해준다

- 프로젝트 또는 index 파일 실행

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>

    
<script>
//	location.href="test.do";
	location.href="board_list.do";
</script>

- "글쓰기" 를 눌러 원문 글을 작성해보자

+ 원문과 댓글 작성 폼이 따로 있다

- 제목 클릭시 상세페이지로 이동하고 조회수 1 증가

- "답변" 을 누르면 댓글을 달 수 있다

- 댓글과 대댓글 달기

* 가장 최근에 달린 글이 위에 온다

- 위의 글 5개는 모두 같은 board_re_ref 값을 가지고 있음

- board_re_seq 의 오름차순 순으로 정렬되었다

- 원문의 board_re_seq 는 0 이고, 가장 최근에 달린 1단계 댓글의 board_re_seq 는 1

 

- SQL Developer 참고


댓글 게시판 프로그램 코드 보기

글 목록 (게시판) 기능 (간략, 자세한 설명은 나중에)

- index.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>

    
<script>
//	location.href="test.do";
	location.href="board_list.do";
</script>

- "board_list.do" 로 요청, 목록을 가져오는 요청이다

 

- Controller 클래스 BoardController.java 에서 "board_list.do" 요청 부분만

	/* 게시판 목록 */
	@RequestMapping(value = "/board_list.do")
	public String list(HttpServletRequest request, Model model) throws Exception {

		List<BoardBean> boardlist = new ArrayList<BoardBean>();

		int page = 1;
		int limit = 10; // 한 화면에 출력할 레코드수

		if (request.getParameter("page") != null) {
			page = Integer.parseInt(request.getParameter("page"));
		}

		// 총 리스트 수를 받아옴.
		int listcount = boardService.getListCount();

		// 페이지 번호(page)를 DAO클래스에게 전달한다.
		boardlist = boardService.getBoardList(page); // 리스트를 받아옴.

		// 총 페이지 수.
		int maxpage = (int) ((double) listcount / limit + 0.95); // 0.95를 더해서 올림
																	// 처리.
		// 현재 페이지에 보여줄 시작 페이지 수(1, 11, 21 등...)
		int startpage = (((int) ((double) page / 10 + 0.9)) - 1) * 10 + 1;
		// 현재 페이지에 보여줄 마지막 페이지 수.(10, 20, 30 등...)
		int endpage = maxpage;

		if (endpage > startpage + 10 - 1)
			endpage = startpage + 10 - 1;

		model.addAttribute("page", page);
		model.addAttribute("startpage", startpage);
		model.addAttribute("endpage", endpage);
		model.addAttribute("maxpage", maxpage);
		model.addAttribute("listcount", listcount);
		model.addAttribute("boardlist", boardlist);
		
		return "board/board_list";
	}

- 자세한 설명은 원문 글 작성 기능 설명 후

- 기본 변수와 파생변수들을 만들고 Model 객체에 저장한 후 board_list.jsp 로 이동

 

- board_list.jsp

<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@ page import="java.util.*"%>
<%@ page import="myspring.model.*"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<!doctype html>
<html lang="ko">
<head>
	<meta charset="UTF-8">
	<title>게시판 목록</title>
    <link rel="stylesheet" href="<%=request.getContextPath() %>/css/bbs.css" type="text/css">
</head>

<body>
	<!-- 게시판 리스트 -->
	<div id="bbslist_wrap">
		<h2 class="bbslist_title">게시판 목록</h2>
		<div id="bbslist_c">글 개수 : ${listcount}</div>

		<table id="bbslist_t">
			<tr align="center" valign="middle" bordercolor="#333333">
				<td style="font-family: Tahoma; font-size: 11pt;" width="8%"
					height="26">
					<div align="center">번호</div>
				</td>
				<td style="font-family: Tahoma; font-size: 11pt;" width="47%">
					<div align="center">제목</div>
				</td>
				<td style="font-family: Tahoma; font-size: 11pt;" width="14%">
					<div align="center">작성자</div>
				</td>
				<td style="font-family: Tahoma; font-size: 11pt;" width="17%">
					<div align="center">날짜</div>
				</td>
				<td style="font-family: Tahoma; font-size: 11pt;" width="14%">
					<div align="center">조회수</div>
				</td>
			</tr>

			<!-- 화면 출력 번호  변수 정의 -->		
			<c:set var="num" value="${listcount-(page-1)*10}"/> 	
	
			<!-- 반복문 시작 -->
			<c:forEach var="b" items="${boardlist}">
			
			<tr align="center" valign="middle" bordercolor="#333333"
				onmouseover="this.style.backgroundColor='F8F8F8'"
				onmouseout="this.style.backgroundColor=''">
				<td height="23" style="font-family: Tahoma; font-size: 10pt;">					
 					<!-- 번호 출력 부분 -->	
 					<c:out value="${num}"/>			
					<c:set var="num" value="${num-1}"/>	 
				</td>
				
				<td style="font-family: Tahoma; font-size: 10pt;">
					<div align="left">						
						
					<c:if test="${b.board_re_lev != 0}"> 
						<c:forEach var="k" begin="1" end="${b.board_re_lev}">
							&nbsp;&nbsp;			
						</c:forEach>
						<img src="./images/AnswerLine.gif">	
					</c:if>					
						
					<!-- 제목 출력 부분 -->	
					<a href="board_cont.do?board_num=${b.board_num}&page=${page}&state=cont">
							${b.board_subject}
					</a>
					</div>
				</td>

				<td style="font-family: Tahoma; font-size: 10pt;">
					<div align="center">${b.board_name}</div>
				</td>
				<td style="font-family: Tahoma; font-size: 10pt;">
					<div align="center">${b.board_date}</div>
				</td>
				<td style="font-family: Tahoma; font-size: 10pt;">
					<div align="center">${b.board_readcount}</div>
				</td>
			</tr>
			
			</c:forEach>
			<!-- 반복문 끝 -->			
		</table>
		

		<div id="bbslist_paging">			
			<c:if test="${page <=1 }">
				[이전]&nbsp;
			</c:if>
			
			<c:if test="${page > 1 }">
				<a href="board_list.do?page=${page-1}">[이전]</a>&nbsp;
			</c:if>			

			<c:forEach var="a" begin="${startpage}" end="${endpage}">
				<c:if test="${a == page }">
					[${a}]
				</c:if>
				<c:if test="${a != page }">
					<a href="board_list.do?page=${a}">[${a}]</a>&nbsp;
				</c:if>
			</c:forEach>			
			
			<c:if test="${page >= maxpage }">
				[다음] 
			</c:if>
			<c:if test="${page < maxpage }">
				<a href="board_list.do?page=${page+1}">[다음]</a>
			</c:if>			
			
		</div>
		<div id="bbslist_w">
			<input type="button" value="글쓰기" class="input_button"
				onclick="location='board_write.do?page=${page}'">
		</div>
		
	</div>
</body>
</html>

- 자세한 설명은 원문 글 작성 기능 설명 후

- 목록 페이지 board_list.jsp 에서 "글쓰기" 클릭시 "board_write.do" 요청

 

원문 글 작성 기능

- Controller 클래스 BoardController.java 에서 "board_write.do" 요청 부분만

	/* 게시판 글쓰기 폼 */
	@RequestMapping(value = "/board_write.do")
	public String board_write() {
		return "board/board_write";
	}

- 원문 글 작성 폼 board_write.jsp 로 이동

 

- board_write.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>    
    
<!doctype html>
<html lang="ko">
<head>
	<meta charset="UTF-8">
	<title>게시판 입력폼</title>
	<link rel="stylesheet" type="text/css" href="<%=request.getContextPath() %>/css/bbs.css" />
	<script src="http://code.jquery.com/jquery-latest.js"></script>
	<script src="<%=request.getContextPath() %>/js/board.js"></script>
</head>

<body>
 <div id="bbswrite_wrap">
  <h2 class="bbswrite_title">게시판 입력폼</h2>
  <form method="post" action="<%=request.getContextPath() %>/board_write_ok.do" onSubmit="return board_check()">
   <table id="bbswrite_t">
    <tr>
     <th>글쓴이</th>
     <td>
     <input name="board_name" id="board_name" size="14" class="input_box" />
     </td>
    </tr>
    
    <tr>
     <th>비밀번호</th>
     <td>
      <input type="password" name="board_pass" id="board_pass" size="14"
      class="input_box" />
     </td>
    </tr>
    
    <tr>
     <th>글제목</th>
     <td>
      <input name="board_subject" id="board_subject" size="40" 
      class="input_box" />
     </td>
    </tr>
    
    <tr>
     <th>글내용</th>
     <td>
      <textarea name="board_content"  id="board_content" rows="8" cols="50"
      class="input_box"></textarea>
     </td>
    </tr> 
    
   </table>
   
   <div id="bbswrite_menu">
    <input type="submit" value="등록" class="input_button" />
    <input type="reset" value="취소" class="input_button"
    onclick="$('#board_name').focus();" />
   </div>
  </form>
 </div>
</body>
</html>

+ Spring 에서는 어노테이션 기반이므로 Model 2 와 달리 action 으로 <%=request.getContext() 가 없어도 잘 찾아간다

- 원문 글 작성 폼의 입력양식 name 값 을 DTO 프로퍼티명과 일치시킨다

- 입력 후 "등록" 클릭시 "board_write_ok.do" 로 요청한다

 

- Controller 클래스 BoardController.java 에서 "board_write_ok.do" 요청 부분만

	/* 게시판 저장 */
	@RequestMapping(value = "/board_write_ok.do", method = RequestMethod.POST)
	public String board_write_ok(@ModelAttribute BoardBean board)
			throws Exception {
//	public String board_write_ok(@RequestParam HashMap board)
//			throws Exception {
	
		boardService.insert(board);// 저장 메서드 호출	

		return "redirect:/board_list.do";
	}

- 입력양식의 name 값을 DTO 프로퍼티명과 일치시켰으므로 @ModelAttribute 를 통해 입력되어 넘어온 값들을 모두 생성된 DTO 객체 board 에 저장

- 이 DTO 객체 board 에 board_re_ref, board_re_lev, board_re_seq, board_readcount 는 자동으로 0으로 초기화된다

+ DTO int 형 멤버변수(필드, 프로퍼티) 는 자동으로 0 으로 초기화 된다

+ 주석으로 설정된 부분은 Map 을 통해서 넘어온 값들을 받는 방법이다, 키 밸류 형태로 저장됨

- Service 클래스의 메소드 insert() 를 호출하고, 삽입할 글을 담은 객체 board 를 매개변수로 전달

<돌아온 후>

- 글 작성 이후에 목록 페이지로 돌아갈 것이므로 여기서 redirect: 를 써서 "board_list.do" 로 요청

 

- Service 클래스 BoardServiceImpl.java 에서 insert() 메소드 부분만

	/* 게시판 저장 */
	public void insert(BoardBean b) throws Exception {
		boardDao.insertBoard(b);
	}

- DAO 클래스 BoardServiceImpl.java 에서 insertBoard() 메소드 부분만

	/* 게시판 저장 */
	public void insertBoard(BoardBean b) throws Exception {
		sqlSession.insert("Test.board_insert", b);
	}

- Mapper 파일 중 namespace 가 Test 인 board.xml 에서 id 가 "board_insert" 인 SQL문 부분만

	<!-- 게시판 저장 -->
	<insert id="board_insert" parameterType="board">
		insert into board53
		(board_num,board_name,board_pass,board_subject,
		board_content,board_re_ref,board_re_lev,board_re_seq,board_readcount,board_date)
		values
		(board53_num_seq.nextval,#{board_name},#{board_pass},#{board_subject},
		#{board_content},board53_num_seq.nextval,0,0,0,SYSDATE)
	</insert>

 - 원문이므로 board_re_ref 는 board_num 과 같은 값이 들어간다

- 즉 board_num 과 board_re_ref 는 같은 시퀀스로 값이 입력됨 

- 원믄 글이므로 board_re_lev, board_re_seq 는 0 이 들어감, 글 작성 SQL문이므로 조회수도 0 으로 설정

- 작성 날짜는 SYSDATE 로 넣기

- 나머지는 전달받은 객체 board 로 부터 값을 꺼내서 세팅


글 목록 (게시판) 기능

- 게시판 글 목록 출력은 많이 했으므로 간략한 설명 또는 처음하는 부분만 설명

- Controller 클래스 BoardController.java 에서 "board_list.do" 요청 부분만

	/* 게시판 목록 */
	@RequestMapping(value = "/board_list.do")
	public String list(HttpServletRequest request, Model model) throws Exception {

		List<BoardBean> boardlist = new ArrayList<BoardBean>();

		int page = 1;
		int limit = 10; // 한 화면에 출력할 레코드수

		if (request.getParameter("page") != null) {
			page = Integer.parseInt(request.getParameter("page"));
		}

		// 총 리스트 수를 받아옴.
		int listcount = boardService.getListCount();

		// 페이지 번호(page)를 DAO클래스에게 전달한다.
		boardlist = boardService.getBoardList(page); // 리스트를 받아옴.

		// 총 페이지 수.
		int maxpage = (int) ((double) listcount / limit + 0.95); // 0.95를 더해서 올림
																	// 처리.
		// 현재 페이지에 보여줄 시작 페이지 수(1, 11, 21 등...)
		int startpage = (((int) ((double) page / 10 + 0.9)) - 1) * 10 + 1;
		// 현재 페이지에 보여줄 마지막 페이지 수.(10, 20, 30 등...)
		int endpage = maxpage;

		if (endpage > startpage + 10 - 1)
			endpage = startpage + 10 - 1;

		model.addAttribute("page", page);
		model.addAttribute("startpage", startpage);
		model.addAttribute("endpage", endpage);
		model.addAttribute("maxpage", maxpage);
		model.addAttribute("listcount", listcount);
		model.addAttribute("boardlist", boardlist);
		
		return "board/board_list";
	}

- 자세한 설명은 원문 글 작성 기능 설명 후

- 기본 변수와 파생변수들을 만들고, 그 페이지에 해당하는 리스트를 받아온 후 변수와 리스트를 Model 객체에 저장한 후 board_list.jsp 로 이동

 

총 페이지 수 계산 부분 (BoardController.java 부분)

		// 총 페이지 수.
		int maxpage = (int) ((double) listcount / limit + 0.95); // 0.95를 더해서 올림 처리

- 이렇게 해도 계산 된다, 새로운 방법

 

startpage, endpage 변수 설정 (BoardController.java 부분)

		// 현재 페이지에 보여줄 시작 페이지 수(1, 11, 21 등...)
		int startpage = (((int) ((double) page / 10 + 0.9)) - 1) * 10 + 1;
		// 현재 페이지에 보여줄 마지막 페이지 수.(10, 20, 30 등...)
		int endpage = maxpage;

- 이렇게 해도 계산 된다, 새로운 방법

 

+ DAO 클래스 BoardDaoImpl.java 에서 getListCount() 부분만

	/* 게시판 총 갯수  */
	public int getListCount() throws Exception {
		int count = 0;	
		count = ((Integer) sqlSession.selectOne("Test.board_count")).intValue();

		return count;
	}

- selectOne() 메소드의 리턴자료형은 Object 이다, 원래는 (Integer) 과 .intValue() 로 다운캐스팅, 언박싱을 해야한다

- 하지만 생략해도 됨, MyBatis 기능이다

 

+ Mapper 파일 board.xml 에서 id 가 "board_list" 인 SQL문 부분만

	<!-- 게시판 목록 (page번호를 전달받아서 startRow와 endRow를 구함) -->
	<select id="board_list" parameterType="int" resultType="board">
	    <![CDATA[
		select * from 
		 (select rownum rnum,BOARD_NUM,BOARD_NAME,BOARD_SUBJECT,BOARD_CONTENT,
		 BOARD_RE_REF,BOARD_RE_LEV,BOARD_RE_SEQ,BOARD_READCOUNT,
	 	 BOARD_DATE from  
	  	 (select * from board53 order by BOARD_RE_REF desc,BOARD_RE_SEQ asc)) 
	   		 where rnum >= ((#{page}-1) * 10+1)  and rnum <= (#{page} * 10)
		]]>
	</select>

- 두번째 서브쿼리를 별칭을 쓰지 않고 있으므로, 첫번째 서브쿼리에서 별칭.* 으로 처리하는 대신 일일히 두번째 서브쿼리의 모든 컬럼을 쓰고 있다

- board_re_ref 로 내림차순 정렬하고 두번째 정렬로 board_re_seq 를 오름차순으로 정렬함

+ 같은 부모를 가진 댓글들의 board_re_ref 는 부모의 board_re_ref 값으로 모두 같다

- < , > 를 인식하지 못하므로 대신해서 &gt; %lt; 를 쓰거나 또는 전체를 CDATA 로 묶은 뒤 < , > 사용

- where 절에 있는 10 은 limit 를 의미함

 

 

- View 페이지로 이동

- board_list.jsp

<%@ page language="java" contentType="text/html; charset=utf-8"%>
<%@ page import="java.util.*"%>
<%@ page import="myspring.model.*"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<!doctype html>
<html lang="ko">
<head>
	<meta charset="UTF-8">
	<title>게시판 목록</title>
    <link rel="stylesheet" href="<%=request.getContextPath() %>/css/bbs.css" type="text/css">
</head>

<body>
	<!-- 게시판 리스트 -->
	<div id="bbslist_wrap">
		<h2 class="bbslist_title">게시판 목록</h2>
		<div id="bbslist_c">글 개수 : ${listcount}</div>

		<table id="bbslist_t">
			<tr align="center" valign="middle" bordercolor="#333333">
				<td style="font-family: Tahoma; font-size: 11pt;" width="8%"
					height="26">
					<div align="center">번호</div>
				</td>
				<td style="font-family: Tahoma; font-size: 11pt;" width="47%">
					<div align="center">제목</div>
				</td>
				<td style="font-family: Tahoma; font-size: 11pt;" width="14%">
					<div align="center">작성자</div>
				</td>
				<td style="font-family: Tahoma; font-size: 11pt;" width="17%">
					<div align="center">날짜</div>
				</td>
				<td style="font-family: Tahoma; font-size: 11pt;" width="14%">
					<div align="center">조회수</div>
				</td>
			</tr>

			<!-- 화면 출력 번호  변수 정의 -->		
			<c:set var="num" value="${listcount-(page-1)*10}"/> 	
	
			<!-- 반복문 시작 -->
			<c:forEach var="b" items="${boardlist}">
			
			<tr align="center" valign="middle" bordercolor="#333333"
				onmouseover="this.style.backgroundColor='F8F8F8'"
				onmouseout="this.style.backgroundColor=''">
				<td height="23" style="font-family: Tahoma; font-size: 10pt;">					
 					<!-- 번호 출력 부분 -->	
 					<c:out value="${num}"/>			
					<c:set var="num" value="${num-1}"/>	 
				</td>
				
				<td style="font-family: Tahoma; font-size: 10pt;">
					<div align="left">						
						
					<c:if test="${b.board_re_lev != 0}"> 
						<c:forEach var="k" begin="1" end="${b.board_re_lev}">
							&nbsp;&nbsp;			
						</c:forEach>
						<img src="./images/AnswerLine.gif">	
					</c:if>					
						
					<!-- 제목 출력 부분 -->	
					<a href="board_cont.do?board_num=${b.board_num}&page=${page}&state=cont">
							${b.board_subject}
					</a>
					</div>
				</td>

				<td style="font-family: Tahoma; font-size: 10pt;">
					<div align="center">${b.board_name}</div>
				</td>
				<td style="font-family: Tahoma; font-size: 10pt;">
					<div align="center">${b.board_date}</div>
				</td>
				<td style="font-family: Tahoma; font-size: 10pt;">
					<div align="center">${b.board_readcount}</div>
				</td>
			</tr>
			
			</c:forEach>
			<!-- 반복문 끝 -->			
		</table>
		

		<div id="bbslist_paging">			
			<c:if test="${page <=1 }">
				[이전]&nbsp;
			</c:if>
			
			<c:if test="${page > 1 }">
				<a href="board_list.do?page=${page-1}">[이전]</a>&nbsp;
			</c:if>			

			<c:forEach var="a" begin="${startpage}" end="${endpage}">
				<c:if test="${a == page }">
					[${a}]
				</c:if>
				<c:if test="${a != page }">
					<a href="board_list.do?page=${a}">[${a}]</a>&nbsp;
				</c:if>
			</c:forEach>			
			
			<c:if test="${page >= maxpage }">
				[다음] 
			</c:if>
			<c:if test="${page < maxpage }">
				<a href="board_list.do?page=${page+1}">[다음]</a>
			</c:if>			
			
		</div>
		<div id="bbslist_w">
			<input type="button" value="글쓰기" class="input_button"
				onclick="location='board_write.do?page=${page}'">
		</div>
		
	</div>
</body>
</html>

- 화면 출력 번호를 만들어서 forEach 루프가 돌아갈때마다 화면 출력번호를 출력 하고, 재정의해서 1씩 감소시킴

- 글 목록에서의 원문과 댓글의 제목 출력 형태를 다르게 함, 댓글의 깊이 board_re_lev만큼 루프를 돌려서 왼쪽 간격 띄움

+ 여기서는 [이전] 클릭시 이전 페이지 ${page-1} 로, [다음] 클릭시 다음 페이지 ${page+1} 로 이동

 

제목 클릭시 상세 페이지로 이동시 넘기는 값 (board_list.jsp 부분)

		<!-- 제목 출력 부분 -->	
		<a href="board_cont.do?board_num=${b.board_num}&page=${page}&state=cont">
				${b.board_subject}
		</a>

- 상세페이지로 이동하려고 한다, "board_cont.do" 로 요청함

- 요청하면서 전달하는 값이 글 번호와 페이지 번호 외에도 state 라는 변수에 cont 라는 값을 저장해서 전달함

- 상세 페이지, 수정, 삭제 등 여러 기능을 1개의 요청으로 처리하기 위해서 state 값을 다르게 설정함

 

- Controller 클래스에서 "board_cont.do" 요청 처리 부분을 보자

	/* 게시판 내용보기,삭제폼,수정폼,답변글폼 */
	@RequestMapping(value = "/board_cont.do")
	public String board_cont(@RequestParam("board_num") int board_num,
			@RequestParam("page") String page,
			@RequestParam("state") String state, 
			Model model) throws Exception {

		if (state.equals("cont")) { // 내용보기일때만
			boardService.hit(board_num); // 조회수 증가
		}

		BoardBean board = boardService.board_cont(board_num);

		model.addAttribute("bcont", board);
		model.addAttribute("page", page);

		if (state.equals("cont")) {// 내용보기일때
			return "board/board_cont";// 내용보기 페이지 설정
			// String board_cont = board.getBoard_content().replace("\n",
			// "<br/>");
			// 글내용중 엔터키 친부분을 웹상에 보이게 할때 다음줄로 개행
			// contM.addObject("board_cont", board_cont);
		} else if (state.equals("edit")) {// 수정폼
			return "board/board_edit";
		} else if (state.equals("del")) {// 삭제폼
			return "board/board_del";
		} else if (state.equals("reply")) {// 답변달기 폼
			return "board/board_reply";
		}
		return null;
	}

- 상세 페이지, 삭제폼, 수정폼, 답변글 폼 요청을 모두 이 @RequestMapping("/board_cont.do") 에서 처리한다

- state 값을 @RequestParam 으로 받아옴

- state 값이 "cont" 인 경우, 즉 상세페이지 요청인 경우에만 조회수 값을 증가시키고 있다

-  상세 페이지, 삭제폼, 수정폼, 답변글 폼 요청은 글 번호와 페이지 번호를 전달하는 등의 같은 형식으로 되어있고 상세 정보를 구해오는 등의 비슷한 기능을 수행

- 그러므로 같은 요청으로 처리하고 다른 부분은 if-else if 문으로 state 값에 따른 다른 처리를 함

 

 

게시판 직접 만들어보기 (이어서)

환경 설정 파일 작성(세팅) 순서

1) pom.xml

2) web.xml

3) servlet-context.xml

4) configuration.xml

5) board.xml

6) root-context.xml

- 4), 5), 6) 은 DB 연동 관련 내용

- configuration.xml, board,xml(Mapper 파일) 환경설정 파일은 resources 폴더 안에 넣는다

- configuration.xml 에서 DTO 개수에 따라 typeAlias 설정이 늘어남


게시판 직접 만들어보기 (이어서)

상세 페이지

- 저번에 만든 목록 페이지 boardlist.jsp 에서 제목을 클릭하면 상세페이지로 넘어간다

<a href="boardcontent.do?no=${b.no}&page=${page}">${b.subject}</a>

- "boardContent.do" 로 요청하며 글 번호 no 와 페이지 번호 page 를 가져간다

- 글 번호와 페이지 번호를 최대 3번까지 전달하게 되는데 여기가 전달하는 출발점이다

ex) 목록 페이지 -> 상세 페이지 -> 수정/삭제 폼

 

- Controller 클래스 BoardController.java 에서 "boardContent.do" 요청 부분을 처리하자

	// 상세 페이지 : 조회수 1 증가 + 상세 정보 구하기
	@RequestMapping("boardcontent.do")
	public String boardcontent(int no, int page, Model model) {
		
		bs.updatecount(no);	// 조회수 1 증가
		Board board = bs.getBoard(no);	// 상세 정보 구하기
		String content = board.getContent().replace("\n", "<br>");
		
		model.addAttribute("board", board);
		model.addAttribute("content", content);
		model.addAttribute("page", page);
		
		return "board/boardcontent";
	}

- 앞에서 넘어온 글 번호, 페이지 번호의 변수명과 같은 이름을 써서 바로 값이 들어가게끔 한다, @RuequestParam 생략

- 앞에서 받아온 Service 객체 bs 를 사용해서 updatecount() , getBoard() 메소드를 호출한다

상세 페이지 Controller 처리에서 해야할 DB 연동 2가지

1. updatecount() : 조회수 증가 Update 문

- 글 번호 no 를 전달한다

- 리턴 받지 않기로 함

2. getBoard() : 상세 정보 구하기 Select 문

- 글 번호 no 를 전달한다

- 리턴은 DTO 클래스 Board 형으로 받는다

 

- 이 후 내용 content 의 줄바꿈 처리를 하기 위해 내용을 따로 구하고 replace() 메소드를 써서 바꾼 후 내용을 따로 Model 객체에 저장한다

View 페이지로 가져갈 값 3가지

1. getBoard() 에서 리턴받은 상세 정보 DTO 객체 board

2. 줄이 바뀐 내용 content

3. 목록 페이지에서 넘어온 페이지 번호 page

- 글 번호는 객체 board 안에 들어있으므로 페이지 번호만 따로 가져간다

- 이후 board 폴더 하위에 boardcontent.jsp 를 생성해야함

 

<조회수 증가 Update 문>
- Service 클래스 BoardService.java 에서 updatecontent() 메소드 작성

	public void updatecount(int no) {
		dao.updatecount(no);
	}

- 리턴 받지 않으므로 return 을 쓰지 않는다, 그냥 DAO의 메소드 updatecount() 만 호출함

 

- DAO 클래스 BoardDao.java 에서 updatecontent() 메소드 작성

	public void updatecount(int no) {
		session.update("hit", no);
	}

- Mapper 파일에서 id 가 "hit" 인 SQL문을 불러옴, 전달하는 값은 글 번호인 no

 

- Mapper 파일 board.xml 에서 id 가 "hit" 인 조회수 증가 Update SQL문 작성

	<!-- 조회수 1 증가 -->
	<update id="hit" parameterType="int">
		update myboard set readcount=readcount+1 where no=#{no}
	</update>

 - 넘어온 값이 글 번호 no 이므로 parameterType 은 "int", where 절에는 #{no} 를 사용

 

<상세 정보 구하기 Select 문>

- Service 클래스 BoardService.java 에서 getBoard() 메소드 작성

	public Board getBoard(int no) {
		return dao.getBoard(no);
	}

- 위에서 주입받은 DAO 객체 dao 로 getBoard() 메소드 호출, 글 번호 전달

 

- DAO 클래스 BoardDao.java 에서 getBoard() 메소드 작성

	public Board getBoard(int no) {
		return session.selectOne("content", no);
	}

- Mapper 파일에서 id 가 "content" 인 SQL문을 불러옴, 전달하는 값은 글 번호인 no

- 글 1개 에 대한 상세정보를 가져오므로 selectOne() 메소드 사용

 

- Mapper 파일 board.xml 에서 id 가 "content" 인 조회수 증가 SelectSQL문 작성

	<!-- 상세정보 구하기 -->
	<select id="content" parameterType="int" resultType="board">
		select * from myboard where no = #{no}
	</select>

 

- View 페이지인 boardcontent.jsp 를 WEB-INF/views/board/ 폴더 하위에 생성하기

- boardcontent.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>상세 페이지</title>
</head>
<body>

<table border=1 width=400 align=center>
	<caption>상세 페이지</caption>
	<tr>
		<td>작성자</td>
		<td>${board.writer}</td>
	</tr>
	<tr>
		<td>날짜</td>
		<td>
			<fmt:formatDate value="${board.register}"
				pattern="yyyy-MM-dd HH:mm:ss"/>
		</td>
	</tr>
	<tr>
		<td>조회수</td>
		<td>${board.readcount}</td>
	</tr>
	<tr>
		<td>제목</td>
		<td>${board.subject}</td>
	</tr>
	<tr>
		<td>내용</td>
		<td>
			<pre>${board.content}</pre>
			${board.content}
		</td>
	</tr>
	<tr>
		<td colspan=2 align=center>
			<input type="button" value="목록"
			onClick="location.href='boardlist.do?page=${page}'"/>
			<input type="button" value="수정"
			onClick="loacation.href='boardupdateform.do?no=${board.no}&page=${page}'"/>
			<input type="button" value="삭제"
			onClick="loacation.href='boarddeleteform.do?no=${board.no}&page=${page}'"/>
		</td>
	</tr>
</table>

</body>
</html>

- 날짜 시간 패턴을 지정하기 위해 국제화 라이브러리 fmt 를 불러옴

- 내용의 줄바꿈을 처리할때는 <pre></pre> 사용 또는 앞에서 replace() 를 써서 바꿔서 저장한 내용 ${content} 를 불러옴

- 현재는 내용이 한 줄이라 줄바꿈이 제대로 되었는지 확인이 어려움

 

버튼 처리 : 목록 버튼 (boardcontent.jsp 부분)

			<input type="button" value="목록"
			onClick="location.href='boardlist.do?page=${page}'"/>

- 상세 페이지에서 '목록' 버튼을 누르면 다시 목록 페이지로 돌아간다

- 돌아갈때 페이지번호를 전달해서 원래 페이지로 돌아가게끔 한다

- 다시 돌아옴

 

버튼 처리 : 수정 버튼 (boardcontent.jsp 부분)

			<input type="button" value="수정"
			onClick="location.href='boardupdateform.do?no=${board.no}&page=${page}'"/>

- 수정 폼으로 가도록 "boardupdateform.do" 로 요청한다, 요청하면서 글 번호, 페이지 번호를 전달

- 글 번호는 객체 board 안에 있으므로 ${board.no} 로 가져오기

 

버튼 처리 : 삭제 버튼 (boardcontent.jsp 부분)

			<input type="button" value="삭제"
			onClick="location.href='boarddeleteform.do?no=${board.no}&page=${page}'"/>

- 삭제 폼으로 가도록 "boarddeleteform.do" 로 요청한다, 요청하면서 글 번호, 페이지 번호를 전달

- 글 번호는 객체 board 안에 있으므로 ${board.no} 로 가져오기

 


글 수정 폼

			<input type="button" value="수정"
			onClick="location.href='boardupdateform.do?no=${board.no}&page=${page}'"/>

- 상세 페이지에서 '수정'을 누르면 "boardupdateform.do" 로 요청한다, 요청하면서 글 번호, 페이지 번호를 전달

 

- Controller 클래스 BoardController.java 에서 "boardupdateform.do" 요청 부분을 처리하자

	// 수정 폼
	@RequestMapping("boardupdateform.do")
	public String boardupdateform(int no, int page, Model model) {
		
		Board board = bs.getBoard(no); // 상세 정보 구하기
		
		model.addAttribute("board", board);
		model.addAttribute("page", page);
		
		return "board/boardupdateform";
	}

- 앞에서 넘어온 글 번호, 페이지 번호의 변수명과 같은 이름을 써서 바로 값이 들어가게끔 한다, @RuequestParam 생략

- 앞의 상세 페이지를 처리할때 만든 상세정보를 가져오는 메소드 getBoard() 를 호출해서 상세 정보 객체를 구해온다

- 객체 board 와 페이지 번호 page 를 Model 객체에 저장

 

- getBoard() 메소드에 대해서는 이전에 설명했으므로 생략

 

- View 페이지 boardupdateform.jsp 를 board 폴더 하위에 생성

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>글수정</title>
</head>
<body>

<form method=post action="boardupdate.do">
<input type="hidden" name="no" value="${board.no}"/>
<input type="hidden" name="page" value="${page}"/>
<table border=1 width=400 align=center>
	<caption>글수정</caption>
	<tr><th>작성자명</th>
		<td><input type=text name="writer" required="required"
		value="${board.writer}" autofocus="autofocus"></td>
	</tr>
	<tr><th>비밀번호</th>
		<td><input type=password name="passwd" required="required"></td>
	</tr>
	<tr><th>제목</th>
		<td><input type=text name="subject" required="required"
		value="${board.subject}"></td>
	</tr>
	<tr><th>내용</th>
		<td><textarea cols=40 rows=5 name="content" required="required">
		${board.content}</textarea></td>
	</tr>
	<tr><td colspan=2 align=center>
			<input type=submit value="글수정">
			<input type=reset value="취소">
		</td>
	</tr>
</table>
</form>

</body>
</html>

- boardform.jsp 파일 내용을 복붙 후 수정

- value 속성을 이용해서 앞에서 넘어온 객체 board 안의 값을 뿌려준다

- 수정 폼 boardupdateform.jsp 에서 정보 입력 후 '수정' 을 누르면 "boardupdate.do" 로 요청한다

- hidden 객체를 사용해서 글 번호와 페이지 번호를 "boardupdate.do" 를 요청하며 전달한다

- 수정 위해 글 번호 필요, 수정 후 원래 페이지로 돌아가기 위해 페이지 번호가 필요

- 수정폼에서 넘어간 값들은 받을 때 DTO 객체에 저장해야한다, 페이지 번호 page 만 DTO 안에 들어갈 수 없으므로 따로 받아야한다

- 비밀번호 값이 맞는 경우에만 수정할 것

 


글 수정

- 앞의 글 수정 폼 boardupdateform.jsp 에서 입력 후 "글수정" 을 누르면 "boardupdate.do" 료 요청

 

- Controller 클래스 BoardController.java 에서 "boardupdate.do" 요청 부분을 처리하자

	// 글 수정
	@RequestMapping("boardupdate.do")
	public String boardupdate(Board board, int page, Model model) {
		int result = 0;
		Board old = bs.getBoard(board.getNo());
		
		// 비번 비교
		if(old.getPasswd().equals(board.getPasswd())) { // 비번 일치시
			result = bs.update(board); // 글 수정
		} else { // 비번 불일치시
			result = -1;
		}
		
		model.addAttribute("result", result);
		model.addAttribute("board", board);
		model.addAttribute("page", page);
		
		return "board/updateresult";
	}

- 넘어온 값들은 DTO 객체에 저장해야한다, @ModelAttribute 를 써서 (생략) 넘어온 값들을 바로 DTO 객체 board 에 저장

- 페이지 번호 page 만 DTO 안에 들어갈 수 없으므로 @RequestParam("page") (생략) 을 써서 따로 받는다

- 수정 성공 메세지를 뿌릴 것이므로 result 값을 Model 객체에 저장해서 넘겨줄 것, Model 객체도 선언함

글 수정 요청에서 해야할 DB연동 2가지

1. 비번 비교 Select SQL

- 비번 비교를 위해 hidden 으로 넘어온 값인 글 번호 no 를 써서 DB에 저장된 해당 글의 상세 정보를 가져옴 

- 이때, 상세 정보를 가져오는 메소드인 getBoard() 를 호출해서 상세정보를 구함

2. 글 수정 Update SQL

- Service 클래스의 메소드 update() 호출

- 전달하는 값은 수정할 데이터인 board

- 돌려받는 값은 글 수정 성공 개수, 즉 성공시 1 을 result 에 돌려받음

 

<View 와 관련>

- 이후 View 에서 수정 성공 / 실패 메세지 처리를 할 것이므로 Model 객체에 result 를 저장해서 전달

- View 에서 원래 페이지로 돌아가기 위해 Model 객체에 페이지 번호 page 를 저장해서 전달

- 수정 완료 후 상세페이지로 갈 때는 글 번호 no 와 페이지 번호 page 를 가져가야하므로  Model 객체에 글 번호를 가지고 있는 객체 board 를 Model 객체에 저장해서 전달

- 만약, 목록페이지로 갈 때는 Model 에 객체 board 를 가져갈 필요없다

 

- Service 클래스 BoardService.java 에서 update() 메소드 생성

	public int update(Board board) {
		return dao.update(board);
	}

 

- DAO 클래스 BoardDao.java 에서 update() 메소드 생성

	public int update(Board board) {
		return session.update("update", board);
	}

- Mapper 파일에서 id 가 "update" 인 SQL문을 불러옴, 전달하는 값은 수정할 데이터인 객체 board

- 수정 성공시 1 을 자동으로 돌려준다

 

- Mapper 파일 board.xml 에서 id 가 "update" 인 Update SQL문 작성

	<!-- 글 수정 -->
	<update id="update" parameterType="board">
		update myboard set writer=#{writer},subject=#{subject},
		content=#{content},register=sysdate where no=#{no}
	</update>

- Mapper 파일의 SQL문에선 SQL문 끝에 ; 를 찍으면 안된다

 

- View 페이지 updateresult.jsp 를 board 폴더 하위에 생성 및 작성

<%@ page language="java" 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">
<title>Insert title here</title>
</head>
<body>

<c:if test="${result == 1}">
	<script>
		alert("글 수정 성공");
		location.href="boardlist.do?page=${page}"; // 목록 페이지
//		location.href="boardcontent.do?no=${board.no}&page=${page}"; // 상세 페이지
	</script>
</c:if>
<c:if test="${result != 1}">
	<script>
		alert("글 수정 실패");
		history.go(-1);
	</script>
</c:if>

</body>
</html>

- if 태그를 사용하기 위해 JSTL core 라이브러리를 불러온다

- 수정 성공 후 목록 페이지로 가기 위해서 페이지 번호 page 를 전달해야 한다

- 수정 성공 후 상세 페이지로 가기 위해서 글 번호 no, 페이지 번호 page 를 전달해야한다

- 수정 실패 시 result 는 -1 이므로 "글 수정 실패" 메세지 출력 후 다시 이전 페이지인 수정 폼으로 돌아간다

 


글 삭제폼

			<input type="button" value="삭제"
			onClick="location.href='boarddeleteform.do?no=${board.no}&page=${page}'"/>

- 상세 페이지에서 '삭제'를 누르면 "boarddeleteform.do" 로 요청한다, 요청하면서 글 번호, 페이지 번호를 전달

 

- 글 번호는 객체 board 안에 있으므로 ${board.no} 로 가져오기

 

- Controller 클래스 BoardController.java 에서 "boarddeleteform.do" 요청 부분을 처리하자

 

- 앞에서 넘어온 글 번호, 페이지 번호의 변수명과 같은 이름을 써서 바로 값이 들어가게끔 한다, @RuequestParam 생략

- 글 삭제 폼에서는 여기서 글 번호, 페이지 번호 값을 받아서 Model 객체에 저장해서 가져가도 되지만, 글 삭제 폼에서 param 으로 바로 받아도 된다

- 그러므로 여기서는 아무값도 받아서 Model 객체에 저장해서 전달하지 않는다

- Model 2 에서도 View 에서 View 로 갈때 Controller 클래스를 거치지만 Contorller 에서 값을 중간 저장하고 전달하지 않았다, 바로 View 에서 전달한 값을 이동한 View 에서 getParameter() 로 받아왔었음, 여기서도 같은 방법을 사용 가능함

 

- View 페이지 boarddeleteform.jsp 를 생성 및 작성

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>글삭제</title>
</head>
<body>

<form method=post action="boarddelete.do">
<input type="hidden" name="no" value="${param.no}"/>
<input type="hidden" name="page" value="${param.page}"/>
<table border=1 width=400 align=center>
	<caption>글삭제</caption>
	<tr><th>비밀번호</th>
		<td><input type=password name="passwd" required="required"></td>
	</tr>
	<tr><td colspan=2 align=center>
			<input type=submit value="글삭제">
			<input type=reset value="취소">
		</td>
	</tr>
</table>
</form>

</body>
</html>

- boardupdateform.jsp 파일 내용을 복붙 후 수정

 

상세 페이지에서 전달된 글 번호, 페이지 번호 받기

- 상세 페이지에서 GET 방식으로 넘어온 값인 글 번호, 페이지 번호를 여기 View 에서 바로 param 으로 받을 수 있다

- 중간에 Controller 클래스를 거치지만 Contorller 에서 중간 저장 후 다시 전달할 필요 없다

- request.getParameter("네임") 은 ${param.네임} 과 같은 의미

<input type="hidden" name="no" value="${param.no}"/>
<input type="hidden" name="page" value="${param.page}"/>

- 이렇게 param 으로 받은 값을 다시 hidden 객체로 전달한다

- 비밀번호를 입력 후 "글삭제" 클릭시 "boarddelete.do" 로 요청한다

- 넘어가는 값 : 글 번호, 페이지 번호, 비밀번호


글 삭제

- 글 삭제 폼 boarddeleteform.jsp 에서 비밀번호 입력 후 "글삭제" 클릭시 "boarddelete.do" 로 요청

 

- Controller 클래스 BoardController.java 에서 "boarddelete.do" 요청 부분을 처리하자

	// 글 삭제
	@RequestMapping("boarddelete.do")
	public String boarddelete(Board board, int page, Model model) {
		int result = 0;
		Board old = bs.getBoard(board.getNo()); // 상세 ㅈ어보 구하기
		
		// 비번 비교
		if(old.getPasswd().equals(board.getPasswd())) { // 비번 일치시
			result = bs.delete(board.getNo());	// 글 삭제
		} else {	// 비번 불일치시
			result = -1;
		}
		model.addAttribute("result", result);
		model.addAttribute("page", result);
		
		return "board/deleteresult";
	}

- 넘어온 값들은 DTO 객체에 저장해야한다, @ModelAttribute 를 써서 (생략) 넘어온 값들을 바로 DTO 객체 board 에 저장

- 페이지 번호 page 만 DTO 안에 들어갈 수 없으므로 @RequestParam("page") (생략) 을 써서 따로 받는다

- 삭제 성공 메세지를 뿌릴 것이므로 result 값을 Model 객체에 저장해서 넘겨줄 것, Model 객체도 선언함

글 삭제 요청에서 해야할 DB연동 2가지

1. 비번 비교 Select SQL

- 비번 비교를 위해 hidden 으로 넘어온 값인 글 번호 no 를 써서 DB에 저장된 해당 글의 상세 정보를 가져옴 

- 이때, 상세 정보를 가져오는 메소드인 getBoard() 를 호출해서 상세정보를 구함

2. 글 수정 Update SQL

- Service 클래스의 메소드 delete() 호출

- 전달하는 값은 삭제할 글 번호인 no

- 돌려받는 값은 글 수정 성공 개수, 즉 성공시 1 을 result 에 돌려받음

 

- 삭제 성공 이후 View deleteresult.jsp 에서 목록 페이지로 갈 것이므로 페이지 번호 page 를 View 로 전달줘야함

+ 상세 페이지로 갈때는 글 번호도 전달해야한다

 

- Service 클래스 BoardService.java 에서 delete() 메소드 생성

	public int delete(int no) {
		return dao.delete(no);
	}

 

- DAO 클래스 BoardDao.java 에서 update() 메소드 생성

	public int delete(int no) {
		return session.delete("delete", no);
	}

- Mapper 파일에서 id 가 "delete" 인 SQL문을 불러옴, 전달하는 값은 수정할 글의 글 번호 no

- 삭제 성공시 1 을 자동으로 돌려준다

 

- Mapper 파일 board.xml 에서 id 가 "delete" 인 DeleteSQL문 작성

	<!-- 글 삭제 -->
	<delete id="delete" parameterType="int">
		delete from myboard where no = #{no}
	</delete>

 

- View 페이지 deleteresult.jsp 를 board 폴더 하위에 생성 및 작성

<%@ page language="java" 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">
<title>Insert title here</title>
</head>
<body>

<c:if test="${result == 1}">
	<script>
		alert("글 삭제 성공");
		location.href = "boardlist.do?page=${page}";
	</script>
</c:if>
<c:if test="${result != 1}">
	<script>
		alert("글 삭제 실패");
		history.go(-1);
	</script>
</c:if>

</body>
</html>

- if 태그를 사용하기 위해 JSTL core 라이브러리를 불러온다

- 삭제 성공 후엔 상세페이지로는 가지 못함, 목록페이지로 이동하자, 이동하면서 페이지 번호를 전달

- 삭제 실패 시 result 는 -1 이므로 "글 수정 실패" 메세지 출력 후 다시 이전 페이지인 수정 폼으로 돌아간다

 

- 삭제 성공시 "boardlist.do"로 요청하며 페이지번호를 전달하므로 원래 페이지로 돌아온다


메일 보내기

- 이메일 보내기 위한 메일 서버를 구축해야 이메일을 보낼 수 있다

- Window 계열에선 Exchange Server 서버 (유료) 사용, Linux 계열에선 Qmail 서버 사용

- Exchange Server 로 직접 서버를 구축(세팅)하지 않더라도 이메일을 제공해주는 서버들이 있다

ex) Naver 메일 또는 Gmail 서버를 사용하면 메일 보내고 받기가 가능하다

- 문자로 보내는 것은 유료가 많음, 메일 보내는 것은 Naver, Google 메일 활용시 무료로 ID, 비번 찾기 구현 가능

 

메일 서버 프로토콜

- Maile 송신(보내기) : STMP (Simple Mail Transfer Protocol) , 기본포트는 25번

- Maile 수신(받기) : POP3(Post Office Protocol 3) , 기본포트는 110번

 

- 네이버 메일 또는 Gmail 에서 이메일 서버를 사용하려면 환경설정이 필요하다

- 환경설정 후 Spring 을 이용해서 메일 서버를 사용해서 메일 보내기 가능

- 주로 회원관리 프로그램의 아이디 찾기, 비밀번호 찾기 시 메일을 보낸다

 

Email 보내기

1. Naver Mail Server 활용 : 네이버 메일에서 STMP 활성화

- 우리는 메일을 보내는 작업을 할 것이므로 네이버에서 SMTP 를 활성화 시켜야한다

- SMTP 기본포트 25번 대신 새로운 포트 465 을 사용하고 있다, 즉 기본 포트는 잘 사용하지 않음, 대부분 포트번호를 바꿔서 사용함

- IMAP(POP3 와 비슷한 기능, 암호화 된 것) 도 사용함으로 설정해준다

 

2. mailTest 프로젝트 가져오기

- 클라우드에서 프로젝트 mailTest 를 다운받아 압축을 풀고 import 한다

 

- 메일을 보내기 위해 pom.xml 부터 먼저 설정

 

3. pom.xml 에 메일 보내기 위한 의존 라이브러리 추가

- 메일을 보내기 위한 의존 라이브러리를 추가해야한다

- 여러가지 라이브러리가 있다, 지금은 그 중 commons-email 라이브러리 사용

- pom.xml 부분

		<!-- EMail -->
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-email</artifactId>
			<version>1.5</version>
		</dependency>

- 이 부분이 추가되어 있어야 메일 보내기 가능

 

+ web.xml 설정

- web.xml 에 servlet-mapping 의 url-pattern 가 *.do 로 설정되어있다, 확장자 do 로 요청해야한다

 

4. index 파일 실행

- index.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
    
<!DOCTYPE html">
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>

<a href="send.do">메일 전송</a><br><br>

<script>
//	location.href="send.do";
</script>

<%
//	response.sendRedirect("send.do");
%>

</body>
</html>

- "메일 전송" 클릭 시 "send.do" 로 요청한다

 

- Contorller 클래스를 보자

- servlet-context.xml 의 base-pacakge 가 controller 이므로 controller 폴더 아래에 Controller 클래스 MailTest.java 가 있다

 

package controller;

import java.util.Random;

import org.apache.commons.mail.HtmlEmail;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

@Controller
public class MailTest {

	@RequestMapping("/send.do")
	public String send(Model model) {

		Random random = new Random();
		int a = random.nextInt(100);

		// Mail Server 설정
		String charSet = "utf-8";
		String hostSMTP = "smtp.naver.com";
		String hostSMTPid = "내아이디@naver.com";
		String hostSMTPpwd = "내비번"; 	// 비밀번호 입력해야함

		// 보내는 사람 EMail, 제목, 내용
		String fromEmail = "내아이디@naver.com";
		String fromName = "친절한 Lay씨";
		String subject = "Overflow인증메일입니다.";

		// 받는 사람 E-Mail 주소
		String mail = "내아이디@naver.com";

		try {
			HtmlEmail email = new HtmlEmail();
			email.setDebug(true);
			email.setCharset(charSet);
			email.setSSL(true);
			email.setHostName(hostSMTP);
			email.setSmtpPort(587);

			email.setAuthentication(hostSMTPid, hostSMTPpwd);
			email.setTLS(true);
			email.addTo(mail, charSet);
			email.setFrom(fromEmail, fromName, charSet);
			email.setSubject(subject);
			email.setHtmlMsg("<p align = 'center'>Overflow에 오신것을 환영합니다.</p><br>" 
							 + "<div align='center'> 인증번호 : " + a + "</div>");
			email.send();
		} catch (Exception e) {
			System.out.println(e);
		}		
		model.addAttribute("result", "good~!!\n 등록된 E-Mail 확인");

		return "result";
	}
}

- 0 ~ 99 까지의 난수값을 메일로 보낼 것

- 난수를 발생시켜서 변수 a 에 저장했다

 

1. 메일 서버 설정

- 네이버 메일의 smtp 서버를 쓰므로 서버의 네임 hostSTMP 는 "smtp.naver.com" 이다

- hostSTMPid 에는 내 이메일, hostSTMPpwd 에는 내 비밀번호

- 반드시 네이버 이메일이어야함

 

2. 보내는 사람 Email, 제목, 내용 설정

- 보내는 사람 Email 도 내 이메일로 설정하자

- 반드시 네이버 이메일이어야함

- 메일 서버의 이메일과 같아야함

 

3. 받는 사람 Email 주소 설정

- 메일을 받을 곳의 이메일 주소를 쓰면 된다

- 반드시 네이버 이메일이 아니어도 된다

 

HtmlEmail 객체 생성 및 세팅

- commons-email 라이브러리에서 제공하는 HtmlEmail 클래스를 통해 여러가지 설정을 한다

+ setSSL(true) 를 통해 보안 연결을 사용한다

 

HtmlEmail 클래스의 메소드

- addTo() 메소드는 "받는 사람"의 메일 주소 설정하는 메소드

- setFrom() 메소드는 "보내는 사람" 의 메일 주소, 이름, charSet 등을 설정

- setSubject() 메소드로 제목을 세팅하고, setHtmlMsg() 메소드로 내용을 세팅, 여기에 변수 a ㄱ밧을 전달

 - send() 메소드로 이메일을 실제로 전송

- 메일을 전송한 후 전송 성공 메세지를 뿌리기 위해 Model 객체에 성공 메세지를 저장

 

- prefix 값이 /jsp/ 로 되어있으므로 webabb/jsp/ 안의 result.jsp 파일로 이동한다

- result.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
    
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>

결과 페이지: ${result}

</body>
</html>

- 메일을 보내보자

- 네이버 메일에 가서 확인

 

보낸 메일함 확인

 

받은 메일함 확인

- 현재는 보낸 메일과 받는 메일을 같게 설정했었다


회원 관리 프로그램

- 회원 가입시 이메일 보내기 기능을 활용해서 아이디 찾기, 비밀번호 찾기를 구현한 회원 관리 프로그램을 보자

- 클라우드의 springmember 프로젝트를 import


파일들 살펴보기 : pom.xml

- pom.xml 부분

		<dependency>
			<groupId>commons-fileupload</groupId>
			<artifactId>commons-fileupload</artifactId>
			<version>1.2</version>
		</dependency>

- 프로필 사진 (첨부파일) 을 업로드하기 위해 fileupload 라이브러리가 추가되어 있다

- 이메일 관련 라이브러리는 javax.mail 과 commons-email 등이 있다, 현재는 commons-email 라이브러리만 사용

		<!-- javax.mail -->
		<dependency>
			<groupId>org.springframework</groupId>
			<artifactId>spring-context-support</artifactId>
			<version>4.1.3.RELEASE</version>
		</dependency>
		<dependency>
			<groupId>javax.mail</groupId>
			<artifactId>mail</artifactId>
			<version>1.4.7</version>
		</dependency>

		<!-- EMail -->
		<dependency>
			<groupId>org.apache.commons</groupId>
			<artifactId>commons-email</artifactId>
			<version>1.5</version>
		</dependency>

- 아래가 commons-email 라이브러리


파일들 살펴보기 : web.xml

- 들어가는 3가지 내용 중 DispatcherServlet 매핑 부분이 변경되었다

	<servlet-mapping>
		<servlet-name>appServlet</servlet-name>
		<url-pattern>*.do</url-pattern>
	</servlet-mapping>

- *.do 로 요청해야함

- 들어가는 3가지 내용 중 한글 인코딩 설정 코드가 들어가 있다

	<filter>
		<filter-name>CharacterEncodingFilter</filter-name>
		<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
		<init-param>
			<param-name>encoding</param-name>
			<param-value>UTF-8</param-value>
		</init-param>
		<init-param>
			<param-name>forceEncoding</param-name>
			<param-value>true</param-value>
		</init-param>
	</filter>
	<filter-mapping>
		<filter-name>CharacterEncodingFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>

- 들어가는 3가지 내용 중 servlet-context.xml , root-context.xml 파일을 불러오고 있다


파일들 살펴보기 : servlet-context.xml

servlet-context.xml 부분 중 base-package

	<context:component-scan base-package="myspring" />

- base-package 를 "myspring" 으로 설정해뒀다, src/main/java 하위에 myspring 디렉토리가 있음

- 이 base-package 안의 클래스들은 4가지 어노테이션 중 하나가 붙어있어야한다

* 4가지 어노테이션 : @Component, @Controller, @Service, @Repository

 

servlet-context.xml 부분 중 ViewResolver

	<!-- ViewResolver -->
	<beans:bean id="internalResourceViewResolver"
		class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<beans:property name="prefix" value="jsp/" />
		<beans:property name="suffix" value=".jsp" />
	</beans:bean>

- View 페이지 저장 위치는 webapp 폴더가 기준이다

- 그러므로 prefix 의 value 에 "jsp/" 라고 썼다면 webapp 하위에 jsp 파일을 생성하고 그 안에 View 파일들이 있어야함

 

servlet-context.xml 부분 중 인터셉터 설정

	<!-- 인터셉터 설정 -->	
	<beans:bean id="sessionChk" class="myspring.controller.SessionCheckInter"/>	
	 <interceptors>
		<interceptor>
			<mapping path="/member_edit.do"/>
			<mapping path="/member_edit_ok.do"/>			
			<mapping path="/member_del.do"/>
			<mapping path="/member_del_ok.do"/>
			<mapping path="/member_logout.do"/>
			<beans:ref bean="sessionChk"/>
		</interceptor>
	</interceptors>

- 인터셉터 매핑을 잡고 있다, 로그인 해야만 쓸 수 있는 메뉴(요청) 들이다

ex) 수정, 삭제, 로그아웃관련 요청

- 로그인 해야만 사용할 수 있는 기능을 사용할때 인터셉터를 사용해서 Controller 로 가기 전에 인터셉터 클래스의 preHandler() 부분으로 감

- 인터셉터 클래스인 SessionCheckInter.java 를 보자

 

- SessionCheckInter.java

package myspring.controller;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.servlet.handler.HandlerInterceptorAdapter;

public class SessionCheckInter extends HandlerInterceptorAdapter {
	
	// preHandle(request,response,handler)메소드
	// 1.Controller에서 요청(*.do)을 받기 전에  preHandle()가 호출되어 가로채는 역할로 사용
	// 2.로그인 하지않고(세션이 없으면) 요청하면 로그인 폼으로 이동 하도록 해주는 역할
	public boolean preHandle(HttpServletRequest request, 
			HttpServletResponse response, Object handler) throws Exception {
		HttpSession session = request.getSession();
		String id = (String)session.getAttribute("id");
		if (id == null || id.equals(""))  {		
			response.sendRedirect("member_login.do");	// 세션이 없으면 로그인 폼으로 이동
			return false;
		}
		return true;
	}
}

- 여기서는 HandlerInterceptor 클래스를 상속받아서 인터셉터 클래스를 구현했다

- preHandle() 메소드를 오버라이딩 하고, 그 안에서 

- 인터셉터 관련 설멍 : https://laker99.tistory.com/145

 

인터셉터 구현 방법

1. HandlerInterceptorAdapter 클래스 상속

2. HandlerInterceptor 인터페이스 상속

 

servlet-context.xml 부분 중 fileupload 설정

	<!-- 파일 업로드  설정 -->
	<beans:bean id="multipartResolver" class="org.springframework.web.multipart.commons.CommonsMultipartResolver">
		<beans:property name="maxUploadSize" value="10000000"/>	
	</beans:bean>

- fileupload 설정 bean 객체를 생성하면서 Setter DI 를 수행함

- 이(1MB) 이상의 크기인 첨부파일 업로드시 오류 발생하며 멈춤, 즉 너무 큰 첨부파일을 첨부할 수 없게 함

- 이 설정을 하지 않아도 상관 없음, 이 설정만으로는 막을 수 없다

- Controller 클래스에서 특정 크기가 넘는 파일을 첨부하지 못하도록 처리 가능

+ Controller 클래스에서 "jpg", "gif", "png" 가 아닌 파일은 업로드 되지 않는 등의 설정도 가능


파일들 살펴보기 : root-context.xml

root-context.xml 부분 1

	<!-- Data Source -->
	<!-- 
	<bean id="dataSource" class="org.springframework.jdbc.datasource.SimpleDriverDataSource"> 
		<property name="driverClass" value="oracle.jdbc.driver.OracleDriver" /> 
		<property name="url" value="jdbc:oracle:thin:@localhost:1521:xe" /> 
		<property name="username" value="spring" /> 
		<property name="password" value="spring123" /> 
	</bean>
 	-->	
 	
 	<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
		<property name="driverClassName" value="oracle.jdbc.driver.OracleDriver" />
		<property name="url" value="jdbc:oracle:thin:@localhost:1521:xe" />
		<property name="username" value="spring" />
		<property name="password" value="spring123" />
	</bean>

- 여러가지 클래스들이 있는데 그 중 DriverManagerDataSource 클래스를 사용하고 있다

- 각 클래스마다 프로퍼티가 다르므로 주의

ex) DriverManagerDataSource 는 "driverClassName" 이 프로퍼티명, SimpleDriverDataSource 는 "driverClass" 가 프로퍼티명

 

root-context.xml 부분 2

	<!-- 스프링으로 oracle 디비 연결 -->
	<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="dataSource" ref="dataSource" />
		<property name="configLocation"	value="classpath:util/SqlMapConfig.xml" />
		<property name="mapperLocations" value="classpath:sql/*.xml" />
	</bean>

- 3줄은 순서대로 DB 연결하는 코드, MyBatis 환경설정 파일 Config 파일 불러오기, Mapper 파일 불러오기 코드이다

- 두 환경설정 파일은 resources 폴더 안에 있으므로 classpath: 를 붙여줌

- Setter DI 로 주입해서 sqlSEssionFactoryBean 객체를 생성하고 있다

 

root-context.xml 부분 3

	<bean id="sqlSession" class="org.mybatis.spring.SqlSessionTemplate">
		<constructor-arg index="0" ref="sqlSessionFactory" />
	</bean>

- 이 sqlSessionTemplate 객체에 sqlSessionFactoryBean 객체인 sqlSessioFactory 가 Constructor DI 로 주입되었다, 주입되면서 sqlSessionTemplate 객체 sqlSession 이 생성됨

- 주입이 되었으므로 Config 파일, Mapper 파일을 DAO 에서 불러오는게 가능해짐

 

+ SqlMapConfig.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"  "http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>

	<typeAliases>
		<typeAlias alias="member" type="myspring.model.MemberBean" />		
	</typeAliases>

</configuration>

- 현재 스프링에서 DB 연동하므로 DTO alis 값 설정하는 역할만 하고 있다



DB 에 테이블 생성

- join_member.sql

--회원관리
select * from tab;
select * from seq;
select * from join_member;

drop table join_member purge;

create table join_member(
  	join_code number(38) unique not null 
  	/*unique 제약 조건은 중복값을 허용하지 않고 null은 허용*/  	
  , join_id varchar2(20) primary key /*회원아이디*/
  , join_pwd varchar2(20) not null /*회원비번*/
  , join_name varchar2(50) not null /*회원이름*/
  , join_zip1 varchar2(5) not null /*첫번째 우편번호 */
  , join_zip2 varchar2(5)           /*두번째 우편번호 */
  , join_addr1 varchar2(100) not null /*주소*/
  , join_addr2 varchar2(100) not null /*나머지 주소 */
  , join_tel varchar2(20) not null /*전번*/
  , join_phone varchar2(20) not null /*폰번호 */
  , join_email varchar2(100) not null /*전자우편 주소*/
  , join_profile varchar2(100)  /*이진파일명*/
  , join_regdate date /*가입 날짜*/
  , join_state number(10) /*가입회원 1, 탈퇴회원 2 */
  , join_delcont varchar2(4000) /*탈퇴 사유 */
  , join_deldate date /*탈퇴 날짜 */
);

/***** join_member 테이블의 join_code 시퀀스 생성 *****/
create sequence join_member_joincode_seq 
increment by 1 start with 1 nocache;

--drop sequence join_member_joincode_seq; 

insert into join_member (join_code,join_id,join_pwd,join_name,join_zip1,
join_zip2,join_addr1,join_addr2,join_tel,join_phone,join_email,join_regdate,
join_state)
values(join_member_joincode_seq.nextval,'aaaaa',
'77777','홍길동','745','850','서울시 마포구 대흥동','중앙정보 처리학원',
'02-7777-7777','010-9999-9999','hong@naver.com',sysdate,1);

select * from join_member;

--delete from join_member where join_code=21;

--update join_member set join_tel='032-999-9999' where join_id='bbbbb';

 

join_member 테이블 생성 부분만 (join_member.sql 부분)

create table join_member(
  	join_code number(38) unique not null 
  	/*unique 제약 조건은 중복값을 허용하지 않고 null은 허용*/  	
  , join_id varchar2(20) primary key /*회원아이디*/
  , join_pwd varchar2(20) not null /*회원비번*/
  , join_name varchar2(50) not null /*회원이름*/
  , join_zip1 varchar2(5) not null /*첫번째 우편번호 */
  , join_zip2 varchar2(5)           /*두번째 우편번호 */
  , join_addr1 varchar2(100) not null /*주소*/
  , join_addr2 varchar2(100) not null /*나머지 주소 */
  , join_tel varchar2(20) not null /*전번*/
  , join_phone varchar2(20) not null /*폰번호 */
  , join_email varchar2(100) not null /*전자우편 주소*/
  , join_profile varchar2(100)  /*이진파일명*/
  , join_regdate date /*가입 날짜*/
  , join_state number(10) /*가입회원 1, 탈퇴회원 2 */
  , join_delcont varchar2(4000) /*탈퇴 사유 */
  , join_deldate date /*탈퇴 날짜 */
);

1. join_code : 회원 번호가 저장되는 컬럼, unique 이자 not null 이므로 primary key 나 마찬가지

- sequence 로 넣는다

2. join_id : 회원 id 값이 저장되는 컬럼, primary key

3. join_tel : 1개의 컬럼에 전화번호 앞자리, 중간자리, 끝자리를 결합해서 저장해야한다

4. join_profile : 프로필 이미지 첨부파일명을 저장할 컬럼

5. join_state : 가입한 회원은 1 을 저장, 탈퇴를 하면 delete 시키는 대신 join_state 를 2 로 저장

- 회원 등록시에 insert 문 안에 1 을 넣어서 삽입, 회원 탈퇴시에 "update 문" 으로 join_state 만 2 로 수정

- 실무에선 회원정보를 함부로 삭제 하지 않는다, 회원 상태값만 변경함

- 이 부분을 실행시켜서 테이블 join_member 를 생성

 

join_member 시퀀스 생성 부분만 (join_member.sql 부분)

/***** join_member 테이블의 join_code 시퀀스 생성 *****/
create sequence join_member_joincode_seq 
increment by 1 start with 1 nocache;

- 이 부분을 실행시켜서 시퀀스 join_member_joincode_seq 를 생성

 

join_member 더미 데이터 삽입 부분만 (join_member.sql 부분)

insert into join_member (join_code,join_id,join_pwd,join_name,join_zip1,
join_zip2,join_addr1,join_addr2,join_tel,join_phone,join_email,join_regdate,
join_state)
values(join_member_joincode_seq.nextval,'aaaaa',
'77777','홍길동','745','850','서울시 마포구 대흥동','중앙정보 처리학원',
'02-7777-7777','010-9999-9999','hong@naver.com',sysdate,1);

- 강제로 더미 데이터들을 삽입

- 이 부분을 실행시켜서 더미 데이터 1개를 생성

 

 

기능 확인

- 이제 index 파일 실행시켜보자

- index.jsp 실행

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>

<%
	response.sendRedirect("member_login.do");
%>

 

- 아이디 "aaaaa", 비번 77777 로 로그인 가능

- 정보 수정해서 프로필 사진을 등록해보자

- 회원 가입 또는 수정시 첨부파일 확장자가 이미지 확장자가 아니거나 100KB 가 넘는 경우 프로필 사진 등록 불가, 즉 회원가입 및 수정 불가

- 100KB 넘지 않는 작은 사진만 업로드 가능

 

- 100KB 가 넘는 큰 사진 업로드시 캡처

- 회원가입을 시켜보자

- 비밀번호 찾기를 할때 이메일 주소로 비밀번호를 알려줘야 하기 때문에 회원가입시 이메일 주소는 실제 이메일을 받을 수 있는 이메일 주소로 등록해야한다

 

- 콘솔창을 확인해서 path: 뒤에 적힌 경로를 확인

Path=C:\Users\admin\Documents\workspace-sts-3.9.11.RELEASE\.metadata\.plugins\org.eclipse.wst.server.core\tmp0\wtpwebapps\springmember\upload

 

- 파일 탐색기를 통해 들어가면 실제 업로드가 되어있음을 확인 가능하고, "난수값 형태"로 파일명이 저장되어 있다

- 중복 파일명 문제를 직접 해결하는 방법으로서, 문자형태의 난수값을 발생시켜서 중복문제를 해결했다

 

- 가입한 계정으로 로그인을 하자

 

- 비번찾기 기능을 하기전에 Controller 클래스인 MemberAction.java 에서 Mail Server, 보내는 사람 Email 설정을 내 이메일로 바꿔줘야한다

- 받는 쪽 주소 설정은 회원가입시 등록했던 이메일 주소로 설정된다

 

- 설정하면 다시 실행해서 비번 찾기 기능을 해보자

 

- 회원가입시 등록했던 이메일 주소의 받은 메일함에서 확인

 

- SQL Developer 에서 spring 계정 연결 후 select 문으로 들어온 DB에 저장된 데이터(회원)를 확인해보자

 

 


코드 확인

- 기능을 봤으니 코드가 어떻게 되어있는지 확인

- 로그인 기능은 생략

- 회원가입 기능부터 보자

 

- 로그인 페이지에서 "회원가입" 클릭시 "member_join.do" 로 요청한다

    <input type="button" value="회원가입" class="input_button"
    		onclick="location='member_join.do'" />

- 이후 Controller -> member_join.jsp 로 온다

 

	/* 회원가입 폼 */
	@RequestMapping(value = "/member_join.do")
	public String member_join() {
		return "member/member_join";
		// member 폴더의 member_join.jsp 뷰 페이지 실행
	}

- member_join.jsp

<%@ page language="java" 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">
<title>회원가입폼</title>
<link rel="stylesheet" type="text/css" href="./css/admin.css" />
<link rel="stylesheet" type="text/css" href="<%=request.getContextPath()%>/css/member.css" />
<!-- <script src="/springmember/js/jquery.js"></script> -->
<script src="http://code.jquery.com/jquery-latest.js"></script>
<script src="<%=request.getContextPath()%>/js/member.js"></script>
<script src="http://dmaps.daum.net/map_js_init/postcode.v2.js"></script>
<script>
//우편번호, 주소 Daum API
function openDaumPostcode() {
	new daum.Postcode({
		oncomplete : function(data) {				
			// 팝업에서 검색결과 항목을 클릭했을때 실행할 코드를 작성하는 부분.
			// 우편번호와 주소 정보를 해당 필드에 넣고, 커서를 상세주소 필드로 이동한다.
			document.getElementById('join_zip1').value = data.zonecode;
			document.getElementById('join_addr1').value = data.address;				
		}
	}).open();
}
</script>

</head>
<body>
 <div id="join_wrap">
  <h2 class="join_title">회원가입</h2>
  <form name="f" method="post" action="member_join_ok.do"
  		onsubmit="return check()" enctype="multipart/form-data">
   <!-- 이진파일을 업로드 할려면 enctype 속성을 지정 -->
   <table id="join_t">
    <tr>
     <th>회원아이디</th>
     <td>
      <input name="join_id" id="join_id" size="14" class="input_box" />
      <input type="button" value="아이디 중복체크" class="input_button"
      	onclick="id_check()" />
      <div id="idcheck"></div>
     </td>
    </tr>
    
    <tr>
     <th>회원비번</th>
     <td>
      <input type="password" name="join_pwd" id="join_pwd1" size="14"
      class="input_box" />
     </td>
    </tr>
    
    <tr>
     <th>회원비번확인</th>
     <td>
      <input type="password" name="join_pwd2" id="join_pwd2" size="14"
      class="input_box" />
     </td>
    </tr>
    
    <tr>
     <th>회원이름</th>
     <td>
      <input name="join_name" id="join_name" size="14" class="input_box" />
     </td>
    </tr>
    
    <tr>
     <th>우편번호</th>
     <td>
      <input name="join_zip1" id="join_zip1" size="5" class="input_box"
      		readonly onclick="post_search()" />
      <!-- -<input name="join_zip2" id="join_zip2" size="3" class="input_box" readonly 
      		onclick="post_search()"/> -->
      <input type="button" value="우편번호검색" class="input_button"
      		onclick="openDaumPostcode()" />
     </td>
    </tr>
    
    <tr>
     <th>주소</th>
     <td>
      <input name="join_addr1" id="join_addr1" size="50" class="input_box"
      readonly onclick="post_search()" />
     </td>
    </tr>
    
    <tr>
     <th>나머지 주소</th>
     <td>
      <input name="join_addr2" id="join_addr2" size="37" class="input_box" />
     </td>
    </tr>
    
    <tr>
     <th>집전화번호</th>
     <td>
     <%@ include file="../../jsp/include/tel_number.jsp"%>    
      <select name="join_tel1" >      
      	<c:forEach var="t" items="${tel}" begin="0" end="16">
      		<option value="${t}">${t}</option>
      	</c:forEach>        
      </select>-<input name="join_tel2" id="join_tel2" size="4"
      maxlength="4" class="input_box" />-<input  name="join_tel3"
      id="join_tel3" size="4" maxlength="4" class="input_box" />
     </td>
    </tr>
    
    <tr>
     <th>휴대전화번호</th>
     <td>
     <%@ include file="../../jsp/include/phone_number.jsp" %>
     <select name="join_phone1">
      <c:forEach var="p" items="${phone}" begin="0" end="5">
       <option value="${p}">${p}</option>
      </c:forEach>
     </select>-<input name="join_phone2" id="join_phone2" size="4"
     maxlength="4" class="input_box" />-<input name="join_phone3"
     id="join_phone3" size="4" maxlength="4" class="input_box" />
     </td>
    </tr>
    
    <tr>
     <th>전자우편</th>
     <td>
      <input name="join_mailid" id="join_mailid" size="10" 
      class="input_box" />@<input name="join_maildomain" 
      id="join_maildomain" size="20" class="input_box" readonly />
      <!--readonly는 단지 쓰기,수정이 불가능하고 읽기만 가능하다 //-->
      <select name="mail_list" onchange="domain_list()">
      <option value="">=이메일선택=</option>
      <option value="daum.net">daum.net</option>
      <option value="nate.com">nate.com</option>
      <option value="naver.com">naver.com</option>
      <option value="hotmail.com">hotmail.com</option>
      <option value="gmail.com">gmail.com</option>
      <option value="0">직접입력</option>
     </select> 
     </td>
    </tr>
    
    <tr>
     <th>프로필사진</th>
     <td>
      <input type="file" name="join_profile1" />
     </td>
    </tr>
   </table>
   
   <div id="join_menu">
    <input type="submit" value="회원가입" class="input_button" />
    <input type="reset" value="가입취소" class="input_button" 
    onclick="$('#join_id').focus();" />
   </div>
  </form>
 </div>
</body>
</html>

- 첨부파일을 전송해야하므로 form 태그에 enctype="multipart/form-data" 속성을 반드시 추가해야한다

 

전화번호 앞자리 출력

1. 별도의 파일 tel_number.jsp 파일 안의 String 배열 tel 에 전화번호 앞자리들을 저장한다

2. 그 배열 tel 을 request 객체로 공유설정

- import/tel_numeber.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%
   String[] tel={"02","031","032","033","041","042","043","044","051","052","053","054","055","061","062","063","064"};

//for(int i=0; i<tel.length;i++){
	 // out.println(tel[i]+"<hr/>");
//}

   request.setAttribute("tel",tel);
%>

 

3. member_join.jsp 파일에서 forEach 태그의 items 에 그 배열 tel 의 요소를 출력

      	<c:forEach var="t" items="${tel}" begin="0" end="16">
      		<option value="${t}">${t}</option>
      	</c:forEach>

- 회원가입 폼에 입력 후 "회원가입" 을 클릭시 "member_join_ok.do" 로 요청한다

 

- Controller 클래스 MemberAction.java 에서 "member_join_ok.do" 요청 부분 일부

- "member_join_ok.do" 처리 부분이 기므로 하나씩 잘라서 설명

- 여기서 넘어간 첨부파일은 @RequestParam 어노테이션을 통해서 MultipartFile 객체 nf 로 받는다

- 나머지 정보들은 @ModelAttribute (생략) 어노테이션을 통해 DTO MemberBean 객체 member 로 받는다

- 그 객체 nf 로 파일의 원래 이름과 크기를 구한다

- 이후 request 객체로 폴더 upload 의 실제 경로를 구함

난수 발생

- UUID 를 통해 난수를 발생시킨다 * 아래 설명

- 난수화된 파일명은 newfilename 에 저장되게 된다

파일 크기 제한 & 파일 확장자 제한

- 파일 크기가 100KB 초과시 result 에 1 을 저장해서 실패 처리를 uploadResult.jsp 에서 할 수 있게끔 함

- 파싱으로 원래 있던 파일명을 . 으로 파싱해서 파일명은 file[0]에, 확장자는 file[1] 에 저장한다

- 그 확장자가 "jpg", "gif", "png" 가 아닌 경우 업로드 불가, 이때 2 를 저장해서 실패 처리를 uploadResult.jsp 에서 할 수 있게끔 함

 

업로드 시키기 (MemberAction.java 에서 "member_join_ok.do" 요청 부분 일부)

		if (size > 0) { // 첨부파일이 전송된 경우

			mf.transferTo(new File(path + "/" + newfilename));

		}

- 실제 업로드 폴더에 업로드를 시켜주는 코드이다

- File 객체를 생성하고, 실제 업로드 폴더 경로인 path 에 newfilename 을 붙여서 업로드 시킴

 

전화번호, 이메일 등 결합

- 컬럼이 하나이므로 결합해서 저장

 

DB에 회원 등록(삽입)

		memberService.insertMember(member);

 

Mapper 파일에서 회원 등록 SQL문

    <!-- 회원저장 -->
    <insert id="member_join" parameterType="member">
     insert into join_member (join_code,join_id,join_pwd,join_name,
     join_zip1,join_addr1,join_addr2,join_tel,join_phone,join_email,join_profile,
     join_regdate,join_state) values(join_member_joincode_seq.nextval,
     #{join_id},#{join_pwd},#{join_name},
     #{join_zip1},#{join_addr1},#{join_addr2},#{join_tel},
     #{join_phone},#{join_email},#{join_profile, jdbcType=VARCHAR},sysdate,1)
    </insert>

- MyBatis 는 null 값을 허용하지 않으므로 첨부파일명을 저장하는 컬럼에는 null 값을 허용해주는 코드인 jdbcType=VARCHAR 를 추가해줘야한다

+ 첨부파일은 등록할 수도 있고 등록하지 않을수도 있기때문에

- 마지막 컬럼은 회원의 상태값을 저장하는 컬럼, 가입시이므로 1을 저장

 

return 문

- Controller 클래스에서 return 문에서 새로운 요청을 할때는 redirect: 를 붙여야한다

- 지금까지는 View 페이지로 바로 갔지만, 이번엔 회원가입 성공 후 회원가입 성공 메세지를 뿌리지 않고 바로 로그인 폼으로 가도록 요청, 이땐 redirect: 를 붙여서 새로운 요청을 해야한다


- 난수를 발생시키는 방법을 정리한 예제 RandomFile.java 를 보자

import java.util.UUID;

public class RandomFile {

	public static void main(String[] args) {
		// TODO Auto-generated method stub

		String filename = "clock.jpg";
		String extension = filename.substring(filename.lastIndexOf("."), filename.length());
		System.out.println("extension:"+extension);
		
		UUID uuid = UUID.randomUUID();
		System.out.println("uuid:"+uuid);
		
		String newfilename = uuid.toString() + extension;
		System.out.println("newfilename:"+newfilename);
		
	}

}

1. clock.jpg 라는 첨부파일을 저장한다고 하면,  확장자를 subsgring() 을 사용해서 분리한 후 extension 에 저장

2. UUID 클래스로 난수를 발생시킨다

3. 그 난수를 String 형으로 변환시킨 후 확장자를 붙이면 파일명을 문자형태 난수로 만들 수 있다

- 난수가 중복될 확률은 현저히 낮다


 

복습

Setter DI

- Setter 메소드가 만들어져 있어야함

ex) Contoller 에 Service 클래스를 주입할때 Service 클래스 안에 DI 가 있어야함

Constructor DI

- 생성자가 만들어져 있어야함

어노테이션 기반 DI

- 3가지 조건을 만족해야함

- 생성자나 Setter 메소드가 없어도 가능함

 

servlet-xml

ViewResolver

- View 파일들이 저장될 최상위 디렉토리 지정

- servlet-context.xml 에서 설정

 

 base-package

- 자바 파일들이 저장될 최상위 디렉토리 지정

- servlet-context.xml 에서 설정

 

root-context.xml

- 직접 bean 을 만들어서 Setter DI, Constructor DI 로 DB 연동 관련 내용 처리

- Controller, Service 와는 달리, DAO 에서 SqlSession 객체를 주입하기 위해서는 root-context.xml 에 bean 이 만들어져 있어야한다

 

+ 프로젝트 실행방법

- index 파일이 만들어져 있을때 프로젝트 Run As -> Run on Server 로 실행 가능

- index 파일 없을땐 다른 방법으로 실행해야한다


프로젝트 myBatis2 : 사원 등록 (이어서)

- 사원 등록폼 및 등록 기능은 모두 구현했고 이제 실제로 50번 부서에 사원을 등록해보자

- 이제 개발부는 참조하는 사원(자식) 이 있으므로 삭제되지 않는다


프로젝트 myBatis2 : 사원 상세 페이지

- 해당 부서의 직원 목록을 출력하는 부서 상세 페이지 empList.jsp 에서 사원의 이름을 클릭하면 사원 상세 페이지로 이동

 

<td><a href="empView.do?empno=${emp.empno}"
		class="btn btn-info">${emp.ename}</a></td>

- 사원명을 클릭하면 "empView.do" 로 요청한다, 요청하면서 사원 번호인 empno 를 GET 방식으로 전달함

 

Controller 클래스 EmpController.java 에서 "empView.do" 요청 부분만

	// 사원 상세페이지
	@RequestMapping("empView.do")
	public String empView(int empno, Model model) {
		Emp emp = es.select(empno); // 사원 상세 정보 구하기
		model.addAttribute("emp", emp);
		return "emp/empView";
	}

- 앞에서 넘어온 값인 사원번호를 @RequestParam (생략) 으로 바로 변수 empno 로 값을 받는다

- 자동으로 int 형으로 형변환되어 저장됨

- 1명에 대한 상세정보를 DB에서 구해와서 View 에 뿌려야 하므로 Model 객체를 매개변수에 선언해서 받음

<돌아온 후>

- 사원의 상세정보는 select() 가 리턴되면서 Emp DTO 객체에 저장되어 돌아온다, 그걸 Emp DTO 객체 emp 에 저장함

- 그 객체 emp 를 Model 객체에 저장하고 /WEB-INF/views/emp/empView.jsp 파일로 이동

- View 페이지로 이동할때 prefix, suffix 를 빼고 작성하고, 하위의 패키지는 적어줘야하므로 "emp/empView" 로 작성

 

Service 클래스 EmpServiceImpl.java 에서 select() 메소드 부분만

	public Emp select(int empno) {
		return ed.select(empno);
	}

- DB 연동을 해야하므로 Service 로 넘어왔다

 

DAO 클래스 EmpDaoImpl.java 에서 select() 메소드 부분만

	public Emp select(int empno) {
		return sst.selectOne("empns.select", empno);
	}

- root-context.xml 에서 SqlSession bean 객체를 생성했고, 그걸 DAO 클래스에 주입했으므로 selectOne() 사용 가능

- 전달된 사원번호 empno 를 selectOne() 을 호추랗며 그대로 전달함

- 사원 1명에 대한 상세정보를 구해야하므로 selectOne() 을 사용

 

Mapper 파일 Emp.xml 에서 id 가 "select" 인 태그 부분만

	<select id="select" parameterType="int" resultType="emp">
		select * from emp where empno=#{empno}
	</select>

- 전달받은 사원 번호로 해당 사원의 상세정보(모든 정보) 를 검색함

- 전달받은 값은 사원번호이므로 전달받은 자료형 parameterType 은 "int"

- 돌려주는 자료형 resultType 은 Emp DTO alias 인 "emp"

 

- 다시 DAO -> Service -> Controller 로 갔다가 View 로 오게 된다

 

View 페이지 empView.jsp

- emp/empView.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ include file="../header.jsp"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<script type="text/javascript">
	$(function() {
		$('#list').load('empList.do?deptno=${emp.deptno}');
	});
</script>
</head>
<body>
	<div class="container">
		<h2 class="text-primary">직원 상세정보</h2>
		<table class="table table-bordered">
			<tr>
				<td>사번</td>
				<td>${emp.empno }</td>
			</tr>
			<tr>
				<td>이름</td>
				<td>${emp.ename}</td>
			</tr>
			<tr>
				<td>업무</td>
				<td>${emp.job }</td>
			</tr>
			<tr>
				<td>관리자</td>
				<td>${emp.mgr }</td>
			</tr>
			<tr>
				<td>입사일</td>
				<td>${emp.hiredate }</td>
			</tr>
			<tr>
				<td>급여</td>
				<td>${emp.sal }</td>
			</tr>
			<tr>
				<td>보너스</td>
				<td>${emp.comm }</td>
			</tr>
			<tr>
				<td>부서코드</td>
				<td>${emp.deptno }</td>
			</tr>
		</table>
		<a href="empUpdateForm.do?empno=${emp.empno}" class="btn btn-info">수정</a>
		<a class="btn btn-danger" href="empDelete.do?empno=${emp.empno}">삭제</a>
		<a href="empList.do?deptno=${emp.deptno}" class="btn btn-default">목록</a>
		<div id="list"></div>
	</div>
</body>
</html>

- Emp DTO 객체 emp 가 Model 객체에 저장되어 넘어왔으므로 ${emp.필드명} 으로 값을 가져와서 출력한다

ex) 사원번호를 출력할때 ${emp.empno}

- header.jsp 안에 공통적인 내용들이 들어있다 (core 라이브러리, Bootstrap 등)

 

버튼 기능 (empView.jsp 부분)

		<a href="empUpdateForm.do?empno=${emp.empno}" class="btn btn-info">수정</a>
		<a class="btn btn-danger" href="empDelete.do?empno=${emp.empno}">삭제</a>
		<a href="empList.do?deptno=${emp.deptno}" class="btn btn-default">목록</a>

- 사원 상세 페이지에도 '수정', '삭제', '목록' 버튼이 있다

- '목록' 을 누르면 해당 부서 상세 페이지로 간다, 해당 부서의 부서번호인 ${emp.deptno} 전달

- '수정' 을 누르면 사원 정보 수정 페이지로 간다

- '삭제' 를 누르면 사원 삭제 페이지로 간다

 


프로젝트 myBatis2 : 사원 정보 수정폼

- 사원 상세 페이지 empView.jsp 에서 '수정' 버튼을 누르면 사원 정보 수정폼으로 간다

		<a href="empUpdateForm.do?empno=${emp.empno}" class="btn btn-info">수정</a>

- '수정' 을 누르면 "empUpdateForm.do" 료 요청한다, 요청하면서 사원 번호인 empno 를 전달

 

Controller 클래스 EmpController.java 에서 "empUpdateFomr.do" 요청 부분만

	// 사원 수정폼
	@RequestMapping("empUpdateForm.do")
	public String empUpdateForm(int empno, Model model) {
		Emp emp = es.select(empno);
		List<Dept> deptList = ds.list();
		model.addAttribute("emp", emp);
		model.addAttribute("deptList", deptList);
		return "emp/empUpdateForm";
	}

- 앞에서 넘어온 사원번호 empno 를 @RequestParam("empno") (생략) 을 통해 바로 매개변수의 empno 에 저장한다

사원 수정폼에서 할 DB 연동 작업 2가지

1. 사원 수정폼에 뿌려줄 사원 상세 정보 구하기

- Emp Service 객체 es 로 Emp Service 클래스의 select() 메소드를 호출하고, 사원 번호 empno 를 전달

2. 사원 수정폼에서 부서를 수정할때, 부서 목록을 가져와야한다 

- Dept Service 객체 ds 로 Dept Service 클래스의 list() 메소드를 호출함, 돌아올땐 부서 목록을 List 형태로 돌려줌

 

1. 사원 수정폼에 뿌려줄 사원 상세 정보 구하기

- 이전에 사원 상세 정보를 구할때 했던 내용과 비슷하므로 설명 생략 https://laker99.tistory.com/147

2. 사원 수정폼에서 부서를 수정할때, 부서 목록을 가져와야한다 

- 이전에 사원 등록폼에서 했던 내용과 비슷하므로 설명 생략 https://laker99.tistory.com/147

 

<돌아올때>

- 해당 사원의 상세 정보를 저장한 DTO 객체 emp 와 부서 전체 목록인 deptList 를 Model 에 저장해서 "empUpdateForm.jsp" 로 이동

 

View 페이지인 empUpdateForm.jsp

- emp/empUpdateForm.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ include file="../header.jsp"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<div class="container" align="center">
		<h2 class="text-primary">직원정보 수정</h2>
		<form action="empUpdate.do" method="post">
			<table class="table table-bordered">
				<tr>
					<td>사번</td>
					<td><input type="text" name="empno" readonly="readonly"
						value="${emp.empno}"></td>
				</tr>
				<tr>
					<td>이름</td>
					<td><input type="text" name="ename" required="required"
						value="${emp.ename }"></td>
				</tr>
				<tr>
					<td>업무</td>
					<td><input type="text" name="job" required="required"
						value="${emp.job }"></td>
				</tr>
				<tr>
					<td>급여</td>
					<td><input type="text" name="sal" required="required"
						value="${emp.sal}"></td>
				</tr>
				<tr>
					<td>보너스</td>
					<td><input type="text" name="comm" required="required"
						value="${emp.comm }"></td>
				</tr>
				<tr>
					<td>부서코드</td>
					<td><select name="deptno">
							<c:forEach var="dept" items="${deptList}">
								<c:if test="${emp.deptno==dept.deptno}">
									<option value="${dept.deptno}" selected="selected">
										${dept.dname}(${dept.deptno})</option>
								</c:if>
								<c:if test="${emp.deptno!=dept.deptno}">
									<option value="${dept.deptno}">${dept.dname}(${dept.deptno})</option>
								</c:if>
							</c:forEach>
					</select></td>
				</tr>
				<tr>
					<td colspan="2"><input type="submit" value="수정"></td>
				</tr>
			</table>
		</form>
	</div>
</body>
</html>

- emp.deptno (해당 사원의 부서) 와 dept.deptno (forEach에 의해 모든 부서의 부서번호가 차례대로 들어감) 가 같으면 selected 함으로서 해당 사원이 가입할때 등록했던 부서는 select-option 에서 선택되어 나타나도록 했다

+ jQuery 로 처리하면 간단하게 처리 가능

- 사원 번호을 readonly 속성으로 비활성화했다, readonly 이므로 값이 넘어간다!

- select-option 에 출력되는 내용은 부서명(부서번호) 형식이지만, 저장하는 값인 value 는 부서 번호가 된다

- 사원 수정폼에 수정할 정보를 입력하고 '수정' 을 누르면 action 인 "empUpdate.do" 로 요청한다


프로젝트 myBatis2 : 사원 정보 수정

Controller 클래스 EmpController.java 에서 "empUpdate.do" 요청 부분만

	// 사원 정보 수정
	@RequestMapping("empUpdate.do")
	public String empUpdate(Emp emp, Model model) {
		int result = es.update(emp);
		model.addAttribute("deptno", emp.getDeptno());
		model.addAttribute("result", result);
		return "emp/empUpdate";
	}

- 사원 수정폼에서 넘어온 값들을 @ModelAttribuate (생략) 을 사용해서 Setter 메소드들로 한번에 Emp DTO 객체인 emp 에 저장한다

- Emp Service 객체 es 를 사용해서 update() 메소드를 호출하고, 호출할때 수정할 정보를 저장한 객체 emp 를 전달

- 수정할 사원을 특정하는 사원 번호는 앞의 사원 수정폼에서 넘어왔으므로 emp 객체 안 emp.empno 에 저장되어있음

<돌아온 후>

- 수정 성공시 result 에는 1이 저장된다

- 수정이 끝난 후 부서 상세 페이지로 돌아가므로, 해당 사원이 소속된 부서의 부서번호가 필요하다, 그래서 emp.getDeptno() 를 "deptno" 네임으로 Model 객체에 저장

- View 페이지에서 수정 성공 / 실패 처리를 하기 위해 update() 에서 돌려받은 값 result 를 Model 객체에 저장

- /WEB-INF/views/emp/empUpdate.jsp 로 이동

 

Service 클래스 EmpServiceImpl.java 에서 update() 메소드 부분만

	public int update(Emp emp) {
		return ed.update(emp);
	}

 

DAO 클래스 EmpDaoImpl.java 에서 update() 메소드 부분만

	public int update(Emp emp) {
		return sst.update("empns.update", emp);
	}

- 수정할 정보를 담은 Emp DTO 객체 emp 를 받아서 그대로 전달

- 수정 성공시 자동으로 1을 리턴한다

 

Mapper 파일 Emp.xml 에서 id 가 "update" 태그 부분만

	<update id="update" parameterType="emp">
		update emp set ename=#{ename},job=#{job},sal=${sal},
			comm=#{comm},deptno=#{deptno} where empno=#{empno}
	</update>

- "emp" 는 MyBatis 환경설정 파일 Configuration.xml 에서 설정한 Emp DTO 의 alias 이다

- 객체 emp 가 넘어왔으므로 #{ename} 은 emp.getEname() 과 같은 의미

- 사원번호는 객체 emp 의 emp.empno 에 저장되어 있으므로 #{empno} 로 가져와서 where 절에 넣는다

 

View 페이지 empUpdate.jsp

- emp/empUpdate.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ include file="../header.jsp"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<c:if test="${result > 0 }">
		<script type="text/javascript">
			alert("수정 성공");
			location.href = "empList.do?deptno=${deptno}";
		</script>
	</c:if>
	<c:if test="${result <= 0 }">
		<script type="text/javascript">
			alert("수정 실패");
			history.go(-1);
		</script>
	</c:if>
</body>
</html>

- Model 객체에 저장된 "result" 를 통해 성공 / 실패 처리를한다

- 수정이 성공한 후에 "empList.do" 로 요청하고, Model 객체에 저장된 "deptno" 를 전달하면서 해당 부서 상세 페이지로 이동한다

 


프로젝트 myBatis2 : 사원 정보 삭제

- 사원 상세 페이지 empView.jsp 에서 '삭제' 버튼을 누르면 사원 삭제를 할 수 있다

- 삭제 폼은 없고 바로 삭제 된다

		<a class="btn btn-danger" href="empDelete.do?empno=${emp.empno}">삭제</a>

- '삭제' 를 누르면 "empDelete.do" 료 요청한다, 요청하면서 사원 번호인 empno 를 전달

 

Controller 클래스 EmpController.java 에서 "empDelete.do" 요청 부분만

	// 사원 삭제
	@RequestMapping("empDelete.do")
	public String empDelete(int empno, Model model) {
		Emp emp = es.select(empno); // 사원 상세 정보 구하기
		int result = es.delete(empno);
		model.addAttribute("result", result);
		model.addAttribute("deptno", emp.getDeptno());
		return "emp/empDelete";
	}

- 앞에서 넘어온 사원번호 empno 를 @RequestParam("empno") (생략) 을 통해 바로 매개변수의 empno 에 저장한다
사원 삭제할때 DB 연동 작업 2가지

1. 사원 상세 정보 구하기

- 사원을 삭제 한 후 그 사원이 속해있었던 부서의 부서 상세 페이지로 이동할 것이다, 그러려면 부서 번호가 필요함

- 부서 상세 페이지로 가기 위해, 삭제될 사원의 상세 정보를 구해서 거기서 부서 번호를 가져올 것
- 사원의 상세 정보를 구하기 위해 select() 메소드를 호출하며, 사원번호를 전달

2. 사원 삭제 하기

- 사원 삭제를 위해 delete() 메소드를 호출하며, 사원번호를 전달

<돌아온 후>

- 삭제된 사원 상세정보를 받은 emp 에서 emp.getDeptno() 로 사원이 소속되어있던 부서번호를 가져와서 Model 객체에 저장하고 empDelete.jsp 로 이동

 

1. 사원 상세 정보 구하기

- 이전에 여러번 했으므로 설명 생략

2. 사원 삭제하기

Service 클래스 EmpServiceImpl.java 에서 delete() 메소드 부분만

	public int delete(int empno) {
		return ed.delete(empno);
	}

 

DAO 클래스 EmpDaoImpl.java 에서 delete() 메소드 부분만

	public int delete(int empno) {
		return sst.delete("empns.delete", empno);
	}

 

 

Mapper 파일 Emp.xml 에서 id 가 "delete" 인 태그 부분만

	<delete id="delete" parameterType="int">
		delete from emp where empno=#{empno}
	</delete>

 

View 페이지 empDelete.jsp

- emp/empDelete.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ include file="../header.jsp"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<c:if test="${result > 0 }">
		<script type="text/javascript">
			alert("삭제 성공 ");
			location.href = "empList.do?deptno=${deptno}";
		</script>
	</c:if>
	<c:if test="${result <= 0 }">
		<script type="text/javascript">
			alert("삭제 실패");
			history.go(-1);
		</script>
	</c:if>
</body>
</html>

- 삭제 성공시 "empList.do" 로 요청하면서 삭제된 사원의 소속 부서인 deptno 를 넘겨주면서 해당 부서 상세 페이지로 이동

 

- 50번 부서에 등록되어있는 사원 LAY2 를 삭제해보자


STS 에 Data Source Management 추가

- STS 에는 이클립스와 다르게 Data Source Management 가 없으므로 DB 연결 불가능하다

- STS 에 plug-in 기능을 추가해야 Data Source Management 를 사용 가능

- 설치 이후 이제 필요한 SQL문을 STS 내에서 실행할 수 있다

 

STS 에 Data Source Management 추가하는 방법

- 이러면 STS 에 plug-in 을 설치한다, 원격 저장소에서 로컬 저장소로 다운받음

- 그 후 STS 를 Restart 시키면 plug-in 이 추가되어있다

- 이후 이제 필요한 SQL문을 STS 내에서 실행할 수 있다


게시판 직접 만들어보기

설계도

spring 실습 설계도

프로젝트 명
spring

계정생성
sqlplus system/oracle
create user spring identified by spring123;

권한 부여 (connect, resource 롤)
grant connect, resource to spring;

테이블명
myboard

create table myboard(
no number primary key,
writer varchar2(20),
passwd varchar2(20),
subject varchar2(50),
content varchar2(100),
readcount number, register date );

시퀀스명
myboard_seq

create sequence myboard_seq;

구조
- base-package 는 myspirng 으로 한다
- prefix 하위에 board 가 있다

환경 설정 파일 작성(세팅) 순서
1) pom.xml
2) web.xml
3) servlet-context.xml
4) configuration.xml
5) board.xml
6) root-context.xml
- 4), 5), 6) 은 DB 연동 관련 내용


오라클 계정 생성 및 권한 부여

- 기존 계정들은 복잡하므로 새 오라클 계정을 생성해서 프로젝트를 하자

 

Spring MVC 프로젝트 생성

- 도메인명 역순으로 지정해야한다

- com.myhome.spring 을 top-level 패키지로 지정

 

프토젝트 테스트

- 프로젝트 오른 마우스 -> Run As -> Run on Server

- 잘 실행되었음을 확인 가능

 

커넥션 생성, 테이블 생성하기

- webapp 폴더 하위에 sql 폴더 추가하고, 안에 SQL 파일 myboard.sql 생성

- 다음으로 커넥션을 연결해야함

- 커넥션이 생성되었다!

- Connection profile 도 설정한다

- 다음은 테이블 생성 위해 myboard.sql 작성

-- 게시판
select * from tab;
select * from seq;
select * from myboard;

-- 테이블명 : myboard
create table myboard(
	  no number primary key,
	  writer varchar2(20),
      passwd varchar2(20),
	  subject varchar2(50),
	  content varchar2(100),
	  readcount number,
	  register date );

-- 시퀀스명 : myboard_seq
create sequence myboard_seq;

- 테이블과 시퀀스 생성하기

- 테이블 및 시퀀스 생성 확인

 

환경설정 파일 세팅

+ 환경 설정 파일 작성(세팅) 순서
1) pom.xml
2) web.xml
3) servlet-context.xml
4) configuration.xml
5) board.xml
6) root-context.xml

 

pom.xml 파일 세팅

- Maven 의 환경설정 파일이다

- 잘 실행되는 내용을 가져와서 복붙하면 된다, 프로젝트 myBatis2 의 pom.xml 을 그대로 가져와서 복붙

- pom.xml 은 프로젝트 바로 하위에 있으므로 프로젝트를 선택하고 붙여넣기 (overwrite)

- 오라클, MyBatis, JSTL 라이브러리들이 모두 들어가있다

- 프로젝트 실행하기

- 환경설정 파일 만들면서 중간중간 실행해서 확인해야함

 

web.xml 파일 세팅

- url-pattern 을 / 에서 *.do 로 수정

	<servlet-mapping>
		<servlet-name>appServlet</servlet-name>
		<url-pattern>*.do</url-pattern>
	</servlet-mapping>

- 한글값 인코딩 처리 코드는 web.xml 생성시에 들어가있지 않는다, 직접 넣어줘야한다

- 프로젝트 myBatis2 에서 web.xml 에서 filter 와 filter-mapping 부분 가져오기

	<!-- 한글 입력 -->
	<filter>
		<filter-name>CharacterEncodingFilter</filter-name>
		<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
		<init-param>
			<param-name>encoding</param-name>
			<param-value>UTF-8</param-value>
		</init-param>
		<init-param>
			<param-name>forceEncoding</param-name>
			<param-value>true</param-value>
		</init-param>
	</filter>
	<filter-mapping>
		<filter-name>CharacterEncodingFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>

- 이 부분 넣어주기

 

요청 -> Dispatcher Servlet -> Controller 테스트

- HomeController.java 부분

- url-pattern 을 *.do 로 바꾸었으므로 샘플로 만들어진 HomeController.java 클래스에서 @RequestMapping 에 value 를 "/" 에서 "/test.do" 로 수정

- 요청 -> Dispatcher Servlet -> Controller 까지의 흐름을 확인하기 위해서이다

- 요청은 index.jsp 에 넣어줄 것

 

index 파일 생성

- 자동으로 실행되도록 하기 위해서 반드시 WEB-INF 폴더 하위에 index.jsp 를 생성해야함

- index.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<script>
	location.href = "test.do";
</script>
</body>
</html>

- "test.do" 로 요청해준다

- index.jsp 실행

- 이 화면이 나오면 테스트 완료

- 요청 -> Dispatcher Servlet -> Controller -> View (home.jsp) 로 갔기때문에 이 화면에 나타난다

 

servlet-context.xml 파일 세팅

- base-package 가 top-level 패키지로 되어있다, 이 패키지는 java 폴더 하위에 생성되어잇음

- 즉 base-pacakge 는 자바파일들이 저장될 최상위 패키지

- 우리는 이 top-level 패키지 사용하지 않을 것

 

- myspring 패키지를 java 폴더 하위에 생성하고 myspring 을 base-package 로 설정하기

	<context:component-scan base-package="myspring" />

 

- 최상위 디렉토리 myspring 을 생성했으므로 그 myspring 아래에 controller, service, dao, model 폴더 생성

 

- 다음으로 샘플로 만들어진 HomeContorller.java 를 myspring/controller 로 옮기기

- 그 후 com.myhome.spring 패키지는 삭제 (com 만 삭제하면 다 삭제된다)

- HomeContorller.java 의 패키지도 수정해주기

package myspring.controller;

 

- 환경파일 하나를 설정했으므로 다시 프로젝트 실행해서 실행확인

- servlet-context.xml 환경설정 파일 설정 완료

- 나머지 환경설정은 DB 연동 관련 내용이다, 나중에 할 것

 

Java 파일들 생성하기

- Controller, Service, DAO, DTO 파일들을 myspring 하위의 해당 폴더 안에 파일 생성

- 생성 된 Java 파일들에 어노테이션 추가 (@Controller, @Service, @Repository 등)

- 그리고 주입할 객체 위에 @Autowired 까지 추가

+ 어노테이션은 import 를 해야한다

* 위에서 프로퍼티명을 bd 대신 dao 로 수정했다

- 일단 @Autowired 는 현재 주석해두고 @Controller, @Service, @Repository 만 붙이기

- 여기까지 설정한 후 index 파일 실행해서 확인

 

- 이제 Controller 와 Service클래스에서의 @Autowired 어노테이션 주석을 하나씩 풀고 실행을 다시 해보면 실행이 잘 된다

- DAO 클래스는 아직 @Autowired 주입이 되지 않는다, root-context.xml 세팅을 먼저 해야 주입 가능

 

- BoardDao.java

- SqlSession 또는 SqlSession 구현 클래스로 객체를 주입해야한다

- 현재는 @Autowired 로 객체 주입이 불가능하다, @Autowired 를 쓰면 실행시 오류 발생

-  @Autowired 를 주석으로 막자

- root-context.xml 에서 SqlSessionTemplate bean 객체를 생성해야하고, Constructor DI 를 해서 bean 객체를 생성해야 여기서 @Autowired 사용해서 주입 가능

 

DTO 클래스 작성

- Board.java (DTO) 부분

- 테이블안의 컬럼과 같은 이름으로 프로퍼티들을 만들고 getter / setter 메소드를 생성한다

- 테이블의 컬럼명과 DTO 프로퍼티명이 일치가 되어야 매핑이 자동으로 된다

- number 타입은 int로, Date 타입은 Date(java.util)로, varchar2 타입은 String 으로 변환

- DTO 클래스 안에는 어노테이션을 붙이지 않음

 

View 파일 위치 설정

- servlet-context.xml 에서 /WEB-INF/views/ 가 prefix 로 설정되어있다, 이건 View 파일들이 저장될 최상위 디렉토리이다

	<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<beans:property name="prefix" value="/WEB-INF/views/" />
		<beans:property name="suffix" value=".jsp" />
	</beans:bean>

- 이 /WEB-INF/views/ 하위에 Viwe 파일들을 저장할 폴더 board 를 생성하자

 

폼 가져오기

- 게시판 글 작성 폼 boardform.jsp 를 클라우드에서 폴더 board 로 가져오자

- 가져오는 파일은 이것 하나뿐임, 나머지는 직접 생성해야함

 

+ 현재 상태에서 프로젝트 시작해서 실행 확인해보기, 실행 잘 된다

+ boardform.jsp 는 View 페이지이므로 결과가 여기서 출력되는 것이다,

- boardform.jsp 를 그냥 실행하면 실행되지 않음, 오류발생

 

- boardform.jsp 를 실행해보자

 

- index 파일을 수정해서 boardform.do 로 요청시키자

- Controller 클래스 BoardController.java 에 boardform.do 요청을 받는 코드를 작성해주자

+ HomeController.java 는 삭제하기

 

- BoardController.java 에서 "boardform.do" 요청 받는 부분만

	// 글 작성 폼
	@RequestMapping("boardform.do")
	public String boardform() {
		return "board/boardform";
	}

- 접근제어자는 public, 리턴자료형은 주로 String 으로 메소드 작성

- DB 연동은 아직 처리하지 않으므로 Controller 에서 View 로 가기

- prefix 하위 폴더인 board 를 경로 앞에 붙여야한다

 

- 이제 index 파일 실행시 글 작성 폼이 나타남

- boardform.jsp 를 보자

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>글작성</title>
</head>
<body>

<form method=post action="boardwrite.do">
<table border=1 width=400 align=center>
	<caption>글작성</caption>
	<tr><th>작성자명</th>
		<td><input type=text name="writer" required="required"></td>
	</tr>
	<tr><th>비밀번호</th>
		<td><input type=password name="passwd" required="required"></td>
	</tr>
	<tr><th>제목</th>
		<td><input type=text name="subject" required="required"></td>
	</tr>
	<tr><th>내용</th>
		<td><textarea cols=40 rows=5 name="content" required="required"></textarea></td>
	</tr>
	<tr><td colspan=2 align=center>
			<input type=submit value="글작성">
			<input type=reset value="취소">
		</td>
	</tr>
</table>
</form>

</body>
</html>

- 이 폼에서 넘어가는 내용은 writer, passwd, subject, content 이다

- 입력하고 "글작성" 클릭시 "boardwrite.do" 로 넘어간다, Controller -> Service -> DAO (Mapper 파일) 으로 가야한다

- 그러기 위해서 DB 연동을 먼저 처리해야함

 

configuration.xml 파일 생성 및 세팅

- 프로젝트 myBatis2 의 configuration.xml 파일 복사해서 현재 프로젝트인 spring 의 resources 폴더에 붙여넣기, 이후 수정

- configuration.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>

	<typeAliases>
		<typeAlias alias="board" type="myspring.model.Board" />
	</typeAliases>	
	
	<!-- 
	<mappers>
		<mapper resource="sql/Dept.xml" />
		<mapper resource="sql/Emp.xml" />
	</mappers>  -->	
	
</configuration>

- 가져온 파일을 현재 프로젝트에 맞게 수정, DTO의 alias 값을 설정해준다

- base-package 부터 시작해서 DTO 까지의 경로를 써준다, myspring.model.Board

- DTO 클래스에 대한 alias 내용 한가지만 들어간다

 

Board.xml (Mapper 파일) 생성 및 세팅

- 먼저 resources 폴더 하위에 sql 폴더를 생성

- myBatis2 의 Mapper 파일인 Dept.xml 이나 Emp.xml 을 현재 프로젝트의 resources/sql로 가져와서 이름을 Board.xml 으로 수정

- Board.xml

- 루트엘리먼트인 mapper 빼고 모두 지우기, namespace 는 "boardns" 로 지정

- 여기까지 하고 프로젝트 실행해서 실행 확인, 잘 된다

 

root-context.xml

- DB 연동 관련 내용이 들어간다

- DB 설정하는 3개의 파일(configuration.xml, board.xml(Mapper), root-context.xml) 중에서 가장 마지막에 root-context.xml 내용을 세팅해야한다

- 프로젝트 myBatis2의 root-context.xml 을 복사해서 같은 위치인 /WEB-INF/spring 폴더에 붙여넣기 (overwrite)

- root-context.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd">
	
	<!-- <context:property-placeholder location="classpath:jdbc.properties" /> -->
	
	<!-- dataSource -->
	<!-- <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
		destroy-method="close">
		<property name="driverClass" value="${jdbc.driverClassName}" />
		<property name="jdbcUrl" value="${jdbc.url}" />
		<property name="user" value="${jdbc.username}" />
		<property name="password" value="${jdbc.password}" />
		<property name="maxPoolSize" value="${jdbc.maxPoolSize}" />
	</bean> -->
	
	<!-- Data Source -->
	<bean id="dataSource"
		class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
		<property name="driverClass" value="oracle.jdbc.driver.OracleDriver" />
		<property name="url" value="jdbc:oracle:thin:@localhost:1521:xe" />
		<property name="username" value="spring" />
		<property name="password" value="spring123" />
	</bean>
	
	<!-- 스프링 jdbc 즉 스프링으로 oracle 디비 연결 -->
	<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="dataSource" ref="dataSource" />
		<property name="configLocation" value="classpath:configuration.xml" />
		<property name="mapperLocations" value="classpath:sql/*.xml" />
	</bean>
	
	<bean id="session" class="org.mybatis.spring.SqlSessionTemplate">
		<constructor-arg index="0" ref="sqlSessionFactory" />
	</bean>	
	
</beans>

- 첫번째 bean 에서 username, password 를 테이블을 만든 계정의 username, password 인 "spring", "spring123" 로 수정

- 두번째 bean 에서 Configuration 파일 위치를 설정하는 configLocation 을 설정해야하는데 위치가 맞으므로 그대로 두기

- 두번째 bean 에서 Mapper 파일 위치를 설정하는 mapperLocations 를 설정해야하는데 위치가 맞으므로 그대로 두기

- 세번째 bean 에서 SqlSessionTemplate bean 객체를 생성하고 있다, 위의 sqlSessionFactory 를 Construct DI 로 주입

 

- 그리고 DAO 인 BoardDao.java 로 가서 @Autowired 주석 풀기, 이제는 주석 풀어도 실행 된다

- index 파일 실행시 잘 실행된다

 

- 환경설정이 모두 끝났다, 모든 환경설정 파일 기본세팅을 완료했음

 

- 이제 흐름에 따라 파일들의 내용을 추가하자

- boardform.jsp 파일을 다시 열어보자

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>글작성</title>
</head>
<body>

<form method=post action="boardwrite.do">
<table border=1 width=400 align=center>
	<caption>글작성</caption>
	<tr><th>작성자명</th>
		<td><input type=text name="writer" required="required"></td>
	</tr>
	<tr><th>비밀번호</th>
		<td><input type=password name="passwd" required="required"></td>
	</tr>
	<tr><th>제목</th>
		<td><input type=text name="subject" required="required"></td>
	</tr>
	<tr><th>내용</th>
		<td><textarea cols=40 rows=5 name="content" required="required"></textarea></td>
	</tr>
	<tr><td colspan=2 align=center>
			<input type=submit value="글작성">
			<input type=reset value="취소">
		</td>
	</tr>
</table>
</form>

</body>
</html>

- DB 연동을 처리한 후 넘어왔다

- 이 폼에서 넘어가는 내용은 writer, passwd, subject, content 이다

- 입력하고 "글작성" 클릭시 "boardwrite.do" 로 넘어간다, Controller -> Service -> DAO (Mapper 파일) 으로 가야한다

 

- Controller 클래스 BoardController.java 에서 "boardwrite.do" 요청 부분을 처리하자

- 아래 내용을 작성

	// 글 작성
	@RequestMapping("boardwrite.do")
	public String boardwrite(Board board, Model model) {
		int result = bs.insert(board);
		model.addAttribute("result", result);
		return "board/insertresult";
	}

- 폼에서 넘어온 값을 DTO 객체로 한번에 받기 위해 @ModelAttribute 사용

- 메소드 명은 요청 이름값과 같게 맞췄다

- 글을 작성(insert) 한 후 성공 / 실패를 View 에서 처리하기위해 어떠한 값들을 가져갈 것이므로 Model 객체 선언해서 받기

 

- Service 객체 bs 를 사용해서 Service 클래스의 insert() 메소드를 호출, 이때 삽입할 데이터인 객체 board 를 가져감, 리턴자료형은 int 이다

 

- Service 클래스 BoardService.java 에서 insert() 메소드를 생성해야한다

	public int insert(Board board) {
		return dao.insert(board);
	}

- Controller 에서 이 메소드를 호출해야하므로 public, 리턴자료형은 int, 메소드 명은 insert(), 매개변수 자료형은 Board

- DAO 의 insert() 를 호출

 

- 이 코드를 복사 후 DAO 클래스 BoardDao.java 로 가서 복붙 하고 수정

- BoardDao.java 에서 insert() 메소드 생성

	public int insert(Board board) {
		return session.insert("insert", board);
	}

- 이 insert() 가 실제 SQL문을 수행하고 성공시 1 을 반환함

 

- Mapper 파일인 Board.xml 에 id 가 "insert" 인 insert (글 작성) 문을 생성해야함

	<!-- 글 작성 -->
	<insert id="insert" parameterType="board">
		insert into myboard values(myboard_seq.nextval,#{writer},
		#{passwd},#{subject},#{content},0,sysdate)
	</insert>

- 전달되는 값이 DTO Board 객체 board 이므로 DTO Board 의 alias 인 "board" 를 parameterType 에 설정

- no 컬럼은 sequence 인 myboard_seq 로 세팅

- 앞에서 객체 board 가 넘어왔으므로 #{writer}, #{passwd}, #{subject}, #{content} 로 세팅

- 조회수 readcount 컬럼은 0, 등록일 register 컬럼은 sysdate 로 세팅

 

- DAO -> Service -> Controller 로 다시 돌아왔다

- Controller "boardwrite.do" 요청 부분 다시 보기

	// 글 작성
	@RequestMapping("boardwrite.do")
	public String boardwrite(Board board, Model model) {
		
		int result = bs.insert(board);
		if(result == 1) System.out.println("글 작성 성공");
		
		model.addAttribute("result", result);
		return "board/insertresult";
	}

- result 값을 한번 찍어보기

- 그리고 /WEB-INF/views/board/insetresult.jsp 로 이동한다, 이 insertresult.jsp 파일을 board 폴더 하위에 생성해야함

- 이 파일에서 성공 / 실패 처리, 즉 출력을 할 것이다

 

- insertresult.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>

<c:if test="${result == 1}">
	<script>
		alert("글 작성 성공");
		location.href = "boardlist.do";
	</script>
</c:if>

<c:if test="${result != 1}">
	<script>
		alert("글 작성 실패");
		history.go(-1);
	</script>
</c:if>

- JSTL core 라이브러리 사용하기 위해 core 라이브러리 불러오는 코드 한 줄 추가

- 글 작성 성공하면 목록 페이지로 갈 것이므로 "boardlist.do" 요청하기

- 글 작성 실패시 이전 페이지인 글 작성 폼으로 돌아감

 

- index 파일을 실행시켜서 글 작성폼에서 글을 작성해보자

+ 글 수정 / 삭제 시 비밀번호가 맞아야 삭제되도록 하기 위해 비밀번호를 입력받는 것임

- "boardlist.do" 요청 처리를 하지 않았으므로 오류가 뜬다

 

- Controller 클래스 BoardController.java 에서 "boardlist.do" 요청을 처리하는 코드를 작성하자 (미완성, 수정 전)

	// 글 목록
	@RequestMapping("boardlist.do")
	public String boardlist(HttpServletRequest request, Model model) {
		
		int page = 1;	// 현재 페이지 번호
		int limit = 10;	// 한 화면에 출력할 데이터 갯수
		
		if(request.getParameter("page") != null) {
			page = Integer.parseInt(request.getParameter("page"));
		}
		
		int startRow = (page - 1) * limit + 1;
		int endRow = page * limit;
		
		int listcount = bs.getCount();	// 총 데이터 갯수
		System.out.println("listcount : " + listcount);
		
		return "board/boardlist";
	}

 

- 기본변수, 파생변수를 설정해서 DB에서 목록을 구해오고, 페이징 처리를 하고, 그걸 View 에 뿌려야함

- request 객체를 매개변수로 부터 받는다, 이렇게 매개변수에 선언하면 자동으로 request 객체를 받아옴

- 결과를 View 페이지에 뿌려야하므로 Model 객체도 매개변수에 선언해서 받아온다

1. 기본변수 page : 현재 페이지를 저장할 변수

2. 기본변수 limit : 한 화면에 출력할 데이터 개수를 저장하는 변수

3. 파생변수 starRow, endRow : page, limit 을 이용해서 구해준다, page, limit 로 생성 가능하므로 굳이 생성하지 않아도 된다

4. 기본변수 listcount : 총 데이터 갯수를 저장, DB 와 연동해서 count(*) 로 가져올 것 

<총 데이터 개수 가져오기>

- Service 클래스의 getCount() 를 호출해서 리턴받는다 * 아래에서 Service 에서 getCount() 를 생성함

- 나머지 부분은 아래에서 다시 완성

 

- Service 클래스 BoardService.java 에서 getCount() 메소드를 생성해야함

	public int getCount() {
		return dao.getCount();
	}

 

- DAO 클래스 BoardDao.java 에서 getCount() 메소드 생성하기

	public int getCount() {
		return session.selectOne("count");
	}

- 그룹함수는 결과가 1개이므로 selectOne() 메소드 사용, id 가 "count" 인 SQL문 불러오기

 

- Mapper 파일 Board.xml 에서 id 가 "count" 인 SQL문 생성

	<!-- 총 데이터 갯수 -->
	<select id="count" resultType="int">
		select count(*) from myboard
	</select>

- 돌려주는 값이 총 데이터 갯수이므로 resultType 은 "int"

 

- 총 데이터 갯수를 가져왔는지 확인하기 위해 현재 상태에서 index 파일에서 "boardlist.do" 로 요청해보자

- index 파일 실행 하고 콘솔창 보기

- listcount 값이 찍혀 나왔다, 글을 하나 작성했었으므로 listcount : 1 이 맞다

 

- 이제 목록을 가져오는 작업을 Controller 부터 처리하자

- Controller 클래스 BoardController.java 에서 "boardlist.do" 요청을 처리하는 코드를 작성하자 (이어서, 완성, 추가되었다)

	// 글 목록
	@RequestMapping("boardlist.do")
	public String boardlist(HttpServletRequest request, Model model) {
		
		int page = 1;	// 현재 페이지 번호
		int limit = 10;	// 한 화면에 출력할 데이터 갯수
		
		if(request.getParameter("page") != null) {
			page = Integer.parseInt(request.getParameter("page"));
		}
		
		int startRow = (page - 1) * limit + 1;
		int endRow = page * limit;
		
		int listcount = bs.getCount();	// 총 데이터 갯수
		System.out.println("listcount : " + listcount);
		
		List<Board> boardlist = bs.getBoardList(page); // 게시판 목록
		System.out.println("boardlist : " + boardlist);
		
		// 총 페이지
		int pageCount = listcount / limit + ((listcount % limit == 0) ? 0 : 1);
		
		int startPage = ((page - 1) / 10) * limit + 1; // 1, 11, 21...
		int endPage = startPage + 10 - 1;	// 10, 20, 30...
		
		if(endPage > pageCount) endPage = pageCount;
		
		model.addAttribute("page", page);
		model.addAttribute("listcount", listcount);
		model.addAttribute("boardlist", boardlist); // 리스트
		model.addAttribute("pageCount", pageCount);
		model.addAttribute("startPage", startPage);
		model.addAttribute("endPage", endPage);
		
		return "board/boardlist";
	}

<목록 가져오기>

- MyBatis 로 처리하면 DAO -> Mapper 파일로 갈때 단 하나의 값만 전달가능하다

- limit 은 고정되어있고, startRow, endRow 는 나중에 페이지 번호 page 로 계산 가능하므로 페이지번호 page 만 넘기기

+ 또는 map 객체에 키밸류 형태로 저장하는 방법도 있다

- getBoardList() 에서 글들이 저장된 리스트를 반환하도록 할 것 * 아래에서 설명

<돌아온 후>

- getBoardList() 에서 돌아온 후 콘솔창 출력해보면 boadlist 에 내가 썼던 글의 주소가 출력된다

5. 파생변수 pageCount : 총 페이지 수를 구함, listcount 와 limit 사용

6. 파생변수 startPage & endPage : 각(현재) 블럭의 시작 페이지, 끝 페이지, page 와 limit 사용

- 이후 기본변수, 파생변수, 가져온 목록(List) 들을 Model 객체에 저장하고 board/boardlist.jsp 로 이동

 

- Service 클래스 BoardService.java 에서 목록을 가져오는 메소드 getBoardList() 를 생성하자

	public List<Board> getBoardList(int page) {
		return dao.getBoardList(page);
	}

 

- DAO 클래스 BoardDao.java 에서 getBoardList() 를 작성하자

	public List<Board> getBoardList(int page) {
		return session.selectList("list", page);
	}

- 여러개의 글(DTO 객체) 을 가져올 것이므로 selectList() 메소드 사용

- 페이지 번호 page 를 전달함, 받는 값은 DTO 객체들이 저장된 리스트이다

 

- Mapper 파일 Baord.xml 에서 id 가 "list" 인 SQL문 작성

	<!-- 글 목록 -->
	<!-- &gt; : > , &lt; : < -->
	<select id="list" parameterType="int" resultType="board">
		<![CDATA[
		select * from (select rownum rnum, board.* from (
		select * from myboard order by no desc) board )
		where rnum >= ((#{page}-1) * 10 + 1) and rnum <= (#{page} * 10)
		]]>
	</select>

- 페이지 번호가 넘어오므로 parameterType 은 "int" , 돌려주는 값은 DTO 가 저장된 리스트 이므로 resultType 은 "board"(DTO alias) 

- limit 은 10으로 고정되어있으므로 10 으로 쓰고, startRow, endRow 는 페이지 번호 page 로 계산 가능해서 사용하기

+ <. > 인식을 못하므로 &gt; , $lt; 사용, 또는 <![CDATA[ ]]> 로 전체 SQL문으로 감싸주면 부등호 기호 <, > 를 인식함

 

- View 페이지를 보기 전에 페이징 처리를 확인하기 위해 myboard.sql 에서 데이터를 강제로 많이 삽입하자

insert into myboard values(myboard_seq.nextval,'Lay','1234','Practice1',
	'Content',0,systdate)

 

- View 페이지 board/boardlist.jsp 를 생성해서 Contorller 에서 Model 객체에 저장된 값들을 활용해서 목록을 출력하자

- boardlist.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core" %>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>게시판 목록</title>
</head>
<body>
	<a href="boardform.do">글작성</a> <br>
	글갯수 : ${listcount }
	
	<table border=1 align=center width=700>
		<caption>게시판 목록</caption>
		<tr>
			<th>번호</th>
			<th>제목</th>
			<th>작성자</th>
			<th>날짜</th>
			<th>조회수</th>
		</tr>
		
		<!-- 화면 출력 번호 -->
		<c:set var="num" value="${listcount - (page - 1) * 10}"/>

		<c:forEach var="b" items="${boardlist}">
		<tr>
			<td>${num}
				<c:set var="num" value="${num-1}"/>
			</td>
			<td>
				<a href="boardcontent.do?no=${b.no}&page=${page}">${b.subject}</a>
			</td>
			<td>${b.writer}</td>
			<td>
				<fmt:formatDate value="${b.register}" 
				pattern="yyyy-MM-dd HH:mm:ss"/>
			</td>
			<td>${b.readcount}</td>
		</tr>
		</c:forEach>
		
	</table>
<br>
<!-- 페이지 처리 -->
<center>
<c:if test="${listcount > 0}">

<!-- 1페이지로 이동 -->
<a href="boardlist.do?page=1" style="text-decoration:none"> << </a>
<!-- 이전 블럭으로 이동 -->
<c:if test="${startpage > 10}">
	<a href="boardlist.do?page=${startPage - 10}">[이전]</a>
</c:if>
<!-- 각 블럭에 10개 페이지 출력 -->
<c:forEach var="i" begin="${startPage}" end="${endPage}">
	<c:if test="${i == page}"> <!-- 현재 페이지 -->
		[${i}]
	</c:if>
	<c:if test="${i != page}"> <!-- 현재 페이지가 아닌 경우-->
		<a href="boardlist.do?page=${i}">[${i}]</a>
	</c:if>
</c:forEach>
<!-- 다음 블럭으로 이동 -->
<c:if test="${endPage < pageCount}">
	<a href="boardlist.do?page=${startPage + 10}">[다음]</a>
</c:if>
<!-- 마지막 페이지로 이동 -->
<a href="boardlist.do?page=${pageCount}" style="text-decoration:none"> >> </a>

</c:if>
</center>

</body>
</html>

- JSTL core 라이브러리, 국제화 라이브러리를 불러오기

7. 파생변수 num : 화면 출력 번호, 코어 라이브러리 set 태그 사용해서 생성했음, listcount, page 를 사용해서 생성

- forEach 태그 items 안에 목록 리스트인 ${boardlist} 를 써서 하나씩 글을 가져오기

- forEach 루프가 돌아갈때마다 num 을 1씩 감소시키기, 이때 코어 라이브러리 set 태그 사용해서 재정의 하는 방식

			<td>${num}
				<c:set var="num" value="${num-1}"
			</td>

- 등록일은 국제화 라이브러리 formatDate 태그로 패턴지정해서 적용

- 제목을 클릭하면 상세페이지로 가기 위해 "boardcontent.do" 로 요청하고 글번호 no, 페이지 번호 page 전달

- table 태그 아래에는 페이지 처리를 하고 있음, 자주 했으므로 설명 생략

 

프로젝트 myBatis1 vs 프로젝트 myBatis2

프로젝트 myBatis1

- 이전 프로젝트는 DAO 클래스에서 configuration.xml(MyBatis 환경설정 파일) 을 읽어와서 그걸로 SqlSessionFactory 객체를 생성

- 이전 프로젝트는 MyBatis 쪽에서 DB 연동을 처리하므로 SqlSession 객체를 @Autowired 로 생성하지 않음

- 이전 프로젝트 myBatis1 프로젝트에서는 Mode 1 - MyBatis , Mode 2 - MyBatis 연동할때와 똑같이 연동한다

 

프로젝트 myBatis2

- 현재 프로젝트는 DAO 클래스에서 @Autowired 로 SqlSession 을 주입할 것

- DB연동을 위해서 MyBatis 환경설정 파일들 대신 root-context.xml 에서 DB 접속에 대한 내용을 처리

- 내용은 프로젝트 myBaits1 과 연결된 내용

- 현재 프로젝트 myBatis2 프로젝트에서는 DB 연동 관련 내용을 Spring에서 제어, Spring 에서 DB 연동할 것


MyBatis 연동해서 Oracle DB 연동

 

Spring - MyBatis 연동 예제 2

프로젝트 myBatis2

 

실습 준비

- 먼저 오라클의 scott 계정이 활성화 되어있어야한다

- scott 계정 소유 테이블을 사용하므로 따로 테이블을 만들지는 않음

- 다음으로 클라우드의 프로젝트 import 하기

 

프로젝트 구조

- DB 연동에 필요한 MyBatis 환경설정 파일들은 있지만 안에 내용이 없음

- 인터페이스와 구현클래스가 1쌍씩 만들어져 있다 ex) Service 인터페이스, Service 구현클래스

- 테이블이 2개이므로 Controller, DAO, Service 가 2개씩 만들어져있다

 

테이블이 늘어나면

1. Controller 클래스가 늘어남

2. DAO 클래스도 늘어남

3. Service 클래스도 늘어남

4. Mapper 파일도 늘어남

- 여기서 테이블을 Dept, Emp 둘 다 사용하므로 Dept 클래스들이 따로, 따로 Emp 클래스들이 따로 만들어짐


파일들 살펴보기 : pom.xml

- pom.xml

 

Spring 버전

- pom.xml 을 보면 Spring 버전이 나타나있음

 

 

- Maven Repository 페이지에서 Spring 최신버전을 확인해 볼 수 있다

- 버전을 높이면 다시 최신버전을 로컬저장소로 다운받아서 사용한다

 

Connection Pool 을 쓰기 위한 라이브러리

		<!-- dbcp jdbc connection pool -->
		<dependency>
			<groupId>commons-dbcp</groupId>
			<artifactId>commons-dbcp</artifactId>
			<version>1.2.2</version>
		</dependency>
		<dependency>
			<groupId>c3p0</groupId>
			<artifactId>c3p0</artifactId>
			<version>0.9.1.2</version>
		</dependency>

- Connection Pool 을 쓰기 위한 commons-dbcp 라이브러리, c3p0 라이브러리가 추가되어있다

 

MyBatis 연동위한 라이브러리

		<!-- mybatis -->
		<dependency>
			<groupId>org.mybatis</groupId>
			<artifactId>mybatis</artifactId>
			<version>3.2.3</version>
		</dependency>
		<dependency>
			<groupId>org.mybatis</groupId>
			<artifactId>mybatis-spring</artifactId>
			<version>1.2.1</version>
		</dependency>

- Spring 에서 MyBatis 를 쓰려면 MyBatis 라이브러리 뿐 아니라 MyBatis-Spring 연결 라이브러리도 필요하다


파일들 살펴보기 : web.xml

- web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
	<!-- 한글 입력 -->
	<filter>
		<filter-name>CharacterEncodingFilter</filter-name>
		<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
		<init-param>
			<param-name>encoding</param-name>
			<param-value>UTF-8</param-value>
		</init-param>
		<init-param>
			<param-name>forceEncoding</param-name>
			<param-value>true</param-value>
		</init-param>
	</filter>
	<filter-mapping>
		<filter-name>CharacterEncodingFilter</filter-name>
		<url-pattern>/*</url-pattern>
	</filter-mapping>
	<!-- The definition of the Root Spring Container shared by all Servlets and Filters -->
	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>/WEB-INF/spring/root-context.xml</param-value>
	</context-param>
	
	<!-- Creates the Spring Container shared by all Servlets and Filters -->
	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>
	<!-- Processes application requests -->
	<servlet>
		<servlet-name>appServlet</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
		</init-param>
		<load-on-startup>1</load-on-startup>
	</servlet>		
	<servlet-mapping>
		<servlet-name>appServlet</servlet-name>
		<url-pattern>*.do</url-pattern>
	</servlet-mapping>
</web-app>

- Apache Tomcat 이 실행시 자동으로 실행되는 파일

 

web.xml 에 들어가는 내용 3가지

1. Dispatcher Servlet 이름, 위치, 매핑 등록

- Dispatcher Servlet 클래스에 대한 위치가 등록되어있다

- Dispatcher Servlet 으로 찾아가기위한 매핑 등록

- 현재는 do 확장자로 요청할때만 Dispatcher Servlet 으로 찾아감

 

2. Spring 환경설정 파일 불러오기

- servlet-context.xml, root-context.xml 을 불러서 실행시켜준다

+ 두 파일이 resources 폴더 내에 있는 경우 classpath: 붙이기

 

3. 한글값 인코딩

- 한글값 인코딩시켜주는 필터와 필터매핑을 잡음


파일들 살펴보기 : servlet-context.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:beans="http://www.springframework.org/schema/beans"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
	
	<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
	<!-- Enables the Spring MVC @Controller programming model -->
	<annotation-driven />
	
	<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
	<resources mapping="/resources/**" location="/resources/" />
	
	<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
	<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<beans:property name="prefix" value="/WEB-INF/views/" />
		<beans:property name="suffix" value=".jsp" />
	</beans:bean>	
	
	<context:component-scan base-package="myBatis2" />	
	
</beans:beans>

<ViewResolver>

- View 파일들이 저장될 최상위 디렉트리를 지정하고 있다

- 현재는 "/WEB-INF/views" 가 View 파일들의 최상위 디렉토리이다

- 주의 : /WEB-INF/views 아래 어떤 폴더가 있고 그 안에 어떤 View 페이지가 있다면 Controller 에서 View 페이지로 찾아갈때 그 하위 폴더를 경로에 추가해야함

ex) 현재는 views 폴더 아래 emp 폴더가 있고 그 안에 일부 View 파일들이 있다, Controller 클래스에서 그 파일로 갈때는 return "emp/empList.jsp" 로 가야함

<base-package>

- base-package 를 지정한다는 의미는 어노테이션 기반을 사용하겠다는 의미

-base-package는 base 가 되는 자바파일들이 저장될 최상위 디렉토리를 지정하는 것이다, 이 패키지는 java 폴더 하위에 있어야한다

- 이 base-package 안에 클래스들이 있어야함, 그 클래스들 위에는 4가지 어노테이션 중 하나가 붙어있어야만 Service / Controller / DAO 기능 수행 가능

- base-package 는 기본적으로는 top-level 패키지 3단계로 만들어진다

- 여기선 수정해서 "myBatis2" 를 base-package 로 설정했다

- src/main/java 폴더 하위의 myBatis2 폴더 안의 클래스들을 모두 읽어온다는 의미

 

+ servlet-context.xml 에서는 Setter DI (ViewResolver) 도 쓰고 있고 어노테이션 기반 DI (base-package) 도 있다

 

+ 원래 Spring 프로젝트 생성시 지정했던 top-level 패키지명 보기

- pom.xml 부분

- com.ch.myBatis2 이었다

- 이 패키지를 지우고 myBatis2 만 남긴것


파일들 살펴보기 : MyBatis 환경설정 파일

1. jdbc.properties

2, configuration.xml

3. Mapper 파일 (테이블 마다)

 


파일들 살펴보기 : Configuration.xml

2, configuration.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">

<configuration>

	<typeAliases>
		<typeAlias alias="dept" type="myBatis2.model.Dept" />
		<typeAlias alias="emp"  type="myBatis2.model.Emp" />
	</typeAliases>	
	
	<!-- 
	<mappers>
		<mapper resource="sql/Dept.xml" />
		<mapper resource="sql/Emp.xml" />
	</mappers>  -->	
	
</configuration>

 

- configuration.xml 의 3가지 내용(DTO alias, DB접속 내용, Mapper 파일 호출) 중 DTO alias 설정하는 코드만 남아있다

- DTO alias 설정 내용만 있다

 

3. Mapper 파일 (테이블 마다)

- Dept.xml, Emp.xml 이 있다

- Mapper 파일들 안에 내용은 모두 그대로 이다

- Spring - MyBatis 연동하고 Spring 으로 DB 연동을 처리하더라도 Mapper 파일의 내용은 같다, 그대로 SQL문들이 있음

- Spring - MyBatis 연동할때 Mapper 파일일은 JSP - MyBatis 연동때 Mapper 파일과 똑같다


파일들 살펴보기 : root-context.xml

- root-context.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.1.xsd">
	
	<!-- <context:property-placeholder location="classpath:jdbc.properties" /> -->
	
	<!-- dataSource -->
	<!-- <bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource"
		destroy-method="close">
		<property name="driverClass" value="${jdbc.driverClassName}" />
		<property name="jdbcUrl" value="${jdbc.url}" />
		<property name="user" value="${jdbc.username}" />
		<property name="password" value="${jdbc.password}" />
		<property name="maxPoolSize" value="${jdbc.maxPoolSize}" />
	</bean> -->
	
	<!-- Data Source -->
	<bean id="dataSource"
		class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
		<property name="driverClass" value="oracle.jdbc.driver.OracleDriver" />
		<property name="url" value="jdbc:oracle:thin:@localhost:1521:xe" />
		<property name="username" value="scott" />
		<property name="password" value="tiger" />
	</bean>
	
	<!-- 스프링 jdbc 즉 스프링으로 oracle 디비 연결 -->
	<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="dataSource" ref="dataSource" />
		<property name="configLocation" value="classpath:configuration.xml" />
		<property name="mapperLocations" value="classpath:sql/*.xml" />
	</bean>
	
	<bean id="session" class="org.mybatis.spring.SqlSessionTemplate">
		<constructor-arg index="0" ref="sqlSessionFactory" />
	</bean>	
	
</beans>

- DB 연동을 Spring 쪽에서 제어하므로, root-context.xml 안에 DB 연결에 필요한 내용을 처리

- 이전 프로젝트와 다르게 configuration.xml 에는 DTO alias 내용만 남고 나머지 설정은 모두 이 root-context. 가 처리

- root-context.xml 에서는 어노테이션 기반 DI + Setter DI 다 있다

- root-context.xml 에서 beans 가 루트 엘리먼트이고 안에서 3개의 bean 을 만들어서 처리하고 있다

- 3개의 bean 을 만들면서 2번의 Setter DI 와 1번의 Constructor DI 를 하고 있다

 

root-context.xml 부분 1

	<!-- Data Source -->
	<bean id="dataSource"
		class="org.springframework.jdbc.datasource.SimpleDriverDataSource">
		<property name="driverClass" value="oracle.jdbc.driver.OracleDriver" />
		<property name="url" value="jdbc:oracle:thin:@localhost:1521:xe" />
		<property name="username" value="scott" />
		<property name="password" value="tiger" />
	</bean>

- 여러 클래스를 사용 가능하지만 현재는 SimpleDriverDataSource 클래스를 사용, 클래스마다 정해진 Property 들이 달라진다

- Setter DI 를 통해 필드 driverClass, url, username, password 에 value 의 값들을 주입하고 있다

- 오라클 연결 관련 내용(계정명, 비번 등) 들을 주입하고 있다

 

root-context.xml 부분 2

	<!-- 스프링 jdbc 즉 스프링으로 oracle 디비 연결 -->
	<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="dataSource" ref="dataSource" />
		<property name="configLocation" value="classpath:configuration.xml" />
		<property name="mapperLocations" value="classpath:sql/*.xml" />
	</bean>

- SqlSessionFactoryBean 객체 sqlSessionFactory 를 생성하고 있다

- SqlSessionFactoryBean 클래스의 프로퍼티 dataSource, configLocation, mapperLocations 에 값들을 주입하고 있다

 

DB 연결 코드 (root-context.xml 부분 2-1)

		<property name="dataSource" ref="dataSource" />

- dataSource 프로퍼티로 위에서 만든 SimpleDriverDataSource 객체인 dataSource 객체를 주입시킨다

- 이 코드가 DB 를 연결하는 코드이다

 

DB 연결 코드 (root-context.xml 부분 2-2)

		<property name="configLocation" value="classpath:configuration.xml" />

- configLocation : MyBatis 환경설정 파일(Configuration.xml) 의 위치를 설정하는 역할

- MyBatis 환경설정 파일인 configuration.xml 을 불러오고 있다

- MyBatis 환경설정 파일인 configuration.xml 은 resources 패키지 아래에 있으므로 classpath: 를 붙여서 불러온다

+ configuration.xml 에는 3가지 내용(DTO alias, DB접속 내용, Mapper 파일 호출) 중 DTO alias 설정하는 코드만 남아있다

 

DB 연결 코드 (root-context.xml 부분 2-3)

		<property name="mapperLocations" value="classpath:sql/*.xml" />

- mapperLocations : Mapper 파일들의 위치를 설정하는 역할

- Mapper 파일들은 resources 폴더 내에 있으므로 classpath: 를 붙이고 그 안에서도 sql 폴더 안에 있으므로 "classpath:sql/" 를 설정

- 그리고 resources/sql 안의 모든 xml 파일을 불러오라는 의미로 "classpath:sql/*xml"

 

 

root-context.xml 부분 3

	<bean id="session" class="org.mybatis.spring.SqlSessionTemplate">
		<constructor-arg index="0" ref="sqlSessionFactory" />
	</bean>

- SqlSessionTemplate 클래스는 SQL 5개 메소드를 제공하는 myBatis 에서 지원하는 구현클래스이다

- 인터페이스 SqlSession 로는 객체를 생성할 수 없으므로 구현 클래스인 SqlSessionTemplate 으로 객체를 생성해야한다

+ SqlSessionTemplate 객체를 생성했으므로 그걸 업캐스팅해서 SqlSession에 주입해도 되고 SqlSessionTemplate 에 주입해도 된다

- 이 코드로 SqlSessionTemplate 객체를 생성하고 있다, 이 객체는 자동으로 DAO 의 @Autowired 가 붙은 SqlSessionTemplate 또는 SqlSession 으로 주입된다

- 이 코드로 이렇게 SqlSessionTemplate 객체를 생성해야만 DAO 클래스 안에서 @Autowired 로 SqlSessionTemplate 또는 SqlSesssion 으로 객체 주입 가능하다

+ 이 코드가 없어도 Service 객체는 Controller 로 , DAO 객체는 Service 로 주입된다, 단 SqlSession 객체를 DAO 에 주입할때는 반드시 이 코드가 있어야함

- Constructor DI 를 하고 있다, 여기가 유일하게 Constructor DI 를 하는 곳이다

- 즉, 생성자의 매개변수로 주입한다, index = "0" 은 생성자 매개변수 중 첫번째 매개변수를 의미함, 그 생성자의 첫번째 매개변수에 위에서 생성된 sqlSessionFactory 객체를 주입함

- 생성자를 통해 SqlSessionTemplate 객체의 프로퍼티를 채우고, 그렇게 생성된 SqlSessionTemplate 객체를 DAO 의 필드로 주입하는 것임

 

+ 이러한 내용을 프로젝트 myBatis1 에서는 Configuration.xml 과 DAO 에서 처리했다

- Configuration.xml 에서 DB 연결하고 Mapper 파일을 불러왔다

- DAO 에서 Configuration.xml 을 읽어오고 SqlSession 객체를 생성했었다


-프로젝트 myBatis1 (MyBatis 에서 DB연동) 의 내용을 잠시 보자

프로젝트 myBatis1 의 Configuration.xml 

- 여기서 DB 연결과 Mapper 파일을 불러왔었다

- 그리고 DAO 에서는 이 Configuration.xml 을 읽어왔다

- 즉 흐름은 아래와 같았다


- 현재 프로젝트 myBatis2 에서 DB 연동하는 걸 보자

- 위와 비교

 

DB 접속 내용

- root-context.xml 에서 SimpleDriverDataSource 객체를 생성할때 필드값으로 들어간다

- 이 SimpleDriverDataSource 객체를 SqlSessionFactoryBean 객체를 생성할때 주입함

 

Mapper 파일을 불러오는 방법 (DAO 에서 Mapper 파일에 접근 가능한 이유)

- SqlSessionFactoryBean 객체에는 Mapper 파일의 위치가 들어간다

- SqlSessionFactoryBean 객체인 sqlSessionFactory 가 SqlSessionTemplate 객체를 생성할때 ref 로 주입되었기 떄문에 DAO 에서 Mapper 파일을 불러와서 id 로 SQL문에 접근 가능한 것

- 현재 프로젝트 myBatis2 에서는 root-context.xml 을 읽어서 SqlSession 객체를 생성한다


파일들 살펴보기 : DeptDaoImpl.java

- DAO 구현 클래스 중 하나를 살펴보자

- DeptDaoImpl.java

package myBatis2.dao;

import java.util.List;
import org.mybatis.spring.SqlSessionTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Repository;
import myBatis2.model.Dept;

@Repository
public class DeptDaoImpl implements DeptDao {
	@Autowired
	private SqlSessionTemplate st;

	public List<Dept> list() {
		return st.selectList("deptns.list");
	}

	public int insert(Dept dept) {
		return st.insert("deptns.insert", dept);
	}

	public Dept select(int deptno) {
		return st.selectOne("deptns.select", deptno);
	}

	public int update(Dept dept) {
		return st.update("deptns.update", dept);
	}

	public int delete(int deptno) {
		return st.delete("deptns.delete", deptno);
	}
}

- @Autowired 어노테이션을 사용해서 SqlSession 객체를 주입하고 있다

- 인터페이스인 SqlSession 을 주입해도 되고, 그걸 상속받는 구현 클래스인 SqlSessionTemplate 를 주입해도 된다

- SqlSession 은 SQL문을 실행시키는 메소드를 제공하는 인터페이스

- 여기선 구현클래스인 SqlSessionTemplate 으로 객체를 주입하고 있다

- 주입되어야만 5개의 메소드(insert(), update(), delete(), selectOne(), selectList() 를 사용 가능함

- root-context.xml 에서 DB 연동을 처리했기 때문에 여기서 @Autowired 로 SqlSession 객체를 주입 가능한 것

- root-context.xml 에서 SqlSessionTemplate 객체를 생성했고 어노테이션 기반 DI 로 여기에 주입했다

 

프로젝트 myBaits1 vs 프로젝트 myBatis2

프로젝트 myBaits1

- 이전 프로젝트 myBatis1은 여기서 MyBatis 환경설정 파일을 읽어와서 SqlSession 객체를 생성했다

- 이전 프로젝트에서는 MyBatis 환경설정 파일을 썼으므므로 @Autowired 로 SqlSession 주입 불가

 

프로젝트 myBatis2

- 현재 프로젝트 myBatis2는 여기서 @Autowired 어노테이션으로 SqlSession 을 주입받았다

- root-context.xml 에서 DB 연동을 처리했기 때문에 여기서 @Autowired 로 SqlSession 객체를 주입 가능한 것

- 그 root-context.xml 에서 객체 생성 및 주입을 했고 시점은 root-context.xml 이 web.xml 에 의해 읽어졌을때


파일들 살펴보기 : DeptServiceImpl.java

- Service 클래스 중 하나를 보자

- DeptServiceImpl.java

package myBatis2.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import myBatis2.dao.DeptDao;
import myBatis2.model.Dept;

@Service
public class DeptServiceImpl implements DeptService {
	@Autowired
	private DeptDao dd;

	public List<Dept> list() {
		return dd.list();
	}

	public int insert(Dept dept) {
		return dd.insert(dept);
	}

	public Dept select(int deptno) {
		return dd.select(deptno);
	}

	public int update(Dept dept) {
		return dd.update(dept);
	}

	public int delete(int deptno) {
		return dd.delete(deptno);
	}
}

- DAO 인터페이스 DeptDao 로 객체를 주입하고 있다

- @Autowired 로 객체를 주입할때 인터페이스로 객체를 주입해도되고, 구현클래스로 주입해도 된다

+ 단 어노테이션을 붙일때는 구현클래스 위에만 붙인다


파일들 살펴보기 : EmpController.java

- Controller 클래스 중 하나를 보자

- EmpController.java

package myBatis2.controller;

import java.sql.Date;
import java.util.List;

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 myBatis2.model.Dept;
import myBatis2.model.Emp;
import myBatis2.service.DeptService;
import myBatis2.service.EmpService;

@Controller
public class EmpController {
	@Autowired
	private EmpService es;
	@Autowired
	private DeptService ds;

	@RequestMapping("empList.do")
	public String empList(int deptno, Model model) {
		Dept dept = ds.select(deptno);
		List<Emp> list = es.list(deptno);
		model.addAttribute("dept", dept);
		model.addAttribute("list", list);
		return "emp/empList";
	}

	@RequestMapping("empInsertForm.do")
	public String empInsertForm(Model model) {
		List<Dept> deptList = ds.list();
		List<Emp> empList = es.empList();
		model.addAttribute("deptList", deptList);
		model.addAttribute("empList", empList);
		return "emp/empInsertForm";
	}

	@RequestMapping("dupCheck.do")
	public String dupCheck(int empno, Model model) {
		System.out.println("empno:"+empno);
		Emp emp = es.select(empno);
		String msg = "";
		if (emp != null)
			msg = "이미 있는 데이터입니다";
		else
			msg = "사용 가능한 사번 입니다";
		model.addAttribute("msg", msg);
		return "emp/dupCheck";
	}

	@RequestMapping("empInsert.do")
//	public String empInsert(Emp emp, String hiredate1, Model model) {
	public String empInsert(Emp emp, Model model) {
//		emp.setHiredate(Date.valueOf(hiredate1)); // String -> Date 형변환
		int result = es.insert(emp);
		model.addAttribute("result", result);
		model.addAttribute("deptno", emp.getDeptno());
		return "emp/empInsert";
	}

	@RequestMapping("empView.do")
	public String empView(int empno, Model model) {
		Emp emp = es.select(empno);
		model.addAttribute("emp", emp);
		return "emp/empView";
	}

	@RequestMapping("empDelete.do")
	public String empDelete(int empno, Model model) {
		Emp emp = es.select(empno);
		int result = es.delete(empno);
		model.addAttribute("result", result);
		model.addAttribute("deptno", emp.getDeptno());
		return "emp/empDelete";
	}

	@RequestMapping("empUpdateForm.do")
	public String empUpdateForm(int empno, Model model) {
		Emp emp = es.select(empno);
		List<Dept> deptList = ds.list();
		model.addAttribute("emp", emp);
		model.addAttribute("deptList", deptList);
		return "emp/empUpdateForm";
	}

	@RequestMapping("empUpdate.do")
	public String empUpdate(Emp emp, Model model) {
		int result = es.update(emp);
		model.addAttribute("deptno", emp.getDeptno());
		model.addAttribute("result", result);
		return "emp/empUpdate";
	}

	@RequestMapping("empAllList.do")
	public String empAllList(Model model) {
		List<Emp> list = es.empAllList();
		model.addAttribute("list", list);
		return "emp/empAllList";
	}
}

- Service 객체를 2개 주입받고 있음

- @Autowired 로 주입 2개 이상도 가능하다


흐름 보기

- @Autowired 로 객체를 주입할때 인터페이스로 객체를 주입해도되고, 구현클래스로 주입해도 된다

- 여기서는 다 인터페이스로 주입하고 있음 ex) BoardService, BoardDao, SqlSession

 

필요한 bean 객체를 어노테이션 기반으로 주입하기 위한 조건

1. base-package 에 적힌 해당 패키지의 모든 클래스를 읽어오는 코드가 있어야함

2. 어노테이션 4개 중 하나 붙어있어야함

3. 만들고자 하는 객체 위에 @Autowired 어노테이션을 붙여야함


프로젝트 실행

- index.jsp 또는 프로젝트 실행

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
    
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<script type="text/javascript">
	location.href="deptList.do";
</script>
</body>
</html>

 

전체 직원 목록

- 사번, 이름, 업무, 급여, 부서코드는 Emp 테이블에 있는 내용이다

- 부서모드, 부서명, 근무지는 Dept 테이블에 있는 내용이다

- 검색할때 조인해서 검색해서 그걸 뿌려준 것이다

 

부서 목록

- 현재는 삭제가 되지 않음, Emp 테이블에서 참조하는 Foreign Key 가 없을때는 삭제됨

- 그 외, 부서 입력(추가), 사원 입력(추가) 도 가능하다

 

조인(join) 과 DTO

- 조인한 결과를 DTO 객체를 생성하여 저장해야한다

- 현재 DTO 클래스는 Dept 와 Emp 가 있다

+ 테이블 개수가 늘어나면 Mapper 파일, Controller , Service , DAO , DTO 도 1개씩 늘어나는게 일반적

- Dept 테이블에 대해 매핑을 잡을때를 위해 Dept DTO의 필드명은 Dept 테이블의 컬럼명과 일치시켰다

- Emp 테이블에는 8 개의 컬럼이 있지만 Emp DTO 의 필드는 8개보다 많다, 조인을 했을때 조인한 결과를 이 DTO 객체에 매핑을 통해서 저장할 것

- deptno, dname, loc 는 Emp 테이블에는 없는 컬럼이다, 조인을 했을때 그 결과를 저장하기 위해 만든 컬럼이다

 

+ Configuration.xml 에서 Dept DTO 의 alias 값을 "dept", Emp DTO 의 alias 값을 "emp" 로 설정했다

 

- 이제 실행되는 순서대로 따라가보자



프로젝트 myBatis2 : 부서 목록

+ 전체 코드는 나중에 한번에 올림

- index.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
    
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
<script type="text/javascript">
	location.href="deptList.do";
</script>
</body>
</html>

- "deptList.do" 로 요청한다, DispatcherServlet -> Controller 로 이동

- 2개의 Controller 클래스가 있지만 @RequestMapping 이 맞는 곳으로 찾아가므로 신경쓰지 않아도 된다

 

Controller 클래스 DeptController.java 에서 "deptList.do" 요청 부분만 + @Autowired 부분

	@Autowired
	private DeptService ds;
	
	// 부서 목록
	@RequestMapping("deptList.do")
	public String deptList(Model model) {
		List<Dept> list = ds.list();
		model.addAttribute("list", list);
		return "deptList";
	}

- @Autowired 를 통해서 DeptServiceImpl 객체를 인터페이스인 DeptService ds 에 주입

+ 엄밀히 말하면 구현클래스가 주입되는 것이지만, 어차피 업캐스팅 하므로 그냥 DeptService 객체가 됨

+ 그래서 하나의 인터페이스에 구현클래스를 2개를 만들면 안된다

- 위에서 주입한 Service 객체 ds 를 사용해서 Service 의 list() 를 호출

- 2개이상의 데이터를 검색해서 List 로 돌아올 것이므로 리턴을 List 로 받는다

<돌아온 후>

- Dept DTO 객체를 저장하고 있는 리스트를 반환받았고 그걸 list 에 저장함

- 그 list 를 Model 객체에 저장하고, /WEB-INF/views/deptList.jsp 로 간다

+ Model 에 리스트가 저장되었으므로 View 페이지에서 출력할때는 EL 이 forEach문의 items 에 들어감

 

Service 클래스 DeptServiceImpl.java 에서 list() 메소드 부분만 + @Autowired 부분

	@Autowired
	private DeptDao dd;

	public List<Dept> list() {
		return dd.list();
	}

- @Autowired 를 통해서 DeptDaoImpl 객체를 인터페이스인 DeptDao dd 에 주입

+ 엄밀히 말하면 구현클래스가 주입되는 것이지만, 어차피 업캐스팅 하므로 그냥 DeptDao 객체가 됨

+ 그래서 하나의 인터페이스에 구현클래스를 2개를 만들면 안된다

- 돌려받은 값(리스트)을 그대로 리턴

 

DAO 클래스 DeptDaoImpl.java 에서 list() 메소드 부분만 + @Autowired 부분

	@Autowired
	private SqlSessionTemplate st;

	public List<Dept> list() {
		return st.selectList("deptns.list");
	}

- @Autowired 를 통해서 SqlSessionTemplate 객체를 SqlSessionTemplate st 에 주입

- 2개 이상의 데이터를 검색할 것이므로 selectList() 를 호출

- 이 selectList() 메소드는 위에서 SqlSessionTemplate 객체를 구해왔으므로 사용 가능한 것

- 현재는 Mapper 파일이 2개이므로 id 중복을 피하기 위해 namespace 를 사용한다

+ Mapper 파일 중 하나인 Dept.xml 의 namespace 는 "deptns"

- 그래서 selectList("deptns.list") 는 Dept.xml 파일에서 id 가 list 인 태그의 select SQL문을 불러오는 것을 의미

- 돌려받은 값(리스트)을 그대로 리턴

 

Mapper 파일 Dept.xml 에서 id 가 "list" 인 태그 부분만 + resultMap

	<!-- Use type aliases to avoid typing the full classname every time. -->
	<resultMap id="deptResult"    type="dept">
		<result property="deptno" column="deptno" />
		<result property="dname"  column="dname" />
		<result property="loc"	  column="loc" />
	</resultMap>
	
	<select id="list" resultMap="deptResult">
		select * from dept order by deptno
	</select>

- Dept 테이블의 모든 데이터를 검색

- order by 로 부서번호 기준 오름차순 정렬되어서 돌려준다 

- 현재는 Dept DTO 클래스 프로퍼티명과 검색되는 컬럼명인 Dept 테이블의 컬럼명이 일치하므로 resultMap 을 사용하지 않아도 된다, 사용은 하고 있음

- resultMap 에서 type 은 "dept" 이다, 즉 Dept DTO 타입,

+ 리스트로 반환할때도 type 이나 returyType 에는 DTO 를 써야함

 

- 이제 다시 DAO 로 돌아가야한다, 돌아갈때의 설명은 하늘색 하이라이트로 표시

 

- 돌아갔다가 이제 View 페이지로 오게 된다

 

VIew 페이지 deptList.jsp

- deptList.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ include file="header.jsp"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<div class="container" align="center">
		<h2 class="text-primary">부서 목록</h2>
		<table class="table table-hover">
			<tr>
				<th>부서코드</th>
				<th>부서명</th>
				<th>근무지</th>
				<th></th>
				<th></th>
			</tr>
			<c:if test="${empty list}">
				<tr>
					<td colspan="5">데이터가 없습니다</td>
				</tr>
			</c:if>
			<c:if test="${not empty list }">
				<c:forEach var="dept" items="${list}">
					<tr>
						<td>${dept.deptno}</td>
						<td><a href="empList.do?deptno=${dept.deptno}"
							   class="btn btn-info">${dept.dname}</a></td>
						<td>${dept.loc }</td>
						<td><a href="deptUpdateForm.do?deptno=${dept.deptno }"
							   class="btn btn-success">수정</a></td>
						<td><a href="deptDelete.do?deptno=${dept.deptno }"
							   class="btn btn-danger">삭제</a></td>
				</c:forEach>
			</c:if>
		</table>
		<a href="deptInsertForm.do" class="btn btn-info">부서입력</a> 
		<a href="empAllList.do" class="btn btn-default">전체 직원목록</a>
	</div>
</body>
</html>

 

- list 가 비어있는 경우는 ${empty list} 로 확인하고, "데이터가 없습니다" 뿌려주기

- list 가 비어있지 않는 경우는 Model 에 저장되어있는 걸 EL 로 가져와서 부서 목록을 출력해준다

- forEach 문의 itmes 에 ${list} 가 들어가게 되고, 반복문으로 하나씩 Dept DTO 객체를 가져와서 뿌려준다

 

- '전체 직원 목록' 클릭시 "empAllList.do" 로 요청한다


프로젝트 myBatis2 : 전체 직원 목록

+ 전체 코드는 나중에 한번에 올림

- '전체 직원 목록' 클릭시

- Dept, Emp 테이블을 등가조인해서 결과를 한번에 출력하고 있다

- '전체 직원 목록' 클릭시 "empAllList.do" 로 요청한다

 

EmpController.java 에서 @Autowired 부분만

	@Autowired
	private EmpService es;
	@Autowired
	private DeptService ds;

- 주입할 객체가 여러개인 경우엔 @Autowired 를 개별적으로 써야한다, 1번만 써선 안된다

- DeptService 객체를 주입하고 있고, EmpService 객체도 주입하고 있다

+ 엄밀히 말하면 구현클래스가 주입되는 것이지만, 어차피 업캐스팅 하므로 그냥 DeptService 객체, EmpService 객체가 됨

+ 그래서 하나의 인터페이스에 구현클래스를 2개를 만들면 안된다

- 2개의 Service 객체를 주입받음

 

Controller 클래스 EmpController.java 에서 "empAllList.do" 요청 부분만

	// 전체 직원 목록
	@RequestMapping("empAllList.do")
	public String empAllList(Model model) {
		List<Emp> list = es.empAllList();
		model.addAttribute("list", list);
		return "emp/empAllList";
	}

- 위에서 주입받은 EmpService 객체 es 를 사용해서 Service 클래스 메소드인 empAllList() 호출

- 2개 이상의 데이터를 검색하므로 리스트로 값이 돌아올 것이므로 List 로 받음

<돌아올 때>

- 돌아온 List 에는 Emp DAO 객체들이 들어있다

- prefix 로 잡혀있는 경로까지는 생략하고, 그 하위의 경로는 적어야한다

- /WEB-INF/views 는 생략하고, 하위의 emp 폴더는 적어야함, 그래서 "emp/empAllList" 이다

- /WEB-INF/views/emp/empAllList.jsp 파일로 이동

 

Service 클래스 EmpServiceImpl.java 에서 empAllList() 메소드 부분만 + @Autowired 부분

	@Autowired
	private EmpDao ed;
	public List<Emp> empAllList() {
		return ed.empAllList();
	}

- 위에서 @Autowired 로 주입받은 EmpDao 객체 ed 를 사용해서 DAO 클래스의 메소드인 empAllList() 호출

 

DAO 클래스 EmpDaoImpl.java 에서 empAllList() 메소드 부분만 + @Autowired 부분

	@Autowired
	private SqlSessionTemplate sst;
	public List<Emp> empAllList() {
		return sst.selectList("empAllList");
	}

- 위에서 @Autowired 로 주입받은 SqlSEssionTemplate 객체 sst 를 사용해서 지원메소드인 selectList() 사용

- 2개 이상의 데이터를 검색하므로 selectList() 메소드 사용

- 반환되는 List 에 들어가는 것은 Emp DTO 객체만 들어간다는 의미로 제네릭 <Emp> 사용

 

Mapper 파일 Emp.xml 에서 id 가 "empAllList" 인 태그 부분만 + resultMap

	<!-- Use type aliases to avoid typing the full classname every time. -->
	<resultMap id="empResult"    	type="emp">
		<result property="empno" 	column="empno" />
		<result property="ename"  	column="ename" />
		<result property="job"		column="job" />
		<result property="mgr" 		column="mgr" />
		<result property="hiredate" column="hiredate" />
		<result property="sal"	  	column="sal" />
		<result property="comm"	   	column="comm" />
		<result property="deptno"   column="deptno" />
		<result property="dname"	column="dname" />
		<result property="loc"   	column="loc" />
	</resultMap>

+ 현재는 모든 10개의 테이블과 검색결과의 컬럼명이 일치하므로 일일히 resultMap 을 사용할 필요 없다, 여기선 resultMap 을 사용했음

- Emp DTO 가 저장된 리스트를 리턴하므로, type 을 "emp" 로 설정함

	<select id="empAllList" resultMap="empResult">
		select e.*,dname,loc from emp e, dept d 
			where e.deptno=d.deptno order by empno
	</select>

- from 절에서 emp 테이블의 별칭을 e, dept 테이블의 별칭을 d 로 설정

- 등가조인(과거부터 사용하던 방식, 안시 방식 등이 있다) 을 사용하고 있음

- e 의 모든 컬럼과 dname, loc 를 검색

+ 공통 컬럼이 아닌 경우는 dname, loc 처럼 그냥 쓰면 된다 

- 등가 조인을 위해 e.deptno 와 d.deptno 가 같은 경우만 가져옴

+ 두 테이블 사이 공통적인 컬럼이 있을때만 가능한게 등가조인

- 사원번호 순서대로 정렬, 사원번호가 빠른(작은) 순대로 나온다

- Emp DTO 는 10개의 정보를 저장하므로 검색결과를 모두 저장할 수 있으므로 resultMap 을 쓰지 않고 resultType 에 "emp" 를 써도 된다

 

View 페이지 empAllList.jsp

- emp/empAllList.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ include file="../header.jsp"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<div class="container">
		<table class="table table-striped">
			<tr>
				<td>사번</td>
				<td>이름</td>
				<td>업무</td>
				<td>급여</td>
				<td>부서코드</td>
				<td>부서명</td>
				<td>근무지</td>
			</tr>
			<c:forEach var="emp" items="${list }">
				<tr>
					<td>${emp.empno }</td>
					<td>${emp.ename }</td>
					<td>${emp.job }</td>
					<td>${emp.sal }</td>
					<td>${emp.deptno }</td>
					<td>${emp.dname }</td>
					<td>${emp.loc }</td>
				</tr>
			</c:forEach>
		</table>
		<a href="deptList.do" class="btn btn-default">부서목록</a>
	</div>
</body>
</html>

- 검색된 결과가 Model 을 통해 넘어왔다, 이 값은 Emp DTO 객체를 저장하는 리스트이다

- 리스트가 Model 객체에 저장되었으므로 forEach 태그의 items 값으로 들어간다

- 각각의 Emp DTO 객체는 10개의 값을 저장하고 있음, 그 값들 중 일부를 출력한다

- 또한 forEach 를 통해 하나씩 가져온 각각의 DTO 객체의 필드에 접근할땐 ${emp.필드명} 를 사용함

+ ${emp.empno} 는 내부적으로는 ${emp.getEmpno()} 이다

- 사번, 이름, 업무, 급여, 부서코드는 Emp 테이블, 부서코드, 부서명, 근무지는 Dept 테이블의 데이터

 

- 아래의 '부서목록' 클릭시 "deptlist.do" 로 요청한다  (위에서 설명했다)

- index.jsp 에서 요청했던 내용과 같음

 

- 사원 14명에 대한 정보를 출력한다


프로젝트 myBatis2 : 부서 입력폼

+ 전체 코드는 나중에 한번에 올림

- 입력폼으로 먼저 이동한 후, 거기서 입력을 해야한다, 입력 폼만 먼저 보자

 

- deptList.jsp (부서 목록) 에서 '부서입력' 클릭시 "deptInsertForm.do" 로 요청한다

		<a href="deptInsertForm.do" class="btn btn-info">부서입력</a>

- 부서 입력 폼으로 가게 됨

 

Controller 클래스 DeptController.java 에서 "deptInsertForm.do" 요청 부분만

	// 부서 등록폼
	@RequestMapping("deptInsertForm.do")
	public String deptInsertForm() {
		return "deptInsertForm";
	}

- 입력폼으로 가는데는 DB 연동이 필요하지 않음, Service 클래스가 아닌 바로 View 페이지로 간다

- 바로 View 파일인 /WEB-INF/views/deptInsertForm.jsp 로 이동한다

 

View 페이지 deptInsertForm.jsp

- deptInsertForm.jsp

 

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ include file="header.jsp"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<div class="container" align="center">
		<h2 class="text-primary">부서정보 입력</h2>
		<form action="deptInsert.do" method="post">
			<table class="table table-hover">
				<tr>
					<td>부서코드</td>
					<td><input type="number" maxlength="2" name="deptno"
						required="required" autofocus="autofocus"></td>
				</tr>
				<tr>
					<td>부서명</td>
					<td><input type="text" name="dname" required="required"></td>
				</tr>
				<tr>
					<td>근무지</td>
					<td><input type="text" name="loc" required="required"></td>
				</tr>
				<tr>
					<td colspan="2"><input type="submit" value="확인"></td>
				</tr>
			</table>
		</form>
	</div>
</body>
</html>

- 부서 입력 폼에서 입력을 한 후 "확인" 을 누르면 "deptInsert.do" 로 요청한다

- "deptInsert.do" 는 실제 입력(insert) 을 위한 요청임

- 각 입력양식의 name 값은 Dept DTO 의 컬럼명과 일치시켰다, "deptInsert.do" 요청을 받는 곳에서 Setter 메소드 직접 사용 없이 바로 DTO 객체에 넘어온 값들이 저장되도록 하기 위해서이다

 

- 부서 입력 폼으로 가보자

- 부서 입력 폼에서 입력을 한 후 "확인" 을 누르면 "deptInsert.do" 로 요청한다

 


프로젝트 myBatis2 : 부서 입력 (부서 추가 등록)

- 부서 입력 폼에서 입력을 한 후 "확인" 을 누르면 "deptInsert.do" 로 요청한다

 

Controller 클래스 DeptController.java 에서 "deptInsert.do" 요청 부분만

	// 부서 등록
	@RequestMapping("deptInsert.do")
	public String deptInsert(@ModelAttribute Dept dept, 
			                 Model model) {
		Dept dt = ds.select(dept.getDeptno()); // 부서번호 중복 검사
		if (dt == null) { // 부서번호가 중복이 아닌 경우
			int result = ds.insert(dept);
			model.addAttribute("result", result);
		} else { // 부서번호 중복
			model.addAttribute("msg", "이미 있는 데이터입니다");
			model.addAttribute("result", -1);
		}
		return "deptInsert";
	}

- 앞의 부서 입력폼에서 넘어온 3가지값을 @ModelAttribute 로 한번에 받아서 Dept DTO 객체 dept 에 저장함

- 부서번호가 중복되지 않을때만 가입을 시키기 위해 부서번호가 중복인지를 중복검사를 검색해야함

<부서번호 중복 검사>

- 위에서 주입된 Dept Service 객체 ds를 사용해서 Service 클래스의 select() 를 호출, 호출하며 부서번호를 넘겨준다 *아래에

<select() 에서 돌아왔을 때>

- 부서 번호로 DB 에 검색했을때 돌아오는 값이 없어야 부서번호가 중복이 아닌 경우이다 그래서 dt == null 을 확인

<부서 입력>

- dt == null 인 경우, 즉 중복이 아닌 경우에는 위에서 주입된 Dept Service 객체 ds를 사용해서 Service 클래스의 insert() 를 호출

- insert() 를 호출하며 사용자가 부서 입력 폼에서 입력했던 데이터를 저장한 객체 dept 를 넘겨준다

<insert() 에서 돌아왔을 때>

- dt == null 인 경우, 즉 중복이 아닌 경우 insert()를 수행 후 돌아오면, result 에 1 을 받게 된다, Model 객체에 "result" (1) 을 저장해서 detpInsert.jsp 로 이동

- dt != null 인 경우, 즉 중복인 경우에는 Model 객체에 "msg" 와 "result"(-1) 을 저장해서 deptInsert.jsp 로 이동

- 입력에 성공했는지 확인하기 위해 리턴값을 result 변수로 받는다, 그 값을 Model 객체에 저장해서 View 에 넘김

+ 그럼 deptInsert.jsp 에서는 "result" 값을 확인해서 조건식을 통해 성공과 실패 처리를 한다


<부서번호 중복 검사>

Service 클래스 DeptServiceImpl.java 에서 select() 메소드 부분만

	public Dept select(int deptno) {
		return dd.select(deptno);
	}

 

 

DAO 클래스 DeptDaoImpl.java 에서 select() 메소드 부분만

	public Dept select(int deptno) {
		return st.selectOne("deptns.select", deptno);
	}

- 매개변수로 받은 부서번호 deptno 를 넘겨준다

 

Mapper 파일 Dept.xml 에서 id 가 "select" 인 태그 부분만

	<select id="select" parameterType="int" resultType="dept">
		select * from dept where deptno=#{deptno}
	</select>

- 전달받은 부서번호를 통해 (#{deptno}) 데이터를 검색한다

- 이때 검색되는 데이터가 있으면 부서번호가 중복되는 것임

- 리턴 자료형은 Dept DTO 객체인 dept


<부서 입력>

Service 클래스 DeptServiceImpl.java 에서 insert() 메소드 부분만

	public int insert(Dept dept) {
		return dd.insert(dept);
	}

- 전달받은 insert 할 Dept DTO 객체 dept 를 DAO의 insert() 를 호출할때 매개변수로 전달한다

 

DAO 클래스 DeptDaoImpl.java 에서 insert() 메소드 부분만

	public int insert(Dept dept) {
		return st.insert("deptns.insert", dept);
	}

- 위에서 주입받은 SqlSessionTemplate 객체 st 를 사용해서 insert() 메소드 사용

- insert 할 데이터를 저장한 객체 dept 를 전달함

 

Mapper 파일 Dept.xml 에서 id 가 "insert" 인 태그 부분만

	<insert id="insert" parameterType="dept">
		insert into dept values(#{deptno},#{dname},#{loc})
	</insert>

- 넘어온 객체 dept 에서 부서코드,부서명,지역 을 가져와서 DB의 dept 테이블에 삽입한다

- 넘어온 값이 객체이므로 그 ${객체의필드명} 으로 값을 가져온다

ex) #{deptno} 는 dept.getDeptno() 와 같은 의미


View 페이지인 deptInsert.jsp

- deptInsert.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ include file="header.jsp"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<c:if test="${result > 0 }">
		<script type="text/javascript">
			alert("입력 성공");
			location.href = "deptList.do";
		</script>
	</c:if>
	<c:if test="${result == 0 }">
		<script type="text/javascript">
			alert("입력 실패");
			history.go(-1);
		</script>
	</c:if>
	<c:if test="${result == -1 }">
		<script type="text/javascript">
			alert("${msg}");
			history.go(-1);
		</script>
	</c:if>
</body>
</html>

- 부서번호가 중복된 경우 result 는 -1 이 되므로,  msg 를 출력하고 이전 페이지인 입력폼으로 돌아간다

- 부서 등록에 성공한 경우 result 는 1 이 되므로, "입력 성공" 을 출력하고 목록 페이지로 가기 위해 "deptList.do" 요청


- 부서를 새로 등록해보자

- 이때, 부서코드는 Primary Key 이므로 중복되선 안됨, 부서 입력시 10,20,30,40 을 부서코드로 쓰면 안된다

 

- 중복된 부서코드 입력시

 

- 중복되지 않은 부서코드 입력시

 


프로젝트 myBatis2 : 부서 정보 수정폼

- 부서 정보 수정을 위해선 먼저 수정폼으로 가야한다

- 부서 정보 수정폼에서는 부서의 상세 정보를 구해서 뿌려줘야함

- deptList.jsp (목록 페이지) 에서 "수정" 을 클릭해보자

<td><a href="deptUpdateForm.do?deptno=${dept.deptno }"
			class="btn btn-success">수정</a></td>

- "수정" 클릭시 "deptUpdateForm.do" 로 요청한다

- GET 방식으로 해당 부서의 부서코드 deptno 를 가져간다

 

Controller 클래스 DeptController.java 에서 "deptUpdateForm.do" 요청 부분만

	// 부서 정보 수정폼
	@RequestMapping("deptUpdateForm.do")
	public String deptUpdateForm(int deptno, Model model) {
		Dept dept = ds.select(deptno);
		model.addAttribute("dept", dept);
		return "deptUpdateForm";
	}

- 앞에서 넘어온 네임값과 이름이 같은 변수 deptno 에 앞에서 넘어온 부서코드가 바로 저장됨

- @RequestParam("deptno") 가 int deptno 앞에 생략되어있다

- Service 객체 ds 를 사용해서 Service 클래스의 select() 메소드 호출, 호출 시 부서번호를 전달

<돌아온 후>

- select() 에서 돌려받은 Dept DTO 객체 dept 를 Model 객체에 저장

+ 객체 dept 가 Model 에 저장되었으므로 View 에서 출력할땐 ${dept.필드명} 으로 출력

- /WEB-INF/views/deptUpdateForm.jsp 로 간다

 

Service 클래스 DeptServiceImpl.java 에서 select() 메소드 부분만

	public Dept select(int deptno) {
		return dd.select(deptno);
	}

 

DAO 클래스 DeptDaoImpl.java 에서 select() 메소드 부분만

	public Dept select(int deptno) {
		return st.selectOne("deptns.select", deptno);
	}

- 하나의 부서에 대한 상세정보를 검색해서 가져오므로 selectOne() 메소드 사용

 

Mapper 파일 Dept.xml 에서 id 가 "select" 인 태그 부분만

	<select id="select" parameterType="int" resultType="dept">
		select * from dept where deptno=#{deptno}
	</select>

- 검색되는 결과는 1개이다

- 결과는 Dept DTO 형으로 돌려준다, 그래서 resultType 에 "dept" 를 썼다

 

View 페이지 deptUpdateForm.jsp

- deptUpdateForm.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ include file="header.jsp"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<div class="container" align="center">
		<h2 class="text-primary">부서정보 수정</h2>
		<form action="deptUpdate.do" method="post">
			<input type="hidden" name="deptno" value="${dept.deptno }">
			<table class="table table-hover">
				<tr>
					<td>부서코드</td>
					<td>${dept.deptno}</td>
				</tr>
				<tr>
					<td>부서명</td>
					<td><input type="text" name="dname" required="required"
							   value="${dept.dname}"></td>
				</tr>
				<tr>
					<td>근무지</td>
					<td><input type="text" name="loc" required="required"
							   value="${dept.loc}"></td>
				</tr>
				<tr>
					<td colspan="2"><input type="submit" value="확인"></td>
				</tr>
			</table>
		</form>
	</div>
</body>
</html>

- 객체 dept 가 Model 에 저장되었으므로 View 에서는 ${dept.필드명} 으로 가져온다

- 부서 정보 수정폼에 수정할 정보를 입력하고 "확인" 을 누르면 "deptUpdate.do" 로 요청한다

- 부서코드는 수정할 수 없다, 수정할 수 있는 정보는 부서명과 근무지이다.

+ 현재 부서코드는 입력양식이 아니라 출력만 하고 있으므로 넘어가지 않음, 그래서 hidden 객체 사용

- 부서코드 또한 수정시에 전달해야하므로 hiddden 객체를 통해 "deptUpdate.do" 로 전달

 


프로젝트 myBatis2 : 부서 정보 수정

- 부서 정보 수정폼 deptUpdateForm.jsp 에 수정할 정보를 입력하고 "확인" 을 누르면 "deptUpdate.do" 로 요청한다

 

Controller 클래스 DeptController.java 에서 "deptUpdate.do" 요청 부분만

	// 부서 정보 수정
	@RequestMapping("deptUpdate.do")
	public String deptUpdate(Dept dept, Model model) {
		int result = ds.update(dept);
		model.addAttribute("result", result);
		return "deptUpdate";
	}

- 앞에서 폼을 통해 넘어온 입력값들을 한번에 @ModelAttribuate (생략) 을 통해 Dept DTO 객체 dept 에 세팅한다

- Service 클래스의 객체 ds 를 사용해서 Service 의 update() 메소드 호출, 호출 시 수정할 정보를 저장한 객체 dept 를 전달

<돌아온 후>

- 수정이 성공적으로 완료되면 update() 에서 1 을 받게 되고, 그걸 result 변수에 저장

- 그 result 변수를 Model 에 저장하고 deptUpdate.jsp 로 이동

 

Service 클래스 DeptServiceImpl.java 에서 update() 메소드 부분만

	public int update(Dept dept) {
		return dd.update(dept);
	}

 

DAO 클래스 DeptDaoImpl.java 에서 update() 메소드 부분만

 

	public int update(Dept dept) {
		return st.update("deptns.update", dept);
	}

- update() 수행 후 DB 에서 수정을 성공하면, update() 는 1 을 자동으로 리턴한다 (수정한 데이터 개수)

- Mapper 파일의 SQL문을 가져오는 것이고, 실제 SQL문의 실행은 여기 DAO의 update() 문이 실행한다

- 그 1 을 다시 Service 로 리턴함

 

Mapper 파일 Dept.xml 에서 id 가 "update" 인 태그 부분만

	<update id="update" parameterType="dept">
		update dept set dname=#{dname},loc=#{loc} where deptno=#{deptno}
	</update>

- 수정할 정보를 저장한 Dept DTO 객체가 전달된다

- #{dname} 은 dept.getDname() 과 같은 의미

 

View 페이지 deptUpdate.jsp

- deptUpdate.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ include file="header.jsp"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<c:if test="${result > 0 }">
		<script type="text/javascript">
			alert("수정 성공");
			location.href = "deptList.do";
		</script>
	</c:if>
	<c:if test="${result <= 0 }">
		<script type="text/javascript">
			alert("수정 실패");
			history.go(-1);
		</script>
	</c:if>
</body>
</html>

- 여기서 "result" 값을 사용해서 성공 / 실패 처리를 한다


 

 

+ 삭제로 넘어가기전 상세 페이지 잠시 보기

- 부서 목록 페이지에서 부서명을 클릭하면 상세 페이지에서 해당 부서 소속 사원들을 보여준다

- 상세페이지는 나중에 구현할 것

- 즉 20번 부서를 참조하는 사원 테이블의 데이터들이 있으므로 20번 부서가 삭제되지 않음

- 20번 부서 삭제 시도시


프로젝트 myBatis2 : 부서 삭제

- deptList.jsp (목록 페이지) 에서 부서 삭제를 해보자

<td><a href="deptDelete.do?deptno=${dept.deptno }"
			class="btn btn-danger">삭제</a></td>

- "삭제" 를 누르면 "deptDelete.do" 로 요청한다, 그때 부서 코드인 deptno 를 GET 방식으로 전달한다

 

삭제시 주의

- Emp 테이블과 Dept 테이블은 참조관계가 있다

- 현재 내가 입력한 50번 부서는 참조하는 Foreign Key 가 없으므로 부서 삭제가 된다

- 10,20,30 번 부서는 참조하는 Foreign Key 가 있으므로 삭제되지 않는다

- 10,20,30 번 부서는 사원 테이블 Emp 에서 참조한다

+ 40번 부서는 원래 참조하는 사원이 없으므로 삭제 된다

 

- 20 번 부서 삭제 시도시

 

Controller 클래스 DeptController.java 에서 "deptDelete.do" 요청 부분만

	// 부서 삭제
	@RequestMapping("deptDelete.do")
	public String deptDelete(int deptno, Model model) {
		int result = ds.delete(deptno);
		model.addAttribute("result", result);
		return "deptDelete";
	}

- 앞에서 넘어온 네임값과 이름이 같은 변수 deptno 에 앞에서 넘어온 부서코드가 바로 저장됨

- @RequestParam("deptno") 가 int deptno 앞에 생략되어있다

 

<돌아온 후>

- 삭제가 성공적으로 완료되었다면 delete() 메소드는 1 을 리턴함, 그 값을 result 변수에 저장

- result 변수를 Model 객체에 저장하고 deptDelete.jsp 로 이동

 

Service 클래스 DeptServiceImpl.java 에서 delete() 메소드 부분만

	public int delete(int deptno) {
		return dd.delete(deptno);
	}

 

DAO 클래스 DeptDaoImpl.java 에서 delete() 메소드 부분만

	public int delete(int deptno) {
		return st.delete("deptns.delete", deptno);
	}

- delete() 수행 후 DB 에서 수정을 성공하면, delete() 는 1 을 자동으로 리턴한다 삭제한 데이터 개수)

- Mapper 파일의 SQL문을 가져오는 것이고, 실제 SQL문의 실행은 여기 DAO의 delete() 문이 실행한다

- 그 1 을 다시 Service 로 리턴함

 

Mapper 파일 Dept.xml 에서 id 가 "delete" 인 태그 부분만

	<delete id="delete" parameterType="int">
		delete from dept where deptno=#{deptno}
	</delete>

- 넘어온 값은 부서코드이다, #{deptno} 로 사용함

- 해당 부서코드를 가진 부서를 dept 테이블에서 삭제

 

View 페이지 deptDelete.jsp

- deptDelete.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ include file="header.jsp"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<c:if test="${result > 0 }">
		<script type="text/javascript">
			alert("삭제 성공");
			location.href = "deptList.do";
		</script>
	</c:if>
	<c:if test="${result <= 0 }">
		<script type="text/javascript">
			alert("삭제 실패");
			location.href = "deptList.do";
		</script>
	</c:if>
</body>
</html>

- "result" 를 통해 삭제 성공 / 실패 처리를 한다

- 삭제 성공시 result 가 1 이므로, "삭제 성공" 을 출력하고, 목록 페이지로 가기 위해 deptList.do 로 요청

- 삭제 실패시 result 가 <=0 이므로 "삭제 실패" 를 출력하고, 목록 페이지로 가기 위해 deptList.do 로 요청

+ 목록 페이지에서 "삭제" 를 눌렀으므로 삭제 실패 후 돌아가는 것도 목록 페이지로 했다

 

- 참조하는 직원이 없는 50 번 부서를 삭제해보자

- 참조하는 직원(자식) 이 없으므로 지워짐

+ 삭제 후 다시 50번 부서를 등록하자(실습 위해)


 

문제

삭제시 주의 (중복)

- Emp 테이블과 Dept 테이블은 참조관계가 있다

- 현재 내가 입력한 50번 부서는 참조하는 Foreign Key 가 없으므로 부서 삭제가 된다

- 10,20,30 번 부서는 참조하는 Foreign Key 가 있으므로 삭제되지 않는다

- 10,20,30 번 부서는 사원 테이블 Emp 에서 참조한다

+ 40번 부서는 원래 참조하는 사원이 없으므로 삭제 된다

 

- 20 번 부서 삭제 시도시

 

문제 해결 방법

참조하는 자식이 있는 부모도 무조건 삭제하려면?

- 테이블 생성 또는 수정(Alert) 시에 On Delete Cascade 를 써야 한다

- 그럼 부서를 삭제할때 그 부서 소속 직원들 또한 연쇄적으로 삭제된다.



프로젝트 MyBatis2 : 부서 상세 페이지 (해당 부서의 직원들 목록)

- deptList.jsp 에서 부서명을 출력하는 코드에 링크가 걸려있다

<td><a href="empList.do?deptno=${dept.deptno}"
		 class="btn btn-info">${dept.dname}</a></td>

- 부서명 을 클릭시 "empList.do" 로 요청하고, 이때 그 부서 소속 직원을 구해와야 하므로 부서코드 deptno 를 가져간다

ex) 20 번 부서 부서명 클릭시 20 번 부서에 속하는 직원들을 select SQL문으로 구해와야한다

- 부서코드 deptno 를 통해 Emp 테이블에서 검색을 해야한다

 

Controller 클래스 EmpController.java 에서 "empList.do" 요청 부분만 + @Autowired 부분

	@Autowired
	private EmpService es;
	@Autowired
	private DeptService ds;

	// 부서 상세 페이지
	@RequestMapping("empList.do")
	public String empList(int deptno, Model model) {
		Dept dept = ds.select(deptno);
		List<Emp> list = es.list(deptno);
		model.addAttribute("dept", dept);
		model.addAttribute("list", list);
		return "emp/empList";
	}

- 앞에서 넘어온 네임값과 이름이 같은 변수 deptno 에 앞에서 넘어온 부서코드가 바로 저장됨

- @RequestParam("deptno") 가 int deptno 앞에 생략되어있다

- 상세 페이지에서 2가지 다른 정보를 DB 에서 구해와야하므로, 2개의 Service 객체를 @Autowired 로 구해옴

- Dept 테이블과 연동 위해 DeptService 객체, Emp 테이블과 연동 위해 EmpService 객체 구함

 

상세 페이지에서 2가지 정보를 구해야한다

1. Dept 테이블에서 부서 상세 정보를 구하기

- 아래에 표시한 부분(부서명) 을 뿌려주기 위해 부서 상세 정보도 구해야함

2. Emp 테이블에서 각 부서 소속 직원들 목록 구하기

<돌아온 후>

 

- 돌려 받은 부서 상세 정보를 저장한 Dept 객체 dept, 검색한 사원 정보들을 저장한 List 를 모두 Model 객체에 저장

- 그리고 /WEB-INF/views/emp/empList.jsp 로 이동


1. Dept 테이블에서 부서 상세 정보를 구하기

- select 는 많이 했으므로 코드 생략

 

2. Emp 테이블에서 각 부서 소속 직원들 목록 구하기

 

Service 클래스 EmpServiceImpl.java 에서 list() 메소드 부분만

	public List<Emp> list(int deptno) {
		return ed.list(deptno);
	}

 

DAO 클래스 EmpDaoImpl.java 에서 list() 메소드 부분만

	public List<Emp> list(int deptno) {
		return sst.selectList("empns.list", deptno);
	}

- Emp 테이블에서 특정 부서번호의 직원을 구할 것이므로 여러 데이터를 검색하는 것임, selectList() 를 쓴다

 

Mapper 파일 Emp.xml 에서 id 가 "list" 인 태그 부분만 + resultMap 

	<!-- Use type aliases to avoid typing the full classname every time. -->
	<resultMap id="empResult"    	type="emp">
		<result property="empno" 	column="empno" />
		<result property="ename"  	column="ename" />
		<result property="job"		column="job" />
		<result property="mgr" 		column="mgr" />
		<result property="hiredate" column="hiredate" />
		<result property="sal"	  	column="sal" />
		<result property="comm"	   	column="comm" />
		<result property="deptno"   column="deptno" />
		<result property="dname"	column="dname" />
		<result property="loc"   	column="loc" />
	</resultMap>
	<select id="list" parameterType="int" resultMap="empResult">
		select * from emp where deptno=#{deptno} order by empno
	</select>

- 현재는 꼭 resultMap 을 쓰지 않아도 된다

- Emp 테이블에서 특정 deptno 를 가진 사원들의 데이터를 모두 검색한다

- order by empno 에 의해 사원번호를 기준으로 오름차순 정렬된다

 

View 페이지 empList.jsp

- emp/empList.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ include file="../header.jsp"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<script type="text/javascript">
	$(function() {
		$('#list').load('deptList.do');
	});
</script>
</head>
<body>
	<div class="container" align="center">
		<h2 class="text-primary">${dept.dname}직원목록</h2>
		<table class="table table-striped">
			<tr>
				<td>사번</td>
				<td>이름</td>
				<td>업무</td>
				<td>급여</td>
				<td>부서코드</td>
			</tr>
			<tr>
				<c:if test="${empty list }">
					<tr>
						<td colspan="5">직원이 없는 부서입니다</td>
					</tr>
				</c:if>
				<c:if test="${not empty list }">
					<c:forEach var="emp" items="${list }">
						<tr>
							<td>${emp.empno }</td>
							<td><a href="empView.do?empno=${emp.empno}"
								   class="btn btn-info">${emp.ename}</a></td>
							<td>${emp.job}</td>
							<td>${emp.sal}</td>
							<td>${emp.deptno}</td>
						</tr>
					</c:forEach>
				</c:if>
		</table>
		<a href="deptList.do" class="btn btn-success">부서목록</a> 
		<a href="empInsertForm.do" class="btn btn-success">직원 입력</a>
		<div id="list"></div>
	</div>
</body>
</html>

- Model 객체에 저장된 해당 부서의 상세 정보 dept 를 사용해서 ${dept.dname} 으로 상단에 부서명을 출력

- Model 객체에 저장된 해당 부서 소속 사원 목록인 list 를 사용해서 ${list} 를 forEach 의 items 에 써서 사원 데이터를 하나씩 가져온다

- forEach의 emp 변수를 통해 하나씩 가져온 사원 데이터에서 ${emp.empno} 등을 활용해 사원 정보를 출력

- 부서 상세페이지에 출력된 사원 정보에서 사원명을 클릭하면 해당 사원의 상세정보를 보여주기 위해 "empView.do" 로 요청한다 

- 부서 상세페이지에서 "직원 입력" 을 누르면 직원을 새로 등록할 수 있는 직원 입력폼으로 가기 위해 "empInsertForm.do" 로 요청한다

 

아래에 부서 목록 뿌리기 (비동기 처리) (empList.jsp 부분)

- 부서 상세 페이지 하단에도 목록 페이지와 같은 부서 목록을 출력하고 있다

<script type="text/javascript">
	$(function() {
		// deptList.do 요청 결과를 div 태그 사이에 출력
		$('#list').load('deptList.do');
	});
</script>

- id 가 "list"인 div 태그를 구해와서 ajax 의 load() 함수를 사용해서 "deptList.do" 요청 결과 전체를 그 div 태그 사이에 출력

+ 이전에도 비슷한 내용을 했다 : https://laker99.tistory.com/146 

(검색어 : 아래에 목록 출력하기 (비동기 처리) (deptView.jsp 부분))

 


- 소속 직원이 없는 부서인 50번 부서 상세 페이지

- 참조하는 직원이 없는 부서이므로 삭제 가능하다

 

- 소속 직원이 있는 20번 부서의 상세 페이지

- 사원을 출력할때 사번이 빠른 순서대로 출력된다(오름차순)


프로젝트 MyBatis2 : 사원 등록폼

- 사원 등록 전 사원 등록폼 부터 가야한다

- 부서 상세페이지에서 "직원 입력" 을 누르면 사원을 새로 등록할 수 있는 사원 입력폼으로 가기 위해 "empInsertForm.do" 로 요청한다

		<a href="empInsertForm.do" class="btn btn-success">직원 입력</a>

- "직원 입력" 클릭시 "empInsertForm.do" 로 요청

 

직원 등록폼에서 DB 와 연동해서 할 일

- 직원 입력폼에서 직원을 등록할때 관리자, 부서코드를 가져와야함

- 사원, 부서는 추가할 수 있으므로 고정된 리스트가 아님, 그래서 DB 와 연동해서 가져와야함

- 즉 사원 목록, 부서 목록을 가져와서 이 직원 입력폼에 뿌려야한다

 

Controller 클래스 EmpController.java 에서 "empInsertForm.do" 요청 부분만

	@RequestMapping("empInsertForm.do")
	public String empInsertForm(Model model) {
		List<Dept> deptList = ds.list();
		List<Emp> empList = es.empList();
		model.addAttribute("deptList", deptList);
		model.addAttribute("empList", empList);
		return "emp/empInsertForm";
	}

- 사원을 새로 등록할때 사번이 중복되지 않은지 중복검사를 해야함

DB 에서 가져와야 할 것 2가지

1. 사원 목록

- 관리자를 선택할때 관리자의 사원명은 바뀔 수 있는 값(사원이 추가될 수도 있다) 이므로, 사원명 리스트를 DB 와 연동해서 가져와야함

2. 부서 목록

- 부서명을 선택할때 부서명은 바뀔 수 있는 값(부서가 추가될 수도 있다)이므로, 부서명 리스트를 DB 와 연동해서 가져와야함

- 사원, 부서는 추가될 수 있으므로 DB와 연동해서 그 리스트를 가져와야함

- 즉 부서명, 사원명 을 가져오기 위해 Dept 테이블, Emp 테이블 둘 다 연동해서 부서 목록, 사원 목록 을 가져와야함

 

- 부서 목록 가져오기는 위에서 했으므로 코드 생략

- 사원 목록 가져오기는 위에서 한것과 유사하므로 코드 생략

- 특정 부서의 사원이 아니라 전체 사원을 구한다는 점만 다르다

<돌아온 후>

- 사원 전체 목록, 부서 전체 목록을 모두 Model 객체에 저장 후 empInsertForm.jsp 로 이동

 

- View 페이지인 사원 등록 폼으로 가자

 

View 페이지 empInsert.jsp

- emp/empInsert.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ include file="../header.jsp"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
<script type="text/javascript">
	$(function() {
		// 사원번호 중복검사
		$('#dupCheck').click(function() {
			var empno = $('#empno').val();
			if (!empno) {
				alert('사번 입력후에 체크');
				$('#empno').focus();
				return false;
			}// $.post("요청이름","전달될 값","콜백함수");
			$.post('dupCheck.do', 'empno=' + empno, function(msg) {
				alert(msg);
			});
		});
	});
</script>
</head>
<body>
	<div class="container">
		<h2 class="text-primary">직원 등록</h2>
		<form action="empInsert.do" method="post">
			<table class="table table-bordered">
				<tr>
					<td>사번</td>
					<td><input type="number" name="empno" required="required"
							   id="empno" autofocus="autofocus"> 
						<input type="button" value="중복" id="dupCheck"></td>
				</tr>
				<tr>
					<td>이름</td>
					<td><input type="text" name="ename" required="required"></td>
				</tr>
				<tr>
					<td>업무</td>
					<td><input type="text" name="job" required="required"></td>
				</tr>
				<tr>
					<td>관리자</td>
					<td><select name="mgr">
							<c:forEach var="emp" items="${empList }">
								<option value="${emp.empno}">${emp.ename}</option>
							</c:forEach>
					    </select>
					</td>
				</tr>
				<tr>
					<td>입사일</td>
					<td><input type="date" name="hiredate" required="required"></td>
				</tr>
				<tr>
					<td>급여</td>
					<td><input type="number" name="sal" required="required"></td>
				</tr>
				<tr>
					<td>보너스</td>
					<td><input type="number" name="comm" required="required"></td>
				</tr>
				<tr>
					<td>부서코드</td>
					<td><select name="deptno">
							<c:forEach var="dept" items="${deptList }">
								<option value="${dept.deptno}">${dept.dname}</option>
							</c:forEach>
						</select>
					</td>
				</tr>
				<tr>
					<td colspan="2"><input type="submit" value="확인"></td>
				</tr>
			</table>
		</form>
	</div>
</body>
</html>

- 가져온 사원, 부서 목록 forEach 태그의 items 태그에 ${empList} , ${deptList} 에 넣는다

- 그 forEach 태그에서 사원 1명 정보를 담은 객체를 가져와서, select 태그 안의 option 에 필요한 필드를 가져와서 뿌리기

- 그 forEach 태그에서 부서 1개 정보를 담은 객체를 가져와서, select 태그 안의 option 에 필요한 필드를 가져와서 뿌리기

<관리자>

- 가져온 관리자 목록에서 관리자를 1명씩 가져와서 그 관리자의 사원명, 사원번호를 꺼낸다

- 관리자를 선택할때, 사원명이 출력되지만 value 는 관리자(상사) 의 사원번호인 empno 이다

- 즉 실제 DB 에 저장되는 값은 관리자의 사원명이 아닌 관리자의 사원번호이다!

<입사일>

- input type 을 "date" 로만 해도 캘린더를 선택할 수 있는 양식이 나타남

<부서>

- 부서를 선택할때, 부서명이 출력되지만 value 는 부서의 부서번호은 deptno 이다

- 즉 실제 DB 에 저장되는 값은 부서의 부서명이 아닌 부서의 부서번호이다!

<"확인" 누른 후>

- 직원 입력폼 empInsertForm.jsp 에서 입력을 한 후 "확인" 을 누르면 "empInsert.do" 로 요청한다

- 이때 이 입력폼에 입력된 값들을 가지고 간다


프로젝트 myBatis2 : 사원 등록폼에서 사번 중복검사 (비동기 처리)

사원 번호 중복 검사 (empInsertForm.jsp 부분)

<script type="text/javascript">
	$(function() {
		// 사원번호 중복검사
		$('#dupCheck').click(function() {
			var empno = $('#empno').val();
			if (!empno) {
				alert('사번 입력후에 체크');
				$('#empno').focus();
				return false;
			}// $.post("요청이름","전달될 값","콜백함수");
			$.post('dupCheck.do', 'empno=' + empno, function(msg) {
				alert(msg);
			});
		});
	});
</script>

- DB 에서 이 사번이 사용가능한지 중복검사를 해야함, 이때 이걸 비동기로 처리하려고 한다

- "중복" 버튼 클릭시 click() 함수 실행됨, 사용자가 입력한 값을 구해오고 그 값이 null 인지 유효성 검사

- 입력 값이 있다면 ajax 기능을 사용해서 post() 함수로 사번 중복검사를 한다

+ 비동기 처리 : 페이지가 바뀌지 않고 처리

 

- 요청 이름값은 "dupCheck.do" 로 한다, 그리고 사용자가 입력한 사번 값을 키 밸류 형식(키=밸류) 으로 전달하며 요청

- 콜백함수로 메세지를 돌려받아서 alert 에 출력

 

ajax post() 함수에 대한 자세한 내용

https://laker99.tistory.com/106 (검색어 : $.post() 함수)

 

Controller 클래스 EmpController.java 에서 "dubCheck.do" 요청 부분만

	// 사원번호 중복검사
	@RequestMapping("dupCheck.do")
	public String dupCheck(int empno, Model model) {
		System.out.println("empno:"+empno);
		Emp emp = es.select(empno);
		String msg = "";
		if (emp != null) // 중복 사원번호
			msg = "이미 있는 데이터입니다";
		else // 사용 가능한 사원번호
			msg = "사용 가능한 사번 입니다";
		model.addAttribute("msg", msg);
		
		// 웹 브라우저에 출력되는 결과가 callback 함수로 리턴된다.
		return "emp/dupCheck";
	}

- 앞에서 키 밸류 형식으로 전달된 사번이, 키값과 같은 이름인 empno 변수로 바로 저장된다

- @RequestParam("empno") 가 int empno 앞에 생략되었다

- 아래에서 empno 를 출력해보면 사용자가 입력한 사번이 출력됨

 

- 그 empno 로 Service 클래스의 select() 를 호출해서 상세 정보를 구해와서 Emp 객체 emp 에 저장

- 만약 이 emp 가 null 이 아니면, 즉 데이터가 존재하면 해당 사번에 해당하는 데이터가 있다는 의미, 즉 중복인 사원번호라는 의미이다

- 사원번호가 중복이면 "이미 있는 데이터입니다", 중복이 아니면 "사용 가능한 사번입니다" 를 msg 변수에 저장

- 브라우저에 출력되는 값이 콜백함수로 리턴되기때문에 출력을 하는 View 페이지로 msg 를 넘겨서 거기서 브라우저 출력을 하자, 그럼 거기서 출력되는 값이 콜백함수로 리턴됨

+ 여기서 out 객체를 생성해서 출력하면 코드가 지저분해짐, msg 출력만을 하기 위한 View 페이지 dubCheck.jsp 를 따로 만든다

 

VIew 페이지 dubCheck.jsp

- dubCheck.jsp

${msg}

- 메세지가 브라우저에 출력되므로 그 결과가 콜백함수로 리턴된다

 

- 다시 empInsertForm.jsp 부분을 보자

<script type="text/javascript">
	$(function() {
		// 사원번호 중복검사
		$('#dupCheck').click(function() {
			var empno = $('#empno').val();
			if (!empno) {
				alert('사번 입력후에 체크');
				$('#empno').focus();
				return false;
			}// $.post("요청이름","전달될 값","콜백함수");
			$.post('dupCheck.do', 'empno=' + empno, function(msg) {
				alert(msg);
			});
		});
	});
</script>

- 콜백함수로 값을 받아서 alert 으로 메세지를 뿌려준다

- 그럼 중복일떄는 "이미 있는 데이터입니다", 중복이 아닐때는 "사용 가능한 사번입니다" 출력


프로젝트 myBatis2 : 사원 등록

- 사원 입력폼 empInsertForm.jsp 에서 입력을 한 후 "확인" 을 누르면 "empInsert.do" 로 요청한다

 

Controller 클래스 EmpController.java 에서 "empInsert.do" 요청 부분만

	// 사원 등록
	@RequestMapping("empInsert.do")
//	public String empInsert(Emp emp, String hiredate1, Model model) {
	public String empInsert(Emp emp, Model model) {
//		emp.setHiredate(Date.valueOf(hiredate1)); // String -> Date 형변환
		int result = es.insert(emp);
		model.addAttribute("result", result);
		model.addAttribute("deptno", emp.getDeptno());
		return "emp/empInsert";
	}

- 이전의 사원 등록폼에서 넘어온 값들을 한번에 @ModelAttribute (생략) 을 통해 DTO Emp 객체 emp 에 세팅

- Service 클래스의 insert() 메소드 호출, 호출하며 사원 등록폼에서 넘어온 값을 저장하는 객체 emp 를 넘겨줌

<돌아온 후>

- insert() 가 호출되어 삽입 성공하면 1 을 반환한다, result 에 그 값을 저장하고 Model 객체에 result 를 저장

- View 인 empInsert.jsp 에서는 삽입 성공시에, 그 사원의 부서 소속 모든 사원을 출력하는 부서 상세페이지로 이동할 것임

- 그러므로 삽입된 사원의 부서번호인 deptno 또한 가져가야한다, Model 객체에 저장 

- /WEB-INF/views/emp/empInsert.jsp 로 이동

 

Service 클래스 EmpServiceImpl.java 에서 insert() 메소드 부분만

	public int insert(Emp emp) {
		return ed.insert(emp);
	}

 

DAO클래스 EmpDaoImpl.java 에서 insert() 메소드 부분만

	public int insert(Emp emp) {
		return sst.insert("empns.insert", emp);
	}

 

Mapper 파일 Emp.xml 에서 id 가 "insert" 인 태그 부분만

	<insert id="insert" parameterType="emp">
		insert into emp values(#{empno},#{ename},#{job},#{mgr},
			#{hiredate},#{sal},#{comm},#{deptno})
	</insert>

- 객체 emp 를 통해 넘어온 값들을 사용해서 insert 를 해준다

- #{empno} 는 emp.getEmpno() 와 같은 의미

- 입사일(hiredate) 은 캘린더에서 선택한 값이었지만, 객체 emp 에서 입사일은 Date 형으로 저장되어 있다

- Emp DTO 클래스에 Date 형으로 되어있기 때문이다

 

View 페이지 empInsert.jsp

- emp/empInsert.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ include file="../header.jsp"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<c:if test="${result > 0 }">
		<script type="text/javascript">
			alert("입력 성공");
			location.href = "empList.do?deptno=${deptno}";
		</script>
	</c:if>
	<c:if test="${result <= 0 }">
		<script type="text/javascript">
			alert("입력 실패");
			history.go(-1);
		</script>
	</c:if>
</body>
</html>

- 삽입 성공시 result 가 1 이므로 "입력 성공" 출력 후 그 사원의 부서 상세 페이지로 이동하기 위해 "empList.do" 로 요청

- 요청하면서, 부서 상세 페이지에서는 해당 부서 소속 모든 사원 목록을 보여주므로 deptno 도 GET 으로 전달한다


- 사원 등록은 아직 하지 않았다

- 내일 등록하자

복습

web.xml

- 프로젝트 환경설정 파일

- Dynamic, Maven, Spring 프로젝트 모두 가지고 있는 환경설정 파일

- Apache Tomcat 이 시작되면 자동으로 실행된다

- Spring 환경설정 파일 2개를 불러서 연쇄적으로 실행시켜준다

 

Controller 클래스

- 여러개를 만들 수 있다, 단 @RequestMapping 이름값은 다르게 설정해야함

- 한가지만 만들어 사용할 수도 있다

 

@RequestParam 어노테이션

- 자동으로 앞에서 넘어온 값을 받아서 뒤의 변수에 저장한다

- 앞에서 넘어온 name 값과 매개변수 변수명이 같으면 @RequestParam 을 생략해도 된다

- 형변환할 필요 없이 @RequestParam 뒤에 쓰는 자료형에 자동 형변환 된다

ex) 여기선 String 형으로 자동형변환되어 저장됨

 

@ModelAttribute 어노테이션

- 한꺼번에 값을 받아서 @ModelAttribute 뒤에서 선언된 매개변수인 DTO 객체에 저장해준다

- 다만, DTO 객체의 필드명과 입력양식의 name 값들이 일치할때만 가능하다!

- 그땐 @ModelAttribute 생략도 가능하다

 

@Autowired

- @inject 어노테이션과 같은 역할

- 이걸 필요한 객체이자 프로퍼티 위에 쓰면 자동으로 객체 생성 및 주입까지 완료된다

- 객체 생성 및 주입 시점 : web.xml 에서 servlet-context.xml 을 불러올떄 모두 완료됨

+ servlet-context.xml 안에서 base-package 를 설정함

 

업로드 기능

- Spring 에선 fileupload 라이브러리를 많이 써서 첨부파일 업로드를 한다

 

cos 라이브러리 vs fileupload 라이브러리

cos 라이브러리

- 중복 문제를 자동으로 해결해준다

- 업로드 시켜주는 코드가 따로 없다

- 객체를 생성하면 업로드 된다

 

fileupload 라이브러리

- 중복 문제를 개발자가 해결해야한다

- 업로드 하는 코드가 따로 있다

 

ViewResolver

- servlet-context.xml 부분

	<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
	<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<beans:property name="prefix" value="/WEB-INF/views/" />
		<beans:property name="suffix" value=".jsp" />
	</beans:bean>

- servlet-context.xml 안에서 View 파일들의 위치를 설정함

- 여기선 /WEB-INF/views/ 폴더로 지정

- View 파일들이 저장될 최상위 디렉토리를 정의


Spring Web Application 예제 2 (이어서)

프로젝트 ch07

 

파일 설명

- LoginCheck.java : Intercepter Class (구현 클래스)

- UploadController.java : Controller Class

 

인터셉터 (Interceptor)

- 각 페이지마다 세션이 있는지 확인하고 로그인폼으로 보내는 대신 인터셉터를 사용한다

- 어떤 이름으로 요청이 들어올때만 DispatcherServlet 에서 Controller 로 가기 전에 해당 요청을 가로챈다

- 주로 로그인 해야만 쓸 수 있는 기능에 대해 (정보수정, 회원탈퇴, 로그아웃) 매핑을 잡아서 세션이 있는지 확인하기 위해 인터셉터 사용

 

인터셉터 설정

- servlet-context.xml 부분

1. 인터셉터 클래스로 bean 을 하나 만든다

2. 인터셉터 클래스가 어떤 경우에 동작할지 매핑을 아래에서 수행한다

3. "/upload.do" 로 요청할때 인터셉터를 동작시키고, 위에서 만든 bean 의 id 값과 연결시켜줌

- 주로 로그인 해야만 쓸 수 있는 기능에 대해 (정보수정, 회원탈퇴, 로그아웃) 매핑을 잡아서 세션이 있는지 확인하기 위해 인터셉터 사용

 

인터셉터 구현 방법

1. abstract class HandlerInterceptorAdapter 클래스를 상속 받아서 구현

- 3개의 메소드 중 필요한 메소드만 오버라이딩

2. interface HandlerInterceptor 인터페이스를 상속 받아서 구현

- 인터페이스를 상속받았을때는 3개의 메소드를 모두 메소드 오버라이딩 해야한다

 

- 다음으로 3개의 메소드를 오버라이딩해서 원하는 기능을 작성

- boolean preHandle()

- void postHandle()

- void afterCompletion()

- 메소드 오버라이딩 했으므로 이름과 형식을 그대로 따라야함

 

인터셉터 메소드 호출 시점

- preHandle() 메소드 : Client 가 요청했을때 DispatcherServlet -> Controller 로 가기전에 호출됨

ex) 여기서는 preHandle() 안에, 세션이 있는지 확인하고 없다면 로그인폼으로 보내는 코드를 작성

- postHandle() 메소드 : Controller 에서 돌아가려고 Dispatcher 로 갈때 호출됨 

- afterCompletion() 메소드 : 완전히 View 페이지에 간 후에 호출됨

 

- LoginCheck.java

package com.ch.ch07;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;

import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;

public class LoginCheck implements HandlerInterceptor {
	
	// DispatcherServlet의 화면 처리가 완료된 상태에서 처리
	public void afterCompletion(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, Exception arg3)
			throws Exception {
	}

	// 지정된 컨트롤러의 동작 이후에 처리, Spring MVC의 Front Controller인 DispatcherServlet의 화면 처리가 완료된 상태에서 처리
	public void postHandle(HttpServletRequest arg0, HttpServletResponse arg1, Object arg2, ModelAndView arg3)
			throws Exception {
	}

	// 지정된 컨트롤러의 동작 이전에 가로채는 역할 (세션이 없으면, 로그인 폼으로 이동 하도록 해준다 )
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object arg2) throws Exception {
		HttpSession session = request.getSession();
		if (session.getAttribute("id") == null) { // 세션이 없으면
			response.sendRedirect("loginForm.do"); // loginform 으로 이동
			return false;
		}
		return true;
	}
}

- preHandle() 메소드 안에서 세션 객체를 구한다

- 세션에 id 값이 있는지 확인 후 없다면 비정상 요청으로 판단해 loginForm.do 로 요청, 그 후 return false 하면 메소드를 빠져나감

- 세션값이 있다면 return true 를 통해, 정상적으로 원래 가던 Controller 클래스로 간다

- 로그인 해야만 쓸 수 있는 기능에 대해 (정보수정, 회원탈퇴, 로그아웃) 매핑을 잡아서 세션이 있는지 확인하기 위해 인터셉터 사용

 

인터셉터 활용

- index.jsp 를 실행하면 "upload.do" 로 요청

- Controller 클래스로 가기 전에 인터셉터가 가로챔

- 세션이 없으므로 loginForm.do 로 요청해서 loginform.jsp 로 이동한다

- 그래서 index.jsp 실행시 로그인 폼으로 가게 되는 것이다

 


"upload.do" 요청이 들어왔을때 가는 곳

- 같은 이름값 요청이지만 가는 곳이 다르다

- UploadController.java 부분

- 요청이름값이 같더라도 인터셉터 영향 또는 어떤 방식(GET/POST) 으로 요청되었는지에 따라 다른 내용이 실행된다

1) index.jsp 에서 "upload.do" 요청시 인터셉터가 잡아서 upload.jsp 가 아닌 loginform.jsp 로 간다

2) 로그인 성공 후 '업로드' 를 눌러 "upload.do" 요청시, 세션이 있으므로 인터셉터가 잡지못하고 Controller 클래스에 온다, 그 후 위의 @RequestMapping 에 의해 uploadForm() 을 실행하고 upload.jsp 로 가게된다

3) upload.jsp 에서 파일을 선택 후 '확인' 하면 "upload.do" 요청이 되고, 세션이 있으므로 인터셉터가 잡지 못하고 Controller 클래스에 온다, 그 후 아래의 @RequestMapping 에 의해 upload() 를 실행하고 uploadResult.jsp 로 가게 된다


틀린 비밀번호 입력시

1. loginForm.jsp 에서 비밀번호 입력하고 '확인'을 누르면 "login.do" 로 요청된다

2. Controller 에서 Model 객체에 "msg" 값을 저장하고, loginForm.jsp 로 이동한다

+ Controller 클래스에서는 로그인폼에서 온 입력값을 @RequestParam 으로 바로 매개변수에 저장

3. 다시 loginForm.jsp 로 이동했을때는 아래의 if 태그를 만족해서 msg 인 "똑바로 입력해" 를 출력

- 즉 한번 Controller 클래스로 갔다와야 msg 가 있으므로 msg 를 출력한다

- 처음 index.jsp 를 실행해서 loginForm.jsp 로 왔을때는 이 msg 가 없으므로 출력되지 않음

 

맞는 비밀번호 입력시

1. loginForm.jsp 에서 비밀번호 입력하고 '확인'을 누르면 "login.do" 로 요청된다

2. Controller 에서 id 값을 세션영역 공유설정하고, loginSuccess.jsp 로 간다

- 여기서부터 세션 영역이 시작된다

+ 매개변수에 Session 객체를 적기만 하면 자동으로 Session 객체가 구해진다, Spring 에선 getSession() 을 쓸 필요 없다

- 로그인 성공해서 세션 영역이 시작되었다

- 위 사진은 loginSuccess.jsp 이다

 

- loginSuccess.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ include file="header.jsp"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<h2 class="text-primary">로그인 성공</h2>
	<a href="upload.do" class="button button-success">업로드</a>
</body>
</html>

 

- '업로드' 메뉴를 선택하면 "upload.do" 로 요청한다

 

- "upload.do" 로 요청했으므로 인터셉터로 간다

+ 메소드 오버라이딩이므로, 메소드 형식을 유지해야한다, 그래서 Session 객체를 매개변수에 써서 자동으로 구하지 않고 직접 구하고 있다

- 세션값이 존재하므로 return true; 되어 원래 가던 Controller 클래스로 간다

- GET 방식 요청이므로 위의 uploadForm() 코드가 실행된다

- upload.jsp 로 간다

 

- upload.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ include file="header.jsp"%>

<!DOCTYPE html>
<html>
<head>
<meta charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
	<div class="container">
		<h2 class="text-primary">파일 업로드</h2>
		<form action="upload.do" method="post" enctype="multipart/form-data">
			<table class="table table-bordered">
				<tr>
					<th>파일</th>
					<td><input type="file" name="file"></td>
				</tr>
				<tr>
					<th colspan="2"><input type="submit" value="확인"></th>
				</tr>
			</table>
		</form>
	</div>
</body>
</html>

- 첨부파일을 전송할때는 form 태그 안에 enctype="multipart/form-data" 를 반드시 쓴다

+ 첨부파일이 없을때는 enctype="multipart/form-data" 를 써선 안된다

- 첨부파일을 전송할때는 반드시 form 태그의 method 를 "post" 로 설정해야한다

- 첨부파일의 input type 은 "file" 이다

+ 파일의 name 값이 "file" 임을 주의해서 보자

- 첨부파일을 선택하고 '확인' 을 클릭시 action 인 "upload.do" 로 요청된다

- "upload.do" 로 요청했으므로 인터셉터에 걸린다

- 세션값이 존재하므로 return true; 되어 원래 가던 Controller 클래스로 간다

- 이번에는 POST 방식으로 요청이 왔으므로 아래의 upload() 가 실행된다

- 이 upload() 에서 첨부파일을 업로드 한다, 자세히 보자

 

첨부파일 업로드

- UploadController.java 에서 upload() 메소드 부분만

	@RequestMapping(value = "upload.do", method = RequestMethod.POST)
	public String upload(@RequestParam("file") MultipartFile mf, Model model, HttpSession session)
			throws IllegalStateException, IOException {
		
		String fileName = mf.getOriginalFilename();
		int fileSize = (int) mf.getSize(); // 단위 : Byte
		
		// 첨부파일 업로드
		// mf.transferTo(new File("/path/"+fileName));
		String path = session.getServletContext().getRealPath("/upload");
		System.out.println("path : " + path);
		
		FileOutputStream fos = new FileOutputStream(path + "/" + fileName);
		fos.write(mf.getBytes());
		fos.close();
		
		model.addAttribute("fileName", fileName);
		model.addAttribute("fileSize", fileSize);
		return "uploadResult";
	}

- fileupload 라이브러리 사용해서 첨부파일 업로드

- MultipartFile 은 fileupload 라이브러리에서 지원되는 클래스명, 파일 업로드를 하기 위한 클래스이며 import 되어있음

import org.springframework.web.multipart.MultipartFile;

- @RequestParam("file") 을 통해 MultipartFile 객체 mf 를 매개변수로 받아온다, upload.jsp 폼에서 사용자가 선택한 파일을 구해온 것

- 그럼 이 mf 는 사용자가 선택한 첨부파일에 대한 정보를 이미 가지고 있게 된다

 

파일명, 파일크기, 첨부파일 외 다른 입력값 구하기 (UploadController.java 부분)

		String fileName = mf.getOriginalFilename(); // 파일명
		int fileSize = (int) mf.getSize(); // 파일크기, 단위 : Byte

- mf.getOriginalFilename() 으로 사용자가 저장한 원래 파일명을 구해서 변수 fileName 저장

- mf.getSize() 는 첨부파일의 크기를 반환, 리턴자료형이 LONG 형(8 byte) 이다, 그래서 int 형으로 다운캐스팅해서 int 변수 fileSize 에 저장, 파일 크기 단위는 Byte 이다

+ 현재는 첨부파일 외 다른 입력값이 없다

- 만약 작성자명, 이메일주소, 제목, 내용등의 입력값들이 있다면 @ModelAttribute 어노테이션으로 DTO 객체에 세팅해서 가져오면 된다

 

 

실제 업로드되는 폴더 경로 구하기 (UploadController.java 부분)

		String path = session.getServletContext().getRealPath("/upload");

- Session 객체를 통해 getServletContext().getRealPath("/upload") 로 upload 폴더의 진짜 절대경로를 구해서 path 변수에 저장한다

 

업로드하는 방법 2가지 (UploadController.java 부분)

1.

mf.transferTo(new File("/path/"+fileName));

- 1줄로 처리 가능, 업로드 시켜주는 코드이다

2.

		FileOutputStream fos = new FileOutputStream(path + "/" + fileName);
		fos.write(mf.getBytes());
		fos.close();

- FileOutputStream 객체를 생성한다

- mf.getBytes() 로 파일 내용을 읽어옴

- 읽어온 내용을 write() 로 쓰고 객체를 닫음

 

- 다음은 첨부파일명과 파일크기를 Model 객체에 저장, 나중에 브라우저에 출력시키기 위해서 저장함

		model.addAttribute("fileName", fileName);
		model.addAttribute("fileSize", fileSize);

- 다음은 출력을 위해 uploadResult.jsp 로 이동

 

- uploadResult.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
	
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<h2>업로드 성공</h2>
	파일명 : ${fileName }
	<p>파일크기 : ${fileSize }
</body>
</html>

- EL 로 파일명과 파일크기를 출력한다

 

- UploadController.java 에서 upload() 메소드 부분에서 upload 폴더의 진짜 경로를 구해서 출력하는 코드가 있다

- 콘솔창을 확인해서 진짜 경로를 확인해보자

- 콘솔창 확인

C:\Users\admin\Documents\workspace-sts-3.9.11.RELEASE\.metadata\.plugins\org.eclipse.wst.server.core\tmp0\wtpwebapps\ch07\upload

- 여기가 진짜 첨부파일이 업로드 되는 upload 폴더의 절대경로이다

- 탐색기에서 확인 가능


MyBatis 연동해서 Oracle DB 연동

 

Spring - MyBatis 연동 예제 1

프로젝트 myBatis1

 

실습 준비

- 먼저 scott 계정이 활성화 되어있어야한다

- 다음으로 클라우드의 프로젝트 import 하기

 

프로젝트 구조


파일들 살펴보기 : pom.xml

 

Spring 버전

- pom.xml 을 보면 Spring 버전이 나타나있음

- Maven Repository 페이지에서 Spring 최신버전을 확인해 볼 수 있다

- 버전을 높이면 다시 최신버전을 로컬저장소로 다운받아서 사용한다

 

Connection Pool 을 쓰기 위한 라이브러리

		<!-- dbcp jdbc connection pool -->
		<dependency>
			<groupId>commons-dbcp</groupId>
			<artifactId>commons-dbcp</artifactId>
			<version>1.2.2</version>
		</dependency>
		<dependency>
			<groupId>c3p0</groupId>
			<artifactId>c3p0</artifactId>
			<version>0.9.1.2</version>
		</dependency>

- Connection Pool 을 쓰기 위한 commons-dbcp 라이브러리, c3p0 라이브러리가 추가되어있다

 

MyBatis 연동위한 라이브러리

		<!-- mybatis -->
		<dependency>
			<groupId>org.mybatis</groupId>
			<artifactId>mybatis</artifactId>
			<version>3.2.3</version>
		</dependency>
		<dependency>
			<groupId>org.mybatis</groupId>
			<artifactId>mybatis-spring</artifactId>
			<version>1.2.1</version>
		</dependency>

- Spring 에서 MyBatis 를 쓰려면 MyBatis 라이브러리 뿐 아니라 MyBatis-Spring 연결 라이브러리도 필요하다


파일들 살펴보기 : web.xml

- Apache Tomcat 이 실행시 자동으로 실행되는 파일

- 코드는 생략

 

web.xml 에 들어가는 내용 3가지

1. Dispatcher Servlet 이름, 위치, 매핑 등록

- Dispatcher Servlet 클래스에 대한 위치가 등록되어있다

- Dispatcher Servlet 으로 찾아가기위한 매핑 등록

- 현재는 do 확장자로 요청할때만 Dispatcher Servlet 으로 찾아감

 

2. Spring 환경설정 파일 불러오기

- servlet-context.xml, root-context.xml 을 불러서 실행시켜준다

+ 두 파일이 resources 폴더 내에 있는 경우 classpath: 붙이기

 

3. 한글값 인코딩

- 한글값 인코딩시켜주는 필터와 필터매핑을 잡음


파일들 살펴보기 : servlet-context.xml

- web.xml 이 실행시켜준다

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xmlns:beans="http://www.springframework.org/schema/beans"
	xmlns:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.xsd
		http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context.xsd">
	
	<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
	<!-- Enables the Spring MVC @Controller programming model -->
	<annotation-driven />
	
	<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
	<resources mapping="/resources/**" location="/resources/" />
	
	<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
	<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<beans:property name="prefix" value="/WEB-INF/views/" />
		<beans:property name="suffix" value=".jsp" />
	</beans:bean>
		
	<context:component-scan base-package="myBatis1" />	
	
</beans:beans>

- View 파일들의 위치를 지정하고 있다

<base-package>

- base-package 는 기본적으로는 top-level 패키지 3단계로 만들어진다

- 여기선 수정해서 "myBatis1" 를 base-package 로 설정했다

- src/main/java 폴더 하위의 myBatis 폴더 안의 클래스들을 모두 읽어온다는 의미


파일들 살펴보기 : root-context.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
	xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd">
	
	<!-- Root Context: defines shared resources visible to all other web components -->
		
</beans>

- 비어있다

 

DB 연동

- 현재 프로젝트인 myBatis1 프로젝트에서는 DB 연동을 MyBatis 에서 제어(처리)하도록 하므로 MyBatis 환경설정 파일 configuration.xml 에서 처리

- 즉, 현재는 root-context.xml 이 아닌 MyBatis 환경설정 파일인 configuration.xml 에서 DB 연동 처리

- 이후에 할 myBatis2 프로젝트에서는 DB 연동을 Spring 에서 제어하도록 하므로 root-context.xml 에 DB 연동 관련 bean 생성, 그땐 root-context.xml 에서 DB연동 처리

 

- 그래서 현재 myBatis1 프로젝트의 root-context.xml 은 비어있다


MyBatis 관련 내용 복습

https://laker99.tistory.com/139

 

코딩 66 일 / 2022.09.26 / MyBatis, JSP Model 1 에 MyBatis 연동

복습 Maven - Spring 프로젝트는 Maven 으로만 라이브러리 관리 가능 - Springboo 프로젝트는 Maven, Gradle 둘 다 사용 가능 Maven 파일 구조 - src/main/java 에 확장자가 Java 인 파일을 넣는다 - src/main/re..

laker99.tistory.com

 

파일들 살펴보기 : MyBatis 환경설정 파일

1. jdbc.properties

2, configuration.xml

3. Mapper 파일 (테이블 마다)

 

- MyBatis 환경설정 파일들은 resources 폴더 안에 저장되어야함

 

- 현재 프로젝트 myBatis1 프로젝트에서는 DB 연동 관련 내용을 MyBatis 에서 제어하도록 해뒀으므로 @Autowired 로 SqlSession 을 주입하고 있지 않는다

- 현재 DAO 클래스에서는 configuration.xml 을 읽어와서 그걸로 SqlSessionFactory 객체를 생성

 

1. jdbc.properties

jdbc.driverClassName=oracle.jdbc.driver.OracleDriver
jdbc.url=jdbc:oracle:thin:@127.0.0.1:1521:xe
jdbc.username=scott
jdbc.password=tiger
jdbc.maxPoolSize=20

- DB 연동에 필요한 내용

 

2. Configuration.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
	<properties resource="jdbc.properties" />
	<typeAliases>
		<typeAlias alias="dept" type="myBatis1.model.Dept" />
	</typeAliases>
	<environments default="development">
		<environment id="development">
			<transactionManager type="JDBC" />
			<dataSource type="POOLED">
				<property name="driver" value="${jdbc.driverClassName}" />
				<property name="url" value="${jdbc.url}" />
				<property name="username" value="${jdbc.username}" />
				<property name="password" value="${jdbc.password}" />
			</dataSource>
		</environment>
	</environments>
	<mappers>
		<mapper resource="Dept.xml" />
	</mappers>
</configuration>

1) typeAlias 로 DTO 클래스 별칭 설정

2) DB 연동

- Setter DI 를 사용

3) Mapper 파일 불러오기

- 같은 폴더 resources 에 있고, 패키지가 없으므로 파일명만으로 불러옴

 

3. Mapper 파일

- Dept.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="deptns">
	
	<!-- Use type aliases to avoid typing the full classname every time. -->
	<resultMap id="deptResult"    type="dept">
		<result property="deptno" column="deptno" />
		<result property="dname"  column="dname" />
		<result property="loc"	  column="loc" />
	</resultMap>
	
	<select id="list" resultMap="deptResult">
		select * from dept order by deptno
	</select>
	
	<select id="select" parameterType="int" resultType="dept">
		select * from dept where deptno=#{deptno}
	</select>
	
	<update id="update" parameterType="dept">
		update dept set dname=#{dname},loc=#{loc} where deptno=#{deptno}
	</update>
	
	<delete id="delete" parameterType="int">
		delete from dept where deptno=#{deptno}
	</delete>
	
</mapper>

- SQL문만 따로 빼서 만든 파일

- DAO 에서 Configuration.xml 을 읽어오고, Configuration.xml 에선 이 Dept.xml 을 읽어오므로 이 SQL문을 DAO 에서 불러올 수 있다


환경설정 파일

- 현재 프로젝트 myBatis1 의 환경설정 파일은 총 6개

 

프로젝트 환경설정 파일

- web.xml

 

Spring 환경설정 파일

- servlet-context.xml

- root-context.xml

 

MyBatis 환경설정 파일

- resources 폴더 내에 위치

- jdbc.properties

- Configuration.xml

- Dept.xml


Controller / Service / DAO

+ 각각 인터페이스와 구현클래스가 존재한다 ex) DeptService 는 인터페이스, DeptServiceImpl.java 는 구현클래스

+ 각 클래스들은 용도에 따라 myBatis 폴더 하위 다른 폴더 안에 들어가있다

- servlet-context.xml 에 의해 myBatis1 폴더 내의 모든 클래스들을 읽어온다

- 이때 이 myBatis1 폴더 내의 클래스들에는 4개의 어노테이션 중 하나가 붙어있어야한다

 

- DeptController.java 부분

- Controller클래스에서는 @Autowired 를 사용해서 Service 객체를 주입하고 있음

- Service 클래스에 Setter 메소드 없어도 Controller 클래스에 Service 객체가 주입됨

 

- DeptServiceImpl.java 부분

- Service 클래스에서는 @Autowired 를 사용해서 DAO 객체를 주입하고 있음

- DAO클래스에 Setter 메소드 없어도 Service 클래스에 DAO객체가 주입됨

 

- DeptDaoImpl.java 부분

 

- 현재 프로젝트 myBatis1 프로젝트에서는 DB 연동 관련 내용을 MyBatis 에서 제어하도록 해뒀으므로 @Autowired 로 SqlSession 을 주입하고 있지 않는다

- 현재 DAO 클래스에서는 configuration.xml 을 읽어와서 그걸로 SqlSessionFactory 객체를 생성

 

+ 나중에 할 myBatis2 프로젝트 에서는 @Autowired 로 SqlSession 객체 주입할 것

 


현재 프로젝트 myBatis1 흐름

- 뒤쪽 DB 와 연동하는 것은 Model 2 - MyBatis 연동과 같은 방식으로 되어있다


프로젝트 실행

- 오라클 서버(OracleServiceXE, OracleXETNSListener) 가 구동되어 있어야한다

- 오라클 scott 계정이 활성화 되어있어야 한다


 - index.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
<script type="text/javascript">
	location.href="deptList.do";
</script>
</body>
</html>

- do 확장자로 요청하므로 Dispatcher Servlet 으로 잘 찾아간다

- Dispatcher Servlet -> Controller 클래스로 이동


- index.jsp 를 실행

- 부서목록을 보여주고 있다

- 부서명 중 하나 클릭시 그 부서명의 상세정보를 보여주고 수정 / 삭제 가능

+ Foreign Key 때문에 삭제가 안된다, 즉 참조하는 자식이 있으므로 부모가 지워지지 않음

+ 삭제를 위해서는 On Delete Cascade 옵션을 붙이거나 해야함



프로젝트 myBatis1 : 부서 목록 페이지

Controller 클래스 DeptControlelr

- index.jsp 에서 "deptList.do" 로 요청했으므로 Controller 클래스쪽으로 가자

- DeptController.java

package myBatis1.controller;

import java.util.List;

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 myBatis1.model.Dept;
import myBatis1.service.DeptService;

@Controller
public class DeptController {
	@Autowired
	private DeptService ds;

	// 부서 목록
	@RequestMapping("deptList.do")
	public String list(Model model) {
		List<Dept> list = ds.list();
		model.addAttribute("list", list);
		return "deptList";
	}

	// 상세 페이지
	@RequestMapping("deptView.do")
	public String deptView(int deptno, Model model) {
		Dept dept = ds.select(deptno);
		model.addAttribute("dept", dept);
		return "deptView";
	}

	// 수정 폼
	@RequestMapping("deptUpdateForm.do")
	public String deptUpdateForm(int deptno, Model model) {
		Dept dept = ds.select(deptno);
		model.addAttribute("dept", dept);
		return "deptUpdateForm";
	}
	
	// 부서 정보 수정
	@RequestMapping("deptUpdate.do")
	public String deptUpdate(Dept dept, Model model) {
		int result = ds.update(dept);
		model.addAttribute("result", result);
		return "deptUpdate";
	}
	
	// 부서 정보 삭제
	@RequestMapping("deptDelete.do")
	public String deptDelete(int deptno, Model model) {
		int result = ds.delete(deptno);
		model.addAttribute("result", result);
		return "deptDelete";
	}
}

+ @Controller, @Autowired 는 Spring 지원되는 라이브러리 클래스를 import 해야함

 

DeptController.java 에서 프로퍼티 부분만

	@Autowired
	private DeptService ds;

 

- 이 DeptService 는 Service 인터페이스이다

- 인터페이스로 주입해도 되고, 구현클래스로 주입해도 된다, 여기서는 인터페이스에 주입하고 있다

- 즉 어노테이션 기반 DI이며 이미 Service 객체 ds 에 주입이 완료되었음

+ 어노테이션 기반 DI 이므로 Service 클래스에 Setter 메소드가 없어도 여기서 주입 가능

 

DeptController.java 에서 "deptList.do" 요청 부분만 

	// 부서 목록
	@RequestMapping("deptList.do")
	public String list(Model model) {
		List<Dept> list = ds.list();
		model.addAttribute("list", list);
		return "deptList";
	}

- index.jsp 실행시 여기로 찾아옴, 자동으로 list() 실행됨

- Service 객체 ds 로 Service 의 메소드인 list() 를 호출한다, 

- 그렇게 Service 로 넘어가는 것 * Service 로 넘어가기 아래에

- 리턴받은 결과인 List 를 리스트 list 에 저장하고, Model 객체를 통해 deptList.jsp 로 그 리스트 list 를 저장한다

+ prefix, suffix 생략한 나머지인 "deptList" 를 리턴

- 그 후 Dispacher Servlet -> View 인 deptList.jsp  로 이동

 

어노테이션 기반 DI 사용 조건

1. 해당 클래스는 servlet-context.xml 에 base-package 로 지정된 myBatis1 패키지의 하위에 들어가있어야함

2. 어노테이션 4가지 중 하나가 붙어있어야함

3. @Autowired 를 주입하고자하는 프로퍼티 위에 쓰기

- 이 DeptService 는 Service 인터페이스이다


Service 구현 클래스 DeptServiceImpl

- Service 의 list() 메소드를 호출했으므로 Service 로 넘어가자

- DeptServiceImpl.java

package myBatis1.service;

import java.util.List;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import myBatis1.dao.DeptDao;
import myBatis1.model.Dept;

@Service
public class DeptServiceImpl implements DeptService {
	@Autowired
	private DeptDao dd;

	public List<Dept> list() {
		return dd.list();
	}

	public Dept select(int deptno) {
		return dd.select(deptno);
	}

	public int update(Dept dept) {
		return dd.update(dept);
	}

	public int delete(int deptno) {
		return dd.delete(deptno);
	}
}

- @Autowired 를 통해 DAO 객체를 프로퍼티 dd 에 주입받고 있다

- DAO 객체 dd 를 통해 DAO의 list() 메소드를 호출한다 * DAO 로 이동 아래에

- DAO 클래스에서 돌아올땐 리턴받은 값을 바로 리턴함

 

- 현재는 MyBatis 쪽에서 DB 연동을 처리하므로 SqlSession 객체를 @Autowired 로 생성하지 않음

- 현재 프로젝트 myBatis1 프로젝트에서는 Mode 1 - MyBatis , Mode 2 - MyBatis 연동할때와 똑같이 연동한다

+ 나중에 할 프로젝트 myBaits2 프로젝트에서는 Spring 쪽에서 DB 연동을 처리할 것, @Autowired 로 SqlSession 객체 생성할 것


DAO 구현 클래스 DeptDaoImpl

- DAO 로 넘어가게 되므로 DAO 를 살펴보자

- DeptDaoImpl.java

package myBatis1.dao;

import java.io.IOException;
import java.io.Reader;
import java.util.List;
import org.apache.ibatis.io.Resources;
import org.apache.ibatis.session.SqlSession;
import org.apache.ibatis.session.SqlSessionFactory;
import org.apache.ibatis.session.SqlSessionFactoryBuilder;
import org.springframework.stereotype.Repository;

import myBatis1.model.Dept;

@Repository
public class DeptDaoImpl implements DeptDao {
	private static SqlSession session;
	static {
	    try { 
	    	Reader reader = Resources.getResourceAsReader("configuration.xml");
		      SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(reader);
		      session = ssf.openSession(true); // auto commit
		      reader.close(); 
		} catch (IOException e) {
		  System.out.println("read file error : "+e.getMessage());
		}
	}
	public List<Dept> list() {
		return session.selectList("deptns.list");
	}
	public Dept select(int deptno) {
		return session.selectOne("deptns.select",deptno);
	}
	public int update(Dept dept) {
		return session.update("deptns.update",dept);
	}
	public int delete(int deptno) {
		return session.delete("deptns.delete",deptno);
	}
}

 

DeptDaoImpl.java 에서 SqlSession 객체 구하는 부분만

	private static SqlSession session;
	static {
	    try { 
	    	Reader reader = Resources.getResourceAsReader("configuration.xml");
		      SqlSessionFactory ssf = new SqlSessionFactoryBuilder().build(reader);
		      session = ssf.openSession(true); // auto commit
		      reader.close(); 
		} catch (IOException e) {
		  System.out.println("read file error : "+e.getMessage());
		}
	}

- SqlSession 객체를 static 을 붙여서 정적필드로 만듬

- static 으로 블럭을 만들어서 공유함, Model 1, Model 2 에서 했었던 getSession() 메소드 부분에 해당

- MyBatis 의 환경설정 파일 configuration.xml 을 읽어와서 Reader 객체 reader 생성

- reader 객체를 통해 SqlSessionFactory 객체 ssf 를 구함

- ssf.openSession(true) 를 통해 SqlSession 객체 session 을 구해옴

- openSession() 안에 true 가 들어가면 자동으로 커밋이 수행됨

 

DeptDaoImpl.java 에서 list() 부분만

	public List<Dept> list() {
		return session.selectList("deptns.list");
	}

- 리스트를 구해오기 위해서는 2개이상의 데이터에 대한 상세정보를 구해와야한다, 그래서 selectList() 를 사용

+ selecList() 의 리턴자료형은 List 이다

- deptns 는 Mapper 파일 Dept.xml 안에 설정된 namespace 를 의미, list 는 id 를 의미

- Mapper 파일인 Dept.xml 에서 id 가 list 인 SQL문을 불러오라는 의미

- 이후 Mapper 파일 에서 돌아오면 그 리스트를 그대로 Service 클래스로 리턴한다


Mapper 파일

- Mapper 파일 Dept.xml 로 넘어가보자

- Dept.xml

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="deptns">
	
	<!-- Use type aliases to avoid typing the full classname every time. -->
	<resultMap id="deptResult"    type="dept">
		<result property="deptno" column="deptno" />
		<result property="dname"  column="dname" />
		<result property="loc"	  column="loc" />
	</resultMap>
	
	<select id="list" resultMap="deptResult">
		select * from dept order by deptno
	</select>
	
	<select id="select" parameterType="int" resultType="dept">
		select * from dept where deptno=#{deptno}
	</select>
	
	<update id="update" parameterType="dept">
		update dept set dname=#{dname},loc=#{loc} where deptno=#{deptno}
	</update>
	
	<delete id="delete" parameterType="int">
		delete from dept where deptno=#{deptno}
	</delete>
	
</mapper>

- namespace 가 "deptns"

 

Dept.xml 에서 id 가 "list" 인 태그 부분만 + resultMap

	<!-- Use type aliases to avoid typing the full classname every time. -->
	<resultMap id="deptResult"    type="dept">
		<result property="deptno" column="deptno" />
		<result property="dname"  column="dname" />
		<result property="loc"	  column="loc" />
	</resultMap>
	
	<select id="list" resultMap="deptResult">
		select * from dept order by deptno
	</select>

- resultType 대신 resultMap 을 쓰고 있다

- resultMap 태그의 id 값으로는 아래의 select 태그에서 resultMap 속성에 쓴 "deptResult" 를 써야함

- resultMap 태그의 type 값으로 DTO alias 인 "dept" 가 들어가야함

- 즉 자동으로 DTO 객체에 세팅해서 돌려주는 resultType 대신 resultMap 으로 일일히 매핑하고 있다

- Dept 테이블 컬럼명과 DTO 프로퍼티명이 다를때 resultMap 을 사용해야한다

- 현재 Dept 테이블 컬럼명과 DTO 인 Dept 클래스 프로퍼티 명이 같으므로 resultMap 안써도 된다 

+ join 을 해야할 경우 등 resultMap 을 써야만 하는 경우도 있다

- 아래의 select SQL문을 실행하고, 그 검색결과를 매핑을 통해 여러개의 DTO 객체에 세팅 하고, 그 DTO 객체들을 List 에 담고 그 List 를 DAO 로 돌려줌

 

- 다시 DAO 로 돌아가야하므로 위로 돌아가자

* 돌아올때 설명은 하늘색 하이라이트

 

- Contorller 까지 돌아간 후 Dispatcher Servlet -> View 로 가게 된다

 

View 페이지 deptList.jsp

- deptList.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ include file="header.jsp"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<div class="container">
		<h2 class="text-primary">부서 목록</h2>
		<table class="table table-hover">
			<tr>
				<th>부서코드</th>
				<th>부서명</th>
				<th>근무지</th>
			</tr>
			<c:if test="${not empty list }">
				<c:forEach var="dept" items="${list}">
					<tr>
						<td>${dept.deptno }</td>
						<td><a href="deptView.do?deptno=${dept.deptno}"
							   class="btn btn-info"> ${dept.dname}</a></td>
						<td>${dept.loc }</td>
					</tr>
				</c:forEach>
			</c:if>
			<c:if test="${empty list }">
				<tr>
					<th colspan="3">데이터가 없습니다</th>
				</tr>
			</c:if>
		</table>
	</div>
</body>
</html>

- Controller 클래스에서 Model 에 저장했던 리스트의 네임값이 "list"

- Model 에 저장된 것이 리스트 이므로 forEach 태그의 items 에 ${list} 를 써서 출력한다

- forEach 태그를 통해 dept 로 개별 요소를 받고 있그 ${dept.deptno}, ${dept.dname}, ${dept.loc} 으로 목록을 출력

+ 부트스트랩 적용된것임

- include 태그로 불러온 header.jsp 파일에서 bootstrap, JSTL core 라이브러리등을 불러왔다

 

상세 페이지로 이동 (deptList.jsp 부분)

<td><a href="deptView.do?deptno=${dept.deptno}"
		 class="btn btn-info"> ${dept.dname}</a></td>

- 부서명 클릭시 상세 페이지로 가기 위해 ${dept.dname} 에는 a 태그로 링크를 걸고 "deptView.do" 를 요청한다

- "deptView.do" 로 요청하면서 Primary Key 인 부서 번호 ${dept.deptno} 을 가지고 간다

+ 이때 부서번호를 저장해서 가지고 가는 변수는 "deptno" 이다!

- Dispatcher Servlet -> Controller 로 간다


프로젝트 myBatis1 : 상세 페이지

- 부서명 클릭시 상세 페이지로 가기 위해 ${dept.dname} 에는 a 태그로 링크를 걸고 "deptView.do" 를 요청한다

 

DeptController.java 에서 "deptView.do" 요청 부분만

	// 상세 페이지
	@RequestMapping("deptView.do")
	public String deptView(int deptno, Model model) {
		Dept dept = ds.select(deptno);
		model.addAttribute("dept", dept);
		return "deptView";
	}

- 넘어온 부서 번호를 저장하는 변수와 같은 이름인 "deptno" 를 매개변수에 사용하므로 넘어온 부서번호가 바로 dept 변수에 저장됨

- 즉 @RequestParam("deptno") 가 int deptno 앞에 생략된 것이다

- int deptno 에 맞춰서 형변환까지 int 형으로 되어서 자동으로 deptno 변수에 저장해줌

- Model 객체를 매개변수에 선언만하면 자동으로 Model 객체가 생성됨

- 위에서 @Autowired 로 구해온 Service 객체 ds 로 Service 클래스의 select() 메소드 호출, 호출하며 부서번호 전달

- 그럼 Service 클래스로 가게 됨 * 아래에 설명

< 돌아왔을때>

- DTO 객체를 돌려받아서 Dept 객체 dept 로 받음

- 그 객체 dept 를 Model 을 사용해서 저장하고 deptView.jsp 로 간다

+ DTO 객체가 Model 에 저장되었으므로 View 에선 ${이름값.필드명} 으로 출력함

 

Service 클래스 DeptServiceImpl.java 에서 select() 메소드 부분만

	public Dept select(int deptno) {
		return dd.select(deptno);
	}

- 위에서 주입된 DAO 객체 dd 를 사용해서 DAO 의 select() 메소드 호출, 호출하며 부서번호 그대로 전달

<돌아왔을때>

- 받은 DTO 객체를 그대로 돌려준다

 

DAO 클래스 DeptDaoImpl.java 에서 select() 메소드 부분만

	public Dept select(int deptno) {
		return session.selectOne("deptns.select",deptno);
	}

- 1개 부서에 대한 상세 정보 (1개 데이터) 를 구해오므로 selectOne() 메소드 사용

- Mapper 파일 Dept.xml 에서 id 가 select 인 SQL문을 호출

- 두번째 매개변수로 부서번호를 그대로 전달

<돌아왔을때>

- 받은 DTO 객체를 그대로 돌려준다

 

Mapper 파일 Dept.xml 에서 id 가 select 인 태그 부분만

	<select id="select" parameterType="int" resultType="dept">
		select * from dept where deptno=#{deptno}
	</select>

- 전달되는 값은 부서번호 이므로 parameterType 은 "int"

- 돌려주는 값은 DTO 객체여야 하므로 resultType 은 DTO alias 은 "dept"

- 전달되는 값인 부서번호를  SQL문에서 #{deptno} 형식으로 사용함

 

- 다시 DAO -> Service -> Controller 로 가서 Dispatcher Servlet -> View 인 deptView.jsp 로 간다

* 돌아올때 설명은 하늘색 하이라이트

 

- deptView.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ include file="header.jsp"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>

<!-- deptList.do 요청한 결과 페이지(deptList.jsp)의 table태그만 불러옴 -->
<script type="text/javascript">
	$(function() {
		$('#list').load("deptList.do table");
	});
</script>
</head>
<body>
	<div class="container">
		<h2>부서 상세정보</h2>
		<table class="table table-bordered">
			<tr>
				<th>부서코드</th>
				<td>${dept.deptno}</td>
			</tr>
			<tr>
				<th>부서명</th>
				<td>${dept.dname }</td>
			</tr>
			<tr>
				<th>근무지</th>
				<td>${dept.loc }</td>
			</tr>
			<tr>
				<th colspan="2"><a href="deptList.do" class="btn btn-info">목록</a>
					<a href="deptUpdateForm.do?deptno=${dept.deptno}" class="btn btn-info">수정</a> 
					<a href="deptDelete.do?deptno=${dept.deptno}" class="btn btn-info">삭제</a></th>
			</tr>
		</table>
	</div>
	<div id="list"></div>
</body>
</html>

- DTO 객체가 Model 에 저장되었으므로 ${이름값.필드명} 으로 출력함, 즉 ${dept.필드명} 으로 출력

ex) ${dept.deptno} 로 부서 번호 출력, ${dept.dname} 으로 부서명 출력

+ 표기법만 ".필드명" 이다, 의미는 getter 메소드로 가져오는 것임

ex) ${dept.dname} 은 dept.getDname() 과 같다

 

아래에 목록 출력하기 (비동기 처리) (deptView.jsp 부분)

- 아래쪽의 내용은 부서 목록 페이지와 같은 내용이 나타남

<!-- deptList.do 요청한 결과 페이지(deptList.jsp)의 table태그만 불러옴 -->
<script type="text/javascript">
	$(function() {
		$('#list').load("deptList.do table");
	});
</script>
	<div id="list"></div>

- ajax 기능 중 load() 함수를 사용해서 구현했다

- id 가 "list" 인 div 태그를 jQuery 로 불러와서 ajax의 load() 함수 사용, 파일을 불러와서 이 영역에 출력시켜라는 의미

- load() 안에는 불러올 파일명이 들어간다, index 파일에 들어간 요청과 같은 (목록 구하기)요청 인 "deptLIst.do" 를 넣는다

- 부서 목록을 출력하는 View 페이지 deptList.jsp 중에서 table 태그 안의 내용만 불러오기 위해 한칸 띄워서 " table" 도 추가 작성

+ 그래서 "부서 목록" 이라는 글자등은 나타나지 않고 딱 테이블만 나타남

 

+ 비동기로 서버에 요청하는 load() 함수

- $("출력시킬 태그").load("요청할 파일명");

- 특정 서버측의 문서를 불러옴, 안에는 특정 파일명이 들어간다

- 서버측에 비동기적으로 요청하고, 서버측의 지정된 파일의 내용을 모두 불러와서 앞의 태그에 결과 출력 시켜주는 역할

 

ajax load() 함수 관련 자세한 내용 (검색어 : load() 함수 예제 3)

https://laker99.tistory.com/105

 

목록 / 수정 / 삭제 링크걸기 (deptView.jsp 부분)

<tr>
	<th colspan="2"><a href="deptList.do" class="btn btn-info">목록</a>
	<a href="deptUpdateForm.do?deptno=${dept.deptno}" class="btn btn-info">수정</a> 
	<a href="deptDelete.do?deptno=${dept.deptno}" class="btn btn-info">삭제</a></th>
</tr>

- '목록' 버튼 클릭시 "deptList.do" 로 요청해서 부서 목록 페이지로 간다

- '수정' 버튼 클릭시 "deptUpdateForm.do" 로 요청해서 수정폼으로 간다, Primary Key 인 부서번호를 가져간다

- '삭제' 버튼 클릭시 "deptDelete.do" 로 요청해서 삭제, Primary Key 인 부서번호를 가져간다

- 현재 '삭제' 를 누르면 오류가 발생함 (참조하는 자식이 있기때문에 삭제 불가)

 

- '목록' 버튼 클릭시에 목록 페이지로 가는 것은 이미 설명했다


프로젝트 myBatis1 : 수정 폼

- '수정' 버튼을 클릭해서 수정폼으로 가보자

 

- "deptUpdateForm.do" 요청시 수정폼으로 가면서 get 방식으로 부서번호 deptno 를 가져간다

- 부서번호를 가져가야 상세 정보를 다시 구해서 수정폼에 뿌려줄 수 있다

 

DeptController.java 에서 "deptUpdateForm.do" 요청 부분만

	// 수정 폼
	@RequestMapping("deptUpdateForm.do")
	public String deptUpdateForm(int deptno, Model model) {
		Dept dept = ds.select(deptno);
		model.addAttribute("dept", dept);
		return "deptUpdateForm";
	}

+ Controller 클래스 메소드에서 리턴자료형은 대부분 String

- 부서 번호가 get 방식으로 넘어왔고, 그게 넘어온 값의 변수명과 같은 이름인 deptno 변수에 자동으로 형변환 후 저장됨

- int deptno 뒤에 @RequestParam 생략 된 것

+ View 페이지로 값을 가져갈 것이므로 Model 객체를 생성해야함, Model 객체를 매개변수에 써주면 Model 객체가 생성됨

- 위에서 @Autowired 로 주입된 Service 객체 ds 로 Service 클래스의 select() 메소드 호출, 호출하면서 부서번호 전달

- 상세정보를 가져와서 수정폼에 뿌려줘야하므로 아까 목록을 구할때 호출했던 메소드 select() 를 여기서 또 호출

- @Autowired 가 써진 객체 생성 및 주입 시점 : web.xml 에서 servlet-context.xml 을 호출할때 

<돌아올 때>

- 상세정보를 저장한 DTO 객체를 dept 로 받아서 Model 객체에 저장

- deptUpdateForm.jsp 로 이동

 

- Service 클래스로 이동한다

DeptServiceImpl.java 에서 select() 메소드 부분만

	public Dept select(int deptno) {
		return dd.select(deptno);
	}

- 아까 목록을 구할때 호출했던 메소드와 같다

DeptDaoImpl.java 에서 select() 메소드 부분만

	public Dept select(int deptno) {
		return session.selectOne("deptns.select",deptno);
	}

- 아까 목록을 구할때 호출했던 메소드와 같다

Mapper 파일 Dept.xml 에서 id 가 "select" 인 태그 부분만

	<select id="select" parameterType="int" resultType="dept">
		select * from dept where deptno=#{deptno}
	</select>

 

수정 폼 View 페이지인 deptUpdateForm.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ include file="header.jsp"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<div class="container" align="center">
		<h2 class="text-primary">부서정보 수정</h2>
		<form action="deptUpdate.do" method="post">
			<input type="hidden" name="deptno" value="${dept.deptno}">
			<table class="table table-striped">
				<tr>
					<th>부서코드</th>
					<td>${dept.deptno}</td>
				</tr>
				<tr>
					<th>부서명</th>
					<td><input type="text" name="dname" required="required"	value="${dept.dname }"></td>
				</tr>
				<tr>
					<th>근무지</th>
					<td><input type="text" name="loc" required="required" value="${dept.loc }"></td>
				</tr>
				<tr>
					<th colspan="2"><input type="submit" value="확인"></th>
				</tr>
			</table>
		</form>
	</div>
</body>
</html>

- Model 객체에 저장된 값이 DTO 객체이므로 ${네임값.필드명} 으로 출력한다, 여기선 ${dept.필드명} 으로 출력

- 입력양식의 value 속성으로 수정폼에 값을 뿌린다

- 수정폼 입력양식에 값을 입력하고 '확인' 을 누르면 action 인 "deptUpdate.do" 로 요청한다

- 부서 코드는 입력양식이 아닌 출력을 하고 있으므로 넘어가지 않는다, 그래서 hidden 으로 전달해야한다

- 이 값을 전달해야만 where 조건절에 deptno 를 넣어서 1개 부서를 수정 가능하다

- "deptUpdate.do" 로 요청하면서 부서 코드인 deptno 를 hidden 객체로 전달한다


프로젝트 myBatis1 : 부서 정보 수정

- 수정폼 deptUpdateForm.jsp 의 입력양식에 값을 입력하고 '확인' 을 누르면 action 인 "deptUpdate.do" 로 요청한다

 

DeptController.java 에서 "deptUpdate.do" 요청 부분만

	// 부서 정보 수정
	@RequestMapping("deptUpdate.do")
	public String deptUpdate(Dept dept, Model model) {
		int result = ds.update(dept);
		model.addAttribute("result", result);
		return "deptUpdate";
	}

- 수정폼 deptUpdateForm.jsp 에서 넘어온 값들을 DTO Dept 객체 dept 에 세팅(저장)해준다

- 즉 @ModelAttribute 어노테이션이 Dept dept 앞에 생략되어 있다

+ 넘어온 name 값과 DTO 프로퍼티명이 같은 이름인 경우에만 가능하다

- 위에서 @Autowired 로 주입한 Service 객체 ds 로 Service 클래스의 update() 메소드를 호출, 호출하며 사용자가 수정폼에 입력한 값들을 전달함

<돌아온 후>

- DB 수정을 성공하면 1 을 리턴할 것, 그 값을 result 변수에 저장한다

- 그 result 변수를 Model 객체에 저장 후, deptUpdate.jsp 로 간다

 

DeptServiceImpl.java 에서 update() 부분만

	public int update(Dept dept) {
		return dd.update(dept);
	}

- 받은 객체를 그대로 다시 전달

<돌아올때>

- 성공시 받은 1 을 다시 리턴

 

DeptDaoImpl.java 에서 update() 부분만

	public int update(Dept dept) {
		return session.update("deptns.update",dept);
	}

- 받은 객체를 그대로 다시 전달

<돌아올 때>

- 이 update() 메소드는 DB 수정 성공시 1을 자동으로 리턴함

 

Mapper 파일 Dept.xml 에서 id 가 "update" 인 태그 부분만

	<update id="update" parameterType="dept">
		update dept set dname=#{dname},loc=#{loc} where deptno=#{deptno}
	</update>

- 넘어오는 것이 DTO 객체이므로 그 값을 사용할땐 #{필드명} 으로 사용함

ex) #{loc} 은 dept.getLoc() 과 같은 의미이다

- 수행 후 돌아간다, update 문이므로 돌려주는 returnType 은 적지 않는다

 

View 페이지인 deptUpdate.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ include file="header.jsp"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<c:if test="${result > 0 }">
		<script type="text/javascript">
			alert("수정 성공");
			location.href = "deptList.do";
		</script>
	</c:if>
	<c:if test="${result <= 0 }">
		<script type="text/javascript">
			alert("수정 실패");
			history.go(-1);
		</script>
	</c:if>
</body>
</html>

- 수정 성공시 result 는 1 이므로 "수정 성공" 메세지를 뿌리고 "deptList.do" 로 요청해서 목록 페이지로 돌아간다

- 수정 실패시 "수정 실패" 메세지를 뿌리고 이전 페이지인 수정 폼으로 돌아간다


프로젝트 myBatis1 : 삭제

- deptView.jsp 에서 '삭제' 버튼을 클릭해서 삭제를 해보자

<a href="deptDelete.do?deptno=${dept.deptno}" class="btn btn-info">삭제</a></th>

 

- "deptDelete.do" 요청시 Controller 클래스로 가면서 get 방식으로 부서 코드 deptno 를 가져간다

- 부서코드를 가져가야 delet SQL문 실행시 where 절에 부서 코드를 넣어서 해당 부서를 삭제할 수 있다

 

DeptController.java 에서 "deltDelete.do" 요청 부분만

	// 부서 정보 삭제
	@RequestMapping("deptDelete.do")
	public String deptDelete(int deptno, Model model) {
		int result = ds.delete(deptno);
		model.addAttribute("result", result);
		return "deptDelete";
	}

- 넘어온 부서코드가 저장된 변수명과 같은 이름인 deptno 를 매개변수에 써서 바로 deptno 에 넘어온 부서코드 저장

+ @RequestParam("deptno") 가 int deptno 앞에 생략

- Service 객체 ds 를 통해 Service 클래스의 delete() 메소드를 호출함, 호출하면서 부서코드 deptno 를 전달

<돌아올 때>

- 성공여부를 확인하기 위해 result 변수로 delete() 의 리턴값을 받는다

- 그 result 를 Model 객체에 저장하고 deptDelete.jsp 로 간다

 

DeptServiceImpl.java 에서 delete() 부분만

	public int delete(int deptno) {
		return dd.delete(deptno);
	}

DeptDaoImpl.java 에서 delete() 부분만

	public int delete(int deptno) {
		return session.delete("deptns.delete",deptno);
	}

- Mapper 파일에서 id 가 "delete" 인 SQL문을 불러온다

- 전달하는 값은 부서 코드인 deptno

Mapper 파일 Dept.xml 에서 id 가 delete 인 태그 부분만

	<delete id="delete" parameterType="int">
		delete from dept where deptno=#{deptno}
	</delete>

- #{deptno} 로 가져옴


문제점

- '삭제' 누르면 오류 발생, 무결성 제약조건때문에 오류가 발생한다

- 즉, 해당 부서(부모)를 참조하는 Foreign Key (자식) 이 있으므로7 삭제가 되지 않는다

 

 

문제 해결

- SQL문에서 On Delete Cascade 를 사용해서 부모를 지울때 참조하는 자식까지 같이 지운다 


View 페이지인 deptDelete.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<%@ include file="header.jsp"%>

<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<c:if test="${result > 0 }">
		<script type="text/javascript">
			alert("삭제 성공");
			location.href = "deptList.do";
		</script>
	</c:if>
	<c:if test="${result != 1 }">
		<script type="text/javascript">
			alert("삭제 실패");
			history.go(-1);
		</script>
	</c:if>
</body>
</html>

- 삭제 성공 메시지 또는 실패 메세지를 뿌리고 이동하는 역할을 해야한다

- result 가 0 보다 큰 경우, 즉 삭제에 성공한 경우엔 "삭제 성공" 출력후 "deptList.do" 로 이동, 목록 페이지로 간다

- result 가 1 이 아닌 경우는 삭제 실패를 의미하므로 "삭제 실패" 출력 후 이전 페이인 상세 페이지로 돌아감


- 오늘 한 내용은 아님, 미리 설정 해두기

 

STS 에 Data Source Management 추가

- STS 는 이클립스와 다르게 Data Source Management 가 없다

- STS 에 plug-in 기능을 추가해야 Data Source Management 를 사용 가능

 

STS 에 Data Source Management 추가하는 방법

+ Recent posts