본문 바로가기
프로젝트/Summernote를 활용한 WYSIWYG 게시판 만들기

3. 에디터 화면 및 저장 만들기

by IT여행자 2023. 1. 1.
728x90

가장 많은 내용들을 설명하고 있는 부분입니다. 많이 복잡하더라도 최종 결론은 그리 복잡하지 않으므로 하나씩 실행해 보시면서 따라오시기 바랍니다.

 

기본 페이지

 

insert.jsp 페이지를 열고 CDN과 기본 폼을 html로 작업합니다. 물론 jquerysummernote 함수를 호출하는 것만으로도 기본 위지윅 에디터 화면을 볼 수 있습니다.

 

파일명 : webapp/insert.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
	pageEncoding="UTF-8"%>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>summernote editor</title>
<!-- include libraries(jQuery, bootstrap) -->
<link rel='stylesheet' type='text/css' href='./css/sn.css'>
<link
	href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css"
	rel="stylesheet">
<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
<script
	src="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/js/bootstrap.min.js"></script>

<!-- include summernote css/js -->
<link
	href="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote.min.css"
	rel="stylesheet">
<script
	src="https://cdn.jsdelivr.net/npm/summernote@0.8.18/dist/summernote.min.js"></script>



</head>
<body>
	<div id='summer'>
		<h2>SUMMERNOTE Editor</h2>
		<form name='frm_summbernote' method='post' action='result.jsp'>
			<label>제목 : <input type='text' size='90' name='subject' value='반갑습니다!! IT여행자입니다.'></label>
			<br />
			<textarea id='summernote' name='doc'>IT 여행자의 수첩입니다.</textarea>

			<button>SAVE</button>
			<a href='list.jsp'>목록으로</a>
		</form>
	</div>

	<script src = './js/sn.js'></script>
</body>
</html>

 

 

 

부트 스트랩과 jQuery와 관련된 CDNsummernote.orgGetting started 항목을 확인하시면 쉽게 작업할 수 있습니다.

 

 

전체 화면 배치와 한글 폰트 추가

 

전체 화면의 넓이와 에디터를 비롯한 전체 화면을 브라우저 가운데 배치하기 위해 간단한 스타일 작업을 합니다. 또한 summernote 기본 폰트에는 한글 폰트가 추가되어 있지 않기 때문에 한글 폰트의 종류도 추가하여 에디터에 반영해 줍니다.

step 1.

화면을 가운데 배치하는 스타일을 추가합니다. 해당 스타일은 모두 css/sn.css 파일에 작성합니다.

 

파일명 : css/sn.css

#summer {
	width: 800px;
	margin: 30px auto;
}

 

step 2.

한글 폰트의 종류를 기술하여 summernote 에디터에 적용해 줍니다. 물론 하단에 나열되어 있는 폰드틀은 로컬 컴퓨터에 설치되어 있어야 합니다. 자바스크립트 코드는 js/sn.js에 작업하여 <script/> 태그를 사용하여 연결합니다.

 

파일면 : js/sn.js

[실행화면]

 

 

 

DB 연결

 

CRUD를 위하여 먼저 mysql JDBC가 설치되어 있어야 합니다. 설치된 후 아래와 같은 코드를 입력하여 mysql JDBC 드라이버를 로딩한 후 Connection 객체를 반환하도록 합니다.

 

파일명 : bean/DBConn.java

 

package bean;

import java.sql.Connection;
import java.sql.DriverManager;

public class DBConn {
	String driver = "com.mysql.cj.jdbc.Driver";
	String path = "jdbc:mysql://localhost:3306/mydb?useUnicode=true&characterEncoding=UTF-8";

	String user = "hong";
	String pwd = "1111";
	
	Connection conn;

	public DBConn() {
		try {
			Class.forName(driver);
			conn = DriverManager.getConnection(path, user, pwd);
		} catch (Exception e) {
			e.printStackTrace();
		}
	}
	
	public Connection getConn() {
		return conn;
	}
}

 

저장 페이지 작성


작성된 내용을 저장하기 위한 result.jsp 페이지를 만들 차례입니다. result.jsp 페이지는 요청자료를 갖고 SnDao의 inesert() 메서드를 호출하여 DB에  작성할 것입니다.

