From aa86729f9a2ab95346a2851e3837de1c367aae17 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Fri, 20 Jun 2025 11:37:31 +0000 Subject: (대표님) 20250620 작업사항 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/evaluation/table/evaluation-filter-sheet.tsx | 1031 ++++++++++++++++++++++ 1 file changed, 1031 insertions(+) create mode 100644 lib/evaluation/table/evaluation-filter-sheet.tsx (limited to 'lib/evaluation/table/evaluation-filter-sheet.tsx') diff --git a/lib/evaluation/table/evaluation-filter-sheet.tsx b/lib/evaluation/table/evaluation-filter-sheet.tsx new file mode 100644 index 00000000..7cda4989 --- /dev/null +++ b/lib/evaluation/table/evaluation-filter-sheet.tsx @@ -0,0 +1,1031 @@ +// ================================================================ +// 2. PERIODIC EVALUATIONS FILTER SHEET +// ================================================================ + +"use client" + +import { useEffect, useTransition, useState, useRef } from "react" +import { useRouter, useParams } from "next/navigation" +import { z } from "zod" +import { useForm } from "react-hook-form" +import { zodResolver } from "@hookform/resolvers/zod" +import { Search, X } from "lucide-react" +import { customAlphabet } from "nanoid" +import { parseAsStringEnum, useQueryState } from "nuqs" + +import { Button } from "@/components/ui/button" +import { + Form, + FormControl, + FormField, + FormItem, + FormLabel, + FormMessage, +} from "@/components/ui/form" +import { Input } from "@/components/ui/input" +import { Badge } from "@/components/ui/badge" +import { + Select, + SelectContent, + SelectItem, + SelectTrigger, + SelectValue, +} from "@/components/ui/select" +import { cn } from "@/lib/utils" +import { getFiltersStateParser } from "@/lib/parsers" + +// nanoid 생성기 +const generateId = customAlphabet("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 6) + +// 정기평가 필터 스키마 정의 +const periodicEvaluationFilterSchema = z.object({ + evaluationYear: z.string().optional(), + evaluationPeriod: z.string().optional(), + division: z.string().optional(), + status: z.string().optional(), + domesticForeign: z.string().optional(), + materialType: z.string().optional(), + vendorCode: z.string().optional(), + vendorName: z.string().optional(), + documentsSubmitted: z.string().optional(), + evaluationGrade: z.string().optional(), + finalGrade: z.string().optional(), + minTotalScore: z.string().optional(), + maxTotalScore: z.string().optional(), +}) + +// 옵션 정의 +const evaluationPeriodOptions = [ + { value: "상반기", label: "상반기" }, + { value: "하반기", label: "하반기" }, + { value: "연간", label: "연간" }, +] + +const divisionOptions = [ + { value: "PLANT", label: "해양" }, + { value: "SHIP", label: "조선" }, +] + +const statusOptions = [ + { value: "PENDING_SUBMISSION", label: "제출대기" }, + { value: "SUBMITTED", label: "제출완료" }, + { value: "IN_REVIEW", label: "검토중" }, + { value: "REVIEW_COMPLETED", label: "검토완료" }, + { value: "FINALIZED", label: "최종확정" }, +] + +const domesticForeignOptions = [ + { value: "DOMESTIC", label: "내자" }, + { value: "FOREIGN", label: "외자" }, +] + +const materialTypeOptions = [ + { value: "EQUIPMENT", label: "기자재" }, + { value: "BULK", label: "벌크" }, + { value: "EQUIPMENT_BULK", label: "기자재/벌크" }, +] + +const documentsSubmittedOptions = [ + { value: "true", label: "제출완료" }, + { value: "false", label: "미제출" }, +] + +const gradeOptions = [ + { value: "S", label: "S등급" }, + { value: "A", label: "A등급" }, + { value: "B", label: "B등급" }, + { value: "C", label: "C등급" }, + { value: "D", label: "D등급" }, +] + +type PeriodicEvaluationFilterFormValues = z.infer + +interface PeriodicEvaluationFilterSheetProps { + isOpen: boolean; + onClose: () => void; + onSearch?: () => void; + isLoading?: boolean; +} + +export function PeriodicEvaluationFilterSheet({ + isOpen, + onClose, + onSearch, + isLoading = false +}: PeriodicEvaluationFilterSheetProps) { + const router = useRouter() + const params = useParams(); + + const [isPending, startTransition] = useTransition() + const [isInitializing, setIsInitializing] = useState(false) + const lastAppliedFilters = useRef("") + + // nuqs로 URL 상태 관리 + const [filters, setFilters] = useQueryState( + "basicFilters", + getFiltersStateParser().withDefault([]) + ) + + const [joinOperator, setJoinOperator] = useQueryState( + "basicJoinOperator", + parseAsStringEnum(["and", "or"]).withDefault("and") + ) + + // 폼 상태 초기화 + const form = useForm({ + resolver: zodResolver(periodicEvaluationFilterSchema), + defaultValues: { + evaluationYear: new Date().getFullYear().toString(), + evaluationPeriod: "", + division: "", + status: "", + domesticForeign: "", + materialType: "", + vendorCode: "", + vendorName: "", + documentsSubmitted: "", + evaluationGrade: "", + finalGrade: "", + minTotalScore: "", + maxTotalScore: "", + }, + }) + + // URL 필터에서 초기 폼 상태 설정 + useEffect(() => { + const currentFiltersString = JSON.stringify(filters); + + if (isOpen && filters && filters.length > 0 && currentFiltersString !== lastAppliedFilters.current) { + setIsInitializing(true); + + const formValues = { ...form.getValues() }; + let formUpdated = false; + + filters.forEach(filter => { + if (filter.id in formValues) { + // @ts-ignore - 동적 필드 접근 + formValues[filter.id] = filter.value; + formUpdated = true; + } + }); + + if (formUpdated) { + form.reset(formValues); + lastAppliedFilters.current = currentFiltersString; + } + + setIsInitializing(false); + } + }, [filters, isOpen]) + + // 현재 적용된 필터 카운트 + const getActiveFilterCount = () => { + return filters?.length || 0 + } + + // 폼 제출 핸들러 + async function onSubmit(data: PeriodicEvaluationFilterFormValues) { + if (isInitializing) return; + + startTransition(async () => { + try { + const newFilters = [] + + if (data.evaluationYear?.trim()) { + newFilters.push({ + id: "evaluationYear", + value: parseInt(data.evaluationYear.trim()), + type: "number", + operator: "eq", + rowId: generateId() + }) + } + + if (data.evaluationPeriod?.trim()) { + newFilters.push({ + id: "evaluationPeriod", + value: data.evaluationPeriod.trim(), + type: "select", + operator: "eq", + rowId: generateId() + }) + } + + if (data.division?.trim()) { + newFilters.push({ + id: "division", + value: data.division.trim(), + type: "select", + operator: "eq", + rowId: generateId() + }) + } + + if (data.status?.trim()) { + newFilters.push({ + id: "status", + value: data.status.trim(), + type: "select", + operator: "eq", + rowId: generateId() + }) + } + + if (data.domesticForeign?.trim()) { + newFilters.push({ + id: "domesticForeign", + value: data.domesticForeign.trim(), + type: "select", + operator: "eq", + rowId: generateId() + }) + } + + if (data.materialType?.trim()) { + newFilters.push({ + id: "materialType", + value: data.materialType.trim(), + type: "select", + operator: "eq", + rowId: generateId() + }) + } + + if (data.vendorCode?.trim()) { + newFilters.push({ + id: "vendorCode", + value: data.vendorCode.trim(), + type: "text", + operator: "iLike", + rowId: generateId() + }) + } + + if (data.vendorName?.trim()) { + newFilters.push({ + id: "vendorName", + value: data.vendorName.trim(), + type: "text", + operator: "iLike", + rowId: generateId() + }) + } + + if (data.documentsSubmitted?.trim()) { + newFilters.push({ + id: "documentsSubmitted", + value: data.documentsSubmitted.trim() === "true", + type: "boolean", + operator: "eq", + rowId: generateId() + }) + } + + if (data.evaluationGrade?.trim()) { + newFilters.push({ + id: "evaluationGrade", + value: data.evaluationGrade.trim(), + type: "select", + operator: "eq", + rowId: generateId() + }) + } + + if (data.finalGrade?.trim()) { + newFilters.push({ + id: "finalGrade", + value: data.finalGrade.trim(), + type: "select", + operator: "eq", + rowId: generateId() + }) + } + + if (data.minTotalScore?.trim()) { + newFilters.push({ + id: "totalScore", + value: parseFloat(data.minTotalScore.trim()), + type: "number", + operator: "gte", + rowId: generateId() + }) + } + + if (data.maxTotalScore?.trim()) { + newFilters.push({ + id: "totalScore", + value: parseFloat(data.maxTotalScore.trim()), + type: "number", + operator: "lte", + rowId: generateId() + }) + } + + // URL 업데이트 + const currentUrl = new URL(window.location.href); + const params = new URLSearchParams(currentUrl.search); + + params.delete('basicFilters'); + params.delete('basicJoinOperator'); + params.delete('page'); + + if (newFilters.length > 0) { + params.set('basicFilters', JSON.stringify(newFilters)); + params.set('basicJoinOperator', joinOperator); + } + + params.set('page', '1'); + + const newUrl = `${currentUrl.pathname}?${params.toString()}`; + window.location.href = newUrl; + + lastAppliedFilters.current = JSON.stringify(newFilters); + + if (onSearch) { + onSearch(); + } + } catch (error) { + console.error("정기평가 필터 적용 오류:", error); + } + }) + } + + // 필터 초기화 핸들러 + async function handleReset() { + try { + setIsInitializing(true); + + form.reset({ + evaluationYear: new Date().getFullYear().toString(), + evaluationPeriod: "", + division: "", + status: "", + domesticForeign: "", + materialType: "", + vendorCode: "", + vendorName: "", + documentsSubmitted: "", + evaluationGrade: "", + finalGrade: "", + minTotalScore: "", + maxTotalScore: "", + }); + + const currentUrl = new URL(window.location.href); + const params = new URLSearchParams(currentUrl.search); + + params.delete('basicFilters'); + params.delete('basicJoinOperator'); + params.set('page', '1'); + + const newUrl = `${currentUrl.pathname}?${params.toString()}`; + window.location.href = newUrl; + + lastAppliedFilters.current = ""; + setIsInitializing(false); + } catch (error) { + console.error("정기평가 필터 초기화 오류:", error); + setIsInitializing(false); + } + } + + if (!isOpen) { + return null; + } + + return ( +
+ {/* Filter Panel Header */} +
+

