From 1a2241c40e10193c5ff7008a7b7b36cc1d855d96 Mon Sep 17 00:00:00 2001 From: joonhoekim <26rote@gmail.com> Date: Tue, 25 Mar 2025 15:55:45 +0900 Subject: initial commit --- components/pq/pq-review-table.tsx | 340 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 340 insertions(+) create mode 100644 components/pq/pq-review-table.tsx (limited to 'components/pq/pq-review-table.tsx') diff --git a/components/pq/pq-review-table.tsx b/components/pq/pq-review-table.tsx new file mode 100644 index 00000000..e778cf91 --- /dev/null +++ b/components/pq/pq-review-table.tsx @@ -0,0 +1,340 @@ +"use client" + +import * as React from "react" +import { ChevronsUpDown, MessagesSquare, Download, Loader2 } from "lucide-react" + +import { + Collapsible, + CollapsibleContent, + CollapsibleTrigger, +} from "@/components/ui/collapsible" +import { + Table, + TableBody, + TableCell, + TableHead, + TableHeader, + TableRow, +} from "@/components/ui/table" +import { Card } from "@/components/ui/card" +import { Button } from "@/components/ui/button" +import { PQGroupData } from "@/lib/pq/service" +import { Dialog, DialogContent, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" +import { Textarea } from "@/components/ui/textarea" +import { addReviewCommentAction, getItemReviewLogsAction } from "@/lib/pq/service" +import { useToast } from "@/hooks/use-toast" +import { formatDate } from "@/lib/utils" +import { downloadFileAction } from "@/lib/downloadFile" + +interface ReviewLog { + id: number + reviewerComment: string + reviewerName: string | null + createdAt: Date +} + +interface VendorPQReviewPageProps { + data: PQGroupData[]; + onCommentAdded?: () => void; // 코멘트 추가 콜백 +} + +export default function VendorPQReviewPage({ data, onCommentAdded }: VendorPQReviewPageProps) { + const { toast } = useToast() + + // 파일 다운로드 함수 - 서버 액션 사용 + const handleFileDownload = async (filePath: string, fileName: string) => { + try { + toast({ + title: "Download Started", + description: `Preparing ${fileName} for download...`, + }); + + // 서버 액션 호출 + const result = await downloadFileAction(filePath); + + if (!result.ok || !result.data) { + throw new Error(result.error || 'Failed to download file'); + } + + // Base64 디코딩하여 Blob 생성 + const binaryString = atob(result.data.content); + const bytes = new Uint8Array(binaryString.length); + for (let i = 0; i < binaryString.length; i++) { + bytes[i] = binaryString.charCodeAt(i); + } + + // Blob 생성 및 다운로드 + const blob = new Blob([bytes.buffer], { type: result.data.mimeType }); + const url = URL.createObjectURL(blob); + + // 다운로드 링크 생성 및 클릭 + const a = document.createElement('a'); + a.href = url; + a.download = fileName; + document.body.appendChild(a); + a.click(); + + // 정리 + URL.revokeObjectURL(url); + document.body.removeChild(a); + + toast({ + title: "Download Complete", + description: `${fileName} downloaded successfully`, + }); + } catch (error) { + console.error('Download error:', error); + toast({ + title: "Download Error", + description: error instanceof Error ? error.message : "Failed to download file", + variant: "destructive" + }); + } + }; + + return ( +
+ {data.map((group) => ( + + +
+

{group.groupName}

+ +
+
+ + + + + + + Code + Check Point + Answer + Attachments + Comments + + + + + {group.items.map((item) => ( + + {item.code} + {item.checkPoint} + + + {item.answer ? ( +

+ {item.answer} +

+ ) : ( +

(no answer)

+ )} +
+ + + {item.attachments.length > 0 ? ( +
    + {item.attachments.map((file) => ( +
  • + +
  • + ))} +
+ ) : ( +

(none)

+ )} +
+ + + + +
+ ))} +
+
+
+
+
+ ))} +
+ ) +} + +interface ItemReviewButtonProps { + answerId?: number; + checkPoint: string; // Check Point 추가 + onCommentAdded?: () => void; +} + +/** + * A button that opens a dialog to show logs + add new comment for a single item (vendorPqCriteriaAnswers). + */ +function ItemReviewButton({ answerId, checkPoint, onCommentAdded }: ItemReviewButtonProps) { + const { toast } = useToast(); + const [open, setOpen] = React.useState(false); + const [logs, setLogs] = React.useState([]); + const [newComment, setNewComment] = React.useState(""); + const [isLoading, setIsLoading] = React.useState(false); + const [hasComments, setHasComments] = React.useState(false); + + // If there's no answerId, item wasn't answered + if (!answerId) { + return

N/A

; + } + + // fetchLogs 함수를 useCallback으로 메모이제이션 + const fetchLogs = React.useCallback(async () => { + try { + setIsLoading(true); + const res = await getItemReviewLogsAction({ answerId }); + + if (res.ok && res.data) { + setLogs(res.data); + // 코멘트 존재 여부 설정 + setHasComments(res.data.length > 0); + } else { + console.error("Error response:", res.error); + toast({ title: "Error", description: res.error, variant: "destructive" }); + } + } catch (error) { + console.error("Fetch error:", error); + toast({ title: "Error", description: String(error), variant: "destructive" }); + } finally { + setIsLoading(false); + } + }, [answerId, toast]); + + // 초기 로드 시 코멘트 존재 여부 확인 (아이콘 색상용) + React.useEffect(() => { + const checkComments = async () => { + try { + const res = await getItemReviewLogsAction({ answerId }); + if (res.ok && res.data) { + setHasComments(res.data.length > 0); + } + } catch (error) { + console.error("Error checking comments:", error); + } + }; + + checkComments(); + }, [answerId]); + + // open 상태가 변경될 때 로그 가져오기 + React.useEffect(() => { + if (open) { + fetchLogs(); + } + }, [open, fetchLogs]); + + // 버튼 클릭 핸들러 - 다이얼로그 열기 + const handleButtonClick = React.useCallback(() => { + setOpen(true); + }, []); + + // 다이얼로그 상태 변경 핸들러 + const handleOpenChange = React.useCallback((nextOpen: boolean) => { + setOpen(nextOpen); + }, []); + + // 코멘트 추가 핸들러 + const handleAddComment = React.useCallback(async () => { + try { + setIsLoading(true); + + const res = await addReviewCommentAction({ + answerId, + comment: newComment, + reviewerName: "AdminUser", + }); + + if (res.ok) { + toast({ title: "Comment added", description: "New review comment saved" }); + setNewComment(""); + setHasComments(true); // 코멘트 추가 성공 시 상태 업데이트 + + // 코멘트가 추가되었음을 부모 컴포넌트에 알림 + if (onCommentAdded) { + onCommentAdded(); + } + + // 로그 다시 가져오기 + fetchLogs(); + } else { + toast({ title: "Error", description: res.error, variant: "destructive" }); + } + } catch (error) { + toast({ title: "Error", description: String(error), variant: "destructive" }); + } finally { + setIsLoading(false); + } + }, [answerId, newComment, onCommentAdded, fetchLogs, toast]); + + return ( + <> + + + + + + {checkPoint} Comments + + + {/* Logs section */} +
+ {isLoading ? ( +
+ +
+ ) : logs.length > 0 ? ( + logs.map((log) => ( +
+

{log.reviewerName}

+

{log.reviewerComment}

+

+ {formatDate(log.createdAt)} +

+
+ )) + ) : ( +

No comments yet.

+ )} +
+ + {/* Add new comment */} +
+