223 lines
7.5 KiB
TypeScript
223 lines
7.5 KiB
TypeScript
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();
|
||
};
|
||
|
||
document.getElementById('btn-clear-all')?.addEventListener('click', () => {
|
||
if(confirm('모든 박스를 삭제할까요?')) {
|
||
this.boxes = [];
|
||
this.render();
|
||
}
|
||
});
|
||
|
||
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(`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);
|
||
});
|
||
}
|
||
}
|