2022.10.12 ~ 2022.10.14 (프로젝트 전)

- 프로젝트 주제 선정

 

2022.10.17 ~ 2022.10.21 (프로젝트 전)

- 요구사항 분석

- 카카오 오븐 사용 UI 기초 작업

- 작업 흐름도 작성

 

2022.10.24 (프로젝트 최종 발표 D-26)

- 카카오 오븐 사용 UI 기초 완성

- DB 설계

 

2022.10.25 (프로젝트 최종 발표 D-25)

- DB 설계, DB 컬럼 및 관계 설정

- AWS 원격 서버에서 오라클 환경 구축 및 프로젝트용 오라클 계정 생성

 

2022.10.26 (프로젝트 최종 발표 D-24)

- DB 데이터 타입 및 제약조건 확정

- eXERD 에서 포워드 엔지니어링으로 원격 서버 오라클에 테이블 생성

- Spring MVC 프로젝트 초기 환경설정 시작, DB 연결 확인 완료(SELECT 실행해봄)

- GIT Clone, 이클립스와 로컬 레포지토리 연결, Commit, Push 연습

 

2022.10.27 (프로젝트 최종 발표 D-23)

- Spring MVC 프로젝트 초기 환경설정 완료

- pom.xml, web.xml, servlet-context.xml, root-context.xml, Mapper 파일 생성 및 namespace 지정, Mybatis 환경설정 파일, 인터셉터 구현 초기 설정 완료

- GITHUB 원격 저장소 생성

- GITHUB 에 초기 환경설정 완료된 Spring MVC 프로젝트 Commit and Push

- 멤버들 각각 Branch 생성 및 Push 테스트

- EVENT 생성 (INSERT) 기능 구현

 

2022.10.28 (프로젝트 최종 발표 D-22)

- EVENT 생성 시 지도 API 기능 추가

- EVENT 리스트 불러오기, 출력하기

- EVENT 생성시 파일명 난수화해서 중복 방지

- 중간발표 발표 준비, PPT 준비

 

2022.10.29 (프로젝트 최종 발표 D-21)

- 전체 이벤트 리스트, 특정 지역의 이벤트 리스트, 특정 모임의 이벤트 리스트, 이벤트 검색결과 리스트 출력

- 이벤트 리스트 페이징 처리

 

2022.10.31 (프로젝트 최종 발표 D-20)

- 이벤트 상세페이지, 수정폼, 수정, 삭제 기능 추가

- 프로젝트 중간 발표

컴파일러 방식 언어

- 컴파일러에 의해 기계어로 번역된 채로 실행된다

- 수정이 빈번하게 발생할 경우에는 수정 후 다시 컴파일을 해야합니다.

- 컴파일 시간이 오래 걸립니다, 간단한 수정에도 오랜 기간의 컴파일 시간이 요구됩니다

 

 

인터프리터 방식 언어

- 수정이 빈번하게 발생하는 경우에는 소스 코드를 줄 단위로 읽어 바로 실행하는 인터프리터 방식이 유리

- 스크립트 소스코드를 컴파일 방식에 의해 중간 코드(Bytecode)로 우선 만들고, 이를 다시 인터프리터 방식으로 해석하여 수행

 

컴파일러 방식 언어 vs 인터프리터 방식 언어

- 컴파일 방식 언어 : C, C++, JAVA

- 인터프리터 방식 언어 : Python, JavaScript, Perl, PHP, Ruby

 

스크립트 언어의 장점

- 개발 시간이 단축

- 소스 코드 수정이 빠르고 간단

 

스크립트 언어의 단점

- 중간 코드를 만드는 것은 간단하지만 그것을 실제로 실행시키는 것은 많은 작업이 필요

- 실행 시간이 오래 걸림

 

파이썬의 특징

1. 들여쓰기

- 반드시 해야한다

2. 풍부한 라이브러리

- 데이터 분석에 최적화

3. 다른 언어와 결합해서 사용 가능

- JAVA 와 결합해서 사용 가능

- JAVA 로 만든 프로그램 파이썬으로 실행 가능

4. 무료에 가까운 라이센스

5. 유니코드

- 기본 타입이 유니코드이므로 인코딩 지정하지 않으면 유니코드로 처리

6. 동적 타이핑과 자동 메모리 관리

- 런타임시 타입체크를 하는 동적 타이핑

 

파이썬과 웹 프로그래밍

- Django, Flask 프레임워크 사용시 파이썬으로 웹 어플리케이션 개발 가능

- 자바의 MVC 를 MTV 라고 부른다

 

파이썬과 데이터 분석

- Numpy

- Pandas

- Metplotlib

 

파이썬 버전

- 2.x 버전과 3.x 버전이 있다

