feat: refine seat assignment flow and profile seat preview

This commit is contained in:
hyunho
2026-03-26 10:03:13 +09:00
parent 6f5e61ca1a
commit 8efb5da65f
4 changed files with 415 additions and 19 deletions

View File

@@ -1020,13 +1020,205 @@ def build_center_chair_viewer_html(layout: dict[str, object]) -> str:
f"const STORAGE_KEY = null;\n const placed = new Set({placed_literal});",
1,
)
html = html.replace(
""" ctx.strokeStyle = selected
? "rgba(220, 38, 38, 0.98)"
: active
? "rgba(15, 118, 110, 0.98)"
: chair.kind === "group"
? "rgba(16, 134, 149, 0.74)"
: "rgba(21, 149, 142, 0.8)";
ctx.lineWidth = (selected ? 2.6 : active ? 2.1 : baseWidth) / camera.scale;""",
""" ctx.strokeStyle = selected
? "rgba(220, 38, 38, 0.98)"
: "rgba(15, 118, 110, 0.88)";
ctx.lineWidth = (selected ? 2.6 : active ? 2.0 : 1.6) / camera.scale;""",
1,
)
html = html.replace(
"function persistPlaced() {\n localStorage.setItem(STORAGE_KEY, JSON.stringify([...placed]));\n }",
"function persistPlaced() {\n return;\n }",
1,
)
html = html.replace(
""" window.addEventListener("pointerup", (event) => {
if (dragging && dragStart) {
const move = Math.hypot(event.clientX - dragStart.x, event.clientY - dragStart.y);
if (move < 4) {
const rect = canvas.getBoundingClientRect();
const picked = pickChair(event.clientX - rect.left, event.clientY - rect.top);
if (picked) {
if (placed.has(picked.key)) placed.delete(picked.key);
else placed.add(picked.key);
persistPlaced();
}
}
}
dragging = false;
dragStart = null;
canvas.classList.remove("dragging");
requestDraw();
});""",
""" window.addEventListener("pointerup", () => {
dragging = false;
dragStart = null;
canvas.classList.remove("dragging");
requestDraw();
});""",
1,
)
html = html.replace(
""" document.getElementById("clear-btn").addEventListener("click", () => {
placed.clear();
persistPlaced();
requestDraw();
});""",
""" document.getElementById("clear-btn").addEventListener("click", () => {
requestDraw();
});""",
1,
)
bridge_script = """
<style>
#clear-btn { display: none !important; }
.seat-popup {
position: absolute;
min-width: 190px;
padding: 12px 14px;
border-radius: 16px;
background: rgba(17,24,39,0.96);
color: white;
box-shadow: 0 18px 36px rgba(15,23,42,0.22);
z-index: 4;
}
.seat-popup[hidden] { display: none; }
.seat-popup strong { display: block; margin-bottom: 6px; font-size: 14px; }
.seat-popup div { font-size: 12px; line-height: 1.45; color: rgba(255,255,255,0.82); }
.seat-popup button {
margin-top: 10px;
width: 100%;
border: none;
border-radius: 12px;
padding: 8px 10px;
font: inherit;
font-weight: 700;
cursor: pointer;
color: white;
background: rgba(220, 38, 38, 0.98);
}
</style>
<script>
const seatAssignments = new Map();
let selectedChairKey = null;
let viewerMode = "default";
const popup = document.createElement("div");
popup.className = "seat-popup";
popup.hidden = true;
document.querySelector(".viewer").appendChild(popup);
function getAssignment(key) {
return seatAssignments.get(String(key)) || null;
}
function hideSeatPopup() {
popup.hidden = true;
popup.innerHTML = "";
}
function setViewerMode(mode) {
viewerMode = mode === "compact" ? "compact" : "default";
const head = document.querySelector(".viewer-head");
const actions = document.querySelector(".viewer-actions");
if (head) head.style.display = viewerMode === "compact" ? "none" : "";
if (actions) actions.style.display = viewerMode === "compact" ? "none" : "";
if (viewerMode === "compact") {
hideSeatPopup();
selectedChairKey = null;
}
}
function showSeatPopup(chairKey, x, y) {
const assignment = getAssignment(chairKey);
if (!assignment) {
hideSeatPopup();
return;
}
popup.innerHTML = `
<strong>${assignment.name}</strong>
<div>직급: ${assignment.rank || "-"}</div>
<div>상태: 배치완료</div>
<button type="button" data-seatmap-delete="${chairKey}">자리 비우기</button>
`;
popup.style.left = `${x + 18}px`;
popup.style.top = `${y + 18}px`;
popup.hidden = false;
}
function focusChair(chairKey, padding = 2200) {
const chair = chairGeometry.find((item) => String(item.key) === String(chairKey));
if (!chair) return;
const rect = canvas.getBoundingClientRect();
const pad = 24;
const minX = chair.minX - padding;
const maxX = chair.maxX + padding;
const minY = chair.minY - padding;
const maxY = chair.maxY + padding;
const width = Math.max(1, maxX - minX);
const height = Math.max(1, maxY - minY);
camera.scale = Math.max(0.002, Math.min(2, Math.min((rect.width - pad * 2) / width, (rect.height - pad * 2) / height)));
camera.offsetX = pad - minX * camera.scale + (rect.width - pad * 2 - width * camera.scale) / 2;
camera.offsetY = pad - (world.maxY - maxY + world.minY) * camera.scale + (rect.height - pad * 2 - height * camera.scale) / 2;
requestDraw();
}
function setAssignments(items) {
seatAssignments.clear();
placed.clear();
(items || []).forEach((item) => {
const key = String(item.key || "");
if (!key) return;
const assignment = {
key,
name: item.name || "-",
rank: item.rank || "-",
memberId: Number(item.member_id || 0),
};
seatAssignments.set(key, assignment);
placed.add(key);
});
if (selectedChairKey && !seatAssignments.has(selectedChairKey)) {
selectedChairKey = null;
hideSeatPopup();
}
if (typeof requestDraw === "function") requestDraw();
}
renderTooltip = function renderTooltipOverride() {
if (!hovered) {
tooltip.classList.remove("visible");
hoverChip.textContent = "chair hover: none";
return;
}
const assignment = getAssignment(hovered.key);
hoverChip.textContent = assignment
? `chair hover: ${assignment.name}`
: "chair hover: 공석";
tooltip.innerHTML = assignment
? `
<strong>${assignment.name}</strong>
<div>직급: ${assignment.rank || "-"}</div>
<div>상태: 배치완료</div>
`
: `
<strong>공석</strong>
<div>좌석: ${hovered.key}</div>
<div>상태: 미배치</div>
`;
tooltip.style.left = `${pointer.x + 14}px`;
tooltip.style.top = `${pointer.y + 14}px`;
tooltip.classList.add("visible");
};
window.__mhSeatmap = {
getCanvas() { return document.getElementById("canvas"); },
pickChairAt(x, y) { return typeof pickChair === "function" ? pickChair(x, y) : null; },
@@ -1034,14 +1226,60 @@ def build_center_chair_viewer_html(layout: dict[str, object]) -> str:
placed.clear();
(keys || []).forEach((key) => placed.add(String(key)));
if (typeof requestDraw === "function") requestDraw();
}
},
setAssignments,
focusChair,
setViewerMode,
};
canvas.addEventListener("click", (event) => {
if (viewerMode === "compact") return;
const rect = canvas.getBoundingClientRect();
const picked = window.__mhSeatmap.pickChairAt(
event.clientX - rect.left,
event.clientY - rect.top,
);
if (!picked) {
selectedChairKey = null;
hideSeatPopup();
if (typeof requestDraw === "function") requestDraw();
return;
}
selectedChairKey = seatAssignments.has(String(picked.key)) ? String(picked.key) : null;
if (selectedChairKey) showSeatPopup(selectedChairKey, event.clientX - rect.left, event.clientY - rect.top);
else hideSeatPopup();
if (typeof requestDraw === "function") requestDraw();
});
popup.addEventListener("click", (event) => {
const button = event.target.closest("[data-seatmap-delete]");
if (!button) return;
const slotKey = String(button.dataset.seatmapDelete || "");
if (!slotKey) return;
selectedChairKey = null;
hideSeatPopup();
window.parent.postMessage({ type: "seatmap-clear-slot", key: slotKey }, window.location.origin);
});
canvas.addEventListener("contextmenu", (event) => {
event.preventDefault();
});
window.addEventListener("message", (event) => {
const data = event.data;
if (!data || typeof data !== "object") return;
if (data.type === "seatmap-set-placed") {
window.__mhSeatmap.setPlaced(Array.isArray(data.keys) ? data.keys : []);
}
if (data.type === "seatmap-set-assignments") {
window.__mhSeatmap.setAssignments(Array.isArray(data.items) ? data.items : []);
}
if (data.type === "seatmap-focus-chair") {
window.__mhSeatmap.focusChair(String(data.key || ""), Number(data.padding || 2200));
}
if (data.type === "seatmap-set-mode") {
window.__mhSeatmap.setViewerMode(String(data.mode || "default"));
}
});
</script>
"""