feat(WEB): 절곡 자재투입 LOT 매핑 프론트엔드 연동

- actions.ts: MaterialForInput에 workOrderItemId/lotPrefix/partType/category 필드 추가
- MaterialInputModal: dynamic_bom 세부품목 단위 그룹핑 + category 배지 표시
- 작업일지 4개 섹션 lotNoMap prop 추가 (GuideRail/BottomBar/ShutterBox/SmokeBarrier)
- WorkLogModal: materialLots에서 BD-* 필터링 → lotNoMap 빌드 후 전달
- utils.ts: lengthToCode() 래퍼 함수 추가
This commit is contained in:
2026-02-22 02:13:13 +09:00
parent e5b706249a
commit a19263334e
9 changed files with 109 additions and 27 deletions

View File

@@ -47,6 +47,7 @@ interface MaterialInputModalProps {
interface MaterialGroup {
itemId: number;
groupKey: string; // 그룹 식별 키 (itemId 또는 itemId_woItemId)
materialName: string;
materialCode: string;
requiredQty: number;
@@ -54,6 +55,11 @@ interface MaterialGroup {
alreadyInputted: number; // 이미 투입된 수량
unit: string;
lots: MaterialForInput[];
// dynamic_bom 추가 정보
workOrderItemId?: number;
lotPrefix?: string;
partType?: string;
category?: string;
}
const fmtQty = (v: number) => formatNumber(parseFloat(String(v)));
@@ -93,19 +99,22 @@ export function MaterialInputModal({
// 품목별 그룹핑
const materialGroups: MaterialGroup[] = useMemo(() => {
const groups = new Map<number, MaterialForInput[]>();
// dynamic_bom 항목은 (itemId, workOrderItemId) 쌍으로 그룹핑
const groups = new Map<string, MaterialForInput[]>();
for (const m of materials) {
const existing = groups.get(m.itemId) || [];
const groupKey = m.workOrderItemId ? `${m.itemId}_${m.workOrderItemId}` : String(m.itemId);
const existing = groups.get(groupKey) || [];
existing.push(m);
groups.set(m.itemId, existing);
groups.set(groupKey, existing);
}
return Array.from(groups.entries()).map(([itemId, lots]) => {
return Array.from(groups.entries()).map(([groupKey, lots]) => {
const first = lots[0];
const itemInput = first as unknown as MaterialForItemInput;
const alreadyInputted = itemInput.alreadyInputted ?? 0;
const effectiveRequiredQty = Math.max(0, itemInput.remainingRequiredQty ?? first.requiredQty);
return {
itemId,
itemId: first.itemId,
groupKey,
materialName: first.materialName,
materialCode: first.materialCode,
requiredQty: first.requiredQty,
@@ -113,6 +122,10 @@ export function MaterialInputModal({
alreadyInputted,
unit: first.unit,
lots: lots.sort((a, b) => a.fifoRank - b.fifoRank),
workOrderItemId: first.workOrderItemId,
lotPrefix: first.lotPrefix,
partType: first.partType,
category: first.category,
};
});
}, [materials]);
@@ -208,13 +221,20 @@ export function MaterialInputModal({
const handleSubmit = async () => {
if (!order) return;
// 배분된 로트만 추출
const inputs: { stock_lot_id: number; qty: number }[] = [];
// 배분된 로트만 추출 (dynamic_bom이면 work_order_item_id 포함)
const inputs: { stock_lot_id: number; qty: number; work_order_item_id?: number }[] = [];
for (const [lotKey, allocQty] of allocations) {
if (allocQty > 0) {
const material = materials.find((m) => getLotKey(m) === lotKey);
if (material?.stockLotId) {
inputs.push({ stock_lot_id: material.stockLotId, qty: allocQty });
const input: { stock_lot_id: number; qty: number; work_order_item_id?: number } = {
stock_lot_id: material.stockLotId,
qty: allocQty,
};
if (material.workOrderItemId) {
input.work_order_item_id = material.workOrderItemId;
}
inputs.push(input);
}
}
}
@@ -310,10 +330,25 @@ export function MaterialInputModal({
const isFulfilled = isAlreadyComplete || groupAllocated >= group.effectiveRequiredQty;
return (
<div key={group.itemId} className="border rounded-lg overflow-hidden">
<div key={group.groupKey} className="border rounded-lg overflow-hidden">
{/* 품목 그룹 헤더 */}
<div className="flex items-center justify-between px-4 py-2.5 bg-gray-50 border-b">
<div className="flex items-center gap-2">
{group.category && (
<span className={`text-[10px] px-1.5 py-0.5 rounded font-medium ${
group.category === 'guideRail' ? 'bg-blue-100 text-blue-700' :
group.category === 'bottomBar' ? 'bg-green-100 text-green-700' :
group.category === 'shutterBox' ? 'bg-orange-100 text-orange-700' :
group.category === 'smokeBarrier' ? 'bg-red-100 text-red-700' :
'bg-gray-100 text-gray-600'
}`}>
{group.category === 'guideRail' ? '가이드레일' :
group.category === 'bottomBar' ? '하단마감재' :
group.category === 'shutterBox' ? '셔터박스' :
group.category === 'smokeBarrier' ? '연기차단재' :
group.category}
</span>
)}
<span className="text-sm font-semibold text-gray-900">
{group.materialName}
</span>