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" ;
// ─── 작업지시 목록 조회 (detail 기준 행 반환) ───
export async function getList ( req : AuthenticatedRequest , res : Response ) {
try {
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 ,
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 ,
2026-03-20 14:18:44 +09:00
wi . routing AS routing_version_id ,
COALESCE ( rv . version_name , '' ) AS routing_name ,
2026-03-19 17:52:17 +09:00
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
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 }
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 {
const companyCode = req . user ! . companyCode ;
const userId = req . user ! . userId ;
2026-03-20 14:18:44 +09:00
const { id : editId , status : wiStatus , progressStatus , reason , startDate , endDate , equipmentId , workTeam , worker , remark , items , routing : routingVersionId } = 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-03-20 14:18:44 +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, 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 ]
2026-03-19 17:52:17 +09:00
) ;
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 (
2026-03-20 14:18:44 +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,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 ]
2026-03-19 17:52:17 +09:00
) ;
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,created_date,writer) VALUES (gen_random_uuid()::text, $ 1, $ 2, $ 3, $ 4, $ 5, $ 6, $ 7, $ 8,NOW(), $ 9) ` ,
[ companyCode , wiNo , item . itemNumber || item . itemCode || "" , item . qty || "0" , item . remark || "" , item . sourceTable || "" , item . sourceId || "" , item . partCode || item . itemNumber || item . itemCode || "" , 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 } ) ; }
}
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 } ) ;
}
}
// ─── 작업지시 라우팅 변경 ───
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 } ) ;
}
}