2026-03-20 16:09:39 +09:00
/ * *
* 입 고 관 리 컨 트 롤 러
*
* 입 고 유 형 별 소 스 테 이 블 :
* - 구 매 입 고 → purchase_order_mng ( 발 주 )
* - 반 품 입 고 → shipment_instruction + shipment_instruction_detail ( 출 하 )
* - 기 타 입 고 → item_info ( 품 목 )
* /
import { Response } from "express" ;
import { AuthenticatedRequest } from "../types/auth" ;
import { getPool } from "../database/db" ;
import { logger } from "../utils/logger" ;
// 입고 목록 조회
export async function getList ( req : AuthenticatedRequest , res : Response ) {
try {
const companyCode = req . user ! . companyCode ;
const {
inbound_type ,
inbound_status ,
search_keyword ,
date_from ,
date_to ,
} = req . query ;
const conditions : string [ ] = [ ] ;
const params : any [ ] = [ ] ;
let paramIdx = 1 ;
if ( companyCode === "*" ) {
// 최고 관리자: 전체 조회
} else {
conditions . push ( ` im.company_code = $ ${ paramIdx } ` ) ;
params . push ( companyCode ) ;
paramIdx ++ ;
}
if ( inbound_type && inbound_type !== "all" ) {
conditions . push ( ` im.inbound_type = $ ${ paramIdx } ` ) ;
params . push ( inbound_type ) ;
paramIdx ++ ;
}
if ( inbound_status && inbound_status !== "all" ) {
conditions . push ( ` im.inbound_status = $ ${ paramIdx } ` ) ;
params . push ( inbound_status ) ;
paramIdx ++ ;
}
if ( search_keyword ) {
conditions . push (
` (im.inbound_number ILIKE $ ${ paramIdx } OR im.item_name ILIKE $ ${ paramIdx } OR im.item_number ILIKE $ ${ paramIdx } OR im.supplier_name ILIKE $ ${ paramIdx } OR im.reference_number ILIKE $ ${ paramIdx } ) `
) ;
params . push ( ` % ${ search_keyword } % ` ) ;
paramIdx ++ ;
}
if ( date_from ) {
conditions . push ( ` im.inbound_date >= $ ${ paramIdx } ::date ` ) ;
params . push ( date_from ) ;
paramIdx ++ ;
}
if ( date_to ) {
conditions . push ( ` im.inbound_date <= $ ${ paramIdx } ::date ` ) ;
params . push ( date_to ) ;
paramIdx ++ ;
}
const whereClause =
conditions . length > 0 ? ` WHERE ${ conditions . join ( " AND " ) } ` : "" ;
const query = `
SELECT
im . * ,
wh . warehouse_name
FROM inbound_mng im
LEFT JOIN warehouse_info wh
ON im . warehouse_code = wh . warehouse_code
AND im . company_code = wh . company_code
$ { whereClause }
ORDER BY im . created_date DESC
` ;
const pool = getPool ( ) ;
const result = await pool . query ( query , params ) ;
logger . info ( "입고 목록 조회" , {
companyCode ,
rowCount : result.rowCount ,
} ) ;
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 create ( req : AuthenticatedRequest , res : Response ) {
const pool = getPool ( ) ;
const client = await pool . connect ( ) ;
try {
const companyCode = req . user ! . companyCode ;
const userId = req . user ! . userId ;
const { items , inbound_number , inbound_date , warehouse_code , location_code , inspector , manager , memo } = req . body ;
if ( ! items || ! Array . isArray ( items ) || items . length === 0 ) {
return res . status ( 400 ) . json ( { success : false , message : "입고 품목이 없습니다." } ) ;
}
await client . query ( "BEGIN" ) ;
const insertedRows : any [ ] = [ ] ;
for ( const item of items ) {
const result = await client . query (
` INSERT INTO inbound_mng (
company_code , inbound_number , inbound_type , inbound_date ,
reference_number , supplier_code , supplier_name ,
item_number , item_name , spec , material , unit ,
inbound_qty , unit_price , total_amount ,
lot_number , warehouse_code , location_code ,
inbound_status , inspection_status ,
inspector , manager , memo ,
source_table , source_id ,
created_date , created_by , writer , status
) VALUES (
$1 , $2 , $3 , $4 : : date ,
$5 , $6 , $7 ,
$8 , $9 , $10 , $11 , $12 ,
$13 , $14 , $15 ,
$16 , $17 , $18 ,
$19 , $20 ,
$21 , $22 , $23 ,
$24 , $25 ,
NOW ( ) , $26 , $26 , '입고'
) RETURNING * ` ,
[
companyCode ,
inbound_number || item . inbound_number ,
item . inbound_type ,
inbound_date || item . inbound_date ,
item . reference_number || null ,
item . supplier_code || null ,
item . supplier_name || null ,
item . item_number || null ,
item . item_name || null ,
item . spec || null ,
item . material || null ,
item . unit || "EA" ,
item . inbound_qty || 0 ,
item . unit_price || 0 ,
item . total_amount || 0 ,
item . lot_number || null ,
warehouse_code || item . warehouse_code || null ,
location_code || item . location_code || null ,
item . inbound_status || "대기" ,
item . inspection_status || "대기" ,
inspector || item . inspector || null ,
manager || item . manager || null ,
memo || item . memo || null ,
item . source_table || null ,
item . source_id || null ,
userId ,
]
) ;
insertedRows . push ( result . rows [ 0 ] ) ;
2026-03-25 10:48:47 +09:00
// 재고 업데이트 (inventory_stock): 입고 수량 증가
const itemCode = item . item_number || null ;
const whCode = warehouse_code || item . warehouse_code || null ;
const locCode = location_code || item . location_code || null ;
const inQty = Number ( item . inbound_qty ) || 0 ;
if ( itemCode && inQty > 0 ) {
const existingStock = await client . query (
` SELECT id FROM inventory_stock
WHERE company_code = $1 AND item_code = $2
AND COALESCE ( warehouse_code , '' ) = COALESCE ( $3 , '' )
AND COALESCE ( location_code , '' ) = COALESCE ( $4 , '' )
LIMIT 1 ` ,
[ companyCode , itemCode , whCode || '' , locCode || '' ]
) ;
if ( existingStock . rows . length > 0 ) {
await client . query (
` UPDATE inventory_stock
SET current_qty = CAST ( COALESCE ( CAST ( NULLIF ( current_qty , '' ) AS numeric ) , 0 ) + $1 AS text ) ,
last_in_date = NOW ( ) ,
updated_date = NOW ( )
WHERE id = $2 ` ,
[ inQty , existingStock . rows [ 0 ] . id ]
) ;
} else {
await client . query (
` INSERT INTO inventory_stock (
company_code , item_code , warehouse_code , location_code ,
current_qty , safety_qty , last_in_date ,
created_date , updated_date , writer
) VALUES ( $1 , $2 , $3 , $4 , $5 , '0' , NOW ( ) , NOW ( ) , NOW ( ) , $6 ) ` ,
[ companyCode , itemCode , whCode , locCode , String ( inQty ) , userId ]
) ;
}
}
2026-03-20 16:09:39 +09:00
// 구매입고인 경우 발주의 received_qty 업데이트
if ( item . inbound_type === "구매입고" && item . source_id && item . source_table === "purchase_order_mng" ) {
await client . query (
` UPDATE purchase_order_mng
SET received_qty = CAST (
COALESCE ( CAST ( NULLIF ( received_qty , '' ) AS numeric ) , 0 ) + $1 AS text
) ,
remain_qty = CAST (
GREATEST ( COALESCE ( CAST ( NULLIF ( order_qty , '' ) AS numeric ) , 0 )
- COALESCE ( CAST ( NULLIF ( received_qty , '' ) AS numeric ) , 0 ) - $1 , 0 ) AS text
) ,
status = CASE
WHEN COALESCE ( CAST ( NULLIF ( received_qty , '' ) AS numeric ) , 0 ) + $1
>= COALESCE ( CAST ( NULLIF ( order_qty , '' ) AS numeric ) , 0 )
THEN '입고완료'
ELSE '부분입고'
END ,
updated_date = NOW ( )
WHERE id = $2 AND company_code = $3 ` ,
[ item . inbound_qty || 0 , item . source_id , companyCode ]
) ;
}
}
await client . query ( "COMMIT" ) ;
logger . info ( "입고 등록 완료" , {
companyCode ,
userId ,
count : insertedRows.length ,
inbound_number ,
} ) ;
return res . json ( {
success : true ,
data : insertedRows ,
message : ` ${ insertedRows . length } 건 입고 등록 완료 ` ,
} ) ;
} catch ( error : any ) {
await client . query ( "ROLLBACK" ) ;
logger . error ( "입고 등록 실패" , { error : error.message } ) ;
return res . status ( 500 ) . json ( { success : false , message : error.message } ) ;
} finally {
client . release ( ) ;
}
}
// 입고 수정
export async function update ( req : AuthenticatedRequest , res : Response ) {
try {
const companyCode = req . user ! . companyCode ;
const userId = req . user ! . userId ;
const { id } = req . params ;
const {
inbound_date , inbound_qty , unit_price , total_amount ,
lot_number , warehouse_code , location_code ,
inbound_status , inspection_status ,
inspector , manager : mgr , memo ,
} = req . body ;
const pool = getPool ( ) ;
const result = await pool . query (
` UPDATE inbound_mng SET
inbound_date = COALESCE ( $1 : : date , inbound_date ) ,
inbound_qty = COALESCE ( $2 , inbound_qty ) ,
unit_price = COALESCE ( $3 , unit_price ) ,
total_amount = COALESCE ( $4 , total_amount ) ,
lot_number = COALESCE ( $5 , lot_number ) ,
warehouse_code = COALESCE ( $6 , warehouse_code ) ,
location_code = COALESCE ( $7 , location_code ) ,
inbound_status = COALESCE ( $8 , inbound_status ) ,
inspection_status = COALESCE ( $9 , inspection_status ) ,
inspector = COALESCE ( $10 , inspector ) ,
manager = COALESCE ( $11 , manager ) ,
memo = COALESCE ( $12 , memo ) ,
updated_date = NOW ( ) ,
updated_by = $13
WHERE id = $14 AND company_code = $15
RETURNING * ` ,
[
inbound_date , inbound_qty , unit_price , total_amount ,
lot_number , warehouse_code , location_code ,
inbound_status , inspection_status ,
inspector , mgr , memo ,
userId , id , companyCode ,
]
) ;
if ( result . rowCount === 0 ) {
return res . status ( 404 ) . json ( { success : false , message : "입고 데이터를 찾을 수 없습니다." } ) ;
}
logger . info ( "입고 수정" , { companyCode , userId , id } ) ;
return res . json ( { success : true , data : result.rows [ 0 ] } ) ;
} catch ( error : any ) {
logger . error ( "입고 수정 실패" , { error : error.message } ) ;
return res . status ( 500 ) . json ( { success : false , message : error.message } ) ;
}
}
// 입고 삭제
export async function deleteReceiving ( req : AuthenticatedRequest , res : Response ) {
try {
const companyCode = req . user ! . companyCode ;
const { id } = req . params ;
const pool = getPool ( ) ;
const result = await pool . query (
` DELETE FROM inbound_mng WHERE id = $ 1 AND company_code = $ 2 RETURNING id ` ,
[ id , companyCode ]
) ;
if ( result . rowCount === 0 ) {
return res . status ( 404 ) . json ( { success : false , message : "데이터를 찾을 수 없습니다." } ) ;
}
logger . info ( "입고 삭제" , { companyCode , id } ) ;
return res . json ( { success : true , message : "삭제 완료" } ) ;
} catch ( error : any ) {
logger . error ( "입고 삭제 실패" , { error : error.message } ) ;
return res . status ( 500 ) . json ( { success : false , message : error.message } ) ;
}
}
// 구매입고용: 발주 데이터 조회 (미입고분)
export async function getPurchaseOrders ( req : AuthenticatedRequest , res : Response ) {
try {
const companyCode = req . user ! . companyCode ;
const { keyword } = req . query ;
const conditions : string [ ] = [ "company_code = $1" ] ;
const params : any [ ] = [ companyCode ] ;
let paramIdx = 2 ;
// 잔량이 있는 것만 조회
conditions . push (
` COALESCE(CAST(NULLIF(remain_qty, '') AS numeric), COALESCE(CAST(NULLIF(order_qty, '') AS numeric), 0) - COALESCE(CAST(NULLIF(received_qty, '') AS numeric), 0)) > 0 `
) ;
conditions . push ( ` status NOT IN ('입고완료', '취소') ` ) ;
if ( keyword ) {
conditions . push (
` (purchase_no ILIKE $ ${ paramIdx } OR item_name ILIKE $ ${ paramIdx } OR item_code ILIKE $ ${ paramIdx } OR supplier_name ILIKE $ ${ paramIdx } ) `
) ;
params . push ( ` % ${ keyword } % ` ) ;
paramIdx ++ ;
}
const pool = getPool ( ) ;
const result = await pool . query (
` SELECT
id , purchase_no , order_date , supplier_code , supplier_name ,
item_code , item_name , spec , material ,
COALESCE ( CAST ( NULLIF ( order_qty , '' ) AS numeric ) , 0 ) AS order_qty ,
COALESCE ( CAST ( NULLIF ( received_qty , '' ) AS numeric ) , 0 ) AS received_qty ,
COALESCE ( CAST ( NULLIF ( remain_qty , '' ) AS numeric ) ,
COALESCE ( CAST ( NULLIF ( order_qty , '' ) AS numeric ) , 0 )
- COALESCE ( CAST ( NULLIF ( received_qty , '' ) AS numeric ) , 0 )
) AS remain_qty ,
COALESCE ( CAST ( NULLIF ( unit_price , '' ) AS numeric ) , 0 ) AS unit_price ,
status , due_date
FROM purchase_order_mng
WHERE $ { conditions . join ( " AND " ) }
ORDER BY order_date DESC , purchase_no ` ,
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 getShipments ( req : AuthenticatedRequest , res : Response ) {
try {
const companyCode = req . user ! . companyCode ;
const { keyword } = req . query ;
const conditions : string [ ] = [ "si.company_code = $1" ] ;
const params : any [ ] = [ companyCode ] ;
let paramIdx = 2 ;
if ( keyword ) {
conditions . push (
` (si.instruction_no ILIKE $ ${ paramIdx } OR sid.item_name ILIKE $ ${ paramIdx } OR sid.item_code ILIKE $ ${ paramIdx } ) `
) ;
params . push ( ` % ${ keyword } % ` ) ;
paramIdx ++ ;
}
const pool = getPool ( ) ;
const result = await pool . query (
` SELECT
sid . id AS detail_id ,
si . id AS instruction_id ,
si . instruction_no ,
si . instruction_date ,
si . partner_id ,
si . status AS instruction_status ,
sid . item_code ,
sid . item_name ,
sid . spec ,
sid . material ,
COALESCE ( sid . ship_qty , 0 ) AS ship_qty ,
COALESCE ( sid . order_qty , 0 ) AS order_qty ,
sid . source_type
FROM shipment_instruction si
JOIN shipment_instruction_detail sid
ON si . id = sid . instruction_id
AND si . company_code = sid . company_code
WHERE $ { conditions . join ( " AND " ) }
ORDER BY si . instruction_date DESC , si . instruction_no ` ,
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 getItems ( req : AuthenticatedRequest , res : Response ) {
try {
const companyCode = req . user ! . companyCode ;
const { keyword } = req . query ;
const conditions : string [ ] = [ "company_code = $1" ] ;
const params : any [ ] = [ companyCode ] ;
let paramIdx = 2 ;
if ( keyword ) {
conditions . push (
` (item_number ILIKE $ ${ paramIdx } OR item_name ILIKE $ ${ paramIdx } ) `
) ;
params . push ( ` % ${ keyword } % ` ) ;
paramIdx ++ ;
}
const pool = getPool ( ) ;
const result = await pool . query (
` SELECT
id , item_number , item_name , size AS spec , material , unit ,
COALESCE ( CAST ( NULLIF ( standard_price , '' ) AS numeric ) , 0 ) AS standard_price
FROM item_info
WHERE $ { conditions . join ( " AND " ) }
ORDER BY item_name ` ,
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 generateNumber ( req : AuthenticatedRequest , res : Response ) {
try {
const companyCode = req . user ! . companyCode ;
const pool = getPool ( ) ;
const today = new Date ( ) ;
const yyyy = today . getFullYear ( ) ;
const prefix = ` RCV- ${ yyyy } - ` ;
const result = await pool . query (
` SELECT inbound_number FROM inbound_mng
WHERE company_code = $1 AND inbound_number LIKE $2
ORDER BY inbound_number DESC LIMIT 1 ` ,
[ companyCode , ` ${ prefix } % ` ]
) ;
let seq = 1 ;
if ( result . rows . length > 0 ) {
const lastNo = result . rows [ 0 ] . inbound_number ;
const lastSeq = parseInt ( lastNo . replace ( prefix , "" ) , 10 ) ;
if ( ! isNaN ( lastSeq ) ) seq = lastSeq + 1 ;
}
const newNumber = ` ${ prefix } ${ String ( seq ) . padStart ( 4 , "0" ) } ` ;
return res . json ( { success : true , data : newNumber } ) ;
} catch ( error : any ) {
logger . error ( "입고번호 생성 실패" , { error : error.message } ) ;
return res . status ( 500 ) . json ( { success : false , message : error.message } ) ;
}
}
// 창고 목록 조회
export async function getWarehouses ( req : AuthenticatedRequest , res : Response ) {
try {
const companyCode = req . user ! . companyCode ;
const pool = getPool ( ) ;
const result = await pool . query (
` SELECT warehouse_code, warehouse_name, warehouse_type
FROM warehouse_info
WHERE company_code = $1 AND status != '삭제'
ORDER BY warehouse_name ` ,
[ 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 } ) ;
}
}