refactor: complete modal class-based architecture, design system integration, and map editor modularization

This commit is contained in:
2026-06-01 14:57:07 +09:00
parent 590ddd0e85
commit 9cd5d59bf8
32 changed files with 1838 additions and 1670 deletions

View File

@@ -3,118 +3,25 @@
<head>
<meta charset="UTF-8">
<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>
<body>
<!-- Left: File Selector -->
<div class="file-sidebar" id="file-sidebar">
<div class="folder-item">IDC</div>
<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>
<!-- Rendered by MapEditor.ts -->
</div>
<!-- Center: Main Editor -->
<div class="editor-container" id="container">
<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>
<!-- Right: Control Panel -->
<div class="sidebar">
<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>
@@ -122,186 +29,12 @@
<div class="box-list" id="box-list"></div>
<div class="actions">
<button class="btn-secondary" onclick="clearAll()">전체 삭제</button>
<button id="btn-save-server" class="btn-primary" onclick="saveToServer()">서버에 즉시 저장</button>
<button class="btn btn-outline" style="height:38px;" onclick="clearAll()">전체 삭제</button>
<button id="btn-save-server" class="btn btn-primary" style="height:38px;" onclick="saveToServer()">서버에 즉시 저장</button>
<div id="save-status"></div>
</div>
</div>
<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>
<script type="module" src="/src/map-editor-main.ts"></script>
</body>
</html>