diff options
Diffstat (limited to 'components/pq-input/pq-review-wrapper.tsx')
| -rw-r--r-- | components/pq-input/pq-review-wrapper.tsx | 392 |
1 files changed, 371 insertions, 21 deletions
diff --git a/components/pq-input/pq-review-wrapper.tsx b/components/pq-input/pq-review-wrapper.tsx index 216df422..1056189e 100644 --- a/components/pq-input/pq-review-wrapper.tsx +++ b/components/pq-input/pq-review-wrapper.tsx @@ -7,8 +7,7 @@ import { CardContent, CardHeader, CardTitle, - CardDescription, - CardFooter + CardDescription } from "@/components/ui/card" import { Button } from "@/components/ui/button" import { Badge } from "@/components/ui/badge" @@ -22,16 +21,18 @@ import { DialogTitle } from "@/components/ui/dialog" import { useToast } from "@/hooks/use-toast" -import { CheckCircle, AlertCircle, FileText, Paperclip } from "lucide-react" +import { CheckCircle, AlertCircle, Paperclip } from "lucide-react" import { PQGroupData } from "@/lib/pq/service" -import { approvePQAction, rejectPQAction } from "@/lib/pq/service" +import { approvePQAction, rejectPQAction, updateSHICommentAction } from "@/lib/pq/service" +// import * as ExcelJS from 'exceljs'; +// import { saveAs } from "file-saver"; // PQ 제출 정보 타입 interface PQSubmission { id: number vendorId: number - vendorName: string - vendorCode: string + vendorName: string | null + vendorCode: string | null type: string status: string projectId: number | null @@ -63,6 +64,21 @@ export function PQReviewWrapper({ const [showApproveDialog, setShowApproveDialog] = React.useState(false) const [showRejectDialog, setShowRejectDialog] = React.useState(false) const [rejectReason, setRejectReason] = React.useState("") + const [shiComments, setShiComments] = React.useState<Record<number, string>>({}) + const [isUpdatingComment, setIsUpdatingComment] = React.useState<number | null>(null) + + // 기존 SHI 코멘트를 로컬 상태에 초기화 + React.useEffect(() => { + const initialComments: Record<number, string> = {} + pqData.forEach(group => { + group.items.forEach(item => { + if (item.answerId && item.shiComment) { + initialComments[item.answerId] = item.shiComment + } + }) + }) + setShiComments(initialComments) + }, [pqData]) // PQ 승인 처리 const handleApprove = async () => { @@ -101,6 +117,178 @@ export function PQReviewWrapper({ } } + // SHI 코멘트 업데이트 처리 + const handleSHICommentUpdate = async (answerId: number) => { + const comment = shiComments[answerId] || "" + + try { + setIsUpdatingComment(answerId) + const result = await updateSHICommentAction({ + answerId, + shiComment: comment, + }) + + if (result.ok) { + toast({ + title: "SHI 코멘트 저장 완료", + description: "SHI 코멘트가 저장되었습니다.", + }) + // 페이지 새로고침 + router.refresh() + } else { + toast({ + title: "저장 실패", + description: result.error || "SHI 코멘트 저장 중 오류가 발생했습니다.", + variant: "destructive" + }) + } + } catch (error) { + console.error("SHI 코멘트 저장 오류:", error) + toast({ + title: "저장 실패", + description: "SHI 코멘트 저장 중 오류가 발생했습니다.", + variant: "destructive" + }) + } finally { + setIsUpdatingComment(null) + } + } + + // // Excel export 처리 + // const handleExportToExcel = async () => { + // try { + // setIsExporting(true) + + // // 워크북 생성 + // const workbook = new ExcelJS.Workbook() + // workbook.creator = 'PQ Management System' + // workbook.created = new Date() + + // // 메인 시트 생성 + // const worksheet = workbook.addWorksheet("PQ 항목") + + // // 헤더 정의 + // const headers = [ + // "그룹명", + // "코드", + // "체크포인트", + // "설명", + // "입력형식", + // "필수여부", + // "벤더답변", + // "SHI 코멘트", + // "벤더 답변", + // ] + + // // 헤더 추가 + // worksheet.addRow(headers) + + // // 헤더 스타일 적용 + // const headerRow = worksheet.getRow(1) + // headerRow.font = { bold: true } + // headerRow.fill = { + // type: 'pattern', + // pattern: 'solid', + // fgColor: { argb: 'FFE0E0E0' } + // } + // headerRow.alignment = { vertical: 'middle', horizontal: 'center' } + + // // 컬럼 너비 설정 + // worksheet.columns = [ + // { header: "그룹명", key: "groupName", width: 15 }, + // { header: "코드", key: "code", width: 12 }, + // { header: "체크포인트", key: "checkPoint", width: 30 }, + // { header: "설명", key: "description", width: 40 }, + // { header: "입력형식", key: "inputFormat", width: 12 }, + + // { header: "벤더답변", key: "answer", width: 30 }, + // { header: "SHI 코멘트", key: "shiComment", width: 30 }, + // { header: "벤더 답변", key: "vendorReply", width: 30 }, + // ] + + // // 데이터 추가 + // pqData.forEach(group => { + // group.items.forEach(item => { + // const rowData = [ + // group.groupName, + // item.code, + // item.checkPoint, + // item.description || "", + // item.inputFormat || "", + + // item.answer || "", + // item.shiComment || "", + // item.vendorReply || "", + // ] + // worksheet.addRow(rowData) + // }) + // }) + + // // 전체 셀에 테두리 추가 + // worksheet.eachRow((row, rowNumber) => { + // row.eachCell((cell) => { + // cell.border = { + // top: { style: 'thin' }, + // left: { style: 'thin' }, + // bottom: { style: 'thin' }, + // right: { style: 'thin' } + // } + // // 긴 텍스트는 자동 줄바꿈 + // cell.alignment = { + // vertical: 'top', + // horizontal: 'left', + // wrapText: true + // } + // }) + // }) + + // // 정보 시트 생성 + // const infoSheet = workbook.addWorksheet("정보") + // infoSheet.addRow(["벤더명", pqSubmission.vendorName]) + // if (pqSubmission.projectName) { + // infoSheet.addRow(["프로젝트명", pqSubmission.projectName]) + // } + // infoSheet.addRow(["생성일", new Date().toLocaleDateString('ko-KR')]) + // infoSheet.addRow(["총 항목 수", pqData.reduce((total, group) => total + group.items.length, 0)]) + + // // 정보 시트 스타일링 + // infoSheet.columns = [ + // { header: "항목", key: "item", width: 20 }, + // { header: "값", key: "value", width: 40 } + // ] + + // const infoHeaderRow = infoSheet.getRow(1) + // infoHeaderRow.font = { bold: true } + // infoHeaderRow.fill = { + // type: 'pattern', + // pattern: 'solid', + // fgColor: { argb: 'FFE6F3FF' } + // } + + // // 파일명 생성 + // const defaultFilename = pqSubmission.projectName + // ? `${pqSubmission.vendorName}_${pqSubmission.projectName}_PQ_${new Date().toISOString().slice(0, 10)}` + // : `${pqSubmission.vendorName}_PQ_${new Date().toISOString().slice(0, 10)}` + // const finalFilename = defaultFilename + + // // 파일 다운로드 + // const buffer = await workbook.xlsx.writeBuffer() + // const blob = new Blob([buffer], { + // type: "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" + // }) + // saveAs(blob, `${finalFilename}.xlsx`) + // } catch (error) { + // console.error("Excel export 오류:", error) + // toast({ + // title: "내보내기 실패", + // description: "Excel 내보내기 중 오류가 발생했습니다.", + // variant: "destructive" + // }) + // } finally { + // setIsExporting(false) + // } + // } + // PQ 거부 처리 const handleReject = async () => { if (!rejectReason.trim()) { @@ -163,12 +351,20 @@ export function PQReviewWrapper({ <div> <CardTitle className="text-base"> {item.code} - {item.checkPoint} + + </CardTitle> {item.description && ( <CardDescription className="mt-1 whitespace-pre-wrap"> {item.description} </CardDescription> )} + {/* <div className="text-sm text-muted-foreground"> + 생성일: {item.createdAt?.toLocaleString('ko-KR')} + </div> + <div className="text-sm text-muted-foreground"> + 수정일: {item.updatedAt?.toLocaleString('ko-KR')} + </div> */} </div> {/* 항목 상태 표시 */} {!!item.answer || item.attachments.length > 0 ? ( @@ -182,6 +378,7 @@ export function PQReviewWrapper({ 답변 없음 </Badge> )} + </div> </CardHeader> <CardContent className="space-y-4"> @@ -204,19 +401,134 @@ export function PQReviewWrapper({ </div> )} - {/* 벤더 답변 */} + + + {/* 벤더 답변 - 입력 형식에 따라 다르게 표시 */} <div className="space-y-1"> <p className="text-sm font-medium flex items-center gap-1"> - <FileText className="h-4 w-4" /> 벤더 답변 + {item.inputFormat && ( + <Badge variant="outline" className="ml-2 text-xs"> + {item.inputFormat === "TEXT" && "텍스트"} + {item.inputFormat === "EMAIL" && "이메일"} + {item.inputFormat === "PHONE" && "전화번호"} + {item.inputFormat === "NUMBER" && "숫자"} + {item.inputFormat === "FILE" && "파일"} + {item.inputFormat === "TEXT_FILE" && "텍스트+파일"} + </Badge> + )} + </p> + <div className="rounded-md border p-3 min-h-20"> + {(() => { + const inputFormat = item.inputFormat || "TEXT"; + + switch (inputFormat) { + case "EMAIL": + return ( + <div className="space-y-2"> + <div className="text-sm font-medium text-muted-foreground">이메일 주소:</div> + <div className="whitespace-pre-wrap"> + {item.answer || <span className="text-muted-foreground">답변 없음</span>} + </div> + </div> + ); + case "PHONE": + return ( + <div className="space-y-2"> + <div className="text-sm font-medium text-muted-foreground">전화번호:</div> + <div className="whitespace-pre-wrap"> + {item.answer || <span className="text-muted-foreground">답변 없음</span>} + </div> + </div> + ); + case "NUMBER": + return ( + <div className="space-y-2"> + <div className="text-sm font-medium text-muted-foreground">숫자 값:</div> + <div className="whitespace-pre-wrap"> + {item.answer || <span className="text-muted-foreground">답변 없음</span>} + </div> + </div> + ); + case "FILE": + return ( + <div className="space-y-2"> + <div className="text-sm font-medium text-muted-foreground">파일 업로드 항목:</div> + <div className="text-sm text-muted-foreground"> + {item.attachments.length > 0 ? "파일이 업로드되었습니다." : "파일이 업로드되지 않았습니다."} + </div> + </div> + ); + case "TEXT_FILE": + return ( + <div className="space-y-2"> + <div className="text-sm font-medium text-muted-foreground">텍스트 답변:</div> + <div className="whitespace-pre-wrap"> + {item.answer || <span className="text-muted-foreground">텍스트 답변 없음</span>} + </div> + <div className="text-sm font-medium text-muted-foreground">파일 업로드:</div> + <div className="text-sm text-muted-foreground"> + {item.attachments.length > 0 ? "파일이 업로드되었습니다." : "파일이 업로드되지 않았습니다."} + </div> + </div> + ); + default: // TEXT + return ( + <div className="whitespace-pre-wrap"> + {item.answer || <span className="text-muted-foreground">답변 없음</span>} + </div> + ); + } + })()} + </div> + </div> + {/* SHI 코멘트 필드 (편집 가능) */} + <div className="space-y-1"> + <p className="text-sm font-medium flex items-center gap-1"> + SHI 코멘트 </p> - <div className="rounded-md border p-3 min-h-20 whitespace-pre-wrap"> - {item.answer || <span className="text-muted-foreground">답변 없음</span>} + <div className="rounded-md border p-3 min-h-20"> + <Textarea + value={shiComments[item.answerId || 0] ?? item.shiComment ?? ""} + onChange={(e) => { + if (item.answerId) { + setShiComments(prev => ({ + ...prev, + [item.answerId!]: e.target.value + })) + } + }} + placeholder="SHI 코멘트를 입력하세요." + className="min-h-20" + /> + {item.answerId && ( + <div className="mt-2 flex justify-end"> + <Button + size="sm" + onClick={() => handleSHICommentUpdate(item.answerId!)} + disabled={isUpdatingComment === item.answerId} + > + {isUpdatingComment === item.answerId ? "저장 중..." : "저장"} + </Button> + </div> + )} </div> </div> + {/* 벤더 답변 필드 (읽기 전용) */} + <div className="space-y-1"> + <p className="text-sm font-medium flex items-center gap-1"> + 벤더 reply + </p> + <div className="rounded-md border p-3 min-h-20 bg-muted/30"> + <div className="whitespace-pre-wrap"> + {item.vendorReply || <span className="text-muted-foreground">벤더 reply 없음</span>} + </div> + </div> + </div> + - {/* 첨부 파일 */} - {item.attachments.length > 0 && ( + {/* 첨부 파일 - FILE 또는 TEXT_FILE 형식에서만 표시 */} + {(item.inputFormat === "FILE" || item.inputFormat === "TEXT_FILE") && item.attachments.length > 0 && ( <div className="space-y-1"> <p className="text-sm font-medium flex items-center gap-1"> <Paperclip className="h-4 w-4" /> @@ -226,15 +538,37 @@ export function PQReviewWrapper({ <ul className="space-y-1"> {item.attachments.map((attachment, idx) => ( <li key={idx} className="flex items-center gap-2"> - <FileText className="h-4 w-4 text-muted-foreground" /> - <a - href={attachment.filePath} - target="_blank" - rel="noopener noreferrer" - className="text-sm text-blue-600 hover:underline" + <button + onClick={async () => { + try { + const { downloadFile } = await import('@/lib/file-download') + await downloadFile(attachment.filePath, attachment.fileName, { + showToast: true, + onError: (error) => { + console.error('다운로드 오류:', error) + toast({ + title: "다운로드 실패", + description: error, + variant: "destructive" + }) + }, + onSuccess: (fileName, fileSize) => { + console.log(`다운로드 성공: ${fileName} (${fileSize} bytes)`) + } + }) + } catch (error) { + console.error('다운로드 오류:', error) + toast({ + title: "다운로드 실패", + description: "파일 다운로드 중 오류가 발생했습니다.", + variant: "destructive" + }) + } + }} + className="text-sm text-blue-600 hover:underline cursor-pointer" > {attachment.fileName} - </a> + </button> </li> ))} </ul> @@ -252,6 +586,14 @@ export function PQReviewWrapper({ {canReview && ( <div className="fixed bottom-4 right-4 bg-background p-4 rounded-lg shadow-md border"> <div className="flex gap-2"> + {/* <Button + variant="outline" + onClick={handleExportToExcel} + disabled={isExporting} + > + <Download className="h-4 w-4 mr-2" /> + {isExporting ? "내보내기 중..." : "Excel 내보내기"} + </Button> */} <Button variant="outline" onClick={() => setShowRejectDialog(true)} @@ -276,7 +618,11 @@ export function PQReviewWrapper({ <DialogHeader> <DialogTitle>PQ 승인 확인</DialogTitle> <DialogDescription> - {pqSubmission.vendorName}의 {pqSubmission.type === "GENERAL" ? "일반" : "프로젝트"} PQ를 승인하시겠습니까? + {pqSubmission.vendorName || "알 수 없는 업체"}의 { + pqSubmission.type === "GENERAL" ? "일반" : + pqSubmission.type === "PROJECT" ? "프로젝트" : + pqSubmission.type === "NON_INSPECTION" ? "미실사" : "일반" + } PQ를 승인하시겠습니까? {pqSubmission.projectId && ( <span> 프로젝트: {pqSubmission.projectName}</span> )} @@ -299,7 +645,11 @@ export function PQReviewWrapper({ <DialogHeader> <DialogTitle>PQ 거부</DialogTitle> <DialogDescription> - {pqSubmission.vendorName}의 {pqSubmission.type === "GENERAL" ? "일반" : "프로젝트"} PQ를 거부하는 이유를 입력해주세요. + {pqSubmission.vendorName || "알 수 없는 업체"}의 { + pqSubmission.type === "GENERAL" ? "일반" : + pqSubmission.type === "PROJECT" ? "프로젝트" : + pqSubmission.type === "NON_INSPECTION" ? "미실사" : "일반" + } PQ를 거부하는 이유를 입력해주세요. {pqSubmission.projectId && ( <span> 프로젝트: {pqSubmission.projectName}</span> )} |
