3766 lines
133 KiB
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="예: 표를 더 상세하게 예: 전문 용어 풀어서 설명"></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="예: 한 줄로 요약해줘 예: 표 형태로 만들어줘"></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> |