feat: 경영성과 대시보드 DB·엑셀 업로드·HTML 차트 연동
- mgmt_perf_uploads / mgmt_perf_snapshots 스키마 - POST /api/mgmt-perf/upload, 기본 페이로드 data/mgmt-perf-default-payload.json - 대시보드 페이지: 업로드 영역 + iframe embed - public/mgmt-perf: 원본 HTML 기반 CSS·dashboard-app.js - xlsx 미설치 시 기본 페이로드+메타만 저장 Made-with: Cursor
This commit is contained in:
92
server.js
92
server.js
@@ -26,6 +26,7 @@ const {
|
||||
isOpsStateSuper,
|
||||
} = require("./lib/ops-state");
|
||||
const { fetchOpenGraphImageUrl } = require("./lib/link-preview");
|
||||
const mgmtPerf = require("./lib/mgmt-perf");
|
||||
|
||||
const app = express();
|
||||
const PORT = process.env.PORT || 8030;
|
||||
@@ -535,6 +536,31 @@ const uploadMeetingAudio = multer({
|
||||
},
|
||||
});
|
||||
|
||||
const MGMT_PERF_UPLOAD_DIR = path.join(ROOT_DIR, "uploads", "mgmt-perf");
|
||||
const mgmtPerfStorage = multer.diskStorage({
|
||||
destination: (_, __, cb) => {
|
||||
fsSync.mkdirSync(MGMT_PERF_UPLOAD_DIR, { recursive: true });
|
||||
cb(null, MGMT_PERF_UPLOAD_DIR);
|
||||
},
|
||||
filename: (_, file, cb) => {
|
||||
const ext = (path.extname(file.originalname) || ".xlsx").toLowerCase();
|
||||
const safeExt = ext === ".xlsx" ? ".xlsx" : ".xlsx";
|
||||
cb(null, `${Date.now()}-${uuidv4()}${safeExt}`);
|
||||
},
|
||||
});
|
||||
const uploadMgmtPerfExcel = multer({
|
||||
storage: mgmtPerfStorage,
|
||||
limits: { fileSize: 55 * 1024 * 1024 },
|
||||
fileFilter: (_, file, cb) => {
|
||||
const ext = path.extname(file.originalname).toLowerCase();
|
||||
if (ext !== ".xlsx") {
|
||||
cb(new Error("Excel (.xlsx) 파일만 업로드할 수 있습니다."));
|
||||
return;
|
||||
}
|
||||
cb(null, true);
|
||||
},
|
||||
});
|
||||
|
||||
const mapRowToAxAssignment = (row) => ({
|
||||
id: row.id,
|
||||
department: row.department || "",
|
||||
@@ -1130,6 +1156,30 @@ app.use("/resources/ax-apply", express.static(RESOURCES_AX_APPLY_DIR));
|
||||
app.use(opsAuth.middleware);
|
||||
opsAuth.registerRoutes(app);
|
||||
|
||||
app.post("/api/mgmt-perf/upload", uploadMgmtPerfExcel.single("file"), async (req, res) => {
|
||||
try {
|
||||
if (!req.file) return res.status(400).json({ error: "파일이 없습니다." });
|
||||
const fiscalYear = Math.min(2100, Math.max(2020, parseInt(req.body.fiscalYear, 10) || new Date().getFullYear()));
|
||||
const quarter = Math.min(4, Math.max(1, parseInt(req.body.quarter, 10) || 1));
|
||||
const email = res.locals.opsUserEmail ? String(res.locals.opsUserEmail).trim() : null;
|
||||
const defaultPayload = mgmtPerf.loadDefaultPayload();
|
||||
const buf = fsSync.readFileSync(req.file.path);
|
||||
const payload = mgmtPerf.buildPayloadFromWorkbook(buf, defaultPayload);
|
||||
await mgmtPerf.saveUploadAndSnapshot(pgPool, {
|
||||
userEmail: email,
|
||||
originalFilename: req.file.originalname,
|
||||
filePath: req.file.path,
|
||||
fiscalYear,
|
||||
quarter,
|
||||
payload,
|
||||
});
|
||||
res.json({ ok: true, message: "저장되었습니다. 아래 대시보드가 갱신되었습니다." });
|
||||
} catch (err) {
|
||||
console.error("mgmt-perf upload:", err);
|
||||
res.status(500).json({ error: err.message || "처리 실패" });
|
||||
}
|
||||
});
|
||||
|
||||
const pageRouter = express.Router();
|
||||
pageRouter.get("/chat", (req, res) =>
|
||||
res.render("chat", {
|
||||
@@ -1174,13 +1224,41 @@ pageRouter.get("/dashboard", (req, res) =>
|
||||
opsUserEmail: !!res.locals.opsUserEmail,
|
||||
})
|
||||
);
|
||||
pageRouter.get("/dashboard/business-performance", (req, res) =>
|
||||
res.render("dashboard-business-performance", {
|
||||
activeMenu: "dashboard",
|
||||
adminMode: res.locals.adminMode,
|
||||
opsUserEmail: !!res.locals.opsUserEmail,
|
||||
})
|
||||
);
|
||||
pageRouter.get("/dashboard/business-performance", async (req, res, next) => {
|
||||
try {
|
||||
const latest = await mgmtPerf.getLatestPayloadRow(pgPool);
|
||||
const uploadHistory = await mgmtPerf.listUploads(pgPool, 12);
|
||||
const y = latest.fiscal_year || new Date().getFullYear();
|
||||
const q = latest.quarter || 1;
|
||||
res.render("dashboard-business-performance", {
|
||||
activeMenu: "dashboard",
|
||||
adminMode: res.locals.adminMode,
|
||||
opsUserEmail: !!res.locals.opsUserEmail,
|
||||
defaultYear: y,
|
||||
selectedQuarter: q,
|
||||
uploadHistory,
|
||||
});
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
pageRouter.get("/dashboard/business-performance/embed", async (req, res, next) => {
|
||||
try {
|
||||
const row = await mgmtPerf.getLatestPayloadRow(pgPool);
|
||||
const fy = row.fiscal_year || new Date().getFullYear();
|
||||
const q = row.quarter || 1;
|
||||
const payloadJson = JSON.stringify(row.payload).replace(/</g, "\\u003c");
|
||||
const dashboardTitle = `${fy} Q${q} 경영성과 대시보드`;
|
||||
const quarterLabel = `Q${q}`;
|
||||
res.render("mgmt_perf_embed", {
|
||||
dashboardTitle,
|
||||
quarterLabel,
|
||||
payloadJson,
|
||||
});
|
||||
} catch (err) {
|
||||
next(err);
|
||||
}
|
||||
});
|
||||
const AI_SUCCESS_ADMIN_LIST_PAGE_SIZE = 5;
|
||||
|
||||
pageRouter.get("/ai-cases/write", (req, res) => {
|
||||
|
||||
Reference in New Issue
Block a user