diff --git a/templates/index.html b/templates/index.html
index 2b6eab6..c78e869 100644
--- a/templates/index.html
+++ b/templates/index.html
@@ -73,7 +73,6 @@ const WORKPLACE_ICON = L.divIcon({
});
let markers = { workers: {}, workplaces: {} };
-let assignmentLines = [];
function init() {
map = L.map('map').setView(YONGSAN, 14);
@@ -110,7 +109,7 @@ function init() {
showToast('근무지 마커 근처에 드롭해주세요.', 'warning');
return;
}
- assignWorker(workerId, nearest.id);
+ doAssignWorker(workerId, nearest.id);
});
document.getElementById('fileInput').addEventListener('change', uploadExcel);
@@ -156,7 +155,6 @@ function render() {
renderWorkplaceMarkers();
renderWorkerMarkers();
renderWorkerList();
- renderAssignmentLines();
updateBadges();
}
@@ -167,9 +165,7 @@ function clearMap() {
for (const key in markers.workplaces) {
map.removeLayer(markers.workplaces[key]);
}
- assignmentLines.forEach(l => map.removeLayer(l));
markers = { workers: {}, workplaces: {} };
- assignmentLines = [];
}
function getWorkplaceAssigneesHtml(wpId) {
@@ -199,11 +195,20 @@ function renderWorkplaceMarkers() {
}
}
+function getWorkerPos(w) {
+ const wpId = store.assignments[w.id];
+ if (wpId) {
+ const wp = store.workplaces.find(p => p.id === wpId);
+ if (wp) return [wp.lat, wp.lng];
+ }
+ return [w.lat, w.lng];
+}
+
function renderWorkerMarkers() {
for (const w of store.workers) {
const isAssigned = store.assignments[w.id];
const icon = isAssigned ? WORKER_ASSIGNED_ICON : WORKER_ICON;
- const marker = L.marker([w.lat, w.lng], { icon, draggable: false })
+ const marker = L.marker(getWorkerPos(w), { icon, draggable: true })
.addTo(map)
.bindPopup(`
`);
+ marker.on('dragend', function () {
+ handleWorkerDragEnd(w.id, this);
+ });
markers.workers[w.id] = marker;
}
}
+function handleWorkerDragEnd(workerId, marker) {
+ const pos = marker.getLatLng();
+ const nearest = findNearestWorkplace(pos.lat, pos.lng);
+
+ if (nearest) {
+ fetch('/assign', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ workerId, workplaceId: nearest.id }),
+ })
+ .then(r => r.json())
+ .then(data => {
+ store.assignments = data.assignments;
+ marker.setLatLng([nearest.lat, nearest.lng]);
+ marker.setIcon(WORKER_ASSIGNED_ICON);
+ marker.setPopupContent(buildWorkerPopup(workerId));
+ updateWorkplacePopup(nearest.id);
+ renderWorkerList();
+ updateBadges();
+ showToast(`${getWorkerName(workerId)} → ${nearest.name} 배치됨`, 'success');
+ })
+ .catch(() => showToast('배치 저장 실패', 'error'));
+ } else if (store.assignments[workerId]) {
+ const oldWpId = store.assignments[workerId];
+ fetch('/unassign', {
+ method: 'POST',
+ headers: { 'Content-Type': 'application/json' },
+ body: JSON.stringify({ workerId }),
+ })
+ .then(r => r.json())
+ .then(data => {
+ store.assignments = data.assignments;
+ const w = store.workers.find(x => x.id === workerId);
+ marker.setLatLng([w.lat, w.lng]);
+ marker.setIcon(WORKER_ICON);
+ marker.setPopupContent(buildWorkerPopup(workerId));
+ updateWorkplacePopup(oldWpId);
+ renderWorkerList();
+ updateBadges();
+ showToast('배치가 취소되었습니다.', 'info');
+ })
+ .catch(() => showToast('배치 취소 실패', 'error'));
+ } else {
+ const w = store.workers.find(x => x.id === workerId);
+ marker.setLatLng([w.lat, w.lng]);
+ }
+}
+
+function buildWorkerPopup(workerId) {
+ const w = store.workers.find(x => x.id === workerId);
+ if (!w) return '';
+ const isAssigned = store.assignments[w.id];
+ const wpName = isAssigned ? getWorkplaceName(isAssigned) : '';
+ return `
+ `;
+}
+
+function getWorkerName(id) {
+ const w = store.workers.find(x => x.id === id);
+ return w ? w.name : '';
+}
+
function renderWorkerList() {
const list = document.getElementById('workerList');
list.innerHTML = '';
@@ -259,17 +337,18 @@ function renderWorkerList() {
}
}
-function renderAssignmentLines() {
- for (const [workerId, workplaceId] of Object.entries(store.assignments)) {
- const wMarker = markers.workers[workerId];
- const wpMarker = markers.workplaces[workplaceId];
- if (!wMarker || !wpMarker) continue;
- const line = L.polyline(
- [wMarker.getLatLng(), wpMarker.getLatLng()],
- { color: '#10b981', weight: 2, dashArray: '6, 4', opacity: 0.6 }
- ).addTo(map);
- assignmentLines.push(line);
- }
+function updateWorkplacePopup(wpId) {
+ const marker = markers.workplaces[wpId];
+ if (!marker) return;
+ const wp = store.workplaces.find(p => p.id === wpId);
+ if (!wp) return;
+ marker.setPopupContent(`
+
+ `);
}
function updateBadges() {
@@ -280,7 +359,7 @@ function updateBadges() {
document.getElementById('statusBadge').className = `status-badge ${assigned === total ? 'complete' : assigned > 0 ? 'partial' : ''}`;
}
-function assignWorker(workerId, workplaceId) {
+function doAssignWorker(workerId, workplaceId) {
if (!store) return;
if (store.assignments[workerId] === workplaceId) return;
@@ -292,13 +371,23 @@ function assignWorker(workerId, workplaceId) {
.then(r => r.json())
.then(data => {
store.assignments = data.assignments;
- render();
+ const w = store.workers.find(x => x.id === workerId);
+ const wp = store.workplaces.find(p => p.id === workplaceId);
+ const marker = markers.workers[workerId];
+ if (marker) {
+ marker.setLatLng([wp.lat, wp.lng]);
+ marker.setIcon(WORKER_ASSIGNED_ICON);
+ marker.setPopupContent(buildWorkerPopup(workerId));
+ }
+ updateWorkplacePopup(workplaceId);
+ renderWorkerList();
+ updateBadges();
showToast('배치가 저장되었습니다.', 'success');
})
.catch(() => showToast('배치 저장 실패', 'error'));
}
-function unassignWorker(workerId) {
+function unassignWorker(workerId, fromPopup) {
if (!store) return;
fetch('/unassign', {
@@ -308,8 +397,19 @@ function unassignWorker(workerId) {
})
.then(r => r.json())
.then(data => {
+ const oldWpId = store.assignments[workerId];
store.assignments = data.assignments;
- render();
+ const w = store.workers.find(x => x.id === workerId);
+ const marker = markers.workers[workerId];
+ if (marker) {
+ marker.setLatLng([w.lat, w.lng]);
+ marker.setIcon(WORKER_ICON);
+ marker.setPopupContent(buildWorkerPopup(workerId));
+ }
+ if (oldWpId) updateWorkplacePopup(oldWpId);
+ renderWorkerList();
+ updateBadges();
+ if (fromPopup) map.closePopup();
showToast('배치가 취소되었습니다.', 'info');
})
.catch(() => showToast('배치 취소 실패', 'error'));
@@ -323,7 +423,19 @@ function resetAssignments() {
.then(r => r.json())
.then(data => {
store.assignments = data.assignments;
- render();
+ for (const w of store.workers) {
+ const marker = markers.workers[w.id];
+ if (marker) {
+ marker.setLatLng([w.lat, w.lng]);
+ marker.setIcon(WORKER_ICON);
+ marker.setPopupContent(buildWorkerPopup(w.id));
+ }
+ }
+ for (const wp of store.workplaces) {
+ updateWorkplacePopup(wp.id);
+ }
+ renderWorkerList();
+ updateBadges();
showToast('모든 배치가 초기화되었습니다.', 'info');
})
.catch(() => showToast('초기화 실패', 'error'));