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>
|
||||
<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>
|
||||
|
||||
Reference in New Issue
Block a user