- Introduced a new cutting plan management page for COMPANY_9, allowing users to manage cutting plans effectively. - Added a Work Instruction Apply Modal to facilitate the application of work instructions linked to cutting plans. - Enhanced data handling by incorporating additional fields such as condition_unit, condition_base_value, condition_tolerance, condition_auto_collect, and condition_plc_data in relevant controllers and database interactions. - Updated UI components to support new features, including displaying batch numbers and item sizes in the work instruction page. These changes aim to improve the efficiency and usability of cutting plan and work instruction management processes.
1005 lines
34 KiB
TypeScript
1005 lines
34 KiB
TypeScript
/**
|
|
* 공정 작업기준 컨트롤러
|
|
* 품목별 라우팅/공정에 대한 작업 항목 및 상세 관리
|
|
*/
|
|
|
|
import { Response } from "express";
|
|
import { getPool } from "../database/db";
|
|
import { logger } from "../utils/logger";
|
|
import { AuthenticatedRequest } from "../types/auth";
|
|
|
|
// ============================================================
|
|
// 품목/라우팅/공정 조회 (좌측 트리 데이터)
|
|
// ============================================================
|
|
|
|
/**
|
|
* 라우팅이 있는 품목 목록 조회
|
|
* 요청 쿼리: tableName(품목테이블), nameColumn, codeColumn
|
|
*/
|
|
export async function getItemsWithRouting(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const companyCode = req.user?.companyCode;
|
|
if (!companyCode) {
|
|
return res.status(401).json({ success: false, message: "인증 필요" });
|
|
}
|
|
|
|
const {
|
|
tableName = "item_info",
|
|
nameColumn = "item_name",
|
|
codeColumn = "item_number",
|
|
routingTable = "item_routing_version",
|
|
routingFkColumn = "item_code",
|
|
search = "",
|
|
extraColumns = "",
|
|
filterConditions = "",
|
|
} = req.query as Record<string, string>;
|
|
|
|
const params: any[] = [companyCode];
|
|
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
|
|
${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}
|
|
${filterWhere}
|
|
GROUP BY i.id, i.${nameColumn}, i.${codeColumn}${extraGroupBy ? ", " + extraGroupBy : ""}, i.created_date
|
|
ORDER BY i.created_date DESC NULLS LAST
|
|
`;
|
|
|
|
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 getRoutingsWithProcesses(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const companyCode = req.user?.companyCode;
|
|
if (!companyCode) {
|
|
return res.status(401).json({ success: false, message: "인증 필요" });
|
|
}
|
|
|
|
const { itemCode } = req.params;
|
|
const {
|
|
routingVersionTable = "item_routing_version",
|
|
routingDetailTable = "item_routing_detail",
|
|
routingFkColumn = "item_code",
|
|
processTable = "process_mng",
|
|
processNameColumn = "process_name",
|
|
processCodeColumn = "process_code",
|
|
} = req.query as Record<string, string>;
|
|
|
|
// 라우팅 버전 목록
|
|
const versionsQuery = `
|
|
SELECT id, version_name, description, created_date, COALESCE(is_default, false) AS is_default
|
|
FROM ${routingVersionTable}
|
|
WHERE ${routingFkColumn} = $1 AND company_code = $2
|
|
ORDER BY is_default DESC, created_date DESC
|
|
`;
|
|
const versionsResult = await getPool().query(versionsQuery, [
|
|
itemCode,
|
|
companyCode,
|
|
]);
|
|
|
|
// 각 버전별 공정 목록
|
|
const routings = [];
|
|
for (const version of versionsResult.rows) {
|
|
const detailsQuery = `
|
|
SELECT
|
|
rd.id AS routing_detail_id,
|
|
rd.seq_no,
|
|
rd.process_code,
|
|
rd.is_required,
|
|
rd.work_type,
|
|
p.${processNameColumn} AS process_name
|
|
FROM ${routingDetailTable} rd
|
|
LEFT JOIN ${processTable} p ON p.${processCodeColumn} = rd.process_code
|
|
AND p.company_code = rd.company_code
|
|
WHERE rd.routing_version_id = $1 AND rd.company_code = $2
|
|
ORDER BY rd.seq_no::integer
|
|
`;
|
|
const detailsResult = await getPool().query(detailsQuery, [
|
|
version.id,
|
|
companyCode,
|
|
]);
|
|
|
|
routings.push({
|
|
...version,
|
|
processes: detailsResult.rows,
|
|
});
|
|
}
|
|
|
|
return res.json({ success: true, data: routings });
|
|
} catch (error: any) {
|
|
logger.error("라우팅/공정 조회 실패", { error: error.message });
|
|
return res.status(500).json({ success: false, message: error.message });
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// 기본 버전 설정
|
|
// ============================================================
|
|
|
|
/**
|
|
* 라우팅 버전을 기본 버전으로 설정
|
|
* 같은 품목의 다른 버전은 기본 해제
|
|
*/
|
|
export async function setDefaultVersion(req: AuthenticatedRequest, res: Response) {
|
|
const pool = getPool();
|
|
const client = await pool.connect();
|
|
try {
|
|
const companyCode = req.user?.companyCode;
|
|
if (!companyCode) {
|
|
return res.status(401).json({ success: false, message: "인증 필요" });
|
|
}
|
|
|
|
const { versionId } = req.params;
|
|
const {
|
|
routingVersionTable = "item_routing_version",
|
|
routingFkColumn = "item_code",
|
|
} = req.body;
|
|
|
|
await client.query("BEGIN");
|
|
|
|
const versionResult = await client.query(
|
|
`SELECT ${routingFkColumn} AS item_code FROM ${routingVersionTable} WHERE id = $1 AND company_code = $2`,
|
|
[versionId, companyCode]
|
|
);
|
|
|
|
if (versionResult.rowCount === 0) {
|
|
await client.query("ROLLBACK");
|
|
return res.status(404).json({ success: false, message: "버전을 찾을 수 없습니다" });
|
|
}
|
|
|
|
const itemCode = versionResult.rows[0].item_code;
|
|
|
|
await client.query(
|
|
`UPDATE ${routingVersionTable} SET is_default = false WHERE ${routingFkColumn} = $1 AND company_code = $2`,
|
|
[itemCode, companyCode]
|
|
);
|
|
|
|
await client.query(
|
|
`UPDATE ${routingVersionTable} SET is_default = true WHERE id = $1 AND company_code = $2`,
|
|
[versionId, companyCode]
|
|
);
|
|
|
|
await client.query("COMMIT");
|
|
|
|
logger.info("기본 버전 설정", { companyCode, versionId, itemCode });
|
|
return res.json({ success: true, message: "기본 버전이 설정되었습니다" });
|
|
} catch (error: any) {
|
|
await client.query("ROLLBACK");
|
|
logger.error("기본 버전 설정 실패", { error: error.message });
|
|
return res.status(500).json({ success: false, message: error.message });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 기본 버전 해제
|
|
*/
|
|
export async function unsetDefaultVersion(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const companyCode = req.user?.companyCode;
|
|
if (!companyCode) {
|
|
return res.status(401).json({ success: false, message: "인증 필요" });
|
|
}
|
|
|
|
const { versionId } = req.params;
|
|
const { routingVersionTable = "item_routing_version" } = req.body;
|
|
|
|
await getPool().query(
|
|
`UPDATE ${routingVersionTable} SET is_default = false WHERE id = $1 AND company_code = $2`,
|
|
[versionId, companyCode]
|
|
);
|
|
|
|
logger.info("기본 버전 해제", { companyCode, versionId });
|
|
return res.json({ success: true, message: "기본 버전이 해제되었습니다" });
|
|
} catch (error: any) {
|
|
logger.error("기본 버전 해제 실패", { error: error.message });
|
|
return res.status(500).json({ success: false, message: error.message });
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// 작업 항목 CRUD
|
|
// ============================================================
|
|
|
|
/**
|
|
* 공정별 작업 항목 목록 조회 (phase별 그룹)
|
|
*/
|
|
export async function getWorkItems(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const companyCode = req.user?.companyCode;
|
|
if (!companyCode) {
|
|
return res.status(401).json({ success: false, message: "인증 필요" });
|
|
}
|
|
|
|
const { routingDetailId } = req.params;
|
|
|
|
const query = `
|
|
SELECT
|
|
wi.id,
|
|
wi.routing_detail_id,
|
|
wi.work_phase,
|
|
wi.title,
|
|
wi.is_required,
|
|
wi.sort_order,
|
|
wi.description,
|
|
wi.created_date,
|
|
(SELECT COUNT(*) FROM process_work_item_detail d
|
|
WHERE d.work_item_id = wi.id AND d.company_code = wi.company_code
|
|
)::integer AS detail_count
|
|
FROM process_work_item wi
|
|
WHERE wi.routing_detail_id = $1 AND wi.company_code = $2
|
|
ORDER BY wi.work_phase, wi.sort_order, wi.created_date
|
|
`;
|
|
|
|
const result = await getPool().query(query, [routingDetailId, companyCode]);
|
|
|
|
// phase별 그룹핑
|
|
const grouped: Record<string, any[]> = {};
|
|
for (const row of result.rows) {
|
|
const phase = row.work_phase;
|
|
if (!grouped[phase]) grouped[phase] = [];
|
|
grouped[phase].push(row);
|
|
}
|
|
|
|
return res.json({ success: true, data: grouped, items: result.rows });
|
|
} catch (error: any) {
|
|
logger.error("작업 항목 조회 실패", { error: error.message });
|
|
return res.status(500).json({ success: false, message: error.message });
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 작업 항목 추가
|
|
*/
|
|
export async function createWorkItem(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const companyCode = req.user?.companyCode;
|
|
const writer = req.user?.userId;
|
|
if (!companyCode) {
|
|
return res.status(401).json({ success: false, message: "인증 필요" });
|
|
}
|
|
|
|
const { routing_detail_id, work_phase, title, is_required, sort_order, description } = req.body;
|
|
|
|
if (!routing_detail_id || !work_phase || !title) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: "routing_detail_id, work_phase, title은 필수입니다",
|
|
});
|
|
}
|
|
|
|
const query = `
|
|
INSERT INTO process_work_item
|
|
(company_code, routing_detail_id, work_phase, title, is_required, sort_order, description, writer)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
RETURNING *
|
|
`;
|
|
|
|
const result = await getPool().query(query, [
|
|
companyCode,
|
|
routing_detail_id,
|
|
work_phase,
|
|
title,
|
|
is_required || "N",
|
|
sort_order || 0,
|
|
description || null,
|
|
writer,
|
|
]);
|
|
|
|
logger.info("작업 항목 생성", { companyCode, id: result.rows[0].id });
|
|
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 updateWorkItem(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 { title, is_required, sort_order, description } = req.body;
|
|
|
|
const query = `
|
|
UPDATE process_work_item
|
|
SET title = COALESCE($1, title),
|
|
is_required = COALESCE($2, is_required),
|
|
sort_order = COALESCE($3, sort_order),
|
|
description = COALESCE($4, description),
|
|
updated_date = NOW()
|
|
WHERE id = $5 AND company_code = $6
|
|
RETURNING *
|
|
`;
|
|
|
|
const result = await getPool().query(query, [
|
|
title,
|
|
is_required,
|
|
sort_order,
|
|
description,
|
|
id,
|
|
companyCode,
|
|
]);
|
|
|
|
if (result.rowCount === 0) {
|
|
return res.status(404).json({ success: false, message: "항목을 찾을 수 없습니다" });
|
|
}
|
|
|
|
logger.info("작업 항목 수정", { companyCode, id });
|
|
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 deleteWorkItem(req: AuthenticatedRequest, res: Response) {
|
|
const client = await getPool().connect();
|
|
try {
|
|
const companyCode = req.user?.companyCode;
|
|
if (!companyCode) {
|
|
return res.status(401).json({ success: false, message: "인증 필요" });
|
|
}
|
|
|
|
const { id } = req.params;
|
|
|
|
await client.query("BEGIN");
|
|
|
|
// 상세 먼저 삭제
|
|
await client.query(
|
|
"DELETE FROM process_work_item_detail WHERE work_item_id = $1 AND company_code = $2",
|
|
[id, companyCode]
|
|
);
|
|
|
|
// 항목 삭제
|
|
const result = await client.query(
|
|
"DELETE FROM process_work_item WHERE id = $1 AND company_code = $2 RETURNING id",
|
|
[id, companyCode]
|
|
);
|
|
|
|
if (result.rowCount === 0) {
|
|
await client.query("ROLLBACK");
|
|
return res.status(404).json({ success: false, message: "항목을 찾을 수 없습니다" });
|
|
}
|
|
|
|
await client.query("COMMIT");
|
|
logger.info("작업 항목 삭제", { companyCode, id });
|
|
return res.json({ success: true });
|
|
} catch (error: any) {
|
|
await client.query("ROLLBACK");
|
|
logger.error("작업 항목 삭제 실패", { error: error.message });
|
|
return res.status(500).json({ success: false, message: error.message });
|
|
} finally {
|
|
client.release();
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// 작업 항목 상세 CRUD
|
|
// ============================================================
|
|
|
|
/**
|
|
* 작업 항목 상세 목록 조회
|
|
*/
|
|
export async function getWorkItemDetails(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const companyCode = req.user?.companyCode;
|
|
if (!companyCode) {
|
|
return res.status(401).json({ success: false, message: "인증 필요" });
|
|
}
|
|
|
|
const { workItemId } = req.params;
|
|
|
|
const query = `
|
|
SELECT id, work_item_id, detail_type, content, is_required, sort_order, remark,
|
|
inspection_code, inspection_method, unit, lower_limit, upper_limit,
|
|
duration_minutes, input_type, lookup_target, display_fields,
|
|
selected_bom_items, process_inspection_apply, equip_inspection_apply,
|
|
condition_unit, condition_base_value, condition_tolerance,
|
|
condition_auto_collect, condition_plc_data,
|
|
created_date
|
|
FROM process_work_item_detail
|
|
WHERE work_item_id = $1 AND company_code = $2
|
|
ORDER BY sort_order, created_date
|
|
`;
|
|
|
|
const result = await getPool().query(query, [workItemId, companyCode]);
|
|
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 createWorkItemDetail(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const companyCode = req.user?.companyCode;
|
|
const writer = req.user?.userId;
|
|
if (!companyCode) {
|
|
return res.status(401).json({ success: false, message: "인증 필요" });
|
|
}
|
|
|
|
const {
|
|
work_item_id, detail_type, content, is_required, sort_order, remark,
|
|
inspection_code, inspection_method, unit, lower_limit, upper_limit,
|
|
duration_minutes, input_type, lookup_target, display_fields,
|
|
selected_bom_items, process_inspection_apply, equip_inspection_apply,
|
|
// 설비조건(equip_condition) 전용 5개 필드 — TASK:ERP-015
|
|
condition_unit, condition_base_value, condition_tolerance,
|
|
condition_auto_collect, condition_plc_data,
|
|
} = req.body;
|
|
|
|
if (!work_item_id || !content) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: "work_item_id, content는 필수입니다",
|
|
});
|
|
}
|
|
|
|
// work_item이 같은 company_code인지 검증
|
|
const ownerCheck = await getPool().query(
|
|
"SELECT id FROM process_work_item WHERE id = $1 AND company_code = $2",
|
|
[work_item_id, companyCode]
|
|
);
|
|
if (ownerCheck.rowCount === 0) {
|
|
return res.status(403).json({ success: false, message: "권한이 없습니다" });
|
|
}
|
|
|
|
const query = `
|
|
INSERT INTO process_work_item_detail
|
|
(company_code, work_item_id, detail_type, content, is_required, sort_order, remark, writer,
|
|
inspection_code, inspection_method, unit, lower_limit, upper_limit,
|
|
duration_minutes, input_type, lookup_target, display_fields, selected_bom_items,
|
|
process_inspection_apply, equip_inspection_apply,
|
|
condition_unit, condition_base_value, condition_tolerance,
|
|
condition_auto_collect, condition_plc_data)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18, $19, $20,
|
|
$21, $22, $23, $24, $25)
|
|
RETURNING *
|
|
`;
|
|
|
|
// selected_bom_items: 배열이면 JSON 문자열로 변환
|
|
const bomItemsJson = Array.isArray(selected_bom_items) ? JSON.stringify(selected_bom_items) : selected_bom_items || null;
|
|
|
|
const result = await getPool().query(query, [
|
|
companyCode,
|
|
work_item_id,
|
|
detail_type || null,
|
|
content,
|
|
is_required || "N",
|
|
sort_order || 0,
|
|
remark || null,
|
|
writer,
|
|
inspection_code || null,
|
|
inspection_method || null,
|
|
unit || null,
|
|
lower_limit || null,
|
|
upper_limit || null,
|
|
duration_minutes || null,
|
|
input_type || null,
|
|
lookup_target || null,
|
|
display_fields || null,
|
|
bomItemsJson,
|
|
process_inspection_apply || null,
|
|
equip_inspection_apply || null,
|
|
condition_unit || null,
|
|
condition_base_value || null,
|
|
condition_tolerance || null,
|
|
condition_auto_collect || null,
|
|
condition_plc_data || null,
|
|
]);
|
|
|
|
logger.info("작업 항목 상세 생성", { companyCode, id: result.rows[0].id });
|
|
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 updateWorkItemDetail(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 {
|
|
detail_type, content, is_required, sort_order, remark,
|
|
inspection_code, inspection_method, unit, lower_limit, upper_limit,
|
|
duration_minutes, input_type, lookup_target, display_fields,
|
|
selected_bom_items, process_inspection_apply, equip_inspection_apply,
|
|
// 설비조건(equip_condition) 전용 5개 필드 — TASK:ERP-015
|
|
condition_unit, condition_base_value, condition_tolerance,
|
|
condition_auto_collect, condition_plc_data,
|
|
} = req.body;
|
|
|
|
const bomItemsJson = Array.isArray(selected_bom_items) ? JSON.stringify(selected_bom_items) : selected_bom_items ?? null;
|
|
|
|
const query = `
|
|
UPDATE process_work_item_detail
|
|
SET detail_type = COALESCE($1, detail_type),
|
|
content = COALESCE($2, content),
|
|
is_required = COALESCE($3, is_required),
|
|
sort_order = COALESCE($4, sort_order),
|
|
remark = COALESCE($5, remark),
|
|
inspection_code = $8,
|
|
inspection_method = $9,
|
|
unit = $10,
|
|
lower_limit = $11,
|
|
upper_limit = $12,
|
|
duration_minutes = $13,
|
|
input_type = $14,
|
|
lookup_target = $15,
|
|
display_fields = $16,
|
|
selected_bom_items = $17,
|
|
process_inspection_apply = $18,
|
|
equip_inspection_apply = $19,
|
|
condition_unit = $20,
|
|
condition_base_value = $21,
|
|
condition_tolerance = $22,
|
|
condition_auto_collect = $23,
|
|
condition_plc_data = $24,
|
|
updated_date = NOW()
|
|
WHERE id = $6 AND company_code = $7
|
|
RETURNING *
|
|
`;
|
|
|
|
const result = await getPool().query(query, [
|
|
detail_type,
|
|
content,
|
|
is_required,
|
|
sort_order,
|
|
remark,
|
|
id,
|
|
companyCode,
|
|
inspection_code || null,
|
|
inspection_method || null,
|
|
unit || null,
|
|
lower_limit || null,
|
|
upper_limit || null,
|
|
duration_minutes || null,
|
|
input_type || null,
|
|
lookup_target || null,
|
|
display_fields || null,
|
|
bomItemsJson,
|
|
process_inspection_apply || null,
|
|
equip_inspection_apply || null,
|
|
condition_unit ?? null,
|
|
condition_base_value ?? null,
|
|
condition_tolerance ?? null,
|
|
condition_auto_collect ?? null,
|
|
condition_plc_data ?? null,
|
|
]);
|
|
|
|
if (result.rowCount === 0) {
|
|
return res.status(404).json({ success: false, message: "상세를 찾을 수 없습니다" });
|
|
}
|
|
|
|
logger.info("작업 항목 상세 수정", { companyCode, id });
|
|
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 deleteWorkItemDetail(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 process_work_item_detail 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 });
|
|
}
|
|
}
|
|
|
|
// ============================================================
|
|
// 전체 저장 (일괄)
|
|
// ============================================================
|
|
|
|
/**
|
|
* 전체 저장: 작업 항목 + 상세를 일괄 저장
|
|
* 기존 데이터를 삭제하고 새로 삽입하는 replace 방식
|
|
*/
|
|
export async function saveAll(req: AuthenticatedRequest, res: Response) {
|
|
const client = await getPool().connect();
|
|
try {
|
|
const companyCode = req.user?.companyCode;
|
|
const writer = req.user?.userId;
|
|
if (!companyCode) {
|
|
return res.status(401).json({ success: false, message: "인증 필요" });
|
|
}
|
|
|
|
const { routing_detail_id, items } = req.body;
|
|
|
|
if (!routing_detail_id || !Array.isArray(items)) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
message: "routing_detail_id와 items 배열이 필요합니다",
|
|
});
|
|
}
|
|
|
|
await client.query("BEGIN");
|
|
|
|
// 기존 상세 삭제
|
|
await client.query(
|
|
`DELETE FROM process_work_item_detail
|
|
WHERE work_item_id IN (
|
|
SELECT id FROM process_work_item
|
|
WHERE routing_detail_id = $1 AND company_code = $2
|
|
)`,
|
|
[routing_detail_id, companyCode]
|
|
);
|
|
|
|
// 기존 항목 삭제
|
|
await client.query(
|
|
"DELETE FROM process_work_item WHERE routing_detail_id = $1 AND company_code = $2",
|
|
[routing_detail_id, companyCode]
|
|
);
|
|
|
|
// 새 항목 + 상세 삽입
|
|
for (const item of items) {
|
|
const itemResult = await client.query(
|
|
`INSERT INTO process_work_item
|
|
(company_code, routing_detail_id, work_phase, title, is_required, sort_order, description, writer)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
|
RETURNING id`,
|
|
[
|
|
companyCode,
|
|
routing_detail_id,
|
|
item.work_phase,
|
|
item.title,
|
|
item.is_required || "N",
|
|
item.sort_order || 0,
|
|
item.description || null,
|
|
writer,
|
|
]
|
|
);
|
|
|
|
const workItemId = itemResult.rows[0].id;
|
|
|
|
if (Array.isArray(item.details)) {
|
|
for (const detail of item.details) {
|
|
await client.query(
|
|
`INSERT INTO process_work_item_detail
|
|
(company_code, work_item_id, detail_type, content, is_required, sort_order, remark, writer,
|
|
inspection_code, inspection_method, unit, lower_limit, upper_limit,
|
|
duration_minutes, input_type, lookup_target, display_fields,
|
|
condition_unit, condition_base_value, condition_tolerance,
|
|
condition_auto_collect, condition_plc_data)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17,
|
|
$18, $19, $20, $21, $22)`,
|
|
[
|
|
companyCode,
|
|
workItemId,
|
|
detail.detail_type || null,
|
|
detail.content,
|
|
detail.is_required || "N",
|
|
detail.sort_order || 0,
|
|
detail.remark || null,
|
|
writer,
|
|
detail.inspection_code || null,
|
|
detail.inspection_method || null,
|
|
detail.unit || null,
|
|
detail.lower_limit || null,
|
|
detail.upper_limit || null,
|
|
detail.duration_minutes || null,
|
|
detail.input_type || null,
|
|
detail.lookup_target || null,
|
|
detail.display_fields || null,
|
|
// 설비조건(equip_condition) 전용 5개 — TASK:ERP-015
|
|
detail.condition_unit || null,
|
|
detail.condition_base_value || null,
|
|
detail.condition_tolerance || null,
|
|
detail.condition_auto_collect || null,
|
|
detail.condition_plc_data || null,
|
|
]
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
await client.query("COMMIT");
|
|
logger.info("작업기준 전체 저장", { companyCode, routing_detail_id, itemCount: items.length });
|
|
return res.json({ success: true, message: "저장 완료" });
|
|
} catch (error: any) {
|
|
await client.query("ROLLBACK");
|
|
logger.error("작업기준 전체 저장 실패", { error: error.message });
|
|
return res.status(500).json({ success: false, message: error.message });
|
|
} finally {
|
|
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]);
|
|
// 기본 라우팅 버전이 없으면 자동 생성
|
|
const itemCode = item.itemCode || item.itemId;
|
|
const existingVersion = await client.query(
|
|
`SELECT id FROM item_routing_version WHERE item_code = $1 AND company_code = $2 LIMIT 1`,
|
|
[itemCode, companyCode]
|
|
);
|
|
if (existingVersion.rowCount === 0) {
|
|
await client.query(
|
|
`INSERT INTO item_routing_version (id, company_code, item_code, version_name, description, is_default, writer)
|
|
VALUES (gen_random_uuid()::text, $1, $2, '기본', '자동 생성된 기본 라우팅', true, $3)`,
|
|
[companyCode, itemCode, req.user?.userId || null]
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
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 });
|
|
}
|
|
}
|