feat(WEB): 자재/출고/생산/품질/단가 기능 대폭 개선 및 신규 페이지 추가

자재관리:
- 입고관리 재고조정 다이얼로그 신규 추가, 상세/목록 기능 확장
- 재고현황 컴포넌트 리팩토링

출고관리:
- 출하관리 생성/수정/목록/상세 개선
- 차량배차관리 상세/수정/목록 기능 보강

생산관리:
- 작업지시서 WIP 생산 모달 신규 추가
- 벤딩WIP/슬랫조인트바 검사 콘텐츠 신규 추가
- 작업자화면 기능 대폭 확장 (카드/목록 개선)
- 검사성적서 모달 개선

품질관리:
- 실적보고서 관리 페이지 신규 추가
- 검사관리 문서/타입/목데이터 개선

단가관리:
- 단가배포 페이지 및 컴포넌트 신규 추가
- 단가표 관리 페이지 및 컴포넌트 신규 추가

공통:
- 권한 시스템 추가 개선 (PermissionContext, usePermission, PermissionGuard)
- 메뉴 폴링 훅 개선, 레이아웃 수정
- 모바일 줌/패닝 CSS 수정
- locale 유틸 추가

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
유병철
2026-02-04 12:46:19 +09:00
parent 17c16028b1
commit c1b63b850a
70 changed files with 6832 additions and 384 deletions

View File

