feat: integrate dashboard modules and document history

This commit is contained in:
hyunho
2026-03-26 18:03:07 +09:00
parent baf6019c1c
commit 61b5638cb1
22 changed files with 14252 additions and 79 deletions

View File

@@ -8,9 +8,16 @@ const logoutBtn = document.getElementById("logout-btn");
const userBadge = document.getElementById("user-badge");
const userPopover = document.getElementById("user-popover");
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 navButtons = Array.from(document.querySelectorAll(".header-center [data-view]"));
const organizationFrame = document.getElementById("organization-frame");
const organizationStage = document.getElementById("organization-stage");
const projectFrame = document.getElementById("project-frame");
const projectStage = document.getElementById("project-stage");
const teamFrame = document.getElementById("team-frame");
const teamStage = document.getElementById("team-stage");
const seatMapAdminStage = document.getElementById("seatmap-admin-stage");
const seatMapReadonlyStage = document.getElementById("seatmap-readonly-stage");
const emptyStage = document.getElementById("empty-stage");
@@ -142,6 +149,11 @@ const seatMapState = {
};
let currentView = "organization";
const globalDateState = {
loaded: true,
startDate: "2026-01-01",
endDate: "2026-01-31",
};
function getSession() {
try {
@@ -159,6 +171,71 @@ function clearSession() {
sessionStorage.removeItem(sessionKey);
}
function buildAuthHeaders(headers) {
const nextHeaders = new Headers(headers || {});
const token = getSession()?.token;
if (token && !nextHeaders.has("Authorization")) {
nextHeaders.set("Authorization", `Bearer ${token}`);
}
return nextHeaders;
}
function shouldShowGlobalDateControls() {
return currentView === "ledger" || currentView === "project" || currentView === "team" || currentView === "organization";
}
function syncGlobalDateControlVisibility() {
if (!globalDateControls) return;
globalDateControls.classList.toggle("hidden", !shouldShowGlobalDateControls());
}
function syncGlobalDateControlInputs() {
if (globalStartDateInput) globalStartDateInput.value = globalDateState.startDate || "";
if (globalEndDateInput) globalEndDateInput.value = globalDateState.endDate || "";
}
function getGlobalDateRangePayload() {
return {
source: "total-control",
type: "date-range",
startDate: globalDateState.startDate || "",
endDate: globalDateState.endDate || "",
};
}
function postGlobalDateRangeToFrame(frame) {
if (!frame?.contentWindow || !globalDateState.loaded) return;
frame.contentWindow.postMessage(getGlobalDateRangePayload(), window.location.origin);
}
function notifyEmbeddedTabActivated() {
if (currentView === "project" && projectFrame?.contentWindow) {
projectFrame.contentWindow.postMessage({ source: "total-control", type: "tab-activated", tab: "project" }, window.location.origin);
}
if (currentView === "team" && teamFrame?.contentWindow) {
teamFrame.contentWindow.postMessage({ source: "total-control", type: "tab-activated", tab: "mh" }, window.location.origin);
}
}
async function ensureGlobalDateRangeLoaded() {
if (globalDateState.loaded) return;
try {
const payload = await fetchJson("/api/integration/summary");
const work = payload?.date_ranges?.work || {};
const voucher = payload?.date_ranges?.voucher || {};
const starts = [work.min_work_date, voucher.min_voucher_date].filter(Boolean).sort();
const ends = [work.max_work_date, voucher.max_voucher_date].filter(Boolean).sort();
globalDateState.startDate = starts[0] ? String(starts[0]).slice(0, 10) : "";
globalDateState.endDate = ends.length ? String(ends[ends.length - 1]).slice(0, 10) : "";
globalDateState.loaded = true;
syncGlobalDateControlInputs();
postGlobalDateRangeToFrame(projectFrame);
postGlobalDateRangeToFrame(teamFrame);
} catch (error) {
console.error("공통 기간을 불러오지 못했습니다.", error);
}
}
function hideUserPopover() {
userPopover?.classList.add("hidden");
}
@@ -1063,7 +1140,11 @@ function handleEmbeddedNavigationMessage(event) {
}
async function fetchJson(url, options) {
const response = await fetch(resolveAppUrl(url), options);
const requestOptions = {
...options,
headers: buildAuthHeaders(options?.headers),
};
const response = await fetch(resolveAppUrl(url), requestOptions);
let payload = null;
try {
payload = await response.json();
@@ -1220,6 +1301,9 @@ async function saveSeatLayout() {
body: JSON.stringify({ placements: seatMapState.draftPlacements }),
});
await loadSeatMapData(true);
if (organizationFrame?.contentWindow) {
organizationFrame.contentWindow.postMessage({ type: "seatmap-layout-updated" }, window.location.origin);
}
setSeatMapStatus("자리배치를 저장했습니다.", "success");
} catch (error) {
setSeatMapStatus(error.message || "자리배치도 저장에 실패했습니다.", "error");
@@ -1285,6 +1369,10 @@ function setActiveView(view) {
if (currentViewTitle) {
currentViewTitle.textContent = viewLabels[currentView];
}
syncGlobalDateControlVisibility();
if (shouldShowGlobalDateControls()) {
ensureGlobalDateRangeLoaded();
}
navButtons.forEach((button) => {
const active = button.dataset.view === currentView;
@@ -1293,12 +1381,22 @@ function setActiveView(view) {
});
const isOrganization = currentView === "organization";
const isProject = currentView === "project";
const isTeam = currentView === "team";
const isSeatMapAdmin = currentView === "seatmap-admin";
const isSeatMapReadonly = currentView === "seatmap-readonly";
if (organizationStage) {
organizationStage.hidden = !isOrganization;
organizationStage.style.display = isOrganization ? "flex" : "none";
}
if (projectStage) {
projectStage.hidden = !isProject;
projectStage.style.display = isProject ? "flex" : "none";
}
if (teamStage) {
teamStage.hidden = !isTeam;
teamStage.style.display = isTeam ? "flex" : "none";
}
if (seatMapAdminStage) {
seatMapAdminStage.hidden = !isSeatMapAdmin;
seatMapAdminStage.style.display = isSeatMapAdmin ? "flex" : "none";
@@ -1308,7 +1406,7 @@ function setActiveView(view) {
seatMapReadonlyStage.style.display = isSeatMapReadonly ? "flex" : "none";
}
if (emptyStage) {
const showEmpty = !isOrganization && !isSeatMapAdmin && !isSeatMapReadonly;
const showEmpty = !isOrganization && !isProject && !isTeam && !isSeatMapAdmin && !isSeatMapReadonly;
emptyStage.hidden = !showEmpty;
emptyStage.style.display = showEmpty ? "flex" : "none";
}
@@ -1317,9 +1415,22 @@ function setActiveView(view) {
const frameSrc = organizationFrame.dataset.src || organizationFrame.src;
organizationFrame.src = resolveAppUrl(frameSrc);
}
if (isProject && previousView !== "project" && projectFrame) {
const frameSrc = projectFrame.dataset.src || projectFrame.src;
projectFrame.src = resolveAppUrl(frameSrc);
} else if (isProject) {
postGlobalDateRangeToFrame(projectFrame);
}
if (isTeam && previousView !== "team" && teamFrame) {
const frameSrc = teamFrame.dataset.src || teamFrame.src;
teamFrame.src = resolveAppUrl(frameSrc);
} else if (isTeam) {
postGlobalDateRangeToFrame(teamFrame);
}
if (isSeatMapAdmin || isSeatMapReadonly) {
loadSeatMapData();
}
notifyEmbeddedTabActivated();
}
function renderAuth() {
@@ -1329,7 +1440,7 @@ function renderAuth() {
dashboardPanel.classList.toggle("hidden", !authenticated);
if (authenticated) {
const displayName = session.user.display_name || "접속자";
const rank = "-";
const rank = session.user.rank || "-";
const employeeId = session.user.username || "-";
userBadge.innerHTML = `<span class="user-chip-icon">◎</span><span class="user-chip-text"><strong>${escapeHtml(displayName)}</strong><em>${escapeHtml(rank)}</em></span><span class="user-chip-caret" aria-hidden="true">▾</span>`;
userBadge.title = `${displayName} / -`;
@@ -1363,7 +1474,7 @@ if (loginForm) {
loginMessage.textContent = "로그인 처리 중입니다.";
const formData = new FormData(loginForm);
try {
const payload = await fetchJson("/api/mock-login", {
const payload = await fetchJson("/api/auth/login", {
method: "POST",
body: formData,
});
@@ -1388,14 +1499,51 @@ if (userBadge) {
}
if (logoutBtn) {
logoutBtn.addEventListener("click", (event) => {
logoutBtn.addEventListener("click", async (event) => {
event.stopPropagation();
try {
await fetchJson("/api/auth/logout", {
method: "POST",
});
} catch {
// Ignore logout API errors and clear the local session regardless.
}
clearSession();
hideUserPopover();
renderAuth();
});
}
if (globalStartDateInput) {
globalStartDateInput.addEventListener("change", () => {
globalDateState.startDate = globalStartDateInput.value || "";
postGlobalDateRangeToFrame(projectFrame);
postGlobalDateRangeToFrame(teamFrame);
});
}
if (globalEndDateInput) {
globalEndDateInput.addEventListener("change", () => {
globalDateState.endDate = globalEndDateInput.value || "";
postGlobalDateRangeToFrame(projectFrame);
postGlobalDateRangeToFrame(teamFrame);
});
}
projectFrame?.addEventListener("load", () => {
postGlobalDateRangeToFrame(projectFrame);
if (currentView === "project") {
notifyEmbeddedTabActivated();
}
});
teamFrame?.addEventListener("load", () => {
postGlobalDateRangeToFrame(teamFrame);
if (currentView === "team") {
notifyEmbeddedTabActivated();
}
});
navButtons.forEach((button) => {
button.addEventListener("click", () => {
hideUserPopover();
@@ -1549,6 +1697,7 @@ document.addEventListener("click", () => {
window.addEventListener("message", handleEmbeddedNavigationMessage);
syncGlobalDateControlInputs();
setActiveView(currentView);
renderAuth();