diff options
Diffstat (limited to 'lib/techsales-rfq/table/rfq-filter-sheet.tsx')
| -rw-r--r-- | lib/techsales-rfq/table/rfq-filter-sheet.tsx | 1516 |
1 files changed, 758 insertions, 758 deletions
diff --git a/lib/techsales-rfq/table/rfq-filter-sheet.tsx b/lib/techsales-rfq/table/rfq-filter-sheet.tsx index 9b6acfb2..a03e6167 100644 --- a/lib/techsales-rfq/table/rfq-filter-sheet.tsx +++ b/lib/techsales-rfq/table/rfq-filter-sheet.tsx @@ -1,759 +1,759 @@ -"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 { DateRangePicker } from "@/components/date-range-picker" -import { - Select, - SelectContent, - SelectItem, - SelectTrigger, - SelectValue, -} from "@/components/ui/select" -import { cn } from "@/lib/utils" -import { useTranslation } from '@/i18n/client' -import { getFiltersStateParser } from "@/lib/parsers" - -// nanoid 생성기 -const generateId = customAlphabet("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 6) - -// 필터 스키마 정의 (TechSales RFQ에 맞게 수정) -const filterSchema = z.object({ - rfqCode: z.string().optional(), - materialCode: z.string().optional(), - itemName: z.string().optional(), - pspid: z.string().optional(), - projNm: z.string().optional(), - ptypeNm: z.string().optional(), - createdByName: z.string().optional(), - status: z.string().optional(), - dateRange: z.object({ - from: z.date().optional(), - to: z.date().optional(), - }).optional(), -}) - -// 상태 옵션 정의 (TechSales RFQ 상태에 맞게 수정) -const statusOptions = [ - { value: "RFQ Created", label: "RFQ Created" }, - { value: "RFQ Vendor Assignned", label: "RFQ Vendor Assignned" }, - { value: "RFQ Sent", label: "RFQ Sent" }, - { value: "Quotation Analysis", label: "Quotation Analysis" }, - { value: "Closed", label: "Closed" }, -] - -type FilterFormValues = z.infer<typeof filterSchema> - -interface RFQFilterSheetProps { - isOpen: boolean; - onClose: () => void; - onSearch?: () => void; - isLoading?: boolean; -} - -// Updated component for inline use (not a sheet anymore) -export function RFQFilterSheet({ - isOpen, - onClose, - onSearch, - isLoading = false -}: RFQFilterSheetProps) { - const router = useRouter() - const params = useParams(); - const lng = params ? (params.lng as string) : 'ko'; - const { t } = useTranslation(lng); - - const [isPending, startTransition] = useTransition() - - // 초기화 상태 추가 - 폼 초기화 중에는 상태 변경을 방지 - const [isInitializing, setIsInitializing] = useState(false) - // 마지막으로 적용된 필터를 추적하기 위한 ref - const lastAppliedFilters = useRef<string>("") - - // nuqs로 URL 상태 관리 - 파라미터명을 'basicFilters'로 변경 - const [filters, setFilters] = useQueryState( - "basicFilters", - getFiltersStateParser().withDefault([]) - ) - - // joinOperator 설정 - const [joinOperator, setJoinOperator] = useQueryState( - "basicJoinOperator", - parseAsStringEnum(["and", "or"]).withDefault("and") - ) - - // 현재 URL의 페이지 파라미터도 가져옴 - const [page, setPage] = useQueryState("page", { defaultValue: "1" }) - - // 폼 상태 초기화 - const form = useForm<FilterFormValues>({ - resolver: zodResolver(filterSchema), - defaultValues: { - rfqCode: "", - materialCode: "", - itemName: "", - pspid: "", - projNm: "", - ptypeNm: "", - createdByName: "", - status: "", - dateRange: { - from: undefined, - to: undefined, - }, - }, - }) - - // 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 === "rfqSendDate" && Array.isArray(filter.value) && filter.value.length > 0) { - formValues.dateRange = { - from: filter.value[0] ? new Date(filter.value[0]) : undefined, - to: filter.value[1] ? new Date(filter.value[1]) : undefined, - }; - formUpdated = true; - } else if (filter.id in formValues) { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - (formValues as any)[filter.id] = filter.value; - formUpdated = true; - } - }); - - // 폼 값이 변경된 경우에만 reset으로 한 번에 업데이트 - if (formUpdated) { - form.reset(formValues); - lastAppliedFilters.current = currentFiltersString; - } - - setIsInitializing(false); - } - }, [filters, isOpen, form]) // form 의존성 추가 - - // 현재 적용된 필터 카운트 - const getActiveFilterCount = () => { - return filters?.length || 0 - } - - // 조회 버튼 클릭 핸들러 - const handleSearch = () => { - // 필터 패널 닫기 로직이 있다면 여기에 추가 - if (onSearch) { - onSearch(); - } - } - - // 폼 제출 핸들러 - 개선된 버전 - async function onSubmit(data: FilterFormValues) { - // 초기화 중이면 제출 방지 - if (isInitializing) return; - - startTransition(async () => { - try { - // 필터 배열 생성 - const newFilters = [] - - if (data.rfqCode?.trim()) { - newFilters.push({ - id: "rfqCode", - value: data.rfqCode.trim(), - type: "text" as const, - operator: "iLike" as const, - rowId: generateId() - }) - } - - if (data.materialCode?.trim()) { - newFilters.push({ - id: "materialCode", - value: data.materialCode.trim(), - type: "text" as const, - operator: "iLike" as const, - rowId: generateId() - }) - } - - if (data.itemName?.trim()) { - newFilters.push({ - id: "itemName", - value: data.itemName.trim(), - type: "text" as const, - operator: "iLike" as const, - rowId: generateId() - }) - } - - if (data.pspid?.trim()) { - newFilters.push({ - id: "pspid", - value: data.pspid.trim(), - type: "text" as const, - operator: "iLike" as const, - rowId: generateId() - }) - } - - if (data.projNm?.trim()) { - newFilters.push({ - id: "projNm", - value: data.projNm.trim(), - type: "text" as const, - operator: "iLike" as const, - rowId: generateId() - }) - } - - if (data.ptypeNm?.trim()) { - newFilters.push({ - id: "ptypeNm", - value: data.ptypeNm.trim(), - type: "text" as const, - operator: "iLike" as const, - rowId: generateId() - }) - } - - if (data.createdByName?.trim()) { - newFilters.push({ - id: "createdByName", - value: data.createdByName.trim(), - type: "text" as const, - operator: "iLike" as const, - rowId: generateId() - }) - } - - if (data.status?.trim()) { - newFilters.push({ - id: "status", - value: data.status.trim(), - type: "select" as const, - operator: "eq" as const, - rowId: generateId() - }) - } - - // Add date range to params if it exists - if (data.dateRange?.from) { - newFilters.push({ - id: "rfqSendDate", - value: [ - data.dateRange.from.toISOString().split('T')[0], - data.dateRange.to ? data.dateRange.to.toISOString().split('T')[0] : undefined - ].filter(Boolean) as string[], - type: "date" as const, - operator: "isBetween" as const, - rowId: generateId() - }) - } - - console.log("기본 필터 적용:", newFilters); - - // 마지막 적용된 필터 업데이트 - lastAppliedFilters.current = JSON.stringify(newFilters); - - // 먼저 필터를 설정 - await setFilters(newFilters.length > 0 ? newFilters : null); - - // 그 다음 페이지를 1로 설정 - await setPage("1"); - - // 필터 업데이트 후 조회 핸들러 호출 (제공된 경우) - handleSearch(); - - // 페이지 새로고침으로 서버 데이터 다시 가져오기 - setTimeout(() => { - window.location.reload(); - }, 100); - } catch (error) { - console.error("필터 적용 오류:", error); - } - }) - } - - // 필터 초기화 핸들러 - 개선된 버전 - async function handleReset() { - try { - setIsInitializing(true); - - form.reset({ - rfqCode: "", - materialCode: "", - itemName: "", - pspid: "", - projNm: "", - ptypeNm: "", - createdByName: "", - status: "", - dateRange: { from: undefined, to: undefined }, - }); - - // 필터와 조인 연산자를 초기화 - await setFilters(null); - await setJoinOperator("and"); - await setPage("1"); - - // 마지막 적용된 필터 초기화 - lastAppliedFilters.current = ""; - - console.log("필터 초기화 완료"); - setIsInitializing(false); - - // 페이지 새로고침으로 서버 데이터 다시 가져오기 - setTimeout(() => { - window.location.reload(); - }, 100); - } catch (error) { - console.error("필터 초기화 오류:", error); - setIsInitializing(false); - } - } - - // Don't render if not open (for side panel use) - if (!isOpen) { - return null; - } - - return ( - <div className="flex flex-col h-full max-h-full p-4"> - {/* Filter Panel Header - 보더 제거, 배경 색상 적용 */} - <div className="flex items-center justify-between px-6 min-h-[60px] shrink-0"> - <h3 className="text-lg font-semibold whitespace-nowrap">검색 필터</h3> - </div> - - {/* Join Operator Selection - 보더 제거, 배경 색상 적용 */} - <div className="px-6 shrink-0"> - <label className="text-sm font-medium">조건 결합 방식</label> - <Select - value={joinOperator} - onValueChange={(value: "and" | "or") => setJoinOperator(value)} - disabled={isInitializing} - > - <SelectTrigger className="h-8 w-[180px] mt-2 bg-white"> - <SelectValue placeholder="조건 결합 방식" /> - </SelectTrigger> - <SelectContent> - <SelectItem value="and">모든 조건 충족 (AND)</SelectItem> - <SelectItem value="or">하나라도 충족 (OR)</SelectItem> - </SelectContent> - </Select> - </div> - - <Form {...form}> - <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col h-full min-h-0"> - {/* Scrollable content area - 헤더와 버튼 사이에서 스크롤 */} - <div className="flex-1 min-h-0 overflow-y-auto px-6 pb-4"> - <div className="space-y-6 pt-4"> - {/* RFQ NO. */} - <FormField - control={form.control} - name="rfqCode" - render={({ field }) => ( - <FormItem> - <FormLabel>{t("RFQ NO.")}</FormLabel> - <FormControl> - <div className="relative"> - <Input - placeholder={t("RFQ 번호 입력")} - {...field} - className={cn(field.value && "pr-8", "bg-white")} - disabled={isInitializing} - /> - {field.value && ( - <Button - type="button" - variant="ghost" - size="icon" - className="absolute right-0 top-0 h-full px-2" - onClick={(e) => { - e.stopPropagation(); - form.setValue("rfqCode", ""); - }} - disabled={isInitializing} - > - <X className="size-3.5" /> - <span className="sr-only">Clear</span> - </Button> - )} - </div> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - {/* 자재그룹 */} - <FormField - control={form.control} - name="materialCode" - render={({ field }) => ( - <FormItem> - <FormLabel>{t("자재그룹")}</FormLabel> - <FormControl> - <div className="relative"> - <Input - placeholder={t("자재그룹 입력")} - {...field} - className={cn(field.value && "pr-8", "bg-white")} - disabled={isInitializing} - /> - {field.value && ( - <Button - type="button" - variant="ghost" - size="icon" - className="absolute right-0 top-0 h-full px-2" - onClick={(e) => { - e.stopPropagation(); - form.setValue("materialCode", ""); - }} - disabled={isInitializing} - > - <X className="size-3.5" /> - <span className="sr-only">Clear</span> - </Button> - )} - </div> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - {/* 자재명 */} - <FormField - control={form.control} - name="itemName" - render={({ field }) => ( - <FormItem> - <FormLabel>{t("자재명")}</FormLabel> - <FormControl> - <div className="relative"> - <Input - placeholder={t("자재명 입력")} - {...field} - className={cn(field.value && "pr-8", "bg-white")} - disabled={isInitializing} - /> - {field.value && ( - <Button - type="button" - variant="ghost" - size="icon" - className="absolute right-0 top-0 h-full px-2" - onClick={(e) => { - e.stopPropagation(); - form.setValue("itemName", ""); - }} - disabled={isInitializing} - > - <X className="size-3.5" /> - <span className="sr-only">Clear</span> - </Button> - )} - </div> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - {/* 프로젝트 ID */} - <FormField - control={form.control} - name="pspid" - render={({ field }) => ( - <FormItem> - <FormLabel>{t("프로젝트 ID")}</FormLabel> - <FormControl> - <div className="relative"> - <Input - placeholder={t("프로젝트 ID 입력")} - {...field} - className={cn(field.value && "pr-8", "bg-white")} - disabled={isInitializing} - /> - {field.value && ( - <Button - type="button" - variant="ghost" - size="icon" - className="absolute right-0 top-0 h-full px-2" - onClick={(e) => { - e.stopPropagation(); - form.setValue("pspid", ""); - }} - disabled={isInitializing} - > - <X className="size-3.5" /> - <span className="sr-only">Clear</span> - </Button> - )} - </div> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - {/* 프로젝트명 */} - <FormField - control={form.control} - name="projNm" - render={({ field }) => ( - <FormItem> - <FormLabel>{t("프로젝트명")}</FormLabel> - <FormControl> - <div className="relative"> - <Input - placeholder={t("프로젝트명 입력")} - {...field} - className={cn(field.value && "pr-8", "bg-white")} - disabled={isInitializing} - /> - {field.value && ( - <Button - type="button" - variant="ghost" - size="icon" - className="absolute right-0 top-0 h-full px-2" - onClick={(e) => { - e.stopPropagation(); - form.setValue("projNm", ""); - }} - disabled={isInitializing} - > - <X className="size-3.5" /> - <span className="sr-only">Clear</span> - </Button> - )} - </div> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - {/* 선종명 */} - <FormField - control={form.control} - name="ptypeNm" - render={({ field }) => ( - <FormItem> - <FormLabel>{t("선종명")}</FormLabel> - <FormControl> - <div className="relative"> - <Input - placeholder={t("선종명 입력")} - {...field} - className={cn(field.value && "pr-8", "bg-white")} - disabled={isInitializing} - /> - {field.value && ( - <Button - type="button" - variant="ghost" - size="icon" - className="absolute right-0 top-0 h-full px-2" - onClick={(e) => { - e.stopPropagation(); - form.setValue("ptypeNm", ""); - }} - disabled={isInitializing} - > - <X className="size-3.5" /> - <span className="sr-only">Clear</span> - </Button> - )} - </div> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - {/* 요청자 */} - <FormField - control={form.control} - name="createdByName" - render={({ field }) => ( - <FormItem> - <FormLabel>{t("요청자")}</FormLabel> - <FormControl> - <div className="relative"> - <Input - placeholder={t("요청자 입력")} - {...field} - className={cn(field.value && "pr-8", "bg-white")} - disabled={isInitializing} - /> - {field.value && ( - <Button - type="button" - variant="ghost" - size="icon" - className="absolute right-0 top-0 h-full px-2" - onClick={(e) => { - e.stopPropagation(); - form.setValue("createdByName", ""); - }} - disabled={isInitializing} - > - <X className="size-3.5" /> - <span className="sr-only">Clear</span> - </Button> - )} - </div> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - {/* Status */} - <FormField - control={form.control} - name="status" - render={({ field }) => ( - <FormItem> - <FormLabel>{t("Status")}</FormLabel> - <Select - value={field.value} - onValueChange={field.onChange} - disabled={isInitializing} - > - <FormControl> - <SelectTrigger className={cn(field.value && "pr-8", "bg-white")}> - <div className="flex justify-between w-full"> - <SelectValue placeholder={t("Select status")} /> - {field.value && ( - <Button - type="button" - variant="ghost" - size="icon" - className="h-4 w-4 -mr-2" - onClick={(e) => { - e.stopPropagation(); - form.setValue("status", ""); - }} - disabled={isInitializing} - > - <X className="size-3" /> - <span className="sr-only">Clear</span> - </Button> - )} - </div> - </SelectTrigger> - </FormControl> - <SelectContent> - {statusOptions.map(option => ( - <SelectItem key={option.value} value={option.value}> - {option.label} - </SelectItem> - ))} - </SelectContent> - </Select> - <FormMessage /> - </FormItem> - )} - /> - - {/* RFQ 전송일 */} - <FormField - control={form.control} - name="dateRange" - render={({ field }) => ( - <FormItem> - <FormLabel>{t("RFQ 전송일")}</FormLabel> - <FormControl> - <div className="relative"> - <DateRangePicker - triggerSize="default" - triggerClassName="w-full bg-white" - align="start" - showClearButton={true} - placeholder={t("RFQ 전송일 범위를 고르세요")} - date={field.value || undefined} - onDateChange={field.onChange} - disabled={isInitializing} - /> - {(field.value?.from || field.value?.to) && ( - <Button - type="button" - variant="ghost" - size="icon" - className="absolute right-10 top-0 h-full px-2" - onClick={(e) => { - e.stopPropagation(); - form.setValue("dateRange", { from: undefined, to: undefined }); - }} - disabled={isInitializing} - > - <X className="size-3.5" /> - <span className="sr-only">Clear</span> - </Button> - )} - </div> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - </div> - </div> - - {/* Fixed buttons at bottom - 보더 제거, 배경 색상 적용 */} - <div className="p-4 shrink-0"> - <div className="flex gap-2 justify-end"> - <Button - type="button" - variant="outline" - onClick={handleReset} - disabled={isPending || getActiveFilterCount() === 0 || isInitializing} - className="px-4" - > - {t("초기화")} - </Button> - <Button - type="submit" - variant="samsung" - disabled={isPending || isLoading || isInitializing} - className="px-4" - > - <Search className="size-4 mr-2" /> - {isPending || isLoading ? t("조회 중...") : t("조회")} - </Button> - </div> - </div> - </form> - </Form> - </div> - ) +"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 { DateRangePicker } from "@/components/date-range-picker"
+import {
+ Select,
+ SelectContent,
+ SelectItem,
+ SelectTrigger,
+ SelectValue,
+} from "@/components/ui/select"
+import { cn } from "@/lib/utils"
+import { useTranslation } from '@/i18n/client'
+import { getFiltersStateParser } from "@/lib/parsers"
+
+// nanoid 생성기
+const generateId = customAlphabet("0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", 6)
+
+// 필터 스키마 정의 (TechSales RFQ에 맞게 수정)
+const filterSchema = z.object({
+ rfqCode: z.string().optional(),
+ materialCode: z.string().optional(),
+ itemName: z.string().optional(),
+ pspid: z.string().optional(),
+ projNm: z.string().optional(),
+ ptypeNm: z.string().optional(),
+ createdByName: z.string().optional(),
+ status: z.string().optional(),
+ dateRange: z.object({
+ from: z.date().optional(),
+ to: z.date().optional(),
+ }).optional(),
+})
+
+// 상태 옵션 정의 (TechSales RFQ 상태에 맞게 수정)
+const statusOptions = [
+ { value: "RFQ Created", label: "RFQ Created" },
+ { value: "RFQ Vendor Assignned", label: "RFQ Vendor Assignned" },
+ { value: "RFQ Sent", label: "RFQ Sent" },
+ { value: "Quotation Analysis", label: "Quotation Analysis" },
+ { value: "Closed", label: "Closed" },
+]
+
+type FilterFormValues = z.infer<typeof filterSchema>
+
+interface RFQFilterSheetProps {
+ isOpen: boolean;
+ onClose: () => void;
+ onSearch?: () => void;
+ isLoading?: boolean;
+}
+
+// Updated component for inline use (not a sheet anymore)
+export function RFQFilterSheet({
+ isOpen,
+ onClose,
+ onSearch,
+ isLoading = false
+}: RFQFilterSheetProps) {
+ const router = useRouter()
+ const params = useParams();
+ const lng = params ? (params.lng as string) : 'ko';
+ const { t } = useTranslation(lng);
+
+ const [isPending, startTransition] = useTransition()
+
+ // 초기화 상태 추가 - 폼 초기화 중에는 상태 변경을 방지
+ const [isInitializing, setIsInitializing] = useState(false)
+ // 마지막으로 적용된 필터를 추적하기 위한 ref
+ const lastAppliedFilters = useRef<string>("")
+
+ // nuqs로 URL 상태 관리 - 파라미터명을 'basicFilters'로 변경
+ const [filters, setFilters] = useQueryState(
+ "basicFilters",
+ getFiltersStateParser().withDefault([])
+ )
+
+ // joinOperator 설정
+ const [joinOperator, setJoinOperator] = useQueryState(
+ "basicJoinOperator",
+ parseAsStringEnum(["and", "or"]).withDefault("and")
+ )
+
+ // 현재 URL의 페이지 파라미터도 가져옴
+ const [page, setPage] = useQueryState("page", { defaultValue: "1" })
+
+ // 폼 상태 초기화
+ const form = useForm<FilterFormValues>({
+ resolver: zodResolver(filterSchema),
+ defaultValues: {
+ rfqCode: "",
+ materialCode: "",
+ itemName: "",
+ pspid: "",
+ projNm: "",
+ ptypeNm: "",
+ createdByName: "",
+ status: "",
+ dateRange: {
+ from: undefined,
+ to: undefined,
+ },
+ },
+ })
+
+ // 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 === "rfqSendDate" && Array.isArray(filter.value) && filter.value.length > 0) {
+ formValues.dateRange = {
+ from: filter.value[0] ? new Date(filter.value[0]) : undefined,
+ to: filter.value[1] ? new Date(filter.value[1]) : undefined,
+ };
+ formUpdated = true;
+ } else if (filter.id in formValues) {
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
+ (formValues as any)[filter.id] = filter.value;
+ formUpdated = true;
+ }
+ });
+
+ // 폼 값이 변경된 경우에만 reset으로 한 번에 업데이트
+ if (formUpdated) {
+ form.reset(formValues);
+ lastAppliedFilters.current = currentFiltersString;
+ }
+
+ setIsInitializing(false);
+ }
+ }, [filters, isOpen, form]) // form 의존성 추가
+
+ // 현재 적용된 필터 카운트
+ const getActiveFilterCount = () => {
+ return filters?.length || 0
+ }
+
+ // 조회 버튼 클릭 핸들러
+ const handleSearch = () => {
+ // 필터 패널 닫기 로직이 있다면 여기에 추가
+ if (onSearch) {
+ onSearch();
+ }
+ }
+
+ // 폼 제출 핸들러 - 개선된 버전
+ async function onSubmit(data: FilterFormValues) {
+ // 초기화 중이면 제출 방지
+ if (isInitializing) return;
+
+ startTransition(async () => {
+ try {
+ // 필터 배열 생성
+ const newFilters = []
+
+ if (data.rfqCode?.trim()) {
+ newFilters.push({
+ id: "rfqCode",
+ value: data.rfqCode.trim(),
+ type: "text" as const,
+ operator: "iLike" as const,
+ rowId: generateId()
+ })
+ }
+
+ if (data.materialCode?.trim()) {
+ newFilters.push({
+ id: "materialCode",
+ value: data.materialCode.trim(),
+ type: "text" as const,
+ operator: "iLike" as const,
+ rowId: generateId()
+ })
+ }
+
+ if (data.itemName?.trim()) {
+ newFilters.push({
+ id: "itemName",
+ value: data.itemName.trim(),
+ type: "text" as const,
+ operator: "iLike" as const,
+ rowId: generateId()
+ })
+ }
+
+ if (data.pspid?.trim()) {
+ newFilters.push({
+ id: "pspid",
+ value: data.pspid.trim(),
+ type: "text" as const,
+ operator: "iLike" as const,
+ rowId: generateId()
+ })
+ }
+
+ if (data.projNm?.trim()) {
+ newFilters.push({
+ id: "projNm",
+ value: data.projNm.trim(),
+ type: "text" as const,
+ operator: "iLike" as const,
+ rowId: generateId()
+ })
+ }
+
+ if (data.ptypeNm?.trim()) {
+ newFilters.push({
+ id: "ptypeNm",
+ value: data.ptypeNm.trim(),
+ type: "text" as const,
+ operator: "iLike" as const,
+ rowId: generateId()
+ })
+ }
+
+ if (data.createdByName?.trim()) {
+ newFilters.push({
+ id: "createdByName",
+ value: data.createdByName.trim(),
+ type: "text" as const,
+ operator: "iLike" as const,
+ rowId: generateId()
+ })
+ }
+
+ if (data.status?.trim()) {
+ newFilters.push({
+ id: "status",
+ value: data.status.trim(),
+ type: "select" as const,
+ operator: "eq" as const,
+ rowId: generateId()
+ })
+ }
+
+ // Add date range to params if it exists
+ if (data.dateRange?.from) {
+ newFilters.push({
+ id: "rfqSendDate",
+ value: [
+ data.dateRange.from.toISOString().split('T')[0],
+ data.dateRange.to ? data.dateRange.to.toISOString().split('T')[0] : undefined
+ ].filter(Boolean) as string[],
+ type: "date" as const,
+ operator: "isBetween" as const,
+ rowId: generateId()
+ })
+ }
+
+ console.log("기본 필터 적용:", newFilters);
+
+ // 마지막 적용된 필터 업데이트
+ lastAppliedFilters.current = JSON.stringify(newFilters);
+
+ // 먼저 필터를 설정
+ await setFilters(newFilters.length > 0 ? newFilters : null);
+
+ // 그 다음 페이지를 1로 설정
+ await setPage("1");
+
+ // 필터 업데이트 후 조회 핸들러 호출 (제공된 경우)
+ handleSearch();
+
+ // 페이지 새로고침으로 서버 데이터 다시 가져오기
+ setTimeout(() => {
+ window.location.reload();
+ }, 100);
+ } catch (error) {
+ console.error("필터 적용 오류:", error);
+ }
+ })
+ }
+
+ // 필터 초기화 핸들러 - 개선된 버전
+ async function handleReset() {
+ try {
+ setIsInitializing(true);
+
+ form.reset({
+ rfqCode: "",
+ materialCode: "",
+ itemName: "",
+ pspid: "",
+ projNm: "",
+ ptypeNm: "",
+ createdByName: "",
+ status: "",
+ dateRange: { from: undefined, to: undefined },
+ });
+
+ // 필터와 조인 연산자를 초기화
+ await setFilters(null);
+ await setJoinOperator("and");
+ await setPage("1");
+
+ // 마지막 적용된 필터 초기화
+ lastAppliedFilters.current = "";
+
+ console.log("필터 초기화 완료");
+ setIsInitializing(false);
+
+ // 페이지 새로고침으로 서버 데이터 다시 가져오기
+ setTimeout(() => {
+ window.location.reload();
+ }, 100);
+ } catch (error) {
+ console.error("필터 초기화 오류:", error);
+ setIsInitializing(false);
+ }
+ }
+
+ // Don't render if not open (for side panel use)
+ if (!isOpen) {
+ return null;
+ }
+
+ return (
+ <div className="flex flex-col h-full max-h-full p-4">
+ {/* Filter Panel Header - 보더 제거, 배경 색상 적용 */}
+ <div className="flex items-center justify-between px-6 min-h-[60px] shrink-0">
+ <h3 className="text-lg font-semibold whitespace-nowrap">검색 필터</h3>
+ </div>
+
+ {/* Join Operator Selection - 보더 제거, 배경 색상 적용 */}
+ <div className="px-6 shrink-0">
+ <label className="text-sm font-medium">조건 결합 방식</label>
+ <Select
+ value={joinOperator}
+ onValueChange={(value: "and" | "or") => setJoinOperator(value)}
+ disabled={isInitializing}
+ >
+ <SelectTrigger className="h-8 w-[180px] mt-2 bg-white">
+ <SelectValue placeholder="조건 결합 방식" />
+ </SelectTrigger>
+ <SelectContent>
+ <SelectItem value="and">모든 조건 충족 (AND)</SelectItem>
+ <SelectItem value="or">하나라도 충족 (OR)</SelectItem>
+ </SelectContent>
+ </Select>
+ </div>
+
+ <Form {...form}>
+ <form onSubmit={form.handleSubmit(onSubmit)} className="flex flex-col h-full min-h-0">
+ {/* Scrollable content area - 헤더와 버튼 사이에서 스크롤 */}
+ <div className="flex-1 min-h-0 overflow-y-auto px-6 pb-4">
+ <div className="space-y-6 pt-4">
+ {/* RFQ NO. */}
+ <FormField
+ control={form.control}
+ name="rfqCode"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>{t("RFQ NO.")}</FormLabel>
+ <FormControl>
+ <div className="relative">
+ <Input
+ placeholder={t("RFQ 번호 입력")}
+ {...field}
+ className={cn(field.value && "pr-8", "bg-white")}
+ disabled={isInitializing}
+ />
+ {field.value && (
+ <Button
+ type="button"
+ variant="ghost"
+ size="icon"
+ className="absolute right-0 top-0 h-full px-2"
+ onClick={(e) => {
+ e.stopPropagation();
+ form.setValue("rfqCode", "");
+ }}
+ disabled={isInitializing}
+ >
+ <X className="size-3.5" />
+ <span className="sr-only">Clear</span>
+ </Button>
+ )}
+ </div>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 자재그룹 */}
+ <FormField
+ control={form.control}
+ name="materialCode"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>{t("자재그룹")}</FormLabel>
+ <FormControl>
+ <div className="relative">
+ <Input
+ placeholder={t("자재그룹 입력")}
+ {...field}
+ className={cn(field.value && "pr-8", "bg-white")}
+ disabled={isInitializing}
+ />
+ {field.value && (
+ <Button
+ type="button"
+ variant="ghost"
+ size="icon"
+ className="absolute right-0 top-0 h-full px-2"
+ onClick={(e) => {
+ e.stopPropagation();
+ form.setValue("materialCode", "");
+ }}
+ disabled={isInitializing}
+ >
+ <X className="size-3.5" />
+ <span className="sr-only">Clear</span>
+ </Button>
+ )}
+ </div>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 자재명 */}
+ <FormField
+ control={form.control}
+ name="itemName"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>{t("자재명")}</FormLabel>
+ <FormControl>
+ <div className="relative">
+ <Input
+ placeholder={t("자재명 입력")}
+ {...field}
+ className={cn(field.value && "pr-8", "bg-white")}
+ disabled={isInitializing}
+ />
+ {field.value && (
+ <Button
+ type="button"
+ variant="ghost"
+ size="icon"
+ className="absolute right-0 top-0 h-full px-2"
+ onClick={(e) => {
+ e.stopPropagation();
+ form.setValue("itemName", "");
+ }}
+ disabled={isInitializing}
+ >
+ <X className="size-3.5" />
+ <span className="sr-only">Clear</span>
+ </Button>
+ )}
+ </div>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 프로젝트 ID */}
+ <FormField
+ control={form.control}
+ name="pspid"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>{t("프로젝트 ID")}</FormLabel>
+ <FormControl>
+ <div className="relative">
+ <Input
+ placeholder={t("프로젝트 ID 입력")}
+ {...field}
+ className={cn(field.value && "pr-8", "bg-white")}
+ disabled={isInitializing}
+ />
+ {field.value && (
+ <Button
+ type="button"
+ variant="ghost"
+ size="icon"
+ className="absolute right-0 top-0 h-full px-2"
+ onClick={(e) => {
+ e.stopPropagation();
+ form.setValue("pspid", "");
+ }}
+ disabled={isInitializing}
+ >
+ <X className="size-3.5" />
+ <span className="sr-only">Clear</span>
+ </Button>
+ )}
+ </div>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 프로젝트명 */}
+ <FormField
+ control={form.control}
+ name="projNm"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>{t("프로젝트명")}</FormLabel>
+ <FormControl>
+ <div className="relative">
+ <Input
+ placeholder={t("프로젝트명 입력")}
+ {...field}
+ className={cn(field.value && "pr-8", "bg-white")}
+ disabled={isInitializing}
+ />
+ {field.value && (
+ <Button
+ type="button"
+ variant="ghost"
+ size="icon"
+ className="absolute right-0 top-0 h-full px-2"
+ onClick={(e) => {
+ e.stopPropagation();
+ form.setValue("projNm", "");
+ }}
+ disabled={isInitializing}
+ >
+ <X className="size-3.5" />
+ <span className="sr-only">Clear</span>
+ </Button>
+ )}
+ </div>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 선종명 */}
+ <FormField
+ control={form.control}
+ name="ptypeNm"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>{t("선종명")}</FormLabel>
+ <FormControl>
+ <div className="relative">
+ <Input
+ placeholder={t("선종명 입력")}
+ {...field}
+ className={cn(field.value && "pr-8", "bg-white")}
+ disabled={isInitializing}
+ />
+ {field.value && (
+ <Button
+ type="button"
+ variant="ghost"
+ size="icon"
+ className="absolute right-0 top-0 h-full px-2"
+ onClick={(e) => {
+ e.stopPropagation();
+ form.setValue("ptypeNm", "");
+ }}
+ disabled={isInitializing}
+ >
+ <X className="size-3.5" />
+ <span className="sr-only">Clear</span>
+ </Button>
+ )}
+ </div>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* 요청자 */}
+ <FormField
+ control={form.control}
+ name="createdByName"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>{t("요청자")}</FormLabel>
+ <FormControl>
+ <div className="relative">
+ <Input
+ placeholder={t("요청자 입력")}
+ {...field}
+ className={cn(field.value && "pr-8", "bg-white")}
+ disabled={isInitializing}
+ />
+ {field.value && (
+ <Button
+ type="button"
+ variant="ghost"
+ size="icon"
+ className="absolute right-0 top-0 h-full px-2"
+ onClick={(e) => {
+ e.stopPropagation();
+ form.setValue("createdByName", "");
+ }}
+ disabled={isInitializing}
+ >
+ <X className="size-3.5" />
+ <span className="sr-only">Clear</span>
+ </Button>
+ )}
+ </div>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* Status */}
+ <FormField
+ control={form.control}
+ name="status"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>{t("Status")}</FormLabel>
+ <Select
+ value={field.value}
+ onValueChange={field.onChange}
+ disabled={isInitializing}
+ >
+ <FormControl>
+ <SelectTrigger className={cn(field.value && "pr-8", "bg-white")}>
+ <div className="flex justify-between w-full">
+ <SelectValue placeholder={t("Select status")} />
+ {field.value && (
+ <Button
+ type="button"
+ variant="ghost"
+ size="icon"
+ className="h-4 w-4 -mr-2"
+ onClick={(e) => {
+ e.stopPropagation();
+ form.setValue("status", "");
+ }}
+ disabled={isInitializing}
+ >
+ <X className="size-3" />
+ <span className="sr-only">Clear</span>
+ </Button>
+ )}
+ </div>
+ </SelectTrigger>
+ </FormControl>
+ <SelectContent>
+ {statusOptions.map(option => (
+ <SelectItem key={option.value} value={option.value}>
+ {option.label}
+ </SelectItem>
+ ))}
+ </SelectContent>
+ </Select>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+
+ {/* RFQ 전송일 */}
+ <FormField
+ control={form.control}
+ name="dateRange"
+ render={({ field }) => (
+ <FormItem>
+ <FormLabel>{t("RFQ 전송일")}</FormLabel>
+ <FormControl>
+ <div className="relative">
+ <DateRangePicker
+ triggerSize="default"
+ triggerClassName="w-full bg-white"
+ align="start"
+ showClearButton={true}
+ placeholder={t("RFQ 전송일 범위를 고르세요")}
+ date={field.value || undefined}
+ onDateChange={field.onChange}
+ disabled={isInitializing}
+ />
+ {(field.value?.from || field.value?.to) && (
+ <Button
+ type="button"
+ variant="ghost"
+ size="icon"
+ className="absolute right-10 top-0 h-full px-2"
+ onClick={(e) => {
+ e.stopPropagation();
+ form.setValue("dateRange", { from: undefined, to: undefined });
+ }}
+ disabled={isInitializing}
+ >
+ <X className="size-3.5" />
+ <span className="sr-only">Clear</span>
+ </Button>
+ )}
+ </div>
+ </FormControl>
+ <FormMessage />
+ </FormItem>
+ )}
+ />
+ </div>
+ </div>
+
+ {/* Fixed buttons at bottom - 보더 제거, 배경 색상 적용 */}
+ <div className="p-4 shrink-0">
+ <div className="flex gap-2 justify-end">
+ <Button
+ type="button"
+ variant="outline"
+ onClick={handleReset}
+ disabled={isPending || getActiveFilterCount() === 0 || isInitializing}
+ className="px-4"
+ >
+ {t("초기화")}
+ </Button>
+ <Button
+ type="submit"
+ variant="samsung"
+ disabled={isPending || isLoading || isInitializing}
+ className="px-4"
+ >
+ <Search className="size-4 mr-2" />
+ {isPending || isLoading ? t("조회 중...") : t("조회")}
+ </Button>
+ </div>
+ </div>
+ </form>
+ </Form>
+ </div>
+ )
}
\ No newline at end of file |
