diff --git a/src/components/quotes/QuoteDocument.tsx b/src/components/quotes/QuoteDocument.tsx
index 2a9fee32..225bf840 100644
--- a/src/components/quotes/QuoteDocument.tsx
+++ b/src/components/quotes/QuoteDocument.tsx
@@ -11,12 +11,14 @@
*/
import { QuoteFormData } from "./QuoteRegistration";
+import type { CompanyFormData } from "@/components/settings/CompanyInfoManagement/types";
interface QuoteDocumentProps {
quote: QuoteFormData;
+ companyInfo?: CompanyFormData | null;
}
-export function QuoteDocument({ quote }: QuoteDocumentProps) {
+export function QuoteDocument({ quote, companyInfo }: QuoteDocumentProps) {
const formatAmount = (amount: number | undefined) => {
if (amount === undefined || amount === null) return '0';
return amount.toLocaleString('ko-KR');
@@ -34,7 +36,7 @@ export function QuoteDocument({ quote }: QuoteDocumentProps) {
itemName: item.productName || '스크린셔터',
spec: `${item.openWidth}×${item.openHeight}`,
quantity: item.quantity || 1,
- unit: '개소',
+ unit: item.unit || '', // 각 품목의 단위 사용, 없으면 빈 문자열
unitPrice: item.unitPrice || 0,
totalPrice: item.totalAmount || 0,
})) || [];
@@ -292,29 +294,29 @@ export function QuoteDocument({ quote }: QuoteDocumentProps) {
| 상호 |
- 동호기업 |
+ {companyInfo?.companyName || '-'} |
사업자등록번호 |
- 139-87-00333 |
+ {companyInfo?.businessNumber || '-'} |
| 대표자 |
- 이 광 호 |
+ {companyInfo?.representativeName || '-'} |
업태 |
- 제조 |
+ {companyInfo?.businessType || '-'} |
| 종목 |
- 방창, 셔터, 금속성호 |
+ {companyInfo?.businessCategory || '-'} |
| 사업장주소 |
- 경기도 안성시 공업용지 오성길 45-22 |
+ {companyInfo?.address || '-'} |
| 전화 |
- 031-983-5130 |
- 팩스 |
- 02-6911-6315 |
+ {companyInfo?.managerPhone || '-'} |
+ 이메일 |
+ {companyInfo?.email || '-'} |
@@ -340,7 +342,7 @@ export function QuoteDocument({ quote }: QuoteDocumentProps) {
모델 |
{quote.items[0]?.productName || '스크린셔터'} |
총 수량 |
- {quote.items.reduce((sum, item) => sum + (item.quantity || 0), 0)}개소 |
+ {quote.items[0]?.quantity || ''}{quote.unitSymbol ? ` ${quote.unitSymbol}` : ''} |
| 오픈사이즈 |
@@ -432,7 +434,7 @@ export function QuoteDocument({ quote }: QuoteDocumentProps) {
{formatDate(quote.registrationDate || '')}
- 공급자: 동호기업 (인)
+ 공급자: {companyInfo?.companyName || '-'} (인)
@@ -452,7 +454,7 @@ export function QuoteDocument({ quote }: QuoteDocumentProps) {
3. 제작 사양 및 수량 변경 시 견적 금액이 변동될 수 있습니다.
4. 현장 여건에 따라 추가 비용이 발생할 수 있습니다.
- 문의: {quote.writer || '담당자'} | {quote.contact || '031-983-5130'}
+ 문의: {companyInfo?.managerName || quote.writer || '담당자'} | {companyInfo?.managerPhone || '-'}
diff --git a/src/components/quotes/QuoteRegistration.tsx b/src/components/quotes/QuoteRegistration.tsx
index 3af7094e..5eef02df 100644
--- a/src/components/quotes/QuoteRegistration.tsx
+++ b/src/components/quotes/QuoteRegistration.tsx
@@ -8,7 +8,7 @@
"use client";
-import { useState, useEffect } from "react";
+import { useState, useEffect, useMemo } from "react";
import { Input } from "../ui/input";
import { Textarea } from "../ui/textarea";
import {
@@ -28,7 +28,7 @@ import {
Plus,
Copy,
Trash2,
- Sparkles,
+ Loader2,
} from "lucide-react";
import { toast } from "sonner";
@@ -50,6 +50,10 @@ import {
FormFieldGrid,
} from "../templates/ResponsiveFormTemplate";
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 {
@@ -64,6 +68,7 @@ export interface QuoteItem {
motorPower: string; // 모터 전원 (MP)
controller: string; // 연동제어기 (CT)
quantity: number; // 수량 (QTY)
+ unit?: string; // 품목 단위
wingSize: string; // 마구리 날개치수 (WS)
inspectionFee: number; // 검사비 (INSP)
unitPrice?: number; // 단가
@@ -83,7 +88,10 @@ export interface QuoteFormData {
contact: string;
dueDate: string;
remarks: string;
+ unitSymbol?: string; // 단위 (EA, 개소 등) - quotes.unit_symbol
items: QuoteItem[];
+ bomMaterials?: BomMaterial[]; // BOM 자재 목록
+ calculationResults?: CalculationResults; // 견적 산출 결과 (저장 시 BOM 자재 변환용)
}
// 초기 견적 항목
@@ -117,58 +125,31 @@ export const INITIAL_QUOTE_FORM: QuoteFormData = {
items: [createNewItem()],
};
-// 샘플 발주처 데이터 (TODO: API에서 가져오기)
-const SAMPLE_CLIENTS = [
- { id: "client-1", name: "인천건설 - 최담당" },
- { id: "client-2", name: "ABC건설" },
- { id: "client-3", name: "XYZ산업" },
-];
-
-// 제품 카테고리 옵션
+// 제품 카테고리 옵션 (MNG 시뮬레이터와 동일)
const PRODUCT_CATEGORIES = [
- { value: "screen", label: "스크린" },
- { value: "steel", label: "철재" },
- { value: "aluminum", label: "알루미늄" },
- { value: "etc", label: "기타" },
+ { value: "ALL", label: "전체" },
+ { value: "SCREEN", label: "스크린" },
+ { value: "STEEL", label: "철재" },
+ { value: "BENDING", label: "절곡" },
+ { value: "ALUMINUM", label: "알루미늄" },
];
-// 제품명 옵션 (카테고리별)
-const PRODUCTS: Record = {
- 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: "기타 제품" },
- ],
-};
-
-// 가이드레일 설치 유형
+// 가이드레일 설치 유형 (API: wall, ceiling, floor)
const GUIDE_RAIL_TYPES = [
- { value: "wall", label: "벽부착형" },
- { value: "ceiling", label: "천장매립형" },
- { value: "floor", label: "바닥매립형" },
+ { value: "wall", label: "벽면형" },
+ { value: "floor", label: "측면형" },
];
-// 모터 전원
+// 모터 전원 (API: single=단상220V, three=삼상380V)
const MOTOR_POWERS = [
- { value: "single", label: "단상 220V" },
- { value: "three", label: "삼상 380V" },
+ { value: "single", label: "220V (단상)" },
+ { value: "three", label: "380V (삼상)" },
];
-// 연동제어기
+// 연동제어기 (API: basic, smart, premium)
const CONTROLLERS = [
- { value: "basic", label: "기본 제어기" },
- { value: "smart", label: "스마트 제어기" },
- { value: "premium", label: "프리미엄 제어기" },
+ { value: "basic", label: "단독" },
+ { value: "smart", label: "연동" },
];
interface QuoteRegistrationProps {
@@ -191,13 +172,118 @@ export function QuoteRegistration({
const [isSaving, setIsSaving] = useState(false);
const [activeItemIndex, setActiveItemIndex] = useState(0);
- // editingQuote가 변경되면 formData 업데이트
+ // 완제품 목록 상태 (API에서 로드)
+ const [finishedGoods, setFinishedGoods] = useState([]);
+ const [isLoadingProducts, setIsLoadingProducts] = useState(false);
+ const [isCalculating, setIsCalculating] = useState(false);
+
+ // 거래처 목록 상태 (API에서 로드)
+ const [clients, setClients] = useState([]);
+ 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([]);
+
+ // 수량 반영 총합계 계산 (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(() => {
+ 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) {
setFormData(editingQuote);
+ // 수정 모드 진입 시 이전 산출 결과 초기화
+ setCalculationResults(null);
}
}, [editingQuote]);
+ // 카테고리별 완제품 필터링
+ const getFilteredProducts = (category: string) => {
+ if (!category || category === "ALL") {
+ return finishedGoods; // 전체 선택 시 모든 완제품
+ }
+ return finishedGoods.filter(fg => fg.item_category === category);
+ };
+
// 유효성 검사
const validateForm = (): boolean => {
const newErrors: Record = {};
@@ -249,7 +335,13 @@ export function QuoteRegistration({
setErrors({});
setIsSaving(true);
try {
- await onSave(formData);
+ // calculationResults를 formData에 포함하여 저장
+ // transformFormDataToApi에서 BOM 자재의 base_quantity, calculated_quantity를 제대로 설정하기 위함
+ const dataToSave: QuoteFormData = {
+ ...formData,
+ calculationResults: calculationResults || undefined,
+ };
+ await onSave(dataToSave);
toast.success(
editingQuote ? "견적이 수정되었습니다." : "견적이 등록되었습니다."
);
@@ -265,6 +357,10 @@ export function QuoteRegistration({
field: keyof QuoteFormData,
value: string | QuoteItem[]
) => {
+ // DEBUG: manager, contact, remarks 필드 변경 추적
+ if (field === 'manager' || field === 'contact' || field === 'remarks') {
+ console.log(`[handleFieldChange] ${field} 변경:`, value);
+ }
setFormData({ ...formData, [field]: value });
if (errors[field]) {
setErrors((prev) => {
@@ -277,11 +373,11 @@ export function QuoteRegistration({
// 발주처 선택
const handleClientChange = (clientId: string) => {
- const client = SAMPLE_CLIENTS.find((c) => c.id === clientId);
+ const client = clients.find((c) => c.id === clientId);
setFormData({
...formData,
clientId,
- clientName: client?.name || "",
+ clientName: client?.vendorName || "",
});
};
@@ -347,14 +443,90 @@ export function QuoteRegistration({
};
// 자동 견적 산출
- const handleAutoCalculate = () => {
- toast.info(`자동 견적 산출 (${formData.items.length}개 항목) - API 연동 필요`);
+ const handleAutoCalculate = async () => {
+ // 필수 입력값 검사
+ 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 = () => {
- toast.info("완벽한 샘플 생성 - API 연동 필요");
- };
+ // 렌더링 직전 디버그 로그
+ console.log('[QuoteRegistration] 렌더링 직전 formData.items[0]:', JSON.stringify({
+ quantity: formData.items[0]?.quantity,
+ wingSize: formData.items[0]?.wingSize,
+ inspectionFee: formData.items[0]?.inspectionFee,
+ }, null, 2));
return (
-
+
-
+
- {SAMPLE_CLIENTS.map((client) => (
+ {clients.map((client) => (
- {client.name}
+ {client.vendorName}
))}
@@ -459,16 +633,22 @@ export function QuoteRegistration({
-
+
handleFieldChange("siteName", e.target.value)}
/>
+
-
+
-
+
-
+
-
+
);
}
\ No newline at end of file