- Created a new .env.example file to provide a template for environment variables, including database connection details, JWT settings, encryption keys, and external API keys. - Updated .gitignore to include additional test output directories and archive files, ensuring that unnecessary files are not tracked by Git. - Removed outdated approval test reports and scripts that are no longer needed, streamlining the project structure. These changes improve the clarity of environment configuration and maintain a cleaner repository.
665 lines
31 KiB
TypeScript
665 lines
31 KiB
TypeScript
/**
|
|
* 작업지시 컨트롤러 (work_instruction + work_instruction_detail)
|
|
*/
|
|
import { Response } from "express";
|
|
import { AuthenticatedRequest } from "../types/auth";
|
|
import { getPool } from "../database/db";
|
|
import { logger } from "../utils/logger";
|
|
import { numberingRuleService } from "../services/numberingRuleService";
|
|
|
|
// 자동 마이그레이션: work_instruction_detail에 routing_version_id 컬럼 추가
|
|
let _migrationDone = false;
|
|
async function ensureDetailRoutingColumn() {
|
|
if (_migrationDone) return;
|
|
try {
|
|
const pool = getPool();
|
|
await pool.query("ALTER TABLE work_instruction_detail ADD COLUMN IF NOT EXISTS routing_version_id VARCHAR(500)");
|
|
_migrationDone = true;
|
|
} catch { /* 이미 존재하거나 권한 문제 시 무시 */ }
|
|
}
|
|
|
|
// ─── 작업지시 목록 조회 (detail 기준 행 반환) ───
|
|
export async function getList(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
await ensureDetailRoutingColumn();
|
|
const companyCode = req.user!.companyCode;
|
|
const { dateFrom, dateTo, status, progressStatus, keyword } = req.query;
|
|
|
|
const conditions: string[] = [];
|
|
const params: any[] = [];
|
|
let idx = 1;
|
|
|
|
if (companyCode !== "*") {
|
|
conditions.push(`wi.company_code = $${idx}`);
|
|
params.push(companyCode);
|
|
idx++;
|
|
}
|
|
if (dateFrom) {
|
|
conditions.push(`wi.start_date >= $${idx}`);
|
|
params.push(dateFrom);
|
|
idx++;
|
|
}
|
|
if (dateTo) {
|
|
conditions.push(`wi.end_date <= $${idx}`);
|
|
params.push(dateTo);
|
|
idx++;
|
|
}
|
|
if (status && status !== "all") {
|
|
conditions.push(`wi.status = $${idx}`);
|
|
params.push(status);
|
|
idx++;
|
|
}
|
|
if (progressStatus && progressStatus !== "all") {
|
|
conditions.push(`wi.progress_status = $${idx}`);
|
|
params.push(progressStatus);
|
|
idx++;
|
|
}
|
|
if (keyword) {
|
|
conditions.push(`(wi.work_instruction_no ILIKE $${idx} OR wi.worker ILIKE $${idx} OR COALESCE(itm.item_name,'') ILIKE $${idx} OR COALESCE(d.item_number,'') ILIKE $${idx})`);
|
|
params.push(`%${keyword}%`);
|
|
idx++;
|
|
}
|
|
|
|
const whereClause = conditions.length > 0 ? `WHERE ${conditions.join(" AND ")}` : "";
|
|
|
|
const query = `
|
|
SELECT
|
|
wi.id AS wi_id,
|
|
wi.work_instruction_no,
|
|
wi.status,
|
|
wi.progress_status,
|
|
wi.qty AS total_qty,
|
|
wi.completed_qty,
|
|
wi.start_date,
|
|
wi.end_date,
|
|
wi.equipment_id,
|
|
wi.work_team,
|
|
wi.worker,
|
|
wi.remark AS wi_remark,
|
|
wi.created_date,
|
|
d.id AS detail_id,
|
|
d.item_number,
|
|
d.qty AS detail_qty,
|
|
d.remark AS detail_remark,
|
|
d.part_code,
|
|
d.source_table,
|
|
d.source_id,
|
|
d.routing_version_id AS detail_routing_version_id,
|
|
COALESCE(itm.item_name, '') AS item_name,
|
|
COALESCE(itm.size, '') AS item_spec,
|
|
COALESCE(e.equipment_name, '') AS equipment_name,
|
|
COALESCE(e.equipment_code, '') AS equipment_code,
|
|
wi.routing AS routing_version_id,
|
|
COALESCE(rv.version_name, '') AS routing_name,
|
|
ROW_NUMBER() OVER (PARTITION BY wi.work_instruction_no ORDER BY d.created_date) AS detail_seq,
|
|
COUNT(*) OVER (PARTITION BY wi.work_instruction_no) AS detail_count
|
|
FROM work_instruction wi
|
|
INNER JOIN work_instruction_detail d
|
|
ON d.work_instruction_no = wi.work_instruction_no AND d.company_code = wi.company_code
|
|
LEFT JOIN LATERAL (
|
|
SELECT item_name, size FROM item_info
|
|
WHERE item_number = d.item_number AND company_code = wi.company_code LIMIT 1
|
|
) itm ON true
|
|
LEFT JOIN equipment_mng e ON wi.equipment_id = e.id AND wi.company_code = e.company_code
|
|
LEFT JOIN item_routing_version rv ON wi.routing = rv.id AND rv.company_code = wi.company_code
|
|
${whereClause}
|
|
ORDER BY wi.created_date DESC, d.created_date ASC
|
|
`;
|
|
|
|
const pool = getPool();
|
|
const result = await pool.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 previewNextNo(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const companyCode = req.user!.companyCode;
|
|
let wiNo: string;
|
|
try {
|
|
const rule = await numberingRuleService.getNumberingRuleByColumn(companyCode, "work_instruction", "work_instruction_no");
|
|
if (rule) {
|
|
wiNo = await numberingRuleService.previewCode(rule.ruleId, companyCode, {});
|
|
} else { throw new Error("채번 규칙 없음"); }
|
|
} catch {
|
|
const pool = getPool();
|
|
const today = new Date().toISOString().split("T")[0].replace(/-/g, "");
|
|
const seqRes = await pool.query(
|
|
`SELECT COUNT(*) + 1 AS seq FROM work_instruction WHERE company_code = $1 AND work_instruction_no LIKE $2`,
|
|
[companyCode, `WI-${today}-%`]
|
|
);
|
|
wiNo = `WI-${today}-${String(seqRes.rows[0].seq).padStart(3, "0")}`;
|
|
}
|
|
return res.json({ success: true, instructionNo: wiNo });
|
|
} catch (error: any) {
|
|
logger.error("작업지시번호 미리보기 실패", { error: error.message });
|
|
return res.status(500).json({ success: false, message: error.message });
|
|
}
|
|
}
|
|
|
|
// ─── 작업지시 저장 (신규/수정) ───
|
|
export async function save(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
await ensureDetailRoutingColumn();
|
|
const companyCode = req.user!.companyCode;
|
|
const userId = req.user!.userId;
|
|
const { id: editId, status: wiStatus, progressStatus, reason, startDate, endDate, equipmentId, workTeam, worker, remark, items, routing: routingVersionId } = req.body;
|
|
|
|
if (!items || items.length === 0) {
|
|
return res.status(400).json({ success: false, message: "품목을 선택해주세요" });
|
|
}
|
|
|
|
const pool = getPool();
|
|
const client = await pool.connect();
|
|
try {
|
|
await client.query("BEGIN");
|
|
let wiId: string;
|
|
let wiNo: string;
|
|
|
|
if (editId) {
|
|
const check = await client.query(`SELECT id, work_instruction_no FROM work_instruction WHERE id = $1 AND company_code = $2`, [editId, companyCode]);
|
|
if (check.rowCount === 0) throw new Error("작업지시를 찾을 수 없습니다");
|
|
wiId = editId;
|
|
wiNo = check.rows[0].work_instruction_no;
|
|
await client.query(
|
|
`UPDATE work_instruction SET status=$1, progress_status=$2, reason=$3, start_date=$4, end_date=$5, equipment_id=$6, work_team=$7, worker=$8, remark=$9, routing=$10, updated_date=NOW(), writer=$11 WHERE id=$12 AND company_code=$13`,
|
|
[wiStatus||"일반", progressStatus||"", reason||"", startDate||"", endDate||"", equipmentId||"", workTeam||"", worker||"", remark||"", routingVersionId||null, userId, editId, companyCode]
|
|
);
|
|
await client.query(`DELETE FROM work_instruction_detail WHERE work_instruction_no=$1 AND company_code=$2`, [wiNo, companyCode]);
|
|
} else {
|
|
try {
|
|
const rule = await numberingRuleService.getNumberingRuleByColumn(companyCode, "work_instruction", "work_instruction_no");
|
|
if (rule) { wiNo = await numberingRuleService.allocateCode(rule.ruleId, companyCode, {}); }
|
|
else { throw new Error("채번 규칙 없음 - 폴백"); }
|
|
} catch {
|
|
const today = new Date().toISOString().split("T")[0].replace(/-/g, "");
|
|
const seqRes = await client.query(`SELECT COUNT(*)+1 AS seq FROM work_instruction WHERE company_code=$1 AND work_instruction_no LIKE $2`, [companyCode, `WI-${today}-%`]);
|
|
wiNo = `WI-${today}-${String(seqRes.rows[0].seq).padStart(3, "0")}`;
|
|
}
|
|
const insertRes = await client.query(
|
|
`INSERT INTO work_instruction (id,company_code,work_instruction_no,status,progress_status,reason,start_date,end_date,equipment_id,work_team,worker,remark,routing,created_date,writer) VALUES (gen_random_uuid()::text,$1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12,NOW(),$13) RETURNING id`,
|
|
[companyCode, wiNo, wiStatus||"일반", progressStatus||"", reason||"", startDate||"", endDate||"", equipmentId||"", workTeam||"", worker||"", remark||"", routingVersionId||null, userId]
|
|
);
|
|
wiId = insertRes.rows[0].id;
|
|
}
|
|
|
|
for (const item of items) {
|
|
await client.query(
|
|
`INSERT INTO work_instruction_detail (id,company_code,work_instruction_no,item_number,qty,remark,source_table,source_id,part_code,routing_version_id,created_date,writer) VALUES (gen_random_uuid()::text,$1,$2,$3,$4,$5,$6,$7,$8,$9,NOW(),$10)`,
|
|
[companyCode, wiNo, item.itemNumber||item.itemCode||"", item.qty||"0", item.remark||"", item.sourceTable||"", item.sourceId||"", item.partCode||item.itemNumber||item.itemCode||"", item.routing||null, userId]
|
|
);
|
|
}
|
|
|
|
await client.query("COMMIT");
|
|
return res.json({ success: true, data: { id: wiId, workInstructionNo: wiNo } });
|
|
} catch (txErr) { await client.query("ROLLBACK"); throw txErr; }
|
|
finally { client.release(); }
|
|
} catch (error: any) {
|
|
logger.error("작업지시 저장 실패", { error: error.message, stack: error.stack });
|
|
return res.status(500).json({ success: false, message: error.message });
|
|
}
|
|
}
|
|
|
|
// ─── 작업지시 삭제 ───
|
|
export async function remove(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const companyCode = req.user!.companyCode;
|
|
const { ids } = req.body;
|
|
if (!ids || ids.length === 0) return res.status(400).json({ success: false, message: "삭제할 항목을 선택해주세요" });
|
|
|
|
const pool = getPool();
|
|
const client = await pool.connect();
|
|
try {
|
|
await client.query("BEGIN");
|
|
const wiNos = await client.query(`SELECT work_instruction_no FROM work_instruction WHERE id=ANY($1) AND company_code=$2`, [ids, companyCode]);
|
|
for (const row of wiNos.rows) {
|
|
await client.query(`DELETE FROM work_instruction_detail WHERE work_instruction_no=$1 AND company_code=$2`, [row.work_instruction_no, companyCode]);
|
|
}
|
|
const result = await client.query(`DELETE FROM work_instruction WHERE id=ANY($1) AND company_code=$2`, [ids, companyCode]);
|
|
await client.query("COMMIT");
|
|
return res.json({ success: true, deletedCount: result.rowCount });
|
|
} catch (txErr) { await client.query("ROLLBACK"); throw txErr; }
|
|
finally { client.release(); }
|
|
} catch (error: any) {
|
|
logger.error("작업지시 삭제 실패", { error: error.message });
|
|
return res.status(500).json({ success: false, message: error.message });
|
|
}
|
|
}
|
|
|
|
// ─── 품목 소스 (페이징) ───
|
|
export async function getItemSource(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const companyCode = req.user!.companyCode;
|
|
const { keyword, page: ps, pageSize: pss } = req.query;
|
|
const page = Math.max(1, parseInt(ps as string) || 1);
|
|
const pageSize = Math.min(100, Math.max(1, parseInt(pss as string) || 20));
|
|
const offset = (page - 1) * pageSize;
|
|
|
|
const conds = ["company_code = $1"]; const params: any[] = [companyCode]; let idx = 2;
|
|
if (keyword) { conds.push(`(item_number ILIKE $${idx} OR item_name ILIKE $${idx})`); params.push(`%${keyword}%`); idx++; }
|
|
const w = conds.join(" AND ");
|
|
const pool = getPool();
|
|
const cnt = await pool.query(`SELECT COUNT(*) AS total FROM item_info WHERE ${w}`, params);
|
|
params.push(pageSize, offset);
|
|
const rows = await pool.query(`SELECT id, item_number AS item_code, item_name, COALESCE(size,'') AS spec FROM item_info WHERE ${w} ORDER BY item_name LIMIT $${idx} OFFSET $${idx+1}`, params);
|
|
return res.json({ success: true, data: rows.rows, totalCount: parseInt(cnt.rows[0].total), page, pageSize });
|
|
} catch (error: any) { return res.status(500).json({ success: false, message: error.message }); }
|
|
}
|
|
|
|
// ─── 수주 소스 (페이징) ───
|
|
export async function getSalesOrderSource(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const companyCode = req.user!.companyCode;
|
|
const { keyword, page: ps, pageSize: pss } = req.query;
|
|
const page = Math.max(1, parseInt(ps as string) || 1);
|
|
const pageSize = Math.min(100, Math.max(1, parseInt(pss as string) || 20));
|
|
const offset = (page - 1) * pageSize;
|
|
|
|
const conds = ["d.company_code = $1"]; const params: any[] = [companyCode]; let idx = 2;
|
|
if (keyword) { conds.push(`(d.part_code ILIKE $${idx} OR COALESCE(i.item_name, d.part_name, d.part_code) ILIKE $${idx} OR d.order_no ILIKE $${idx})`); params.push(`%${keyword}%`); idx++; }
|
|
const fromClause = `FROM sales_order_detail d LEFT JOIN LATERAL (SELECT item_name FROM item_info WHERE item_number = d.part_code AND company_code = d.company_code LIMIT 1) i ON true WHERE ${conds.join(" AND ")}`;
|
|
const pool = getPool();
|
|
const cnt = await pool.query(`SELECT COUNT(*) AS total ${fromClause}`, params);
|
|
params.push(pageSize, offset);
|
|
const rows = await pool.query(`SELECT d.id, d.order_no, d.part_code AS item_code, COALESCE(i.item_name, d.part_name, d.part_code) AS item_name, COALESCE(d.spec,'') AS spec, COALESCE(NULLIF(d.qty,'')::numeric,0) AS qty, d.due_date ${fromClause} ORDER BY d.created_date DESC LIMIT $${idx} OFFSET $${idx+1}`, params);
|
|
return res.json({ success: true, data: rows.rows, totalCount: parseInt(cnt.rows[0].total), page, pageSize });
|
|
} catch (error: any) { return res.status(500).json({ success: false, message: error.message }); }
|
|
}
|
|
|
|
// ─── 생산계획 소스 (페이징) ───
|
|
export async function getProductionPlanSource(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const companyCode = req.user!.companyCode;
|
|
const { keyword, page: ps, pageSize: pss } = req.query;
|
|
const page = Math.max(1, parseInt(ps as string) || 1);
|
|
const pageSize = Math.min(100, Math.max(1, parseInt(pss as string) || 20));
|
|
const offset = (page - 1) * pageSize;
|
|
|
|
const conds = ["p.company_code = $1"]; const params: any[] = [companyCode]; let idx = 2;
|
|
if (keyword) { conds.push(`(p.plan_no ILIKE $${idx} OR p.item_code ILIKE $${idx} OR COALESCE(p.item_name,'') ILIKE $${idx})`); params.push(`%${keyword}%`); idx++; }
|
|
const w = conds.join(" AND ");
|
|
const pool = getPool();
|
|
const cnt = await pool.query(`SELECT COUNT(*) AS total FROM production_plan_mng p WHERE ${w}`, params);
|
|
params.push(pageSize, offset);
|
|
const rows = await pool.query(`SELECT p.id, p.plan_no, p.item_code, COALESCE(p.item_name,'') AS item_name, COALESCE(p.plan_qty,0) AS plan_qty, p.start_date, p.end_date, p.status, COALESCE(p.equipment_name,'') AS equipment_name FROM production_plan_mng p WHERE ${w} ORDER BY p.created_date DESC LIMIT $${idx} OFFSET $${idx+1}`, params);
|
|
return res.json({ success: true, data: rows.rows, totalCount: parseInt(cnt.rows[0].total), page, pageSize });
|
|
} catch (error: any) { return res.status(500).json({ success: false, message: error.message }); }
|
|
}
|
|
|
|
// ─── 사원 목록 (작업자 Select용) ───
|
|
export async function getEmployeeList(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const companyCode = req.user!.companyCode;
|
|
const pool = getPool();
|
|
let query: string;
|
|
let params: any[];
|
|
if (companyCode !== "*") {
|
|
query = `SELECT user_id, user_name, dept_name FROM user_info WHERE company_code = $1 AND company_code != '*' ORDER BY user_name`;
|
|
params = [companyCode];
|
|
} else {
|
|
query = `SELECT user_id, user_name, dept_name, company_code FROM user_info WHERE company_code != '*' ORDER BY user_name`;
|
|
params = [];
|
|
}
|
|
const result = await pool.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 });
|
|
}
|
|
}
|
|
|
|
// ─── 설비 목록 (Select용) ───
|
|
export async function getEquipmentList(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const companyCode = req.user!.companyCode;
|
|
const pool = getPool();
|
|
const cond = companyCode !== "*" ? "WHERE company_code = $1" : "";
|
|
const params = companyCode !== "*" ? [companyCode] : [];
|
|
const result = await pool.query(`SELECT id, equipment_code, equipment_name FROM equipment_mng ${cond} ORDER BY equipment_name`, params);
|
|
return res.json({ success: true, data: result.rows });
|
|
} catch (error: any) { return res.status(500).json({ success: false, message: error.message }); }
|
|
}
|
|
|
|
// ─── 품목의 라우팅 버전 + 공정 조회 ───
|
|
export async function getRoutingVersions(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const companyCode = req.user!.companyCode;
|
|
const { itemCode } = req.params;
|
|
const pool = getPool();
|
|
|
|
const versionsResult = await pool.query(
|
|
`SELECT id, version_name, description, created_date, COALESCE(is_default, false) AS is_default
|
|
FROM item_routing_version
|
|
WHERE item_code = $1 AND company_code = $2
|
|
ORDER BY is_default DESC, created_date DESC`,
|
|
[itemCode, companyCode]
|
|
);
|
|
|
|
const routings = [];
|
|
for (const version of versionsResult.rows) {
|
|
const detailsResult = await pool.query(
|
|
`SELECT rd.id AS routing_detail_id, rd.seq_no, rd.process_code,
|
|
rd.is_required, rd.work_type,
|
|
COALESCE(p.process_name, rd.process_code) AS process_name
|
|
FROM item_routing_detail rd
|
|
LEFT JOIN process_mng p ON p.process_code = 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`,
|
|
[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 updateRouting(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const companyCode = req.user!.companyCode;
|
|
const { wiNo } = req.params;
|
|
const { routingVersionId } = req.body;
|
|
const pool = getPool();
|
|
|
|
await pool.query(
|
|
`UPDATE work_instruction SET routing = $1, updated_date = NOW() WHERE work_instruction_no = $2 AND company_code = $3`,
|
|
[routingVersionId || null, wiNo, companyCode]
|
|
);
|
|
|
|
return res.json({ success: true });
|
|
} catch (error: any) {
|
|
logger.error("라우팅 변경 실패", { error: error.message });
|
|
return res.status(500).json({ success: false, message: error.message });
|
|
}
|
|
}
|
|
|
|
// ─── 작업지시 전용 공정작업기준 조회 ───
|
|
export async function getWorkStandard(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const companyCode = req.user!.companyCode;
|
|
const { wiNo } = req.params;
|
|
const { routingVersionId } = req.query;
|
|
const pool = getPool();
|
|
|
|
if (!routingVersionId) {
|
|
return res.status(400).json({ success: false, message: "routingVersionId 필요" });
|
|
}
|
|
|
|
// 라우팅 디테일(공정) 목록 조회
|
|
const processesResult = await pool.query(
|
|
`SELECT rd.id AS routing_detail_id, rd.seq_no, rd.process_code,
|
|
COALESCE(p.process_name, rd.process_code) AS process_name
|
|
FROM item_routing_detail rd
|
|
LEFT JOIN process_mng p ON p.process_code = 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`,
|
|
[routingVersionId, companyCode]
|
|
);
|
|
|
|
// 커스텀 작업기준이 있는지 확인
|
|
const customCheck = await pool.query(
|
|
`SELECT COUNT(*) AS cnt FROM wi_process_work_item WHERE work_instruction_no = $1 AND company_code = $2`,
|
|
[wiNo, companyCode]
|
|
);
|
|
const hasCustom = parseInt(customCheck.rows[0].cnt) > 0;
|
|
|
|
const processes = [];
|
|
for (const proc of processesResult.rows) {
|
|
let workItems;
|
|
|
|
if (hasCustom) {
|
|
// 커스텀 버전에서 조회
|
|
const wiResult = await pool.query(
|
|
`SELECT wi.id, wi.routing_detail_id, wi.work_phase, wi.title, wi.is_required, wi.sort_order, wi.description,
|
|
(SELECT COUNT(*) FROM wi_process_work_item_detail d WHERE d.wi_work_item_id = wi.id AND d.company_code = wi.company_code)::integer AS detail_count
|
|
FROM wi_process_work_item wi
|
|
WHERE wi.work_instruction_no = $1 AND wi.routing_detail_id = $2 AND wi.company_code = $3
|
|
ORDER BY wi.work_phase, wi.sort_order`,
|
|
[wiNo, proc.routing_detail_id, companyCode]
|
|
);
|
|
workItems = wiResult.rows;
|
|
|
|
// 각 work_item의 상세도 로드
|
|
for (const wi of workItems) {
|
|
const detailsResult = await pool.query(
|
|
`SELECT id, wi_work_item_id AS 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
|
|
FROM wi_process_work_item_detail
|
|
WHERE wi_work_item_id = $1 AND company_code = $2
|
|
ORDER BY sort_order`,
|
|
[wi.id, companyCode]
|
|
);
|
|
wi.details = detailsResult.rows;
|
|
}
|
|
} else {
|
|
// 원본에서 조회
|
|
const origResult = await pool.query(
|
|
`SELECT wi.id, wi.routing_detail_id, wi.work_phase, wi.title, wi.is_required, wi.sort_order, wi.description,
|
|
(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`,
|
|
[proc.routing_detail_id, companyCode]
|
|
);
|
|
workItems = origResult.rows;
|
|
|
|
for (const wi of workItems) {
|
|
const detailsResult = await pool.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
|
|
FROM process_work_item_detail
|
|
WHERE work_item_id = $1 AND company_code = $2
|
|
ORDER BY sort_order`,
|
|
[wi.id, companyCode]
|
|
);
|
|
wi.details = detailsResult.rows;
|
|
}
|
|
}
|
|
|
|
processes.push({
|
|
...proc,
|
|
workItems,
|
|
});
|
|
}
|
|
|
|
return res.json({ success: true, data: { processes, isCustom: hasCustom } });
|
|
} catch (error: any) {
|
|
logger.error("작업지시 공정작업기준 조회 실패", { error: error.message });
|
|
return res.status(500).json({ success: false, message: error.message });
|
|
}
|
|
}
|
|
|
|
// ─── 원본 공정작업기준 -> 작업지시 전용 복사 ───
|
|
export async function copyWorkStandard(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const companyCode = req.user!.companyCode;
|
|
const userId = req.user!.userId;
|
|
const { wiNo } = req.params;
|
|
const { routingVersionId } = req.body;
|
|
const pool = getPool();
|
|
const client = await pool.connect();
|
|
|
|
try {
|
|
await client.query("BEGIN");
|
|
|
|
// 기존 커스텀 데이터 삭제
|
|
const existingItems = await client.query(
|
|
`SELECT id FROM wi_process_work_item WHERE work_instruction_no = $1 AND company_code = $2`,
|
|
[wiNo, companyCode]
|
|
);
|
|
for (const row of existingItems.rows) {
|
|
await client.query(
|
|
`DELETE FROM wi_process_work_item_detail WHERE wi_work_item_id = $1 AND company_code = $2`,
|
|
[row.id, companyCode]
|
|
);
|
|
}
|
|
await client.query(
|
|
`DELETE FROM wi_process_work_item WHERE work_instruction_no = $1 AND company_code = $2`,
|
|
[wiNo, companyCode]
|
|
);
|
|
|
|
// 라우팅 디테일 목록 조회
|
|
const routingDetails = await client.query(
|
|
`SELECT id FROM item_routing_detail WHERE routing_version_id = $1 AND company_code = $2`,
|
|
[routingVersionId, companyCode]
|
|
);
|
|
|
|
// 각 공정(routing_detail)별 원본 작업항목 복사
|
|
for (const rd of routingDetails.rows) {
|
|
const origItems = await client.query(
|
|
`SELECT * FROM process_work_item WHERE routing_detail_id = $1 AND company_code = $2`,
|
|
[rd.id, companyCode]
|
|
);
|
|
|
|
for (const origItem of origItems.rows) {
|
|
const newItemResult = await client.query(
|
|
`INSERT INTO wi_process_work_item (company_code, work_instruction_no, routing_detail_id, work_phase, title, is_required, sort_order, description, source_work_item_id, writer)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id`,
|
|
[companyCode, wiNo, rd.id, origItem.work_phase, origItem.title, origItem.is_required, origItem.sort_order, origItem.description, origItem.id, userId]
|
|
);
|
|
const newItemId = newItemResult.rows[0].id;
|
|
|
|
// 상세 복사
|
|
const origDetails = await client.query(
|
|
`SELECT * FROM process_work_item_detail WHERE work_item_id = $1 AND company_code = $2`,
|
|
[origItem.id, companyCode]
|
|
);
|
|
|
|
for (const origDetail of origDetails.rows) {
|
|
await client.query(
|
|
`INSERT INTO wi_process_work_item_detail (company_code, wi_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, writer)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)`,
|
|
[companyCode, newItemId, origDetail.detail_type, origDetail.content, origDetail.is_required, origDetail.sort_order, origDetail.remark, origDetail.inspection_code, origDetail.inspection_method, origDetail.unit, origDetail.lower_limit, origDetail.upper_limit, origDetail.duration_minutes, origDetail.input_type, origDetail.lookup_target, origDetail.display_fields, userId]
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
await client.query("COMMIT");
|
|
logger.info("공정작업기준 복사 완료", { companyCode, wiNo, routingVersionId });
|
|
return res.json({ success: true });
|
|
} catch (txErr) {
|
|
await client.query("ROLLBACK");
|
|
throw txErr;
|
|
} finally {
|
|
client.release();
|
|
}
|
|
} catch (error: any) {
|
|
logger.error("공정작업기준 복사 실패", { error: error.message });
|
|
return res.status(500).json({ success: false, message: error.message });
|
|
}
|
|
}
|
|
|
|
// ─── 작업지시 전용 공정작업기준 저장 (일괄) ───
|
|
export async function saveWorkStandard(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const companyCode = req.user!.companyCode;
|
|
const userId = req.user!.userId;
|
|
const { wiNo } = req.params;
|
|
const { routingDetailId, workItems } = req.body;
|
|
const pool = getPool();
|
|
const client = await pool.connect();
|
|
|
|
try {
|
|
await client.query("BEGIN");
|
|
|
|
// 해당 공정의 기존 커스텀 데이터 삭제
|
|
const existing = await client.query(
|
|
`SELECT id FROM wi_process_work_item WHERE work_instruction_no = $1 AND routing_detail_id = $2 AND company_code = $3`,
|
|
[wiNo, routingDetailId, companyCode]
|
|
);
|
|
for (const row of existing.rows) {
|
|
await client.query(
|
|
`DELETE FROM wi_process_work_item_detail WHERE wi_work_item_id = $1 AND company_code = $2`,
|
|
[row.id, companyCode]
|
|
);
|
|
}
|
|
await client.query(
|
|
`DELETE FROM wi_process_work_item WHERE work_instruction_no = $1 AND routing_detail_id = $2 AND company_code = $3`,
|
|
[wiNo, routingDetailId, companyCode]
|
|
);
|
|
|
|
// 새 데이터 삽입
|
|
for (const wi of workItems) {
|
|
const wiResult = await client.query(
|
|
`INSERT INTO wi_process_work_item (company_code, work_instruction_no, routing_detail_id, work_phase, title, is_required, sort_order, description, source_work_item_id, writer)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id`,
|
|
[companyCode, wiNo, routingDetailId, wi.work_phase, wi.title, wi.is_required, wi.sort_order, wi.description || null, wi.source_work_item_id || null, userId]
|
|
);
|
|
const newId = wiResult.rows[0].id;
|
|
|
|
if (wi.details && Array.isArray(wi.details)) {
|
|
for (const d of wi.details) {
|
|
await client.query(
|
|
`INSERT INTO wi_process_work_item_detail (company_code, wi_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, writer)
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)`,
|
|
[companyCode, newId, d.detail_type, d.content, d.is_required, d.sort_order, d.remark || null, d.inspection_code || null, d.inspection_method || null, d.unit || null, d.lower_limit || null, d.upper_limit || null, d.duration_minutes || null, d.input_type || null, d.lookup_target || null, d.display_fields || null, userId]
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
await client.query("COMMIT");
|
|
logger.info("작업지시 공정작업기준 저장 완료", { companyCode, wiNo, routingDetailId });
|
|
return res.json({ success: true });
|
|
} catch (txErr) {
|
|
await client.query("ROLLBACK");
|
|
throw txErr;
|
|
} finally {
|
|
client.release();
|
|
}
|
|
} catch (error: any) {
|
|
logger.error("작업지시 공정작업기준 저장 실패", { error: error.message });
|
|
return res.status(500).json({ success: false, message: error.message });
|
|
}
|
|
}
|
|
|
|
// ─── 작업지시 전용 커스텀 데이터 삭제 (원본으로 초기화) ───
|
|
export async function resetWorkStandard(req: AuthenticatedRequest, res: Response) {
|
|
try {
|
|
const companyCode = req.user!.companyCode;
|
|
const { wiNo } = req.params;
|
|
const pool = getPool();
|
|
const client = await pool.connect();
|
|
|
|
try {
|
|
await client.query("BEGIN");
|
|
const items = await client.query(
|
|
`SELECT id FROM wi_process_work_item WHERE work_instruction_no = $1 AND company_code = $2`,
|
|
[wiNo, companyCode]
|
|
);
|
|
for (const row of items.rows) {
|
|
await client.query(
|
|
`DELETE FROM wi_process_work_item_detail WHERE wi_work_item_id = $1 AND company_code = $2`,
|
|
[row.id, companyCode]
|
|
);
|
|
}
|
|
await client.query(
|
|
`DELETE FROM wi_process_work_item WHERE work_instruction_no = $1 AND company_code = $2`,
|
|
[wiNo, companyCode]
|
|
);
|
|
await client.query("COMMIT");
|
|
logger.info("작업지시 공정작업기준 초기화", { companyCode, wiNo });
|
|
return res.json({ success: true });
|
|
} catch (txErr) {
|
|
await client.query("ROLLBACK");
|
|
throw txErr;
|
|
} finally {
|
|
client.release();
|
|
}
|
|
} catch (error: any) {
|
|
logger.error("작업지시 공정작업기준 초기화 실패", { error: error.message });
|
|
return res.status(500).json({ success: false, message: error.message });
|
|
}
|
|
}
|