@@ -0,0 +1,235 @@
'use client';
/**
* 재고 조정 팝업 (기획서 Page 78)
*
* - 품목 선택 (검색)
* - 유형 필터 셀렉트 박스 (전체, 유형 목록)
* - 테이블: 로트번호, 품목코드, 품목유형, 품목명, 규격, 단위, 재고량, 증감 수량
* - 증감 수량: 양수/음수 입력 가능
* - 취소/저장 버튼
*/
import { useState, useMemo, useCallback } from 'react';
import { Search, Loader2 } from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Input } from '@/components/ui/input';
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
} from '@/components/ui/dialog';
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@/components/ui/select';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import { toast } from 'sonner';
import type { InventoryAdjustmentItem } from './types';
// 목데이터 - 품목 유형 목록
const ITEM_TYPE_OPTIONS = [
{ value: 'all', label: '전체' },
{ value: 'raw', label: '원자재' },
{ value: 'sub', label: '부자재' },
{ value: 'part', label: '부품' },
{ value: 'product', label: '완제품' },
];
// 목데이터 - 재고 품목 목록
const MOCK_STOCK_ITEMS: InventoryAdjustmentItem[] = [
{ id: '1', lotNo: 'LOT-2026-001', itemCode: 'STEEL-001', itemType: '원자재', itemName: 'SUS304 스테인리스 판재', specification: '1000x2000x3T', unit: 'EA', stockQty: 100 },
{ id: '2', lotNo: 'LOT-2026-002', itemCode: 'ELEC-002', itemType: '부품', itemName: 'MCU 컨트롤러 IC', specification: 'STM32F103C8T6', unit: 'EA', stockQty: 500 },
{ id: '3', lotNo: 'LOT-2026-003', itemCode: 'PLAS-003', itemType: '부자재', itemName: 'ABS 사출 케이스', specification: '150x100x50', unit: 'SET', stockQty: 200 },
{ id: '4', lotNo: 'LOT-2026-004', itemCode: 'STEEL-002', itemType: '원자재', itemName: '알루미늄 프로파일', specification: '40x40x2000L', unit: 'EA', stockQty: 50 },
{ id: '5', lotNo: 'LOT-2026-005', itemCode: 'ELEC-005', itemType: '부품', itemName: 'DC 모터 24V', specification: '24V 100RPM', unit: 'EA', stockQty: 80 },
{ id: '6', lotNo: 'LOT-2026-006', itemCode: 'CHEM-001', itemType: '부자재', itemName: '에폭시 접착제', specification: '500ml', unit: 'EA', stockQty: 300 },
{ id: '7', lotNo: 'LOT-2026-007', itemCode: 'ELEC-007', itemType: '부품', itemName: '커패시터 100uF', specification: '100uF 50V', unit: 'EA', stockQty: 1000 },
];
interface Props {
open: boolean;
onOpenChange: (open: boolean) => void;
onSave?: (items: InventoryAdjustmentItem[]) => void;
}
export function InventoryAdjustmentDialog({ open, onOpenChange, onSave }: Props) {
const [search, setSearch] = useState('');
const [typeFilter, setTypeFilter] = useState('all');
const [adjustments, setAdjustments] = useState<Record<string, number | undefined>>({});
const [isSaving, setIsSaving] = useState(false);
// 필터링된 품목 목록
const filteredItems = useMemo(() => {
return MOCK_STOCK_ITEMS.filter((item) => {
const matchesSearch = !search ||
item.itemCode.toLowerCase().includes(search.toLowerCase()) ||
item.itemName.toLowerCase().includes(search.toLowerCase()) ||
item.lotNo.toLowerCase().includes(search.toLowerCase());
const matchesType = typeFilter === 'all' || item.itemType === ITEM_TYPE_OPTIONS.find(o => o.value === typeFilter)?.label;
return matchesSearch && matchesType;
});
}, [search, typeFilter]);
// 증감 수량 변경
const handleAdjustmentChange = useCallback((itemId: string, value: string) => {
const numValue = value === '' || value === '-' ? undefined : Number(value);
setAdjustments((prev) => ({
...prev,
[itemId]: numValue,
}));
}, []);
// 저장
const handleSave = async () => {
const itemsWithAdjustment = MOCK_STOCK_ITEMS
.filter((item) => adjustments[item.id] !== undefined && adjustments[item.id] !== 0)
.map((item) => ({ ...item, adjustmentQty: adjustments[item.id] }));
if (itemsWithAdjustment.length === 0) {
toast.error('증감 수량을 입력해주세요.');
return;
}
setIsSaving(true);
try {
onSave?.(itemsWithAdjustment);
toast.success('재고 조정이 저장되었습니다.');
handleClose();
} catch {
toast.error('저장 중 오류가 발생했습니다.');
} finally {
setIsSaving(false);
}
};
// 닫기 (상태 초기화)
const handleClose = () => {
setSearch('');
setTypeFilter('all');
setAdjustments({});
onOpenChange(false);
};
return (
<Dialog open={open} onOpenChange={handleClose}>
<DialogContent className="max-w-[700px] max-h-[80vh] flex flex-col">
<DialogHeader>
<DialogTitle className="text-lg font-semibold"> </DialogTitle>
</DialogHeader>
<div className="flex flex-col gap-4 flex-1 min-h-0">
{/* 품목 선택 - 검색 */}
<div className="font-medium text-sm"> </div>
<div className="relative">
<Input
value={search}
onChange={(e) => setSearch(e.target.value)}
placeholder="품목코드, 품목명, 로트번호 검색"
className="pr-10"
/>
<Search className="absolute right-3 top-1/2 -translate-y-1/2 w-4 h-4 text-muted-foreground" />
</div>
{/* 총 건수 + 유형 필터 */}
<div className="flex items-center justify-between">
<span className="text-sm text-muted-foreground">
<strong>{filteredItems.length}</strong>
</span>
<Select value={typeFilter} onValueChange={setTypeFilter}>
<SelectTrigger className="w-[120px] h-8 text-sm">
<SelectValue />
</SelectTrigger>
<SelectContent>
{ITEM_TYPE_OPTIONS.map((option) => (
<SelectItem key={option.value} value={option.value}>
{option.label}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
{/* 테이블 */}
<div className="flex-1 overflow-auto border rounded-md">
<Table>
<TableHeader>
<TableRow className="bg-gray-50">
<TableHead className="text-center w-[100px]"></TableHead>
<TableHead className="text-center w-[90px]"></TableHead>
<TableHead className="text-center w-[70px]"></TableHead>
<TableHead className="min-w-[120px]"></TableHead>
<TableHead className="w-[90px]"></TableHead>
<TableHead className="text-center w-[50px]"></TableHead>
<TableHead className="text-center w-[70px]"></TableHead>
<TableHead className="text-center w-[90px]"> </TableHead>
</TableRow>
</TableHeader>
<TableBody>
{filteredItems.map((item) => (
<TableRow key={item.id}>
<TableCell className="text-center text-sm">{item.lotNo}</TableCell>
<TableCell className="text-center text-sm">{item.itemCode}</TableCell>
<TableCell className="text-center text-sm">{item.itemType}</TableCell>
<TableCell className="text-sm">{item.itemName}</TableCell>
<TableCell className="text-sm">{item.specification}</TableCell>
<TableCell className="text-center text-sm">{item.unit}</TableCell>
<TableCell className="text-center text-sm">{item.stockQty}</TableCell>
<TableCell className="text-center">
<Input
type="number"
value={adjustments[item.id] ?? ''}
onChange={(e) => handleAdjustmentChange(item.id, e.target.value)}
className="h-8 text-sm text-center w-[80px] mx-auto"
placeholder="0"
/>
</TableCell>
</TableRow>
))}
{filteredItems.length === 0 && (
<TableRow>
<TableCell colSpan={8} className="text-center py-8 text-muted-foreground">
.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
{/* 하단 버튼 */}
<div className="flex justify-center gap-3 pt-2">
<Button
variant="outline"
onClick={handleClose}
className="min-w-[120px]"
disabled={isSaving}
>
</Button>
<Button
variant="default"
onClick={handleSave}
className="min-w-[120px] bg-gray-900 text-white hover:bg-gray-800"
disabled={isSaving}
>
{isSaving && <Loader2 className="w-4 h-4 mr-1 animate-spin" />}
</Button>
</div>
</div>
</DialogContent>
</Dialog>
);
}

View File

@@ -16,7 +16,7 @@
import { useState, useCallback, useEffect } from 'react';
import { useRouter } from 'next/navigation';
import { Upload, FileText, Search, X } from 'lucide-react';
import { Upload, FileText, Search, X, Plus } from 'lucide-react';
import { FileDropzone } from '@/components/ui/file-dropzone';
import { ItemSearchModal } from '@/components/quotes/ItemSearchModal';
import { InspectionModalV2 } from '@/app/[locale]/(protected)/quality/qms/components/InspectionModalV2';
@@ -40,10 +40,19 @@ import {
createReceiving,
updateReceiving,
} from './actions';
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from '@/components/ui/table';
import {
RECEIVING_STATUS_OPTIONS,
type ReceivingDetail as ReceivingDetailType,
type ReceivingStatus,
type InventoryAdjustmentRecord,
} from './types';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
import { toast } from 'sonner';
@@ -56,6 +65,7 @@ interface Props {
// 초기 폼 데이터
const INITIAL_FORM_DATA: Partial<ReceivingDetailType> = {
materialNo: '',
lotNo: '',
itemCode: '',
itemName: '',
@@ -71,6 +81,7 @@ const INITIAL_FORM_DATA: Partial<ReceivingDetailType> = {
inspectionDate: '',
inspectionResult: '',
certificateFile: undefined,
inventoryAdjustments: [],
};
// 로트번호 생성 (YYMMDD-NN)
@@ -121,6 +132,9 @@ export function ReceivingDetail({ id, mode = 'view' }: Props) {
const [isItemSearchOpen, setIsItemSearchOpen] = useState(false);
const [isSupplierSearchOpen, setIsSupplierSearchOpen] = useState(false);
// 재고 조정 이력 상태
const [adjustments, setAdjustments] = useState<InventoryAdjustmentRecord[]>([]);
// Dev 모드 폼 자동 채우기
useDevFill(
'receiving',
@@ -159,9 +173,14 @@ export function ReceivingDetail({ id, mode = 'view' }: Props) {
if (result.success && result.data) {
setDetail(result.data);
// 재고 조정 이력 설정
if (result.data.inventoryAdjustments) {
setAdjustments(result.data.inventoryAdjustments);
}
// 수정 모드일 때 폼 데이터 설정
if (isEditMode) {
setFormData({
materialNo: result.data.materialNo || '',
lotNo: result.data.lotNo || '',
itemCode: result.data.itemCode,
itemName: result.data.itemName,
@@ -239,6 +258,30 @@ export function ReceivingDetail({ id, mode = 'view' }: Props) {
setIsInspectionModalOpen(true);
};
// 재고 조정 행 추가
const handleAddAdjustment = () => {
const newRecord: InventoryAdjustmentRecord = {
id: `adj-${Date.now()}`,
adjustmentDate: new Date().toISOString().split('T')[0],
quantity: 0,
inspector: getLoggedInUserName() || '홍길동',
};
setAdjustments((prev) => [...prev, newRecord]);
};
// 재고 조정 행 삭제
const handleRemoveAdjustment = (adjId: string) => {
setAdjustments((prev) => prev.filter((a) => a.id !== adjId));
};
// 재고 조정 수량 변경
const handleAdjustmentQtyChange = (adjId: string, value: string) => {
const numValue = value === '' || value === '-' ? 0 : Number(value);
setAdjustments((prev) =>
prev.map((a) => (a.id === adjId ? { ...a, quantity: numValue } : a))
);
};
// 취소 핸들러 - 등록 모드면 목록으로, 수정 모드면 상세로 이동
const handleCancel = () => {
if (isNewMode) {
@@ -277,6 +320,7 @@ export function ReceivingDetail({ id, mode = 'view' }: Props) {
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{renderReadOnlyField('자재번호', detail.materialNo)}
{renderReadOnlyField('로트번호', detail.lotNo)}
{renderReadOnlyField('품목코드', detail.itemCode)}
{renderReadOnlyField('품목명', detail.itemName)}
@@ -293,6 +337,9 @@ export function ReceivingDetail({ id, mode = 'view' }: Props) {
detail.status === 'inspection_completed' ? '검사완료' :
detail.status
)}
</div>
{/* 비고 - 전체 너비 */}
<div className="mt-4">
{renderReadOnlyField('비고', detail.remark)}
</div>
</CardContent>
@@ -317,15 +364,54 @@ export function ReceivingDetail({ id, mode = 'view' }: Props) {
<span>{detail.certificateFileName}</span>
</div>
) : (
'등록된 파일이 없습니다.'
'클릭하여 파일을 찾거나, 마우스로 파일을 끌어오세요.'
)}
</div>
</div>
</CardContent>
</Card>
{/* 재고 조정 */}
<Card>
<CardHeader className="pb-4 flex flex-row items-center justify-between">
<CardTitle className="text-base font-medium"> </CardTitle>
</CardHeader>
<CardContent>
<div className="border rounded-md overflow-hidden">
<Table>
<TableHeader>
<TableRow className="bg-gray-50">
<TableHead className="text-center w-[50px]">No</TableHead>
<TableHead className="text-center min-w-[140px]"></TableHead>
<TableHead className="text-center min-w-[120px]"> </TableHead>
<TableHead className="text-center min-w-[120px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{adjustments.length > 0 ? (
adjustments.map((adj, idx) => (
<TableRow key={adj.id}>
<TableCell className="text-center">{idx + 1}</TableCell>
<TableCell className="text-center">{adj.adjustmentDate}</TableCell>
<TableCell className="text-center">{adj.quantity}</TableCell>
<TableCell className="text-center">{adj.inspector}</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={4} className="text-center py-6 text-muted-foreground">
.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</div>
);
}, [detail]);
}, [detail, adjustments]);
// ===== 등록/수정 폼 콘텐츠 =====
const renderFormContent = useCallback(() => {
@@ -338,6 +424,9 @@ export function ReceivingDetail({ id, mode = 'view' }: Props) {
</CardHeader>
<CardContent>
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{/* 자재번호 - 읽기전용 */}
{renderReadOnlyField('자재번호', formData.materialNo, true)}
{/* 로트번호 - 읽기전용 */}
{renderReadOnlyField('로트번호', formData.lotNo, true)}
@@ -512,17 +601,107 @@ export function ReceivingDetail({ id, mode = 'view' }: Props) {
</div>
</CardContent>
</Card>
{/* 재고 조정 */}
<Card>
<CardHeader className="pb-4 flex flex-row items-center justify-between">
<CardTitle className="text-base font-medium"> </CardTitle>
<Button
type="button"
variant="outline"
size="sm"
onClick={handleAddAdjustment}
>
<Plus className="w-4 h-4 mr-1" />
</Button>
</CardHeader>
<CardContent>
<div className="border rounded-md overflow-hidden">
<Table>
<TableHeader>
<TableRow className="bg-gray-50">
<TableHead className="text-center w-[50px]">No</TableHead>
<TableHead className="text-center min-w-[140px]"></TableHead>
<TableHead className="text-center min-w-[120px]"> </TableHead>
<TableHead className="text-center min-w-[120px]"></TableHead>
<TableHead className="text-center w-[60px]"></TableHead>
</TableRow>
</TableHeader>
<TableBody>
{adjustments.length > 0 ? (
adjustments.map((adj, idx) => (
<TableRow key={adj.id}>
<TableCell className="text-center">{idx + 1}</TableCell>
<TableCell className="text-center">
<Input
type="date"
value={adj.adjustmentDate}
onChange={(e) => {
setAdjustments((prev) =>
prev.map((a) =>
a.id === adj.id ? { ...a, adjustmentDate: e.target.value } : a
)
);
}}
className="h-8 text-sm"
/>
</TableCell>
<TableCell className="text-center">
<Input
type="number"
value={adj.quantity || ''}
onChange={(e) => handleAdjustmentQtyChange(adj.id, e.target.value)}
className="h-8 text-sm text-center w-[100px] mx-auto"
placeholder="0"
/>
</TableCell>
<TableCell className="text-center">{adj.inspector}</TableCell>
<TableCell className="text-center">
<Button
type="button"
variant="ghost"
size="icon"
className="h-7 w-7 text-red-500 hover:text-red-700 hover:bg-red-50"
onClick={() => handleRemoveAdjustment(adj.id)}
>
<X className="w-4 h-4" />
</Button>
</TableCell>
</TableRow>
))
) : (
<TableRow>
<TableCell colSpan={5} className="text-center py-6 text-muted-foreground">
.
</TableCell>
</TableRow>
)}
</TableBody>
</Table>
</div>
</CardContent>
</Card>
</div>
);
}, [formData]);
}, [formData, adjustments]);
// ===== 커스텀 헤더 액션 (view/edit 모드) =====
const customHeaderActions = (isViewMode || isEditMode) && detail ? (
<>
<div className="flex items-center gap-2">
<Button variant="outline" onClick={handleInspection}>
</Button>
</>
{isViewMode && (
<Button
variant="default"
className="bg-gray-900 text-white hover:bg-gray-800"
onClick={() => router.push(`/ko/material/receiving-management/${id}?mode=edit`)}
>
</Button>
)}
</div>
) : undefined;
// 에러 상태 표시 (view/edit 모드에서만)

View File

@@ -20,6 +20,7 @@ import {
ClipboardCheck,
Plus,
Eye,
Settings2,
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Badge } from '@/components/ui/badge';
@@ -35,6 +36,7 @@ import {
type FilterFieldConfig,
} from '@/components/templates/UniversalListPage';
import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard';
import { InventoryAdjustmentDialog } from './InventoryAdjustmentDialog';
import { getReceivings, getReceivingStats } from './actions';
import { RECEIVING_STATUS_LABELS, RECEIVING_STATUS_STYLES } from './types';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
@@ -50,6 +52,9 @@ export function ReceivingList() {
const [stats, setStats] = useState<ReceivingStats | null>(null);
const [totalItems, setTotalItems] = useState(0);
// ===== 재고 조정 팝업 상태 =====
const [isAdjustmentOpen, setIsAdjustmentOpen] = useState(false);
// ===== 날짜 범위 상태 (최근 30일) =====
const today = new Date();
const thirtyDaysAgo = new Date(today.getTime() - 30 * 24 * 60 * 60 * 1000);
@@ -139,7 +144,7 @@ export function ReceivingList() {
const tableFooter = useMemo(
() => (
<TableRow className="bg-gray-50 hover:bg-gray-50">
<TableCell colSpan={15} className="py-3">
<TableCell colSpan={18} className="py-3">
<span className="text-sm text-muted-foreground">
{totalItems}
</span>
@@ -200,20 +205,23 @@ export function ReceivingList() {
},
},
// 테이블 컬럼 (기획서 순서)
// 테이블 컬럼 (기획서 2026-02-03 순서)
columns: [
{ key: 'no', label: 'No.', className: 'w-[60px] text-center' },
{ key: 'no', label: 'No.', className: 'w-[50px] text-center' },
{ key: 'materialNo', label: '자재번호', className: 'w-[100px]' },
{ key: 'lotNo', label: '로트번호', className: 'w-[120px]' },
{ key: 'inspectionStatus', label: '수입검사', className: 'w-[80px] text-center' },
{ key: 'inspectionDate', label: '검사일', className: 'w-[100px] text-center' },
{ key: 'inspectionStatus', label: '수입검사', className: 'w-[70px] text-center' },
{ key: 'inspectionDate', label: '검사일', className: 'w-[90px] text-center' },
{ key: 'supplier', label: '발주처', className: 'min-w-[100px]' },
{ key: 'itemCode', label: '품목코드', className: 'min-w-[120px]' },
{ key: 'itemName', label: '품목', className: 'min-w-[150px]' },
{ key: 'specification', label: '규격', className: 'w-[100px]' },
{ key: 'unit', label: '단위', className: 'w-[60px] text-center' },
{ key: 'receivingQty', label: '입고수량', className: 'w-[80px] text-center' },
{ key: 'receivingDate', label: '입고일', className: 'w-[100px] text-center' },
{ key: 'createdBy', label: '작성자', className: 'w-[80px] text-center' },
{ key: 'manufacturer', label: '제조사', className: 'min-w-[100px]' },
{ key: 'itemCode', label: '품목코드', className: 'min-w-[100px]' },
{ key: 'itemType', label: '품목유형', className: 'w-[80px] text-center' },
{ key: 'itemName', label: '품목명', className: 'min-w-[130px]' },
{ key: 'specification', label: '규격', className: 'w-[90px]' },
{ key: 'unit', label: '단위', className: 'w-[50px] text-center' },
{ key: 'receivingQty', label: '수량', className: 'w-[60px] text-center' },
{ key: 'receivingDate', label: '입고변경일', className: 'w-[100px] text-center' },
{ key: 'createdBy', label: '작성자', className: 'w-[70px] text-center' },
{ key: 'status', label: '상태', className: 'w-[80px] text-center' },
],
@@ -250,17 +258,27 @@ export function ReceivingList() {
// 통계 카드
stats: statCards,
// 헤더 액션 (입고 등록 버튼)
// 헤더 액션 (재고 조정 + 입고 등록 버튼)
headerActions: () => (
<Button
variant="default"
size="sm"
className="bg-gray-900 text-white hover:bg-gray-800"
onClick={handleRegister}
>
<Plus className="w-4 h-4 mr-1" />
</Button>
<div className="flex items-center gap-2">
<Button
variant="outline"
size="sm"
onClick={() => setIsAdjustmentOpen(true)}
>
<Settings2 className="w-4 h-4 mr-1" />
</Button>
<Button
variant="default"
size="sm"
className="bg-gray-900 text-white hover:bg-gray-800"
onClick={handleRegister}
>
<Plus className="w-4 h-4 mr-1" />
</Button>
</div>
),
// 테이블 푸터
@@ -286,12 +304,15 @@ export function ReceivingList() {
/>
</TableCell>
<TableCell className="text-center text-muted-foreground">{globalIndex}</TableCell>
<TableCell>{item.materialNo || '-'}</TableCell>
<TableCell className="font-medium">{item.lotNo || '-'}</TableCell>
<TableCell className="text-center">{item.inspectionStatus || '-'}</TableCell>
<TableCell className="text-center">{item.inspectionDate || '-'}</TableCell>
<TableCell>{item.supplier}</TableCell>
<TableCell>{item.manufacturer || '-'}</TableCell>
<TableCell>{item.itemCode}</TableCell>
<TableCell className="max-w-[150px] truncate">{item.itemName}</TableCell>
<TableCell className="text-center">{item.itemType || '-'}</TableCell>
<TableCell className="max-w-[130px] truncate">{item.itemName}</TableCell>
<TableCell>{item.specification || '-'}</TableCell>
<TableCell className="text-center">{item.unit}</TableCell>
<TableCell className="text-center">
@@ -342,12 +363,14 @@ export function ReceivingList() {
}
infoGrid={
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
<InfoField label="자재번호" value={item.materialNo || '-'} />
<InfoField label="품목코드" value={item.itemCode} />
<InfoField label="품목유형" value={item.itemType || '-'} />
<InfoField label="발주처" value={item.supplier} />
<InfoField label="제조사" value={item.manufacturer || '-'} />
<InfoField label="수입검사" value={item.inspectionStatus || '-'} />
<InfoField label="검사일" value={item.inspectionDate || '-'} />
<InfoField label="입고수량" value={item.receivingQty !== undefined ? `${item.receivingQty}` : '-'} />
<InfoField label="입고일" value={item.receivingDate || '-'} />
<InfoField label="수량" value={item.receivingQty !== undefined ? `${item.receivingQty}` : '-'} />
<InfoField label="입고변경일" value={item.receivingDate || '-'} />
</div>
}
actions={
@@ -376,9 +399,17 @@ export function ReceivingList() {
);
return (
<UniversalListPage
config={config}
onFilterChange={(newFilters) => setFilterValues(newFilters)}
/>
<>
<UniversalListPage
config={config}
onFilterChange={(newFilters) => setFilterValues(newFilters)}
/>
{/* 재고 조정 팝업 */}
<InventoryAdjustmentDialog
open={isAdjustmentOpen}
onOpenChange={setIsAdjustmentOpen}
/>
</>
);
}

View File

@@ -31,11 +31,14 @@ import type {
const MOCK_RECEIVING_LIST: ReceivingItem[] = [
{
id: '1',
materialNo: 'MAT-001',
lotNo: 'LOT-2026-001',
inspectionStatus: '적',
inspectionDate: '2026-01-25',
supplier: '(주)대한철강',
manufacturer: '포스코',
itemCode: 'STEEL-001',
itemType: '원자재',
itemName: 'SUS304 스테인리스 판재',
specification: '1000x2000x3T',
unit: 'EA',
@@ -46,11 +49,14 @@ const MOCK_RECEIVING_LIST: ReceivingItem[] = [
},
{
id: '2',
materialNo: 'MAT-002',
lotNo: 'LOT-2026-002',
inspectionStatus: '적',
inspectionDate: '2026-01-26',
supplier: '삼성전자부품',
manufacturer: '삼성전자',
itemCode: 'ELEC-002',
itemType: '부품',
itemName: 'MCU 컨트롤러 IC',
specification: 'STM32F103C8T6',
unit: 'EA',
@@ -61,11 +67,14 @@ const MOCK_RECEIVING_LIST: ReceivingItem[] = [
},
{
id: '3',
materialNo: 'MAT-003',
lotNo: 'LOT-2026-003',
inspectionStatus: '-',
inspectionDate: undefined,
supplier: '한국플라스틱',
manufacturer: '한국플라스틱',
itemCode: 'PLAS-003',
itemType: '부자재',
itemName: 'ABS 사출 케이스',
specification: '150x100x50',
unit: 'SET',
@@ -76,11 +85,14 @@ const MOCK_RECEIVING_LIST: ReceivingItem[] = [
},
{
id: '4',
materialNo: 'MAT-004',
lotNo: 'LOT-2026-004',
inspectionStatus: '부적',
inspectionDate: '2026-01-27',
supplier: '(주)대한철강',
manufacturer: '포스코',
itemCode: 'STEEL-002',
itemType: '원자재',
itemName: '알루미늄 프로파일',
specification: '40x40x2000L',
unit: 'EA',
@@ -91,11 +103,14 @@ const MOCK_RECEIVING_LIST: ReceivingItem[] = [
},
{
id: '5',
materialNo: 'MAT-005',
lotNo: 'LOT-2026-005',
inspectionStatus: '-',
inspectionDate: undefined,
supplier: '글로벌전자',
manufacturer: '글로벌전자',
itemCode: 'ELEC-005',
itemType: '부품',
itemName: 'DC 모터 24V',
specification: '24V 100RPM',
unit: 'EA',
@@ -106,11 +121,14 @@ const MOCK_RECEIVING_LIST: ReceivingItem[] = [
},
{
id: '6',
materialNo: 'MAT-006',
lotNo: 'LOT-2026-006',
inspectionStatus: '적',
inspectionDate: '2026-01-24',
supplier: '동양화학',
manufacturer: '동양화학',
itemCode: 'CHEM-001',
itemType: '부자재',
itemName: '에폭시 접착제',
specification: '500ml',
unit: 'EA',
@@ -121,11 +139,14 @@ const MOCK_RECEIVING_LIST: ReceivingItem[] = [
},
{
id: '7',
materialNo: 'MAT-007',
lotNo: 'LOT-2026-007',
inspectionStatus: '적',
inspectionDate: '2026-01-28',
supplier: '삼성전자부품',
manufacturer: '삼성전자',
itemCode: 'ELEC-007',
itemType: '부품',
itemName: '커패시터 100uF',
specification: '100uF 50V',
unit: 'EA',
@@ -136,11 +157,14 @@ const MOCK_RECEIVING_LIST: ReceivingItem[] = [
},
{
id: '8',
materialNo: 'MAT-008',
lotNo: 'LOT-2026-008',
inspectionStatus: '-',
inspectionDate: undefined,
supplier: '한국볼트',
manufacturer: '한국볼트',
itemCode: 'BOLT-001',
itemType: '부품',
itemName: 'SUS 볼트 M8x30',
specification: 'M8x30 SUS304',
unit: 'EA',
@@ -158,11 +182,11 @@ const MOCK_RECEIVING_STATS: ReceivingStats = {
inspectionCompletedCount: 5,
};
// 기획서 2026-01-28 기준 상세 목데이터
// 기획서 2026-02-03 기준 상세 목데이터
const MOCK_RECEIVING_DETAIL: Record<string, ReceivingDetail> = {
'1': {
id: '1',
// 기본 정보
materialNo: 'MAT-001',
lotNo: 'LOT-2026-001',
itemCode: 'STEEL-001',
itemName: 'SUS304 스테인리스 판재',
@@ -175,16 +199,21 @@ const MOCK_RECEIVING_DETAIL: Record<string, ReceivingDetail> = {
createdBy: '김철수',
status: 'completed',
remark: '',
// 수입검사 정보
inspectionDate: '2026-01-25',
inspectionResult: '합격',
certificateFile: undefined,
// 하위 호환
inventoryAdjustments: [
{ id: 'adj-1', adjustmentDate: '2026-01-05', quantity: 10, inspector: '홍길동' },
{ id: 'adj-2', adjustmentDate: '2026-01-05', quantity: 5, inspector: '홍길동' },
{ id: 'adj-3', adjustmentDate: '2026-01-05', quantity: -15, inspector: '홍길동' },
{ id: 'adj-4', adjustmentDate: '2026-01-05', quantity: 5, inspector: '홍길동' },
],
orderNo: 'PO-2026-001',
orderUnit: 'EA',
},
'2': {
id: '2',
materialNo: 'MAT-002',
lotNo: 'LOT-2026-002',
itemCode: 'ELEC-002',
itemName: 'MCU 컨트롤러 IC',
@@ -199,11 +228,13 @@ const MOCK_RECEIVING_DETAIL: Record<string, ReceivingDetail> = {
remark: '긴급 입고',
inspectionDate: '2026-01-26',
inspectionResult: '합격',
inventoryAdjustments: [],
orderNo: 'PO-2026-002',
orderUnit: 'EA',
},
'3': {
id: '3',
materialNo: 'MAT-003',
lotNo: 'LOT-2026-003',
itemCode: 'PLAS-003',
itemName: 'ABS 사출 케이스',
@@ -218,11 +249,13 @@ const MOCK_RECEIVING_DETAIL: Record<string, ReceivingDetail> = {
remark: '',
inspectionDate: undefined,
inspectionResult: undefined,
inventoryAdjustments: [],
orderNo: 'PO-2026-003',
orderUnit: 'SET',
},
'4': {
id: '4',
materialNo: 'MAT-004',
lotNo: 'LOT-2026-004',
itemCode: 'STEEL-002',
itemName: '알루미늄 프로파일',
@@ -237,11 +270,13 @@ const MOCK_RECEIVING_DETAIL: Record<string, ReceivingDetail> = {
remark: '검사 진행 중',
inspectionDate: '2026-01-27',
inspectionResult: '불합격',
inventoryAdjustments: [],
orderNo: 'PO-2026-004',
orderUnit: 'EA',
},
'5': {
id: '5',
materialNo: 'MAT-005',
lotNo: 'LOT-2026-005',
itemCode: 'ELEC-005',
itemName: 'DC 모터 24V',
@@ -256,6 +291,7 @@ const MOCK_RECEIVING_DETAIL: Record<string, ReceivingDetail> = {
remark: '',
inspectionDate: undefined,
inspectionResult: undefined,
inventoryAdjustments: [],
orderNo: 'PO-2026-005',
orderUnit: 'EA',
},

View File

@@ -7,5 +7,6 @@ export { ReceivingDetail } from './ReceivingDetail';
export { InspectionCreate } from './InspectionCreate';
export { ReceivingProcessDialog } from './ReceivingProcessDialog';
export { ReceivingReceiptDialog } from './ReceivingReceiptDialog';
export { InventoryAdjustmentDialog } from './InventoryAdjustmentDialog';
export { SuccessDialog } from './SuccessDialog';
export * from './types';

View File

@@ -41,16 +41,19 @@ export const RECEIVING_STATUS_OPTIONS = [
// 입고 목록 아이템
export interface ReceivingItem {
id: string;
materialNo?: string; // 자재번호
lotNo?: string; // 로트번호
inspectionStatus?: string; // 수입검사 (적/부적/-)
inspectionDate?: string; // 검사일
supplier: string; // 발주처
manufacturer?: string; // 제조사
itemCode: string; // 품목코드
itemType?: string; // 품목유형
itemName: string; // 품목명
specification?: string; // 규격
unit: string; // 단위
receivingQty?: number; // 입고수량
receivingDate?: string; // 입고일
receivingQty?: number; // 수량
receivingDate?: string; // 입고변경
createdBy?: string; // 작성자
status: ReceivingStatus; // 상태
// 기존 필드 (하위 호환)
@@ -59,10 +62,11 @@ export interface ReceivingItem {
orderUnit?: string; // 발주단위
}
// 입고 상세 정보 (기획서 2026-01-28 기준)
// 입고 상세 정보 (기획서 2026-02-03 기준)
export interface ReceivingDetail {
id: string;
// 기본 정보
materialNo?: string; // 자재번호 (읽기전용)
lotNo?: string; // 로트번호 (읽기전용)
itemCode: string; // 품목코드 (수정가능)
itemName: string; // 품목명 (읽기전용 - 품목코드 선택 시 자동)
@@ -80,6 +84,8 @@ export interface ReceivingDetail {
inspectionResult?: string; // 검사결과 (읽기전용) - 합격/불합격
certificateFile?: string; // 업체 제공 성적서 자료 (수정가능)
certificateFileName?: string; // 파일명
// 재고 조정 이력
inventoryAdjustments?: InventoryAdjustmentRecord[];
// 기존 필드 (하위 호환)
orderNo?: string; // 발주번호
orderDate?: string; // 발주일자
@@ -92,6 +98,27 @@ export interface ReceivingDetail {
receivingManager?: string; // 입고담당
}
// 재고 조정 이력 레코드 (입고 상세 내)
export interface InventoryAdjustmentRecord {
id: string;
adjustmentDate: string; // 조정일시
quantity: number; // 증감 수량 (양수/음수)
inspector: string; // 검수자
}
// 재고 조정 팝업용 품목 아이템
export interface InventoryAdjustmentItem {
id: string;
lotNo: string; // 로트번호
itemCode: string; // 품목코드
itemType: string; // 품목유형
itemName: string; // 품목명
specification: string; // 규격
unit: string; // 단위
stockQty: number; // 재고량
adjustmentQty?: number; // 증감 수량
}
// 검사 대상 아이템
export interface InspectionTarget {
id: string;