- Added new endpoints for managing registered items, including retrieval, registration, and batch registration. - Enhanced the existing processWorkStandardController to support filtering and additional columns in item queries. - Updated the processWorkStandardRoutes to include routes for registered items management. - Introduced a new documentation file detailing the design and structure of the POP 작업진행 관리 system. These changes aim to improve the management of registered items within the process work standard, enhancing usability and functionality. Made-with: Cursor
937 lines
30 KiB
TypeScript
937 lines
30 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,
|
|
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,
|
|
} = 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)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
|
|
RETURNING *
|
|
`;
|
|
|
|
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,
|
|
]);
|
|
|
|
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,
|
|
} = 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),
|
|
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,
|
|
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,
|
|
]);
|
|
|
|
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)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)`,
|
|
[
|
|
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,
|
|
]
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
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]);
|
|
}
|
|
|
|
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 });
|
|
}
|
|
}
|