2026-03-19 17:52:17 +09:00
/ * *
* 작 업 지 시 컨 트 롤 러 ( 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" ;
2026-04-22 14:49:40 +09:00
// 자동 마이그레이션: work_instruction_detail에 routing_version_id + 품목별 일정/설비/작업조/작업자 컬럼 추가
2026-04-01 12:12:15 +09:00
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)" ) ;
2026-04-22 14:49:40 +09:00
// 품목별 일정/설비/작업조/작업자 컬럼 (옵션 A — 다중선택 지원)
await pool . query ( "ALTER TABLE work_instruction_detail ADD COLUMN IF NOT EXISTS start_date VARCHAR(500)" ) ;
await pool . query ( "ALTER TABLE work_instruction_detail ADD COLUMN IF NOT EXISTS end_date VARCHAR(500)" ) ;
await pool . query ( "ALTER TABLE work_instruction_detail ADD COLUMN IF NOT EXISTS equipment_ids VARCHAR(1000)" ) ;
await pool . query ( "ALTER TABLE work_instruction_detail ADD COLUMN IF NOT EXISTS work_teams VARCHAR(200)" ) ;
await pool . query ( "ALTER TABLE work_instruction_detail ADD COLUMN IF NOT EXISTS workers VARCHAR(1000)" ) ;
2026-04-01 12:12:15 +09:00
_migrationDone = true ;
} catch { /* 이미 존재하거나 권한 문제 시 무시 */ }
}
2026-03-19 17:52:17 +09:00
// ─── 작업지시 목록 조회 (detail 기준 행 반환) ───
export async function getList ( req : AuthenticatedRequest , res : Response ) {
try {
2026-04-01 12:12:15 +09:00
await ensureDetailRoutingColumn ( ) ;
2026-03-19 17:52:17 +09:00
const companyCode = req . user ! . companyCode ;
2026-04-20 14:51:32 +09:00
const { dateFrom , dateTo , status , progressStatus , keyword , page , pageSize } = req . query ;
// 페이지네이션 파라미터 파싱 (page 없으면 전체 반환 — 하위호환)
const pageNum = page ? Math . max ( 1 , parseInt ( page as string , 10 ) || 1 ) : null ;
const sizeNum = pageSize ? Math . max ( 1 , Math . min ( 1000 , parseInt ( pageSize as string , 10 ) || 20 ) ) : null ;
const paginated = pageNum !== null && sizeNum !== null ;
2026-03-19 17:52:17 +09:00
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 ++ ;
}
2026-04-20 14:51:32 +09:00
// keyword 검색: wi 자체 필드 + detail.item_number 존재 여부로 EXISTS
2026-03-19 17:52:17 +09:00
if ( keyword ) {
2026-04-20 14:51:32 +09:00
conditions . push ( ` (
wi . work_instruction_no ILIKE $ $ { idx }
OR wi . worker ILIKE $ $ { idx }
OR EXISTS (
SELECT 1 FROM work_instruction_detail dd
LEFT JOIN item_info ii ON ii . item_number = dd . item_number AND ii . company_code = wi . company_code
WHERE dd . work_instruction_id = wi . id
AND ( dd . item_number ILIKE $ $ { idx } OR COALESCE ( ii . item_name , '' ) ILIKE $ $ { idx } )
)
) ` );
2026-03-19 17:52:17 +09:00
params . push ( ` % ${ keyword } % ` ) ;
idx ++ ;
}
const whereClause = conditions . length > 0 ? ` WHERE ${ conditions . join ( " AND " ) } ` : "" ;
2026-04-20 14:51:32 +09:00
const pool = getPool ( ) ;
// 페이지네이션 모드: WI 단위로 페이지 잘라낸 뒤 detail과 JOIN
if ( paginated ) {
// 1) 총 WI 개수 카운트
const countSql = `
SELECT COUNT ( * ) : : int AS cnt
FROM work_instruction wi
$ { whereClause }
` ;
const countRes = await pool . query ( countSql , params ) ;
const totalCount = countRes . rows [ 0 ] ? . cnt ? ? 0 ;
2026-04-23 14:32:52 +09:00
// 2) 현재 페이지 WI id 목록 (최신 생성순, 동일 created_date일 때 번호 내림차순)
2026-04-20 14:51:32 +09:00
const offset = ( pageNum ! - 1 ) * sizeNum ! ;
const pageSql = `
SELECT wi . id
FROM work_instruction wi
$ { whereClause }
2026-04-23 14:32:52 +09:00
ORDER BY wi . created_date DESC NULLS LAST , wi . work_instruction_no DESC
2026-04-20 14:51:32 +09:00
LIMIT $ { sizeNum } OFFSET $ { offset }
` ;
const pageRes = await pool . query ( pageSql , params ) ;
const wiIds = pageRes . rows . map ( ( r ) = > r . id ) ;
if ( wiIds . length === 0 ) {
return res . json ( { success : true , data : [ ] , totalCount , page : pageNum , pageSize : sizeNum } ) ;
}
// 3) 해당 WI들의 detail + 품목/설비/라우팅 JOIN
const dataSql = `
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 ,
2026-04-23 14:32:52 +09:00
wi . batch_no ,
wi . cutting_plan_id ,
2026-04-20 14:51:32 +09:00
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 ,
2026-04-22 14:49:40 +09:00
d . start_date AS detail_start_date ,
d . end_date AS detail_end_date ,
d . equipment_ids AS detail_equipment_ids ,
d . work_teams AS detail_work_teams ,
d . workers AS detail_workers ,
2026-04-20 14:51:32 +09:00
COALESCE ( itm . item_name , '' ) AS item_name ,
COALESCE ( itm . type , '' ) AS item_type ,
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 ,
2026-04-23 14:32:52 +09:00
ROW_NUMBER ( ) OVER ( PARTITION BY wi . work_instruction_no ORDER BY d . created_date , d . id ) AS detail_seq ,
2026-04-20 14:51:32 +09:00
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_id = wi . id
LEFT JOIN item_info itm
ON itm . item_number = d . item_number AND itm . company_code = wi . company_code
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
WHERE wi . id = ANY ( $1 : : varchar [ ] )
2026-04-23 14:32:52 +09:00
ORDER BY wi . created_date DESC NULLS LAST , wi . work_instruction_no DESC , d . created_date ASC , d . id ASC
2026-04-20 14:51:32 +09:00
` ;
const dataRes = await pool . query ( dataSql , [ wiIds ] ) ;
return res . json ( {
success : true ,
data : dataRes.rows ,
totalCount ,
page : pageNum ,
pageSize : sizeNum ,
} ) ;
}
// 비페이지 모드 (하위호환): 기존 방식 유지, LATERAL만 LEFT JOIN으로 교체
2026-03-19 17:52:17 +09:00
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 ,
2026-04-23 14:32:52 +09:00
wi . batch_no ,
wi . cutting_plan_id ,
2026-03-19 17:52:17 +09:00
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 ,
2026-04-01 12:12:15 +09:00
d . routing_version_id AS detail_routing_version_id ,
2026-04-22 14:49:40 +09:00
d . start_date AS detail_start_date ,
d . end_date AS detail_end_date ,
d . equipment_ids AS detail_equipment_ids ,
d . work_teams AS detail_work_teams ,
d . workers AS detail_workers ,
2026-03-19 17:52:17 +09:00
COALESCE ( itm . item_name , '' ) AS item_name ,
2026-04-10 17:30:01 +09:00
COALESCE ( itm . type , '' ) AS item_type ,
2026-03-19 17:52:17 +09:00
COALESCE ( itm . size , '' ) AS item_spec ,
COALESCE ( e . equipment_name , '' ) AS equipment_name ,
COALESCE ( e . equipment_code , '' ) AS equipment_code ,
2026-03-20 14:18:44 +09:00
wi . routing AS routing_version_id ,
COALESCE ( rv . version_name , '' ) AS routing_name ,
2026-04-23 14:32:52 +09:00
ROW_NUMBER ( ) OVER ( PARTITION BY wi . work_instruction_no ORDER BY d . created_date , d . id ) AS detail_seq ,
2026-03-19 17:52:17 +09:00
COUNT ( * ) OVER ( PARTITION BY wi . work_instruction_no ) AS detail_count
FROM work_instruction wi
INNER JOIN work_instruction_detail d
2026-04-14 12:09:06 +09:00
ON d . work_instruction_id = wi . id
2026-04-20 14:51:32 +09:00
LEFT JOIN item_info itm
ON itm . item_number = d . item_number AND itm . company_code = wi . company_code
2026-03-19 17:52:17 +09:00
LEFT JOIN equipment_mng e ON wi . equipment_id = e . id AND wi . company_code = e . company_code
2026-03-20 14:18:44 +09:00
LEFT JOIN item_routing_version rv ON wi . routing = rv . id AND rv . company_code = wi . company_code
2026-03-19 17:52:17 +09:00
$ { whereClause }
2026-04-23 14:32:52 +09:00
ORDER BY wi . created_date DESC NULLS LAST , wi . work_instruction_no DESC , d . created_date ASC , d . id ASC
2026-03-19 17:52:17 +09:00
` ;
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 {
2026-04-01 12:12:15 +09:00
await ensureDetailRoutingColumn ( ) ;
2026-03-19 17:52:17 +09:00
const companyCode = req . user ! . companyCode ;
const userId = req . user ! . userId ;
2026-04-23 14:32:52 +09:00
const { id : editId , status : wiStatus , progressStatus , reason , startDate , endDate , equipmentId , workTeam , worker , remark , items , routing : routingVersionId , batchNo , cuttingPlanId } = req . body ;
2026-03-19 17:52:17 +09:00
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 (
2026-04-23 14:32:52 +09:00
` 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, batch_no=COALESCE( $ 11, batch_no), cutting_plan_id=COALESCE( $ 12, cutting_plan_id), updated_date=NOW(), writer= $ 13 WHERE id= $ 14 AND company_code= $ 15 ` ,
[ wiStatus || "일반" , progressStatus || "" , reason || "" , startDate || "" , endDate || "" , equipmentId || "" , workTeam || "" , worker || "" , remark || "" , routingVersionId || null , batchNo || null , cuttingPlanId || null , userId , editId , companyCode ]
2026-03-19 17:52:17 +09:00
) ;
2026-04-14 12:09:06 +09:00
await client . query ( ` DELETE FROM work_instruction_detail WHERE work_instruction_id= $ 1 ` , [ wiId ] ) ;
2026-03-19 17:52:17 +09:00
} 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 (
2026-04-23 14:32:52 +09:00
` 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,batch_no,cutting_plan_id,created_date,writer) VALUES (gen_random_uuid()::text, $ 1, $ 2, $ 3, $ 4, $ 5, $ 6, $ 7, $ 8, $ 9, $ 10, $ 11, $ 12, $ 13, $ 14,NOW(), $ 15) RETURNING id ` ,
[ companyCode , wiNo , wiStatus || "일반" , progressStatus || "" , reason || "" , startDate || "" , endDate || "" , equipmentId || "" , workTeam || "" , worker || "" , remark || "" , routingVersionId || null , batchNo || null , cuttingPlanId || null , userId ]
2026-03-19 17:52:17 +09:00
) ;
wiId = insertRes . rows [ 0 ] . id ;
}
2026-04-14 12:27:27 +09:00
let totalQty = 0 ;
let firstRouting : string | null = null ;
2026-03-19 17:52:17 +09:00
for ( const item of items ) {
2026-04-14 12:27:27 +09:00
const itemRouting = item . routing || null ;
if ( ! firstRouting && itemRouting ) firstRouting = itemRouting ;
totalQty += Number ( item . qty || 0 ) ;
2026-03-19 17:52:17 +09:00
await client . query (
2026-04-22 14:49:40 +09:00
` INSERT INTO work_instruction_detail (id,company_code,work_instruction_no,work_instruction_id,item_number,qty,remark,source_table,source_id,part_code,routing_version_id,start_date,end_date,equipment_ids,work_teams,workers,created_date,writer) VALUES (gen_random_uuid()::text, $ 1, $ 2, $ 3, $ 4, $ 5, $ 6, $ 7, $ 8, $ 9, $ 10, $ 11, $ 12, $ 13, $ 14, $ 15,NOW(), $ 16) ` ,
[
companyCode ,
wiNo ,
wiId ,
item . itemNumber || item . itemCode || "" ,
item . qty || "0" ,
item . remark || "" ,
item . sourceTable || "" ,
item . sourceId || "" ,
item . partCode || item . itemNumber || item . itemCode || "" ,
itemRouting ,
item . startDate || "" ,
item . endDate || "" ,
item . equipmentIds || "" ,
item . workTeams || "" ,
item . workers || "" ,
userId ,
]
2026-03-19 17:52:17 +09:00
) ;
}
2026-04-14 12:27:27 +09:00
// 마스터 qty/routing 자동 동기화 (디테일 합계 + 첫 번째 라우팅)
const effectiveRouting = routingVersionId || firstRouting ;
await client . query (
` UPDATE work_instruction SET qty = $ 1, routing = COALESCE(routing, $ 2) WHERE id = $ 3 ` ,
[ String ( totalQty ) , effectiveRouting , wiId ]
) ;
2026-03-19 17:52:17 +09:00
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" ) ;
2026-04-14 12:09:06 +09:00
// 디테일 삭제 (id 기반)
await client . query ( ` DELETE FROM work_instruction_detail WHERE work_instruction_id=ANY( $ 1) ` , [ ids ] ) ;
2026-03-19 17:52:17 +09:00
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 ) ;
2026-04-22 09:27:45 +09:00
// work_instruction_detail에서 해당 계획에 이미 내린 작업지시 수량 합계 → applied_qty, remain_qty
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 ,
COALESCE ( wi . applied_qty , 0 ) AS applied_qty ,
( COALESCE ( CAST ( NULLIF ( p . plan_qty : : text , '' ) AS numeric ) , 0 )
- COALESCE ( wi . applied_qty , 0 ) ) AS remain_qty
FROM production_plan_mng p
LEFT JOIN (
SELECT source_id ,
SUM ( COALESCE ( CAST ( NULLIF ( qty , '' ) AS numeric ) , 0 ) ) AS applied_qty
FROM work_instruction_detail
WHERE source_table = 'production_plan_mng'
AND company_code = $1
GROUP BY source_id
) wi ON wi . source_id = p . id : : text
WHERE $ { w }
ORDER BY p . created_date DESC
LIMIT $ $ { idx } OFFSET $ $ { idx + 1 } ` ,
params ,
) ;
2026-03-19 17:52:17 +09:00
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 } ) ; }
}
2026-03-20 14:18:44 +09:00
// ─── 품목의 라우팅 버전 + 공정 조회 ───
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 } ) ;
}
}
2026-04-15 14:23:44 +09:00
// ─── 품목별 라우팅 벌크 조회 (엑셀 업로드용) ───
export async function getRoutingVersionsBulk ( req : AuthenticatedRequest , res : Response ) {
try {
const companyCode = req . user ! . companyCode ;
const { itemCodes } = req . body as { itemCodes : string [ ] } ;
if ( ! itemCodes || ! Array . isArray ( itemCodes ) || itemCodes . length === 0 ) {
return res . json ( { success : true , data : { } } ) ;
}
const pool = getPool ( ) ;
const result : Record < string , { code : string ; name : string } [ ] > = { } ;
// 청크 단위로 분할 (PostgreSQL placeholder 제한 대응)
const CHUNK_SIZE = 5000 ;
for ( let ci = 0 ; ci < itemCodes . length ; ci += CHUNK_SIZE ) {
const chunk = itemCodes . slice ( ci , ci + CHUNK_SIZE ) ;
// 1. 기본 라우팅 버전 조회
const placeholders = chunk . map ( ( _ , i ) = > ` $ ${ i + 2 } ` ) . join ( "," ) ;
const versionsResult = await pool . query (
` SELECT DISTINCT ON (item_code) id, item_code, version_name
FROM item_routing_version
WHERE company_code = $1 AND item_code IN ( $ { placeholders } )
ORDER BY item_code , is_default DESC , created_date DESC ` ,
[ companyCode , . . . chunk ]
) ;
if ( versionsResult . rows . length === 0 ) continue ;
// 2. 라우팅 디테일 조회
const versionIds = versionsResult . rows . map ( ( v : any ) = > v . id ) ;
const vPlaceholders = versionIds . map ( ( _ : any , i : number ) = > ` $ ${ i + 2 } ` ) . join ( "," ) ;
const detailsResult = await pool . query (
` SELECT rd.routing_version_id, 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 . company_code = $1 AND rd . routing_version_id IN ( $ { vPlaceholders } )
ORDER BY rd . seq_no : : integer ` ,
[ companyCode , . . . versionIds ]
) ;
// 3. 매핑
const versionToItem : Record < string , string > = { } ;
for ( const v of versionsResult . rows ) {
versionToItem [ v . id ] = v . item_code ;
}
for ( const d of detailsResult . rows ) {
const itemCode = versionToItem [ d . routing_version_id ] ;
if ( ! itemCode ) continue ;
if ( ! result [ itemCode ] ) result [ itemCode ] = [ ] ;
result [ itemCode ] . push ( { code : d.process_code , name : d.process_name } ) ;
}
}
return res . json ( { success : true , data : result } ) ;
} catch ( error : any ) {
logger . error ( "벌크 라우팅 조회 실패" , { error : error.message } ) ;
return res . status ( 500 ) . json ( { success : false , message : error.message } ) ;
}
}
2026-03-20 14:18:44 +09:00
// ─── 작업지시 라우팅 변경 ───
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 ,
2026-04-14 17:51:47 +09:00
duration_minutes , input_type , lookup_target , display_fields ,
2026-04-24 11:12:32 +09:00
process_inspection_apply , equip_inspection_apply ,
condition_unit , condition_base_value , condition_tolerance ,
condition_auto_collect , condition_plc_data
2026-03-20 14:18:44 +09:00
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 ,
2026-04-14 17:51:47 +09:00
duration_minutes , input_type , lookup_target , display_fields ,
2026-04-24 11:12:32 +09:00
process_inspection_apply , equip_inspection_apply ,
condition_unit , condition_base_value , condition_tolerance ,
condition_auto_collect , condition_plc_data
2026-03-20 14:18:44 +09:00
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 (
2026-04-23 17:36:04 +09:00
` INSERT INTO wi_process_work_item (id, company_code, work_instruction_no, routing_detail_id, work_phase, title, is_required, sort_order, description, source_work_item_id, writer)
VALUES ( gen_random_uuid ( ) : : text , $1 , $2 , $3 , $4 , $5 , $6 , $7 , $8 , $9 , $10 ) RETURNING id ` ,
2026-03-20 14:18:44 +09:00
[ 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 (
2026-04-24 11:12:32 +09:00
` INSERT INTO wi_process_work_item_detail (id, 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, process_inspection_apply, equip_inspection_apply, condition_unit, condition_base_value, condition_tolerance, condition_auto_collect, condition_plc_data, writer)
VALUES ( gen_random_uuid ( ) : : text , $1 , $2 , $3 , $4 , $5 , $6 , $7 , $8 , $9 , $10 , $11 , $12 , $13 , $14 , $15 , $16 , $17 , $18 , $19 , $20 , $21 , $22 , $23 , $24 ) ` ,
[ 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 , origDetail . process_inspection_apply || null , origDetail . equip_inspection_apply || null , origDetail . condition_unit || null , origDetail . condition_base_value || null , origDetail . condition_tolerance || null , origDetail . condition_auto_collect || null , origDetail . condition_plc_data || null , userId ]
2026-03-20 14:18:44 +09:00
) ;
}
}
}
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 ]
) ;
// 새 데이터 삽입
2026-04-23 17:36:04 +09:00
// NOTE: wi_process_work_item / wi_process_work_item_detail.id 컬럼에 DEFAULT(gen_random_uuid()) 누락
// → id를 명시하지 않으면 NULL 저장되어 재조회 시 wi_work_item_id 매칭 실패(0건 반환)로 이어짐.
// 원본 테이블(process_work_item) DEFAULT와 동기되지 않은 스키마 이슈. 여기서 명시 바인딩으로 회피.
2026-03-20 14:18:44 +09:00
for ( const wi of workItems ) {
const wiResult = await client . query (
2026-04-23 17:36:04 +09:00
` INSERT INTO wi_process_work_item (id, company_code, work_instruction_no, routing_detail_id, work_phase, title, is_required, sort_order, description, source_work_item_id, writer)
VALUES ( gen_random_uuid ( ) : : text , $1 , $2 , $3 , $4 , $5 , $6 , $7 , $8 , $9 , $10 ) RETURNING id ` ,
2026-03-20 14:18:44 +09:00
[ 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 (
2026-04-24 11:12:32 +09:00
` INSERT INTO wi_process_work_item_detail (id, 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, process_inspection_apply, equip_inspection_apply, condition_unit, condition_base_value, condition_tolerance, condition_auto_collect, condition_plc_data, writer)
VALUES ( gen_random_uuid ( ) : : text , $1 , $2 , $3 , $4 , $5 , $6 , $7 , $8 , $9 , $10 , $11 , $12 , $13 , $14 , $15 , $16 , $17 , $18 , $19 , $20 , $21 , $22 , $23 , $24 ) ` ,
[ 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 , d . process_inspection_apply || null , d . equip_inspection_apply || null , d . condition_unit || null , d . condition_base_value || null , d . condition_tolerance || null , d . condition_auto_collect || null , d . condition_plc_data || null , userId ]
2026-03-20 14:18:44 +09:00
) ;
}
}
}
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 } ) ;
}
}