v4:코드모듈화_20260123
This commit is contained in:
@@ -5,12 +5,25 @@
|
||||
padding: 8px 12px;
|
||||
background: var(--ui-panel);
|
||||
border-bottom: 1px solid var(--ui-border);
|
||||
gap: 4px;
|
||||
gap: 6px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.format-bar.active { display: flex; }
|
||||
|
||||
/* 편집 바 2줄 구조 */
|
||||
.format-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.format-row:first-child {
|
||||
border-bottom: 1px solid var(--ui-border);
|
||||
padding-bottom: 8px;
|
||||
}
|
||||
|
||||
.format-btn {
|
||||
padding: 6px 10px;
|
||||
background: none;
|
||||
@@ -61,6 +74,26 @@
|
||||
|
||||
.format-btn:hover .tooltip { opacity: 1; }
|
||||
|
||||
/* 페이지 버튼 스타일 */
|
||||
.format-btn.page-btn {
|
||||
padding: 6px 12px;
|
||||
font-size: 12px;
|
||||
white-space: nowrap;
|
||||
flex-shrink: 0;
|
||||
min-width: fit-content;
|
||||
}
|
||||
|
||||
/* 페이지 브레이크 표시 */
|
||||
.page-break-forced {
|
||||
border-top: 3px solid #e65100 !important;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.move-to-prev-page {
|
||||
border-top: 3px dashed #1976d2 !important;
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
/* 색상 선택기 */
|
||||
.color-picker-btn {
|
||||
position: relative;
|
||||
@@ -185,6 +218,65 @@
|
||||
animation: toastIn 0.3s ease, toastOut 0.3s ease 2.7s forwards;
|
||||
}
|
||||
|
||||
.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;
|
||||
bottom: 100%;
|
||||
right: 0;
|
||||
background: rgba(0,0,0,0.8);
|
||||
color: white;
|
||||
padding: 2px 6px;
|
||||
border-radius: 3px;
|
||||
font-size: 10px;
|
||||
white-space: nowrap;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.resizable-container:hover .size-tooltip,
|
||||
.resizable-container.resizing .size-tooltip { opacity: 1; }
|
||||
|
||||
|
||||
@keyframes toastIn {
|
||||
from { opacity: 0; transform: translateY(20px); }
|
||||
to { opacity: 1; transform: translateY(0); }
|
||||
|
||||
@@ -11,6 +11,7 @@ let redoStack = [];
|
||||
const MAX_HISTORY = 50;
|
||||
let isApplyingFormat = false;
|
||||
|
||||
// ===== 편집 바 HTML 생성 =====
|
||||
// ===== 편집 바 HTML 생성 =====
|
||||
function createFormatBar() {
|
||||
const formatBarHTML = `
|
||||
@@ -21,52 +22,120 @@ function createFormatBar() {
|
||||
<option value="나눔고딕">나눔고딕</option>
|
||||
<option value="돋움">돋움</option>
|
||||
</select>
|
||||
<button class="format-btn" onclick="loadLocalFonts()">📁<span class="tooltip">폰트 불러오기</span></button>
|
||||
<input type="number" class="format-select" id="fontSizeInput" value="12" min="8" max="72"
|
||||
style="width:55px;" onchange="applyFontSizeInput(this.value)">
|
||||
<div class="format-divider"></div>
|
||||
<button class="format-btn" id="btnBold" onclick="formatText('bold')"><b>B</b><span class="tooltip">굵게 (Ctrl+B)</span></button>
|
||||
<button class="format-btn" id="btnItalic" onclick="formatText('italic')"><i>I</i><span class="tooltip">기울임 (Ctrl+I)</span></button>
|
||||
<button class="format-btn" id="btnUnderline" onclick="formatText('underline')"><u>U</u><span class="tooltip">밑줄 (Ctrl+U)</span></button>
|
||||
<button class="format-btn" id="btnStrike" onclick="formatText('strikeThrough')"><s>S</s><span class="tooltip">취소선</span></button>
|
||||
<button class="format-btn" onclick="formatText('bold')"><b>B</b><span class="tooltip">굵게</span></button>
|
||||
<button class="format-btn" onclick="formatText('italic')"><i>I</i><span class="tooltip">기울임</span></button>
|
||||
<button class="format-btn" onclick="formatText('underline')"><u>U</u><span class="tooltip">밑줄</span></button>
|
||||
<button class="format-btn" onclick="formatText('strikeThrough')"><s>S</s><span class="tooltip">취소선</span></button>
|
||||
<div class="format-divider"></div>
|
||||
<button class="format-btn" onclick="adjustLetterSpacing(-0.5)">A⇠<span class="tooltip">자간 줄이기</span></button>
|
||||
<button class="format-btn" onclick="adjustLetterSpacing(0.5)">A⇢<span class="tooltip">자간 늘리기</span></button>
|
||||
<select class="format-select" onchange="if(this.value) formatText(this.value); this.selectedIndex=0;">
|
||||
<option value="">정렬 ▾</option>
|
||||
<option value="justifyLeft">⫷ 왼쪽</option>
|
||||
<option value="justifyCenter">☰ 가운데</option>
|
||||
<option value="justifyRight">⫸ 오른쪽</option>
|
||||
</select>
|
||||
<select class="format-select" onchange="if(this.value) adjustLetterSpacing(parseFloat(this.value)); this.selectedIndex=0;">
|
||||
<option value="">자간 ▾</option>
|
||||
<option value="-0.5">좁게</option>
|
||||
<option value="-1">더 좁게</option>
|
||||
<option value="0.5">넓게</option>
|
||||
<option value="1">더 넓게</option>
|
||||
</select>
|
||||
<div class="format-divider"></div>
|
||||
<div class="color-picker-btn format-btn">
|
||||
<span style="border-bottom:3px solid #000;">A</span>
|
||||
<input type="color" id="textColor" value="#000000" onchange="applyTextColor(this.value)">
|
||||
<span class="tooltip">글자 색상</span>
|
||||
</div>
|
||||
<div class="color-picker-btn format-btn">
|
||||
<span style="background:#ff0;padding:0 4px;">A</span>
|
||||
<input type="color" id="bgColor" value="#ffff00" onchange="applyBgColor(this.value)">
|
||||
<span class="tooltip">배경 색상</span>
|
||||
</div>
|
||||
<div class="format-divider"></div>
|
||||
<button class="format-btn" onclick="formatText('justifyLeft')">⫷<span class="tooltip">왼쪽 정렬</span></button>
|
||||
<button class="format-btn" onclick="formatText('justifyCenter')">☰<span class="tooltip">가운데 정렬</span></button>
|
||||
<button class="format-btn" onclick="formatText('justifyRight')">⫸<span class="tooltip">오른쪽 정렬</span></button>
|
||||
<div class="format-divider"></div>
|
||||
<button class="format-btn" onclick="toggleBulletList()">•≡<span class="tooltip">글머리 기호</span></button>
|
||||
<button class="format-btn" onclick="toggleNumberList()">1.<span class="tooltip">번호 목록</span></button>
|
||||
<button class="format-btn" onclick="adjustIndent(-1)">⇤<span class="tooltip">내어쓰기</span></button>
|
||||
<button class="format-btn" onclick="adjustIndent(1)">⇥<span class="tooltip">들여쓰기</span></button>
|
||||
<div class="format-divider"></div>
|
||||
<button class="format-btn" onclick="openTableModal()">▦<span class="tooltip">표 삽입</span></button>
|
||||
<button class="format-btn" onclick="insertImage()">🖼️<span class="tooltip">그림 삽입</span></button>
|
||||
<button class="format-btn" onclick="insertHR()">―<span class="tooltip">구분선</span></button>
|
||||
<div class="format-divider"></div>
|
||||
<select class="format-select" onchange="applyHeading(this.value)" style="min-width:100px;">
|
||||
<option value="">본문</option>
|
||||
<option value="h1">제목 1</option>
|
||||
<option value="h2">제목 2</option>
|
||||
<option value="h3">제목 3</option>
|
||||
<select class="format-select" onchange="handleInsert(this.value); this.selectedIndex=0;">
|
||||
<option value="">삽입 ▾</option>
|
||||
<option value="table">▦ 표</option>
|
||||
<option value="image">🖼️ 그림</option>
|
||||
<option value="hr">― 구분선</option>
|
||||
</select>
|
||||
<select class="format-select" onchange="applyHeading(this.value)">
|
||||
<option value="">본문</option>
|
||||
<option value="h1">제목1</option>
|
||||
<option value="h2">제목2</option>
|
||||
<option value="h3">제목3</option>
|
||||
</select>
|
||||
<div class="format-divider"></div>
|
||||
<button class="format-btn page-btn" onclick="smartAlign()">🔄 지능형 정렬</button>
|
||||
<button class="format-btn page-btn" onclick="forcePageBreak()">📄 새페이지</button>
|
||||
<button class="format-btn page-btn" onclick="moveToPrevPage()">📤 전페이지</button>
|
||||
</div>
|
||||
`;
|
||||
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 = `
|
||||
@@ -457,11 +526,196 @@ function handleEditorKeydown(e) {
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// ===== 리사이즈 핸들 추가 함수 =====
|
||||
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);
|
||||
@@ -479,6 +733,81 @@ function bindIframeEditEvents() {
|
||||
}
|
||||
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('📊 열 너비 조절됨');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// ===== 편집 모드 토글 =====
|
||||
@@ -533,7 +862,7 @@ function toggleEditMode() {
|
||||
function initEditor() {
|
||||
// 편집 바가 없으면 생성
|
||||
if (!document.getElementById('formatBar')) {
|
||||
const previewContainer = document.querySelector('.preview-container');
|
||||
const previewContainer = document.querySelector('.main');
|
||||
if (previewContainer) {
|
||||
previewContainer.insertAdjacentHTML('afterbegin', createFormatBar());
|
||||
}
|
||||
@@ -550,5 +879,330 @@ function initEditor() {
|
||||
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 = `
|
||||
<div class="page-header">${headerText}</div>
|
||||
<div class="body-content"></div>
|
||||
<div class="page-footer">
|
||||
<span class="rpt-title">${reportTitle}</span>
|
||||
<span class="pg-num">- ${pageNum++} -</span>
|
||||
</div>`;
|
||||
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 = '<!DOCTYPE html>' + 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 = `
|
||||
<div class="page-header">${oldHeader ? oldHeader.innerText : ''}</div>
|
||||
<div class="body-content"></div>
|
||||
<div class="page-footer">
|
||||
<span class="rpt-title">${oldFooter?.querySelector('.rpt-title')?.innerText || ''}</span>
|
||||
<span class="pg-num">- - -</span>
|
||||
</div>`;
|
||||
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);
|
||||
document.addEventListener('DOMContentLoaded', initEditor);
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user