feat(WEB): 견적 시스템 개선, 엑셀 다운로드, PDF 생성 기능 추가
견적 시스템: - QuoteRegistrationV2: 할인 모달, 거래명세서 모달, vatType 필드 추가 - DiscountModal: 할인율/할인금액 상호 계산 모달 - QuoteTransactionModal: 거래명세서 미리보기 모달 - LocationDetailPanel, LocationListPanel 개선 템플릿 기능: - UniversalListPage: 엑셀 다운로드 기능 추가 (전체/선택 다운로드) - DocumentViewer: PDF 생성 기능 개선 신규 API: - /api/pdf/generate: Puppeteer 기반 PDF 생성 엔드포인트 UI 개선: - 입력 컴포넌트 placeholder 스타일 개선 (opacity 50%) - 각종 리스트 컴포넌트 정렬/필터링 개선 패키지 추가: - html2canvas, jspdf, puppeteer, dom-to-image-more Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -10,7 +10,7 @@
|
||||
"use client";
|
||||
|
||||
import { useState, useMemo, useEffect } from "react";
|
||||
import { Package, Settings, Plus, Trash2, Loader2 } from "lucide-react";
|
||||
import { Package, Settings, Plus, Trash2, Loader2, Calculator, Save } from "lucide-react";
|
||||
import { getItemCategoryTree, type ItemCategoryNode } from "./actions";
|
||||
|
||||
import { Badge } from "../ui/badge";
|
||||
@@ -35,7 +35,6 @@ import {
|
||||
} from "../ui/table";
|
||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from "../ui/tabs";
|
||||
import { ItemSearchModal } from "./ItemSearchModal";
|
||||
import { LocationEditModal } from "./LocationEditModal";
|
||||
|
||||
import type { LocationItem } from "./QuoteRegistrationV2";
|
||||
import type { FinishedGoods } from "./actions";
|
||||
@@ -138,8 +137,12 @@ const DEFAULT_TABS: TabDefinition[] = [
|
||||
interface LocationDetailPanelProps {
|
||||
location: LocationItem | null;
|
||||
onUpdateLocation: (locationId: string, updates: Partial<LocationItem>) => void;
|
||||
onDeleteLocation?: (locationId: string) => void;
|
||||
onCalculateLocation?: (locationId: string) => Promise<void>;
|
||||
onSaveItems?: () => void;
|
||||
finishedGoods: FinishedGoods[];
|
||||
disabled?: boolean;
|
||||
isCalculating?: boolean;
|
||||
}
|
||||
|
||||
// =============================================================================
|
||||
@@ -149,8 +152,12 @@ interface LocationDetailPanelProps {
|
||||
export function LocationDetailPanel({
|
||||
location,
|
||||
onUpdateLocation,
|
||||
onDeleteLocation,
|
||||
onCalculateLocation,
|
||||
onSaveItems,
|
||||
finishedGoods,
|
||||
disabled = false,
|
||||
isCalculating = false,
|
||||
}: LocationDetailPanelProps) {
|
||||
// ---------------------------------------------------------------------------
|
||||
// 상태
|
||||
@@ -158,7 +165,6 @@ export function LocationDetailPanel({
|
||||
|
||||
const [activeTab, setActiveTab] = useState("BODY");
|
||||
const [itemSearchOpen, setItemSearchOpen] = useState(false);
|
||||
const [editModalOpen, setEditModalOpen] = useState(false);
|
||||
const [itemCategories, setItemCategories] = useState<ItemCategoryNode[]>([]);
|
||||
const [categoriesLoading, setCategoriesLoading] = useState(true);
|
||||
|
||||
@@ -336,125 +342,184 @@ export function LocationDetailPanel({
|
||||
|
||||
return (
|
||||
<div className="flex flex-col h-full overflow-hidden">
|
||||
{/* 헤더 */}
|
||||
<div className="bg-white px-4 py-3 border-b">
|
||||
<div className="flex items-center justify-between">
|
||||
<h3 className="text-lg font-bold">
|
||||
{location.floor} / {location.code}
|
||||
</h3>
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-sm text-gray-500">제품명:</span>
|
||||
<Badge variant="outline" className="bg-blue-50 text-blue-700 border-blue-200">
|
||||
{location.productCode}
|
||||
</Badge>
|
||||
{location.bomResult && (
|
||||
<Badge variant="default" className="bg-green-600">
|
||||
산출완료
|
||||
</Badge>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 제품 정보 */}
|
||||
<div className="bg-gray-50 px-4 py-3 border-b space-y-3">
|
||||
{/* 오픈사이즈 */}
|
||||
<div className="flex items-center gap-4">
|
||||
<span className="text-sm text-gray-600 w-20">오픈사이즈</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<NumberInput
|
||||
value={location.openWidth}
|
||||
onChange={(value) => handleFieldChange("openWidth", value ?? 0)}
|
||||
disabled={disabled}
|
||||
className="w-24 h-8 text-center font-bold"
|
||||
/>
|
||||
<span className="text-gray-400">×</span>
|
||||
<NumberInput
|
||||
value={location.openHeight}
|
||||
onChange={(value) => handleFieldChange("openHeight", value ?? 0)}
|
||||
disabled={disabled}
|
||||
className="w-24 h-8 text-center font-bold"
|
||||
/>
|
||||
{!disabled && (
|
||||
<Button
|
||||
variant="secondary"
|
||||
size="sm"
|
||||
className="text-xs h-7"
|
||||
onClick={() => setEditModalOpen(true)}
|
||||
{/* ②-1 개소 정보 영역 */}
|
||||
<div className="bg-gray-50 border-b">
|
||||
{/* 1행: 층, 부호, 가로, 세로, 제품코드 */}
|
||||
<div className="px-4 py-3 space-y-3">
|
||||
<div className="grid grid-cols-5 gap-3">
|
||||
<div>
|
||||
<label className="text-xs text-gray-600">층</label>
|
||||
<Input
|
||||
value={location.floor}
|
||||
onChange={(e) => handleFieldChange("floor", e.target.value)}
|
||||
disabled={disabled}
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-600">부호</label>
|
||||
<Input
|
||||
value={location.code}
|
||||
onChange={(e) => handleFieldChange("code", e.target.value)}
|
||||
disabled={disabled}
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-600">가로</label>
|
||||
<NumberInput
|
||||
value={location.openWidth}
|
||||
onChange={(value) => handleFieldChange("openWidth", value ?? 0)}
|
||||
disabled={disabled}
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-600">세로</label>
|
||||
<NumberInput
|
||||
value={location.openHeight}
|
||||
onChange={(value) => handleFieldChange("openHeight", value ?? 0)}
|
||||
disabled={disabled}
|
||||
className="h-8 text-sm"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-600">제품코드</label>
|
||||
<Select
|
||||
value={location.productCode}
|
||||
onValueChange={(value) => {
|
||||
const product = finishedGoods.find((fg) => fg.item_code === value);
|
||||
onUpdateLocation(location.id, {
|
||||
productCode: value,
|
||||
productName: product?.item_name || value,
|
||||
});
|
||||
}}
|
||||
disabled={disabled}
|
||||
>
|
||||
수정
|
||||
<SelectTrigger className="h-8 text-sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{finishedGoods.map((fg) => (
|
||||
<SelectItem key={fg.item_code} value={fg.item_code}>
|
||||
{fg.item_code}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 2행: 가이드레일, 전원, 제어기 */}
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label className="text-xs text-gray-600 flex items-center gap-1">
|
||||
🔧 가이드레일
|
||||
</label>
|
||||
<Select
|
||||
value={location.guideRailType}
|
||||
onValueChange={(value) => handleFieldChange("guideRailType", value)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{GUIDE_RAIL_TYPES.map((type) => (
|
||||
<SelectItem key={type.value} value={type.value}>
|
||||
{type.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-600 flex items-center gap-1">
|
||||
⚡ 전원
|
||||
</label>
|
||||
<Select
|
||||
value={location.motorPower}
|
||||
onValueChange={(value) => handleFieldChange("motorPower", value)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{MOTOR_POWERS.map((power) => (
|
||||
<SelectItem key={power.value} value={power.value}>
|
||||
{power.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-600 flex items-center gap-1">
|
||||
📦 제어기
|
||||
</label>
|
||||
<Select
|
||||
value={location.controller}
|
||||
onValueChange={(value) => handleFieldChange("controller", value)}
|
||||
disabled={disabled}
|
||||
>
|
||||
<SelectTrigger className="h-8 text-sm">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{CONTROLLERS.map((ctrl) => (
|
||||
<SelectItem key={ctrl.value} value={ctrl.value}>
|
||||
{ctrl.label}
|
||||
</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 3행: 제작사이즈, 산출중량, 산출면적, 수량 */}
|
||||
<div className="grid grid-cols-4 gap-3 text-sm pt-2 border-t border-gray-200">
|
||||
<div>
|
||||
<span className="text-xs text-gray-500">제적사이즈</span>
|
||||
<p className="font-semibold">
|
||||
{location.manufactureWidth || location.openWidth + 280}X{location.manufactureHeight || location.openHeight + 280}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-xs text-gray-500">-</span>
|
||||
<p className="font-semibold">kg</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-xs text-gray-500">-</span>
|
||||
<p className="font-semibold">m²</p>
|
||||
</div>
|
||||
<div className="flex items-end">
|
||||
<Button
|
||||
onClick={() => onCalculateLocation?.(location.id)}
|
||||
disabled={disabled || isCalculating}
|
||||
className="w-full h-8 bg-orange-500 hover:bg-orange-600 text-white"
|
||||
>
|
||||
{isCalculating ? (
|
||||
<>
|
||||
<Loader2 className="h-4 w-4 mr-1 animate-spin" />
|
||||
산출중...
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<Calculator className="h-4 w-4 mr-1" />
|
||||
산출하기
|
||||
</>
|
||||
)}
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 제작사이즈, 산출중량, 산출면적, 수량 */}
|
||||
<div className="grid grid-cols-4 gap-4 text-sm">
|
||||
<div>
|
||||
<span className="text-gray-500">제작사이즈</span>
|
||||
<p className="font-semibold">
|
||||
{location.manufactureWidth || location.openWidth + 280} × {location.manufactureHeight || location.openHeight + 280}
|
||||
</p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">산출중량</span>
|
||||
<p className="font-semibold">{location.weight?.toFixed(1) || "-"} <span className="text-xs text-gray-400">kg</span></p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">산출면적</span>
|
||||
<p className="font-semibold">{location.area?.toFixed(1) || "-"} <span className="text-xs text-gray-400">m²</span></p>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-gray-500">수량</span>
|
||||
<QuantityInput
|
||||
value={location.quantity}
|
||||
onChange={(value) => handleFieldChange("quantity", value ?? 1)}
|
||||
disabled={disabled}
|
||||
className="w-24 h-7 text-center font-semibold"
|
||||
min={1}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 필수 설정 (읽기 전용) */}
|
||||
<div className="bg-white px-4 py-3 border-b">
|
||||
<h4 className="text-sm font-semibold text-gray-700 mb-2 flex items-center gap-1">
|
||||
<Settings className="h-4 w-4" />
|
||||
필수 설정
|
||||
</h4>
|
||||
<div className="grid grid-cols-3 gap-3">
|
||||
<div>
|
||||
<label className="text-xs text-gray-500 flex items-center gap-1 mb-1">
|
||||
🔧 가이드레일
|
||||
</label>
|
||||
<Badge variant="outline" className="bg-gray-50 text-gray-700 border-gray-300 px-3 py-1.5">
|
||||
{GUIDE_RAIL_TYPES.find(t => t.value === location.guideRailType)?.label || location.guideRailType}
|
||||
</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-500 flex items-center gap-1 mb-1">
|
||||
⚡ 전원
|
||||
</label>
|
||||
<Badge variant="outline" className="bg-gray-50 text-gray-700 border-gray-300 px-3 py-1.5">
|
||||
{MOTOR_POWERS.find(p => p.value === location.motorPower)?.label || location.motorPower}
|
||||
</Badge>
|
||||
</div>
|
||||
<div>
|
||||
<label className="text-xs text-gray-500 flex items-center gap-1 mb-1">
|
||||
📦 제어기
|
||||
</label>
|
||||
<Badge variant="outline" className="bg-gray-50 text-gray-700 border-gray-300 px-3 py-1.5">
|
||||
{CONTROLLERS.find(c => c.value === location.controller)?.label || location.controller}
|
||||
</Badge>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* 탭 및 품목 테이블 */}
|
||||
{/* ②-2 품목 상세 영역 */}
|
||||
<div className="flex-1 overflow-hidden flex flex-col">
|
||||
<Tabs value={activeTab} onValueChange={setActiveTab} className="flex flex-col h-full">
|
||||
{/* 탭 목록 - 스크롤 가능 */}
|
||||
{/* 탭 목록 */}
|
||||
<div className="border-b bg-white overflow-x-auto">
|
||||
{categoriesLoading ? (
|
||||
<div className="flex items-center justify-center py-2 px-4 text-gray-500">
|
||||
@@ -467,7 +532,7 @@ export function LocationDetailPanel({
|
||||
<TabsTrigger
|
||||
key={tab.value}
|
||||
value={tab.value}
|
||||
className="rounded-none border-b-2 border-transparent data-[state=active]:border-blue-500 data-[state=active]:bg-blue-50 data-[state=active]:text-blue-700 px-4 py-2 text-sm whitespace-nowrap"
|
||||
className="rounded-none border-b-2 border-transparent data-[state=active]:border-orange-500 data-[state=active]:bg-orange-50 data-[state=active]:text-orange-700 px-4 py-2 text-sm whitespace-nowrap"
|
||||
>
|
||||
{tab.label}
|
||||
</TabsTrigger>
|
||||
@@ -476,122 +541,95 @@ export function LocationDetailPanel({
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* 동적 탭 콘텐츠 렌더링 */}
|
||||
{/* 탭 콘텐츠 */}
|
||||
{detailTabs.map((tab) => {
|
||||
const items = bomItemsByTab[tab.value] || [];
|
||||
const isBendingTab = tab.parentCode === "BENDING";
|
||||
const isMotorTab = tab.value === "MOTOR_CTRL";
|
||||
const isAccessoryTab = tab.value === "ACCESSORY";
|
||||
|
||||
return (
|
||||
<TabsContent key={tab.value} value={tab.value} className="flex-1 overflow-auto m-0 p-0">
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-lg m-4">
|
||||
<div className="bg-amber-50 border border-amber-200 rounded-lg m-3">
|
||||
<Table>
|
||||
<TableHeader>
|
||||
<TableRow className="bg-amber-100/50">
|
||||
<TableHead className="font-semibold">품목명</TableHead>
|
||||
{/* 본체: 제작사이즈 */}
|
||||
{!isBendingTab && !isMotorTab && !isAccessoryTab && (
|
||||
<TableHead className="text-center font-semibold">제작사이즈</TableHead>
|
||||
)}
|
||||
{/* 절곡품: 재질, 규격, 납품길이 */}
|
||||
<TableHead className="text-center font-semibold">규격</TableHead>
|
||||
{isBendingTab && (
|
||||
<>
|
||||
<TableHead className="text-center font-semibold">재질</TableHead>
|
||||
<TableHead className="text-center font-semibold">규격</TableHead>
|
||||
<TableHead className="text-center font-semibold w-28">납품길이</TableHead>
|
||||
</>
|
||||
<TableHead className="text-center font-semibold w-24">납품길이</TableHead>
|
||||
)}
|
||||
{/* 모터: 유형, 사양 */}
|
||||
{isMotorTab && (
|
||||
<>
|
||||
<TableHead className="text-center font-semibold">유형</TableHead>
|
||||
<TableHead className="text-center font-semibold">사양</TableHead>
|
||||
</>
|
||||
)}
|
||||
{/* 부자재: 규격, 납품길이 */}
|
||||
{isAccessoryTab && (
|
||||
<>
|
||||
<TableHead className="text-center font-semibold">규격</TableHead>
|
||||
<TableHead className="text-center font-semibold w-28">납품길이</TableHead>
|
||||
</>
|
||||
)}
|
||||
<TableHead className="text-center font-semibold w-24">수량</TableHead>
|
||||
<TableHead className="text-center font-semibold w-20">작업</TableHead>
|
||||
<TableHead className="text-center font-semibold w-20">수량</TableHead>
|
||||
<TableHead className="text-center font-semibold w-12"></TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{items.map((item: any, index: number) => (
|
||||
<TableRow key={item.id || `${tab.value}-${index}`} className="bg-white">
|
||||
<TableCell className="font-medium">{item.item_name}</TableCell>
|
||||
{/* 본체: 제작사이즈 */}
|
||||
{!isBendingTab && !isMotorTab && !isAccessoryTab && (
|
||||
<TableCell className="text-center text-gray-600">{item.manufacture_size || "-"}</TableCell>
|
||||
)}
|
||||
{/* 절곡품: 재질, 규격, 납품길이 */}
|
||||
{isBendingTab && (
|
||||
<>
|
||||
<TableCell className="text-center text-gray-600">{item.material || "-"}</TableCell>
|
||||
<TableCell className="text-center text-gray-600">{item.spec || "-"}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Select defaultValue={item.delivery_length} disabled={disabled}>
|
||||
<SelectTrigger className="w-24 h-8">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{DELIVERY_LENGTH_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
</>
|
||||
)}
|
||||
{/* 모터: 유형, 사양 */}
|
||||
{isMotorTab && (
|
||||
<>
|
||||
<TableCell className="text-center text-gray-600">{item.type || "-"}</TableCell>
|
||||
<TableCell className="text-center text-gray-600">{item.spec || "-"}</TableCell>
|
||||
</>
|
||||
)}
|
||||
{/* 부자재: 규격, 납품길이 */}
|
||||
{isAccessoryTab && (
|
||||
<>
|
||||
<TableCell className="text-center text-gray-600">{item.spec || "-"}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Select defaultValue={item.delivery_length} disabled={disabled}>
|
||||
<SelectTrigger className="w-24 h-8">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{DELIVERY_LENGTH_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
</>
|
||||
)}
|
||||
<TableCell className="text-center">
|
||||
<QuantityInput
|
||||
value={item.quantity}
|
||||
onChange={() => {}}
|
||||
className="w-16 h-8 text-center"
|
||||
min={1}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Button variant="ghost" size="sm" className="text-red-500 hover:text-red-700 hover:bg-red-50 p-1" disabled={disabled}>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
{items.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={isBendingTab ? 5 : 4} className="text-center text-gray-400 py-6">
|
||||
산출된 품목이 없습니다
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
) : (
|
||||
items.map((item: any, index: number) => (
|
||||
<TableRow key={item.id || `${tab.value}-${index}`} className="bg-white">
|
||||
<TableCell className="font-medium">{item.item_name}</TableCell>
|
||||
<TableCell className="text-center text-gray-600">{item.spec || item.specification || "-"}</TableCell>
|
||||
{isBendingTab && (
|
||||
<TableCell className="text-center">
|
||||
<Select defaultValue={item.delivery_length || "4000"} disabled={disabled}>
|
||||
<SelectTrigger className="w-20 h-7 text-xs">
|
||||
<SelectValue />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
{DELIVERY_LENGTH_OPTIONS.map((opt) => (
|
||||
<SelectItem key={opt.value} value={opt.value}>{opt.label}</SelectItem>
|
||||
))}
|
||||
</SelectContent>
|
||||
</Select>
|
||||
</TableCell>
|
||||
)}
|
||||
<TableCell className="text-center">
|
||||
<QuantityInput
|
||||
value={item.quantity}
|
||||
onChange={() => {}}
|
||||
className="w-14 h-7 text-center text-xs"
|
||||
min={1}
|
||||
disabled={disabled}
|
||||
/>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Button
|
||||
variant="ghost"
|
||||
size="icon"
|
||||
className="h-7 w-7 text-gray-400 hover:text-red-500 hover:bg-red-50"
|
||||
disabled={disabled}
|
||||
onClick={() => {
|
||||
if (!location) return;
|
||||
// 품목 삭제 로직
|
||||
const existingBomResult = location.bomResult;
|
||||
if (!existingBomResult) return;
|
||||
|
||||
const updatedItems = (existingBomResult.items || []).filter(
|
||||
(_: any, i: number) => !(bomItemsByTab[tab.value]?.[index] === _)
|
||||
);
|
||||
|
||||
onUpdateLocation(location.id, {
|
||||
bomResult: {
|
||||
...existingBomResult,
|
||||
items: updatedItems,
|
||||
},
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Trash2 className="h-4 w-4" />
|
||||
</Button>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
|
||||
{/* 품목 추가 버튼 + 안내 */}
|
||||
{/* 품목 추가 + 저장 버튼 */}
|
||||
<div className="p-3 flex items-center justify-between border-t border-amber-200">
|
||||
<Button
|
||||
variant="outline"
|
||||
@@ -603,9 +641,15 @@ export function LocationDetailPanel({
|
||||
<Plus className="h-4 w-4 mr-1" />
|
||||
품목 추가
|
||||
</Button>
|
||||
<span className="text-sm text-gray-500 flex items-center gap-1">
|
||||
💡 금액은 아래 견적금액요약에서 확인하세요
|
||||
</span>
|
||||
<Button
|
||||
size="sm"
|
||||
className="bg-blue-600 hover:bg-blue-700 text-white"
|
||||
onClick={onSaveItems}
|
||||
disabled={disabled}
|
||||
>
|
||||
<Save className="h-4 w-4 mr-1" />
|
||||
저장
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</TabsContent>
|
||||
@@ -614,13 +658,6 @@ export function LocationDetailPanel({
|
||||
</Tabs>
|
||||
</div>
|
||||
|
||||
{/* 금액 안내 */}
|
||||
{!location.bomResult && (
|
||||
<div className="bg-blue-50 px-4 py-2 border-t border-blue-200 text-center text-sm text-blue-700">
|
||||
💡 금액은 아래 견적금액요약에서 확인하세요
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* 품목 검색 모달 */}
|
||||
<ItemSearchModal
|
||||
open={itemSearchOpen}
|
||||
@@ -628,12 +665,10 @@ export function LocationDetailPanel({
|
||||
onSelectItem={(item) => {
|
||||
if (!location) return;
|
||||
|
||||
// 현재 탭 정보 가져오기
|
||||
const currentTab = detailTabs.find((t) => t.value === activeTab);
|
||||
const categoryCode = activeTab; // 카테고리 코드를 직접 사용
|
||||
const categoryCode = activeTab;
|
||||
const categoryLabel = currentTab?.label || activeTab;
|
||||
|
||||
// 새 품목 생성 (카테고리 코드 포함)
|
||||
const newItem: BomCalculationResultItem & { category_code?: string; is_manual?: boolean } = {
|
||||
item_code: item.code,
|
||||
item_name: item.name,
|
||||
@@ -643,11 +678,10 @@ export function LocationDetailPanel({
|
||||
unit_price: 0,
|
||||
total_price: 0,
|
||||
process_group: categoryLabel,
|
||||
category_code: categoryCode, // 새 카테고리 코드 사용
|
||||
is_manual: true, // 수동 추가 품목 표시
|
||||
category_code: categoryCode,
|
||||
is_manual: true,
|
||||
};
|
||||
|
||||
// 기존 bomResult 가져오기
|
||||
const existingBomResult = location.bomResult || {
|
||||
finished_goods: { code: location.productCode || "", name: location.productName || "" },
|
||||
subtotals: {},
|
||||
@@ -656,11 +690,9 @@ export function LocationDetailPanel({
|
||||
items: [],
|
||||
};
|
||||
|
||||
// 기존 items에 새 아이템 추가
|
||||
const existingItems = existingBomResult.items || [];
|
||||
const updatedItems = [...existingItems, newItem];
|
||||
|
||||
// subtotals 업데이트 (해당 카테고리의 count, subtotal 증가)
|
||||
const existingSubtotals = existingBomResult.subtotals || {};
|
||||
const rawCategorySubtotal = existingSubtotals[categoryCode];
|
||||
const categorySubtotal = (typeof rawCategorySubtotal === 'object' && rawCategorySubtotal !== null)
|
||||
@@ -675,7 +707,6 @@ export function LocationDetailPanel({
|
||||
},
|
||||
};
|
||||
|
||||
// grouped_items 업데이트 (해당 카테고리의 items 배열에 추가)
|
||||
const existingGroupedItems = existingBomResult.grouped_items || {};
|
||||
const categoryGroupedItems = existingGroupedItems[categoryCode] || { items: [] };
|
||||
const updatedGroupedItems = {
|
||||
@@ -686,7 +717,6 @@ export function LocationDetailPanel({
|
||||
},
|
||||
};
|
||||
|
||||
// grand_total 업데이트
|
||||
const updatedGrandTotal = (existingBomResult.grand_total || 0) + (newItem.total_price || 0);
|
||||
|
||||
const updatedBomResult = {
|
||||
@@ -697,23 +727,11 @@ export function LocationDetailPanel({
|
||||
grand_total: updatedGrandTotal,
|
||||
};
|
||||
|
||||
// location 업데이트
|
||||
onUpdateLocation(location.id, { bomResult: updatedBomResult });
|
||||
|
||||
console.log(`[품목 추가] ${item.code} - ${item.name} → ${categoryLabel} (${categoryCode})`);
|
||||
}}
|
||||
tabLabel={detailTabs.find((t) => t.value === activeTab)?.label}
|
||||
/>
|
||||
|
||||
{/* 개소 정보 수정 모달 */}
|
||||
<LocationEditModal
|
||||
open={editModalOpen}
|
||||
onOpenChange={setEditModalOpen}
|
||||
location={location}
|
||||
onSave={(locationId, updates) => {
|
||||
onUpdateLocation(locationId, updates);
|
||||
}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user