v6:HWPX 템플릿 분석·저장·관리_20260128

This commit is contained in:
2026-02-20 11:43:44 +09:00
parent 71740ce912
commit 5129ee69d4
8 changed files with 1482 additions and 94 deletions

View File

@@ -19,6 +19,334 @@
--ui-error: #f85149;
--ui-info: #58a6ff;
}
/* ===== 사용자 템플릿 영역 ===== */
.user-templates-section {
max-height: 200px;
overflow-y: auto;
margin: 10px 0;
padding-right: 5px;
}
.user-templates-section::-webkit-scrollbar {
width: 4px;
}
.user-templates-section::-webkit-scrollbar-thumb {
background: var(--ui-border);
border-radius: 2px;
}
.user-templates-section::-webkit-scrollbar-thumb:hover {
background: var(--ui-dim);
}
.template-divider {
height: 1px;
background: var(--ui-border);
margin: 10px 0;
position: relative;
}
.template-divider::after {
content: '사용자 템플릿';
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
background: var(--ui-nav);
padding: 0 8px;
font-size: 9px;
color: var(--ui-dim);
text-transform: uppercase;
letter-spacing: 0.5px;
}
/* 사용자 템플릿 아이템 */
.user-template-item {
display: flex;
align-items: center;
gap: 10px;
padding: 10px 12px;
background: var(--ui-panel);
border: 1px solid var(--ui-border);
border-radius: 6px;
cursor: pointer;
transition: all 0.15s;
position: relative;
margin-bottom: 4px;
}
.user-template-item:hover {
background: var(--ui-hover);
border-color: var(--ui-dim);
}
.user-template-item.selected {
border-color: var(--ui-accent);
background: rgba(0, 200, 83, 0.1);
}
.user-template-item .label {
flex: 1;
font-size: 13px;
font-weight: 500;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.user-template-item .delete-btn {
width: 20px;
height: 20px;
border: none;
background: transparent;
color: var(--ui-dim);
font-size: 14px;
cursor: pointer;
border-radius: 4px;
display: flex;
align-items: center;
justify-content: center;
opacity: 0;
transition: all 0.15s;
}
.user-template-item:hover .delete-btn {
opacity: 1;
}
.user-template-item .delete-btn:hover {
background: var(--ui-error);
color: white;
}
/* 템플릿 추가 모달 */
.template-modal {
display: none;
position: fixed;
inset: 0;
background: rgba(0,0,0,0.75);
align-items: center;
justify-content: center;
z-index: 2000;
}
.template-modal.active {
display: flex;
}
.template-modal-content {
background: var(--ui-panel);
border: 1px solid var(--ui-border);
border-radius: 12px;
padding: 24px;
width: 420px;
box-shadow: 0 15px 50px rgba(0,0,0,0.5);
}
.template-modal-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 20px;
}
.template-modal-title {
font-size: 16px;
font-weight: 700;
color: var(--ui-accent);
display: flex;
align-items: center;
gap: 8px;
}
.template-modal-close {
width: 28px;
height: 28px;
border: none;
background: transparent;
color: var(--ui-dim);
font-size: 18px;
cursor: pointer;
border-radius: 6px;
}
.template-modal-close:hover {
background: var(--ui-hover);
color: var(--ui-text);
}
.template-input-group {
margin-bottom: 16px;
}
.template-input-label {
font-size: 12px;
font-weight: 600;
color: var(--ui-dim);
margin-bottom: 8px;
display: block;
}
.template-name-input {
width: 100%;
padding: 10px 12px;
border: 1px solid var(--ui-border);
border-radius: 6px;
background: var(--ui-bg);
color: var(--ui-text);
font-size: 13px;
}
.template-name-input:focus {
outline: none;
border-color: var(--ui-accent);
}
.template-dropzone {
border: 2px dashed var(--ui-border);
border-radius: 8px;
padding: 30px;
text-align: center;
cursor: pointer;
transition: all 0.2s;
}
.template-dropzone:hover,
.template-dropzone.dragover {
border-color: var(--ui-accent);
background: rgba(0, 200, 83, 0.05);
}
.template-dropzone-icon {
font-size: 36px;
margin-bottom: 10px;
}
.template-dropzone-text {
font-size: 13px;
color: var(--ui-text);
margin-bottom: 5px;
}
.template-dropzone-hint {
font-size: 11px;
color: var(--ui-dim);
}
.template-dropzone-file {
display: none;
align-items: center;
gap: 10px;
padding: 10px;
background: var(--ui-bg);
border-radius: 6px;
margin-top: 10px;
}
.template-dropzone-file.show {
display: flex;
}
.template-dropzone-file .filename {
flex: 1;
font-size: 12px;
color: var(--ui-accent);
}
.template-dropzone-file .remove {
color: var(--ui-dim);
cursor: pointer;
}
.template-dropzone-file .remove:hover {
color: var(--ui-error);
}
.template-submit-btn {
width: 100%;
padding: 12px;
border: none;
border-radius: 8px;
background: linear-gradient(135deg, var(--ui-accent), #00a844);
color: #003300;
font-size: 14px;
font-weight: 700;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
gap: 8px;
margin-top: 20px;
transition: all 0.2s;
}
.template-submit-btn:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 15px rgba(0, 200, 83, 0.3);
}
.template-submit-btn:disabled {
background: var(--ui-border);
color: var(--ui-dim);
cursor: not-allowed;
}
.template-submit-btn .spinner {
display: none;
width: 16px;
height: 16px;
border: 2px solid transparent;
border-top-color: currentColor;
border-radius: 50%;
animation: spin 0.8s linear infinite;
}
/* 사용자 템플릿 프리뷰 */
.user-template-preview {
display: none;
position: fixed;
width: 280px;
background: var(--ui-panel);
border: 1px solid var(--ui-border);
border-radius: 12px;
padding: 15px;
box-shadow: 0 10px 40px rgba(0,0,0,0.5);
z-index: 1000;
pointer-events: none;
}
.user-template-preview::after {
content: '';
position: absolute;
right: -8px;
top: 50%;
transform: translateY(-50%);
border: 8px solid transparent;
border-left-color: var(--ui-panel);
}
.user-template-preview.show {
display: block;
}
.preview-analyzed-features {
margin-top: 12px;
}
.preview-analyzed-feature {
font-size: 11px;
color: var(--ui-accent);
padding: 3px 0;
display: flex;
align-items: center;
gap: 6px;
}
.preview-analyzed-feature::before {
content: '✓';
}
* { margin: 0; padding: 0; box-sizing: border-box; }
@@ -1354,9 +1682,16 @@
</div>
</div>
</div>
<div id="userTemplatesArea" style="display: none;">
<div class="template-divider"></div>
<div class="user-templates-section" id="userTemplatesList">
<!-- 동적으로 추가됨 -->
</div>
</div>
<!-- 템플릿 추가 버튼 -->
<button class="add-template-btn" onclick="addTemplate()">+ 템플릿 추가</button>
<button class="add-template-btn" onclick="openTemplateModal()">+ 템플릿 추가</button>
</div>
<!-- 기획서 옵션 -->
@@ -1411,7 +1746,15 @@
</div>
</div>
</div>
<!-- 템플릿 옵션 (요청사항만) -->
<div id="templateOptions" style="display:none;">
<div class="option-section">
<div class="option-title">요청사항</div>
<textarea class="request-textarea" id="templateInstructionInput" placeholder="예: 요약을 상세하게 작성해줘&#10;예: 특정 섹션 강조"></textarea>
</div>
</div>
<!-- 요청사항 -->
<div class="option-section">
<div class="option-title">요청사항</div>
@@ -1502,6 +1845,232 @@
let selectedText = '';
let selectedRange = null;
// ===== 템플릿 관련 변수 =====
let userTemplates = []; // [{id, name, features, created_at}, ...]
let selectedTemplateFile = null;
// ===== 템플릿 모달 =====
function openTemplateModal() {
document.getElementById('templateModal').classList.add('active');
document.getElementById('templateNameInput').value = '';
removeTemplateFile();
// 이벤트 리스너 재연결
document.getElementById('templateNameInput').oninput = updateTemplateSubmitBtn;
}
function closeTemplateModal() {
document.getElementById('templateModal').classList.remove('active');
}
function handleTemplateFile(input) {
if (input.files.length > 0) {
handleTemplateFileSelect(input.files[0]);
}
}
function handleTemplateFileSelect(file) {
const validExtensions = ['.hwpx', '.hwp', '.pdf'];
const ext = '.' + file.name.split('.').pop().toLowerCase();
if (!validExtensions.includes(ext)) {
alert('지원하지 않는 파일 형식입니다.\n(HWPX, HWP, PDF만 지원)');
return;
}
selectedTemplateFile = file;
document.getElementById('templateFileName').textContent = file.name;
document.getElementById('templateFileInfo').classList.add('show');
document.getElementById('templateDropzone').style.display = 'none';
// 파일명에서 템플릿 이름 자동 추출
const nameInput = document.getElementById('templateNameInput');
if (!nameInput.value.trim()) {
const baseName = file.name.replace(/\.[^/.]+$/, '');
nameInput.value = baseName;
}
updateTemplateSubmitBtn();
}
function removeTemplateFile() {
selectedTemplateFile = null;
document.getElementById('templateFileInput').value = '';
document.getElementById('templateFileInfo').classList.remove('show');
document.getElementById('templateDropzone').style.display = 'block';
updateTemplateSubmitBtn();
}
function updateTemplateSubmitBtn() {
const nameInput = document.getElementById('templateNameInput');
const btn = document.getElementById('templateSubmitBtn');
btn.disabled = !selectedTemplateFile || !nameInput.value.trim();
}
document.getElementById('templateNameInput')?.addEventListener('input', updateTemplateSubmitBtn);
// ===== 템플릿 제출 =====
async function submitTemplate() {
const name = document.getElementById('templateNameInput').value.trim();
if (!name || !selectedTemplateFile) return;
const btn = document.getElementById('templateSubmitBtn');
const spinner = document.getElementById('templateSpinner');
const text = document.getElementById('templateSubmitText');
btn.disabled = true;
spinner.style.display = 'inline-block';
text.textContent = '분석 중...';
try {
const formData = new FormData();
formData.append('name', name);
formData.append('file', selectedTemplateFile);
const response = await fetch('/analyze-template', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
// 템플릿 목록에 추가
userTemplates.push(data.template);
renderUserTemplates();
closeTemplateModal();
setStatus(`템플릿 "${name}" 추가 완료`, true);
} catch (error) {
alert('템플릿 분석 오류: ' + error.message);
} finally {
btn.disabled = false;
spinner.style.display = 'none';
text.textContent = '✨ 분석 및 추가';
}
}
// ===== 템플릿 목록 렌더링 =====
function renderUserTemplates() {
const container = document.getElementById('userTemplatesList');
const area = document.getElementById('userTemplatesArea');
if (userTemplates.length === 0) {
area.style.display = 'none';
return;
}
area.style.display = 'block';
container.innerHTML = userTemplates.map((tpl, idx) => `
<div class="user-template-item" data-id="${tpl.id}" onclick="selectDocType('template_${tpl.id}')">
<input type="radio" name="docType">
<span class="label">📑 ${tpl.name}</span>
<button class="delete-btn" onclick="event.stopPropagation(); deleteTemplate('${tpl.id}')" title="삭제">✕</button>
<div class="user-template-preview doc-type-preview">
<div class="preview-thumbnail report">
<div class="line h1"></div>
<div class="line body"></div>
<div class="line body" style="width:90%"></div>
<div class="line h2"></div>
<div class="line body" style="width:85%"></div>
</div>
<div class="preview-title">${tpl.name}</div>
<div class="preview-desc">사용자 정의 템플릿</div>
<div class="preview-analyzed-features">
${(tpl.features || []).map(f => `<div class="preview-analyzed-feature">${f}</div>`).join('')}
</div>
</div>
</div>
`).join('');
// 호버 이벤트 연결
container.querySelectorAll('.user-template-item').forEach(item => {
const preview = item.querySelector('.user-template-preview');
if (!preview) return;
item.addEventListener('mouseenter', (e) => {
const rect = item.getBoundingClientRect();
preview.style.top = (rect.top + rect.height / 2 - 150) + 'px';
preview.style.left = (rect.left - 295) + 'px';
preview.classList.add('show');
});
item.addEventListener('mouseleave', () => {
preview.classList.remove('show');
});
});
}
// ===== 템플릿 목록 로드 =====
async function loadUserTemplates() {
try {
const response = await fetch('/templates');
const data = await response.json();
if (data.templates) {
userTemplates = data.templates;
renderUserTemplates();
}
} catch (error) {
console.error('템플릿 목록 로드 실패:', error);
}
}
// ===== 문서 유형 선택 =====
function selectDocType(type) {
// PPT는 비활성화
if (type === 'presentation') {
return;
}
currentDocType = type;
// 모든 doc-type-item 선택 해제
document.querySelectorAll('.doc-type-item').forEach(item => {
item.classList.remove('selected');
const radio = item.querySelector('input[type="radio"]');
if (radio) radio.checked = false;
});
// 모든 user-template-item 선택 해제
document.querySelectorAll('.user-template-item').forEach(item => {
item.classList.remove('selected');
const radio = item.querySelector('input[type="radio"]');
if (radio) radio.checked = false;
});
// 선택한 아이템 활성화
let selectedItem;
if (type.startsWith('template_')) {
const templateId = type.replace('template_', '');
selectedItem = document.querySelector(`.user-template-item[data-id="${templateId}"]`);
} else {
selectedItem = document.querySelector(`.doc-type-item[data-type="${type}"]`);
}
if (selectedItem && !selectedItem.classList.contains('disabled')) {
selectedItem.classList.add('selected');
const radio = selectedItem.querySelector('input[type="radio"]');
if (radio) radio.checked = true;
}
// ⭐ 옵션 패널 전환 (3가지로 분기)
const isBriefing = (type === 'briefing');
const isReport = (type === 'report');
const isTemplate = type.startsWith('template_');
document.getElementById('briefingOptions').style.display = isBriefing ? 'block' : 'none';
document.getElementById('reportOptions').style.display = isReport ? 'block' : 'none';
document.getElementById('templateOptions').style.display = isTemplate ? 'block' : 'none';
}
// ===== HWP 추출 =====
async function exportHwp() {
if (!generatedHTML) {
@@ -1920,32 +2489,7 @@
reader.readAsText(file);
}
// ===== 문서 유형 선택 =====
function selectDocType(type) {
if (type === 'presentation') {
return; // PPT만 disabled
}
currentDocType = type;
document.querySelectorAll('.doc-type-item').forEach(item => {
item.classList.remove('selected');
if (item.dataset.type === type) {
item.classList.add('selected');
item.querySelector('input[type="radio"]').checked = true;
}
});
// 옵션 패널 표시/숨김
document.getElementById('briefingOptions').style.display = (type === 'briefing') ? 'block' : 'none';
document.getElementById('reportOptions').style.display = (type === 'report') ? 'block' : 'none';
}
// ===== 템플릿 추가 =====
function addTemplate() {
alert('템플릿 추가 기능은 준비중입니다.\n\n향후 사용자 정의 양식을 추가할 수 있습니다.');
}
// ===== 페이지 옵션 선택 =====
// ===== 페이지 옵션 선택 =====
function selectPageOption(option) {
currentPageOption = option;
document.querySelectorAll('.option-item').forEach(item => {
@@ -1961,9 +2505,13 @@
await generateBriefing();
} else if (currentDocType === 'report') {
await generateReport();
} else if (currentDocType.startsWith('template_')) {
// ⭐ 사용자 템플릿: 보고서와 동일하게 처리
await generateReport();
}
}
// ===== 기획서 생성 (기존 로직) =====
async function generateBriefing() {
if (!inputContent && !folderPath && referenceLinks.length === 0) {
@@ -2083,19 +2631,27 @@
updateStep(i, 'done');
}
let instruction = '';
if (currentDocType === 'report') {
instruction = document.getElementById('reportInstructionInput').value;
} else if (currentDocType.startsWith('template_')) {
instruction = document.getElementById('templateInstructionInput').value;
}
const response = await fetch('/generate-report', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
content: inputContent, // HTML 내용 추가
content: inputContent,
folder_path: folderPath,
cover: document.getElementById('reportCover').checked,
toc: document.getElementById('reportToc').checked,
divider: document.getElementById('reportDivider').checked,
instruction: document.getElementById('reportInstructionInput').value
template_id: currentDocType.startsWith('template_') ? currentDocType.replace('template_', '') : null,
cover: currentDocType === 'report' ? document.getElementById('reportCover').checked : false,
toc: currentDocType === 'report' ? document.getElementById('reportToc').checked : false,
divider: currentDocType === 'report' ? document.getElementById('reportDivider').checked : false,
instruction: instruction
})
});
const data = await response.json();
if (data.error) {
@@ -2333,6 +2889,33 @@
preview.classList.remove('show');
});
});
// 드롭존 이벤트 연결
const dropzone = document.getElementById('templateDropzone');
if (dropzone) {
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
dropzone.addEventListener(eventName, (e) => {
e.preventDefault();
e.stopPropagation();
});
});
['dragenter', 'dragover'].forEach(eventName => {
dropzone.addEventListener(eventName, () => dropzone.classList.add('dragover'));
});
['dragleave', 'drop'].forEach(eventName => {
dropzone.addEventListener(eventName, () => dropzone.classList.remove('dragover'));
});
dropzone.addEventListener('drop', (e) => {
const files = e.dataTransfer.files;
if (files.length > 0) {
handleTemplateFileSelect(files[0]);
}
});
}
// 템플릿 목록 로드
loadUserTemplates();
});
// Enter 키로 피드백 제출
@@ -2352,5 +2935,41 @@
<button class="ai-edit-btn" onclick="submitAiEdit()">✨ 수정하기</button>
</div>
<script src="/static/js/editor.js"></script>
<!-- 템플릿 추가 모달 -->
<div class="template-modal" id="templateModal">
<div class="template-modal-content">
<div class="template-modal-header">
<div class="template-modal-title">📁 템플릿 추가</div>
<button class="template-modal-close" onclick="closeTemplateModal()"></button>
</div>
<div class="template-input-group">
<label class="template-input-label">템플릿 이름</label>
<input type="text" class="template-name-input" id="templateNameInput"
placeholder="예: 걸포4지구 교통영향평가">
</div>
<div class="template-input-group">
<label class="template-input-label">템플릿 파일</label>
<div class="template-dropzone" id="templateDropzone" onclick="document.getElementById('templateFileInput').click()">
<div class="template-dropzone-icon">📄</div>
<div class="template-dropzone-text">파일을 드래그하거나 클릭하여 선택</div>
<div class="template-dropzone-hint">HWPX, HWP, PDF 지원</div>
</div>
<input type="file" id="templateFileInput" accept=".hwpx,.hwp,.pdf" style="display:none" onchange="handleTemplateFile(this)">
<div class="template-dropzone-file" id="templateFileInfo">
<span class="filename" id="templateFileName"></span>
<span class="remove" onclick="removeTemplateFile()"></span>
</div>
</div>
<button class="template-submit-btn" id="templateSubmitBtn" onclick="submitTemplate()" disabled>
<span class="spinner" id="templateSpinner"></span>
<span id="templateSubmitText">✨ 분석 및 추가</span>
</button>
</div>
</div>
</body>
</html>