'use client'; /** * 자재투입 모달 (로트 선택 기반) * * 로트를 체크박스로 선택하면 필요수량만큼 FIFO 순서로 자동 배분합니다. * 같은 품목의 여러 로트를 조합하여 필요수량을 충족시킬 수 있습니다. * * 기능: * - 기투입 LOT 표시 및 수정 (replace 모드) * - 선택완료 배지 * - 필요수량 배정 완료 시에만 투입 가능 * - FIFO 자동입력 버튼 */ import { useState, useEffect, useCallback, useMemo, useRef } from 'react'; import { Loader2, Check, Zap, Info, Search, X } from 'lucide-react'; import { ContentSkeleton } from '@/components/ui/skeleton'; import { Dialog, DialogContent, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { Button } from '@/components/ui/button'; import { cn } from '@/lib/utils'; import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from '@/components/ui/table'; import { toast } from 'sonner'; import { isNextRedirectError } from '@/lib/utils/redirect-error'; import { getMaterialsForWorkOrder, registerMaterialInput, getMaterialsForItem, registerMaterialInputForItem, searchStockByCode, forceCreateReceiving, searchItems, type MaterialForInput, type MaterialForItemInput, type StockSearchResult, type ItemSearchResult } from './actions'; import type { WorkOrder } from '../ProductionDashboard/types'; import type { MaterialInput } from './types'; import { formatNumber } from '@/lib/utils/amount'; interface MaterialInputModalProps { open: boolean; onOpenChange: (open: boolean) => void; order: WorkOrder | null; workOrderItemId?: number; // 개소(작업지시품목) ID (첫 번째 item, 호환용) workOrderItemIds?: number[]; // 개소 내 모든 작업지시품목 IDs (절곡 등 복수 item) workOrderItemName?: string; // 개소명 (모달 헤더 표시용) onComplete?: () => void; isCompletionFlow?: boolean; onSaveMaterials?: (orderId: string, materials: MaterialInput[]) => void; savedMaterials?: MaterialInput[]; } interface MaterialGroup { itemId: number; groupKey: string; // 그룹 식별 키 (itemId 또는 itemId_woItemId) materialName: string; materialCode: string; requiredQty: number; effectiveRequiredQty: number; // 남은 필요수량 (이미 투입분 차감) alreadyInputted: number; // 이미 투입된 수량 unit: string; lots: MaterialForInput[]; // dynamic_bom 추가 정보 workOrderItemId?: number; lotPrefix?: string; partType?: string; category?: string; } const fmtQty = (v: number) => formatNumber(parseFloat(String(v))); export function MaterialInputModal({ open, onOpenChange, order, workOrderItemId, workOrderItemIds, workOrderItemName, onComplete, isCompletionFlow = false, onSaveMaterials, }: MaterialInputModalProps) { const [materials, setMaterials] = useState([]); const [selectedLotKeys, setSelectedLotKeys] = useState>(new Set()); const [manualAllocations, setManualAllocations] = useState>(new Map()); const [isLoading, setIsLoading] = useState(false); const [isSubmitting, setIsSubmitting] = useState(false); const [showUnfulfilledOnly, setShowUnfulfilledOnly] = useState(false); const materialsLoadedRef = useRef(false); // 재공품 여부 판별 — 재공품이면 검색 시 원자재(RM)만 조회 const isWipOrder = useMemo(() => { if (!order) return false; return order.projectName === '재고생산' || order.salesOrderNo?.startsWith('STK'); }, [order]); // 재고 검색 상태 const [searchOpenGroup, setSearchOpenGroup] = useState(null); const [searchQuery, setSearchQuery] = useState(''); const [searchResults, setSearchResults] = useState([]); const [isSearching, setIsSearching] = useState(false); // 품목 검색 결과 (재고 없는 품목 포함) const [itemSearchResults, setItemSearchResults] = useState([]); const handleStockSearch = useCallback(async (query: string | undefined) => { if (!query?.trim()) { setSearchResults([]); setItemSearchResults([]); return; } setIsSearching(true); // 재공품: 원자재(RM)만 검색, 일반: 전체 검색 const typeFilter = isWipOrder ? 'RM' : undefined; const [stockResult, itemResult] = await Promise.all([ searchStockByCode(query.trim(), typeFilter), searchItems(query.trim(), typeFilter), ]); if (stockResult.success) setSearchResults(stockResult.data); if (itemResult.success) { // 재고 검색 결과에 이미 있는 품목은 제외 const stockItemIds = new Set(stockResult.data?.map(s => s.itemId) || []); setItemSearchResults(itemResult.data.filter(i => !stockItemIds.has(i.itemId))); } setIsSearching(false); }, [isWipOrder]); const [isForceCreating, setIsForceCreating] = useState(false); // 목업 자재 데이터 (개발용) 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, })); // 로트 키 생성 (그룹별 독립 — 같은 LOT가 여러 그룹에 있어도 구분) const getLotKey = useCallback((material: MaterialForInput, groupKey: string) => `${String(material.stockLotId ?? `item-${material.itemId}`)}__${groupKey}`, []); // 기투입 LOT 존재 여부 const hasPreInputted = useMemo(() => { return materials.some(m => { const itemInput = m as unknown as MaterialForItemInput; return (itemInput.lotInputtedQty ?? 0) > 0; }); }, [materials]); // 품목별 그룹핑 (BOM 엔트리별 고유키 사용 — 같은 item_id라도 category+partType 다르면 별도 그룹) const materialGroups: MaterialGroup[] = useMemo(() => { const groups = new Map(); for (const m of materials) { const itemInput = m as unknown as MaterialForItemInput; const groupKey = itemInput.bomGroupKey ?? (m.workOrderItemId ? `${m.itemId}_${m.workOrderItemId}` : String(m.itemId)); const existing = groups.get(groupKey) || []; existing.push(m); groups.set(groupKey, existing); } // 작업일지와 동일한 카테고리 순서 const categoryOrder: Record = { guideRail: 0, bottomBar: 1, shutterBox: 2, smokeBarrier: 3, }; 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: first.itemId, groupKey, materialName: first.materialName, materialCode: first.materialCode, requiredQty: first.requiredQty, effectiveRequiredQty, 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, }; }).sort((a, b) => { const catA = categoryOrder[a.category ?? ''] ?? 99; const catB = categoryOrder[b.category ?? ''] ?? 99; return catA - catB; }); }, [materials]); // 그룹별 목표 수량 (기투입 있으면 전체 필요수량, 없으면 남은 필요수량) const getGroupTargetQty = useCallback((group: MaterialGroup) => { return group.alreadyInputted > 0 ? group.requiredQty : group.effectiveRequiredQty; }, []); // 배정 수량 계산 (manual 우선 → 나머지 FIFO 자동배분, 그룹별 독립) const allocations = useMemo(() => { const result = new Map(); for (const group of materialGroups) { const targetQty = getGroupTargetQty(group); let remaining = targetQty; // 1차: manual allocations 적용 for (const lot of group.lots) { const lotKey = getLotKey(lot, group.groupKey); if (selectedLotKeys.has(lotKey) && lot.stockLotId && manualAllocations.has(lotKey)) { const val = manualAllocations.get(lotKey)!; result.set(lotKey, val); remaining -= val; } } // 2차: non-manual 선택 로트 FIFO 자동배분 (그룹 내 독립 계산) for (const lot of group.lots) { const lotKey = getLotKey(lot, group.groupKey); if (selectedLotKeys.has(lotKey) && lot.stockLotId && !manualAllocations.has(lotKey)) { const itemInput = lot as unknown as MaterialForItemInput; const maxAvail = lot.lotAvailableQty + (itemInput.lotInputtedQty ?? 0); const alloc = remaining > 0 ? Math.min(maxAvail, remaining) : 0; result.set(lotKey, alloc); remaining -= alloc; } } } return result; }, [materialGroups, selectedLotKeys, manualAllocations, getLotKey, getGroupTargetQty]); // 전체 배정 완료 여부 const allGroupsFulfilled = useMemo(() => { if (materialGroups.length === 0) return false; return materialGroups.every((group) => { const targetQty = getGroupTargetQty(group); if (targetQty <= 0) return true; const allocated = group.lots.reduce( (sum, lot) => sum + (allocations.get(getLotKey(lot, group.groupKey)) || 0), 0 ); return allocated >= targetQty; }); }, [materialGroups, allocations, getLotKey, getGroupTargetQty]); // 배정된 항목 존재 여부 const hasAnyAllocation = useMemo(() => { return Array.from(allocations.values()).some((v) => v > 0); }, [allocations]); // 로트 선택/해제 const toggleLot = useCallback((lotKey: string) => { setSelectedLotKeys((prev) => { const next = new Set(prev); if (next.has(lotKey)) { next.delete(lotKey); setManualAllocations(prev => { const n = new Map(prev); n.delete(lotKey); return n; }); } else { next.add(lotKey); } return next; }); }, []); // 수량 수동 변경 const handleAllocationChange = useCallback((lotKey: string, value: number, maxAvailable: number) => { const clamped = Math.max(0, Math.min(value, maxAvailable)); setManualAllocations(prev => { const next = new Map(prev); next.set(lotKey, clamped); return next; }); }, []); // FIFO 자동입력 (그룹별 독립 배정 — 각 그룹의 LOT 가용량 독립 계산) const handleAutoFill = useCallback(() => { const newSelected = new Set(); const newAllocations = new Map(); for (const group of materialGroups) { const targetQty = getGroupTargetQty(group); if (targetQty <= 0) continue; let remaining = targetQty; for (const lot of group.lots) { if (!lot.stockLotId || remaining <= 0) continue; const lotKey = getLotKey(lot, group.groupKey); const itemInput = lot as unknown as MaterialForItemInput; // 해당 그룹에서의 LOT 가용량 = 현재 가용 + 이 그룹에서 기투입된 수량 const maxAvail = lot.lotAvailableQty + (itemInput.lotInputtedQty ?? 0); const alloc = Math.min(maxAvail, remaining); if (alloc > 0) { newSelected.add(lotKey); newAllocations.set(lotKey, alloc); remaining -= alloc; } } } setSelectedLotKeys(newSelected); setManualAllocations(newAllocations); }, [materialGroups, getLotKey, getGroupTargetQty]); // API로 자재 목록 로드 const loadMaterials = useCallback(async () => { if (!order) return; setIsLoading(true); materialsLoadedRef.current = false; try { // 목업 아이템인 경우 목업 자재 데이터 사용 if (order.id.startsWith('mock-')) { setMaterials(MOCK_MATERIALS); setIsLoading(false); materialsLoadedRef.current = true; return; } // 개소 대표 아이템 1개만 조회 (dynamic_bom은 개소 내 모든 아이템에 동일하게 저장됨) const itemId = workOrderItemId ?? (workOrderItemIds && workOrderItemIds.length > 0 ? workOrderItemIds[0] : null); if (itemId) { const result = await getMaterialsForItem(order.id, itemId); if (result.success) { const tagged = result.data.map((m) => ({ ...m, workOrderItemId: m.workOrderItemId || itemId, })); setMaterials(tagged); materialsLoadedRef.current = true; } else { toast.error(result.error || '자재 목록 조회에 실패했습니다.'); } } else { // 전체 작업지시 기준 조회 const result = await getMaterialsForWorkOrder(order.id); if (result.success) { setMaterials(result.data); materialsLoadedRef.current = true; } else { toast.error(result.error || '자재 목록 조회에 실패했습니다.'); } } } catch (error) { if (isNextRedirectError(error)) throw error; console.error('[MaterialInputModal] loadMaterials error:', error); toast.error('자재 목록 로드 중 오류가 발생했습니다.'); } finally { setIsLoading(false); } }, [order, workOrderItemId, workOrderItemIds]); // [개발전용] 입고 강제생성 핸들러 const handleForceReceiving = useCallback(async (itemId: number, itemCode: string) => { setIsForceCreating(true); const result = await forceCreateReceiving(itemId, 100); if (result.success && result.data) { const d = result.data as Record; const rm = d.rm_item_code || d.rm_item_name; const pt = d.pt_item_code || d.item_code; const label = rm ? `원자재 ${rm} → 재공품 ${pt}` : String(pt); toast.success(`${label} 입고 완료 (LOT: ${d.lot_no}, ${d.qty}EA)`); handleStockSearch(itemCode); loadMaterials(); } else { toast.error(result.error || '입고 생성 실패'); } setIsForceCreating(false); }, [handleStockSearch, loadMaterials]); // 모달이 열릴 때 데이터 로드 + 선택 초기화 useEffect(() => { if (open && order) { loadMaterials(); setSelectedLotKeys(new Set()); setManualAllocations(new Map()); } }, [open, order, loadMaterials]); // 자재 로드 후 기투입 LOT 자동 선택 (그룹별 독립 처리) useEffect(() => { if (!materialsLoadedRef.current || materials.length === 0 || materialGroups.length === 0) return; const preSelected = new Set(); const preAllocations = new Map(); for (const group of materialGroups) { for (const m of group.lots) { const itemInput = m as unknown as MaterialForItemInput; const lotInputted = itemInput.lotInputtedQty ?? 0; if (lotInputted > 0 && m.stockLotId) { const lotKey = getLotKey(m, group.groupKey); preSelected.add(lotKey); preAllocations.set(lotKey, lotInputted); } } } if (preSelected.size > 0) { setSelectedLotKeys(prev => new Set([...prev, ...preSelected])); setManualAllocations(prev => new Map([...prev, ...preAllocations])); } // 한 번만 실행하도록 ref 초기화 materialsLoadedRef.current = false; }, [materials, materialGroups, getLotKey]); // 투입 등록 const handleSubmit = async () => { if (!order) return; // 배분된 로트를 그룹별 개별 엔트리로 추출 (bom_group_key 포함) const inputs: { stock_lot_id: number; qty: number; bom_group_key: string }[] = []; for (const group of materialGroups) { for (const lot of group.lots) { const lotKey = getLotKey(lot, group.groupKey); const allocQty = allocations.get(lotKey) || 0; if (allocQty > 0 && lot.stockLotId) { inputs.push({ stock_lot_id: lot.stockLotId, qty: allocQty, bom_group_key: group.groupKey, }); } } } if (inputs.length === 0) { toast.error('투입할 로트를 선택해주세요.'); return; } setIsSubmitting(true); try { // 대표 아이템 기준 자재 투입 등록 let result: { success: boolean; error?: string }; const targetItemId = workOrderItemId ?? (workOrderItemIds && workOrderItemIds.length > 0 ? workOrderItemIds[0] : null); if (targetItemId) { // 기투입 LOT 있으면 replace 모드 (기존 투입 삭제 후 재등록) result = await registerMaterialInputForItem(order.id, targetItemId, inputs, hasPreInputted); } else { result = await registerMaterialInput(order.id, inputs); } if (result.success) { toast.success('자재 투입이 등록되었습니다.'); if (onSaveMaterials) { // 표시용: 같은 LOT는 합산 (자재투입목록 UI) const lotTotals = new Map(); for (const inp of inputs) { lotTotals.set(inp.stock_lot_id, (lotTotals.get(inp.stock_lot_id) || 0) + inp.qty); } const savedList: MaterialInput[] = []; const processedLotIds = new Set(); for (const group of materialGroups) { for (const lot of group.lots) { if (!lot.stockLotId || processedLotIds.has(lot.stockLotId)) continue; const totalQty = lotTotals.get(lot.stockLotId) || 0; if (totalQty > 0) { processedLotIds.add(lot.stockLotId); savedList.push({ id: String(lot.stockLotId), lotNo: lot.lotNo || '', materialName: lot.materialName, quantity: lot.lotAvailableQty, unit: lot.unit, inputQuantity: totalQty, }); } } } onSaveMaterials(order.id, savedList); } resetAndClose(); if (isCompletionFlow && onComplete) { onComplete(); } } else { toast.error(result.error || '자재 투입 등록에 실패했습니다.'); } } catch (error) { if (isNextRedirectError(error)) throw error; console.error('[MaterialInputModal] handleSubmit error:', error); toast.error('자재 투입 등록 중 오류가 발생했습니다.'); } finally { setIsSubmitting(false); } }; const resetAndClose = () => { setSelectedLotKeys(new Set()); setManualAllocations(new Map()); onOpenChange(false); }; if (!order) return null; return ( {/* 헤더 */} 자재 투입{workOrderItemName ? ` - ${workOrderItemName}` : ''}

로트를 선택하면 필요수량만큼 자동 배분됩니다.

{!isLoading && materialGroups.length > 0 && (() => { const fulfilled = materialGroups.filter((g) => { const target = getGroupTargetQty(g); if (target <= 0 && g.alreadyInputted <= 0) return true; const allocated = g.lots.reduce((sum, lot) => sum + (allocations.get(getLotKey(lot, g.groupKey)) || 0), 0); return allocated >= target; }).length; const total = materialGroups.length; return ( ); })()}
{!isLoading && materials.length > 0 && ( )}
{/* 자재 목록 */} {isLoading ? ( ) : materials.length === 0 ? (
이 공정에 배정된 자재가 없습니다.
{/* 자재 없을 때도 재고 검색 + 강제입고 가능 */}
setSearchQuery(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleStockSearch(searchQuery)} placeholder={isWipOrder ? "원자재 코드 또는 자재명으로 검색" : "품목코드 또는 자재명으로 재고 검색"} className="flex-1 text-xs px-2 py-1.5 border rounded bg-white" />
{searchResults.length > 0 ? (

{searchResults.length}건 검색됨:

{searchResults.map((s, si) => (
0 ? "bg-white border-emerald-200" : "bg-gray-50 border-gray-200" )}>
{s.itemCode} 0 ? "text-emerald-600 font-semibold" : "text-gray-400"}> 가용 {formatNumber(s.availableQty)} EA
{s.itemName}
{s.lots.length > 0 && s.lots.slice(0, 3).map((l, li) => (
LOT {l.lotNo} | {formatNumber(l.availableQty)} EA | FIFO #{l.fifoOrder}
))}
))}
) : isSearching ? (

검색 중...

) : searchQuery && searchResults.length === 0 && itemSearchResults.length === 0 ? (

검색 결과 없음

) : null} {/* 재고 없는 품목 목록 (items 테이블에만 존재) */} {itemSearchResults.length > 0 && (

재고 없는 품목 {itemSearchResults.length}건:

{itemSearchResults.map((item, idx) => (
{item.itemCode} {item.itemName} ({item.itemType})
))}
)}
) : (
{materialGroups.filter((group) => { if (!showUnfulfilledOnly) return true; const target = getGroupTargetQty(group); if (target <= 0 && group.alreadyInputted <= 0) return false; const allocated = group.lots.reduce((sum, lot) => sum + (allocations.get(getLotKey(lot, group.groupKey)) || 0), 0); return allocated < target; }).map((group, groupIdx) => { // 같은 카테고리 내 순번 계산 (①②③...) const categoryIndex = group.category ? materialGroups.slice(0, groupIdx).filter(g => g.category === group.category).length : -1; const circledNumbers = ['①','②','③','④','⑤','⑥','⑦','⑧','⑨','⑩']; const circledNum = categoryIndex >= 0 && categoryIndex < circledNumbers.length ? circledNumbers[categoryIndex] : ''; const targetQty = getGroupTargetQty(group); const groupAllocated = group.lots.reduce( (sum, lot) => sum + (allocations.get(getLotKey(lot, group.groupKey)) || 0), 0 ); const isGroupComplete = targetQty <= 0 && group.alreadyInputted <= 0; const isFulfilled = isGroupComplete || groupAllocated >= targetQty; return (
{/* 품목 그룹 헤더 */}
{group.category && ( {group.category === 'guideRail' ? '가이드레일' : group.category === 'bottomBar' ? '하단마감재' : group.category === 'shutterBox' ? '셔터박스' : group.category === 'smokeBarrier' ? '연기차단재' : group.category} )} {group.partType && ( {circledNum}{group.partType} )} {group.materialName} {group.materialCode && ( {group.materialCode} )} {/* 매칭 정보 말풍선 */} l.stockLotId !== null) ? "text-blue-400" : "text-red-400" )} /> 자재 매칭 정보 품목코드: {group.materialCode || '-'}
규격: {group.lots[0]?.specification || '-'}
매칭기준: 품목코드 + 규격 일치하는 입고 LOT 검색 (FIFO)
{(() => { const availableLots = group.lots.filter(l => l.stockLotId !== null); const noStockLots = group.lots.filter(l => l.stockLotId === null); if (availableLots.length > 0) { return ( ✓ {availableLots.length}건 매칭 완료 {availableLots.map((l, i) => ( LOT {l.lotNo} | 가용 {formatNumber(l.lotAvailableQty)}{group.unit} | FIFO #{l.fifoRank} ))} ); } return ( ✗ 매칭 가능한 입고 LOT 없음 → [{group.materialCode}] {group.lots[0]?.specification || ''} 자재를 입고해주세요 ); })()}
{group.alreadyInputted > 0 ? ( <> 필요:{' '} {fmtQty(group.requiredQty)} {' '} {group.unit} (기투입: {fmtQty(group.alreadyInputted)}) ) : ( <> 필요:{' '} {fmtQty(group.requiredQty)} {' '} {group.unit} )} 0 ? 'bg-amber-100 text-amber-700' : 'bg-gray-100 text-gray-500' }`} > {isFulfilled ? ( <> 배정 완료 ) : ( `${fmtQty(groupAllocated)} / ${fmtQty(targetQty)}` )}
{/* 로트 테이블 */}
선택 로트번호 가용수량 단위 배정수량 {group.lots.map((lot, idx) => { const lotKey = getLotKey(lot, group.groupKey); const hasStock = lot.stockLotId !== null; const isSelected = selectedLotKeys.has(lotKey); const allocated = allocations.get(lotKey) || 0; const itemInput = lot as unknown as MaterialForItemInput; const lotInputted = itemInput.lotInputtedQty ?? 0; const isPreInputted = lotInputted > 0; // 가용수량 = 현재 가용 + 기투입분 (replace 시 복원되므로) const effectiveAvailable = lot.lotAvailableQty + lotInputted; const canSelect = hasStock && (!isFulfilled || isSelected); return ( 0 ? 'bg-blue-50/50' : '', isPreInputted && isSelected ? 'bg-blue-50/70' : '' )} > {hasStock ? ( ) : null}
{lot.lotNo || ( 재고 없음 )} {isPreInputted && ( 기투입 )}
{hasStock ? ( isPreInputted ? ( {fmtQty(lot.lotAvailableQty)} (+{fmtQty(lotInputted)}) ) : ( fmtQty(lot.lotAvailableQty) ) ) : ( 0 )} {lot.unit} {isSelected && hasStock ? ( { const val = parseFloat(e.target.value) || 0; handleAllocationChange(lotKey, val, effectiveAvailable); }} className="w-20 text-center text-blue-600 font-semibold border border-blue-200 rounded px-2 py-1 focus:outline-none focus:ring-1 focus:ring-blue-400 [appearance:textfield] [&::-webkit-outer-spin-button]:appearance-none [&::-webkit-inner-spin-button]:appearance-none" min={0} max={effectiveAvailable} /> ) : allocated > 0 ? ( {fmtQty(allocated)} ) : ( - )}
); })}
{/* 재고 검색 패널 */} {!group.lots.some(l => l.stockLotId !== null) && searchOpenGroup !== group.groupKey && ( )} {searchOpenGroup === group.groupKey && (
setSearchQuery(e.target.value)} onKeyDown={(e) => e.key === 'Enter' && handleStockSearch(searchQuery)} placeholder={isWipOrder ? "원자재 코드 또는 자재명 검색" : "품목코드 또는 자재명 검색"} className="flex-1 text-xs px-2 py-1.5 border rounded bg-white" />
{searchResults.length > 0 ? (

{searchResults.length}건 검색됨:

{searchResults.map((s, si) => (
0 ? "bg-white border-emerald-200" : "bg-gray-50 border-gray-200" )}>
{s.itemCode} 0 ? "text-emerald-600 font-semibold" : "text-gray-400"}> 가용 {formatNumber(s.availableQty)} EA
{s.itemName}
{s.lots.length > 0 && (
{s.lots.slice(0, 3).map((l, li) => (
LOT {l.lotNo} | {formatNumber(l.availableQty)} EA | FIFO #{l.fifoOrder}
))}
)}
))}
) : isSearching ? (

검색 중...

) : searchQuery && (

검색 결과 없음 — 해당 품목의 입고 처리가 필요합니다

)} {/* [개발전용] 입고 강제생성 버튼 */} {group.lots[0]?.itemId && ( )}
)}
); })}
)} {/* 버튼 영역 */}
); }