feat: DevToolbar 견적V2 채우기 기능 추가

- DevFillContext: quoteV2 페이지 타입 추가
- DevToolbar: test-new/test/[id] 패턴 및 견적V2 버튼 추가
- DevToolbar: 축소 상태에서 채우기 버튼 표시
- QuoteRegistrationV2: DevFill 훅 연동 (1~5개 랜덤 개소 생성)
  - 층, 부호, 가로, 세로, 제품, 수량 등 랜덤 데이터
  - 로그인 사용자 정보(localStorage) 연동
This commit is contained in:
2026-01-26 13:39:21 +09:00
parent 49d6e7e271
commit 22d16bbb91
3 changed files with 115 additions and 19 deletions

View File

@@ -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'; // 기준정보

View File

@@ -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 && (

View File

@@ -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]);
// ---------------------------------------------------------------------------
// 초기 데이터 로드