fix: [자재투입] 입고 로트번호 표시 및 로트별 수량 투입으로 변경

- MaterialForInput 타입: stockLotId, lotNo, lotAvailableQty 추가
- 로트번호 컬럼에 실제 입고 로트번호(lot_no) 표시
- 수량 컬럼을 가용수량(lotAvailableQty)으로 변경
- 가용수량 초과 검증 추가
- registerMaterialInput: stock_lot_id+qty 로트별 투입 방식으로 변경
This commit is contained in:
2026-02-07 05:06:34 +09:00
parent a523bb482e
commit 1fca5ed477
2 changed files with 110 additions and 67 deletions

View File

@@ -1,10 +1,10 @@
'use client';
/**
* 자재투입 모달 (기획서 기반)
* 자재투입 모달 (로트 기반)
*
* 기획서 변경: BOM 체크박스 → 투입수량 입력 테이블
* 컬럼: 로트번호 | 품목명 | 수량 | 단위 | 투입 수량 (input, 숫자만)
* 입고관리에서 생성된 실제 로트번호 기준으로 자재를 표시합니다.
* 컬럼: 로트번호 | 품목명 | 가용수량 | 단위 | 투입 수량 (input, 숫자만)
* 하단: 취소 / 투입
*/
@@ -56,13 +56,17 @@ export function MaterialInputModal({
const [isLoading, setIsLoading] = useState(false);
const [isSubmitting, setIsSubmitting] = useState(false);
// 목업 자재 데이터 (기획서 기반 10행)
const MOCK_MATERIALS: MaterialForInput[] = Array.from({ length: 10 }, (_, i) => ({
id: 100 + i,
materialCode: '123123',
materialName: '품목명',
unit: 'm',
currentStock: 500,
// 목업 자재 데이터 (개발용)
const MOCK_MATERIALS: MaterialForInput[] = Array.from({ length: 5 }, (_, i) => ({
stockLotId: 100 + i,
itemId: 200 + i,
lotNo: `LOT-2026-${String(i + 1).padStart(3, '0')}`,
materialCode: `MAT-${String(i + 1).padStart(3, '0')}`,
materialName: `자재 ${i + 1}`,
specification: '',
unit: 'EA',
requiredQty: 100,
lotAvailableQty: 500 - i * 50,
fifoRank: i + 1,
}));
@@ -77,7 +81,7 @@ export function MaterialInputModal({
setMaterials(MOCK_MATERIALS);
const initialQuantities: Record<string, string> = {};
MOCK_MATERIALS.forEach((m) => {
initialQuantities[String(m.id)] = '';
initialQuantities[String(m.stockLotId)] = '';
});
setInputQuantities(initialQuantities);
setIsLoading(false);
@@ -87,10 +91,9 @@ export function MaterialInputModal({
const result = await getMaterialsForWorkOrder(order.id);
if (result.success) {
setMaterials(result.data);
// 초기 투입 수량 비우기
const initialQuantities: Record<string, string> = {};
result.data.forEach((m) => {
initialQuantities[String(m.id)] = '';
initialQuantities[String(m.stockLotId ?? `item-${m.itemId}`)] = '';
});
setInputQuantities(initialQuantities);
} else {
@@ -113,22 +116,26 @@ export function MaterialInputModal({
}, [open, order, loadMaterials]);
// 투입 수량 변경 핸들러 (숫자만 허용)
const handleQuantityChange = (materialId: string, value: string) => {
// 숫자만 허용
const handleQuantityChange = (key: string, value: string) => {
const numericValue = value.replace(/[^0-9]/g, '');
setInputQuantities((prev) => ({
...prev,
[materialId]: numericValue,
[key]: numericValue,
}));
};
// 로트 키 생성
const getLotKey = (material: MaterialForInput) =>
String(material.stockLotId ?? `item-${material.itemId}`);
// 투입 등록
const handleSubmit = async () => {
if (!order) return;
// 투입 수량이 입력된 항목 필터
// 투입 수량이 입력된 항목 필터 (재고 있는 로트만)
const materialsWithQuantity = materials.filter((m) => {
const qty = inputQuantities[String(m.id)];
if (!m.stockLotId) return false;
const qty = inputQuantities[getLotKey(m)];
return qty && parseInt(qty) > 0;
});
@@ -137,23 +144,35 @@ export function MaterialInputModal({
return;
}
// 가용수량 초과 검증
for (const m of materialsWithQuantity) {
const inputQty = parseInt(inputQuantities[getLotKey(m)] || '0');
if (inputQty > m.lotAvailableQty) {
toast.error(`${m.lotNo}: 가용수량(${m.lotAvailableQty})을 초과할 수 없습니다.`);
return;
}
}
setIsSubmitting(true);
try {
const materialIds = materialsWithQuantity.map((m) => m.id);
const result = await registerMaterialInput(order.id, materialIds);
const inputs = materialsWithQuantity.map((m) => ({
stock_lot_id: m.stockLotId!,
qty: parseInt(inputQuantities[getLotKey(m)] || '0'),
}));
const result = await registerMaterialInput(order.id, inputs);
if (result.success) {
toast.success('자재 투입이 등록되었습니다.');
// onSaveMaterials 콜백 호출
if (onSaveMaterials) {
const savedList: MaterialInput[] = materialsWithQuantity.map((m) => ({
id: String(m.id),
lotNo: '', // API에서 가져올 필드
id: String(m.stockLotId),
lotNo: m.lotNo || '',
materialName: m.materialName,
quantity: m.currentStock,
quantity: m.lotAvailableQty,
unit: m.unit,
inputQuantity: parseInt(inputQuantities[String(m.id)] || '0'),
inputQuantity: parseInt(inputQuantities[getLotKey(m)] || '0'),
}));
onSaveMaterials(order.id, savedList);
}
@@ -205,7 +224,7 @@ export function MaterialInputModal({
<TableRow className="bg-gray-50">
<TableHead className="text-center"></TableHead>
<TableHead className="text-center"></TableHead>
<TableHead className="text-center"></TableHead>
<TableHead className="text-center"></TableHead>
<TableHead className="text-center"></TableHead>
<TableHead className="text-center"> </TableHead>
</TableRow>
@@ -226,40 +245,52 @@ export function MaterialInputModal({
<TableRow className="bg-gray-50">
<TableHead className="text-center font-medium"></TableHead>
<TableHead className="text-center font-medium"></TableHead>
<TableHead className="text-center font-medium"></TableHead>
<TableHead className="text-center font-medium"></TableHead>
<TableHead className="text-center font-medium"></TableHead>
<TableHead className="text-center font-medium"> </TableHead>
</TableRow>
</TableHeader>
<TableBody>
{materials.map((material, index) => (
<TableRow key={`mat-${material.id}-${index}`}>
<TableCell className="text-center text-sm">
{material.materialCode}
</TableCell>
<TableCell className="text-center text-sm">
{material.materialName}
</TableCell>
<TableCell className="text-center text-sm">
{material.currentStock.toLocaleString()}
</TableCell>
<TableCell className="text-center text-sm">
{material.unit}
</TableCell>
<TableCell className="text-center">
<Input
type="text"
inputMode="numeric"
placeholder="0"
value={inputQuantities[String(material.id)] || ''}
onChange={(e) =>
handleQuantityChange(String(material.id), e.target.value)
}
className="w-20 mx-auto text-center h-8 text-sm"
/>
</TableCell>
</TableRow>
))}
{materials.map((material, index) => {
const lotKey = getLotKey(material);
const hasStock = material.stockLotId !== null;
return (
<TableRow key={`mat-${lotKey}-${index}`}>
<TableCell className="text-center text-sm">
{material.lotNo || (
<span className="text-gray-400"> </span>
)}
</TableCell>
<TableCell className="text-center text-sm">
{material.materialName}
</TableCell>
<TableCell className="text-center text-sm">
{hasStock ? material.lotAvailableQty.toLocaleString() : (
<span className="text-red-500">0</span>
)}
</TableCell>
<TableCell className="text-center text-sm">
{material.unit}
</TableCell>
<TableCell className="text-center">
{hasStock ? (
<Input
type="text"
inputMode="numeric"
placeholder="0"
value={inputQuantities[lotKey] || ''}
onChange={(e) =>
handleQuantityChange(lotKey, e.target.value)
}
className="w-20 mx-auto text-center h-8 text-sm"
/>
) : (
<span className="text-gray-400 text-sm">-</span>
)}
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
@@ -294,4 +325,4 @@ export function MaterialInputModal({
</DialogContent>
</Dialog>
);
}
}