feat: DevToolbar 견적V2 채우기 기능 추가
- DevFillContext: quoteV2 페이지 타입 추가 - DevToolbar: test-new/test/[id] 패턴 및 견적V2 버튼 추가 - DevToolbar: 축소 상태에서 채우기 버튼 표시 - QuoteRegistrationV2: DevFill 훅 연동 (1~5개 랜덤 개소 생성) - 층, 부호, 가로, 세로, 제품, 수량 등 랜덤 데이터 - 로그인 사용자 정보(localStorage) 연동
This commit is contained in:
@@ -16,7 +16,7 @@ import React, { createContext, useContext, useState, useCallback, useEffect, Rea
|
||||
|
||||
// 지원하는 페이지 타입
|
||||
export type DevFillPageType =
|
||||
| 'quote' | 'order' | 'workOrder' | 'workOrderComplete' | 'shipment' // 판매/생산 플로우
|
||||
| 'quote' | 'quoteV2' | 'order' | 'workOrder' | 'workOrderComplete' | 'shipment' // 판매/생산 플로우
|
||||
| 'deposit' | 'withdrawal' | 'purchaseApproval' | 'cardTransaction' // 회계 플로우
|
||||
| 'client'; // 기준정보
|
||||
|
||||
|
||||
@@ -43,6 +43,8 @@ import { useDevFillContext, type DevFillPageType } from './DevFillContext';
|
||||
// 페이지 경로와 타입 매핑 (pathname만으로 매칭되는 패턴)
|
||||
const PAGE_PATTERNS: { pattern: RegExp; type: DevFillPageType; label: string }[] = [
|
||||
// 판매/생산 플로우
|
||||
{ pattern: /\/quote-management\/test-new/, type: 'quoteV2', label: '견적V2' },
|
||||
{ pattern: /\/quote-management\/test\/\d+/, type: 'quoteV2', label: '견적V2' },
|
||||
{ pattern: /\/quote-management\/new/, type: 'quote', label: '견적' },
|
||||
{ pattern: /\/quote-management\/\d+\/edit/, type: 'quote', label: '견적' },
|
||||
{ pattern: /\/order-management-sales\/new/, type: 'order', label: '수주' },
|
||||
@@ -67,6 +69,7 @@ const MODE_NEW_PAGES: { pattern: RegExp; type: DevFillPageType; label: string }[
|
||||
|
||||
// 플로우 단계 정의
|
||||
const FLOW_STEPS: { type: DevFillPageType; label: string; icon: typeof FileText; path: string }[] = [
|
||||
{ type: 'quoteV2', label: '견적V2', icon: FileText, path: '/sales/quote-management/test-new' },
|
||||
{ type: 'quote', label: '견적', icon: FileText, path: '/sales/quote-management/new' },
|
||||
{ type: 'order', label: '수주', icon: ClipboardList, path: '/sales/order-management-sales/new' },
|
||||
{ type: 'workOrder', label: '작업지시', icon: Wrench, path: '/production/work-orders/create' },
|
||||
@@ -192,6 +195,24 @@ export function DevToolbar() {
|
||||
{flowData.workOrderId && ` → 작업#${flowData.workOrderId}`}
|
||||
</Badge>
|
||||
)}
|
||||
{/* 축소 상태에서 채우기 버튼 */}
|
||||
{!isExpanded && activePage && hasRegisteredForm(activePage) && (
|
||||
<Button
|
||||
size="sm"
|
||||
variant="default"
|
||||
disabled={isLoading === activePage}
|
||||
className="h-6 bg-yellow-500 hover:bg-yellow-600 text-white text-xs px-2"
|
||||
onClick={() => handleFillForm(activePage)}
|
||||
title="폼 자동 채우기"
|
||||
>
|
||||
{isLoading === activePage ? (
|
||||
<Loader2 className="w-3 h-3 mr-1 animate-spin" />
|
||||
) : (
|
||||
<Play className="w-3 h-3 mr-1" />
|
||||
)}
|
||||
채우기
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
<div className="flex items-center gap-1">
|
||||
{hasFlowData && (
|
||||
|
||||
@@ -43,7 +43,8 @@ import {
|
||||
} from "./actions";
|
||||
import { getClients } from "../accounting/VendorManagement/actions";
|
||||
import { isNextRedirectError } from "@/lib/utils/redirect-error";
|
||||
import { useAuth } from "@/contexts/AuthContext";
|
||||
// 실제 로그인 사용자 정보는 localStorage('user')에 저장됨 (LoginPage.tsx 참조)
|
||||
import { useDevFill } from "@/components/dev/useDevFill";
|
||||
import type { Vendor } from "../accounting/VendorManagement";
|
||||
import type { BomMaterial, CalculationResults } from "./types";
|
||||
|
||||
@@ -156,22 +157,12 @@ export function QuoteRegistrationV2({
|
||||
isLoading = false,
|
||||
hideHeader = false,
|
||||
}: QuoteRegistrationV2Props) {
|
||||
// ---------------------------------------------------------------------------
|
||||
// 인증 정보
|
||||
// ---------------------------------------------------------------------------
|
||||
const { currentUser } = useAuth();
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 상태
|
||||
// ---------------------------------------------------------------------------
|
||||
const [formData, setFormData] = useState<QuoteFormDataV2>(() => {
|
||||
const data = initialData || INITIAL_FORM_DATA;
|
||||
// create 모드에서 writer가 비어있으면 현재 사용자명으로 설정
|
||||
if (mode === "create" && !data.writer && currentUser?.name) {
|
||||
return { ...data, writer: currentUser.name };
|
||||
}
|
||||
return data;
|
||||
});
|
||||
const [formData, setFormData] = useState<QuoteFormDataV2>(
|
||||
initialData || INITIAL_FORM_DATA
|
||||
);
|
||||
const [selectedLocationId, setSelectedLocationId] = useState<string | null>(null);
|
||||
const [isSaving, setIsSaving] = useState(false);
|
||||
const [isCalculating, setIsCalculating] = useState(false);
|
||||
@@ -184,6 +175,79 @@ export function QuoteRegistrationV2({
|
||||
const [isLoadingClients, setIsLoadingClients] = useState(false);
|
||||
const [isLoadingProducts, setIsLoadingProducts] = useState(false);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// DevFill (개발/테스트용 자동 채우기)
|
||||
// ---------------------------------------------------------------------------
|
||||
useDevFill("quoteV2", useCallback(() => {
|
||||
// 랜덤 개소 생성 함수
|
||||
const createRandomLocation = (index: number): LocationItem => {
|
||||
const floors = ["B2", "B1", "1F", "2F", "3F", "4F", "5F", "R"];
|
||||
const codePrefix = ["SD", "FSS", "FD", "SS", "DS"];
|
||||
const guideRailTypes = ["wall", "floor"];
|
||||
const motorPowers = ["single", "three"];
|
||||
const controllers = ["basic", "smart", "premium"];
|
||||
|
||||
const randomFloor = floors[Math.floor(Math.random() * floors.length)];
|
||||
const randomPrefix = codePrefix[Math.floor(Math.random() * codePrefix.length)];
|
||||
const randomWidth = Math.floor(Math.random() * 4000) + 2000; // 2000~6000
|
||||
const randomHeight = Math.floor(Math.random() * 3000) + 2000; // 2000~5000
|
||||
const randomProduct = finishedGoods[Math.floor(Math.random() * finishedGoods.length)];
|
||||
|
||||
return {
|
||||
id: `loc-${Date.now()}-${index}`,
|
||||
floor: randomFloor,
|
||||
code: `${randomPrefix}-${String(index + 1).padStart(2, "0")}`,
|
||||
openWidth: randomWidth,
|
||||
openHeight: randomHeight,
|
||||
productCode: randomProduct?.item_code || "FG-001",
|
||||
productName: randomProduct?.item_name || "방화셔터",
|
||||
quantity: Math.floor(Math.random() * 3) + 1, // 1~3
|
||||
guideRailType: guideRailTypes[Math.floor(Math.random() * guideRailTypes.length)],
|
||||
motorPower: motorPowers[Math.floor(Math.random() * motorPowers.length)],
|
||||
controller: controllers[Math.floor(Math.random() * controllers.length)],
|
||||
wingSize: [50, 60, 70][Math.floor(Math.random() * 3)],
|
||||
inspectionFee: [50000, 60000, 70000][Math.floor(Math.random() * 3)],
|
||||
};
|
||||
};
|
||||
|
||||
// 1~5개 랜덤 개소 생성
|
||||
const locationCount = Math.floor(Math.random() * 5) + 1;
|
||||
const testLocations: LocationItem[] = [];
|
||||
for (let i = 0; i < locationCount; i++) {
|
||||
testLocations.push(createRandomLocation(i));
|
||||
}
|
||||
|
||||
// 로그인 사용자 정보 가져오기
|
||||
let writerName = "";
|
||||
try {
|
||||
const userStr = localStorage.getItem("user");
|
||||
if (userStr) {
|
||||
const user = JSON.parse(userStr);
|
||||
writerName = user?.name || "";
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[DevFill] 사용자 정보 로드 실패:", e);
|
||||
}
|
||||
|
||||
const testData: QuoteFormDataV2 = {
|
||||
registrationDate: new Date().toISOString().split("T")[0],
|
||||
writer: writerName,
|
||||
clientId: clients[0]?.id?.toString() || "",
|
||||
clientName: clients[0]?.company_name || "테스트 거래처",
|
||||
siteName: "테스트 현장",
|
||||
manager: "홍길동",
|
||||
contact: "010-1234-5678",
|
||||
dueDate: new Date(Date.now() + 7 * 24 * 60 * 60 * 1000).toISOString().split("T")[0],
|
||||
remarks: "[DevFill] 테스트 견적입니다.",
|
||||
status: "draft",
|
||||
locations: testLocations,
|
||||
};
|
||||
|
||||
setFormData(testData);
|
||||
setSelectedLocationId(testLocations[0].id);
|
||||
toast.success(`[DevFill] 테스트 데이터가 채워졌습니다. (${locationCount}개 개소)`);
|
||||
}, [clients, finishedGoods]));
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 계산된 값
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -211,13 +275,24 @@ export function QuoteRegistrationV2({
|
||||
}, [formData.locations]);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 작성자 자동 설정 (create 모드에서 currentUser 로드 시)
|
||||
// 작성자 자동 설정 (create 모드에서 로그인 사용자 정보 로드)
|
||||
// ---------------------------------------------------------------------------
|
||||
useEffect(() => {
|
||||
if (mode === "create" && !formData.writer && currentUser?.name) {
|
||||
setFormData((prev) => ({ ...prev, writer: currentUser.name }));
|
||||
if (mode === "create" && !formData.writer) {
|
||||
// 실제 로그인 사용자 정보는 localStorage('user')에 저장됨
|
||||
try {
|
||||
const userStr = localStorage.getItem("user");
|
||||
if (userStr) {
|
||||
const user = JSON.parse(userStr);
|
||||
if (user?.name) {
|
||||
setFormData((prev) => ({ ...prev, writer: user.name }));
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
console.error("[QuoteRegistrationV2] 사용자 정보 로드 실패:", e);
|
||||
}
|
||||
}
|
||||
}, [mode, currentUser?.name, formData.writer]);
|
||||
}, [mode, formData.writer]);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 초기 데이터 로드
|
||||
|
||||
Reference in New Issue
Block a user