Refactor Summernote initialization for consistency and improved readability
This commit is contained in:
+13
-32
@@ -9,16 +9,7 @@
|
|||||||
<param-name>contextConfigLocation</param-name>
|
<param-name>contextConfigLocation</param-name>
|
||||||
<param-value>/WEB-INF/applicationContext.xml</param-value>
|
<param-value>/WEB-INF/applicationContext.xml</param-value>
|
||||||
</context-param>
|
</context-param>
|
||||||
<!-- XSS Filter -->
|
<!-- Encoding Filter (반드시 다른 필터보다 먼저 적용되어야 함) -->
|
||||||
<filter>
|
|
||||||
<filter-name>CrossScriptingFilter</filter-name>
|
|
||||||
<filter-class>com.eactive.eai.rms.common.filter.CrossScriptingFilter</filter-class>
|
|
||||||
</filter>
|
|
||||||
<filter-mapping>
|
|
||||||
<filter-name>CrossScriptingFilter</filter-name>
|
|
||||||
<url-pattern>/*</url-pattern>
|
|
||||||
</filter-mapping>
|
|
||||||
|
|
||||||
<filter>
|
<filter>
|
||||||
<filter-name>encodingFilter</filter-name>
|
<filter-name>encodingFilter</filter-name>
|
||||||
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
|
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
|
||||||
@@ -31,30 +22,20 @@
|
|||||||
<param-value>true</param-value>
|
<param-value>true</param-value>
|
||||||
</init-param>
|
</init-param>
|
||||||
</filter>
|
</filter>
|
||||||
|
<!-- encodingFilter를 모든 요청에 먼저 적용 -->
|
||||||
|
<filter-mapping>
|
||||||
|
<filter-name>encodingFilter</filter-name>
|
||||||
|
<url-pattern>/*</url-pattern>
|
||||||
|
</filter-mapping>
|
||||||
|
|
||||||
|
<!-- XSS Filter (encodingFilter 다음에 적용) -->
|
||||||
<filter>
|
<filter>
|
||||||
<filter-name>encodingFilterEUCKR</filter-name>
|
<filter-name>CrossScriptingFilter</filter-name>
|
||||||
<filter-class>org.springframework.web.filter.CharacterEncodingFilter</filter-class>
|
<filter-class>com.eactive.eai.rms.common.filter.CrossScriptingFilter</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>
|
||||||
<filter-mapping>
|
<filter-mapping>
|
||||||
<filter-name>encodingFilterEUCKR</filter-name>
|
<filter-name>CrossScriptingFilter</filter-name>
|
||||||
<url-pattern>*.excel</url-pattern>
|
<url-pattern>/*</url-pattern>
|
||||||
</filter-mapping>
|
|
||||||
<filter-mapping>
|
|
||||||
<filter-name>encodingFilter</filter-name>
|
|
||||||
<url-pattern>*.file</url-pattern>
|
|
||||||
</filter-mapping>
|
|
||||||
<!-- Tomcat 에서는 필요. Weblogic 에서는 불필요함 -->
|
|
||||||
<filter-mapping>
|
|
||||||
<filter-name>encodingFilter</filter-name>
|
|
||||||
<url-pattern>*.json</url-pattern>
|
|
||||||
</filter-mapping>
|
</filter-mapping>
|
||||||
|
|
||||||
<!--
|
<!--
|
||||||
@@ -168,6 +149,6 @@
|
|||||||
</jsp-config>
|
</jsp-config>
|
||||||
|
|
||||||
<session-config>
|
<session-config>
|
||||||
<session-timeout>30</session-timeout>
|
<session-timeout>30</session-timeout>
|
||||||
</session-config>
|
</session-config>
|
||||||
</web-app>
|
</web-app>
|
||||||
@@ -60,8 +60,247 @@
|
|||||||
<script src="<c:url value="/addon/summernote/summernote-cleaner.js"/>"></script>
|
<script src="<c:url value="/addon/summernote/summernote-cleaner.js"/>"></script>
|
||||||
<script src="<c:url value="/addon/summernote/lang/summernote-ko-KR.js"/>"></script>
|
<script src="<c:url value="/addon/summernote/lang/summernote-ko-KR.js"/>"></script>
|
||||||
|
|
||||||
|
<!-- Summernote 스타일 -->
|
||||||
|
<style>
|
||||||
|
.note-editable img {
|
||||||
|
max-width: 100%;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
.note-editable img.note-image-resizing {
|
||||||
|
outline: 2px solid #007bff;
|
||||||
|
}
|
||||||
|
/* 이미지 리사이즈 핸들 스타일 */
|
||||||
|
.note-control-sizing {
|
||||||
|
display: block !important;
|
||||||
|
}
|
||||||
|
.note-control-holder {
|
||||||
|
position: relative;
|
||||||
|
}
|
||||||
|
.note-control-holder .note-control-sizing {
|
||||||
|
position: absolute;
|
||||||
|
width: 10px;
|
||||||
|
height: 10px;
|
||||||
|
border: 1px solid #333;
|
||||||
|
background-color: #fff;
|
||||||
|
}
|
||||||
|
/* 코드뷰 모드 가로 사이즈 고정 */
|
||||||
|
.note-editor .note-codable {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 100% !important;
|
||||||
|
box-sizing: border-box !important;
|
||||||
|
resize: vertical !important;
|
||||||
|
white-space: pre-wrap !important;
|
||||||
|
word-wrap: break-word !important;
|
||||||
|
overflow-x: auto !important;
|
||||||
|
}
|
||||||
|
.note-editor.note-frame {
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
|
|
||||||
<script language="javascript">
|
<script language="javascript">
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Summernote 에디터 공통 초기화 함수
|
||||||
|
* - 이미지 붙여넣기 시 data-uri(Base64) 방식으로 처리
|
||||||
|
* - 이미지 리사이징 지원
|
||||||
|
* @param {string} selector - 에디터 selector (예: '#contents')
|
||||||
|
* @param {object} customOptions - 추가 옵션 (선택사항)
|
||||||
|
*/
|
||||||
|
function initSummernote(selector, customOptions) {
|
||||||
|
customOptions = customOptions || {};
|
||||||
|
|
||||||
|
// 기본 설정
|
||||||
|
var defaultOptions = {
|
||||||
|
placeholder: customOptions.placeholder || '여기에 내용을 입력하세요.',
|
||||||
|
height: customOptions.height || 300,
|
||||||
|
lang: 'ko-KR',
|
||||||
|
toolbar: customOptions.toolbar || [
|
||||||
|
['style', ['style']],
|
||||||
|
['font', ['bold', 'underline', 'clear']],
|
||||||
|
['color', ['color']],
|
||||||
|
['para', ['ul', 'ol', 'paragraph']],
|
||||||
|
['table', ['table']],
|
||||||
|
['insert', ['link', 'picture']],
|
||||||
|
['view', ['fullscreen', 'codeview', 'help']]
|
||||||
|
],
|
||||||
|
// 이미지 팝오버 설정 - 리사이징 지원
|
||||||
|
popover: {
|
||||||
|
image: [
|
||||||
|
['resize', ['resizeFull', 'resizeHalf', 'resizeQuarter', 'resizeNone']],
|
||||||
|
['float', ['floatLeft', 'floatRight', 'floatNone']],
|
||||||
|
['remove', ['removeMedia']]
|
||||||
|
]
|
||||||
|
},
|
||||||
|
callbacks: {
|
||||||
|
onInit: function() {
|
||||||
|
var $editable = $(selector).next('.note-editor').find('.note-editable');
|
||||||
|
$editable.addClass('editor-content');
|
||||||
|
$editable.on('keydown', function(e) {
|
||||||
|
if (e.keyCode === 8) {
|
||||||
|
e.stopPropagation();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// 커스텀 onInit 콜백 호출
|
||||||
|
if (customOptions.callbacks && customOptions.callbacks.onInit) {
|
||||||
|
customOptions.callbacks.onInit.call(this);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 이미지 업로드 처리 - data-uri(Base64) 방식
|
||||||
|
onImageUpload: function(files) {
|
||||||
|
for (var i = 0; i < files.length; i++) {
|
||||||
|
insertImageAsDataUri(files[i], $(selector));
|
||||||
|
}
|
||||||
|
},
|
||||||
|
// 붙여넣기 시 이미지 처리 (MS Office 등에서 복사한 이미지 포함)
|
||||||
|
onPaste: function(e) {
|
||||||
|
var clipboardData = e.originalEvent.clipboardData;
|
||||||
|
if (!clipboardData || !clipboardData.items) return;
|
||||||
|
|
||||||
|
var items = clipboardData.items;
|
||||||
|
var imageFile = null;
|
||||||
|
|
||||||
|
// 1. 먼저 순수 이미지 파일이 있는지 확인
|
||||||
|
for (var i = 0; i < items.length; i++) {
|
||||||
|
if (items[i].type.indexOf('image') !== -1) {
|
||||||
|
imageFile = items[i].getAsFile();
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 이미지 파일이 있으면 data-uri로 변환하여 삽입
|
||||||
|
if (imageFile) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
insertImageAsDataUri(imageFile, $(selector));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. HTML 붙여넣기인 경우 (MS Office 등) - 이미지 태그 정리
|
||||||
|
var htmlData = clipboardData.getData('text/html');
|
||||||
|
if (htmlData && htmlData.indexOf('<img') !== -1) {
|
||||||
|
e.preventDefault();
|
||||||
|
e.stopPropagation();
|
||||||
|
|
||||||
|
// HTML에서 이미지 src 추출 및 정리
|
||||||
|
var cleanedHtml = cleanPastedHtml(htmlData);
|
||||||
|
|
||||||
|
// 에디터에 포커스 후 HTML 삽입
|
||||||
|
$(selector).summernote('focus');
|
||||||
|
setTimeout(function() {
|
||||||
|
$(selector).summernote('pasteHTML', cleanedHtml);
|
||||||
|
}, 10);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 커스텀 옵션 병합 (callbacks는 위에서 별도 처리)
|
||||||
|
var mergedOptions = $.extend(true, {}, defaultOptions, customOptions);
|
||||||
|
|
||||||
|
// callbacks는 기본 콜백을 유지하면서 커스텀 콜백 추가
|
||||||
|
mergedOptions.callbacks = defaultOptions.callbacks;
|
||||||
|
if (customOptions.callbacks) {
|
||||||
|
// onInit은 위에서 이미 처리됨
|
||||||
|
// 다른 커스텀 콜백 추가
|
||||||
|
for (var key in customOptions.callbacks) {
|
||||||
|
if (key !== 'onInit' && key !== 'onImageUpload' && key !== 'onPaste') {
|
||||||
|
mergedOptions.callbacks[key] = customOptions.callbacks[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
$(selector).summernote(mergedOptions);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 이미지를 Base64 data-uri로 변환하여 에디터에 삽입
|
||||||
|
* @param {File} file - 이미지 파일
|
||||||
|
* @param {jQuery} $editor - summernote 에디터 jQuery 객체
|
||||||
|
*/
|
||||||
|
function insertImageAsDataUri(file, $editor) {
|
||||||
|
// 파일 크기 제한 (5MB)
|
||||||
|
var maxSize = 5 * 1024 * 1024;
|
||||||
|
if (file.size > maxSize) {
|
||||||
|
alert('이미지 크기는 5MB를 초과할 수 없습니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 이미지 타입 확인
|
||||||
|
if (!file.type.match(/image\/(jpeg|jpg|png|gif|bmp|webp)/i)) {
|
||||||
|
alert('지원하지 않는 이미지 형식입니다. (jpeg, png, gif, bmp, webp만 지원)');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
var reader = new FileReader();
|
||||||
|
reader.onload = function(e) {
|
||||||
|
var dataUri = e.target.result;
|
||||||
|
$editor.summernote('insertImage', dataUri, function($image) {
|
||||||
|
// 이미지 스타일 설정
|
||||||
|
$image.css({
|
||||||
|
'max-width': '100%',
|
||||||
|
'height': 'auto'
|
||||||
|
});
|
||||||
|
$image.attr('data-filename', file.name);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
reader.onerror = function() {
|
||||||
|
alert('이미지 읽기에 실패했습니다.');
|
||||||
|
};
|
||||||
|
reader.readAsDataURL(file);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 붙여넣기된 HTML 정리 (MS Office 등에서 복사한 내용)
|
||||||
|
* - 불필요한 Office 속성 제거
|
||||||
|
* - 이미지 태그 정리 (alt, v:shapes 등 제거)
|
||||||
|
* @param {string} html - 원본 HTML
|
||||||
|
* @returns {string} 정리된 HTML
|
||||||
|
*/
|
||||||
|
function cleanPastedHtml(html) {
|
||||||
|
// 임시 div에서 HTML 파싱
|
||||||
|
var $temp = $('<div>').html(html);
|
||||||
|
|
||||||
|
// 이미지 태그 정리
|
||||||
|
$temp.find('img').each(function() {
|
||||||
|
var $img = $(this);
|
||||||
|
var src = $img.attr('src');
|
||||||
|
|
||||||
|
// src가 없거나 file:// 프로토콜이면 제거
|
||||||
|
if (!src || src.indexOf('file://') === 0) {
|
||||||
|
$img.remove();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 불필요한 속성 제거 (Office 관련)
|
||||||
|
var attributesToRemove = ['alt', 'v:shapes', 'o:title', 'style', 'class', 'id', 'name'];
|
||||||
|
attributesToRemove.forEach(function(attr) {
|
||||||
|
$img.removeAttr(attr);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 깨끗한 스타일만 적용
|
||||||
|
$img.css({
|
||||||
|
'max-width': '100%',
|
||||||
|
'height': 'auto'
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
// VML 태그 제거 (v:, o:, w: 등)
|
||||||
|
var cleanedHtml = $temp.html();
|
||||||
|
cleanedHtml = cleanedHtml.replace(/<v:[^>]*>[\s\S]*?<\/v:[^>]*>/gi, '');
|
||||||
|
cleanedHtml = cleanedHtml.replace(/<o:[^>]*>[\s\S]*?<\/o:[^>]*>/gi, '');
|
||||||
|
cleanedHtml = cleanedHtml.replace(/<w:[^>]*>[\s\S]*?<\/w:[^>]*>/gi, '');
|
||||||
|
cleanedHtml = cleanedHtml.replace(/<!\[if[^>]*>[\s\S]*?<!\[endif\]>/gi, '');
|
||||||
|
|
||||||
|
// Office 네임스페이스 속성 제거
|
||||||
|
cleanedHtml = cleanedHtml.replace(/\s*v:[a-z]+="[^"]*"/gi, '');
|
||||||
|
cleanedHtml = cleanedHtml.replace(/\s*o:[a-z]+="[^"]*"/gi, '');
|
||||||
|
|
||||||
|
return cleanedHtml;
|
||||||
|
}
|
||||||
|
|
||||||
function buttonControl() {
|
function buttonControl() {
|
||||||
if (arguments.length == 0) {
|
if (arguments.length == 0) {
|
||||||
$("img[level], button[level]").hide();
|
$("img[level], button[level]").hide();
|
||||||
|
|||||||
@@ -67,28 +67,10 @@
|
|||||||
buttonControl(false);
|
buttonControl(false);
|
||||||
}
|
}
|
||||||
|
|
||||||
$('#faqAnswer').summernote({
|
// Summernote 에디터 초기화 (이미지 붙여넣기 data-uri 방식, 리사이징 지원)
|
||||||
|
initSummernote('#faqAnswer', {
|
||||||
placeholder: '여기에 답변을 입력하세요.',
|
placeholder: '여기에 답변을 입력하세요.',
|
||||||
height: 300,
|
height: 300
|
||||||
toolbar: [
|
|
||||||
['style', ['style']],
|
|
||||||
['font', ['bold', 'underline', 'clear']],
|
|
||||||
['color', ['color']],
|
|
||||||
['para', ['ul', 'ol', 'paragraph']],
|
|
||||||
['table', ['table']],
|
|
||||||
['insert', ['link', 'picture']],
|
|
||||||
['view', ['fullscreen', 'codeview', 'help']]
|
|
||||||
],
|
|
||||||
callbacks: {
|
|
||||||
onInit: function() {
|
|
||||||
$('.note-editable').addClass('editor-content');
|
|
||||||
$('.note-editable').on('keydown', function(e) {
|
|
||||||
if (e.keyCode === 8) {
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (key) {
|
if (key) {
|
||||||
|
|||||||
@@ -109,26 +109,10 @@
|
|||||||
var returnUrl = getReturnUrlForReturn();
|
var returnUrl = getReturnUrlForReturn();
|
||||||
var key = "${param.id}";
|
var key = "${param.id}";
|
||||||
|
|
||||||
$('#responseDetail').summernote({
|
// Summernote 에디터 초기화 (이미지 붙여넣기 data-uri 방식, 리사이징 지원)
|
||||||
height: 200,
|
initSummernote('#responseDetail', {
|
||||||
toolbar: [
|
placeholder: '여기에 답변을 입력하세요.',
|
||||||
['style', ['style']],
|
height: 200
|
||||||
['font', ['bold', 'underline', 'clear']],
|
|
||||||
['color', ['color']],
|
|
||||||
['para', ['ul', 'ol', 'paragraph']],
|
|
||||||
['table', ['table']],
|
|
||||||
['insert', ['link']],
|
|
||||||
['view', ['codeview', 'help']]
|
|
||||||
],
|
|
||||||
callbacks: {
|
|
||||||
onInit: function () {
|
|
||||||
$('.note-editable').on('keydown', function (e) {
|
|
||||||
if (e.keyCode === 8) {
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (key) {
|
if (key) {
|
||||||
|
|||||||
@@ -137,28 +137,10 @@
|
|||||||
|
|
||||||
init();
|
init();
|
||||||
|
|
||||||
$('#contents').summernote({
|
// Summernote 에디터 초기화 (이미지 붙여넣기 data-uri 방식, 리사이징 지원)
|
||||||
|
initSummernote('#contents', {
|
||||||
placeholder: '여기에 내용을 입력하세요.',
|
placeholder: '여기에 내용을 입력하세요.',
|
||||||
height: 300,
|
height: 300
|
||||||
toolbar: [
|
|
||||||
['style', ['style']],
|
|
||||||
['font', ['bold', 'underline', 'clear']],
|
|
||||||
['color', ['color']],
|
|
||||||
['para', ['ul', 'ol', 'paragraph']],
|
|
||||||
['table', ['table']],
|
|
||||||
['insert', ['link']],
|
|
||||||
['view', ['codeview', 'help']]
|
|
||||||
],
|
|
||||||
callbacks: {
|
|
||||||
onInit: function() {
|
|
||||||
$('.note-editable').addClass('editor-content');
|
|
||||||
$('.note-editable').on('keydown', function(e) {
|
|
||||||
if (e.keyCode === 8) {
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (key) {
|
if (key) {
|
||||||
|
|||||||
@@ -127,28 +127,10 @@
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
$('#contents').summernote({
|
// Summernote 에디터 초기화 (이미지 붙여넣기 data-uri 방식, 리사이징 지원)
|
||||||
placeholder: '여기에 답변을 입력하세요.',
|
initSummernote('#contents', {
|
||||||
height: 300,
|
placeholder: '여기에 약관 내용을 입력하세요.',
|
||||||
toolbar: [
|
height: 300
|
||||||
['style', ['style']],
|
|
||||||
['font', ['bold', 'underline', 'clear']],
|
|
||||||
['color', ['color']],
|
|
||||||
['para', ['ul', 'ol', 'paragraph']],
|
|
||||||
['table', ['table']],
|
|
||||||
['insert', ['link']],
|
|
||||||
['view', ['fullscreen', 'codeview', 'help']]
|
|
||||||
],
|
|
||||||
callbacks: {
|
|
||||||
onInit: function() {
|
|
||||||
$('.note-editable').addClass('editor-content');
|
|
||||||
$('.note-editable').on('keydown', function(e) {
|
|
||||||
if (e.keyCode === 8) {
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (agreementsType && revision) {
|
if (agreementsType && revision) {
|
||||||
|
|||||||
@@ -54,7 +54,8 @@ function detail(key){
|
|||||||
}
|
}
|
||||||
|
|
||||||
$(document).ready(function() {
|
$(document).ready(function() {
|
||||||
$('#emailTemplate').summernote({
|
// Summernote 에디터 초기화 (이미지 붙여넣기 data-uri 방식, 리사이징 지원)
|
||||||
|
initSummernote('#emailTemplate', {
|
||||||
placeholder: '여기에 내용을 입력하세요.',
|
placeholder: '여기에 내용을 입력하세요.',
|
||||||
height: 300,
|
height: 300,
|
||||||
toolbar: [
|
toolbar: [
|
||||||
@@ -65,16 +66,7 @@ $(document).ready(function() {
|
|||||||
['table', ['table']],
|
['table', ['table']],
|
||||||
['insert', ['link', 'picture', 'video']],
|
['insert', ['link', 'picture', 'video']],
|
||||||
['view', ['codeview', 'help']]
|
['view', ['codeview', 'help']]
|
||||||
],
|
]
|
||||||
callbacks: {
|
|
||||||
onInit: function() {
|
|
||||||
$('.note-editable').on('keydown', function(e) {
|
|
||||||
if (e.keyCode === 8) {
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
var key ="${param.id}";
|
var key ="${param.id}";
|
||||||
|
|||||||
@@ -69,6 +69,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function initializeSummernote() {
|
function initializeSummernote() {
|
||||||
|
// Summernote 에디터 초기화 (이미지 붙여넣기 data-uri 방식, 리사이징 지원)
|
||||||
var commonOptions = {
|
var commonOptions = {
|
||||||
height: 300,
|
height: 300,
|
||||||
toolbar: [
|
toolbar: [
|
||||||
@@ -79,21 +80,12 @@
|
|||||||
['table', ['table']],
|
['table', ['table']],
|
||||||
['insert', ['link', 'picture', 'video']],
|
['insert', ['link', 'picture', 'video']],
|
||||||
['view', ['codeview', 'help']]
|
['view', ['codeview', 'help']]
|
||||||
],
|
]
|
||||||
callbacks: {
|
|
||||||
onInit: function () {
|
|
||||||
$('.note-editable').on('keydown', function (e) {
|
|
||||||
if (e.keyCode === 8) {
|
|
||||||
e.stopPropagation();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
$('#description').summernote(commonOptions);
|
initSummernote('#description', commonOptions);
|
||||||
$('#apiRequestSpec').summernote(commonOptions);
|
initSummernote('#apiRequestSpec', commonOptions);
|
||||||
$('#apiResponseSpec').summernote(commonOptions);
|
initSummernote('#apiResponseSpec', commonOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
function detail(key) {
|
function detail(key) {
|
||||||
|
|||||||
@@ -15,12 +15,9 @@ class RequestWrapper extends HttpServletRequestWrapper {
|
|||||||
|
|
||||||
// GS인증 크로스사이트스크립트 보안성 문제로 XSS 공격을 방지하기 위한 악성 스크립트 패턴 정의.
|
// GS인증 크로스사이트스크립트 보안성 문제로 XSS 공격을 방지하기 위한 악성 스크립트 패턴 정의.
|
||||||
// 2025.05.27 패턴 추가
|
// 2025.05.27 패턴 추가
|
||||||
|
// 2025.12.30 src 패턴 수정 - 화이트리스트 방식 (http, https, data:image/ 만 허용)
|
||||||
private static final Pattern[] PATTERNS = new Pattern[] {
|
private static final Pattern[] PATTERNS = new Pattern[] {
|
||||||
Pattern.compile("<script>(.*?)</script>", Pattern.CASE_INSENSITIVE),
|
Pattern.compile("<script>(.*?)</script>", Pattern.CASE_INSENSITIVE),
|
||||||
Pattern.compile("src[\r\n]*=[\r\n]*\\\'(.*?)\\\'",
|
|
||||||
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
|
|
||||||
Pattern.compile("src[\r\n]*=[\r\n]*\\\"(.*?)\\\"",
|
|
||||||
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
|
|
||||||
Pattern.compile("</script>", Pattern.CASE_INSENSITIVE),
|
Pattern.compile("</script>", Pattern.CASE_INSENSITIVE),
|
||||||
Pattern.compile("<script(.*?)>",
|
Pattern.compile("<script(.*?)>",
|
||||||
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
|
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL),
|
||||||
@@ -46,6 +43,16 @@ class RequestWrapper extends HttpServletRequestWrapper {
|
|||||||
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL)
|
Pattern.CASE_INSENSITIVE | Pattern.MULTILINE | Pattern.DOTALL)
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// src 속성 화이트리스트 패턴 - data:image/만 허용
|
||||||
|
private static final Pattern SRC_ATTR_PATTERN = Pattern.compile(
|
||||||
|
"(src\\s*=\\s*)([\"'])([^\"']*?)\\2",
|
||||||
|
Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
// 허용되는 src 값 패턴 (화이트리스트) - data:image/만 허용
|
||||||
|
private static final Pattern ALLOWED_SRC_PATTERN = Pattern.compile(
|
||||||
|
"^data:image/.*",
|
||||||
|
Pattern.CASE_INSENSITIVE);
|
||||||
|
|
||||||
|
|
||||||
public RequestWrapper(HttpServletRequest servletRequest) {
|
public RequestWrapper(HttpServletRequest servletRequest) {
|
||||||
super(servletRequest);
|
super(servletRequest);
|
||||||
@@ -123,6 +130,10 @@ class RequestWrapper extends HttpServletRequestWrapper {
|
|||||||
|
|
||||||
// script 문자열을 완전히 제거
|
// script 문자열을 완전히 제거
|
||||||
value = value.replaceAll("(?i)\\bscript\\b", "");
|
value = value.replaceAll("(?i)\\bscript\\b", "");
|
||||||
|
|
||||||
|
// src 속성 화이트리스트 필터링 - 허용된 프로토콜만 통과
|
||||||
|
value = filterSrcAttribute(value);
|
||||||
|
|
||||||
StringBuilder sb = new StringBuilder();
|
StringBuilder sb = new StringBuilder();
|
||||||
|
|
||||||
value = convertChars(value, sb);
|
value = convertChars(value, sb);
|
||||||
@@ -133,6 +144,37 @@ class RequestWrapper extends HttpServletRequestWrapper {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* src 속성을 화이트리스트 방식으로 필터링
|
||||||
|
* 허용: data:image/ (Base64 이미지)
|
||||||
|
* 비허용: 그 외 모든 프로토콜
|
||||||
|
*/
|
||||||
|
private String filterSrcAttribute(String value) {
|
||||||
|
if (value == null || !value.toLowerCase().contains("src")) {
|
||||||
|
return value;
|
||||||
|
}
|
||||||
|
|
||||||
|
Matcher matcher = SRC_ATTR_PATTERN.matcher(value);
|
||||||
|
StringBuffer result = new StringBuffer();
|
||||||
|
|
||||||
|
while (matcher.find()) {
|
||||||
|
String srcPrefix = matcher.group(1); // src=
|
||||||
|
String quote = matcher.group(2); // " 또는 '
|
||||||
|
String srcValue = matcher.group(3); // src 값
|
||||||
|
|
||||||
|
// 빈 값이거나 화이트리스트에 매칭되면 유지
|
||||||
|
if (srcValue.isEmpty() || ALLOWED_SRC_PATTERN.matcher(srcValue).matches()) {
|
||||||
|
matcher.appendReplacement(result, Matcher.quoteReplacement(srcPrefix + quote + srcValue + quote));
|
||||||
|
} else {
|
||||||
|
// 허용되지 않는 프로토콜은 src="" 로 대체
|
||||||
|
matcher.appendReplacement(result, Matcher.quoteReplacement(srcPrefix + quote + quote));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
matcher.appendTail(result);
|
||||||
|
|
||||||
|
return result.toString();
|
||||||
|
}
|
||||||
|
|
||||||
// '(', ')', '"' 문자는 getMethod()가 "get"일 때만 변환됨.
|
// '(', ')', '"' 문자는 getMethod()가 "get"일 때만 변환됨.
|
||||||
// 이는 gridData에서 사용되는 큰따옴표(")와 변환 함수에서 사용하는 괄호('(', ')')의 처리를 위함.
|
// 이는 gridData에서 사용되는 큰따옴표(")와 변환 함수에서 사용하는 괄호('(', ')')의 처리를 위함.
|
||||||
private String convertChars(String value, StringBuilder sb) {
|
private String convertChars(String value, StringBuilder sb) {
|
||||||
|
|||||||
Reference in New Issue
Block a user