diff --git a/README.md b/README.md index d0ea74d..94e742f 100644 --- a/README.md +++ b/README.md @@ -43,6 +43,7 @@ - **AI 탐색** (`/ai-explore`): 전체 너비 레이아웃, AI 서비스 카드(프롬프트·회의록 등). 검색창에 **「프롬프트」**가 포함된 채 검색(Enter) 시 프롬프트 라이브러리로 이동 - **대시보드** (`/dashboard`): AI 탐색과 유사한 카드 그리드·검색으로 대시보드를 모아 표시. 첫 카드 **경영성과 대시보드**는 `/dashboard/business-performance`로 연결 - **경영성과 대시보드** (`/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` 설치 후 서버 재시작. +- **경영성과 데이터 확인**: 브라우저에서 `GET /api/mgmt-perf/status`(JSON)로 최근 스냅샷의 `payloadKeys`, `_uploadMeta`(행 수 등)를 확인할 수 있습니다. **현재 구현**은 엑셀에서 **매출일보 행 수·시트명만** `payload._uploadMeta`에 넣고, **차트 수치는 기본 시드 JSON**(`data/mgmt-perf-default-payload.json`)을 씁니다. 5,000행이어도 차트가 엑셀 집계와 일치하려면 **별도 집계·매핑 로직**이 필요합니다. - **프롬프트 라이브러리** (`/ai-explore/prompts`): 업무별 기본 프롬프트 카드 선택·미리보기·클립보드 복사 (`data/company-prompts.json`). **좌측 메뉴(채팅·AI·AI 성공 사례·학습센터·AX 과제 신청)는 관리자 여부와 관계없이 접근 가능**(강의 삭제·관리자 대시보드 등 일부 기능은 관리자 모드에서만) - 검색/필터/페이지네이션 - 검색어(`q`) 기반 제목/설명/태그 필터 diff --git a/public/mgmt-perf/dashboard-app.js b/public/mgmt-perf/dashboard-app.js index 7778f8a..ad62d16 100644 --- a/public/mgmt-perf/dashboard-app.js +++ b/public/mgmt-perf/dashboard-app.js @@ -39,12 +39,26 @@ let state = { return days[month - 1]; } + function isSectionActive(sectionId) { + const el = document.getElementById(sectionId); + return !!(el && el.classList.contains("active")); + } + // Initialize (iframe에서도 DOMContentLoaded가 이미 지난 경우 대비) function bootMgmtPerfDashboard() { - setupEventListeners(); - renderOverviewMode(); - renderOrderSection(); - renderForecastSection(); + try { + setupEventListeners(); + // 숨겨진 탭(display:none)에 Chart를 그리면 캔버스 크기 0 → 매출현황만 먼저 그림. 수주/예상은 해당 탭이 보일 때만 렌더. + renderDivisionView(); + } catch (err) { + console.error("mgmt-perf dashboard boot:", err); + var banner = document.createElement("div"); + banner.setAttribute("role", "alert"); + banner.style.cssText = "background:#ffebee;color:#b71c1c;padding:12px;margin:8px;border-radius:8px;font-size:13px;"; + banner.textContent = "대시보드 스크립트 오류: " + (err && err.message ? err.message : String(err)); + var root = document.querySelector(".mgmt-perf-embed") || document.body; + root.insertBefore(banner, root.firstChild); + } } if (document.readyState === "loading") { document.addEventListener("DOMContentLoaded", bootMgmtPerfDashboard); @@ -82,6 +96,10 @@ let state = { const sectionName = e.target.dataset.section; state.currentSection = sectionName; document.getElementById(sectionName).classList.add('active'); + // 탭 전환 후 레이아웃이 잡힌 뒤 차트 재생성 (숨겨진 패널 문제 방지) + requestAnimationFrame(function () { + renderDivisionView(); + }); }); }); } @@ -105,34 +123,39 @@ let state = { function renderDivisionView() { const isOverviewMode = state.currentDivision === 'all'; - document.getElementById('overviewMode').style.display = isOverviewMode ? 'block' : 'none'; - document.getElementById('divisionDetailMode').style.display = isOverviewMode ? 'none' : 'block'; - - if (isOverviewMode) { - renderOverviewMode(); - } else { - renderDivisionDetailMode(); - } - // Hide FSCAN/XSCAN model charts when 배터리 or 신사업 is selected - const showModels = !(['battery','newbiz'].includes(state.currentDivision)); - const modelDisplay = showModels ? 'grid' : 'none'; - const smr = document.getElementById('salesModelRow'); - const omr = document.getElementById('orderModelRow'); - if(smr) smr.style.display = modelDisplay; - if(omr) omr.style.display = modelDisplay; - - // Update model charts and category charts for current division/month + const showModels = !["battery", "newbiz"].includes(state.currentDivision); + const modelDisplay = showModels ? "grid" : "none"; const mon = state.currentMonth; - const monthIdx = mon === 'all' ? [0,1,2] : [parseInt(mon)-1]; - if (showModels) { - renderSalesModelCharts(monthIdx); - } - // Always update category breakdown - renderSalesCategoryCharts(monthIdx); + const monthIdx = mon === "all" ? [0, 1, 2] : [parseInt(mon, 10) - 1]; - // Also update 수주현황 and 예상전망 - renderOrderSection(); - renderForecastSection(); + // 매출현황 탭이 보일 때만 KPI·매출 차트 렌더 (숨겨진 영역에 그리면 Chart.js 높이 0) + if (isSectionActive("매출현황")) { + document.getElementById("overviewMode").style.display = isOverviewMode ? "block" : "none"; + document.getElementById("divisionDetailMode").style.display = isOverviewMode ? "none" : "block"; + if (isOverviewMode) { + renderOverviewMode(); + } else { + renderDivisionDetailMode(); + } + const smr = document.getElementById("salesModelRow"); + if (smr) smr.style.display = modelDisplay; + if (showModels) { + renderSalesModelCharts(monthIdx); + } + renderSalesCategoryCharts(monthIdx); + } + + const omr = document.getElementById("orderModelRow"); + if (omr && isSectionActive("수주현황")) { + omr.style.display = modelDisplay; + } + + if (isSectionActive("수주현황")) { + renderOrderSection(); + } + if (isSectionActive("예상전망")) { + renderForecastSection(); + } } // Helper: get total sales for a division for given month indices diff --git a/public/mgmt-perf/dashboard.css b/public/mgmt-perf/dashboard.css index c795b15..8521119 100644 --- a/public/mgmt-perf/dashboard.css +++ b/public/mgmt-perf/dashboard.css @@ -419,3 +419,8 @@ border-radius: 10px; overflow-x: auto; } + +.mgmt-perf-embed .chart-wrapper { + min-height: 280px; + position: relative; + } diff --git a/server.js b/server.js index 11d71b0..6206868 100644 --- a/server.js +++ b/server.js @@ -1156,6 +1156,26 @@ app.use("/resources/ax-apply", express.static(RESOURCES_AX_APPLY_DIR)); app.use(opsAuth.middleware); opsAuth.registerRoutes(app); +app.get("/api/mgmt-perf/status", async (req, res) => { + try { + const row = await mgmtPerf.getLatestPayloadRow(pgPool); + const p = row.payload && typeof row.payload === "object" ? row.payload : {}; + const keys = Object.keys(p); + res.json({ + ok: true, + usingPostgres: !!pgPool, + fiscalYear: row.fiscal_year, + quarter: row.quarter, + lastFilename: row.original_filename, + lastImportedAt: row.created_at, + payloadKeys: keys, + uploadMeta: p._uploadMeta || null, + }); + } catch (err) { + res.status(500).json({ ok: false, error: err?.message || "조회 실패" }); + } +}); + app.post("/api/mgmt-perf/upload", uploadMgmtPerfExcel.single("file"), async (req, res) => { try { if (!req.file) return res.status(400).json({ error: "파일이 없습니다." });