diff options
| author | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-05-29 05:17:13 +0000 |
|---|---|---|
| committer | dujinkim <dujin.kim@dtsolution.co.kr> | 2025-05-29 05:17:13 +0000 |
| commit | 37f55540833c2d5894513eca9fc8f7c6233fc2d2 (patch) | |
| tree | 6807978e7150358b3444c33b825c83e2c9cda8e8 /lib/tech-vendor-rfq-response/vendor-cbe-table/cbe-table.tsx | |
| parent | 4b9bdb29e637f67761beb2db7f75dab0432d6712 (diff) | |
(대표님) 0529 14시 16분 변경사항 저장 (Vendor Data, Docu)
Diffstat (limited to 'lib/tech-vendor-rfq-response/vendor-cbe-table/cbe-table.tsx')
| -rw-r--r-- | lib/tech-vendor-rfq-response/vendor-cbe-table/cbe-table.tsx | 272 |
1 files changed, 272 insertions, 0 deletions
diff --git a/lib/tech-vendor-rfq-response/vendor-cbe-table/cbe-table.tsx b/lib/tech-vendor-rfq-response/vendor-cbe-table/cbe-table.tsx new file mode 100644 index 00000000..94e29a95 --- /dev/null +++ b/lib/tech-vendor-rfq-response/vendor-cbe-table/cbe-table.tsx @@ -0,0 +1,272 @@ +"use client" + +import * as React from "react" +import { useRouter } from "next/navigation" +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 { getColumns } from "./cbe-table-columns" +import { + fetchRfqAttachmentsbyCommentId, + getCBEbyVendorId, + getFileFromRfqAttachmentsbyid, + fetchCbeFiles +} from "../../rfqs-tech/service" +import { useSession } from "next-auth/react" +import { CbeComment, CommentSheet } from "./comments-sheet" +import { VendorWithCbeFields } from "@/config/vendorCbeColumnsConfig" +import { toast } from "sonner" +import { RfqDeailDialog } from "./rfq-detail-dialog" +import { CommercialResponseSheet } from "./respond-cbe-sheet" + +interface VendorsTableProps { + promises: Promise< + [ + Awaited<ReturnType<typeof getCBEbyVendorId>>, + ] + > +} + +export function CbeVendorTable({ promises }: VendorsTableProps) { + const { data: session } = useSession() + const userVendorId = session?.user?.companyId + const userId = Number(session?.user?.id) + // Suspense로 받아온 데이터 + const [{ data, pageCount }] = React.use(promises) + const [rowAction, setRowAction] = React.useState<DataTableRowAction<VendorWithCbeFields> | null>(null) + const [selectedCbeId, setSelectedCbeId] = React.useState<number | null>(null) + + // 개별 협력업체별 로딩 상태를 관리하는 맵 + const [loadingVendors, setLoadingVendors] = React.useState<Record<string, boolean>>({}) + + const router = useRouter() + + // 코멘트 관련 상태 + const [initialComments, setInitialComments] = React.useState<CbeComment[]>([]) + const [commentSheetOpen, setCommentSheetOpen] = React.useState(false) + const [selectedRfqIdForComments, setSelectedRfqIdForComments] = React.useState<number | null>(null) + + // 상업 응답 관련 상태 + const [commercialResponseSheetOpen, setCommercialResponseSheetOpen] = React.useState(false) + const [selectedResponseId, setSelectedResponseId] = React.useState<number | null>(null) + const [selectedRfq, setSelectedRfq] = React.useState<VendorWithCbeFields | null>(null) + + // RFQ 상세 관련 상태 + const [rfqDetailDialogOpen, setRfqDetailDialogOpen] = React.useState(false) + const [selectedRfqId, setSelectedRfqId] = React.useState<number | null>(null) + const [selectedRfqDetail, setSelectedRfqDetail] = React.useState<VendorWithCbeFields | null>(null) + + React.useEffect(() => { + if (rowAction?.type === "comments") { + // rowAction가 새로 세팅된 뒤 여기서 openCommentSheet 실행 + openCommentSheet(Number(rowAction.row.original.responseId)) + } + }, [rowAction]) + + async function openCommentSheet(responseId: number) { + setInitialComments([]) + + const comments = rowAction?.row.original.comments + const rfqId = rowAction?.row.original.rfqId + + if (comments && comments.length > 0) { + const commentWithAttachments: CbeComment[] = await Promise.all( + comments.map(async (c) => { + // 서버 액션을 사용하여 코멘트 첨부 파일 가져오기 + const attachments = await fetchRfqAttachmentsbyCommentId(c.id) + + return { + ...c, + commentedBy: userId, // DB나 API 응답에 있다고 가정 + attachments, + } + }) + ) + + setInitialComments(commentWithAttachments) + } + + if(rfqId) { + setSelectedRfqIdForComments(rfqId) + } + setSelectedCbeId(responseId) + setCommentSheetOpen(true) + } + + // 상업 응답 시트 열기 + function openCommercialResponseSheet(responseId: number, rfq: VendorWithCbeFields) { + setSelectedResponseId(responseId) + setSelectedRfq(rfq) + setCommercialResponseSheetOpen(true) + } + + // RFQ 상세 대화상자 열기 + function openRfqDetailDialog(rfqId: number, rfq: VendorWithCbeFields) { + setSelectedRfqId(rfqId) + setSelectedRfqDetail(rfq) + setRfqDetailDialogOpen(true) + } + + const handleDownloadCbeFiles = React.useCallback( + async (vendorId: number, rfqId: number) => { + // 고유 키 생성: vendorId_rfqId + const rowKey = `${vendorId}_${rfqId}` + + // 해당 협력업체의 로딩 상태만 true로 설정 + setLoadingVendors(prev => ({ + ...prev, + [rowKey]: true + })) + + try { + const { files, error } = await fetchCbeFiles(vendorId, rfqId); + if (error) { + toast.error(error); + return; + } + if (files.length === 0) { + toast.warning("다운로드할 CBE 파일이 없습니다"); + return; + } + // 순차적으로 파일 다운로드 + for (const file of files) { + await downloadFile(file.id); + } + toast.success(`${files.length}개의 CBE 파일이 다운로드되었습니다`); + } catch (error) { + toast.error("CBE 파일을 다운로드하는 데 실패했습니다"); + console.error(error); + } finally { + // 해당 협력업체의 로딩 상태만 false로 되돌림 + setLoadingVendors(prev => ({ + ...prev, + [rowKey]: false + })) + } + }, + [] + ); + + const downloadFile = React.useCallback(async (fileId: number) => { + try { + const { file, error } = await getFileFromRfqAttachmentsbyid(fileId); + if (error || !file) { + throw new Error(error || "파일 정보를 가져오는 데 실패했습니다"); + } + + const link = document.createElement("a"); + link.href = `/api/rfq-download?path=${encodeURIComponent(file.filePath)}`; + link.download = file.fileName; + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + + return true; + } catch (error) { + console.error(error); + return false; + } + }, []); + + // 응답 성공 후 데이터 갱신 + const handleResponseSuccess = React.useCallback(() => { + // 필요한 경우 데이터 다시 가져오기 + router.refresh() + }, [router]); + + // getColumns() 호출 시 필요한 핸들러들 주입 + const columns = React.useMemo( + () => getColumns({ + setRowAction, + router, + openCommentSheet, + handleDownloadCbeFiles, + loadingVendors, + openVendorContactsDialog: openRfqDetailDialog, + openCommercialResponseSheet, + }), + [ + setRowAction, + router, + openCommentSheet, + handleDownloadCbeFiles, + loadingVendors, + openRfqDetailDialog, + openCommercialResponseSheet + ] + ); + + // 필터 필드 정의 + const filterFields: DataTableFilterField<VendorWithCbeFields>[] = [] + const advancedFilterFields: DataTableAdvancedFilterField<VendorWithCbeFields>[] = [ + + ] + + const { table } = useDataTable({ + data, + columns, + pageCount, + filterFields, + enablePinning: true, + enableAdvancedFilter: true, + initialState: { + sorting: [{ id: "respondedAt", desc: true }], + columnPinning: { right: ["respond", "comments"] }, // respond 컬럼을 오른쪽에 고정 + }, + getRowId: (originalRow) => String(originalRow.responseId), + shallow: false, + clearOnDefault: true, + }) + + return ( + <> + <DataTable table={table}> + <DataTableAdvancedToolbar + table={table} + filterFields={advancedFilterFields} + shallow={false} + /> + </DataTable> + + {/* 코멘트 시트 */} + {commentSheetOpen && selectedRfqIdForComments && selectedCbeId !== null && ( + <CommentSheet + open={commentSheetOpen} + onOpenChange={setCommentSheetOpen} + rfqId={selectedRfqIdForComments} + initialComments={initialComments} + vendorId={userVendorId || 0} + currentUserId={userId || 0} + cbeId={selectedCbeId} + /> + )} + + {/* 상업 응답 시트 */} + {commercialResponseSheetOpen && selectedResponseId !== null && selectedRfq && ( + <CommercialResponseSheet + open={commercialResponseSheetOpen} + onOpenChange={setCommercialResponseSheetOpen} + responseId={selectedResponseId} + rfq={selectedRfq} + onSuccess={handleResponseSuccess} + /> + )} + + {/* RFQ 상세 대화상자 */} + {rfqDetailDialogOpen && selectedRfqId !== null && ( + <RfqDeailDialog + isOpen={rfqDetailDialogOpen} + onOpenChange={setRfqDetailDialogOpen} + rfqId={selectedRfqId} + rfq={selectedRfqDetail} + /> + )} + </> + ) +}
\ No newline at end of file |
