diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-10-03 04:48:47 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-10-03 04:48:47 +0000 |
| commit | defda07c0bb4b0bd444ca8dc4fd3f89322bda0ce (patch) | |
| tree | d7f257781f107d7ec2fd4ef76cb4f840f5065a06 /components/pq-input/pq-input-tabs.tsx | |
| parent | 00743c8b4190fac9117c2d9c08981bbfdce576de (diff) | |
(대표님) edp, tbe, dolce 등
Diffstat (limited to 'components/pq-input/pq-input-tabs.tsx')
| -rw-r--r-- | components/pq-input/pq-input-tabs.tsx | 278 |
1 files changed, 197 insertions, 81 deletions
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> |
