fix(WEB): 영업 페이지 및 견적 타입 수정

- 생산지시 페이지 에러 처리 개선
- 견적관리 상세 페이지 개선
- quotes types 수정
This commit is contained in:
2026-01-13 19:48:03 +09:00
parent 777872486a
commit 42b0a5778e
3 changed files with 128 additions and 9 deletions

View File

@@ -340,8 +340,7 @@ export default function ProductionOrderCreatePage() {
// 성공 다이얼로그 상태
const [showSuccessDialog, setShowSuccessDialog] = useState(false);
const [generatedOrderNumber, setGeneratedOrderNumber] = useState("");
const [generatedWorkOrderId, setGeneratedWorkOrderId] = useState<string | null>(null);
const [generatedWorkOrders, setGeneratedWorkOrders] = useState<Array<{ workOrderNo: string; processName?: string }>>([]);
// 수주 데이터 및 공정 목록 로드
const fetchData = useCallback(async () => {
@@ -388,16 +387,39 @@ export default function ProductionOrderCreatePage() {
setIsSubmitting(true);
setError(null);
try {
// 공정별 품목 그룹핑 (processIds 추출을 위해)
const groups = groupItemsByProcess(
(order.items || []).map((item) => ({
itemName: item.itemName,
itemCode: item.itemCode,
quantity: item.quantity,
})),
processes
);
// 모든 매칭된 공정 ID 추출 (공정별로 각각 작업지시 생성)
const matchedGroups = groups.filter((g) => g.process.id !== "unmatched");
const allProcessIds = matchedGroups
.map((g) => parseInt(g.process.id, 10))
.filter((id) => !isNaN(id));
const productionData: CreateProductionOrderData = {
priority: selectedPriority,
memo: memo || undefined,
processIds: allProcessIds.length > 0 ? allProcessIds : undefined,
};
const result = await createProductionOrder(orderId, productionData);
if (result.success && result.data) {
setGeneratedOrderNumber(result.data.workOrder.workOrderNo);
setGeneratedWorkOrderId(result.data.workOrder.id);
// 다중 작업지시 응답 처리
const workOrders = result.data.workOrders || (result.data.workOrder ? [result.data.workOrder] : []);
setGeneratedWorkOrders(
workOrders.map((wo: { workOrderNo: string; process?: { processName: string } }) => ({
workOrderNo: wo.workOrderNo,
processName: wo.process?.processName,
}))
);
setShowSuccessDialog(true);
} else {
setError(result.error || "생산지시 생성에 실패했습니다.");
@@ -911,16 +933,27 @@ export default function ProductionOrderCreatePage() {
<AlertDialogHeader>
<AlertDialogTitle className="flex items-center gap-2">
<CheckCircle2 className="h-5 w-5 text-green-600" />
생산지시가 생성되었습니다.
작업지시가 {generatedWorkOrders.length}건 생성되었습니다.
</AlertDialogTitle>
<AlertDialogDescription asChild>
<div className="space-y-4 pt-2">
<div>
<p className="text-sm text-muted-foreground">생산지시번호:</p>
<p className="font-mono font-semibold text-foreground">{generatedOrderNumber}</p>
<p className="text-sm text-muted-foreground mb-2">생성된 작업지시:</p>
<ul className="space-y-1">
{generatedWorkOrders.map((wo, idx) => (
<li key={idx} className="flex items-center gap-2">
<span className="font-mono font-semibold text-foreground">{wo.workOrderNo}</span>
{wo.processName && (
<BadgeSm className="bg-blue-100 text-blue-700 border-blue-200">
{wo.processName}
</BadgeSm>
)}
</li>
))}
</ul>
</div>
<p className="text-sm text-muted-foreground">
&gt; .
&gt; .
</p>
</div>
</AlertDialogDescription>

View File

@@ -47,6 +47,9 @@ import {
MessageCircle,
X,
FileCheck,
Package,
ChevronDown,
ChevronUp,
} from "lucide-react";
import { ContentLoadingSpinner } from "@/components/ui/loading-spinner";
@@ -69,6 +72,9 @@ export default function QuoteDetailPage() {
const [showDetailedBreakdown, setShowDetailedBreakdown] = useState(true);
const [showMaterialList, setShowMaterialList] = useState(true);
// BOM 자재 상세 펼침/접힘 상태
const [isBomExpanded, setIsBomExpanded] = useState(true);
// 견적 데이터 조회
const fetchQuote = useCallback(async () => {
setIsLoading(true);
@@ -464,6 +470,84 @@ export default function QuoteDetailPage() {
</CardContent>
</Card>
{/* BOM 자재 상세 */}
{quote.bomMaterials && quote.bomMaterials.length > 0 && (
<Card>
<CardHeader className="pb-3">
<div className="flex items-center justify-between">
<CardTitle className="text-lg flex items-center gap-2">
<Package className="h-5 w-5" />
BOM
<Badge variant="secondary" className="ml-2">
{quote.bomMaterials.length}
</Badge>
</CardTitle>
<Button
variant="ghost"
size="sm"
onClick={() => setIsBomExpanded(!isBomExpanded)}
>
{isBomExpanded ? (
<ChevronUp className="h-4 w-4" />
) : (
<ChevronDown className="h-4 w-4" />
)}
</Button>
</div>
</CardHeader>
{isBomExpanded && (
<CardContent>
<div className="overflow-x-auto">
<table className="w-full text-sm">
<thead>
<tr className="border-b bg-muted/50">
<th className="text-left p-2 font-medium">No</th>
<th className="text-left p-2 font-medium"></th>
<th className="text-left p-2 font-medium"></th>
<th className="text-left p-2 font-medium"></th>
<th className="text-left p-2 font-medium"></th>
<th className="text-center p-2 font-medium"></th>
<th className="text-right p-2 font-medium"></th>
<th className="text-right p-2 font-medium"></th>
<th className="text-right p-2 font-medium"></th>
</tr>
</thead>
<tbody>
{quote.bomMaterials.map((material, index) => (
<tr key={index} className="border-b hover:bg-muted/30">
<td className="p-2 text-muted-foreground">{index + 1}</td>
<td className="p-2 font-mono text-xs">{material.itemCode}</td>
<td className="p-2">{material.itemName}</td>
<td className="p-2">
<Badge variant="outline" className="text-xs">
{material.itemType === 'RM' ? '원자재' :
material.itemType === 'SM' ? '부자재' :
material.itemType === 'CS' ? '소모품' : material.itemType}
</Badge>
</td>
<td className="p-2 text-muted-foreground">{material.specification || '-'}</td>
<td className="p-2 text-center">{material.unit}</td>
<td className="p-2 text-right">{material.quantity.toLocaleString()}</td>
<td className="p-2 text-right">{material.unitPrice.toLocaleString()}</td>
<td className="p-2 text-right font-medium">{material.totalPrice.toLocaleString()}</td>
</tr>
))}
</tbody>
<tfoot>
<tr className="border-t-2 bg-muted/30">
<td colSpan={8} className="p-2 text-right font-medium"></td>
<td className="p-2 text-right font-bold text-blue-600">
{quote.bomMaterials.reduce((sum, m) => sum + m.totalPrice, 0).toLocaleString()}
</td>
</tr>
</tfoot>
</table>
</div>
</CardContent>
)}
</Card>
)}
{/* 견적서 다이얼로그 */}
<Dialog open={isQuoteDocumentOpen} onOpenChange={setIsQuoteDocumentOpen}>
<DialogContent className="max-w-[95vw] md:max-w-[800px] lg:max-w-[900px] h-[95vh] flex flex-col p-0">

View File

@@ -69,6 +69,7 @@ export interface QuoteItem {
id: string;
quoteId: string;
productId?: string;
itemCode?: string; // 품목코드 (item_code)
productName: string;
specification?: string;
unit?: string;
@@ -298,6 +299,7 @@ export function transformItemApiToFrontend(apiData: QuoteItemApiData): QuoteItem
id: String(apiData.id),
quoteId: String(apiData.quote_id),
productId: apiData.item_id ? String(apiData.item_id) : (apiData.product_id ? String(apiData.product_id) : undefined),
itemCode: apiData.item_code || undefined, // 품목코드
productName,
specification: apiData.specification || undefined,
unit: apiData.unit || undefined,
@@ -545,7 +547,7 @@ export function transformQuoteToFormData(quote: Quote): QuoteFormData {
? quote.items.map((item, index) => ({
itemIndex: index,
finishedGoodsCode: '',
itemCode: item.productId || item.id || '',
itemCode: item.itemCode || '', // 품목코드 사용
itemName: item.productName,
itemType: '',
itemCategory: '',