backup: save fixed office seatmap snapshot

This commit is contained in:
hyunho
2026-03-26 09:42:25 +09:00
parent e62a6a5458
commit 6f5e61ca1a
15 changed files with 2042 additions and 224 deletions

View File

@@ -31,6 +31,7 @@ const seatMapFormGap = document.getElementById("seatmap-form-gap");
const seatMapFormImage = document.getElementById("seatmap-form-image");
const seatMapSearch = document.getElementById("seatmap-search");
const seatMapUnassigned = document.getElementById("seatmap-unassigned");
const APP_BASE_URL = String(window.__MH_BASE_URL || "").replace(/\/$/, "");
const viewLabels = {
ledger: "사업관리대장",
@@ -60,6 +61,16 @@ const seatMapState = {
panStartY: 0,
panScrollLeft: 0,
panScrollTop: 0,
hoveredSlotId: null,
viewerOffsetX: 0,
viewerOffsetY: 0,
viewerPointerX: 0,
viewerPointerY: 0,
viewerDragging: false,
viewerDragStartX: 0,
viewerDragStartY: 0,
viewerDragOffsetX: 0,
viewerDragOffsetY: 0,
};
let currentView = "organization";
@@ -101,6 +112,14 @@ function escapeHtml(value) {
.replaceAll("'", "'");
}
function resolveAppUrl(path) {
if (!path) return path;
if (/^https?:\/\//i.test(path)) return path;
if (!APP_BASE_URL) return path;
if (path.startsWith("/")) return `${APP_BASE_URL}${path}`;
return path;
}
function clonePlacements(items) {
return items.map((item) => ({
member_id: Number(item.member_id),
@@ -149,7 +168,7 @@ function resetSeatMapDraft() {
}
function clampSeatMapZoom(nextZoom) {
return Math.min(3, Math.max(0.5, Number(nextZoom.toFixed(2))));
return Math.min(4, Math.max(0.35, Number(nextZoom.toFixed(2))));
}
function setSeatMapZoom(nextZoom) {
@@ -157,6 +176,261 @@ function setSeatMapZoom(nextZoom) {
renderSeatMap();
}
function getDxfCanvasSize() {
return {
width: Math.max(960, seatMapBoardWrap?.clientWidth || seatMapBoard?.clientWidth || 960),
height: Math.max(680, seatMapBoardWrap?.clientHeight || seatMapBoard?.clientHeight || 680),
};
}
function centerSeatMapBoard() {
fitDxfSeatMapBoard();
}
function fitDxfSeatMapBoard() {
const viewerData = seatMapState.seatMap?.viewer_data;
if (!viewerData) return;
const world = viewerData.meta?.world;
const canvas = seatMapBoard?.querySelector("#seatmap-dxf-canvas");
if (!world || !canvas) return;
const rect = canvas.getBoundingClientRect();
const pad = 36;
const scaleX = (rect.width - pad * 2) / Math.max(Number(world.width || 1), 1);
const scaleY = (rect.height - pad * 2) / Math.max(Number(world.height || 1), 1);
seatMapState.zoom = clampSeatMapZoom(Math.min(scaleX, scaleY));
seatMapState.viewerOffsetX =
pad - Number(world.min_x) * seatMapState.zoom + (rect.width - pad * 2 - Number(world.width) * seatMapState.zoom) / 2;
seatMapState.viewerOffsetY =
pad - Number(world.min_y) * seatMapState.zoom + (rect.height - pad * 2 - Number(world.height) * seatMapState.zoom) / 2;
drawDxfCanvasViewer();
}
function zoomDxfSeatMapAtPoint(clientX, clientY, factor) {
const viewerData = seatMapState.seatMap?.viewer_data;
const world = viewerData?.meta?.world;
const canvas = seatMapBoard?.querySelector("#seatmap-dxf-canvas");
if (!viewerData || !world || !canvas) return;
const rect = canvas.getBoundingClientRect();
const mx = clientX - rect.left;
const my = clientY - rect.top;
const before = screenToWorld(mx, my, world);
const nextZoom = clampSeatMapZoom(seatMapState.zoom * factor);
if (nextZoom === seatMapState.zoom) return;
seatMapState.zoom = nextZoom;
const after = worldToScreen(before.x, before.y, world);
seatMapState.viewerOffsetX += mx - after.x;
seatMapState.viewerOffsetY += my - after.y;
drawDxfCanvasViewer();
}
function getHoveredSeatMapSlotMeta() {
if (seatMapState.hoveredSlotId == null) return null;
const slot = getSeatSlotMap().get(Number(seatMapState.hoveredSlotId));
if (!slot) return null;
const placement = getSlotPlacementMap().get(Number(slot.id));
const member = placement ? getMemberMap().get(Number(placement.member_id)) : null;
return {
label: slot.label || `SLOT-${slot.id}`,
memberName: member?.name || "",
};
}
function updateSeatMapViewerHoverChip() {
const chip = seatMapBoard?.querySelector("[data-seatmap-chip='hover']");
if (!chip) return;
const hoveredMeta = getHoveredSeatMapSlotMeta();
chip.textContent = hoveredMeta
? `hover ${hoveredMeta.label}${hoveredMeta.memberName ? ` · ${hoveredMeta.memberName}` : ""}`
: "hover none";
}
function worldToScreen(x, y, world) {
return {
x: x * seatMapState.zoom + seatMapState.viewerOffsetX,
y: (Number(world.max_y) - y + Number(world.min_y)) * seatMapState.zoom + seatMapState.viewerOffsetY,
};
}
function screenToWorld(x, y, world) {
return {
x: (x - seatMapState.viewerOffsetX) / seatMapState.zoom,
y: Number(world.max_y) + Number(world.min_y) - (y - seatMapState.viewerOffsetY) / seatMapState.zoom,
};
}
function pickViewerChair(screenX, screenY, viewerData) {
const world = viewerData.meta.world;
const threshold = 12;
let best = null;
for (const chair of viewerData.chairs || []) {
const min = worldToScreen(Number(chair.min_x), Number(chair.max_y), world);
const max = worldToScreen(Number(chair.max_x), Number(chair.min_y), world);
const left = Math.min(min.x, max.x) - threshold;
const right = Math.max(min.x, max.x) + threshold;
const top = Math.min(min.y, max.y) - threshold;
const bottom = Math.max(min.y, max.y) + threshold;
if (screenX < left || screenX > right || screenY < top || screenY > bottom) continue;
let dist = Infinity;
for (let index = Number(chair.start); index < Number(chair.start) + Number(chair.count); index += 1) {
const segment = viewerData.chair_segments[index];
if (!segment) continue;
const a = worldToScreen(Number(segment[0]), Number(segment[1]), world);
const b = worldToScreen(Number(segment[2]), Number(segment[3]), world);
const dx = b.x - a.x;
const dy = b.y - a.y;
const len2 = dx * dx + dy * dy;
let segDist;
if (len2 === 0) {
segDist = Math.hypot(screenX - a.x, screenY - a.y);
} else {
let t = ((screenX - a.x) * dx + (screenY - a.y) * dy) / len2;
t = Math.max(0, Math.min(1, t));
const px = a.x + t * dx;
const py = a.y + t * dy;
segDist = Math.hypot(screenX - px, screenY - py);
}
if (segDist < dist) dist = segDist;
if (dist <= threshold) break;
}
if (dist > threshold) continue;
if (!best || dist < best.dist) {
best = { chair, dist };
}
}
return best?.chair || null;
}
function drawViewerSegments(ctx, viewerData) {
const world = viewerData.meta.world;
const bgSegments = viewerData.background_segments || [];
const chairSegments = viewerData.chair_segments || [];
const placementMap = getSlotPlacementMap();
const slotByKey = new Map((seatMapState.slots || []).map((slot) => [String(slot.slot_key), slot]));
ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.fillStyle = "#ffffff";
ctx.fillRect(0, 0, ctx.canvas.width, ctx.canvas.height);
ctx.save();
ctx.strokeStyle = "rgba(21,35,48,0.10)";
ctx.lineWidth = 1;
for (let index = 0; index < bgSegments.length; index += 1) {
const segment = bgSegments[index];
const a = worldToScreen(Number(segment[0]), Number(segment[1]), world);
const b = worldToScreen(Number(segment[2]), Number(segment[3]), world);
ctx.beginPath();
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
ctx.stroke();
}
ctx.restore();
for (const chair of viewerData.chairs || []) {
const slot = slotByKey.get(String(chair.key));
const occupied = slot ? placementMap.has(Number(slot.id)) : false;
const hovered = slot && Number(slot.id) === seatMapState.hoveredSlotId;
ctx.save();
ctx.strokeStyle = occupied
? "rgba(220, 38, 38, 0.98)"
: hovered
? "rgba(15, 118, 110, 0.98)"
: "rgba(15, 118, 110, 0.82)";
ctx.lineWidth = occupied ? 2.8 : hovered ? 2.2 : 1.5;
ctx.lineCap = "round";
ctx.lineJoin = "round";
ctx.beginPath();
for (let index = Number(chair.start); index < Number(chair.start) + Number(chair.count); index += 1) {
const segment = chairSegments[index];
if (!segment) continue;
const a = worldToScreen(Number(segment[0]), Number(segment[1]), world);
const b = worldToScreen(Number(segment[2]), Number(segment[3]), world);
ctx.moveTo(a.x, a.y);
ctx.lineTo(b.x, b.y);
}
ctx.stroke();
ctx.restore();
}
}
function drawDxfCanvasViewer() {
const canvas = seatMapBoard?.querySelector("#seatmap-dxf-canvas");
const viewerData = seatMapState.seatMap?.viewer_data;
if (!canvas || !viewerData) return;
const ctx = canvas.getContext("2d");
const pixelRatio = window.devicePixelRatio || 1;
const rect = canvas.getBoundingClientRect();
canvas.width = Math.round(rect.width * pixelRatio);
canvas.height = Math.round(rect.height * pixelRatio);
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
drawViewerSegments(ctx, viewerData);
updateSeatMapViewerHoverChip();
}
function setupDxfCanvasViewer() {
const canvas = seatMapBoard?.querySelector("#seatmap-dxf-canvas");
const viewerData = seatMapState.seatMap?.viewer_data;
if (!canvas || !viewerData) return;
canvas.addEventListener("pointerdown", (event) => {
seatMapState.viewerDragging = true;
seatMapState.viewerDragStartX = event.clientX;
seatMapState.viewerDragStartY = event.clientY;
seatMapState.viewerDragOffsetX = seatMapState.viewerOffsetX;
seatMapState.viewerDragOffsetY = seatMapState.viewerOffsetY;
canvas.classList.add("dragging");
});
canvas.addEventListener("pointermove", (event) => {
const rect = canvas.getBoundingClientRect();
seatMapState.viewerPointerX = event.clientX - rect.left;
seatMapState.viewerPointerY = event.clientY - rect.top;
if (seatMapState.viewerDragging) {
seatMapState.viewerOffsetX = seatMapState.viewerDragOffsetX + (event.clientX - seatMapState.viewerDragStartX);
seatMapState.viewerOffsetY = seatMapState.viewerDragOffsetY + (event.clientY - seatMapState.viewerDragStartY);
drawDxfCanvasViewer();
return;
}
const chair = pickViewerChair(seatMapState.viewerPointerX, seatMapState.viewerPointerY, viewerData);
const slot = chair ? (seatMapState.slots || []).find((item) => String(item.slot_key) === String(chair.key)) : null;
const nextSlotId = slot ? Number(slot.id) : null;
if (nextSlotId !== seatMapState.hoveredSlotId) {
seatMapState.hoveredSlotId = nextSlotId;
drawDxfCanvasViewer();
}
});
canvas.addEventListener("pointerleave", () => {
seatMapState.hoveredSlotId = null;
drawDxfCanvasViewer();
});
canvas.addEventListener("pointerup", () => {
if (!seatMapState.viewerDragging) return;
seatMapState.viewerDragging = false;
canvas.classList.remove("dragging");
});
canvas.addEventListener("click", (event) => {
const rect = canvas.getBoundingClientRect();
const chair = pickViewerChair(event.clientX - rect.left, event.clientY - rect.top, viewerData);
const slot = chair ? (seatMapState.slots || []).find((item) => String(item.slot_key) === String(chair.key)) : null;
seatMapState.hoveredSlotId = slot ? Number(slot.id) : null;
drawDxfCanvasViewer();
});
canvas.addEventListener("wheel", (event) => {
event.preventDefault();
zoomDxfSeatMapAtPoint(event.clientX, event.clientY, event.deltaY < 0 ? 1.08 : 0.92);
}, { passive: false });
fitDxfSeatMapBoard();
}
function getSeatSlotMap() {
return new Map((seatMapState.slots || []).map((slot) => [Number(slot.id), slot]));
}
@@ -278,7 +552,7 @@ function renderUnassignedMemberCard(member, draggable) {
function renderSeatMapBoard() {
if (!seatMapBoard || !seatMapState.seatMap) return;
if (seatMapState.seatMap.source_type === "dxf") {
if (seatMapState.seatMap.source_type === "dxf" || seatMapState.seatMap.source_type === "fixed_html") {
renderDxfSeatMapBoard();
return;
}
@@ -315,43 +589,77 @@ function renderSeatMapBoard() {
function renderDxfSeatMapBoard() {
if (!seatMapBoard || !seatMapState.seatMap) return;
const memberMap = getMemberMap();
const placementMap = getSlotPlacementMap();
const slots = Array.isArray(seatMapState.slots) ? seatMapState.slots : [];
const editable = seatMapState.editMode && isAdmin();
const minX = Number(seatMapState.seatMap.view_box_min_x || 0);
const minY = Number(seatMapState.seatMap.view_box_min_y || 0);
const width = Number(seatMapState.seatMap.view_box_width || 1);
const height = Number(seatMapState.seatMap.view_box_height || 1);
const previewSvg = seatMapState.seatMap.preview_svg || "";
const slotHtml = slots
.map((slot) => {
const slotId = Number(slot.id);
const placement = placementMap.get(slotId);
const member = placement ? memberMap.get(Number(placement.member_id)) : null;
if (!member && !editable) {
return "";
}
const left = ((Number(slot.x) - minX) / width) * 100;
const top = (1 - (Number(slot.y) - minY) / height) * 100;
return `
<div class="seatmap-slot${placement ? " occupied" : ""}${editable ? " editable" : ""}${!member ? " empty" : ""}" data-slot-id="${slotId}" style="left:${left}%; top:${top}%;">
${member ? renderMemberCard(member, editable) : ""}
</div>
`;
})
.join("");
const viewerData = seatMapState.seatMap.viewer_data;
if (!viewerData) {
seatMapBoard.innerHTML = `<div class="seatmap-empty-card"><strong>DXF 뷰어 데이터를 준비하지 못했습니다.</strong></div>`;
return;
}
const viewerUrl = resolveAppUrl(`/api/seat-maps/${seatMapState.seatMap.id}/viewer`);
seatMapBoard.innerHTML = `
<div class="seatmap-dxf-canvas">
<div class="seatmap-dxf-stage" style="transform: scale(${seatMapState.zoom}); --seatmap-zoom:${seatMapState.zoom};">
<div class="seatmap-dxf-preview">${previewSvg}</div>
<div class="seatmap-dxf-slots">${slotHtml}</div>
</div>
<div class="seatmap-dxf-frame-shell">
<iframe
id="seatmap-dxf-frame"
class="seatmap-dxf-frame"
src="${escapeHtml(viewerUrl)}"
title="${escapeHtml(seatMapState.seatMap.name || "DXF Viewer")}"
loading="eager"
referrerpolicy="same-origin"
></iframe>
</div>
`;
setupSeatMapViewerFrame();
}
function getDraftPlacedSlotKeys() {
const slotMap = getSeatSlotMap();
return (seatMapState.draftPlacements || [])
.map((placement) => slotMap.get(Number(placement.seat_slot_id))?.slot_key)
.filter(Boolean)
.map((value) => String(value));
}
function syncSeatMapViewerFrame() {
const frame = seatMapBoard?.querySelector("#seatmap-dxf-frame");
if (!frame?.contentWindow) return;
frame.contentWindow.postMessage(
{ type: "seatmap-set-placed", keys: getDraftPlacedSlotKeys() },
window.location.origin,
);
}
function setupSeatMapViewerFrame() {
const frame = seatMapBoard?.querySelector("#seatmap-dxf-frame");
if (!frame) return;
frame.addEventListener("load", () => {
syncSeatMapViewerFrame();
if (!seatMapState.editMode) return;
const frameWindow = frame.contentWindow;
const frameDocument = frame.contentDocument;
const canvas = frameDocument?.getElementById("canvas");
if (!frameWindow || !frameDocument || !canvas || !frameWindow.__mhSeatmap) return;
canvas.addEventListener("dragover", (event) => {
event.preventDefault();
event.dataTransfer.dropEffect = "move";
});
canvas.addEventListener("drop", (event) => {
event.preventDefault();
const memberId = getDraggedMemberId(event);
if (!memberId) return;
const rect = canvas.getBoundingClientRect();
const picked = frameWindow.__mhSeatmap.pickChairAt(
event.clientX - rect.left,
event.clientY - rect.top,
);
if (!picked?.key) return;
const matchedSlot = (seatMapState.slots || []).find((item) => String(item.slot_key) === String(picked.key));
if (!matchedSlot) return;
upsertDraftPlacementForSlot(memberId, Number(matchedSlot.id));
renderSeatMap();
});
}, { once: true });
}
function renderUnassignedMembers() {
@@ -413,6 +721,7 @@ function syncSeatMapSettingsForm() {
function renderSeatMap() {
const hasSeatMap = Boolean(seatMapState.seatMap);
const admin = isAdmin();
const fixedViewerMap = seatMapState.seatMap?.source_type === "fixed_html";
if (seatMapName) {
seatMapName.textContent = hasSeatMap ? seatMapState.seatMap.name : "자리배치도";
@@ -422,7 +731,7 @@ function renderSeatMap() {
seatMapStatus.dataset.tone = seatMapState.statusTone;
}
if (seatMapSettingsPanel) {
seatMapSettingsPanel.classList.toggle("hidden", !admin);
seatMapSettingsPanel.classList.toggle("hidden", !admin || fixedViewerMap);
}
if (seatMapSaveBtn) {
seatMapSaveBtn.hidden = !admin || !hasSeatMap;
@@ -461,7 +770,7 @@ function handleEmbeddedNavigationMessage(event) {
}
async function fetchJson(url, options) {
const response = await fetch(url, options);
const response = await fetch(resolveAppUrl(url), options);
let payload = null;
try {
payload = await response.json();
@@ -487,11 +796,15 @@ async function loadSeatMapData(force = false) {
const activePayload = await fetchJson("/api/seat-maps/active");
const activeSeatMap = activePayload.item;
const layoutPayload = await fetchJson(`/api/seat-maps/${activeSeatMap.id}/layout`);
seatMapState.seatMap = layoutPayload.seat_map;
seatMapState.seatMap = {
...(layoutPayload.seat_map || {}),
viewer_data: layoutPayload.viewer_data || null,
};
seatMapState.members = Array.isArray(layoutPayload.members) ? layoutPayload.members : [];
seatMapState.slots = Array.isArray(layoutPayload.slots) ? layoutPayload.slots : [];
seatMapState.placements = clonePlacements(layoutPayload.placements || []);
seatMapState.zoom = 1;
seatMapState.hoveredSlotId = null;
seatMapState.editMode = isAdmin();
resetSeatMapDraft();
seatMapState.loaded = true;
@@ -504,6 +817,7 @@ async function loadSeatMapData(force = false) {
seatMapState.slots = [];
seatMapState.placements = [];
seatMapState.zoom = 1;
seatMapState.hoveredSlotId = null;
seatMapState.editMode = isAdmin();
resetSeatMapDraft();
seatMapState.loaded = true;
@@ -615,8 +929,20 @@ function handleSeatMapCellDrop(event) {
if (!memberId) return;
if (seatMapState.seatMap?.source_type === "dxf") {
const slot = event.target.closest(".seatmap-slot");
if (!slot) return;
upsertDraftPlacementForSlot(memberId, Number(slot.dataset.slotId));
if (slot) {
upsertDraftPlacementForSlot(memberId, Number(slot.dataset.slotId));
renderSeatMap();
return;
}
const viewerData = seatMapState.seatMap.viewer_data;
const canvas = seatMapBoard?.querySelector("#seatmap-dxf-canvas");
if (!viewerData || !canvas) return;
const rect = canvas.getBoundingClientRect();
const chair = pickViewerChair(event.clientX - rect.left, event.clientY - rect.top, viewerData);
if (!chair) return;
const matchedSlot = (seatMapState.slots || []).find((item) => String(item.slot_key) === String(chair.key));
if (!matchedSlot) return;
upsertDraftPlacementForSlot(memberId, Number(matchedSlot.id));
} else {
const cell = event.target.closest(".seatmap-cell");
if (!cell) return;
@@ -665,7 +991,7 @@ function setActiveView(view) {
if (isOrganization && previousView !== "organization" && organizationFrame) {
const frameSrc = organizationFrame.dataset.src || organizationFrame.src;
organizationFrame.src = frameSrc;
organizationFrame.src = resolveAppUrl(frameSrc);
}
if (isSeatMap) {
loadSeatMapData();
@@ -784,13 +1110,17 @@ if (seatMapBoard) {
seatMapBoard.addEventListener("wheel", (event) => {
if (seatMapState.seatMap?.source_type !== "dxf") return;
event.preventDefault();
const delta = event.deltaY < 0 ? 0.1 : -0.1;
setSeatMapZoom(seatMapState.zoom + delta);
zoomDxfSeatMapAtPoint(event.clientX, event.clientY, event.deltaY < 0 ? 1.08 : 0.92);
}, { passive: false });
seatMapBoard.addEventListener("click", (event) => {
const fitButton = event.target.closest("[data-seatmap-action='fit']");
if (!fitButton) return;
fitDxfSeatMapBoard();
});
seatMapBoard.addEventListener("dragover", (event) => {
if (!seatMapState.editMode) return;
const target = seatMapState.seatMap?.source_type === "dxf"
? event.target.closest(".seatmap-slot")
? (event.target.closest(".seatmap-slot") || event.target.closest("#seatmap-dxf-canvas"))
: event.target.closest(".seatmap-cell");
if (!target) return;
event.preventDefault();
@@ -802,7 +1132,9 @@ if (seatMapBoard) {
if (seatMapBoardWrap) {
seatMapBoardWrap.addEventListener("mousedown", (event) => {
if (seatMapState.seatMap?.source_type !== "dxf") return;
if (event.button !== 1) return;
if (seatMapBoard?.querySelector("#seatmap-dxf-canvas")) return;
if (event.button !== 0) return;
if (event.target.closest(".seatmap-member-card, button, input, label")) return;
event.preventDefault();
seatMapState.panning = true;
seatMapState.panStartX = event.clientX;
@@ -811,6 +1143,21 @@ if (seatMapBoardWrap) {
seatMapState.panScrollTop = seatMapBoardWrap.scrollTop;
seatMapBoardWrap.classList.add("is-panning");
});
seatMapBoardWrap.addEventListener("mouseleave", () => {
if (seatMapBoard?.querySelector("#seatmap-dxf-canvas")) return;
if (seatMapState.hoveredSlotId == null) return;
seatMapState.hoveredSlotId = null;
updateSeatMapViewerHoverChip();
});
seatMapBoardWrap.addEventListener("mousemove", (event) => {
if (seatMapState.seatMap?.source_type !== "dxf") return;
if (seatMapBoard?.querySelector("#seatmap-dxf-canvas")) return;
const slot = event.target.closest(".seatmap-slot");
const nextSlotId = slot ? Number(slot.dataset.slotId) : null;
if (nextSlotId === seatMapState.hoveredSlotId) return;
seatMapState.hoveredSlotId = nextSlotId;
updateSeatMapViewerHoverChip();
});
}
document.addEventListener("mousemove", (event) => {
@@ -858,3 +1205,12 @@ window.addEventListener("message", handleEmbeddedNavigationMessage);
setActiveView(currentView);
renderAuth();
window.addEventListener("resize", () => {
if (seatMapState.seatMap?.source_type !== "dxf" || currentView !== "seatmap") return;
requestAnimationFrame(() => {
if (seatMapState.zoom === 1) {
centerSeatMapBoard();
}
});
});

View File

@@ -94,13 +94,13 @@
<aside class="seatmap-sidebar">
<section id="seatmap-settings-panel" class="seatmap-panel hidden">
<div class="seatmap-panel-head">
<h4>배치도 설정</h4>
<p>DXF 파일의 chair 레이어를 좌석 위치로 사용합니다.</p>
<h4> 설정</h4>
<p>현재는 기술개발센터 고정 도면을 사용합니다.</p>
</div>
<form id="seatmap-settings-form" class="seatmap-form">
<label>
<span>배치도 이름</span>
<input id="seatmap-form-name" name="name" type="text" placeholder="예: 본사 3층" required>
<span> 이름</span>
<input id="seatmap-form-name" name="name" type="text" placeholder="예: 기술개발센터" required>
</label>
<div>
<span>DXF 파일</span>

View File

@@ -477,9 +477,10 @@
height: 100%;
overflow: auto;
border-radius: 24px;
background: #fff;
background: #ffffff;
padding: 0;
overscroll-behavior: contain;
cursor: grab;
}
.seatmap-board-wrap.is-panning {
@@ -493,45 +494,117 @@
.seatmap-dxf-canvas {
position: relative;
width: 100%;
min-width: 100%;
min-height: 100%;
margin: 0 auto;
padding: 72px 24px 24px;
border-radius: 24px;
overflow: hidden;
overflow: visible;
box-shadow: none;
background: #fff;
background: #ffffff;
}
.seatmap-dxf-frame-shell {
width: 100%;
height: 100%;
min-height: 720px;
background: #ffffff;
}
.seatmap-dxf-frame {
display: block;
width: 100%;
height: 100%;
min-height: 720px;
border: 0;
background: #ffffff;
}
.seatmap-dxf-viewer-head {
position: sticky;
top: 16px;
z-index: 6;
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
margin: 0 0 18px;
pointer-events: none;
}
.seatmap-viewer-chip,
.seatmap-viewer-fit {
pointer-events: auto;
display: inline-flex;
align-items: center;
min-height: 40px;
padding: 10px 14px;
border-radius: 16px;
border: 1px solid rgba(21, 35, 48, 0.1);
background: rgba(255, 255, 255, 0.96);
color: #627286;
font-size: 13px;
font-weight: 800;
box-shadow: 0 8px 24px rgba(21, 35, 48, 0.08);
}
.seatmap-viewer-fit {
border: none;
color: #ffffff;
background: linear-gradient(135deg, #0f766e, #115e59);
box-shadow: 0 10px 24px rgba(15, 118, 110, 0.22);
}
.seatmap-dxf-canvas-surface {
display: block;
width: 100%;
height: 680px;
border-radius: 24px;
background: #ffffff;
border: 1px solid rgba(21, 35, 48, 0.08);
box-shadow: 0 22px 48px rgba(21, 35, 48, 0.06);
cursor: grab;
}
.seatmap-dxf-canvas-surface.dragging {
cursor: grabbing;
}
.seatmap-dxf-stage {
position: relative;
transform-origin: center center;
transition: transform 0.12s ease-out;
transform-origin: top left;
transition: width 0.12s ease-out, height 0.12s ease-out;
margin: 0 auto;
}
.seatmap-dxf-preview {
position: relative;
z-index: 1;
line-height: 0;
filter: contrast(1.9) saturate(1.1) brightness(0.88);
filter: none;
border-radius: 24px;
overflow: hidden;
border: 1px solid rgba(21, 35, 48, 0.08);
background: #ffffff;
box-shadow: 0 22px 48px rgba(21, 35, 48, 0.06);
}
.seatmap-preview-svg {
display: block;
width: 100%;
height: auto;
height: 100%;
background: #fff;
}
.seatmap-preview-svg .seatmap-dxf-entity {
stroke: #000 !important;
stroke: rgba(21, 35, 48, 0.16) !important;
stroke-opacity: 1 !important;
stroke-width: 12 !important;
stroke-width: 4 !important;
}
.seatmap-preview-svg .seatmap-dxf-chair-entity {
stroke: #2563eb !important;
stroke: rgba(15, 118, 110, 0.96) !important;
stroke-opacity: 1 !important;
stroke-width: 6 !important;
stroke-width: 5.5 !important;
}
.seatmap-preview-svg rect {
@@ -547,19 +620,19 @@
.seatmap-slot {
position: absolute;
transform: translate(-50%, -50%);
width: 30px;
min-height: 30px;
width: 28px;
min-height: 28px;
border: 0;
border-radius: 999px;
background: transparent;
pointer-events: auto;
transition: box-shadow 0.18s ease, background 0.18s ease, transform 0.18s ease;
transition: box-shadow 0.18s ease, background 0.18s ease, transform 0.18s ease, opacity 0.18s ease;
}
.seatmap-slot.editable:hover {
box-shadow: 0 0 0 4px rgba(37, 99, 235, 0.4);
background: rgba(37, 99, 235, 0.12);
transform: translate(-50%, -50%) scale(1.02);
box-shadow: 0 0 0 6px rgba(15, 118, 110, 0.18);
background: rgba(15, 118, 110, 0.08);
transform: translate(-50%, -50%) scale(1.06);
}
.seatmap-slot.occupied {
@@ -569,7 +642,31 @@
}
.seatmap-slot.empty {
opacity: 0.14;
opacity: 0.72;
}
.seatmap-slot[data-slot-id]::after {
content: "";
position: absolute;
inset: 50% auto auto 50%;
width: 10px;
height: 10px;
transform: translate(-50%, -50%);
border-radius: 999px;
background: rgba(15, 118, 110, 0.28);
border: 1px solid rgba(15, 118, 110, 0.55);
}
.seatmap-slot.occupied::after {
width: 14px;
height: 14px;
background: rgba(220, 38, 38, 0.3);
border-color: rgba(220, 38, 38, 0.72);
}
.seatmap-slot:hover::after {
width: 16px;
height: 16px;
}
.seatmap-canvas {
@@ -968,6 +1065,19 @@
align-items: flex-start;
flex-direction: column;
}
.seatmap-dxf-canvas {
padding: 68px 16px 16px;
}
.seatmap-dxf-canvas-surface {
height: 620px;
}
.seatmap-dxf-frame-shell,
.seatmap-dxf-frame {
min-height: 620px;
}
}
@media (max-width: 720px) {