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,342 @@
'use client';
/**
* 재고현황 목록 페이지
* IntegratedListTemplateV2 패턴 적용
*/
import { useState, useMemo, useCallback } from 'react';
import { useRouter } from 'next/navigation';
import {
Package,
CheckCircle2,
Clock,
AlertCircle,
Download,
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 { mockStockItems, mockStats, mockFilterTabs } from './mockData';
import { ITEM_TYPE_LABELS, ITEM_TYPE_STYLES, STOCK_STATUS_LABELS } from './types';
import type { StockItem, ItemType } from './types';
// 페이지당 항목 수
const ITEMS_PER_PAGE = 20;
export function StockStatusList() {
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' | ItemType>('all');
// 통계 카드
const stats: StatCard[] = [
{
label: '전체 품목',
value: `${mockStats.totalItems}`,
icon: Package,
iconColor: 'text-gray-600',
},
{
label: '정상 재고',
value: `${mockStats.normalCount}`,
icon: CheckCircle2,
iconColor: 'text-green-600',
},
{
label: '재고 부족',
value: `${mockStats.lowCount}`,
icon: Clock,
iconColor: 'text-orange-600',
},
{
label: '재고 없음',
value: `${mockStats.outCount}`,
icon: AlertCircle,
iconColor: 'text-red-600',
},
];
// 테이블 컬럼
const tableColumns: TableColumn[] = [
{ key: 'no', label: '번호', className: 'w-[60px] text-center' },
{ key: 'itemCode', label: '품목코드', className: 'min-w-[120px]' },
{ key: 'itemName', label: '품목명', className: 'min-w-[200px]' },
{ key: 'itemType', label: '품목유형', className: 'w-[100px]' },
{ key: 'unit', label: '단위', className: 'w-[60px] text-center' },
{ key: 'stockQty', label: '재고량', className: 'w-[80px] text-center' },
{ key: 'safetyStock', label: '안전재고', className: 'w-[80px] text-center' },
{ key: 'lot', label: 'LOT', className: 'w-[100px] text-center' },
{ key: 'status', label: '상태', className: 'w-[60px] text-center' },
{ key: 'location', label: '위치', className: 'w-[60px] text-center' },
{ key: 'actions', label: '작업', className: 'w-[60px] text-center' },
];
// 필터링된 데이터
const filteredResults = useMemo(() => {
let result = [...mockStockItems];
// 품목유형 필터
if (activeFilter !== 'all') {
result = result.filter((item) => item.itemType === activeFilter);
}
// 검색 필터
if (searchTerm) {
const term = searchTerm.toLowerCase();
result = result.filter(
(item) =>
item.itemCode.toLowerCase().includes(term) ||
item.itemName.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' | ItemType);
setCurrentPage(1);
setSelectedItems(new Set());
};
// 엑셀 다운로드
const handleExcelDownload = useCallback(() => {
console.log('엑셀 다운로드:', filteredResults);
// TODO: 엑셀 다운로드 기능 구현
}, [filteredResults]);
// 상세 보기
const handleView = useCallback((id: string) => {
router.push(`/ko/material/stock-status/${id}`);
}, [router]);
// 재고부족 수 계산
const lowStockCount = useMemo(() => {
return filteredResults.filter((item) => item.status === 'low').length;
}, [filteredResults]);
// 테이블 행 렌더링
const renderTableRow = (item: StockItem, 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.itemCode}</TableCell>
<TableCell className="max-w-[200px] truncate">{item.itemName}</TableCell>
<TableCell>
<Badge className={`text-xs ${ITEM_TYPE_STYLES[item.itemType]}`}>
{ITEM_TYPE_LABELS[item.itemType]}
</Badge>
</TableCell>
<TableCell className="text-center">{item.unit}</TableCell>
<TableCell className="text-center">{item.stockQty}</TableCell>
<TableCell className="text-center">{item.safetyStock}</TableCell>
<TableCell className="text-center">
<div className="flex flex-col items-center">
<span>{item.lotCount}</span>
{item.lotDaysElapsed > 0 && (
<span className="text-xs text-muted-foreground">
{item.lotDaysElapsed}
</span>
)}
</div>
</TableCell>
<TableCell className="text-center">
<span className={item.status === 'low' ? 'text-orange-600 font-medium' : ''}>
{STOCK_STATUS_LABELS[item.status]}
</span>
</TableCell>
<TableCell className="text-center">{item.location}</TableCell>
<TableCell className="text-center" onClick={(e) => e.stopPropagation()}>
{isSelected && (
<Eye
className="w-5 h-5 text-gray-500 hover:text-gray-700 cursor-pointer mx-auto"
onClick={() => handleView(item.id)}
/>
)}
</TableCell>
</TableRow>
);
};
// 모바일 카드 렌더링
const renderMobileCard = (
item: StockItem,
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.itemCode}</Badge>
</>
}
title={item.itemName}
statusBadge={
<Badge className={`text-xs ${ITEM_TYPE_STYLES[item.itemType]}`}>
{ITEM_TYPE_LABELS[item.itemType]}
</Badge>
}
infoGrid={
<div className="grid grid-cols-2 gap-x-4 gap-y-3">
<InfoField label="단위" value={item.unit} />
<InfoField label="위치" value={item.location} />
<InfoField label="재고량" value={`${item.stockQty}`} />
<InfoField label="안전재고" value={`${item.safetyStock}`} />
<InfoField
label="LOT"
value={`${item.lotCount}${item.lotDaysElapsed > 0 ? ` (${item.lotDaysElapsed}일 경과)` : ''}`}
/>
<InfoField
label="상태"
value={STOCK_STATUS_LABELS[item.status]}
className={item.status === 'low' ? 'text-orange-600' : ''}
/>
</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>
)
}
/>
);
};
// 탭 옵션 (TabChip 사용)
const tabs: TabOption[] = mockFilterTabs.map((tab) => ({
value: tab.key,
label: tab.label,
count: tab.count,
}));
// 헤더 액션
const headerActions = (
<Button variant="outline" onClick={handleExcelDownload}>
<Download className="w-4 h-4 mr-1.5" />
</Button>
);
// 하단 요약 (테이블 푸터)
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} / {lowStockCount}
</span>
</TableCell>
</TableRow>
);
return (
<IntegratedListTemplateV2<StockItem>
title="재고 목록"
icon={Package}
headerActions={headerActions}
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}
/>
);
}