2026-03-25 10:48:47 +09:00
/ * *
* 출 고 관 리 컨 트 롤 러
*
* 출 고 유 형 별 소 스 테 이 블 :
* - 판 매 출 고 → shipment_instruction + shipment_instruction_detail ( 출 하 지 시 )
* - 반 품 출 고 → purchase_order_mng ( 발 주 / 입 고 )
* - 기 타 출 고 → item_info ( 품 목 )
* /
2026-04-09 14:38:28 +09:00
import type { Response } from "express" ;
2026-03-25 10:48:47 +09:00
import { getPool } from "../database/db" ;
2026-04-09 14:38:28 +09:00
import type { AuthenticatedRequest } from "../types/auth" ;
2026-04-20 14:14:24 +09:00
import { adjustInventory } from "../utils/inventoryUtils" ;
2026-03-25 10:48:47 +09:00
import { logger } from "../utils/logger" ;
// 출고 목록 조회
export async function getList ( req : AuthenticatedRequest , res : Response ) {
2026-04-09 14:38:28 +09:00
try {
const companyCode = req . user ! . companyCode ;
const {
outbound_type ,
outbound_status ,
search_keyword ,
date_from ,
date_to ,
} = req . query ;
const conditions : string [ ] = [ ] ;
const params : any [ ] = [ ] ;
let paramIdx = 1 ;
if ( companyCode === "*" ) {
// 최고 관리자: 전체 조회
} else {
conditions . push ( ` om.company_code = $ ${ paramIdx } ` ) ;
params . push ( companyCode ) ;
paramIdx ++ ;
}
if ( outbound_type && outbound_type !== "all" ) {
conditions . push ( ` om.outbound_type = $ ${ paramIdx } ` ) ;
params . push ( outbound_type ) ;
paramIdx ++ ;
}
if ( outbound_status && outbound_status !== "all" ) {
conditions . push ( ` om.outbound_status = $ ${ paramIdx } ` ) ;
params . push ( outbound_status ) ;
paramIdx ++ ;
}
if ( search_keyword ) {
conditions . push (
` (om.outbound_number ILIKE $ ${ paramIdx } OR om.item_name ILIKE $ ${ paramIdx } OR om.item_code ILIKE $ ${ paramIdx } OR om.customer_name ILIKE $ ${ paramIdx } OR om.reference_number ILIKE $ ${ paramIdx } ) ` ,
) ;
params . push ( ` % ${ search_keyword } % ` ) ;
paramIdx ++ ;
}
if ( date_from ) {
conditions . push ( ` om.outbound_date >= $ ${ paramIdx } ` ) ;
params . push ( date_from ) ;
paramIdx ++ ;
}
if ( date_to ) {
conditions . push ( ` om.outbound_date <= $ ${ paramIdx } ` ) ;
params . push ( date_to ) ;
paramIdx ++ ;
}
const whereClause =
conditions . length > 0 ? ` WHERE ${ conditions . join ( " AND " ) } ` : "" ;
const query = `
2026-03-25 10:48:47 +09:00
SELECT
om . * ,
wh . warehouse_name
FROM outbound_mng om
LEFT JOIN warehouse_info wh
ON om . warehouse_code = wh . warehouse_code
AND om . company_code = wh . company_code
$ { whereClause }
ORDER BY om . created_date DESC
` ;
2026-04-09 14:38:28 +09:00
const pool = getPool ( ) ;
const result = await pool . query ( query , params ) ;
2026-03-25 10:48:47 +09:00
2026-04-09 14:38:28 +09:00
logger . info ( "출고 목록 조회" , {
companyCode ,
rowCount : result.rowCount ,
} ) ;
2026-03-25 10:48:47 +09:00
2026-04-09 14:38:28 +09:00
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 } ) ;
}
2026-03-25 10:48:47 +09:00
}
// 출고 등록 (다건)
export async function create ( req : AuthenticatedRequest , res : Response ) {
2026-04-09 14:38:28 +09:00
const pool = getPool ( ) ;
const client = await pool . connect ( ) ;
try {
const companyCode = req . user ! . companyCode ;
const userId = req . user ! . userId ;
const {
items ,
outbound_number ,
outbound_date ,
warehouse_code ,
location_code ,
manager_id ,
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 outbound_mng (
2026-04-02 17:39:42 +09:00
id , company_code , outbound_number , outbound_type , outbound_date ,
2026-03-25 10:48:47 +09:00
reference_number , customer_code , customer_name ,
item_code , item_name , specification , material , unit ,
outbound_qty , unit_price , total_amount ,
lot_number , warehouse_code , location_code ,
outbound_status , manager_id , memo ,
source_type , sales_order_id , shipment_plan_id , item_info_id ,
destination_code , delivery_destination , delivery_address ,
created_date , created_by , writer , status
) VALUES (
2026-04-02 17:39:42 +09:00
gen_random_uuid ( ) : : text , $1 , $2 , $3 , $4 ,
2026-03-25 10:48:47 +09:00
$5 , $6 , $7 ,
$8 , $9 , $10 , $11 , $12 ,
$13 , $14 , $15 ,
$16 , $17 , $18 ,
$19 , $20 , $21 ,
$22 , $23 , $24 , $25 ,
$26 , $27 , $28 ,
NOW ( ) , $29 , $29 , '출고'
) RETURNING * ` ,
2026-04-09 14:38:28 +09:00
[
companyCode ,
outbound_number || item . outbound_number ,
item . outbound_type ,
outbound_date || item . outbound_date ,
item . reference_number || null ,
item . customer_code || null ,
item . customer_name || null ,
item . item_code || item . item_number || null ,
item . item_name || null ,
item . spec || item . specification || null ,
item . material || null ,
item . unit || "EA" ,
item . outbound_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 . outbound_status || "대기" ,
manager_id || item . manager_id || null ,
memo || item . memo || null ,
item . source_type || null ,
item . sales_order_id || null ,
item . shipment_plan_id || null ,
item . item_info_id || null ,
item . destination_code || null ,
item . delivery_destination || null ,
item . delivery_address || null ,
userId ,
] ,
) ;
insertedRows . push ( result . rows [ 0 ] ) ;
// 재고 업데이트 (inventory_stock): 출고 수량 차감
const itemCode = item . item_code || item . item_number || null ;
const whCode = warehouse_code || item . warehouse_code || null ;
const locCode = location_code || item . location_code || null ;
const outQty = Number ( item . outbound_qty ) || 0 ;
if ( itemCode && outQty > 0 ) {
// 재고 사전 검증: 부족 시 즉시 에러 (트랜잭션 ROLLBACK)
const stockCheck = await client . query (
` SELECT COALESCE(CAST(NULLIF(current_qty, '') AS numeric), 0) as cur
2026-04-09 14:28:57 +09:00
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 ` ,
2026-04-09 14:38:28 +09:00
[ companyCode , itemCode , whCode || "" , locCode || "" ] ,
) ;
const currentStock = parseFloat ( stockCheck . rows [ 0 ] ? . cur || "0" ) ;
if ( currentStock < outQty ) {
throw new Error (
` 재고 부족: ${ item . item_name || itemCode } (창고 ${ whCode || "미지정" } ) — 현재 재고 ${ currentStock } , 요청 출고 ${ outQty } ` ,
) ;
}
const existingStock = await client . query (
` SELECT id FROM inventory_stock
2026-03-25 10:48:47 +09:00
WHERE company_code = $1 AND item_code = $2
AND COALESCE ( warehouse_code , '' ) = COALESCE ( $3 , '' )
AND COALESCE ( location_code , '' ) = COALESCE ( $4 , '' )
LIMIT 1 ` ,
2026-04-09 14:38:28 +09:00
[ companyCode , itemCode , whCode || "" , locCode || "" ] ,
) ;
2026-03-25 10:48:47 +09:00
2026-04-09 14:38:28 +09:00
if ( existingStock . rows . length > 0 ) {
await client . query (
` UPDATE inventory_stock
2026-03-25 10:48:47 +09:00
SET current_qty = CAST ( GREATEST ( COALESCE ( CAST ( NULLIF ( current_qty , '' ) AS numeric ) , 0 ) - $1 , 0 ) AS text ) ,
last_out_date = NOW ( ) ,
updated_date = NOW ( )
WHERE id = $2 ` ,
2026-04-09 14:38:28 +09:00
[ outQty , existingStock . rows [ 0 ] . id ] ,
) ;
} else {
// 재고 레코드가 없으면 0으로 생성 (마이너스 방지)
await client . query (
` INSERT INTO inventory_stock (
2026-04-03 16:02:14 +09:00
id , company_code , item_code , warehouse_code , location_code ,
2026-03-25 10:48:47 +09:00
current_qty , safety_qty , last_out_date ,
created_date , updated_date , writer
2026-04-03 16:02:14 +09:00
) VALUES ( gen_random_uuid ( ) : : text , $1 , $2 , $3 , $4 , '0' , '0' , NOW ( ) , NOW ( ) , NOW ( ) , $5 ) ` ,
2026-04-09 14:38:28 +09:00
[ companyCode , itemCode , whCode , locCode , userId ] ,
) ;
}
2026-04-03 16:02:14 +09:00
2026-04-09 14:38:28 +09:00
// 재고 이력 기록 (inventory_history)
const afterStockRes = await client . query (
` SELECT current_qty FROM inventory_stock
2026-04-03 16:02:14 +09:00
WHERE company_code = $1 AND item_code = $2
AND COALESCE ( warehouse_code , '' ) = COALESCE ( $3 , '' )
AND COALESCE ( location_code , '' ) = COALESCE ( $4 , '' )
LIMIT 1 ` ,
2026-04-09 14:38:28 +09:00
[ companyCode , itemCode , whCode || "" , locCode || "" ] ,
) ;
const afterQty = afterStockRes . rows [ 0 ] ? . current_qty || "0" ;
await client . query (
` INSERT INTO inventory_history (
2026-04-03 17:38:14 +09:00
id , company_code , item_code , warehouse_code , location_code ,
transaction_type , transaction_date , quantity , balance_qty , remark ,
writer , created_date
) VALUES ( gen_random_uuid ( ) : : text , $1 , $2 , $3 , $4 , '출고' , NOW ( ) , $5 , $6 , $7 , $8 , NOW ( ) ) ` ,
2026-04-09 14:38:28 +09:00
[
companyCode ,
itemCode ,
whCode ,
locCode ,
String ( - outQty ) ,
afterQty ,
item . outbound_type || "출고" ,
userId ,
] ,
) ;
}
// 판매출고인 경우 출하지시의 ship_qty 업데이트 + 수주상세 ship_qty 반영
if (
item . outbound_type === "판매출고" &&
item . source_id &&
item . source_type === "shipment_instruction_detail"
) {
const outQtyNum = Number ( item . outbound_qty ) || 0 ;
await client . query (
` UPDATE shipment_instruction_detail
2026-03-25 10:48:47 +09:00
SET ship_qty = COALESCE ( ship_qty , 0 ) + $1 ,
updated_date = NOW ( )
WHERE id = $2 AND company_code = $3 ` ,
2026-04-09 14:38:28 +09:00
[ outQtyNum , item . source_id , companyCode ] ,
) ;
// 출하지시 상세의 detail_id로 수주상세(sales_order_detail) ship_qty도 업데이트
const sidRes = await client . query (
` SELECT detail_id FROM shipment_instruction_detail WHERE id = $ 1 AND company_code = $ 2 ` ,
[ item . source_id , companyCode ] ,
) ;
const detailId = sidRes . rows [ 0 ] ? . detail_id ;
if ( detailId ) {
await client . query (
` UPDATE sales_order_detail
2026-04-08 15:33:09 +09:00
SET ship_qty = ( COALESCE ( NULLIF ( ship_qty , '' ) : : numeric , 0 ) + $1 ) : : text ,
balance_qty = ( COALESCE ( NULLIF ( qty , '' ) : : numeric , 0 ) - COALESCE ( NULLIF ( ship_qty , '' ) : : numeric , 0 ) - $1 ) : : text ,
updated_date = NOW ( )
WHERE id = $2 AND company_code = $3 ` ,
2026-04-09 14:38:28 +09:00
[ outQtyNum , detailId , companyCode ] ,
) ;
}
}
}
await client . query ( "COMMIT" ) ;
logger . info ( "출고 등록 완료" , {
companyCode ,
userId ,
count : insertedRows.length ,
outbound_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 ( ) ;
}
2026-03-25 10:48:47 +09:00
}
// 출고 수정
export async function update ( req : AuthenticatedRequest , res : Response ) {
2026-04-20 14:14:24 +09:00
const pool = getPool ( ) ;
const client = await pool . connect ( ) ;
2026-04-09 14:38:28 +09:00
try {
const companyCode = req . user ! . companyCode ;
const userId = req . user ! . userId ;
const { id } = req . params ;
const {
outbound_date ,
outbound_qty ,
unit_price ,
total_amount ,
lot_number ,
warehouse_code ,
location_code ,
outbound_status ,
manager_id : mgr ,
memo ,
} = req . body ;
2026-04-20 14:14:24 +09:00
await client . query ( "BEGIN" ) ;
// 변경 전 값 조회
const oldRes = await client . query (
` SELECT * FROM outbound_mng WHERE id = $ 1 AND company_code = $ 2 ` ,
[ id , companyCode ] ,
) ;
if ( oldRes . rowCount === 0 ) {
await client . query ( "ROLLBACK" ) ;
return res
. status ( 404 )
. json ( { success : false , message : "출고 데이터를 찾을 수 없습니다." } ) ;
}
const old = oldRes . rows [ 0 ] ;
const oldQty = Number ( old . outbound_qty ) || 0 ;
const oldWhCode = old . warehouse_code || null ;
const oldLocCode = old . location_code || null ;
const itemCode = old . item_code || old . item_number || null ;
const outboundNumber = old . outbound_number ;
const newQty =
outbound_qty !== undefined && outbound_qty !== null
? Number ( outbound_qty )
: oldQty ;
const newWhCode =
warehouse_code !== undefined ? warehouse_code : oldWhCode ;
const newLocCode =
location_code !== undefined ? location_code : oldLocCode ;
// 재고/이력 반영 (append-only): 수량 또는 창고/위치 변경 시
const qtyChanged = newQty !== oldQty ;
const whChanged =
( newWhCode || "" ) !== ( oldWhCode || "" ) ||
( newLocCode || "" ) !== ( oldLocCode || "" ) ;
if ( itemCode && ( qtyChanged || whChanged ) ) {
if ( whChanged ) {
// 기존 창고 복구
if ( oldQty > 0 ) {
await adjustInventory ( client , {
companyCode ,
userId ,
itemCode ,
whCode : oldWhCode ,
locCode : oldLocCode ,
delta : + oldQty ,
transactionType : "출고취소" ,
remark : ` 출고수정-창고변경 ( ${ outboundNumber } ) ${ oldWhCode || "" } → ${ newWhCode || "" } ` ,
} ) ;
}
// 신규 창고 차감 (재고부족 검증)
if ( newQty > 0 ) {
await adjustInventory ( client , {
companyCode ,
userId ,
itemCode ,
whCode : newWhCode ,
locCode : newLocCode ,
delta : - newQty ,
transactionType : "출고수정" ,
remark : ` 출고수정-창고변경 ( ${ outboundNumber } ) ${ oldWhCode || "" } → ${ newWhCode || "" } , 수량 ${ oldQty } → ${ newQty } ` ,
validateStockEnough : true ,
} ) ;
}
} else {
// 창고 동일, 수량만 변경: 기존 복구(+oldQty) + 신규 차감(-newQty) = delta(+복구/-추가차감)
const delta = oldQty - newQty ;
if ( delta !== 0 ) {
await adjustInventory ( client , {
companyCode ,
userId ,
itemCode ,
whCode : newWhCode ,
locCode : newLocCode ,
delta ,
transactionType : "출고수정" ,
remark : ` 출고수정 ( ${ outboundNumber } ) 수량 ${ oldQty } → ${ newQty } ` ,
validateStockEnough : delta < 0 ,
} ) ;
}
}
}
const result = await client . query (
2026-04-09 14:38:28 +09:00
` UPDATE outbound_mng SET
2026-03-25 10:48:47 +09:00
outbound_date = COALESCE ( $1 , outbound_date ) ,
outbound_qty = COALESCE ( $2 , outbound_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 ) ,
outbound_status = COALESCE ( $8 , outbound_status ) ,
manager_id = COALESCE ( $9 , manager_id ) ,
memo = COALESCE ( $10 , memo ) ,
updated_date = NOW ( ) ,
updated_by = $11
WHERE id = $12 AND company_code = $13
RETURNING * ` ,
2026-04-09 14:38:28 +09:00
[
outbound_date ,
outbound_qty ,
unit_price ,
total_amount ,
lot_number ,
warehouse_code ,
location_code ,
outbound_status ,
mgr ,
memo ,
userId ,
id ,
companyCode ,
] ,
) ;
2026-04-20 14:14:24 +09:00
await client . query ( "COMMIT" ) ;
2026-04-09 14:38:28 +09:00
2026-04-20 14:14:24 +09:00
logger . info ( "출고 수정" , {
companyCode ,
userId ,
id ,
oldQty ,
newQty ,
oldWhCode ,
newWhCode ,
} ) ;
2026-04-09 14:38:28 +09:00
return res . json ( { success : true , data : result.rows [ 0 ] } ) ;
} catch ( error : any ) {
2026-04-20 14:14:24 +09:00
await client . query ( "ROLLBACK" ) ;
2026-04-09 14:38:28 +09:00
logger . error ( "출고 수정 실패" , { error : error.message } ) ;
return res . status ( 500 ) . json ( { success : false , message : error.message } ) ;
2026-04-20 14:14:24 +09:00
} finally {
client . release ( ) ;
2026-04-09 14:38:28 +09:00
}
2026-03-25 10:48:47 +09:00
}
2026-04-20 14:14:24 +09:00
// 출고 삭제 (재고 복구 + '출고취소' 이력 기록 포함)
2026-03-25 10:48:47 +09:00
export async function deleteOutbound ( req : AuthenticatedRequest , res : Response ) {
2026-04-20 14:14:24 +09:00
const pool = getPool ( ) ;
const client = await pool . connect ( ) ;
2026-04-09 14:38:28 +09:00
try {
const companyCode = req . user ! . companyCode ;
2026-04-20 14:14:24 +09:00
const userId = req . user ! . userId ;
2026-04-09 14:38:28 +09:00
const { id } = req . params ;
2026-04-20 14:14:24 +09:00
await client . query ( "BEGIN" ) ;
// 대상 출고 조회
const oldRes = await client . query (
` SELECT * FROM outbound_mng WHERE id = $ 1 AND company_code = $ 2 ` ,
2026-04-09 14:38:28 +09:00
[ id , companyCode ] ,
) ;
2026-04-20 14:14:24 +09:00
if ( oldRes . rowCount === 0 ) {
await client . query ( "ROLLBACK" ) ;
2026-04-09 14:38:28 +09:00
return res
. status ( 404 )
. json ( { success : false , message : "데이터를 찾을 수 없습니다." } ) ;
}
2026-04-20 14:14:24 +09:00
const old = oldRes . rows [ 0 ] ;
const itemCode = old . item_code || old . item_number || null ;
const whCode = old . warehouse_code || null ;
const locCode = old . location_code || null ;
const qty = Number ( old . outbound_qty ) || 0 ;
const outboundNumber = old . outbound_number ;
// 재고 복구 + 이력
if ( itemCode && qty > 0 ) {
await adjustInventory ( client , {
companyCode ,
userId ,
itemCode ,
whCode ,
locCode ,
delta : + qty ,
transactionType : "출고취소" ,
remark : ` 출고 삭제 ( ${ outboundNumber } ) ` ,
} ) ;
} else {
logger . warn ( "출고 삭제 - 재고 복구 스킵" , {
companyCode ,
id ,
itemCode ,
qty ,
} ) ;
}
2026-04-09 14:38:28 +09:00
2026-04-20 14:14:24 +09:00
await client . query (
` DELETE FROM outbound_mng WHERE id = $ 1 AND company_code = $ 2 ` ,
[ id , companyCode ] ,
) ;
await client . query ( "COMMIT" ) ;
logger . info ( "출고 삭제" , { companyCode , userId , id , itemCode , qty } ) ;
2026-04-09 14:38:28 +09:00
return res . json ( { success : true , message : "삭제 완료" } ) ;
} catch ( error : any ) {
2026-04-20 14:14:24 +09:00
await client . query ( "ROLLBACK" ) ;
2026-04-09 14:38:28 +09:00
logger . error ( "출고 삭제 실패" , { error : error.message } ) ;
return res . status ( 500 ) . json ( { success : false , message : error.message } ) ;
2026-04-20 14:14:24 +09:00
} finally {
client . release ( ) ;
2026-04-09 14:38:28 +09:00
}
2026-03-25 10:48:47 +09:00
}
// 판매출고용: 출하지시 데이터 조회
2026-04-09 14:38:28 +09:00
export async function getShipmentInstructions (
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
2026-03-25 10:48:47 +09:00
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 . plan_qty , 0 ) AS plan_qty ,
COALESCE ( sid . ship_qty , 0 ) AS ship_qty ,
COALESCE ( sid . order_qty , 0 ) AS order_qty ,
GREATEST ( COALESCE ( sid . plan_qty , 0 ) - COALESCE ( sid . ship_qty , 0 ) , 0 ) AS remain_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 " ) }
AND COALESCE ( sid . plan_qty , 0 ) > COALESCE ( sid . ship_qty , 0 )
ORDER BY si . instruction_date DESC , si . instruction_no ` ,
2026-04-09 14:38:28 +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 } ) ;
}
2026-03-25 10:48:47 +09:00
}
// 반품출고용: 발주(입고) 데이터 조회
2026-04-09 14:38:28 +09:00
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(received_qty, '') AS numeric), 0) > 0 ` ,
) ;
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
2026-03-25 10:48:47 +09:00
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 ( 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 ` ,
2026-04-09 14:38:28 +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 } ) ;
}
2026-03-25 10:48:47 +09:00
}
// 기타출고용: 품목 데이터 조회
export async function getItems ( req : AuthenticatedRequest , res : Response ) {
2026-04-09 14:38:28 +09:00
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
2026-03-25 10:48:47 +09:00
id , item_number , item_name , size AS spec , material , unit ,
2026-04-15 11:46:05 +09:00
COALESCE ( width : : text , '' ) AS width ,
COALESCE ( height : : text , '' ) AS height ,
COALESCE ( thickness : : text , '' ) AS thickness ,
2026-03-25 10:48:47 +09:00
COALESCE ( CAST ( NULLIF ( standard_price , '' ) AS numeric ) , 0 ) AS standard_price
FROM item_info
WHERE $ { conditions . join ( " AND " ) }
ORDER BY item_name ` ,
2026-04-09 14:38:28 +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 } ) ;
}
2026-03-25 10:48:47 +09:00
}
// 출고번호 자동생성
export async function generateNumber ( req : AuthenticatedRequest , res : Response ) {
2026-04-09 14:38:28 +09:00
try {
const companyCode = req . user ! . companyCode ;
const ruleId =
( req . query . ruleId as string ) || ( req . query . rule_id as string ) ;
// 1순위: POP 화면설정에서 선택한 채번규칙 사용
if ( ruleId && ruleId !== "__none__" ) {
try {
const { numberingRuleService } = await import (
"../services/numberingRuleService"
) ;
const newNumber = await numberingRuleService . allocateCode (
ruleId ,
companyCode ,
) ;
return res . json ( { success : true , data : newNumber } ) ;
} catch ( e : any ) {
logger . warn ( "선택한 채번규칙 사용 실패, 기본 채번으로 폴백" , {
ruleId ,
error : e.message ,
} ) ;
}
}
// 2순위: 기본 하드코딩 채번 (OUT-YYYY-XXXX)
const pool = getPool ( ) ;
const today = new Date ( ) ;
const yyyy = today . getFullYear ( ) ;
const prefix = ` OUT- ${ yyyy } - ` ;
const result = await pool . query (
` SELECT outbound_number FROM outbound_mng
2026-03-25 10:48:47 +09:00
WHERE company_code = $1 AND outbound_number LIKE $2
ORDER BY outbound_number DESC LIMIT 1 ` ,
2026-04-09 14:38:28 +09:00
[ companyCode , ` ${ prefix } % ` ] ,
) ;
let seq = 1 ;
if ( result . rows . length > 0 ) {
const lastNo = result . rows [ 0 ] . outbound_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 } ) ;
}
2026-03-25 10:48:47 +09:00
}
// 창고 목록 조회
export async function getWarehouses ( req : AuthenticatedRequest , res : Response ) {
2026-04-09 14:38:28 +09:00
try {
const companyCode = req . user ! . companyCode ;
const pool = getPool ( ) ;
2026-03-25 10:48:47 +09:00
2026-04-09 14:38:28 +09:00
const result = await pool . query (
` SELECT warehouse_code, warehouse_name, warehouse_type
2026-03-25 10:48:47 +09:00
FROM warehouse_info
2026-04-03 17:38:14 +09:00
WHERE company_code = $1 AND COALESCE ( status , '' ) != '삭제'
2026-03-25 10:48:47 +09:00
ORDER BY warehouse_name ` ,
2026-04-09 14:38:28 +09:00
[ 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 } ) ;
}
2026-03-25 10:48:47 +09:00
}
2026-04-03 17:38:14 +09:00
// 창고별 위치 목록 조회
export async function getLocations ( req : AuthenticatedRequest , res : Response ) {
2026-04-09 14:38:28 +09:00
try {
const companyCode = req . user ! . companyCode ;
const warehouseCode = req . query . warehouse_code as string ;
const pool = getPool ( ) ;
2026-04-03 17:38:14 +09:00
2026-04-09 14:38:28 +09:00
const result = await pool . query (
` SELECT location_code, location_name, warehouse_code
2026-04-03 17:38:14 +09:00
FROM warehouse_location
WHERE company_code = $1 $ { warehouseCode ? "AND warehouse_code = $2" : "" }
ORDER BY location_code ` ,
2026-04-09 14:38:28 +09:00
warehouseCode ? [ companyCode , warehouseCode ] : [ 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 } ) ;
}
2026-04-03 17:38:14 +09:00
}