From defda07c0bb4b0bd444ca8dc4fd3f89322bda0ce Mon Sep 17 00:00:00 2001
From: dujinkim <dujin.kim@dtsolution.co.kr>
Date: Fri, 3 Oct 2025 04:48:47 +0000
Subject: (대표님) edp, tbe, dolce 등
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

---
 components/pq-input/pq-input-tabs.tsx | 278 ++++++++++++++++++++++++----------
 1 file changed, 197 insertions(+), 81 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 a37a52db..3f7e1718 100644
--- a/components/pq-input/pq-input-tabs.tsx
+++ b/components/pq-input/pq-input-tabs.tsx
@@ -15,7 +15,7 @@ import {
 import { Button } from "@/components/ui/button"
 import { Input } from "@/components/ui/input"
 import { Textarea } from "@/components/ui/textarea"
-import { X, Save, CheckCircle2, AlertTriangle, ChevronsUpDown, Download } from "lucide-react"
+import { X, Save, CheckCircle2, AlertTriangle, ChevronsUpDown, Download, Loader2 } from "lucide-react"
 import prettyBytes from "pretty-bytes"
 import { useToast } from "@/hooks/use-toast"
 import {
@@ -68,6 +68,7 @@ import {
 // Additional UI
 
 import { Badge } from "@/components/ui/badge"
+import { Checkbox } from "@/components/ui/checkbox"
 
 // Server actions
 import {
@@ -156,6 +157,14 @@ export function PQInputTabs({
   const [allSaved, setAllSaved] = React.useState(false)
   const [showConfirmDialog, setShowConfirmDialog] = React.useState(false)
 
+  // 필터 상태 관리
+  const [filterOptions, setFilterOptions] = React.useState({
+    showAll: true,
+    showSaved: true,
+    showNotSaved: true,
+  })
+
+
   const { toast } = useToast()
 
   const shouldDisableInput = isReadOnly;
@@ -166,10 +175,10 @@ export function PQInputTabs({
       const parseCode = (code: string) => {
         return code.split('-').map(part => parseInt(part, 10))
       }
-      
+
       const aCode = parseCode(a.code)
       const bCode = parseCode(b.code)
-      
+
       for (let i = 0; i < Math.max(aCode.length, bCode.length); i++) {
         const aPart = aCode[i] || 0
         const bPart = bCode[i] || 0
@@ -181,6 +190,14 @@ export function PQInputTabs({
     })
   }
 
+  // 필터링 함수
+  const shouldShowItem = (isSaved: boolean) => {
+    if (filterOptions.showAll) return true;
+    if (isSaved && filterOptions.showSaved) return true;
+    if (!isSaved && filterOptions.showNotSaved) return true;
+    return false;
+  }
+
   // ----------------------------------------------------------------------
   // A) Create initial form values
   //     Mark items as "saved" if they have existing answer or attachments
@@ -219,6 +236,7 @@ export function PQInputTabs({
     return { answers }
   }
 
+
   // ----------------------------------------------------------------------
   // B) Set up react-hook-form
   // ----------------------------------------------------------------------
@@ -339,7 +357,7 @@ export function PQInputTabs({
       if (answerData.answer) {
         switch (inputFormat) {
           case "EMAIL":
-            const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/
+            const emailRegex = /^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$/
             if (!emailRegex.test(answerData.answer)) {
               toast({
                 title: "이메일 형식 오류",
@@ -350,22 +368,24 @@ export function PQInputTabs({
             }
             break
           case "PHONE":
-            const phoneRegex = /^[\d-]+$/
+          case "FAX":
+            // 전화번호/팩스번호는 숫자만 허용
+            const phoneRegex = /^\d+$/
             if (!phoneRegex.test(answerData.answer)) {
               toast({
-                title: "전화번호 형식 오류",
-                description: "올바른 전화번호 형식을 입력해주세요. (예: 02-1234-5678)",
+                title: `${inputFormat === "PHONE" ? "전화번호" : "팩스번호"} 형식 오류`,
+                description: `숫자만 입력해주세요.`,
                 variant: "destructive",
               })
               return
             }
             break
           case "NUMBER":
-            const numberRegex = /^-?\d*\.?\d*$/
+            const numberRegex = /^-?\d+(\.\d+)?$/
             if (!numberRegex.test(answerData.answer)) {
               toast({
                 title: "숫자 형식 오류",
-                description: "숫자만 입력해주세요. (소수점, 음수 허용)",
+                description: "올바른 숫자 형식을 입력해주세요. (예: 123, -123, 123.45)",
                 variant: "destructive",
               })
               return
@@ -389,7 +409,7 @@ export function PQInputTabs({
         for (const localFile of answerData.newUploads) {
           try {
             const uploadResult = await uploadVendorFileAction(localFile.fileObj)
-            const currentUploaded = form.getValues(`answers.${answerIndex}.uploadedFiles`)
+            const currentUploaded = [...form.getValues(`answers.${answerIndex}.uploadedFiles`)]
             currentUploaded.push({
               fileName: uploadResult.fileName,
               url: uploadResult.url,
@@ -435,10 +455,7 @@ export function PQInputTabs({
       if (saveResult.ok) {
         // Mark as saved
         form.setValue(`answers.${answerIndex}.saved`, true, { shouldDirty: false })
-        toast({
-          title: "Saved",
-          description: "Item saved successfully",
-        })
+        // Individual save toast removed - only show toast in handleSaveAll
       }
     } catch (error) {
       console.error("Save error:", error)
@@ -470,6 +487,7 @@ export function PQInputTabs({
     try {
       setIsSaving(true)
       const answers = form.getValues().answers
+      let savedCount = 0
 
       // Only save items that are dirty or have new uploads
       for (let i = 0; i < answers.length; i++) {
@@ -478,17 +496,26 @@ export function PQInputTabs({
         if (!itemDirty && !hasNewUploads) continue
 
         await handleSaveItem(i)
+        savedCount++
       }
 
-      toast({
-        title: "All Saved",
-        description: "All items saved successfully",
-      })
+      // 저장된 항목이 있을 때만 토스트 메시지 표시
+      if (savedCount > 0) {
+        toast({
+          title: "임시 저장 완료",
+          description: `항목이 저장되었습니다.`,
+        })
+      } else {
+        toast({
+          title: "저장할 항목 없음",
+          description: "변경된 항목이 없습니다.",
+        })
+      }
     } catch (error) {
       console.error("Save all error:", error)
       toast({
-        title: "Save Error",
-        description: "Failed to save all items",
+        title: "저장 실패",
+        description: "일괄 저장 중 오류가 발생했습니다.",
         variant: "destructive",
       })
     } finally {
@@ -614,53 +641,125 @@ export function PQInputTabs({
         {renderProjectInfo()}
 
         <Tabs defaultValue={data[0]?.groupName || ""} className="w-full">
-          {/* Top Controls */}
-          <div className="flex justify-between items-center mb-4">
-            <TabsList className="grid grid-cols-4">
-              {data.map((group) => (
-                <TabsTrigger
-                  key={group.groupName}
-                  value={group.groupName}
-                  className="truncate"
+          {/* Top Controls - Sticky Header */}
+          <div className="sticky top-0 z-10 bg-background border-b border-border mb-4 pb-4">
+            {/* Filter Controls */}
+            <div className="mb-3 flex items-center gap-4">
+              <span className="text-sm font-medium">필터:</span>
+              <div className="flex items-center gap-4">
+                <div className="flex items-center space-x-2">
+                  <Checkbox
+                    id="showAll"
+                    checked={filterOptions.showAll}
+                    onCheckedChange={(checked) => {
+                      const newOptions = { ...filterOptions, showAll: !!checked };
+                      if (!checked && !filterOptions.showSaved && !filterOptions.showNotSaved) {
+                        // 최소 하나는 체크되어 있어야 함
+                        newOptions.showSaved = true;
+                      }
+                      setFilterOptions(newOptions);
+                    }}
+                  />
+                  <label htmlFor="showAll" className="text-sm">전체 항목</label>
+                </div>
+                <div className="flex items-center space-x-2">
+                  <Checkbox
+                    id="showSaved"
+                    checked={filterOptions.showSaved}
+                    onCheckedChange={(checked) => {
+                      const newOptions = { ...filterOptions, showSaved: !!checked };
+                      if (!checked && !filterOptions.showAll && !filterOptions.showNotSaved) {
+                        // 최소 하나는 체크되어 있어야 함
+                        newOptions.showAll = true;
+                      }
+                      setFilterOptions(newOptions);
+                    }}
+                  />
+                  <label htmlFor="showSaved" className="text-sm text-green-600">Save 항목</label>
+                </div>
+                <div className="flex items-center space-x-2">
+                  <Checkbox
+                    id="showNotSaved"
+                    checked={filterOptions.showNotSaved}
+                    onCheckedChange={(checked) => {
+                      const newOptions = { ...filterOptions, showNotSaved: !!checked };
+                      if (!checked && !filterOptions.showAll && !filterOptions.showSaved) {
+                        // 최소 하나는 체크되어 있어야 함
+                        newOptions.showAll = true;
+                      }
+                      setFilterOptions(newOptions);
+                    }}
+                  />
+                  <label htmlFor="showNotSaved" className="text-sm text-amber-600">Not Save 항목</label>
+                </div>
+              </div>
+            </div>
+
+            <div className="flex justify-between items-center">
+              <TabsList className="grid grid-cols-4">
+                {data.map((group) => (
+                  <TabsTrigger
+                    key={group.groupName}
+                    value={group.groupName}
+                    className="truncate"
+                  >
+                    <div className="flex items-center gap-2">
+                      {/* Mobile: truncated version */}
+                      <span className="block sm:hidden">
+                        {group.groupName.length > 5
+                          ? group.groupName.slice(0, 5) + "..."
+                          : group.groupName}
+                      </span>
+                      {/* Desktop: full text */}
+                      <span className="hidden sm:block">{group.groupName}</span>
+                      <span className="inline-flex items-center justify-center h-5 min-w-5 px-1 rounded-full bg-muted text-xs font-medium">
+                        {group.items.length}
+                      </span>
+                    </div>
+                  </TabsTrigger>
+                ))}
+              </TabsList>
+
+              <div className="flex gap-2">
+                {/* Save All button */}
+                <Button
+                  type="button"
+                  variant="outline"
+                  disabled={isSaving || !isAnyItemDirty || shouldDisableInput}
+                  onClick={handleSaveAll}
                 >
-                  <div className="flex items-center gap-2">
-                    {/* Mobile: truncated version */}
-                    <span className="block sm:hidden">
-                      {group.groupName.length > 5
-                        ? group.groupName.slice(0, 5) + "..."
-                        : group.groupName}
-                    </span>
-                    {/* Desktop: full text */}
-                    <span className="hidden sm:block">{group.groupName}</span>
-                    <span className="inline-flex items-center justify-center h-5 min-w-5 px-1 rounded-full bg-muted text-xs font-medium">
-                      {group.items.length}
-                    </span>
-                  </div>
-                </TabsTrigger>
-              ))}
-            </TabsList>
-
-            <div className="flex gap-2">
-              {/* Save All button */}
-              <Button
-                type="button"
-                variant="outline"
-                disabled={isSaving || !isAnyItemDirty || shouldDisableInput}
-                onClick={handleSaveAll}
-              >
-                {isSaving ? "Saving..." : "임시 저장"}
-                <Save className="ml-2 h-4 w-4" />
-              </Button>
-
-              {/* Submit PQ button */}
-              <Button
-                type="button"
-                disabled={!allSaved || isSubmitting || shouldDisableInput}
-                onClick={handleSubmitPQ}
-              >
-                {isSubmitting ? "Submitting..." : "최종 제출"}
-                <CheckCircle2 className="ml-2 h-4 w-4" />
-              </Button>
+                  {isSaving ? (
+                    <>
+                      <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+                      저장 중...
+                    </>
+                  ) : (
+                    <>
+                      <Save className="mr-2 h-4 w-4" />
+                      임시 저장
+                    </>
+                  )}
+                </Button>
+
+                {/* Submit PQ button */}
+                <Button
+                  type="button"
+                  disabled={!allSaved || isSubmitting || shouldDisableInput}
+                  onClick={handleSubmitPQ}
+                >
+                  {isSubmitting ? (
+                    <>
+                      <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+                      제출 중...
+                    </>
+                  ) : (
+                    <>
+                      <CheckCircle2 className="mr-2 h-4 w-4" />
+                      최종 제출
+                    </>
+                  )}
+                </Button>
+              </div>
             </div>
           </div>
 
@@ -681,7 +780,12 @@ export function PQInputTabs({
                   const isItemDirty = !!dirtyFieldsItem
                   const hasNewUploads = newUploads.length > 0
                   const canSave = isItemDirty || hasNewUploads
-            
+
+                  // 면제된 항목은 입력 비활성화
+                  const isDisabled = shouldDisableInput
+
+                  // 필터링 적용
+                  if (!shouldShowItem(isSaved)) return null
 
                   return (
                     <Collapsible key={criteriaId} defaultOpen={isReadOnly || !isSaved} className="w-full">
@@ -698,7 +802,6 @@ export function PQInputTabs({
                                 </CollapsibleTrigger>
                                 <CardTitle className="text-md">
                                   {code} - {checkPoint}
-
                                 </CardTitle>
                               </div>
                               {description && (
@@ -731,14 +834,16 @@ export function PQInputTabs({
                                 </span>
                               )}
 
+                              {/* 개별 저장 버튼 주석처리
                               <Button
                                 size="sm"
                                 variant="outline"
-                                disabled={isSaving || !canSave}
+                                disabled={isSaving || !canSave || isDisabled}
                                 onClick={() => handleSaveItem(answerIndex)}
                               >
                                 Save
                               </Button>
+                              */}
                             </div>
                           </div>
                         </CardHeader>
@@ -798,7 +903,7 @@ export function PQInputTabs({
                                               <Input
                                                 {...field}
                                                 type="email"
-                                                disabled={shouldDisableInput}
+                                                disabled={isDisabled}
                                                 placeholder="example@company.com"
                                                 onChange={(e) => {
                                                   field.onChange(e)
@@ -811,14 +916,18 @@ export function PQInputTabs({
                                               />
                                             );
                                           case "PHONE":
+                                          case "FAX":
                                             return (
                                               <Input
                                                 {...field}
                                                 type="tel"
-                                                disabled={shouldDisableInput}
+                                                disabled={isDisabled}
                                                 placeholder="02-1234-5678"
                                                 onChange={(e) => {
-                                                  field.onChange(e)
+                                                  // 전화번호 형식만 허용 (숫자, -, +, 공백)
+                                                  const value = e.target.value;
+                                                  const filteredValue = value.replace(/[^\d\-\+\s]/g, '');
+                                                  field.onChange(filteredValue);
                                                   form.setValue(
                                                     `answers.${answerIndex}.saved`,
                                                     false,
@@ -832,7 +941,7 @@ export function PQInputTabs({
                                               <Input
                                                 {...field}
                                                 type="text"
-                                                disabled={shouldDisableInput}
+                                                disabled={isDisabled}
                                                 placeholder="숫자를 입력하세요"
                                                 onChange={(e) => {
                                                   // 숫자만 허용
@@ -853,7 +962,7 @@ export function PQInputTabs({
                                               <div className="space-y-2">
                                                 <Textarea
                                                   {...field}
-                                                  disabled={shouldDisableInput}
+                                                  disabled={isDisabled}
                                                   className="min-h-24"
                                                   placeholder="텍스트 답변을 입력하세요"
                                                   onChange={(e) => {
@@ -874,7 +983,7 @@ export function PQInputTabs({
                                             return (
                                               <Textarea
                                                 {...field}
-                                                disabled={shouldDisableInput}
+                                                disabled={isDisabled}
                                                 className="min-h-24"
                                                 placeholder="답변을 입력해주세요."
                                                 onChange={(e) => {
@@ -916,7 +1025,7 @@ export function PQInputTabs({
                                   handleDropAccepted(criteriaId, files)
                                 }
                                 onDropRejected={handleDropRejected}
-                                disabled={shouldDisableInput}
+                                disabled={isDisabled}
                               >
                                 {() => (
                                   <FormItem>
@@ -1050,8 +1159,8 @@ export function PQInputTabs({
                               </div>
                             )}
 
-                            {/* SHI 코멘트 필드 (읽기 전용) */}
-                            {item.shiComment && (
+                            {/* SHI 코멘트 필드 (읽기 전용) - 승인 상태에서는 거부사유 숨김 */}
+                            {item.shiComment && currentPQ?.status !== "APPROVED" && (
                               <FormField
                                 control={form.control}
                                 name={`answers.${answerIndex}.shiComment`}
@@ -1082,7 +1191,7 @@ export function PQInputTabs({
                                   <FormControl>
                                     <Textarea
                                       {...field}
-                                      disabled={shouldDisableInput}
+                                      disabled={isDisabled}
                                       className="min-h-20 bg-muted/50"
                                       placeholder="벤더 Reply를 입력하세요."
                                       onChange={(e) => {
@@ -1180,10 +1289,17 @@ export function PQInputTabs({
               onClick={() => setShowConfirmDialog(false)}
               disabled={isSubmitting}
             >
-              Cancel
+              취소
             </Button>
             <Button onClick={handleConfirmSubmission} disabled={isSubmitting}>
-              {isSubmitting ? "Submitting..." : "Confirm Submit"}
+              {isSubmitting ? (
+                <>
+                  <Loader2 className="mr-2 h-4 w-4 animate-spin" />
+                  제출 중...
+                </>
+              ) : (
+                "제출 확인"
+              )}
             </Button>
           </DialogFooter>
         </DialogContent>
-- 
cgit v1.2.3

