diff options
Diffstat (limited to 'components/pq/pq-review-table.tsx')
| -rw-r--r-- | components/pq/pq-review-table.tsx | 344 |
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 |
