Merge branch 'main' of http://39.117.244.52:3000/kjs/ERP-node into jskim-node
This commit is contained in:
@@ -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;
|
||||
@@ -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로 변환 필요`);
|
||||
|
||||
Reference in New Issue
Block a user