Files
ai_platform/lib/mgmt-perf.js
dsyoon fdcf1e0528 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
2026-04-13 13:21:31 +09:00

185 lines
5.3 KiB
JavaScript

/**
* 경영성과 대시보드: 엑셀 업로드 파싱·스냅샷 저장·최신 페이로드 조회
*/
const fs = require("fs");
const path = require("path");
function loadXlsx() {
try {
return require("xlsx");
} catch {
return null;
}
}
const ROOT = path.join(__dirname, "..");
const DEFAULT_PAYLOAD_PATH = path.join(ROOT, "data", "mgmt-perf-default-payload.json");
const FILE_STATE_PATH = path.join(ROOT, "data", "mgmt-perf-last-state.json");
function loadDefaultPayload() {
const raw = fs.readFileSync(DEFAULT_PAYLOAD_PATH, "utf8");
return JSON.parse(raw);
}
/**
* 매출일보 시트를 읽어 기본 페이로드에 메타를 덧붙입니다. (집계 치환은 추후 확장)
* @param {Buffer} buffer
* @param {object} defaultPayload
*/
function buildPayloadFromWorkbook(buffer, defaultPayload) {
const XLSX = loadXlsx();
if (!XLSX) {
const payload = JSON.parse(JSON.stringify(defaultPayload));
payload._uploadMeta = {
sheets: [],
primarySheet: null,
rowCount: 0,
importedAt: new Date().toISOString(),
note: "npm 패키지 `xlsx`가 없어 엑셀을 파싱하지 못했습니다. 프로젝트 루트에서 `npm install` 후 서버를 재시작하세요.",
};
return payload;
}
const wb = XLSX.read(buffer, { type: "buffer", cellDates: true });
const names = wb.SheetNames || [];
const sheetName = names.includes("매출일보") ? "매출일보" : names[0];
const ws = wb.Sheets[sheetName];
const matrix = XLSX.utils.sheet_to_json(ws, { header: 1, defval: "" });
const nonEmptyRows = matrix.filter((r) => Array.isArray(r) && r.some((c) => c !== "" && c != null));
const payload = JSON.parse(JSON.stringify(defaultPayload));
payload._uploadMeta = {
sheets: names,
primarySheet: sheetName,
rowCount: nonEmptyRows.length,
importedAt: new Date().toISOString(),
note:
"매출일보 행 수·시트명만 반영했습니다. 차트 수치를 엑셀 집계로 치환하려면 별도 매핑 로직이 필요합니다.",
};
return payload;
}
/**
* @param {import("pg").Pool | null} pgPool
* @param {{ userEmail: string | null, originalFilename: string, filePath: string, fiscalYear: number, quarter: number, payload: object }} row
*/
async function saveUploadAndSnapshot(pgPool, row) {
const { userEmail, originalFilename, filePath, fiscalYear, quarter, payload } = row;
const stat = fs.statSync(filePath);
if (!pgPool) {
fs.writeFileSync(
FILE_STATE_PATH,
JSON.stringify(
{
payload,
meta: {
originalFilename,
fiscalYear,
quarter,
savedAt: new Date().toISOString(),
fileSize: stat.size,
},
},
null,
2
),
"utf8"
);
return { id: null, fileBacked: true };
}
const ins = await pgPool.query(
`INSERT INTO mgmt_perf_uploads (user_email, original_filename, fiscal_year, quarter, file_path, file_size, parse_status)
VALUES ($1,$2,$3,$4,$5,$6,'ok')
RETURNING id`,
[userEmail || null, originalFilename, fiscalYear, quarter, filePath, stat.size]
);
const uploadId = ins.rows[0].id;
await pgPool.query(`INSERT INTO mgmt_perf_snapshots (upload_id, payload) VALUES ($1, $2::jsonb)`, [
uploadId,
JSON.stringify(payload),
]);
return { id: uploadId, fileBacked: false };
}
/**
* @param {import("pg").Pool | null} pgPool
*/
async function getLatestPayloadRow(pgPool) {
if (pgPool) {
const r = await pgPool.query(`
SELECT s.payload, u.fiscal_year, u.quarter, u.original_filename, u.created_at
FROM mgmt_perf_snapshots s
JOIN mgmt_perf_uploads u ON u.id = s.upload_id
ORDER BY u.created_at DESC
LIMIT 1
`);
if (r.rows[0]) {
const row = r.rows[0];
return {
payload: row.payload,
fiscal_year: row.fiscal_year,
quarter: row.quarter,
original_filename: row.original_filename,
created_at: row.created_at,
};
}
}
try {
const j = JSON.parse(fs.readFileSync(FILE_STATE_PATH, "utf8"));
return {
payload: j.payload,
fiscal_year: j.meta?.fiscalYear,
quarter: j.meta?.quarter,
original_filename: j.meta?.originalFilename,
created_at: j.meta?.savedAt ? new Date(j.meta.savedAt) : null,
};
} catch {
return {
payload: loadDefaultPayload(),
fiscal_year: new Date().getFullYear(),
quarter: 1,
original_filename: null,
created_at: null,
};
}
}
/**
* @param {import("pg").Pool | null} pgPool
* @param {number} [limit=20]
*/
async function listUploads(pgPool, limit = 20) {
if (!pgPool) {
try {
const j = JSON.parse(fs.readFileSync(FILE_STATE_PATH, "utf8"));
return [
{
id: "file",
original_filename: j.meta?.originalFilename,
fiscal_year: j.meta?.fiscalYear,
quarter: j.meta?.quarter,
created_at: j.meta?.savedAt,
parse_status: "ok",
},
];
} catch {
return [];
}
}
const r = await pgPool.query(
`SELECT id, original_filename, fiscal_year, quarter, parse_status, created_at
FROM mgmt_perf_uploads
ORDER BY created_at DESC
LIMIT $1`,
[limit]
);
return r.rows;
}
module.exports = {
loadDefaultPayload,
buildPayloadFromWorkbook,
saveUploadAndSnapshot,
getLatestPayloadRow,
listUploads,
FILE_STATE_PATH,
};