From 1dc24d48e52f2e490f5603ceb02842586ecae533 Mon Sep 17 00:00:00 2001 From: dujinkim Date: Thu, 24 Jul 2025 11:06:32 +0000 Subject: (대표님) 정기평가 피드백 반영, 설계 피드백 반영, (최겸) 기술영업 피드백 반영 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/evaluation/table/evaluation-filter-sheet.tsx | 616 ++++++++--------------- 1 file changed, 203 insertions(+), 413 deletions(-) (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 index 7f4de6a6..8f435e36 100644 --- a/lib/evaluation/table/evaluation-filter-sheet.tsx +++ b/lib/evaluation/table/evaluation-filter-sheet.tsx @@ -1,19 +1,15 @@ -// ================================================================ -// 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" +"use client"; + +import { useEffect, useTransition, useState, useRef } from "react"; +import { useRouter } 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, @@ -21,50 +17,28 @@ import { FormItem, FormLabel, FormMessage, -} from "@/components/ui/form" -import { Input } from "@/components/ui/input" -import { Badge } from "@/components/ui/badge" +} 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" +} from "@/components/ui/select"; +import { cn } from "@/lib/utils"; +import { getFiltersStateParser } from "@/lib/parsers"; +import { EVALUATION_TARGET_FILTER_OPTIONS } from "@/lib/evaluation-target-list/validation"; -// nanoid 생성기 -const generateId = customAlphabet("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 6) +/***************************************************************************************** + * UTILS & CONSTANTS + *****************************************************************************************/ -// 정기평가 필터 스키마 정의 -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(), -}) +// nanoid generator (6‑chars [0-9a-zA-Z]) +const generateId = customAlphabet("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 6); -// 옵션 정의 -const evaluationPeriodOptions = [ - { value: "상반기", label: "상반기" }, - { value: "하반기", label: "하반기" }, - { value: "연간", label: "연간" }, -] - -const divisionOptions = [ - { value: "PLANT", label: "해양" }, - { value: "SHIP", label: "조선" }, -] +// ── SELECT OPTIONS ────────────────────────────────────────────────────────────────────── const statusOptions = [ { value: "PENDING", label: "대상확정" }, @@ -73,70 +47,76 @@ const statusOptions = [ { 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: "A", label: "A등급" }, { value: "B", label: "B등급" }, { value: "C", label: "C등급" }, { value: "D", label: "D등급" }, -] +]; -type PeriodicEvaluationFilterFormValues = z.infer +/***************************************************************************************** + * ZOD SCHEMA & TYPES + *****************************************************************************************/ +const periodicEvaluationFilterSchema = z.object({ + evaluationYear: 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(), +}); +export type PeriodicEvaluationFilterFormValues = z.infer< + typeof periodicEvaluationFilterSchema +>; + +/***************************************************************************************** + * COMPONENT + *****************************************************************************************/ interface PeriodicEvaluationFilterSheetProps { + /** Slide‑over visibility */ isOpen: boolean; + /** Close panel handler */ onClose: () => void; - onSearch?: () => void; + /** Show skeleton / spinner while outer data grid fetches */ isLoading?: boolean; + /** Optional: fire immediately after URL is patched so parent can refetch */ + onFiltersApply: (filters: any[], joinOperator: "and" | "or") => void; // ✅ 필터 전달 콜백 } export function PeriodicEvaluationFilterSheet({ isOpen, onClose, - onSearch, - isLoading = false + isLoading = false, + onFiltersApply, }: PeriodicEvaluationFilterSheetProps) { - const router = useRouter() - const params = useParams(); - - const [isPending, startTransition] = useTransition() - const [isInitializing, setIsInitializing] = useState(false) - const lastAppliedFilters = useRef("") + /** Router (needed only for pathname) */ + const router = useRouter(); - // nuqs로 URL 상태 관리 - const [filters, setFilters] = useQueryState( - "basicFilters", - getFiltersStateParser().withDefault([]) - ) + /** Track pending state while we update URL */ + const [isPending, startTransition] = useTransition(); + const [joinOperator, setJoinOperator] = useState<"and" | "or">("and") - const [joinOperator, setJoinOperator] = useQueryState( - "basicJoinOperator", - parseAsStringEnum(["and", "or"]).withDefault("and") - ) - // 폼 상태 초기화 + /** React‑Hook‑Form */ const form = useForm({ resolver: zodResolver(periodicEvaluationFilterSchema), defaultValues: { evaluationYear: new Date().getFullYear().toString(), - evaluationPeriod: "", division: "", status: "", domesticForeign: "", @@ -149,273 +129,130 @@ export function PeriodicEvaluationFilterSheet({ 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 - } - // 폼 제출 핸들러 + /***************************************************************************************** + * 3️⃣ Submit → build filter array → push to URL (and reset page=1) + *****************************************************************************************/ async function onSubmit(data: PeriodicEvaluationFilterFormValues) { - if (isInitializing) return; - - startTransition(async () => { + startTransition(() => { try { - const newFilters = [] + const newFilters: any[] = []; - if (data.evaluationYear?.trim()) { - newFilters.push({ - id: "evaluationYear", - value: parseInt(data.evaluationYear.trim()), - type: "number", - operator: "eq", - rowId: generateId() - }) - } + const pushFilter = ( + id: string, + value: any, + type: "text" | "select" | "number" | "boolean", + operator: "eq" | "iLike" | "gte" | "lte" + ) => { + newFilters.push({ id, value, type, operator, rowId: generateId() }); + }; - if (data.evaluationPeriod?.trim()) { - newFilters.push({ - id: "evaluationPeriod", - value: data.evaluationPeriod.trim(), - type: "select", - operator: "eq", - rowId: generateId() - }) - } + if (data.evaluationYear?.trim()) + pushFilter("evaluationYear", Number(data.evaluationYear), "number", "eq"); - if (data.division?.trim()) { - newFilters.push({ - id: "division", - value: data.division.trim(), - type: "select", - operator: "eq", - rowId: generateId() - }) - } + if (data.division?.trim()) + pushFilter("division", data.division.trim(), "select", "eq"); - if (data.status?.trim()) { - newFilters.push({ - id: "status", - value: data.status.trim(), - type: "select", - operator: "eq", - rowId: generateId() - }) - } + if (data.status?.trim()) + pushFilter("status", data.status.trim(), "select", "eq"); - if (data.domesticForeign?.trim()) { - newFilters.push({ - id: "domesticForeign", - value: data.domesticForeign.trim(), - type: "select", - operator: "eq", - rowId: generateId() - }) - } + if (data.domesticForeign?.trim()) + pushFilter("domesticForeign", data.domesticForeign.trim(), "select", "eq"); - if (data.materialType?.trim()) { - newFilters.push({ - id: "materialType", - value: data.materialType.trim(), - type: "select", - operator: "eq", - rowId: generateId() - }) - } + if (data.materialType?.trim()) + pushFilter("materialType", data.materialType.trim(), "select", "eq"); - if (data.vendorCode?.trim()) { - newFilters.push({ - id: "vendorCode", - value: data.vendorCode.trim(), - type: "text", - operator: "iLike", - rowId: generateId() - }) - } + if (data.vendorCode?.trim()) + pushFilter("vendorCode", data.vendorCode.trim(), "text", "iLike"); - if (data.vendorName?.trim()) { - newFilters.push({ - id: "vendorName", - value: data.vendorName.trim(), - type: "text", - operator: "iLike", - rowId: generateId() - }) - } + if (data.vendorName?.trim()) + pushFilter("vendorName", data.vendorName.trim(), "text", "iLike"); - if (data.documentsSubmitted?.trim()) { - newFilters.push({ - id: "documentsSubmitted", - value: data.documentsSubmitted.trim() === "true", - type: "boolean", - operator: "eq", - rowId: generateId() - }) - } + if (data.documentsSubmitted?.trim()) + pushFilter( + "documentsSubmitted", + data.documentsSubmitted.trim() === "true", + "boolean", + "eq" + ); - if (data.evaluationGrade?.trim()) { - newFilters.push({ - id: "evaluationGrade", - value: data.evaluationGrade.trim(), - type: "select", - operator: "eq", - rowId: generateId() - }) - } + if (data.evaluationGrade?.trim()) + pushFilter("evaluationGrade", data.evaluationGrade.trim(), "select", "eq"); - if (data.finalGrade?.trim()) { - newFilters.push({ - id: "finalGrade", - value: data.finalGrade.trim(), - type: "select", - operator: "eq", - rowId: generateId() - }) - } + if (data.finalGrade?.trim()) + pushFilter("finalGrade", data.finalGrade.trim(), "select", "eq"); - if (data.minTotalScore?.trim()) { - newFilters.push({ - id: "totalScore", - value: parseFloat(data.minTotalScore.trim()), - type: "number", - operator: "gte", - rowId: generateId() - }) - } + if (data.minTotalScore?.trim()) + pushFilter("totalScore", Number(data.minTotalScore), "number", "gte"); - if (data.maxTotalScore?.trim()) { - newFilters.push({ - id: "totalScore", - value: parseFloat(data.maxTotalScore.trim()), - type: "number", - operator: "lte", - rowId: generateId() - }) - } + if (data.maxTotalScore?.trim()) + pushFilter("totalScore", Number(data.maxTotalScore), "number", "lte"); - // 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; + setJoinOperator(joinOperator); - lastAppliedFilters.current = JSON.stringify(newFilters); - if (onSearch) { - onSearch(); - } - } catch (error) { - console.error("정기평가 필터 적용 오류:", error); + onFiltersApply(newFilters, joinOperator); + } catch (err) { + // eslint-disable-next-line no-console + console.error("정기평가 필터 적용 오류:", err); } - }) + }); } - // 필터 초기화 핸들러 + /***************************************************************************************** + * 4️⃣ Reset → clear form & URL + *****************************************************************************************/ 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'); + form.reset({ + evaluationYear: new Date().getFullYear().toString(), + division: "", + status: "", + domesticForeign: "", + materialType: "", + vendorCode: "", + vendorName: "", + documentsSubmitted: "", + evaluationGrade: "", + finalGrade: "", + minTotalScore: "", + maxTotalScore: "", + }); - const newUrl = `${currentUrl.pathname}?${params.toString()}`; - window.location.href = newUrl; + onFiltersApply([], "and"); + setJoinOperator("and"); - lastAppliedFilters.current = ""; - setIsInitializing(false); - } catch (error) { - console.error("정기평가 필터 초기화 오류:", error); - setIsInitializing(false); - } } - if (!isOpen) { - return null; - } + /***************************************************************************************** + * 5️⃣ RENDER + *****************************************************************************************/ + if (!isOpen) return null; return ( -
- {/* Filter Panel Header */} -
-

정기평가 검색 필터

+
+ {/* Header */} +
+

정기평가 검색 필터

- {getActiveFilterCount() > 0 && ( - - {getActiveFilterCount()}개 필터 적용됨 - - )} +
- {/* Join Operator Selection */} -
+ {/* Join‑operator selector */} +
+ {/* Form */}
- - {/* Scrollable content area */} + + {/* Scrollable area */}
- {/* 평가년도 */} {field.value && ( @@ -469,52 +306,6 @@ export function PeriodicEvaluationFilterSheet({ )} /> - {/* 평가기간 */} - {/* ( - - 평가기간 - - - - )} - /> */} {/* 구분 */} -
+
{field.value && (
-
- {/* Fixed buttons at bottom */} -
-
+ {/* Footer buttons */} +
+
@@ -1027,5 +817,5 @@ export function PeriodicEvaluationFilterSheet({
- ) -} \ No newline at end of file + ); +} -- cgit v1.2.3