2026-01-20 20:38:29 +09:00
|
|
|
'use client';
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* DevToolbar - 개발/테스트용 플로팅 툴바
|
|
|
|
|
*
|
|
|
|
|
* 화면 하단에 플로팅으로 표시되며,
|
|
|
|
|
* 각 단계(견적→수주→작업지시→완료→출하)의 폼을 자동으로 채울 수 있습니다.
|
|
|
|
|
*
|
2026-01-22 16:06:03 +09:00
|
|
|
* 기능:
|
|
|
|
|
* - 현재 페이지에서 활성화: 버튼 클릭 시 폼 자동 채우기
|
|
|
|
|
* - 다른 페이지에서 비활성화: 버튼 클릭 시 해당 페이지로 이동
|
|
|
|
|
*
|
2026-01-20 20:38:29 +09:00
|
|
|
* 환경변수로 활성화/비활성화:
|
|
|
|
|
* NEXT_PUBLIC_DEV_TOOLBAR_ENABLED=true
|
|
|
|
|
*/
|
|
|
|
|
|
|
|
|
|
import { useState } from 'react';
|
2026-01-22 16:06:03 +09:00
|
|
|
import { usePathname, useRouter } from 'next/navigation';
|
2026-01-20 20:38:29 +09:00
|
|
|
import {
|
|
|
|
|
FileText, // 견적
|
|
|
|
|
ClipboardList, // 수주
|
|
|
|
|
Wrench, // 작업지시
|
|
|
|
|
CheckCircle2, // 완료
|
|
|
|
|
Truck, // 출하
|
|
|
|
|
ChevronDown,
|
|
|
|
|
ChevronUp,
|
|
|
|
|
X,
|
|
|
|
|
Loader2,
|
|
|
|
|
Play,
|
|
|
|
|
RotateCcw,
|
|
|
|
|
} from 'lucide-react';
|
|
|
|
|
import { Button } from '@/components/ui/button';
|
|
|
|
|
import { Badge } from '@/components/ui/badge';
|
|
|
|
|
import { useDevFillContext, type DevFillPageType } from './DevFillContext';
|
|
|
|
|
|
|
|
|
|
// 페이지 경로와 타입 매핑
|
|
|
|
|
const PAGE_PATTERNS: { pattern: RegExp; type: DevFillPageType; label: string }[] = [
|
|
|
|
|
{ pattern: /\/quote-management\/new/, type: 'quote', label: '견적' },
|
|
|
|
|
{ pattern: /\/quote-management\/\d+\/edit/, type: 'quote', label: '견적' },
|
|
|
|
|
{ pattern: /\/order-management-sales\/new/, type: 'order', label: '수주' },
|
|
|
|
|
{ pattern: /\/order-management-sales\/\d+\/edit/, type: 'order', label: '수주' },
|
|
|
|
|
{ pattern: /\/work-orders\/create/, type: 'workOrder', label: '작업지시' },
|
|
|
|
|
{ pattern: /\/work-orders\/\d+\/edit/, type: 'workOrder', label: '작업지시' },
|
|
|
|
|
{ pattern: /\/work-orders\/\d+$/, type: 'workOrderComplete', label: '작업완료' },
|
|
|
|
|
{ pattern: /\/shipments\/new/, type: 'shipment', label: '출하' },
|
|
|
|
|
{ pattern: /\/shipments\/\d+\/edit/, type: 'shipment', label: '출하' },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
// 플로우 단계 정의
|
|
|
|
|
const FLOW_STEPS: { type: DevFillPageType; label: string; icon: typeof FileText; path: string }[] = [
|
|
|
|
|
{ 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' },
|
|
|
|
|
{ type: 'workOrderComplete', label: '완료', icon: CheckCircle2, path: '' }, // 상세 페이지에서 처리
|
|
|
|
|
{ type: 'shipment', label: '출하', icon: Truck, path: '/outbound/shipments/new' },
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
export function DevToolbar() {
|
|
|
|
|
const pathname = usePathname();
|
2026-01-22 16:06:03 +09:00
|
|
|
const router = useRouter();
|
2026-01-20 20:38:29 +09:00
|
|
|
const {
|
|
|
|
|
isEnabled,
|
|
|
|
|
isVisible,
|
|
|
|
|
setIsVisible,
|
|
|
|
|
currentPage,
|
|
|
|
|
fillForm,
|
|
|
|
|
hasRegisteredForm,
|
|
|
|
|
flowData,
|
|
|
|
|
clearFlowData,
|
|
|
|
|
} = useDevFillContext();
|
|
|
|
|
|
|
|
|
|
const [isExpanded, setIsExpanded] = useState(true);
|
|
|
|
|
const [isLoading, setIsLoading] = useState<DevFillPageType | null>(null);
|
|
|
|
|
|
2026-01-22 16:06:03 +09:00
|
|
|
// 현재 locale 추출 (유효한 locale만 인식)
|
|
|
|
|
const VALID_LOCALES = ['ko', 'en'];
|
|
|
|
|
const firstSegment = pathname.split('/')[1];
|
|
|
|
|
const locale = VALID_LOCALES.includes(firstSegment) ? firstSegment : '';
|
|
|
|
|
|
2026-01-20 20:38:29 +09:00
|
|
|
// 비활성화 시 렌더링하지 않음
|
|
|
|
|
if (!isEnabled) return null;
|
|
|
|
|
|
|
|
|
|
// 숨김 상태일 때 작은 버튼만 표시
|
|
|
|
|
if (!isVisible) {
|
|
|
|
|
return (
|
|
|
|
|
<div className="fixed bottom-4 right-4 z-[9999]">
|
|
|
|
|
<Button
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="outline"
|
|
|
|
|
className="bg-yellow-100 border-yellow-400 text-yellow-800 hover:bg-yellow-200 shadow-lg"
|
|
|
|
|
onClick={() => setIsVisible(true)}
|
|
|
|
|
>
|
|
|
|
|
<Play className="w-4 h-4 mr-1" />
|
|
|
|
|
Dev
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 현재 페이지 타입 감지
|
|
|
|
|
const detectedPage = PAGE_PATTERNS.find(p => p.pattern.test(pathname));
|
|
|
|
|
const activePage = detectedPage?.type || null;
|
|
|
|
|
|
|
|
|
|
// 폼 채우기 실행
|
|
|
|
|
const handleFillForm = async (pageType: DevFillPageType) => {
|
|
|
|
|
if (!hasRegisteredForm(pageType)) {
|
|
|
|
|
console.warn(`[DevToolbar] Form not registered for: ${pageType}`);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setIsLoading(pageType);
|
|
|
|
|
try {
|
|
|
|
|
await fillForm(pageType, flowData);
|
|
|
|
|
} catch (err) {
|
|
|
|
|
console.error('[DevToolbar] Fill form error:', err);
|
|
|
|
|
} finally {
|
|
|
|
|
setIsLoading(null);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-22 16:06:03 +09:00
|
|
|
// 페이지 이동
|
|
|
|
|
const handleNavigate = (path: string) => {
|
|
|
|
|
if (path) {
|
|
|
|
|
router.push(locale ? `/${locale}${path}` : path);
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
2026-01-20 20:38:29 +09:00
|
|
|
// 플로우 데이터 표시
|
|
|
|
|
const hasFlowData = flowData.quoteId || flowData.orderId || flowData.workOrderId || flowData.lotNo;
|
|
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="fixed bottom-4 left-1/2 -translate-x-1/2 z-[9999]">
|
|
|
|
|
<div className="bg-yellow-50 border-2 border-yellow-400 rounded-lg shadow-2xl overflow-hidden">
|
|
|
|
|
{/* 헤더 */}
|
|
|
|
|
<div className="flex items-center justify-between px-3 py-2 bg-yellow-100 border-b border-yellow-300">
|
|
|
|
|
<div className="flex items-center gap-2">
|
|
|
|
|
<Badge variant="outline" className="bg-yellow-200 border-yellow-500 text-yellow-800">
|
|
|
|
|
DEV MODE
|
|
|
|
|
</Badge>
|
|
|
|
|
{detectedPage && (
|
|
|
|
|
<span className="text-sm text-yellow-700">
|
|
|
|
|
현재: <strong>{detectedPage.label}</strong>
|
|
|
|
|
</span>
|
|
|
|
|
)}
|
|
|
|
|
{hasFlowData && (
|
|
|
|
|
<Badge variant="secondary" className="text-xs">
|
|
|
|
|
{flowData.quoteId && `견적#${flowData.quoteId}`}
|
|
|
|
|
{flowData.orderId && ` → 수주#${flowData.orderId}`}
|
|
|
|
|
{flowData.workOrderId && ` → 작업#${flowData.workOrderId}`}
|
|
|
|
|
</Badge>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
<div className="flex items-center gap-1">
|
|
|
|
|
{hasFlowData && (
|
|
|
|
|
<Button
|
|
|
|
|
size="icon"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
className="h-6 w-6 text-yellow-700 hover:bg-yellow-200"
|
|
|
|
|
onClick={clearFlowData}
|
|
|
|
|
title="플로우 초기화"
|
|
|
|
|
>
|
|
|
|
|
<RotateCcw className="w-3 h-3" />
|
|
|
|
|
</Button>
|
|
|
|
|
)}
|
|
|
|
|
<Button
|
|
|
|
|
size="icon"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
className="h-6 w-6 text-yellow-700 hover:bg-yellow-200"
|
|
|
|
|
onClick={() => setIsExpanded(!isExpanded)}
|
|
|
|
|
>
|
|
|
|
|
{isExpanded ? <ChevronDown className="w-4 h-4" /> : <ChevronUp className="w-4 h-4" />}
|
|
|
|
|
</Button>
|
|
|
|
|
<Button
|
|
|
|
|
size="icon"
|
|
|
|
|
variant="ghost"
|
|
|
|
|
className="h-6 w-6 text-yellow-700 hover:bg-yellow-200"
|
|
|
|
|
onClick={() => setIsVisible(false)}
|
|
|
|
|
>
|
|
|
|
|
<X className="w-4 h-4" />
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* 버튼 영역 */}
|
|
|
|
|
{isExpanded && (
|
|
|
|
|
<div className="flex items-center gap-2 px-3 py-3">
|
|
|
|
|
{FLOW_STEPS.map((step, index) => {
|
|
|
|
|
const Icon = step.icon;
|
|
|
|
|
const isActive = activePage === step.type;
|
|
|
|
|
const isRegistered = hasRegisteredForm(step.type);
|
|
|
|
|
const isCurrentLoading = isLoading === step.type;
|
2026-01-22 16:06:03 +09:00
|
|
|
const hasPath = !!step.path;
|
2026-01-20 20:38:29 +09:00
|
|
|
|
2026-01-22 16:06:03 +09:00
|
|
|
// 완료 버튼은 상세 페이지에서만 활성화 (이동 경로 없음)
|
2026-01-20 20:38:29 +09:00
|
|
|
if (step.type === 'workOrderComplete' && !isActive) {
|
|
|
|
|
return (
|
|
|
|
|
<div key={step.type} className="flex items-center">
|
|
|
|
|
{index > 0 && <span className="text-yellow-400 mx-1">→</span>}
|
|
|
|
|
<Button
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="outline"
|
|
|
|
|
disabled
|
|
|
|
|
className="opacity-50"
|
2026-01-22 16:06:03 +09:00
|
|
|
title="작업지시 상세 페이지에서 사용 가능"
|
2026-01-20 20:38:29 +09:00
|
|
|
>
|
|
|
|
|
<Icon className="w-4 h-4 mr-1" />
|
|
|
|
|
{step.label}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-22 16:06:03 +09:00
|
|
|
// 활성화된 페이지: 폼 채우기
|
|
|
|
|
if (isActive) {
|
|
|
|
|
return (
|
|
|
|
|
<div key={step.type} className="flex items-center">
|
|
|
|
|
{index > 0 && <span className="text-yellow-400 mx-1">→</span>}
|
|
|
|
|
<Button
|
|
|
|
|
size="sm"
|
|
|
|
|
variant="default"
|
|
|
|
|
disabled={!isRegistered || isCurrentLoading}
|
|
|
|
|
className="bg-yellow-500 hover:bg-yellow-600 text-white border-yellow-600"
|
|
|
|
|
onClick={() => handleFillForm(step.type)}
|
|
|
|
|
title="폼 자동 채우기"
|
|
|
|
|
>
|
|
|
|
|
{isCurrentLoading ? (
|
|
|
|
|
<Loader2 className="w-4 h-4 mr-1 animate-spin" />
|
|
|
|
|
) : (
|
|
|
|
|
<Icon className="w-4 h-4 mr-1" />
|
|
|
|
|
)}
|
|
|
|
|
{step.label} 채우기
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 비활성화된 페이지: 해당 페이지로 이동
|
2026-01-20 20:38:29 +09:00
|
|
|
return (
|
|
|
|
|
<div key={step.type} className="flex items-center">
|
|
|
|
|
{index > 0 && <span className="text-yellow-400 mx-1">→</span>}
|
|
|
|
|
<Button
|
|
|
|
|
size="sm"
|
2026-01-22 16:06:03 +09:00
|
|
|
variant="outline"
|
|
|
|
|
disabled={!hasPath}
|
|
|
|
|
className="border-yellow-300 text-yellow-700 hover:bg-yellow-100 hover:border-yellow-500"
|
|
|
|
|
onClick={() => handleNavigate(step.path)}
|
|
|
|
|
title={hasPath ? `${step.label} 페이지로 이동` : '이동 경로 없음'}
|
2026-01-20 20:38:29 +09:00
|
|
|
>
|
2026-01-22 16:06:03 +09:00
|
|
|
<Icon className="w-4 h-4 mr-1" />
|
2026-01-20 20:38:29 +09:00
|
|
|
{step.label}
|
|
|
|
|
</Button>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
})}
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
|
|
|
|
|
{/* 안내 메시지 */}
|
|
|
|
|
{isExpanded && !activePage && (
|
|
|
|
|
<div className="px-3 pb-3">
|
|
|
|
|
<p className="text-xs text-yellow-600">
|
|
|
|
|
견적/수주/작업지시/출하 페이지에서 활성화됩니다
|
|
|
|
|
</p>
|
|
|
|
|
</div>
|
|
|
|
|
)}
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default DevToolbar;
|