/** * 글벗 Light - 편집 바 모듈 * editor.js */ // ===== 전역 변수 ===== let isEditing = false; let activeBlock = null; let historyStack = []; let redoStack = []; const MAX_HISTORY = 50; let isApplyingFormat = false; // ===== 편집 바 HTML 생성 ===== // ===== 편집 바 HTML 생성 ===== function createFormatBar() { const formatBarHTML = `
A
A
`; return formatBarHTML; } // ===== 로컬 폰트 불러오기 ===== async function loadLocalFonts() { // API 지원 여부 확인 if (!('queryLocalFonts' in window)) { toast('⚠️ 이 브라우저는 폰트 불러오기를 지원하지 않습니다 (Chrome/Edge 필요)'); return; } try { toast('🔄 폰트 불러오는 중...'); // 사용자 권한 요청 & 폰트 목록 가져오기 const fonts = await window.queryLocalFonts(); const fontSelect = document.getElementById('fontFamily'); // 기존 옵션들의 값 수집 (중복 방지) const existingFonts = new Set(); fontSelect.querySelectorAll('option').forEach(opt => { existingFonts.add(opt.value); }); // 중복 제거 (family 기준) const families = [...new Set(fonts.map(f => f.family))]; // 구분선 추가 const separator = document.createElement('option'); separator.disabled = true; separator.textContent = '──── 내 컴퓨터 ────'; fontSelect.appendChild(separator); // 새 폰트 추가 let addedCount = 0; families.sort().forEach(family => { if (!existingFonts.has(family)) { const option = document.createElement('option'); option.value = family; option.textContent = family; fontSelect.appendChild(option); addedCount++; } }); toast(`✅ ${addedCount}개 폰트 추가됨 (총 ${families.length}개)`); } catch (e) { if (e.name === 'NotAllowedError') { toast('⚠️ 폰트 접근 권한이 거부되었습니다'); } else { console.error('폰트 로드 오류:', e); toast('❌ 폰트 불러오기 실패: ' + e.message); } } } // ===== 삽입 핸들러 ===== function handleInsert(type) { if (type === 'table') openTableModal(); else if (type === 'image') insertImage(); else if (type === 'hr') insertHR(); } // ===== 표 삽입 모달 HTML 생성 ===== function createTableModal() { const modalHTML = `
▦ 표 삽입
`; return modalHTML; } // ===== 토스트 컨테이너 생성 ===== function createToastContainer() { if (!document.getElementById('toastContainer')) { const container = document.createElement('div'); container.id = 'toastContainer'; container.className = 'toast-container'; document.body.appendChild(container); } } // ===== 토스트 메시지 ===== function toast(message) { createToastContainer(); const container = document.getElementById('toastContainer'); const toastEl = document.createElement('div'); toastEl.className = 'toast'; toastEl.textContent = message; container.appendChild(toastEl); setTimeout(() => toastEl.remove(), 3000); } // ===== iframe 참조 가져오기 ===== function getPreviewIframe() { return document.getElementById('previewFrame'); } function getIframeDoc() { const iframe = getPreviewIframe(); if (!iframe) return null; return iframe.contentDocument || iframe.contentWindow.document; } // ===== 기본 포맷 명령 ===== function formatText(command, value = null) { const doc = getIframeDoc(); if (!doc || !isEditing) return; saveState(); doc.execCommand(command, false, value); } // ===== 자간 조절 ===== function adjustLetterSpacing(delta) { const doc = getIframeDoc(); if (!doc || !isEditing) return; isApplyingFormat = true; const selection = doc.getSelection(); if (!selection || selection.rangeCount === 0 || selection.isCollapsed) { toast('텍스트를 선택해주세요'); return; } saveState(); const range = selection.getRangeAt(0); let targetNode = range.commonAncestorContainer; if (targetNode.nodeType === Node.TEXT_NODE) targetNode = targetNode.parentNode; const computed = doc.defaultView.getComputedStyle(targetNode); const currentSpacing = parseFloat(computed.letterSpacing) || 0; const newSpacing = currentSpacing + delta; if (targetNode.tagName === 'SPAN' && range.toString() === targetNode.textContent) { targetNode.style.letterSpacing = newSpacing + 'px'; } else { try { const span = doc.createElement('span'); span.style.letterSpacing = newSpacing + 'px'; range.surroundContents(span); } catch (e) { const fragment = range.extractContents(); const span = doc.createElement('span'); span.style.letterSpacing = newSpacing + 'px'; span.appendChild(fragment); range.insertNode(span); } } toast('자간: ' + newSpacing.toFixed(1) + 'px'); setTimeout(() => { isApplyingFormat = false; }, 100); } // ===== 색상 적용 ===== function applyTextColor(color) { formatText('foreColor', color); } function applyBgColor(color) { formatText('hiliteColor', color); } // ===== 목록 ===== function toggleBulletList() { formatText('insertUnorderedList'); } function toggleNumberList() { formatText('insertOrderedList'); } // ===== 들여쓰기 ===== function adjustIndent(direction) { const doc = getIframeDoc(); if (!doc || !isEditing) return; if (activeBlock) { saveState(); const current = parseInt(activeBlock.style.marginLeft) || 0; activeBlock.style.marginLeft = Math.max(0, current + (direction * 20)) + 'px'; toast(direction > 0 ? '→ 들여쓰기' : '← 내어쓰기'); } else { formatText(direction > 0 ? 'indent' : 'outdent'); } } // ===== 제목 스타일 ===== function applyHeading(tag) { const doc = getIframeDoc(); if (!doc || !isEditing || !activeBlock) return; saveState(); const content = activeBlock.innerHTML; let newEl; if (tag === '') { newEl = doc.createElement('p'); newEl.innerHTML = content; newEl.style.fontSize = '12pt'; newEl.style.lineHeight = '1.6'; } else { newEl = doc.createElement(tag); newEl.innerHTML = content; if (tag === 'h1') { newEl.style.cssText = 'font-size:20pt; font-weight:900; color:#1a365d; border-bottom:2px solid #1a365d; margin-bottom:20px;'; } else if (tag === 'h2') { newEl.style.cssText = 'font-size:18pt; border-left:5px solid #2c5282; padding-left:10px; color:#1a365d;'; } else if (tag === 'h3') { newEl.style.cssText = 'font-size:14pt; color:#2c5282;'; } } newEl.setAttribute('contenteditable', 'true'); activeBlock.replaceWith(newEl); setActiveBlock(newEl); } // ===== 폰트 ===== function applyFontFamily(fontName) { if (!isEditing) return; formatText('fontName', fontName); } function applyFontSizeInput(size) { const doc = getIframeDoc(); if (!doc || !isEditing) return; const selection = doc.getSelection(); if (!selection || selection.rangeCount === 0 || selection.isCollapsed) return; saveState(); const range = selection.getRangeAt(0); try { const span = doc.createElement('span'); span.style.fontSize = size + 'pt'; range.surroundContents(span); } catch (e) { const fragment = range.extractContents(); const span = doc.createElement('span'); span.style.fontSize = size + 'pt'; span.appendChild(fragment); range.insertNode(span); } toast('글씨 크기: ' + size + 'pt'); } // ===== 표 삽입 ===== function openTableModal() { document.getElementById('tableModal').classList.add('active'); } function closeTableModal() { document.getElementById('tableModal').classList.remove('active'); } function insertTable() { const doc = getIframeDoc(); if (!doc || !isEditing) return; const rows = parseInt(document.getElementById('tableRows').value) || 3; const cols = parseInt(document.getElementById('tableCols').value) || 3; const hasHeader = document.getElementById('tableHeader').checked; saveState(); let tableHTML = ''; for (let i = 0; i < rows; i++) { tableHTML += ''; for (let j = 0; j < cols; j++) { if (i === 0 && hasHeader) { tableHTML += ''; } else { tableHTML += ''; } } tableHTML += ''; } tableHTML += '
헤더내용
'; insertAtCursor(tableHTML); closeTableModal(); toast('▦ 표가 삽입되었습니다'); } // ===== 이미지 삽입 ===== function insertImage() { const doc = getIframeDoc(); if (!doc || !isEditing) return; const input = document.createElement('input'); input.type = 'file'; input.accept = 'image/*'; input.onchange = e => { const file = e.target.files[0]; if (!file) return; const reader = new FileReader(); reader.onload = ev => { saveState(); const html = `
그림 설명
`; insertAtCursor(html); toast('🖼️ 이미지가 삽입되었습니다'); }; reader.readAsDataURL(file); }; input.click(); } // ===== 이미지 리사이즈 ===== function selectImageForResize(img) { if (!isEditing) return; // 기존 선택 해제 const doc = getIframeDoc(); doc.querySelectorAll('img.selected-image').forEach(i => { i.classList.remove('selected-image'); i.style.outline = ''; }); // 새 선택 img.classList.add('selected-image'); img.style.outline = '3px solid #00c853'; // 크기 조절 핸들러 img.onmousedown = function(e) { if (!isEditing) return; e.preventDefault(); const startX = e.clientX; const startWidth = img.offsetWidth; function onMouseMove(e) { const diff = e.clientX - startX; const newWidth = Math.max(50, startWidth + diff); img.style.width = newWidth + 'px'; img.style.height = 'auto'; } function onMouseUp() { document.removeEventListener('mousemove', onMouseMove); document.removeEventListener('mouseup', onMouseUp); saveState(); toast('이미지 크기 조절됨'); } document.addEventListener('mousemove', onMouseMove); document.addEventListener('mouseup', onMouseUp); }; } // ===== 구분선 삽입 ===== function insertHR() { const doc = getIframeDoc(); if (!doc || !isEditing) return; saveState(); insertAtCursor('
'); toast('― 구분선 삽입'); } // ===== 커서 위치에 HTML 삽입 ===== function insertAtCursor(html) { const doc = getIframeDoc(); if (!doc) return; const selection = doc.getSelection(); if (selection && selection.rangeCount > 0) { const range = selection.getRangeAt(0); range.deleteContents(); const temp = doc.createElement('div'); temp.innerHTML = html; const frag = doc.createDocumentFragment(); while (temp.firstChild) frag.appendChild(temp.firstChild); range.insertNode(frag); } else if (activeBlock) { activeBlock.insertAdjacentHTML('afterend', html); } } // ===== 블록 선택/관리 ===== function setActiveBlock(el) { clearActiveBlock(); activeBlock = el; if (activeBlock) activeBlock.classList.add('active-block'); } function clearActiveBlock() { if (activeBlock) activeBlock.classList.remove('active-block'); activeBlock = null; } // ===== Undo/Redo ===== function saveState() { const doc = getIframeDoc(); if (!doc) return; if (redoStack.length > 0) redoStack.length = 0; historyStack.push(doc.body.innerHTML); if (historyStack.length > MAX_HISTORY) historyStack.shift(); } function performUndo() { const doc = getIframeDoc(); if (!doc || historyStack.length <= 1) return; redoStack.push(doc.body.innerHTML); historyStack.pop(); doc.body.innerHTML = historyStack[historyStack.length - 1]; bindIframeEditEvents(); toast('↩️ 실행 취소'); } function performRedo() { const doc = getIframeDoc(); if (!doc || redoStack.length === 0) return; const nextState = redoStack.pop(); historyStack.push(nextState); doc.body.innerHTML = nextState; bindIframeEditEvents(); toast('↪️ 다시 실행'); } // ===== 키보드 단축키 ===== function handleEditorKeydown(e) { if (!isEditing) return; if (e.ctrlKey || e.metaKey) { switch (e.key.toLowerCase()) { case 'b': e.preventDefault(); formatText('bold'); break; case 'i': e.preventDefault(); formatText('italic'); break; case 'u': e.preventDefault(); formatText('underline'); break; case 'z': e.preventDefault(); e.shiftKey ? performRedo() : performUndo(); break; case 'y': e.preventDefault(); performRedo(); break; case '=': case '+': e.preventDefault(); adjustLetterSpacing(0.5); break; case '-': e.preventDefault(); adjustLetterSpacing(-0.5); break; } } if (e.key === 'Tab') { e.preventDefault(); adjustIndent(e.shiftKey ? -1 : 1); } } // ===== 리사이즈 핸들 추가 함수 ===== function addResizeHandle(doc, element, type) { // wrapper 생성 const wrapper = doc.createElement('div'); wrapper.className = 'resizable-container ' + (type === 'table' ? 'table-resize block-type' : 'figure-resize'); // 초기 크기 설정 const rect = element.getBoundingClientRect(); wrapper.style.width = element.style.width || (rect.width + 'px'); // 크기 표시 툴팁 const tooltip = doc.createElement('div'); tooltip.className = 'size-tooltip'; tooltip.textContent = Math.round(rect.width) + ' × ' + Math.round(rect.height); // 리사이즈 핸들 const handle = doc.createElement('div'); handle.className = 'resize-handle'; handle.title = '드래그하여 크기 조절'; // DOM 구조 변경 element.parentNode.insertBefore(wrapper, element); wrapper.appendChild(element); wrapper.appendChild(tooltip); wrapper.appendChild(handle); // 표는 width 100%로 시작 if (type === 'table') { element.style.width = '100%'; } // 리사이즈 이벤트 let isResizing = false; let startX, startY, startWidth, startHeight; handle.addEventListener('mousedown', function(e) { e.preventDefault(); e.stopPropagation(); isResizing = true; wrapper.classList.add('resizing'); startX = e.clientX; startY = e.clientY; startWidth = wrapper.offsetWidth; startHeight = wrapper.offsetHeight; doc.addEventListener('mousemove', onMouseMove); doc.addEventListener('mouseup', onMouseUp); }); function onMouseMove(e) { if (!isResizing) return; e.preventDefault(); const deltaX = e.clientX - startX; const deltaY = e.clientY - startY; const aspectRatio = startWidth / startHeight; let newWidth = Math.max(100, startWidth + deltaX); let newHeight; if (e.shiftKey) { newHeight = newWidth / aspectRatio; // 비율 유지 } else { newHeight = Math.max(50, startHeight + deltaY); } wrapper.style.width = newWidth + 'px'; // 이미지인 경우 width, height 둘 다 조절 if (type !== 'table') { const img = wrapper.querySelector('img'); if (img) { img.style.width = newWidth + 'px'; img.style.height = newHeight + 'px'; img.style.maxWidth = 'none'; img.style.maxHeight = 'none'; } } tooltip.textContent = Math.round(newWidth) + ' × ' + Math.round(newHeight); } function onMouseUp(e) { if (!isResizing) return; isResizing = false; wrapper.classList.remove('resizing'); doc.removeEventListener('mousemove', onMouseMove); doc.removeEventListener('mouseup', onMouseUp); saveState(); toast('📐 크기 조절: ' + Math.round(wrapper.offsetWidth) + 'px'); } } // ===== iframe 내부에 편집용 스타일 주입 ===== function injectEditStyles(doc) { if (doc.getElementById('editor-inject-style')) return; const style = doc.createElement('style'); style.id = 'editor-inject-style'; style.textContent = ` /* 리사이즈 컨테이너 */ .resizable-container { position: relative; display: inline-block; max-width: 100%; } .resizable-container.block-type { display: block; } /* 리사이즈 핸들 */ .resize-handle { position: absolute; right: -2px; bottom: -2px; width: 18px; height: 18px; background: #00C853; cursor: se-resize; opacity: 0; transition: opacity 0.2s; z-index: 100; border-radius: 3px 0 3px 0; display: flex; align-items: center; justify-content: center; } .resize-handle::after { content: '⤡'; color: white; font-size: 12px; font-weight: bold; } .resizable-container:hover .resize-handle { opacity: 0.8; } .resize-handle:hover { opacity: 1 !important; transform: scale(1.1); } .resizable-container.resizing { outline: 2px dashed #00C853 !important; } .resizable-container.resizing .resize-handle { opacity: 1; background: #FF9800; } /* 표 전용 - 파란색 핸들 */ .resizable-container.table-resize .resize-handle { background: #2196F3; } .resizable-container.table-resize.resizing .resize-handle { background: #FF5722; } /* 이미지 전용 */ .resizable-container.figure-resize img { display: block; } /* 크기 표시 툴팁 */ .size-tooltip { position: absolute; top: -25px; right: 0; background: rgba(0,0,0,0.8); color: white; padding: 2px 8px; border-radius: 3px; font-size: 11px; white-space: nowrap; opacity: 0; transition: opacity 0.2s; pointer-events: none; } .resizable-container:hover .size-tooltip, .resizable-container.resizing .size-tooltip { opacity: 1; } /* 열 리사이즈 핸들 */ .col-resize-handle { position: absolute; top: 0; width: 6px; height: 100%; background: transparent; cursor: col-resize; z-index: 50; } .col-resize-handle:hover { background: rgba(33, 150, 243, 0.3); } .col-resize-handle.dragging { background: rgba(33, 150, 243, 0.5); } /* 편집 중 하이라이트 */ [contenteditable]:focus { outline: 2px solid #00C853 !important; } [contenteditable]:hover { outline: 1px dashed rgba(0,200,83,0.5); } `; doc.head.appendChild(style); } // ===== iframe 편집 이벤트 바인딩 ===== // ===== iframe 편집 이벤트 바인딩 ===== function bindIframeEditEvents() { const doc = getIframeDoc(); if (!doc) return; // 편집용 스타일 주입 injectEditStyles(doc); // 키보드 이벤트 doc.removeEventListener('keydown', handleEditorKeydown); doc.addEventListener('keydown', handleEditorKeydown); // 블록 클릭 이벤트 doc.body.addEventListener('click', function(e) { if (!isEditing) return; let target = e.target; while (target && target !== doc.body) { if (['DIV', 'P', 'H1', 'H2', 'H3', 'LI', 'TD', 'TH', 'FIGCAPTION'].includes(target.tagName)) { setActiveBlock(target); return; } target = target.parentElement; } clearActiveBlock(); }); // ===== 표에 리사이즈 핸들 추가 ===== doc.querySelectorAll('.body-content table, .sheet table').forEach(table => { if (table.closest('.resizable-container')) return; addResizeHandle(doc, table, 'table'); addColumnResizeHandles(doc, table); // 열 리사이즈 추가 }); // ===== 이미지에 리사이즈 핸들 추가 ===== doc.querySelectorAll('figure img, .body-content img, .sheet img').forEach(img => { if (img.closest('.resizable-container')) return; addResizeHandle(doc, img, 'image'); }); } // ===== 표 열 리사이즈 핸들 추가 ===== function addColumnResizeHandles(doc, table) { // 테이블에 position relative 설정 table.style.position = 'relative'; // 첫 번째 행의 셀들을 기준으로 열 핸들 생성 const firstRow = table.querySelector('tr'); if (!firstRow) return; const cells = firstRow.querySelectorAll('th, td'); cells.forEach((cell, index) => { if (index === cells.length - 1) return; // 마지막 열은 제외 // 이미 핸들이 있으면 스킵 if (cell.querySelector('.col-resize-handle')) return; cell.style.position = 'relative'; const handle = doc.createElement('div'); handle.className = 'col-resize-handle'; handle.style.right = '-3px'; cell.appendChild(handle); let startX, startWidth, nextStartWidth; let nextCell = cells[index + 1]; handle.addEventListener('mousedown', function(e) { e.preventDefault(); e.stopPropagation(); handle.classList.add('dragging'); startX = e.clientX; startWidth = cell.offsetWidth; nextStartWidth = nextCell ? nextCell.offsetWidth : 0; doc.addEventListener('mousemove', onMouseMove); doc.addEventListener('mouseup', onMouseUp); }); function onMouseMove(e) { const delta = e.clientX - startX; const newWidth = Math.max(30, startWidth + delta); cell.style.width = newWidth + 'px'; // 다음 열도 조정 (테이블 전체 너비 유지) if (nextCell && nextStartWidth > 30) { const newNextWidth = Math.max(30, nextStartWidth - delta); nextCell.style.width = newNextWidth + 'px'; } } function onMouseUp() { handle.classList.remove('dragging'); doc.removeEventListener('mousemove', onMouseMove); doc.removeEventListener('mouseup', onMouseUp); saveState(); toast('📊 열 너비 조절됨'); } }); } // ===== 편집 모드 토글 ===== function toggleEditMode() { const doc = getIframeDoc(); if (!doc) return; isEditing = !isEditing; const formatBar = document.getElementById('formatBar'); const editBtn = document.getElementById('editModeBtn'); if (isEditing) { // 편집 모드 ON doc.designMode = 'on'; if (formatBar) formatBar.classList.add('active'); if (editBtn) { editBtn.textContent = '✏️ 편집 중'; editBtn.classList.add('active'); } // contenteditable 설정 doc.querySelectorAll('.sheet *').forEach(el => { if (['DIV', 'P', 'H1', 'H2', 'H3', 'SPAN', 'LI', 'TD', 'TH', 'FIGCAPTION'].includes(el.tagName)) { el.setAttribute('contenteditable', 'true'); } }); bindIframeEditEvents(); saveState(); toast('✏️ 편집 모드 시작'); } else { // 편집 모드 OFF doc.designMode = 'off'; if (formatBar) formatBar.classList.remove('active'); if (editBtn) { editBtn.textContent = '✏️ 편집하기'; editBtn.classList.remove('active'); } // contenteditable 제거 doc.querySelectorAll('[contenteditable]').forEach(el => { el.removeAttribute('contenteditable'); }); clearActiveBlock(); toast('✏️ 편집 모드 종료'); } } // ===== 편집기 초기화 ===== function initEditor() { // 편집 바가 없으면 생성 if (!document.getElementById('formatBar')) { const previewContainer = document.querySelector('.main'); if (previewContainer) { previewContainer.insertAdjacentHTML('afterbegin', createFormatBar()); } } // 표 모달이 없으면 생성 if (!document.getElementById('tableModal')) { document.body.insertAdjacentHTML('beforeend', createTableModal()); } // 토스트 컨테이너 생성 createToastContainer(); console.log('Editor initialized'); } // ===== 지능형 정렬 ===== function smartAlign() { const doc = getIframeDoc(); if (!doc) { toast('⚠️ 문서가 로드되지 않았습니다'); return; } // ===== 현재 스크롤 위치 저장 ===== const iframe = getPreviewIframe(); const scrollY = iframe?.contentWindow?.scrollY || 0; const sheets = Array.from(doc.querySelectorAll('.sheet')); if (sheets.length < 2) { toast('⚠️ 정렬할 본문 페이지가 없습니다'); return; } toast('지능형 정렬 실행 중...'); setTimeout(() => { try { // 1. 표지 유지 const coverSheet = sheets[0]; // 2. 보고서 제목 추출 let reportTitle = "보고서"; const existingTitle = sheets[1]?.querySelector('.rpt-title, .header-title'); if (existingTitle) reportTitle = existingTitle.innerText; // 3. 콘텐츠 수집 (표지 제외) const contentSheets = sheets.slice(1); let allNodes = []; contentSheets.forEach(sheet => { const body = sheet.querySelector('.body-content'); if (body) { Array.from(body.children).forEach(child => { if (child.classList.contains('add-after-btn') || child.classList.contains('delete-block-btn') || child.classList.contains('empty-placeholder')) return; if (['P', 'DIV', 'SPAN'].includes(child.tagName) && child.innerText.trim() === '' && !child.querySelector('img, table, figure')) return; allNodes.push(child); }); } sheet.remove(); }); // 4. 설정값 const MAX_HEIGHT = 970; const HEADING_RESERVE = 90; let currentHeaderTitle = "목차"; let pageNum = 1; // 5. 새 페이지 생성 함수 function createNewPage(headerText) { const newSheet = doc.createElement('div'); newSheet.className = 'sheet'; newSheet.innerHTML = `
`; doc.body.appendChild(newSheet); return newSheet; } // 6. 페이지 재구성 let currentPage = createNewPage(currentHeaderTitle); let currentBody = currentPage.querySelector('.body-content'); allNodes.forEach(node => { // 강제 페이지 브레이크 if (node.classList && node.classList.contains('page-break-forced')) { currentPage = createNewPage(currentHeaderTitle); currentBody = currentPage.querySelector('.body-content'); currentBody.appendChild(node); return; } // H1: 새 섹션 시작 if (node.tagName === 'H1') { currentHeaderTitle = node.innerText.split('-')[0].trim(); if (currentBody.children.length > 0) { currentPage = createNewPage(currentHeaderTitle); currentBody = currentPage.querySelector('.body-content'); } else { currentPage.querySelector('.page-header').innerText = currentHeaderTitle; } } // H2, H3: 남은 공간 부족하면 새 페이지 if (['H2', 'H3'].includes(node.tagName)) { const spaceLeft = MAX_HEIGHT - currentBody.scrollHeight; if (spaceLeft < HEADING_RESERVE) { currentPage = createNewPage(currentHeaderTitle); currentBody = currentPage.querySelector('.body-content'); } } // 노드 추가 currentBody.appendChild(node); // 전 페이지로 강제 이동 설정된 경우 스킵 if (node.classList && node.classList.contains('move-to-prev-page')) { return; } // 높이 초과 시 새 페이지로 이동 if (currentBody.scrollHeight > MAX_HEIGHT) { currentBody.removeChild(node); currentPage = createNewPage(currentHeaderTitle); currentBody = currentPage.querySelector('.body-content'); currentBody.appendChild(node); } }); // 7. 편집 모드였으면 복원 if (isEditing) { bindIframeEditEvents(); } // 8. generatedHTML 업데이트 (전역 변수) if (typeof generatedHTML !== 'undefined') { generatedHTML = '' + doc.documentElement.outerHTML; } // ===== 스크롤 위치 복원 ===== setTimeout(() => { if (iframe?.contentWindow) { iframe.contentWindow.scrollTo(0, scrollY); } }, 50); toast('✅ 지능형 정렬 완료 (' + pageNum + '페이지)'); } catch (e) { console.error('smartAlign 오류:', e); toast('❌ 정렬 중 오류: ' + e.message); } }, 100); } // ===== 새페이지 시작 ===== function forcePageBreak() { const doc = getIframeDoc(); if (!doc) { toast('⚠️ 문서가 로드되지 않았습니다'); return; } const selection = doc.getSelection(); if (!selection || !selection.anchorNode) { toast('⚠️ 분리할 위치를 클릭하세요'); return; } let targetEl = selection.anchorNode.nodeType === 1 ? selection.anchorNode : selection.anchorNode.parentElement; while (targetEl && targetEl.parentElement) { if (targetEl.parentElement.classList && targetEl.parentElement.classList.contains('body-content')) { break; } targetEl = targetEl.parentElement; } if (!targetEl || !targetEl.parentElement || !targetEl.parentElement.classList.contains('body-content')) { toast('⚠️ 본문 블록을 먼저 클릭하세요'); return; } saveState(); const currentBody = targetEl.parentElement; const currentSheet = currentBody.closest('.sheet'); const sheets = Array.from(doc.querySelectorAll('.sheet')); const currentIndex = sheets.indexOf(currentSheet); // 클릭한 요소부터 끝까지 수집 const elementsToMove = []; let sibling = targetEl; while (sibling) { elementsToMove.push(sibling); sibling = sibling.nextElementSibling; } if (elementsToMove.length === 0) { toast('⚠️ 이동할 내용이 없습니다'); return; } // 다음 페이지 찾기 let nextSheet = sheets[currentIndex + 1]; let nextBody; if (!nextSheet || !nextSheet.querySelector('.body-content')) { const oldHeader = currentSheet.querySelector('.page-header'); const oldFooter = currentSheet.querySelector('.page-footer'); nextSheet = doc.createElement('div'); nextSheet.className = 'sheet'; nextSheet.innerHTML = `
`; currentSheet.after(nextSheet); } nextBody = nextSheet.querySelector('.body-content'); // 역순으로 맨 앞에 삽입 (순서 유지) for (let i = elementsToMove.length - 1; i >= 0; i--) { nextBody.insertBefore(elementsToMove[i], nextBody.firstChild); } // 첫 번째 요소에 페이지 브레이크 마커 추가 (나중에 지능형 정렬이 존중함) targetEl.classList.add('page-break-forced'); // 페이지 번호만 재정렬 (smartAlign 호출 안 함!) renumberPages(doc); toast('✅ 다음 페이지로 이동됨'); } // ===== 전페이지로 이동 (즉시 적용) ===== function moveToPrevPage() { const doc = getIframeDoc(); if (!doc) { toast('⚠️ 문서가 로드되지 않았습니다'); return; } const selection = doc.getSelection(); if (!selection || !selection.anchorNode) { toast('⚠️ 이동할 블록을 클릭하세요'); return; } // 현재 선택된 요소에서 body-content 직계 자식 찾기 let targetEl = selection.anchorNode.nodeType === 1 ? selection.anchorNode : selection.anchorNode.parentElement; while (targetEl && targetEl.parentElement) { if (targetEl.parentElement.classList && targetEl.parentElement.classList.contains('body-content')) { break; } targetEl = targetEl.parentElement; } if (!targetEl || !targetEl.parentElement || !targetEl.parentElement.classList.contains('body-content')) { toast('⚠️ 본문 블록을 먼저 클릭하세요'); return; } saveState(); // 현재 sheet 찾기 const currentSheet = targetEl.closest('.sheet'); const sheets = Array.from(doc.querySelectorAll('.sheet')); const currentIndex = sheets.indexOf(currentSheet); // 이전 페이지 찾기 (표지 제외) if (currentIndex <= 1) { toast('⚠️ 이전 페이지가 없습니다'); return; } const prevSheet = sheets[currentIndex - 1]; const prevBody = prevSheet.querySelector('.body-content'); if (!prevBody) { toast('⚠️ 이전 페이지에 본문 영역이 없습니다'); return; } // 요소를 이전 페이지 맨 아래로 이동 prevBody.appendChild(targetEl); // 현재 페이지가 비었으면 삭제 const currentBody = currentSheet.querySelector('.body-content'); if (currentBody && currentBody.children.length === 0) { currentSheet.remove(); } // 페이지 번호 재정렬 renumberPages(doc); toast('✅ 전 페이지로 이동됨'); } // ===== 페이지 번호 재정렬 ===== function renumberPages(doc) { const sheets = doc.querySelectorAll('.sheet'); let pageNum = 1; sheets.forEach((sheet, idx) => { if (idx === 0) return; // 표지는 번호 없음 const pgNum = sheet.querySelector('.pg-num'); if (pgNum) { pgNum.innerText = `- ${pageNum++} -`; } }); } // DOM 로드 시 초기화 document.addEventListener('DOMContentLoaded', initEditor);