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", [])
|
for slot in layout.get("slots", [])
|
||||||
if slot.get("id") is not None and slot.get("slot_key") is not None
|
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] = []
|
placed_keys: list[str] = []
|
||||||
|
assignment_items: list[dict[str, object]] = []
|
||||||
for placement in layout.get("placements", []):
|
for placement in layout.get("placements", []):
|
||||||
slot_id = placement.get("seat_slot_id")
|
slot_id = placement.get("seat_slot_id")
|
||||||
if slot_id is None:
|
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))
|
slot_key = slot_key_by_id.get(int(slot_id))
|
||||||
if slot_key:
|
if slot_key:
|
||||||
placed_keys.append(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 {}
|
seat_map = layout.get("seat_map") or {}
|
||||||
placed_literal = json.dumps(sorted(set(placed_keys)), ensure_ascii=False, separators=(",", ":"))
|
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":
|
if seat_map.get("source_type") == "fixed_html":
|
||||||
html = parse_fixed_office_template()["html"]
|
html = parse_fixed_office_template()["html"]
|
||||||
else:
|
else:
|
||||||
@@ -1080,6 +1097,7 @@ def build_center_chair_viewer_html(layout: dict[str, object]) -> str:
|
|||||||
)
|
)
|
||||||
bridge_script = """
|
bridge_script = """
|
||||||
<style>
|
<style>
|
||||||
|
#fit-btn { display: none !important; }
|
||||||
#clear-btn { display: none !important; }
|
#clear-btn { display: none !important; }
|
||||||
.seat-popup {
|
.seat-popup {
|
||||||
position: absolute;
|
position: absolute;
|
||||||
@@ -1126,12 +1144,12 @@ def build_center_chair_viewer_html(layout: dict[str, object]) -> str:
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setViewerMode(mode) {
|
function setViewerMode(mode) {
|
||||||
viewerMode = mode === "compact" ? "compact" : "default";
|
viewerMode = mode === "compact" || mode === "readonly" ? mode : "default";
|
||||||
const head = document.querySelector(".viewer-head");
|
const head = document.querySelector(".viewer-head");
|
||||||
const actions = document.querySelector(".viewer-actions");
|
const actions = document.querySelector(".viewer-actions");
|
||||||
if (head) head.style.display = viewerMode === "compact" ? "none" : "";
|
if (head) head.style.display = viewerMode === "compact" ? "none" : "";
|
||||||
if (actions) actions.style.display = viewerMode === "compact" ? "none" : "";
|
if (actions) actions.style.display = viewerMode !== "default" ? "none" : "";
|
||||||
if (viewerMode === "compact") {
|
if (viewerMode !== "default") {
|
||||||
hideSeatPopup();
|
hideSeatPopup();
|
||||||
selectedChairKey = null;
|
selectedChairKey = null;
|
||||||
}
|
}
|
||||||
@@ -1147,7 +1165,7 @@ def build_center_chair_viewer_html(layout: dict[str, object]) -> str:
|
|||||||
<strong>${assignment.name}</strong>
|
<strong>${assignment.name}</strong>
|
||||||
<div>직급: ${assignment.rank || "-"}</div>
|
<div>직급: ${assignment.rank || "-"}</div>
|
||||||
<div>상태: 배치완료</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.left = `${x + 18}px`;
|
||||||
popup.style.top = `${y + 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");
|
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 = {
|
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; },
|
||||||
@@ -1232,6 +1288,8 @@ def build_center_chair_viewer_html(layout: dict[str, object]) -> str:
|
|||||||
setViewerMode,
|
setViewerMode,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
setAssignments(__INITIAL_ASSIGNMENTS__);
|
||||||
|
|
||||||
canvas.addEventListener("click", (event) => {
|
canvas.addEventListener("click", (event) => {
|
||||||
if (viewerMode === "compact") return;
|
if (viewerMode === "compact") return;
|
||||||
const rect = canvas.getBoundingClientRect();
|
const rect = canvas.getBoundingClientRect();
|
||||||
@@ -1283,6 +1341,7 @@ def build_center_chair_viewer_html(layout: dict[str, object]) -> str:
|
|||||||
});
|
});
|
||||||
</script>
|
</script>
|
||||||
"""
|
"""
|
||||||
|
bridge_script = bridge_script.replace("__INITIAL_ASSIGNMENTS__", assignments_literal, 1)
|
||||||
html = html.replace("</body>", f"{bridge_script}\n</body>", 1)
|
html = html.replace("</body>", f"{bridge_script}\n</body>", 1)
|
||||||
return html
|
return html
|
||||||
|
|
||||||
@@ -1381,21 +1440,114 @@ def get_member_count() -> int:
|
|||||||
return int(cur.fetchone()["count"])
|
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]]:
|
def replace_members(items: list[MemberPayload]) -> list[dict[str, object]]:
|
||||||
with get_conn() as conn:
|
with get_conn() as conn:
|
||||||
with conn.cursor() as cur:
|
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):
|
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(
|
cur.execute(
|
||||||
"""
|
"""
|
||||||
INSERT INTO members (
|
UPDATE members
|
||||||
name, employee_id, company, rank, role, department, grp, division, team, cell,
|
SET name = %s,
|
||||||
work_status, work_time, phone, email, seat_label, photo_url, sort_order
|
employee_id = %s,
|
||||||
)
|
company = %s,
|
||||||
VALUES (%s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %s, %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()
|
conn.commit()
|
||||||
return fetch_members()
|
return fetch_members()
|
||||||
|
|
||||||
|
|||||||
@@ -11,34 +11,100 @@ const currentViewTitle = document.getElementById("current-view-title");
|
|||||||
const navButtons = Array.from(document.querySelectorAll(".header-center [data-view]"));
|
const navButtons = Array.from(document.querySelectorAll(".header-center [data-view]"));
|
||||||
const organizationFrame = document.getElementById("organization-frame");
|
const organizationFrame = document.getElementById("organization-frame");
|
||||||
const organizationStage = document.getElementById("organization-stage");
|
const organizationStage = document.getElementById("organization-stage");
|
||||||
const seatMapStage = document.getElementById("seatmap-stage");
|
const seatMapAdminStage = document.getElementById("seatmap-admin-stage");
|
||||||
|
const seatMapReadonlyStage = document.getElementById("seatmap-readonly-stage");
|
||||||
const emptyStage = document.getElementById("empty-stage");
|
const emptyStage = document.getElementById("empty-stage");
|
||||||
|
|
||||||
const seatMapName = document.getElementById("seatmap-name");
|
const seatMapDom = {
|
||||||
const seatMapStatus = document.getElementById("seatmap-status");
|
admin: {
|
||||||
const seatMapSaveBtn = document.getElementById("seatmap-save-btn");
|
stage: seatMapAdminStage,
|
||||||
const seatMapCancelBtn = document.getElementById("seatmap-cancel-btn");
|
name: document.getElementById("seatmap-admin-name"),
|
||||||
const seatMapBoardWrap = document.getElementById("seatmap-board-wrap");
|
status: document.getElementById("seatmap-admin-status"),
|
||||||
const seatMapBoard = document.getElementById("seatmap-board");
|
saveBtn: document.getElementById("seatmap-admin-save-btn"),
|
||||||
const seatMapEmpty = document.getElementById("seatmap-empty");
|
cancelBtn: null,
|
||||||
const seatMapSettingsPanel = document.getElementById("seatmap-settings-panel");
|
exitBtn: document.getElementById("seatmap-admin-exit-btn"),
|
||||||
const seatMapSettingsForm = document.getElementById("seatmap-settings-form");
|
actions: document.getElementById("seatmap-admin-actions"),
|
||||||
const seatMapFormName = document.getElementById("seatmap-form-name");
|
boardWrap: document.getElementById("seatmap-admin-board-wrap"),
|
||||||
const seatMapFileName = document.getElementById("seatmap-file-name");
|
board: document.getElementById("seatmap-admin-board"),
|
||||||
const seatMapFormRows = document.getElementById("seatmap-form-rows");
|
empty: document.getElementById("seatmap-admin-empty"),
|
||||||
const seatMapFormCols = document.getElementById("seatmap-form-cols");
|
settingsPanel: document.getElementById("seatmap-admin-settings-panel"),
|
||||||
const seatMapFormGap = document.getElementById("seatmap-form-gap");
|
settingsForm: document.getElementById("seatmap-admin-settings-form"),
|
||||||
const seatMapFormImage = document.getElementById("seatmap-form-image");
|
formName: document.getElementById("seatmap-admin-form-name"),
|
||||||
const seatMapSearch = document.getElementById("seatmap-search");
|
fileName: document.getElementById("seatmap-admin-file-name"),
|
||||||
const seatMapUnassigned = document.getElementById("seatmap-unassigned");
|
formRows: null,
|
||||||
|
formCols: null,
|
||||||
|
formGap: null,
|
||||||
|
formImage: document.getElementById("seatmap-admin-form-image"),
|
||||||
|
search: document.getElementById("seatmap-admin-search"),
|
||||||
|
unassigned: document.getElementById("seatmap-admin-unassigned"),
|
||||||
|
officeTabs: document.getElementById("seatmap-admin-office-tabs"),
|
||||||
|
sidebarTitle: document.getElementById("seatmap-admin-sidebar-title"),
|
||||||
|
sidebarDesc: document.getElementById("seatmap-admin-sidebar-desc"),
|
||||||
|
},
|
||||||
|
readonly: {
|
||||||
|
stage: seatMapReadonlyStage,
|
||||||
|
name: document.getElementById("seatmap-readonly-name"),
|
||||||
|
status: document.getElementById("seatmap-readonly-status"),
|
||||||
|
saveBtn: null,
|
||||||
|
cancelBtn: null,
|
||||||
|
exitBtn: document.getElementById("seatmap-readonly-exit-btn"),
|
||||||
|
actions: document.getElementById("seatmap-readonly-actions"),
|
||||||
|
boardWrap: document.getElementById("seatmap-readonly-board-wrap"),
|
||||||
|
board: document.getElementById("seatmap-readonly-board"),
|
||||||
|
empty: document.getElementById("seatmap-readonly-empty"),
|
||||||
|
settingsPanel: null,
|
||||||
|
settingsForm: null,
|
||||||
|
formName: null,
|
||||||
|
fileName: null,
|
||||||
|
formRows: null,
|
||||||
|
formCols: null,
|
||||||
|
formGap: null,
|
||||||
|
formImage: null,
|
||||||
|
search: document.getElementById("seatmap-readonly-search"),
|
||||||
|
unassigned: document.getElementById("seatmap-readonly-unassigned"),
|
||||||
|
officeTabs: document.getElementById("seatmap-readonly-office-tabs"),
|
||||||
|
sidebarTitle: document.getElementById("seatmap-readonly-sidebar-title"),
|
||||||
|
sidebarDesc: document.getElementById("seatmap-readonly-sidebar-desc"),
|
||||||
|
},
|
||||||
|
};
|
||||||
|
|
||||||
|
let seatMapName = seatMapDom.admin.name;
|
||||||
|
let seatMapStatus = seatMapDom.admin.status;
|
||||||
|
let seatMapSaveBtn = seatMapDom.admin.saveBtn;
|
||||||
|
let seatMapCancelBtn = seatMapDom.admin.cancelBtn;
|
||||||
|
let seatMapExitBtn = seatMapDom.admin.exitBtn;
|
||||||
|
let seatMapActions = seatMapDom.admin.actions;
|
||||||
|
let seatMapBoardWrap = seatMapDom.admin.boardWrap;
|
||||||
|
let seatMapBoard = seatMapDom.admin.board;
|
||||||
|
let seatMapEmpty = seatMapDom.admin.empty;
|
||||||
|
let seatMapSettingsPanel = seatMapDom.admin.settingsPanel;
|
||||||
|
let seatMapSettingsForm = seatMapDom.admin.settingsForm;
|
||||||
|
let seatMapFormName = seatMapDom.admin.formName;
|
||||||
|
let seatMapFileName = seatMapDom.admin.fileName;
|
||||||
|
let seatMapFormRows = seatMapDom.admin.formRows;
|
||||||
|
let seatMapFormCols = seatMapDom.admin.formCols;
|
||||||
|
let seatMapFormGap = seatMapDom.admin.formGap;
|
||||||
|
let seatMapFormImage = seatMapDom.admin.formImage;
|
||||||
|
let seatMapSearch = seatMapDom.admin.search;
|
||||||
|
let seatMapUnassigned = seatMapDom.admin.unassigned;
|
||||||
|
let seatMapOfficeTabs = seatMapDom.admin.officeTabs;
|
||||||
|
let seatMapSidebarTitle = seatMapDom.admin.sidebarTitle;
|
||||||
|
let seatMapSidebarDesc = seatMapDom.admin.sidebarDesc;
|
||||||
const APP_BASE_URL = String(window.__MH_BASE_URL || "").replace(/\/$/, "");
|
const APP_BASE_URL = String(window.__MH_BASE_URL || "").replace(/\/$/, "");
|
||||||
|
|
||||||
|
const seatMapOffices = [
|
||||||
|
{ key: "technical-development-center", label: "기술개발센터", ready: true },
|
||||||
|
{ key: "hanmac-building-7f", label: "한맥빌딩 7층", ready: false },
|
||||||
|
{ key: "hanmac-building-6f", label: "한맥빌딩 6층", ready: false },
|
||||||
|
];
|
||||||
|
|
||||||
const viewLabels = {
|
const viewLabels = {
|
||||||
ledger: "사업관리대장",
|
ledger: "사업관리대장",
|
||||||
project: "프로젝트별 분석",
|
project: "프로젝트별 분석",
|
||||||
team: "팀/개인별 분석",
|
team: "팀/개인별 분석",
|
||||||
organization: "조직 현황",
|
organization: "조직 현황",
|
||||||
seatmap: "조직 현황",
|
"seatmap-admin": "자리배치도",
|
||||||
|
"seatmap-readonly": "자리배치도",
|
||||||
};
|
};
|
||||||
|
|
||||||
const seatMapState = {
|
const seatMapState = {
|
||||||
@@ -71,6 +137,8 @@ const seatMapState = {
|
|||||||
viewerDragStartY: 0,
|
viewerDragStartY: 0,
|
||||||
viewerDragOffsetX: 0,
|
viewerDragOffsetX: 0,
|
||||||
viewerDragOffsetY: 0,
|
viewerDragOffsetY: 0,
|
||||||
|
officeKey: "technical-development-center",
|
||||||
|
forceReadOnly: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
let currentView = "organization";
|
let currentView = "organization";
|
||||||
@@ -107,6 +175,48 @@ function isSlotBasedSeatMap() {
|
|||||||
return seatMapState.seatMap?.source_type === "dxf" || seatMapState.seatMap?.source_type === "fixed_html";
|
return seatMapState.seatMap?.source_type === "dxf" || seatMapState.seatMap?.source_type === "fixed_html";
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function canEditSeatMap() {
|
||||||
|
return isAdmin() && !seatMapState.forceReadOnly;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSeatMapScreenMode() {
|
||||||
|
return canEditSeatMap() ? "admin" : "readonly";
|
||||||
|
}
|
||||||
|
|
||||||
|
function isSeatMapAdminMode() {
|
||||||
|
return getSeatMapScreenMode() === "admin";
|
||||||
|
}
|
||||||
|
|
||||||
|
function syncSeatMapDomRefs() {
|
||||||
|
const dom = currentView === "seatmap-readonly" ? seatMapDom.readonly : seatMapDom.admin;
|
||||||
|
seatMapName = dom.name;
|
||||||
|
seatMapStatus = dom.status;
|
||||||
|
seatMapSaveBtn = dom.saveBtn;
|
||||||
|
seatMapCancelBtn = dom.cancelBtn;
|
||||||
|
seatMapExitBtn = dom.exitBtn;
|
||||||
|
seatMapActions = dom.actions;
|
||||||
|
seatMapBoardWrap = dom.boardWrap;
|
||||||
|
seatMapBoard = dom.board;
|
||||||
|
seatMapEmpty = dom.empty;
|
||||||
|
seatMapSettingsPanel = dom.settingsPanel;
|
||||||
|
seatMapSettingsForm = dom.settingsForm;
|
||||||
|
seatMapFormName = dom.formName;
|
||||||
|
seatMapFileName = dom.fileName;
|
||||||
|
seatMapFormRows = dom.formRows;
|
||||||
|
seatMapFormCols = dom.formCols;
|
||||||
|
seatMapFormGap = dom.formGap;
|
||||||
|
seatMapFormImage = dom.formImage;
|
||||||
|
seatMapSearch = dom.search;
|
||||||
|
seatMapUnassigned = dom.unassigned;
|
||||||
|
seatMapOfficeTabs = dom.officeTabs;
|
||||||
|
seatMapSidebarTitle = dom.sidebarTitle;
|
||||||
|
seatMapSidebarDesc = dom.sidebarDesc;
|
||||||
|
}
|
||||||
|
|
||||||
|
function getCurrentSeatMapOffice() {
|
||||||
|
return seatMapOffices.find((item) => item.key === seatMapState.officeKey) || seatMapOffices[0];
|
||||||
|
}
|
||||||
|
|
||||||
function escapeHtml(value) {
|
function escapeHtml(value) {
|
||||||
return String(value ?? "")
|
return String(value ?? "")
|
||||||
.replaceAll("&", "&")
|
.replaceAll("&", "&")
|
||||||
@@ -443,14 +553,72 @@ function getMemberMap() {
|
|||||||
return new Map(seatMapState.members.map((member) => [Number(member.id), member]));
|
return new Map(seatMapState.members.map((member) => [Number(member.id), member]));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getPlacementForMember(memberId) {
|
||||||
|
return getPlacementSource().find((item) => Number(item.member_id) === Number(memberId)) || null;
|
||||||
|
}
|
||||||
|
|
||||||
|
function memberMatchesSeatMapSearch(member) {
|
||||||
|
const keyword = seatMapState.search.trim().toLowerCase();
|
||||||
|
if (!keyword) return true;
|
||||||
|
const haystack = `${member.name || ""} ${member.department || ""} ${member.team || ""} ${member.rank || ""}`.toLowerCase();
|
||||||
|
return haystack.includes(keyword);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSidebarMembers() {
|
||||||
|
const members = isSeatMapAdminMode()
|
||||||
|
? getUnassignedMembers()
|
||||||
|
: seatMapState.members.filter((member) => Boolean(getPlacementForMember(Number(member.id))));
|
||||||
|
return members.filter(memberMatchesSeatMapSearch);
|
||||||
|
}
|
||||||
|
|
||||||
|
function focusSeatMapMember(memberId) {
|
||||||
|
const placement = getPlacementForMember(memberId);
|
||||||
|
const member = getMemberMap().get(Number(memberId));
|
||||||
|
if (!placement) {
|
||||||
|
setSeatMapStatus("해당 인원은 아직 배치되지 않았습니다.", "info");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const slot = getSeatSlotMap().get(Number(placement.seat_slot_id));
|
||||||
|
if (!slot) {
|
||||||
|
setSeatMapStatus("좌석 정보를 찾지 못했습니다.", "error");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const frame = seatMapBoard?.querySelector("#seatmap-dxf-frame");
|
||||||
|
if (!frame?.contentWindow) {
|
||||||
|
setSeatMapStatus("현재 선택한 사무실 도면에서는 좌석을 표시할 수 없습니다.", "info");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
frame.contentWindow.postMessage(
|
||||||
|
{ type: "seatmap-focus-chair", key: String(slot.slot_key), padding: 2600 },
|
||||||
|
window.location.origin,
|
||||||
|
);
|
||||||
|
setSeatMapStatus(`${member?.name || "구성원"} 좌석으로 이동했습니다.`, "info");
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderSeatMapOfficeTabs() {
|
||||||
|
if (!seatMapOfficeTabs) return;
|
||||||
|
seatMapOfficeTabs.innerHTML = seatMapOffices.map((office) => `
|
||||||
|
<button
|
||||||
|
class="seatmap-office-tab${seatMapState.officeKey === office.key ? " active" : ""}"
|
||||||
|
type="button"
|
||||||
|
data-seatmap-office="${office.key}"
|
||||||
|
>${escapeHtml(office.label)}</button>
|
||||||
|
`).join("");
|
||||||
|
}
|
||||||
|
|
||||||
function getUnassignedMembers() {
|
function getUnassignedMembers() {
|
||||||
const placedIds = new Set(getPlacementSource().map((item) => Number(item.member_id)));
|
const placedIds = new Set(getPlacementSource().map((item) => Number(item.member_id)));
|
||||||
const keyword = seatMapState.search.trim().toLowerCase();
|
|
||||||
return seatMapState.members.filter((member) => {
|
return seatMapState.members.filter((member) => {
|
||||||
if (placedIds.has(Number(member.id))) return false;
|
if (placedIds.has(Number(member.id))) return false;
|
||||||
if (!keyword) return true;
|
return memberMatchesSeatMapSearch(member);
|
||||||
const haystack = `${member.name || ""} ${member.department || ""} ${member.team || ""}`.toLowerCase();
|
});
|
||||||
return haystack.includes(keyword);
|
}
|
||||||
|
|
||||||
|
function getPlacedMembers() {
|
||||||
|
const placedIds = new Set(getPlacementSource().map((item) => Number(item.member_id)));
|
||||||
|
return seatMapState.members.filter((member) => {
|
||||||
|
if (!placedIds.has(Number(member.id))) return false;
|
||||||
|
return memberMatchesSeatMapSearch(member);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -553,6 +721,21 @@ function renderUnassignedMemberCard(member, draggable) {
|
|||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderSeatMapSearchCard(member) {
|
||||||
|
const placement = getPlacementForMember(Number(member.id));
|
||||||
|
if (!placement) return "";
|
||||||
|
const badge = `<span class="seatmap-member-badge occupied">${escapeHtml(placement.seat_label || "배치완료")}</span>`;
|
||||||
|
return `
|
||||||
|
<button class="seatmap-member-search-card" type="button" data-member-id="${Number(member.id)}">
|
||||||
|
<span class="seatmap-member-text seatmap-member-text-inline">
|
||||||
|
<strong>${escapeHtml(member.name || "-")}</strong>
|
||||||
|
<em>${escapeHtml(member.rank || member.department || "-")}</em>
|
||||||
|
</span>
|
||||||
|
${badge}
|
||||||
|
</button>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
function renderSeatMapBoard() {
|
function renderSeatMapBoard() {
|
||||||
if (!seatMapBoard || !seatMapState.seatMap) return;
|
if (!seatMapBoard || !seatMapState.seatMap) return;
|
||||||
|
|
||||||
@@ -643,6 +826,10 @@ function getSeatAssignmentPayload() {
|
|||||||
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;
|
||||||
|
frame.contentWindow.postMessage(
|
||||||
|
{ type: "seatmap-set-mode", mode: isSeatMapAdminMode() ? "default" : "readonly" },
|
||||||
|
window.location.origin,
|
||||||
|
);
|
||||||
frame.contentWindow.postMessage(
|
frame.contentWindow.postMessage(
|
||||||
{ type: "seatmap-set-placed", keys: getDraftPlacedSlotKeys() },
|
{ type: "seatmap-set-placed", keys: getDraftPlacedSlotKeys() },
|
||||||
window.location.origin,
|
window.location.origin,
|
||||||
@@ -653,14 +840,23 @@ function syncSeatMapViewerFrame() {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
function updateSeatMapDraftUi() {
|
function renderSeatMapActions() {
|
||||||
|
const hasSeatMap = Boolean(seatMapState.seatMap);
|
||||||
|
const adminMode = isSeatMapAdminMode();
|
||||||
if (seatMapSaveBtn) {
|
if (seatMapSaveBtn) {
|
||||||
seatMapSaveBtn.hidden = !isAdmin() || !seatMapState.seatMap;
|
seatMapSaveBtn.hidden = !adminMode || !hasSeatMap;
|
||||||
seatMapSaveBtn.disabled = !seatMapState.dirty;
|
seatMapSaveBtn.disabled = !seatMapState.dirty;
|
||||||
}
|
}
|
||||||
if (seatMapCancelBtn) {
|
if (seatMapExitBtn) {
|
||||||
seatMapCancelBtn.hidden = !seatMapState.seatMap;
|
seatMapExitBtn.hidden = !hasSeatMap;
|
||||||
}
|
}
|
||||||
|
if (seatMapActions) {
|
||||||
|
seatMapActions.hidden = !hasSeatMap;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateSeatMapDraftUi() {
|
||||||
|
renderSeatMapActions();
|
||||||
renderUnassignedMembers();
|
renderUnassignedMembers();
|
||||||
syncSeatMapViewerFrame();
|
syncSeatMapViewerFrame();
|
||||||
}
|
}
|
||||||
@@ -700,25 +896,80 @@ function setupSeatMapViewerFrame() {
|
|||||||
}, { once: true });
|
}, { once: true });
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderUnassignedMembers() {
|
function renderAdminSeatMapSidebar() {
|
||||||
if (!seatMapUnassigned) return;
|
const unassignedMembers = getUnassignedMembers();
|
||||||
const editable = seatMapState.editMode && isAdmin();
|
const placedMembers = getPlacedMembers();
|
||||||
const members = getUnassignedMembers();
|
if (seatMapSidebarTitle) {
|
||||||
|
seatMapSidebarTitle.textContent = "전체 인원";
|
||||||
if (!members.length) {
|
}
|
||||||
|
if (seatMapSidebarDesc) {
|
||||||
|
seatMapSidebarDesc.textContent = "미배치 인원은 상단, 배치 완료 인원은 하단에 표시됩니다.";
|
||||||
|
}
|
||||||
|
if (!unassignedMembers.length && !placedMembers.length) {
|
||||||
seatMapUnassigned.innerHTML = `
|
seatMapUnassigned.innerHTML = `
|
||||||
<div class="seatmap-list-empty">
|
<div class="seatmap-list-empty">
|
||||||
${seatMapState.search ? "검색 결과가 없습니다." : "미배치 인원이 없습니다."}
|
${seatMapState.search ? "검색 결과가 없습니다." : "표시할 인원이 없습니다."}
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
seatMapUnassigned.innerHTML = `
|
||||||
|
<section class="seatmap-member-section">
|
||||||
|
<div class="seatmap-member-section-head">
|
||||||
|
<strong>미배치 인원</strong>
|
||||||
|
<span>${unassignedMembers.length}명</span>
|
||||||
|
</div>
|
||||||
|
<div class="seatmap-member-section-list">
|
||||||
|
${unassignedMembers.length
|
||||||
|
? unassignedMembers.map((member) => renderUnassignedMemberCard(member, true)).join("")
|
||||||
|
: '<div class="seatmap-list-empty seatmap-list-empty-inline">미배치 인원이 없습니다.</div>'}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section class="seatmap-member-section">
|
||||||
|
<div class="seatmap-member-section-head">
|
||||||
|
<strong>배치 완료</strong>
|
||||||
|
<span>${placedMembers.length}명</span>
|
||||||
|
</div>
|
||||||
|
<div class="seatmap-member-section-list">
|
||||||
|
${placedMembers.length
|
||||||
|
? placedMembers.map((member) => renderSeatMapSearchCard(member)).join("")
|
||||||
|
: '<div class="seatmap-list-empty seatmap-list-empty-inline">배치된 인원이 없습니다.</div>'}
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
`;
|
||||||
|
}
|
||||||
|
|
||||||
seatMapUnassigned.innerHTML = members.map((member) => renderUnassignedMemberCard(member, editable)).join("");
|
function renderReadonlySeatMapSidebar() {
|
||||||
|
const members = getSidebarMembers();
|
||||||
|
if (seatMapSidebarTitle) {
|
||||||
|
seatMapSidebarTitle.textContent = "배치 인원 검색";
|
||||||
|
}
|
||||||
|
if (seatMapSidebarDesc) {
|
||||||
|
seatMapSidebarDesc.textContent = "이름이나 부서를 검색하고 클릭하면 해당 좌석으로 바로 확대 이동합니다.";
|
||||||
|
}
|
||||||
|
if (!members.length) {
|
||||||
|
seatMapUnassigned.innerHTML = `
|
||||||
|
<div class="seatmap-list-empty">
|
||||||
|
${seatMapState.search ? "검색 결과가 없습니다." : "배치된 인원이 없습니다."}
|
||||||
|
</div>
|
||||||
|
`;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
seatMapUnassigned.innerHTML = members.map((member) => renderSeatMapSearchCard(member)).join("");
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderUnassignedMembers() {
|
||||||
|
if (!seatMapUnassigned) return;
|
||||||
|
if (isSeatMapAdminMode()) {
|
||||||
|
renderAdminSeatMapSidebar();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
renderReadonlySeatMapSidebar();
|
||||||
}
|
}
|
||||||
|
|
||||||
function renderSeatMapEmpty() {
|
function renderSeatMapEmpty() {
|
||||||
if (!seatMapEmpty) return;
|
if (!seatMapEmpty) return;
|
||||||
|
const office = getCurrentSeatMapOffice();
|
||||||
if (seatMapState.seatMap) {
|
if (seatMapState.seatMap) {
|
||||||
seatMapEmpty.classList.add("hidden");
|
seatMapEmpty.classList.add("hidden");
|
||||||
seatMapEmpty.innerHTML = "";
|
seatMapEmpty.innerHTML = "";
|
||||||
@@ -728,8 +979,8 @@ function renderSeatMapEmpty() {
|
|||||||
seatMapEmpty.classList.remove("hidden");
|
seatMapEmpty.classList.remove("hidden");
|
||||||
seatMapEmpty.innerHTML = `
|
seatMapEmpty.innerHTML = `
|
||||||
<div class="seatmap-empty-card">
|
<div class="seatmap-empty-card">
|
||||||
<strong>등록된 자리배치도가 없습니다.</strong>
|
<strong>${escapeHtml(office.label)} 도면이 아직 준비되지 않았습니다.</strong>
|
||||||
<p>${isAdmin() ? "오른쪽 설정 패널에서 이미지와 그리드를 등록하세요." : "관리자에게 자리배치도 등록을 요청하세요."}</p>
|
<p>${office.ready ? (canEditSeatMap() ? "오른쪽 설정 패널에서 이미지와 그리드를 등록하세요." : "관리자에게 자리배치도 등록을 요청하세요.") : "도면 파일을 추후 연결하면 여기서 바로 전환해 볼 수 있습니다."}</p>
|
||||||
</div>
|
</div>
|
||||||
`;
|
`;
|
||||||
}
|
}
|
||||||
@@ -758,11 +1009,12 @@ function syncSeatMapSettingsForm() {
|
|||||||
|
|
||||||
function renderSeatMap() {
|
function renderSeatMap() {
|
||||||
const hasSeatMap = Boolean(seatMapState.seatMap);
|
const hasSeatMap = Boolean(seatMapState.seatMap);
|
||||||
const admin = isAdmin();
|
const admin = isSeatMapAdminMode();
|
||||||
const fixedViewerMap = seatMapState.seatMap?.source_type === "fixed_html";
|
const fixedViewerMap = seatMapState.seatMap?.source_type === "fixed_html";
|
||||||
|
const office = getCurrentSeatMapOffice();
|
||||||
|
|
||||||
if (seatMapName) {
|
if (seatMapName) {
|
||||||
seatMapName.textContent = hasSeatMap ? seatMapState.seatMap.name : "자리배치도";
|
seatMapName.textContent = hasSeatMap ? seatMapState.seatMap.name : office.label;
|
||||||
}
|
}
|
||||||
if (seatMapStatus) {
|
if (seatMapStatus) {
|
||||||
seatMapStatus.textContent = seatMapState.status;
|
seatMapStatus.textContent = seatMapState.status;
|
||||||
@@ -771,16 +1023,11 @@ function renderSeatMap() {
|
|||||||
if (seatMapSettingsPanel) {
|
if (seatMapSettingsPanel) {
|
||||||
seatMapSettingsPanel.classList.toggle("hidden", !admin || fixedViewerMap);
|
seatMapSettingsPanel.classList.toggle("hidden", !admin || fixedViewerMap);
|
||||||
}
|
}
|
||||||
if (seatMapSaveBtn) {
|
renderSeatMapActions();
|
||||||
seatMapSaveBtn.hidden = !admin || !hasSeatMap;
|
|
||||||
seatMapSaveBtn.disabled = !seatMapState.dirty;
|
|
||||||
}
|
|
||||||
if (seatMapCancelBtn) {
|
|
||||||
seatMapCancelBtn.hidden = !hasSeatMap;
|
|
||||||
}
|
|
||||||
if (seatMapSettingsForm) {
|
if (seatMapSettingsForm) {
|
||||||
seatMapSettingsForm.querySelector("button[type='submit']").textContent = hasSeatMap ? "배치도 저장" : "배치도 생성";
|
seatMapSettingsForm.querySelector("button[type='submit']").textContent = hasSeatMap ? "배치도 저장" : "배치도 생성";
|
||||||
}
|
}
|
||||||
|
renderSeatMapOfficeTabs();
|
||||||
|
|
||||||
renderSeatMapEmpty();
|
renderSeatMapEmpty();
|
||||||
if (seatMapBoardWrap) {
|
if (seatMapBoardWrap) {
|
||||||
@@ -797,15 +1044,16 @@ function renderSeatMap() {
|
|||||||
function handleEmbeddedNavigationMessage(event) {
|
function handleEmbeddedNavigationMessage(event) {
|
||||||
const data = event.data;
|
const data = event.data;
|
||||||
if (!data || typeof data !== "object") return;
|
if (!data || typeof data !== "object") return;
|
||||||
if (data.type === "open-seatmap" && isAdmin()) {
|
if (data.type === "open-seatmap") {
|
||||||
hideUserPopover();
|
hideUserPopover();
|
||||||
setActiveView("seatmap");
|
seatMapState.forceReadOnly = Boolean(data.readOnly);
|
||||||
|
setActiveView(data.readOnly ? "seatmap-readonly" : "seatmap-admin");
|
||||||
}
|
}
|
||||||
if (data.type === "open-organization") {
|
if (data.type === "open-organization") {
|
||||||
hideUserPopover();
|
hideUserPopover();
|
||||||
setActiveView("organization");
|
setActiveView("organization");
|
||||||
}
|
}
|
||||||
if (data.type === "seatmap-clear-slot" && isAdmin()) {
|
if (data.type === "seatmap-clear-slot" && canEditSeatMap()) {
|
||||||
const cleared = clearDraftPlacementBySlotKey(String(data.key || ""));
|
const cleared = clearDraftPlacementBySlotKey(String(data.key || ""));
|
||||||
if (cleared) {
|
if (cleared) {
|
||||||
setSeatMapStatus("구성원을 공석으로 이동했습니다. 저장 버튼으로 반영하세요.", "info");
|
setSeatMapStatus("구성원을 공석으로 이동했습니다. 저장 버튼으로 반영하세요.", "info");
|
||||||
@@ -838,6 +1086,22 @@ async function loadSeatMapData(force = false) {
|
|||||||
renderSeatMap();
|
renderSeatMap();
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
const office = getCurrentSeatMapOffice();
|
||||||
|
if (!office.ready) {
|
||||||
|
const membersPayload = await fetchJson("/api/members");
|
||||||
|
seatMapState.seatMap = null;
|
||||||
|
seatMapState.members = Array.isArray(membersPayload.items) ? membersPayload.items : [];
|
||||||
|
seatMapState.slots = [];
|
||||||
|
seatMapState.placements = [];
|
||||||
|
seatMapState.zoom = 1;
|
||||||
|
seatMapState.hoveredSlotId = null;
|
||||||
|
seatMapState.editMode = canEditSeatMap();
|
||||||
|
resetSeatMapDraft();
|
||||||
|
seatMapState.loaded = true;
|
||||||
|
setSeatMapStatus(`${office.label} 도면은 아직 등록 전입니다.`, "info");
|
||||||
|
renderSeatMap();
|
||||||
|
return;
|
||||||
|
}
|
||||||
const activePayload = await fetchJson("/api/seat-maps/active");
|
const activePayload = await fetchJson("/api/seat-maps/active");
|
||||||
const activeSeatMap = activePayload.item;
|
const activeSeatMap = activePayload.item;
|
||||||
const layoutPayload = await fetchJson(`/api/seat-maps/${activeSeatMap.id}/layout`);
|
const layoutPayload = await fetchJson(`/api/seat-maps/${activeSeatMap.id}/layout`);
|
||||||
@@ -850,10 +1114,10 @@ async function loadSeatMapData(force = false) {
|
|||||||
seatMapState.placements = clonePlacements(layoutPayload.placements || []);
|
seatMapState.placements = clonePlacements(layoutPayload.placements || []);
|
||||||
seatMapState.zoom = 1;
|
seatMapState.zoom = 1;
|
||||||
seatMapState.hoveredSlotId = null;
|
seatMapState.hoveredSlotId = null;
|
||||||
seatMapState.editMode = isAdmin();
|
seatMapState.editMode = canEditSeatMap();
|
||||||
resetSeatMapDraft();
|
resetSeatMapDraft();
|
||||||
seatMapState.loaded = true;
|
seatMapState.loaded = true;
|
||||||
setSeatMapStatus(isAdmin() ? "구성원을 바로 드래그해서 배치한 뒤 저장하세요." : "자리배치도를 불러왔습니다.", "success");
|
setSeatMapStatus(canEditSeatMap() ? "구성원을 바로 드래그해서 배치한 뒤 저장하세요." : "자리배치도를 불러왔습니다.", "success");
|
||||||
syncSeatMapSettingsForm();
|
syncSeatMapSettingsForm();
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
if (error.status === 404) {
|
if (error.status === 404) {
|
||||||
@@ -863,7 +1127,7 @@ async function loadSeatMapData(force = false) {
|
|||||||
seatMapState.placements = [];
|
seatMapState.placements = [];
|
||||||
seatMapState.zoom = 1;
|
seatMapState.zoom = 1;
|
||||||
seatMapState.hoveredSlotId = null;
|
seatMapState.hoveredSlotId = null;
|
||||||
seatMapState.editMode = isAdmin();
|
seatMapState.editMode = canEditSeatMap();
|
||||||
resetSeatMapDraft();
|
resetSeatMapDraft();
|
||||||
seatMapState.loaded = true;
|
seatMapState.loaded = true;
|
||||||
setSeatMapStatus("활성화된 자리배치도가 없습니다.", "info");
|
setSeatMapStatus("활성화된 자리배치도가 없습니다.", "info");
|
||||||
@@ -914,7 +1178,7 @@ async function uploadSeatMapImage(file, name) {
|
|||||||
|
|
||||||
async function submitSeatMapSettings(event) {
|
async function submitSeatMapSettings(event) {
|
||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
if (!isAdmin()) return;
|
if (!canEditSeatMap()) return;
|
||||||
|
|
||||||
const name = seatMapFormName?.value?.trim() || "";
|
const name = seatMapFormName?.value?.trim() || "";
|
||||||
const imageFile = seatMapFormImage?.files?.[0] || null;
|
const imageFile = seatMapFormImage?.files?.[0] || null;
|
||||||
@@ -965,8 +1229,8 @@ async function saveSeatLayout() {
|
|||||||
|
|
||||||
function cancelSeatMapEdit() {
|
function cancelSeatMapEdit() {
|
||||||
resetSeatMapDraft();
|
resetSeatMapDraft();
|
||||||
setSeatMapStatus("", "info");
|
setSeatMapStatus("현재까지 수정한 배치를 취소했습니다.", "info");
|
||||||
setActiveView("organization");
|
renderSeatMap();
|
||||||
}
|
}
|
||||||
|
|
||||||
function getDraggedMemberId(event) {
|
function getDraggedMemberId(event) {
|
||||||
@@ -1017,6 +1281,7 @@ function handleSeatMapListDrop(event) {
|
|||||||
function setActiveView(view) {
|
function setActiveView(view) {
|
||||||
const previousView = currentView;
|
const previousView = currentView;
|
||||||
currentView = view in viewLabels ? view : "organization";
|
currentView = view in viewLabels ? view : "organization";
|
||||||
|
syncSeatMapDomRefs();
|
||||||
if (currentViewTitle) {
|
if (currentViewTitle) {
|
||||||
currentViewTitle.textContent = viewLabels[currentView];
|
currentViewTitle.textContent = viewLabels[currentView];
|
||||||
}
|
}
|
||||||
@@ -1028,17 +1293,22 @@ function setActiveView(view) {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const isOrganization = currentView === "organization";
|
const isOrganization = currentView === "organization";
|
||||||
const isSeatMap = currentView === "seatmap";
|
const isSeatMapAdmin = currentView === "seatmap-admin";
|
||||||
|
const isSeatMapReadonly = currentView === "seatmap-readonly";
|
||||||
if (organizationStage) {
|
if (organizationStage) {
|
||||||
organizationStage.hidden = !isOrganization;
|
organizationStage.hidden = !isOrganization;
|
||||||
organizationStage.style.display = isOrganization ? "flex" : "none";
|
organizationStage.style.display = isOrganization ? "flex" : "none";
|
||||||
}
|
}
|
||||||
if (seatMapStage) {
|
if (seatMapAdminStage) {
|
||||||
seatMapStage.hidden = !isSeatMap;
|
seatMapAdminStage.hidden = !isSeatMapAdmin;
|
||||||
seatMapStage.style.display = isSeatMap ? "flex" : "none";
|
seatMapAdminStage.style.display = isSeatMapAdmin ? "flex" : "none";
|
||||||
|
}
|
||||||
|
if (seatMapReadonlyStage) {
|
||||||
|
seatMapReadonlyStage.hidden = !isSeatMapReadonly;
|
||||||
|
seatMapReadonlyStage.style.display = isSeatMapReadonly ? "flex" : "none";
|
||||||
}
|
}
|
||||||
if (emptyStage) {
|
if (emptyStage) {
|
||||||
const showEmpty = !isOrganization && !isSeatMap;
|
const showEmpty = !isOrganization && !isSeatMapAdmin && !isSeatMapReadonly;
|
||||||
emptyStage.hidden = !showEmpty;
|
emptyStage.hidden = !showEmpty;
|
||||||
emptyStage.style.display = showEmpty ? "flex" : "none";
|
emptyStage.style.display = showEmpty ? "flex" : "none";
|
||||||
}
|
}
|
||||||
@@ -1047,7 +1317,7 @@ function setActiveView(view) {
|
|||||||
const frameSrc = organizationFrame.dataset.src || organizationFrame.src;
|
const frameSrc = organizationFrame.dataset.src || organizationFrame.src;
|
||||||
organizationFrame.src = resolveAppUrl(frameSrc);
|
organizationFrame.src = resolveAppUrl(frameSrc);
|
||||||
}
|
}
|
||||||
if (isSeatMap) {
|
if (isSeatMapAdmin || isSeatMapReadonly) {
|
||||||
loadSeatMapData();
|
loadSeatMapData();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1101,7 +1371,7 @@ if (loginForm) {
|
|||||||
loginForm.reset();
|
loginForm.reset();
|
||||||
loginMessage.textContent = "";
|
loginMessage.textContent = "";
|
||||||
renderAuth();
|
renderAuth();
|
||||||
if (currentView === "seatmap") {
|
if (currentView === "seatmap-admin" || currentView === "seatmap-readonly") {
|
||||||
await loadSeatMapData(true);
|
await loadSeatMapData(true);
|
||||||
}
|
}
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -1133,45 +1403,77 @@ navButtons.forEach((button) => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
if (seatMapSettingsForm) {
|
Object.values(seatMapDom).forEach((dom) => {
|
||||||
seatMapSettingsForm.addEventListener("submit", submitSeatMapSettings);
|
dom.officeTabs?.addEventListener("click", (event) => {
|
||||||
}
|
const button = event.target.closest("[data-seatmap-office]");
|
||||||
|
if (!button) return;
|
||||||
if (seatMapSaveBtn) {
|
const officeKey = button.dataset.seatmapOffice || "";
|
||||||
seatMapSaveBtn.addEventListener("click", saveSeatLayout);
|
if (!officeKey || officeKey === seatMapState.officeKey) return;
|
||||||
}
|
seatMapState.officeKey = officeKey;
|
||||||
|
seatMapState.loaded = false;
|
||||||
if (seatMapCancelBtn) {
|
seatMapState.search = "";
|
||||||
seatMapCancelBtn.addEventListener("click", cancelSeatMapEdit);
|
Object.values(seatMapDom).forEach((item) => {
|
||||||
}
|
if (item.search) item.search.value = "";
|
||||||
|
});
|
||||||
if (seatMapSearch) {
|
loadSeatMapData(true);
|
||||||
seatMapSearch.addEventListener("input", () => {
|
|
||||||
seatMapState.search = seatMapSearch.value || "";
|
|
||||||
renderSeatMap();
|
|
||||||
});
|
});
|
||||||
}
|
|
||||||
|
|
||||||
if (seatMapFormImage) {
|
dom.settingsForm?.addEventListener("submit", submitSeatMapSettings);
|
||||||
seatMapFormImage.addEventListener("change", () => {
|
dom.saveBtn?.addEventListener("click", saveSeatLayout);
|
||||||
if (seatMapFileName) {
|
dom.exitBtn?.addEventListener("click", () => {
|
||||||
seatMapFileName.textContent = seatMapFormImage.files?.[0]?.name || "선택된 파일 없음";
|
if (dom === seatMapDom.admin && seatMapState.dirty) {
|
||||||
|
const confirmed = window.confirm("저장하지 않고 나가면 저장하지 않은 내용은 사라집니다. 나가시겠습니까?");
|
||||||
|
if (!confirmed) return;
|
||||||
|
resetSeatMapDraft();
|
||||||
|
}
|
||||||
|
hideUserPopover();
|
||||||
|
setActiveView("organization");
|
||||||
|
});
|
||||||
|
dom.search?.addEventListener("input", () => {
|
||||||
|
seatMapState.search = dom.search.value || "";
|
||||||
|
const otherMode = dom === seatMapDom.admin ? seatMapDom.readonly : seatMapDom.admin;
|
||||||
|
if (otherMode.search) otherMode.search.value = seatMapState.search;
|
||||||
|
renderUnassignedMembers();
|
||||||
|
});
|
||||||
|
|
||||||
|
dom.unassigned?.addEventListener("click", (event) => {
|
||||||
|
const button = event.target.closest("[data-member-id]");
|
||||||
|
if (!button) return;
|
||||||
|
if (button.classList.contains("seatmap-member-search-card")) {
|
||||||
|
focusSeatMapMember(Number(button.dataset.memberId));
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
if (canEditSeatMap()) return;
|
||||||
|
focusSeatMapMember(Number(button.dataset.memberId));
|
||||||
|
});
|
||||||
|
dom.unassigned?.addEventListener("dragover", (event) => {
|
||||||
|
if (!seatMapState.editMode) return;
|
||||||
|
event.preventDefault();
|
||||||
|
event.dataTransfer.dropEffect = "move";
|
||||||
|
});
|
||||||
|
dom.unassigned?.addEventListener("drop", handleSeatMapListDrop);
|
||||||
|
});
|
||||||
|
|
||||||
|
Object.values(seatMapDom).forEach((dom) => {
|
||||||
|
dom.formImage?.addEventListener("change", () => {
|
||||||
|
if (dom.fileName) {
|
||||||
|
dom.fileName.textContent = dom.formImage.files?.[0]?.name || "선택된 파일 없음";
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
|
||||||
if (seatMapBoard) {
|
Object.values(seatMapDom).forEach((dom) => {
|
||||||
seatMapBoard.addEventListener("wheel", (event) => {
|
dom.board?.addEventListener("wheel", (event) => {
|
||||||
if (!isSlotBasedSeatMap()) 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 });
|
||||||
seatMapBoard.addEventListener("click", (event) => {
|
dom.board?.addEventListener("click", (event) => {
|
||||||
const fitButton = event.target.closest("[data-seatmap-action='fit']");
|
const fitButton = event.target.closest("[data-seatmap-action='fit']");
|
||||||
if (!fitButton) return;
|
if (!fitButton) return;
|
||||||
fitDxfSeatMapBoard();
|
fitDxfSeatMapBoard();
|
||||||
});
|
});
|
||||||
seatMapBoard.addEventListener("dragover", (event) => {
|
dom.board?.addEventListener("dragover", (event) => {
|
||||||
if (!seatMapState.editMode) return;
|
if (!seatMapState.editMode) return;
|
||||||
const target = isSlotBasedSeatMap()
|
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"))
|
||||||
@@ -1180,11 +1482,9 @@ if (seatMapBoard) {
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
event.dataTransfer.dropEffect = "move";
|
event.dataTransfer.dropEffect = "move";
|
||||||
});
|
});
|
||||||
seatMapBoard.addEventListener("drop", handleSeatMapCellDrop);
|
dom.board?.addEventListener("drop", handleSeatMapCellDrop);
|
||||||
}
|
|
||||||
|
|
||||||
if (seatMapBoardWrap) {
|
dom.boardWrap?.addEventListener("mousedown", (event) => {
|
||||||
seatMapBoardWrap.addEventListener("mousedown", (event) => {
|
|
||||||
if (!isSlotBasedSeatMap()) 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;
|
||||||
@@ -1197,13 +1497,13 @@ if (seatMapBoardWrap) {
|
|||||||
seatMapState.panScrollTop = seatMapBoardWrap.scrollTop;
|
seatMapState.panScrollTop = seatMapBoardWrap.scrollTop;
|
||||||
seatMapBoardWrap.classList.add("is-panning");
|
seatMapBoardWrap.classList.add("is-panning");
|
||||||
});
|
});
|
||||||
seatMapBoardWrap.addEventListener("mouseleave", () => {
|
dom.boardWrap?.addEventListener("mouseleave", () => {
|
||||||
if (seatMapBoard?.querySelector("#seatmap-dxf-canvas")) return;
|
if (seatMapBoard?.querySelector("#seatmap-dxf-canvas")) return;
|
||||||
if (seatMapState.hoveredSlotId == null) return;
|
if (seatMapState.hoveredSlotId == null) return;
|
||||||
seatMapState.hoveredSlotId = null;
|
seatMapState.hoveredSlotId = null;
|
||||||
updateSeatMapViewerHoverChip();
|
updateSeatMapViewerHoverChip();
|
||||||
});
|
});
|
||||||
seatMapBoardWrap.addEventListener("mousemove", (event) => {
|
dom.boardWrap?.addEventListener("mousemove", (event) => {
|
||||||
if (!isSlotBasedSeatMap()) 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");
|
||||||
@@ -1212,7 +1512,7 @@ if (seatMapBoardWrap) {
|
|||||||
seatMapState.hoveredSlotId = nextSlotId;
|
seatMapState.hoveredSlotId = nextSlotId;
|
||||||
updateSeatMapViewerHoverChip();
|
updateSeatMapViewerHoverChip();
|
||||||
});
|
});
|
||||||
}
|
});
|
||||||
|
|
||||||
document.addEventListener("mousemove", (event) => {
|
document.addEventListener("mousemove", (event) => {
|
||||||
if (!seatMapState.panning || !seatMapBoardWrap) return;
|
if (!seatMapState.panning || !seatMapBoardWrap) return;
|
||||||
@@ -1228,14 +1528,6 @@ document.addEventListener("mouseup", () => {
|
|||||||
seatMapBoardWrap.classList.remove("is-panning");
|
seatMapBoardWrap.classList.remove("is-panning");
|
||||||
});
|
});
|
||||||
|
|
||||||
if (seatMapUnassigned) {
|
|
||||||
seatMapUnassigned.addEventListener("dragover", (event) => {
|
|
||||||
if (!seatMapState.editMode) return;
|
|
||||||
event.preventDefault();
|
|
||||||
event.dataTransfer.dropEffect = "move";
|
|
||||||
});
|
|
||||||
seatMapUnassigned.addEventListener("drop", handleSeatMapListDrop);
|
|
||||||
}
|
|
||||||
|
|
||||||
document.addEventListener("dragstart", (event) => {
|
document.addEventListener("dragstart", (event) => {
|
||||||
const card = event.target.closest(".seatmap-member-card");
|
const card = event.target.closest(".seatmap-member-card");
|
||||||
@@ -1261,7 +1553,7 @@ setActiveView(currentView);
|
|||||||
renderAuth();
|
renderAuth();
|
||||||
|
|
||||||
window.addEventListener("resize", () => {
|
window.addEventListener("resize", () => {
|
||||||
if (!isSlotBasedSeatMap() || currentView !== "seatmap") return;
|
if (!isSlotBasedSeatMap() || (currentView !== "seatmap-admin" && currentView !== "seatmap-readonly")) return;
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
if (seatMapState.zoom === 1) {
|
if (seatMapState.zoom === 1) {
|
||||||
centerSeatMapBoard();
|
centerSeatMapBoard();
|
||||||
|
|||||||
@@ -68,62 +68,102 @@
|
|||||||
<iframe id="organization-frame" src="/legacy/organization?v=20260325-11" data-src="/legacy/organization?v=20260325-11" title="조직도 메인 화면"></iframe>
|
<iframe id="organization-frame" src="/legacy/organization?v=20260325-11" data-src="/legacy/organization?v=20260325-11" title="조직도 메인 화면"></iframe>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
<section id="seatmap-stage" class="main-stage" hidden>
|
<section id="seatmap-admin-stage" class="main-stage" hidden>
|
||||||
<div class="seatmap-layout">
|
<div class="seatmap-layout">
|
||||||
<div class="seatmap-topbar">
|
<div class="seatmap-topbar">
|
||||||
<div>
|
<div>
|
||||||
<p class="eyebrow">Seat Layout</p>
|
<p class="eyebrow">Seat Layout</p>
|
||||||
<h3 id="seatmap-name">자리배치도</h3>
|
<h3 id="seatmap-admin-name">자리배치도</h3>
|
||||||
</div>
|
</div>
|
||||||
<div class="seatmap-actions">
|
<div id="seatmap-admin-office-tabs" class="seatmap-office-tabs"></div>
|
||||||
<button id="seatmap-save-btn" class="ghost-button" type="button" hidden disabled>저장</button>
|
<div class="seatmap-actions" id="seatmap-admin-actions">
|
||||||
<button id="seatmap-cancel-btn" class="ghost-button ghost-button-soft" type="button" hidden>취소</button>
|
<button id="seatmap-admin-exit-btn" class="ghost-button ghost-button-soft" type="button" hidden>나가기</button>
|
||||||
|
<button id="seatmap-admin-save-btn" class="ghost-button" type="button" hidden disabled>저장</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<p id="seatmap-status" class="seatmap-status" role="status"></p>
|
<p id="seatmap-admin-status" class="seatmap-status" role="status"></p>
|
||||||
|
|
||||||
<div class="seatmap-content">
|
<div class="seatmap-content">
|
||||||
<div class="seatmap-board-panel">
|
<div class="seatmap-board-panel">
|
||||||
<div id="seatmap-empty" class="seatmap-empty hidden"></div>
|
<div id="seatmap-admin-empty" class="seatmap-empty hidden"></div>
|
||||||
<div id="seatmap-board-wrap" class="seatmap-board-wrap hidden">
|
<div id="seatmap-admin-board-wrap" class="seatmap-board-wrap hidden">
|
||||||
<div id="seatmap-board" class="seatmap-board"></div>
|
<div id="seatmap-admin-board" class="seatmap-board"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<aside class="seatmap-sidebar">
|
<aside class="seatmap-sidebar">
|
||||||
<section id="seatmap-settings-panel" class="seatmap-panel hidden">
|
<section id="seatmap-admin-settings-panel" class="seatmap-panel hidden">
|
||||||
<div class="seatmap-panel-head">
|
<div class="seatmap-panel-head">
|
||||||
<h4>도면 설정</h4>
|
<h4>도면 설정</h4>
|
||||||
<p>현재는 기술개발센터 고정 도면을 사용합니다.</p>
|
<p>현재는 기술개발센터 고정 도면을 사용합니다.</p>
|
||||||
</div>
|
</div>
|
||||||
<form id="seatmap-settings-form" class="seatmap-form">
|
<form id="seatmap-admin-settings-form" class="seatmap-form">
|
||||||
<label>
|
<label>
|
||||||
<span>도면 이름</span>
|
<span>도면 이름</span>
|
||||||
<input id="seatmap-form-name" name="name" type="text" placeholder="예: 기술개발센터" required>
|
<input id="seatmap-admin-form-name" name="name" type="text" placeholder="예: 기술개발센터" required>
|
||||||
</label>
|
</label>
|
||||||
<div>
|
<div>
|
||||||
<span>DXF 파일</span>
|
<span>DXF 파일</span>
|
||||||
<label class="seatmap-file-input" for="seatmap-form-image">
|
<label class="seatmap-file-input" for="seatmap-admin-form-image">
|
||||||
<input id="seatmap-form-image" name="image" type="file" accept=".dxf" required>
|
<input id="seatmap-admin-form-image" name="image" type="file" accept=".dxf" required>
|
||||||
<span class="seatmap-file-button">DXF 선택</span>
|
<span class="seatmap-file-button">DXF 선택</span>
|
||||||
<strong id="seatmap-file-name" class="seatmap-file-name">선택된 파일 없음</strong>
|
<strong id="seatmap-admin-file-name" class="seatmap-file-name">선택된 파일 없음</strong>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<button id="seatmap-settings-submit" type="submit">DXF 업로드</button>
|
<button id="seatmap-admin-settings-submit" type="submit">DXF 업로드</button>
|
||||||
</form>
|
</form>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="seatmap-panel">
|
<section class="seatmap-panel">
|
||||||
<div class="seatmap-panel-head">
|
<div class="seatmap-panel-head">
|
||||||
<h4>미배치 인원</h4>
|
<h4 id="seatmap-admin-sidebar-title">전체 인원</h4>
|
||||||
<p>이름을 검색하고 자리배치도에 바로 드래그하세요.</p>
|
<p id="seatmap-admin-sidebar-desc">미배치 인원은 상단, 배치 완료 인원은 하단에 표시됩니다.</p>
|
||||||
</div>
|
</div>
|
||||||
<label class="seatmap-search">
|
<label class="seatmap-search">
|
||||||
<span class="hidden">구성원 검색</span>
|
<span class="hidden">구성원 검색</span>
|
||||||
<input id="seatmap-search" type="search" placeholder="이름 또는 부서 검색">
|
<input id="seatmap-admin-search" type="search" placeholder="이름 또는 부서 검색">
|
||||||
</label>
|
</label>
|
||||||
<div id="seatmap-unassigned" class="seatmap-member-list"></div>
|
<div id="seatmap-admin-unassigned" class="seatmap-member-list"></div>
|
||||||
|
</section>
|
||||||
|
</aside>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
<section id="seatmap-readonly-stage" class="main-stage" hidden>
|
||||||
|
<div class="seatmap-layout">
|
||||||
|
<div class="seatmap-topbar">
|
||||||
|
<div>
|
||||||
|
<p class="eyebrow">Seat Layout</p>
|
||||||
|
<h3 id="seatmap-readonly-name">자리배치도</h3>
|
||||||
|
</div>
|
||||||
|
<div id="seatmap-readonly-office-tabs" class="seatmap-office-tabs"></div>
|
||||||
|
<div class="seatmap-actions" id="seatmap-readonly-actions">
|
||||||
|
<button id="seatmap-readonly-exit-btn" class="ghost-button ghost-button-soft" type="button" hidden>나가기</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<p id="seatmap-readonly-status" class="seatmap-status" role="status"></p>
|
||||||
|
|
||||||
|
<div class="seatmap-content">
|
||||||
|
<div class="seatmap-board-panel">
|
||||||
|
<div id="seatmap-readonly-empty" class="seatmap-empty hidden"></div>
|
||||||
|
<div id="seatmap-readonly-board-wrap" class="seatmap-board-wrap hidden">
|
||||||
|
<div id="seatmap-readonly-board" class="seatmap-board"></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<aside class="seatmap-sidebar">
|
||||||
|
<section class="seatmap-panel">
|
||||||
|
<div class="seatmap-panel-head">
|
||||||
|
<h4 id="seatmap-readonly-sidebar-title">배치 인원 검색</h4>
|
||||||
|
<p id="seatmap-readonly-sidebar-desc">이름이나 부서를 검색하고 클릭하면 해당 좌석으로 바로 확대 이동합니다.</p>
|
||||||
|
</div>
|
||||||
|
<label class="seatmap-search">
|
||||||
|
<span class="hidden">구성원 검색</span>
|
||||||
|
<input id="seatmap-readonly-search" type="search" placeholder="이름 또는 부서 검색">
|
||||||
|
</label>
|
||||||
|
<div id="seatmap-readonly-unassigned" class="seatmap-member-list"></div>
|
||||||
</section>
|
</section>
|
||||||
</aside>
|
</aside>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,3 +1,19 @@
|
|||||||
|
.dashboard-shell,
|
||||||
|
.dashboard-main,
|
||||||
|
.main-stage,
|
||||||
|
.seatmap-layout,
|
||||||
|
.seatmap-content,
|
||||||
|
.seatmap-board-panel,
|
||||||
|
.seatmap-sidebar {
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
.hidden {
|
.hidden {
|
||||||
display: none !important;
|
display: none !important;
|
||||||
}
|
}
|
||||||
@@ -143,9 +159,10 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-shell {
|
.dashboard-shell {
|
||||||
min-height: 100vh;
|
height: 100dvh;
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.dashboard-header {
|
.dashboard-header {
|
||||||
@@ -371,14 +388,16 @@
|
|||||||
|
|
||||||
.dashboard-main {
|
.dashboard-main {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
min-height: calc(100vh - 68px);
|
height: calc(100dvh - 68px);
|
||||||
padding: 0;
|
padding: 0;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.main-stage {
|
.main-stage {
|
||||||
height: calc(100vh - 68px);
|
height: calc(100dvh - 68px);
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.stage-frame {
|
.stage-frame {
|
||||||
@@ -407,18 +426,45 @@
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
gap: 12px;
|
gap: 12px;
|
||||||
padding: 18px;
|
padding: 18px;
|
||||||
|
overflow: hidden;
|
||||||
background:
|
background:
|
||||||
linear-gradient(180deg, rgba(248, 250, 252, 0.94), rgba(241, 245, 249, 0.92)),
|
linear-gradient(180deg, rgba(248, 250, 252, 0.94), rgba(241, 245, 249, 0.92)),
|
||||||
radial-gradient(circle at top left, rgba(14, 165, 233, 0.1), transparent 32%);
|
radial-gradient(circle at top left, rgba(14, 165, 233, 0.1), transparent 32%);
|
||||||
}
|
}
|
||||||
|
|
||||||
.seatmap-topbar {
|
.seatmap-topbar {
|
||||||
display: flex;
|
display: grid;
|
||||||
align-items: flex-end;
|
grid-template-columns: minmax(180px, 1fr) auto minmax(180px, 1fr);
|
||||||
justify-content: space-between;
|
align-items: end;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.seatmap-office-tabs {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
justify-content: center;
|
||||||
|
grid-column: 2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seatmap-office-tab {
|
||||||
|
min-height: 38px;
|
||||||
|
padding: 0 14px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 1px solid rgba(148, 163, 184, 0.26);
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
color: #475569;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 800;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seatmap-office-tab.active {
|
||||||
|
border-color: rgba(15, 118, 110, 0.22);
|
||||||
|
background: rgba(15, 118, 110, 0.1);
|
||||||
|
color: #0f766e;
|
||||||
|
}
|
||||||
|
|
||||||
.seatmap-topbar h3,
|
.seatmap-topbar h3,
|
||||||
.seatmap-panel-head h4 {
|
.seatmap-panel-head h4 {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@@ -432,6 +478,12 @@
|
|||||||
display: flex;
|
display: flex;
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
gap: 8px;
|
gap: 8px;
|
||||||
|
justify-content: flex-end;
|
||||||
|
grid-column: 3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seatmap-actions[hidden] {
|
||||||
|
display: none !important;
|
||||||
}
|
}
|
||||||
|
|
||||||
.seatmap-status {
|
.seatmap-status {
|
||||||
@@ -456,6 +508,7 @@
|
|||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: minmax(0, 1fr) 320px;
|
grid-template-columns: minmax(0, 1fr) 320px;
|
||||||
gap: 16px;
|
gap: 16px;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.seatmap-board-panel,
|
.seatmap-board-panel,
|
||||||
@@ -468,6 +521,7 @@
|
|||||||
|
|
||||||
.seatmap-board-panel {
|
.seatmap-board-panel {
|
||||||
min-height: 0;
|
min-height: 0;
|
||||||
|
height: 100%;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
}
|
}
|
||||||
@@ -475,7 +529,7 @@
|
|||||||
.seatmap-board-wrap {
|
.seatmap-board-wrap {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
overflow: auto;
|
overflow: hidden;
|
||||||
border-radius: 24px;
|
border-radius: 24px;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
padding: 0;
|
padding: 0;
|
||||||
@@ -490,6 +544,7 @@
|
|||||||
.seatmap-board {
|
.seatmap-board {
|
||||||
min-width: 100%;
|
min-width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.seatmap-dxf-canvas {
|
.seatmap-dxf-canvas {
|
||||||
@@ -506,15 +561,16 @@
|
|||||||
.seatmap-dxf-frame-shell {
|
.seatmap-dxf-frame-shell {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 720px;
|
min-height: 0;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.seatmap-dxf-frame {
|
.seatmap-dxf-frame {
|
||||||
display: block;
|
display: block;
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
min-height: 720px;
|
min-height: 0;
|
||||||
border: 0;
|
border: 0;
|
||||||
background: #ffffff;
|
background: #ffffff;
|
||||||
}
|
}
|
||||||
@@ -924,11 +980,78 @@
|
|||||||
padding-right: 4px;
|
padding-right: 4px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.seatmap-member-section {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seatmap-member-section + .seatmap-member-section {
|
||||||
|
margin-top: 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seatmap-member-section-head {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seatmap-member-section-head strong {
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 900;
|
||||||
|
color: #0f172a;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seatmap-member-section-head span {
|
||||||
|
color: #64748b;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seatmap-member-section-list {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 8px;
|
||||||
|
}
|
||||||
|
|
||||||
.seatmap-member-list .seatmap-member-card {
|
.seatmap-member-list .seatmap-member-card {
|
||||||
position: relative;
|
position: relative;
|
||||||
inset: auto;
|
inset: auto;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.seatmap-member-search-card {
|
||||||
|
width: 100%;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border: 1px solid rgba(226, 232, 240, 0.9);
|
||||||
|
border-radius: 16px;
|
||||||
|
background: #fff;
|
||||||
|
cursor: pointer;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seatmap-member-badge {
|
||||||
|
flex: 0 0 auto;
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 28px;
|
||||||
|
padding: 0 10px;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #e2e8f0;
|
||||||
|
color: #475569;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 900;
|
||||||
|
}
|
||||||
|
|
||||||
|
.seatmap-member-badge.occupied {
|
||||||
|
background: rgba(220, 38, 38, 0.12);
|
||||||
|
color: #b91c1c;
|
||||||
|
}
|
||||||
|
|
||||||
.seatmap-member-card-compact {
|
.seatmap-member-card-compact {
|
||||||
position: relative;
|
position: relative;
|
||||||
inset: auto;
|
inset: auto;
|
||||||
@@ -1005,6 +1128,11 @@
|
|||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.seatmap-list-empty-inline {
|
||||||
|
min-height: 64px;
|
||||||
|
padding: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
@media (max-width: 1180px) {
|
@media (max-width: 1180px) {
|
||||||
.dashboard-header {
|
.dashboard-header {
|
||||||
flex-wrap: wrap;
|
flex-wrap: wrap;
|
||||||
@@ -1062,8 +1190,13 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.seatmap-topbar {
|
.seatmap-topbar {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
align-items: flex-start;
|
align-items: flex-start;
|
||||||
flex-direction: column;
|
}
|
||||||
|
|
||||||
|
.seatmap-office-tabs {
|
||||||
|
grid-column: 1;
|
||||||
|
justify-content: center;
|
||||||
}
|
}
|
||||||
|
|
||||||
.seatmap-dxf-canvas {
|
.seatmap-dxf-canvas {
|
||||||
@@ -1076,7 +1209,7 @@
|
|||||||
|
|
||||||
.seatmap-dxf-frame-shell,
|
.seatmap-dxf-frame-shell,
|
||||||
.seatmap-dxf-frame {
|
.seatmap-dxf-frame {
|
||||||
min-height: 620px;
|
min-height: 0;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,7 @@
|
|||||||
html,
|
html,
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
|
height: 100%;
|
||||||
min-height: 100%;
|
min-height: 100%;
|
||||||
font-family: var(--font-sans);
|
font-family: var(--font-sans);
|
||||||
color: var(--color-text);
|
color: var(--color-text);
|
||||||
@@ -53,6 +54,7 @@ body {
|
|||||||
|
|
||||||
body {
|
body {
|
||||||
min-height: 100vh;
|
min-height: 100vh;
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
button,
|
button,
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
body {
|
body {
|
||||||
overflow-x: hidden;
|
overflow-x: hidden;
|
||||||
|
overflow-y: hidden;
|
||||||
}
|
}
|
||||||
|
|
||||||
.org-canvas {
|
.org-canvas {
|
||||||
@@ -332,6 +333,29 @@ body {
|
|||||||
align-items: stretch;
|
align-items: stretch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.member-edit-layout {
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: minmax(320px, 380px) minmax(0, 1fr);
|
||||||
|
gap: 16px;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-edit-left-pane,
|
||||||
|
.member-edit-right-pane {
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-edit-left-pane {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.member-seat-field {
|
||||||
|
flex: 1 1 auto;
|
||||||
|
min-height: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.member-detail-top-row {
|
.member-detail-top-row {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
display: grid;
|
display: grid;
|
||||||
@@ -577,6 +601,10 @@ body {
|
|||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.member-edit-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
.member-detail-top-row,
|
.member-detail-top-row,
|
||||||
.member-inline-info-grid {
|
.member-inline-info-grid {
|
||||||
grid-template-columns: 1fr;
|
grid-template-columns: 1fr;
|
||||||
|
|||||||
@@ -593,8 +593,8 @@ function updateFabMenu() {
|
|||||||
const menu = document.getElementById('fab-menu');
|
const menu = document.getElementById('fab-menu');
|
||||||
let html = '<button class="fab-sub shadow-xl" data-label="리스트" onclick="openListViewModal(event)">📋</button>';
|
let html = '<button class="fab-sub shadow-xl" data-label="리스트" onclick="openListViewModal(event)">📋</button>';
|
||||||
html += '<button class="fab-sub shadow-xl" data-label="조직도 인쇄(A3)" onclick="printA3()">🖨️</button>';
|
html += '<button class="fab-sub shadow-xl" data-label="조직도 인쇄(A3)" onclick="printA3()">🖨️</button>';
|
||||||
|
html += '<button class="fab-sub shadow-xl" data-label="자리배치도" onclick="openSeatMapView(event)">🪑</button>';
|
||||||
if (isAdmin) {
|
if (isAdmin) {
|
||||||
html += '<button class="fab-sub shadow-xl" data-label="자리배치도" onclick="openSeatMapView(event)">🪑</button>';
|
|
||||||
html += '<button class="fab-sub shadow-xl" data-label="조직현황 업로드" onclick="triggerUpload(event)">⬆️</button>';
|
html += '<button class="fab-sub shadow-xl" data-label="조직현황 업로드" onclick="triggerUpload(event)">⬆️</button>';
|
||||||
html += '<button class="fab-sub shadow-xl" data-label="신규 구성원" onclick="openAddModal(event)">👤</button>';
|
html += '<button class="fab-sub shadow-xl" data-label="신규 구성원" onclick="openAddModal(event)">👤</button>';
|
||||||
html += '<button class="fab-sub shadow-xl" data-label="신규 팀/그룹/셀" onclick="openUnitAddModal(event)">🏢</button>';
|
html += '<button class="fab-sub shadow-xl" data-label="신규 팀/그룹/셀" onclick="openUnitAddModal(event)">🏢</button>';
|
||||||
@@ -606,7 +606,7 @@ function openSeatMapView(event) {
|
|||||||
event.stopPropagation();
|
event.stopPropagation();
|
||||||
document.getElementById('fab-container').classList.remove('active');
|
document.getElementById('fab-container').classList.remove('active');
|
||||||
if (window.parent && window.parent !== window) {
|
if (window.parent && window.parent !== window) {
|
||||||
window.parent.postMessage({ type: 'open-seatmap' }, '*');
|
window.parent.postMessage({ type: 'open-seatmap', readOnly: !isAdmin }, '*');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -960,6 +960,22 @@ async function hydrateMemberSeatPreview(member) {
|
|||||||
if (!frame.contentWindow) {
|
if (!frame.contentWindow) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
frame.contentWindow.postMessage({
|
||||||
|
type: 'seatmap-set-assignments',
|
||||||
|
items: Array.isArray(layout?.placements) && Array.isArray(layout?.members) && Array.isArray(layout?.slots)
|
||||||
|
? layout.placements.map((placement) => {
|
||||||
|
const slot = layout.slots.find((item) => Number(item.id) === Number(placement.seat_slot_id));
|
||||||
|
const memberItem = layout.members.find((item) => Number(item.id) === Number(placement.member_id));
|
||||||
|
if (!slot || !memberItem) return null;
|
||||||
|
return {
|
||||||
|
key: String(slot.slot_key || ''),
|
||||||
|
member_id: Number(memberItem.id),
|
||||||
|
name: memberItem.name || '-',
|
||||||
|
rank: memberItem.rank || '-',
|
||||||
|
};
|
||||||
|
}).filter(Boolean)
|
||||||
|
: [],
|
||||||
|
}, window.location.origin);
|
||||||
frame.contentWindow.postMessage({ type: 'seatmap-set-mode', mode: 'compact' }, window.location.origin);
|
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);
|
frame.contentWindow.postMessage({ type: 'seatmap-focus-chair', key: seatInfo.slotKey, padding: 2600 }, window.location.origin);
|
||||||
}, { once: true });
|
}, { once: true });
|
||||||
@@ -1023,6 +1039,7 @@ function openModal(id) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
document.getElementById('modal-title').innerText = id ? '구성원 정보 수정' : '신규 구성원 추가';
|
document.getElementById('modal-title').innerText = id ? '구성원 정보 수정' : '신규 구성원 추가';
|
||||||
|
modal.querySelector('.modal-content').classList.add('wide');
|
||||||
fieldsArea.className = 'flex flex-col w-full';
|
fieldsArea.className = 'flex flex-col w-full';
|
||||||
fieldsArea.style.maxHeight = 'none';
|
fieldsArea.style.maxHeight = 'none';
|
||||||
fieldsArea.style.overflowY = 'visible';
|
fieldsArea.style.overflowY = 'visible';
|
||||||
@@ -1078,44 +1095,47 @@ function openModal(id) {
|
|||||||
<input type="hidden" id="m-id" value="${id || ''}">
|
<input type="hidden" id="m-id" value="${id || ''}">
|
||||||
<input type="hidden" id="m-photo-hidden" value="${member['사진'] || ''}">
|
<input type="hidden" id="m-photo-hidden" value="${member['사진'] || ''}">
|
||||||
<input type="hidden" id="m-seat-hidden" value="${member['자리위치'] || ''}">
|
<input type="hidden" id="m-seat-hidden" value="${member['자리위치'] || ''}">
|
||||||
<div class="col-span-2 member-basic-top-row">
|
<div class="col-span-2 member-edit-layout">
|
||||||
<div class="member-photo-field">
|
<div class="member-edit-left-pane">
|
||||||
<label class="text-[11px] font-black text-slate-600 block">프로필 사진</label>
|
<div class="member-photo-field">
|
||||||
<div class="member-photo-upload-card member-photo-upload-card-compact">
|
<label class="text-[11px] font-black text-slate-600 block">프로필 사진</label>
|
||||||
<div class="member-photo-preview-wrap">
|
<div class="member-photo-upload-card member-photo-upload-card-compact">
|
||||||
<img id="m-photo-preview" src="${member['사진'] || getPhotoPlaceholder(member['이름'] || '')}" alt="프로필 미리보기" class="member-photo-preview">
|
<div class="member-photo-preview-wrap">
|
||||||
|
<img id="m-photo-preview" src="${member['사진'] || getPhotoPlaceholder(member['이름'] || '')}" alt="프로필 미리보기" class="member-photo-preview">
|
||||||
|
</div>
|
||||||
|
<div class="member-photo-upload-controls">
|
||||||
|
<label class="member-photo-file-label" for="m-photo-file">
|
||||||
|
<input id="m-photo-file" type="file" accept="image/png,image/jpeg,image/webp,image/gif" onchange="handlePhotoFileChange(event)">
|
||||||
|
<span>사진 파일 선택</span>
|
||||||
|
</label>
|
||||||
|
<strong id="m-photo-file-name" class="member-photo-file-name">선택된 파일 없음</strong>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="member-photo-upload-controls">
|
</div>
|
||||||
<label class="member-photo-file-label" for="m-photo-file">
|
<div class="member-seat-field">
|
||||||
<input id="m-photo-file" type="file" accept="image/png,image/jpeg,image/webp,image/gif" onchange="handlePhotoFileChange(event)">
|
<div id="member-seat-preview">${renderSeatPreviewCard({ assigned: false, seatLabel: member['자리위치'] || '', seatMapName: '자리배치도', slotKey: '' })}</div>
|
||||||
<span>사진 파일 선택</span>
|
</div>
|
||||||
</label>
|
</div>
|
||||||
<strong id="m-photo-file-name" class="member-photo-file-name">선택된 파일 없음</strong>
|
<div class="member-edit-right-pane">
|
||||||
|
<div class="member-name-field">
|
||||||
|
<label class="text-[11px] font-black text-slate-600 block">이름 (필수)</label>
|
||||||
|
<input id="m-name" value="${member['이름'] || ''}" oninput="syncPhotoPreviewFromUrl()" class="w-full bg-slate-50 p-3 rounded-xl border font-bold text-sm outline-none">
|
||||||
|
<div class="member-inline-info-grid member-inline-info-grid-edit">
|
||||||
|
<div class="member-inline-info-card">
|
||||||
|
<label>사번</label>
|
||||||
|
<input id="m-employee-id" value="${member['사번'] || ''}" class="w-full bg-white p-3 rounded-xl border font-bold text-sm outline-none">
|
||||||
|
</div>
|
||||||
|
<div class="member-inline-info-card">
|
||||||
|
<label>전화번호</label>
|
||||||
|
<input id="m-phone" value="${member['전화번호'] || ''}" class="w-full bg-white p-3 rounded-xl border font-bold text-sm outline-none">
|
||||||
|
</div>
|
||||||
|
<div class="member-inline-info-card member-inline-info-card-full">
|
||||||
|
<label>이메일</label>
|
||||||
|
<input id="m-email" value="${member['이메일'] || ''}" class="w-full bg-white p-3 rounded-xl border font-bold text-sm outline-none">
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="member-name-field">
|
|
||||||
<label class="text-[11px] font-black text-slate-600 block">이름 (필수)</label>
|
|
||||||
<input id="m-name" value="${member['이름'] || ''}" oninput="syncPhotoPreviewFromUrl()" class="w-full bg-slate-50 p-3 rounded-xl border font-bold text-sm outline-none">
|
|
||||||
<div class="member-inline-info-grid member-inline-info-grid-edit">
|
|
||||||
<div class="member-inline-info-card">
|
|
||||||
<label>사번</label>
|
|
||||||
<input id="m-employee-id" value="${member['사번'] || ''}" class="w-full bg-white p-3 rounded-xl border font-bold text-sm outline-none">
|
|
||||||
</div>
|
|
||||||
<div class="member-inline-info-card">
|
|
||||||
<label>전화번호</label>
|
|
||||||
<input id="m-phone" value="${member['전화번호'] || ''}" class="w-full bg-white p-3 rounded-xl border font-bold text-sm outline-none">
|
|
||||||
</div>
|
|
||||||
<div class="member-inline-info-card member-inline-info-card-full">
|
|
||||||
<label>이메일</label>
|
|
||||||
<input id="m-email" value="${member['이메일'] || ''}" class="w-full bg-white p-3 rounded-xl border font-bold text-sm outline-none">
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="col-span-2">
|
|
||||||
<label class="text-[11px] font-black text-slate-600 block mb-2">자리 위치</label>
|
|
||||||
${renderSeatPreviewCard(member['자리위치'] || '')}
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${orgFields}
|
${orgFields}
|
||||||
@@ -1130,6 +1150,9 @@ function openModal(id) {
|
|||||||
<button onclick="saveMember()" class="flex-1 bg-indigo-600 text-white py-3.5 rounded-xl font-bold text-sm">저장</button>
|
<button onclick="saveMember()" class="flex-1 bg-indigo-600 text-white py-3.5 rounded-xl font-bold text-sm">저장</button>
|
||||||
`;
|
`;
|
||||||
modal.style.display = 'flex';
|
modal.style.display = 'flex';
|
||||||
|
if (id) {
|
||||||
|
hydrateMemberSeatPreview(member);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function closeModal() {
|
function closeModal() {
|
||||||
|
|||||||
Reference in New Issue
Block a user