feat(mgmt-perf): 대시보드 연도·분기 조회(스냅샷별 로드)

- getPayloadRowForPeriod, listDistinctPeriods
- /dashboard/business-performance?year=&quarter= 및 상단 GET 폼
- 해당 기간 업로드 없을 때 샘플+안내

Made-with: Cursor
This commit is contained in:
2026-04-13 20:05:35 +09:00
parent dc7fca414e
commit 3872c32a91
4 changed files with 227 additions and 8 deletions

View File

@@ -142,6 +142,100 @@ async function getLatestPayloadRow(pgPool) {
}
}
/**
* DB에 저장된 연도·분기별 최신 스냅샷 1건. 해당 기간 업로드가 없으면 기본 페이로드와 `_noSnapshotForPeriod`.
* @param {import("pg").Pool | null} pgPool
* @param {number} fiscalYear
* @param {number} quarter 14
*/
async function getPayloadRowForPeriod(pgPool, fiscalYear, quarter) {
const y = Math.min(2100, Math.max(2020, Math.floor(Number(fiscalYear)) || new Date().getFullYear()));
const q = Math.min(4, Math.max(1, Math.floor(Number(quarter)) || 1));
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
WHERE u.fiscal_year = $1 AND u.quarter = $2
ORDER BY u.created_at DESC
LIMIT 1
`,
[y, q]
);
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,
_noSnapshotForPeriod: false,
};
}
return {
payload: loadDefaultPayload(),
fiscal_year: y,
quarter: q,
original_filename: null,
created_at: null,
_noSnapshotForPeriod: true,
};
}
try {
const j = JSON.parse(fs.readFileSync(FILE_STATE_PATH, "utf8"));
const my = j.meta?.fiscalYear;
const mq = j.meta?.quarter;
if (my === y && mq === q) {
return {
payload: j.payload,
fiscal_year: y,
quarter: q,
original_filename: j.meta?.originalFilename,
created_at: j.meta?.savedAt ? new Date(j.meta.savedAt) : null,
_noSnapshotForPeriod: false,
};
}
} catch (_) {
/* no file */
}
return {
payload: loadDefaultPayload(),
fiscal_year: y,
quarter: q,
original_filename: null,
created_at: null,
_noSnapshotForPeriod: true,
};
}
/**
* 업로드가 존재하는 (연도, 분기) 목록 — 조회 셀렉트 옵션용
* @param {import("pg").Pool | null} pgPool
*/
async function listDistinctPeriods(pgPool) {
if (!pgPool) {
try {
const j = JSON.parse(fs.readFileSync(FILE_STATE_PATH, "utf8"));
if (j.meta?.fiscalYear != null && j.meta?.quarter != null) {
return [{ fiscal_year: j.meta.fiscalYear, quarter: j.meta.quarter }];
}
} catch (_) {
/* empty */
}
return [];
}
const r = await pgPool.query(`
SELECT DISTINCT fiscal_year, quarter
FROM mgmt_perf_uploads
ORDER BY fiscal_year DESC, quarter DESC
`);
return r.rows;
}
/**
* @param {import("pg").Pool | null} pgPool
* @param {number} [limit=20]
@@ -215,6 +309,8 @@ module.exports = {
buildPayloadFromWorkbook,
saveUploadAndSnapshot,
getLatestPayloadRow,
getPayloadRowForPeriod,
listDistinctPeriods,
listUploads,
deleteUpload,
FILE_STATE_PATH,