diff --git a/backend/app/db.py b/backend/app/db.py index c696db5..cb97550 100755 --- a/backend/app/db.py +++ b/backend/app/db.py @@ -673,7 +673,7 @@ def ensure_history_backfill(cur) -> None: 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.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 WHERE NOT EXISTS ( SELECT 1 @@ -692,7 +692,7 @@ def ensure_history_backfill(cur) -> None: ) SELECT 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 WHERE NOT EXISTS ( SELECT 1 @@ -702,3 +702,23 @@ def ensure_history_backfill(cur) -> None: """, (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,), + ) diff --git a/backend/app/main.py b/backend/app/main.py index b786a5d..7b48295 100755 --- a/backend/app/main.py +++ b/backend/app/main.py @@ -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") -def get_seat_map_viewer(seat_map_id: int) -> HTMLResponse: - layout = fetch_seat_layout(seat_map_id) +def get_seat_map_viewer(seat_map_id: int, as_of: str | None = None) -> HTMLResponse: + layout = fetch_seat_layout(seat_map_id, parse_as_of(as_of)) seat_map = layout.get("seat_map") or {} 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.") diff --git a/frontend/public/app.js b/frontend/public/app.js index 5d34fe3..c2413b3 100644 --- a/frontend/public/app.js +++ b/frontend/public/app.js @@ -155,6 +155,10 @@ const globalDateState = { endDate: "2026-01-31", }; +function getGlobalAsOfDate() { + return globalDateState.endDate || ""; +} + function getSession() { try { return JSON.parse(sessionStorage.getItem(sessionKey) || "null"); @@ -181,7 +185,12 @@ function buildAuthHeaders(headers) { } 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() { @@ -208,6 +217,12 @@ function postGlobalDateRangeToFrame(frame) { frame.contentWindow.postMessage(getGlobalDateRangePayload(), window.location.origin); } +function buildAsOfQuery() { + const asOf = getGlobalAsOfDate(); + if (!asOf) return ""; + return `?as_of=${encodeURIComponent(asOf)}`; +} + function notifyEmbeddedTabActivated() { if (currentView === "project" && projectFrame?.contentWindow) { 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.loaded = true; syncGlobalDateControlInputs(); + postGlobalDateRangeToFrame(organizationFrame); postGlobalDateRangeToFrame(projectFrame); postGlobalDateRangeToFrame(teamFrame); } catch (error) { @@ -858,7 +874,7 @@ function renderDxfSeatMapBoard() { seatMapBoard.innerHTML = `
DXF 뷰어 데이터를 준비하지 못했습니다.
`; return; } - const viewerUrl = resolveAppUrl(`/api/seat-maps/${seatMapState.seatMap.id}/viewer`); + const viewerUrl = resolveAppUrl(`/api/seat-maps/${seatMapState.seatMap.id}/viewer${buildAsOfQuery()}`); seatMapBoard.innerHTML = `
@@ -1201,7 +1217,7 @@ async function loadSeatMapData(force = false) { const office = getCurrentSeatMapOffice(); const activePayload = await fetchJson(`/api/seat-maps/active?office_key=${encodeURIComponent(office.key)}`); 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 = { ...(layoutPayload.seat_map || {}), viewer_data: layoutPayload.viewer_data || null, @@ -1531,6 +1547,7 @@ if (logoutBtn) { if (globalStartDateInput) { globalStartDateInput.addEventListener("change", () => { globalDateState.startDate = globalStartDateInput.value || ""; + postGlobalDateRangeToFrame(organizationFrame); postGlobalDateRangeToFrame(projectFrame); postGlobalDateRangeToFrame(teamFrame); }); @@ -1539,11 +1556,20 @@ if (globalStartDateInput) { if (globalEndDateInput) { globalEndDateInput.addEventListener("change", () => { globalDateState.endDate = globalEndDateInput.value || ""; + postGlobalDateRangeToFrame(organizationFrame); postGlobalDateRangeToFrame(projectFrame); postGlobalDateRangeToFrame(teamFrame); + if (currentView === "seatmap-admin" || currentView === "seatmap-readonly") { + seatMapState.loaded = false; + loadSeatMapData(true); + } }); } +organizationFrame?.addEventListener("load", () => { + postGlobalDateRangeToFrame(organizationFrame); +}); + projectFrame?.addEventListener("load", () => { postGlobalDateRangeToFrame(projectFrame); if (currentView === "project") { diff --git a/legacy/static/organization.js b/legacy/static/organization.js index deed61a..893168b 100644 --- a/legacy/static/organization.js +++ b/legacy/static/organization.js @@ -7,6 +7,7 @@ let isListMode = false; let emptyStateMessage = '서버에 조직 데이터가 없습니다. 상단의 업로드 버튼으로 초기 데이터를 넣어주세요.'; let photoPreviewObjectUrl = null; let seatMapLayoutCache = null; +let activeAsOfDate = ''; const seatMapOfficeKeys = ['technical-development-center', 'hanmac-building-6f', 'hanmac-building-7f']; const levelOrder = ['부서', '그룹', '디비전', '팀', '셀']; @@ -117,6 +118,14 @@ async function apiFetch(url, options = {}) { 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) { const formData = new FormData(); formData.append('file', file); @@ -140,7 +149,7 @@ async function loadMembers(message) { if (message) { emptyStateMessage = message; } - const payload = await apiFetch('/api/members'); + const payload = await apiFetch(withAsOf('/api/members')); setMembers(payload.items || []); if (!members.length) { emptyStateMessage = '서버에 조직 데이터가 없습니다. 상단의 업로드 버튼으로 초기 데이터를 넣어주세요.'; @@ -160,7 +169,7 @@ async function loadSeatMapLayouts(force = false) { if (!seatMap?.id) { return null; } - return await apiFetch(`/api/seat-maps/${seatMap.id}/layout`); + return await apiFetch(withAsOf(`/api/seat-maps/${seatMap.id}/layout`)); } catch { return null; } @@ -666,6 +675,15 @@ window.addEventListener('message', (event) => { if (!data || typeof data !== 'object') { 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') { handleSeatMapLayoutUpdated(); }