ckeditor5 및 이미지 업로더 설치
ckeditor를 몇 번 사용해 본 것 같은데, 버전이 업데이트되면서 설치 방법이나 옵션들이 달라진다.
비단 ckeditor 뿐만이 아니겠지만, 그래도 정확한 것은 사이트 공식 매뉴얼이니 매뉴얼을 보고 설정하는 방법에 익숙해져야겠다.
ckeditor 설치
https://ckeditor.com/ckeditor-5/download/
다운로드 사이트에 가서 타입을 선택한다.
npm으로 설치해도 되고 압축파일을 다운받아도 되고 CDN을 직접 이용해도 된다.
여기서는 직접 다운로드 받아 설치해 보자.
(ckeditor5 버전은 설치, 라이브러리 등 npm 관련된 부분이 많다. 그래서 npm으로 진행하지 않으면 불편한 부분들이 많이 생긴다.)
아래 주소에서는 플러그인을 직접 선택하고, 언어를 세팅해서 다운로드 받을 수 있다.
(예를 들어 소스보기, 폰트크기, 색상 등을 추가로 넣을 수 있다.)
https://ckeditor.com/ckeditor-5/online-builder/
설치는 본인 편한 것으로...
ckeditor 적용
https://ckeditor.com/docs/ckeditor5/latest/installation/getting-started/quick-start.html
적용하는 방법은 간단하다. 영역을 잡고 함수에 영역을 넘겨주면 된다.
나는 textarea에 적용을 했는데 아래처럼 height가 사라졌다.
그래서 스타일을 추가했다. 높이는 400px, 글자는 13px
<style>
.ck-editor__editable { height: 400px; }
.ck-content { font-size: 13px; }
</style>
형태가 그럭저럭 잘 나왔다.
한글 설정 및 p태그 설정
언어셋을 한글로 설정해보자.
상단에 한글 리소스를 추가하고 에디터 호출 시 언어를 설정해 준다.
<script src="/ckeditor/ckeditor.js"></script>
<script src="/ckeditor/translations/ko.js"></script>
<script>
ClassicEditor.create(document.querySelector('#contents'), {
language:'ko'
}).then(editor => {
window.editor = editor;
}).catch( error => {
console.error( error );
});
</cript>
이미 한글을 적용한 상태라 캡처이미지는 모두 한글로 설정되어 있다.
글 작성 후 엔터를 입력하면 줄 사이의 간격이 벌어지는데, 엔터 입력 시 p태그가 추가되기 때문이다.
ckeditor4 버전에는 엔터 입력시 br, div, p 태그 중 선택할 수 있게 되어있었는데, 5 버전에는 없다.
검색해보니 p태그의 마진을 없애라고 설명이 나와있다.(공식적인 설명은 아니다.)
그래서 스타일을 추가했다.
<style>
.ck-editor__editable { height: 400px; }
.ck-editor__editable p {margin: 0}
.ck-content { font-size: 13px; }
</style>
또 작성 후 뷰페이지에도 적용이 되어야 하기 때문에 뷰영역에 클래스를 추가해 뷰 영역만 p태그의 마진이 없도록 했다.
<style>
.ck-editor__editable { height: 400px; }
.ck-editor__editable p {margin: 0}
.ck-content { font-size: 13px; }
.contentsArea p {margin:0}
</style>
입력할 때도 볼 때도 p태그의 마진을 잘 처리했다.
이미지 업로드
에디터의 이미지 아이콘을 클릭하면 파일을 선택할 수 있는 팝업은 뜨지만 아무런 동작도 하지 않는다.
콘솔에 보면 에러를 확인할 수 있다.
이제 이미지 업로드를 구현해보자.
이미지 업로드 방법은 위 링크처럼 몇개가 있다. 금액을 지불하면 쉽게 처리할 수 있는 유료 제품들이다.
무료도 몇개 있는데, Base64 upload는 이미지를 base64로 만드는 방법인데, base64로 인코딩할 경우 보통 30% 이상의 오버헤드가 발생한다. 대용량의 경우 별로 추천하지 못한다고 한다. 여기서는 Simple upload adapter를 이용해본다.
Simple upload adapter는 아래 링크에서 잘 설명이 되어 있어 이곳을 참고했다.
https://jjong-factory.tistory.com/55
UploadAdapter.js 파일을 구현
먼저 uploadAdpater.js를 구현한다.
class UploadAdapter {
constructor(loader) {
this.loader = loader;
}
upload() {
return this.loader.file.then( file => new Promise(((resolve, reject) => {
this._initRequest();
this._initListeners( resolve, reject, file );
this._sendRequest( file );
})))
}
_initRequest() {
const xhr = this.xhr = new XMLHttpRequest();
xhr.open('POST', '/common/fms/ckeditor5Upload.do', true);
xhr.responseType = 'json';
}
_initListeners(resolve, reject, file) {
const xhr = this.xhr;
const loader = this.loader;
const genericErrorText = '파일을 업로드 할 수 없습니다.'
xhr.addEventListener('error', () => {reject(genericErrorText)})
xhr.addEventListener('abort', () => reject())
xhr.addEventListener('load', () => {
const response = xhr.response
if(!response || response.error) {
return reject( response && response.error ? response.error.message : genericErrorText );
}
resolve({
default: response.url
})
})
}
_sendRequest(file) {
const data = new FormData()
data.append('upload',file)
this.xhr.send(data)
}
}
중간에 서버가 업로드를 처리할 URL를 설정했다.
/common/fms/ckeditor5Upload.do
업로드 시 name은 'upload'로 설정했다.
서버 파일 업로드 구현
서버는 java spring으로 구성했다.
@ResponseBody
@RequestMapping("/common/fms/ckeditor5Upload.do")
public void fileUpload( MultipartHttpServletRequest multiRequest, HttpServletRequest request, HttpServletResponse response) {
try {
final String real_save_path = MainGlobals.FILE_STORE_PATH + "/contents/";
// 폴더가 없을 경우 생성
File saveFolder = new File(EgovWebUtil.filePathBlackList(real_save_path));
if(!saveFolder.exists() || saveFolder.isFile()) {
saveFolder.mkdirs();
}
final Map<String, MultipartFile> files = multiRequest.getFileMap();
MultipartFile fileload = (MultipartFile)files.get("upload");
//filename 취득
String fileName = fileload.getOriginalFilename();
int index = fileName.lastIndexOf(".");
String ext = fileName.substring(index+1);
Random ran = new Random(System.currentTimeMillis());
fileName = System.currentTimeMillis()+"_"+(int)(ran.nextDouble()*10000)+"."+ext;
//폴더 경로 설정
String newfilename = real_save_path + File.separator + fileName;
fileload.transferTo(new File(EgovWebUtil.filePathBlackList(newfilename)));
JSONObject outData = new JSONObject();
outData.put("uploaded", true);
outData.put("url", request.getScheme() + "://" + request.getServerName() + ":" + request.getServerPort() + "/common/fms/getImageForContents.do?fileNm=" + fileName);
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.getWriter().print(outData.toString());
} catch (Exception e) {
System.out.println("오류발생");
}
}
서버의 업로더는 json형태로 반환해주는데 key값으로 url을 넘겨준다.
url은 /common/fms/getImageForContents.do로 했다.
@RequestMapping("/common/fms/getImageForContents.do")
public void getImageForContents(SessionVO sessionVO, ModelMap model, @RequestParam Map<String, Object> commandMap, HttpServletResponse response) throws Exception {
String fileNm = (String)commandMap.get("fileNm");
String fileStr = MainGlobals.FILE_STORE_PATH + "contents/";
File tmpDir = new File(fileStr);
if(!tmpDir.exists()) {
tmpDir.mkdirs();
}
FileInputStream fis = null;
BufferedInputStream in = null;
ByteArrayOutputStream bStream = null;
try {
fis = new FileInputStream(new File(fileStr, fileNm));
in = new BufferedInputStream(fis);
bStream = new ByteArrayOutputStream();
int imgByte;
while ((imgByte = in.read()) != -1) {
bStream.write(imgByte);
}
String type = "";
String ext = fileNm.substring(fileNm.lastIndexOf(".") + 1).toLowerCase();
if ("jpg".equals(ext)) {
type = "image/jpeg";
} else {
type = "image/" + ext;
}
response.setHeader("Content-Type", type);
response.setContentLength(bStream.size());
bStream.writeTo(response.getOutputStream());
response.getOutputStream().flush();
response.getOutputStream().close();
} finally {
EgovResourceCloseHelper.close(bStream, in, fis);
}
}
클라이언트에 Upload Adapter 설정
다시 클라이언트 설정을 해보자.
만들어진 'UploadAdapter.js'를 추가하고 , 에디터 생성 시 'extraPlugins'이름으로 함수를 만들어 추가해준다.
<script src="/ckeditor/ckeditor.js"></script>
<script src="/ckeditor/translations/ko.js"></script>
<script src="/ckeditor/UploadAdapter.js"></script>
function MyCustomUploadAdapterPlugin(editor) {
editor.plugins.get('FileRepository').createUploadAdapter = (loader) => {
return new UploadAdapter(loader)
}
}
ClassicEditor.create(document.querySelector('#contents'), {
language:'ko',
extraPlugins: [MyCustomUploadAdapterPlugin]
}).then(editor => {
window.editor = editor;
}).catch( error => {
console.error( error );
});
let editor_data = editor.getData();
호출된 에디터의 데이터는 위처럼 호출할 수 있다.
함수 호출 성공 시 window.editor에 editor를 추가하는데, 전역 객체인 window를 생략할 수 있어 editor에 직접 getData()를 호출할 수 있다.
이미지 업로드 성공!!
XSS 필터
소스내에 html 태그가 변환되지 않도록 보안상의 이유로 필터에 변환 함수를 추가 했었다.
private String cleanXSS(String value) {
if(type == 1) {
value = value.replaceAll("<s.*>.*</script>","");
value = value.replaceAll("(\\.location|location\\.|onload=|\\.cookie|alert\\(|window\\.open\\(|onmouse|onkey|onclick|view\\-source\\:)+", "/");
} else if(type == 2){
value = value.replaceAll("<", "<").replaceAll(">", ">");
value = value.replaceAll("eval\\((.*)\\)", "");
value = value.replaceAll("[\\\"\\\'][\\s]*javascript:(.*)[\\\"\\\']", "\"\"");
}
return value;
}
그래서 에디터로 작성된 태그들이 모두 변환되어 실제 뷰에서는 태그가 적용되지 않았다.
그래서 필터에 예외값을 두기로 했다.
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
int[] b = isNotFindXSS(request);
if(b[0] == 1) {
if(b[1] == 0) {
chain.doFilter(new XssWrapper((HttpServletRequest)request, type), response);
} else {
// Xss 필터를 통과하지 않고 그냥 지나친다.
chain.doFilter(request, response);
}
} else {
// 비정상적 요청인 경우 경고 페이지로 이동
RequestDispatcher dispatcher = request.getRequestDispatcher(infoUrl);
dispatcher.forward(request, response);
}
}
public int[] isNotFindXSS(ServletRequest request) {
ArrayList<String> params = new ArrayList();
String name = "";
String value = "";
int[] b = {1, 0};
Enumeration names = request.getParameterNames();
while(names.hasMoreElements()) {
name = (String)names.nextElement();
value = request.getParameter(name);
params.add(value);
if(name.equals("passKey")) {
b[1] = 1;
}
}
return b;
}
다시 등록 및 수정 페이지에서 에디터를 이용할 경우 예외를 추가할 수 있도록 키를 추가한다.
<form:form commandName="dmsBrdPostVO" id="detailForm" action="${requestScope['javax.servlet.forward.request_uri']}" enctype="multipart/form-data">
...
<input type="hidden" name="passKey" />
</form:form>