Files
ITAM/src/views/MapEditor.ts

368 lines
13 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { IMAGE_LOCATIONS } from '../components/Modal/SharedData';
import { createIcons, X, Save, Trash2, ChevronLeft, ChevronRight } from 'lucide';
import { QRPrinter } from '../core/qr_print';
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 = '';
private assetOptions: {id: string, name: 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();
await this.loadAssets();
this.bindEvents();
this.selectFirstFile();
createIcons({ icons: { X, Save, Trash2, ChevronLeft, ChevronRight } });
}
private async loadAssets() {
try {
const res = await fetch('/api/assets/master');
const masterData = await res.json();
const allHw = [
...(masterData.pc || []),
...(masterData.server || []),
...(masterData.storage || []),
...(masterData.network || []),
...(masterData.equipment || []),
...(masterData.survey || []),
...(masterData.officeSupplies || []),
...(masterData.pcParts || [])
];
this.assetOptions = allHw.map(a => ({
id: a.id,
name: `[${a.asset_code}] ${a.asset_purpose || a.model_name || a.category}`
}));
} catch (err) {
console.error('Failed to load assets for mapping', err);
}
}
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('/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),
asset_id: null
};
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();
};
document.getElementById('btn-clear-all')?.addEventListener('click', () => {
if(confirm('모든 박스를 삭제할까요?')) {
this.boxes = [];
this.render();
}
});
document.getElementById('btn-print-map-qrs')?.addEventListener('click', () => {
if (this.boxes.length === 0) {
alert('인쇄할 구역이 없습니다.');
return;
}
const cleanKey = getCleanMapKey(this.currentPath);
const locName = getLocationName(this.currentPath);
const items = this.boxes.map((box, index) => {
const padIdx = String(index + 1).padStart(3, '0');
const locCode = `LOC-${cleanKey}-${padIdx}`;
const locDetail = getLocationDetail(this.currentPath, index);
return {
type: 'location' as const,
code: locCode,
title: '[ HM LOCATION ]',
subtitle: locName,
dept: locDetail
};
});
QRPrinter.print(items);
});
document.getElementById('btn-save-server')?.addEventListener('click', () => this.saveToServer());
}
private async saveToServer() {
if (!this.currentPath) return;
try {
this.saveBtn.disabled = true;
this.saveBtn.textContent = '저장 중...';
const res = await fetch('/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);
// Create asset options dropdown
let optionsHtml = '<option value="">-- 자산 매핑 안 됨 --</option>';
this.assetOptions.forEach(opt => {
const selected = box.asset_id === opt.id ? 'selected' : '';
optionsHtml += `<option value="${opt.id}" ${selected}>${opt.name}</option>`;
});
const item = document.createElement('div');
item.className = 'box-item';
item.innerHTML = `
<div class="box-header">
<span class="box-index">#${i+1}</span>
<div style="display: flex; gap: 0.5rem; align-items: center;">
<button class="btn btn-outline btn-sm" onclick="printBoxQR(${i})" style="padding: 2px 6px; font-size: 11px; margin: 0; cursor: pointer;">QR</button>
<button class="btn-del" onclick="removeBox(${i})">×</button>
</div>
</div>
<div class="box-inputs margin-bottom">
<select data-index="${i}" data-prop="asset_id">
${optionsHtml}
</select>
</div>
<div class="box-inputs">
<div class="input-group">
<label>X</label>
<input type="number" step="0.01" value="${box.x}" data-index="${i}" data-prop="x">
</div>
<div class="input-group">
<label>Y</label>
<input type="number" step="0.01" value="${box.y}" data-index="${i}" data-prop="y">
</div>
<div class="input-group">
<label>W</label>
<input type="number" step="0.01" value="${box.w}" data-index="${i}" data-prop="w">
</div>
<div class="input-group">
<label>H</label>
<input type="number" step="0.01" value="${box.h}" data-index="${i}" data-prop="h">
</div>
</div>
`;
this.boxListEl.appendChild(item);
});
// Add events to new inputs and selects
this.boxListEl.querySelectorAll('input, select').forEach(input => {
input.addEventListener('change', (e) => {
const target = e.target as HTMLInputElement | HTMLSelectElement;
const index = parseInt(target.dataset.index!);
const prop = target.dataset.prop!;
if (this.boxes[index]) {
if (prop === 'asset_id') {
this.boxes[index][prop] = target.value || null;
} else {
this.boxes[index][prop] = parseFloat(target.value).toFixed(2);
this.render(); // Re-render to update the map visual size
}
}
});
});
(window as any).printBoxQR = (index: number) => {
const box = this.boxes[index];
if (!box) return;
const cleanKey = getCleanMapKey(this.currentPath);
const padIdx = String(index + 1).padStart(3, '0');
const locCode = `LOC-${cleanKey}-${padIdx}`;
const locDetail = getLocationDetail(this.currentPath, index);
const locName = getLocationName(this.currentPath);
QRPrinter.print([{
type: 'location',
code: locCode,
title: '[ HM LOCATION ]',
subtitle: locName,
dept: locDetail
}]);
};
}
}
function getCleanMapKey(path: string) {
let clean = path.replace('img/location_photo/', '').replace('.png', '');
clean = clean.replace('서관', 'W').replace('동관', 'E');
clean = clean.replace('한맥빌딩/MDF실/MDF_', 'HAN-MDF-');
clean = clean.replace('기술개발센터/서버실/서버실_', 'DEV-SVR-');
clean = clean.replace(/\//g, '-');
return clean;
}
function getLocationName(path: string) {
if (path.includes('IDC')) return 'IDC';
if (path.includes('한맥빌딩')) return '한맥빌딩';
if (path.includes('기술개발센터')) return '기술개발센터';
return '기타';
}
function getLocationDetail(path: string, idx: number) {
let clean = path.replace('img/location_photo/', '').replace('.png', '');
let parts = clean.split('/');
let lastPart = parts[parts.length - 1];
return `${lastPart} 구역 자리 #${idx + 1}`;
}