feat(mgmt-perf): 업로드 영역 하단 배치, 업로드 삭제 API, 앱 내 밝은 배경

- 대시보드 조회를 위·엑셀 업로드를 아래로 재배치
- DELETE /api/mgmt-perf/upload/:id 및 최근 업로드 행 삭제 버튼
- dashboard.css 전역 body 어두운 배경을 body.mgmt-perf-standalone로 한정, 임베드는 투명
- mgmt_perf_embed에 standalone 클래스 유지

Made-with: Cursor
This commit is contained in:
2026-04-13 19:43:57 +09:00
parent 200632f580
commit 90358f05a7
6 changed files with 163 additions and 24 deletions

View File

@@ -174,11 +174,48 @@ async function listUploads(pgPool, limit = 20) {
return r.rows;
}
/**
* 업로드 행 삭제(PG: 스냅샷은 ON DELETE CASCADE). 디스크의 업로드 파일도 제거합니다.
* 파일 전용 모드: `id === "file"` 일 때 스냅샷 JSON만 삭제합니다.
* @param {import("pg").Pool | null} pgPool
* @param {string} uploadId UUID 또는 `"file"`
*/
async function deleteUpload(pgPool, uploadId) {
const id = String(uploadId || "").trim();
if (!id) return { ok: false, error: "id가 없습니다." };
if (!pgPool) {
if (id !== "file") return { ok: false, error: "지원하지 않는 항목입니다." };
try {
if (fs.existsSync(FILE_STATE_PATH)) fs.unlinkSync(FILE_STATE_PATH);
} catch (e) {
return { ok: false, error: e.message || "삭제 실패" };
}
return { ok: true, fileBacked: true };
}
const uuidRe = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/i;
if (!uuidRe.test(id)) return { ok: false, error: "잘못된 id입니다." };
const r = await pgPool.query(`DELETE FROM mgmt_perf_uploads WHERE id = $1::uuid RETURNING file_path`, [id]);
if (r.rowCount === 0) return { ok: false, error: "항목을 찾을 수 없습니다." };
const fp = r.rows[0].file_path;
if (fp && fs.existsSync(fp)) {
try {
fs.unlinkSync(fp);
} catch (_) {
/* 로그만; DB는 이미 삭제됨 */
}
}
return { ok: true };
}
module.exports = {
loadDefaultPayload,
buildPayloadFromWorkbook,
saveUploadAndSnapshot,
getLatestPayloadRow,
listUploads,
deleteUpload,
FILE_STATE_PATH,
};