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 =
|
export type DevFillPageType =
|
||||||
| 'quote' | 'order' | 'workOrder' | 'workOrderComplete' | 'shipment' // 판매/생산 플로우
|
| 'quote' | 'quoteV2' | 'order' | 'workOrder' | 'workOrderComplete' | 'shipment' // 판매/생산 플로우
|
||||||
| 'deposit' | 'withdrawal' | 'purchaseApproval' | 'cardTransaction' // 회계 플로우
|
| 'deposit' | 'withdrawal' | 'purchaseApproval' | 'cardTransaction' // 회계 플로우
|
||||||
| 'client'; // 기준정보
|
| 'client'; // 기준정보
|
||||||
|
|
||||||
|
|||||||
@@ -43,6 +43,8 @@ import { useDevFillContext, type DevFillPageType } from './DevFillContext';
|
|||||||
// 페이지 경로와 타입 매핑 (pathname만으로 매칭되는 패턴)
|
// 페이지 경로와 타입 매핑 (pathname만으로 매칭되는 패턴)
|
||||||
const PAGE_PATTERNS: { pattern: RegExp; type: DevFillPageType; label: string }[] = [
|
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\/new/, type: 'quote', label: '견적' },
|
||||||
{ pattern: /\/quote-management\/\d+\/edit/, type: 'quote', label: '견적' },
|
{ pattern: /\/quote-management\/\d+\/edit/, type: 'quote', label: '견적' },
|
||||||
{ pattern: /\/order-management-sales\/new/, type: 'order', 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 }[] = [
|
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: 'quote', label: '견적', icon: FileText, path: '/sales/quote-management/new' },
|
||||||
{ type: 'order', label: '수주', icon: ClipboardList, path: '/sales/order-management-sales/new' },
|
{ type: 'order', label: '수주', icon: ClipboardList, path: '/sales/order-management-sales/new' },
|
||||||
{ type: 'workOrder', label: '작업지시', icon: Wrench, path: '/production/work-orders/create' },
|
{ type: 'workOrder', label: '작업지시', icon: Wrench, path: '/production/work-orders/create' },
|
||||||
@@ -192,6 +195,24 @@ export function DevToolbar() {
|
|||||||
{flowData.workOrderId && ` → 작업#${flowData.workOrderId}`}
|
{flowData.workOrderId && ` → 작업#${flowData.workOrderId}`}
|
||||||
</Badge>
|
</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>
|
||||||
<div className="flex items-center gap-1">
|
<div className="flex items-center gap-1">
|
||||||
{hasFlowData && (
|
{hasFlowData && (
|
||||||
|
|||||||
@@ -43,7 +43,8 @@ import {
|
|||||||
} from "./actions";
|
} from "./actions";
|
||||||
import { getClients } from "../accounting/VendorManagement/actions";
|
import { getClients } from "../accounting/VendorManagement/actions";
|
||||||
import { isNextRedirectError } from "@/lib/utils/redirect-error";
|
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 { Vendor } from "../accounting/VendorManagement";
|
||||||
import type { BomMaterial, CalculationResults } from "./types";
|
import type { BomMaterial, CalculationResults } from "./types";
|
||||||
|
|
||||||
@@ -156,22 +157,12 @@ export function QuoteRegistrationV2({
|
|||||||
isLoading = false,
|
isLoading = false,
|
||||||
hideHeader = false,
|
hideHeader = false,
|
||||||
}: QuoteRegistrationV2Props) {
|
}: QuoteRegistrationV2Props) {
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
// 인증 정보
|
|
||||||
// ---------------------------------------------------------------------------
|
|
||||||
const { currentUser } = useAuth();
|
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// 상태
|
// 상태
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
const [formData, setFormData] = useState<QuoteFormDataV2>(() => {
|
const [formData, setFormData] = useState<QuoteFormDataV2>(
|
||||||
const data = initialData || INITIAL_FORM_DATA;
|
initialData || INITIAL_FORM_DATA
|
||||||
// create 모드에서 writer가 비어있으면 현재 사용자명으로 설정
|
);
|
||||||
if (mode === "create" && !data.writer && currentUser?.name) {
|
|
||||||
return { ...data, writer: currentUser.name };
|
|
||||||
}
|
|
||||||
return data;
|
|
||||||
});
|
|
||||||
const [selectedLocationId, setSelectedLocationId] = useState<string | null>(null);
|
const [selectedLocationId, setSelectedLocationId] = useState<string | null>(null);
|
||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [isCalculating, setIsCalculating] = useState(false);
|
const [isCalculating, setIsCalculating] = useState(false);
|
||||||
@@ -184,6 +175,79 @@ export function QuoteRegistrationV2({
|
|||||||
const [isLoadingClients, setIsLoadingClients] = useState(false);
|
const [isLoadingClients, setIsLoadingClients] = useState(false);
|
||||||
const [isLoadingProducts, setIsLoadingProducts] = 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]);
|
}, [formData.locations]);
|
||||||
|
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
// 작성자 자동 설정 (create 모드에서 currentUser 로드 시)
|
// 작성자 자동 설정 (create 모드에서 로그인 사용자 정보 로드)
|
||||||
// ---------------------------------------------------------------------------
|
// ---------------------------------------------------------------------------
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (mode === "create" && !formData.writer && currentUser?.name) {
|
if (mode === "create" && !formData.writer) {
|
||||||
setFormData((prev) => ({ ...prev, writer: currentUser.name }));
|
// 실제 로그인 사용자 정보는 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