step 1.

자료를 저장하기 위한 테이블은 아래와 같이 생성되어 있습니다.

 

CREATE TABLE summernote (
	serial INT(11) primary key auto_increment,
	subject VARCHAR(200) ,
	doc LONGTEXT 
)

본문내용을 저장하기 위해 컬럼의 크기는 약 4GB를 저장할 수 있는 longtext로 지정하였습니다.

 

 

step 2.

result.jsp로 부터 값을 전달받아 DB에 데이터를 저장하는 기능을 SnDao에 작성합니다.

 

파일명 : bean/SnDao.java

 

package bean;

import java.io.File;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.sql.ResultSet;
import java.util.ArrayList;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;

public class SnDao {
	String uploadPath = "N:\\workspace\\summernote\\src\\main\\webapp\\";
	Connection conn;
	PreparedStatement ps;
	ResultSet rs;
	String sql;
	public SnDao() {
		try {
			conn = new DBConn().getConn();
		} catch (Exception ex) {
			ex.printStackTrace();
		}
	}

	public boolean insert(SnVo vo) {
		boolean b = false;
		sql = "insert into summernote(subject, doc) values(?,?)";
		try {
			conn.setAutoCommit(false);
			ps = conn.prepareStatement(sql);
			ps.setString(1, vo.getSubject());
			ps.setString(2, vo.getDoc());

			int c = ps.executeUpdate();
			if (c > 0) {
				conn.commit();
				b = true;
			} else {
				conn.rollback();
			}

		} catch (Exception ex) {
			ex.printStackTrace();
		}

		return b;

	}

}

 

step 3.

파라메터로 전달되는 값은 subject과 doc일 것입니다.

 

파일명 : webapp/result.jsp

 

<%@page import="bean.SnDao"%>
<%@page import="bean.SnVo"%>
<%@page import="java.sql.PreparedStatement"%>
<%@page import="java.sql.ResultSet"%>
<%@page import="com.mysql.cj.xdevapi.PreparableStatement"%>
<%@page import="bean.DBConn"%>
<%@page import="java.sql.Connection"%>
<%@ 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>
<link rel='stylesheet' type='text/css' href='./css/sn.css'>


</head>
<body>
<%
	request.setCharacterEncoding("utf-8");
		
	String subject = request.getParameter("subject");
	String doc = request.getParameter("doc");

	SnVo vo = new SnVo();
	vo.setSubject(subject);
	vo.setDoc(doc);
	
	SnDao dao = new SnDao();
	boolean b = dao.insert(vo);
	
	if( !b ){
		out.print("오류 발생 !!!!!");
		out.print("<a href='insert.jsp'>입력화면</a>");
		return;
	}

%>
<div id='list'>
	<h1>입력 결과</h1>
	<div class='item'>
		<div class='subject'><%=subject %> </div>
		<div class='doc'><%=doc %></div>
	</div>
	<hr/>
	<a href='list.jsp'>목록으로</a>
</div>
</body>
</html>

 

DB에 자료를 저장하는 코드를 한 두 번 작성해 보신 독자라면 아마도 특별히 어려운 코드 부분은 없으리라 생각됩니다. 위 jsp 페이지의 내용은 자바 Dao 객체로 작업하더라도 큰 무리를 없을 것입니다.

본문글에 문자와 이미지를 함께 저장하더라도 정상적으로 데이터가 DB에 저장됩니다. 그러나 저장된 데이터를 확인해 보면 이미지가 있는 본문글은 용량이 매우 커진것을 확인할 수 있습니다.

 

이미지 한장과 약간의 문자열을 입력하여 저장해 봅니다.

 

저장된 내용을 쿼리로 확인해 보면 이미지가 Base64 방법으로 바뀌어 저장되었음을 알 수 있습니다.

 

간편하게 컨텐츠 내용을 저장할 수 있어 매우 편리하지만, DB입장에서는 이미지 몇 장으로 인해 용량이 커지기 때문에 장기적으로는 DB에 많은 부하를 안기게 될 것입니다. 따라서 우리는 이미지를 별도의 파일로 따로 저장하게 하고 DB에는 이미지의 경로만을 남겨 이미지들로 인해 용량이 커지는 것을 최대한 줄일 것입니다.

 

