diff options
Diffstat (limited to 'lib/procurement-rfqs/vendor-response/quotation-editor.tsx')
| -rw-r--r-- | lib/procurement-rfqs/vendor-response/quotation-editor.tsx | 955 |
1 files changed, 0 insertions, 955 deletions
diff --git a/lib/procurement-rfqs/vendor-response/quotation-editor.tsx b/lib/procurement-rfqs/vendor-response/quotation-editor.tsx deleted file mode 100644 index 66bb2613..00000000 --- a/lib/procurement-rfqs/vendor-response/quotation-editor.tsx +++ /dev/null @@ -1,955 +0,0 @@ -"use client" - -import * as React from "react" -import { useState, useEffect, useMemo } from "react" -import { useForm } from "react-hook-form" -import { zodResolver } from "@hookform/resolvers/zod" -import * as z from "zod" -import { format } from "date-fns" -import { toast } from "sonner" -import { MessageSquare, Paperclip } from "lucide-react" - -import { Button } from "@/components/ui/button" -import { - Form, - FormControl, - FormDescription, - FormField, - FormItem, - FormLabel, - FormMessage, -} from "@/components/ui/form" -import { Input } from "@/components/ui/input" -import { Textarea } from "@/components/ui/textarea" -import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" -import { Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle } from "@/components/ui/card" -import { Badge } from "@/components/ui/badge" -import { Separator } from "@/components/ui/separator" -import { ScrollArea } from "@/components/ui/scroll-area" -import { DatePicker } from "@/components/ui/date-picker" -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select" -import { ClientDataTable } from "@/components/client-data-table/data-table" -import { Skeleton } from "@/components/ui/skeleton" - -import { formatCurrency, formatDate } from "@/lib/utils" -import { QuotationItemEditor } from "./quotation-item-editor" -import { - submitVendorQuotation, - updateVendorQuotation, - fetchCurrencies, - fetchPaymentTerms, - fetchIncoterms, - fetchBuyerVendorComments, - Comment -} from "../services" -import { BuyerCommunicationDrawer } from "./buyer-communication-drawer" - -// 견적서 폼 스키마 -const quotationFormSchema = z.object({ - quotationVersion: z.number().min(1), - // 필수값 표시됨 - currency: z.string().min(1, "통화를 선택해주세요"), - // 필수값 표시됨 - validUntil: z.date({ - required_error: "견적 유효기간을 선택해주세요", - invalid_type_error: "유효한 날짜를 선택해주세요", - }), - // 필수값 표시됨 - estimatedDeliveryDate: z.date({ - required_error: "예상 납품일을 선택해주세요", - invalid_type_error: "유효한 날짜를 선택해주세요", - }), - // 필수값 표시됨 - paymentTermsCode: z.string({ - required_error: "지불 조건을 선택해주세요", - }).min(1, "지불 조건을 선택해주세요"), - // 필수값 표시됨 - incotermsCode: z.string({ - required_error: "인코텀즈를 선택해주세요", - }).min(1, "인코텀즈를 선택해주세요"), - // 필수값 아님 - incotermsDetail: z.string().optional(), - // 필수값 아님 - remark: z.string().optional(), -}) - -type QuotationFormValues = z.infer<typeof quotationFormSchema> - -// 데이터 타입 정의 -interface Currency { - code: string - name: string -} - -interface PaymentTerm { - code: string - description: string -} - -interface Incoterm { - code: string - description: string -} - -// 이 컴포넌트에 전달되는 견적서 데이터 타입 -interface VendorQuotation { - id: number - rfqId: number - vendorId: number - quotationCode: string | null - quotationVersion: number | null - totalItemsCount: number | null - subTotal: string| null - taxTotal: string| null - discountTotal: string| null - totalPrice: string| null - currency: string| null - validUntil: Date | null - estimatedDeliveryDate: Date | null - paymentTermsCode: string | null - incotermsCode: string | null - incotermsDetail: string | null - status: "Draft" | "Submitted" | "Revised" | "Rejected" | "Accepted" - remark: string | null - rejectionReason: string | null - submittedAt: Date | null - acceptedAt: Date | null - createdAt: Date - updatedAt: Date - rfq: { - id: number - rfqCode: string| null - dueDate: Date | null - status: string| null - // 기타 필요한 정보 - } - vendor: { - id: number - vendorName: string - vendorCode: string| null - // 기타 필요한 정보 - } - items: QuotationItem[] -} - -// 견적 아이템 타입 -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 - createdAt: Date - updatedAt: Date - prItem?: { - id: number - materialCode: string | null - materialDescription: string | null - // 기타 필요한 정보 - } -} - -// 견적서 편집 컴포넌트 프롭스 -interface VendorQuotationEditorProps { - quotation: VendorQuotation -} - -export default function VendorQuotationEditor({ quotation }: VendorQuotationEditorProps) { - - - console.log(quotation) - - const [activeTab, setActiveTab] = useState("items") - const [isSubmitting, setIsSubmitting] = useState(false) - const [isSaving, setIsSaving] = useState(false) - const [items, setItems] = useState<QuotationItem[]>(quotation.items || []) - - // 서버에서 가져온 데이터 상태 - const [currencies, setCurrencies] = useState<Currency[]>([]) - const [paymentTerms, setPaymentTerms] = useState<PaymentTerm[]>([]) - const [incoterms, setIncoterms] = useState<Incoterm[]>([]) - - // 데이터 로딩 상태 - const [loadingCurrencies, setLoadingCurrencies] = useState(true) - const [loadingPaymentTerms, setLoadingPaymentTerms] = useState(true) - const [loadingIncoterms, setLoadingIncoterms] = useState(true) - - // 커뮤니케이션 드로어 상태 - const [communicationDrawerOpen, setCommunicationDrawerOpen] = useState(false) - - const [comments, setComments] = useState<Comment[]>([]); - const [unreadCount, setUnreadCount] = useState(0); - const [loadingComments, setLoadingComments] = useState(false); - - // 컴포넌트 마운트 시 메시지 미리 로드 - useEffect(() => { - if (quotation) { - loadCommunicationData(); - } - }, [quotation]); - - // 메시지 데이터 로드 함수 - const loadCommunicationData = async () => { - try { - setLoadingComments(true); - const commentsData = await fetchBuyerVendorComments(quotation.rfqId, quotation.vendorId); - setComments(commentsData); - - // 읽지 않은 메시지 수 계산 - const unread = commentsData.filter( - comment => !comment.isVendorComment && !comment.isRead - ).length; - setUnreadCount(unread); - } catch (error) { - console.error("메시지 데이터 로드 오류:", error); - } finally { - setLoadingComments(false); - } - }; - - // 커뮤니케이션 드로어가 닫힐 때 데이터 새로고침 - const handleCommunicationDrawerChange = (open: boolean) => { - setCommunicationDrawerOpen(open); - if (!open) { - loadCommunicationData(); // 드로어가 닫힐 때 데이터 새로고침 - } - }; - - // 버튼 비활성화 - const isBeforeDueDate = () => { - if (!quotation.rfq.dueDate) { - // dueDate가 null인 경우 기본적으로 수정 불가능하도록 설정 (false 반환) - return false; - } - - const now = new Date(); - const dueDate = new Date(quotation.rfq.dueDate); - return now < dueDate; - }; - // 수정된 isDisabled 조건 - const isDisabled = (quotation.status === "Accepted") || - ((quotation.status === "Submitted" || quotation.status === "Revised") && - !isBeforeDueDate()); - - - // 견적서 총합 계산 - const totals = useMemo(() => { - const subTotal = items.reduce((sum, item) => sum + Number(item.totalPrice), 0) - const taxTotal = items.reduce((sum, item) => sum + (Number(item.taxAmount) || 0), 0) - const discountTotal = items.reduce((sum, item) => sum + (Number(item.discountAmount) || 0), 0) - const totalPrice = subTotal + taxTotal - discountTotal - - return { - subTotal, - taxTotal, - discountTotal, - totalPrice - } - }, [items]) - - // 폼 설정 - const form = useForm<QuotationFormValues>({ - resolver: zodResolver(quotationFormSchema), - defaultValues: { - quotationVersion: quotation.quotationVersion || 0, - currency: quotation.currency || "KRW", - validUntil: quotation.validUntil || undefined, - estimatedDeliveryDate: quotation.estimatedDeliveryDate || undefined, - paymentTermsCode: quotation.paymentTermsCode || "", - incotermsCode: quotation.incotermsCode || "", - incotermsDetail: quotation.incotermsDetail || "", - remark: quotation.remark || "", - }, - mode: "onChange", // 실시간 검증 활성화 - }) - - // 마운트 시 데이터 로드 - useEffect(() => { - // 통화 데이터 로드 - const loadCurrencies = async () => { - try { - setLoadingCurrencies(true) - const result = await fetchCurrencies() - if (result.success) { - setCurrencies(result.data) - } else { - toast.error(result.message || "통화 데이터 로드 실패") - } - } catch (error) { - console.error("통화 데이터 로드 오류:", error) - toast.error("통화 데이터를 불러오는 중 오류가 발생했습니다") - } finally { - setLoadingCurrencies(false) - } - } - - // 지불 조건 데이터 로드 - const loadPaymentTerms = async () => { - try { - setLoadingPaymentTerms(true) - const result = await fetchPaymentTerms() - if (result.success) { - setPaymentTerms(result.data) - } else { - toast.error(result.message || "지불 조건 데이터 로드 실패") - } - } catch (error) { - console.error("지불 조건 데이터 로드 오류:", error) - toast.error("지불 조건 데이터를 불러오는 중 오류가 발생했습니다") - } finally { - setLoadingPaymentTerms(false) - } - } - - // 인코텀즈 데이터 로드 - const loadIncoterms = async () => { - try { - setLoadingIncoterms(true) - const result = await fetchIncoterms() - if (result.success) { - setIncoterms(result.data) - } else { - toast.error(result.message || "인코텀즈 데이터 로드 실패") - } - } catch (error) { - console.error("인코텀즈 데이터 로드 오류:", error) - toast.error("인코텀즈 데이터를 불러오는 중 오류가 발생했습니다") - } finally { - setLoadingIncoterms(false) - } - } - - // 함수 호출 - loadCurrencies() - loadPaymentTerms() - loadIncoterms() - }, []) - - // 견적서 저장 - const handleSave = async () => { - try { - setIsSaving(true) - - // 기본 검증 (통화는 필수) - const validationResult = await form.trigger(['currency']); - if (!validationResult) { - toast.warning("통화는 필수 항목입니다"); - return; - } - - const values = form.getValues() - - const result = await updateVendorQuotation({ - id: quotation.id, - ...values, - subTotal: totals.subTotal.toString(), - taxTotal: totals.taxTotal.toString(), - discountTotal: totals.discountTotal.toString(), - totalPrice: totals.totalPrice.toString(), - totalItemsCount: items.length, - }) - - if (result.success) { - toast.success("견적서가 저장되었습니다") - - // 견적서 제출 준비 상태 점검 - const formValid = await form.trigger(); - const itemsValid = !items.some(item => item.unitPrice <= 0 || !item.deliveryDate); - const alternativeItemsValid = !items.some(item => - item.isAlternative && (!item.vendorMaterialDescription || !item.remark) - ); - - if (formValid && itemsValid && alternativeItemsValid) { - toast.info("모든 필수 정보가 입력되었습니다. '견적서 제출' 버튼을 클릭하여 제출하세요."); - } else { - const missingFields = []; - if (!formValid) missingFields.push("견적서 기본 정보"); - if (!itemsValid) missingFields.push("견적 항목의 단가/납품일"); - if (!alternativeItemsValid) missingFields.push("대체품 정보"); - - toast.info(`제출하기 전에 다음 정보를 입력해주세요: ${missingFields.join(', ')}`); - } - } else { - toast.error(result.message || "견적서 저장 중 오류가 발생했습니다") - } - } catch (error) { - console.error("견적서 저장 오류:", error) - toast.error("견적서 저장 중 오류가 발생했습니다") - } finally { - setIsSaving(false) - } - } - - // 견적서 제출 - const handleSubmit = async () => { - try { - setIsSubmitting(true) - - // 1. 폼 스키마 검증 (기본 정보) - const formValid = await form.trigger(); - if (!formValid) { - const formState = form.getFieldState("validUntil"); - const estimatedDeliveryState = form.getFieldState("estimatedDeliveryDate"); - const paymentTermsState = form.getFieldState("paymentTermsCode"); - const incotermsState = form.getFieldState("incotermsCode"); - - // 주요 필드별 오류 메시지 표시 - if (!form.getValues("validUntil")) { - toast.error("견적 유효기간을 선택해주세요"); - } else if (!form.getValues("estimatedDeliveryDate")) { - toast.error("예상 납품일을 선택해주세요"); - } else if (!form.getValues("paymentTermsCode")) { - toast.error("지불 조건을 선택해주세요"); - } else if (!form.getValues("incotermsCode")) { - toast.error("인코텀즈를 선택해주세요"); - } else { - toast.error("견적서 기본 정보를 모두 입력해주세요"); - } - - // 견적 정보 탭으로 이동 - setActiveTab("details"); - return; - } - - // 2. 견적 항목 검증 - const emptyItems = items.filter(item => - item.unitPrice <= 0 || !item.deliveryDate - ); - - if (emptyItems.length > 0) { - toast.error(`${emptyItems.length}개 항목의 단가와 납품일을 입력해주세요`); - setActiveTab("items"); - return; - } - - // 3. 대체품 정보 검증 - const invalidAlternativeItems = items.filter(item => - item.isAlternative && (!item.vendorMaterialDescription || !item.remark) - ); - - if (invalidAlternativeItems.length > 0) { - toast.error(`${invalidAlternativeItems.length}개의 대체품 항목에 정보를 모두 입력해주세요`); - setActiveTab("items"); - return; - } - - // 모든 검증 통과 - 제출 진행 - const values = form.getValues(); - - const result = await submitVendorQuotation({ - id: quotation.id, - ...values, - subTotal: totals.subTotal.toString(), - taxTotal: totals.taxTotal.toString(), - discountTotal: totals.discountTotal.toString(), - totalPrice: totals.totalPrice.toString(), - totalItemsCount: items.length, - }); - - if (result.success && isBeforeDueDate()) { - toast.success("견적서가 제출되었습니다. 마감일 전까지 수정 가능합니다."); - - // 페이지 새로고침 - window.location.reload(); - } else { - toast.error(result.message || "견적서 제출 중 오류가 발생했습니다"); - } - } catch (error) { - console.error("견적서 제출 오류:", error); - toast.error("견적서 제출 중 오류가 발생했습니다"); - } finally { - setIsSubmitting(false); - } - } - - const isSubmitReady = () => { - // 폼 유효성 - const formValid = !Object.keys(form.formState.errors).length; - - // 항목 유효성 - const itemsValid = !items.some(item => - item.unitPrice <= 0 || !item.deliveryDate - ); - - // 대체품 유효성 - const alternativeItemsValid = !items.some(item => - item.isAlternative && (!item.vendorMaterialDescription || !item.remark) - ); - - // 유효하지 않은 항목 또는 대체품이 있으면 제출 불가 - return formValid && itemsValid && alternativeItemsValid; - } - - // 아이템 업데이트 핸들러 - const handleItemsUpdate = (updatedItems: QuotationItem[]) => { - setItems(updatedItems) - } - - // 상태에 따른 배지 색상 - const getStatusBadge = (status: string) => { - switch (status) { - case "Draft": - return <Badge variant="outline">초안</Badge> - case "Submitted": - return <Badge variant="default">제출됨</Badge> - case "Revised": - return <Badge variant="secondary">수정됨</Badge> - case "Rejected": - return <Badge variant="destructive">반려됨</Badge> - case "Accepted": - return <Badge variant="default">승인됨</Badge> - default: - return <Badge>{status}</Badge> - } - } - - // 셀렉트 로딩 상태 표시 컴포넌트 - const SelectSkeleton = () => ( - <div className="flex flex-col gap-2"> - <Skeleton className="h-4 w-[40%]" /> - <Skeleton className="h-10 w-full" /> - </div> - ) - - return ( - <div className="space-y-6"> - <div className="flex justify-between items-start"> - <div> - <h1 className="text-2xl font-bold tracking-tight">견적서 작성</h1> - <p className="text-muted-foreground"> - RFQ 번호: {quotation.rfq.rfqCode} | 견적서 번호: {quotation.quotationCode} - </p> - {quotation.rfq.dueDate ? ( - <p className={`text-sm ${isBeforeDueDate() ? 'text-green-600' : 'text-red-600'}`}> - 마감일: {formatDate(new Date(quotation.rfq.dueDate))} - {isBeforeDueDate() - ? ' (마감 전: 수정 가능)' - : ' (마감 됨: 수정 불가)'} - </p> - ) : ( - <p className="text-sm text-amber-600"> - 마감일이 설정되지 않았습니다 - </p> - )} - </div> - <div className="flex items-center gap-2"> - {getStatusBadge(quotation.status)} - {quotation.status === "Rejected" && ( - <div className="text-sm text-destructive"> - <span className="font-medium">반려 사유:</span> {quotation.rejectionReason || "사유 없음"} - </div> - )} - </div> - </div> - - <Tabs value={activeTab} onValueChange={setActiveTab} className="w-full"> - <TabsList> - <TabsTrigger value="items">견적 항목</TabsTrigger> - <TabsTrigger value="details">견적 정보</TabsTrigger> - <TabsTrigger value="communication">커뮤니케이션</TabsTrigger> - </TabsList> - - {/* 견적 항목 탭 */} - <TabsContent value="items" className="p-0 pt-4"> - <Card> - <CardHeader> - <CardTitle>견적 항목 정보</CardTitle> - <CardDescription> - 각 항목에 대한 가격, 납품일 등을 입력해주세요 - </CardDescription> - </CardHeader> - <CardContent> - <QuotationItemEditor - items={items} - onItemsChange={handleItemsUpdate} - disabled={isDisabled} - currency={form.watch("currency")} - /> - </CardContent> - <CardFooter className="flex justify-between border-t p-4"> - <div className="space-y-1"> - <div className="text-sm text-muted-foreground"> - <span className="font-medium">소계:</span> {formatCurrency(totals.subTotal, quotation.currency)} - </div> - <div className="text-sm text-muted-foreground"> - <span className="font-medium">세액:</span> {formatCurrency(totals.taxTotal, quotation.currency)} - </div> - <div className="text-sm text-muted-foreground"> - <span className="font-medium">할인액:</span> {formatCurrency(totals.discountTotal, quotation.currency)} - </div> - <div className="text-base font-bold"> - <span>총액:</span> {formatCurrency(totals.totalPrice, quotation.currency)} - </div> - </div> - <div className="flex space-x-2"> - <Button - variant="outline" - onClick={handleSave} - disabled={isDisabled || isSaving} - > - {isSaving ? "저장 중..." : "저장"} - </Button> - <Button - onClick={handleSubmit} - disabled={isDisabled || isSubmitting || !isSubmitReady()} - > - {isSubmitting ? "제출 중..." : "견적서 제출"} - </Button> - </div> - </CardFooter> - </Card> - </TabsContent> - - {/* 견적 정보 탭 */} - <TabsContent value="details" className="p-0 pt-4"> - <Form {...form}> - <form className="space-y-6"> - <Card> - <CardHeader> - <CardTitle>견적서 기본 정보</CardTitle> - <CardDescription> - 견적서의 일반 정보를 입력해주세요 - </CardDescription> - </CardHeader> - <CardContent className="grid grid-cols-1 md:grid-cols-2 gap-6"> - {/* 통화 필드 */} - {loadingCurrencies ? ( - <SelectSkeleton /> - ) : ( - <FormField - control={form.control} - name="currency" - render={({ field }) => ( - <FormItem> - <FormLabel className="flex items-center"> - 통화 - <span className="text-destructive ml-1">*</span> - </FormLabel> - <Select - value={field.value} - onValueChange={field.onChange} - disabled={isDisabled} - > - <FormControl> - <SelectTrigger> - <SelectValue placeholder="통화 선택" /> - </SelectTrigger> - </FormControl> - <SelectContent> - {currencies.map((currency) => ( - <SelectItem key={currency.code} value={currency.code}> - {currency.code} ({currency.name}) - </SelectItem> - ))} - </SelectContent> - </Select> - <FormMessage /> - </FormItem> - )} - /> - )} - - <FormField - control={form.control} - name="validUntil" - render={({ field }) => ( - <FormItem> - <FormLabel className="flex items-center"> - 견적 유효기간 - <span className="text-destructive ml-1">*</span> {/* 필수값 표시 */} - </FormLabel> - <FormControl> - <DatePicker - date={field.value} - onSelect={field.onChange} - disabled={isDisabled} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - <FormField - control={form.control} - name="estimatedDeliveryDate" - render={({ field }) => ( - <FormItem> - <FormLabel className="flex items-center"> - 예상 납품일 - <span className="text-destructive ml-1">*</span> - </FormLabel> - <FormControl> - <DatePicker - date={field.value} - onSelect={field.onChange} - disabled={isDisabled} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - {/* 지불 조건 필드 */} - {loadingPaymentTerms ? ( - <SelectSkeleton /> - ) : ( - <FormField - control={form.control} - name="paymentTermsCode" - render={({ field }) => ( - <FormItem> - <FormLabel className="flex items-center"> - 지불 조건 - <span className="text-destructive ml-1">*</span> - </FormLabel> - <Select - value={field.value || ""} - onValueChange={field.onChange} - disabled={isDisabled} - > - <FormControl> - <SelectTrigger> - <SelectValue placeholder="지불 조건 선택" /> - </SelectTrigger> - </FormControl> - <SelectContent> - {paymentTerms.map((term) => ( - <SelectItem key={term.code} value={term.code}> - {term.description} - </SelectItem> - ))} - </SelectContent> - </Select> - <FormMessage /> - </FormItem> - )} - /> - )} - - {/* 인코텀즈 필드 */} - {loadingIncoterms ? ( - <SelectSkeleton /> - ) : ( - <FormField - control={form.control} - name="incotermsCode" - render={({ field }) => ( - <FormItem> - <FormLabel className="flex items-center"> - 인코텀즈 - <span className="text-destructive ml-1">*</span> - </FormLabel> - <Select - value={field.value || ""} - onValueChange={field.onChange} - disabled={isDisabled} - > - <FormControl> - <SelectTrigger> - <SelectValue placeholder="인코텀즈 선택" /> - </SelectTrigger> - </FormControl> - <SelectContent> - {incoterms.map((term) => ( - <SelectItem key={term.code} value={term.code}> - {term.code} ({term.description}) - </SelectItem> - ))} - </SelectContent> - </Select> - <FormMessage /> - </FormItem> - )} - /> - )} - - <FormField - control={form.control} - name="incotermsDetail" - render={({ field }) => ( - <FormItem> - <FormLabel className="flex items-center"> - 인코텀즈 상세 - <span className="text-destructive ml-1"></span> - </FormLabel> - <FormControl> - <Input - placeholder="인코텀즈 상세 정보 입력" - {...field} - value={field.value || ""} - disabled={isDisabled} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - <FormField - control={form.control} - name="remark" - render={({ field }) => ( - <FormItem className="col-span-2"> - <FormLabel className="flex items-center"> - 비고 - <span className="text-destructive ml-1"></span> - </FormLabel> - <FormControl> - <Textarea - placeholder="추가 정보나 특이사항을 입력해주세요" - className="resize-none min-h-[100px]" - {...field} - value={field.value || ""} - disabled={isDisabled} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - </CardContent> - <CardFooter className="flex justify-end"> - <Button - variant="outline" - onClick={handleSave} - disabled={isDisabled || isSaving} - > - {isSaving ? "저장 중..." : "저장"} - </Button> - </CardFooter> - </Card> - </form> - </Form> - </TabsContent> - - {/* 커뮤니케이션 탭 */} - <TabsContent value="communication" className="p-0 pt-4"> - <Card> - <CardHeader className="flex flex-row items-center justify-between"> - <div> - <CardTitle className="flex items-center gap-2"> - 커뮤니케이션 - {unreadCount > 0 && ( - <Badge variant="destructive" className="ml-2"> - 새 메시지 {unreadCount} - </Badge> - )} - </CardTitle> - <CardDescription> - 구매자와의 메시지 및 첨부파일 - </CardDescription> - </div> - <Button - onClick={() => setCommunicationDrawerOpen(true)} - variant="outline" - size="sm" - > - <MessageSquare className="h-4 w-4 mr-2" /> - {unreadCount > 0 ? "새 메시지 확인" : "메시지 보내기"} - </Button> - </CardHeader> - <CardContent> - {loadingComments ? ( - <div className="flex items-center justify-center p-8"> - <div className="text-center"> - <Skeleton className="h-4 w-32 mx-auto mb-2" /> - <Skeleton className="h-4 w-48 mx-auto" /> - </div> - </div> - ) : comments.length === 0 ? ( - <div className="min-h-[200px] flex flex-col items-center justify-center text-center p-8"> - <div className="max-w-md"> - <div className="mx-auto bg-primary/10 rounded-full w-12 h-12 flex items-center justify-center mb-4"> - <MessageSquare className="h-6 w-6 text-primary" /> - </div> - <h3 className="text-lg font-medium mb-2">아직 메시지가 없습니다</h3> - <p className="text-muted-foreground mb-4"> - 견적서에 대한 질문이나 의견이 있으신가요? 구매자와 메시지를 주고받으세요. - </p> - <Button - onClick={() => setCommunicationDrawerOpen(true)} - className="mx-auto" - > - 메시지 보내기 - </Button> - </div> - </div> - ) : ( - <div className="space-y-4"> - {/* 최근 메시지 3개 미리보기 */} - <div className="space-y-2"> - <h3 className="text-sm font-medium">최근 메시지</h3> - <ScrollArea className="h-[250px] rounded-md border p-4"> - {comments.slice(-3).map(comment => ( - <div - key={comment.id} - className={`p-3 mb-3 rounded-lg ${!comment.isVendorComment && !comment.isRead - ? 'bg-primary/10 border-l-4 border-primary' - : 'bg-muted/50' - }`} - > - <div className="flex justify-between items-center mb-1"> - <span className="text-sm font-medium"> - {comment.isVendorComment - ? '나' - : comment.userName || '구매 담당자'} - </span> - <span className="text-xs text-muted-foreground"> - {new Date(comment.createdAt).toLocaleDateString()} - </span> - </div> - <p className="text-sm line-clamp-2">{comment.content}</p> - {comment.attachments.length > 0 && ( - <div className="mt-1 text-xs text-muted-foreground"> - <Paperclip className="h-3 w-3 inline mr-1" /> - 첨부파일 {comment.attachments.length}개 - </div> - )} - </div> - ))} - </ScrollArea> - </div> - - <div className="flex justify-center"> - <Button - onClick={() => setCommunicationDrawerOpen(true)} - className="w-full" - > - 전체 메시지 보기 ({comments.length}개) - </Button> - </div> - </div> - )} - </CardContent> - </Card> - - {/* 커뮤니케이션 드로어 */} - <BuyerCommunicationDrawer - open={communicationDrawerOpen} - onOpenChange={handleCommunicationDrawerChange} - quotation={quotation} - onSuccess={loadCommunicationData} - /> - </TabsContent> - </Tabs> - </div> - ) -}
\ No newline at end of file |
