SummernoteEditor.vue 5.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204
  1. <template>
  2. <div class="summernote-wrapper">
  3. <div ref="editorContainer" class="editor-container">
  4. <textarea
  5. ref="summernoteElement"
  6. :id="editorId"
  7. :placeholder="placeholder"
  8. ></textarea>
  9. </div>
  10. </div>
  11. </template>
  12. <script setup>
  13. import { ref, onMounted, onBeforeUnmount, watch, nextTick } from 'vue'
  14. const props = defineProps({
  15. modelValue: {
  16. type: String,
  17. default: ''
  18. },
  19. height: {
  20. type: Number,
  21. default: 400
  22. },
  23. placeholder: {
  24. type: String,
  25. default: '내용을 입력하세요'
  26. }
  27. })
  28. const emit = defineEmits(['update:modelValue'])
  29. const summernoteElement = ref(null)
  30. const editorContainer = ref(null)
  31. const editorId = `summernote-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`
  32. let isInitialized = false
  33. const initializeSummernote = async () => {
  34. if (process.client && summernoteElement.value && !isInitialized) {
  35. try {
  36. // jQuery와 관련 라이브러리 로드
  37. const { default: $ } = await import('jquery')
  38. window.jQuery = window.$ = $
  39. // Bootstrap CSS를 Summernote 컨테이너에만 적용
  40. await import('~/assets/scss/summernote-bootstrap-full.scss')
  41. // Summernote CSS 로드
  42. await import('summernote/dist/summernote-bs4.min.css')
  43. // Bootstrap JS 로드
  44. await import('bootstrap')
  45. // Summernote JS 로드
  46. await import('summernote/dist/summernote-bs4.min.js')
  47. // 한국어 언어팩 로드
  48. try {
  49. await import('summernote/dist/lang/summernote-ko-KR.min.js')
  50. } catch (err) {
  51. console.log('Korean language pack not loaded:', err)
  52. }
  53. // DOM 요소가 준비될 때까지 대기
  54. await nextTick()
  55. if (summernoteElement.value && $) {
  56. const $editor = $(summernoteElement.value)
  57. // Summernote 초기화
  58. $editor.summernote({
  59. height: props.height,
  60. placeholder: props.placeholder,
  61. lang: 'ko-KR',
  62. toolbar: [
  63. ['style', ['style']],
  64. ['font', ['bold', 'italic', 'underline', 'strikethrough', 'clear']],
  65. ['fontname', ['fontname']],
  66. ['fontsize', ['fontsize']],
  67. ['color', ['color']],
  68. ['para', ['ul', 'ol', 'paragraph']],
  69. ['table', ['table']],
  70. ['insert', ['link', 'picture', 'video']],
  71. ['view', ['fullscreen', 'help']]
  72. ],
  73. callbacks: {
  74. onChange: function(contents) {
  75. emit('update:modelValue', contents)
  76. },
  77. onInit: function() {
  78. console.log('Summernote initialized successfully')
  79. if (props.modelValue) {
  80. $editor.summernote('code', props.modelValue)
  81. }
  82. isInitialized = true
  83. },
  84. onImageUpload: function(files) {
  85. console.log('Image upload:', files)
  86. // 이미지 업로드 처리가 필요한 경우 여기에 구현
  87. }
  88. }
  89. })
  90. console.log('Summernote setup completed')
  91. }
  92. } catch (error) {
  93. console.error('Summernote initialization error:', error)
  94. isInitialized = false
  95. }
  96. }
  97. }
  98. const destroySummernote = () => {
  99. if (process.client && window.$ && summernoteElement.value && isInitialized) {
  100. try {
  101. const $editor = window.$(summernoteElement.value)
  102. if ($editor.length && $editor.summernote) {
  103. $editor.summernote('destroy')
  104. isInitialized = false
  105. console.log('Summernote destroyed')
  106. }
  107. } catch (error) {
  108. console.error('Error destroying Summernote:', error)
  109. }
  110. }
  111. }
  112. // Props 변경 감지
  113. watch(() => props.modelValue, (newValue) => {
  114. if (process.client && window.$ && summernoteElement.value && isInitialized) {
  115. const $editor = window.$(summernoteElement.value)
  116. if ($editor.length && $editor.summernote) {
  117. const currentCode = $editor.summernote('code')
  118. if (currentCode !== newValue) {
  119. $editor.summernote('code', newValue || '')
  120. }
  121. }
  122. }
  123. })
  124. onMounted(async () => {
  125. // 약간의 지연을 두고 초기화
  126. setTimeout(initializeSummernote, 100)
  127. })
  128. onBeforeUnmount(() => {
  129. destroySummernote()
  130. })
  131. </script>
  132. <style scoped>
  133. .summernote-wrapper {
  134. width: 100%;
  135. }
  136. .editor-container {
  137. width: 100%;
  138. min-height: 50px;
  139. }
  140. /* Summernote 에디터 스타일 커스터마이징 */
  141. :deep(.note-editor) {
  142. border: 1px solid #ddd !important;
  143. border-radius: 4px !important;
  144. }
  145. :deep(.note-editor.note-frame) {
  146. margin-bottom: 0;
  147. }
  148. :deep(.note-editor .note-toolbar) {
  149. background-color: #f8f9fa;
  150. border-bottom: 1px solid #ddd;
  151. padding: 8px;
  152. }
  153. :deep(.note-editor .note-editing-area .note-editable) {
  154. font-family: inherit;
  155. font-size: 14px;
  156. line-height: 1.6;
  157. padding: 15px;
  158. min-height: 300px;
  159. }
  160. :deep(.note-editor.note-airframe .note-editing-area .note-editable),
  161. :deep(.note-editor.note-frame .note-editing-area .note-editable) {
  162. padding: 15px;
  163. }
  164. /* 로딩 중일 때의 fallback 스타일 */
  165. textarea {
  166. width: 100%;
  167. min-height: 400px;
  168. padding: 15px;
  169. border: 1px solid #ddd;
  170. border-radius: 4px;
  171. font-family: inherit;
  172. font-size: 14px;
  173. line-height: 1.6;
  174. resize: vertical;
  175. }
  176. </style>