[Spring] summernote 에디터 적용하기 (이미지 업로드 포함)

yunSeok·2024년 1월 6일
3

사이드 프로젝트

목록 보기
11/14
post-thumbnail

summernote 에디터 구현하는 글입니다!!

프로젝트에 텍스트 에디터를 적용해야겠다!! 라고 생각하시고 구현해보려고 하신 분들은 ckEditor를 들어보셨거나 적용하셨거나.. 적용해보려고 하셨을거라고 생각합니다. ckEdtor가 툴바가 스타일도 깔끔하기도 하고 플러그인도 커스텀해서 사용할 수 있어서 저도 구현해보려고 했습니다... 결론적으로는 플러그인 문제, CSRF 문제, 컨트롤러 문제들과 며칠동안 싸우다가 졌습니다...😭😭😭

해결이 안된다면 다른 방법을 찾아서 구현하는 것도 능력아닐까!! 하는 자기 합리화를 하며... summernote를 구현했습니다. summernote는 ckEditor 보다 구현 난이도는 낮은 편입니다. ckEditor는 유료 플러그인이 있고 보통 React에서 많이 구현하다 보니 공식 문서를 참고해고 Spring 레거시 프로젝트에서 구현하는 것은 어렵더라구요...

summernote는 무료 오픈 소스이기 때문에 중간에 막힐 일은 없을 겁니다. 다른 에디터와 마찬가지로 CDN을 사용하여 쉽게 적용할 수도 있습니다.
단지 조금 단점이라면 css 차제도 CDN를 통해 다 받고 있기 때문에 bootstrap를 프로젝트에서 사용중이라면 css 충돌이 일어날 수도 있습니다.. (제가 그랬습니다..)

css 충돌이 생기면 CDN을 구현할 페이지에만 적용하고 최소한의 수정을 하거나
summernote 파일을 아예 다운로드 하셔서 필요한 css를 편집해서 사용하시거나
해야할 거 같습니다 ㅠㅠ

ckEditor를 구현하지 못해 조금 서러워서 글을 써봤는데.. summernote 구현하는 방법 적어보겠습니다!!


1. 구현 순서

  1. CDN 적용
  2. 자바스크립트 구현
  3. Ajax 구현
  4. 컨트롤러 구현
  5. 글 등록

📌 일단 구현 전에 참고!!

에디터에 이미지를 첨부할 때, Ajax가 요청이 되고 응답 받은 컨트롤러에서

1. 이미지를 서버에 저장
2. 저장할 사진의 이름 설정

하는 과정이 실행되고,

Ajax 요청이 성공적으로 이루어 진다면,

1. summernote 에디터에 서버에 저장된 사진을 출력

해주는 과정이 실행됩니다.

✅ 따라서 컨트롤러, Ajax등 하나라도 맞게 작성되지 않으면 이미지가 summernote 에디터에 첨부조차 되지 않는다는 것을 알고 가셨으면 합니다!!

📌 이미지 업로드하는 서버 문제는요..??

AWS S3 버킷을 만들어서 구현하는 분들도 많더라구요.
저는 이클립스에서 구현중이라서 간단하게 이클립스 서버를 사용해서 구현하도록 하였습니다.


2. 적용해봅시다!!

1. CDN 적용

저는 JSP 파일에서 적용중 입니다!!

<head>
	<link href="https://stackpath.bootstrapcdn.com/bootstrap/3.4.1/css/bootstrap.min.css" rel="stylesheet">
	<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>

이런식으로 적용하실 페이지 안의 <head> 부분에 전부 추가해주도록 하겠습니다.


2. 자바스크립트 구현

<div class="post-form">
	<textarea name="postContent" id="summernote">
	</textarea>
</div>  

<textarea> 태그 id에 summernote를 작성해주었습니다.

