From 14f61e24947fb92dd71ec0a7196a6e815f8e66da Mon Sep 17 00:00:00 2001 From: dujinkim Date: Mon, 21 Jul 2025 07:54:26 +0000 Subject: (최겸)기술영업 RFQ 담당자 초대, 요구사항 반영 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- lib/techsales-rfq/table/rfq-table.tsx | 1223 +++++++++++++++++---------------- 1 file changed, 635 insertions(+), 588 deletions(-) (limited to 'lib/techsales-rfq/table/rfq-table.tsx') diff --git a/lib/techsales-rfq/table/rfq-table.tsx b/lib/techsales-rfq/table/rfq-table.tsx index 615753cd..e3551625 100644 --- a/lib/techsales-rfq/table/rfq-table.tsx +++ b/lib/techsales-rfq/table/rfq-table.tsx @@ -1,589 +1,636 @@ -"use client" - -import * as React from "react" -import { useSearchParams } from "next/navigation" -import { Button } from "@/components/ui/button" -import { PanelLeftClose, PanelLeftOpen } from "lucide-react" -import type { - DataTableAdvancedFilterField, - DataTableRowAction, -} from "@/types/table" -import { - ResizablePanelGroup, - ResizablePanel, - ResizableHandle, -} from "@/components/ui/resizable" - -import { useDataTable } from "@/hooks/use-data-table" -import { DataTable } from "@/components/data-table/data-table" -import { getColumns } from "./rfq-table-column" -import { useEffect, useMemo } from "react" -import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar" -import { RFQTableToolbarActions } from "./rfq-table-toolbar-actions" -import { getTechSalesRfqsWithJoin, getTechSalesRfqAttachments } from "@/lib/techsales-rfq/service" -import { toast } from "sonner" -import { useTablePresets } from "@/components/data-table/use-table-presets" -import { RfqDetailTables } from "./detail-table/rfq-detail-table" -import { cn } from "@/lib/utils" -import { ProjectDetailDialog } from "./project-detail-dialog" -import { RFQFilterSheet } from "./rfq-filter-sheet" -import { TechSalesRfqAttachmentsSheet, ExistingTechSalesAttachment } from "./tech-sales-rfq-attachments-sheet" -import { RfqItemsViewDialog } from "./rfq-items-view-dialog" -// 기본적인 RFQ 타입 정의 (repository selectTechSalesRfqsWithJoin 반환 타입에 맞춤) -interface TechSalesRfq { - id: number - rfqCode: string | null - biddingProjectId: number | null - materialCode: string | null - dueDate: Date - rfqSendDate: Date | null - status: "RFQ Created" | "RFQ Vendor Assignned" | "RFQ Sent" | "Quotation Analysis" | "Closed" - description: string | null - remark: string | null - cancelReason: string | null - createdAt: Date - updatedAt: Date - createdBy: number | null - createdByName: string - updatedBy: number | null - updatedByName: string - sentBy: number | null - sentByName: string | null - // 조인된 프로젝트 정보 - pspid: string - projNm: string - sector: string - projMsrm: number - ptypeNm: string - attachmentCount: number - quotationCount: number - rfqType: "SHIP" | "TOP" | "HULL" | null - // 필요에 따라 다른 필드들 추가 - [key: string]: unknown -} - -interface RFQListTableProps { - promises: Promise<[Awaited>]> - className?: string; - calculatedHeight?: string; // 계산된 높이 추가 - rfqType: "SHIP" | "TOP" | "HULL"; -} - -export function RFQListTable({ - promises, - className, - calculatedHeight, - rfqType -}: RFQListTableProps) { - const searchParams = useSearchParams() - - // 필터 패널 상태 - const [isFilterPanelOpen, setIsFilterPanelOpen] = React.useState(false) - - // 선택된 RFQ 상태 - const [selectedRfq, setSelectedRfq] = React.useState(null) - - // 프로젝트 상세정보 다이얼로그 상태 - const [isProjectDetailOpen, setIsProjectDetailOpen] = React.useState(false) - const [projectDetailRfq, setProjectDetailRfq] = React.useState(null) - - // 첨부파일 시트 상태 - const [attachmentsOpen, setAttachmentsOpen] = React.useState(false) - const [selectedRfqForAttachments, setSelectedRfqForAttachments] = React.useState(null) - const [attachmentsDefault, setAttachmentsDefault] = React.useState([]) - - // 아이템 다이얼로그 상태 - const [itemsDialogOpen, setItemsDialogOpen] = React.useState(false) - const [selectedRfqForItems, setSelectedRfqForItems] = React.useState(null) - - // 패널 collapse 상태 - const [panelHeight, setPanelHeight] = React.useState(55) - - // 고정 높이 설정을 위한 상수 (실제 측정값으로 조정 필요) - const LAYOUT_HEADER_HEIGHT = 64 // Layout Header 높이 - const LAYOUT_FOOTER_HEIGHT = 60 // Layout Footer 높이 (있다면 실제 값) - const LOCAL_HEADER_HEIGHT = 72 // 로컬 헤더 바 높이 (p-4 + border) - const FILTER_PANEL_WIDTH = 400 // 필터 패널 너비 - - // 높이 계산 - // 필터 패널 높이 - Layout Header와 Footer 사이 - const FIXED_FILTER_HEIGHT = `calc(100vh - ${LAYOUT_HEADER_HEIGHT*2}px)` - - console.log(calculatedHeight) - - // 테이블 컨텐츠 높이 - 전달받은 높이에서 로컬 헤더 제외 - const FIXED_TABLE_HEIGHT = calculatedHeight - ? `calc(${calculatedHeight} - ${LOCAL_HEADER_HEIGHT}px)` - : `calc(100vh - ${LAYOUT_HEADER_HEIGHT + LAYOUT_FOOTER_HEIGHT + LOCAL_HEADER_HEIGHT+76}px)` // fallback - - // Suspense 방식으로 데이터 처리 - const [promiseData] = React.use(promises) - const tableData = promiseData - - const [rowAction, setRowAction] = React.useState | null>(null) - - // 초기 설정 정의 - 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') ? JSON.parse(searchParams.get('basicFilters')!) : [], - 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: ["items", "attachments", "tbe-attachments", "cbe-attachments"] }, - groupBy: [], - expandedRows: [] - }), [searchParams]) - - // DB 기반 프리셋 훅 사용 - const { - // presets, - // activePresetId, - // hasUnsavedChanges, - // isLoading: presetsLoading, - // createPreset, - // applyPreset, - // updatePreset, - // deletePreset, - // setDefaultPreset, - // renamePreset, - getCurrentSettings, - } = useTablePresets('rfq-list-table', initialSettings) - - // 조회 버튼 클릭 핸들러 - const handleSearch = () => { - setIsFilterPanelOpen(false) - } - - // 행 액션 처리 - useEffect(() => { - if (rowAction) { - switch (rowAction.type) { - case "select": - // 객체 참조 안정화를 위해 필요한 필드만 추출 - const rfqData = rowAction.row.original; - setSelectedRfq({ - id: rfqData.id, - rfqCode: rfqData.rfqCode, - rfqType: rfqData.rfqType, // 빠뜨린 rfqType 필드 추가 - biddingProjectId: rfqData.biddingProjectId, - materialCode: rfqData.materialCode, - dueDate: rfqData.dueDate, - rfqSendDate: rfqData.rfqSendDate, - status: rfqData.status, - description: rfqData.description, - remark: rfqData.remark, - cancelReason: rfqData.cancelReason, - createdAt: rfqData.createdAt, - updatedAt: rfqData.updatedAt, - createdBy: rfqData.createdBy, - createdByName: rfqData.createdByName, - updatedBy: rfqData.updatedBy, - updatedByName: rfqData.updatedByName, - sentBy: rfqData.sentBy, - sentByName: rfqData.sentByName, - pspid: rfqData.pspid, - projNm: rfqData.projNm, - sector: rfqData.sector, - projMsrm: rfqData.projMsrm, - ptypeNm: rfqData.ptypeNm, - attachmentCount: rfqData.attachmentCount, - quotationCount: rfqData.quotationCount, - }); - break; - case "view": - // 프로젝트 상세정보 다이얼로그 열기 - const projectRfqData = rowAction.row.original; - setProjectDetailRfq({ - id: projectRfqData.id, - rfqCode: projectRfqData.rfqCode, - rfqType: projectRfqData.rfqType, // 빠뜨린 rfqType 필드 추가 - biddingProjectId: projectRfqData.biddingProjectId, - materialCode: projectRfqData.materialCode, - dueDate: projectRfqData.dueDate, - rfqSendDate: projectRfqData.rfqSendDate, - status: projectRfqData.status, - description: projectRfqData.description, - remark: projectRfqData.remark, - cancelReason: projectRfqData.cancelReason, - createdAt: projectRfqData.createdAt, - updatedAt: projectRfqData.updatedAt, - createdBy: projectRfqData.createdBy, - createdByName: projectRfqData.createdByName, - updatedBy: projectRfqData.updatedBy, - updatedByName: projectRfqData.updatedByName, - sentBy: projectRfqData.sentBy, - sentByName: projectRfqData.sentByName, - pspid: projectRfqData.pspid, - projNm: projectRfqData.projNm, - sector: projectRfqData.sector, - projMsrm: projectRfqData.projMsrm, - ptypeNm: projectRfqData.ptypeNm, - attachmentCount: projectRfqData.attachmentCount, - quotationCount: projectRfqData.quotationCount, - }); - setIsProjectDetailOpen(true); - break; - case "update": - console.log("Update rfq:", rowAction.row.original) - break; - case "delete": - console.log("Delete rfq:", rowAction.row.original) - break; - } - setRowAction(null) - } - }, [rowAction]) - - // 첨부파일 시트 상태에 타입 추가 - const [attachmentType, setAttachmentType] = React.useState<"RFQ_COMMON" | "TBE_RESULT" | "CBE_RESULT">("RFQ_COMMON") - - // 첨부파일 시트 열기 함수 - const openAttachmentsSheet = React.useCallback(async (rfqId: number, attachmentType: 'RFQ_COMMON' | 'TBE_RESULT' | 'CBE_RESULT' = 'RFQ_COMMON') => { - try { - // 선택된 RFQ 찾기 - const rfq = tableData?.data?.find(r => r.id === rfqId) - if (!rfq) { - toast.error("RFQ를 찾을 수 없습니다.") - return - } - - // attachmentType을 RFQ_COMMON, TBE_RESULT, CBE_RESULT 중 하나로 변환 - const validAttachmentType=attachmentType as "RFQ_COMMON" | "TBE_RESULT" | "CBE_RESULT" - - // 실제 첨부파일 목록 조회 API 호출 - const result = await getTechSalesRfqAttachments(rfqId) - - if (result.error) { - toast.error(result.error) - return - } - - // 해당 타입의 첨부파일만 필터링 - const filteredAttachments = result.data.filter(att => att.attachmentType === validAttachmentType) - - // API 응답을 ExistingTechSalesAttachment 형식으로 변환 - const attachments: ExistingTechSalesAttachment[] = filteredAttachments.map(att => ({ - id: att.id, - techSalesRfqId: att.techSalesRfqId || rfqId, // null인 경우 rfqId 사용 - fileName: att.fileName, - originalFileName: att.originalFileName, - filePath: att.filePath, - fileSize: att.fileSize || undefined, - fileType: att.fileType || undefined, - attachmentType: att.attachmentType as "RFQ_COMMON" | "TBE_RESULT" | "CBE_RESULT", - description: att.description || undefined, - createdBy: att.createdBy, - createdAt: att.createdAt, - })) - - setAttachmentType(validAttachmentType) - setAttachmentsDefault(attachments) - setSelectedRfqForAttachments(rfq as unknown as TechSalesRfq) - setAttachmentsOpen(true) - } catch (error) { - console.error("첨부파일 조회 오류:", error) - toast.error("첨부파일 조회 중 오류가 발생했습니다.") - } - }, [tableData?.data]) - - // 첨부파일 업데이트 콜백 - const handleAttachmentsUpdated = React.useCallback((rfqId: number, newAttachmentCount: number) => { - // Service에서 이미 revalidateTag와 revalidatePath로 캐시 무효화 처리됨 - console.log(`RFQ ${rfqId}의 첨부파일 개수가 ${newAttachmentCount}개로 업데이트됨`) - - // 성공 피드백 - setTimeout(() => { - toast.success(`첨부파일 개수가 업데이트되었습니다. (${newAttachmentCount}개)`, { - duration: 3000 - }) - }, 500) - }, []) - - // 아이템 다이얼로그 열기 함수 - const openItemsDialog = React.useCallback((rfq: TechSalesRfq) => { - console.log("Opening items dialog for RFQ:", rfq.id, rfq) - setSelectedRfqForItems(rfq as unknown as TechSalesRfq) - setItemsDialogOpen(true) - }, []) - - const columns = React.useMemo( - () => getColumns({ - setRowAction, - openAttachmentsSheet, - openItemsDialog - }), - [openAttachmentsSheet, openItemsDialog] - ) - - // 고급 필터 필드 정의 - const advancedFilterFields: DataTableAdvancedFilterField[] = [ - { - id: "rfqCode", - label: "RFQ No.", - type: "text", - }, - { - id: "description", - label: "설명", - type: "text", - }, - { - id: "projNm", - label: "프로젝트명", - type: "text", - }, - { - id: "rfqSendDate", - label: "RFQ 전송일", - type: "date", - }, - { - id: "dueDate", - label: "RFQ 마감일", - type: "date", - }, - { - id: "createdByName", - label: "요청자", - type: "text", - }, - { - id: "status", - label: "상태", - type: "text", - }, - ] - - // 현재 설정 가져오기 - const currentSettings = useMemo(() => { - return getCurrentSettings() - }, [getCurrentSettings]) - - // useDataTable 초기 상태 설정 - const initialState = useMemo(() => { - return { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - sorting: initialSettings.sort.filter((sortItem: any) => { - // eslint-disable-next-line @typescript-eslint/no-explicit-any - const columnExists = columns.some((col: any) => col.accessorKey === sortItem.id) - return columnExists - // eslint-disable-next-line @typescript-eslint/no-explicit-any - }) as any, - columnVisibility: currentSettings.columnVisibility, - columnPinning: currentSettings.pinnedColumns, - } - }, [currentSettings, initialSettings.sort, columns]) - - // useDataTable 훅 설정 - const { table } = useDataTable({ - data: tableData?.data || [], - // eslint-disable-next-line @typescript-eslint/no-explicit-any - columns: columns as any, - pageCount: tableData?.pageCount || 0, - rowCount: tableData?.total || 0, - filterFields: [], - enablePinning: true, - enableAdvancedFilter: true, - initialState, - getRowId: (originalRow) => String(originalRow.id), - shallow: false, - clearOnDefault: true, - columnResizeMode: "onEnd", - }) - - // Get active basic filter count - const getActiveBasicFilterCount = () => { - try { - const basicFilters = searchParams?.get('basicFilters') - return basicFilters ? JSON.parse(basicFilters).length : 0 - } catch { - return 0 - } - } - - console.log(panelHeight) - - return ( -
- {/* Filter Panel - 계산된 높이 적용 */} -
- {/* Filter Content */} -
- setIsFilterPanelOpen(false)} - onSearch={handleSearch} - isLoading={false} - /> -
-
- - {/* Main Content */} -
- {/* Header Bar - 고정 높이 */} -
-
- -
- - {/* Right side info */} -
- {tableData && ( - 총 {tableData.total || 0}건 - )} -
-
- - {/* Table Content Area - 계산된 높이 사용 */} -
- - { - setPanelHeight(size) - }} - className="flex flex-col overflow-hidden" - > - {/* 상단 테이블 영역 */} -
- - -
- {/* 아직 개발/테스트 전이라서 주석처리함. 지우지 말 것! 미사용 린터 에러는 무시. */} - {/* - presets={presets} - activePresetId={activePresetId} - currentSettings={currentSettings} - hasUnsavedChanges={hasUnsavedChanges} - isLoading={presetsLoading} - onCreatePreset={createPreset} - onUpdatePreset={updatePreset} - onDeletePreset={deletePreset} - onApplyPreset={applyPreset} - onSetDefaultPreset={setDefaultPreset} - onRenamePreset={renamePreset} - /> */} - - {}} - rfqType={rfqType} - /> -
-
-
-
-
- - - - - {/* 하단 상세 테이블 영역 */} -
- -
-
-
-
-
- - {/* 프로젝트 상세정보 다이얼로그 */} - - - {/* 첨부파일 관리 시트 */} - - - {/* 아이템 보기 다이얼로그 */} - -
- ) +"use client" + +import * as React from "react" +import { useSearchParams } from "next/navigation" +import { Button } from "@/components/ui/button" +import { PanelLeftClose, PanelLeftOpen } from "lucide-react" +import type { + DataTableAdvancedFilterField, + DataTableRowAction, +} from "@/types/table" +import { + ResizablePanelGroup, + ResizablePanel, + ResizableHandle, +} from "@/components/ui/resizable" + +import { useDataTable } from "@/hooks/use-data-table" +import { DataTable } from "@/components/data-table/data-table" +import { getColumns } from "./rfq-table-column" +import { useEffect, useMemo } from "react" +import { DataTableAdvancedToolbar } from "@/components/data-table/data-table-advanced-toolbar" +import { RFQTableToolbarActions } from "./rfq-table-toolbar-actions" +import { getTechSalesRfqsWithJoin, getTechSalesRfqAttachments } from "@/lib/techsales-rfq/service" +import { toast } from "sonner" +import { useTablePresets } from "@/components/data-table/use-table-presets" +import { RfqDetailTables } from "./detail-table/rfq-detail-table" +import { cn } from "@/lib/utils" +import { ProjectDetailDialog } from "./project-detail-dialog" +import { RFQFilterSheet } from "./rfq-filter-sheet" +import { TechSalesRfqAttachmentsSheet, ExistingTechSalesAttachment } from "./tech-sales-rfq-attachments-sheet" +import { RfqItemsViewDialog } from "./rfq-items-view-dialog" +// 기본적인 RFQ 타입 정의 (repository selectTechSalesRfqsWithJoin 반환 타입에 맞춤) +export interface TechSalesRfq { + id: number + rfqCode: string | null + biddingProjectId: number | null + materialCode: string | null + dueDate: Date + rfqSendDate: Date | null + status: "RFQ Created" | "RFQ Vendor Assignned" | "RFQ Sent" | "Quotation Analysis" | "Closed" + description: string | null + picCode: string | null + remark: string | null + cancelReason: string | null + createdAt: Date + updatedAt: Date + createdBy: number | null + createdByName: string + updatedBy: number | null + updatedByName: string + sentBy: number | null + sentByName: string | null + // 조인된 프로젝트 정보 + pspid: string + projNm: string + sector: string + projMsrm: number + ptypeNm: string + attachmentCount: number + hasTbeAttachments: boolean + hasCbeAttachments: boolean + quotationCount: number + rfqType: "SHIP" | "TOP" | "HULL" | null + itemCount: number + workTypes: string | null + // 필요에 따라 다른 필드들 추가 + [key: string]: unknown +} + +interface RFQListTableProps { + promises: Promise<[Awaited>]> + className?: string; + calculatedHeight?: string; // 계산된 높이 추가 + rfqType: "SHIP" | "TOP" | "HULL"; +} + +export function RFQListTable({ + promises, + className, + calculatedHeight, + rfqType +}: RFQListTableProps) { + const searchParams = useSearchParams() + + // 필터 패널 상태 + const [isFilterPanelOpen, setIsFilterPanelOpen] = React.useState(false) + + // 선택된 RFQ 상태 + const [selectedRfq, setSelectedRfq] = React.useState(null) + + // 프로젝트 상세정보 다이얼로그 상태 + const [isProjectDetailOpen, setIsProjectDetailOpen] = React.useState(false) + const [projectDetailRfq, setProjectDetailRfq] = React.useState(null) + + // 첨부파일 시트 상태 + const [attachmentsOpen, setAttachmentsOpen] = React.useState(false) + const [selectedRfqForAttachments, setSelectedRfqForAttachments] = React.useState(null) + const [attachmentsDefault, setAttachmentsDefault] = React.useState([]) + + // 아이템 다이얼로그 상태 + const [itemsDialogOpen, setItemsDialogOpen] = React.useState(false) + const [selectedRfqForItems, setSelectedRfqForItems] = React.useState(null) + + // 패널 collapse 상태 + const [panelHeight, setPanelHeight] = React.useState(55) + + // 고정 높이 설정을 위한 상수 (실제 측정값으로 조정 필요) + const LAYOUT_HEADER_HEIGHT = 64 // Layout Header 높이 + const LAYOUT_FOOTER_HEIGHT = 60 // Layout Footer 높이 (있다면 실제 값) + const LOCAL_HEADER_HEIGHT = 72 // 로컬 헤더 바 높이 (p-4 + border) + const FILTER_PANEL_WIDTH = 400 // 필터 패널 너비 + + // 높이 계산 + // 필터 패널 높이 - Layout Header와 Footer 사이 + const FIXED_FILTER_HEIGHT = `calc(100vh - ${LAYOUT_HEADER_HEIGHT*2}px)` + + console.log(calculatedHeight) + + // 테이블 컨텐츠 높이 - 전달받은 높이에서 로컬 헤더 제외 + const FIXED_TABLE_HEIGHT = calculatedHeight + ? `calc(${calculatedHeight} - ${LOCAL_HEADER_HEIGHT}px)` + : `calc(100vh - ${LAYOUT_HEADER_HEIGHT + LAYOUT_FOOTER_HEIGHT + LOCAL_HEADER_HEIGHT+76}px)` // fallback + + // Suspense 방식으로 데이터 처리 + const [promiseData] = React.use(promises) + const tableData = promiseData + + const [rowAction, setRowAction] = React.useState | null>(null) + + // 초기 설정 정의 + 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') ? JSON.parse(searchParams.get('basicFilters')!) : [], + 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: ["items", "attachments", "tbe-attachments", "cbe-attachments"] }, + groupBy: [], + expandedRows: [] + }), [searchParams]) + + // DB 기반 프리셋 훅 사용 + const { + // presets, + // activePresetId, + // hasUnsavedChanges, + // isLoading: presetsLoading, + // createPreset, + // applyPreset, + // updatePreset, + // deletePreset, + // setDefaultPreset, + // renamePreset, + getCurrentSettings, + } = useTablePresets('rfq-list-table', initialSettings) + + // 조회 버튼 클릭 핸들러 + const handleSearch = () => { + setIsFilterPanelOpen(false) + } + + // 행 액션 처리 + useEffect(() => { + if (rowAction) { + switch (rowAction.type) { + case "select": + // 객체 참조 안정화를 위해 필요한 필드만 추출 + const rfqData = rowAction.row.original; + setSelectedRfq({ + id: rfqData.id, + rfqCode: rfqData.rfqCode, + rfqType: rfqData.rfqType, // 빠뜨린 rfqType 필드 추가 + biddingProjectId: rfqData.biddingProjectId, + hasTbeAttachments: rfqData.hasTbeAttachments, + hasCbeAttachments: rfqData.hasCbeAttachments, + materialCode: rfqData.materialCode, + dueDate: rfqData.dueDate, + rfqSendDate: rfqData.rfqSendDate, + status: rfqData.status, + description: rfqData.description, + picCode: rfqData.picCode, + remark: rfqData.remark, + cancelReason: rfqData.cancelReason, + createdAt: rfqData.createdAt, + updatedAt: rfqData.updatedAt, + createdBy: rfqData.createdBy, + createdByName: rfqData.createdByName, + updatedBy: rfqData.updatedBy, + updatedByName: rfqData.updatedByName, + sentBy: rfqData.sentBy, + sentByName: rfqData.sentByName, + pspid: rfqData.pspid, + projNm: rfqData.projNm, + sector: rfqData.sector, + projMsrm: rfqData.projMsrm, + ptypeNm: rfqData.ptypeNm, + attachmentCount: rfqData.attachmentCount, + quotationCount: rfqData.quotationCount, + itemCount: rfqData.itemCount, + workTypes: rfqData.workTypes, + }); + break; + case "view": + // 프로젝트 상세정보 다이얼로그 열기 + const projectRfqData = rowAction.row.original; + setProjectDetailRfq({ + id: projectRfqData.id, + rfqCode: projectRfqData.rfqCode, + rfqType: projectRfqData.rfqType, // 빠뜨린 rfqType 필드 추가 + biddingProjectId: projectRfqData.biddingProjectId, + hasTbeAttachments: projectRfqData.hasTbeAttachments, + hasCbeAttachments: projectRfqData.hasCbeAttachments, + materialCode: projectRfqData.materialCode, + dueDate: projectRfqData.dueDate, + rfqSendDate: projectRfqData.rfqSendDate, + status: projectRfqData.status, + description: projectRfqData.description, + picCode: projectRfqData.picCode, + remark: projectRfqData.remark, + cancelReason: projectRfqData.cancelReason, + createdAt: projectRfqData.createdAt, + updatedAt: projectRfqData.updatedAt, + createdBy: projectRfqData.createdBy, + createdByName: projectRfqData.createdByName, + updatedBy: projectRfqData.updatedBy, + updatedByName: projectRfqData.updatedByName, + sentBy: projectRfqData.sentBy, + sentByName: projectRfqData.sentByName, + pspid: projectRfqData.pspid, + projNm: projectRfqData.projNm, + sector: projectRfqData.sector, + projMsrm: projectRfqData.projMsrm, + ptypeNm: projectRfqData.ptypeNm, + attachmentCount: projectRfqData.attachmentCount, + quotationCount: projectRfqData.quotationCount, + itemCount: projectRfqData.itemCount, + workTypes: projectRfqData.workTypes, + }); + setIsProjectDetailOpen(true); + break; + case "update": + console.log("Update rfq:", rowAction.row.original) + break; + case "delete": + console.log("Delete rfq:", rowAction.row.original) + break; + } + setRowAction(null) + } + }, [rowAction]) + + // 첨부파일 시트 상태에 타입 추가 + const [attachmentType, setAttachmentType] = React.useState<"RFQ_COMMON" | "TBE_RESULT" | "CBE_RESULT">("RFQ_COMMON") + + // 첨부파일 시트 열기 함수 + const openAttachmentsSheet = React.useCallback(async (rfqId: number, attachmentType: 'RFQ_COMMON' | 'TBE_RESULT' | 'CBE_RESULT' = 'RFQ_COMMON') => { + try { + // 선택된 RFQ 찾기 + const rfq = tableData?.data?.find(r => r.id === rfqId) + if (!rfq) { + toast.error("RFQ를 찾을 수 없습니다.") + return + } + + // attachmentType을 RFQ_COMMON, TBE_RESULT, CBE_RESULT 중 하나로 변환 + const validAttachmentType=attachmentType as "RFQ_COMMON" | "TBE_RESULT" | "CBE_RESULT" + + // 실제 첨부파일 목록 조회 API 호출 + const result = await getTechSalesRfqAttachments(rfqId) + + if (result.error) { + toast.error(result.error) + return + } + + // 해당 타입의 첨부파일만 필터링 + const filteredAttachments = result.data.filter(att => att.attachmentType === validAttachmentType) + + // API 응답을 ExistingTechSalesAttachment 형식으로 변환 + const attachments: ExistingTechSalesAttachment[] = filteredAttachments.map(att => ({ + id: att.id, + techSalesRfqId: att.techSalesRfqId || rfqId, // null인 경우 rfqId 사용 + fileName: att.fileName, + originalFileName: att.originalFileName, + filePath: att.filePath, + fileSize: att.fileSize || undefined, + fileType: att.fileType || undefined, + attachmentType: att.attachmentType as "RFQ_COMMON" | "TBE_RESULT" | "CBE_RESULT", + description: att.description || undefined, + createdBy: att.createdBy, + createdAt: att.createdAt, + })) + + setAttachmentType(validAttachmentType) + setAttachmentsDefault(attachments) + setSelectedRfqForAttachments(rfq as unknown as TechSalesRfq) + setAttachmentsOpen(true) + } catch (error) { + console.error("첨부파일 조회 오류:", error) + toast.error("첨부파일 조회 중 오류가 발생했습니다.") + } + }, [tableData?.data]) + + // // 첨부파일 업데이트 콜백 + // const handleAttachmentsUpdated = React.useCallback((rfqId: number, newAttachmentCount: number) => { + // // Service에서 이미 revalidateTag와 revalidatePath로 캐시 무효화 처리됨 + // console.log(`RFQ ${rfqId}의 첨부파일 개수가 ${newAttachmentCount}개로 업데이트됨`) + + // // 성공 피드백 + // setTimeout(() => { + // toast.success(`첨부파일 개수가 업데이트되었습니다. (${newAttachmentCount}개)`, { + // duration: 3000 + // }) + // }, 500) + // }, []) + + // 아이템 다이얼로그 열기 함수 + const openItemsDialog = React.useCallback((rfq: TechSalesRfq) => { + console.log("Opening items dialog for RFQ:", rfq.id, rfq) + setSelectedRfqForItems(rfq) + setItemsDialogOpen(true) + }, []) + + const columns = React.useMemo( + () => getColumns({ + setRowAction, + openAttachmentsSheet, + openItemsDialog + }), + [openAttachmentsSheet, openItemsDialog, setRowAction] + ) + + // 고급 필터 필드 정의 + const advancedFilterFields: DataTableAdvancedFilterField[] = [ + { + id: "rfqCode", + label: "RFQ No.", + type: "text", + }, + { + id: "description", + label: "설명", + type: "text", + }, + { + id: "projNm", + label: "프로젝트명", + type: "text", + }, + { + id: "rfqSendDate", + label: "RFQ 전송일", + type: "date", + }, + { + id: "dueDate", + label: "RFQ 마감일", + type: "date", + }, + { + id: "createdByName", + label: "요청자", + type: "text", + }, + { + id: "status", + label: "상태", + type: "text", + }, + { + id: "workTypes", + label: "Work Type", + type: "multi-select", + options: [ + // 조선 workTypes + { label: "기장", value: "기장" }, + { label: "전장", value: "전장" }, + { label: "선실", value: "선실" }, + { label: "배관", value: "배관" }, + { label: "철의", value: "철의" }, + { label: "선체", value: "선체" }, + // 해양TOP workTypes + { label: "TM", value: "TM" }, + { label: "TS", value: "TS" }, + { label: "TE", value: "TE" }, + { label: "TP", value: "TP" }, + // 해양HULL workTypes + { label: "HA", value: "HA" }, + { label: "HE", value: "HE" }, + { label: "HH", value: "HH" }, + { label: "HM", value: "HM" }, + { label: "NC", value: "NC" }, + { label: "HO", value: "HO" }, + { label: "HP", value: "HP" }, + ], + }, + ] + + // 현재 설정 가져오기 + const currentSettings = useMemo(() => { + return getCurrentSettings() + }, [getCurrentSettings]) + + // useDataTable 초기 상태 설정 + const initialState = useMemo(() => { + return { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + sorting: initialSettings.sort.filter((sortItem: any) => { + // eslint-disable-next-line @typescript-eslint/no-explicit-any + const columnExists = columns.some((col: any) => col.accessorKey === sortItem.id) + return columnExists + // eslint-disable-next-line @typescript-eslint/no-explicit-any + }) as any, + columnVisibility: currentSettings.columnVisibility, + columnPinning: currentSettings.pinnedColumns, + } + }, [currentSettings, initialSettings.sort, columns]) + + // useDataTable 훅 설정 + const { table } = useDataTable({ + data: tableData?.data || [], + // eslint-disable-next-line @typescript-eslint/no-explicit-any + columns: columns as any, + pageCount: tableData?.pageCount || 0, + rowCount: tableData?.total || 0, + filterFields: [], + enablePinning: true, + enableAdvancedFilter: true, + initialState, + getRowId: (originalRow) => String(originalRow.id), + shallow: false, + clearOnDefault: true, + columnResizeMode: "onEnd", + }) + + // Get active basic filter count + const getActiveBasicFilterCount = () => { + try { + const basicFilters = searchParams?.get('basicFilters') + return basicFilters ? JSON.parse(basicFilters).length : 0 + } catch { + return 0 + } + } + + console.log(panelHeight) + + return ( +
+ {/* Filter Panel - 계산된 높이 적용 */} +
+ {/* Filter Content */} +
+ setIsFilterPanelOpen(false)} + onSearch={handleSearch} + isLoading={false} + /> +
+
+ + {/* Main Content */} +
+ {/* Header Bar - 고정 높이 */} +
+
+ +
+ + {/* Right side info */} +
+ {tableData && ( + 총 {tableData.total || 0}건 + )} +
+
+ + {/* Table Content Area - 계산된 높이 사용 */} +
+ + { + setPanelHeight(size) + }} + className="flex flex-col overflow-hidden" + > + {/* 상단 테이블 영역 */} +
+ + +
+ {/* 아직 개발/테스트 전이라서 주석처리함. 지우지 말 것! 미사용 린터 에러는 무시. */} + {/* + presets={presets} + activePresetId={activePresetId} + currentSettings={currentSettings} + hasUnsavedChanges={hasUnsavedChanges} + isLoading={presetsLoading} + onCreatePreset={createPreset} + onUpdatePreset={updatePreset} + onDeletePreset={deletePreset} + onApplyPreset={applyPreset} + onSetDefaultPreset={setDefaultPreset} + onRenamePreset={renamePreset} + /> */} + + {}} + rfqType={rfqType} + /> +
+
+
+
+
+ + + + + {/* 하단 상세 테이블 영역 */} +
+ +
+
+
+
+
+ + {/* 프로젝트 상세정보 다이얼로그 */} + + + {/* 첨부파일 관리 시트 */} + + + {/* 아이템 보기 다이얼로그 */} + +
+ ) } \ No newline at end of file -- cgit v1.2.3