summaryrefslogtreecommitdiff
path: root/components/pq/pq-review-table.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/pq/pq-review-table.tsx')
-rw-r--r--components/pq/pq-review-table.tsx344
1 files changed, 0 insertions, 344 deletions
diff --git a/components/pq/pq-review-table.tsx b/components/pq/pq-review-table.tsx
deleted file mode 100644
index ce30bac0..00000000
--- a/components/pq/pq-review-table.tsx
+++ /dev/null
@@ -1,344 +0,0 @@
-"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"
-import { useSession } from "next-auth/react"
-
-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 (
- <div className="space-y-4">
- {data.map((group) => (
- <Collapsible key={group.groupName} defaultOpen>
- <CollapsibleTrigger asChild>
- <div className="flex items-center justify-between cursor-pointer p-3 bg-muted rounded">
- <h2 className="font-semibold text-lg">{group.groupName}</h2>
- <Button variant="ghost" size="sm" className="p-0 h-7 w-7">
- <ChevronsUpDown className="h-4 w-4" />
- </Button>
- </div>
- </CollapsibleTrigger>
-
- <CollapsibleContent>
- <Card className="mt-2 p-4">
- <Table>
- <TableHeader>
- <TableRow>
- <TableHead className="w-[60px]">Code</TableHead>
- <TableHead>Check Point</TableHead>
- <TableHead>Answer</TableHead>
- <TableHead className="w-[180px]">Attachments</TableHead>
- <TableHead className="w-[60px] text-center">Comments</TableHead>
- </TableRow>
- </TableHeader>
-
- <TableBody>
- {group.items.map((item) => (
- <TableRow key={item.criteriaId}>
- <TableCell className="font-medium">{item.code}</TableCell>
- <TableCell>{item.checkPoint}</TableCell>
-
- <TableCell>
- {item.answer ? (
- <p className="whitespace-pre-wrap text-sm">
- {item.answer}
- </p>
- ) : (
- <p className="text-sm text-muted-foreground">(no answer)</p>
- )}
- </TableCell>
-
- <TableCell>
- {item.attachments.length > 0 ? (
- <ul className="list-none space-y-1">
- {item.attachments.map((file) => (
- <li key={file.attachId} className="text-sm flex items-center">
- <button
- className="text-blue-600 hover:text-blue-800 hover:underline flex items-center truncate max-w-[160px]"
- onClick={() => handleFileDownload(file.filePath, file.fileName)}
- >
- <Download className="h-3 w-3 mr-1 flex-shrink-0" />
- <span className="truncate">{file.fileName}</span>
- </button>
- </li>
- ))}
- </ul>
- ) : (
- <p className="text-sm text-muted-foreground">(none)</p>
- )}
- </TableCell>
-
- <TableCell className="text-center">
- <ItemReviewButton
- answerId={item.answerId ?? undefined}
- checkPoint={item.checkPoint}
- onCommentAdded={onCommentAdded}
- />
- </TableCell>
- </TableRow>
- ))}
- </TableBody>
- </Table>
- </Card>
- </CollapsibleContent>
- </Collapsible>
- ))}
- </div>
- )
-}
-
-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<ReviewLog[]>([]);
- const [newComment, setNewComment] = React.useState("");
- const [isLoading, setIsLoading] = React.useState(false);
- const [hasComments, setHasComments] = React.useState(false);
- const { data: session } = useSession()
- const reviewerName = session?.user?.name || "Unknown Reviewer"
- const reviewerId = session?.user?.id
-
- // If there's no answerId, item wasn't answered
- if (!answerId) {
- return <p className="text-xs text-muted-foreground">N/A</p>;
- }
-
- // 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,
- });
-
- 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 (
- <>
- <Button variant="ghost" size="sm" onClick={handleButtonClick}>
- <MessagesSquare
- className={`h-4 w-4 ${hasComments ? 'text-blue-600' : ''}`}
- />
- </Button>
-
- <Dialog open={open} onOpenChange={handleOpenChange}>
- <DialogContent>
- <DialogHeader>
- <DialogTitle>{checkPoint} Comments</DialogTitle>
- </DialogHeader>
-
- {/* Logs section */}
- <div className="max-h-[200px] overflow-y-auto space-y-2">
- {isLoading ? (
- <div className="flex justify-center p-4">
- <Loader2 className="h-6 w-6 animate-spin text-muted-foreground" />
- </div>
- ) : logs.length > 0 ? (
- logs.map((log) => (
- <div key={log.id} className="p-2 border rounded text-sm">
- <p className="font-medium">{log.reviewerName}</p>
- <p>{log.reviewerComment}</p>
- <p className="text-xs text-muted-foreground">
- {formatDate(log.createdAt, "KR")}
- </p>
- </div>
- ))
- ) : (
- <p className="text-sm text-muted-foreground">No comments yet.</p>
- )}
- </div>
-
- {/* Add new comment */}
- <div className="space-y-2">
- <Textarea
- placeholder="Add a new comment..."
- value={newComment}
- onChange={(e) => setNewComment(e.target.value)}
- />
- <Button
- size="sm"
- onClick={handleAddComment}
- disabled={isLoading || !newComment.trim()}
- >
- Add Comment
- </Button>
- </div>
- </DialogContent>
- </Dialog>
- </>
- );
-} \ No newline at end of file