정기평가 검색 필터

+
+ {getActiveFilterCount() > 0 && ( + + {getActiveFilterCount()}개 필터 적용됨 + + )} +
+
+ + {/* Join Operator Selection */} +
+ + +
+ +
+ + {/* Scrollable content area */} +
+
+ + {/* 평가년도 */} + ( + + 평가년도 + +
+ + {field.value && ( + + )} +
+
+ +
+ )} + /> + + {/* 평가기간 */} + ( + + 평가기간 + + + + )} + /> + + {/* 구분 */} + ( + + 구분 + + + + )} + /> + + {/* 진행상태 */} + ( + + 진행상태 + + + + )} + /> + + {/* 내외자 구분 */} + ( + + 내외자 구분 + + + + )} + /> + + {/* 자재구분 */} + ( + + 자재구분 + + + + )} + /> + + {/* 벤더 코드 */} + ( + + 벤더 코드 + +
+ + {field.value && ( + + )} +
+
+ +
+ )} + /> + + {/* 벤더명 */} + ( + + 벤더명 + +
+ + {field.value && ( + + )} +
+
+ +
+ )} + /> + + {/* 문서제출여부 */} + ( + + 문서제출여부 + + + + )} + /> + + {/* 평가등급 */} + ( + + 평가등급 + + + + )} + /> + + {/* 최종등급 */} + ( + + 최종등급 + + + + )} + /> + + {/* 점수 범위 */} +
+ ( + + 최소점수 + +
+ + {field.value && ( + + )} +
+
+ +
+ )} + /> + + ( + + 최대점수 + +
+ + {field.value && ( + + )} +
+
+ +
+ )} + /> +
+ +
+
+ + {/* Fixed buttons at bottom */} +
+
+ + +
+
+
+ +
+ ) +} \ No newline at end of file -- cgit v1.2.3