/** * 재고현황 Mock 데이터 */ import type { StockItem, StockDetail, StockStats, FilterTab } from './types'; import { getLocalDateString } from '@/utils/date'; // 재고 상태 결정 함수 function getStockStatus(stockQty: number, safetyStock: number): 'normal' | 'low' | 'out' { if (stockQty === 0) return 'out'; if (stockQty < safetyStock) return 'low'; return 'normal'; } // 시드 기반 의사 난수 생성 (일관된 결과 보장) function seededRandom(seed: number): number { const x = Math.sin(seed) * 10000; return x - Math.floor(x); } function seededInt(seed: number, min: number, max: number): number { return Math.floor(seededRandom(seed) * (max - min + 1)) + min; } // 위치 코드 생성 (시드 기반) function generateLocation(type: string, seed: number): string { const prefixes: Record = { raw_material: ['A'], bent_part: ['C', 'D'], purchased_part: ['I', 'J'], sub_material: ['A', 'B'], consumable: ['B'], }; const prefixList = prefixes[type] || ['A']; const prefixIndex = seededInt(seed, 0, prefixList.length - 1); const prefix = prefixList[prefixIndex]; return `${prefix}-${String(seededInt(seed + 1, 1, 10)).padStart(2, '0')}`; } // 원자재 데이터 (4개) const rawMaterialItems: StockItem[] = [ { id: 'rm-1', stockNumber: 'STK-RM-001', itemCode: 'SCR-FABRIC-WHT-03T', itemName: '스크린원단-백색-0.3T', itemType: 'raw_material', specification: '-', unit: 'm²', calculatedQty: 500, actualQty: 500, stockQty: 500, safetyStock: 100, wipQty: 30, lotCount: 3, lotDaysElapsed: 21, status: 'normal', useStatus: 'active', location: 'A-01', hasStock: true, }, { id: 'rm-2', stockNumber: 'STK-RM-002', itemCode: 'SCR-FABRIC-GRY-03T', itemName: '스크린원단-회색-0.3T', itemType: 'raw_material', specification: '-', unit: 'm²', calculatedQty: 350, actualQty: 350, stockQty: 350, safetyStock: 80, wipQty: 20, lotCount: 2, lotDaysElapsed: 15, status: 'normal', useStatus: 'active', location: 'A-02', hasStock: true, }, { id: 'rm-3', stockNumber: 'STK-RM-003', itemCode: 'SCR-FABRIC-BLK-03T', itemName: '스크린원단-흑색-0.3T', itemType: 'raw_material', specification: '-', unit: 'm²', calculatedQty: 280, actualQty: 280, stockQty: 280, safetyStock: 70, wipQty: 15, lotCount: 2, lotDaysElapsed: 18, status: 'normal', useStatus: 'active', location: 'A-03', hasStock: true, }, { id: 'rm-4', stockNumber: 'STK-RM-004', itemCode: 'SCR-FABRIC-BEI-03T', itemName: '스크린원단-베이지-0.3T', itemType: 'raw_material', specification: '-', unit: 'm²', calculatedQty: 420, actualQty: 420, stockQty: 420, safetyStock: 90, wipQty: 25, lotCount: 4, lotDaysElapsed: 12, status: 'normal', useStatus: 'active', location: 'A-04', hasStock: true, }, ]; // 절곡부품 데이터 (41개) const bentPartItems: StockItem[] = Array.from({ length: 41 }, (_, i) => { const types = ['브라켓', '지지대', '연결판', '고정판', '받침대', '가이드', '프레임']; const variants = ['A', 'B', 'C', 'D', 'E', 'S', 'M', 'L', 'XL']; const type = types[i % types.length]; const variant = variants[i % variants.length]; const seed = i * 100; const stockQty = seededInt(seed, 50, 500); const safetyStock = seededInt(seed + 1, 20, 100); return { id: `bp-${i + 1}`, stockNumber: `STK-BP-${String(i + 1).padStart(3, '0')}`, itemCode: `BENT-${type.toUpperCase().slice(0, 3)}-${variant}-${String(i + 1).padStart(2, '0')}`, itemName: `${type}-${variant}형-${i + 1}`, itemType: 'bent_part' as const, specification: '-', unit: 'EA', calculatedQty: stockQty, actualQty: stockQty, stockQty, safetyStock, wipQty: seededInt(seed + 5, 0, 50), lotCount: seededInt(seed + 2, 1, 5), lotDaysElapsed: seededInt(seed + 3, 0, 45), status: getStockStatus(stockQty, safetyStock), useStatus: 'active' as const, location: generateLocation('bent_part', seed + 4), hasStock: true, }; }); // 구매부품 데이터 (80개) const purchasedPartItems: StockItem[] = [ // 각파이프류 (20개) ...Array.from({ length: 20 }, (_, i) => { const sizes = ['30×30', '40×40', '50×50', '60×60', '75×75']; const lengths = ['3000', '4000', '5000', '6000']; const size = sizes[i % sizes.length]; const length = lengths[i % lengths.length]; const seed = 1000 + i * 10; const stockQty = seededInt(seed, 80, 300); const safetyStock = seededInt(seed + 1, 30, 60); return { id: `pp-sqp-${i + 1}`, stockNumber: `STK-PP-SQP-${String(i + 1).padStart(3, '0')}`, itemCode: `SQP-${size.replace('×', '')}-${length.slice(0, 2)}`, itemName: `각파이프 ${size} L:${length}`, itemType: 'purchased_part' as const, specification: '-', unit: 'EA', calculatedQty: stockQty, actualQty: stockQty, stockQty, safetyStock, wipQty: seededInt(seed + 5, 0, 30), lotCount: seededInt(seed + 2, 2, 5), lotDaysElapsed: seededInt(seed + 3, 0, 40), status: getStockStatus(stockQty, safetyStock), useStatus: 'active' as const, location: 'I-05', hasStock: true, }; }), // 앵글류 (15개) ...Array.from({ length: 15 }, (_, i) => { const sizes = ['50×50', '65×65', '75×75', '90×90', '100×100']; const lengths = ['3000', '4000', '5000']; const size = sizes[i % sizes.length]; const length = lengths[i % lengths.length]; const seed = 2000 + i * 10; const stockQty = seededInt(seed, 60, 200); const safetyStock = seededInt(seed + 1, 20, 50); return { id: `pp-ang-${i + 1}`, stockNumber: `STK-PP-ANG-${String(i + 1).padStart(3, '0')}`, itemCode: `ANG-${size.replace('×', '')}-${length.slice(0, 2)}`, itemName: `앵글 ${size} L:${length}`, itemType: 'purchased_part' as const, specification: '-', unit: 'EA', calculatedQty: stockQty, actualQty: stockQty, stockQty, safetyStock, wipQty: seededInt(seed + 5, 0, 25), lotCount: seededInt(seed + 2, 2, 4), lotDaysElapsed: seededInt(seed + 3, 0, 35), status: getStockStatus(stockQty, safetyStock), useStatus: 'active' as const, location: 'I-04', hasStock: true, }; }), // 전동개폐기류 (10개) ...Array.from({ length: 10 }, (_, i) => { const weights = ['300KG', '500KG', '700KG', '1000KG']; const voltages = ['110V', '220V', '380V']; const types = ['유선', '무선']; const weight = weights[i % weights.length]; const voltage = voltages[i % voltages.length]; const type = types[i % types.length]; const seed = 3000 + i * 10; const stockQty = seededInt(seed, 10, 50); const safetyStock = seededInt(seed + 1, 5, 15); return { id: `pp-motor-${i + 1}`, stockNumber: `STK-PP-MOT-${String(i + 1).padStart(3, '0')}`, itemCode: `MOTOR-${voltage}${weight}${type === '무선' ? '-W' : ''}`, itemName: `전동개폐기-${voltage}${weight}${type}`, itemType: 'purchased_part' as const, specification: '-', unit: 'EA', calculatedQty: stockQty, actualQty: stockQty, stockQty, safetyStock, wipQty: seededInt(seed + 5, 0, 10), lotCount: seededInt(seed + 2, 1, 3), lotDaysElapsed: seededInt(seed + 3, 0, 30), status: getStockStatus(stockQty, safetyStock), useStatus: 'active' as const, location: 'I-01', hasStock: true, }; }), // 볼트/너트류 (15개) ...Array.from({ length: 15 }, (_, i) => { const sizes = ['M6', 'M8', 'M10', 'M12', 'M16']; const lengths = ['20', '30', '40', '50', '60']; const size = sizes[i % sizes.length]; const length = lengths[i % lengths.length]; const seed = 4000 + i * 10; const stockQty = seededInt(seed, 500, 2000); const safetyStock = seededInt(seed + 1, 100, 500); return { id: `pp-bolt-${i + 1}`, stockNumber: `STK-PP-BLT-${String(i + 1).padStart(3, '0')}`, itemCode: `BOLT-${size}-${length}`, itemName: `볼트 ${size}×${length}mm`, itemType: 'purchased_part' as const, specification: '-', unit: 'EA', calculatedQty: stockQty, actualQty: stockQty, stockQty, safetyStock, wipQty: seededInt(seed + 5, 0, 100), lotCount: seededInt(seed + 2, 3, 6), lotDaysElapsed: seededInt(seed + 3, 0, 25), status: getStockStatus(stockQty, safetyStock), useStatus: 'active' as const, location: 'J-01', hasStock: true, }; }), // 베어링류 (10개) ...Array.from({ length: 10 }, (_, i) => { const types = ['6200', '6201', '6202', '6203', '6204', '6205', '6206', '6207', '6208', '6209']; const type = types[i % types.length]; const seed = 5000 + i * 10; const stockQty = seededInt(seed, 30, 150); const safetyStock = seededInt(seed + 1, 10, 40); return { id: `pp-bearing-${i + 1}`, stockNumber: `STK-PP-BRG-${String(i + 1).padStart(3, '0')}`, itemCode: `BEARING-${type}`, itemName: `베어링 ${type}`, itemType: 'purchased_part' as const, specification: '-', unit: 'EA', calculatedQty: stockQty, actualQty: stockQty, stockQty, safetyStock, wipQty: seededInt(seed + 5, 0, 20), lotCount: seededInt(seed + 2, 2, 4), lotDaysElapsed: seededInt(seed + 3, 0, 20), status: getStockStatus(stockQty, safetyStock), useStatus: 'active' as const, location: 'J-02', hasStock: true, }; }), // 스프링류 (10개) ...Array.from({ length: 10 }, (_, i) => { const types = ['인장', '압축', '토션']; const sizes = ['S', 'M', 'L', 'XL']; const type = types[i % types.length]; const size = sizes[i % sizes.length]; const seed = 6000 + i * 10; const stockQty = seededInt(seed, 100, 400); const safetyStock = seededInt(seed + 1, 30, 80); return { id: `pp-spring-${i + 1}`, stockNumber: `STK-PP-SPR-${String(i + 1).padStart(3, '0')}`, itemCode: `SPRING-${type.toUpperCase().slice(0, 2)}-${size}`, itemName: `스프링-${type}-${size}`, itemType: 'purchased_part' as const, specification: '-', unit: 'EA', calculatedQty: stockQty, actualQty: stockQty, stockQty, safetyStock, wipQty: seededInt(seed + 5, 0, 40), lotCount: seededInt(seed + 2, 2, 5), lotDaysElapsed: seededInt(seed + 3, 0, 30), status: getStockStatus(stockQty, safetyStock), useStatus: 'active' as const, location: 'J-03', hasStock: true, }; }), ]; // 부자재 데이터 (7개) const subMaterialItems: StockItem[] = [ { id: 'sm-1', stockNumber: 'STK-SM-001', itemCode: 'SEW-WHT', itemName: '미싱실-백색', itemType: 'sub_material', specification: '-', unit: 'M', calculatedQty: 5000, actualQty: 5000, stockQty: 5000, safetyStock: 1000, wipQty: 100, lotCount: 3, lotDaysElapsed: 28, status: 'normal', useStatus: 'active', location: 'A-04', hasStock: true, }, { id: 'sm-2', stockNumber: 'STK-SM-002', itemCode: 'ALU-BAR', itemName: '하단바-알루미늄', itemType: 'sub_material', specification: '-', unit: 'EA', calculatedQty: 120, actualQty: 120, stockQty: 120, safetyStock: 30, wipQty: 10, lotCount: 1, lotDaysElapsed: 5, status: 'normal', useStatus: 'active', location: 'A-03', hasStock: true, }, { id: 'sm-3', stockNumber: 'STK-SM-003', itemCode: 'END-CAP-STD', itemName: '앤드락-표준', itemType: 'sub_material', specification: '-', unit: 'EA', calculatedQty: 800, actualQty: 800, stockQty: 800, safetyStock: 200, wipQty: 50, lotCount: 2, lotDaysElapsed: 12, status: 'normal', useStatus: 'active', location: 'A-02', hasStock: true, }, { id: 'sm-4', stockNumber: 'STK-SM-004', itemCode: 'SILICON-TRANS', itemName: '실리콘-투명', itemType: 'sub_material', specification: '-', unit: 'EA', calculatedQty: 200, actualQty: 200, stockQty: 200, safetyStock: 50, wipQty: 15, lotCount: 5, lotDaysElapsed: 37, status: 'normal', useStatus: 'active', location: 'B-03', hasStock: true, }, { id: 'sm-5', stockNumber: 'STK-SM-005', itemCode: 'TAPE-DBL-25', itemName: '양면테이프-25mm', itemType: 'sub_material', specification: '-', unit: 'EA', calculatedQty: 150, actualQty: 150, stockQty: 150, safetyStock: 40, wipQty: 8, lotCount: 2, lotDaysElapsed: 10, status: 'normal', useStatus: 'active', location: 'B-02', hasStock: true, }, { id: 'sm-6', stockNumber: 'STK-SM-006', itemCode: 'RIVET-STL-4', itemName: '리벳-스틸-4mm', itemType: 'sub_material', specification: '-', unit: 'EA', calculatedQty: 3000, actualQty: 3000, stockQty: 3000, safetyStock: 500, wipQty: 200, lotCount: 4, lotDaysElapsed: 8, status: 'normal', useStatus: 'active', location: 'B-01', hasStock: true, }, { id: 'sm-7', stockNumber: 'STK-SM-007', itemCode: 'WASHER-M8', itemName: '와셔-M8', itemType: 'sub_material', specification: '-', unit: 'EA', calculatedQty: 2500, actualQty: 2500, stockQty: 2500, safetyStock: 400, wipQty: 150, lotCount: 3, lotDaysElapsed: 15, status: 'normal', useStatus: 'active', location: 'B-04', hasStock: true, }, ]; // 소모품 데이터 (2개) const consumableItems: StockItem[] = [ { id: 'cs-1', stockNumber: 'STK-CS-001', itemCode: 'PKG-BOX-L', itemName: '포장박스-대형', itemType: 'consumable', specification: '-', unit: 'EA', calculatedQty: 200, actualQty: 200, stockQty: 200, safetyStock: 50, wipQty: 20, lotCount: 2, lotDaysElapsed: 8, status: 'normal', useStatus: 'active', location: 'B-01', hasStock: true, }, { id: 'cs-2', stockNumber: 'STK-CS-002', itemCode: 'PKG-BOX-M', itemName: '포장박스-중형', itemType: 'consumable', specification: '-', unit: 'EA', calculatedQty: 350, actualQty: 350, stockQty: 350, safetyStock: 80, wipQty: 30, lotCount: 3, lotDaysElapsed: 5, status: 'normal', useStatus: 'active', location: 'B-02', hasStock: true, }, ]; // 재고 목록 Mock 데이터 (134개) export const mockStockItems: StockItem[] = [ ...rawMaterialItems, // 4개 ...bentPartItems, // 41개 ...purchasedPartItems, // 80개 ...subMaterialItems, // 7개 ...consumableItems, // 2개 ]; // ID로 아이템 찾기 헬퍼 export function findStockItemById(id: string): StockItem | undefined { return mockStockItems.find(item => item.id === id); } // 재고 상세 Mock 데이터 생성 함수 export function generateStockDetail(item: StockItem): StockDetail { const suppliers = ['포스코', '현대제철', '동국제강', '세아제강', '한국철강', '삼성물산']; const locations = ['A-01', 'A-02', 'B-01', 'B-02', 'C-01']; // 시드 기반으로 일관된 LOT 데이터 생성 const itemSeed = parseInt(item.id.replace(/\D/g, '') || '0', 10); // LOT 데이터 생성 const lots = Array.from({ length: item.lotCount }, (_, i) => { const lotSeed = itemSeed * 1000 + i * 100; const daysAgo = seededInt(lotSeed, 5, 60); const date = new Date('2025-12-23'); // 고정 날짜 사용 date.setDate(date.getDate() - daysAgo); const dateStr = getLocalDateString(date); const lotDate = dateStr.replace(/-/g, '').slice(2); return { id: `lot-${item.id}-${i + 1}`, fifoOrder: i + 1, lotNo: `${lotDate}-${String(i + 1).padStart(2, '0')}`, receiptDate: dateStr, daysElapsed: daysAgo, supplier: suppliers[seededInt(lotSeed + 1, 0, suppliers.length - 1)], poNumber: `PO-${lotDate}-${String(seededInt(lotSeed + 2, 1, 99)).padStart(2, '0')}`, qty: Math.floor(item.stockQty / item.lotCount) + (i === 0 ? item.stockQty % item.lotCount : 0), unit: item.unit, location: locations[seededInt(lotSeed + 3, 0, locations.length - 1)], status: 'available' as const, }; }); // 카테고리 추출 const categoryMap: Record = { 'SQP': '각파이프', 'ANG': '앵글', 'MOTOR': '전동개폐기', 'BOLT': '볼트', 'BEARING': '베어링', 'SPRING': '스프링', 'BENT': '절곡부품', 'SCR': '스크린원단', 'SEW': '미싱실', 'ALU': '알루미늄바', 'END': '앤드락', 'SILICON': '실리콘', 'TAPE': '테이프', 'RIVET': '리벳', 'WASHER': '와셔', 'PKG': '포장재', }; const prefix = item.itemCode.split('-')[0]; const category = categoryMap[prefix] || '기타'; return { id: item.id, itemCode: item.itemCode, itemName: item.itemName, itemType: item.itemType, category, specification: '-', unit: item.unit, currentStock: item.stockQty, safetyStock: item.safetyStock, location: item.location, lotCount: item.lotCount, lastReceiptDate: lots[lots.length - 1]?.receiptDate || '2025-12-23', status: item.status, hasStock: item.hasStock, lots, }; } // 재고 상세 Mock 데이터 (동적 생성) export const mockStockDetails: Record = {}; // 상세 데이터 가져오기 함수 export function getStockDetail(id: string): StockDetail | undefined { // 캐시된 데이터가 있으면 반환 if (mockStockDetails[id]) { return mockStockDetails[id]; } // 없으면 생성 const item = findStockItemById(id); if (item) { mockStockDetails[id] = generateStockDetail(item); return mockStockDetails[id]; } return undefined; } // 통계 데이터 계산 const calculateStats = (): StockStats => { const normalCount = mockStockItems.filter(item => item.status === 'normal').length; const lowCount = mockStockItems.filter(item => item.status === 'low').length; const outCount = mockStockItems.filter(item => item.status === 'out').length; const noStockCount = mockStockItems.filter(item => !item.hasStock).length; return { totalItems: mockStockItems.length, normalCount, lowCount, outCount, noStockCount, }; }; export const mockStats: StockStats = calculateStats(); // 필터 탭 데이터 계산 const calculateFilterTabs = (): FilterTab[] => { const rawMaterialCount = mockStockItems.filter(item => item.itemType === 'raw_material').length; const bentPartCount = mockStockItems.filter(item => item.itemType === 'bent_part').length; const purchasedPartCount = mockStockItems.filter(item => item.itemType === 'purchased_part').length; const subMaterialCount = mockStockItems.filter(item => item.itemType === 'sub_material').length; const consumableCount = mockStockItems.filter(item => item.itemType === 'consumable').length; return [ { key: 'all', label: '전체', count: mockStockItems.length }, { key: 'raw_material', label: '원자재', count: rawMaterialCount }, { key: 'bent_part', label: '절곡부품', count: bentPartCount }, { key: 'purchased_part', label: '구매부품', count: purchasedPartCount }, { key: 'sub_material', label: '부자재', count: subMaterialCount }, { key: 'consumable', label: '소모품', count: consumableCount }, ]; }; export const mockFilterTabs: FilterTab[] = calculateFilterTabs();