/** * 공정 작업기준 컨트롤러 * 품목별 라우팅/공정에 대한 작업 항목 및 상세 관리 */ 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; 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; // 라우팅 버전 목록 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 = {}; 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(); } }