From f1c4ab62bf85877e569fc8ba6df4e43d65ccf424 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Wed, 4 Feb 2026 23:04:47 +0900 Subject: [PATCH] =?UTF-8?q?feat(WEB):=20=EC=88=98=EC=A3=BC=EB=93=B1?= =?UTF-8?q?=EB=A1=9D=20=ED=92=88=EB=AA=A9=EC=9D=84=20=EC=A0=9C=ED=92=88=20?= =?UTF-8?q?=EB=AA=A8=EB=8D=B8+=ED=83=80=EC=9E=85=EB=B3=84=20=EA=B7=B8?= =?UTF-8?q?=EB=A3=B9=ED=95=91=20=ED=91=9C=EC=8B=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 견적의 calculation_inputs에서 productCode 정보를 수주등록으로 전달 - quote_items.note 파싱으로 floor/code 추출 (type_code/symbol 컬럼 부재 대응) - 제품코드(FG-KWE01-벽면형-SUS)에서 그룹핑 키(KWE01-SUS) 추출 - 그룹 내 동일 품목(item_code 기준) 수량/금액 합산하여 1행으로 표시 - quotes/actions.ts BomCalculationResult 타입을 types.ts와 일치시켜 TS 에러 해결 --- src/components/orders/OrderRegistration.tsx | 327 ++++++++++++++++---- src/components/orders/actions.ts | 52 +++- src/components/quotes/QuoteSummaryPanel.tsx | 2 +- src/components/quotes/actions.ts | 9 +- 4 files changed, 316 insertions(+), 74 deletions(-) diff --git a/src/components/orders/OrderRegistration.tsx b/src/components/orders/OrderRegistration.tsx index 2afedb23..40938e54 100644 --- a/src/components/orders/OrderRegistration.tsx +++ b/src/components/orders/OrderRegistration.tsx @@ -11,7 +11,7 @@ * - 품목 내역 섹션 */ -import { useState, useEffect, useCallback } from "react"; +import { useState, useEffect, useCallback, useMemo } from "react"; import { useDaumPostcode } from "@/hooks/useDaumPostcode"; import { useClientList } from "@/hooks/useClientList"; import { Input } from "@/components/ui/input"; @@ -232,6 +232,136 @@ export function OrderRegistration({ }, []) ); + // 제품코드에서 그룹핑 키 추출: FG-KWE01-벽면형-SUS → KWE01-SUS + const extractGroupKey = useCallback((productName: string): string => { + const parts = productName.split('-'); + if (parts.length >= 4) { + // FG-{model}-{installationType}-{finishType} + return `${parts[1]}-${parts[3]}`; + } + return productName; + }, []); + + // 아이템을 제품 모델+타입별로 그룹핑 (제품 단위 집약) + const itemGroups = useMemo(() => { + const calcItems = form.selectedQuotation?.calculationInputs?.items; + if (!calcItems || calcItems.length === 0) { + return null; + } + + // floor+code → productCode 매핑 + const locationProductMap = new Map(); + calcItems.forEach(ci => { + if (ci.floor && ci.code && ci.productCode) { + locationProductMap.set(`${ci.floor}|${ci.code}`, ci.productCode); + } + }); + + // 그룹별 데이터 집계 + const groups = new Map; // 개소 목록 + quantity: number; // 개소별 수량 합계 (calculation_inputs 기준) + }>(); + const ungrouped: OrderItem[] = []; + + form.items.forEach(item => { + const locKey = `${item.type}|${item.symbol}`; + const productCode = locationProductMap.get(locKey); + if (productCode) { + const groupKey = extractGroupKey(productCode); + if (!groups.has(groupKey)) { + groups.set(groupKey, { items: [], productCode, locations: new Set(), quantity: 0 }); + } + const g = groups.get(groupKey)!; + g.items.push(item); + g.locations.add(locKey); + } else { + ungrouped.push(item); + } + }); + + // calculation_inputs에서 개소별 수량 합산 + calcItems.forEach(ci => { + if (ci.productCode) { + const groupKey = extractGroupKey(ci.productCode); + const g = groups.get(groupKey); + if (g) { + g.quantity += ci.quantity ?? 1; + } + } + }); + + if (groups.size <= 1 && ungrouped.length === 0) { + return null; + } + + // 그룹 내 동일 품목(item_code) 합산 + const aggregateItems = (items: OrderItem[]) => { + const map = new Map(); + items.forEach(item => { + const code = item.itemCode || item.itemName; + if (map.has(code)) { + const existing = map.get(code)!; + existing.quantity += item.quantity; + existing.amount = (existing.amount ?? 0) + (item.amount ?? 0); + existing._sourceIds.push(item.id); + } else { + map.set(code, { + ...item, + quantity: item.quantity, + amount: item.amount ?? 0, + _sourceIds: [item.id], + }); + } + }); + return Array.from(map.values()); + }; + + const result: Array<{ + key: string; + label: string; + productCode: string; + locationCount: number; + quantity: number; + amount: number; + items: OrderItem[]; + aggregatedItems: (OrderItem & { _sourceIds: string[] })[]; + }> = []; + let orderNum = 1; + groups.forEach((value, key) => { + const amount = value.items.reduce((sum, item) => sum + (item.amount ?? 0), 0); + result.push({ + key, + label: `수주 ${orderNum}: ${key}`, + productCode: key, + locationCount: value.locations.size, + quantity: value.quantity, + amount, + items: value.items, + aggregatedItems: aggregateItems(value.items), + }); + orderNum++; + }); + + if (ungrouped.length > 0) { + const amount = ungrouped.reduce((sum, item) => sum + (item.amount ?? 0), 0); + result.push({ + key: '_ungrouped', + label: '기타', + productCode: '', + locationCount: 0, + quantity: ungrouped.length, + amount, + items: ungrouped, + aggregatedItems: aggregateItems(ungrouped), + }); + } + + return result; + }, [form.items, form.selectedQuotation?.calculationInputs, extractGroupKey]); + // 견적 선택 핸들러 const handleQuotationSelect = (quotation: QuotationForSelect) => { // 견적 정보로 폼 자동 채우기 (견적에서 가져온 품목은 삭제 불가) @@ -769,74 +899,140 @@ export function OrderRegistration({

