2026-03-20 11:58:01 +09:00
/ * *
* 공 정 정 보 관 리 컨 트 롤 러
* - 공 정 마 스 터 CRUD
* - 공 정 별 설 비 관 리
* - 품 목 별 라 우 팅 관 리
* /
import { Response } from "express" ;
import { AuthenticatedRequest } from "../types/auth" ;
import { pool } from "../database/db" ;
import { logger } from "../utils/logger" ;
// ═══════════════════════════════════════════
// 공정 마스터 CRUD
// ═══════════════════════════════════════════
export async function getProcessList ( req : AuthenticatedRequest , res : Response ) {
try {
const companyCode = req . user ! . companyCode ;
const { processCode , processName , processType , useYn } = req . query ;
const conditions : string [ ] = [ ] ;
const params : any [ ] = [ ] ;
let idx = 1 ;
if ( companyCode !== "*" ) {
conditions . push ( ` company_code = $ ${ idx ++ } ` ) ;
params . push ( companyCode ) ;
}
if ( processCode ) {
conditions . push ( ` process_code ILIKE $ ${ idx ++ } ` ) ;
params . push ( ` % ${ processCode } % ` ) ;
}
if ( processName ) {
conditions . push ( ` process_name ILIKE $ ${ idx ++ } ` ) ;
params . push ( ` % ${ processName } % ` ) ;
}
if ( processType ) {
conditions . push ( ` process_type = $ ${ idx ++ } ` ) ;
params . push ( processType ) ;
}
if ( useYn ) {
2026-04-06 15:50:33 +09:00
// "Y" → "USE_Y"도 매칭, "N" → "USE_N"도 매칭
const useYnValue = String ( useYn ) ;
if ( useYnValue === "Y" || useYnValue === "N" ) {
conditions . push ( ` use_yn IN ( $ ${ idx ++ } , $ ${ idx ++ } ) ` ) ;
params . push ( useYnValue , ` USE_ ${ useYnValue } ` ) ;
} else {
conditions . push ( ` use_yn = $ ${ idx ++ } ` ) ;
params . push ( useYnValue ) ;
}
2026-03-20 11:58:01 +09:00
}
const where = conditions . length > 0 ? ` WHERE ${ conditions . join ( " AND " ) } ` : "" ;
const result = await pool . query (
` SELECT * FROM process_mng ${ where } ORDER BY process_code ` ,
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 createProcess ( req : AuthenticatedRequest , res : Response ) {
try {
const companyCode = req . user ! . companyCode ;
const writer = req . user ! . userId ;
const { process_name , process_type , standard_time , worker_count , use_yn } = req . body ;
// 공정코드 자동 채번: PROC-001, PROC-002, ...
const seqRes = await pool . query (
` SELECT process_code FROM process_mng WHERE company_code = $ 1 AND process_code LIKE 'PROC-%' ORDER BY process_code DESC LIMIT 1 ` ,
[ companyCode ]
) ;
let nextNum = 1 ;
if ( seqRes . rowCount ! > 0 ) {
const lastCode = seqRes . rows [ 0 ] . process_code ;
const numPart = parseInt ( lastCode . replace ( "PROC-" , "" ) , 10 ) ;
if ( ! isNaN ( numPart ) ) nextNum = numPart + 1 ;
}
const processCode = ` PROC- ${ String ( nextNum ) . padStart ( 3 , "0" ) } ` ;
const result = await pool . query (
` INSERT INTO process_mng (id, company_code, process_code, process_name, process_type, standard_time, worker_count, use_yn, writer)
VALUES ( gen_random_uuid ( ) : : text , $1 , $2 , $3 , $4 , $5 , $6 , $7 , $8 ) RETURNING * ` ,
[ companyCode , processCode , process_name , process_type , standard_time || "0" , worker_count || "0" , use_yn || "Y" , writer ]
) ;
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 updateProcess ( req : AuthenticatedRequest , res : Response ) {
try {
const companyCode = req . user ! . companyCode ;
const { id } = req . params ;
const { process_name , process_type , standard_time , worker_count , use_yn } = req . body ;
const result = await pool . query (
` UPDATE process_mng SET process_name= $ 1, process_type= $ 2, standard_time= $ 3, worker_count= $ 4, use_yn= $ 5, updated_date=NOW()
WHERE id = $6 AND company_code = $7 RETURNING * ` ,
[ process_name , process_type , standard_time , worker_count , use_yn , id , companyCode ]
) ;
if ( result . rowCount === 0 ) {
return res . status ( 404 ) . json ( { success : false , message : "공정을 찾을 수 없습니다." } ) ;
}
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 deleteProcesses ( req : AuthenticatedRequest , res : Response ) {
try {
const companyCode = req . user ! . companyCode ;
const { ids } = req . body ;
if ( ! ids || ! Array . isArray ( ids ) || ids . length === 0 ) {
return res . status ( 400 ) . json ( { success : false , message : "삭제할 공정을 선택해주세요." } ) ;
}
const placeholders = ids . map ( ( _ : any , i : number ) = > ` $ ${ i + 1 } ` ) . join ( "," ) ;
// 설비 매핑도 삭제
await pool . query (
` DELETE FROM process_equipment WHERE process_code IN (SELECT process_code FROM process_mng WHERE id IN ( ${ placeholders } ) AND company_code = $ ${ ids . length + 1 } ) ` ,
[ . . . ids , companyCode ]
) ;
const result = await pool . query (
` DELETE FROM process_mng WHERE id IN ( ${ placeholders } ) AND company_code = $ ${ ids . length + 1 } RETURNING id ` ,
[ . . . ids , companyCode ]
) ;
return res . json ( { success : true , deletedCount : result.rowCount } ) ;
} catch ( error : any ) {
logger . error ( "공정 삭제 실패" , { error : error.message } ) ;
return res . status ( 500 ) . json ( { success : false , message : error.message } ) ;
}
}
// ═══════════════════════════════════════════
// 공정별 설비 관리
// ═══════════════════════════════════════════
export async function getProcessEquipments ( req : AuthenticatedRequest , res : Response ) {
try {
const companyCode = req . user ! . companyCode ;
const { processCode } = req . params ;
2026-04-21 13:54:14 +09:00
// equipment_code 컬럼에 코드(legacy) 또는 id(신규)가 들어올 수 있어 두 경우 모두 매칭
2026-03-20 11:58:01 +09:00
const result = await pool . query (
2026-03-25 15:18:38 +09:00
` SELECT pe.*, em.equipment_name
2026-03-20 11:58:01 +09:00
FROM process_equipment pe
2026-04-21 13:54:14 +09:00
LEFT JOIN equipment_mng em
ON pe . company_code = em . company_code
AND ( pe . equipment_code = em . equipment_code OR pe . equipment_code = em . id )
2026-03-20 11:58:01 +09:00
WHERE pe . process_code = $1 AND pe . company_code = $2
ORDER BY pe . equipment_code ` ,
[ processCode , 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 addProcessEquipment ( req : AuthenticatedRequest , res : Response ) {
try {
const companyCode = req . user ! . companyCode ;
const writer = req . user ! . userId ;
const { process_code , equipment_code } = req . body ;
const dupCheck = await pool . query (
` SELECT id FROM process_equipment WHERE process_code= $ 1 AND equipment_code= $ 2 AND company_code= $ 3 ` ,
[ process_code , equipment_code , companyCode ]
) ;
if ( dupCheck . rowCount ! > 0 ) {
return res . status ( 400 ) . json ( { success : false , message : "이미 등록된 설비입니다." } ) ;
}
const result = await pool . query (
` INSERT INTO process_equipment (id, company_code, process_code, equipment_code, writer)
VALUES ( gen_random_uuid ( ) : : text , $1 , $2 , $3 , $4 ) RETURNING * ` ,
[ companyCode , process_code , equipment_code , writer ]
) ;
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 removeProcessEquipment ( req : AuthenticatedRequest , res : Response ) {
try {
const companyCode = req . user ! . companyCode ;
const { id } = req . params ;
await pool . query (
` DELETE FROM process_equipment WHERE id= $ 1 AND company_code= $ 2 ` ,
[ id , 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 getEquipmentList ( req : AuthenticatedRequest , res : Response ) {
try {
const companyCode = req . user ! . companyCode ;
const condition = companyCode === "*" ? "" : ` WHERE company_code = $ 1 ` ;
const params = companyCode === "*" ? [ ] : [ companyCode ] ;
const result = await pool . query (
2026-03-27 22:32:18 +09:00
` SELECT id, equipment_code, equipment_name FROM equipment_mng ${ condition } ORDER BY equipment_code ` ,
2026-03-20 11:58:01 +09:00
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 getItemsForRouting ( req : AuthenticatedRequest , res : Response ) {
try {
const companyCode = req . user ! . companyCode ;
const { search } = req . query ;
const conditions : string [ ] = [ "i.company_code = rv.company_code" ] ;
const params : any [ ] = [ ] ;
let idx = 1 ;
if ( companyCode !== "*" ) {
conditions . push ( ` i.company_code = $ ${ idx ++ } ` ) ;
params . push ( companyCode ) ;
}
if ( search ) {
conditions . push ( ` (i.item_number ILIKE $ ${ idx } OR i.item_name ILIKE $ ${ idx } ) ` ) ;
params . push ( ` % ${ search } % ` ) ;
idx ++ ;
}
const where = conditions . length > 0 ? ` WHERE ${ conditions . join ( " AND " ) } ` : "" ;
const result = await pool . query (
` SELECT DISTINCT i.id, i.item_number, i.item_name, i.size, i.unit, i.type
FROM item_info i
INNER JOIN item_routing_version rv ON rv . item_code = i . item_number AND rv . company_code = i . company_code
$ { where }
ORDER BY i . item_number LIMIT 200 ` ,
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 searchAllItems ( req : AuthenticatedRequest , res : Response ) {
try {
const companyCode = req . user ! . companyCode ;
const { search } = req . query ;
const conditions : string [ ] = [ ] ;
const params : any [ ] = [ ] ;
let idx = 1 ;
if ( companyCode !== "*" ) {
conditions . push ( ` company_code = $ ${ idx ++ } ` ) ;
params . push ( companyCode ) ;
}
if ( search ) {
conditions . push ( ` (item_number ILIKE $ ${ idx } OR item_name ILIKE $ ${ idx } ) ` ) ;
params . push ( ` % ${ search } % ` ) ;
idx ++ ;
}
const where = conditions . length > 0 ? ` WHERE ${ conditions . join ( " AND " ) } ` : "" ;
const result = await pool . query (
` SELECT id, item_number, item_name, size, unit, type FROM item_info ${ where } ORDER BY item_number LIMIT 200 ` ,
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 getRoutingVersions ( req : AuthenticatedRequest , res : Response ) {
try {
const companyCode = req . user ! . companyCode ;
const { itemCode } = req . params ;
const result = await pool . query (
` SELECT * FROM item_routing_version WHERE item_code= $ 1 AND company_code= $ 2 ORDER BY created_date ` ,
[ itemCode , 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 createRoutingVersion ( req : AuthenticatedRequest , res : Response ) {
try {
const companyCode = req . user ! . companyCode ;
const writer = req . user ! . userId ;
const { item_code , version_name , description , is_default } = req . body ;
if ( is_default ) {
await pool . query (
` UPDATE item_routing_version SET is_default=false WHERE item_code= $ 1 AND company_code= $ 2 ` ,
[ item_code , companyCode ]
) ;
}
const result = await pool . query (
` INSERT INTO item_routing_version (id, company_code, item_code, version_name, description, is_default, writer)
VALUES ( gen_random_uuid ( ) : : text , $1 , $2 , $3 , $4 , $5 , $6 ) RETURNING * ` ,
[ companyCode , item_code , version_name , description || "" , is_default || false , writer ]
) ;
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 deleteRoutingVersion ( req : AuthenticatedRequest , res : Response ) {
try {
const companyCode = req . user ! . companyCode ;
const { id } = req . params ;
await pool . query (
` DELETE FROM item_routing_detail WHERE routing_version_id= $ 1 AND company_code= $ 2 ` ,
[ id , companyCode ]
) ;
await pool . query (
` DELETE FROM item_routing_version WHERE id= $ 1 AND company_code= $ 2 ` ,
[ id , 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 getRoutingDetails ( req : AuthenticatedRequest , res : Response ) {
try {
const companyCode = req . user ! . companyCode ;
const { versionId } = req . params ;
const result = await pool . query (
` SELECT rd.*, pm.process_name
FROM item_routing_detail rd
LEFT JOIN process_mng pm ON rd . process_code = pm . process_code AND rd . company_code = pm . company_code
WHERE rd . routing_version_id = $1 AND rd . company_code = $2
ORDER BY CAST ( rd . seq_no AS INTEGER ) ` ,
[ versionId , companyCode ]
) ;
2026-04-17 18:25:35 +09:00
const rows = result . rows ;
const detailIds = rows . map ( ( r : any ) = > r . id ) . filter ( Boolean ) ;
2026-04-20 14:14:24 +09:00
let idsByDetail : Record < string , string [ ] > = { } ;
let codesByDetail : Record < string , string [ ] > = { } ;
2026-04-17 18:25:35 +09:00
if ( detailIds . length > 0 ) {
const mapRes = await pool . query (
2026-04-20 14:14:24 +09:00
` SELECT irs.routing_detail_id, irs.subcontractor_id, sm.subcontractor_code
FROM item_routing_subcontractor irs
LEFT JOIN subcontractor_mng sm ON irs . subcontractor_id = sm . id
WHERE irs . routing_detail_id = ANY ( $1 : : varchar [ ] )
ORDER BY irs . seq_order ` ,
2026-04-17 18:25:35 +09:00
[ detailIds ]
) ;
for ( const m of mapRes . rows ) {
const key = String ( m . routing_detail_id ) ;
2026-04-20 14:14:24 +09:00
( idsByDetail [ key ] || = [ ] ) . push ( m . subcontractor_id ) ;
if ( m . subcontractor_code ) ( codesByDetail [ key ] || = [ ] ) . push ( m . subcontractor_code ) ;
2026-04-17 18:25:35 +09:00
}
}
const enriched = rows . map ( ( r : any ) = > {
2026-04-20 14:14:24 +09:00
const ids = idsByDetail [ String ( r . id ) ] || [ ] ;
const codes = codesByDetail [ String ( r . id ) ] || [ ] ;
// 레거시 폴백: 매핑이 비어있고 legacy 단일 컬럼(code)에 값이 있으면 code 배열로 반환
const legacyCodes = ids . length === 0 && r . outsource_supplier ? [ r . outsource_supplier ] : codes ;
return {
. . . r ,
outsource_supplier_ids : ids ,
outsource_supplier_list : legacyCodes , // 하위호환 별칭 (code 배열)
} ;
2026-04-17 18:25:35 +09:00
} ) ;
return res . json ( { success : true , data : enriched } ) ;
2026-03-20 11:58:01 +09:00
} catch ( error : any ) {
logger . error ( "라우팅 상세 조회 실패" , { error : error.message } ) ;
return res . status ( 500 ) . json ( { success : false , message : error.message } ) ;
}
}
export async function saveRoutingDetails ( req : AuthenticatedRequest , res : Response ) {
try {
const companyCode = req . user ! . companyCode ;
const writer = req . user ! . userId ;
const { versionId } = req . params ;
const { details } = req . body ;
const client = await pool . connect ( ) ;
try {
await client . query ( "BEGIN" ) ;
2026-04-17 18:25:35 +09:00
// 기존 상세의 외주업체 매핑을 먼저 제거
await client . query (
` DELETE FROM item_routing_subcontractor
WHERE routing_detail_id IN (
SELECT id FROM item_routing_detail WHERE routing_version_id = $1 AND company_code = $2
) ` ,
[ versionId , companyCode ]
) ;
2026-03-20 11:58:01 +09:00
// 기존 상세 삭제 후 재입력
await client . query (
` DELETE FROM item_routing_detail WHERE routing_version_id= $ 1 AND company_code= $ 2 ` ,
[ versionId , companyCode ]
) ;
for ( const d of details ) {
2026-04-20 14:14:24 +09:00
const supplierIds : string [ ] = Array . isArray ( d . outsource_supplier_ids )
? d . outsource_supplier_ids . filter ( ( s : any ) = > typeof s === "string" && s . trim ( ) !== "" )
: [ ] ;
// legacy code 해석: 첫 번째 subcontractor_id → subcontractor_code 조회
let legacyCode = "" ;
if ( supplierIds . length > 0 ) {
const codeRes = await client . query (
` SELECT subcontractor_code FROM subcontractor_mng WHERE id= $ 1 LIMIT 1 ` ,
[ supplierIds [ 0 ] ]
) ;
legacyCode = codeRes . rows [ 0 ] ? . subcontractor_code || "" ;
} else if ( d . outsource_supplier ) {
// 프론트가 아직 id 없이 code만 보낸 경우(레거시 호환)
legacyCode = d . outsource_supplier ;
}
2026-04-17 18:25:35 +09:00
const insertRes = await client . query (
2026-04-28 16:14:27 +09:00
` INSERT INTO item_routing_detail (id, company_code, routing_version_id, seq_no, process_code, is_required, is_fixed_order, work_type, standard_time, outsource_supplier, execution_type, writer)
VALUES ( gen_random_uuid ( ) : : text , $1 , $2 , $3 , $4 , $5 , $6 , $7 , $8 , $9 , $10 , $11 )
2026-04-17 18:25:35 +09:00
RETURNING id ` ,
2026-04-28 16:14:27 +09:00
[ companyCode , versionId , d . seq_no , d . process_code , d . is_required || "Y" , d . is_fixed_order || "Y" , d . work_type || "내부" , d . standard_time || "0" , legacyCode , d . execution_type || null , writer ]
2026-03-20 11:58:01 +09:00
) ;
2026-04-17 18:25:35 +09:00
const newDetailId = insertRes . rows [ 0 ] . id ;
2026-04-20 14:14:24 +09:00
for ( let i = 0 ; i < supplierIds . length ; i ++ ) {
2026-04-17 18:25:35 +09:00
await client . query (
2026-04-24 17:58:11 +09:00
// 본서버 id 컬럼이 uuid 타입, 개발서버는 varchar — ::text 캐스팅하면 본서버에서 타입 불일치 오류 발생
2026-04-20 14:14:24 +09:00
` INSERT INTO item_routing_subcontractor (id, company_code, routing_detail_id, subcontractor_id, seq_order)
2026-04-24 17:58:11 +09:00
VALUES ( gen_random_uuid ( ) , $1 , $2 , $3 , $4 ) ` ,
2026-04-20 14:14:24 +09:00
[ companyCode , newDetailId , supplierIds [ i ] , i ]
2026-04-17 18:25:35 +09:00
) ;
}
2026-03-20 11:58:01 +09:00
}
await client . query ( "COMMIT" ) ;
return res . json ( { success : true } ) ;
} catch ( err ) {
await client . query ( "ROLLBACK" ) ;
throw err ;
} finally {
client . release ( ) ;
}
} catch ( error : any ) {
logger . error ( "라우팅 상세 저장 실패" , { error : error.message } ) ;
return res . status ( 500 ) . json ( { success : false , message : error.message } ) ;
}
}
2026-03-20 13:46:30 +09:00
// ═══════════════════════════════════════════
// BOM 구성 자재 조회 (품목코드 기반)
// ═══════════════════════════════════════════
export async function getBomMaterials ( req : AuthenticatedRequest , res : Response ) {
try {
const companyCode = req . user ! . companyCode ;
const { itemCode } = req . params ;
if ( ! itemCode ) {
return res . status ( 400 ) . json ( { success : false , message : "itemCode는 필수입니다" } ) ;
}
const query = `
SELECT
bd . id ,
bd . child_item_id ,
bd . quantity ,
bd . unit as detail_unit ,
bd . process_type ,
i . item_name as child_item_name ,
i . item_number as child_item_code ,
i . type as child_item_type ,
i . unit as item_unit
FROM bom b
JOIN bom_detail bd ON b . id = bd . bom_id AND b . company_code = bd . company_code
LEFT JOIN item_info i ON bd . child_item_id = i . id AND bd . company_code = i . company_code
WHERE b . item_code = $1 AND b . company_code = $2
ORDER BY bd . seq_no ASC , bd . created_date ASC
` ;
const result = await pool . query ( query , [ itemCode , companyCode ] ) ;
logger . info ( "BOM 자재 조회 성공" , { companyCode , itemCode , count : result.rowCount } ) ;
return res . json ( { success : true , data : result.rows } ) ;
} catch ( error : any ) {
logger . error ( "BOM 자재 조회 실패" , { error : error.message } ) ;
return res . status ( 500 ) . json ( { success : false , message : error.message } ) ;
}
}