Files
test/templates/index.html

3766 lines
133 KiB
HTML

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>글벗 - AI 문서 자동화 시스템</title>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+KR:wght@300;400;500;700;900&display=swap" rel="stylesheet">
<style>
:root {
--ui-bg: #1a1d21;
--ui-nav: #12151a;
--ui-panel: #1e2228;
--ui-hover: #282d35;
--ui-border: #2d333b;
--ui-text: #e6edf3;
--ui-dim: #8b949e;
--ui-accent: #00C853;
--ui-warning: #FF9800;
--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;
}
/* 템플릿 리스트 */
.template-list {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 10px;
}
.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;
}
.template-item:hover {
background: var(--ui-hover);
border-color: var(--ui-dim);
}
.template-item.selected {
border-color: var(--ui-accent);
background: rgba(0, 200, 83, 0.1);
}
.template-item input[type="radio"] {
accent-color: var(--ui-accent);
}
.template-item .label {
flex: 1;
font-size: 13px;
font-weight: 500;
}
.template-item .delete-btn {
opacity: 0;
background: transparent;
border: none;
color: var(--ui-dim);
cursor: pointer;
font-size: 14px;
}
.template-item:hover .delete-btn {
opacity: 1;
}
.template-item .delete-btn:hover {
color: var(--ui-error);
}
/* 템플릿 요소 체크박스 */
.template-elements {
margin-top: 12px;
padding: 12px;
background: var(--ui-bg);
border: 1px solid var(--ui-border);
border-radius: 6px;
}
.elements-title {
font-size: 10px;
font-weight: 600;
color: var(--ui-dim);
margin-bottom: 8px;
text-transform: uppercase;
}
.elements-list {
display: flex;
flex-direction: column;
gap: 6px;
}
.element-checkbox {
display: flex;
align-items: center;
gap: 8px;
font-size: 12px;
color: var(--ui-text);
cursor: pointer;
}
.element-checkbox input[type="checkbox"] {
accent-color: var(--ui-accent);
}
.element-checkbox .element-icon {
font-size: 14px;
}
/* 사용자 템플릿 프리뷰 */
.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; }
body {
font-family: 'Noto Sans KR', sans-serif;
background: var(--ui-bg);
color: var(--ui-text);
height: 100vh;
display: flex;
flex-direction: column;
overflow: hidden;
}
/* ===== 상단 툴바 ===== */
.toolbar {
height: 50px;
background: var(--ui-panel);
border-bottom: 1px solid var(--ui-border);
display: flex;
align-items: center;
padding: 0 15px;
gap: 8px;
}
.toolbar-logo {
display: flex;
align-items: center;
gap: 8px;
font-weight: 900;
color: var(--ui-accent);
font-size: 18px;
}
.toolbar-spacer { flex: 1; }
.toolbar-divider {
width: 1px;
height: 24px;
background: var(--ui-border);
margin: 0 8px;
}
.toolbar-btn {
padding: 7px 14px;
border-radius: 5px;
border: 1px solid var(--ui-border);
background: var(--ui-hover);
color: var(--ui-text);
font-size: 12px;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
gap: 6px;
transition: all 0.15s;
}
.toolbar-btn:hover {
background: var(--ui-panel);
border-color: var(--ui-dim);
}
.toolbar-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.toolbar-btn.active {
background: rgba(0, 200, 83, 0.2);
border-color: var(--ui-accent);
color: var(--ui-accent);
}
.zoom-select {
padding: 6px 10px;
border-radius: 5px;
border: 1px solid var(--ui-border);
background: var(--ui-hover);
color: var(--ui-text);
font-size: 12px;
cursor: pointer;
}
/* ===== 메인 컨테이너 ===== */
.app {
display: flex;
flex: 1;
overflow: hidden;
}
/* ===== 좌측 사이드바 ===== */
.sidebar {
width: 280px;
background: var(--ui-nav);
border-right: 1px solid var(--ui-border);
display: flex;
flex-direction: column;
}
.sidebar-header {
padding: 15px;
border-bottom: 1px solid var(--ui-border);
background: var(--ui-panel);
}
.sidebar-title {
font-size: 11px;
font-weight: 700;
color: var(--ui-dim);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 10px;
}
.sidebar-btn {
width: 100%;
padding: 10px 12px;
border-radius: 6px;
border: 1px solid var(--ui-border);
background: var(--ui-hover);
color: var(--ui-text);
font-size: 13px;
font-weight: 500;
cursor: pointer;
display: flex;
align-items: center;
gap: 10px;
transition: all 0.15s;
text-align: left;
margin-bottom: 8px;
}
.sidebar-btn:hover {
background: var(--ui-panel);
border-color: var(--ui-dim);
}
.sidebar-btn .icon { font-size: 16px; }
.sidebar-content {
flex: 1;
overflow-y: auto;
padding: 15px;
display: flex;
flex-direction: column;
gap: 15px;
}
.section-title {
font-size: 11px;
font-weight: 700;
color: var(--ui-dim);
text-transform: uppercase;
letter-spacing: 0.5px;
margin-bottom: 8px;
}
/* 참고 파일 확인 */
.file-check-box {
background: var(--ui-panel);
border: 1px solid var(--ui-border);
border-radius: 8px;
padding: 12px;
}
.file-check-row {
display: flex;
justify-content: space-between;
align-items: center;
font-size: 12px;
padding: 5px 0;
}
.file-check-label { color: var(--ui-dim); }
.file-check-value { font-weight: 600; }
.file-check-value.ok { color: var(--ui-accent); }
.file-check-value.warn { color: var(--ui-warning); cursor: pointer; }
.file-check-value.warn:hover { text-decoration: underline; }
.file-path {
font-size: 11px;
color: var(--ui-info);
word-break: break-all;
padding: 6px 0;
border-bottom: 1px solid var(--ui-border);
margin-bottom: 5px;
}
.file-path.empty { color: var(--ui-dim); font-style: italic; }
/* 미확인 파일 펼침 */
.unknown-files {
background: var(--ui-bg);
border: 1px solid var(--ui-border);
border-radius: 6px;
margin-top: 8px;
padding: 10px;
display: none;
}
.unknown-files.show { display: block; }
.unknown-file-item {
font-size: 11px;
color: var(--ui-warning);
padding: 3px 0;
}
.open-folder-btn {
margin-top: 8px;
padding: 6px 10px;
border-radius: 4px;
border: 1px solid var(--ui-border);
background: var(--ui-hover);
color: var(--ui-dim);
font-size: 11px;
cursor: pointer;
width: 100%;
}
.open-folder-btn:hover {
border-color: var(--ui-accent);
color: var(--ui-accent);
}
/* 작성 방식 선택 (가로 탭) */
.write-mode-box {
background: var(--ui-panel);
border: 1px solid var(--ui-border);
border-radius: 8px;
padding: 10px;
}
.write-mode-tabs {
display: flex;
gap: 4px;
}
.write-mode-tab {
flex: 1;
padding: 10px 8px;
border: 1px solid var(--ui-border);
border-radius: 6px;
background: transparent;
cursor: pointer;
transition: all 0.15s;
text-align: center;
}
.write-mode-tab:hover {
background: var(--ui-hover);
border-color: var(--ui-dim);
}
.write-mode-tab.selected {
background: rgba(0, 200, 83, 0.15);
border-color: var(--ui-accent);
}
.write-mode-tab input[type="radio"] {
display: none;
}
.write-mode-icon {
font-size: 16px;
margin-bottom: 4px;
}
.write-mode-label {
font-size: 11px;
font-weight: 600;
color: var(--ui-text);
}
.write-mode-tab.selected .write-mode-label {
color: var(--ui-accent);
}
.write-mode-notice {
margin-top: 8px;
padding: 6px 8px;
background: rgba(255, 152, 0, 0.1);
border-radius: 4px;
font-size: 10px;
color: var(--ui-warning);
display: flex;
align-items: center;
gap: 5px;
}
/* 진행 상태 */
.step-list {
display: flex;
flex-direction: column;
gap: 2px;
}
.step-item {
font-size: 11px;
color: var(--ui-dim);
padding: 6px 8px;
display: flex;
align-items: center;
gap: 8px;
border-radius: 4px;
transition: all 0.15s;
}
.step-item .status {
width: 16px;
text-align: center;
font-size: 12px;
}
.step-item.pending { color: #555; }
.step-item.running {
color: var(--ui-info);
background: rgba(88, 166, 255, 0.1);
}
.step-item.running .status { animation: pulse 1s infinite; }
.step-item.done { color: var(--ui-accent); }
.step-item.error { color: var(--ui-error); }
.step-divider {
height: 1px;
background: var(--ui-border);
margin: 6px 0;
}
@keyframes pulse {
0%, 100% { opacity: 1; }
50% { opacity: 0.4; }
}
/* 좌측 생성 버튼 */
.sidebar-generate-btn {
width: 100%;
padding: 14px;
border-radius: 8px;
border: none;
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;
transition: all 0.2s;
}
.sidebar-generate-btn:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 15px rgba(0, 200, 83, 0.3);
}
.sidebar-generate-btn:disabled {
background: var(--ui-border);
color: var(--ui-dim);
cursor: not-allowed;
transform: none;
box-shadow: none;
}
/* ===== 가운데 뷰어 ===== */
.main {
flex: 1;
display: flex;
flex-direction: column;
min-width: 0;
position: relative;
}
/* 서식 바 */
.format-bar {
height: 44px;
background: #252a30;
border-bottom: 1px solid var(--ui-border);
display: none;
align-items: center;
padding: 0 12px;
gap: 4px;
}
.format-bar.active { display: flex; }
.format-btn {
width: 32px; height: 32px; border-radius: 4px; border: none;
background: transparent; color: var(--ui-text); cursor: pointer;
font-size: 14px; display: flex; align-items: center; justify-content: center;
transition: all 0.15s;
}
.format-btn:hover { background: var(--ui-hover); }
.format-btn.active { background: rgba(0, 200, 83, 0.3); color: var(--ui-accent); }
.format-select {
padding: 4px 8px; border-radius: 4px; border: 1px solid var(--ui-border);
background: var(--ui-hover); color: var(--ui-text); font-size: 12px;
cursor: pointer; min-width: 80px;
}
.format-divider { width: 1px; height: 24px; background: var(--ui-border); margin: 0 6px; }
.viewer {
flex: 1;
background: #525659;
overflow: auto;
display: flex;
justify-content: center;
padding: 30px;
}
.a4-wrapper {
transform-origin: top center;
}
.a4-preview {
width: 210mm;
min-height: 297mm;
background: white;
box-shadow: 0 0 20px rgba(0,0,0,0.3);
color: #333;
position: relative;
}
.preview-iframe {
width: 100%;
min-height: 297mm;
border: none;
background: white;
display: none;
}
.preview-iframe.active {
display: block;
}
.placeholder {
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
height: 297mm;
color: #888;
text-align: center;
padding: 40px;
}
.placeholder .icon {
font-size: 72px;
margin-bottom: 25px;
opacity: 0.4;
}
.placeholder .text {
font-size: 18px;
color: #666;
margin-bottom: 10px;
}
.placeholder .sub-text {
font-size: 14px;
color: #999;
}
/* 하단 피드백 바 */
.feedback-bar {
background: var(--ui-panel);
border-top: 1px solid var(--ui-border);
padding: 12px 20px;
display: none;
align-items: center;
gap: 12px;
}
/* 목차 확인 액션바 */
.toc-action-bar {
background: var(--ui-panel);
border-top: 1px solid var(--ui-border);
padding: 12px 20px;
display: none;
align-items: center;
justify-content: flex-end;
gap: 12px;
}
.toc-action-bar.show { display: flex; }
.toc-action-btn {
padding: 10px 24px;
border-radius: 6px;
border: none;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
}
.toc-action-btn.primary {
background: var(--ui-accent);
color: #003300;
}
.toc-action-btn.primary:hover {
background: #00e676;
}
.toc-action-btn.secondary {
background: var(--ui-hover);
color: var(--ui-text);
border: 1px solid var(--ui-border);
}
.toc-action-btn.secondary:hover {
border-color: var(--ui-accent);
color: var(--ui-accent);
}
.feedback-bar.show { display: flex; }
.feedback-input {
flex: 1;
padding: 10px 14px;
border-radius: 6px;
border: 1px solid var(--ui-border);
background: var(--ui-bg);
color: var(--ui-text);
font-size: 13px;
}
.feedback-input:focus {
outline: none;
border-color: var(--ui-accent);
}
.feedback-btn {
padding: 10px 20px;
border-radius: 6px;
border: none;
font-size: 13px;
font-weight: 600;
cursor: pointer;
transition: all 0.15s;
}
.feedback-btn.primary {
background: var(--ui-accent);
color: #003300;
}
.feedback-btn.primary:hover {
background: #00e676;
}
.feedback-btn.secondary {
background: var(--ui-hover);
color: var(--ui-text);
border: 1px solid var(--ui-border);
}
/* ===== 우측 패널 ===== */
.right-panel {
width: 280px;
background: var(--ui-nav);
border-left: 1px solid var(--ui-border);
display: flex;
flex-direction: column;
}
.panel-header {
padding: 15px;
border-bottom: 1px solid var(--ui-border);
background: var(--ui-panel);
}
.panel-title {
font-size: 11px;
font-weight: 700;
color: var(--ui-dim);
text-transform: uppercase;
letter-spacing: 0.5px;
}
.panel-body {
flex: 1;
overflow-y: auto;
padding: 15px;
}
/* 문서 유형 선택 - 심플 리스트 */
.doc-type-list {
display: flex;
flex-direction: column;
gap: 4px;
margin-bottom: 10px;
}
.doc-type-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;
}
.doc-type-item:hover:not(.disabled) {
background: var(--ui-hover);
border-color: var(--ui-dim);
}
.doc-type-item.selected {
border-color: var(--ui-accent);
background: rgba(0, 200, 83, 0.1);
}
.doc-type-item.disabled {
opacity: 0.5;
cursor: not-allowed;
}
.doc-type-item input[type="radio"] {
accent-color: var(--ui-accent);
}
.doc-type-item .label {
flex: 1;
font-size: 13px;
font-weight: 500;
}
.doc-type-item .badge {
font-size: 9px;
padding: 2px 6px;
border-radius: 4px;
background: var(--ui-warning);
color: #000;
font-weight: 600;
}
.doc-type-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;
}
.doc-type-item:hover .delete-btn {
opacity: 1;
}
.doc-type-item .delete-btn:hover {
background: var(--ui-error);
color: white;
}
/* 플로팅 프리뷰 팝업 - fixed로 변경 */
.doc-type-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;
}
.doc-type-preview::after {
content: '';
position: absolute;
right: -8px;
top: 50%;
transform: translateY(-50%);
border: 8px solid transparent;
border-left-color: var(--ui-panel);
}
.doc-type-preview.show {
display: block;
}
/* 프리뷰 썸네일 */
.preview-thumbnail {
width: 100%;
height: 120px;
background: #ffffff;
border-radius: 8px;
display: flex;
justify-content: center;
align-items: center;
gap: 8px;
margin-bottom: 12px;
border: 1px solid var(--ui-border);
overflow: hidden;
padding: 10px;
}
/* 기획서 프리뷰 - 2페이지 */
.preview-thumbnail.briefing .page {
width: 45px;
height: 64px;
border: 1px solid #ccc;
border-radius: 2px;
padding: 3px;
background: white;
box-shadow: 0 1px 3px rgba(0,0,0,0.1);
}
.preview-thumbnail.briefing .page-header {
height: 4px;
background: #1a365d;
margin-bottom: 3px;
border-radius: 1px;
}
.preview-thumbnail.briefing .page-title {
height: 5px;
background: #1a365d;
margin-bottom: 3px;
border-radius: 1px;
}
.preview-thumbnail.briefing .page-divider {
height: 2px;
background: linear-gradient(90deg, #1a365d, #2c5282);
margin-bottom: 3px;
}
.preview-thumbnail.briefing .page-lead {
height: 8px;
background: #f7fafc;
border-left: 2px solid #1a365d;
margin-bottom: 3px;
}
.preview-thumbnail.briefing .page-body {
height: 4px;
background: #e2e8f0;
margin-bottom: 2px;
border-radius: 1px;
}
.preview-thumbnail.briefing .page-bottom {
height: 6px;
background: linear-gradient(90deg, #1a365d 22%, #f0f0f0 22%);
margin-top: auto;
border-radius: 1px;
}
.preview-thumbnail.briefing .page-attach {
font-size: 4px;
color: #666;
text-align: center;
margin-bottom: 2px;
}
/* 보고서 프리뷰 */
.preview-thumbnail.report {
flex-direction: column;
justify-content: flex-start;
padding: 15px;
}
.preview-thumbnail.report .line {
width: 100%;
height: 3px;
background: #ddd;
margin-bottom: 4px;
border-radius: 1px;
}
.preview-thumbnail.report .line.h1 {
background: #333;
height: 5px;
width: 50%;
margin-bottom: 8px;
}
.preview-thumbnail.report .line.h2 {
background: #555;
height: 4px;
width: 40%;
margin-top: 6px;
margin-bottom: 6px;
}
.preview-thumbnail.report .line.body {
background: #ccc;
width: 95%;
}
/* PPT 프리뷰 */
.preview-thumbnail.ppt {
background: #2d3748;
flex-direction: row;
gap: 6px;
padding: 15px;
}
.preview-thumbnail.ppt .slide {
width: 55px;
height: 40px;
background: #1a365d;
border-radius: 3px;
display: flex;
flex-direction: column;
justify-content: center;
align-items: center;
padding: 4px;
}
.preview-thumbnail.ppt .slide-title {
font-size: 5px;
color: white;
font-weight: bold;
margin-bottom: 3px;
}
.preview-thumbnail.ppt .slide-body {
width: 80%;
height: 2px;
background: rgba(255,255,255,0.3);
margin-bottom: 2px;
}
/* custom 프리뷰 */
.preview-thumbnail.custom {
flex-direction: column;
justify-content: flex-start;
padding: 15px;
}
.preview-thumbnail.custom .line {
width: 100%;
height: 3px;
background: #ddd;
margin-bottom: 4px;
border-radius: 1px;
}
.preview-thumbnail.custom .line.h1 {
background: #1a365d;
height: 5px;
width: 60%;
margin-bottom: 8px;
}
.preview-thumbnail.custom .line.h2 {
background: #2c5282;
height: 4px;
width: 45%;
margin-top: 6px;
margin-bottom: 6px;
}
.preview-thumbnail.custom .line.body {
background: #cbd5e0;
width: 90%;
}
/* 프리뷰 정보 */
.preview-title {
font-size: 14px;
font-weight: 700;
color: var(--ui-text);
margin-bottom: 4px;
}
.preview-desc {
font-size: 11px;
color: var(--ui-dim);
margin-bottom: 12px;
}
.preview-features {
display: flex;
flex-direction: column;
gap: 5px;
}
.preview-feature {
display: flex;
align-items: center;
gap: 8px;
font-size: 11px;
color: var(--ui-dim);
}
.preview-feature .icon {
font-size: 12px;
}
/* 템플릿 추가 버튼 */
.add-template-btn {
width: 100%;
padding: 12px;
border-radius: 8px;
border: 1px dashed var(--ui-border);
background: transparent;
color: var(--ui-dim);
font-size: 13px;
cursor: pointer;
transition: all 0.15s;
margin-top: 10px;
}
.add-template-btn:hover {
border-color: var(--ui-accent);
color: var(--ui-accent);
background: rgba(0, 200, 83, 0.05);
}
/* 옵션 섹션 */
.option-section {
margin-bottom: 20px;
}
.option-title {
font-size: 11px;
font-weight: 700;
color: var(--ui-dim);
margin-bottom: 10px;
}
.option-group {
display: flex;
flex-direction: column;
gap: 6px;
}
.option-item {
display: flex;
align-items: center;
gap: 10px;
padding: 8px 12px;
background: var(--ui-panel);
border: 1px solid var(--ui-border);
border-radius: 6px;
cursor: pointer;
transition: all 0.15s;
}
.option-item:hover {
background: var(--ui-hover);
}
.option-item.selected {
border-color: var(--ui-accent);
background: rgba(0, 200, 83, 0.1);
}
.option-item input[type="radio"],
.option-item input[type="checkbox"] {
accent-color: var(--ui-accent);
}
.option-item label {
font-size: 12px;
cursor: pointer;
flex: 1;
}
/* 페이지 수 입력 필드 */
.page-input {
width: 45px;
padding: 4px 6px;
border: 1px solid var(--ui-border);
border-radius: 4px;
background: var(--ui-bg);
color: var(--ui-text);
font-size: 12px;
text-align: center;
margin-left: 4px;
}
.page-input:focus {
outline: none;
border-color: var(--ui-accent);
}
.page-input:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.page-input-suffix {
font-size: 12px;
color: var(--ui-dim);
margin-left: 2px;
}
/* 스피너 숨기기 (Chrome) */
.page-input::-webkit-inner-spin-button,
.page-input::-webkit-outer-spin-button {
-webkit-appearance: none;
margin: 0;
}
/* 요청사항 */
.request-textarea {
width: 100%;
height: 100px;
padding: 12px;
border-radius: 6px;
border: 1px solid var(--ui-border);
background: var(--ui-panel);
color: var(--ui-text);
font-size: 12px;
resize: vertical;
font-family: inherit;
}
.request-textarea:focus {
outline: none;
border-color: var(--ui-accent);
}
.request-textarea::placeholder {
color: var(--ui-dim);
}
/* 생성 버튼 */
.generate-btn {
width: 100%;
padding: 14px;
border-radius: 8px;
border: none;
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;
transition: all 0.2s;
margin-top: 20px;
}
.generate-btn:hover:not(:disabled) {
transform: translateY(-1px);
box-shadow: 0 4px 15px rgba(0, 200, 83, 0.3);
}
.generate-btn:disabled {
background: var(--ui-border);
color: var(--ui-dim);
cursor: not-allowed;
transform: none;
box-shadow: none;
}
/* ===== 상태바 ===== */
.status-bar {
height: 28px;
background: var(--ui-nav);
border-top: 1px solid var(--ui-border);
padding: 0 15px;
display: flex;
align-items: center;
justify-content: space-between;
font-size: 11px;
color: var(--ui-dim);
}
.status-left {
display: flex;
align-items: center;
gap: 10px;
}
.status-dot {
width: 8px;
height: 8px;
border-radius: 50%;
background: #555;
}
.status-dot.connected {
background: var(--ui-accent);
box-shadow: 0 0 8px var(--ui-accent);
}
/* ===== 모달 ===== */
.modal-overlay {
position: fixed;
inset: 0;
background: rgba(0,0,0,0.75);
display: none;
align-items: center;
justify-content: center;
z-index: 1000;
}
.modal-overlay.active { display: flex; }
.modal {
background: var(--ui-panel);
border: 1px solid var(--ui-border);
border-radius: 12px;
padding: 20px;
min-width: 450px;
max-width: 550px;
box-shadow: 0 15px 50px rgba(0,0,0,0.5);
}
.modal-header {
font-size: 16px;
font-weight: 700;
color: var(--ui-accent);
margin-bottom: 20px;
display: flex;
align-items: center;
gap: 10px;
}
.modal-body { margin-bottom: 20px; }
.modal-textarea {
width: 100%;
height: 220px;
padding: 12px 14px;
border-radius: 6px;
border: 1px solid var(--ui-border);
background: var(--ui-bg);
color: var(--ui-text);
font-size: 12px;
font-family: 'Consolas', 'Monaco', monospace;
resize: vertical;
}
.modal-textarea:focus {
outline: none;
border-color: var(--ui-accent);
}
.modal-footer {
display: flex;
justify-content: flex-end;
gap: 10px;
}
.modal-btn {
padding: 10px 20px;
border-radius: 6px;
border: 1px solid var(--ui-border);
background: var(--ui-hover);
color: var(--ui-text);
font-size: 13px;
cursor: pointer;
transition: all 0.15s;
}
.modal-btn:hover { background: var(--ui-panel); }
.modal-btn.primary {
background: var(--ui-accent);
border-color: var(--ui-accent);
color: #003300;
font-weight: 700;
}
.modal-btn.primary:hover { background: #00e676; }
/* 스크롤바 */
::-webkit-scrollbar { width: 8px; height: 8px; }
::-webkit-scrollbar-track { background: var(--ui-bg); }
::-webkit-scrollbar-thumb { background: #444; border-radius: 4px; }
::-webkit-scrollbar-thumb:hover { background: #555; }
/* 로딩 스피너 */
.loading-spinner {
border: 2px solid transparent;
border-top: 2px solid currentColor;
border-radius: 50%;
width: 16px;
height: 16px;
animation: spin 0.8s linear infinite;
}
@keyframes spin {
to { transform: rotate(360deg); }
}
/* AI 수정 플로팅 박스 */
.ai-edit-popup {
display: none;
position: fixed;
top: 80px;
right: 300px;
width: 320px;
background: var(--ui-panel);
border: 1px solid var(--ui-accent);
border-radius: 12px;
padding: 15px;
box-shadow: 0 10px 40px rgba(0, 200, 83, 0.2);
z-index: 500;
}
.ai-edit-popup.show { display: block; }
.ai-edit-header {
display: flex;
align-items: center;
gap: 8px;
font-size: 14px;
font-weight: 700;
color: var(--ui-accent);
margin-bottom: 12px;
}
.ai-edit-selected {
font-size: 11px;
color: var(--ui-dim);
margin-bottom: 8px;
}
.ai-edit-selected-text {
background: var(--ui-bg);
border: 1px solid var(--ui-border);
border-radius: 6px;
padding: 8px;
font-size: 12px;
color: var(--ui-text);
max-height: 80px;
overflow-y: auto;
margin-bottom: 12px;
}
.ai-edit-input {
width: 100%;
padding: 10px;
border-radius: 6px;
border: 1px solid var(--ui-border);
background: var(--ui-bg);
color: var(--ui-text);
font-size: 12px;
margin-bottom: 12px;
resize: none;
}
.ai-edit-btn {
width: 100%;
padding: 10px;
border-radius: 6px;
border: none;
background: var(--ui-accent);
color: #003300;
font-size: 13px;
font-weight: 700;
cursor: pointer;
}
.ai-edit-close {
position: absolute;
top: 10px;
right: 10px;
background: none;
border: none;
color: var(--ui-dim);
cursor: pointer;
font-size: 16px;
}
/* 문서 유형별 옵션 컨테이너 */
#docTypeOptionsContainer {
margin-bottom: 20px;
}
/* 사용자 문서 유형 구분선 */
.doc-type-divider {
height: 1px;
background: var(--ui-border);
margin: 10px 0;
position: relative;
}
.doc-type-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;
}
/* 로딩 상태 */
.doc-type-list.loading {
min-height: 100px;
display: flex;
align-items: center;
justify-content: center;
}
.doc-type-list.loading::after {
content: '로딩 중...';
color: var(--ui-dim);
font-size: 12px;
}
.analysis-progress {
padding: 20px 0;
}
.analysis-step {
display: flex;
align-items: center;
padding: 8px 12px;
margin-bottom: 4px;
border-radius: 6px;
background: var(--ui-bg);
}
.analysis-step.running {
background: var(--accent-light);
}
.analysis-step.done {
opacity: 0.7;
}
.analysis-step .step-icon {
width: 24px;
text-align: center;
}
.analysis-step .step-name {
flex: 1;
margin-left: 8px;
}
.analysis-step .step-status {
font-size: 12px;
color: var(--ui-text-muted);
}
.progress-bar-container {
height: 8px;
background: var(--ui-border);
border-radius: 4px;
margin-top: 16px;
overflow: hidden;
}
.progress-bar {
height: 100%;
background: var(--accent-primary);
transition: width 0.3s ease;
}
/* 분석 결과 UI */
.analysis-result h4 {
margin-bottom: 16px;
}
.result-summary {
display: grid;
grid-template-columns: repeat(3, 1fr);
gap: 12px;
margin-bottom: 20px;
}
.summary-item {
padding: 12px;
background: var(--ui-bg);
border-radius: 6px;
text-align: center;
}
.toc-list {
list-style: none;
padding: 0;
}
.toc-list li {
padding: 6px 12px;
border-left: 2px solid var(--ui-border);
margin-bottom: 2px;
}
.toc-level-2 { padding-left: 28px; }
.toc-level-3 { padding-left: 44px; }
.step-icon.spinning {
display: inline-block;
animation: spin 1s linear infinite;
}
.analysis-step .step-status {
font-size: 10px;
color: var(--ui-dim);
margin-left: auto;
max-width: 150px;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
</style>
</head>
<body>
<!-- 상단 툴바 -->
<div class="toolbar">
<div class="toolbar-logo">📝 글벗</div>
<div class="toolbar-spacer"></div>
<button class="toolbar-btn" id="editModeBtn" onclick="toggleEditMode()">✏️ 편집하기</button>
<div class="toolbar-divider"></div>
<select class="zoom-select" id="zoomSelect" onchange="setZoom(this.value)">
<option value="50">50%</option>
<option value="75">75%</option>
<option value="100" selected>100%</option>
<option value="125">125%</option>
<option value="150">150%</option>
</select>
<div class="toolbar-divider"></div>
<button class="toolbar-btn" onclick="exportHwp()">📄 HWP 추출</button>
<button class="toolbar-btn" onclick="saveHtml()">💾 HTML 저장</button>
<button class="toolbar-btn" disabled title="준비중">📊 PPT 저장</button>
<button class="toolbar-btn" onclick="printDoc()">🖨️ PDF/인쇄</button>
</div>
<!-- 메인 앱 -->
<div class="app">
<!-- 좌측 사이드바: 입력 -->
<div class="sidebar">
<div class="sidebar-header">
<div class="sidebar-title">문서 업로드</div>
<button class="sidebar-btn" onclick="openFolderModal()">
<span class="icon">📁</span>
<span>폴더 위치</span>
</button>
<button class="sidebar-btn" onclick="openLinkModal()">
<span class="icon">🔗</span>
<span>외부 링크</span>
</button>
<button class="sidebar-btn" onclick="openHtmlModal()">
<span class="icon">📋</span>
<span>HTML 붙여넣기</span>
</button>
</div>
<div class="sidebar-content">
<!-- 참고 파일 확인 -->
<div>
<div class="section-title">업로드 파일 검토</div>
<div class="file-check-box">
<div class="file-path empty" id="folderPathDisplay">폴더 경로가 설정되지 않음</div>
<div class="file-check-row">
<span class="file-check-label">전체 파일</span>
<span class="file-check-value" id="totalCount">0개</span>
</div>
<div class="file-check-row">
<span class="file-check-label">확인 (변환 가능)</span>
<span class="file-check-value ok" id="okCount">0개 ✓</span>
</div>
<div class="file-check-row">
<span class="file-check-label">미확인</span>
<span class="file-check-value warn" id="unknownCount" onclick="toggleUnknownFiles()">0개</span>
</div>
<div class="unknown-files" id="unknownFilesBox">
<div id="unknownFilesList"></div>
<button class="open-folder-btn" onclick="openFolder()">📂 폴더 열기</button>
</div>
<div class="file-check-row" style="border-top: 1px solid var(--ui-border); margin-top: 8px; padding-top: 8px;">
<span class="file-check-label">참고 링크</span>
<span class="file-check-value" id="linkCount">0개</span>
</div>
<div class="file-check-row">
<span class="file-check-label">HTML 입력</span>
<span class="file-check-value" id="htmlInputStatus">없음</span>
</div>
</div>
</div>
<!-- 작성 방식 -->
<div>
<div class="section-title">업로드 자료 활용 범위</div>
<div class="write-mode-box">
<div class="write-mode-tabs">
<label class="write-mode-tab" onclick="selectWriteMode('format')">
<input type="radio" name="writeMode" value="format">
<div class="write-mode-icon">📄</div>
<div class="write-mode-label">형식만 <br>변경</div>
</label>
<label class="write-mode-tab selected" onclick="selectWriteMode('restructure')">
<input type="radio" name="writeMode" value="restructure" checked>
<div class="write-mode-icon">🔄</div>
<div class="write-mode-label">내용의<br>재구성</div>
</label>
<label class="write-mode-tab" onclick="selectWriteMode('new')">
<input type="radio" name="writeMode" value="new">
<div class="write-mode-icon"></div>
<div class="write-mode-label">문서 참고 <br> 신규 작성</div>
</label>
</div>
<div class="write-mode-notice">
<span>⚠️</span>
<span>문서 유형에 적합하지 않은 콘텐츠 업로드시,<br> 자동 재구성 및 신규 콘텐츠 작성 </span>
</div>
</div>
</div>
<!-- 진행 상태 -->
<div>
<div class="section-title">진행 상태</div>
<div class="step-list" id="stepList">
<div class="step-item pending" data-step="0">
<span class="status"></span>
<span>참고 파일 확인</span>
</div>
<div class="step-divider"></div>
<div class="step-item pending" data-step="1">
<span class="status"></span>
<span>Step 1: 파일 변환</span>
</div>
<div class="step-item pending" data-step="2">
<span class="status"></span>
<span>Step 2: 텍스트 추출</span>
</div>
<div class="step-item pending" data-step="3">
<span class="status"></span>
<span>Step 3: 도메인 분석</span>
</div>
<div class="step-item pending" data-step="4">
<span class="status"></span>
<span>Step 4: 청킹/요약</span>
</div>
<div class="step-item pending" data-step="5">
<span class="status"></span>
<span>Step 5: RAG 구축</span>
</div>
<div class="step-item pending" data-step="6">
<span class="status"></span>
<span>Step 6: Corpus 생성</span>
</div>
<div class="step-item pending" data-step="7">
<span class="status"></span>
<span>Step 7: 목차 구성</span>
</div>
<div class="step-divider"></div>
<div class="step-item pending" data-step="8">
<span class="status"></span>
<span>Step 8: 콘텐츠 생성</span>
</div>
<div class="step-item pending" data-step="9">
<span class="status"></span>
<span>Step 9: HTML 변환</span>
</div>
</div>
</div>
</div>
</div>
<!-- 가운데 뷰어 -->
<div class="main">
<div class="viewer" id="viewer">
<div class="a4-wrapper" id="a4Wrapper">
<div class="a4-preview" id="a4Preview">
<iframe id="previewFrame" class="preview-iframe"></iframe>
<div class="placeholder" id="placeholder">
<div class="icon">📄</div>
<div class="text">HTML을 입력하고 생성하세요</div>
<div class="sub-text">좌측에서 HTML 붙여넣기 또는 파일 업로드</div>
</div>
</div>
</div>
</div>
<!-- 목차 확인 액션바 -->
<div class="toc-action-bar" id="tocActionBar">
<button class="toc-action-btn secondary" onclick="editToc()">✏️ 편집</button>
<button class="toc-action-btn primary" id="approveBtn" onclick="approveToc()">✅ 승인 & 생성하기</button>
</div>
<!-- 하단 피드백 바 -->
<div class="feedback-bar" id="feedbackBar">
<input type="text" class="feedback-input" id="feedbackInput" placeholder="수정 요청사항을 입력하세요... (예: 5 Step을 첨부로 이동해줘)">
<button class="feedback-btn primary" id="feedbackBtn" onclick="submitFeedback()">
<span id="feedbackBtnText">🔄 수정 반영</span>
<span id="feedbackSpinner" class="spinner" style="display:none;"></span>
</button>
<button class="feedback-btn secondary" onclick="regenerate()">🗑️ 다시 생성</button>
</div>
</div>
<!-- 우측 패널: 옵션 -->
<div class="right-panel">
<div class="panel-header">
<span class="panel-title">문서 설정</span>
</div>
<div class="panel-body">
<!-- ===== 문서 유형 (동적 로드) ===== -->
<div class="option-section">
<div class="option-title">문서 유형</div>
<div class="doc-type-list loading" id="docTypeList">
<!-- API에서 동적으로 로드 -->
</div>
<!-- 문서 유형 추가 버튼 -->
<button class="add-template-btn" onclick="openDocTypeModal()">+ 문서 유형 추가</button>
</div>
<!-- ===== 문서 유형별 옵션 (동적) ===== -->
<div id="docTypeOptionsContainer">
<!-- selectDocType() 호출 시 동적으로 렌더링 -->
</div>
<!-- ===== 템플릿 선택 ===== -->
<div class="option-section">
<div class="option-title">템플릿</div>
<div class="template-list">
<div class="template-item selected" data-template="default" onclick="selectTemplate('default')">
<input type="radio" name="template" checked>
<span class="label">📄 기본 템플릿</span>
</div>
<div id="userTemplatesListNew"></div>
</div>
<button class="add-template-btn" onclick="openTemplateModal()">+ 템플릿 추가</button>
<div id="templateElementOptions" class="template-elements" style="display:none;">
<div class="elements-title">적용 요소</div>
<div class="elements-list"></div>
</div>
</div>
<!-- ===== 요청사항 ===== -->
<div class="option-section">
<div class="option-title">요청사항</div>
<textarea class="request-textarea" id="globalInstructionInput"
placeholder="예: 표를 더 상세하게&#10;예: 전문 용어 풀어서 설명"></textarea>
</div>
<!-- ===== 생성 버튼 ===== -->
<button class="generate-btn" id="generateBtn" onclick="generate()" disabled>
<span id="generateBtnText">🚀 생성하기</span>
<div class="loading-spinner" id="generateSpinner" style="display:none;"></div>
</button>
</div>
</div>
</div>
<!-- 상태바 -->
<div class="status-bar">
<div class="status-left">
<span class="status-dot" id="statusDot"></span>
<span id="statusMessage">준비됨</span>
</div>
<div id="statusRight">글벗 Light v2.1</div>
</div>
<!-- HTML 입력 모달 -->
<div class="modal-overlay" id="htmlModal">
<div class="modal">
<div class="modal-header">📋 HTML 붙여넣기</div>
<div class="modal-body">
<textarea class="modal-textarea" id="htmlContent" placeholder="HTML 코드를 붙여넣으세요..."></textarea>
</div>
<div class="modal-footer">
<button class="modal-btn" onclick="closeHtmlModal()">취소</button>
<button class="modal-btn primary" onclick="submitHtml()">확인</button>
</div>
</div>
</div>
<!-- 폴더 모달 -->
<div class="modal-overlay" id="folderModal">
<div class="modal">
<div class="modal-header">📁 폴더 위치 입력</div>
<div class="modal-body">
<input type="text" id="folderPath" class="modal-input" placeholder="C:\Users\...\Documents\참고자료" style="width:100%; padding:12px; border-radius:6px; border:1px solid var(--ui-border); background:var(--ui-bg); color:var(--ui-text); font-size:13px;">
<p style="margin-top:10px; font-size:11px; color:var(--ui-dim);">* 로컬 폴더 경로를 입력하세요. (Engine 실행 필요)</p>
</div>
<div class="modal-footer">
<button class="modal-btn" onclick="closeFolderModal()">취소</button>
<button class="modal-btn primary" onclick="submitFolder()">확인</button>
</div>
</div>
</div>
<!-- 참고 링크 모달 -->
<div class="modal-overlay" id="linkModal">
<div class="modal">
<div class="modal-header">🔗 참고 링크 입력</div>
<div class="modal-body">
<div id="linkInputList">
<input type="text" class="link-input" placeholder="https://..." style="width:100%; padding:10px; border-radius:6px; border:1px solid var(--ui-border); background:var(--ui-bg); color:var(--ui-text); font-size:12px; margin-bottom:8px;">
</div>
<button onclick="addLinkInput()" style="padding:8px 12px; border-radius:4px; border:1px dashed var(--ui-border); background:transparent; color:var(--ui-dim); font-size:12px; cursor:pointer; width:100%;">+ 링크 추가</button>
</div>
<div class="modal-footer">
<button class="modal-btn" onclick="closeLinkModal()">취소</button>
<button class="modal-btn primary" onclick="submitLinks()">확인</button>
</div>
</div>
</div>
<!-- AI 부분 수정 팝업 -->
<div class="ai-edit-popup" id="aiEditPopup">
<button class="ai-edit-close" onclick="closeAiEditPopup()"></button>
<div class="ai-edit-header">🤖 AI로 수정하기</div>
<div class="ai-edit-selected">선택된 텍스트:</div>
<div class="ai-edit-selected-text" id="aiEditSelectedText"></div>
<textarea class="ai-edit-input" id="aiEditInput" rows="3" placeholder="예: 한 줄로 요약해줘&#10;예: 표 형태로 만들어줘"></textarea>
<button class="ai-edit-btn" onclick="submitAiEdit()">✨ 수정하기</button>
</div>
<!-- 템플릿 추가 모달 -->
<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>
<!-- 문서 유형 추가 모달 -->
<div class="modal-overlay" id="addDocTypeModal">
<div class="modal" style="min-width: 500px; max-width: 550px;">
<div class="modal-header">
<span id="addDocTypeModalTitle">📄 문서 유형 추가</span>
</div>
<!-- Step 1: 입력 -->
<div id="docTypeStep1" class="modal-body">
<div style="margin-bottom: 16px;">
<label style="font-size: 12px; font-weight: 600; color: var(--ui-dim); display: block; margin-bottom: 8px;">문서 유형 이름</label>
<input type="text" id="newDocTypeName" placeholder="예: 제안서, 회의록..." style="width:100%; padding:10px; border-radius:6px; border:1px solid var(--ui-border); background:var(--ui-bg); color:var(--ui-text); font-size:13px;">
</div>
<div style="margin-bottom: 16px;">
<label style="font-size: 12px; font-weight: 600; color: var(--ui-dim); display: block; margin-bottom: 8px;">설명 (선택)</label>
<input type="text" id="newDocTypeDesc" placeholder="문서 유형에 대한 간단한 설명" style="width:100%; padding:10px; border-radius:6px; border:1px solid var(--ui-border); background:var(--ui-bg); color:var(--ui-text); font-size:13px;">
</div>
<div style="margin-bottom: 16px;">
<label style="font-size: 12px; font-weight: 600; color: var(--ui-dim); display: block; margin-bottom: 8px;">샘플 문서 (HWPX 권장)</label>
<input type="file" id="newDocTypeFile" accept=".hwpx,.hwp,.pdf" style="width:100%; padding:10px; border-radius:6px; border:1px solid var(--ui-border); background:var(--ui-bg); color:var(--ui-text); font-size:12px;">
<p style="margin-top:8px; font-size:11px; color:var(--ui-dim);">샘플 문서를 분석하여 스타일과 구조를 추출합니다</p>
</div>
</div>
<!-- Step 2: 분석 진행 -->
<div id="docTypeStep2" class="modal-body" style="display:none;">
<div class="analysis-progress">
<div id="analysisSteps"></div>
<div class="progress-bar-container">
<div id="analysisProgressBar" class="progress-bar" style="width: 0%"></div>
</div>
<p id="analysisProgressText" style="text-align: center; margin-top: 12px; font-size: 12px; color: var(--ui-dim);">준비 중...</p>
</div>
</div>
<!-- Step 3: 결과 확인 -->
<div id="docTypeStep3" class="modal-body" style="display:none;">
<div class="analysis-result">
<h4 style="margin-bottom: 16px; color: var(--ui-accent);">📋 분석 완료</h4>
<div id="analysisResultSummary" class="result-summary"></div>
<div id="analysisResultToc" class="result-toc" style="margin-top: 16px;"></div>
</div>
</div>
<div class="modal-footer" id="docTypeModalFooter">
<button class="modal-btn" onclick="closeAddDocTypeModal()">취소</button>
<button class="modal-btn primary" id="docTypeActionBtn" onclick="startDocTypeAnalysis()">분석 시작</button>
</div>
</div>
</div>
<script>
// ===== 상태 변수 =====
let inputContent = '';
let generatedHTML = '';
let currentDocType = 'briefing';
let currentWriteMode = 'restructure';
let currentZoom = 100;
let folderPath = '';
let referenceLinks = [];
let currentTemplate = 'default';
let templateElements = {};
let analysisResult = null;
// ===== 문서 유형 데이터 =====
let docTypes = []; // 전체 문서 유형 (default + user)
let selectedDocTypeFile = null;
// ===== 템플릿 관련 =====
let userTemplates = [];
let selectedTemplateFile = null;
// ===== AI 부분 수정 =====
let selectedText = '';
let selectedRange = null;
// ===== 썸네일 템플릿 =====
const thumbnailTemplates = {
briefing: `
<div class="page">
<div class="page-header"></div>
<div class="page-title"></div>
<div class="page-divider"></div>
<div class="page-lead"></div>
<div class="page-body"></div>
<div class="page-bottom"></div>
</div>
<div class="page">
<div class="page-header"></div>
<div class="page-attach">첨부</div>
<div class="page-body"></div>
<div class="page-body"></div>
<div class="page-bottom"></div>
</div>
`,
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>
`,
ppt: `
<div class="slide"><div class="slide-title">TITLE</div><div class="slide-body"></div></div>
<div class="slide"><div class="slide-title">CONTENT</div><div class="slide-body"></div></div>
`,
custom: `
<div class="line h1"></div>
<div class="line body"></div>
<div class="line h2"></div>
<div class="line body"></div>
`
};
// ===== 문서 유형 로드 =====
async function loadDocTypes() {
const container = document.getElementById('docTypeList');
container.classList.add('loading');
try {
const response = await fetch('/api/doc-types');
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
docTypes = data;
renderDocTypeList();
// 첫 번째 활성화된 유형 선택
const firstEnabled = docTypes.find(t => t.enabled);
if (firstEnabled) {
selectDocType(firstEnabled.id);
}
} catch (error) {
console.error('문서 유형 로드 실패:', error);
container.innerHTML = '<div style="color:var(--ui-error);font-size:12px;">로드 실패</div>';
} finally {
container.classList.remove('loading');
}
}
// ===== 문서 유형 리스트 렌더링 =====
function renderDocTypeList() {
const container = document.getElementById('docTypeList');
// 기본 유형과 사용자 유형 분리
const defaultTypes = docTypes.filter(t => t.isDefault);
const userTypes = docTypes.filter(t => !t.isDefault);
let html = defaultTypes.map(type => createDocTypeHTML(type)).join('');
if (userTypes.length > 0) {
html += '<div class="doc-type-divider"></div>';
html += userTypes.map(type => createDocTypeHTML(type)).join('');
}
container.innerHTML = html;
attachDocTypeEvents();
}
// ===== 개별 문서 유형 HTML 생성 =====
function createDocTypeHTML(type) {
const isSelected = currentDocType === type.id;
const isDisabled = !type.enabled;
return `
<div class="doc-type-item ${isSelected ? 'selected' : ''} ${isDisabled ? 'disabled' : ''}"
data-type="${type.id}">
<input type="radio" name="docType" ${isSelected ? 'checked' : ''} ${isDisabled ? 'disabled' : ''}>
<span class="label">${type.icon} ${type.name}</span>
${type.badge ? `<span class="badge">${type.badge}</span>` : ''}
${!type.isDefault ? `<button class="delete-btn" onclick="event.stopPropagation(); deleteDocType('${type.id}')" title="삭제">✕</button>` : ''}
<div class="doc-type-preview">
<div class="preview-thumbnail ${type.thumbnailType || 'custom'}">
${thumbnailTemplates[type.thumbnailType] || thumbnailTemplates.custom}
</div>
<div class="preview-title">${type.name}</div>
<div class="preview-desc">${type.description || ''}</div>
<div class="preview-features">
${(type.features || []).map(f => `
<div class="preview-feature">
<span class="icon">${f.icon || '✓'}</span> ${f.text || f}
</div>
`).join('')}
</div>
</div>
</div>
`;
}
// ===== 문서 유형 이벤트 연결 =====
function attachDocTypeEvents() {
document.querySelectorAll('.doc-type-item').forEach(item => {
if (item.classList.contains('disabled')) return;
// 클릭 이벤트
item.onclick = () => selectDocType(item.dataset.type);
// 호버 프리뷰 이벤트
const preview = item.querySelector('.doc-type-preview');
if (preview) {
item.addEventListener('mouseenter', () => {
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'));
}
});
}
// ===== 문서 유형 선택 =====
function selectDocType(typeId) {
const type = docTypes.find(t => t.id === typeId);
if (!type || !type.enabled) return;
currentDocType = typeId;
// 선택 상태 업데이트
document.querySelectorAll('.doc-type-item').forEach(item => {
const isSelected = item.dataset.type === typeId;
item.classList.toggle('selected', isSelected);
const radio = item.querySelector('input[type="radio"]');
if (radio) radio.checked = isSelected;
});
// 옵션 렌더링
renderDocTypeOptions(type);
// 버튼 텍스트 업데이트
const generateBtnText = document.getElementById('generateBtnText');
if (type.generateFlow === 'draft-first') {
generateBtnText.textContent = '📋 목차 확인하기';
} else {
generateBtnText.textContent = '🚀 생성하기';
}
console.log('문서 유형 선택:', typeId);
}
// ===== 문서 유형별 옵션 렌더링 =====
function renderDocTypeOptions(type) {
const container = document.getElementById('docTypeOptionsContainer');
if (!type.options) {
container.innerHTML = '';
return;
}
let html = '';
// 페이지 구성 (기획서 - 새로운 구조)
if (type.options.pageConfig) {
const config = type.options.pageConfig;
html += `
<div class="option-section">
<div class="option-title">페이지 구성</div>
<div class="option-group">
${config.choices.map((opt, idx) => `
<div class="option-item ${opt.default ? 'selected' : ''}" onclick="selectPageConfig('${opt.value}', ${idx})">
<input type="radio" name="pageConfig" value="${opt.value}" id="pageConfig${idx}" ${opt.default ? 'checked' : ''}>
<label for="pageConfig${idx}">${opt.label}</label>
${opt.hasInput ? `
<input type="number"
id="attachPages"
class="page-input"
value="${opt.inputDefault || 1}"
min="${opt.inputMin || 1}"
max="${opt.inputMax || 10}"
onclick="event.stopPropagation(); selectPageConfig('${opt.value}', ${idx});"
onchange="updateAttachPages(this.value)">
<span class="page-input-suffix">${opt.inputSuffix || ''}</span>
` : ''}
</div>
`).join('')}
</div>
</div>
`;
}
// 기존 페이지 옵션 (하위 호환)
if (type.options.pageOptions) {
html += `
<div class="option-section">
<div class="option-title">페이지 구성</div>
<div class="option-group">
${type.options.pageOptions.map((opt, idx) => `
<div class="option-item ${opt.default ? 'selected' : ''}" onclick="selectPageOption('${opt.value}')">
<input type="radio" name="pages" value="${opt.value}" id="page${idx}" ${opt.default ? 'checked' : ''}>
<label for="page${idx}">${opt.label}</label>
</div>
`).join('')}
</div>
</div>
`;
}
// 구성 요소 (보고서)
if (type.options.components) {
html += `
<div class="option-section">
<div class="option-title">보고서 구성</div>
<div class="option-group">
${type.options.components.map(comp => `
<div class="option-item" style="cursor:${comp.required ? 'default' : 'pointer'}; ${comp.required ? 'opacity:0.6;' : ''}">
<input type="checkbox" id="${comp.id}" ${comp.default ? 'checked' : ''} ${comp.required ? 'disabled' : ''}>
<label for="${comp.id}">${comp.icon} ${comp.label}</label>
</div>
`).join('')}
</div>
</div>
`;
}
// 슬라이드 수 (발표자료)
if (type.options.slideCount) {
html += `
<div class="option-section">
<div class="option-title">슬라이드 수</div>
<div class="option-group">
${type.options.slideCount.map((opt, idx) => `
<div class="option-item ${opt.default ? 'selected' : ''}" onclick="selectSlideCount('${opt.value}')">
<input type="radio" name="slideCount" value="${opt.value}" id="slide${idx}" ${opt.default ? 'checked' : ''}>
<label for="slide${idx}">${opt.label}</label>
</div>
`).join('')}
</div>
</div>
`;
}
container.innerHTML = html;
}
// ===== 페이지 구성 선택 (새로운 방식) =====
let currentPageConfig = 'body-attach';
let attachPageCount = 1;
function selectPageConfig(value, idx) {
currentPageConfig = value;
document.querySelectorAll('#docTypeOptionsContainer .option-item').forEach(item => {
const radio = item.querySelector('input[type="radio"][name="pageConfig"]');
if (radio) {
const isSelected = radio.value === value;
item.classList.toggle('selected', isSelected);
radio.checked = isSelected;
}
});
// 첨부 페이지 입력 활성화/비활성화
const attachInput = document.getElementById('attachPages');
if (attachInput) {
attachInput.disabled = (value === 'body-only');
attachInput.style.opacity = (value === 'body-only') ? '0.5' : '1';
}
}
function updateAttachPages(value) {
attachPageCount = parseInt(value) || 1;
console.log('첨부 페이지 수:', attachPageCount);
}
// ===== 슬라이드 수 선택 =====
function selectSlideCount(count) {
document.querySelectorAll('#docTypeOptionsContainer .option-item').forEach(item => {
const radio = item.querySelector('input[type="radio"][name="slideCount"]');
if (radio) {
const isSelected = radio.value === count;
item.classList.toggle('selected', isSelected);
radio.checked = isSelected;
}
});
}
// ===== 문서 유형 분석 관련 =====
const ANALYSIS_STEPS = [
{id: 1, name: "문서 파싱"},
{id: 2, name: "레이아웃 분석"},
{id: 3, name: "맥락 분석"},
{id: 4, name: "구조 분석"},
{id: 5, name: "템플릿 추출"},
{id: 6, name: "최종 검증"}
];
// 모달 열기
function openDocTypeModal() {
resetDocTypeModal();
document.getElementById('addDocTypeModal').classList.add('active');
}
// 모달 닫기
function closeAddDocTypeModal() {
document.getElementById('addDocTypeModal').classList.remove('active');
resetDocTypeModal();
}
// 모달 초기화
function resetDocTypeModal() {
analysisResult = null;
// Step 표시 초기화
document.getElementById('docTypeStep1').style.display = 'block';
document.getElementById('docTypeStep2').style.display = 'none';
document.getElementById('docTypeStep3').style.display = 'none';
// 제목 초기화
document.getElementById('addDocTypeModalTitle').textContent = '📄 문서 유형 추가';
// 입력 초기화
document.getElementById('newDocTypeName').value = '';
document.getElementById('newDocTypeDesc').value = '';
document.getElementById('newDocTypeFile').value = '';
// 버튼 초기화
const footer = document.getElementById('docTypeModalFooter');
footer.style.display = 'flex';
const actionBtn = document.getElementById('docTypeActionBtn');
actionBtn.textContent = '분석 시작';
actionBtn.onclick = startDocTypeAnalysis;
}
// 분석 시작
function startDocTypeAnalysis() {
const name = document.getElementById('newDocTypeName').value.trim();
const file = document.getElementById('newDocTypeFile').files[0];
if (!name) {
alert('문서 유형 이름을 입력해주세요.');
return;
}
if (!file) {
alert('샘플 문서를 선택해주세요.');
return;
}
// Step 2로 전환
document.getElementById('docTypeStep1').style.display = 'none';
document.getElementById('docTypeStep2').style.display = 'block';
document.getElementById('addDocTypeModalTitle').textContent = '🔄 문서 분석 중...';
// 푸터 버튼 숨기기
document.getElementById('docTypeModalFooter').style.display = 'none';
// 진행 단계 UI 생성
renderAnalysisSteps();
// 분석 시작
performAnalysis(name, file);
}
// 진행 단계 렌더링
function renderAnalysisSteps() {
const container = document.getElementById('analysisSteps');
container.innerHTML = ANALYSIS_STEPS.map(step => `
<div class="analysis-step" id="analysisStep${step.id}">
<span class="step-icon">⏳</span>
<span class="step-name">Step ${step.id}: ${step.name}</span>
<span class="step-status">대기</span>
</div>
`).join('');
}
// 단계 상태 업데이트 (메시지 지원)
function updateAnalysisStep(stepId, status, message = '') {
const stepEl = document.getElementById(`analysisStep${stepId}`);
if (!stepEl) return;
const iconEl = stepEl.querySelector('.step-icon');
const statusEl = stepEl.querySelector('.step-status');
if (status === 'running') {
iconEl.textContent = '🔄';
iconEl.classList.add('spinning');
statusEl.textContent = message || '진행중...';
stepEl.classList.add('running');
} else if (status === 'done') {
iconEl.textContent = '✅';
iconEl.classList.remove('spinning');
statusEl.textContent = message || '완료';
stepEl.classList.remove('running');
stepEl.classList.add('done');
} else if (status === 'error') {
iconEl.textContent = '❌';
iconEl.classList.remove('spinning');
statusEl.textContent = message || '실패';
stepEl.classList.add('error');
}
// 진행률 업데이트
const doneCount = document.querySelectorAll('.analysis-step.done').length;
const progress = Math.round((doneCount / ANALYSIS_STEPS.length) * 100);
document.getElementById('analysisProgressBar').style.width = progress + '%';
document.getElementById('analysisProgressText').textContent = `${progress}% 완료`;
}
// 분석 수행 (SSE 방식)
async function performAnalysis(name, file) {
console.log('🚀 performAnalysis 시작 (SSE 모드)');
const formData = new FormData();
formData.append('name', name);
formData.append('description', document.getElementById('newDocTypeDesc').value.trim());
formData.append('file', file);
// XMLHttpRequest로 SSE 연결 (FormData 전송)
const xhr = new XMLHttpRequest();
xhr.open('POST', '/api/doc-types/analyze-stream', true);
let buffer = '';
xhr.onprogress = function() {
const newData = xhr.responseText.substring(buffer.length);
buffer = xhr.responseText;
// SSE 메시지 파싱
const lines = newData.split('\n');
for (const line of lines) {
if (line.startsWith('data: ')) {
try {
const data = JSON.parse(line.substring(6));
handleSSEMessage(data);
} catch (e) {
console.warn('SSE 파싱 오류:', e);
}
}
}
};
xhr.onerror = function() {
console.error('❌ SSE 연결 오류');
alert('서버 연결 오류');
closeAddDocTypeModal();
};
xhr.ontimeout = function() {
console.error('❌ SSE 타임아웃');
alert('분석 시간 초과');
closeAddDocTypeModal();
};
xhr.timeout = 120000; // 2분
xhr.send(formData);
}
// SSE 메시지 처리
function handleSSEMessage(data) {
console.log('📨 SSE:', data);
switch(data.type) {
case 'progress':
updateAnalysisStep(data.step, data.status, data.message);
break;
case 'result':
console.log('✅ 분석 완료!');
analysisResult = data.data;
showAnalysisResult(data.data);
break;
case 'error':
console.error('❌ 분석 오류:', data.error);
alert('분석 중 오류: ' + data.error.message);
closeAddDocTypeModal();
break;
}
}
// 분석 결과 표시 (v2.0 맥락 기반)
function showAnalysisResult(data) {
document.getElementById('docTypeStep2').style.display = 'none';
document.getElementById('docTypeStep3').style.display = 'block';
document.getElementById('addDocTypeModalTitle').textContent = '✅ 분석 완료';
// 푸터 버튼 복원 및 변경
const footer = document.getElementById('docTypeModalFooter');
footer.style.display = 'flex';
const actionBtn = document.getElementById('docTypeActionBtn');
actionBtn.textContent = '저장';
actionBtn.onclick = saveAnalyzedDocType;
// v2.0: 맥락 정보
const context = data.context || {};
const structure = data.structure || {};
const config = data.config || {};
// 요약 표시
document.getElementById('analysisResultSummary').innerHTML = `
<div class="summary-item"><strong>유형:</strong> ${context.documentType || '?'}</div>
<div class="summary-item"><strong>페이지:</strong> ${structure.pageEstimate || '?'}p</div>
<div class="summary-item"><strong>섹션:</strong> ${(structure.sectionGuides || structure.sections)?.length || '?'}개</div>
`;
// 상세 결과 표시
const sections = structure.sectionGuides || structure.sections || [];
document.getElementById('analysisResultToc').innerHTML = `
<div style="margin-bottom: 16px; padding: 12px; background: var(--ui-bg); border-radius: 6px;">
<h5 style="margin-bottom: 8px; font-size: 11px; color: var(--ui-accent);">📋 문서 맥락</h5>
<p style="font-size: 11px; color: var(--ui-dim); margin-bottom: 4px;"><strong>목적:</strong> ${context.purpose || '-'}</p>
<p style="font-size: 11px; color: var(--ui-dim); margin-bottom: 4px;"><strong>대상:</strong> ${context.audience || '-'}</p>
<p style="font-size: 11px; color: var(--ui-dim);"><strong>톤:</strong> ${context.tone || '-'}</p>
</div>
<h5 style="margin-bottom: 10px; font-size: 12px; color: var(--ui-dim);">📐 문서 구조</h5>
<p style="font-size: 11px; color: var(--ui-text); margin-bottom: 10px;"><strong>논리 흐름:</strong> ${structure.logicFlow || '-'}</p>
${sections.length > 0 ? `
<ul class="toc-list">
${sections.map(s => `
<li class="toc-level-${s.level || 1}">
<strong>${s.name || s.title}</strong>
<span style="font-size: 10px; color: var(--ui-dim); display: block;">${s.role || ''}</span>
</li>
`).join('')}
</ul>
` : `
<p style="font-size: 12px; color: var(--ui-warning);">⚠️ 구조를 파악하지 못했습니다.</p>
`}
`;
}
// 분석 결과 저장
async function saveAnalyzedDocType() {
if (!analysisResult || !analysisResult.config) {
alert('저장할 분석 결과가 없습니다.');
return;
}
const savedName = analysisResult.config.name; // ← 미리 저장!
const actionBtn = document.getElementById('docTypeActionBtn');
actionBtn.disabled = true;
actionBtn.textContent = '저장 중...';
try {
const response = await fetch('/api/doc-types', {
method: 'POST',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(analysisResult.config)
});
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
closeAddDocTypeModal();
loadDocTypes();
loadUserTemplates();
setStatus(`문서 유형 "${savedName}" 추가 완료`, true); // ← 저장한 값 사용!
} catch (error) {
alert('저장 실패: ' + error.message);
} finally {
actionBtn.disabled = false;
actionBtn.textContent = '저장';
}
}
// ===== 문서 유형 삭제 =====
async function deleteDocType(typeId) {
if (!confirm('이 문서 유형을 삭제하시겠습니까?')) return;
try {
const response = await fetch(`/api/doc-types/${typeId}`, {
method: 'DELETE'
});
if (!response.ok) {
throw new Error('삭제 실패');
}
docTypes = docTypes.filter(t => t.id !== typeId);
renderDocTypeList();
// 삭제된 유형이 선택되어 있었으면 첫 번째로 변경
if (currentDocType === typeId) {
const firstEnabled = docTypes.find(t => t.enabled);
if (firstEnabled) selectDocType(firstEnabled.id);
}
setStatus('문서 유형 삭제 완료', true);
} catch (error) {
alert('삭제 오류: ' + error.message);
}
}
// ===== 작성 방식 선택 =====
function selectWriteMode(mode) {
currentWriteMode = mode;
document.querySelectorAll('.write-mode-tab').forEach(tab => {
tab.classList.remove('selected');
tab.querySelector('input[type="radio"]').checked = false;
});
const selectedTab = document.querySelector(`.write-mode-tab input[value="${mode}"]`);
if (selectedTab) {
selectedTab.checked = true;
selectedTab.closest('.write-mode-tab').classList.add('selected');
}
}
// ===== 템플릿 모달 =====
function openTemplateModal() {
document.getElementById('templateModal').classList.add('active');
document.getElementById('templateNameInput').value = '';
removeTemplateFile();
}
function closeTemplateModal() {
document.getElementById('templateModal').classList.remove('active');
}
function handleTemplateFile(input) {
if (input.files.length > 0) {
const file = input.files[0];
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()) {
nameInput.value = file.name.replace(/\.[^/.]+$/, '');
}
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();
}
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.meta);
renderUserTemplates();
closeTemplateModal();
setStatus(`템플릿 "${name}" 추가 완료`, true);
} catch (error) {
alert('템플릿 분석 오류: ' + error.message);
} finally {
btn.disabled = false;
spinner.style.display = 'none';
text.textContent = '✨ 분석 및 추가';
}
}
function selectTemplate(templateId) {
currentTemplate = templateId;
document.querySelectorAll('.template-item').forEach(item => {
item.classList.remove('selected');
const radio = item.querySelector('input[type="radio"]');
if (radio) radio.checked = false;
});
const selectedItem = document.querySelector(`.template-item[data-template="${templateId}"]`);
if (selectedItem) {
selectedItem.classList.add('selected');
const radio = selectedItem.querySelector('input[type="radio"]');
if (radio) radio.checked = true;
}
const elementsPanel = document.getElementById('templateElementOptions');
if (templateId === 'default') {
elementsPanel.style.display = 'none';
} else {
showTemplateElements(templateId);
elementsPanel.style.display = 'block';
}
}
function showTemplateElements(templateId) {
const template = userTemplates.find(t => t.id === templateId);
if (!template || !template.elements) return;
const container = document.querySelector('#templateElementOptions .elements-list');
container.innerHTML = template.elements.map(el => `
<label class="element-checkbox">
<input type="checkbox"
data-element="${el.type}"
${el.default ? 'checked' : ''}
onchange="toggleTemplateElement('${templateId}', '${el.type}', this.checked)">
<span class="element-icon">${el.icon}</span>
<span>${el.name}</span>
</label>
`).join('');
}
function toggleTemplateElement(templateId, elementType, checked) {
if (!templateElements[templateId]) {
templateElements[templateId] = {};
}
templateElements[templateId][elementType] = checked;
}
function renderUserTemplates() {
const container = document.getElementById('userTemplatesListNew');
if (userTemplates.length === 0) {
container.innerHTML = '';
return;
}
container.innerHTML = userTemplates.map(tpl => `
<div class="template-item" data-template="${tpl.id}" onclick="selectTemplate('${tpl.id}')">
<input type="radio" name="template">
<span class="label">📑 ${tpl.name}</span>
<button class="delete-btn" onclick="event.stopPropagation(); deleteTemplate('${tpl.id}')" title="삭제">✕</button>
</div>
`).join('');
}
async function loadUserTemplates() {
try {
const response = await fetch('/api/templates');
const data = await response.json();
if (Array.isArray(data)) {
userTemplates = data;
renderUserTemplates();
}
} catch (error) {
console.error('템플릿 목록 로드 실패:', error);
}
}
async function deleteTemplate(templateId) {
if (!confirm('이 템플릿을 삭제하시겠습니까?')) return;
try {
await fetch(`/api/templates/${templateId}`, { method: 'DELETE' });
userTemplates = userTemplates.filter(t => t.id !== templateId);
renderUserTemplates();
if (currentTemplate === templateId) {
selectTemplate('default');
}
setStatus('템플릿 삭제 완료', true);
} catch (error) {
alert('삭제 오류: ' + error.message);
}
}
// ===== 상태 표시 =====
function setStatus(msg, connected = false) {
document.getElementById('statusMessage').textContent = msg;
document.getElementById('statusDot').classList.toggle('connected', connected);
}
// ===== 입력 상태 업데이트 =====
function updateInputStatus() {
const hasFolder = folderPath.length > 0;
const hasLinks = referenceLinks.length > 0;
const hasHtml = inputContent.length > 0;
const pathEl = document.getElementById('folderPathDisplay');
if (hasFolder) {
pathEl.textContent = folderPath;
pathEl.classList.remove('empty');
} else {
pathEl.textContent = '폴더 경로가 설정되지 않음';
pathEl.classList.add('empty');
}
document.getElementById('linkCount').textContent = referenceLinks.length + '개';
const htmlStatus = document.getElementById('htmlInputStatus');
if (hasHtml) {
htmlStatus.textContent = '✓ 입력됨';
htmlStatus.classList.add('ok');
} else {
htmlStatus.textContent = '없음';
htmlStatus.classList.remove('ok');
}
const canGenerate = hasHtml || hasFolder || hasLinks;
document.getElementById('generateBtn').disabled = !canGenerate;
if (canGenerate) {
updateStep(0, 'done');
} else {
updateStep(0, 'pending');
}
}
// ===== 폴더 모달 =====
function openFolderModal() {
document.getElementById('folderModal').classList.add('active');
document.getElementById('folderPath').focus();
}
function closeFolderModal() {
document.getElementById('folderModal').classList.remove('active');
}
function submitFolder() {
const path = document.getElementById('folderPath').value.trim();
if (!path) {
alert('폴더 경로를 입력해주세요.');
return;
}
folderPath = path;
closeFolderModal();
updateInputStatus();
setStatus('폴더 경로 설정됨', true);
document.getElementById('totalCount').textContent = '5개';
document.getElementById('okCount').textContent = '3개 ✓';
document.getElementById('unknownCount').textContent = '2개';
}
function toggleUnknownFiles() {
document.getElementById('unknownFilesBox').classList.toggle('show');
}
function openFolder() {
alert('폴더 열기는 Engine이 실행 중일 때만 가능합니다.');
}
// ===== 링크 모달 =====
function openLinkModal() {
document.getElementById('linkModal').classList.add('active');
}
function closeLinkModal() {
document.getElementById('linkModal').classList.remove('active');
}
function addLinkInput() {
const container = document.getElementById('linkInputList');
const input = document.createElement('input');
input.type = 'text';
input.className = 'link-input';
input.placeholder = 'https://...';
input.style = 'width:100%; padding:10px; border-radius:6px; border:1px solid var(--ui-border); background:var(--ui-bg); color:var(--ui-text); font-size:12px; margin-bottom:8px;';
container.appendChild(input);
}
function submitLinks() {
const inputs = document.querySelectorAll('#linkInputList .link-input');
referenceLinks = [];
inputs.forEach(input => {
const val = input.value.trim();
if (val) referenceLinks.push(val);
});
closeLinkModal();
updateInputStatus();
if (referenceLinks.length > 0) {
setStatus(`참고 링크 ${referenceLinks.length}개 설정됨`, true);
}
}
// ===== HTML 모달 =====
function openHtmlModal() {
document.getElementById('htmlModal').classList.add('active');
document.getElementById('htmlContent').focus();
}
function closeHtmlModal() {
document.getElementById('htmlModal').classList.remove('active');
}
function submitHtml() {
const html = document.getElementById('htmlContent').value.trim();
if (!html) {
alert('HTML을 입력해주세요.');
return;
}
inputContent = html;
closeHtmlModal();
updateInputStatus();
setStatus('HTML 입력 완료', true);
}
// ===== 진행 상태 =====
function updateStep(num, status) {
const item = document.querySelector(`.step-item[data-step="${num}"]`);
if (!item) return;
item.classList.remove('pending', 'running', 'done', 'error');
item.classList.add(status);
item.querySelector('.status').textContent =
status === 'pending' ? '○' :
status === 'running' ? '◐' :
status === 'done' ? '●' : '✕';
}
function resetSteps() {
for (let i = 0; i <= 9; i++) {
updateStep(i, 'pending');
}
}
// ===== 생성 =====
async function generate() {
const type = docTypes.find(t => t.id === currentDocType);
if (!type) return;
if (type.generateFlow === 'draft-first') {
await generateDraft();
} else {
await generateBriefing();
}
}
async function generateBriefing() {
if (!inputContent && !folderPath && referenceLinks.length === 0) {
alert('먼저 폴더 위치, 참고 링크, 또는 HTML을 입력해주세요.');
return;
}
const btn = document.getElementById('generateBtn');
const btnText = document.getElementById('generateBtnText');
const spinner = document.getElementById('generateSpinner');
btn.disabled = true;
btnText.textContent = '생성 중...';
spinner.style.display = 'block';
resetSteps();
updateStep(0, 'done');
setStatus('생성 중...', true);
try {
for (let i = 1; i <= 7; i++) {
updateStep(i, 'running');
await new Promise(r => setTimeout(r, 200));
updateStep(i, 'done');
}
updateStep(8, 'running');
// 페이지 구성 값 가져오기
let pageOption = '2'; // 기본값
if (currentPageConfig === 'body-only') {
pageOption = '1';
} else if (currentPageConfig === 'body-attach') {
pageOption = String(1 + attachPageCount); // 본문 1p + 첨부 np
}
const formData = new FormData();
formData.append('content', inputContent);
formData.append('doc_type', currentDocType); // ← 이거 추가!
formData.append('page_option', pageOption);
formData.append('attach_pages', attachPageCount);
formData.append('instruction', document.getElementById('globalInstructionInput').value);
const response = await fetch('/generate', {
method: 'POST',
body: formData
});
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
updateStep(8, 'done');
updateStep(9, 'running');
await new Promise(r => setTimeout(r, 300));
updateStep(9, 'done');
if (data.success && data.html) {
generatedHTML = data.html;
document.getElementById('placeholder').style.display = 'none';
const frame = document.getElementById('previewFrame');
frame.classList.add('active');
frame.srcdoc = generatedHTML;
setTimeout(setupIframeSelection, 500);
document.getElementById('feedbackBar').classList.add('show');
setStatus('생성 완료', true);
}
} catch (error) {
alert('생성 오류: ' + error.message);
setStatus('오류 발생', false);
for (let i = 0; i <= 9; i++) {
const item = document.querySelector(`.step-item[data-step="${i}"]`);
if (item && item.classList.contains('running')) {
updateStep(i, 'error');
}
}
} finally {
btn.disabled = false;
btnText.textContent = '🚀 생성하기';
spinner.style.display = 'none';
}
}
async function generateDraft() {
if (!folderPath && !inputContent && referenceLinks.length === 0) {
alert('먼저 폴더 위치, 참고 링크, 또는 HTML을 입력해주세요.');
return;
}
const btn = document.getElementById('generateBtn');
const btnText = document.getElementById('generateBtnText');
const spinner = document.getElementById('generateSpinner');
btn.disabled = true;
btnText.textContent = '분석 중...';
spinner.style.display = 'block';
resetSteps();
updateStep(0, 'done');
setStatus('목차 생성 중...', true);
try {
for (let i = 1; i <= 7; i++) {
updateStep(i, 'running');
await new Promise(r => setTimeout(r, 500));
updateStep(i, 'done');
}
document.getElementById('placeholder').style.display = 'none';
document.getElementById('tocActionBar').classList.add('show');
document.getElementById('feedbackBar').classList.remove('show');
setStatus('목차 생성 완료 - 확인 후 승인해주세요', true);
} catch (error) {
alert('목차 생성 오류: ' + error.message);
setStatus('오류 발생', false);
} finally {
btn.disabled = false;
btnText.textContent = '📋 목차 확인하기';
spinner.style.display = 'none';
}
}
function editToc() {
const tocContainer = document.getElementById('tocContainer');
if (tocContainer) {
tocContainer.contentEditable = true;
tocContainer.style.outline = '2px solid var(--ui-accent)';
}
setStatus('목차 편집 모드 - 직접 수정 가능합니다', true);
}
async function approveToc() {
const btn = document.getElementById('approveBtn');
btn.disabled = true;
btn.textContent = '⏳ 생성 중...';
document.getElementById('tocActionBar').classList.remove('show');
setStatus('최종 문서 생성 중...', true);
try {
for (let i = 8; i <= 9; i++) {
updateStep(i, 'running');
await new Promise(r => setTimeout(r, 800));
updateStep(i, 'done');
}
await generateReport();
} catch (error) {
alert('문서 생성 오류: ' + error.message);
setStatus('오류 발생', false);
} finally {
btn.disabled = false;
btn.textContent = '✅ 승인 & 생성하기';
}
}
async function generateReport() {
// 보고서 옵션 수집
const coverCheck = document.getElementById('cover');
const tocCheck = document.getElementById('toc');
const dividerCheck = document.getElementById('divider');
const response = await fetch('/generate-report', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
content: inputContent,
folder_path: folderPath,
cover: coverCheck ? coverCheck.checked : true,
toc: tocCheck ? tocCheck.checked : true,
divider: dividerCheck ? dividerCheck.checked : false,
instruction: document.getElementById('globalInstructionInput').value
})
});
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
if (data.success && data.html) {
generatedHTML = data.html;
document.getElementById('placeholder').style.display = 'none';
const frame = document.getElementById('previewFrame');
frame.classList.add('active');
frame.srcdoc = generatedHTML;
setTimeout(setupIframeSelection, 500);
document.getElementById('feedbackBar').classList.add('show');
setStatus('생성 완료', true);
}
}
// ===== 피드백 =====
async function submitFeedback() {
const feedback = document.getElementById('feedbackInput').value.trim();
if (!feedback) {
alert('수정 내용을 입력해주세요.');
return;
}
if (!generatedHTML) {
alert('먼저 문서를 생성해주세요.');
return;
}
const btn = document.getElementById('feedbackBtn');
const btnText = document.getElementById('feedbackBtnText');
const spinner = document.getElementById('feedbackSpinner');
btn.disabled = true;
btnText.textContent = '⏳ 수정 중...';
spinner.style.display = 'inline-block';
setStatus('수정 중...', true);
try {
const response = await fetch('/refine', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
feedback: feedback,
current_html: generatedHTML
})
});
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
if (data.success && data.html) {
generatedHTML = data.html;
document.getElementById('previewFrame').srcdoc = generatedHTML;
document.getElementById('feedbackInput').value = '';
setTimeout(setupIframeSelection, 500);
setStatus('수정 완료', true);
}
} catch (error) {
alert('수정 오류: ' + error.message);
setStatus('오류 발생', false);
} finally {
btn.disabled = false;
btnText.textContent = '🔄 수정 반영';
spinner.style.display = 'none';
}
}
function regenerate() {
if (confirm('현재 결과를 버리고 다시 생성하시겠습니까?')) {
generate();
}
}
// ===== 줌 =====
function setZoom(value) {
currentZoom = parseInt(value);
document.getElementById('a4Wrapper').style.transform = `scale(${currentZoom / 100})`;
}
// ===== 저장/출력 =====
function saveHtml() {
if (!generatedHTML) {
alert('먼저 문서를 생성해주세요.');
return;
}
const frame = document.getElementById('previewFrame');
const html = frame.contentDocument ?
'<!DOCTYPE html>' + frame.contentDocument.documentElement.outerHTML :
generatedHTML;
const blob = new Blob([html], { type: 'text/html' });
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `report_${new Date().toISOString().slice(0,10)}.html`;
a.click();
URL.revokeObjectURL(url);
}
async function exportHwp() {
if (!generatedHTML) {
alert('먼저 문서를 생성해주세요.');
return;
}
const frame = document.getElementById('previewFrame');
const html = frame.contentDocument ?
'<!DOCTYPE html>' + frame.contentDocument.documentElement.outerHTML :
generatedHTML;
setStatus('HWP 변환 중...', true);
try {
const response = await fetch('/export-hwp', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
html: html,
doc_type: currentDocType,
style_grouping: true
})
});
if (!response.ok) {
const error = await response.json();
throw new Error(error.error || 'HWP 변환 실패');
}
const blob = await response.blob();
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = `report_${new Date().toISOString().slice(0,10)}.hwp`;
a.click();
URL.revokeObjectURL(url);
setStatus('HWP 변환 완료', true);
} catch (error) {
alert('HWP 변환 오류: ' + error.message);
setStatus('오류 발생', false);
}
}
function printDoc() {
const frame = document.getElementById('previewFrame');
if (frame.contentWindow) {
frame.contentWindow.print();
}
}
function toggleEditMode() {
// TODO: 편집 모드 구현
}
// ===== AI 부분 수정 =====
function setupIframeSelection() {
const frame = document.getElementById('previewFrame');
if (!frame.contentDocument) return;
frame.contentDocument.addEventListener('mouseup', function(e) {
const selection = frame.contentWindow.getSelection();
const text = selection.toString().trim();
if (text.length > 0) {
selectedText = text;
selectedRange = selection.getRangeAt(0).cloneRange();
showAiEditPopup(text);
}
});
frame.contentDocument.addEventListener('mousedown', function(e) {
if (frame.contentWindow.getSelection().toString().trim() === '') {
closeAiEditPopup();
}
});
}
function showAiEditPopup(text) {
const popup = document.getElementById('aiEditPopup');
const textDisplay = document.getElementById('aiEditSelectedText');
const displayText = text.length > 150 ? text.substring(0, 150) + '...' : text;
textDisplay.textContent = displayText;
popup.classList.add('show');
document.getElementById('aiEditInput').focus();
}
function closeAiEditPopup() {
document.getElementById('aiEditPopup').classList.remove('show');
document.getElementById('aiEditInput').value = '';
selectedText = '';
selectedRange = null;
}
async function submitAiEdit() {
const request = document.getElementById('aiEditInput').value.trim();
if (!request) {
alert('수정 요청을 입력해주세요.');
return;
}
if (!selectedText) {
alert('선택된 텍스트가 없습니다.');
return;
}
const btn = document.querySelector('.ai-edit-btn');
const originalText = btn.textContent;
btn.textContent = '⏳ 수정 중...';
btn.disabled = true;
setStatus('부분 수정 중...', true);
try {
const response = await fetch('/refine-selection', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
current_html: generatedHTML,
selected_text: selectedText,
request: request,
doc_type: currentDocType
})
});
const data = await response.json();
if (data.error) {
throw new Error(data.error);
}
if (data.success && data.html) {
const frame = document.getElementById('previewFrame');
const doc = frame.contentDocument;
// 간단한 텍스트 교체
const modifiedContent = data.html.replace(/```html\n?/g, '').replace(/```\n?/g, '').trim();
const searchStr = selectedText.substring(0, 30);
const allElements = doc.body.getElementsByTagName('*');
for (const el of allElements) {
if (el.textContent && el.textContent.includes(searchStr)) {
let hasChildWithText = false;
for (const child of el.children) {
if (child.textContent && child.textContent.includes(searchStr)) {
hasChildWithText = true;
break;
}
}
if (!hasChildWithText) {
el.innerHTML = modifiedContent;
break;
}
}
}
generatedHTML = '<!DOCTYPE html>' + doc.documentElement.outerHTML;
setTimeout(setupIframeSelection, 500);
closeAiEditPopup();
setStatus('부분 수정 완료', true);
}
} catch (error) {
alert('수정 오류: ' + error.message);
setStatus('오류 발생', false);
} finally {
btn.textContent = originalText;
btn.disabled = false;
}
}
// ===== 초기화 =====
document.addEventListener('DOMContentLoaded', function() {
setStatus('로딩 중...', false);
// 문서 유형 로드
loadDocTypes();
loadUserTemplates();
// 템플릿 로드
loadUserTemplates();
// 드롭존 이벤트
const dropzones = ['templateDropzone', 'docTypeDropzone'];
dropzones.forEach(id => {
const dropzone = document.getElementById(id);
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'));
});
}
});
// 템플릿 입력 이벤트
const templateNameInput = document.getElementById('templateNameInput');
if (templateNameInput) {
templateNameInput.addEventListener('input', updateTemplateSubmitBtn);
}
setStatus('준비됨', true);
});
// Enter 키로 피드백 제출
document.getElementById('feedbackInput').addEventListener('keypress', function(e) {
if (e.key === 'Enter') {
submitFeedback();
}
});
</script>
</body>
</html>