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 && (