Files
sam-react-prod/src/components/material/StockStatus/mockData.ts
유병철 a2c3e4c41e refactor(WEB): 프론트엔드 대규모 코드 정리 및 리팩토링
- 미사용 코드 삭제: ThemeContext, itemStore, utils/date.ts, utils/formatAmount.ts
- 유틸리티 이동: date, formatAmount → src/lib/utils/ (중앙 집중화)
- 다수 page.tsx 클라이언트 컴포넌트 패턴 통일
- DateRangeSelector 리팩토링 및 date-range-picker UI 컴포넌트 추가
- ThemeSelect/themeStore Zustand 직접 연동으로 전환
- 건설/회계/영업/품목/출하 등 전반적 컴포넌트 개선
- UniversalListPage, IntegratedListTemplateV2 타입 확장
- 프론트엔드 종합 리뷰 문서 및 개선 체크리스트 추가

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-19 16:30:07 +09:00

658 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 '@/lib/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',
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',
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',
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',
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}`,
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}`,
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}`,
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}`,
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}`,
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}`,
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}`,
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',
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',
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',
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',
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',
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',
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',
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',
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',
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();