웹 소켓 기능

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

- 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 도 가져가야만함

+ Recent posts