feat: 수식보기 모달 추가 (개발환경 전용)
- FormulaViewModal 컴포넌트 신규 생성 - 10단계 계산 과정을 수식 형태로 표시 - 변수 계산: 수식 + 대입값 + 결과 테이블 형식 - 품목별 수량/금액 계산 과정 표시 - QuoteFooterBar에 수식보기 버튼 추가 - NEXT_PUBLIC_APP_ENV가 local/development일 때만 버튼 표시 - BomDebugStep, formulas 타입 추가 - calculateBomBulk에 debug=true 파라미터 추가
This commit is contained in:
316
src/components/quotes/FormulaViewModal.tsx
Normal file
316
src/components/quotes/FormulaViewModal.tsx
Normal file
@@ -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 (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="max-w-2xl">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Calculator className="h-5 w-5" />
|
||||
수식 및 계산 결과
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<div className="py-8 text-center text-gray-500">
|
||||
계산된 결과가 없습니다. 견적 산출을 먼저 실행해주세요.
|
||||
</div>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Dialog open={open} onOpenChange={onOpenChange}>
|
||||
<DialogContent className="sm:max-w-[95vw] w-[1600px] max-h-[90vh]">
|
||||
<DialogHeader>
|
||||
<DialogTitle className="flex items-center gap-2">
|
||||
<Calculator className="h-5 w-5" />
|
||||
수식 및 계산 결과
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
|
||||
<Tabs defaultValue={locationsWithBom[0]?.id} className="w-full">
|
||||
<TabsList className="mb-3 flex-wrap h-auto gap-1">
|
||||
{locationsWithBom.map((loc) => (
|
||||
<TabsTrigger key={loc.id} value={loc.id} className="text-xs">
|
||||
{loc.floor}/{loc.code}
|
||||
</TabsTrigger>
|
||||
))}
|
||||
</TabsList>
|
||||
|
||||
{locationsWithBom.map((location) => (
|
||||
<TabsContent key={location.id} value={location.id}>
|
||||
<ScrollArea className="h-[78vh] pr-4">
|
||||
<LocationDetail location={location} />
|
||||
</ScrollArea>
|
||||
</TabsContent>
|
||||
))}
|
||||
</Tabs>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
|
||||
function LocationDetail({ location }: { location: LocationItem }) {
|
||||
const bom = location.bomResult;
|
||||
if (!bom) return null;
|
||||
|
||||
const debugSteps = bom.debug_steps || [];
|
||||
|
||||
return (
|
||||
<div className="space-y-2">
|
||||
{/* 10단계 계산 과정 */}
|
||||
{debugSteps.length > 0 ? (
|
||||
<div className="space-y-1">
|
||||
{debugSteps.map((step, idx) => (
|
||||
<DebugStepCard key={idx} step={step} />
|
||||
))}
|
||||
</div>
|
||||
) : (
|
||||
<div className="text-sm text-gray-500 p-3 bg-yellow-50 rounded border border-yellow-200">
|
||||
수식 정보가 없습니다. 견적을 다시 산출하면 10단계 계산 과정이 표시됩니다.
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 합계 */}
|
||||
<div className="flex justify-between items-center bg-blue-50 p-3 rounded-lg border border-blue-200 mt-4">
|
||||
<div className="text-sm">
|
||||
<span className="text-gray-600">완제품:</span>{" "}
|
||||
<span className="font-mono">{bom.finished_goods?.code}</span>{" "}
|
||||
<span className="text-gray-500">{bom.finished_goods?.name}</span>
|
||||
</div>
|
||||
<div className="text-right">
|
||||
<span className="text-sm text-gray-600">1개당 </span>
|
||||
<span className="font-bold text-blue-600">{bom.grand_total.toLocaleString()}원</span>
|
||||
<span className="mx-2">×</span>
|
||||
<span className="text-sm">{location.quantity}개 =</span>
|
||||
<span className="font-bold text-green-600 text-lg ml-2">
|
||||
{(bom.grand_total * location.quantity).toLocaleString()}원
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function DebugStepCard({ step }: { step: BomDebugStep }) {
|
||||
const [expanded, setExpanded] = useState(step.step >= 1 && step.step <= 3);
|
||||
|
||||
const stepColors: Record<number, string> = {
|
||||
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 (
|
||||
<div className="border rounded overflow-hidden">
|
||||
<button
|
||||
onClick={() => setExpanded(!expanded)}
|
||||
className="w-full flex items-center gap-2 px-3 py-2 bg-gray-50 hover:bg-gray-100 text-left text-sm"
|
||||
>
|
||||
{expanded ? <ChevronDown className="h-4 w-4" /> : <ChevronRight className="h-4 w-4" />}
|
||||
<span className={`w-6 h-6 rounded text-white text-xs flex items-center justify-center flex-shrink-0 ${stepColors[step.step] || "bg-gray-500"}`}>
|
||||
{step.step}
|
||||
</span>
|
||||
<span className="font-medium">{step.name}</span>
|
||||
{hasFormulas && (
|
||||
<span className="text-xs text-blue-500 ml-2">({formulas.length}개 수식)</span>
|
||||
)}
|
||||
</button>
|
||||
{expanded && (
|
||||
<div className="p-3 bg-white border-t">
|
||||
{hasFormulas ? (
|
||||
<FormulaTable formulas={formulas} stepName={step.name} />
|
||||
) : (
|
||||
<pre className="text-xs bg-gray-50 p-2 rounded overflow-x-auto max-h-40 border font-mono">
|
||||
{JSON.stringify(step.data, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
function FormulaTable({ formulas, stepName }: { formulas: FormulaItem[]; stepName: string }) {
|
||||
// 입력값 (Step 1)
|
||||
if (formulas[0]?.var && formulas[0]?.value !== undefined) {
|
||||
return (
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="p-2 text-left border w-28">변수</th>
|
||||
<th className="p-2 text-left border">설명</th>
|
||||
<th className="p-2 text-right border w-32">값</th>
|
||||
<th className="p-2 text-left border w-20">단위</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{formulas.map((f, i) => (
|
||||
<tr key={i} className="border-b hover:bg-gray-50">
|
||||
<td className="p-2 border font-mono font-bold text-blue-600">{f.var}</td>
|
||||
<td className="p-2 border text-gray-600">{f.desc}</td>
|
||||
<td className="p-2 border text-right font-mono">{typeof f.value === 'number' ? f.value.toLocaleString() : f.value}</td>
|
||||
<td className="p-2 border text-gray-500">{f.unit}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
// 변수 계산 (Step 3) - formula, calculation, result
|
||||
if (formulas[0]?.formula && formulas[0]?.calculation) {
|
||||
return (
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="p-2 text-left border w-36">변수</th>
|
||||
<th className="p-2 text-left border">수식</th>
|
||||
<th className="p-2 text-left border">대입</th>
|
||||
<th className="p-2 text-right border w-28">결과</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{formulas.map((f, i) => (
|
||||
<tr key={i} className="border-b hover:bg-gray-50">
|
||||
<td className="p-2 border">
|
||||
<span className="font-mono font-bold text-blue-600">{f.var}</span>
|
||||
<span className="text-gray-500 text-xs ml-2">{f.desc}</span>
|
||||
</td>
|
||||
<td className="p-2 border font-mono text-purple-600">{f.formula}</td>
|
||||
<td className="p-2 border font-mono text-gray-600">{f.calculation}</td>
|
||||
<td className="p-2 border text-right">
|
||||
<span className="font-mono font-bold">{typeof f.result === 'number' ? f.result.toLocaleString() : f.result}</span>
|
||||
<span className="text-gray-500 text-xs ml-1">{f.unit}</span>
|
||||
</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
// 품목 수량/금액 계산 (Step 6, 7)
|
||||
if (formulas[0]?.item) {
|
||||
return (
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="p-2 text-left border">품목</th>
|
||||
<th className="p-2 text-left border w-40">수량 수식</th>
|
||||
<th className="p-2 text-right border w-20">수량</th>
|
||||
<th className="p-2 text-right border w-24">단가</th>
|
||||
<th className="p-2 text-left border w-40">금액 계산</th>
|
||||
<th className="p-2 text-right border w-28">금액</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{formulas.map((f, i) => (
|
||||
<tr key={i} className="border-b hover:bg-gray-50">
|
||||
<td className="p-2 border font-medium">{f.item}</td>
|
||||
<td className="p-2 border font-mono text-purple-600 text-xs">{f.qty_formula}</td>
|
||||
<td className="p-2 border text-right font-mono">{f.qty_result}</td>
|
||||
<td className="p-2 border text-right font-mono">{f.unit_price?.toLocaleString()}</td>
|
||||
<td className="p-2 border font-mono text-gray-600 text-xs">{f.price_calc}</td>
|
||||
<td className="p-2 border text-right font-mono font-bold">{f.total?.toLocaleString()}</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
// 소계 (Step 9)
|
||||
if (formulas[0]?.category) {
|
||||
return (
|
||||
<table className="w-full text-sm border-collapse">
|
||||
<thead>
|
||||
<tr className="bg-gray-100">
|
||||
<th className="p-2 text-left border w-32">카테고리</th>
|
||||
<th className="p-2 text-left border">포함 품목</th>
|
||||
<th className="p-2 text-right border w-28">소계</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{formulas.map((f, i) => (
|
||||
<tr key={i} className="border-b hover:bg-gray-50">
|
||||
<td className="p-2 border font-medium">{f.category}</td>
|
||||
<td className="p-2 border text-gray-600 text-xs">{f.formula}</td>
|
||||
<td className="p-2 border text-right font-mono font-bold">{f.result?.toLocaleString()}원</td>
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
// 기본 폴백
|
||||
return (
|
||||
<pre className="text-xs bg-gray-50 p-2 rounded overflow-x-auto max-h-40 border font-mono">
|
||||
{JSON.stringify(formulas, null, 2)}
|
||||
</pre>
|
||||
);
|
||||
}
|
||||
@@ -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({
|
||||
거래명세서 보기
|
||||
</Button>
|
||||
|
||||
{/* 수식보기 - 개발환경(local/development)에서만 표시 */}
|
||||
{onFormulaView && ['local', 'development'].includes(process.env.NEXT_PUBLIC_APP_ENV || '') && (
|
||||
<Button
|
||||
onClick={onFormulaView}
|
||||
disabled={!hasBomResult}
|
||||
variant="outline"
|
||||
className="gap-2 px-6 border-purple-300 text-purple-600 hover:bg-purple-50"
|
||||
>
|
||||
<Calculator className="h-4 w-4" />
|
||||
수식보기
|
||||
</Button>
|
||||
)}
|
||||
|
||||
{/* 수정 - view 모드에서만 표시 */}
|
||||
{isViewMode && onEdit && (
|
||||
<Button
|
||||
|
||||
@@ -35,6 +35,7 @@ import { QuoteFooterBar } from "./QuoteFooterBar";
|
||||
import { QuotePreviewModal } from "./QuotePreviewModal";
|
||||
import { QuoteTransactionModal } from "./QuoteTransactionModal";
|
||||
import { DiscountModal } from "./DiscountModal";
|
||||
import { FormulaViewModal } from "./FormulaViewModal";
|
||||
|
||||
import {
|
||||
getFinishedGoods,
|
||||
@@ -179,6 +180,7 @@ export function QuoteRegistrationV2({
|
||||
const [quotePreviewOpen, setQuotePreviewOpen] = useState(false);
|
||||
const [transactionPreviewOpen, setTransactionPreviewOpen] = useState(false);
|
||||
const [discountModalOpen, setDiscountModalOpen] = useState(false);
|
||||
const [formulaViewOpen, setFormulaViewOpen] = useState(false);
|
||||
const [discountRate, setDiscountRate] = useState(0);
|
||||
const [discountAmount, setDiscountAmount] = useState(0);
|
||||
const pendingAutoCalculateRef = useRef(false);
|
||||
@@ -323,6 +325,11 @@ export function QuoteRegistrationV2({
|
||||
}));
|
||||
}, [formData.locations]);
|
||||
|
||||
// BOM 결과 유무
|
||||
const hasBomResult = useMemo(() => {
|
||||
return formData.locations.some((loc) => loc.bomResult);
|
||||
}, [formData.locations]);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// 작성자 자동 설정 (create 모드에서 로그인 사용자 정보 로드)
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -909,6 +916,8 @@ export function QuoteRegistrationV2({
|
||||
onEdit={onEdit}
|
||||
onOrderRegister={onOrderRegister}
|
||||
onDiscount={() => setDiscountModalOpen(true)}
|
||||
onFormulaView={() => setFormulaViewOpen(true)}
|
||||
hasBomResult={hasBomResult}
|
||||
isSaving={isSaving}
|
||||
isViewMode={isViewMode}
|
||||
/>
|
||||
@@ -940,6 +949,13 @@ export function QuoteRegistrationV2({
|
||||
initialDiscountAmount={discountAmount}
|
||||
onApply={handleApplyDiscount}
|
||||
/>
|
||||
|
||||
{/* 수식보기 모달 */}
|
||||
<FormulaViewModal
|
||||
open={formulaViewOpen}
|
||||
onOpenChange={setFormulaViewOpen}
|
||||
locations={formData.locations}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -932,6 +932,8 @@ export interface BomCalculationResult {
|
||||
}>;
|
||||
subtotals: Record<string, { name?: string; count?: number; subtotal?: number; items?: unknown[] } | number>;
|
||||
grand_total: number;
|
||||
variables?: Record<string, unknown>; // 계산된 변수들
|
||||
debug_steps?: Array<{ step: number; name: string; data: Record<string, unknown> }>; // 10단계 계산 과정
|
||||
}
|
||||
|
||||
// API 서버 응답 구조 (QuoteCalculationService::calculateBomBulk)
|
||||
@@ -951,7 +953,7 @@ export interface BomBulkResponse {
|
||||
}>;
|
||||
}
|
||||
|
||||
export async function calculateBomBulk(items: BomCalculateItem[]): Promise<{
|
||||
export async function calculateBomBulk(items: BomCalculateItem[], debug: boolean = true): Promise<{
|
||||
success: boolean;
|
||||
data: BomBulkResponse | null;
|
||||
error?: string;
|
||||
@@ -960,11 +962,11 @@ export async function calculateBomBulk(items: BomCalculateItem[]): Promise<{
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/calculate/bom/bulk`;
|
||||
|
||||
console.log('[QuoteActions] POST calculate BOM bulk:', { items });
|
||||
console.log('[QuoteActions] POST calculate BOM bulk:', { items, debug });
|
||||
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ items }),
|
||||
body: JSON.stringify({ items, debug }),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
@@ -1023,6 +1025,71 @@ export async function calculateBomBulk(items: BomCalculateItem[]): Promise<{
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 품목 단가 조회 =====
|
||||
export interface ItemPriceResult {
|
||||
item_code: string;
|
||||
unit_price: number;
|
||||
}
|
||||
|
||||
export async function getItemPrices(itemCodes: string[]): Promise<{
|
||||
success: boolean;
|
||||
data: Record<string, ItemPriceResult> | null;
|
||||
error?: string;
|
||||
__authError?: boolean;
|
||||
}> {
|
||||
try {
|
||||
const url = `${process.env.NEXT_PUBLIC_API_URL}/api/v1/quotes/items/prices`;
|
||||
|
||||
console.log('[QuoteActions] POST getItemPrices:', { itemCodes });
|
||||
|
||||
const { response, error } = await serverFetch(url, {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ item_codes: itemCodes }),
|
||||
});
|
||||
|
||||
if (error) {
|
||||
return {
|
||||
success: false,
|
||||
data: null,
|
||||
error: error.message,
|
||||
__authError: error.code === 'UNAUTHORIZED',
|
||||
};
|
||||
}
|
||||
|
||||
if (!response) {
|
||||
return {
|
||||
success: false,
|
||||
data: null,
|
||||
error: '단가 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
const result = await response.json();
|
||||
console.log('[QuoteActions] POST getItemPrices response:', result);
|
||||
|
||||
if (!response.ok || !result.success) {
|
||||
return {
|
||||
success: false,
|
||||
data: null,
|
||||
error: result.message || '단가 조회에 실패했습니다.',
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
success: true,
|
||||
data: result.data || null,
|
||||
};
|
||||
} catch (error) {
|
||||
if (isNextRedirectError(error)) throw error;
|
||||
console.error('[QuoteActions] getItemPrices error:', error);
|
||||
return {
|
||||
success: false,
|
||||
data: null,
|
||||
error: '서버 오류가 발생했습니다.',
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 견적 요약 통계 =====
|
||||
export async function getQuotesSummary(params?: {
|
||||
dateFrom?: string;
|
||||
|
||||
@@ -116,6 +116,7 @@ export interface CalculationInputItem {
|
||||
|
||||
export interface CalculationInputs {
|
||||
items?: CalculationInputItem[];
|
||||
bomResults?: BomCalculationResult[]; // BOM 산출 결과 (수동 추가 품목 포함)
|
||||
}
|
||||
|
||||
// BOM 자재 항목 타입 (API 응답)
|
||||
@@ -411,6 +412,13 @@ export interface BomCalculationResultItem {
|
||||
is_manual?: boolean; // 수동 추가 품목 여부
|
||||
}
|
||||
|
||||
// BOM 계산 디버그 스텝 타입
|
||||
export interface BomDebugStep {
|
||||
step: number;
|
||||
name: string;
|
||||
data: Record<string, unknown>;
|
||||
}
|
||||
|
||||
// BOM 계산 결과 타입
|
||||
export interface BomCalculationResult {
|
||||
finished_goods: {
|
||||
@@ -421,6 +429,8 @@ export interface BomCalculationResult {
|
||||
items: BomCalculationResultItem[];
|
||||
subtotals: Record<string, { name?: string; count?: number; subtotal?: number } | number>;
|
||||
grand_total: number;
|
||||
variables?: Record<string, unknown>; // 계산된 변수들
|
||||
debug_steps?: BomDebugStep[]; // 디버그 스텝 (개발용)
|
||||
}
|
||||
|
||||
// 견적 산출 결과 타입
|
||||
@@ -704,6 +714,16 @@ export function transformV2ToApi(
|
||||
.map(loc => loc.bomResult)
|
||||
.filter((br): br is BomCalculationResult => br !== undefined);
|
||||
|
||||
// DEBUG: bomResults 수집 상태 확인
|
||||
console.log('[transformV2ToApi] locations 수:', data.locations.length);
|
||||
console.log('[transformV2ToApi] locations bomResult 현황:', data.locations.map(loc => ({
|
||||
id: loc.id,
|
||||
hasBomResult: !!loc.bomResult,
|
||||
bomItemCount: loc.bomResult?.items?.length || 0,
|
||||
hasManualItems: loc.bomResult?.items?.some(item => (item as BomCalculationResultItem & { is_manual?: boolean }).is_manual) || false,
|
||||
})));
|
||||
console.log('[transformV2ToApi] 수집된 bomResults 수:', collectedBomResults.length);
|
||||
|
||||
const calculationInputs: CalculationInputs & { bomResults?: BomCalculationResult[] } = {
|
||||
items: data.locations.map(loc => ({
|
||||
productCategory: 'screen', // TODO: 동적으로 결정
|
||||
@@ -868,6 +888,16 @@ export function transformApiToV2(apiData: QuoteApiData): QuoteFormDataV2 {
|
||||
// BOM 결과 복원 (저장 시 calculation_inputs.bomResults에 저장됨)
|
||||
const savedBomResults = (calculationInputs as { bomResults?: BomCalculationResult[] } | undefined)?.bomResults || [];
|
||||
|
||||
// DEBUG: 로딩 시 bomResults 확인
|
||||
console.log('[transformApiToV2] rawCalcInputs 존재:', !!rawCalcInputs);
|
||||
console.log('[transformApiToV2] transformedCalcInputs 존재:', !!transformedCalcInputs);
|
||||
console.log('[transformApiToV2] calculationInputs.bomResults 존재:', !!(calculationInputs as { bomResults?: BomCalculationResult[] })?.bomResults);
|
||||
console.log('[transformApiToV2] savedBomResults 수:', savedBomResults.length);
|
||||
if (savedBomResults.length > 0) {
|
||||
console.log('[transformApiToV2] 첫번째 bomResult items 수:', savedBomResults[0]?.items?.length || 0);
|
||||
console.log('[transformApiToV2] 수동 추가 품목 있음:', savedBomResults.some(br => br.items?.some(item => (item as BomCalculationResultItem & { is_manual?: boolean }).is_manual)));
|
||||
}
|
||||
|
||||
// calculation_inputs에서 locations 복원
|
||||
let locations: LocationItem[] = [];
|
||||
|
||||
|
||||
Reference in New Issue
Block a user