feat(WEB): DevToolbar - 견적→수주→작업지시→출하 테스트 자동화 도구
- DevFillContext: 전역 상태 관리 (활성화/페이지 타입/폼 채우기 함수) - DevToolbar: 플로팅 UI 컴포넌트 (토글/자동 채우기 버튼) - useDevFill: 각 폼에서 자동 채우기 함수 등록 커스텀 훅 - 데이터 생성기: 견적/수주/작업지시/출하 샘플 데이터 - 환경변수 제어: NEXT_PUBLIC_DEV_TOOLBAR_ENABLED로 On/Off - 통합: QuoteRegistration, OrderRegistration, WorkOrderCreate, ShipmentCreate - Hydration 불일치 방지: useState 초기값 false + useEffect 패턴
This commit is contained in:
79
src/components/dev/generators/index.ts
Normal file
79
src/components/dev/generators/index.ts
Normal file
@@ -0,0 +1,79 @@
|
||||
/**
|
||||
* 샘플 데이터 생성 공통 유틸리티
|
||||
*/
|
||||
|
||||
// 랜덤 선택
|
||||
export function randomPick<T>(arr: T[]): T {
|
||||
return arr[Math.floor(Math.random() * arr.length)];
|
||||
}
|
||||
|
||||
// 범위 내 랜덤 정수
|
||||
export function randomInt(min: number, max: number): number {
|
||||
return Math.floor(Math.random() * (max - min + 1)) + min;
|
||||
}
|
||||
|
||||
// 범위 내 랜덤 정수 (100 단위)
|
||||
export function randomInt100(min: number, max: number): number {
|
||||
const minH = Math.ceil(min / 100);
|
||||
const maxH = Math.floor(max / 100);
|
||||
return randomInt(minH, maxH) * 100;
|
||||
}
|
||||
|
||||
// 랜덤 전화번호
|
||||
export function randomPhone(): string {
|
||||
const middle = randomInt(1000, 9999);
|
||||
const last = randomInt(1000, 9999);
|
||||
return `010-${middle}-${last}`;
|
||||
}
|
||||
|
||||
// 오늘 기준 N일 후 날짜
|
||||
export function dateAfterDays(days: number): string {
|
||||
const date = new Date();
|
||||
date.setDate(date.getDate() + days);
|
||||
return date.toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
// 오늘 날짜
|
||||
export function today(): string {
|
||||
return new Date().toISOString().split('T')[0];
|
||||
}
|
||||
|
||||
// 랜덤 층수
|
||||
export function randomFloor(): string {
|
||||
const floors = ['B1', '1F', '2F', '3F', '4F', '5F', '6F', '7F', '8F', '9F', '10F'];
|
||||
return randomPick(floors);
|
||||
}
|
||||
|
||||
// 순차 부호 생성 (F001, F002, ...)
|
||||
let codeCounter = 1;
|
||||
export function nextCode(): string {
|
||||
const code = `F${String(codeCounter).padStart(3, '0')}`;
|
||||
codeCounter++;
|
||||
return code;
|
||||
}
|
||||
|
||||
// 부호 카운터 리셋
|
||||
export function resetCodeCounter(): void {
|
||||
codeCounter = 1;
|
||||
}
|
||||
|
||||
// 랜덤 비고
|
||||
export function randomRemark(): string {
|
||||
const remarks = [
|
||||
'특이사항 없음',
|
||||
'긴급 배송 요청',
|
||||
'우천 시 배송 연기',
|
||||
'오전 중 배송 희망',
|
||||
'현장 담당자 부재 시 경비실 전달',
|
||||
'설치 시 안전관리자 필요',
|
||||
'화물용 엘리베이터 사용 가능',
|
||||
'주차 공간 협소, 사전 연락 필수',
|
||||
'',
|
||||
];
|
||||
return randomPick(remarks);
|
||||
}
|
||||
|
||||
// 랜덤 ID 생성 (임시용)
|
||||
export function tempId(): string {
|
||||
return `temp-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`;
|
||||
}
|
||||
99
src/components/dev/generators/orderData.ts
Normal file
99
src/components/dev/generators/orderData.ts
Normal file
@@ -0,0 +1,99 @@
|
||||
/**
|
||||
* 수주 샘플 데이터 생성기
|
||||
*
|
||||
* 수주는 대부분 견적에서 전환되므로,
|
||||
* 견적 선택 후 자동 채움되는 필드 외의 추가 정보만 생성
|
||||
*/
|
||||
|
||||
import {
|
||||
randomPick,
|
||||
randomPhone,
|
||||
dateAfterDays,
|
||||
randomRemark,
|
||||
} from './index';
|
||||
import type { OrderFormData } from '@/components/orders/OrderRegistration';
|
||||
|
||||
// 배송방식
|
||||
const DELIVERY_METHODS = ['direct', 'pickup', 'courier'];
|
||||
|
||||
// 운임비용
|
||||
const SHIPPING_COSTS = ['free', 'prepaid', 'collect', 'negotiable'];
|
||||
|
||||
/**
|
||||
* 수주 추가 정보 생성 (견적 전환 후 채울 필드들)
|
||||
*/
|
||||
export interface GenerateOrderDataOptions {
|
||||
// 견적에서 가져온 기본 정보 (이미 채워진 상태)
|
||||
baseData?: Partial<OrderFormData>;
|
||||
}
|
||||
|
||||
export function generateOrderData(options: GenerateOrderDataOptions = {}): Partial<OrderFormData> {
|
||||
const { baseData = {} } = options;
|
||||
|
||||
// 견적에서 전환된 경우 이미 채워진 필드들은 그대로 유지
|
||||
// 추가로 채워야 할 필드들만 생성
|
||||
|
||||
return {
|
||||
...baseData,
|
||||
|
||||
// 배송 정보
|
||||
expectedShipDate: dateAfterDays(14), // 2주 후
|
||||
expectedShipDateUndecided: false,
|
||||
deliveryRequestDate: dateAfterDays(21), // 3주 후
|
||||
deliveryRequestDateUndecided: false,
|
||||
deliveryMethod: randomPick(DELIVERY_METHODS),
|
||||
shippingCost: randomPick(SHIPPING_COSTS),
|
||||
|
||||
// 수신자 정보 (견적의 담당자 정보와 다를 수 있음)
|
||||
receiver: baseData.manager || randomPick(['김수신', '이수신', '박수신']),
|
||||
receiverContact: baseData.contact || randomPhone(),
|
||||
|
||||
// 주소 (테스트용 기본값)
|
||||
zipCode: '06234',
|
||||
address: '서울특별시 강남구 테헤란로 123',
|
||||
addressDetail: '삼성빌딩 10층',
|
||||
|
||||
// 비고
|
||||
remarks: randomRemark(),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 견적 없이 수주 직접 생성 시 사용
|
||||
*/
|
||||
export function generateOrderDataFull(): OrderFormData {
|
||||
return {
|
||||
// 기본 정보
|
||||
clientId: '',
|
||||
clientName: '테스트 거래처',
|
||||
siteName: '테스트 현장',
|
||||
manager: randomPick(['김담당', '이담당', '박담당']),
|
||||
contact: randomPhone(),
|
||||
|
||||
// 배송 정보
|
||||
expectedShipDate: dateAfterDays(14),
|
||||
expectedShipDateUndecided: false,
|
||||
deliveryRequestDate: dateAfterDays(21),
|
||||
deliveryRequestDateUndecided: false,
|
||||
deliveryMethod: randomPick(DELIVERY_METHODS),
|
||||
shippingCost: randomPick(SHIPPING_COSTS),
|
||||
receiver: randomPick(['김수신', '이수신', '박수신']),
|
||||
receiverContact: randomPhone(),
|
||||
|
||||
// 주소
|
||||
zipCode: '06234',
|
||||
address: '서울특별시 강남구 테헤란로 123',
|
||||
addressDetail: '삼성빌딩 10층',
|
||||
|
||||
// 비고
|
||||
remarks: randomRemark(),
|
||||
|
||||
// 품목 (빈 배열 - 견적 선택 또는 수동 추가 필요)
|
||||
items: [],
|
||||
|
||||
// 금액
|
||||
subtotal: 0,
|
||||
discountRate: 0,
|
||||
totalAmount: 0,
|
||||
};
|
||||
}
|
||||
126
src/components/dev/generators/quoteData.ts
Normal file
126
src/components/dev/generators/quoteData.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
/**
|
||||
* 견적 샘플 데이터 생성기
|
||||
*/
|
||||
|
||||
import {
|
||||
randomPick,
|
||||
randomInt,
|
||||
randomInt100,
|
||||
randomPhone,
|
||||
dateAfterDays,
|
||||
today,
|
||||
randomFloor,
|
||||
nextCode,
|
||||
resetCodeCounter,
|
||||
randomRemark,
|
||||
tempId,
|
||||
} from './index';
|
||||
import type { QuoteFormData, QuoteItem } from '@/components/quotes/QuoteRegistration';
|
||||
import type { Vendor } from '@/components/accounting/VendorManagement';
|
||||
import type { FinishedGoods } from '@/components/quotes/actions';
|
||||
|
||||
// 제품 카테고리
|
||||
const PRODUCT_CATEGORIES = ['SCREEN', 'STEEL'];
|
||||
|
||||
// 가이드레일 설치 유형
|
||||
const GUIDE_RAIL_TYPES = ['wall', 'floor'];
|
||||
|
||||
// 모터 전원
|
||||
const MOTOR_POWERS = ['single', 'three'];
|
||||
|
||||
// 연동제어기
|
||||
const CONTROLLERS = ['basic', 'smart', 'premium'];
|
||||
|
||||
// 작성자 목록 (실제로는 로그인 사용자 사용)
|
||||
const WRITERS = ['드미트리', '김철수', '이영희', '박지민', '최서연'];
|
||||
|
||||
/**
|
||||
* 견적 품목 1개 생성
|
||||
*/
|
||||
export function generateQuoteItem(
|
||||
index: number,
|
||||
products?: FinishedGoods[]
|
||||
): QuoteItem {
|
||||
const category = randomPick(PRODUCT_CATEGORIES);
|
||||
|
||||
// 카테고리에 맞는 제품 필터링
|
||||
let productName = '';
|
||||
if (products && products.length > 0) {
|
||||
const categoryProducts = products.filter(p =>
|
||||
p.categoryCode?.toUpperCase() === category || !p.categoryCode
|
||||
);
|
||||
if (categoryProducts.length > 0) {
|
||||
productName = randomPick(categoryProducts).name;
|
||||
}
|
||||
}
|
||||
|
||||
// 제품명이 없으면 기본값
|
||||
if (!productName) {
|
||||
productName = category === 'SCREEN'
|
||||
? randomPick(['방화스크린 FSC-1', '방화스크린 FSC-2', '방화스크린 FSC-3'])
|
||||
: randomPick(['방화철재셔터 FSD-1', '방화철재셔터 FSD-2']);
|
||||
}
|
||||
|
||||
return {
|
||||
id: tempId(),
|
||||
floor: randomFloor(),
|
||||
code: nextCode(),
|
||||
productCategory: category,
|
||||
productName,
|
||||
openWidth: String(randomInt100(2000, 5000)),
|
||||
openHeight: String(randomInt100(2000, 5000)),
|
||||
guideRailType: randomPick(GUIDE_RAIL_TYPES),
|
||||
motorPower: randomPick(MOTOR_POWERS),
|
||||
controller: randomPick(CONTROLLERS),
|
||||
quantity: randomInt(1, 10),
|
||||
wingSize: '50',
|
||||
inspectionFee: 50000,
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* 견적 폼 데이터 생성
|
||||
*/
|
||||
export interface GenerateQuoteDataOptions {
|
||||
clients?: Vendor[]; // 거래처 목록
|
||||
products?: FinishedGoods[]; // 제품 목록
|
||||
itemCount?: number; // 품목 수 (기본: 1~5개 랜덤)
|
||||
}
|
||||
|
||||
export function generateQuoteData(options: GenerateQuoteDataOptions = {}): QuoteFormData {
|
||||
const { clients = [], products = [], itemCount } = options;
|
||||
|
||||
// 부호 카운터 리셋
|
||||
resetCodeCounter();
|
||||
|
||||
// 거래처 선택
|
||||
let clientId = '';
|
||||
let clientName = '';
|
||||
if (clients.length > 0) {
|
||||
const client = randomPick(clients);
|
||||
clientId = String(client.id);
|
||||
clientName = client.name;
|
||||
}
|
||||
|
||||
// 품목 수 결정
|
||||
const count = itemCount ?? randomInt(1, 5);
|
||||
|
||||
// 품목 생성
|
||||
const items: QuoteItem[] = [];
|
||||
for (let i = 0; i < count; i++) {
|
||||
items.push(generateQuoteItem(i, products));
|
||||
}
|
||||
|
||||
return {
|
||||
registrationDate: today(),
|
||||
writer: randomPick(WRITERS),
|
||||
clientId,
|
||||
clientName,
|
||||
siteName: clientName ? `${clientName} 현장` : '테스트 현장',
|
||||
manager: randomPick(['김담당', '이담당', '박담당', '최담당']),
|
||||
contact: randomPhone(),
|
||||
dueDate: dateAfterDays(7), // 1주일 후
|
||||
remarks: randomRemark(),
|
||||
items,
|
||||
};
|
||||
}
|
||||
77
src/components/dev/generators/shipmentData.ts
Normal file
77
src/components/dev/generators/shipmentData.ts
Normal file
@@ -0,0 +1,77 @@
|
||||
/**
|
||||
* 출하 샘플 데이터 생성기
|
||||
*/
|
||||
|
||||
import {
|
||||
randomPick,
|
||||
today,
|
||||
randomRemark,
|
||||
} from './index';
|
||||
import type {
|
||||
ShipmentCreateFormData,
|
||||
ShipmentPriority,
|
||||
DeliveryMethod,
|
||||
LotOption,
|
||||
LogisticsOption,
|
||||
VehicleTonnageOption,
|
||||
} from '@/components/outbound/ShipmentManagement/types';
|
||||
|
||||
// 우선순위
|
||||
const PRIORITIES: ShipmentPriority[] = ['urgent', 'normal', 'low'];
|
||||
|
||||
// 배송방식
|
||||
const DELIVERY_METHODS: DeliveryMethod[] = ['pickup', 'direct', 'logistics'];
|
||||
|
||||
/**
|
||||
* 출하 폼 데이터 생성
|
||||
*/
|
||||
export interface GenerateShipmentDataOptions {
|
||||
lotOptions?: LotOption[]; // 로트 목록
|
||||
logisticsOptions?: LogisticsOption[]; // 물류사 목록
|
||||
tonnageOptions?: VehicleTonnageOption[]; // 차량 톤수 목록
|
||||
lotNo?: string; // 지정 로트번호 (플로우에서 전달)
|
||||
}
|
||||
|
||||
export function generateShipmentData(
|
||||
options: GenerateShipmentDataOptions = {}
|
||||
): ShipmentCreateFormData {
|
||||
const {
|
||||
lotOptions = [],
|
||||
logisticsOptions = [],
|
||||
tonnageOptions = [],
|
||||
lotNo,
|
||||
} = options;
|
||||
|
||||
// 로트 선택
|
||||
let selectedLotNo = lotNo || '';
|
||||
if (!selectedLotNo && lotOptions.length > 0) {
|
||||
selectedLotNo = randomPick(lotOptions).lotNo;
|
||||
}
|
||||
|
||||
// 배송방식
|
||||
const deliveryMethod = randomPick(DELIVERY_METHODS);
|
||||
|
||||
// 물류사 (물류사 배송일 때만)
|
||||
let logisticsCompany = '';
|
||||
if (deliveryMethod === 'logistics' && logisticsOptions.length > 0) {
|
||||
logisticsCompany = randomPick(logisticsOptions).name;
|
||||
}
|
||||
|
||||
// 차량 톤수
|
||||
let vehicleTonnage = '';
|
||||
if (tonnageOptions.length > 0) {
|
||||
vehicleTonnage = randomPick(tonnageOptions).value;
|
||||
}
|
||||
|
||||
return {
|
||||
lotNo: selectedLotNo,
|
||||
scheduledDate: today(),
|
||||
priority: randomPick(PRIORITIES),
|
||||
deliveryMethod,
|
||||
logisticsCompany,
|
||||
vehicleTonnage,
|
||||
loadingTime: '',
|
||||
loadingManager: '',
|
||||
remarks: randomRemark(),
|
||||
};
|
||||
}
|
||||
46
src/components/dev/generators/workOrderData.ts
Normal file
46
src/components/dev/generators/workOrderData.ts
Normal file
@@ -0,0 +1,46 @@
|
||||
/**
|
||||
* 작업지시 샘플 데이터 생성기
|
||||
*
|
||||
* 작업지시는 수주 선택 후 자동 채움되는 필드 외의 추가 정보만 생성
|
||||
*/
|
||||
|
||||
import {
|
||||
randomPick,
|
||||
randomInt,
|
||||
dateAfterDays,
|
||||
randomRemark,
|
||||
} from './index';
|
||||
import type { ProcessOption } from '@/components/production/WorkOrders/actions';
|
||||
|
||||
/**
|
||||
* 작업지시 추가 정보 생성
|
||||
*/
|
||||
export interface GenerateWorkOrderDataOptions {
|
||||
processOptions?: ProcessOption[]; // 공정 목록
|
||||
}
|
||||
|
||||
export interface WorkOrderFormDataPartial {
|
||||
processId: number | null;
|
||||
shipmentDate: string;
|
||||
priority: number;
|
||||
note: string;
|
||||
}
|
||||
|
||||
export function generateWorkOrderData(
|
||||
options: GenerateWorkOrderDataOptions = {}
|
||||
): WorkOrderFormDataPartial {
|
||||
const { processOptions = [] } = options;
|
||||
|
||||
// 공정 선택 (있으면 랜덤 선택, 없으면 null)
|
||||
let processId: number | null = null;
|
||||
if (processOptions.length > 0) {
|
||||
processId = randomPick(processOptions).id;
|
||||
}
|
||||
|
||||
return {
|
||||
processId,
|
||||
shipmentDate: dateAfterDays(randomInt(7, 21)), // 1~3주 후
|
||||
priority: randomPick([3, 5, 7]), // 높음(3), 보통(5), 낮음(7)
|
||||
note: randomRemark(),
|
||||
};
|
||||
}
|
||||
Reference in New Issue
Block a user