From 95866a13ba4e1c235373834460aa284b763fe0d9 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 23 Jun 2025 09:03:29 +0000 Subject: (최겸) 기술영업 RFQ 개발(0620 요구사항, 첨부파일, REV 등) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../vendor-response/quotation-item-editor.tsx | 664 --------------------- 1 file changed, 664 deletions(-) delete mode 100644 lib/techsales-rfq/vendor-response/quotation-item-editor.tsx (limited to 'lib/techsales-rfq/vendor-response/quotation-item-editor.tsx') diff --git a/lib/techsales-rfq/vendor-response/quotation-item-editor.tsx b/lib/techsales-rfq/vendor-response/quotation-item-editor.tsx deleted file mode 100644 index 92bec96a..00000000 --- a/lib/techsales-rfq/vendor-response/quotation-item-editor.tsx +++ /dev/null @@ -1,664 +0,0 @@ -"use client" - -import * as React from "react" -import { useState, useEffect, useRef } from "react" -import { toast } from "sonner" -import { format } from "date-fns" - -import { Button } from "@/components/ui/button" -import { Input } from "@/components/ui/input" -import { Checkbox } from "@/components/ui/checkbox" -import { DatePicker } from "@/components/ui/date-picker" -import { - Table, - TableBody, - TableCaption, - TableCell, - TableHead, - TableHeader, - TableRow -} from "@/components/ui/table" -import { Badge } from "@/components/ui/badge" -import { ScrollArea } from "@/components/ui/scroll-area" -import { - Tooltip, - TooltipContent, - TooltipProvider, - TooltipTrigger -} from "@/components/ui/tooltip" -import { - Info, - Clock, - CalendarIcon, - ClipboardCheck, - AlertTriangle, - CheckCircle2, - RefreshCw, - Save, - FileText, - Sparkles -} from "lucide-react" - -import { formatCurrency } from "@/lib/utils" -import { updateQuotationItem } from "../services" -import { Textarea } from "@/components/ui/textarea" - -// 견적 아이템 타입 -interface QuotationItem { - id: number - quotationId: number - prItemId: number - materialCode: string | null - materialDescription: string | null - quantity: number - uom: string | null - unitPrice: number - totalPrice: number - currency: string - vendorMaterialCode: string | null - vendorMaterialDescription: string | null - deliveryDate: Date | null - leadTimeInDays: number | null - taxRate: number | null - taxAmount: number | null - discountRate: number | null - discountAmount: number | null - remark: string | null - isAlternative: boolean - isRecommended: boolean // 남겨두지만 UI에서는 사용하지 않음 - createdAt: Date - updatedAt: Date - prItem?: { - id: number - materialCode: string | null - materialDescription: string | null - // 기타 필요한 정보 - } -} - -// debounce 함수 구현 -function debounce any>( - func: T, - wait: number -): (...args: Parameters) => void { - let timeout: NodeJS.Timeout | null = null; - - return function (...args: Parameters) { - if (timeout) clearTimeout(timeout); - timeout = setTimeout(() => func(...args), wait); - }; -} - -interface QuotationItemEditorProps { - items: QuotationItem[] - onItemsChange: (items: QuotationItem[]) => void - disabled?: boolean - currency: string -} - -export function QuotationItemEditor({ - items, - onItemsChange, - disabled = false, - currency -}: QuotationItemEditorProps) { - const [editingItem, setEditingItem] = useState(null) - const [isSaving, setIsSaving] = useState(false) - - // 저장이 필요한 항목들을 추적 - const [pendingChanges, setPendingChanges] = useState>(new Set()) - - // 로컬 상태 업데이트 함수 - 화면에 즉시 반영하지만 서버에는 즉시 저장하지 않음 - const updateLocalItem = ( - index: number, - field: K, - value: QuotationItem[K] - ) => { - // 로컬 상태 업데이트 - const updatedItems = [...items] - const item = { ...updatedItems[index] } - - // 필드 업데이트 - item[field] = value - - // 대체품 체크 해제 시 관련 필드 초기화 - if (field === 'isAlternative' && value === false) { - item.vendorMaterialCode = null; - item.vendorMaterialDescription = null; - item.remark = null; - } - - // 단가나 수량이 변경되면 총액 계산 - if (field === 'unitPrice' || field === 'quantity') { - item.totalPrice = Number(item.unitPrice) * Number(item.quantity) - - // 세금이 있으면 세액 계산 - if (item.taxRate) { - item.taxAmount = item.totalPrice * (item.taxRate / 100) - } - - // 할인이 있으면 할인액 계산 - if (item.discountRate) { - item.discountAmount = item.totalPrice * (item.discountRate / 100) - } - } - - // 세율이 변경되면 세액 계산 - if (field === 'taxRate') { - item.taxAmount = item.totalPrice * (value as number / 100) - } - - // 할인율이 변경되면 할인액 계산 - if (field === 'discountRate') { - item.discountAmount = item.totalPrice * (value as number / 100) - } - - // 변경된 아이템으로 교체 - updatedItems[index] = item - - // 미저장 항목으로 표시 - setPendingChanges(prev => new Set(prev).add(item.id)) - - // 부모 컴포넌트에 변경 사항 알림 - onItemsChange(updatedItems) - - // 저장 필요함을 표시 - return item - } - - // 서버에 저장하는 함수 - const saveItemToServer = async (item: QuotationItem, field: keyof QuotationItem, value: any) => { - if (disabled) return - - try { - setIsSaving(true) - - const result = await updateQuotationItem({ - id: item.id, - [field]: value, - totalPrice: item.totalPrice, - taxAmount: item.taxAmount ?? 0, - discountAmount: item.discountAmount ?? 0 - }) - - // 저장 완료 후 pendingChanges에서 제거 - setPendingChanges(prev => { - const newSet = new Set(prev) - newSet.delete(item.id) - return newSet - }) - - if (!result.success) { - toast.error(result.message || "항목 저장 중 오류가 발생했습니다") - } - } catch (error) { - console.error("항목 저장 오류:", error) - toast.error("항목 저장 중 오류가 발생했습니다") - } finally { - setIsSaving(false) - } - } - - // debounce된 저장 함수 - const debouncedSave = useRef(debounce( - (item: QuotationItem, field: keyof QuotationItem, value: any) => { - saveItemToServer(item, field, value) - }, - 800 // 800ms 지연 - )).current - - // 견적 항목 업데이트 함수 - const handleItemUpdate = (index: number, field: keyof QuotationItem, value: any) => { - const updatedItem = updateLocalItem(index, field, value) - - // debounce를 통해 서버 저장 지연 - if (!disabled) { - debouncedSave(updatedItem, field, value) - } - } - - // 모든 변경 사항 저장 - const saveAllChanges = async () => { - if (disabled || pendingChanges.size === 0) return - - setIsSaving(true) - toast.info(`${pendingChanges.size}개 항목 저장 중...`) - - try { - // 변경된 모든 항목 저장 - for (const itemId of pendingChanges) { - const index = items.findIndex(item => item.id === itemId) - if (index !== -1) { - const item = items[index] - await updateQuotationItem({ - id: item.id, - unitPrice: item.unitPrice, - totalPrice: item.totalPrice, - taxRate: item.taxRate ?? 0, - taxAmount: item.taxAmount ?? 0, - discountRate: item.discountRate ?? 0, - discountAmount: item.discountAmount ?? 0, - deliveryDate: item.deliveryDate, - leadTimeInDays: item.leadTimeInDays ?? 0, - vendorMaterialCode: item.vendorMaterialCode ?? "", - vendorMaterialDescription: item.vendorMaterialDescription ?? "", - isAlternative: item.isAlternative, - isRecommended: false, // 항상 false로 설정 (사용하지 않음) - remark: item.remark ?? "" - }) - } - } - - // 모든 변경 사항 저장 완료 - setPendingChanges(new Set()) - toast.success("모든 변경 사항이 저장되었습니다") - } catch (error) { - console.error("변경 사항 저장 오류:", error) - toast.error("변경 사항 저장 중 오류가 발생했습니다") - } finally { - setIsSaving(false) - } - } - - // blur 이벤트로 저장 트리거 (사용자가 입력 완료 후) - const handleBlur = (index: number, field: keyof QuotationItem, value: any) => { - const itemId = items[index].id - - // 해당 항목이 pendingChanges에 있다면 즉시 저장 - if (pendingChanges.has(itemId)) { - const item = items[index] - saveItemToServer(item, field, value) - } - } - - // 전체 단가 업데이트 (일괄 반영) - const handleBulkUnitPriceUpdate = () => { - if (items.length === 0) return - - // 첫 번째 아이템의 단가 가져오기 - const firstUnitPrice = items[0].unitPrice - - if (!firstUnitPrice) { - toast.error("첫 번째 항목의 단가를 먼저 입력해주세요") - return - } - - // 모든 아이템에 동일한 단가 적용 - const updatedItems = items.map(item => ({ - ...item, - unitPrice: firstUnitPrice, - totalPrice: firstUnitPrice * item.quantity, - taxAmount: item.taxRate ? (firstUnitPrice * item.quantity) * (item.taxRate / 100) : item.taxAmount, - discountAmount: item.discountRate ? (firstUnitPrice * item.quantity) * (item.discountRate / 100) : item.discountAmount - })) - - // 모든 아이템을 변경 필요 항목으로 표시 - setPendingChanges(new Set(updatedItems.map(item => item.id))) - - // 부모 컴포넌트에 변경 사항 알림 - onItemsChange(updatedItems) - - toast.info("모든 항목의 단가가 업데이트되었습니다. 변경 사항을 저장하려면 '저장' 버튼을 클릭하세요.") - } - - // 입력 핸들러 - const handleNumberInputChange = ( - index: number, - field: keyof QuotationItem, - e: React.ChangeEvent - ) => { - const value = e.target.value === '' ? 0 : parseFloat(e.target.value) - handleItemUpdate(index, field, value) - } - - const handleTextInputChange = ( - index: number, - field: keyof QuotationItem, - e: React.ChangeEvent - ) => { - handleItemUpdate(index, field, e.target.value) - } - - const handleDateChange = ( - index: number, - field: keyof QuotationItem, - date: Date | undefined - ) => { - handleItemUpdate(index, field, date || null) - } - - const handleCheckboxChange = ( - index: number, - field: keyof QuotationItem, - checked: boolean - ) => { - handleItemUpdate(index, field, checked) - } - - // 날짜 형식 지정 - const formatDeliveryDate = (date: Date | null) => { - if (!date) return "-" - return format(date, "yyyy-MM-dd") - } - - // 입력 폼 필드 렌더링 - const renderInputField = (item: QuotationItem, index: number, field: keyof QuotationItem) => { - if (field === 'unitPrice' || field === 'taxRate' || field === 'discountRate' || field === 'leadTimeInDays') { - return ( - handleNumberInputChange(index, field, e)} - onBlur={(e) => handleBlur(index, field, parseFloat(e.target.value) || 0)} - disabled={disabled || isSaving} - className="w-full" - /> - ) - } else if (field === 'vendorMaterialCode' || field === 'vendorMaterialDescription') { - return ( - handleTextInputChange(index, field, e)} - onBlur={(e) => handleBlur(index, field, e.target.value)} - disabled={disabled || isSaving || !item.isAlternative} - className="w-full" - placeholder={field === 'vendorMaterialCode' ? "벤더 자재그룹" : "벤더 자재명"} - /> - ) - } else if (field === 'deliveryDate') { - return ( - { - handleDateChange(index, field, date); - // DatePicker는 blur 이벤트가 없으므로 즉시 저장 트리거 - if (date) handleBlur(index, field, date); - }} - disabled={disabled || isSaving} - /> - ) - } else if (field === 'isAlternative') { - return ( -
- { - handleCheckboxChange(index, field, checked as boolean); - handleBlur(index, field, checked as boolean); - }} - disabled={disabled || isSaving} - /> - 대체품 -
- ) - } - - return null - } - - // 대체품 필드 렌더링 - const renderAlternativeFields = (item: QuotationItem, index: number) => { - if (!item.isAlternative) return null; - - return ( -
- {/*
- - handleTextInputChange(index, 'vendorMaterialCode', e)} - onBlur={(e) => handleBlur(index, 'vendorMaterialCode', e.target.value)} - disabled={disabled || isSaving} - className="h-8 text-sm" - placeholder="벤더 자재그룹 입력" - /> -
*/} - -
- - handleTextInputChange(index, 'vendorMaterialDescription', e)} - onBlur={(e) => handleBlur(index, 'vendorMaterialDescription', e.target.value)} - disabled={disabled || isSaving} - className="h-8 text-sm" - placeholder="벤더 자재명 입력" - /> -
- -
- -