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-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 (
                     <Collapsible key={criteriaId} defaultOpen={!isSaved} className="w-full">
@@ -612,6 +684,7 @@ export function PQInputTabs({
                                 </CollapsibleTrigger>
                                 <CardTitle className="text-md">
                                   {code} - {checkPoint}
+
                                 </CardTitle>
                               </div>
                               {description && (
@@ -669,44 +742,159 @@ export function PQInputTabs({
                               </div>
                             )}
 
-                            {/* Answer Field */}
-                            <FormField
-                              control={form.control}
-                              name={`answers.${answerIndex}.answer`}
-                              render={({ field }) => (
-                                <FormItem className="mt-2">
-                                  <FormLabel>Answer</FormLabel>
-                                  <FormControl>
-                                    <Textarea
-                                      {...field}
-                                      disabled={shouldDisableInput}
-                                      className="min-h-24"
-                                      placeholder="Enter your answer here"
-                                      onChange={(e) => {
-                                        field.onChange(e)
-                                        form.setValue(
-                                          `answers.${answerIndex}.saved`,
-                                          false,
-                                          { shouldDirty: true }
-                                        )
-                                      }}
-                                    />
-                                  </FormControl>
-                                  <FormMessage />
-                                </FormItem>
-                              )}
-                            />
+                            {/* Answer Field - 입력 형식에 따라 다르게 렌더링 */}
+                            {item.inputFormat !== "FILE" && (
+                              <FormField
+                                control={form.control}
+                                name={`answers.${answerIndex}.answer`}
+                                render={({ field }) => (
+                                  <FormItem className="mt-2">
+                                    <FormLabel>
+                                      {(() => {
+                                        const inputFormat = item.inputFormat || "TEXT";
+                                        switch (inputFormat) {
+                                          case "EMAIL":
+                                            return "이메일 주소";
+                                          case "PHONE":
+                                            return "전화번호";
+                                          case "NUMBER":
+                                            return "숫자 값";
+                                          case "TEXT_FILE":
+                                            return "텍스트 답변";
+                                          default:
+                                            return "답변";
+                                        }
+                                      })()}
+                                    </FormLabel>
+                                    <FormControl>
+                                      {(() => {
+                                        const inputFormat = item.inputFormat || "TEXT";
+                                        
+                                        switch (inputFormat) {
+                                          case "EMAIL":
+                                            return (
+                                              <Input
+                                                {...field}
+                                                type="email"
+                                                disabled={shouldDisableInput}
+                                                placeholder="example@company.com"
+                                                onChange={(e) => {
+                                                  field.onChange(e)
+                                                  form.setValue(
+                                                    `answers.${answerIndex}.saved`,
+                                                    false,
+                                                    { shouldDirty: true }
+                                                  )
+                                                }}
+                                              />
+                                            );
+                                          case "PHONE":
+                                            return (
+                                              <Input
+                                                {...field}
+                                                type="tel"
+                                                disabled={shouldDisableInput}
+                                                placeholder="02-1234-5678"
+                                                onChange={(e) => {
+                                                  field.onChange(e)
+                                                  form.setValue(
+                                                    `answers.${answerIndex}.saved`,
+                                                    false,
+                                                    { shouldDirty: true }
+                                                  )
+                                                }}
+                                              />
+                                            );
+                                          case "NUMBER":
+                                            return (
+                                              <Input
+                                                {...field}
+                                                type="text"
+                                                disabled={shouldDisableInput}
+                                                placeholder="숫자를 입력하세요"
+                                                onChange={(e) => {
+                                                  // 숫자만 허용
+                                                  const value = e.target.value;
+                                                  if (value === '' || /^-?\d*\.?\d*$/.test(value)) {
+                                                    field.onChange(value)
+                                                    form.setValue(
+                                                      `answers.${answerIndex}.saved`,
+                                                      false,
+                                                      { shouldDirty: true }
+                                                    )
+                                                  }
+                                                }}
+                                              />
+                                            );
+                                          case "TEXT_FILE":
+                                            return (
+                                              <div className="space-y-2">
+                                                <Textarea
+                                                  {...field}
+                                                  disabled={shouldDisableInput}
+                                                  className="min-h-24"
+                                                  placeholder="텍스트 답변을 입력하세요"
+                                                  onChange={(e) => {
+                                                    field.onChange(e)
+                                                    form.setValue(
+                                                      `answers.${answerIndex}.saved`,
+                                                      false,
+                                                      { shouldDirty: true }
+                                                    )
+                                                  }}
+                                                />
+                                                <div className="text-sm text-muted-foreground">
+                                                  &quot;파일 업로드는 첨부 파일 섹션에서 진행해주세요.&quot;
+                                                </div>
+                                              </div>
+                                            );
+                                          default: // TEXT
+                                            return (
+                                              <Textarea
+                                                {...field}
+                                                disabled={shouldDisableInput}
+                                                className="min-h-24"
+                                                placeholder="답변을 입력해주세요."
+                                                onChange={(e) => {
+                                                  field.onChange(e)
+                                                  form.setValue(
+                                                    `answers.${answerIndex}.saved`,
+                                                    false,
+                                                    { shouldDirty: true }
+                                                  )
+                                                }}
+                                              />
+                                            );
+                                        }
+                                      })()}
+                                    </FormControl>
+                                    <FormMessage />
+                                  </FormItem>
+                                )}
+                              />
+                            )}
 
+                            {/* FILE 형식일 때 안내 메시지 */}
+                            {item.inputFormat === "FILE" && (
+                              <div className="mt-2">
+                                <FormLabel>파일 업로드</FormLabel>
+                                <div className="text-sm text-muted-foreground">
+                                  &quot;파일을 업로드해주세요.&quot;
+                                </div>
+                              </div>
+                            )}
 
-                            {/* Attachments / Dropzone */}
-                            <div className="grid gap-2 mt-3">
-                              <FormLabel>Attachments</FormLabel>
+                            {/* Attachments / Dropzone - FILE 또는 TEXT_FILE 형식에서만 활성화 */}
+                            {(item.inputFormat === "FILE" || item.inputFormat === "TEXT_FILE") && (
+                              <div className="grid gap-2 mt-3">
+                                <FormLabel>첨부 파일</FormLabel>
                               <Dropzone
                                 maxSize={6e8} // 600MB
                                 onDropAccepted={(files) =>
                                   handleDropAccepted(criteriaId, files)
                                 }
                                 onDropRejected={handleDropRejected}
+                                disabled={shouldDisableInput}
                               >
                                 {() => (
                                   <FormItem>
@@ -717,15 +905,15 @@ export function PQInputTabs({
                                       <div className="flex items-center gap-6">
                                         <DropzoneUploadIcon />
                                         <div className="grid gap-0.5">
-                                          <DropzoneTitle>Drop files here</DropzoneTitle>
+                                          <DropzoneTitle>파일을 드래그하거나 클릭하여 업로드</DropzoneTitle>
                                           <DropzoneDescription>
-                                            Max size: 600MB
+                                            PDF, Word, Excel, 이미지 파일 (최대 600MB)
                                           </DropzoneDescription>
                                         </div>
                                       </div>
                                     </DropzoneZone>
                                     <FormDescription>
-                                      Or click to browse files
+                                      또는 클릭하여 파일 선택
                                     </FormDescription>
                                     <FormMessage />
                                   </FormItem>
@@ -733,75 +921,163 @@ export function PQInputTabs({
                               </Dropzone>
                             </div>
 
-                            {/* Existing + Pending Files */}
-                            <div className="mt-4 space-y-4">
-                              {/* 1) Not-yet-uploaded files */}
-                              {newUploads.length > 0 && (
-                                <div className="grid gap-2">
-                                  <h6 className="text-sm font-medium">
-                                    Pending Files ({newUploads.length})
-                                  </h6>
-                                  <FileList>
-                                    {newUploads.map((f, fileIndex) => {
-                                      const fileObj = f.fileObj
-                                      if (!fileObj) return null
-
-                                      return (
-                                        <FileListItem key={fileIndex}>
-                                          <FileListHeader>
-                                            <FileListIcon />
-                                            <FileListInfo>
-                                              <FileListName>{fileObj.name}</FileListName>
-                                              <FileListDescription>
-                                                {prettyBytes(fileObj.size)}
-                                              </FileListDescription>
-                                            </FileListInfo>
-                                            <FileListAction
-                                              onClick={() =>
-                                                removeNewUpload(answerIndex, fileIndex)
+                            )}
+
+                            {/* Existing + Pending Files - FILE 또는 TEXT_FILE 형식에서만 활성화 */}
+                            {(item.inputFormat === "FILE" || item.inputFormat === "TEXT_FILE") && (
+                              <div className="mt-4 space-y-4">
+                                {/* 1) Not-yet-uploaded files */}
+                                {newUploads.length > 0 && (
+                                  <div className="grid gap-2">
+                                    <h6 className="text-sm font-medium">
+                                      업로드 대기 중인 파일 ({newUploads.length})
+                                    </h6>
+                                    <FileList>
+                                      {newUploads.map((f, fileIndex) => {
+                                        const fileObj = f.fileObj
+                                        if (!fileObj) return null
+
+                                        return (
+                                          <FileListItem key={fileIndex}>
+                                            <FileListHeader>
+                                              <FileListIcon />
+                                              <FileListInfo>
+                                                <FileListName>{fileObj.name}</FileListName>
+                                                <FileListDescription>
+                                                  {prettyBytes(fileObj.size)}
+                                                </FileListDescription>
+                                              </FileListInfo>
+                                              <FileListAction
+                                                onClick={() =>
+                                                  removeNewUpload(answerIndex, fileIndex)
+                                                }
+                                              >
+                                                <X className="h-4 w-4" />
+                                                <span className="sr-only">Remove</span>
+                                              </FileListAction>
+                                            </FileListHeader>
+                                          </FileListItem>
+                                        )
+                                      })}
+                                    </FileList>
+                                  </div>
+                                )}
+
+                                {/* 2) Already uploaded files */}
+                                {form
+                                  .watch(`answers.${answerIndex}.uploadedFiles`)
+                                  .map((file, fileIndex) => (
+                                    <FileListItem key={fileIndex}>
+                                      <FileListHeader>
+                                        <FileListIcon />
+                                        <FileListInfo>
+                                          <FileListName>{file.fileName}</FileListName>
+                                          {/* If you want to display the path: 
+                                              <FileListDescription>{file.url}</FileListDescription> 
+                                          */}
+                                        </FileListInfo>
+                                        {file.size && (
+                                          <span className="text-xs text-muted-foreground">
+                                            {prettyBytes(file.size)}
+                                          </span>
+                                        )}
+                                        <div className="flex gap-1">
+                                          <FileListAction
+                                            onClick={async () => {
+                                              try {
+                                                const { downloadFile } = await import('@/lib/file-download')
+                                                await downloadFile(file.url, file.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"
+                                                })
                                               }
-                                            >
-                                              <X className="h-4 w-4" />
-                                              <span className="sr-only">Remove</span>
-                                            </FileListAction>
-                                          </FileListHeader>
-                                        </FileListItem>
-                                      )
-                                    })}
-                                  </FileList>
-                                </div>
+                                            }}
+                                          >
+                                            <Download className="h-4 w-4" />
+                                            <span className="sr-only">Download</span>
+                                          </FileListAction>
+                                          <FileListAction
+                                            onClick={() =>
+                                              removeUploadedFile(answerIndex, fileIndex)
+                                            }
+                                          >
+                                            <X className="h-4 w-4" />
+                                            <span className="sr-only">Remove</span>
+                                          </FileListAction>
+                                        </div>
+                                      </FileListHeader>
+                                    </FileListItem>
+                                  ))}
+                              </div>
+                            )}
+
+                            {/* SHI 코멘트 필드 (읽기 전용) */}
+                            {item.shiComment && (
+                              <FormField
+                                control={form.control}
+                                name={`answers.${answerIndex}.shiComment`}
+                                render={({ field }) => (
+                                  <FormItem className="mt-2">
+                                    <FormLabel className="text-amber-600">SHI 코멘트</FormLabel>
+                                    <FormControl>
+                                      <Textarea
+                                        {...field}
+                                        disabled={true}
+                                        className="min-h-20 bg-muted/50"
+                                        placeholder="SHI 코멘트가 없습니다."
+                                      />
+                                    </FormControl>
+                                    <FormMessage />
+                                  </FormItem>
+                                )}
+                              />
+                            )}
+
+                            {/* Vendor Reply 필드 */}
+                            <FormField
+                              control={form.control}
+                              name={`answers.${answerIndex}.vendorReply`}
+                              render={({ field }) => (
+                                <FormItem className="mt-2">
+                                  <FormLabel className="text-blue-600">벤더 Reply</FormLabel>
+                                  <FormControl>
+                                    <Textarea
+                                      {...field}
+                                      disabled={shouldDisableInput}
+                                      className="min-h-20 bg-muted/50"
+                                      placeholder="벤더 Reply를 입력하세요."
+                                      onChange={(e) => {
+                                        field.onChange(e)
+                                        form.setValue(
+                                          `answers.${answerIndex}.saved`,
+                                          false,
+                                          { shouldDirty: true }
+                                        )
+                                      }}
+                                    />
+                                  </FormControl>
+                                  <FormMessage />
+                                </FormItem>
                               )}
+                            />
 
-                              {/* 2) Already uploaded files */}
-                              {form
-                                .watch(`answers.${answerIndex}.uploadedFiles`)
-                                .map((file, fileIndex) => (
-                                  <FileListItem key={fileIndex}>
-                                    <FileListHeader>
-                                      <FileListIcon />
-                                      <FileListInfo>
-                                        <FileListName>{file.fileName}</FileListName>
-                                        {/* If you want to display the path: 
-                                            <FileListDescription>{file.url}</FileListDescription> 
-                                        */}
-                                      </FileListInfo>
-                                      {file.size && (
-                                        <span className="text-xs text-muted-foreground">
-                                          {prettyBytes(file.size)}
-                                        </span>
-                                      )}
-                                      <FileListAction
-                                        onClick={() =>
-                                          removeUploadedFile(answerIndex, fileIndex)
-                                        }
-                                      >
-                                        <X className="h-4 w-4" />
-                                        <span className="sr-only">Remove</span>
-                                      </FileListAction>
-                                    </FileListHeader>
-                                  </FileListItem>
-                                ))}
-                            </div>
                           </CardContent>
                         </CollapsibleContent>
                       </Card>
-- 
cgit v1.2.3

