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

1235 lines
38 KiB
HTML
Raw Normal View History

2025-10-01 11:10:54 +09:00
<!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>