feat: 생산/품질/자재/출고/주문 관리 페이지 구현

- 생산관리: 대시보드, 작업지시, 작업실적, 작업자화면
- 품질관리: 검사관리 (리스트/등록/상세)
- 자재관리: 입고관리, 재고현황
- 출고관리: 출하관리 (리스트/등록/상세/수정)
- 주문관리: 수주관리, 생산의뢰
- 기존 컴포넌트 개선: CardTransactionInquiry, VendorDetail, QuoteRegistration
- IntegratedListTemplateV2 개선
- 공통 컴포넌트 분석 문서 추가

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
byeongcheolryu
2025-12-23 21:13:07 +09:00
parent 2ebcea0255
commit f0e8e51d06
108 changed files with 21156 additions and 84 deletions

View File

@@ -0,0 +1,323 @@
'use client';
/**
* 수입검사 등록 (IQC) 페이지
* - 검사 대상 선택
* - 검사 정보 입력 (검사일, 검사자*, LOT번호)
* - 검사 항목 테이블 (겉모양, 두께, 폭, 길이)
* - 종합 의견
*/
import { useState, useCallback, useMemo } from 'react';
import { useRouter } from 'next/navigation';
import { ClipboardCheck, Calendar } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import { PageLayout } from '@/components/organisms/PageLayout';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { mockInspectionTargets, defaultInspectionItems, generateLotNo } from './mockData';
import type { InspectionCheckItem } from './types';
import { SuccessDialog } from './SuccessDialog';
interface Props {
id?: string; // 특정 발주건으로 바로 진입하는 경우
}
export function InspectionCreate({ id }: Props) {
const router = useRouter();
// 선택된 검사 대상
const [selectedTargetId, setSelectedTargetId] = useState<string>(
id || mockInspectionTargets[2]?.id || ''
);
// 검사 정보
const [inspectionDate, setInspectionDate] = useState(() => {
const today = new Date();
return today.toISOString().split('T')[0];
});
const [inspector, setInspector] = useState('');
const [lotNo, setLotNo] = useState(() => generateLotNo());
// 검사 항목
const [inspectionItems, setInspectionItems] = useState<InspectionCheckItem[]>(
defaultInspectionItems.map((item) => ({ ...item }))
);
// 종합 의견
const [opinion, setOpinion] = useState('');
// 유효성 검사 에러
const [validationErrors, setValidationErrors] = useState<string[]>([]);
// 성공 다이얼로그
const [showSuccess, setShowSuccess] = useState(false);
// 선택된 대상 정보
const selectedTarget = useMemo(() => {
return mockInspectionTargets.find((t) => t.id === selectedTargetId);
}, [selectedTargetId]);
// 대상 선택 핸들러
const handleTargetSelect = useCallback((targetId: string) => {
setSelectedTargetId(targetId);
setValidationErrors([]);
}, []);
// 판정 변경 핸들러
const handleJudgmentChange = useCallback((itemId: string, judgment: '적' | '부적') => {
setInspectionItems((prev) =>
prev.map((item) => (item.id === itemId ? { ...item, judgment } : item))
);
setValidationErrors([]);
}, []);
// 비고 변경 핸들러
const handleRemarkChange = useCallback((itemId: string, remark: string) => {
setInspectionItems((prev) =>
prev.map((item) => (item.id === itemId ? { ...item, remark } : item))
);
}, []);
// 유효성 검사
const validateForm = useCallback((): boolean => {
const errors: string[] = [];
// 필수 필드: 검사자
if (!inspector.trim()) {
errors.push('검사자는 필수 입력 항목입니다.');
}
// 검사 항목 판정 확인
inspectionItems.forEach((item, index) => {
if (!item.judgment) {
errors.push(`${index + 1}. ${item.name}: 판정을 선택해주세요.`);
}
});
setValidationErrors(errors);
return errors.length === 0;
}, [inspector, inspectionItems]);
// 검사 저장
const handleSubmit = useCallback(() => {
if (!validateForm()) {
return;
}
// TODO: API 호출
console.log('검사 저장:', {
targetId: selectedTargetId,
inspectionDate,
inspector,
lotNo,
items: inspectionItems,
opinion,
});
setShowSuccess(true);
}, [validateForm, selectedTargetId, inspectionDate, inspector, lotNo, inspectionItems, opinion]);
// 취소 - 목록으로
const handleCancel = useCallback(() => {
router.push('/ko/material/receiving-management');
}, [router]);
// 성공 다이얼로그 닫기
const handleSuccessClose = useCallback(() => {
setShowSuccess(false);
router.push('/ko/material/receiving-management');
}, [router]);
return (
<PageLayout>
<div className="space-y-6">
{/* 헤더 */}
<div className="flex items-center justify-between">
<div className="flex items-center gap-3">
<ClipboardCheck className="w-6 h-6" />
<h1 className="text-xl font-semibold"> (IQC)</h1>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" onClick={handleCancel}>
</Button>
<Button onClick={handleSubmit}>
<ClipboardCheck className="w-4 h-4 mr-1.5" />
</Button>
</div>
</div>
<div className="grid grid-cols-1 lg:grid-cols-4 gap-6">
{/* 좌측: 검사 대상 선택 */}
<div className="lg:col-span-1 space-y-2">
<Label className="text-sm font-medium"> </Label>
<div className="space-y-2 border rounded-lg p-2 bg-white">
{mockInspectionTargets.map((target) => (
<div
key={target.id}
onClick={() => handleTargetSelect(target.id)}
className={`p-3 rounded-lg cursor-pointer border transition-colors ${
selectedTargetId === target.id
? 'bg-blue-50 border-blue-300'
: 'bg-white border-gray-200 hover:bg-gray-50'
}`}
>
<p className="font-medium text-sm">{target.orderNo}</p>
<p className="text-xs text-muted-foreground">
{target.supplier} · {target.qty}
</p>
</div>
))}
</div>
</div>
{/* 우측: 검사 정보 및 항목 */}
<div className="lg:col-span-3 space-y-6">
{/* Validation 에러 표시 */}
{validationErrors.length > 0 && (
<Alert className="bg-red-50 border-red-200">
<AlertDescription className="text-red-900">
<div className="flex items-start gap-2">
<span className="text-lg"></span>
<div className="flex-1">
<strong className="block mb-2">
({validationErrors.length} )
</strong>
<ul className="space-y-1 text-sm">
{validationErrors.map((error, index) => (
<li key={index} className="flex items-start gap-1">
<span></span>
<span>{error}</span>
</li>
))}
</ul>
</div>
</div>
</AlertDescription>
</Alert>
)}
{/* 검사 정보 */}
<div className="space-y-4 bg-white p-4 rounded-lg border">
<h3 className="font-medium"> </h3>
<div className="grid grid-cols-1 md:grid-cols-3 gap-4">
<div className="space-y-2">
<Label className="text-sm text-muted-foreground"></Label>
<div className="relative">
<Input
type="date"
value={inspectionDate}
onChange={(e) => setInspectionDate(e.target.value)}
className="pr-10"
/>
<Calendar className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground pointer-events-none" />
</div>
</div>
<div className="space-y-2">
<Label className="text-sm text-muted-foreground">
<span className="text-red-500">*</span>
</Label>
<Input
value={inspector}
onChange={(e) => {
setInspector(e.target.value);
setValidationErrors([]);
}}
placeholder="검사자명 입력"
/>
</div>
<div className="space-y-2">
<Label className="text-sm text-muted-foreground">LOT번호 (YYMMDD-##)</Label>
<Input value={lotNo} onChange={(e) => setLotNo(e.target.value)} />
</div>
</div>
</div>
{/* 검사 항목 */}
<div className="space-y-4 bg-white p-4 rounded-lg border">
<h3 className="font-medium"> </h3>
<div className="border rounded-lg overflow-hidden">
<table className="w-full text-sm">
<thead className="bg-gray-50">
<tr>
<th className="px-3 py-2 text-left font-medium"></th>
<th className="px-3 py-2 text-left font-medium"></th>
<th className="px-3 py-2 text-left font-medium"></th>
<th className="px-3 py-2 text-center font-medium w-[100px]"></th>
<th className="px-3 py-2 text-left font-medium w-[120px]"></th>
</tr>
</thead>
<tbody>
{inspectionItems.map((item) => (
<tr key={item.id} className="border-t">
<td className="px-3 py-2">{item.name}</td>
<td className="px-3 py-2 text-muted-foreground text-xs">
{item.specification}
</td>
<td className="px-3 py-2">{item.method}</td>
<td className="px-3 py-2">
<Select
value={item.judgment || ''}
onValueChange={(value) =>
handleJudgmentChange(item.id, value as '적' | '부적')
}
>
<SelectTrigger className="h-8">
<SelectValue placeholder="선택" />
</SelectTrigger>
<SelectContent>
<SelectItem value="적"></SelectItem>
<SelectItem value="부적"></SelectItem>
</SelectContent>
</Select>
</td>
<td className="px-3 py-2">
<Input
value={item.remark || ''}
onChange={(e) => handleRemarkChange(item.id, e.target.value)}
placeholder="비고"
className="h-8"
/>
</td>
</tr>
))}
</tbody>
</table>
</div>
</div>
{/* 종합 의견 */}
<div className="space-y-2 bg-white p-4 rounded-lg border">
<Label className="text-sm font-medium"> </Label>
<Textarea
value={opinion}
onChange={(e) => setOpinion(e.target.value)}
placeholder="검사 관련 특이사항 입력"
rows={3}
/>
</div>
</div>
</div>
</div>
{/* 성공 다이얼로그 */}
<SuccessDialog
open={showSuccess}
type="inspection"
lotNo={lotNo}
onClose={handleSuccessClose}
/>
</PageLayout>
);
}

View File

@@ -0,0 +1,281 @@
'use client';
/**
* 입고 상세 페이지
* 상태에 따라 다른 UI 표시:
* - 검사대기: 입고증, 목록, 검사등록 버튼
* - 배송중/발주완료: 목록, 입고처리 버튼 (입고증 없음)
* - 입고대기: 목록 버튼만 (입고증 없음)
* - 입고완료: 입고증, 목록 버튼
*/
import { useState, useCallback, useMemo } from 'react';
import { useRouter } from 'next/navigation';
import { Package, FileText, List, ClipboardCheck, Download } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { PageLayout } from '@/components/organisms/PageLayout';
import { mockReceivingItems, mockReceivingDetails } from './mockData';
import { RECEIVING_STATUS_LABELS, RECEIVING_STATUS_STYLES } from './types';
import type { ReceivingDetail as ReceivingDetailType } from './types';
import { ReceivingProcessDialog } from './ReceivingProcessDialog';
import { ReceivingReceiptDialog } from './ReceivingReceiptDialog';
import { SuccessDialog } from './SuccessDialog';
interface Props {
id: string;
}
export function ReceivingDetail({ id }: Props) {
const router = useRouter();
const [isReceivingProcessDialogOpen, setIsReceivingProcessDialogOpen] = useState(false);
const [isReceiptDialogOpen, setIsReceiptDialogOpen] = useState(false);
const [successDialog, setSuccessDialog] = useState<{
open: boolean;
type: 'inspection' | 'receiving';
lotNo?: string;
}>({ open: false, type: 'receiving' });
// 데이터 가져오기
const detail: ReceivingDetailType | undefined = useMemo(() => {
// 먼저 상세 데이터에서 찾기
if (mockReceivingDetails[id]) {
return mockReceivingDetails[id];
}
// 없으면 목록에서 변환
const item = mockReceivingItems.find((i) => i.id === id);
if (item) {
return {
id: item.id,
orderNo: item.orderNo,
orderDate: undefined,
supplier: item.supplier,
itemCode: item.itemCode,
itemName: item.itemName,
specification: undefined,
orderQty: item.orderQty,
orderUnit: item.orderUnit,
dueDate: undefined,
status: item.status,
receivingDate: undefined,
receivingQty: item.receivingQty,
receivingLot: item.lotNo,
supplierLot: undefined,
receivingLocation: undefined,
receivingManager: undefined,
};
}
return undefined;
}, [id]);
// 목록으로 돌아가기
const handleGoBack = useCallback(() => {
router.push('/ko/material/receiving-management');
}, [router]);
// 입고증 다이얼로그 열기
const handleOpenReceipt = useCallback(() => {
setIsReceiptDialogOpen(true);
}, []);
// 검사등록 페이지로 이동
const handleGoToInspection = useCallback(() => {
router.push('/ko/material/receiving-management/inspection');
}, [router]);
// 입고처리 다이얼로그 열기
const handleOpenReceivingProcessDialog = useCallback(() => {
setIsReceivingProcessDialogOpen(true);
}, []);
// 입고 완료 처리
const handleReceivingComplete = useCallback((lotNo: string) => {
setIsReceivingProcessDialogOpen(false);
setSuccessDialog({ open: true, type: 'receiving', lotNo });
}, []);
// 성공 다이얼로그 닫기
const handleSuccessDialogClose = useCallback(() => {
setSuccessDialog({ open: false, type: 'receiving' });
router.push('/ko/material/receiving-management');
}, [router]);
if (!detail) {
return (
<PageLayout>
<div className="flex items-center justify-center h-64">
<p className="text-muted-foreground"> .</p>
</div>
</PageLayout>
);
}
// 상태별 버튼 구성
// 검사등록 버튼: 검사대기만
const showInspectionButton = detail.status === 'inspection_pending';
// 입고처리 버튼: 발주완료, 배송중만
const showReceivingProcessButton =
detail.status === 'order_completed' || detail.status === 'shipping';
// 입고증 버튼: 검사대기, 입고완료만 (입고대기, 발주완료, 배송중은 없음)
const showReceiptButton =
detail.status === 'inspection_pending' || detail.status === 'completed';
// 입고 정보 표시 여부: 검사대기, 입고대기, 입고완료
const showReceivingInfo =
detail.status === 'inspection_pending' ||
detail.status === 'receiving_pending' ||
detail.status === 'completed';
return (
<PageLayout>
<div className="space-y-6">
{/* 헤더 */}
<div className="flex flex-col gap-4 sm:flex-row sm:items-center sm:justify-between">
<div className="flex items-center gap-3">
<Package className="w-6 h-6" />
<h1 className="text-xl font-semibold"> </h1>
<span className="text-lg text-muted-foreground">{detail.orderNo}</span>
<Badge className={`${RECEIVING_STATUS_STYLES[detail.status]}`}>
{RECEIVING_STATUS_LABELS[detail.status]}
</Badge>
</div>
<div className="flex items-center gap-2">
{showReceiptButton && (
<Button variant="outline" onClick={handleOpenReceipt}>
<FileText className="w-4 h-4 mr-1.5" />
</Button>
)}
<Button variant="outline" onClick={handleGoBack}>
<List className="w-4 h-4 mr-1.5" />
</Button>
{showInspectionButton && (
<Button onClick={handleGoToInspection}>
<ClipboardCheck className="w-4 h-4 mr-1.5" />
</Button>
)}
{showReceivingProcessButton && (
<Button onClick={handleOpenReceivingProcessDialog}>
<Download className="w-4 h-4 mr-1.5" />
</Button>
)}
</div>
</div>
{/* 발주 정보 */}
<Card>
<CardHeader className="pb-4">
<CardTitle className="text-base"> </CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<p className="text-sm text-muted-foreground mb-1"></p>
<p className="font-medium">{detail.orderNo}</p>
</div>
<div>
<p className="text-sm text-muted-foreground mb-1"></p>
<p className="font-medium">{detail.orderDate || '-'}</p>
</div>
<div>
<p className="text-sm text-muted-foreground mb-1"></p>
<p className="font-medium">{detail.supplier}</p>
</div>
<div>
<p className="text-sm text-muted-foreground mb-1"></p>
<p className="font-medium">{detail.itemCode}</p>
</div>
<div>
<p className="text-sm text-muted-foreground mb-1"></p>
<p className="font-medium">{detail.itemName}</p>
</div>
<div>
<p className="text-sm text-muted-foreground mb-1"></p>
<p className="font-medium">{detail.specification || '-'}</p>
</div>
<div>
<p className="text-sm text-muted-foreground mb-1"></p>
<p className="font-medium">
{detail.orderQty} {detail.orderUnit}
</p>
</div>
<div>
<p className="text-sm text-muted-foreground mb-1"></p>
<p className="font-medium">{detail.dueDate || '-'}</p>
</div>
</div>
</CardContent>
</Card>
{/* 입고 정보 - 검사대기/입고대기/입고완료 상태에서만 표시 */}
{showReceivingInfo && (
<Card>
<CardHeader className="pb-4">
<CardTitle className="text-base"> </CardTitle>
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<div>
<p className="text-sm text-muted-foreground mb-1"></p>
<p className="font-medium">{detail.receivingDate || '-'}</p>
</div>
<div>
<p className="text-sm text-muted-foreground mb-1"></p>
<p className="font-medium">
{detail.receivingQty !== undefined
? `${detail.receivingQty} ${detail.orderUnit}`
: '-'}
</p>
</div>
<div>
<p className="text-sm text-muted-foreground mb-1">LOT</p>
<p className="font-medium">{detail.receivingLot || '-'}</p>
</div>
<div>
<p className="text-sm text-muted-foreground mb-1">LOT</p>
<p className="font-medium">{detail.supplierLot || '-'}</p>
</div>
<div>
<p className="text-sm text-muted-foreground mb-1"></p>
<p className="font-medium">{detail.receivingLocation || '-'}</p>
</div>
<div>
<p className="text-sm text-muted-foreground mb-1"></p>
<p className="font-medium">{detail.receivingManager || '-'}</p>
</div>
</div>
</CardContent>
</Card>
)}
</div>
{/* 입고증 다이얼로그 */}
<ReceivingReceiptDialog
open={isReceiptDialogOpen}
onOpenChange={setIsReceiptDialogOpen}
detail={detail}
/>
{/* 입고처리 다이얼로그 */}
<ReceivingProcessDialog
open={isReceivingProcessDialogOpen}
onOpenChange={setIsReceivingProcessDialogOpen}
detail={detail}
onComplete={handleReceivingComplete}
/>
{/* 성공 다이얼로그 */}
<SuccessDialog
open={successDialog.open}
type={successDialog.type}
lotNo={successDialog.lotNo}
onClose={handleSuccessDialogClose}
/>
</PageLayout>
);
}

View File

@@ -0,0 +1,313 @@
'use client';
/**
* 입고 목록 페이지
* IntegratedListTemplateV2 패턴 적용
*/
import { useState, useMemo, useCallback } from 'react';
import { useRouter } from 'next/navigation';
import {
Package,
Truck,
ClipboardCheck,
Calendar,
Eye,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
import { TableCell, TableRow } from '@/components/ui/table';
import { Checkbox } from '@/components/ui/checkbox';
import {
IntegratedListTemplateV2,
TabOption,
TableColumn,
StatCard,
} from '@/components/templates/IntegratedListTemplateV2';
import { ListMobileCard, InfoField } from '@/components/organisms/ListMobileCard';
import { mockReceivingItems, mockStats, mockFilterTabs } from './mockData';
import { RECEIVING_STATUS_LABELS, RECEIVING_STATUS_STYLES } from './types';
import type { ReceivingItem, ReceivingStatus } from './types';
// 페이지당 항목 수
const ITEMS_PER_PAGE = 20;
export function ReceivingList() {
const router = useRouter();
const [searchTerm, setSearchTerm] = useState('');
const [selectedItems, setSelectedItems] = useState<Set<string>>(new Set());
const [currentPage, setCurrentPage] = useState(1);
const [activeFilter, setActiveFilter] = useState<'all' | 'receiving_pending' | 'completed'>('all');
// 통계 카드
const stats: StatCard[] = [
{
label: '입고대기',
value: `${mockStats.receivingPendingCount}`,
icon: Package,
iconColor: 'text-yellow-600',
},
{
label: '배송중',
value: `${mockStats.shippingCount}`,
icon: Truck,
iconColor: 'text-blue-600',
},
{
label: '검사대기',
value: `${mockStats.inspectionPendingCount}`,
icon: ClipboardCheck,
iconColor: 'text-orange-600',
},
{
label: '금일입고',
value: `${mockStats.todayReceivingCount}`,
icon: Calendar,
iconColor: 'text-green-600',
},
];
// 테이블 컬럼
const tableColumns: TableColumn[] = [
{ key: 'no', label: '번호', className: 'w-[60px] text-center' },
{ key: 'orderNo', label: '발주번호', className: 'min-w-[150px]' },
{ key: 'itemCode', label: '품목코드', className: 'min-w-[130px]' },
{ key: 'itemName', label: '품목명', className: 'min-w-[150px]' },
{ key: 'supplier', label: '공급업체', className: 'min-w-[100px]' },
{ key: 'orderQty', label: '발주수량', className: 'w-[100px] text-center' },
{ key: 'receivingQty', label: '입고수량', className: 'w-[100px] text-center' },
{ key: 'lotNo', label: 'LOT번호', className: 'w-[120px] text-center' },
{ key: 'status', label: '상태', className: 'w-[100px] text-center' },
];
// 필터링된 데이터
const filteredResults = useMemo(() => {
let result = [...mockReceivingItems];
// 탭 필터
if (activeFilter === 'receiving_pending') {
result = result.filter((item) =>
['receiving_pending', 'inspection_pending'].includes(item.status)
);
} else if (activeFilter === 'completed') {
result = result.filter((item) => item.status === 'completed');
}
// 검색 필터
if (searchTerm) {
const term = searchTerm.toLowerCase();
result = result.filter(
(item) =>
item.orderNo.toLowerCase().includes(term) ||
item.itemCode.toLowerCase().includes(term) ||
item.itemName.toLowerCase().includes(term) ||
item.supplier.toLowerCase().includes(term)
);
}
return result;
}, [searchTerm, activeFilter]);
// 페이지네이션 데이터
const paginatedData = useMemo(() => {
const startIndex = (currentPage - 1) * ITEMS_PER_PAGE;
return filteredResults.slice(startIndex, startIndex + ITEMS_PER_PAGE);
}, [filteredResults, currentPage]);
// 페이지네이션 설정
const pagination = {
currentPage,
totalPages: Math.ceil(filteredResults.length / ITEMS_PER_PAGE),
totalItems: filteredResults.length,
itemsPerPage: ITEMS_PER_PAGE,
onPageChange: setCurrentPage,
};
// 선택 핸들러
const handleToggleSelection = useCallback((id: string) => {
setSelectedItems((prev) => {
const newSet = new Set(prev);
if (newSet.has(id)) {
newSet.delete(id);
} else {
newSet.add(id);
}
return newSet;
});
}, []);
const handleToggleSelectAll = useCallback(() => {
if (selectedItems.size === paginatedData.length) {
setSelectedItems(new Set());
} else {
setSelectedItems(new Set(paginatedData.map((item) => item.id)));
}
}, [paginatedData, selectedItems.size]);
// 검색 변경 시 페이지 리셋
const handleSearchChange = (value: string) => {
setSearchTerm(value);
setCurrentPage(1);
};
// 탭 변경 시 페이지 리셋
const handleTabChange = (value: string) => {
setActiveFilter(value as 'all' | 'receiving_pending' | 'completed');
setCurrentPage(1);
setSelectedItems(new Set());
};
// 상세 보기
const handleView = useCallback(
(id: string) => {
router.push(`/ko/material/receiving-management/${id}`);
},
[router]
);
// 테이블 행 렌더링
const renderTableRow = (item: ReceivingItem, index: number, globalIndex: number) => {
const isSelected = selectedItems.has(item.id);
return (
<TableRow
key={item.id}
className={`cursor-pointer hover:bg-muted/50 ${isSelected ? 'bg-blue-50' : ''}`}
onClick={() => handleView(item.id)}
>
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
<Checkbox
checked={isSelected}
onCheckedChange={() => handleToggleSelection(item.id)}
/>
</TableCell>
<TableCell className="text-center text-muted-foreground">{globalIndex}</TableCell>
<TableCell className="font-medium">{item.orderNo}</TableCell>
<TableCell>{item.itemCode}</TableCell>
<TableCell className="max-w-[150px] truncate">{item.itemName}</TableCell>
<TableCell>{item.supplier}</TableCell>
<TableCell className="text-center">
{item.orderQty} {item.orderUnit}
</TableCell>
<TableCell className="text-center">
{item.receivingQty !== undefined ? item.receivingQty : '-'}
</TableCell>
<TableCell className="text-center">{item.lotNo || '-'}</TableCell>
<TableCell className="text-center">
<Badge className={`text-xs ${RECEIVING_STATUS_STYLES[item.status]}`}>
{RECEIVING_STATUS_LABELS[item.status]}
</Badge>
</TableCell>
</TableRow>
);
};
// 모바일 카드 렌더링
const renderMobileCard = (
item: ReceivingItem,
index: number,
globalIndex: number,
isSelected: boolean,
onToggle: () => void
) => {
return (
<ListMobileCard
id={item.id}
isSelected={isSelected}
onToggleSelection={onToggle}
onClick={() => handleView(item.id)}
headerBadges={
<>
<Badge variant="outline" className="text-xs">
#{globalIndex}
</Badge>
<Badge variant="outline" className="text-xs">
{item.orderNo}
</Badge>
</>
}
title={item.itemName}
statusBadge={
<Badge className={`text-xs ${RECEIVING_STATUS_STYLES[item.status]}`}>
{RECEIVING_STATUS_LABELS[item.status]}
</Badge>
}
infoGrid={
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
<InfoField label="품목코드" value={item.itemCode} />
<InfoField label="공급업체" value={item.supplier} />
<InfoField label="발주수량" value={`${item.orderQty} ${item.orderUnit}`} />
<InfoField
label="입고수량"
value={item.receivingQty !== undefined ? `${item.receivingQty}` : '-'}
/>
<InfoField label="LOT번호" value={item.lotNo || '-'} />
</div>
}
actions={
isSelected && (
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
className="flex-1"
onClick={(e) => {
e.stopPropagation();
handleView(item.id);
}}
>
<Eye className="w-4 h-4 mr-1" />
</Button>
</div>
)
}
/>
);
};
// 탭 옵션
const tabs: TabOption[] = mockFilterTabs.map((tab) => ({
value: tab.key,
label: tab.label,
count: tab.count,
}));
// 하단 요약
const tableFooter = (
<TableRow className="bg-gray-50 hover:bg-gray-50">
<TableCell colSpan={tableColumns.length + 1} className="py-3">
<span className="text-sm text-muted-foreground">
{filteredResults.length} / {mockStats.receivingPendingCount} / {' '}
{mockStats.inspectionPendingCount}
</span>
</TableCell>
</TableRow>
);
return (
<IntegratedListTemplateV2<ReceivingItem>
title="입고 목록"
icon={Package}
stats={stats}
searchValue={searchTerm}
onSearchChange={handleSearchChange}
searchPlaceholder="발주번호, 품목코드, 품목명, 공급업체 검색..."
tabs={tabs}
activeTab={activeFilter}
onTabChange={handleTabChange}
tableColumns={tableColumns}
data={paginatedData}
totalCount={filteredResults.length}
allData={filteredResults}
selectedItems={selectedItems}
onToggleSelection={handleToggleSelection}
onToggleSelectAll={handleToggleSelectAll}
getItemId={(item) => item.id}
renderTableRow={renderTableRow}
renderMobileCard={renderMobileCard}
pagination={pagination}
tableFooter={tableFooter}
/>
);
}

View File

@@ -0,0 +1,227 @@
'use client';
/**
* 입고처리 다이얼로그
* - 발주 정보 표시
* - 입고LOT*, 공급업체LOT, 입고수량*, 입고위치* 입력
* - 비고 입력
*/
import { useState, useCallback } from 'react';
import { Download } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
import { Textarea } from '@/components/ui/textarea';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import { Alert, AlertDescription } from '@/components/ui/alert';
import { generateLotNo } from './mockData';
import type { ReceivingDetail } from './types';
interface Props {
open: boolean;
onOpenChange: (open: boolean) => void;
detail: ReceivingDetail;
onComplete: (lotNo: string) => void;
}
export function ReceivingProcessDialog({ open, onOpenChange, detail, onComplete }: Props) {
// 폼 데이터
const [receivingLot, setReceivingLot] = useState(() => generateLotNo());
const [supplierLot, setSupplierLot] = useState('');
const [receivingQty, setReceivingQty] = useState<string>(detail.orderQty.toString());
const [receivingLocation, setReceivingLocation] = useState('');
const [remark, setRemark] = useState('');
// 유효성 검사 에러
const [validationErrors, setValidationErrors] = useState<string[]>([]);
// 유효성 검사
const validateForm = useCallback((): boolean => {
const errors: string[] = [];
if (!receivingLot.trim()) {
errors.push('입고LOT는 필수 입력 항목입니다.');
}
if (!receivingQty.trim() || isNaN(Number(receivingQty)) || Number(receivingQty) <= 0) {
errors.push('입고수량은 필수 입력 항목입니다. 유효한 숫자를 입력해주세요.');
}
if (!receivingLocation.trim()) {
errors.push('입고위치는 필수 입력 항목입니다.');
}
setValidationErrors(errors);
return errors.length === 0;
}, [receivingLot, receivingQty, receivingLocation]);
// 입고 처리
const handleSubmit = useCallback(() => {
if (!validateForm()) {
return;
}
// TODO: API 호출
console.log('입고 처리:', {
detailId: detail.id,
receivingLot,
supplierLot,
receivingQty: Number(receivingQty),
receivingLocation,
remark,
});
onComplete(receivingLot);
}, [validateForm, detail.id, receivingLot, supplierLot, receivingQty, receivingLocation, remark, onComplete]);
// 취소
const handleCancel = useCallback(() => {
onOpenChange(false);
}, [onOpenChange]);
// 다이얼로그 닫힐 때 상태 초기화
const handleOpenChange = useCallback(
(newOpen: boolean) => {
if (!newOpen) {
setValidationErrors([]);
}
onOpenChange(newOpen);
},
[onOpenChange]
);
return (
<Dialog open={open} onOpenChange={handleOpenChange}>
<DialogContent className="max-w-md">
<DialogHeader>
<DialogTitle> </DialogTitle>
</DialogHeader>
<div className="space-y-6 mt-4">
{/* 발주 정보 요약 */}
<div className="grid grid-cols-2 gap-4 text-sm">
<div>
<span className="text-muted-foreground">:</span>{' '}
<span className="font-medium">{detail.orderNo}</span>
</div>
<div>
<span className="text-muted-foreground">:</span>{' '}
<span className="font-medium">{detail.supplier}</span>
</div>
<div>
<span className="text-muted-foreground">:</span>{' '}
<span className="font-medium">{detail.itemName}</span>
</div>
<div>
<span className="text-muted-foreground">:</span>{' '}
<span className="font-medium">{detail.orderQty} {detail.orderUnit}</span>
</div>
</div>
{/* Validation 에러 표시 */}
{validationErrors.length > 0 && (
<Alert className="bg-red-50 border-red-200">
<AlertDescription className="text-red-900">
<div className="flex items-start gap-2">
<span className="text-lg"></span>
<div className="flex-1">
<strong className="block mb-2">
({validationErrors.length} )
</strong>
<ul className="space-y-1 text-sm">
{validationErrors.map((error, index) => (
<li key={index} className="flex items-start gap-1">
<span></span>
<span>{error}</span>
</li>
))}
</ul>
</div>
</div>
</AlertDescription>
</Alert>
)}
{/* 입력 필드 */}
<div className="grid grid-cols-2 gap-4">
<div className="space-y-2">
<Label className="text-sm">
LOT <span className="text-red-500">*</span>
</Label>
<Input
value={receivingLot}
onChange={(e) => {
setReceivingLot(e.target.value);
setValidationErrors([]);
}}
placeholder="예: 251223-41"
/>
</div>
<div className="space-y-2">
<Label className="text-sm text-muted-foreground">LOT</Label>
<Input
value={supplierLot}
onChange={(e) => setSupplierLot(e.target.value)}
placeholder="예: 2402944"
/>
</div>
<div className="space-y-2">
<Label className="text-sm">
<span className="text-red-500">*</span>
</Label>
<Input
type="number"
value={receivingQty}
onChange={(e) => {
setReceivingQty(e.target.value);
setValidationErrors([]);
}}
placeholder="수량 입력"
/>
</div>
<div className="space-y-2">
<Label className="text-sm">
<span className="text-red-500">*</span>
</Label>
<Input
value={receivingLocation}
onChange={(e) => {
setReceivingLocation(e.target.value);
setValidationErrors([]);
}}
placeholder="예: A-01"
/>
</div>
</div>
{/* 비고 */}
<div className="space-y-2">
<Label className="text-sm text-muted-foreground"></Label>
<Textarea
value={remark}
onChange={(e) => setRemark(e.target.value)}
placeholder="특이사항 입력"
rows={3}
/>
</div>
</div>
{/* 버튼 영역 */}
<div className="flex justify-end gap-2 mt-6 pt-4 border-t">
<Button variant="outline" onClick={handleCancel}>
</Button>
<Button onClick={handleSubmit}>
</Button>
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,184 @@
'use client';
/**
* 입고증 다이얼로그 (인쇄용)
* - 작업일지(WorkLogModal) 스타일 적용
*/
import { Printer, Download, X } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
DialogTitle,
} from '@/components/ui/dialog';
import { VisuallyHidden } from '@radix-ui/react-visually-hidden';
import type { ReceivingDetail } from './types';
interface Props {
open: boolean;
onOpenChange: (open: boolean) => void;
detail: ReceivingDetail;
}
export function ReceivingReceiptDialog({ open, onOpenChange, detail }: Props) {
const handlePrint = () => {
window.print();
};
const handleDownload = () => {
// TODO: PDF 다운로드 기능
console.log('PDF 다운로드:', detail);
};
const today = new Date();
const formattedDate = `${today.getFullYear()}${today.getMonth() + 1}${today.getDate()}`;
return (
<Dialog open={open} onOpenChange={onOpenChange}>
<DialogContent className="sm:max-w-4xl max-h-[90vh] overflow-y-auto p-0 gap-0 bg-gray-100">
{/* 접근성을 위한 숨겨진 타이틀 */}
<VisuallyHidden>
<DialogTitle> - {detail.orderNo}</DialogTitle>
</VisuallyHidden>
{/* 모달 헤더 - 작업일지 스타일 */}
<div className="flex items-center justify-between px-6 py-4 border-b bg-white sticky top-0 z-10">
<div className="flex items-center gap-3">
<span className="font-semibold text-lg"></span>
<span className="text-sm text-muted-foreground">
{detail.supplier}
</span>
<span className="text-sm text-muted-foreground">
({detail.orderNo})
</span>
</div>
<div className="flex items-center gap-2">
<Button variant="outline" size="sm" onClick={handlePrint}>
<Printer className="w-4 h-4 mr-1.5" />
</Button>
<Button variant="outline" size="sm" onClick={handleDownload}>
<Download className="w-4 h-4 mr-1.5" />
</Button>
<Button
variant="ghost"
size="icon"
className="h-8 w-8"
onClick={() => onOpenChange(false)}
>
<X className="w-4 h-4" />
</Button>
</div>
</div>
{/* 문서 본문 */}
<div className="m-6 p-8 bg-white rounded-lg shadow-sm print:m-0 print:shadow-none">
{/* 제목 */}
<div className="text-center mb-8">
<h2 className="text-2xl font-bold"></h2>
<p className="text-sm text-muted-foreground">RECEIVING SLIP</p>
</div>
{/* 입고 정보 / 공급업체 정보 */}
<div className="grid grid-cols-2 gap-8 mb-8">
{/* 입고 정보 */}
<div className="space-y-3 text-sm">
<h3 className="font-semibold border-b pb-2"> </h3>
<div className="grid grid-cols-[100px_1fr] gap-y-2">
<span className="text-muted-foreground"></span>
<span className="font-medium">{detail.orderNo}</span>
<span className="text-muted-foreground"></span>
<span>{detail.receivingDate || today.toISOString().split('T')[0]}</span>
<span className="text-muted-foreground"></span>
<span>{detail.orderNo}</span>
<span className="text-muted-foreground">LOT</span>
<span>{detail.receivingLot || '-'}</span>
</div>
</div>
{/* 공급업체 정보 */}
<div className="space-y-3 text-sm">
<h3 className="font-semibold border-b pb-2"> </h3>
<div className="grid grid-cols-[100px_1fr] gap-y-2">
<span className="text-muted-foreground"></span>
<span className="font-medium">{detail.supplier}</span>
<span className="text-muted-foreground">LOT</span>
<span>{detail.supplierLot || '-'}</span>
<span className="text-muted-foreground"></span>
<span>{detail.receivingManager || '-'}</span>
<span className="text-muted-foreground"></span>
<span>{detail.receivingLocation || '-'}</span>
</div>
</div>
</div>
{/* 입고 품목 상세 */}
<div className="mb-8">
<h3 className="font-semibold text-sm mb-3"> </h3>
<table className="w-full text-sm border-collapse">
<thead>
<tr className="border-y bg-gray-50">
<th className="px-3 py-2 text-center font-medium w-12">No</th>
<th className="px-3 py-2 text-left font-medium"></th>
<th className="px-3 py-2 text-left font-medium"></th>
<th className="px-3 py-2 text-left font-medium"></th>
<th className="px-3 py-2 text-center font-medium w-24"></th>
<th className="px-3 py-2 text-center font-medium w-24"></th>
<th className="px-3 py-2 text-center font-medium w-16"></th>
<th className="px-3 py-2 text-left font-medium w-24"></th>
</tr>
</thead>
<tbody>
<tr className="border-b">
<td className="px-3 py-2 text-center">1</td>
<td className="px-3 py-2">{detail.itemCode}</td>
<td className="px-3 py-2">{detail.itemName}</td>
<td className="px-3 py-2">{detail.specification || '-'}</td>
<td className="px-3 py-2 text-center">{detail.orderQty}</td>
<td className="px-3 py-2 text-center">{detail.receivingQty || '-'}</td>
<td className="px-3 py-2 text-center">{detail.orderUnit}</td>
<td className="px-3 py-2">-</td>
</tr>
</tbody>
</table>
</div>
{/* 수입검사 안내 */}
<div className="mb-8 p-4 bg-gray-50 rounded-lg text-sm">
<p className="flex items-start gap-2">
<span className="font-medium">📋 </span>
</p>
<p className="text-muted-foreground mt-1">
<span className="font-medium text-blue-600">(IQC)</span> .<br />
&gt; (IQC) .
</p>
</div>
{/* 서명란 */}
<div className="grid grid-cols-3 gap-6 mb-8">
<div className="border rounded p-4 text-center">
<p className="text-sm font-medium mb-12"></p>
<p className="text-xs text-muted-foreground">()</p>
</div>
<div className="border rounded p-4 text-center">
<p className="text-sm font-medium mb-12"></p>
<p className="text-xs text-muted-foreground">()</p>
</div>
<div className="border rounded p-4 text-center">
<p className="text-sm font-medium mb-12"></p>
<p className="text-xs text-muted-foreground">()</p>
</div>
</div>
{/* 발행일 / 회사명 */}
<div className="text-right text-sm text-muted-foreground">
<p>: {formattedDate}</p>
<p>() </p>
</div>
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,49 @@
'use client';
/**
* 성공 다이얼로그 (검사 등록 완료 / 입고 처리 완료)
*/
import { CheckCircle2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import {
Dialog,
DialogContent,
} from '@/components/ui/dialog';
interface Props {
open: boolean;
type: 'inspection' | 'receiving';
lotNo?: string;
onClose: () => void;
}
export function SuccessDialog({ open, type, lotNo, onClose }: Props) {
const title = type === 'inspection' ? '수입검사가 합격 처리되었습니다.' : '입고 처리가 완료되었습니다.';
const message = type === 'inspection'
? `LOT번호: ${lotNo}\n입고 처리가 완료되었습니다.`
: `LOT번호: ${lotNo}\n재고에 반영되었습니다.`;
return (
<Dialog open={open} onOpenChange={(newOpen) => !newOpen && onClose()}>
<DialogContent className="max-w-sm">
<div className="flex flex-col items-center text-center py-6 space-y-4">
<div className="w-16 h-16 rounded-full bg-green-100 flex items-center justify-center">
<CheckCircle2 className="w-10 h-10 text-green-600" />
</div>
<div className="space-y-2">
<h3 className="text-lg font-semibold">{title}</h3>
<div className="text-sm text-muted-foreground whitespace-pre-line">
{message}
</div>
</div>
<Button onClick={onClose} className="w-full mt-4">
</Button>
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -0,0 +1,11 @@
/**
* 입고관리 컴포넌트 내보내기
*/
export { ReceivingList } from './ReceivingList';
export { ReceivingDetail } from './ReceivingDetail';
export { InspectionCreate } from './InspectionCreate';
export { ReceivingProcessDialog } from './ReceivingProcessDialog';
export { ReceivingReceiptDialog } from './ReceivingReceiptDialog';
export { SuccessDialog } from './SuccessDialog';
export * from './types';

View File

@@ -0,0 +1,158 @@
/**
* 입고관리 목업 데이터
*/
import type {
ReceivingItem,
ReceivingDetail,
ReceivingStats,
FilterTab,
InspectionTarget,
InspectionCheckItem,
} from './types';
// 목업 입고 목록
export const mockReceivingItems: ReceivingItem[] = [
{
id: '1',
orderNo: 'KD-SO-250312-001',
itemCode: 'RM-COIL-08',
itemName: '갈바코일 0.8T',
supplier: '포항철강',
orderQty: 5,
orderUnit: '톤',
receivingQty: undefined,
lotNo: undefined,
status: 'inspection_pending',
},
{
id: '2',
orderNo: 'KD-SO-250311-001',
itemCode: 'PP-ENDLOCK-01',
itemName: '엔드락 2100',
supplier: '부품산업',
orderQty: 200,
orderUnit: 'EA',
receivingQty: undefined,
lotNo: undefined,
status: 'receiving_pending',
},
{
id: '3',
orderNo: 'KD-SO-250310-001',
itemCode: 'RM-SCREEN-01',
itemName: '방충망 원단 1016',
supplier: '한국망사',
orderQty: 100,
orderUnit: 'M',
receivingQty: undefined,
lotNo: undefined,
status: 'inspection_pending',
},
{
id: '4',
orderNo: 'KD-SO-250302-001',
itemCode: 'PP-MOTOR-01',
itemName: '튜블러모터',
supplier: '모터공사',
orderQty: 50,
orderUnit: 'EA',
receivingQty: undefined,
lotNo: undefined,
status: 'order_completed',
},
{
id: '5',
orderNo: 'KD-SO-250301-001',
itemCode: 'RM-COIL-05',
itemName: '갈바코일 0.5T',
supplier: '철강공업',
orderQty: 10,
orderUnit: '톤',
receivingQty: undefined,
lotNo: undefined,
status: 'shipping',
},
];
// 상세 정보 목업
export const mockReceivingDetails: Record<string, ReceivingDetail> = {
'1': {
id: '1',
orderNo: 'KD-SO-250312-001',
orderDate: undefined,
supplier: '포항철강',
itemCode: 'RM-COIL-08',
itemName: '갈바코일 0.8T',
specification: undefined,
orderQty: 5,
orderUnit: '톤',
dueDate: undefined,
status: 'inspection_pending',
receivingDate: undefined,
receivingQty: undefined,
receivingLot: undefined,
supplierLot: undefined,
receivingLocation: undefined,
receivingManager: '자재팀',
},
'5': {
id: '5',
orderNo: 'KD-SO-250301-001',
orderDate: undefined,
supplier: '철강공업',
itemCode: 'RM-COIL-05',
itemName: '갈바코일 0.5T',
specification: undefined,
orderQty: 10,
orderUnit: '톤',
dueDate: undefined,
status: 'shipping',
receivingDate: undefined,
receivingQty: undefined,
receivingLot: undefined,
supplierLot: undefined,
receivingLocation: undefined,
receivingManager: undefined,
},
};
// 통계 목업
export const mockStats: ReceivingStats = {
receivingPendingCount: 2,
shippingCount: 1,
inspectionPendingCount: 2,
todayReceivingCount: 0,
};
// 필터 탭 목업
export const mockFilterTabs: FilterTab[] = [
{ key: 'all', label: '전체', count: 5 },
{ key: 'receiving_pending', label: '입고대기', count: 2 },
{ key: 'completed', label: '입고완료', count: 2 },
];
// 검사 대상 목업
export const mockInspectionTargets: InspectionTarget[] = [
{ id: '3', orderNo: 'KD-SO-250310-001', supplier: '한국망사', qty: '100EA' },
{ id: '2', orderNo: 'KD-SO-250311-001', supplier: '부품산업', qty: '200EA' },
{ id: '1', orderNo: 'KD-SO-250312-001', supplier: '포항철강', qty: '5EA' },
];
// 검사 항목 템플릿
export const defaultInspectionItems: InspectionCheckItem[] = [
{ id: '1', name: '겉모양', specification: '사용상 해로운 결함이 없을 것', method: '육안검사', judgment: '', remark: '' },
{ id: '2', name: '두께', specification: '규격 참조', method: '마이크로미터', judgment: '', remark: '' },
{ id: '3', name: '폭', specification: '규격 참조', method: '줄자', judgment: '', remark: '' },
{ id: '4', name: '길이', specification: '규격 참조', method: '줄자', judgment: '', remark: '' },
];
// LOT 번호 생성
export function generateLotNo(): string {
const now = new Date();
const year = now.getFullYear().toString().slice(-2);
const month = (now.getMonth() + 1).toString().padStart(2, '0');
const day = now.getDate().toString().padStart(2, '0');
const seq = Math.floor(Math.random() * 100).toString().padStart(2, '0');
return `${year}${month}${day}-${seq}`;
}

View File

@@ -0,0 +1,117 @@
/**
* 입고관리 타입 정의
*/
// 입고 상태
export type ReceivingStatus =
| 'order_completed' // 발주완료
| 'shipping' // 배송중
| 'inspection_pending' // 검사대기
| 'receiving_pending' // 입고대기
| 'completed'; // 입고완료
// 상태 라벨
export const RECEIVING_STATUS_LABELS: Record<ReceivingStatus, string> = {
order_completed: '발주완료',
shipping: '배송중',
inspection_pending: '검사대기',
receiving_pending: '입고대기',
completed: '입고완료',
};
// 상태 스타일
export const RECEIVING_STATUS_STYLES: Record<ReceivingStatus, string> = {
order_completed: 'bg-gray-100 text-gray-800',
shipping: 'bg-blue-100 text-blue-800',
inspection_pending: 'bg-orange-100 text-orange-800',
receiving_pending: 'bg-yellow-100 text-yellow-800',
completed: 'bg-green-100 text-green-800',
};
// 입고 목록 아이템
export interface ReceivingItem {
id: string;
orderNo: string; // 발주번호
itemCode: string; // 품목코드
itemName: string; // 품목명
supplier: string; // 공급업체
orderQty: number; // 발주수량
orderUnit: string; // 발주단위
receivingQty?: number; // 입고수량
lotNo?: string; // LOT번호
status: ReceivingStatus; // 상태
}
// 입고 상세 정보
export interface ReceivingDetail {
id: string;
orderNo: string; // 발주번호
orderDate?: string; // 발주일자
supplier: string; // 공급업체
itemCode: string; // 품목코드
itemName: string; // 품목명
specification?: string; // 규격
orderQty: number; // 발주수량
orderUnit: string; // 발주단위
dueDate?: string; // 납기일
status: ReceivingStatus;
// 입고 정보
receivingDate?: string; // 입고일자
receivingQty?: number; // 입고수량
receivingLot?: string; // 입고LOT
supplierLot?: string; // 공급업체LOT
receivingLocation?: string; // 입고위치
receivingManager?: string; // 입고담당
}
// 검사 대상 아이템
export interface InspectionTarget {
id: string;
orderNo: string;
supplier: string;
qty: string;
}
// 검사 항목
export interface InspectionCheckItem {
id: string;
name: string; // 검사항목
specification: string; // 규격
method: string; // 검사방법
judgment: '적' | '부적' | ''; // 판정
remark?: string; // 비고
}
// 검사 등록 폼 데이터
export interface InspectionFormData {
targetId: string; // 검사 대상 ID
inspectionDate: string; // 검사일
inspector: string; // 검사자
lotNo: string; // LOT번호
items: InspectionCheckItem[];
opinion?: string; // 종합 의견
}
// 입고처리 폼 데이터
export interface ReceivingProcessFormData {
receivingLot: string; // 입고LOT *
supplierLot?: string; // 공급업체LOT
receivingQty: number; // 입고수량 *
receivingLocation: string; // 입고위치 *
remark?: string; // 비고
}
// 통계 데이터
export interface ReceivingStats {
receivingPendingCount: number; // 입고대기
shippingCount: number; // 배송중
inspectionPendingCount: number; // 검사대기
todayReceivingCount: number; // 금일입고
}
// 필터 탭
export interface FilterTab {
key: 'all' | 'receiving_pending' | 'completed';
label: string;
count: number;
}