Files
vexplor/control.html

1434 lines
57 KiB
HTML

<!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: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
min-height: 100vh;
padding: 20px;
}
.container {
max-width: 1900px;
margin: 0 auto;
background: white;
border-radius: 15px;
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
overflow: hidden;
}
.header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
padding: 20px 30px;
text-align: center;
}
.header h1 {
font-size: 2.2em;
margin-bottom: 10px;
}
.header p {
opacity: 0.9;
font-size: 1.1em;
}
.main-content {
display: flex;
height: calc(100vh - 200px);
}
.sidebar {
width: 420px;
background: #f8f9fa;
border-right: 1px solid #e9ecef;
padding: 20px;
overflow-y: auto;
}
.canvas-area {
flex: 1;
position: relative;
background: #fff;
overflow: hidden;
}
.section {
margin-bottom: 25px;
}
.section h3 {
color: #495057;
margin-bottom: 15px;
font-size: 1.2em;
border-bottom: 2px solid #667eea;
padding-bottom: 5px;
}
.table-selector {
background: #f8f9fa;
padding: 15px;
border-radius: 8px;
border: 1px solid #e9ecef;
}
.form-group {
margin-bottom: 15px;
}
.form-group label {
display: block;
margin-bottom: 8px;
font-weight: 600;
color: #495057;
font-size: 14px;
}
.form-group select,
.form-group input {
width: 100%;
padding: 10px 12px;
border: 1px solid #ced4da;
border-radius: 4px;
font-size: 14px;
background: white;
}
.btn {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
color: white;
border: none;
padding: 10px 20px;
border-radius: 5px;
cursor: pointer;
font-size: 14px;
transition: all 0.3s ease;
width: 100%;
}
.btn:hover {
transform: translateY(-1px);
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.3);
}
.btn:disabled {
background: #6c757d;
cursor: not-allowed;
transform: none;
box-shadow: none;
}
.canvas {
width: 100%;
height: 100%;
position: relative;
background:
radial-gradient(circle at 25px 25px, lightgray 2px, transparent 0),
radial-gradient(circle at 75px 75px, lightgray 2px, transparent 0);
background-size: 100px 100px;
background-position: 0 0, 50px 50px;
}
.table-node {
position: absolute;
background: white;
border: 2px solid #28a745;
border-radius: 10px;
min-width: 320px;
max-width: 400px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
z-index: 10;
}
.table-node.dragging {
z-index: 1000;
transform: rotate(2deg);
}
.table-node-header {
background: linear-gradient(135deg, #28a745 0%, #20c997 100%);
color: white;
padding: 15px;
border-radius: 8px 8px 0 0;
cursor: move;
}
.table-node-title {
font-weight: bold;
font-size: 1.1em;
margin-bottom: 5px;
}
.table-node-id {
font-size: 0.85em;
opacity: 0.9;
margin-bottom: 8px;
}
.table-node-body {
padding: 15px;
}
.field-item {
font-size: 0.8em;
padding: 8px 10px;
margin: 3px 0;
border-radius: 4px;
cursor: pointer;
transition: all 0.2s;
border: 1px solid transparent;
position: relative;
display: flex;
justify-content: space-between;
align-items: center;
}
.field-item:hover {
background: #e3f2fd;
border-color: #2196f3;
}
.field-item.selected {
background: #fff3cd;
border-color: #ffc107;
font-weight: bold;
}
.field-item.primary-key {
color: #007bff;
font-weight: bold;
}
.field-item.foreign-key {
color: #28a745;
font-weight: bold;
}
/* 제어 설정 모달 */
.control-setup-modal {
position: fixed;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: rgba(0,0,0,0.7);
display: none;
justify-content: center;
align-items: center;
z-index: 1000;
}
.modal-content {
background: white;
padding: 30px;
border-radius: 15px;
width: 800px;
max-width: 95%;
max-height: 90vh;
overflow-y: auto;
}
.modal-content h3 {
margin-bottom: 20px;
color: #495057;
text-align: center;
}
.control-setup-form {
display: grid;
gap: 20px;
}
.form-row {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 15px;
}
/* 제어 조건 설정 */
.control-conditions {
background: #f8f9fa;
padding: 20px;
border-radius: 8px;
border-left: 4px solid #007bff;
}
.condition-row {
display: flex;
gap: 10px;
margin: 10px 0;
align-items: center;
background: white;
padding: 10px;
border-radius: 5px;
}
.condition-row select {
flex: 1;
padding: 8px;
border: 1px solid #ced4da;
border-radius: 4px;
}
.condition-row input {
flex: 1;
padding: 8px;
border: 1px solid #ced4da;
border-radius: 4px;
}
/* 제어 액션 설정 */
.control-actions {
background: #e8f5e8;
padding: 20px;
border-radius: 8px;
border-left: 4px solid #28a745;
}
.action-type-options {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(150px, 1fr));
gap: 10px;
margin: 15px 0;
}
.action-type-option {
padding: 15px;
border: 2px solid #e9ecef;
border-radius: 8px;
cursor: pointer;
text-align: center;
transition: all 0.3s;
}
.action-type-option:hover {
border-color: #28a745;
background: #f8fff8;
}
.action-type-option.selected {
border-color: #28a745;
background: #e8f5e8;
}
.action-type-option .type-title {
font-weight: bold;
margin-bottom: 5px;
}
.action-type-option .type-desc {
font-size: 0.8em;
color: #6c757d;
}
/* 데이터 저장 설정 */
.data-save-settings {
display: none;
background: #f0f8ff;
padding: 20px;
border-radius: 8px;
margin: 15px 0;
}
.data-save-settings.show {
display: block;
}
.mapping-section {
background: white;
padding: 15px;
border-radius: 5px;
margin: 10px 0;
}
.mapping-row {
display: flex;
align-items: center;
gap: 10px;
margin: 8px 0;
}
.mapping-arrow {
font-weight: bold;
color: #007bff;
margin: 0 10px;
}
/* 외부 호출 설정 */
.external-call-settings {
display: none;
background: #fff8e1;
padding: 20px;
border-radius: 8px;
margin: 15px 0;
}
.external-call-settings.show {
display: block;
}
/* 연결선 */
.control-connection-line {
position: absolute;
height: 4px;
z-index: 8;
pointer-events: none;
border-radius: 2px;
background: linear-gradient(135deg, #ff6b6b 0%, #ee5a52 100%);
}
.control-connection-line::before,
.control-connection-line::after {
content: '';
position: absolute;
width: 8px;
height: 8px;
border-radius: 50%;
background: #ff6b6b;
top: -2px;
}
.control-connection-line::before {
left: -4px;
}
.control-connection-line::after {
right: -4px;
}
/* 제어 정보 패널 */
.controls-panel {
position: absolute;
top: 10px;
right: 10px;
width: 450px;
background: white;
border: 1px solid #e9ecef;
border-radius: 8px;
box-shadow: 0 5px 15px rgba(0,0,0,0.1);
max-height: 700px;
overflow-y: auto;
z-index: 20;
}
.controls-panel h4 {
background: #f8f9fa;
padding: 15px;
margin: 0;
border-bottom: 1px solid #e9ecef;
font-size: 1.1em;
}
.control-item {
padding: 15px;
border-bottom: 1px solid #e9ecef;
}
.control-item:last-child {
border-bottom: none;
}
.control-header {
display: flex;
justify-content: space-between;
align-items: center;
margin-bottom: 10px;
}
.control-name {
font-weight: bold;
color: #495057;
}
.control-details {
background: #f8f9fa;
padding: 12px;
border-radius: 6px;
margin: 10px 0;
font-size: 0.85em;
}
.btn-test {
background: #17a2b8;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 0.8em;
margin-right: 5px;
}
.btn-delete {
background: #dc3545;
color: white;
border: none;
padding: 6px 12px;
border-radius: 4px;
cursor: pointer;
font-size: 0.8em;
}
.toast {
position: fixed;
top: 20px;
right: 20px;
color: white;
padding: 15px 20px;
border-radius: 5px;
box-shadow: 0 5px 15px rgba(0,0,0,0.2);
z-index: 1001;
transform: translateX(400px);
transition: transform 0.3s ease;
}
.toast.show {
transform: translateX(0);
}
.toast.success {
background: #28a745;
}
.toast.error {
background: #dc3545;
}
.toast.info {
background: #17a2b8;
}
</style>
</head>
<body>
<div class="container">
<div class="header">
<h1>🎛️ 테이블 관계 및 제어관리 시스템</h1>
<p>테이블 간 관계를 설정하고 비즈니스 로직 제어를 구성하세요</p>
</div>
<div class="main-content">
<div class="sidebar">
<div class="section">
<h3>📊 테이블 추가</h3>
<div class="table-selector">
<div class="form-group">
<label for="tableSelect">사용 가능한 테이블</label>
<select id="tableSelect">
<option value="">테이블을 선택하세요</option>
<option value="order_mgmt">영업관리 (order_mgmt)</option>
<option value="project_mgmt">프로젝트관리 (project_mgmt)</option>
<option value="employees">사원관리 (employees)</option>
<option value="departments">부서관리 (departments)</option>
<option value="contracts">계약관리 (contracts)</option>
</select>
</div>
<button class="btn" id="addTableBtn" onclick="addSelectedTable()" disabled>테이블 추가</button>
</div>
</div>
<div class="section">
<h3>🔗 제어 연결 상태</h3>
<div id="controlStatus" style="background: #e8f4fd; padding: 15px; border-radius: 8px; border-left: 4px solid #007bff;">
<div style="font-weight: bold; margin-bottom: 5px;">제어 모드: OFF</div>
<div style="font-size: 0.9em; color: #495057;">테이블 필드를 클릭하면 제어 설정이 시작됩니다</div>
</div>
<button class="btn" onclick="cancelControl()" style="margin-top: 10px;">제어 취소</button>
</div>
<div class="section">
<h3>📈 제어 통계</h3>
<div id="controlStats" style="background: #f8f9fa; padding: 15px; border-radius: 8px;">
<div>데이터 저장 제어: <span id="dataSaveControlCount">0</span></div>
<div>외부 호출 제어: <span id="externalCallControlCount">0</span></div>
<div>조건부 제어: <span id="conditionalControlCount">0</span></div>
<div>총 제어 수: <span id="totalControlCount">0</span></div>
</div>
</div>
<div class="section">
<h3>⚙️ 관리</h3>
<button class="btn" onclick="clearCanvas()">캔버스 초기화</button>
<button class="btn" onclick="autoArrangeTable()" style="margin-top: 10px;">테이블 자동 정렬</button>
<button class="btn" onclick="loadSampleTables()" style="margin-top: 10px; background: #28a745;">샘플 테이블 로드</button>
</div>
</div>
<div class="canvas-area">
<div class="canvas" id="canvas"></div>
<div class="controls-panel">
<h4>🎛️ 제어 관리</h4>
<div id="controlsList">
<p style="padding: 15px; color: #6c757d; text-align: center;">생성된 제어가 없습니다.</p>
</div>
</div>
</div>
</div>
</div>
<!-- 제어 설정 모달 -->
<div class="control-setup-modal" id="controlModal">
<div class="modal-content">
<h3>테이블 제어 설정</h3>
<div class="control-setup-form">
<div class="form-group">
<label>제어 정보</label>
<div id="controlInfo" style="background: #f8f9fa; padding: 12px; border-radius: 6px; font-size: 0.9em;"></div>
</div>
<div class="form-row">
<div class="form-group">
<label>제어 이름</label>
<input type="text" id="controlName" placeholder="제어 이름을 입력하세요">
</div>
<div class="form-group">
<label>제어 설명</label>
<input type="text" id="controlDescription" placeholder="제어 설명을 입력하세요">
</div>
</div>
<!-- 제어 조건 설정 -->
<div class="control-conditions">
<h4>📋 제어 조건 추가버튼</h4>
<div id="conditionsContainer">
<div class="condition-row">
<select id="conditionField">
<option value="">필드 선택</option>
</select>
<select>
<option value="=">=</option>
<option value="!=">!=</option>
<option value=">">></option>
<option value="<"><</option>
<option value="LIKE">LIKE</option>
</select>
<input type="text" placeholder="값">
<select>
<option value="AND">AND</option>
<option value="OR">OR</option>
</select>
<button type="button" onclick="addConditionRow()">+</button>
</div>
</div>
</div>
<!-- 제어 액션 설정 -->
<div class="control-actions">
<h4>⚡ 제어 타입</h4>
<div class="action-type-options">
<div class="action-type-option" data-action="data-save">
<div class="type-title">데이터 저장</div>
<div class="type-desc">외부호출</div>
</div>
<div class="action-type-option" data-action="external-call">
<div class="type-title">외부 호출</div>
<div class="type-desc">API/이메일 호출</div>
</div>
<div class="action-type-option" data-action="data-process">
<div class="type-title">데이터 처리</div>
<div class="type-desc">관계 연결 클릭시 form테이블 to 테이블 관계 설정</div>
</div>
</div>
<!-- 데이터 저장 설정 -->
<div class="data-save-settings" id="dataSaveSettings">
<h5>💾 데이터 저장 상세 설정</h5>
<div class="form-group">
<label>수주제품의 데이터가 여러건일때 화면단에서 저장을 멀티셀렉트로 한다 같은 예로 컵퓨터,마우스,키보드 이렇게, 로 구분되어 들어감</label>
<textarea id="dataProcessRule" placeholder="데이터 분할 및 처리 규칙을 입력하세요" style="height: 60px;"></textarea>
</div>
<div class="form-group">
<label>저장을 여러번실행</label>
<select id="saveExecutionType">
<option value="multiple">저장을 여러번 실행</option>
<option value="batch">배치로 한번에 저장</option>
<option value="transaction">트랜잭션으로 모두 성공/실패</option>
</select>
</div>
<div class="mapping-section">
<h6>매핑정보(계약테이블)</h6>
<div class="mapping-row">
<input type="text" value="contract_no" readonly style="background: #f8f9fa;">
<span class="mapping-arrow">----------></span>
<input type="text" value="contract_no" readonly style="background: #f8f9fa;">
</div>
<div class="mapping-row">
<input type="text" placeholder="product">
<span class="mapping-arrow">----------></span>
<input type="text" placeholder="product">
</div>
<div class="mapping-row">
<input type="text" placeholder="pm_user_id">
<span class="mapping-arrow">----------></span>
<input type="text" placeholder="pm_user_id">
</div>
</div>
<div class="mapping-section">
<h6>매핑정보(프로젝트테이블)</h6>
<div class="mapping-row">
<input type="text" value="contract_no" readonly style="background: #f8f9fa;">
<span class="mapping-arrow">----------></span>
<input type="text" value="contract_no" readonly style="background: #f8f9fa;">
</div>
<div class="mapping-row">
<span>자동생성</span>
<span class="mapping-arrow">----------></span>
<input type="text" value="project_no(자동생성)" readonly style="background: #e7f3ff;">
</div>
<div class="mapping-row">
<input type="text" placeholder="product">
<span class="mapping-arrow">----------></span>
<input type="text" placeholder="product">
</div>
<div class="mapping-row">
<input type="text" placeholder="pm_user_id">
<span class="mapping-arrow">----------></span>
<input type="text" placeholder="pm_user_id">
</div>
</div>
<div class="form-group">
<label>저장 제어조건에 따른 저장 배열이 30이면 제품을 3개로 분할하여 프로젝트에 3건 저장</label>
<div style="display: flex; gap: 10px; align-items: center; background: white; padding: 10px; border-radius: 5px;">
<input type="number" placeholder="30" style="width: 80px;">
<span>개 항목을</span>
<input type="number" placeholder="3" style="width: 80px;">
<span>개로 분할하여</span>
<select style="width: 150px;">
<option value="project">프로젝트 테이블</option>
<option value="contract">계약 테이블</option>
</select>
<span></span>
<input type="number" placeholder="3" style="width: 80px;">
<span>건 저장</span>
</div>
</div>
</div>
<!-- 외부 호출 설정 -->
<div class="external-call-settings" id="externalCallSettings">
<h5>🌐 외부 호출 상세 설정</h5>
<div class="form-row">
<div class="form-group">
<label>호출 유형</label>
<select id="externalCallType">
<option value="email">이메일 발송</option>
<option value="api">API 호출</option>
<option value="sms">SMS 발송</option>
<option value="file">파일 생성</option>
</select>
</div>
<div class="form-group">
<label>호출 시점</label>
<select>
<option value="before">데이터 저장 전</option>
<option value="after">데이터 저장 후</option>
<option value="both">저장 전후</option>
</select>
</div>
</div>
<div class="form-group">
<label>호출 URL/설정</label>
<input type="text" id="externalCallUrl" placeholder="API URL 또는 이메일 설정">
</div>
<div class="form-group">
<label>호출 데이터 템플릿</label>
<textarea id="externalCallTemplate" placeholder="호출 시 전송할 데이터 템플릿" style="height: 80px;"></textarea>
</div>
</div>
</div>
<div style="display: flex; gap: 10px; margin-top: 20px;">
<button class="btn" onclick="confirmControl()" style="flex: 1; background: #28a745;">제어 생성</button>
<button class="btn" onclick="closeControlModal()" style="flex: 1; background: #dc3545;">취소</button>
</div>
</div>
</div>
</div>
<script>
// 테이블 정의
const availableTables = {
'order_mgmt': {
id: 'order_mgmt',
name: '영업관리',
description: '수주 및 영업 관리 테이블',
fields: [
{ name: 'contract_no', type: 'VARCHAR', description: '계약번호' },
{ name: 'pm_user_id', type: 'VARCHAR', description: 'PM아이디' },
{ name: 'product', type: 'VARCHAR', description: '수주제품' },
{ name: 'status_cd', type: 'VARCHAR', description: '수주상태' },
{ name: 'writer', type: 'VARCHAR', description: '작성자' }
]
},
'project_mgmt': {
id: 'project_mgmt',
name: '프로젝트관리',
description: '대상테이블(프로젝트관리)',
fields: [
{ name: 'contract_no', type: 'VARCHAR', description: '계약번호' },
{ name: 'project_no', type: 'VARCHAR', description: '프로젝트 넘버' },
{ name: 'pm_user_id', type: 'VARCHAR', description: 'pmid' }
]
},
'employees': {
id: 'employees',
name: '사원관리',
description: '사원 정보 관리 테이블',
fields: [
{ name: 'employee_id', type: 'PK', description: '사원번호' },
{ name: 'first_name', type: 'VARCHAR', description: '이름' },
{ name: 'email', type: 'VARCHAR', description: '이메일' },
{ name: 'department_id', type: 'FK', description: '부서ID' }
]
},
'departments': {
id: 'departments',
name: '부서관리',
description: '부서 정보 관리 테이블',
fields: [
{ name: 'department_id', type: 'PK', description: '부서ID' },
{ name: 'department_name', type: 'VARCHAR', description: '부서명' },
{ name: 'manager_id', type: 'FK', description: '부서장ID' }
]
},
'contracts': {
id: 'contracts',
name: '계약관리',
description: '계약 정보 관리 테이블',
fields: [
{ name: 'contract_id', type: 'PK', description: '계약ID' },
{ name: 'contract_no', type: 'VARCHAR', description: '계약번호' },
{ name: 'client_name', type: 'VARCHAR', description: '고객명' },
{ name: 'contract_date', type: 'DATE', description: '계약일' }
]
}
};
// 전역 변수
let canvasTables = [];
let tableControls = [];
let draggedElement = null;
let dragOffset = {x: 0, y: 0};
let selectedField = null;
let pendingControl = null;
// 초기화
document.addEventListener('DOMContentLoaded', function() {
setupEventListeners();
updateControlStats();
});
function setupEventListeners() {
document.getElementById('tableSelect').addEventListener('change', function() {
document.getElementById('addTableBtn').disabled = !this.value;
});
// 액션 타입 선택 이벤트
document.querySelectorAll('.action-type-option').forEach(option => {
option.addEventListener('click', function() {
selectActionType(this.dataset.action);
});
});
}
function addSelectedTable() {
const tableSelect = document.getElementById('tableSelect');
const selectedTableId = tableSelect.value;
if (!selectedTableId) {
showToast('테이블을 선택해주세요.', 'error');
return;
}
if (canvasTables.some(table => table.id === selectedTableId)) {
showToast('이미 추가된 테이블입니다.', 'error');
return;
}
const tableData = { ...availableTables[selectedTableId] };
tableData.x = Math.random() * 400 + 100;
tableData.y = Math.random() * 300 + 100;
canvasTables.push(tableData);
createTableNode(tableData, tableData.x, tableData.y);
showToast(`${tableData.name} 테이블이 추가되었습니다.`, 'success');
tableSelect.value = '';
document.getElementById('addTableBtn').disabled = true;
}
function createTableNode(tableData, x, y) {
const canvas = document.getElementById('canvas');
const tableNode = document.createElement('div');
tableNode.className = 'table-node';
tableNode.style.left = x + 'px';
tableNode.style.top = y + 'px';
tableNode.setAttribute('data-table-id', tableData.id);
const header = document.createElement('div');
header.className = 'table-node-header';
header.innerHTML = `
<div class="table-node-title">${tableData.name}</div>
<div class="table-node-id">${tableData.id}</div>
`;
const body = document.createElement('div');
body.className = 'table-node-body';
const description = document.createElement('div');
description.style.fontSize = '0.9em';
description.style.color = '#666';
description.style.marginBottom = '12px';
description.style.fontStyle = 'italic';
description.textContent = tableData.description;
const fieldsSection = document.createElement('div');
fieldsSection.style.background = '#f8f9fa';
fieldsSection.style.borderRadius = '6px';
fieldsSection.style.padding = '10px';
const fieldsTitle = document.createElement('div');
fieldsTitle.style.fontSize = '0.85em';
fieldsTitle.style.fontWeight = 'bold';
fieldsTitle.style.color = '#495057';
fieldsTitle.style.marginBottom = '8px';
fieldsTitle.innerHTML = `필드 목록 <span>(${tableData.fields.length}개)</span>`;
fieldsSection.appendChild(fieldsTitle);
tableData.fields.forEach(field => {
const fieldItem = document.createElement('div');
fieldItem.className = `field-item ${field.type.toLowerCase() === 'pk' ? 'primary-key' : field.type.toLowerCase() === 'fk' ? 'foreign-key' : ''}`;
fieldItem.setAttribute('data-field-name', field.name);
fieldItem.setAttribute('data-field-type', field.type);
const fieldName = document.createElement('span');
fieldName.innerHTML = `<strong>${field.name}</strong> (${field.type})<br><small>${field.description}</small>`;
fieldItem.appendChild(fieldName);
fieldItem.addEventListener('click', () => handleFieldClick(tableData.id, field.name, fieldItem));
fieldsSection.appendChild(fieldItem);
});
body.appendChild(description);
body.appendChild(fieldsSection);
tableNode.appendChild(header);
tableNode.appendChild(body);
canvas.appendChild(tableNode);
makeTableDraggable(tableNode, tableData);
}
function handleFieldClick(tableId, fieldName, fieldElement) {
if (!selectedField) {
// 첫 번째 필드 선택 (트리거 필드)
selectedField = { tableId, fieldName, element: fieldElement };
fieldElement.classList.add('selected');
updateControlStatus(`${tableId}.${fieldName} 선택됨`, '제어를 적용할 대상 필드를 클릭하세요');
showToast(`${fieldName} 필드가 트리거로 선택되었습니다.`, 'info');
return;
}
if (selectedField.tableId === tableId && selectedField.fieldName === fieldName) {
// 같은 필드 클릭 - 취소
cancelControl();
return;
}
// 두 번째 필드 선택 - 제어 설정 모달 열기
pendingControl = {
trigger: selectedField,
target: { tableId, fieldName, element: fieldElement }
};
openControlModal();
}
function openControlModal() {
const modal = document.getElementById('controlModal');
const controlInfo = document.getElementById('controlInfo');
const triggerTable = getTableData(pendingControl.trigger.tableId);
const targetTable = getTableData(pendingControl.target.tableId);
controlInfo.innerHTML = `
<div style="padding: 15px; background: linear-gradient(135deg, #e3f2fd 0%, #e8f5e8 100%); border-radius: 8px; border: 2px solid #ff6b6b;">
<div style="text-align: center; margin-bottom: 15px;">
<strong style="color: #d32f2f; font-size: 1.1em;">🎛️ 테이블 제어 설정</strong>
</div>
<div style="display: flex; align-items: center; margin: 10px 0;">
<div style="flex: 1; text-align: center; padding: 10px; background: white; border-radius: 5px; margin-right: 10px;">
<div style="font-weight: bold; color: #d32f2f;">트리거 테이블</div>
<div style="color: #666; font-size: 0.9em; margin: 5px 0;">${triggerTable.name}</div>
<div style="background: #ff6b6b; color: white; padding: 4px 10px; border-radius: 15px; font-size: 0.8em; display: inline-block;">
${pendingControl.trigger.fieldName}
</div>
</div>
<div style="margin: 0 10px; font-size: 2em; color: #ff6b6b;">
</div>
<div style="flex: 1; text-align: center; padding: 10px; background: white; border-radius: 5px; margin-left: 10px;">
<div style="font-weight: bold; color: #ff6b6b;">대상 테이블</div>
<div style="color: #666; font-size: 0.9em; margin: 5px 0;">${targetTable.name}</div>
<div style="background: #ff9800; color: white; padding: 4px 10px; border-radius: 15px; font-size: 0.8em; display: inline-block;">
${pendingControl.target.fieldName}
</div>
</div>
</div>
<div style="text-align: center; color: #666; font-size: 0.9em; margin-top: 10px;">
⬇️ 아래에서 제어 조건과 액션을 설정하세요
</div>
</div>
`;
// 기본값 설정
document.getElementById('controlName').value = `${triggerTable.name}_${targetTable.name}_제어`;
document.getElementById('controlDescription').value = `${triggerTable.name}에서 ${targetTable.name}으로의 제어`;
// 조건 필드 옵션 설정
populateConditionFields(triggerTable);
modal.style.display = 'flex';
}
function populateConditionFields(triggerTable) {
const conditionField = document.getElementById('conditionField');
conditionField.innerHTML = '<option value="">필드 선택</option>';
triggerTable.fields.forEach(field => {
const option = document.createElement('option');
option.value = field.name;
option.textContent = `${field.name} (${field.description})`;
conditionField.appendChild(option);
});
}
function addConditionRow() {
const container = document.getElementById('conditionsContainer');
const newRow = document.createElement('div');
newRow.className = 'condition-row';
newRow.innerHTML = `
<select>
<option value="">필드 선택</option>
${document.getElementById('conditionField').innerHTML}
</select>
<select>
<option value="=">=</option>
<option value="!=">!=</option>
<option value=">">></option>
<option value="<"><</option>
<option value="LIKE">LIKE</option>
</select>
<input type="text" placeholder="값">
<select>
<option value="AND">AND</option>
<option value="OR">OR</option>
</select>
<button type="button" onclick="this.parentElement.remove()">-</button>
`;
container.appendChild(newRow);
}
function selectActionType(actionType) {
// 모든 액션 타입에서 선택 제거
document.querySelectorAll('.action-type-option').forEach(option => {
option.classList.remove('selected');
});
// 선택된 옵션 표시
document.querySelector(`[data-action="${actionType}"]`).classList.add('selected');
// 모든 설정 숨기기
document.querySelectorAll('.data-save-settings, .external-call-settings').forEach(setting => {
setting.classList.remove('show');
});
// 선택된 타입의 설정 표시
if (actionType === 'data-save' || actionType === 'data-process') {
document.getElementById('dataSaveSettings').classList.add('show');
} else if (actionType === 'external-call') {
document.getElementById('externalCallSettings').classList.add('show');
}
}
function confirmControl() {
const selectedAction = document.querySelector('.action-type-option.selected');
if (!selectedAction) {
showToast('제어 타입을 선택해주세요.', 'error');
return;
}
const controlName = document.getElementById('controlName').value;
const controlDescription = document.getElementById('controlDescription').value;
// 조건 수집
const conditions = [];
document.querySelectorAll('#conditionsContainer .condition-row').forEach(row => {
const selects = row.querySelectorAll('select');
const input = row.querySelector('input');
if (selects[0].value && input.value) {
conditions.push({
field: selects[0].value,
operator: selects[1].value,
value: input.value,
logic: selects[2].value
});
}
});
// 제어 생성
const control = {
id: Date.now(),
name: controlName,
description: controlDescription,
triggerTable: pendingControl.trigger.tableId,
triggerField: pendingControl.trigger.fieldName,
targetTable: pendingControl.target.tableId,
targetField: pendingControl.target.fieldName,
actionType: selectedAction.dataset.action,
conditions: conditions,
settings: getActionSettings(selectedAction.dataset.action)
};
tableControls.push(control);
drawControlConnection(control);
updateControlsList();
updateControlStats();
closeControlModal();
cancelControl();
showToast(`제어 "${controlName}"이 생성되었습니다.`, 'success');
}
function getActionSettings(actionType) {
const settings = {};
if (actionType === 'data-save' || actionType === 'data-process') {
settings.dataProcessRule = document.getElementById('dataProcessRule').value;
settings.saveExecutionType = document.getElementById('saveExecutionType').value;
// 매핑 정보 수집
settings.mappings = [];
document.querySelectorAll('.mapping-section .mapping-row').forEach(row => {
const inputs = row.querySelectorAll('input');
if (inputs.length >= 2 && inputs[0].value && inputs[1].value) {
settings.mappings.push({
source: inputs[0].value,
target: inputs[1].value
});
}
});
} else if (actionType === 'external-call') {
settings.callType = document.getElementById('externalCallType').value;
settings.callUrl = document.getElementById('externalCallUrl').value;
settings.callTemplate = document.getElementById('externalCallTemplate').value;
}
return settings;
}
function drawControlConnection(control) {
const triggerNode = document.querySelector(`[data-table-id="${control.triggerTable}"]`);
const targetNode = document.querySelector(`[data-table-id="${control.targetTable}"]`);
if (triggerNode && targetNode) {
const line = document.createElement('div');
line.className = 'control-connection-line';
line.setAttribute('data-control-id', control.id);
updateControlConnectionPosition(line, triggerNode, targetNode);
document.getElementById('canvas').appendChild(line);
}
}
function updateControlConnectionPosition(line, triggerNode, targetNode) {
const triggerRect = triggerNode.getBoundingClientRect();
const targetRect = targetNode.getBoundingClientRect();
const canvasRect = document.getElementById('canvas').getBoundingClientRect();
const triggerX = triggerRect.left + triggerRect.width - canvasRect.left;
const triggerY = triggerRect.top + triggerRect.height / 2 - canvasRect.top;
const targetX = targetRect.left - canvasRect.left;
const targetY = targetRect.top + targetRect.height / 2 - canvasRect.top;
const length = Math.sqrt((targetX - triggerX) ** 2 + (targetY - triggerY) ** 2);
const angle = Math.atan2(targetY - triggerY, targetX - triggerX) * (180 / Math.PI);
line.style.width = length + 'px';
line.style.left = triggerX + 'px';
line.style.top = triggerY + 'px';
line.style.transform = `rotate(${angle}deg)`;
line.style.transformOrigin = '0 50%';
}
function updateControlsList() {
const list = document.getElementById('controlsList');
if (tableControls.length === 0) {
list.innerHTML = '<p style="padding: 15px; color: #6c757d; text-align: center;">생성된 제어가 없습니다.</p>';
return;
}
list.innerHTML = tableControls.map(control => {
const triggerTableName = getTableName(control.triggerTable);
const targetTableName = getTableName(control.targetTable);
return `
<div class="control-item">
<div class="control-header">
<div class="control-name">${control.name}</div>
</div>
<div class="control-details">
<strong>트리거:</strong> ${triggerTableName}.${control.triggerField}<br>
<strong>대상:</strong> ${targetTableName}.${control.targetField}<br>
<strong>타입:</strong> ${control.actionType}<br>
<strong>조건:</strong> ${control.conditions.length}
</div>
<div style="margin-top: 10px;">
<button class="btn-test" onclick="testControl(${control.id})">테스트</button>
<button class="btn-delete" onclick="deleteControl(${control.id})">삭제</button>
</div>
</div>
`;
}).join('');
}
function updateControlStats() {
const stats = {
'data-save': 0,
'external-call': 0,
'data-process': 0
};
tableControls.forEach(control => {
if (stats[control.actionType] !== undefined) {
stats[control.actionType]++;
}
});
document.getElementById('dataSaveControlCount').textContent = stats['data-save'] + stats['data-process'];
document.getElementById('externalCallControlCount').textContent = stats['external-call'];
document.getElementById('conditionalControlCount').textContent = tableControls.filter(c => c.conditions.length > 0).length;
document.getElementById('totalControlCount').textContent = tableControls.length;
}
function testControl(controlId) {
const control = tableControls.find(c => c.id === controlId);
if (!control) return;
showToast(`제어 "${control.name}" 테스트 실행됨`, 'info');
}
function deleteControl(controlId) {
if (!confirm('이 제어를 삭제하시겠습니까?')) return;
const controlIndex = tableControls.findIndex(c => c.id === controlId);
if (controlIndex === -1) return;
tableControls.splice(controlIndex, 1);
// 연결선 제거
const line = document.querySelector(`[data-control-id="${controlId}"]`);
if (line) line.remove();
updateControlsList();
updateControlStats();
showToast('제어가 삭제되었습니다.', 'success');
}
function closeControlModal() {
document.getElementById('controlModal').style.display = 'none';
cancelControl();
}
function cancelControl() {
if (selectedField && selectedField.element) {
selectedField.element.classList.remove('selected');
}
selectedField = null;
pendingControl = null;
updateControlStatus('제어 모드: OFF', '테이블 필드를 클릭하면 제어 설정이 시작됩니다');
}
function updateControlStatus(title, description) {
const status = document.getElementById('controlStatus');
status.innerHTML = `
<div style="font-weight: bold; margin-bottom: 5px;">${title}</div>
<div style="font-size: 0.9em; color: #495057;">${description}</div>
`;
}
function getTableData(tableId) {
return canvasTables.find(t => t.id === tableId) || availableTables[tableId];
}
function getTableName(tableId) {
const table = getTableData(tableId);
return table ? table.name : 'Unknown';
}
function makeTableDraggable(element, tableData) {
const header = element.querySelector('.table-node-header');
header.addEventListener('mousedown', function(e) {
draggedElement = element;
const rect = element.getBoundingClientRect();
dragOffset.x = e.clientX - rect.left;
dragOffset.y = e.clientY - rect.top;
element.classList.add('dragging');
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
e.preventDefault();
});
function handleMouseMove(e) {
if (draggedElement) {
const canvasRect = document.getElementById('canvas').getBoundingClientRect();
const x = e.clientX - canvasRect.left - dragOffset.x;
const y = e.clientY - canvasRect.top - dragOffset.y;
draggedElement.style.left = Math.max(0, Math.min(x, canvasRect.width - draggedElement.offsetWidth)) + 'px';
draggedElement.style.top = Math.max(0, Math.min(y, canvasRect.height - draggedElement.offsetHeight)) + 'px';
tableData.x = parseInt(draggedElement.style.left);
tableData.y = parseInt(draggedElement.style.top);
updateAllControlConnections();
}
}
function handleMouseUp() {
if (draggedElement) {
draggedElement.classList.remove('dragging');
draggedElement = null;
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
}
}
}
function updateAllControlConnections() {
tableControls.forEach(control => {
const line = document.querySelector(`[data-control-id="${control.id}"]`);
const triggerNode = document.querySelector(`[data-table-id="${control.triggerTable}"]`);
const targetNode = document.querySelector(`[data-table-id="${control.targetTable}"]`);
if (line && triggerNode && targetNode) {
updateControlConnectionPosition(line, triggerNode, targetNode);
}
});
}
function clearCanvas() {
if (confirm('모든 테이블과 제어를 초기화하시겠습니까?')) {
document.getElementById('canvas').innerHTML = '';
canvasTables = [];
tableControls = [];
updateControlsList();
updateControlStats();
cancelControl();
showToast('캔버스가 초기화되었습니다.', 'success');
}
}
function autoArrangeTable() {
const tableNodes = document.querySelectorAll('.table-node');
const cols = Math.ceil(Math.sqrt(tableNodes.length));
const spacing = 420;
tableNodes.forEach((node, index) => {
const row = Math.floor(index / cols);
const col = index % cols;
const x = 50 + (col * spacing);
const y = 50 + (row * spacing);
node.style.left = x + 'px';
node.style.top = y + 'px';
const tableId = node.getAttribute('data-table-id');
const tableData = canvasTables.find(t => t.id === tableId);
if (tableData) {
tableData.x = x;
tableData.y = y;
}
});
updateAllControlConnections();
showToast('테이블이 자동으로 정렬되었습니다.', 'success');
}
function loadSampleTables() {
if (confirm('샘플 테이블을 로드하시겠습니까?')) {
// 영업관리 테이블 추가
const orderTable = { ...availableTables['order_mgmt'], x: 100, y: 100 };
canvasTables.push(orderTable);
createTableNode(orderTable, orderTable.x, orderTable.y);
// 프로젝트관리 테이블 추가
const projectTable = { ...availableTables['project_mgmt'], x: 600, y: 100 };
canvasTables.push(projectTable);
createTableNode(projectTable, projectTable.x, projectTable.y);
showToast('샘플 테이블이 로드되었습니다! 필드를 클릭해서 제어를 설정해보세요.', 'success');
}
}
function showToast(message, type = 'success') {
const existingToast = document.querySelector('.toast');
if (existingToast) existingToast.remove();
const toast = document.createElement('div');
toast.className = `toast ${type}`;
toast.textContent = message;
document.body.appendChild(toast);
setTimeout(() => toast.classList.add('show'), 100);
setTimeout(() => {
toast.classList.remove('show');
setTimeout(() => {
if (toast.parentNode) toast.parentNode.removeChild(toast);
}, 300);
}, 3000);
}
// 키보드 단축키
document.addEventListener('keydown', function(e) {
if (e.key === 'Escape') {
if (document.getElementById('controlModal').style.display === 'flex') {
closeControlModal();
} else {
cancelControl();
}
}
});
</script>
</body>
</html>