feat: wire as-of date into organization and seatmap views

This commit is contained in:
hyunho
2026-03-30 09:32:06 +09:00
parent 6e55b99e9a
commit b735a4cdd1
4 changed files with 73 additions and 9 deletions

View File

@@ -673,7 +673,7 @@ def ensure_history_backfill(cur) -> None:
m.id, m.name, COALESCE(m.company, ''), COALESCE(m.rank, ''), COALESCE(m.role, ''), m.id, m.name, COALESCE(m.company, ''), COALESCE(m.rank, ''), COALESCE(m.role, ''),
COALESCE(m.department, ''), COALESCE(m.grp, ''), COALESCE(m.division, ''), COALESCE(m.team, ''), COALESCE(m.cell, ''), COALESCE(m.department, ''), COALESCE(m.grp, ''), COALESCE(m.division, ''), COALESCE(m.team, ''), COALESCE(m.cell, ''),
COALESCE(m.work_status, ''), COALESCE(m.work_time, ''), COALESCE(m.phone, ''), COALESCE(m.email, ''), COALESCE(m.photo_url, ''), COALESCE(m.work_status, ''), COALESCE(m.work_time, ''), COALESCE(m.phone, ''), COALESCE(m.email, ''), COALESCE(m.photo_url, ''),
COALESCE(m.updated_at, m.created_at, NOW()), NULL, %s, NULL, 'initial-backfill' TIMESTAMPTZ '1970-01-01 00:00:00+00', NULL, %s, NULL, 'initial-backfill'
FROM members AS m FROM members AS m
WHERE NOT EXISTS ( WHERE NOT EXISTS (
SELECT 1 SELECT 1
@@ -692,7 +692,7 @@ def ensure_history_backfill(cur) -> None:
) )
SELECT SELECT
sp.member_id, sp.seat_map_id, sp.seat_slot_id, COALESCE(sp.seat_label, ''), sp.member_id, sp.seat_map_id, sp.seat_slot_id, COALESCE(sp.seat_label, ''),
COALESCE(sp.updated_at, NOW()), NULL, %s, NULL, 'initial-backfill' TIMESTAMPTZ '1970-01-01 00:00:00+00', NULL, %s, NULL, 'initial-backfill'
FROM seat_positions AS sp FROM seat_positions AS sp
WHERE NOT EXISTS ( WHERE NOT EXISTS (
SELECT 1 SELECT 1
@@ -702,3 +702,23 @@ def ensure_history_backfill(cur) -> None:
""", """,
(revision_id,), (revision_id,),
) )
cur.execute(
"""
UPDATE member_versions
SET valid_from = TIMESTAMPTZ '1970-01-01 00:00:00+00'
WHERE revision_no = %s
AND change_reason = 'initial-backfill'
""",
(revision_id,),
)
cur.execute(
"""
UPDATE seat_assignment_versions
SET valid_from = TIMESTAMPTZ '1970-01-01 00:00:00+00'
WHERE revision_no = %s
AND change_reason = 'initial-backfill'
""",
(revision_id,),
)

View File

