2020년 02월 17일
업데이트:
SummerNote
- Summer note는 글쓰는 것을 효율적으로 하기위한 API
- API는 이미 만들어진 것을 원하는 형태로 가공해서 사용하는것
- 홈페이지 예제를 보면서 어떻게 사용하는지 분석해서 활용하기
-
글자 크기, 색, 폰트, 이미지, 동영상이 어떻게 첨부되는지 원리 파악하기 -> 글을 작성하거나, 이미지 등을 업로드하면 HTML 코드로 번역돼서 작성됨.-> DB저장 시 HTML 코드를 유지해야 한다.-> 섬머노트 사용해서 저장할 때는 크로스 사이팅 스크립팅 처리를 하지 않음.
- https://summernote.org/ -> getting started -> js,css 다운로드->압축풀기
- Deployed Resources -> webapp -> resources -> summernote 폴더 생성
- 생성된 폴더에 섬머노트 드래그 앤 드롭해서 넣어주기.
font폴더 -> 섬머노트에서 제공하는 폰트들이 모여있음
summernote.css/js -> 섬머노트에 관련된 모든 파일이 있음
summernote-lite.css/js -> 중요한 것만 압축되어있음.
summernote-lite.js.map
lang폴더 -> summernote-ko-KR -> 섬머노트 한글버전으로 바꾸기.
- 생성된 폴더에 섬머노트 드래그 앤 드롭해서 넣어주기.
섬머노트 활용하기 위한 코드 작성
// mySummernote.js
$(document).ready(function() {
$('#summernote').summernote({
width : 1000,
height : 600,
lang : 'ko-KR', // 언어 : 한국어
// 이미지 업로드 이벤트가 발생했을 때 callback 함수 수행
callbacks : {
onImageUpload : function(files){
// files : 업로드된 이미지가 배열로 저장되어 있음.
// editor==this : 이미지가 업로드된 섬머노트 에디터.
sendFile(files[0], this);
}
}
});
});
// 섬머노트에 업로드된 이미지를 비동기로 서버로 전송하여 저장하는 함수
function sendFile(file,editor){
var formData = new FormData();
// FormData : form 태그 내부 값 전송을 위한 객체로
// 추가된 값을 k=v 형태로 쉽게 생성해주는 객체
// 알아서 쿼리 스트링을 만들어준다.
formData.append("uploadFile", file);
$.ajax({
url : "insertImage", // spring/board/2/insertImage
type : "post",
enctype : "multipart/form-data", // 파일 전송 형식으로 인코딩 지정
data : formData, // {"uploadFile": file}
contentType : false, // 서버로 전성되는 데이터 형식
// 기본값 : application/x-www-form-urlencoded; charset=utf-8(텍스트를 의미함)
// false : 데이터 형식을 바이트 코드 있는 그대로.
cache : false,
// 캐시메모리(메모리에 미리 저장) false = 메모리에 저장안하고 바로 서버로 전송
processData : false,
// 서버로 전달되는 값을 쿼리스트링으로 전달할 경우 true, 아니면 false
// 쿼리스트링으로 전달 -> 주소에 담아서 데이터를 전송할 지
dataType : "json",
success : function(at){ //at == 파일 정보(객체), fileName, filePath 담겨있음.
// console.log(at);
// 자바스크립트를 이용한 contextPath 얻어오는 방법
var contextPath = location.pathname.substring(0, window.location.pathname.indexOf("/",2));
// localhost:8080/spring
// 저장된 이미지를 summernote 에디터에 반영(삽입)
$(editor).summernote('editor.insertImage', contextPath + at.filePath + "/" + at.fileName);
}
});
}
글 등록 코드 수정하기
- Summernote가 적용될 페이지
// boardInsert2.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8" pageEncoding="UTF-8"%>
<%@ taglib prefix="fmt" uri="http://java.sun.com/jsp/jstl/fmt" %>
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>게시글 등록</title>
<style>
.insert-label {
display: inline-block;
width: 80px;
line-height: 40px
}
</style>
<!-- summernote 사용 시 필요한 css 파일 추가 -->
<link rel="stylesheet" href="${contextPath }/resources/summernote/css/summernote-lite.css">
</head>
<body>
<jsp:include page="../common/header.jsp"/>
<!-- summernote 사용 시 필요한 js 파일 추가 -->
<script src="${contextPath }/resources/summernote/js/summernote-lite.js"></script> <!-- 이 코드가 있어야 섬머노트 사용 가능 -->
<script src="${contextPath }/resources/summernote/js/summernote-ko-KR.js"></script> <!-- 한글 패치 -->
<script src="${contextPath }/resources/summernote/js/mySummernote.js"></script> <!-- 개인이 만든 js -->
<div class="container">
<div class="container pb-5 mb-5">
<h3>게시글 등록</h3>
<hr>
<!-- 파일 업로드 시 enctype="multipart/form-data" 지정 -->
<!--
- enctype : form 태그 데이터가 서버로 제출 될 때 인코딩 되는 방법을 지정. (POST 방식일 때만 사용 가능)
- application/x-www-form-urlencoded : 모든 문자를 서버로 전송하기 전에 인코딩 (form태그 기본값)
- multipart/form-data : 모든 문자를 인코딩 하지 않음.(원본 데이터가 유지되어 이미지, 파일등을 서버로 전송 할 수 있음.)
-->
<form action="insertAction" enctype="multipart/form-data" method="post" role="form" onsubmit="return validate();">
<div class="mb-2">
<label class="input-group-addon mr-3 insert-label">카테고리</label>
<select class="custom-select" id="category" name="categoryName" style="width: 150px;">
<option value="10">운동</option>
<option value="20">영화</option>
<option value="30">음악</option>
<option value="40">요리</option>
<option value="50">게임</option>
<option value="60">기타</option>
</select>
</div>
<div class="form-inline mb-2">
<label class="input-group-addon mr-3 insert-label">제목</label>
<input type="text" class="form-control" id="title" name="boardTitle" size="70">
</div>
<div class="form-inline mb-2">
<label class="input-group-addon mr-3 insert-label">작성자</label>
<h5 class="my-0" id="writer">${loginMember.memberId}</h5>
</div>
<div class="form-inline mb-2">
<label class="input-group-addon mr-3 insert-label">작성일</label>
<h5 class="my-0" id="today">
<!--java.util.Date를 이용해서 객체를 하나 만들어 now라고 부른다. now의 출력 형식은 yyyy-MM-dd로 설정한다. -->
<jsp:useBean id="now" class="java.util.Date" />
<fmt:formatDate value="${now}" pattern="yyyy-MM-dd"/>
</h5>
</div>
<hr>
<div class="form-inline mb-2">
<label class="input-group-addon mr-3 insert-label">썸네일</label>
<div class="boardImg" id="titleImgArea">
<img id="titleImg" width="200" height="200">
</div>
</div>
<!-- 파일 업로드 하는 부분 -->
<div id="fileArea">
<input type="file" id="img0" name="images" onchange="LoadImg(this,0)">
</div>
<div class="form-group">
<div>
<label for="content">내용</label>
</div>
<textarea class="form-control" id="summernote" name="boardContent"
rows="10" style="resize: none;"></textarea>
</div>
<hr class="mb-4">
<div class="text-center">
<button type="submit" class="btn btn-success">등록</button>
<a class="btn btn-success float-right" href="${sessionScope.returnListURL}">목록으로</a>
</div>
</form>
</div>
</div>
<jsp:include page="../common/footer.jsp"/>
<script>
// 유효성 검사
function validate() {
if ($("#title").val().trim().length == 0) {
alert("제목을 입력해 주세요.");
$("#title").focus();
return false;
}
if ($("#content").val().trim().length == 0) {
alert("내용을 입력해 주세요.");
$("#content").focus();
return false;
}
}
// 이미지 영역을 클릭할 때 파일 첨부 창이 뜨도록 설정하는 함수
$(function(){
$("#fileArea").hide(); // #fileArea 요소를 숨김.
$(".boardImg").on("click", function(){ // 이미지 영역이 클릭 되었을 때
// 클릭한 이미지 영역 인덱스 얻어오기
var index = $(".boardImg").index(this);
// -> 클릭된 요소가 .boardImg 중 몇번째 인덱스인지 반환
//console.log(index);
// 클릭된 영역 인덱스에 맞는 input file 태그 클릭
$("#img" + index).click();
});
});
// 각각의 영역에 파일을 첨부 했을 경우 미리 보기가 가능하도록 하는 함수
function LoadImg(value, num) {
// value.files : 파일이 업로드되어 있으면 true
// value.files[0] : 여러 파일 중 첫번째 파일이 업로드 되어 있으면 true
if(value.files && value.files[0]){ // 해당 요소에 업로드된 파일이 있을 경우
var reader = new FileReader();
// 자바스크립트 FileReader
// 웹 애플리케이션이 비동기적으로 데이터를 읽기 위하여
// 읽을 파일을 가리키는 File 혹은 Blob객체를 이용해
// 파일의 내용을 읽고 사용자의 컴퓨터에 저장하는 것을 가능하게 해주는 객체
reader.readAsDataURL(value.files[0]);
// FileReader.readAsDataURL()
// 지정된의 내용을 읽기 시작합니다.
// Blob완료되면 result속성 data:에 파일 데이터를 나타내는 URL이 포함 됩니다.
reader.onload = function(e){
// FileReader.onload
// load 이벤트의 핸들러.
// 이 이벤트는 읽기 동작이 성공적으로 완료 되었을 때마다 발생합니다.
// 읽어들인 내용(이미지 파일)을 화면에 출력
$(".boardImg").eq(num).children("img").attr("src", e.target.result);
// e.target.result : 파일 읽기 동작을 성공한 요소가 읽어들인 파일 내용
}
}
}
</script>
</body>
</html>
- summernote에 업로드된 이미지 저장 Controller
// BoardController.java
// summernote에 업로드된 이미지 저장 Controller
@ResponseBody
@RequestMapping("{type}/insertImage")
public String insertImage(HttpServletRequest request,
@RequestParam("uploadFile") MultipartFile uploadFile) {
// 실제 파일 경로(파일을 어디에 저장할지) 지정할 때 사용 -> HttpServletRequest request
// 비동기로 파일 저장 == 저장하려는 이미지 얻어옴
// ->@RequestParam("uploadFile") MultipartFile uploadFile (uploadFile->파일자체를 담고있음 )
// 서버에 파일(이미지)를 저장할 폴더 경로 얻어오기
String savePath = request.getSession().getServletContext().getRealPath("resources/infoImages");
Attachment at = service.insertImage(uploadFile,savePath);
// 어느 위치에, 어떤 파일을 저장하겠다는 service 수행
// java에서 js로 객체 전달할 때 JSON을 사용한다.
// 문자열이지만 자바스크립트 객체 모양 "{}"을 하고있다.
// gson을 사용함.
return new Gson().toJson(at);
}
- summernote 업로드 이미지 저장 Service 구현
// BoardServiceImpl.java
// summernote 업로드 이미지 저장 Service 구현
@Override
public Attachment insertImage(MultipartFile uploadFile, String savePath) {
// 중복을 방지하기 위해, 파일명 변경해 줌
String fileName = rename(uploadFile.getOriginalFilename());
// 웹 상 접근 주소
String filePath = "/resources/infoImages";
// 돌려 보내줄 파일 정보를 Attachment 객체에 담아서 전달.
Attachment at = new Attachment();
at.setFilePath(filePath);
at.setFileName(fileName);
// 서버에 파일 저장(transferTo())
try {
uploadFile.transferTo(new File(savePath + "/" + fileName));
// savePath : ~~~~/infoImages fileName = 20210217~~.png
}catch(Exception e) {
e.printStackTrace();
throw new InsertAttachmentFailException("summernote 파일 업로드 실패");
}
return at;
}
- 게시글 등록 Controller 수정
// BoardController.java
// 게시글 등록Controller
@RequestMapping("{type}/insertAction")
public String insertAction(@PathVariable("type") int type,
@ModelAttribute Board board,
@ModelAttribute("loginMember") Member loginMember,
@RequestParam(value="images", required=false) List<MultipartFile> images,
HttpServletRequest request, RedirectAttributes ra) {
// @ModelAttribute Board board == categoryName,boardTitle, boardContent 가져옴
// @RequestParam(value="images", required=false) List<MultipartFile> images
// -> <input type="file" name="images"> 태그를 모두 얻어와 images라는 List에 매핑
//System.out.println("type:" + type);
//System.out.println("board:" + board);
//System.out.println("loginMember:" + loginMember);
// map을 이용하여 필요한 데이터 모두 담기
Map<String,Object> map = new HashMap<String,Object>();
map.put("memberNo", loginMember.getMemberNo());
map.put("boardTitle", board.getBoardTitle());
map.put("boardContent", board.getBoardContent());
map.put("categoryCode",board.getCategoryName()); // name 은 categoryName 이지만 value는 코드로 되어있다.
map.put("boardType",type);
// 파일 업로드 확인
/* for(int i=0; i<images.size(); i++) {
System.out.println("images["+i+"] : " + images.get(i).getOriginalFilename());
} */
// 파일이 업로드 되지 않은 부분도 출력되고 있음을 확인 함.
// == 모든 input type="file" 태그가 순서대로 넘어오고 있음을 확인.
// 넘어오는 순서를 file level로 사용 가능하다.
// 파일 저장 경로 설정
// HttpServletRequest 객체가 있어야지만 파일 저장 경로를 얻어올 수 있다.
// HttpServletRequest는 Controller에서만 사용 가능.
String savePath = null;
if(type == 1) {
savePath = request.getSession().getServletContext().getRealPath("resources/uploadImages");
}else {
savePath = request.getSession().getServletContext().getRealPath("resources/infoImages");
}
// System.out.println(savePath);
// 게시글 map, 이미지 images, 저장경로 savePath
// 게시글 삽입 Service 호출
int result = service.insertBoard(map, images, savePath);
String url = null;
// 게시글 삽입 결과에 따른 View 연결 처리
if(result>0) {
swalIcon= "success";
swalTitle= "게시글 등록 성공";
url = "redirect:"+result;
// 새로 작성한 게시글 상세 조회 시 목록으로 버튼 경로 지정하기
request.getSession().setAttribute("returnListURL", "../list/"+type);
}else {
swalIcon="error";
swalTitle = "게시글 등록 실패";
url = "redirect:insert";
}
ra.addFlashAttribute("swalIcon",swalIcon);
ra.addFlashAttribute("swalTitle",swalTitle);
return url;
}
- 게시글 등록 ServiceImpl 수정
// BoardServiceImpl.java
// 게시글 등록 Service 구현
@Transactional(rollbackFor = Exception.class)
@Override
public int insertBoard(Map<String, Object> map, List<MultipartFile> images, String savePath) {
int result =0; // 최종 결과 저장 변수 선언
// 1) 게시글 번호 얻어오기 -> SEQ_BNO.NEXTVAL
int boardNo = dao.selectNextNo();
// 2) 게시글 삽입
if(boardNo>0) { // 다음 게시글 번호를 얻어온 경우
map.put("boardNo", boardNo); // map에 boardNo 추가
// 크로스 사이트 스크립팅 방지 처리
// summernote API 사용 시 html, css 로 작성되기 때문에 크로스 사이트 스크립팅 방지 처리가 되면 안된다.
// 게시판 타입별로 크로스 사이트 스크립팅 방지 처리를 선택적으로 진행.
if( (int)map.get("boardType")==1) { //자유게시판
String boardTitle = (String)map.get("boardTitle");
String boardContent = (String)map.get("boardContent");
// 크로스 사이트 스크립팅 방지 처리 적용
boardTitle = replaceParameter(boardTitle);
boardContent = replaceParameter(boardContent);
// 처리된 문자열을 다시 map 에 세팅
map.put("boardTitle",boardTitle);
map.put("boardContent",boardContent);
// 개행문자 처리 -> 화면에서 JSTL을 이용해 처리할 예정
}
// 게시글 삽입 DAO 메소드 호출
result = dao.insertBoard(map);
// 3) 게시글 삽입 성공 시 이미지 정보 삽입
if(result>0) { // 게시글 삽입에 성공한다면 result에 글 번호 등록(상세 조회를 위해서)
// 이미지 정보를 Attachment 객체에 저장하여 List에 추가
List<Attachment> uploadImages = new ArrayList<Attachment>();
// images.get(i).getOriginalFileName() 메소드를 수행하면 업로드된 파일의 원본 파일명이 출력된다.
// --> 중복 상황을 대비하여 파일명 변경하는 코드 필요.(rename() 메소드)
// DB에 저장할 웹 상 접근 주소(filePath)
String filePath = null;
if((int)map.get("boardType") ==1) {
filePath = "/resources/uploadImages";
}else {
filePath ="/resources/infoImages";
}
// for문을 이용하여 파일정보가 담긴 images를 반복 접근
// -> 업로드된 파일이 있을 경우에만 uploadImages 리스트에 추가
for(int i=0; i<images.size(); i++) {
// i == 인덱스 == FileLevel과 같은 값
// 현재 접근한 images의 요소(MultipartFile)에 업로드된 파일이 있는지 확인.
if( !images.get(i).getOriginalFilename().equals("") ) {
// 파일이 업로드 된 경우 == 업로드된 원본 파일명이 있는 경우
// 원본 파일명 변경
String fileName = rename(images.get(i).getOriginalFilename());
// Attachment 객체 생성
Attachment at = new Attachment(filePath, fileName, i, boardNo);
// filePath, fileName, fileLever, parentBoardNo
uploadImages.add(at); // 리스트에 추가
}
}
// uploadImage 확인
/* for(Attachment at : uploadImages) {
System.out.println(at);
} */
//-----------------------------summernote----------------------------
// 게시판 타입이 2번(summernote를 이용한 게시글 작성)일 경우
// boardContent 내부에 업로드된 이미지 정보 (filePath, fileName)이 들어있음
// -> boardContent에서 <img> 태그만을 골라내어 img 태그의 src 속성값을 추출 후 filePath, fileName을 얻어냄.
if((int)map.get("boardType") == 2) {
Pattern pattern = Pattern.compile("<img[^>]*src=[\"']?([^>\"']+)[\"']?[^>]*>");
//img 태그 src 추출 정규표현식
// SummerNote에 작성된 내용 중 img태그의 src속성의 값을 검사하여 매칭되는 값을 Matcher객체에 저장함.
Matcher matcher = pattern.matcher((String)map.get("boardContent"));
String fileName = null; // 파일명 변환 후 저장할 임시 참조 변수
String src = null; // src 속성값을 저장할 임시 참조 변수
// matcher.find() : Matcher 객체에 저장된 값(검사를 통해 매칭된 src 속성 값)에 반복 접근하여 값이 있을 경우 true
while(matcher.find()){
src= matcher.group(1); // 매칭된 src 속성값을 Matcher 객체에서 꺼내서 src에 저장
filePath = src.substring(src.indexOf("/", 2), src.lastIndexOf("/"));
// 파일명을 제외한 경로만 별도로 저장.--> 2번째 / 부터 마지막 / 까지
fileName = src.substring(src.lastIndexOf("/")+ 1); // 업로드된 파일명만 잘라서 별도로 저장.
// Attachment 객체를 이용하여 DB에 파일 정보를 저장
Attachment at = new Attachment(filePath, fileName, 1, boardNo);
// 파일 레벨 숫자는 상관없음,
uploadImages.add(at);
}
}
//-----------------------------summernote----------------------------
if(!uploadImages.isEmpty()) {
// 파일 정보 삽입 DAO 호출
result = dao.insertAttachmentList(uploadImages);
// result == 삽입된 행의 개수
if(result == uploadImages.size()) {
result = boardNo;
// 반환 될 result 에 boardNo 저장. (상세정보 페이지로 넘어가기 위해서)
// MultipartFile.transferTo()
// -> MultipartFile 객체에 저장된 파일을 지정된 경로에 실제 파일의 형태로 변h환하여 저장하는 메소드.
int size = 0;
if((int)map.get("boardType")==1) {
size = uploadImages.size();
}else if (!images.get(0).getOriginalFilename().equals("")){
size = images.size(); //==1
}
for(int i=0; i<size; i++) {
// uploadImages라는 List에는 Attachment가 담겨져 있다. (파일이 업로드된 개수만큼 담겨져있으면서,파일 레벨이 지정되어 있음)
// uploadImages.get(i).getFileLevel() = i에 따라서 파일 레벨이 나온다.
// images는 input type="file" 태그 정보를 담은 , 실제 파일이 업로드 된 MultipartFile 들이 모여있는 List
// savePath : 실제 컴퓨터의 서버 경로
// uploadImages를 만들 때 각 요소의 파일 레벨은 images의 index를 이용하여 부여한다.--> images의 index를 통해 uploadImages의 레벨을 구할 수 있다.
// uploadImages.get(i).getFileName() : 업로드된 i번째 이미지의 변경된 이름을 가져온다.
// 실제 파일들을 변경된 파일의 이름으로 새로운 경로에 저장한다.
try {
images.get(uploadImages.get(i).getFileLevel()).transferTo(new File(savePath+"/"+uploadImages.get(i).getFileName()) );
}catch(Exception e) {
e.printStackTrace();
// transferTo()는 IOException(CheckedException)을 발생시킨다. 어쩔 수 없이 try catch를 작성해서 예외처리 함.
// 예외가 처리되면 @Transactional이 정상 동작하지 못 함.
// 꼭 예외처리를 하지 않아도 되는 UncheckedException을 강제 발생시켜 @Transactional이 예외가 발생했음을 감지하게 함.
// 상황에 맞는 사용자 정의 예외를 작성함.
throw new InsertAttachmentFailException("파일 서버 저장 실패");
}
}
}else { // 파일 정보를 DB에 삽입 실패
throw new InsertAttachmentFailException("파일 정보 DB 삽입 실패");
}
}else { // 업로드된 이미지가 없을 경우
result = boardNo;
}
}
}
return result;
}
공유하기
Twitter Google+ LinkedIn
댓글남기기