feat: add monthly history controls for organization view

This commit is contained in:
hyunho
2026-03-30 10:08:00 +09:00
parent 2053791589
commit bbebe24763
4 changed files with 178 additions and 11 deletions

View File

@@ -11,6 +11,9 @@ const currentViewTitle = document.getElementById("current-view-title");
const globalDateControls = document.getElementById("global-date-controls");
const globalStartDateInput = document.getElementById("global-start-date");
const globalEndDateInput = document.getElementById("global-end-date");
const organizationHistoryControls = document.getElementById("organization-history-controls");
const organizationMonthSelect = document.getElementById("organization-month-select");
const organizationCompareBtn = document.getElementById("organization-compare-btn");
const navButtons = Array.from(document.querySelectorAll(".header-center [data-view]"));
const organizationFrame = document.getElementById("organization-frame");
const organizationStage = document.getElementById("organization-stage");
@@ -155,6 +158,89 @@ const globalDateState = {
endDate: "2026-01-31",
};
const organizationHistoryState = {
selectedMonth: "",
currentMonth: "",
};
function padDatePart(value) {
return String(value).padStart(2, "0");
}
function getCurrentMonthValue() {
const now = new Date();
return `${now.getFullYear()}-${padDatePart(now.getMonth() + 1)}`;
}
function getMonthLabel(monthValue) {
const [, month] = String(monthValue || "").split("-");
if (!month) return "";
const monthNumber = Number(month);
if (!Number.isInteger(monthNumber) || monthNumber <= 0) return "";
return `${monthNumber}`;
}
function getMonthEndDate(monthValue) {
const [yearText, monthText] = String(monthValue || "").split("-");
const year = Number(yearText);
const month = Number(monthText);
if (!Number.isInteger(year) || !Number.isInteger(month) || month <= 0) return "";
const lastDay = new Date(year, month, 0);
return `${lastDay.getFullYear()}-${padDatePart(lastDay.getMonth() + 1)}-${padDatePart(lastDay.getDate())}`;
}
function getTodayDate() {
const now = new Date();
return `${now.getFullYear()}-${padDatePart(now.getMonth() + 1)}-${padDatePart(now.getDate())}`;
}
function syncOrganizationHistoryControls() {
if (!organizationHistoryControls) return;
const visible = currentView === "organization";
organizationHistoryControls.classList.toggle("hidden", !visible);
if (organizationCompareBtn) {
const isCurrentMonth = !organizationHistoryState.selectedMonth || organizationHistoryState.selectedMonth === organizationHistoryState.currentMonth;
organizationCompareBtn.classList.toggle("hidden", !visible || isCurrentMonth);
}
}
function initializeOrganizationMonthOptions() {
if (!organizationMonthSelect) return;
const now = new Date();
const year = now.getFullYear();
const monthCount = now.getMonth() + 1;
organizationMonthSelect.innerHTML = "";
for (let month = monthCount; month >= 1; month -= 1) {
const value = `${year}-${padDatePart(month)}`;
const option = document.createElement("option");
option.value = value;
option.textContent = month === monthCount ? `${month}월 (최신)` : `${month}`;
organizationMonthSelect.append(option);
}
organizationHistoryState.currentMonth = `${year}-${padDatePart(monthCount)}`;
organizationHistoryState.selectedMonth = organizationHistoryState.currentMonth;
organizationMonthSelect.value = organizationHistoryState.selectedMonth;
syncOrganizationHistoryControls();
}
function postOrganizationHistoryState() {
if (!organizationFrame?.contentWindow) return;
const selectedMonth = organizationHistoryState.selectedMonth || organizationHistoryState.currentMonth;
const currentMonth = organizationHistoryState.currentMonth || getCurrentMonthValue();
const isHistorical = Boolean(selectedMonth) && selectedMonth !== currentMonth;
organizationFrame.contentWindow.postMessage(
{
source: "total-control",
type: "organization-history-view",
month: selectedMonth,
asOfDate: isHistorical ? getMonthEndDate(selectedMonth) : "",
historical: isHistorical,
},
window.location.origin,
);
syncOrganizationHistoryControls();
}
function getGlobalAsOfDate() {
return globalDateState.endDate || "";
}
@@ -188,7 +274,6 @@ function shouldShowGlobalDateControls() {
return currentView === "ledger"
|| currentView === "project"
|| currentView === "team"
|| currentView === "organization"
|| currentView === "seatmap-admin"
|| currentView === "seatmap-readonly";
}
@@ -196,6 +281,7 @@ function shouldShowGlobalDateControls() {
function syncGlobalDateControlVisibility() {
if (!globalDateControls) return;
globalDateControls.classList.toggle("hidden", !shouldShowGlobalDateControls());
syncOrganizationHistoryControls();
}
function syncGlobalDateControlInputs() {
@@ -244,7 +330,6 @@ 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) {
@@ -1446,6 +1531,8 @@ function setActiveView(view) {
if (isOrganization && previousView !== "organization" && organizationFrame) {
const frameSrc = organizationFrame.dataset.src || organizationFrame.src;
organizationFrame.src = resolveAppUrl(frameSrc);
} else if (isOrganization) {
postOrganizationHistoryState();
}
if (isProject && previousView !== "project" && projectFrame) {
const frameSrc = projectFrame.dataset.src || projectFrame.src;
@@ -1547,7 +1634,6 @@ if (logoutBtn) {
if (globalStartDateInput) {
globalStartDateInput.addEventListener("change", () => {
globalDateState.startDate = globalStartDateInput.value || "";
postGlobalDateRangeToFrame(organizationFrame);
postGlobalDateRangeToFrame(projectFrame);
postGlobalDateRangeToFrame(teamFrame);
});
@@ -1556,7 +1642,6 @@ if (globalStartDateInput) {
if (globalEndDateInput) {
globalEndDateInput.addEventListener("change", () => {
globalDateState.endDate = globalEndDateInput.value || "";
postGlobalDateRangeToFrame(organizationFrame);
postGlobalDateRangeToFrame(projectFrame);
postGlobalDateRangeToFrame(teamFrame);
if (currentView === "seatmap-admin" || currentView === "seatmap-readonly") {
@@ -1567,7 +1652,7 @@ if (globalEndDateInput) {
}
organizationFrame?.addEventListener("load", () => {
postGlobalDateRangeToFrame(organizationFrame);
postOrganizationHistoryState();
});
projectFrame?.addEventListener("load", () => {
@@ -1591,6 +1676,32 @@ navButtons.forEach((button) => {
});
});
if (organizationMonthSelect) {
initializeOrganizationMonthOptions();
organizationMonthSelect.addEventListener("change", () => {
organizationHistoryState.selectedMonth = organizationMonthSelect.value || organizationHistoryState.currentMonth;
postOrganizationHistoryState();
});
}
if (organizationCompareBtn) {
organizationCompareBtn.addEventListener("click", () => {
if (!organizationFrame?.contentWindow) return;
const fromDate = getMonthEndDate(organizationHistoryState.selectedMonth);
const toDate = getTodayDate();
if (!fromDate) return;
organizationFrame.contentWindow.postMessage(
{
source: "total-control",
type: "open-history-compare",
fromDate,
toDate,
},
window.location.origin,
);
});
}
Object.values(seatMapDom).forEach((dom) => {
dom.officeTabs?.addEventListener("click", (event) => {
const button = event.target.closest("[data-seatmap-office]");

View File

@@ -13,7 +13,7 @@
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
<link href="https://fonts.googleapis.com/css2?family=Pretendard:wght@400;600;700;900&display=swap" rel="stylesheet">
<link rel="stylesheet" href="/legacy/static/common.css">
<link rel="stylesheet" href="/styles.css?v=20260326-01">
<link rel="stylesheet" href="/styles.css?v=20260330-01">
</head>
<body>
<section id="login-panel" class="login-screen">
@@ -58,6 +58,14 @@
<input id="global-end-date" type="date" aria-label="종료일">
</label>
</div>
<div id="organization-history-controls" class="header-date-controls hidden">
<span class="header-date-label">조직 기준월</span>
<label class="header-date-field">
<select id="organization-month-select" aria-label="조직 기준월"></select>
</label>
<button id="organization-compare-btn" class="ghost-button ghost-button-soft hidden" type="button">조직도 변경사항 확인</button>
</div>
</div>
<div class="header-right">
@@ -85,7 +93,7 @@
<main class="dashboard-main">
<section id="organization-stage" class="main-stage">
<div class="stage-frame">
<iframe id="organization-frame" src="/legacy/organization?v=20260326-02" data-src="/legacy/organization?v=20260326-02" title="조직도 메인 화면"></iframe>
<iframe id="organization-frame" src="/legacy/organization?v=20260330-01" data-src="/legacy/organization?v=20260330-01" title="조직도 메인 화면"></iframe>
</div>
</section>
<section id="project-stage" class="main-stage" hidden>
@@ -205,6 +213,6 @@
</main>
</section>
<script src="/app.js?v=20260326-02"></script>
<script src="/app.js?v=20260330-01"></script>
</body>
</html>

View File

@@ -306,6 +306,17 @@ body {
outline: none;
}
.header-date-field select {
border: 0;
background: transparent;
color: var(--color-text);
font-size: 12px;
font-weight: 700;
outline: none;
appearance: none;
padding-right: 8px;
}
.header-date-sep {
color: #94a3b8;
font-size: 12px;