`;
}
@@ -1027,14 +1170,13 @@ function renderUnassignedMemberCard(member, draggable) {
function renderSeatMapSearchCard(member) {
const placement = getPlacementForMember(Number(member.id));
if (!placement) return "";
- const badge = `
`;
}
@@ -1054,14 +1196,16 @@ function renderSeatMapBoard() {
const gap = Number(seatMapState.seatMap.cell_gap || 0);
const editable = seatMapState.editMode && isAdmin();
const cells = [];
+ const overlays = buildSeatMapGridTeamOverlays(rows, cols, placementMap, memberMap);
for (let rowIndex = 0; rowIndex < rows; rowIndex += 1) {
for (let colIndex = 0; colIndex < cols; colIndex += 1) {
const key = `${rowIndex}:${colIndex}`;
const placement = placementMap.get(key);
const member = placement ? memberMap.get(Number(placement.member_id)) : null;
+ const teamStyle = member ? ` style="${escapeHtml(getSeatMapTeamStyle(member))}"` : "";
cells.push(`
-
+
${escapeHtml(computeSeatLabel(rowIndex, colIndex))}
${member ? renderMemberCard(member, editable) : ""}
@@ -1072,7 +1216,7 @@ function renderSeatMapBoard() {
seatMapBoard.innerHTML = `
})
-
${cells.join("")}
+
${overlays.join("")}${cells.join("")}
`;
}
@@ -1175,6 +1319,7 @@ function renderSeatMapActions() {
function updateSeatMapDraftUi() {
renderSeatMapActions();
renderUnassignedMembers();
+ renderSeatMapMemberContext(seatMapState.selectedMemberId);
syncSeatMapViewerFrame();
}
@@ -1394,6 +1539,9 @@ function handleEmbeddedNavigationMessage(event) {
updateSeatMapDraftUi();
}
}
+ if (data.type === "seatmap-member-selected") {
+ renderSeatMapMemberContext(Number(data.memberId || 0) || null);
+ }
}
async function fetchJson(url, options) {
@@ -1437,6 +1585,7 @@ async function loadSeatMapData(force = false) {
seatMapState.placements = clonePlacements(layoutPayload.placements || []);
seatMapState.zoom = 1;
seatMapState.hoveredSlotId = null;
+ seatMapState.selectedMemberId = null;
seatMapState.editMode = canEditSeatMap();
resetSeatMapDraft();
seatMapState.loaded = true;
@@ -1450,6 +1599,7 @@ async function loadSeatMapData(force = false) {
seatMapState.placements = [];
seatMapState.zoom = 1;
seatMapState.hoveredSlotId = null;
+ seatMapState.selectedMemberId = null;
seatMapState.editMode = canEditSeatMap();
resetSeatMapDraft();
seatMapState.loaded = true;
@@ -1690,7 +1840,7 @@ function setActiveView(view) {
dbStatusFrame.src = resolveAppUrl(frameSrc);
}
if (isSeatMapAdmin || isSeatMapReadonly) {
- loadSeatMapData();
+ loadSeatMapData(previousView !== currentView);
}
notifyEmbeddedTabActivated();
}
@@ -1886,14 +2036,15 @@ Object.values(seatMapDom).forEach((dom) => {
});
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));
+ const target = event.target.closest("[data-member-id]");
+ if (!target) return;
+ if (target.classList.contains("seatmap-member-search-card")) {
+ focusSeatMapMember(Number(target.dataset.memberId), { showContext: false });
return;
}
+ renderSeatMapMemberContext(Number(target.dataset.memberId));
if (canEditSeatMap()) return;
- focusSeatMapMember(Number(button.dataset.memberId));
+ focusSeatMapMember(Number(target.dataset.memberId));
});
dom.unassigned?.addEventListener("dragover", (event) => {
if (!seatMapState.editMode) return;
@@ -1922,6 +2073,17 @@ Object.values(seatMapDom).forEach((dom) => {
if (!fitButton) return;
fitDxfSeatMapBoard();
});
+ dom.board?.addEventListener("click", (event) => {
+ const memberCard = event.target.closest(".seatmap-member-card[data-member-id]");
+ if (memberCard) {
+ renderSeatMapMemberContext(Number(memberCard.dataset.memberId));
+ return;
+ }
+ const cell = event.target.closest(".seatmap-cell[data-row][data-col]");
+ if (!cell) return;
+ const placement = getCellPlacementMap().get(`${Number(cell.dataset.row)}:${Number(cell.dataset.col)}`);
+ renderSeatMapMemberContext(placement ? Number(placement.member_id) : null);
+ });
dom.board?.addEventListener("dragover", (event) => {
if (!seatMapState.editMode) return;
const target = isSlotBasedSeatMap()
diff --git a/frontend/public/index.html b/frontend/public/index.html
index 370eed6..b395b65 100644
--- a/frontend/public/index.html
+++ b/frontend/public/index.html
@@ -181,6 +181,7 @@
구성원 검색
+
@@ -220,6 +221,7 @@
구성원 검색
+
diff --git a/frontend/public/styles.css b/frontend/public/styles.css
index 6003b4e..63bb78d 100644
--- a/frontend/public/styles.css
+++ b/frontend/public/styles.css
@@ -913,6 +913,39 @@ body {
padding: var(--seatmap-gap);
}
+.seatmap-team-overlay {
+ pointer-events: none;
+ align-self: stretch;
+ justify-self: stretch;
+ border-radius: 20px;
+ border: 2px solid color-mix(in srgb, var(--seatmap-team-accent, rgba(13, 148, 136, 0.3)) 62%, white);
+ background: linear-gradient(
+ 180deg,
+ color-mix(in srgb, var(--seatmap-team-soft, rgba(13, 148, 136, 0.12)) 100%, transparent),
+ color-mix(in srgb, var(--seatmap-team-soft, rgba(13, 148, 136, 0.08)) 90%, transparent)
+ );
+ box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.22), 0 8px 18px rgba(16, 37, 29, 0.08);
+ margin: 4px;
+ z-index: 0;
+}
+
+.seatmap-team-overlay::before {
+ content: attr(data-team-label);
+ position: absolute;
+ top: 10px;
+ left: 12px;
+ display: inline-flex;
+ align-items: center;
+ min-height: 26px;
+ padding: 0 11px;
+ border-radius: 999px;
+ background: color-mix(in srgb, var(--seatmap-team-soft, rgba(13, 148, 136, 0.14)) 84%, white);
+ color: color-mix(in srgb, var(--seatmap-team-accent, #0f766e) 86%, #10251d);
+ font-size: 11px;
+ font-weight: 900;
+ letter-spacing: 0.02em;
+}
+
.seatmap-cell {
position: relative;
min-width: 0;
@@ -920,6 +953,7 @@ body {
border: 1px dashed rgba(15, 23, 42, 0.14);
background: rgba(255, 255, 255, 0.12);
transition: border-color 0.18s ease, background 0.18s ease;
+ z-index: 1;
}
.seatmap-cell.editable:hover {
@@ -931,6 +965,13 @@ body {
background: rgba(255, 255, 255, 0.18);
}
+.seatmap-cell.occupied.team-colored {
+ border-color: color-mix(in srgb, var(--seatmap-team-accent, rgba(15, 118, 110, 0.72)) 62%, white);
+ background:
+ linear-gradient(180deg, color-mix(in srgb, var(--seatmap-team-soft, rgba(15, 118, 110, 0.14)) 86%, transparent), rgba(255, 255, 255, 0.14)),
+ rgba(255, 255, 255, 0.18);
+}
+
.seatmap-cell-label {
position: absolute;
top: 4px;
@@ -959,6 +1000,12 @@ body {
overflow: hidden;
}
+.seatmap-member-card.team-colored {
+ border-color: color-mix(in srgb, var(--seatmap-team-accent, rgba(245, 158, 11, 0.95)) 42%, rgba(255,255,255,0.4));
+ background:
+ linear-gradient(180deg, color-mix(in srgb, var(--seatmap-team-soft, rgba(245, 158, 11, 0.18)) 68%, rgba(15, 23, 42, 0.84)) 0%, rgba(15, 23, 42, 0.84) 100%);
+}
+
.seatmap-member-card.draggable {
cursor: grab;
}
@@ -1008,11 +1055,26 @@ body {
}
.seatmap-member-text em {
- color: rgba(226, 232, 240, 0.84);
+ color: rgba(255, 247, 237, 0.96);
font-size: 10px;
+ font-weight: 800;
font-style: normal;
}
+.seatmap-team-chip {
+ display: inline-flex;
+ align-items: center;
+ max-width: 100%;
+ padding: 2px 6px;
+ border-radius: 999px;
+ background: color-mix(in srgb, var(--seatmap-team-soft, rgba(245, 158, 11, 0.18)) 88%, white);
+ color: color-mix(in srgb, var(--seatmap-team-accent, #b45309) 85%, #10251d);
+ font-size: 10px;
+ font-weight: 900;
+ line-height: 1;
+ letter-spacing: 0.01em;
+}
+
.seatmap-sidebar {
height: 100%;
min-height: 0;
@@ -1195,6 +1257,11 @@ body {
text-align: left;
}
+.seatmap-member-search-card.team-colored {
+ border-color: color-mix(in srgb, var(--seatmap-team-accent, rgba(245, 158, 11, 0.24)) 34%, rgba(226, 232, 240, 0.9));
+ background: linear-gradient(180deg, color-mix(in srgb, var(--seatmap-team-soft, rgba(245, 158, 11, 0.1)) 78%, white), #fff);
+}
+
.seatmap-member-badge {
flex: 0 0 auto;
display: inline-flex;
@@ -1219,8 +1286,8 @@ body {
min-height: 42px;
padding: 10px 12px;
border-radius: 12px;
- background: #3f4658;
- border: 1px solid rgba(148, 163, 184, 0.14);
+ background: transparent;
+ border: 1px solid rgba(194, 170, 134, 0.34);
box-shadow: none;
}
@@ -1232,12 +1299,14 @@ body {
.seatmap-member-text-inline strong {
font-size: 13px;
+ color: #10251d;
}
.seatmap-member-text-inline em {
display: inline;
- color: rgba(226, 232, 240, 0.8);
+ color: #6f5b3e;
font-size: 11px;
+ font-weight: 800;
}
.seatmap-slot .seatmap-member-card {
@@ -1266,6 +1335,81 @@ body {
display: none;
}
+.seatmap-context-panel {
+ display: grid;
+ gap: 10px;
+ padding: 14px;
+ border: 1px solid rgba(194, 170, 134, 0.28);
+ border-radius: 18px;
+ background: linear-gradient(180deg, rgba(255, 252, 247, 0.96), rgba(246, 239, 227, 0.92));
+ box-shadow: 0 14px 28px rgba(16, 37, 29, 0.08);
+}
+
+.seatmap-context-panel.hidden {
+ display: none;
+}
+
+.seatmap-context-head {
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ gap: 10px;
+}
+
+.seatmap-context-title {
+ display: grid;
+ gap: 4px;
+}
+
+.seatmap-context-title strong {
+ color: #10251d;
+ font-size: 14px;
+ font-weight: 900;
+}
+
+.seatmap-context-title span {
+ color: #6b7280;
+ font-size: 11px;
+ font-weight: 700;
+}
+
+.seatmap-context-badge {
+ display: inline-flex;
+ align-items: center;
+ min-height: 28px;
+ padding: 0 10px;
+ border-radius: 999px;
+ background: color-mix(in srgb, var(--seatmap-team-soft, rgba(245, 158, 11, 0.14)) 88%, white);
+ color: color-mix(in srgb, var(--seatmap-team-accent, #b45309) 84%, #10251d);
+ font-size: 11px;
+ font-weight: 900;
+}
+
+.seatmap-context-tree {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+}
+
+.seatmap-context-node {
+ display: inline-flex;
+ align-items: center;
+ min-height: 30px;
+ padding: 0 11px;
+ border-radius: 999px;
+ background: rgba(255, 255, 255, 0.84);
+ border: 1px solid rgba(194, 170, 134, 0.28);
+ color: #30423a;
+ font-size: 11px;
+ font-weight: 800;
+}
+
+.seatmap-context-empty {
+ color: #7b7b6c;
+ font-size: 12px;
+ line-height: 1.5;
+}
+
.seatmap-dxf-stage {
cursor: grab;
}
diff --git a/scripts/check_8081_smoke.sh b/scripts/check_8081_smoke.sh
new file mode 100644
index 0000000..56d8510
--- /dev/null
+++ b/scripts/check_8081_smoke.sh
@@ -0,0 +1,53 @@
+#!/usr/bin/env bash
+set -euo pipefail
+
+ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
+cd "$ROOT_DIR"
+
+echo "[smoke] checking dev containers"
+docker ps --format '{{.Names}}' | grep -qx 'mh-dashboard-organization-dev-backend-1'
+docker ps --format '{{.Names}}' | grep -qx 'mh-dashboard-organization-dev-proxy-1'
+
+echo "[smoke] running 8081 endpoint checks"
+docker exec mh-dashboard-organization-dev-backend-1 python - <<'PY'
+import sys
+import urllib.request
+
+
+checks = [
+ ("health", "http://127.0.0.1:8000/api/health", b'"status"'),
+ ("db-status-api", "http://127.0.0.1:8000/api/admin/db-status", b'"tables"'),
+ ("ledger-default-api", "http://127.0.0.1:8000/api/integration/business-ledger-default", b'PK'),
+ ("legacy-organization", "http://127.0.0.1:8000/legacy/organization", b'organization'),
+ ("payment", "http://127.0.0.1:8000/integrations/payment", b'const App = () =>'),
+ ("ledger", "http://127.0.0.1:8000/integrations/ledger", b'xlsx.full.min.js'),
+ ("mh", "http://127.0.0.1:8000/integrations/mh", b'const App = () =>'),
+ ("proxy-root", "http://proxy/", b'app.js'),
+ ("proxy-db-status", "http://proxy/db-status.html", b'전체 테이블 현황'),
+]
+
+failed = []
+
+for name, url, needle in checks:
+ try:
+ with urllib.request.urlopen(url, timeout=8) as response:
+ body = response.read()
+ status = getattr(response, "status", response.getcode())
+ if status != 200:
+ failed.append(f"{name}: unexpected status {status}")
+ continue
+ if needle not in body:
+ failed.append(f"{name}: missing expected marker {needle!r}")
+ continue
+ print(f"[ok] {name} -> {status}")
+ except Exception as exc:
+ failed.append(f"{name}: {exc}")
+
+if failed:
+ print("[smoke] failures detected:")
+ for item in failed:
+ print(f" - {item}")
+ sys.exit(1)
+
+print("[smoke] all checks passed")
+PY