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 = ` +
+ + +
+ + + + +
+ + +
+
+ A + + 글자 색상 +
+
+ A + + 배경 색상 +
+
+ + + +
+ + + + +
+ + + +
+ +
+ `; + 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 - 문서 입력 -

- - -
- - -
- -
- -
- -
- - - +
참고 파일 확인
+
+
폴더 경로가 설정되지 않음
+
+ 전체 파일 + 0개 +
+
+ 확인 (변환 가능) + 0개 ✓ +
+
+ 미확인 + 0개
-
- - - - - -
-

옵션 설정

-
- -
- - -
- - -
- - -
+
+
+
- - -
- - + +
+ 참고 링크 + 0개 +
+ +
+ HTML 입력 + 없음
- - - - +
+ + + + + +
+
진행 상태
+
+
+ + 참고 파일 확인 +
+
+
+ + Step 1: 파일 변환 +
+
+ + Step 2: 텍스트 추출 +
+
+ + Step 3: 도메인 분석 +
+
+ + Step 4: 청킹/요약 +
+
+ + Step 5: RAG 구축 +
+
+ + Step 6: Corpus 생성 +
+
+ + Step 7: 목차 구성 +
+
+
+ + Step 8: 콘텐츠 생성 +
+
+ + Step 9: HTML 변환 +
+
+
- - -
-
-

- 2 - 미리보기 & 다운로드 -

+
+ + +
+ +
+ + + +
+ + + +
+ +
+
+
+ +
+
📄
+
HTML을 입력하고 생성하세요
+
좌측에서 HTML 붙여넣기 또는 파일 업로드
+
+
+
+
+ + + +
+ + +
+
+ 문서 설정 +
+ +
+ +
+
문서 유형
+
+ +
+ + 📋 기획서 + + +
+
+
+ +
+
+
+
+
+
+
+
+
+ +
[첨부]
+
+
+
+
+
+
+
+
기획서 (보고자료)
+
임원보고용 정형화된 1~2페이지 문서
+
+
📄 1p 본문만 / 1p+1p첨부 / 1p+np첨부
+
🎨 Navy 양식 (A4 인쇄 최적화)
+
✍️ 개조식 자동 변환
+
+
+
+ + +
+ + 📄 보고서 + 준비중 + +
+
+
+
+
+
+
+
+
+
+
+
+
보고서 (HWP)
+
RAG 기반 장문 보고서 → HWPX 출력
+
+
🏷️ AI 스타일 자동 태깅
+
📝 대제목/중제목/소제목/본문
+
한글에서 스타일 일괄 변경
+
+
+
+ + +
+ + 📊 발표자료 + 준비중 + +
+
+
+
제목
+
+
+
+
+
본문
+
+
+
+
+
+
결론
+
+
+
+
발표자료 (PPT)
+
프레젠테이션 형식 슬라이드
+
+
📊 슬라이드 자동 구성
+
🎯 핵심 내용 추출
+
🖼️ 도식화 자동 생성
+
+
+
+
- - + +
- - -
-
-
- - - -

문서를 입력하고 생성 버튼을 누르세요

+ + +
+ +
+
페이지 구성
+
+
+ + +
+
+ + +
+
+ + +
- -
- - - - - -
- - -
-

💡 HWP 파일이 필요하신가요?

-

- HWP 변환은 Windows + 한글 프로그램이 필요합니다. HTML 다운로드 후 제공되는 Python 스크립트로 로컬에서 변환할 수 있습니다. -

- - HWP 변환 스크립트 받기 → - +
+ + +
+
+ + 준비됨
-
- - - + + + + + + + - + +
+ +
🤖 AI로 수정하기
+
선택된 텍스트:
+
+ + +
+ - + \ No newline at end of file