Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into ycshin-node
This commit is contained in:
@@ -1854,7 +1854,7 @@ export async function toggleMenuStatus(
|
||||
|
||||
// 현재 상태 및 회사 코드 조회
|
||||
const currentMenu = await queryOne<any>(
|
||||
`SELECT objid, status, company_code FROM menu_info WHERE objid = $1`,
|
||||
`SELECT objid, status, company_code, menu_name_kor FROM menu_info WHERE objid = $1`,
|
||||
[Number(menuId)]
|
||||
);
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { Response } from "express";
|
||||
import { AuthenticatedRequest } from "../middleware/authMiddleware";
|
||||
import { auditLogService } from "../services/auditLogService";
|
||||
import { auditLogService, getClientIp, AuditAction, AuditResourceType } from "../services/auditLogService";
|
||||
import { query } from "../database/db";
|
||||
import logger from "../utils/logger";
|
||||
|
||||
@@ -137,3 +137,40 @@ export const getAuditLogUsers = async (
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* 프론트엔드에서 직접 감사 로그 기록 (그룹 복제 등 프론트 오케스트레이션 작업용)
|
||||
*/
|
||||
export const createAuditLog = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
): Promise<void> => {
|
||||
try {
|
||||
const { action, resourceType, resourceId, resourceName, tableName, summary, changes } = req.body;
|
||||
|
||||
if (!action || !resourceType) {
|
||||
res.status(400).json({ success: false, message: "action, resourceType은 필수입니다." });
|
||||
return;
|
||||
}
|
||||
|
||||
await auditLogService.log({
|
||||
companyCode: req.user?.companyCode || "",
|
||||
userId: req.user?.userId || "",
|
||||
userName: req.user?.userName || "",
|
||||
action: action as AuditAction,
|
||||
resourceType: resourceType as AuditResourceType,
|
||||
resourceId: resourceId || undefined,
|
||||
resourceName: resourceName || undefined,
|
||||
tableName: tableName || undefined,
|
||||
summary: summary || undefined,
|
||||
changes: changes || undefined,
|
||||
ipAddress: getClientIp(req),
|
||||
requestPath: req.originalUrl,
|
||||
});
|
||||
|
||||
res.json({ success: true });
|
||||
} catch (error: any) {
|
||||
logger.error("감사 로그 기록 실패", { error: error.message });
|
||||
res.status(500).json({ success: false, message: "감사 로그 기록 실패" });
|
||||
}
|
||||
};
|
||||
|
||||
@@ -6,6 +6,7 @@ import { Router, Request, Response } from "express";
|
||||
import { categoryTreeService, CreateCategoryValueInput, UpdateCategoryValueInput } from "../services/categoryTreeService";
|
||||
import { logger } from "../utils/logger";
|
||||
import { authenticateToken } from "../middleware/authMiddleware";
|
||||
import { auditLogService, getClientIp } from "../services/auditLogService";
|
||||
|
||||
const router = Router();
|
||||
|
||||
@@ -16,6 +17,7 @@ router.use(authenticateToken);
|
||||
interface AuthenticatedRequest extends Request {
|
||||
user?: {
|
||||
userId: string;
|
||||
userName: string;
|
||||
companyCode: string;
|
||||
};
|
||||
}
|
||||
@@ -157,6 +159,21 @@ router.post("/test/value", async (req: AuthenticatedRequest, res: Response) => {
|
||||
|
||||
const value = await categoryTreeService.createCategoryValue(companyCode, input, createdBy);
|
||||
|
||||
auditLogService.log({
|
||||
companyCode,
|
||||
userId: req.user?.userId || "",
|
||||
userName: req.user?.userName,
|
||||
action: "CREATE",
|
||||
resourceType: "CODE_CATEGORY",
|
||||
resourceId: String(value.valueId),
|
||||
resourceName: input.valueLabel,
|
||||
tableName: "category_values",
|
||||
summary: `카테고리 값 "${input.valueLabel}" 생성 (${input.tableName}.${input.columnName})`,
|
||||
changes: { after: { tableName: input.tableName, columnName: input.columnName, valueCode: input.valueCode, valueLabel: input.valueLabel } },
|
||||
ipAddress: getClientIp(req),
|
||||
requestPath: req.originalUrl,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: value,
|
||||
@@ -182,6 +199,7 @@ router.put("/test/value/:valueId", async (req: AuthenticatedRequest, res: Respon
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
const updatedBy = req.user?.userId;
|
||||
|
||||
const beforeValue = await categoryTreeService.getCategoryValue(companyCode, Number(valueId));
|
||||
const value = await categoryTreeService.updateCategoryValue(companyCode, Number(valueId), input, updatedBy);
|
||||
|
||||
if (!value) {
|
||||
@@ -191,6 +209,24 @@ router.put("/test/value/:valueId", async (req: AuthenticatedRequest, res: Respon
|
||||
});
|
||||
}
|
||||
|
||||
auditLogService.log({
|
||||
companyCode,
|
||||
userId: req.user?.userId || "",
|
||||
userName: req.user?.userName,
|
||||
action: "UPDATE",
|
||||
resourceType: "CODE_CATEGORY",
|
||||
resourceId: valueId,
|
||||
resourceName: value.valueLabel,
|
||||
tableName: "category_values",
|
||||
summary: `카테고리 값 "${value.valueLabel}" 수정 (${value.tableName}.${value.columnName})`,
|
||||
changes: {
|
||||
before: beforeValue ? { valueLabel: beforeValue.valueLabel, valueCode: beforeValue.valueCode } : undefined,
|
||||
after: input,
|
||||
},
|
||||
ipAddress: getClientIp(req),
|
||||
requestPath: req.originalUrl,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: value,
|
||||
@@ -239,6 +275,7 @@ router.delete("/test/value/:valueId", async (req: AuthenticatedRequest, res: Res
|
||||
const { valueId } = req.params;
|
||||
const companyCode = req.user?.companyCode || "*";
|
||||
|
||||
const beforeValue = await categoryTreeService.getCategoryValue(companyCode, Number(valueId));
|
||||
const success = await categoryTreeService.deleteCategoryValue(companyCode, Number(valueId));
|
||||
|
||||
if (!success) {
|
||||
@@ -248,6 +285,21 @@ router.delete("/test/value/:valueId", async (req: AuthenticatedRequest, res: Res
|
||||
});
|
||||
}
|
||||
|
||||
auditLogService.log({
|
||||
companyCode,
|
||||
userId: req.user?.userId || "",
|
||||
userName: req.user?.userName,
|
||||
action: "DELETE",
|
||||
resourceType: "CODE_CATEGORY",
|
||||
resourceId: valueId,
|
||||
resourceName: beforeValue?.valueLabel || valueId,
|
||||
tableName: "category_values",
|
||||
summary: `카테고리 값 "${beforeValue?.valueLabel || valueId}" 삭제 (${beforeValue?.tableName || ""}.${beforeValue?.columnName || ""})`,
|
||||
changes: beforeValue ? { before: { valueLabel: beforeValue.valueLabel, valueCode: beforeValue.valueCode, tableName: beforeValue.tableName, columnName: beforeValue.columnName } } : undefined,
|
||||
ipAddress: getClientIp(req),
|
||||
requestPath: req.originalUrl,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
message: "삭제되었습니다",
|
||||
|
||||
@@ -396,6 +396,20 @@ export class CommonCodeController {
|
||||
companyCode
|
||||
);
|
||||
|
||||
auditLogService.log({
|
||||
companyCode: companyCode || "",
|
||||
userId: userId || "",
|
||||
action: "UPDATE",
|
||||
resourceType: "CODE",
|
||||
resourceId: codeValue,
|
||||
resourceName: codeData.codeName || codeValue,
|
||||
tableName: "code_info",
|
||||
summary: `코드 "${categoryCode}.${codeValue}" 수정`,
|
||||
changes: { after: codeData },
|
||||
ipAddress: getClientIp(req as any),
|
||||
requestPath: req.originalUrl,
|
||||
});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: code,
|
||||
@@ -440,6 +454,19 @@ export class CommonCodeController {
|
||||
companyCode
|
||||
);
|
||||
|
||||
auditLogService.log({
|
||||
companyCode: companyCode || "",
|
||||
userId: req.user?.userId || "",
|
||||
action: "DELETE",
|
||||
resourceType: "CODE",
|
||||
resourceId: codeValue,
|
||||
tableName: "code_info",
|
||||
summary: `코드 "${categoryCode}.${codeValue}" 삭제`,
|
||||
changes: { before: { categoryCode, codeValue } },
|
||||
ipAddress: getClientIp(req as any),
|
||||
requestPath: req.originalUrl,
|
||||
});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: "코드 삭제 성공",
|
||||
|
||||
@@ -438,6 +438,19 @@ export class DDLController {
|
||||
);
|
||||
|
||||
if (result.success) {
|
||||
auditLogService.log({
|
||||
companyCode: userCompanyCode || "",
|
||||
userId,
|
||||
action: "DELETE",
|
||||
resourceType: "TABLE",
|
||||
resourceId: tableName,
|
||||
resourceName: tableName,
|
||||
tableName,
|
||||
summary: `테이블 "${tableName}" 삭제`,
|
||||
ipAddress: getClientIp(req as any),
|
||||
requestPath: req.originalUrl,
|
||||
});
|
||||
|
||||
res.status(200).json({
|
||||
success: true,
|
||||
message: result.message,
|
||||
|
||||
@@ -193,6 +193,7 @@ router.post(
|
||||
auditLogService.log({
|
||||
companyCode,
|
||||
userId,
|
||||
userName: req.user?.userName,
|
||||
action: "CREATE",
|
||||
resourceType: "NUMBERING_RULE",
|
||||
resourceId: String(newRule.ruleId),
|
||||
@@ -243,6 +244,7 @@ router.put(
|
||||
auditLogService.log({
|
||||
companyCode,
|
||||
userId: req.user?.userId || "",
|
||||
userName: req.user?.userName,
|
||||
action: "UPDATE",
|
||||
resourceType: "NUMBERING_RULE",
|
||||
resourceId: ruleId,
|
||||
@@ -285,6 +287,7 @@ router.delete(
|
||||
auditLogService.log({
|
||||
companyCode,
|
||||
userId: req.user?.userId || "",
|
||||
userName: req.user?.userName,
|
||||
action: "DELETE",
|
||||
resourceType: "NUMBERING_RULE",
|
||||
resourceId: ruleId,
|
||||
@@ -522,6 +525,56 @@ router.post(
|
||||
companyCode,
|
||||
userId
|
||||
);
|
||||
|
||||
const isUpdate = !!ruleConfig.ruleId;
|
||||
|
||||
const resetPeriodLabel: Record<string, string> = {
|
||||
none: "초기화 안함", daily: "일별", monthly: "월별", yearly: "연별",
|
||||
};
|
||||
const partTypeLabel: Record<string, string> = {
|
||||
sequence: "순번", number: "숫자", date: "날짜", text: "문자", category: "카테고리", reference: "참조",
|
||||
};
|
||||
const partsDescription = (ruleConfig.parts || [])
|
||||
.sort((a: any, b: any) => (a.order || 0) - (b.order || 0))
|
||||
.map((p: any) => {
|
||||
const type = partTypeLabel[p.partType] || p.partType;
|
||||
if (p.partType === "text" && p.autoConfig?.textValue) return `${type}("${p.autoConfig.textValue}")`;
|
||||
if (p.partType === "sequence" && p.autoConfig?.sequenceLength) return `${type}(${p.autoConfig.sequenceLength}자리)`;
|
||||
if (p.partType === "date" && p.autoConfig?.dateFormat) return `${type}(${p.autoConfig.dateFormat})`;
|
||||
if (p.partType === "category") return `${type}(${p.autoConfig?.categoryKey || ""})`;
|
||||
if (p.partType === "reference") return `${type}(${p.autoConfig?.referenceColumnName || ""})`;
|
||||
return type;
|
||||
})
|
||||
.join(` ${ruleConfig.separator || "-"} `);
|
||||
|
||||
auditLogService.log({
|
||||
companyCode,
|
||||
userId,
|
||||
userName: req.user?.userName,
|
||||
action: isUpdate ? "UPDATE" : "CREATE",
|
||||
resourceType: "NUMBERING_RULE",
|
||||
resourceId: String(savedRule.ruleId),
|
||||
resourceName: ruleConfig.ruleName,
|
||||
tableName: "numbering_rules",
|
||||
summary: isUpdate
|
||||
? `채번 규칙 "${ruleConfig.ruleName}" 수정`
|
||||
: `채번 규칙 "${ruleConfig.ruleName}" 생성`,
|
||||
changes: {
|
||||
after: {
|
||||
규칙명: ruleConfig.ruleName,
|
||||
적용테이블: ruleConfig.tableName || "(미지정)",
|
||||
적용컬럼: ruleConfig.columnName || "(미지정)",
|
||||
구분자: ruleConfig.separator || "-",
|
||||
리셋주기: resetPeriodLabel[ruleConfig.resetPeriod] || ruleConfig.resetPeriod || "초기화 안함",
|
||||
적용범위: ruleConfig.scopeType === "menu" ? "메뉴별" : "전역",
|
||||
코드구성: partsDescription || "(파트 없음)",
|
||||
파트수: (ruleConfig.parts || []).length,
|
||||
},
|
||||
},
|
||||
ipAddress: getClientIp(req),
|
||||
requestPath: req.originalUrl,
|
||||
});
|
||||
|
||||
return res.json({ success: true, data: savedRule });
|
||||
} catch (error: any) {
|
||||
logger.error("[테스트] 채번 규칙 저장 실패", { error: error.message });
|
||||
@@ -536,10 +589,25 @@ router.delete(
|
||||
authenticateToken,
|
||||
async (req: AuthenticatedRequest, res: Response) => {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const userId = req.user!.userId;
|
||||
const { ruleId } = req.params;
|
||||
|
||||
try {
|
||||
await numberingRuleService.deleteRuleFromTest(ruleId, companyCode);
|
||||
|
||||
auditLogService.log({
|
||||
companyCode,
|
||||
userId,
|
||||
userName: req.user?.userName,
|
||||
action: "DELETE",
|
||||
resourceType: "NUMBERING_RULE",
|
||||
resourceId: ruleId,
|
||||
tableName: "numbering_rules",
|
||||
summary: `채번 규칙(ID:${ruleId}) 삭제`,
|
||||
ipAddress: getClientIp(req),
|
||||
requestPath: req.originalUrl,
|
||||
});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
message: "테스트 채번 규칙이 삭제되었습니다",
|
||||
|
||||
291
backend-node/src/controllers/popProductionController.ts
Normal file
291
backend-node/src/controllers/popProductionController.ts
Normal file
@@ -0,0 +1,291 @@
|
||||
import { Response } from "express";
|
||||
import { getPool } from "../database/db";
|
||||
import logger from "../utils/logger";
|
||||
import { AuthenticatedRequest } from "../middleware/authMiddleware";
|
||||
|
||||
/**
|
||||
* D-BE1: 작업지시 공정 일괄 생성
|
||||
* PC에서 작업지시 생성 후 호출. 1 트랜잭션으로 work_order_process + process_work_result 일괄 생성.
|
||||
*/
|
||||
export const createWorkProcesses = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
) => {
|
||||
const pool = getPool();
|
||||
const client = await pool.connect();
|
||||
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const userId = req.user!.userId;
|
||||
|
||||
const { work_instruction_id, item_code, routing_version_id, plan_qty } =
|
||||
req.body;
|
||||
|
||||
if (!work_instruction_id || !routing_version_id) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message:
|
||||
"work_instruction_id와 routing_version_id는 필수입니다.",
|
||||
});
|
||||
}
|
||||
|
||||
logger.info("[pop/production] create-work-processes 요청", {
|
||||
companyCode,
|
||||
userId,
|
||||
work_instruction_id,
|
||||
item_code,
|
||||
routing_version_id,
|
||||
plan_qty,
|
||||
});
|
||||
|
||||
await client.query("BEGIN");
|
||||
|
||||
// 중복 호출 방지: 이미 생성된 공정이 있는지 확인
|
||||
const existCheck = await client.query(
|
||||
`SELECT COUNT(*) as cnt FROM work_order_process
|
||||
WHERE wo_id = $1 AND company_code = $2`,
|
||||
[work_instruction_id, companyCode]
|
||||
);
|
||||
if (parseInt(existCheck.rows[0].cnt, 10) > 0) {
|
||||
await client.query("ROLLBACK");
|
||||
return res.status(409).json({
|
||||
success: false,
|
||||
message: "이미 공정이 생성된 작업지시입니다.",
|
||||
});
|
||||
}
|
||||
|
||||
// 1. item_routing_detail + process_mng JOIN (공정 목록 + 공정명)
|
||||
const routingDetails = await client.query(
|
||||
`SELECT rd.id, rd.seq_no, rd.process_code,
|
||||
COALESCE(pm.process_name, rd.process_code) as process_name,
|
||||
rd.is_required, rd.is_fixed_order, rd.standard_time
|
||||
FROM item_routing_detail rd
|
||||
LEFT JOIN process_mng pm ON pm.process_code = rd.process_code
|
||||
AND pm.company_code = rd.company_code
|
||||
WHERE rd.routing_version_id = $1 AND rd.company_code = $2
|
||||
ORDER BY CAST(rd.seq_no AS int) NULLS LAST`,
|
||||
[routing_version_id, companyCode]
|
||||
);
|
||||
|
||||
if (routingDetails.rows.length === 0) {
|
||||
await client.query("ROLLBACK");
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "라우팅 버전에 등록된 공정이 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
const processes: Array<{
|
||||
id: string;
|
||||
seq_no: string;
|
||||
process_name: string;
|
||||
checklist_count: number;
|
||||
}> = [];
|
||||
let totalChecklists = 0;
|
||||
|
||||
for (const rd of routingDetails.rows) {
|
||||
// 2. work_order_process INSERT
|
||||
const wopResult = await client.query(
|
||||
`INSERT INTO work_order_process (
|
||||
company_code, wo_id, seq_no, process_code, process_name,
|
||||
is_required, is_fixed_order, standard_time, plan_qty,
|
||||
status, routing_detail_id, writer
|
||||
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12)
|
||||
RETURNING id`,
|
||||
[
|
||||
companyCode,
|
||||
work_instruction_id,
|
||||
rd.seq_no,
|
||||
rd.process_code,
|
||||
rd.process_name,
|
||||
rd.is_required,
|
||||
rd.is_fixed_order,
|
||||
rd.standard_time,
|
||||
plan_qty || null,
|
||||
"waiting",
|
||||
rd.id,
|
||||
userId,
|
||||
]
|
||||
);
|
||||
const wopId = wopResult.rows[0].id;
|
||||
|
||||
// 3. process_work_result INSERT (스냅샷 복사)
|
||||
// process_work_item + process_work_item_detail에서 해당 routing_detail의 항목 조회 후 복사
|
||||
const snapshotResult = await client.query(
|
||||
`INSERT INTO process_work_result (
|
||||
company_code, work_order_process_id,
|
||||
source_work_item_id, source_detail_id,
|
||||
work_phase, item_title, item_sort_order,
|
||||
detail_content, detail_type, detail_sort_order, is_required,
|
||||
inspection_code, inspection_method, unit, lower_limit, upper_limit,
|
||||
input_type, lookup_target, display_fields, duration_minutes,
|
||||
status, writer
|
||||
)
|
||||
SELECT
|
||||
pwi.company_code, $1,
|
||||
pwi.id, pwd.id,
|
||||
pwi.work_phase, pwi.title, pwi.sort_order::text,
|
||||
pwd.content, pwd.detail_type, pwd.sort_order::text, pwd.is_required,
|
||||
pwd.inspection_code, pwd.inspection_method, pwd.unit, pwd.lower_limit, pwd.upper_limit,
|
||||
pwd.input_type, pwd.lookup_target, pwd.display_fields, pwd.duration_minutes::text,
|
||||
'pending', $2
|
||||
FROM process_work_item pwi
|
||||
JOIN process_work_item_detail pwd ON pwd.work_item_id = pwi.id
|
||||
AND pwd.company_code = pwi.company_code
|
||||
WHERE pwi.routing_detail_id = $3
|
||||
AND pwi.company_code = $4
|
||||
ORDER BY pwi.sort_order, pwd.sort_order`,
|
||||
[wopId, userId, rd.id, companyCode]
|
||||
);
|
||||
|
||||
const checklistCount = snapshotResult.rowCount ?? 0;
|
||||
totalChecklists += checklistCount;
|
||||
|
||||
processes.push({
|
||||
id: wopId,
|
||||
seq_no: rd.seq_no,
|
||||
process_name: rd.process_name,
|
||||
checklist_count: checklistCount,
|
||||
});
|
||||
|
||||
logger.info("[pop/production] 공정 생성 완료", {
|
||||
wopId,
|
||||
processName: rd.process_name,
|
||||
checklistCount,
|
||||
});
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
|
||||
logger.info("[pop/production] create-work-processes 완료", {
|
||||
companyCode,
|
||||
work_instruction_id,
|
||||
total_processes: processes.length,
|
||||
total_checklists: totalChecklists,
|
||||
});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: {
|
||||
processes,
|
||||
total_processes: processes.length,
|
||||
total_checklists: totalChecklists,
|
||||
},
|
||||
});
|
||||
} catch (error: any) {
|
||||
await client.query("ROLLBACK");
|
||||
logger.error("[pop/production] create-work-processes 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || "공정 생성 중 오류가 발생했습니다.",
|
||||
});
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
* D-BE2: 타이머 API (시작/일시정지/재시작)
|
||||
*/
|
||||
export const controlTimer = async (
|
||||
req: AuthenticatedRequest,
|
||||
res: Response
|
||||
) => {
|
||||
const pool = getPool();
|
||||
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const userId = req.user!.userId;
|
||||
|
||||
const { work_order_process_id, action } = req.body;
|
||||
|
||||
if (!work_order_process_id || !action) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "work_order_process_id와 action은 필수입니다.",
|
||||
});
|
||||
}
|
||||
|
||||
if (!["start", "pause", "resume"].includes(action)) {
|
||||
return res.status(400).json({
|
||||
success: false,
|
||||
message: "action은 start, pause, resume 중 하나여야 합니다.",
|
||||
});
|
||||
}
|
||||
|
||||
logger.info("[pop/production] timer 요청", {
|
||||
companyCode,
|
||||
userId,
|
||||
work_order_process_id,
|
||||
action,
|
||||
});
|
||||
|
||||
let result;
|
||||
|
||||
switch (action) {
|
||||
case "start":
|
||||
// 최초 1회만 설정, 이미 있으면 무시
|
||||
result = await pool.query(
|
||||
`UPDATE work_order_process
|
||||
SET started_at = CASE WHEN started_at IS NULL THEN NOW()::text ELSE started_at END,
|
||||
status = CASE WHEN status = 'waiting' THEN 'in_progress' ELSE status END,
|
||||
updated_date = NOW()
|
||||
WHERE id = $1 AND company_code = $2
|
||||
RETURNING id, started_at, status`,
|
||||
[work_order_process_id, companyCode]
|
||||
);
|
||||
break;
|
||||
|
||||
case "pause":
|
||||
result = await pool.query(
|
||||
`UPDATE work_order_process
|
||||
SET paused_at = NOW()::text,
|
||||
updated_date = NOW()
|
||||
WHERE id = $1 AND company_code = $2 AND paused_at IS NULL
|
||||
RETURNING id, paused_at`,
|
||||
[work_order_process_id, companyCode]
|
||||
);
|
||||
break;
|
||||
|
||||
case "resume":
|
||||
// 일시정지 시간 누적 후 paused_at 초기화
|
||||
result = await pool.query(
|
||||
`UPDATE work_order_process
|
||||
SET total_paused_time = (
|
||||
COALESCE(total_paused_time::int, 0)
|
||||
+ EXTRACT(EPOCH FROM NOW() - paused_at::timestamp)::int
|
||||
)::text,
|
||||
paused_at = NULL,
|
||||
updated_date = NOW()
|
||||
WHERE id = $1 AND company_code = $2 AND paused_at IS NOT NULL
|
||||
RETURNING id, total_paused_time`,
|
||||
[work_order_process_id, companyCode]
|
||||
);
|
||||
break;
|
||||
}
|
||||
|
||||
if (!result || result.rowCount === 0) {
|
||||
return res.status(404).json({
|
||||
success: false,
|
||||
message: "대상 공정을 찾을 수 없거나 현재 상태에서 수행할 수 없습니다.",
|
||||
});
|
||||
}
|
||||
|
||||
logger.info("[pop/production] timer 완료", {
|
||||
action,
|
||||
work_order_process_id,
|
||||
result: result.rows[0],
|
||||
});
|
||||
|
||||
return res.json({
|
||||
success: true,
|
||||
data: result.rows[0],
|
||||
});
|
||||
} catch (error: any) {
|
||||
logger.error("[pop/production] timer 오류:", error);
|
||||
return res.status(500).json({
|
||||
success: false,
|
||||
message: error.message || "타이머 처리 중 오류가 발생했습니다.",
|
||||
});
|
||||
}
|
||||
};
|
||||
@@ -30,26 +30,68 @@ export async function getItemsWithRouting(req: AuthenticatedRequest, res: Respon
|
||||
routingTable = "item_routing_version",
|
||||
routingFkColumn = "item_code",
|
||||
search = "",
|
||||
extraColumns = "",
|
||||
filterConditions = "",
|
||||
} = req.query as Record<string, string>;
|
||||
|
||||
const searchCondition = search
|
||||
? `AND (i.${nameColumn} ILIKE $2 OR i.${codeColumn} ILIKE $2)`
|
||||
: "";
|
||||
const params: any[] = [companyCode];
|
||||
if (search) params.push(`%${search}%`);
|
||||
let paramIndex = 2;
|
||||
|
||||
// 검색 조건
|
||||
let searchCondition = "";
|
||||
if (search) {
|
||||
searchCondition = `AND (i.${nameColumn} ILIKE $${paramIndex} OR i.${codeColumn} ILIKE $${paramIndex})`;
|
||||
params.push(`%${search}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
// 추가 컬럼 SELECT
|
||||
const extraColumnNames: string[] = extraColumns
|
||||
? extraColumns.split(",").map((c: string) => c.trim()).filter(Boolean)
|
||||
: [];
|
||||
const extraSelect = extraColumnNames.map((col) => `i.${col}`).join(", ");
|
||||
const extraGroupBy = extraColumnNames.map((col) => `i.${col}`).join(", ");
|
||||
|
||||
// 사전 필터 조건
|
||||
let filterWhere = "";
|
||||
if (filterConditions) {
|
||||
try {
|
||||
const filters = JSON.parse(filterConditions) as Array<{
|
||||
column: string;
|
||||
operator: string;
|
||||
value: string;
|
||||
}>;
|
||||
for (const f of filters) {
|
||||
if (!f.column || !f.value) continue;
|
||||
if (f.operator === "equals") {
|
||||
filterWhere += ` AND i.${f.column} = $${paramIndex}`;
|
||||
params.push(f.value);
|
||||
} else if (f.operator === "contains") {
|
||||
filterWhere += ` AND i.${f.column} ILIKE $${paramIndex}`;
|
||||
params.push(`%${f.value}%`);
|
||||
} else if (f.operator === "not_equals") {
|
||||
filterWhere += ` AND i.${f.column} != $${paramIndex}`;
|
||||
params.push(f.value);
|
||||
}
|
||||
paramIndex++;
|
||||
}
|
||||
} catch { /* 파싱 실패 시 무시 */ }
|
||||
}
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
i.id,
|
||||
i.${nameColumn} AS item_name,
|
||||
i.${codeColumn} AS item_code,
|
||||
i.${codeColumn} AS item_code
|
||||
${extraSelect ? ", " + extraSelect : ""},
|
||||
COUNT(rv.id) AS routing_count
|
||||
FROM ${tableName} i
|
||||
LEFT JOIN ${routingTable} rv ON rv.${routingFkColumn} = i.${codeColumn}
|
||||
AND rv.company_code = i.company_code
|
||||
WHERE i.company_code = $1
|
||||
${searchCondition}
|
||||
GROUP BY i.id, i.${nameColumn}, i.${codeColumn}, i.created_date
|
||||
${filterWhere}
|
||||
GROUP BY i.id, i.${nameColumn}, i.${codeColumn}${extraGroupBy ? ", " + extraGroupBy : ""}, i.created_date
|
||||
ORDER BY i.created_date DESC NULLS LAST
|
||||
`;
|
||||
|
||||
@@ -711,3 +753,184 @@ export async function saveAll(req: AuthenticatedRequest, res: Response) {
|
||||
client.release();
|
||||
}
|
||||
}
|
||||
|
||||
// ============================================================
|
||||
// 등록 품목 관리 (item_routing_registered)
|
||||
// ============================================================
|
||||
|
||||
/**
|
||||
* 화면별 등록된 품목 목록 조회
|
||||
*/
|
||||
export async function getRegisteredItems(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({ success: false, message: "인증 필요" });
|
||||
}
|
||||
|
||||
const { screenCode } = req.params;
|
||||
const {
|
||||
tableName = "item_info",
|
||||
nameColumn = "item_name",
|
||||
codeColumn = "item_number",
|
||||
routingTable = "item_routing_version",
|
||||
routingFkColumn = "item_code",
|
||||
search = "",
|
||||
extraColumns = "",
|
||||
} = req.query as Record<string, string>;
|
||||
|
||||
const params: any[] = [companyCode, screenCode];
|
||||
let paramIndex = 3;
|
||||
|
||||
let searchCondition = "";
|
||||
if (search) {
|
||||
searchCondition = `AND (i.${nameColumn} ILIKE $${paramIndex} OR i.${codeColumn} ILIKE $${paramIndex})`;
|
||||
params.push(`%${search}%`);
|
||||
paramIndex++;
|
||||
}
|
||||
|
||||
const extraColumnNames: string[] = extraColumns
|
||||
? extraColumns.split(",").map((c: string) => c.trim()).filter(Boolean)
|
||||
: [];
|
||||
const extraSelect = extraColumnNames.map((col) => `i.${col}`).join(", ");
|
||||
const extraGroupBy = extraColumnNames.map((col) => `i.${col}`).join(", ");
|
||||
|
||||
const query = `
|
||||
SELECT
|
||||
irr.id AS registered_id,
|
||||
irr.sort_order,
|
||||
i.id,
|
||||
i.${nameColumn} AS item_name,
|
||||
i.${codeColumn} AS item_code
|
||||
${extraSelect ? ", " + extraSelect : ""},
|
||||
COUNT(rv.id) AS routing_count
|
||||
FROM item_routing_registered irr
|
||||
JOIN ${tableName} i ON irr.item_id = i.id
|
||||
AND i.company_code = irr.company_code
|
||||
LEFT JOIN ${routingTable} rv ON rv.${routingFkColumn} = i.${codeColumn}
|
||||
AND rv.company_code = i.company_code
|
||||
WHERE irr.company_code = $1
|
||||
AND irr.screen_code = $2
|
||||
${searchCondition}
|
||||
GROUP BY irr.id, irr.sort_order, i.id, i.${nameColumn}, i.${codeColumn}${extraGroupBy ? ", " + extraGroupBy : ""}
|
||||
ORDER BY CAST(irr.sort_order AS int) ASC, irr.created_date ASC
|
||||
`;
|
||||
|
||||
const result = await getPool().query(query, params);
|
||||
return res.json({ success: true, data: result.rows });
|
||||
} catch (error: any) {
|
||||
logger.error("등록 품목 조회 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 품목 등록 (화면에 품목 추가)
|
||||
*/
|
||||
export async function registerItem(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({ success: false, message: "인증 필요" });
|
||||
}
|
||||
|
||||
const { screenCode, itemId, itemCode } = req.body;
|
||||
if (!screenCode || !itemId) {
|
||||
return res.status(400).json({ success: false, message: "screenCode, itemId 필수" });
|
||||
}
|
||||
|
||||
const query = `
|
||||
INSERT INTO item_routing_registered (screen_code, item_id, item_code, company_code, writer)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (screen_code, item_id, company_code) DO NOTHING
|
||||
RETURNING *
|
||||
`;
|
||||
const result = await getPool().query(query, [
|
||||
screenCode, itemId, itemCode || null, companyCode, req.user?.userId || null,
|
||||
]);
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
return res.json({ success: true, message: "이미 등록된 품목입니다", data: null });
|
||||
}
|
||||
|
||||
logger.info("품목 등록", { companyCode, screenCode, itemId });
|
||||
return res.json({ success: true, data: result.rows[0] });
|
||||
} catch (error: any) {
|
||||
logger.error("품목 등록 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 여러 품목 일괄 등록
|
||||
*/
|
||||
export async function registerItemsBatch(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({ success: false, message: "인증 필요" });
|
||||
}
|
||||
|
||||
const { screenCode, items } = req.body;
|
||||
if (!screenCode || !Array.isArray(items) || items.length === 0) {
|
||||
return res.status(400).json({ success: false, message: "screenCode, items[] 필수" });
|
||||
}
|
||||
|
||||
const client = await getPool().connect();
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
const inserted: any[] = [];
|
||||
|
||||
for (const item of items) {
|
||||
const result = await client.query(
|
||||
`INSERT INTO item_routing_registered (screen_code, item_id, item_code, company_code, writer)
|
||||
VALUES ($1, $2, $3, $4, $5)
|
||||
ON CONFLICT (screen_code, item_id, company_code) DO NOTHING
|
||||
RETURNING *`,
|
||||
[screenCode, item.itemId, item.itemCode || null, companyCode, req.user?.userId || null]
|
||||
);
|
||||
if (result.rows[0]) inserted.push(result.rows[0]);
|
||||
}
|
||||
|
||||
await client.query("COMMIT");
|
||||
logger.info("품목 일괄 등록", { companyCode, screenCode, count: inserted.length });
|
||||
return res.json({ success: true, data: inserted });
|
||||
} catch (err) {
|
||||
await client.query("ROLLBACK");
|
||||
throw err;
|
||||
} finally {
|
||||
client.release();
|
||||
}
|
||||
} catch (error: any) {
|
||||
logger.error("품목 일괄 등록 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 등록 품목 제거
|
||||
*/
|
||||
export async function unregisterItem(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user?.companyCode;
|
||||
if (!companyCode) {
|
||||
return res.status(401).json({ success: false, message: "인증 필요" });
|
||||
}
|
||||
|
||||
const { id } = req.params;
|
||||
const result = await getPool().query(
|
||||
`DELETE FROM item_routing_registered WHERE id = $1 AND company_code = $2 RETURNING id`,
|
||||
[id, companyCode]
|
||||
);
|
||||
|
||||
if (result.rowCount === 0) {
|
||||
return res.status(404).json({ success: false, message: "데이터를 찾을 수 없습니다" });
|
||||
}
|
||||
|
||||
logger.info("등록 품목 제거", { companyCode, id });
|
||||
return res.json({ success: true });
|
||||
} catch (error: any) {
|
||||
logger.error("등록 품목 제거 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
191
backend-node/src/controllers/productionController.ts
Normal file
191
backend-node/src/controllers/productionController.ts
Normal file
@@ -0,0 +1,191 @@
|
||||
/**
|
||||
* 생산계획 컨트롤러
|
||||
*/
|
||||
|
||||
import { Response } from "express";
|
||||
import { AuthenticatedRequest } from "../types/auth";
|
||||
import * as productionService from "../services/productionPlanService";
|
||||
import { logger } from "../utils/logger";
|
||||
|
||||
// ─── 수주 데이터 조회 (품목별 그룹핑) ───
|
||||
|
||||
export async function getOrderSummary(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const { excludePlanned, itemCode, itemName } = req.query;
|
||||
|
||||
const data = await productionService.getOrderSummary(companyCode, {
|
||||
excludePlanned: excludePlanned === "true",
|
||||
itemCode: itemCode as string,
|
||||
itemName: itemName as string,
|
||||
});
|
||||
|
||||
return res.json({ success: true, data });
|
||||
} catch (error: any) {
|
||||
logger.error("수주 데이터 조회 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 안전재고 부족분 조회 ───
|
||||
|
||||
export async function getStockShortage(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const data = await productionService.getStockShortage(companyCode);
|
||||
return res.json({ success: true, data });
|
||||
} catch (error: any) {
|
||||
logger.error("안전재고 부족분 조회 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 생산계획 상세 조회 ───
|
||||
|
||||
export async function getPlanById(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const planId = parseInt(req.params.id, 10);
|
||||
const data = await productionService.getPlanById(companyCode, planId);
|
||||
|
||||
if (!data) {
|
||||
return res.status(404).json({ success: false, message: "생산계획을 찾을 수 없습니다" });
|
||||
}
|
||||
return res.json({ success: true, data });
|
||||
} catch (error: any) {
|
||||
logger.error("생산계획 조회 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 생산계획 수정 ───
|
||||
|
||||
export async function updatePlan(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const planId = parseInt(req.params.id, 10);
|
||||
const updatedBy = req.user!.userId;
|
||||
|
||||
const data = await productionService.updatePlan(companyCode, planId, req.body, updatedBy);
|
||||
return res.json({ success: true, data });
|
||||
} catch (error: any) {
|
||||
logger.error("생산계획 수정 실패", { error: error.message });
|
||||
return res.status(error.message.includes("찾을 수 없") ? 404 : 500).json({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 생산계획 삭제 ───
|
||||
|
||||
export async function deletePlan(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const planId = parseInt(req.params.id, 10);
|
||||
|
||||
await productionService.deletePlan(companyCode, planId);
|
||||
return res.json({ success: true, message: "삭제되었습니다" });
|
||||
} catch (error: any) {
|
||||
logger.error("생산계획 삭제 실패", { error: error.message });
|
||||
return res.status(error.message.includes("찾을 수 없") ? 404 : 500).json({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 자동 스케줄 생성 ───
|
||||
|
||||
export async function generateSchedule(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const createdBy = req.user!.userId;
|
||||
const { items, options } = req.body;
|
||||
|
||||
if (!items || !Array.isArray(items) || items.length === 0) {
|
||||
return res.status(400).json({ success: false, message: "품목 정보가 필요합니다" });
|
||||
}
|
||||
|
||||
const data = await productionService.generateSchedule(companyCode, items, options || {}, createdBy);
|
||||
return res.json({ success: true, data });
|
||||
} catch (error: any) {
|
||||
logger.error("자동 스케줄 생성 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 스케줄 병합 ───
|
||||
|
||||
export async function mergeSchedules(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const mergedBy = req.user!.userId;
|
||||
const { schedule_ids, product_type } = req.body;
|
||||
|
||||
if (!schedule_ids || !Array.isArray(schedule_ids) || schedule_ids.length < 2) {
|
||||
return res.status(400).json({ success: false, message: "2개 이상의 스케줄을 선택해주세요" });
|
||||
}
|
||||
|
||||
const data = await productionService.mergeSchedules(
|
||||
companyCode,
|
||||
schedule_ids,
|
||||
product_type || "완제품",
|
||||
mergedBy
|
||||
);
|
||||
return res.json({ success: true, data });
|
||||
} catch (error: any) {
|
||||
logger.error("스케줄 병합 실패", { error: error.message });
|
||||
const status = error.message.includes("동일 품목") || error.message.includes("찾을 수 없") ? 400 : 500;
|
||||
return res.status(status).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 반제품 계획 자동 생성 ───
|
||||
|
||||
export async function generateSemiSchedule(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const createdBy = req.user!.userId;
|
||||
const { plan_ids, options } = req.body;
|
||||
|
||||
if (!plan_ids || !Array.isArray(plan_ids) || plan_ids.length === 0) {
|
||||
return res.status(400).json({ success: false, message: "완제품 계획을 선택해주세요" });
|
||||
}
|
||||
|
||||
const data = await productionService.generateSemiSchedule(
|
||||
companyCode,
|
||||
plan_ids,
|
||||
options || {},
|
||||
createdBy
|
||||
);
|
||||
return res.json({ success: true, data });
|
||||
} catch (error: any) {
|
||||
logger.error("반제품 계획 생성 실패", { error: error.message });
|
||||
return res.status(500).json({ success: false, message: error.message });
|
||||
}
|
||||
}
|
||||
|
||||
// ─── 스케줄 분할 ───
|
||||
|
||||
export async function splitSchedule(req: AuthenticatedRequest, res: Response) {
|
||||
try {
|
||||
const companyCode = req.user!.companyCode;
|
||||
const splitBy = req.user!.userId;
|
||||
const planId = parseInt(req.params.id, 10);
|
||||
const { split_qty } = req.body;
|
||||
|
||||
if (!split_qty || split_qty <= 0) {
|
||||
return res.status(400).json({ success: false, message: "분할 수량을 입력해주세요" });
|
||||
}
|
||||
|
||||
const data = await productionService.splitSchedule(companyCode, planId, split_qty, splitBy);
|
||||
return res.json({ success: true, data });
|
||||
} catch (error: any) {
|
||||
logger.error("스케줄 분할 실패", { error: error.message });
|
||||
return res.status(error.message.includes("찾을 수 없") ? 404 : 400).json({
|
||||
success: false,
|
||||
message: error.message,
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -614,20 +614,6 @@ export const copyScreenWithModals = async (
|
||||
modalScreens: modalScreens || [],
|
||||
});
|
||||
|
||||
auditLogService.log({
|
||||
companyCode: targetCompanyCode || companyCode,
|
||||
userId: userId || "",
|
||||
userName: (req.user as any)?.userName || "",
|
||||
action: "COPY",
|
||||
resourceType: "SCREEN",
|
||||
resourceId: id,
|
||||
resourceName: mainScreen?.screenName,
|
||||
summary: `화면 일괄 복사 (메인 1개 + 모달 ${result.modalScreens.length}개, 원본 ID:${id})`,
|
||||
changes: { after: { sourceScreenId: id, targetCompanyCode, mainScreenName: mainScreen?.screenName } },
|
||||
ipAddress: getClientIp(req),
|
||||
requestPath: req.originalUrl,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: result,
|
||||
@@ -663,20 +649,6 @@ export const copyScreen = async (
|
||||
}
|
||||
);
|
||||
|
||||
auditLogService.log({
|
||||
companyCode,
|
||||
userId: userId || "",
|
||||
userName: (req.user as any)?.userName || "",
|
||||
action: "COPY",
|
||||
resourceType: "SCREEN",
|
||||
resourceId: String(copiedScreen?.screenId || ""),
|
||||
resourceName: screenName,
|
||||
summary: `화면 "${screenName}" 복사 (원본 ID:${id})`,
|
||||
changes: { after: { sourceScreenId: id, screenName, screenCode } },
|
||||
ipAddress: getClientIp(req),
|
||||
requestPath: req.originalUrl,
|
||||
});
|
||||
|
||||
res.json({
|
||||
success: true,
|
||||
data: copiedScreen,
|
||||
|
||||
@@ -963,6 +963,15 @@ export async function addTableData(
|
||||
|
||||
logger.info(`테이블 데이터 추가 완료: ${tableName}, id: ${result.insertedId}`);
|
||||
|
||||
const systemFields = new Set([
|
||||
"id", "created_date", "updated_date", "writer", "company_code",
|
||||
"createdDate", "updatedDate", "companyCode",
|
||||
]);
|
||||
const auditData: Record<string, any> = {};
|
||||
for (const [k, v] of Object.entries(data)) {
|
||||
if (!systemFields.has(k)) auditData[k] = v;
|
||||
}
|
||||
|
||||
auditLogService.log({
|
||||
companyCode: req.user?.companyCode || "",
|
||||
userId: req.user?.userId || "",
|
||||
@@ -973,7 +982,7 @@ export async function addTableData(
|
||||
resourceName: tableName,
|
||||
tableName,
|
||||
summary: `${tableName} 데이터 추가`,
|
||||
changes: { after: data },
|
||||
changes: { after: auditData },
|
||||
ipAddress: getClientIp(req),
|
||||
requestPath: req.originalUrl,
|
||||
});
|
||||
@@ -1096,10 +1105,14 @@ export async function editTableData(
|
||||
return;
|
||||
}
|
||||
|
||||
// 변경된 필드만 추출
|
||||
const systemFieldsForEdit = new Set([
|
||||
"id", "created_date", "updated_date", "writer", "company_code",
|
||||
"createdDate", "updatedDate", "companyCode",
|
||||
]);
|
||||
const changedBefore: Record<string, any> = {};
|
||||
const changedAfter: Record<string, any> = {};
|
||||
for (const key of Object.keys(updatedData)) {
|
||||
if (systemFieldsForEdit.has(key)) continue;
|
||||
if (String(originalData[key] ?? "") !== String(updatedData[key] ?? "")) {
|
||||
changedBefore[key] = originalData[key];
|
||||
changedAfter[key] = updatedData[key];
|
||||
|
||||
Reference in New Issue
Block a user