From 53ad72732f781e6c6d5ddb3776ea47aec010af8e Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Mon, 4 Aug 2025 09:39:21 +0000
Subject: (최겸) PQ/실사 수정 및 개발
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 components/pq-input/pq-review-wrapper.tsx | 392 ++++++++++++++++++++++++++++--
 1 file changed, 371 insertions(+), 21 deletions(-)

(limited to 'components/pq-input/pq-review-wrapper.tsx')

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>
               )}
-- 
cgit v1.2.3