<script>	
$('#summernote').summernote({
      
	  // 에디터 크기 설정
	  height: 800,
	  // 에디터 한글 설정
	  lang: 'ko-KR',
	  // 에디터에 커서 이동 (input창의 autofocus라고 생각하시면 됩니다.)
	  toolbar: [
		    // 글자 크기 설정
		    ['fontsize', ['fontsize']],
		    // 글자 [굵게, 기울임, 밑줄, 취소 선, 지우기]
		    ['style', ['bold', 'italic', 'underline','strikethrough', 'clear']],
		    // 글자색 설정
		    ['color', ['color']],
		    // 표 만들기
		    ['table', ['table']],
		    // 서식 [글머리 기호, 번호매기기, 문단정렬]
		    ['para', ['ul', 'ol', 'paragraph']],
		    // 줄간격 설정
		    ['height', ['height']],
		    // 이미지 첨부
		    ['insert',['picture']]
		  ],
		  // 추가한 글꼴
		fontNames: ['Arial', 'Arial Black', 'Comic Sans MS', 'Courier New','맑은 고딕','궁서','굴림체','굴림','돋음체','바탕체'],
		 // 추가한 폰트사이즈
		fontSizes: ['8','9','10','11','12','14','16','18','20','22','24','28','30','36','50','72','96'],
        // focus는 작성 페이지 접속시 에디터에 커서를 위치하도록 하려면 설정해주세요.
		focus : true,
        // callbacks은 이미지 업로드 처리입니다.
		callbacks : {                                                    
			onImageUpload : function(files, editor, welEditable) {   
                // 다중 이미지 처리를 위해 for문을 사용했습니다.
				for (var i = 0; i < files.length; i++) {
					imageUploader(files[i], this);
				}
			}
		}
		
  });

</script>

위 코드를 작성해주시면 화면에 이런식으로 출력되게 됩니다!!


3. Ajax 구현

Ajax에서 이미지 업로드 요청을 처리하게 됩니다.

summernote에서는 Ajax를 이용해 이미지 업로드 처리는 하는게 개인적으로는 장점이었습니다. 일반 자바스크립트에서는 에러 내용도 안보일때가 많아서... Ajax는 요청과 응답에 대해서 확실한 정보를 얻을 수 있어서 편했습니다.

컨트롤러를 두 번 거칠 필요없이 Ajax 성공 처리에서 이미지를 뿌려주도록 하였습니다.

function imageUploader(file, el) {
	var formData = new FormData();
	formData.append('file', file);
  
	$.ajax({                                                              
		data : formData,
		type : "POST",
        // url은 자신의 이미지 업로드 처리 컨트롤러 경로로 설정해주세요.
		url : '/post/image-upload',  
		contentType : false,
		processData : false,
		enctype : 'multipart/form-data',                                  
		success : function(data) {   
			$(el).summernote('insertImage', "${pageContext.request.contextPath}/assets/images/upload/"+data, function($image) {
				$image.css('width', "100%");
			});
            // 값이 잘 넘어오는지 콘솔 확인 해보셔도됩니다.
			console.log(data);
		}
	});
}

저는 여기서 insertImage 할 때, ${pageContext.request.contextPath}/assets/images/upload/ 라고 작성을 해주었습니다. 이클립스 서버를 사용하고 있기 때문에 컨트롤러에서 받아오는 응답 값이 조금 다르기 때문인데요, 왜 이렇게 작성했는지는 컨트롤러에서 설명하도록 하겠습니다.


4. 컨트롤러 구현


@RestController
@RequestMapping("/post")
@RequiredArgsConstructor


    @Autowired
	private final WebApplicationContext context; 
    
    @PostMapping(value = "/image-upload")
    // @RequestParam은 자바스크립트에서 설정한 이름과 반드시 같아야합니다.
	public ResponseEntity<?> imageUpload(@RequestParam("file") MultipartFile file) throws IllegalStateException, IOException {
		try {
			// 서버에 저장할 경로
			String uploadDirectory = context.getServletContext().getRealPath("/resources/assets/images/upload"); 
			
			// 업로드 된 파일의 이름
			String originalFileName = file.getOriginalFilename();
			
			// 업로드 된 파일의 확장자
			String fileExtension = originalFileName.substring(originalFileName.lastIndexOf("."));
			
			// 업로드 될 파일의 이름 재설정 (중복 방지를 위해 UUID 사용)
			String uuidFileName = UUID.randomUUID().toString() + fileExtension;
			
			// 위에서 설정한 서버 경로에 이미지 저장
			file.transferTo(new File(uploadDirectory, uuidFileName));
		
			System.out.println("************************ 업로드 컨트롤러 실행 ************************");
			System.out.println(uploadDirectory);
			
			// Ajax에서 업로드 된 파일의 이름을 응답 받을 수 있도록 해줍니다.
			return ResponseEntity.ok(uuidFileName);
		} catch (Exception e) {
			return ResponseEntity.badRequest().body("이미지 업로드 실패");
		}
		
	}

