diff options
| author | joonhoekim <26rote@gmail.com> | 2025-12-01 19:52:06 +0900 |
|---|---|---|
| committer | joonhoekim <26rote@gmail.com> | 2025-12-01 19:52:06 +0900 |
| commit | 44b74ff4170090673b6eeacd8c528e0abf47b7aa (patch) | |
| tree | 3f3824b4e2cb24536c1677188b4cae5b8909d3da /lib/b-rfq/summary-table | |
| parent | 4953e770929b82ef77da074f77071ebd0f428529 (diff) | |
(김준회) deprecated code 정리
Diffstat (limited to 'lib/b-rfq/summary-table')
| -rw-r--r-- | lib/b-rfq/summary-table/add-new-rfq-dialog.tsx | 523 | ||||
| -rw-r--r-- | lib/b-rfq/summary-table/summary-rfq-columns.tsx | 499 | ||||
| -rw-r--r-- | lib/b-rfq/summary-table/summary-rfq-filter-sheet.tsx | 617 | ||||
| -rw-r--r-- | lib/b-rfq/summary-table/summary-rfq-table-toolbar-actions.tsx | 68 | ||||
| -rw-r--r-- | lib/b-rfq/summary-table/summary-rfq-table.tsx | 285 |
5 files changed, 0 insertions, 1992 deletions
diff --git a/lib/b-rfq/summary-table/add-new-rfq-dialog.tsx b/lib/b-rfq/summary-table/add-new-rfq-dialog.tsx deleted file mode 100644 index 2333d9cf..00000000 --- a/lib/b-rfq/summary-table/add-new-rfq-dialog.tsx +++ /dev/null @@ -1,523 +0,0 @@ -"use client" - -import * as React from "react" -import { useForm } from "react-hook-form" -import { zodResolver } from "@hookform/resolvers/zod" -import { z } from "zod" -import { format } from "date-fns" -import { CalendarIcon, Plus, Loader2, Eye } from "lucide-react" -import { useRouter } from "next/navigation" -import { useSession } from "next-auth/react" - -import { Button } from "@/components/ui/button" -import { - Dialog, - DialogContent, - DialogDescription, - DialogFooter, - DialogHeader, - DialogTitle, - DialogTrigger, -} from "@/components/ui/dialog" -import { - Form, - FormControl, - FormField, - FormItem, - FormLabel, - FormMessage, - FormDescription, -} from "@/components/ui/form" -import { Input } from "@/components/ui/input" -import { Textarea } from "@/components/ui/textarea" -import { - Popover, - PopoverContent, - PopoverTrigger, -} from "@/components/ui/popover" -import { Calendar } from "@/components/ui/calendar" -import { Badge } from "@/components/ui/badge" -import { cn } from "@/lib/utils" -import { toast } from "sonner" -import { ProjectSelector } from "@/components/ProjectSelector" -import { createRfqAction, previewNextRfqCode } from "../service" - -export type Project = { - id: number; - projectCode: string; - projectName: string; -} - -// 클라이언트 폼 스키마 (projectId 필수로 변경) -const createRfqSchema = z.object({ - projectId: z.number().min(1, "프로젝트를 선택해주세요"), // 필수로 변경 - dueDate: z.date({ - required_error: "마감일을 선택해주세요", - }), - picCode: z.string().min(1, "구매 담당자 코드를 입력해주세요"), - picName: z.string().optional(), - engPicName: z.string().optional(), - packageNo: z.string().min(1, "패키지 번호를 입력해주세요"), - packageName: z.string().min(1, "패키지명을 입력해주세요"), - remark: z.string().optional(), - projectCompany: z.string().optional(), - projectFlag: z.string().optional(), - projectSite: z.string().optional(), -}) - -type CreateRfqFormValues = z.infer<typeof createRfqSchema> - -interface CreateRfqDialogProps { - onSuccess?: () => void; -} - -export function CreateRfqDialog({ onSuccess }: CreateRfqDialogProps) { - const [open, setOpen] = React.useState(false) - const [isLoading, setIsLoading] = React.useState(false) - const [previewCode, setPreviewCode] = React.useState<string>("") - const [isLoadingPreview, setIsLoadingPreview] = React.useState(false) - const router = useRouter() - const { data: session } = useSession() - - const userId = React.useMemo(() => { - return session?.user?.id ? Number(session.user.id) : null; - }, [session]); - - const form = useForm<CreateRfqFormValues>({ - resolver: zodResolver(createRfqSchema), - defaultValues: { - projectId: undefined, - dueDate: undefined, - picCode: "", - picName: "", - engPicName: "", - packageNo: "", - packageName: "", - remark: "", - projectCompany: "", - projectFlag: "", - projectSite: "", - }, - }) - - // picCode 변경 시 미리보기 업데이트 - const watchedPicCode = form.watch("picCode") - - React.useEffect(() => { - if (watchedPicCode && watchedPicCode.length > 0) { - setIsLoadingPreview(true) - const timer = setTimeout(async () => { - try { - const preview = await previewNextRfqCode(watchedPicCode) - setPreviewCode(preview) - } catch (error) { - console.error("미리보기 오류:", error) - setPreviewCode("") - } finally { - setIsLoadingPreview(false) - } - }, 500) // 500ms 디바운스 - - return () => clearTimeout(timer) - } else { - setPreviewCode("") - } - }, [watchedPicCode]) - - // 다이얼로그 열림/닫힘 처리 및 폼 리셋 - const handleOpenChange = (newOpen: boolean) => { - setOpen(newOpen) - - // 다이얼로그가 닫힐 때 폼과 상태 초기화 - if (!newOpen) { - form.reset() - setPreviewCode("") - setIsLoadingPreview(false) - } - } - - const handleCancel = () => { - form.reset() - setOpen(false) - } - - - const onSubmit = async (data: CreateRfqFormValues) => { - if (!userId) { - toast.error("로그인이 필요합니다") - return - } - - setIsLoading(true) - - try { - // 서버 액션 호출 - Date 객체를 직접 전달 - const result = await createRfqAction({ - projectId: data.projectId, // 이제 항상 값이 있음 - dueDate: data.dueDate, // Date 객체 직접 전달 - picCode: data.picCode, - picName: data.picName || "", - engPicName: data.engPicName || "", - packageNo: data.packageNo, - packageName: data.packageName, - remark: data.remark || "", - projectCompany: data.projectCompany || "", - projectFlag: data.projectFlag || "", - projectSite: data.projectSite || "", - createdBy: userId, - updatedBy: userId, - }) - - if (result.success) { - toast.success(result.message, { - description: `RFQ 코드: ${result.data?.rfqCode}`, - }) - - // 다이얼로그 닫기 (handleOpenChange에서 리셋 처리됨) - setOpen(false) - - // 성공 콜백 실행 - if (onSuccess) { - onSuccess() - } - - } else { - toast.error(result.error || "RFQ 생성에 실패했습니다") - } - - } catch (error) { - console.error('RFQ 생성 오류:', error) - toast.error("RFQ 생성에 실패했습니다", { - description: "알 수 없는 오류가 발생했습니다", - }) - } finally { - setIsLoading(false) - } - } - - const handleProjectSelect = (project: Project | null) => { - if (project === null) { - form.setValue("projectId", undefined as any); // 타입 에러 방지 - return; - } - form.setValue("projectId", project.id); - }; - - return ( - <Dialog open={open} onOpenChange={handleOpenChange}> - <DialogTrigger asChild> - <Button size="sm" variant="outline"> - <Plus className="mr-2 h-4 w-4" /> - 새 RFQ - </Button> - </DialogTrigger> - <DialogContent className="max-w-3xl h-[90vh] flex flex-col"> - {/* 고정된 헤더 */} - <DialogHeader className="flex-shrink-0"> - <DialogTitle>새 RFQ 생성</DialogTitle> - <DialogDescription> - 새로운 RFQ를 생성합니다. 필수 정보를 입력해주세요. - </DialogDescription> - </DialogHeader> - - {/* 스크롤 가능한 컨텐츠 영역 */} - <div className="flex-1 overflow-y-auto px-1"> - <Form {...form}> - <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4 py-2"> - - {/* 프로젝트 선택 (필수) */} - <FormField - control={form.control} - name="projectId" - render={({ field }) => ( - <FormItem> - <FormLabel> - 프로젝트 <span className="text-red-500">*</span> - </FormLabel> - <FormControl> - <ProjectSelector - selectedProjectId={field.value} - onProjectSelect={handleProjectSelect} - placeholder="프로젝트 선택..." - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - {/* 마감일 (필수) */} - <FormField - control={form.control} - name="dueDate" - render={({ field }) => ( - <FormItem className="flex flex-col"> - <FormLabel> - 마감일 <span className="text-red-500">*</span> - </FormLabel> - <Popover> - <PopoverTrigger asChild> - <FormControl> - <Button - variant="outline" - className={cn( - "w-full pl-3 text-left font-normal", - !field.value && "text-muted-foreground" - )} - > - {field.value ? ( - format(field.value, "yyyy-MM-dd") - ) : ( - <span>마감일을 선택하세요</span> - )} - <CalendarIcon className="ml-auto h-4 w-4 opacity-50" /> - </Button> - </FormControl> - </PopoverTrigger> - <PopoverContent className="w-auto p-0" align="start"> - <Calendar - mode="single" - selected={field.value} - onSelect={field.onChange} - disabled={(date) => - date < new Date() || date < new Date("1900-01-01") - } - initialFocus - /> - </PopoverContent> - </Popover> - <FormMessage /> - </FormItem> - )} - /> - - {/* 구매 담당자 코드 (필수) + 미리보기 */} - <FormField - control={form.control} - name="picCode" - render={({ field }) => ( - <FormItem> - <FormLabel> - 구매 담당자 코드 <span className="text-red-500">*</span> - </FormLabel> - <FormControl> - <div className="space-y-2"> - <Input - placeholder="예: P001, P002, MGR01 등" - {...field} - /> - {/* RFQ 코드 미리보기 */} - {previewCode && ( - <div className="flex items-center gap-2 p-2 bg-muted rounded-md"> - <Eye className="h-4 w-4 text-muted-foreground" /> - <span className="text-sm text-muted-foreground"> - 생성될 RFQ 코드: - </span> - <Badge variant="outline" className="font-mono"> - {isLoadingPreview ? "생성 중..." : previewCode} - </Badge> - </div> - )} - </div> - </FormControl> - <FormDescription> - RFQ 코드는 N + 담당자코드 + 시리얼번호(5자리) 형식으로 자동 생성됩니다 - </FormDescription> - <FormMessage /> - </FormItem> - )} - /> - - {/* 담당자 정보 (두 개 나란히) */} - <div className="space-y-3"> - <h4 className="text-sm font-medium">담당자 정보</h4> - <div className="grid grid-cols-2 gap-4"> - {/* 구매 담당자 */} - <FormField - control={form.control} - name="picName" - render={({ field }) => ( - <FormItem> - <FormLabel>구매 담당자명</FormLabel> - <FormControl> - <Input - placeholder="구매 담당자명" - {...field} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - {/* 설계 담당자 */} - <FormField - control={form.control} - name="engPicName" - render={({ field }) => ( - <FormItem> - <FormLabel>설계 담당자명</FormLabel> - <FormControl> - <Input - placeholder="설계 담당자명" - {...field} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - </div> - </div> - - {/* 패키지 정보 (두 개 나란히) - 필수 */} - <div className="space-y-3"> - <h4 className="text-sm font-medium">패키지 정보</h4> - <div className="grid grid-cols-2 gap-4"> - {/* 패키지 번호 (필수) */} - <FormField - control={form.control} - name="packageNo" - render={({ field }) => ( - <FormItem> - <FormLabel> - 패키지 번호 <span className="text-red-500">*</span> - </FormLabel> - <FormControl> - <Input - placeholder="패키지 번호" - {...field} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - {/* 패키지명 (필수) */} - <FormField - control={form.control} - name="packageName" - render={({ field }) => ( - <FormItem> - <FormLabel> - 패키지명 <span className="text-red-500">*</span> - </FormLabel> - <FormControl> - <Input - placeholder="패키지명" - {...field} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - </div> - </div> - - {/* 프로젝트 상세 정보 */} - <div className="space-y-3"> - <h4 className="text-sm font-medium">프로젝트 상세 정보</h4> - <div className="grid grid-cols-1 gap-3"> - <FormField - control={form.control} - name="projectCompany" - render={({ field }) => ( - <FormItem> - <FormLabel>프로젝트 회사</FormLabel> - <FormControl> - <Input - placeholder="프로젝트 회사명" - {...field} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - <div className="grid grid-cols-2 gap-3"> - <FormField - control={form.control} - name="projectFlag" - render={({ field }) => ( - <FormItem> - <FormLabel>프로젝트 플래그</FormLabel> - <FormControl> - <Input - placeholder="프로젝트 플래그" - {...field} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - <FormField - control={form.control} - name="projectSite" - render={({ field }) => ( - <FormItem> - <FormLabel>프로젝트 사이트</FormLabel> - <FormControl> - <Input - placeholder="프로젝트 사이트" - {...field} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - </div> - </div> - </div> - - {/* 비고 */} - <FormField - control={form.control} - name="remark" - render={({ field }) => ( - <FormItem> - <FormLabel>비고</FormLabel> - <FormControl> - <Textarea - placeholder="추가 비고사항을 입력하세요" - className="resize-none" - rows={3} - {...field} - /> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - </form> - </Form> - </div> - - {/* 고정된 푸터 */} - <DialogFooter className="flex-shrink-0"> - <Button - type="button" - variant="outline" - onClick={handleCancel} - disabled={isLoading} - > - 취소 - </Button> - <Button - type="submit" - onClick={form.handleSubmit(onSubmit)} - disabled={isLoading} - > - {isLoading && <Loader2 className="mr-2 h-4 w-4 animate-spin" />} - {isLoading ? "생성 중..." : "RFQ 생성"} - </Button> - </DialogFooter> - </DialogContent> - </Dialog> - ) -}
\ No newline at end of file diff --git a/lib/b-rfq/summary-table/summary-rfq-columns.tsx b/lib/b-rfq/summary-table/summary-rfq-columns.tsx deleted file mode 100644 index af5c22b2..00000000 --- a/lib/b-rfq/summary-table/summary-rfq-columns.tsx +++ /dev/null @@ -1,499 +0,0 @@ -"use client" - -import * as React from "react" -import { type DataTableRowAction } from "@/types/table" -import { type ColumnDef } from "@tanstack/react-table" -import { Ellipsis, Eye, Calendar, AlertTriangle, CheckCircle2, Clock, FileText } from "lucide-react" - -import { formatDate, cn } from "@/lib/utils" -import { Badge } from "@/components/ui/badge" -import { Button } from "@/components/ui/button" -import { Checkbox } from "@/components/ui/checkbox" -import { Progress } from "@/components/ui/progress" -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" -import { useRouter } from "next/navigation" -import { RfqDashboardView } from "@/db/schema" -import { DataTableColumnHeaderSimple } from "@/components/data-table/data-table-column-simple-header" - -type NextRouter = ReturnType<typeof useRouter>; - -interface GetRFQColumnsProps { - setRowAction: React.Dispatch<React.SetStateAction<DataTableRowAction<RfqDashboardView> | null>>; - router: NextRouter; -} - -// 상태에 따른 Badge 변형 결정 함수 -function getStatusBadge(status: string) { - switch (status) { - case "DRAFT": - return { variant: "outline" as const, label: "초안" }; - case "Doc. Received": - return { variant: "secondary" as const, label: "문서접수" }; - case "PIC Assigned": - return { variant: "secondary" as const, label: "담당자배정" }; - case "Doc. Confirmed": - return { variant: "default" as const, label: "문서확정" }; - case "Init. RFQ Sent": - return { variant: "default" as const, label: "초기RFQ발송" }; - case "Init. RFQ Answered": - return { variant: "default" as const, label: "초기RFQ회신" }; - case "TBE started": - return { variant: "secondary" as const, label: "TBE시작" }; - case "TBE finished": - return { variant: "secondary" as const, label: "TBE완료" }; - case "Final RFQ Sent": - return { variant: "default" as const, label: "최종RFQ발송" }; - case "Quotation Received": - return { variant: "default" as const, label: "견적접수" }; - case "Vendor Selected": - return { variant: "success" as const, label: "업체선정" }; - default: - return { variant: "outline" as const, label: status }; - } -} - -function getProgressBadge(progress: number) { - if (progress >= 100) { - return { variant: "success" as const, label: "완료" }; - } else if (progress >= 70) { - return { variant: "default" as const, label: "진행중" }; - } else if (progress >= 30) { - return { variant: "secondary" as const, label: "초기진행" }; - } else { - return { variant: "outline" as const, label: "시작" }; - } -} - -function getUrgencyLevel(daysToDeadline: number): "high" | "medium" | "low" { - if (daysToDeadline <= 3) return "high"; - if (daysToDeadline <= 7) return "medium"; - return "low"; -} - -export function getRFQColumns({ setRowAction, router }: GetRFQColumnsProps): ColumnDef<RfqDashboardView>[] { - - // Select 컬럼 - const selectColumn: ColumnDef<RfqDashboardView> = { - id: "select", - header: ({ table }) => ( - <Checkbox - checked={ - table.getIsAllPageRowsSelected() || - (table.getIsSomePageRowsSelected() && "indeterminate") - } - onCheckedChange={(value) => table.toggleAllPageRowsSelected(!!value)} - aria-label="Select all" - className="translate-y-0.5" - /> - ), - cell: ({ row }) => ( - <Checkbox - checked={row.getIsSelected()} - onCheckedChange={(value) => row.toggleSelected(!!value)} - aria-label="Select row" - className="translate-y-0.5" - /> - ), - size: 40, - enableSorting: false, - enableHiding: false, - }; - - // RFQ 코드 컬럼 - const rfqCodeColumn: ColumnDef<RfqDashboardView> = { - accessorKey: "rfqCode", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="RFQ 코드" /> - ), - cell: ({ row }) => ( - <div className="flex flex-col"> - <span className="font-medium">{row.getValue("rfqCode")}</span> - {row.original.description && ( - <span className="text-xs text-muted-foreground truncate max-w-[200px]"> - {row.original.description} - </span> - )} - </div> - ), - }; - - // 프로젝트 정보 컬럼 - const projectColumn: ColumnDef<RfqDashboardView> = { - accessorKey: "projectName", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="프로젝트" /> - ), - cell: ({ row }) => { - const projectName = row.original.projectName; - const projectCode = row.original.projectCode; - - if (!projectName) { - return <span className="text-muted-foreground">-</span>; - } - - return ( - <div className="flex flex-col"> - <span className="font-medium">{projectName}</span> - <div className="flex items-center gap-2 text-xs text-muted-foreground"> - {projectCode && <span>{projectCode}</span>} - </div> - </div> - ); - }, - }; - - // 패키지 정보 컬럼 - const packageColumn: ColumnDef<RfqDashboardView> = { - accessorKey: "packageNo", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="패키지" /> - ), - cell: ({ row }) => { - const packageNo = row.original.packageNo; - const packageName = row.original.packageName; - - if (!packageNo) { - return <span className="text-muted-foreground">-</span>; - } - - return ( - <div className="flex flex-col"> - <span className="font-medium">{packageNo}</span> - {packageName && ( - <span className="text-xs text-muted-foreground truncate max-w-[150px]"> - {packageName} - </span> - )} - </div> - ); - }, - }; - - const updatedColumn: ColumnDef<RfqDashboardView> = { - accessorKey: "updatedBy", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="Updated By" /> - ), - cell: ({ row }) => { - const updatedByName = row.original.updatedByName; - const updatedByEmail = row.original.updatedByEmail; - - if (!updatedByName) { - return <span className="text-muted-foreground">-</span>; - } - - return ( - <div className="flex flex-col"> - <span className="font-medium">{updatedByName}</span> - {updatedByEmail && ( - <span className="text-xs text-muted-foreground truncate max-w-[150px]"> - {updatedByEmail} - </span> - )} - </div> - ); - }, - }; - - - // 상태 컬럼 - const statusColumn: ColumnDef<RfqDashboardView> = { - accessorKey: "status", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="상태" /> - ), - cell: ({ row }) => { - const statusBadge = getStatusBadge(row.original.status); - return <Badge variant={statusBadge.variant}>{statusBadge.label}</Badge>; - }, - filterFn: (row, id, value) => { - return value.includes(row.getValue(id)); - }, - }; - - // 진행률 컬럼 - const progressColumn: ColumnDef<RfqDashboardView> = { - accessorKey: "overallProgress", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="진행률" /> - ), - cell: ({ row }) => { - const progress = row.original.overallProgress; - const progressBadge = getProgressBadge(progress); - - return ( - <div className="flex flex-col gap-1 min-w-[120px]"> - <div className="flex items-center justify-between"> - <span className="text-sm font-medium">{progress}%</span> - <Badge variant={progressBadge.variant} className="text-xs"> - {progressBadge.label} - </Badge> - </div> - <Progress value={progress} className="h-2" /> - </div> - ); - }, - }; - - // 마감일 컬럼 - const dueDateColumn: ColumnDef<RfqDashboardView> = { - accessorKey: "dueDate", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="마감일" /> - ), - cell: ({ row }) => { - const dueDate = row.original.dueDate; - const daysToDeadline = row.original.daysToDeadline; - const urgencyLevel = getUrgencyLevel(daysToDeadline); - - if (!dueDate) { - return <span className="text-muted-foreground">-</span>; - } - - return ( - <div className="flex flex-col"> - <div className="flex items-center gap-2"> - <Calendar className="h-4 w-4 text-muted-foreground" /> - <span>{formatDate(dueDate, 'KR')}</span> - </div> - <div className="flex items-center gap-1 text-xs"> - {urgencyLevel === "high" && ( - <AlertTriangle className="h-3 w-3 text-red-500" /> - )} - {urgencyLevel === "medium" && ( - <Clock className="h-3 w-3 text-yellow-500" /> - )} - {urgencyLevel === "low" && ( - <CheckCircle2 className="h-3 w-3 text-green-500" /> - )} - <span className={cn( - urgencyLevel === "high" && "text-red-500", - urgencyLevel === "medium" && "text-yellow-600", - urgencyLevel === "low" && "text-green-600" - )}> - {daysToDeadline > 0 ? `${daysToDeadline}일 남음` : - daysToDeadline === 0 ? "오늘 마감" : - `${Math.abs(daysToDeadline)}일 지남`} - </span> - </div> - </div> - ); - }, - }; - - // 담당자 컬럼 - const picColumn: ColumnDef<RfqDashboardView> = { - accessorKey: "picName", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="구매 담당자" /> - ), - cell: ({ row }) => { - const picName = row.original.picName; - return picName ? ( - <span>{picName}</span> - ) : ( - <span className="text-muted-foreground">미배정</span> - ); - }, - }; - - const engPicColumn: ColumnDef<RfqDashboardView> = { - accessorKey: "engPicName", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="설계 담당자" /> - ), - cell: ({ row }) => { - const picName = row.original.engPicName; - return picName ? ( - <span>{picName}</span> - ) : ( - <span className="text-muted-foreground">미배정</span> - ); - }, - }; - - - const pjtCompanyColumn: ColumnDef<RfqDashboardView> = { - accessorKey: "projectCompany", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="프로젝트 Company" /> - ), - cell: ({ row }) => { - const projectCompany = row.original.projectCompany; - return projectCompany ? ( - <span>{projectCompany}</span> - ) : ( - <span className="text-muted-foreground">-</span> - ); - }, - }; - - const pjtFlagColumn: ColumnDef<RfqDashboardView> = { - accessorKey: "projectFlag", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="프로젝트 Flag" /> - ), - cell: ({ row }) => { - const projectFlag = row.original.projectFlag; - return projectFlag ? ( - <span>{projectFlag}</span> - ) : ( - <span className="text-muted-foreground">-</span> - ); - }, - }; - - - const pjtSiteColumn: ColumnDef<RfqDashboardView> = { - accessorKey: "projectSite", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="프로젝트 Site" /> - ), - cell: ({ row }) => { - const projectSite = row.original.projectSite; - return projectSite ? ( - <span>{projectSite}</span> - ) : ( - <span className="text-muted-foreground">-</span> - ); - }, - }; - const remarkColumn: ColumnDef<RfqDashboardView> = { - accessorKey: "remark", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="비고" /> - ), - cell: ({ row }) => { - const remark = row.original.remark; - return remark ? ( - <span>{remark}</span> - ) : ( - <span className="text-muted-foreground">-</span> - ); - }, - }; - - // 첨부파일 수 컬럼 - const attachmentColumn: ColumnDef<RfqDashboardView> = { - accessorKey: "totalAttachments", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="첨부파일" /> - ), - cell: ({ row }) => { - const count = row.original.totalAttachments; - return ( - <div className="flex items-center gap-2"> - <FileText className="h-4 w-4 text-muted-foreground" /> - <span>{count}</span> - </div> - ); - }, - }; - - // 벤더 현황 컬럼 - const vendorStatusColumn: ColumnDef<RfqDashboardView> = { - accessorKey: "initialVendorCount", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="벤더 현황" /> - ), - cell: ({ row }) => { - const initial = row.original.initialVendorCount; - const final = row.original.finalVendorCount; - const initialRate = row.original.initialResponseRate; - const finalRate = row.original.finalResponseRate; - - return ( - <div className="flex flex-col gap-1 text-xs"> - <div className="flex items-center justify-between"> - <span className="text-muted-foreground">초기:</span> - <span>{initial}개사 ({Number(initialRate).toFixed(0)}%)</span> - </div> - <div className="flex items-center justify-between"> - <span className="text-muted-foreground">최종:</span> - <span>{final}개사 ({Number(finalRate).toFixed(0)}%)</span> - </div> - </div> - ); - }, - }; - - // 생성일 컬럼 - const createdAtColumn: ColumnDef<RfqDashboardView> = { - accessorKey: "createdAt", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="생성일" /> - ), - cell: ({ row }) => { - const dateVal = row.original.createdAt as Date; - return formatDate(dateVal, 'KR'); - }, - }; - - const updatedAtColumn: ColumnDef<RfqDashboardView> = { - accessorKey: "updatedAt", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="수정일" /> - ), - cell: ({ row }) => { - const dateVal = row.original.updatedAt as Date; - return formatDate(dateVal, 'KR'); - }, - }; - - // Actions 컬럼 - const actionsColumn: ColumnDef<RfqDashboardView> = { - id: "detail", - header: ({ column }) => ( - <DataTableColumnHeaderSimple column={column} title="상세내용" /> - ), - // enableHiding: false, - cell: function Cell({ row }) { - const rfq = row.original; - const detailUrl = `/evcp/b-rfq/${rfq.rfqId}/initial`; - - return ( - - <Button - aria-label="Open menu" - variant="ghost" - className="flex size-8 p-0 data-[state=open]:bg-muted" - onClick={() => router.push(detailUrl)} - > - <Ellipsis className="size-4" aria-hidden="true" /> - </Button> - ); - }, - size: 40, - }; - - return [ - selectColumn, - rfqCodeColumn, - projectColumn, - packageColumn, - statusColumn, - picColumn, - progressColumn, - dueDateColumn, - actionsColumn, - - engPicColumn, - - pjtCompanyColumn, - pjtFlagColumn, - pjtSiteColumn, - - attachmentColumn, - vendorStatusColumn, - createdAtColumn, - - updatedAtColumn, - updatedColumn, - remarkColumn - ]; -}
\ No newline at end of file diff --git a/lib/b-rfq/summary-table/summary-rfq-filter-sheet.tsx b/lib/b-rfq/summary-table/summary-rfq-filter-sheet.tsx deleted file mode 100644 index ff3bc132..00000000 --- a/lib/b-rfq/summary-table/summary-rfq-filter-sheet.tsx +++ /dev/null @@ -1,617 +0,0 @@ -"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) - -// RFQ 필터 스키마 정의 -const rfqFilterSchema = z.object({ - rfqCode: z.string().optional(), - projectCode: z.string().optional(), - picName: z.string().optional(), - packageNo: z.string().optional(), - packageName: z.string().optional(), - status: z.string().optional(), -}) - -// RFQ 상태 옵션 정의 -const rfqStatusOptions = [ - { value: "DRAFT", label: "초안" }, - { value: "Doc. Received", label: "문서접수" }, - { value: "PIC Assigned", label: "담당자배정" }, - { value: "Doc. Confirmed", label: "문서확인" }, - { value: "Init. RFQ Sent", label: "초기RFQ발송" }, - { value: "Init. RFQ Answered", label: "초기RFQ회신" }, - { value: "TBE started", label: "TBE시작" }, - { value: "TBE finished", label: "TBE완료" }, - { value: "Final RFQ Sent", label: "최종RFQ발송" }, - { value: "Quotation Received", label: "견적접수" }, - { value: "Vendor Selected", label: "업체선정" }, -] - -type RFQFilterFormValues = z.infer<typeof rfqFilterSchema> - -interface RFQFilterSheetProps { - isOpen: boolean; - onClose: () => void; - onSearch?: () => void; - isLoading?: boolean; -} - -export function RFQFilterSheet({ - isOpen, - onClose, - onSearch, - isLoading = false -}: RFQFilterSheetProps) { - const router = useRouter() - const params = useParams(); - const lng = params ? (params.lng as string) : 'ko'; - - const [isPending, startTransition] = useTransition() - - // 초기화 상태 추가 - 폼 초기화 중에는 상태 변경을 방지 - const [isInitializing, setIsInitializing] = useState(false) - // 마지막으로 적용된 필터를 추적하기 위한 ref - const lastAppliedFilters = useRef<string>("") - - // nuqs로 URL 상태 관리 - 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<RFQFilterFormValues>({ - resolver: zodResolver(rfqFilterSchema), - defaultValues: { - rfqCode: "", - projectCode: "", - picName: "", - packageNo: "", - packageName: "", - status: "", - }, - }) - - // 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; - } - }); - - // 폼 값이 변경된 경우에만 reset으로 한 번에 업데이트 - if (formUpdated) { - form.reset(formValues); - lastAppliedFilters.current = currentFiltersString; - } - - setIsInitializing(false); - } - }, [filters, isOpen]) - - // 현재 적용된 필터 카운트 - const getActiveFilterCount = () => { - return filters?.length || 0 - } - - // 폼 제출 핸들러 - async function onSubmit(data: RFQFilterFormValues) { - // 초기화 중이면 제출 방지 - if (isInitializing) return; - - startTransition(async () => { - try { - // 필터 배열 생성 - const newFilters = [] - - if (data.rfqCode?.trim()) { - newFilters.push({ - id: "rfqCode", - value: data.rfqCode.trim(), - type: "text", - operator: "iLike", - rowId: generateId() - }) - } - - if (data.projectCode?.trim()) { - newFilters.push({ - id: "projectCode", - value: data.projectCode.trim(), - type: "text", - operator: "iLike", - rowId: generateId() - }) - } - - if (data.picName?.trim()) { - newFilters.push({ - id: "picName", - value: data.picName.trim(), - type: "text", - operator: "iLike", - rowId: generateId() - }) - } - - if (data.packageNo?.trim()) { - newFilters.push({ - id: "packageNo", - value: data.packageNo.trim(), - type: "text", - operator: "iLike", - rowId: generateId() - }) - } - - if (data.packageName?.trim()) { - newFilters.push({ - id: "packageName", - value: data.packageName.trim(), - type: "text", - operator: "iLike", - rowId: generateId() - }) - } - - if (data.status?.trim()) { - newFilters.push({ - id: "status", - value: data.status.trim(), - type: "select", - operator: "eq", - rowId: generateId() - }) - } - - // 수동으로 URL 업데이트 (nuqs 대신) - const currentUrl = new URL(window.location.href); - const params = new URLSearchParams(currentUrl.search); - - // 기존 필터 관련 파라미터 제거 - params.delete('basicFilters'); - params.delete('rfqBasicFilters'); - params.delete('basicJoinOperator'); - params.delete('rfqBasicJoinOperator'); - params.delete('page'); - - // 새로운 필터 추가 - if (newFilters.length > 0) { - params.set('basicFilters', JSON.stringify(newFilters)); - params.set('basicJoinOperator', joinOperator); - } - - // 페이지를 1로 설정 - params.set('page', '1'); - - const newUrl = `${currentUrl.pathname}?${params.toString()}`; - console.log("New RFQ Filter URL:", newUrl); - - // 페이지 완전 새로고침으로 서버 렌더링 강제 - window.location.href = newUrl; - - // 마지막 적용된 필터 업데이트 - lastAppliedFilters.current = JSON.stringify(newFilters); - - // 필터 업데이트 후 조회 핸들러 호출 (제공된 경우) - if (onSearch) { - console.log("Calling RFQ onSearch..."); - onSearch(); - } - - console.log("=== RFQ Filter Submit Complete ==="); - } catch (error) { - console.error("RFQ 필터 적용 오류:", error); - } - }) - } - - // 필터 초기화 핸들러 - async function handleReset() { - try { - setIsInitializing(true); - - form.reset({ - rfqCode: "", - projectCode: "", - picName: "", - packageNo: "", - packageName: "", - status: "", - }); - - console.log("=== RFQ Filter Reset Debug ==="); - console.log("Current URL before reset:", window.location.href); - - // 수동으로 URL 초기화 - const currentUrl = new URL(window.location.href); - const params = new URLSearchParams(currentUrl.search); - - // 필터 관련 파라미터 제거 - params.delete('basicFilters'); - params.delete('rfqBasicFilters'); - params.delete('basicJoinOperator'); - params.delete('rfqBasicJoinOperator'); - params.set('page', '1'); - - const newUrl = `${currentUrl.pathname}?${params.toString()}`; - console.log("Reset URL:", newUrl); - - // 페이지 완전 새로고침 - window.location.href = newUrl; - - // 마지막 적용된 필터 초기화 - lastAppliedFilters.current = ""; - - console.log("RFQ 필터 초기화 완료"); - setIsInitializing(false); - } catch (error) { - console.error("RFQ 필터 초기화 오류:", 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 bg-[#F5F7FB] px-6 sm:px-8" style={{backgroundColor:"#F5F7FB", paddingLeft:"2rem", paddingRight:"2rem"}}> - {/* 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">RFQ 검색 필터</h3> - <div className="flex items-center gap-2"> - {getActiveFilterCount() > 0 && ( - <Badge variant="secondary" className="px-2 py-1"> - {getActiveFilterCount()}개 필터 적용됨 - </Badge> - )} - </div> - </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-4 pt-2"> - - {/* RFQ 코드 */} - <FormField - control={form.control} - name="rfqCode" - render={({ field }) => ( - <FormItem> - <FormLabel>RFQ 코드</FormLabel> - <FormControl> - <div className="relative"> - <Input - placeholder="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="projectCode" - render={({ field }) => ( - <FormItem> - <FormLabel>프로젝트 코드</FormLabel> - <FormControl> - <div className="relative"> - <Input - placeholder="프로젝트 코드 입력" - {...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("projectCode", ""); - }} - disabled={isInitializing} - > - <X className="size-3.5" /> - <span className="sr-only">Clear</span> - </Button> - )} - </div> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - {/* 담당자명 */} - <FormField - control={form.control} - name="picName" - render={({ field }) => ( - <FormItem> - <FormLabel>담당자명</FormLabel> - <FormControl> - <div className="relative"> - <Input - placeholder="담당자명 입력" - {...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("picName", ""); - }} - disabled={isInitializing} - > - <X className="size-3.5" /> - <span className="sr-only">Clear</span> - </Button> - )} - </div> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - {/* 패키지 번호 */} - <FormField - control={form.control} - name="packageNo" - render={({ field }) => ( - <FormItem> - <FormLabel>패키지 번호</FormLabel> - <FormControl> - <div className="relative"> - <Input - placeholder="패키지 번호 입력" - {...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("packageNo", ""); - }} - disabled={isInitializing} - > - <X className="size-3.5" /> - <span className="sr-only">Clear</span> - </Button> - )} - </div> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - {/* 패키지명 */} - <FormField - control={form.control} - name="packageName" - render={({ field }) => ( - <FormItem> - <FormLabel>패키지명</FormLabel> - <FormControl> - <div className="relative"> - <Input - placeholder="패키지명 입력" - {...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("packageName", ""); - }} - disabled={isInitializing} - > - <X className="size-3.5" /> - <span className="sr-only">Clear</span> - </Button> - )} - </div> - </FormControl> - <FormMessage /> - </FormItem> - )} - /> - - {/* RFQ 상태 */} - <FormField - control={form.control} - name="status" - render={({ field }) => ( - <FormItem> - <FormLabel>RFQ 상태</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="RFQ 상태 선택" /> - {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> - {rfqStatusOptions.map(option => ( - <SelectItem key={option.value} value={option.value}> - {option.label} - </SelectItem> - ))} - </SelectContent> - </Select> - <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" - > - 초기화 - </Button> - <Button - type="submit" - variant="samsung" - disabled={isPending || isLoading || isInitializing} - className="px-4" - > - <Search className="size-4 mr-2" /> - {isPending || isLoading ? "조회 중..." : "조회"} - </Button> - </div> - </div> - </form> - </Form> - </div> - ) -}
\ No newline at end of file diff --git a/lib/b-rfq/summary-table/summary-rfq-table-toolbar-actions.tsx b/lib/b-rfq/summary-table/summary-rfq-table-toolbar-actions.tsx deleted file mode 100644 index 02ba4aaa..00000000 --- a/lib/b-rfq/summary-table/summary-rfq-table-toolbar-actions.tsx +++ /dev/null @@ -1,68 +0,0 @@ -"use client" - -import * as React from "react" -import { type Table } from "@tanstack/react-table" -import { Download, FileText, Mail, Search } from "lucide-react" -import { useRouter } from "next/navigation" - -import { Button } from "@/components/ui/button" -import { - DropdownMenu, - DropdownMenuContent, - DropdownMenuItem, - DropdownMenuSeparator, - DropdownMenuTrigger, -} from "@/components/ui/dropdown-menu" -import { RfqDashboardView } from "@/db/schema" -import { CreateRfqDialog } from "./add-new-rfq-dialog" - -interface RFQTableToolbarActionsProps { - table: Table<RfqDashboardView> -} - -export function RFQTableToolbarActions({ table }: RFQTableToolbarActionsProps) { - const router = useRouter() - - // 선택된 행 정보 - const selectedRows = table.getFilteredSelectedRowModel().rows - const selectedCount = selectedRows.length - const isSingleSelected = selectedCount === 1 - - // RFQ 문서 확인 핸들러 - const handleDocumentCheck = () => { - if (isSingleSelected) { - const selectedRfq = selectedRows[0].original - const rfqId = selectedRfq.rfqId - - // RFQ 첨부문서 확인 페이지로 이동 - router.push(`/evcp/b-rfq/${rfqId}`) - } - } - - // 테이블 새로고침 핸들러 - const handleRefresh = () => { - // 페이지 새로고침 또는 데이터 다시 fetch - router.refresh() - } - - return ( - <div className="flex items-center gap-2"> - {/* 새 RFQ 생성 다이얼로그 */} - <CreateRfqDialog onSuccess={handleRefresh} /> - - {/* RFQ 문서 확인 버튼 - 단일 선택시만 활성화 */} - <Button - size="sm" - variant="outline" - onClick={handleDocumentCheck} - disabled={!isSingleSelected} - className="flex items-center" - > - <Search className="mr-2 h-4 w-4" /> - RFQ 문서 확인 - </Button> - - - </div> - ) -} diff --git a/lib/b-rfq/summary-table/summary-rfq-table.tsx b/lib/b-rfq/summary-table/summary-rfq-table.tsx deleted file mode 100644 index 83d50685..00000000 --- a/lib/b-rfq/summary-table/summary-rfq-table.tsx +++ /dev/null @@ -1,285 +0,0 @@ -"use client" - -import * as React from "react" -import { useRouter, useSearchParams } from "next/navigation" -import { Button } from "@/components/ui/button" -import { PanelLeftClose, PanelLeftOpen } from "lucide-react" -import type { - DataTableAdvancedFilterField, - DataTableFilterField, - DataTableRowAction, -} from "@/types/table" - -import { useDataTable } from "@/hooks/use-data-table" -import { DataTable } from "@/components/data-table/data-table" -import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar" -import { getRFQDashboard } from "../service" -import { cn } from "@/lib/utils" -import { useTablePresets } from "@/components/data-table/use-table-presets" -import { TablePresetManager } from "@/components/data-table/data-table-preset" -import { useMemo } from "react" -import { getRFQColumns } from "./summary-rfq-columns" -import { RfqDashboardView } from "@/db/schema" -import { RFQTableToolbarActions } from "./summary-rfq-table-toolbar-actions" -import { RFQFilterSheet } from "./summary-rfq-filter-sheet" - -interface RFQDashboardTableProps { - promises: Promise<[Awaited<ReturnType<typeof getRFQDashboard>>]> - className?: string -} - -export function RFQDashboardTable({ promises, className }: RFQDashboardTableProps) { - const [rowAction, setRowAction] = React.useState<DataTableRowAction<RfqDashboardView> | null>(null) - const [isFilterPanelOpen, setIsFilterPanelOpen] = React.useState(false) - - const router = useRouter() - const searchParams = useSearchParams() - - const containerRef = React.useRef<HTMLDivElement>(null) - const [containerTop, setContainerTop] = React.useState(0) - - const updateContainerBounds = React.useCallback(() => { - if (containerRef.current) { - const rect = containerRef.current.getBoundingClientRect() - setContainerTop(rect.top) - } - }, []) - - React.useEffect(() => { - updateContainerBounds() - - const handleResize = () => { - updateContainerBounds() - } - - window.addEventListener('resize', handleResize) - window.addEventListener('scroll', updateContainerBounds) - - return () => { - window.removeEventListener('resize', handleResize) - window.removeEventListener('scroll', updateContainerBounds) - } - }, [updateContainerBounds]) - - const [promiseData] = React.use(promises) - const tableData = promiseData - - console.log("RFQ Dashboard Table Data:", { - dataLength: tableData.data?.length, - pageCount: tableData.pageCount, - total: tableData.total, - sampleData: tableData.data?.[0] - }) - - const initialSettings = React.useMemo(() => ({ - page: parseInt(searchParams.get('page') || '1'), - perPage: parseInt(searchParams.get('perPage') || '10'), - sort: searchParams.get('sort') ? JSON.parse(searchParams.get('sort')!) : [{ id: "createdAt", desc: true }], - filters: searchParams.get('filters') ? JSON.parse(searchParams.get('filters')!) : [], - joinOperator: (searchParams.get('joinOperator') as "and" | "or") || "and", - basicFilters: searchParams.get('basicFilters') || searchParams.get('rfqBasicFilters') ? - JSON.parse(searchParams.get('basicFilters') || searchParams.get('rfqBasicFilters')!) : [], - basicJoinOperator: (searchParams.get('basicJoinOperator') as "and" | "or") || "and", - search: searchParams.get('search') || '', - columnVisibility: {}, - columnOrder: [], - pinnedColumns: { left: [], right: ["actions"] }, - groupBy: [], - expandedRows: [] - }), [searchParams]) - - const { - presets, - activePresetId, - hasUnsavedChanges, - isLoading: presetsLoading, - createPreset, - applyPreset, - updatePreset, - deletePreset, - setDefaultPreset, - renamePreset, - updateClientState, - getCurrentSettings, - } = useTablePresets<RfqDashboardView>('rfq-dashboard-table', initialSettings) - - const columns = React.useMemo( - () => getRFQColumns({ setRowAction, router }), - [setRowAction, router] - ) - - const filterFields: DataTableFilterField<RfqDashboardView>[] = [ - { id: "rfqCode", label: "RFQ 코드" }, - { id: "projectName", label: "프로젝트" }, - { id: "status", label: "상태" }, - ] - - const advancedFilterFields: DataTableAdvancedFilterField<RfqDashboardView>[] = [ - { id: "rfqCode", label: "RFQ 코드", type: "text" }, - { id: "description", label: "설명", type: "text" }, - { id: "projectName", label: "프로젝트명", type: "text" }, - { id: "projectCode", label: "프로젝트 코드", type: "text" }, - { id: "packageNo", label: "패키지 번호", type: "text" }, - { id: "packageName", label: "패키지명", type: "text" }, - { id: "picName", label: "담당자", type: "text" }, - { id: "status", label: "상태", type: "select", options: [ - { label: "초안", value: "DRAFT" }, - { label: "문서접수", value: "Doc. Received" }, - { label: "담당자배정", value: "PIC Assigned" }, - { label: "문서확인", value: "Doc. Confirmed" }, - { label: "초기RFQ발송", value: "Init. RFQ Sent" }, - { label: "초기RFQ회신", value: "Init. RFQ Answered" }, - { label: "TBE시작", value: "TBE started" }, - { label: "TBE완료", value: "TBE finished" }, - { label: "최종RFQ발송", value: "Final RFQ Sent" }, - { label: "견적접수", value: "Quotation Received" }, - { label: "업체선정", value: "Vendor Selected" }, - ]}, - { id: "overallProgress", label: "진행률", type: "number" }, - { id: "dueDate", label: "마감일", type: "date" }, - { id: "createdAt", label: "생성일", type: "date" }, - ] - - const currentSettings = useMemo(() => { - return getCurrentSettings() - }, [getCurrentSettings]) - - const initialState = useMemo(() => { - return { - sorting: initialSettings.sort.filter(sortItem => { - const columnExists = columns.some(col => col.accessorKey === sortItem.id) - return columnExists - }) as any, - columnVisibility: currentSettings.columnVisibility, - columnPinning: currentSettings.pinnedColumns, - } - }, [currentSettings, initialSettings.sort, columns]) - - const { table } = useDataTable({ - data: tableData.data, - columns, - pageCount: tableData.pageCount, - rowCount: tableData.total || tableData.data.length, - filterFields, - enablePinning: true, - enableAdvancedFilter: true, - initialState, - getRowId: (originalRow) => String(originalRow.rfqId), - shallow: false, - clearOnDefault: true, - }) - - const handleSearch = () => { - setIsFilterPanelOpen(false) - } - - const getActiveBasicFilterCount = () => { - try { - const basicFilters = searchParams.get('basicFilters') || searchParams.get('rfqBasicFilters') - return basicFilters ? JSON.parse(basicFilters).length : 0 - } catch (e) { - return 0 - } - } - - const FILTER_PANEL_WIDTH = 400; - - return ( - <> - {/* Filter Panel */} - <div - className={cn( - "fixed left-0 bg-background border-r z-50 flex flex-col transition-all duration-300 ease-in-out overflow-hidden", - isFilterPanelOpen ? "border-r shadow-lg" : "border-r-0" - )} - style={{ - width: isFilterPanelOpen ? `${FILTER_PANEL_WIDTH}px` : '0px', - top: `${containerTop}px`, - height: `calc(100vh - ${containerTop}px)` - }} - > - <div className="h-full"> - <RFQFilterSheet - isOpen={isFilterPanelOpen} - onClose={() => setIsFilterPanelOpen(false)} - onSearch={handleSearch} - isLoading={false} - /> - </div> - </div> - - {/* Main Content Container */} - <div - ref={containerRef} - className={cn("relative w-full overflow-hidden", className)} - > - <div className="flex w-full h-full"> - <div - className="flex flex-col min-w-0 overflow-hidden transition-all duration-300 ease-in-out" - style={{ - width: isFilterPanelOpen ? `calc(100% - ${FILTER_PANEL_WIDTH}px)` : '100%', - marginLeft: isFilterPanelOpen ? `${FILTER_PANEL_WIDTH}px` : '0px' - }} - > - {/* Header Bar */} - <div className="flex items-center justify-between p-4 bg-background shrink-0"> - <div className="flex items-center gap-3"> - <Button - variant="outline" - size="sm" - type='button' - onClick={() => setIsFilterPanelOpen(!isFilterPanelOpen)} - className="flex items-center shadow-sm" - > - {isFilterPanelOpen ? <PanelLeftClose className="size-4"/> : <PanelLeftOpen className="size-4"/>} - {getActiveBasicFilterCount() > 0 && ( - <span className="ml-2 bg-primary text-primary-foreground rounded-full px-2 py-0.5 text-xs"> - {getActiveBasicFilterCount()} - </span> - )} - </Button> - </div> - - <div className="text-sm text-muted-foreground"> - {tableData && ( - <span>총 {tableData.total || tableData.data.length}건</span> - )} - </div> - </div> - - {/* Table Content Area */} - <div className="flex-1 overflow-hidden" style={{ height: 'calc(100vh - 380px)' }}> - <div className="h-full w-full"> - <DataTable table={table} className="h-full"> - <DataTableAdvancedToolbar - table={table} - filterFields={advancedFilterFields} - shallow={false} - > - <div className="flex items-center gap-2"> - <TablePresetManager<RfqDashboardView> - presets={presets} - activePresetId={activePresetId} - currentSettings={currentSettings} - hasUnsavedChanges={hasUnsavedChanges} - isLoading={presetsLoading} - onCreatePreset={createPreset} - onUpdatePreset={updatePreset} - onDeletePreset={deletePreset} - onApplyPreset={applyPreset} - onSetDefaultPreset={setDefaultPreset} - onRenamePreset={renamePreset} - /> - - <RFQTableToolbarActions table={table} /> - </div> - </DataTableAdvancedToolbar> - </DataTable> - </div> - </div> - </div> - </div> - </div> - </> - ) -}
\ No newline at end of file |
