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 (
+
+ );
+}
+
+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 && (