ERP-node/레포트드자이너.html

1235 lines
38 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="ko">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>리포트 디자이너</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "Malgun Gothic", "맑은 고딕", sans-serif;
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
}
.top-toolbar {
background-color: #2c3e50;
color: white;
padding: 12px 20px;
display: flex;
align-items: center;
gap: 15px;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.1);
}
.top-toolbar h1 {
font-size: 18px;
margin-right: 30px;
}
.toolbar-btn {
padding: 8px 16px;
background-color: #3498db;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
transition: background-color 0.2s;
}
.toolbar-btn:hover {
background-color: #2980b9;
}
.toolbar-btn.save {
background-color: #27ae60;
}
.toolbar-btn.save:hover {
background-color: #229954;
}
.main-container {
display: flex;
flex: 1;
overflow: hidden;
}
.left-panel {
width: 220px;
background-color: #f8f9fa;
border-right: 1px solid #ddd;
overflow-y: auto;
padding: 15px;
}
.panel-section {
margin-bottom: 25px;
}
.panel-section h3 {
font-size: 14px;
color: #2c3e50;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 2px solid #3498db;
}
.template-item,
.component-item {
padding: 10px;
margin: 8px 0;
background-color: white;
border: 1px solid #ddd;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
transition: all 0.2s;
}
.template-item:hover,
.component-item:hover {
background-color: #e8f4f8;
border-color: #3498db;
transform: translateX(3px);
}
.component-item {
cursor: move;
}
.work-area {
flex: 1;
background-color: #ecf0f1;
position: relative;
overflow: auto;
padding: 20px;
}
.canvas-container {
background-color: white;
min-height: 800px;
width: 100%;
max-width: 210mm;
margin: 0 auto;
box-shadow: 0 0 20px rgba(0, 0, 0, 0.1);
position: relative;
padding: 20px;
}
.canvas-container.drag-over {
border: 2px dashed #3498db;
background-color: #f0f8ff;
}
.work-area-title {
text-align: center;
padding: 10px;
background-color: #fff;
margin: -20px -20px 20px -20px;
border-bottom: 2px solid #3498db;
font-weight: bold;
color: #2c3e50;
}
.placed-component {
position: absolute;
background-color: white;
border: 2px solid #666;
border-radius: 5px;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
min-width: 100px;
min-height: 50px;
cursor: move;
}
.placed-component.selected {
border-color: #3498db;
box-shadow: 0 0 10px rgba(52, 152, 219, 0.3);
}
.component-label {
font-size: 11px;
color: #7f8c8d;
margin-bottom: 5px;
}
.component-content {
font-size: 13px;
color: #2c3e50;
}
.resize-handle {
position: absolute;
width: 10px;
height: 10px;
background-color: #4caf50;
border: 1px solid #fff;
}
.resize-handle.se {
bottom: -5px;
right: -5px;
cursor: se-resize;
}
.resize-handle.ne {
top: -5px;
right: -5px;
cursor: ne-resize;
}
.resize-handle.sw {
bottom: -5px;
left: -5px;
cursor: sw-resize;
}
.resize-handle.nw {
top: -5px;
left: -5px;
cursor: nw-resize;
}
.right-panel {
width: 400px;
background-color: #fff;
border-left: 1px solid #ddd;
overflow-y: auto;
padding: 20px;
}
.property-section {
margin-bottom: 25px;
}
.property-section h3 {
font-size: 14px;
color: #2c3e50;
margin-bottom: 12px;
padding-bottom: 8px;
border-bottom: 2px solid #e74c3c;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
font-size: 12px;
color: #555;
margin-bottom: 5px;
font-weight: bold;
}
.form-group input[type="text"],
.form-group select {
width: 100%;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
font-size: 13px;
font-family: inherit;
}
/* 쿼리 카드 스타일 - 각 쿼리를 카드 형태로 표시합니다 */
.query-card {
background-color: #f8f9fa;
border: 2px solid #ddd;
border-radius: 8px;
padding: 15px;
margin-bottom: 15px;
position: relative;
}
.query-card.master {
border-left: 4px solid #3498db;
}
.query-card.detail {
border-left: 4px solid #e67e22;
}
.query-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.query-name {
font-weight: bold;
font-size: 13px;
color: #2c3e50;
}
.query-type-badge {
padding: 3px 8px;
border-radius: 3px;
font-size: 11px;
font-weight: bold;
}
.query-type-badge.master {
background-color: #3498db;
color: white;
}
.query-type-badge.detail {
background-color: #e67e22;
color: white;
}
.query-delete-btn {
background: none;
border: none;
color: #e74c3c;
cursor: pointer;
font-size: 18px;
padding: 0 5px;
}
.query-delete-btn:hover {
color: #c0392b;
}
.query-textarea {
width: 100%;
min-height: 100px;
padding: 8px;
border: 1px solid #ddd;
border-radius: 4px;
background-color: #2c3e50;
color: #2ecc71;
font-family: "Courier New", monospace;
font-size: 12px;
resize: vertical;
}
.parameter-section {
display: none;
margin-top: 10px;
background-color: #fff3cd;
padding: 10px;
border-radius: 4px;
border: 1px solid #ffc107;
}
.parameter-section.show {
display: block;
}
.parameter-field {
margin-bottom: 8px;
}
.parameter-field label {
display: block;
font-size: 11px;
color: #856404;
margin-bottom: 3px;
font-weight: bold;
}
.parameter-field input,
.parameter-field select {
padding: 5px 8px;
border: 1px solid #ffc107;
border-radius: 3px;
font-size: 12px;
background-color: white;
}
.param-type-select {
width: 80px;
}
.param-value-input {
flex: 1;
}
.add-query-btn {
width: 100%;
padding: 10px;
background-color: #3498db;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
margin-bottom: 15px;
}
.add-query-btn:hover {
background-color: #2980b9;
}
.execute-btn {
width: 100%;
padding: 8px;
background-color: #e74c3c;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 12px;
margin-top: 8px;
}
.execute-btn:hover {
background-color: #c0392b;
}
.result-area {
margin-top: 10px;
padding: 10px;
background-color: #e8f8f5;
border-radius: 4px;
border: 1px solid #16a085;
}
.result-fields {
display: flex;
gap: 5px;
flex-wrap: wrap;
margin-top: 8px;
}
.field-chip {
padding: 4px 10px;
background-color: #16a085;
color: white;
border-radius: 12px;
font-size: 11px;
cursor: pointer;
}
.field-chip:hover {
background-color: #138d75;
}
/* URL 파라미터 정보 표시 영역 */
.url-params-info {
background-color: #d5f4e6;
padding: 12px;
border-radius: 4px;
border-left: 4px solid #27ae60;
font-size: 12px;
margin-top: 10px;
}
.url-params-info strong {
display: block;
margin-bottom: 8px;
color: #27ae60;
}
.url-params-info code {
display: block;
background-color: #fff;
padding: 8px;
border-radius: 3px;
margin-top: 5px;
font-family: "Courier New", monospace;
font-size: 11px;
word-break: break-all;
}
.info-text {
font-size: 11px;
color: #7f8c8d;
margin-top: 10px;
line-height: 1.5;
}
.modal {
display: none;
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background-color: rgba(0, 0, 0, 0.7);
z-index: 1000;
justify-content: center;
align-items: center;
}
.modal.active {
display: flex;
}
.modal-content {
background-color: white;
width: 90%;
max-width: 900px;
max-height: 90%;
border-radius: 8px;
overflow: hidden;
display: flex;
flex-direction: column;
}
.modal-header {
background-color: #2c3e50;
color: white;
padding: 15px 20px;
display: flex;
justify-content: space-between;
align-items: center;
}
.modal-header h2 {
font-size: 18px;
}
.modal-close {
background: none;
border: none;
color: white;
font-size: 24px;
cursor: pointer;
}
.modal-body {
padding: 20px;
overflow-y: auto;
flex: 1;
}
.modal-footer {
padding: 15px 20px;
background-color: #f8f9fa;
border-top: 1px solid #ddd;
display: flex;
gap: 10px;
justify-content: flex-end;
}
.export-btn {
padding: 10px 20px;
border: none;
border-radius: 4px;
cursor: pointer;
font-size: 13px;
}
.export-btn.pdf {
background-color: #e74c3c;
color: white;
}
.export-btn.word {
background-color: #3498db;
color: white;
}
</style>
</head>
<body>
<div class="top-toolbar">
<h1>📄 리포트 디자이너</h1>
<button class="toolbar-btn save" onclick="saveReport()">💾 저장</button>
<button class="toolbar-btn" onclick="showPreview()">👁 미리보기</button>
<button class="toolbar-btn" onclick="clearCanvas()">🗑 초기화</button>
</div>
<div class="main-container">
<div class="left-panel">
<div class="panel-section">
<h3>기본 템플릿</h3>
<div class="template-item" onclick="applyTemplate('order')">
📋 발주서
</div>
<div class="template-item" onclick="applyTemplate('invoice')">
💰 청구서
</div>
<div class="template-item" onclick="applyTemplate('basic')">
📄 기본
</div>
</div>
<div class="panel-section">
<h3>컴포넌트</h3>
<div class="component-item" draggable="true" data-type="text">
📝 텍스트
</div>
<div class="component-item" draggable="true" data-type="table">
📊 테이블
</div>
<div class="component-item" draggable="true" data-type="label">
🏷 레이블
</div>
</div>
</div>
<div class="work-area">
<div id="canvas" class="canvas-container">
<div class="work-area-title">작업 영역</div>
<p style="text-align: center; color: #95a5a6; margin-top: 100px">
왼쪽에서 컴포넌트를 드래그하거나 템플릿을 선택하세요
</p>
</div>
</div>
<div class="right-panel">
<div class="property-section">
<h3>입력창</h3>
<div class="form-group">
<label>레포트 제목</label>
<input
type="text"
id="report-title"
placeholder="리포트 제목을 입력하세요"
/>
</div>
</div>
<div class="property-section">
<h3>쿼리 관리</h3>
<button class="add-query-btn" onclick="addQuery()">
쿼리 추가
</button>
<div id="queries-container">
<!-- 쿼리 카드들이 여기에 동적으로 추가됩니다 -->
</div>
<div class="info-text">
💡 <strong>마스터 쿼리</strong>는 1건의 데이터를 가져오고,
<strong>디테일 쿼리</strong>는 여러 건의 데이터를 반복 표시합니다.
발주서의 경우 상단 헤더 정보는 마스터, 하단 품목 리스트는 디테일로
설정하세요.
</div>
</div>
<div class="property-section">
<h3>외부 호출 정보</h3>
<div class="url-params-info">
<strong>🔗 URL로 파라미터 전달 방법</strong>
<div id="url-example">
다른 프로그램에서 이 리포트를 호출할 때는 URL에 파라미터를
추가하세요.
<code id="url-sample">report.html?$1=admin&$2=2020-12</code>
</div>
</div>
</div>
</div>
</div>
<div id="previewModal" class="modal">
<div class="modal-content">
<div class="modal-header">
<h2>미리보기</h2>
<button class="modal-close" onclick="closePreview()">×</button>
</div>
<div class="modal-body" id="previewContent"></div>
<div class="modal-footer">
<button class="export-btn pdf" onclick="exportPDF()">📑 PDF</button>
<button class="export-btn word" onclick="exportWord()">
📘 WORD
</button>
</div>
</div>
</div>
<script>
let draggedComponent = null;
let selectedElement = null;
let isDragging = false;
let isResizing = false;
let currentHandle = null;
let startX, startY, startWidth, startHeight, startLeft, startTop;
let componentCounter = 0;
let queryCounter = 0; // 각 쿼리에 고유 ID를 부여하기 위한 카운터
let queries = {}; // 쿼리 정보를 저장하는 객체
const canvas = document.getElementById("canvas");
// 페이지 로드 시 URL 파라미터를 읽어옵니다
window.addEventListener("load", function () {
loadUrlParameters();
});
// URL에서 파라미터를 읽어서 자동으로 채워주는 함수
function loadUrlParameters() {
const urlParams = new URLSearchParams(window.location.search);
const params = {};
// URL의 모든 파라미터를 객체로 변환합니다
for (const [key, value] of urlParams) {
params[key] = value;
console.log("URL 파라미터 로드:", key, "=", value);
}
// 파라미터가 있으면 알림을 표시합니다
if (Object.keys(params).length > 0) {
alert("URL에서 파라미터를 불러왔습니다: " + JSON.stringify(params));
}
return params;
}
// 컴포넌트 드래그 앤 드롭 설정
document.querySelectorAll(".component-item").forEach((item) => {
item.addEventListener("dragstart", handleDragStart);
});
canvas.addEventListener("dragover", handleDragOver);
canvas.addEventListener("drop", handleDrop);
canvas.addEventListener("dragleave", handleDragLeave);
function handleDragStart(e) {
draggedComponent = e.target;
e.dataTransfer.effectAllowed = "copy";
e.dataTransfer.setData("type", e.target.dataset.type);
}
function handleDragOver(e) {
e.preventDefault();
e.dataTransfer.dropEffect = "copy";
canvas.classList.add("drag-over");
}
function handleDragLeave(e) {
if (e.target === canvas) {
canvas.classList.remove("drag-over");
}
}
function handleDrop(e) {
e.preventDefault();
canvas.classList.remove("drag-over");
const type = e.dataTransfer.getData("type");
const rect = canvas.getBoundingClientRect();
const x = e.clientX - rect.left + canvas.scrollLeft;
const y = e.clientY - rect.top + canvas.scrollTop;
createElement(type, x, y);
}
// 캔버스에 컴포넌트를 생성하는 함수
function createElement(type, x, y, config) {
const element = document.createElement("div");
element.className = "placed-component";
element.style.left = x + "px";
element.style.top = y + "px";
element.style.width = config && config.width ? config.width : "200px";
element.style.height =
config && config.height ? config.height : "100px";
element.dataset.id = ++componentCounter;
element.dataset.type = type;
// 컴포넌트와 연결된 쿼리 ID를 저장할 수 있습니다
if (config && config.queryId) {
element.dataset.queryId = config.queryId;
}
let label = "";
let content = "";
switch (type) {
case "text":
label = "텍스트 필드";
content =
'<input type="text" style="width: 100%; padding: 5px;" placeholder="텍스트 입력">';
break;
case "label":
label = "레이블";
content =
'<div style="padding: 5px; font-weight: bold;">레이블 텍스트</div>';
break;
case "table":
label = "테이블 (디테일 데이터)";
content =
'<table style="width: 100%; border-collapse: collapse; font-size: 11px;"><thead><tr><th style="border: 1px solid #ddd; padding: 5px; background: #f5f5f5;">품목명</th><th style="border: 1px solid #ddd; padding: 5px; background: #f5f5f5;">수량</th><th style="border: 1px solid #ddd; padding: 5px; background: #f5f5f5;">단가</th></tr></thead><tbody><tr><td style="border: 1px solid #ddd; padding: 5px;">품목1</td><td style="border: 1px solid #ddd; padding: 5px;">10</td><td style="border: 1px solid #ddd; padding: 5px;">50,000</td></tr><tr><td style="border: 1px solid #ddd; padding: 5px;">품목2</td><td style="border: 1px solid #ddd; padding: 5px;">5</td><td style="border: 1px solid #ddd; padding: 5px;">30,000</td></tr></tbody></table>';
element.style.height = "200px";
break;
}
element.innerHTML =
'<div class="component-label">' +
label +
'</div><div class="component-content">' +
content +
'</div><div class="resize-handle nw"></div><div class="resize-handle ne"></div><div class="resize-handle sw"></div><div class="resize-handle se"></div>';
canvas.appendChild(element);
addComponentListeners(element);
}
function addComponentListeners(component) {
component.addEventListener("mousedown", function (e) {
if (e.target.classList.contains("resize-handle")) {
isResizing = true;
currentHandle = e.target.classList[1];
selectedElement = component;
startX = e.clientX;
startY = e.clientY;
startWidth = component.offsetWidth;
startHeight = component.offsetHeight;
startLeft = component.offsetLeft;
startTop = component.offsetTop;
e.stopPropagation();
e.preventDefault();
return;
}
isDragging = true;
selectedElement = component;
document.querySelectorAll(".placed-component").forEach((el) => {
el.classList.remove("selected");
});
component.classList.add("selected");
startX = e.clientX;
startY = e.clientY;
startLeft = component.offsetLeft;
startTop = component.offsetTop;
e.preventDefault();
});
}
document.addEventListener("mousemove", function (e) {
if (isDragging && selectedElement) {
const dx = e.clientX - startX;
const dy = e.clientY - startY;
selectedElement.style.left = startLeft + dx + "px";
selectedElement.style.top = startTop + dy + "px";
} else if (isResizing && selectedElement) {
const dx = e.clientX - startX;
const dy = e.clientY - startY;
switch (currentHandle) {
case "se":
selectedElement.style.width =
Math.max(100, startWidth + dx) + "px";
selectedElement.style.height =
Math.max(50, startHeight + dy) + "px";
break;
case "sw":
selectedElement.style.width =
Math.max(100, startWidth - dx) + "px";
selectedElement.style.height =
Math.max(50, startHeight + dy) + "px";
selectedElement.style.left = startLeft + dx + "px";
break;
case "ne":
selectedElement.style.width =
Math.max(100, startWidth + dx) + "px";
selectedElement.style.height =
Math.max(50, startHeight - dy) + "px";
selectedElement.style.top = startTop + dy + "px";
break;
case "nw":
selectedElement.style.width =
Math.max(100, startWidth - dx) + "px";
selectedElement.style.height =
Math.max(50, startHeight - dy) + "px";
selectedElement.style.left = startLeft + dx + "px";
selectedElement.style.top = startTop + dy + "px";
break;
}
}
});
document.addEventListener("mouseup", function () {
isDragging = false;
isResizing = false;
currentHandle = null;
});
// 새로운 쿼리를 추가하는 함수
function addQuery(config) {
const queryId = "query_" + ++queryCounter;
const container = document.getElementById("queries-container");
// 쿼리 정보를 저장합니다
queries[queryId] = {
name: config && config.name ? config.name : "쿼리 " + queryCounter,
type: config && config.type ? config.type : "master",
sql: config && config.sql ? config.sql : "",
parameters: {},
};
// 쿼리 카드 HTML을 생성합니다
const card = document.createElement("div");
card.className = "query-card " + queries[queryId].type;
card.id = queryId;
card.innerHTML =
'<div class="query-header"><div><span class="query-name">' +
queries[queryId].name +
'</span> <span class="query-type-badge ' +
queries[queryId].type +
'">' +
(queries[queryId].type === "master" ? "MASTER" : "DETAIL") +
'</span></div><button class="query-delete-btn" onclick="deleteQuery(\'' +
queryId +
'\')">×</button></div><div class="form-group"><label>쿼리명</label><input type="text" value="' +
queries[queryId].name +
'" onchange="updateQueryName(\'' +
queryId +
'\', this.value)"></div><div class="form-group"><label>쿼리 타입</label><select onchange="updateQueryType(\'' +
queryId +
'\', this.value)"><option value="master" ' +
(queries[queryId].type === "master" ? "selected" : "") +
'>마스터 (1건)</option><option value="detail" ' +
(queries[queryId].type === "detail" ? "selected" : "") +
'>디테일 (N건)</option></select></div><textarea class="query-textarea" placeholder="SELECT * FROM table WHERE id = $1" onchange="updateQuerySql(\'' +
queryId +
"', this.value)\">" +
queries[queryId].sql +
'</textarea><div class="parameter-section" id="params-' +
queryId +
'"></div><button class="execute-btn" onclick="executeQuery(\'' +
queryId +
'\')">🚀 실행</button><div class="result-area" id="result-' +
queryId +
'" style="display: none;"><strong style="font-size: 11px;">결과 필드</strong><div class="result-fields"></div></div>';
container.appendChild(card);
// 쿼리 텍스트가 변경될 때마다 파라미터를 감지합니다
const textarea = card.querySelector(".query-textarea");
textarea.addEventListener("input", function () {
detectQueryParameters(queryId);
});
// 초기 SQL이 있으면 파라미터를 감지합니다
if (queries[queryId].sql) {
detectQueryParameters(queryId);
}
}
// 쿼리 이름을 업데이트하는 함수
function updateQueryName(queryId, name) {
queries[queryId].name = name;
const nameSpan = document.querySelector("#" + queryId + " .query-name");
nameSpan.textContent = name;
}
// 쿼리 타입을 업데이트하는 함수
function updateQueryType(queryId, type) {
queries[queryId].type = type;
const card = document.getElementById(queryId);
card.className = "query-card " + type;
const badge = card.querySelector(".query-type-badge");
badge.className = "query-type-badge " + type;
badge.textContent = type === "master" ? "MASTER" : "DETAIL";
}
// 쿼리 SQL을 업데이트하는 함수
function updateQuerySql(queryId, sql) {
queries[queryId].sql = sql;
}
// 쿼리를 삭제하는 함수
function deleteQuery(queryId) {
if (confirm("이 쿼리를 삭제하시겠습니까?")) {
document.getElementById(queryId).remove();
delete queries[queryId];
}
}
// 쿼리에서 파라미터를 감지하는 함수
function detectQueryParameters(queryId) {
const card = document.getElementById(queryId);
const textarea = card.querySelector(".query-textarea");
const sql = textarea.value;
const paramSection = document.getElementById("params-" + queryId);
const regex = /\$\d+/g;
const matches = sql.match(regex);
if (matches && matches.length > 0) {
const uniqueParams = Array.from(new Set(matches));
uniqueParams.sort(function (a, b) {
return parseInt(a.substring(1)) - parseInt(b.substring(1));
});
paramSection.classList.add("show");
paramSection.innerHTML =
'<strong style="font-size: 11px; color: #856404; display: block; margin-bottom: 8px;">📎 파라미터</strong>';
uniqueParams.forEach(function (param) {
const paramNum = param.substring(1);
const fieldDiv = document.createElement("div");
fieldDiv.className = "parameter-field";
fieldDiv.style.display = "flex";
fieldDiv.style.gap = "5px";
fieldDiv.style.alignItems = "center";
const label = document.createElement("div");
label.textContent = param;
label.style.minWidth = "35px";
label.style.fontWeight = "bold";
label.style.fontSize = "11px";
const select = document.createElement("select");
select.className = "param-type-select";
select.innerHTML =
'<option value="text">텍스트</option><option value="number">숫자</option><option value="date">날짜</option>';
const input = document.createElement("input");
input.type = "text";
input.className = "param-value-input";
input.placeholder = "값";
input.dataset.param = param;
input.dataset.queryId = queryId;
// URL 파라미터가 있으면 자동으로 채웁니다
const urlParams = loadUrlParameters();
if (urlParams[param]) {
input.value = urlParams[param];
}
select.addEventListener("change", function () {
input.type = select.value;
});
fieldDiv.appendChild(label);
fieldDiv.appendChild(select);
fieldDiv.appendChild(input);
paramSection.appendChild(fieldDiv);
});
updateUrlExample();
} else {
paramSection.classList.remove("show");
}
}
// URL 예시를 업데이트하는 함수
function updateUrlExample() {
const allParams = [];
// 모든 쿼리의 파라미터를 수집합니다
Object.keys(queries).forEach(function (queryId) {
const paramSection = document.getElementById("params-" + queryId);
if (paramSection) {
const inputs = paramSection.querySelectorAll("input[data-param]");
inputs.forEach(function (input) {
const param = input.dataset.param;
if (!allParams.includes(param)) {
allParams.push(param);
}
});
}
});
// URL 예시를 생성합니다
if (allParams.length > 0) {
allParams.sort(function (a, b) {
return parseInt(a.substring(1)) - parseInt(b.substring(1));
});
const paramString = allParams
.map(function (p) {
return p + "=값";
})
.join("&");
document.getElementById("url-sample").textContent =
window.location.pathname + "?" + paramString;
}
}
// 쿼리를 실행하는 함수 (시뮬레이션)
function executeQuery(queryId) {
const query = queries[queryId];
let finalSql = query.sql;
// 파라미터를 실제 값으로 치환합니다
const paramSection = document.getElementById("params-" + queryId);
const inputs = paramSection.querySelectorAll("input[data-param]");
let allFilled = true;
inputs.forEach(function (input) {
const param = input.dataset.param;
const value = input.value;
const type = input.parentElement.querySelector("select").value;
if (value) {
let formattedValue = value;
if (type === "text" || type === "date") {
formattedValue = "'" + value + "'";
}
finalSql = finalSql.split(param).join(formattedValue);
} else {
allFilled = false;
}
});
if (!allFilled) {
alert("모든 파라미터 값을 입력해주세요!");
return;
}
console.log("[" + query.name + "] 실행 쿼리:", finalSql);
// 결과 영역에 샘플 필드를 표시합니다
const resultArea = document.getElementById("result-" + queryId);
resultArea.style.display = "block";
const fieldsDiv = resultArea.querySelector(".result-fields");
// 샘플 필드를 생성합니다
if (query.type === "master") {
fieldsDiv.innerHTML =
'<div class="field-chip">order_no</div><div class="field-chip">order_date</div><div class="field-chip">supplier</div>';
} else {
fieldsDiv.innerHTML =
'<div class="field-chip">item_name</div><div class="field-chip">quantity</div><div class="field-chip">price</div>';
}
alert("쿼리가 실행되었습니다!\n콘솔에서 확인하세요.");
}
// 템플릿을 적용하는 함수
function applyTemplate(templateType) {
clearCanvas();
switch (templateType) {
case "order":
// 발주서 템플릿: 마스터 쿼리와 디테일 쿼리를 모두 생성합니다
addQuery({
name: "발주 마스터",
type: "master",
sql: "SELECT order_no, order_date, supplier FROM purchase_order WHERE order_no = $1",
});
addQuery({
name: "발주 상세",
type: "detail",
sql: "SELECT item_name, quantity, price FROM purchase_order_detail WHERE order_no = $1",
});
// 캔버스에 컴포넌트를 배치합니다
createElement("label", 50, 80, { width: "150px", height: "40px" });
createElement("text", 210, 80, {
width: "300px",
height: "40px",
queryId: "query_1",
});
createElement("label", 50, 140, { width: "150px", height: "40px" });
createElement("text", 210, 140, {
width: "300px",
height: "40px",
queryId: "query_1",
});
createElement("table", 50, 220, {
width: "650px",
height: "300px",
queryId: "query_2",
});
break;
case "invoice":
// 청구서 템플릿
addQuery({
name: "청구 마스터",
type: "master",
sql: "SELECT invoice_no, invoice_date, customer FROM invoice WHERE invoice_no = $1",
});
addQuery({
name: "청구 항목",
type: "detail",
sql: "SELECT description, amount FROM invoice_items WHERE invoice_no = $1",
});
createElement("text", 100, 100);
createElement("table", 100, 250, {
width: "600px",
height: "250px",
});
break;
case "basic":
addQuery({
name: "기본 쿼리",
type: "master",
sql: "SELECT * FROM table WHERE id = $1",
});
createElement("text", 100, 100);
break;
}
}
// 파라미터 테스트 함수 - 실제 URL 파라미터를 시뮬레이션합니다
function testWithParams() {
const testUrl = window.location.pathname + "?$1=PO-2025-001&$2=2025-01";
alert(
"테스트 URL:\n" +
testUrl +
"\n\n이 URL로 페이지를 열면 파라미터가 자동으로 입력됩니다."
);
// 실제로 URL을 변경합니다
if (confirm("테스트 파라미터로 페이지를 새로고침하시겠습니까?")) {
window.location.href = testUrl;
}
}
function clearCanvas() {
const components = canvas.querySelectorAll(".placed-component");
components.forEach(function (comp) {
comp.remove();
});
}
function saveReport() {
const reportData = {
title: document.getElementById("report-title").value,
queries: queries,
components: [],
};
document.querySelectorAll(".placed-component").forEach(function (comp) {
reportData.components.push({
type: comp.dataset.type,
queryId: comp.dataset.queryId,
left: comp.style.left,
top: comp.style.top,
width: comp.style.width,
height: comp.style.height,
});
});
console.log("저장된 리포트:", JSON.stringify(reportData, null, 2));
alert("리포트가 저장되었습니다!\n콘솔에서 확인하세요.");
}
function showPreview() {
const modal = document.getElementById("previewModal");
const previewContent = document.getElementById("previewContent");
previewContent.innerHTML = canvas.innerHTML;
modal.classList.add("active");
}
function closePreview() {
document.getElementById("previewModal").classList.remove("active");
}
function exportPDF() {
alert("PDF 다운로드 기능이 실행됩니다.");
}
function exportWord() {
alert("WORD 다운로드 기능이 실행됩니다.");
}
document
.getElementById("previewModal")
.addEventListener("click", function (e) {
if (e.target === this) {
closePreview();
}
});
</script>
</body>
</html>