182 lines
6.5 KiB
TypeScript
182 lines
6.5 KiB
TypeScript
|
|
/**
|
||
|
|
* BOM 이력 및 버전 관리 서비스
|
||
|
|
* 설정 패널에서 지정한 테이블명을 동적으로 사용
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { query, queryOne, transaction } from "../database/db";
|
||
|
|
import { logger } from "../utils/logger";
|
||
|
|
|
||
|
|
// SQL 인젝션 방지: 테이블명은 알파벳, 숫자, 언더스코어만 허용
|
||
|
|
function safeTableName(name: string, fallback: string): string {
|
||
|
|
if (!name || !/^[a-zA-Z_][a-zA-Z0-9_]*$/.test(name)) return fallback;
|
||
|
|
return name;
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─── 이력 (History) ─────────────────────────────
|
||
|
|
|
||
|
|
export async function getBomHistory(bomId: string, companyCode: string, tableName?: string) {
|
||
|
|
const table = safeTableName(tableName || "", "bom_history");
|
||
|
|
const sql = companyCode === "*"
|
||
|
|
? `SELECT * FROM ${table} WHERE bom_id = $1 ORDER BY changed_date DESC`
|
||
|
|
: `SELECT * FROM ${table} WHERE bom_id = $1 AND company_code = $2 ORDER BY changed_date DESC`;
|
||
|
|
const params = companyCode === "*" ? [bomId] : [bomId, companyCode];
|
||
|
|
return query(sql, params);
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function addBomHistory(
|
||
|
|
bomId: string,
|
||
|
|
companyCode: string,
|
||
|
|
data: {
|
||
|
|
revision?: string;
|
||
|
|
version?: string;
|
||
|
|
change_type: string;
|
||
|
|
change_description?: string;
|
||
|
|
changed_by?: string;
|
||
|
|
},
|
||
|
|
tableName?: string,
|
||
|
|
) {
|
||
|
|
const table = safeTableName(tableName || "", "bom_history");
|
||
|
|
const sql = `
|
||
|
|
INSERT INTO ${table} (bom_id, revision, version, change_type, change_description, changed_by, company_code)
|
||
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7)
|
||
|
|
RETURNING *
|
||
|
|
`;
|
||
|
|
return queryOne(sql, [
|
||
|
|
bomId,
|
||
|
|
data.revision || null,
|
||
|
|
data.version || null,
|
||
|
|
data.change_type,
|
||
|
|
data.change_description || null,
|
||
|
|
data.changed_by || null,
|
||
|
|
companyCode,
|
||
|
|
]);
|
||
|
|
}
|
||
|
|
|
||
|
|
// ─── 버전 (Version) ─────────────────────────────
|
||
|
|
|
||
|
|
export async function getBomVersions(bomId: string, companyCode: string, tableName?: string) {
|
||
|
|
const table = safeTableName(tableName || "", "bom_version");
|
||
|
|
const sql = companyCode === "*"
|
||
|
|
? `SELECT * FROM ${table} WHERE bom_id = $1 ORDER BY created_date DESC`
|
||
|
|
: `SELECT * FROM ${table} WHERE bom_id = $1 AND company_code = $2 ORDER BY created_date DESC`;
|
||
|
|
const params = companyCode === "*" ? [bomId] : [bomId, companyCode];
|
||
|
|
return query(sql, params);
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function createBomVersion(
|
||
|
|
bomId: string, companyCode: string, createdBy: string,
|
||
|
|
versionTableName?: string, detailTableName?: string,
|
||
|
|
) {
|
||
|
|
const vTable = safeTableName(versionTableName || "", "bom_version");
|
||
|
|
const dTable = safeTableName(detailTableName || "", "bom_detail");
|
||
|
|
|
||
|
|
return transaction(async (client) => {
|
||
|
|
const bomRow = await client.query(`SELECT * FROM bom WHERE id = $1`, [bomId]);
|
||
|
|
if (bomRow.rows.length === 0) throw new Error("BOM을 찾을 수 없습니다");
|
||
|
|
const bomData = bomRow.rows[0];
|
||
|
|
|
||
|
|
const detailRows = await client.query(
|
||
|
|
`SELECT * FROM ${dTable} WHERE bom_id = $1 ORDER BY parent_detail_id NULLS FIRST, id`,
|
||
|
|
[bomId],
|
||
|
|
);
|
||
|
|
|
||
|
|
const lastVersion = await client.query(
|
||
|
|
`SELECT version_name FROM ${vTable} WHERE bom_id = $1 ORDER BY created_date DESC LIMIT 1`,
|
||
|
|
[bomId],
|
||
|
|
);
|
||
|
|
let nextVersionNum = 1;
|
||
|
|
if (lastVersion.rows.length > 0) {
|
||
|
|
const parsed = parseFloat(lastVersion.rows[0].version_name);
|
||
|
|
if (!isNaN(parsed)) nextVersionNum = Math.floor(parsed) + 1;
|
||
|
|
}
|
||
|
|
const versionName = `${nextVersionNum}.0`;
|
||
|
|
|
||
|
|
const snapshot = {
|
||
|
|
bom: bomData,
|
||
|
|
details: detailRows.rows,
|
||
|
|
detailTable: dTable,
|
||
|
|
created_at: new Date().toISOString(),
|
||
|
|
};
|
||
|
|
|
||
|
|
const insertSql = `
|
||
|
|
INSERT INTO ${vTable} (bom_id, version_name, revision, status, snapshot_data, created_by, company_code)
|
||
|
|
VALUES ($1, $2, $3, 'developing', $4, $5, $6)
|
||
|
|
RETURNING *
|
||
|
|
`;
|
||
|
|
const result = await client.query(insertSql, [
|
||
|
|
bomId,
|
||
|
|
versionName,
|
||
|
|
bomData.revision ? parseInt(bomData.revision, 10) || 0 : 0,
|
||
|
|
JSON.stringify(snapshot),
|
||
|
|
createdBy,
|
||
|
|
companyCode,
|
||
|
|
]);
|
||
|
|
|
||
|
|
logger.info("BOM 버전 생성", { bomId, versionName, companyCode, vTable, dTable });
|
||
|
|
return result.rows[0];
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function loadBomVersion(
|
||
|
|
bomId: string, versionId: string, companyCode: string,
|
||
|
|
versionTableName?: string, detailTableName?: string,
|
||
|
|
) {
|
||
|
|
const vTable = safeTableName(versionTableName || "", "bom_version");
|
||
|
|
const dTable = safeTableName(detailTableName || "", "bom_detail");
|
||
|
|
|
||
|
|
return transaction(async (client) => {
|
||
|
|
const verRow = await client.query(
|
||
|
|
`SELECT * FROM ${vTable} WHERE id = $1 AND bom_id = $2`,
|
||
|
|
[versionId, bomId],
|
||
|
|
);
|
||
|
|
if (verRow.rows.length === 0) throw new Error("버전을 찾을 수 없습니다");
|
||
|
|
|
||
|
|
const snapshot = verRow.rows[0].snapshot_data;
|
||
|
|
if (!snapshot || !snapshot.bom) throw new Error("스냅샷 데이터가 없습니다");
|
||
|
|
|
||
|
|
// 스냅샷에 기록된 detailTable을 우선 사용, 없으면 파라미터 사용
|
||
|
|
const snapshotDetailTable = safeTableName(snapshot.detailTable || "", dTable);
|
||
|
|
|
||
|
|
await client.query(`DELETE FROM ${snapshotDetailTable} WHERE bom_id = $1`, [bomId]);
|
||
|
|
|
||
|
|
const b = snapshot.bom;
|
||
|
|
await client.query(
|
||
|
|
`UPDATE bom SET base_qty = $1, unit = $2, revision = $3, remark = $4 WHERE id = $5`,
|
||
|
|
[b.base_qty || null, b.unit || null, b.revision || null, b.remark || null, bomId],
|
||
|
|
);
|
||
|
|
|
||
|
|
const oldToNew: Record<string, string> = {};
|
||
|
|
for (const d of snapshot.details || []) {
|
||
|
|
const insertResult = await client.query(
|
||
|
|
`INSERT INTO ${snapshotDetailTable} (bom_id, parent_detail_id, child_item_id, quantity, unit, process_type, loss_rate, remark, level, base_qty, revision, company_code)
|
||
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12) RETURNING id`,
|
||
|
|
[
|
||
|
|
bomId,
|
||
|
|
d.parent_detail_id ? (oldToNew[d.parent_detail_id] || null) : null,
|
||
|
|
d.child_item_id,
|
||
|
|
d.quantity,
|
||
|
|
d.unit,
|
||
|
|
d.process_type,
|
||
|
|
d.loss_rate,
|
||
|
|
d.remark,
|
||
|
|
d.level,
|
||
|
|
d.base_qty,
|
||
|
|
d.revision,
|
||
|
|
companyCode,
|
||
|
|
],
|
||
|
|
);
|
||
|
|
oldToNew[d.id] = insertResult.rows[0].id;
|
||
|
|
}
|
||
|
|
|
||
|
|
logger.info("BOM 버전 불러오기 완료", { bomId, versionId, vTable, snapshotDetailTable });
|
||
|
|
return { restored: true, versionName: verRow.rows[0].version_name };
|
||
|
|
});
|
||
|
|
}
|
||
|
|
|
||
|
|
export async function deleteBomVersion(bomId: string, versionId: string, tableName?: string) {
|
||
|
|
const table = safeTableName(tableName || "", "bom_version");
|
||
|
|
const sql = `DELETE FROM ${table} WHERE id = $1 AND bom_id = $2 RETURNING id`;
|
||
|
|
const result = await query(sql, [versionId, bomId]);
|
||
|
|
return result.length > 0;
|
||
|
|
}
|