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;

View File

@@ -4,13 +4,12 @@
* 재고현황 상세/수정 페이지
*
* 기획서 기준:
* - 기본 정보: 재번호, 품목코드, 품목명, 규격, 단위, 계산 재고량 (읽기 전용)
* - 수정 가능: 실제 재고량, 안전재고, 상태 (사용/미사용)
* - 기본 정보: 재번호, 품목코드, 품목유형, 품목명, 규격, 단위, 재고량 (읽기 전용)
* - 수정 가능: 안전재고, 상태 (사용/미사용)
*/
import { useState, useCallback, useEffect } from 'react';
import { useRouter, useSearchParams } from 'next/navigation';
import { Loader2 } from 'lucide-react';
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/card';
import { Input } from '@/components/ui/input';
import { Label } from '@/components/ui/label';
@@ -25,7 +24,8 @@ import { IntegratedDetailTemplate } from '@/components/templates/IntegratedDetai
import { stockStatusConfig } from './stockStatusConfig';
import { ServerErrorPage } from '@/components/common/ServerErrorPage';
import { getStockById, updateStock } from './actions';
import { USE_STATUS_LABELS } from './types';
import { USE_STATUS_LABELS, ITEM_TYPE_LABELS } from './types';
import type { ItemType } from './types';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
import { toast } from 'sonner';
@@ -38,11 +38,11 @@ interface StockDetailData {
id: string;
stockNumber: string;
itemCode: string;
itemType: ItemType;
itemName: string;
specification: string;
unit: string;
calculatedQty: number;
actualQty: number;
safetyStock: number;
useStatus: 'active' | 'inactive';
}
@@ -59,11 +59,9 @@ export function StockStatusDetail({ id }: StockStatusDetailProps) {
// 폼 데이터 (수정 모드용)
const [formData, setFormData] = useState<{
actualQty: number;
safetyStock: number;
useStatus: 'active' | 'inactive';
}>({
actualQty: 0,
safetyStock: 0,
useStatus: 'active',
});
@@ -86,17 +84,16 @@ export function StockStatusDetail({ id }: StockStatusDetailProps) {
id: data.id,
stockNumber: data.id, // stockNumber가 없으면 id 사용
itemCode: data.itemCode,
itemType: (data.itemType || 'RM') as ItemType,
itemName: data.itemName,
specification: data.specification || '-',
unit: data.unit,
calculatedQty: data.currentStock, // 계산 재고량
actualQty: data.currentStock, // 실제 재고량 (별도 필드 없으면 currentStock 사용)
calculatedQty: data.currentStock, // 재고량
safetyStock: data.safetyStock,
useStatus: data.status === null ? 'active' : 'active', // 기본값
};
setDetail(detailData);
setFormData({
actualQty: detailData.actualQty,
safetyStock: detailData.safetyStock,
useStatus: detailData.useStatus,
});
@@ -140,7 +137,6 @@ export function StockStatusDetail({ id }: StockStatusDetailProps) {
prev
? {
...prev,
actualQty: formData.actualQty,
safetyStock: formData.safetyStock,
useStatus: formData.useStatus,
}
@@ -189,19 +185,19 @@ export function StockStatusDetail({ id }: StockStatusDetailProps) {
</CardHeader>
<CardContent>
<div className="space-y-6">
{/* Row 1: 재번호, 품목코드, 품목명, 규격 */}
{/* Row 1: 재번호, 품목코드, 품목유형, 품목명 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{renderReadOnlyField('재번호', detail.stockNumber)}
{renderReadOnlyField('재번호', detail.stockNumber)}
{renderReadOnlyField('품목코드', detail.itemCode)}
{renderReadOnlyField('품목유형', ITEM_TYPE_LABELS[detail.itemType] || '-')}
{renderReadOnlyField('품목명', detail.itemName)}
{renderReadOnlyField('규격', detail.specification)}
</div>
{/* Row 2: 단위, 계산 재고량, 실제 재고량, 안전재고 */}
{/* Row 2: 규격, 단위, 재고량, 안전재고 */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{renderReadOnlyField('규격', detail.specification)}
{renderReadOnlyField('단위', detail.unit)}
{renderReadOnlyField('계산 재고량', detail.calculatedQty)}
{renderReadOnlyField('실제 재고량', detail.actualQty)}
{renderReadOnlyField('재고량', detail.calculatedQty)}
{renderReadOnlyField('안전재고', detail.safetyStock)}
</div>
@@ -226,33 +222,19 @@ export function StockStatusDetail({ id }: StockStatusDetailProps) {
</CardHeader>
<CardContent>
<div className="space-y-6">
{/* Row 1: 재번호, 품목코드, 품목명, 규격 (읽기 전용) */}
{/* Row 1: 재번호, 품목코드, 품목유형, 품목명 (읽기 전용) */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{renderReadOnlyField('재번호', detail.stockNumber, true)}
{renderReadOnlyField('재번호', detail.stockNumber, true)}
{renderReadOnlyField('품목코드', detail.itemCode, true)}
{renderReadOnlyField('품목유형', ITEM_TYPE_LABELS[detail.itemType] || '-', true)}
{renderReadOnlyField('품목명', detail.itemName, true)}
{renderReadOnlyField('규격', detail.specification, true)}
</div>
{/* Row 2: 단위, 계산 재고량 (읽기 전용) + 실제 재고량, 안전재고 (수정 가능) */}
{/* Row 2: 규격, 단위, 재고량 (읽기 전용) + 안전재고 (수정 가능) */}
<div className="grid grid-cols-1 md:grid-cols-4 gap-4">
{renderReadOnlyField('규격', detail.specification, true)}
{renderReadOnlyField('단위', detail.unit, true)}
{renderReadOnlyField('계산 재고량', detail.calculatedQty, true)}
{/* 실제 재고량 (수정 가능) */}
<div>
<Label htmlFor="actualQty" className="text-sm text-muted-foreground">
</Label>
<Input
id="actualQty"
type="number"
value={formData.actualQty}
onChange={(e) => handleInputChange('actualQty', e.target.value)}
className="mt-1.5 border-gray-300 focus:border-blue-500 focus:ring-blue-500"
min={0}
/>
</div>
{renderReadOnlyField('재고량', detail.calculatedQty, true)}
{/* 안전재고 (수정 가능) */}
<div>
@@ -322,4 +304,4 @@ export function StockStatusDetail({ id }: StockStatusDetailProps) {
onSubmit={async () => { await handleSave(); return { success: true }; }}
/>
);
}
}

View File

@@ -17,7 +17,7 @@ import {
CheckCircle2,
AlertCircle,
Eye,
Loader2,
AlertTriangle,
} from 'lucide-react';
import type { ExcelColumn } from '@/lib/utils/excel-download';
import { Button } from '@/components/ui/button';
@@ -33,11 +33,9 @@ import {
} from '@/components/templates/UniversalListPage';
import { ListMobileCard, InfoField } from '@/components/organisms/MobileCard';
import { getStocks, getStockStats } from './actions';
import { USE_STATUS_LABELS } from './types';
import { USE_STATUS_LABELS, ITEM_TYPE_LABELS } from './types';
import { isNextRedirectError } from '@/lib/utils/redirect-error';
import type { StockItem, StockStats, ItemType, StockStatusType } from './types';
import { ClipboardList } from 'lucide-react';
import { StockAuditModal } from './StockAuditModal';
// 페이지당 항목 수
const ITEMS_PER_PAGE = 20;
@@ -65,10 +63,6 @@ export function StockStatusList() {
useStatus: 'all',
});
// ===== 재고 실사 모달 상태 =====
const [isAuditModalOpen, setIsAuditModalOpen] = useState(false);
const [isAuditLoading, setIsAuditLoading] = useState(false);
// 데이터 로드 함수
const loadData = useCallback(async () => {
try {
@@ -130,30 +124,15 @@ export function StockStatusList() {
router.push(`/ko/material/stock-status/${item.id}?mode=view`);
};
// ===== 재고 실사 버튼 핸들러 =====
const handleStockAudit = () => {
setIsAuditLoading(true);
// 약간의 딜레이 후 모달 오픈 (로딩 UI 표시를 위해)
setTimeout(() => {
setIsAuditModalOpen(true);
setIsAuditLoading(false);
}, 100);
};
// ===== 재고 실사 완료 핸들러 =====
const handleAuditComplete = () => {
loadData(); // 데이터 새로고침
};
// ===== 엑셀 컬럼 정의 =====
const excelColumns: ExcelColumn<StockItem>[] = [
{ header: '재번호', key: 'stockNumber' },
{ header: '재번호', key: 'stockNumber' },
{ header: '품목코드', key: 'itemCode' },
{ header: '품목유형', key: 'itemType', transform: (value) => ITEM_TYPE_LABELS[value as ItemType] || '-' },
{ header: '품목명', key: 'itemName' },
{ header: '규격', key: 'specification' },
{ header: '단위', key: 'unit' },
{ header: '계산 재고량', key: 'calculatedQty' },
{ header: '실제 재고량', key: 'actualQty' },
{ header: '재고량', key: 'calculatedQty' },
{ header: '안전재고', key: 'safetyStock' },
{ header: '상태', key: 'useStatus', transform: (value) => USE_STATUS_LABELS[value as 'active' | 'inactive'] || '-' },
];
@@ -202,11 +181,17 @@ export function StockStatusList() {
iconColor: 'text-green-600',
},
{
label: '재고부족',
label: '재고 부족',
value: `${stockStats?.lowCount || 0}`,
icon: AlertCircle,
iconColor: 'text-red-600',
},
{
label: '안전재고 미달',
value: `${stockStats?.outCount || 0}`,
icon: AlertTriangle,
iconColor: 'text-orange-600',
},
];
// ===== 필터 설정 (전체/사용/미사용) =====
@@ -226,13 +211,13 @@ export function StockStatusList() {
// ===== 테이블 컬럼 =====
const tableColumns = [
{ key: 'no', label: 'No.', className: 'w-[60px] text-center' },
{ key: 'stockNumber', label: '재번호', className: 'w-[100px]' },
{ key: 'stockNumber', label: '재번호', className: 'w-[100px]' },
{ key: 'itemCode', label: '품목코드', className: 'min-w-[100px]' },
{ key: 'itemType', label: '품목유형', className: 'w-[80px]' },
{ key: 'itemName', label: '품목명', className: 'min-w-[150px]' },
{ key: 'specification', label: '규격', className: 'w-[100px]' },
{ key: 'unit', label: '단위', className: 'w-[60px] text-center' },
{ key: 'calculatedQty', label: '계산 재고량', className: 'w-[100px] text-center' },
{ key: 'actualQty', label: '실제 재고량', className: 'w-[100px] text-center' },
{ key: 'calculatedQty', label: '재고량', className: 'w-[80px] text-center' },
{ key: 'safetyStock', label: '안전재고', className: 'w-[80px] text-center' },
{ key: 'useStatus', label: '상태', className: 'w-[80px] text-center' },
];
@@ -259,11 +244,11 @@ export function StockStatusList() {
<TableCell className="text-center text-muted-foreground">{globalIndex}</TableCell>
<TableCell className="font-medium">{item.stockNumber}</TableCell>
<TableCell>{item.itemCode}</TableCell>
<TableCell>{ITEM_TYPE_LABELS[item.itemType] || '-'}</TableCell>
<TableCell className="max-w-[150px] truncate">{item.itemName}</TableCell>
<TableCell>{item.specification || '-'}</TableCell>
<TableCell className="text-center">{item.unit}</TableCell>
<TableCell className="text-center">{item.calculatedQty}</TableCell>
<TableCell className="text-center">{item.actualQty}</TableCell>
<TableCell className="text-center">{item.safetyStock}</TableCell>
<TableCell className="text-center">
<span className={item.useStatus === 'inactive' ? 'text-gray-400' : ''}>
@@ -306,10 +291,10 @@ export function StockStatusList() {
infoGrid={
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
<InfoField label="품목코드" value={item.itemCode} />
<InfoField label="품목유형" value={ITEM_TYPE_LABELS[item.itemType] || '-'} />
<InfoField label="규격" value={item.specification || '-'} />
<InfoField label="단위" value={item.unit} />
<InfoField label="계산 재고량" value={`${item.calculatedQty}`} />
<InfoField label="실제 재고량" value={`${item.actualQty}`} />
<InfoField label="재고량" value={`${item.calculatedQty}`} />
<InfoField label="안전재고" value={`${item.safetyStock}`} />
</div>
}
@@ -401,24 +386,6 @@ export function StockStatusList() {
// 통계
computeStats: () => stats,
// 헤더 액션 버튼
headerActions: () => (
<Button
variant="default"
size="sm"
className="bg-gray-900 text-white hover:bg-gray-800"
onClick={handleStockAudit}
disabled={isAuditLoading}
>
{isAuditLoading ? (
<Loader2 className="w-4 h-4 mr-1 animate-spin" />
) : (
<ClipboardList className="w-4 h-4 mr-1" />
)}
{isAuditLoading ? '로딩 중...' : '재고 실사'}
</Button>
),
// 테이블 푸터
tableFooter: (
<TableRow className="bg-gray-50 hover:bg-gray-50">
@@ -468,22 +435,12 @@ export function StockStatusList() {
}
return (
<>
<UniversalListPage<StockItem>
config={config}
initialData={filteredStocks}
initialTotalCount={filteredStocks.length}
onFilterChange={(newFilters) => setFilterValues(newFilters)}
onSearchChange={setSearchTerm}
/>
{/* 재고 실사 모달 */}
<StockAuditModal
open={isAuditModalOpen}
onOpenChange={setIsAuditModalOpen}
stocks={stocks}
onComplete={handleAuditComplete}
/>
</>
<UniversalListPage<StockItem>
config={config}
initialData={filteredStocks}
initialTotalCount={filteredStocks.length}
onFilterChange={(newFilters) => setFilterValues(newFilters)}
onSearchChange={setSearchTerm}
/>
);
}
}

View File

@@ -439,7 +439,6 @@ export async function getStockById(id: string): Promise<{
export async function updateStock(
id: string,
data: {
actualQty: number;
safetyStock: number;
useStatus: 'active' | 'inactive';
}
@@ -457,7 +456,6 @@ export async function updateStock(
'Content-Type': 'application/json',
},
body: JSON.stringify({
actual_qty: data.actualQty,
safety_stock: data.safetyStock,
is_active: data.useStatus === 'active',
}),