fix(WEB): 견적서 문서 표시 개선 (#3, #4)

이슈 #3: 상세 견적서 담당자/연락처 표시
이슈 #4: 품목내역 올바른 단위 표시

주요 변경:
- QuoteDocument.tsx: 품목별 unit 필드 사용하여 올바른 단위 표시
- QuoteRegistration.tsx: manager, contact, remarks 필드 폼에 반영
This commit is contained in:
2026-01-06 21:20:49 +09:00
parent bf08447cd6
commit 1c338f4d3f
2 changed files with 421 additions and 124 deletions

View File

@@ -11,12 +11,14 @@
*/
import { QuoteFormData } from "./QuoteRegistration";
import type { CompanyFormData } from "@/components/settings/CompanyInfoManagement/types";
interface QuoteDocumentProps {
quote: QuoteFormData;
companyInfo?: CompanyFormData | null;
}
export function QuoteDocument({ quote }: QuoteDocumentProps) {
export function QuoteDocument({ quote, companyInfo }: QuoteDocumentProps) {
const formatAmount = (amount: number | undefined) => {
if (amount === undefined || amount === null) return '0';
return amount.toLocaleString('ko-KR');
@@ -34,7 +36,7 @@ export function QuoteDocument({ quote }: QuoteDocumentProps) {
itemName: item.productName || '스크린셔터',
spec: `${item.openWidth}×${item.openHeight}`,
quantity: item.quantity || 1,
unit: '개소',
unit: item.unit || '', // 각 품목의 단위 사용, 없으면 빈 문자열
unitPrice: item.unitPrice || 0,
totalPrice: item.totalAmount || 0,
})) || [];
@@ -292,29 +294,29 @@ export function QuoteDocument({ quote }: QuoteDocumentProps) {
<tbody>
<tr>
<th></th>
<td></td>
<td>{companyInfo?.companyName || '-'}</td>
<th></th>
<td>139-87-00333</td>
<td>{companyInfo?.businessNumber || '-'}</td>
</tr>
<tr>
<th></th>
<td> </td>
<td>{companyInfo?.representativeName || '-'}</td>
<th></th>
<td></td>
<td>{companyInfo?.businessType || '-'}</td>
</tr>
<tr>
<th></th>
<td colSpan={3}>, , </td>
<td colSpan={3}>{companyInfo?.businessCategory || '-'}</td>
</tr>
<tr>
<th></th>
<td colSpan={3}> 45-22</td>
<td colSpan={3}>{companyInfo?.address || '-'}</td>
</tr>
<tr>
<th></th>
<td>031-983-5130</td>
<th></th>
<td>02-6911-6315</td>
<td>{companyInfo?.managerPhone || '-'}</td>
<th></th>
<td>{companyInfo?.email || '-'}</td>
</tr>
</tbody>
</table>
@@ -340,7 +342,7 @@ export function QuoteDocument({ quote }: QuoteDocumentProps) {
<th></th>
<td>{quote.items[0]?.productName || '스크린셔터'}</td>
<th> </th>
<td>{quote.items.reduce((sum, item) => sum + (item.quantity || 0), 0)}</td>
<td>{quote.items[0]?.quantity || ''}{quote.unitSymbol ? ` ${quote.unitSymbol}` : ''}</td>
</tr>
<tr>
<th></th>
@@ -432,7 +434,7 @@ export function QuoteDocument({ quote }: QuoteDocumentProps) {
<div>
<div style={{ fontSize: '13px', marginBottom: '5px' }}>{formatDate(quote.registrationDate || '')}</div>
<div style={{ fontSize: '15px', fontWeight: '600' }}>
공급자: 동호기업 ()
: {companyInfo?.companyName || '-'} ()
</div>
</div>
<div className="stamp-area">
@@ -452,7 +454,7 @@ export function QuoteDocument({ quote }: QuoteDocumentProps) {
<p>3. .</p>
<p>4. .</p>
<p style={{ marginTop: '12px', textAlign: 'center', fontWeight: '600' }}>
: {quote.writer || '담당자'} | {quote.contact || '031-983-5130'}
: {companyInfo?.managerName || quote.writer || '담당자'} | {companyInfo?.managerPhone || '-'}
</p>
</div>
</div>

View File

@@ -8,7 +8,7 @@
"use client";
import { useState, useEffect } from "react";
import { useState, useEffect, useMemo } from "react";
import { Input } from "../ui/input";
import { Textarea } from "../ui/textarea";
import {
@@ -28,7 +28,7 @@ import {
Plus,
Copy,
Trash2,
Sparkles,
Loader2,
} from "lucide-react";
import { toast } from "sonner";
@@ -50,6 +50,10 @@ import {
FormFieldGrid,
} from "../templates/ResponsiveFormTemplate";
import { FormField } from "../molecules/FormField";
import { getFinishedGoods, calculateBomBulk, getSiteNames, type FinishedGoods, type BomCalculationResult } from "./actions";
import { getClients } from "../accounting/VendorManagement/actions";
import type { Vendor } from "../accounting/VendorManagement";
import type { BomMaterial, CalculationResults } from "./types";
// 견적 항목 타입
export interface QuoteItem {
@@ -64,6 +68,7 @@ export interface QuoteItem {
motorPower: string; // 모터 전원 (MP)
controller: string; // 연동제어기 (CT)
quantity: number; // 수량 (QTY)
unit?: string; // 품목 단위
wingSize: string; // 마구리 날개치수 (WS)
inspectionFee: number; // 검사비 (INSP)
unitPrice?: number; // 단가
@@ -83,7 +88,10 @@ export interface QuoteFormData {
contact: string;
dueDate: string;
remarks: string;
unitSymbol?: string; // 단위 (EA, 개소 등) - quotes.unit_symbol
items: QuoteItem[];
bomMaterials?: BomMaterial[]; // BOM 자재 목록
calculationResults?: CalculationResults; // 견적 산출 결과 (저장 시 BOM 자재 변환용)
}
// 초기 견적 항목
@@ -117,58 +125,31 @@ export const INITIAL_QUOTE_FORM: QuoteFormData = {
items: [createNewItem()],
};
// 샘플 발주처 데이터 (TODO: API에서 가져오기)
const SAMPLE_CLIENTS = [
{ id: "client-1", name: "인천건설 - 최담당" },
{ id: "client-2", name: "ABC건설" },
{ id: "client-3", name: "XYZ산업" },
];
// 제품 카테고리 옵션
// 제품 카테고리 옵션 (MNG 시뮬레이터와 동일)
const PRODUCT_CATEGORIES = [
{ value: "screen", label: "스크린" },
{ value: "steel", label: "철재" },
{ value: "aluminum", label: "알루미늄" },
{ value: "etc", label: "기타" },
{ value: "ALL", label: "전체" },
{ value: "SCREEN", label: "스크린" },
{ value: "STEEL", label: "철재" },
{ value: "BENDING", label: "절곡" },
{ value: "ALUMINUM", label: "알루미늄" },
];
// 제품명 옵션 (카테고리별)
const PRODUCTS: Record<string, { value: string; label: string }[]> = {
screen: [
{ value: "SCR-001", label: "스크린 A형" },
{ value: "SCR-002", label: "스크린 B형" },
{ value: "SCR-003", label: "스크린 C형" },
],
steel: [
{ value: "STL-001", label: "철재 도어 A" },
{ value: "STL-002", label: "철재 도어 B" },
],
aluminum: [
{ value: "ALU-001", label: "알루미늄 프레임" },
],
etc: [
{ value: "ETC-001", label: "기타 제품" },
],
};
// 가이드레일 설치 유형
// 가이드레일 설치 유형 (API: wall, ceiling, floor)
const GUIDE_RAIL_TYPES = [
{ value: "wall", label: "벽부착형" },
{ value: "ceiling", label: "천장매립형" },
{ value: "floor", label: "바닥매립형" },
{ value: "wall", label: "벽형" },
{ value: "floor", label: "측면형" },
];
// 모터 전원
// 모터 전원 (API: single=단상220V, three=삼상380V)
const MOTOR_POWERS = [
{ value: "single", label: "단상 220V" },
{ value: "three", label: "삼상 380V" },
{ value: "single", label: "220V (단상)" },
{ value: "three", label: "380V (삼상)" },
];
// 연동제어기
// 연동제어기 (API: basic, smart, premium)
const CONTROLLERS = [
{ value: "basic", label: "기본 제어기" },
{ value: "smart", label: "스마트 제어기" },
{ value: "premium", label: "프리미엄 제어기" },
{ value: "basic", label: "단독" },
{ value: "smart", label: "연동" },
];
interface QuoteRegistrationProps {
@@ -191,13 +172,118 @@ export function QuoteRegistration({
const [isSaving, setIsSaving] = useState(false);
const [activeItemIndex, setActiveItemIndex] = useState(0);
// editingQuote가 변경되면 formData 업데이트
// 완제품 목록 상태 (API에서 로드)
const [finishedGoods, setFinishedGoods] = useState<FinishedGoods[]>([]);
const [isLoadingProducts, setIsLoadingProducts] = useState(false);
const [isCalculating, setIsCalculating] = useState(false);
// 거래처 목록 상태 (API에서 로드)
const [clients, setClients] = useState<Vendor[]>([]);
const [isLoadingClients, setIsLoadingClients] = useState(false);
// 견적 산출 결과 상태
const [calculationResults, setCalculationResults] = useState<{
summary: { grand_total: number };
items: Array<{
index: number;
result: BomCalculationResult;
}>;
} | null>(null);
// 현장명 자동완성 목록 상태
const [siteNames, setSiteNames] = useState<string[]>([]);
// 수량 반영 총합계 계산 (useMemo로 최적화)
const calculatedGrandTotal = useMemo(() => {
if (!calculationResults?.items) return 0;
return calculationResults.items.reduce((sum, itemResult) => {
const formItem = formData.items[itemResult.index];
return sum + (itemResult.result.grand_total * (formItem?.quantity || 1));
}, 0);
}, [calculationResults, formData.items]);
// 컴포넌트 마운트 시 완제품 목록 로드
useEffect(() => {
const loadFinishedGoods = async () => {
setIsLoadingProducts(true);
try {
const result = await getFinishedGoods();
if (result.success) {
setFinishedGoods(result.data);
} else {
toast.error(`완제품 목록 로드 실패: ${result.error}`);
}
} catch (error) {
toast.error("완제품 목록을 불러오는데 실패했습니다.");
} finally {
setIsLoadingProducts(false);
}
};
loadFinishedGoods();
}, []);
// 컴포넌트 마운트 시 거래처 목록 로드
useEffect(() => {
const loadClients = async () => {
setIsLoadingClients(true);
try {
const result = await getClients();
if (result.success) {
setClients(result.data);
} else {
toast.error(`거래처 목록 로드 실패: ${result.error}`);
}
} catch (error) {
toast.error("거래처 목록을 불러오는데 실패했습니다.");
} finally {
setIsLoadingClients(false);
}
};
loadClients();
}, []);
// 컴포넌트 마운트 시 현장명 목록 로드 (자동완성용)
useEffect(() => {
const loadSiteNames = async () => {
try {
const result = await getSiteNames();
if (result.success) {
setSiteNames(result.data);
}
} catch (error) {
// 현장명 로드 실패는 무시 (선택적 기능)
console.error("현장명 목록 로드 실패:", error);
}
};
loadSiteNames();
}, []);
// editingQuote가 변경되면 formData 업데이트 및 calculationResults 초기화
useEffect(() => {
console.log('[QuoteRegistration] useEffect editingQuote:', JSON.stringify({
hasEditingQuote: !!editingQuote,
itemCount: editingQuote?.items?.length,
item0: editingQuote?.items?.[0] ? {
quantity: editingQuote.items[0].quantity,
wingSize: editingQuote.items[0].wingSize,
inspectionFee: editingQuote.items[0].inspectionFee,
} : null,
}, null, 2));
if (editingQuote) {
setFormData(editingQuote);
// 수정 모드 진입 시 이전 산출 결과 초기화
setCalculationResults(null);
}
}, [editingQuote]);
// 카테고리별 완제품 필터링
const getFilteredProducts = (category: string) => {
if (!category || category === "ALL") {
return finishedGoods; // 전체 선택 시 모든 완제품
}
return finishedGoods.filter(fg => fg.item_category === category);
};
// 유효성 검사
const validateForm = (): boolean => {
const newErrors: Record<string, string> = {};
@@ -249,7 +335,13 @@ export function QuoteRegistration({
setErrors({});
setIsSaving(true);
try {
await onSave(formData);
// calculationResults를 formData에 포함하여 저장
// transformFormDataToApi에서 BOM 자재의 base_quantity, calculated_quantity를 제대로 설정하기 위함
const dataToSave: QuoteFormData = {
...formData,
calculationResults: calculationResults || undefined,
};
await onSave(dataToSave);
toast.success(
editingQuote ? "견적이 수정되었습니다." : "견적이 등록되었습니다."
);
@@ -265,6 +357,10 @@ export function QuoteRegistration({
field: keyof QuoteFormData,
value: string | QuoteItem[]
) => {
// DEBUG: manager, contact, remarks 필드 변경 추적
if (field === 'manager' || field === 'contact' || field === 'remarks') {
console.log(`[handleFieldChange] ${field} 변경:`, value);
}
setFormData({ ...formData, [field]: value });
if (errors[field]) {
setErrors((prev) => {
@@ -277,11 +373,11 @@ export function QuoteRegistration({
// 발주처 선택
const handleClientChange = (clientId: string) => {
const client = SAMPLE_CLIENTS.find((c) => c.id === clientId);
const client = clients.find((c) => c.id === clientId);
setFormData({
...formData,
clientId,
clientName: client?.name || "",
clientName: client?.vendorName || "",
});
};
@@ -347,14 +443,90 @@ export function QuoteRegistration({
};
// 자동 견적 산출
const handleAutoCalculate = () => {
toast.info(`자동 견적 산출 (${formData.items.length}개 항목) - API 연동 필요`);
const handleAutoCalculate = async () => {
// 필수 입력값 검사
const incompleteItems = formData.items.filter(
(item) => !item.productName || !item.openWidth || !item.openHeight
);
if (incompleteItems.length > 0) {
toast.error("모든 견적 항목의 필수 입력값(제품명, 오픈사이즈)을 입력해주세요.");
return;
}
setIsCalculating(true);
try {
// BOM 계산 요청 데이터 구성 (API는 플랫한 구조 기대)
const bomItems = formData.items.map((item) => ({
finished_goods_code: item.productName, // item_code가 productName에 저장됨
// React 필드명 (camelCase) 사용 - API가 W0/H0로 변환
openWidth: parseFloat(item.openWidth) || 0,
openHeight: parseFloat(item.openHeight) || 0,
quantity: item.quantity,
guideRailType: item.guideRailType || undefined,
motorPower: item.motorPower || undefined,
controller: item.controller || undefined,
wingSize: parseFloat(item.wingSize) || undefined,
inspectionFee: item.inspectionFee || undefined,
}));
const result = await calculateBomBulk(bomItems);
if (result.success && result.data) {
// API 응답: { success, summary, items: [{ index, result: BomCalculationResult }] }
const apiData = result.data as {
summary?: { grand_total: number };
items?: Array<{ index: number; result: BomCalculationResult }>;
};
const bomItems = apiData.items || [];
// 계산 결과를 폼 데이터에 반영
const updatedItems = formData.items.map((item, index) => {
const bomResult = bomItems.find((b) => b.index === index);
if (bomResult?.result) {
return {
...item,
unitPrice: bomResult.result.grand_total,
totalAmount: bomResult.result.grand_total * item.quantity,
};
}
return item;
});
setFormData({ ...formData, items: updatedItems });
// 전체 계산 결과 저장
setCalculationResults({
summary: apiData.summary || { grand_total: 0 },
items: bomItems,
});
toast.success(`${formData.items.length}개 항목의 견적이 산출되었습니다.`);
// 계산 결과 요약 표시 (수량 반영 총합계 계산 - updatedItems 사용)
const totalWithQuantity = bomItems.reduce((sum, itemResult) => {
const formItem = updatedItems[itemResult.index];
return sum + (itemResult.result.grand_total * (formItem?.quantity || 1));
}, 0);
toast.info(`총 견적 금액: ${totalWithQuantity.toLocaleString()}`);
} else {
toast.error(`견적 산출 실패: ${result.error || "알 수 없는 오류"}`);
}
} catch (error) {
console.error("견적 산출 오류:", error);
toast.error("견적 산출 중 오류가 발생했습니다.");
} finally {
setIsCalculating(false);
}
};
// 샘플 데이터 생성
const handleGenerateSample = () => {
toast.info("완벽한 샘플 생성 - API 연동 필요");
};
// 렌더링 직전 디버그 로그
console.log('[QuoteRegistration] 렌더링 직전 formData.items[0]:', JSON.stringify({
quantity: formData.items[0]?.quantity,
wingSize: formData.items[0]?.wingSize,
inspectionFee: formData.items[0]?.inspectionFee,
}, null, 2));
return (
<ResponsiveFormTemplate
@@ -415,7 +587,7 @@ export function QuoteRegistration({
icon={FileText}
>
<FormFieldGrid columns={3}>
<FormField label="등록일" htmlFor="registrationDate">
<FormField label="등록일" htmlFor="registrationDate" type="custom">
<Input
id="registrationDate"
type="date"
@@ -436,6 +608,7 @@ export function QuoteRegistration({
<FormField
label="발주처 선택"
type="custom"
required
error={errors.clientId}
htmlFor="clientId"
@@ -443,14 +616,15 @@ export function QuoteRegistration({
<Select
value={formData.clientId}
onValueChange={handleClientChange}
disabled={isLoadingClients}
>
<SelectTrigger id="clientId">
<SelectValue placeholder="발주처를 선택하세요" />
<SelectValue placeholder={isLoadingClients ? "로딩 중..." : "발주처를 선택하세요"} />
</SelectTrigger>
<SelectContent>
{SAMPLE_CLIENTS.map((client) => (
{clients.map((client) => (
<SelectItem key={client.id} value={client.id}>
{client.name}
{client.vendorName}
</SelectItem>
))}
</SelectContent>
@@ -459,16 +633,22 @@ export function QuoteRegistration({
</FormFieldGrid>
<FormFieldGrid columns={3}>
<FormField label="현장명" htmlFor="siteName">
<FormField label="현장명" htmlFor="siteName" type="custom">
<Input
id="siteName"
placeholder="현장명을 입력하세요"
list="siteNameList"
placeholder="현장명을 입력 또는 선택하세요"
value={formData.siteName}
onChange={(e) => handleFieldChange("siteName", e.target.value)}
/>
<datalist id="siteNameList">
{siteNames.map((name) => (
<option key={name} value={name} />
))}
</datalist>
</FormField>
<FormField label="발주 담당자" htmlFor="manager">
<FormField label="발주 담당자" htmlFor="manager" type="custom">
<Input
id="manager"
placeholder="담당자명을 입력하세요"
@@ -477,7 +657,7 @@ export function QuoteRegistration({
/>
</FormField>
<FormField label="연락처" htmlFor="contact">
<FormField label="연락처" htmlFor="contact" type="custom">
<Input
id="contact"
placeholder="010-1234-5678"
@@ -488,7 +668,7 @@ export function QuoteRegistration({
</FormFieldGrid>
<FormFieldGrid columns={3}>
<FormField label="납기일" htmlFor="dueDate">
<FormField label="납기일" htmlFor="dueDate" type="custom">
<Input
id="dueDate"
type="date"
@@ -499,7 +679,7 @@ export function QuoteRegistration({
<div className="col-span-2" />
</FormFieldGrid>
<FormField label="비고" htmlFor="remarks">
<FormField label="비고" htmlFor="remarks" type="custom">
<Textarea
id="remarks"
placeholder="특이사항을 입력하세요"
@@ -559,7 +739,7 @@ export function QuoteRegistration({
{formData.items[activeItemIndex] && (
<>
<FormFieldGrid columns={3}>
<FormField label="층수" htmlFor={`floor-${activeItemIndex}`}>
<FormField label="층수" htmlFor={`floor-${activeItemIndex}`} type="custom">
<Input
id={`floor-${activeItemIndex}`}
placeholder="예: 1층, B1, 지하1층"
@@ -570,7 +750,7 @@ export function QuoteRegistration({
/>
</FormField>
<FormField label="부호" htmlFor={`code-${activeItemIndex}`}>
<FormField label="부호" htmlFor={`code-${activeItemIndex}`} type="custom">
<Input
id={`code-${activeItemIndex}`}
placeholder="예: A, B, C"
@@ -583,6 +763,7 @@ export function QuoteRegistration({
<FormField
label="제품 카테고리 (PC)"
type="custom"
required
error={errors[`item-${activeItemIndex}-productCategory`]}
htmlFor={`productCategory-${activeItemIndex}`}
@@ -610,6 +791,7 @@ export function QuoteRegistration({
<FormFieldGrid columns={3}>
<FormField
label="제품명"
type="custom"
required
error={errors[`item-${activeItemIndex}-productName`]}
htmlFor={`productName-${activeItemIndex}`}
@@ -619,15 +801,15 @@ export function QuoteRegistration({
onValueChange={(value) =>
handleItemChange(activeItemIndex, "productName", value)
}
disabled={!formData.items[activeItemIndex].productCategory}
disabled={isLoadingProducts}
>
<SelectTrigger id={`productName-${activeItemIndex}`}>
<SelectValue placeholder="제품을 선택하세요" />
<SelectValue placeholder={isLoadingProducts ? "로딩 중..." : "제품을 선택하세요"} />
</SelectTrigger>
<SelectContent>
{(PRODUCTS[formData.items[activeItemIndex].productCategory] || []).map((product) => (
<SelectItem key={product.value} value={product.value}>
{product.label}
{getFilteredProducts(formData.items[activeItemIndex].productCategory).map((product) => (
<SelectItem key={product.item_code} value={product.item_code}>
{product.item_name} ({product.item_code})
</SelectItem>
))}
</SelectContent>
@@ -636,6 +818,7 @@ export function QuoteRegistration({
<FormField
label="오픈사이즈 (W0)"
type="custom"
required
error={errors[`item-${activeItemIndex}-openWidth`]}
htmlFor={`openWidth-${activeItemIndex}`}
@@ -652,6 +835,7 @@ export function QuoteRegistration({
<FormField
label="오픈사이즈 (H0)"
type="custom"
required
error={errors[`item-${activeItemIndex}-openHeight`]}
htmlFor={`openHeight-${activeItemIndex}`}
@@ -670,6 +854,7 @@ export function QuoteRegistration({
<FormFieldGrid columns={3}>
<FormField
label="가이드레일 설치 유형 (GT)"
type="custom"
required
error={errors[`item-${activeItemIndex}-guideRailType`]}
htmlFor={`guideRailType-${activeItemIndex}`}
@@ -695,6 +880,7 @@ export function QuoteRegistration({
<FormField
label="모터 전원 (MP)"
type="custom"
required
error={errors[`item-${activeItemIndex}-motorPower`]}
htmlFor={`motorPower-${activeItemIndex}`}
@@ -720,6 +906,7 @@ export function QuoteRegistration({
<FormField
label="연동제어기 (CT)"
type="custom"
required
error={errors[`item-${activeItemIndex}-controller`]}
htmlFor={`controller-${activeItemIndex}`}
@@ -747,6 +934,7 @@ export function QuoteRegistration({
<FormFieldGrid columns={3}>
<FormField
label="수량 (QTY)"
type="custom"
required
error={errors[`item-${activeItemIndex}-quantity`]}
htmlFor={`quantity-${activeItemIndex}`}
@@ -764,6 +952,7 @@ export function QuoteRegistration({
<FormField
label="마구리 날개치수 (WS)"
type="custom"
htmlFor={`wingSize-${activeItemIndex}`}
>
<Input
@@ -778,6 +967,7 @@ export function QuoteRegistration({
<FormField
label="검사비 (INSP)"
type="custom"
htmlFor={`inspectionFee-${activeItemIndex}`}
>
<Input
@@ -811,49 +1001,154 @@ export function QuoteRegistration({
variant="default"
className="w-full bg-blue-600 hover:bg-blue-700"
onClick={handleAutoCalculate}
disabled={isCalculating || isLoadingProducts}
>
({formData.items.length} )
{isCalculating ? (
<>
<Loader2 className="h-4 w-4 mr-2 animate-spin" />
...
</>
) : (
<>
<Calculator className="h-4 w-4 mr-2" />
({formData.items.length} )
</>
)}
</Button>
{/* 견적 산출 결과 표시 */}
{calculationResults && calculationResults.items.length > 0 && (
<Card className="border-green-200 bg-green-50/50">
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-base font-semibold flex items-center gap-2">
<Calculator className="h-5 w-5 text-green-600" />
</CardTitle>
<Badge variant="default" className="bg-green-600">
{calculatedGrandTotal.toLocaleString()}
</Badge>
</div>
</CardHeader>
<CardContent className="space-y-4">
{/* 항목별 결과 */}
{calculationResults.items.map((itemResult, idx) => {
const formItem = formData.items[itemResult.index];
const product = finishedGoods.find(fg => fg.item_code === formItem?.productName);
return (
<div key={idx} className="border border-green-200 rounded-lg p-4 bg-white">
<div className="flex items-center justify-between mb-3">
<div className="flex items-center gap-2">
<Badge variant="outline" className="bg-green-100">
{itemResult.index + 1}
</Badge>
<span className="font-medium">
{itemResult.result.finished_goods?.name || product?.item_name || formItem?.productName || "-"}
</span>
<span className="text-sm text-muted-foreground">
({itemResult.result.finished_goods?.code || formItem?.productName || "-"})
</span>
</div>
<div className="text-right">
<div className="text-sm text-muted-foreground">
: {itemResult.result.grand_total.toLocaleString()}
</div>
<div className="font-semibold text-green-700">
: {((itemResult.result.grand_total || 0) * (formItem?.quantity || 1)).toLocaleString()}
<span className="text-xs text-muted-foreground ml-1">
(×{formItem?.quantity || 1})
</span>
</div>
</div>
</div>
{/* BOM 상세 내역 */}
{itemResult.result.items && itemResult.result.items.length > 0 && (
<div className="mt-3">
<details className="group">
<summary className="cursor-pointer text-sm text-muted-foreground hover:text-foreground">
BOM ({itemResult.result.items.length} )
</summary>
<div className="mt-2 overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b bg-gray-50">
<th className="text-left py-2 px-2"></th>
<th className="text-left py-2 px-2"></th>
<th className="text-right py-2 px-2"></th>
<th className="text-right py-2 px-2"></th>
<th className="text-right py-2 px-2"></th>
<th className="text-left py-2 px-2"></th>
</tr>
</thead>
<tbody>
{itemResult.result.items.map((bomItem, bomIdx) => (
<tr key={bomIdx} className="border-b last:border-0">
<td className="py-1.5 px-2 font-mono text-xs">
{bomItem.item_code}
</td>
<td className="py-1.5 px-2">{bomItem.item_name}</td>
<td className="py-1.5 px-2 text-right">
{bomItem.unit === 'EA'
? Math.round((bomItem.quantity || 0) * (formItem?.quantity || 1))
: parseFloat(((bomItem.quantity || 0) * (formItem?.quantity || 1)).toFixed(2))
} {bomItem.unit || ""}
</td>
<td className="py-1.5 px-2 text-right">
{bomItem.unit_price?.toLocaleString()}
</td>
<td className="py-1.5 px-2 text-right font-medium">
{((bomItem.total_price || 0) * (formItem?.quantity || 1)).toLocaleString()}
</td>
<td className="py-1.5 px-2">
<Badge variant="secondary" className="text-xs">
{bomItem.process_group || "-"}
</Badge>
</td>
</tr>
))}
</tbody>
</table>
</div>
</details>
{/* 공정별 소계 */}
{itemResult.result.subtotals && Object.keys(itemResult.result.subtotals).length > 0 && (
<div className="mt-2 pt-2 border-t">
<div className="flex flex-wrap gap-2">
{Object.entries(itemResult.result.subtotals).map(([process, data]) => {
// data는 객체 {name, count, subtotal} 또는 숫자일 수 있음
const subtotalData = data as { name?: string; count?: number; subtotal?: number } | number;
const amount = typeof subtotalData === 'object' ? subtotalData.subtotal : subtotalData;
const name = typeof subtotalData === 'object' ? subtotalData.name : process;
return (
<Badge key={process} variant="outline" className="text-xs">
{name || process}: {(amount || 0).toLocaleString()}
</Badge>
);
})}
</div>
</div>
)}
</div>
)}
</div>
);
})}
{/* 총합계 (수량 반영) */}
<div className="border-t pt-4 flex justify-between items-center">
<span className="text-lg font-semibold"> </span>
<span className="text-2xl font-bold text-green-700">
{calculatedGrandTotal.toLocaleString()}
</span>
</div>
</CardContent>
</Card>
)}
</FormSection>
{/* 3. 샘플 데이터 (개발용) */}
<Card className="border-blue-200 bg-blue-50/50">
<CardHeader className="pb-2">
<div className="flex items-center justify-between">
<div>
<CardTitle className="text-sm font-medium">
()
</CardTitle>
<p className="text-xs text-muted-foreground mt-1">
.
14( 5, 5, 4), 40, 25, 20
, BOM (2~3 )
.
</p>
</div>
<Button
variant="outline"
size="sm"
onClick={handleGenerateSample}
className="bg-white"
>
<Sparkles className="h-4 w-4 mr-2" />
</Button>
</div>
</CardHeader>
<CardContent className="pt-2">
<div className="flex flex-wrap gap-2">
<Badge variant="secondary"> 14</Badge>
<Badge variant="secondary"> 40</Badge>
<Badge variant="secondary"> 25</Badge>
<Badge variant="secondary"> 20</Badge>
<Badge variant="outline">BOM 2~3 </Badge>
<Badge variant="outline"> </Badge>
<Badge variant="outline"> </Badge>
</div>
</CardContent>
</Card>
</ResponsiveFormTemplate>
);
}