076184aad2
- Changed the request type from `Request` to `AuthenticatedRequest` in multiple functions within the processWorkStandardController. - This update ensures that user authentication details are accessible in the request object, enhancing security and functionality for work item management operations.
575 lines
18 KiB
TypeScript
575 lines
18 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 = "",
|
|
} = 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: 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
|
|
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: 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, 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 } = 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: 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 } = 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: 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)
|
|
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();
|
|
}
|
|
}
|