파일 업로드


Base64 유형인 아닌 일반적인 파일로 저장하기 위해서는 다소 복잡한 절차를 거쳐야 됩니다. 이는 원격지의 파일을 서버에 저장해야 하는 과정 속에서 일정한 시간이 지연되거나, 파일 용량이 커서 저장 장치에 저장되는 시간으로 인해 지연될 수 도 있기 때문에 이에 따른 적절한 조치도 필요할 것이기 때문입니다.

1)  callbacks 지정

summernote 영역에서 이벤트가 발생하면 callbacks 요소가 이벤트를 감지하게 됩니다. 이미지가 로딩된다면 아래와 같이 이벤트 핸들러를 작성합니다.

 

파일명 : js/sn.js(일부분)

$('#summernote').summernote({
              … 
	callbacks : {

		onImageUpload : function(files, editor, welEditable) {
                                     //TODO
		}
	}
});

 

2) 업로드 서버 프로그램


파일을 업로드 하는 프로그램은 서블릿형태로 만들어져 있습니다. 파일 업로드뿐만 아니라 사용자가 summernote에서 파일을 제거할 때 서버에 업로드된 파일도 삭제하도록 구성하였습니다.

 

파일명 : bean/FileUpload.java

package bean;

import java.io.File;
import java.io.IOException;
import java.io.PrintWriter;
import java.util.Collection;
import java.util.Date;
import java.util.UUID;

import javax.servlet.ServletException;
import javax.servlet.annotation.MultipartConfig;
import javax.servlet.annotation.WebServlet;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.Part;


@MultipartConfig(
		location="c:/temp", 
		maxFileSize = -1,
		maxRequestSize = -1,
		fileSizeThreshold= 2000)

@WebServlet(urlPatterns = "/upload.up")
public class FileUpload extends HttpServlet {
	String uploadPath = "N:\\workspace\\summernote\\src\\main\\webapp\\upload\\";
	UUID uuid = null;
	
	// 폴더가 없으면 생성해 두어야 함
	// 클라이언트로 부터 파일이 전송되었을 때 임시로 저장되는 폴더
	String tempDir = "c:/temp/"; 

	HttpServletRequest req;
	
	@Override
	protected void service(HttpServletRequest req, HttpServletResponse resp)
			throws ServletException, IOException {

		
		req.setCharacterEncoding("utf-8");
		resp.setContentType("text/html;charset=utf-8");
		PrintWriter out = resp.getWriter();
		String flag = "";

		
		if(req.getParameter("flag") != null) {
			flag = req.getParameter("flag");
		};
		
		if(flag.equals("delete")) {
			String target = req.getParameter("target");
			String[] temp = target.split("/");
			File delFile = new File(uploadPath + temp[temp.length-1]);
			if(delFile.exists()) delFile.delete();
			return;
		}else {
			
			Collection<Part> parts = req.getParts();
			
			
			for(Part p : parts) {
				
				
				if(p.getHeader("Content-Disposition").contains("filename=")){
					
					if(p.getSize()>0) {
						uuid = UUID.randomUUID();
						String temp = String.format("%s-%s",
												uuid.getLeastSignificantBits(),
												p.getSubmittedFileName() );
						p.write(uploadPath + temp);
						p.delete();
						
						
						try {
							Thread.sleep(10);
							out.print("./upload/" + temp);
						}catch(Exception ex) {
							ex.printStackTrace();
						}
						
							
					}
					
				}
			}// end of for
		
		}		
		
	}
	
}

 

3) 업로드 파일별 선택

 

파일명 : js/sn.js (일부분)

var loadInterval = [];
$('#summernote').summernote({
	… 

	callbacks : {
		onImageUpload : function(files, editor, welEditable) {
			
			loadInterval.length = files.length;
			$('#summer').addClass('spinner');//spinner
					
			for (var i = files.length - 1; i >= 0; i--) {
				sendFile(i, files[i], this);
			}
					
			
		}
	}

})

 

