diff --git a/app.py b/app.py
index 3081851..81ab3ce 100644
--- a/app.py
+++ b/app.py
@@ -359,6 +359,76 @@ def refine():
return jsonify({'error': f'서버 오류: {str(e)}'}), 500
+@app.route('/refine-selection', methods=['POST'])
+def refine_selection():
+ """선택된 부분만 수정"""
+ try:
+ data = request.json
+ current_html = data.get('current_html', '')
+ selected_text = data.get('selected_text', '')
+ user_request = data.get('request', '')
+
+ if not current_html or not selected_text or not user_request:
+ return jsonify({'error': '필수 데이터가 없습니다.'}), 400
+
+ # Claude API 호출
+ message = client.messages.create(
+ model="claude-sonnet-4-20250514",
+ max_tokens=8000,
+ messages=[{
+ "role": "user",
+"content" : f"""HTML 문서에서 지정된 부분만 수정해주세요.
+
+## 전체 문서 (컨텍스트 파악용)
+{current_html}
+
+## 수정 대상 텍스트
+"{selected_text}"
+
+## 수정 요청
+{user_request}
+
+## 규칙
+1. 요청을 분석하여 수정 유형을 판단:
+ - TEXT: 텍스트 내용만 수정 (요약, 문장 변경, 단어 수정, 번역 등)
+ - STRUCTURE: HTML 구조 변경 필요 (표 생성, 박스 추가, 레이아웃 변경 등)
+
+2. 반드시 다음 형식으로만 출력:
+
+TYPE: (TEXT 또는 STRUCTURE)
+CONTENT:
+(수정된 내용)
+
+3. TEXT인 경우: 순수 텍스트만 출력 (HTML 태그 없이)
+4. STRUCTURE인 경우: 완전한 HTML 요소 출력 (기존 클래스명 유지)
+5. 개조식 문체 유지 (~임, ~함, ~필요)
+"""
+ }]
+ )
+
+ result = message.content[0].text
+ result = result.replace('```html', '').replace('```', '').strip()
+
+ # TYPE과 CONTENT 파싱
+ edit_type = 'TEXT'
+ content = result
+
+ if 'TYPE:' in result and 'CONTENT:' in result:
+ type_line = result.split('CONTENT:')[0]
+ if 'STRUCTURE' in type_line:
+ edit_type = 'STRUCTURE'
+ content = result.split('CONTENT:')[1].strip()
+
+ return jsonify({
+ 'success': True,
+ 'type': edit_type,
+ 'html': content
+ })
+
+ except Exception as e:
+ return jsonify({'error': str(e)}), 500
+
+
@app.route('/download/html', methods=['POST'])
def download_html():
"""HTML 파일 다운로드"""
diff --git a/static/css/editor.css b/static/css/editor.css
new file mode 100644
index 0000000..fc9d982
--- /dev/null
+++ b/static/css/editor.css
@@ -0,0 +1,205 @@
+/* ===== 편집 바 스타일 ===== */
+.format-bar {
+ display: none;
+ align-items: center;
+ padding: 8px 12px;
+ background: var(--ui-panel);
+ border-bottom: 1px solid var(--ui-border);
+ gap: 4px;
+ flex-wrap: wrap;
+}
+
+.format-bar.active { display: flex; }
+
+.format-btn {
+ padding: 6px 10px;
+ background: none;
+ border: 1px solid transparent;
+ border-radius: 4px;
+ cursor: pointer;
+ color: var(--ui-text);
+ font-size: 14px;
+ position: relative;
+}
+
+.format-btn:hover { background: var(--ui-hover); }
+.format-btn.active { background: rgba(0, 200, 83, 0.3); color: var(--ui-accent); }
+
+.format-select {
+ padding: 5px 8px;
+ border: 1px solid var(--ui-border);
+ border-radius: 4px;
+ background: var(--ui-bg);
+ color: var(--ui-text);
+ font-size: 12px;
+}
+
+.format-divider {
+ width: 1px;
+ height: 24px;
+ background: var(--ui-border);
+ margin: 0 6px;
+}
+
+/* 툴팁 */
+.format-btn .tooltip {
+ position: absolute;
+ bottom: -28px;
+ left: 50%;
+ transform: translateX(-50%);
+ background: #333;
+ color: #fff;
+ padding: 4px 8px;
+ border-radius: 4px;
+ font-size: 11px;
+ white-space: nowrap;
+ opacity: 0;
+ pointer-events: none;
+ transition: opacity 0.2s;
+ z-index: 100;
+}
+
+.format-btn:hover .tooltip { opacity: 1; }
+
+/* 색상 선택기 */
+.color-picker-btn {
+ position: relative;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+}
+
+.color-picker-btn input[type="color"] {
+ position: absolute;
+ width: 100%;
+ height: 100%;
+ opacity: 0;
+ cursor: pointer;
+}
+
+/* 편집 모드 활성 블록 */
+.active-block {
+ outline: 2px dashed var(--ui-accent) !important;
+ outline-offset: 2px;
+}
+
+/* 표 삽입 모달 */
+.table-modal {
+ display: none;
+ position: fixed;
+ top: 0;
+ left: 0;
+ right: 0;
+ bottom: 0;
+ background: rgba(0, 0, 0, 0.7);
+ z-index: 2000;
+ align-items: center;
+ justify-content: center;
+}
+
+.table-modal.active { display: flex; }
+
+.table-modal-content {
+ background: var(--ui-panel);
+ border-radius: 12px;
+ padding: 24px;
+ width: 320px;
+ border: 1px solid var(--ui-border);
+}
+
+.table-modal-title {
+ font-size: 16px;
+ font-weight: 700;
+ color: var(--ui-text);
+ margin-bottom: 20px;
+}
+
+.table-modal-row {
+ display: flex;
+ align-items: center;
+ margin-bottom: 12px;
+}
+
+.table-modal-row label {
+ flex: 1;
+ font-size: 13px;
+ color: var(--ui-dim);
+}
+
+.table-modal-row input[type="number"] {
+ width: 60px;
+ padding: 6px 8px;
+ border: 1px solid var(--ui-border);
+ border-radius: 4px;
+ background: var(--ui-bg);
+ color: var(--ui-text);
+ text-align: center;
+}
+
+.table-modal-row input[type="checkbox"] {
+ width: 18px;
+ height: 18px;
+}
+
+.table-modal-buttons {
+ display: flex;
+ gap: 10px;
+ margin-top: 20px;
+}
+
+.table-modal-btn {
+ flex: 1;
+ padding: 10px;
+ border-radius: 6px;
+ border: none;
+ font-size: 13px;
+ cursor: pointer;
+}
+
+.table-modal-btn.primary {
+ background: var(--ui-accent);
+ color: #003300;
+ font-weight: 600;
+}
+
+.table-modal-btn.secondary {
+ background: var(--ui-border);
+ color: var(--ui-text);
+}
+
+/* 토스트 메시지 */
+.toast-container {
+ position: fixed;
+ bottom: 80px;
+ left: 50%;
+ transform: translateX(-50%);
+ z-index: 3000;
+}
+
+.toast {
+ background: #333;
+ color: #fff;
+ padding: 10px 20px;
+ border-radius: 8px;
+ font-size: 13px;
+ animation: toastIn 0.3s ease, toastOut 0.3s ease 2.7s forwards;
+}
+
+@keyframes toastIn {
+ from { opacity: 0; transform: translateY(20px); }
+ to { opacity: 1; transform: translateY(0); }
+}
+
+@keyframes toastOut {
+ from { opacity: 1; }
+ to { opacity: 0; }
+}
+
+/* 인쇄 시 숨김 */
+@media print {
+ .format-bar,
+ .table-modal,
+ .toast-container {
+ display: none !important;
+ }
+}
\ No newline at end of file
diff --git a/static/js/editor.js b/static/js/editor.js
new file mode 100644
index 0000000..0aafac8
--- /dev/null
+++ b/static/js/editor.js
@@ -0,0 +1,554 @@
+/**
+ * 글벗 Light - 편집 바 모듈
+ * editor.js
+ */
+
+// ===== 전역 변수 =====
+let isEditing = false;
+let activeBlock = null;
+let historyStack = [];
+let redoStack = [];
+const MAX_HISTORY = 50;
+let isApplyingFormat = false;
+
+// ===== 편집 바 HTML 생성 =====
+function createFormatBar() {
+ const formatBarHTML = `
+
+ `;
+ return formatBarHTML;
+}
+
+// ===== 표 삽입 모달 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);
+ }
+}
+
+// ===== iframe 편집 이벤트 바인딩 =====
+function bindIframeEditEvents() {
+ const doc = getIframeDoc();
+ if (!doc) return;
+
+ // 키보드 이벤트
+ 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();
+ });
+}
+
+// ===== 편집 모드 토글 =====
+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('.preview-container');
+ if (previewContainer) {
+ previewContainer.insertAdjacentHTML('afterbegin', createFormatBar());
+ }
+ }
+
+ // 표 모달이 없으면 생성
+ if (!document.getElementById('tableModal')) {
+ document.body.insertAdjacentHTML('beforeend', createTableModal());
+ }
+
+ // 토스트 컨테이너 생성
+ createToastContainer();
+
+ console.log('Editor initialized');
+}
+
+// DOM 로드 시 초기화
+document.addEventListener('DOMContentLoaded', initEditor);
\ No newline at end of file
diff --git a/templates/index.html b/templates/index.html
index 1efcb2c..3268249 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -3,381 +3,1962 @@
- 글벗 Light - 상시 업무용 보고서 생성기
-
-
+ 글벗 - AI 문서 자동화 시스템
+
-
-
-
-
-
+
+
+
+
+
+
+
+
-
-
-
-
-
-
-
- 1
- 문서 입력
-
-
-
-
-
-
-
-
-