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

@@ -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}
/>
);
}
}