fix: 경영성과 대시보드 iframe 제거·인라인 렌더, 업로드 오류 메시지 개선
- embed 404/검은 화면 방지: 한 페이지에서 페이로드+차트 로드 - fetch 비JSON 응답 시 상태코드 표시, 성공 시 새로고침 - 전역 .container grid와 충돌 시 .mgmt-perf-embed 스코프 CSS Made-with: Cursor
This commit is contained in:
@@ -42,7 +42,7 @@
|
|||||||
- 학습센터 UI (좌측 메뉴 + 상단 헤더 + 강의 카드 레이아웃)
|
- 학습센터 UI (좌측 메뉴 + 상단 헤더 + 강의 카드 레이아웃)
|
||||||
- **AI 탐색** (`/ai-explore`): 전체 너비 레이아웃, AI 서비스 카드(프롬프트·회의록 등). 검색창에 **「프롬프트」**가 포함된 채 검색(Enter) 시 프롬프트 라이브러리로 이동
|
- **AI 탐색** (`/ai-explore`): 전체 너비 레이아웃, AI 서비스 카드(프롬프트·회의록 등). 검색창에 **「프롬프트」**가 포함된 채 검색(Enter) 시 프롬프트 라이브러리로 이동
|
||||||
- **대시보드** (`/dashboard`): AI 탐색과 유사한 카드 그리드·검색으로 대시보드를 모아 표시. 첫 카드 **경영성과 대시보드**는 `/dashboard/business-performance`로 연결
|
- **대시보드** (`/dashboard`): AI 탐색과 유사한 카드 그리드·검색으로 대시보드를 모아 표시. 첫 카드 **경영성과 대시보드**는 `/dashboard/business-performance`로 연결
|
||||||
- **경영성과 대시보드** (`/dashboard/business-performance`): 상단 **엑셀 업로드**(`.xlsx`, 매출일보 시트) → DB(`mgmt_perf_uploads` / `mgmt_perf_snapshots`) 또는 DB 미연결 시 `data/mgmt-perf-last-state.json`에 스냅샷 저장. 하단 **대시보드 조회**는 기존 HTML 템플릿(Chart.js, 매출·수주·예상실적 탭)을 iframe(`/dashboard/business-performance/embed`)으로 표시. 엑셀에서 수치 집계를 치환하려면 `npm install`로 `xlsx` 패키지를 설치한 뒤 서버를 재시작하세요.
|
- **경영성과 대시보드** (`/dashboard/business-performance`): 상단 **엑셀 업로드**(`.xlsx`, 매출일보 시트) → DB(`mgmt_perf_uploads` / `mgmt_perf_snapshots`) 또는 DB 미연결 시 `data/mgmt-perf-last-state.json`에 스냅샷 저장. 하단 **대시보드 조회**는 동일 페이지에 Chart.js 템플릿을 인라인으로 렌더(iframe 제거, 별도 `/dashboard/business-performance/embed`는 직접 열람용으로 유지). 리버스 프록시 사용 시 업로드 실패하면 **`client_max_body_size`**(예: 64m)와 **`/api/` → Node** 전달 여부를 확인. 엑셀 집계 치환은 `npm install`로 `xlsx` 설치 후 서버 재시작.
|
||||||
- **프롬프트 라이브러리** (`/ai-explore/prompts`): 업무별 기본 프롬프트 카드 선택·미리보기·클립보드 복사 (`data/company-prompts.json`). **좌측 메뉴(채팅·AI·AI 성공 사례·학습센터·AX 과제 신청)는 관리자 여부와 관계없이 접근 가능**(강의 삭제·관리자 대시보드 등 일부 기능은 관리자 모드에서만)
|
- **프롬프트 라이브러리** (`/ai-explore/prompts`): 업무별 기본 프롬프트 카드 선택·미리보기·클립보드 복사 (`data/company-prompts.json`). **좌측 메뉴(채팅·AI·AI 성공 사례·학습센터·AX 과제 신청)는 관리자 여부와 관계없이 접근 가능**(강의 삭제·관리자 대시보드 등 일부 기능은 관리자 모드에서만)
|
||||||
- 검색/필터/페이지네이션
|
- 검색/필터/페이지네이션
|
||||||
- 검색어(`q`) 기반 제목/설명/태그 필터
|
- 검색어(`q`) 기반 제목/설명/태그 필터
|
||||||
|
|||||||
@@ -397,3 +397,25 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 앱 셸 안에 인라인 삽입 시 전역 .container(grid)와 충돌 방지 */
|
||||||
|
.mgmt-perf-embed .container {
|
||||||
|
display: block !important;
|
||||||
|
max-width: 1600px !important;
|
||||||
|
width: auto !important;
|
||||||
|
margin: 0 auto !important;
|
||||||
|
padding: 0 !important;
|
||||||
|
gap: unset !important;
|
||||||
|
background: white;
|
||||||
|
border-radius: 8px;
|
||||||
|
box-shadow: 0 4px 20px rgba(0, 0, 0, 0.3);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
.mgmt-perf-embed {
|
||||||
|
font-family: '맑은 고딕', 'Apple SD Gothic Neo', -apple-system, BlinkMacSystemFont, 'Segoe UI', sans-serif;
|
||||||
|
background: linear-gradient(135deg, var(--blk) 0%, var(--blk2) 100%);
|
||||||
|
color: var(--blk);
|
||||||
|
line-height: 1.6;
|
||||||
|
padding: 16px;
|
||||||
|
border-radius: 10px;
|
||||||
|
overflow-x: auto;
|
||||||
|
}
|
||||||
|
|||||||
@@ -1230,6 +1230,9 @@ pageRouter.get("/dashboard/business-performance", async (req, res, next) => {
|
|||||||
const uploadHistory = await mgmtPerf.listUploads(pgPool, 12);
|
const uploadHistory = await mgmtPerf.listUploads(pgPool, 12);
|
||||||
const y = latest.fiscal_year || new Date().getFullYear();
|
const y = latest.fiscal_year || new Date().getFullYear();
|
||||||
const q = latest.quarter || 1;
|
const q = latest.quarter || 1;
|
||||||
|
const payloadJson = JSON.stringify(latest.payload).replace(/</g, "\\u003c");
|
||||||
|
const dashboardTitle = `${y} Q${q} 경영성과 대시보드`;
|
||||||
|
const quarterLabel = `Q${q}`;
|
||||||
res.render("dashboard-business-performance", {
|
res.render("dashboard-business-performance", {
|
||||||
activeMenu: "dashboard",
|
activeMenu: "dashboard",
|
||||||
adminMode: res.locals.adminMode,
|
adminMode: res.locals.adminMode,
|
||||||
@@ -1237,6 +1240,9 @@ pageRouter.get("/dashboard/business-performance", async (req, res, next) => {
|
|||||||
defaultYear: y,
|
defaultYear: y,
|
||||||
selectedQuarter: q,
|
selectedQuarter: q,
|
||||||
uploadHistory,
|
uploadHistory,
|
||||||
|
payloadJson,
|
||||||
|
dashboardTitle,
|
||||||
|
quarterLabel,
|
||||||
});
|
});
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
next(err);
|
next(err);
|
||||||
|
|||||||
@@ -6,6 +6,7 @@
|
|||||||
<%- include('partials/favicon') %>
|
<%- include('partials/favicon') %>
|
||||||
<title>경영성과 대시보드 - XAVIS</title>
|
<title>경영성과 대시보드 - XAVIS</title>
|
||||||
<link rel="stylesheet" href="/public/styles.css" />
|
<link rel="stylesheet" href="/public/styles.css" />
|
||||||
|
<link rel="stylesheet" href="/mgmt-perf/dashboard.css" />
|
||||||
<style>
|
<style>
|
||||||
.mgmt-perf-page main.container {
|
.mgmt-perf-page main.container {
|
||||||
max-width: 1200px;
|
max-width: 1200px;
|
||||||
@@ -80,18 +81,11 @@
|
|||||||
font-size: 1.05rem;
|
font-size: 1.05rem;
|
||||||
margin: 0 0 10px;
|
margin: 0 0 10px;
|
||||||
}
|
}
|
||||||
.mgmt-dash-embed-wrap {
|
.mgmt-dash-inline-wrap {
|
||||||
border: 1px solid var(--border, #e0e0e0);
|
border: 1px solid var(--border, #e0e0e0);
|
||||||
border-radius: 10px;
|
border-radius: 10px;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #1a1a1a;
|
min-height: 400px;
|
||||||
min-height: 820px;
|
|
||||||
}
|
|
||||||
.mgmt-dash-embed-wrap iframe {
|
|
||||||
display: block;
|
|
||||||
width: 100%;
|
|
||||||
min-height: 820px;
|
|
||||||
border: 0;
|
|
||||||
}
|
}
|
||||||
.mgmt-upload-history {
|
.mgmt-upload-history {
|
||||||
margin-top: 16px;
|
margin-top: 16px;
|
||||||
@@ -173,7 +167,7 @@
|
|||||||
<tr>
|
<tr>
|
||||||
<td><%= u.created_at ? new Date(u.created_at).toLocaleString('ko-KR') : '-' %></td>
|
<td><%= u.created_at ? new Date(u.created_at).toLocaleString('ko-KR') : '-' %></td>
|
||||||
<td><%= u.original_filename || '-' %></td>
|
<td><%= u.original_filename || '-' %></td>
|
||||||
<td><%= u.fiscal_year %>년 Q<%= u.quarter %></td>
|
<td><%= u.fiscal_year != null && u.fiscal_year !== '' ? u.fiscal_year + '년 Q' + u.quarter : '-' %></td>
|
||||||
</tr>
|
</tr>
|
||||||
<% }); %>
|
<% }); %>
|
||||||
</tbody>
|
</tbody>
|
||||||
@@ -184,13 +178,17 @@
|
|||||||
|
|
||||||
<section class="mgmt-dash-panel" aria-labelledby="mgmt-dash-heading">
|
<section class="mgmt-dash-panel" aria-labelledby="mgmt-dash-heading">
|
||||||
<h2 id="mgmt-dash-heading">대시보드 조회</h2>
|
<h2 id="mgmt-dash-heading">대시보드 조회</h2>
|
||||||
<div class="mgmt-dash-embed-wrap">
|
<div class="mgmt-dash-inline-wrap">
|
||||||
<iframe
|
<div class="mgmt-perf-embed" id="mgmtPerfDashRoot">
|
||||||
id="mgmtPerfDashFrame"
|
<%- include('partials/mgmt_perf_dashboard_container', {
|
||||||
title="경영성과 차트"
|
dashboardTitle: typeof dashboardTitle !== 'undefined' ? dashboardTitle : '경영성과 대시보드',
|
||||||
src="/dashboard/business-performance/embed"
|
quarterLabel: typeof quarterLabel !== 'undefined' ? quarterLabel : 'Q1',
|
||||||
></iframe>
|
}) %>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<script type="application/json" id="mgmt-perf-payload-json"><%- typeof payloadJson !== 'undefined' ? payloadJson : '{}' %></script>
|
||||||
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.1/dist/chart.umd.min.js"></script>
|
||||||
|
<script src="/mgmt-perf/dashboard-app.js"></script>
|
||||||
</section>
|
</section>
|
||||||
</div>
|
</div>
|
||||||
</main>
|
</main>
|
||||||
@@ -201,7 +199,6 @@
|
|||||||
var form = document.getElementById("mgmtPerfUploadForm");
|
var form = document.getElementById("mgmtPerfUploadForm");
|
||||||
var btn = document.getElementById("mgmtPerfUploadBtn");
|
var btn = document.getElementById("mgmtPerfUploadBtn");
|
||||||
var msg = document.getElementById("mgmtPerfUploadMsg");
|
var msg = document.getElementById("mgmtPerfUploadMsg");
|
||||||
var frame = document.getElementById("mgmtPerfDashFrame");
|
|
||||||
if (!form || !btn || !msg) return;
|
if (!form || !btn || !msg) return;
|
||||||
|
|
||||||
form.addEventListener("submit", function (e) {
|
form.addEventListener("submit", function (e) {
|
||||||
@@ -212,24 +209,37 @@
|
|||||||
btn.disabled = true;
|
btn.disabled = true;
|
||||||
fetch("/api/mgmt-perf/upload", { method: "POST", body: fd, credentials: "same-origin" })
|
fetch("/api/mgmt-perf/upload", { method: "POST", body: fd, credentials: "same-origin" })
|
||||||
.then(function (r) {
|
.then(function (r) {
|
||||||
return r.json().then(function (j) {
|
return r.text().then(function (text) {
|
||||||
return { ok: r.ok, body: j };
|
var j = {};
|
||||||
|
if (text) {
|
||||||
|
try {
|
||||||
|
j = JSON.parse(text);
|
||||||
|
} catch (parseErr) {
|
||||||
|
j = { error: "서버 응답을 해석할 수 없습니다. (" + r.status + ")" };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { ok: r.ok, body: j, status: r.status };
|
||||||
});
|
});
|
||||||
})
|
})
|
||||||
.then(function (_ref) {
|
.then(function (_ref) {
|
||||||
var ok = _ref.ok;
|
var ok = _ref.ok;
|
||||||
var body = _ref.body;
|
var body = _ref.body;
|
||||||
if (ok) {
|
if (ok) {
|
||||||
msg.textContent = body.message || "저장되었습니다.";
|
msg.textContent = body.message || "저장되었습니다. 화면을 새로고침합니다.";
|
||||||
msg.className = "mgmt-upload-msg ok";
|
msg.className = "mgmt-upload-msg ok";
|
||||||
if (frame) frame.src = "/dashboard/business-performance/embed?t=" + Date.now();
|
window.setTimeout(function () {
|
||||||
|
window.location.reload();
|
||||||
|
}, 600);
|
||||||
} else {
|
} else {
|
||||||
msg.textContent = body.error || "업로드에 실패했습니다.";
|
msg.textContent =
|
||||||
|
(body && body.error) ||
|
||||||
|
"업로드에 실패했습니다. (" + (_ref.status || "") + ")";
|
||||||
msg.className = "mgmt-upload-msg err";
|
msg.className = "mgmt-upload-msg err";
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
.catch(function () {
|
.catch(function (err) {
|
||||||
msg.textContent = "네트워크 오류입니다.";
|
msg.textContent =
|
||||||
|
(err && err.message) || "요청에 실패했습니다. 서버가 최신 코드인지·프록시에서 /api 경로가 Node로 전달되는지 확인하세요.";
|
||||||
msg.className = "mgmt-upload-msg err";
|
msg.className = "mgmt-upload-msg err";
|
||||||
})
|
})
|
||||||
.finally(function () {
|
.finally(function () {
|
||||||
|
|||||||
@@ -8,7 +8,9 @@
|
|||||||
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.1/dist/chart.umd.min.js"></script>
|
<script src="https://cdn.jsdelivr.net/npm/chart.js@4.5.1/dist/chart.umd.min.js"></script>
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
|
<div class="mgmt-perf-embed">
|
||||||
<%- include('partials/mgmt_perf_dashboard_container', { dashboardTitle, quarterLabel }) %>
|
<%- include('partials/mgmt_perf_dashboard_container', { dashboardTitle, quarterLabel }) %>
|
||||||
|
</div>
|
||||||
<script type="application/json" id="mgmt-perf-payload-json"><%- payloadJson %></script>
|
<script type="application/json" id="mgmt-perf-payload-json"><%- payloadJson %></script>
|
||||||
<script src="/mgmt-perf/dashboard-app.js"></script>
|
<script src="/mgmt-perf/dashboard-app.js"></script>
|
||||||
</body>
|
</body>
|
||||||
|
|||||||
Reference in New Issue
Block a user