2356 lines
88 KiB
HTML
2356 lines
88 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;
|
|
}
|
|
|
|
* { 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);
|
|
}
|
|
|
|
/* 진행 상태 */
|
|
.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;
|
|
}
|
|
.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: 20px;
|
|
}
|
|
|
|
.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;
|
|
}
|
|
|
|
/* 플로팅 프리뷰 팝업 - 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;
|
|
}
|
|
|
|
/* 프리뷰 정보 */
|
|
.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"] {
|
|
accent-color: var(--ui-accent);
|
|
}
|
|
|
|
.option-item label {
|
|
font-size: 12px;
|
|
cursor: pointer;
|
|
flex: 1;
|
|
}
|
|
|
|
/* 요청사항 */
|
|
.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;
|
|
}
|
|
</style>
|
|
<link rel="stylesheet" href="/static/css/editor.css">
|
|
<script src="/static/js/editor.js"></script>
|
|
|
|
</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>
|
|
<input type="file" id="fileInput" accept=".html,.htm,.txt" style="display:none" onchange="handleFileUpload(this)">
|
|
</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>
|
|
|
|
<!-- 초안 생성 버튼 -->
|
|
<button class="sidebar-generate-btn" id="generateBtnSide" onclick="generate()" disabled>
|
|
🚀 초안 생성하기
|
|
</button>
|
|
|
|
<!-- 진행 상태 -->
|
|
<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="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">
|
|
<!-- 기획서 -->
|
|
<div class="doc-type-item selected" data-type="briefing" onclick="selectDocType('briefing')">
|
|
<input type="radio" name="docType" checked>
|
|
<span class="label">📋 기획서</span>
|
|
|
|
<!-- 플로팅 프리뷰 -->
|
|
<div class="doc-type-preview">
|
|
<div class="preview-thumbnail 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-body" style="width:80%"></div>
|
|
<div class="page-body" style="width:90%"></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" style="width:85%"></div>
|
|
<div class="page-body"></div>
|
|
<div class="page-body" style="width:75%"></div>
|
|
<div class="page-bottom"></div>
|
|
</div>
|
|
</div>
|
|
<div class="preview-title">기획서 (보고자료)</div>
|
|
<div class="preview-desc">임원보고용 정형화된 1~2페이지 문서</div>
|
|
<div class="preview-features">
|
|
<div class="preview-feature"><span class="icon">📄</span> 1p 본문만 / 1p+1p첨부 / 1p+np첨부</div>
|
|
<div class="preview-feature"><span class="icon">🎨</span> Navy 양식 (A4 인쇄 최적화)</div>
|
|
<div class="preview-feature"><span class="icon">✍️</span> 개조식 자동 변환</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 보고서 -->
|
|
<div class="doc-type-item" data-type="report" onclick="selectDocType('report')">
|
|
<input type="radio" name="docType">
|
|
<span class="label">📄 보고서</span>
|
|
|
|
<div class="doc-type-preview">
|
|
<div class="preview-thumbnail report">
|
|
<div class="line h1"></div>
|
|
<div class="line body"></div>
|
|
<div class="line body" style="width:90%"></div>
|
|
<div class="line body" style="width:85%"></div>
|
|
<div class="line h2"></div>
|
|
<div class="line body"></div>
|
|
<div class="line body" style="width:92%"></div>
|
|
<div class="line h2"></div>
|
|
<div class="line body" style="width:88%"></div>
|
|
</div>
|
|
<div class="preview-title">보고서 (HWP)</div>
|
|
<div class="preview-desc">RAG 기반 장문 보고서 → HWPX 출력</div>
|
|
<div class="preview-features">
|
|
<div class="preview-feature"><span class="icon">🏷️</span> AI 스타일 자동 태깅</div>
|
|
<div class="preview-feature"><span class="icon">📝</span> 대제목/중제목/소제목/본문</div>
|
|
<div class="preview-feature"><span class="icon">✨</span> 한글에서 스타일 일괄 변경</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 발표자료 -->
|
|
<div class="doc-type-item disabled" data-type="presentation">
|
|
<input type="radio" name="docType" disabled>
|
|
<span class="label">📊 발표자료</span>
|
|
<span class="badge">준비중</span>
|
|
|
|
<div class="doc-type-preview">
|
|
<div class="preview-thumbnail ppt">
|
|
<div class="slide">
|
|
<div class="slide-title">제목</div>
|
|
<div class="slide-body"></div>
|
|
<div class="slide-body"></div>
|
|
</div>
|
|
<div class="slide">
|
|
<div class="slide-title">본문</div>
|
|
<div class="slide-body"></div>
|
|
<div class="slide-body"></div>
|
|
<div class="slide-body"></div>
|
|
</div>
|
|
<div class="slide">
|
|
<div class="slide-title">결론</div>
|
|
<div class="slide-body"></div>
|
|
</div>
|
|
</div>
|
|
<div class="preview-title">발표자료 (PPT)</div>
|
|
<div class="preview-desc">프레젠테이션 형식 슬라이드</div>
|
|
<div class="preview-features">
|
|
<div class="preview-feature"><span class="icon">📊</span> 슬라이드 자동 구성</div>
|
|
<div class="preview-feature"><span class="icon">🎯</span> 핵심 내용 추출</div>
|
|
<div class="preview-feature"><span class="icon">🖼️</span> 도식화 자동 생성</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 템플릿 추가 버튼 -->
|
|
<button class="add-template-btn" onclick="addTemplate()">+ 템플릿 추가</button>
|
|
</div>
|
|
|
|
<!-- 기획서 옵션 -->
|
|
<div id="briefingOptions">
|
|
<!-- 페이지 구성 -->
|
|
<div class="option-section">
|
|
<div class="option-title">페이지 구성</div>
|
|
<div class="option-group">
|
|
<div class="option-item" onclick="selectPageOption('1')">
|
|
<input type="radio" name="pages" value="1" id="page1">
|
|
<label for="page1"> (본문) 1p</label>
|
|
</div>
|
|
<div class="option-item selected" onclick="selectPageOption('2')">
|
|
<input type="radio" name="pages" value="2" id="page2" checked>
|
|
<label for="page2"> (본문) 1p + (첨부) 1p</label>
|
|
</div>
|
|
<div class="option-item" onclick="selectPageOption('n')">
|
|
<input type="radio" name="pages" value="n" id="pageN">
|
|
<label for="pageN"> (본문) 1p + (첨부) np</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 요청사항 -->
|
|
<div class="option-section">
|
|
<div class="option-title">요청사항</div>
|
|
<textarea class="request-textarea" id="instructionInput" placeholder="예: 세무 리스크 중심으로 작성해줘 예: 표를 더 상세하게 만들어줘"></textarea>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 보고서 옵션 -->
|
|
<div id="reportOptions" style="display:none;">
|
|
<!-- 보고서 구성 -->
|
|
<div class="option-section">
|
|
<div class="option-title">보고서 구성</div>
|
|
<div class="option-group">
|
|
<div class="option-item" style="cursor:default;">
|
|
<input type="checkbox" id="reportCover" checked>
|
|
<label for="reportCover">📘 표지</label>
|
|
</div>
|
|
<div class="option-item" style="cursor:default;">
|
|
<input type="checkbox" id="reportToc" checked>
|
|
<label for="reportToc">📑 목차</label>
|
|
</div>
|
|
<div class="option-item" style="cursor:default;">
|
|
<input type="checkbox" id="reportDivider">
|
|
<label for="reportDivider">📄 간지</label>
|
|
</div>
|
|
<div class="option-item" style="cursor:default; opacity:0.6;">
|
|
<input type="checkbox" id="reportContent" checked disabled>
|
|
<label for="reportContent">📝 내지 (필수)</label>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 요청사항 -->
|
|
<div class="option-section">
|
|
<div class="option-title">요청사항</div>
|
|
<textarea class="request-textarea" id="reportInstructionInput" placeholder="예: 요약을 상세하게 작성해줘 예: 표지에 로고 추가"></textarea>
|
|
</div>
|
|
</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.0</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>
|
|
|
|
<script>
|
|
// ===== 상태 변수 =====
|
|
let inputContent = '';
|
|
let generatedHTML = '';
|
|
let currentDocType = 'briefing';
|
|
let currentPageOption = '2';
|
|
let currentZoom = 100;
|
|
let folderPath = '';
|
|
let referenceLinks = [];
|
|
|
|
// ===== AI 부분 수정 관련 =====
|
|
let selectedText = '';
|
|
let selectedRange = null;
|
|
|
|
// ===== HWP 추출 =====
|
|
async function exportHwp() {
|
|
if (!generatedHTML) {
|
|
alert('먼저 문서를 생성해주세요.');
|
|
return;
|
|
}
|
|
|
|
// 현재 편집된 HTML 가져오기
|
|
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);
|
|
}
|
|
}
|
|
|
|
// iframe 로드 후 선택 이벤트 연결
|
|
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;
|
|
btn.style.opacity = '0.7';
|
|
|
|
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) {
|
|
// 디버깅
|
|
console.log('=== AI 수정 디버깅 ===');
|
|
console.log('data.type:', data.type);
|
|
console.log('data.html:', data.html);
|
|
console.log('selectedText:', selectedText);
|
|
|
|
const editType = data.type || 'TEXT';
|
|
let modifiedContent = data.html.replace(/```html\n?/g, '').replace(/```\n?/g, '').trim();
|
|
|
|
console.log('editType:', editType);
|
|
console.log('modifiedContent:', modifiedContent);
|
|
|
|
const frame = document.getElementById('previewFrame');
|
|
const doc = frame.contentDocument;
|
|
|
|
if (editType === 'TEXT') {
|
|
// 선택 텍스트를 포함하는 가장 작은 요소 찾기
|
|
const searchStr = selectedText.substring(0, 30);
|
|
const allElements = doc.body.getElementsByTagName('*');
|
|
let smallestElement = null;
|
|
let smallestLength = Infinity;
|
|
|
|
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.length < smallestLength) {
|
|
smallestElement = el;
|
|
smallestLength = el.innerHTML.length;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (smallestElement) {
|
|
console.log('가장 작은 요소 찾음:', smallestElement.tagName, smallestElement.className);
|
|
console.log('기존 innerHTML:', smallestElement.innerHTML.substring(0, 100));
|
|
smallestElement.innerHTML = modifiedContent;
|
|
console.log('교체 후:', smallestElement.innerHTML.substring(0, 100));
|
|
} else {
|
|
console.log('요소 못찾음!');
|
|
}
|
|
|
|
} else {
|
|
// STRUCTURE: 부모 요소 전체 교체
|
|
if (selectedRange && selectedRange.commonAncestorContainer) {
|
|
let targetEl = selectedRange.commonAncestorContainer;
|
|
if (targetEl.nodeType === 3) {
|
|
targetEl = targetEl.parentElement;
|
|
}
|
|
|
|
if (currentDocType === 'report') {
|
|
// ===== 보고서: 안전한 부모만 교체 =====
|
|
// sheet, body-content 등 페이지 구조는 절대 건드리지 않음
|
|
const dangerousClasses = ['sheet', 'body-content', 'page-header', 'page-footer', 'a4-preview'];
|
|
|
|
// p, li, td, h1~h6 등 안전한 요소 찾기
|
|
const safeParent = targetEl.closest('p, li, td, th, h1, h2, h3, h4, h5, h6, ul, ol, table, figure');
|
|
|
|
if (safeParent && !dangerousClasses.some(cls => safeParent.classList.contains(cls))) {
|
|
// 선택된 요소 뒤에 새 구조 삽입
|
|
safeParent.insertAdjacentHTML('afterend', modifiedContent);
|
|
// 기존 요소는 유지하거나 숨김 처리
|
|
// safeParent.style.display = 'none'; // 선택: 기존 숨기기
|
|
safeParent.remove(); // 또는 삭제
|
|
console.log('보고서 - 안전한 구조 교체:', safeParent.tagName);
|
|
} else {
|
|
// 안전한 부모를 못 찾으면 텍스트 앞에 구조 삽입
|
|
console.log('보고서 - 안전한 부모 없음, 선택 위치에 삽입');
|
|
const range = selectedRange.cloneRange();
|
|
range.deleteContents();
|
|
const temp = document.createElement('div');
|
|
temp.innerHTML = modifiedContent;
|
|
range.insertNode(temp.firstElementChild || temp);
|
|
}
|
|
|
|
} else {
|
|
// ===== 기획서: 기존 로직 =====
|
|
const parent = targetEl.closest('.lead-box, .section, .info-table, .data-table, .process-flow') || targetEl.closest('div');
|
|
if (parent) {
|
|
parent.outerHTML = modifiedContent;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
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;
|
|
btn.style.opacity = '1';
|
|
}
|
|
}
|
|
|
|
|
|
// ===== 상태 표시 =====
|
|
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 + '개';
|
|
|
|
// HTML 입력 상태 표시
|
|
const htmlStatus = document.getElementById('htmlInputStatus');
|
|
if (hasHtml) {
|
|
htmlStatus.textContent = '✓ 입력됨';
|
|
htmlStatus.classList.add('ok');
|
|
} else {
|
|
htmlStatus.textContent = '없음';
|
|
htmlStatus.classList.remove('ok');
|
|
}
|
|
|
|
// 생성 버튼 활성화 (HTML 입력이 있거나, 폴더/링크가 있으면)
|
|
const canGenerate = hasHtml || hasFolder || hasLinks;
|
|
document.getElementById('generateBtnSide').disabled = !canGenerate;
|
|
document.getElementById('generateBtn').disabled = !canGenerate;
|
|
|
|
// Step 0 상태 업데이트
|
|
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);
|
|
|
|
// TODO: Engine API로 폴더 스캔
|
|
// 임시로 더미 데이터
|
|
document.getElementById('totalCount').textContent = '5개';
|
|
document.getElementById('okCount').textContent = '3개 ✓';
|
|
document.getElementById('unknownCount').textContent = '2개';
|
|
}
|
|
|
|
function toggleUnknownFiles() {
|
|
const box = document.getElementById('unknownFilesBox');
|
|
box.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);
|
|
}
|
|
}
|
|
|
|
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');
|
|
}
|
|
}
|
|
|
|
// ===== 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 handleFileUpload(input) {
|
|
const file = input.files[0];
|
|
if (!file) return;
|
|
|
|
const reader = new FileReader();
|
|
reader.onload = function(e) {
|
|
inputContent = e.target.result;
|
|
updateInputStatus();
|
|
setStatus('파일 로드 완료: ' + file.name, true);
|
|
};
|
|
reader.readAsText(file);
|
|
}
|
|
|
|
// ===== 문서 유형 선택 =====
|
|
function selectDocType(type) {
|
|
if (type === 'presentation') {
|
|
return; // PPT만 disabled
|
|
}
|
|
|
|
currentDocType = type;
|
|
document.querySelectorAll('.doc-type-item').forEach(item => {
|
|
item.classList.remove('selected');
|
|
if (item.dataset.type === type) {
|
|
item.classList.add('selected');
|
|
item.querySelector('input[type="radio"]').checked = true;
|
|
}
|
|
});
|
|
|
|
// 옵션 패널 표시/숨김
|
|
document.getElementById('briefingOptions').style.display = (type === 'briefing') ? 'block' : 'none';
|
|
document.getElementById('reportOptions').style.display = (type === 'report') ? 'block' : 'none';
|
|
}
|
|
|
|
// ===== 템플릿 추가 =====
|
|
function addTemplate() {
|
|
alert('템플릿 추가 기능은 준비중입니다.\n\n향후 사용자 정의 양식을 추가할 수 있습니다.');
|
|
}
|
|
|
|
// ===== 페이지 옵션 선택 =====
|
|
function selectPageOption(option) {
|
|
currentPageOption = option;
|
|
document.querySelectorAll('.option-item').forEach(item => {
|
|
item.classList.remove('selected');
|
|
});
|
|
event.currentTarget.classList.add('selected');
|
|
document.querySelector(`input[value="${option}"]`).checked = true;
|
|
}
|
|
|
|
// ===== 생성 =====
|
|
async function generate() {
|
|
if (currentDocType === 'briefing') {
|
|
await generateBriefing();
|
|
} else if (currentDocType === 'report') {
|
|
await generateReport();
|
|
}
|
|
}
|
|
|
|
// ===== 기획서 생성 (기존 로직) =====
|
|
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');
|
|
const sideBtn = document.getElementById('generateBtnSide');
|
|
|
|
btn.disabled = true;
|
|
sideBtn.disabled = true;
|
|
btnText.textContent = '생성 중...';
|
|
spinner.style.display = 'block';
|
|
resetSteps();
|
|
updateStep(0, 'done'); // 참고 파일 확인 완료
|
|
setStatus('생성 중...', true);
|
|
|
|
try {
|
|
// Step 1~7 시뮬레이션 (향후 Engine 연동 시 실제 진행)
|
|
for (let i = 1; i <= 7; i++) {
|
|
updateStep(i, 'running');
|
|
await new Promise(r => setTimeout(r, 200));
|
|
updateStep(i, 'done');
|
|
}
|
|
|
|
// Step 8: 콘텐츠 생성
|
|
updateStep(8, 'running');
|
|
|
|
const formData = new FormData();
|
|
formData.append('content', inputContent);
|
|
formData.append('page_option', currentPageOption);
|
|
formData.append('instruction', document.getElementById('instructionInput').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');
|
|
|
|
// Step 9: HTML 변환
|
|
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;
|
|
sideBtn.disabled = false;
|
|
btnText.textContent = '🚀 생성하기';
|
|
spinner.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// ===== 보고서 생성 (새로 추가) =====
|
|
async function generateReport() {
|
|
if (!folderPath && !inputContent) {
|
|
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();
|
|
|
|
// 체크박스 값 수집
|
|
const options = {
|
|
content: inputContent, // ← 추가!
|
|
folder_path: folderPath,
|
|
cover: document.getElementById('reportCover').checked,
|
|
toc: document.getElementById('reportToc').checked,
|
|
divider: document.getElementById('reportDivider').checked,
|
|
instruction: document.getElementById('reportInstructionInput').value
|
|
};
|
|
|
|
setStatus('보고서 생성 중...', true);
|
|
|
|
try {
|
|
// Step 1~9 진행 표시
|
|
for (let i = 1; i <= 9; i++) {
|
|
updateStep(i, 'running');
|
|
await new Promise(r => setTimeout(r, 500));
|
|
// TODO: 실제 API 호출
|
|
updateStep(i, 'done');
|
|
}
|
|
|
|
const response = await fetch('/generate-report', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
content: inputContent, // HTML 내용 추가
|
|
folder_path: folderPath,
|
|
cover: document.getElementById('reportCover').checked,
|
|
toc: document.getElementById('reportToc').checked,
|
|
divider: document.getElementById('reportDivider').checked,
|
|
instruction: document.getElementById('reportInstructionInput').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);
|
|
}
|
|
|
|
} catch (error) {
|
|
alert('생성 오류: ' + error.message);
|
|
setStatus('오류 발생', false);
|
|
} finally {
|
|
btn.disabled = false;
|
|
btnText.textContent = '🚀 생성하기';
|
|
spinner.style.display = 'none';
|
|
}
|
|
}
|
|
|
|
// ===== 피드백 수정 =====
|
|
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);
|
|
}
|
|
|
|
// ===== HWP 다운로드 (스타일 그루핑) =====
|
|
async function downloadHwpStyled() {
|
|
if (!generatedHTML) {
|
|
alert('먼저 문서를 생성해주세요.');
|
|
return;
|
|
}
|
|
|
|
// 편집된 내용 가져오기
|
|
const frame = document.getElementById('previewFrame');
|
|
const html = frame.contentDocument ?
|
|
'<!DOCTYPE html>' + frame.contentDocument.documentElement.outerHTML :
|
|
generatedHTML;
|
|
|
|
try {
|
|
setStatus('HWP 변환 중...', true);
|
|
|
|
const response = await fetch('/export-hwp', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({
|
|
html: html,
|
|
doc_type: currentDocType || 'report',
|
|
style_grouping: true // ★ 스타일 그루핑 활성화
|
|
})
|
|
});
|
|
|
|
if (!response.ok) {
|
|
const err = await response.json();
|
|
throw new Error(err.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);
|
|
}
|
|
}
|
|
|
|
// ===== 스타일 분석 미리보기 (선택사항) =====
|
|
async function analyzeStyles() {
|
|
if (!generatedHTML) {
|
|
alert('먼저 문서를 생성해주세요.');
|
|
return;
|
|
}
|
|
|
|
const frame = document.getElementById('previewFrame');
|
|
const html = frame.contentDocument ?
|
|
'<!DOCTYPE html>' + frame.contentDocument.documentElement.outerHTML :
|
|
generatedHTML;
|
|
|
|
try {
|
|
const response = await fetch('/analyze-styles', {
|
|
method: 'POST',
|
|
headers: { 'Content-Type': 'application/json' },
|
|
body: JSON.stringify({ html: html })
|
|
});
|
|
|
|
const data = await response.json();
|
|
|
|
if (data.error) {
|
|
throw new Error(data.error);
|
|
}
|
|
|
|
// 결과 표시
|
|
let summary = `📊 스타일 분석 결과\n\n총 ${data.total_elements}개 요소\n\n`;
|
|
summary += Object.entries(data.summary)
|
|
.map(([k, v]) => `${k}: ${v}개`)
|
|
.join('\n');
|
|
|
|
alert(summary);
|
|
console.log('스타일 분석 상세:', data);
|
|
|
|
} catch (error) {
|
|
alert('분석 오류: ' + error.message);
|
|
}
|
|
}
|
|
|
|
|
|
function printDoc() {
|
|
const frame = document.getElementById('previewFrame');
|
|
if (frame.contentWindow) {
|
|
frame.contentWindow.print();
|
|
}
|
|
}
|
|
|
|
// ===== 초기화 =====
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
setStatus('준비됨', false);
|
|
|
|
// 문서 유형 호버 프리뷰
|
|
document.querySelectorAll('.doc-type-item').forEach(item => {
|
|
const preview = item.querySelector('.doc-type-preview');
|
|
if (!preview) return;
|
|
|
|
item.addEventListener('mouseenter', (e) => {
|
|
const rect = item.getBoundingClientRect();
|
|
// 프리뷰를 아이템 왼쪽에 배치
|
|
preview.style.top = (rect.top + rect.height / 2 - 150) + 'px';
|
|
preview.style.left = (rect.left - 295) + 'px';
|
|
preview.classList.add('show');
|
|
});
|
|
|
|
item.addEventListener('mouseleave', () => {
|
|
preview.classList.remove('show');
|
|
});
|
|
});
|
|
});
|
|
|
|
// Enter 키로 피드백 제출
|
|
document.getElementById('feedbackInput').addEventListener('keypress', function(e) {
|
|
if (e.key === 'Enter') {
|
|
submitFeedback();
|
|
}
|
|
});
|
|
</script>
|
|
|
|
<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>
|
|
<script src="/static/js/editor.js"></script>
|
|
</body>
|
|
</html> |