- 현재는 3.x 문법만 거의 사용

 

파이썬 다운로드

https://www.python.org/downloads/

- 최신 버전 설치 가능

- 실습시엔 파이썬 설치하지 않는다

 

아나콘다

https://www.anaconda.com/

- 파이썬 설치하는 대신 아나콘다 프로그램을 다운

- 아나콘다 안에는 파이썬 내장, 여러 파이썬 라이브러리인 파이썬 분석관련 numpy, pandas, metaplotlib 까지 내장

- Just Me 는 현재 내가 로그인한 계정 admin 에서만 사용 가능

- All Users 는 모든 계정에서 사용 가능

- 위의 Add Anaconda3 to my PATH environment variable 에 체크해야만 pip 명령어를 쓸 수 있다

+ pip 명령어로 프로그램을 설치함

 

아나콘다 설치 후 확인 (파이썬 설치 확인)

- 명령 프롬프트 창에서 python 이라고 입력시 '>>>' 가 나타나면 접속된 상태

- PATH 가 잡혀있어야만 python 이란 명령이 적용됨

+ quit() 으로 빠져나감

 

pip

- 라이브러리 관리

- 라이브러리 설치/관리/업데이트시 사용하는 명령

 

pip list 명령어

- 현재 아나콘다에 설치된 모든 라이브러리 종류를 보여준다

- 파이썬 포함 여러 라이브러리를 보여줌

 

IDE 프로그램

- 소스 작성, 인터프리터, 디버깅 모두 가능한 프로그램

ex) 자바는 eclipse, C 는 Visual Studio

- PyCharm : python 전용 IDE 프로그램

- ipython : 아나콘다를 설치하면 같이 자동 설치

- Visual Studio : Visual Studio 의 파이썬 도구

- Eclipse : Eclipse 에 PyDev 플러그인 이용해서 사용

- Spyder : 아나콘다를 설치하면 같이 자동 설치

 

ipython 명령

- ipython 은 C 언어로 만들어진 인터프리터 사용

- ipython 명령 입력시 대화형으로 처리됨

 

Jupyter Notebook

- ipython 명령으로 처리하는 작업들을 브라우저 기반으로 처리할 수 있도록 해줌

- 아나콘다를 설치하면 같이 자동 설치

- 파이썬 분석시 사용

 

Jupyter Notebook 실행

- Anaconda3 에서 Jupyther Notebook 메뉴 클릭

- 클릭 시 브라우저창이 열리는데, 같이 열리는 명령 프롬프트 창을 닫으면 안됨

- 계정명이 한글명이면 동작하지 않는다

- 내 계정 디렉토리 안의 파일 폴더들이 보인다

- 우측 New - Python 3 (jpykernel) 클릭시 작업을 위한 새로운 노트북이 열림

- 여기서 파이썬 작업 가능, 데이터 분석시 주피터 노트북을 사용한다

- 실행방법 : Ctrl + Enter, Shift + Enter 또는 Run 버튼 클릭

 

새로운 창 추가

 

그래프 그리기

import matplotlib.pyplot as plt
plt.plot([1,2,3,4]) 
plt.ylabel('some numbers')
plt.show()

- 위 코드들을 주피터 노트북에 입력 후 Shift + Enter 클릭

- 그대로 저장했을때는 Untitled 가 된다, 자동 저장됨

 

이름 수정

 

- 나와서 이 화면으로 돌아가기

- 이 파일 하나만 가져가면 된다

- 클릭시 내용을 볼 수 있다

- 여기 데이터가 저장되어있다

 

Pycharm

- python 기본 문법을 공부할때 사용할 프로그램

- 주피터 프로그램은 python 분석시 사용, 지금은 Pycharm 으로 사용

 

Pycharm 다운로드

- Professional 버전, Community 버전

- Community 버전은 콘솔 명령어로 프로젝트 생성

- Community 버전을 다운로드 하자

https://www.jetbrains.com/pycharm/download/#section=windows

 

워크스페이스와 프로젝트 같이 생성

- Prjoect - New 선택시 프로젝트 생성되며 워크스페이스가 같이 생성

- C:\Users\admin\PycharmProjects 까지가 워크 스페이스, pythonProject 가 프로젝트명

- 가상환경을 필요에 따라 여러개 만들 수 있고, 여러 버전으로 테스트 가능

- 가상환경 위치 : C:\Users\admin\PycharmProjects\pythonProject\venv

- Create 누르면 새로운 가상환경을 만들게 되면서 실행된다


 

 

 

 

 

 

 

 

 

 

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 태그 아래에는 페이지 처리를 하고 있음, 자주 했으므로 설명 생략

 

+ Recent posts