summaryrefslogtreecommitdiff
path: root/lib/pq/pq-review-table-new/vendors-table.tsx
diff options
context:
space:
mode:
authordujinkim <dujin.kim@dtsolution.co.kr>2025-08-04 09:39:21 +0000
committerdujinkim <dujin.kim@dtsolution.co.kr>2025-08-04 09:39:21 +0000
commit53ad72732f781e6c6d5ddb3776ea47aec010af8e (patch)
treee676287827f8634be767a674b8ad08b6ed7eb3e6 /lib/pq/pq-review-table-new/vendors-table.tsx
parent3e4d15271322397764601dee09441af8a5b3adf5 (diff)
(최겸) PQ/실사 수정 및 개발
Diffstat (limited to 'lib/pq/pq-review-table-new/vendors-table.tsx')
-rw-r--r--lib/pq/pq-review-table-new/vendors-table.tsx772
1 files changed, 465 insertions, 307 deletions
diff --git a/lib/pq/pq-review-table-new/vendors-table.tsx b/lib/pq/pq-review-table-new/vendors-table.tsx
index e1c4cefe..c2712611 100644
--- a/lib/pq/pq-review-table-new/vendors-table.tsx
+++ b/lib/pq/pq-review-table-new/vendors-table.tsx
@@ -1,308 +1,466 @@
-"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 { getPQSubmissions } from "../service"
-import { getColumns, PQSubmission } from "./vendors-table-columns"
-import { VendorsTableToolbarActions } from "./vendors-table-toolbar-actions"
-import { PQFilterSheet } from "./pq-filter-sheet"
-import { cn } from "@/lib/utils"
-// TablePresetManager 관련 import 추가
-import { useTablePresets } from "@/components/data-table/use-table-presets"
-import { TablePresetManager } from "@/components/data-table/data-table-preset"
-import { useMemo } from "react"
-
-interface PQSubmissionsTableProps {
- promises: Promise<[Awaited<ReturnType<typeof getPQSubmissions>>]>
- className?: string
-}
-
-export function PQSubmissionsTable({ promises, className }: PQSubmissionsTableProps) {
- const [rowAction, setRowAction] = React.useState<DataTableRowAction<PQSubmission> | null>(null)
- const [isFilterPanelOpen, setIsFilterPanelOpen] = React.useState(false)
-
- const router = useRouter()
- const searchParams = useSearchParams()
-
- // Container wrapper의 위치를 측정하기 위한 ref
- const containerRef = React.useRef<HTMLDivElement>(null)
- const [containerTop, setContainerTop] = React.useState(0)
-
- // Container 위치 측정 함수 - top만 측정
- 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])
-
- // Suspense 방식으로 데이터 처리
- const [promiseData] = React.use(promises)
- const tableData = promiseData
-
- // 디버깅용 로그
- console.log("PQ Table Data:", {
- dataLength: tableData.data?.length,
- pageCount: tableData.pageCount,
- sampleData: tableData.data?.[0]
- })
-
- // 초기 설정 정의 (RFQ와 동일한 패턴)
- 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: "updatedAt", 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('pqBasicFilters') ?
- JSON.parse(searchParams.get('basicFilters') || searchParams.get('pqBasicFilters')!) : [],
- basicJoinOperator: (searchParams.get('basicJoinOperator') as "and" | "or") || "and",
- search: searchParams.get('search') || '',
- from: searchParams.get('from') || undefined,
- to: searchParams.get('to') || undefined,
- columnVisibility: {},
- columnOrder: [],
- pinnedColumns: { left: [], right: ["actions"] }, // PQ는 actions를 오른쪽에 고정
- groupBy: [],
- expandedRows: []
- }), [searchParams])
-
- // DB 기반 프리셋 훅 사용
- const {
- presets,
- activePresetId,
- hasUnsavedChanges,
- isLoading: presetsLoading,
- createPreset,
- applyPreset,
- updatePreset,
- deletePreset,
- setDefaultPreset,
- renamePreset,
- updateClientState,
- getCurrentSettings,
- } = useTablePresets<PQSubmission>('pq-submissions-table', initialSettings)
-
- const columns = React.useMemo(
- () => getColumns({ setRowAction, router }),
- [setRowAction, router]
- )
-
- // PQ 제출 필터링을 위한 필드 정의
- const filterFields: DataTableFilterField<PQSubmission>[] = [
- { id: "vendorName", label: "협력업체" },
- { id: "projectName", label: "프로젝트" },
- { id: "status", label: "상태" },
- ]
-
- // 고급 필터 필드 정의
- const advancedFilterFields: DataTableAdvancedFilterField<PQSubmission>[] = [
- { id: "requesterName", label: "요청자명", type: "text" },
- { id: "pqNumber", label: "PQ 번호", type: "text" },
- { id: "vendorName", label: "협력업체명", type: "text" },
- { id: "vendorCode", label: "협력업체 코드", type: "text" },
- { id: "type", label: "PQ 유형", type: "select", options: [
- { label: "일반 PQ", value: "GENERAL" },
- { label: "프로젝트 PQ", value: "PROJECT" },
- ]},
- { id: "projectName", label: "프로젝트명", type: "text" },
- { id: "status", label: "PQ 상태", type: "select", options: [
- { label: "요청됨", value: "REQUESTED" },
- { label: "진행 중", value: "IN_PROGRESS" },
- { label: "제출됨", value: "SUBMITTED" },
- { label: "승인됨", value: "APPROVED" },
- { label: "거부됨", value: "REJECTED" },
- ]},
- { id: "evaluationResult", label: "평가 결과", type: "select", options: [
- { label: "승인", value: "APPROVED" },
- { label: "보완", value: "SUPPLEMENT" },
- { label: "불가", value: "REJECTED" },
- ]},
- { id: "createdAt", label: "생성일", type: "date" },
- { id: "submittedAt", label: "제출일", type: "date" },
- { id: "approvedAt", label: "승인일", type: "date" },
- { id: "rejectedAt", label: "거부일", type: "date" },
- ]
-
- // 현재 설정 가져오기
- const currentSettings = useMemo(() => {
- return getCurrentSettings()
- }, [getCurrentSettings])
-
- // useDataTable 초기 상태 설정 (RFQ와 동일한 패턴)
- 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, // total 추가
- filterFields, // RFQ와 달리 빈 배열이 아닌 실제 필터 필드 사용
- enablePinning: true,
- enableAdvancedFilter: true,
- initialState,
- getRowId: (originalRow) => String(originalRow.id),
- shallow: false,
- clearOnDefault: true,
- })
-
- // 조회 버튼 클릭 핸들러 - PQFilterSheet에 전달
- const handleSearch = () => {
- // Close the panel after search
- setIsFilterPanelOpen(false)
- }
-
- // Get active basic filter count
- const getActiveBasicFilterCount = () => {
- try {
- const basicFilters = searchParams.get('basicFilters') || searchParams.get('pqBasicFilters')
- return basicFilters ? JSON.parse(basicFilters).length : 0
- } catch (e) {
- return 0
- }
- }
-
- // Filter panel width
- const FILTER_PANEL_WIDTH = 400;
-
- return (
- <>
- {/* Filter Panel - fixed positioning으로 화면 최대 좌측에서 시작 */}
- <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)`
- }}
- >
- {/* Filter Content */}
- <div className="h-full">
- <PQFilterSheet
- 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">
- {/* Main Content - 너비 조정으로 필터 패널 공간 확보 */}
- <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>
-
- {/* Right side info */}
- <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">
- {/* DB 기반 테이블 프리셋 매니저 추가 */}
- <TablePresetManager<PQSubmission>
- presets={presets}
- activePresetId={activePresetId}
- currentSettings={currentSettings}
- hasUnsavedChanges={hasUnsavedChanges}
- isLoading={presetsLoading}
- onCreatePreset={createPreset}
- onUpdatePreset={updatePreset}
- onDeletePreset={deletePreset}
- onApplyPreset={applyPreset}
- onSetDefaultPreset={setDefaultPreset}
- onRenamePreset={renamePreset}
- />
-
- {/* 기존 툴바 액션들 */}
- <VendorsTableToolbarActions table={table} />
- </div>
- </DataTableAdvancedToolbar>
- </DataTable>
- </div>
- </div>
- </div>
- </div>
- </div>
- </>
- )
+"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 { toast } from "sonner"
+import type {
+ DataTableAdvancedFilterField,
+ DataTableFilterField,
+ DataTableRowAction,
+} from "@/types/table"
+import { updateInvestigationDetailsAction } from "../service"
+import { createSiteVisitRequestAction, getSiteVisitRequestAction } from "@/lib/site-visit/service"
+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 { getPQSubmissions } from "../service"
+import { getColumns, PQSubmission } from "./vendors-table-columns"
+import { VendorsTableToolbarActions } from "./vendors-table-toolbar-actions"
+import { PQFilterSheet } from "./pq-filter-sheet"
+import { SiteVisitDialog } from "./site-visit-dialog"
+import { VendorInfoViewDialog } from "@/lib/site-visit/vendor-info-view-dialog"
+import { EditInvestigationDialog } from "./edit-investigation-dialog"
+import { cn } from "@/lib/utils"
+// TablePresetManager 관련 import 추가
+import { useTablePresets } from "@/components/data-table/use-table-presets"
+import { TablePresetManager } from "@/components/data-table/data-table-preset"
+import { useMemo } from "react"
+
+interface PQSubmissionsTableProps {
+ promises: Promise<[Awaited<ReturnType<typeof getPQSubmissions>>]>
+ className?: string
+}
+
+export function PQSubmissionsTable({ promises, className }: PQSubmissionsTableProps) {
+ const [rowAction, setRowAction] = React.useState<DataTableRowAction<PQSubmission> | null>(null)
+ const [isFilterPanelOpen, setIsFilterPanelOpen] = React.useState(false)
+
+ // 방문실사 다이얼로그 상태
+ const [isSiteVisitDialogOpen, setIsSiteVisitDialogOpen] = React.useState(false)
+ const [selectedInvestigation, setSelectedInvestigation] = React.useState<PQSubmission | null>(null)
+ const [isVendorInfoViewDialogOpen, setIsVendorInfoViewDialogOpen] = React.useState(false)
+ const [selectedSiteVisitRequestId, setSelectedSiteVisitRequestId] = React.useState<number | null>(null)
+
+ // 실사 정보 수정 다이얼로그 상태
+ const [isEditInvestigationDialogOpen, setIsEditInvestigationDialogOpen] = React.useState(false)
+ const [selectedInvestigationForEdit, setSelectedInvestigationForEdit] = React.useState<PQSubmission | null>(null)
+
+ const router = useRouter()
+ const searchParams = useSearchParams()
+
+ // Container wrapper의 위치를 측정하기 위한 ref
+ const containerRef = React.useRef<HTMLDivElement>(null)
+ const [containerTop, setContainerTop] = React.useState(0)
+
+ // Container 위치 측정 함수 - top만 측정
+ 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])
+
+ // Suspense 방식으로 데이터 처리
+ const [promiseData] = React.use(promises)
+ const tableData = promiseData
+
+ // 디버깅용 로그
+ console.log("PQ Table Data:", {
+ dataLength: tableData.data?.length,
+ pageCount: tableData.pageCount,
+ sampleData: tableData.data?.[0]
+ })
+
+ // 방문실사 다이얼로그 핸들러
+ const handleSiteVisitRequest = async (data: {
+ inspectionDuration: number
+ requestedStartDate: Date
+ requestedEndDate: Date
+ shiAttendees: Record<string, boolean>
+ shiAttendeeDetails?: string
+ vendorRequests: Record<string, boolean>
+ otherVendorRequests?: string
+ additionalRequests?: string
+ }, attachments?: File[]) => {
+ try {
+ const result = await createSiteVisitRequestAction({
+ investigationId: selectedInvestigation?.investigation?.id || 0,
+ ...data,
+ attachments
+ })
+
+ if (result.success) {
+ toast.success(result.message || "방문실사 요청이 성공적으로 발송되었습니다.")
+ handleCloseSiteVisitDialog()
+ } else {
+ toast.error(result.error || "방문실사 요청 발송 중 오류가 발생했습니다.")
+ }
+ } catch (error) {
+ console.error("방문실사 요청 오류:", error)
+ toast.error("방문실사 요청 발송 중 오류가 발생했습니다.")
+ }
+ }
+
+ // 방문실사 다이얼로그 열기
+ const handleOpenSiteVisitDialog = async (investigation: PQSubmission) => {
+ try {
+ // 기존 방문실사 요청이 있는지 확인
+ const existingRequest = await getSiteVisitRequestAction(investigation.investigation?.id || 0)
+
+ if (existingRequest.success && existingRequest.data) {
+ toast.error("이미 방문실사 요청이 존재합니다. 추가 요청은 불가능합니다.")
+ return
+ }
+
+ setSelectedInvestigation(investigation)
+ setIsSiteVisitDialogOpen(true)
+ } catch (error) {
+ console.error("방문실사 요청 상태 확인 중 오류:", error)
+ toast.error("방문실사 요청 상태 확인 중 오류가 발생했습니다.")
+ }
+ }
+
+ // 방문실사 다이얼로그 닫기
+ const handleCloseSiteVisitDialog = () => {
+ setIsSiteVisitDialogOpen(false)
+ setSelectedInvestigation(null)
+ }
+
+ // 실사 정보 수정 핸들러
+ const handleEditInvestigation = async (data: {
+ confirmedAt?: Date
+ evaluationResult?: "APPROVED" | "SUPPLEMENT" | "REJECTED"
+ investigationNotes?: string
+ }) => {
+ if (!selectedInvestigationForEdit?.investigation?.id) {
+ toast.error("실사 정보를 찾을 수 없습니다.")
+ return
+ }
+
+ try {
+ const result = await updateInvestigationDetailsAction({
+ investigationId: selectedInvestigationForEdit.investigation.id,
+ ...data
+ })
+
+ if (result.success) {
+ toast.success(result.message || "실사 정보가 성공적으로 업데이트되었습니다.")
+ setIsEditInvestigationDialogOpen(false)
+ setSelectedInvestigationForEdit(null)
+ } else {
+ toast.error(result.error || "실사 정보 업데이트 중 오류가 발생했습니다.")
+ }
+ } catch (error) {
+ console.error("실사 정보 업데이트 오류:", error)
+ toast.error("실사 정보 업데이트 중 오류가 발생했습니다.")
+ }
+ }
+
+ // 실사 정보 수정 다이얼로그 닫기
+ const handleCloseEditInvestigationDialog = () => {
+ setIsEditInvestigationDialogOpen(false)
+ setSelectedInvestigationForEdit(null)
+ }
+
+ // rowAction 핸들러
+ React.useEffect(() => {
+ if (rowAction?.type === "site-visit") {
+ // 방문실사 다이얼로그 열기
+ handleOpenSiteVisitDialog(rowAction.row)
+ setRowAction(null)
+ } else if (rowAction?.type === "vendor-info-view") {
+ // 협력업체 정보 조회 다이얼로그 열기
+ setSelectedSiteVisitRequestId(rowAction.row.siteVisitRequestId || null)
+ setIsVendorInfoViewDialogOpen(true)
+ setRowAction(null)
+ } else if (rowAction?.type === "edit-investigation") {
+ // 실사 정보 수정 다이얼로그 열기
+ setSelectedInvestigationForEdit(rowAction.row)
+ setIsEditInvestigationDialogOpen(true)
+ setRowAction(null)
+ }
+ }, [rowAction])
+
+ // 초기 설정 정의 (RFQ와 동일한 패턴)
+ 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: "updatedAt", 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('pqBasicFilters') ?
+ JSON.parse(searchParams.get('basicFilters') || searchParams.get('pqBasicFilters')!) : [],
+ basicJoinOperator: (searchParams.get('basicJoinOperator') as "and" | "or") || "and",
+ search: searchParams.get('search') || '',
+ from: searchParams.get('from') || undefined,
+ to: searchParams.get('to') || undefined,
+ columnVisibility: {},
+ columnOrder: [],
+ pinnedColumns: { left: [], right: ["actions"] }, // PQ는 actions를 오른쪽에 고정
+ groupBy: [],
+ expandedRows: []
+ }), [searchParams])
+
+ // DB 기반 프리셋 훅 사용
+ const {
+ presets,
+ activePresetId,
+ hasUnsavedChanges,
+ isLoading: presetsLoading,
+ createPreset,
+ applyPreset,
+ updatePreset,
+ deletePreset,
+ setDefaultPreset,
+ renamePreset,
+ getCurrentSettings,
+ } = useTablePresets<PQSubmission>('pq-submissions-table', initialSettings)
+
+ const columns = React.useMemo(
+ () => getColumns({ setRowAction, router }),
+ [setRowAction, router]
+ )
+
+ // PQ 제출 필터링을 위한 필드 정의
+ const filterFields: DataTableFilterField<PQSubmission>[] = [
+ { id: "vendorName", label: "협력업체" },
+ { id: "projectName", label: "프로젝트" },
+ { id: "status", label: "상태" },
+ ]
+
+ // 고급 필터 필드 정의
+ const advancedFilterFields: DataTableAdvancedFilterField<PQSubmission>[] = [
+ { id: "requesterName", label: "요청자명", type: "text" },
+ { id: "pqNumber", label: "PQ 번호", type: "text" },
+ { id: "vendorName", label: "협력업체명", type: "text" },
+ { id: "vendorCode", label: "협력업체 코드", type: "text" },
+ { id: "type", label: "PQ 유형", type: "select", options: [
+ { label: "일반 PQ", value: "GENERAL" },
+ { label: "프로젝트 PQ", value: "PROJECT" },
+ ]},
+ { id: "projectName", label: "프로젝트명", type: "text" },
+ { id: "status", label: "PQ 상태", type: "select", options: [
+ { label: "요청됨", value: "REQUESTED" },
+ { label: "진행 중", value: "IN_PROGRESS" },
+ { label: "제출됨", value: "SUBMITTED" },
+ { label: "승인됨", value: "APPROVED" },
+ { label: "거부됨", value: "REJECTED" },
+ ]},
+
+ { id: "createdAt", label: "생성일", type: "date" },
+ { id: "submittedAt", label: "제출일", type: "date" },
+ { id: "approvedAt", label: "승인일", type: "date" },
+ { id: "rejectedAt", label: "거부일", type: "date" },
+ ]
+
+ // 현재 설정 가져오기
+ const currentSettings = useMemo(() => {
+ return getCurrentSettings()
+ }, [getCurrentSettings])
+
+ // useDataTable 초기 상태 설정 (RFQ와 동일한 패턴)
+ const initialState = useMemo(() => {
+ return {
+ sorting: initialSettings.sort.filter(sortItem => {
+ const columnExists = columns.some(col => col.accessorKey === sortItem.id)
+ return columnExists
+ }),
+ 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, // total 추가
+ filterFields, // RFQ와 달리 빈 배열이 아닌 실제 필터 필드 사용
+ enablePinning: true,
+ enableAdvancedFilter: true,
+ initialState,
+ getRowId: (originalRow) => String(originalRow.id),
+ shallow: false,
+ clearOnDefault: true,
+ })
+
+ // 조회 버튼 클릭 핸들러 - PQFilterSheet에 전달
+ const handleSearch = () => {
+ // Close the panel after search
+ setIsFilterPanelOpen(false)
+ }
+
+ // Get active basic filter count
+ const getActiveBasicFilterCount = () => {
+ try {
+ const basicFilters = searchParams.get('basicFilters') || searchParams.get('pqBasicFilters')
+ return basicFilters ? JSON.parse(basicFilters).length : 0
+ } catch {
+ return 0
+ }
+ }
+
+ // Filter panel width
+ const FILTER_PANEL_WIDTH = 400;
+
+ return (
+ <>
+ {/* Filter Panel - fixed positioning으로 화면 최대 좌측에서 시작 */}
+ <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)`
+ }}
+ >
+ {/* Filter Content */}
+ <div className="h-full">
+ <PQFilterSheet
+ 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">
+ {/* Main Content - 너비 조정으로 필터 패널 공간 확보 */}
+ <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>
+
+ {/* Right side info */}
+ <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">
+ {/* DB 기반 테이블 프리셋 매니저 추가 */}
+ <TablePresetManager<PQSubmission>
+ presets={presets}
+ activePresetId={activePresetId}
+ currentSettings={currentSettings}
+ hasUnsavedChanges={hasUnsavedChanges}
+ isLoading={presetsLoading}
+ onCreatePreset={createPreset}
+ onUpdatePreset={updatePreset}
+ onDeletePreset={deletePreset}
+ onApplyPreset={applyPreset}
+ onSetDefaultPreset={setDefaultPreset}
+ onRenamePreset={renamePreset}
+ />
+
+ {/* 기존 툴바 액션들 */}
+ <VendorsTableToolbarActions table={table} />
+ </div>
+ </DataTableAdvancedToolbar>
+ </DataTable>
+ </div>
+ </div>
+ </div>
+ </div>
+ </div>
+
+ {/* 방문실사 다이얼로그 */}
+ {selectedInvestigation && (
+ <SiteVisitDialog
+ isOpen={isSiteVisitDialogOpen}
+ onClose={handleCloseSiteVisitDialog}
+ onSubmit={handleSiteVisitRequest}
+ investigation={{
+ id: selectedInvestigation.investigation?.id || 0,
+ evaluationType: selectedInvestigation.investigation?.evaluationType as "PRODUCT_INSPECTION" | "SITE_VISIT_EVAL",
+ investigationMethod: selectedInvestigation.investigation?.investigationMethod,
+ investigationAddress: selectedInvestigation.investigation?.investigationAddress,
+ vendorName: selectedInvestigation.vendorName,
+ vendorCode: selectedInvestigation.vendorCode,
+ projectName: selectedInvestigation.projectName || undefined,
+ projectCode: selectedInvestigation.projectCode || undefined,
+ pqItems: selectedInvestigation.pqItems,
+ }}
+ />
+ )}
+
+ {/* 협력업체 정보 조회 다이얼로그 */}
+ <VendorInfoViewDialog
+ isOpen={isVendorInfoViewDialogOpen}
+ onClose={() => {
+ setIsVendorInfoViewDialogOpen(false)
+ setSelectedSiteVisitRequestId(null)
+ }}
+ siteVisitRequestId={selectedSiteVisitRequestId}
+ />
+
+ {/* 실사 정보 수정 다이얼로그 */}
+ <EditInvestigationDialog
+ isOpen={isEditInvestigationDialogOpen}
+ onClose={handleCloseEditInvestigationDialog}
+ investigation={selectedInvestigationForEdit?.investigation || null}
+ onSubmit={handleEditInvestigation}
+ />
+ </>
+ )
} \ No newline at end of file