574 lines
18 KiB
TypeScript
574 lines
18 KiB
TypeScript
|
|
/**
|
||
|
|
* 공정 작업기준 컨트롤러
|
||
|
|
* 품목별 라우팅/공정에 대한 작업 항목 및 상세 관리
|
||
|
|
*/
|
||
|
|
|
||
|
|
import { Request, Response } from "express";
|
||
|
|
import { getPool } from "../database/db";
|
||
|
|
import { logger } from "../utils/logger";
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// 품목/라우팅/공정 조회 (좌측 트리 데이터)
|
||
|
|
// ============================================================
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 라우팅이 있는 품목 목록 조회
|
||
|
|
* 요청 쿼리: tableName(품목테이블), nameColumn, codeColumn
|
||
|
|
*/
|
||
|
|
export async function getItemsWithRouting(req: Request, 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 = "",
|
||
|
|
} = 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}%`);
|
||
|
|
|
||
|
|
const query = `
|
||
|
|
SELECT DISTINCT
|
||
|
|
i.id,
|
||
|
|
i.${nameColumn} AS item_name,
|
||
|
|
i.${codeColumn} AS item_code
|
||
|
|
FROM ${tableName} i
|
||
|
|
INNER JOIN ${routingTable} rv ON rv.${routingFkColumn} = i.${codeColumn}
|
||
|
|
AND rv.company_code = i.company_code
|
||
|
|
WHERE i.company_code = $1
|
||
|
|
${searchCondition}
|
||
|
|
ORDER BY i.${codeColumn}
|
||
|
|
`;
|
||
|
|
|
||
|
|
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: Request, 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
|
||
|
|
FROM ${routingVersionTable}
|
||
|
|
WHERE ${routingFkColumn} = $1 AND company_code = $2
|
||
|
|
ORDER BY 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 });
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
// ============================================================
|
||
|
|
// 작업 항목 CRUD
|
||
|
|
// ============================================================
|
||
|
|
|
||
|
|
/**
|
||
|
|
* 공정별 작업 항목 목록 조회 (phase별 그룹)
|
||
|
|
*/
|
||
|
|
export async function getWorkItems(req: Request, 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: Request, 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: Request, 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: Request, 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: Request, 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, 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: Request, 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 } = 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)
|
||
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)
|
||
|
|
RETURNING *
|
||
|
|
`;
|
||
|
|
|
||
|
|
const result = await getPool().query(query, [
|
||
|
|
companyCode,
|
||
|
|
work_item_id,
|
||
|
|
detail_type || null,
|
||
|
|
content,
|
||
|
|
is_required || "N",
|
||
|
|
sort_order || 0,
|
||
|
|
remark || 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 updateWorkItemDetail(req: Request, 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 } = req.body;
|
||
|
|
|
||
|
|
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),
|
||
|
|
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,
|
||
|
|
]);
|
||
|
|
|
||
|
|
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: Request, 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: Request, 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)
|
||
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`,
|
||
|
|
[
|
||
|
|
companyCode,
|
||
|
|
workItemId,
|
||
|
|
detail.detail_type || null,
|
||
|
|
detail.content,
|
||
|
|
detail.is_required || "N",
|
||
|
|
detail.sort_order || 0,
|
||
|
|
detail.remark || null,
|
||
|
|
writer,
|
||
|
|
]
|
||
|
|
);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
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();
|
||
|
|
}
|
||
|
|
}
|