@@ -4308,8 +4308,8 @@ def get_seat_layout(seat_map_id: int, as_of: str | None = None) -> dict[str, obj
@app.get("/api/seat-maps/{seat_map_id}/viewer") @app.get("/api/seat-maps/{seat_map_id}/viewer")
def get_seat_map_viewer(seat_map_id: int) -> HTMLResponse: def get_seat_map_viewer(seat_map_id: int, as_of: str | None = None) -> HTMLResponse:
layout = fetch_seat_layout(seat_map_id) layout = fetch_seat_layout(seat_map_id, parse_as_of(as_of))
seat_map = layout.get("seat_map") or {} seat_map = layout.get("seat_map") or {}
if seat_map.get("source_type") not in {"dxf", "fixed_html"}: if seat_map.get("source_type") not in {"dxf", "fixed_html"}:
raise HTTPException(status_code=400, detail="Viewer is only available for supported seat maps.") raise HTTPException(status_code=400, detail="Viewer is only available for supported seat maps.")

View File

@@ -155,6 +155,10 @@ const globalDateState = {
endDate: "2026-01-31", endDate: "2026-01-31",
}; };
function getGlobalAsOfDate() {
return globalDateState.endDate || "";
}
function getSession() { function getSession() {
try { try {
return JSON.parse(sessionStorage.getItem(sessionKey) || "null"); return JSON.parse(sessionStorage.getItem(sessionKey) || "null");
@@ -181,7 +185,12 @@ function buildAuthHeaders(headers) {
} }
function shouldShowGlobalDateControls() { function shouldShowGlobalDateControls() {
return currentView === "ledger" || currentView === "project" || currentView === "team" || currentView === "organization"; return currentView === "ledger"
|| currentView === "project"
|| currentView === "team"
|| currentView === "organization"
|| currentView === "seatmap-admin"
|| currentView === "seatmap-readonly";
} }
function syncGlobalDateControlVisibility() { function syncGlobalDateControlVisibility() {
@@ -208,6 +217,12 @@ function postGlobalDateRangeToFrame(frame) {
frame.contentWindow.postMessage(getGlobalDateRangePayload(), window.location.origin); frame.contentWindow.postMessage(getGlobalDateRangePayload(), window.location.origin);
} }
function buildAsOfQuery() {
const asOf = getGlobalAsOfDate();
if (!asOf) return "";
return `?as_of=${encodeURIComponent(asOf)}`;
}
function notifyEmbeddedTabActivated() { function notifyEmbeddedTabActivated() {
if (currentView === "project" && projectFrame?.contentWindow) { if (currentView === "project" && projectFrame?.contentWindow) {
projectFrame.contentWindow.postMessage({ source: "total-control", type: "tab-activated", tab: "project" }, window.location.origin); projectFrame.contentWindow.postMessage({ source: "total-control", type: "tab-activated", tab: "project" }, window.location.origin);
@@ -229,6 +244,7 @@ async function ensureGlobalDateRangeLoaded() {
globalDateState.endDate = ends.length ? String(ends[ends.length - 1]).slice(0, 10) : ""; globalDateState.endDate = ends.length ? String(ends[ends.length - 1]).slice(0, 10) : "";
globalDateState.loaded = true; globalDateState.loaded = true;
syncGlobalDateControlInputs(); syncGlobalDateControlInputs();
postGlobalDateRangeToFrame(organizationFrame);
postGlobalDateRangeToFrame(projectFrame); postGlobalDateRangeToFrame(projectFrame);
postGlobalDateRangeToFrame(teamFrame); postGlobalDateRangeToFrame(teamFrame);
} catch (error) { } catch (error) {
@@ -858,7 +874,7 @@ function renderDxfSeatMapBoard() {
seatMapBoard.innerHTML = `<div class="seatmap-empty-card"><strong>DXF 뷰어 데이터를 준비하지 못했습니다.</strong></div>`; seatMapBoard.innerHTML = `<div class="seatmap-empty-card"><strong>DXF 뷰어 데이터를 준비하지 못했습니다.</strong></div>`;
return; return;
} }
const viewerUrl = resolveAppUrl(`/api/seat-maps/${seatMapState.seatMap.id}/viewer`); const viewerUrl = resolveAppUrl(`/api/seat-maps/${seatMapState.seatMap.id}/viewer${buildAsOfQuery()}`);
seatMapBoard.innerHTML = ` seatMapBoard.innerHTML = `
<div class="seatmap-dxf-frame-shell"> <div class="seatmap-dxf-frame-shell">
<div class="seatmap-dxf-drop-overlay" data-seatmap-drop-overlay></div> <div class="seatmap-dxf-drop-overlay" data-seatmap-drop-overlay></div>
@@ -1201,7 +1217,7 @@ async function loadSeatMapData(force = false) {
const office = getCurrentSeatMapOffice(); const office = getCurrentSeatMapOffice();
const activePayload = await fetchJson(`/api/seat-maps/active?office_key=${encodeURIComponent(office.key)}`); const activePayload = await fetchJson(`/api/seat-maps/active?office_key=${encodeURIComponent(office.key)}`);
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${buildAsOfQuery()}`);
seatMapState.seatMap = { seatMapState.seatMap = {
...(layoutPayload.seat_map || {}), ...(layoutPayload.seat_map || {}),
viewer_data: layoutPayload.viewer_data || null, viewer_data: layoutPayload.viewer_data || null,
@@ -1531,6 +1547,7 @@ if (logoutBtn) {
if (globalStartDateInput) { if (globalStartDateInput) {
globalStartDateInput.addEventListener("change", () => { globalStartDateInput.addEventListener("change", () => {
globalDateState.startDate = globalStartDateInput.value || ""; globalDateState.startDate = globalStartDateInput.value || "";
postGlobalDateRangeToFrame(organizationFrame);
postGlobalDateRangeToFrame(projectFrame); postGlobalDateRangeToFrame(projectFrame);
postGlobalDateRangeToFrame(teamFrame); postGlobalDateRangeToFrame(teamFrame);
}); });
@@ -1539,11 +1556,20 @@ if (globalStartDateInput) {
if (globalEndDateInput) { if (globalEndDateInput) {
globalEndDateInput.addEventListener("change", () => { globalEndDateInput.addEventListener("change", () => {
globalDateState.endDate = globalEndDateInput.value || ""; globalDateState.endDate = globalEndDateInput.value || "";
postGlobalDateRangeToFrame(organizationFrame);
postGlobalDateRangeToFrame(projectFrame); postGlobalDateRangeToFrame(projectFrame);
postGlobalDateRangeToFrame(teamFrame); postGlobalDateRangeToFrame(teamFrame);
if (currentView === "seatmap-admin" || currentView === "seatmap-readonly") {
seatMapState.loaded = false;
loadSeatMapData(true);
}
}); });
} }
organizationFrame?.addEventListener("load", () => {
postGlobalDateRangeToFrame(organizationFrame);
});
projectFrame?.addEventListener("load", () => { projectFrame?.addEventListener("load", () => {
postGlobalDateRangeToFrame(projectFrame); postGlobalDateRangeToFrame(projectFrame);
if (currentView === "project") { if (currentView === "project") {

View File

@@ -7,6 +7,7 @@ let isListMode = false;
let emptyStateMessage = '서버에 조직 데이터가 없습니다. 상단의 업로드 버튼으로 초기 데이터를 넣어주세요.'; let emptyStateMessage = '서버에 조직 데이터가 없습니다. 상단의 업로드 버튼으로 초기 데이터를 넣어주세요.';
let photoPreviewObjectUrl = null; let photoPreviewObjectUrl = null;
let seatMapLayoutCache = null; let seatMapLayoutCache = null;
let activeAsOfDate = '';
const seatMapOfficeKeys = ['technical-development-center', 'hanmac-building-6f', 'hanmac-building-7f']; const seatMapOfficeKeys = ['technical-development-center', 'hanmac-building-6f', 'hanmac-building-7f'];
const levelOrder = ['부서', '그룹', '디비전', '팀', '셀']; const levelOrder = ['부서', '그룹', '디비전', '팀', '셀'];
@@ -117,6 +118,14 @@ async function apiFetch(url, options = {}) {
return payload; return payload;
} }
function withAsOf(url) {
if (!activeAsOfDate) {
return url;
}
const separator = url.includes('?') ? '&' : '?';
return `${url}${separator}as_of=${encodeURIComponent(activeAsOfDate)}`;
}
async function uploadProfilePhoto(file, memberName) { async function uploadProfilePhoto(file, memberName) {
const formData = new FormData(); const formData = new FormData();
formData.append('file', file); formData.append('file', file);
@@ -140,7 +149,7 @@ async function loadMembers(message) {
if (message) { if (message) {
emptyStateMessage = message; emptyStateMessage = message;
} }
const payload = await apiFetch('/api/members'); const payload = await apiFetch(withAsOf('/api/members'));
setMembers(payload.items || []); setMembers(payload.items || []);
if (!members.length) { if (!members.length) {
emptyStateMessage = '서버에 조직 데이터가 없습니다. 상단의 업로드 버튼으로 초기 데이터를 넣어주세요.'; emptyStateMessage = '서버에 조직 데이터가 없습니다. 상단의 업로드 버튼으로 초기 데이터를 넣어주세요.';
@@ -160,7 +169,7 @@ async function loadSeatMapLayouts(force = false) {
if (!seatMap?.id) { if (!seatMap?.id) {
return null; return null;
} }
return await apiFetch(`/api/seat-maps/${seatMap.id}/layout`); return await apiFetch(withAsOf(`/api/seat-maps/${seatMap.id}/layout`));
} catch { } catch {
return null; return null;
} }
@@ -666,6 +675,15 @@ window.addEventListener('message', (event) => {
if (!data || typeof data !== 'object') { if (!data || typeof data !== 'object') {
return; return;
} }
if (data.type === 'date-range') {
const nextAsOfDate = String(data.endDate || '').slice(0, 10);
if (nextAsOfDate !== activeAsOfDate) {
activeAsOfDate = nextAsOfDate;
seatMapLayoutCache = null;
loadMembers().catch(() => { });
}
return;
}
if (data.type === 'seatmap-layout-updated') { if (data.type === 'seatmap-layout-updated') {
handleSeatMapLayoutUpdated(); handleSeatMapLayoutUpdated();
} }