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 = {}; 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 += `
${bldg}
`; Object.entries(details).forEach(([detail, paths]) => { paths.forEach(path => { const fileName = path.split('/').pop() || path; html += `
${fileName}
`; }); }); }); 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 = ` #${i+1}: [${box.x}, ${box.y}] `; this.boxListEl.appendChild(item); }); } }