refactor: complete modal class-based architecture, design system integration, and map editor modularization
This commit is contained in:
279
map_editor.html
279
map_editor.html
@@ -3,118 +3,25 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<meta charset="UTF-8">
|
||||||
<title>ITAM Map Coordinate Editor v3.0</title>
|
<title>ITAM Map Coordinate Editor v3.0</title>
|
||||||
<style>
|
|
||||||
:root {
|
|
||||||
--primary: #1E5149;
|
|
||||||
--bg: #f5f5f5;
|
|
||||||
}
|
|
||||||
body { font-family: sans-serif; margin: 0; display: flex; height: 100vh; background: var(--bg); overflow: hidden; }
|
|
||||||
|
|
||||||
/* Left Sidebar: File Explorer */
|
|
||||||
.file-sidebar { width: 260px; background: white; border-right: 1px solid #ddd; display: flex; flex-direction: column; overflow-y: auto; }
|
|
||||||
.folder-item { padding: 10px 15px; background: #eee; font-weight: bold; font-size: 13px; border-bottom: 1px solid #ddd; color: var(--primary); }
|
|
||||||
.file-item { padding: 8px 25px; cursor: pointer; font-size: 12px; border-bottom: 1px solid #f9f9f9; transition: background 0.2s; }
|
|
||||||
.file-item:hover { background: #f0f0f0; }
|
|
||||||
.file-item.active { background: var(--primary); color: white; font-weight: bold; }
|
|
||||||
|
|
||||||
/* Center: Editor Area */
|
|
||||||
.editor-container { flex: 1; position: relative; overflow: auto; padding: 20px; display: flex; align-items: center; justify-content: center; background: #e0e0e0; }
|
|
||||||
.img-wrapper { position: relative; display: inline-block; box-shadow: 0 0 30px rgba(0,0,0,0.3); background: white; line-height: 0; }
|
|
||||||
img {
|
|
||||||
display: block;
|
|
||||||
max-width: calc(100vw - 650px); /* 좌우 사이드바 제외 */
|
|
||||||
max-height: 85vh;
|
|
||||||
width: auto;
|
|
||||||
height: auto;
|
|
||||||
user-select: none;
|
|
||||||
-webkit-user-drag: none;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Right Sidebar: Control Panel */
|
|
||||||
.sidebar { width: 350px; background: white; border-left: 1px solid #ddd; display: flex; flex-direction: column; padding: 20px; box-shadow: -5px 0 15px rgba(0,0,0,0.05); }
|
|
||||||
h2 { margin-top: 0; color: var(--primary); font-size: 1.2rem; }
|
|
||||||
p { font-size: 0.85rem; color: #666; line-height: 1.4; margin-bottom: 20px; }
|
|
||||||
|
|
||||||
.current-path { font-size: 11px; color: #888; margin-bottom: 10px; word-break: break-all; font-family: monospace; }
|
|
||||||
|
|
||||||
.box-list { flex: 1; overflow-y: auto; margin-bottom: 15px; border: 1px solid #eee; border-radius: 4px; padding: 10px; background: #fafafa; }
|
|
||||||
.box-item { font-family: monospace; font-size: 11px; padding: 6px; border-bottom: 1px solid #eee; display: flex; justify-content: space-between; align-items: center; }
|
|
||||||
.box-item:hover { background: #fff; }
|
|
||||||
.btn-del { cursor: pointer; color: #ff4444; border: none; background: none; font-size: 16px; padding: 0 5px; }
|
|
||||||
|
|
||||||
.actions { display: flex; flex-direction: column; gap: 8px; }
|
|
||||||
button { padding: 12px; border-radius: 4px; border: none; cursor: pointer; font-weight: bold; transition: all 0.2s; }
|
|
||||||
.btn-primary { background: var(--primary); color: white; }
|
|
||||||
.btn-secondary { background: #f0f0f0; color: #333; border: 1px solid #ccc; }
|
|
||||||
button:hover { filter: brightness(1.1); }
|
|
||||||
button:active { transform: scale(0.98); }
|
|
||||||
button:disabled { background: #ccc; cursor: not-allowed; }
|
|
||||||
|
|
||||||
/* Drawing Elements */
|
|
||||||
.draw-box { position: absolute; border: 2px solid #FF3D00; background: rgba(255, 61, 0, 0.2); pointer-events: none; z-index: 100; }
|
|
||||||
.placed-box { position: absolute; border: 1.5px solid var(--primary); background: rgba(30, 81, 73, 0.15); cursor: pointer; z-index: 50; }
|
|
||||||
.placed-box:hover { background: rgba(30, 81, 73, 0.4); border-color: #000; }
|
|
||||||
.placed-box.selected { border: 2.5px solid #FF3D00; z-index: 60; box-shadow: 0 0 10px rgba(255,61,0,0.5); }
|
|
||||||
|
|
||||||
.box-label {
|
|
||||||
position: absolute;
|
|
||||||
top: 50%;
|
|
||||||
left: 50%;
|
|
||||||
transform: translate(-50%, -50%);
|
|
||||||
font-size: 10px;
|
|
||||||
font-weight: bold;
|
|
||||||
color: var(--primary);
|
|
||||||
pointer-events: none;
|
|
||||||
white-space: nowrap;
|
|
||||||
background: rgba(255,255,255,0.7);
|
|
||||||
padding: 0 2px;
|
|
||||||
border-radius: 2px;
|
|
||||||
line-height: 1;
|
|
||||||
}
|
|
||||||
|
|
||||||
.draw-box .box-label {
|
|
||||||
color: #FF3D00;
|
|
||||||
background: rgba(255,255,255,0.8);
|
|
||||||
}
|
|
||||||
|
|
||||||
#save-status { margin-top: 8px; font-size: 11px; color: #27ae60; text-align: center; font-weight: bold; height: 14px; }
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
|
||||||
<!-- Left: File Selector -->
|
<!-- Left: File Selector -->
|
||||||
<div class="file-sidebar" id="file-sidebar">
|
<div class="file-sidebar" id="file-sidebar">
|
||||||
<div class="folder-item">IDC</div>
|
<!-- Rendered by MapEditor.ts -->
|
||||||
<div class="file-item active" data-path="img/location_photo/IDC/서관202.png">서관202.png</div>
|
|
||||||
<div class="file-item" data-path="img/location_photo/IDC/서관203.png">서관203.png</div>
|
|
||||||
<div class="file-item" data-path="img/location_photo/IDC/서관204.png">서관204.png</div>
|
|
||||||
<div class="file-item" data-path="img/location_photo/IDC/서관205.png">서관205.png</div>
|
|
||||||
<div class="file-item" data-path="img/location_photo/IDC/동관53.png">동관53.png</div>
|
|
||||||
<div class="file-item" data-path="img/location_photo/IDC/동관54.png">동관54.png</div>
|
|
||||||
|
|
||||||
<div class="folder-item">기술개발센터</div>
|
|
||||||
<div class="file-item" data-path="img/location_photo/기술개발센터/서버실/서버실_1.png">서버실_1.png</div>
|
|
||||||
<div class="file-item" data-path="img/location_photo/기술개발센터/서버실/서버실_2.png">서버실_2.png</div>
|
|
||||||
|
|
||||||
<div class="folder-item">한맥빌딩</div>
|
|
||||||
<div class="file-item" data-path="img/location_photo/한맥빌딩/7층_로비.png">7층_배치도(예시)</div>
|
|
||||||
<div class="file-item" data-path="img/location_photo/한맥빌딩/MDF실/MDF_1.png">MDF_1.png</div>
|
|
||||||
<div class="file-item" data-path="img/location_photo/한맥빌딩/MDF실/MDF_2.png">MDF_2.png</div>
|
|
||||||
<div class="file-item" data-path="img/location_photo/한맥빌딩/MDF실/MDF_3.png">MDF_3.png</div>
|
|
||||||
<div class="file-item" data-path="img/location_photo/한맥빌딩/MDF실/MDF_4.png">MDF_4.png</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Center: Main Editor -->
|
<!-- Center: Main Editor -->
|
||||||
<div class="editor-container" id="container">
|
<div class="editor-container" id="container">
|
||||||
<div class="img-wrapper" id="wrapper">
|
<div class="img-wrapper" id="wrapper">
|
||||||
<img src="img/location_photo/IDC/서관202.png" id="target-img" alt="Map Image">
|
<img src="" id="target-img" alt="Map Image">
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- Right: Control Panel -->
|
<!-- Right: Control Panel -->
|
||||||
<div class="sidebar">
|
<div class="sidebar">
|
||||||
<h2>Map Editor <small style="font-size: 0.6em; color: #888;">v3.0</small></h2>
|
<h2>Map Editor <small style="font-size: 0.6em; color: #888;">v3.0</small></h2>
|
||||||
<div class="current-path" id="current-path">img/location_photo/IDC/서관202.png</div>
|
<div class="current-path" id="current-path">파일을 선택하세요</div>
|
||||||
<p>
|
<p>
|
||||||
드래그하여 구역을 정의하세요. 저장 버튼을 누르면 즉시 시스템에 반영됩니다.
|
드래그하여 구역을 정의하세요. 저장 버튼을 누르면 즉시 시스템에 반영됩니다.
|
||||||
</p>
|
</p>
|
||||||
@@ -122,186 +29,12 @@
|
|||||||
<div class="box-list" id="box-list"></div>
|
<div class="box-list" id="box-list"></div>
|
||||||
|
|
||||||
<div class="actions">
|
<div class="actions">
|
||||||
<button class="btn-secondary" onclick="clearAll()">전체 삭제</button>
|
<button class="btn btn-outline" style="height:38px;" onclick="clearAll()">전체 삭제</button>
|
||||||
<button id="btn-save-server" class="btn-primary" onclick="saveToServer()">서버에 즉시 저장</button>
|
<button id="btn-save-server" class="btn btn-primary" style="height:38px;" onclick="saveToServer()">서버에 즉시 저장</button>
|
||||||
<div id="save-status"></div>
|
<div id="save-status"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script>
|
<script type="module" src="/src/map-editor-main.ts"></script>
|
||||||
const wrapper = document.getElementById('wrapper');
|
|
||||||
const img = document.getElementById('target-img');
|
|
||||||
const boxListEl = document.getElementById('box-list');
|
|
||||||
const pathLabel = document.getElementById('current-path');
|
|
||||||
const fileItems = document.querySelectorAll('.file-item');
|
|
||||||
const statusEl = document.getElementById('save-status');
|
|
||||||
const saveBtn = document.getElementById('btn-save-server');
|
|
||||||
|
|
||||||
let allMapConfig = {};
|
|
||||||
let boxes = [];
|
|
||||||
let isDrawing = false;
|
|
||||||
let startX, startY;
|
|
||||||
let currentBox = null;
|
|
||||||
|
|
||||||
// 1. 서버에서 기존 설정 로드
|
|
||||||
async function loadConfig() {
|
|
||||||
try {
|
|
||||||
const res = await fetch(`http://${location.hostname}:3000/api/maps`);
|
|
||||||
allMapConfig = await res.json();
|
|
||||||
renderCurrentFile();
|
|
||||||
} catch (err) {
|
|
||||||
console.error('Failed to load config:', err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderCurrentFile() {
|
|
||||||
const activeItem = document.querySelector('.file-item.active');
|
|
||||||
const activeFile = activeItem.dataset.path;
|
|
||||||
boxes = allMapConfig[activeFile] || [];
|
|
||||||
pathLabel.textContent = activeFile;
|
|
||||||
img.src = activeFile;
|
|
||||||
render();
|
|
||||||
}
|
|
||||||
|
|
||||||
// File Selection
|
|
||||||
fileItems.forEach(item => {
|
|
||||||
item.addEventListener('click', () => {
|
|
||||||
fileItems.forEach(i => i.classList.remove('active'));
|
|
||||||
item.classList.add('active');
|
|
||||||
renderCurrentFile();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 2. 서버에 저장
|
|
||||||
async function saveToServer() {
|
|
||||||
const activeFile = document.querySelector('.file-item.active').dataset.path;
|
|
||||||
|
|
||||||
try {
|
|
||||||
saveBtn.disabled = true;
|
|
||||||
saveBtn.textContent = '저장 중...';
|
|
||||||
|
|
||||||
const res = await fetch(`http://${location.hostname}:3000/api/maps/save`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify({ path: activeFile, boxes: boxes })
|
|
||||||
});
|
|
||||||
|
|
||||||
if (res.ok) {
|
|
||||||
allMapConfig[activeFile] = [...boxes];
|
|
||||||
statusEl.textContent = '✅ 서버 저장 완료 (' + new Date().toLocaleTimeString() + ')';
|
|
||||||
setTimeout(() => statusEl.textContent = '', 3000);
|
|
||||||
} else {
|
|
||||||
alert('저장 실패!');
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
alert('서버 연결 오류!');
|
|
||||||
} finally {
|
|
||||||
saveBtn.disabled = false;
|
|
||||||
saveBtn.textContent = '서버에 즉시 저장';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
wrapper.addEventListener('mousedown', (e) => {
|
|
||||||
if (e.button !== 0) return;
|
|
||||||
isDrawing = true;
|
|
||||||
const rect = wrapper.getBoundingClientRect();
|
|
||||||
startX = e.clientX - rect.left;
|
|
||||||
startY = e.clientY - rect.top;
|
|
||||||
|
|
||||||
currentBox = document.createElement('div');
|
|
||||||
currentBox.className = 'draw-box';
|
|
||||||
currentBox.style.left = startX + 'px';
|
|
||||||
currentBox.style.top = startY + 'px';
|
|
||||||
|
|
||||||
const label = document.createElement('div');
|
|
||||||
label.className = 'box-label';
|
|
||||||
label.textContent = '#' + (boxes.length + 1);
|
|
||||||
currentBox.appendChild(label);
|
|
||||||
|
|
||||||
wrapper.appendChild(currentBox);
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener('mousemove', (e) => {
|
|
||||||
if (!isDrawing) return;
|
|
||||||
const rect = wrapper.getBoundingClientRect();
|
|
||||||
const currentX = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
|
|
||||||
const currentY = Math.max(0, Math.min(e.clientY - rect.top, rect.height));
|
|
||||||
|
|
||||||
const width = currentX - startX;
|
|
||||||
const height = currentY - startY;
|
|
||||||
|
|
||||||
currentBox.style.width = Math.abs(width) + 'px';
|
|
||||||
currentBox.style.height = Math.abs(height) + 'px';
|
|
||||||
currentBox.style.left = (width > 0 ? startX : currentX) + 'px';
|
|
||||||
currentBox.style.top = (height > 0 ? startY : currentY) + 'px';
|
|
||||||
});
|
|
||||||
|
|
||||||
window.addEventListener('mouseup', (e) => {
|
|
||||||
if (!isDrawing) return;
|
|
||||||
isDrawing = false;
|
|
||||||
|
|
||||||
const width = parseFloat(currentBox.style.width);
|
|
||||||
const height = parseFloat(currentBox.style.height);
|
|
||||||
|
|
||||||
if (width > 3 && height > 3) {
|
|
||||||
const rect = wrapper.getBoundingClientRect();
|
|
||||||
const boxData = {
|
|
||||||
x: (parseFloat(currentBox.style.left) / rect.width * 100).toFixed(2),
|
|
||||||
y: (parseFloat(currentBox.style.top) / rect.height * 100).toFixed(2),
|
|
||||||
w: (width / rect.width * 100).toFixed(2),
|
|
||||||
h: (height / rect.height * 100).toFixed(2)
|
|
||||||
};
|
|
||||||
boxes.push(boxData);
|
|
||||||
render();
|
|
||||||
}
|
|
||||||
|
|
||||||
currentBox.remove();
|
|
||||||
currentBox = null;
|
|
||||||
});
|
|
||||||
|
|
||||||
function removeBox(index) {
|
|
||||||
boxes.splice(index, 1);
|
|
||||||
render();
|
|
||||||
}
|
|
||||||
|
|
||||||
function clearAll() {
|
|
||||||
if(confirm('모든 박스를 삭제할까요?')) {
|
|
||||||
boxes = [];
|
|
||||||
render();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function render() {
|
|
||||||
boxListEl.innerHTML = '';
|
|
||||||
const oldBoxes = wrapper.querySelectorAll('.placed-box');
|
|
||||||
oldBoxes.forEach(b => b.remove());
|
|
||||||
|
|
||||||
boxes.forEach((box, i) => {
|
|
||||||
const div = document.createElement('div');
|
|
||||||
div.className = 'placed-box';
|
|
||||||
div.style.left = box.x + '%';
|
|
||||||
div.style.top = box.y + '%';
|
|
||||||
div.style.width = box.w + '%';
|
|
||||||
div.style.height = box.h + '%';
|
|
||||||
|
|
||||||
const label = document.createElement('div');
|
|
||||||
label.className = 'box-label';
|
|
||||||
label.textContent = '#' + (i + 1);
|
|
||||||
div.appendChild(label);
|
|
||||||
|
|
||||||
wrapper.appendChild(div);
|
|
||||||
|
|
||||||
const item = document.createElement('div');
|
|
||||||
item.className = 'box-item';
|
|
||||||
item.innerHTML = `
|
|
||||||
<span>#${i+1}: [${box.x}, ${box.y}]</span>
|
|
||||||
<button class="btn-del" onclick="removeBox(${i})">×</button>
|
|
||||||
`;
|
|
||||||
boxListEl.appendChild(item);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
loadConfig();
|
|
||||||
</script>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@@ -1,5 +1,106 @@
|
|||||||
|
import { createIcons, X } from 'lucide';
|
||||||
|
import { setEditLock } from './ModalUtils';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 모든 모달의 공통 기능 (닫기, ESC 처리, 배경 클릭 등)을 관리하는 베이스 모듈입니다.
|
* 모든 모달의 공통 기능을 관리하는 베이스 추상 클래스입니다.
|
||||||
|
*/
|
||||||
|
export abstract class BaseModal {
|
||||||
|
protected idPrefix: string;
|
||||||
|
protected title: string;
|
||||||
|
protected currentAsset: any | null = null;
|
||||||
|
protected isEditMode: boolean = false;
|
||||||
|
protected modalEl: HTMLElement | null = null;
|
||||||
|
protected formEl: HTMLFormElement | null = null;
|
||||||
|
|
||||||
|
constructor(idPrefix: string, title: string) {
|
||||||
|
this.idPrefix = idPrefix;
|
||||||
|
this.title = title;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 모달 초기화: HTML 삽입 및 공통 이벤트 바인딩
|
||||||
|
*/
|
||||||
|
public init(onSave: () => void, closeModalsFn: () => void) {
|
||||||
|
// 1. 프레임 HTML 삽입 (자식 클래스에서 정의한 HTML 사용)
|
||||||
|
if (!document.getElementById(`${this.idPrefix}-asset-modal`)) {
|
||||||
|
document.body.insertAdjacentHTML('beforeend', this.renderFrameHTML());
|
||||||
|
}
|
||||||
|
|
||||||
|
this.modalEl = document.getElementById(`${this.idPrefix}-asset-modal`);
|
||||||
|
this.formEl = document.getElementById(`${this.idPrefix}-asset-form`) as HTMLFormElement;
|
||||||
|
|
||||||
|
// 2. 공통 버튼 이벤트 바인딩 (닫기, 취소 등)
|
||||||
|
const btnCloseHeader = document.getElementById(`btn-close-${this.idPrefix}-modal`);
|
||||||
|
const btnCancelFooter = document.getElementById(`btn-cancel-${this.idPrefix}-modal`);
|
||||||
|
|
||||||
|
const closeAction = () => {
|
||||||
|
this.close();
|
||||||
|
closeModalsFn(); // 전역 모달 상태 해제 콜백
|
||||||
|
};
|
||||||
|
|
||||||
|
btnCloseHeader?.addEventListener('click', closeAction);
|
||||||
|
btnCancelFooter?.addEventListener('click', closeAction);
|
||||||
|
|
||||||
|
// 3. 자식 클래스 전용 초기화 로직 실행
|
||||||
|
this.initChildLogic(onSave, closeModalsFn);
|
||||||
|
|
||||||
|
// 4. 아이콘 초기화
|
||||||
|
createIcons({ icons: { X } });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 모달 열기: 데이터 바인딩 및 모드 설정
|
||||||
|
*/
|
||||||
|
public open(asset: any, mode: 'view' | 'edit' | 'add' = 'view') {
|
||||||
|
this.currentAsset = asset;
|
||||||
|
this.isEditMode = (mode === 'add' || mode === 'edit');
|
||||||
|
|
||||||
|
this.setEditLockMode(mode);
|
||||||
|
this.fillFormData(asset);
|
||||||
|
|
||||||
|
if (this.modalEl) {
|
||||||
|
this.modalEl.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.onAfterOpen(asset, mode);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 모달 닫기: 상태 초기화
|
||||||
|
*/
|
||||||
|
public close() {
|
||||||
|
if (this.modalEl) {
|
||||||
|
this.modalEl.classList.add('hidden');
|
||||||
|
}
|
||||||
|
this.isEditMode = false;
|
||||||
|
this.currentAsset = null;
|
||||||
|
this.onAfterClose();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 조회/수정 모드에 따른 UI 잠금 및 버튼 제어
|
||||||
|
*/
|
||||||
|
protected setEditLockMode(mode: 'view' | 'edit' | 'add') {
|
||||||
|
setEditLock(`${this.idPrefix}-asset-form`, mode, {
|
||||||
|
saveBtnId: `btn-save-${this.idPrefix}-asset`,
|
||||||
|
revertBtnId: `btn-revert-${this.idPrefix}-edit`,
|
||||||
|
addLogBtnId: `btn-add-${this.idPrefix}-log`
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// --- 추상 메서드: 자식 클래스에서 구현해야 함 ---
|
||||||
|
protected abstract renderFrameHTML(): string;
|
||||||
|
protected abstract initChildLogic(onSave: () => void, closeModals: () => void): void;
|
||||||
|
protected abstract fillFormData(asset: any): void;
|
||||||
|
protected abstract onAfterOpen(asset: any, mode: string): void;
|
||||||
|
|
||||||
|
// --- 훅(Hook) 메서드: 필요 시 오버라이드 ---
|
||||||
|
protected onAfterClose(): void {}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* --- 레거시 호환성을 위한 함수형 익스포트 ---
|
||||||
|
* 기존 코드들이 참조하고 있는 함수들을 유지합니다.
|
||||||
*/
|
*/
|
||||||
export function closeModals() {
|
export function closeModals() {
|
||||||
const modals = document.querySelectorAll('.modal-overlay');
|
const modals = document.querySelectorAll('.modal-overlay');
|
||||||
@@ -7,28 +108,14 @@ export function closeModals() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export function initBaseModal() {
|
export function initBaseModal() {
|
||||||
// ESC 키로 닫기
|
// ESC 키로 모든 모달 닫기
|
||||||
window.addEventListener('keydown', (e) => {
|
window.addEventListener('keydown', (e) => {
|
||||||
if (e.key === 'Escape') closeModals();
|
if (e.key === 'Escape') closeModals();
|
||||||
});
|
});
|
||||||
|
|
||||||
// 배경(Overlay) 클릭 시 닫기 (요청에 의해 비활성화됨)
|
|
||||||
/*
|
|
||||||
document.addEventListener('click', (e) => {
|
|
||||||
const target = e.target as HTMLElement;
|
|
||||||
if (target.classList.contains('modal-overlay')) {
|
|
||||||
closeModals();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
|
|
||||||
return { closeAllModals: closeModals };
|
return { closeAllModals: closeModals };
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* 특정 모달을 엽니다.
|
|
||||||
* @param modalId 모달 엘리먼트의 ID
|
|
||||||
*/
|
|
||||||
export function openModal(modalId: string) {
|
export function openModal(modalId: string) {
|
||||||
const modal = document.getElementById(modalId);
|
const modal = document.getElementById(modalId);
|
||||||
if (modal) {
|
if (modal) {
|
||||||
|
|||||||
@@ -1,121 +1,188 @@
|
|||||||
import { state, saveAsset, deleteAsset } from '../../core/state';
|
import { state, saveAsset, deleteAsset } from '../../core/state';
|
||||||
import { closeModals, openModal } from './BaseModal';
|
import { BaseModal } from './BaseModal';
|
||||||
import { CORP_LIST } from './SharedData';
|
import { CORP_LIST } from './SharedData';
|
||||||
import { generateOptionsHTML, setEditLock } from './ModalUtils';
|
import { generateOptionsHTML, setFieldValue, getFieldValue } from './ModalUtils';
|
||||||
import { createIcons, X, Save, Database, CalendarClock, Edit2 } from 'lucide';
|
import { createIcons, X, Save, Database, CalendarClock, Edit2, History, Plus } from 'lucide';
|
||||||
import { formatExcelDate } from '../../core/excelHandler';
|
import { formatExcelDate } from '../../core/excelHandler';
|
||||||
import { UI_TEXT } from '../../core/schema';
|
import { UI_TEXT } from '../../core/schema';
|
||||||
import { API_BASE_URL } from '../../core/utils';
|
|
||||||
|
|
||||||
let currentItem: any = null;
|
class DomainAssetModal extends BaseModal {
|
||||||
|
constructor() {
|
||||||
const DOMAIN_MODAL_HTML = `
|
super('domain', '도메인 정보');
|
||||||
... (rest of DOMAIN_MODAL_HTML remains same) ...
|
|
||||||
`;
|
|
||||||
|
|
||||||
export function initDomainModal() {
|
|
||||||
if (!document.getElementById('domain-asset-modal')) {
|
|
||||||
document.body.insertAdjacentHTML('beforeend', DOMAIN_MODAL_HTML);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const modal = document.getElementById('domain-asset-modal')!;
|
protected renderFrameHTML(): string {
|
||||||
document.getElementById('btn-close-domain-modal')?.addEventListener('click', () => closeModals());
|
return `
|
||||||
document.getElementById('btn-cancel-domain')?.addEventListener('click', () => closeModals());
|
<div id="domain-asset-modal" class="modal-overlay hidden">
|
||||||
|
<div class="modal-content wide">
|
||||||
const saveBtn = document.getElementById('btn-save-domain');
|
<div class="modal-header">
|
||||||
const revertBtn = document.getElementById('btn-revert-domain');
|
<h2 id="domain-modal-title">${this.title}</h2>
|
||||||
const deleteBtn = document.getElementById('btn-delete-domain');
|
<button id="btn-close-domain-modal" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></button>
|
||||||
const headerEditBtn = document.getElementById('btn-edit-domain-header');
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="modal-body-split">
|
||||||
|
<div class="modal-form-area">
|
||||||
|
<form id="domain-asset-form" class="grid-form">
|
||||||
|
<input type="hidden" id="domain-id" name="id" />
|
||||||
|
|
||||||
|
<div class="form-section-title">기본 정보</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>구분</label>
|
||||||
|
<select id="domain-type" name="type">
|
||||||
|
<option value="호스팅">호스팅</option>
|
||||||
|
<option value="도메인">도메인</option>
|
||||||
|
<option value="기타">기타</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>관리법인</label>
|
||||||
|
<select id="domain-corp" name="corp">${generateOptionsHTML(CORP_LIST)}</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group full-width">
|
||||||
|
<label>서비스명</label>
|
||||||
|
<input type="text" id="domain-service-name" name="service_name" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group full-width">
|
||||||
|
<label>관리도메인</label>
|
||||||
|
<input type="text" id="domain-name" name="domain_name" required />
|
||||||
|
</div>
|
||||||
|
|
||||||
saveBtn?.addEventListener('click', () => {
|
<div class="form-section-title">계약 및 비용</div>
|
||||||
if (!currentItem) return;
|
<div class="form-group">
|
||||||
if (saveBtn.textContent?.includes('수정')) {
|
<label>계약시작일</label>
|
||||||
setEditLock('domain-asset-form', 'edit', { saveBtnId: 'btn-save-domain', revertBtnId: 'btn-revert-domain' });
|
<input type="date" id="domain-start-date" name="start_date" />
|
||||||
return;
|
</div>
|
||||||
}
|
<div class="form-group">
|
||||||
saveDomain();
|
<label>만료예정일</label>
|
||||||
});
|
<input type="date" id="domain-expiry-date" name="expiry_date" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>비용 (연간/월간)</label>
|
||||||
|
<input type="text" id="domain-price" name="price" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\\\\B(?=(\\\\d{3})+(?!\\\\d))/g, ',')" />
|
||||||
|
</div>
|
||||||
|
|
||||||
headerEditBtn?.addEventListener('click', () => {
|
<div class="form-section-title">담당자 및 비고</div>
|
||||||
setEditLock('domain-asset-form', 'edit', { saveBtnId: 'btn-save-domain', revertBtnId: 'btn-revert-domain' });
|
<div class="form-group">
|
||||||
});
|
<label>정담당자</label>
|
||||||
|
<input type="text" id="domain-manager-main" name="manager_main" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>부담당자</label>
|
||||||
|
<input type="text" id="domain-manager-sub" name="manager_sub" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group full-width">
|
||||||
|
<label>비고</label>
|
||||||
|
<textarea id="domain-remarks" name="remarks" rows="3"></textarea>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-history-area">
|
||||||
|
<div class="history-header">
|
||||||
|
<h3><i data-lucide="history" style="width:16px; height:16px;"></i> 변경 이력</h3>
|
||||||
|
<button type="button" id="btn-add-domain-log" class="btn btn-outline btn-sm">
|
||||||
|
이력 추가 <i data-lucide="plus" style="width:14px; height:14px;"></i>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div id="domain-history-list" class="history-timeline"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button id="btn-delete-domain-asset" class="btn btn-outline btn-danger">삭제</button>
|
||||||
|
<div class="footer-actions">
|
||||||
|
<button id="btn-revert-domain-edit" class="btn btn-outline hidden">수정 취소</button>
|
||||||
|
<button id="btn-cancel-domain-modal" class="btn btn-outline">닫기</button>
|
||||||
|
<button id="btn-save-domain-asset" class="btn btn-primary">수정</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
revertBtn?.addEventListener('click', () => {
|
protected initChildLogic(onSave: () => void, closeModals: () => void): void {
|
||||||
setEditLock('domain-asset-form', 'view', { saveBtnId: 'btn-save-domain', revertBtnId: 'btn-revert-domain' });
|
const saveBtn = document.getElementById('btn-save-domain-asset')!;
|
||||||
if (currentItem) openDomainModal(currentItem);
|
const revertBtn = document.getElementById('btn-revert-domain-edit')!;
|
||||||
});
|
const deleteBtn = document.getElementById('btn-delete-domain-asset')!;
|
||||||
|
|
||||||
deleteBtn?.addEventListener('click', async () => {
|
saveBtn.addEventListener('click', async () => {
|
||||||
if (currentItem && confirm(UI_TEXT.MESSAGES.CONFIRM_DELETE)) {
|
if (!this.currentAsset) return;
|
||||||
const success = await deleteAsset('domain', currentItem.id);
|
if (!this.isEditMode) {
|
||||||
if (success) {
|
this.setEditLockMode('edit');
|
||||||
alert('성공적으로 삭제되었습니다.');
|
this.isEditMode = true;
|
||||||
closeModals();
|
return;
|
||||||
window.dispatchEvent(new CustomEvent('refresh-view'));
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
export function openDomainModal(item: any = null) {
|
const formData = new FormData(this.formEl!);
|
||||||
currentItem = item;
|
const updated = { ...this.currentAsset };
|
||||||
const isEdit = !!item;
|
formData.forEach((value, key) => { updated[key] = value; });
|
||||||
const mode = isEdit ? 'view' : 'add';
|
|
||||||
|
|
||||||
const titleEl = document.getElementById('domain-modal-title');
|
|
||||||
if (titleEl) titleEl.textContent = isEdit ? '도메인 정보 상세' : '신규 도메인 등록';
|
|
||||||
|
|
||||||
setEditLock('domain-asset-form', mode, { saveBtnId: 'btn-save-domain', revertBtnId: 'btn-revert-domain' });
|
if (!updated.service_name || !updated.domain_name) {
|
||||||
|
alert('서비스명과 관리도메인은 필수 입력 사항입니다.');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
const setVal = (id: string, val: any) => {
|
if (await saveAsset('domain', updated)) {
|
||||||
const el = document.getElementById(id) as HTMLInputElement | HTMLSelectElement | HTMLTextAreaElement;
|
alert(UI_TEXT.MESSAGES.SAVE_SUCCESS);
|
||||||
if (el) el.value = val || '';
|
onSave(); this.close(); closeModals();
|
||||||
};
|
}
|
||||||
|
});
|
||||||
|
|
||||||
setVal('domain-type', item?.type || '호스팅');
|
revertBtn.addEventListener('click', () => {
|
||||||
setVal('domain-corp', item?.corp || '');
|
this.setEditLockMode('view');
|
||||||
setVal('domain-service-name', item?.service_name || '');
|
if (this.currentAsset) this.fillFormData(this.currentAsset);
|
||||||
setVal('domain-name', item?.domain_name || '');
|
});
|
||||||
setVal('domain-start-date', formatExcelDate(item?.start_date));
|
|
||||||
setVal('domain-expiry-date', formatExcelDate(item?.expiry_date));
|
|
||||||
setVal('domain-price', item?.price || '');
|
|
||||||
setVal('domain-manager-main', item?.manager_main || '');
|
|
||||||
setVal('domain-manager-sub', item?.manager_sub || '');
|
|
||||||
setVal('domain-remarks', item?.remarks || '');
|
|
||||||
|
|
||||||
const deleteBtn = document.getElementById('btn-delete-domain');
|
deleteBtn.addEventListener('click', async () => {
|
||||||
if (deleteBtn) deleteBtn.style.display = isEdit ? 'block' : 'none';
|
if (!this.currentAsset || !confirm(UI_TEXT.MESSAGES.CONFIRM_DELETE)) return;
|
||||||
|
if (await deleteAsset('domain', this.currentAsset.id)) {
|
||||||
|
alert('성공적으로 삭제되었습니다.');
|
||||||
|
onSave(); this.close(); closeModals();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
openModal('domain-asset-modal');
|
createIcons({ icons: { History, Plus, Save, CalendarClock, Database } });
|
||||||
createIcons({ icons: { X, Save, Database, CalendarClock, Edit2 } });
|
|
||||||
}
|
|
||||||
|
|
||||||
async function saveDomain() {
|
|
||||||
const getVal = (id: string) => (document.getElementById(id) as HTMLInputElement)?.value || '';
|
|
||||||
|
|
||||||
const newDomain = {
|
|
||||||
id: currentItem ? currentItem.id : `DOM-${Date.now()}`,
|
|
||||||
type: getVal('domain-type'),
|
|
||||||
corp: getVal('domain-corp'),
|
|
||||||
service_name: getVal('domain-service-name'),
|
|
||||||
domain_name: getVal('domain-name'),
|
|
||||||
start_date: getVal('domain-start-date'),
|
|
||||||
expiry_date: getVal('domain-expiry-date'),
|
|
||||||
price: getVal('domain-price'),
|
|
||||||
manager_main: getVal('domain-manager-main'),
|
|
||||||
manager_sub: getVal('domain-manager-sub'),
|
|
||||||
remarks: getVal('domain-remarks')
|
|
||||||
};
|
|
||||||
|
|
||||||
if (!newDomain.service_name || !newDomain.domain_name) {
|
|
||||||
alert('서비스명과 관리도메인은 필수 입력 사항입니다.');
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const success = await saveAsset('domain', newDomain);
|
protected fillFormData(asset: any): void {
|
||||||
if (success) {
|
setFieldValue('domain-id', asset.id);
|
||||||
alert(UI_TEXT.MESSAGES.SAVE_SUCCESS);
|
setFieldValue('domain-type', asset.type || '호스팅');
|
||||||
closeModals();
|
setFieldValue('domain-corp', asset.corp || '');
|
||||||
window.dispatchEvent(new CustomEvent('refresh-view'));
|
setFieldValue('domain-service-name', asset.service_name || '');
|
||||||
|
setFieldValue('domain-name', asset.domain_name || '');
|
||||||
|
setFieldValue('domain-start-date', formatExcelDate(asset.start_date));
|
||||||
|
setFieldValue('domain-expiry-date', formatExcelDate(asset.expiry_date));
|
||||||
|
setFieldValue('domain-price', asset.price || '');
|
||||||
|
setFieldValue('domain-manager-main', asset.manager_main || '');
|
||||||
|
setFieldValue('domain-manager-sub', asset.manager_sub || '');
|
||||||
|
setFieldValue('domain-remarks', asset.remarks || '');
|
||||||
|
|
||||||
|
this.renderHistory(asset.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onAfterOpen(asset: any, mode: string): void {
|
||||||
|
const titleEl = document.getElementById('domain-modal-title');
|
||||||
|
if (titleEl) titleEl.textContent = (mode === 'add') ? '신규 도메인 등록' : '도메인 정보 상세';
|
||||||
|
|
||||||
|
const deleteBtn = document.getElementById('btn-delete-domain-asset');
|
||||||
|
if (deleteBtn) deleteBtn.style.display = (mode === 'add') ? 'none' : 'block';
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderHistory(assetId: string) {
|
||||||
|
const container = document.getElementById('domain-history-list');
|
||||||
|
if (!container) return;
|
||||||
|
const logs = (state.masterData.logs || []).filter(l => l.assetId === assetId);
|
||||||
|
if (logs.length === 0) { container.innerHTML = '<div class="empty-history">이력이 없습니다.</div>'; return; }
|
||||||
|
container.innerHTML = logs.map(l => `<div class=\"history-item\"><div class=\"history-date\">${l.date}</div><div class=\"history-user\">${l.user}</div><div class=\"history-details\">${l.details}</div></div>`).join('');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const domainModal = new DomainAssetModal();
|
||||||
|
|
||||||
|
export function initDomainModal(onSave: () => void, closeModals: () => void) {
|
||||||
|
domainModal.init(onSave, closeModals);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function openDomainModal(asset: any, mode: 'view' | 'edit' | 'add' = 'view') {
|
||||||
|
domainModal.open(asset, mode);
|
||||||
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,7 +1,7 @@
|
|||||||
import { state, saveAsset, deleteAsset } from '../../core/state';
|
import { state, saveAsset, deleteAsset } from '../../core/state';
|
||||||
import { openModal, closeModals } from './BaseModal';
|
import { BaseModal } from './BaseModal';
|
||||||
import { openSwUserModal } from './SWUserModal';
|
import { openSwUserModal } from './SWUserModal';
|
||||||
import { createIcons, History, Plus, X, Save, Edit2, RotateCcw, Calendar } from 'lucide';
|
import { createIcons, History, Plus, X, Save, Edit2, RotateCcw, Calendar, Users } from 'lucide';
|
||||||
import { CORP_LIST } from './SharedData';
|
import { CORP_LIST } from './SharedData';
|
||||||
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
|
import { ASSET_SCHEMA, UI_TEXT } from '../../core/schema';
|
||||||
import { API_BASE_URL } from '../../core/utils';
|
import { API_BASE_URL } from '../../core/utils';
|
||||||
@@ -9,438 +9,363 @@ import {
|
|||||||
generateOptionsHTML,
|
generateOptionsHTML,
|
||||||
setFieldValue,
|
setFieldValue,
|
||||||
getFieldValue,
|
getFieldValue,
|
||||||
setEditLock,
|
|
||||||
applyDateMask
|
applyDateMask
|
||||||
} from './ModalUtils';
|
} from './ModalUtils';
|
||||||
|
|
||||||
let currentSwAsset: any | null = null;
|
class SwAssetModal extends BaseModal {
|
||||||
let isEditMode = false;
|
constructor() {
|
||||||
|
super('sw', '소프트웨어 상세 정보');
|
||||||
|
}
|
||||||
|
|
||||||
const SW_MODAL_HTML = `
|
protected renderFrameHTML(): string {
|
||||||
<div id="sw-asset-modal" class="modal-overlay hidden">
|
return `
|
||||||
<div class="modal-content wide">
|
<div id="sw-asset-modal" class="modal-overlay hidden">
|
||||||
<div class="modal-header">
|
<div class="modal-content wide">
|
||||||
<h2 id="sw-modal-title">소프트웨어 상세 정보</h2>
|
<div class="modal-header">
|
||||||
<button id="btn-close-sw-modal" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></button>
|
<h2 id="sw-modal-title">${this.title}</h2>
|
||||||
</div>
|
<button id="btn-close-sw-modal" class="btn-icon" aria-label="닫기"><i data-lucide="x"></i></button>
|
||||||
<div class="modal-body">
|
</div>
|
||||||
<div class="modal-body-split">
|
<div class="modal-body">
|
||||||
<div class="modal-form-area">
|
<div class="modal-body-split">
|
||||||
<form id="sw-asset-form" class="grid-form">
|
<div class="modal-form-area">
|
||||||
<input type="hidden" id="sw-asset-id" name="id" />
|
<form id="sw-asset-form" class="grid-form">
|
||||||
|
<input type="hidden" id="sw-asset-id" name="id" />
|
||||||
<!-- Group 1: 기본 정보 (Identity) -->
|
|
||||||
<div class="form-section-title">기본 정보 (Identity)</div>
|
<div class="form-section-title">기본 정보 (Identity)</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="sw-asset-type">자산 유형</label>
|
<label>자산 유형</label>
|
||||||
<select id="sw-asset-type" name="asset_type" required>
|
<select id="sw-asset-type" name="asset_type" required>
|
||||||
<option value="내부SW">내부SW</option>
|
<option value="내부SW">내부SW</option>
|
||||||
<option value="외부SW">외부SW</option>
|
<option value="외부SW">외부SW</option>
|
||||||
<option value="클라우드">클라우드</option>
|
<option value="클라우드">클라우드</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
<div class="form-group">
|
||||||
<label for="sw-분야">${ASSET_SCHEMA.SW_FIELD.ui}</label>
|
<label>${ASSET_SCHEMA.SW_FIELD.ui}</label>
|
||||||
<select id="sw-분야" name="sw_field" required>
|
<select id="sw-분야" name="sw_field" required>
|
||||||
<option value="업무공통">업무공통</option>
|
<option value="업무공통">업무공통</option>
|
||||||
<option value="개발S/W">개발S/W</option>
|
<option value="개발S/W">개발S/W</option>
|
||||||
<option value="디자인">디자인</option>
|
<option value="디자인">디자인</option>
|
||||||
<option value="설계S/W">설계S/W</option>
|
<option value="설계S/W">설계S/W</option>
|
||||||
</select>
|
</select>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>${ASSET_SCHEMA.PURCHASE_CORP.ui}</label>
|
||||||
|
<select id="sw-법인" name="purchase_corp" required>${generateOptionsHTML(CORP_LIST)}</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group full-width">
|
||||||
|
<label>${ASSET_SCHEMA.PRODUCT_NAME.ui}</label>
|
||||||
|
<input type="text" id="sw-제품명" name="product_name" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group cloud-only">
|
||||||
|
<label>${ASSET_SCHEMA.DEV_OBJ.ui} / 플랫폼</label>
|
||||||
|
<input type="text" id="sw-플랫폼명" name="dev_objective" placeholder="개발목적 또는 플랫폼명" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>${ASSET_SCHEMA.CURRENT_DEPT.ui}</label>
|
||||||
|
<input type="text" id="sw-부서" name="current_dept" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group sw-user-tracking">
|
||||||
|
<label>${ASSET_SCHEMA.CURRENT_USER.ui}</label>
|
||||||
|
<input type="text" id="sw-user-current" name="user_current" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group sw-user-tracking">
|
||||||
|
<label>${ASSET_SCHEMA.PREV_USER.ui}</label>
|
||||||
|
<input type="text" id="sw-previous-user" name="previous_user" />
|
||||||
|
</div>
|
||||||
|
|
||||||
<div class="form-group">
|
<div class="form-section-title">라이선스 및 계약 정보</div>
|
||||||
<label for="sw-법인">${ASSET_SCHEMA.PURCHASE_CORP.ui}</label>
|
<div class="form-group sw-standard-field">
|
||||||
<select id="sw-법인" name="purchase_corp" required>${generateOptionsHTML(CORP_LIST)}</select>
|
<label>${ASSET_SCHEMA.ASSET_COUNT.ui}</label>
|
||||||
</div>
|
<input type="number" id="sw-수량" name="asset_count" min="0" />
|
||||||
<div class="form-group full-width">
|
</div>
|
||||||
<label for="sw-제품명">${ASSET_SCHEMA.PRODUCT_NAME.ui}</label>
|
<div class="form-group sw-standard-field">
|
||||||
<input type="text" id="sw-제품명" name="product_name" required />
|
<label>${ASSET_SCHEMA.PURCHASE_AMOUNT.ui}</label>
|
||||||
</div>
|
<input type="text" id="sw-금액" name="purchase_amount" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\\\\B(?=(\\\\d{3})+(?!\\\\d))/g, ',')" />
|
||||||
<div class="form-group cloud-only">
|
</div>
|
||||||
<label for="sw-플랫폼명">${ASSET_SCHEMA.DEV_OBJ.ui} / 플랫폼</label>
|
|
||||||
<input type="text" id="sw-플랫폼명" name="dev_objective" placeholder="개발목적 또는 플랫폼명" />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label for="sw-부서">${ASSET_SCHEMA.CURRENT_DEPT.ui}</label>
|
|
||||||
<input type="text" id="sw-부서" name="current_dept" />
|
|
||||||
</div>
|
|
||||||
<div class="form-group sw-user-tracking">
|
|
||||||
<label for="sw-user-current">${ASSET_SCHEMA.CURRENT_USER.ui}</label>
|
|
||||||
<input type="text" id="sw-user-current" name="user_current" />
|
|
||||||
</div>
|
|
||||||
<div class="form-group sw-user-tracking">
|
|
||||||
<label for="sw-previous-user">${ASSET_SCHEMA.PREV_USER.ui}</label>
|
|
||||||
<input type="text" id="sw-previous-user" name="previous_user" />
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- Group 2: 라이선스 및 계약 (License/Contract) -->
|
<div class="form-group cloud-only">
|
||||||
<div class="form-section-title">라이선스 및 계약 정보</div>
|
<label>${ASSET_SCHEMA.EMAIL_ACCOUNT.ui}</label>
|
||||||
<div class="form-group sw-standard-field">
|
<input type="text" id="sw-계정명" name="email_account" />
|
||||||
<label for="sw-수량">${ASSET_SCHEMA.ASSET_COUNT.ui}</label>
|
</div>
|
||||||
<input type="number" id="sw-수량" name="asset_count" min="0" />
|
<div class="form-group cloud-only">
|
||||||
</div>
|
<label>${ASSET_SCHEMA.PURCHASE_METHOD.ui}</label>
|
||||||
<div class="form-group sw-standard-field">
|
<select id="sw-결제수단" name="purchase_method">
|
||||||
<label for="sw-금액">${ASSET_SCHEMA.PURCHASE_AMOUNT.ui}</label>
|
<option value="">선택안함</option>
|
||||||
<input type="text" id="sw-금액" name="purchase_amount" oninput="this.value = this.value.replace(/[^0-9]/g, '').replace(/\\B(?=(\\d{3})+(?!\\d))/g, ',')" />
|
<option value="법인카드">법인카드</option>
|
||||||
</div>
|
<option value="인보이스">인보이스</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
|
||||||
<!-- Group 3: 클라우드 전용 정보 (Cloud Specific) -->
|
<div class="form-section-title">관리 및 비고</div>
|
||||||
<div class="form-group cloud-only">
|
<div class="form-group sw-standard-field">
|
||||||
<label for="sw-계정명">${ASSET_SCHEMA.EMAIL_ACCOUNT.ui}</label>
|
<label>${ASSET_SCHEMA.PURCHASE_DATE.ui}</label>
|
||||||
<input type="text" id="sw-계정명" name="email_account" />
|
<div style="display:flex; gap:0.25rem; align-items:center; position:relative;">
|
||||||
</div>
|
<input type="text" id="sw-구매일" name="purchase_date" style="flex:1;" />
|
||||||
<div class="form-group cloud-only">
|
<button type="button" class="btn-icon" onclick="const p = document.getElementById('sw-구매일-picker'); p.value = document.getElementById('sw-구매일').value; p.showPicker();" style="padding:0.25rem;">
|
||||||
<label for="sw-결제수단">${ASSET_SCHEMA.PURCHASE_METHOD.ui}</label>
|
<i data-lucide="calendar" style="width:18px; height:18px; color:var(--primary-color);"></i>
|
||||||
<select id="sw-결제수단" name="purchase_method">
|
</button>
|
||||||
<option value="">선택안함</option>
|
<input type="date" id="sw-구매일-picker" style="position:absolute; width:0; height:0; opacity:0; pointer-events:none;" onchange="document.getElementById('sw-구매일').value = this.value" tabindex="-1" />
|
||||||
<option value="법인카드">법인카드</option>
|
</div>
|
||||||
<option value="인보이스">인보이스</option>
|
</div>
|
||||||
</select>
|
<div class="form-group sw-standard-field">
|
||||||
</div>
|
<label>${ASSET_SCHEMA.PURCHASE_VENDOR.ui}</label>
|
||||||
|
<input type="text" id="sw-납품업체" name="purchase_vendor" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group sw-standard-field">
|
||||||
|
<label>${ASSET_SCHEMA.DEV_MGR.ui}</label>
|
||||||
|
<input type="text" id="sw-개발담당자" name="dev_manager" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group sw-standard-field">
|
||||||
|
<label>${ASSET_SCHEMA.PLANNING_MGR.ui}</label>
|
||||||
|
<input type="text" id="sw-기획담당자" name="planning_manager" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group sw-standard-field">
|
||||||
|
<label>${ASSET_SCHEMA.SALES_MGR.ui}</label>
|
||||||
|
<input type="text" id="sw-영업담당자" name="sales_manager" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group sw-standard-field" id="sw-expiry-group">
|
||||||
|
<label>${ASSET_SCHEMA.EXPIRED_DATE.ui}</label>
|
||||||
|
<div style="display:flex; gap:0.25rem; align-items:center; position:relative;">
|
||||||
|
<input type="text" id="sw-만료일" name="expiry_date" style="flex:1;" />
|
||||||
|
<button type="button" class="btn-icon" onclick="const p = document.getElementById('sw-만료일-picker'); p.value = document.getElementById('sw-만료일').value; p.showPicker();" style="padding:0.25rem;">
|
||||||
|
<i data-lucide="calendar" style="width:18px; height:18px; color:var(--primary-color);"></i>
|
||||||
|
</button>
|
||||||
|
<input type="date" id="sw-만료일-picker" style="position:absolute; width:0; height:0; opacity:0; pointer-events:none;" onchange="document.getElementById('sw-만료일').value = this.value" tabindex="-1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group full-width">
|
||||||
|
<label>${ASSET_SCHEMA.MEMO.ui}</label>
|
||||||
|
<textarea id="sw-비고" name="memo" rows="2"></textarea>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
<!-- Group 4: 관리 정보 (Management) -->
|
<div id="sw-user-section" class="user-management-section" style="margin-top: 2rem; border-top: 1px solid var(--border-color); padding-top: 1.5rem;">
|
||||||
<div class="form-section-title">관리 및 비고</div>
|
<button type="button" id="btn-open-sw-user" class="btn btn-outline btn-sm" title="사용자 관리">
|
||||||
<div class="form-group sw-standard-field">
|
<i data-lucide="users" style="width:16px; height:16px; margin-right:4px;"></i> 사용자 관리
|
||||||
<label for="sw-구매일">${ASSET_SCHEMA.PURCHASE_DATE.ui}</label>
|
|
||||||
<div style="display:flex; gap:0.25rem; align-items:center; position:relative;">
|
|
||||||
<input type="text" id="sw-구매일" name="purchase_date" style="flex:1;" />
|
|
||||||
<button type="button" class="btn-icon" onclick="const p = document.getElementById('sw-구매일-picker'); p.value = document.getElementById('sw-구매일').value; p.showPicker();" style="padding:0.25rem;">
|
|
||||||
<i data-lucide="calendar" style="width:18px; height:18px; color:var(--primary-color);"></i>
|
|
||||||
</button>
|
</button>
|
||||||
<input type="date" id="sw-구매일-picker" style="position:absolute; width:0; height:0; opacity:0; pointer-events:none;" onchange="document.getElementById('sw-구매일').value = this.value" tabindex="-1" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group sw-standard-field">
|
|
||||||
<label for="sw-납품업체">${ASSET_SCHEMA.PURCHASE_VENDOR.ui}</label>
|
<div class="modal-history-area">
|
||||||
<input type="text" id="sw-납품업체" name="purchase_vendor" />
|
<div class="history-header" style="display:flex; justify-content:space-between; align-items:center;">
|
||||||
</div>
|
<h3><i data-lucide="history" style="width:16px; height:16px;"></i> 업데이트 내역</h3>
|
||||||
<div class="form-group sw-standard-field">
|
<button type="button" id="btn-open-sw-update" class="btn btn-outline btn-sm">
|
||||||
<label for="sw-개발담당자">${ASSET_SCHEMA.DEV_MGR.ui}</label>
|
계약 업데이트 <i data-lucide="refresh-ccw" style="width:14px; height:14px;"></i>
|
||||||
<input type="text" id="sw-개발담당자" name="dev_manager" />
|
|
||||||
</div>
|
|
||||||
<div class="form-group sw-standard-field">
|
|
||||||
<label for="sw-기획담당자">${ASSET_SCHEMA.PLANNING_MGR.ui}</label>
|
|
||||||
<input type="text" id="sw-기획담당자" name="planning_manager" />
|
|
||||||
</div>
|
|
||||||
<div class="form-group sw-standard-field">
|
|
||||||
<label for="sw-영업담당자">${ASSET_SCHEMA.SALES_MGR.ui}</label>
|
|
||||||
<input type="text" id="sw-영업담당자" name="sales_manager" />
|
|
||||||
</div>
|
|
||||||
<div class="form-group sw-standard-field" id="sw-expiry-group">
|
|
||||||
<label for="sw-만료일">${ASSET_SCHEMA.EXPIRED_DATE.ui}</label>
|
|
||||||
<div style="display:flex; gap:0.25rem; align-items:center; position:relative;">
|
|
||||||
<input type="text" id="sw-만료일" name="expiry_date" style="flex:1;" />
|
|
||||||
<button type="button" class="btn-icon" onclick="const p = document.getElementById('sw-만료일-picker'); p.value = document.getElementById('sw-만료일').value; p.showPicker();" style="padding:0.25rem;">
|
|
||||||
<i data-lucide="calendar" style="width:18px; height:18px; color:var(--primary-color);"></i>
|
|
||||||
</button>
|
</button>
|
||||||
<input type="date" id="sw-만료일-picker" style="position:absolute; width:0; height:0; opacity:0; pointer-events:none;" onchange="document.getElementById('sw-만료일').value = this.value" tabindex="-1" />
|
</div>
|
||||||
|
<div id="sw-history-list" class="history-timeline"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button id="btn-delete-sw-asset" class="btn btn-outline btn-danger">삭제</button>
|
||||||
|
<div class="footer-actions">
|
||||||
|
<button id="btn-revert-sw-edit" class="btn btn-outline hidden">수정 취소</button>
|
||||||
|
<button id="btn-cancel-sw-modal" class="btn btn-outline">닫기</button>
|
||||||
|
<button id="btn-save-sw-asset" class="btn btn-primary">수정</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<!-- 계약 업데이트 서브 모달 -->
|
||||||
|
<div id="sw-update-modal" class="modal-overlay hidden" style="z-index: 1100;">
|
||||||
|
<div class="modal-content" style="max-width: 500px;">
|
||||||
|
<div class="modal-header">
|
||||||
|
<h2>계약 업데이트 반영</h2>
|
||||||
|
<button id="btn-close-sw-update" class="btn-icon"><i data-lucide="x"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<div class="grid-form" style="grid-template-columns: 1fr;">
|
||||||
|
<div class="form-group">
|
||||||
|
<label>업데이트 일자</label>
|
||||||
|
<input type="date" id="sw-update-date" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group sub-sw-update">
|
||||||
|
<label>새로운 계약 기간</label>
|
||||||
|
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
||||||
|
<input type="text" id="sw-update-start" placeholder="YYYY-MM-DD" style="flex: 1;" />
|
||||||
|
<span>~</span>
|
||||||
|
<input type="text" id="sw-update-end" placeholder="YYYY-MM-DD" style="flex: 1;" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group full-width">
|
<div class="form-group">
|
||||||
<label for="sw-비고">${ASSET_SCHEMA.MEMO.ui}</label>
|
<label>발생 비용</label>
|
||||||
<textarea id="sw-비고" name="memo" rows="2"></textarea>
|
<input type="text" id="sw-update-cost" oninput="this.value = this.value.replace(/[^0-9]/g, '') ? Number(this.value.replace(/[^0-9]/g, '')).toLocaleString() : ''" placeholder="ex) 500,000" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>상세 내용 (메모)</label>
|
||||||
|
<input type="text" id="sw-update-note" placeholder="예: 25년도 구독 연장 결제 완료" />
|
||||||
</div>
|
</div>
|
||||||
</form>
|
|
||||||
|
|
||||||
<div id="sw-user-section" class="user-management-section" style="margin-top: 2rem; border-top: 1px solid var(--border-color); padding-top: 1.5rem;">
|
|
||||||
<button type="button" id="btn-open-sw-user" class="btn btn-outline btn-sm" title="사용자 관리">
|
|
||||||
<i data-lucide="users" style="width:16px; height:16px; margin-right:4px;"></i> 사용자 관리
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
<div class="modal-history-area">
|
<div></div>
|
||||||
<div class="history-header" style="display:flex; justify-content:space-between; align-items:center;">
|
<div class="footer-actions">
|
||||||
<h3><i data-lucide="history" style="width:16px; height:16px;"></i> 업데이트 내역</h3>
|
<button id="btn-cancel-sw-update" class="btn btn-outline">취소</button>
|
||||||
<button type="button" id="btn-open-sw-update" class="btn btn-outline btn-sm">
|
<button id="btn-save-sw-update" class="btn btn-primary">반영하기</button>
|
||||||
계약 업데이트 <i data-lucide="refresh-ccw" style="width:14px; height:14px;"></i>
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
<div id="sw-history-list" class="history-timeline"></div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button id="btn-delete-sw-asset" class="btn btn-outline btn-danger">삭제</button>
|
|
||||||
<div class="footer-actions">
|
|
||||||
<button id="btn-revert-sw-edit" class="btn btn-outline hidden">수정 취소</button>
|
|
||||||
<button id="btn-cancel-sw-modal" class="btn btn-outline">닫기</button>
|
|
||||||
<button id="btn-save-sw-asset" class="btn btn-primary">수정</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 계약/유지보수 기간 갱신 및 업데이트 모달 -->
|
|
||||||
<div id="sw-update-modal" class="modal-overlay hidden" style="z-index: 1100;">
|
|
||||||
<div class="modal-content" style="max-width: 500px;">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2>계약 업데이트 반영</h2>
|
|
||||||
<button id="btn-close-sw-update" class="btn-icon"><i data-lucide="x"></i></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="grid-form" style="grid-template-columns: 1fr;">
|
|
||||||
<div class="form-group">
|
|
||||||
<label>업데이트 일자</label>
|
|
||||||
<input type="date" id="sw-update-date" />
|
|
||||||
</div>
|
|
||||||
<div class="form-group sub-sw-update">
|
|
||||||
<label>새로운 계약 기간</label>
|
|
||||||
<div style="display: flex; align-items: center; gap: 0.5rem;">
|
|
||||||
<input type="text" id="sw-update-start" placeholder="YYYY-MM-DD" style="flex: 1;" />
|
|
||||||
<span>~</span>
|
|
||||||
<input type="text" id="sw-update-end" placeholder="YYYY-MM-DD" style="flex: 1;" />
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-group">
|
|
||||||
<label>발생 비용</label>
|
|
||||||
<input type="text" id="sw-update-cost" oninput="this.value = this.value.replace(/[^0-9]/g, '') ? Number(this.value.replace(/[^0-9]/g, '')).toLocaleString() : ''" placeholder="ex) 500,000" />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>상세 내용 (메모)</label>
|
|
||||||
<input type="text" id="sw-update-note" placeholder="예: 25년도 구독 연장 결제 완료" />
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-footer">
|
`;
|
||||||
<div></div>
|
}
|
||||||
<div class="footer-actions">
|
|
||||||
<button id="btn-cancel-sw-update" class="btn btn-outline">취소</button>
|
|
||||||
<button id="btn-save-sw-update" class="btn btn-primary">반영하기</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
function applySwTypeUI(type: string) {
|
protected initChildLogic(onSave: () => void, closeModals: () => void): void {
|
||||||
const cloudFields = document.querySelectorAll('.cloud-only');
|
const saveBtn = document.getElementById('btn-save-sw-asset')!;
|
||||||
const swFields = document.querySelectorAll('.sw-standard-field');
|
const revertBtn = document.getElementById('btn-revert-sw-edit')!;
|
||||||
const userSection = document.getElementById('sw-user-section');
|
const deleteBtn = document.getElementById('btn-delete-sw-asset')!;
|
||||||
const expiryGroup = document.getElementById('sw-expiry-group');
|
const typeSelect = document.getElementById('sw-asset-type') as HTMLSelectElement;
|
||||||
const userTracking = document.querySelectorAll('.sw-user-tracking');
|
const userAssignBtn = document.getElementById('btn-open-sw-user')!;
|
||||||
|
const btnOpenUpdate = document.getElementById('btn-open-sw-update')!;
|
||||||
|
|
||||||
if (type === '클라우드') {
|
typeSelect?.addEventListener('change', () => this.applySwTypeUI(typeSelect.value));
|
||||||
cloudFields.forEach(el => (el as HTMLElement).style.display = 'flex');
|
|
||||||
swFields.forEach(el => (el as HTMLElement).style.display = 'none');
|
|
||||||
if (userSection) userSection.style.display = 'none';
|
|
||||||
userTracking.forEach(el => (el as HTMLElement).style.display = 'none');
|
|
||||||
} else {
|
|
||||||
cloudFields.forEach(el => (el as HTMLElement).style.display = 'none');
|
|
||||||
swFields.forEach(el => (el as HTMLElement).style.display = 'flex');
|
|
||||||
if (userSection) userSection.style.display = 'block';
|
|
||||||
|
|
||||||
if (type === '외부SW' || type === '내부SW') {
|
['sw-구매일', 'sw-시작일', 'sw-만료일', 'sw-update-start', 'sw-update-end'].forEach(id => {
|
||||||
if (expiryGroup) expiryGroup.style.display = 'flex';
|
const el = document.getElementById(id) as HTMLInputElement;
|
||||||
|
if (el) applyDateMask(el);
|
||||||
|
});
|
||||||
|
|
||||||
|
userAssignBtn.addEventListener('click', () => {
|
||||||
|
if (this.currentAsset) openSwUserModal(this.currentAsset);
|
||||||
|
});
|
||||||
|
|
||||||
|
// 업데이트 모달 로직
|
||||||
|
const subModal = document.getElementById('sw-update-modal')!;
|
||||||
|
const closeUpdate = () => subModal.classList.add('hidden');
|
||||||
|
document.getElementById('btn-close-sw-update')?.addEventListener('click', closeUpdate);
|
||||||
|
document.getElementById('btn-cancel-sw-update')?.addEventListener('click', closeUpdate);
|
||||||
|
|
||||||
|
btnOpenUpdate?.addEventListener('click', (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
if (!this.isEditMode) { alert('자산을 수정 모드로 변경한 후 업데이트를 진행해주세요.'); return; }
|
||||||
|
subModal.classList.remove('hidden');
|
||||||
|
});
|
||||||
|
|
||||||
|
document.getElementById('btn-save-sw-update')?.addEventListener('click', async (e) => {
|
||||||
|
e.preventDefault();
|
||||||
|
const date = (document.getElementById('sw-update-date') as HTMLInputElement).value;
|
||||||
|
const start = (document.getElementById('sw-update-start') as HTMLInputElement).value;
|
||||||
|
const end = (document.getElementById('sw-update-end') as HTMLInputElement).value;
|
||||||
|
const cost = (document.getElementById('sw-update-cost') as HTMLInputElement).value;
|
||||||
|
const note = (document.getElementById('sw-update-note') as HTMLInputElement).value;
|
||||||
|
|
||||||
|
if (start) setFieldValue('sw-시작일', start);
|
||||||
|
if (end) setFieldValue('sw-만료일', end);
|
||||||
|
if (cost) setFieldValue('sw-금액', cost);
|
||||||
|
|
||||||
|
const log = { assetId: this.currentAsset.id, date, details: `[계약갱신] ${note} (${start} ~ ${end}, 비용: ${cost})`, user: '관리자' };
|
||||||
|
await fetch(`${API_BASE_URL}/api/asset/history/batch`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify([...state.masterData.logs, log])
|
||||||
|
});
|
||||||
|
|
||||||
|
closeUpdate(); onSave();
|
||||||
|
});
|
||||||
|
|
||||||
|
revertBtn.addEventListener('click', () => {
|
||||||
|
this.setEditLockMode('view');
|
||||||
|
if (this.currentAsset) this.fillFormData(this.currentAsset);
|
||||||
|
});
|
||||||
|
|
||||||
|
saveBtn.addEventListener('click', async () => {
|
||||||
|
if (!this.currentAsset) return;
|
||||||
|
if (!this.isEditMode) { this.setEditLockMode('edit'); this.isEditMode = true; return; }
|
||||||
|
|
||||||
|
const type = getFieldValue('sw-asset-type');
|
||||||
|
const formData = new FormData(this.formEl!);
|
||||||
|
const updated = { ...this.currentAsset };
|
||||||
|
formData.forEach((value, key) => { updated[key] = value; });
|
||||||
|
|
||||||
// 외부SW에만 현 사용자/직전 사용자 표시 (내부SW는 user tracking 제외 요청됨)
|
let categoryKey = (type === '내부SW') ? 'swInternal' : (type === '클라우드' ? 'cloud' : 'swExternal');
|
||||||
userTracking.forEach(el => (el as HTMLElement).style.display = (type === '외부SW') ? 'flex' : 'none');
|
if (await saveAsset(categoryKey, updated)) { onSave(); this.close(); closeModals(); }
|
||||||
|
});
|
||||||
|
|
||||||
|
deleteBtn.addEventListener('click', async () => {
|
||||||
|
if (!this.currentAsset || !confirm(UI_TEXT.MESSAGES.CONFIRM_DELETE)) return;
|
||||||
|
const type = this.currentAsset.asset_type || this.currentAsset.type;
|
||||||
|
let categoryKey = (type === '내부SW') ? 'swInternal' : (type === '클라우드' ? 'cloud' : 'swExternal');
|
||||||
|
if (await deleteAsset(categoryKey, this.currentAsset.id)) {
|
||||||
|
alert('성공적으로 삭제되었습니다.'); onSave(); this.close(); closeModals();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
createIcons({ icons: { History, Plus, Save, Calendar, Users, RotateCcw } });
|
||||||
|
}
|
||||||
|
|
||||||
|
protected fillFormData(asset: any): void {
|
||||||
|
setFieldValue('sw-asset-id', asset.id);
|
||||||
|
setFieldValue('sw-asset-type', asset.asset_type || asset.type);
|
||||||
|
setFieldValue('sw-분야', asset.sw_field || '');
|
||||||
|
setFieldValue('sw-법인', asset.purchase_corp || '');
|
||||||
|
setFieldValue('sw-부서', asset.current_dept || '');
|
||||||
|
setFieldValue('sw-user-current', asset.user_current || '');
|
||||||
|
setFieldValue('sw-previous-user', asset.previous_user || '');
|
||||||
|
setFieldValue('sw-제품명', asset.product_name || '');
|
||||||
|
setFieldValue('sw-수량', asset.asset_count || '');
|
||||||
|
setFieldValue('sw-금액', asset.purchase_amount || '');
|
||||||
|
setFieldValue('sw-구매일', asset.purchase_date || '');
|
||||||
|
setFieldValue('sw-납품업체', asset.purchase_vendor || '');
|
||||||
|
setFieldValue('sw-개발담당자', asset.dev_manager || '');
|
||||||
|
setFieldValue('sw-기획담당자', asset.planning_manager || '');
|
||||||
|
setFieldValue('sw-영업담당자', asset.sales_manager || '');
|
||||||
|
setFieldValue('sw-비고', asset.memo || '');
|
||||||
|
|
||||||
|
if (asset.type === '클라우드' || asset.asset_type === '클라우드') {
|
||||||
|
setFieldValue('sw-플랫폼명', asset.dev_objective || '');
|
||||||
|
setFieldValue('sw-계정명', asset.email_account || '');
|
||||||
|
setFieldValue('sw-결제수단', asset.purchase_method || '');
|
||||||
|
} else {
|
||||||
|
setFieldValue('sw-만료일', asset.expiry_date || '');
|
||||||
|
}
|
||||||
|
|
||||||
|
this.renderHistory(asset.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onAfterOpen(asset: any, mode: string): void {
|
||||||
|
this.applySwTypeUI(asset.asset_type || asset.type);
|
||||||
|
}
|
||||||
|
|
||||||
|
private applySwTypeUI(type: string) {
|
||||||
|
const cloudFields = document.querySelectorAll('.cloud-only');
|
||||||
|
const swFields = document.querySelectorAll('.sw-standard-field');
|
||||||
|
const userSection = document.getElementById('sw-user-section');
|
||||||
|
const expiryGroup = document.getElementById('sw-expiry-group');
|
||||||
|
const userTracking = document.querySelectorAll('.sw-user-tracking');
|
||||||
|
|
||||||
|
if (type === '클라우드') {
|
||||||
|
cloudFields.forEach(el => (el as HTMLElement).style.display = 'flex');
|
||||||
|
swFields.forEach(el => (el as HTMLElement).style.display = 'none');
|
||||||
|
if (userSection) userSection.style.display = 'none';
|
||||||
|
userTracking.forEach(el => (el as HTMLElement).style.display = 'none');
|
||||||
|
} else {
|
||||||
|
cloudFields.forEach(el => (el as HTMLElement).style.display = 'none');
|
||||||
|
swFields.forEach(el => (el as HTMLElement).style.display = 'flex');
|
||||||
|
if (userSection) userSection.style.display = 'block';
|
||||||
|
if (type === '외부SW' || type === '내부SW') {
|
||||||
|
if (expiryGroup) expiryGroup.style.display = 'flex';
|
||||||
|
userTracking.forEach(el => (el as HTMLElement).style.display = (type === '외부SW') ? 'flex' : 'none');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private renderHistory(swId: string) {
|
||||||
|
const container = document.getElementById('sw-history-list');
|
||||||
|
if (!container) return;
|
||||||
|
const logs = (state.masterData.logs || []).filter(l => l.assetId === swId);
|
||||||
|
if (logs.length === 0) { container.innerHTML = '<div class="empty-history">수정 이력이 없습니다.</div>'; return; }
|
||||||
|
container.innerHTML = logs.map(l => `<div class=\"history-item\"><div class=\"history-date\">${l.date}</div><div class=\"history-user\">${l.user}</div><div class=\"history-details\">${l.details}</div></div>`).join('');
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function fillSwFormData(asset: any) {
|
export const swModal = new SwAssetModal();
|
||||||
setFieldValue('sw-asset-id', asset.id);
|
|
||||||
setFieldValue('sw-asset-type', asset.asset_type || asset.type);
|
|
||||||
setFieldValue('sw-분야', asset.sw_field || '');
|
|
||||||
setFieldValue('sw-법인', asset.purchase_corp || '');
|
|
||||||
|
|
||||||
setFieldValue('sw-부서', asset.current_dept || '');
|
export function initSwModal(onSave: () => void, closeModals: () => void) {
|
||||||
setFieldValue('sw-user-current', asset.user_current || '');
|
swModal.init(onSave, closeModals);
|
||||||
setFieldValue('sw-previous-user', asset.previous_user || '');
|
|
||||||
setFieldValue('sw-previous_dept', asset.previous_dept || '');
|
|
||||||
setFieldValue('sw-제품명', asset.product_name || '');
|
|
||||||
setFieldValue('sw-수량', asset.asset_count || '');
|
|
||||||
setFieldValue('sw-금액', asset.purchase_amount || '');
|
|
||||||
setFieldValue('sw-구매일', asset.purchase_date || '');
|
|
||||||
setFieldValue('sw-시작일', asset.start_date || '');
|
|
||||||
setFieldValue('sw-납품업체', asset.purchase_vendor || '');
|
|
||||||
setFieldValue('sw-개발담당자', asset.dev_manager || '');
|
|
||||||
setFieldValue('sw-기획담당자', asset.planning_manager || '');
|
|
||||||
setFieldValue('sw-영업담당자', asset.sales_manager || '');
|
|
||||||
setFieldValue('sw-비고', asset.memo || '');
|
|
||||||
|
|
||||||
if (asset.type === '클라우드' || asset.asset_type === '클라우드') {
|
|
||||||
setFieldValue('sw-플랫폼명', asset.dev_objective || '');
|
|
||||||
setFieldValue('sw-계정명', asset.email_account || '');
|
|
||||||
setFieldValue('sw-결제수단', asset.purchase_method || '');
|
|
||||||
} else {
|
|
||||||
setFieldValue('sw-만료일', asset.expiry_date || '');
|
|
||||||
}
|
|
||||||
|
|
||||||
renderSwHistory(asset.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderSwHistory(swId: string) {
|
|
||||||
const container = document.getElementById('sw-history-list');
|
|
||||||
if (!container) return;
|
|
||||||
const logs = (state.masterData.logs || []).filter(l => l.assetId === swId);
|
|
||||||
if (logs.length === 0) {
|
|
||||||
container.innerHTML = '<div class="empty-history">수정 이력이 없습니다.</div>';
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
container.innerHTML = logs.map(l => `
|
|
||||||
<div class="history-item">
|
|
||||||
<div class="history-date">${l.date}</div>
|
|
||||||
<div class="history-user">${l.user}</div>
|
|
||||||
<div class="history-details">${l.details}</div>
|
|
||||||
</div>
|
|
||||||
`).join('');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export function openSwModal(asset: any, mode: 'view' | 'add' | 'edit' = 'view') {
|
export function openSwModal(asset: any, mode: 'view' | 'add' | 'edit' = 'view') {
|
||||||
currentSwAsset = asset;
|
swModal.open(asset, mode);
|
||||||
const modal = document.getElementById('sw-asset-modal')!;
|
|
||||||
|
|
||||||
setEditLock('sw-asset-form', mode, {
|
|
||||||
saveBtnId: 'btn-save-sw-asset',
|
|
||||||
revertBtnId: 'btn-revert-sw-edit'
|
|
||||||
});
|
|
||||||
|
|
||||||
isEditMode = (mode === 'add' || mode === 'edit');
|
|
||||||
|
|
||||||
fillSwFormData(asset);
|
|
||||||
applySwTypeUI(asset.asset_type || asset.type);
|
|
||||||
|
|
||||||
modal.classList.remove('hidden');
|
|
||||||
createIcons({ icons: { X, History, Plus } });
|
|
||||||
}
|
|
||||||
|
|
||||||
export function initSwModal(onSave: () => void, closeModals: () => void) {
|
|
||||||
if (!document.getElementById('sw-asset-modal')) {
|
|
||||||
document.body.insertAdjacentHTML('beforeend', SW_MODAL_HTML);
|
|
||||||
}
|
|
||||||
|
|
||||||
const form = document.getElementById('sw-asset-form') as HTMLFormElement;
|
|
||||||
const saveBtn = document.getElementById('btn-save-sw-asset')!;
|
|
||||||
const revertBtn = document.getElementById('btn-revert-sw-edit')!;
|
|
||||||
const deleteBtn = document.getElementById('btn-delete-sw-asset')!;
|
|
||||||
const userAssignBtn = document.getElementById('btn-open-sw-user')!;
|
|
||||||
const btnOpenUpdate = document.getElementById('btn-open-sw-update')!;
|
|
||||||
const typeSelect = document.getElementById('sw-asset-type') as HTMLSelectElement;
|
|
||||||
|
|
||||||
typeSelect?.addEventListener('change', () => {
|
|
||||||
applySwTypeUI(typeSelect.value);
|
|
||||||
});
|
|
||||||
|
|
||||||
['sw-구매일', 'sw-시작일', 'sw-만료일', 'sw-update-start', 'sw-update-end'].forEach(id => {
|
|
||||||
applyDateMask(document.getElementById(id) as HTMLInputElement);
|
|
||||||
});
|
|
||||||
|
|
||||||
createIcons({ icons: { Calendar } });
|
|
||||||
|
|
||||||
const closeModalAction = () => { closeModals(); isEditMode = false; };
|
|
||||||
document.getElementById('btn-close-sw-modal')?.addEventListener('click', closeModalAction);
|
|
||||||
document.getElementById('btn-cancel-sw-modal')?.addEventListener('click', closeModalAction);
|
|
||||||
|
|
||||||
revertBtn.addEventListener('click', () => {
|
|
||||||
setEditLock('sw-asset-form', 'view', {
|
|
||||||
saveBtnId: 'btn-save-sw-asset',
|
|
||||||
revertBtnId: 'btn-revert-sw-edit'
|
|
||||||
});
|
|
||||||
isEditMode = false;
|
|
||||||
if (currentSwAsset) fillSwFormData(currentSwAsset);
|
|
||||||
});
|
|
||||||
|
|
||||||
saveBtn.addEventListener('click', async () => {
|
|
||||||
if (!currentSwAsset) return;
|
|
||||||
if (!isEditMode) {
|
|
||||||
setEditLock('sw-asset-form', 'edit', {
|
|
||||||
saveBtnId: 'btn-save-sw-asset',
|
|
||||||
revertBtnId: 'btn-revert-sw-edit'
|
|
||||||
});
|
|
||||||
isEditMode = true;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const type = getFieldValue('sw-asset-type');
|
|
||||||
const formData = new FormData(form);
|
|
||||||
const updated: any = { ...currentSwAsset };
|
|
||||||
formData.forEach((value, key) => {
|
|
||||||
updated[key] = value;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Mapping for generic saveAsset
|
|
||||||
let categoryKey = 'swExternal';
|
|
||||||
if (type === '내부SW') categoryKey = 'swInternal';
|
|
||||||
else if (type === '클라우드') categoryKey = 'cloud';
|
|
||||||
|
|
||||||
const success = await saveAsset(categoryKey, updated);
|
|
||||||
if (success) {
|
|
||||||
onSave();
|
|
||||||
closeModalAction();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
deleteBtn.addEventListener('click', async () => {
|
|
||||||
if (!currentSwAsset) return;
|
|
||||||
if (!confirm(UI_TEXT.MESSAGES.CONFIRM_DELETE)) return;
|
|
||||||
|
|
||||||
const type = currentSwAsset.asset_type || currentSwAsset.type;
|
|
||||||
let categoryKey = 'swExternal';
|
|
||||||
if (type === '내부SW') categoryKey = 'swInternal';
|
|
||||||
else if (type === '클라우드') categoryKey = 'cloud';
|
|
||||||
|
|
||||||
const success = await deleteAsset(categoryKey, currentSwAsset.id);
|
|
||||||
if (success) {
|
|
||||||
alert('성공적으로 삭제되었습니다.');
|
|
||||||
onSave(); // Refresh list
|
|
||||||
closeModalAction();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
userAssignBtn.addEventListener('click', () => {
|
|
||||||
if (currentSwAsset) openSwUserModal(currentSwAsset);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 자산 업데이트(계약 갱신) 모달 로직
|
|
||||||
const subModal = document.getElementById('sw-update-modal')!;
|
|
||||||
const btnCloseUpdate = document.getElementById('btn-close-sw-update')!;
|
|
||||||
const btnCancelUpdate = document.getElementById('btn-cancel-sw-update')!;
|
|
||||||
const btnSaveUpdate = document.getElementById('btn-save-sw-update')!;
|
|
||||||
|
|
||||||
const closeUpdateModal = () => subModal.classList.add('hidden');
|
|
||||||
btnCloseUpdate?.addEventListener('click', closeUpdateModal);
|
|
||||||
btnCancelUpdate?.addEventListener('click', closeUpdateModal);
|
|
||||||
|
|
||||||
btnOpenUpdate?.addEventListener('click', (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
if (!isEditMode) {
|
|
||||||
alert('자산을 수정 모드로 변경한 후 업데이트를 진행해주세요.');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
subModal.classList.remove('hidden');
|
|
||||||
});
|
|
||||||
|
|
||||||
btnSaveUpdate?.addEventListener('click', async (e) => {
|
|
||||||
e.preventDefault();
|
|
||||||
const date = (document.getElementById('sw-update-date') as HTMLInputElement).value;
|
|
||||||
const start = (document.getElementById('sw-update-start') as HTMLInputElement).value;
|
|
||||||
const end = (document.getElementById('sw-update-end') as HTMLInputElement).value;
|
|
||||||
const cost = (document.getElementById('sw-update-cost') as HTMLInputElement).value;
|
|
||||||
const note = (document.getElementById('sw-update-note') as HTMLInputElement).value;
|
|
||||||
|
|
||||||
if (start) setFieldValue('sw-시작일', start);
|
|
||||||
if (end) setFieldValue('sw-만료일', end);
|
|
||||||
if (cost) setFieldValue('sw-금액', cost);
|
|
||||||
|
|
||||||
// Save as log
|
|
||||||
const log = {
|
|
||||||
assetId: currentSwAsset.id,
|
|
||||||
date,
|
|
||||||
details: `[계약갱신] ${note} (${start} ~ ${end}, 비용: ${cost})`,
|
|
||||||
user: '관리자'
|
|
||||||
};
|
|
||||||
|
|
||||||
// Call generic API for logs (could be added to state.ts)
|
|
||||||
await fetch(`${API_BASE_URL}/api/asset/history/batch`, {
|
|
||||||
method: 'POST',
|
|
||||||
headers: { 'Content-Type': 'application/json' },
|
|
||||||
body: JSON.stringify([...state.masterData.logs, log])
|
|
||||||
});
|
|
||||||
|
|
||||||
closeUpdateModal();
|
|
||||||
onSave();
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,280 +1,267 @@
|
|||||||
import { state } from '../../core/state';
|
import { state } from '../../core/state';
|
||||||
import { SoftwareAsset, SWUser } from '../../core/excelHandler';
|
import { BaseModal } from './BaseModal';
|
||||||
import { openModal } from './BaseModal';
|
import { createIcons, Edit2, X, Paperclip, Calendar, Plus } from 'lucide';
|
||||||
import { createIcons, Edit2, X, Paperclip, Calendar } from 'lucide';
|
import { ORG_LIST } from './SharedData';
|
||||||
import { CORP_LIST, ORG_LIST } from './SharedData';
|
|
||||||
import { generateOptionsHTML, setFieldValue, getFieldValue, applyDateMask } from './ModalUtils';
|
import { generateOptionsHTML, setFieldValue, getFieldValue, applyDateMask } from './ModalUtils';
|
||||||
|
|
||||||
let currentSwUserAsset: SoftwareAsset | null = null;
|
class SwUserModal extends BaseModal {
|
||||||
let tempSwUsers: any[] = [];
|
private tempSwUsers: any[] = [];
|
||||||
|
|
||||||
const SW_USER_MODAL_HTML = `
|
constructor() {
|
||||||
<div id="sw-user-modal" class="modal-overlay hidden">
|
super('sw-user', '소프트웨어 사용자 관리');
|
||||||
<div class="modal-content wide">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h2 id="sw-user-title">소프트웨어 사용자 관리</h2>
|
|
||||||
<button id="btn-close-sw-user-modal" class="btn-icon"><i data-lucide="x"></i></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<div class="sw-info-summary" id="sw-user-sw-info"></div>
|
|
||||||
|
|
||||||
<div class="user-list-toolbar" style="display:flex; justify-content:space-between; margin-bottom:1rem; align-items:center;">
|
|
||||||
<h3 style="font-size:1rem; font-weight:600;">할당된 사용자 목록</h3>
|
|
||||||
<button type="button" id="btn-open-add-user" class="btn btn-primary btn-sm"><i data-lucide="plus"></i> 사용자 추가</button>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="table-container">
|
|
||||||
<table>
|
|
||||||
<thead>
|
|
||||||
<tr>
|
|
||||||
<th>조직</th>
|
|
||||||
<th>부서</th>
|
|
||||||
<th>직위</th>
|
|
||||||
<th>이름</th>
|
|
||||||
<th>사용기간</th>
|
|
||||||
<th>신청서</th>
|
|
||||||
<th>관리</th>
|
|
||||||
</tr>
|
|
||||||
</thead>
|
|
||||||
<tbody id="sw-user-table-body"></tbody>
|
|
||||||
</table>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button id="btn-cancel-sw-user" class="btn btn-outline">취소</button>
|
|
||||||
<button id="btn-save-sw-user" class="btn btn-primary">저장</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 사용자 추가/수정 서브 모달 -->
|
|
||||||
<div id="sw-user-edit-modal" class="modal-overlay hidden" style="z-index:1100;">
|
|
||||||
<div class="modal-content" style="width:400px;">
|
|
||||||
<div class="modal-header">
|
|
||||||
<h3 id="sw-user-edit-title">사용자 정보</h3>
|
|
||||||
<button id="btn-close-user-edit" class="btn-icon"><i data-lucide="x"></i></button>
|
|
||||||
</div>
|
|
||||||
<div class="modal-body">
|
|
||||||
<form id="sw-user-edit-form" class="grid-form" style="grid-template-columns: 1fr;">
|
|
||||||
<input type="hidden" id="edit-user-index" value="-1" />
|
|
||||||
<div class="form-group">
|
|
||||||
<label>조직</label>
|
|
||||||
<select id="new-user-조직">${generateOptionsHTML(ORG_LIST)}</select>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>부서</label>
|
|
||||||
<input type="text" id="new-user-부서" />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>직위</label>
|
|
||||||
<input type="text" id="new-user-직위" />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>이름</label>
|
|
||||||
<input type="text" id="new-user-이름" required />
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>사용 시작일</label>
|
|
||||||
<div style="display:flex; gap:0.25rem; align-items:center; position:relative;">
|
|
||||||
<input type="text" id="new-user-시작일" style="flex:1;" />
|
|
||||||
<button type="button" class="btn-icon" onclick="const p = document.getElementById('new-user-시작일-picker'); p.value = document.getElementById('new-user-시작일').value; p.showPicker();" style="padding:0.25rem;">
|
|
||||||
<i data-lucide="calendar" style="width:18px; height:18px; color:var(--primary-color);"></i>
|
|
||||||
</button>
|
|
||||||
<input type="date" id="new-user-시작일-picker" style="position:absolute; width:0; height:0; opacity:0; pointer-events:none;" onchange="document.getElementById('new-user-시작일').value = this.value" tabindex="-1" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>사용 종료일</label>
|
|
||||||
<div style="display:flex; gap:0.25rem; align-items:center; position:relative;">
|
|
||||||
<input type="text" id="new-user-종료일" style="flex:1;" />
|
|
||||||
<button type="button" class="btn-icon" onclick="const p = document.getElementById('new-user-종료일-picker'); p.value = document.getElementById('new-user-종료일').value; p.showPicker();" style="padding:0.25rem;">
|
|
||||||
<i data-lucide="calendar" style="width:18px; height:18px; color:var(--primary-color);"></i>
|
|
||||||
</button>
|
|
||||||
<input type="date" id="new-user-종료일-picker" style="position:absolute; width:0; height:0; opacity:0; pointer-events:none;" onchange="document.getElementById('new-user-종료일').value = this.value" tabindex="-1" />
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="form-group">
|
|
||||||
<label>신청서 (증빙)</label>
|
|
||||||
<input type="file" id="new-user-신청서" />
|
|
||||||
</div>
|
|
||||||
</form>
|
|
||||||
</div>
|
|
||||||
<div class="modal-footer">
|
|
||||||
<button id="btn-close-user-sub" class="btn btn-outline">취소</button>
|
|
||||||
<button id="btn-confirm-user-edit" class="btn btn-primary">확인</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
export function openSwUserModal(asset: SoftwareAsset) {
|
|
||||||
currentSwUserAsset = asset;
|
|
||||||
const modal = document.getElementById('sw-user-modal')!;
|
|
||||||
|
|
||||||
const swInfo = document.getElementById('sw-user-sw-info')!;
|
|
||||||
swInfo.innerHTML = `
|
|
||||||
<div style="background:var(--bg-light); padding:1rem; border-radius:6px; margin-bottom:1.5rem;">
|
|
||||||
<div style="font-size:0.8rem; color:var(--text-muted); margin-bottom:0.25rem;">${asset.법인}</div>
|
|
||||||
<div style="font-size:1.1rem; font-weight:700; color:var(--primary-color);">${asset.제품명}</div>
|
|
||||||
</div>
|
|
||||||
`;
|
|
||||||
|
|
||||||
// 기존 사용자 데이터 복사 (원본 보호를 위해 temp 사용)
|
|
||||||
const existingMapping = state.masterData.swUsers.find(u => u.sw_id === asset.id);
|
|
||||||
tempSwUsers = existingMapping ? (existingMapping.userData || []).map((u: any) => ({
|
|
||||||
조직: u[0], 부서: u[1], 직위: u[2], 이름: u[3], 사용기간: u[4], 신청서명: u[5]
|
|
||||||
})) : [];
|
|
||||||
|
|
||||||
renderUserList();
|
|
||||||
modal.classList.remove('hidden');
|
|
||||||
createIcons({ icons: { Edit2, X, Paperclip } });
|
|
||||||
}
|
|
||||||
|
|
||||||
function renderUserList() {
|
|
||||||
const tbody = document.getElementById('sw-user-table-body')!;
|
|
||||||
tbody.innerHTML = '';
|
|
||||||
|
|
||||||
if (tempSwUsers.length === 0) {
|
|
||||||
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center; padding:2rem; color:var(--text-muted);">할당된 사용자가 없습니다.</td></tr>';
|
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tempSwUsers.forEach((user, idx) => {
|
protected renderFrameHTML(): string {
|
||||||
const tr = document.createElement('tr');
|
return `
|
||||||
tr.innerHTML = `
|
<div id="sw-user-asset-modal" class="modal-overlay hidden">
|
||||||
<td>${user.조직 || ''}</td>
|
<div class="modal-content wide">
|
||||||
<td>${user.부서 || ''}</td>
|
<div class="modal-header">
|
||||||
<td>${user.직위 || ''}</td>
|
<h2 id="sw-user-title">${this.title}</h2>
|
||||||
<td>${user.이름 || ''}</td>
|
<button id="btn-close-sw-user-modal" class="btn-icon"><i data-lucide="x"></i></button>
|
||||||
<td>${user.사용기간 || ''}</td>
|
</div>
|
||||||
<td style="text-align:center;">${user.신청서명 ? '<i data-lucide="paperclip" class="text-primary"></i>' : '-'}</td>
|
<div class="modal-body">
|
||||||
<td>
|
<div class="sw-info-summary" id="sw-user-sw-info"></div>
|
||||||
<div style="display:flex; gap:0.5rem;">
|
|
||||||
<button class="btn btn-outline btn-sm btn-edit-user" data-idx="${idx}">수정</button>
|
<div class="user-list-toolbar" style="display:flex; justify-content:space-between; margin-bottom:1rem; align-items:center;">
|
||||||
<button class="btn btn-outline btn-sm btn-danger btn-del-user" data-idx="${idx}">삭제</button>
|
<h3 style="font-size:1rem; font-weight:600;">할당된 사용자 목록</h3>
|
||||||
|
<button type="button" id="btn-open-add-user" class="btn btn-primary btn-sm"><i data-lucide="plus"></i> 사용자 추가</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="table-container">
|
||||||
|
<table>
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>조직</th>
|
||||||
|
<th>부서</th>
|
||||||
|
<th>직위</th>
|
||||||
|
<th>이름</th>
|
||||||
|
<th>사용기간</th>
|
||||||
|
<th>신청서</th>
|
||||||
|
<th>관리</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody id="sw-user-table-body"></tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
<!-- 더미 폼 (BaseModal 필수 요건 충족용) -->
|
||||||
|
<form id="sw-user-asset-form" class="hidden"></form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button id="btn-cancel-sw-user" class="btn btn-outline">취소</button>
|
||||||
|
<button id="btn-save-sw-user" class="btn btn-primary">저장</button>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
</div>
|
||||||
tbody.appendChild(tr);
|
|
||||||
});
|
|
||||||
|
|
||||||
// 이벤트 연결
|
<!-- 사용자 추가/수정 서브 모달 -->
|
||||||
tbody.querySelectorAll('.btn-edit-user').forEach(btn => {
|
<div id="sw-user-edit-modal" class="modal-overlay hidden" style="z-index: 1100;">
|
||||||
btn.addEventListener('click', (e) => {
|
<div class="modal-content" style="width: 400px;">
|
||||||
const idx = parseInt((e.currentTarget as HTMLElement).getAttribute('data-idx')!);
|
<div class="modal-header">
|
||||||
openUserEditSubModal(idx);
|
<h3 id="sw-user-edit-title">사용자 정보</h3>
|
||||||
|
<button id="btn-close-user-edit" class="btn-icon"><i data-lucide="x"></i></button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<form id="sw-user-edit-form" class="grid-form" style="grid-template-columns: 1fr;">
|
||||||
|
<input type="hidden" id="edit-user-index" value="-1" />
|
||||||
|
<div class="form-group">
|
||||||
|
<label>조직</label>
|
||||||
|
<select id="new-user-조직">${generateOptionsHTML(ORG_LIST)}</select>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>부서</label>
|
||||||
|
<input type="text" id="new-user-부서" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>직위</label>
|
||||||
|
<input type="text" id="new-user-직위" />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>이름</label>
|
||||||
|
<input type="text" id="new-user-이름" required />
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>사용 시작일</label>
|
||||||
|
<div style="display:flex; gap:0.25rem; align-items:center; position:relative;">
|
||||||
|
<input type="text" id="new-user-시작일" style="flex:1;" />
|
||||||
|
<button type="button" class="btn-icon" onclick="const p = document.getElementById('new-user-시작일-picker'); p.value = document.getElementById('new-user-시작일').value; p.showPicker();" style="padding:0.25rem;">
|
||||||
|
<i data-lucide="calendar" style="width:18px; height:18px; color:var(--primary-color);"></i>
|
||||||
|
</button>
|
||||||
|
<input type="date" id="new-user-시작일-picker" style="position:absolute; width:0; height:0; opacity:0; pointer-events:none;" onchange="document.getElementById('new-user-시작일').value = this.value" tabindex="-1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>사용 종료일</label>
|
||||||
|
<div style="display:flex; gap:0.25rem; align-items:center; position:relative;">
|
||||||
|
<input type="text" id="new-user-종료일" style="flex:1;" />
|
||||||
|
<button type="button" class="btn-icon" onclick="const p = document.getElementById('new-user-종료일-picker'); p.value = document.getElementById('new-user-종료일').value; p.showPicker();" style="padding:0.25rem;">
|
||||||
|
<i data-lucide="calendar" style="width:18px; height:18px; color:var(--primary-color);"></i>
|
||||||
|
</button>
|
||||||
|
<input type="date" id="new-user-종료일-picker" style="position:absolute; width:0; height:0; opacity:0; pointer-events:none;" onchange="document.getElementById('new-user-종료일').value = this.value" tabindex="-1" />
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="form-group">
|
||||||
|
<label>신청서 (증빙)</label>
|
||||||
|
<input type="file" id="new-user-신청서" />
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button id="btn-close-user-sub" class="btn btn-outline">취소</button>
|
||||||
|
<button id="btn-confirm-user-edit" class="btn btn-primary">확인</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
|
protected initChildLogic(onSave: () => void, closeModals: () => void): void {
|
||||||
|
const mainSaveBtn = document.getElementById('btn-save-sw-user')!;
|
||||||
|
const addUserBtn = document.getElementById('btn-open-add-user')!;
|
||||||
|
const confirmUserBtn = document.getElementById('btn-confirm-user-edit')!;
|
||||||
|
|
||||||
|
['new-user-시작일', 'new-user-종료일'].forEach(id => {
|
||||||
|
const el = document.getElementById(id) as HTMLInputElement;
|
||||||
|
if (el) applyDateMask(el);
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
tbody.querySelectorAll('.btn-del-user').forEach(btn => {
|
addUserBtn.addEventListener('click', () => this.openUserEditSubModal());
|
||||||
btn.addEventListener('click', (e) => {
|
confirmUserBtn.addEventListener('click', () => this.saveUserDataToList());
|
||||||
const idx = parseInt((e.currentTarget as HTMLElement).getAttribute('data-idx')!);
|
|
||||||
if (confirm('사용자 할당을 삭제하시겠습니까?')) {
|
mainSaveBtn.addEventListener('click', () => {
|
||||||
tempSwUsers.splice(idx, 1);
|
if (!this.currentAsset) return;
|
||||||
renderUserList();
|
const existingIdx = state.masterData.swUsers.findIndex(u => u.sw_id === this.currentAsset!.id);
|
||||||
}
|
const newMapping = {
|
||||||
|
sw_id: this.currentAsset!.id,
|
||||||
|
userData: this.tempSwUsers.map(u => [u.조직, u.부서, u.직위, u.이름, u.사용기간, u.신청서명])
|
||||||
|
};
|
||||||
|
if (existingIdx > -1) state.masterData.swUsers[existingIdx] = newMapping as any;
|
||||||
|
else state.masterData.swUsers.push(newMapping as any);
|
||||||
|
|
||||||
|
onSave(); this.close(); closeModals();
|
||||||
});
|
});
|
||||||
});
|
|
||||||
|
|
||||||
createIcons({ icons: { Paperclip } });
|
// 닫기 이벤트들 (BaseModal의 공통 버튼 외 추가분)
|
||||||
}
|
document.getElementById('btn-close-sw-user-modal')?.addEventListener('click', () => this.close());
|
||||||
|
document.getElementById('btn-cancel-sw-user')?.addEventListener('click', () => this.close());
|
||||||
function openUserEditSubModal(idx: number = -1) {
|
|
||||||
const subModal = document.getElementById('sw-user-edit-modal')!;
|
|
||||||
const form = document.getElementById('sw-user-edit-form') as HTMLFormElement;
|
|
||||||
form.reset();
|
|
||||||
|
|
||||||
setFieldValue('edit-user-index', idx);
|
|
||||||
|
|
||||||
if (idx > -1) {
|
|
||||||
const user = tempSwUsers[idx];
|
|
||||||
setFieldValue('new-user-조직', user.조직);
|
|
||||||
setFieldValue('new-user-부서', user.부서);
|
|
||||||
setFieldValue('new-user-직위', user.직위);
|
|
||||||
setFieldValue('new-user-이름', user.이름);
|
|
||||||
|
|
||||||
// 사용기간 파싱 (yyyy-mm-dd ~ yyyy-mm-dd)
|
const subModal = document.getElementById('sw-user-edit-modal')!;
|
||||||
if (user.사용기간 && user.사용기간.includes('~')) {
|
const closeSub = () => subModal.classList.add('hidden');
|
||||||
const parts = user.사용기간.split('~');
|
document.getElementById('btn-close-user-edit')?.addEventListener('click', closeSub);
|
||||||
setFieldValue('new-user-시작일', parts[0].trim());
|
document.getElementById('btn-close-user-sub')?.addEventListener('click', closeSub);
|
||||||
setFieldValue('new-user-종료일', parts[1].trim());
|
|
||||||
} else {
|
createIcons({ icons: { X, Plus, Calendar, Edit2, Paperclip } });
|
||||||
setFieldValue('new-user-시작일', '');
|
|
||||||
setFieldValue('new-user-종료일', '');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
subModal.classList.remove('hidden');
|
protected fillFormData(asset: any): void {
|
||||||
|
const swInfo = document.getElementById('sw-user-sw-info')!;
|
||||||
|
swInfo.innerHTML = `
|
||||||
|
<div style="background:var(--bg-light); padding:1rem; border-radius:6px; margin-bottom:1.5rem;">
|
||||||
|
<div style="font-size:0.8rem; color:var(--text-muted); margin-bottom:0.25rem;">${asset.purchase_corp || asset.법인 || ''}</div>
|
||||||
|
<div style="font-size:1.1rem; font-weight:700; color:var(--primary-color);">${asset.product_name || asset.제품명 || ''}</div>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
|
const existingMapping = state.masterData.swUsers.find(u => u.sw_id === asset.id);
|
||||||
|
this.tempSwUsers = existingMapping ? (existingMapping.userData || []).map((u: any) => ({
|
||||||
|
조직: u[0], 부서: u[1], 직위: u[2], 이름: u[3], 사용기간: u[4], 신청서명: u[5]
|
||||||
|
})) : [];
|
||||||
|
|
||||||
|
this.renderUserList();
|
||||||
|
}
|
||||||
|
|
||||||
|
protected onAfterOpen(): void {}
|
||||||
|
|
||||||
|
private renderUserList() {
|
||||||
|
const tbody = document.getElementById('sw-user-table-body')!;
|
||||||
|
tbody.innerHTML = '';
|
||||||
|
if (this.tempSwUsers.length === 0) {
|
||||||
|
tbody.innerHTML = '<tr><td colspan="7" style="text-align:center; padding:2rem; color:var(--text-muted);">할당된 사용자가 없습니다.</td></tr>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
this.tempSwUsers.forEach((user, idx) => {
|
||||||
|
const tr = document.createElement('tr');
|
||||||
|
tr.innerHTML = `
|
||||||
|
<td>${user.조직 || ''}</td>
|
||||||
|
<td>${user.부서 || ''}</td>
|
||||||
|
<td>${user.직위 || ''}</td>
|
||||||
|
<td>${user.이름 || ''}</td>
|
||||||
|
<td>${user.사용기간 || ''}</td>
|
||||||
|
<td style="text-align:center;">${user.신청서명 ? '<i data-lucide="paperclip" class="text-primary"></i>' : '-'}</td>
|
||||||
|
<td>
|
||||||
|
<div style="display:flex; gap:0.5rem;">
|
||||||
|
<button class="btn btn-outline btn-sm btn-edit-user" data-idx="${idx}">수정</button>
|
||||||
|
<button class="btn btn-outline btn-sm btn-danger btn-del-user" data-idx="${idx}">삭제</button>
|
||||||
|
</div>
|
||||||
|
</td>
|
||||||
|
`;
|
||||||
|
tbody.appendChild(tr);
|
||||||
|
});
|
||||||
|
|
||||||
|
tbody.querySelectorAll('.btn-edit-user').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
const idx = parseInt((e.currentTarget as HTMLElement).getAttribute('data-idx')!);
|
||||||
|
this.openUserEditSubModal(idx);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
tbody.querySelectorAll('.btn-del-user').forEach(btn => {
|
||||||
|
btn.addEventListener('click', (e) => {
|
||||||
|
const idx = parseInt((e.currentTarget as HTMLElement).getAttribute('data-idx')!);
|
||||||
|
if (confirm('사용자 할당을 삭제하시겠습니까?')) {
|
||||||
|
this.tempSwUsers.splice(idx, 1); this.renderUserList();
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
createIcons({ icons: { Paperclip } });
|
||||||
|
}
|
||||||
|
|
||||||
|
private openUserEditSubModal(idx: number = -1) {
|
||||||
|
const subModal = document.getElementById('sw-user-edit-modal')!;
|
||||||
|
const form = document.getElementById('sw-user-edit-form') as HTMLFormElement;
|
||||||
|
form.reset();
|
||||||
|
setFieldValue('edit-user-index', idx);
|
||||||
|
if (idx > -1) {
|
||||||
|
const user = this.tempSwUsers[idx];
|
||||||
|
setFieldValue('new-user-조직', user.조직);
|
||||||
|
setFieldValue('new-user-부서', user.부서);
|
||||||
|
setFieldValue('new-user-직위', user.직위);
|
||||||
|
setFieldValue('new-user-이름', user.이름);
|
||||||
|
if (user.사용기간 && user.사용기간.includes('~')) {
|
||||||
|
const parts = user.사용기간.split('~');
|
||||||
|
setFieldValue('new-user-시작일', parts[0].trim());
|
||||||
|
setFieldValue('new-user-종료일', parts[1].trim());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
subModal.classList.remove('hidden');
|
||||||
|
}
|
||||||
|
|
||||||
|
private saveUserDataToList() {
|
||||||
|
const idx = parseInt(getFieldValue('edit-user-index'));
|
||||||
|
const 신청서Input = document.getElementById('new-user-신청서') as HTMLInputElement;
|
||||||
|
const 신청서명 = 신청서Input.files && 신청서Input.files.length > 0 ? 신청서Input.files[0].name : (idx > -1 ? this.tempSwUsers[idx].신청서명 : '');
|
||||||
|
|
||||||
|
const userData: any = {
|
||||||
|
조직: getFieldValue('new-user-조직'),
|
||||||
|
부서: getFieldValue('new-user-부서'),
|
||||||
|
직위: getFieldValue('new-user-직위'),
|
||||||
|
이름: getFieldValue('new-user-이름'),
|
||||||
|
사용기간: `${getFieldValue('new-user-시작일')} ~ ${getFieldValue('new-user-종료일')}`,
|
||||||
|
신청서명
|
||||||
|
};
|
||||||
|
if (idx === -1) this.tempSwUsers.push(userData);
|
||||||
|
else this.tempSwUsers[idx] = userData;
|
||||||
|
document.getElementById('sw-user-edit-modal')?.classList.add('hidden');
|
||||||
|
this.renderUserList();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export const swUserModal = new SwUserModal();
|
||||||
|
|
||||||
export function initSwUserModal(onSave: () => void, closeModals: () => void) {
|
export function initSwUserModal(onSave: () => void, closeModals: () => void) {
|
||||||
if (!document.getElementById('sw-user-modal')) {
|
swUserModal.init(onSave, closeModals);
|
||||||
document.body.insertAdjacentHTML('beforeend', SW_USER_MODAL_HTML);
|
|
||||||
}
|
|
||||||
|
|
||||||
const mainSaveBtn = document.getElementById('btn-save-sw-user')!;
|
|
||||||
const addUserBtn = document.getElementById('btn-open-add-user')!;
|
|
||||||
const confirmUserBtn = document.getElementById('btn-confirm-user-edit')!;
|
|
||||||
|
|
||||||
['new-user-시작일', 'new-user-종료일'].forEach(id => {
|
|
||||||
applyDateMask(document.getElementById(id) as HTMLInputElement);
|
|
||||||
});
|
|
||||||
|
|
||||||
createIcons({ icons: { Calendar } });
|
|
||||||
|
|
||||||
addUserBtn.addEventListener('click', () => openUserEditSubModal());
|
|
||||||
|
|
||||||
confirmUserBtn.addEventListener('click', () => {
|
|
||||||
saveUserDataToList();
|
|
||||||
});
|
|
||||||
|
|
||||||
mainSaveBtn.addEventListener('click', () => {
|
|
||||||
if (!currentSwUserAsset) return;
|
|
||||||
|
|
||||||
// 전역 상태 업데이트
|
|
||||||
const existingIdx = state.masterData.swUsers.findIndex(u => u.sw_id === currentSwUserAsset!.id);
|
|
||||||
const newMapping = {
|
|
||||||
sw_id: currentSwUserAsset!.id,
|
|
||||||
userData: tempSwUsers.map(u => [u.조직, u.부서, u.직위, u.이름, u.사용기간, u.신청서명])
|
|
||||||
};
|
|
||||||
|
|
||||||
if (existingIdx > -1) state.masterData.swUsers[existingIdx] = newMapping as any;
|
|
||||||
else state.masterData.swUsers.push(newMapping as any);
|
|
||||||
|
|
||||||
onSave();
|
|
||||||
document.getElementById('sw-user-modal')?.classList.add('hidden');
|
|
||||||
});
|
|
||||||
|
|
||||||
document.getElementById('btn-close-sw-user-modal')?.addEventListener('click', () => {
|
|
||||||
document.getElementById('sw-user-modal')?.classList.add('hidden');
|
|
||||||
});
|
|
||||||
document.getElementById('btn-cancel-sw-user')?.addEventListener('click', () => {
|
|
||||||
document.getElementById('sw-user-modal')?.classList.add('hidden');
|
|
||||||
});
|
|
||||||
document.getElementById('btn-close-user-edit')?.addEventListener('click', () => {
|
|
||||||
document.getElementById('sw-user-edit-modal')?.classList.add('hidden');
|
|
||||||
});
|
|
||||||
document.getElementById('btn-close-user-sub')?.addEventListener('click', () => {
|
|
||||||
document.getElementById('sw-user-edit-modal')?.classList.add('hidden');
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
function saveUserDataToList() {
|
export function openSwUserModal(asset: any) {
|
||||||
const idx = parseInt(getFieldValue('edit-user-index'));
|
swUserModal.open(asset);
|
||||||
const 신청서Input = document.getElementById('new-user-신청서') as HTMLInputElement;
|
|
||||||
const 신청서명 = 신청서Input.files && 신청서Input.files.length > 0 ? 신청서Input.files[0].name : (idx > -1 ? tempSwUsers[idx].신청서명 : '');
|
|
||||||
|
|
||||||
const userData: any = {
|
|
||||||
조직: getFieldValue('new-user-조직'),
|
|
||||||
부서: getFieldValue('new-user-부서'),
|
|
||||||
직위: getFieldValue('new-user-직위'),
|
|
||||||
이름: getFieldValue('new-user-이름'),
|
|
||||||
사용기간: `${getFieldValue('new-user-시작일')} ~ ${getFieldValue('new-user-종료일')}`,
|
|
||||||
신청서명
|
|
||||||
};
|
|
||||||
|
|
||||||
if (idx === -1) tempSwUsers.push(userData);
|
|
||||||
else tempSwUsers[idx] = userData;
|
|
||||||
|
|
||||||
document.getElementById('sw-user-edit-modal')?.classList.add('hidden');
|
|
||||||
renderUserList();
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ export const CATEGORY_TYPE_MAP: Record<string, string[]> = {
|
|||||||
// 설치위치 종속성 데이터
|
// 설치위치 종속성 데이터
|
||||||
export const LOCATION_DATA: Record<string, string[]> = {
|
export const LOCATION_DATA: Record<string, string[]> = {
|
||||||
'한맥빌딩': ['MDF실', '1층', '2층', '3층', '4층', '5층', '6층', '7층', '파고라'],
|
'한맥빌딩': ['MDF실', '1층', '2층', '3층', '4층', '5층', '6층', '7층', '파고라'],
|
||||||
'기술개발센터': ['서버실', '1층', '기타'],
|
'기술개발센터': ['서버실', 'BLUE ZONE', 'GREEN ZONE', 'ORANGE ZONE', '회의실2', '회의실3', '회의실5', '회의실6', '회의실7', '사이니지룸'],
|
||||||
'유니온빌딩': ['4층', '5층', '6층'],
|
'유니온빌딩': ['4층', '5층', '6층'],
|
||||||
'뉴코아빌딩': ['4층', '6층', '7층'],
|
'뉴코아빌딩': ['4층', '6층', '7층'],
|
||||||
'IDC': ['서관202', '서관203', '서관204', '서관205', '동관53', '동관54']
|
'IDC': ['서관202', '서관203', '서관204', '서관205', '동관53', '동관54']
|
||||||
@@ -38,8 +38,35 @@ export const LOCATION_DATA: Record<string, string[]> = {
|
|||||||
|
|
||||||
// 유형별 자산번호 접두사(Prefix) 매핑
|
// 유형별 자산번호 접두사(Prefix) 매핑
|
||||||
export const TYPE_PREFIX_MAP: Record<string, string> = {
|
export const TYPE_PREFIX_MAP: Record<string, string> = {
|
||||||
'서버': 'SVR', '개인PC': 'PC', '공용PC': 'PC', '서버PC': 'PC', 'NAS': 'NAS', 'DAS': 'DAS', '스토리지': 'STO',
|
'서버': 'SVR', '워크스테이션': 'SVR', '개인PC': 'PC', '공용PC': 'PC', '서버PC': 'PC', 'NAS': 'NAS', 'DAS': 'DAS', '스토리지': 'STO',
|
||||||
'HDD': 'HDD', 'SSD': 'SSD', '노트북': 'NBK', '태블릿': 'TAB',
|
'HDD': 'HDD', 'SSD': 'SSD', '노트북': 'NBK', '태블릿': 'TAB',
|
||||||
'드론': 'DRO', '측량장비': 'SUR', '보조기기': 'SUR', '허브': 'NET',
|
'드론': 'DRO', '측량장비': 'SUR', '보조기기': 'SUR', '허브': 'NET',
|
||||||
'구독SW': 'SW', '영구SW': 'SW', '내부' : 'INT'
|
'구독SW': 'SW', '영구SW': 'SW', '내부' : 'INT'
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 배치도 이미지 매핑 데이터
|
||||||
|
export const IMAGE_LOCATIONS: Record<string, Record<string, string[]>> = {
|
||||||
|
'IDC': {
|
||||||
|
'서관202': ['img/location_photo/IDC/서관202.png'],
|
||||||
|
'서관203': ['img/location_photo/IDC/서관203.png'],
|
||||||
|
'서관204': ['img/location_photo/IDC/서관204.png'],
|
||||||
|
'서관205': ['img/location_photo/IDC/서관205.png'],
|
||||||
|
'동관53': ['img/location_photo/IDC/동관53.png'],
|
||||||
|
'동관54': ['img/location_photo/IDC/동관54.png'],
|
||||||
|
},
|
||||||
|
'기술개발센터': {
|
||||||
|
'서버실': [
|
||||||
|
'img/location_photo/기술개발센터/서버실/서버실_1.png',
|
||||||
|
'img/location_photo/기술개발센터/서버실/서버실_2.png'
|
||||||
|
]
|
||||||
|
},
|
||||||
|
'한맥빌딩': {
|
||||||
|
'7층': ['img/location_photo/한맥빌딩/7층_로비.png'],
|
||||||
|
'MDF실': [
|
||||||
|
'img/location_photo/한맥빌딩/MDF실/MDF_1.png',
|
||||||
|
'img/location_photo/한맥빌딩/MDF실/MDF_2.png',
|
||||||
|
'img/location_photo/한맥빌딩/MDF실/MDF_3.png',
|
||||||
|
'img/location_photo/한맥빌딩/MDF실/MDF_4.png'
|
||||||
|
]
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@@ -86,7 +86,7 @@ export function renderNavigation(onTabChange: (tab: string) => void) {
|
|||||||
adminTrigger.style.paddingLeft = '1.5rem';
|
adminTrigger.style.paddingLeft = '1.5rem';
|
||||||
|
|
||||||
adminTrigger.addEventListener('click', () => {
|
adminTrigger.addEventListener('click', () => {
|
||||||
alert('준비중입니다.');
|
window.open('/map_editor.html', '_blank');
|
||||||
});
|
});
|
||||||
|
|
||||||
adminGroup.appendChild(adminTrigger);
|
adminGroup.appendChild(adminTrigger);
|
||||||
|
|||||||
@@ -14,18 +14,26 @@ export interface FilterOptions {
|
|||||||
showDept?: boolean;
|
showDept?: boolean;
|
||||||
showLoc?: boolean;
|
showLoc?: boolean;
|
||||||
showField?: boolean;
|
showField?: boolean;
|
||||||
|
showType?: boolean;
|
||||||
extraHTML?: string;
|
extraHTML?: string;
|
||||||
onFilterChange: (filters: any) => void;
|
onFilterChange: (filters: any) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function renderFilterBar(container: HTMLElement, options: FilterOptions) {
|
export function renderFilterBar(container: HTMLElement, options: FilterOptions) {
|
||||||
const { keywordLabel = '통합 검색', showCorp = false, showDept = false, showLoc = false, showField = false, extraHTML = '', onFilterChange } = options;
|
const { keywordLabel = '통합 검색', showCorp = false, showDept = false, showLoc = false, showField = false, showType = false, extraHTML = '', onFilterChange } = options;
|
||||||
|
|
||||||
container.innerHTML = `
|
container.innerHTML = `
|
||||||
<div class="search-item flex-1">
|
<div class="search-item flex-1">
|
||||||
<label>${keywordLabel}</label>
|
<label>${keywordLabel}</label>
|
||||||
<input type="text" id="filter-keyword" placeholder="검색어를 입력하세요..." autocomplete="off">
|
<input type="text" id="filter-keyword" placeholder="검색어를 입력하세요..." autocomplete="off">
|
||||||
</div>
|
</div>
|
||||||
|
${showType ? `
|
||||||
|
<div class="search-item">
|
||||||
|
<label>${ASSET_SCHEMA.ASSET_TYPE.ui}</label>
|
||||||
|
<select id="filter-type">
|
||||||
|
<option value="">전체 유형</option>
|
||||||
|
</select>
|
||||||
|
</div>` : ''}
|
||||||
${showField ? `
|
${showField ? `
|
||||||
<div class="search-item">
|
<div class="search-item">
|
||||||
<label>${ASSET_SCHEMA.SW_FIELD.ui}</label>
|
<label>${ASSET_SCHEMA.SW_FIELD.ui}</label>
|
||||||
@@ -66,7 +74,8 @@ export function renderFilterBar(container: HTMLElement, options: FilterOptions)
|
|||||||
corp: (container.querySelector('#filter-corp') as HTMLSelectElement)?.value || '',
|
corp: (container.querySelector('#filter-corp') as HTMLSelectElement)?.value || '',
|
||||||
dept: (container.querySelector('#filter-dept') as HTMLSelectElement)?.value || '',
|
dept: (container.querySelector('#filter-dept') as HTMLSelectElement)?.value || '',
|
||||||
loc: (container.querySelector('#filter-loc') as HTMLSelectElement)?.value || '',
|
loc: (container.querySelector('#filter-loc') as HTMLSelectElement)?.value || '',
|
||||||
field: (container.querySelector('#filter-field') as HTMLSelectElement)?.value || ''
|
field: (container.querySelector('#filter-field') as HTMLSelectElement)?.value || '',
|
||||||
|
type: (container.querySelector('#filter-type') as HTMLSelectElement)?.value || ''
|
||||||
};
|
};
|
||||||
onFilterChange(filters);
|
onFilterChange(filters);
|
||||||
};
|
};
|
||||||
@@ -76,9 +85,10 @@ export function renderFilterBar(container: HTMLElement, options: FilterOptions)
|
|||||||
container.querySelector('#filter-dept')?.addEventListener('change', triggerChange);
|
container.querySelector('#filter-dept')?.addEventListener('change', triggerChange);
|
||||||
container.querySelector('#filter-loc')?.addEventListener('change', triggerChange);
|
container.querySelector('#filter-loc')?.addEventListener('change', triggerChange);
|
||||||
container.querySelector('#filter-field')?.addEventListener('change', triggerChange);
|
container.querySelector('#filter-field')?.addEventListener('change', triggerChange);
|
||||||
|
container.querySelector('#filter-type')?.addEventListener('change', triggerChange);
|
||||||
|
|
||||||
container.querySelector('#btn-reset-filters')?.addEventListener('click', () => {
|
container.querySelector('#btn-reset-filters')?.addEventListener('click', () => {
|
||||||
['filter-keyword', 'filter-corp', 'filter-dept', 'filter-loc', 'filter-field'].forEach(id => {
|
['filter-keyword', 'filter-corp', 'filter-dept', 'filter-loc', 'filter-field', 'filter-type'].forEach(id => {
|
||||||
const el = container.querySelector(`#${id}`);
|
const el = container.querySelector(`#${id}`);
|
||||||
if (el) (el as any).value = '';
|
if (el) (el as any).value = '';
|
||||||
});
|
});
|
||||||
@@ -98,7 +108,8 @@ export function applyCommonFilters(list: any[], filters: any, searchKeys: (keyof
|
|||||||
const matchDept = !filters.dept || (item[ASSET_SCHEMA.CURRENT_DEPT.key] || item[ASSET_SCHEMA.CURRENT_DEPT.db]) === filters.dept;
|
const matchDept = !filters.dept || (item[ASSET_SCHEMA.CURRENT_DEPT.key] || item[ASSET_SCHEMA.CURRENT_DEPT.db]) === filters.dept;
|
||||||
const matchLoc = !filters.loc || (item[ASSET_SCHEMA.LOCATION.key] || item[ASSET_SCHEMA.LOCATION.db]) === filters.loc;
|
const matchLoc = !filters.loc || (item[ASSET_SCHEMA.LOCATION.key] || item[ASSET_SCHEMA.LOCATION.db]) === filters.loc;
|
||||||
const matchField = !filters.field || (item[ASSET_SCHEMA.SW_FIELD.key] || item[ASSET_SCHEMA.SW_FIELD.db]) === filters.field;
|
const matchField = !filters.field || (item[ASSET_SCHEMA.SW_FIELD.key] || item[ASSET_SCHEMA.SW_FIELD.db]) === filters.field;
|
||||||
|
const matchType = !filters.type || (item[ASSET_SCHEMA.ASSET_TYPE.key] || item[ASSET_SCHEMA.ASSET_TYPE.db]) === filters.type;
|
||||||
|
|
||||||
return matchKeyword && matchCorp && matchDept && matchLoc && matchField;
|
return matchKeyword && matchCorp && matchDept && matchLoc && matchField && matchType;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,6 +21,9 @@ export const ASSET_SCHEMA = {
|
|||||||
MANAGER_SUB: { key: 'manager_secondary', db: 'manager_secondary', ui: '담당자(부)' },
|
MANAGER_SUB: { key: 'manager_secondary', db: 'manager_secondary', ui: '담당자(부)' },
|
||||||
LOCATION: { key: 'location', db: 'location', ui: '자산위치' },
|
LOCATION: { key: 'location', db: 'location', ui: '자산위치' },
|
||||||
LOC_DETAIL: { key: 'location_detail', db: 'location_detail', ui: '상세위치' },
|
LOC_DETAIL: { key: 'location_detail', db: 'location_detail', ui: '상세위치' },
|
||||||
|
LOCATION_PHOTO: { key: 'location_photo', db: 'location_photo', ui: '배치도이미지' },
|
||||||
|
LOC_X: { key: 'loc_x', db: 'loc_x', ui: '위치X' },
|
||||||
|
LOC_Y: { key: 'loc_y', db: 'loc_y', ui: '위치Y' },
|
||||||
MEMO: { key: 'memo', db: 'memo', ui: '메모' },
|
MEMO: { key: 'memo', db: 'memo', ui: '메모' },
|
||||||
|
|
||||||
// ─── 하드웨어 상세 (Hardware) ───
|
// ─── 하드웨어 상세 (Hardware) ───
|
||||||
|
|||||||
@@ -83,15 +83,14 @@ function initApp() {
|
|||||||
|
|
||||||
initHwModal(() => saveAllDataToDB(), closeAllModals);
|
initHwModal(() => saveAllDataToDB(), closeAllModals);
|
||||||
initSwModal(() => saveAllDataToDB(), closeAllModals);
|
initSwModal(() => saveAllDataToDB(), closeAllModals);
|
||||||
|
|
||||||
initSwUserModal(() => {
|
initSwUserModal(() => {
|
||||||
saveSwUsersToDB().then(() => {
|
saveSwUsersToDB().then(() => {
|
||||||
loadMasterDataFromDB().then(() => refreshView());
|
loadMasterDataFromDB().then(() => refreshView());
|
||||||
});
|
});
|
||||||
}, closeAllModals);
|
}, closeAllModals);
|
||||||
|
initDomainModal(() => saveAllDataToDB(), closeAllModals);
|
||||||
|
|
||||||
initDashboardDetailModal();
|
initDashboardDetailModal();
|
||||||
initDomainModal();
|
|
||||||
initGuide();
|
initGuide();
|
||||||
|
|
||||||
loadMasterDataFromDB().then((success) => {
|
loadMasterDataFromDB().then((success) => {
|
||||||
|
|||||||
8
src/map-editor-main.ts
Normal file
8
src/map-editor-main.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import './styles/common.css';
|
||||||
|
import './styles/map-editor.css';
|
||||||
|
import { MapEditor } from './views/MapEditor';
|
||||||
|
|
||||||
|
document.addEventListener('DOMContentLoaded', () => {
|
||||||
|
const editor = new MapEditor();
|
||||||
|
editor.init();
|
||||||
|
});
|
||||||
@@ -41,9 +41,10 @@
|
|||||||
--color-yellow-medium: #FFE599;
|
--color-yellow-medium: #FFE599;
|
||||||
--color-orange-medium: #FFD699;
|
--color-orange-medium: #FFD699;
|
||||||
--color-dahong-medium: #FFB199;
|
--color-dahong-medium: #FFB199;
|
||||||
--color-brown-medium: #D9C6BF;
|
--color-dahong: #FF3D00;
|
||||||
--color-iron-medium: #CCCCCC;
|
--color-dahong-light: #FFECE6;
|
||||||
--color-steel-medium: #C3CFD5;
|
--color-dahong-medium: #FFB199;
|
||||||
|
--color-dahong-dark: #cc3100;
|
||||||
|
|
||||||
/* --- Primary Brand Levels --- */
|
/* --- Primary Brand Levels --- */
|
||||||
--primary-lv-0: #E9EEED;
|
--primary-lv-0: #E9EEED;
|
||||||
@@ -57,11 +58,16 @@
|
|||||||
--primary-lv-8: #193833;
|
--primary-lv-8: #193833;
|
||||||
--primary-lv-9: #162A27;
|
--primary-lv-9: #162A27;
|
||||||
|
|
||||||
/* --- Legacy Aliases (Maintained for compatibility) --- */
|
/* --- Semantic Colors --- */
|
||||||
--primary-color: var(--primary-lv-6);
|
--primary-color: var(--primary-lv-6);
|
||||||
--primary-hover: var(--primary-lv-5);
|
--primary-hover: var(--primary-lv-5);
|
||||||
--primary-light: var(--primary-lv-0);
|
--primary-light: var(--primary-lv-0);
|
||||||
|
|
||||||
|
--edit-mode-color: var(--color-dahong);
|
||||||
|
--edit-mode-light: var(--color-dahong-light);
|
||||||
|
--edit-mode-focus: var(--color-dahong-medium);
|
||||||
|
--edit-mode-dark: var(--color-dahong-dark);
|
||||||
|
|
||||||
--text-main: #111827;
|
--text-main: #111827;
|
||||||
--text-muted: #6B7280;
|
--text-muted: #6B7280;
|
||||||
--border-color: #E5E7EB;
|
--border-color: #E5E7EB;
|
||||||
@@ -70,13 +76,16 @@
|
|||||||
--sidebar-bg: #ffffff;
|
--sidebar-bg: #ffffff;
|
||||||
--white: #FFFFFF;
|
--white: #FFFFFF;
|
||||||
--danger: var(--color-red);
|
--danger: var(--color-red);
|
||||||
|
--info: var(--color-blue);
|
||||||
|
--success: var(--color-green);
|
||||||
|
--warning: var(--color-orange);
|
||||||
|
|
||||||
--dash-primary: #6cc020;
|
--dash-primary: #6cc020;
|
||||||
--dash-light: #f2f9ec;
|
--dash-light: #f2f9ec;
|
||||||
--dash-danger: #cf222e;
|
--dash-danger: #cf222e;
|
||||||
|
|
||||||
--header-height: 52px;
|
--header-height: 52px;
|
||||||
}
|
}
|
||||||
|
|
||||||
* {
|
* {
|
||||||
box-sizing: border-box;
|
box-sizing: border-box;
|
||||||
@@ -303,7 +312,7 @@ body {
|
|||||||
font-weight: 300;
|
font-weight: 300;
|
||||||
line-height: 1.25rem;
|
line-height: 1.25rem;
|
||||||
letter-spacing: -0.0175rem;
|
letter-spacing: -0.0175rem;
|
||||||
color: #777777;
|
color: var(--text-muted);
|
||||||
user-select: none;
|
user-select: none;
|
||||||
pointer-events: all;
|
pointer-events: all;
|
||||||
-webkit-user-drag: none;
|
-webkit-user-drag: none;
|
||||||
|
|||||||
159
src/styles/map-editor.css
Normal file
159
src/styles/map-editor.css
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
/* ITAM Map Coordinate Editor Styles */
|
||||||
|
|
||||||
|
.file-sidebar {
|
||||||
|
width: 260px;
|
||||||
|
background: var(--white);
|
||||||
|
border-right: 1px solid var(--border-color);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
overflow-y: auto;
|
||||||
|
}
|
||||||
|
|
||||||
|
.folder-item {
|
||||||
|
padding: 10px 15px;
|
||||||
|
background: var(--bg-light);
|
||||||
|
font-weight: bold;
|
||||||
|
font-size: 13px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
color: var(--primary-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item {
|
||||||
|
padding: 8px 25px;
|
||||||
|
cursor: pointer;
|
||||||
|
font-size: 12px;
|
||||||
|
border-bottom: 1px solid var(--bg-color);
|
||||||
|
transition: background 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-item:hover { background: var(--bg-light); }
|
||||||
|
.file-item.active { background: var(--primary-color); color: var(--white); font-weight: bold; }
|
||||||
|
|
||||||
|
/* Center: Editor Area */
|
||||||
|
.editor-container {
|
||||||
|
flex: 1;
|
||||||
|
position: relative;
|
||||||
|
overflow: auto;
|
||||||
|
padding: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
background: #e0e0e0; /* 전용 배경색 유지 */
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-wrapper {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
box-shadow: 0 0 30px rgba(0,0,0,0.3);
|
||||||
|
background: var(--white);
|
||||||
|
line-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.img-wrapper img {
|
||||||
|
display: block;
|
||||||
|
max-width: calc(100vw - 650px);
|
||||||
|
max-height: 85vh;
|
||||||
|
width: auto;
|
||||||
|
height: auto;
|
||||||
|
user-select: none;
|
||||||
|
-webkit-user-drag: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Right Sidebar: Control Panel */
|
||||||
|
.sidebar {
|
||||||
|
width: 350px;
|
||||||
|
background: var(--white);
|
||||||
|
border-left: 1px solid var(--border-color);
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
padding: 20px;
|
||||||
|
box-shadow: -5px 0 15px rgba(0,0,0,0.05);
|
||||||
|
}
|
||||||
|
|
||||||
|
.sidebar h2 { margin-top: 0; color: var(--primary-color); font-size: 1.2rem; }
|
||||||
|
.sidebar p { font-size: 0.85rem; color: var(--text-muted); line-height: 1.4; margin-bottom: 20px; }
|
||||||
|
|
||||||
|
.current-path { font-size: 11px; color: var(--text-muted); margin-bottom: 10px; word-break: break-all; font-family: monospace; }
|
||||||
|
|
||||||
|
.box-list {
|
||||||
|
flex: 1;
|
||||||
|
overflow-y: auto;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
border-radius: 4px;
|
||||||
|
padding: 10px;
|
||||||
|
background: var(--bg-light);
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-item {
|
||||||
|
font-family: monospace;
|
||||||
|
font-size: 11px;
|
||||||
|
padding: 6px;
|
||||||
|
border-bottom: 1px solid var(--border-color);
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-item:hover { background: var(--white); }
|
||||||
|
.btn-del { cursor: pointer; color: var(--danger); border: none; background: none; font-size: 16px; padding: 0 5px; }
|
||||||
|
|
||||||
|
.actions { display: flex; flex-direction: column; gap: 8px; }
|
||||||
|
|
||||||
|
/* Drawing Elements */
|
||||||
|
.draw-box {
|
||||||
|
position: absolute;
|
||||||
|
border: 2px solid var(--edit-mode-color);
|
||||||
|
background: rgba(255, 61, 0, 0.2);
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placed-box {
|
||||||
|
position: absolute;
|
||||||
|
border: 1.5px solid var(--primary-color);
|
||||||
|
background: rgba(30, 81, 73, 0.15);
|
||||||
|
cursor: pointer;
|
||||||
|
z-index: 50;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placed-box:hover {
|
||||||
|
background: rgba(30, 81, 73, 0.4);
|
||||||
|
border-color: #000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.placed-box.selected {
|
||||||
|
border: 2.5px solid var(--edit-mode-color);
|
||||||
|
z-index: 60;
|
||||||
|
box-shadow: 0 0 10px rgba(255,61,0,0.5);
|
||||||
|
}
|
||||||
|
|
||||||
|
.box-label {
|
||||||
|
position: absolute;
|
||||||
|
top: 50%;
|
||||||
|
left: 50%;
|
||||||
|
transform: translate(-50%, -50%);
|
||||||
|
font-size: 10px;
|
||||||
|
font-weight: bold;
|
||||||
|
color: var(--primary-color);
|
||||||
|
pointer-events: none;
|
||||||
|
white-space: nowrap;
|
||||||
|
background: rgba(255,255,255,0.7);
|
||||||
|
padding: 0 2px;
|
||||||
|
border-radius: 2px;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.draw-box .box-label {
|
||||||
|
color: var(--edit-mode-color);
|
||||||
|
background: rgba(255,255,255,0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
#save-status {
|
||||||
|
margin-top: 8px;
|
||||||
|
font-size: 11px;
|
||||||
|
color: var(--success);
|
||||||
|
text-align: center;
|
||||||
|
font-weight: bold;
|
||||||
|
height: 14px;
|
||||||
|
}
|
||||||
@@ -47,7 +47,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.modal-header .btn-icon {
|
.modal-header .btn-icon {
|
||||||
color: #FFFFFF !important;
|
color: var(--white) !important;
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
background: none !important;
|
background: none !important;
|
||||||
border: none !important;
|
border: none !important;
|
||||||
@@ -143,7 +143,7 @@
|
|||||||
.grid-form.is-edit-mode input,
|
.grid-form.is-edit-mode input,
|
||||||
.grid-form.is-edit-mode select,
|
.grid-form.is-edit-mode select,
|
||||||
.grid-form.is-edit-mode textarea {
|
.grid-form.is-edit-mode textarea {
|
||||||
color: #FF3D00; /* 수정 시 글자색 변경 */
|
color: var(--edit-mode-color); /* 수정 시 글자색 변경 */
|
||||||
border: 1px solid var(--border-color);
|
border: 1px solid var(--border-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -160,8 +160,8 @@
|
|||||||
.grid-form.is-edit-mode input:focus,
|
.grid-form.is-edit-mode input:focus,
|
||||||
.grid-form.is-edit-mode select:focus,
|
.grid-form.is-edit-mode select:focus,
|
||||||
.grid-form.is-edit-mode textarea:focus {
|
.grid-form.is-edit-mode textarea:focus {
|
||||||
border-color: #FF3D00;
|
border-color: var(--edit-mode-color);
|
||||||
box-shadow: 0 0 0 2px rgba(255, 61, 0, 0.1);
|
box-shadow: 0 0 0 2px var(--edit-mode-focus);
|
||||||
}
|
}
|
||||||
|
|
||||||
.form-section-title:first-child {
|
.form-section-title:first-child {
|
||||||
|
|||||||
@@ -128,7 +128,7 @@ table {
|
|||||||
|
|
||||||
th, td {
|
th, td {
|
||||||
padding: 0.8rem 1.2rem;
|
padding: 0.8rem 1.2rem;
|
||||||
border-bottom: 1px solid #F3F4F6;
|
border-bottom: 1px solid var(--border-color);
|
||||||
text-align: left; /* 기본은 좌측 정렬 */
|
text-align: left; /* 기본은 좌측 정렬 */
|
||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
@@ -140,7 +140,7 @@ thead {
|
|||||||
}
|
}
|
||||||
|
|
||||||
th {
|
th {
|
||||||
background-color: #FAFAFA !important;
|
background-color: var(--bg-light) !important;
|
||||||
font-size: 13px;
|
font-size: 13px;
|
||||||
font-weight: 600;
|
font-weight: 600;
|
||||||
color: var(--text-muted);
|
color: var(--text-muted);
|
||||||
@@ -158,7 +158,7 @@ td {
|
|||||||
}
|
}
|
||||||
|
|
||||||
tbody tr:hover {
|
tbody tr:hover {
|
||||||
background-color: #F9FAFB;
|
background-color: var(--bg-color);
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 정렬 클래스 강제 적용 */
|
/* 정렬 클래스 강제 적용 */
|
||||||
|
|||||||
@@ -8,17 +8,19 @@ export function renderCloudList(container: HTMLElement) {
|
|||||||
createListView(container, {
|
createListView(container, {
|
||||||
title: '클라우드',
|
title: '클라우드',
|
||||||
dataSource: () => state.masterData.cloud || [],
|
dataSource: () => state.masterData.cloud || [],
|
||||||
searchKeys: ['PRODUCT_NAME', 'ASSET_PURPOSE', 'PURCHASE_VENDOR'],
|
searchKeys: ['PRODUCT_NAME', 'ASSET_PURPOSE', 'PURCHASE_VENDOR', 'ASSET_TYPE'],
|
||||||
filterOptions: {
|
filterOptions: {
|
||||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui}/${ASSET_SCHEMA.PURCHASE_VENDOR.ui})`,
|
keywordLabel: `통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui}/${ASSET_SCHEMA.PURCHASE_VENDOR.ui})`,
|
||||||
showCorp: true,
|
showCorp: true,
|
||||||
showDept: true
|
showDept: true,
|
||||||
|
showType: true
|
||||||
},
|
},
|
||||||
onRowClick: (asset) => openSwModal(asset, 'view'),
|
onRowClick: (asset) => openSwModal(asset, 'view'),
|
||||||
columns: [
|
columns: [
|
||||||
{ header: ASSET_SCHEMA.PRODUCT_NAME.ui, sortKey: ASSET_SCHEMA.PRODUCT_NAME.key, render: a => a[ASSET_SCHEMA.PRODUCT_NAME.key] || '' },
|
{ header: ASSET_SCHEMA.PRODUCT_NAME.ui, sortKey: ASSET_SCHEMA.PRODUCT_NAME.key, render: a => a[ASSET_SCHEMA.PRODUCT_NAME.key] || '' },
|
||||||
{ header: ASSET_SCHEMA.ASSET_PURPOSE.ui, sortKey: ASSET_SCHEMA.ASSET_PURPOSE.key, render: a => a[ASSET_SCHEMA.ASSET_PURPOSE.key] || '' },
|
{ header: ASSET_SCHEMA.ASSET_PURPOSE.ui, sortKey: ASSET_SCHEMA.ASSET_PURPOSE.key, render: a => a[ASSET_SCHEMA.ASSET_PURPOSE.key] || '' },
|
||||||
{ header: ASSET_SCHEMA.PURCHASE_VENDOR.ui, sortKey: ASSET_SCHEMA.PURCHASE_VENDOR.key, render: a => a[ASSET_SCHEMA.PURCHASE_VENDOR.key] || '' },
|
{ header: ASSET_SCHEMA.PURCHASE_VENDOR.ui, sortKey: ASSET_SCHEMA.PURCHASE_VENDOR.key, render: a => a[ASSET_SCHEMA.PURCHASE_VENDOR.key] || '' },
|
||||||
|
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||||
{
|
{
|
||||||
header: ASSET_SCHEMA.PURCHASE_AMOUNT.ui,
|
header: ASSET_SCHEMA.PURCHASE_AMOUNT.ui,
|
||||||
sortKey: ASSET_SCHEMA.PURCHASE_AMOUNT.key,
|
sortKey: ASSET_SCHEMA.PURCHASE_AMOUNT.key,
|
||||||
|
|||||||
@@ -7,15 +7,16 @@ export function renderCostList(container: HTMLElement) {
|
|||||||
createListView(container, {
|
createListView(container, {
|
||||||
title: '비용관리',
|
title: '비용관리',
|
||||||
dataSource: () => sortAssets(state.masterData.cloud?.filter((a: any) => a.category === '비용관리') || []),
|
dataSource: () => sortAssets(state.masterData.cloud?.filter((a: any) => a.category === '비용관리') || []),
|
||||||
searchKeys: ['PRODUCT_NAME', 'MANAGER_MAIN', 'EMAIL_ACCOUNT'],
|
searchKeys: ['PRODUCT_NAME', 'MANAGER_MAIN', 'EMAIL_ACCOUNT', 'ASSET_TYPE'],
|
||||||
filterOptions: {
|
filterOptions: {
|
||||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui}/${ASSET_SCHEMA.MANAGER_MAIN.ui})`,
|
keywordLabel: `통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui}/${ASSET_SCHEMA.MANAGER_MAIN.ui})`,
|
||||||
showCorp: true,
|
showCorp: true,
|
||||||
showDept: true
|
showDept: true,
|
||||||
|
showType: true
|
||||||
},
|
},
|
||||||
onRowClick: () => alert('상세 정보 준비 중입니다.'),
|
onRowClick: () => alert('상세 정보 준비 중입니다.'),
|
||||||
columns: [
|
columns: [
|
||||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '' },
|
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||||
{ header: ASSET_SCHEMA.ASSET_PURPOSE.ui, sortKey: ASSET_SCHEMA.ASSET_PURPOSE.key, render: a => formatInline(a[ASSET_SCHEMA.ASSET_PURPOSE.key] || '-') },
|
{ header: ASSET_SCHEMA.ASSET_PURPOSE.ui, sortKey: ASSET_SCHEMA.ASSET_PURPOSE.key, render: a => formatInline(a[ASSET_SCHEMA.ASSET_PURPOSE.key] || '-') },
|
||||||
{ header: '현 사용자', sortKey: ASSET_SCHEMA.MANAGER_MAIN.key, align: 'center', render: a => a[ASSET_SCHEMA.MANAGER_MAIN.key] || '-' },
|
{ header: '현 사용자', sortKey: ASSET_SCHEMA.MANAGER_MAIN.key, align: 'center', render: a => a[ASSET_SCHEMA.MANAGER_MAIN.key] || '-' },
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -12,24 +12,20 @@ export function renderDomainList(container: HTMLElement) {
|
|||||||
createListView(container, {
|
createListView(container, {
|
||||||
title: '도메인',
|
title: '도메인',
|
||||||
dataSource: () => state.masterData.domain || [],
|
dataSource: () => state.masterData.domain || [],
|
||||||
searchKeys: ['DOMAIN_ADDR', 'ASSET_PURPOSE', 'PRODUCT_NAME'],
|
searchKeys: ['DOMAIN_ADDR', 'ASSET_PURPOSE', 'PRODUCT_NAME', 'ASSET_TYPE'],
|
||||||
persistentSortState,
|
persistentSortState,
|
||||||
emptyMessage: '등록된 도메인 정보가 없습니다.',
|
emptyMessage: '등록된 도메인 정보가 없습니다.',
|
||||||
filterOptions: {
|
filterOptions: {
|
||||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.DOMAIN_ADDR.ui}/${ASSET_SCHEMA.PRODUCT_NAME.ui})`,
|
keywordLabel: `통합 검색 (${ASSET_SCHEMA.DOMAIN_ADDR.ui}/${ASSET_SCHEMA.PRODUCT_NAME.ui})`,
|
||||||
showCorp: true,
|
showCorp: true,
|
||||||
showDept: true
|
showDept: true,
|
||||||
|
showType: true
|
||||||
},
|
},
|
||||||
onRowClick: (item) => openDomainModal(item),
|
onRowClick: (item) => openDomainModal(item),
|
||||||
columns: [
|
columns: [
|
||||||
{ header: ASSET_SCHEMA.DOMAIN_ADDR.ui, sortKey: ASSET_SCHEMA.DOMAIN_ADDR.key, align: 'left', render: a => a[ASSET_SCHEMA.DOMAIN_ADDR.key] || '' },
|
{ header: ASSET_SCHEMA.DOMAIN_ADDR.ui, sortKey: ASSET_SCHEMA.DOMAIN_ADDR.key, align: 'left', render: a => a[ASSET_SCHEMA.DOMAIN_ADDR.key] || '' },
|
||||||
{ header: ASSET_SCHEMA.ASSET_PURPOSE.ui, sortKey: ASSET_SCHEMA.ASSET_PURPOSE.key, align: 'left', render: a => a[ASSET_SCHEMA.ASSET_PURPOSE.key] || '' },
|
{ header: ASSET_SCHEMA.ASSET_PURPOSE.ui, sortKey: ASSET_SCHEMA.ASSET_PURPOSE.key, align: 'left', render: a => a[ASSET_SCHEMA.ASSET_PURPOSE.key] || '' },
|
||||||
{
|
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||||
header: ASSET_SCHEMA.ASSET_TYPE.ui,
|
|
||||||
sortKey: ASSET_SCHEMA.ASSET_TYPE.key,
|
|
||||||
align: 'center',
|
|
||||||
render: a => `<span class="badge badge-${a[ASSET_SCHEMA.ASSET_TYPE.key] === '관리중' ? 'primary' : 'muted'}">${a[ASSET_SCHEMA.ASSET_TYPE.key] || '-'}</span>`
|
|
||||||
},
|
|
||||||
{ header: ASSET_SCHEMA.PURCHASE_CORP.ui, sortKey: ASSET_SCHEMA.PURCHASE_CORP.key, align: 'center', render: a => a[ASSET_SCHEMA.PURCHASE_CORP.key] || '' },
|
{ header: ASSET_SCHEMA.PURCHASE_CORP.ui, sortKey: ASSET_SCHEMA.PURCHASE_CORP.key, align: 'center', render: a => a[ASSET_SCHEMA.PURCHASE_CORP.key] || '' },
|
||||||
{ header: ASSET_SCHEMA.EXPIRED_DATE.ui, sortKey: ASSET_SCHEMA.EXPIRED_DATE.key, align: 'center', render: a => a[ASSET_SCHEMA.EXPIRED_DATE.key] || '' },
|
{ header: ASSET_SCHEMA.EXPIRED_DATE.ui, sortKey: ASSET_SCHEMA.EXPIRED_DATE.key, align: 'center', render: a => a[ASSET_SCHEMA.EXPIRED_DATE.key] || '' },
|
||||||
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
|
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ export function renderEquipmentList(container: HTMLElement) {
|
|||||||
createListView(container, {
|
createListView(container, {
|
||||||
title: '업무지원장비',
|
title: '업무지원장비',
|
||||||
dataSource: () => sortAssets(state.masterData.equipment || []),
|
dataSource: () => sortAssets(state.masterData.equipment || []),
|
||||||
searchKeys: ['MODEL_NAME', 'CURRENT_USER', 'ASSET_MFR'],
|
searchKeys: ['MODEL_NAME', 'CURRENT_USER', 'ASSET_MFR', 'ASSET_TYPE'],
|
||||||
filterOptions: {
|
filterOptions: {
|
||||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.ASSET_MFR.ui})`,
|
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.ASSET_MFR.ui})`,
|
||||||
showLoc: true,
|
showLoc: true,
|
||||||
showDept: true
|
showDept: true,
|
||||||
|
showType: true
|
||||||
},
|
},
|
||||||
onRowClick: (asset) => openHwModal(asset, 'view'),
|
onRowClick: (asset) => openHwModal(asset, 'view'),
|
||||||
columns: [
|
columns: [
|
||||||
@@ -23,7 +24,7 @@ export function renderEquipmentList(container: HTMLElement) {
|
|||||||
render: a => `<span class="badge badge-${a[ASSET_SCHEMA.HW_STATUS.key] === '대여중' ? 'primary' : 'success'}">${a[ASSET_SCHEMA.HW_STATUS.key] || '보관중'}</span>`
|
render: a => `<span class="badge badge-${a[ASSET_SCHEMA.HW_STATUS.key] === '대여중' ? 'primary' : 'success'}">${a[ASSET_SCHEMA.HW_STATUS.key] || '보관중'}</span>`
|
||||||
},
|
},
|
||||||
{ header: ASSET_SCHEMA.CURRENT_USER.ui, sortKey: ASSET_SCHEMA.CURRENT_USER.key, align: 'center', render: a => a[ASSET_SCHEMA.CURRENT_USER.key] || '-' },
|
{ header: ASSET_SCHEMA.CURRENT_USER.ui, sortKey: ASSET_SCHEMA.CURRENT_USER.key, align: 'center', render: a => a[ASSET_SCHEMA.CURRENT_USER.key] || '-' },
|
||||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '' },
|
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||||
{ header: ASSET_SCHEMA.ASSET_MFR.ui, sortKey: ASSET_SCHEMA.ASSET_MFR.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_MFR.key] || '' },
|
{ header: ASSET_SCHEMA.ASSET_MFR.ui, sortKey: ASSET_SCHEMA.ASSET_MFR.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_MFR.key] || '' },
|
||||||
{ header: ASSET_SCHEMA.MODEL_NAME.ui, sortKey: ASSET_SCHEMA.MODEL_NAME.key, render: a => formatInline(a[ASSET_SCHEMA.MODEL_NAME.key] || a.명칭 || '-') },
|
{ header: ASSET_SCHEMA.MODEL_NAME.ui, sortKey: ASSET_SCHEMA.MODEL_NAME.key, render: a => formatInline(a[ASSET_SCHEMA.MODEL_NAME.key] || a.명칭 || '-') },
|
||||||
{ header: ASSET_SCHEMA.ASSET_COUNT.ui, sortKey: ASSET_SCHEMA.ASSET_COUNT.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_COUNT.key] || '1' },
|
{ header: ASSET_SCHEMA.ASSET_COUNT.ui, sortKey: ASSET_SCHEMA.ASSET_COUNT.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_COUNT.key] || '1' },
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ export function renderFacilityList(container: HTMLElement) {
|
|||||||
createListView(container, {
|
createListView(container, {
|
||||||
title: '사무가구',
|
title: '사무가구',
|
||||||
dataSource: () => sortAssets(state.masterData.equipment?.filter((a: any) => a.category === '시설자산') || []),
|
dataSource: () => sortAssets(state.masterData.equipment?.filter((a: any) => a.category === '시설자산') || []),
|
||||||
searchKeys: ['MODEL_NAME', 'ASSET_MFR'],
|
searchKeys: ['MODEL_NAME', 'ASSET_MFR', 'ASSET_TYPE'],
|
||||||
filterOptions: {
|
filterOptions: {
|
||||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui})`,
|
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui})`,
|
||||||
showLoc: true,
|
showLoc: true,
|
||||||
showDept: true
|
showDept: true,
|
||||||
|
showType: true
|
||||||
},
|
},
|
||||||
onRowClick: (asset) => openHwModal(asset, 'view'),
|
onRowClick: (asset) => openHwModal(asset, 'view'),
|
||||||
columns: [
|
columns: [
|
||||||
@@ -22,7 +23,7 @@ export function renderFacilityList(container: HTMLElement) {
|
|||||||
align: 'center',
|
align: 'center',
|
||||||
render: a => `<span class="badge badge-success">${a[ASSET_SCHEMA.HW_STATUS.key] || '보관중'}</span>`
|
render: a => `<span class="badge badge-success">${a[ASSET_SCHEMA.HW_STATUS.key] || '보관중'}</span>`
|
||||||
},
|
},
|
||||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '' },
|
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||||
{ header: ASSET_SCHEMA.ASSET_MFR.ui, sortKey: ASSET_SCHEMA.ASSET_MFR.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_MFR.key] || '' },
|
{ header: ASSET_SCHEMA.ASSET_MFR.ui, sortKey: ASSET_SCHEMA.ASSET_MFR.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_MFR.key] || '' },
|
||||||
{ header: ASSET_SCHEMA.MODEL_NAME.ui, sortKey: ASSET_SCHEMA.MODEL_NAME.key, render: a => formatInline(a[ASSET_SCHEMA.MODEL_NAME.key] || '-') },
|
{ header: ASSET_SCHEMA.MODEL_NAME.ui, sortKey: ASSET_SCHEMA.MODEL_NAME.key, render: a => formatInline(a[ASSET_SCHEMA.MODEL_NAME.key] || '-') },
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -7,15 +7,17 @@ export function renderGiftList(container: HTMLElement) {
|
|||||||
createListView(container, {
|
createListView(container, {
|
||||||
title: '선물',
|
title: '선물',
|
||||||
dataSource: () => sortAssets(state.masterData.equipment?.filter((a: any) => a.category === '선물') || []),
|
dataSource: () => sortAssets(state.masterData.equipment?.filter((a: any) => a.category === '선물') || []),
|
||||||
searchKeys: ['PRODUCT_NAME', 'MODEL_NAME'],
|
searchKeys: ['PRODUCT_NAME', 'MODEL_NAME', 'ASSET_TYPE'],
|
||||||
filterOptions: {
|
filterOptions: {
|
||||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui})`,
|
keywordLabel: `통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui})`,
|
||||||
showCorp: true,
|
showCorp: true,
|
||||||
showDept: true
|
showDept: true,
|
||||||
|
showType: true
|
||||||
},
|
},
|
||||||
onRowClick: () => alert('상세 정보 준비 중입니다.'),
|
onRowClick: () => alert('상세 정보 준비 중입니다.'),
|
||||||
columns: [
|
columns: [
|
||||||
{ header: '자산명', sortKey: ASSET_SCHEMA.PRODUCT_NAME.key, render: a => formatInline(a[ASSET_SCHEMA.PRODUCT_NAME.key] || a[ASSET_SCHEMA.MODEL_NAME.key] || '-') },
|
{ header: '자산명', sortKey: ASSET_SCHEMA.PRODUCT_NAME.key, render: a => formatInline(a[ASSET_SCHEMA.PRODUCT_NAME.key] || a[ASSET_SCHEMA.MODEL_NAME.key] || '-') },
|
||||||
|
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||||
{ header: '구매연월', sortKey: ASSET_SCHEMA.PURCHASE_DATE.key, align: 'center', render: a => a[ASSET_SCHEMA.PURCHASE_DATE.key] || '' },
|
{ header: '구매연월', sortKey: ASSET_SCHEMA.PURCHASE_DATE.key, align: 'center', render: a => a[ASSET_SCHEMA.PURCHASE_DATE.key] || '' },
|
||||||
{ header: ASSET_SCHEMA.EXPIRED_DATE.ui, sortKey: ASSET_SCHEMA.EXPIRED_DATE.key, align: 'center', render: a => a[ASSET_SCHEMA.EXPIRED_DATE.key] || '' },
|
{ header: ASSET_SCHEMA.EXPIRED_DATE.ui, sortKey: ASSET_SCHEMA.EXPIRED_DATE.key, align: 'center', render: a => a[ASSET_SCHEMA.EXPIRED_DATE.key] || '' },
|
||||||
{ header: ASSET_SCHEMA.ASSET_COUNT.ui, sortKey: ASSET_SCHEMA.ASSET_COUNT.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_COUNT.key] || '1' },
|
{ header: ASSET_SCHEMA.ASSET_COUNT.ui, sortKey: ASSET_SCHEMA.ASSET_COUNT.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_COUNT.key] || '1' },
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ export interface ListViewConfig {
|
|||||||
showDept?: boolean;
|
showDept?: boolean;
|
||||||
showLoc?: boolean;
|
showLoc?: boolean;
|
||||||
showField?: boolean;
|
showField?: boolean;
|
||||||
|
showType?: boolean;
|
||||||
};
|
};
|
||||||
columns: ColumnDef[];
|
columns: ColumnDef[];
|
||||||
onRowClick?: (asset: any) => void;
|
onRowClick?: (asset: any) => void;
|
||||||
@@ -37,7 +38,7 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
|
|||||||
let sortState: SortState = config.persistentSortState || { key: '', direction: 'asc' };
|
let sortState: SortState = config.persistentSortState || { key: '', direction: 'asc' };
|
||||||
|
|
||||||
// Initialize currentFilters with all possible keys to avoid undefined issues
|
// Initialize currentFilters with all possible keys to avoid undefined issues
|
||||||
let currentFilters: any = { keyword: '', corp: '', dept: '', loc: '', field: '' };
|
let currentFilters: any = { keyword: '', corp: '', dept: '', loc: '', field: '', type: '' };
|
||||||
|
|
||||||
const filterBar = document.createElement('div');
|
const filterBar = document.createElement('div');
|
||||||
filterBar.className = 'search-bar';
|
filterBar.className = 'search-bar';
|
||||||
@@ -151,6 +152,7 @@ export function createListView(container: HTMLElement, config: ListViewConfig) {
|
|||||||
if (config.filterOptions.showLoc) populateSelect('#filter-loc', ASSET_SCHEMA.LOCATION.key);
|
if (config.filterOptions.showLoc) populateSelect('#filter-loc', ASSET_SCHEMA.LOCATION.key);
|
||||||
if (config.filterOptions.showDept) populateSelect('#filter-dept', ASSET_SCHEMA.CURRENT_DEPT.key);
|
if (config.filterOptions.showDept) populateSelect('#filter-dept', ASSET_SCHEMA.CURRENT_DEPT.key);
|
||||||
if (config.filterOptions.showCorp) populateSelect('#filter-corp', ASSET_SCHEMA.PURCHASE_CORP.key);
|
if (config.filterOptions.showCorp) populateSelect('#filter-corp', ASSET_SCHEMA.PURCHASE_CORP.key);
|
||||||
|
if (config.filterOptions.showType) populateSelect('#filter-type', ASSET_SCHEMA.ASSET_TYPE.key);
|
||||||
|
|
||||||
// 6. 초기 렌더링
|
// 6. 초기 렌더링
|
||||||
updateTable();
|
updateTable();
|
||||||
|
|||||||
@@ -8,16 +8,18 @@ export function renderMobileList(container: HTMLElement) {
|
|||||||
createListView(container, {
|
createListView(container, {
|
||||||
title: 'PC', // Legacy support
|
title: 'PC', // Legacy support
|
||||||
dataSource: () => sortAssets(state.masterData.mobile || []),
|
dataSource: () => sortAssets(state.masterData.mobile || []),
|
||||||
searchKeys: ['MODEL_NAME'],
|
searchKeys: ['MODEL_NAME', 'ASSET_TYPE'],
|
||||||
filterOptions: {
|
filterOptions: {
|
||||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui})`,
|
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui})`,
|
||||||
showCorp: true,
|
showCorp: true,
|
||||||
showDept: true
|
showDept: true,
|
||||||
|
showType: true
|
||||||
},
|
},
|
||||||
onRowClick: (asset) => openHwModal(asset, 'view'),
|
onRowClick: (asset) => openHwModal(asset, 'view'),
|
||||||
columns: [
|
columns: [
|
||||||
{ header: ASSET_SCHEMA.HW_STATUS.ui, sortKey: ASSET_SCHEMA.HW_STATUS.key, align: 'center', render: a => a[ASSET_SCHEMA.HW_STATUS.key] || '운영중' },
|
{ header: ASSET_SCHEMA.HW_STATUS.ui, sortKey: ASSET_SCHEMA.HW_STATUS.key, align: 'center', render: a => a[ASSET_SCHEMA.HW_STATUS.key] || '운영중' },
|
||||||
{ header: ASSET_SCHEMA.PURCHASE_CORP.ui, sortKey: ASSET_SCHEMA.PURCHASE_CORP.key, align: 'center', render: a => a[ASSET_SCHEMA.PURCHASE_CORP.key] || '' },
|
{ header: ASSET_SCHEMA.PURCHASE_CORP.ui, sortKey: ASSET_SCHEMA.PURCHASE_CORP.key, align: 'center', render: a => a[ASSET_SCHEMA.PURCHASE_CORP.key] || '' },
|
||||||
|
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||||
{ header: ASSET_SCHEMA.MODEL_NAME.ui, sortKey: ASSET_SCHEMA.MODEL_NAME.key, render: a => a[ASSET_SCHEMA.MODEL_NAME.key] || '' },
|
{ header: ASSET_SCHEMA.MODEL_NAME.ui, sortKey: ASSET_SCHEMA.MODEL_NAME.key, render: a => a[ASSET_SCHEMA.MODEL_NAME.key] || '' },
|
||||||
{
|
{
|
||||||
header: ASSET_SCHEMA.LOCATION.ui,
|
header: ASSET_SCHEMA.LOCATION.ui,
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ export function renderNetworkList(container: HTMLElement) {
|
|||||||
createListView(container, {
|
createListView(container, {
|
||||||
title: '네트워크',
|
title: '네트워크',
|
||||||
dataSource: () => sortAssets(state.masterData.network || []),
|
dataSource: () => sortAssets(state.masterData.network || []),
|
||||||
searchKeys: ['MODEL_NAME', 'CURRENT_USER', 'ASSET_MFR'],
|
searchKeys: ['MODEL_NAME', 'CURRENT_USER', 'ASSET_MFR', 'ASSET_TYPE'],
|
||||||
filterOptions: {
|
filterOptions: {
|
||||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.ASSET_MFR.ui})`,
|
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.ASSET_MFR.ui})`,
|
||||||
showLoc: true,
|
showLoc: true,
|
||||||
showDept: true
|
showDept: true,
|
||||||
|
showType: true
|
||||||
},
|
},
|
||||||
onRowClick: (asset) => openHwModal(asset, 'view'),
|
onRowClick: (asset) => openHwModal(asset, 'view'),
|
||||||
columns: [
|
columns: [
|
||||||
@@ -23,7 +24,7 @@ export function renderNetworkList(container: HTMLElement) {
|
|||||||
render: a => `<span class="badge badge-success">${a[ASSET_SCHEMA.HW_STATUS.key] || '운영중'}</span>`
|
render: a => `<span class="badge badge-success">${a[ASSET_SCHEMA.HW_STATUS.key] || '운영중'}</span>`
|
||||||
},
|
},
|
||||||
{ header: ASSET_SCHEMA.CURRENT_USER.ui, sortKey: ASSET_SCHEMA.CURRENT_USER.key, align: 'center', render: a => a[ASSET_SCHEMA.CURRENT_USER.key] || '-' },
|
{ header: ASSET_SCHEMA.CURRENT_USER.ui, sortKey: ASSET_SCHEMA.CURRENT_USER.key, align: 'center', render: a => a[ASSET_SCHEMA.CURRENT_USER.key] || '-' },
|
||||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '' },
|
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||||
{ header: ASSET_SCHEMA.ASSET_MFR.ui, sortKey: ASSET_SCHEMA.ASSET_MFR.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_MFR.key] || '' },
|
{ header: ASSET_SCHEMA.ASSET_MFR.ui, sortKey: ASSET_SCHEMA.ASSET_MFR.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_MFR.key] || '' },
|
||||||
{ header: ASSET_SCHEMA.MODEL_NAME.ui, sortKey: ASSET_SCHEMA.MODEL_NAME.key, render: a => formatInline(a[ASSET_SCHEMA.MODEL_NAME.key] || '-') },
|
{ header: ASSET_SCHEMA.MODEL_NAME.ui, sortKey: ASSET_SCHEMA.MODEL_NAME.key, render: a => formatInline(a[ASSET_SCHEMA.MODEL_NAME.key] || '-') },
|
||||||
{ header: ASSET_SCHEMA.ASSET_COUNT.ui, sortKey: ASSET_SCHEMA.ASSET_COUNT.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_COUNT.key] || '1' },
|
{ header: ASSET_SCHEMA.ASSET_COUNT.ui, sortKey: ASSET_SCHEMA.ASSET_COUNT.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_COUNT.key] || '1' },
|
||||||
|
|||||||
@@ -8,32 +8,40 @@ export function renderPcList(container: HTMLElement) {
|
|||||||
createListView(container, {
|
createListView(container, {
|
||||||
title: 'PC',
|
title: 'PC',
|
||||||
dataSource: () => sortAssets((state.masterData.pc || []).filter((a: any) => a.asset_type !== '서버PC')),
|
dataSource: () => sortAssets((state.masterData.pc || []).filter((a: any) => a.asset_type !== '서버PC')),
|
||||||
searchKeys: ['CURRENT_DEPT', 'CURRENT_USER', 'MODEL_NAME', 'MAC_ADDR', 'MANAGER_MAIN'],
|
searchKeys: ['CURRENT_DEPT', 'CURRENT_USER', 'MODEL_NAME', 'MAC_ADDR', 'MANAGER_MAIN', 'ASSET_TYPE'],
|
||||||
filterOptions: {
|
filterOptions: {
|
||||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.MANAGER_MAIN.ui}/${ASSET_SCHEMA.CURRENT_USER.ui})`,
|
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.MANAGER_MAIN.ui}/${ASSET_SCHEMA.CURRENT_USER.ui})`,
|
||||||
showLoc: true,
|
showLoc: true,
|
||||||
showDept: true
|
showDept: true,
|
||||||
|
showType: true
|
||||||
},
|
},
|
||||||
onRowClick: (asset) => openHwModal(asset, 'view'),
|
onRowClick: (asset) => openHwModal(asset, 'view'),
|
||||||
columns: [
|
columns: [
|
||||||
{ header: ASSET_SCHEMA.CURRENT_USER.ui, sortKey: ASSET_SCHEMA.CURRENT_USER.key, align: 'center', render: a => a[ASSET_SCHEMA.CURRENT_USER.key] || '-' },
|
{ header: ASSET_SCHEMA.CURRENT_USER.ui, sortKey: ASSET_SCHEMA.CURRENT_USER.key, align: 'center', render: a => a[ASSET_SCHEMA.CURRENT_USER.key] || '-' },
|
||||||
|
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||||
{ header: ASSET_SCHEMA.CPU.ui, sortKey: ASSET_SCHEMA.CPU.key, align: 'center', render: a => a[ASSET_SCHEMA.CPU.key] || '' },
|
{ header: ASSET_SCHEMA.CPU.ui, sortKey: ASSET_SCHEMA.CPU.key, align: 'center', render: a => a[ASSET_SCHEMA.CPU.key] || '' },
|
||||||
{ header: ASSET_SCHEMA.MAINBOARD.ui, sortKey: ASSET_SCHEMA.MAINBOARD.key, align: 'center', render: a => a[ASSET_SCHEMA.MAINBOARD.key] || '-' },
|
{ header: ASSET_SCHEMA.MAINBOARD.ui, sortKey: ASSET_SCHEMA.MAINBOARD.key, align: 'center', render: a => a[ASSET_SCHEMA.MAINBOARD.key] || '-' },
|
||||||
{ header: ASSET_SCHEMA.RAM.ui, sortKey: ASSET_SCHEMA.RAM.key, align: 'center', render: a => a[ASSET_SCHEMA.RAM.key] || '' },
|
{ header: ASSET_SCHEMA.RAM.ui, sortKey: ASSET_SCHEMA.RAM.key, align: 'center', render: a => a[ASSET_SCHEMA.RAM.key] || '' },
|
||||||
{ header: ASSET_SCHEMA.GPU.ui, sortKey: ASSET_SCHEMA.GPU.key, align: 'center', render: a => a[ASSET_SCHEMA.GPU.key] || '-' },
|
{ header: ASSET_SCHEMA.GPU.ui, sortKey: ASSET_SCHEMA.GPU.key, align: 'center', render: a => a[ASSET_SCHEMA.GPU.key] || '-' },
|
||||||
{ header: 'SSD1', sortKey: ASSET_SCHEMA.SSD1.key, align: 'center', render: a => a[ASSET_SCHEMA.SSD1.key] || '-' },
|
{
|
||||||
{ header: 'SSD2', sortKey: ASSET_SCHEMA.SSD2.key, align: 'center', render: a => a[ASSET_SCHEMA.SSD2.key] || '-' },
|
header: 'SSD',
|
||||||
{ header: 'HDD1', sortKey: ASSET_SCHEMA.HDD1.key, align: 'center', render: a => a[ASSET_SCHEMA.HDD1.key] || '-' },
|
align: 'center',
|
||||||
{ header: 'HDD2', sortKey: ASSET_SCHEMA.HDD2.key, align: 'center', render: a => a[ASSET_SCHEMA.HDD2.key] || '-' },
|
width: '8%',
|
||||||
{ header: 'HDD3', sortKey: ASSET_SCHEMA.HDD3.key, align: 'center', render: a => a[ASSET_SCHEMA.HDD3.key] || '-' },
|
render: a => [a[ASSET_SCHEMA.SSD1.key], a[ASSET_SCHEMA.SSD2.key]].filter(Boolean).join(' / ') || '-'
|
||||||
{ header: 'HDD4', sortKey: ASSET_SCHEMA.HDD4.key, align: 'center', render: a => a[ASSET_SCHEMA.HDD4.key] || '-' },
|
},
|
||||||
|
{
|
||||||
|
header: 'HDD',
|
||||||
|
align: 'center',
|
||||||
|
width: '12%',
|
||||||
|
render: a => [a[ASSET_SCHEMA.HDD1.key], a[ASSET_SCHEMA.HDD2.key], a[ASSET_SCHEMA.HDD3.key], a[ASSET_SCHEMA.HDD4.key]].filter(Boolean).join(' / ') || '-'
|
||||||
|
},
|
||||||
{
|
{
|
||||||
header: ASSET_SCHEMA.MAC_ADDR.ui,
|
header: ASSET_SCHEMA.MAC_ADDR.ui,
|
||||||
sortKey: ASSET_SCHEMA.MAC_ADDR.key,
|
sortKey: ASSET_SCHEMA.MAC_ADDR.key,
|
||||||
align: 'center',
|
align: 'center',
|
||||||
render: a => `<span style="font-family:monospace; font-size:11px;">${a[ASSET_SCHEMA.MAC_ADDR.key] || '-'}</span>`
|
render: a => `<span style="font-family:monospace; font-size:11px;">${a[ASSET_SCHEMA.MAC_ADDR.key] || '-'}</span>`
|
||||||
},
|
},
|
||||||
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
|
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', width: '30%', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
|
||||||
]
|
]
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ export function renderPcPartList(container: HTMLElement) {
|
|||||||
filterOptions: {
|
filterOptions: {
|
||||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui})`,
|
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui})`,
|
||||||
showLoc: true,
|
showLoc: true,
|
||||||
showDept: true
|
showDept: true,
|
||||||
|
showType: true
|
||||||
},
|
},
|
||||||
onRowClick: (asset) => openHwModal(asset, 'view'),
|
onRowClick: (asset) => openHwModal(asset, 'view'),
|
||||||
columns: [
|
columns: [
|
||||||
@@ -22,7 +23,7 @@ export function renderPcPartList(container: HTMLElement) {
|
|||||||
align: 'center',
|
align: 'center',
|
||||||
render: a => `<span class="badge badge-success">${a[ASSET_SCHEMA.HW_STATUS.key] || '보관중'}</span>`
|
render: a => `<span class="badge badge-success">${a[ASSET_SCHEMA.HW_STATUS.key] || '보관중'}</span>`
|
||||||
},
|
},
|
||||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '' },
|
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||||
{ header: ASSET_SCHEMA.ASSET_MFR.ui, sortKey: ASSET_SCHEMA.ASSET_MFR.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_MFR.key] || '' },
|
{ header: ASSET_SCHEMA.ASSET_MFR.ui, sortKey: ASSET_SCHEMA.ASSET_MFR.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_MFR.key] || '' },
|
||||||
{ header: ASSET_SCHEMA.MODEL_NAME.ui, sortKey: ASSET_SCHEMA.MODEL_NAME.key, render: a => formatInline(a[ASSET_SCHEMA.MODEL_NAME.key] || '-') },
|
{ header: ASSET_SCHEMA.MODEL_NAME.ui, sortKey: ASSET_SCHEMA.MODEL_NAME.key, render: a => formatInline(a[ASSET_SCHEMA.MODEL_NAME.key] || '-') },
|
||||||
{ header: ASSET_SCHEMA.VOLUME.ui, sortKey: ASSET_SCHEMA.VOLUME.key, align: 'center', render: a => a[ASSET_SCHEMA.VOLUME.key] || '-' },
|
{ header: ASSET_SCHEMA.VOLUME.ui, sortKey: ASSET_SCHEMA.VOLUME.key, align: 'center', render: a => a[ASSET_SCHEMA.VOLUME.key] || '-' },
|
||||||
|
|||||||
@@ -12,11 +12,12 @@ export function renderServerList(container: HTMLElement) {
|
|||||||
const serverPcList = (state.masterData.pc || []).filter((a: any) => a.asset_type === '서버PC');
|
const serverPcList = (state.masterData.pc || []).filter((a: any) => a.asset_type === '서버PC');
|
||||||
return sortAssets([...serverList, ...serverPcList]);
|
return sortAssets([...serverList, ...serverPcList]);
|
||||||
},
|
},
|
||||||
searchKeys: ['CURRENT_DEPT', 'MODEL_NAME', 'ASSET_PURPOSE'],
|
searchKeys: ['CURRENT_DEPT', 'MODEL_NAME', 'ASSET_PURPOSE', 'ASSET_TYPE'],
|
||||||
filterOptions: {
|
filterOptions: {
|
||||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.CURRENT_DEPT.ui}/${ASSET_SCHEMA.MODEL_NAME.ui})`,
|
keywordLabel: `통합 검색 (${ASSET_SCHEMA.CURRENT_DEPT.ui}/${ASSET_SCHEMA.MODEL_NAME.ui})`,
|
||||||
showLoc: true,
|
showLoc: true,
|
||||||
showDept: true
|
showDept: true,
|
||||||
|
showType: true
|
||||||
},
|
},
|
||||||
onRowClick: (asset) => openHwModal(asset, 'view'),
|
onRowClick: (asset) => openHwModal(asset, 'view'),
|
||||||
columns: [
|
columns: [
|
||||||
|
|||||||
@@ -8,11 +8,12 @@ export function renderSpaceInfoList(container: HTMLElement) {
|
|||||||
createListView(container, {
|
createListView(container, {
|
||||||
title: '공간정보장비',
|
title: '공간정보장비',
|
||||||
dataSource: () => sortAssets(state.masterData.equipment?.filter((a: any) => a.category === '공간정보장비') || []),
|
dataSource: () => sortAssets(state.masterData.equipment?.filter((a: any) => a.category === '공간정보장비') || []),
|
||||||
searchKeys: ['MODEL_NAME', 'PRODUCT_NAME', 'CURRENT_USER'],
|
searchKeys: ['MODEL_NAME', 'PRODUCT_NAME', 'CURRENT_USER', 'ASSET_TYPE'],
|
||||||
filterOptions: {
|
filterOptions: {
|
||||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.CURRENT_USER.ui})`,
|
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.CURRENT_USER.ui})`,
|
||||||
showLoc: true,
|
showLoc: true,
|
||||||
showDept: true
|
showDept: true,
|
||||||
|
showType: true
|
||||||
},
|
},
|
||||||
onRowClick: (asset) => openHwModal(asset, 'view'),
|
onRowClick: (asset) => openHwModal(asset, 'view'),
|
||||||
columns: [
|
columns: [
|
||||||
@@ -24,7 +25,7 @@ export function renderSpaceInfoList(container: HTMLElement) {
|
|||||||
},
|
},
|
||||||
{ header: ASSET_SCHEMA.CURRENT_USER.ui, sortKey: ASSET_SCHEMA.CURRENT_USER.key, align: 'center', render: a => a[ASSET_SCHEMA.CURRENT_USER.key] || '-' },
|
{ header: ASSET_SCHEMA.CURRENT_USER.ui, sortKey: ASSET_SCHEMA.CURRENT_USER.key, align: 'center', render: a => a[ASSET_SCHEMA.CURRENT_USER.key] || '-' },
|
||||||
{ header: ASSET_SCHEMA.ASSET_NAME.ui, sortKey: ASSET_SCHEMA.ASSET_NAME.key, render: a => formatInline(a[ASSET_SCHEMA.PRODUCT_NAME.key] || a[ASSET_SCHEMA.MODEL_NAME.key] || a[ASSET_SCHEMA.ASSET_NAME.key] || '-') },
|
{ header: ASSET_SCHEMA.ASSET_NAME.ui, sortKey: ASSET_SCHEMA.ASSET_NAME.key, render: a => formatInline(a[ASSET_SCHEMA.PRODUCT_NAME.key] || a[ASSET_SCHEMA.MODEL_NAME.key] || a[ASSET_SCHEMA.ASSET_NAME.key] || '-') },
|
||||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '' },
|
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||||
{
|
{
|
||||||
header: ASSET_SCHEMA.LOCATION.ui,
|
header: ASSET_SCHEMA.LOCATION.ui,
|
||||||
sortKey: ASSET_SCHEMA.LOCATION.key,
|
sortKey: ASSET_SCHEMA.LOCATION.key,
|
||||||
|
|||||||
@@ -8,17 +8,18 @@ export function renderStorageList(container: HTMLElement) {
|
|||||||
createListView(container, {
|
createListView(container, {
|
||||||
title: '스토리지',
|
title: '스토리지',
|
||||||
dataSource: () => sortAssets(state.masterData.storage || []),
|
dataSource: () => sortAssets(state.masterData.storage || []),
|
||||||
searchKeys: ['MODEL_NAME', 'CURRENT_USER', 'SERIAL_NUM'],
|
searchKeys: ['MODEL_NAME', 'CURRENT_USER', 'SERIAL_NUM', 'ASSET_TYPE'],
|
||||||
filterOptions: {
|
filterOptions: {
|
||||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.CURRENT_USER.ui})`,
|
keywordLabel: `통합 검색 (${ASSET_SCHEMA.MODEL_NAME.ui}/${ASSET_SCHEMA.CURRENT_USER.ui})`,
|
||||||
showLoc: true,
|
showLoc: true,
|
||||||
showDept: true
|
showDept: true,
|
||||||
|
showType: true
|
||||||
},
|
},
|
||||||
onRowClick: (asset) => openHwModal(asset, 'view'),
|
onRowClick: (asset) => openHwModal(asset, 'view'),
|
||||||
columns: [
|
columns: [
|
||||||
{ header: ASSET_SCHEMA.HW_STATUS.ui, sortKey: ASSET_SCHEMA.HW_STATUS.key, align: 'center', render: a => a[ASSET_SCHEMA.HW_STATUS.key] || '-' },
|
{ header: ASSET_SCHEMA.HW_STATUS.ui, sortKey: ASSET_SCHEMA.HW_STATUS.key, align: 'center', render: a => a[ASSET_SCHEMA.HW_STATUS.key] || '-' },
|
||||||
{ header: ASSET_SCHEMA.CURRENT_USER.ui, sortKey: ASSET_SCHEMA.CURRENT_USER.key, align: 'center', render: a => a[ASSET_SCHEMA.CURRENT_USER.key] || '-' },
|
{ header: ASSET_SCHEMA.CURRENT_USER.ui, sortKey: ASSET_SCHEMA.CURRENT_USER.key, align: 'center', render: a => a[ASSET_SCHEMA.CURRENT_USER.key] || '-' },
|
||||||
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||||
{ header: ASSET_SCHEMA.VOLUME.ui, sortKey: ASSET_SCHEMA.VOLUME.key, align: 'center', render: a => a[ASSET_SCHEMA.VOLUME.key] || '-' },
|
{ header: ASSET_SCHEMA.VOLUME.ui, sortKey: ASSET_SCHEMA.VOLUME.key, align: 'center', render: a => a[ASSET_SCHEMA.VOLUME.key] || '-' },
|
||||||
{ header: ASSET_SCHEMA.MODEL_NAME.ui, sortKey: ASSET_SCHEMA.MODEL_NAME.key, render: a => formatInline(a[ASSET_SCHEMA.MODEL_NAME.key] || a[ASSET_SCHEMA.ASSET_NAME.key] || '-') },
|
{ header: ASSET_SCHEMA.MODEL_NAME.ui, sortKey: ASSET_SCHEMA.MODEL_NAME.key, render: a => formatInline(a[ASSET_SCHEMA.MODEL_NAME.key] || a[ASSET_SCHEMA.ASSET_NAME.key] || '-') },
|
||||||
{ header: ASSET_SCHEMA.SERIAL_NUM.ui, sortKey: ASSET_SCHEMA.SERIAL_NUM.key, align: 'center', render: a => a[ASSET_SCHEMA.SERIAL_NUM.key] || '-' },
|
{ header: ASSET_SCHEMA.SERIAL_NUM.ui, sortKey: ASSET_SCHEMA.SERIAL_NUM.key, align: 'center', render: a => a[ASSET_SCHEMA.SERIAL_NUM.key] || '-' },
|
||||||
|
|||||||
@@ -10,24 +10,26 @@ export function renderSwList(container: HTMLElement) {
|
|||||||
createListView(container, {
|
createListView(container, {
|
||||||
title: isInternal ? '내부' : '외부',
|
title: isInternal ? '내부' : '외부',
|
||||||
dataSource: () => sortAssets(isInternal ? state.masterData.swInternal : state.masterData.swExternal),
|
dataSource: () => sortAssets(isInternal ? state.masterData.swInternal : state.masterData.swExternal),
|
||||||
searchKeys: ['PRODUCT_NAME', 'CURRENT_USER', 'CURRENT_DEPT'],
|
searchKeys: ['PRODUCT_NAME', 'CURRENT_USER', 'CURRENT_DEPT', 'ASSET_TYPE'],
|
||||||
emptyMessage: '검색 결과가 없습니다.',
|
emptyMessage: '검색 결과가 없습니다.',
|
||||||
filterOptions: {
|
filterOptions: {
|
||||||
keywordLabel: `통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui}/${ASSET_SCHEMA.CURRENT_DEPT.ui})`,
|
keywordLabel: `통합 검색 (${ASSET_SCHEMA.PRODUCT_NAME.ui}/${ASSET_SCHEMA.CURRENT_DEPT.ui})`,
|
||||||
showField: true,
|
showField: true,
|
||||||
showCorp: true,
|
showCorp: true,
|
||||||
showDept: true
|
showDept: true,
|
||||||
|
showType: true
|
||||||
},
|
},
|
||||||
onRowClick: (asset) => openSwModal(asset, 'view'),
|
onRowClick: (asset) => openSwModal(asset, 'view'),
|
||||||
columns: isInternal ? [
|
columns: isInternal ? [
|
||||||
{ header: ASSET_SCHEMA.SW_FIELD.ui, sortKey: ASSET_SCHEMA.SW_FIELD.key, align: 'center', render: a => a[ASSET_SCHEMA.SW_FIELD.key] || '' },
|
{ header: ASSET_SCHEMA.SW_FIELD.ui, sortKey: ASSET_SCHEMA.SW_FIELD.key, align: 'center', render: a => a[ASSET_SCHEMA.SW_FIELD.key] || '' },
|
||||||
{ header: ASSET_SCHEMA.DEV_OBJ.ui, sortKey: ASSET_SCHEMA.DEV_OBJ.key, align: 'center', render: a => a[ASSET_SCHEMA.DEV_OBJ.key] || '' },
|
{ header: ASSET_SCHEMA.DEV_OBJ.ui, sortKey: ASSET_SCHEMA.DEV_OBJ.key, align: 'center', render: a => a[ASSET_SCHEMA.DEV_OBJ.key] || '' },
|
||||||
{ header: ASSET_SCHEMA.SW_STATUS.ui, sortKey: ASSET_SCHEMA.SW_STATUS.key, align: 'center', render: a => a[ASSET_SCHEMA.SW_STATUS.key] || '보유중' },
|
{ header: ASSET_SCHEMA.SW_STATUS.ui, sortKey: ASSET_SCHEMA.SW_STATUS.key, align: 'center', render: a => a[ASSET_SCHEMA.SW_STATUS.key] || '보유중' },
|
||||||
|
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||||
{ header: ASSET_SCHEMA.SW_TYPE.ui, sortKey: ASSET_SCHEMA.SW_TYPE.key, align: 'center', render: a => a[ASSET_SCHEMA.SW_TYPE.key] || '내부' },
|
{ header: ASSET_SCHEMA.SW_TYPE.ui, sortKey: ASSET_SCHEMA.SW_TYPE.key, align: 'center', render: a => a[ASSET_SCHEMA.SW_TYPE.key] || '내부' },
|
||||||
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
|
{ header: ASSET_SCHEMA.MEMO.ui, sortKey: ASSET_SCHEMA.MEMO.key, className: 'col-memo', render: a => formatInline(a[ASSET_SCHEMA.MEMO.key] || '-') }
|
||||||
] : [
|
] : [
|
||||||
{ header: '자산명', sortKey: ASSET_SCHEMA.PRODUCT_NAME.key, render: a => a[ASSET_SCHEMA.PRODUCT_NAME.key] || '' },
|
{ header: '자산명', sortKey: ASSET_SCHEMA.PRODUCT_NAME.key, render: a => a[ASSET_SCHEMA.PRODUCT_NAME.key] || '' },
|
||||||
{ header: '유형', sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '외부' },
|
{ header: ASSET_SCHEMA.ASSET_TYPE.ui, sortKey: ASSET_SCHEMA.ASSET_TYPE.key, align: 'center', width: '10%', render: a => a[ASSET_SCHEMA.ASSET_TYPE.key] || '-' },
|
||||||
{ header: ASSET_SCHEMA.SW_STATUS.ui, sortKey: ASSET_SCHEMA.SW_STATUS.key, align: 'center', render: a => a[ASSET_SCHEMA.SW_STATUS.key] || '사용중' },
|
{ header: ASSET_SCHEMA.SW_STATUS.ui, sortKey: ASSET_SCHEMA.SW_STATUS.key, align: 'center', render: a => a[ASSET_SCHEMA.SW_STATUS.key] || '사용중' },
|
||||||
{ header: ASSET_SCHEMA.SW_FIELD.ui, sortKey: ASSET_SCHEMA.SW_FIELD.key, align: 'center', render: a => a[ASSET_SCHEMA.SW_FIELD.key] || '' },
|
{ header: ASSET_SCHEMA.SW_FIELD.ui, sortKey: ASSET_SCHEMA.SW_FIELD.key, align: 'center', render: a => a[ASSET_SCHEMA.SW_FIELD.key] || '' },
|
||||||
{ header: ASSET_SCHEMA.CURRENT_DEPT.ui, sortKey: ASSET_SCHEMA.CURRENT_DEPT.key, align: 'center', render: a => a[ASSET_SCHEMA.CURRENT_DEPT.key] || '' },
|
{ header: ASSET_SCHEMA.CURRENT_DEPT.ui, sortKey: ASSET_SCHEMA.CURRENT_DEPT.key, align: 'center', render: a => a[ASSET_SCHEMA.CURRENT_DEPT.key] || '' },
|
||||||
|
|||||||
222
src/views/MapEditor.ts
Normal file
222
src/views/MapEditor.ts
Normal file
@@ -0,0 +1,222 @@
|
|||||||
|
import { IMAGE_LOCATIONS } from '../components/Modal/SharedData';
|
||||||
|
import { createIcons, X, Save, Trash2, ChevronLeft, ChevronRight } from 'lucide';
|
||||||
|
|
||||||
|
export class MapEditor {
|
||||||
|
private container: HTMLElement;
|
||||||
|
private wrapper: HTMLElement;
|
||||||
|
private img: HTMLImageElement;
|
||||||
|
private boxListEl: HTMLElement;
|
||||||
|
private pathLabel: HTMLElement;
|
||||||
|
private statusEl: HTMLElement;
|
||||||
|
private saveBtn: HTMLButtonElement;
|
||||||
|
private fileSidebar: HTMLElement;
|
||||||
|
|
||||||
|
private allMapConfig: Record<string, any[]> = {};
|
||||||
|
private boxes: any[] = [];
|
||||||
|
private isDrawing: boolean = false;
|
||||||
|
private startX: number = 0;
|
||||||
|
private startY: number = 0;
|
||||||
|
private currentBox: HTMLElement | null = null;
|
||||||
|
private currentPath: string = '';
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
this.container = document.getElementById('container')!;
|
||||||
|
this.wrapper = document.getElementById('wrapper')!;
|
||||||
|
this.img = document.getElementById('target-img') as HTMLImageElement;
|
||||||
|
this.boxListEl = document.getElementById('box-list')!;
|
||||||
|
this.pathLabel = document.getElementById('current-path')!;
|
||||||
|
this.statusEl = document.getElementById('save-status')!;
|
||||||
|
this.saveBtn = document.getElementById('btn-save-server') as HTMLButtonElement;
|
||||||
|
this.fileSidebar = document.getElementById('file-sidebar')!;
|
||||||
|
}
|
||||||
|
|
||||||
|
public async init() {
|
||||||
|
this.renderFileSidebar();
|
||||||
|
await this.loadConfig();
|
||||||
|
this.bindEvents();
|
||||||
|
this.selectFirstFile();
|
||||||
|
createIcons({ icons: { X, Save, Trash2, ChevronLeft, ChevronRight } });
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderFileSidebar() {
|
||||||
|
let html = '';
|
||||||
|
Object.entries(IMAGE_LOCATIONS).forEach(([bldg, details]) => {
|
||||||
|
html += `<div class="folder-item">${bldg}</div>`;
|
||||||
|
Object.entries(details).forEach(([detail, paths]) => {
|
||||||
|
paths.forEach(path => {
|
||||||
|
const fileName = path.split('/').pop() || path;
|
||||||
|
html += `<div class="file-item" data-path="${path}">${fileName}</div>`;
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
this.fileSidebar.innerHTML = html;
|
||||||
|
|
||||||
|
this.fileSidebar.querySelectorAll('.file-item').forEach(item => {
|
||||||
|
item.addEventListener('click', () => {
|
||||||
|
this.fileSidebar.querySelectorAll('.file-item').forEach(i => i.classList.remove('active'));
|
||||||
|
item.classList.add('active');
|
||||||
|
this.renderCurrentFile();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
private selectFirstFile() {
|
||||||
|
const firstItem = this.fileSidebar.querySelector('.file-item') as HTMLElement;
|
||||||
|
if (firstItem) {
|
||||||
|
firstItem.classList.add('active');
|
||||||
|
this.renderCurrentFile();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private async loadConfig() {
|
||||||
|
try {
|
||||||
|
const res = await fetch(`http://${location.hostname}:3000/api/maps`);
|
||||||
|
this.allMapConfig = await res.json();
|
||||||
|
} catch (err) {
|
||||||
|
console.error('Failed to load config:', err);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private renderCurrentFile() {
|
||||||
|
const activeItem = this.fileSidebar.querySelector('.file-item.active') as HTMLElement;
|
||||||
|
if (!activeItem) return;
|
||||||
|
|
||||||
|
this.currentPath = activeItem.dataset.path || '';
|
||||||
|
this.boxes = this.allMapConfig[this.currentPath] || [];
|
||||||
|
this.pathLabel.textContent = this.currentPath;
|
||||||
|
this.img.src = this.currentPath;
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
private bindEvents() {
|
||||||
|
this.wrapper.addEventListener('mousedown', (e) => {
|
||||||
|
if (e.button !== 0) return;
|
||||||
|
this.isDrawing = true;
|
||||||
|
const rect = this.wrapper.getBoundingClientRect();
|
||||||
|
this.startX = e.clientX - rect.left;
|
||||||
|
this.startY = e.clientY - rect.top;
|
||||||
|
|
||||||
|
this.currentBox = document.createElement('div');
|
||||||
|
this.currentBox.className = 'draw-box';
|
||||||
|
this.currentBox.style.left = this.startX + 'px';
|
||||||
|
this.currentBox.style.top = this.startY + 'px';
|
||||||
|
|
||||||
|
const label = document.createElement('div');
|
||||||
|
label.className = 'box-label';
|
||||||
|
label.textContent = '#' + (this.boxes.length + 1);
|
||||||
|
this.currentBox.appendChild(label);
|
||||||
|
|
||||||
|
this.wrapper.appendChild(this.currentBox);
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('mousemove', (e) => {
|
||||||
|
if (!this.isDrawing || !this.currentBox) return;
|
||||||
|
const rect = this.wrapper.getBoundingClientRect();
|
||||||
|
const currentX = Math.max(0, Math.min(e.clientX - rect.left, rect.width));
|
||||||
|
const currentY = Math.max(0, Math.min(e.clientY - rect.top, rect.height));
|
||||||
|
|
||||||
|
const width = currentX - this.startX;
|
||||||
|
const height = currentY - this.startY;
|
||||||
|
|
||||||
|
this.currentBox.style.width = Math.abs(width) + 'px';
|
||||||
|
this.currentBox.style.height = Math.abs(height) + 'px';
|
||||||
|
this.currentBox.style.left = (width > 0 ? this.startX : currentX) + 'px';
|
||||||
|
this.currentBox.style.top = (height > 0 ? this.startY : currentY) + 'px';
|
||||||
|
});
|
||||||
|
|
||||||
|
window.addEventListener('mouseup', () => {
|
||||||
|
if (!this.isDrawing || !this.currentBox) return;
|
||||||
|
this.isDrawing = false;
|
||||||
|
|
||||||
|
const width = parseFloat(this.currentBox.style.width);
|
||||||
|
const height = parseFloat(this.currentBox.style.height);
|
||||||
|
|
||||||
|
if (width > 3 && height > 3) {
|
||||||
|
const rect = this.wrapper.getBoundingClientRect();
|
||||||
|
const boxData = {
|
||||||
|
x: (parseFloat(this.currentBox.style.left) / rect.width * 100).toFixed(2),
|
||||||
|
y: (parseFloat(this.currentBox.style.top) / rect.height * 100).toFixed(2),
|
||||||
|
w: (width / rect.width * 100).toFixed(2),
|
||||||
|
h: (height / rect.height * 100).toFixed(2)
|
||||||
|
};
|
||||||
|
this.boxes.push(boxData);
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
|
||||||
|
this.currentBox.remove();
|
||||||
|
this.currentBox = null;
|
||||||
|
});
|
||||||
|
|
||||||
|
(window as any).removeBox = (index: number) => {
|
||||||
|
this.boxes.splice(index, 1);
|
||||||
|
this.render();
|
||||||
|
};
|
||||||
|
|
||||||
|
(window as any).clearAll = () => {
|
||||||
|
if(confirm('모든 박스를 삭제할까요?')) {
|
||||||
|
this.boxes = [];
|
||||||
|
this.render();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
(window as any).saveToServer = () => this.saveToServer();
|
||||||
|
}
|
||||||
|
|
||||||
|
private async saveToServer() {
|
||||||
|
if (!this.currentPath) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
this.saveBtn.disabled = true;
|
||||||
|
this.saveBtn.textContent = '저장 중...';
|
||||||
|
|
||||||
|
const res = await fetch(`http://${location.hostname}:3000/api/maps/save`, {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify({ path: this.currentPath, boxes: this.boxes })
|
||||||
|
});
|
||||||
|
|
||||||
|
if (res.ok) {
|
||||||
|
this.allMapConfig[this.currentPath] = [...this.boxes];
|
||||||
|
this.statusEl.textContent = '✅ 서버 저장 완료 (' + new Date().toLocaleTimeString() + ')';
|
||||||
|
setTimeout(() => this.statusEl.textContent = '', 3000);
|
||||||
|
} else {
|
||||||
|
alert('저장 실패!');
|
||||||
|
}
|
||||||
|
} catch (err) {
|
||||||
|
alert('서버 연결 오류!');
|
||||||
|
} finally {
|
||||||
|
this.saveBtn.disabled = false;
|
||||||
|
this.saveBtn.textContent = '서버에 즉시 저장';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private render() {
|
||||||
|
this.boxListEl.innerHTML = '';
|
||||||
|
const oldBoxes = this.wrapper.querySelectorAll('.placed-box');
|
||||||
|
oldBoxes.forEach(b => b.remove());
|
||||||
|
|
||||||
|
this.boxes.forEach((box, i) => {
|
||||||
|
const div = document.createElement('div');
|
||||||
|
div.className = 'placed-box';
|
||||||
|
div.style.left = box.x + '%';
|
||||||
|
div.style.top = box.y + '%';
|
||||||
|
div.style.width = box.w + '%';
|
||||||
|
div.style.height = box.h + '%';
|
||||||
|
|
||||||
|
const label = document.createElement('div');
|
||||||
|
label.className = 'box-label';
|
||||||
|
label.textContent = '#' + (i + 1);
|
||||||
|
div.appendChild(label);
|
||||||
|
|
||||||
|
this.wrapper.appendChild(div);
|
||||||
|
|
||||||
|
const item = document.createElement('div');
|
||||||
|
item.className = 'box-item';
|
||||||
|
item.innerHTML = `
|
||||||
|
<span>#${i+1}: [${box.x}, ${box.y}]</span>
|
||||||
|
<button class="btn-del" onclick="removeBox(${i})">×</button>
|
||||||
|
`;
|
||||||
|
this.boxListEl.appendChild(item);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user