Files
sam-react-prod/src/components/material/StockStatus/mockData.ts
유병철 efcc645e24 feat(WEB): 생산/검사 기능 대폭 확장 및 작업자화면 검사입력 추가
생산관리:
- WipProductionModal 기능 개선
- WorkOrderDetail/Edit 확장 (+265줄)
- 검사성적서 콘텐츠 5종 대폭 확장 (벤딩/벤딩WIP/스크린/슬랫/슬랫조인트바)
- InspectionReportModal 기능 강화

작업자화면:
- WorkerScreen 기능 대폭 확장 (+211줄)
- WorkItemCard 개선
- InspectionInputModal 신규 추가 (작업자 검사입력)

공정관리:
- StepForm 검사항목 설정 기능 추가
- InspectionSettingModal 신규 추가
- InspectionPreviewModal 신규 추가
- process.ts 타입 확장 (+102줄)

자재관리:
- StockStatus 상세/목록/타입/목데이터 개선

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-02-05 21:43:28 +09:00

678 lines
19 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/**
* 재고현황 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<string, string[]> = {
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<string, string> = {
'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<string, StockDetail> = {};
// 상세 데이터 가져오기 함수
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();