diff --git a/WebContent/addon/summernote/summernote-custom.css b/WebContent/addon/summernote/summernote-custom.css new file mode 100644 index 0000000..b84bb22 --- /dev/null +++ b/WebContent/addon/summernote/summernote-custom.css @@ -0,0 +1,187 @@ +/** + * Summernote Custom Styles + * - 이미지 리사이징, 레이아웃, 다이얼로그 스타일 등 + */ + +/* 에디터 이미지 스타일 */ +.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-frame { + width: 100% !important; + max-width: 100% !important; + min-width: 0 !important; + box-sizing: border-box !important; + overflow: hidden !important; + display: block !important; +} +.note-editor .note-editing-area { + width: 100% !important; + max-width: 100% !important; + min-width: 0 !important; + box-sizing: border-box !important; + overflow: hidden !important; +} +.note-editor .note-codable { + width: 100% !important; + max-width: 100% !important; + min-width: 0 !important; + box-sizing: border-box !important; + resize: vertical !important; + white-space: pre-wrap !important; + word-wrap: break-word !important; + word-break: break-all !important; + overflow-x: hidden !important; + overflow-y: auto !important; + overflow-wrap: break-word !important; +} + +/* Summernote 부모 컨테이너 오버플로우 방지 */ +.table_row td .note-editor, +td .note-editor, +div .note-editor { + width: 100% !important; + max-width: 100% !important; +} + +/* 테이블 레이아웃 고정 - Summernote 포함 테이블 */ +.table_row { + table-layout: fixed !important; + width: 100% !important; +} + +/* Summernote가 있는 td 스타일 */ +td:has(.note-editor) { + overflow: hidden !important; + word-break: break-all !important; +} + +/* IE/구형 브라우저 대응 - summernote 래퍼 클래스 */ +.summernote-wrapper { + width: 100% !important; + max-width: 100% !important; + overflow: hidden !important; +} + +/* 이미지 다이얼로그 버튼 overflow 수정 */ +.note-image-dialog .note-modal-footer { + padding: 10px 15px !important; + overflow: visible !important; +} +.note-image-dialog .note-modal-content { + overflow: visible !important; +} +.note-image-dialog .note-btn { + margin: 0 !important; +} + +/* 이미지 업로드 안내 문구 스타일 */ +.image-upload-guide { + margin: 0 0 15px 0; + padding: 10px 12px; + background: #e8f4fd; + border: 1px solid #b8daff; + border-radius: 4px; + font-size: 12px; + color: #004085; + display: flex; + align-items: center; +} +.image-upload-guide i.material-icons { + font-size: 16px; + margin-right: 6px; + color: #0056b3; +} +.image-upload-guide strong { + color: #0056b3; +} + +/* 색상 팔레트 UI overflow 수정 */ +.note-editor .note-toolbar { + overflow: visible !important; +} +.note-editor .note-btn-group { + overflow: visible !important; +} +.note-editor .note-toolbar .note-color.open .note-dropdown-menu { + min-width: 340px !important; + overflow: visible !important; + display: flex !important; + flex-wrap: nowrap !important; +} +.note-editor .note-holder { + overflow: visible !important; +} + +/* 색상 팔레트 2컬럼 레이아웃 유지 */ +.note-editor .note-toolbar .note-color .note-dropdown-menu .note-palette { + display: inline-block !important; + vertical-align: top !important; + width: auto !important; + min-width: 160px !important; + margin: 0 5px !important; + flex-shrink: 0 !important; +} +.note-editor .note-toolbar .note-color .note-dropdown-menu .note-palette .note-palette-title { + margin: 0 0 5px 0 !important; + font-size: 12px !important; + font-weight: bold !important; +} +.note-editor .note-toolbar .note-color .note-dropdown-menu .note-palette .note-color-palette { + width: auto !important; +} + +/* 드롭다운 메뉴 위치 조정 */ +.note-editor .note-dropdown-menu { + z-index: 1050 !important; +} + +/* 전체화면 모드 배경색 */ +.note-editor.note-frame.fullscreen, +.note-editor.note-airframe.fullscreen { + background-color: #fff !important; +} +.note-editor.note-frame.fullscreen .note-editable, +.note-editor.note-airframe.fullscreen .note-editable { + background-color: #fff !important; +} + +/* 이미지 다이얼로그 수정 */ +.note-modal .note-modal-content { + overflow: hidden !important; +} +.note-modal .note-modal-footer { + overflow: hidden !important; + height: auto !important; + padding: 10px 15px !important; +} +.note-modal .note-modal-footer .note-btn { + float: right !important; + margin: 0 !important; +} +.note-modal .note-modal-footer::after { + content: "" !important; + display: table !important; + clear: both !important; +} diff --git a/WebContent/addon/summernote/summernote-custom.js b/WebContent/addon/summernote/summernote-custom.js new file mode 100644 index 0000000..5682528 --- /dev/null +++ b/WebContent/addon/summernote/summernote-custom.js @@ -0,0 +1,247 @@ +/** + * Summernote Custom JavaScript + * - 에디터 초기화, 이미지 처리, 붙여넣기 처리 등 + */ + +/** + * 이미지 다이얼로그에 안내 문구 추가 + * @param {jQuery} $modal - 모달 jQuery 객체 + */ +function addImageUploadGuide($modal) { + // 이미 안내 문구가 있으면 무시 + if ($modal.find('.image-upload-guide').length) return; + + var guideHtml = + '
' + + 'info' + + '지원 형식: JPG, PNG, GIF, BMP, WEBP / 최대 크기: 10MB' + + '
'; + + // summernote-lite 다이얼로그 구조: .note-modal > .note-modal-content > .note-modal-body + var $modalBody = $modal.find('.note-modal-body'); + if ($modalBody.length) { + $modalBody.prepend(guideHtml); + } else { + // 대체 위치: 모달 콘텐츠의 첫 번째 자식 앞에 삽입 + var $modalContent = $modal.find('.note-modal-content'); + if ($modalContent.length) { + $modalContent.find('.note-modal-header').after(guideHtml); + } + } +} + +/** + * 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 || [ + ['font', ['bold', 'underline', 'clear']], + ['fontsize', ['fontsize']], + ['color', ['color']], + ['para', ['ul', 'ol', 'paragraph']], + ['table', ['table']], + ['insert', ['link', 'picture']], + ['view', ['fullscreen', 'codeview', 'help']] + ], + fontSizes: ['8', '9', '10', '11', '12', '14', '16', '18', '20', '24', '28', '36'], + // 이미지 팝오버 설정 - 리사이징 지원 + 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(' maxSize) { + alert('이미지 크기는 10MB를 초과할 수 없습니다.'); + 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 = $('
').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(/]*>[\s\S]*?<\/v:[^>]*>/gi, ''); + cleanedHtml = cleanedHtml.replace(/]*>[\s\S]*?<\/o:[^>]*>/gi, ''); + cleanedHtml = cleanedHtml.replace(/]*>[\s\S]*?<\/w:[^>]*>/gi, ''); + cleanedHtml = cleanedHtml.replace(/]*>[\s\S]*?/gi, ''); + + // Office 네임스페이스 속성 제거 + cleanedHtml = cleanedHtml.replace(/\s*v:[a-z]+="[^"]*"/gi, ''); + cleanedHtml = cleanedHtml.replace(/\s*o:[a-z]+="[^"]*"/gi, ''); + + return cleanedHtml; +} diff --git a/WebContent/jsp/common/include/script.jsp b/WebContent/jsp/common/include/script.jsp index d96ba34..1e96177 100644 --- a/WebContent/jsp/common/include/script.jsp +++ b/WebContent/jsp/common/include/script.jsp @@ -56,251 +56,15 @@ + - - - + +"/>