feat: refine seat assignment flow and profile seat preview
This commit is contained in:
@@ -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});",
|
f"const STORAGE_KEY = null;\n const placed = new Set({placed_literal});",
|
||||||
1,
|
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(
|
html = html.replace(
|
||||||
"function persistPlaced() {\n localStorage.setItem(STORAGE_KEY, JSON.stringify([...placed]));\n }",
|
"function persistPlaced() {\n localStorage.setItem(STORAGE_KEY, JSON.stringify([...placed]));\n }",
|
||||||
"function persistPlaced() {\n return;\n }",
|
"function persistPlaced() {\n return;\n }",
|
||||||
1,
|
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 = """
|
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>
|
<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 = {
|
window.__mhSeatmap = {
|
||||||
getCanvas() { return document.getElementById("canvas"); },
|
getCanvas() { return document.getElementById("canvas"); },
|
||||||
pickChairAt(x, y) { return typeof pickChair === "function" ? pickChair(x, y) : null; },
|
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();
|
placed.clear();
|
||||||
(keys || []).forEach((key) => placed.add(String(key)));
|
(keys || []).forEach((key) => placed.add(String(key)));
|
||||||
if (typeof requestDraw === "function") requestDraw();
|
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) => {
|
window.addEventListener("message", (event) => {
|
||||||
const data = event.data;
|
const data = event.data;
|
||||||
if (!data || typeof data !== "object") return;
|
if (!data || typeof data !== "object") return;
|
||||||
if (data.type === "seatmap-set-placed") {
|
if (data.type === "seatmap-set-placed") {
|
||||||
window.__mhSeatmap.setPlaced(Array.isArray(data.keys) ? data.keys : []);
|
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>
|
</script>
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -103,6 +103,10 @@ function isAdmin() {
|
|||||||
return getSession()?.user?.role === "admin";
|
return getSession()?.user?.role === "admin";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function isSlotBasedSeatMap() {
|
||||||
|
return seatMapState.seatMap?.source_type === "dxf" || seatMapState.seatMap?.source_type === "fixed_html";
|
||||||
|
}
|
||||||
|
|
||||||
function escapeHtml(value) {
|
function escapeHtml(value) {
|
||||||
return String(value ?? "")
|
return String(value ?? "")
|
||||||
.replaceAll("&", "&")
|
.replaceAll("&", "&")
|
||||||
@@ -618,6 +622,24 @@ function getDraftPlacedSlotKeys() {
|
|||||||
.map((value) => String(value));
|
.map((value) => String(value));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getSeatAssignmentPayload() {
|
||||||
|
const slotMap = getSeatSlotMap();
|
||||||
|
const memberMap = getMemberMap();
|
||||||
|
return getPlacementSource()
|
||||||
|
.map((placement) => {
|
||||||
|
const slot = slotMap.get(Number(placement.seat_slot_id));
|
||||||
|
const member = memberMap.get(Number(placement.member_id));
|
||||||
|
if (!slot || !member) return null;
|
||||||
|
return {
|
||||||
|
key: String(slot.slot_key),
|
||||||
|
member_id: Number(member.id),
|
||||||
|
name: member.name || "-",
|
||||||
|
rank: member.rank || "-",
|
||||||
|
};
|
||||||
|
})
|
||||||
|
.filter(Boolean);
|
||||||
|
}
|
||||||
|
|
||||||
function syncSeatMapViewerFrame() {
|
function syncSeatMapViewerFrame() {
|
||||||
const frame = seatMapBoard?.querySelector("#seatmap-dxf-frame");
|
const frame = seatMapBoard?.querySelector("#seatmap-dxf-frame");
|
||||||
if (!frame?.contentWindow) return;
|
if (!frame?.contentWindow) return;
|
||||||
@@ -625,6 +647,22 @@ function syncSeatMapViewerFrame() {
|
|||||||
{ type: "seatmap-set-placed", keys: getDraftPlacedSlotKeys() },
|
{ type: "seatmap-set-placed", keys: getDraftPlacedSlotKeys() },
|
||||||
window.location.origin,
|
window.location.origin,
|
||||||
);
|
);
|
||||||
|
frame.contentWindow.postMessage(
|
||||||
|
{ type: "seatmap-set-assignments", items: getSeatAssignmentPayload() },
|
||||||
|
window.location.origin,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSeatMapDraftUi() {
|
||||||
|
if (seatMapSaveBtn) {
|
||||||
|
seatMapSaveBtn.hidden = !isAdmin() || !seatMapState.seatMap;
|
||||||
|
seatMapSaveBtn.disabled = !seatMapState.dirty;
|
||||||
|
}
|
||||||
|
if (seatMapCancelBtn) {
|
||||||
|
seatMapCancelBtn.hidden = !seatMapState.seatMap;
|
||||||
|
}
|
||||||
|
renderUnassignedMembers();
|
||||||
|
syncSeatMapViewerFrame();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setupSeatMapViewerFrame() {
|
function setupSeatMapViewerFrame() {
|
||||||
@@ -657,7 +695,7 @@ function setupSeatMapViewerFrame() {
|
|||||||
const matchedSlot = (seatMapState.slots || []).find((item) => String(item.slot_key) === String(picked.key));
|
const matchedSlot = (seatMapState.slots || []).find((item) => String(item.slot_key) === String(picked.key));
|
||||||
if (!matchedSlot) return;
|
if (!matchedSlot) return;
|
||||||
upsertDraftPlacementForSlot(memberId, Number(matchedSlot.id));
|
upsertDraftPlacementForSlot(memberId, Number(matchedSlot.id));
|
||||||
renderSeatMap();
|
updateSeatMapDraftUi();
|
||||||
});
|
});
|
||||||
}, { once: true });
|
}, { once: true });
|
||||||
}
|
}
|
||||||
@@ -767,6 +805,13 @@ function handleEmbeddedNavigationMessage(event) {
|
|||||||
hideUserPopover();
|
hideUserPopover();
|
||||||
setActiveView("organization");
|
setActiveView("organization");
|
||||||
}
|
}
|
||||||
|
if (data.type === "seatmap-clear-slot" && isAdmin()) {
|
||||||
|
const cleared = clearDraftPlacementBySlotKey(String(data.key || ""));
|
||||||
|
if (cleared) {
|
||||||
|
setSeatMapStatus("구성원을 공석으로 이동했습니다. 저장 버튼으로 반영하세요.", "info");
|
||||||
|
updateSeatMapDraftUi();
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
async function fetchJson(url, options) {
|
async function fetchJson(url, options) {
|
||||||
@@ -848,6 +893,15 @@ async function getImageDimensions(file) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function clearDraftPlacementBySlotKey(slotKey) {
|
||||||
|
const matchedSlot = (seatMapState.slots || []).find((item) => String(item.slot_key) === String(slotKey));
|
||||||
|
if (!matchedSlot) return false;
|
||||||
|
const placement = (seatMapState.draftPlacements || []).find((item) => Number(item.seat_slot_id) === Number(matchedSlot.id));
|
||||||
|
if (!placement) return false;
|
||||||
|
removeDraftPlacement(Number(placement.member_id));
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
async function uploadSeatMapImage(file, name) {
|
async function uploadSeatMapImage(file, name) {
|
||||||
const formData = new FormData();
|
const formData = new FormData();
|
||||||
formData.append("file", file);
|
formData.append("file", file);
|
||||||
@@ -927,7 +981,7 @@ function handleSeatMapCellDrop(event) {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
const memberId = getDraggedMemberId(event);
|
const memberId = getDraggedMemberId(event);
|
||||||
if (!memberId) return;
|
if (!memberId) return;
|
||||||
if (seatMapState.seatMap?.source_type === "dxf") {
|
if (isSlotBasedSeatMap()) {
|
||||||
const slot = event.target.closest(".seatmap-slot");
|
const slot = event.target.closest(".seatmap-slot");
|
||||||
if (slot) {
|
if (slot) {
|
||||||
upsertDraftPlacementForSlot(memberId, Number(slot.dataset.slotId));
|
upsertDraftPlacementForSlot(memberId, Number(slot.dataset.slotId));
|
||||||
@@ -957,7 +1011,7 @@ function handleSeatMapListDrop(event) {
|
|||||||
const memberId = getDraggedMemberId(event);
|
const memberId = getDraggedMemberId(event);
|
||||||
if (!memberId) return;
|
if (!memberId) return;
|
||||||
removeDraftPlacement(memberId);
|
removeDraftPlacement(memberId);
|
||||||
renderSeatMap();
|
updateSeatMapDraftUi();
|
||||||
}
|
}
|
||||||
|
|
||||||
function setActiveView(view) {
|
function setActiveView(view) {
|
||||||
@@ -1108,7 +1162,7 @@ if (seatMapFormImage) {
|
|||||||
|
|
||||||
if (seatMapBoard) {
|
if (seatMapBoard) {
|
||||||
seatMapBoard.addEventListener("wheel", (event) => {
|
seatMapBoard.addEventListener("wheel", (event) => {
|
||||||
if (seatMapState.seatMap?.source_type !== "dxf") return;
|
if (!isSlotBasedSeatMap()) return;
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
zoomDxfSeatMapAtPoint(event.clientX, event.clientY, event.deltaY < 0 ? 1.08 : 0.92);
|
zoomDxfSeatMapAtPoint(event.clientX, event.clientY, event.deltaY < 0 ? 1.08 : 0.92);
|
||||||
}, { passive: false });
|
}, { passive: false });
|
||||||
@@ -1119,7 +1173,7 @@ if (seatMapBoard) {
|
|||||||
});
|
});
|
||||||
seatMapBoard.addEventListener("dragover", (event) => {
|
seatMapBoard.addEventListener("dragover", (event) => {
|
||||||
if (!seatMapState.editMode) return;
|
if (!seatMapState.editMode) return;
|
||||||
const target = seatMapState.seatMap?.source_type === "dxf"
|
const target = isSlotBasedSeatMap()
|
||||||
? (event.target.closest(".seatmap-slot") || event.target.closest("#seatmap-dxf-canvas"))
|
? (event.target.closest(".seatmap-slot") || event.target.closest("#seatmap-dxf-canvas"))
|
||||||
: event.target.closest(".seatmap-cell");
|
: event.target.closest(".seatmap-cell");
|
||||||
if (!target) return;
|
if (!target) return;
|
||||||
@@ -1131,7 +1185,7 @@ if (seatMapBoard) {
|
|||||||
|
|
||||||
if (seatMapBoardWrap) {
|
if (seatMapBoardWrap) {
|
||||||
seatMapBoardWrap.addEventListener("mousedown", (event) => {
|
seatMapBoardWrap.addEventListener("mousedown", (event) => {
|
||||||
if (seatMapState.seatMap?.source_type !== "dxf") return;
|
if (!isSlotBasedSeatMap()) return;
|
||||||
if (seatMapBoard?.querySelector("#seatmap-dxf-canvas")) return;
|
if (seatMapBoard?.querySelector("#seatmap-dxf-canvas")) return;
|
||||||
if (event.button !== 0) return;
|
if (event.button !== 0) return;
|
||||||
if (event.target.closest(".seatmap-member-card, button, input, label")) return;
|
if (event.target.closest(".seatmap-member-card, button, input, label")) return;
|
||||||
@@ -1150,7 +1204,7 @@ if (seatMapBoardWrap) {
|
|||||||
updateSeatMapViewerHoverChip();
|
updateSeatMapViewerHoverChip();
|
||||||
});
|
});
|
||||||
seatMapBoardWrap.addEventListener("mousemove", (event) => {
|
seatMapBoardWrap.addEventListener("mousemove", (event) => {
|
||||||
if (seatMapState.seatMap?.source_type !== "dxf") return;
|
if (!isSlotBasedSeatMap()) return;
|
||||||
if (seatMapBoard?.querySelector("#seatmap-dxf-canvas")) return;
|
if (seatMapBoard?.querySelector("#seatmap-dxf-canvas")) return;
|
||||||
const slot = event.target.closest(".seatmap-slot");
|
const slot = event.target.closest(".seatmap-slot");
|
||||||
const nextSlotId = slot ? Number(slot.dataset.slotId) : null;
|
const nextSlotId = slot ? Number(slot.dataset.slotId) : null;
|
||||||
@@ -1207,7 +1261,7 @@ setActiveView(currentView);
|
|||||||
renderAuth();
|
renderAuth();
|
||||||
|
|
||||||
window.addEventListener("resize", () => {
|
window.addEventListener("resize", () => {
|
||||||
if (seatMapState.seatMap?.source_type !== "dxf" || currentView !== "seatmap") return;
|
if (!isSlotBasedSeatMap() || currentView !== "seatmap") return;
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
if (seatMapState.zoom === 1) {
|
if (seatMapState.zoom === 1) {
|
||||||
centerSeatMapBoard();
|
centerSeatMapBoard();
|
||||||
|
|||||||
@@ -541,6 +541,14 @@ body {
|
|||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.seat-preview-frame {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
height: 300px;
|
||||||
|
border: 0;
|
||||||
|
background: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
.seat-preview-placeholder {
|
.seat-preview-placeholder {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
@@ -551,6 +559,7 @@ body {
|
|||||||
font-weight: 900;
|
font-weight: 900;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
.seat-preview-placeholder-icon {
|
.seat-preview-placeholder-icon {
|
||||||
width: 52px;
|
width: 52px;
|
||||||
height: 52px;
|
height: 52px;
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ let collapsedUnits = new Set();
|
|||||||
let isListMode = false;
|
let isListMode = false;
|
||||||
let emptyStateMessage = '서버에 조직 데이터가 없습니다. 상단의 업로드 버튼으로 초기 데이터를 넣어주세요.';
|
let emptyStateMessage = '서버에 조직 데이터가 없습니다. 상단의 업로드 버튼으로 초기 데이터를 넣어주세요.';
|
||||||
let photoPreviewObjectUrl = null;
|
let photoPreviewObjectUrl = null;
|
||||||
|
let seatMapLayoutCache = null;
|
||||||
|
|
||||||
const levelOrder = ['부서', '그룹', '디비전', '팀', '셀'];
|
const levelOrder = ['부서', '그룹', '디비전', '팀', '셀'];
|
||||||
const dropdownFields = ['소속회사', '직급', '직책', ...levelOrder];
|
const dropdownFields = ['소속회사', '직급', '직책', ...levelOrder];
|
||||||
@@ -146,6 +147,44 @@ async function loadMembers(message) {
|
|||||||
render();
|
render();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function loadActiveSeatMapLayout(force = false) {
|
||||||
|
if (seatMapLayoutCache && !force) {
|
||||||
|
return seatMapLayoutCache;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
const activePayload = await apiFetch('/api/seat-maps/active');
|
||||||
|
const seatMap = activePayload?.item;
|
||||||
|
if (!seatMap?.id) {
|
||||||
|
seatMapLayoutCache = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const layoutPayload = await apiFetch(`/api/seat-maps/${seatMap.id}/layout`);
|
||||||
|
seatMapLayoutCache = layoutPayload;
|
||||||
|
return layoutPayload;
|
||||||
|
} catch {
|
||||||
|
seatMapLayoutCache = null;
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function getMemberSeatInfo(layout, memberId) {
|
||||||
|
if (!layout || !memberId) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const placement = (layout.placements || []).find((item) => Number(item.member_id) === Number(memberId));
|
||||||
|
if (!placement) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
const slot = (layout.slots || []).find((item) => Number(item.id) === Number(placement.seat_slot_id));
|
||||||
|
return {
|
||||||
|
seatMapId: layout.seat_map?.id || null,
|
||||||
|
seatMapName: layout.seat_map?.name || '자리배치도',
|
||||||
|
seatLabel: placement.seat_label || slot?.label || '',
|
||||||
|
slotKey: slot?.slot_key || '',
|
||||||
|
assigned: true,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
async function syncMembers(nextMembers) {
|
async function syncMembers(nextMembers) {
|
||||||
const payload = await apiFetch('/api/members/bulk-sync', {
|
const payload = await apiFetch('/api/members/bulk-sync', {
|
||||||
method: 'PUT',
|
method: 'PUT',
|
||||||
@@ -846,31 +885,86 @@ function handlePhotoFileChange(event) {
|
|||||||
updatePhotoPreview(photoPreviewObjectUrl, name);
|
updatePhotoPreview(photoPreviewObjectUrl, name);
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSeatPreviewCard(seatLabel) {
|
function renderSeatPreviewCard(seatInfo) {
|
||||||
const safeLabel = escapeHtml(seatLabel || '');
|
const assigned = Boolean(seatInfo?.assigned);
|
||||||
const badge = safeLabel
|
const safeLabel = escapeHtml(seatInfo?.seatLabel || '');
|
||||||
? `<span class="seat-preview-badge">${safeLabel}</span>`
|
const safeSeatMapName = escapeHtml(seatInfo?.seatMapName || '자리배치도');
|
||||||
|
const safeSlotKey = escapeHtml(seatInfo?.slotKey || '');
|
||||||
|
const badge = assigned
|
||||||
|
? `<span class="seat-preview-badge">${safeLabel || '배치완료'}</span>`
|
||||||
: '<span class="seat-preview-badge seat-preview-badge-muted">미배치</span>';
|
: '<span class="seat-preview-badge seat-preview-badge-muted">미배치</span>';
|
||||||
|
const body = assigned
|
||||||
|
? `
|
||||||
|
<iframe
|
||||||
|
id="member-seat-preview-frame"
|
||||||
|
class="seat-preview-frame"
|
||||||
|
src="/api/seat-maps/${Number(seatInfo.seatMapId || 0)}/viewer"
|
||||||
|
title="${safeSeatMapName} 좌석 미리보기"
|
||||||
|
loading="eager"
|
||||||
|
referrerpolicy="same-origin"
|
||||||
|
></iframe>
|
||||||
|
`
|
||||||
|
: `
|
||||||
|
<div class="seat-preview-placeholder">
|
||||||
|
<span class="seat-preview-placeholder-icon">⌖</span>
|
||||||
|
<span>현재 공석 또는 미배치 상태입니다.</span>
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
|
||||||
return `
|
return `
|
||||||
<div class="seat-preview-card">
|
<div class="seat-preview-card">
|
||||||
<div class="seat-preview-head">
|
<div class="seat-preview-head">
|
||||||
<div>
|
<div>
|
||||||
<strong>재석위치</strong>
|
<strong>재석위치</strong>
|
||||||
<p>향후 해당 인원의 좌석 영역을 크롭해 표시하고, 스크롤 확대/축소를 지원할 예정입니다.</p>
|
<p>${assigned ? '현재 자리배치도 기준으로 배치된 좌석 정보를 표시합니다.' : '현재 자리배치도에서 배치된 좌석이 없습니다.'}</p>
|
||||||
</div>
|
</div>
|
||||||
${badge}
|
${badge}
|
||||||
</div>
|
</div>
|
||||||
<div class="seat-preview-canvas">
|
<div class="seat-preview-canvas">
|
||||||
<div class="seat-preview-placeholder">
|
${body}
|
||||||
<span class="seat-preview-placeholder-icon">⌖</span>
|
|
||||||
<span>좌석 이미지 연동 예정</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function hydrateMemberSeatPreview(member) {
|
||||||
|
const target = document.getElementById('member-seat-preview');
|
||||||
|
if (!target) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
target.innerHTML = renderSeatPreviewCard({
|
||||||
|
assigned: false,
|
||||||
|
seatMapName: '자리배치도',
|
||||||
|
seatLabel: member['자리위치'] || '',
|
||||||
|
slotKey: '',
|
||||||
|
});
|
||||||
|
const layout = await loadActiveSeatMapLayout(true);
|
||||||
|
if (!document.getElementById('member-seat-preview')) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const seatInfo = getMemberSeatInfo(layout, member.id) || {
|
||||||
|
seatMapName: layout?.seat_map?.name || '자리배치도',
|
||||||
|
seatLabel: member['자리위치'] || '',
|
||||||
|
slotKey: '',
|
||||||
|
assigned: Boolean(member['자리위치']),
|
||||||
|
};
|
||||||
|
target.innerHTML = renderSeatPreviewCard(seatInfo);
|
||||||
|
if (!seatInfo.assigned || !seatInfo.seatMapId || !seatInfo.slotKey) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const frame = document.getElementById('member-seat-preview-frame');
|
||||||
|
if (!frame) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
frame.addEventListener('load', () => {
|
||||||
|
if (!frame.contentWindow) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
frame.contentWindow.postMessage({ type: 'seatmap-set-mode', mode: 'compact' }, window.location.origin);
|
||||||
|
frame.contentWindow.postMessage({ type: 'seatmap-focus-chair', key: seatInfo.slotKey, padding: 2600 }, window.location.origin);
|
||||||
|
}, { once: true });
|
||||||
|
}
|
||||||
|
|
||||||
function switchModalTab(tab) {
|
function switchModalTab(tab) {
|
||||||
const isBasic = tab === 'basic';
|
const isBasic = tab === 'basic';
|
||||||
document.getElementById('modal-sec-basic').classList.toggle('hidden', !isBasic);
|
document.getElementById('modal-sec-basic').classList.toggle('hidden', !isBasic);
|
||||||
@@ -919,11 +1013,12 @@ function openModal(id) {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="w-full mt-2">
|
<div class="w-full mt-2">
|
||||||
${renderSeatPreviewCard(member['자리위치'] || '')}
|
<div id="member-seat-preview">${renderSeatPreviewCard({ assigned: false, seatLabel: member['자리위치'] || '', seatMapName: '자리배치도', slotKey: '' })}</div>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
footer.innerHTML = '<button onclick="closeModal()" class="w-full bg-slate-800 text-white py-4 rounded-xl font-bold text-sm shadow-lg">닫기</button>';
|
footer.innerHTML = '<button onclick="closeModal()" class="w-full bg-slate-800 text-white py-4 rounded-xl font-bold text-sm shadow-lg">닫기</button>';
|
||||||
modal.style.display = 'flex';
|
modal.style.display = 'flex';
|
||||||
|
hydrateMemberSeatPreview(member);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user