summaryrefslogtreecommitdiff
path: root/components/pq-input/pq-review-wrapper.tsx
diff options
context:
space:
mode:
Diffstat (limited to 'components/pq-input/pq-review-wrapper.tsx')
-rw-r--r--components/pq-input/pq-review-wrapper.tsx392
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>
)}