loadInterval 배열값은 업로드될 파일별로 저장 장치에 모두 전송되어 전송이 완료될 때까지 저장 상태를 반복적으로 체크하여 summernote에게 모두 저장되었음을 알려주기 위한 인터벌 객체 배열입니다.
선택된 파일 갯수 만큼  sendFile() 함수를 호출하여 비동기 방식으로 파일을 업로드합니다. 이때 파일 전송을 요청하기 전에  spinner 효과를 화면 중앙에 표시해 줍니다.


4) 비동기 방식으로 전송


파일별로 FormData 객체에 추가하여 비 동기 방식으로 파일을 업로드하고 파일이 저장장치에 모두 저장되었는지를 반복적으로 체크하기 위해 loadCheck() 함수를 interval을 사용하여 주기적으로 체크하도록 작성합니다.

 

파일명 : js/sn.js(일부분)

function sendFile(intervalPos, file, el) {
	var form_data = new FormData();
	form_data.append('file', file);
	$.ajax({
		data : form_data,
		type : "POST",
		url : 'upload.up',
		enctype : 'multipart/form-data',
		cache : false,
		contentType : false,
		processData : false,
		success : function(img_name) {
		
			loadInterval[intervalPos] = setInterval(loadCheck.bind(
					null, intervalPos, img_name), 1500);
		}
		
	});
}

 

5) 전송 완료 조치

파일이 모두 저장되었다면 summernote에게 파일을 표시해 주도록 요청하고 작동되었던 spinner를 화면에서 제거해 줍니다.

 

파일명 : js/sn.js(일부분)

 

function loadCheck(intervalPos, img) {
	
	var t = new Image();
	t.onload = function() {
		clearInterval(loadInterval[intervalPos]);
		$('#summernote').summernote('editor.insertImage', img);
		$('#summer').removeClass('spinner');//spinner
	}
	t.src = img;

}

 

 

5) spinner 효과 넣기

파일이 업로드 중일 때 CSS만을 사용한 간단한 spinner를 넣어 보도록 하겠습니다.

참조 : https://stephanwagner.me/only-css-loading-spinner

 

파일명 : css/sn.css

@keyframes spinner {
  to {transform: rotate(360deg);}
}
 
.spinner:before {
  content: '';
  box-sizing: border-box;
  position: absolute;
  top: 50%;
  left: 50%;
  width: 70px;
  height: 70px;
  margin-top: -15px;
  margin-left: -15px;
  border-radius: 50%;
  border: 3px solid #ccc;
  border-top-color: #07d;
  animation: spinner .6s linear infinite;
}

 

위와 같은 스타일 파일을 작성한 후 아래와 같은 방법으로 jQuery를 사용하여 특정 영역에 spinner 효과를 지정합니다.

 

파일명 : webapp/insert.jsp

<script src="https://code.jquery.com/jquery-3.5.1.min.js"></script>
</head>
<body>

<div id="mySpinner">Spinner 1<div >

<script>
$('#mySpinner').addClass('spinner');//spinner 보이기
$('#mySpinner').removeClass('spinner'); // spinneer 감추기
</script>

 

6) 이미지 삭제

에디터 화면에서 이미지를 제거하면 summernote에 표시되고 업로드된 이미지를 삭제하기 위해서는 callbacks 영역에 이미지가 제거되었다는 이벤트를 처리해야 하며 이벤트를 다시 비 동기 방식으로 제거해야 합니다.

 

$('#summernote').summernote({
             … 
	callbacks : {
		onMediaDelete : function(target) {
			deleteFile(target[0].src);
		},
               … 
	}

});

파일을 제거하는 ajax입니다.

 

function deleteFile(target) {
	console.table("target:" + target)
	$.ajax({
		data : {
			target : target
		},
		type : "POST",
		url : 'upload.up?flag=delete', // replace with your url
		cache : false,
		success : function(resp) {
			console.log(resp);
		}
	});
}

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

'프로젝트 > Summernote를 활용한 WYSIWYG 게시판 만들기' 카테고리의 다른 글

1. 개요 및 개발환경  (0) 2023.01.01
2. 기본 구조  (0) 2023.01.01
4. 수정하기  (0) 2023.01.01
5. 삭제하기  (0) 2023.01.01
6. 조회하기  (0) 2023.01.01