{fieldErrors.items}

)} {/* 품목 테이블 */} -
- - - - 순번 - 품목코드 - 품명 - 규격 - 수량 - 단위 - 단가 - 금액 - - - - - {form.items.length === 0 ? ( + {itemGroups ? ( + // 그룹핑 표시 +
+ {itemGroups.map((group) => { + return ( +
+
+
+ + {group.label} + + + ({group.locationCount}개소 / {group.quantity}대) + +
+ + 소계: {formatAmount(group.amount)} + +
+
+ + + 순번 + 품목코드 + 품명 + 규격 + 수량 + 단위 + 단가 + 금액 + + + + + {group.aggregatedItems.map((item, index) => ( + + {index + 1} + + + {item.itemCode} + + + {item.itemName} + {item.spec} + + {item.quantity} + + {item.unit} + + {formatAmount(item.unitPrice)} + + + {formatAmount(item.amount ?? 0)} + + + + ))} + +
+
+ ); + })} + + ) : ( + // 기본 플랫 리스트 +
+ + - - 품목이 없습니다. 견적을 선택하거나 품목을 추가해주세요. - + 순번 + 품목코드 + 품명 + 규격 + 수량 + 단위 + 단가 + 금액 + - ) : ( - form.items.map((item, index) => ( - - {index + 1} - - - {item.itemCode} - - - {item.itemName} - {item.spec} - - - handleQuantityChange( - item.id, - value ?? 1 - ) - } - className="w-16 text-center" - /> - - {item.unit} - - {formatAmount(item.unitPrice)} - - - {formatAmount(item.amount ?? 0)} - - - + + + {form.items.length === 0 ? ( + + + 품목이 없습니다. 견적을 선택하거나 품목을 추가해주세요. - )) - )} - -
-
+ ) : ( + form.items.map((item, index) => ( + + {index + 1} + + + {item.itemCode} + + + {item.itemName} + {item.spec} + + + handleQuantityChange( + item.id, + value ?? 1 + ) + } + className="w-16 text-center" + /> + + {item.unit} + + {formatAmount(item.unitPrice)} + + + {formatAmount(item.amount ?? 0)} + + + + + + )) + )} + + + + )} {/* 품목 추가 버튼 */}