여기서 uploadDirectory(서버 저장 경로) + uuidFileName(파일 이름) 으로 응답 해줄 수도 있지만 서버 저장 경로까지 넘겨버리게 되면 404에러가 발생하더라구요..
서버 경로를 찾지 못해서 발생하는 문제 같아서 컨트롤러에서는 파일 이름만 응답해주도록 하고,
Ajax 응답시 ${pageContext.request.contextPath} 설정으로 경로를 찾도록 해주었습니다.


여기까지 구현하게 되면 기존 에디터에 업로드 했을때는

이런식으로 summernote에서 지원하는 파일 업로드 방식인 base64로 인코딩되어서 업로드 되어지는 방식에서,

컨트롤러에서 설정한 실제 경로로 잘 적용되는 것을 볼 수 있습니다.

아래는 업로드하는 화면 보여드릴게요.


5. 글 등록

에디터에 작성한 정보를 DB에 저장할때는 HTML 태그로 저장되는데요, 그냥 글이 저장된다고 생각하시면 될 거 같아요. 이미지를 첨부할때 이미 사진은 저장되고 에디터에 작성한 이미지나 글은 HTML 형태로 저장되어 집니다.

DB를 확인해보면

이런식으로 이미지나 글이 저장되게 됩니다!!

만약 글 등록을 Ajax로 처리하고 계신다면

   var editorContent = $('#summernote').summernote('code');
    
   var formData = new FormData();
     formData.append("postContent", editorContent);

   $.ajax({
        type: "POST",
        url: '/post/add',
        data: formData,
                .
                .
                .

$('#summernote').summernote('code');
이런식으로 에디터에 작성한 정보들을 HTML 태그 형식으로 받아와서 저장할 수 있습니다.


❗️❗️ 경로 설정도 다 되는데 로컬에서 업로드가 안된다면!?

📌 프로그램에서 톰켓 서버를 사용중이고, 만약 사진이 출력이 안된다면 톰켓 서버의 context.xml 파일에서 심볼릭 링크 설정을 추가하고 다시 업로드 해보시는걸 추천드립니다.

context.xml 입니다.

<Context>
	<Resources allowLinking="true" />
    <!-- Default set of monitored resources. If one of these changes, the    -->
    <!-- web application will be reloaded.                                   -->
    <WatchedResource>WEB-INF/web.xml</WatchedResource>
    <WatchedResource>WEB-INF/tomcat-web.xml</WatchedResource>
    <WatchedResource>${catalina.base}/conf/web.xml</WatchedResource>

    <!-- Uncomment this to disable session persistence across Tomcat restarts -->
    <!--
    <Manager pathname="" />
    -->
</Context>

여기서 <Context> 안에 <Resources allowLinking="true" />를 추가해주시면됩니다.

심볼릭 링크를 사용하게 되면,
웹 애플리케이션에서 루트 디렉토리와 같은 민감한 디렉토리에 접근할 수 있게 되기 때문에 보안을 고려해야 합니다...

또한, 파일 시스템에 대한 접근이 느려질 수 있습니다...

따라서 임시로 사용해주시면 될거같아요..!!
다음엔 보안을 고려해서 AWS S3 서버를 이용한다던지 해야할거같습니다.

혹시 코드 궁금하시면..
https://github.com/cyseok/project

0개의 댓글