// 선택된 텍스트에 스타일 적용 function applyStyleToSelection(styleName, styleValue) { const selection = window.getSelection(); if (selection.rangeCount > 0 && !selection.isCollapsed) { const range = selection.getRangeAt(0); const fragment = range.cloneContents(); const wrapper = document.createDocumentFragment(); Array.from(fragment.childNodes).forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { node.style[styleName] = styleValue; wrapper.appendChild(node); } else if (node.nodeType === Node.TEXT_NODE) { const span = document.createElement('span'); span.style[styleName] = styleValue; span.textContent = node.nodeValue; wrapper.appendChild(span); } }); range.deleteContents(); range.insertNode(wrapper); // 한글 초성 분리 방지를 위해 normalize() 실행 document.getElementById('editor').normalize(); } } function toggleStyleToSelection(styleName, styleValue) { const selection = window.getSelection(); if (selection.rangeCount > 0 && !selection.isCollapsed) { const range = selection.getRangeAt(0); const fragment = range.extractContents(); const wrapper = document.createDocumentFragment(); Array.from(fragment.childNodes).forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { // 기존 요소에 스타일이 이미 있으면 제거, 없으면 추가 if (node.style[styleName] === styleValue) { node.style[styleName] = ''; // 스타일 제거 } else { node.style[styleName] = styleValue; // 스타일 추가 } wrapper.appendChild(node); } else if (node.nodeType === Node.TEXT_NODE) { // 텍스트 노드라면 새 태그로 감싸고 스타일 적용 const span = document.createElement("span"); span.style[styleName] = styleValue; span.textContent = node.textContent; wrapper.appendChild(span); } }); range.deleteContents(); range.insertNode(wrapper); } } function normalizeEditorContent() { const editor = document.getElementById('editor'); // ✅ 중첩된 병합 방지 editor.querySelectorAll('span').forEach((span) => { const parent = span.parentElement; // 부모가 이고 스타일이 동일하면 병합 if (parent.tagName === 'SPAN') { const sameStyle = [...span.style].every(prop => parent.style[prop] === span.style[prop]); if (sameStyle) { while (span.firstChild) { parent.insertBefore(span.firstChild, span); } parent.removeChild(span); } } // 빈 제거 if (!span.textContent.trim()) { span.remove(); } }); // ⚠️
요소 정리할 때도 기존 스타일 유지 editor.querySelectorAll('div').forEach((div) => { if (div.childNodes.length === 1 && div.firstChild.tagName === 'SPAN') { div.replaceWith(...div.childNodes); } }); // ⚠️ 한글 조합이 분리되지 않도록 normalize() 적용하되 스타일 병합 방지 setTimeout(() => { editor.normalize(); }, 100); } function toggleFontSizeToSelection(fontSize) { const selection = window.getSelection(); if (selection.rangeCount > 0 && !selection.isCollapsed) { const range = selection.getRangeAt(0); const fragment = range.extractContents(); // 선택된 내용을 추출 const wrapper = document.createDocumentFragment(); Array.from(fragment.childNodes).forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { // 기존 요소라면 폰트 크기 스타일 추가 node.style.fontSize = fontSize; wrapper.appendChild(node); } else if (node.nodeType === Node.TEXT_NODE) { // 텍스트 노드라면 새 태그로 감싸고 폰트 크기 적용 const span = document.createElement("span"); span.style.fontSize = fontSize; span.textContent = node.textContent; wrapper.appendChild(span); } }); // 선택된 영역에 새 내용 삽입 range.deleteContents(); range.insertNode(wrapper); } } function applyColorWithoutAffectingStyles(color) { const selection = window.getSelection(); if (selection.rangeCount > 0 && !selection.isCollapsed) { const range = selection.getRangeAt(0); // 선택된 범위 가져오기 const ancestor = range.commonAncestorContainer; // 선택된 범위 내의 텍스트 노드를 탐색 const walker = document.createTreeWalker( ancestor, NodeFilter.SHOW_TEXT, { acceptNode: (node) => { return range.intersectsNode(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; }, } ); let currentNode; while ((currentNode = walker.nextNode())) { wrapTextNodeInFont(currentNode, color); } } } // 태그 대신 태그를 사용하도록 변경 function wrapTextNodeInFont(textNode, color) { const parent = textNode.parentNode; // 이미 동일한 컬러가 적용된 span이 있으면 중복 방지 if (parent.tagName === 'SPAN' && parent.style.color === color) { return; } const span = document.createElement('span'); span.style.color = color; span.textContent = textNode.nodeValue; parent.replaceChild(span, textNode); } // 텍스트 노드를 태그로 감싸고 컬러 적용 function wrapTextNodeInFontTag(textNode, color) { wrapTextNodeInFont(textNode, color); } // 컬러를 적용하는 함수 (기존 스타일 처리와 동일한 방식) function applyStyleWithColor(styleProperty, value) { const selection = window.getSelection(); if (selection.rangeCount > 0 && !selection.isCollapsed) { const range = selection.getRangeAt(0); const commonAncestor = range.commonAncestorContainer; // 선택된 텍스트에만 스타일 적용 const walker = document.createTreeWalker( commonAncestor, NodeFilter.SHOW_TEXT, { acceptNode: (node) => { return range.intersectsNode(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; }, } ); let currentNode; while ((currentNode = walker.nextNode())) { wrapTextInStyle(currentNode, styleProperty, value); } } } function wrapTextInStyle(textNode, styleProperty, value) { const parent = textNode.parentNode; // 이미 동일한 스타일이 적용된 경우 처리하지 않음 if (parent.tagName === 'SPAN' && parent.style[styleProperty] === value) { return; } const span = document.createElement('span'); span.style[styleProperty] = value; span.textContent = textNode.nodeValue; parent.replaceChild(span, textNode); } function applyColorWithoutDiv(color) { const selection = window.getSelection(); if (selection.rangeCount > 0 && !selection.isCollapsed) { const range = selection.getRangeAt(0); // 선택된 범위 가져오기 const commonAncestor = range.commonAncestorContainer; // 모든 텍스트 노드를 포함한 범위 처리 const walker = document.createTreeWalker( commonAncestor, NodeFilter.SHOW_TEXT, { acceptNode: (node) => { // 선택된 범위에 포함된 텍스트 노드만 처리 return range.intersectsNode(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; }, } ); const nodesToWrap = []; let currentNode; while ((currentNode = walker.nextNode())) { nodesToWrap.push(currentNode); } nodesToWrap.forEach((node) => { wrapTextNodeInSpanWithoutDiv(node, color); }); // 선택 영역 삭제 및 새로운 노드 삽입 const newRange = document.createRange(); newRange.setStart(nodesToWrap[0], 0); newRange.setEnd(nodesToWrap[nodesToWrap.length - 1], nodesToWrap[nodesToWrap.length - 1].length); selection.removeAllRanges(); selection.addRange(newRange); } } // 텍스트 노드를 으로 감싸고 div 생성을 방지 function wrapTextNodeInSpanWithoutDiv(textNode, color) { const parent = textNode.parentNode; // 이미 동일한 이 적용되어 있는 경우 처리하지 않음 if (parent.tagName === 'SPAN' && parent.style.color === color) { return; } const span = document.createElement('span'); span.style.color = color; // 텍스트 노드를 으로 감싸기 span.textContent = textNode.nodeValue; parent.replaceChild(span, textNode); } function applyColorToExactSelection(color) { const selection = window.getSelection(); if (selection.rangeCount > 0 && !selection.isCollapsed) { const range = selection.getRangeAt(0); // 선택된 범위 가져오기 // 선택한 범위를 분리 const startContainer = range.startContainer; const endContainer = range.endContainer; if (startContainer === endContainer && startContainer.nodeType === Node.TEXT_NODE) { // 단일 텍스트 노드 선택인 경우 wrapPartialTextInSpan(range, color); } else { // 여러 노드가 선택된 경우 splitAndWrapRange(range, color); } } } function splitAndWrapRange(range, color) { const commonAncestor = range.commonAncestorContainer; // 선택된 범위 내의 노드를 추출하여 처리 const fragment = range.cloneContents(); const walker = document.createTreeWalker( fragment, NodeFilter.SHOW_TEXT, null, false ); let currentNode; const nodesToWrap = []; while ((currentNode = walker.nextNode())) { nodesToWrap.push(currentNode); } // 선택된 텍스트를 각각 으로 감싸기 nodesToWrap.forEach((node) => { const span = document.createElement('span'); span.style.color = color; span.textContent = node.textContent; const parent = node.parentNode; if (parent) { parent.replaceChild(span, node); } }); // 선택 영역 삭제 및 새로운 노드 삽입 range.deleteContents(); range.insertNode(fragment); } // 단일 텍스트 노드의 선택된 부분을 으로 감싸기 function wrapPartialTextInSpan(range, color) { const text = range.toString(); if (!text.trim()) return; const span = document.createElement('span'); span.style.color = color; span.textContent = text; range.deleteContents(); // 선택된 텍스트 삭제 range.insertNode(span); // 삽입 } // 텍스트 노드를 으로 감싸고 컬러 스타일 적용 function wrapTextNodeInSpan(textNode, color) { const parent = textNode.parentNode; // 이미 으로 감싸져 있고 동일한 컬러인 경우 처리하지 않음 if (parent.tagName === 'SPAN' && parent.style.color === color) { return; } const span = document.createElement('span'); span.style.color = color; span.textContent = textNode.textContent; parent.replaceChild(span, textNode); } function applyColorToSelectionSafely(color) { const selection = window.getSelection(); if (selection.rangeCount > 0 && !selection.isCollapsed) { const range = selection.getRangeAt(0); // 선택된 범위 가져오기 const ancestor = range.commonAncestorContainer; // 순회하며 모든 텍스트 노드에 컬러 적용 const walker = document.createTreeWalker( ancestor, NodeFilter.SHOW_TEXT, { acceptNode: (node) => { return range.intersectsNode(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; }, } ); let currentNode; const nodesToWrap = []; while ((currentNode = walker.nextNode())) { nodesToWrap.push(currentNode); } nodesToWrap.forEach((node) => { wrapTextNodeInSpan(node, color); }); } } function applyColorToMultipleBlocks(color) { const selection = window.getSelection(); if (selection.rangeCount > 0 && !selection.isCollapsed) { const range = selection.getRangeAt(0); // 선택된 범위 가져오기 const startContainer = range.startContainer; const endContainer = range.endContainer; if (startContainer !== endContainer) { // 선택 영역이 여러 블록에 걸쳐 있는 경우 처리 handleMultiBlockSelection(range, color); } else { // 단일 블록 내에서 처리 applyColorToSelection(color); } } } function handleMultiBlockSelection(range, color) { const startContainer = range.startContainer; const endContainer = range.endContainer; const ancestor = range.commonAncestorContainer; // 순회하며 모든 텍스트 노드에 컬러 적용 const walker = document.createTreeWalker( ancestor, NodeFilter.SHOW_TEXT, { acceptNode: (node) => { const nodeRange = document.createRange(); nodeRange.selectNodeContents(node); // 텍스트 노드가 선택 범위에 포함된 경우만 처리 return range.intersectsNode(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; }, } ); let currentNode; while ((currentNode = walker.nextNode())) { wrapTextInSpan(currentNode, color); } // 선택 영역 정리 range.deleteContents(); } // 텍스트 노드를 으로 감싸고 컬러 스타일 적용 function wrapTextInSpan(textNode, color) { const span = document.createElement('span'); span.style.color = color; span.textContent = textNode.textContent; const parent = textNode.parentNode; parent.replaceChild(span, textNode); } function applyColorWithSpan(color) { const selection = window.getSelection(); if (selection.rangeCount > 0 && !selection.isCollapsed) { const range = selection.getRangeAt(0); // 선택된 범위 가져오기 const fragment = range.extractContents(); // 선택된 내용을 추출 const wrapper = document.createDocumentFragment(); Array.from(fragment.childNodes).forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { // 기존 요소를 으로 감싸기 const span = document.createElement('span'); span.style.color = color; span.appendChild(node.cloneNode(true)); wrapper.appendChild(span); } else if (node.nodeType === Node.TEXT_NODE) { // 텍스트 노드를 으로 감싸기 const span = document.createElement('span'); span.style.color = color; span.textContent = node.textContent; wrapper.appendChild(span); } }); range.deleteContents(); // 기존 내용을 삭제 range.insertNode(wrapper); // 새로 생성한 내용 삽입 } } // 선택된 텍스트에 컬러를 적용하는 함수 function applyColorToSelection(color) { const selection = window.getSelection(); if (selection.rangeCount > 0 && !selection.isCollapsed) { const range = selection.getRangeAt(0); // 선택된 범위 가져오기 const commonAncestor = range.commonAncestorContainer; // 모든 텍스트 노드를 포함한 범위 처리 const walker = document.createTreeWalker( commonAncestor, NodeFilter.SHOW_TEXT, { acceptNode: (node) => { // 선택된 범위에 포함된 텍스트 노드만 처리 return range.intersectsNode(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; }, } ); let currentNode; const nodesToWrap = []; while ((currentNode = walker.nextNode())) { nodesToWrap.push(currentNode); } // 모든 텍스트 노드에 태그 추가 nodesToWrap.forEach((node) => { wrapNodeInSpan(node, color); }); // 선택 영역 복원 restoreSelection(); } } function wrapNodeInSpan(node, color) { const parent = node.parentNode; // 이미 동일한 이 적용되어 있는 경우 처리하지 않음 if (parent.tagName === 'SPAN' && parent.style.color === color) { return; } const span = document.createElement('span'); span.style.color = color; if (node.nodeType === Node.TEXT_NODE) { span.textContent = node.textContent; // 텍스트 노드 복사 } else { span.innerHTML = node.outerHTML; // 요소 노드 복사 } parent.replaceChild(span, node); } function normalizeSpans() { $('#editor span').each(function () { const $this = $(this); const parent = $this.parent(); // 부모가 이고, 스타일이 동일하면 병합 if (parent.is('span') && parent.css('color') === $this.css('color')) { $this.contents().unwrap(); // 현재 을 제거하고 부모로 병합 } }); } function applyFontTagWithColor(color) { document.execCommand('styleWithCSS', false, false); // CSS 스타일 대신 태그 사용 document.execCommand('foreColor', false, color); // 컬러 적용 } function applyColorWithExecCommand(color) { // 컬러 변경 명령 실행 document.execCommand('foreColor', false, color); // #editor 영역 내의 모든 태그를 찾아 으로 변경 $('#editor font').each(function () { const $font = $(this); // 태그의 color 속성을 읽음 let fontColor = $font.attr('color') || $font.css('color'); // 새 태그 생성 후 스타일과 내용을 복사 const $span = $('').css('color', fontColor).html($font.html()); // 태그를 새 태그로 교체 $font.replaceWith($span); }); } // 컬러 선택 후 텍스트에 색상 적용 $('#text-color-picker').on('input', function () { const selectedColor = $(this).val(); // 선택된 색상 값 $('#color-btn .color-btn-svg path.path2_color1').attr('fill', selectedColor); // SVG의 fill 속성 업데이트 $('#color-btn .color-btn-svg path.path2_color2').attr('fill', selectedColor); // SVG의 fill 속성 업데이트 restoreSelection(); // 선택 영역 복원 applyTextColor(selectedColor); // 컬러 변경 saveSelection(); // 선택 영역 다시 저장 }); $('#background-color-picker').on('input', function () { const selectedBgColor = $(this).val(); restoreSelection(); //applyBackgroundColor(selectedBgColor); applyBackgroundColorNoBreaking(selectedBgColor); // 선택된 컬러를 SVG 등에 업데이트 $('#background-color-btn .background-color-btn-svg path').attr('fill', selectedBgColor); saveSelection(); }); // 컬러 버튼 클릭 시 선택 영역 저장 $('#color-btn').on('mousedown', function (e) { saveSelection(); // 선택 영역 저장 e.preventDefault(); // 기본 동작 방지 }); // 텍스트 선택 후 컬러 버튼 클릭 시 Coloris 활성화 $('#color-btn').click(function (e) { e.preventDefault(); // 기본 동작 방지 saveSelection(); // 선택 영역 저장 $('#text-color-picker').click(); // 컬러 선택기 활성화 }); // 텍스트 선택 후 백그라운드 버튼 클릭 시 Coloris 활성화 $('#background-color-btn').click(function (e) { e.preventDefault(); // 기본 동작 방지 saveSelection(); // 선택 영역 저장 $('#background-color-picker').click(); // 컬러 선택기 활성화 }); function applyTextColor(color) { if (currentMode === 'regular') { if (savedSelection) { const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(savedSelection); } document.execCommand('foreColor', false, color); updateMiniToolbar(); // #mini-toolbar는 숨기지 않음 } else if (currentMode === 'canvas') { applyCanvasTextColor(color); } } function applyBackgroundColorNoBreaking(color) { const selection = window.getSelection(); if (!selection.rangeCount || selection.isCollapsed) return; // 현재 선택 범위 가져오기 const range = selection.getRangeAt(0); const startContainer = range.startContainer; const endContainer = range.endContainer; // ✅ 두 줄 이상 선택한 경우 기존 로직 사용 if (startContainer !== endContainer) { const textNodes = getTextNodesInRange(range); textNodes.forEach(node => highlightTextNodePartially(node, range, color)); return; } // ✅ 한 줄 부분 선택인 경우 일반적인 스타일 적용 방식 사용 applyStyleToSelection("backgroundColor", color); } function highlightTextNodePartially(textNode, selectionRange, color) { if (!textNode || textNode.nodeType !== Node.TEXT_NODE) return; const nodeRange = document.createRange(); nodeRange.selectNodeContents(textNode); const intersectionRange = nodeRange.cloneRange(); if (selectionRange.compareBoundaryPoints(Range.START_TO_START, nodeRange) > 0) { intersectionRange.setStart(selectionRange.startContainer, selectionRange.startOffset); } if (selectionRange.compareBoundaryPoints(Range.END_TO_END, nodeRange) < 0) { intersectionRange.setEnd(selectionRange.endContainer, selectionRange.endOffset); } const selectedText = intersectionRange.toString(); if (!selectedText.trim()) return; const startOffsetInNode = intersectionRange.startOffset - nodeRange.startOffset; const endOffsetInNode = intersectionRange.endOffset - nodeRange.startOffset; const originalText = textNode.nodeValue; const beforeText = originalText.slice(0, startOffsetInNode); const middleText = originalText.slice(startOffsetInNode, endOffsetInNode); const afterText = originalText.slice(endOffsetInNode); let highlightSpan; if (textNode.parentNode.tagName === 'SPAN') { highlightSpan = textNode.parentNode.cloneNode(); highlightSpan.style.backgroundColor = color; } else { highlightSpan = document.createElement('span'); highlightSpan.style.backgroundColor = color; } highlightSpan.textContent = middleText; const parent = textNode.parentNode; if (!parent) return; const fragment = document.createDocumentFragment(); if (beforeText) { fragment.appendChild(document.createTextNode(beforeText)); } fragment.appendChild(highlightSpan); if (afterText) { fragment.appendChild(document.createTextNode(afterText)); } parent.replaceChild(fragment, textNode); } function getTextNodesInRange(range) { const container = range.commonAncestorContainer; const textNodes = []; const walker = document.createTreeWalker( container, NodeFilter.SHOW_TEXT, { acceptNode: (node) => { return range.intersectsNode(node) ? NodeFilter.FILTER_ACCEPT : NodeFilter.FILTER_REJECT; } } ); while (walker.nextNode()) { textNodes.push(walker.currentNode); } return textNodes; } function mergeBackgroundSpans(editor) { $(editor).find('span').each(function () { const $span = $(this); // 부모가 span이고 스타일이 같으면 병합 const parent = $span.parent(); if (parent.is('span') && parent.css('background-color') === $span.css('background-color')) { $span.contents().unwrap(); // span 병합 } // 빈 span 제거 if ($span.text().trim() === '' && $span.children().length === 0) { $span.remove(); } }); } function applyBackgroundColor(color) { if (savedSelection) { const sel = window.getSelection(); sel.removeAllRanges(); sel.addRange(savedSelection); } const selection = window.getSelection(); if (selection.rangeCount > 0 && !selection.isCollapsed) { const range = selection.getRangeAt(0); const commonAncestor = range.commonAncestorContainer; // ✅ 드래그로 여러 개의 ``, ``가 선택된 경우 처리 const selectedCells = getSelectedCells(range); if (selectedCells.length > 0) { selectedCells.forEach(cell => { applyBackgroundToCell(cell, color); }); } else { // ✅ 단일 선택일 경우 기존 로직 사용 let targetElement = commonAncestor.nodeType === Node.ELEMENT_NODE ? commonAncestor : commonAncestor.parentElement; if (targetElement.tagName === 'TD' || targetElement.tagName === 'TH') { applyBackgroundToCell(targetElement, color); } else { wrapTextWithSpan(range, color); } } } updateMiniToolbar(); } // ✅ 선택된 ``, `` 목록 가져오기 function getSelectedCells(range) { const selectedCells = []; const ancestor = range.commonAncestorContainer; const table = ancestor.closest ? ancestor.closest('table') : null; if (table) { const allCells = table.querySelectorAll('td, th'); allCells.forEach(cell => { if (range.intersectsNode(cell)) { selectedCells.push(cell); } }); } return selectedCells; } // ✅ 개별 `` 또는 `` 내부의 텍스트에만 배경색 적용 function applyBackgroundToCell(cell, color) { // 기존 `span`이 있는지 확인하고 스타일 변경 let existingSpan = cell.querySelector('span'); if (existingSpan) { existingSpan.style.backgroundColor = color; } else { // 기존 `span`이 없으면 내부 텍스트를 `span`으로 감싸기 cell.childNodes.forEach((node) => { if (node.nodeType === Node.TEXT_NODE && node.nodeValue.trim() !== '') { const span = document.createElement("span"); span.style.backgroundColor = color; span.textContent = node.nodeValue; node.replaceWith(span); } }); } } // 선택한 텍스트를 개별적으로 감싸서 스타일 적용하는 함수 function wrapTextWithSpan(range, color) { const span = document.createElement("span"); span.style.backgroundColor = color; // 선택한 내용을 가져옴 const selectedContents = range.extractContents(); // 선택한 텍스트를 감싸서 배경색 적용 span.appendChild(selectedContents); // 기존 내용을 삭제하고 새로 추가 range.deleteContents(); range.insertNode(span); } function applyFontSize(fontSize) { const selection = window.getSelection(); if (!selection.rangeCount || selection.isCollapsed) return; const range = selection.getRangeAt(0); // 시작/끝 컨테이너가 텍스트 노드라면 부모 요소로 변경 const startContainer = range.startContainer.nodeType === Node.TEXT_NODE ? range.startContainer.parentElement : range.startContainer; const endContainer = range.endContainer.nodeType === Node.TEXT_NODE ? range.endContainer.parentElement : range.endContainer; // 시작과 끝이 각각 td 또는 th 내부에 있는지 확인 const startCell = startContainer.closest && startContainer.closest("td, th"); const endCell = endContainer.closest && endContainer.closest("td, th"); // 두 셀 모두 존재하고, 같은 테이블에 속해 있다면 if ( startCell && endCell && startCell.closest("table") === endCell.closest("table") ) { // 테이블 내 모든 셀 중 선택 영역과 교차하는 셀에 대해 스타일 적용 const table = startCell.closest("table"); const cells = table.querySelectorAll("td, th"); cells.forEach((cell) => { // Range API의 intersectsNode 메서드를 사용하여 // 해당 셀과 선택 영역이 겹치는지 검사 if (range.intersectsNode(cell)) { cell.style.fontSize = fontSize; } }); return; } // 테이블 셀 내부가 아닌 경우 – 기존 로직대로 선택 영역 내부의 텍스트/요소에 span 등을 삽입하여 폰트 크기 적용 const extractedContents = range.extractContents(); const wrapper = document.createDocumentFragment(); Array.from(extractedContents.childNodes).forEach((node) => { if (node.nodeType === Node.TEXT_NODE) { // 텍스트 노드인 경우 새로운 span으로 감싸서 적용 const span = document.createElement("span"); span.style.fontSize = fontSize; span.textContent = node.textContent; wrapper.appendChild(span); } else if (node.nodeType === Node.ELEMENT_NODE) { // 요소 노드인 경우 style만 변경 node.style.fontSize = fontSize; wrapper.appendChild(node); } }); range.insertNode(wrapper); // 편집 영역(normalize)에서 불필요한 텍스트 노드 분리(한글 초성 분리 방지) document.getElementById("editor").normalize(); } // ✅ `td`, `th` 내부에서 폰트 크기 변경 (새로운 ``, `` 생성 방지) function applyFontSizeInsideTable(fontSize, $currentCell) { const selection = window.getSelection(); if (selection.rangeCount > 0 && !selection.isCollapsed) { const range = selection.getRangeAt(0); const selectedNodes = []; const walker = document.createTreeWalker(range.commonAncestorContainer, NodeFilter.SHOW_TEXT, null); let currentNode; while ((currentNode = walker.nextNode())) { if (selection.containsNode(currentNode, true)) { selectedNodes.push(currentNode); } } function applyStyleToSelection(node) { if (node.nodeType === Node.TEXT_NODE && node.textContent.trim()) { let parent = node.parentNode; // ✅ `td`, `th` 자체가 아니라 내부 텍스트에만 스타일 적용 if ($currentCell[0] === parent) { const span = document.createElement("span"); span.style.fontSize = fontSize; span.textContent = node.textContent; parent.replaceChild(span, node); } else if (parent.tagName === "SPAN") { parent.style.fontSize = fontSize; } else { const span = document.createElement("span"); span.style.fontSize = fontSize; span.appendChild(node.cloneNode(true)); parent.replaceChild(span, node); } } } selectedNodes.forEach(applyStyleToSelection); } } // ✅ `td`, `th` 외부 또는 `td`, `th` 내부의 텍스트에서 폰트 크기 변경 (새로운 `` 추가) function applyFontSizeOutsideTable(fontSize) { const selection = window.getSelection(); if (selection.rangeCount > 0 && !selection.isCollapsed) { const range = selection.getRangeAt(0); const fragment = range.cloneContents(); // ✅ 기존 구조 유지 const wrapper = document.createDocumentFragment(); Array.from(fragment.childNodes).forEach((node) => { if (node.nodeType === Node.ELEMENT_NODE) { node.style.fontSize = fontSize; wrapper.appendChild(node); } else if (node.nodeType === Node.TEXT_NODE) { const span = document.createElement("span"); span.style.fontSize = fontSize; span.textContent = node.textContent; wrapper.appendChild(span); } }); range.deleteContents(); range.insertNode(wrapper); } else { if (!$('#editor').text().trim()) { $('#editor').css('font-size', fontSize); } } } function alignText(alignment) { const selection = window.getSelection(); const range = selection.rangeCount > 0 ? selection.getRangeAt(0) : null; if (currentMode === 'regular') { const $currentCell = $(selection.anchorNode).closest('td, th'); if ($currentCell.length) { // 커서가 표 안에 있는 경우 해당 셀에만 정렬 적용 $currentCell.css('text-align', alignment); } else { // 커서가 표 밖에 있으면 전체 문서에 정렬 적용 (표는 제외) $('#editor').children(':not(table)').css('text-align', alignment); } } else if (currentMode === 'canvas') { const activeObject = fabricCanvas.getActiveObject(); if (!activeObject) return; if (activeObject.type === 'i-text') { activeObject.set('textAlign', alignment); fabricCanvas.renderAll(); } } updateMiniToolbar(); }