From 53ad72732f781e6c6d5ddb3776ea47aec010af8e Mon Sep 17 00:00:00 2001 From: dujinkim 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-input-tabs.tsx | 524 ++++++++++++++++++++++++++-------- 1 file changed, 400 insertions(+), 124 deletions(-) (limited to 'components/pq-input/pq-input-tabs.tsx') diff --git a/components/pq-input/pq-input-tabs.tsx b/components/pq-input/pq-input-tabs.tsx index 2574a5b0..1bc2fc38 100644 --- a/components/pq-input/pq-input-tabs.tsx +++ b/components/pq-input/pq-input-tabs.tsx @@ -13,8 +13,9 @@ import { CardContent, } from "@/components/ui/card" import { Button } from "@/components/ui/button" +import { Input } from "@/components/ui/input" import { Textarea } from "@/components/ui/textarea" -import { X, Save, CheckCircle2, AlertTriangle, ChevronsUpDown } from "lucide-react" +import { X, Save, CheckCircle2, AlertTriangle, ChevronsUpDown, Download } from "lucide-react" import prettyBytes from "pretty-bytes" import { useToast } from "@/hooks/use-toast" import { @@ -65,12 +66,12 @@ import { } from "@/components/ui/dialog" // Additional UI -import { Separator } from "@/components/ui/separator" + import { Badge } from "@/components/ui/badge" // Server actions import { - uploadFileAction, + uploadVendorFileAction, savePQAnswersAction, submitPQAction, ProjectPQ, @@ -80,12 +81,6 @@ import { PQGroupData } from "@/lib/pq/service" // ---------------------------------------------------------------------- // 1) Define client-side file shapes // ---------------------------------------------------------------------- -interface UploadedFileState { - fileName: string - url: string - size?: number -} - interface LocalFileState { fileObj: File uploaded: boolean @@ -101,6 +96,10 @@ const pqFormSchema = z.object({ // Must have at least 1 char answer: z.string().min(1, "Answer is required"), + // SHI 코멘트와 벤더 답변 필드 추가 + shiComment: z.string().optional(), + vendorReply: z.string().optional(), + // Existing, uploaded files uploadedFiles: z .array( @@ -179,6 +178,8 @@ export function PQInputTabs({ answers.push({ criteriaId: item.criteriaId, answer: item.answer || "", + shiComment: item.shiComment || "", + vendorReply: item.vendorReply || "", uploadedFiles: item.attachments.map((attach) => ({ fileName: attach.fileName, url: attach.filePath, @@ -271,15 +272,90 @@ export function PQInputTabs({ const handleSaveItem = async (answerIndex: number) => { try { const answerData = form.getValues(`answers.${answerIndex}`) - + const criteriaId = answerData.criteriaId + const item = data.flatMap(group => group.items).find(item => item.criteriaId === criteriaId) + const inputFormat = item?.inputFormat || "TEXT" // Validation - if (!answerData.answer) { - toast({ - title: "Validation Error", - description: "Answer is required", - variant: "destructive", - }) - return + // 모든 항목은 필수로 처리 (isRequired 제거됨) + { + if (inputFormat === "FILE") { + // 파일 업로드 항목의 경우 첨부 파일이 있어야 함 + const hasFiles = answerData.uploadedFiles.length > 0 || answerData.newUploads.length > 0 + if (!hasFiles) { + toast({ + title: "필수 항목", + description: "필수 항목입니다. 파일을 업로드해주세요.", + variant: "destructive", + }) + return + } + } else if (inputFormat === "TEXT_FILE") { + // 텍스트+파일 항목의 경우 텍스트 답변과 파일이 모두 있어야 함 + const hasFiles = answerData.uploadedFiles.length > 0 || answerData.newUploads.length > 0 + if (!answerData.answer || !hasFiles) { + toast({ + title: "필수 항목", + description: "필수 항목입니다. 텍스트 답변과 파일을 모두 입력해주세요.", + variant: "destructive", + }) + return + } + } else if (!answerData.answer) { + // 일반 텍스트 입력 항목의 경우 답변이 있어야 함 + toast({ + title: "필수 항목", + description: "필수 항목입니다. 답변을 입력해주세요.", + variant: "destructive", + }) + return + } + } + + // 입력 형식별 유효성 검사 + if (answerData.answer) { + switch (inputFormat) { + case "EMAIL": + const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/ + if (!emailRegex.test(answerData.answer)) { + toast({ + title: "이메일 형식 오류", + description: "올바른 이메일 형식을 입력해주세요. (예: example@company.com)", + variant: "destructive", + }) + return + } + break + case "PHONE": + const phoneRegex = /^[\d-]+$/ + if (!phoneRegex.test(answerData.answer)) { + toast({ + title: "전화번호 형식 오류", + description: "올바른 전화번호 형식을 입력해주세요. (예: 02-1234-5678)", + variant: "destructive", + }) + return + } + break + case "NUMBER": + const numberRegex = /^-?\d*\.?\d*$/ + if (!numberRegex.test(answerData.answer)) { + toast({ + title: "숫자 형식 오류", + description: "숫자만 입력해주세요. (소수점, 음수 허용)", + variant: "destructive", + }) + return + } + break + case "TEXT": + case "TEXT_FILE": + case "FILE": + // 텍스트 입력과 파일 업로드는 추가 검증 없음 + break + default: + // 알 수 없는 입력 형식 + break + } } // Upload new files (if any) @@ -288,7 +364,7 @@ export function PQInputTabs({ for (const localFile of answerData.newUploads) { try { - const uploadResult = await uploadFileAction(localFile.fileObj) + const uploadResult = await uploadVendorFileAction(localFile.fileObj) const currentUploaded = form.getValues(`answers.${answerIndex}.uploadedFiles`) currentUploaded.push({ fileName: uploadResult.fileName, @@ -321,6 +397,8 @@ export function PQInputTabs({ { criteriaId: updatedAnswer.criteriaId, answer: updatedAnswer.answer, + shiComment: updatedAnswer.shiComment, + vendorReply: updatedAnswer.vendorReply, attachments: updatedAnswer.uploadedFiles.map((f) => ({ fileName: f.fileName, url: f.url, @@ -583,19 +661,13 @@ export function PQInputTabs({ if (answerIndex === -1) return null const isSaved = form.watch(`answers.${answerIndex}.saved`) - const hasAnswer = form.watch(`answers.${answerIndex}.answer`) const newUploads = form.watch(`answers.${answerIndex}.newUploads`) const dirtyFieldsItem = form.formState.dirtyFields.answers?.[answerIndex] const isItemDirty = !!dirtyFieldsItem const hasNewUploads = newUploads.length > 0 const canSave = isItemDirty || hasNewUploads - - // For "Not Saved" vs. "Saved" status label - const hasUploads = - form.watch(`answers.${answerIndex}.uploadedFiles`).length > 0 || - newUploads.length > 0 - const isValid = !!hasAnswer || hasUploads + return ( @@ -612,6 +684,7 @@ export function PQInputTabs({ {code} - {checkPoint} + {description && ( @@ -669,44 +742,159 @@ export function PQInputTabs({ )} - {/* Answer Field */} - ( - - Answer - -