From db70147468595adfbfb31fd0f0cde0ed680a7596 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B6=8C=ED=98=81=EC=84=B1?= Date: Thu, 29 Jan 2026 08:17:25 +0900 Subject: [PATCH] =?UTF-8?q?feat:=20=EC=88=98=EC=8B=9D=EB=B3=B4=EA=B8=B0=20?= =?UTF-8?q?=EB=AA=A8=EB=8B=AC=20=EC=B6=94=EA=B0=80=20(=EA=B0=9C=EB=B0=9C?= =?UTF-8?q?=ED=99=98=EA=B2=BD=20=EC=A0=84=EC=9A=A9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - FormulaViewModal 컴포넌트 신규 생성 - 10단계 계산 과정을 수식 형태로 표시 - 변수 계산: 수식 + 대입값 + 결과 테이블 형식 - 품목별 수량/금액 계산 과정 표시 - QuoteFooterBar에 수식보기 버튼 추가 - NEXT_PUBLIC_APP_ENV가 local/development일 때만 버튼 표시 - BomDebugStep, formulas 타입 추가 - calculateBomBulk에 debug=true 파라미터 추가 --- src/components/quotes/FormulaViewModal.tsx | 316 ++++++++++++++++++ src/components/quotes/QuoteFooterBar.tsx | 21 +- src/components/quotes/QuoteRegistrationV2.tsx | 16 + src/components/quotes/actions.ts | 73 +++- src/components/quotes/types.ts | 30 ++ 5 files changed, 452 insertions(+), 4 deletions(-) create mode 100644 src/components/quotes/FormulaViewModal.tsx diff --git a/src/components/quotes/FormulaViewModal.tsx b/src/components/quotes/FormulaViewModal.tsx new file mode 100644 index 00000000..f589cae2 --- /dev/null +++ b/src/components/quotes/FormulaViewModal.tsx @@ -0,0 +1,316 @@ +/** + * 수식 보기 모달 (개발용) + * - 10단계 계산 과정 (debug_steps) + * - 실제 계산 수식 표시 + */ + +"use client"; + +import { Calculator, ChevronDown, ChevronRight } from "lucide-react"; +import { useState } from "react"; +import { + Dialog, + DialogContent, + DialogHeader, + DialogTitle, +} from "../ui/dialog"; +import { ScrollArea } from "../ui/scroll-area"; +import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs"; + +import type { LocationItem, BomDebugStep } from "./types"; + +interface FormulaViewModalProps { + open: boolean; + onOpenChange: (open: boolean) => void; + locations: LocationItem[]; +} + +// 수식 항목 타입 +interface FormulaItem { + var?: string; + desc?: string; + formula?: string; + calculation?: string; + result?: number | string; + value?: number | string; + unit?: string; + item?: string; + qty_formula?: string; + qty_result?: number; + unit_price?: number; + price_formula?: string; + price_calc?: string; + total?: number; + category?: string; +} + +export function FormulaViewModal({ + open, + onOpenChange, + locations, +}: FormulaViewModalProps) { + const locationsWithBom = locations.filter((loc) => loc.bomResult); + + if (locationsWithBom.length === 0) { + return ( + + + + + + 수식 및 계산 결과 + + +
+ 계산된 결과가 없습니다. 견적 산출을 먼저 실행해주세요. +
+
+
+ ); + } + + return ( + + + + + + 수식 및 계산 결과 + + + + + + {locationsWithBom.map((loc) => ( + + {loc.floor}/{loc.code} + + ))} + + + {locationsWithBom.map((location) => ( + + + + + + ))} + + + + ); +} + +function LocationDetail({ location }: { location: LocationItem }) { + const bom = location.bomResult; + if (!bom) return null; + + const debugSteps = bom.debug_steps || []; + + return ( +
+ {/* 10단계 계산 과정 */} + {debugSteps.length > 0 ? ( +
+ {debugSteps.map((step, idx) => ( + + ))} +
+ ) : ( +
+ 수식 정보가 없습니다. 견적을 다시 산출하면 10단계 계산 과정이 표시됩니다. +
+ )} + + {/* 합계 */} +
+
+ 완제품:{" "} + {bom.finished_goods?.code}{" "} + {bom.finished_goods?.name} +
+
+ 1개당 + {bom.grand_total.toLocaleString()}원 + × + {location.quantity}개 = + + {(bom.grand_total * location.quantity).toLocaleString()}원 + +
+
+
+ ); +} + +function DebugStepCard({ step }: { step: BomDebugStep }) { + const [expanded, setExpanded] = useState(step.step >= 1 && step.step <= 3); + + const stepColors: Record = { + 0: "bg-gray-400", + 1: "bg-blue-500", + 2: "bg-blue-600", + 3: "bg-green-500", + 4: "bg-green-600", + 5: "bg-yellow-500", + 6: "bg-yellow-600", + 7: "bg-orange-500", + 8: "bg-purple-500", + 9: "bg-purple-600", + 10: "bg-red-500", + }; + + // formulas 배열이 있는지 확인 + const formulas = step.data.formulas as FormulaItem[] | undefined; + const hasFormulas = formulas && Array.isArray(formulas) && formulas.length > 0; + + return ( +
+ + {expanded && ( +
+ {hasFormulas ? ( + + ) : ( +
+              {JSON.stringify(step.data, null, 2)}
+            
+ )} +
+ )} +
+ ); +} + +function FormulaTable({ formulas, stepName }: { formulas: FormulaItem[]; stepName: string }) { + // 입력값 (Step 1) + if (formulas[0]?.var && formulas[0]?.value !== undefined) { + return ( + + + + + + + + + + + {formulas.map((f, i) => ( + + + + + + + ))} + +
변수설명단위
{f.var}{f.desc}{typeof f.value === 'number' ? f.value.toLocaleString() : f.value}{f.unit}
+ ); + } + + // 변수 계산 (Step 3) - formula, calculation, result + if (formulas[0]?.formula && formulas[0]?.calculation) { + return ( + + + + + + + + + + + {formulas.map((f, i) => ( + + + + + + + ))} + +
변수수식대입결과
+ {f.var} + {f.desc} + {f.formula}{f.calculation} + {typeof f.result === 'number' ? f.result.toLocaleString() : f.result} + {f.unit} +
+ ); + } + + // 품목 수량/금액 계산 (Step 6, 7) + if (formulas[0]?.item) { + return ( + + + + + + + + + + + + + {formulas.map((f, i) => ( + + + + + + + + + ))} + +
품목수량 수식수량단가금액 계산금액
{f.item}{f.qty_formula}{f.qty_result}{f.unit_price?.toLocaleString()}{f.price_calc}{f.total?.toLocaleString()}
+ ); + } + + // 소계 (Step 9) + if (formulas[0]?.category) { + return ( + + + + + + + + + + {formulas.map((f, i) => ( + + + + + + ))} + +
카테고리포함 품목소계
{f.category}{f.formula}{f.result?.toLocaleString()}원
+ ); + } + + // 기본 폴백 + return ( +
+      {JSON.stringify(formulas, null, 2)}
+    
+ ); +} \ No newline at end of file diff --git a/src/components/quotes/QuoteFooterBar.tsx b/src/components/quotes/QuoteFooterBar.tsx index e272aadc..abf1d54e 100644 --- a/src/components/quotes/QuoteFooterBar.tsx +++ b/src/components/quotes/QuoteFooterBar.tsx @@ -8,7 +8,7 @@ "use client"; -import { Save, Check, ArrowLeft, Loader2, FileText, Pencil, ClipboardList, Percent } from "lucide-react"; +import { Save, Check, ArrowLeft, Loader2, FileText, Pencil, ClipboardList, Percent, Calculator } from "lucide-react"; import { Button } from "../ui/button"; @@ -36,6 +36,10 @@ interface QuoteFooterBarProps { onOrderRegister?: () => void; /** 할인하기 */ onDiscount?: () => void; + /** 수식보기 */ + onFormulaView?: () => void; + /** BOM 결과 유무 */ + hasBomResult?: boolean; isSaving?: boolean; disabled?: boolean; /** view 모드 여부 (view: 수정+최종확정, edit: 저장+최종확정) */ @@ -58,6 +62,8 @@ export function QuoteFooterBar({ onEdit, onOrderRegister, onDiscount, + onFormulaView, + hasBomResult = false, isSaving = false, disabled = false, isViewMode = false, @@ -109,6 +115,19 @@ export function QuoteFooterBar({ 거래명세서 보기 + {/* 수식보기 - 개발환경(local/development)에서만 표시 */} + {onFormulaView && ['local', 'development'].includes(process.env.NEXT_PUBLIC_APP_ENV || '') && ( + + )} + {/* 수정 - view 모드에서만 표시 */} {isViewMode && onEdit && (