- 생산관리: 대시보드, 작업지시, 작업실적, 작업자화면 - 품질관리: 검사관리 (리스트/등록/상세) - 자재관리: 입고관리, 재고현황 - 출고관리: 출하관리 (리스트/등록/상세/수정) - 주문관리: 수주관리, 생산의뢰 - 기존 컴포넌트 개선: CardTransactionInquiry, VendorDetail, QuoteRegistration - IntegratedListTemplateV2 개선 - 공통 컴포넌트 분석 문서 추가 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
859 lines
29 KiB
TypeScript
859 lines
29 KiB
TypeScript
/**
|
||
* 견적 등록/수정 컴포넌트
|
||
*
|
||
* ResponsiveFormTemplate 적용
|
||
* - 기본 정보 섹션
|
||
* - 자동 견적 산출 섹션 (동적 항목 추가/삭제)
|
||
*/
|
||
|
||
"use client";
|
||
|
||
import { useState, useEffect } from "react";
|
||
import { Input } from "../ui/input";
|
||
import { Textarea } from "../ui/textarea";
|
||
import {
|
||
Select,
|
||
SelectContent,
|
||
SelectItem,
|
||
SelectTrigger,
|
||
SelectValue,
|
||
} from "../ui/select";
|
||
import { Button } from "../ui/button";
|
||
import { Card, CardContent, CardHeader, CardTitle } from "../ui/card";
|
||
import { Badge } from "../ui/badge";
|
||
import { Alert, AlertDescription } from "../ui/alert";
|
||
import {
|
||
FileText,
|
||
Calculator,
|
||
Plus,
|
||
Copy,
|
||
Trash2,
|
||
Sparkles,
|
||
} from "lucide-react";
|
||
import { toast } from "sonner";
|
||
|
||
// 필드명 매핑
|
||
const FIELD_NAME_MAP: Record<string, string> = {
|
||
clientId: "발주처",
|
||
productCategory: "제품 카테고리",
|
||
productName: "제품명",
|
||
openWidth: "오픈사이즈(W)",
|
||
openHeight: "오픈사이즈(H)",
|
||
guideRailType: "가이드레일 설치 유형",
|
||
motorPower: "모터 전원",
|
||
controller: "연동제어기",
|
||
quantity: "수량",
|
||
};
|
||
import {
|
||
ResponsiveFormTemplate,
|
||
FormSection,
|
||
FormFieldGrid,
|
||
} from "../templates/ResponsiveFormTemplate";
|
||
import { FormField } from "../molecules/FormField";
|
||
|
||
// 견적 항목 타입
|
||
export interface QuoteItem {
|
||
id: string;
|
||
floor: string; // 층수
|
||
code: string; // 부호
|
||
productCategory: string; // 제품 카테고리 (PC)
|
||
productName: string; // 제품명
|
||
openWidth: string; // 오픈사이즈 W0
|
||
openHeight: string; // 오픈사이즈 H0
|
||
guideRailType: string; // 가이드레일 설치 유형 (GT)
|
||
motorPower: string; // 모터 전원 (MP)
|
||
controller: string; // 연동제어기 (CT)
|
||
quantity: number; // 수량 (QTY)
|
||
wingSize: string; // 마구리 날개치수 (WS)
|
||
inspectionFee: number; // 검사비 (INSP)
|
||
unitPrice?: number; // 단가
|
||
totalAmount?: number; // 합계
|
||
installType?: string; // 설치유형
|
||
}
|
||
|
||
// 견적 폼 데이터 타입
|
||
export interface QuoteFormData {
|
||
id?: string;
|
||
registrationDate: string;
|
||
writer: string;
|
||
clientId: string;
|
||
clientName: string;
|
||
siteName: string; // 현장명 (직접 입력)
|
||
manager: string;
|
||
contact: string;
|
||
dueDate: string;
|
||
remarks: string;
|
||
items: QuoteItem[];
|
||
}
|
||
|
||
// 초기 견적 항목
|
||
const createNewItem = (): QuoteItem => ({
|
||
id: `item-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||
floor: "",
|
||
code: "",
|
||
productCategory: "",
|
||
productName: "",
|
||
openWidth: "",
|
||
openHeight: "",
|
||
guideRailType: "",
|
||
motorPower: "",
|
||
controller: "",
|
||
quantity: 1,
|
||
wingSize: "50",
|
||
inspectionFee: 50000,
|
||
});
|
||
|
||
// 초기 폼 데이터
|
||
export const INITIAL_QUOTE_FORM: QuoteFormData = {
|
||
registrationDate: new Date().toISOString().split("T")[0],
|
||
writer: "드미트리", // TODO: 로그인 사용자 정보로 대체
|
||
clientId: "",
|
||
clientName: "",
|
||
siteName: "", // 현장명 (직접 입력)
|
||
manager: "",
|
||
contact: "",
|
||
dueDate: "",
|
||
remarks: "",
|
||
items: [createNewItem()],
|
||
};
|
||
|
||
// 샘플 발주처 데이터 (TODO: API에서 가져오기)
|
||
const SAMPLE_CLIENTS = [
|
||
{ id: "client-1", name: "인천건설 - 최담당" },
|
||
{ id: "client-2", name: "ABC건설" },
|
||
{ id: "client-3", name: "XYZ산업" },
|
||
];
|
||
|
||
// 제품 카테고리 옵션
|
||
const PRODUCT_CATEGORIES = [
|
||
{ value: "screen", label: "스크린" },
|
||
{ value: "steel", label: "철재" },
|
||
{ value: "aluminum", label: "알루미늄" },
|
||
{ value: "etc", label: "기타" },
|
||
];
|
||
|
||
// 제품명 옵션 (카테고리별)
|
||
const PRODUCTS: Record<string, { value: string; label: string }[]> = {
|
||
screen: [
|
||
{ value: "SCR-001", label: "스크린 A형" },
|
||
{ value: "SCR-002", label: "스크린 B형" },
|
||
{ value: "SCR-003", label: "스크린 C형" },
|
||
],
|
||
steel: [
|
||
{ value: "STL-001", label: "철재 도어 A" },
|
||
{ value: "STL-002", label: "철재 도어 B" },
|
||
],
|
||
aluminum: [
|
||
{ value: "ALU-001", label: "알루미늄 프레임" },
|
||
],
|
||
etc: [
|
||
{ value: "ETC-001", label: "기타 제품" },
|
||
],
|
||
};
|
||
|
||
// 가이드레일 설치 유형
|
||
const GUIDE_RAIL_TYPES = [
|
||
{ value: "wall", label: "벽부착형" },
|
||
{ value: "ceiling", label: "천장매립형" },
|
||
{ value: "floor", label: "바닥매립형" },
|
||
];
|
||
|
||
// 모터 전원
|
||
const MOTOR_POWERS = [
|
||
{ value: "single", label: "단상 220V" },
|
||
{ value: "three", label: "삼상 380V" },
|
||
];
|
||
|
||
// 연동제어기
|
||
const CONTROLLERS = [
|
||
{ value: "basic", label: "기본 제어기" },
|
||
{ value: "smart", label: "스마트 제어기" },
|
||
{ value: "premium", label: "프리미엄 제어기" },
|
||
];
|
||
|
||
interface QuoteRegistrationProps {
|
||
onBack: () => void;
|
||
onSave: (quote: QuoteFormData) => Promise<void>;
|
||
editingQuote?: QuoteFormData | null;
|
||
isLoading?: boolean;
|
||
}
|
||
|
||
export function QuoteRegistration({
|
||
onBack,
|
||
onSave,
|
||
editingQuote,
|
||
isLoading = false,
|
||
}: QuoteRegistrationProps) {
|
||
const [formData, setFormData] = useState<QuoteFormData>(
|
||
editingQuote || INITIAL_QUOTE_FORM
|
||
);
|
||
const [errors, setErrors] = useState<Record<string, string>>({});
|
||
const [isSaving, setIsSaving] = useState(false);
|
||
const [activeItemIndex, setActiveItemIndex] = useState(0);
|
||
|
||
// editingQuote가 변경되면 formData 업데이트
|
||
useEffect(() => {
|
||
if (editingQuote) {
|
||
setFormData(editingQuote);
|
||
}
|
||
}, [editingQuote]);
|
||
|
||
// 유효성 검사
|
||
const validateForm = (): boolean => {
|
||
const newErrors: Record<string, string> = {};
|
||
|
||
if (!formData.clientId) {
|
||
newErrors.clientId = "발주처를 선택해주세요";
|
||
}
|
||
|
||
// 견적 항목 검사
|
||
formData.items.forEach((item, index) => {
|
||
if (!item.productCategory) {
|
||
newErrors[`item-${index}-productCategory`] = "제품 카테고리를 선택해주세요";
|
||
}
|
||
if (!item.productName) {
|
||
newErrors[`item-${index}-productName`] = "제품명을 선택해주세요";
|
||
}
|
||
if (!item.openWidth) {
|
||
newErrors[`item-${index}-openWidth`] = "오픈사이즈(W)를 입력해주세요";
|
||
}
|
||
if (!item.openHeight) {
|
||
newErrors[`item-${index}-openHeight`] = "오픈사이즈(H)를 입력해주세요";
|
||
}
|
||
if (!item.guideRailType) {
|
||
newErrors[`item-${index}-guideRailType`] = "설치 유형을 선택해주세요";
|
||
}
|
||
if (!item.motorPower) {
|
||
newErrors[`item-${index}-motorPower`] = "모터 전원을 선택해주세요";
|
||
}
|
||
if (!item.controller) {
|
||
newErrors[`item-${index}-controller`] = "제어기를 선택해주세요";
|
||
}
|
||
if (item.quantity < 1) {
|
||
newErrors[`item-${index}-quantity`] = "수량은 1 이상이어야 합니다";
|
||
}
|
||
});
|
||
|
||
setErrors(newErrors);
|
||
return Object.keys(newErrors).length === 0;
|
||
};
|
||
|
||
const handleSubmit = async () => {
|
||
if (!validateForm()) {
|
||
// 페이지 상단으로 스크롤
|
||
window.scrollTo({ top: 0, behavior: 'smooth' });
|
||
return;
|
||
}
|
||
|
||
// 에러 초기화
|
||
setErrors({});
|
||
setIsSaving(true);
|
||
try {
|
||
await onSave(formData);
|
||
toast.success(
|
||
editingQuote ? "견적이 수정되었습니다." : "견적이 등록되었습니다."
|
||
);
|
||
onBack();
|
||
} catch (error) {
|
||
toast.error("저장 중 오류가 발생했습니다.");
|
||
} finally {
|
||
setIsSaving(false);
|
||
}
|
||
};
|
||
|
||
const handleFieldChange = (
|
||
field: keyof QuoteFormData,
|
||
value: string | QuoteItem[]
|
||
) => {
|
||
setFormData({ ...formData, [field]: value });
|
||
if (errors[field]) {
|
||
setErrors((prev) => {
|
||
const newErrors = { ...prev };
|
||
delete newErrors[field];
|
||
return newErrors;
|
||
});
|
||
}
|
||
};
|
||
|
||
// 발주처 선택
|
||
const handleClientChange = (clientId: string) => {
|
||
const client = SAMPLE_CLIENTS.find((c) => c.id === clientId);
|
||
setFormData({
|
||
...formData,
|
||
clientId,
|
||
clientName: client?.name || "",
|
||
});
|
||
};
|
||
|
||
// 견적 항목 변경
|
||
const handleItemChange = (
|
||
index: number,
|
||
field: keyof QuoteItem,
|
||
value: string | number
|
||
) => {
|
||
const newItems = [...formData.items];
|
||
newItems[index] = { ...newItems[index], [field]: value };
|
||
|
||
// 제품 카테고리 변경 시 제품명 초기화
|
||
if (field === "productCategory") {
|
||
newItems[index].productName = "";
|
||
}
|
||
|
||
setFormData({ ...formData, items: newItems });
|
||
|
||
// 에러 클리어
|
||
const errorKey = `item-${index}-${field}`;
|
||
if (errors[errorKey]) {
|
||
setErrors((prev) => {
|
||
const newErrors = { ...prev };
|
||
delete newErrors[errorKey];
|
||
return newErrors;
|
||
});
|
||
}
|
||
};
|
||
|
||
// 견적 항목 추가
|
||
const handleAddItem = () => {
|
||
const newItems = [...formData.items, createNewItem()];
|
||
setFormData({ ...formData, items: newItems });
|
||
setActiveItemIndex(newItems.length - 1);
|
||
};
|
||
|
||
// 견적 항목 복사
|
||
const handleCopyItem = (index: number) => {
|
||
const itemToCopy = formData.items[index];
|
||
const newItem: QuoteItem = {
|
||
...itemToCopy,
|
||
id: `item-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||
};
|
||
const newItems = [...formData.items, newItem];
|
||
setFormData({ ...formData, items: newItems });
|
||
setActiveItemIndex(newItems.length - 1);
|
||
toast.success("견적 항목이 복사되었습니다.");
|
||
};
|
||
|
||
// 견적 항목 삭제
|
||
const handleDeleteItem = (index: number) => {
|
||
if (formData.items.length === 1) {
|
||
toast.error("최소 1개의 견적 항목이 필요합니다.");
|
||
return;
|
||
}
|
||
const newItems = formData.items.filter((_, i) => i !== index);
|
||
setFormData({ ...formData, items: newItems });
|
||
if (activeItemIndex >= newItems.length) {
|
||
setActiveItemIndex(newItems.length - 1);
|
||
}
|
||
toast.success("견적 항목이 삭제되었습니다.");
|
||
};
|
||
|
||
// 자동 견적 산출
|
||
const handleAutoCalculate = () => {
|
||
toast.info(`자동 견적 산출 (${formData.items.length}개 항목) - API 연동 필요`);
|
||
};
|
||
|
||
// 샘플 데이터 생성
|
||
const handleGenerateSample = () => {
|
||
toast.info("완벽한 샘플 생성 - API 연동 필요");
|
||
};
|
||
|
||
return (
|
||
<ResponsiveFormTemplate
|
||
title={editingQuote ? "견적 수정" : "견적 등록"}
|
||
description=""
|
||
icon={FileText}
|
||
onSave={handleSubmit}
|
||
onCancel={onBack}
|
||
saveLabel="저장"
|
||
cancelLabel="취소"
|
||
isEditMode={!!editingQuote}
|
||
saveLoading={isSaving || isLoading}
|
||
saveDisabled={isSaving || isLoading}
|
||
maxWidth="2xl"
|
||
>
|
||
{/* Validation 에러 표시 */}
|
||
{Object.keys(errors).length > 0 && (
|
||
<Alert className="bg-red-50 border-red-200 mb-6">
|
||
<AlertDescription className="text-red-900">
|
||
<div className="flex items-start gap-2">
|
||
<span className="text-lg">⚠️</span>
|
||
<div className="flex-1">
|
||
<strong className="block mb-2">
|
||
입력 내용을 확인해주세요 ({Object.keys(errors).length}개 오류)
|
||
</strong>
|
||
<ul className="space-y-1 text-sm">
|
||
{Object.entries(errors).map(([field, message]) => {
|
||
// item-0-productCategory 형태의 키에서 필드명 추출
|
||
const fieldParts = field.split("-");
|
||
let fieldName = field;
|
||
if (fieldParts.length === 3) {
|
||
const itemIndex = parseInt(fieldParts[1]) + 1;
|
||
const fieldKey = fieldParts[2];
|
||
fieldName = `견적 ${itemIndex} - ${FIELD_NAME_MAP[fieldKey] || fieldKey}`;
|
||
} else {
|
||
fieldName = FIELD_NAME_MAP[field] || field;
|
||
}
|
||
return (
|
||
<li key={field} className="flex items-start gap-1">
|
||
<span>•</span>
|
||
<span>
|
||
<strong>{fieldName}</strong>: {message}
|
||
</span>
|
||
</li>
|
||
);
|
||
})}
|
||
</ul>
|
||
</div>
|
||
</div>
|
||
</AlertDescription>
|
||
</Alert>
|
||
)}
|
||
|
||
{/* 1. 기본 정보 */}
|
||
<FormSection
|
||
title="기본 정보"
|
||
description=""
|
||
icon={FileText}
|
||
>
|
||
<FormFieldGrid columns={3}>
|
||
<FormField label="등록일" htmlFor="registrationDate">
|
||
<Input
|
||
id="registrationDate"
|
||
type="date"
|
||
value={formData.registrationDate}
|
||
disabled
|
||
className="bg-gray-50"
|
||
/>
|
||
</FormField>
|
||
|
||
<FormField label="작성자" htmlFor="writer">
|
||
<Input
|
||
id="writer"
|
||
value={formData.writer}
|
||
disabled
|
||
className="bg-gray-50"
|
||
/>
|
||
</FormField>
|
||
|
||
<FormField
|
||
label="발주처 선택"
|
||
required
|
||
error={errors.clientId}
|
||
htmlFor="clientId"
|
||
>
|
||
<Select
|
||
value={formData.clientId}
|
||
onValueChange={handleClientChange}
|
||
>
|
||
<SelectTrigger id="clientId">
|
||
<SelectValue placeholder="발주처를 선택하세요" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{SAMPLE_CLIENTS.map((client) => (
|
||
<SelectItem key={client.id} value={client.id}>
|
||
{client.name}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</FormField>
|
||
</FormFieldGrid>
|
||
|
||
<FormFieldGrid columns={3}>
|
||
<FormField label="현장명" htmlFor="siteName">
|
||
<Input
|
||
id="siteName"
|
||
placeholder="현장명을 입력하세요"
|
||
value={formData.siteName}
|
||
onChange={(e) => handleFieldChange("siteName", e.target.value)}
|
||
/>
|
||
</FormField>
|
||
|
||
<FormField label="발주 담당자" htmlFor="manager">
|
||
<Input
|
||
id="manager"
|
||
placeholder="담당자명을 입력하세요"
|
||
value={formData.manager}
|
||
onChange={(e) => handleFieldChange("manager", e.target.value)}
|
||
/>
|
||
</FormField>
|
||
|
||
<FormField label="연락처" htmlFor="contact">
|
||
<Input
|
||
id="contact"
|
||
placeholder="010-1234-5678"
|
||
value={formData.contact}
|
||
onChange={(e) => handleFieldChange("contact", e.target.value)}
|
||
/>
|
||
</FormField>
|
||
</FormFieldGrid>
|
||
|
||
<FormFieldGrid columns={3}>
|
||
<FormField label="납기일" htmlFor="dueDate">
|
||
<Input
|
||
id="dueDate"
|
||
type="date"
|
||
value={formData.dueDate}
|
||
onChange={(e) => handleFieldChange("dueDate", e.target.value)}
|
||
/>
|
||
</FormField>
|
||
<div className="col-span-2" />
|
||
</FormFieldGrid>
|
||
|
||
<FormField label="비고" htmlFor="remarks">
|
||
<Textarea
|
||
id="remarks"
|
||
placeholder="특이사항을 입력하세요"
|
||
value={formData.remarks}
|
||
onChange={(e) => handleFieldChange("remarks", e.target.value)}
|
||
rows={3}
|
||
/>
|
||
</FormField>
|
||
</FormSection>
|
||
|
||
{/* 2. 자동 견적 산출 */}
|
||
<FormSection
|
||
title="자동 견적 산출"
|
||
description="입력값을 기반으로 견적을 자동으로 산출합니다"
|
||
icon={Calculator}
|
||
>
|
||
{/* 견적 탭 */}
|
||
<Card className="border-gray-200">
|
||
<CardHeader className="pb-3">
|
||
<div className="flex items-center justify-between">
|
||
<div className="flex gap-2 flex-wrap">
|
||
{formData.items.map((item, index) => (
|
||
<Button
|
||
key={item.id}
|
||
variant={activeItemIndex === index ? "default" : "outline"}
|
||
size="sm"
|
||
onClick={() => setActiveItemIndex(index)}
|
||
className="min-w-[70px]"
|
||
>
|
||
견적 {index + 1}
|
||
</Button>
|
||
))}
|
||
</div>
|
||
<div className="flex gap-1">
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
onClick={() => handleCopyItem(activeItemIndex)}
|
||
title="복사"
|
||
>
|
||
<Copy className="h-4 w-4" />
|
||
</Button>
|
||
<Button
|
||
variant="ghost"
|
||
size="icon"
|
||
onClick={() => handleDeleteItem(activeItemIndex)}
|
||
title="삭제"
|
||
className="text-red-500 hover:text-red-600"
|
||
disabled={formData.items.length === 1}
|
||
>
|
||
<Trash2 className="h-4 w-4" />
|
||
</Button>
|
||
</div>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent className="space-y-4">
|
||
{formData.items[activeItemIndex] && (
|
||
<>
|
||
<FormFieldGrid columns={3}>
|
||
<FormField label="층수" htmlFor={`floor-${activeItemIndex}`}>
|
||
<Input
|
||
id={`floor-${activeItemIndex}`}
|
||
placeholder="예: 1층, B1, 지하1층"
|
||
value={formData.items[activeItemIndex].floor}
|
||
onChange={(e) =>
|
||
handleItemChange(activeItemIndex, "floor", e.target.value)
|
||
}
|
||
/>
|
||
</FormField>
|
||
|
||
<FormField label="부호" htmlFor={`code-${activeItemIndex}`}>
|
||
<Input
|
||
id={`code-${activeItemIndex}`}
|
||
placeholder="예: A, B, C"
|
||
value={formData.items[activeItemIndex].code}
|
||
onChange={(e) =>
|
||
handleItemChange(activeItemIndex, "code", e.target.value)
|
||
}
|
||
/>
|
||
</FormField>
|
||
|
||
<FormField
|
||
label="제품 카테고리 (PC)"
|
||
required
|
||
error={errors[`item-${activeItemIndex}-productCategory`]}
|
||
htmlFor={`productCategory-${activeItemIndex}`}
|
||
>
|
||
<Select
|
||
value={formData.items[activeItemIndex].productCategory}
|
||
onValueChange={(value) =>
|
||
handleItemChange(activeItemIndex, "productCategory", value)
|
||
}
|
||
>
|
||
<SelectTrigger id={`productCategory-${activeItemIndex}`}>
|
||
<SelectValue placeholder="카테고리 선택" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{PRODUCT_CATEGORIES.map((cat) => (
|
||
<SelectItem key={cat.value} value={cat.value}>
|
||
{cat.label}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</FormField>
|
||
</FormFieldGrid>
|
||
|
||
<FormFieldGrid columns={3}>
|
||
<FormField
|
||
label="제품명"
|
||
required
|
||
error={errors[`item-${activeItemIndex}-productName`]}
|
||
htmlFor={`productName-${activeItemIndex}`}
|
||
>
|
||
<Select
|
||
value={formData.items[activeItemIndex].productName}
|
||
onValueChange={(value) =>
|
||
handleItemChange(activeItemIndex, "productName", value)
|
||
}
|
||
disabled={!formData.items[activeItemIndex].productCategory}
|
||
>
|
||
<SelectTrigger id={`productName-${activeItemIndex}`}>
|
||
<SelectValue placeholder="제품을 선택하세요" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{(PRODUCTS[formData.items[activeItemIndex].productCategory] || []).map((product) => (
|
||
<SelectItem key={product.value} value={product.value}>
|
||
{product.label}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</FormField>
|
||
|
||
<FormField
|
||
label="오픈사이즈 (W0)"
|
||
required
|
||
error={errors[`item-${activeItemIndex}-openWidth`]}
|
||
htmlFor={`openWidth-${activeItemIndex}`}
|
||
>
|
||
<Input
|
||
id={`openWidth-${activeItemIndex}`}
|
||
placeholder="예: 2000"
|
||
value={formData.items[activeItemIndex].openWidth}
|
||
onChange={(e) =>
|
||
handleItemChange(activeItemIndex, "openWidth", e.target.value)
|
||
}
|
||
/>
|
||
</FormField>
|
||
|
||
<FormField
|
||
label="오픈사이즈 (H0)"
|
||
required
|
||
error={errors[`item-${activeItemIndex}-openHeight`]}
|
||
htmlFor={`openHeight-${activeItemIndex}`}
|
||
>
|
||
<Input
|
||
id={`openHeight-${activeItemIndex}`}
|
||
placeholder="예: 2500"
|
||
value={formData.items[activeItemIndex].openHeight}
|
||
onChange={(e) =>
|
||
handleItemChange(activeItemIndex, "openHeight", e.target.value)
|
||
}
|
||
/>
|
||
</FormField>
|
||
</FormFieldGrid>
|
||
|
||
<FormFieldGrid columns={3}>
|
||
<FormField
|
||
label="가이드레일 설치 유형 (GT)"
|
||
required
|
||
error={errors[`item-${activeItemIndex}-guideRailType`]}
|
||
htmlFor={`guideRailType-${activeItemIndex}`}
|
||
>
|
||
<Select
|
||
value={formData.items[activeItemIndex].guideRailType}
|
||
onValueChange={(value) =>
|
||
handleItemChange(activeItemIndex, "guideRailType", value)
|
||
}
|
||
>
|
||
<SelectTrigger id={`guideRailType-${activeItemIndex}`}>
|
||
<SelectValue placeholder="설치 유형 선택" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{GUIDE_RAIL_TYPES.map((type) => (
|
||
<SelectItem key={type.value} value={type.value}>
|
||
{type.label}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</FormField>
|
||
|
||
<FormField
|
||
label="모터 전원 (MP)"
|
||
required
|
||
error={errors[`item-${activeItemIndex}-motorPower`]}
|
||
htmlFor={`motorPower-${activeItemIndex}`}
|
||
>
|
||
<Select
|
||
value={formData.items[activeItemIndex].motorPower}
|
||
onValueChange={(value) =>
|
||
handleItemChange(activeItemIndex, "motorPower", value)
|
||
}
|
||
>
|
||
<SelectTrigger id={`motorPower-${activeItemIndex}`}>
|
||
<SelectValue placeholder="전원 선택" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{MOTOR_POWERS.map((power) => (
|
||
<SelectItem key={power.value} value={power.value}>
|
||
{power.label}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</FormField>
|
||
|
||
<FormField
|
||
label="연동제어기 (CT)"
|
||
required
|
||
error={errors[`item-${activeItemIndex}-controller`]}
|
||
htmlFor={`controller-${activeItemIndex}`}
|
||
>
|
||
<Select
|
||
value={formData.items[activeItemIndex].controller}
|
||
onValueChange={(value) =>
|
||
handleItemChange(activeItemIndex, "controller", value)
|
||
}
|
||
>
|
||
<SelectTrigger id={`controller-${activeItemIndex}`}>
|
||
<SelectValue placeholder="제어기 선택" />
|
||
</SelectTrigger>
|
||
<SelectContent>
|
||
{CONTROLLERS.map((ctrl) => (
|
||
<SelectItem key={ctrl.value} value={ctrl.value}>
|
||
{ctrl.label}
|
||
</SelectItem>
|
||
))}
|
||
</SelectContent>
|
||
</Select>
|
||
</FormField>
|
||
</FormFieldGrid>
|
||
|
||
<FormFieldGrid columns={3}>
|
||
<FormField
|
||
label="수량 (QTY)"
|
||
required
|
||
error={errors[`item-${activeItemIndex}-quantity`]}
|
||
htmlFor={`quantity-${activeItemIndex}`}
|
||
>
|
||
<Input
|
||
id={`quantity-${activeItemIndex}`}
|
||
type="number"
|
||
min="1"
|
||
value={formData.items[activeItemIndex].quantity}
|
||
onChange={(e) =>
|
||
handleItemChange(activeItemIndex, "quantity", parseInt(e.target.value) || 1)
|
||
}
|
||
/>
|
||
</FormField>
|
||
|
||
<FormField
|
||
label="마구리 날개치수 (WS)"
|
||
htmlFor={`wingSize-${activeItemIndex}`}
|
||
>
|
||
<Input
|
||
id={`wingSize-${activeItemIndex}`}
|
||
placeholder="예: 50"
|
||
value={formData.items[activeItemIndex].wingSize}
|
||
onChange={(e) =>
|
||
handleItemChange(activeItemIndex, "wingSize", e.target.value)
|
||
}
|
||
/>
|
||
</FormField>
|
||
|
||
<FormField
|
||
label="검사비 (INSP)"
|
||
htmlFor={`inspectionFee-${activeItemIndex}`}
|
||
>
|
||
<Input
|
||
id={`inspectionFee-${activeItemIndex}`}
|
||
type="number"
|
||
placeholder="예: 50000"
|
||
value={formData.items[activeItemIndex].inspectionFee}
|
||
onChange={(e) =>
|
||
handleItemChange(activeItemIndex, "inspectionFee", parseInt(e.target.value) || 0)
|
||
}
|
||
/>
|
||
</FormField>
|
||
</FormFieldGrid>
|
||
</>
|
||
)}
|
||
</CardContent>
|
||
</Card>
|
||
|
||
{/* 견적 추가 버튼 */}
|
||
<Button
|
||
variant="outline"
|
||
className="w-full"
|
||
onClick={handleAddItem}
|
||
>
|
||
<Plus className="h-4 w-4 mr-2" />
|
||
견적 추가
|
||
</Button>
|
||
|
||
{/* 자동 견적 산출 버튼 */}
|
||
<Button
|
||
variant="default"
|
||
className="w-full bg-blue-600 hover:bg-blue-700"
|
||
onClick={handleAutoCalculate}
|
||
>
|
||
자동 견적 산출 ({formData.items.length}개 항목)
|
||
</Button>
|
||
</FormSection>
|
||
|
||
{/* 3. 샘플 데이터 (개발용) */}
|
||
<Card className="border-blue-200 bg-blue-50/50">
|
||
<CardHeader className="pb-2">
|
||
<div className="flex items-center justify-between">
|
||
<div>
|
||
<CardTitle className="text-sm font-medium">
|
||
견적 산출 샘플 데이터 (완전판)
|
||
</CardTitle>
|
||
<p className="text-xs text-muted-foreground mt-1">
|
||
자동 견적 산출 기능을 테스트하기 위한 완벽한 샘플 데이터를 생성합니다.
|
||
완제품 14종(스크린 5종, 철재 5종, 절곡 4종), 반제품 40종, 부자재 25종, 원자재 20종이
|
||
생성되며, 모든 제품에 실제 BOM 구조(2~3단계 계층)와 단가 정보가 포함되어
|
||
즉시 견적 산출이 가능합니다.
|
||
</p>
|
||
</div>
|
||
<Button
|
||
variant="outline"
|
||
size="sm"
|
||
onClick={handleGenerateSample}
|
||
className="bg-white"
|
||
>
|
||
<Sparkles className="h-4 w-4 mr-2" />
|
||
완벽한 샘플 생성
|
||
</Button>
|
||
</div>
|
||
</CardHeader>
|
||
<CardContent className="pt-2">
|
||
<div className="flex flex-wrap gap-2">
|
||
<Badge variant="secondary">완제품 14종</Badge>
|
||
<Badge variant="secondary">반제품 40종</Badge>
|
||
<Badge variant="secondary">부자재 25종</Badge>
|
||
<Badge variant="secondary">원자재 20종</Badge>
|
||
<Badge variant="outline">BOM 2~3단계 계층 ▾</Badge>
|
||
<Badge variant="outline">단가 정보 포함 ▾</Badge>
|
||
<Badge variant="outline">절곡 제품 포함 ▾</Badge>
|
||
</div>
|
||
</CardContent>
|
||
</Card>
|
||
</ResponsiveFormTemplate>
|
||
);
|
||
} |