feat: split seatmap admin and readonly flows
This commit is contained in:
@@ -986,7 +986,13 @@ def build_center_chair_viewer_html(layout: dict[str, object]) -> str:
|
||||
for slot in layout.get("slots", [])
|
||||
if slot.get("id") is not None and slot.get("slot_key") is not None
|
||||
}
|
||||
members_by_id = {
|
||||
int(member["id"]): member
|
||||
for member in layout.get("members", [])
|
||||
if member.get("id") is not None
|
||||
}
|
||||
placed_keys: list[str] = []
|
||||
assignment_items: list[dict[str, object]] = []
|
||||
for placement in layout.get("placements", []):
|
||||
slot_id = placement.get("seat_slot_id")
|
||||
if slot_id is None:
|
||||
@@ -994,9 +1000,20 @@ def build_center_chair_viewer_html(layout: dict[str, object]) -> str:
|
||||
slot_key = slot_key_by_id.get(int(slot_id))
|
||||
if slot_key:
|
||||
placed_keys.append(slot_key)
|
||||
member = members_by_id.get(int(placement.get("member_id") or 0))
|
||||
if member:
|
||||
assignment_items.append(
|
||||
{
|
||||
"key": slot_key,
|
||||
"member_id": int(member["id"]),
|
||||
"name": str(member.get("name") or "-"),
|
||||
"rank": str(member.get("rank") or "-"),
|
||||
}
|
||||
)
|
||||
|
||||
seat_map = layout.get("seat_map") or {}
|
||||
placed_literal = json.dumps(sorted(set(placed_keys)), ensure_ascii=False, separators=(",", ":"))
|
||||
assignments_literal = json.dumps(assignment_items, ensure_ascii=False, separators=(",", ":"))
|
||||
if seat_map.get("source_type") == "fixed_html":
|
||||
html = parse_fixed_office_template()["html"]
|
||||
else:
|
||||
@@ -1080,6 +1097,7 @@ def build_center_chair_viewer_html(layout: dict[str, object]) -> str:
|
||||
)
|
||||
bridge_script = """
|
||||
<style>
|
||||
#fit-btn { display: none !important; }
|
||||
#clear-btn { display: none !important; }
|
||||
.seat-popup {
|
||||
position: absolute;
|
||||
@@ -1126,12 +1144,12 @@ def build_center_chair_viewer_html(layout: dict[str, object]) -> str:
|
||||
}
|
||||
|
||||
function setViewerMode(mode) {
|
||||
viewerMode = mode === "compact" ? "compact" : "default";
|
||||
viewerMode = mode === "compact" || mode === "readonly" ? mode : "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") {
|
||||
if (actions) actions.style.display = viewerMode !== "default" ? "none" : "";
|
||||
if (viewerMode !== "default") {
|
||||
hideSeatPopup();
|
||||
selectedChairKey = null;
|
||||
}
|
||||
@@ -1147,7 +1165,7 @@ def build_center_chair_viewer_html(layout: dict[str, object]) -> str:
|
||||
<strong>${assignment.name}</strong>
|
||||
<div>직급: ${assignment.rank || "-"}</div>
|
||||
<div>상태: 배치완료</div>
|
||||
<button type="button" data-seatmap-delete="${chairKey}">자리 비우기</button>
|
||||
${viewerMode === "default" ? `<button type="button" data-seatmap-delete="${chairKey}">자리 비우기</button>` : ""}
|
||||
`;
|
||||
popup.style.left = `${x + 18}px`;
|
||||
popup.style.top = `${y + 18}px`;
|
||||
@@ -1219,6 +1237,44 @@ def build_center_chair_viewer_html(layout: dict[str, object]) -> str:
|
||||
tooltip.classList.add("visible");
|
||||
};
|
||||
|
||||
const originalDraw = draw;
|
||||
draw = function drawWithAssignments() {
|
||||
originalDraw();
|
||||
if (!seatAssignments.size) return;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
ctx.setTransform(pixelRatio, 0, 0, pixelRatio, 0, 0);
|
||||
ctx.textBaseline = "middle";
|
||||
for (const chair of chairGeometry) {
|
||||
const assignment = getAssignment(chair.key);
|
||||
if (!assignment) continue;
|
||||
const center = worldToScreen((chair.minX + chair.maxX) / 2, (chair.minY + chair.maxY) / 2);
|
||||
if (center.x < -120 || center.x > rect.width + 120 || center.y < -50 || center.y > rect.height + 50) continue;
|
||||
const primary = `${assignment.name}`;
|
||||
const secondary = `${assignment.rank || "-"}`;
|
||||
ctx.font = "700 12px Pretendard, sans-serif";
|
||||
const primaryWidth = ctx.measureText(primary).width;
|
||||
ctx.font = "600 10px Pretendard, sans-serif";
|
||||
const secondaryWidth = ctx.measureText(secondary).width;
|
||||
const boxWidth = Math.max(primaryWidth, secondaryWidth) + 20;
|
||||
const boxHeight = 34;
|
||||
const boxX = center.x - boxWidth / 2;
|
||||
const boxY = center.y - 46;
|
||||
ctx.fillStyle = "rgba(255,255,255,0.96)";
|
||||
ctx.strokeStyle = "rgba(220,38,38,0.18)";
|
||||
ctx.lineWidth = 1;
|
||||
ctx.beginPath();
|
||||
ctx.roundRect(boxX, boxY, boxWidth, boxHeight, 10);
|
||||
ctx.fill();
|
||||
ctx.stroke();
|
||||
ctx.fillStyle = "#111827";
|
||||
ctx.font = "700 12px Pretendard, sans-serif";
|
||||
ctx.fillText(primary, boxX + 10, boxY + 12);
|
||||
ctx.fillStyle = "#6b7280";
|
||||
ctx.font = "600 10px Pretendard, sans-serif";
|
||||
ctx.fillText(secondary, boxX + 10, boxY + 25);
|
||||
}
|
||||
};
|
||||
|
||||
window.__mhSeatmap = {
|
||||
getCanvas() { return document.getElementById("canvas"); },
|
||||
pickChairAt(x, y) { return typeof pickChair === "function" ? pickChair(x, y) : null; },
|
||||
@@ -1232,6 +1288,8 @@ def build_center_chair_viewer_html(layout: dict[str, object]) -> str:
|
||||
setViewerMode,
|
||||
};
|
||||
|
||||
setAssignments(__INITIAL_ASSIGNMENTS__);
|
||||
|
||||
canvas.addEventListener("click", (event) => {
|
||||
if (viewerMode === "compact") return;
|
||||
const rect = canvas.getBoundingClientRect();
|
||||
@@ -1283,6 +1341,7 @@ def build_center_chair_viewer_html(layout: dict[str, object]) -> str:
|
||||
});
|
||||
</script>
|
||||
"""
|
||||
bridge_script = bridge_script.replace("__INITIAL_ASSIGNMENTS__", assignments_literal, 1)
|
||||
html = html.replace("</body>", f"{bridge_script}\n</body>", 1)
|
||||
return html
|
||||
|
||||
@@ -1381,21 +1440,114 @@ def get_member_count() -> int:
|
||||
return int(cur.fetchone()["count"])
|
||||
|
||||
|
||||
def merge_import_member(item: MemberPayload, existing: dict[str, object] | None) -> MemberPayload:
|
||||
if existing is None:
|
||||
return item
|
||||
|
||||
payload = item.model_copy(deep=True)
|
||||
if not payload.photo_url.strip():
|
||||
payload.photo_url = str(existing.get("photo_url") or "")
|
||||
if not payload.seat_label.strip():
|
||||
payload.seat_label = str(existing.get("seat_label") or "")
|
||||
return payload
|
||||
|
||||
|
||||
def pick_existing_member(
|
||||
item: MemberPayload,
|
||||
existing_by_employee_id: dict[str, list[dict[str, object]]],
|
||||
existing_by_name: dict[str, list[dict[str, object]]],
|
||||
matched_ids: set[int],
|
||||
) -> dict[str, object] | None:
|
||||
employee_id = item.employee_id.strip()
|
||||
if employee_id:
|
||||
for candidate in existing_by_employee_id.get(employee_id, []):
|
||||
candidate_id = int(candidate["id"])
|
||||
if candidate_id not in matched_ids:
|
||||
return candidate
|
||||
|
||||
name = item.name.strip()
|
||||
if name:
|
||||
available = [
|
||||
candidate
|
||||
for candidate in existing_by_name.get(name, [])
|
||||
if int(candidate["id"]) not in matched_ids
|
||||
]
|
||||
if len(available) == 1:
|
||||
return available[0]
|
||||
|
||||
return None
|
||||
|
||||
|
||||
def replace_members(items: list[MemberPayload]) -> list[dict[str, object]]:
|
||||
with get_conn() as conn:
|
||||
with conn.cursor() as cur:
|
||||
cur.execute("TRUNCATE TABLE members RESTART IDENTITY CASCADE")
|
||||
cur.execute(
|
||||
"""
|
||||
SELECT id, name, employee_id, company, rank, role, department, grp, division, team, cell,
|
||||
work_status, work_time, phone, email, seat_label, photo_url,
|
||||
sort_order, created_at, updated_at
|
||||
FROM members
|
||||
ORDER BY id ASC
|
||||
"""
|
||||
)
|
||||
existing_members = cur.fetchall()
|
||||
|
||||
existing_by_employee_id: dict[str, list[dict[str, object]]] = {}
|
||||
existing_by_name: dict[str, list[dict[str, object]]] = {}
|
||||
for member in existing_members:
|
||||
employee_id = str(member.get("employee_id") or "").strip()
|
||||
name = str(member.get("name") or "").strip()
|
||||
if employee_id:
|
||||
existing_by_employee_id.setdefault(employee_id, []).append(member)
|
||||
if name:
|
||||
existing_by_name.setdefault(name, []).append(member)
|
||||
|
||||
matched_ids: set[int] = set()
|
||||
for index, item in enumerate(items):
|
||||
existing = pick_existing_member(item, existing_by_employee_id, existing_by_name, matched_ids)
|
||||
merged_item = merge_import_member(item, existing)
|
||||
if existing is None:
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO members (
|
||||
name, employee_id, company, rank, role, department, grp, division, team, cell,
|
||||
work_status, work_time, phone, email, seat_label, photo_url, sort_order
|
||||
)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
""",
|
||||
serialize_member_payload(merged_item, index),
|
||||
)
|
||||
continue
|
||||
|
||||
matched_ids.add(int(existing["id"]))
|
||||
cur.execute(
|
||||
"""
|
||||
INSERT INTO members (
|
||||
name, employee_id, company, rank, role, department, grp, division, team, cell,
|
||||
work_status, work_time, phone, email, seat_label, photo_url, sort_order
|
||||
)
|
||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s)
|
||||
UPDATE members
|
||||
SET name = %s,
|
||||
employee_id = %s,
|
||||
company = %s,
|
||||
rank = %s,
|
||||
role = %s,
|
||||
department = %s,
|
||||
grp = %s,
|
||||
division = %s,
|
||||
team = %s,
|
||||
cell = %s,
|
||||
work_status = %s,
|
||||
work_time = %s,
|
||||
phone = %s,
|
||||
email = %s,
|
||||
seat_label = %s,
|
||||
photo_url = %s,
|
||||
sort_order = %s,
|
||||
updated_at = NOW()
|
||||
WHERE id = %s
|
||||
""",
|
||||
serialize_member_payload(item, index),
|
||||
(*serialize_member_payload(merged_item, index), int(existing["id"])),
|
||||
)
|
||||
stale_ids = [int(member["id"]) for member in existing_members if int(member["id"]) not in matched_ids]
|
||||
if stale_ids:
|
||||
cur.execute("DELETE FROM members WHERE id = ANY(%s)", (stale_ids,))
|
||||
conn.commit()
|
||||
return fetch_members()
|
||||
|
||||
|
||||
Reference in New Issue
Block a user