Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node

This commit is contained in:
kjs
2026-03-30 11:51:45 +09:00
27 changed files with 5213 additions and 698 deletions
+16
View File
@@ -128,6 +128,7 @@ import screenEmbeddingRoutes from "./routes/screenEmbeddingRoutes"; // 화면
import screenGroupRoutes from "./routes/screenGroupRoutes"; // 화면 그룹 관리
import popActionRoutes from "./routes/popActionRoutes"; // POP 액션 실행
import popProductionRoutes from "./routes/popProductionRoutes"; // POP 생산 관리 (공정 생성/타이머)
import inspectionResultRoutes from "./routes/inspectionResultRoutes"; // POP 검사 결과 관리
import vehicleTripRoutes from "./routes/vehicleTripRoutes"; // 차량 운행 이력 관리
import approvalRoutes from "./routes/approvalRoutes"; // 결재 시스템
import driverRoutes from "./routes/driverRoutes"; // 공차중계 운전자 관리
@@ -255,12 +256,26 @@ app.use("/api/", limiter);
app.use("/api/", refreshTokenIfNeeded);
// 헬스 체크 엔드포인트
let appVersion = "unknown";
try {
// 로컬: ../../package.json, Docker(/app/src/): ../package.json
// eslint-disable-next-line @typescript-eslint/no-var-requires
appVersion = require("../../package.json").version;
} catch {
try {
// eslint-disable-next-line @typescript-eslint/no-var-requires
appVersion = require("../package.json").version;
} catch {
/* version stays "unknown" */
}
}
app.get("/health", (req, res) => {
res.status(200).json({
status: "OK",
timestamp: new Date().toISOString(),
uptime: process.uptime(),
environment: config.nodeEnv,
version: appVersion,
});
});
@@ -274,6 +289,7 @@ app.use("/api/screen-management", screenManagementRoutes);
app.use("/api/screen-groups", screenGroupRoutes); // 화면 그룹 관리
app.use("/api/pop", popActionRoutes); // POP 액션 실행
app.use("/api/pop/production", popProductionRoutes); // POP 생산 관리
app.use("/api/pop/inspection-result", inspectionResultRoutes); // POP 검사 결과 관리
app.use("/api/common-codes", commonCodeRoutes);
app.use("/api/dynamic-form", dynamicFormRoutes);
app.use("/api/files", fileRoutes);
@@ -0,0 +1,191 @@
import { Router, Request, Response } from "express";
import { getPool } from "../database/db";
import { authenticateToken } from "../middleware/authMiddleware";
const router = Router();
router.use(authenticateToken);
// ---- 검사 기준 조회 (item_inspection_info) ----
// GET /api/pop/inspection-result/info?itemCode=ITEM-001&inspectionType=입고검사
router.get("/info", async (req: Request, res: Response) => {
const pool = getPool();
const companyCode = (req as any).user?.companyCode;
const { itemCode, itemId, inspectionType } = req.query;
if (!companyCode) {
return res.status(401).json({ success: false, message: "인증 정보 없음" });
}
const conditions: string[] = ["company_code = $1", "is_active = 'Y'"];
const params: unknown[] = [companyCode];
let idx = 2;
if (itemCode) {
conditions.push(`item_code = $${idx++}`);
params.push(itemCode);
}
if (itemId) {
conditions.push(`item_id = $${idx++}`);
params.push(itemId);
}
if (inspectionType) {
conditions.push(`inspection_type = $${idx++}`);
params.push(inspectionType);
}
const sql = `
SELECT id, item_id, item_code, item_name,
inspection_type, inspection_item_name, inspection_standard,
inspection_method, pass_criteria, is_required, sort_order, memo
FROM item_inspection_info
WHERE ${conditions.join(" AND ")}
ORDER BY sort_order, inspection_item_name
`;
try {
const result = await pool.query(sql, params);
return res.json({ success: true, data: result.rows });
} catch (err: any) {
return res.status(500).json({ success: false, message: err.message });
}
});
// ---- 검사 결과 조회 ----
// GET /api/pop/inspection-result?referenceId=xxx&referenceTable=yyy&screenId=zzz
router.get("/", async (req: Request, res: Response) => {
const pool = getPool();
const companyCode = (req as any).user?.companyCode;
const { referenceId, referenceTable, screenId } = req.query;
if (!companyCode) {
return res.status(401).json({ success: false, message: "인증 정보 없음" });
}
const conditions: string[] = ["company_code = $1"];
const params: unknown[] = [companyCode];
let idx = 2;
if (referenceId) {
conditions.push(`reference_id = $${idx++}`);
params.push(referenceId);
}
if (referenceTable) {
conditions.push(`reference_table = $${idx++}`);
params.push(referenceTable);
}
if (screenId) {
conditions.push(`screen_id = $${idx++}`);
params.push(screenId);
}
const sql = `
SELECT *
FROM inspection_result
WHERE ${conditions.join(" AND ")}
ORDER BY created_date DESC
`;
try {
const result = await pool.query(sql, params);
return res.json({ success: true, data: result.rows });
} catch (err: any) {
return res.status(500).json({ success: false, message: err.message });
}
});
// ---- 검사 결과 저장 (INSERT or UPDATE) ----
// POST /api/pop/inspection-result
router.post("/", async (req: Request, res: Response) => {
const pool = getPool();
const companyCode = (req as any).user?.companyCode;
const writer = (req as any).user?.userId;
if (!companyCode) {
return res.status(401).json({ success: false, message: "인증 정보 없음" });
}
const {
referenceTable,
referenceId,
screenId,
itemId,
itemCode,
itemName,
inspectionType,
items, // 검사 항목별 결과 배열
overallJudgment,
memo,
isCompleted,
} = req.body;
if (!items || !Array.isArray(items) || items.length === 0) {
return res.status(400).json({ success: false, message: "검사 항목이 없습니다" });
}
const client = await pool.connect();
try {
await client.query("BEGIN");
// 기존 결과 삭제 (동일 referenceId + referenceTable 기준 덮어쓰기)
if (referenceId && referenceTable) {
await client.query(
`DELETE FROM inspection_result
WHERE company_code = $1 AND reference_id = $2 AND reference_table = $3`,
[companyCode, referenceId, referenceTable]
);
}
const insertedIds: string[] = [];
for (const item of items) {
const completedFlag = isCompleted ? "Y" : "N";
const completedDate = isCompleted ? new Date() : null;
const insertSql = `
INSERT INTO inspection_result (
company_code, writer,
reference_table, reference_id, screen_id,
inspection_info_id, item_id, item_code, item_name,
inspection_type, inspection_item_name, inspection_standard, pass_criteria, is_required,
measured_value, judgment, overall_judgment, memo,
is_completed, completed_date
) VALUES (
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20
)
RETURNING id
`;
const result = await client.query(insertSql, [
companyCode,
writer,
referenceTable || null,
referenceId || null,
screenId || null,
item.inspectionInfoId || null,
itemId || item.itemId || null,
itemCode || item.itemCode || null,
itemName || item.itemName || null,
inspectionType || item.inspectionType || null,
item.inspectionItemName || null,
item.inspectionStandard || null,
item.passCriteria || null,
item.isRequired || "Y",
item.measuredValue || null,
item.judgment || null,
overallJudgment || null,
memo || null,
completedFlag,
completedDate,
]);
insertedIds.push(result.rows[0].id);
}
await client.query("COMMIT");
return res.json({ success: true, data: { ids: insertedIds } });
} catch (err: any) {
await client.query("ROLLBACK");
return res.status(500).json({ success: false, message: err.message });
} finally {
client.release();
}
});
export default router;
+80 -1
View File
@@ -183,7 +183,8 @@ router.post("/execute-action", authenticateToken, async (req: Request, res: Resp
const cardMapping = mappings?.cardList;
const fieldMapping = mappings?.field;
if (cardMapping?.targetTable && Object.keys(cardMapping.columnMapping).length > 0) {
if (cardMapping?.targetTable && Object.keys(cardMapping.columnMapping).length > 0 && items.length > 0) {
// ── 카드리스트 기반 INSERT (기존: items 반복) ──
if (!isSafeIdentifier(cardMapping.targetTable)) {
throw new Error(`유효하지 않은 테이블명: ${cardMapping.targetTable}`);
}
@@ -300,6 +301,84 @@ router.post("/execute-action", authenticateToken, async (req: Request, res: Resp
insertedCount++;
}
}
} else if (
// ── 필드 단독 INSERT (카드리스트 없이 pop-field만으로 저장) ──
items.length === 0 &&
fieldMapping?.targetTable &&
Object.keys(fieldMapping.columnMapping).length > 0 &&
Object.keys(fieldValues).length > 0
) {
if (!isSafeIdentifier(fieldMapping.targetTable)) {
throw new Error(`유효하지 않은 테이블명: ${fieldMapping.targetTable}`);
}
const columns: string[] = ["company_code"];
const values: unknown[] = [companyCode];
// 필드 매핑 값 추가
for (const [sourceField, targetColumn] of Object.entries(fieldMapping.columnMapping)) {
if (!isSafeIdentifier(targetColumn)) continue;
columns.push(`"${targetColumn}"`);
values.push(fieldValues[sourceField] ?? null);
}
// hiddenMappings 처리
for (const hm of (fieldMapping.hiddenMappings ?? [])) {
if (!hm.targetColumn || !isSafeIdentifier(hm.targetColumn)) continue;
if (columns.includes(`"${hm.targetColumn}"`)) continue;
let value: unknown = null;
if (hm.valueSource === "static") {
value = hm.staticValue ?? null;
} else if (hm.valueSource === "db_column" && hm.sourceDbColumn) {
value = fieldValues[hm.sourceDbColumn] ?? null;
}
columns.push(`"${hm.targetColumn}"`);
values.push(value);
}
// autoGenMappings 채번 처리
for (const ag of (fieldMapping.autoGenMappings ?? [])) {
if (!ag.numberingRuleId || !ag.targetColumn) continue;
if (!isSafeIdentifier(ag.targetColumn)) continue;
if (columns.includes(`"${ag.targetColumn}"`)) continue;
try {
const generatedCode = await numberingRuleService.allocateCode(
ag.numberingRuleId, companyCode, fieldValues,
);
columns.push(`"${ag.targetColumn}"`);
values.push(generatedCode);
generatedCodes.push({ targetColumn: ag.targetColumn, code: generatedCode, showResultModal: ag.showResultModal ?? false });
} catch (err: any) {
logger.error("[pop/execute-action] 필드 단독 채번 실패", { ruleId: ag.numberingRuleId, error: err.message });
}
}
// 자동 필드 추가 (created_date, updated_date, writer)
if (!columns.includes('"created_date"')) {
columns.push('"created_date"');
values.push(new Date().toISOString());
}
if (!columns.includes('"updated_date"')) {
columns.push('"updated_date"');
values.push(new Date().toISOString());
}
if (!columns.includes('"writer"') && userId) {
columns.push('"writer"');
values.push(userId);
}
if (columns.length > 1) {
const placeholders = values.map((_, i) => `$${i + 1}`).join(", ");
await client.query(
`INSERT INTO "${fieldMapping.targetTable}" (${columns.join(", ")}) VALUES (${placeholders})`,
values,
);
insertedCount++;
logger.info("[pop/execute-action] 필드 단독 INSERT 실행", {
table: fieldMapping.targetTable,
columnCount: columns.length,
});
}
}
break;
}
@@ -5957,7 +5957,21 @@ export class ScreenManagementService {
return null;
}
const layoutData = layout.layout_data;
let layoutData = layout.layout_data;
// 이중 래핑 감지 및 자동 언래핑
// layout_data 컬럼에 { version, layout_data: { components, ... } } 형태로 저장된 경우
// 실제 레이아웃은 내부 layout_data에 있으므로 언래핑한다
if (
layoutData &&
layoutData.layout_data &&
typeof layoutData.layout_data === "object" &&
!layoutData.components &&
layoutData.layout_data.components
) {
console.log(`POP 레이아웃 이중 래핑 감지 (screen_id=${screenId}), 자동 언래핑`);
layoutData = layoutData.layout_data;
}
// v1 → v2 자동 마이그레이션
if (layoutData && layoutData.version === "pop-1.0") {
@@ -5994,10 +6008,22 @@ export class ScreenManagementService {
console.log(`=== POP 레이아웃 저장 (v5 그리드 시스템) ===`);
console.log(`화면 ID: ${screenId}, 회사: ${companyCode}`);
// 이중 래핑 방지: { version, layout_data: { components, ... } } 형태로 전달된 경우 언래핑
if (
layoutData &&
layoutData.layout_data &&
typeof layoutData.layout_data === "object" &&
!layoutData.components &&
layoutData.layout_data.components
) {
console.log(`저장 시 이중 래핑 감지 (screen_id=${screenId}), 자동 언래핑`);
layoutData = layoutData.layout_data;
}
// v5 그리드 레이아웃만 지원
const componentCount = Object.keys(layoutData.components || {}).length;
console.log(`컴포넌트: ${componentCount}`);
// v5 형식 검증
if (layoutData.version && layoutData.version !== "pop-5.0") {
console.warn(`레거시 버전 감지 (${layoutData.version}), v5로 변환 필요`);