feat: integrate dashboard modules and document history
This commit is contained in:
@@ -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();
|
||||
|
||||
|
||||
@@ -8,7 +8,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=20260325-11">
|
||||
<link rel="stylesheet" href="/styles.css?v=20260326-01">
|
||||
</head>
|
||||
<body>
|
||||
<section id="login-panel" class="login-screen">
|
||||
@@ -37,35 +37,60 @@
|
||||
|
||||
<section id="dashboard-panel" class="dashboard-shell hidden">
|
||||
<header class="dashboard-header">
|
||||
<div class="brand-block">
|
||||
<p class="eyebrow">MH Dashboard</p>
|
||||
<h2 id="current-view-title">조직 현황</h2>
|
||||
<div class="header-left">
|
||||
<div class="brand-block">
|
||||
<p class="eyebrow">MH Dashboard</p>
|
||||
<h2 id="current-view-title">조직 현황</h2>
|
||||
</div>
|
||||
|
||||
<div id="global-date-controls" class="header-date-controls hidden">
|
||||
<span class="header-date-label">기간</span>
|
||||
<label class="header-date-field">
|
||||
<input id="global-start-date" type="date" aria-label="시작일">
|
||||
</label>
|
||||
<span class="header-date-sep">~</span>
|
||||
<label class="header-date-field">
|
||||
<input id="global-end-date" type="date" aria-label="종료일">
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="header-center">
|
||||
<button class="nav-pill" type="button" data-view="ledger">사업관리대장</button>
|
||||
<button class="nav-pill" type="button" data-view="project">프로젝트별 분석</button>
|
||||
<button class="nav-pill" type="button" data-view="team">팀/개인별 분석</button>
|
||||
<button class="nav-pill active" type="button" data-view="organization">조직 현황</button>
|
||||
</div>
|
||||
<div class="header-right">
|
||||
<div class="header-center">
|
||||
<button class="nav-pill" type="button" data-view="ledger">사업관리대장</button>
|
||||
<button class="nav-pill" type="button" data-view="project">프로젝트별 분석</button>
|
||||
<button class="nav-pill" type="button" data-view="team">팀/개인별 분석</button>
|
||||
<button class="nav-pill active" type="button" data-view="organization">조직 현황</button>
|
||||
</div>
|
||||
|
||||
<div class="header-actions">
|
||||
<button id="user-badge" class="ghost-button ghost-button-soft user-chip" type="button"></button>
|
||||
<div id="user-popover" class="user-popover hidden"></div>
|
||||
<button id="logout-btn" class="ghost-button icon-button" type="button" title="로그아웃" aria-label="로그아웃">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
|
||||
<polyline points="16 17 21 12 16 7"></polyline>
|
||||
<line x1="21" y1="12" x2="9" y2="12"></line>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="header-actions">
|
||||
<button id="user-badge" class="ghost-button ghost-button-soft user-chip" type="button"></button>
|
||||
<div id="user-popover" class="user-popover hidden"></div>
|
||||
<button id="logout-btn" class="ghost-button icon-button" type="button" title="로그아웃" aria-label="로그아웃">
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round">
|
||||
<path d="M9 21H5a2 2 0 0 1-2-2V5a2 2 0 0 1 2-2h4"></path>
|
||||
<polyline points="16 17 21 12 16 7"></polyline>
|
||||
<line x1="21" y1="12" x2="9" y2="12"></line>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="dashboard-main">
|
||||
<section id="organization-stage" class="main-stage">
|
||||
<div class="stage-frame">
|
||||
<iframe id="organization-frame" src="/legacy/organization?v=20260325-11" data-src="/legacy/organization?v=20260325-11" title="조직도 메인 화면"></iframe>
|
||||
<iframe id="organization-frame" src="/legacy/organization?v=20260326-02" data-src="/legacy/organization?v=20260326-02" title="조직도 메인 화면"></iframe>
|
||||
</div>
|
||||
</section>
|
||||
<section id="project-stage" class="main-stage" hidden>
|
||||
<div class="stage-frame">
|
||||
<iframe id="project-frame" src="/integrations/payment" data-src="/integrations/payment" title="프로젝트별 분석 화면"></iframe>
|
||||
</div>
|
||||
</section>
|
||||
<section id="team-stage" class="main-stage" hidden>
|
||||
<div class="stage-frame">
|
||||
<iframe id="team-frame" src="/integrations/mh" data-src="/integrations/mh" title="팀/개인별 분석 화면"></iframe>
|
||||
</div>
|
||||
</section>
|
||||
<section id="seatmap-admin-stage" class="main-stage" hidden>
|
||||
@@ -175,6 +200,6 @@
|
||||
</main>
|
||||
</section>
|
||||
|
||||
<script src="/app.js?v=20260325-11"></script>
|
||||
<script src="/app.js?v=20260326-02"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -180,12 +180,35 @@ body {
|
||||
backdrop-filter: blur(12px);
|
||||
}
|
||||
|
||||
.header-left,
|
||||
.header-right,
|
||||
.brand-block,
|
||||
.header-actions {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
}
|
||||
|
||||
.header-left {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
flex: 0 0 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
margin-left: auto;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 18px;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.brand-block {
|
||||
flex: 0 0 auto;
|
||||
width: 180px;
|
||||
}
|
||||
|
||||
.dashboard-header .eyebrow {
|
||||
color: var(--color-accent);
|
||||
margin-bottom: 2px;
|
||||
@@ -199,14 +222,13 @@ body {
|
||||
}
|
||||
|
||||
.header-center {
|
||||
margin-left: auto;
|
||||
margin-right: 48px;
|
||||
display: inline-flex;
|
||||
justify-content: center;
|
||||
justify-content: flex-end;
|
||||
gap: 24px;
|
||||
flex-wrap: nowrap;
|
||||
white-space: nowrap;
|
||||
z-index: 1;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.nav-pill {
|
||||
@@ -246,6 +268,48 @@ body {
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
position: relative;
|
||||
padding-left: 18px;
|
||||
border-left: 1px solid #dbe2ea;
|
||||
}
|
||||
|
||||
.header-date-controls {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
margin-left: 0;
|
||||
flex: 0 0 auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.header-date-label {
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
color: #64748b;
|
||||
}
|
||||
|
||||
.header-date-field {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
min-height: 36px;
|
||||
padding: 0 10px;
|
||||
border: 1px solid #dbe2ea;
|
||||
border-radius: 999px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.header-date-field input {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--color-text);
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
outline: none;
|
||||
}
|
||||
|
||||
.header-date-sep {
|
||||
color: #94a3b8;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.ghost-button {
|
||||
@@ -1139,18 +1203,37 @@ body {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header-left,
|
||||
.header-right {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.header-right {
|
||||
justify-content: space-between;
|
||||
}
|
||||
|
||||
.header-center {
|
||||
position: static;
|
||||
transform: none;
|
||||
order: 3;
|
||||
width: 100%;
|
||||
justify-content: flex-start;
|
||||
order: 2;
|
||||
width: auto;
|
||||
justify-content: flex-end;
|
||||
margin-top: 8px;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.header-actions {
|
||||
flex-wrap: wrap;
|
||||
padding-left: 0;
|
||||
border-left: 0;
|
||||
}
|
||||
|
||||
.header-date-controls {
|
||||
order: 2;
|
||||
width: auto;
|
||||
justify-content: flex-start;
|
||||
margin-left: 0;
|
||||
margin-top: 8px;
|
||||
}
|
||||
|
||||
.seatmap-content {
|
||||
|
||||
Reference in New Issue
Block a user