이슈 #3: 상세 견적서 담당자/연락처 표시 이슈 #4: 품목내역 올바른 단위 표시 주요 변경: - QuoteDocument.tsx: 품목별 unit 필드 사용하여 올바른 단위 표시 - QuoteRegistration.tsx: manager, contact, remarks 필드 폼에 반영
This commit is contained in:
@@ -11,12 +11,14 @@
|
|||||||
*/
|
*/
|
||||||
|
|
||||||
import { QuoteFormData } from "./QuoteRegistration";
|
import { QuoteFormData } from "./QuoteRegistration";
|
||||||
|
import type { CompanyFormData } from "@/components/settings/CompanyInfoManagement/types";
|
||||||
|
|
||||||
interface QuoteDocumentProps {
|
interface QuoteDocumentProps {
|
||||||
quote: QuoteFormData;
|
quote: QuoteFormData;
|
||||||
|
companyInfo?: CompanyFormData | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function QuoteDocument({ quote }: QuoteDocumentProps) {
|
export function QuoteDocument({ quote, companyInfo }: QuoteDocumentProps) {
|
||||||
const formatAmount = (amount: number | undefined) => {
|
const formatAmount = (amount: number | undefined) => {
|
||||||
if (amount === undefined || amount === null) return '0';
|
if (amount === undefined || amount === null) return '0';
|
||||||
return amount.toLocaleString('ko-KR');
|
return amount.toLocaleString('ko-KR');
|
||||||
@@ -34,7 +36,7 @@ export function QuoteDocument({ quote }: QuoteDocumentProps) {
|
|||||||
itemName: item.productName || '스크린셔터',
|
itemName: item.productName || '스크린셔터',
|
||||||
spec: `${item.openWidth}×${item.openHeight}`,
|
spec: `${item.openWidth}×${item.openHeight}`,
|
||||||
quantity: item.quantity || 1,
|
quantity: item.quantity || 1,
|
||||||
unit: '개소',
|
unit: item.unit || '', // 각 품목의 단위 사용, 없으면 빈 문자열
|
||||||
unitPrice: item.unitPrice || 0,
|
unitPrice: item.unitPrice || 0,
|
||||||
totalPrice: item.totalAmount || 0,
|
totalPrice: item.totalAmount || 0,
|
||||||
})) || [];
|
})) || [];
|
||||||
@@ -292,29 +294,29 @@ export function QuoteDocument({ quote }: QuoteDocumentProps) {
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr>
|
<tr>
|
||||||
<th>상호</th>
|
<th>상호</th>
|
||||||
<td>동호기업</td>
|
<td>{companyInfo?.companyName || '-'}</td>
|
||||||
<th>사업자등록번호</th>
|
<th>사업자등록번호</th>
|
||||||
<td>139-87-00333</td>
|
<td>{companyInfo?.businessNumber || '-'}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>대표자</th>
|
<th>대표자</th>
|
||||||
<td>이 광 호</td>
|
<td>{companyInfo?.representativeName || '-'}</td>
|
||||||
<th>업태</th>
|
<th>업태</th>
|
||||||
<td>제조</td>
|
<td>{companyInfo?.businessType || '-'}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>종목</th>
|
<th>종목</th>
|
||||||
<td colSpan={3}>방창, 셔터, 금속성호</td>
|
<td colSpan={3}>{companyInfo?.businessCategory || '-'}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>사업장주소</th>
|
<th>사업장주소</th>
|
||||||
<td colSpan={3}>경기도 안성시 공업용지 오성길 45-22</td>
|
<td colSpan={3}>{companyInfo?.address || '-'}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>전화</th>
|
<th>전화</th>
|
||||||
<td>031-983-5130</td>
|
<td>{companyInfo?.managerPhone || '-'}</td>
|
||||||
<th>팩스</th>
|
<th>이메일</th>
|
||||||
<td>02-6911-6315</td>
|
<td>{companyInfo?.email || '-'}</td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
@@ -340,7 +342,7 @@ export function QuoteDocument({ quote }: QuoteDocumentProps) {
|
|||||||
<th>모델</th>
|
<th>모델</th>
|
||||||
<td>{quote.items[0]?.productName || '스크린셔터'}</td>
|
<td>{quote.items[0]?.productName || '스크린셔터'}</td>
|
||||||
<th>총 수량</th>
|
<th>총 수량</th>
|
||||||
<td>{quote.items.reduce((sum, item) => sum + (item.quantity || 0), 0)}개소</td>
|
<td>{quote.items[0]?.quantity || ''}{quote.unitSymbol ? ` ${quote.unitSymbol}` : ''}</td>
|
||||||
</tr>
|
</tr>
|
||||||
<tr>
|
<tr>
|
||||||
<th>오픈사이즈</th>
|
<th>오픈사이즈</th>
|
||||||
@@ -432,7 +434,7 @@ export function QuoteDocument({ quote }: QuoteDocumentProps) {
|
|||||||
<div>
|
<div>
|
||||||
<div style={{ fontSize: '13px', marginBottom: '5px' }}>{formatDate(quote.registrationDate || '')}</div>
|
<div style={{ fontSize: '13px', marginBottom: '5px' }}>{formatDate(quote.registrationDate || '')}</div>
|
||||||
<div style={{ fontSize: '15px', fontWeight: '600' }}>
|
<div style={{ fontSize: '15px', fontWeight: '600' }}>
|
||||||
공급자: 동호기업 (인)
|
공급자: {companyInfo?.companyName || '-'} (인)
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="stamp-area">
|
<div className="stamp-area">
|
||||||
@@ -452,7 +454,7 @@ export function QuoteDocument({ quote }: QuoteDocumentProps) {
|
|||||||
<p>3. 제작 사양 및 수량 변경 시 견적 금액이 변동될 수 있습니다.</p>
|
<p>3. 제작 사양 및 수량 변경 시 견적 금액이 변동될 수 있습니다.</p>
|
||||||
<p>4. 현장 여건에 따라 추가 비용이 발생할 수 있습니다.</p>
|
<p>4. 현장 여건에 따라 추가 비용이 발생할 수 있습니다.</p>
|
||||||
<p style={{ marginTop: '12px', textAlign: 'center', fontWeight: '600' }}>
|
<p style={{ marginTop: '12px', textAlign: 'center', fontWeight: '600' }}>
|
||||||
문의: {quote.writer || '담당자'} | {quote.contact || '031-983-5130'}
|
문의: {companyInfo?.managerName || quote.writer || '담당자'} | {companyInfo?.managerPhone || '-'}
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -8,7 +8,7 @@
|
|||||||
|
|
||||||
"use client";
|
"use client";
|
||||||
|
|
||||||
import { useState, useEffect } from "react";
|
import { useState, useEffect, useMemo } from "react";
|
||||||
import { Input } from "../ui/input";
|
import { Input } from "../ui/input";
|
||||||
import { Textarea } from "../ui/textarea";
|
import { Textarea } from "../ui/textarea";
|
||||||
import {
|
import {
|
||||||
@@ -28,7 +28,7 @@ import {
|
|||||||
Plus,
|
Plus,
|
||||||
Copy,
|
Copy,
|
||||||
Trash2,
|
Trash2,
|
||||||
Sparkles,
|
Loader2,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
import { toast } from "sonner";
|
import { toast } from "sonner";
|
||||||
|
|
||||||
@@ -50,6 +50,10 @@ import {
|
|||||||
FormFieldGrid,
|
FormFieldGrid,
|
||||||
} from "../templates/ResponsiveFormTemplate";
|
} from "../templates/ResponsiveFormTemplate";
|
||||||
import { FormField } from "../molecules/FormField";
|
import { FormField } from "../molecules/FormField";
|
||||||
|
import { getFinishedGoods, calculateBomBulk, getSiteNames, type FinishedGoods, type BomCalculationResult } from "./actions";
|
||||||
|
import { getClients } from "../accounting/VendorManagement/actions";
|
||||||
|
import type { Vendor } from "../accounting/VendorManagement";
|
||||||
|
import type { BomMaterial, CalculationResults } from "./types";
|
||||||
|
|
||||||
// 견적 항목 타입
|
// 견적 항목 타입
|
||||||
export interface QuoteItem {
|
export interface QuoteItem {
|
||||||
@@ -64,6 +68,7 @@ export interface QuoteItem {
|
|||||||
motorPower: string; // 모터 전원 (MP)
|
motorPower: string; // 모터 전원 (MP)
|
||||||
controller: string; // 연동제어기 (CT)
|
controller: string; // 연동제어기 (CT)
|
||||||
quantity: number; // 수량 (QTY)
|
quantity: number; // 수량 (QTY)
|
||||||
|
unit?: string; // 품목 단위
|
||||||
wingSize: string; // 마구리 날개치수 (WS)
|
wingSize: string; // 마구리 날개치수 (WS)
|
||||||
inspectionFee: number; // 검사비 (INSP)
|
inspectionFee: number; // 검사비 (INSP)
|
||||||
unitPrice?: number; // 단가
|
unitPrice?: number; // 단가
|
||||||
@@ -83,7 +88,10 @@ export interface QuoteFormData {
|
|||||||
contact: string;
|
contact: string;
|
||||||
dueDate: string;
|
dueDate: string;
|
||||||
remarks: string;
|
remarks: string;
|
||||||
|
unitSymbol?: string; // 단위 (EA, 개소 등) - quotes.unit_symbol
|
||||||
items: QuoteItem[];
|
items: QuoteItem[];
|
||||||
|
bomMaterials?: BomMaterial[]; // BOM 자재 목록
|
||||||
|
calculationResults?: CalculationResults; // 견적 산출 결과 (저장 시 BOM 자재 변환용)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 초기 견적 항목
|
// 초기 견적 항목
|
||||||
@@ -117,58 +125,31 @@ export const INITIAL_QUOTE_FORM: QuoteFormData = {
|
|||||||
items: [createNewItem()],
|
items: [createNewItem()],
|
||||||
};
|
};
|
||||||
|
|
||||||
// 샘플 발주처 데이터 (TODO: API에서 가져오기)
|
// 제품 카테고리 옵션 (MNG 시뮬레이터와 동일)
|
||||||
const SAMPLE_CLIENTS = [
|
|
||||||
{ id: "client-1", name: "인천건설 - 최담당" },
|
|
||||||
{ id: "client-2", name: "ABC건설" },
|
|
||||||
{ id: "client-3", name: "XYZ산업" },
|
|
||||||
];
|
|
||||||
|
|
||||||
// 제품 카테고리 옵션
|
|
||||||
const PRODUCT_CATEGORIES = [
|
const PRODUCT_CATEGORIES = [
|
||||||
{ value: "screen", label: "스크린" },
|
{ value: "ALL", label: "전체" },
|
||||||
{ value: "steel", label: "철재" },
|
{ value: "SCREEN", label: "스크린" },
|
||||||
{ value: "aluminum", label: "알루미늄" },
|
{ value: "STEEL", label: "철재" },
|
||||||
{ value: "etc", label: "기타" },
|
{ value: "BENDING", label: "절곡" },
|
||||||
|
{ value: "ALUMINUM", label: "알루미늄" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// 제품명 옵션 (카테고리별)
|
// 가이드레일 설치 유형 (API: wall, ceiling, floor)
|
||||||
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 = [
|
const GUIDE_RAIL_TYPES = [
|
||||||
{ value: "wall", label: "벽부착형" },
|
{ value: "wall", label: "벽면형" },
|
||||||
{ value: "ceiling", label: "천장매립형" },
|
{ value: "floor", label: "측면형" },
|
||||||
{ value: "floor", label: "바닥매립형" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
// 모터 전원
|
// 모터 전원 (API: single=단상220V, three=삼상380V)
|
||||||
const MOTOR_POWERS = [
|
const MOTOR_POWERS = [
|
||||||
{ value: "single", label: "단상 220V" },
|
{ value: "single", label: "220V (단상)" },
|
||||||
{ value: "three", label: "삼상 380V" },
|
{ value: "three", label: "380V (삼상)" },
|
||||||
];
|
];
|
||||||
|
|
||||||
// 연동제어기
|
// 연동제어기 (API: basic, smart, premium)
|
||||||
const CONTROLLERS = [
|
const CONTROLLERS = [
|
||||||
{ value: "basic", label: "기본 제어기" },
|
{ value: "basic", label: "단독" },
|
||||||
{ value: "smart", label: "스마트 제어기" },
|
{ value: "smart", label: "연동" },
|
||||||
{ value: "premium", label: "프리미엄 제어기" },
|
|
||||||
];
|
];
|
||||||
|
|
||||||
interface QuoteRegistrationProps {
|
interface QuoteRegistrationProps {
|
||||||
@@ -191,13 +172,118 @@ export function QuoteRegistration({
|
|||||||
const [isSaving, setIsSaving] = useState(false);
|
const [isSaving, setIsSaving] = useState(false);
|
||||||
const [activeItemIndex, setActiveItemIndex] = useState(0);
|
const [activeItemIndex, setActiveItemIndex] = useState(0);
|
||||||
|
|
||||||
// editingQuote가 변경되면 formData 업데이트
|
// 완제품 목록 상태 (API에서 로드)
|
||||||
|
const [finishedGoods, setFinishedGoods] = useState<FinishedGoods[]>([]);
|
||||||
|
const [isLoadingProducts, setIsLoadingProducts] = useState(false);
|
||||||
|
const [isCalculating, setIsCalculating] = useState(false);
|
||||||
|
|
||||||
|
// 거래처 목록 상태 (API에서 로드)
|
||||||
|
const [clients, setClients] = useState<Vendor[]>([]);
|
||||||
|
const [isLoadingClients, setIsLoadingClients] = useState(false);
|
||||||
|
|
||||||
|
// 견적 산출 결과 상태
|
||||||
|
const [calculationResults, setCalculationResults] = useState<{
|
||||||
|
summary: { grand_total: number };
|
||||||
|
items: Array<{
|
||||||
|
index: number;
|
||||||
|
result: BomCalculationResult;
|
||||||
|
}>;
|
||||||
|
} | null>(null);
|
||||||
|
|
||||||
|
// 현장명 자동완성 목록 상태
|
||||||
|
const [siteNames, setSiteNames] = useState<string[]>([]);
|
||||||
|
|
||||||
|
// 수량 반영 총합계 계산 (useMemo로 최적화)
|
||||||
|
const calculatedGrandTotal = useMemo(() => {
|
||||||
|
if (!calculationResults?.items) return 0;
|
||||||
|
return calculationResults.items.reduce((sum, itemResult) => {
|
||||||
|
const formItem = formData.items[itemResult.index];
|
||||||
|
return sum + (itemResult.result.grand_total * (formItem?.quantity || 1));
|
||||||
|
}, 0);
|
||||||
|
}, [calculationResults, formData.items]);
|
||||||
|
|
||||||
|
// 컴포넌트 마운트 시 완제품 목록 로드
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
const loadFinishedGoods = async () => {
|
||||||
|
setIsLoadingProducts(true);
|
||||||
|
try {
|
||||||
|
const result = await getFinishedGoods();
|
||||||
|
if (result.success) {
|
||||||
|
setFinishedGoods(result.data);
|
||||||
|
} else {
|
||||||
|
toast.error(`완제품 목록 로드 실패: ${result.error}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("완제품 목록을 불러오는데 실패했습니다.");
|
||||||
|
} finally {
|
||||||
|
setIsLoadingProducts(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadFinishedGoods();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 컴포넌트 마운트 시 거래처 목록 로드
|
||||||
|
useEffect(() => {
|
||||||
|
const loadClients = async () => {
|
||||||
|
setIsLoadingClients(true);
|
||||||
|
try {
|
||||||
|
const result = await getClients();
|
||||||
|
if (result.success) {
|
||||||
|
setClients(result.data);
|
||||||
|
} else {
|
||||||
|
toast.error(`거래처 목록 로드 실패: ${result.error}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
toast.error("거래처 목록을 불러오는데 실패했습니다.");
|
||||||
|
} finally {
|
||||||
|
setIsLoadingClients(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadClients();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// 컴포넌트 마운트 시 현장명 목록 로드 (자동완성용)
|
||||||
|
useEffect(() => {
|
||||||
|
const loadSiteNames = async () => {
|
||||||
|
try {
|
||||||
|
const result = await getSiteNames();
|
||||||
|
if (result.success) {
|
||||||
|
setSiteNames(result.data);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// 현장명 로드 실패는 무시 (선택적 기능)
|
||||||
|
console.error("현장명 목록 로드 실패:", error);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
loadSiteNames();
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
// editingQuote가 변경되면 formData 업데이트 및 calculationResults 초기화
|
||||||
|
useEffect(() => {
|
||||||
|
console.log('[QuoteRegistration] useEffect editingQuote:', JSON.stringify({
|
||||||
|
hasEditingQuote: !!editingQuote,
|
||||||
|
itemCount: editingQuote?.items?.length,
|
||||||
|
item0: editingQuote?.items?.[0] ? {
|
||||||
|
quantity: editingQuote.items[0].quantity,
|
||||||
|
wingSize: editingQuote.items[0].wingSize,
|
||||||
|
inspectionFee: editingQuote.items[0].inspectionFee,
|
||||||
|
} : null,
|
||||||
|
}, null, 2));
|
||||||
if (editingQuote) {
|
if (editingQuote) {
|
||||||
setFormData(editingQuote);
|
setFormData(editingQuote);
|
||||||
|
// 수정 모드 진입 시 이전 산출 결과 초기화
|
||||||
|
setCalculationResults(null);
|
||||||
}
|
}
|
||||||
}, [editingQuote]);
|
}, [editingQuote]);
|
||||||
|
|
||||||
|
// 카테고리별 완제품 필터링
|
||||||
|
const getFilteredProducts = (category: string) => {
|
||||||
|
if (!category || category === "ALL") {
|
||||||
|
return finishedGoods; // 전체 선택 시 모든 완제품
|
||||||
|
}
|
||||||
|
return finishedGoods.filter(fg => fg.item_category === category);
|
||||||
|
};
|
||||||
|
|
||||||
// 유효성 검사
|
// 유효성 검사
|
||||||
const validateForm = (): boolean => {
|
const validateForm = (): boolean => {
|
||||||
const newErrors: Record<string, string> = {};
|
const newErrors: Record<string, string> = {};
|
||||||
@@ -249,7 +335,13 @@ export function QuoteRegistration({
|
|||||||
setErrors({});
|
setErrors({});
|
||||||
setIsSaving(true);
|
setIsSaving(true);
|
||||||
try {
|
try {
|
||||||
await onSave(formData);
|
// calculationResults를 formData에 포함하여 저장
|
||||||
|
// transformFormDataToApi에서 BOM 자재의 base_quantity, calculated_quantity를 제대로 설정하기 위함
|
||||||
|
const dataToSave: QuoteFormData = {
|
||||||
|
...formData,
|
||||||
|
calculationResults: calculationResults || undefined,
|
||||||
|
};
|
||||||
|
await onSave(dataToSave);
|
||||||
toast.success(
|
toast.success(
|
||||||
editingQuote ? "견적이 수정되었습니다." : "견적이 등록되었습니다."
|
editingQuote ? "견적이 수정되었습니다." : "견적이 등록되었습니다."
|
||||||
);
|
);
|
||||||
@@ -265,6 +357,10 @@ export function QuoteRegistration({
|
|||||||
field: keyof QuoteFormData,
|
field: keyof QuoteFormData,
|
||||||
value: string | QuoteItem[]
|
value: string | QuoteItem[]
|
||||||
) => {
|
) => {
|
||||||
|
// DEBUG: manager, contact, remarks 필드 변경 추적
|
||||||
|
if (field === 'manager' || field === 'contact' || field === 'remarks') {
|
||||||
|
console.log(`[handleFieldChange] ${field} 변경:`, value);
|
||||||
|
}
|
||||||
setFormData({ ...formData, [field]: value });
|
setFormData({ ...formData, [field]: value });
|
||||||
if (errors[field]) {
|
if (errors[field]) {
|
||||||
setErrors((prev) => {
|
setErrors((prev) => {
|
||||||
@@ -277,11 +373,11 @@ export function QuoteRegistration({
|
|||||||
|
|
||||||
// 발주처 선택
|
// 발주처 선택
|
||||||
const handleClientChange = (clientId: string) => {
|
const handleClientChange = (clientId: string) => {
|
||||||
const client = SAMPLE_CLIENTS.find((c) => c.id === clientId);
|
const client = clients.find((c) => c.id === clientId);
|
||||||
setFormData({
|
setFormData({
|
||||||
...formData,
|
...formData,
|
||||||
clientId,
|
clientId,
|
||||||
clientName: client?.name || "",
|
clientName: client?.vendorName || "",
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -347,14 +443,90 @@ export function QuoteRegistration({
|
|||||||
};
|
};
|
||||||
|
|
||||||
// 자동 견적 산출
|
// 자동 견적 산출
|
||||||
const handleAutoCalculate = () => {
|
const handleAutoCalculate = async () => {
|
||||||
toast.info(`자동 견적 산출 (${formData.items.length}개 항목) - API 연동 필요`);
|
// 필수 입력값 검사
|
||||||
|
const incompleteItems = formData.items.filter(
|
||||||
|
(item) => !item.productName || !item.openWidth || !item.openHeight
|
||||||
|
);
|
||||||
|
|
||||||
|
if (incompleteItems.length > 0) {
|
||||||
|
toast.error("모든 견적 항목의 필수 입력값(제품명, 오픈사이즈)을 입력해주세요.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
setIsCalculating(true);
|
||||||
|
try {
|
||||||
|
// BOM 계산 요청 데이터 구성 (API는 플랫한 구조 기대)
|
||||||
|
const bomItems = formData.items.map((item) => ({
|
||||||
|
finished_goods_code: item.productName, // item_code가 productName에 저장됨
|
||||||
|
// React 필드명 (camelCase) 사용 - API가 W0/H0로 변환
|
||||||
|
openWidth: parseFloat(item.openWidth) || 0,
|
||||||
|
openHeight: parseFloat(item.openHeight) || 0,
|
||||||
|
quantity: item.quantity,
|
||||||
|
guideRailType: item.guideRailType || undefined,
|
||||||
|
motorPower: item.motorPower || undefined,
|
||||||
|
controller: item.controller || undefined,
|
||||||
|
wingSize: parseFloat(item.wingSize) || undefined,
|
||||||
|
inspectionFee: item.inspectionFee || undefined,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const result = await calculateBomBulk(bomItems);
|
||||||
|
|
||||||
|
if (result.success && result.data) {
|
||||||
|
// API 응답: { success, summary, items: [{ index, result: BomCalculationResult }] }
|
||||||
|
const apiData = result.data as {
|
||||||
|
summary?: { grand_total: number };
|
||||||
|
items?: Array<{ index: number; result: BomCalculationResult }>;
|
||||||
|
};
|
||||||
|
|
||||||
|
const bomItems = apiData.items || [];
|
||||||
|
|
||||||
|
// 계산 결과를 폼 데이터에 반영
|
||||||
|
const updatedItems = formData.items.map((item, index) => {
|
||||||
|
const bomResult = bomItems.find((b) => b.index === index);
|
||||||
|
if (bomResult?.result) {
|
||||||
|
return {
|
||||||
|
...item,
|
||||||
|
unitPrice: bomResult.result.grand_total,
|
||||||
|
totalAmount: bomResult.result.grand_total * item.quantity,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
return item;
|
||||||
|
});
|
||||||
|
|
||||||
|
setFormData({ ...formData, items: updatedItems });
|
||||||
|
|
||||||
|
// 전체 계산 결과 저장
|
||||||
|
setCalculationResults({
|
||||||
|
summary: apiData.summary || { grand_total: 0 },
|
||||||
|
items: bomItems,
|
||||||
|
});
|
||||||
|
|
||||||
|
toast.success(`${formData.items.length}개 항목의 견적이 산출되었습니다.`);
|
||||||
|
|
||||||
|
// 계산 결과 요약 표시 (수량 반영 총합계 계산 - updatedItems 사용)
|
||||||
|
const totalWithQuantity = bomItems.reduce((sum, itemResult) => {
|
||||||
|
const formItem = updatedItems[itemResult.index];
|
||||||
|
return sum + (itemResult.result.grand_total * (formItem?.quantity || 1));
|
||||||
|
}, 0);
|
||||||
|
toast.info(`총 견적 금액: ${totalWithQuantity.toLocaleString()}원`);
|
||||||
|
} else {
|
||||||
|
toast.error(`견적 산출 실패: ${result.error || "알 수 없는 오류"}`);
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error("견적 산출 오류:", error);
|
||||||
|
toast.error("견적 산출 중 오류가 발생했습니다.");
|
||||||
|
} finally {
|
||||||
|
setIsCalculating(false);
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// 샘플 데이터 생성
|
// 렌더링 직전 디버그 로그
|
||||||
const handleGenerateSample = () => {
|
console.log('[QuoteRegistration] 렌더링 직전 formData.items[0]:', JSON.stringify({
|
||||||
toast.info("완벽한 샘플 생성 - API 연동 필요");
|
quantity: formData.items[0]?.quantity,
|
||||||
};
|
wingSize: formData.items[0]?.wingSize,
|
||||||
|
inspectionFee: formData.items[0]?.inspectionFee,
|
||||||
|
}, null, 2));
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<ResponsiveFormTemplate
|
<ResponsiveFormTemplate
|
||||||
@@ -415,7 +587,7 @@ export function QuoteRegistration({
|
|||||||
icon={FileText}
|
icon={FileText}
|
||||||
>
|
>
|
||||||
<FormFieldGrid columns={3}>
|
<FormFieldGrid columns={3}>
|
||||||
<FormField label="등록일" htmlFor="registrationDate">
|
<FormField label="등록일" htmlFor="registrationDate" type="custom">
|
||||||
<Input
|
<Input
|
||||||
id="registrationDate"
|
id="registrationDate"
|
||||||
type="date"
|
type="date"
|
||||||
@@ -436,6 +608,7 @@ export function QuoteRegistration({
|
|||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
label="발주처 선택"
|
label="발주처 선택"
|
||||||
|
type="custom"
|
||||||
required
|
required
|
||||||
error={errors.clientId}
|
error={errors.clientId}
|
||||||
htmlFor="clientId"
|
htmlFor="clientId"
|
||||||
@@ -443,14 +616,15 @@ export function QuoteRegistration({
|
|||||||
<Select
|
<Select
|
||||||
value={formData.clientId}
|
value={formData.clientId}
|
||||||
onValueChange={handleClientChange}
|
onValueChange={handleClientChange}
|
||||||
|
disabled={isLoadingClients}
|
||||||
>
|
>
|
||||||
<SelectTrigger id="clientId">
|
<SelectTrigger id="clientId">
|
||||||
<SelectValue placeholder="발주처를 선택하세요" />
|
<SelectValue placeholder={isLoadingClients ? "로딩 중..." : "발주처를 선택하세요"} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{SAMPLE_CLIENTS.map((client) => (
|
{clients.map((client) => (
|
||||||
<SelectItem key={client.id} value={client.id}>
|
<SelectItem key={client.id} value={client.id}>
|
||||||
{client.name}
|
{client.vendorName}
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@@ -459,16 +633,22 @@ export function QuoteRegistration({
|
|||||||
</FormFieldGrid>
|
</FormFieldGrid>
|
||||||
|
|
||||||
<FormFieldGrid columns={3}>
|
<FormFieldGrid columns={3}>
|
||||||
<FormField label="현장명" htmlFor="siteName">
|
<FormField label="현장명" htmlFor="siteName" type="custom">
|
||||||
<Input
|
<Input
|
||||||
id="siteName"
|
id="siteName"
|
||||||
placeholder="현장명을 입력하세요"
|
list="siteNameList"
|
||||||
|
placeholder="현장명을 입력 또는 선택하세요"
|
||||||
value={formData.siteName}
|
value={formData.siteName}
|
||||||
onChange={(e) => handleFieldChange("siteName", e.target.value)}
|
onChange={(e) => handleFieldChange("siteName", e.target.value)}
|
||||||
/>
|
/>
|
||||||
|
<datalist id="siteNameList">
|
||||||
|
{siteNames.map((name) => (
|
||||||
|
<option key={name} value={name} />
|
||||||
|
))}
|
||||||
|
</datalist>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField label="발주 담당자" htmlFor="manager">
|
<FormField label="발주 담당자" htmlFor="manager" type="custom">
|
||||||
<Input
|
<Input
|
||||||
id="manager"
|
id="manager"
|
||||||
placeholder="담당자명을 입력하세요"
|
placeholder="담당자명을 입력하세요"
|
||||||
@@ -477,7 +657,7 @@ export function QuoteRegistration({
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField label="연락처" htmlFor="contact">
|
<FormField label="연락처" htmlFor="contact" type="custom">
|
||||||
<Input
|
<Input
|
||||||
id="contact"
|
id="contact"
|
||||||
placeholder="010-1234-5678"
|
placeholder="010-1234-5678"
|
||||||
@@ -488,7 +668,7 @@ export function QuoteRegistration({
|
|||||||
</FormFieldGrid>
|
</FormFieldGrid>
|
||||||
|
|
||||||
<FormFieldGrid columns={3}>
|
<FormFieldGrid columns={3}>
|
||||||
<FormField label="납기일" htmlFor="dueDate">
|
<FormField label="납기일" htmlFor="dueDate" type="custom">
|
||||||
<Input
|
<Input
|
||||||
id="dueDate"
|
id="dueDate"
|
||||||
type="date"
|
type="date"
|
||||||
@@ -499,7 +679,7 @@ export function QuoteRegistration({
|
|||||||
<div className="col-span-2" />
|
<div className="col-span-2" />
|
||||||
</FormFieldGrid>
|
</FormFieldGrid>
|
||||||
|
|
||||||
<FormField label="비고" htmlFor="remarks">
|
<FormField label="비고" htmlFor="remarks" type="custom">
|
||||||
<Textarea
|
<Textarea
|
||||||
id="remarks"
|
id="remarks"
|
||||||
placeholder="특이사항을 입력하세요"
|
placeholder="특이사항을 입력하세요"
|
||||||
@@ -559,7 +739,7 @@ export function QuoteRegistration({
|
|||||||
{formData.items[activeItemIndex] && (
|
{formData.items[activeItemIndex] && (
|
||||||
<>
|
<>
|
||||||
<FormFieldGrid columns={3}>
|
<FormFieldGrid columns={3}>
|
||||||
<FormField label="층수" htmlFor={`floor-${activeItemIndex}`}>
|
<FormField label="층수" htmlFor={`floor-${activeItemIndex}`} type="custom">
|
||||||
<Input
|
<Input
|
||||||
id={`floor-${activeItemIndex}`}
|
id={`floor-${activeItemIndex}`}
|
||||||
placeholder="예: 1층, B1, 지하1층"
|
placeholder="예: 1층, B1, 지하1층"
|
||||||
@@ -570,7 +750,7 @@ export function QuoteRegistration({
|
|||||||
/>
|
/>
|
||||||
</FormField>
|
</FormField>
|
||||||
|
|
||||||
<FormField label="부호" htmlFor={`code-${activeItemIndex}`}>
|
<FormField label="부호" htmlFor={`code-${activeItemIndex}`} type="custom">
|
||||||
<Input
|
<Input
|
||||||
id={`code-${activeItemIndex}`}
|
id={`code-${activeItemIndex}`}
|
||||||
placeholder="예: A, B, C"
|
placeholder="예: A, B, C"
|
||||||
@@ -583,6 +763,7 @@ export function QuoteRegistration({
|
|||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
label="제품 카테고리 (PC)"
|
label="제품 카테고리 (PC)"
|
||||||
|
type="custom"
|
||||||
required
|
required
|
||||||
error={errors[`item-${activeItemIndex}-productCategory`]}
|
error={errors[`item-${activeItemIndex}-productCategory`]}
|
||||||
htmlFor={`productCategory-${activeItemIndex}`}
|
htmlFor={`productCategory-${activeItemIndex}`}
|
||||||
@@ -610,6 +791,7 @@ export function QuoteRegistration({
|
|||||||
<FormFieldGrid columns={3}>
|
<FormFieldGrid columns={3}>
|
||||||
<FormField
|
<FormField
|
||||||
label="제품명"
|
label="제품명"
|
||||||
|
type="custom"
|
||||||
required
|
required
|
||||||
error={errors[`item-${activeItemIndex}-productName`]}
|
error={errors[`item-${activeItemIndex}-productName`]}
|
||||||
htmlFor={`productName-${activeItemIndex}`}
|
htmlFor={`productName-${activeItemIndex}`}
|
||||||
@@ -619,15 +801,15 @@ export function QuoteRegistration({
|
|||||||
onValueChange={(value) =>
|
onValueChange={(value) =>
|
||||||
handleItemChange(activeItemIndex, "productName", value)
|
handleItemChange(activeItemIndex, "productName", value)
|
||||||
}
|
}
|
||||||
disabled={!formData.items[activeItemIndex].productCategory}
|
disabled={isLoadingProducts}
|
||||||
>
|
>
|
||||||
<SelectTrigger id={`productName-${activeItemIndex}`}>
|
<SelectTrigger id={`productName-${activeItemIndex}`}>
|
||||||
<SelectValue placeholder="제품을 선택하세요" />
|
<SelectValue placeholder={isLoadingProducts ? "로딩 중..." : "제품을 선택하세요"} />
|
||||||
</SelectTrigger>
|
</SelectTrigger>
|
||||||
<SelectContent>
|
<SelectContent>
|
||||||
{(PRODUCTS[formData.items[activeItemIndex].productCategory] || []).map((product) => (
|
{getFilteredProducts(formData.items[activeItemIndex].productCategory).map((product) => (
|
||||||
<SelectItem key={product.value} value={product.value}>
|
<SelectItem key={product.item_code} value={product.item_code}>
|
||||||
{product.label}
|
{product.item_name} ({product.item_code})
|
||||||
</SelectItem>
|
</SelectItem>
|
||||||
))}
|
))}
|
||||||
</SelectContent>
|
</SelectContent>
|
||||||
@@ -636,6 +818,7 @@ export function QuoteRegistration({
|
|||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
label="오픈사이즈 (W0)"
|
label="오픈사이즈 (W0)"
|
||||||
|
type="custom"
|
||||||
required
|
required
|
||||||
error={errors[`item-${activeItemIndex}-openWidth`]}
|
error={errors[`item-${activeItemIndex}-openWidth`]}
|
||||||
htmlFor={`openWidth-${activeItemIndex}`}
|
htmlFor={`openWidth-${activeItemIndex}`}
|
||||||
@@ -652,6 +835,7 @@ export function QuoteRegistration({
|
|||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
label="오픈사이즈 (H0)"
|
label="오픈사이즈 (H0)"
|
||||||
|
type="custom"
|
||||||
required
|
required
|
||||||
error={errors[`item-${activeItemIndex}-openHeight`]}
|
error={errors[`item-${activeItemIndex}-openHeight`]}
|
||||||
htmlFor={`openHeight-${activeItemIndex}`}
|
htmlFor={`openHeight-${activeItemIndex}`}
|
||||||
@@ -670,6 +854,7 @@ export function QuoteRegistration({
|
|||||||
<FormFieldGrid columns={3}>
|
<FormFieldGrid columns={3}>
|
||||||
<FormField
|
<FormField
|
||||||
label="가이드레일 설치 유형 (GT)"
|
label="가이드레일 설치 유형 (GT)"
|
||||||
|
type="custom"
|
||||||
required
|
required
|
||||||
error={errors[`item-${activeItemIndex}-guideRailType`]}
|
error={errors[`item-${activeItemIndex}-guideRailType`]}
|
||||||
htmlFor={`guideRailType-${activeItemIndex}`}
|
htmlFor={`guideRailType-${activeItemIndex}`}
|
||||||
@@ -695,6 +880,7 @@ export function QuoteRegistration({
|
|||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
label="모터 전원 (MP)"
|
label="모터 전원 (MP)"
|
||||||
|
type="custom"
|
||||||
required
|
required
|
||||||
error={errors[`item-${activeItemIndex}-motorPower`]}
|
error={errors[`item-${activeItemIndex}-motorPower`]}
|
||||||
htmlFor={`motorPower-${activeItemIndex}`}
|
htmlFor={`motorPower-${activeItemIndex}`}
|
||||||
@@ -720,6 +906,7 @@ export function QuoteRegistration({
|
|||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
label="연동제어기 (CT)"
|
label="연동제어기 (CT)"
|
||||||
|
type="custom"
|
||||||
required
|
required
|
||||||
error={errors[`item-${activeItemIndex}-controller`]}
|
error={errors[`item-${activeItemIndex}-controller`]}
|
||||||
htmlFor={`controller-${activeItemIndex}`}
|
htmlFor={`controller-${activeItemIndex}`}
|
||||||
@@ -747,6 +934,7 @@ export function QuoteRegistration({
|
|||||||
<FormFieldGrid columns={3}>
|
<FormFieldGrid columns={3}>
|
||||||
<FormField
|
<FormField
|
||||||
label="수량 (QTY)"
|
label="수량 (QTY)"
|
||||||
|
type="custom"
|
||||||
required
|
required
|
||||||
error={errors[`item-${activeItemIndex}-quantity`]}
|
error={errors[`item-${activeItemIndex}-quantity`]}
|
||||||
htmlFor={`quantity-${activeItemIndex}`}
|
htmlFor={`quantity-${activeItemIndex}`}
|
||||||
@@ -764,6 +952,7 @@ export function QuoteRegistration({
|
|||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
label="마구리 날개치수 (WS)"
|
label="마구리 날개치수 (WS)"
|
||||||
|
type="custom"
|
||||||
htmlFor={`wingSize-${activeItemIndex}`}
|
htmlFor={`wingSize-${activeItemIndex}`}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
@@ -778,6 +967,7 @@ export function QuoteRegistration({
|
|||||||
|
|
||||||
<FormField
|
<FormField
|
||||||
label="검사비 (INSP)"
|
label="검사비 (INSP)"
|
||||||
|
type="custom"
|
||||||
htmlFor={`inspectionFee-${activeItemIndex}`}
|
htmlFor={`inspectionFee-${activeItemIndex}`}
|
||||||
>
|
>
|
||||||
<Input
|
<Input
|
||||||
@@ -811,49 +1001,154 @@ export function QuoteRegistration({
|
|||||||
variant="default"
|
variant="default"
|
||||||
className="w-full bg-blue-600 hover:bg-blue-700"
|
className="w-full bg-blue-600 hover:bg-blue-700"
|
||||||
onClick={handleAutoCalculate}
|
onClick={handleAutoCalculate}
|
||||||
|
disabled={isCalculating || isLoadingProducts}
|
||||||
>
|
>
|
||||||
자동 견적 산출 ({formData.items.length}개 항목)
|
{isCalculating ? (
|
||||||
|
<>
|
||||||
|
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
|
||||||
|
견적 산출 중...
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
<>
|
||||||
|
<Calculator className="h-4 w-4 mr-2" />
|
||||||
|
자동 견적 산출 ({formData.items.length}개 항목)
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
|
{/* 견적 산출 결과 표시 */}
|
||||||
|
{calculationResults && calculationResults.items.length > 0 && (
|
||||||
|
<Card className="border-green-200 bg-green-50/50">
|
||||||
|
<CardHeader className="pb-3">
|
||||||
|
<div className="flex items-center justify-between">
|
||||||
|
<CardTitle className="text-base font-semibold flex items-center gap-2">
|
||||||
|
<Calculator className="h-5 w-5 text-green-600" />
|
||||||
|
견적 산출 결과
|
||||||
|
</CardTitle>
|
||||||
|
<Badge variant="default" className="bg-green-600">
|
||||||
|
총 {calculatedGrandTotal.toLocaleString()}원
|
||||||
|
</Badge>
|
||||||
|
</div>
|
||||||
|
</CardHeader>
|
||||||
|
<CardContent className="space-y-4">
|
||||||
|
{/* 항목별 결과 */}
|
||||||
|
{calculationResults.items.map((itemResult, idx) => {
|
||||||
|
const formItem = formData.items[itemResult.index];
|
||||||
|
const product = finishedGoods.find(fg => fg.item_code === formItem?.productName);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div key={idx} className="border border-green-200 rounded-lg p-4 bg-white">
|
||||||
|
<div className="flex items-center justify-between mb-3">
|
||||||
|
<div className="flex items-center gap-2">
|
||||||
|
<Badge variant="outline" className="bg-green-100">
|
||||||
|
견적 {itemResult.index + 1}
|
||||||
|
</Badge>
|
||||||
|
<span className="font-medium">
|
||||||
|
{itemResult.result.finished_goods?.name || product?.item_name || formItem?.productName || "-"}
|
||||||
|
</span>
|
||||||
|
<span className="text-sm text-muted-foreground">
|
||||||
|
({itemResult.result.finished_goods?.code || formItem?.productName || "-"})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="text-right">
|
||||||
|
<div className="text-sm text-muted-foreground">
|
||||||
|
단가: {itemResult.result.grand_total.toLocaleString()}원
|
||||||
|
</div>
|
||||||
|
<div className="font-semibold text-green-700">
|
||||||
|
합계: {((itemResult.result.grand_total || 0) * (formItem?.quantity || 1)).toLocaleString()}원
|
||||||
|
<span className="text-xs text-muted-foreground ml-1">
|
||||||
|
(×{formItem?.quantity || 1})
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* BOM 상세 내역 */}
|
||||||
|
{itemResult.result.items && itemResult.result.items.length > 0 && (
|
||||||
|
<div className="mt-3">
|
||||||
|
<details className="group">
|
||||||
|
<summary className="cursor-pointer text-sm text-muted-foreground hover:text-foreground">
|
||||||
|
BOM 상세 내역 ({itemResult.result.items.length}개 항목)
|
||||||
|
</summary>
|
||||||
|
<div className="mt-2 overflow-x-auto">
|
||||||
|
<table className="w-full text-sm">
|
||||||
|
<thead>
|
||||||
|
<tr className="border-b bg-gray-50">
|
||||||
|
<th className="text-left py-2 px-2">품목코드</th>
|
||||||
|
<th className="text-left py-2 px-2">품목명</th>
|
||||||
|
<th className="text-right py-2 px-2">수량</th>
|
||||||
|
<th className="text-right py-2 px-2">단가</th>
|
||||||
|
<th className="text-right py-2 px-2">금액</th>
|
||||||
|
<th className="text-left py-2 px-2">공정</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{itemResult.result.items.map((bomItem, bomIdx) => (
|
||||||
|
<tr key={bomIdx} className="border-b last:border-0">
|
||||||
|
<td className="py-1.5 px-2 font-mono text-xs">
|
||||||
|
{bomItem.item_code}
|
||||||
|
</td>
|
||||||
|
<td className="py-1.5 px-2">{bomItem.item_name}</td>
|
||||||
|
<td className="py-1.5 px-2 text-right">
|
||||||
|
{bomItem.unit === 'EA'
|
||||||
|
? Math.round((bomItem.quantity || 0) * (formItem?.quantity || 1))
|
||||||
|
: parseFloat(((bomItem.quantity || 0) * (formItem?.quantity || 1)).toFixed(2))
|
||||||
|
} {bomItem.unit || ""}
|
||||||
|
</td>
|
||||||
|
<td className="py-1.5 px-2 text-right">
|
||||||
|
{bomItem.unit_price?.toLocaleString()}
|
||||||
|
</td>
|
||||||
|
<td className="py-1.5 px-2 text-right font-medium">
|
||||||
|
{((bomItem.total_price || 0) * (formItem?.quantity || 1)).toLocaleString()}
|
||||||
|
</td>
|
||||||
|
<td className="py-1.5 px-2">
|
||||||
|
<Badge variant="secondary" className="text-xs">
|
||||||
|
{bomItem.process_group || "-"}
|
||||||
|
</Badge>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
))}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
{/* 공정별 소계 */}
|
||||||
|
{itemResult.result.subtotals && Object.keys(itemResult.result.subtotals).length > 0 && (
|
||||||
|
<div className="mt-2 pt-2 border-t">
|
||||||
|
<div className="flex flex-wrap gap-2">
|
||||||
|
{Object.entries(itemResult.result.subtotals).map(([process, data]) => {
|
||||||
|
// data는 객체 {name, count, subtotal} 또는 숫자일 수 있음
|
||||||
|
const subtotalData = data as { name?: string; count?: number; subtotal?: number } | number;
|
||||||
|
const amount = typeof subtotalData === 'object' ? subtotalData.subtotal : subtotalData;
|
||||||
|
const name = typeof subtotalData === 'object' ? subtotalData.name : process;
|
||||||
|
return (
|
||||||
|
<Badge key={process} variant="outline" className="text-xs">
|
||||||
|
{name || process}: {(amount || 0).toLocaleString()}원
|
||||||
|
</Badge>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
|
||||||
|
{/* 총합계 (수량 반영) */}
|
||||||
|
<div className="border-t pt-4 flex justify-between items-center">
|
||||||
|
<span className="text-lg font-semibold">총 견적 금액</span>
|
||||||
|
<span className="text-2xl font-bold text-green-700">
|
||||||
|
{calculatedGrandTotal.toLocaleString()}원
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
)}
|
||||||
</FormSection>
|
</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>
|
</ResponsiveFormTemplate>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user