1434 lines
57 KiB
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> |