feat(WEB): 자재/출고/생산/품질/단가 기능 대폭 개선 및 신규 페이지 추가
자재관리: - 입고관리 재고조정 다이얼로그 신규 추가, 상세/목록 기능 확장 - 재고현황 컴포넌트 리팩토링 출고관리: - 출하관리 생성/수정/목록/상세 개선 - 차량배차관리 상세/수정/목록 기능 보강 생산관리: - 작업지시서 WIP 생산 모달 신규 추가 - 벤딩WIP/슬랫조인트바 검사 콘텐츠 신규 추가 - 작업자화면 기능 대폭 확장 (카드/목록 개선) - 검사성적서 모달 개선 품질관리: - 실적보고서 관리 페이지 신규 추가 - 검사관리 문서/타입/목데이터 개선 단가관리: - 단가배포 페이지 및 컴포넌트 신규 추가 - 단가표 관리 페이지 및 컴포넌트 신규 추가 공통: - 권한 시스템 추가 개선 (PermissionContext, usePermission, PermissionGuard) - 메뉴 폴링 훅 개선, 레이아웃 수정 - 모바일 줌/패닝 CSS 수정 - locale 유틸 추가 Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user